@@ -10,3 +10,178 @@ export function getWeekStartsOnFromIntl(locales?: string): DayOfWeek {
1010 const weekInfo = locale . weekInfo ?? locale . getWeekInfo ?.( ) ;
1111 return ( weekInfo ?. firstDay ?? 0 ) % 7 ; // (in Intl, sunday is 7 not 0, so we need to mod 7)
1212}
13+
14+ /**
15+ * Unicode to strftime format mapping
16+ * Based on Unicode TR35 Date Field Symbol Table and POSIX strftime
17+ * @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
18+ * @see https://pubs.opengroup.org/onlinepubs/9699919799/functions/strftime.html
19+ */
20+ const unicodeToStrftime = {
21+ // ===== YEAR =====
22+ y : '%y' , // 2-digit year (00-99)
23+ yy : '%y' , // 2-digit year with leading zero
24+ yyyy : '%Y' , // 4-digit year
25+ Y : '%Y' , // 4-digit year (short form)
26+
27+ // ===== MONTH =====
28+ M : '%m' , // Month as number (1-12, but strftime uses 01-12)
29+ MM : '%m' , // Month as 2-digit number (01-12)
30+ MMM : '%b' , // Abbreviated month name (Jan, Feb, etc.)
31+ MMMM : '%B' , // Full month name (January, February, etc.)
32+ L : '%m' , // Standalone month number (same as M in most cases)
33+ LL : '%m' , // Standalone month number, 2-digit
34+ LLL : '%b' , // Standalone abbreviated month name
35+ LLLL : '%B' , // Standalone full month name
36+
37+ // ===== WEEK =====
38+ w : null , // ❌ Week of year (1-53) - no direct strftime equivalent
39+ ww : null , // ❌ Week of year, 2-digit - no direct strftime equivalent
40+ W : '%W' , // Week of year (Monday as first day) - close match
41+
42+ // ===== DAY =====
43+ d : '%d' , // Day of month (1-31, but strftime uses 01-31)
44+ dd : '%d' , // Day of month, 2-digit (01-31)
45+ D : '%j' , // Day of year (1-366, but strftime uses 001-366)
46+ DD : '%j' , // Day of year, 2-digit - strftime always uses 3 digits
47+ DDD : '%j' , // Day of year, 3-digit (001-366)
48+
49+ // ===== WEEKDAY =====
50+ E : '%a' , // Abbreviated weekday name (Mon, Tue, etc.)
51+ EE : '%a' , // Abbreviated weekday name
52+ EEE : '%a' , // Abbreviated weekday name
53+ EEEE : '%A' , // Full weekday name (Monday, Tuesday, etc.)
54+ EEEEE : null , // ❌ Narrow weekday name (M, T, W) - no strftime equivalent
55+ EEEEEE : null , // ❌ Short weekday name - no strftime equivalent
56+ e : '%u' , // Local weekday number (1-7, Monday=1) - close match
57+ ee : '%u' , // Local weekday number, 2-digit
58+ eee : '%a' , // Local abbreviated weekday name
59+ eeee : '%A' , // Local full weekday name
60+ c : '%u' , // Standalone weekday number
61+ cc : '%u' , // Standalone weekday number, 2-digit
62+ ccc : '%a' , // Standalone abbreviated weekday name
63+ cccc : '%A' , // Standalone full weekday name
64+
65+ // ===== PERIOD (AM/PM) =====
66+ a : '%p' , // AM/PM
67+ aa : '%p' , // AM/PM
68+ aaa : '%p' , // AM/PM
69+ aaaa : '%p' , // AM/PM (long form, but strftime only has short)
70+ aaaaa : null , // ❌ Narrow AM/PM (A/P) - no strftime equivalent
71+
72+ // ===== HOUR =====
73+ h : '%I' , // Hour in 12-hour format (1-12)
74+ hh : '%I' , // Hour in 12-hour format, 2-digit (01-12)
75+ H : '%H' , // Hour in 24-hour format (0-23)
76+ HH : '%H' , // Hour in 24-hour format, 2-digit (00-23)
77+ K : null , // ❌ Hour in 12-hour format (0-11) - no direct strftime equivalent
78+ KK : null , // ❌ Hour in 12-hour format, 2-digit (00-11) - no strftime equivalent
79+ k : null , // ❌ Hour in 24-hour format (1-24) - no direct strftime equivalent
80+ kk : null , // ❌ Hour in 24-hour format, 2-digit (01-24) - no strftime equivalent
81+
82+ // ===== MINUTE =====
83+ m : '%M' , // Minutes (0-59)
84+ mm : '%M' , // Minutes, 2-digit (00-59)
85+
86+ // ===== SECOND =====
87+ s : '%S' , // Seconds (0-59)
88+ ss : '%S' , // Seconds, 2-digit (00-59)
89+ S : null , // ❌ Fractional seconds (1 digit) - no direct strftime equivalent
90+ SS : null , // ❌ Fractional seconds (2 digits) - no direct strftime equivalent
91+ SSS : null , // ❌ Fractional seconds (3 digits) - no direct strftime equivalent
92+ A : null , // ❌ Milliseconds in day - no strftime equivalent
93+
94+ // ===== TIMEZONE =====
95+ z : '%Z' , // Timezone name (EST, PST, etc.)
96+ zz : '%Z' , // Timezone name
97+ zzz : '%Z' , // Timezone name
98+ zzzz : '%Z' , // Full timezone name
99+ Z : '%z' , // Timezone offset (+0000, -0500, etc.)
100+ ZZ : '%z' , // Timezone offset
101+ ZZZ : '%z' , // Timezone offset
102+ ZZZZ : null , // ❌ GMT-relative timezone - partial strftime support
103+ ZZZZZ : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
104+ O : null , // ❌ Localized GMT offset - no strftime equivalent
105+ OOOO : null , // ❌ Full localized GMT offset - no strftime equivalent
106+ v : null , // ❌ Generic timezone - no strftime equivalent
107+ vvvv : null , // ❌ Generic timezone full - no strftime equivalent
108+ V : null , // ❌ Timezone ID - no strftime equivalent
109+ VV : null , // ❌ Timezone ID - no strftime equivalent
110+ VVV : null , // ❌ Timezone exemplar city - no strftime equivalent
111+ VVVV : null , // ❌ Generic location format - no strftime equivalent
112+ X : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
113+ XX : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
114+ XXX : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
115+ XXXX : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
116+ XXXXX : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
117+ x : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
118+ xx : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
119+ xxx : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
120+ xxxx : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
121+ xxxxx : null , // ❌ ISO 8601 timezone - no direct strftime equivalent
122+
123+ // ===== QUARTER =====
124+ Q : null , // ❌ Quarter (1-4) - no strftime equivalent
125+ QQ : null , // ❌ Quarter, 2-digit (01-04) - no strftime equivalent
126+ QQQ : null , // ❌ Abbreviated quarter (Q1, Q2, etc.) - no strftime equivalent
127+ QQQQ : null , // ❌ Full quarter (1st quarter, etc.) - no strftime equivalent
128+ QQQQQ : null , // ❌ Narrow quarter - no strftime equivalent
129+ q : null , // ❌ Standalone quarter - no strftime equivalent
130+ qq : null , // ❌ Standalone quarter, 2-digit - no strftime equivalent
131+ qqq : null , // ❌ Standalone abbreviated quarter - no strftime equivalent
132+ qqqq : null , // ❌ Standalone full quarter - no strftime equivalent
133+ qqqqq : null , // ❌ Standalone narrow quarter - no strftime equivalent
134+
135+ // ===== ERA =====
136+ G : null , // ❌ Era designator (AD, BC) - no strftime equivalent
137+ GG : null , // ❌ Era designator - no strftime equivalent
138+ GGG : null , // ❌ Era designator - no strftime equivalent
139+ GGGG : null , // ❌ Era designator full - no strftime equivalent
140+ GGGGG : null , // ❌ Era designator narrow - no strftime equivalent
141+ } ;
142+
143+ /**
144+ * Convert a Unicode format string to strftime format
145+ * @param unicodeFormat - The Unicode format string to convert
146+ * @returns The strftime format string
147+ */
148+ export function convertUnicodeToStrftime ( unicodeFormat : string ) {
149+ let result = '' ;
150+ let i = 0 ;
151+ let unsupportedPatterns = [ ] ;
152+
153+ while ( i < unicodeFormat . length ) {
154+ let matched = false ;
155+
156+ // Try to match the longest possible pattern starting at current position
157+ for ( let len = Math . min ( 5 , unicodeFormat . length - i ) ; len >= 1 ; len -- ) {
158+ const pattern = unicodeFormat . substring ( i , i + len ) ;
159+ if ( pattern in unicodeToStrftime ) {
160+ const strftimeEquivalent = unicodeToStrftime [ pattern as keyof typeof unicodeToStrftime ] ;
161+
162+ if ( strftimeEquivalent === null ) {
163+ unsupportedPatterns . push ( pattern ) ;
164+ result += pattern ; // Keep original if unsupported
165+ } else {
166+ result += strftimeEquivalent ;
167+ }
168+
169+ i += len ;
170+ matched = true ;
171+ break ;
172+ }
173+ }
174+
175+ // If no pattern matched, copy the character as-is
176+ if ( ! matched ) {
177+ result += unicodeFormat [ i ] ;
178+ i ++ ;
179+ }
180+ }
181+
182+ if ( unsupportedPatterns . length > 0 ) {
183+ console . warn ( 'Unsupported patterns:' , [ ...new Set ( unsupportedPatterns ) ] ) ;
184+ }
185+
186+ return result ;
187+ }
0 commit comments