nhb-toolbox
Version:
A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.
1,159 lines • 69.2 kB
JavaScript
import { isValidArray } from '../guards/non-primitives.js';
import { isString } from '../guards/primitives.js';
import { getOrdinal, roundToNearest } from '../number/utilities.js';
import { DAYS, INTERNALS, MONTHS, SORTED_TIME_FORMATS, TIME_ZONE_LABELS, } from './constants.js';
import { isLeapYear } from './guards.js';
import { extractMinutesFromUTC } from './utils.js';
/**
* * Creates a new immutable `Chronos` instance.
*
* **Note**: *If a date is provided **without a time component**, the instance will default to `00:00:00.000` UTC
* and convert it to the **equivalent local time** using the current environment's UTC offset.*
*
* @param value - A date value (`number`, `string`, `Date`, or `Chronos` object).
* - If a string is provided, it should be in a format that can be parsed by the Date constructor.
* - If a number is provided, it should be a timestamp (milliseconds since the Unix epoch).
* - If a Date object is provided, it will be used as is.
* - If a Chronos object is provided, it will be converted to a Date object.
*
* **It also accepts number values as following:**
* - **`year, month, date, hours, minutes, seconds, milliseconds`**: Individual components of a date-time to construct a `Chronos` instance.
* - **`year`**: A number representing the year. If the year is between 0 and 99, it will be assumed to be the year 1900 + the provided year.
* - **`month`**: A number between 1 and 12 representing the month (1 for January, 12 for December). It is adjusted internally to a 0-based index (0 for January, 11 for December).
* - **`date`**: A number between 1 and 31 representing the day of the month.
* - **`hours`**: A number between 0 and 23 representing the hour of the day.
* - **`minutes`**: A number between 0 and 59 representing the minutes past the hour.
* - **`seconds`**: A number between 0 and 59 representing the seconds past the minute.
* - **`milliseconds`**: A number between 0 and 999 representing the milliseconds past the second.
*
* @returns Instance of `Chronos` with all methods and properties.
*/
export class Chronos {
#date;
#offset;
#ORIGIN;
static #plugins = new Set();
/** Use `readonly and/or private` methods outside `Chronos`. Purpose: Plugin creation. */
static [INTERNALS] = {
internalDate(instance) {
return instance.#date;
},
offset(instance) {
return instance.#offset;
},
withOrigin(instance, method, label) {
return instance.#withOrigin(method, label);
},
toNewDate(instance, value) {
return instance.#toNewDate(value);
},
};
/**
* * Chronos date/time in Native JS `Date` format.
*
* - **NOTE**: It is **HIGHLY** advised *not to rely* on this public property to access native JS `Date`. It's not reliable when timezone and/or UTC related operations are performed. If you really need to use native `Date`, use `toDate` method. THis property is purely for developer convenience and sugar.
*/
native;
/** Origin of the `Chronos` instance (Method that created `new Chronos`), useful fo tracking instance. */
origin;
/**
* * Creates a new immutable `Chronos` instance.
*
* **Note**: *If a date is provided **without a time component**, the instance will default to `00:00:00.000` UTC
* and convert it to the **equivalent local time** using the current environment's UTC offset.*
*
* @param valueOrYear The value in number, string, Date or Chronos format or the full year designation is required for cross-century date accuracy. If year is between 0 and 99 is used, then year is assumed to be 1900 + year.
* @param month The month as a number between 1 and 12 (January to December).
* @param date The date as a number between 1 and 31.
* @param hours Must be supplied if minutes is supplied. A number from 0 to 23 (midnight to 11pm) that specifies the hour.
* @param minutes Must be supplied if seconds is supplied. A number from 0 to 59 that specifies the minutes.
* @param seconds Must be supplied if milliseconds is supplied. A number from 0 to 59 that specifies the seconds.
* @param ms A number from 0 to 999 that specifies the milliseconds.
*
* @returns Instance of `Chronos` with all methods and properties.
*/
constructor(valueOrYear, month, date, hours, minutes, seconds, ms) {
if (typeof valueOrYear === 'number' && typeof month === 'number') {
this.#date = new Date(valueOrYear, month - 1, date ?? 1, hours ?? 0, minutes ?? 0, seconds ?? 0, ms ?? 0);
this.native = this.#date;
}
else {
this.#date = this.#toNewDate(valueOrYear);
this.native = this.#date;
}
this.#ORIGIN = 'root';
this.origin = this.#ORIGIN;
this.#offset = `UTC${this.getUTCOffset()}`;
}
*[Symbol.iterator]() {
yield ['year', this.year];
yield ['month', this.month];
yield ['isoMonth', this.isoMonth];
yield ['date', this.date];
yield ['weekDay', this.weekDay];
yield ['isoWeekDay', this.isoWeekDay];
yield ['hour', this.hour];
yield ['minute', this.minute];
yield ['second', this.second];
yield ['millisecond', this.millisecond];
yield ['timestamp', this.timestamp];
yield ['unix', this.unix];
}
/**
* * Enables primitive coercion like `console.log`, `${chronos}`, etc.
* @param hint - The type hint provided by the JS engine.
* @returns The primitive value based on the hint.
*/
[Symbol.toPrimitive](hint) {
if (hint === 'number')
return this.valueOf();
return this.toLocalISOString();
}
[Symbol.replace](string, replacement) {
switch (this.#ORIGIN) {
case 'timeZone':
case 'toUTC':
case 'utc':
return string.replace(this.toISOString().replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, ''), replacement);
default:
return string.replace(this.toLocalISOString().replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, ''), replacement);
}
}
[Symbol.search](string) {
switch (this.#ORIGIN) {
case 'timeZone':
case 'toUTC':
case 'utc':
return string.indexOf(this.toISOString().replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, ''));
default:
return string.indexOf(this.toLocalISOString().replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, ''));
}
}
[Symbol.split](string) {
switch (this.#ORIGIN) {
case 'timeZone':
case 'toUTC':
case 'utc':
return string.split(this.toISOString().replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, ''));
default:
return string.split(this.toLocalISOString().replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, ''));
}
}
get [Symbol.toStringTag]() {
switch (this.#ORIGIN) {
case 'timeZone':
return this.toISOString().replace('Z', this.#offset.slice(3));
case 'toUTC':
case 'utc':
return this.#toLocalISOString().replace(this.getUTCOffset(), 'Z');
default:
return this.#toLocalISOString();
}
}
/**
* @private Method to create native `Date` instance from date-like data types.
* @param value The value to convert into `Date`.
* @returns Instance of native Date object.
*/
#toNewDate(value) {
const date = value instanceof Chronos ?
value.toDate()
: new Date(value ?? Date.now());
// Check if the date is invalid
if (isNaN(date.getTime())) {
throw new Error('Provided date is invalid!');
}
return date;
}
/**
* @private Method to tag origin of the `Chronos` instance.
*
* @param origin Origin of the instance, the method name from where it was created.
* @param offset Optional UTC offset in `UTC+12:00` format.
* @returns The `Chronos` instance with the specified origin.
*/
#withOrigin(origin, offset) {
const instance = new Chronos(this.#date);
instance.#ORIGIN = origin;
instance.origin = origin;
instance.native = instance.#date;
if (offset)
instance.#offset = offset;
return instance;
}
/**
* @private Formats the current `Chronos` date using the specified template.
*
* @param format - The desired date format.
* @param useUTC - Whether to use UTC or local time.
* @returns Formatted date string.
*/
#format(format, useUTC = false) {
const year = useUTC ? this.#date.getUTCFullYear() : this.#date.getFullYear();
const month = useUTC ? this.#date.getUTCMonth() : this.#date.getMonth();
const day = useUTC ? this.#date.getUTCDay() : this.#date.getDay();
const date = useUTC ? this.#date.getUTCDate() : this.#date.getDate();
const hours = useUTC ? this.#date.getUTCHours() : this.#date.getHours();
const minutes = useUTC ? this.#date.getUTCMinutes() : this.#date.getMinutes();
const seconds = useUTC ? this.#date.getUTCSeconds() : this.#date.getSeconds();
const milliseconds = useUTC ?
this.#date.getUTCMilliseconds()
: this.#date.getMilliseconds();
const timeZone = useUTC ? 'Z' : this.getTimeZoneOffset();
const dateComponents = {
YYYY: String(year),
YY: String(year).slice(-2),
yyyy: String(year),
yy: String(year).slice(-2),
M: String(month + 1),
MM: String(month + 1).padStart(2, '0'),
mmm: MONTHS[month].slice(0, 3),
mmmm: MONTHS[month],
d: DAYS[day].slice(0, 2),
dd: DAYS[day].slice(0, 3),
ddd: DAYS[day],
D: String(date),
DD: String(date).padStart(2, '0'),
Do: getOrdinal(date),
H: String(hours),
HH: String(hours).padStart(2, '0'),
h: String(hours % 12 || 12),
hh: String(hours % 12 || 12).padStart(2, '0'),
m: String(minutes),
mm: String(minutes).padStart(2, '0'),
s: String(seconds),
ss: String(seconds).padStart(2, '0'),
ms: String(milliseconds),
mss: String(milliseconds).padStart(3, '0'),
a: hours < 12 ? 'am' : 'pm',
A: hours < 12 ? 'AM' : 'PM',
ZZ: timeZone,
};
const tokenRegex = new RegExp(`^(${SORTED_TIME_FORMATS.join('|')})`);
let result = '';
let i = 0;
while (i < format.length) {
// Handle [escaped literal]
if (format[i] === '[') {
const end = format.indexOf(']', i);
if (end !== -1) {
result += format.slice(i + 1, end);
i = end + 1;
continue;
}
}
// Try to match a format token
const match = tokenRegex.exec(format.slice(i));
if (match) {
result += dateComponents[match[0]];
i += match[0].length;
}
else {
result += format[i];
i++;
}
}
return result;
}
/** @private Returns ISO string with local time zone offset */
#toLocalISOString() {
const pad = (n, p = 2) => String(n).padStart(p, '0');
return `${this.year}-${pad(this.month + 1)}-${pad(this.date)}T${pad(this.hour)}:${pad(this.minute)}:${pad(this.second)}.${pad(this.millisecond, 3)}${this.getUTCOffset()}`;
}
/**
* @private Normalizes duration values based on sign and `absolute` flag.
* @param result The raw time breakdown to normalize.
* @param absolute If true, ensures all values are positive.
* @param isFuture Whether the duration was forward (true) or backward (false).
* @returns The normalized duration object.
*/
#normalizeDuration(result, absolute, isFuture) {
const entries = Object.entries(result);
if (!absolute && !isFuture) {
for (const [key, value] of entries) {
if (value !== 0) {
result[key] = value * -1;
}
}
}
else if (absolute) {
for (const [key, value] of entries) {
result[key] = Math.abs(value);
}
}
return result;
}
/** Gets the full year of the date. */
get year() {
return this.#date.getFullYear();
}
/** Gets the month (0-11) of the date. */
get month() {
return this.#date.getMonth();
}
/** Gets the day of the month (1-31). */
get date() {
return this.#date.getDate();
}
/** Gets the day of the week (0-6, where 0 is Sunday). */
get weekDay() {
return this.#date.getDay();
}
/** Gets the hour (0-23) of the date. */
get hour() {
return this.#date.getHours();
}
/** Gets the minute (0-59) of the date. */
get minute() {
return this.#date.getMinutes();
}
/** Gets the second (0-59) of the date. */
get second() {
return this.#date.getSeconds();
}
/** Gets the millisecond (0-999) of the date. */
get millisecond() {
return this.#date.getMilliseconds();
}
/** Gets ISO weekday: 1 = Monday, 7 = Sunday */
get isoWeekDay() {
const day = this.weekDay;
return day === 0 ? 7 : day;
}
/** Gets ISO month (1–12 instead of 0–11) */
get isoMonth() {
return (this.month + 1);
}
/** Returns the Unix timestamp (seconds since the Unix epoch: January 1, 1970, UTC). */
get unix() {
return Math.floor(this.#date.getTime() / 1000);
}
/** Gets the time value in milliseconds since midnight, January 1, 1970 UTC. */
get timestamp() {
return this.#date.getTime();
}
/** * Gets the last date (number) of the current month `(28, 29, 30 or 31)`. */
get lastDateOfMonth() {
return this.lastDayOfMonth().#date.getDate();
}
/** @instance Returns a debug-friendly string for `console.log` or `util.inspect`. */
inspect() {
return `[Chronos ${this.toLocalISOString()}]`;
}
/** @instance Enables JSON.stringify and logging in the console (in Browser environment) to show readable output. */
toJSON() {
return this.toLocalISOString();
}
/** @instance Enables arithmetic and comparison operations (e.g., +new Chronos()). */
valueOf() {
return this.getTimeStamp();
}
/** @instance Clones and returns a new Chronos instance with the same date. */
clone() {
return new Chronos(this.#date).#withOrigin(this.#ORIGIN);
}
/** @instance Gets the native `Date` instance (read-only). */
toDate() {
switch (this.#ORIGIN) {
case 'toUTC':
case 'utc': {
const mins = this.getUTCOffsetMinutes();
const date = this.addMinutes(mins);
return date.toDate();
}
default:
return new Date(this.#date);
}
}
/** @instance Returns a string representation of a date. The format of the string depends on the locale. */
toString() {
switch (this.#ORIGIN) {
case 'timeZone': {
const gmt = this.#offset.replace('UTC', 'GMT').replace(':', '');
const label = TIME_ZONE_LABELS[this.#offset] ?? this.#offset;
return this.#date
.toString()
.replace(/GMT[+-]\d{4} \([^)]+\)/, `${gmt} (${label})`);
}
case 'toUTC':
case 'utc': {
const mins = this.getUTCOffsetMinutes();
const date = this.addMinutes(mins);
return date.toString();
}
default:
return this.#date.toString();
}
}
/** @instance Returns ISO string with local time zone offset */
toLocalISOString() {
switch (this.#ORIGIN) {
case 'timeZone':
case 'toUTC':
case 'utc': {
const previousOffset = this.getTimeZoneOffsetMinutes();
const currentOffset = this.getUTCOffsetMinutes();
const date = this.addMinutes(-previousOffset - currentOffset);
return date.#toLocalISOString();
}
default:
return this.#toLocalISOString();
}
}
/** @instance Returns a date as a string value in ISO format. */
toISOString() {
switch (this.#ORIGIN) {
case 'timeZone':
return this.#toLocalISOString().replace(this.getUTCOffset(), this.#offset.slice(3));
case 'toUTC':
case 'utc':
return this.#toLocalISOString().replace(this.getUTCOffset(), 'Z');
default:
return this.#date.toISOString();
}
}
/**
* @instance Wrapper over native `toLocaleString`
* @description Converts a date and time to a string by using the current or specified locale.
*
* @param locales A locale string, array of locale strings, Intl.Locale object, or array of Intl.Locale objects that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used.
* @param options An object that contains one or more properties that specify comparison options.
*/
toLocaleString(locale, options) {
return this.#date.toLocaleString(locale, options);
}
/** @instance Returns the time value in milliseconds since midnight, January 1, 1970 UTC. */
getTimeStamp() {
return this.#date.getTime();
}
/**
* @instance Formats the current date into a custom string format (local time by default).
*
* @param format - The desired format string (Default: `dd, mmm DD, YYYY HH:mm:ss` → e.g., `Sun, Apr 06, 2025 16:11:55`).
*
* - To output raw text (i.e., not interpreted as a date token), wrap it in square brackets.
* - For example, `[Today is] ddd` results in `Today is Sunday`, and `YYYY[ year]` results in `2025 year`.
*
* - Supported format tokens include: `YYYY`, `YY`, `mmmm`, `mmm`, `MM`, `M`, `DD`, `D`, `dd`, `ddd`, `Do`, `HH`, `H`, `hh`, `h`, `mm`, `m`, `ss`, `s`, `ms`, `mss`, `a`, `A`, and `ZZ`.
* - *Any token not wrapped in brackets will be parsed and replaced with its corresponding date component.*
* - Please refer to {@link https://nhb-toolbox.vercel.app/docs/classes/Chronos/format#format-tokens format tokens} for detailed usage.
*
* @param useUTC - Optional boolean to format the date using UTC time.
* When `true`, it behaves like `formatUTC()` and outputs time based on UTC offset. Defaults to `false`.
*
* @returns Formatted date string using the specified format.
* Uses local time by default unless `useUTC` is set to `true`.
*/
format(format, useUTC = false) {
return this.#format(format ?? 'dd, mmm DD, YYYY HH:mm:ss', useUTC);
}
/**
* @instance Formats the date into a predefined strict string format using local time or UTC.
*
* @remarks Offers over 21,000 predefined formats with full IntelliSense support.
*
* @param format - The desired format string. Defaults to `'dd, mmm DD, YYYY HH:mm:ss'`
* (e.g., `'Sun, Apr 06, 2025 16:11:55'`).
* - Please refer to {@link https://nhb-toolbox.vercel.app/docs/classes/Chronos/format#format-tokens format tokens} for detailed usage.
*
* @param useUTC - If `true`, formats the date in UTC (equivalent to `formatUTC()`);
* Defaults to `false` (local time).
* @returns A formatted date string in the specified format
*/
formatStrict(format, useUTC = false) {
return this.#format(format ?? 'dd, mmm DD, YYYY HH:mm:ss', useUTC);
}
/**
* @instance Formats the date into a custom string format (UTC time).
*
* @param format - The desired format (Default format is `dd, mmm DD, YYYY HH:mm:ss:mss` = `Sun, Apr 06, 2025 16:11:55:379`).
*
* - To output raw text (i.e., not interpreted as a date token), wrap it in square brackets.
* - For example, `[Today is] ddd` results in `Today is Sunday`, and `YYYY[ year]` results in `2025 year`.
*
* - Supported format tokens include: `YYYY`, `YY`, `mmmm`, `mmm`, `MM`, `M`, `DD`, `D`, `dd`, `ddd`, `Do`, `HH`, `H`, `hh`, `h`, `mm`, `m`, `ss`, `s`, `mss`, `a`, `A`, and `ZZ`.
* - *Any token not wrapped in brackets will be parsed and replaced with its corresponding date component.*
* - Please refer to {@link https://nhb-toolbox.vercel.app/docs/classes/Chronos/format#format-tokens format tokens} for detailed usage.
*
* @returns Formatted date string in desired format (UTC time).
*/
formatUTC(format = 'dd, mmm DD, YYYY HH:mm:ss:mss') {
switch (this.#offset) {
case 'UTC+00:00':
return this.#format(format, false);
default:
return this.#format(format, true);
}
}
/**
* @instance Adds seconds and returns a new immutable instance.
* @param seconds - Number of seconds to add.
* @returns A new `Chronos` instance with the updated date.
*/
addSeconds(seconds) {
const newDate = new Date(this.#date);
newDate.setSeconds(newDate.getSeconds() + seconds);
return new Chronos(newDate).#withOrigin('addSeconds');
}
/**
* @instance Adds minutes and returns a new immutable instance.
* @param minutes - Number of minutes to add.
* @returns A new `Chronos` instance with the updated date.
*/
addMinutes(minutes) {
const newDate = new Date(this.#date);
newDate.setMinutes(newDate.getMinutes() + minutes);
return new Chronos(newDate).#withOrigin('addMinutes');
}
/**
* @instance Adds hours and returns a new immutable instance.
* @param hours - Number of hours to add.
* @returns A new `Chronos` instance with the updated date.
*/
addHours(hours) {
const newDate = new Date(this.#date);
newDate.setHours(newDate.getHours() + hours);
return new Chronos(newDate).#withOrigin('addHours');
}
/**
* @instance Adds days and returns a new immutable instance.
* @param days - Number of days to add.
* @returns A new `Chronos` instance with the updated date.
*/
addDays(days) {
const newDate = new Date(this.#date);
newDate.setDate(newDate.getDate() + days);
return new Chronos(newDate).#withOrigin('addDays');
}
/**
* @instance Adds weeks and returns a new immutable instance.
* @param weeks - Number of weeks to add.
* @returns A new `Chronos` instance with the updated date.
*/
addWeeks(weeks) {
const newDate = new Date(this.#date);
newDate.setDate(newDate.getDate() + weeks * 7);
return new Chronos(newDate).#withOrigin('addWeeks');
}
/**
* @instance Adds months and returns a new immutable instance.
* @param months - Number of months to add.
* @returns A new `Chronos` instance with the updated date.
*/
addMonths(months) {
const newDate = new Date(this.#date);
newDate.setMonth(newDate.getMonth() + months);
return new Chronos(newDate).#withOrigin('addMonths');
}
/**
* @instance Adds years and returns a new immutable instance.
* @param years - Number of years to add.
* @returns A new `Chronos` instance with the updated date.
*/
addYears(years) {
const newDate = new Date(this.#date);
newDate.setFullYear(newDate.getFullYear() + years);
return new Chronos(newDate).#withOrigin('addYears');
}
/**
* @instance Checks if the current year is a leap year.
* - A year is a leap year if it is divisible by 4, but not divisible by 100, unless it is also divisible by 400.
* - For example, 2000 and 2400 are leap years, but 1900 and 2100 are not.
* @param year - Optional year to check. Default is the year from current `Chronos` instance.
* @returns `true` if the year is a leap year, `false` otherwise.
*/
isLeapYear(year) {
return isLeapYear(year ?? this.year);
}
/** @instance Checks if another date is exactly equal to this one */
isEqual(other) {
const time = other instanceof Chronos ? other : new Chronos(other);
return this.timestamp === time.timestamp;
}
/** @instance Checks if another date is exactly equal to or before this one */
isEqualOrBefore(other) {
const time = other instanceof Chronos ? other : new Chronos(other);
return this.timestamp <= time.timestamp;
}
/** @instance Checks if another date is exactly equal to or after this one */
isEqualOrAfter(other) {
const time = other instanceof Chronos ? other : new Chronos(other);
return this.timestamp >= time.timestamp;
}
/**
* @instance Checks if another date is the same as this one in a specific unit.
* @param other The other date to compare.
* @param unit The unit to compare.
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
isSame(other, unit, weekStartsOn = 0) {
const time = other instanceof Chronos ? other : new Chronos(other);
return (this.startOf(unit, weekStartsOn).toDate().getTime() ===
time.startOf(unit, weekStartsOn).toDate().getTime());
}
/**
* @instance Checks if this date is before another date in a specific unit.
* @param other The other date to compare.
* @param unit The unit to compare.
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
isBefore(other, unit, weekStartsOn = 0) {
const time = other instanceof Chronos ? other : new Chronos(other);
return (this.startOf(unit, weekStartsOn).toDate().getTime() <
time.startOf(unit, weekStartsOn).toDate().getTime());
}
/**
* @instance Checks if this date is after another date in a specific unit.
* @param other The other date to compare.
* @param unit The unit to compare.
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
isAfter(other, unit, weekStartsOn = 0) {
const time = other instanceof Chronos ? other : new Chronos(other);
return (this.startOf(unit, weekStartsOn).toDate().getTime() >
time.startOf(unit, weekStartsOn).toDate().getTime());
}
/**
* @instance Checks if this date is the same or before another date in a specific unit.
* @param other The other date to compare.
* @param unit The unit to compare.
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
isSameOrBefore(other, unit, weekStartsOn = 0) {
return (this.isSame(other, unit, weekStartsOn) ||
this.isBefore(other, unit, weekStartsOn));
}
/**
* @instance Checks if this date is the same or after another date in a specific unit.
* @param other The other date to compare.
* @param unit The unit to compare.
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
isSameOrAfter(other, unit, weekStartsOn = 0) {
return (this.isSame(other, unit, weekStartsOn) ||
this.isAfter(other, unit, weekStartsOn));
}
/**
* @instance Checks if the current date is between the given start and end dates.
*
* @param start - The start of the range.
* @param end - The end of the range.
* @param inclusive - Specifies whether the comparison is inclusive or exclusive:
* - `'[]'`: inclusive of both start and end (≥ start and ≤ end)
* - `'[)'`: inclusive of start, exclusive of end (≥ start and < end)
* - `'(]'`: exclusive of start, inclusive of end (> start and ≤ end)
* - `'()'`: exclusive of both start and end (> start and < end)
*
* @returns `true` if the current date is within the specified range based on the `inclusive` mode.
*/
isBetween(start, end, inclusive = '()') {
const s = new Chronos(start).valueOf();
const e = new Chronos(end).valueOf();
const t = this.valueOf();
switch (inclusive) {
case '[]':
return t >= s && t <= e;
case '[)':
return t >= s && t < e;
case '(]':
return t > s && t <= e;
case '()':
return t > s && t < e;
}
}
/**
* @instance Checks if the date is within daylight saving time (DST).
* @returns Whether the date is in DST.
*/
isDST() {
const year = this.#date.getFullYear();
const jan = new Date(year, 0, 1).getTimezoneOffset();
const jul = new Date(year, 6, 1).getTimezoneOffset();
return this.#date.getTimezoneOffset() < Math.max(jan, jul);
}
/** @instance Checks if current day is the first day of the current month. */
isFirstDayOfMonth() {
return this.isSame(this.firstDayOfMonth(), 'day');
}
/** @instance Checks if current day is the last day of the current month. */
isLastDayOfMonth() {
return this.isSame(this.lastDayOfMonth(), 'day');
}
/** @instance Returns a new Chronos instance set to the first day of the current month. */
firstDayOfMonth() {
const year = this.#date.getFullYear();
const month = this.#date.getMonth();
const lastDate = new Date(year, month, 1);
return new Chronos(lastDate).#withOrigin('firstDayOfMonth');
}
/** @instance Returns a new Chronos instance set to the last day of the current month. */
lastDayOfMonth() {
const year = this.#date.getFullYear();
const month = this.#date.getMonth() + 1;
const lastDate = new Date(year, month, 0);
return new Chronos(lastDate).#withOrigin('lastDayOfMonth');
}
/**
* @instance Returns a new Chronos instance at the start of a given unit.
* @param unit The unit to reset (e.g., year, month, day).
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
startOf(unit, weekStartsOn = 0) {
const d = new Date(this.#date);
switch (unit) {
case 'year':
d.setMonth(0, 1);
d.setHours(0, 0, 0, 0);
break;
case 'month':
d.setDate(1);
d.setHours(0, 0, 0, 0);
break;
case 'week': {
const day = d.getDay();
const diff = (day - weekStartsOn + 7) % 7;
d.setDate(d.getDate() - diff);
d.setHours(0, 0, 0, 0);
break;
}
case 'day':
d.setHours(0, 0, 0, 0);
break;
case 'hour':
d.setMinutes(0, 0, 0);
break;
case 'minute':
d.setSeconds(0, 0);
break;
case 'second':
d.setMilliseconds(0);
break;
case 'millisecond':
break;
}
return new Chronos(d).#withOrigin('startOf');
}
/**
* @instance Returns a new Chronos instance at the end of a given unit.
* @param unit The unit to adjust (e.g., year, month, day).
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
*/
endOf(unit, weekStartsOn = 0) {
return this.startOf(unit, weekStartsOn)
.add(1, unit)
.add(-1, 'millisecond')
.#withOrigin('endOf');
}
/**
* @instance Returns a new Chronos instance with the specified unit added.
* @param number The number of time unit to add (can be negative).
* @param unit The time unit to add.
*/
add(number, unit) {
const d = new Date(this.#date);
switch (unit) {
case 'millisecond':
d.setMilliseconds(d.getMilliseconds() + number);
break;
case 'second':
d.setSeconds(d.getSeconds() + number);
break;
case 'minute':
d.setMinutes(d.getMinutes() + number);
break;
case 'hour':
d.setHours(d.getHours() + number);
break;
case 'day':
d.setDate(d.getDate() + number);
break;
case 'week':
d.setDate(d.getDate() + number * 7);
break;
case 'month':
d.setMonth(d.getMonth() + number);
break;
case 'year':
d.setFullYear(d.getFullYear() + number);
break;
}
return new Chronos(d).#withOrigin('add');
}
/**
* @instance Returns a new Chronos instance with the specified unit subtracted.
* @param number The number of time unit to subtract (can be negative).
* @param unit The time unit to add.
*/
subtract(number, unit) {
return this.add(-number, unit).#withOrigin('subtract');
}
/**
* @instance Gets the value of a specific time unit from the date.
* @param unit The unit to retrieve.
*/
get(unit) {
switch (unit) {
case 'year':
return this.#date.getFullYear();
case 'month':
return this.#date.getMonth();
case 'day':
return this.#date.getDate();
case 'week':
return this.getWeek();
case 'hour':
return this.#date.getHours();
case 'minute':
return this.#date.getMinutes();
case 'second':
return this.#date.getSeconds();
case 'millisecond':
return this.#date.getMilliseconds();
}
}
/**
* @instance Returns a new Chronos instance with the specified unit set to the given value.
* @param unit The unit to modify.
* @param value The value to set for the unit.
*/
set(unit, value) {
const d = new Date(this.#date);
switch (unit) {
case 'year':
d.setFullYear(value);
break;
case 'month':
d.setMonth(value);
break;
case 'day':
d.setDate(value);
break;
case 'week':
return this.setWeek(value);
case 'hour':
d.setHours(value);
break;
case 'minute':
d.setMinutes(value);
break;
case 'second':
d.setSeconds(value);
break;
case 'millisecond':
d.setMilliseconds(value);
break;
}
return new Chronos(d).#withOrigin('set');
}
/**
* @instance Returns the difference between this and another date in the given unit.
* @param other The other date to compare.
* @param unit The unit in which to return the difference.
*/
diff(other, unit) {
const time = other instanceof Chronos ? other : new Chronos(other);
const msDiff = this.#date.getTime() - time.toDate().getTime();
switch (unit) {
case 'millisecond':
return msDiff;
case 'second':
return msDiff / 1e3;
case 'minute':
return msDiff / 6e4;
case 'hour':
return msDiff / 3.6e6;
case 'day':
return msDiff / 8.64e7;
case 'week':
return msDiff / 6.048e8;
case 'month':
return ((this.get('year') - time.get('year')) * 12 +
(this.get('month') - time.get('month')));
case 'year':
return this.get('year') - time.get('year');
}
}
/**
* @instance Returns a human-readable relative calendar time like "Today at 3:00 PM"
* @param baseDate Optional base date to compare with.
*/
calendar(baseDate) {
const base = baseDate ? new Chronos(baseDate) : new Chronos();
const input = this.startOf('day');
const comparison = base.startOf('day');
const diff = input.diff(comparison, 'day');
const timeStr = this.toDate().toLocaleString(undefined, {
hour: 'numeric',
minute: '2-digit',
});
if (diff === 0)
return `Today at ${timeStr}`;
if (diff === 1)
return `Tomorrow at ${timeStr}`;
if (diff === -1)
return `Yesterday at ${timeStr}`;
return this.toDate().toLocaleString(undefined, {
month: 'long',
day: '2-digit',
year: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: '2-digit',
});
}
/** @instance Returns a short human-readable string like "2h ago", "in 5m" */
fromNowShort() {
const now = new Chronos();
const diffInSeconds = this.diff(now, 'second');
const abs = Math.abs(diffInSeconds);
const suffix = diffInSeconds >= 0 ? 'in ' : '';
const postfix = diffInSeconds < 0 ? ' ago' : '';
if (abs < 60) {
return `${suffix}${Math.floor(abs)}s${postfix}`;
}
else if (abs < 3600) {
return `${suffix}${Math.floor(abs / 60)}m${postfix}`;
}
else if (abs < 86400) {
return `${suffix}${Math.floor(abs / 3600)}h${postfix}`;
}
else if (abs < 2592000) {
return `${suffix}${Math.floor(abs / 86400)}d${postfix}`;
}
else if (abs < 31536000) {
return `${suffix}${Math.floor(abs / 2592000)}mo${postfix}`;
}
else {
return `${suffix}${Math.floor(abs / 31536000)}y${postfix}`;
}
}
/**
* @instance Sets the date to the Monday of the specified ISO week number within the current year.
* This method assumes ISO week logic, where week 1 is the week containing January 4th.
*
* @param week The ISO week number (1–53) to set the date to.
* @returns A new Chronos instance set to the start (Monday) of the specified week.
*/
setWeek(week) {
const d = new Date(this.#date);
const year = d.getFullYear();
const jan4 = new Date(year, 0, 4);
const dayOfWeek = jan4.getDay() || 7; // Make Sunday (0) into 7
const weekStart = new Date(jan4);
weekStart.setDate(jan4.getDate() - (dayOfWeek - 1)); // Move to Monday
weekStart.setDate(weekStart.getDate() + (week - 1) * 7); // Move to target week
d.setFullYear(weekStart.getFullYear());
d.setMonth(weekStart.getMonth());
d.setDate(weekStart.getDate());
return new Chronos(d).#withOrigin('setWeek');
}
/**
* @instance Calculates the ISO 8601 week number of the year.
*
* ISO weeks start on Monday, and the first week of the year is the one containing January 4th.
*
* @returns Week number (1–53).
*/
getWeek() {
const target = this.startOf('week', 1).add(3, 'day'); // Thursday of current ISO week
const firstThursday = new Chronos(target.year, 1, 4) // January 4
.startOf('week', 1)
.add(3, 'day'); // Thursday of first ISO week
return (target.diff(firstThursday, 'week') + 1);
}
/**
* @instance Calculates the week number of the year based on custom week start.
* @param weekStartsOn Optional: Day the week starts on (0 = Sunday, 1 = Monday). Applicable if week day is required. Default is `0`.
* @returns Week number (1-53).
*/
getWeekOfYear(weekStartsOn = 0) {
const startOfYear = new Chronos(this.year, 1, 1);
const startOfFirstWeek = startOfYear.startOf('week', weekStartsOn);
const week = this.startOf('week', weekStartsOn).diff(startOfFirstWeek, 'week');
return (week + 1);
}
/**
* @instance Returns the ISO week-numbering year for the current date.
*
* The ISO week-numbering year may differ from the calendar year.
* For example, January 1st may fall in the last ISO week of the previous year.
*
* @param weekStartsOn Optional: Defines the start day of the week (0 = Sunday, 1 = Monday).
* Defaults to 0 (Sunday). Use 1 for strict ISO 8601.
* @returns The ISO week-numbering year.
*/
getWeekYear(weekStartsOn = 0) {
const d = this.startOf('week', weekStartsOn).add(3, 'day'); // Thursday of current ISO week
return d.year;
}
/** @instance Returns day of year (1 - 366) */
getDayOfYear() {
const start = new Date(this.year, 0, 1);
const diff = this.#date.getTime() - start.getTime();
return (Math.floor(diff / 86400000) + 1);
}
/** @instance Returns number of days in current month */
daysInMonth() {
return new Date(this.year, this.month + 1, 0).getDate();
}
/** @instance Converts to object with all date unit parts */
toObject() {
return Object.fromEntries([...this]);
}
/** @instance Converts to array with all date unit parts */
toArray() {
return Object.values(this.toObject());
}
/**
* @instance Returns the **calendar quarter** (1 to 4) of the current date.
*
* @remarks
* A calendar year is divided into four quarters:
*
* - `Q1`: January to March
* - `Q2`: April to June
* - `Q3`: July to September
* - `Q4`: October to December
*
* This method strictly uses the **calendar year**. For fiscal quarters, use `toFiscalQuarter()` instead.
*
* @example
* new Chronos('2025-02-14').toQuarter(); // 1
* new Chronos('2025-08-09').toQuarter(); // 3
*
* @returns The calendar quarter number (1–4).
*/
toQuarter() {
const month = this.#date.getMonth();
return (Math.floor(month / 3) + 1);
}
/**
* @instance Returns the system's current UTC offset formatted as `+06:00` or `-07:00`.
*
* - *Unlike `Date.prototype.getTimezoneOffset()`, which returns the offset in minutes **behind** UTC (positive for locations west of UTC and negative for east), this method returns the more intuitive sign format used in time zone representations (e.g., `UTC+06:00` means 6 hours **ahead** of UTC).*
*
* @returns The (local) system's UTC offset in `±HH:mm` format.
*/
getUTCOffset() {
const offset = -this.#date.getTimezoneOffset();
const sign = offset >= 0 ? '+' : '-';
const pad = (n) => String(Math.floor(Math.abs(n))).padStart(2, '0');
return `${sign}${pad(offset / 60)}:${pad(offset % 60)}`;
}
/**
* @instance Returns the timezone offset of this `Chronos` instance in `+06:00` or `-07:00` format maintaining current timezone.
*
* - *Unlike `Date.prototype.getTimezoneOffset()`, which returns the offset in minutes **behind** UTC (positive for locations west of UTC and negative for east), this method returns the more intuitive sign format used in time zone representations (e.g., `UTC+06:00` means 6 hours **ahead** of UTC).*
*
* @returns The timezone offset string in `±HH:mm` format maintaining the current timezone regardless of system having different one.
*/
getTimeZoneOffset() {
return this.#offset.replace('UTC', '');
}
/**
* @instance Returns the system's UTC offset in minutes.
*
* - *Unlike JavaScript's `Date.prototype.getTimezoneOffset()`, this method returns a positive value if the local time is ahead of UTC, and negative if behind UTC.*
*
* For example, for `UTC+06:00`, this returns `360`; for `UTC-05:30`, this returns `-330`.
*
* @returns The system's UTC offset in minutes, matching the sign convention used in `±HH:mm`.
*/
getUTCOffsetMinutes() {
return -this.#date.getTimezoneOffset();
}
/**
* @instance Returns the current `Chronos` instance's UTC offset in minutes.
*
* This reflects the parsed or stored offset used internally by Chronos and follows the same
* sign convention: positive for timezones ahead of UTC, negative for behind.
*
* @returns The UTC offset in minutes maintaining the current timezone regardless of system having different one.
*/
getTimeZoneOffsetMinutes() {
return extractMinutesFromUTC(this.#offset);
}
/**
* @instance Returns the current time zone name as a full descriptive string (e.g. `"Bangladesh Standard Time"`).
* @param utc Optional UTC offset in `"UTC+06:00"` format. When passed, it bypasses the current time zone offset.
* @returns Time zone name in full descriptive string or UTC offset if it is not a valid time zone.
* @remarks
* - This method uses a predefined mapping of UTC offsets to time zone names.
* - If multiple time zones share the same UTC offset, it returns the **first match** from the predefined list.
* - If no match is found (which is rare), it falls back to returning the UTC offset (e.g. `"UTC+06:00"`).
*/
getTimeZoneName(utc) {
const UTC = utc ?? `UTC${this.getTimeZoneOffset()}`;
return TIME_ZONE_LABELS?.[UTC] ?? UTC;
}
/** @instance Returns new Chronos instance in UTC */
toUTC() {
if (this.#offset === 'UTC+00:00') {
return this.#withOrigin('toUTC');
}
const date = this.#date;
const previousOffset = this.getTimeZoneOffsetMinutes();
const utc = new Date(date.getTime() - previousOffset * 60 * 1000);
return new Chronos(utc).#withOrigin('toUTC');
}
/** @instance Returns new Chronos instance in local time */
toLocal() {
const previousOffset = this.getTimeZoneOffsetMinutes();
const localOffset = -this.#date.getTimezoneOffset();
const relativeOffset = previousOffset - localOffset;
const localTime = new Date(this.#date.getTime() - relativeOffset * 60 * 1000);
return new Chronos(localTime).#withOrigin('toLocal');
}
/**
* @instance Rounds the current date-time to the nearest specified unit and interval.
*
* - *Rounding is based on proximity to the start or end of the specified unit.*
* - *For example, rounding `2025-05-23` by 'day' returns either midnight of May 23 or May 24, depending on the time of day.*
*
* @param unit - The time unit to round to (`year`, `month`, `week`, `day`, `hour`, `minute`, `second`, `millisecond`).
* @param nearest - Optional granularity of rounding. (Defaults to `1`).
*
* @returns A new `Chronos` instance at the nearest rounded point in time. For wrong unit returns current instance.
*
* @remarks
* - Rounding for `'month'` is based on how far into the month the date is. If past the midpoint, it rounds to the next month.
* - Month indices are 0-based internally (January = 0), but the resulting date reflects the correct calendar month.
* - For `'week'` unit, rounding is performed by comparing proximity to the start and end of the ISO week (Monday to Sunday).
* - If the date is closer to the next Monday, it rounds forward; otherwise, it rounds back to the previous Monday.
*/
round(unit, nearest = 1) {
const date = new Date(this.#date);
switch (unit) {
case 'millisecond': {
const rounded = roundToNearest(date.getMilliseconds(), nearest);
date.setMilliseconds(rounded);
break;
}
case 'second': {
const fullSecond = date.getSeconds() + date.getMilliseconds() / 1000;
const rounded = roundToNearest(fullSecond, nearest);
date.setSeconds(rounded, 0);
break;
}
case 'minute': {
const fullMinute = date.getMinutes() +
date.getSeconds() / 60 +
date.getMilliseconds() / 60000;
const rounded = roundToNearest(fullMinute, nearest);
date.setMinutes(rounded, 0, 0);
break;
}
case 'hour': {
const fullHour = date.getHours() +
date.getMinutes() / 60 +
date.getSeconds() / 3600 +
date.getMilliseconds() / 3600000;
const rounded = roundToNearest(fullHour, nearest);
date.setHours(rounded, 0, 0, 0);
break;
}
case 'day': {
const fullDay = date.getDate() +
(date.getHours() / 24 +
date.getMinutes() / 1440 +
date.getSeconds