Datelish.js

/**
 * @author Leandro Silva
 * @copyright 2016-2017 Leandro Silva (http://grafluxe.com)
 * @license MIT
 *
 * @classdesc Originally written to help work with calendar data, this class helps parse, format, and
 *            transform dates. It supports both Node and browser use.
 */

//jshint esversion:6, node:true

class Datelish {
    /**
     * Returns an array of month names.
     * @param   {Boolean} short=false Whether to return full month names or the 3 letter abbreviated versions.
     * @returns {Array}
     */
    static monthAsNames(short = false) {
      if (short) {
        return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
      } else {
        return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
      }
    }

    /**
     * Returns an array of days for a given month of the specified year. Including the year is requied since
     * days change due to leap year.
     * @param   {Number|String} month Expects either a number representing the current month (0-11) or
     *                                a string with the month name (as either a three letter abbreviation
     *                                or the full name in American English).
     * @param   {Number}        year  Can be formatted as either two digits or four digits.
     * @returns {Array}
     */
    static daysPerMonth(month, year) {
      let arr = [],
          i,
          len = Datelish.daysTotal(month, year);

      for (i = 1; i <= len; i++) {
        arr.push(i);
      }

      return arr;
    }

    /**
     * Returns the total numbers of days in a given month of the specified year. Including the year is requied since
     * days change due to leap year.
     * @param   {Number|String} month Either a number representing the current month (0-11) or a string
     *                                with the month name (as either a three letter abbreviation or the full
     *                                name in American English).
     * @param   {Number}        year  Can be formatted as either two digits or four digits.
     * @returns {Number}
     */
    static daysTotal(month, year) {
      if (typeof month === "string") {
        month = Datelish.monthNameToIndex(month);
      }

      if (month < 0) {
        return null;
      }

      return new Date(year, month + 1, 0).getDate();
    }

    /**
     * Returns an array of years (in four digit format) from the range inputted.
     * @param   {Number}        from A four digit year.
     * @param   {Number|String} to   A number or string. If a string is used, a range is returned with
     *                               the 'from' year plus amount in the 'to' string. For example: Assuming
     *                               the current year is 2012, both Datelish.years(Datelish.currYear, 2014)
     *                               and Datelish.years(Datelish.currYear, "2") return [2012, 2013, 2014].
     * @returns {Array}
     */
    static years(from, to) {
      let arr = [],
          i,
          diff;

      if (from < 1000 || from > 9999 || (typeof to === "number" && (to < 1000 || to > 9999))) {
        throw new Error("The years method expects a four digit year.");
      }

      if (typeof to === "string") { to = from + parseInt(to, 10); }

      diff = to - from;

      if (diff < 0) { diff = 0; }

      for (i = 0; i <= diff; i++) {
        arr.push(from + i);
      }

      return arr;
    }

    /**
     * Returns the current year (in four digit format).
     * @returns {Number}
     */
    static currYear() {
      return new Date().getFullYear();
    }

    /**
     * Returns the index (0–11) for a given month.
     * @param   {String} month The month name (as either a three letter abbreviation or the full name in American English).
     * @returns {Number}
     */
    static monthNameToIndex(month) {
      month = month.substr(0, 1).toUpperCase() + month.substr(1);

      return Datelish.monthAsNames(month.length <= 3 ? true : false).indexOf(month);
    }

    /**
     * Returns the name for a given month index (0–11).
     * @param   {Number}   index       The index to search.
     * @param   {Boolean=} short=false Whether to return full a month name or the 3 letter abbreviated version.
     * @returns {String}
     */
    static monthIndexToName(index, short = false) {
      return Datelish.monthAsNames(short)[index];
    }

    /**
     * Returns a boolean determining whether a given date is today.
     * @param   {Date} date The date to check against.
     * @returns {Boolean}
     */
    static todayIs(date) {
      let today = new Date();

      return (date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate());
    }

    /**
     * Returns a boolean determining whether a given date is between two dates.
     * @param   {Date}     date                 The date to check against.
     * @param   {Date}     startDate            The start date.
     * @param   {Date}     endDate              The end date.
     * @param   {Boolean=} includeEndDate=false Whether to include the end date.
     * @returns {Boolean}
     */
    static dateIsBetween(date, startDate, endDate, includeEndDate = false) {
        return (date >= startDate && (includeEndDate ? date <= endDate : date < endDate));
    }

