millis-js
Version:
A tiny and dependency-free datetime library with a chainable and immutable API
939 lines (933 loc) • 26.2 kB
JavaScript
'use strict';
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;
};
const pad = (value, length) => {
return value.toString().padStart(length, "0");
};
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;
};
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)) {
const millis = (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;
return new Duration(millis);
}
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() / (60 * 60 * 1e3);
return this.return(value, options);
}
/**
* Returns the number of days of the `Duration` object.
*/
days(options) {
const value = this.millis() / (24 * 60 * 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 / (24 * 60 * 60 * 1e3));
const hours = Math.floor(
this.value % (24 * 60 * 60 * 1e3) / (60 * 60 * 1e3)
);
const minutes = Math.floor(this.value % (60 * 60 * 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;
}
}
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();
const start = now;
const end = now.plus({ days });
return new Interval(start, end);
}
/**
* 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;
const days = Duration.between(startDate, endDate).days({
round: "down"
});
return range(0, days + 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;
const endDate = this.end.isStartOfYear() ? this.end.minus({ millis: 1 }) : this.end;
const years = endDate.year() - startDate.year();
return range(0, years + 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);
}
}
const YYYY_REGEX = /^\d{4}$/;
const YYYY_MM_DD_REGEX = /^\d{4}-\d{2}-\d{2}$/;
const YYYY_DDD_REGEX = /^\d{4}-\d{3}$/;
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();
const length = str.length;
switch (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 date2 = new Date(
Date.UTC(
year,
0,
// January
1,
// First day
hour,
minute,
second,
millisecond
)
);
date2.setUTCDate(dayOfYear);
if (date2.getUTCFullYear() !== year) {
throw new Error(
`Invalid day of year: ${dayOfYear} for year ${year}`
);
}
return new DateTime(date2.getTime());
}
const { month, dayOfMonth } = dateTime;
const date = new Date(
Date.UTC(
year,
month !== undefined ? month - 1 : undefined,
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 (format instanceof Intl.DateTimeFormat)
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 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());
}
}
exports.DateTime = DateTime;
exports.Duration = Duration;
exports.Interval = Interval;