universal-common
Version:
Library that provides useful missing base class library functionality.
744 lines (683 loc) • 29.5 kB
JavaScript
import ArgumentError from './ArgumentError.js';
import TimeSpan from './TimeSpan.js';
import DateTime from './DateTime.js';
/**
* Represents a time of day, as would be read from a clock, within the range 00:00:00 to 23:59:59.9999999.
*
* TimeOnly provides time-only functionality without date components, making it ideal
* for scenarios where only the time portion is relevant (schedules, opening hours, etc.).
*
* @example
* // Create TimeOnly instances
* const morning = new TimeOnly(9, 30); // 09:30:00
* const precise = new TimeOnly(14, 15, 30, 500); // 14:15:30.500
* const fromTicks = new TimeOnly(450000000000); // From ticks
*
* // Time arithmetic
* const later = morning.addHours(2.5); // 12:00:00
* const duration = later.subtract(morning); // 2:30:00 TimeSpan
*
* // Comparisons and ranges
* console.log(morning.isBetween(new TimeOnly(8, 0), new TimeOnly(17, 0))); // true
*
* // Formatting
* console.log(morning.toString()); // "09:30:00"
* console.log(precise.toString()); // "14:15:30.5"
*/
export default class TimeOnly {
/** @private @type {bigint} Internal ticks storage */
#ticks;
/** @private @type {bigint} Minimum time ticks (midnight) */
static #MIN_TIME_TICKS = 0n;
/** @private @type {bigint} Maximum time ticks (23:59:59.9999999) */
static #MAX_TIME_TICKS = BigInt(TimeSpan.TICKS_PER_DAY) - 1n;
/**
* Creates a new TimeOnly instance.
*
* @constructor
* @param {...number} args - Constructor arguments in various formats:
* - (ticks) - 100-nanosecond intervals since midnight
* - (hour, minute) - Hour and minute components
* - (hour, minute, second) - Hour, minute, and second components
* - (hour, minute, second, millisecond) - With milliseconds
* - (hour, minute, second, millisecond, microsecond) - With microseconds
*
* @throws {ArgumentError} If invalid number of arguments provided
* @throws {RangeError} If time components are out of valid range
*
* @example
* // From ticks
* const time1 = new TimeOnly(450000000000); // 12:30:00
*
* // From hour and minute
* const time2 = new TimeOnly(14, 30); // 14:30:00
*
* // From hour, minute, second
* const time3 = new TimeOnly(9, 15, 45); // 09:15:45
*
* // With milliseconds
* const time4 = new TimeOnly(16, 30, 0, 500); // 16:30:00.500
*/
constructor(...args) {
if (args.length === 1) {
// TimeOnly(ticks)
const ticks = typeof args[0] === 'bigint' ? args[0] : BigInt(Math.floor(args[0]));
if (ticks > TimeOnly.#MAX_TIME_TICKS || ticks < TimeOnly.#MIN_TIME_TICKS) {
throw new RangeError("TimeOnly ticks out of range.");
}
this.#ticks = ticks;
} else if (args.length === 2) {
// TimeOnly(hour, minute)
this.#ticks = TimeOnly.#timeToTicks(args[0], args[1], 0, 0);
} else if (args.length === 3) {
// TimeOnly(hour, minute, second)
this.#ticks = TimeOnly.#timeToTicks(args[0], args[1], args[2], 0);
} else if (args.length === 4) {
// TimeOnly(hour, minute, second, millisecond)
this.#ticks = TimeOnly.#timeToTicks(args[0], args[1], args[2], args[3]);
} else if (args.length === 5) {
// TimeOnly(hour, minute, second, millisecond, microsecond)
const hour = args[0];
const minute = args[1];
const second = args[2];
const millisecond = args[3];
const microsecond = args[4];
if (hour < 0 || hour > 23) throw new RangeError("Hour must be between 0 and 23.");
if (minute < 0 || minute > 59) throw new RangeError("Minute must be between 0 and 59.");
if (second < 0 || second > 59) throw new RangeError("Second must be between 0 and 59.");
if (millisecond < 0 || millisecond > 999) throw new RangeError("Millisecond must be between 0 and 999.");
if (microsecond < 0 || microsecond > 999) throw new RangeError("Microsecond must be between 0 and 999.");
this.#ticks = BigInt(hour) * BigInt(TimeSpan.TICKS_PER_HOUR) +
BigInt(minute) * BigInt(TimeSpan.TICKS_PER_MINUTE) +
BigInt(second) * BigInt(TimeSpan.TICKS_PER_SECOND) +
BigInt(millisecond) * BigInt(TimeSpan.TICKS_PER_MILLISECOND) +
BigInt(microsecond) * BigInt(TimeSpan.TICKS_PER_MICROSECOND);
} else {
throw new ArgumentError("Invalid TimeOnly constructor arguments.");
}
}
/**
* Converts time components to ticks.
*
* @private
* @param {number} hour - Hour component (0-23)
* @param {number} minute - Minute component (0-59)
* @param {number} second - Second component (0-59)
* @param {number} millisecond - Millisecond component (0-999)
* @returns {bigint} Total ticks representing the time
* @throws {RangeError} If any component is out of valid range
*/
static #timeToTicks(hour, minute, second, millisecond) {
if (hour < 0 || hour > 23) throw new RangeError("Hour must be between 0 and 23.");
if (minute < 0 || minute > 59) throw new RangeError("Minute must be between 0 and 59.");
if (second < 0 || second > 59) throw new RangeError("Second must be between 0 and 59.");
if (millisecond < 0 || millisecond > 999) throw new RangeError("Millisecond must be between 0 and 999.");
return BigInt(hour) * BigInt(TimeSpan.TICKS_PER_HOUR) +
BigInt(minute) * BigInt(TimeSpan.TICKS_PER_MINUTE) +
BigInt(second) * BigInt(TimeSpan.TICKS_PER_SECOND) +
BigInt(millisecond) * BigInt(TimeSpan.TICKS_PER_MILLISECOND);
}
/**
* Gets the hour component of the time represented by this instance (0-23).
*
* @type {number}
* @readonly
* @example
* const time = new TimeOnly(14, 30, 45);
* console.log(time.hour); // 14
*/
get hour() {
return Number(this.#ticks / BigInt(TimeSpan.TICKS_PER_HOUR));
}
/**
* Gets the minute component of the time represented by this instance (0-59).
*
* @type {number}
* @readonly
* @example
* const time = new TimeOnly(14, 30, 45);
* console.log(time.minute); // 30
*/
get minute() {
return Number((this.#ticks / BigInt(TimeSpan.TICKS_PER_MINUTE)) % BigInt(TimeSpan.MINUTES_PER_HOUR));
}
/**
* Gets the second component of the time represented by this instance (0-59).
*
* @type {number}
* @readonly
* @example
* const time = new TimeOnly(14, 30, 45);
* console.log(time.second); // 45
*/
get second() {
return Number((this.#ticks / BigInt(TimeSpan.TICKS_PER_SECOND)) % BigInt(TimeSpan.SECONDS_PER_MINUTE));
}
/**
* Gets the millisecond component of the time represented by this instance (0-999).
*
* @type {number}
* @readonly
* @example
* const time = new TimeOnly(14, 30, 45, 500);
* console.log(time.millisecond); // 500
*/
get millisecond() {
return Number((this.#ticks / BigInt(TimeSpan.TICKS_PER_MILLISECOND)) % BigInt(TimeSpan.MILLISECONDS_PER_SECOND));
}
/**
* Gets the microsecond component of the time represented by this instance (0-999).
*
* @type {number}
* @readonly
* @example
* const time = new TimeOnly(14, 30, 45, 500, 250);
* console.log(time.microsecond); // 250
*/
get microsecond() {
return Number((this.#ticks / BigInt(TimeSpan.TICKS_PER_MICROSECOND)) % BigInt(TimeSpan.MICROSECONDS_PER_MILLISECOND));
}
/**
* Gets the nanosecond component of the time represented by this instance (0-900).
* Note: Resolution is limited to 100-nanosecond intervals.
*
* @type {number}
* @readonly
*/
get nanosecond() {
return Number(this.#ticks % BigInt(TimeSpan.TICKS_PER_MICROSECOND)) * TimeSpan.NANOSECONDS_PER_TICK;
}
/**
* Gets the number of ticks that represent the time of this instance.
*
* @type {number}
* @readonly
*/
get ticks() {
return Number(this.#ticks);
}
/**
* Adds ticks to this TimeOnly, wrapping around midnight if necessary.
*
* @private
* @param {number|bigint} ticks - The ticks to add
* @returns {TimeOnly} A new TimeOnly with the added ticks
*/
#addTicks(ticks) {
const ticksBigInt = typeof ticks === 'bigint' ? ticks : BigInt(Math.floor(ticks));
const ticksPerDay = BigInt(TimeSpan.TICKS_PER_DAY);
// Add ticks and handle wrapping
let newTicks = (this.#ticks + ticksPerDay + (ticksBigInt % ticksPerDay)) % ticksPerDay;
return new TimeOnly(newTicks);
}
/**
* Adds ticks to this TimeOnly, returning wrapped days as an output parameter.
*
* @private
* @param {number|bigint} ticks - The ticks to add
* @returns {{time: TimeOnly, wrappedDays: number}} Object containing the new time and wrapped days
*/
#addTicksWithWrappedDays(ticks) {
const ticksBigInt = typeof ticks === 'bigint' ? ticks : BigInt(Math.floor(ticks));
const ticksPerDay = BigInt(TimeSpan.TICKS_PER_DAY);
// Calculate days and new ticks
const totalTicks = this.#ticks + ticksBigInt;
let days = totalTicks / ticksPerDay;
let newTicks = totalTicks % ticksPerDay;
if (newTicks < 0n) {
days--;
newTicks += ticksPerDay;
}
return {
time: new TimeOnly(newTicks),
wrappedDays: Number(days)
};
}
/**
* Returns a new TimeOnly that adds the value of the specified TimeSpan to the value of this instance.
*
* @param {TimeSpan} value - A positive or negative time interval
* @returns {TimeOnly} An object whose value is the sum of the time represented by this instance and the time interval represented by value
* @throws {TypeError} If value is not a TimeSpan
*
* @example
* const morning = new TimeOnly(9, 0, 0);
* const duration = TimeSpan.fromHours(2.5);
* const later = morning.add(duration); // 11:30:00
*/
add(value) {
if (!(value instanceof TimeSpan)) {
throw new TypeError("Argument must be a TimeSpan.");
}
return this.#addTicks(value.ticks);
}
/**
* Returns a new TimeOnly that adds the value of the specified TimeSpan to the value of this instance.
* If the result wraps past the end of the day, this method will return the number of excess days.
*
* @param {TimeSpan} value - A positive or negative time interval
* @returns {{time: TimeOnly, wrappedDays: number}} Object containing the new time and wrapped days
* @throws {TypeError} If value is not a TimeSpan
*
* @example
* const evening = new TimeOnly(22, 0, 0);
* const duration = TimeSpan.fromHours(4);
* const result = evening.addWithWrappedDays(duration);
* console.log(result.time.toString()); // "02:00:00"
* console.log(result.wrappedDays); // 1
*/
addWithWrappedDays(value) {
if (!(value instanceof TimeSpan)) {
throw new TypeError("Argument must be a TimeSpan.");
}
return this.#addTicksWithWrappedDays(value.ticks);
}
/**
* Returns a new TimeOnly that adds the specified number of hours to the value of this instance.
*
* @param {number} value - A number of whole and fractional hours. The value parameter can be negative or positive
* @returns {TimeOnly} An object whose value is the sum of the time represented by this instance and the number of hours represented by value
*
* @example
* const time = new TimeOnly(10, 30, 0);
* const later = time.addHours(2.5); // 13:00:00
*/
addHours(value) {
return this.#addTicks(value * TimeSpan.TICKS_PER_HOUR);
}
/**
* Returns a new TimeOnly that adds the specified number of hours to the value of this instance.
* If the result wraps past the end of the day, this method will return the number of excess days.
*
* @param {number} value - A number of whole and fractional hours. The value parameter can be negative or positive
* @returns {{time: TimeOnly, wrappedDays: number}} Object containing the new time and wrapped days
*
* @example
* const time = new TimeOnly(22, 0, 0);
* const result = time.addHoursWithWrappedDays(4);
* console.log(result.time.toString()); // "02:00:00"
* console.log(result.wrappedDays); // 1
*/
addHoursWithWrappedDays(value) {
return this.#addTicksWithWrappedDays(value * TimeSpan.TICKS_PER_HOUR);
}
/**
* Returns a new TimeOnly that adds the specified number of minutes to the value of this instance.
*
* @param {number} value - A number of whole and fractional minutes. The value parameter can be negative or positive
* @returns {TimeOnly} An object whose value is the sum of the time represented by this instance and the number of minutes represented by value
*
* @example
* const time = new TimeOnly(10, 30, 0);
* const later = time.addMinutes(45); // 11:15:00
*/
addMinutes(value) {
return this.#addTicks(value * TimeSpan.TICKS_PER_MINUTE);
}
/**
* Returns a new TimeOnly that adds the specified number of minutes to the value of this instance.
* If the result wraps past the end of the day, this method will return the number of excess days.
*
* @param {number} value - A number of whole and fractional minutes. The value parameter can be negative or positive
* @returns {{time: TimeOnly, wrappedDays: number}} Object containing the new time and wrapped days
*
* @example
* const time = new TimeOnly(23, 30, 0);
* const result = time.addMinutesWithWrappedDays(45);
* console.log(result.time.toString()); // "00:15:00"
* console.log(result.wrappedDays); // 1
*/
addMinutesWithWrappedDays(value) {
return this.#addTicksWithWrappedDays(value * TimeSpan.TICKS_PER_MINUTE);
}
/**
* Determines if a time falls within the range provided.
* Supports both "normal" ranges such as 10:00-12:00, and ranges that span midnight such as 23:00-01:00.
*
* @param {TimeOnly} start - The starting time of day, inclusive
* @param {TimeOnly} end - The ending time of day, exclusive
* @returns {boolean} True, if the time falls within the range, false otherwise
* @throws {TypeError} If start or end is not a TimeOnly instance
*
* @example
* const workStart = new TimeOnly(9, 0, 0);
* const workEnd = new TimeOnly(17, 0, 0);
* const currentTime = new TimeOnly(14, 30, 0);
*
* console.log(currentTime.isBetween(workStart, workEnd)); // true
*
* // Midnight-spanning range
* const nightStart = new TimeOnly(22, 0, 0);
* const nightEnd = new TimeOnly(6, 0, 0);
* const lateNight = new TimeOnly(1, 0, 0);
*
* console.log(lateNight.isBetween(nightStart, nightEnd)); // true
*/
isBetween(start, end) {
if (!(start instanceof TimeOnly) || !(end instanceof TimeOnly)) {
throw new TypeError("Arguments must be TimeOnly instances.");
}
const time = this.#ticks;
const startTicks = start.#ticks;
const endTicks = end.#ticks;
if (startTicks <= endTicks) {
// Normal range (doesn't cross midnight)
// Start is inclusive, end is exclusive: [start, end)
return time >= startTicks && time < endTicks;
} else {
// Range crosses midnight (e.g., 22:00 to 06:00)
// Time is either >= start OR < end: [start, 24:00) OR [00:00, end)
return time >= startTicks || time < endTicks;
}
}
/**
* Determines whether two specified instances of TimeOnly are equal.
*
* @param {TimeOnly} other - The other TimeOnly to compare with
* @returns {boolean} true if left and right represent the same time; otherwise, false
*
* @example
* const time1 = new TimeOnly(14, 30, 0);
* const time2 = new TimeOnly(14, 30, 0);
* const time3 = new TimeOnly(14, 30, 1);
*
* console.log(time1.equals(time2)); // true
* console.log(time1.equals(time3)); // false
*/
equals(other) {
return other instanceof TimeOnly && this.#ticks === other.#ticks;
}
/**
* Compares this instance to a specified TimeOnly value and indicates whether this instance is earlier than, the same as, or later than the specified TimeOnly value.
*
* @param {TimeOnly} other - The object to compare to the current instance
* @returns {number}
* - Less than zero if this instance is earlier than value
* - Zero if this instance is the same as value
* - Greater than zero if this instance is later than value
* @throws {TypeError} If other is not a TimeOnly instance
*
* @example
* const time1 = new TimeOnly(9, 0, 0);
* const time2 = new TimeOnly(17, 0, 0);
*
* console.log(time1.compareTo(time2)); // -1 (time1 is earlier)
* console.log(time2.compareTo(time1)); // 1 (time2 is later)
* console.log(time1.compareTo(time1)); // 0 (same time)
*/
compareTo(other) {
if (!(other instanceof TimeOnly)) {
throw new TypeError("Argument must be a TimeOnly instance.");
}
if (this.#ticks < other.#ticks) return -1;
if (this.#ticks > other.#ticks) return 1;
return 0;
}
/**
* Gives the elapsed time between two points on a circular clock, which will always be a positive value.
*
* @param {TimeOnly} other - The other TimeOnly instance
* @returns {TimeSpan} The elapsed time between this and other
* @throws {TypeError} If other is not a TimeOnly instance
*
* @example
* const morning = new TimeOnly(9, 0, 0);
* const evening = new TimeOnly(17, 0, 0);
*
* const duration = evening.subtract(morning);
* console.log(duration.toString()); // "08:00:00"
*
* // Circular clock behavior - always positive
* const reverseDuration = morning.subtract(evening);
* console.log(reverseDuration.toString()); // "16:00:00" (going forward around the clock)
*/
subtract(other) {
if (!(other instanceof TimeOnly)) {
throw new TypeError("Argument must be a TimeOnly instance.");
}
let diff = Number(this.#ticks - other.#ticks);
// If the result is negative, add 24 hours to make it positive (circular clock)
if (diff < 0) {
diff += TimeSpan.TICKS_PER_DAY;
}
return new TimeSpan(diff);
}
/**
* Converts the value of the current TimeOnly object to its equivalent string representation.
*
* @returns {string} A string representation of the TimeOnly in HH:mm:ss format, with fractional seconds if present
*
* @example
* const time1 = new TimeOnly(9, 30, 0);
* console.log(time1.toString()); // "09:30:00"
*
* const time2 = new TimeOnly(14, 15, 30, 500);
* console.log(time2.toString()); // "14:15:30.5"
*
* const time3 = new TimeOnly(23, 59, 59, 999, 999);
* console.log(time3.toString()); // "23:59:59.999999"
*/
toString() {
const hour = this.hour;
const minute = this.minute;
const second = this.second;
const fraction = Number(this.#ticks % BigInt(TimeSpan.TICKS_PER_SECOND));
let result = `${hour.toString().padStart(2, '0')}:`;
result += `${minute.toString().padStart(2, '0')}:`;
result += second.toString().padStart(2, '0');
if (fraction !== 0) {
// Convert fraction to string and remove trailing zeros
result += `.${fraction.toString().padStart(7, '0').replace(/0+$/, '')}`;
}
return result;
}
/**
* Converts the value of the current TimeOnly object to its equivalent long time string representation.
*
* @returns {string} A string that contains the long time string representation of the current TimeOnly object
*/
toLongTimeString() {
return this.toString();
}
/**
* Converts the value of the current TimeOnly object to its equivalent short time string representation.
*
* @returns {string} A string that contains the short time string representation of the current TimeOnly object
*/
toShortTimeString() {
const hour = this.hour;
const minute = this.minute;
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
}
/**
* Constructs a TimeOnly object from a TimeSpan representing the time elapsed since midnight.
*
* @param {TimeSpan} timeSpan - The time interval measured since midnight. This value has to be positive and not exceeding 24 hours
* @returns {TimeOnly} A TimeOnly object representing the time elapsed since midnight using the timeSpan value
* @throws {TypeError} If timeSpan is not a TimeSpan instance
* @throws {RangeError} If timeSpan is negative or exceeds 24 hours
*
* @example
* const span = TimeSpan.fromHours(14.5); // 14 hours 30 minutes
* const time = TimeOnly.fromTimeSpan(span);
* console.log(time.toString()); // "14:30:00"
*/
static fromTimeSpan(timeSpan) {
if (!(timeSpan instanceof TimeSpan)) {
throw new TypeError("Argument must be a TimeSpan.");
}
const ticks = timeSpan.ticks;
if (ticks < 0 || ticks >= TimeSpan.TICKS_PER_DAY) {
throw new RangeError("TimeSpan must be non-negative and less than 24 hours.");
}
return new TimeOnly(ticks);
}
/**
* Constructs a TimeOnly object from a DateTime representing the time of the day in this DateTime object.
*
* @param {DateTime} dateTime - The DateTime object to extract the time of the day from
* @returns {TimeOnly} A TimeOnly object representing time of the day specified in the DateTime object
* @throws {TypeError} If dateTime is not a DateTime instance
*
* @example
* const dateTime = new DateTime(2024, 12, 25, 14, 30, 45);
* const timeOnly = TimeOnly.fromDateTime(dateTime);
* console.log(timeOnly.toString()); // "14:30:45"
*/
static fromDateTime(dateTime) {
if (!(dateTime instanceof DateTime)) {
throw new TypeError("Argument must be a DateTime.");
}
const timeTicks = BigInt(dateTime.ticks) % BigInt(TimeSpan.TICKS_PER_DAY);
return new TimeOnly(timeTicks);
}
/**
* Convert the current TimeOnly instance to a TimeSpan object.
*
* @returns {TimeSpan} A TimeSpan object spanning to the time specified in the current TimeOnly object
*
* @example
* const time = new TimeOnly(14, 30, 45);
* const span = time.toTimeSpan();
* console.log(span.toString()); // "14:30:45"
*/
toTimeSpan() {
return new TimeSpan(Number(this.#ticks));
}
/**
* Gets the smallest possible value of TimeOnly (00:00:00.0000000).
*
* @type {TimeOnly}
* @readonly
* @static
*
* @example
* const minTime = TimeOnly.minValue;
* console.log(minTime.toString()); // "00:00:00"
*/
static get minValue() {
return new TimeOnly(TimeOnly.#MIN_TIME_TICKS);
}
/**
* Gets the largest possible value of TimeOnly (23:59:59.9999999).
*
* @type {TimeOnly}
* @readonly
* @static
*
* @example
* const maxTime = TimeOnly.maxValue;
* console.log(maxTime.toString()); // "23:59:59.9999999"
*/
static get maxValue() {
return new TimeOnly(TimeOnly.#MAX_TIME_TICKS);
}
/**
* Compares two TimeOnly values and returns an indication of their relative values.
*
* @param {TimeOnly} t1 - The first TimeOnly
* @param {TimeOnly} t2 - The second TimeOnly
* @returns {number} -1 if t1 is earlier than t2, 0 if equal, 1 if t1 is later than t2
*
* @example
* const morning = new TimeOnly(9, 0, 0);
* const evening = new TimeOnly(17, 0, 0);
*
* console.log(TimeOnly.compare(morning, evening)); // -1
* console.log(TimeOnly.compare(evening, morning)); // 1
* console.log(TimeOnly.compare(morning, morning)); // 0
*/
static compare(t1, t2) {
if (!(t1 instanceof TimeOnly) || !(t2 instanceof TimeOnly)) {
throw new TypeError("Both arguments must be TimeOnly instances.");
}
return t1.compareTo(t2);
}
/**
* Returns the primitive value of this TimeOnly (ticks).
*
* This method is called automatically when the TimeOnly is used in contexts
* that require a primitive value, such as arithmetic operations or comparisons.
*
* @returns {number} The ticks value representing this time
*
* @example
* const time = new TimeOnly(12, 0, 0);
* console.log(+time); // Outputs the ticks value
* console.log(time.valueOf()); // Same as above
*/
valueOf() {
return Number(this.#ticks);
}
/**
* Parses a string representation of a time and returns a TimeOnly instance.
*
* The string can be in various formats:
* - "HH:mm" (e.g., "14:30")
* - "HH:mm:ss" (e.g., "14:30:45")
* - "HH:mm:ss.fff" (e.g., "14:30:45.123")
* - "HH:mm:ss.fffffff" (e.g., "14:30:45.1234567")
*
* @param {string} s - The string to parse
* @returns {TimeOnly} A TimeOnly instance representing the parsed time
* @throws {TypeError} If the input is not a string
* @throws {ArgumentError} If the string format is invalid
* @throws {RangeError} If the time components are out of valid range
*
* @example
* const time1 = TimeOnly.parse("14:30"); // 14:30:00
* const time2 = TimeOnly.parse("09:15:30"); // 09:15:30
* const time3 = TimeOnly.parse("16:45:30.500"); // 16:45:30.5
*/
static parse(s) {
if (typeof s !== "string") {
throw new TypeError("Input must be a string.");
}
// Try HH:mm:ss.fffffff format
let match = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,7}))?)?$/);
if (match) {
const hour = parseInt(match[1], 10);
const minute = parseInt(match[2], 10);
const second = match[3] ? parseInt(match[3], 10) : 0;
const fraction = match[4] ? match[4].padEnd(7, '0') : '0000000';
const ticks = parseInt(fraction, 10);
if (hour > 23 || minute > 59 || second > 59) {
throw new RangeError("Time components out of range.");
}
const totalTicks = BigInt(hour) * BigInt(TimeSpan.TICKS_PER_HOUR) +
BigInt(minute) * BigInt(TimeSpan.TICKS_PER_MINUTE) +
BigInt(second) * BigInt(TimeSpan.TICKS_PER_SECOND) +
BigInt(ticks);
return new TimeOnly(totalTicks);
}
throw new ArgumentError("Invalid TimeOnly format.");
}
/**
* Attempts to parse a string representation of a time and returns the result.
*
* Unlike parse(), this method does not throw exceptions on invalid input.
*
* @param {string} s - The string to parse
* @returns {{success: boolean, value: TimeOnly|null}}
* An object containing:
* - success: true if parsing succeeded, false otherwise
* - value: The parsed TimeOnly instance if successful, null otherwise
*
* @example
* const result1 = TimeOnly.tryParse("14:30:45");
* if (result1.success) {
* console.log(result1.value.toString()); // "14:30:45"
* }
*
* const result2 = TimeOnly.tryParse("invalid-time");
* console.log(result2.success); // false
* console.log(result2.value); // null
*/
static tryParse(s) {
try {
return { success: true, value: TimeOnly.parse(s) };
} catch {
return { success: false, value: null };
}
}
}