Reference Source

coral-spectrum/coral-datetime/src/scripts/DateTime.js

/**
 * Copyright 2019 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

// todo add tests

// Used to store DateTimeFormat
const dateTimeFormats = {};

// Default supported format
const DEFAULT_FORMAT = 'YYYY-MM-DD';

const transform2digit = (value) => {
  const s = value.toString();
  return s.length === 1 ? `0${s}` : s;
};

// Default locale
let globalLocale = document.documentElement.lang || window.navigator.language || 'en-US';

// Uses Intl.DateTimeFormat to return a formatted date string
const formatDate = function (date, locale, options) {
  let formattedDateString = '';
  try {
    const key = `${JSON.stringify(locale)}${JSON.stringify(options)}`;
    const dateTimeFormat = dateTimeFormats[key];

    // Use existing DateTimeFormat or create new one
    if (!dateTimeFormat) {
      dateTimeFormats[key] = new window.Intl.DateTimeFormat(locale, options);
    }

    // Format to string
    formattedDateString = dateTimeFormats[key].format(date);
  } catch (e) {
    console.warn(e.message);
  }

  return formattedDateString;
};

/**
 The DateTime API is used as fallback to {@link momentJS}.

 @param {DateTime|Date|Array<Number>|String} value
 The initial date value. If none provided, the current day is used instead.
 */
