Oregami
Repositories/oxedyne/fe2o3

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
4use 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
17use oxedyne_fe2o3_core::prelude::*;
18
19use 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)]
37pub struct ClockTime {
38 hour: ClockHour,
39 minute: ClockMinute,
40 second: ClockSecond,
41 nanosecond: ClockNanoSecond,
42 zone: CalClockZone,
43}
44
45impl 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
394impl 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
445impl 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
584impl PartialOrd for ClockTime {
585 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
586 Some(self.cmp(other))
587 }
588}
589
590impl 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
596impl 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)]
603mod 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}