UNPKG

universal-common

Version:

Library that provides useful missing base class library functionality.

1,135 lines (1,015 loc) 36.6 kB
import ArgumentError from './ArgumentError.js'; import DateTime from './DateTime.js'; import DateTimeKind from './DateTimeKind.js'; import TimeSpan from './TimeSpan.js'; /** * Provides culture-specific information about date and time formatting. * This class supplies the patterns, names, and other formatting data used by DateTime formatting. */ export default class DateTimeFormatInfo { // Internal properties #locale; #isReadOnly; #patterns; #names; #separators; #formatFlags; // Static caches static #invariantInfo = null; static #cultureCache = new Map(); /** * Creates a new DateTimeFormatInfo instance. * * @param {string} [locale='en-US'] - The locale identifier */ constructor(locale = 'en-US') { this.#locale = locale; this.#isReadOnly = false; this.#formatFlags = null; // Initialize with default patterns and names this.#initializeFromLocale(locale); } /** * Initializes the format info from a locale. * * @private * @param {string} locale - The locale identifier */ #initializeFromLocale(locale) { // Use Intl API to get locale-specific formatting information try { // Get date/time formatting patterns const dateFormatter = new Intl.DateTimeFormat(locale); const timeFormatter = new Intl.DateTimeFormat(locale, { timeStyle: 'medium' }); this.#patterns = this.#getPatterns(locale); this.#names = this.#getNames(locale); this.#separators = this.#getSeparators(locale); } catch (error) { // Fallback to invariant if locale is not supported this.#initializeInvariant(); } } /** * Gets patterns for the locale. * * @private * @param {string} locale - The locale identifier * @returns {Object} Patterns object */ #getPatterns(locale) { // These would ideally come from locale data // For now, providing reasonable defaults based on common patterns const isUS = locale.startsWith('en-US'); return { shortDatePattern: isUS ? 'M/d/yyyy' : 'dd/MM/yyyy', longDatePattern: 'dddd, MMMM d, yyyy', shortTimePattern: 'h:mm tt', longTimePattern: 'h:mm:ss tt', fullDateTimePattern: 'dddd, MMMM d, yyyy h:mm:ss tt', monthDayPattern: 'MMMM d', yearMonthPattern: 'MMMM yyyy', generalShortTimePattern: null, // Will be computed generalLongTimePattern: null, // Will be computed dateTimeOffsetPattern: null, // Will be computed rfc1123Pattern: 'ddd, dd MMM yyyy HH:mm:ss GMT', sortableDateTimePattern: 'yyyy-MM-ddTHH:mm:ss', universalSortableDateTimePattern: 'yyyy-MM-dd HH:mm:ssZ' }; } /** * Gets names for the locale. * * @private * @param {string} locale - The locale identifier * @returns {Object} Names object */ #getNames(locale) { const names = { monthNames: [], abbreviatedMonthNames: [], dayNames: [], abbreviatedDayNames: [], amDesignator: 'AM', pmDesignator: 'PM' }; try { // Get month names for (let i = 0; i < 12; i++) { const date = new Date(2000, i, 15); // Mid-month to avoid timezone issues const fullMonth = new Intl.DateTimeFormat(locale, { month: 'long' }).format(date); const shortMonth = new Intl.DateTimeFormat(locale, { month: 'short' }).format(date); names.monthNames.push(fullMonth); names.abbreviatedMonthNames.push(shortMonth); } names.monthNames.push(''); // 13th element for leap year support names.abbreviatedMonthNames.push(''); // Get day names (start with Sunday) for (let i = 0; i < 7; i++) { const date = new Date(2000, 0, 2 + i); // Start from Sunday (Jan 2, 2000) const fullDay = new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(date); const shortDay = new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(date); names.dayNames.push(fullDay); names.abbreviatedDayNames.push(shortDay); } // Get AM/PM designators const amDate = new Date(2000, 0, 1, 10, 0, 0); const pmDate = new Date(2000, 0, 1, 22, 0, 0); const amFormatted = new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true }).formatToParts(amDate); const pmFormatted = new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true }).formatToParts(pmDate); const amPart = amFormatted.find(part => part.type === 'dayPeriod'); const pmPart = pmFormatted.find(part => part.type === 'dayPeriod'); if (amPart) names.amDesignator = amPart.value; if (pmPart) names.pmDesignator = pmPart.value; } catch (error) { // Fallback to English names names.monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', '' ]; names.abbreviatedMonthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', '' ]; names.dayNames = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; names.abbreviatedDayNames = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]; } return names; } /** * Gets separators for the locale. * * @private * @param {string} locale - The locale identifier * @returns {Object} Separators object */ #getSeparators(locale) { try { // Try to extract separators from formatted dates const testDate = new Date(2000, 5, 15, 14, 30, 45); // June 15, 2000 2:30:45 PM const dateFormatted = new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit' }).format(testDate); const timeFormatted = new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(testDate); // Extract separators by looking for non-digit characters const dateSeparator = dateFormatted.match(/[^\d]/)?.[0] || '/'; const timeSeparator = timeFormatted.match(/[^\d]/)?.[0] || ':'; return { dateSeparator, timeSeparator }; } catch (error) { return { dateSeparator: '/', timeSeparator: ':' }; } } /** * Initializes as invariant culture. * * @private */ #initializeInvariant() { this.#patterns = { shortDatePattern: 'MM/dd/yyyy', longDatePattern: 'dddd, dd MMMM yyyy', shortTimePattern: 'HH:mm', longTimePattern: 'HH:mm:ss', fullDateTimePattern: 'dddd, dd MMMM yyyy HH:mm:ss', monthDayPattern: 'MMMM dd', yearMonthPattern: 'yyyy MMMM', rfc1123Pattern: 'ddd, dd MMM yyyy HH:mm:ss GMT', sortableDateTimePattern: 'yyyy-MM-ddTHH:mm:ss', universalSortableDateTimePattern: 'yyyy-MM-dd HH:mm:ssZ' }; this.#names = { monthNames: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', '' ], abbreviatedMonthNames: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', '' ], dayNames: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ], abbreviatedDayNames: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], amDesignator: 'AM', pmDesignator: 'PM' }; this.#separators = { dateSeparator: '/', timeSeparator: ':' }; } /** * Gets the computed patterns, including derived ones. * * @private * @returns {Object} All patterns */ #getAllPatterns() { if (!this.#patterns.generalShortTimePattern) { this.#patterns.generalShortTimePattern = `${this.#patterns.shortDatePattern} ${this.#patterns.shortTimePattern}`; } if (!this.#patterns.generalLongTimePattern) { this.#patterns.generalLongTimePattern = `${this.#patterns.shortDatePattern} ${this.#patterns.longTimePattern}`; } if (!this.#patterns.dateTimeOffsetPattern) { this.#patterns.dateTimeOffsetPattern = `${this.#patterns.shortDatePattern} ${this.#patterns.longTimePattern} zzz`; } return this.#patterns; } // Public properties /** * Gets or sets the string that separates the components of a date. * * @type {string} */ get dateSeparator() { return this.#separators.dateSeparator; } set dateSeparator(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Date separator must be a string"); } this.#separators.dateSeparator = value; } /** * Gets or sets the string that separates the components of time. * * @type {string} */ get timeSeparator() { return this.#separators.timeSeparator; } set timeSeparator(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Time separator must be a string"); } this.#separators.timeSeparator = value; } /** * Gets or sets the string designator for hours that are "ante meridiem" (before noon). * * @type {string} */ get amDesignator() { return this.#names.amDesignator; } set amDesignator(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("AM designator must be a string"); } this.#names.amDesignator = value; } /** * Gets or sets the string designator for hours that are "post meridiem" (after noon). * * @type {string} */ get pmDesignator() { return this.#names.pmDesignator; } set pmDesignator(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("PM designator must be a string"); } this.#names.pmDesignator = value; } // Pattern properties /** * Gets or sets the custom format string for a short date pattern. * * @type {string} */ get shortDatePattern() { return this.#patterns.shortDatePattern; } set shortDatePattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Short date pattern must be a string"); } this.#patterns.shortDatePattern = value; // Clear computed patterns this.#patterns.generalShortTimePattern = null; this.#patterns.generalLongTimePattern = null; this.#patterns.dateTimeOffsetPattern = null; } /** * Gets or sets the custom format string for a long date pattern. * * @type {string} */ get longDatePattern() { return this.#patterns.longDatePattern; } set longDatePattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Long date pattern must be a string"); } this.#patterns.longDatePattern = value; this.#patterns.fullDateTimePattern = null; } /** * Gets or sets the custom format string for a short time pattern. * * @type {string} */ get shortTimePattern() { return this.#patterns.shortTimePattern; } set shortTimePattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Short time pattern must be a string"); } this.#patterns.shortTimePattern = value; this.#patterns.generalShortTimePattern = null; } /** * Gets or sets the custom format string for a long time pattern. * * @type {string} */ get longTimePattern() { return this.#patterns.longTimePattern; } set longTimePattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Long time pattern must be a string"); } this.#patterns.longTimePattern = value; this.#patterns.fullDateTimePattern = null; this.#patterns.generalLongTimePattern = null; this.#patterns.dateTimeOffsetPattern = null; } /** * Gets or sets the custom format string for a full date and time pattern. * * @type {string} */ get fullDateTimePattern() { if (!this.#patterns.fullDateTimePattern) { this.#patterns.fullDateTimePattern = `${this.longDatePattern} ${this.longTimePattern}`; } return this.#patterns.fullDateTimePattern; } set fullDateTimePattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Full date time pattern must be a string"); } this.#patterns.fullDateTimePattern = value; } /** * Gets or sets the custom format string for a month and day pattern. * * @type {string} */ get monthDayPattern() { return this.#patterns.monthDayPattern; } set monthDayPattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Month day pattern must be a string"); } this.#patterns.monthDayPattern = value; } /** * Gets or sets the custom format string for a year and month pattern. * * @type {string} */ get yearMonthPattern() { return this.#patterns.yearMonthPattern; } set yearMonthPattern(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (typeof value !== 'string') { throw new TypeError("Year month pattern must be a string"); } this.#patterns.yearMonthPattern = value; } // Computed pattern properties /** * Gets the general short time pattern (short date + short time). * * @type {string} * @readonly */ get generalShortTimePattern() { const patterns = this.#getAllPatterns(); return patterns.generalShortTimePattern; } /** * Gets the general long time pattern (short date + long time). * * @type {string} * @readonly */ get generalLongTimePattern() { const patterns = this.#getAllPatterns(); return patterns.generalLongTimePattern; } /** * Gets the DateTimeOffset pattern (short date + long time + offset). * * @type {string} * @readonly */ get dateTimeOffsetPattern() { const patterns = this.#getAllPatterns(); return patterns.dateTimeOffsetPattern; } // Standard patterns (read-only) /** * Gets the RFC1123 pattern. * * @type {string} * @readonly */ get rfc1123Pattern() { return this.#patterns.rfc1123Pattern; } /** * Gets the sortable date time pattern. * * @type {string} * @readonly */ get sortableDateTimePattern() { return this.#patterns.sortableDateTimePattern; } /** * Gets the universal sortable date time pattern. * * @type {string} * @readonly */ get universalSortableDateTimePattern() { return this.#patterns.universalSortableDateTimePattern; } // Name arrays /** * Gets or sets a one-dimensional array of type String containing the culture-specific full names of the months. * * @type {string[]} */ get monthNames() { return [...this.#names.monthNames]; // Return copy } set monthNames(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (!Array.isArray(value) || value.length !== 13) { throw new ArgumentError("Month names must be an array of 13 strings"); } this.#names.monthNames = [...value]; } /** * Gets or sets a one-dimensional array of type String containing the culture-specific abbreviated names of the months. * * @type {string[]} */ get abbreviatedMonthNames() { return [...this.#names.abbreviatedMonthNames]; // Return copy } set abbreviatedMonthNames(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (!Array.isArray(value) || value.length !== 13) { throw new ArgumentError("Abbreviated month names must be an array of 13 strings"); } this.#names.abbreviatedMonthNames = [...value]; } /** * Gets or sets a one-dimensional array of type String containing the culture-specific full names of the days of the week. * * @type {string[]} */ get dayNames() { return [...this.#names.dayNames]; // Return copy } set dayNames(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (!Array.isArray(value) || value.length !== 7) { throw new ArgumentError("Day names must be an array of 7 strings"); } this.#names.dayNames = [...value]; } /** * Gets or sets a one-dimensional array of type String containing the culture-specific abbreviated names of the days of the week. * * @type {string[]} */ get abbreviatedDayNames() { return [...this.#names.abbreviatedDayNames]; // Return copy } set abbreviatedDayNames(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (!Array.isArray(value) || value.length !== 7) { throw new ArgumentError("Abbreviated day names must be an array of 7 strings"); } this.#names.abbreviatedDayNames = [...value]; } // Helper flags /** * Gets whether genitive month names should be used. * * @type {boolean} * @readonly */ get useGenitiveMonth() { // For simplicity, assume false for now // Real implementation would analyze the culture return false; } /** * Gets a value indicating whether this DateTimeFormatInfo object is read-only. * * @type {boolean} * @readonly */ get isReadOnly() { return this.#isReadOnly; } // Methods /** * Gets the full name of the specified month. * * @param {number} month - An integer from 1 to 12 representing the month * @param {string} [style='regular'] - The style ('regular', 'genitive', 'leap') * @param {boolean} [abbreviated=false] - Whether to return abbreviated name * @returns {string} The full name of the month */ getMonthName(month, style = 'regular', abbreviated = false) { if (month < 1 || month > 12) { throw new RangeError("Month must be between 1 and 12"); } const names = abbreviated ? this.#names.abbreviatedMonthNames : this.#names.monthNames; // For now, ignore style and return regular month name // Real implementation would handle genitive/leap year forms return names[month - 1]; } /** * Gets the abbreviated name of the specified month. * * @param {number} month - An integer from 1 to 12 representing the month * @returns {string} The abbreviated name of the month */ getAbbreviatedMonthName(month) { return this.getMonthName(month, 'regular', true); } /** * Gets the full name of the specified day of the week. * * @param {number} dayOfWeek - An integer from 0 to 6 representing the day (Sunday = 0) * @returns {string} The full name of the day of the week */ getDayName(dayOfWeek) { if (dayOfWeek < 0 || dayOfWeek > 6) { throw new RangeError("Day of week must be between 0 and 6"); } return this.#names.dayNames[dayOfWeek]; } /** * Gets the abbreviated name of the specified day of the week. * * @param {number} dayOfWeek - An integer from 0 to 6 representing the day (Sunday = 0) * @returns {string} The abbreviated name of the day of the week */ getAbbreviatedDayName(dayOfWeek) { if (dayOfWeek < 0 || dayOfWeek > 6) { throw new RangeError("Day of week must be between 0 and 6"); } return this.#names.abbreviatedDayNames[dayOfWeek]; } /** * Gets the era name for the current calendar. * * @param {DateTime} dateTime - The DateTime to get era for * @returns {string} The era name */ getEraName(dateTime) { // For Gregorian calendar, always return A.D. return 'A.D.'; } /** * Creates a shallow copy of the DateTimeFormatInfo. * * @returns {DateTimeFormatInfo} A shallow copy of the DateTimeFormatInfo */ clone() { const cloned = new DateTimeFormatInfo(this.#locale); cloned.#patterns = { ...this.#patterns }; cloned.#names = { monthNames: [...this.#names.monthNames], abbreviatedMonthNames: [...this.#names.abbreviatedMonthNames], dayNames: [...this.#names.dayNames], abbreviatedDayNames: [...this.#names.abbreviatedDayNames], amDesignator: this.#names.amDesignator, pmDesignator: this.#names.pmDesignator }; cloned.#separators = { ...this.#separators }; cloned.#isReadOnly = false; return cloned; } /** * Returns a read-only DateTimeFormatInfo wrapper. * * @param {DateTimeFormatInfo} dtfi - The DateTimeFormatInfo to make read-only * @returns {DateTimeFormatInfo} A read-only wrapper */ static readOnly(dtfi) { if (!(dtfi instanceof DateTimeFormatInfo)) { throw new TypeError("Argument must be a DateTimeFormatInfo instance"); } if (dtfi.isReadOnly) { return dtfi; } const readOnlyInfo = dtfi.clone(); readOnlyInfo.#isReadOnly = true; return readOnlyInfo; } // Static properties and methods /** * Gets a read-only DateTimeFormatInfo that formats values based on the invariant culture. * * @type {DateTimeFormatInfo} * @readonly * @static */ static get invariantInfo() { if (!DateTimeFormatInfo.#invariantInfo) { const invariant = new DateTimeFormatInfo('en-US'); invariant.#initializeInvariant(); DateTimeFormatInfo.#invariantInfo = DateTimeFormatInfo.readOnly(invariant); } return DateTimeFormatInfo.#invariantInfo; } /** * Gets a read-only DateTimeFormatInfo that formats values based on the current culture. * * @type {DateTimeFormatInfo} * @readonly * @static */ static get currentInfo() { // Use the system default locale const locale = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'en-US'; return DateTimeFormatInfo.getInstance(locale); } /** * Returns a DateTimeFormatInfo associated with the specified locale. * * @param {string} [locale] - The locale identifier, or null for current culture * @returns {DateTimeFormatInfo} A DateTimeFormatInfo associated with the locale */ static getInstance(locale = null) { if (!locale) { return DateTimeFormatInfo.currentInfo; } // Check cache first if (DateTimeFormatInfo.#cultureCache.has(locale)) { return DateTimeFormatInfo.#cultureCache.get(locale); } // Create new instance const dtfi = new DateTimeFormatInfo(locale); const readOnlyDtfi = DateTimeFormatInfo.readOnly(dtfi); // Cache it DateTimeFormatInfo.#cultureCache.set(locale, readOnlyDtfi); return readOnlyDtfi; } /** * Gets all the standard patterns for the specified format character. * * @param {string} format - Standard format character * @returns {string[]} Array of patterns for the format */ getAllDateTimePatterns(format) { switch (format) { case 'd': return [this.shortDatePattern]; case 'D': return [this.longDatePattern]; case 'f': return [`${this.longDatePattern} ${this.shortTimePattern}`]; case 'F': return [this.fullDateTimePattern]; case 'g': return [this.generalShortTimePattern]; case 'G': return [this.generalLongTimePattern]; case 'm': case 'M': return [this.monthDayPattern]; case 'o': case 'O': return ['yyyy-MM-ddTHH:mm:ss.fffffffK']; case 'r': case 'R': return [this.rfc1123Pattern]; case 's': return [this.sortableDateTimePattern]; case 't': return [this.shortTimePattern]; case 'T': return [this.longTimePattern]; case 'u': return [this.universalSortableDateTimePattern]; case 'U': return [this.fullDateTimePattern]; case 'y': case 'Y': return [this.yearMonthPattern]; default: throw new ArgumentError(`Invalid format character: ${format}`); } } /** * Gets all possible DateTime patterns for all standard formats. * * @returns {string[]} Array of all standard patterns */ getAllDateTimePatterns() { const allPatterns = []; const standardFormats = 'dDfFgGmMoOrRstTuUyY'; for (const format of standardFormats) { allPatterns.push(...this.getAllDateTimePatterns(format)); } return allPatterns; } /** * Sets all the date time patterns for a specific standard format character. * * @param {string[]} patterns - Array of patterns * @param {string} format - Standard format character */ setAllDateTimePatterns(patterns, format) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } if (!Array.isArray(patterns) || patterns.length === 0) { throw new ArgumentError("Patterns must be a non-empty array"); } for (let i = 0; i < patterns.length; i++) { if (typeof patterns[i] !== 'string') { throw new ArgumentError(`Pattern at index ${i} must be a string`); } } // Set the first pattern as the default for the format switch (format) { case 'd': this.shortDatePattern = patterns[0]; break; case 'D': this.longDatePattern = patterns[0]; break; case 't': this.shortTimePattern = patterns[0]; break; case 'T': this.longTimePattern = patterns[0]; break; case 'y': case 'Y': this.yearMonthPattern = patterns[0]; break; default: throw new ArgumentError(`Cannot set patterns for format character: ${format}`); } } /** * Validates a custom format string for DateOnly. * * @param {string} format - Format string to validate * @param {boolean} throwOnError - Whether to throw on invalid format * @returns {boolean} Whether the format is valid for DateOnly */ static isValidCustomDateOnlyFormat(format, throwOnError = false) { let i = 0; while (i < format.length) { switch (format[i]) { case '\\': if (i === format.length - 1) { if (throwOnError) { throw new ArgumentError("Invalid format string"); } return false; } i += 2; break; case "'": case '"': const quoteChar = format[i++]; while (i < format.length && format[i] !== quoteChar) { i++; } if (i >= format.length) { if (throwOnError) { throw new ArgumentError(`Unmatched quote: ${quoteChar}`); } return false; } i++; break; case ':': case 't': case 'f': case 'F': case 'h': case 'H': case 'm': case 's': case 'z': case 'K': // Reject time-related formats if (throwOnError) { throw new ArgumentError("Invalid format string for DateOnly"); } return false; default: i++; break; } } return true; } /** * Validates a custom format string for TimeOnly. * * @param {string} format - Format string to validate * @param {boolean} throwOnError - Whether to throw on invalid format * @returns {boolean} Whether the format is valid for TimeOnly */ static isValidCustomTimeOnlyFormat(format, throwOnError = false) { let i = 0; while (i < format.length) { switch (format[i]) { case '\\': if (i === format.length - 1) { if (throwOnError) { throw new ArgumentError("Invalid format string"); } return false; } i += 2; break; case "'": case '"': const quoteChar = format[i++]; while (i < format.length && format[i] !== quoteChar) { i++; } if (i >= format.length) { if (throwOnError) { throw new ArgumentError(`Unmatched quote: ${quoteChar}`); } return false; } i++; break; case 'd': case 'M': case 'y': case '/': case 'z': case 'K': // Reject date-related formats if (throwOnError) { throw new ArgumentError("Invalid format string for TimeOnly"); } return false; default: i++; break; } } return true; } /** * Gets the native calendar name. * * @type {string} * @readonly */ get nativeCalendarName() { // For now, assume Gregorian calendar return 'Gregorian Calendar'; } /** * Gets or sets the calendar used for formatting. * * @type {Object} */ get calendar() { // Simple calendar object for now return { isNonGregorian: false, id: 1 // Gregorian }; } set calendar(value) { if (this.#isReadOnly) { throw new Error("DateTimeFormatInfo is read-only"); } // Calendar setting would be implemented here } /** * Gets the first day of the week. * * @type {number} * @readonly */ get firstDayOfWeek() { // Use Intl API to determine first day of week for locale try { const locale = new Intl.Locale(this.#locale); const weekInfo = locale.getWeekInfo?.(); if (weekInfo) { // Convert from ISO week day (Monday=1) to JavaScript (Sunday=0) return weekInfo.firstDay === 7 ? 0 : weekInfo.firstDay; } } catch (error) { // Fallback } // Default to Sunday for most locales, Monday for European locales return this.#locale.startsWith('en') ? 0 : 1; } /** * Gets the rule used to determine the first week of the year. * * @type {number} * @readonly */ get calendarWeekRule() { // 0 = FirstDay, 1 = FirstFullWeek, 2 = FirstFourDayWeek // Most cultures use FirstDay, European cultures often use FirstFourDayWeek return this.#locale.startsWith('en') ? 0 : 2; } /** * Returns a string representation of the DateTimeFormatInfo. * * @returns {string} String representation */ toString() { return `DateTimeFormatInfo [${this.#locale}]`; } }