    /**
     * Returns a boolean determining whether a given date is after a date.
     * @param   {Date}    date        The date to check against.
     * @param   {Date}    isAfterDate The date to compare against.
     * @returns {Boolean}
     */
    static dateIsAfter(date, isAfterDate) {
      return date > isAfterDate;
    }

    /**
     * Returns a boolean determining whether given date is before a date.
     * @param   {Date}    date         The date to check against.
     * @param   {Date}    isBeforeDate The date to compare against.
     * @returns {Boolean}
     */
    static dateIsBefore(date, isBeforeDate) {
      return date < isBeforeDate;
    }

    /**
     * Returns a date in ISO 8601 format (YYYY-MM-DDThh:mm:ssTZD).
     * @param   {Date}   date The date to check against.
     * @returns {String}
     */
    static toISO8601(date) {
      let dateStr = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
          time = `T${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`,
          tzd = date.toString().substr(-11, 3) + ":" + date.toString().substr(-8, 2);

      return (dateStr + time).replace(/(\D)(\d)(?=\D)|(\D)(\d)$/g, "$1$30$2$4") + tzd;
    }

    /**
     * Returns a date in little-endian format (DD/MM/YYYY).
     * @param   {Date}     date          The date to check against.
     * @param   {String=}  divider="/"   The character to use as a divider.
     * @param   {Boolean=} prepend0=true Whether to prepend a "0" if the value is less than the 10.
     * @returns {String}
     */
    static toLittleEndian(date, divider = "/", prepend0 = true) {
      if (prepend0) {
        return Datelish.prepend0(date.getDate()) + divider + Datelish.prepend0(date.getMonth() + 1) + divider + date.getFullYear();
      } else {
        return date.getDate() + divider + (date.getMonth() + 1) + divider + date.getFullYear();
      }
    }

    /**
     * Returns a date from a little-endian formatted string (DD/MM/YYYY).
     * @param   {String} dateStr The date to check against.
     * @returns {Date}
     */
    static fromLittleEndian(dateStr) {
      let match = dateStr.match(/\d+/g);

      if (match[2].length !== 4) {
        throw new Error("The fromLittleEndian method expects a four digit year (DD/MM/YYYY).");
      }

      return new Date(match[2], match[1] - 1, match[0]);
    }

    /**
     * Returns a date in middle-endian format (MM-DD-YYYY).
     * @param   {Date}     date          The date to check against.
     * @param   {String=}  divider="/"   The character to use as a divider.
     * @param   {Boolean=} prepend0=true Whether to prepend a "0" if the value is less than the 10.
     * @returns {String}
     */
    static toMiddleEndian(date, divider = "/", prepend0 = true) {
      if (prepend0) {
        return Datelish.prepend0(date.getMonth() + 1) + divider + Datelish.prepend0(date.getDate()) + divider + date.getFullYear();
      } else {
        return (date.getMonth() + 1) + divider + date.getDate() + divider + date.getFullYear();
      }
    }

    /**
     * Returns a date from a middle-endian formatted string (MM/DD/YYYY).
     * @param   {String} dateStr The date to check against.
     * @returns {Date}
     */
    static fromMiddleEndian(dateStr) {
      let match = dateStr.match(/\d+/g);

      if (match[2].length !== 4) {
        throw new Error("The fromMiddleEndian method expects a four digit year (MM/DD/YYYY).");
      }

      return new Date(match[2], match[0] - 1, match[1]);
    }

    /**
     * Returns a date in big-endian format (YYYY/MM/DD).
     * @param   {Date}     date          The date to check against.
     * @param   {String=}  divider="/"   The character to use as a divider.
     * @param   {Boolean=} prepend0=true Whether to prepend a "0" if the value is less than the 10.
     * @returns {String}
     */
    static toBigEndian(date, divider = "/", prepend0 = true) {
      if (prepend0) {
        return date.getFullYear() + divider + Datelish.prepend0(date.getMonth() + 1) + divider + Datelish.prepend0(date.getDate());
      } else {
        return date.getFullYear() + divider + (date.getMonth() + 1) + divider + date.getDate();
      }
    }

