@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
1,000 lines (999 loc) • 32.4 kB
JavaScript
import { _assert } from '../error/assert.js';
import { localDate } from './localDate.js';
import { _ms } from './time.util.js';
import { WallTime } from './wallTime.js';
export var ISODayOfWeek;
(function (ISODayOfWeek) {
ISODayOfWeek[ISODayOfWeek["MONDAY"] = 1] = "MONDAY";
ISODayOfWeek[ISODayOfWeek["TUESDAY"] = 2] = "TUESDAY";
ISODayOfWeek[ISODayOfWeek["WEDNESDAY"] = 3] = "WEDNESDAY";
ISODayOfWeek[ISODayOfWeek["THURSDAY"] = 4] = "THURSDAY";
ISODayOfWeek[ISODayOfWeek["FRIDAY"] = 5] = "FRIDAY";
ISODayOfWeek[ISODayOfWeek["SATURDAY"] = 6] = "SATURDAY";
ISODayOfWeek[ISODayOfWeek["SUNDAY"] = 7] = "SUNDAY";
})(ISODayOfWeek || (ISODayOfWeek = {}));
const weekStartsOn = 1; // mon, as per ISO
const MILLISECONDS_IN_WEEK = 604800000;
const SECONDS_IN_DAY = 86400;
// const MILLISECONDS_IN_DAY = 86400000
// const MILLISECONDS_IN_MINUTE = 60000
export const VALID_DAYS_OF_WEEK = new Set([1, 2, 3, 4, 5, 6, 7]);
/**
* It supports 2 forms:
* 1. 2023-03-03
* 2. 2023-03-03T05:10:02
*/
const DATE_TIME_REGEX_LOOSE = /^(\d{4})-(\d{2})-(\d{2})([Tt\s](\d{2}):?(\d{2})?:?(\d{2})?)?/;
/**
* Supports 2 forms:
* 1. 2023-03-03
* 2. 2023-03-03T05:10:02
* Ok, now it allows arbitrary stuff after `:ss`, to allow millis/timezone info,
* but it will not take it into account.
*/
const DATE_TIME_REGEX_STRICT = /^(\d{4})-(\d{2})-(\d{2})[Tt\s](\d{2}):(\d{2}):(\d{2})/;
const DATE_REGEX_STRICT = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
export class LocalTime {
$date;
constructor($date) {
this.$date = $date;
}
/**
* Returns [cloned] LocalTime that is based on the same unixtimestamp, but in UTC timezone.
* Opposite of `.local()` method.
*/
toUTC() {
return new LocalTime(new Date(this.$date.toISOString()));
}
/**
* Returns [cloned] LocalTime that is based on the same unixtimestamp, but in local timezone.
* Opposite of `.utc()` method.
*/
toLocal() {
return new LocalTime(new Date(this.$date));
}
/**
* Returns [cloned] fake LocalTime that has yyyy-mm-dd hh:mm:ss in the provided timezone.
* It is a fake LocalTime in a sense that it's timezone is not real.
* See this ("common errors"): https://stackoverflow.com/a/15171030/4919972
* Fake also means that unixTimestamp of that new LocalDate is not the same.
* For that reason we return WallTime, and not a LocalTime.
* WallTime can be pretty-printed as Date-only, Time-only or DateAndTime.
*
* E.g `inTimezone('America/New_York').toISOTime()`
*
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*
* @experimental
*/
inTimezone(tz) {
const d = new Date(this.$date.toLocaleString('en-US', { timeZone: tz }));
return new WallTime({
year: d.getFullYear(),
month: d.getMonth() + 1,
day: d.getDate(),
hour: d.getHours(),
minute: d.getMinutes(),
second: d.getSeconds(),
});
}
/**
* UTC offset is the opposite of "timezone offset" - it's the number of minutes to add
* to the local time to get UTC time.
*
* E.g utcOffset for CEST is -120,
* which means that you need to add -120 minutes to the local time to get UTC time.
*
* Instead of -0 it returns 0, for the peace of mind and less weird test/snapshot differences.
*
* If timezone (tz) is specified, e.g `America/New_York`,
* it will return the UTC offset for that timezone.
*
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*/
getUTCOffsetMinutes(tz) {
if (tz) {
// based on: https://stackoverflow.com/a/53652131/4919972
const nowTime = this.$date.getTime();
const tzTime = new Date(this.$date.toLocaleString('en-US', { timeZone: tz })).getTime();
return Math.round((tzTime - nowTime) / 60000) || 0;
}
return -this.$date.getTimezoneOffset() || 0;
}
/**
* Same as getUTCOffsetMinutes, but rounded to hours.
*
* E.g for CEST it is -2.
*
* Instead of -0 it returns 0, for the peace of mind and less weird test/snapshot differences.
*
* If timezone (tz) is specified, e.g `America/New_York`,
* it will return the UTC offset for that timezone.
*/
getUTCOffsetHours(tz) {
return Math.round(this.getUTCOffsetMinutes(tz) / 60);
}
/**
* Returns e.g `-05:00` for New_York winter time.
*/
getUTCOffsetString(tz) {
const minutes = this.getUTCOffsetMinutes(tz);
const hours = Math.trunc(minutes / 60);
const sign = hours < 0 ? '-' : '+';
const h = String(Math.abs(hours)).padStart(2, '0');
const m = String(minutes % 60).padStart(2, '0');
return `${sign}${h}:${m}`;
}
get(unit) {
if (unit === 'year') {
return this.$date.getFullYear();
}
if (unit === 'month') {
return this.$date.getMonth() + 1;
}
if (unit === 'day') {
return this.$date.getDate();
}
if (unit === 'hour') {
return this.$date.getHours();
}
if (unit === 'minute') {
return this.$date.getMinutes();
}
if (unit === 'week') {
return getWeek(this.$date);
}
// second
return this.$date.getSeconds();
}
set(unit, v, opt = {}) {
const t = opt.mutate ? this : this.clone();
if (unit === 'year') {
t.$date.setFullYear(v);
}
else if (unit === 'month') {
t.$date.setMonth(v - 1);
}
else if (unit === 'day') {
t.$date.setDate(v);
}
else if (unit === 'hour') {
t.$date.setHours(v);
}
else if (unit === 'minute') {
t.$date.setMinutes(v);
}
else if (unit === 'second') {
t.$date.setSeconds(v);
}
else if (unit === 'week') {
setWeek(t.$date, v, { mutate: true });
}
return t;
}
get year() {
return this.$date.getFullYear();
}
setYear(v) {
return this.set('year', v);
}
get month() {
return this.$date.getMonth() + 1;
}
setMonth(v) {
return this.set('month', v);
}
get week() {
return getWeek(this.$date);
}
setWeek(v) {
return this.set('week', v);
}
get day() {
return this.$date.getDate();
}
setDay(v) {
return this.set('day', v);
}
get hour() {
return this.$date.getHours();
}
setHour(v) {
return this.set('hour', v);
}
get minute() {
return this.$date.getMinutes();
}
setMinute(v) {
return this.set('minute', v);
}
get second() {
return this.$date.getSeconds();
}
setSecond(v) {
return this.set('second', v);
}
/**
* Based on ISO: 1-7 is Mon-Sun.
*/
get dayOfWeek() {
return (this.$date.getDay() || 7);
}
/**
* Returns LocalTime for the given DayOfWeek (e.g Monday), that is in the same week as this.
* It may move the time into the future, or the past, depending on how the desired DayOfWeek is in
* relation to `this`.
*/
setDayOfWeek(dow) {
_assert(VALID_DAYS_OF_WEEK.has(dow), `Invalid dayOfWeek: ${dow}`);
const delta = dow - this.dayOfWeek;
return this.plus(delta, 'day');
}
/**
* Returns LocalTime for the given DayOfWeek (e.g Monday), that is in the future,
* in relation to this.
* If this LocalTime is Monday, and desired DoW is also Monday - `this` is returned.
*/
setNextDayOfWeek(dow) {
_assert(VALID_DAYS_OF_WEEK.has(dow), `Invalid dayOfWeek: ${dow}`);
let delta = dow - this.dayOfWeek;
if (delta < 0)
delta += 7;
return this.plus(delta, 'day');
}
setComponents(c, opt = {}) {
const d = opt.mutate ? this.$date : new Date(this.$date);
// Year, month and day set all-at-once, to avoid 30/31 (and 28/29) mishap
if (c.day || c.month !== undefined || c.year !== undefined) {
d.setFullYear(c.year ?? d.getFullYear(), c.month ? c.month - 1 : d.getMonth(), c.day || d.getDate());
}
if (c.hour !== undefined) {
d.setHours(c.hour);
}
if (c.minute !== undefined) {
d.setMinutes(c.minute);
}
if (c.second !== undefined) {
d.setSeconds(c.second);
}
return opt.mutate ? this : new LocalTime(d);
}
plusSeconds(num) {
return this.plus(num, 'second');
}
plusMinutes(num) {
return this.plus(num, 'minute');
}
plusHours(num) {
return this.plus(num, 'hour');
}
plusDays(num) {
return this.plus(num, 'day');
}
plusWeeks(num) {
return this.plus(num, 'week');
}
plusMonths(num) {
return this.plus(num, 'month');
}
plusYears(num) {
return this.plus(num, 'year');
}
minusSeconds(num) {
return this.plus(-num, 'second');
}
minusMinutes(num) {
return this.plus(-num, 'minute');
}
minusHours(num) {
return this.plus(-num, 'hour');
}
minusDays(num) {
return this.plus(-num, 'day');
}
minusWeeks(num) {
return this.plus(-num, 'week');
}
minusMonths(num) {
return this.plus(-num, 'month');
}
minusYears(num) {
return this.plus(-num, 'year');
}
plus(num, unit, opt = {}) {
if (unit === 'week') {
num *= 7;
unit = 'day';
}
if (unit === 'year' || unit === 'month') {
const d = addMonths(this.$date, unit === 'month' ? num : num * 12, opt);
return opt.mutate ? this : localTime.fromInput(d);
}
return this.set(unit, this.get(unit) + num, opt);
}
minus(num, unit, opt = {}) {
return this.plus(num * -1, unit, opt);
}
absDiff(other, unit) {
return Math.abs(this.diff(other, unit));
}
diff(other, unit) {
const date2 = localTime.fromInput(other).$date;
const secDiff = (this.$date.valueOf() - date2.valueOf()) / 1000;
if (!secDiff)
return 0;
let r;
if (unit === 'year') {
r = differenceInMonths(this.$date, date2) / 12;
}
else if (unit === 'month') {
r = differenceInMonths(this.$date, date2);
}
else if (unit === 'day') {
r = secDiff / SECONDS_IN_DAY;
}
else if (unit === 'week') {
r = secDiff / (7 * 24 * 60 * 60);
}
else if (unit === 'hour') {
r = secDiff / 3600;
}
else if (unit === 'minute') {
r = secDiff / 60;
}
else {
// unit === 'second'
r = secDiff;
}
// `|| 0` is to avoid returning -0
return Math.trunc(r) || 0;
}
startOf(unit, opt = {}) {
if (unit === 'second')
return this;
const d = opt.mutate ? this.$date : new Date(this.$date);
d.setSeconds(0, 0);
if (unit !== 'minute') {
d.setMinutes(0);
if (unit !== 'hour') {
d.setHours(0);
if (unit !== 'day') {
// year, month or week
if (unit === 'year') {
d.setMonth(0);
d.setDate(1);
}
else if (unit === 'month') {
d.setDate(1);
}
else {
// week
startOfWeek(d, { mutate: true });
}
}
}
}
return opt.mutate ? this : new LocalTime(d);
}
endOf(unit, opt = {}) {
if (unit === 'second')
return this;
const d = opt.mutate ? this.$date : new Date(this.$date);
d.setSeconds(59, 0);
if (unit !== 'minute') {
d.setMinutes(59);
if (unit !== 'hour') {
d.setHours(23);
if (unit !== 'day') {
// year, month or week
if (unit === 'year') {
d.setMonth(11);
}
if (unit === 'week') {
endOfWeek(d, { mutate: true });
}
else {
// year or month
const lastDay = localDate.getMonthLength(d.getFullYear(), d.getMonth() + 1);
d.setDate(lastDay);
}
}
}
}
return opt.mutate ? this : new LocalTime(d);
}
/**
* Returns how many days are in the current month.
* E.g 31 for January.
*/
get daysInMonth() {
return localDate.getMonthLength(this.$date.getFullYear(), this.$date.getMonth() + 1);
}
isSame(d) {
return this.compare(d) === 0;
}
isBefore(d, inclusive = false) {
const r = this.compare(d);
return r === -1 || (r === 0 && inclusive);
}
isSameOrBefore(d) {
return this.compare(d) <= 0;
}
isAfter(d, inclusive = false) {
const r = this.compare(d);
return r === 1 || (r === 0 && inclusive);
}
isSameOrAfter(d) {
return this.compare(d) >= 0;
}
isBetween(min, max, incl) {
let r = this.compare(min);
if (r < 0)
return false;
r = this.compare(max);
if (r > 0 || (r === 0 && incl[1] === ')'))
return false;
return true;
}
/**
* Checks if this localTime is older (<) than "now" by X units.
*
* Example:
*
* localTime(expirationDate).isOlderThan(5, 'day')
*
* Third argument allows to override "now".
*/
isOlderThan(n, unit, now) {
return this.isBefore(localTime.fromInput(now ?? new Date()).plus(-n, unit));
}
/**
* Checks if this localTime is same or older (<=) than "now" by X units.
*/
isSameOrOlderThan(n, unit, now) {
return this.isSameOrBefore(localTime.fromInput(now ?? new Date()).plus(-n, unit));
}
/**
* Checks if this localTime is younger (>) than "now" by X units.
*
* Example:
*
* localTime(expirationDate).isYoungerThan(5, 'day')
*
* Third argument allows to override "now".
*/
isYoungerThan(n, unit, now) {
return this.isAfter(localTime.fromInput(now ?? new Date()).plus(-n, unit));
}
/**
* Checks if this localTime is same or younger (>=) than "now" by X units.
*/
isSameOrYoungerThan(n, unit, now) {
return this.isSameOrAfter(localTime.fromInput(now ?? new Date()).plus(-n, unit));
}
getAgeInYears(now) {
return this.getAgeIn('year', now);
}
getAgeInMonths(now) {
return this.getAgeIn('month', now);
}
getAgeInDays(now) {
return this.getAgeIn('day', now);
}
getAgeInHours(now) {
return this.getAgeIn('hour', now);
}
getAgeInMinutes(now) {
return this.getAgeIn('minute', now);
}
getAgeInSeconds(now) {
return this.getAgeIn('second', now);
}
getAgeIn(unit, now) {
return localTime.fromInput(now ?? new Date()).diff(this, unit);
}
isAfterNow() {
return this.$date.valueOf() > Date.now();
}
isBeforeNow() {
return this.$date.valueOf() < Date.now();
}
/**
* Returns 1 if this > d
* returns 0 if they are equal
* returns -1 if this < d
*/
compare(d) {
const t1 = this.$date.valueOf();
const t2 = localTime.fromInput(d).$date.valueOf();
if (t1 === t2)
return 0;
return t1 < t2 ? -1 : 1;
}
toDateTimeObject() {
return {
...this.toDateObject(),
...this.toTimeObject(),
};
}
toDateObject() {
return {
year: this.$date.getFullYear(),
month: this.$date.getMonth() + 1,
day: this.$date.getDate(),
};
}
toTimeObject() {
return {
hour: this.$date.getHours(),
minute: this.$date.getMinutes(),
second: this.$date.getSeconds(),
};
}
toFromNowString(now = new Date()) {
const msDiff = localTime.fromInput(now).$date.valueOf() - this.$date.valueOf();
if (msDiff === 0)
return 'now';
if (msDiff >= 0) {
return `${_ms(msDiff)} ago`;
}
return `in ${_ms(msDiff * -1)}`;
}
toDate() {
return this.$date;
}
clone() {
return new LocalTime(new Date(this.$date));
}
get unix() {
return Math.floor(this.$date.valueOf() / 1000);
}
get unixMillis() {
return this.$date.valueOf();
}
valueOf() {
return Math.floor(this.$date.valueOf() / 1000);
}
toLocalDate() {
return localDate.fromDate(this.$date);
}
/**
* Returns e.g: `1984-06-21 17:56:21`
* or (if seconds=false):
* `1984-06-21 17:56`
*/
toPretty(seconds = true) {
return (this.toISODate() + ' ' + this.toISOTime(seconds));
// !! Not using toISOString(), as it returns time in UTC, not in local timezone (unexpected!)
// const s = this.$date.toISOString()
// return s.slice(0, 10) + ' ' + s.slice(11, seconds ? 19 : 16)
}
/**
* Returns e.g: `1984-06-21T17:56:21`
*/
toISODateTime() {
return (this.toISODate() + 'T' + this.toISOTime());
// !! Not using toISOString(), as it returns time in UTC, not in local timezone (unexpected!)
// return this.$date.toISOString().slice(0, 19)
}
/**
* Returns e.g: `1984-06-21`, only the date part of DateTime
*/
toISODate() {
const { year, month, day } = this.toDateObject();
return [
String(year).padStart(4, '0'),
String(month).padStart(2, '0'),
String(day).padStart(2, '0'),
].join('-');
// !! Not using toISOString(), as it returns time in UTC, not in local timezone (unexpected!)
// return this.$date.toISOString().slice(0, 10)
}
/**
* Returns e.g: `1984-06`
*/
toISOMonth() {
return this.toISODate().slice(0, 7);
}
/**
* Returns e.g: `17:03:15` (or `17:03` with seconds=false)
*/
toISOTime(seconds = true) {
const { hour, minute, second } = this.toTimeObject();
return [
String(hour).padStart(2, '0'),
String(minute).padStart(2, '0'),
seconds && String(second).padStart(2, '0'),
]
.filter(Boolean)
.join(':');
// !! Not using toISOString(), as it returns time in UTC, not in local timezone (unexpected!)
// return this.$date.toISOString().slice(11, seconds ? 19 : 16)
}
toWallTime() {
return new WallTime(this.toDateTimeObject());
}
/**
* Returns e.g: `19840621_1705`
*/
toStringCompact(seconds = false) {
const { year, month, day, hour, minute, second } = this.toDateTimeObject();
return [
String(year).padStart(4, '0'),
String(month).padStart(2, '0'),
String(day).padStart(2, '0'),
'_',
String(hour).padStart(2, '0'),
String(minute).padStart(2, '0'),
seconds ? String(second).padStart(2, '0') : '',
].join('');
}
toString() {
return this.toISODateTime();
}
toJSON() {
return this.unix;
}
format(fmt) {
if (fmt instanceof Intl.DateTimeFormat) {
return fmt.format(this.$date);
}
return fmt(this);
}
}
class LocalTimeFactory {
/**
* Creates a LocalTime from the input, unless it's falsy - then returns undefined.
*
* `localTime` function will instead return LocalTime of `now` for falsy input.
*/
orUndefined(input) {
return input || input === 0 ? this.fromInput(input) : undefined;
}
/**
* Creates a LocalTime from the input, unless it's falsy - then returns LocalTime.now
*/
orNow(input) {
return input || input === 0 ? this.fromInput(input) : this.now();
}
now() {
return new LocalTime(new Date());
}
/**
Convenience function to return current Unix timestamp in seconds.
Like Date.now(), but in seconds.
*/
nowUnix() {
return Math.floor(Date.now() / 1000);
}
/**
Convenience function that retuns the same as Date.now(), but with proper type of UnixTimestampMillis.
*/
nowUnixMillis() {
return Date.now();
}
/**
* Create LocalTime from LocalTimeInput.
* Input can already be a LocalTime - it is returned as-is in that case.
* Date - will be used as-is.
* String - will be parsed as strict `yyyy-mm-ddThh:mm:ss`.
* Number - will be treated as unix timestamp in seconds.
*/
fromInput(input) {
if (input instanceof LocalTime)
return input;
if (input instanceof Date) {
return this.fromDate(input);
}
if (typeof input === 'number') {
return this.fromUnix(input);
}
// It means it's a string
// Will parse it STRICTLY
return this.fromIsoDateTimeString(input);
}
/**
* Returns true if input is valid to create LocalTime.
*/
isValid(input) {
if (!input)
return false;
if (input instanceof LocalTime)
return true;
if (input instanceof Date)
return !Number.isNaN(input.getDate());
// We currently don't validate Unixtimestamp input, treat it as always valid
if (typeof input === 'number')
return true;
return this.isValidString(input);
}
/**
* Returns true if isoString is a valid iso8601 string like `yyyy-mm-ddThh:mm:dd`.
*/
isValidString(isoString) {
return !!this.parseStrictlyOrUndefined(isoString);
}
/**
* Tries to convert/parse the input into LocalTime.
* Uses LOOSE parsing.
* If invalid - doesn't throw, but returns undefined instead.
*/
try(input) {
if (input instanceof LocalTime)
return input;
if (input instanceof Date) {
if (Number.isNaN(input.getDate()))
return;
return new LocalTime(input);
}
if (typeof input === 'number') {
return this.fromUnix(input);
}
if (!input)
return;
const date = this.parseLooselyOrUndefined(input);
return date ? new LocalTime(date) : undefined;
}
/**
* Performs STRICT parsing.
* Only allows IsoDateTime or IsoDate input, nothing else.
*/
fromIsoDateTimeString(s) {
const d = this.parseStrictlyOrUndefined(s);
_assert(d, `Cannot parse "${s}" into LocalTime`);
return new LocalTime(d);
}
/**
* Performs LOOSE parsing.
* Tries to coerce imprefect/incorrect string input into IsoDateTimeString.
* Use with caution.
* Allows to input IsoDate, will set h:m:s to zeros.
*/
parse(s) {
const d = this.parseLooselyOrUndefined(String(s));
_assert(d, `Cannot parse "${s}" into LocalTime`);
return new LocalTime(d);
}
parseStrictlyOrUndefined(s) {
if (!s || typeof s !== 'string')
return;
let m = DATE_TIME_REGEX_STRICT.exec(s);
if (!m) {
// DateTime regex didn't match, try just-Date regex
m = DATE_REGEX_STRICT.exec(s);
if (!m)
return;
}
const o = {
year: Number(m[1]),
month: Number(m[2]),
day: Number(m[3]),
hour: Number(m[4]) || 0,
minute: Number(m[5]) || 0,
second: Number(m[6]) || 0,
};
if (!this.isDateTimeObjectValid(o))
return;
return this.createDateFromDateTimeObject(o);
}
parseLooselyOrUndefined(s) {
if (!s || typeof s !== 'string')
return;
const m = DATE_TIME_REGEX_LOOSE.exec(s);
if (!m) {
if (s.length < 8)
return;
// Attempt to parse with Date constructor
const d = new Date(s);
return Number.isNaN(d.getDate()) ? undefined : d;
}
const o = {
year: Number(m[1]),
month: Number(m[2]),
day: Number(m[3]) || 1,
// [4] is skipped due to extra regex parentheses group
hour: Number(m[5]) || 0,
minute: Number(m[6]) || 0,
second: Number(m[7]) || 0,
};
if (!this.isDateTimeObjectValid(o))
return;
return this.createDateFromDateTimeObject(o);
}
/**
* Throws on invalid value.
*/
validateDateTimeObject(o) {
_assert(this.isDateTimeObjectValid(o), `Cannot construct LocalTime from: ${o.year}-${o.month}-${o.day} ${o.hour}:${o.minute}:${o.second}`);
}
isDateTimeObjectValid(o) {
return localDate.isDateObjectValid(o) && this.isTimeObjectValid(o);
}
isTimeObjectValid({ hour, minute, second }) {
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 && second >= 0 && second <= 59;
}
fromDate(date) {
_assert(!Number.isNaN(date.getDate()), 'localTime.fromDate is called on Date object that is invalid');
return new LocalTime(date);
}
fromUnix(ts) {
return new LocalTime(new Date(ts * 1000));
}
/**
* Create LocalTime from unixTimestamp in milliseconds (not in seconds).
*/
fromMillis(millis) {
return new LocalTime(new Date(millis));
}
fromDateTimeObject(o) {
// todo: validate?
return new LocalTime(this.createDateFromDateTimeObject(o));
}
createDateFromDateTimeObject(o) {
return new Date(o.year, o.month - 1, o.day || 1, o.hour || 0, o.minute || 0, o.second || 0);
}
// private assertNotNull(
// lt: LocalTime | null,
// input: LocalTimeInputNullable,
// ): asserts lt is LocalTime {
// _assert(lt !== null, `Cannot parse "${input}" into LocalTime`, {
// input,
// })
// }
/**
* Returns the IANA timezone e.g `Europe/Stockholm`.
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*/
getTimezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
/**
* Returns true if passed IANA timezone is valid/supported.
* E.g `Europe/Stockholm` is valid, but `Europe/Stockholm2` is not.
*
* This implementation is not optimized for performance. If you need frequent validation -
* consider caching the Intl.supportedValuesOf values as Set and reuse that.
*/
isTimezoneValid(tz) {
if (tz === 'UTC')
return true; // we deliberately consider UTC a valid timezone, while it's mostly used in testing
return Intl.supportedValuesOf('timeZone').includes(tz);
}
sort(items, dir = 'asc', opt = {}) {
const mod = dir === 'desc' ? -1 : 1;
return (opt.mutate ? items : [...items]).sort((a, b) => {
const v1 = a.$date.valueOf();
const v2 = b.$date.valueOf();
if (v1 === v2)
return 0;
return (v1 < v2 ? -1 : 1) * mod;
});
}
minOrUndefined(items) {
let min;
for (const item of items) {
if (!item)
continue;
const lt = this.fromInput(item);
if (!min || lt.$date.valueOf() < min.$date.valueOf()) {
min = lt;
}
}
return min;
}
min(items) {
const min = this.minOrUndefined(items);
_assert(min, 'localTime.min called on empty array');
return min;
}
maxOrUndefined(items) {
let max;
for (const item of items) {
if (!item)
continue;
const lt = this.fromInput(item);
if (!max || lt.$date.valueOf() > max.$date.valueOf()) {
max = lt;
}
}
return max;
}
max(items) {
const max = this.maxOrUndefined(items);
_assert(max, 'localTime.max called on empty array');
return max;
}
}
// based on: https://github.com/date-fns/date-fns/blob/master/src/getISOWeek/index.ts
function getWeek(date) {
const diff = startOfWeek(date).getTime() - startOfWeekYear(date).getTime();
return Math.round(diff / MILLISECONDS_IN_WEEK) + 1;
}
function setWeek(date, week, opt = {}) {
const d = opt.mutate ? date : new Date(date);
const diff = getWeek(d) - week;
d.setDate(d.getDate() - diff * 7);
return d;
}
// based on: https://github.com/date-fns/date-fns/blob/master/src/startOfISOWeekYear/index.ts
function startOfWeekYear(date) {
const year = getWeekYear(date);
const fourthOfJanuary = new Date(0);
fourthOfJanuary.setFullYear(year, 0, 4);
fourthOfJanuary.setHours(0, 0, 0, 0);
return startOfWeek(fourthOfJanuary, { mutate: true });
}
// based on: https://github.com/date-fns/date-fns/blob/fd6bb1a0bab143f2da068c05a9c562b9bee1357d/src/getISOWeekYear/index.ts
function getWeekYear(date) {
const year = date.getFullYear();
const fourthOfJanuaryOfNextYear = new Date(0);
fourthOfJanuaryOfNextYear.setFullYear(year + 1, 0, 4);
fourthOfJanuaryOfNextYear.setHours(0, 0, 0, 0);
const startOfNextYear = startOfWeek(fourthOfJanuaryOfNextYear, { mutate: true });
const fourthOfJanuaryOfThisYear = new Date(0);
fourthOfJanuaryOfThisYear.setFullYear(year, 0, 4);
fourthOfJanuaryOfThisYear.setHours(0, 0, 0, 0);
const startOfThisYear = startOfWeek(fourthOfJanuaryOfThisYear, { mutate: true });
if (date.getTime() >= startOfNextYear.getTime()) {
return year + 1;
}
if (date.getTime() >= startOfThisYear.getTime()) {
return year;
}
return year - 1;
}
// based on: https://github.com/date-fns/date-fns/blob/fd6bb1a0bab143f2da068c05a9c562b9bee1357d/src/startOfWeek/index.ts
function startOfWeek(date, opt = {}) {
const d = opt.mutate ? date : new Date(date);
const day = d.getDay();
const diff = (day < weekStartsOn ? 7 : 0) + day - weekStartsOn;
d.setDate(d.getDate() - diff);
d.setHours(0, 0, 0, 0);
return d;
}
// based on: https://github.com/date-fns/date-fns/blob/master/src/endOfWeek/index.ts
function endOfWeek(date, opt = {}) {
const d = opt.mutate ? date : new Date(date);
const day = d.getDay();
const diff = (day < weekStartsOn ? -7 : 0) + 6 - (day - weekStartsOn);
d.setDate(d.getDate() + diff);
return d;
}
function addMonths(d, num, opt = {}) {
if (!opt.mutate)
d = new Date(d);
let day = d.getDate();
let month = d.getMonth() + 1 + num;
if (day < 29) {
d.setMonth(month - 1);
return d;
}
let year = d.getFullYear();
while (month > 12) {
year++;
month -= 12;
}
while (month < 1) {
year--;
month += 12;
}
const monthLen = localDate.getMonthLength(year, month);
if (day > monthLen)
day = monthLen;
d.setFullYear(year, month - 1, day);
return d;
}
function differenceInMonths(a, b) {
if (a.getDate() < b.getDate())
return -differenceInMonths(b, a);
const wholeMonthDiff = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
const anchor = addMonths(a, wholeMonthDiff).getTime();
const sign = b.getTime() - anchor >= 0 ? 1 : -1;
const anchor2 = addMonths(a, wholeMonthDiff + sign).getTime();
return -(wholeMonthDiff + ((b.getTime() - anchor) / (anchor2 - anchor)) * sign);
}
const localTimeFactory = new LocalTimeFactory();
export const localTime = localTimeFactory.fromInput.bind(localTimeFactory);
// The line below is the blackest of black magic I have ever written in 2024.
// And probably 2023 as well.
Object.setPrototypeOf(localTime, localTimeFactory);