mauss
Version:
practical functions and reusable configurations
264 lines (263 loc) • 10.7 kB
JavaScript
/**
* Creates a fluent, immutable date utility wrapper around a given date.
*
* Supports manipulation, comparison, formatting, and localization.
*
* @throws {Error} If the provided input date is invalid.
*/
export function date(input = new Date()) {
const d = input instanceof Date ? new Date(input.getTime()) : new Date(input);
if (Number.isNaN(d.getTime()))
throw new Error(`Invalid date: ${input}`);
return {
/** Returns a new `date()` instance with the same timestamp */
clone() {
return date(d);
},
/** Returns a fresh copy of the native `Date` representing the internal timestamp */
get raw() {
return new Date(d.getTime());
},
/** Returns the internal timestamp in milliseconds */
epoch: d.getTime(),
/** Returns a new `date()` instance with the specified time added */
add(amount, unit) {
const next = this.raw;
switch (unit) {
case 'millisecond':
next.setMilliseconds(d.getMilliseconds() + amount);
break;
case 'second':
next.setSeconds(d.getSeconds() + amount);
break;
case 'minute':
next.setMinutes(d.getMinutes() + amount);
break;
case 'hour':
next.setHours(d.getHours() + amount);
break;
case 'day':
next.setDate(d.getDate() + amount);
break;
case 'month':
next.setMonth(d.getMonth() + amount);
break;
case 'year':
next.setFullYear(d.getFullYear() + amount);
break;
}
return date(next);
},
/** Returns a new `date()` instance with the specified time subtracted */
subtract(amount, unit) {
return this.add(-amount, unit);
},
/** Computes the time difference between this date and the `other` */
delta(other) {
const from = date(other).raw;
const ms = d.getTime() - from.getTime();
return {
/** Returns the raw time difference in milliseconds */
get milliseconds() {
return ms;
},
/** Returns the time difference in seconds */
get seconds() {
return ms / 1000;
},
/** Returns the time difference in minutes */
get minutes() {
return ms / 60000;
},
/** Returns the time difference in hours */
get hours() {
return ms / 3600000;
},
/** Returns the time difference in days */
get days() {
return ms / 86400000;
},
/** Returns the time difference in months, adjusted for day of month */
get months() {
const years = d.getFullYear() - from.getFullYear();
const months = d.getMonth() - from.getMonth();
const adjust = d.getDate() < from.getDate() ? -1 : 0;
return years * 12 + months + adjust;
},
/** Returns the time difference in years, derived from months */
get years() {
return this.months / 12;
},
};
},
/** A set of boolean checks and comparisons for the current date */
is: {
/** True if the date falls on the current day */
get today() {
const now = date().raw;
return (d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate());
},
/** True if the date is exactly one day before today */
get yesterday() {
const yesterday = new Date(d);
yesterday.setDate(d.getDate() - 1);
return (d.getFullYear() === yesterday.getFullYear() &&
d.getMonth() === yesterday.getMonth() &&
d.getDate() === yesterday.getDate());
},
/** True if the date is exactly one day after today */
get tomorrow() {
const tomorrow = new Date(d);
tomorrow.setDate(d.getDate() + 1);
return (d.getFullYear() === tomorrow.getFullYear() &&
d.getMonth() === tomorrow.getMonth() &&
d.getDate() === tomorrow.getDate());
},
/** True if the date falls between Monday and Friday */
get weekday() {
const day = d.getDay();
return day >= 1 && day <= 5;
},
/** True if the date is Saturday or Sunday */
get weekend() {
const day = d.getDay();
return day === 0 || day === 6;
},
/** True if the date's year is a leap year */
get leap() {
const year = d.getFullYear();
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
},
/** True if the date is before the given date */
before(other) {
return d.getTime() < new Date(other).getTime();
},
/** True if the date is after the given date */
after(other) {
return d.getTime() > new Date(other).getTime();
},
/** True if the date is the same as the given date */
same(other) {
return d.getTime() === new Date(other).getTime();
},
},
to: {
/**
* Returns a localized, human-readable relative time string such as "yesterday", "in 2 hours", "3 months ago", etc.
*
* Falls back to "now" if the difference is negligible.
*/
relative(base = new Date(), locale = 'en') {
const intl = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const delta = d.getTime() - date(base).raw.getTime();
const abs = Math.abs(delta);
const units = [
['year', 31556952000],
['month', 2629746000],
['day', 86400000],
['hour', 3600000],
['minute', 60000],
['second', 1000],
];
for (const [unit, ms] of units) {
if (abs < ms)
continue;
const value = Math.round(delta / ms);
return intl.format(value, unit);
}
return intl.format(0, 'second');
},
},
/**
* Returns a formatted string of the current date based on a mask pattern.
* Supports common patterns (e.g. `YYYY-MM-DD`) and localized weekday/month names.
*
* Default format: `YYYY-MM-DDTHH:mm:ssZZZ`
* Locale affects the output of tokens like `MMM`, `MMMM`, `DDD`, `DDDD`.
*
* Token reference:
* - Date: `D`, `DD`
* - Weekday: `DDD`, `DDDD`
* - Month: `M`, `MM`, `MMM`, `MMMM`
* - Year: `YY`, `YYYY`
* - Hour (24): `H`, `HH`
* - Hour (12): `h`, `hh`
* - Minute: `m`, `mm`
* - Second: `s`, `ss`
* - Meridiem: `a`, `A`, `p`, `P`
* - Timezone: `Z`, `ZZ`, `ZZZ`
* - Literal text: wrap in square brackets `[like this]`
*/
get format() {
const pad = (n) => n.toString().padStart(2, '0');
const now = {
date: d.getDate(),
day: d.getDay(),
month: d.getMonth(),
year: d.getFullYear(),
hours: d.getHours(),
minutes: d.getMinutes(),
seconds: d.getSeconds(),
marker: d.getHours() < 12 ? 'AM' : 'PM',
get tzo() {
const offset = d.getTimezoneOffset();
const sign = offset <= 0 ? '+' : '-';
const abs = Math.abs(offset);
const tz = [Math.floor(abs / 60), abs % 60];
return {
short: `${sign}${tz[0]}`,
long: `${sign}${pad(tz[0])}${pad(tz[1])}`,
iso: `${sign}${pad(tz[0])}:${pad(tz[1])}`,
};
},
};
const tokens = {
D: now.date,
DD: pad(now.date),
M: now.month + 1,
MM: pad(now.month + 1),
YY: `${now.year}`.slice(2),
YYYY: now.year,
H: now.hours,
HH: pad(now.hours),
h: now.hours % 12 || 12,
hh: pad(now.hours % 12 || 12),
m: now.minutes,
mm: pad(now.minutes),
s: now.seconds,
ss: pad(now.seconds),
a: now.marker,
p: now.marker,
A: now.marker,
P: now.marker,
Z: now.tzo.short,
ZZ: now.tzo.long,
ZZZ: now.tzo.iso,
};
const EXP = /D{1,4}|M{1,4}|YY(?:YY)?|([hHmsAPap])\1?|Z{1,3}|\[([^\]\[]|\[[^\[\]]*\])*\]/g;
return (mask = 'YYYY-MM-DDTHH:mm:ssZZZ', locale = 'en') => {
const intl = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long' });
const day = intl.formatToParts(d).find(({ type }) => type === 'weekday')?.value || '';
const month = intl.formatToParts(d).find(({ type }) => type === 'month')?.value || '';
tokens.DDD = day.slice(0, 3);
tokens.DDDD = day;
tokens.MMM = month.slice(0, 3);
tokens.MMMM = month;
return mask.replace(EXP, ($) => {
const exe = tokens[$];
return `${exe || ''}` || $.slice(1, $.length - 1);
});
};
},
};
}
date.sort = {
oldest(x, y) {
return date(x).epoch - date(y).epoch;
},
newest(x, y) {
return date(y).epoch - date(x).epoch;
},
};