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 {
147 Self {
148 pitch_classes: pitch_classes.into_iter().map(wrap).collect(),
149 }
150 }
151
152 pub fn from_pitches<'a>(pitches: impl IntoIterator<Item = &'a Pitch>) -> Self {
154 Self {
155 pitch_classes: pitches.into_iter().map(root::pitch_class).collect(),
156 }
157 }
158
159 pub fn pitch_classes(&self) -> &[u8] {
161 &self.pitch_classes
162 }
163
164 pub fn len(&self) -> usize {
166 self.pitch_classes.len()
167 }
168
169 pub fn is_empty(&self) -> bool {
171 self.pitch_classes.is_empty()
172 }
173
174 pub fn pitches(&self) -> Vec<Pitch> {
177 self.pitch_classes
178 .iter()
179 .map(|&pc| {
180 Pitch::from_pitch_class(IntegerType::from(pc))
181 .expect("a pitch class below twelve always spells")
182 })
183 .collect()
184 }
185
186 pub fn note_names(&self) -> Vec<String> {
194 self.pitches().iter().map(Pitch::name).collect()
195 }
196
197 pub fn is_twelve_tone_row(&self) -> bool {
199 self.pitch_classes.len() == 12 && (0..12u8).all(|pc| self.pitch_classes.contains(&pc))
200 }
201
202 pub fn make_twelve_tone_row(&self) -> Result<ToneRow> {
207 if !self.is_twelve_tone_row() {
208 return Err(Error::Serial(
209 "A twelve-tone row must contain each pitch class exactly once".to_string(),
210 ));
211 }
212 Ok(self.clone())
213 }
214
215 pub fn is_same_row(&self, other: &ToneRow) -> bool {
217 self == other
218 }
219
220 pub fn intervals_as_string(&self) -> String {
223 self.pitch_classes
224 .windows(2)
225 .map(|pair| {
226 let interval = wrap(IntegerType::from(pair[1]) - IntegerType::from(pair[0]));
227 match interval {
228 10 => 'T',
229 11 => 'E',
230 digit => char::from(b'0' + digit),
231 }
232 })
233 .collect()
234 }
235
236 pub fn zero_centered_transformation(
249 &self,
250 transformation: Transformation,
251 index: IntegerType,
252 ) -> ToneRow {
253 let Some(&first) = self.pitch_classes.first() else {
254 return ToneRow::default();
255 };
256 let first = IntegerType::from(first);
257 let forward = self.pitch_classes.iter().map(|&pc| IntegerType::from(pc));
258 let backward = self
259 .pitch_classes
260 .iter()
261 .rev()
262 .map(|&pc| IntegerType::from(pc));
263 let transformed: Vec<IntegerType> = match transformation {
264 Transformation::Prime => forward.map(|pc| pc - first + index).collect(),
265 Transformation::Inversion => forward.map(|pc| index + first - pc).collect(),
266 Transformation::Retrograde => backward.map(|pc| index + pc - first).collect(),
267 Transformation::RetrogradeInversion => backward.map(|pc| index - pc + first).collect(),
268 };
269 ToneRow::new(transformed)
270 }
271
272 pub fn original_centered_transformation(
276 &self,
277 transformation: Transformation,
278 index: IntegerType,
279 ) -> ToneRow {
280 let Some(&first) = self.pitch_classes.first() else {
281 return ToneRow::default();
282 };
283 self.zero_centered_transformation(transformation, IntegerType::from(first) + index)
284 }
285
286 pub fn transformation(
288 &self,
289 convention: TransformationConvention,
290 transformation: Transformation,
291 index: IntegerType,
292 ) -> ToneRow {
293 match convention {
294 TransformationConvention::ZeroCentered => {
295 self.zero_centered_transformation(transformation, index)
296 }
297 TransformationConvention::OriginalCentered => {
298 self.original_centered_transformation(transformation, index)
299 }
300 }
301 }
302
303 pub fn find_zero_centered_transformations(
306 &self,
307 other: &ToneRow,
308 ) -> Vec<IndexedTransformation> {
309 let (Some(&first), Some(&last)) = (other.pitch_classes.first(), other.pitch_classes.last())
310 else {
311 return Vec::new();
312 };
313 if self.len() != other.len() {
314 return Vec::new();
315 }
316 let candidates = [
317 (Transformation::Prime, first),
318 (Transformation::Inversion, first),
319 (Transformation::Retrograde, last),
320 (Transformation::RetrogradeInversion, last),
321 ];
322 candidates
323 .into_iter()
324 .filter(|&(transformation, index)| {
325 self.zero_centered_transformation(transformation, IntegerType::from(index))
326 == *other
327 })
328 .collect()
329 }
330
331 pub fn find_original_centered_transformations(
334 &self,
335 other: &ToneRow,
336 ) -> Vec<IndexedTransformation> {
337 let (Some(&old_first), Some(&old_last), Some(&new_first)) = (
338 self.pitch_classes.first(),
339 self.pitch_classes.last(),
340 other.pitch_classes.first(),
341 ) else {
342 return Vec::new();
343 };
344 if self.len() != other.len() {
345 return Vec::new();
346 }
347 let (old_first, old_last, new_first) = (
348 IntegerType::from(old_first),
349 IntegerType::from(old_last),
350 IntegerType::from(new_first),
351 );
352 let transposition = wrap(new_first - old_first);
353 let retrograde = wrap(new_first - old_last);
354 let retrograde_inversion = wrap(new_first - 2 * old_first + old_last);
355 let candidates = [
356 (Transformation::Prime, transposition),
357 (Transformation::Inversion, transposition),
358 (Transformation::Retrograde, retrograde),
359 (Transformation::RetrogradeInversion, retrograde_inversion),
360 ];
361 candidates
362 .into_iter()
363 .filter(|&(transformation, index)| {
364 self.original_centered_transformation(transformation, IntegerType::from(index))
365 == *other
366 })
367 .collect()
368 }
369
370 pub fn find_transformations(
373 &self,
374 convention: TransformationConvention,
375 other: &ToneRow,
376 ) -> Vec<IndexedTransformation> {
377 match convention {
378 TransformationConvention::ZeroCentered => {
379 self.find_zero_centered_transformations(other)
380 }
381 TransformationConvention::OriginalCentered => {
382 self.find_original_centered_transformations(other)
383 }
384 }
385 }
386
387 pub fn matrix(&self) -> TwelveToneMatrix {
401 let rows = self
402 .pitch_classes
403 .iter()
404 .map(|&pc| {
405 let transposition = wrap(12 - IntegerType::from(pc));
406 ToneRow::new(
407 self.pitch_classes
408 .iter()
409 .map(|&x| IntegerType::from(x) + IntegerType::from(transposition)),
410 )
411 })
412 .collect();
413 TwelveToneMatrix { rows }
414 }
415
416 pub fn find_historical(&self) -> Vec<&'static HistoricalRow> {
418 HISTORICAL_ROWS
419 .iter()
420 .filter(|historical| historical.pitch_classes == self.pitch_classes.as_slice())
421 .collect()
422 }
423
424 pub fn find_transformed_historical(
427 &self,
428 convention: TransformationConvention,
429 ) -> Vec<(&'static HistoricalRow, Vec<IndexedTransformation>)> {
430 HISTORICAL_ROWS
431 .iter()
432 .filter_map(|historical| {
433 let transformations = historical.row().find_transformations(convention, self);
434 (!transformations.is_empty()).then_some((historical, transformations))
435 })
436 .collect()
437 }
438
439 fn require_twelve_tone(&self, purpose: &str) -> Result<()> {
440 if self.is_twelve_tone_row() {
441 Ok(())
442 } else {
443 Err(Error::Serial(format!(
444 "{purpose} must be a twelve-tone row."
445 )))
446 }
447 }
448
449 pub fn is_all_interval(&self) -> Result<bool> {
462 self.require_twelve_tone("An all-interval row")?;
463 let intervals = self.intervals_as_string();
464 Ok("123456789TE"
465 .chars()
466 .all(|interval| intervals.contains(interval)))
467 }
468
469 pub fn link_classification(&self) -> Result<Option<LinkClassification>> {
475 self.require_twelve_tone("A Link Chord")?;
476 let forms = [
477 self.clone(),
478 self.zero_centered_transformation(Transformation::Inversion, 0),
479 self.zero_centered_transformation(Transformation::Retrograde, 0),
480 self.zero_centered_transformation(Transformation::RetrogradeInversion, 0),
481 ];
482 let mut number = None;
483 let mut special_intervals = Vec::new();
484 for form in &forms {
485 let intervals = form.intervals_as_string();
486 for link in LINK_CHORDS
487 .iter()
488 .filter(|link| link.intervals == intervals)
489 {
490 number = Some(link.classification);
491 special_intervals.push(link.special_intervals);
492 }
493 }
494 Ok(number.map(|number| LinkClassification {
495 number,
496 special_intervals,
497 }))
498 }
499
500 pub fn is_link_chord(&self) -> Result<bool> {
502 Ok(self.link_classification()?.is_some())
503 }
504
505 pub fn are_combinatorial(
511 &self,
512 first: Transformation,
513 first_index: IntegerType,
514 second: Transformation,
515 second_index: IntegerType,
516 ) -> Result<bool> {
517 self.require_twelve_tone("Combinatoriality applies only to twelve-tone rows; this")?;
518 let first = self.zero_centered_transformation(first, first_index);
519 let second = self.zero_centered_transformation(second, second_index);
520 let combined = first.pitch_classes[..6]
521 .iter()
522 .chain(&second.pitch_classes[..6])
523 .map(|&pc| IntegerType::from(pc));
524 Ok(ToneRow::new(combined).is_twelve_tone_row())
525 }
526}
527
528impl Index<usize> for ToneRow {
529 type Output = u8;
530
531 fn index(&self, index: usize) -> &Self::Output {
532 &self.pitch_classes[index]
533 }
534}
535
536impl<'a> IntoIterator for &'a ToneRow {
537 type Item = &'a u8;
538 type IntoIter = std::slice::Iter<'a, u8>;
539
540 fn into_iter(self) -> Self::IntoIter {
541 self.pitch_classes.iter()
542 }
543}
544
545impl fmt::Display for ToneRow {
546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
548 for &pc in &self.pitch_classes {
549 f.write_str(&convert_pitch_class_to_str(IntegerType::from(pc)))?;
550 }
551 Ok(())
552 }
553}
554
555impl From<Vec<u8>> for ToneRow {
556 fn from(pitch_classes: Vec<u8>) -> Self {
557 ToneRow::new(pitch_classes.into_iter().map(IntegerType::from))
558 }
559}
560
561impl<const N: usize> From<[u8; N]> for ToneRow {
562 fn from(pitch_classes: [u8; N]) -> Self {
563 ToneRow::new(pitch_classes.into_iter().map(IntegerType::from))
564 }
565}
566
567#[derive(Debug, Clone, PartialEq, Eq)]
570pub struct LinkClassification {
571 pub number: UnsignedIntegerType,
573 pub special_intervals: Vec<&'static str>,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq)]
580#[must_use]
581pub struct TwelveToneMatrix {
582 rows: Vec<ToneRow>,
583}
584
585impl TwelveToneMatrix {
586 pub fn rows(&self) -> &[ToneRow] {
588 &self.rows
589 }
590
591 pub fn row(&self, index: usize) -> Option<&ToneRow> {
593 self.rows.get(index)
594 }
595}
596
597impl fmt::Display for TwelveToneMatrix {
598 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601 for (index, row) in self.rows.iter().enumerate() {
602 if index > 0 {
603 writeln!(f)?;
604 }
605 for &pc in row.pitch_classes() {
606 write!(
607 f,
608 "{:>3}",
609 convert_pitch_class_to_str(IntegerType::from(pc))
610 )?;
611 }
612 }
613 Ok(())
614 }
615}
616
617pub fn row_to_matrix(pitch_classes: &[IntegerType]) -> String {
620 ToneRow::new(pitch_classes.iter().copied())
621 .matrix()
622 .rows()
623 .iter()
624 .map(|row| {
625 row.pitch_classes()
626 .iter()
627 .map(|pc| format!("{pc:>3}"))
628 .collect::<String>()
629 })
630 .collect::<Vec<_>>()
631 .join("\n")
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
637#[must_use]
638pub struct HistoricalRow {
639 pub name: &'static str,
641 pub composer: &'static str,
643 pub opus: Option<&'static str>,
645 pub title: &'static str,
647 pub pitch_classes: [u8; 12],
649}
650
651impl HistoricalRow {
652 pub fn row(&self) -> ToneRow {
654 ToneRow::from(self.pitch_classes)
655 }
656}
657
658pub fn historical_row_by_name(name: &str) -> Result<&'static HistoricalRow> {
669 let name = name.strip_prefix("Row").unwrap_or(name);
670 HISTORICAL_ROWS
671 .iter()
672 .find(|row| row.name == name)
673 .ok_or_else(|| Error::Serial("No historical row with given name found".to_string()))
674}
675
676struct LinkChord {
677 intervals: &'static str,
678 special_intervals: &'static str,
679 classification: UnsignedIntegerType,
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn only_a_permutation_of_the_twelve_classes_makes_a_twelve_tone_row() {
688 assert!(ToneRow::new(0..11).make_twelve_tone_row().is_err());
689 let row = ToneRow::new(0..12).make_twelve_tone_row().unwrap();
690 assert_eq!(row.pitch_classes().len(), 12);
691 }
692
693 #[test]
694 fn a_row_indexes_and_iterates_over_its_pitch_classes() {
695 let row = ToneRow::new([0, 13, -1]);
696
697 assert_eq!(row[1], 1);
698 let collected: Vec<u8> = (&row).into_iter().copied().collect();
699 assert_eq!(collected, vec![0, 1, 11]);
700 }
701
702 fn row(pitch_classes: impl IntoIterator<Item = IntegerType>) -> ToneRow {
703 ToneRow::new(pitch_classes)
704 }
705
706 fn chromatic() -> ToneRow {
707 row(0..12)
708 }
709
710 fn historical(name: &str) -> ToneRow {
711 historical_row_by_name(name).unwrap().row()
712 }
713
714 fn labelled(
715 transformations: &[IndexedTransformation],
716 convention: TransformationConvention,
717 ) -> Vec<(&'static str, u8)> {
718 transformations
719 .iter()
720 .map(|&(transformation, index)| (transformation.label(convention), index))
721 .collect()
722 }
723
724 #[test]
725 fn pitch_classes_are_reduced_modulo_twelve() {
726 let quintuple = row((0..12).map(|i| 5 * i));
727 assert_eq!(
728 quintuple.pitch_classes(),
729 &[0, 5, 10, 3, 8, 1, 6, 11, 4, 9, 2, 7]
730 );
731 assert_eq!(quintuple.to_string(), "05A3816B4927");
732 assert_eq!(row([-6, 19, 128]).pitch_classes(), &[6, 7, 8]);
733 assert_eq!(row([-6, 19, 128]).note_names(), ["F#", "G", "G#"]);
734 }
735
736 #[test]
737 fn note_names_spell_pitch_classes_as_music21_does() {
738 assert_eq!(
739 chromatic().note_names(),
740 [
741 "C", "C#", "D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B"
742 ]
743 );
744 }
745
746 #[test]
747 fn twelve_tone_row_needs_all_twelve_pitch_classes_once() {
748 assert!(chromatic().is_twelve_tone_row());
749 assert!(!row([0, 4, 8]).is_twelve_tone_row());
750 assert!(!row([3; 12]).is_twelve_tone_row());
751 assert!(!ToneRow::default().is_twelve_tone_row());
752 }
753
754 #[test]
755 fn same_row_compares_pitch_classes_in_order() {
756 assert!(row([6, 7, 8]).is_same_row(&row([-6, 19, 128])));
757 assert!(!row([6, 7, 8]).is_same_row(&row([6, 7, -8])));
758 assert!(!row([6, 7, 8]).is_same_row(&row([6, 7])));
759 }
760
761 #[test]
762 fn intervals_as_string_uses_t_and_e() {
763 assert_eq!(row([0]).intervals_as_string(), "");
764 assert_eq!(ToneRow::default().intervals_as_string(), "");
765 assert_eq!(row((0..12).rev()).intervals_as_string(), "EEEEEEEEEEE");
766 assert_eq!(
767 historical("BergLyricSuite").intervals_as_string(),
768 "E89T7652341"
769 );
770 }
771
772 #[test]
773 fn zero_centered_transformations_match_music21() {
774 assert_eq!(
775 chromatic()
776 .zero_centered_transformation(Transformation::Prime, 3)
777 .pitch_classes(),
778 &[3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2]
779 );
780 assert_eq!(
781 chromatic()
782 .zero_centered_transformation(Transformation::Inversion, 6)
783 .pitch_classes(),
784 &[6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7]
785 );
786 let schoenberg = historical("SchoenbergOp26");
787 assert_eq!(
788 schoenberg.pitch_classes(),
789 &[3, 7, 9, 11, 1, 0, 10, 2, 4, 6, 8, 5]
790 );
791 assert_eq!(
792 schoenberg
793 .zero_centered_transformation(Transformation::Retrograde, 8)
794 .pitch_classes(),
795 &[10, 1, 11, 9, 7, 3, 5, 6, 4, 2, 0, 8]
796 );
797 assert_eq!(
798 schoenberg
799 .zero_centered_transformation(Transformation::RetrogradeInversion, 9)
800 .note_names(),
801 [
802 "G", "E", "F#", "G#", "Bb", "D", "C", "B", "C#", "Eb", "F", "A"
803 ]
804 );
805 }
806
807 #[test]
808 fn original_centered_transformations_match_music21() {
809 assert_eq!(
810 chromatic()
811 .original_centered_transformation(Transformation::Prime, 3)
812 .pitch_classes(),
813 &[3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2]
814 );
815 assert_eq!(
816 chromatic()
817 .original_centered_transformation(Transformation::Inversion, 6)
818 .pitch_classes(),
819 &[6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7]
820 );
821 let schoenberg = historical("SchoenbergOp26");
822 assert_eq!(
823 schoenberg
824 .original_centered_transformation(Transformation::Retrograde, 8)
825 .pitch_classes(),
826 &[1, 4, 2, 0, 10, 6, 8, 9, 7, 5, 3, 11]
827 );
828 assert_eq!(
829 schoenberg
830 .original_centered_transformation(Transformation::RetrogradeInversion, 9)
831 .note_names(),
832 [
833 "Bb", "G", "A", "B", "C#", "F", "Eb", "D", "E", "F#", "G#", "C"
834 ]
835 );
836 }
837
838 #[test]
839 fn transforming_an_empty_row_gives_an_empty_row() {
840 let empty = ToneRow::default();
841 assert!(
842 empty
843 .zero_centered_transformation(Transformation::Retrograde, 3)
844 .is_empty()
845 );
846 assert!(
847 empty
848 .original_centered_transformation(Transformation::Inversion, 3)
849 .is_empty()
850 );
851 assert!(empty.find_zero_centered_transformations(&empty).is_empty());
852 assert!(
853 empty
854 .find_original_centered_transformations(&empty)
855 .is_empty()
856 );
857 }
858
859 #[test]
860 fn finds_zero_centered_transformations_between_rows() {
861 let zero = TransformationConvention::ZeroCentered;
862 let rising = row([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1]);
863 let falling = row([8, 7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9]);
864 assert_eq!(
865 labelled(&rising.find_zero_centered_transformations(&falling), zero),
866 [("I", 8), ("R", 9)]
867 );
868 let op25 = historical("SchoenbergOp25");
869 let op26 = historical("SchoenbergOp26");
870 assert!(op25.find_zero_centered_transformations(&op26).is_empty());
871 let ri8 = op26.zero_centered_transformation(Transformation::RetrogradeInversion, 8);
872 assert_eq!(
873 labelled(&op26.find_zero_centered_transformations(&ri8), zero),
874 [("RI", 8)]
875 );
876 assert!(
877 op26.find_zero_centered_transformations(&row([0]))
878 .is_empty()
879 );
880 }
881
882 #[test]
883 fn finds_original_centered_transformations_between_rows() {
884 let original = TransformationConvention::OriginalCentered;
885 let rising = row([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1]);
886 let falling = row([8, 7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9]);
887 assert_eq!(
888 labelled(
889 &rising.find_original_centered_transformations(&falling),
890 original
891 ),
892 [("I", 6), ("R", 7)]
893 );
894 let op25 = historical("SchoenbergOp25");
895 let op26 = historical("SchoenbergOp26");
896 assert!(
897 op25.find_original_centered_transformations(&op26)
898 .is_empty()
899 );
900 let ri8 = op26.original_centered_transformation(Transformation::RetrogradeInversion, 8);
901 assert_eq!(
902 labelled(&op26.find_original_centered_transformations(&ri8), original),
903 [("RI", 8)]
904 );
905 assert_eq!(
906 labelled(&rising.find_transformations(original, &falling), original),
907 [("I", 6), ("R", 7)]
908 );
909 }
910
911 #[test]
912 fn every_transformation_is_found_again() {
913 let source = historical("WebernOp27");
914 for convention in [
915 TransformationConvention::ZeroCentered,
916 TransformationConvention::OriginalCentered,
917 ] {
918 for transformation in Transformation::ALL {
919 for index in 0..12 {
920 let target = source.transformation(convention, transformation, index);
921 let found = source.find_transformations(convention, &target);
922 assert!(
923 found.contains(&(transformation, index as u8)),
924 "{transformation:?} {index} under {convention:?} not found in {found:?}"
925 );
926 }
927 }
928 }
929 }
930
931 #[test]
932 fn matrix_matches_music21_layout() {
933 let matrix = row([0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]).matrix();
934 let expected = [
935 " 0 2 B 7 8 3 9 1 4 A 6 5",
936 " A 0 9 5 6 1 7 B 2 8 4 3",
937 " 1 3 0 8 9 4 A 2 5 B 7 6",
938 " 5 7 4 0 1 8 2 6 9 3 B A",
939 " 4 6 3 B 0 7 1 5 8 2 A 9",
940 " 9 B 8 4 5 0 6 A 1 7 3 2",
941 " 3 5 2 A B 6 0 4 7 1 9 8",
942 " B 1 A 6 7 2 8 0 3 9 5 4",
943 " 8 A 7 3 4 B 5 9 0 6 2 1",
944 " 2 4 1 9 A 5 B 3 6 0 8 7",
945 " 6 8 5 1 2 9 3 7 A 4 0 B",
946 " 7 9 6 2 3 A 4 8 B 5 1 0",
947 ]
948 .join(
949 "
950",
951 );
952 assert_eq!(matrix.to_string(), expected);
953 assert_eq!(matrix.rows().len(), 12);
954 assert_eq!(
955 matrix.row(0).unwrap().pitch_classes(),
956 &[0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]
957 );
958 assert!(matrix.row(12).is_none());
959
960 let op37 = historical("SchoenbergOp37").matrix();
961 assert_eq!(
962 op37.row(0).unwrap().note_names(),
963 [
964 "C", "B", "G", "G#", "Eb", "C#", "D", "Bb", "F#", "F", "E", "A"
965 ]
966 );
967 assert!(
968 op37.to_string()
969 .starts_with(" 0 B 7 8 3 1 2 A 6 5 4 9\n")
970 );
971 }
972
973 #[test]
974 fn row_to_matrix_writes_decimal_pitch_classes() {
975 let text = row_to_matrix(&[0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]);
976 let lines: Vec<&str> = text.lines().collect();
977 assert_eq!(lines.len(), 12);
978 assert_eq!(lines[0], " 0 2 11 7 8 3 9 1 4 10 6 5");
979 assert_eq!(lines[1], " 10 0 9 5 6 1 7 11 2 8 4 3");
980 assert_eq!(lines[11], " 7 9 6 2 3 10 4 8 11 5 1 0");
981 }
982
983 #[test]
984 fn historical_rows_are_found_by_name_with_or_without_the_old_prefix() {
985 let webern = historical_row_by_name("WebernOp29").unwrap();
986 assert_eq!(webern.composer, "Webern");
987 assert_eq!(webern.opus, Some("Op. 29"));
988 assert_eq!(webern.title, "Cantata I");
989 assert_eq!(webern.pitch_classes, [3, 11, 2, 1, 5, 4, 7, 6, 10, 9, 0, 8]);
990 assert_eq!(historical_row_by_name("RowWebernOp29").unwrap(), webern);
991 assert_eq!(
992 historical_row_by_name("SchoenbergJakobsleiter")
993 .unwrap()
994 .opus,
995 None
996 );
997 assert!(matches!(
998 historical_row_by_name("Nope"),
999 Err(Error::Serial(_))
1000 ));
1001 }
1002
1003 #[test]
1004 fn every_historical_row_is_a_twelve_tone_row_with_a_unique_name() {
1005 let mut names: Vec<&str> = HISTORICAL_ROWS.iter().map(|row| row.name).collect();
1006 names.sort_unstable();
1007 names.dedup();
1008 assert_eq!(names.len(), HISTORICAL_ROWS.len());
1009 for historical in &HISTORICAL_ROWS {
1010 assert!(
1011 historical.row().is_twelve_tone_row(),
1012 "{} is not a twelve-tone row",
1013 historical.name
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn finds_historical_rows_and_their_transformations() {
1020 let names = |rows: Vec<&HistoricalRow>| -> Vec<&str> {
1021 rows.into_iter().map(|row| row.name).collect()
1022 };
1023 assert_eq!(
1024 names(row([2, 3, 9, 1, 11, 5, 8, 7, 4, 0, 10, 6]).find_historical()),
1025 ["SchoenbergOp32"]
1026 );
1027 assert!(chromatic().find_historical().is_empty());
1028
1029 let transformed = row([5, 9, 11, 3, 6, 7, 4, 10, 0, 8, 2, 1]);
1030 let original =
1031 transformed.find_transformed_historical(TransformationConvention::OriginalCentered);
1032 assert_eq!(original.len(), 1);
1033 assert_eq!(original[0].0.name, "SchoenbergOp32");
1034 assert_eq!(
1035 labelled(&original[0].1, TransformationConvention::OriginalCentered),
1036 [("R", 11)]
1037 );
1038 let zero = transformed.find_transformed_historical(TransformationConvention::ZeroCentered);
1039 assert_eq!(zero[0].0.name, "SchoenbergOp32");
1040 assert_eq!(
1041 labelled(&zero[0].1, TransformationConvention::ZeroCentered),
1042 [("R", 1)]
1043 );
1044 }
1045
1046 #[test]
1047 fn all_interval_rows() {
1048 assert!(!chromatic().is_all_interval().unwrap());
1049 let berg = historical("BergLyricSuite");
1050 assert_eq!(
1051 berg.pitch_classes(),
1052 &[5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11]
1053 );
1054 assert!(berg.is_all_interval().unwrap());
1055 assert!(matches!(
1056 row([0, 4, 8]).is_all_interval(),
1057 Err(Error::Serial(_))
1058 ));
1059 }
1060
1061 #[test]
1062 fn link_chords_match_music21() {
1063 let berg = historical("BergLyricSuite");
1064 assert_eq!(berg.link_classification().unwrap(), None);
1065 assert!(!berg.is_link_chord().unwrap());
1066
1067 let link = row([0, 3, 8, 2, 10, 11, 9, 4, 1, 5, 7, 6]);
1068 assert_eq!(
1069 link.link_classification().unwrap(),
1070 Some(LinkClassification {
1071 number: 62,
1072 special_intervals: vec!["8352E"],
1073 })
1074 );
1075 assert!(link.is_link_chord().unwrap());
1076
1077 let double = row([0, 1, 8, 5, 7, 10, 4, 3, 11, 9, 2, 6]);
1078 assert_eq!(
1079 double.link_classification().unwrap(),
1080 Some(LinkClassification {
1081 number: 33,
1082 special_intervals: vec!["236E8", "36E8T"],
1083 })
1084 );
1085 assert!(matches!(
1086 row([0, 4, 8]).is_link_chord(),
1087 Err(Error::Serial(_))
1088 ));
1089 }
1090
1091 #[test]
1092 fn every_link_chord_interval_string_classifies_as_itself() {
1093 for link in &LINK_CHORDS {
1094 let mut pitch_classes = vec![0];
1095 for interval in link.intervals.chars() {
1096 let step = match interval {
1097 'T' => 10,
1098 'E' => 11,
1099 digit => IntegerType::from(digit.to_digit(10).unwrap() as u8),
1100 };
1101 pitch_classes.push(pitch_classes.last().unwrap() + step);
1102 }
1103 let classified = row(pitch_classes).link_classification().unwrap().unwrap();
1104 assert_eq!(classified.number, link.classification, "{}", link.intervals);
1105 assert!(
1106 classified
1107 .special_intervals
1108 .contains(&link.special_intervals)
1109 );
1110 }
1111 }
1112
1113 #[test]
1114 fn combinatoriality_matches_music21() {
1115 let moses = historical("SchoenbergMosesAron");
1116 assert_eq!(
1117 moses.pitch_classes(),
1118 &[9, 10, 4, 2, 3, 1, 7, 5, 6, 8, 11, 0]
1119 );
1120 let (p, i, r, ri) = (
1121 Transformation::Prime,
1122 Transformation::Inversion,
1123 Transformation::Retrograde,
1124 Transformation::RetrogradeInversion,
1125 );
1126 assert!(moses.are_combinatorial(p, 0, i, 3).unwrap());
1127 assert!(moses.are_combinatorial(p, 1, i, 4).unwrap());
1128 assert!(moses.are_combinatorial(r, 1, ri, 4).unwrap());
1129 assert!(!moses.are_combinatorial(r, 6, ri, 4).unwrap());
1130 assert!(matches!(
1131 row([0, 4, 8]).are_combinatorial(p, 0, i, 3),
1132 Err(Error::Serial(_))
1133 ));
1134 }
1135
1136 #[test]
1137 fn transformation_labels_round_trip() {
1138 for transformation in Transformation::ALL {
1139 for convention in [
1140 TransformationConvention::ZeroCentered,
1141 TransformationConvention::OriginalCentered,
1142 ] {
1143 let label = transformation.label(convention);
1144 assert_eq!(Transformation::from_name(label).unwrap(), transformation);
1145 assert_eq!(label.parse::<Transformation>().unwrap(), transformation);
1146 }
1147 }
1148 assert_eq!(Transformation::Prime.to_string(), "P");
1149 assert!(matches!(
1150 Transformation::from_name("X"),
1151 Err(Error::Serial(_))
1152 ));
1153 assert_eq!(
1154 TransformationConvention::from_name("zero").unwrap(),
1155 TransformationConvention::ZeroCentered
1156 );
1157 assert!(TransformationConvention::from_name("sideways").is_err());
1158 }
1159
1160 #[test]
1161 fn rows_build_from_pitches_and_arrays() {
1162 let pitches: Vec<Pitch> = ["C4", "F#3", "B-5"]
1163 .iter()
1164 .map(|&name| Pitch::from_name(name).unwrap())
1165 .collect();
1166 assert_eq!(ToneRow::from_pitches(&pitches).pitch_classes(), &[0, 6, 10]);
1167 assert_eq!(ToneRow::from([0u8, 6, 10]).pitch_classes(), &[0, 6, 10]);
1168 assert_eq!(ToneRow::from(vec![0u8, 6, 10]).len(), 3);
1169 assert_eq!(
1170 ToneRow::from([0u8, 6, 10]).pitches()[2].name_with_octave(),
1171 "Bb"
1172 );
1173 }
1174}