khive_runtime/time_anchor.rs
1//! Calendar-date → earliest-instant anchoring (ADR-169 D1), shared by every
2//! surface that accepts a date-only value against the configured display
3//! timezone. Moved verbatim from `khive-pack-gtd`'s handlers when
4//! `brain.event_counts` gained the same date-only semantics — packs do not
5//! depend on packs, so the shared rule lives here beside the
6//! `display_timezone` config it interprets.
7
8use chrono::{DateTime, TimeZone};
9use chrono_tz::Tz;
10
11/// Half-width of the UTC window the skipped-midnight branch bisects. Real UTC
12/// offsets stay well inside this, so the window always contains the boundary
13/// the search is looking for. A date whose local midnight cannot carry the full
14/// width is still answered: the window clamps to the representable range rather
15/// than being abandoned.
16const ANCHOR_SEARCH_RADIUS_HOURS: i64 = 48;
17
18/// The `[midnight - radius, midnight + radius]` window the skipped-midnight
19/// branch searches, clamped to the representable range.
20///
21/// Checked arithmetic, because chrono's `-` and `+` PANIC on overflow and
22/// `date` reaches here from a caller. Clamped rather than failing: near the
23/// ends of the range the window is truncated, not absent, and the instants
24/// that remain in it are exactly the ones a search could return anyway.
25fn anchor_search_window(date: chrono::NaiveDate) -> (chrono::NaiveDateTime, chrono::NaiveDateTime) {
26 let midnight = date
27 .and_hms_opt(0, 0, 0)
28 .expect("00:00:00 is a valid time on every date");
29 let radius = chrono::Duration::hours(ANCHOR_SEARCH_RADIUS_HOURS);
30 (
31 midnight
32 .checked_sub_signed(radius)
33 .unwrap_or(chrono::NaiveDateTime::MIN),
34 midnight
35 .checked_add_signed(radius)
36 .unwrap_or(chrono::NaiveDateTime::MAX),
37 )
38}
39
40/// Order the local date at UTC instant `utc`, in `tz`, against `date`.
41///
42/// Total where the obvious spelling is not. `tz.from_utc_datetime(&utc)
43/// .date_naive()` PANICS when the zone's offset pushes the local wall clock
44/// outside `NaiveDateTime`'s range, and both ends are reachable once the search
45/// window is clamped: a positive-offset zone overflows at
46/// `NaiveDateTime::MAX`, a negative-offset zone underflows at `MIN`.
47///
48/// The overflow direction carries the answer. A local time past the top of the
49/// range is later than every representable date, and one below the bottom is
50/// earlier, so the sign of the offset decides the ordering without needing the
51/// date that cannot be built.
52fn local_date_cmp(
53 tz: Tz,
54 utc: chrono::NaiveDateTime,
55 date: chrono::NaiveDate,
56) -> std::cmp::Ordering {
57 use chrono::Offset;
58 let offset_seconds = tz.offset_from_utc_datetime(&utc).fix().local_minus_utc();
59 match utc.checked_add_signed(chrono::Duration::seconds(offset_seconds as i64)) {
60 Some(local) => local.date().cmp(&date),
61 None if offset_seconds > 0 => std::cmp::Ordering::Greater,
62 None => std::cmp::Ordering::Less,
63 }
64}
65
66/// Resolve a calendar date to the earliest instant that belongs to it in
67/// `tz` (ADR-169 D1). Midnight is not a total function of date and zone: on a
68/// zone's own transition date local midnight can fail to exist (the clock
69/// jumps over it) or occur twice (the clock repeats it). Both are one rule —
70/// take the least instant whose local date, in `tz`, is `date` — not two
71/// exceptions to it; neither case may be resolved by unwrapping to UTC,
72/// which would silently reintroduce the defect ADR-169 exists to remove.
73///
74/// Returns `None` only when no representable instant carries `date` as its
75/// local date in `tz` — the zone's local calendar skips the date entirely, a
76/// case ADR-169 does not name since its own examples are ordinary sub-day
77/// transitions. The function is total over every date `%Y` can parse, so a
78/// caller never needs a range check of its own.
79pub fn anchor_date_to_earliest_instant(date: chrono::NaiveDate, tz: Tz) -> Option<DateTime<Tz>> {
80 let midnight = date
81 .and_hms_opt(0, 0, 0)
82 .expect("00:00:00 is a valid time on every date");
83 match tz.from_local_datetime(&midnight) {
84 chrono::LocalResult::Single(dt) => Some(dt),
85 // Fall-back overlap: two instants map to the same wall-clock time.
86 // The earlier one is the least instant whose local date is `date`.
87 chrono::LocalResult::Ambiguous(earliest, _latest) => Some(earliest),
88 // Spring-forward gap (or a larger jump): local midnight does not
89 // exist. Solve for the boundary directly rather than projecting to
90 // it. Within the window searched below, the local date is a
91 // non-decreasing function of UTC time, so the least instant whose
92 // local date is `date` is the bisection point on the UTC axis
93 // between "still the previous date" and "already this date".
94 //
95 // That premise is SCOPED to this window on purpose: it is not true
96 // globally. Zones that crossed the international date line run the
97 // local calendar backwards at the crossing — `America/Adak` at
98 // 1867-10-19T00:31:13Z moves from local 1867-10-19 to 1867-10-18 as
99 // the offset goes from +12:13:22 to -11:46:38 — and the pinned
100 // chrono-tz table holds 107 such date-decreasing transitions. What
101 // makes the bisection sound is that none of them falls inside a
102 // window this branch searches.
103 //
104 // That is checked rather than argued, and it is checked as the
105 // CONCLUSION rather than the premise. `every_gap_date_resolves_to_the
106 // _least_instant` sweeps every zone in the pinned database over
107 // 1850-2100 and asserts directly that one nanosecond before the returned
108 // instant is a different local date — the least-instant property
109 // itself, which is what a monotonicity violation would cost. At the
110 // current pin it reports 597 zones over 91311 dates each, 4299 gap
111 // dates, 818 folds, 7 whole days a zone's calendar skips, and zero
112 // violations, in about three seconds. Those counts are printed by the
113 // test rather than asserted, because their magnitudes are the
114 // database's business and only their reaching zero would mean the
115 // sweep broke. The sweep is re-derived on a chrono-tz pin bump by the
116 // command:
117 //
118 // cargo test -p khive-runtime --lib --release -- --ignored --nocapture
119 //
120 // It is ignored by default because it is an all-zone sweep, not
121 // because it is optional. A violation costs the LEAST-instant
122 // property only; the same-date check at the end of this branch keeps
123 // a wrong DATE from being returned regardless.
124 //
125 // The earlier implementation instead read the offset in effect at
126 // noon on the previous calendar date and projected local midnight
127 // forward under it. That is correct only when the gap BEGINS at
128 // local midnight. It need not: for `America/Toronto` on 1919-03-31
129 // the clock jumped from 23:30 on the previous date to 00:30, so the
130 // first instant belonging to the date is 00:30 local (04:30Z), while
131 // the projection under the prior -05:00 offset returned 01:00 local
132 // (05:00Z) — thirty minutes late, and late in a way that still
133 // passed a same-date check. Six IANA zones share that transition.
134 //
135 // Bisect to nanosecond resolution, chrono's finest. The branch returns
136 // `hi`, the upper end of the final interval, so any coarser floor leaves
137 // `hi` strictly later than the transition whenever the boundary does not
138 // land on the search grid — an overshoot up to the floor width, which
139 // makes `hi` not the least instant. A one-second floor was not enough:
140 // the 96-hour window bisects to grid points that are dyadic fractions of
141 // its span, and a historical transition second (e.g. America/Havana's
142 // midnight spring-forward, whose UTC instant sits at 53/96 of the
143 // window) is not one of them, so the search stopped a fraction of a
144 // second past the boundary and returned a late, non-round instant.
145 chrono::LocalResult::None => {
146 let (mut lo, mut hi) = anchor_search_window(date);
147 // The window's lower bound can itself already carry `date`. At the
148 // bottom of the representable range a positive-offset zone has no
149 // representable local midnight, but later local times on that same
150 // date do exist: in `Pacific/Apia` the least representable instant
151 // is local 12:33:04 on chrono's minimum date. That instant IS the
152 // least one carrying the date, so return it rather than reporting
153 // the date as absent.
154 if local_date_cmp(tz, lo, date) == std::cmp::Ordering::Equal {
155 return Some(tz.from_utc_datetime(&lo));
156 }
157 // Guard the bisection invariant instead of assuming it. Real UTC
158 // offsets stay well inside +/-48h, so a violation here means the
159 // zone is outside anything this rule can answer for; fail closed.
160 if local_date_cmp(tz, lo, date) == std::cmp::Ordering::Greater
161 || local_date_cmp(tz, hi, date) == std::cmp::Ordering::Less
162 {
163 return None;
164 }
165 while hi - lo > chrono::Duration::nanoseconds(1) {
166 let mid = lo + (hi - lo) / 2;
167 if local_date_cmp(tz, mid, date) == std::cmp::Ordering::Less {
168 lo = mid;
169 } else {
170 hi = mid;
171 }
172 }
173 // `hi` is now the least instant whose local date is >= `date`.
174 // It is > `date` exactly when the zone's local calendar skips
175 // the date entirely, which is the documented `None` case.
176 (local_date_cmp(tz, hi, date) == std::cmp::Ordering::Equal)
177 .then(|| tz.from_utc_datetime(&hi))
178 }
179 }
180}
181
182#[cfg(test)]
183mod tz_database_audit {
184 use super::*;
185
186 /// Search a window around `base` at `step` granularity for any instant whose local date is
187 /// `target`, returning the first hit.
188 ///
189 /// Generic over the date lookup rather than over `TimeZone`, and that is the whole point. The
190 /// only thing `step` decides is whether a local-date interval SHORTER THAN THE STEP can hide
191 /// between two probes, and the pinned `chrono-tz` table contains no such interval — so the real
192 /// database cannot exercise the one property this function's granularity governs. Taking the
193 /// lookup as a closure lets a fabricated calendar do it instead, which is why
194 /// `a_sub_step_interval_is_missed_by_a_coarse_probe` below can go red against a coarse step and
195 /// green against a fine one without touching the pinned data or the production signature.
196 /// The probe granularity, as a function rather than a literal at the call site, so that the
197 /// sweep below and the synthetic arm read the SAME value. Inline the literal in both places and
198 /// the arm pins only itself: someone coarsening the sweep would leave the arm green while
199 /// reintroducing exactly the defect it exists to catch. Coarsen this and
200 /// `a_sub_step_interval_is_missed_by_a_coarse_probe` goes red.
201 fn probe_step() -> chrono::Duration {
202 chrono::Duration::seconds(1)
203 }
204
205 /// Search a CLOSED WINDOW, not a centre and a radius.
206 ///
207 /// The caller supplies the bounds so the searched window has a single definition: the sweep
208 /// passes `anchor_search_window`, the same function the resolver uses, and a change to the
209 /// radius cannot leave this probe checking the old window while still reporting no violations.
210 /// Taking a centre and adding a radius here would recreate that second definition, and would
211 /// also reintroduce bare `-`/`+` on `NaiveDateTime`, which panics at the ends of the
212 /// representable range rather than returning `None`.
213 fn find_instant_carrying_date(
214 lo: chrono::NaiveDateTime,
215 hi: chrono::NaiveDateTime,
216 step: chrono::Duration,
217 target: chrono::NaiveDate,
218 local_date_at: impl Fn(chrono::NaiveDateTime) -> chrono::NaiveDate,
219 ) -> Option<chrono::NaiveDateTime> {
220 let mut probe = lo;
221 while probe <= hi {
222 if local_date_at(probe) == target {
223 return Some(probe);
224 }
225 // Checked, for the same reason the bounds are: stepping past the representable
226 // range must end the search, not abort the process. `?` is the whole behaviour
227 // here — the function already returns `Option`, so an overflow IS "no instant
228 // carries this date within the window".
229 probe = probe.checked_add_signed(step)?;
230 }
231 None
232 }
233
234 /// The discriminating arm for the probe granularity.
235 ///
236 /// A fabricated calendar in which one local date exists for THIRTY SECONDS. A one-minute probe
237 /// steps straight over it and reports that no instant carries that date, which is the failure
238 /// this granularity exists to prevent and is invisible against the real table. A one-second
239 /// probe finds it. Synthetic data is the correct fixture here precisely because the pinned
240 /// database has no transition of this shape: an assertion that cannot be made to fail is not an
241 /// assertion, and the alternative was shipping the finer step on argument alone.
242 #[test]
243 fn a_sub_step_interval_is_missed_by_a_coarse_probe() {
244 let base = chrono::NaiveDate::from_ymd_opt(2000, 1, 2)
245 .expect("base date")
246 .and_hms_opt(0, 0, 0)
247 .expect("midnight");
248 let target = chrono::NaiveDate::from_ymd_opt(2000, 1, 2).expect("target date");
249 let before = chrono::NaiveDate::from_ymd_opt(2000, 1, 1).expect("previous date");
250 let after = chrono::NaiveDate::from_ymd_opt(2000, 1, 3).expect("next date");
251
252 // `target` occupies [base + 10s, base + 40s) and nothing else does. The window is offset
253 // from every whole minute in the search so a coarse probe lands either side of it.
254 let window_start = base + chrono::Duration::seconds(10);
255 let window_end = base + chrono::Duration::seconds(40);
256 let calendar = move |probe: chrono::NaiveDateTime| {
257 if probe >= window_start && probe < window_end {
258 target
259 } else if probe < window_start {
260 before
261 } else {
262 after
263 }
264 };
265
266 // Guard the fixture itself: the interval must actually be shorter than the coarse step,
267 // otherwise this test would pass for a reason that has nothing to do with granularity.
268 assert!(
269 window_end - window_start < chrono::Duration::minutes(1),
270 "fixture is not sub-step: the interval must be shorter than the coarse probe"
271 );
272
273 // The radius is a whole number of minutes, so a one-minute probe lands on base exactly and
274 // steps to base + 60s, straddling the window. That is why the coarse arm misses
275 // deterministically rather than by luck, and the assertion below fails loudly if it ever
276 // stops being true.
277 // `base` is a fixed ordinary date, so this arithmetic is provably in range; the helper
278 // takes bounds rather than a radius, so the window is stated once here.
279 let radius = chrono::Duration::hours(48);
280 let lo = base - radius;
281 let hi = base + radius;
282 let coarse =
283 find_instant_carrying_date(lo, hi, chrono::Duration::minutes(1), target, calendar);
284 // The FINE arm reads the same `probe_step()` the sweep passes, so coarsening the sweep's
285 // granularity reddens this test.
286 let fine = find_instant_carrying_date(lo, hi, probe_step(), target, calendar);
287
288 assert!(
289 coarse.is_none(),
290 "the one-minute probe found {coarse:?}, so this fixture does not discriminate and \
291 the arm proves nothing"
292 );
293 let hit = fine.expect("the one-second probe must find the 30-second window");
294 assert_eq!(
295 calendar(hit),
296 target,
297 "the one-second probe returned {hit}, whose local date is not the target"
298 );
299 }
300
301 /// Regression for the skipped-midnight bisection's resolution, on the boundary
302 /// the review named. America/Havana springs forward AT local midnight on its
303 /// DST-start dates: 00:00 does not exist, so `anchor_date_to_earliest_instant`
304 /// takes the gap branch and must return the transition instant itself — the
305 /// least instant carrying the date.
306 ///
307 /// The pre-fix bisection stopped at a one-second interval and returned its
308 /// upper end, which is strictly later than the transition whenever that
309 /// transition's UTC second is not a dyadic fraction of the 96-hour search
310 /// window — it is not: a midnight spring-forward under Cuba's -05:00 standard
311 /// offset lands at 05:00Z, i.e. 53/96 of the window. The overshoot is
312 /// sub-second, so a one-second check (the all-zone sweep's old granularity)
313 /// was blind to it. This asserts the least-instant property at NANOSECOND
314 /// resolution: one nanosecond before the returned instant is a different,
315 /// earlier local date. It reddens against the one-second floor and greens
316 /// against the nanosecond floor.
317 #[test]
318 fn havana_skipped_midnight_resolves_to_the_true_least_instant() {
319 let tz = chrono_tz::America::Havana;
320 let start = chrono::NaiveDate::from_ymd_opt(1928, 1, 1).expect("start date");
321 let end = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).expect("end date");
322
323 let mut gap_dates = 0usize;
324 let mut date = start;
325 while date < end {
326 let midnight = date.and_hms_opt(0, 0, 0).expect("midnight");
327 if matches!(tz.from_local_datetime(&midnight), chrono::LocalResult::None) {
328 gap_dates += 1;
329 let resolved = anchor_date_to_earliest_instant(date, tz)
330 .expect("Havana carries this date at the post-gap local time");
331 assert_eq!(
332 resolved.date_naive(),
333 date,
334 "{date}: resolved instant {resolved} is not on the requested date"
335 );
336 let just_before = resolved - chrono::Duration::nanoseconds(1);
337 assert_ne!(
338 just_before.date_naive(),
339 date,
340 "{date}: resolved to {resolved}, but one nanosecond earlier is still on \
341 the date — the bisection overshot the transition and did not return the \
342 least instant"
343 );
344 }
345 date = date.succ_opt().expect("date in range has a successor");
346 }
347
348 // Only meaningful if Havana actually skips some local midnights in the
349 // range; a zero means the fixture stopped exercising the gap branch and
350 // the test would pass vacuously.
351 assert!(
352 gap_dates > 0,
353 "expected America/Havana to skip local midnight on at least one date in \
354 {start}..{end}; found none, so this regression exercises nothing"
355 );
356 }
357
358 /// Sweep every zone in the pinned `chrono-tz` over 1850-2100 and assert that
359 /// `anchor_date_to_earliest_instant` returns the LEAST instant of the requested local date.
360 ///
361 /// This is the re-derivation duty the gap branch's comment names, and it exists because the
362 /// comment used to assert a premise instead: that a local date is monotonically non-decreasing
363 /// in UTC time. That premise is false globally — zones that crossed the international date line
364 /// run the local calendar backwards, `America/Adak` at 1867-10-19T00:31:13Z being one — and a
365 /// true premise supporting a wrong inference reads as verified, so this checks the inference.
366 ///
367 /// The property asserted is direct and needs no transition table: for the returned instant `r`,
368 /// `r` has the requested local date and `r - 1ns` does not. Anything less than the least instant
369 /// fails the first half; anything more fails the second. The step is one NANOSECOND, chrono's
370 /// finest, not one second: a search that stopped a fraction of a second past the boundary
371 /// returned an `r` whose second-earlier neighbour was still on the date, so a one-second check
372 /// was blind to exactly the sub-second overshoot this sweep exists to catch.
373 ///
374 /// Ignored by default because it is an all-zone sweep, not because it is optional:
375 ///
376 /// cargo test -p khive-runtime --lib --release -- --ignored --nocapture
377 #[test]
378 #[ignore = "all-zone chrono-tz sweep; run on a pin bump (see the command in the doc comment)"]
379 fn every_gap_date_resolves_to_the_least_instant() {
380 let start = chrono::NaiveDate::from_ymd_opt(1850, 1, 1).expect("start date");
381 let end = chrono::NaiveDate::from_ymd_opt(2100, 1, 1).expect("end date");
382
383 let mut zones = 0usize;
384 let mut gaps = 0usize;
385 let mut folds = 0usize;
386 let mut skipped_days = 0usize;
387 let mut dates_examined = 0usize;
388 let mut failures: Vec<String> = Vec::new();
389
390 for tz in chrono_tz::TZ_VARIANTS.iter().copied() {
391 zones += 1;
392 let mut date = start;
393 while date < end {
394 dates_examined += 1;
395 let midnight = date.and_hms_opt(0, 0, 0).expect("midnight");
396 match tz.from_local_datetime(&midnight) {
397 chrono::LocalResult::Single(_) => {}
398 chrono::LocalResult::Ambiguous(_, _) => folds += 1,
399 chrono::LocalResult::None => gaps += 1,
400 }
401
402 match anchor_date_to_earliest_instant(date, tz) {
403 Some(resolved) => {
404 let previous = resolved - chrono::Duration::nanoseconds(1);
405 if resolved.date_naive() != date {
406 failures.push(format!(
407 "{tz}: {date} resolved to {resolved}, whose local date is \
408 {}",
409 resolved.date_naive()
410 ));
411 } else if tz.from_utc_datetime(&previous.naive_utc()).date_naive() == date {
412 failures.push(format!(
413 "{tz}: {date} resolved to {resolved}, but one nanosecond earlier \
414 is still {date} — not the least instant"
415 ));
416 }
417 }
418 // `None` is correct only for a date the zone's local calendar skips whole.
419 // Verify that rather than accepting it: no instant in the searched window
420 // may carry the requested local date.
421 None => {
422 skipped_days += 1;
423 // ONE-SECOND steps, matching the granularity the implementation itself
424 // searches at. A coarser step was here first and was wrong in the
425 // reassuring direction: a local-date interval shorter than the step can
426 // sit between two probes, so the search reports "no instant carries this
427 // date" without having looked at the instants that would. The pinned table
428 // happens to contain no such interval, but that is a property of today's
429 // data and this test exists to be run against data that has changed, which
430 // is exactly why the step size is pinned by
431 // `a_sub_step_interval_is_missed_by_a_coarse_probe` against a fabricated
432 // calendar rather than by this sweep. Skipped days are rare (single digits
433 // across the whole sweep), so the finer step costs nothing measurable.
434 //
435 // The bounds come from `anchor_search_window`, not from a literal, so the
436 // searched window has one definition. A second literal here would let a
437 // change to `ANCHOR_SEARCH_RADIUS_HOURS` leave this sweep checking the old
438 // window while still reporting zero violations.
439 let (lo, hi) = anchor_search_window(date);
440 let found =
441 find_instant_carrying_date(lo, hi, probe_step(), date, |probe| {
442 tz.from_utc_datetime(&probe).date_naive()
443 });
444 if let Some(hit) = found {
445 failures.push(format!(
446 "{tz}: {date} was reported as carried by no instant, but {hit}Z \
447 has that local date"
448 ));
449 }
450 }
451 }
452
453 date = date.succ_opt().expect("date in range has a successor");
454 }
455 }
456
457 println!(
458 "swept {zones} zones over {} dates each: {gaps} gaps, {folds} folds, \
459 {skipped_days} whole days skipped by a zone's calendar, {} violations",
460 (end - start).num_days(),
461 failures.len()
462 );
463
464 // The substantive result is asserted FIRST, deliberately. An earlier version put the
465 // population checks above this one and guessed their thresholds; one guess was wrong
466 // (it demanded thousands of fold dates against an actual 818), the test failed on the
467 // guess, and the property it exists to check never ran. A bound nobody measured is not
468 // a safety net, it is a second thing that can be wrong.
469 assert!(
470 failures.is_empty(),
471 "{} least-instant violations, first 10:\n{}",
472 failures.len(),
473 failures
474 .iter()
475 .take(10)
476 .cloned()
477 .collect::<Vec<_>>()
478 .join("\n")
479 );
480
481 // Then assert the sweep actually examined something, so an audit that silently swept
482 // nothing cannot pass by finding nothing.
483 //
484 // TRAVERSAL counts are exact, because they are ours: the number of zones and the number
485 // of dates per zone are fixed by this test's own range and by the pinned table's length,
486 // so anything else means the loop did not run. A floor here would have accepted a
487 // 501-zone subset of 597 as "the full database".
488 //
489 // TRANSITION counts stay floors at zero per SHAPE. Those belong to the database, not to
490 // us, and this test exists to be run when the database changes; pinning 4299 gaps would
491 // make a legitimate pin bump fail for a reason that has nothing to do with the property
492 // under test. Zero for a shape means the traversal never reached that kind of date.
493 let expected_dates = (end - start).num_days() as usize;
494 assert_eq!(
495 zones,
496 chrono_tz::TZ_VARIANTS.len(),
497 "swept {zones} zones but the pinned table has {}",
498 chrono_tz::TZ_VARIANTS.len()
499 );
500 assert_eq!(
501 dates_examined,
502 zones * expected_dates,
503 "expected {zones} zones x {expected_dates} dates = {}, examined {dates_examined}",
504 zones * expected_dates
505 );
506 assert!(
507 gaps > 0,
508 "no gap dates found -- the sweep did not reach a transition"
509 );
510 assert!(
511 folds > 0,
512 "no fold dates found -- the sweep did not reach a transition"
513 );
514 assert!(
515 skipped_days > 0,
516 "no whole-day skips found -- the None-return path was never exercised"
517 );
518 }
519}
520
521#[cfg(test)]
522mod window_tests {
523 use super::*;
524
525 /// The boundary is a property of the window, not of a magic year, so pin
526 /// it against `anchor_search_window` itself rather than a literal. Every
527 /// place that needs the searched window — the resolver and the all-zone
528 /// sweep — calls this one function, so this is the single definition.
529 #[test]
530 fn the_search_window_clamps_at_the_bounds_and_is_exact_elsewhere() {
531 let (lo, hi) = super::anchor_search_window(chrono::NaiveDate::MIN);
532 assert_eq!(
533 lo,
534 chrono::NaiveDateTime::MIN,
535 "the minimum date cannot carry a full -48h window, so the bound clamps \
536 rather than vanishing"
537 );
538 assert!(
539 hi > chrono::NaiveDateTime::MIN,
540 "the upper bound is exact here"
541 );
542
543 let (_, hi_max) = super::anchor_search_window(chrono::NaiveDate::MAX);
544 assert_eq!(
545 hi_max,
546 chrono::NaiveDateTime::MAX,
547 "and symmetrically at the top of the range"
548 );
549
550 // The must-not-fire half: away from the bounds the window is the real
551 // +/-48h, so the clamp cannot be silently swallowing ordinary dates.
552 let ordinary = chrono::NaiveDate::from_ymd_opt(2026, 8, 23).expect("valid date");
553 let midnight = ordinary.and_hms_opt(0, 0, 0).expect("midnight");
554 let radius = chrono::Duration::hours(super::ANCHOR_SEARCH_RADIUS_HOURS);
555 assert_eq!(
556 super::anchor_search_window(ordinary),
557 (midnight - radius, midnight + radius),
558 "an ordinary date gets the exact window"
559 );
560 }
561
562 /// `local_date_cmp` exists because `date_naive()` PANICS at the bounds.
563 /// Exercise exactly those two spellings' disagreement, or the helper looks
564 /// like ceremony to the next reader.
565 #[test]
566 fn local_date_cmp_is_total_where_date_naive_panics() {
567 let east: Tz = "Pacific/Apia".parse().expect("known IANA zone");
568 assert_eq!(
569 super::local_date_cmp(east, chrono::NaiveDateTime::MAX, chrono::NaiveDate::MAX),
570 std::cmp::Ordering::Greater,
571 "east of UTC the top of the range maps past the last representable date"
572 );
573 // NOT `America/Adak`, whose earliest LMT is +12:13:22 because it
574 // crossed the date line; a zone is chosen here by its MEASURED offset
575 // at the instant under test, never by what its name suggests.
576 let west: Tz = "America/New_York".parse().expect("known IANA zone");
577 assert_eq!(
578 super::local_date_cmp(west, chrono::NaiveDateTime::MIN, chrono::NaiveDate::MIN),
579 std::cmp::Ordering::Less,
580 "west of UTC the bottom of the range maps before the first representable date"
581 );
582 }
583}