Skip to main content

music21_rs/tuningsystem/
monzo.rs

1//! Monzos and vals, the two vectors regular temperament theory is written in.
2//!
3//! A *monzo* is an interval written as the exponents of the primes that
4//! multiply out to its ratio, so the syntonic comma `81/80` is
5//! `2^-4 * 3^4 * 5^-1` and reads `[-4 4 -1⟩`. A *val* is the other side of the
6//! same coin: a map from those primes to how many steps of some tuning each
7//! one is worth, so twelve-tone equal temperament is `⟨12 19 28]` — twelve
8//! steps to the octave, nineteen to the twelfth, twenty-eight to the
9//! seventeenth. Pairing the two says how wide an interval comes out in a
10//! tuning, and a val that maps a comma to nothing is a tuning that *tempers it
11//! out*: `⟨12 19 28]` sends `[-4 4 -1⟩` to zero, which is why four fifths and
12//! a major third are the same note on a piano.
13
14use crate::defaults::{FloatType, FractionType, IntegerType};
15use crate::error::{Error, Result};
16
17use std::fmt::{Display, Formatter};
18use std::str::FromStr;
19
20/// The primes a [`Monzo`] or [`Val`] is written over, in order.
21///
22/// The vectors are positional — an entry means the prime standing at its
23/// index — so this list is what fixes what a monzo says. Twenty-five primes
24/// reach the 97-limit, well past anything regular temperament practice uses.
25pub const PRIMES: [IntegerType; 25] = [
26    2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
27];
28
29/// The index of `prime` in [`PRIMES`], or `None` if it is not one of them.
30fn prime_index(prime: IntegerType) -> Option<usize> {
31    PRIMES.iter().position(|&candidate| candidate == prime)
32}
33
34/// How many entries a monzo or val needs to reach `limit`.
35fn entries_for_limit(limit: IntegerType) -> Result<usize> {
36    prime_index(limit).map(|index| index + 1).ok_or_else(|| {
37        Error::TuningSystem(format!(
38            "{limit} is not a prime this module carries; the largest is {}",
39            PRIMES[PRIMES.len() - 1]
40        ))
41    })
42}
43
44/// Trailing zeros say nothing, so a monzo is stored without them.
45fn trimmed(mut entries: Vec<IntegerType>) -> Vec<IntegerType> {
46    while entries.last() == Some(&0) {
47        let _ = entries.pop();
48    }
49    entries
50}
51
52/// Parses the numbers between a vector's brackets.
53fn parse_entries(body: &str) -> Result<Vec<IntegerType>> {
54    body.split([' ', ',', '\t'])
55        .filter(|piece| !piece.is_empty())
56        .map(|piece| {
57            piece.parse::<IntegerType>().map_err(|_| {
58                Error::TuningSystem(format!("{piece} is not a whole number in a monzo or val"))
59            })
60        })
61        .collect()
62}
63
64/// How many times `prime` divides `value`.
65fn factor_out(mut value: IntegerType, prime: IntegerType) -> IntegerType {
66    let mut count = 0;
67    while value % prime == 0 {
68        value /= prime;
69        count += 1;
70    }
71    count
72}
73
74/// What is left of `value` once every prime in [`PRIMES`] is divided out.
75fn strip_primes(mut value: IntegerType) -> IntegerType {
76    for &prime in &PRIMES {
77        while value % prime == 0 {
78            value /= prime;
79        }
80    }
81    value
82}
83
84/// An interval written as the exponents of the primes making up its ratio.
85///
86/// The entries are positional over [`PRIMES`], so `[-4 4 -1⟩` is
87/// `2^-4 * 3^4 * 5^-1`, the syntonic comma. Multiplying two intervals adds
88/// their monzos, which is the whole reason for writing intervals this way.
89///
90/// ```
91/// use music21_rs::tuningsystem::Monzo;
92///
93/// let comma = Monzo::from_ratio(81, 80)?;
94/// assert_eq!(comma.exponents(), [-4, 4, -1]);
95/// assert_eq!(comma.limit(), Some(5));
96/// assert_eq!(comma.to_string(), "[-4 4 -1⟩");
97/// # Ok::<(), music21_rs::Error>(())
98/// ```
99#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101#[must_use]
102pub struct Monzo {
103    exponents: Vec<IntegerType>,
104}
105
106impl Monzo {
107    /// Builds a monzo from prime exponents, lowest prime first.
108    pub fn new(exponents: impl Into<Vec<IntegerType>>) -> Self {
109        Self {
110            exponents: trimmed(exponents.into()),
111        }
112    }
113
114    /// The unison, whose every exponent is nought.
115    pub fn unison() -> Self {
116        Self::default()
117    }
118
119    /// Factors a ratio into prime exponents.
120    ///
121    /// Errors on a ratio that is not positive, or one carrying a prime factor
122    /// larger than [`PRIMES`] reaches.
123    pub fn from_ratio(numerator: IntegerType, denominator: IntegerType) -> Result<Self> {
124        if numerator <= 0 || denominator <= 0 {
125            return Err(Error::TuningSystem(format!(
126                "{numerator}/{denominator} is not a positive ratio"
127            )));
128        }
129        if strip_primes(numerator) * strip_primes(denominator) != 1 {
130            return Err(Error::TuningSystem(format!(
131                "{numerator}/{denominator} has a prime factor past the {}-limit",
132                PRIMES[PRIMES.len() - 1]
133            )));
134        }
135        let exponents = PRIMES
136            .iter()
137            .map(|&prime| factor_out(numerator, prime) - factor_out(denominator, prime))
138            .collect::<Vec<_>>();
139        Ok(Self::new(exponents))
140    }
141
142    /// The Pythagorean reading of a spelled interval: the octaves and fifths
143    /// that move `staff_steps` staff positions and `semitones` semitones.
144    ///
145    /// A spelling is a place on the chain of fifths, so this is exact for any
146    /// accidental: a major third, two steps and four semitones, is `81/64`
147    /// (`[-6 4⟩`), and the augmented unison `C`–`C#` is `[-11 7⟩`, which is not
148    /// the minor second `C`–`Db`, `[8 -5⟩`.
149    pub fn pythagorean(staff_steps: IntegerType, semitones: IntegerType) -> Self {
150        // An octave is seven steps and twelve semitones, a twelfth (3/1)
151        // eleven and nineteen; that matrix has determinant one, so its
152        // inverse is whole.
153        Self::new([
154            19 * staff_steps - 11 * semitones,
155            7 * semitones - 12 * staff_steps,
156        ])
157    }
158
159    /// Factors a [`FractionType`] into prime exponents.
160    pub fn from_fraction(ratio: FractionType) -> Result<Self> {
161        match (ratio.numer(), ratio.denom()) {
162            (Some(&numerator), Some(&denominator)) => Self::from_ratio(numerator, denominator),
163            _ => Err(Error::TuningSystem(
164                "an infinite or undefined ratio has no monzo".to_owned(),
165            )),
166        }
167    }
168
169    /// The prime exponents, lowest prime first, without trailing zeros.
170    #[must_use]
171    pub fn exponents(&self) -> &[IntegerType] {
172        &self.exponents
173    }
174
175    /// The exponent of `prime`, which is nought for a prime not written.
176    ///
177    /// Errors only for a number that is not a prime this module carries.
178    pub fn exponent_of(&self, prime: IntegerType) -> Result<IntegerType> {
179        let index = prime_index(prime).ok_or_else(|| {
180            Error::TuningSystem(format!("{prime} is not a prime this module carries"))
181        })?;
182        Ok(self.at(index))
183    }
184
185    /// The largest prime the monzo actually uses, or `None` for the unison.
186    #[must_use]
187    pub fn limit(&self) -> Option<IntegerType> {
188        self.exponents
189            .iter()
190            .rposition(|&exponent| exponent != 0)
191            .map(|index| PRIMES[index])
192    }
193
194    /// Whether every exponent is nought.
195    #[must_use]
196    pub fn is_unison(&self) -> bool {
197        self.exponents.is_empty()
198    }
199
200    /// The ratio the exponents multiply out to.
201    ///
202    /// Errors when the ratio does not fit an [`IntegerType`], which a stack of
203    /// any size will reach — `[-4 4 -1⟩` is `81/80`, but twelve of them are not
204    /// a fraction of two `i32`s.
205    pub fn ratio(&self) -> Result<FractionType> {
206        let mut numerator: IntegerType = 1;
207        let mut denominator: IntegerType = 1;
208        for (index, &exponent) in self.exponents.iter().enumerate() {
209            let prime = PRIMES[index];
210            let magnitude = exponent.unsigned_abs();
211            let power = prime.checked_pow(magnitude).ok_or_else(|| {
212                Error::TuningSystem(format!("{prime}^{magnitude} does not fit the ratio type"))
213            })?;
214            let side = if exponent >= 0 {
215                &mut numerator
216            } else {
217                &mut denominator
218            };
219            *side = side
220                .checked_mul(power)
221                .ok_or_else(|| Error::TuningSystem("the ratio does not fit its type".to_owned()))?;
222        }
223        Ok(FractionType::new(numerator, denominator))
224    }
225
226    /// How wide the interval is, in cents.
227    #[must_use]
228    pub fn cents(&self) -> FloatType {
229        1200.0
230            * self
231                .exponents
232                .iter()
233                .enumerate()
234                .map(|(index, &exponent)| {
235                    FloatType::from(exponent) * FloatType::from(PRIMES[index]).log2()
236                })
237                .sum::<FloatType>()
238    }
239
240    /// The interval reached by sounding both, which adds the exponents.
241    pub fn multiply(&self, other: &Self) -> Self {
242        let width = self.exponents.len().max(other.exponents.len());
243        Self::new(
244            (0..width)
245                .map(|index| self.at(index) + other.at(index))
246                .collect::<Vec<_>>(),
247        )
248    }
249
250    /// The interval left by taking `other` off this one.
251    pub fn divide(&self, other: &Self) -> Self {
252        self.multiply(&other.inverse())
253    }
254
255    /// The same interval measured the other way.
256    pub fn inverse(&self) -> Self {
257        Self::new(
258            self.exponents
259                .iter()
260                .map(|exponent| -exponent)
261                .collect::<Vec<_>>(),
262        )
263    }
264
265    /// The interval stacked `count` times.
266    pub fn pow(&self, count: IntegerType) -> Self {
267        Self::new(
268            self.exponents
269                .iter()
270                .map(|exponent| exponent * count)
271                .collect::<Vec<_>>(),
272        )
273    }
274
275    /// The exponent at `index`, nought past the end.
276    fn at(&self, index: usize) -> IntegerType {
277        self.exponents.get(index).copied().unwrap_or(0)
278    }
279}
280
281impl Display for Monzo {
282    /// Writes the monzo in the wiki's own notation, `[-4 4 -1⟩`.
283    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
284        write!(f, "[")?;
285        for (position, exponent) in self.exponents.iter().enumerate() {
286            if position > 0 {
287                write!(f, " ")?;
288            }
289            write!(f, "{exponent}")?;
290        }
291        write!(f, "⟩")
292    }
293}
294
295impl FromStr for Monzo {
296    type Err = Error;
297
298    /// Reads `[-4 4 -1⟩`, and the ASCII spelling `|-4 4 -1>` beside it.
299    fn from_str(text: &str) -> Result<Self> {
300        let text = text.trim();
301        let body = text
302            .strip_prefix('[')
303            .or_else(|| text.strip_prefix('|'))
304            .and_then(|rest| rest.strip_suffix('⟩').or_else(|| rest.strip_suffix('>')))
305            .ok_or_else(|| {
306                Error::TuningSystem(format!("{text} is not a monzo; a monzo reads [-4 4 -1⟩"))
307            })?;
308        Ok(Self::new(parse_entries(body)?))
309    }
310}
311
312/// How many steps of a tuning each prime is worth.
313///
314/// `⟨12 19 28]` is twelve-tone equal temperament read to the 5-limit: twelve
315/// steps to the octave, nineteen to the perfect twelfth, twenty-eight to the
316/// major seventeenth. Applying it to a [`Monzo`] says how many steps that
317/// interval comes to, and a comma it sends to nothing is a comma the tuning
318/// tempers out.
319///
320/// ```
321/// use music21_rs::tuningsystem::{Monzo, Val};
322///
323/// let twelve = Val::patent(12, 5)?;
324/// assert_eq!(twelve.entries(), [12, 19, 28]);
325/// assert_eq!(twelve.map(&Monzo::from_ratio(3, 2)?), 7);
326/// assert!(twelve.tempers_out(&Monzo::from_ratio(81, 80)?));
327/// # Ok::<(), music21_rs::Error>(())
328/// ```
329#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
331#[must_use]
332pub struct Val {
333    entries: Vec<IntegerType>,
334}
335
336impl Val {
337    /// Builds a val from step counts, lowest prime first.
338    ///
339    /// Trailing zeros are kept, unlike a monzo's: a val saying the seventh
340    /// harmonic is worth no steps at all has said something about it, while a
341    /// val that stops before it has not.
342    pub fn new(entries: impl Into<Vec<IntegerType>>) -> Self {
343        Self {
344            entries: entries.into(),
345        }
346    }
347
348    /// The patent val of `divisions` equal steps, read up to `limit`.
349    ///
350    /// Each prime is mapped to the nearest whole number of steps, which is what
351    /// *patent* means: the obvious reading, before anyone chooses a warped one.
352    /// Errors for a limit that is not a prime this module carries.
353    pub fn patent(divisions: IntegerType, limit: IntegerType) -> Result<Self> {
354        let width = entries_for_limit(limit)?;
355        Ok(Self::new(
356            PRIMES[..width]
357                .iter()
358                .map(|&prime| {
359                    (FloatType::from(divisions) * FloatType::from(prime).log2()).round()
360                        as IntegerType
361                })
362                .collect::<Vec<_>>(),
363        ))
364    }
365
366    /// The step counts, lowest prime first.
367    #[must_use]
368    pub fn entries(&self) -> &[IntegerType] {
369        &self.entries
370    }
371
372    /// How many steps the val makes `interval` come to.
373    ///
374    /// A prime the val does not reach is read as nought steps, so ask
375    /// [`Val::reaches`] first where that would be a lie rather than an answer.
376    #[must_use]
377    pub fn map(&self, interval: &Monzo) -> IntegerType {
378        interval
379            .exponents()
380            .iter()
381            .enumerate()
382            .map(|(index, &exponent)| exponent * self.entries.get(index).copied().unwrap_or(0))
383            .sum()
384    }
385
386    /// Whether the val says anything about every prime `interval` uses.
387    #[must_use]
388    pub fn reaches(&self, interval: &Monzo) -> bool {
389        interval.exponents().len() <= self.entries.len()
390    }
391
392    /// Whether the tuning tempers `comma` out — maps it to no steps at all.
393    #[must_use]
394    pub fn tempers_out(&self, comma: &Monzo) -> bool {
395        self.reaches(comma) && self.map(comma) == 0
396    }
397
398    /// How many steps the val divides the octave into.
399    ///
400    /// That is its first entry, since the first prime is two.
401    #[must_use]
402    pub fn divisions(&self) -> IntegerType {
403        self.entries.first().copied().unwrap_or(0)
404    }
405
406    /// The largest prime the val reaches, or `None` for an empty one.
407    #[must_use]
408    pub fn limit(&self) -> Option<IntegerType> {
409        self.entries
410            .len()
411            .checked_sub(1)
412            .and_then(|index| PRIMES.get(index).copied())
413    }
414
415    /// How far off `interval` sounds in this tuning, in cents.
416    ///
417    /// Positive is sharp of just. The step size comes from the val's own
418    /// octave, so a val mapping the octave to nothing has no answer and this is
419    /// an error.
420    pub fn error_cents(&self, interval: &Monzo) -> Result<FloatType> {
421        let divisions = self.divisions();
422        if divisions == 0 {
423            return Err(Error::TuningSystem(
424                "a val mapping the octave to no steps has no step size".to_owned(),
425            ));
426        }
427        let step = 1200.0 / FloatType::from(divisions);
428        Ok(FloatType::from(self.map(interval)) * step - interval.cents())
429    }
430}
431
432impl Display for Val {
433    /// Writes the val in the wiki's own notation, `⟨12 19 28]`.
434    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
435        write!(f, "⟨")?;
436        for (position, entry) in self.entries.iter().enumerate() {
437            if position > 0 {
438                write!(f, " ")?;
439            }
440            write!(f, "{entry}")?;
441        }
442        write!(f, "]")
443    }
444}
445
446impl FromStr for Val {
447    type Err = Error;
448
449    /// Reads `⟨12 19 28]`, and the ASCII spelling `<12 19 28]` beside it.
450    fn from_str(text: &str) -> Result<Self> {
451        let text = text.trim();
452        let body = text
453            .strip_prefix('⟨')
454            .or_else(|| text.strip_prefix('<'))
455            .and_then(|rest| rest.strip_suffix(']'))
456            .ok_or_else(|| {
457                Error::TuningSystem(format!("{text} is not a val; a val reads ⟨12 19 28]"))
458            })?;
459        Ok(Self::new(parse_entries(body)?))
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn a_monzo_is_read_from_a_fraction_and_back() {
469        use super::Monzo;
470        use crate::FractionType;
471
472        let comma = Monzo::from_fraction(FractionType::new(81, 80)).unwrap();
473        assert_eq!(comma.limit(), Some(5));
474        assert!(!comma.is_unison());
475        assert_eq!(comma.ratio().unwrap(), FractionType::new(81, 80));
476        let unison = Monzo::from_fraction(FractionType::new(1, 1)).unwrap();
477        assert!(unison.is_unison());
478        assert_eq!(unison.limit(), None);
479        assert!(Monzo::from_fraction(FractionType::new(1, 0)).is_err());
480    }
481
482    /// Every ratio the wiki writes a monzo for, factored and written back.
483    #[test]
484    fn monzos_factor_the_commas_they_are_named_for() {
485        for (numerator, denominator, exponents) in [
486            (81, 80, vec![-4, 4, -1]),         // syntonic comma
487            (531_441, 524_288, vec![-19, 12]), // Pythagorean comma
488            (128, 125, vec![7, 0, -3]),        // diesis
489            (2048, 2025, vec![11, -4, -2]),    // diaschisma
490            (64, 63, vec![6, -2, 0, -1]),      // septimal comma
491            (225, 224, vec![-5, 2, 2, -1]),    // marvel comma
492            (2, 1, vec![1]),                   // the octave itself
493            (3, 2, vec![-1, 1]),               // the fifth
494            (5, 4, vec![-2, 0, 1]),            // the just major third
495        ] {
496            let monzo = Monzo::from_ratio(numerator, denominator).expect("a factorable ratio");
497            assert_eq!(monzo.exponents(), exponents, "{numerator}/{denominator}");
498            assert_eq!(
499                monzo.ratio().expect("a ratio that fits"),
500                FractionType::new(numerator, denominator),
501                "{numerator}/{denominator} did not come back"
502            );
503        }
504    }
505
506    #[test]
507    fn a_monzos_cents_is_the_width_of_its_ratio() {
508        let comma = Monzo::from_ratio(81, 80).expect("the syntonic comma");
509        assert!((comma.cents() - 21.506_29).abs() < 1e-5);
510        let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
511        assert!((fifth.cents() - 701.955_00).abs() < 1e-5);
512        assert_eq!(Monzo::unison().cents(), 0.0);
513    }
514
515    #[test]
516    fn stacking_intervals_adds_their_monzos() {
517        let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
518        let octave = Monzo::from_ratio(2, 1).expect("the octave");
519        // Four fifths less two octaves and a just third is the syntonic comma.
520        let third = Monzo::from_ratio(5, 4).expect("the third");
521        let comma = fifth.pow(4).divide(&octave.pow(2)).divide(&third);
522        assert_eq!(comma, Monzo::from_ratio(81, 80).expect("the comma"));
523        // Twelve fifths less seven octaves is the Pythagorean comma.
524        let pythagorean = fifth.pow(12).divide(&octave.pow(7));
525        assert_eq!(
526            pythagorean,
527            Monzo::from_ratio(531_441, 524_288).expect("the comma")
528        );
529    }
530
531    #[test]
532    fn a_monzo_writes_and_reads_the_wikis_notation() {
533        let comma = Monzo::from_ratio(81, 80).expect("the syntonic comma");
534        assert_eq!(comma.to_string(), "[-4 4 -1⟩");
535        assert_eq!("[-4 4 -1⟩".parse::<Monzo>().expect("a monzo"), comma);
536        assert_eq!("|-4 4 -1>".parse::<Monzo>().expect("a monzo"), comma);
537        assert_eq!("[-4, 4, -1⟩".parse::<Monzo>().expect("a monzo"), comma);
538        assert_eq!(Monzo::unison().to_string(), "[⟩");
539        assert!("-4 4 -1".parse::<Monzo>().is_err());
540        assert!("[-4 x -1⟩".parse::<Monzo>().is_err());
541    }
542
543    #[test]
544    fn a_ratio_past_the_table_or_below_zero_has_no_monzo() {
545        assert!(Monzo::from_ratio(101, 100).is_err());
546        assert!(Monzo::from_ratio(-3, 2).is_err());
547        assert!(Monzo::from_ratio(1, 0).is_err());
548        assert!(Monzo::from_ratio(97, 89).is_ok());
549    }
550
551    /// The patent vals the wiki lists for the equal temperaments it uses.
552    #[test]
553    fn patent_vals_match_the_ones_the_wiki_lists() {
554        for (divisions, limit, entries) in [
555            (5, 5, vec![5, 8, 12]),
556            (7, 5, vec![7, 11, 16]),
557            (12, 5, vec![12, 19, 28]),
558            (12, 7, vec![12, 19, 28, 34]),
559            (19, 5, vec![19, 30, 44]),
560            (22, 5, vec![22, 35, 51]),
561            (31, 11, vec![31, 49, 72, 87, 107]),
562            (41, 5, vec![41, 65, 95]),
563            (53, 5, vec![53, 84, 123]),
564            (72, 11, vec![72, 114, 167, 202, 249]),
565        ] {
566            let val = Val::patent(divisions, limit).expect("a prime limit");
567            assert_eq!(
568                val.entries(),
569                entries,
570                "{divisions}edo to the {limit}-limit"
571            );
572            assert_eq!(val.divisions(), divisions);
573            assert_eq!(val.limit(), Some(limit));
574        }
575    }
576
577    #[test]
578    fn a_temperament_is_the_commas_its_val_sends_to_nothing() {
579        let syntonic = Monzo::from_ratio(81, 80).expect("the syntonic comma");
580        let pythagorean = Monzo::from_ratio(531_441, 524_288).expect("the Pythagorean comma");
581
582        // Meantone tempers the syntonic comma out; 12, 19 and 31 are meantone.
583        for divisions in [12, 19, 31] {
584            let val = Val::patent(divisions, 5).expect("the 5-limit");
585            assert!(val.tempers_out(&syntonic), "{divisions}edo is meantone");
586        }
587        // 22edo deliberately is not, which is the whole point of it.
588        let twenty_two = Val::patent(22, 5).expect("the 5-limit");
589        assert!(!twenty_two.tempers_out(&syntonic));
590
591        // Only 12 of those tempers the Pythagorean comma out.
592        let twelve = Val::patent(12, 5).expect("the 5-limit");
593        assert!(twelve.tempers_out(&pythagorean));
594        assert!(
595            !Val::patent(19, 5)
596                .expect("the 5-limit")
597                .tempers_out(&pythagorean)
598        );
599    }
600
601    #[test]
602    fn a_val_says_nothing_about_a_prime_it_does_not_reach() {
603        let five_limit = Val::patent(12, 5).expect("the 5-limit");
604        let septimal = Monzo::from_ratio(7, 4).expect("the harmonic seventh");
605        assert!(!five_limit.reaches(&septimal));
606        assert!(!five_limit.tempers_out(&Monzo::from_ratio(64, 63).expect("the comma")));
607        assert!(Val::patent(12, 7).expect("the 7-limit").reaches(&septimal));
608    }
609
610    #[test]
611    fn error_cents_says_how_far_off_a_tuning_sounds() {
612        let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
613        let twelve = Val::patent(12, 5).expect("the 5-limit");
614        // 12edo's fifth is a bit under two cents flat of just.
615        let error = twelve.error_cents(&fifth).expect("a step size");
616        assert!((error + 1.955).abs() < 1e-3, "{error}");
617        // Its major third is fourteen cents sharp, which is the famous one.
618        let third = Monzo::from_ratio(5, 4).expect("the third");
619        let error = twelve.error_cents(&third).expect("a step size");
620        assert!((error - 13.686).abs() < 1e-3, "{error}");
621        assert!(Val::new([0, 0]).error_cents(&fifth).is_err());
622    }
623
624    #[test]
625    fn a_val_writes_and_reads_the_wikis_notation() {
626        let twelve = Val::patent(12, 5).expect("the 5-limit");
627        assert_eq!(twelve.to_string(), "⟨12 19 28]");
628        assert_eq!("⟨12 19 28]".parse::<Val>().expect("a val"), twelve);
629        assert_eq!("<12 19 28]".parse::<Val>().expect("a val"), twelve);
630        assert!("12 19 28".parse::<Val>().is_err());
631    }
632
633    #[test]
634    fn a_limit_that_is_not_a_prime_is_refused() {
635        assert!(Val::patent(12, 9).is_err());
636        assert!(Val::patent(12, 101).is_err());
637        assert!(
638            Monzo::from_ratio(9, 8)
639                .expect("a ratio")
640                .exponent_of(9)
641                .is_err()
642        );
643    }
644}