    /**
     * Returns a date from a big-endian formatted string (YYYY/MM/DD).
     * @param   {String} dateStr The date to check against.
     * @returns {Date}
     */
    static fromBigEndian(dateStr) {
      let match = dateStr.match(/\d+/g);

      if (match[0].length !== 4) {
        throw new Error("The fromBigEndian method expects a four digit year (YYYY/MM/DD).");
      }

      return new Date(match[0], match[1] - 1, match[2]);
    }

    /**
     * Converts seconds to minutes.
     * @param   {Number}   sec           Seconds.
     * @param   {Boolean=} prepend0=true Whether to prepend a "0" if the value is less than the 10.
     * @returns {String}
     */
    static toMinutes(sec, prepend0 = true) {
      let mins = String(sec / 60),
          secs = String(sec % 60);

      if (mins.indexOf(".") > -1) {
        mins = mins.substr(0, mins.indexOf("."));
      }

      return (prepend0 && mins.length < 2 ? "0" + mins : mins) + ":" + (secs.length < 2 ? "0" + secs : secs);
    }

    /**
     * Returns the today including the current time.
     * @returns {Date}
     */
    static now() {
      return new Date();
    }

    /**
     * Returns today.
     * @returns {Date}
     */
    static today() {
      let now = new Date();

      return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    }

    /**
     * Returns tomorrow.
     * @returns {Date}
     */
    static tomorrow() {
      return Datelish.daysFromDate(Datelish.today(), 1);
    }

    /**
     * Returns yesterday.
     * @returns {Date}
     */
    static yesterday() {
      return Datelish.daysFromDate(Datelish.today(), -1);
    }

    /**
     * Returns a number of days before/after a given date.
     * @param   {Date}   date The date to check against.
     * @param   {Number} n    The number of days to advance/retract. Use a negative number to go back days.
     * @returns {Date}
     */
    static daysFromDate(date, n) {
      return new Date(date.getFullYear(), date.getMonth(), date.getDate() + n);
    }

    /**
     * Returns the previous month.
     * @returns {Date}
     */
    static prevMonth() {
      let today = Datelish.today();

      return new Date(today.getFullYear(), today.getMonth() - 1, 1);
    }

    /**
     * Returns the next month.
     * @returns {Date}
     */
    static nextMonth() {
      let today = Datelish.today();

      return new Date(today.getFullYear(), today.getMonth(), Datelish.daysTotal(today.getMonth(), today.getFullYear()) + 1);
    }

    /**
     * Returns a string with a prepended "0" (if the value is less than 10).
     * @param   {Number|String} val The string/number to check against.
     * @returns {String}
     */
    static prepend0(val) {
      if (String(val).indexOf(".") > -1 || isNaN(val)) {
        throw new Error("The prepend0 method expects a whole number.");
      }

      val = Number(val);

      return (val < 10 ? "0" + val : val);
    }

    /**
     * Returns the number of days between two dates.
     * @param   {Date}   startDate The start date.
     * @param   {Date}   endDate   The end date.
     * @returns {Number}
     */
    static dayCount(startDate, endDate) {
      let days = ((((endDate.getTime() - startDate.getTime()) / 1000) / 60) / 60) / 24;

      return days + (startDate.getTimezoneOffset() > endDate.getTimezoneOffset() ? 1 : 0);
    }

    /**
     * Returns an array of months from the given dates.
     * @param   {Date}  startDate The start date.
     * @param   {Date}  endDate   The end date.
     * @returns {Array}
     */
    static monthsFrom(startDate, endDate) {
      let yrs = (endDate.getFullYear() - startDate.getFullYear()) * 12,
          mth = (endDate.getMonth() - startDate.getMonth()),
          dates = [],
          i,
          len = yrs + mth + 1;

      for (i = 1; i < len; i++) {
        dates.push(new Date(startDate.getFullYear(), startDate.getMonth() + i, 1));
      }

      return dates;
    }

}

//Support CJS/Node
if (typeof module === "object" && module.exports) {
  module.exports = Datelish;
}