class DateTime {
  /**
   @see https://momentjs.com/docs/#/parsing/now/
   */
  constructor(value) {
    if (value instanceof this.constructor) {
      // Copy properties
      this._locale = value._locale;
      this._value = value._value;
      this._date = value._date;
    } else {
      this._locale = globalLocale;
      this._value = value;

      // Support Array
      if (Array.isArray(value)) {
        this._date = value.length ? new Date(value[0], value[1] || 0, value[2] || 1) : new Date();
      } else if (typeof value === 'string') {
        const isTime = value.indexOf(':') === 2;

        // For time, we only need to set hours and minutes using current date
        if (isTime) {
          const time = value.split(':');
          const hours = parseInt(time[0], 10);
          const minutes = parseInt(time[1], 10);

          if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
            this._date = new Date();
            this._date.setHours(time[0]);
            this._date.setMinutes(time[1]);
          } else {
            this._date = new Date('Invalid Date');
          }
        } else {
          // If string is invalid, the date will be invalid too
          // "replace" fixes the one day off issue
          this._date = new Date(this._value.replace(/-/g, '/').replace(/T.+/, ''));
        }
      } else if (this._value === null) {
        this._date = new Date('Invalid Date');
      } else {
        // Create a Date instance from the value or use current day if value is missing
        this._date = this._value ? new Date(this._value) : new Date();
      }
    }
  }

  /**
   @see https://momentjs.com/docs/#/i18n/instance-locale/
   */
  locale(value) {
    if (value) {
      this._locale = value;
    }

    return this._locale;
  }

  /**
   @see https://momentjs.com/docs/#/displaying/as-javascript-date/
   */
  toDate() {
    return this._date;
  }

  /**
   @see https://momentjs.com/docs/#/parsing/moment-clone/
   */
  clone() {
    const clone = new this.constructor(this._value);
    clone._date = this._date;
    return clone;
  }

  /**
   @see https://momentjs.com/docs/#/displaying/format/
   */
  format(format) {
    let formattedDateString = '';

    if (!format) {
      format = DEFAULT_FORMAT;
    }

    if (format === DEFAULT_FORMAT) {
      formattedDateString += this._date.getFullYear();
      formattedDateString += '-';
      formattedDateString += transform2digit(this._date.getMonth() + 1);
      formattedDateString += '-';
      formattedDateString += transform2digit(this._date.getDate());
    } else if (format === 'MMMM YYYY') {
      formattedDateString += formatDate(this._date, this._locale, {month: 'long'});
      formattedDateString += ' ';
      formattedDateString += this._date.getFullYear();
    } else if (format === 'LL') {
      formattedDateString += formatDate(this._date, this._locale, {
        month: 'long',
        year: 'numeric',
        day: '2-digit'
      });
    } else if (format === 'dd') {
      formattedDateString += formatDate(this._date, this._locale, {weekday: 'short'});
    } else if (format === 'dddd') {
      formattedDateString += formatDate(this._date, this._locale, {weekday: 'long'});
    } else if (format === 'HH:mm') {
      formattedDateString += transform2digit(this._date.getHours());
      formattedDateString += ':';
      formattedDateString += transform2digit(this._date.getMinutes());
    } else if (format === 'HH') {
      formattedDateString += transform2digit(this._date.getHours());
    } else if (format === 'mm') {
      formattedDateString += transform2digit(this._date.getMinutes());
    } else if (format === 'YYYY-MM-DD[T]HH:mmZ') {
      formattedDateString += this._date.getFullYear();
      formattedDateString += '-';
      formattedDateString += transform2digit(this._date.getMonth() + 1);
      formattedDateString += '-';
      formattedDateString += transform2digit(this._date.getDate());

      formattedDateString += 'T';

      formattedDateString += transform2digit(this._date.getHours());
      formattedDateString += ':';
      formattedDateString += transform2digit(this._date.getMinutes());

      const timezone = -1 * (this._date.getTimezoneOffset() / 60);
      let abs = Math.abs(timezone);
      abs = abs < 10 ? `0${abs}` : abs.toString();

      formattedDateString += timezone < 0 ? `-${abs}:00` : `+${abs}:00`;
    } else {
      format = typeof format === 'object' ? format : {};
      formattedDateString = formatDate(this._date, this._locale, format);
    }

    return formattedDateString;
  }

  /**
   @see https://momentjs.com/docs/#/get-set/year/
   */
  year() {
    return this._date.getFullYear();
  }

  /**
   @see https://momentjs.com/docs/#/get-set/month/
   */
  month() {
    return this._date.getMonth();
  }

  /**
   @see https://momentjs.com/docs/#/get-set/week/
   */
  week() {
    // Source : https://stackoverflow.com/questions/6117814/get-week-of-year-in-javascript-like-in-php
    const date = new Date(Date.UTC(this._date.getFullYear(), this._date.getMonth(), this._date.getDate()));
    const dayNum = date.getUTCDay() || 7;
    date.setUTCDate(date.getUTCDate() + 4 - dayNum);
    const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
    return Math.ceil(((date - yearStart) / 86400000 + 1) / 7);
  }

  /**
   @see https://momentjs.com/docs/#/get-set/day/
   */
  day(day) {
    if (typeof day === 'number') {
      this._date.setDate(this._date.getDate() - (this._date.getDay() || 7) + day);

      return this;
    }

    return this._date.getDay();
  }

  /**
   @see https://momentjs.com/docs/#/get-set/hour/
   */
  hours(hours) {
    if (typeof hours === 'number') {
      this._date.setHours(hours);

      return this;
    }

    return this._date.getHours();
  }

  /**
   @see https://momentjs.com/docs/#/get-set/minute/
   */
  minutes(minutes) {
    if (typeof minutes === 'number') {
      this._date.setMinutes(minutes);

      return this;
    }

    return this._date.getMinutes();
  }

  /**
   @see https://momentjs.com/docs/#/get-set/date/
   */
  date() {
    return this._date.getDate();
  }

  /**
   @see https://momentjs.com/docs/#/manipulating/add/
   */
  add(value, type) {
    let multiplier = 1;
    switch (type) {
      case 'year':
      case 'years':
        multiplier = 12;
      case 'month':
      case 'months':
        const dayOfMonth = this._date.getDate();
        this._date.setMonth(this._date.getMonth() + multiplier * value);
        if (this._date.getDate() != dayOfMonth) {
          this._date.setDate(0);
        }
        break;
      case 'week':
      case 'weeks':
        multiplier = 7;
      case 'day':
      case 'days':
        this._date.setDate(this._date.getDate() + multiplier * value);
        break;
    }

    return this;
  }

  /**
   @see https://momentjs.com/docs/#/manipulating/subtract/
   */
  subtract(value, type) {
    let multiplier = 1;
    switch (type) {
      case 'year':
      case 'years':
        multiplier = 12;
      case 'month':
      case 'months':
        const dayOfMonth = this._date.getDate();
        this._date.setMonth(this._date.getMonth() - multiplier * value);
        if (this._date.getDate() != dayOfMonth) {
          this._date.setDate(0);
        }
        break;
      case 'week':
      case 'weeks':
        multiplier = 7;
      case 'day':
      case 'days':
        this._date.setDate(this._date.getDate() - multiplier * value);
        break;
    }

    return this;
  }


  /**
   @see https://momentjs.com/docs/#/displaying/days-in-month/
  */
  daysInMonth() {
    return new Date(this._date.getFullYear(), this._date.getMonth() + 1, 0).getDate();
  }

  /**
   @see https://momentjs.com/docs/#/displaying/difference/
   */
  diff(obj) {
    let diff = this._date.getTime() - obj._date.getTime();

    let timezoneDiff = this._date.getTimezoneOffset() - obj._date.getTimezoneOffset();
    if (timezoneDiff !== 0) {
      diff -= timezoneDiff * 60000;
    }

    return diff / 86400000;
  }

  /**
   @see https://momentjs.com/docs/#/manipulating/start-of/
   */
  startOf(value) {
    if (value === 'day') {
      // Today
      this._date = new Date(this._date.getFullYear(), this._date.getMonth(), this._date.getDate());
    } else if (value === 'month') {
      this._date = new Date(this._date.getFullYear(), this._date.getMonth(), 1);
    } else if (value === 'year') {
      this._date = new Date(new Date().getFullYear(), 0, 1);
    }

    return this;
  }

  /**
   @see https://momentjs.com/docs/#/query/is-before/
   */
  isBefore(coralDate, unit) {
    if (coralDate && coralDate._date) {
      return unit ? coralDate[unit]() > this[unit]() : coralDate._date > this._date;
    }

    return false;
  }

  /**
   @see https://momentjs.com/docs/#/query/is-after/
   */
  isAfter(coralDate, unit) {
    if (coralDate && coralDate._date) {
      return unit ? coralDate[unit]() < this[unit]() : coralDate._date < this._date;
    }

    return false;
  }

  /**
   @see https://momentjs.com/docs/#/query/is-same/
   */
  isSame(obj, type) {
    if (type === 'hour') {
      return obj && obj.clone()._date.getHours() === this.clone()._date.getHours();
    } else if (type === 'minute') {
      return obj && obj.clone()._date.getMinutes() === this.clone()._date.getMinutes();
    } else if (type === 'day') {
      return obj && obj.clone().startOf('day')._date.getTime() === this.clone().startOf('day')._date.getTime();
    }

    return obj && obj.clone()._date.getTime() === this.clone()._date.getTime();
  }

  /**
   @see https://momentjs.com/docs/#/parsing/is-valid/
   */
  isValid() {
    return this._date.toString() !== 'Invalid Date';
  }

  /**
   @ignore
   Not supported so we return an empty object
   */
  static localeData() {
    return {};
  }

  /**
   @see https://momentjs.com/docs/#/i18n/changing-locale/
   */
  static locale(value) {
    if (value) {
      globalLocale = value;
    }

    return globalLocale;
  }

  /**
   @see https://momentjs.com/docs/#/query/is-a-moment/
   */
  static isMoment(obj) {
    return obj instanceof this;
  }

  /**
   @return {momentJS|DateTime}
   */
  static get Moment() {
    return window.moment || this;
  }
}

export default DateTime;