Skip to main content

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}