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