oxedyne/fe2o3/fe2o3_datime/src/clock/time.rs
17.8 KiB, 78 runs
created by r1870400018:6335, 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 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 2 | //! Anthropic Claude |
| 3 | |
| 4 | use crate::{ |
| 5 | core::Time, |
| 6 | clock::{ |
| 7 | ClockHour, |
| 8 | ClockMinute, |
| 9 | ClockSecond, |
| 10 | ClockNanoSecond, |
| 11 | ClockFields, |
| 12 | ClockDuration, |
| 13 | }, |
| 14 | time::{CalClockZone, LeapSecondConfig}, |
| 15 | }; |
| 16 | |
| 17 | use oxedyne_fe2o3_core::prelude::*; |
| 18 | |
| 19 | use std::fmt; |
| 20 | |
| 21 | /// 00:00:00.000000000 through 24:00:00.000000000 inclusive, the last being the |
| 22 | /// end of the day rather than a time within it. The zone is carried but not |
| 23 | /// applied: with no date there is nothing for its rules to act on. |
| 24 | /// |
| 25 | /// ```ignore |
| 26 | /// use oxedyne_fe2o3_datime::{ |
| 27 | /// clock::ClockTime, |
| 28 | /// time::CalClockZone, |
| 29 | /// }res!(); |
| 30 | /// |
| 31 | /// let zone = CalClockZone::utc()res!(); |
| 32 | /// let time = ClockTime::new(14, 30, 45, 123_456_789, zone)?res!(); |
| 33 | /// assert_eq!(time.hour().of(), 14)res!(); |
| 34 | /// assert_eq!(time.to_twelve_hour_string(), "2:30:45 PM")res!(); |
| 35 | /// ``` |
| 36 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] |
| 37 | pub struct ClockTime { |
| 38 | hour: ClockHour, |
| 39 | minute: ClockMinute, |
| 40 | second: ClockSecond, |
| 41 | nanosecond: ClockNanoSecond, |
| 42 | zone: CalClockZone, |
| 43 | } |
| 44 | |
| 45 | impl ClockTime { |
| 46 | pub fn new( |
| 47 | hour: u8, |
| 48 | minute: u8, |
| 49 | second: u8, |
| 50 | nanosecond: u32, |
| 51 | zone: CalClockZone, |
| 52 | ) -> Outcome<Self> { |
| 53 | // ClockTime uses stricter validation than individual components |
| 54 | // Only hours 0-23 are valid for ClockTime (not 24) |
| 55 | if hour >= 24 { |
| 56 | return Err(err!( |
| 57 | "Hour {} is invalid for ClockTime, must be 0-23", |
| 58 | hour; |
| 59 | Invalid, Input |
| 60 | )); |
| 61 | } |
| 62 | if minute >= 60 { |
| 63 | return Err(err!( |
| 64 | "Minute {} is invalid, must be 0-59", |
| 65 | minute; |
| 66 | Invalid, Input |
| 67 | )); |
| 68 | } |
| 69 | if second >= 60 { |
| 70 | return Err(err!( |
| 71 | "Second {} is invalid, must be 0-59", |
| 72 | second; |
| 73 | Invalid, Input |
| 74 | )); |
| 75 | } |
| 76 | |
| 77 | Ok(Self { |
| 78 | hour: res!(ClockHour::new(hour)), |
| 79 | minute: res!(ClockMinute::new(minute)), |
| 80 | second: res!(ClockSecond::new(second)), |
| 81 | nanosecond: res!(ClockNanoSecond::new(nanosecond)), |
| 82 | zone, |
| 83 | }) |
| 84 | } |
| 85 | |
| 86 | /// Second 60 is accepted only where the configuration allows it and the leap |
| 87 | /// second table agrees. |
| 88 | pub fn new_with_leap_seconds( |
| 89 | hour: u8, |
| 90 | minute: u8, |
| 91 | second: u8, |
| 92 | nanosecond: u32, |
| 93 | zone: CalClockZone, |
| 94 | config: &LeapSecondConfig, |
| 95 | ) -> Outcome<Self> { |
| 96 | // Standard validation for hour, minute |
| 97 | if hour >= 24 { |
| 98 | return Err(err!( |
| 99 | "Hour {} is invalid for ClockTime, must be 0-23", |
| 100 | hour; |
| 101 | Invalid, Input |
| 102 | )); |
| 103 | } |
| 104 | if minute >= 60 { |
| 105 | return Err(err!( |
| 106 | "Minute {} is invalid, must be 0-59", |
| 107 | minute; |
| 108 | Invalid, Input |
| 109 | )); |
| 110 | } |
| 111 | |
| 112 | // Leap second validation |
| 113 | if second == 60 { |
| 114 | // Second 60 is only allowed if leap seconds are enabled and allowed in parsing |
| 115 | if !config.enabled || !config.allow_leap_second_parsing { |
| 116 | return Err(err!( |
| 117 | "Leap second (second=60) not allowed: leap seconds disabled or parsing not allowed"; |
| 118 | Invalid, Input |
| 119 | )); |
| 120 | } |
| 121 | |
| 122 | // If validation is enabled, check that this is actually a valid leap second |
| 123 | if config.validate_leap_seconds { |
| 124 | let table = config.get_table(); |
| 125 | // For time-only validation, we can only check if leap seconds are generally possible |
| 126 | // Full validation would require a complete date context |
| 127 | if !table.is_enabled() { |
| 128 | return Err(err!( |
| 129 | "Leap second validation failed: leap second table is disabled"; |
| 130 | Invalid, Input |
| 131 | )); |
| 132 | } |
| 133 | |
| 134 | // Basic validation: leap seconds only occur at 23:59:60 UTC |
| 135 | if hour != 23 || minute != 59 { |
| 136 | return Err(err!( |
| 137 | "Leap second (second=60) only valid at 23:59:60 UTC, got {}:{}:60", |
| 138 | hour, minute; |
| 139 | Invalid, Input |
| 140 | )); |
| 141 | } |
| 142 | } |
| 143 | } else if second > 60 { |
| 144 | return Err(err!( |
| 145 | "Second {} is invalid, must be 0-60", |
| 146 | second; |
| 147 | Invalid, Input |
| 148 | )); |
| 149 | } |
| 150 | |
| 151 | Ok(Self { |
| 152 | hour: res!(ClockHour::new(hour)), |
| 153 | minute: res!(ClockMinute::new(minute)), |
| 154 | second: res!(ClockSecond::new(second)), |
| 155 | nanosecond: res!(ClockNanoSecond::new(nanosecond)), |
| 156 | zone, |
| 157 | }) |
| 158 | } |
| 159 | |
| 160 | pub fn from_components( |
| 161 | hour: ClockHour, |
| 162 | minute: ClockMinute, |
| 163 | second: ClockSecond, |
| 164 | nanosecond: ClockNanoSecond, |
| 165 | zone: CalClockZone, |
| 166 | ) -> Self { |
| 167 | Self { hour, minute, second, nanosecond, zone } |
| 168 | } |
| 169 | |
| 170 | pub fn midnight(zone: CalClockZone) -> Outcome<Self> { |
| 171 | Self::new(0, 0, 0, 0, zone) |
| 172 | } |
| 173 | |
| 174 | pub fn noon(zone: CalClockZone) -> Outcome<Self> { |
| 175 | Self::new(12, 0, 0, 0, zone) |
| 176 | } |
| 177 | |
| 178 | pub fn end_of_day(zone: CalClockZone) -> Outcome<Self> { |
| 179 | // Special case: create end-of-day time with hour 24 |
| 180 | // This bypasses normal validation since 24:00:00 is conceptually valid as end-of-day |
| 181 | Ok(Self { |
| 182 | hour: res!(ClockHour::new(24)), // ClockHour allows 24 |
| 183 | minute: res!(ClockMinute::new(0)), |
| 184 | second: res!(ClockSecond::new(0)), |
| 185 | nanosecond: res!(ClockNanoSecond::new(0)), |
| 186 | zone, |
| 187 | }) |
| 188 | } |
| 189 | |
| 190 | pub fn from_nanos_of_day(nanos: u64, zone: CalClockZone) -> Outcome<Self> { |
| 191 | const NANOS_PER_DAY: u64 = 24 * 60 * 60 * 1_000_000_000; |
| 192 | |
| 193 | if nanos >= NANOS_PER_DAY { |
| 194 | return Err(err!( |
| 195 | "Nanoseconds {} exceed maximum for one day {}", |
| 196 | nanos, |
| 197 | NANOS_PER_DAY; |
| 198 | Invalid, Input |
| 199 | )); |
| 200 | } |
| 201 | |
| 202 | let hours = nanos / (60 * 60 * 1_000_000_000); |
| 203 | let remaining = nanos % (60 * 60 * 1_000_000_000); |
| 204 | |
| 205 | let minutes = remaining / (60 * 1_000_000_000); |
| 206 | let remaining = remaining % (60 * 1_000_000_000); |
| 207 | |
| 208 | let seconds = remaining / 1_000_000_000; |
| 209 | let nanoseconds = remaining % 1_000_000_000; |
| 210 | |
| 211 | Self::new( |
| 212 | hours as u8, |
| 213 | minutes as u8, |
| 214 | seconds as u8, |
| 215 | nanoseconds as u32, |
| 216 | zone, |
| 217 | ) |
| 218 | } |
| 219 | |
| 220 | pub fn hour(&self) -> ClockHour { |
| 221 | self.hour |
| 222 | } |
| 223 | |
| 224 | pub fn minute(&self) -> ClockMinute { |
| 225 | self.minute |
| 226 | } |
| 227 | |
| 228 | pub fn second(&self) -> ClockSecond { |
| 229 | self.second |
| 230 | } |
| 231 | |
| 232 | pub fn nanosecond(&self) -> ClockNanoSecond { |
| 233 | self.nanosecond |
| 234 | } |
| 235 | |
| 236 | pub fn zone(&self) -> &CalClockZone { |
| 237 | &self.zone |
| 238 | } |
| 239 | |
| 240 | pub fn to_nanos_of_day(&self) -> u64 { |
| 241 | let hour_nanos = self.hour.of() as u64 * ClockHour::NANOS_PER_HOUR; |
| 242 | let minute_nanos = self.minute.of() as u64 * ClockMinute::NANOS_PER_MINUTE; |
| 243 | let second_nanos = self.second.of() as u64 * ClockSecond::NANOS_PER_SECOND; |
| 244 | let nanos = self.nanosecond.of() as u64; |
| 245 | |
| 246 | hour_nanos + minute_nanos + second_nanos + nanos |
| 247 | } |
| 248 | |
| 249 | pub fn nanos_of_day(&self) -> u64 { |
| 250 | self.to_nanos_of_day() |
| 251 | } |
| 252 | |
| 253 | pub fn millis_of_day(&self) -> u32 { |
| 254 | let nanos = self.to_nanos_of_day(); |
| 255 | (nanos / 1_000_000) as u32 |
| 256 | } |
| 257 | |
| 258 | pub fn from_millis_of_day(millis: u32, zone: CalClockZone) -> Outcome<Self> { |
| 259 | const MILLIS_PER_DAY: u32 = 24 * 60 * 60 * 1_000; |
| 260 | |
| 261 | if millis >= MILLIS_PER_DAY { |
| 262 | return Err(err!( |
| 263 | "Milliseconds {} exceed maximum for one day {}", |
| 264 | millis, |
| 265 | MILLIS_PER_DAY; |
| 266 | Invalid, Input |
| 267 | )); |
| 268 | } |
| 269 | |
| 270 | // Convert to nanoseconds and use existing method |
| 271 | let nanos = millis as u64 * 1_000_000; |
| 272 | Self::from_nanos_of_day(nanos, zone) |
| 273 | } |
| 274 | |
| 275 | pub fn is_valid_day_time(&self) -> bool { |
| 276 | self.hour.is_valid_day_hour() |
| 277 | } |
| 278 | |
| 279 | pub fn is_end_of_day(&self) -> bool { |
| 280 | self.hour.is_end_of_day() && |
| 281 | self.minute.of() == 0 && |
| 282 | self.second.of() == 0 && |
| 283 | self.nanosecond.of() == 0 |
| 284 | } |
| 285 | |
| 286 | pub fn is_leap_second(&self) -> bool { |
| 287 | self.second.is_leap_second() |
| 288 | } |
| 289 | |
| 290 | /// 23:59:60 is the only time at which a UTC leap second can fall. |
| 291 | pub fn is_potential_leap_second(&self) -> bool { |
| 292 | self.hour.of() == 23 && self.minute.of() == 59 && self.second.of() == 60 |
| 293 | } |
| 294 | |
| 295 | /// 23:59:60 becomes 00:00:00 of the next day. |
| 296 | pub fn normalize_leap_second(&self) -> Outcome<(Self, bool)> { |
| 297 | if !self.is_leap_second() { |
| 298 | return Ok((self.clone(), false)); |
| 299 | } |
| 300 | |
| 301 | // Leap second at 23:59:60 becomes 00:00:00 of next day |
| 302 | if self.hour.of() == 23 && self.minute.of() == 59 { |
| 303 | let normalized = res!(Self::new(0, 0, 0, self.nanosecond.of(), self.zone.clone())); |
| 304 | Ok((normalized, true)) // true indicates day overflow |
| 305 | } else { |
| 306 | // Invalid leap second time |
| 307 | Err(err!("Invalid leap second time: leap seconds only valid at 23:59:60"; Invalid, Input)) |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | pub fn is_before(&self, other: &Self) -> bool { |
| 312 | self.to_nanos_of_day() < other.to_nanos_of_day() |
| 313 | } |
| 314 | |
| 315 | pub fn is_after(&self, other: &Self) -> bool { |
| 316 | self.to_nanos_of_day() > other.to_nanos_of_day() |
| 317 | } |
| 318 | |
| 319 | pub fn or_earlier(&self, other: &Self) -> bool { |
| 320 | self.to_nanos_of_day() <= other.to_nanos_of_day() |
| 321 | } |
| 322 | |
| 323 | pub fn or_later(&self, other: &Self) -> bool { |
| 324 | self.to_nanos_of_day() >= other.to_nanos_of_day() |
| 325 | } |
| 326 | |
| 327 | pub fn plus(&self, duration: &ClockDuration) -> Outcome<(Self, i32)> { |
| 328 | let mut fields = ClockFields::from_time( |
| 329 | self.hour.of(), |
| 330 | self.minute.of(), |
| 331 | self.second.of(), |
| 332 | self.nanosecond.of(), |
| 333 | ); |
| 334 | |
| 335 | // Add duration nanoseconds |
| 336 | fields.nanosecond += duration.total_nanos() as i64; |
| 337 | |
| 338 | // Normalise and extract components |
| 339 | let (hour, minute, second, nanosecond, day_carry) = res!(fields |
| 340 | .to_time_components() |
| 341 | .ok_or_else(|| err!("Time arithmetic resulted in invalid time"; Invalid))); |
| 342 | |
| 343 | let new_time = res!(Self::new(hour, minute, second, nanosecond, self.zone.clone())); |
| 344 | Ok((new_time, day_carry)) |
| 345 | } |
| 346 | |
| 347 | pub fn minus(&self, duration: &ClockDuration) -> Outcome<(Self, i32)> { |
| 348 | let mut fields = ClockFields::from_time( |
| 349 | self.hour.of(), |
| 350 | self.minute.of(), |
| 351 | self.second.of(), |
| 352 | self.nanosecond.of(), |
| 353 | ); |
| 354 | |
| 355 | // Subtract duration nanoseconds |
| 356 | fields.nanosecond -= duration.total_nanos() as i64; |
| 357 | |
| 358 | // Normalise and extract components |
| 359 | let (hour, minute, second, nanosecond, day_carry) = res!(fields |
| 360 | .to_time_components() |
| 361 | .ok_or_else(|| err!("Time arithmetic resulted in invalid time"; Invalid))); |
| 362 | |
| 363 | let new_time = res!(Self::new(hour, minute, second, nanosecond, self.zone.clone())); |
| 364 | Ok((new_time, day_carry)) |
| 365 | } |
| 366 | |
| 367 | pub fn duration_until(&self, other: &Self) -> ClockDuration { |
| 368 | let self_nanos = self.to_nanos_of_day() as i64; |
| 369 | let other_nanos = other.to_nanos_of_day() as i64; |
| 370 | let diff_nanos = other_nanos - self_nanos; |
| 371 | |
| 372 | ClockDuration::from_nanos(diff_nanos) |
| 373 | } |
| 374 | |
| 375 | pub fn to_twelve_hour_string(&self) -> String { |
| 376 | let (hour, is_pm) = self.hour.to_twelve_hour(); |
| 377 | let ampm = if is_pm { "PM" } else { "AM" }; |
| 378 | fmt!("{}:{:02}:{:02} {}", hour, self.minute.of(), self.second.of(), ampm) |
| 379 | } |
| 380 | |
| 381 | pub fn to_twenty_four_hour_string(&self) -> String { |
| 382 | fmt!("{}:{}:{}", self.hour, self.minute, self.second) |
| 383 | } |
| 384 | |
| 385 | pub fn to_iso_string(&self) -> String { |
| 386 | if self.nanosecond.of() == 0 { |
| 387 | fmt!("{}:{}:{}", self.hour, self.minute, self.second) |
| 388 | } else { |
| 389 | fmt!("{}:{}:{}.{}", self.hour, self.minute, self.second, self.nanosecond) |
| 390 | } |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | impl Time for ClockTime { |
| 395 | fn get_zone(&self) -> &CalClockZone { |
| 396 | &self.zone |
| 397 | } |
| 398 | |
| 399 | fn to_zone(&self, new_zone: CalClockZone) -> Outcome<Self> { |
| 400 | // For ClockTime, this is administrative conversion |
| 401 | // (no actual time transformation since we don't have date context) |
| 402 | Ok(Self { |
| 403 | hour: self.hour, |
| 404 | minute: self.minute, |
| 405 | second: self.second, |
| 406 | nanosecond: self.nanosecond, |
| 407 | zone: new_zone, |
| 408 | }) |
| 409 | } |
| 410 | |
| 411 | fn format(&self, stencil: &str) -> String { |
| 412 | self.format_with_stencil(stencil) |
| 413 | } |
| 414 | |
| 415 | fn is_recognised_format_char(&self, c: char) -> bool { |
| 416 | matches!(c, 'H' | 'h' | 'm' | 's' | 'f' | 'F' | 't' | 'z') |
| 417 | } |
| 418 | |
| 419 | fn is_before(&self, other: &Self) -> bool { |
| 420 | self.to_nanos_of_day() < other.to_nanos_of_day() |
| 421 | } |
| 422 | |
| 423 | fn is_after(&self, other: &Self) -> bool { |
| 424 | self.to_nanos_of_day() > other.to_nanos_of_day() |
| 425 | } |
| 426 | |
| 427 | fn or_earlier(&self, other: &Self) -> Self { |
| 428 | if self.to_nanos_of_day() <= other.to_nanos_of_day() { |
| 429 | self.clone() |
| 430 | } else { |
| 431 | other.clone() |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | fn or_later(&self, other: &Self) -> Self { |
| 436 | if self.to_nanos_of_day() >= other.to_nanos_of_day() { |
| 437 | self.clone() |
| 438 | } else { |
| 439 | other.clone() |
| 440 | } |
| 441 | } |
| 442 | } |
| 443 | |
| 444 | // Validation methods |
| 445 | impl ClockTime { |
| 446 | pub fn is_valid(&self) -> bool { |
| 447 | self.hour.of() <= 24 && |
| 448 | self.minute.of() <= 60 && |
| 449 | self.second.of() <= 60 && |
| 450 | self.nanosecond.of() <= 999_999_999 |
| 451 | } |
| 452 | |
| 453 | /// Day overflow is dropped here, because CalClock carries it. |
| 454 | pub fn add_duration(&self, duration: &ClockDuration) -> Outcome<Self> { |
| 455 | let (new_time, _day_overflow) = res!(self.plus(duration)); |
| 456 | Ok(new_time) |
| 457 | } |
| 458 | |
| 459 | pub fn subtract_duration(&self, duration: &ClockDuration) -> Outcome<Self> { |
| 460 | let (new_time, _day_underflow) = res!(self.minus(duration)); |
| 461 | Ok(new_time) |
| 462 | } |
| 463 | |
| 464 | // ======================================================================== |
| 465 | // String Formatting Implementation (ported from Java original) |
| 466 | // ======================================================================== |
| 467 | |
| 468 | /// The stencil characters, from the Java implementation: |
| 469 | /// - h hh Hour (12hr) 4 04 |
| 470 | /// - H HH Hour (24hr) 16 16 |
| 471 | /// - m mm Minute 5 05 |
| 472 | /// - s ss Second 7 07 |
| 473 | /// - f ff fff ffff Sec frac 1 12 123 1230 |
| 474 | /// - F FF FFF FFFF Sec frac nozero 1 12 123 123 |
| 475 | /// - t tt A.M. or P.M. P PM |
| 476 | /// - z zz zzz zzzz Timezone +1 +01 +01:00 UTC |
| 477 | pub fn format_with_stencil(&self, stencil: &str) -> String { |
| 478 | let mut output = String::new(); |
| 479 | let chars: Vec<char> = stencil.chars().collect(); |
| 480 | let mut i = 0; |
| 481 | |
| 482 | while i < chars.len() { |
| 483 | let curr = chars[i]; |
| 484 | |
| 485 | if self.is_recognised_format_char(curr) { |
| 486 | // Count consecutive occurrences of the same format character |
| 487 | let mut token_len = 1; |
| 488 | while i + token_len < chars.len() && chars[i + token_len] == curr { |
| 489 | token_len += 1; |
| 490 | } |
| 491 | |
| 492 | // Process the token |
| 493 | self.switch_on_format_char(curr, token_len, &mut output); |
| 494 | i += token_len; |
| 495 | } else { |
| 496 | // Regular character, append as-is |
| 497 | output.push(curr); |
| 498 | i += 1; |
| 499 | } |
| 500 | } |
| 501 | |
| 502 | output |
| 503 | } |
| 504 | |
| 505 | fn switch_on_format_char(&self, c: char, n: usize, sb: &mut String) { |
| 506 | match c { |
| 507 | 'h' => self.format_hour_12(n, sb, self.hour.of() as i32), |
| 508 | 'H' => self.format_hour_24(n, sb, self.hour.of() as i32), |
| 509 | 'm' => self.format_minute(n, sb, self.minute.of() as i32), |
| 510 | 's' => self.format_second(n, sb, self.second.of() as i32), |
| 511 | 'f' => self.format_fraction(n, sb, self.nanosecond.of(), false), |
| 512 | 'F' => self.format_fraction(n, sb, self.nanosecond.of(), true), |
| 513 | 't' => self.format_am_pm(n, sb, self.hour.of() as i32), |
| 514 | 'z' => self.format_timezone(n, sb), |
| 515 | _ => {} |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | fn format_hour_12(&self, n: usize, sb: &mut String, val: i32) { |
| 520 | let hour_12 = if val == 0 { 12 } else if val > 12 { val - 12 } else { val }; |
| 521 | match n { |
| 522 | 1 => sb.push_str(&hour_12.to_string()), |
| 523 | _ => sb.push_str(&fmt!("{:02}", hour_12)), |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | fn format_hour_24(&self, n: usize, sb: &mut String, val: i32) { |
| 528 | match n { |
| 529 | 1 => sb.push_str(&val.to_string()), |
| 530 | _ => sb.push_str(&fmt!("{:02}", val)), |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | fn format_minute(&self, n: usize, sb: &mut String, val: i32) { |
| 535 | match n { |
| 536 | 1 => sb.push_str(&val.to_string()), |
| 537 | _ => sb.push_str(&fmt!("{:02}", val)), |
| 538 | } |
| 539 | } |
| 540 | |
| 541 | fn format_second(&self, n: usize, sb: &mut String, val: i32) { |
| 542 | match n { |
| 543 | 1 => sb.push_str(&val.to_string()), |
| 544 | _ => sb.push_str(&fmt!("{:02}", val)), |
| 545 | } |
| 546 | } |
| 547 | |
| 548 | fn format_fraction(&self, n: usize, sb: &mut String, nanos: u32, suppress_trailing_zeros: bool) { |
| 549 | let fraction_str = fmt!("{:09}", nanos); |
| 550 | let mut result = if n <= 9 { |
| 551 | fraction_str[..n].to_string() |
| 552 | } else { |
| 553 | fraction_str |
| 554 | }; |
| 555 | |
| 556 | if suppress_trailing_zeros { |
| 557 | result = result.trim_end_matches('0').to_string(); |
| 558 | if result.is_empty() { |
| 559 | result = "0".to_string(); |
| 560 | } |
| 561 | } |
| 562 | |
| 563 | sb.push_str(&result); |
| 564 | } |
| 565 | |
| 566 | fn format_am_pm(&self, n: usize, sb: &mut String, hour: i32) { |
| 567 | let is_pm = hour >= 12; |
| 568 | match n { |
| 569 | 1 => sb.push_str(if is_pm { "P" } else { "A" }), |
| 570 | _ => sb.push_str(if is_pm { "PM" } else { "AM" }), |
| 571 | } |
| 572 | } |
| 573 | |
| 574 | fn format_timezone(&self, n: usize, sb: &mut String) { |
| 575 | match n { |
| 576 | 1 => sb.push_str(self.zone.id()), // Simple ID |
| 577 | 2 => sb.push_str(self.zone.id()), // Short format |
| 578 | 3 => sb.push_str(self.zone.id()), // Medium format |
| 579 | _ => sb.push_str(self.zone.id()), // Long format |
| 580 | } |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | impl PartialOrd for ClockTime { |
| 585 | fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { |
| 586 | Some(self.cmp(other)) |
| 587 | } |
| 588 | } |
| 589 | |
| 590 | impl Ord for ClockTime { |
| 591 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
| 592 | self.to_nanos_of_day().cmp(&other.to_nanos_of_day()) |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | impl fmt::Display for ClockTime { |
| 597 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 598 | write!(f, "{}", self.to_iso_string()) |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | #[cfg(test)] |
| 603 | mod tests { |
| 604 | use super::*; |
| 605 | |
| 606 | fn test_zone() -> CalClockZone { |
| 607 | CalClockZone::utc() |
| 608 | } |
| 609 | |
| 610 | #[test] |
| 611 | fn test_clock_time_creation() { |
| 612 | let time = ClockTime::new(14, 30, 45, 123_456_789, test_zone()); |
| 613 | assert!(time.is_ok()); |
| 614 | |
| 615 | let time = time.unwrap(); |
| 616 | assert_eq!(time.hour().of(), 14); |
| 617 | assert_eq!(time.minute().of(), 30); |
| 618 | assert_eq!(time.second().of(), 45); |
| 619 | assert_eq!(time.nanosecond().of(), 123_456_789); |
| 620 | } |
| 621 | |
| 622 | #[test] |
| 623 | fn test_special_times() { |
| 624 | let midnight = ClockTime::midnight(test_zone()).unwrap(); |
| 625 | assert_eq!(midnight.hour().of(), 0); |
| 626 | assert!(midnight.is_valid_day_time()); |
| 627 | |
| 628 | let noon = ClockTime::noon(test_zone()).unwrap(); |
| 629 | assert_eq!(noon.hour().of(), 12); |
| 630 | |
| 631 | let eod = ClockTime::end_of_day(test_zone()).unwrap(); |
| 632 | assert_eq!(eod.hour().of(), 24); |
| 633 | assert!(eod.is_end_of_day()); |
| 634 | assert!(!eod.is_valid_day_time()); |
| 635 | } |
| 636 | |
| 637 | #[test] |
| 638 | fn test_nanos_of_day_conversion() { |
| 639 | let time = ClockTime::new(12, 30, 45, 123_456_789, test_zone()).unwrap(); |
| 640 | let nanos = time.to_nanos_of_day(); |
| 641 | |
| 642 | let restored = ClockTime::from_nanos_of_day(nanos, test_zone()).unwrap(); |
| 643 | assert_eq!(time, restored); |
| 644 | } |
| 645 | |
| 646 | #[test] |
| 647 | fn test_time_comparison() { |
| 648 | let time1 = ClockTime::new(10, 30, 0, 0, test_zone()).unwrap(); |
| 649 | let time2 = ClockTime::new(14, 30, 0, 0, test_zone()).unwrap(); |
| 650 | |
| 651 | assert!(time1.is_before(&time2)); |
| 652 | assert!(time2.is_after(&time1)); |
| 653 | assert!(time1.or_earlier(&time2)); |
| 654 | assert!(time2.or_later(&time1)); |
| 655 | |
| 656 | assert!(!time1.is_after(&time2)); |
| 657 | assert!(!time2.is_before(&time1)); |
| 658 | } |
| 659 | |
| 660 | #[test] |
| 661 | fn test_twelve_hour_format() { |
| 662 | let time = ClockTime::new(14, 30, 45, 0, test_zone()).unwrap(); |
| 663 | assert_eq!(time.to_twelve_hour_string(), "2:30:45 PM"); |
| 664 | |
| 665 | let time = ClockTime::new(0, 15, 30, 0, test_zone()).unwrap(); |
| 666 | assert_eq!(time.to_twelve_hour_string(), "12:15:30 AM"); |
| 667 | } |
| 668 | |
| 669 | #[test] |
| 670 | fn test_iso_format() { |
| 671 | let time = ClockTime::new(14, 30, 45, 0, test_zone()).unwrap(); |
| 672 | assert_eq!(time.to_iso_string(), "14:30:45"); |
| 673 | |
| 674 | let time = ClockTime::new(14, 30, 45, 123_456_789, test_zone()).unwrap(); |
| 675 | assert_eq!(time.to_iso_string(), "14:30:45.123456789"); |
| 676 | } |
| 677 | } |