Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/time/calclock.rs

33.4 KiB, 153 runs

created by r1870400018:6383, 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 calendar::{Calendar, CalendarDate},
6 clock::{ClockDuration, ClockTime},
7 constant::{DayOfWeek, MonthOfYear},
8 time::{CalClockDuration, CalClockZone, LeapSecondConfig},
9};
10
11use oxedyne_fe2o3_core::prelude::*;
12use oxedyne_fe2o3_jdat::{prelude::*, tup2dat};
13
14use std::{
15 cmp::Ordering,
16 fmt,
17};
18
19/// A CalendarDate and a ClockTime together, holding the invariant that both use
20/// the same zone.
21///
22/// ```ignore
23/// use oxedyne_fe2o3_datime::{
24/// calendar::CalendarDate,
25/// clock::ClockTime,
26/// time::{CalClock, CalClockZone},
27/// }res!();
28///
29/// let zone = CalClockZone::utc()res!();
30/// let calclock = res!(CalClock::new(2024, 6, 15, 14, 30, 0, 0, zone))res!();
31/// assert_eq!(calclock.date().year(), 2024)res!();
32/// assert_eq!(calclock.time().hour().of(), 14)res!();
33/// ```
34#[derive(Clone, Debug, PartialEq, Hash)]
35pub struct CalClock {
36 date: CalendarDate,
37 time: ClockTime,
38}
39
40impl CalClock {
41 pub fn from_date_time(date: CalendarDate, time: ClockTime) -> Outcome<Self> {
42 // Ensure both components use the same time zone.
43 if date.zone() != time.zone() {
44 return Err(err!("Date and time must use the same time zone"; Invalid, Input));
45 }
46
47 Ok(Self { date, time })
48 }
49
50 pub fn new(
51 year: i32,
52 month: u8,
53 day: u8,
54 hour: u8,
55 minute: u8,
56 second: u8,
57 nanosecond: u32,
58 zone: CalClockZone,
59 ) -> Outcome<Self> {
60 let calendar = Calendar::new(); // Default to Gregorian
61 let date = res!(calendar.date(year, month, day, zone.clone()));
62 let time = res!(ClockTime::new(hour, minute, second, nanosecond, zone));
63 Self::from_date_time(date, time)
64 }
65
66 /// Second 60 is accepted only where the configuration allows it and the leap
67 /// second table agrees.
68 pub fn new_with_leap_seconds(
69 year: i32,
70 month: u8,
71 day: u8,
72 hour: u8,
73 minute: u8,
74 second: u8,
75 nanosecond: u32,
76 zone: CalClockZone,
77 config: &LeapSecondConfig,
78 ) -> Outcome<Self> {
79 let calendar = Calendar::new(); // Default to Gregorian
80 let date = res!(calendar.date(year, month, day, zone.clone()));
81
82 // For leap second validation, we need to check the complete date/time context
83 if second == 60 && config.validate_leap_seconds {
84 let table = config.get_table();
85 if !table.validate_leap_second(year, month, day, hour, minute) {
86 return Err(err!(
87 "Invalid leap second: {}-{:02}-{:02} {}:{:02}:60 is not a valid leap second according to the leap second table",
88 year, month, day, hour, minute;
89 Invalid, Input
90 ));
91 }
92 }
93
94 let time = res!(ClockTime::new_with_leap_seconds(hour, minute, second, nanosecond, zone, config));
95 Self::from_date_time(date, time)
96 }
97
98 pub fn date(&self) -> &CalendarDate {
99 &self.date
100 }
101
102 pub fn time(&self) -> &ClockTime {
103 &self.time
104 }
105
106 pub fn zone(&self) -> &CalClockZone {
107 self.date.zone()
108 }
109
110 // ========================================================================
111 // Timestamp and Epoch Conversion Methods
112 // ========================================================================
113
114 /// The zone offset in force at that date and time is applied.
115 pub fn to_millis(&self) -> Outcome<i64> {
116 let days_since_epoch = res!(self.date.days_since_epoch());
117 let millis_from_days = days_since_epoch * 24 * 60 * 60 * 1000;
118 let millis_from_time = self.time.millis_of_day();
119 let total_millis = millis_from_days + millis_from_time as i64;
120
121 // Adjust for timezone offset
122 let offset_millis = res!(self.zone().offset_millis_at_time(total_millis));
123 Ok(total_millis - offset_millis as i64)
124 }
125
126 pub fn to_seconds(&self) -> Outcome<i64> {
127 let millis = res!(self.to_millis());
128 Ok(millis / 1000)
129 }
130
131 pub fn to_nanos(&self) -> Outcome<i64> {
132 let millis = res!(self.to_millis());
133 // `to_millis` already carries the whole-millisecond part of the sub-second
134 // field, because `millis_of_day` is `nanos_of_day / 1_000_000`. Adding the
135 // entire nanosecond-of-second field back would count those milliseconds
136 // twice, and would leave the result non-monotonic across a second boundary.
137 // Only the sub-millisecond remainder is still missing. Zone offsets are
138 // whole milliseconds, so nothing below a millisecond is lost by `to_millis`.
139 let sub_milli = (self.time.nanosecond().of() % 1_000_000) as i64;
140 Ok(millis * 1_000_000 + sub_milli)
141 }
142
143 pub fn from_millis(millis: i64, zone: CalClockZone) -> Outcome<Self> {
144 // Get timezone offset for this timestamp
145 let offset_millis = res!(zone.offset_millis_at_time(millis));
146 let local_millis = millis + offset_millis as i64;
147
148 // Euclidean, so a pre-epoch instant floors onto the day below it and
149 // leaves a non-negative millisecond-of-day. Truncating division would
150 // put 1969-12-31T23:59:59.999Z on day 0 with a negative remainder, which
151 // then wraps when cast to u32.
152 let days_since_epoch = local_millis.div_euclid(24 * 60 * 60 * 1000);
153 let millis_of_day = local_millis.rem_euclid(24 * 60 * 60 * 1000) as u32;
154
155 // Create date from days since epoch
156 let date = res!(CalendarDate::from_days_since_epoch(days_since_epoch, zone.clone()));
157
158 // Create time from milliseconds of day
159 let time = res!(ClockTime::from_millis_of_day(millis_of_day, zone));
160
161 Self::from_date_time(date, time)
162 }
163
164 pub fn from_seconds(seconds: i64, zone: CalClockZone) -> Outcome<Self> {
165 Self::from_millis(seconds * 1000, zone)
166 }
167
168 pub fn from_nanos(nanos: i64, zone: CalClockZone) -> Outcome<Self> {
169 let millis = nanos.div_euclid(1_000_000);
170 let mut result = res!(Self::from_millis(millis, zone));
171
172 // `from_millis` has already set the millisecond part of the sub-second
173 // field. The sub-millisecond remainder adds to it; replacing it, as this
174 // once did, threw the milliseconds away and broke the round trip with
175 // `to_nanos`.
176 let sub_milli = nanos.rem_euclid(1_000_000) as u32;
177 result.time = res!(ClockTime::new(
178 result.time.hour().of(),
179 result.time.minute().of(),
180 result.time.second().of(),
181 result.time.nanosecond().of() + sub_milli,
182 result.zone().clone()
183 ));
184
185 Ok(result)
186 }
187
188 // ========================================================================
189 // Date Component Access Methods
190 // ========================================================================
191
192 pub fn year(&self) -> i32 {
193 self.date.year()
194 }
195
196 pub fn month(&self) -> u8 {
197 self.date.month()
198 }
199
200 pub fn day(&self) -> u8 {
201 self.date.day()
202 }
203
204 pub fn day_of_week(&self) -> DayOfWeek {
205 self.date.day_of_week()
206 }
207
208 pub fn day_of_year(&self) -> Outcome<u16> {
209 self.date.day_of_year()
210 }
211
212 pub fn week_of_year(&self) -> Outcome<u8> {
213 self.date.week_of_year()
214 }
215
216 pub fn month_of_year(&self) -> MonthOfYear {
217 self.date.month_of_year()
218 }
219
220 // ========================================================================
221 // Time Component Access Methods
222 // ========================================================================
223
224 pub fn hour(&self) -> u8 {
225 self.time.hour().of()
226 }
227
228 pub fn minute(&self) -> u8 {
229 self.time.minute().of()
230 }
231
232 pub fn second(&self) -> u8 {
233 self.time.second().of()
234 }
235
236 pub fn nanosecond(&self) -> u32 {
237 self.time.nanosecond().of()
238 }
239
240 pub fn millisecond(&self) -> u16 {
241 (self.nanosecond() / 1_000_000) as u16
242 }
243
244 pub fn microsecond(&self) -> u16 {
245 ((self.nanosecond() % 1_000_000) / 1_000) as u16
246 }
247
248 // ========================================================================
249 // Leap Second Support Methods
250 // ========================================================================
251
252 pub fn is_leap_second(&self) -> bool {
253 self.time.is_leap_second()
254 }
255
256 pub fn is_potential_leap_second(&self) -> bool {
257 self.time.is_potential_leap_second()
258 }
259
260 /// True also when this is not a leap second at all.
261 pub fn validate_leap_second(&self, config: &LeapSecondConfig) -> bool {
262 if !self.is_leap_second() {
263 return true;
264 }
265
266 if !config.enabled || !config.validate_leap_seconds {
267 return config.allow_leap_second_parsing;
268 }
269
270 let table = config.get_table();
271 table.validate_leap_second(self.year(), self.month(), self.day(), self.hour(), self.minute())
272 }
273
274 /// UTC plus the TAI-UTC offset in force, which is atomic time.
275 pub fn to_tai_timestamp(&self, config: &LeapSecondConfig) -> Outcome<i64> {
276 let utc_seconds = res!(self.to_seconds());
277 let table = config.get_table();
278 Ok(table.utc_to_tai(utc_seconds))
279 }
280
281 pub fn from_tai_timestamp(tai_seconds: i64, zone: CalClockZone, config: &LeapSecondConfig) -> Outcome<Self> {
282 let table = config.get_table();
283 let utc_seconds = res!(table.tai_to_utc(tai_seconds));
284 Self::from_seconds(utc_seconds, zone)
285 }
286
287 /// Returns the normalised time and whether the date advanced with it.
288 pub fn normalize_leap_second(&self) -> Outcome<(Self, bool)> {
289 if !self.is_leap_second() {
290 return Ok((self.clone(), false));
291 }
292
293 // Leap second at 23:59:60 becomes 00:00:00 of next day
294 if self.hour() == 23 && self.minute() == 59 {
295 let next_day = res!(self.add_days(1));
296 let normalized = res!(Self::new(
297 next_day.year(),
298 next_day.month(),
299 next_day.day(),
300 0, 0, 0,
301 self.nanosecond(),
302 self.zone().clone()
303 ));
304 Ok((normalized, true))
305 } else {
306 Err(err!("Invalid leap second time: leap seconds only valid at 23:59:60"; Invalid, Input))
307 }
308 }
309
310 // ========================================================================
311 // Arithmetic and Mutation Methods
312 // ========================================================================
313
314 pub fn add_duration(&self, duration: &CalClockDuration) -> Outcome<Self> {
315 const NANOS_PER_DAY: i64 = 86_400 * 1_000_000_000;
316 // Fold the whole duration -- its day field and its nanoseconds together -- and the time already
317 // held into a single nanosecond count, then split off whole days once at the end. Adding the
318 // date part and the time part in isolation loses the carry: a duration whose nanoseconds run to
319 // more than a day (which is how `from_seconds` and thus a Unix timestamp arrive, with every
320 // second in the nanosecond field and none in the day field) advances only the time, which wraps
321 // at midnight and drops the days; and a duration that pushes the held time past midnight never
322 // moves the date. One normalisation with `div_euclid` handles both, and negatives besides --
323 // the remainder is always in `[0, NANOS_PER_DAY)`, so it is a valid nanosecond-of-day.
324 let held = self.time.to_nanos_of_day() as i64;
325 let added = duration.days() as i64 * NANOS_PER_DAY + duration.nanoseconds();
326 let total = held + added;
327 let day_carry = total.div_euclid(NANOS_PER_DAY);
328 let rem = total.rem_euclid(NANOS_PER_DAY);
329 let new_date = res!(self.date.add_days(day_carry as i32));
330 let new_time = res!(ClockTime::from_nanos_of_day(rem as u64, self.time.zone().clone()));
331 Self::from_date_time(new_date, new_time)
332 }
333
334 /// The negation of [`add_duration`](Self::add_duration), and routed through it so the same carry
335 /// across midnight is handled the same way rather than a second time and differently.
336 pub fn subtract_duration(&self, duration: &CalClockDuration) -> Outcome<Self> {
337 self.add_duration(&duration.negate())
338 }
339
340 pub fn add_years(&self, years: i32) -> Outcome<Self> {
341 let new_date = res!(self.date.add_years(years));
342 Self::from_date_time(new_date, self.time.clone())
343 }
344
345 pub fn add_months(&self, months: i32) -> Outcome<Self> {
346 let new_date = res!(self.date.add_months(months));
347 Self::from_date_time(new_date, self.time.clone())
348 }
349
350 pub fn add_weeks(&self, weeks: i32) -> Outcome<Self> {
351 let new_date = res!(self.date.add_days(weeks * 7));
352 Self::from_date_time(new_date, self.time.clone())
353 }
354
355 pub fn add_days(&self, days: i32) -> Outcome<Self> {
356 let new_date = res!(self.date.add_days(days));
357 Self::from_date_time(new_date, self.time.clone())
358 }
359
360 pub fn add_hours(&self, hours: i32) -> Outcome<Self> {
361 let duration = ClockDuration::from_hours(hours.into());
362 let new_time = res!(self.time.add_duration(&duration));
363
364 // Handle day overflow
365 if new_time.hour().of() < self.time.hour().of() && hours > 0 {
366 let new_date = res!(self.date.add_days(1));
367 Self::from_date_time(new_date, new_time)
368 } else if new_time.hour().of() > self.time.hour().of() && hours < 0 {
369 let new_date = res!(self.date.add_days(-1));
370 Self::from_date_time(new_date, new_time)
371 } else {
372 Self::from_date_time(self.date.clone(), new_time)
373 }
374 }
375
376 pub fn add_minutes(&self, minutes: i32) -> Outcome<Self> {
377 let duration = ClockDuration::from_minutes(minutes.into());
378 let new_time = res!(self.time.add_duration(&duration));
379
380 // Handle day overflow/underflow
381 let time_diff = new_time.to_nanos_of_day() as i64 - self.time.to_nanos_of_day() as i64;
382 let expected_diff = minutes as i64 * 60 * 1_000_000_000;
383
384 if time_diff != expected_diff {
385 let day_adjustment = if minutes > 0 { 1 } else { -1 };
386 let new_date = res!(self.date.add_days(day_adjustment));
387 Self::from_date_time(new_date, new_time)
388 } else {
389 Self::from_date_time(self.date.clone(), new_time)
390 }
391 }
392
393 pub fn add_seconds(&self, seconds: i32) -> Outcome<Self> {
394 let duration = ClockDuration::from_seconds(seconds.into());
395 let new_time = res!(self.time.add_duration(&duration));
396
397 // Handle day overflow/underflow
398 let time_diff = new_time.to_nanos_of_day() as i64 - self.time.to_nanos_of_day() as i64;
399 let expected_diff = seconds as i64 * 1_000_000_000;
400
401 if time_diff != expected_diff {
402 let day_adjustment = if seconds > 0 { 1 } else { -1 };
403 let new_date = res!(self.date.add_days(day_adjustment));
404 Self::from_date_time(new_date, new_time)
405 } else {
406 Self::from_date_time(self.date.clone(), new_time)
407 }
408 }
409
410 pub fn add_millis(&self, millis: i64) -> Outcome<Self> {
411 let duration = ClockDuration::from_millis(millis);
412 let new_time = res!(self.time.add_duration(&duration));
413
414 // Handle day overflow/underflow
415 let time_diff = new_time.to_nanos_of_day() as i64 - self.time.to_nanos_of_day() as i64;
416 let expected_diff = millis * 1_000_000;
417
418 if time_diff != expected_diff {
419 let day_adjustment = if millis > 0 { 1 } else { -1 };
420 let new_date = res!(self.date.add_days(day_adjustment));
421 Self::from_date_time(new_date, new_time)
422 } else {
423 Self::from_date_time(self.date.clone(), new_time)
424 }
425 }
426
427 // ========================================================================
428 // Time Zone Conversion Methods
429 // ========================================================================
430
431 /// The instant is preserved; only the local reading of it changes.
432 pub fn with_zone(&self, new_zone: CalClockZone) -> Outcome<Self> {
433 // Get the UTC timestamp
434 let utc_millis = res!(self.to_millis());
435
436 // Create new CalClock in the target zone
437 Self::from_millis(utc_millis, new_zone)
438 }
439
440 pub fn is_same_instant(&self, other: &Self) -> Outcome<bool> {
441 let self_millis = res!(self.to_millis());
442 let other_millis = res!(other.to_millis());
443 Ok(self_millis == other_millis)
444 }
445
446 // ========================================================================
447 // Comparison and Validation Methods
448 // ========================================================================
449
450 pub fn is_leap_year(&self) -> bool {
451 self.date.is_leap_year()
452 }
453
454 pub fn is_valid_date(&self) -> bool {
455 self.date.is_valid()
456 }
457
458 pub fn is_valid_time(&self) -> bool {
459 self.time.is_valid()
460 }
461
462 pub fn is_valid(&self) -> bool {
463 self.is_valid_date() && self.is_valid_time()
464 }
465
466 // ========================================================================
467 // Utility Methods
468 // ========================================================================
469
470 pub fn start_of_day(&self) -> Outcome<Self> {
471 let start_time = res!(ClockTime::new(0, 0, 0, 0, self.zone().clone()));
472 Self::from_date_time(self.date.clone(), start_time)
473 }
474
475 pub fn end_of_day(&self) -> Outcome<Self> {
476 let end_time = res!(ClockTime::new(23, 59, 59, 999_999_999, self.zone().clone()));
477 Self::from_date_time(self.date.clone(), end_time)
478 }
479
480 pub fn start_of_month(&self) -> Outcome<Self> {
481 let calendar = Calendar::new(); // Default to Gregorian
482 let start_date = res!(calendar.date(self.year(), self.month(), 1, self.zone().clone()));
483 let start_time = res!(ClockTime::new(0, 0, 0, 0, self.zone().clone()));
484 Self::from_date_time(start_date, start_time)
485 }
486
487 pub fn end_of_month(&self) -> Outcome<Self> {
488 let days_in_month = res!(self.date.days_in_month());
489 let calendar = Calendar::new(); // Default to Gregorian
490 let end_date = res!(calendar.date(self.year(), self.month(), days_in_month, self.zone().clone()));
491 let end_time = res!(ClockTime::new(23, 59, 59, 999_999_999, self.zone().clone()));
492 Self::from_date_time(end_date, end_time)
493 }
494
495 pub fn start_of_year(&self) -> Outcome<Self> {
496 let calendar = Calendar::new(); // Default to Gregorian
497 let start_date = res!(calendar.date(self.year(), 1, 1, self.zone().clone()));
498 let start_time = res!(ClockTime::new(0, 0, 0, 0, self.zone().clone()));
499 Self::from_date_time(start_date, start_time)
500 }
501
502 pub fn end_of_year(&self) -> Outcome<Self> {
503 let calendar = Calendar::new(); // Default to Gregorian
504 let end_date = res!(calendar.date(self.year(), 12, 31, self.zone().clone()));
505 let end_time = res!(ClockTime::new(23, 59, 59, 999_999_999, self.zone().clone()));
506 Self::from_date_time(end_date, end_time)
507 }
508
509 pub fn at_noon(&self) -> Outcome<Self> {
510 let noon_time = res!(ClockTime::new(12, 0, 0, 0, self.zone().clone()));
511 Self::from_date_time(self.date.clone(), noon_time)
512 }
513
514 pub fn at_midnight(&self) -> Outcome<Self> {
515 self.start_of_day()
516 }
517
518 pub fn now(zone: CalClockZone) -> Outcome<Self> {
519 use std::time::{SystemTime, UNIX_EPOCH};
520
521 let duration = res!(SystemTime::now().duration_since(UNIX_EPOCH)
522 .map_err(|e| err!("System time before Unix epoch: {}", e; System)));
523 let millis = duration.as_millis() as i64;
524
525 Self::from_millis(millis, zone)
526 }
527
528 pub fn now_utc() -> Outcome<Self> {
529 Self::now(CalClockZone::utc())
530 }
531
532 pub fn now_local() -> Outcome<Self> {
533 Self::now(CalClockZone::local())
534 }
535
536 // ========================================================================
537 // Duration Between CalClocks
538 // ========================================================================
539
540 /// Positive when other is the later of the two.
541 pub fn duration_until(&self, other: &Self) -> Outcome<CalClockDuration> {
542 let self_nanos = res!(self.to_nanos());
543 let other_nanos = res!(other.to_nanos());
544 let diff_nanos = other_nanos - self_nanos;
545
546 Ok(CalClockDuration::from_nanos(diff_nanos))
547 }
548
549 pub fn duration_since(&self, other: &Self) -> Outcome<CalClockDuration> {
550 other.duration_until(self)
551 }
552
553 pub fn days_until(&self, other: &Self) -> Outcome<i64> {
554 let duration = res!(self.duration_until(other));
555 Ok(duration.total_days())
556 }
557
558 pub fn days_since(&self, other: &Self) -> Outcome<i64> {
559 other.days_until(self)
560 }
561
562 // ========================================================================
563 // Comparison Methods
564 // ========================================================================
565
566 pub fn is_before(&self, other: &Self) -> bool {
567 match (self.to_nanos(), other.to_nanos()) {
568 (Ok(self_nanos), Ok(other_nanos)) => self_nanos < other_nanos,
569 _ => false,
570 }
571 }
572
573 pub fn is_after(&self, other: &Self) -> bool {
574 match (self.to_nanos(), other.to_nanos()) {
575 (Ok(self_nanos), Ok(other_nanos)) => self_nanos > other_nanos,
576 _ => false,
577 }
578 }
579
580 // ========================================================================
581 // Formatting Methods
582 // ========================================================================
583
584 pub fn to_iso8601(&self) -> Outcome<String> {
585 // Format: YYYY-MM-DDTHH:MM:SS.nnnnnnnnn+HH:MM
586 let date_part = format!("{:04}-{:02}-{:02}",
587 self.year(), self.month(), self.day());
588 let time_part = format!("{:02}:{:02}:{:02}.{:09}",
589 self.hour(), self.minute(), self.second(), self.nanosecond());
590
591 // Get timezone offset
592 let offset_millis = res!(self.zone().offset_millis_at_time(res!(self.to_millis())));
593 let offset_hours = offset_millis / (60 * 60 * 1000);
594 let offset_minutes = (offset_millis.abs() % (60 * 60 * 1000)) / (60 * 1000);
595
596 let offset_part = if offset_millis == 0 {
597 "Z".to_string()
598 } else {
599 format!("{:+03}:{:02}", offset_hours, offset_minutes)
600 };
601
602 Ok(format!("{}T{}{}", date_part, time_part, offset_part))
603 }
604}
605
606// ========================================================================
607// Trait Implementations
608// ========================================================================
609
610impl PartialOrd for CalClock {
611 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
612 match self.to_nanos() {
613 Ok(self_nanos) => match other.to_nanos() {
614 Ok(other_nanos) => self_nanos.partial_cmp(&other_nanos),
615 Err(_) => None,
616 },
617 Err(_) => None,
618 }
619 }
620}
621
622// ============================================================================
623// JDAT Integration for CalClock
624// ============================================================================
625
626impl ToDat for CalClock {
627 fn to_dat(&self) -> Outcome<Dat> {
628 // Use existing string formatting
629 let datetime_string = fmt!("{}", self);
630 Ok(Dat::Str(datetime_string))
631 }
632}
633
634impl FromDat for CalClock {
635 /// Accepts either the string or the binary form.
636 fn from_dat(dat: Dat) -> Outcome<Self> {
637 match dat {
638 // String format - use existing ISO parser
639 Dat::Str(s) => {
640 // Parse ISO 8601 string using existing parser
641 Self::parse_iso(&s)
642 },
643
644 // Binary packed format (i64 nanoseconds since epoch + zone info)
645 Dat::Tup2(elements) if elements.len() == 2 => {
646 let nanos = match &elements[0] {
647 Dat::I64(n) => n,
648 _ => return Err(err!("Expected i64 nanoseconds in CalClock tuple"; Invalid, Input)),
649 };
650
651 let zone_str = match &elements[1] {
652 Dat::Str(s) => s,
653 _ => return Err(err!("Expected string for timezone in CalClock tuple"; Invalid, Input)),
654 };
655
656 let zone = res!(CalClockZone::new(zone_str));
657 Self::from_nanos_since_epoch(*nanos, zone)
658 },
659
660 _ => Err(err!("Expected string or tuple for CalClock"; Invalid, Input)),
661 }
662 }
663}
664
665impl CalClock {
666 /// A (nanoseconds since the epoch, zone) tuple, far shorter than the string form.
667 pub fn to_dat_binary(&self) -> Outcome<Dat> {
668 let nanos = res!(self.to_nanos_since_epoch());
669 // Zone as string
670
671 Ok(tup2dat!(
672 dat!(nanos),
673 dat!(self.zone().to_string()),
674 ))
675 }
676
677 pub fn to_dat_structured(&self) -> Outcome<Dat> {
678 Ok(mapdat! {
679 "year" => self.year(),
680 "month" => self.month(),
681 "day" => self.day(),
682 "hour" => self.hour(),
683 "minute" => self.minute(),
684 "second" => self.second(),
685 "nanosecond" => self.nanosecond(),
686 "timezone" => self.zone().to_string(),
687 "calendar" => "gregorian", // Default calendar system
688 "datetime_string" => fmt!("{}", self),
689 "unix_millis" => res!(self.to_millis()),
690 })
691 }
692
693 /// Accepts "2024-06-23T14:30:15.123456789Z" and "2024-06-23T14:30:15+05:00".
694 pub fn parse_iso(input: &str) -> Outcome<Self> {
695 // Use existing parser infrastructure
696 crate::parser::Parser::parse_datetime(input, CalClockZone::utc())
697 }
698
699 /// Identical to `to_nanos`; `to_millis` is already reckoned from the Unix epoch.
700 pub fn to_nanos_since_epoch(&self) -> Outcome<i64> {
701 self.to_nanos()
702 }
703
704 /// Identical to `from_nanos`; both read the count from the Unix epoch.
705 pub fn from_nanos_since_epoch(nanos: i64, zone: CalClockZone) -> Outcome<Self> {
706 Self::from_nanos(nanos, zone)
707 }
708
709 // ========================================================================
710 // ADDITIONAL UTILITY METHODS FOR 100% JAVA COMPATIBILITY
711 // ========================================================================
712
713 pub fn plus_all_components(
714 &self,
715 inc_year: i32,
716 inc_month: i32,
717 inc_day: i32,
718 inc_hour: i64,
719 inc_minute: i64,
720 inc_second: i64,
721 inc_nanosecond: i64,
722 ) -> Outcome<Self> {
723 // Start with date arithmetic (years, months, days)
724 let mut result_date = res!(self.date.plus(inc_year, inc_month, inc_day));
725
726 // Handle time components with proper carry
727 let total_nanos = (self.time.nanosecond().of() as i64) + inc_nanosecond;
728 let total_seconds = self.time.second().of() as i64 + inc_second + (total_nanos / 1_000_000_000);
729 let remaining_nanos = (total_nanos % 1_000_000_000) as u32;
730
731 let total_minutes = self.time.minute().of() as i64 + inc_minute + (total_seconds / 60);
732 let remaining_seconds = (total_seconds % 60) as u8;
733
734 let total_hours = self.time.hour().of() as i64 + inc_hour + (total_minutes / 60);
735 let remaining_minutes = (total_minutes % 60) as u8;
736
737 let day_overflow = total_hours / 24;
738 let remaining_hours = (total_hours % 24) as u8;
739
740 // Apply day overflow to date
741 if day_overflow != 0 {
742 result_date = res!(result_date.add_days(day_overflow as i32));
743 }
744
745 // Create new time with remaining components
746 let result_time = res!(ClockTime::new(
747 remaining_hours,
748 remaining_minutes,
749 remaining_seconds,
750 remaining_nanos,
751 self.zone().clone()
752 ));
753
754 Ok(Self {
755 date: result_date,
756 time: result_time,
757 })
758 }
759
760 pub fn format(&self, pattern: &str) -> Outcome<String> {
761 // Create a basic formatter for now - can be enhanced later
762 match pattern {
763 "ISO" | "iso" => Ok(format!("{}T{}", self.date, self.time)),
764 "DEBUG" | "debug" => Ok(self.to_debug()),
765 _ => {
766 // For custom patterns, use a simple implementation for now
767 Ok(format!("{} {}", self.date, self.time))
768 }
769 }
770 }
771
772 pub fn to_debug(&self) -> String {
773 format!(
774 "CalClock[{}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {}]",
775 self.date.year(),
776 self.date.month_of_year().of(),
777 self.date.day(),
778 self.time.hour().of(),
779 self.time.minute().of(),
780 self.time.second().of(),
781 self.time.nanosecond(),
782 self.zone().id()
783 )
784 }
785
786 pub fn previous_day_of_week(&self, dow: DayOfWeek) -> Outcome<Self> {
787 let current_dow = self.date.day_of_week();
788 let days_back = match current_dow.days_until(&dow) {
789 0 => 7, // Same day, go back a full week
790 n => n,
791 };
792 self.add_days(-(days_back as i32))
793 }
794
795 pub fn next_day_of_week(&self, dow: DayOfWeek) -> Outcome<Self> {
796 let current_dow = self.date.day_of_week();
797 let days_forward = match dow.days_until(&current_dow) {
798 0 => 7, // Same day, go forward a full week
799 n => n,
800 };
801 self.add_days(days_forward as i32)
802 }
803
804 pub fn this_or_previous_day_of_week(&self, dow: DayOfWeek) -> Outcome<Self> {
805 let current_dow = self.date.day_of_week();
806 if current_dow == dow {
807 Ok(self.clone())
808 } else {
809 self.previous_day_of_week(dow)
810 }
811 }
812
813 pub fn this_or_next_day_of_week(&self, dow: DayOfWeek) -> Outcome<Self> {
814 let current_dow = self.date.day_of_week();
815 if current_dow == dow {
816 Ok(self.clone())
817 } else {
818 self.next_day_of_week(dow)
819 }
820 }
821
822 pub fn abs_diff(&self, other: &Self) -> Outcome<CalClockDuration> {
823 let diff = res!(self.duration_until(other));
824 if diff.nanoseconds() < 0 {
825 Ok(diff.negate())
826 } else {
827 Ok(diff)
828 }
829 }
830
831 pub fn to_midnight(&self) -> Outcome<Self> {
832 let next_day = res!(self.date.add_days(1));
833 let midnight_time = res!(ClockTime::new(0, 0, 0, 0, self.zone().clone()));
834 Ok(Self {
835 date: next_day,
836 time: midnight_time,
837 })
838 }
839
840 pub fn is_within_seconds(&self, other: &Self, tolerance_seconds: f64) -> Outcome<bool> {
841 let diff = res!(self.abs_diff(other));
842 let diff_seconds = diff.total_seconds() as f64 + (diff.nanoseconds() as f64 / 1_000_000_000.0);
843 Ok(diff_seconds <= tolerance_seconds)
844 }
845
846 pub fn round_to_millis(&self) -> Outcome<Self> {
847 let millis = (self.time.nanosecond().of() + 500_000) / 1_000_000; // Round to nearest ms
848 let rounded_nanos = millis * 1_000_000;
849
850 let new_time = res!(ClockTime::new(
851 self.time.hour().of(),
852 self.time.minute().of(),
853 self.time.second().of(),
854 rounded_nanos,
855 self.zone().clone()
856 ));
857
858 Ok(Self {
859 date: self.date.clone(),
860 time: new_time,
861 })
862 }
863
864 pub fn zero_nanoseconds(&self) -> Outcome<Self> {
865 let new_time = res!(ClockTime::new(
866 self.time.hour().of(),
867 self.time.minute().of(),
868 self.time.second().of(),
869 0,
870 self.zone().clone()
871 ));
872
873 Ok(Self {
874 date: self.date.clone(),
875 time: new_time,
876 })
877 }
878
879 /// Converts to Java time as if the timezone is UTC.
880 pub fn to_java_time_as_utc(&self) -> Outcome<i64> {
881 let utc_clock = res!(self.to_utc_zone());
882 utc_clock.to_java_timestamp()
883 }
884
885 pub fn as_zone(&self, zone: CalClockZone) -> Outcome<Self> {
886 let new_date = res!(CalendarDate::new(
887 self.date.year(),
888 self.date.month_of_year().of(),
889 self.date.day(),
890 zone.clone()
891 ));
892
893 let new_time = res!(ClockTime::new(
894 self.time.hour().of(),
895 self.time.minute().of(),
896 self.time.second().of(),
897 self.time.nanosecond().of(),
898 zone
899 ));
900
901 Ok(Self {
902 date: new_date,
903 time: new_time,
904 })
905 }
906
907 pub fn inc_duration(&self, duration: &CalClockDuration) -> Outcome<Self> {
908 self.add_duration(duration)
909 }
910
911 pub fn inc_days(&self, days: i32) -> Outcome<Self> {
912 self.add_days(days)
913 }
914
915 pub fn is_recognized_format_char(c: char) -> bool {
916 match c {
917 'Y' | 'y' | 'M' | 'D' | 'd' | 'H' | 'h' | 'm' | 's' | 'S' | 'Z' | 'z' => true,
918 _ => false,
919 }
920 }
921
922 pub fn unix_epoch() -> Outcome<Self> {
923 Self::new(1970, 1, 1, 0, 0, 0, 0, CalClockZone::utc())
924 }
925
926 pub fn to_utc_zone(&self) -> Outcome<Self> {
927 self.as_zone(CalClockZone::utc())
928 }
929
930 pub fn from_unix_timestamp_seconds(unix_seconds: i64, zone: CalClockZone) -> Outcome<Self> {
931 let epoch = res!(Self::new(1970, 1, 1, 0, 0, 0, 0, CalClockZone::utc()));
932 let duration = CalClockDuration::from_seconds(unix_seconds);
933 res!(epoch.add_duration(&duration)).as_zone(zone)
934 }
935
936 pub fn to_java_timestamp(&self) -> Outcome<i64> {
937 self.to_millis()
938 }
939}
940
941impl Ord for CalClock {
942 fn cmp(&self, other: &Self) -> Ordering {
943 self.partial_cmp(other).unwrap_or(Ordering::Equal)
944 }
945}
946
947impl Eq for CalClock {}
948
949impl fmt::Display for CalClock {
950 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951 write!(f, "{} {}", self.date, self.time)
952 }
953}
954
955#[cfg(test)]
956mod add_duration_tests {
957 use super::*;
958
959 /// A Unix timestamp past its first day keeps its days.
960 ///
961 /// The regression this pins: `from_unix_timestamp_seconds` builds the epoch and adds the seconds as
962 /// a duration whose day field is zero and whose nanoseconds hold the lot. When the date part read
963 /// only the day field, every whole day was dropped and the result was the epoch's date with the
964 /// timestamp's time-of-day -- so 2026-07-18T10:00:00Z came back as 1970-01-01T10:00:00Z.
965 #[test]
966 fn test_a_unix_timestamp_keeps_its_days_00() -> Outcome<()> {
967 // 2026-07-18T10:00:00Z.
968 let secs = 1_784_368_800_i64;
969 let cc = res!(CalClock::from_unix_timestamp_seconds(secs, CalClockZone::utc()));
970 assert_eq!(cc.year(), 2026);
971 assert_eq!(cc.month(), 7);
972 assert_eq!(cc.day(), 18);
973 assert_eq!(cc.hour(), 10);
974 assert_eq!(cc.minute(), 0);
975 assert_eq!(cc.second(), 0);
976 // And it round-trips back to the second it came from.
977 assert_eq!(res!(cc.to_seconds()), secs);
978 Ok(())
979 }
980
981 /// The epoch itself is still the epoch.
982 #[test]
983 fn test_the_epoch_is_the_epoch_01() -> Outcome<()> {
984 let cc = res!(CalClock::from_unix_timestamp_seconds(0, CalClockZone::utc()));
985 assert_eq!((cc.year(), cc.month(), cc.day()), (1970, 1, 1));
986 assert_eq!((cc.hour(), cc.minute(), cc.second()), (0, 0, 0));
987 Ok(())
988 }
989
990 /// A duration that carries the held time past midnight advances the date, and one that carries it
991 /// back before midnight moves the date back. This is the carry the old split-and-add lost even when
992 /// the day field was right.
993 #[test]
994 fn test_a_duration_carries_the_date_across_midnight_02() -> Outcome<()> {
995 let z = CalClockZone::utc();
996 // 2026-07-18T18:00:00 + 12h = 2026-07-19T06:00:00.
997 let base = res!(CalClock::new(2026, 7, 18, 18, 0, 0, 0, z.clone()));
998 let fwd = res!(base.add_duration(&CalClockDuration::from_seconds(12 * 3600)));
999 assert_eq!((fwd.year(), fwd.month(), fwd.day(), fwd.hour()), (2026, 7, 19, 6));
1000 // And back again.
1001 let back = res!(fwd.subtract_duration(&CalClockDuration::from_seconds(12 * 3600)));
1002 assert_eq!((back.year(), back.month(), back.day(), back.hour()), (2026, 7, 18, 18));
1003 Ok(())
1004 }
1005}