atomic-fns
Version:
Like Lodash, but for ESNext and with types. Stop shipping code built for browsers from 2015.
531 lines (530 loc) • 17.8 kB
JavaScript
import { isObject, ValueError } from '../../globals/index.js';
import { Duration, UNITS_PLURAL } from '../duration.js';
import { asDate, formatDate, strftime } from '../format.js';
import { asNumber, dayOfYear, daysInMonth, daysInYear, isLeapYear, maxDate, minDate, weekOfYear, weeksInYear } from './utils.js';
// Export date functions
export { asDate, daysInMonth, daysInYear, formatDate, isLeapYear, maxDate, minDate, strftime, weeksInYear };
const INVALID_DATE_STRING = 'Invalid Date';
/**
* Creates a date tied to a given locale (default system locale) which can be formatted in that locale's language using the native {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl Intl Apis} directly or using a formatting string.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl Intl Apis}
*/
export class IntlDate {
_date;
locale;
intlRelativeFormat;
/** Parses the provided value as UTC date or returns the current UTC date. */
static UTC(value, locale) {
return new IntlDate(value, { utc: true, locale });
}
/** Returns the current local date and time.*/
static now() {
return new IntlDate();
}
/** Creates a new date from a Unix timestamp (seconds since the unix epoch) */
static unix(seconds) {
return new IntlDate(seconds * 1000);
}
/**
* Creates a new `IntlDate` in local time and specified locale (default is system locale).
* **Note:** The allowed values for `month` start at 1, which is different from legacy `Date`
* @param {DateLike} obj The date value (default is current local time)
* @param {Object} opts The options for this date
* @param {boolean?} opts.utc Converts input or current time to UTC
* @param {string?} opts.locale A locale string to use for this date
* @returns
*/
constructor(obj, { locale, utc } = {}) {
this.locale = locale;
if (obj instanceof IntlDate) {
obj = obj._date;
}
if (typeof obj === 'string' || typeof obj === 'number' || obj instanceof Date) {
this._date = asDate(obj, utc);
}
else if (isObject(obj)) {
let { year, month, day, hour, minute, second, millisecond } = obj;
// NOTE month is from 1-12, not from 0-11
if (typeof month === 'number') {
month -= 1;
}
if (utc) {
// from UTC to current
this._date = new Date(Date.UTC(year, month, day || 1, hour || 0, minute || 0, second || 0, millisecond || 0));
}
else {
this._date = new Date(year, month, day || 1, hour || 0, minute || 0, second || 0, millisecond || 0);
}
}
else {
this._date = asDate(obj, utc);
}
return this;
}
/** Get the date's year. */
get year() {
return this._date.getFullYear();
}
/** Get the this date's month as a number from 1 to 12, inclusive. */
get month() {
return this._date.getMonth() + 1;
}
/** Returns the weekday as a number between 1 and 7, inclusive, where Monday is 1 and Sunday is 7. */
get dayOfWeek() {
if (this._date.getDay() === 0)
return 7;
return this._date.getDay();
}
/** Get the this date's current day of the month. */
get day() {
return this._date.getDate();
}
/** Get the this date's current hour. */
get hour() {
return this._date.getHours();
}
/** Get the this date's current minute. */
get minute() {
return this._date.getMinutes();
}
/** Get the this date's current second. */
get second() {
return this._date.getSeconds();
}
/** Get the this date's current millisecond. */
get millisecond() {
return this._date.getMilliseconds();
}
isValid() {
return this._date.toString() !== INVALID_DATE_STRING;
}
/** Get the number of days in this date's month. */
daysInMonth() {
return daysInMonth(this._date);
}
/** Get the number of days in this date's year. */
daysInYear() {
return daysInYear(this.year);
}
dayOfYear() {
return dayOfYear(this._date);
}
/** Returns `true` if this date's year is a leap year. */
isLeapYear() {
return isLeapYear(this.year);
}
/** Gets the number of weeks according to locale in the current year. */
weeksInYear() {
return weeksInYear(this.year);
}
/** Gets the number of weeks in the current year, according to ISO weeks. */
isoWeeksInYear() {
return weeksInYear(this.year, 1, 4);
}
/** Returns the current week of the year. */
week() {
return weekOfYear(this._date).week;
}
/** Returns the ISO week of the year. */
isoWeek() {
return weekOfYear(this._date, 1, 4).week;
}
/**
* Returns a localized string representation of this date.
* @param {Intl.DateTimeFormatOptions} opts The options to use for the format
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat Intl.DateTimeFormat()}
*/
toString(opts) {
return this._date.toLocaleString(this.locale, opts);
}
/**
* Returns a localized string representation of this date, according to the given format string. Format codes use the same specification as {@link https://momentjs.com/docs/#/displaying/format/ moment}.
* @param {string} str The format string to use
* @see {@link https://momentjs.com/docs/#/displaying/format/ List of formats}
* @example
```js
new IntlDate().format('MM/DD/YYYY') // '10/31/2022'
```
*/
format(str) {
return formatDate(str, this._date, this.locale);
}
/**
* Returns a new copy of the native `Date` object used by this instance.
* @returns {Date} A new `Date` object
*/
toDate() {
return new Date(this.isValid() ? this._date : NaN);
}
/**
* Returns the number of seconds since the Unix Epoch (January 1, 1970 UTC).
* @see {@link IntlDate.timestamp}
* @see {@link IntlDate.unix}
* @example
```js
new IntlDate().toSeconds() // 1318874398
```
*/
toSeconds() {
return Math.trunc(this.getTime() / 1000);
}
/**
* Returns the number of seconds since the Unix Epoch (January 1, 1970 UTC).
* @see {@link IntlDate.toSeconds}
* @see {@link IntlDate.unix}
* @example
```js
new IntlDate().timestamp() // 1318874398
```
*/
timestamp() {
return this.toSeconds();
}
/**
* Returns the number of seconds since the Unix Epoch (January 1, 1970 UTC).
* @see {@link IntlDate.toSeconds}
* @see {@link IntlDate.timestamp}
* @example
```js
new IntlDate().unix() // 1318874398
```
*/
unix() {
return this.toSeconds();
}
/** Formats a string to the ISO8601 standard.
* @example
```js
new IntlDate().toISOString() // '2022-10-31T22:44:30.652Z'
```
*/
toISOString() {
return this._date.toISOString();
}
/** Returns the date part formatted as ISO8601.
* @example
```js
new IntlDate().toISODate() // '2022-10-31'
```
*/
toISODate() {
return this._date.toISOString().slice(0, 10);
}
/** Returns the time part formatted as ISO8601.
* @example
```js
new IntlDate().toISOTime() // 'T22:44:30.652Z'
```
*/
toISOTime() {
return this._date.toISOString().slice(11);
}
/**
* Returns an object containing year, month, day-of-month, hours, minutes, seconds, milliseconds.
* @returns {Object} An object like `{year, month, date, hours, minutes, seconds, ms}`
*/
toObject() {
return {
year: this.year,
month: this.month,
day: this.day,
hour: this.hour,
minute: this.minute,
second: this.second,
millisecond: this.millisecond
};
}
/** Returns the number of milliseconds since the Unix Epoch (January 1, 1970 UTC) */
getTime() {
return this.isValid() ? this._date.getTime() : NaN;
}
/** Alias of {@link IntlDate.getTime} */
valueOf() {
return this.getTime();
}
/** Returns the timezone string name */
zoneName() {
return this.format('zzz');
}
/** Returns the timezone GMT offset as a string */
zone() {
return this.format('ZZ');
}
/** Returns the difference in `minutes` between this date and UTC */
utcOffset() {
return this._date.getTimezoneOffset();
}
/**
* Check if this date is before another date. The other value will be parsed as an `IntlDate` if not already so.
* @param {DateLike} other Another date or date like object
* @returns {boolean} Returns `true` if this date is before the given value
*/
isBefore(other) {
if (other instanceof IntlDate) {
return this.getTime() < other.getTime();
}
return this.getTime() < new IntlDate(other).getTime();
}
/**
* Check if this date is strictly after the given `start` and before `stop` dates. The values will be parsed as an `IntlDate` if not already so.
* @param {DateLike} start The smaller date
* @param {DateLike} stop The larger date
* @returns {boolean} Returns `true` if this date is greater than `start` and less than `stop`.
*/
isBetween(start, stop) {
return this.isAfter(start) && this.isBefore(stop);
}
/**
* Check if this date is after another date. The other value will be parsed as an `IntlDate` if not already so.
* @param {DateLike} other Another date or date like object
* @returns {boolean} Returns `true` if this date is after the given value
*/
isAfter(other) {
if (other instanceof IntlDate) {
return this.getTime() > other.getTime();
}
return this.getTime() > new IntlDate(other).getTime();
}
/**
* Check if this date is the same as `other`.
* @param other Another date object
* @returns {boolean} Returns `true` if this date is the same as other
*/
isSame(other) {
if (other instanceof IntlDate) {
return (this.isValid() &&
other.isValid() &&
this.getTime() === other.getTime() &&
this.locale === other.locale);
}
return this.getTime() === other.getTime();
}
lt(other) {
return this.isBefore(other);
}
eq(other) {
return this.isSame(other);
}
compare(other) {
if (!this.isValid())
return 1;
let dt = new IntlDate(other);
if (!dt.isValid())
return -1;
return this.getTime() - dt.getTime();
}
set(values) {
if (!this.isValid())
return this;
const normalized = normalizeObject(values, normalizeUnit);
let mixed = { ...this.toObject(), ...normalized };
// if we didn't set the day but we ended up on an overflow date,
// use the last day of the right month
if (normalized.day === undefined) {
mixed.day = Math.min(daysInMonth(this._date), mixed.day);
}
return this.clone(mixed);
}
get relativeFormat() {
if (this.intlRelativeFormat)
return this.intlRelativeFormat;
this.intlRelativeFormat = new Intl.RelativeTimeFormat(this.locale);
return new Intl.RelativeTimeFormat(this.locale);
}
relativeTime(n, unit = 'seconds') {
return this.relativeFormat.format(n, unit);
}
fromNow(unit = 'seconds') {
let diff = this.diff(new Date(), unit);
return this.relativeTime(diff, unit);
}
from(other, unit = 'seconds') {
let diff = this.diff(other, unit);
return this.relativeTime(diff, unit);
}
toNow(unit = 'seconds') {
let diff = this.diff(new Date(), unit);
return this.relativeTime(-diff, unit);
}
to(other, unit = 'seconds') {
let diff = this.diff(other, unit);
return this.relativeTime(-diff, unit);
}
diff(other, unit = 'milliseconds', exact = false) {
let dt = new IntlDate(other);
let dur = new Duration(this.getTime() - dt.getTime(), exact);
let total = dur.total(unit);
if (!exact)
total = Math.trunc(total);
return total;
}
add(duration, exact = false) {
return this.clone(addDuration(this._date, duration, exact));
}
subtract(duration, exact = false) {
return this.clone(subDuration(this._date, duration, exact));
}
startOf(unit) {
if (!this.isValid())
return this;
const obj = {}, normalizedUnit = UNITS_PLURAL[unit.toLowerCase()];
switch (normalizedUnit) {
case 'years':
obj.month = 1;
// falls through
case 'quarters':
case 'months':
obj.day = 1;
// falls through
case 'weeks':
case 'days':
obj.hour = 0;
// falls through
case 'hours':
obj.minute = 0;
// falls through
case 'minutes':
obj.second = 0;
// falls through
case 'seconds':
obj.millisecond = 0;
break;
case 'milliseconds':
break;
// no default, invalid units throw in normalizeUnit()
}
if (normalizedUnit === 'weeks') {
obj.weekday = 1;
}
if (normalizedUnit === 'quarters') {
const q = Math.ceil(this.month / 3);
obj.month = (q - 1) * 3 + 1;
}
return this.set(obj);
}
endOf(unit) {
return this.isValid()
? this.add({ [unit]: 1 })
.startOf(unit)
.subtract({ milliseconds: 1 })
: this;
}
clone(date) {
return new IntlDate(date ?? this, { locale: this.locale });
}
static max(...args) {
return maxDate.apply(undefined, args);
}
static min(...args) {
return minDate.apply(undefined, args);
}
inspect() {
return `new IntlDate("${this.toISOString()}")`;
}
}
// Check if the new offset is different because we crossed to DST
function fixOffset(startDate, endDate) {
let startOffset = startDate.getTimezoneOffset();
let endOffset = endDate.getTimezoneOffset();
if (startOffset === endOffset)
return new Date(endDate);
let postoffset = (startOffset - endOffset) * 60 * 1000;
return new Date(+endDate + postoffset);
}
export function addDuration(date, duration, exact = false) {
// Get the current offset of this date timezone
let newDate = new Date(date);
// Get the number of ticks forward
let { values } = new Duration(duration, exact).normalize();
// Apply any time changes that may have happened
for (const unit in values) {
setDateUnit(newDate, unit, Math.floor(values[unit]));
}
return fixOffset(new Date(date), newDate);
}
function setDateUnit(date, unit, value) {
switch (unit) {
case 'years':
date.setFullYear(date.getFullYear() + value);
break;
case 'quarters':
date.setMonth(date.getMonth() + value * 3);
break;
case 'months':
date.setMonth(date.getMonth() + value);
break;
case 'weeks':
date.setDate(date.getDate() + value * 7);
break;
case 'days':
date.setDate(date.getDate() + value);
break;
case 'hours':
date.setHours(date.getHours() + value);
break;
case 'minutes':
date.setMinutes(date.getMinutes() + value);
break;
case 'seconds':
date.setSeconds(date.getSeconds() + value);
break;
case 'milliseconds':
date.setMilliseconds(date.getMilliseconds() + value);
break;
default:
break;
}
return date;
}
/**
* Subtract the specified years, months, weeks, days, hours, minutes and seconds from the given date.
* @param date
* @param duration
* @param exact
* @returns
*/
export function subDuration(date, duration, exact = false) {
return addDuration(date, new Duration(duration, exact).negated(), exact);
}
const UNITS_SINGULAR = {
year: 'year',
years: 'year',
month: 'month',
months: 'month',
day: 'day',
days: 'day',
hour: 'hour',
hours: 'hour',
minute: 'minute',
minutes: 'minute',
quarter: 'quarter',
quarters: 'quarter',
second: 'second',
seconds: 'second',
millisecond: 'millisecond',
milliseconds: 'millisecond',
weekday: 'weekday',
weekdays: 'weekday',
weeknumber: 'weekNumber',
weeksnumber: 'weekNumber',
weeknumbers: 'weekNumber',
weekyear: 'weekYear',
weekyears: 'weekYear',
ordinal: 'ordinal'
};
function normalizeUnit(unit) {
const normalized = UNITS_SINGULAR[unit.toLowerCase()];
if (!normalized)
throw new ValueError(`Invalid unit ${unit}`);
return normalized;
}
function normalizeObject(obj, normalizer) {
const normalized = {};
for (const key of Object.keys(obj)) {
const value = obj[key];
if (value == null)
continue;
normalized[normalizer(key)] = asNumber(value);
}
return normalized;
}