Skip to main content

music21_rs/analysis/
neoriemannian.rs

1//! Neo-Riemannian transformations of triads: music21's
2//! `analysis.neoRiemannian`.
3//!
4//! Each of L, P and R moves one pitch of a major or minor triad and keeps
5//! the other two, turning major into minor and back:
6//!
7//! ```text
8//!           C E G (C major)
9//!   L: C -> B      B E G   (E minor)
10//!   P: E -> E-     C E- G  (C minor)
11//!   R: G -> A      C E A   (A minor)
12//! ```
13//!
14//! Chains of them name the other relations: slide is `LPR`, the
15//! Nebenverwandt `RLP`, the chromatic and disjunct mediants two or three
16//! steps each, and `PLPLPL` walks a hexatonic cycle back to where it began.
17
18use crate::{
19    chord::Chord,
20    error::{Error, Result},
21    interval::Interval,
22    pitch::Pitch,
23};
24
25use super::enharmonics::{EnharmonicRules, best_spelling};
26
27/// The Forte class every major and minor triad belongs to.
28const TRIAD_FORTE_CLASS: &str = "3-11";
29
30/// The hexatonic cycle: `PLPLPL`.
31const HEXATONIC_CYCLE: [Transform; 6] = [
32    Transform::P,
33    Transform::L,
34    Transform::P,
35    Transform::L,
36    Transform::P,
37    Transform::L,
38];
39
40/// Slide: `LPR`.
41const SLIDE: [Transform; 3] = [Transform::L, Transform::P, Transform::R];
42
43/// Nebenverwandt: `RLP`.
44const NEBENVERWANDT: [Transform; 3] = [Transform::R, Transform::L, Transform::P];
45
46/// One of the three neo-Riemannian transformations.
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum Transform {
50    /// Leading-tone exchange: a major root falls a semitone, a minor fifth
51    /// rises one. C major becomes E minor.
52    L,
53    /// Parallel: the third moves a semitone. C major becomes C minor.
54    P,
55    /// Relative: a major fifth rises a tone, a minor root falls one. C
56    /// major becomes A minor.
57    R,
58}
59
60/// The order a chain of transformations is read in.
61#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub enum ChainOrder {
64    /// `LPR` is L, then P, then R. music21's default.
65    #[default]
66    LeftToRight,
67    /// `LPR` is R, then P, then L: function notation, music21's
68    /// `leftOrdered=True`.
69    RightToLeft,
70}
71
72/// Whether a result keeps the spelling the transformations gave it.
73#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub enum Respell {
76    /// Keep double flats and sharps where they arise: `Dbb4 Fb4 Abb4`.
77    #[default]
78    Keep,
79    /// Respell with the best enharmonics: `C4 E4 G4`.
80    Simplify,
81}
82
83/// A chromatic mediant: root motion by a third, mode kept, one common tone.
84#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum Mediant {
87    /// Upper flat mediant, `UFM`: C major to E- major.
88    UpperFlat,
89    /// Upper sharp mediant, `USM`: C major to E major.
90    UpperSharp,
91    /// Lower flat mediant, `LFM`: C major to A- major.
92    LowerFlat,
93    /// Lower sharp mediant, `LSM`: C major to A major.
94    LowerSharp,
95}
96
97/// Which of the two disjunct mediants: root motion by a chromatic third,
98/// mode changed, no common tone.
99#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub enum MediantSide {
102    /// The mediant above: C major to E- minor.
103    Upper,
104    /// The mediant below: C major to G# minor.
105    Lower,
106}
107
108/// Richard Cohn's four hexatonic systems, each the six triads one cycle of
109/// `PL` passes through, named by the pitch classes of their roots.
110#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum HexatonicSystem {
113    /// Roots 0, 4 and 8: C, E, A-.
114    Northern,
115    /// Roots 1, 5 and 9: C#, F, A.
116    Eastern,
117    /// Roots 2, 6 and 10: D, F#, B-.
118    Southern,
119    /// Roots 3, 7 and 11: E-, G, B.
120    Western,
121}
122
123impl Transform {
124    /// The transformation a letter names: `L`, `P` or `R`.
125    ///
126    /// # Errors
127    ///
128    /// Any other letter.
129    pub fn from_symbol(symbol: char) -> Result<Self> {
130        match symbol {
131            'L' => Ok(Self::L),
132            'P' => Ok(Self::P),
133            'R' => Ok(Self::R),
134            other => Err(Error::Analysis(format!(
135                "{other} is not a NeoRiemannian transformation (L, R, or P)"
136            ))),
137        }
138    }
139
140    /// The letter naming the transformation.
141    pub fn symbol(self) -> char {
142        match self {
143            Self::L => 'L',
144            Self::P => 'P',
145            Self::R => 'R',
146        }
147    }
148
149    /// The transformations a string of letters names, in the order written.
150    ///
151    /// # Errors
152    ///
153    /// A letter other than `L`, `P` or `R`.
154    pub fn parse_chain(symbols: &str) -> Result<Vec<Self>> {
155        symbols.chars().map(Self::from_symbol).collect()
156    }
157
158    /// music21's `L`, `P` and `R`: `chord` transformed, every other pitch
159    /// kept where it was. The result's spelling is no longer inferred, and
160    /// it keeps `chord`'s duration.
161    ///
162    /// ```
163    /// use music21_rs::{Chord, Pitch, analysis::neoriemannian::Transform};
164    ///
165    /// let c_major = Chord::new("C4 E4 G4")?;
166    /// let e_minor = Transform::L.apply(&c_major)?;
167    /// assert_eq!(e_minor.pitches().iter().map(Pitch::name_with_octave).collect::<Vec<_>>(), ["B3", "E4", "G4"]);
168    /// # Ok::<(), music21_rs::Error>(())
169    /// ```
170    ///
171    /// # Errors
172    ///
173    /// A chord that is not a major or minor triad.
174    pub fn apply(self, chord: &Chord) -> Result<Chord> {
175        let (interval, changing) = if chord.is_major_triad() {
176            match self {
177                Self::L => ("-m2", chord.root()),
178                Self::P => ("-A1", chord.third()),
179                Self::R => ("M2", chord.fifth()),
180            }
181        } else if chord.is_minor_triad() {
182            match self {
183                Self::L => ("m2", chord.fifth()),
184                Self::P => ("A1", chord.third()),
185                Self::R => ("-M2", chord.root()),
186            }
187        } else {
188            return Err(self.not_a_triad());
189        };
190
191        let changing = changing.ok_or_else(|| self.not_a_triad())?;
192        single_pitch_transform(chord, &Interval::from_name(interval)?, changing)
193    }
194
195    /// music21's refusal, which spells the modes in capitals for P and R.
196    fn not_a_triad(self) -> Error {
197        let modes = match self {
198            Self::L => "major or minor",
199            Self::P | Self::R => "Major or Minor",
200        };
201        Error::Analysis(format!(
202            "Cannot perform {} on this chord: not a {modes} triad",
203            self.symbol()
204        ))
205    }
206}
207
208impl Mediant {
209    /// Every mediant, in the order music21 tries them.
210    pub const ALL: [Self; 4] = [
211        Self::UpperFlat,
212        Self::UpperSharp,
213        Self::LowerFlat,
214        Self::LowerSharp,
215    ];
216
217    /// The mediant music21 abbreviates so: `UFM`, `USM`, `LFM` or `LSM`.
218    ///
219    /// # Errors
220    ///
221    /// Any other abbreviation.
222    pub fn from_code(code: &str) -> Result<Self> {
223        Self::ALL
224            .into_iter()
225            .find(|mediant| mediant.code() == code)
226            .ok_or_else(|| {
227                Error::Value(
228                    "Transformation must be one of ['UFM', 'USM', 'LFM', 'LSM']".to_string(),
229                )
230            })
231    }
232
233    /// music21's abbreviation.
234    pub fn code(self) -> &'static str {
235        match self {
236            Self::UpperFlat => "UFM",
237            Self::UpperSharp => "USM",
238            Self::LowerFlat => "LFM",
239            Self::LowerSharp => "LSM",
240        }
241    }
242
243    /// The chain reaching the mediant from a major triad, read left to
244    /// right; from a minor one it is read the other way.
245    fn chain(self) -> [Transform; 2] {
246        match self {
247            Self::UpperFlat => [Transform::P, Transform::R],
248            Self::UpperSharp => [Transform::L, Transform::P],
249            Self::LowerFlat => [Transform::P, Transform::L],
250            Self::LowerSharp => [Transform::R, Transform::P],
251        }
252    }
253}
254
255impl HexatonicSystem {
256    /// music21's lowercase name: `northern`, `eastern`, `southern` or
257    /// `western`.
258    pub fn name(self) -> &'static str {
259        match self {
260            Self::Northern => "northern",
261            Self::Eastern => "eastern",
262            Self::Southern => "southern",
263            Self::Western => "western",
264        }
265    }
266}
267
268/// Moves every pitch spelled as `changing` by `interval` and builds a new
269/// chord of the lot: music21's `_singlePitchTransform`.
270fn single_pitch_transform(chord: &Chord, interval: &Interval, changing: &Pitch) -> Result<Chord> {
271    let target = changing.name();
272    let mut pitches = chord.pitches();
273    for pitch in &mut pitches {
274        pitch.set_spelling_is_inferred(false);
275        if pitch.name() != target {
276            continue;
277        }
278        *pitch = pitch.transpose(interval)?;
279    }
280
281    with_duration_of(chord, Chord::new(pitches.as_slice())?)
282}
283
284/// `made` with `source`'s duration, as music21 copies `quarterLength` over.
285fn with_duration_of(source: &Chord, mut made: Chord) -> Result<Chord> {
286    if let Some(duration) = source.duration() {
287        made.set_duration(duration.clone());
288    }
289    Ok(made)
290}
291
292/// `chord`'s pitch names with their octaves, as music21 prints a chord.
293fn names_with_octave(chord: &Chord) -> Vec<String> {
294    chord
295        .pitches()
296        .iter()
297        .map(Pitch::name_with_octave)
298        .collect()
299}
300
301/// The refusal of a chord a chain cannot start from.
302fn not_a_triad_chain(chord: &Chord) -> Error {
303    Error::Analysis(format!(
304        "Cannot perform transformations on chord {}: not a major or minor triad",
305        names_with_octave(chord).join(" ")
306    ))
307}
308
309/// `chord` respelled with the best enharmonics, duration kept: music21's
310/// `_simplerEnharmonics`. `B# F- G` comes back as `C E G`.
311///
312/// # Errors
313///
314/// An empty chord, or a pitch base 40 has no place for.
315pub fn simpler_enharmonics(chord: &Chord) -> Result<Chord> {
316    let best = best_spelling(&chord.pitches(), EnharmonicRules::MELODIC)?;
317    with_duration_of(chord, Chord::new(best.as_slice())?)
318}
319
320/// Every chord a chain of transformations passes through, in order:
321/// music21's `LRP_combinations` with `eachOne=True`. With
322/// [`Respell::Simplify`] each is respelled on its own; the chain itself
323/// runs on the spellings the transformations give.
324///
325/// # Errors
326///
327/// A chord that is not a major or minor triad.
328pub fn lrp_chain(
329    chord: &Chord,
330    transforms: &[Transform],
331    order: ChainOrder,
332    respell: Respell,
333) -> Result<Vec<Chord>> {
334    if !chord.is_major_triad() && !chord.is_minor_triad() {
335        return Err(not_a_triad_chain(chord));
336    }
337
338    let mut transforms = transforms.to_vec();
339    if order == ChainOrder::RightToLeft {
340        transforms.reverse();
341    }
342
343    let mut chain: Vec<Chord> = Vec::with_capacity(transforms.len());
344    for transform in transforms {
345        let last = chain.last().unwrap_or(chord);
346        chain.push(transform.apply(last)?);
347    }
348
349    if respell == Respell::Keep {
350        return Ok(chain);
351    }
352    chain.iter().map(simpler_enharmonics).collect()
353}
354
355/// Where a chain of transformations ends: music21's `LRP_combinations`.
356/// No transformations at all leave `chord` as it was.
357///
358/// ```
359/// use music21_rs::{Chord, Pitch, analysis::neoriemannian::*};
360///
361/// let c_major = Chord::new("C4 E4 G4")?;
362/// let lp = Transform::parse_chain("LP")?;
363/// let ended = lrp_combination(&c_major, &lp, ChainOrder::LeftToRight, Respell::Keep)?;
364/// assert_eq!(ended.pitches().iter().map(Pitch::name_with_octave).collect::<Vec<_>>(), ["B3", "E4", "G#4"]);
365/// # Ok::<(), music21_rs::Error>(())
366/// ```
367///
368/// # Errors
369///
370/// A chord that is not a major or minor triad.
371pub fn lrp_combination(
372    chord: &Chord,
373    transforms: &[Transform],
374    order: ChainOrder,
375    respell: Respell,
376) -> Result<Chord> {
377    let chain = lrp_chain(chord, transforms, order, Respell::Keep)?;
378    let last = chain.last().unwrap_or(chord);
379
380    match respell {
381        Respell::Keep => Ok(last.clone()),
382        Respell::Simplify => simpler_enharmonics(last),
383    }
384}
385
386/// The six triads of `chord`'s hexatonic cycle, `PLPLPL`, ending on
387/// `chord` again or its enharmonic: music21's `completeHexatonic`. With
388/// [`Respell::Simplify`] each step is respelled before the next is taken.
389///
390/// # Errors
391///
392/// A chord that is not a major or minor triad.
393pub fn complete_hexatonic(chord: &Chord, respell: Respell) -> Result<Vec<Chord>> {
394    if chord.forte_class_tni().as_deref() != Some(TRIAD_FORTE_CLASS) {
395        return Err(Error::Analysis(
396            "Cannot perform transformations on this chord: not a major or minor triad".to_string(),
397        ));
398    }
399
400    let mut cycle: Vec<Chord> = Vec::with_capacity(HEXATONIC_CYCLE.len());
401    for transform in HEXATONIC_CYCLE {
402        let last = cycle.last().unwrap_or(chord);
403        let mut next = transform.apply(last)?;
404        if respell == Respell::Simplify {
405            next = simpler_enharmonics(&next)?;
406        }
407        cycle.push(next);
408    }
409    Ok(cycle)
410}
411
412/// The hexatonic system of `chord`'s root: music21's `hexatonicSystem`.
413/// Only the root's pitch class counts, so sevenths and diminished triads
414/// are classed too.
415///
416/// # Errors
417///
418/// A chord with no root.
419pub fn hexatonic_system(chord: &Chord) -> Result<HexatonicSystem> {
420    const SYSTEMS: [HexatonicSystem; 4] = [
421        HexatonicSystem::Northern,
422        HexatonicSystem::Eastern,
423        HexatonicSystem::Southern,
424        HexatonicSystem::Western,
425    ];
426
427    let root = chord
428        .root()
429        .ok_or_else(|| Error::Analysis("no pitches in chord".to_string()))?;
430
431    // Roots a major third apart share a system, so the system is the pitch
432    // class modulo four: C (0), E (4) and A- (8) are northern.
433    let pitch_class = root.pitch_class().number() as usize;
434    Ok(SYSTEMS[pitch_class % SYSTEMS.len()])
435}
436
437/// `chord`'s chromatic mediant, respelled: music21's `chromaticMediants`.
438///
439/// # Errors
440///
441/// A chord that is not a major or minor triad.
442pub fn chromatic_mediant(chord: &Chord, mediant: Mediant) -> Result<Chord> {
443    let order = if chord.is_minor_triad() {
444        ChainOrder::RightToLeft
445    } else if chord.is_major_triad() {
446        ChainOrder::LeftToRight
447    } else {
448        return Err(Error::Value("Chord must be major or minor".to_string()));
449    };
450
451    lrp_combination(chord, &mediant.chain(), order, Respell::Simplify)
452}
453
454/// `chord`'s disjunct mediant on `side`, respelled: music21's
455/// `disjunctMediants`.
456///
457/// # Errors
458///
459/// A chord that is not a major or minor triad.
460pub fn disjunct_mediant(chord: &Chord, side: MediantSide) -> Result<Chord> {
461    const PRP: [Transform; 3] = [Transform::P, Transform::R, Transform::P];
462    const PLP: [Transform; 3] = [Transform::P, Transform::L, Transform::P];
463
464    // Upper from major and lower from minor is PRP; the other two PLP.
465    let chain = match (chord.is_major_triad(), chord.is_minor_triad(), side) {
466        (true, _, MediantSide::Upper) | (_, true, MediantSide::Lower) => PRP,
467        (true, _, MediantSide::Lower) | (_, true, MediantSide::Upper) => PLP,
468        _ => return Err(Error::Value("Chord must be major or minor".to_string())),
469    };
470
471    lrp_combination(chord, &chain, ChainOrder::LeftToRight, Respell::Simplify)
472}
473
474/// Slide, `LPR`: the third held, major root a semitone below the minor.
475/// music21's `S`.
476///
477/// # Errors
478///
479/// A chord that is not a major or minor triad.
480pub fn slide(chord: &Chord) -> Result<Chord> {
481    lrp_combination(chord, &SLIDE, ChainOrder::LeftToRight, Respell::Keep)
482}
483
484/// Nebenverwandt, `RLP`: a minor triad to its major dominant, a major one
485/// to its minor subdominant. music21's `N`.
486///
487/// # Errors
488///
489/// A chord that is not a major or minor triad.
490pub fn nebenverwandt(chord: &Chord) -> Result<Chord> {
491    lrp_combination(
492        chord,
493        &NEBENVERWANDT,
494        ChainOrder::LeftToRight,
495        Respell::Keep,
496    )
497}
498
499/// The first of `transforms` taking `from` to `to`'s normal order:
500/// music21's `isNeoR`.
501///
502/// # Errors
503///
504/// `from` is not a major or minor triad.
505pub fn is_neo_r(from: &Chord, to: &Chord, transforms: &[Transform]) -> Result<Option<Transform>> {
506    let target = to.normal_order();
507    for transform in transforms {
508        if transform.apply(from)?.normal_order() == target {
509            return Ok(Some(*transform));
510        }
511    }
512    Ok(None)
513}
514
515/// The chromatic mediant taking `from` to `to`'s normal order: music21's
516/// `isChromaticMediant`.
517///
518/// # Errors
519///
520/// `from` is not a major or minor triad.
521pub fn is_chromatic_mediant(from: &Chord, to: &Chord) -> Result<Option<Mediant>> {
522    let target = to.normal_order();
523    for mediant in Mediant::ALL {
524        if chromatic_mediant(from, mediant)?.normal_order() == target {
525            return Ok(Some(mediant));
526        }
527    }
528    Ok(None)
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use crate::duration::Duration;
535
536    fn chord(names: &str) -> Chord {
537        Chord::new(names).unwrap()
538    }
539
540    fn named(chord: &Chord) -> Vec<String> {
541        names_with_octave(chord)
542    }
543
544    fn all_named(chords: &[Chord]) -> Vec<Vec<String>> {
545        chords.iter().map(named).collect()
546    }
547
548    fn chain(symbols: &str) -> Vec<Transform> {
549        Transform::parse_chain(symbols).unwrap()
550    }
551
552    #[test]
553    fn each_transform_moves_one_pitch() {
554        let c_major = chord("C4 E4 G4");
555        assert_eq!(
556            named(&Transform::L.apply(&c_major).unwrap()),
557            ["B3", "E4", "G4"]
558        );
559        assert_eq!(
560            named(&Transform::P.apply(&c_major).unwrap()),
561            ["C4", "Eb4", "G4"]
562        );
563        assert_eq!(
564            named(&Transform::R.apply(&c_major).unwrap()),
565            ["C4", "E4", "A4"]
566        );
567    }
568
569    #[test]
570    fn integer_chords_have_their_spelling_fixed() {
571        let c_minor = Chord::new([0, 3, 7].as_slice()).unwrap();
572        let l = Transform::L.apply(&c_minor).unwrap();
573        assert_eq!(named(&l), ["C", "Eb", "Ab"]);
574        assert!(l.pitches().iter().all(|p| !p.spelling_is_inferred()));
575    }
576
577    #[test]
578    fn a_non_triad_is_refused() {
579        let error = Transform::L.apply(&chord("C4 D4 E4")).unwrap_err();
580        assert!(error.to_string().contains("Cannot perform L on this chord"));
581        assert!(Transform::from_symbol('X').is_err());
582    }
583
584    #[test]
585    fn chains_run_in_either_order() {
586        let c_major = chord("C4 E4 G4");
587        let lp = chain("LP");
588        let forward = lrp_combination(&c_major, &lp, ChainOrder::LeftToRight, Respell::Keep);
589        let backward = lrp_combination(&c_major, &lp, ChainOrder::RightToLeft, Respell::Keep);
590        assert_eq!(named(&forward.unwrap()), ["B3", "E4", "G#4"]);
591        assert_eq!(named(&backward.unwrap()), ["C4", "Eb4", "Ab4"]);
592
593        let doubled = chord("C4 E4 G4 C5 E5");
594        let rlp = lrp_combination(
595            &doubled,
596            &chain("RLP"),
597            ChainOrder::LeftToRight,
598            Respell::Keep,
599        );
600        assert_eq!(named(&rlp.unwrap()), ["C4", "F4", "Ab4", "C5", "F5"]);
601    }
602
603    #[test]
604    fn a_hexatonic_chain_comes_back_respelled() {
605        let hexatonic = chain("LPLPLP");
606        let b_major = chord("B4 D#5 F#5");
607        let back = lrp_combination(
608            &b_major,
609            &hexatonic,
610            ChainOrder::RightToLeft,
611            Respell::Simplify,
612        );
613        assert_eq!(named(&back.unwrap()), ["B4", "D#5", "F#5"]);
614
615        let c_major = chord("C4 E4 G4");
616        let kept = lrp_combination(&c_major, &hexatonic, ChainOrder::RightToLeft, Respell::Keep);
617        assert_eq!(named(&kept.unwrap()), ["Dbb4", "Fb4", "Abb4"]);
618
619        let a_flat = chord("A-4 C4 E-5");
620        let sharp = lrp_combination(&a_flat, &hexatonic, ChainOrder::LeftToRight, Respell::Keep);
621        assert_eq!(named(&sharp.unwrap()), ["G#4", "B#3", "D#5"]);
622    }
623
624    #[test]
625    fn every_step_of_a_chain_can_be_kept() {
626        let steps = lrp_chain(
627            &chord("C4 E4 G4"),
628            &chain("LPLPLP"),
629            ChainOrder::LeftToRight,
630            Respell::Simplify,
631        );
632        assert_eq!(
633            all_named(&steps.unwrap()),
634            [
635                ["B3", "E4", "G4"],
636                ["B3", "E4", "G#4"],
637                ["B3", "D#4", "G#4"],
638                ["C4", "Eb4", "Ab4"],
639                ["C4", "Eb4", "G4"],
640                ["C4", "E4", "G4"],
641            ]
642        );
643    }
644
645    #[test]
646    fn the_hexatonic_cycle_is_music21s() {
647        let c_major = chord("C4 E4 G4");
648        assert_eq!(
649            all_named(&complete_hexatonic(&c_major, Respell::Keep).unwrap()),
650            [
651                ["C4", "Eb4", "G4"],
652                ["C4", "Eb4", "Ab4"],
653                ["Cb4", "Eb4", "Ab4"],
654                ["Cb4", "Fb4", "Ab4"],
655                ["Cb4", "Fb4", "Abb4"],
656                ["Dbb4", "Fb4", "Abb4"],
657            ]
658        );
659        assert_eq!(
660            all_named(&complete_hexatonic(&c_major, Respell::Simplify).unwrap()),
661            [
662                ["C4", "Eb4", "G4"],
663                ["C4", "Eb4", "Ab4"],
664                ["B3", "D#4", "G#4"],
665                ["B3", "E4", "G#4"],
666                ["B3", "E4", "G4"],
667                ["C4", "E4", "G4"],
668            ]
669        );
670        assert!(complete_hexatonic(&chord("C4 D4 E4"), Respell::Keep).is_err());
671    }
672
673    #[test]
674    fn hexatonic_systems_follow_the_root() {
675        assert_eq!(
676            hexatonic_system(&chord("C E G")).unwrap(),
677            HexatonicSystem::Northern
678        );
679        assert_eq!(
680            hexatonic_system(&chord("G B- D")).unwrap(),
681            HexatonicSystem::Western
682        );
683        assert_eq!(
684            hexatonic_system(&chord("F#4 D5 A5 C6")).unwrap(),
685            HexatonicSystem::Southern
686        );
687        assert!(hexatonic_system(&Chord::empty()).is_err());
688    }
689
690    #[test]
691    fn mediants_are_music21s() {
692        let c_major = chord("C5 E5 G5");
693        let normal = |mediant| chromatic_mediant(&c_major, mediant).unwrap().normal_order();
694        assert_eq!(normal(Mediant::UpperFlat), [3, 7, 10]);
695        assert_eq!(normal(Mediant::UpperSharp), [4, 8, 11]);
696        assert_eq!(
697            named(&chromatic_mediant(&c_major, Mediant::LowerFlat).unwrap()),
698            ["C5", "Eb5", "Ab5"]
699        );
700        assert_eq!(
701            named(&chromatic_mediant(&c_major, Mediant::LowerSharp).unwrap()),
702            ["C#5", "E5", "A5"]
703        );
704        assert_eq!(
705            named(&chromatic_mediant(&c_major, Mediant::UpperSharp).unwrap()),
706            ["B4", "E5", "G#5"]
707        );
708
709        let names = |c: Chord| -> Vec<String> { c.pitches().iter().map(Pitch::name).collect() };
710        assert_eq!(
711            names(disjunct_mediant(&c_major, MediantSide::Upper).unwrap()),
712            ["Bb", "Eb", "Gb"]
713        );
714        assert_eq!(
715            names(disjunct_mediant(&c_major, MediantSide::Lower).unwrap()),
716            ["B", "D#", "G#"]
717        );
718        assert_eq!(names(slide(&c_major).unwrap()), ["C#", "E", "G#"]);
719        assert_eq!(names(slide(&chord("A4 C5 E5")).unwrap()), ["Ab", "C", "Eb"]);
720        assert_eq!(names(nebenverwandt(&c_major).unwrap()), ["C", "F", "Ab"]);
721        assert_eq!(
722            names(nebenverwandt(&chord("A4 C5 E5")).unwrap()),
723            ["G#", "B", "E"]
724        );
725
726        assert_eq!(Mediant::from_code("LFM").unwrap(), Mediant::LowerFlat);
727        assert!(Mediant::from_code("XYZ").is_err());
728    }
729
730    #[test]
731    fn relations_are_recognised() {
732        let c_major = chord("C4 E4 G4");
733        let lrp = chain("LRP");
734        assert_eq!(
735            is_neo_r(&c_major, &chord("B3 E4 G4"), &lrp).unwrap(),
736            Some(Transform::L)
737        );
738        assert_eq!(
739            is_neo_r(&c_major, &chord("C4 E-4 G4"), &lrp).unwrap(),
740            Some(Transform::P)
741        );
742        assert_eq!(
743            is_neo_r(&c_major, &chord("C4 E4 A4"), &lrp).unwrap(),
744            Some(Transform::R)
745        );
746        assert_eq!(
747            is_neo_r(&c_major, &chord("C-4 E-4 A-4"), &lrp).unwrap(),
748            None
749        );
750        assert_eq!(
751            is_neo_r(&c_major, &chord("C4 E-4 G4"), &chain("LR")).unwrap(),
752            None
753        );
754
755        assert_eq!(
756            is_chromatic_mediant(&c_major, &chord("A-3 C4 E-4")).unwrap(),
757            Some(Mediant::LowerFlat)
758        );
759        assert_eq!(
760            is_chromatic_mediant(&c_major, &chord("C-4 E-4 A-4")).unwrap(),
761            None
762        );
763    }
764
765    #[test]
766    fn minor_triads_take_their_mediants_as_music21_does() {
767        let a_minor = chord("A4 C5 E5");
768        let mediant = |mediant| named(&chromatic_mediant(&a_minor, mediant).unwrap());
769        assert_eq!(mediant(Mediant::UpperFlat), ["G4", "C5", "Eb5"]);
770        assert_eq!(mediant(Mediant::UpperSharp), ["G#4", "C#5", "E5"]);
771        assert_eq!(mediant(Mediant::LowerFlat), ["Ab4", "C5", "F5"]);
772        assert_eq!(mediant(Mediant::LowerSharp), ["A4", "C#5", "F#5"]);
773
774        let side = |side| named(&disjunct_mediant(&a_minor, side).unwrap());
775        assert_eq!(side(MediantSide::Upper), ["Ab4", "Db5", "F5"]);
776        assert_eq!(side(MediantSide::Lower), ["A#4", "C#5", "F#5"]);
777    }
778
779    #[test]
780    fn only_triads_take_mediants_or_chains() {
781        let cluster = chord("C4 D4 E4");
782        assert!(matches!(
783            chromatic_mediant(&cluster, Mediant::UpperFlat),
784            Err(Error::Value(_))
785        ));
786        assert!(matches!(
787            disjunct_mediant(&cluster, MediantSide::Upper),
788            Err(Error::Value(_))
789        ));
790
791        let chain = lrp_chain(
792            &cluster,
793            &[Transform::L],
794            ChainOrder::LeftToRight,
795            Respell::Keep,
796        );
797        let message = chain.unwrap_err().to_string();
798        assert!(message.contains("Cannot perform transformations on chord C4 D4 E4"));
799    }
800
801    #[test]
802    fn p_and_r_refuse_in_music21s_capitals() {
803        let cluster = chord("C4 D4 E4");
804        for transform in [Transform::P, Transform::R] {
805            let message = transform.apply(&cluster).unwrap_err().to_string();
806            let expected = format!(
807                "Cannot perform {} on this chord: not a Major or Minor triad",
808                transform.symbol()
809            );
810            assert!(message.contains(&expected), "{message}");
811        }
812    }
813
814    #[test]
815    fn a_chain_parses_back_to_its_letters() {
816        let symbols: String = chain("LPR").into_iter().map(Transform::symbol).collect();
817        assert_eq!(symbols, "LPR");
818    }
819
820    #[test]
821    fn a_result_lasts_as_long_as_its_source() {
822        let mut c_major = chord("C4 E4 G4");
823        c_major.set_duration(Duration::half());
824        let lasts = |chord: &Chord| chord.duration().map(Duration::quarter_length);
825
826        assert_eq!(lasts(&Transform::L.apply(&c_major).unwrap()), Some(2.0));
827        let cycle = complete_hexatonic(&c_major, Respell::Simplify).unwrap();
828        assert_eq!(lasts(&cycle[5]), Some(2.0));
829    }
830
831    #[test]
832    fn hexatonic_systems_have_music21s_names() {
833        let names: Vec<&str> = [
834            HexatonicSystem::Northern,
835            HexatonicSystem::Eastern,
836            HexatonicSystem::Southern,
837            HexatonicSystem::Western,
838        ]
839        .into_iter()
840        .map(HexatonicSystem::name)
841        .collect();
842        assert_eq!(names, ["northern", "eastern", "southern", "western"]);
843    }
844
845    #[test]
846    fn simpler_enharmonics_respell() {
847        assert_eq!(
848            named(&simpler_enharmonics(&chord("B# F- G")).unwrap()),
849            ["C", "E", "G"]
850        );
851    }
852}