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 | |
| 4 | use crate::{ |
| 5 | calendar::{Calendar, CalendarDate}, |
| 6 | clock::{ClockDuration, ClockTime}, |
| 7 | constant::{DayOfWeek, MonthOfYear}, |
| 8 | time::{CalClockDuration, CalClockZone, LeapSecondConfig}, |
| 9 | }; |
| 10 | |
| 11 | use oxedyne_fe2o3_core::prelude::*; |
| 12 | use oxedyne_fe2o3_jdat::{prelude::*, tup2dat}; |
| 13 | |
| 14 | use 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)] |
| 35 | pub struct CalClock { |
| 36 | date: CalendarDate, |
| 37 | time: ClockTime, |
| 38 | } |
| 39 | |
| 40 | impl 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 | |
| 610 | impl 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 | |
| 626 | impl 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 | |
| 634 | impl 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 | |
| 665 | impl 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(¤t_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 | |
| 941 | impl Ord for CalClock { |
| 942 | fn cmp(&self, other: &Self) -> Ordering { |
| 943 | self.partial_cmp(other).unwrap_or(Ordering::Equal) |
| 944 | } |
| 945 | } |
| 946 | |
| 947 | impl Eq for CalClock {} |
| 948 | |
| 949 | impl 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)] |
| 956 | mod 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 | } |