UNPKG

universal-common

Version:

Library that provides useful missing base class library functionality.

953 lines (856 loc) 33.9 kB
import ArgumentError from "./ArgumentError.js"; /** * Represents a time interval with nanosecond precision. * * The TimeSpan class provides a way to represent and manipulate time durations. * It stores time internally as 100-nanosecond intervals (ticks) using BigInt for full precision. * * @example * // Create a TimeSpan of 1 day, 2 hours, 30 minutes * const ts1 = new TimeSpan(1, 2, 30, 0); * * // Create a TimeSpan from hours * const ts2 = TimeSpan.fromHours(25.5); * * // Add two TimeSpans * const total = ts1.add(ts2); * * // Parse from string * const ts3 = TimeSpan.parse("1.02:30:00"); */ export default class TimeSpan { // Stored as BigInt internally for full precision #ticks; /** @type {number} Number of nanoseconds in one tick (100ns) */ static NANOSECONDS_PER_TICK = 100; /** @type {number} Number of ticks in one microsecond */ static TICKS_PER_MICROSECOND = 10; /** @type {number} Number of ticks in one millisecond */ static TICKS_PER_MILLISECOND = 10000; /** @type {number} Number of ticks in one second */ static TICKS_PER_SECOND = 10000000; /** @type {number} Number of ticks in one minute */ static TICKS_PER_MINUTE = 600000000; /** @type {number} Number of ticks in one hour */ static TICKS_PER_HOUR = 36000000000; /** @type {number} Number of ticks in one day */ static TICKS_PER_DAY = 864000000000; /** @type {number} Number of microseconds in one millisecond */ static MICROSECONDS_PER_MILLISECOND = 1000; /** @type {number} Number of microseconds in one second */ static MICROSECONDS_PER_SECOND = 1000000; /** @type {number} Number of microseconds in one minute */ static MICROSECONDS_PER_MINUTE = 60000000; /** @type {number} Number of microseconds in one hour */ static MICROSECONDS_PER_HOUR = 3600000000; /** @type {number} Number of microseconds in one day */ static MICROSECONDS_PER_DAY = 86400000000; /** @type {number} Number of milliseconds in one second */ static MILLISECONDS_PER_SECOND = 1000; /** @type {number} Number of milliseconds in one minute */ static MILLISECONDS_PER_MINUTE = 60000; /** @type {number} Number of milliseconds in one hour */ static MILLISECONDS_PER_HOUR = 3600000; /** @type {number} Number of milliseconds in one day */ static MILLISECONDS_PER_DAY = 86400000; /** @type {number} Number of seconds in one minute */ static SECONDS_PER_MINUTE = 60; /** @type {number} Number of seconds in one hour */ static SECONDS_PER_HOUR = 3600; /** @type {number} Number of seconds in one day */ static SECONDS_PER_DAY = 86400; /** @type {number} Number of minutes in one hour */ static MINUTES_PER_HOUR = 60; /** @type {number} Number of minutes in one day */ static MINUTES_PER_DAY = 1440; /** @type {number} Number of hours in one day */ static HOURS_PER_DAY = 24; /** @private @type {bigint} Minimum allowed ticks value */ static #MIN_TICKS = -9223372036854775808n; /** @private @type {bigint} Maximum allowed ticks value */ static #MAX_TICKS = 9223372036854775807n; /** @private @type {number} Minimum allowed milliseconds for safe conversion */ static #MIN_MILLISECONDS = -922337203685477; /** @private @type {number} Maximum allowed milliseconds for safe conversion */ static #MAX_MILLISECONDS = 922337203685477; /** @private @type {number} Minimum allowed seconds for safe conversion */ static #MIN_SECONDS = -922337203685; /** @private @type {number} Maximum allowed seconds for safe conversion */ static #MAX_SECONDS = 922337203685; /** @private @type {number} Minimum allowed minutes for safe conversion */ static #MIN_MINUTES = -15372286728; /** @private @type {number} Maximum allowed minutes for safe conversion */ static #MAX_MINUTES = 15372286728; /** @private @type {number} Minimum allowed hours for safe conversion */ static #MIN_HOURS = -256204778; /** @private @type {number} Maximum allowed hours for safe conversion */ static #MAX_HOURS = 256204778; /** @private @type {number} Minimum allowed days for safe conversion */ static #MIN_DAYS = -10675199; /** @private @type {number} Maximum allowed days for safe conversion */ static #MAX_DAYS = 10675199; /** * Creates a new TimeSpan instance. * * @constructor * @param {...(number|bigint)} args - Constructor arguments in one of the following formats: * - (ticks) - Single value representing 100-nanosecond intervals * - (hours, minutes, seconds) - Time components * - (days, hours, minutes, seconds) - Time components with days * - (days, hours, minutes, seconds, milliseconds) - Time components with milliseconds * - (days, hours, minutes, seconds, milliseconds, microseconds) - Full precision time components * * @throws {ArgumentError} If invalid number of arguments provided * @throws {RangeError} If resulting TimeSpan exceeds allowed range * * @example * // From ticks * const ts1 = new TimeSpan(10000000); // 1 second * * // From hours, minutes, seconds * const ts2 = new TimeSpan(1, 30, 45); // 1:30:45 * * // From days, hours, minutes, seconds * const ts3 = new TimeSpan(2, 12, 30, 0); // 2.12:30:00 */ constructor(...args) { if (args.length === 1) { // TimeSpan(ticks) - accept Number or BigInt const tickValue = typeof args[0] === "bigint" ? args[0] : BigInt(Math.floor(args[0])); if (tickValue > TimeSpan.#MAX_TICKS || tickValue < TimeSpan.#MIN_TICKS) { throw new RangeError("TimeSpan too long."); } this.#ticks = tickValue; } else if (args.length === 3) { // TimeSpan(hours, minutes, seconds) this.#ticks = BigInt(TimeSpan.#timeToTicks(args[0], args[1], args[2])); } else if (args.length === 4) { // TimeSpan(days, hours, minutes, seconds) this.#ticks = BigInt(TimeSpan.#timeToTicks(args[1], args[2], args[3])) + BigInt(args[0]) * BigInt(TimeSpan.TICKS_PER_DAY); } else if (args.length === 5) { // TimeSpan(days, hours, minutes, seconds, milliseconds) const totalMicroseconds = args[0] * TimeSpan.MICROSECONDS_PER_DAY + args[1] * TimeSpan.MICROSECONDS_PER_HOUR + args[2] * TimeSpan.MICROSECONDS_PER_MINUTE + args[3] * TimeSpan.MICROSECONDS_PER_SECOND + args[4] * TimeSpan.MICROSECONDS_PER_MILLISECOND; if (totalMicroseconds > Number.MAX_SAFE_INTEGER / TimeSpan.TICKS_PER_MICROSECOND || totalMicroseconds < Number.MIN_SAFE_INTEGER / TimeSpan.TICKS_PER_MICROSECOND) { throw new RangeError("TimeSpan too long."); } this.#ticks = BigInt(Math.floor(totalMicroseconds * TimeSpan.TICKS_PER_MICROSECOND)); } else if (args.length === 6) { // TimeSpan(days, hours, minutes, seconds, milliseconds, microseconds) const totalMicroseconds = args[0] * TimeSpan.MICROSECONDS_PER_DAY + args[1] * TimeSpan.MICROSECONDS_PER_HOUR + args[2] * TimeSpan.MICROSECONDS_PER_MINUTE + args[3] * TimeSpan.MICROSECONDS_PER_SECOND + args[4] * TimeSpan.MICROSECONDS_PER_MILLISECOND + args[5]; if (totalMicroseconds > Number.MAX_SAFE_INTEGER / TimeSpan.TICKS_PER_MICROSECOND || totalMicroseconds < Number.MIN_SAFE_INTEGER / TimeSpan.TICKS_PER_MICROSECOND) { throw new RangeError("TimeSpan too long."); } this.#ticks = BigInt(Math.floor(totalMicroseconds * TimeSpan.TICKS_PER_MICROSECOND)); } else { throw new ArgumentError("Invalid TimeSpan constructor arguments."); } if (this.#ticks > TimeSpan.#MAX_TICKS || this.#ticks < TimeSpan.#MIN_TICKS) { throw new RangeError("TimeSpan too long."); } } /** * Converts time components to ticks. * * @private * @param {number} hour - Hours component * @param {number} minute - Minutes component * @param {number} second - Seconds component * @returns {number} Total ticks * @throws {RangeError} If resulting value exceeds allowed range */ static #timeToTicks(hour, minute, second) { const totalSeconds = hour * TimeSpan.SECONDS_PER_HOUR + minute * TimeSpan.SECONDS_PER_MINUTE + second; if (totalSeconds > TimeSpan.#MAX_SECONDS || totalSeconds < TimeSpan.#MIN_SECONDS) { throw new RangeError("TimeSpan too long."); } return totalSeconds * TimeSpan.TICKS_PER_SECOND; } /** * Gets the value of this TimeSpan expressed in whole and fractional ticks. * * @type {number} * @readonly * @warning May lose precision if value exceeds Number.MAX_SAFE_INTEGER */ get ticks() { const num = Number(this.#ticks); return num; } /** * Gets the days component of this TimeSpan. * * @type {number} * @readonly * @example * const ts = new TimeSpan(2, 12, 30, 0); * console.log(ts.days); // 2 */ get days() { return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_DAY)); } /** * Gets the hours component of this TimeSpan (0-23). * * @type {number} * @readonly * @example * const ts = new TimeSpan(2, 12, 30, 0); * console.log(ts.hours); // 12 */ get hours() { return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_HOUR) % BigInt(TimeSpan.HOURS_PER_DAY)); } /** * Gets the minutes component of this TimeSpan (0-59). * * @type {number} * @readonly * @example * const ts = new TimeSpan(2, 12, 30, 0); * console.log(ts.minutes); // 30 */ get minutes() { return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_MINUTE) % BigInt(TimeSpan.MINUTES_PER_HOUR)); } /** * Gets the seconds component of this TimeSpan (0-59). * * @type {number} * @readonly * @example * const ts = new TimeSpan(0, 0, 0, 45); * console.log(ts.seconds); // 45 */ get seconds() { return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_SECOND) % BigInt(TimeSpan.SECONDS_PER_MINUTE)); } /** * Gets the milliseconds component of this TimeSpan (0-999). * * @type {number} * @readonly * @example * const ts = new TimeSpan(0, 0, 0, 0, 500); * console.log(ts.milliseconds); // 500 */ get milliseconds() { return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_MILLISECOND) % BigInt(TimeSpan.MILLISECONDS_PER_SECOND)); } /** * Gets the microseconds component of this TimeSpan (0-999). * * @type {number} * @readonly * @example * const ts = new TimeSpan(0, 0, 0, 0, 0, 123); * console.log(ts.microseconds); // 123 */ get microseconds() { return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_MICROSECOND) % BigInt(TimeSpan.MICROSECONDS_PER_MILLISECOND)); } /** * Gets the nanoseconds component of this TimeSpan (0-900). * Note: Resolution is limited to 100-nanosecond intervals. * * @type {number} * @readonly */ get nanoseconds() { return Number(this.#ticks % BigInt(TimeSpan.TICKS_PER_MICROSECOND)) * TimeSpan.NANOSECONDS_PER_TICK; } /** * Gets the total number of days represented by this TimeSpan. * * @type {number} * @readonly * @example * const ts = new TimeSpan(36, 0, 0, 0); // 36 hours * console.log(ts.totalDays); // 1.5 */ get totalDays() { return Number(this.#ticks) / TimeSpan.TICKS_PER_DAY; } /** * Gets the total number of hours represented by this TimeSpan. * * @type {number} * @readonly * @example * const ts = new TimeSpan(1, 12, 0, 0); * console.log(ts.totalHours); // 36 */ get totalHours() { return Number(this.#ticks) / TimeSpan.TICKS_PER_HOUR; } /** * Gets the total number of minutes represented by this TimeSpan. * * @type {number} * @readonly * @example * const ts = new TimeSpan(0, 1, 30, 0); * console.log(ts.totalMinutes); // 90 */ get totalMinutes() { return Number(this.#ticks) / TimeSpan.TICKS_PER_MINUTE; } /** * Gets the total number of seconds represented by this TimeSpan. * * @type {number} * @readonly * @example * const ts = new TimeSpan(0, 0, 2, 30); * console.log(ts.totalSeconds); // 150 */ get totalSeconds() { return Number(this.#ticks) / TimeSpan.TICKS_PER_SECOND; } /** * Gets the total number of milliseconds represented by this TimeSpan. * Clamped to safe millisecond range. * * @type {number} * @readonly * @example * const ts = new TimeSpan(0, 0, 0, 1, 500); * console.log(ts.totalMilliseconds); // 1500 */ get totalMilliseconds() { const ms = Number(this.#ticks) / TimeSpan.TICKS_PER_MILLISECOND; if (ms > TimeSpan.#MAX_MILLISECONDS) return TimeSpan.#MAX_MILLISECONDS; if (ms < TimeSpan.#MIN_MILLISECONDS) return TimeSpan.#MIN_MILLISECONDS; return ms; } /** * Gets the total number of microseconds represented by this TimeSpan. * * @type {number} * @readonly */ get totalMicroseconds() { return Number(this.#ticks) / TimeSpan.TICKS_PER_MICROSECOND; } /** * Gets the total number of nanoseconds represented by this TimeSpan. * * @type {number} * @readonly */ get totalNanoseconds() { return Number(this.#ticks) * TimeSpan.NANOSECONDS_PER_TICK; } /** * Internal getter for BigInt ticks (for internal operations). * * @private * @type {bigint} * @readonly */ get _ticksBigInt() { return this.#ticks; } /** * Returns a new TimeSpan that is the sum of this instance and the specified TimeSpan. * * @param {TimeSpan} ts - The TimeSpan to add * @returns {TimeSpan} A new TimeSpan representing the sum * @throws {TypeError} If argument is not a TimeSpan * @throws {RangeError} If result exceeds allowed range * * @example * const ts1 = TimeSpan.fromHours(2); * const ts2 = TimeSpan.fromMinutes(30); * const sum = ts1.add(ts2); // 2:30:00 */ add(ts) { if (!(ts instanceof TimeSpan)) { throw new TypeError("Argument must be a TimeSpan."); } const result = this.#ticks + ts.#ticks; if (result > TimeSpan.#MAX_TICKS || result < TimeSpan.#MIN_TICKS) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(result); } /** * Returns a new TimeSpan that is the difference between this instance and the specified TimeSpan. * * @param {TimeSpan} ts - The TimeSpan to subtract * @returns {TimeSpan} A new TimeSpan representing the difference * @throws {TypeError} If argument is not a TimeSpan * @throws {RangeError} If result exceeds allowed range * * @example * const ts1 = TimeSpan.fromHours(3); * const ts2 = TimeSpan.fromHours(1); * const diff = ts1.subtract(ts2); // 2:00:00 */ subtract(ts) { if (!(ts instanceof TimeSpan)) { throw new TypeError("Argument must be a TimeSpan."); } const result = this.#ticks - ts.#ticks; if (result > TimeSpan.#MAX_TICKS || result < TimeSpan.#MIN_TICKS) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(result); } /** * Returns a new TimeSpan whose value is the negated value of this instance. * * @returns {TimeSpan} A new TimeSpan with negated value * @throws {RangeError} If this instance equals MinValue * * @example * const ts = TimeSpan.fromHours(2); * const negated = ts.negate(); // -2:00:00 */ negate() { if (this.#ticks === TimeSpan.#MIN_TICKS) { throw new RangeError("Cannot negate minimum TimeSpan value."); } return new TimeSpan(-this.#ticks); } /** * Returns a new TimeSpan whose value is the absolute value of this instance. * * @returns {TimeSpan} A new TimeSpan with absolute value * @throws {RangeError} If this instance equals MinValue * * @example * const ts = TimeSpan.fromHours(-2); * const abs = ts.duration(); // 2:00:00 */ duration() { if (this.#ticks === TimeSpan.#MIN_TICKS) { throw new RangeError("Cannot get duration of minimum TimeSpan value."); } return new TimeSpan(this.#ticks >= 0n ? this.#ticks : -this.#ticks); } /** * Returns a new TimeSpan that is multiplied by the specified factor. * * @param {number} factor - The multiplication factor * @returns {TimeSpan} A new TimeSpan multiplied by factor * @throws {ArgumentError} If factor is NaN * @throws {RangeError} If result exceeds allowed range * * @example * const ts = TimeSpan.fromHours(2); * const doubled = ts.multiply(2); // 4:00:00 */ multiply(factor) { if (Number.isNaN(factor)) { throw new ArgumentError("Factor cannot be NaN."); } const ticks = Math.round(Number(this.#ticks) * factor); return TimeSpan.#intervalFromDoubleTicks(ticks); } /** * Returns a new TimeSpan that is divided by the specified divisor. * * @param {number} divisor - The divisor * @returns {TimeSpan} A new TimeSpan divided by divisor * @throws {ArgumentError} If divisor is NaN * @throws {RangeError} If result exceeds allowed range * * @example * const ts = TimeSpan.fromHours(4); * const halved = ts.divide(2); // 2:00:00 */ divide(divisor) { if (Number.isNaN(divisor)) { throw new ArgumentError("Divisor cannot be NaN."); } const ticks = Math.round(Number(this.#ticks) / divisor); return TimeSpan.#intervalFromDoubleTicks(ticks); } /** * Returns the ratio of this TimeSpan to the specified TimeSpan. * * @param {TimeSpan} ts - The divisor TimeSpan * @returns {number} The ratio of the two TimeSpans * @throws {TypeError} If argument is not a TimeSpan * * @example * const ts1 = TimeSpan.fromHours(6); * const ts2 = TimeSpan.fromHours(2); * const ratio = ts1.divideBy(ts2); // 3 */ divideBy(ts) { if (!(ts instanceof TimeSpan)) { throw new TypeError("Argument must be a TimeSpan."); } return Number(this.#ticks) / Number(ts.#ticks); } /** * Determines whether this instance is equal to a specified TimeSpan. * * @param {TimeSpan} other - The TimeSpan to compare with * @returns {boolean} true if the values are equal; otherwise, false * * @example * const ts1 = TimeSpan.fromHours(2); * const ts2 = TimeSpan.fromMinutes(120); * console.log(ts1.equals(ts2)); // true */ equals(other) { return other instanceof TimeSpan && this.#ticks === other.#ticks; } /** * Compares this instance to a specified TimeSpan and returns an indication of their relative values. * * @param {TimeSpan} other - The TimeSpan to compare with * @returns {number} -1 if this instance is less than other, 0 if equal, 1 if greater * @throws {TypeError} If argument is not a TimeSpan * * @example * const ts1 = TimeSpan.fromHours(1); * const ts2 = TimeSpan.fromHours(2); * console.log(ts1.compareTo(ts2)); // -1 */ compareTo(other) { if (!(other instanceof TimeSpan)) { throw new TypeError("Argument must be a TimeSpan."); } if (this.#ticks < other.#ticks) return -1; if (this.#ticks > other.#ticks) return 1; return 0; } /** * Converts the value of this TimeSpan to its string representation. * * @param {string} [format="c"] - The format string (currently only "c" is supported) * @returns {string} String representation of the TimeSpan * @throws {ArgumentError} If unsupported format is specified * * @example * const ts = new TimeSpan(1, 12, 30, 45, 123); * console.log(ts.toString()); // "1.12:30:45.123" */ toString(format) { if (!format || format === "c") { const negative = this.#ticks < 0n; const ts = negative ? this.negate() : this; const days = ts.days; const hours = ts.hours; const minutes = ts.minutes; const seconds = ts.seconds; const fraction = Number(ts.#ticks % BigInt(TimeSpan.TICKS_PER_SECOND)); let result = negative ? "-" : ""; if (days !== 0) { result += `${days}.`; } result += `${hours.toString().padStart(2, "0")}:`; result += `${minutes.toString().padStart(2, "0")}:`; result += seconds.toString().padStart(2, "0"); if (fraction !== 0) { result += `.${fraction.toString().padStart(7, "0").replace(/0+$/, "")}`; } return result; } throw new ArgumentError(`Unsupported format: ${format}`); } /** * Creates a TimeSpan that represents a specified number of days. * * @param {number} value - The number of days * @returns {TimeSpan} A TimeSpan representing the specified days * @throws {RangeError} If value exceeds allowed range * * @example * const ts = TimeSpan.fromDays(1.5); // 1 day and 12 hours */ static fromDays(value) { if (typeof value === "number") { return TimeSpan.#interval(value, TimeSpan.TICKS_PER_DAY); } if (value < TimeSpan.#MIN_DAYS || value > TimeSpan.#MAX_DAYS) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(BigInt(value) * BigInt(TimeSpan.TICKS_PER_DAY)); } /** * Creates a TimeSpan that represents a specified number of hours. * * @param {number} value - The number of hours * @returns {TimeSpan} A TimeSpan representing the specified hours * @throws {RangeError} If value exceeds allowed range * * @example * const ts = TimeSpan.fromHours(2.5); // 2 hours and 30 minutes */ static fromHours(value) { if (typeof value === "number" && arguments.length === 1) { return TimeSpan.#interval(value, TimeSpan.TICKS_PER_HOUR); } if (arguments.length === 1) { if (value < TimeSpan.#MIN_HOURS || value > TimeSpan.#MAX_HOURS) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(BigInt(value) * BigInt(TimeSpan.TICKS_PER_HOUR)); } throw new Error("Multi-parameter fromHours not implemented"); } /** * Creates a TimeSpan that represents a specified number of minutes. * * @param {number} value - The number of minutes * @returns {TimeSpan} A TimeSpan representing the specified minutes * @throws {RangeError} If value exceeds allowed range * * @example * const ts = TimeSpan.fromMinutes(90); // 1 hour and 30 minutes */ static fromMinutes(value) { if (typeof value === "number") { return TimeSpan.#interval(value, TimeSpan.TICKS_PER_MINUTE); } if (value < TimeSpan.#MIN_MINUTES || value > TimeSpan.#MAX_MINUTES) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(BigInt(value) * BigInt(TimeSpan.TICKS_PER_MINUTE)); } /** * Creates a TimeSpan that represents a specified number of seconds. * * @param {number} value - The number of seconds * @returns {TimeSpan} A TimeSpan representing the specified seconds * @throws {RangeError} If value exceeds allowed range * * @example * const ts = TimeSpan.fromSeconds(90); // 1 minute and 30 seconds */ static fromSeconds(value) { if (typeof value === "number") { return TimeSpan.#interval(value, TimeSpan.TICKS_PER_SECOND); } if (value < TimeSpan.#MIN_SECONDS || value > TimeSpan.#MAX_SECONDS) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(BigInt(value) * BigInt(TimeSpan.TICKS_PER_SECOND)); } /** * Creates a TimeSpan that represents a specified number of milliseconds. * * @param {number} value - The number of milliseconds * @returns {TimeSpan} A TimeSpan representing the specified milliseconds * @throws {RangeError} If value exceeds allowed range * * @example * const ts = TimeSpan.fromMilliseconds(1500); // 1.5 seconds */ static fromMilliseconds(value) { if (typeof value === "number") { return TimeSpan.#interval(value, TimeSpan.TICKS_PER_MILLISECOND); } if (value < TimeSpan.#MIN_MILLISECONDS || value > TimeSpan.#MAX_MILLISECONDS) { throw new RangeError("TimeSpan too long."); } return new TimeSpan(BigInt(value) * BigInt(TimeSpan.TICKS_PER_MILLISECOND)); } /** * Creates a TimeSpan that represents a specified number of microseconds. * * @param {number} value - The number of microseconds * @returns {TimeSpan} A TimeSpan representing the specified microseconds * * @example * const ts = TimeSpan.fromMicroseconds(1500); // 1.5 milliseconds */ static fromMicroseconds(value) { return TimeSpan.#interval(value, TimeSpan.TICKS_PER_MICROSECOND); } /** * Creates a TimeSpan that represents a specified number of ticks. * * @param {number|bigint} value - The number of 100-nanosecond intervals * @returns {TimeSpan} A TimeSpan representing the specified ticks * * @example * const ts = TimeSpan.fromTicks(10000000); // 1 second */ static fromTicks(value) { // Accept Number, convert to BigInt internally return new TimeSpan(typeof value === "bigint" ? value : BigInt(Math.floor(value))); } /** * Creates a TimeSpan from a value and scale factor. * * @private * @param {number} value - The value * @param {number} scale - The scale factor (ticks per unit) * @returns {TimeSpan} A new TimeSpan * @throws {ArgumentError} If value is NaN */ static #interval(value, scale) { if (Number.isNaN(value)) { throw new ArgumentError("Value cannot be NaN."); } return TimeSpan.#intervalFromDoubleTicks(value * scale); } /** * Creates a TimeSpan from a floating-point ticks value. * * @private * @param {number} ticks - The ticks as a floating-point number * @returns {TimeSpan} A new TimeSpan * @throws {RangeError} If ticks exceed allowed range */ static #intervalFromDoubleTicks(ticks) { if (Number.isNaN(ticks) || ticks > Number(TimeSpan.#MAX_TICKS) || ticks < Number(TimeSpan.#MIN_TICKS)) { throw new RangeError("TimeSpan too long."); } if (ticks === Number(TimeSpan.#MAX_TICKS)) { return TimeSpan.maxValue; } return new TimeSpan(BigInt(Math.floor(ticks))); } /** * Gets a TimeSpan that represents a zero time interval. * * @type {TimeSpan} * @readonly * @static * * @example * const zero = TimeSpan.zero; * console.log(zero.toString()); // "00:00:00" */ static get zero() { return new TimeSpan(0n); } /** * Gets the maximum TimeSpan value. * * @type {TimeSpan} * @readonly * @static */ static get maxValue() { return new TimeSpan(TimeSpan.#MAX_TICKS); } /** * Gets the minimum TimeSpan value. * * @type {TimeSpan} * @readonly * @static */ static get minValue() { return new TimeSpan(TimeSpan.#MIN_TICKS); } /** * Compares two TimeSpan values and returns an indication of their relative values. * * @param {TimeSpan} t1 - The first TimeSpan * @param {TimeSpan} t2 - The second TimeSpan * @returns {number} -1 if t1 is less than t2, 0 if equal, 1 if greater * * @example * const result = TimeSpan.compare(TimeSpan.fromHours(1), TimeSpan.fromHours(2)); // -1 */ static compare(t1, t2) { return t1.compareTo(t2); } /** * Returns a value indicating whether two TimeSpan instances are equal. * * @param {TimeSpan} t1 - The first TimeSpan * @param {TimeSpan} t2 - The second TimeSpan * @returns {boolean} true if the values are equal; otherwise, false * * @example * const equal = TimeSpan.equals(TimeSpan.fromHours(2), TimeSpan.fromMinutes(120)); // true */ static equals(t1, t2) { return t1.equals(t2); } /** * Returns the primitive value of this TimeSpan. * Used by JavaScript when converting to primitive types. * * @returns {number} The ticks value as a number * @warning May lose precision if value exceeds Number.MAX_SAFE_INTEGER */ valueOf() { // Return Number for standard JavaScript operations const num = Number(this.#ticks); return num; } /** * Parses a TimeSpan from its string representation. * * @param {string} s - The string to parse * @returns {TimeSpan} A TimeSpan parsed from the string * @throws {TypeError} If input is not a string * @throws {ArgumentError} If string format is invalid * * @example * const ts1 = TimeSpan.parse("1:30:45"); // 1 hour, 30 minutes, 45 seconds * const ts2 = TimeSpan.parse("2.12:30:00"); // 2 days, 12 hours, 30 minutes * const ts3 = TimeSpan.parse("-00:15:30.5"); // Negative 15 minutes, 30.5 seconds */ static parse(s) { if (typeof s !== "string") { throw new TypeError("Input must be a string."); } let negative = false; let parseString = s; if (s.startsWith("-")) { negative = true; parseString = s.substring(1); } // Try d.hh:mm:ss.fffffff format let match = parseString.match(/^(?:(\d+)\.)?(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?$/); if (match) { const days = match[1] ? parseInt(match[1], 10) : 0; const hours = parseInt(match[2], 10); const minutes = parseInt(match[3], 10); const seconds = parseInt(match[4], 10); const fraction = match[5] ? match[5].padEnd(7, "0") : "0000000"; const ticks = parseInt(fraction, 10); const totalTicks = BigInt(days) * BigInt(TimeSpan.TICKS_PER_DAY) + BigInt(hours) * BigInt(TimeSpan.TICKS_PER_HOUR) + BigInt(minutes) * BigInt(TimeSpan.TICKS_PER_MINUTE) + BigInt(seconds) * BigInt(TimeSpan.TICKS_PER_SECOND) + BigInt(ticks); return new TimeSpan(negative ? -totalTicks : totalTicks); } throw new ArgumentError("Invalid TimeSpan format."); } /** * Tries to parse a TimeSpan from its string representation. * * @param {string} s - The string to parse * @returns {{success: boolean, value: TimeSpan|null}} An object indicating success and the parsed value * * @example * const result = TimeSpan.tryParse("1:30:45"); * if (result.success) { * console.log(result.value.toString()); // "01:30:45" * } */ static tryParse(s) { try { return { success: true, value: TimeSpan.parse(s) }; } catch { return { success: false, value: null }; } } }