UNPKG

millis-js

Version:

A tiny and dependency-free datetime library with a chainable and immutable API

935 lines (930 loc) 26.1 kB
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()); } } export { DateTime, Duration, Interval };