1#[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
60pub type ParseError = TimeError;
63
64pub const SEP: char = '·';
66
67pub const ALT_SEP: char = '.';
69
70pub const SUB_SEP: char = ':';
72
73pub const HUMAN_GROUP_WIDTH: usize = 4;
75
76pub const DIGIT5_GROUP_WIDTH: usize = 5;
78
79#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
82pub enum Form {
83 #[default]
85 HumanGroups,
86 Digit5,
89 Named,
91}
92
93#[derive(Clone, Copy, PartialEq, Eq, Debug)]
95pub struct Fmt {
96 pub form: Form,
98 pub sep: char,
100 pub sub_sep: char,
102 pub precision: Precision,
104 pub pad: bool,
107 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 pub fn human() -> Fmt {
127 Fmt::default()
128 }
129
130 pub fn human_at(tier: Tier) -> Fmt {
132 Fmt {
133 precision: Precision::Tier(tier),
134 ..Fmt::default()
135 }
136 }
137
138 pub fn digit5() -> Fmt {
143 Fmt {
144 form: Form::Digit5,
145 sep: ALT_SEP,
146 pad: true,
147 ..Fmt::default()
148 }
149 }
150
151 pub fn named() -> Fmt {
153 Fmt {
154 form: Form::Named,
155 ..Fmt::default()
156 }
157 }
158
159 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#[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#[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#[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
244pub 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
255pub 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#[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#[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 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 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 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 if g != 0 || k == k_lo.index() {
385 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#[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 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#[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 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 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 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 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 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
647pub fn resolve_tier_name(s: &str) -> Result<Tier> {
649 locale::resolve(LocaleId::default(), s)
650}
651
652pub 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 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 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 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 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 #[test]
731 fn full_width_encode_takes_45_steps_not_44() {
732 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 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 #[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 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 #[test]
828 fn truncated_notation_never_yields_tick_precision() {
829 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 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 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 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 let f = Fmt {
873 form: Form::Named,
874 precision: Precision::Tier(Tier::DRIFT),
875 ..Fmt::default()
876 };
877 let named = render(&inst, &f).unwrap();
880 assert_eq!(named, "0 drift", "a sub-drift value must still state its tier");
881
882 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 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 let (back, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
922 assert_eq!(back, inst);
923 assert_eq!(p, Precision::Tick);
924 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 #[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 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 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 assert_ne!(a.len(), b.len());
966 }
967
968 #[test]
971 fn mixed_forms_are_rejected() {
972 let e = parse::<UC1>("UC1 00111·10222", &Fmt::human()).unwrap_err();
974 assert_eq!(e.code, Code::E0003);
975 let e = parse::<UC1>("UC1/5 0031·0687", &Fmt::digit5()).unwrap_err();
977 assert_eq!(e.code, Code::E0003);
978 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), ("0001·0002", Code::E0001), ("UC1 3125", Code::E0004), ("UC1 99999", Code::E0004), ("UC1/5 00095", Code::E0005), ("UC1 0001:", Code::E0001), ("UC1 12a4", Code::E0001), ] {
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 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 #[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 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 assert_eq!(
1062 resolve_tier_name_in(LocaleId::Ru, "\u{431}\u{43e}\u{439}").unwrap(),
1063 Tier::BEAT
1064 );
1065 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 #[test]
1081 fn reproduces_appendix_c_beat_parts() {
1082 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 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}