Ontica/Empiria.Land.Intranet

View on GitHub
src/app/core/data-types/date-string-library.ts

Summary

Maintainability
C
1 day
Test Coverage
/**
 * @license
 * Copyright (c) La Vía Óntica SC, Ontica LLC and contributors. All rights reserved.
 *
 * See LICENSE.txt in the project root for complete license information.
 */

import { DateFormat, LocalizationLibrary, DEFAULT_LANGUAGE, Language } from '../localization';

import { Assertion } from '../general/assertion';

import * as moment from 'moment';


export type DateString = Date | string;

export const MINUTES_IN_HOUR = 60;

export const MINUTES_IN_DAY = 1440;

export class DateStringLibrary {

  static validateDateValue(obj: any): Date {
    if (!obj) {
      return null;
    }

    let date: Date;
    if (moment.isMoment(obj)) {
      date = this.toDate(obj.toDate());
    } else {
      date = this.toDate(obj);
    }

    if (date) {
      return date;
    } else {
      return null;
    }
  }


  static compareDates(value1: DateString, value2: DateString): number {
    const date1 = this.datePart(value1);
    const date2 = this.datePart(value2);

    if (date1 && date2) {
      return date1.localeCompare(date2);
    } else if (date1 && !date2) {
      return -1;
    } else if (!date1 && date2) {
      return 1;
    } else if (!date1 && !date2) {
      return 0;
    } else {
      throw Assertion.assertNoReachThisCode('DateStringLibrary.compareDates() programming error.');
    }
  }


  static daysBetween(value1: DateString, value2: DateString): number {
    const date1 = moment(value1);
    const date2 = moment(value2);

    return date2.diff(date1, 'days');
  }


  static shortMonthName(value: number, lang: Language = DEFAULT_LANGUAGE) {
    return LocalizationLibrary.shortMonthName(value, lang);
  }


  static today(): DateString {
    return this.mapDateStringFromMoment(moment());
  }


  static getFirstDayOfMonthFromDateString(value: DateString): DateString {
    if (!this.isDate(value)) {
      return '';
    }

    const date = this.toDate(value);
    const firstDate = moment().date(1).month(date.getMonth()).year(date.getFullYear());
    return this.mapDateStringFromMoment(firstDate);
  }


  static todayAddDays(days: number): DateString {
    const date = moment().add(days, 'days');
    return this.mapDateStringFromMoment(date);
  }


  static mapDateStringFromMoment(date: moment.Moment): DateString {
    return date.format('YYYY-MM-DD');
  }


  static datePart(value: DateString): string {
    const date = this.toDate(value);

    if (!date) {
      return '';
    }

    return `${date.getFullYear()}-` +
           `${this.padZeros(date.getMonth() + 1)}-` +
           `${this.padZeros(date.getDate())}`;
  }


  static dateTimePart(value: DateString): string {
    const date = this.toDate(value);

    if (!date) {
      return '';
    }

    return this.datePart(value) +
           `T:${this.padZeros(date.getHours())}:` +
           `${this.padZeros(date.getMinutes())}:${this.padZeros(date.getSeconds())}` +
           `:00Z`;
  }


  static isDate(dateString: DateString): boolean {
    return (!!this.toDate(dateString));
  }


  static isLeapYear(year: number): boolean {
    const divisibleBy4 = ((year % 4) === 0);
    const divisibleBy100 = ((year % 100) === 0);
    const divisibleBy400 = ((year % 400) === 0);

    if (divisibleBy4 && (!divisibleBy100 || divisibleBy400)) {
      return true;
    }
    return false;
  }


  static format(value: DateString, returnedFormat: DateFormat = 'DMY'): string {
    const date = this.toDate(value);

    if (!date) {
      return null;
    }

    const day = this.padZeros(date.getDate());
    const month = LocalizationLibrary.shortMonthName(date.getMonth());
    const year = date.getFullYear();

    switch (returnedFormat) {
      case 'DM':
        return `${day}/${month}`;

      case 'DM HH:mm':
        return `${day}/${month} ${this.militaryTimeFormat(value)}`;

      case 'DMY':
        return `${day}/${month}/${year}`;

      case 'DMY HH:mm':
        return `${day}/${month}/${year} ${this.militaryTimeFormat(value)}`;

      case 'YMD':
        return `${year}/${month}/${day}`;

      case 'MDY':
        return `${month}/${day}/${year}`;

      default:
        Assertion.assertNoReachThisCode(`Unrecognized format ${returnedFormat}.`);
    }
  }


  static toDate(value: DateString): Date {
    if (!value) {
      return null;
    }

    if (value instanceof Date) {
      return this.tryParse(value.toISOString());
    }

    if (typeof value === 'string') {
      return this.tryParse(value as string);
    }

    return null;
  }


  static yearMonth(dateString: DateString): string {
    const date = this.toDate(dateString);

    return date.getFullYear() + '-' + this.padZeros(date.getMonth() + 1);
  }


  static militaryTimeFormat(value: DateString) {
    if (value instanceof Date || typeof value === 'string' && value.includes('T')) {
      const time = typeof value === 'string' ? new Date(value) : value;

      const hour = this.padZeros(time.getHours());
      const minute = this.padZeros(time.getMinutes());

      return `${hour}:${minute}`;
    }

    return '00:00';
  }


  static addTimes(time0, time1) {
    const min0 = this.timeSpanToMins(time0 ?? '00:00:00:00');
    const min1 = this.timeSpanToMins(time1 ?? '00:00:00:00');

    return this.timeFromMins(min0 + min1);
  }


