Skip to main content

ucal_core/
value.rs

1//! The value types (§5): `Instant`, `Delta`, `Signed`, `SignedWindow`, `Window`,
2//! `Precision`, `Rounding`.
3//!
4//! Three rules shape this module and are worth stating before the code:
5//!
6//! - **Rule Z / Rule O.** The domain is unsigned and closed. `Sub` is *not*
7//!   implemented on `Instant`; subtraction is [`Instant::since`], which fails
8//!   with `UCAL-E0020`, or [`Instant::between`], which returns a [`Signed`].
9//!   Nothing wraps and nothing saturates.
10//! - **Rule T.** A value stated to tier precision denotes a closed interval.
11//!   [`Instant::window_at`] materialises it, and no parse path may hand back a
12//!   bare tick-precision `Instant` from truncated input.
13//! - **Rule Q.3.** [`SignedWindow`] is metadata. It has no arithmetic operators
14//!   and no conversion into [`Delta`], [`Instant`] or [`Window`]. §21.3-3 requires
15//!   a compile-fail test proving that lifting the restriction breaks the build.
16
17#[cfg(feature = "alloc")]
18use alloc::vec::Vec;
19use core::cmp::Ordering;
20use core::marker::PhantomData;
21
22use crate::backend::{TickInt, Ticks, CANONICAL_BYTES};
23use crate::error::{Code, Result, TimeError};
24use crate::profile::Profile;
25use crate::tier::{Tier, GROUP_BASE};
26
27/// How to round when rendering an exact internal value into a coarser form.
28///
29/// Rule R: rounding exists **only** when rendering. No API that constructs
30/// absolute time may round — construction from a foreign unit is exact or it is
31/// `UCAL-E0043`.
32#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
33pub enum Rounding {
34    /// Toward the datum.
35    Trunc,
36    /// Away from the datum.
37    Ceil,
38    /// Nearest, ties to even. The default, and the mode §2.2 uses for the datum.
39    #[default]
40    HalfEven,
41    /// Nearest, ties away from the datum.
42    HalfUp,
43}
44
45/// The precision at which a value is stated.
46///
47/// D-13 makes this a runtime field rather than a type parameter: type-level
48/// precision would infect every signature for little safety gain over Rule T.
49#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
50pub enum Precision {
51    /// Exact to one tick. The finest addressable precision (G2).
52    Tick,
53    /// Stated to a tier, denoting the closed interval `[v, v + 5^e - 1]`.
54    Tier(Tier),
55}
56
57impl Precision {
58    /// The tier this precision corresponds to.
59    pub fn tier(self) -> Tier {
60        match self {
61            Precision::Tick => Tier::TICK,
62            Precision::Tier(t) => t,
63        }
64    }
65
66    /// Whether this precision denotes a single tick.
67    pub fn is_exact(self) -> bool {
68        matches!(self, Precision::Tick) || self.tier().is_tick()
69    }
70}
71
72/// Sign of a [`Signed`] magnitude.
73#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
74pub enum Sign {
75    /// Zero or forward in time.
76    Positive,
77    /// Backward in time.
78    Negative,
79}
80
81// ---------------------------------------------------------------------------
82// Delta — an unsigned magnitude in ticks
83// ---------------------------------------------------------------------------
84
85/// An unsigned magnitude in ticks. Not tied to a profile: a count of ticks means
86/// the same thing under any profile that shares the tick (§5).
87#[cfg_attr(feature = "u512", derive(Copy))]
88#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
89pub struct Delta {
90    ticks: Ticks,
91}
92
93impl Delta {
94    /// Zero ticks.
95    pub fn zero() -> Self {
96        Delta {
97            ticks: <Ticks as TickInt>::zero(),
98        }
99    }
100
101    /// One tick — the finest representable interval (G2, N10).
102    pub fn one_tick() -> Self {
103        Delta {
104            ticks: <Ticks as TickInt>::one(),
105        }
106    }
107
108    /// From a raw tick count.
109    pub fn from_ticks(ticks: Ticks) -> Self {
110        Delta { ticks }
111    }
112
113    /// From a small tick count.
114    pub fn from_u64(v: u64) -> Self {
115        Delta {
116            ticks: <Ticks as TickInt>::from_u64(v),
117        }
118    }
119
120    /// Whole multiples of a tier: `count x 5^e`.
121    pub fn from_tier(tier: Tier, count: u64) -> Result<Self> {
122        let t = tier.ticks();
123        let c = <Ticks as TickInt>::from_u64(count);
124        t.try_mul(&c)
125            .map(|ticks| Delta { ticks })
126            .ok_or(TimeError::new(Code::E0021))
127    }
128
129    /// The raw tick count.
130    pub fn ticks(&self) -> &Ticks {
131        &self.ticks
132    }
133
134    /// Whether this is zero ticks.
135    pub fn is_zero(&self) -> bool {
136        self.ticks.is_zero_ticks()
137    }
138
139    /// `self + other`, failing on domain exit (Rule O).
140    pub fn checked_add(&self, other: &Delta) -> Result<Delta> {
141        self.ticks
142            .try_add(&other.ticks)
143            .map(|ticks| Delta { ticks })
144            .ok_or(TimeError::new(Code::E0021))
145    }
146
147    /// `self - other`, failing rather than wrapping (Rules Z, O).
148    pub fn checked_sub(&self, other: &Delta) -> Result<Delta> {
149        self.ticks
150            .try_sub(&other.ticks)
151            .map(|ticks| Delta { ticks })
152            .ok_or(TimeError::new(Code::E0020))
153    }
154
155    /// `self x n`, failing on domain exit.
156    pub fn mul_u64(&self, n: u64) -> Result<Delta> {
157        self.ticks
158            .try_mul(&<Ticks as TickInt>::from_u64(n))
159            .map(|ticks| Delta { ticks })
160            .ok_or(TimeError::new(Code::E0021))
161    }
162
163    /// Truncating `self / n`. Panics only if `n` is zero.
164    pub fn div_u64(&self, n: u64) -> Result<Delta> {
165        if n == 0 {
166            return Err(TimeError::with_context(Code::E0021, "division by zero"));
167        }
168        let (q, _) = self.ticks.quot_rem(&<Ticks as TickInt>::from_u64(n));
169        Ok(Delta { ticks: q })
170    }
171
172    /// Quotient and remainder against another magnitude.
173    pub fn divmod(&self, divisor: &Delta) -> Result<(Delta, Delta)> {
174        if divisor.is_zero() {
175            return Err(TimeError::with_context(Code::E0021, "division by zero"));
176        }
177        let (q, r) = self.ticks.quot_rem(&divisor.ticks);
178        Ok((Delta { ticks: q }, Delta { ticks: r }))
179    }
180
181    /// The largest tier no greater than `self`, or `None` if `self` is zero or
182    /// smaller than one tick (which cannot happen — one tick is the floor).
183    pub fn tier_of(&self) -> Option<Tier> {
184        if self.is_zero() {
185            return None;
186        }
187        Tier::all_descending().find(|t| t.ticks() <= self.ticks)
188    }
189
190    /// Whole count of a tier contained in `self`, and the remainder in ticks.
191    pub fn in_tier(&self, tier: Tier) -> (Ticks, Ticks) {
192        self.ticks.quot_rem(&tier.ticks())
193    }
194}
195
196// ---------------------------------------------------------------------------
197// Signed — a difference, which may be negative
198// ---------------------------------------------------------------------------
199
200/// A signed difference between two instants. The domain itself is unsigned
201/// (Rule Z, N12), so this type exists to let [`Instant::between`] be total.
202#[cfg_attr(feature = "u512", derive(Copy))]
203#[derive(Clone, PartialEq, Eq, Debug)]
204pub struct Signed {
205    sign: Sign,
206    mag: Delta,
207}
208
209impl Signed {
210    /// Construct from a sign and magnitude. A zero magnitude is normalised to
211    /// positive, so `Signed` has no negative zero.
212    pub fn new(sign: Sign, mag: Delta) -> Self {
213        if mag.is_zero() {
214            Signed {
215                sign: Sign::Positive,
216                mag,
217            }
218        } else {
219            Signed { sign, mag }
220        }
221    }
222
223    /// Zero.
224    pub fn zero() -> Self {
225        Signed::new(Sign::Positive, Delta::zero())
226    }
227
228    /// The sign.
229    pub fn sign(&self) -> Sign {
230        self.sign
231    }
232
233    /// The magnitude.
234    pub fn magnitude(&self) -> &Delta {
235        &self.mag
236    }
237
238    /// Whether this is zero.
239    pub fn is_zero(&self) -> bool {
240        self.mag.is_zero()
241    }
242
243    /// The magnitude, discarding the sign. Named so that dropping the sign is a
244    /// visible act at the call site.
245    pub fn into_magnitude(self) -> Delta {
246        self.mag
247    }
248}
249
250impl PartialOrd for Signed {
251    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
252        Some(self.cmp(other))
253    }
254}
255
256impl Ord for Signed {
257    fn cmp(&self, other: &Self) -> Ordering {
258        match (self.sign, other.sign) {
259            (Sign::Positive, Sign::Positive) => self.mag.cmp(&other.mag),
260            (Sign::Negative, Sign::Negative) => other.mag.cmp(&self.mag),
261            (Sign::Positive, Sign::Negative) => Ordering::Greater,
262            (Sign::Negative, Sign::Positive) => Ordering::Less,
263        }
264    }
265}
266
267// ---------------------------------------------------------------------------
268// SignedWindow — metadata only (Rule Q.3)
269// ---------------------------------------------------------------------------
270
271/// A signed interval, used **only** for profile metadata such as
272/// `BIG_BANG_CLAIM` (Rule Q.3).
273///
274/// The window is signed because the FLRW t→0 limit may lie *before* the datum,
275/// which is not representable as a tick (N12).
276///
277/// This type deliberately has:
278///
279/// - no arithmetic operators of any kind,
280/// - no `From<SignedWindow>` for [`Delta`], [`Instant`] or [`Window`],
281/// - no method returning any of those types.
282///
283/// It cannot be added to an `Instant` and cannot be widened into one. Attempting
284/// to use it as an operand is `UCAL-E0025`, and the type system is what makes
285/// that unreachable rather than a runtime check. §21.3-3 requires a compile-fail
286/// test; see `tests/compile_fail/`.
287#[cfg_attr(feature = "u512", derive(Copy))]
288#[derive(Clone, PartialEq, Eq, Debug)]
289pub struct SignedWindow {
290    lo: Signed,
291    hi: Signed,
292}
293
294impl SignedWindow {
295    /// Construct from bounds. Fails with `UCAL-E0022` if inverted.
296    pub fn new(lo: Signed, hi: Signed) -> Result<Self> {
297        if lo > hi {
298            return Err(TimeError::new(Code::E0022));
299        }
300        Ok(SignedWindow { lo, hi })
301    }
302
303    /// A symmetric window `+/- half_width` about zero. D-16: the half-width form
304    /// matches how the Planck 2018 uncertainty is published, without foreclosing
305    /// asymmetric windows, which [`SignedWindow::new`] still permits.
306    pub fn symmetric(half_width: Delta) -> Self {
307        SignedWindow {
308            lo: Signed::new(Sign::Negative, half_width.clone()),
309            hi: Signed::new(Sign::Positive, half_width),
310        }
311    }
312
313    /// The lower bound. Returns a [`Signed`], which is itself inert.
314    pub fn lo(&self) -> &Signed {
315        &self.lo
316    }
317
318    /// The upper bound.
319    pub fn hi(&self) -> &Signed {
320        &self.hi
321    }
322
323    /// Render for reporting. This is the only intended consumer: `ucal datum`,
324    /// `ucal doctor`, `ucal explain --claim`.
325    #[cfg(feature = "alloc")]
326    pub fn describe(&self) -> alloc::string::String {
327        use alloc::format;
328        let s = |v: &Signed| {
329            let m = v.magnitude().ticks().to_dec_string();
330            match v.sign() {
331                Sign::Negative => format!("-{m}"),
332                Sign::Positive => m,
333            }
334        };
335        format!("[{}, {}] ticks", s(&self.lo), s(&self.hi))
336    }
337}
338
339// ---------------------------------------------------------------------------
340// Instant
341// ---------------------------------------------------------------------------
342
343/// A point in absolute time: an unsigned integer count of ticks since the datum,
344/// parameterised by profile at the type level (Rule P).
345///
346/// Cross-profile arithmetic and comparison do not compile. `Instant<UC1>` and
347/// `Instant<UC2>` are distinct types with no coercion between them; conversion is
348/// available only through [`Instant::rebase`], which reports the constant shift.
349pub struct Instant<P: Profile> {
350    ticks: Ticks,
351    _p: PhantomData<P>,
352}
353
354// These are written by hand rather than derived. `derive` would place a bound on
355// `P` for each trait, but `P` appears only in `PhantomData` and carries no data,
356// so an `Instant` is comparable exactly when its tick count is. Deriving would,
357// for instance, demand `P: Ord` — which no profile has any reason to be.
358impl<P: Profile> Clone for Instant<P> {
359    fn clone(&self) -> Self {
360        Instant {
361            ticks: self.ticks.clone(),
362            _p: PhantomData,
363        }
364    }
365}
366
367#[cfg(feature = "u512")]
368impl<P: Profile> Copy for Instant<P> {}
369
370impl<P: Profile> PartialEq for Instant<P> {
371    fn eq(&self, other: &Self) -> bool {
372        self.ticks == other.ticks
373    }
374}
375
376impl<P: Profile> Eq for Instant<P> {}
377
378impl<P: Profile> PartialOrd for Instant<P> {
379    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
380        Some(self.cmp(other))
381    }
382}
383
384/// Rule M: for instants in one profile, exactly one of `a < b`, `a = b`, `a > b`
385/// holds, and it is the chronological order. Rule P makes the "in one profile"
386/// part a type-level guarantee rather than a runtime check.
387impl<P: Profile> Ord for Instant<P> {
388    fn cmp(&self, other: &Self) -> Ordering {
389        self.ticks.cmp(&other.ticks)
390    }
391}
392
393impl<P: Profile> core::hash::Hash for Instant<P> {
394    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
395        self.ticks.hash(state);
396    }
397}
398
399impl<P: Profile> core::fmt::Debug for Instant<P> {
400    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
401        // The profile tag is part of the identity of the value (Rule P), so it
402        // belongs in the debug output.
403        write!(f, "Instant<{}>({:?})", P::TAG, self.ticks)
404    }
405}
406
407/// The datum, as a `const`, on the default backend only.
408///
409/// §13 specifies `pub const ZERO: Self`. That is achievable when `Ticks` is
410/// `const`-constructible, which it is on the default backend and is not on
411/// `bigint` (a heap value cannot be a `const`). [`Instant::zero`] is the portable
412/// form and is available on both.
413#[cfg(feature = "u512")]
414impl<P: Profile> Instant<P> {
415    /// Tick 0 — the datum (Rule Z, Rule Q).
416    pub const ZERO: Self = Instant {
417        ticks: bnum::types::U512::MIN,
418        _p: PhantomData,
419    };
420}
421
422impl<P: Profile> Instant<P> {
423    /// Tick 0 — the datum. A **stipulated** reference point, conventionally
424    /// identified with the FLRW t→0 limit; not a measurement and not an observed
425    /// event (Rule Q, N17).
426    pub fn zero() -> Self {
427        Instant {
428            ticks: <Ticks as TickInt>::zero(),
429            _p: PhantomData,
430        }
431    }
432
433    /// Construct from a tick count, rejecting values outside the profile domain.
434    pub fn from_ticks(ticks: Ticks) -> Result<Self> {
435        if ticks > P::domain_max() {
436            return Err(TimeError::new(Code::E0021));
437        }
438        Ok(Instant {
439            ticks,
440            _p: PhantomData,
441        })
442    }
443
444    /// Construct from a small tick count.
445    pub fn from_u64(v: u64) -> Result<Self> {
446        Self::from_ticks(<Ticks as TickInt>::from_u64(v))
447    }
448
449    /// The raw tick count.
450    pub fn ticks(&self) -> &Ticks {
451        &self.ticks
452    }
453
454    /// The decimal group value at a tier, in `0..=3124`.
455    pub fn tier_value(&self, tier: Tier) -> u16 {
456        let (shifted, _) = self.ticks.quot_rem(&tier.ticks());
457        let (_, r) = shifted.quot_rem(&<Ticks as TickInt>::from_u64(GROUP_BASE as u64));
458        // r < 3125 fits u16; the conversion goes through the canonical bytes so
459        // that it needs no backend-specific cast.
460        let b = r.to_canonical_bytes();
461        u16::from_be_bytes([b[CANONICAL_BYTES - 2], b[CANONICAL_BYTES - 1]])
462    }
463
464    /// Group values for tiers `from` down to `to`, most significant first.
465    #[cfg(feature = "alloc")]
466    pub fn groups(&self, from: Tier, to: Tier) -> Result<Vec<u16>> {
467        if from < to {
468            return Err(TimeError::with_context(
469                Code::E0006,
470                "group range must descend",
471            ));
472        }
473        let mut out = Vec::new();
474        let mut k = from.index();
475        while k >= to.index() {
476            out.push(self.tier_value(Tier::new(k)?));
477            k -= 1;
478        }
479        Ok(out)
480    }
481
482    /// Truncate toward the datum. Truncation *is* rounding (G3, Rule G).
483    pub fn floor_to(&self, tier: Tier) -> Self {
484        let t = tier.ticks();
485        let (q, _) = self.ticks.quot_rem(&t);
486        let ticks = q
487            .try_mul(&t)
488            .expect("floor of an in-domain value is in domain");
489        Instant {
490            ticks,
491            _p: PhantomData,
492        }
493    }
494
495    /// Round away from the datum, failing on domain exit.
496    pub fn ceil_to(&self, tier: Tier) -> Result<Self> {
497        let t = tier.ticks();
498        let (q, r) = self.ticks.quot_rem(&t);
499        if r.is_zero_ticks() {
500            return Ok(self.clone());
501        }
502        let next = q
503            .try_add(&<Ticks as TickInt>::one())
504            .and_then(|n| n.try_mul(&t))
505            .ok_or(TimeError::new(Code::E0021))?;
506        Self::from_ticks(next)
507    }
508
509    /// Round to a tier under an explicit mode (Rule R).
510    pub fn round_to(&self, tier: Tier, mode: Rounding) -> Result<Self> {
511        let t = tier.ticks();
512        let (q, r) = self.ticks.quot_rem(&t);
513        if r.is_zero_ticks() {
514            return Ok(self.clone());
515        }
516        let up = match mode {
517            Rounding::Trunc => false,
518            Rounding::Ceil => true,
519            Rounding::HalfUp | Rounding::HalfEven => {
520                let twice = r
521                    .try_add(&r)
522                    .expect("2r < 2 x tier, which is in domain");
523                match twice.cmp(&t) {
524                    Ordering::Greater => true,
525                    Ordering::Less => false,
526                    Ordering::Equal => match mode {
527                        Rounding::HalfUp => true,
528                        // ties to even
529                        _ => q.is_odd(),
530                    },
531                }
532            }
533        };
534        if up {
535            self.ceil_to(tier)
536        } else {
537            Ok(self.floor_to(tier))
538        }
539    }
540
541    /// The closed interval a value stated at this precision denotes (Rule T).
542    ///
543    /// For [`Precision::Tick`] this is the degenerate window `[v, v]`. For a tier
544    /// it is `[floor, floor + 5^e - 1]` — never a bare instant, which is what
545    /// keeps failure mode F2 closed.
546    pub fn window_at(&self, precision: Precision) -> Result<Window<P>> {
547        match precision {
548            Precision::Tick => Window::new(self.clone(), self.clone()),
549            Precision::Tier(tier) => {
550                if tier.is_tick() {
551                    return Window::new(self.clone(), self.clone());
552                }
553                let lo = self.floor_to(tier);
554                let span = tier
555                    .ticks()
556                    .try_sub(&<Ticks as TickInt>::one())
557                    .expect("a tier is at least one tick");
558                // Rule T's interval is intersected with the closed domain. Near
559                // the ceiling, `floor + 5^e - 1` can run past `domain_max`; since
560                // no representable instant lies beyond it, clamping keeps the
561                // window a sound enclosure and makes it a tighter one. Failing
562                // here instead would mean a legitimate coarse statement about a
563                // late instant had no representable meaning.
564                let hi_ticks = match lo.ticks.try_add(&span) {
565                    Some(t) if t <= P::domain_max() => t,
566                    _ => P::domain_max(),
567                };
568                Window::new(lo, Self::from_ticks(hi_ticks)?)
569            }
570        }
571    }
572
573    /// Elapsed ticks since an earlier instant. `UCAL-E0020` if `earlier` is later
574    /// than `self` — Rule Z admits no negative result.
575    pub fn since(&self, earlier: &Self) -> Result<Delta> {
576        self.ticks
577            .try_sub(&earlier.ticks)
578            .map(Delta::from_ticks)
579            .ok_or(TimeError::new(Code::E0020))
580    }
581
582    /// The signed difference `self - other`. Always succeeds.
583    pub fn between(&self, other: &Self) -> Signed {
584        match self.ticks.cmp(&other.ticks) {
585            Ordering::Greater | Ordering::Equal => Signed::new(
586                Sign::Positive,
587                Delta::from_ticks(
588                    self.ticks
589                        .try_sub(&other.ticks)
590                        .expect("self >= other"),
591                ),
592            ),
593            Ordering::Less => Signed::new(
594                Sign::Negative,
595                Delta::from_ticks(
596                    other
597                        .ticks
598                        .try_sub(&self.ticks)
599                        .expect("other > self"),
600                ),
601            ),
602        }
603    }
604
605    /// Advance by a magnitude, failing on domain exit (`UCAL-E0021`).
606    pub fn checked_add(&self, d: &Delta) -> Result<Self> {
607        let ticks = self
608            .ticks
609            .try_add(d.ticks())
610            .ok_or(TimeError::new(Code::E0021))?;
611        Self::from_ticks(ticks)
612    }
613
614    /// Retreat by a magnitude, failing before the datum (`UCAL-E0020`).
615    pub fn checked_sub(&self, d: &Delta) -> Result<Self> {
616        self.ticks
617            .try_sub(d.ticks())
618            .map(|ticks| Instant {
619                ticks,
620                _p: PhantomData,
621            })
622            .ok_or(TimeError::new(Code::E0020))
623    }
624
625    /// Canonical binary form: 64 bytes, big-endian, zero-padded (§7.1, Rule B).
626    ///
627    /// Byte order is chronological order, so the encoding is directly usable as a
628    /// database key. Identical on every backend, which is what makes the
629    /// cross-backend differential test a conformance test (Rule W).
630    pub fn to_bytes(&self) -> [u8; CANONICAL_BYTES] {
631        self.ticks.to_canonical_bytes()
632    }
633
634    /// Inverse of [`Instant::to_bytes`].
635    pub fn from_bytes(bytes: &[u8; CANONICAL_BYTES]) -> Result<Self> {
636        let ticks =
637            <Ticks as TickInt>::from_canonical_bytes(bytes).ok_or(TimeError::new(Code::E0021))?;
638        Self::from_ticks(ticks)
639    }
640
641    /// Reinterpret under another profile, reporting the constant tick shift
642    /// (D-14, Rule P).
643    ///
644    /// The shift is `Q::origin_offset() - P::origin_offset()`: both profiles date
645    /// from their own datum, so rebasing is a translation. Returns the new instant
646    /// and the shift that was applied, because a caller that does not see the
647    /// shift cannot audit the conversion.
648    pub fn rebase<Q: Profile>(&self) -> Result<(Instant<Q>, Signed)> {
649        let from = P::origin_offset();
650        let to = Q::origin_offset();
651        let (shift, ticks) = match to.cmp(&from) {
652            Ordering::Greater | Ordering::Equal => {
653                let d = to.try_sub(&from).expect("to >= from");
654                (
655                    Signed::new(Sign::Positive, Delta::from_ticks(d.clone())),
656                    self.ticks
657                        .try_add(&d)
658                        .ok_or(TimeError::new(Code::E0021))?,
659                )
660            }
661            Ordering::Less => {
662                let d = from.try_sub(&to).expect("from > to");
663                (
664                    Signed::new(Sign::Negative, Delta::from_ticks(d.clone())),
665                    self.ticks
666                        .try_sub(&d)
667                        .ok_or(TimeError::new(Code::E0020))?,
668                )
669            }
670        };
671        Ok((Instant::<Q>::from_ticks(ticks)?, shift))
672    }
673}
674
675// ---------------------------------------------------------------------------
676// Window — a closed interval (Rules T, U)
677// ---------------------------------------------------------------------------
678
679/// A closed interval in absolute time, `lo <= hi`.
680///
681/// Rule U: window arithmetic is interval arithmetic with outward rounding.
682/// Windows are never silently collapsed — [`Window::midpoint`] must be called
683/// explicitly and takes a [`Rounding`].
684pub struct Window<P: Profile> {
685    lo: Instant<P>,
686    hi: Instant<P>,
687}
688
689// Hand-written for the same reason as `Instant`'s: `P` carries no data.
690impl<P: Profile> Clone for Window<P> {
691    fn clone(&self) -> Self {
692        Window {
693            lo: self.lo.clone(),
694            hi: self.hi.clone(),
695        }
696    }
697}
698
699#[cfg(feature = "u512")]
700impl<P: Profile> Copy for Window<P> {}
701
702impl<P: Profile> PartialEq for Window<P> {
703    fn eq(&self, other: &Self) -> bool {
704        self.lo == other.lo && self.hi == other.hi
705    }
706}
707
708impl<P: Profile> Eq for Window<P> {}
709
710impl<P: Profile> core::fmt::Debug for Window<P> {
711    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
712        write!(f, "Window<{}>[{:?}, {:?}]", P::TAG, self.lo.ticks, self.hi.ticks)
713    }
714}
715
716// Deliberately NOT implemented for `Window`: `PartialOrd` and `Ord`.
717//
718// Rule T requires comparison across unequal precision to use interval semantics
719// and to be able to return indeterminate. A total order on windows would silently
720// resolve overlapping intervals, which is the whole failure the rule exists to
721// prevent. Use `Window::compare` or `Window::try_compare` instead.
722
723/// The result of comparing two values whose precisions differ (Rule T).
724#[derive(Clone, Copy, PartialEq, Eq, Debug)]
725pub enum IntervalOrdering {
726    /// Strictly earlier: `self.hi < other.lo`.
727    Before,
728    /// Strictly later: `self.lo > other.hi`.
729    After,
730    /// The same single tick.
731    EqualExact,
732    /// The intervals overlap, so the order is not determined. `UCAL-E0023`.
733    Indeterminate,
734}
735
736impl<P: Profile> Window<P> {
737    /// Construct from bounds, rejecting inversion with `UCAL-E0022`.
738    pub fn new(lo: Instant<P>, hi: Instant<P>) -> Result<Self> {
739        if lo > hi {
740            return Err(TimeError::new(Code::E0022));
741        }
742        Ok(Window { lo, hi })
743    }
744
745    /// The degenerate window at a single tick.
746    pub fn exact(at: Instant<P>) -> Self {
747        Window {
748            lo: at.clone(),
749            hi: at,
750        }
751    }
752
753    /// Lower bound.
754    pub fn lo(&self) -> &Instant<P> {
755        &self.lo
756    }
757
758    /// Upper bound.
759    pub fn hi(&self) -> &Instant<P> {
760        &self.hi
761    }
762
763    /// Width in ticks. A degenerate window has width zero, not one: it spans one
764    /// tick, and the width is the difference between its bounds.
765    pub fn width(&self) -> Delta {
766        self.hi
767            .since(&self.lo)
768            .expect("Window maintains lo <= hi")
769    }
770
771    /// Whether the window spans exactly one tick.
772    pub fn is_exact(&self) -> bool {
773        self.lo == self.hi
774    }
775
776    /// Whether an instant lies within the closed interval.
777    pub fn contains(&self, t: &Instant<P>) -> bool {
778        self.lo <= *t && *t <= self.hi
779    }
780
781    /// Whether two windows overlap.
782    pub fn overlaps(&self, other: &Self) -> bool {
783        self.lo <= other.hi && other.lo <= self.hi
784    }
785
786    /// Interval-aware comparison, which may be indeterminate (Rule T).
787    pub fn compare(&self, other: &Self) -> IntervalOrdering {
788        if self.is_exact() && other.is_exact() && self.lo == other.lo {
789            return IntervalOrdering::EqualExact;
790        }
791        if self.hi < other.lo {
792            IntervalOrdering::Before
793        } else if self.lo > other.hi {
794            IntervalOrdering::After
795        } else {
796            IntervalOrdering::Indeterminate
797        }
798    }
799
800    /// Interval-aware comparison as a `Result`, so that indeterminacy surfaces as
801    /// `UCAL-E0023` at call sites that require a total order.
802    pub fn try_compare(&self, other: &Self) -> Result<Ordering> {
803        match self.compare(other) {
804            IntervalOrdering::Before => Ok(Ordering::Less),
805            IntervalOrdering::After => Ok(Ordering::Greater),
806            IntervalOrdering::EqualExact => Ok(Ordering::Equal),
807            IntervalOrdering::Indeterminate => Err(TimeError::new(Code::E0023)),
808        }
809    }
810
811    /// Shift the whole window by a magnitude. Outward rounding is trivial here
812    /// because a translation preserves width exactly (Rule U).
813    pub fn checked_add(&self, d: &Delta) -> Result<Self> {
814        Window::new(self.lo.checked_add(d)?, self.hi.checked_add(d)?)
815    }
816
817    /// Shift the window back by a magnitude.
818    pub fn checked_sub(&self, d: &Delta) -> Result<Self> {
819        Window::new(self.lo.checked_sub(d)?, self.hi.checked_sub(d)?)
820    }
821
822    /// Widen outward by a magnitude on both sides. The lower bound saturates at
823    /// the datum rather than failing, because a window clipped by Rule Z is still
824    /// a sound enclosure; the clipping is reported by the returned flag.
825    pub fn widen(&self, d: &Delta) -> Result<(Self, bool)> {
826        let hi = self.hi.checked_add(d)?;
827        match self.lo.checked_sub(d) {
828            Ok(lo) => Ok((Window::new(lo, hi)?, false)),
829            Err(_) => Ok((Window::new(Instant::zero(), hi)?, true)),
830        }
831    }
832
833    /// Shift by an *uncertain* magnitude: `lo` with `lo`, `hi` with `hi` (Rule U).
834    ///
835    /// The result is at least as wide as either input, which is the point — an
836    /// uncertain instant displaced by an uncertain duration cannot be known better
837    /// than either. This is the operation Rule J.2 needs when an anchor window is
838    /// carried forward into a derived field.
839    pub fn checked_add_span(&self, s: &Span) -> Result<Self> {
840        Window::new(self.lo.checked_add(s.lo())?, self.hi.checked_add(s.hi())?)
841    }
842
843    /// Shift back by an uncertain magnitude: `[lo - s.hi, hi - s.lo]`, outward.
844    ///
845    /// Strict at the datum — `UCAL-E0020` rather than a clamp — because a window
846    /// that has been silently clipped is no longer the interval the caller asked
847    /// for. Use [`Window::widen`] where clipping is acceptable and reported.
848    pub fn checked_sub_span(&self, s: &Span) -> Result<Self> {
849        Window::new(self.lo.checked_sub(s.hi())?, self.hi.checked_sub(s.lo())?)
850    }
851
852    /// Elapsed time from an earlier window to this one, as an uncertain
853    /// magnitude.
854    ///
855    /// `[max(0, self.lo - earlier.hi), self.hi - earlier.lo]`. The lower bound
856    /// clamps at zero because overlapping windows genuinely admit zero elapsed
857    /// time; the flag reports whether that happened. `UCAL-E0020` when this window
858    /// lies wholly before `earlier`, which is Rule Z applied to intervals.
859    pub fn since_window(&self, earlier: &Self) -> Result<(Span, bool)> {
860        let hi = self.hi.since(&earlier.lo)?;
861        match self.lo.since(&earlier.hi) {
862            Ok(lo) => Ok((Span::new(lo, hi)?, false)),
863            Err(_) => Ok((Span::new(Delta::zero(), hi)?, true)),
864        }
865    }
866
867    /// The union enclosure of two windows — the smallest window containing both.
868    pub fn hull(&self, other: &Self) -> Self {
869        Window {
870            lo: if self.lo <= other.lo {
871                self.lo.clone()
872            } else {
873                other.lo.clone()
874            },
875            hi: if self.hi >= other.hi {
876                self.hi.clone()
877            } else {
878                other.hi.clone()
879            },
880        }
881    }
882
883    /// The intersection, or `None` if the windows are disjoint.
884    pub fn intersect(&self, other: &Self) -> Option<Self> {
885        if !self.overlaps(other) {
886            return None;
887        }
888        Some(Window {
889            lo: if self.lo >= other.lo {
890                self.lo.clone()
891            } else {
892                other.lo.clone()
893            },
894            hi: if self.hi <= other.hi {
895                self.hi.clone()
896            } else {
897                other.hi.clone()
898            },
899        })
900    }
901
902    /// Collapse to a single instant under an explicit rounding mode.
903    ///
904    /// Rule U forbids collapsing a window silently, so this is deliberately a
905    /// named call that cannot be reached by coercion.
906    pub fn midpoint(&self, mode: Rounding) -> Result<Instant<P>> {
907        let width = self.width();
908        let (half, rem) = width.divmod(&Delta::from_u64(2))?;
909        let mut ticks = self
910            .lo
911            .ticks
912            .try_add(half.ticks())
913            .ok_or(TimeError::new(Code::E0021))?;
914        if !rem.is_zero() {
915            let bump = match mode {
916                Rounding::Trunc => false,
917                Rounding::Ceil | Rounding::HalfUp => true,
918                Rounding::HalfEven => ticks.is_odd(),
919            };
920            if bump {
921                ticks = ticks
922                    .try_add(&<Ticks as TickInt>::one())
923                    .ok_or(TimeError::new(Code::E0021))?;
924            }
925        }
926        Instant::from_ticks(ticks)
927    }
928}
929
930// ---------------------------------------------------------------------------
931// Span — an uncertain magnitude (Rule U)
932// ---------------------------------------------------------------------------
933
934/// A closed interval of durations: a magnitude that is known only to within a
935/// range.
936///
937/// [`Window`] is to [`Instant`] as `Span` is to [`Delta`], and the pairing is
938/// what lets Rule U's propagation actually happen. Rule J.2 requires an anchor's
939/// uncertainty to reach every derived field, and an anchor is a window; the
940/// elapsed time from an uncertain anchor to an uncertain instant is therefore not
941/// a `Delta` but a `Span`, and typing it as one keeps the uncertainty from being
942/// dropped on the way through.
943#[cfg_attr(feature = "u512", derive(Copy))]
944#[derive(Clone, PartialEq, Eq, Debug)]
945pub struct Span {
946    lo: Delta,
947    hi: Delta,
948}
949
950impl Span {
951    /// Construct from bounds. `UCAL-E0022` if inverted.
952    pub fn new(lo: Delta, hi: Delta) -> Result<Span> {
953        if lo > hi {
954            return Err(TimeError::new(Code::E0022));
955        }
956        Ok(Span { lo, hi })
957    }
958
959    /// A magnitude known exactly.
960    pub fn exact(d: Delta) -> Span {
961        Span {
962            lo: d.clone(),
963            hi: d,
964        }
965    }
966
967    /// Zero, exactly.
968    pub fn zero() -> Span {
969        Span::exact(Delta::zero())
970    }
971
972    /// Lower bound.
973    pub fn lo(&self) -> &Delta {
974        &self.lo
975    }
976
977    /// Upper bound.
978    pub fn hi(&self) -> &Delta {
979        &self.hi
980    }
981
982    /// How wide the uncertainty is.
983    pub fn uncertainty(&self) -> Delta {
984        self.hi
985            .checked_sub(&self.lo)
986            .expect("Span maintains lo <= hi")
987    }
988
989    /// Whether the magnitude is known exactly.
990    pub fn is_exact(&self) -> bool {
991        self.lo == self.hi
992    }
993
994    /// Interval addition: `lo` with `lo`, `hi` with `hi` (Rule U).
995    pub fn checked_add(&self, other: &Span) -> Result<Span> {
996        Span::new(
997            self.lo.checked_add(&other.lo)?,
998            self.hi.checked_add(&other.hi)?,
999        )
1000    }
1001
1002    /// Interval subtraction, `[lo - other.hi, hi - other.lo]`.
1003    ///
1004    /// The lower bound clamps at zero rather than failing: a magnitude is
1005    /// non-negative, and two overlapping spans genuinely admit a difference of
1006    /// zero. The clamp is reported so a caller can tell it happened.
1007    pub fn checked_sub(&self, other: &Span) -> Result<(Span, bool)> {
1008        let hi = self.hi.checked_sub(&other.lo)?;
1009        match self.lo.checked_sub(&other.hi) {
1010            Ok(lo) => Ok((Span::new(lo, hi)?, false)),
1011            Err(_) => Ok((Span::new(Delta::zero(), hi)?, true)),
1012        }
1013    }
1014
1015    /// Collapse to a single magnitude under an explicit mode (Rule U).
1016    pub fn midpoint(&self, mode: Rounding) -> Result<Delta> {
1017        let (half, rem) = self.uncertainty().divmod(&Delta::from_u64(2))?;
1018        let mut d = self.lo.checked_add(&half)?;
1019        if !rem.is_zero() {
1020            let bump = match mode {
1021                Rounding::Trunc => false,
1022                Rounding::Ceil | Rounding::HalfUp => true,
1023                Rounding::HalfEven => d.ticks().is_odd(),
1024            };
1025            if bump {
1026                d = d.checked_add(&Delta::one_tick())?;
1027            }
1028        }
1029        Ok(d)
1030    }
1031}
1032
1033// ---------------------------------------------------------------------------
1034// Stated — a value together with the precision it was stated at (Rule T)
1035// ---------------------------------------------------------------------------
1036
1037/// An instant together with the precision at which it was stated.
1038///
1039/// Rule T requires comparison across unequal precision to use interval semantics
1040/// and to be able to return indeterminate. Carrying the precision alongside the
1041/// value makes that the *easy* path: [`Stated::try_compare`] cannot be reached
1042/// without confronting `UCAL-E0023`, whereas comparing two bare `Instant`s that
1043/// came from truncated text would quietly compare their floors.
1044///
1045/// This is what [`crate::codec::parse`] returns, as a pair; the type exists so
1046/// the pair need not be carried by hand.
1047pub struct Stated<P: Profile> {
1048    value: Instant<P>,
1049    precision: Precision,
1050}
1051
1052impl<P: Profile> Clone for Stated<P> {
1053    fn clone(&self) -> Self {
1054        Stated {
1055            value: self.value.clone(),
1056            precision: self.precision,
1057        }
1058    }
1059}
1060
1061#[cfg(feature = "u512")]
1062impl<P: Profile> Copy for Stated<P> {}
1063
1064impl<P: Profile> PartialEq for Stated<P> {
1065    /// Equality is on the *statement*, not the interval: two statements are equal
1066    /// when they say the same thing at the same precision. Use
1067    /// [`Stated::try_compare`] to ask about the underlying instants.
1068    fn eq(&self, other: &Self) -> bool {
1069        self.value == other.value && self.precision == other.precision
1070    }
1071}
1072
1073impl<P: Profile> Eq for Stated<P> {}
1074
1075impl<P: Profile> core::fmt::Debug for Stated<P> {
1076    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1077        write!(
1078            f,
1079            "Stated<{}>({:?} @ {:?})",
1080            P::TAG,
1081            self.value.ticks(),
1082            self.precision
1083        )
1084    }
1085}
1086
1087impl<P: Profile> Stated<P> {
1088    /// Pair a value with its stated precision.
1089    pub fn new(value: Instant<P>, precision: Precision) -> Self {
1090        Stated { value, precision }
1091    }
1092
1093    /// A value stated to the tick.
1094    pub fn exact(value: Instant<P>) -> Self {
1095        Stated {
1096            value,
1097            precision: Precision::Tick,
1098        }
1099    }
1100
1101    /// The value as stated — its floor at the stated precision, not a tick unless
1102    /// the precision says so.
1103    pub fn value(&self) -> &Instant<P> {
1104        &self.value
1105    }
1106
1107    /// The precision it was stated at.
1108    pub fn precision(&self) -> Precision {
1109        self.precision
1110    }
1111
1112    /// Whether the statement pins a single tick.
1113    pub fn is_exact(&self) -> bool {
1114        self.precision.is_exact()
1115    }
1116
1117    /// The interval the statement denotes (Rule T).
1118    pub fn window(&self) -> Result<Window<P>> {
1119        self.value.window_at(self.precision)
1120    }
1121
1122    /// Interval-aware comparison, which may be indeterminate.
1123    pub fn compare(&self, other: &Self) -> Result<IntervalOrdering> {
1124        Ok(self.window()?.compare(&other.window()?))
1125    }
1126
1127    /// Interval-aware comparison as a total order, or `UCAL-E0023` when the
1128    /// intervals overlap and the order is genuinely not determined.
1129    pub fn try_compare(&self, other: &Self) -> Result<Ordering> {
1130        self.window()?.try_compare(&other.window()?)
1131    }
1132
1133    /// Re-state at a coarser precision. `UCAL-E0023` if asked to *refine*, since
1134    /// no operation may invent precision the statement does not carry (F2).
1135    pub fn coarsen(&self, to: Tier) -> Result<Stated<P>> {
1136        if to < self.precision.tier() {
1137            return Err(TimeError::with_context(
1138                Code::E0023,
1139                "cannot restate at a finer precision than the value was given at",
1140            ));
1141        }
1142        Ok(Stated {
1143            value: self.value.floor_to(to),
1144            precision: if to.is_tick() {
1145                Precision::Tick
1146            } else {
1147                Precision::Tier(to)
1148            },
1149        })
1150    }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::*;
1156    use crate::profile::UC1;
1157
1158    type I = Instant<UC1>;
1159
1160    fn at(n: u64) -> I {
1161        I::from_u64(n).unwrap()
1162    }
1163    fn d(n: u64) -> Delta {
1164        Delta::from_u64(n)
1165    }
1166
1167    // ---- Rule Z: the domain is unsigned and closed at the datum ----
1168
1169    #[test]
1170    fn nothing_precedes_the_datum() {
1171        assert_eq!(I::zero().ticks(), &<Ticks as TickInt>::zero());
1172        let err = I::zero().checked_sub(&Delta::one_tick()).unwrap_err();
1173        assert_eq!(err.code, Code::E0020);
1174        // ...and it fails rather than wrapping or saturating (Rule O).
1175        assert_eq!(at(5).checked_sub(&d(6)).unwrap_err().code, Code::E0020);
1176        assert_eq!(at(5).checked_sub(&d(5)).unwrap(), I::zero());
1177    }
1178
1179    #[test]
1180    fn since_fails_backwards_but_between_does_not() {
1181        let early = at(10);
1182        let late = at(30);
1183        assert_eq!(late.since(&early).unwrap(), d(20));
1184        assert_eq!(early.since(&late).unwrap_err().code, Code::E0020);
1185
1186        // `between` is total and signed (§5.1).
1187        let b = early.between(&late);
1188        assert_eq!(b.sign(), Sign::Negative);
1189        assert_eq!(b.magnitude(), &d(20));
1190        let f = late.between(&early);
1191        assert_eq!(f.sign(), Sign::Positive);
1192        assert_eq!(f.magnitude(), &d(20));
1193        // No negative zero.
1194        assert_eq!(early.between(&early).sign(), Sign::Positive);
1195        assert!(early.between(&early).is_zero());
1196    }
1197
1198    // ---- Rule O / Rule W: the ceiling is a typed error on both backends ----
1199
1200    #[test]
1201    fn domain_ceiling_is_enforced() {
1202        let max = I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap();
1203        assert_eq!(
1204            max.checked_add(&Delta::one_tick()).unwrap_err().code,
1205            Code::E0021
1206        );
1207        assert_eq!(max.checked_add(&Delta::zero()).unwrap(), max.clone());
1208        // Rule W: identical ceiling regardless of backend representation.
1209        assert_eq!(<Ticks as TickInt>::domain_max().bit_len(), 512);
1210    }
1211
1212    // ---- Rule M: monotone total order ----
1213
1214    #[test]
1215    fn order_is_chronological_and_total() {
1216        let a = at(1);
1217        let b = at(2);
1218        assert!(a < b && b > a && a == a.clone());
1219        assert_eq!(a.cmp(&b), Ordering::Less);
1220        // Exactly one of <, =, > holds.
1221        for (x, y) in [(1u64, 2u64), (2, 1), (7, 7)] {
1222            let (x, y) = (at(x), at(y));
1223            let n = [x < y, x == y, x > y].iter().filter(|b| **b).count();
1224            assert_eq!(n, 1);
1225        }
1226    }
1227
1228    // ---- Rule G: truncation is rounding; prefix comparison is chronological ----
1229
1230    #[test]
1231    fn floor_ceil_bracket_the_value() {
1232        let t = Tier::BEAT;
1233        let beat = t.ticks();
1234        // A value one tick into the second beat.
1235        let v = I::from_ticks(beat.try_mul(&<Ticks as TickInt>::from_u64(2)).unwrap())
1236            .unwrap()
1237            .checked_add(&Delta::one_tick())
1238            .unwrap();
1239        let lo = v.floor_to(t);
1240        let hi = v.ceil_to(t).unwrap();
1241        assert!(lo <= v && v <= hi);
1242        assert_eq!(lo.ticks(), &beat.try_mul(&<Ticks as TickInt>::from_u64(2)).unwrap());
1243        assert_eq!(hi.ticks(), &beat.try_mul(&<Ticks as TickInt>::from_u64(3)).unwrap());
1244        // Already-aligned values are fixed points of both.
1245        assert_eq!(lo.floor_to(t), lo);
1246        assert_eq!(lo.ceil_to(t).unwrap(), lo);
1247    }
1248
1249    #[test]
1250    fn truncation_is_monotone() {
1251        // If a <= b then floor(a) <= floor(b): this is what makes prefix
1252        // comparison chronological comparison (G3).
1253        let t = Tier::ARC;
1254        let step = Tier::BEAT.ticks();
1255        let mut prev: Option<I> = None;
1256        for n in 0..40u64 {
1257            let v = I::from_ticks(step.try_mul(&<Ticks as TickInt>::from_u64(n * 7)).unwrap())
1258                .unwrap();
1259            let f = v.floor_to(t);
1260            if let Some(p) = prev {
1261                assert!(p <= f);
1262            }
1263            prev = Some(f);
1264        }
1265    }
1266
1267    // ---- Rule R: rounding only on rendering, and the mode is explicit ----
1268
1269    #[test]
1270    fn rounding_modes_behave() {
1271        let t = Tier::new(-11).unwrap(); // 5^5 = 3125 ticks
1272        let unit = t.ticks();
1273        let half = unit.quot_rem(&<Ticks as TickInt>::from_u64(2)).0; // 1562, since 3125 is odd
1274
1275        let mk = |mult: u64, extra: &Ticks| {
1276            I::from_ticks(
1277                unit.try_mul(&<Ticks as TickInt>::from_u64(mult))
1278                    .unwrap()
1279                    .try_add(extra)
1280                    .unwrap(),
1281            )
1282            .unwrap()
1283        };
1284
1285        // Below the midpoint (3125 is odd, so 1562 < half of 3125).
1286        let below = mk(2, &half);
1287        assert_eq!(below.round_to(t, Rounding::Trunc).unwrap(), mk(2, &<Ticks as TickInt>::zero()));
1288        assert_eq!(below.round_to(t, Rounding::Ceil).unwrap(), mk(3, &<Ticks as TickInt>::zero()));
1289        assert_eq!(
1290            below.round_to(t, Rounding::HalfEven).unwrap(),
1291            mk(2, &<Ticks as TickInt>::zero())
1292        );
1293
1294        // Just above the midpoint.
1295        let above = mk(2, &half.try_add(&<Ticks as TickInt>::one()).unwrap());
1296        assert_eq!(
1297            above.round_to(t, Rounding::HalfEven).unwrap(),
1298            mk(3, &<Ticks as TickInt>::zero())
1299        );
1300
1301        // An exact tie needs an even-sized tier; use ticks with a tier of 5^0's
1302        // neighbour instead: 2 ticks is not a tier, so exercise the tie through
1303        // Window::midpoint, which is where ties actually arise.
1304        let w = Window::new(at(0), at(3)).unwrap();
1305        assert_eq!(w.midpoint(Rounding::Trunc).unwrap(), at(1));
1306        assert_eq!(w.midpoint(Rounding::Ceil).unwrap(), at(2));
1307        assert_eq!(w.midpoint(Rounding::HalfUp).unwrap(), at(2));
1308        // lo + half = 1, which is odd, so half-even rounds up to 2.
1309        assert_eq!(w.midpoint(Rounding::HalfEven).unwrap(), at(2));
1310        // An exact midpoint is returned unchanged by every mode.
1311        let w2 = Window::new(at(0), at(4)).unwrap();
1312        for m in [Rounding::Trunc, Rounding::Ceil, Rounding::HalfEven, Rounding::HalfUp] {
1313            assert_eq!(w2.midpoint(m).unwrap(), at(2));
1314        }
1315    }
1316
1317    // ---- Rule T: truncation is uncertainty ----
1318
1319    #[test]
1320    fn tier_precision_denotes_a_closed_interval() {
1321        let t = Tier::BEAT;
1322        let v = at(12_345);
1323        let w = v.window_at(Precision::Tier(t)).unwrap();
1324        assert_eq!(w.lo(), &v.floor_to(t));
1325        // [floor, floor + 5^e - 1] — inclusive, so width is 5^e - 1.
1326        assert_eq!(
1327            w.width().ticks(),
1328            &t.ticks().try_sub(&<Ticks as TickInt>::one()).unwrap()
1329        );
1330        assert!(w.contains(&v));
1331        assert!(!w.is_exact());
1332
1333        // Tick precision is the degenerate window.
1334        let e = v.window_at(Precision::Tick).unwrap();
1335        assert!(e.is_exact());
1336        assert_eq!(e.width(), Delta::zero());
1337        assert_eq!(e.lo(), e.hi());
1338        // ...as is an explicit tick tier.
1339        assert!(v.window_at(Precision::Tier(Tier::TICK)).unwrap().is_exact());
1340    }
1341
1342    #[test]
1343    fn comparison_across_precision_can_be_indeterminate() {
1344        let t = Tier::BEAT;
1345        let a = at(10).window_at(Precision::Tier(t)).unwrap();
1346        let b = at(20).window_at(Precision::Tier(t)).unwrap();
1347        // Both truncate into the same beat, so their order is undetermined.
1348        assert_eq!(a.compare(&b), IntervalOrdering::Indeterminate);
1349        assert_eq!(a.try_compare(&b).unwrap_err().code, Code::E0023);
1350
1351        // Windows a whole tier apart are determinate.
1352        let far = I::from_ticks(t.ticks().try_mul(&<Ticks as TickInt>::from_u64(5)).unwrap())
1353            .unwrap()
1354            .window_at(Precision::Tier(t))
1355            .unwrap();
1356        assert_eq!(a.compare(&far), IntervalOrdering::Before);
1357        assert_eq!(far.compare(&a), IntervalOrdering::After);
1358        assert_eq!(a.try_compare(&far).unwrap(), Ordering::Less);
1359
1360        // Two exact windows at the same tick compare equal.
1361        let x = Window::exact(at(7));
1362        assert_eq!(x.compare(&Window::exact(at(7))), IntervalOrdering::EqualExact);
1363    }
1364
1365    // ---- Rule U: interval arithmetic, no silent collapse ----
1366
1367    #[test]
1368    fn window_arithmetic_is_interval_arithmetic() {
1369        let w = Window::new(at(10), at(20)).unwrap();
1370        let s = w.checked_add(&d(5)).unwrap();
1371        assert_eq!((s.lo(), s.hi()), (&at(15), &at(25)));
1372        // Translation preserves width exactly.
1373        assert_eq!(s.width(), w.width());
1374
1375        assert_eq!(Window::new(at(20), at(10)).unwrap_err().code, Code::E0022);
1376
1377        let (wide, clipped) = w.widen(&d(5)).unwrap();
1378        assert_eq!((wide.lo(), wide.hi()), (&at(5), &at(25)));
1379        assert!(!clipped);
1380        // Widening past the datum clips at zero and says so (Rule Z).
1381        let (wide2, clipped2) = w.widen(&d(50)).unwrap();
1382        assert_eq!(wide2.lo(), &I::zero());
1383        assert!(clipped2);
1384
1385        let other = Window::new(at(15), at(30)).unwrap();
1386        assert!(w.overlaps(&other));
1387        assert_eq!(w.hull(&other), Window::new(at(10), at(30)).unwrap());
1388        assert_eq!(w.intersect(&other), Some(Window::new(at(15), at(20)).unwrap()));
1389        assert_eq!(w.intersect(&Window::new(at(40), at(50)).unwrap()), None);
1390    }
1391
1392    #[test]
1393    fn signed_window_is_inert() {
1394        // Rule Q.3: the runtime half of the guarantee. The compile-time half is
1395        // `tests/compile_fail/signed_window_*.rs`, which §21.3-3 requires.
1396        let claim = UC1::big_bang_claim();
1397        assert_eq!(claim.lo().sign(), Sign::Negative);
1398        assert_eq!(claim.hi().sign(), Sign::Positive);
1399        // It reports, and that is all it does.
1400        #[cfg(feature = "alloc")]
1401        assert!(claim.describe().contains("ticks"));
1402    }
1403
1404    // ---- Rule B: canonical binary ----
1405
1406    #[test]
1407    fn canonical_binary_is_64_bytes_and_round_trips() {
1408        for n in [0u64, 1, 255, 256, 65_535, u64::MAX] {
1409            let v = at(n);
1410            let b = v.to_bytes();
1411            assert_eq!(b.len(), 64);
1412            assert_eq!(I::from_bytes(&b).unwrap(), v);
1413        }
1414        assert_eq!(I::zero().to_bytes(), [0u8; 64]);
1415        let max = I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap();
1416        assert_eq!(max.to_bytes(), [0xffu8; 64]);
1417    }
1418
1419    #[test]
1420    fn byte_order_is_chronological_order() {
1421        // Rule S: lexicographic order equals chronological order for the binary
1422        // form, which is what makes it usable directly as a database key.
1423        let mut vals: Vec<I> = (0..64u64).map(|i| at(i * 2_654_435_761)).collect();
1424        vals.push(at(0));
1425        vals.push(I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap());
1426        vals.sort();
1427        let mut bytes: Vec<[u8; 64]> = vals.iter().map(|v| v.to_bytes()).collect();
1428        let numeric = bytes.clone();
1429        bytes.sort();
1430        assert_eq!(bytes, numeric, "byte order diverges from numeric order");
1431    }
1432
1433    // ---- Rule P: rebase reports the shift ----
1434
1435    #[test]
1436    fn rebase_is_identity_within_a_profile() {
1437        let v = at(1_000_000);
1438        let (out, shift) = v.rebase::<UC1>().unwrap();
1439        assert_eq!(out, v);
1440        assert!(shift.is_zero());
1441    }
1442
1443    // ---- Delta ----
1444
1445    #[test]
1446    fn delta_arithmetic() {
1447        assert_eq!(d(3).checked_add(&d(4)).unwrap(), d(7));
1448        assert_eq!(d(3).checked_sub(&d(4)).unwrap_err().code, Code::E0020);
1449        assert_eq!(d(12).mul_u64(3).unwrap(), d(36));
1450        assert_eq!(d(12).div_u64(5).unwrap(), d(2));
1451        assert_eq!(d(12).divmod(&d(5)).unwrap(), (d(2), d(2)));
1452        assert_eq!(d(1).div_u64(0).unwrap_err().code, Code::E0021);
1453        assert!(Delta::zero().is_zero());
1454        assert_eq!(Delta::one_tick(), d(1));
1455    }
1456
1457    #[test]
1458    fn delta_tier_of_finds_the_largest_fitting_tier() {
1459        // One beat exactly.
1460        let beat = Delta::from_ticks(Tier::BEAT.ticks());
1461        assert_eq!(beat.tier_of(), Some(Tier::BEAT));
1462        // One tick under a beat lands on the tier below.
1463        let just_under = beat.checked_sub(&Delta::one_tick()).unwrap();
1464        assert_eq!(just_under.tier_of(), Some(Tier::new(-1).unwrap()));
1465        // One tick is the tick tier; zero has no tier.
1466        assert_eq!(Delta::one_tick().tier_of(), Some(Tier::TICK));
1467        assert_eq!(Delta::zero().tier_of(), None);
1468        // Whole-tier counts come back exactly.
1469        let three_beats = Delta::from_tier(Tier::BEAT, 3).unwrap();
1470        assert_eq!(three_beats.in_tier(Tier::BEAT).0, <Ticks as TickInt>::from_u64(3));
1471        assert!(three_beats.in_tier(Tier::BEAT).1.is_zero_ticks());
1472    }
1473
1474    // ---- group extraction ----
1475
1476    #[test]
1477    fn tier_values_are_base_5_groups() {
1478        // Build a value with known group digits: 2 beats + 3 arcs.
1479        let v = I::from_ticks(
1480            Tier::BEAT
1481                .ticks()
1482                .try_mul(&<Ticks as TickInt>::from_u64(2))
1483                .unwrap()
1484                .try_add(
1485                    &Tier::ARC
1486                        .ticks()
1487                        .try_mul(&<Ticks as TickInt>::from_u64(3))
1488                        .unwrap(),
1489                )
1490                .unwrap(),
1491        )
1492        .unwrap();
1493        assert_eq!(v.tier_value(Tier::BEAT), 2);
1494        assert_eq!(v.tier_value(Tier::ARC), 3);
1495        assert_eq!(v.tier_value(Tier::SWEEP), 0);
1496        // Every group is in range (Rule G / UCAL-E0004).
1497        for t in Tier::all_ascending() {
1498            assert!(v.tier_value(t) < GROUP_BASE);
1499        }
1500    }
1501
1502    #[cfg(feature = "alloc")]
1503    #[test]
1504    fn groups_descend_and_reassemble() {
1505        let v = at(987_654_321);
1506        let gs = v.groups(Tier::BEAT, Tier::TICK).unwrap();
1507        assert_eq!(gs.len(), 13); // T0 down to T-12
1508        // §21.1: tier decomposition reassembles the original value.
1509        let mut acc = <Ticks as TickInt>::zero();
1510        let base = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
1511        for g in &gs {
1512            acc = acc
1513                .try_mul(&base)
1514                .unwrap()
1515                .try_add(&<Ticks as TickInt>::from_u64(*g as u64))
1516                .unwrap();
1517        }
1518        assert_eq!(&acc, v.ticks());
1519        // An ascending range is rejected rather than silently reversed.
1520        assert_eq!(
1521            v.groups(Tier::TICK, Tier::BEAT).unwrap_err().code,
1522            Code::E0006
1523        );
1524    }
1525}
1526
1527#[cfg(test)]
1528mod rule_u_tests {
1529    use super::*;
1530    use crate::profile::UC1;
1531
1532    type I = Instant<UC1>;
1533
1534    fn at(n: u64) -> I {
1535        I::from_u64(n).unwrap()
1536    }
1537    fn d(n: u64) -> Delta {
1538        Delta::from_u64(n)
1539    }
1540    fn w(lo: u64, hi: u64) -> Window<UC1> {
1541        Window::new(at(lo), at(hi)).unwrap()
1542    }
1543    fn sp(lo: u64, hi: u64) -> Span {
1544        Span::new(d(lo), d(hi)).unwrap()
1545    }
1546
1547    // ---- Span ----
1548
1549    #[test]
1550    fn span_is_an_interval_of_magnitudes() {
1551        let s = sp(10, 20);
1552        assert_eq!(s.uncertainty(), d(10));
1553        assert!(!s.is_exact());
1554        assert!(Span::exact(d(7)).is_exact());
1555        assert_eq!(Span::exact(d(7)).uncertainty(), Delta::zero());
1556        assert!(Span::zero().is_exact());
1557        assert_eq!(Span::new(d(20), d(10)).unwrap_err().code, Code::E0022);
1558    }
1559
1560    #[test]
1561    fn span_addition_is_interval_addition() {
1562        // Rule U: lo combines with lo, hi with hi.
1563        let a = sp(10, 20);
1564        let b = sp(1, 5);
1565        let s = a.checked_add(&b).unwrap();
1566        assert_eq!((s.lo(), s.hi()), (&d(11), &d(25)));
1567        // Uncertainty accumulates; it never shrinks.
1568        assert_eq!(s.uncertainty(), d(14));
1569        assert!(s.uncertainty() >= a.uncertainty());
1570        assert!(s.uncertainty() >= b.uncertainty());
1571    }
1572
1573    #[test]
1574    fn span_subtraction_clamps_at_zero_and_says_so() {
1575        // [10,20] - [1,5] = [5,19]
1576        let (s, clamped) = sp(10, 20).checked_sub(&sp(1, 5)).unwrap();
1577        assert_eq!((s.lo(), s.hi()), (&d(5), &d(19)));
1578        assert!(!clamped);
1579        // Overlapping spans genuinely admit a difference of zero.
1580        let (s, clamped) = sp(10, 20).checked_sub(&sp(15, 25)).unwrap();
1581        assert_eq!(s.lo(), &Delta::zero());
1582        assert_eq!(s.hi(), &d(5));
1583        assert!(clamped, "the clamp must be reported, not hidden");
1584        // Wholly smaller is still an error: the upper bound cannot go negative.
1585        assert_eq!(
1586            sp(1, 2).checked_sub(&sp(10, 20)).unwrap_err().code,
1587            Code::E0020
1588        );
1589    }
1590
1591    #[test]
1592    fn span_midpoint_must_be_asked_for() {
1593        let s = sp(0, 3);
1594        assert_eq!(s.midpoint(Rounding::Trunc).unwrap(), d(1));
1595        assert_eq!(s.midpoint(Rounding::Ceil).unwrap(), d(2));
1596        assert_eq!(s.midpoint(Rounding::HalfUp).unwrap(), d(2));
1597        assert_eq!(sp(0, 4).midpoint(Rounding::Trunc).unwrap(), d(2));
1598    }
1599
1600    // ---- Window x Span, the Rule J.2 propagation path ----
1601
1602    #[test]
1603    fn uncertain_instant_plus_uncertain_duration_widens() {
1604        // The operation a derived calendar performs: an anchor known to a window,
1605        // displaced by an elapsed time known to a span.
1606        let anchor = w(100, 110); // 10 ticks of anchor uncertainty
1607        let elapsed = sp(1000, 1005); // 5 ticks of parameter uncertainty
1608        let out = anchor.checked_add_span(&elapsed).unwrap();
1609        assert_eq!((out.lo(), out.hi()), (&at(1100), &at(1115)));
1610        // Rule U: the result cannot be narrower than either input.
1611        assert_eq!(out.width(), d(15));
1612        assert!(out.width() >= anchor.width());
1613        assert!(out.width() >= elapsed.uncertainty());
1614    }
1615
1616    #[test]
1617    fn subtracting_a_span_is_outward_and_strict_at_the_datum() {
1618        let out = w(1000, 1010).checked_sub_span(&sp(100, 200)).unwrap();
1619        // [1000 - 200, 1010 - 100]
1620        assert_eq!((out.lo(), out.hi()), (&at(800), &at(910)));
1621        assert!(out.width() > w(1000, 1010).width());
1622        // Strict rather than clamped: a silently clipped window is not the
1623        // interval that was asked for.
1624        assert_eq!(
1625            w(10, 20).checked_sub_span(&sp(0, 100)).unwrap_err().code,
1626            Code::E0020
1627        );
1628    }
1629
1630    #[test]
1631    fn elapsed_between_windows_is_a_span() {
1632        let earlier = w(100, 110);
1633        let later = w(1000, 1010);
1634        let (s, clamped) = later.since_window(&earlier).unwrap();
1635        // [1000 - 110, 1010 - 100]
1636        assert_eq!((s.lo(), s.hi()), (&d(890), &d(910)));
1637        assert!(!clamped);
1638        // Overlapping windows admit zero elapsed time.
1639        let (s, clamped) = w(100, 200).since_window(&w(150, 250)).unwrap();
1640        assert_eq!(s.lo(), &Delta::zero());
1641        assert_eq!(s.hi(), &d(50));
1642        assert!(clamped);
1643        // Wholly earlier is Rule Z applied to intervals.
1644        assert_eq!(
1645            w(10, 20).since_window(&w(100, 200)).unwrap_err().code,
1646            Code::E0020
1647        );
1648    }
1649
1650    #[test]
1651    fn round_trip_through_span_preserves_containment() {
1652        // The property that makes the propagation sound: displacing a window and
1653        // then undoing it must not lose the original.
1654        let start = w(10_000, 10_050);
1655        let s = sp(300, 320);
1656        let moved = start.checked_add_span(&s).unwrap();
1657        let back = moved.checked_sub_span(&s).unwrap();
1658        assert!(back.contains(start.lo()));
1659        assert!(back.contains(start.hi()));
1660        // ...and is no narrower, because uncertainty is never recovered.
1661        assert!(back.width() >= start.width());
1662    }
1663
1664    // ---- Stated: Rule T comparison ----
1665
1666    #[test]
1667    fn stated_comparison_uses_interval_semantics() {
1668        let a = Stated::new(at(10).floor_to(Tier::BEAT), Precision::Tier(Tier::BEAT));
1669        let b = Stated::new(at(20).floor_to(Tier::BEAT), Precision::Tier(Tier::BEAT));
1670        // Both fall in the same beat, so their order is genuinely undetermined.
1671        assert_eq!(a.compare(&b).unwrap(), IntervalOrdering::Indeterminate);
1672        assert_eq!(a.try_compare(&b).unwrap_err().code, Code::E0023);
1673
1674        // Exact statements at the same tick compare equal.
1675        let x = Stated::exact(at(7));
1676        assert_eq!(
1677            x.compare(&Stated::exact(at(7))).unwrap(),
1678            IntervalOrdering::EqualExact
1679        );
1680        assert_eq!(x.try_compare(&Stated::exact(at(8))).unwrap(), Ordering::Less);
1681        assert!(x.is_exact());
1682    }
1683
1684    #[test]
1685    fn stated_comparison_across_unequal_precision() {
1686        // A coarse statement and a fine one, where the fine one lies inside the
1687        // coarse one's window: indeterminate, not equal and not ordered.
1688        let coarse = Stated::new(at(0).floor_to(Tier::BEAT), Precision::Tier(Tier::BEAT));
1689        let fine = Stated::exact(at(5));
1690        assert_eq!(coarse.compare(&fine).unwrap(), IntervalOrdering::Indeterminate);
1691        assert_eq!(coarse.try_compare(&fine).unwrap_err().code, Code::E0023);
1692
1693        // A fine statement outside the coarse window is determinate.
1694        let far = Stated::exact(
1695            I::from_ticks(
1696                Tier::BEAT
1697                    .ticks()
1698                    .try_mul(&<Ticks as TickInt>::from_u64(3))
1699                    .unwrap(),
1700            )
1701            .unwrap(),
1702        );
1703        assert_eq!(coarse.try_compare(&far).unwrap(), Ordering::Less);
1704        assert_eq!(far.try_compare(&coarse).unwrap(), Ordering::Greater);
1705    }
1706
1707    #[test]
1708    fn stated_cannot_be_refined_only_coarsened() {
1709        // F2 as an API constraint: no operation may invent precision.
1710        let s = Stated::new(at(1_000_000).floor_to(Tier::BEAT), Precision::Tier(Tier::BEAT));
1711        assert!(s.coarsen(Tier::ARC).is_ok());
1712        assert_eq!(s.coarsen(Tier::ARC).unwrap().precision(), Precision::Tier(Tier::ARC));
1713        // Refining is refused.
1714        assert_eq!(
1715            s.coarsen(Tier::new(-3).unwrap()).unwrap_err().code,
1716            Code::E0023
1717        );
1718        assert_eq!(s.coarsen(Tier::TICK).unwrap_err().code, Code::E0023);
1719        // Coarsening widens the window it denotes.
1720        assert!(s.coarsen(Tier::ARC).unwrap().window().unwrap().width() > s.window().unwrap().width());
1721    }
1722
1723    #[test]
1724    fn stated_equality_is_about_the_statement() {
1725        // Two statements that denote overlapping intervals are not "equal"; only
1726        // the same value at the same precision is.
1727        let a = Stated::new(at(0), Precision::Tier(Tier::BEAT));
1728        let b = Stated::new(at(0), Precision::Tier(Tier::ARC));
1729        assert_ne!(a, b);
1730        assert_eq!(a, Stated::new(at(0), Precision::Tier(Tier::BEAT)));
1731    }
1732
1733    // ---- Rule T near the domain ceiling ----
1734
1735    #[test]
1736    fn coarse_window_at_the_ceiling_is_clipped_to_the_domain() {
1737        // `floor + 5^e - 1` can run past domain_max. The interval is intersected
1738        // with the closed domain rather than failing: no representable instant
1739        // lies beyond the ceiling, so clamping stays a sound enclosure.
1740        let top = I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap();
1741        let t32 = Tier::new(crate::tier::K_MAX).unwrap();
1742        let win = top.window_at(Precision::Tier(t32)).unwrap();
1743        assert!(win.contains(&top));
1744        assert_eq!(win.hi(), &top);
1745        assert_eq!(win.lo(), &top.floor_to(t32));
1746        assert!(!win.is_exact());
1747    }
1748}