1use crate::defaults::{FloatType, FractionType, IntegerType};
15use crate::error::{Error, Result};
16
17use std::fmt::{Display, Formatter};
18use std::str::FromStr;
19
20pub 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
29fn prime_index(prime: IntegerType) -> Option<usize> {
31 PRIMES.iter().position(|&candidate| candidate == prime)
32}
33
34fn 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
44fn trimmed(mut entries: Vec<IntegerType>) -> Vec<IntegerType> {
46 while entries.last() == Some(&0) {
47 let _ = entries.pop();
48 }
49 entries
50}
51
52fn 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
64fn 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
74fn 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#[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 pub fn new(exponents: impl Into<Vec<IntegerType>>) -> Self {
109 Self {
110 exponents: trimmed(exponents.into()),
111 }
112 }
113
114 pub fn unison() -> Self {
116 Self::default()
117 }
118
119 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 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 #[must_use]
154 pub fn exponents(&self) -> &[IntegerType] {
155 &self.exponents
156 }
157
158 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 #[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 #[must_use]
179 pub fn is_unison(&self) -> bool {
180 self.exponents.is_empty()
181 }
182
183 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 #[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 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 pub fn divide(&self, other: &Self) -> Self {
235 self.multiply(&other.inverse())
236 }
237
238 pub fn inverse(&self) -> Self {
240 Self::new(
241 self.exponents
242 .iter()
243 .map(|exponent| -exponent)
244 .collect::<Vec<_>>(),
245 )
246 }
247
248 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 fn at(&self, index: usize) -> IntegerType {
260 self.exponents.get(index).copied().unwrap_or(0)
261 }
262}
263
264impl Display for Monzo {
265 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 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#[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 pub fn new(entries: impl Into<Vec<IntegerType>>) -> Self {
326 Self {
327 entries: entries.into(),
328 }
329 }
330
331 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 #[must_use]
351 pub fn entries(&self) -> &[IntegerType] {
352 &self.entries
353 }
354
355 #[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 #[must_use]
371 pub fn reaches(&self, interval: &Monzo) -> bool {
372 interval.exponents().len() <= self.entries.len()
373 }
374
375 #[must_use]
377 pub fn tempers_out(&self, comma: &Monzo) -> bool {
378 self.reaches(comma) && self.map(comma) == 0
379 }
380
381 #[must_use]
385 pub fn divisions(&self) -> IntegerType {
386 self.entries.first().copied().unwrap_or(0)
387 }
388
389 #[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 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 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 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 #[test]
467 fn monzos_factor_the_commas_they_are_named_for() {
468 for (numerator, denominator, exponents) in [
469 (81, 80, vec![-4, 4, -1]), (531_441, 524_288, vec![-19, 12]), (128, 125, vec![7, 0, -3]), (2048, 2025, vec![11, -4, -2]), (64, 63, vec![6, -2, 0, -1]), (225, 224, vec![-5, 2, 2, -1]), (2, 1, vec![1]), (3, 2, vec![-1, 1]), (5, 4, vec![-2, 0, 1]), ] {
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 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 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 #[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 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 let twenty_two = Val::patent(22, 5).expect("the 5-limit");
572 assert!(!twenty_two.tempers_out(&syntonic));
573
574 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 let error = twelve.error_cents(&fifth).expect("a step size");
599 assert!((error + 1.955).abs() < 1e-3, "{error}");
600 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}