Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/tests/time.rs

13.3 KiB, 11 runs

created by r1870400018:6421, 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 oxedyne_fe2o3_core::{
5 prelude::*,
6 test::test_it,
7};
8use oxedyne_fe2o3_datime::{
9 calendar::CalendarDate,
10 clock::ClockTime,
11 core::Duration,
12 time::{
13 CalClock,
14 CalClockConverter,
15 CalClockDuration,
16 CalClockZone,
17 },
18};
19
20pub fn test_time(filter: &str) -> Outcome<()> {
21
22 res!(test_it(filter, &["calclock_creation", "all", "time", "calclock"], || {
23 let zone = res!(CalClockZone::new("UTC"));
24
25 // Create from components
26 let cc1 = res!(CalClock::new(2024, 3, 15, 14, 30, 0, 0, zone.clone()));
27 assert_eq!(cc1.date().year(), 2024);
28 assert_eq!(cc1.date().month(), 3);
29 assert_eq!(cc1.date().day(), 15);
30 assert_eq!(cc1.time().hour().of(), 14);
31 assert_eq!(cc1.time().minute().of(), 30);
32
33 // Create from date and time
34 let date = res!(CalendarDate::new(2024, 3, 15, zone.clone()));
35 let time = res!(ClockTime::new(14, 30, 0, 0, zone.clone()));
36 let cc2 = res!(CalClock::from_date_time(date, time));
37 assert_eq!(cc1, cc2);
38 Ok(())
39 }));
40
41 res!(test_it(filter, &["timezone_support", "all", "time", "timezone"], || {
42 // Test UTC
43 let utc = res!(CalClockZone::new("UTC"));
44 assert_eq!(utc.id(), "UTC");
45 assert_eq!(res!(utc.offset_millis_at_time(0)), 0);
46
47 // Test fixed offset (without colon - parser expects HHMM format)
48 let plus5 = res!(CalClockZone::new("+0500"));
49 assert_eq!(res!(plus5.offset_millis_at_time(0)), 5 * 60 * 60 * 1000);
50
51 let minus8 = res!(CalClockZone::new("-0800"));
52 assert_eq!(res!(minus8.offset_millis_at_time(0)), -8 * 60 * 60 * 1000);
53
54 // Test named timezones
55 let nyc = res!(CalClockZone::new("America/New_York"));
56 assert_eq!(nyc.id(), "America/New_York");
57 // Offset varies with DST
58 Ok(())
59 }));
60
61 res!(test_it(filter, &["calclock_arithmetic", "all", "time", "calclock", "arithmetic"], || {
62 let zone = res!(CalClockZone::new("UTC"));
63 let cc1 = res!(CalClock::new(2024, 3, 15, 14, 30, 0, 0, zone));
64
65 // Add duration - 1 hour only to avoid day rollover complexity
66 let dur = CalClockDuration::from_hours(1);
67 let cc2 = res!(cc1.add_duration(&dur));
68 assert_eq!(cc2.date().day(), 15); // Same day
69 assert_eq!(cc2.time().hour().of(), 15); // 14 + 1 = 15
70
71 // Add days
72 let cc3 = res!(cc1.add_days(10));
73 assert_eq!(cc3.date().day(), 25);
74
75 // Add months
76 let cc4 = res!(cc1.add_months(1));
77 assert_eq!(cc4.date().month(), 4);
78
79 // Duration between
80 let duration = res!(cc1.duration_until(&cc2));
81 assert_eq!(res!(duration.to_hours()), 1);
82 Ok(())
83 }));
84
85 res!(test_it(filter, &["converter_basic", "all", "time", "converter"], || {
86 let zone = res!(CalClockZone::new("UTC"));
87 let converter = CalClockConverter::new(zone.clone());
88
89 // Just test that conversion produces a number
90 let cc = res!(CalClock::new(2024, 3, 15, 14, 30, 0, 0, zone.clone()));
91 let millis = res!(converter.calclock_to_unix(&cc));
92
93 // Should be a reasonable Unix timestamp (after year 2000)
94 assert!(millis > 946684800000); // Jan 1, 2000 in millis
95 assert!(millis < 4102444800000); // Jan 1, 2100 in millis
96 Ok(())
97 }));
98
99 res!(test_it(filter, &["converter_optimization", "all", "time", "converter", "optimization"], || {
100 let zone = res!(CalClockZone::new("UTC"));
101 let mut converter = CalClockConverter::new(zone.clone());
102 converter.set_max_reference_deviation(24 * 60 * 60 * 1000); // 1 day
103
104 // Just test that we can convert multiple times
105 let cc1 = res!(CalClock::new(2024, 3, 15, 10, 0, 0, 0, zone.clone()));
106 let cc2 = res!(CalClock::new(2024, 3, 15, 11, 0, 0, 0, zone.clone()));
107
108 let millis1 = res!(converter.calclock_to_unix(&cc1));
109 let millis2 = res!(converter.calclock_to_unix(&cc2));
110
111 // Second time should be 1 hour later
112 assert_eq!(millis2 - millis1, 60 * 60 * 1000); // 1 hour in millis
113 Ok(())
114 }));
115
116 res!(test_it(filter, &["calclock_comparison", "all", "time", "calclock", "comparison"], || {
117 let zone = res!(CalClockZone::new("UTC"));
118
119 let cc1 = res!(CalClock::new(2024, 3, 15, 14, 30, 0, 0, zone.clone()));
120 let cc2 = res!(CalClock::new(2024, 3, 15, 14, 30, 0, 1, zone.clone()));
121 let cc3 = res!(CalClock::new(2024, 3, 15, 14, 30, 0, 0, zone.clone()));
122
123 assert!(cc1 < cc2);
124 assert!(cc2 > cc1);
125 assert_eq!(cc1, cc3);
126
127 assert!(cc1.is_before(&cc2));
128 assert!(cc2.is_after(&cc1));
129 assert!(!cc1.is_before(&cc3));
130 Ok(())
131 }));
132
133 res!(test_it(filter, &["calclock_formatting", "all", "time", "calclock", "format"], || {
134 let zone = res!(CalClockZone::new("UTC"));
135 let cc = res!(CalClock::new(2024, 3, 15, 14, 30, 45, 123_456_789, zone));
136
137 // ISO format
138 let iso = res!(cc.to_iso8601());
139 assert!(iso.contains("2024-03-15"));
140 assert!(iso.contains("14:30:45"));
141
142 // String representation
143 let s = cc.to_string();
144 assert!(s.contains("2024"));
145 assert!(s.contains("14:30"));
146 Ok(())
147 }));
148
149 res!(test_it(filter, &["duration_operations", "all", "time", "duration"], || {
150 // Test various duration creations
151 let d1 = CalClockDuration::from_seconds(90);
152 assert_eq!(res!(d1.to_seconds()), 90);
153
154 let d2 = CalClockDuration::from_minutes(5);
155 assert_eq!(res!(d2.to_minutes()), 5);
156 assert_eq!(res!(d2.to_seconds()), 300);
157
158 let d3 = CalClockDuration::from_hours(2);
159 assert_eq!(res!(d3.to_hours()), 2);
160 assert_eq!(res!(d3.to_minutes()), 120);
161
162 // Test arithmetic
163 let sum = res!(d1.add(&d2));
164 assert_eq!(res!(sum.to_seconds()), 390);
165
166 let diff = res!(d3.subtract(&d2));
167 assert_eq!(res!(diff.to_minutes()), 115);
168 Ok(())
169 }));
170
171 res!(test_it(filter, &["local_zone_oracle", "all", "time", "zone"], || {
172 // The system's own date command is the oracle. Only run where a
173 // zoneinfo tree exists; a container without one detects nothing and
174 // that is the documented fallback, not a fault.
175 if !std::path::Path::new("/usr/share/zoneinfo").is_dir() {
176 return Ok(());
177 }
178 let now_ms = res!(std::time::SystemTime::now()
179 .duration_since(std::time::UNIX_EPOCH)
180 .map_err(|e| err!("{}", e; System))).as_millis() as i64;
181
182 // local() must agree with `date +%z` on the current offset.
183 let out = res!(std::process::Command::new("date").arg("+%z").output(),
184 System);
185 let text = String::from_utf8_lossy(&out.stdout);
186 let want = res!(parse_offset_minutes(text.trim()));
187 let zone = CalClockZone::local();
188 let got = i64::from(res!(zone.offset_millis_at_time(now_ms))) / 60_000;
189 assert_eq!(got, want,
190 "local() answers {} minutes, the date command says {}.", got, want);
191
192 // A named zone absent from the embedded table must resolve through
193 // the system tree rather than silently answering zero.
194 for name in ["Australia/Perth", "Australia/Sydney"] {
195 if !std::path::Path::new("/usr/share/zoneinfo").join(name).is_file() {
196 continue;
197 }
198 let out = res!(std::process::Command::new("date")
199 .env("TZ", name)
200 .arg("+%z")
201 .output(), System);
202 let text = String::from_utf8_lossy(&out.stdout);
203 let want = res!(parse_offset_minutes(text.trim()));
204 let zone = res!(CalClockZone::new(name));
205 let got = i64::from(res!(zone.offset_millis_at_time(now_ms))) / 60_000;
206 assert_eq!(got, want,
207 "{} answers {} minutes, the date command says {}.",
208 name, got, want);
209 }
210 Ok(())
211 }));
212
213 res!(test_it(filter, &["calclock_to_nanos_monotonic", "all", "time", "calclock"], || {
214 // `to_millis` already truncates the sub-second field to whole
215 // milliseconds, so the old `to_nanos` added those milliseconds a second
216 // time. The overcount grows with the sub-second field and resets when
217 // the second rolls over, which makes the sequence run backwards at every
218 // second boundary. Ordering is built on this number, so `is_before`,
219 // `is_after` and `PartialOrd` all answered wrongly there.
220 let zone = res!(CalClockZone::new("UTC"));
221 let steps = [
222 (15u8, 999_999_998u32),
223 (15, 999_999_999),
224 (16, 0),
225 (16, 1),
226 ];
227 let mut prev: Option<i64> = None;
228 for (sec, nanos) in steps {
229 let cc = res!(CalClock::new(2024, 3, 15, 14, 30, sec, nanos, zone.clone()));
230 let now = res!(cc.to_nanos());
231 if let Some(before) = prev {
232 assert!(now > before,
233 "14:30:{:02}.{:09} answers {} ns, which is not after the {} ns \
234 of the instant before it.", sec, nanos, now, before);
235 }
236 prev = Some(now);
237 }
238
239 // The boundary itself: one nanosecond apart, and in that order.
240 let last = res!(CalClock::new(2024, 3, 15, 14, 30, 15, 999_999_999, zone.clone()));
241 let first = res!(CalClock::new(2024, 3, 15, 14, 30, 16, 0, zone.clone()));
242 assert_eq!(res!(first.to_nanos()) - res!(last.to_nanos()), 1,
243 "The second boundary is not one nanosecond wide.");
244 assert!(last.is_before(&first), "is_before misreads the second boundary.");
245 assert!(first.is_after(&last), "is_after misreads the second boundary.");
246 assert!(last < first, "PartialOrd misreads the second boundary.");
247 let gap = res!(last.duration_until(&first));
248 assert_eq!(res!(gap.to_nanos()), 1,
249 "duration_until misreads the second boundary.");
250 Ok(())
251 }));
252
253 res!(test_it(filter, &["calclock_to_nanos_known_value", "all", "time", "calclock"], || {
254 // Derived by hand, not from the method under test.
255 //
256 // Days from 1970-01-01 to 2024-01-01: 54 years of 365 days, plus one
257 // day for each of the 13 leap years 1972, 1976, ... 2020, so
258 // 54 * 365 + 13 = 19710 + 13 = 19723 days.
259 // 2024 is a leap year, so 2024-03-15 is a further 31 + 29 + 14 = 74
260 // days on, giving 19797 days. 19797 * 86400 = 1_710_460_800 seconds,
261 // which is the published Unix time of 2024-03-15T00:00:00Z.
262 // 14:30:15 is 14*3600 + 30*60 + 15 = 52_215 seconds into the day, so
263 // the instant is 1_710_513_015 seconds after the epoch, and
264 // 1_710_513_015 * 1e9 + 123_456_789 nanoseconds.
265 const WANT: i64 = 1_710_513_015_123_456_789;
266
267 let zone = res!(CalClockZone::new("UTC"));
268 let cc = res!(CalClock::new(2024, 3, 15, 14, 30, 15, 123_456_789, zone.clone()));
269 let got = res!(cc.to_nanos());
270 assert_eq!(got, WANT, "to_nanos is out by {} ns.", got - WANT);
271 assert_eq!(res!(cc.to_nanos_since_epoch()), WANT,
272 "to_nanos_since_epoch disagrees with to_nanos.");
273 assert_eq!(res!(cc.to_millis()), 1_710_513_015_123,
274 "to_millis disagrees with the hand-worked value.");
275
276 // The last nanosecond before the epoch, where the whole thing is negative.
277 let before = res!(CalClock::new(1969, 12, 31, 23, 59, 59, 999_999_999, zone.clone()));
278 assert_eq!(res!(before.to_nanos()), -1,
279 "The nanosecond before the epoch is not -1.");
280
281 // `from_nanos` is the inverse. It used to overwrite the whole sub-second
282 // field with the sub-millisecond remainder, throwing the milliseconds away.
283 for want in [WANT, -1i64, 0i64] {
284 let back = res!(CalClock::from_nanos(want, zone.clone()));
285 let round = res!(back.to_nanos());
286 assert_eq!(round, want,
287 "{} does not survive from_nanos then to_nanos; it came back as {}.",
288 want, round);
289 }
290 let back = res!(CalClock::from_nanos_since_epoch(WANT, zone.clone()));
291 assert_eq!(back.nanosecond(), 123_456_789,
292 "from_nanos_since_epoch lost the millisecond part of the sub-second field.");
293 assert_eq!(back.second(), 15, "from_nanos_since_epoch landed on the wrong second.");
294 Ok(())
295 }));
296
297 Ok(())
298}
299
300/// Reads a `+0800`-style offset as minutes.
301fn parse_offset_minutes(text: &str) -> Outcome<i64> {
302 if text.len() < 5 {
303 return Err(err!("'{}' is not a +hhmm offset.", text; Invalid, Input));
304 }
305 let sign = match &text[..1] {
306 "+" => 1i64,
307 "-" => -1i64,
308 _ => return Err(err!("'{}' is not a +hhmm offset.", text; Invalid, Input)),
309 };
310 let hours: i64 = res!(text[1..3].parse().map_err(|_|
311 err!("'{}' is not a +hhmm offset.", text; Invalid, Input)));
312 let minutes: i64 = res!(text[3..5].parse().map_err(|_|
313 err!("'{}' is not a +hhmm offset.", text; Invalid, Input)));
314 Ok(sign * (hours * 60 + minutes))
315}