Skip to main content

ucal_core/
codec.rs

1//! Text codecs (§6, Appendix F) — the two forms of one value (Rule D).
2//!
3//! # What a printed form means
4//!
5//! §6.1 is explicit: *the last group's tier is the stated precision*. A rendering
6//! therefore denotes the closed interval `[v, v + 5^e - 1]` of Rule T, and a
7//! rendering that stops at T0 denotes a beat, not a tick.
8//!
9//! Appendix C reads the other way — it prints SI_EPOCH to T0 and annotates "all
10//! tiers below T0 are zero", treating omission as exactness. The two readings
11//! cannot both hold, and this implementation takes §6.1's, because the
12//! alternative *is* failure mode F2: if trailing omission meant "exact", then
13//! reading a T-5 form would zero-fill it to a tick, which is precision invented
14//! by zero-filling a truncated timestamp. See `spec/SPEC-DELTAS.md` D-A8.
15//!
16//! The practical consequence: **tick-exact text runs down to T-12.** For an
17//! instant at a whole SI second the last six groups are guaranteed zero by §2.4,
18//! which is exactly what tempts one to drop them — but dropping them changes what
19//! the string says.
20//!
21//! # How a form is anchored
22//!
23//! Neither form states which tier it starts at, so each needs an anchor, and they
24//! use different ones:
25//!
26//! - **Human form** anchors at the *bottom of the whole part*: the group
27//!   immediately before `:` is T0, and groups after it run T-1 downward (§6.4).
28//!   With no `:`, the last group is T0. This is what makes
29//!   `UC1 0031·0687·2481·2999·3108·2437` unambiguous.
30//! - **Digit form** anchors at the *top*: it always begins at T32, the highest
31//!   tier the domain holds. The group count then fixes the precision. This is
32//!   what lets D-9 call the digit form canonical for parse and sort, and what
33//!   satisfies Rule S — a fixed tier width is the only condition under which
34//!   lexicographic order on text equals chronological order.
35
36#[cfg(feature = "alloc")]
37use alloc::string::{String, ToString};
38#[cfg(feature = "alloc")]
39use alloc::vec::Vec;
40
41use crate::error::{Code, Result, TimeError};
42use crate::tier::Tier;
43#[cfg(feature = "alloc")]
44use crate::backend::{TickInt, Ticks};
45#[cfg(feature = "alloc")]
46use crate::tier::GROUP_BASE;
47use crate::locale::LocaleId;
48#[cfg(feature = "alloc")]
49use crate::profile::Profile;
50#[cfg(feature = "alloc")]
51use crate::tier::{K_MAX, K_MIN, TIER_COUNT};
52use crate::locale;
53
54#[doc(inline)]
55pub use crate::locale::LocaleId as LocaleIdAlias;
56use crate::value::Precision;
57#[cfg(feature = "alloc")]
58use crate::value::{Instant, Window};
59
60/// §13 names a distinct parse error type; it is the crate's error type, because
61/// the Appendix E codes are the contract.
62pub type ParseError = TimeError;
63
64/// The default group separator, U+00B7 MIDDLE DOT (§6.3, D-10).
65pub const SEP: char = '·';
66
67/// Always accepted on input, for shell-hostile contexts (§6.3, D-10).
68pub const ALT_SEP: char = '.';
69
70/// Introduces the sub-beat part (§6.4).
71pub const SUB_SEP: char = ':';
72
73/// Decimal digits per group in the human form: `3124` is four characters.
74pub const HUMAN_GROUP_WIDTH: usize = 4;
75
76/// Base-5 digits per group in the digit form (Rule G).
77pub const DIGIT5_GROUP_WIDTH: usize = 5;
78
79/// Which of the two forms (Rule D), plus the parseable-but-not-canonical named
80/// form of §6.5.
81#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
82pub enum Form {
83    /// Decimal group values, most significant tier first. Tagged `UC1`.
84    #[default]
85    HumanGroups,
86    /// Base-5 digits, five per group. Tagged `UC1/5`. Canonical for parse and
87    /// sort (D-9).
88    Digit5,
89    /// `31 deep, 687 drift, ...`. Parseable, not canonical (§6.5).
90    Named,
91}
92
93/// Formatting context (§13).
94#[derive(Clone, Copy, PartialEq, Eq, Debug)]
95pub struct Fmt {
96    /// Which form to render.
97    pub form: Form,
98    /// Group separator. Must not be a decimal or base-5 digit (§6.3).
99    pub sep: char,
100    /// Sub-beat introducer.
101    pub sub_sep: char,
102    /// The tier to render down to. This *is* the stated precision.
103    pub precision: Precision,
104    /// Pad the high end to a fixed tier width, which is the only condition under
105    /// which text sorts chronologically (Rule S).
106    pub pad: bool,
107    /// Locale for the named form.
108    pub locale: LocaleId,
109}
110
111impl Default for Fmt {
112    fn default() -> Self {
113        Fmt {
114            form: Form::HumanGroups,
115            sep: SEP,
116            sub_sep: SUB_SEP,
117            precision: Precision::Tick,
118            pad: false,
119            locale: LocaleId::En,
120        }
121    }
122}
123
124impl Fmt {
125    /// Human form at tick precision.
126    pub fn human() -> Fmt {
127        Fmt::default()
128    }
129
130    /// Human form truncated to a tier. The result denotes a window (Rule T).
131    pub fn human_at(tier: Tier) -> Fmt {
132        Fmt {
133            precision: Precision::Tier(tier),
134            ..Fmt::default()
135        }
136    }
137
138    /// Canonical digit form: fixed width from T32, tick precision, sortable.
139    ///
140    /// Uses `.` rather than the `·` default, matching §6.2's printed example.
141    /// Both separators parse either form (§6.3), so this is presentation only.
142    pub fn digit5() -> Fmt {
143        Fmt {
144            form: Form::Digit5,
145            sep: ALT_SEP,
146            pad: true,
147            ..Fmt::default()
148        }
149    }
150
151    /// Named form.
152    pub fn named() -> Fmt {
153        Fmt {
154            form: Form::Named,
155            ..Fmt::default()
156        }
157    }
158
159    /// Validate the separator choices (§6.3).
160    ///
161    /// A separator that is also a digit would make the notation ambiguous, so
162    /// this is checked rather than assumed.
163    pub fn validate(&self) -> Result<()> {
164        for (c, what) in [(self.sep, "separator"), (self.sub_sep, "sub-separator")] {
165            if c.is_ascii_digit() {
166                return Err(TimeError::with_context(
167                    Code::E0001,
168                    "separator must not be a decimal or base-5 digit (§6.3)",
169                ));
170            }
171            let _ = what;
172        }
173        if self.sep == self.sub_sep {
174            return Err(TimeError::with_context(
175                Code::E0001,
176                "separator and sub-separator must differ",
177            ));
178        }
179        Ok(())
180    }
181
182    #[cfg(feature = "alloc")]
183    fn low_tier(&self) -> Tier {
184        self.precision.tier()
185    }
186}
187
188// ---------------------------------------------------------------------------
189// Appendix F — the group codec
190// ---------------------------------------------------------------------------
191
192/// Decimal group values for tiers `k_hi` down to `k_lo`, most significant first.
193///
194/// Appendix F: repeated `divmod` by `5^5 = 3125`, one group per step. Digit-by-
195/// digit division by 5 MUST NOT be used — it would take 221 steps at full width
196/// instead of 45.
197#[cfg(feature = "alloc")]
198pub fn encode_groups(t: &Ticks, k_hi: Tier, k_lo: Tier) -> Result<Vec<u16>> {
199    if k_hi < k_lo {
200        return Err(TimeError::with_context(
201            Code::E0006,
202            "group range must descend",
203        ));
204    }
205    let gb = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
206    let (mut x, _) = t.quot_rem(&k_lo.ticks());
207    let n = (k_hi.index() as isize - k_lo.index() as isize + 1) as usize;
208    let mut out = Vec::with_capacity(n);
209    for _ in 0..n {
210        let (q, r) = x.quot_rem(&gb);
211        out.push(group_to_u16(&r));
212        x = q;
213    }
214    out.reverse();
215    Ok(out)
216}
217
218/// Reassemble a tick count from group values (Appendix F).
219#[cfg(feature = "alloc")]
220pub fn decode_groups(groups: &[u16], k_lo: Tier) -> Result<Ticks> {
221    let gb = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
222    let mut acc = <Ticks as TickInt>::zero();
223    for g in groups {
224        if *g >= GROUP_BASE {
225            return Err(TimeError::new(Code::E0004));
226        }
227        acc = acc
228            .try_mul(&gb)
229            .and_then(|v| v.try_add(&<Ticks as TickInt>::from_u64(*g as u64)))
230            .ok_or(TimeError::new(Code::E0021))?;
231    }
232    acc.try_mul(&k_lo.ticks())
233        .ok_or(TimeError::new(Code::E0021))
234}
235
236/// A group value is always below 3125, so it fits `u16`. Converted through the
237/// canonical bytes so no backend-specific cast is needed.
238#[cfg(feature = "alloc")]
239fn group_to_u16(r: &Ticks) -> u16 {
240    let b = r.to_canonical_bytes();
241    u16::from_be_bytes([b[b.len() - 2], b[b.len() - 1]])
242}
243
244/// The five base-5 digits of a group, most significant first (Appendix F).
245pub fn digit5_of_group(g: u16) -> [u8; DIGIT5_GROUP_WIDTH] {
246    let mut out = [b'0'; DIGIT5_GROUP_WIDTH];
247    let mut v = g;
248    for i in (0..DIGIT5_GROUP_WIDTH).rev() {
249        out[i] = b'0' + (v % 5) as u8;
250        v /= 5;
251    }
252    out
253}
254
255/// Inverse of [`digit5_of_group`]. `UCAL-E0005` on a digit outside `0..=4`.
256pub fn group_of_digit5(s: &str) -> Result<u16> {
257    if s.len() != DIGIT5_GROUP_WIDTH {
258        return Err(TimeError::with_context(
259            Code::E0005,
260            "a base-5 group is exactly five digits",
261        ));
262    }
263    let mut v: u16 = 0;
264    for c in s.bytes() {
265        if !(b'0'..=b'4').contains(&c) {
266            return Err(TimeError::new(Code::E0005));
267        }
268        v = v * 5 + (c - b'0') as u16;
269    }
270    Ok(v)
271}
272
273// ---------------------------------------------------------------------------
274// Rendering
275// ---------------------------------------------------------------------------
276
277/// Render an instant (§6, Rule D).
278#[cfg(feature = "alloc")]
279pub fn render<P: Profile>(v: &Instant<P>, f: &Fmt) -> Result<String> {
280    f.validate()?;
281    let k_lo = f.low_tier();
282    match f.form {
283        Form::HumanGroups => render_human::<P>(v, f, k_lo),
284        Form::Digit5 => render_digit5::<P>(v, f, k_lo),
285        Form::Named => render_named::<P>(v, f, k_lo),
286    }
287}
288
289/// The highest tier that must be shown: the highest non-zero one, but never
290/// below T0, because the human form's anchor is the group before the `:`.
291#[cfg(feature = "alloc")]
292fn human_high_tier<P: Profile>(v: &Instant<P>, pad: bool) -> Tier {
293    if pad {
294        return Tier::new(K_MAX).expect("K_MAX is on the grid");
295    }
296    let mut hi = Tier::BEAT;
297    for k in (1..=K_MAX).rev() {
298        let t = Tier::new(k).expect("in range");
299        if v.tier_value(t) != 0 {
300            hi = t;
301            break;
302        }
303    }
304    hi
305}
306
307#[cfg(feature = "alloc")]
308fn render_human<P: Profile>(v: &Instant<P>, f: &Fmt, k_lo: Tier) -> Result<String> {
309    // The human form's only anchor is §6.4's sub-beat separator, which fixes the
310    // group before it as T0. A rendering that stopped above T0 would therefore be
311    // read back as though its last group *were* T0, silently changing the value
312    // by whole tiers. Rather than invent syntax the RFC does not define, the
313    // human form covers T0 and finer; the digit form (anchored at T32) and the
314    // named form (self-describing) cover the whole grid. See D-A8.
315    if k_lo.index() > 0 {
316        return Err(TimeError::with_context(
317            Code::E0006,
318            "the human form anchors at T0 and cannot state a coarser precision; \
319             use the digit form or the named form",
320        ));
321    }
322    let k_hi = human_high_tier(v, f.pad);
323    let whole_lo = Tier::BEAT;
324    let mut s = String::new();
325    s.push_str(P::TAG);
326    s.push(' ');
327
328    let whole = encode_groups(v.ticks(), k_hi, whole_lo)?;
329    push_joined(&mut s, &whole, f.sep, |g| {
330        let mut b = [b'0'; HUMAN_GROUP_WIDTH];
331        write_dec4(g, &mut b);
332        b
333    });
334
335    if k_lo.index() < 0 {
336        s.push(f.sub_sep);
337        let sub = encode_groups(
338            v.ticks(),
339            Tier::new(-1).expect("on the grid"),
340            k_lo,
341        )?;
342        push_joined(&mut s, &sub, f.sep, |g| {
343            let mut b = [b'0'; HUMAN_GROUP_WIDTH];
344            write_dec4(g, &mut b);
345            b
346        });
347    }
348    Ok(s)
349}
350
351#[cfg(feature = "alloc")]
352fn render_digit5<P: Profile>(v: &Instant<P>, f: &Fmt, k_lo: Tier) -> Result<String> {
353    // Anchored at the top: always begins at T32, so the group count fixes the
354    // precision and the width is constant for a given precision (Rule S).
355    let k_hi = Tier::new(K_MAX).expect("K_MAX is on the grid");
356    let groups = encode_groups(v.ticks(), k_hi, k_lo)?;
357    let mut s = String::new();
358    s.push_str(P::TAG);
359    s.push_str("/5 ");
360    push_joined(&mut s, &groups, f.sep, digit5_of_group);
361    Ok(s)
362}
363
364#[cfg(feature = "alloc")]
365fn render_named<P: Profile>(v: &Instant<P>, f: &Fmt, k_lo: Tier) -> Result<String> {
366    // When the requested precision is coarser than the value's own magnitude the
367    // value floors to zero at that tier, and the honest rendering is `0 drift` —
368    // not an empty string. Clamping the high end up to the precision keeps the
369    // stated tier present, which is what carries the precision (§6.1).
370    let k_hi = {
371        let h = human_high_tier(v, f.pad);
372        if h.index() < k_lo.index() {
373            k_lo
374        } else {
375            h
376        }
377    };
378    let mut parts: Vec<String> = Vec::new();
379    let mut k = k_hi.index();
380    while k >= k_lo.index() {
381        let t = Tier::new(k)?;
382        let g = v.tier_value(t);
383        // Zero groups are omitted except the last, which carries the precision.
384        if g != 0 || k == k_lo.index() {
385            // Rule N: a named tier prints its locale name, an unnamed one its
386            // T[k] form, which is accepted wherever a name is.
387            let mut p = g.to_string();
388            p.push(' ');
389            p.push_str(&locale::display(f.locale, t));
390            parts.push(p);
391        }
392        k -= 1;
393    }
394    let _ = f;
395    Ok(parts.join(", "))
396}
397
398#[cfg(feature = "alloc")]
399fn push_joined<F, const N: usize>(s: &mut String, groups: &[u16], sep: char, render_one: F)
400where
401    F: Fn(u16) -> [u8; N],
402{
403    for (i, g) in groups.iter().enumerate() {
404        if i > 0 {
405            s.push(sep);
406        }
407        let b = render_one(*g);
408        s.push_str(core::str::from_utf8(&b).expect("ascii digits"));
409    }
410}
411
412#[cfg(feature = "alloc")]
413fn write_dec4(g: u16, out: &mut [u8; HUMAN_GROUP_WIDTH]) {
414    let mut v = g;
415    for i in (0..HUMAN_GROUP_WIDTH).rev() {
416        out[i] = b'0' + (v % 10) as u8;
417        v /= 10;
418    }
419}
420
421// ---------------------------------------------------------------------------
422// Parsing
423// ---------------------------------------------------------------------------
424
425/// Parse either text form, returning the value **and its stated precision**.
426///
427/// Rule T: a truncated notation never yields a bare tick-precision instant. The
428/// returned [`Precision`] is the tier of the last group, and
429/// [`parse_window`] turns it into the interval the notation actually denotes.
430#[cfg(feature = "alloc")]
431pub fn parse<P: Profile>(s: &str, ctx: &Fmt) -> core::result::Result<(Instant<P>, Precision), ParseError> {
432    let s = s.trim();
433    let (tag, rest) = split_tag(s)?;
434
435    let digit_tag = {
436        let mut t = String::from(P::TAG);
437        t.push_str("/5");
438        t
439    };
440    if tag == digit_tag {
441        parse_digit5::<P>(rest, ctx)
442    } else if tag == P::TAG {
443        // The named form has no digits-only groups; detect it by its separator.
444        if rest.contains(',') || rest.bytes().any(|b| b.is_ascii_alphabetic()) {
445            parse_named::<P>(rest, ctx)
446        } else {
447            parse_human::<P>(rest, ctx)
448        }
449    } else {
450        Err(TimeError::new(Code::E0002))
451    }
452}
453
454/// Parse and immediately materialise the interval the notation denotes (Rule T).
455#[cfg(feature = "alloc")]
456pub fn parse_window<P: Profile>(s: &str, ctx: &Fmt) -> Result<Window<P>> {
457    let (v, p) = parse::<P>(s, ctx)?;
458    v.window_at(p)
459}
460
461#[cfg(feature = "alloc")]
462fn split_tag(s: &str) -> core::result::Result<(&str, &str), ParseError> {
463    match s.split_once(char::is_whitespace) {
464        None => Err(TimeError::with_context(
465            Code::E0001,
466            "missing profile tag; every serialised form carries one (Rule P)",
467        )),
468        Some((tag, rest)) => Ok((tag, rest.trim())),
469    }
470}
471
472#[cfg(feature = "alloc")]
473fn is_sep(c: char, ctx: &Fmt) -> bool {
474    c == ctx.sep || c == ALT_SEP || c == SEP
475}
476
477#[cfg(feature = "alloc")]
478fn split_groups<'a>(s: &'a str, ctx: &Fmt) -> Vec<&'a str> {
479    s.split(|c| is_sep(c, ctx))
480        .filter(|p| !p.is_empty())
481        .collect()
482}
483
484#[cfg(feature = "alloc")]
485fn parse_human<P: Profile>(
486    rest: &str,
487    ctx: &Fmt,
488) -> core::result::Result<(Instant<P>, Precision), ParseError> {
489    let (whole_str, sub_str) = match rest.split_once(ctx.sub_sep) {
490        None => (rest, None),
491        Some((w, s)) => (w, Some(s)),
492    };
493
494    let whole_parts = split_groups(whole_str, ctx);
495    if whole_parts.is_empty() {
496        return Err(TimeError::new(Code::E0001));
497    }
498    let mut groups: Vec<u16> = Vec::new();
499    for p in &whole_parts {
500        groups.push(parse_human_group(p)?);
501    }
502    // Anchor: the last whole group is T0.
503    let k_hi = whole_parts.len() as i32 - 1;
504    if k_hi > K_MAX as i32 {
505        return Err(TimeError::with_context(
506            Code::E0006,
507            "more whole groups than the tier grid holds",
508        ));
509    }
510
511    let mut k_lo = 0i32;
512    if let Some(sub) = sub_str {
513        let sub_parts = split_groups(sub, ctx);
514        if sub_parts.is_empty() {
515            return Err(TimeError::with_context(
516                Code::E0001,
517                "sub-beat separator with no groups after it",
518            ));
519        }
520        for p in &sub_parts {
521            groups.push(parse_human_group(p)?);
522        }
523        k_lo = -(sub_parts.len() as i32);
524        if k_lo < K_MIN as i32 {
525            return Err(TimeError::with_context(
526                Code::E0006,
527                "more sub-beat groups than the tier grid holds",
528            ));
529        }
530    }
531
532    let low = Tier::new(k_lo as i8)?;
533    let ticks = decode_groups(&groups, low)?;
534    let v = Instant::<P>::from_ticks(ticks)?;
535    Ok((v, precision_of(low)))
536}
537
538#[cfg(feature = "alloc")]
539fn parse_human_group(p: &str) -> core::result::Result<u16, ParseError> {
540    // A five-character group in a decimal-tagged string is the digit form:
541    // the two forms must not be mixed (Rule D, UCAL-E0003).
542    if p.len() == DIGIT5_GROUP_WIDTH && p.bytes().all(|b| (b'0'..=b'4').contains(&b)) {
543        return Err(TimeError::with_context(
544            Code::E0003,
545            "base-5 group in a decimal-tagged string",
546        ));
547    }
548    if p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()) {
549        return Err(TimeError::new(Code::E0001));
550    }
551    // Parse before checking the width, so that an out-of-range group reports
552    // E0004 ("group value out of range") rather than the vaguer E0001. Extra
553    // leading zeros are tolerated on input; §6.1's four-digit padding is a
554    // rendering rule, not a parsing one.
555    let v: u32 = p.parse().map_err(|_| TimeError::new(Code::E0004))?;
556    if v >= GROUP_BASE as u32 {
557        return Err(TimeError::new(Code::E0004));
558    }
559    Ok(v as u16)
560}
561
562#[cfg(feature = "alloc")]
563fn parse_digit5<P: Profile>(
564    rest: &str,
565    ctx: &Fmt,
566) -> core::result::Result<(Instant<P>, Precision), ParseError> {
567    if rest.contains(ctx.sub_sep) {
568        return Err(TimeError::with_context(
569            Code::E0003,
570            "the digit form is anchored at T32 and takes no sub-beat separator",
571        ));
572    }
573    let parts = split_groups(rest, ctx);
574    if parts.is_empty() {
575        return Err(TimeError::new(Code::E0001));
576    }
577    if parts.len() > TIER_COUNT {
578        return Err(TimeError::with_context(
579            Code::E0006,
580            "more groups than the tier grid holds",
581        ));
582    }
583    let mut groups = Vec::with_capacity(parts.len());
584    for p in &parts {
585        if p.len() == HUMAN_GROUP_WIDTH {
586            return Err(TimeError::with_context(
587                Code::E0003,
588                "decimal group in a base-5-tagged string",
589            ));
590        }
591        groups.push(group_of_digit5(p)?);
592    }
593    // Anchor: the first group is T32, so the count fixes the precision.
594    let k_lo = K_MAX as i32 - (parts.len() as i32 - 1);
595    let low = Tier::new(k_lo as i8)?;
596    let ticks = decode_groups(&groups, low)?;
597    let v = Instant::<P>::from_ticks(ticks)?;
598    Ok((v, precision_of(low)))
599}
600
601#[cfg(feature = "alloc")]
602fn parse_named<P: Profile>(
603    rest: &str,
604    _ctx: &Fmt,
605) -> core::result::Result<(Instant<P>, Precision), ParseError> {
606    let mut acc = <Ticks as TickInt>::zero();
607    let mut lowest: Option<Tier> = None;
608    let mut prev: Option<i8> = None;
609    for term in rest.split(',') {
610        let term = term.trim();
611        if term.is_empty() {
612            continue;
613        }
614        let (n_str, name) = term
615            .split_once(char::is_whitespace)
616            .ok_or(TimeError::with_context(Code::E0001, "expected `<count> <tier>`"))?;
617        let count: u64 = n_str
618            .trim()
619            .parse()
620            .map_err(|_| TimeError::new(Code::E0001))?;
621        if count >= GROUP_BASE as u64 {
622            return Err(TimeError::new(Code::E0004));
623        }
624        let tier = resolve_tier_name(name.trim())?;
625        // Descending order is required so that the precision is the last term.
626        if let Some(p) = prev {
627            if tier.index() >= p {
628                return Err(TimeError::with_context(
629                    Code::E0006,
630                    "named terms must descend",
631                ));
632            }
633        }
634        prev = Some(tier.index());
635        lowest = Some(tier);
636        let add = tier
637            .ticks()
638            .try_mul(&<Ticks as TickInt>::from_u64(count))
639            .ok_or(TimeError::new(Code::E0021))?;
640        acc = acc.try_add(&add).ok_or(TimeError::new(Code::E0021))?;
641    }
642    let low = lowest.ok_or(TimeError::new(Code::E0001))?;
643    let v = Instant::<P>::from_ticks(acc)?;
644    Ok((v, precision_of(low)))
645}
646
647/// Resolve a tier by name in the default locale, `T<k>`, or `5^e` (Rule N).
648pub fn resolve_tier_name(s: &str) -> Result<Tier> {
649    locale::resolve(LocaleId::default(), s)
650}
651
652/// Resolve a tier by name in a stated locale (Rule N).
653pub fn resolve_tier_name_in(loc: LocaleId, s: &str) -> Result<Tier> {
654    locale::resolve(loc, s)
655}
656
657#[cfg(feature = "alloc")]
658fn precision_of(t: Tier) -> Precision {
659    if t.is_tick() {
660        Precision::Tick
661    } else {
662        Precision::Tier(t)
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::profile::UC1;
670    use crate::value::Rounding;
671    use alloc::vec;
672
673    type I = Instant<UC1>;
674
675    fn at(n: u64) -> I {
676        I::from_u64(n).unwrap()
677    }
678
679    /// A deterministic xorshift, so the "full domain" sweep is reproducible and
680    /// needs no float and no rand dependency.
681    struct Rng(u64);
682    impl Rng {
683        fn next_u64(&mut self) -> u64 {
684            let mut x = self.0;
685            x ^= x << 13;
686            x ^= x >> 7;
687            x ^= x << 17;
688            self.0 = x;
689            x
690        }
691        /// A value spread across the whole 512-bit domain.
692        fn next_ticks(&mut self) -> Ticks {
693            let mut bytes = [0u8; 64];
694            for chunk in bytes.chunks_mut(8) {
695                chunk.copy_from_slice(&self.next_u64().to_be_bytes());
696            }
697            <Ticks as TickInt>::from_canonical_bytes(&bytes).unwrap()
698        }
699    }
700
701    fn sample_values() -> Vec<Ticks> {
702        let mut v = vec![
703            <Ticks as TickInt>::zero(),
704            <Ticks as TickInt>::one(),
705            <Ticks as TickInt>::domain_max(),
706            UC1::origin_offset(),
707            UC1::beat(),
708        ];
709        // Every tier boundary, and one tick either side of it.
710        for t in Tier::all_ascending() {
711            let b = t.ticks();
712            v.push(b.clone());
713            if let Some(x) = b.try_sub(&<Ticks as TickInt>::one()) {
714                v.push(x);
715            }
716            if let Some(x) = b.try_add(&<Ticks as TickInt>::one()) {
717                v.push(x);
718            }
719        }
720        // And a spread across the domain.
721        let mut rng = Rng(0x5EED_1234_9ABC_DEF0);
722        for _ in 0..256 {
723            v.push(rng.next_ticks());
724        }
725        v
726    }
727
728    // ---- Appendix F ----
729
730    #[test]
731    fn full_width_encode_takes_45_steps_not_44() {
732        // Appendix F and §13.1 both say 44. 2^512-1 has 221 base-5 digits, and
733        // T32's group of domain_max is non-zero, so the 45th group is needed.
734        // See spec/SPEC-DELTAS.md D-A7.
735        let m = <Ticks as TickInt>::domain_max();
736        let gs = encode_groups(&m, Tier::new(K_MAX).unwrap(), Tier::TICK).unwrap();
737        assert_eq!(gs.len(), 45);
738        assert_eq!(gs.len(), TIER_COUNT);
739        assert_ne!(gs[0], 0, "the top group must be load-bearing");
740        assert_eq!(gs[0], 2);
741        // 221 base-5 digits over 5 per group is 44.2, i.e. 45 groups.
742        assert_eq!(m.to_radix_string(5).len(), 221);
743    }
744
745    #[test]
746    fn group_codec_round_trips() {
747        for v in sample_values() {
748            let gs = encode_groups(&v, Tier::new(K_MAX).unwrap(), Tier::TICK).unwrap();
749            assert_eq!(gs.len(), TIER_COUNT);
750            assert!(gs.iter().all(|g| *g < GROUP_BASE));
751            let back = decode_groups(&gs, Tier::TICK).unwrap();
752            assert_eq!(back, v);
753        }
754    }
755
756    #[test]
757    fn digit5_group_codec_round_trips() {
758        for g in 0..GROUP_BASE {
759            let d = digit5_of_group(g);
760            let s = core::str::from_utf8(&d).unwrap();
761            assert_eq!(s.len(), 5);
762            assert!(s.bytes().all(|b| (b'0'..=b'4').contains(&b)));
763            assert_eq!(group_of_digit5(s).unwrap(), g);
764        }
765        assert_eq!(group_of_digit5("00005").unwrap_err().code, Code::E0005);
766        assert_eq!(group_of_digit5("0000").unwrap_err().code, Code::E0005);
767    }
768
769    // ---- Rule D: both forms round-trip, over the whole domain ----
770
771    #[test]
772    fn human_form_round_trips_over_the_domain() {
773        let f = Fmt::human();
774        for v in sample_values() {
775            let inst = I::from_ticks(v.clone()).unwrap();
776            let s = render(&inst, &f).unwrap();
777            let (back, p) = parse::<UC1>(&s, &f).unwrap();
778            assert_eq!(back, inst, "round trip failed for {s}");
779            assert_eq!(p, Precision::Tick, "tick-precision render must parse as tick");
780        }
781    }
782
783    #[test]
784    fn digit_form_round_trips_over_the_domain() {
785        let f = Fmt::digit5();
786        for v in sample_values() {
787            let inst = I::from_ticks(v.clone()).unwrap();
788            let s = render(&inst, &f).unwrap();
789            let (back, p) = parse::<UC1>(&s, &f).unwrap();
790            assert_eq!(back, inst, "round trip failed for {s}");
791            assert_eq!(p, Precision::Tick);
792        }
793    }
794
795    #[test]
796    fn named_form_round_trips() {
797        let f = Fmt::named();
798        for v in sample_values().into_iter().take(64) {
799            let inst = I::from_ticks(v).unwrap();
800            let s = render(&inst, &f).unwrap();
801            let tagged = {
802                let mut t = String::from(UC1::TAG);
803                t.push(' ');
804                t.push_str(&s);
805                t
806            };
807            let (back, _) = parse::<UC1>(&tagged, &f).unwrap();
808            assert_eq!(back, inst, "named round trip failed for {s}");
809        }
810    }
811
812    #[test]
813    fn the_two_forms_denote_the_same_value() {
814        // Rule D: one value, two tagged forms.
815        for v in sample_values().into_iter().take(64) {
816            let inst = I::from_ticks(v).unwrap();
817            let h = render(&inst, &Fmt::human()).unwrap();
818            let d = render(&inst, &Fmt::digit5()).unwrap();
819            let (a, _) = parse::<UC1>(&h, &Fmt::human()).unwrap();
820            let (b, _) = parse::<UC1>(&d, &Fmt::digit5()).unwrap();
821            assert_eq!(a, b);
822        }
823    }
824
825    // ---- Rule T: precision is the last group's tier ----
826
827    #[test]
828    fn truncated_notation_never_yields_tick_precision() {
829        // Failure mode F2, stated as a test: no parse path may return
830        // Precision::Tick from a string that stops above T-12.
831        let inst = at(123_456_789);
832        for k in (K_MIN + 1)..=0 {
833            let t = Tier::new(k).unwrap();
834            let s = render(&inst, &Fmt::human_at(t)).unwrap();
835            let (v, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
836            assert_eq!(p, Precision::Tier(t), "precision must be the last group's tier");
837            assert_ne!(p, Precision::Tick);
838            // The value is the floor, and the window contains the original.
839            assert_eq!(v, inst.floor_to(t));
840            let w = v.window_at(p).unwrap();
841            assert!(w.contains(&inst));
842            assert!(!w.is_exact());
843        }
844    }
845
846    #[test]
847    fn coarse_precision_needs_the_digit_or_named_form() {
848        // The human form's T0 anchor is a real constraint, so it is reported
849        // rather than silently mis-rendered.
850        let inst = at(123_456_789);
851        let e = render(&inst, &Fmt::human_at(Tier::DEEP)).unwrap_err();
852        assert_eq!(e.code, Code::E0006);
853
854        // The digit form is anchored at T32, so the group count states the
855        // precision unambiguously at any tier.
856        for k in [5i8, 3, 1, 0, -4, -12] {
857            let t = Tier::new(k).unwrap();
858            let f = Fmt {
859                form: Form::Digit5,
860                precision: Precision::Tier(t),
861                ..Fmt::default()
862            };
863            let s = render(&inst, &f).unwrap();
864            let (v, p) = parse::<UC1>(&s, &f).unwrap();
865            assert_eq!(p, precision_of(t), "digit form must state T{k}");
866            assert_eq!(v, inst.floor_to(t));
867            let groups = s.split(f.sep).count();
868            assert_eq!(groups, (K_MAX as i32 - k as i32 + 1) as usize);
869        }
870
871        // The named form carries its tiers explicitly, so it needs no anchor.
872        let f = Fmt {
873            form: Form::Named,
874            precision: Precision::Tier(Tier::DRIFT),
875            ..Fmt::default()
876        };
877        // A value far below the stated precision floors to zero there, and the
878        // tier still has to appear, because it is what states the precision.
879        let named = render(&inst, &f).unwrap();
880        assert_eq!(named, "0 drift", "a sub-drift value must still state its tier");
881
882        // A value above the precision renders its real groups.
883        let big = I::from_ticks(UC1::origin_offset()).unwrap();
884        let named = render(&big, &f).unwrap();
885        assert!(named.starts_with("31 deep"), "got {named}");
886        assert!(named.ends_with("687 drift"), "got {named}");
887    }
888
889    #[test]
890    fn parse_window_materialises_the_interval() {
891        let inst = at(999_999);
892        let s = render(&inst, &Fmt::human_at(Tier::BEAT)).unwrap();
893        let w = parse_window::<UC1>(&s, &Fmt::human()).unwrap();
894        assert!(w.contains(&inst));
895        assert_eq!(w.lo(), &inst.floor_to(Tier::BEAT));
896        assert_eq!(
897            w.width().ticks(),
898            &Tier::BEAT
899                .ticks()
900                .try_sub(&<Ticks as TickInt>::one())
901                .unwrap()
902        );
903    }
904
905    #[test]
906    fn tick_exact_text_runs_to_t_minus_12() {
907        // D-A8: a whole SI second has 30 trailing base-5 zeros (§2.4), so its last
908        // six groups are zero — and dropping them would change what the string
909        // says, from a tick to a T-6 window.
910        let bridge_unit = UC1::bridge().ticks;
911        let inst = I::from_ticks(UC1::origin_offset().try_add(&bridge_unit).unwrap()).unwrap();
912        let s = render(&inst, &Fmt::human()).unwrap();
913        let sub = s.split(SUB_SEP).nth(1).unwrap();
914        let groups: Vec<&str> = sub.split(SEP).collect();
915        assert_eq!(groups.len(), 12, "T-1..T-12");
916        assert!(
917            groups[6..].iter().all(|g| *g == "0000"),
918            "§2.4 guarantees the last six groups are zero"
919        );
920        // Parsing it back gives a tick, not a window.
921        let (back, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
922        assert_eq!(back, inst);
923        assert_eq!(p, Precision::Tick);
924        // Truncating to T-6 gives the same digits but a different meaning.
925        let t6 = render(&inst, &Fmt::human_at(Tier::new(-6).unwrap())).unwrap();
926        let (_, p6) = parse::<UC1>(&t6, &Fmt::human()).unwrap();
927        assert_eq!(p6, Precision::Tier(Tier::new(-6).unwrap()));
928    }
929
930    // ---- Rule S: text sorts only when padded to a fixed tier width ----
931
932    #[test]
933    fn padded_digit_form_sorts_chronologically() {
934        let mut rng = Rng(0xC0FFEE);
935        let mut vals: Vec<I> = (0..128)
936            .map(|_| I::from_ticks(rng.next_ticks()).unwrap())
937            .collect();
938        vals.push(I::zero());
939        vals.push(I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap());
940        vals.sort();
941
942        let f = Fmt::digit5();
943        let rendered: Vec<String> = vals.iter().map(|v| render(v, &f).unwrap()).collect();
944        assert!(rendered[0].contains(ALT_SEP), "§6.2 prints the digit form with `.`");
945        // Fixed width is the precondition Rule S names.
946        let w = rendered[0].len();
947        assert!(rendered.iter().all(|s| s.len() == w));
948        let mut sorted = rendered.clone();
949        sorted.sort();
950        assert_eq!(sorted, rendered, "padded digit form must sort chronologically");
951    }
952
953    #[test]
954    fn unpadded_human_form_is_not_sortable() {
955        // Rule S is a real caveat, not a formality: without a fixed tier width the
956        // lexicographic order genuinely differs from the chronological one, which
957        // is why §6 forbids documenting text forms as sortable.
958        let f = Fmt::human();
959        let small = at(1);
960        let large = I::from_ticks(Tier::DEEP.ticks()).unwrap();
961        assert!(small < large);
962        let (a, b) = (render(&small, &f).unwrap(), render(&large, &f).unwrap());
963        // The larger value has more groups, so the shorter string sorts first only
964        // by accident of its leading digits.
965        assert_ne!(a.len(), b.len());
966    }
967
968    // ---- Rule D: forms must not be mixed ----
969
970    #[test]
971    fn mixed_forms_are_rejected() {
972        // A base-5 group inside a decimal-tagged string...
973        let e = parse::<UC1>("UC1 00111·10222", &Fmt::human()).unwrap_err();
974        assert_eq!(e.code, Code::E0003);
975        // ...and a decimal group inside a base-5-tagged string.
976        let e = parse::<UC1>("UC1/5 0031·0687", &Fmt::digit5()).unwrap_err();
977        assert_eq!(e.code, Code::E0003);
978        // The digit form takes no sub-beat separator; it is anchored at the top.
979        let e = parse::<UC1>("UC1/5 00111.10222:00000", &Fmt::digit5()).unwrap_err();
980        assert_eq!(e.code, Code::E0003);
981    }
982
983    #[test]
984    fn malformed_input_maps_to_the_right_code() {
985        for (s, code) in [
986            ("XX1 0001", Code::E0002),           // unknown profile tag
987            ("0001·0002", Code::E0001),          // missing tag
988            ("UC1 3125", Code::E0004),           // group out of range
989            ("UC1 99999", Code::E0004),          // and well out of range
990            ("UC1/5 00095", Code::E0005),        // invalid base-5 digit
991            ("UC1 0001:", Code::E0001),          // sub separator, no groups
992            ("UC1 12a4", Code::E0001),           // not a number
993        ] {
994            let e = parse::<UC1>(s, &Fmt::human()).unwrap_err();
995            assert_eq!(e.code, code, "input {s:?}");
996        }
997    }
998
999    #[test]
1000    fn alternate_separator_is_accepted_on_input() {
1001        // §6.3 / D-10: `.` must always be accepted, for shell-hostile contexts.
1002        let inst = at(987_654_321);
1003        let canonical = render(&inst, &Fmt::human()).unwrap();
1004        let dotted = canonical.replace(SEP, ".");
1005        let (a, _) = parse::<UC1>(&canonical, &Fmt::human()).unwrap();
1006        let (b, _) = parse::<UC1>(&dotted, &Fmt::human()).unwrap();
1007        assert_eq!(a, b);
1008        assert!(dotted.contains('.'));
1009    }
1010
1011    #[test]
1012    fn separator_must_not_be_a_digit() {
1013        let bad = Fmt {
1014            sep: '5',
1015            ..Fmt::default()
1016        };
1017        assert_eq!(bad.validate().unwrap_err().code, Code::E0001);
1018        let same = Fmt {
1019            sep: ':',
1020            ..Fmt::default()
1021        };
1022        assert_eq!(same.validate().unwrap_err().code, Code::E0001);
1023        assert!(Fmt::default().validate().is_ok());
1024    }
1025
1026    // ---- named form ----
1027
1028    #[test]
1029    fn named_form_uses_keys_and_index_notation() {
1030        let inst = I::from_ticks(
1031            Tier::DEEP
1032                .ticks()
1033                .try_mul(&<Ticks as TickInt>::from_u64(31))
1034                .unwrap(),
1035        )
1036        .unwrap();
1037        let s = render(&inst, &Fmt::human_at(Tier::BEAT)).unwrap();
1038        assert!(s.starts_with("UC1 0031"));
1039        let named = render(&inst, &{
1040            Fmt {
1041                form: Form::Named,
1042                precision: Precision::Tier(Tier::BEAT),
1043                ..Fmt::default()
1044            }
1045        })
1046        .unwrap();
1047        assert!(named.starts_with("31 deep"), "got {named}");
1048        assert!(named.ends_with("0 beat"));
1049    }
1050
1051    #[test]
1052    fn tier_names_resolve_by_key_index_and_exponent() {
1053        // Rule N: T[k] and 5^e must be accepted wherever a name is.
1054        assert_eq!(resolve_tier_name("deep").unwrap(), Tier::DEEP);
1055        assert_eq!(resolve_tier_name("T5").unwrap(), Tier::DEEP);
1056        assert_eq!(resolve_tier_name("5^85").unwrap(), Tier::DEEP);
1057        assert_eq!(resolve_tier_name("T-12").unwrap(), Tier::TICK);
1058        assert_eq!(resolve_tier_name("5^0").unwrap(), Tier::TICK);
1059        assert_eq!(resolve_tier_name("nope").unwrap_err().code, Code::E0011);
1060        // Locale names resolve too (Appendix D).
1061        assert_eq!(
1062            resolve_tier_name_in(LocaleId::Ru, "\u{431}\u{43e}\u{439}").unwrap(),
1063            Tier::BEAT
1064        );
1065        // An unnamed tier has no key but is still addressable.
1066        assert!(resolve_tier_name("T7").is_ok());
1067        assert_eq!(resolve_tier_name("5^61").unwrap_err().code, Code::E0080);
1068    }
1069
1070    #[test]
1071    fn named_terms_must_descend() {
1072        let f = Fmt::named();
1073        assert!(parse::<UC1>("UC1 3 deep, 2 drift", &f).is_ok());
1074        let e = parse::<UC1>("UC1 2 drift, 3 deep", &f).unwrap_err();
1075        assert_eq!(e.code, Code::E0006);
1076    }
1077
1078    // ---- Appendix C cross-check ----
1079
1080    #[test]
1081    fn reproduces_appendix_c_beat_parts() {
1082        // The RFC's printed human forms are T0-precision windows under D-A8; the
1083        // digits themselves must still match exactly.
1084        let cases = [
1085            (UC1::origin_offset(), "UC1 0031·0687·2437·0454·2703·2885"),
1086            (
1087                <Ticks as TickInt>::from_dec_str(
1088                    "8070205189123984864657505252035637180530466139316558837890625",
1089                )
1090                .unwrap(),
1091                "UC1 0031·0687·2481·2999·3108·2437",
1092            ),
1093        ];
1094        for (ticks, want) in cases {
1095            let inst = I::from_ticks(ticks).unwrap();
1096            let s = render(&inst, &Fmt::human_at(Tier::BEAT)).unwrap();
1097            assert_eq!(s, want);
1098            // ...and it parses back as a beat window, not a tick.
1099            let (v, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
1100            assert_eq!(p, Precision::Tier(Tier::BEAT));
1101            assert_eq!(v, inst.floor_to(Tier::BEAT));
1102        }
1103    }
1104
1105    #[test]
1106    fn rounding_to_a_tier_then_rendering_is_stable() {
1107        let inst = at(123_456_789_012);
1108        for mode in [
1109            Rounding::Trunc,
1110            Rounding::Ceil,
1111            Rounding::HalfEven,
1112            Rounding::HalfUp,
1113        ] {
1114            let r = inst.round_to(Tier::new(-8).unwrap(), mode).unwrap();
1115            let s = render(&r, &Fmt::human()).unwrap();
1116            let (back, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
1117            assert_eq!(back, r);
1118            assert_eq!(p, Precision::Tick);
1119        }
1120    }
1121}