Oregami
Repositories/oxedyne/fe2o3

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

23.2 KiB, 85 runs

created by r1870400018:6363, 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//! Formatting of CalClock, CalendarDate and ClockTime values.
2//!
3//! A formatter walks a parsed pattern, and may carry a locale together with
4//! overrides for month names, day names and the AM/PM markers.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use crate::{
10 cache::string_intern::convenience as intern,
11 calendar::CalendarDate,
12 clock::ClockTime,
13 constant::{DayOfWeek, MonthOfYear},
14 format::{FormatPattern, FormatToken, FormatStyle, Locale},
15 time::CalClock,
16};
17
18use oxedyne_fe2o3_core::prelude::*;
19
20use std::{fmt, collections::HashMap};
21
22#[derive(Clone, Debug)]
23pub enum FormattingError {
24 InvalidPattern(String),
25 UnsupportedToken(String),
26 InvalidValue(String),
27}
28
29impl fmt::Display for FormattingError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 FormattingError::InvalidPattern(msg) => write!(f, "Invalid pattern: {}", msg),
33 FormattingError::UnsupportedToken(msg) => write!(f, "Unsupported token: {}", msg),
34 FormattingError::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
35 }
36 }
37}
38
39/// # Examples
40///
41/// ```ignore
42/// use oxedyne_fe2o3_datime::{
43/// format::{CalClockFormatter, FormatPattern},
44/// time::CalClock,
45/// }res!();
46///
47/// let calclock = CalClock::now_utc()?res!();
48/// let formatter = CalClockFormatter::new()res!();
49///
50/// // Using predefined patterns
51/// let iso_string = formatter.format_with_pattern(&calclock, &FormatPattern::iso_datetime())?res!();
52/// let us_string = formatter.format_with_pattern(&calclock, &FormatPattern::us_date())?res!();
53///
54/// // Using custom patterns
55/// let custom_pattern = FormatPattern::new("EEEE, MMMM d, yyyy 'at' h:mm a")?res!();
56/// let custom_string = formatter.format_with_pattern(&calclock, &custom_pattern)?res!();
57/// ```
58#[derive(Debug, Clone)]
59pub struct CalClockFormatter {
60 #[allow(dead_code)]
61 locale: Option<Locale>, // sets names and date order
62 month_names: Option<HashMap<MonthOfYear, (String, String)>>, // (short, long)
63 day_names: Option<HashMap<DayOfWeek, (String, String)>>, // (short, long)
64 am_pm_markers: Option<(String, String)>, // (AM, PM)
65 use_ordinals: bool, // 1st, 2nd, 3rd
66}
67
68impl CalClockFormatter {
69 pub fn new() -> Self {
70 Self {
71 locale: None,
72 month_names: None,
73 day_names: None,
74 am_pm_markers: None,
75 use_ordinals: false,
76 }
77 }
78
79 pub fn with_locale(locale: Locale) -> Self {
80 Self {
81 locale: Some(locale),
82 month_names: None,
83 day_names: None,
84 am_pm_markers: None,
85 use_ordinals: false,
86 }
87 }
88
89 pub fn set_month_names(mut self, names: HashMap<MonthOfYear, (String, String)>) -> Self {
90 self.month_names = Some(names);
91 self
92 }
93
94 pub fn set_day_names(mut self, names: HashMap<DayOfWeek, (String, String)>) -> Self {
95 self.day_names = Some(names);
96 self
97 }
98
99 pub fn set_am_pm_markers(mut self, am: String, pm: String) -> Self {
100 self.am_pm_markers = Some((am, pm));
101 self
102 }
103
104 pub fn enable_ordinals(mut self) -> Self {
105 self.use_ordinals = true;
106 self
107 }
108
109 pub fn format_with_pattern(&self, calclock: &CalClock, pattern: &FormatPattern) -> Outcome<String> {
110 let mut result = String::new();
111
112 for token in pattern.tokens() {
113 let formatted = res!(self.format_token(token, calclock));
114 result.push_str(&formatted);
115 }
116
117 Ok(result)
118 }
119
120 pub fn format_date_with_pattern(&self, date: &CalendarDate, pattern: &FormatPattern) -> Outcome<String> {
121 let mut result = String::new();
122
123 for token in pattern.tokens() {
124 let formatted = res!(self.format_date_token(token, date));
125 result.push_str(&formatted);
126 }
127
128 Ok(result)
129 }
130
131 pub fn format_time_with_pattern(&self, time: &ClockTime, pattern: &FormatPattern) -> Outcome<String> {
132 let mut result = String::new();
133
134 for token in pattern.tokens() {
135 let formatted = res!(self.format_time_token(token, time));
136 result.push_str(&formatted);
137 }
138
139 Ok(result)
140 }
141
142 pub fn format(&self, calclock: &CalClock, pattern_string: &str) -> Outcome<String> {
143 let pattern = res!(FormatPattern::new(pattern_string));
144 self.format_with_pattern(calclock, &pattern)
145 }
146
147 pub fn format_with_locale(&self, calclock: &CalClock, locale: &Locale) -> Outcome<String> {
148 self.format_with_pattern(calclock, locale.datetime_pattern())
149 }
150
151 pub fn format_date_with_locale(&self, calclock: &CalClock, locale: &Locale) -> Outcome<String> {
152 self.format_with_pattern(calclock, locale.date_pattern())
153 }
154
155 pub fn format_time_with_locale(&self, calclock: &CalClock, locale: &Locale) -> Outcome<String> {
156 self.format_with_pattern(calclock, locale.time_pattern())
157 }
158
159 pub fn format_date(&self, date: &CalendarDate, pattern_string: &str) -> Outcome<String> {
160 let pattern = res!(FormatPattern::new(pattern_string));
161 self.format_date_with_pattern(date, &pattern)
162 }
163
164 pub fn format_time(&self, time: &ClockTime, pattern_string: &str) -> Outcome<String> {
165 let pattern = res!(FormatPattern::new(pattern_string));
166 self.format_time_with_pattern(time, &pattern)
167 }
168
169 // ========================================================================
170 // Token Formatting Implementation
171 // ========================================================================
172
173 fn format_token(&self, token: &FormatToken, calclock: &CalClock) -> Outcome<String> {
174 match token {
175 // Date tokens
176 FormatToken::Year(style) => self.format_year(calclock.year(), style),
177 FormatToken::Month(style) => self.format_month(calclock.month_of_year(), style),
178 FormatToken::Day(style) => self.format_day(calclock.day(), style),
179 FormatToken::DayOfWeek(style) => self.format_day_of_week(calclock.day_of_week(), style),
180 FormatToken::DayOfYear(style) => {
181 let day_of_year = res!(calclock.day_of_year());
182 self.format_day_of_year(day_of_year, style)
183 },
184 FormatToken::Quarter(style) => self.format_quarter(calclock.date().quarter(), style),
185 FormatToken::WeekOfYear(style) => {
186 let week_of_year = res!(self.calculate_week_of_year(calclock.date()));
187 self.format_week_of_year(week_of_year, style)
188 },
189 FormatToken::Era(style) => self.format_era(calclock.date(), style),
190
191 // Time tokens
192 FormatToken::Hour12(style) => {
193 let hour_12 = if calclock.hour() == 0 { 12 } else if calclock.hour() > 12 { calclock.hour() - 12 } else { calclock.hour() };
194 self.format_hour(hour_12, style)
195 },
196 FormatToken::Hour24(style) => self.format_hour(calclock.hour(), style),
197 FormatToken::Minute(style) => self.format_minute(calclock.minute(), style),
198 FormatToken::Second(style) => self.format_second(calclock.second(), style),
199 FormatToken::Millisecond(style) => self.format_millisecond(calclock.millisecond(), style),
200 FormatToken::Microsecond(style) => self.format_microsecond(calclock.microsecond(), style),
201 FormatToken::Nanosecond(style) => self.format_nanosecond(calclock.nanosecond(), style),
202 FormatToken::AmPm(style) => self.format_am_pm(calclock.hour() >= 12, style),
203
204 // Timezone tokens
205 FormatToken::TimezoneId(style) => self.format_timezone_id(calclock.zone().id(), style),
206 FormatToken::TimezoneOffset(style) => {
207 // Get the offset for the current time
208 let millis = res!(calclock.to_millis());
209 let offset_millis = res!(calclock.zone().offset_millis_at_time(millis));
210 self.format_timezone_offset(offset_millis, style)
211 },
212 FormatToken::TimezoneName(style) => self.format_timezone_name(calclock.zone().id(), style),
213
214 // Special characters
215 FormatToken::Space => Ok(" ".to_string()),
216 FormatToken::Colon => Ok(":".to_string()),
217 FormatToken::Dash => Ok("-".to_string()),
218 FormatToken::Slash => Ok("/".to_string()),
219 FormatToken::Comma => Ok(",".to_string()),
220 FormatToken::Period => Ok(".".to_string()),
221
222 // Literal text
223 FormatToken::Literal(text) => Ok(text.clone()),
224 }
225 }
226
227 fn format_date_token(&self, token: &FormatToken, date: &CalendarDate) -> Outcome<String> {
228 match token {
229 FormatToken::Year(style) => self.format_year(date.year(), style),
230 FormatToken::Month(style) => self.format_month(date.month_of_year(), style),
231 FormatToken::Day(style) => self.format_day(date.day(), style),
232 FormatToken::DayOfWeek(style) => self.format_day_of_week(date.day_of_week(), style),
233 FormatToken::DayOfYear(style) => {
234 let day_of_year = res!(date.day_of_year());
235 self.format_day_of_year(day_of_year, style)
236 },
237 FormatToken::Quarter(style) => self.format_quarter(date.quarter(), style),
238 FormatToken::WeekOfYear(style) => {
239 let week_of_year = res!(self.calculate_week_of_year(date));
240 self.format_week_of_year(week_of_year, style)
241 },
242 FormatToken::Era(style) => self.format_era(date, style),
243
244 // Special characters
245 FormatToken::Space => Ok(" ".to_string()),
246 FormatToken::Colon => Ok(":".to_string()),
247 FormatToken::Dash => Ok("-".to_string()),
248 FormatToken::Slash => Ok("/".to_string()),
249 FormatToken::Comma => Ok(",".to_string()),
250 FormatToken::Period => Ok(".".to_string()),
251
252 // Literal text
253 FormatToken::Literal(text) => Ok(text.clone()),
254
255 // Invalid for date-only formatting
256 _ => Err(err!("Invalid token for CalendarDate: {:?}", token; Invalid, Input)),
257 }
258 }
259
260 fn format_time_token(&self, token: &FormatToken, time: &ClockTime) -> Outcome<String> {
261 match token {
262 FormatToken::Hour12(style) => {
263 let hour_12 = if time.hour().of() == 0 { 12 } else if time.hour().of() > 12 { time.hour().of() - 12 } else { time.hour().of() };
264 self.format_hour(hour_12, style)
265 },
266 FormatToken::Hour24(style) => self.format_hour(time.hour().of(), style),
267 FormatToken::Minute(style) => self.format_minute(time.minute().of(), style),
268 FormatToken::Second(style) => self.format_second(time.second().of(), style),
269 FormatToken::Millisecond(style) => {
270 let millis = (time.nanosecond().of() / 1_000_000) as u16;
271 self.format_millisecond(millis, style)
272 },
273 FormatToken::Microsecond(style) => {
274 let micros = ((time.nanosecond().of() % 1_000_000) / 1_000) as u16;
275 self.format_microsecond(micros, style)
276 },
277 FormatToken::Nanosecond(style) => self.format_nanosecond(time.nanosecond().of(), style),
278 FormatToken::AmPm(style) => self.format_am_pm(time.hour().of() >= 12, style),
279
280 // Special characters
281 FormatToken::Space => Ok(" ".to_string()),
282 FormatToken::Colon => Ok(":".to_string()),
283 FormatToken::Dash => Ok("-".to_string()),
284 FormatToken::Slash => Ok("/".to_string()),
285 FormatToken::Comma => Ok(",".to_string()),
286 FormatToken::Period => Ok(".".to_string()),
287
288 // Literal text
289 FormatToken::Literal(text) => Ok(text.clone()),
290
291 // Invalid for time-only formatting
292 _ => Err(err!("Invalid token for ClockTime: {:?}", token; Invalid, Input)),
293 }
294 }
295
296 // ========================================================================
297 // Individual Component Formatters
298 // ========================================================================
299
300 fn format_year(&self, year: i32, style: &FormatStyle) -> Outcome<String> {
301 match style {
302 FormatStyle::Numeric => Ok(year.to_string()),
303 FormatStyle::Custom { width, pad_char: _ } => {
304 match width {
305 2 => Ok(format!("{:02}", year % 100)),
306 4 => Ok(format!("{:04}", year)),
307 _ => Ok(format!("{:0width$}", year, width = width)),
308 }
309 },
310 _ => Ok(year.to_string()),
311 }
312 }
313
314 fn format_month(&self, month: MonthOfYear, style: &FormatStyle) -> Outcome<String> {
315 match style {
316 FormatStyle::Numeric => Ok(month.of().to_string()),
317 FormatStyle::Short => {
318 if let Some(ref names) = self.month_names {
319 if let Some((short, _)) = names.get(&month) {
320 return Ok(short.clone());
321 }
322 }
323 Ok((*intern::intern_month(month.short_name())).clone())
324 },
325 FormatStyle::Long => {
326 if let Some(ref names) = self.month_names {
327 if let Some((_, long)) = names.get(&month) {
328 return Ok(long.clone());
329 }
330 }
331 Ok((*intern::intern_month(month.long_name())).clone())
332 },
333 FormatStyle::Custom { width, pad_char: _ } => {
334 Ok(format!("{:0width$}", month.of(), width = width))
335 },
336 _ => Ok((*intern::intern_month(month.short_name())).clone()),
337 }
338 }
339
340 fn format_day(&self, day: u8, style: &FormatStyle) -> Outcome<String> {
341 match style {
342 FormatStyle::Numeric => {
343 if self.use_ordinals {
344 let ordinal = match day {
345 1 | 21 | 31 => format!("{}st", day),
346 2 | 22 => format!("{}nd", day),
347 3 | 23 => format!("{}rd", day),
348 _ => format!("{}th", day),
349 };
350 Ok(ordinal)
351 } else {
352 Ok(day.to_string())
353 }
354 },
355 FormatStyle::Custom { width, pad_char: _ } => {
356 Ok(format!("{:0width$}", day, width = width))
357 },
358 _ => {
359 if self.use_ordinals {
360 let ordinal = match day {
361 1 | 21 | 31 => format!("{}st", day),
362 2 | 22 => format!("{}nd", day),
363 3 | 23 => format!("{}rd", day),
364 _ => format!("{}th", day),
365 };
366 Ok(ordinal)
367 } else {
368 Ok(day.to_string())
369 }
370 },
371 }
372 }
373
374 fn format_day_of_week(&self, day_of_week: DayOfWeek, style: &FormatStyle) -> Outcome<String> {
375 match style {
376 FormatStyle::Short => {
377 if let Some(ref names) = self.day_names {
378 if let Some((short, _)) = names.get(&day_of_week) {
379 return Ok(short.clone());
380 }
381 }
382 Ok((*intern::intern_day(day_of_week.short_name())).clone())
383 },
384 FormatStyle::Long => {
385 if let Some(ref names) = self.day_names {
386 if let Some((_, long)) = names.get(&day_of_week) {
387 return Ok(long.clone());
388 }
389 }
390 Ok((*intern::intern_day(day_of_week.long_name())).clone())
391 },
392 _ => Ok((*intern::intern_day(day_of_week.short_name())).clone()),
393 }
394 }
395
396 fn format_day_of_year(&self, day_of_year: u16, style: &FormatStyle) -> Outcome<String> {
397 match style {
398 FormatStyle::Numeric => Ok(day_of_year.to_string()),
399 FormatStyle::Custom { width, pad_char: _ } => {
400 Ok(format!("{:0width$}", day_of_year, width = width))
401 },
402 _ => Ok(day_of_year.to_string()),
403 }
404 }
405
406 fn format_quarter(&self, quarter: u8, style: &FormatStyle) -> Outcome<String> {
407 match style {
408 _ => Ok(quarter.to_string()),
409 }
410 }
411
412 fn format_hour(&self, hour: u8, style: &FormatStyle) -> Outcome<String> {
413 match style {
414 FormatStyle::Numeric => Ok(hour.to_string()),
415 FormatStyle::Custom { width, pad_char: _ } => {
416 Ok(format!("{:0width$}", hour, width = width))
417 },
418 _ => Ok(hour.to_string()),
419 }
420 }
421
422 fn format_minute(&self, minute: u8, style: &FormatStyle) -> Outcome<String> {
423 match style {
424 FormatStyle::Numeric => Ok(minute.to_string()),
425 FormatStyle::Custom { width, pad_char: _ } => {
426 Ok(format!("{:0width$}", minute, width = width))
427 },
428 _ => Ok(minute.to_string()),
429 }
430 }
431
432 fn format_second(&self, second: u8, style: &FormatStyle) -> Outcome<String> {
433 match style {
434 FormatStyle::Numeric => Ok(second.to_string()),
435 FormatStyle::Custom { width, pad_char: _ } => {
436 Ok(format!("{:0width$}", second, width = width))
437 },
438 _ => Ok(second.to_string()),
439 }
440 }
441
442 fn format_millisecond(&self, millisecond: u16, style: &FormatStyle) -> Outcome<String> {
443 match style {
444 FormatStyle::Custom { width, pad_char: _ } => {
445 Ok(format!("{:0width$}", millisecond, width = width))
446 },
447 _ => Ok(format!("{:03}", millisecond)),
448 }
449 }
450
451 fn format_microsecond(&self, microsecond: u16, style: &FormatStyle) -> Outcome<String> {
452 match style {
453 FormatStyle::Custom { width, pad_char: _ } => {
454 Ok(format!("{:0width$}", microsecond, width = width))
455 },
456 _ => Ok(format!("{:03}", microsecond)),
457 }
458 }
459
460 fn format_nanosecond(&self, nanosecond: u32, style: &FormatStyle) -> Outcome<String> {
461 match style {
462 FormatStyle::Custom { width, pad_char: _ } => {
463 Ok(format!("{:0width$}", nanosecond, width = width))
464 },
465 _ => Ok(format!("{:09}", nanosecond)),
466 }
467 }
468
469 fn format_am_pm(&self, is_pm: bool, _style: &FormatStyle) -> Outcome<String> {
470 if let Some((ref am, ref pm)) = self.am_pm_markers {
471 Ok(if is_pm { pm.clone() } else { am.clone() })
472 } else {
473 let marker = if is_pm { "PM" } else { "AM" };
474 Ok((*intern::intern(marker)).clone())
475 }
476 }
477
478 fn format_timezone_id(&self, timezone_id: &str, _style: &FormatStyle) -> Outcome<String> {
479 Ok(timezone_id.to_string())
480 }
481
482 fn format_timezone_offset(&self, offset_millis: i32, _style: &FormatStyle) -> Outcome<String> {
483 let offset_seconds = offset_millis / 1000;
484 let offset_hours = offset_seconds / 3600;
485 let offset_mins = (offset_seconds % 3600) / 60;
486
487 let sign = if offset_seconds >= 0 { "+" } else { "-" };
488 let abs_hours = offset_hours.abs();
489 let abs_mins = offset_mins.abs();
490
491 Ok(format!("{}{:02}{:02}", sign, abs_hours, abs_mins))
492 }
493
494 fn format_timezone_name(&self, timezone_id: &str, _style: &FormatStyle) -> Outcome<String> {
495 // Map common timezone IDs to abbreviations
496 let abbrev = match timezone_id {
497 "America/New_York" => "EST",
498 "America/Chicago" => "CST",
499 "America/Denver" => "MST",
500 "America/Los_Angeles" => "PST",
501 "Europe/London" => "GMT",
502 "Europe/Paris" => "CET",
503 "Asia/Tokyo" => "JST",
504 "Australia/Sydney" => "AEDT",
505 _ => timezone_id,
506 };
507 Ok((*intern::intern_timezone(abbrev)).clone())
508 }
509
510 fn format_week_of_year(&self, week: u8, style: &FormatStyle) -> Outcome<String> {
511 match style {
512 FormatStyle::Numeric => Ok(week.to_string()),
513 FormatStyle::Custom { width, pad_char: _ } => {
514 Ok(format!("{:0width$}", week, width = width))
515 },
516 _ => Ok(week.to_string()),
517 }
518 }
519
520 fn format_era(&self, date: &CalendarDate, style: &FormatStyle) -> Outcome<String> {
521 // Use calendar-specific era handling
522 // Since CalendarSystem only has Gregorian and Julian currently,
523 // we'll determine the calendar type from the internal Calendar enum if available
524 // For now, default to CE/BCE for all calendar systems
525 let era_text = match style {
526 FormatStyle::Short => {
527 if date.year() > 0 { "CE" } else { "BCE" }
528 },
529 FormatStyle::Long => {
530 if date.year() > 0 { "Common Era" } else { "Before Common Era" }
531 },
532 _ => {
533 if date.year() > 0 { "CE" } else { "BCE" }
534 },
535 };
536
537 Ok((*intern::intern(era_text)).clone())
538 }
539
540 fn calculate_week_of_year(&self, date: &CalendarDate) -> Outcome<u8> {
541 // ISO 8601 week calculation
542 // Week 1 is the first week with Thursday in the new year
543 let jan_1 = res!(CalendarDate::from_ymd(date.year(), MonthOfYear::January, 1, date.zone().clone()));
544 let jan_1_dow = jan_1.day_of_week();
545
546 // Find the Monday of week 1
547 let days_from_monday = match jan_1_dow {
548 DayOfWeek::Monday => 0,
549 DayOfWeek::Tuesday => 1,
550 DayOfWeek::Wednesday => 2,
551 DayOfWeek::Thursday => 3,
552 DayOfWeek::Friday => 4,
553 DayOfWeek::Saturday => 5,
554 DayOfWeek::Sunday => 6,
555 };
556
557 let week_1_monday = if days_from_monday <= 3 {
558 // January 1 is in week 1
559 res!(jan_1.add_days(-(days_from_monday as i32)))
560 } else {
561 // January 1 is in the last week of the previous year
562 res!(jan_1.add_days((7 - days_from_monday) as i32))
563 };
564
565 // Calculate the number of days from week 1 Monday to the date
566 let date_day_number = res!(date.to_day_number());
567 let week_1_day_number = res!(week_1_monday.to_day_number());
568 let days_diff = date_day_number - week_1_day_number;
569
570 if days_diff < 0 {
571 // Date is in the last week of the previous year
572 // Calculate the week number for the previous year
573 let prev_year_date = res!(CalendarDate::from_ymd(date.year() - 1, MonthOfYear::December, 31, date.zone().clone()));
574 self.calculate_week_of_year(&prev_year_date)
575 } else {
576 let week_number = (days_diff / 7) + 1;
577 Ok(week_number as u8)
578 }
579 }
580}
581
582impl Default for CalClockFormatter {
583 fn default() -> Self {
584 Self::new()
585 }
586}