UNPKG

calendar-date

Version:

Immutable object to represent a calendar date with zero dependencies

348 lines (347 loc) 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CalendarDate = exports.DayOfTheWeek = void 0; const DAY_IN_SECONDS = 86400; const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; var DayOfTheWeek; (function (DayOfTheWeek) { DayOfTheWeek[DayOfTheWeek["MONDAY"] = 1] = "MONDAY"; DayOfTheWeek[DayOfTheWeek["TUESDAY"] = 2] = "TUESDAY"; DayOfTheWeek[DayOfTheWeek["WEDNESDAY"] = 3] = "WEDNESDAY"; DayOfTheWeek[DayOfTheWeek["THURSDAY"] = 4] = "THURSDAY"; DayOfTheWeek[DayOfTheWeek["FRIDAY"] = 5] = "FRIDAY"; DayOfTheWeek[DayOfTheWeek["SATURDAY"] = 6] = "SATURDAY"; DayOfTheWeek[DayOfTheWeek["SUNDAY"] = 7] = "SUNDAY"; })(DayOfTheWeek || (exports.DayOfTheWeek = DayOfTheWeek = {})); function parseIsoString(isoString) { if (!isoString.match(new RegExp(/^\d{4}-\d{2}-\d{2}$/))) { throw new Error(`CalendarDate Validation Error: Input ${isoString.toString()} is not valid, it should follow the pattern YYYY-MM-DD.`); } const split = isoString.split('-'); return { year: parseInt(split[0]), month: parseInt(split[1]), day: parseInt(split[2]), }; } class CalendarDate { /** * Customizes the default string description for instances of `CalendarDate`. */ get [Symbol.toStringTag]() { return 'CalendarDate'; } constructor(input1, input2, input3) { if (typeof input1 === 'string') { const result = parseIsoString(input1); this.year = result.year; this.month = result.month; this.day = result.day; } else if (typeof input1 === 'number' && typeof input2 === 'number' && typeof input3 === 'number') { this.year = input1; this.month = input2; this.day = input3; } else { const inputs = [input1, input2, input3].filter((input) => input !== undefined); throw new Error(`CalendarDate Validation Error: Input [ ${inputs.join(' , ')} ] of type [ ${inputs.map((input) => typeof input).join(' , ')} ] is not valid.`); } if (this.year < 0 || this.year > 9999) { throw new Error(`CalendarDate Validation Error: Input year ${this.year} is not valid. Year must be a number between 0 and 9999.`); } if (this.month < 1 || this.month > 12) { throw new Error(`CalendarDate Validation Error: Input month ${this.month} is not valid. Month must be a number between 1 and 12.`); } const maxDayOfMonth = CalendarDate.getMaxDayOfMonth(this.year, this.month); if (this.day < 1) { throw new Error(`CalendarDate Validation Error: Input day ${this.day} is not valid. Day must be a number greater than 0.`); } if (this.day > maxDayOfMonth) { throw new Error(`CalendarDate Validation Error: Input date ${this.year}-${this.month}-${this.day} is not a valid calendar date.`); } const date = new Date(`${this.toString()}T00:00:00.000Z`); this.unixTimestampInSeconds = date.getTime() / 1000; this.weekday = date.getUTCDay() === 0 ? 7 : date.getUTCDay(); Object.freeze(this); } static isLeapYear(year) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } static getMaxDayOfMonth(year, month) { return month === 2 && CalendarDate.isLeapYear(year) ? 29 : DAYS_IN_MONTH[month - 1]; } static getIntlDateTimeFormatter(timeZone) { let formatter = CalendarDate.dateTimeFormatterByTimezone.get(timeZone); if (!formatter) { formatter = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone, }); CalendarDate.dateTimeFormatterByTimezone.set(timeZone, formatter); } return formatter; } /** * returns a CalendarDate instance for the supplied Date, using UTC values */ static fromDateUTC(date) { return new CalendarDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()); } /** * returns a CalendarDate instance for the supplied Date, using local time zone values */ static fromDateLocal(date) { return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); } /** * returns a CalendarDate instance for the supplied Date, using the supplied time zone string */ static fromDateWithTimeZone(date, timeZone) { return new CalendarDate(CalendarDate.getIntlDateTimeFormatter(timeZone).format(date).padStart(10, '0')); } /** * returns a CalendarDate instance for the current UTC Date */ static nowUTC() { return CalendarDate.fromDateUTC(new Date()); } /** * returns a CalendarDate instance for the current Date using the local time zone of your environment */ static nowLocal() { return CalendarDate.fromDateLocal(new Date()); } /** * returns a CalendarDate instance for the current Date using the supplied time zone string */ static nowTimeZone(timeZone) { return CalendarDate.fromDateWithTimeZone(new Date(), timeZone); } /** * * @param isoString pattern YYYY-MM-DD */ static parse(isoString) { const { year, month, day } = parseIsoString(isoString); return new CalendarDate(year, month, day); } static max(...values) { if (!values.length) { throw new Error('CalendarDate.max Validation Error: Function max requires at least one input argument.'); } return values.reduce((maxValue, currentValue) => (currentValue > maxValue ? currentValue : maxValue), values[0]); } static min(...values) { if (!values.length) { throw new Error('CalendarDate.min Validation Error: Function min requires at least one input argument.'); } return values.reduce((minValue, currentValue) => (currentValue < minValue ? currentValue : minValue), values[0]); } /** * Returns a copy of the supplied array of CalendarDates sorted ascending */ static sortAscending(calendarDates) { return [...calendarDates].sort((a, b) => a.valueOf() - b.valueOf()); } /** * Returns a copy of the supplied array of CalendarDates sorted descending */ static sortDescending(calendarDates) { return [...calendarDates].sort((a, b) => b.valueOf() - a.valueOf()); } /** * Returns the ISO string representation yyyy-MM-dd */ toString() { return `${this.year.toString().padStart(4, '0')}-${this.month .toString() .padStart(2, '0')}-${this.day.toString().padStart(2, '0')}`; } toFormat(input, options) { if (options) { const formatter = new Intl.DateTimeFormat(input, options); return formatter.format(new Date(this.year, this.month - 1, this.day)); } else { return input .replace(/yyyy/g, this.year.toString().padStart(4, '0')) .replace(/yy/g, this.year.toString().slice(-2).padStart(2, '0')) .replace(/y/g, this.year.toString()) .replace(/MM/g, this.month.toString().padStart(2, '0')) .replace(/M/g, this.month.toString()) .replace(/dd/g, this.day.toString().padStart(2, '0')) .replace(/d/g, this.day.toString()); } } /** * Used by JSON stringify method. */ toJSON() { return this.toString(); } toDateUTC() { return new Date(this.unixTimestampInSeconds * 1000); } /** * @deprecated use toDateUTC instead. This method will be removed in the next major version. */ toDate() { return this.toDateUTC(); } toDateLocal() { return new Date(this.year, this.month - 1, this.day); } /** * Returns the unix timestamp in seconds. */ valueOf() { return this.unixTimestampInSeconds; } equals(calendarDate) { return this.valueOf() === calendarDate.valueOf(); } /** * Checks if the month and year of this CalendarDate are the same as the month and year of the supplied CalendarDate. */ isSameMonth(calendarDate) { return this.year === calendarDate.year && this.month === calendarDate.month; } /** * Checks if the year of this CalendarDate is the same as the year of the supplied CalendarDate. */ isSameYear(calendarDate) { return this.year === calendarDate.year; } isBefore(calendarDate) { return this.valueOf() < calendarDate.valueOf(); } isAfter(calendarDate) { return this.valueOf() > calendarDate.valueOf(); } isBeforeOrEqual(calendarDate) { return this.valueOf() <= calendarDate.valueOf(); } isAfterOrEqual(calendarDate) { return this.valueOf() >= calendarDate.valueOf(); } /** * Returns a new CalendarDate with the specified amount of months added. * * @param amount * @param enforceEndOfMonth If set to true the addition will never cause an overflow to the next month. */ addMonths(amount, enforceEndOfMonth = false) { const totalMonths = this.month + amount - 1; let year = this.year + Math.floor(totalMonths / 12); let month = totalMonths % 12; let day = this.day; if (month < 0) { month = (month + 12) % 12; } const maxDayOfMonth = CalendarDate.getMaxDayOfMonth(year, month + 1); if (enforceEndOfMonth) { day = Math.min(maxDayOfMonth, day); } else if (day > maxDayOfMonth) { day = day - maxDayOfMonth; year = year + Math.floor((month + 1) / 12); month = (month + 1) % 12; } return new CalendarDate(year, month + 1, day); } /** * Returns a new CalendarDate with the specified amount of days added. * Allows overflow. */ addDays(amount) { return CalendarDate.parse(new Date((this.valueOf() + DAY_IN_SECONDS * amount) * 1000).toISOString().slice(0, 10)); } getLastDayOfMonth() { return new CalendarDate(this.year, this.month, CalendarDate.getMaxDayOfMonth(this.year, this.month)); } getFirstDayOfMonth() { return new CalendarDate(this.year, this.month, 1); } isFirstDayOfMonth() { return this.day === 1; } isLastDayOfMonth() { return this.day === CalendarDate.getMaxDayOfMonth(this.year, this.month); } /** * returns the last day (sunday) of the week of this calendar date as a new calendar date object. */ getLastDayOfWeek() { return this.addDays(7 - this.weekday); } /** * returns the first day (monday) of the week of this calendar date as a new calendar date object. */ getFirstDayOfWeek() { return this.addDays(-(this.weekday - 1)); } /** * returns true if the weekday is monday(1) */ isFirstDayOfWeek() { return this.weekday === 1; } /** * returns true if the weekday is sunday(7) */ isLastDayOfWeek() { return this.weekday === 7; } /** * subtracts the input CalendarDate from this CalendarDate and returns the difference in days */ getDifferenceInDays(calendarDate, absolute) { const days = (this.valueOf() - calendarDate.valueOf()) / DAY_IN_SECONDS; return absolute ? Math.abs(days) : days; } /** * The number of the week of the year that the day is in. By definition (ISO 8601), the first week of a year contains January 4 of that year. * (The ISO-8601 week starts on Monday.) In other words, the first Thursday of a year is in week 1 of that year. (for timestamp values only). * [source: https://www.postgresql.org/docs/8.1/functions-datetime.html] */ get week() { return CalendarDate.getWeekNumber(this.year, this.month, this.day); } /** * Source: https://weeknumber.com/how-to/javascript */ static getWeekNumber(year, month, day) { const date = new Date(year, month - 1, day); date.setHours(0, 0, 0, 0); // Thursday in current week decides the year. date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7)); // January 4 is always in week 1. const week1 = new Date(date.getFullYear(), 0, 4); // Adjust to Thursday in week 1 and count number of weeks from date to week1. return (1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7)); } /** * The quarter of the year (1-4) based on the month of the date. * * Quarter breakdown: * - 1st Quarter: January to March (1-3) * - 2nd Quarter: April to June (4-6) * - 3rd Quarter: July to September (7-9) * - 4th Quarter: October to December (10-12) */ get quarter() { return Math.floor((this.month - 1) / 3) + 1; } } exports.CalendarDate = CalendarDate; /** * cache for Intl.DateTimeFormat instances * @private */ CalendarDate.dateTimeFormatterByTimezone = new Map();