Oregami
Repositories/oxedyne/fe2o3

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

16.5 KiB, 125 runs

created by r1870400018:6385, 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::CalendarDate,
6 clock::ClockTime,
7 time::{CalClock, CalClockZone},
8};
9
10use oxedyne_fe2o3_core::prelude::*;
11
12use std::sync::Mutex;
13
14/// Converts between Unix milliseconds and CalClock. A timestamp near the last
15/// one converted is answered from a cached reference point rather than from the
16/// epoch, which is what makes a sequence cheap. The reference is behind a mutex,
17/// so one converter serves several threads.
18///
19/// ```ignore
20/// use oxedyne_fe2o3_datime::time::{CalClockConverter, CalClockZone}res!();
21///
22/// let zone = res!(CalClockZone::new("America/New_York"))res!();
23/// let mut converter = CalClockConverter::new(zone)res!();
24///
25/// // Convert single timestamp
26/// let calclock = res!(converter.unix_to_calclock(1640995200000))res!(); // 2022-01-01 UTC
27///
28/// // Convert back to Unix timestamp
29/// let unix_millis = res!(converter.calclock_to_unix(&calclock))res!();
30///
31/// // Optimized batch conversion
32/// let timestamps = vec![1640995200000, 1640995260000, 1640995320000]res!();
33/// let calclocks = res!(converter.convert_sequence(&timestamps))res!();
34/// ```
35#[derive(Debug)]
36pub struct CalClockConverter {
37 zone: CalClockZone,
38 reference: Mutex<ReferencePoint>,
39 max_reference_deviation: i64, // milliseconds
40 use_optimization: bool,
41}
42
43#[derive(Clone, Debug)]
44struct ReferencePoint {
45 unix_millis: Option<i64>,
46 calclock: Option<CalClock>,
47 hit_count: u64,
48 miss_count: u64,
49}
50
51impl CalClockConverter {
52 /// ```ignore
53 /// let utc_converter = CalClockConverter::new(CalClockZone::utc())res!();
54 /// let eastern_converter = CalClockConverter::new(
55 /// res!(CalClockZone::new("America/New_York"))
56 /// )res!();
57 /// ```
58 pub fn new(zone: CalClockZone) -> Self {
59 Self {
60 zone,
61 reference: Mutex::new(ReferencePoint::new()),
62 max_reference_deviation: 24 * 60 * 60 * 1000, // 24 hours
63 use_optimization: true,
64 }
65 }
66
67 /// Seeds the reference point, so the first conversions are already cheap.
68 ///
69 /// ```ignore
70 /// let now = 1640995200000res!(); // Known approximate timestamp
71 /// let converter = CalClockConverter::with_reference(
72 /// CalClockZone::utc(),
73 /// now
74 /// ))res!();
75 /// ```
76 pub fn with_reference(zone: CalClockZone, reference_unix_millis: i64) -> Outcome<Self> {
77 let converter = Self::new(zone);
78 res!(converter.set_reference_point(reference_unix_millis));
79 Ok(converter)
80 }
81
82 /// ```ignore
83 /// let converter = CalClockConverter::new(CalClockZone::utc())res!();
84 /// let calclock = res!(converter.unix_to_calclock(1640995200000))res!();
85 /// assert_eq!(calclock.date().year(), 2022)res!();
86 /// ```
87 pub fn unix_to_calclock(&self, unix_millis: i64) -> Outcome<CalClock> {
88 if self.use_optimization {
89 self.unix_to_calclock_optimised(unix_millis)
90 } else {
91 self.unix_to_calclock_full(unix_millis)
92 }
93 }
94
95 /// ```ignore
96 /// let calclock = res!(CalClock::new(2022, 1, 1, 0, 0, 0, 0, CalClockZone::utc()))res!();
97 /// let converter = CalClockConverter::new(CalClockZone::utc())res!();
98 /// let unix_millis = res!(converter.calclock_to_unix(&calclock))res!();
99 /// assert_eq!(unix_millis, 1640995200000)res!();
100 /// ```
101 pub fn calclock_to_unix(&self, calclock: &CalClock) -> Outcome<i64> {
102 // Convert CalClock to UTC milliseconds
103 let local_millis = res!(self.calclock_to_local_millis(calclock));
104
105 // Get timezone offset for this CalClock
106 // We need to iterate to find the correct offset since we don't know
107 // the exact UTC time yet (chicken-and-egg problem)
108 let utc_millis = res!(self.local_to_utc_millis(local_millis, calclock));
109
110 Ok(utc_millis)
111 }
112
113 /// The reference point moves with the sequence, so a run of nearby
114 /// timestamps costs far less than the same timestamps converted singly.
115 ///
116 /// ```ignore
117 /// let timestamps = vec![1640995200000, 1640995260000, 1640995320000]res!();
118 /// let converter = CalClockConverter::new(CalClockZone::utc())res!();
119 /// let calclocks = res!(converter.convert_sequence(&timestamps))res!();
120 /// assert_eq!(calclocks.len(), 3)res!();
121 /// ```
122 pub fn convert_sequence(&self, unix_timestamps: &[i64]) -> Outcome<Vec<CalClock>> {
123 let mut results = Vec::with_capacity(unix_timestamps.len());
124
125 // Process timestamps, updating reference point as needed
126 for &timestamp in unix_timestamps {
127 let calclock = res!(self.unix_to_calclock(timestamp));
128 results.push(calclock);
129 }
130
131 Ok(results)
132 }
133
134 /// Hits, misses, and the hit rate.
135 pub fn reference_stats(&self) -> (u64, u64, f64) {
136 let reference = match self.reference.lock() {
137 Ok(guard) => guard,
138 Err(_) => {
139 // Return default stats if mutex is poisoned
140 return (0, 0, 0.0);
141 }
142 };
143 let total = reference.hit_count + reference.miss_count;
144 let hit_ratio = if total > 0 {
145 reference.hit_count as f64 / total as f64 * 100.0
146 } else {
147 0.0
148 };
149 (reference.hit_count, reference.miss_count, hit_ratio)
150 }
151
152 pub fn reset_reference(&self) {
153 let mut reference = match self.reference.lock() {
154 Ok(guard) => guard,
155 Err(_) => {
156 // If mutex is poisoned, we can't reset reference
157 eprintln!("Warning: Could not reset reference due to poisoned mutex");
158 return;
159 }
160 };
161 *reference = ReferencePoint::new();
162 }
163
164 pub fn set_optimization(&mut self, enabled: bool) {
165 self.use_optimization = enabled;
166 }
167
168 pub fn set_max_reference_deviation(&mut self, deviation_millis: i64) {
169 self.max_reference_deviation = deviation_millis;
170 }
171
172 pub fn zone(&self) -> &CalClockZone {
173 &self.zone
174 }
175
176 fn unix_to_calclock_optimised(&self, unix_millis: i64) -> Outcome<CalClock> {
177 // Check if we can use reference point optimisation
178 let optimization_data = {
179 let reference = lock_mutex!(self.reference);
180 match (reference.unix_millis, reference.calclock.as_ref()) {
181 (Some(ref_millis), Some(ref_calclock)) => {
182 let deviation = (unix_millis - ref_millis).abs();
183 if deviation <= self.max_reference_deviation {
184 Some((ref_calclock.clone(), unix_millis - ref_millis))
185 } else {
186 None
187 }
188 },
189 _ => None,
190 }
191 };
192
193 if let Some((ref_calclock, offset)) = optimization_data {
194 // Fast path: calculate offset from reference
195 {
196 let mut reference = lock_mutex!(self.reference);
197 reference.hit_count += 1;
198 }
199 return self.calculate_from_reference(&ref_calclock, offset);
200 }
201
202 // Slow path: full conversion + update reference
203 {
204 let mut reference = lock_mutex!(self.reference);
205 reference.miss_count += 1;
206 }
207
208 let calclock = res!(self.unix_to_calclock_full(unix_millis));
209 res!(self.update_reference_point(unix_millis, &calclock));
210
211 Ok(calclock)
212 }
213
214 fn unix_to_calclock_full(&self, unix_millis: i64) -> Outcome<CalClock> {
215 // 1. Get timezone offset for this timestamp
216 let zone_offset_millis = res!(self.zone.offset_millis_at_time(unix_millis));
217 let local_millis = unix_millis + zone_offset_millis as i64;
218
219 // 2. Convert to date and time components
220 let (year, month, day, hour, minute, second, nanos) =
221 res!(self.millis_to_components(local_millis));
222
223 // 3. Create CalClock instance
224 CalClock::new(year, month, day, hour, minute, second, nanos, self.zone.clone())
225 }
226
227 fn calculate_from_reference(&self, reference: &CalClock, offset_millis: i64) -> Outcome<CalClock> {
228 // Convert offset to duration and add to reference CalClock
229 // This is much faster than full conversion for small offsets
230
231 if offset_millis == 0 {
232 return Ok(reference.clone());
233 }
234
235 // For small offsets, we can do fast arithmetic
236 if offset_millis.abs() < 60 * 60 * 1000 { // Less than 1 hour
237 return self.add_millis_fast(reference, offset_millis);
238 }
239
240 // For larger offsets, use full arithmetic
241 self.add_millis_full(reference, offset_millis)
242 }
243
244 /// Fast millisecond addition for small offsets (< 1 hour).
245 fn add_millis_fast(&self, base: &CalClock, offset_millis: i64) -> Outcome<CalClock> {
246 // Fast path for small time additions that don't cross day boundaries
247 // Calculate total nanoseconds in the day
248 let base_nanos = base.time().hour().of() as i64 * 3600 * 1_000_000_000 +
249 base.time().minute().of() as i64 * 60 * 1_000_000_000 +
250 base.time().second().of() as i64 * 1_000_000_000 +
251 base.time().nanosecond().of() as i64;
252 let total_nanos = base_nanos + offset_millis * 1_000_000;
253
254 if total_nanos >= 0 && total_nanos < 24 * 60 * 60 * 1_000_000_000 {
255 // Still within the same day
256 let (hour, minute, second, nanos) = res!(self.nanos_to_time_components(total_nanos as u64));
257 return CalClock::from_date_time(
258 base.date().clone(),
259 res!(ClockTime::new(hour, minute, second, nanos, self.zone.clone()))
260 );
261 }
262
263 // Crosses day boundary, use full arithmetic
264 self.add_millis_full(base, offset_millis)
265 }
266
267 fn add_millis_full(&self, base: &CalClock, offset_millis: i64) -> Outcome<CalClock> {
268 // Convert to total milliseconds since a reference epoch
269 let base_millis = res!(self.calclock_to_local_millis(base));
270 let result_millis = base_millis + offset_millis;
271
272 // Convert back to CalClock components
273 let (year, month, day, hour, minute, second, nanos) =
274 res!(self.millis_to_components(result_millis));
275
276 CalClock::new(year, month, day, hour, minute, second, nanos, self.zone.clone())
277 }
278
279 fn set_reference_point(&self, unix_millis: i64) -> Outcome<()> {
280 let calclock = res!(self.unix_to_calclock_full(unix_millis));
281 res!(self.update_reference_point(unix_millis, &calclock));
282 Ok(())
283 }
284
285 fn update_reference_point(&self, unix_millis: i64, calclock: &CalClock) -> Outcome<()> {
286 let mut reference = lock_mutex!(self.reference);
287 reference.unix_millis = Some(unix_millis);
288 reference.calclock = Some(calclock.clone());
289 Ok(())
290 }
291
292 fn millis_to_components(&self, millis: i64) -> Outcome<(i32, u8, u8, u8, u8, u8, u32)> {
293 // Convert milliseconds to days since epoch
294 let days_since_epoch = millis / (24 * 60 * 60 * 1000);
295 let millis_in_day = millis % (24 * 60 * 60 * 1000);
296
297 // Convert days to calendar date (using Julian day number algorithm)
298 let (year, month, day) = res!(self.days_to_date(days_since_epoch as i32));
299
300 // Convert milliseconds in day to time components
301 let (hour, minute, second, nanos) = res!(self.millis_to_time_components(millis_in_day));
302
303 Ok((year, month, day, hour, minute, second, nanos))
304 }
305
306 fn days_to_date(&self, days_since_epoch: i32) -> Outcome<(i32, u8, u8)> {
307 // Use CalendarDate's proper from_days_since_epoch method
308 // which uses Julian day arithmetic for accurate calculation
309 let date = res!(CalendarDate::from_days_since_epoch(days_since_epoch as i64, self.zone.clone()));
310 Ok((date.year(), date.month(), date.day()))
311 }
312
313 fn millis_to_time_components(&self, millis_in_day: i64) -> Outcome<(u8, u8, u8, u32)> {
314 if millis_in_day < 0 || millis_in_day >= 24 * 60 * 60 * 1000 {
315 return Err(err!("Milliseconds in day out of range: {}", millis_in_day; Invalid, Input));
316 }
317
318 let total_seconds = millis_in_day / 1000;
319 let millis_remainder = millis_in_day % 1000;
320
321 let hour = (total_seconds / 3600) as u8;
322 let minute = ((total_seconds % 3600) / 60) as u8;
323 let second = (total_seconds % 60) as u8;
324 let nanos = (millis_remainder * 1_000_000) as u32;
325
326 Ok((hour, minute, second, nanos))
327 }
328
329 fn nanos_to_time_components(&self, nanos_in_day: u64) -> Outcome<(u8, u8, u8, u32)> {
330 const NANOS_PER_DAY: u64 = 24 * 60 * 60 * 1_000_000_000;
331
332 if nanos_in_day >= NANOS_PER_DAY {
333 return Err(err!("Nanoseconds in day out of range: {}", nanos_in_day; Invalid, Input));
334 }
335
336 let total_seconds = nanos_in_day / 1_000_000_000;
337 let nanos_remainder = nanos_in_day % 1_000_000_000;
338
339 let hour = (total_seconds / 3600) as u8;
340 let minute = ((total_seconds % 3600) / 60) as u8;
341 let second = (total_seconds % 60) as u8;
342 let nanos = nanos_remainder as u32;
343
344 Ok((hour, minute, second, nanos))
345 }
346
347 fn calclock_to_local_millis(&self, calclock: &CalClock) -> Outcome<i64> {
348 // Convert date to days since epoch
349 let days = res!(self.date_to_days(calclock.date()));
350 let millis_from_days = days as i64 * 24 * 60 * 60 * 1000;
351
352 // Add time component
353 // Calculate total nanoseconds in the day and convert to milliseconds
354 let time_nanos = calclock.time().hour().of() as i64 * 3600 * 1_000_000_000 +
355 calclock.time().minute().of() as i64 * 60 * 1_000_000_000 +
356 calclock.time().second().of() as i64 * 1_000_000_000 +
357 calclock.time().nanosecond().of() as i64;
358 let time_millis = time_nanos / 1_000_000;
359
360 Ok(millis_from_days + time_millis)
361 }
362
363 fn local_to_utc_millis(&self, local_millis: i64, _calclock: &CalClock) -> Outcome<i64> {
364 // This is tricky because we need the UTC time to get the timezone offset,
365 // but we need the timezone offset to get the UTC time.
366 // We'll use an iterative approach to resolve this.
367
368 // Start with assumption that timezone offset is current raw offset
369 let mut utc_estimate = local_millis - self.zone.raw_offset_millis() as i64;
370
371 // Iterate to find correct offset (handles DST transitions)
372 for _ in 0..3 { // Usually converges in 1-2 iterations
373 let actual_offset = res!(self.zone.offset_millis_at_time(utc_estimate));
374 let new_utc_estimate = local_millis - actual_offset as i64;
375
376 if (new_utc_estimate - utc_estimate).abs() < 1000 { // Within 1 second
377 return Ok(new_utc_estimate);
378 }
379
380 utc_estimate = new_utc_estimate;
381 }
382
383 Ok(utc_estimate)
384 }
385
386 fn date_to_days(&self, date: &CalendarDate) -> Outcome<i32> {
387 // Use the CalendarDate's proper days_since_epoch method
388 // which uses Julian day arithmetic for accurate calculation
389 let days = res!(date.days_since_epoch());
390 Ok(days as i32)
391 }
392}
393
394impl ReferencePoint {
395 fn new() -> Self {
396 Self {
397 unix_millis: None,
398 calclock: None,
399 hit_count: 0,
400 miss_count: 0,
401 }
402 }
403}
404
405impl Default for CalClockConverter {
406 fn default() -> Self {
407 Self::new(CalClockZone::utc())
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_basic_conversion() -> Outcome<()> {
417 let converter = CalClockConverter::new(CalClockZone::utc());
418
419 // Test known timestamp: 2022-01-01 00:00:00 UTC
420 let unix_millis = 1640995200000;
421 let calclock = res!(converter.unix_to_calclock(unix_millis));
422
423 assert_eq!(calclock.date().year(), 2022);
424 assert_eq!(calclock.date().month(), 1);
425 assert_eq!(calclock.date().day(), 1);
426 assert_eq!(calclock.time().hour().of(), 0);
427 assert_eq!(calclock.time().minute().of(), 0);
428 assert_eq!(calclock.time().second().of(), 0);
429 Ok(())
430 }
431
432 #[test]
433 fn test_round_trip_conversion() -> Outcome<()> {
434 let converter = CalClockConverter::new(CalClockZone::utc());
435 let original_unix = 1640995200000;
436
437 let calclock = res!(converter.unix_to_calclock(original_unix));
438 let converted_unix = res!(converter.calclock_to_unix(&calclock));
439
440 // Should be equal within millisecond precision
441 assert!((original_unix - converted_unix).abs() < 1000);
442 Ok(())
443 }
444
445 #[test]
446 fn test_reference_point_optimization() -> Outcome<()> {
447 let converter = CalClockConverter::new(CalClockZone::utc());
448
449 // Convert multiple nearby timestamps
450 let base_time = 1640995200000;
451 for i in 0..10 {
452 let timestamp = base_time + i * 60 * 1000; // 1 minute intervals
453 let _ = res!(converter.unix_to_calclock(timestamp));
454 }
455
456 let (hits, _misses, ratio) = converter.reference_stats();
457 assert!(hits > 0, "Should have some reference point hits");
458 assert!(ratio > 0.0, "Hit ratio should be positive");
459 Ok(())
460 }
461
462 #[test]
463 fn test_batch_conversion() -> Outcome<()> {
464 let converter = CalClockConverter::new(CalClockZone::utc());
465
466 let timestamps = vec![
467 1640995200000, // 2022-01-01 00:00:00
468 1640995260000, // 2022-01-01 00:01:00
469 1640995320000, // 2022-01-01 00:02:00
470 ];
471
472 let calclocks = res!(converter.convert_sequence(&timestamps));
473 assert_eq!(calclocks.len(), 3);
474
475 // Verify first timestamp
476 assert_eq!(calclocks[0].date().year(), 2022);
477 assert_eq!(calclocks[0].time().minute().of(), 0);
478
479 // Verify second timestamp (1 minute later)
480 assert_eq!(calclocks[1].time().minute().of(), 1);
481 Ok(())
482 }
483
484 #[test]
485 fn test_timezone_conversion() -> Outcome<()> {
486 let eastern = res!(CalClockZone::new("America/New_York"));
487 let converter = CalClockConverter::new(eastern);
488
489 // Test conversion with timezone offset
490 let unix_millis = 1640995200000; // 2022-01-01 00:00:00 UTC
491 let calclock = res!(converter.unix_to_calclock(unix_millis));
492
493 // Verify that timezone offset is applied correctly
494 assert_eq!(calclock.time().hour().of(), 19); // Should be 19:00 (UTC-5)
495
496 // Verify that timezone offset is being applied
497 let utc_converter = CalClockConverter::new(CalClockZone::utc());
498 let utc_calclock = res!(utc_converter.unix_to_calclock(unix_millis));
499 assert_ne!(calclock.time().hour().of(), utc_calclock.time().hour().of());
500 Ok(())
501 }
502}