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 | |
| 9 | use 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 | |
| 18 | use oxedyne_fe2o3_core::prelude::*; |
| 19 | |
| 20 | use std::{fmt, collections::HashMap}; |
| 21 | |
| 22 | #[derive(Clone, Debug)] |
| 23 | pub enum FormattingError { |
| 24 | InvalidPattern(String), |
| 25 | UnsupportedToken(String), |
| 26 | InvalidValue(String), |
| 27 | } |
| 28 | |
| 29 | impl 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)] |
| 59 | pub 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 | |
| 68 | impl 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 | |
| 582 | impl Default for CalClockFormatter { |
| 583 | fn default() -> Self { |
| 584 | Self::new() |
| 585 | } |
| 586 | } |