UNPKG

universal-common

Version:

Library that provides useful missing base class library functionality.

742 lines (675 loc) 30.3 kB
import ArgumentError from './ArgumentError.js'; import DateOnly from './DateOnly.js'; import DateTimeFormat from './DateTimeFormat.js'; import DateTime from './DateTime.js'; import DateTimeKind from './DateTimeKind.js'; import TimeOnly from './TimeOnly.js'; import TimeSpan from './TimeSpan.js'; /** * Represents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC). * * DateTimeOffset consists of a DateTime and a TimeSpan offset from UTC. Unlike DateTime, it always represents * an unambiguous point in time by including timezone offset information. * * @example * // Create DateTimeOffset instances * const now = DateTimeOffset.now; * const utcNow = DateTimeOffset.utcNow; * const specific = new DateTimeOffset(2024, 12, 25, 14, 30, 0, TimeSpan.fromHours(-5)); * * // Convert between time zones * const easternTime = utcNow.toOffset(TimeSpan.fromHours(-5)); * const pacificTime = easternTime.toOffset(TimeSpan.fromHours(-8)); * * // Arithmetic operations preserve offset * const tomorrow = now.addDays(1); * const duration = tomorrow.subtract(now); */ export default class DateTimeOffset { /** @private @type {DateTime} Stores UTC time as unspecified DateTime */ #dateTime; /** @private @type {number} Offset from UTC in minutes */ #offsetMinutes; // Constants for offset validation (±14 hours in minutes) static #MAX_OFFSET_MINUTES = 14 * 60; static #MIN_OFFSET_MINUTES = -DateTimeOffset.#MAX_OFFSET_MINUTES; // Unix epoch constants static #UNIX_EPOCH_SECONDS = DateTime.unixEpoch.ticks / BigInt(TimeSpan.TICKS_PER_SECOND); static #UNIX_EPOCH_MILLISECONDS = DateTime.unixEpoch.ticks / BigInt(TimeSpan.TICKS_PER_MILLISECOND); // Min/Max unix time bounds static #UNIX_MIN_SECONDS = Number(DateTime.minValue.ticks / BigInt(TimeSpan.TICKS_PER_SECOND) - DateTimeOffset.#UNIX_EPOCH_SECONDS); static #UNIX_MAX_SECONDS = Number(DateTime.maxValue.ticks / BigInt(TimeSpan.TICKS_PER_SECOND) - DateTimeOffset.#UNIX_EPOCH_SECONDS); // Static values static #minValue = null; static #maxValue = null; static #unixEpoch = null; /** * Creates a new DateTimeOffset instance. * * @constructor * @param {...*} args - Constructor arguments in various formats: * - (ticks, offset) - Ticks since 1/1/0001 and TimeSpan offset * - (dateTime) - DateTime instance (extracts local offset if Local/Unspecified, uses zero offset if UTC) * - (dateTime, offset) - DateTime and TimeSpan offset * - (date, time, offset) - DateOnly, TimeOnly, and TimeSpan offset * - (year, month, day, hour, minute, second, offset) - Date/time components and offset * - (year, month, day, hour, minute, second, millisecond, offset) - With milliseconds * - (year, month, day, hour, minute, second, millisecond, microsecond, offset) - With microseconds * * @throws {ArgumentError} If invalid arguments provided * @throws {RangeError} If date/time values or offset are out of range * * @example * // From DateTime (uses local offset for Local/Unspecified, zero for UTC) * const dto1 = new DateTimeOffset(DateTime.now); * * // From DateTime with explicit offset * const dto2 = new DateTimeOffset(new DateTime(2024, 12, 25, 14, 30, 0), TimeSpan.fromHours(-5)); * * // From date/time components with offset * const dto3 = new DateTimeOffset(2024, 12, 25, 14, 30, 0, TimeSpan.fromHours(-8)); */ constructor(...args) { if (args.length === 2 && typeof args[0] === 'bigint') { // DateTimeOffset(ticks, offset) const ticks = args[0]; const offset = args[1]; this.#offsetMinutes = DateTimeOffset.#validateOffset(offset); this.#dateTime = DateTimeOffset.#validateDate(new DateTime(ticks), offset); } else if (args.length === 1) { // DateTimeOffset(dateTime) const dateTime = args[0]; if (!(dateTime instanceof DateTime)) { throw new ArgumentError("Argument must be a DateTime instance."); } if (dateTime.kind !== DateTimeKind.UTC) { // Local and Unspecified are treated as Local - get system offset // For this implementation, we'll use a simple heuristic based on JS Date const jsDate = dateTime.toDate(); const utcDate = new Date(jsDate.getTime()); const offsetMs = jsDate.getTimezoneOffset() * -60000; // getTimezoneOffset returns opposite sign this.#offsetMinutes = Math.round(offsetMs / 60000); this.#dateTime = DateTimeOffset.#validateDate(dateTime, TimeSpan.fromMinutes(this.#offsetMinutes)); } else { // UTC DateTime gets zero offset this.#offsetMinutes = 0; this.#dateTime = new DateTime(dateTime.ticks, DateTimeKind.UNSPECIFIED); } } else if (args.length === 2 && args[0] instanceof DateTime) { // DateTimeOffset(dateTime, offset) const dateTime = args[0]; const offset = args[1]; if (dateTime.kind === DateTimeKind.LOCAL) { // Validate that offset matches local time const jsDate = dateTime.toDate(); const expectedOffsetMs = jsDate.getTimezoneOffset() * -60000; const expectedOffset = TimeSpan.fromMilliseconds(expectedOffsetMs); if (Math.abs(offset.totalMinutes - expectedOffset.totalMinutes) > 0.5) { throw new ArgumentError("Offset does not match local time offset."); } } else if (dateTime.kind === DateTimeKind.UTC) { // UTC DateTime must have zero offset if (Math.abs(offset.ticks) > 0) { throw new ArgumentError("UTC DateTime must have zero offset."); } } this.#offsetMinutes = DateTimeOffset.#validateOffset(offset); this.#dateTime = DateTimeOffset.#validateDate(dateTime, offset); } else if (args.length === 3 && args[0] instanceof DateOnly && args[1] instanceof TimeOnly) { // DateTimeOffset(date, time, offset) const date = args[0]; const time = args[1]; const offset = args[2]; const dateTime = date.toDateTime(time); this.#offsetMinutes = DateTimeOffset.#validateOffset(offset); this.#dateTime = DateTimeOffset.#validateDate(dateTime, offset); } else if (args.length === 7) { // DateTimeOffset(year, month, day, hour, minute, second, offset) const [year, month, day, hour, minute, second, offset] = args; const dateTime = new DateTime(year, month, day, hour, minute, second); this.#offsetMinutes = DateTimeOffset.#validateOffset(offset); this.#dateTime = DateTimeOffset.#validateDate(dateTime, offset); } else if (args.length === 8) { // DateTimeOffset(year, month, day, hour, minute, second, millisecond, offset) const [year, month, day, hour, minute, second, millisecond, offset] = args; const dateTime = new DateTime(year, month, day, hour, minute, second, millisecond); this.#offsetMinutes = DateTimeOffset.#validateOffset(offset); this.#dateTime = DateTimeOffset.#validateDate(dateTime, offset); } else if (args.length === 9) { // DateTimeOffset(year, month, day, hour, minute, second, millisecond, microsecond, offset) const [year, month, day, hour, minute, second, millisecond, microsecond, offset] = args; // Create DateTime with microsecond precision const baseTicks = new DateTime(year, month, day, hour, minute, second, millisecond).ticks; const microsecondTicks = BigInt(microsecond) * BigInt(TimeSpan.TICKS_PER_MICROSECOND); const dateTime = new DateTime(baseTicks + microsecondTicks); this.#offsetMinutes = DateTimeOffset.#validateOffset(offset); this.#dateTime = DateTimeOffset.#validateDate(dateTime, offset); } else { throw new ArgumentError("Invalid DateTimeOffset constructor arguments."); } } /** * Validates and converts TimeSpan offset to minutes. * * @private * @param {TimeSpan} offset - The offset to validate * @returns {number} Offset in minutes * @throws {ArgumentError} If offset doesn't represent whole minutes * @throws {RangeError} If offset is out of valid range */ static #validateOffset(offset) { if (!(offset instanceof TimeSpan)) { throw new ArgumentError("Offset must be a TimeSpan."); } const minutes = offset.ticks / TimeSpan.TICKS_PER_MINUTE; const wholeMinutes = Math.round(minutes); // Check if it represents whole minutes (within a small tolerance) if (Math.abs(minutes - wholeMinutes) > 1e-10) { throw new ArgumentError("Offset must represent whole minutes."); } if (wholeMinutes < DateTimeOffset.#MIN_OFFSET_MINUTES || wholeMinutes > DateTimeOffset.#MAX_OFFSET_MINUTES) { throw new RangeError("Offset must be between -14:00 and +14:00."); } return wholeMinutes; } /** * Validates that the DateTime and offset combination results in a valid UTC time. * * @private * @param {DateTime} dateTime - The DateTime to validate * @param {TimeSpan} offset - The offset * @returns {DateTime} DateTime with Unspecified kind storing the UTC time * @throws {RangeError} If the resulting UTC time is out of range */ static #validateDate(dateTime, offset) { // Calculate UTC ticks by subtracting the offset const utcTicks = dateTime.ticks - BigInt(offset.ticks); if (utcTicks > DateTime.maxValue.ticks || utcTicks < DateTime.minValue.ticks) { throw new RangeError("The resulting UTC time is out of the valid range."); } // Store as Unspecified DateTime representing UTC time return new DateTime(utcTicks, DateTimeKind.UNSPECIFIED); } /** * Gets the DateTime component of this DateTimeOffset (the local/clock time). * * @type {DateTime} * @readonly */ get dateTime() { return this.clockDateTime; } /** * Gets the clock time (local time accounting for offset). * * @private * @type {DateTime} * @readonly */ get clockDateTime() { const clockTicks = this.#dateTime.ticks + BigInt(this.#offsetMinutes * TimeSpan.TICKS_PER_MINUTE); return new DateTime(clockTicks, DateTimeKind.UNSPECIFIED); } /** * Gets the UTC DateTime represented by this DateTimeOffset. * * @type {DateTime} * @readonly */ get utcDateTime() { return new DateTime(this.#dateTime.ticks, DateTimeKind.UTC); } /** * Gets the local DateTime equivalent of this DateTimeOffset. * * @type {DateTime} * @readonly */ get localDateTime() { return this.utcDateTime.toDate(); // Convert to JS Date and back to get local time } /** * Gets the time zone offset from UTC. * * @type {TimeSpan} * @readonly */ get offset() { return TimeSpan.fromMinutes(this.#offsetMinutes); } /** * Gets the total offset in minutes from UTC. * * @type {number} * @readonly */ get totalOffsetMinutes() { return this.#offsetMinutes; } /** * Gets the number of ticks for the clock time. * * @type {bigint} * @readonly */ get ticks() { return this.clockDateTime.ticks; } /** * Gets the number of ticks for the UTC time. * * @type {bigint} * @readonly */ get utcTicks() { return this.#dateTime.ticks; } // Date/time component properties (from clock time) get year() { return this.clockDateTime.year; } get month() { return this.clockDateTime.month; } get day() { return this.clockDateTime.day; } get hour() { return this.clockDateTime.hour; } get minute() { return this.clockDateTime.minute; } get second() { return this.clockDateTime.second; } get millisecond() { return this.clockDateTime.millisecond; } get microsecond() { return this.clockDateTime.microsecond; } get nanosecond() { return this.clockDateTime.nanosecond; } get dayOfWeek() { return this.clockDateTime.dayOfWeek; } get dayOfYear() { return this.clockDateTime.dayOfYear; } get date() { return this.clockDateTime.date; } get timeOfDay() { return this.clockDateTime.timeOfDay; } /** * Adjusts this DateTimeOffset to a different time zone offset. * * @param {TimeSpan} offset - The new offset from UTC * @returns {DateTimeOffset} A new DateTimeOffset with the same UTC time but different offset * @throws {ArgumentError} If offset is invalid * @throws {RangeError} If offset is out of range * * @example * const utc = DateTimeOffset.utcNow; * const eastern = utc.toOffset(TimeSpan.fromHours(-5)); // UTC-5 * const pacific = utc.toOffset(TimeSpan.fromHours(-8)); // UTC-8 */ toOffset(offset) { const offsetMinutes = DateTimeOffset.#validateOffset(offset); const newClockTicks = this.#dateTime.ticks + BigInt(offsetMinutes * TimeSpan.TICKS_PER_MINUTE); const newClockDateTime = new DateTime(newClockTicks, DateTimeKind.UNSPECIFIED); return new DateTimeOffset(newClockDateTime, TimeSpan.fromMinutes(offsetMinutes)); } /** * Adds a TimeSpan to this DateTimeOffset. * * @param {TimeSpan} timeSpan - The time interval to add * @returns {DateTimeOffset} A new DateTimeOffset with the added time * @throws {TypeError} If timeSpan is not a TimeSpan * @throws {RangeError} If result is out of range * * @example * const dto = DateTimeOffset.now; * const later = dto.add(TimeSpan.fromHours(2)); */ add(timeSpan) { if (!(timeSpan instanceof TimeSpan)) { throw new TypeError("Argument must be a TimeSpan."); } const newClockDateTime = this.clockDateTime.add(timeSpan); return new DateTimeOffset(newClockDateTime, this.offset); } // Convenience add methods addDays(days) { return this.add(TimeSpan.fromDays(days)); } addHours(hours) { return this.add(TimeSpan.fromHours(hours)); } addMinutes(minutes) { return this.add(TimeSpan.fromMinutes(minutes)); } addSeconds(seconds) { return this.add(TimeSpan.fromSeconds(seconds)); } addMilliseconds(milliseconds) { return this.add(TimeSpan.fromMilliseconds(milliseconds)); } addMicroseconds(microseconds) { return this.add(TimeSpan.fromMicroseconds(microseconds)); } addMonths(months) { return new DateTimeOffset(this.clockDateTime.addMonths(months), this.offset); } addYears(years) { return new DateTimeOffset(this.clockDateTime.addYears(years), this.offset); } addTicks(ticks) { return this.add(TimeSpan.fromTicks(ticks)); } /** * Subtracts another DateTimeOffset or TimeSpan from this DateTimeOffset. * * @param {DateTimeOffset|TimeSpan} value - The value to subtract * @returns {TimeSpan|DateTimeOffset} TimeSpan if subtracting DateTimeOffset, DateTimeOffset if subtracting TimeSpan * @throws {TypeError} If value is not a DateTimeOffset or TimeSpan * * @example * const dto1 = new DateTimeOffset(2024, 12, 25, 12, 0, 0, TimeSpan.fromHours(-5)); * const dto2 = new DateTimeOffset(2024, 12, 25, 10, 0, 0, TimeSpan.fromHours(-8)); * const difference = dto1.subtract(dto2); // TimeSpan representing the difference */ subtract(value) { if (value instanceof DateTimeOffset) { // Return the difference in UTC time return TimeSpan.fromTicks(Number(this.utcTicks - value.utcTicks)); } else if (value instanceof TimeSpan) { return this.add(value.negate()); } else { throw new TypeError("Argument must be a DateTimeOffset or TimeSpan."); } } /** * Compares this DateTimeOffset with another DateTimeOffset. * Comparison is based on UTC time. * * @param {DateTimeOffset} other - The DateTimeOffset to compare with * @returns {number} -1 if this is earlier, 0 if equal, 1 if later * @throws {TypeError} If other is not a DateTimeOffset * * @example * const dto1 = new DateTimeOffset(2024, 12, 25, 12, 0, 0, TimeSpan.fromHours(-5)); * const dto2 = new DateTimeOffset(2024, 12, 25, 10, 0, 0, TimeSpan.fromHours(-8)); * console.log(dto1.compareTo(dto2)); // -1 (dto1 is 3 hours earlier in UTC) */ compareTo(other) { if (!(other instanceof DateTimeOffset)) { throw new TypeError("Argument must be a DateTimeOffset."); } if (this.utcTicks < other.utcTicks) return -1; if (this.utcTicks > other.utcTicks) return 1; return 0; } /** * Determines whether this DateTimeOffset is equal to another DateTimeOffset. * Equality is based on UTC time only. * * @param {DateTimeOffset} other - The DateTimeOffset to compare with * @returns {boolean} true if they represent the same UTC instant; otherwise, false * * @example * const dto1 = new DateTimeOffset(2024, 12, 25, 17, 0, 0, TimeSpan.zero); // 17:00 UTC * const dto2 = new DateTimeOffset(2024, 12, 25, 12, 0, 0, TimeSpan.fromHours(-5)); // 12:00 UTC-5 (17:00 UTC) * console.log(dto1.equals(dto2)); // true (same UTC time) */ equals(other) { return other instanceof DateTimeOffset && this.utcTicks === other.utcTicks; } /** * Determines whether this DateTimeOffset is exactly equal to another (including offset). * * @param {DateTimeOffset} other - The DateTimeOffset to compare with * @returns {boolean} true if both UTC time and offset are equal; otherwise, false * * @example * const dto1 = new DateTimeOffset(2024, 12, 25, 17, 0, 0, TimeSpan.zero); * const dto2 = new DateTimeOffset(2024, 12, 25, 12, 0, 0, TimeSpan.fromHours(-5)); * console.log(dto1.equalsExact(dto2)); // false (same UTC time but different offsets) */ equalsExact(other) { return other instanceof DateTimeOffset && this.utcTicks === other.utcTicks && this.#offsetMinutes === other.#offsetMinutes; } /** * Converts this DateTimeOffset to a JavaScript Date object. * * @returns {Date} A JavaScript Date representing the same instant */ toDate() { return this.utcDateTime.toDate(); } /** * Converts this DateTimeOffset to local time. * * @returns {DateTimeOffset} A DateTimeOffset adjusted to the local time zone */ toLocalTime() { const jsDate = this.toDate(); const localOffset = TimeSpan.fromMinutes(-jsDate.getTimezoneOffset()); return this.toOffset(localOffset); } /** * Converts this DateTimeOffset to UTC. * * @returns {DateTimeOffset} A DateTimeOffset with zero offset (UTC) */ toUniversalTime() { return this.toOffset(TimeSpan.zero); } /** * Gets the Unix timestamp in seconds. * * @type {number} * @readonly */ toUnixTimeSeconds() { const seconds = Number(this.utcTicks / BigInt(TimeSpan.TICKS_PER_SECOND)); return seconds - Number(DateTimeOffset.#UNIX_EPOCH_SECONDS); } /** * Gets the Unix timestamp in milliseconds. * * @type {number} * @readonly */ toUnixTimeMilliseconds() { const milliseconds = Number(this.utcTicks / BigInt(TimeSpan.TICKS_PER_MILLISECOND)); return milliseconds - Number(DateTimeOffset.#UNIX_EPOCH_MILLISECONDS); } /** * Converts the DateTimeOffset to its string representation. * * @param {string} [format] - A standard or custom date and time format string * @returns {string} A string representation of the DateTimeOffset * * @example * const dto = new DateTimeOffset(2024, 12, 25, 14, 30, 45, 123, TimeSpan.fromHours(-5)); * console.log(dto.toString()); // Default format with offset * console.log(dto.toString('d')); // Short date format * console.log(dto.toString('D')); // Long date format * console.log(dto.toString('yyyy-MM-dd HH:mm:ss zzz')); // Custom format with offset */ toString(format) { if (arguments.length === 0) { // toString() - no arguments, use default format return DateTimeFormat.format(this.clockDateTime, null, null, this.offset); } else { // toString(format) - format string provided return DateTimeFormat.format(this.clockDateTime, format, null, this.offset); } } /** * Gets the current DateTimeOffset in the local time zone. * * @type {DateTimeOffset} * @readonly * @static */ static get now() { return new DateTimeOffset(DateTime.now); } /** * Gets the current DateTimeOffset in UTC. * * @type {DateTimeOffset} * @readonly * @static */ static get utcNow() { return new DateTimeOffset(DateTime.utcNow); } /** * Gets the minimum DateTimeOffset value. * * @type {DateTimeOffset} * @readonly * @static */ static get minValue() { if (!DateTimeOffset.#minValue) { DateTimeOffset.#minValue = new DateTimeOffset(DateTime.minValue, TimeSpan.zero); } return DateTimeOffset.#minValue; } /** * Gets the maximum DateTimeOffset value. * * @type {DateTimeOffset} * @readonly * @static */ static get maxValue() { if (!DateTimeOffset.#maxValue) { DateTimeOffset.#maxValue = new DateTimeOffset(DateTime.maxValue, TimeSpan.zero); } return DateTimeOffset.#maxValue; } /** * Gets the Unix epoch as a DateTimeOffset (1970-01-01 00:00:00 +00:00). * * @type {DateTimeOffset} * @readonly * @static */ static get unixEpoch() { if (!DateTimeOffset.#unixEpoch) { DateTimeOffset.#unixEpoch = new DateTimeOffset(DateTime.unixEpoch, TimeSpan.zero); } return DateTimeOffset.#unixEpoch; } /** * Creates a DateTimeOffset from Unix time in seconds. * * @param {number} seconds - Seconds since Unix epoch * @returns {DateTimeOffset} A DateTimeOffset representing the specified Unix time * @throws {RangeError} If seconds is out of valid range * * @example * const dto = DateTimeOffset.fromUnixTimeSeconds(1640458800); // 2021-12-25 17:00:00 UTC */ static fromUnixTimeSeconds(seconds) { if (seconds < DateTimeOffset.#UNIX_MIN_SECONDS || seconds > DateTimeOffset.#UNIX_MAX_SECONDS) { throw new RangeError("Unix time seconds out of valid range."); } const ticks = BigInt(seconds) * BigInt(TimeSpan.TICKS_PER_SECOND) + DateTime.unixEpoch.ticks; return new DateTimeOffset(new DateTime(ticks, DateTimeKind.UTC), TimeSpan.zero); } /** * Creates a DateTimeOffset from Unix time in milliseconds. * * @param {number} milliseconds - Milliseconds since Unix epoch * @returns {DateTimeOffset} A DateTimeOffset representing the specified Unix time * @throws {RangeError} If milliseconds is out of valid range * * @example * const dto = DateTimeOffset.fromUnixTimeMilliseconds(1640458800000); // 2021-12-25 17:00:00.000 UTC */ static fromUnixTimeMilliseconds(milliseconds) { const maxMs = Number(DateTime.maxValue.ticks / BigInt(TimeSpan.TICKS_PER_MILLISECOND) - DateTimeOffset.#UNIX_EPOCH_MILLISECONDS); const minMs = Number(DateTime.minValue.ticks / BigInt(TimeSpan.TICKS_PER_MILLISECOND) - DateTimeOffset.#UNIX_EPOCH_MILLISECONDS); if (milliseconds < minMs || milliseconds > maxMs) { throw new RangeError("Unix time milliseconds out of valid range."); } const ticks = BigInt(milliseconds) * BigInt(TimeSpan.TICKS_PER_MILLISECOND) + DateTime.unixEpoch.ticks; return new DateTimeOffset(new DateTime(ticks, DateTimeKind.UTC), TimeSpan.zero); } /** * Creates a DateTimeOffset from a JavaScript Date. * * @param {Date} date - The JavaScript Date to convert * @param {TimeSpan} [offset] - Optional offset (defaults to local offset) * @returns {DateTimeOffset} A DateTimeOffset representing the Date * @throws {TypeError} If date is not a Date instance * * @example * const jsDate = new Date(); * const dto1 = DateTimeOffset.fromDate(jsDate); // Uses local offset * const dto2 = DateTimeOffset.fromDate(jsDate, TimeSpan.zero); // UTC */ static fromDate(date, offset = null) { if (!(date instanceof Date)) { throw new TypeError("Argument must be a Date instance."); } const dateTime = DateTime.fromDate(date, DateTimeKind.UNSPECIFIED); if (offset === null) { // Use local offset const offsetMs = -date.getTimezoneOffset() * 60000; offset = TimeSpan.fromMilliseconds(offsetMs); } return new DateTimeOffset(dateTime, offset); } /** * Compares two DateTimeOffset values. * * @param {DateTimeOffset} first - The first DateTimeOffset * @param {DateTimeOffset} second - The second DateTimeOffset * @returns {number} -1 if first < second, 0 if equal, 1 if first > second */ static compare(first, second) { return first.compareTo(second); } /** * Determines whether two DateTimeOffset values are equal. * * @param {DateTimeOffset} first - The first DateTimeOffset * @param {DateTimeOffset} second - The second DateTimeOffset * @returns {boolean} true if equal; otherwise, false */ static equals(first, second) { return first.equals(second); } /** * Parses a DateTimeOffset from its string representation. * * @param {string} input - The string to parse * @returns {DateTimeOffset} A DateTimeOffset parsed from the string * @throws {TypeError} If input is not a string * @throws {ArgumentError} If string format is invalid * * @example * const dto1 = DateTimeOffset.parse("2024-12-25T14:30:00-05:00"); * const dto2 = DateTimeOffset.parse("2024-12-25 14:30:00 -05:00"); */ static parse(input) { if (typeof input !== "string") { throw new TypeError("Input must be a string."); } // Try ISO 8601 format: YYYY-MM-DDTHH:mm:ss±HH:mm let match = input.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?([+-])(\d{2}):(\d{2})$/); if (!match) { // Try alternative format: YYYY-MM-DD HH:mm:ss ±HH:mm match = input.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))? ([+-])(\d{2}):(\d{2})$/); } if (match) { const year = parseInt(match[1], 10); const month = parseInt(match[2], 10); const day = parseInt(match[3], 10); const hour = parseInt(match[4], 10); const minute = parseInt(match[5], 10); const second = parseInt(match[6], 10); const fraction = match[7] ? parseInt(match[7].padEnd(7, '0'), 10) : 0; const offsetSign = match[8] === '+' ? 1 : -1; const offsetHours = parseInt(match[9], 10); const offsetMinutes = parseInt(match[10], 10); const totalOffsetMinutes = offsetSign * (offsetHours * 60 + offsetMinutes); const offset = TimeSpan.fromMinutes(totalOffsetMinutes); if (fraction > 0) { const dateTime = new DateTime(year, month, day, hour, minute, second); const ticks = dateTime.ticks + BigInt(fraction); return new DateTimeOffset(new DateTime(ticks), offset); } else { return new DateTimeOffset(year, month, day, hour, minute, second, offset); } } throw new ArgumentError("Invalid DateTimeOffset format."); } /** * Attempts to parse a DateTimeOffset from its string representation. * * @param {string} input - The string to parse * @returns {{success: boolean, value: DateTimeOffset|null}} Parse result * * @example * const result = DateTimeOffset.tryParse("2024-12-25T14:30:00-05:00"); * if (result.success) { * console.log(result.value.toString()); * } */ static tryParse(input) { try { return { success: true, value: DateTimeOffset.parse(input) }; } catch { return { success: false, value: null }; } } }