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