millis-js
Version:
A tiny and dependency-free datetime library with a chainable and immutable API
812 lines (807 loc) • 24 kB
JavaScript
//#region src/utils.ts
/**
* Parses a string, number, or undefined value to a number.
*/
const parseNumber = (value) => {
if (!value) throw new Error("Invalid value");
const parsed = typeof value === "string" ? Number.parseInt(value) : value;
if (Number.isNaN(parsed)) throw new Error("Invalid value");
return parsed;
};
/**
* Pads a number with leading zeros to a given length.
*/
const pad = (value, length) => {
return value.toString().padStart(length, "0");
};
/**
* Creates an array of numbers progressing from start up to, but not including, end.
*/
function range(start, end) {
const length = Math.abs(end - start);
const step = end >= start ? 1 : -1;
return Array.from({ length }, (_, i) => start + i * step);
}
const isObject = (value) => {
return typeof value === "object" && value !== null;
};
/**
* Checks if a value is an Intl.DateTimeFormat instance.
*/
const isDateTimeFormat = (value) => {
return value instanceof Intl.DateTimeFormat || isObject(value) && "format" in value && typeof value.format === "function";
};
//#endregion
//#region src/duration.ts
/**
* A `Duration` represents a length of time.
*/
var Duration = class Duration {
value;
constructor(value) {
this.value = value;
}
/**
* Creates a `Duration` object from an absolute duration.
* @deprecated Use `Duration.from` instead.
*/
static of(duration) {
return Duration.from(duration);
}
/**
* Creates a `Duration` object from an absolute duration.
*/
static from(duration) {
if (typeof duration === "number") return new Duration(duration);
if (duration instanceof Duration) return new Duration(duration.millis());
if (isObject(duration)) return new Duration((duration.millis || 0) + (duration.seconds || 0) * 1e3 + (duration.minutes || 0) * 60 * 1e3 + (duration.hours || 0) * 60 * 60 * 1e3 + (duration.days || 0) * 24 * 60 * 60 * 1e3);
throw new Error(`Invalid type for duration: ${typeof duration}`, { cause: duration });
}
/**
* Creates a `Duration` object from the difference between two `DateTimeLike` objects.
* @deprecated Use `Duration.between` instead.
*/
static diff(start, end) {
return Duration.between(start, end);
}
/**
* Creates a `Duration` object from the difference between two `DateTimeLike` objects.
*/
static between(start, end) {
const startDate = DateTime.from(start);
const endDate = DateTime.from(end);
return Duration.from({ millis: endDate.millis() - startDate.millis() });
}
/**
* Creates a `Duration` object from a number of days.
*/
static days(days) {
return Duration.from({ days });
}
/**
* Creates a `Duration` object from a number of hours.
*/
static hours(hours) {
return Duration.from({ hours });
}
/**
* Creates a `Duration` object from a number of minutes.
*/
static minutes(minutes) {
return Duration.from({ minutes });
}
/**
* Creates a `Duration` object from a number of seconds.
*/
static seconds(seconds) {
return Duration.from({ seconds });
}
/**
* Creates a `Duration` object from a number of milliseconds.
*/
static millis(millis) {
return Duration.from({ millis });
}
/**
* Returns the number of milliseconds of the `Duration` object.
*/
millis() {
return this.value;
}
/**
* Returns the number of seconds of the `Duration` object.
*/
seconds(options) {
const value = this.millis() / 1e3;
return this.return(value, options);
}
/**
* Returns the number of minutes of the `Duration` object.
*/
minutes(options) {
const value = this.millis() / (60 * 1e3);
return this.return(value, options);
}
/**
* Returns the number of hours of the `Duration` object.
*/
hours(options) {
const value = this.millis() / (3600 * 1e3);
return this.return(value, options);
}
/**
* Returns the number of days of the `Duration` object.
*/
days(options) {
const value = this.millis() / (1440 * 60 * 1e3);
return this.return(value, options);
}
/**
* Returns the ISO string representation of the `Duration` object.
* Example: "P2DT4H12M30S"
*/
iso() {
const days = Math.floor(this.value / (1440 * 60 * 1e3));
const hours = Math.floor(this.value % (1440 * 60 * 1e3) / (3600 * 1e3));
const minutes = Math.floor(this.value % (3600 * 1e3) / (60 * 1e3));
const seconds = Math.floor(this.value % (60 * 1e3) / 1e3);
let iso = "P";
if (days > 0) iso += `${days}D`;
if (hours > 0 || minutes > 0 || seconds > 0) {
iso += "T";
if (hours > 0) iso += `${hours}H`;
if (minutes > 0) iso += `${minutes}M`;
if (seconds > 0) iso += `${seconds}S`;
}
return iso === "P" ? "PT0S" : iso;
}
/**
* Returns a new `Duration` object by adding a duration to the current `Duration` object.
*/
plus(duration) {
return new Duration(this.millis() + Duration.from(duration).millis());
}
/**
* Returns a new `Duration` object by subtracting a duration from the current `Duration` object.
*/
minus(duration) {
return new Duration(this.millis() - Duration.from(duration).millis());
}
/**
* Returns a new `Duration` object with the absolute value of the current `Duration` object.
*/
abs() {
return new Duration(Math.abs(this.millis()));
}
/**
* Returns the ISO string representation of the `Duration` object.
*/
toString() {
return this.iso();
}
/**
* Returns the milliseconds of the `Duration` object when coercing to a number
*/
valueOf() {
return this.millis();
}
return(value, options) {
const { round } = options ?? {};
if (round === true) return Math.round(value);
if (round === "up") return Math.ceil(value);
if (round === "down") return Math.floor(value);
return value;
}
};
//#endregion
//#region src/interval.ts
/**
* An `Interval` represents a time span between two `DateTime` objects.
*/
var Interval = class Interval {
start;
end;
constructor(start, end) {
this.start = start;
this.end = end;
}
/**
* Creates a new Interval between two DateTimeLike values
*/
static between(start, end) {
return new Interval(DateTime.from(start), DateTime.from(end));
}
/**
* Creates a new Interval starting from now and extending for the specified number of days
*/
static days(days) {
const now = DateTime.now();
return new Interval(now, now.plus({ days }));
}
/**
* Returns the number of milliseconds between the start and end of the interval.
*/
millis() {
return this.end.millis() - this.start.millis();
}
/**
* Returns the duration between the start and end of the interval.
*/
duration() {
return Duration.between(this.start, this.end);
}
/**
* Returns the start DateTime of the interval.
*/
starts() {
return this.start;
}
/**
* Returns the end DateTime of the interval.
*/
ends() {
return this.end;
}
/**
* Returns a new Interval with the start and end dates reversed.
*/
reverse() {
return new Interval(this.end, this.start);
}
/**
* Returns the ISO string representation of the interval.
*/
iso() {
return `${this.start.iso()}/${this.end.iso()}`;
}
toString() {
return this.iso();
}
/**
* Returns an array of DateTime objects for each day in the interval, going from start to end.
* The day of the end date is only included if it is not exactly at the start of a day.
*
* @example
* - `2024-01-01T00:00:00.000Z` to `2024-01-02T00:00:00.000Z` will include only `2024-01-01`
* - `2024-01-01T00:00:00.000Z` to `2024-01-02T23:59:59.999Z` will include both `2024-01-01` and `2024-01-02`
*/
days() {
const startDate = this.start;
const endDate = this.end.isStartOfDay() ? this.end.minus({ millis: 1 }) : this.end;
return range(0, Duration.between(startDate, endDate).days({ round: "down" }) + 1).map((day) => startDate.plus({ days: day }));
}
/**
* Returns an array of DateTime objects for each year in the interval, going from start to end.
* The year of the end date is only included if it is not exactly at the start of a year.
*
* @example
* - `2024-01-01T00:00:00.000Z` to `2025-01-01T00:00:00.000Z` will include only `2024`
* - `2024-01-01T00:00:00.000Z` to `2025-01-01T23:59:59.999Z` will include both `2024` and `2025`
*/
years() {
const startDate = this.start;
return range(0, (this.end.isStartOfYear() ? this.end.minus({ millis: 1 }) : this.end).year() - startDate.year() + 1).map((year) => startDate.plus({ years: year }));
}
/**
* Returns true if the Interval contains the specified DateTime.
*/
contains(dateTime) {
return this.start.isBefore(dateTime) && this.end.isAfter(dateTime);
}
/**
* Returns true if the Interval start date is before the Interval end date.
*/
isPositive() {
return this.start.isBefore(this.end);
}
/**
* Returns true if the Interval start date is after the Interval end date.
*/
isNegative() {
return this.start.isAfter(this.end);
}
};
//#endregion
//#region src/datetime.ts
const YYYY_REGEX = /^\d{4}$/;
const YYYY_MM_DD_REGEX = /^\d{4}-\d{2}-\d{2}$/;
const YYYY_DDD_REGEX = /^\d{4}-\d{3}$/;
/**
* A `DateTime` represents a point in time.
*/
var DateTime = class DateTime {
value;
constructor(value) {
if (Number.isNaN(value)) throw new Error("Invalid date time");
this.value = value;
}
/**
* Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).
*/
static millis() {
return Date.now();
}
/**
* Returns a new DateTime object representing the current date and time.
*/
static now() {
return new DateTime(DateTime.millis());
}
/**
* Returns a new DateTime object from a DateTimeLike value.
*/
static from(dateTime) {
if (typeof dateTime === "number") return new DateTime(dateTime);
if (typeof dateTime === "string") {
const str = dateTime.trim();
switch (str.length) {
case 4:
if (YYYY_REGEX.test(str)) return new DateTime(Date.parse(dateTime));
break;
case 10:
if (YYYY_MM_DD_REGEX.test(str)) return new DateTime(Date.parse(dateTime));
break;
case 8:
if (YYYY_DDD_REGEX.test(str)) {
const [year, dayOfYear] = str.split("-").map((s) => parseNumber(s));
return DateTime.from({
year,
dayOfYear
});
}
break;
default: return new DateTime(Date.parse(dateTime));
}
}
if (dateTime instanceof Date || isObject(dateTime) && "getTime" in dateTime) return new DateTime(dateTime.getTime());
if (dateTime instanceof DateTime) return new DateTime(dateTime.millis());
if (isObject(dateTime)) {
if ("year" in dateTime) {
const { year = 0, hour = 0, minute = 0, second = 0, millisecond = 0 } = dateTime;
if ("dayOfYear" in dateTime) {
const { dayOfYear } = dateTime;
const date$1 = new Date(Date.UTC(year, 0, 1, hour, minute, second, millisecond));
date$1.setUTCDate(dayOfYear);
if (date$1.getUTCFullYear() !== year) throw new Error(`Invalid day of year: ${dayOfYear} for year ${year}`);
return new DateTime(date$1.getTime());
}
const { month, dayOfMonth } = dateTime;
const date = new Date(Date.UTC(year, month !== void 0 ? month - 1 : void 0, dayOfMonth, hour, minute, second, millisecond));
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== dayOfMonth) throw new Error("Invalid date components");
return new DateTime(date.getTime());
}
}
throw new Error(`Invalid type for date time: ${typeof dateTime}`, { cause: dateTime });
}
/**
* Creates an Interval from this DateTime to the specified end DateTime
*/
until(end) {
return Interval.between(this, end);
}
/**
* Formats the DateTime according to the specified format
*/
format(format) {
if (isDateTimeFormat(format)) return format.format(this.date());
switch (format) {
case "YYYY": return `${pad(this.year(), 4)}`;
case "YYYY-DDD": return `${pad(this.year(), 4)}-${pad(this.dayOfYear(), 3)}`;
case "YYYY-MM-DD": return `${pad(this.year(), 4)}-${pad(this.month(), 2)}-${pad(this.dayOfMonth(), 2)}`;
case "HH:mm:ss": return `${pad(this.hour(), 2)}:${pad(this.minute(), 2)}:${pad(this.second(), 2)}`;
}
throw new Error(`Unsupported format: ${format}`);
}
/**
* Returns the number of milliseconds of the DateTime object.
*/
millis() {
return this.value;
}
/**
* Returns the ISO string representation of the DateTime object.
*/
iso() {
return new Date(this.millis()).toISOString();
}
/**
* Returns the number of seconds since the Unix Epoch. The value is floored to the nearest integer.
*/
timestamp() {
return Math.floor(this.millis() / 1e3);
}
/**
* Returns the JavaScript Date object representation of the DateTime object.
*/
date() {
return new Date(this.millis());
}
/**
* Returns the year for the current DateTime object.
*/
year() {
return this.date().getUTCFullYear();
}
/**
* Returns the day of the year (1-365/366) for the current DateTime object.
*/
dayOfYear() {
const currentDate = this.date();
const startOfYear = new Date(Date.UTC(currentDate.getUTCFullYear(), 0, 1));
return Math.floor((this.millis() - startOfYear.getTime()) / (1e3 * 60 * 60 * 24)) + 1;
}
/**
* Returns the day of the month (1-31) for the current DateTime object.
*/
dayOfMonth() {
return this.date().getUTCDate();
}
/**
* Returns the month of the year (1-12) for the current DateTime object.
*/
month() {
return this.date().getUTCMonth() + 1;
}
/**
* Returns the day of the week (1-7, Monday=1, Sunday=7) for the current DateTime object.
*/
dayOfWeek() {
const day = this.date().getUTCDay();
return day === 0 ? 7 : day;
}
/**
* Returns the hour of the day (0-23) for the current DateTime object.
*/
hour() {
return this.date().getUTCHours();
}
/**
* Returns the minute of the hour (0-59) for the current DateTime object.
*/
minute() {
return this.date().getUTCMinutes();
}
/**
* Returns the second of the minute (0-59) for the current DateTime object.
*/
second() {
return this.date().getUTCSeconds();
}
/**
* Returns the millisecond of the second (0-999) for the current DateTime object.
*/
millisecond() {
return this.date().getUTCMilliseconds();
}
/**
* Returns a new DateTime object by adding a duration to the current DateTime object.
*/
plus(duration) {
const millis = typeof duration === "number" ? duration : duration instanceof Duration ? duration.millis() : this.relativeDuration(duration, false) + this.absoluteDuration(duration);
return new DateTime(this.millis() + millis);
}
/**
* Returns a new DateTime object by subtracting a duration from the current DateTime object.
*/
minus(duration) {
const millis = typeof duration === "number" ? duration : duration instanceof Duration ? duration.millis() : this.relativeDuration(duration, true) + this.absoluteDuration(duration);
return new DateTime(this.millis() - millis);
}
/**
* Returns the ISO string representation of the DateTime object.
*/
toString() {
return this.iso();
}
/**
* Returns the milliseconds value when coercing to a number
*/
valueOf() {
return this.millis();
}
/**
* Returns a new DateTime object set to the start of the second
*/
startOfSecond() {
return this.startOf("second");
}
/**
* Returns a new DateTime object set to the start of the minute
*/
startOfMinute() {
return this.startOf("minute");
}
/**
* Returns a new DateTime object set to the start of the hour
*/
startOfHour() {
return this.startOf("hour");
}
/**
* Returns a new DateTime object set to the start of the day
*/
startOfDay() {
return this.startOf("day");
}
/**
* Returns a new DateTime object set to the start of the month
*/
startOfMonth() {
return this.startOf("month");
}
/**
* Returns a new DateTime object set to the start of the year
*/
startOfYear() {
return this.startOf("year");
}
/**
* Returns a new DateTime object set to the end of the second
*/
endOfSecond() {
return this.endOf("second");
}
/**
* Returns a new DateTime object set to the end of the minute
*/
endOfMinute() {
return this.endOf("minute");
}
/**
* Returns a new DateTime object set to the end of the hour
*/
endOfHour() {
return this.endOf("hour");
}
/**
* Returns a new DateTime object set to the end of the day
*/
endOfDay() {
return this.endOf("day");
}
/**
* Returns a new DateTime object set to the end of the month
*/
endOfMonth() {
return this.endOf("month");
}
/**
* Returns a new DateTime object set to the end of the year
*/
endOfYear() {
return this.endOf("year");
}
/**
* Checks if the current DateTime is at the start of the second
*/
isStartOfSecond() {
return this.isStartOf("second");
}
/**
* Checks if the current DateTime is at the start of the minute
*/
isStartOfMinute() {
return this.isStartOf("minute");
}
/**
* Checks if the current DateTime is at the start of the hour
*/
isStartOfHour() {
return this.isStartOf("hour");
}
/**
* Checks if the current DateTime is at the start of the day
*/
isStartOfDay() {
return this.isStartOf("day");
}
/**
* Checks if the current DateTime is at the start of the month
*/
isStartOfMonth() {
return this.isStartOf("month");
}
/**
* Checks if the current DateTime is at the start of the year
*/
isStartOfYear() {
return this.isStartOf("year");
}
/**
* Checks if the current DateTime is at the end of the second
*/
isEndOfSecond() {
return this.isEndOf("second");
}
/**
* Checks if the current DateTime is at the end of the minute
*/
isEndOfMinute() {
return this.isEndOf("minute");
}
/**
* Checks if the current DateTime is at the end of the hour
*/
isEndOfHour() {
return this.isEndOf("hour");
}
/**
* Checks if the current DateTime is at the end of the day
*/
isEndOfDay() {
return this.isEndOf("day");
}
/**
* Checks if the current DateTime is at the end of the month
*/
isEndOfMonth() {
return this.isEndOf("month");
}
/**
* Checks if the current DateTime is at the end of the year
*/
isEndOfYear() {
return this.isEndOf("year");
}
/**
* Checks if the current DateTime is in the same second as the given DateTime
*/
isSameSecond(dateTime) {
return this.isSame(dateTime, "second");
}
/**
* Checks if the current DateTime is in the same minute as the given DateTime
*/
isSameMinute(dateTime) {
return this.isSame(dateTime, "minute");
}
/**
* Checks if the current DateTime is in the same hour as the given DateTime
*/
isSameHour(dateTime) {
return this.isSame(dateTime, "hour");
}
/**
* Checks if the current DateTime is in the same day as the given DateTime
*/
isSameDay(dateTime) {
return this.isSame(dateTime, "day");
}
/**
* Checks if the current DateTime is in the same month as the given DateTime
*/
isSameMonth(dateTime) {
return this.isSame(dateTime, "month");
}
/**
* Checks if the current DateTime is in the same year as the given DateTime
*/
isSameYear(dateTime) {
return this.isSame(dateTime, "year");
}
/**
* Returns true if the current DateTime is before the specified DateTime.
*/
isBefore(dateTime) {
return this < DateTime.from(dateTime);
}
/**
* Returns true if the current DateTime is after the specified DateTime.
*/
isAfter(dateTime) {
return this > DateTime.from(dateTime);
}
/**
* Returns true if the current DateTime is between the specified DateTime.
*/
isBetween(start, end) {
return this.isAfter(start) && this.isBefore(end);
}
/**
* Returns true if the current DateTime is equal to the specified DateTime.
*/
equals(dateTime) {
return this.millis() === DateTime.from(dateTime).millis();
}
/**
* Returns a comparison value of the current DateTime object to the given DateTime object to be used in sorting.
* The result is:
* - negative (< 0) if the current DateTime object is before the given DateTime object (a < b)
* - positive (> 0) if the current DateTime object is after the given DateTime object (a > b)
* - zero (0) if the current DateTime object and the given DateTime object are the same (a === b)
*
* @example
* const dates = [
* DateTime.from('2024-01-02T00:00:00.000Z'),
* DateTime.from('2024-01-01T00:00:00.000Z'),
* ];
*
* dates.sort((a, b) => a.compare(b));
*/
compare(dateTime) {
return this.millis() - DateTime.from(dateTime).millis();
}
/**
* Returns a new DateTime object set to the start of the specified unit
*/
startOf(unit) {
const date = this.date();
switch (unit) {
case "second": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), 0));
case "minute": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), 0, 0));
case "hour": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), 0, 0, 0));
case "day": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0));
case "month": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 0, 0, 0, 0));
case "year": return DateTime.from(Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0));
}
}
/**
* Returns a new DateTime object set to the end of the specified unit
*/
endOf(unit) {
const date = this.date();
switch (unit) {
case "second": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), 999));
case "minute": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), 59, 999));
case "hour": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), 59, 59, 999));
case "day": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 23, 59, 59, 999));
case "month": return DateTime.from(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0, 23, 59, 59, 999));
case "year": return DateTime.from(Date.UTC(date.getUTCFullYear(), 11, 31, 23, 59, 59, 999));
}
}
/**
* Checks if the current DateTime is at the start of the specified unit
*/
isStartOf(unit) {
return this.millis() === this.startOf(unit).millis();
}
/**
* Checks if the current DateTime is at the end of the specified unit
*/
isEndOf(unit) {
return this.millis() === this.endOf(unit).millis();
}
/**
* Checks if the current DateTime is in the same time unit as the given DateTime
*/
isSame(dateTime, unit) {
const other = DateTime.from(dateTime);
return this.startOf(unit).millis() === other.startOf(unit).millis();
}
/**
* Calculates the absolute duration in milliseconds between the current DateTime and the absolute duration.
*/
absoluteDuration(duration) {
const { millis, seconds, minutes, hours, days } = duration;
return Duration.from({
millis,
seconds,
minutes,
hours,
days
}).millis();
}
/**
* Calculates the duration in milliseconds between the current DateTime and the relative duration.
* The minus parameter is used to determine the direction of the duration.
* If minus is true, the duration is subtracted from the current DateTime.
* If minus is false, the duration is added to the current DateTime.
* In other words, are we going back in time or forward in time?
*/
relativeDuration(duration, minus) {
const { months = 0, years = 0 } = duration;
const currentDate = this.date();
const targetDate = new Date(currentDate);
const currentDay = targetDate.getUTCDate();
targetDate.setUTCDate(1);
targetDate.setUTCFullYear(targetDate.getUTCFullYear() + years * (minus ? -1 : 1));
targetDate.setUTCMonth(targetDate.getUTCMonth() + months * (minus ? -1 : 1));
const daysInMonth = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth() + 1, 0)).getUTCDate();
targetDate.setUTCDate(Math.min(currentDay, daysInMonth));
return Math.abs(Duration.between(currentDate, targetDate).millis());
}
};
//#endregion
exports.DateTime = DateTime;
exports.Duration = Duration;
exports.Interval = Interval;