Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/format/pattern.rs

16.5 KiB, 58 runs

created by r1870400018:6369, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Format patterns and the tokens they are built from.
2//!
3//! A pattern string is parsed once into a token sequence, which the formatter
4//! then walks.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use oxedyne_fe2o3_core::prelude::*;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub enum FormatStyle {
13 Short, // "Jan", "Mon", "1"
14 Medium, // "Jan 15", "Monday"
15 Long, // "January", "Monday"
16 Full, // "Monday, January 15, 2024"
17 Numeric, // "01", "15"
18 Custom { width: usize, pad_char: char },
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub enum FormatToken {
23 // Date tokens
24 Year(FormatStyle),
25 Month(FormatStyle),
26 Day(FormatStyle),
27 DayOfWeek(FormatStyle),
28 DayOfYear(FormatStyle),
29 WeekOfYear(FormatStyle),
30 Quarter(FormatStyle),
31
32 // Time tokens
33 Hour12(FormatStyle),
34 Hour24(FormatStyle),
35 Minute(FormatStyle),
36 Second(FormatStyle),
37 Millisecond(FormatStyle),
38 Microsecond(FormatStyle),
39 Nanosecond(FormatStyle),
40 AmPm(FormatStyle),
41
42 // Timezone tokens
43 TimezoneId(FormatStyle),
44 TimezoneOffset(FormatStyle),
45 TimezoneName(FormatStyle),
46
47 // Era tokens
48 Era(FormatStyle),
49
50 // Literal text
51 Literal(String),
52
53 // Special characters
54 Space,
55 Colon,
56 Dash,
57 Slash,
58 Comma,
59 Period,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Hash)]
63pub struct FormatPattern {
64 tokens: Vec<FormatToken>,
65 pattern_string: String,
66}
67
68impl FormatPattern {
69 /// # Pattern Syntax
70 ///
71 /// ## Date Patterns
72 /// - `yyyy` - 4-digit year (e.g., 2024)
73 /// - `yy` - 2-digit year (e.g., 24)
74 /// - `MMMM` - Full month name (e.g., January)
75 /// - `MMM` - Short month name (e.g., Jan)
76 /// - `MM` - 2-digit month (e.g., 01)
77 /// - `M` - Month number (e.g., 1)
78 /// - `dd` - 2-digit day (e.g., 05)
79 /// - `d` - Day number (e.g., 5)
80 /// - `EEEE` - Full day name (e.g., Monday)
81 /// - `EEE` - Short day name (e.g., Mon)
82 /// - `DDD` - Day of year (e.g., 365)
83 /// - `Q` - Quarter (e.g., 1)
84 /// - `w` - Week of year (e.g., 42)
85 /// - `ww` - 2-digit week of year (e.g., 02)
86 /// - `G` - Era abbreviation (e.g., CE, BCE)
87 /// - `GGGG` - Full era name (e.g., Common Era)
88 ///
89 /// ## Time Patterns
90 /// - `HH` - 24-hour format hour (e.g., 14)
91 /// - `H` - 24-hour format hour, no padding (e.g., 14)
92 /// - `hh` - 12-hour format hour (e.g., 02)
93 /// - `h` - 12-hour format hour, no padding (e.g., 2)
94 /// - `mm` - Minute (e.g., 30)
95 /// - `m` - Minute, no padding (e.g., 30)
96 /// - `ss` - Second (e.g., 45)
97 /// - `s` - Second, no padding (e.g., 45)
98 /// - `SSS` - Millisecond (e.g., 123)
99 /// - `SSSSSS` - Microsecond (e.g., 123456)
100 /// - `SSSSSSSSS` - Nanosecond (e.g., 123456789)
101 /// - `a` - AM/PM marker
102 ///
103 /// ## Timezone Patterns
104 /// - `z` - Timezone name (e.g., UTC, EST)
105 /// - `Z` - Timezone offset (e.g., +0000, -0500)
106 /// - `v` - Timezone ID (e.g., America/New_York)
107 ///
108 /// ## Literal Text
109 /// - Any text not matching the above patterns is treated as literal
110 /// - Use single quotes to escape pattern letters: 'yyyy' becomes "yyyy"
111 ///
112 /// # Examples
113 /// ```ignore
114 /// let pattern = FormatPattern::new("yyyy-MM-dd HH:mm:ss")?res!();
115 /// let pattern = FormatPattern::new("MMMM d, yyyy 'at' h:mm a")?res!();
116 /// let pattern = FormatPattern::new("EEE, MMM d, ''yy")?res!();
117 /// ```
118 pub fn new(pattern: &str) -> Outcome<Self> {
119 let tokens = res!(Self::parse_pattern(pattern));
120 Ok(Self {
121 tokens,
122 pattern_string: pattern.to_string(),
123 })
124 }
125
126 pub fn tokens(&self) -> &[FormatToken] {
127 &self.tokens
128 }
129
130 pub fn pattern_string(&self) -> &str {
131 &self.pattern_string
132 }
133
134 fn parse_pattern(pattern: &str) -> Outcome<Vec<FormatToken>> {
135 let mut tokens = Vec::new();
136 let mut chars = pattern.chars().peekable();
137
138 while let Some(ch) = chars.next() {
139 match ch {
140 // Handle quoted literals
141 '\'' => {
142 let mut literal = String::new();
143 let mut escaped = false;
144
145 while let Some(ch) = chars.next() {
146 if ch == '\'' {
147 if chars.peek() == Some(&'\'') {
148 // Double quote becomes single quote
149 chars.next();
150 literal.push('\'');
151 } else {
152 // End of quoted section
153 escaped = true;
154 break;
155 }
156 } else {
157 literal.push(ch);
158 }
159 }
160
161 if !escaped {
162 return Err(err!("Unterminated quoted literal in pattern"; Invalid, Input));
163 }
164
165 if !literal.is_empty() {
166 tokens.push(FormatToken::Literal(literal));
167 }
168 },
169
170 // Year patterns
171 'y' => {
172 let count = Self::count_consecutive(&mut chars, 'y') + 1;
173 match count {
174 2 => tokens.push(FormatToken::Year(FormatStyle::Custom { width: 2, pad_char: '0' })),
175 4 => tokens.push(FormatToken::Year(FormatStyle::Numeric)),
176 _ => tokens.push(FormatToken::Year(FormatStyle::Numeric)),
177 }
178 },
179
180 // Month patterns
181 'M' => {
182 let count = Self::count_consecutive(&mut chars, 'M') + 1;
183 match count {
184 1 => tokens.push(FormatToken::Month(FormatStyle::Numeric)),
185 2 => tokens.push(FormatToken::Month(FormatStyle::Custom { width: 2, pad_char: '0' })),
186 3 => tokens.push(FormatToken::Month(FormatStyle::Short)),
187 4 => tokens.push(FormatToken::Month(FormatStyle::Long)),
188 _ => tokens.push(FormatToken::Month(FormatStyle::Long)),
189 }
190 },
191
192 // Day patterns
193 'd' => {
194 let count = Self::count_consecutive(&mut chars, 'd') + 1;
195 match count {
196 1 => tokens.push(FormatToken::Day(FormatStyle::Numeric)),
197 2 => tokens.push(FormatToken::Day(FormatStyle::Custom { width: 2, pad_char: '0' })),
198 _ => tokens.push(FormatToken::Day(FormatStyle::Custom { width: 2, pad_char: '0' })),
199 }
200 },
201
202 // Day of week patterns
203 'E' => {
204 let count = Self::count_consecutive(&mut chars, 'E') + 1;
205 match count {
206 1..=3 => tokens.push(FormatToken::DayOfWeek(FormatStyle::Short)),
207 4 => tokens.push(FormatToken::DayOfWeek(FormatStyle::Long)),
208 _ => tokens.push(FormatToken::DayOfWeek(FormatStyle::Long)),
209 }
210 },
211
212 // Day of year patterns
213 'D' => {
214 let count = Self::count_consecutive(&mut chars, 'D') + 1;
215 match count {
216 1 => tokens.push(FormatToken::DayOfYear(FormatStyle::Numeric)),
217 2 => tokens.push(FormatToken::DayOfYear(FormatStyle::Custom { width: 2, pad_char: '0' })),
218 3 => tokens.push(FormatToken::DayOfYear(FormatStyle::Custom { width: 3, pad_char: '0' })),
219 _ => tokens.push(FormatToken::DayOfYear(FormatStyle::Custom { width: 3, pad_char: '0' })),
220 }
221 },
222
223 // Quarter patterns
224 'Q' => {
225 let _count = Self::count_consecutive(&mut chars, 'Q');
226 tokens.push(FormatToken::Quarter(FormatStyle::Numeric));
227 },
228
229 // Week of year patterns
230 'w' => {
231 let count = Self::count_consecutive(&mut chars, 'w') + 1;
232 match count {
233 1 => tokens.push(FormatToken::WeekOfYear(FormatStyle::Numeric)),
234 2 => tokens.push(FormatToken::WeekOfYear(FormatStyle::Custom { width: 2, pad_char: '0' })),
235 _ => tokens.push(FormatToken::WeekOfYear(FormatStyle::Custom { width: 2, pad_char: '0' })),
236 }
237 },
238
239 // Era patterns
240 'G' => {
241 let count = Self::count_consecutive(&mut chars, 'G') + 1;
242 match count {
243 1 => tokens.push(FormatToken::Era(FormatStyle::Short)),
244 2..=4 => tokens.push(FormatToken::Era(FormatStyle::Long)),
245 _ => tokens.push(FormatToken::Era(FormatStyle::Long)),
246 }
247 },
248
249 // 24-hour patterns
250 'H' => {
251 let count = Self::count_consecutive(&mut chars, 'H') + 1;
252 match count {
253 1 => tokens.push(FormatToken::Hour24(FormatStyle::Numeric)),
254 2 => tokens.push(FormatToken::Hour24(FormatStyle::Custom { width: 2, pad_char: '0' })),
255 _ => tokens.push(FormatToken::Hour24(FormatStyle::Custom { width: 2, pad_char: '0' })),
256 }
257 },
258
259 // 12-hour patterns
260 'h' => {
261 let count = Self::count_consecutive(&mut chars, 'h') + 1;
262 match count {
263 1 => tokens.push(FormatToken::Hour12(FormatStyle::Numeric)),
264 2 => tokens.push(FormatToken::Hour12(FormatStyle::Custom { width: 2, pad_char: '0' })),
265 _ => tokens.push(FormatToken::Hour12(FormatStyle::Custom { width: 2, pad_char: '0' })),
266 }
267 },
268
269 // Minute patterns
270 'm' => {
271 let count = Self::count_consecutive(&mut chars, 'm') + 1;
272 match count {
273 1 => tokens.push(FormatToken::Minute(FormatStyle::Numeric)),
274 2 => tokens.push(FormatToken::Minute(FormatStyle::Custom { width: 2, pad_char: '0' })),
275 _ => tokens.push(FormatToken::Minute(FormatStyle::Custom { width: 2, pad_char: '0' })),
276 }
277 },
278
279 // Second patterns
280 's' => {
281 let count = Self::count_consecutive(&mut chars, 's') + 1;
282 match count {
283 1 => tokens.push(FormatToken::Second(FormatStyle::Numeric)),
284 2 => tokens.push(FormatToken::Second(FormatStyle::Custom { width: 2, pad_char: '0' })),
285 _ => tokens.push(FormatToken::Second(FormatStyle::Custom { width: 2, pad_char: '0' })),
286 }
287 },
288
289 // Subsecond patterns
290 'S' => {
291 let count = Self::count_consecutive(&mut chars, 'S') + 1;
292 match count {
293 1..=3 => tokens.push(FormatToken::Millisecond(FormatStyle::Custom { width: count, pad_char: '0' })),
294 4..=6 => tokens.push(FormatToken::Microsecond(FormatStyle::Custom { width: count, pad_char: '0' })),
295 7..=9 => tokens.push(FormatToken::Nanosecond(FormatStyle::Custom { width: count, pad_char: '0' })),
296 _ => tokens.push(FormatToken::Nanosecond(FormatStyle::Custom { width: 9, pad_char: '0' })),
297 }
298 },
299
300 // AM/PM patterns
301 'a' => {
302 let _count = Self::count_consecutive(&mut chars, 'a');
303 tokens.push(FormatToken::AmPm(FormatStyle::Short));
304 },
305
306 // Timezone patterns
307 'z' => {
308 let count = Self::count_consecutive(&mut chars, 'z') + 1;
309 match count {
310 1..=3 => tokens.push(FormatToken::TimezoneName(FormatStyle::Short)),
311 4 => tokens.push(FormatToken::TimezoneName(FormatStyle::Long)),
312 _ => tokens.push(FormatToken::TimezoneName(FormatStyle::Long)),
313 }
314 },
315
316 'Z' => {
317 let _count = Self::count_consecutive(&mut chars, 'Z');
318 tokens.push(FormatToken::TimezoneOffset(FormatStyle::Short));
319 },
320
321 'v' => {
322 let _count = Self::count_consecutive(&mut chars, 'v');
323 tokens.push(FormatToken::TimezoneId(FormatStyle::Short));
324 },
325
326 // Common separators
327 ' ' => tokens.push(FormatToken::Space),
328 ':' => tokens.push(FormatToken::Colon),
329 '-' => tokens.push(FormatToken::Dash),
330 '/' => tokens.push(FormatToken::Slash),
331 ',' => tokens.push(FormatToken::Comma),
332 '.' => tokens.push(FormatToken::Period),
333
334 // Everything else is literal
335 _ => {
336 // Collect consecutive literal characters
337 let mut literal = String::new();
338 literal.push(ch);
339
340 while let Some(&next_ch) = chars.peek() {
341 if Self::is_pattern_char(next_ch) || next_ch == '\'' {
342 break;
343 }
344 literal.push(chars.next().unwrap());
345 }
346
347 tokens.push(FormatToken::Literal(literal));
348 },
349 }
350 }
351
352 Ok(tokens)
353 }
354
355 fn count_consecutive(chars: &mut std::iter::Peekable<std::str::Chars>, target: char) -> usize {
356 let mut count = 0;
357 while chars.peek() == Some(&target) {
358 chars.next();
359 count += 1;
360 }
361 count
362 }
363
364 fn is_pattern_char(ch: char) -> bool {
365 matches!(ch, 'y' | 'M' | 'd' | 'E' | 'D' | 'Q' | 'w' | 'G' | 'H' | 'h' | 'm' | 's' | 'S' | 'a' | 'z' | 'Z' | 'v' | ' ' | ':' | '-' | '/' | ',' | '.')
366 }
367
368 // ========================================================================
369 // Predefined Common Patterns
370 // ========================================================================
371
372 pub fn iso_date() -> Self {
373 Self::new("yyyy-MM-dd").unwrap() // 2024-01-15
374 }
375
376 pub fn iso_time() -> Self {
377 Self::new("HH:mm:ss").unwrap() // 14:30:45
378 }
379
380 pub fn iso_datetime() -> Self {
381 Self::new("yyyy-MM-dd'T'HH:mm:ss").unwrap() // 2024-01-15T14:30:45
382 }
383
384 pub fn iso_datetime_utc() -> Self {
385 Self::new("yyyy-MM-dd'T'HH:mm:ss'Z'").unwrap() // 2024-01-15T14:30:45Z
386 }
387
388 pub fn us_date() -> Self {
389 Self::new("MM/dd/yyyy").unwrap() // 01/15/2024
390 }
391
392 pub fn european_date() -> Self {
393 Self::new("dd/MM/yyyy").unwrap() // 15/01/2024
394 }
395
396 pub fn long_date() -> Self {
397 Self::new("MMMM d, yyyy").unwrap() // January 15, 2024
398 }
399
400 pub fn full_date() -> Self {
401 Self::new("EEEE, MMMM d, yyyy").unwrap() // Monday, January 15, 2024
402 }
403
404 pub fn time_12h() -> Self {
405 Self::new("h:mm:ss a").unwrap() // 2:30:45 PM
406 }
407
408 pub fn time_24h() -> Self {
409 Self::new("HH:mm:ss").unwrap() // 14:30:45
410 }
411
412 pub fn time_short() -> Self {
413 Self::new("h:mm a").unwrap() // 2:30 PM
414 }
415
416 pub fn rfc2822() -> Self {
417 Self::new("EEE, d MMM yyyy HH:mm:ss Z").unwrap() // Mon, 15 Jan 2024 14:30:45 +0000
418 }
419
420 pub fn common_log() -> Self {
421 Self::new("dd/MMM/yyyy:HH:mm:ss Z").unwrap() // 15/Jan/2024:14:30:45 +0000
422 }
423}