Skip to main content

music21/common/
numbertools.rs

1//! music21's `common.numberTools`: quarter lengths kept exact, numbers
2//! rounded and written the way music21 writes them, Roman numerals and
3//! ordinals.
4
5use std::convert::TryFrom;
6use std::fmt::{self, Display};
7use std::ops::Range;
8use std::sync::LazyLock;
9
10use num::bigint::BigInt;
11use num::rational::BigRational;
12use num::{FromPrimitive, Integer, One, Signed, ToPrimitive, Zero};
13use num_traits::Float;
14
15use crate::defaults::{FloatType, UnsignedIntegerType};
16use crate::error::{Error, Result};
17
18macro_rules! define_ordinals {
19    (
20        $(#[$meta:meta])*
21        $vis:vis enum $name:ident {
22            $($variant:ident => ($display:expr, $lower:expr)),* $(,)?
23        }
24    ) => {
25        $(#[$meta])*
26        #[derive(Debug, Copy, Clone, PartialEq, Eq)]
27        #[repr(u8)]
28        $vis enum $name {
29            $($variant),*
30        }
31
32        impl TryFrom<UnsignedIntegerType> for $name {
33            type Error = crate::error::Error;
34            fn try_from(value: UnsignedIntegerType) -> Result<Self, Self::Error> {
35                const VARIANTS: &[$name] = &[$($name::$variant),*];
36                let idx = value as usize;
37                if idx < VARIANTS.len() {
38                    Ok(VARIANTS[idx])
39                } else {
40                    Err(Error::Ordinal(format!(
41                        "Invalid {} value: {}",
42                        stringify!($name),
43                        value
44                    )))
45                }
46            }
47        }
48
49        impl TryFrom<&str> for $name {
50            type Error = crate::error::Error;
51            fn try_from(value: &str) -> Result<Self, Self::Error> {
52                match value.to_lowercase().as_str() {
53                    $(
54                        $lower => Ok($name::$variant),
55                    )*
56                    _ => Err(Error::Ordinal(format!(
57                        "Invalid {} string: {}",
58                        stringify!($name),
59                        value
60                    ))),
61                }
62            }
63        }
64
65        impl Display for $name {
66            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67                let s = match self {
68                    $(
69                        $name::$variant => $display,
70                    )*
71                };
72                write!(f, "{}", s)
73            }
74        }
75    };
76}
77
78// Define the musical ordinals (override a few names)
79define_ordinals! {
80    pub(crate) enum MusicOrdinals {
81        Zeroth      => ("Zeroth", "zeroth"),
82        Unison      => ("Unison", "unison"),          // instead of First
83        Second      => ("Second", "second"),
84        Third       => ("Third", "third"),
85        Fourth      => ("Fourth", "fourth"),
86        Fifth       => ("Fifth", "fifth"),
87        Sixth       => ("Sixth", "sixth"),
88        Seventh     => ("Seventh", "seventh"),
89        Octave      => ("Octave", "octave"),           // instead of Eighth
90        Ninth       => ("Ninth", "ninth"),
91        Tenth       => ("Tenth", "tenth"),
92        Eleventh    => ("Eleventh", "eleventh"),
93        Twelfth     => ("Twelfth", "twelfth"),
94        Thirteenth  => ("Thirteenth", "thirteenth"),
95        Fourteenth  => ("Fourteenth", "fourteenth"),
96        DoubleOctave=> ("Double-octave", "double-octave"), // instead of Fifteenth
97        Sixteenth   => ("Sixteenth", "sixteenth"),
98        Seventeenth => ("Seventeenth", "seventeenth"),
99        Eighteenth  => ("Eighteenth", "eighteenth"),
100        Nineteenth  => ("Nineteenth", "nineteenth"),
101        Twentieth   => ("Twentieth", "twentieth"),
102        TwentyFirst => ("Twenty-first", "twenty-first"),
103        TripleOctave=> ("Triple-octave", "triple-octave"), // instead of Twenty-second
104    }
105}
106
107/// music21's plain `ordinals` list, as far as any chord inversion can reach.
108pub(crate) const ORDINALS: [&str; 8] = [
109    "Zeroth", "First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh",
110];
111
112pub(crate) static MUSICAL_ORDINAL_STRINGS: LazyLock<[String; 23]> = LazyLock::new(|| {
113    std::array::from_fn(|i| {
114        let val: UnsignedIntegerType = i as UnsignedIntegerType;
115        let ordinal = MusicOrdinals::try_from(val).unwrap();
116        format!("{ordinal}")
117    })
118});
119
120/// The same names lowered, for the case-insensitive search an interval name
121/// does over all twenty-three of them.
122pub(crate) static MUSICAL_ORDINAL_STRINGS_LOWER: LazyLock<[String; 23]> =
123    LazyLock::new(|| std::array::from_fn(|i| MUSICAL_ORDINAL_STRINGS[i].to_ascii_lowercase()));
124
125/// music21's `defaults.limitOffsetDenominator`: the largest denominator
126/// [`op_frac`] keeps a quarter length as a fraction over.
127pub const DENOM_LIMIT: u64 = 65535;
128
129/// music21's `ordinals`: the names of the numbers from nought to
130/// twenty-two, in English.
131pub const ORDINAL_NAMES: [&str; 23] = [
132    "Zeroth",
133    "First",
134    "Second",
135    "Third",
136    "Fourth",
137    "Fifth",
138    "Sixth",
139    "Seventh",
140    "Eighth",
141    "Ninth",
142    "Tenth",
143    "Eleventh",
144    "Twelfth",
145    "Thirteenth",
146    "Fourteenth",
147    "Fifteenth",
148    "Sixteenth",
149    "Seventeenth",
150    "Eighteenth",
151    "Nineteenth",
152    "Twentieth",
153    "Twenty-first",
154    "Twenty-second",
155];
156
157/// music21's `musicOrdinals`: [`ORDINAL_NAMES`] with an interval's own
158/// names for the first, the eighth, the fifteenth and the twenty-second.
159pub const MUSIC_ORDINAL_NAMES: [&str; 23] = [
160    "Zeroth",
161    "Unison",
162    "Second",
163    "Third",
164    "Fourth",
165    "Fifth",
166    "Sixth",
167    "Seventh",
168    "Octave",
169    "Ninth",
170    "Tenth",
171    "Eleventh",
172    "Twelfth",
173    "Thirteenth",
174    "Fourteenth",
175    "Double-octave",
176    "Sixteenth",
177    "Seventeenth",
178    "Eighteenth",
179    "Nineteenth",
180    "Twentieth",
181    "Twenty-first",
182    "Triple-octave",
183];
184
185/// A quarter length or an offset as music21 keeps one: a float where a
186/// float says it exactly, a fraction where none does.
187#[derive(Clone, Copy, Debug, PartialEq)]
188pub enum OffsetQL {
189    /// A value a float holds exactly.
190    Float(FloatType),
191    /// A value in lowest terms whose denominator is not a power of two.
192    Fraction {
193        /// The numerator, which carries the sign.
194        numerator: i64,
195        /// The denominator, always positive.
196        denominator: i64,
197    },
198}
199
200impl OffsetQL {
201    /// The value as the nearest float.
202    pub fn to_f64(self) -> FloatType {
203        match self {
204            Self::Float(value) => value,
205            Self::Fraction {
206                numerator,
207                denominator,
208            } => numerator as FloatType / denominator as FloatType,
209        }
210    }
211}
212
213/// A whole number or a float, which is what music21's `numToIntOrFloat`
214/// and `roundToHalfInteger` give back.
215#[derive(Clone, Debug, PartialEq)]
216pub enum IntOrFloat {
217    /// A whole number, of any size.
218    Int(BigInt),
219    /// Anything else.
220    Float(FloatType),
221}
222
223/// Python's `math.isclose(a, b, abs_tol=abs_tol)`, with its relative
224/// tolerance of one in a billion.
225///
226/// ```
227/// use music21::common::numbertools::is_close;
228///
229/// assert!(is_close(0.1 + 0.2, 0.3, 0.0));
230/// assert!(!is_close(1.0, 1.1, 0.01));
231/// ```
232pub fn is_close(a: FloatType, b: FloatType, abs_tol: FloatType) -> bool {
233    if a == b {
234        return true;
235    }
236    if a.is_infinite() || b.is_infinite() {
237        return false;
238    }
239    let difference = (b - a).abs();
240    difference <= (1e-9 * b).abs() || difference <= (1e-9 * a).abs() || difference <= abs_tol
241}
242
243/// A float written as Python's `repr` writes it: the shortest digits that
244/// read back as the same float, `1.0` for a whole number, and an exponent
245/// below a ten-thousandth or from 1e16 up.
246///
247/// ```
248/// use music21::common::numbertools::float_repr;
249///
250/// assert_eq!(float_repr(1.0), "1.0");
251/// assert_eq!(float_repr(0.125), "0.125");
252/// assert_eq!(float_repr(1e16), "1e+16");
253/// assert_eq!(float_repr(0.00001), "1e-05");
254/// ```
255pub fn float_repr(value: FloatType) -> String {
256    if value.is_nan() {
257        return "nan".to_string();
258    }
259    if value.is_infinite() {
260        return if value > 0.0 { "inf" } else { "-inf" }.to_string();
261    }
262    let scientific = format!("{value:e}");
263    let (mantissa, exponent) = scientific
264        .split_once('e')
265        .unwrap_or((scientific.as_str(), "0"));
266    let exponent: i32 = exponent.parse().unwrap_or(0);
267    let (sign, mantissa) = match mantissa.strip_prefix('-') {
268        Some(rest) => ("-", rest),
269        None => ("", mantissa),
270    };
271    let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
272    if (-4..16).contains(&exponent) {
273        let point = exponent + 1;
274        if point <= 0 {
275            let zeros = "0".repeat(point.unsigned_abs() as usize);
276            return format!("{sign}0.{zeros}{digits}");
277        }
278        let point = point as usize;
279        if digits.len() <= point {
280            let zeros = "0".repeat(point - digits.len());
281            return format!("{sign}{digits}{zeros}.0");
282        }
283        return format!("{sign}{}.{}", &digits[..point], &digits[point..]);
284    }
285    let rest = if digits.len() > 1 {
286        format!(".{}", &digits[1..])
287    } else {
288        String::new()
289    };
290    let exponent_sign = if exponent < 0 { '-' } else { '+' };
291    format!(
292        "{sign}{}{rest}e{exponent_sign}{:02}",
293        &digits[..1],
294        exponent.unsigned_abs()
295    )
296}
297
298/// Python's `round(value, digits)` for a float: the value written out to
299/// that many decimal places, correctly rounded, and read back.
300pub(crate) fn round_digits(value: FloatType, digits: usize) -> FloatType {
301    if !value.is_finite() {
302        return value;
303    }
304    format!("{value:.digits$}").parse().unwrap_or(value)
305}
306
307/// Python's `divmod` for floats: the floored quotient, and the remainder
308/// with the sign of the divisor.
309pub(crate) fn float_divmod(x: FloatType, y: FloatType) -> (FloatType, FloatType) {
310    let mut modulo = x % y;
311    let mut division = (x - modulo) / y;
312    if modulo != 0.0 {
313        if (y < 0.0) != (modulo < 0.0) {
314            modulo += y;
315            division -= 1.0;
316        }
317    } else {
318        modulo = 0.0f64.copysign(y);
319    }
320    if division == 0.0 {
321        return (0.0f64.copysign(x / y), modulo);
322    }
323    let mut floored = division.floor();
324    if division - floored > 0.5 {
325        floored += 1.0;
326    }
327    (floored, modulo)
328}
329
330/// The exact fraction a float stands for, in lowest terms, as Python's
331/// `float.as_integer_ratio` gives it.
332///
333/// ```
334/// use music21::common::numbertools::float_ratio;
335/// use num::BigInt;
336///
337/// assert_eq!(float_ratio(0.75), Some((BigInt::from(3), BigInt::from(4))));
338/// assert_eq!(float_ratio(f64::NAN), None);
339/// ```
340pub fn float_ratio(value: FloatType) -> Option<(BigInt, BigInt)> {
341    if !value.is_finite() {
342        return None;
343    }
344    let (mantissa, exponent, sign) = Float::integer_decode(value);
345    if mantissa == 0 {
346        return Some((BigInt::zero(), BigInt::one()));
347    }
348    let zeros = mantissa.trailing_zeros();
349    let mantissa = BigInt::from(mantissa >> zeros) * BigInt::from(sign);
350    let exponent = i32::from(exponent) + zeros as i32;
351    if exponent >= 0 {
352        return Some((mantissa << exponent as usize, BigInt::one()));
353    }
354    Some((mantissa, BigInt::one() << exponent.unsigned_abs() as usize))
355}
356
357/// The fraction nearest `numerator / denominator` whose denominator is at
358/// most `limit`: Python's `Fraction.limit_denominator`, which music21's
359/// `_preFracLimitDenominator` copies. A fraction already within the limit
360/// comes back as it is.
361///
362/// ```
363/// use music21::common::numbertools::limit_denominator;
364/// use num::BigInt;
365///
366/// let third = limit_denominator(&100_001.into(), &300_001.into(), &65535.into())?;
367/// assert_eq!(third, (BigInt::from(1), BigInt::from(3)));
368/// # Ok::<(), music21::Error>(())
369/// ```
370///
371/// # Errors
372///
373/// A limit below one, which no denominator meets.
374pub fn limit_denominator(
375    numerator: &BigInt,
376    denominator: &BigInt,
377    limit: &BigInt,
378) -> Result<(BigInt, BigInt)> {
379    if *limit < BigInt::one() {
380        return Err(Error::Value(
381            "max_denominator should be at least 1".to_string(),
382        ));
383    }
384    if denominator <= limit {
385        return Ok((numerator.clone(), denominator.clone()));
386    }
387    let (mut p0, mut q0, mut p1, mut q1) =
388        (BigInt::zero(), BigInt::one(), BigInt::one(), BigInt::zero());
389    let (mut n, mut d) = (numerator.clone(), denominator.clone());
390    loop {
391        // Only a fraction not in lowest terms runs out before the limit.
392        if d.is_zero() {
393            return Ok((p1, q1));
394        }
395        let a = n.div_floor(&d);
396        let q2 = &q0 + &a * &q1;
397        if q2 > *limit {
398            break;
399        }
400        let p2 = &p0 + &a * &p1;
401        (p0, q0, p1, q1) = (p1, q1, p2, q2);
402        let next = &n - &a * &d;
403        n = d;
404        d = next;
405    }
406    let k = (limit - &q0).div_floor(&q1);
407    let far_n = &p0 + &k * &p1;
408    let far_d = &q0 + &k * &q1;
409    let far_off = (&far_n * denominator - numerator * &far_d).abs();
410    let near_off = (&p1 * denominator - numerator * &q1).abs();
411    if far_off * (denominator * &q1) - near_off * (denominator * &far_d) >= BigInt::zero() {
412        return Ok((p1, q1));
413    }
414    Ok((far_n, far_d))
415}
416
417fn is_power_of_two(value: &BigInt) -> bool {
418    value.is_positive() && (value & (value - BigInt::one())).is_zero()
419}
420
421/// music21's `opFrac` for a float: the float itself where its exact
422/// fraction has a denominator up to [`DENOM_LIMIT`], and otherwise the
423/// nearest fraction within the limit, written as a float again where its
424/// denominator is a power of two.
425///
426/// It is what keeps three triplet eighths summing to exactly one beat.
427///
428/// ```
429/// use music21::common::numbertools::{op_frac, OffsetQL};
430///
431/// assert_eq!(op_frac(0.25)?, OffsetQL::Float(0.25));
432/// assert_eq!(
433///     op_frac(1.0 / 3.0)?,
434///     OffsetQL::Fraction { numerator: 1, denominator: 3 }
435/// );
436/// assert_eq!(op_frac(0.99999999842)?, OffsetQL::Float(1.0));
437/// # Ok::<(), music21::Error>(())
438/// ```
439///
440/// # Errors
441///
442/// A value that is not finite, which has no fraction.
443pub fn op_frac(value: FloatType) -> Result<OffsetQL> {
444    let (numerator, denominator) = float_ratio(value).ok_or_else(|| {
445        let which = if value.is_nan() { "NaN" } else { "Infinity" };
446        Error::Value(format!("cannot convert {which} to integer ratio"))
447    })?;
448    let limit = BigInt::from(DENOM_LIMIT);
449    if denominator <= limit {
450        return Ok(OffsetQL::Float(value));
451    }
452    let (numerator, denominator) = limit_denominator(&numerator, &denominator, &limit)?;
453    // Within the limit, a numerator is far inside an `i64`: a float with
454    // bits below 2^-16 is under 2^37.
455    let numerator = numerator.to_i64().unwrap_or(i64::MAX);
456    let denominator = denominator.to_i64().unwrap_or(1);
457    if denominator & (denominator - 1) == 0 {
458        return Ok(OffsetQL::Float(
459            numerator as FloatType / denominator as FloatType,
460        ));
461    }
462    Ok(OffsetQL::Fraction {
463        numerator,
464        denominator,
465    })
466}
467
468/// Whether `denominator` is a power of two, which is when music21's
469/// `opFrac` turns a fraction into a float.
470pub fn is_binary_denominator(denominator: &BigInt) -> bool {
471    is_power_of_two(denominator)
472}
473
474/// A value close enough to a whole number, within a millionth, as that
475/// whole number, and anything else as it is: music21's `numToIntOrFloat`.
476///
477/// ```
478/// use music21::common::numbertools::{num_to_int_or_float, IntOrFloat};
479///
480/// assert_eq!(num_to_int_or_float(0.999999999)?, IntOrFloat::Int(1.into()));
481/// assert_eq!(num_to_int_or_float(1.5)?, IntOrFloat::Float(1.5));
482/// # Ok::<(), music21::Error>(())
483/// ```
484///
485/// # Errors
486///
487/// A value that is not finite, which rounds to no whole number.
488pub fn num_to_int_or_float(value: FloatType) -> Result<IntOrFloat> {
489    let rounded = whole(value.round_ties_even())?;
490    let back = rounded.to_f64().unwrap_or(FloatType::INFINITY);
491    if is_close(back, value, 1e-6) {
492        return Ok(IntOrFloat::Int(rounded));
493    }
494    Ok(IntOrFloat::Float(value))
495}
496
497/// A whole float as a whole number, refused as Python's `int` refuses it.
498fn whole(value: FloatType) -> Result<BigInt> {
499    if value.is_nan() {
500        return Err(Error::Value(
501            "cannot convert float NaN to integer".to_string(),
502        ));
503    }
504    BigInt::from_f64(value.trunc())
505        .ok_or_else(|| Error::Value("cannot convert float infinity to integer".to_string()))
506}
507
508/// A fraction as Python's `str` writes one: `3/7`, or `3` over one.
509fn fraction_text(numerator: &BigInt, denominator: &BigInt) -> String {
510    if denominator.is_one() {
511        return numerator.to_string();
512    }
513    format!("{numerator}/{denominator}")
514}
515
516fn mixed_text(quotient: &BigInt, remainder: &BigRational) -> String {
517    let fraction = fraction_text(remainder.numer(), remainder.denom());
518    if !quotient.is_zero() {
519        if remainder.is_zero() {
520            return quotient.to_string();
521        }
522        return format!("{quotient} {fraction}");
523    }
524    if !remainder.is_zero() {
525        return fraction;
526    }
527    "0".to_string()
528}
529
530/// A number as a whole number and a fraction, `1 1/3`, with the fraction's
531/// denominator limited to `limit`: music21's `mixedNumeral` for a float.
532///
533/// ```
534/// use music21::common::numbertools::mixed_numeral;
535///
536/// assert_eq!(mixed_numeral(1.333333, 65535)?, "1 1/3");
537/// assert_eq!(mixed_numeral(-0.333333, 65535)?, "-1/3");
538/// assert_eq!(mixed_numeral(2.0000001, 65535)?, "2");
539/// # Ok::<(), music21::Error>(())
540/// ```
541///
542/// # Errors
543///
544/// A value that is not finite, or a limit below one.
545pub fn mixed_numeral(value: FloatType, limit: u64) -> Result<String> {
546    let (mut quotient, remainder) = float_divmod(value, 1.0);
547    let (numerator, denominator) = float_ratio(remainder)
548        .ok_or_else(|| Error::Value("cannot convert NaN to integer ratio".to_string()))?;
549    let (numerator, denominator) =
550        limit_denominator(&numerator, &denominator, &BigInt::from(limit))?;
551    let mut fraction = BigRational::new(numerator, denominator);
552    if quotient < -1.0 {
553        quotient += 1.0;
554        fraction = BigRational::one() - fraction;
555    } else if quotient == -1.0 {
556        quotient = 0.0;
557        fraction -= BigRational::one();
558    }
559    Ok(mixed_text(&whole(quotient)?, &fraction))
560}
561
562/// [`mixed_numeral`] for an exact fraction, whose denominator is not
563/// limited: `-4 3/7` for -31/7.
564///
565/// ```
566/// use music21::common::numbertools::mixed_numeral_fraction;
567///
568/// assert_eq!(mixed_numeral_fraction(&31.into(), &7.into())?, "4 3/7");
569/// assert_eq!(mixed_numeral_fraction(&(-4).into(), &5.into())?, "-4/5");
570/// # Ok::<(), music21::Error>(())
571/// ```
572///
573/// # Errors
574///
575/// A denominator of nought, or a fraction too large for a float, which
576/// music21 passes through on the way.
577pub fn mixed_numeral_fraction(numerator: &BigInt, denominator: &BigInt) -> Result<String> {
578    if denominator.is_zero() {
579        return Err(Error::Value("Fraction(_, 0)".to_string()));
580    }
581    let value = BigRational::new(numerator.clone(), denominator.clone());
582    let quotient = whole(value.to_f64().unwrap_or(FloatType::NAN))?;
583    let mut remainder = &value - BigRational::from_integer(quotient.clone());
584    if quotient.is_negative() {
585        remainder = -remainder;
586    }
587    Ok(mixed_text(&quotient, &remainder))
588}
589
590/// A number rounded to the nearest half, a quarter rounding up, as
591/// music21's `roundToHalfInteger` does: a whole number where it lands on
592/// one, and a float on a half.
593///
594/// ```
595/// use music21::common::numbertools::{round_to_half_integer, IntOrFloat};
596///
597/// assert_eq!(round_to_half_integer(1.35)?, IntOrFloat::Float(1.5));
598/// assert_eq!(round_to_half_integer(1.75)?, IntOrFloat::Int(2.into()));
599/// assert_eq!(round_to_half_integer(-0.25)?, IntOrFloat::Int(0.into()));
600/// # Ok::<(), music21::Error>(())
601/// ```
602///
603/// # Errors
604///
605/// A value that is not finite.
606pub fn round_to_half_integer(value: FloatType) -> Result<IntOrFloat> {
607    let (floor, fraction) = float_divmod(value, 1.0);
608    let floor = whole(floor)?;
609    if fraction < 0.25 {
610        return Ok(IntOrFloat::Int(floor));
611    }
612    if fraction < 0.75 {
613        let floor = floor.to_f64().unwrap_or(FloatType::INFINITY);
614        return Ok(IntOrFloat::Float(floor + 0.5));
615    }
616    Ok(IntOrFloat::Int(floor + 1))
617}
618
619/// The third or sixth a value is within `grain` of, as [`op_frac`] gives
620/// it, or nothing for a value near neither: music21's
621/// `addFloatPrecision`, which leaves such a value alone.
622///
623/// ```
624/// use music21::common::numbertools::{add_float_precision, OffsetQL};
625///
626/// assert_eq!(
627///     add_float_precision(0.33, 1e-2),
628///     Some(OffsetQL::Fraction { numerator: 1, denominator: 3 })
629/// );
630/// assert_eq!(add_float_precision(0.2, 1e-2), None);
631/// ```
632pub fn add_float_precision(value: FloatType, grain: FloatType) -> Option<OffsetQL> {
633    [1.0 / 3.0, 2.0 / 3.0, 1.0 / 6.0, 5.0 / 6.0]
634        .into_iter()
635        .find(|candidate| is_close(value, *candidate, grain))
636        .and_then(|candidate| op_frac(candidate).ok())
637}
638
639/// A float written with `max_digits` decimals and its trailing zeros
640/// dropped, one decimal always kept: music21's `strTrimFloat`.
641///
642/// ```
643/// use music21::common::numbertools::str_trim_float;
644///
645/// assert_eq!(str_trim_float(42.3333333333, 4)?, "42.3333");
646/// assert_eq!(str_trim_float(6.66666666666666, 2)?, "6.67");
647/// assert_eq!(str_trim_float(2.0, 4)?, "2.0");
648/// # Ok::<(), music21::Error>(())
649/// ```
650///
651/// # Errors
652///
653/// No decimals asked for, or a value that is not finite: neither is
654/// written with a decimal point, which music21 looks for.
655pub fn str_trim_float(value: FloatType, max_digits: usize) -> Result<String> {
656    let written = format!("{value:.max_digits$}");
657    let point = written
658        .find('.')
659        .ok_or_else(|| Error::Value("substring not found".to_string()))?;
660    let bytes = written.as_bytes();
661    let mut length = written.len();
662    for index in (point + 2..written.len()).rev() {
663        if bytes[index] != b'0' {
664            break;
665        }
666        length -= 1;
667    }
668    Ok(written[..length].to_string())
669}
670
671/// The multiple of a unit nearest a value, as [`nearest_multiple`] finds
672/// it.
673#[derive(Clone, Copy, Debug, PartialEq)]
674pub struct NearestMultiple {
675    /// The multiple itself.
676    pub multiple: FloatType,
677    /// How far the value is from it, to seven decimals.
678    pub error: FloatType,
679    /// The value less the multiple, to seven decimals.
680    pub signed_error: FloatType,
681}
682
683/// The multiple of `unit` nearest a value that is not negative, how far
684/// off it is and which way: music21's `nearestMultiple`. Half way rounds
685/// down.
686///
687/// ```
688/// use music21::common::numbertools::nearest_multiple;
689///
690/// let found = nearest_multiple(0.4, 0.25)?;
691/// assert_eq!(found.multiple, 0.5);
692/// assert_eq!(found.signed_error, -0.1);
693/// # Ok::<(), music21::Error>(())
694/// ```
695///
696/// # Errors
697///
698/// A negative value, or one that falls between no two multiples.
699pub fn nearest_multiple(value: FloatType, unit: FloatType) -> Result<NearestMultiple> {
700    if value < 0.0 {
701        return Err(Error::Value(format!(
702            "n ({}) is less than zero. Thus, cannot find the nearest multiple for a value \
703             less than the unit, {}",
704            float_repr(value),
705            float_repr(unit)
706        )));
707    }
708    // Python's `math.floor` gives a whole number, which has no sign.
709    let multiple = (value / unit).floor() + 0.0;
710    let low = unit * multiple;
711    let high = unit * (multiple + 1.0);
712    if low >= value && value >= high {
713        return Err(Error::Value(format!(
714            "cannot place n between multiples: {}, {}",
715            float_repr(low),
716            float_repr(high)
717        )));
718    }
719    if low <= value && value <= low + unit / 2.0 {
720        let error = round_digits(value - low, 7);
721        return Ok(NearestMultiple {
722            multiple: low,
723            error,
724            signed_error: error,
725        });
726    }
727    Ok(NearestMultiple {
728        multiple: high,
729        error: round_digits(high - value, 7),
730        signed_error: round_digits(value - high, 7),
731    })
732}
733
734/// music21's `_DOT_LOOKUP`: what one to eight dots multiply a length by.
735const DOT_MULTIPLIERS: [FloatType; 9] = [
736    1.0, 1.5, 1.75, 1.875, 1.9375, 1.96875, 1.984375, 1.9921875, 1.99609375,
737];
738
739/// What `dots` dots multiply a note's length by: music21's
740/// `dotMultiplier`. It is always exact, since its denominator is a power
741/// of two.
742///
743/// ```
744/// use music21::common::numbertools::dot_multiplier;
745///
746/// assert_eq!(dot_multiplier(0), 1.0);
747/// assert_eq!(dot_multiplier(2), 1.75);
748/// ```
749pub fn dot_multiplier(dots: u32) -> FloatType {
750    if let Some(found) = DOT_MULTIPLIERS.get(dots as usize) {
751        return *found;
752    }
753    let dots = FloatType::from(dots);
754    (2.0f64.powf(dots + 1.0) - 1.0) / 2.0f64.powf(dots)
755}
756
757/// The tuplet ratio, in lowest terms, that a simple decimal stands for:
758/// music21's `decimalToTuplet`. Below one, the denominator is the larger.
759///
760/// ```
761/// use music21::common::numbertools::decimal_to_tuplet;
762///
763/// assert_eq!(decimal_to_tuplet(1.25)?, (5, 4));
764/// assert_eq!(decimal_to_tuplet(0.8)?, (4, 5));
765/// # Ok::<(), music21::Error>(())
766/// ```
767///
768/// # Errors
769///
770/// A value not above nought, or one no ratio with a denominator under a
771/// thousand comes within a ten-millionth of.
772pub fn decimal_to_tuplet(value: FloatType) -> Result<(i128, i128)> {
773    if value <= 0.0 {
774        return Err(Error::Value("number must be greater than zero".to_string()));
775    }
776    let flipped = value < 1.0;
777    let value = if flipped { 1.0 / value } else { value };
778    let multiplier = value.trunc();
779    let working = value / multiplier;
780    let (numerator, denominator) =
781        simple_fraction(working).ok_or_else(|| Error::Value("No such luck".to_string()))?;
782    let scaled = FloatType::from(numerator) * multiplier;
783    let divisor = num::integer::gcd(scaled as i128, i128::from(denominator));
784    let numerator = (scaled / divisor as FloatType) as i128;
785    let denominator = (FloatType::from(denominator) / divisor as FloatType) as i128;
786    if flipped {
787        return Ok((denominator, numerator));
788    }
789    Ok((numerator, denominator))
790}
791
792/// The first `j / i`, `i` under a thousand and `j` from `i` to under twice
793/// it, within a ten-millionth of `value`.
794fn simple_fraction(value: FloatType) -> Option<(u32, u32)> {
795    for index in 1..1000u32 {
796        for numerator in index..index * 2 {
797            if is_close(
798                value,
799                FloatType::from(numerator) / FloatType::from(index),
800                1e-7,
801            ) {
802                return Some((numerator, index));
803            }
804        }
805    }
806    None
807}
808
809/// Each value as its share of their sum: music21's
810/// `unitNormalizeProportion`.
811///
812/// ```
813/// use music21::common::numbertools::unit_normalize_proportion;
814///
815/// assert_eq!(unit_normalize_proportion(&[1.0, 1.0, 2.0])?, [0.25, 0.25, 0.5]);
816/// # Ok::<(), music21::Error>(())
817/// ```
818///
819/// # Errors
820///
821/// A negative value. Values summing to nought give no finite shares.
822pub fn unit_normalize_proportion(values: &[FloatType]) -> Result<Vec<FloatType>> {
823    let mut sum = 0.0;
824    for value in values {
825        if *value < 0.0 {
826            return Err(Error::Value("value members must be positive".to_string()));
827        }
828        sum += value;
829    }
830    Ok(values.iter().map(|value| value / sum).collect())
831}
832
833/// The span of the unit interval each value's share of their sum takes,
834/// one after another, the last ending at exactly one: music21's
835/// `unitBoundaryProportion`.
836///
837/// ```
838/// use music21::common::numbertools::unit_boundary_proportion;
839///
840/// assert_eq!(
841///     unit_boundary_proportion(&[1.0, 1.0, 2.0])?,
842///     [(0.0, 0.25), (0.25, 0.5), (0.5, 1.0)]
843/// );
844/// # Ok::<(), music21::Error>(())
845/// ```
846///
847/// # Errors
848///
849/// A negative value.
850pub fn unit_boundary_proportion(values: &[FloatType]) -> Result<Vec<(FloatType, FloatType)>> {
851    let shares = unit_normalize_proportion(values)?;
852    let mut bounds = Vec::with_capacity(shares.len());
853    let mut sum = 0.0;
854    for (index, share) in shares.iter().enumerate() {
855        if index == shares.len() - 1 {
856            bounds.push((sum, 1.0));
857            break;
858        }
859        bounds.push((sum, sum + share));
860        sum += share;
861    }
862    Ok(bounds)
863}
864
865/// Which of the weighted choices a draw `draw` from the unit interval
866/// lands on, as music21's `weightedSelection` picks one: the last where it
867/// lands on none.
868///
869/// ```
870/// use music21::common::numbertools::weighted_index;
871///
872/// assert_eq!(weighted_index(&[1.0, 3.0], 0.2)?, 0);
873/// assert_eq!(weighted_index(&[1.0, 3.0], 0.3)?, 1);
874/// # Ok::<(), music21::Error>(())
875/// ```
876///
877/// # Errors
878///
879/// A negative weight.
880pub fn weighted_index(weights: &[FloatType], draw: FloatType) -> Result<usize> {
881    let bounds = unit_boundary_proportion(weights)?;
882    let found = bounds
883        .iter()
884        .position(|(low, high)| *low <= draw && draw < *high);
885    Ok(found.unwrap_or(bounds.len().saturating_sub(1)))
886}
887
888/// The greatest length that goes a whole number of times, to within
889/// `grain`, into every one of `values`: music21's `approximateGCD`, which
890/// tries the smallest value, then each value divided by one to sixteen.
891///
892/// ```
893/// use music21::common::numbertools::approximate_gcd;
894///
895/// assert_eq!(approximate_gcd(&[2.5, 10.0, 0.25], 1e-4)?, 0.25);
896/// assert_eq!(approximate_gcd(&[1.5, 5.0, 2.0, 7.0], 1e-4)?, 0.5);
897/// # Ok::<(), music21::Error>(())
898/// ```
899///
900/// # Errors
901///
902/// No values, or no length that goes into them all. The smallest value
903/// being nought gives no finite answer.
904pub fn approximate_gcd(values: &[FloatType], grain: FloatType) -> Result<FloatType> {
905    let lowest = values
906        .iter()
907        .copied()
908        .reduce(FloatType::min)
909        .ok_or_else(|| Error::Value("min() iterable argument is empty".to_string()))?;
910    let divides = values.iter().all(|value| {
911        let adjusted = value / lowest;
912        is_close(adjusted - adjusted.trunc(), 0.0, grain)
913    });
914    if divides {
915        return Ok(lowest);
916    }
917    let divisions: Vec<Vec<FloatType>> = values
918        .iter()
919        .map(|value| (1..=16).map(|d| value / FloatType::from(d)).collect())
920        .collect();
921    divisions
922        .iter()
923        .flatten()
924        .copied()
925        .filter(|candidate| {
926            divisions
927                .iter()
928                .all(|row| row.iter().any(|x| is_close(*x, *candidate, grain)))
929        })
930        .reduce(FloatType::max)
931        .ok_or_else(|| Error::Value("cannot find a common divisor".to_string()))
932}
933
934/// Whether each value is one more than the one before: music21's
935/// `contiguousList`, which wants them sorted first. An empty list is
936/// contiguous here; music21 cannot look at its first value.
937///
938/// ```
939/// use music21::common::numbertools::contiguous_list;
940///
941/// assert!(contiguous_list(&[3, 4, 5, 6]));
942/// assert!(!contiguous_list(&[3, 4, 5, 6, 8]));
943/// ```
944pub fn contiguous_list<T>(values: &[T]) -> bool
945where
946    T: Copy + PartialEq + std::ops::Add<Output = T> + One,
947{
948    values.windows(2).all(|pair| pair[1] == pair[0] + T::one())
949}
950
951/// The runs of sorted values each one more than the one before, as ranges
952/// of their positions: what music21's `groupContiguousIntegers` groups.
953///
954/// ```
955/// use music21::common::numbertools::contiguous_runs;
956///
957/// assert_eq!(contiguous_runs(&[3, 4, 6, 7, 20]), [0..2, 2..4, 4..5]);
958/// ```
959pub fn contiguous_runs<T>(sorted: &[T]) -> Vec<Range<usize>>
960where
961    T: Copy + PartialEq + std::ops::Add<Output = T> + One,
962{
963    let mut runs = Vec::new();
964    let mut start = 0;
965    for index in 1..=sorted.len() {
966        let ends = index == sorted.len() || sorted[index] != sorted[index - 1] + T::one();
967        if ends {
968            runs.push(start..index);
969            start = index;
970        }
971    }
972    runs
973}
974
975/// A Roman numeral, upper or lower case, read as a number: music21's
976/// `fromRoman`. The medieval `IIII` and `IC` are read too, unless
977/// `strict_modern` refuses a subtraction of less than a tenth.
978///
979/// ```
980/// use music21::common::numbertools::from_roman;
981///
982/// assert_eq!(from_roman("MCDLXXXIX", false)?, 1489);
983/// assert_eq!(from_roman("ic", false)?, 99);
984/// assert!(from_roman("ic", true).is_err());
985/// # Ok::<(), music21::Error>(())
986/// ```
987///
988/// # Errors
989///
990/// A letter that is not a numeral, or a subtraction no Roman wrote.
991pub fn from_roman(text: &str, strict_modern: bool) -> Result<i64> {
992    let upper = text.to_uppercase();
993    let value_of = |letter: char| match letter {
994        'M' => Some(1000),
995        'D' => Some(500),
996        'C' => Some(100),
997        'L' => Some(50),
998        'X' => Some(10),
999        'V' => Some(5),
1000        'I' => Some(1),
1001        _ => None,
1002    };
1003    let values: Vec<i64> = upper
1004        .chars()
1005        .map(value_of)
1006        .collect::<Option<_>>()
1007        .ok_or_else(|| Error::Value(format!("value is not a valid roman numeral: {upper}")))?;
1008    let mut sum = 0;
1009    for (index, value) in values.iter().enumerate() {
1010        let mut value = *value;
1011        if let Some(next) = values.get(index + 1).copied() {
1012            if next > value && [1, 10, 100].contains(&value) {
1013                if strict_modern && next >= value * 10 {
1014                    return Err(Error::Value(format!(
1015                        "input contains an invalid subtraction element (modern interpretation): \
1016                         {text}"
1017                    )));
1018                }
1019                value = -value;
1020            } else if next > value {
1021                return Err(Error::Value(format!(
1022                    "input contains an invalid subtraction element: {text}"
1023                )));
1024            }
1025        }
1026        sum += value;
1027    }
1028    Ok(sum)
1029}
1030
1031/// A number from 1 to 3999 as a Roman numeral: music21's `toRoman`.
1032///
1033/// ```
1034/// use music21::common::numbertools::to_roman;
1035///
1036/// assert_eq!(to_roman(1999)?, "MCMXCIX");
1037/// assert!(to_roman(0).is_err());
1038/// # Ok::<(), music21::Error>(())
1039/// ```
1040///
1041/// # Errors
1042///
1043/// A number outside that range.
1044pub fn to_roman(number: i64) -> Result<String> {
1045    if !(1..4000).contains(&number) {
1046        return Err(Error::Value(
1047            "Argument must be between 1 and 3999".to_string(),
1048        ));
1049    }
1050    const NUMERALS: [(i64, &str); 13] = [
1051        (1000, "M"),
1052        (900, "CM"),
1053        (500, "D"),
1054        (400, "CD"),
1055        (100, "C"),
1056        (90, "XC"),
1057        (50, "L"),
1058        (40, "XL"),
1059        (10, "X"),
1060        (9, "IX"),
1061        (5, "V"),
1062        (4, "IV"),
1063        (1, "I"),
1064    ];
1065    let mut rest = number;
1066    let mut written = String::new();
1067    for (value, numeral) in NUMERALS {
1068        let count = rest / value;
1069        written.push_str(&numeral.repeat(count as usize));
1070        rest -= value * count;
1071    }
1072    Ok(written)
1073}
1074
1075/// The letters written after a number to make it an ordinal, `st`, `nd`,
1076/// `rd` or `th`, with an `s` after all but `st` for `plural`: music21's
1077/// `ordinalAbbreviation`.
1078///
1079/// ```
1080/// use music21::common::numbertools::ordinal_abbreviation;
1081///
1082/// assert_eq!(ordinal_abbreviation(3, false), "rd");
1083/// assert_eq!(ordinal_abbreviation(12, false), "th");
1084/// assert_eq!(ordinal_abbreviation(255, true), "ths");
1085/// ```
1086pub fn ordinal_abbreviation(value: i64, plural: bool) -> &'static str {
1087    let suffix = if (11..=13).contains(&value.rem_euclid(100)) {
1088        "th"
1089    } else {
1090        match value.rem_euclid(10) {
1091            1 => "st",
1092            2 => "nd",
1093            3 => "rd",
1094            _ => "th",
1095        }
1096    };
1097    if !plural || suffix == "st" {
1098        return suffix;
1099    }
1100    match suffix {
1101        "nd" => "nds",
1102        "rd" => "rds",
1103        _ => "ths",
1104    }
1105}
1106
1107/// music21's `ordinalsToNumbers`, in the order it fills it: each ordinal
1108/// name, lowered and not, and abbreviated, `1st`, and each interval name
1109/// that differs from it, mapped to its number.
1110pub fn ordinals_to_numbers() -> Vec<(String, usize)> {
1111    let mut entries = Vec::new();
1112    for (index, name) in ORDINAL_NAMES.iter().enumerate() {
1113        entries.push(((*name).to_string(), index));
1114        entries.push((name.to_lowercase(), index));
1115        let abbreviation = ordinal_abbreviation(index as i64, false);
1116        entries.push((format!("{index}{abbreviation}"), index));
1117        let music = MUSIC_ORDINAL_NAMES[index];
1118        if music != *name {
1119            entries.push((music.to_string(), index));
1120            entries.push((music.to_lowercase(), index));
1121        }
1122    }
1123    entries
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129    use std::convert::TryFrom;
1130
1131    #[test]
1132    fn floats_past_the_limit_snap_to_a_fraction() {
1133        assert_eq!(
1134            op_frac(0.123456789).unwrap(),
1135            OffsetQL::Fraction {
1136                numerator: 10,
1137                denominator: 81
1138            }
1139        );
1140        assert_eq!(op_frac(0.000001).unwrap(), OffsetQL::Float(0.0));
1141        assert_eq!(op_frac(-1.0 / 3.0).unwrap().to_f64(), -1.0 / 3.0);
1142        assert!(op_frac(f64::NAN).is_err());
1143        assert!(op_frac(f64::INFINITY).is_err());
1144    }
1145
1146    #[test]
1147    fn music21s_own_limit_matches_pythons() {
1148        let limited = limit_denominator(&100_001.into(), &300_001.into(), &65535.into()).unwrap();
1149        assert_eq!(limited, (BigInt::from(1), BigInt::from(3)));
1150        let (n, d) = float_ratio(1e-300).unwrap();
1151        let limited = limit_denominator(&n, &d, &65535.into()).unwrap();
1152        assert_eq!(limited, (BigInt::from(0), BigInt::from(1)));
1153    }
1154
1155    #[test]
1156    fn mixed_numerals_written_as_music21_writes_them() {
1157        assert_eq!(mixed_numeral(-1.333333, 65535).unwrap(), "-1 1/3");
1158        assert_eq!(mixed_numeral(0.0, 65535).unwrap(), "0");
1159        assert_eq!(mixed_numeral(-0.0, 65535).unwrap(), "0");
1160        assert_eq!(
1161            mixed_numeral(2.0000001, 10_000_000).unwrap(),
1162            "2 1/10000000"
1163        );
1164        let fraction = |n: i64, d: i64| mixed_numeral_fraction(&n.into(), &d.into()).unwrap();
1165        assert_eq!(fraction(-31, 7), "-4 3/7");
1166        assert_eq!(fraction(-1, 5), "-1/5");
1167        assert_eq!(fraction(1, 5), "1/5");
1168    }
1169
1170    #[test]
1171    fn floats_written_as_python_writes_them() {
1172        assert_eq!(float_repr(-0.5), "-0.5");
1173        assert_eq!(float_repr(23404.0), "23404.0");
1174        assert_eq!(float_repr(1e22), "1e+22");
1175        assert_eq!(float_repr(1.5e-7), "1.5e-07");
1176        assert_eq!(float_repr(0.0001), "0.0001");
1177        assert_eq!(float_repr(123456789012345.6), "123456789012345.6");
1178        assert_eq!(round_digits(0.35 - 0.25, 7), 0.1);
1179    }
1180
1181    #[test]
1182    fn halves_round_up_on_the_boundaries() {
1183        let int = |value: i64| IntOrFloat::Int(value.into());
1184        assert_eq!(round_to_half_integer(1.2).unwrap(), int(1));
1185        assert_eq!(round_to_half_integer(0.25).unwrap(), IntOrFloat::Float(0.5));
1186        assert_eq!(round_to_half_integer(0.75).unwrap(), int(1));
1187        assert_eq!(
1188            round_to_half_integer(-0.26).unwrap(),
1189            IntOrFloat::Float(-0.5)
1190        );
1191        assert!(round_to_half_integer(f64::INFINITY).is_err());
1192    }
1193
1194    #[test]
1195    fn nearest_multiples_and_their_errors() {
1196        let found = nearest_multiple(23404.0 - 0.0625, 0.125).unwrap();
1197        assert_eq!(
1198            (found.multiple, found.error, found.signed_error),
1199            (23403.875, 0.0625, 0.0625)
1200        );
1201        let error = nearest_multiple(-0.5, 0.125).unwrap_err();
1202        assert_eq!(
1203            error.to_string(),
1204            "Value error: n (-0.5) is less than zero. Thus, cannot find the nearest multiple \
1205             for a value less than the unit, 0.125"
1206        );
1207    }
1208
1209    /// Python's `math.floor` gives an int, which has no sign, so music21's
1210    /// multiple of nought is a positive nought even for a negative one.
1211    #[test]
1212    fn the_multiple_of_negative_nought_is_unsigned() {
1213        let found = nearest_multiple(-0.0, 0.125).unwrap();
1214        assert!(found.multiple.is_sign_positive());
1215        assert!(found.error.is_sign_negative());
1216    }
1217
1218    #[test]
1219    fn numbers_and_strings_of_music21s_examples() {
1220        assert_eq!(str_trim_float(-5.0, 4).unwrap(), "-5.0");
1221        assert!(str_trim_float(1.0, 0).is_err());
1222        assert_eq!(decimal_to_tuplet(1.5).unwrap(), (3, 2));
1223        assert!(decimal_to_tuplet(-0.02).is_err());
1224        assert_eq!(dot_multiplier(3), 1.875);
1225        assert_eq!(dot_multiplier(10), 2047.0 / 1024.0);
1226        assert_eq!(approximate_gcd(&[2.0, 5.0, 10.0], 1e-4).unwrap(), 1.0);
1227        let third = approximate_gcd(&[5.0 / 3.0, 2.0 / 3.0, 5.0 / 6.0, 3.0 / 6.0], 1e-4).unwrap();
1228        assert_eq!(str_trim_float(third, 4).unwrap(), "0.1667");
1229        assert_eq!(
1230            num_to_int_or_float(1.00003).unwrap(),
1231            IntOrFloat::Float(1.00003)
1232        );
1233        assert!(unit_normalize_proportion(&[0.0, -2.0, -8.0]).is_err());
1234        assert_eq!(weighted_index(&[1.0, 0.0], 0.99).unwrap(), 0);
1235        assert_eq!(contiguous_runs(&[3, 3, 4]), [0..1, 1..3]);
1236        assert_eq!(contiguous_runs::<i64>(&[]), Vec::<Range<usize>>::new());
1237    }
1238
1239    #[test]
1240    fn roman_numerals_both_ways() {
1241        assert_eq!(from_roman("vii", false).unwrap(), 7);
1242        assert_eq!(from_roman("MCCCCLXXXIX", false).unwrap(), 1489);
1243        assert_eq!(
1244            from_roman("vx", false).unwrap_err().to_string(),
1245            "Value error: input contains an invalid subtraction element: vx"
1246        );
1247        assert_eq!(
1248            from_roman("ab", false).unwrap_err().to_string(),
1249            "Value error: value is not a valid roman numeral: AB"
1250        );
1251        assert_eq!(to_roman(7).unwrap(), "VII");
1252        assert!(to_roman(4000).is_err());
1253    }
1254
1255    #[test]
1256    fn ordinals_map_to_their_numbers() {
1257        let entries = ordinals_to_numbers();
1258        let find = |name: &str| entries.iter().find(|(key, _)| key == name).map(|(_, n)| *n);
1259        assert_eq!(find("unison"), Some(1));
1260        assert_eq!(find("1st"), Some(1));
1261        assert_eq!(find("Octave"), Some(8));
1262        assert_eq!(find("8th"), Some(8));
1263        assert_eq!(find("22nd"), Some(22));
1264        assert_eq!(
1265            &entries[..3],
1266            &[
1267                ("Zeroth".to_string(), 0),
1268                ("zeroth".to_string(), 0),
1269                ("0th".to_string(), 0)
1270            ]
1271        );
1272    }
1273
1274    #[test]
1275    fn test_music_ordinal_try_from_integer_valid() {
1276        let cases = [
1277            (0, MusicOrdinals::Zeroth),
1278            (1, MusicOrdinals::Unison),
1279            (2, MusicOrdinals::Second),
1280            (8, MusicOrdinals::Octave),
1281            (15, MusicOrdinals::DoubleOctave),
1282            (22, MusicOrdinals::TripleOctave),
1283        ];
1284        for (input, expected) in cases {
1285            let ordinal = MusicOrdinals::try_from(input).unwrap();
1286            assert_eq!(ordinal, expected);
1287        }
1288    }
1289
1290    #[test]
1291    fn test_music_ordinal_try_from_integer_invalid() {
1292        let result = MusicOrdinals::try_from(23);
1293        assert!(result.is_err());
1294        if let Err(Error::Ordinal(msg)) = result {
1295            assert!(msg.contains("Invalid MusicOrdinals value: 23"));
1296        } else {
1297            panic!("Expected Error::Ordinal error");
1298        }
1299    }
1300
1301    #[test]
1302    fn test_music_ordinal_try_from_str_valid() {
1303        let cases = [
1304            ("zeroth", MusicOrdinals::Zeroth),
1305            ("Unison", MusicOrdinals::Unison),
1306            ("SECOND", MusicOrdinals::Second),
1307            ("octave", MusicOrdinals::Octave),
1308            ("double-octave", MusicOrdinals::DoubleOctave),
1309            ("triple-octave", MusicOrdinals::TripleOctave),
1310        ];
1311        for (input, expected) in cases {
1312            let ordinal = MusicOrdinals::try_from(input).unwrap();
1313            assert_eq!(ordinal, expected);
1314        }
1315    }
1316
1317    #[test]
1318    fn test_music_ordinal_try_from_str_invalid() {
1319        let result = MusicOrdinals::try_from("invalid");
1320        assert!(result.is_err());
1321        if let Err(Error::Ordinal(msg)) = result {
1322            assert!(msg.contains("Invalid MusicOrdinals string: invalid"));
1323        } else {
1324            panic!("Expected Error::Ordinal error");
1325        }
1326    }
1327
1328    #[test]
1329    fn test_music_ordinal_display() {
1330        let ordinal = MusicOrdinals::TwentyFirst;
1331        assert_eq!(format!("{ordinal}"), "Twenty-first");
1332    }
1333
1334    #[test]
1335    fn test_static_arrays() {
1336        let musical_strings: Vec<_> = MUSICAL_ORDINAL_STRINGS.iter().cloned().collect();
1337        assert_eq!(musical_strings[0], "Zeroth");
1338        assert_eq!(musical_strings[1], "Unison");
1339        assert_eq!(musical_strings[8], "Octave");
1340        assert_eq!(musical_strings[15], "Double-octave");
1341        assert_eq!(musical_strings[22], "Triple-octave");
1342    }
1343}