universal-common
Version:
Library that provides useful missing base class library functionality.
797 lines (725 loc) • 30.4 kB
JavaScript
import DateTime from "./DateTime.js";
import TimeOnly from "./TimeOnly.js";
/**
* Represents dates with values ranging from January 1, 0001 Anno Domini (Common Era)
* through December 31, 9999 A.D. (C.E.) in the Gregorian calendar.
*
* DateOnly provides date-only functionality without time components, making it ideal
* for scenarios where only the date portion is relevant (birthdays, deadlines, etc.).
*
* @example
* // Create DateOnly instances
* const today = new DateOnly(2024, 12, 25);
* const fromDayNumber = DateOnly.fromDayNumber(738500);
* const fromDate = DateOnly.fromDate(new Date());
*
* // Date arithmetic
* const tomorrow = today.addDays(1);
* const nextMonth = today.addMonths(1);
* const nextYear = today.addYears(1);
*
* // Comparisons
* console.log(today.equals(tomorrow)); // false
* console.log(today.compareTo(tomorrow)); // -1
*
* // Formatting
* console.log(today.toString()); // "2024-12-25"
*/
export default class DateOnly {
/** @private */
#dayNumber;
/** @private */
static #MIN_DAY_NUMBER = 0;
/** @private */
static #MAX_DAY_NUMBER = 3652058; // December 31, 9999
/** @private */
static #TICKS_PER_DAY = 864000000000; // 10,000 ticks per millisecond * 1000 ms/s * 60 s/min * 60 min/hr * 24 hr/day
/**
* Creates a new DateOnly instance.
*
* @param {number} year - The year (1 through 9999)
* @param {number} month - The month (1 through 12)
* @param {number} day - The day (1 through the number of days in the specified month)
* @throws {RangeError} If any parameter is out of valid range
* @throws {RangeError} If the day is invalid for the specified month and year
*
* @example
* const date = new DateOnly(2024, 12, 25); // December 25, 2024
* const leapDay = new DateOnly(2024, 2, 29); // Valid leap year date
*/
constructor(year, month, day) {
// Validate input parameters
if (year < 1 || year > 9999) {
throw new RangeError("Year must be between 1 and 9999");
}
if (month < 1 || month > 12) {
throw new RangeError("Month must be between 1 and 12");
}
if (day < 1) {
throw new RangeError("Day must be at least 1");
}
// Validate day for the given month and year
const daysInMonth = DateOnly.#getDaysInMonth(year, month);
if (day > daysInMonth) {
throw new RangeError(`Day must be between 1 and ${daysInMonth} for ${year}-${month}`);
}
this.#dayNumber = DateOnly.#dateToTicks(year, month, day);
}
/**
* Private constructor that creates DateOnly from day number directly.
* @private
* @param {number} dayNumber - The day number (0 to MAX_DAY_NUMBER)
*/
static #fromDayNumber(dayNumber) {
const instance = DateOnly.fromDateTime(DateTime.today);
instance.#dayNumber = dayNumber;
return instance;
}
/**
* Creates a DateOnly instance from the specified day number.
*
* @param {number} dayNumber - The number of days since January 1, 0001 in the Proleptic Gregorian calendar
* @returns {DateOnly} A new DateOnly instance representing the specified day
* @throws {RangeError} If dayNumber is out of valid range (0 to 3652058)
*
* @example
* const date = DateOnly.fromDayNumber(738500); // Represents a specific date
* console.log(date.toString()); // Outputs the corresponding date string
*/
static fromDayNumber(dayNumber) {
if (!Number.isInteger(dayNumber) || dayNumber < DateOnly.#MIN_DAY_NUMBER || dayNumber > DateOnly.#MAX_DAY_NUMBER) {
throw new RangeError(`Day number must be an integer between ${DateOnly.#MIN_DAY_NUMBER} and ${DateOnly.#MAX_DAY_NUMBER}`);
}
return DateOnly.#fromDayNumber(dayNumber);
}
/**
* Converts date components to day number.
*
* @private
* @param {number} year - The year
* @param {number} month - The month
* @param {number} day - The day
* @returns {number} The day number representing the date
*/
static #dateToTicks(year, month, day) {
// Calculate days from year 1 to the beginning of the specified year
const daysToYear = DateOnly.#daysToYear(year);
// Calculate days from beginning of year to beginning of month
const daysToMonth = DateOnly.#getDaysToMonth(year, month);
// Add the day (subtract 1 because day is 1-based)
return daysToYear + daysToMonth + day - 1;
}
/**
* Calculates the number of days from year 1 to the beginning of the specified year.
*
* @private
* @param {number} year - The year
* @returns {number} Number of days from year 1 to the start of the specified year
*/
static #daysToYear(year) {
const y = year - 1;
return (y * 365) + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400);
}
/**
* Gets the number of days in the specified month.
*
* @private
* @param {number} year - The year
* @param {number} month - The month (1-12)
* @returns {number} Number of days in the month
*/
static #getDaysInMonth(year, month) {
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (month === 2 && DateOnly.isLeapYear(year)) {
return 29;
}
return daysInMonth[month - 1];
}
/**
* Gets the number of days from the beginning of the year to the beginning of the specified month.
*
* @private
* @param {number} year - The year
* @param {number} month - The month (1-12)
* @returns {number} Number of days from beginning of year to beginning of month
*/
static #getDaysToMonth(year, month) {
const daysToMonth365 = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
const daysToMonth366 = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
const daysToMonth = DateOnly.isLeapYear(year) ? daysToMonth366 : daysToMonth365;
return daysToMonth[month - 1];
}
/**
* Converts day number back to year, month, day components.
* This is used internally for property getters.
*
* @private
* @param {number} dayNumber - The day number
* @returns {{year: number, month: number, day: number}} Date components
*/
static #dayNumberToDate(dayNumber) {
// Start with an approximation of the year
let year = Math.floor(dayNumber / 365.25) + 1;
// Adjust the year if our approximation is off
while (DateOnly.#daysToYear(year) > dayNumber) {
year--;
}
while (DateOnly.#daysToYear(year + 1) <= dayNumber) {
year++;
}
// Calculate remaining days in the year
const dayOfYear = dayNumber - DateOnly.#daysToYear(year) + 1;
// Find the month
let month = 1;
while (month <= 12) {
const daysToMonth = DateOnly.#getDaysToMonth(year, month);
const daysToNextMonth = month === 12 ?
(DateOnly.isLeapYear(year) ? 366 : 365) :
DateOnly.#getDaysToMonth(year, month + 1);
if (dayOfYear > daysToMonth && dayOfYear <= daysToNextMonth) {
break;
}
month++;
}
// Calculate the day
const daysToMonth = DateOnly.#getDaysToMonth(year, month);
const day = dayOfYear - daysToMonth;
return { year, month, day };
}
/**
* Determines whether the specified year is a leap year.
*
* A leap year is divisible by 4, except for years divisible by 100 unless they are also divisible by 400.
*
* @param {number} year - The year to check (1 through 9999)
* @returns {boolean} true if the year is a leap year; otherwise, false
* @throws {RangeError} If year is out of valid range
*
* @example
* DateOnly.isLeapYear(2024); // true (divisible by 4)
* DateOnly.isLeapYear(1900); // false (divisible by 100 but not 400)
* DateOnly.isLeapYear(2000); // true (divisible by 400)
*/
static isLeapYear(year) {
if (year < 1 || year > 9999) {
throw new RangeError("Year must be between 1 and 9999");
}
return (year % 4 === 0) && (year % 100 !== 0 || year % 400 === 0);
}
/**
* Gets the year component of the date represented by this instance.
*
* @type {number}
* @readonly
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(date.year); // 2024
*/
get year() {
return DateOnly.#dayNumberToDate(this.#dayNumber).year;
}
/**
* Gets the month component of the date represented by this instance.
*
* @type {number}
* @readonly
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(date.month); // 12
*/
get month() {
return DateOnly.#dayNumberToDate(this.#dayNumber).month;
}
/**
* Gets the day component of the date represented by this instance.
*
* @type {number}
* @readonly
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(date.day); // 25
*/
get day() {
return DateOnly.#dayNumberToDate(this.#dayNumber).day;
}
/**
* Gets the day of the week represented by this instance.
*
* @type {number}
* @readonly
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(date.dayOfWeek); // 3 (Wednesday, where Sunday = 0)
*/
get dayOfWeek() {
// January 1, 0001 was a Monday (day 1), so we adjust accordingly
// Day number 0 = Monday (1), so (0 + 1) % 7 = 1
return (this.#dayNumber + 1) % 7;
}
/**
* Gets the day of the year represented by this instance.
*
* @type {number}
* @readonly
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(date.dayOfYear); // 360 (the 360th day of 2024)
*/
get dayOfYear() {
const { year } = DateOnly.#dayNumberToDate(this.#dayNumber);
const daysToYear = DateOnly.#daysToYear(year);
return this.#dayNumber - daysToYear + 1;
}
/**
* Gets the number of days since January 1, 0001 in the Proleptic Gregorian calendar
* represented by this instance.
*
* @type {number}
* @readonly
* @example
* const date = new DateOnly(2024, 1, 1);
* console.log(date.dayNumber); // Number of days since year 1
*/
get dayNumber() {
return this.#dayNumber;
}
/**
* Adds the specified number of days to the value of this instance.
*
* @param {number} value - The number of days to add. Can be negative to subtract days
* @returns {DateOnly} A new DateOnly instance with the specified number of days added
* @throws {RangeError} If the resulting date would be out of valid range
* @throws {TypeError} If value is not a number
*
* @example
* const date = new DateOnly(2024, 12, 25);
* const nextDay = date.addDays(1); // December 26, 2024
* const prevDay = date.addDays(-1); // December 24, 2024
* const nextWeek = date.addDays(7); // January 1, 2025
*/
addDays(value) {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeError("Value must be an integer");
}
const newDayNumber = this.#dayNumber + value;
if (newDayNumber < DateOnly.#MIN_DAY_NUMBER || newDayNumber > DateOnly.#MAX_DAY_NUMBER) {
throw new RangeError("Resulting date is out of valid range");
}
return DateOnly.#fromDayNumber(newDayNumber);
}
/**
* Adds the specified number of months to the value of this instance.
*
* If the resulting day is invalid for the target month (e.g., adding 1 month to January 31
* would result in February 31), the day is adjusted to the last valid day of that month.
*
* @param {number} months - The number of months to add. Can be negative to subtract months
* @returns {DateOnly} A new DateOnly instance with the specified number of months added
* @throws {RangeError} If months parameter is out of range (-120000 to 120000)
* @throws {RangeError} If the resulting year would be out of valid range
* @throws {TypeError} If months is not a number
*
* @example
* const date = new DateOnly(2024, 1, 31);
* const nextMonth = date.addMonths(1); // February 29, 2024 (adjusted from Feb 31)
* const prevMonth = date.addMonths(-1); // December 31, 2023
* const nextYear = date.addMonths(12); // January 31, 2025
*/
addMonths(months) {
if (typeof months !== 'number' || !Number.isInteger(months)) {
throw new TypeError("Months must be an integer");
}
if (months < -120000 || months > 120000) {
throw new RangeError("Months must be between -120000 and 120000");
}
let { year, month, day } = DateOnly.#dayNumberToDate(this.#dayNumber);
// Calculate new year and month
const totalMonths = year * 12 + (month - 1) + months;
const newYear = Math.floor(totalMonths / 12);
const newMonth = (totalMonths % 12) + 1;
if (newYear < 1 || newYear > 9999) {
throw new RangeError("Resulting year is out of valid range (1-9999)");
}
// Adjust day if it's invalid for the new month
const daysInNewMonth = DateOnly.#getDaysInMonth(newYear, newMonth);
const newDay = Math.min(day, daysInNewMonth);
return new DateOnly(newYear, newMonth, newDay);
}
/**
* Adds the specified number of years to the value of this instance.
*
* If the current date is February 29 (leap day) and the target year is not a leap year,
* the resulting date will be February 28.
*
* @param {number} years - The number of years to add. Can be negative to subtract years
* @returns {DateOnly} A new DateOnly instance with the specified number of years added
* @throws {RangeError} If years parameter is out of range (-10000 to 10000)
* @throws {RangeError} If the resulting year would be out of valid range (1 to 9999)
* @throws {TypeError} If years is not a number
*
* @example
* const date = new DateOnly(2024, 2, 29); // Leap day
* const nextYear = date.addYears(1); // February 28, 2025 (not a leap year)
* const prevYear = date.addYears(-1); // February 28, 2023 (not a leap year)
* const nextLeapYear = date.addYears(4); // February 29, 2028 (leap year)
*/
addYears(years) {
if (typeof years !== 'number' || !Number.isInteger(years)) {
throw new TypeError("Years must be an integer");
}
if (years < -10000 || years > 10000) {
throw new RangeError("Years must be between -10000 and 10000");
}
let { year, month, day } = DateOnly.#dayNumberToDate(this.#dayNumber);
const newYear = year + years;
if (newYear < 1 || newYear > 9999) {
throw new RangeError("Resulting year is out of valid range (1-9999)");
}
// Handle leap day edge case
if (month === 2 && day === 29 && !DateOnly.isLeapYear(newYear)) {
day = 28;
}
return new DateOnly(newYear, month, day);
}
/**
* Determines whether this DateOnly instance is equal to another DateOnly instance.
*
* @param {DateOnly} other - The DateOnly instance to compare with
* @returns {boolean} true if both instances represent the same date; otherwise, false
*
* @example
* const date1 = new DateOnly(2024, 12, 25);
* const date2 = new DateOnly(2024, 12, 25);
* const date3 = new DateOnly(2024, 12, 26);
*
* console.log(date1.equals(date2)); // true
* console.log(date1.equals(date3)); // false
*/
equals(other) {
return other instanceof DateOnly && this.#dayNumber === other.#dayNumber;
}
/**
* Compares this DateOnly instance to another DateOnly instance.
*
* @param {DateOnly} other - The DateOnly instance to compare with
* @returns {number}
* - Less than 0 if this instance is earlier than other
* - 0 if this instance is equal to other
* - Greater than 0 if this instance is later than other
* @throws {TypeError} If other is not a DateOnly instance
*
* @example
* const date1 = new DateOnly(2024, 12, 24);
* const date2 = new DateOnly(2024, 12, 25);
* const date3 = new DateOnly(2024, 12, 26);
*
* console.log(date1.compareTo(date2)); // -1 (date1 is earlier)
* console.log(date2.compareTo(date2)); // 0 (same date)
* console.log(date3.compareTo(date2)); // 1 (date3 is later)
*/
compareTo(other) {
if (!(other instanceof DateOnly)) {
throw new TypeError("Argument must be a DateOnly instance");
}
return this.#dayNumber - other.#dayNumber;
}
/**
* Comparison operators as methods (since JavaScript doesn't support operator overloading)
*/
/**
* Returns true if this date is less than the other date.
* @param {DateOnly} other - The date to compare with
* @returns {boolean}
*/
isLessThan(other) {
return this.compareTo(other) < 0;
}
/**
* Returns true if this date is less than or equal to the other date.
* @param {DateOnly} other - The date to compare with
* @returns {boolean}
*/
isLessThanOrEqual(other) {
return this.compareTo(other) <= 0;
}
/**
* Returns true if this date is greater than the other date.
* @param {DateOnly} other - The date to compare with
* @returns {boolean}
*/
isGreaterThan(other) {
return this.compareTo(other) > 0;
}
/**
* Returns true if this date is greater than or equal to the other date.
* @param {DateOnly} other - The date to compare with
* @returns {boolean}
*/
isGreaterThanOrEqual(other) {
return this.compareTo(other) >= 0;
}
/**
* Converts the DateOnly to its string representation in ISO 8601 format (YYYY-MM-DD).
*
* @returns {string} A string representation of the date in YYYY-MM-DD format
*
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(date.toString()); // "2024-12-25"
*
* const date2 = new DateOnly(2024, 1, 5);
* console.log(date2.toString()); // "2024-01-05"
*/
toString() {
const { year, month, day } = DateOnly.#dayNumberToDate(this.#dayNumber);
return `${year.toString().padStart(4, "0")}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`;
}
/**
* Creates a DateOnly instance from a JavaScript Date object.
*
* Only the date components (year, month, day) are used; time components are ignored.
*
* @param {Date} date - The JavaScript Date object to convert
* @returns {DateOnly} A new DateOnly instance representing the date portion of the input Date
* @throws {TypeError} If the input is not a Date object
*
* @example
* const jsDate = new Date(2024, 11, 25, 15, 30, 45); // December 25, 2024 3:30:45 PM
* const dateOnly = DateOnly.fromDate(jsDate);
* console.log(dateOnly.toString()); // "2024-12-25" (time ignored)
*/
static fromDate(date) {
if (!(date instanceof Date)) {
throw new TypeError("Argument must be a Date instance");
}
if (isNaN(date.getTime())) {
throw new TypeError("Date instance must be valid");
}
return new DateOnly(date.getFullYear(), date.getMonth() + 1, date.getDate());
}
/**
* Creates a DateOnly instance from a DateTime object.
*
* Only the date components are extracted; time components are ignored.
*
* @param {DateTime} dateTime - The DateTime object to convert
* @returns {DateOnly} A new DateOnly instance representing the date portion of the DateTime
* @throws {TypeError} If the input is not a DateTime object
*
* @example
* // Assuming DateTime is imported
* const dateTime = new DateTime(2024, 12, 25, 15, 30, 45);
* const dateOnly = DateOnly.fromDateTime(dateTime);
* console.log(dateOnly.toString()); // "2024-12-25"
*/
static fromDateTime(dateTime) {
// Check if DateTime is available and if the parameter is a DateTime instance
if (typeof DateTime !== 'undefined' && dateTime instanceof DateTime) {
return new DateOnly(dateTime.year, dateTime.month, dateTime.day);
}
// Fallback check for duck typing (object has year, month, day properties)
if (dateTime && typeof dateTime.year === 'number' &&
typeof dateTime.month === 'number' && typeof dateTime.day === 'number') {
return new DateOnly(dateTime.year, dateTime.month, dateTime.day);
}
throw new TypeError("Argument must be a DateTime instance");
}
/**
* Combines this DateOnly with a time to create a JavaScript Date object.
*
* @param {number} [hours=0] - The hour component (0-23)
* @param {number} [minutes=0] - The minute component (0-59)
* @param {number} [seconds=0] - The second component (0-59)
* @param {number} [milliseconds=0] - The millisecond component (0-999)
* @param {boolean} [useUtc=false] - Whether to create a UTC date or local date
* @returns {Date} A JavaScript Date object combining this date with the specified time
*
* @example
* const dateOnly = new DateOnly(2024, 12, 25);
* const localDate = dateOnly.toDate(10, 30, 0); // 10:30 AM local time
* const utcDate = dateOnly.toDate(10, 30, 0, 0, true); // 10:30 AM UTC
*/
toDate(hours = 0, minutes = 0, seconds = 0, milliseconds = 0, useUtc = false) {
const { year, month, day } = DateOnly.#dayNumberToDate(this.#dayNumber);
if (useUtc) {
return new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds, milliseconds));
} else {
return new Date(year, month - 1, day, hours, minutes, seconds, milliseconds);
}
}
/**
* Combines this DateOnly with a TimeOnly to create a DateTime object.
*
* @param {TimeOnly} time - The TimeOnly instance to combine with this date
* @param {number} [kind] - Optional DateTimeKind for the resulting DateTime
* @returns {DateTime} A DateTime object combining this date with the specified time
* @throws {TypeError} If time is not a TimeOnly instance
*
* @example
* // Assuming TimeOnly and DateTime are available
* const dateOnly = new DateOnly(2024, 12, 25);
* const timeOnly = new TimeOnly(10, 30, 0);
* const dateTime = dateOnly.toDateTime(timeOnly);
* console.log(dateTime.toString()); // "2024-12-25 10:30:00"
*/
toDateTime(time, kind) {
// Check if the required classes are available
if (typeof TimeOnly !== 'undefined' && typeof DateTime !== 'undefined') {
if (!(time instanceof TimeOnly)) {
throw new TypeError("Argument must be a TimeOnly instance");
}
const { year, month, day } = DateOnly.#dayNumberToDate(this.#dayNumber);
if (kind !== undefined) {
return new DateTime(year, month, day,
time.hour, time.minute, time.second, time.millisecond, kind);
} else {
return new DateTime(year, month, day,
time.hour, time.minute, time.second, time.millisecond);
}
}
// Fallback for duck typing
if (time && typeof time.hour === 'number' && typeof time.minute === 'number' &&
typeof time.second === 'number') {
const millisecond = time.millisecond || 0;
const { year, month, day } = DateOnly.#dayNumberToDate(this.#dayNumber);
// Try to create DateTime if available
if (typeof DateTime !== 'undefined') {
if (kind !== undefined) {
return new DateTime(year, month, day,
time.hour, time.minute, time.second, millisecond, kind);
} else {
return new DateTime(year, month, day,
time.hour, time.minute, time.second, millisecond);
}
}
}
throw new TypeError("TimeOnly and DateTime classes must be available, or argument must be a TimeOnly instance");
}
/**
* Gets the earliest possible DateOnly value (January 1, 0001).
*
* @type {DateOnly}
* @readonly
* @static
* @example
* const minDate = DateOnly.minValue;
* console.log(minDate.toString()); // "0001-01-01"
*/
static get minValue() {
return DateOnly.#fromDayNumber(DateOnly.#MIN_DAY_NUMBER);
}
/**
* Gets the latest possible DateOnly value (December 31, 9999).
*
* @type {DateOnly}
* @readonly
* @static
* @example
* const maxDate = DateOnly.maxValue;
* console.log(maxDate.toString()); // "9999-12-31"
*/
static get maxValue() {
return DateOnly.#fromDayNumber(DateOnly.#MAX_DAY_NUMBER);
}
/**
* Compares two DateOnly instances.
*
* @param {DateOnly} d1 - The first DateOnly instance
* @param {DateOnly} d2 - The second DateOnly instance
* @returns {number}
* - Less than 0 if d1 is earlier than d2
* - 0 if d1 equals d2
* - Greater than 0 if d1 is later than d2
*
* @example
* const date1 = new DateOnly(2024, 12, 24);
* const date2 = new DateOnly(2024, 12, 25);
*
* console.log(DateOnly.compare(date1, date2)); // -1
* console.log(DateOnly.compare(date2, date1)); // 1
* console.log(DateOnly.compare(date1, date1)); // 0
*/
static compare(d1, d2) {
if (!(d1 instanceof DateOnly) || !(d2 instanceof DateOnly)) {
throw new TypeError("Both arguments must be DateOnly instances");
}
return d1.#dayNumber - d2.#dayNumber;
}
/**
* Returns the primitive value of the DateOnly instance (day number).
*
* This method is called automatically when the DateOnly is used in contexts
* that require a primitive value, such as arithmetic operations or comparisons.
*
* @returns {number} The day number representing this date
*
* @example
* const date = new DateOnly(2024, 12, 25);
* console.log(+date); // Outputs the day number
* console.log(date.valueOf()); // Same as above
*/
valueOf() {
return this.#dayNumber;
}
/**
* Parses a string representation of a date and returns a DateOnly instance.
*
* The string must be in ISO 8601 format (YYYY-MM-DD).
*
* @param {string} s - The string to parse
* @returns {DateOnly} A DateOnly instance representing the parsed date
* @throws {TypeError} If the input is not a string
* @throws {Error} If the string format is invalid
* @throws {RangeError} If the date components are out of valid range
*
* @example
* const date1 = DateOnly.parse("2024-12-25");
* const date2 = DateOnly.parse("2024-02-29"); // Valid leap year date
*
* // These will throw errors:
* // DateOnly.parse("25-12-2024"); // Wrong format
* // DateOnly.parse("2024-13-01"); // Invalid month
* // DateOnly.parse("2023-02-29"); // Invalid leap day
*/
static parse(s) {
if (typeof s !== "string") {
throw new TypeError("Input must be a string");
}
const match = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
throw new Error("Invalid date format. Expected YYYY-MM-DD");
}
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10);
const day = parseInt(match[3], 10);
return new DateOnly(year, month, day);
}
/**
* Attempts to parse a string representation of a date 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: DateOnly|null}}
* An object containing:
* - success: true if parsing succeeded, false otherwise
* - value: The parsed DateOnly instance if successful, null otherwise
*
* @example
* const result1 = DateOnly.tryParse("2024-12-25");
* if (result1.success) {
* console.log(result1.value.toString()); // "2024-12-25"
* }
*
* const result2 = DateOnly.tryParse("invalid-date");
* console.log(result2.success); // false
* console.log(result2.value); // null
*/
static tryParse(s) {
try {
return { success: true, value: DateOnly.parse(s) };
} catch {
return { success: false, value: null };
}
}
}