datetime.js

/**
 * `Date` object creation and manipulation functions
 *
 * @module datetimejs.datetime
 */

import {DEFAULT_CONFIG} from './config';

/**
 * Clone a `Date` object
 *
 * Creates a copy of a `Date` object that points to the exact same time.
 *
 * @example
 *
 * let dt = new Date(2019, 8, 1, 14, 2);
 * let dt1 = datetime.clone(dt);
 *
 * dt.getTime() === dt1.getTime(); // true
 * dt === dt1; // false
 *
 * @param {Date} dt `Date` object to clone
 */
export function clone(dt) {
  return new Date(dt.getTime());
};

/**
 * Create a clone of a `Date` object with specified number of days added
 *
 * As the days are added, the date object is correctly adjusted for month and
 * year. For example, adding 31 days to a Date object will result in a date
 * object that is also one or two months ahead as well.
 *
 * Negative number of days can also be added in which case days are shifted
 * bakcwards.
 *
 * It is not recommended to use this method to add months or years to a date
 * object. Adding different parts of the date has different edge cases.
 *
 * @example
 *
 * let d = new Date(2019, 4, 15);
 *
 * let d1 = datetime.addDays(d, 4); // => new Date(2019, 4, 19)
 * let d2 = datetime.addDays(d, -4); // => new Date(2019, 4, 11)
 *
 * // Cross the month boundary
 * let d3 = datetime.addDays(d, 20); // => new Date(2019, 5, 4);
 *
 * @param {Date} dt The input date
 * @param {number} days The number of days to add
 */
export function addDays(dt, days) {
  dt = clone(dt);
  dt.setDate(dt.getDate() + days);
  return dt;
};

/**
 * Create a clone of a `Date` object with a specified number of months added
 *
 * As the months are added, the date object is correctly adjusted for year and
 * date. For instance, adding one month to a December date object will result
 * in year shifting by one as well.
 *
 * Negative number of months can also be added, in which case the date is
 * shifted backwards.
 *
 * **CAVEAT:** This method will have unexpected behavior when the date of the
 * `Date` object is outside the range of dates in the target month.
 *
 * @example
 *
 * let d = new Date(2019, 4, 15);
 *
 * let d1 = datetime.addMonths(d, 4); // => new Date(2019, 8, 15);
 * let d2 = datetime.addMonths(d, -2); // => new Date(2019, 2, 15);
 *
 * // Edge case demonstration (see caveat in the description)
 * let d3 = new Date(2019, 0, 31);
 * let d4 = datetime.addMonths(d3, 1); // => new Date(2019, 2, 3);
 *
 * @param {Date} dt The input date
 * @param {number} months The number of months to add
 */
export function addMonths(dt, months) {
  dt = clone(dt);
  dt.setMonth(dt.getMonth() + months);
  return dt;
};

/**
 * Create a clone of a `Date` object with a specified number of years added
 *
 * When the years are added, the date object is adjusted for possible changes
 * in the date. For example, if we are on Feb 29 of a leap year, and we add one
 * year, we end up on Mar 1 of the next year.
 *
 * Negative number of years can be added, and then the object is shifted
 * backwards.
 *
 * @example
 *
 * let d = new Date(2019, 4, 15);
 *
 * let d1 = datetime.addYears(d, 3); // => new Date(2022, 4, 15);
 * let d2 = datetime.addYears(d, -1); // => new Date(2018, 4, 15);
 *
 * @param {Date} dt The input date
 * @param {number} years The number of years to add
 */
export function addYears(dt, years) {
  dt = clone(dt);
  dt.setFullYear(dt.getFullYear() + years);
  return dt;
};

/**
 * Create a clone of a `Date` object with time shifted by the specified delta
 *
 * The delta object does not have to be complete. As long as it has a numeric
 * `delta` property, it will work.
 *
 * @example
 *
 * let d = new Date(2019, 4, 15, 12, 23);
 *
 * // Add 5 minutes
 * let dlt1 = dtdelta.createDelta(5 * 60 * 1000);
 * let dt1 = dateitme.addDelta(d, dlt1); // => new Date(2019, 4, 15, 12, 28)
 *
 * // Subtract 10 hours
 * let dlt2 = dtdelta.createDelta(10 * 60 * 60 * 1000);
 * let dt2 = datetime.addDelta(d, dlt2); // => new Date(2019, 4, 15, 2, 23)
 *
 * // We can cheat and use any object with `delta` property
 * let dlt3 = {delta: 10800000} // 3 hours
 * let dt3 = datetime.addDelta(d, dlt3); // => new Date(2019, 4, 14, 15, 23)
 *
 * @param {Date} dt The input date
 * @param {object} delta The delta object (e.g., created by `createDelta()`)
 */
export function addDelta(dt, {delta}) {
  dt = clone(dt);
  dt.setTime(dt.getTime() + delta);
  return dt;
};

/**
 * Create a clone of a `Date` object with the time reset to 0 (local time)
 *
 * **TIP:** JavaScript does not have "pure" date objects. The `Date` object,
 * despite its name, also contains the time. Furthermore, it is always in local
 * time.  Because of this, when we are dealing with date alone, this is not the
 * ideal object to use. It is recommended to create your own class for dates,
 * or use strings in a specific format.
 *
 * @example
 *
 * let d = new Date(2019, 4, 15, 12, 23);
 * let d1 = dateitme.resetTime(d); // => new Date(2019, 4, 15, 0, 0)
 *
 * @param {Date} dt The input date
 */
export function resetTime(dt) {
  dt = clone(dt);
  dt.setHours(0);
  dt.setMinutes(0);
  dt.setSeconds(0);
  dt.setMilliseconds(0);
  return dt;
};

/**
 * Create a clone of a `Date` object with date reset to the start of the week
 *
 * The first day of the week is taken from the configuration object, which
 * defaults to `DEFAULT_CONFIG`. The `WEEK_START` configuration option is a
 * number between 0 and 6, where 0 is Sunday, 1 is Monday, and so on, through 6
 * as Saturday.
 *
 * @example
 *
 * let d = new Date(2019, 4, 15);
 *
 * // Reset to previous Sunday (default)
 * let d1 = datetime.resetWeek(d); // => new Date(2019, 4, 12)
 *
 * // Use Monday as custom week start
 * let conf = {WEEK_START: 1};
 * let d2 = datetime.resetWeek(d, conf); // => new Date(2019, 4, 13)
 *
 * @param {Date} dt The input date
 * @param {object} [config=DEFAULT_CONFIG] Optional configuration object
 */
export function resetWeek(dt, config = {}) {
  config = {...DEFAULT_CONFIG, ...config};

  let firstDay = config.WEEK_START;
  let weekDay = dt.getDay();

  if (weekDay === firstDay) {
    return clone(dt);
  }

  if (weekDay > firstDay) {
    return addDays(dt, firstDay - weekDay);
  }

  return addDays(dt, firstDay - weekDay - 7);
};