Skip to main content

music21/serial/
mod.rs

1//! Tone rows and twelve-tone serial transformations, a port of music21's
2//! `serial` module.
3//!
4//! A [`ToneRow`] is an ordered sequence of pitch classes. It need not have
5//! twelve members and need not be a permutation of the aggregate; the methods
6//! that only make sense for a true twelve-tone row (`is_all_interval`,
7//! `link_classification`, `are_combinatorial`) return an error otherwise,
8//! where music21 raises `SerialException`.
9//!
10//! music21 separates `ToneRow`, `TwelveToneRow` and `HistoricalTwelveToneRow`
11//! as stream subclasses. Nothing here dispatches on that distinction, so there
12//! is one row type, and the historical rows are a table of [`HistoricalRow`]
13//! entries that build a `ToneRow` on demand.
14
15use 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/// A serial transformation of a tone row.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub enum Transformation {
35    /// The row itself, transposed. music21 writes this `P` in the
36    /// zero-centered convention and `T` in the original-centered one.
37    Prime,
38    /// The row inverted about its first pitch class.
39    Inversion,
40    /// The row backwards.
41    Retrograde,
42    /// The inversion backwards.
43    RetrogradeInversion,
44}
45
46impl Transformation {
47    /// The four transformations in music21's order.
48    pub const ALL: [Transformation; 4] = [
49        Transformation::Prime,
50        Transformation::Inversion,
51        Transformation::Retrograde,
52        Transformation::RetrogradeInversion,
53    ];
54
55    /// music21's label under the given convention: `P`/`T`, `I`, `R`, `RI`.
56    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    /// Parses music21's label. `P` and `T` both mean [`Transformation::Prime`].
67    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/// What the index of a transformation refers to.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97pub enum TransformationConvention {
98    /// `P(n)` and `I(n)` start on pitch class `n`; `R(n)` and `RI(n)` end on
99    /// it. This is the common convention and music21's default.
100    ZeroCentered,
101    /// `T(n)` transposes the original row up `n` semitones, and `I(n)`,
102    /// `R(n)` and `RI(n)` transform the row in place and then transpose the
103    /// result by `n`.
104    OriginalCentered,
105}
106
107impl TransformationConvention {
108    /// Parses music21's convention argument, `"zero"` or `"original"`.
109    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
120/// A transformation and its index, as music21 reports one: `("I", 8)`.
121pub type IndexedTransformation = (Transformation, u8);
122
123/// An ordered sequence of pitch classes.
124#[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    /// Builds a row from pitch-class integers, which are reduced modulo 12 the
137    /// way music21's `pcToToneRow` does, so `[-6, 19, 128]` is `[6, 7, 8]`.
138    ///
139    /// ```
140    /// use music21::ToneRow;
141    ///
142    /// let row = ToneRow::new([0, 11, 7, 8, 3, 1, 2, 10, 6, 5, 4, 9]);
143    /// assert!(row.is_twelve_tone_row());
144    /// assert_eq!(row[1], 11);
145    /// ```
146    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    /// Builds a row from the pitch classes of a sequence of pitches.
153    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    /// The pitch classes in order.
160    pub fn pitch_classes(&self) -> &[u8] {
161        &self.pitch_classes
162    }
163
164    /// The number of pitch classes in the row.
165    pub fn len(&self) -> usize {
166        self.pitch_classes.len()
167    }
168
169    /// Whether the row has no pitch classes.
170    pub fn is_empty(&self) -> bool {
171        self.pitch_classes.is_empty()
172    }
173
174    /// The row as octave-less pitches, spelled the way music21 spells a pitch
175    /// built from a pitch class (`C#`, `E-`, `F#`, `G#`, `B-`).
176    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    /// The note names of the row, music21's `noteNames`.
187    ///
188    /// ```
189    /// use music21::ToneRow;
190    ///
191    /// assert_eq!(ToneRow::new([0, 4, 7]).note_names(), ["C", "E", "G"]);
192    /// ```
193    pub fn note_names(&self) -> Vec<String> {
194        self.pitches().iter().map(Pitch::name).collect()
195    }
196
197    /// Whether the row is a permutation of all twelve pitch classes.
198    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    /// The row as a twelve-tone row: music21's `makeTwelveToneRow`. There is
203    /// one row type here, so this is the row itself, and a row that is not a
204    /// permutation of the twelve pitch classes is an error rather than a row
205    /// whose twelve-tone questions fail one by one.
206    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    /// Whether two rows have the same pitch classes in the same order.
216    pub fn is_same_row(&self, other: &ToneRow) -> bool {
217        self == other
218    }
219
220    /// The intervals between consecutive pitch classes as one character each,
221    /// with `T` for ten and `E` for eleven: music21's `getIntervalsAsString`.
222    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    /// Transforms the row in the zero-centered convention, where `P(n)` and
237    /// `I(n)` start on pitch class `n` and `R(n)` and `RI(n)` end on it.
238    ///
239    /// ```
240    /// use music21::{ToneRow, Transformation};
241    ///
242    /// let row = ToneRow::new([0, 4, 7]);
243    /// let inverted = row.zero_centered_transformation(Transformation::Inversion, 2);
244    /// assert_eq!(inverted.pitch_classes(), [2, 10, 7]);
245    /// let backwards = row.zero_centered_transformation(Transformation::Retrograde, 0);
246    /// assert_eq!(backwards.pitch_classes(), [7, 4, 0]);
247    /// ```
248    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    /// Transforms the row in the original-centered convention, where `T(n)`
273    /// transposes the row up `n` semitones and the other transformations act
274    /// in place before transposing by `n`.
275    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    /// Transforms the row under either convention.
287    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    /// The zero-centered transformations that take this row to `other`, in
304    /// music21's order `P`, `I`, `R`, `RI`.
305    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    /// The original-centered transformations that take this row to `other`,
332    /// in music21's order `T`, `I`, `R`, `RI`.
333    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    /// The transformations that take this row to `other` under either
371    /// convention.
372    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    /// The row's matrix: every transposition of the row, ordered so that the
388    /// leading diagonal is zero and each column reads the inversion.
389    ///
390    /// ```
391    /// use music21::ToneRow;
392    ///
393    /// let row = ToneRow::new([0, 11, 7, 8, 3, 1, 2, 10, 6, 5, 4, 9]);
394    /// let matrix = row.matrix();
395    /// assert_eq!(matrix.rows().len(), 12);
396    /// // The second row is the prime starting where the inversion's second
397    /// // note does.
398    /// assert_eq!(matrix.row(1).map(|row| row[0]), Some(1));
399    /// ```
400    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    /// The historical rows identical to this one.
417    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    /// The historical rows of which this row is a transformation, each with
425    /// the transformations taking the historical row to this one.
426    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    /// Whether every interval class from one to eleven occurs between
450    /// consecutive members of the row.
451    ///
452    /// Errors unless the row is a twelve-tone row.
453    ///
454    /// ```
455    /// use music21::ToneRow;
456    ///
457    /// let row = ToneRow::new([0, 11, 7, 4, 2, 9, 3, 8, 10, 1, 5, 6]);
458    /// assert!(row.is_all_interval()?);
459    /// # Ok::<(), music21::Error>(())
460    /// ```
461    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    /// The Link chord classification of the row, if it is one: an
470    /// all-interval row containing a voicing of the all-trichord hexachord
471    /// `[0, 1, 2, 4, 7, 8]`, numbered as in John Link's catalogue.
472    ///
473    /// Errors unless the row is a twelve-tone row.
474    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    /// Whether the row is a Link chord. Errors unless it is a twelve-tone row.
501    pub fn is_link_chord(&self) -> Result<bool> {
502        Ok(self.link_classification()?.is_some())
503    }
504
505    /// Whether two zero-centered transformations of the row are
506    /// hexachordally combinatorial: their first hexachords together form the
507    /// aggregate.
508    ///
509    /// Errors unless the row is a twelve-tone row.
510    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    /// The pitch classes as single characters, `A` and `B` for ten and eleven.
547    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/// The Link chord number of a row and the interval sets that voice the
568/// all-trichord hexachord within it.
569#[derive(Debug, Clone, PartialEq, Eq)]
570pub struct LinkClassification {
571    /// The classification number, 1 to 194.
572    pub number: UnsignedIntegerType,
573    /// The five-interval strings within the row (or a transformation of it)
574    /// that voice the all-trichord hexachord.
575    pub special_intervals: Vec<&'static str>,
576}
577
578/// The transpositions of a row laid out as a matrix.
579#[derive(Debug, Clone, PartialEq, Eq)]
580#[must_use]
581pub struct TwelveToneMatrix {
582    rows: Vec<ToneRow>,
583}
584
585impl TwelveToneMatrix {
586    /// The rows of the matrix, top to bottom.
587    pub fn rows(&self) -> &[ToneRow] {
588        &self.rows
589    }
590
591    /// One row of the matrix.
592    pub fn row(&self, index: usize) -> Option<&ToneRow> {
593        self.rows.get(index)
594    }
595}
596
597impl fmt::Display for TwelveToneMatrix {
598    /// music21's layout: each pitch class right-aligned in three columns,
599    /// with `A` and `B` for ten and eleven.
600    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
617/// music21's `rowToMatrix`: the matrix of a pitch-class list as text, with
618/// the pitch classes written in decimal rather than as `A` and `B`.
619pub 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/// A twelve-tone row from the historical literature, with the attributes
635/// music21 stores for it.
636#[derive(Debug, Clone, PartialEq, Eq)]
637#[must_use]
638pub struct HistoricalRow {
639    /// music21's key for the row, such as `SchoenbergOp37`.
640    pub name: &'static str,
641    /// The composer's surname.
642    pub composer: &'static str,
643    /// The opus, where the work has one.
644    pub opus: Option<&'static str>,
645    /// The title of the work, or of the row within it.
646    pub title: &'static str,
647    /// The row's pitch classes.
648    pub pitch_classes: [u8; 12],
649}
650
651impl HistoricalRow {
652    /// The row as a [`ToneRow`].
653    pub fn row(&self) -> ToneRow {
654        ToneRow::from(self.pitch_classes)
655    }
656}
657
658/// Looks a historical row up by music21's name for it. The pre-v6 names with a
659/// `Row` prefix are accepted too.
660///
661/// ```
662/// use music21::historical_row_by_name;
663///
664/// let row = historical_row_by_name("WebernOp21")?.row();
665/// assert!(row.is_twelve_tone_row());
666/// # Ok::<(), music21::Error>(())
667/// ```
668pub 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}