calendar-date
Version:
Immutable object to represent a calendar date with zero dependencies
348 lines (347 loc) • 13.3 kB
JavaScript
"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();