universal-common
Version:
Library that provides useful missing base class library functionality.
1,135 lines (1,015 loc) • 36.6 kB
JavaScript
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}]`;
}
}