  // private methods


  private static timeSpanToMins(time: string) { // format 01:12:30:00 = 1 day 12 hours 30 minutes 00 seconds
    const b = time.split(':');

    const min = parseInt(b[0]) * MINUTES_IN_DAY + parseInt(b[1]) * MINUTES_IN_HOUR + parseInt(b[2]);

    return min;
  }


  private static timeFromMins(minutes: number) {
    const day = Math.floor(minutes / MINUTES_IN_DAY) || 0;
    const hour = Math.floor((minutes % MINUTES_IN_DAY) / MINUTES_IN_HOUR) || 0;
    const min = Math.floor(minutes % MINUTES_IN_HOUR);
    const seg = 0;

    return `${this.padZeros(day)}:${this.padZeros(hour)}:${this.padZeros(min)}:${this.padZeros(seg)}`;
  }


  private static getYearAsString(year: number): string {
    if (0 <= year && year <= 40) {
      return (year + 2000).toString();

    } else if (40 < year && year <= 100) {
      return (year + 1900).toString();

    } else if (1900 <= year && year <= 2078) {
      return year.toString();

    } else {
      return null;
    }
  }


  private static isValidDate(year: number, month: number, dayOfMonth: number): boolean {
    const monthsWith30Days = [3, 5, 8, 10];
    const monthsWith31Days = [0, 2, 4, 6, 7, 9, 11];

    if (!(1900 <= year && year <= 2078)) {
      return false;
    }

    if (!(0 <= month && month <= 11)) {
      return false;
    }

    if (monthsWith30Days.includes(month)) {
      return (1 <= dayOfMonth && dayOfMonth <= 30);
    }

    if (monthsWith31Days.includes(month)) {
      return (1 <= dayOfMonth && dayOfMonth <= 31);
    }

    if (this.isLeapYear(year)) {
      return (1 <= dayOfMonth && dayOfMonth <= 29);
    } else {
      return (1 <= dayOfMonth && dayOfMonth <= 28);
    }
  }


  private static isNumericValue(value: string): boolean {
    if (isNaN(Number(value))) {
      return false;
    }
    return true;
  }


  private static padZeros(value: number): string {
    const temp = String(value);

    return temp.padStart(2, '0');
  }


  private static tryParse(value: string): Date {
    const dateParts: string[] = this.tryToConvertToDatePartsArray(value);

    if (!dateParts) {
      return null;
    }

    let monthIndex = dateParts.findIndex(x => LocalizationLibrary.findMonth(x) !== -1);
    if (monthIndex !== -1) {
      dateParts[monthIndex] = LocalizationLibrary.findMonth(dateParts[monthIndex]).toString();
    } else {
      monthIndex = this.tryToDeterminateMonthIndex(dateParts);
      if (monthIndex === -1) {
        return null;
      }
      dateParts[monthIndex] = (+dateParts[monthIndex] - 1).toString();
    }

    const yearIndex = this.getArrayIndexWithYear(dateParts);
    dateParts[yearIndex] = this.getYearAsString(+dateParts[yearIndex]);


    let dayIndex = dateParts.findIndex(x => x && x.includes('T'));
    if (dayIndex !== -1) {
      dateParts[dayIndex] = dateParts[dayIndex].substr(0, 2);
    } else {
      dayIndex = this.tryToDeterminateDayIndex(yearIndex, monthIndex);
      if (dayIndex === -1) {
        return null;
      }
    }

    if (!this.isValidDate(+dateParts[yearIndex], +dateParts[monthIndex], +dateParts[dayIndex])) {
      return null;
    }

    return this.tryToGetParsedDate(dateParts, yearIndex, monthIndex, dayIndex);
  }


  private static getArrayIndexWithYear(dateParts: string[]): number {
    let yearIndex = dateParts.findIndex(x => this.isNumericValue(x) &&
      (1900 <= +x && +x <= 2078) || x.length === 4);
    if (yearIndex === -1) {
      yearIndex = 2;
    }
    return yearIndex;
  }


  private static isNumericMonth(value: string): boolean {
    return this.isNumericValue(value) && (1 <= +value && +value <= 12);
  }


  private static tryToConvertToDatePartsArray(value: string): string[] {
    if (!value) {
      return null;
    }

    const dateParts = value.replace(new RegExp('-', 'g'), '/').split('/');

    if (dateParts.length === 3) {
      return dateParts;
    } else {
      return null;
    }
  }


  private static tryToDeterminateDayIndex(yearIndex: number, monthIndex: number): number {
    if (yearIndex === 2 && monthIndex === 1) {
      return 0;
    } else if (yearIndex === 2 && monthIndex === 0) {
      return 1;
    } else if (yearIndex === 0 && monthIndex === 1) {
      return 2;
    } else {
      return -1;
    }
  }


  private static tryToDeterminateMonthIndex(dateParts: string[]): number {
    if (this.isNumericMonth(dateParts[1])) {
      return 1;
    } else if (this.isNumericMonth(dateParts[0])) {
      return 0;
    } else {
      return -1;
    }
  }


  private static tryToGetParsedDate(dateParts: string[], yearIndex: number,
                                    monthIndex: number, dayIndex: number): Date {
    const parsedDate = moment(`${+dateParts[yearIndex]}-` +
      `${this.padZeros(+dateParts[monthIndex] + 1)}-` +
      `${this.padZeros(+dateParts[dayIndex])}`).toDate();

    if (parsedDate && !isNaN(parsedDate.getFullYear())) {
      return parsedDate;
    } else {
      return null;
    }
  }

}