#[cfg(feature = "alloc")]
use alloc::string::{String, ToString};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::error::{Code, Result, TimeError};
use crate::tier::Tier;
#[cfg(feature = "alloc")]
use crate::backend::{TickInt, Ticks};
#[cfg(feature = "alloc")]
use crate::tier::GROUP_BASE;
use crate::locale::LocaleId;
#[cfg(feature = "alloc")]
use crate::profile::Profile;
#[cfg(feature = "alloc")]
use crate::tier::{K_MAX, K_MIN, TIER_COUNT};
use crate::locale;
#[doc(inline)]
pub use crate::locale::LocaleId as LocaleIdAlias;
use crate::value::Precision;
#[cfg(feature = "alloc")]
use crate::value::{Instant, Window};
pub type ParseError = TimeError;
pub const SEP: char = '·';
pub const ALT_SEP: char = '.';
pub const SUB_SEP: char = ':';
pub const HUMAN_GROUP_WIDTH: usize = 4;
pub const DIGIT5_GROUP_WIDTH: usize = 5;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Form {
#[default]
HumanGroups,
Digit5,
Named,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Fmt {
pub form: Form,
pub sep: char,
pub sub_sep: char,
pub precision: Precision,
pub pad: bool,
pub locale: LocaleId,
}
impl Default for Fmt {
fn default() -> Self {
Fmt {
form: Form::HumanGroups,
sep: SEP,
sub_sep: SUB_SEP,
precision: Precision::Tick,
pad: false,
locale: LocaleId::En,
}
}
}
impl Fmt {
pub fn human() -> Fmt {
Fmt::default()
}
pub fn human_at(tier: Tier) -> Fmt {
Fmt {
precision: Precision::Tier(tier),
..Fmt::default()
}
}
pub fn digit5() -> Fmt {
Fmt {
form: Form::Digit5,
sep: ALT_SEP,
pad: true,
..Fmt::default()
}
}
pub fn named() -> Fmt {
Fmt {
form: Form::Named,
..Fmt::default()
}
}
pub fn validate(&self) -> Result<()> {
for (c, what) in [(self.sep, "separator"), (self.sub_sep, "sub-separator")] {
if c.is_ascii_digit() {
return Err(TimeError::with_context(
Code::E0001,
"separator must not be a decimal or base-5 digit (§6.3)",
));
}
let _ = what;
}
if self.sep == self.sub_sep {
return Err(TimeError::with_context(
Code::E0001,
"separator and sub-separator must differ",
));
}
Ok(())
}
#[cfg(feature = "alloc")]
fn low_tier(&self) -> Tier {
self.precision.tier()
}
}
#[cfg(feature = "alloc")]
pub fn encode_groups(t: &Ticks, k_hi: Tier, k_lo: Tier) -> Result<Vec<u16>> {
if k_hi < k_lo {
return Err(TimeError::with_context(
Code::E0006,
"group range must descend",
));
}
let gb = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
let (mut x, _) = t.quot_rem(&k_lo.ticks());
let n = (k_hi.index() as isize - k_lo.index() as isize + 1) as usize;
let mut out = Vec::with_capacity(n);
for _ in 0..n {
let (q, r) = x.quot_rem(&gb);
out.push(group_to_u16(&r));
x = q;
}
out.reverse();
Ok(out)
}
#[cfg(feature = "alloc")]
pub fn decode_groups(groups: &[u16], k_lo: Tier) -> Result<Ticks> {
let gb = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
let mut acc = <Ticks as TickInt>::zero();
for g in groups {
if *g >= GROUP_BASE {
return Err(TimeError::new(Code::E0004));
}
acc = acc
.try_mul(&gb)
.and_then(|v| v.try_add(&<Ticks as TickInt>::from_u64(*g as u64)))
.ok_or(TimeError::new(Code::E0021))?;
}
acc.try_mul(&k_lo.ticks())
.ok_or(TimeError::new(Code::E0021))
}
#[cfg(feature = "alloc")]
fn group_to_u16(r: &Ticks) -> u16 {
let b = r.to_canonical_bytes();
u16::from_be_bytes([b[b.len() - 2], b[b.len() - 1]])
}
pub fn digit5_of_group(g: u16) -> [u8; DIGIT5_GROUP_WIDTH] {
let mut out = [b'0'; DIGIT5_GROUP_WIDTH];
let mut v = g;
for i in (0..DIGIT5_GROUP_WIDTH).rev() {
out[i] = b'0' + (v % 5) as u8;
v /= 5;
}
out
}
pub fn group_of_digit5(s: &str) -> Result<u16> {
if s.len() != DIGIT5_GROUP_WIDTH {
return Err(TimeError::with_context(
Code::E0005,
"a base-5 group is exactly five digits",
));
}
let mut v: u16 = 0;
for c in s.bytes() {
if !(b'0'..=b'4').contains(&c) {
return Err(TimeError::new(Code::E0005));
}
v = v * 5 + (c - b'0') as u16;
}
Ok(v)
}
#[cfg(feature = "alloc")]
pub fn render<P: Profile>(v: &Instant<P>, f: &Fmt) -> Result<String> {
f.validate()?;
let k_lo = f.low_tier();
match f.form {
Form::HumanGroups => render_human::<P>(v, f, k_lo),
Form::Digit5 => render_digit5::<P>(v, f, k_lo),
Form::Named => render_named::<P>(v, f, k_lo),
}
}
#[cfg(feature = "alloc")]
fn human_high_tier<P: Profile>(v: &Instant<P>, pad: bool) -> Tier {
if pad {
return Tier::new(K_MAX).expect("K_MAX is on the grid");
}
let mut hi = Tier::BEAT;
for k in (1..=K_MAX).rev() {
let t = Tier::new(k).expect("in range");
if v.tier_value(t) != 0 {
hi = t;
break;
}
}
hi
}
#[cfg(feature = "alloc")]
fn render_human<P: Profile>(v: &Instant<P>, f: &Fmt, k_lo: Tier) -> Result<String> {
if k_lo.index() > 0 {
return Err(TimeError::with_context(
Code::E0006,
"the human form anchors at T0 and cannot state a coarser precision; \
use the digit form or the named form",
));
}
let k_hi = human_high_tier(v, f.pad);
let whole_lo = Tier::BEAT;
let mut s = String::new();
s.push_str(P::TAG);
s.push(' ');
let whole = encode_groups(v.ticks(), k_hi, whole_lo)?;
push_joined(&mut s, &whole, f.sep, |g| {
let mut b = [b'0'; HUMAN_GROUP_WIDTH];
write_dec4(g, &mut b);
b
});
if k_lo.index() < 0 {
s.push(f.sub_sep);
let sub = encode_groups(
v.ticks(),
Tier::new(-1).expect("on the grid"),
k_lo,
)?;
push_joined(&mut s, &sub, f.sep, |g| {
let mut b = [b'0'; HUMAN_GROUP_WIDTH];
write_dec4(g, &mut b);
b
});
}
Ok(s)
}
#[cfg(feature = "alloc")]
fn render_digit5<P: Profile>(v: &Instant<P>, f: &Fmt, k_lo: Tier) -> Result<String> {
let k_hi = Tier::new(K_MAX).expect("K_MAX is on the grid");
let groups = encode_groups(v.ticks(), k_hi, k_lo)?;
let mut s = String::new();
s.push_str(P::TAG);
s.push_str("/5 ");
push_joined(&mut s, &groups, f.sep, digit5_of_group);
Ok(s)
}
#[cfg(feature = "alloc")]
fn render_named<P: Profile>(v: &Instant<P>, f: &Fmt, k_lo: Tier) -> Result<String> {
let k_hi = {
let h = human_high_tier(v, f.pad);
if h.index() < k_lo.index() {
k_lo
} else {
h
}
};
let mut parts: Vec<String> = Vec::new();
let mut k = k_hi.index();
while k >= k_lo.index() {
let t = Tier::new(k)?;
let g = v.tier_value(t);
if g != 0 || k == k_lo.index() {
let mut p = g.to_string();
p.push(' ');
p.push_str(&locale::display(f.locale, t));
parts.push(p);
}
k -= 1;
}
let _ = f;
Ok(parts.join(", "))
}
#[cfg(feature = "alloc")]
fn push_joined<F, const N: usize>(s: &mut String, groups: &[u16], sep: char, render_one: F)
where
F: Fn(u16) -> [u8; N],
{
for (i, g) in groups.iter().enumerate() {
if i > 0 {
s.push(sep);
}
let b = render_one(*g);
s.push_str(core::str::from_utf8(&b).expect("ascii digits"));
}
}
#[cfg(feature = "alloc")]
fn write_dec4(g: u16, out: &mut [u8; HUMAN_GROUP_WIDTH]) {
let mut v = g;
for i in (0..HUMAN_GROUP_WIDTH).rev() {
out[i] = b'0' + (v % 10) as u8;
v /= 10;
}
}
#[cfg(feature = "alloc")]
pub fn parse<P: Profile>(s: &str, ctx: &Fmt) -> core::result::Result<(Instant<P>, Precision), ParseError> {
let s = s.trim();
let (tag, rest) = split_tag(s)?;
let digit_tag = {
let mut t = String::from(P::TAG);
t.push_str("/5");
t
};
if tag == digit_tag {
parse_digit5::<P>(rest, ctx)
} else if tag == P::TAG {
if rest.contains(',') || rest.bytes().any(|b| b.is_ascii_alphabetic()) {
parse_named::<P>(rest, ctx)
} else {
parse_human::<P>(rest, ctx)
}
} else {
Err(TimeError::new(Code::E0002))
}
}
#[cfg(feature = "alloc")]
pub fn parse_window<P: Profile>(s: &str, ctx: &Fmt) -> Result<Window<P>> {
let (v, p) = parse::<P>(s, ctx)?;
v.window_at(p)
}
#[cfg(feature = "alloc")]
fn split_tag(s: &str) -> core::result::Result<(&str, &str), ParseError> {
match s.split_once(char::is_whitespace) {
None => Err(TimeError::with_context(
Code::E0001,
"missing profile tag; every serialised form carries one (Rule P)",
)),
Some((tag, rest)) => Ok((tag, rest.trim())),
}
}
#[cfg(feature = "alloc")]
fn is_sep(c: char, ctx: &Fmt) -> bool {
c == ctx.sep || c == ALT_SEP || c == SEP
}
#[cfg(feature = "alloc")]
fn split_groups<'a>(s: &'a str, ctx: &Fmt) -> Vec<&'a str> {
s.split(|c| is_sep(c, ctx))
.filter(|p| !p.is_empty())
.collect()
}
#[cfg(feature = "alloc")]
fn parse_human<P: Profile>(
rest: &str,
ctx: &Fmt,
) -> core::result::Result<(Instant<P>, Precision), ParseError> {
let (whole_str, sub_str) = match rest.split_once(ctx.sub_sep) {
None => (rest, None),
Some((w, s)) => (w, Some(s)),
};
let whole_parts = split_groups(whole_str, ctx);
if whole_parts.is_empty() {
return Err(TimeError::new(Code::E0001));
}
let mut groups: Vec<u16> = Vec::new();
for p in &whole_parts {
groups.push(parse_human_group(p)?);
}
let k_hi = whole_parts.len() as i32 - 1;
if k_hi > K_MAX as i32 {
return Err(TimeError::with_context(
Code::E0006,
"more whole groups than the tier grid holds",
));
}
let mut k_lo = 0i32;
if let Some(sub) = sub_str {
let sub_parts = split_groups(sub, ctx);
if sub_parts.is_empty() {
return Err(TimeError::with_context(
Code::E0001,
"sub-beat separator with no groups after it",
));
}
for p in &sub_parts {
groups.push(parse_human_group(p)?);
}
k_lo = -(sub_parts.len() as i32);
if k_lo < K_MIN as i32 {
return Err(TimeError::with_context(
Code::E0006,
"more sub-beat groups than the tier grid holds",
));
}
}
let low = Tier::new(k_lo as i8)?;
let ticks = decode_groups(&groups, low)?;
let v = Instant::<P>::from_ticks(ticks)?;
Ok((v, precision_of(low)))
}
#[cfg(feature = "alloc")]
fn parse_human_group(p: &str) -> core::result::Result<u16, ParseError> {
if p.len() == DIGIT5_GROUP_WIDTH && p.bytes().all(|b| (b'0'..=b'4').contains(&b)) {
return Err(TimeError::with_context(
Code::E0003,
"base-5 group in a decimal-tagged string",
));
}
if p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()) {
return Err(TimeError::new(Code::E0001));
}
let v: u32 = p.parse().map_err(|_| TimeError::new(Code::E0004))?;
if v >= GROUP_BASE as u32 {
return Err(TimeError::new(Code::E0004));
}
Ok(v as u16)
}
#[cfg(feature = "alloc")]
fn parse_digit5<P: Profile>(
rest: &str,
ctx: &Fmt,
) -> core::result::Result<(Instant<P>, Precision), ParseError> {
if rest.contains(ctx.sub_sep) {
return Err(TimeError::with_context(
Code::E0003,
"the digit form is anchored at T32 and takes no sub-beat separator",
));
}
let parts = split_groups(rest, ctx);
if parts.is_empty() {
return Err(TimeError::new(Code::E0001));
}
if parts.len() > TIER_COUNT {
return Err(TimeError::with_context(
Code::E0006,
"more groups than the tier grid holds",
));
}
let mut groups = Vec::with_capacity(parts.len());
for p in &parts {
if p.len() == HUMAN_GROUP_WIDTH {
return Err(TimeError::with_context(
Code::E0003,
"decimal group in a base-5-tagged string",
));
}
groups.push(group_of_digit5(p)?);
}
let k_lo = K_MAX as i32 - (parts.len() as i32 - 1);
let low = Tier::new(k_lo as i8)?;
let ticks = decode_groups(&groups, low)?;
let v = Instant::<P>::from_ticks(ticks)?;
Ok((v, precision_of(low)))
}
#[cfg(feature = "alloc")]
fn parse_named<P: Profile>(
rest: &str,
_ctx: &Fmt,
) -> core::result::Result<(Instant<P>, Precision), ParseError> {
let mut acc = <Ticks as TickInt>::zero();
let mut lowest: Option<Tier> = None;
let mut prev: Option<i8> = None;
for term in rest.split(',') {
let term = term.trim();
if term.is_empty() {
continue;
}
let (n_str, name) = term
.split_once(char::is_whitespace)
.ok_or(TimeError::with_context(Code::E0001, "expected `<count> <tier>`"))?;
let count: u64 = n_str
.trim()
.parse()
.map_err(|_| TimeError::new(Code::E0001))?;
if count >= GROUP_BASE as u64 {
return Err(TimeError::new(Code::E0004));
}
let tier = resolve_tier_name(name.trim())?;
if let Some(p) = prev {
if tier.index() >= p {
return Err(TimeError::with_context(
Code::E0006,
"named terms must descend",
));
}
}
prev = Some(tier.index());
lowest = Some(tier);
let add = tier
.ticks()
.try_mul(&<Ticks as TickInt>::from_u64(count))
.ok_or(TimeError::new(Code::E0021))?;
acc = acc.try_add(&add).ok_or(TimeError::new(Code::E0021))?;
}
let low = lowest.ok_or(TimeError::new(Code::E0001))?;
let v = Instant::<P>::from_ticks(acc)?;
Ok((v, precision_of(low)))
}
pub fn resolve_tier_name(s: &str) -> Result<Tier> {
locale::resolve(LocaleId::default(), s)
}
pub fn resolve_tier_name_in(loc: LocaleId, s: &str) -> Result<Tier> {
locale::resolve(loc, s)
}
#[cfg(feature = "alloc")]
fn precision_of(t: Tier) -> Precision {
if t.is_tick() {
Precision::Tick
} else {
Precision::Tier(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::UC1;
use crate::value::Rounding;
use alloc::vec;
type I = Instant<UC1>;
fn at(n: u64) -> I {
I::from_u64(n).unwrap()
}
struct Rng(u64);
impl Rng {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn next_ticks(&mut self) -> Ticks {
let mut bytes = [0u8; 64];
for chunk in bytes.chunks_mut(8) {
chunk.copy_from_slice(&self.next_u64().to_be_bytes());
}
<Ticks as TickInt>::from_canonical_bytes(&bytes).unwrap()
}
}
fn sample_values() -> Vec<Ticks> {
let mut v = vec![
<Ticks as TickInt>::zero(),
<Ticks as TickInt>::one(),
<Ticks as TickInt>::domain_max(),
UC1::origin_offset(),
UC1::beat(),
];
for t in Tier::all_ascending() {
let b = t.ticks();
v.push(b.clone());
if let Some(x) = b.try_sub(&<Ticks as TickInt>::one()) {
v.push(x);
}
if let Some(x) = b.try_add(&<Ticks as TickInt>::one()) {
v.push(x);
}
}
let mut rng = Rng(0x5EED_1234_9ABC_DEF0);
for _ in 0..256 {
v.push(rng.next_ticks());
}
v
}
#[test]
fn full_width_encode_takes_45_steps_not_44() {
let m = <Ticks as TickInt>::domain_max();
let gs = encode_groups(&m, Tier::new(K_MAX).unwrap(), Tier::TICK).unwrap();
assert_eq!(gs.len(), 45);
assert_eq!(gs.len(), TIER_COUNT);
assert_ne!(gs[0], 0, "the top group must be load-bearing");
assert_eq!(gs[0], 2);
assert_eq!(m.to_radix_string(5).len(), 221);
}
#[test]
fn group_codec_round_trips() {
for v in sample_values() {
let gs = encode_groups(&v, Tier::new(K_MAX).unwrap(), Tier::TICK).unwrap();
assert_eq!(gs.len(), TIER_COUNT);
assert!(gs.iter().all(|g| *g < GROUP_BASE));
let back = decode_groups(&gs, Tier::TICK).unwrap();
assert_eq!(back, v);
}
}
#[test]
fn digit5_group_codec_round_trips() {
for g in 0..GROUP_BASE {
let d = digit5_of_group(g);
let s = core::str::from_utf8(&d).unwrap();
assert_eq!(s.len(), 5);
assert!(s.bytes().all(|b| (b'0'..=b'4').contains(&b)));
assert_eq!(group_of_digit5(s).unwrap(), g);
}
assert_eq!(group_of_digit5("00005").unwrap_err().code, Code::E0005);
assert_eq!(group_of_digit5("0000").unwrap_err().code, Code::E0005);
}
#[test]
fn human_form_round_trips_over_the_domain() {
let f = Fmt::human();
for v in sample_values() {
let inst = I::from_ticks(v.clone()).unwrap();
let s = render(&inst, &f).unwrap();
let (back, p) = parse::<UC1>(&s, &f).unwrap();
assert_eq!(back, inst, "round trip failed for {s}");
assert_eq!(p, Precision::Tick, "tick-precision render must parse as tick");
}
}
#[test]
fn digit_form_round_trips_over_the_domain() {
let f = Fmt::digit5();
for v in sample_values() {
let inst = I::from_ticks(v.clone()).unwrap();
let s = render(&inst, &f).unwrap();
let (back, p) = parse::<UC1>(&s, &f).unwrap();
assert_eq!(back, inst, "round trip failed for {s}");
assert_eq!(p, Precision::Tick);
}
}
#[test]
fn named_form_round_trips() {
let f = Fmt::named();
for v in sample_values().into_iter().take(64) {
let inst = I::from_ticks(v).unwrap();
let s = render(&inst, &f).unwrap();
let tagged = {
let mut t = String::from(UC1::TAG);
t.push(' ');
t.push_str(&s);
t
};
let (back, _) = parse::<UC1>(&tagged, &f).unwrap();
assert_eq!(back, inst, "named round trip failed for {s}");
}
}
#[test]
fn the_two_forms_denote_the_same_value() {
for v in sample_values().into_iter().take(64) {
let inst = I::from_ticks(v).unwrap();
let h = render(&inst, &Fmt::human()).unwrap();
let d = render(&inst, &Fmt::digit5()).unwrap();
let (a, _) = parse::<UC1>(&h, &Fmt::human()).unwrap();
let (b, _) = parse::<UC1>(&d, &Fmt::digit5()).unwrap();
assert_eq!(a, b);
}
}
#[test]
fn truncated_notation_never_yields_tick_precision() {
let inst = at(123_456_789);
for k in (K_MIN + 1)..=0 {
let t = Tier::new(k).unwrap();
let s = render(&inst, &Fmt::human_at(t)).unwrap();
let (v, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
assert_eq!(p, Precision::Tier(t), "precision must be the last group's tier");
assert_ne!(p, Precision::Tick);
assert_eq!(v, inst.floor_to(t));
let w = v.window_at(p).unwrap();
assert!(w.contains(&inst));
assert!(!w.is_exact());
}
}
#[test]
fn coarse_precision_needs_the_digit_or_named_form() {
let inst = at(123_456_789);
let e = render(&inst, &Fmt::human_at(Tier::DEEP)).unwrap_err();
assert_eq!(e.code, Code::E0006);
for k in [5i8, 3, 1, 0, -4, -12] {
let t = Tier::new(k).unwrap();
let f = Fmt {
form: Form::Digit5,
precision: Precision::Tier(t),
..Fmt::default()
};
let s = render(&inst, &f).unwrap();
let (v, p) = parse::<UC1>(&s, &f).unwrap();
assert_eq!(p, precision_of(t), "digit form must state T{k}");
assert_eq!(v, inst.floor_to(t));
let groups = s.split(f.sep).count();
assert_eq!(groups, (K_MAX as i32 - k as i32 + 1) as usize);
}
let f = Fmt {
form: Form::Named,
precision: Precision::Tier(Tier::DRIFT),
..Fmt::default()
};
let named = render(&inst, &f).unwrap();
assert_eq!(named, "0 drift", "a sub-drift value must still state its tier");
let big = I::from_ticks(UC1::origin_offset()).unwrap();
let named = render(&big, &f).unwrap();
assert!(named.starts_with("31 deep"), "got {named}");
assert!(named.ends_with("687 drift"), "got {named}");
}
#[test]
fn parse_window_materialises_the_interval() {
let inst = at(999_999);
let s = render(&inst, &Fmt::human_at(Tier::BEAT)).unwrap();
let w = parse_window::<UC1>(&s, &Fmt::human()).unwrap();
assert!(w.contains(&inst));
assert_eq!(w.lo(), &inst.floor_to(Tier::BEAT));
assert_eq!(
w.width().ticks(),
&Tier::BEAT
.ticks()
.try_sub(&<Ticks as TickInt>::one())
.unwrap()
);
}
#[test]
fn tick_exact_text_runs_to_t_minus_12() {
let bridge_unit = UC1::bridge().ticks;
let inst = I::from_ticks(UC1::origin_offset().try_add(&bridge_unit).unwrap()).unwrap();
let s = render(&inst, &Fmt::human()).unwrap();
let sub = s.split(SUB_SEP).nth(1).unwrap();
let groups: Vec<&str> = sub.split(SEP).collect();
assert_eq!(groups.len(), 12, "T-1..T-12");
assert!(
groups[6..].iter().all(|g| *g == "0000"),
"§2.4 guarantees the last six groups are zero"
);
let (back, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
assert_eq!(back, inst);
assert_eq!(p, Precision::Tick);
let t6 = render(&inst, &Fmt::human_at(Tier::new(-6).unwrap())).unwrap();
let (_, p6) = parse::<UC1>(&t6, &Fmt::human()).unwrap();
assert_eq!(p6, Precision::Tier(Tier::new(-6).unwrap()));
}
#[test]
fn padded_digit_form_sorts_chronologically() {
let mut rng = Rng(0xC0FFEE);
let mut vals: Vec<I> = (0..128)
.map(|_| I::from_ticks(rng.next_ticks()).unwrap())
.collect();
vals.push(I::zero());
vals.push(I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap());
vals.sort();
let f = Fmt::digit5();
let rendered: Vec<String> = vals.iter().map(|v| render(v, &f).unwrap()).collect();
assert!(rendered[0].contains(ALT_SEP), "§6.2 prints the digit form with `.`");
let w = rendered[0].len();
assert!(rendered.iter().all(|s| s.len() == w));
let mut sorted = rendered.clone();
sorted.sort();
assert_eq!(sorted, rendered, "padded digit form must sort chronologically");
}
#[test]
fn unpadded_human_form_is_not_sortable() {
let f = Fmt::human();
let small = at(1);
let large = I::from_ticks(Tier::DEEP.ticks()).unwrap();
assert!(small < large);
let (a, b) = (render(&small, &f).unwrap(), render(&large, &f).unwrap());
assert_ne!(a.len(), b.len());
}
#[test]
fn mixed_forms_are_rejected() {
let e = parse::<UC1>("UC1 00111·10222", &Fmt::human()).unwrap_err();
assert_eq!(e.code, Code::E0003);
let e = parse::<UC1>("UC1/5 0031·0687", &Fmt::digit5()).unwrap_err();
assert_eq!(e.code, Code::E0003);
let e = parse::<UC1>("UC1/5 00111.10222:00000", &Fmt::digit5()).unwrap_err();
assert_eq!(e.code, Code::E0003);
}
#[test]
fn malformed_input_maps_to_the_right_code() {
for (s, code) in [
("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), ] {
let e = parse::<UC1>(s, &Fmt::human()).unwrap_err();
assert_eq!(e.code, code, "input {s:?}");
}
}
#[test]
fn alternate_separator_is_accepted_on_input() {
let inst = at(987_654_321);
let canonical = render(&inst, &Fmt::human()).unwrap();
let dotted = canonical.replace(SEP, ".");
let (a, _) = parse::<UC1>(&canonical, &Fmt::human()).unwrap();
let (b, _) = parse::<UC1>(&dotted, &Fmt::human()).unwrap();
assert_eq!(a, b);
assert!(dotted.contains('.'));
}
#[test]
fn separator_must_not_be_a_digit() {
let bad = Fmt {
sep: '5',
..Fmt::default()
};
assert_eq!(bad.validate().unwrap_err().code, Code::E0001);
let same = Fmt {
sep: ':',
..Fmt::default()
};
assert_eq!(same.validate().unwrap_err().code, Code::E0001);
assert!(Fmt::default().validate().is_ok());
}
#[test]
fn named_form_uses_keys_and_index_notation() {
let inst = I::from_ticks(
Tier::DEEP
.ticks()
.try_mul(&<Ticks as TickInt>::from_u64(31))
.unwrap(),
)
.unwrap();
let s = render(&inst, &Fmt::human_at(Tier::BEAT)).unwrap();
assert!(s.starts_with("UC1 0031"));
let named = render(&inst, &{
Fmt {
form: Form::Named,
precision: Precision::Tier(Tier::BEAT),
..Fmt::default()
}
})
.unwrap();
assert!(named.starts_with("31 deep"), "got {named}");
assert!(named.ends_with("0 beat"));
}
#[test]
fn tier_names_resolve_by_key_index_and_exponent() {
assert_eq!(resolve_tier_name("deep").unwrap(), Tier::DEEP);
assert_eq!(resolve_tier_name("T5").unwrap(), Tier::DEEP);
assert_eq!(resolve_tier_name("5^85").unwrap(), Tier::DEEP);
assert_eq!(resolve_tier_name("T-12").unwrap(), Tier::TICK);
assert_eq!(resolve_tier_name("5^0").unwrap(), Tier::TICK);
assert_eq!(resolve_tier_name("nope").unwrap_err().code, Code::E0011);
assert_eq!(
resolve_tier_name_in(LocaleId::Ru, "\u{431}\u{43e}\u{439}").unwrap(),
Tier::BEAT
);
assert!(resolve_tier_name("T7").is_ok());
assert_eq!(resolve_tier_name("5^61").unwrap_err().code, Code::E0080);
}
#[test]
fn named_terms_must_descend() {
let f = Fmt::named();
assert!(parse::<UC1>("UC1 3 deep, 2 drift", &f).is_ok());
let e = parse::<UC1>("UC1 2 drift, 3 deep", &f).unwrap_err();
assert_eq!(e.code, Code::E0006);
}
#[test]
fn reproduces_appendix_c_beat_parts() {
let cases = [
(UC1::origin_offset(), "UC1 0031·0687·2437·0454·2703·2885"),
(
<Ticks as TickInt>::from_dec_str(
"8070205189123984864657505252035637180530466139316558837890625",
)
.unwrap(),
"UC1 0031·0687·2481·2999·3108·2437",
),
];
for (ticks, want) in cases {
let inst = I::from_ticks(ticks).unwrap();
let s = render(&inst, &Fmt::human_at(Tier::BEAT)).unwrap();
assert_eq!(s, want);
let (v, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
assert_eq!(p, Precision::Tier(Tier::BEAT));
assert_eq!(v, inst.floor_to(Tier::BEAT));
}
}
#[test]
fn rounding_to_a_tier_then_rendering_is_stable() {
let inst = at(123_456_789_012);
for mode in [
Rounding::Trunc,
Rounding::Ceil,
Rounding::HalfEven,
Rounding::HalfUp,
] {
let r = inst.round_to(Tier::new(-8).unwrap(), mode).unwrap();
let s = render(&r, &Fmt::human()).unwrap();
let (back, p) = parse::<UC1>(&s, &Fmt::human()).unwrap();
assert_eq!(back, r);
assert_eq!(p, Precision::Tick);
}
}
}