universal-common
Version:
Library that provides useful missing base class library functionality.
953 lines (856 loc) • 33.9 kB
JavaScript
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 };
}
}
}