1use std::fmt;
16use std::ops::Index;
17use std::str::FromStr;
18
19use crate::{
20 chord::root,
21 defaults::{IntegerType, UnsignedIntegerType},
22 error::{Error, Result},
23 pitch::{Pitch, pitchclass::convert_pitch_class_to_str},
24};
25
26mod tables;
27
28pub use tables::HISTORICAL_ROWS;
29use tables::LINK_CHORDS;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub enum Transformation {
35 Prime,
38 Inversion,
40 Retrograde,
42 RetrogradeInversion,
44}
45
46impl Transformation {
47 pub const ALL: [Transformation; 4] = [
49 Transformation::Prime,
50 Transformation::Inversion,
51 Transformation::Retrograde,
52 Transformation::RetrogradeInversion,
53 ];
54
55 pub fn label(self, convention: TransformationConvention) -> &'static str {
57 match (self, convention) {
58 (Transformation::Prime, TransformationConvention::ZeroCentered) => "P",
59 (Transformation::Prime, TransformationConvention::OriginalCentered) => "T",
60 (Transformation::Inversion, _) => "I",
61 (Transformation::Retrograde, _) => "R",
62 (Transformation::RetrogradeInversion, _) => "RI",
63 }
64 }
65
66 pub fn from_name(name: &str) -> Result<Self> {
68 match name {
69 "P" | "T" => Ok(Transformation::Prime),
70 "I" => Ok(Transformation::Inversion),
71 "R" => Ok(Transformation::Retrograde),
72 "RI" => Ok(Transformation::RetrogradeInversion),
73 other => Err(Error::Serial(format!(
74 "Invalid transformation type: {other}"
75 ))),
76 }
77 }
78}
79
80impl fmt::Display for Transformation {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str(self.label(TransformationConvention::ZeroCentered))
83 }
84}
85
86impl FromStr for Transformation {
87 type Err = Error;
88
89 fn from_str(name: &str) -> Result<Self> {
90 Transformation::from_name(name)
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97pub enum TransformationConvention {
98 ZeroCentered,
101 OriginalCentered,
105}
106
107impl TransformationConvention {
108 pub fn from_name(name: &str) -> Result<Self> {
110 match name {
111 "zero" => Ok(TransformationConvention::ZeroCentered),
112 "original" => Ok(TransformationConvention::OriginalCentered),
113 _ => Err(Error::Serial(
114 "Invalid convention - choose 'zero' or 'original'.".to_string(),
115 )),
116 }
117 }
118}
119
120pub type IndexedTransformation = (Transformation, u8);
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126#[must_use]
127pub struct ToneRow {
128 pitch_classes: Vec<u8>,
129}
130
131fn wrap(value: IntegerType) -> u8 {
132 value.rem_euclid(12) as u8
133}
134
135impl ToneRow {
136 pub fn new(pitch_classes: impl IntoIterator<Item = IntegerType>) -> Self {
139 Self {
140 pitch_classes: pitch_classes.into_iter().map(wrap).collect(),
141 }
142 }
143
144 pub fn from_pitches<'a>(pitches: impl IntoIterator<Item = &'a Pitch>) -> Self {
146 Self {
147 pitch_classes: pitches.into_iter().map(root::pitch_class).collect(),
148 }
149 }
150
151 pub fn pitch_classes(&self) -> &[u8] {
153 &self.pitch_classes
154 }
155
156 pub fn len(&self) -> usize {
158 self.pitch_classes.len()
159 }
160
161 pub fn is_empty(&self) -> bool {
163 self.pitch_classes.is_empty()
164 }
165
166 pub fn pitches(&self) -> Vec<Pitch> {
169 self.pitch_classes
170 .iter()
171 .map(|&pc| {
172 Pitch::from_pitch_class(IntegerType::from(pc))
173 .expect("a pitch class below twelve always spells")
174 })
175 .collect()
176 }
177
178 pub fn note_names(&self) -> Vec<String> {
180 self.pitches().iter().map(Pitch::name).collect()
181 }
182
183 pub fn is_twelve_tone_row(&self) -> bool {
185 self.pitch_classes.len() == 12 && (0..12u8).all(|pc| self.pitch_classes.contains(&pc))
186 }
187
188 pub fn make_twelve_tone_row(&self) -> Result<ToneRow> {
193 if !self.is_twelve_tone_row() {
194 return Err(Error::Serial(
195 "A twelve-tone row must contain each pitch class exactly once".to_string(),
196 ));
197 }
198 Ok(self.clone())
199 }
200
201 pub fn is_same_row(&self, other: &ToneRow) -> bool {
203 self == other
204 }
205
206 pub fn intervals_as_string(&self) -> String {
209 self.pitch_classes
210 .windows(2)
211 .map(|pair| {
212 let interval = wrap(IntegerType::from(pair[1]) - IntegerType::from(pair[0]));
213 match interval {
214 10 => 'T',
215 11 => 'E',
216 digit => char::from(b'0' + digit),
217 }
218 })
219 .collect()
220 }
221
222 pub fn zero_centered_transformation(
225 &self,
226 transformation: Transformation,
227 index: IntegerType,
228 ) -> ToneRow {
229 let Some(&first) = self.pitch_classes.first() else {
230 return ToneRow::default();
231 };
232 let first = IntegerType::from(first);
233 let forward = self.pitch_classes.iter().map(|&pc| IntegerType::from(pc));
234 let backward = self
235 .pitch_classes
236 .iter()
237 .rev()
238 .map(|&pc| IntegerType::from(pc));
239 let transformed: Vec<IntegerType> = match transformation {
240 Transformation::Prime => forward.map(|pc| pc - first + index).collect(),
241 Transformation::Inversion => forward.map(|pc| index + first - pc).collect(),
242 Transformation::Retrograde => backward.map(|pc| index + pc - first).collect(),
243 Transformation::RetrogradeInversion => backward.map(|pc| index - pc + first).collect(),
244 };
245 ToneRow::new(transformed)
246 }
247
248 pub fn original_centered_transformation(
252 &self,
253 transformation: Transformation,
254 index: IntegerType,
255 ) -> ToneRow {
256 let Some(&first) = self.pitch_classes.first() else {
257 return ToneRow::default();
258 };
259 self.zero_centered_transformation(transformation, IntegerType::from(first) + index)
260 }
261
262 pub fn transformation(
264 &self,
265 convention: TransformationConvention,
266 transformation: Transformation,
267 index: IntegerType,
268 ) -> ToneRow {
269 match convention {
270 TransformationConvention::ZeroCentered => {
271 self.zero_centered_transformation(transformation, index)
272 }
273 TransformationConvention::OriginalCentered => {
274 self.original_centered_transformation(transformation, index)
275 }
276 }
277 }
278
279 pub fn find_zero_centered_transformations(
282 &self,
283 other: &ToneRow,
284 ) -> Vec<IndexedTransformation> {
285 let (Some(&first), Some(&last)) = (other.pitch_classes.first(), other.pitch_classes.last())
286 else {
287 return Vec::new();
288 };
289 if self.len() != other.len() {
290 return Vec::new();
291 }
292 let candidates = [
293 (Transformation::Prime, first),
294 (Transformation::Inversion, first),
295 (Transformation::Retrograde, last),
296 (Transformation::RetrogradeInversion, last),
297 ];
298 candidates
299 .into_iter()
300 .filter(|&(transformation, index)| {
301 self.zero_centered_transformation(transformation, IntegerType::from(index))
302 == *other
303 })
304 .collect()
305 }
306
307 pub fn find_original_centered_transformations(
310 &self,
311 other: &ToneRow,
312 ) -> Vec<IndexedTransformation> {
313 let (Some(&old_first), Some(&old_last), Some(&new_first)) = (
314 self.pitch_classes.first(),
315 self.pitch_classes.last(),
316 other.pitch_classes.first(),
317 ) else {
318 return Vec::new();
319 };
320 if self.len() != other.len() {
321 return Vec::new();
322 }
323 let (old_first, old_last, new_first) = (
324 IntegerType::from(old_first),
325 IntegerType::from(old_last),
326 IntegerType::from(new_first),
327 );
328 let transposition = wrap(new_first - old_first);
329 let retrograde = wrap(new_first - old_last);
330 let retrograde_inversion = wrap(new_first - 2 * old_first + old_last);
331 let candidates = [
332 (Transformation::Prime, transposition),
333 (Transformation::Inversion, transposition),
334 (Transformation::Retrograde, retrograde),
335 (Transformation::RetrogradeInversion, retrograde_inversion),
336 ];
337 candidates
338 .into_iter()
339 .filter(|&(transformation, index)| {
340 self.original_centered_transformation(transformation, IntegerType::from(index))
341 == *other
342 })
343 .collect()
344 }
345
346 pub fn find_transformations(
349 &self,
350 convention: TransformationConvention,
351 other: &ToneRow,
352 ) -> Vec<IndexedTransformation> {
353 match convention {
354 TransformationConvention::ZeroCentered => {
355 self.find_zero_centered_transformations(other)
356 }
357 TransformationConvention::OriginalCentered => {
358 self.find_original_centered_transformations(other)
359 }
360 }
361 }
362
363 pub fn matrix(&self) -> TwelveToneMatrix {
366 let rows = self
367 .pitch_classes
368 .iter()
369 .map(|&pc| {
370 let transposition = wrap(12 - IntegerType::from(pc));
371 ToneRow::new(
372 self.pitch_classes
373 .iter()
374 .map(|&x| IntegerType::from(x) + IntegerType::from(transposition)),
375 )
376 })
377 .collect();
378 TwelveToneMatrix { rows }
379 }
380
381 pub fn find_historical(&self) -> Vec<&'static HistoricalRow> {
383 HISTORICAL_ROWS
384 .iter()
385 .filter(|historical| historical.pitch_classes == self.pitch_classes.as_slice())
386 .collect()
387 }
388
389 pub fn find_transformed_historical(
392 &self,
393 convention: TransformationConvention,
394 ) -> Vec<(&'static HistoricalRow, Vec<IndexedTransformation>)> {
395 HISTORICAL_ROWS
396 .iter()
397 .filter_map(|historical| {
398 let transformations = historical.row().find_transformations(convention, self);
399 (!transformations.is_empty()).then_some((historical, transformations))
400 })
401 .collect()
402 }
403
404 fn require_twelve_tone(&self, purpose: &str) -> Result<()> {
405 if self.is_twelve_tone_row() {
406 Ok(())
407 } else {
408 Err(Error::Serial(format!(
409 "{purpose} must be a twelve-tone row."
410 )))
411 }
412 }
413
414 pub fn is_all_interval(&self) -> Result<bool> {
419 self.require_twelve_tone("An all-interval row")?;
420 let intervals = self.intervals_as_string();
421 Ok("123456789TE"
422 .chars()
423 .all(|interval| intervals.contains(interval)))
424 }
425
426 pub fn link_classification(&self) -> Result<Option<LinkClassification>> {
432 self.require_twelve_tone("A Link Chord")?;
433 let forms = [
434 self.clone(),
435 self.zero_centered_transformation(Transformation::Inversion, 0),
436 self.zero_centered_transformation(Transformation::Retrograde, 0),
437 self.zero_centered_transformation(Transformation::RetrogradeInversion, 0),
438 ];
439 let mut number = None;
440 let mut special_intervals = Vec::new();
441 for form in &forms {
442 let intervals = form.intervals_as_string();
443 for link in LINK_CHORDS
444 .iter()
445 .filter(|link| link.intervals == intervals)
446 {
447 number = Some(link.classification);
448 special_intervals.push(link.special_intervals);
449 }
450 }
451 Ok(number.map(|number| LinkClassification {
452 number,
453 special_intervals,
454 }))
455 }
456
457 pub fn is_link_chord(&self) -> Result<bool> {
459 Ok(self.link_classification()?.is_some())
460 }
461
462 pub fn are_combinatorial(
468 &self,
469 first: Transformation,
470 first_index: IntegerType,
471 second: Transformation,
472 second_index: IntegerType,
473 ) -> Result<bool> {
474 self.require_twelve_tone("Combinatoriality applies only to twelve-tone rows; this")?;
475 let first = self.zero_centered_transformation(first, first_index);
476 let second = self.zero_centered_transformation(second, second_index);
477 let combined = first.pitch_classes[..6]
478 .iter()
479 .chain(&second.pitch_classes[..6])
480 .map(|&pc| IntegerType::from(pc));
481 Ok(ToneRow::new(combined).is_twelve_tone_row())
482 }
483}
484
485impl Index<usize> for ToneRow {
486 type Output = u8;
487
488 fn index(&self, index: usize) -> &Self::Output {
489 &self.pitch_classes[index]
490 }
491}
492
493impl<'a> IntoIterator for &'a ToneRow {
494 type Item = &'a u8;
495 type IntoIter = std::slice::Iter<'a, u8>;
496
497 fn into_iter(self) -> Self::IntoIter {
498 self.pitch_classes.iter()
499 }
500}
501
502impl fmt::Display for ToneRow {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505 for &pc in &self.pitch_classes {
506 f.write_str(&convert_pitch_class_to_str(IntegerType::from(pc)))?;
507 }
508 Ok(())
509 }
510}
511
512impl From<Vec<u8>> for ToneRow {
513 fn from(pitch_classes: Vec<u8>) -> Self {
514 ToneRow::new(pitch_classes.into_iter().map(IntegerType::from))
515 }
516}
517
518impl<const N: usize> From<[u8; N]> for ToneRow {
519 fn from(pitch_classes: [u8; N]) -> Self {
520 ToneRow::new(pitch_classes.into_iter().map(IntegerType::from))
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct LinkClassification {
528 pub number: UnsignedIntegerType,
530 pub special_intervals: Vec<&'static str>,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq)]
537#[must_use]
538pub struct TwelveToneMatrix {
539 rows: Vec<ToneRow>,
540}
541
542impl TwelveToneMatrix {
543 pub fn rows(&self) -> &[ToneRow] {
545 &self.rows
546 }
547
548 pub fn row(&self, index: usize) -> Option<&ToneRow> {
550 self.rows.get(index)
551 }
552}
553
554impl fmt::Display for TwelveToneMatrix {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558 for (index, row) in self.rows.iter().enumerate() {
559 if index > 0 {
560 writeln!(f)?;
561 }
562 for &pc in row.pitch_classes() {
563 write!(
564 f,
565 "{:>3}",
566 convert_pitch_class_to_str(IntegerType::from(pc))
567 )?;
568 }
569 }
570 Ok(())
571 }
572}
573
574pub fn row_to_matrix(pitch_classes: &[IntegerType]) -> String {
577 ToneRow::new(pitch_classes.iter().copied())
578 .matrix()
579 .rows()
580 .iter()
581 .map(|row| {
582 row.pitch_classes()
583 .iter()
584 .map(|pc| format!("{pc:>3}"))
585 .collect::<String>()
586 })
587 .collect::<Vec<_>>()
588 .join("\n")
589}
590
591#[derive(Debug, Clone, PartialEq, Eq)]
594#[must_use]
595pub struct HistoricalRow {
596 pub name: &'static str,
598 pub composer: &'static str,
600 pub opus: Option<&'static str>,
602 pub title: &'static str,
604 pub pitch_classes: [u8; 12],
606}
607
608impl HistoricalRow {
609 pub fn row(&self) -> ToneRow {
611 ToneRow::from(self.pitch_classes)
612 }
613}
614
615pub fn historical_row_by_name(name: &str) -> Result<&'static HistoricalRow> {
618 let name = name.strip_prefix("Row").unwrap_or(name);
619 HISTORICAL_ROWS
620 .iter()
621 .find(|row| row.name == name)
622 .ok_or_else(|| Error::Serial("No historical row with given name found".to_string()))
623}
624
625struct LinkChord {
626 intervals: &'static str,
627 special_intervals: &'static str,
628 classification: UnsignedIntegerType,
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn only_a_permutation_of_the_twelve_classes_makes_a_twelve_tone_row() {
637 assert!(ToneRow::new(0..11).make_twelve_tone_row().is_err());
638 let row = ToneRow::new(0..12).make_twelve_tone_row().unwrap();
639 assert_eq!(row.pitch_classes().len(), 12);
640 }
641
642 #[test]
643 fn a_row_indexes_and_iterates_over_its_pitch_classes() {
644 let row = ToneRow::new([0, 13, -1]);
645
646 assert_eq!(row[1], 1);
647 let collected: Vec<u8> = (&row).into_iter().copied().collect();
648 assert_eq!(collected, vec![0, 1, 11]);
649 }
650
651 fn row(pitch_classes: impl IntoIterator<Item = IntegerType>) -> ToneRow {
652 ToneRow::new(pitch_classes)
653 }
654
655 fn chromatic() -> ToneRow {
656 row(0..12)
657 }
658
659 fn historical(name: &str) -> ToneRow {
660 historical_row_by_name(name).unwrap().row()
661 }
662
663 fn labelled(
664 transformations: &[IndexedTransformation],
665 convention: TransformationConvention,
666 ) -> Vec<(&'static str, u8)> {
667 transformations
668 .iter()
669 .map(|&(transformation, index)| (transformation.label(convention), index))
670 .collect()
671 }
672
673 #[test]
674 fn pitch_classes_are_reduced_modulo_twelve() {
675 let quintuple = row((0..12).map(|i| 5 * i));
676 assert_eq!(
677 quintuple.pitch_classes(),
678 &[0, 5, 10, 3, 8, 1, 6, 11, 4, 9, 2, 7]
679 );
680 assert_eq!(quintuple.to_string(), "05A3816B4927");
681 assert_eq!(row([-6, 19, 128]).pitch_classes(), &[6, 7, 8]);
682 assert_eq!(row([-6, 19, 128]).note_names(), ["F#", "G", "G#"]);
683 }
684
685 #[test]
686 fn note_names_spell_pitch_classes_as_music21_does() {
687 assert_eq!(
688 chromatic().note_names(),
689 [
690 "C", "C#", "D", "E-", "E", "F", "F#", "G", "G#", "A", "B-", "B"
691 ]
692 );
693 }
694
695 #[test]
696 fn twelve_tone_row_needs_all_twelve_pitch_classes_once() {
697 assert!(chromatic().is_twelve_tone_row());
698 assert!(!row([0, 4, 8]).is_twelve_tone_row());
699 assert!(!row([3; 12]).is_twelve_tone_row());
700 assert!(!ToneRow::default().is_twelve_tone_row());
701 }
702
703 #[test]
704 fn same_row_compares_pitch_classes_in_order() {
705 assert!(row([6, 7, 8]).is_same_row(&row([-6, 19, 128])));
706 assert!(!row([6, 7, 8]).is_same_row(&row([6, 7, -8])));
707 assert!(!row([6, 7, 8]).is_same_row(&row([6, 7])));
708 }
709
710 #[test]
711 fn intervals_as_string_uses_t_and_e() {
712 assert_eq!(row([0]).intervals_as_string(), "");
713 assert_eq!(ToneRow::default().intervals_as_string(), "");
714 assert_eq!(row((0..12).rev()).intervals_as_string(), "EEEEEEEEEEE");
715 assert_eq!(
716 historical("BergLyricSuite").intervals_as_string(),
717 "E89T7652341"
718 );
719 }
720
721 #[test]
722 fn zero_centered_transformations_match_music21() {
723 assert_eq!(
724 chromatic()
725 .zero_centered_transformation(Transformation::Prime, 3)
726 .pitch_classes(),
727 &[3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2]
728 );
729 assert_eq!(
730 chromatic()
731 .zero_centered_transformation(Transformation::Inversion, 6)
732 .pitch_classes(),
733 &[6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7]
734 );
735 let schoenberg = historical("SchoenbergOp26");
736 assert_eq!(
737 schoenberg.pitch_classes(),
738 &[3, 7, 9, 11, 1, 0, 10, 2, 4, 6, 8, 5]
739 );
740 assert_eq!(
741 schoenberg
742 .zero_centered_transformation(Transformation::Retrograde, 8)
743 .pitch_classes(),
744 &[10, 1, 11, 9, 7, 3, 5, 6, 4, 2, 0, 8]
745 );
746 assert_eq!(
747 schoenberg
748 .zero_centered_transformation(Transformation::RetrogradeInversion, 9)
749 .note_names(),
750 [
751 "G", "E", "F#", "G#", "B-", "D", "C", "B", "C#", "E-", "F", "A"
752 ]
753 );
754 }
755
756 #[test]
757 fn original_centered_transformations_match_music21() {
758 assert_eq!(
759 chromatic()
760 .original_centered_transformation(Transformation::Prime, 3)
761 .pitch_classes(),
762 &[3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2]
763 );
764 assert_eq!(
765 chromatic()
766 .original_centered_transformation(Transformation::Inversion, 6)
767 .pitch_classes(),
768 &[6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7]
769 );
770 let schoenberg = historical("SchoenbergOp26");
771 assert_eq!(
772 schoenberg
773 .original_centered_transformation(Transformation::Retrograde, 8)
774 .pitch_classes(),
775 &[1, 4, 2, 0, 10, 6, 8, 9, 7, 5, 3, 11]
776 );
777 assert_eq!(
778 schoenberg
779 .original_centered_transformation(Transformation::RetrogradeInversion, 9)
780 .note_names(),
781 [
782 "B-", "G", "A", "B", "C#", "F", "E-", "D", "E", "F#", "G#", "C"
783 ]
784 );
785 }
786
787 #[test]
788 fn transforming_an_empty_row_gives_an_empty_row() {
789 let empty = ToneRow::default();
790 assert!(
791 empty
792 .zero_centered_transformation(Transformation::Retrograde, 3)
793 .is_empty()
794 );
795 assert!(
796 empty
797 .original_centered_transformation(Transformation::Inversion, 3)
798 .is_empty()
799 );
800 assert!(empty.find_zero_centered_transformations(&empty).is_empty());
801 assert!(
802 empty
803 .find_original_centered_transformations(&empty)
804 .is_empty()
805 );
806 }
807
808 #[test]
809 fn finds_zero_centered_transformations_between_rows() {
810 let zero = TransformationConvention::ZeroCentered;
811 let rising = row([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1]);
812 let falling = row([8, 7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9]);
813 assert_eq!(
814 labelled(&rising.find_zero_centered_transformations(&falling), zero),
815 [("I", 8), ("R", 9)]
816 );
817 let op25 = historical("SchoenbergOp25");
818 let op26 = historical("SchoenbergOp26");
819 assert!(op25.find_zero_centered_transformations(&op26).is_empty());
820 let ri8 = op26.zero_centered_transformation(Transformation::RetrogradeInversion, 8);
821 assert_eq!(
822 labelled(&op26.find_zero_centered_transformations(&ri8), zero),
823 [("RI", 8)]
824 );
825 assert!(
826 op26.find_zero_centered_transformations(&row([0]))
827 .is_empty()
828 );
829 }
830
831 #[test]
832 fn finds_original_centered_transformations_between_rows() {
833 let original = TransformationConvention::OriginalCentered;
834 let rising = row([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1]);
835 let falling = row([8, 7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9]);
836 assert_eq!(
837 labelled(
838 &rising.find_original_centered_transformations(&falling),
839 original
840 ),
841 [("I", 6), ("R", 7)]
842 );
843 let op25 = historical("SchoenbergOp25");
844 let op26 = historical("SchoenbergOp26");
845 assert!(
846 op25.find_original_centered_transformations(&op26)
847 .is_empty()
848 );
849 let ri8 = op26.original_centered_transformation(Transformation::RetrogradeInversion, 8);
850 assert_eq!(
851 labelled(&op26.find_original_centered_transformations(&ri8), original),
852 [("RI", 8)]
853 );
854 assert_eq!(
855 labelled(&rising.find_transformations(original, &falling), original),
856 [("I", 6), ("R", 7)]
857 );
858 }
859
860 #[test]
861 fn every_transformation_is_found_again() {
862 let source = historical("WebernOp27");
863 for convention in [
864 TransformationConvention::ZeroCentered,
865 TransformationConvention::OriginalCentered,
866 ] {
867 for transformation in Transformation::ALL {
868 for index in 0..12 {
869 let target = source.transformation(convention, transformation, index);
870 let found = source.find_transformations(convention, &target);
871 assert!(
872 found.contains(&(transformation, index as u8)),
873 "{transformation:?} {index} under {convention:?} not found in {found:?}"
874 );
875 }
876 }
877 }
878 }
879
880 #[test]
881 fn matrix_matches_music21_layout() {
882 let matrix = row([0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]).matrix();
883 let expected = [
884 " 0 2 B 7 8 3 9 1 4 A 6 5",
885 " A 0 9 5 6 1 7 B 2 8 4 3",
886 " 1 3 0 8 9 4 A 2 5 B 7 6",
887 " 5 7 4 0 1 8 2 6 9 3 B A",
888 " 4 6 3 B 0 7 1 5 8 2 A 9",
889 " 9 B 8 4 5 0 6 A 1 7 3 2",
890 " 3 5 2 A B 6 0 4 7 1 9 8",
891 " B 1 A 6 7 2 8 0 3 9 5 4",
892 " 8 A 7 3 4 B 5 9 0 6 2 1",
893 " 2 4 1 9 A 5 B 3 6 0 8 7",
894 " 6 8 5 1 2 9 3 7 A 4 0 B",
895 " 7 9 6 2 3 A 4 8 B 5 1 0",
896 ]
897 .join(
898 "
899",
900 );
901 assert_eq!(matrix.to_string(), expected);
902 assert_eq!(matrix.rows().len(), 12);
903 assert_eq!(
904 matrix.row(0).unwrap().pitch_classes(),
905 &[0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]
906 );
907 assert!(matrix.row(12).is_none());
908
909 let op37 = historical("SchoenbergOp37").matrix();
910 assert_eq!(
911 op37.row(0).unwrap().note_names(),
912 [
913 "C", "B", "G", "G#", "E-", "C#", "D", "B-", "F#", "F", "E", "A"
914 ]
915 );
916 assert!(
917 op37.to_string()
918 .starts_with(" 0 B 7 8 3 1 2 A 6 5 4 9\n")
919 );
920 }
921
922 #[test]
923 fn row_to_matrix_writes_decimal_pitch_classes() {
924 let text = row_to_matrix(&[0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]);
925 let lines: Vec<&str> = text.lines().collect();
926 assert_eq!(lines.len(), 12);
927 assert_eq!(lines[0], " 0 2 11 7 8 3 9 1 4 10 6 5");
928 assert_eq!(lines[1], " 10 0 9 5 6 1 7 11 2 8 4 3");
929 assert_eq!(lines[11], " 7 9 6 2 3 10 4 8 11 5 1 0");
930 }
931
932 #[test]
933 fn historical_rows_are_found_by_name_with_or_without_the_old_prefix() {
934 let webern = historical_row_by_name("WebernOp29").unwrap();
935 assert_eq!(webern.composer, "Webern");
936 assert_eq!(webern.opus, Some("Op. 29"));
937 assert_eq!(webern.title, "Cantata I");
938 assert_eq!(webern.pitch_classes, [3, 11, 2, 1, 5, 4, 7, 6, 10, 9, 0, 8]);
939 assert_eq!(historical_row_by_name("RowWebernOp29").unwrap(), webern);
940 assert_eq!(
941 historical_row_by_name("SchoenbergJakobsleiter")
942 .unwrap()
943 .opus,
944 None
945 );
946 assert!(matches!(
947 historical_row_by_name("Nope"),
948 Err(Error::Serial(_))
949 ));
950 }
951
952 #[test]
953 fn every_historical_row_is_a_twelve_tone_row_with_a_unique_name() {
954 let mut names: Vec<&str> = HISTORICAL_ROWS.iter().map(|row| row.name).collect();
955 names.sort_unstable();
956 names.dedup();
957 assert_eq!(names.len(), HISTORICAL_ROWS.len());
958 for historical in &HISTORICAL_ROWS {
959 assert!(
960 historical.row().is_twelve_tone_row(),
961 "{} is not a twelve-tone row",
962 historical.name
963 );
964 }
965 }
966
967 #[test]
968 fn finds_historical_rows_and_their_transformations() {
969 let names = |rows: Vec<&HistoricalRow>| -> Vec<&str> {
970 rows.into_iter().map(|row| row.name).collect()
971 };
972 assert_eq!(
973 names(row([2, 3, 9, 1, 11, 5, 8, 7, 4, 0, 10, 6]).find_historical()),
974 ["SchoenbergOp32"]
975 );
976 assert!(chromatic().find_historical().is_empty());
977
978 let transformed = row([5, 9, 11, 3, 6, 7, 4, 10, 0, 8, 2, 1]);
979 let original =
980 transformed.find_transformed_historical(TransformationConvention::OriginalCentered);
981 assert_eq!(original.len(), 1);
982 assert_eq!(original[0].0.name, "SchoenbergOp32");
983 assert_eq!(
984 labelled(&original[0].1, TransformationConvention::OriginalCentered),
985 [("R", 11)]
986 );
987 let zero = transformed.find_transformed_historical(TransformationConvention::ZeroCentered);
988 assert_eq!(zero[0].0.name, "SchoenbergOp32");
989 assert_eq!(
990 labelled(&zero[0].1, TransformationConvention::ZeroCentered),
991 [("R", 1)]
992 );
993 }
994
995 #[test]
996 fn all_interval_rows() {
997 assert!(!chromatic().is_all_interval().unwrap());
998 let berg = historical("BergLyricSuite");
999 assert_eq!(
1000 berg.pitch_classes(),
1001 &[5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11]
1002 );
1003 assert!(berg.is_all_interval().unwrap());
1004 assert!(matches!(
1005 row([0, 4, 8]).is_all_interval(),
1006 Err(Error::Serial(_))
1007 ));
1008 }
1009
1010 #[test]
1011 fn link_chords_match_music21() {
1012 let berg = historical("BergLyricSuite");
1013 assert_eq!(berg.link_classification().unwrap(), None);
1014 assert!(!berg.is_link_chord().unwrap());
1015
1016 let link = row([0, 3, 8, 2, 10, 11, 9, 4, 1, 5, 7, 6]);
1017 assert_eq!(
1018 link.link_classification().unwrap(),
1019 Some(LinkClassification {
1020 number: 62,
1021 special_intervals: vec!["8352E"],
1022 })
1023 );
1024 assert!(link.is_link_chord().unwrap());
1025
1026 let double = row([0, 1, 8, 5, 7, 10, 4, 3, 11, 9, 2, 6]);
1027 assert_eq!(
1028 double.link_classification().unwrap(),
1029 Some(LinkClassification {
1030 number: 33,
1031 special_intervals: vec!["236E8", "36E8T"],
1032 })
1033 );
1034 assert!(matches!(
1035 row([0, 4, 8]).is_link_chord(),
1036 Err(Error::Serial(_))
1037 ));
1038 }
1039
1040 #[test]
1041 fn every_link_chord_interval_string_classifies_as_itself() {
1042 for link in &LINK_CHORDS {
1043 let mut pitch_classes = vec![0];
1044 for interval in link.intervals.chars() {
1045 let step = match interval {
1046 'T' => 10,
1047 'E' => 11,
1048 digit => IntegerType::from(digit.to_digit(10).unwrap() as u8),
1049 };
1050 pitch_classes.push(pitch_classes.last().unwrap() + step);
1051 }
1052 let classified = row(pitch_classes).link_classification().unwrap().unwrap();
1053 assert_eq!(classified.number, link.classification, "{}", link.intervals);
1054 assert!(
1055 classified
1056 .special_intervals
1057 .contains(&link.special_intervals)
1058 );
1059 }
1060 }
1061
1062 #[test]
1063 fn combinatoriality_matches_music21() {
1064 let moses = historical("SchoenbergMosesAron");
1065 assert_eq!(
1066 moses.pitch_classes(),
1067 &[9, 10, 4, 2, 3, 1, 7, 5, 6, 8, 11, 0]
1068 );
1069 let (p, i, r, ri) = (
1070 Transformation::Prime,
1071 Transformation::Inversion,
1072 Transformation::Retrograde,
1073 Transformation::RetrogradeInversion,
1074 );
1075 assert!(moses.are_combinatorial(p, 0, i, 3).unwrap());
1076 assert!(moses.are_combinatorial(p, 1, i, 4).unwrap());
1077 assert!(moses.are_combinatorial(r, 1, ri, 4).unwrap());
1078 assert!(!moses.are_combinatorial(r, 6, ri, 4).unwrap());
1079 assert!(matches!(
1080 row([0, 4, 8]).are_combinatorial(p, 0, i, 3),
1081 Err(Error::Serial(_))
1082 ));
1083 }
1084
1085 #[test]
1086 fn transformation_labels_round_trip() {
1087 for transformation in Transformation::ALL {
1088 for convention in [
1089 TransformationConvention::ZeroCentered,
1090 TransformationConvention::OriginalCentered,
1091 ] {
1092 let label = transformation.label(convention);
1093 assert_eq!(Transformation::from_name(label).unwrap(), transformation);
1094 assert_eq!(label.parse::<Transformation>().unwrap(), transformation);
1095 }
1096 }
1097 assert_eq!(Transformation::Prime.to_string(), "P");
1098 assert!(matches!(
1099 Transformation::from_name("X"),
1100 Err(Error::Serial(_))
1101 ));
1102 assert_eq!(
1103 TransformationConvention::from_name("zero").unwrap(),
1104 TransformationConvention::ZeroCentered
1105 );
1106 assert!(TransformationConvention::from_name("sideways").is_err());
1107 }
1108
1109 #[test]
1110 fn rows_build_from_pitches_and_arrays() {
1111 let pitches: Vec<Pitch> = ["C4", "F#3", "B-5"]
1112 .iter()
1113 .map(|&name| Pitch::from_name(name).unwrap())
1114 .collect();
1115 assert_eq!(ToneRow::from_pitches(&pitches).pitch_classes(), &[0, 6, 10]);
1116 assert_eq!(ToneRow::from([0u8, 6, 10]).pitch_classes(), &[0, 6, 10]);
1117 assert_eq!(ToneRow::from(vec![0u8, 6, 10]).len(), 3);
1118 assert_eq!(
1119 ToneRow::from([0u8, 6, 10]).pitches()[2].name_with_octave(),
1120 "B-"
1121 );
1122 }
1123}