Skip to main content

klib/core/
mode.rs

1//! A module for working with modes.
2
3use std::fmt::{Display, Error, Formatter};
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use pest::Parser;
9
10use crate::core::{
11    base::{HasDescription, HasName, HasPreciseName, HasStaticName, Parsable, Res},
12    chord::HasRoot,
13    interval::{HasIntervals, Interval},
14    mode_kind::ModeKind,
15    note::Note,
16    parser::{mode_name_str_to_mode_kind, note_str_to_note, ChordParser, Rule},
17};
18
19// Traits.
20
21/// A trait that represents a type that has a mode kind.
22pub trait HasModeKind {
23    /// Returns the mode kind of the implementor (most likely a [`Mode`]).
24    fn kind(&self) -> ModeKind;
25}
26
27// Struct.
28
29/// A mode with a root note.
30///
31/// This combines a root note with a mode kind to produce an actual mode
32/// with specific notes.
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34#[derive(PartialEq, Eq, Copy, Clone, Debug)]
35pub struct Mode {
36    /// The root note of the mode.
37    root: Note,
38    /// The kind of mode.
39    kind: ModeKind,
40}
41
42// Impls.
43
44impl Mode {
45    /// Creates a new mode with the given root note and mode kind.
46    pub fn new(root: Note, kind: ModeKind) -> Self {
47        Self { root, kind }
48    }
49
50    /// Returns the intervals of this mode (delegates to the mode kind).
51    pub fn intervals(&self) -> &'static [Interval] {
52        self.kind.intervals()
53    }
54
55    /// Returns the notes of this mode (root + each interval).
56    pub fn notes(&self) -> Vec<Note> {
57        self.intervals().iter().map(|&interval| self.root + interval).collect()
58    }
59}
60
61impl HasRoot for Mode {
62    fn root(&self) -> Note {
63        self.root
64    }
65}
66
67impl HasModeKind for Mode {
68    fn kind(&self) -> ModeKind {
69        self.kind
70    }
71}
72
73impl HasIntervals for Mode {
74    fn intervals(&self) -> &'static [Interval] {
75        self.kind.intervals()
76    }
77}
78
79impl HasStaticName for Mode {
80    fn static_name(&self) -> &'static str {
81        self.kind.static_name()
82    }
83}
84
85impl HasName for Mode {
86    fn name(&self) -> String {
87        format!("{} {}", self.root.static_name(), self.kind.static_name())
88    }
89}
90
91impl HasPreciseName for Mode {
92    fn precise_name(&self) -> String {
93        format!("{} {}", self.root.name(), self.kind.static_name())
94    }
95}
96
97impl HasDescription for Mode {
98    fn description(&self) -> &'static str {
99        self.kind.description()
100    }
101}
102
103impl Display for Mode {
104    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
105        let notes = self.notes().iter().map(|n| n.static_name()).collect::<Vec<_>>().join(", ");
106        write!(f, "{}\n   {}\n   {}", self.name(), self.description(), notes)
107    }
108}
109
110impl Parsable for Mode {
111    fn parse(input: &str) -> Res<Self>
112    where
113        Self: Sized,
114    {
115        let pairs = ChordParser::parse(Rule::mode, input)?;
116        let root = pairs.clone().next().unwrap();
117
118        assert_eq!(Rule::mode, root.as_rule());
119
120        let mut components = root.into_inner();
121
122        let note = components.next().unwrap();
123        assert_eq!(Rule::note_atomic, note.as_rule());
124        let root_note = note_str_to_note(note.as_str().trim())?;
125
126        let mode_name = components.next().unwrap();
127        assert_eq!(Rule::mode_name, mode_name.as_rule());
128        let mode_kind = mode_name_str_to_mode_kind(mode_name.as_str())?;
129
130        Ok(Mode::new(root_note, mode_kind))
131    }
132}
133
134// Tests.
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::core::named_pitch::{HasLetter, HasNamedPitch};
140    use crate::core::note::*;
141    use pretty_assertions::assert_eq;
142
143    impl Mode {
144        /// Validates that the mode has correct enharmonic spelling.
145        ///
146        /// For 7-note modes (most modes), each letter A-G should appear exactly once.
147        /// For other modes, no letter should repeat unless it's a chromatic/octatonic exception.
148        pub(crate) fn validate_spelling(&self) -> Result<(), String> {
149            use crate::core::named_pitch::{HasLetter, HasNamedPitch};
150            use std::collections::HashMap;
151
152            let notes = self.notes();
153            let intervals_count = self.intervals().len();
154
155            // For chromatic scale (12 notes), we allow letter repeats
156            if intervals_count == 12 {
157                return Ok(());
158            }
159
160            // Check for letter uniqueness
161            let mut letter_counts: HashMap<&str, usize> = HashMap::new();
162            for note in &notes {
163                let letter = note.named_pitch().letter();
164                *letter_counts.entry(letter).or_insert(0) += 1;
165            }
166
167            // For 7-note collections, we expect exactly one of each letter
168            if intervals_count == 7 {
169                if letter_counts.len() != 7 {
170                    return Err(format!(
171                        "{} {} has {} unique letters, expected 7. Letters: {:?}",
172                        self.root().static_name(),
173                        self.kind().static_name(),
174                        letter_counts.len(),
175                        notes.iter().map(|n| n.static_name()).collect::<Vec<_>>()
176                    ));
177                }
178
179                for (letter, count) in &letter_counts {
180                    if *count != 1 {
181                        return Err(format!(
182                            "{} {} has letter {} appearing {} times, expected 1. Notes: {:?}",
183                            self.root().static_name(),
184                            self.kind().static_name(),
185                            letter,
186                            count,
187                            notes.iter().map(|n| n.static_name()).collect::<Vec<_>>()
188                        ));
189                    }
190                }
191            } else {
192                // For non-7-note collections (pentatonic, etc.), just check no duplicates
193                for (letter, count) in &letter_counts {
194                    if *count > 1 {
195                        return Err(format!(
196                            "{} {} has letter {} appearing {} times. Notes: {:?}",
197                            self.root().static_name(),
198                            self.kind().static_name(),
199                            letter,
200                            count,
201                            notes.iter().map(|n| n.static_name()).collect::<Vec<_>>()
202                        ));
203                    }
204                }
205            }
206
207            Ok(())
208        }
209    }
210
211    #[test]
212    fn test_mode_creation() {
213        let mode = Mode::new(D, ModeKind::Dorian);
214        assert_eq!(mode.root(), D);
215        assert_eq!(mode.kind(), ModeKind::Dorian);
216    }
217
218    #[test]
219    fn test_mode_intervals() {
220        let mode = Mode::new(D, ModeKind::Dorian);
221        assert_eq!(mode.intervals().len(), 7);
222        assert_eq!(
223            mode.intervals(),
224            &[
225                Interval::PerfectUnison,
226                Interval::MajorSecond,
227                Interval::MinorThird,
228                Interval::PerfectFourth,
229                Interval::PerfectFifth,
230                Interval::MajorSixth,
231                Interval::MinorSeventh,
232            ]
233        );
234    }
235
236    #[test]
237    fn test_mode_notes() {
238        // D Dorian
239        let mode = Mode::new(D, ModeKind::Dorian);
240        assert_eq!(mode.notes(), vec![D, E, F, G, A, B, CFive]);
241
242        // C Ionian (same as C major)
243        let mode = Mode::new(C, ModeKind::Ionian);
244        assert_eq!(mode.notes(), vec![C, D, E, F, G, A, B]);
245
246        // E Phrygian
247        let mode = Mode::new(E, ModeKind::Phrygian);
248        assert_eq!(mode.notes(), vec![E, F, G, A, B, CFive, DFive]);
249
250        // F Lydian
251        let mode = Mode::new(F, ModeKind::Lydian);
252        assert_eq!(mode.notes(), vec![F, G, A, B, CFive, DFive, EFive]);
253
254        // G Mixolydian
255        let mode = Mode::new(G, ModeKind::Mixolydian);
256        assert_eq!(mode.notes(), vec![G, A, B, CFive, DFive, EFive, FFive]);
257
258        // A Aeolian (natural minor)
259        let mode = Mode::new(A, ModeKind::Aeolian);
260        assert_eq!(mode.notes(), vec![A, B, CFive, DFive, EFive, FFive, GFive]);
261
262        // B Locrian
263        let mode = Mode::new(B, ModeKind::Locrian);
264        assert_eq!(mode.notes(), vec![B, CFive, DFive, EFive, FFive, GFive, AFive]);
265    }
266
267    #[test]
268    fn test_mode_names() {
269        let mode = Mode::new(D, ModeKind::Dorian);
270        assert_eq!(mode.name(), "D dorian");
271        assert_eq!(mode.static_name(), "dorian");
272
273        let mode = Mode::new(FSharp, ModeKind::Lydian);
274        assert_eq!(mode.name(), "F♯ lydian");
275
276        let mode = Mode::new(BFlat, ModeKind::Mixolydian);
277        assert_eq!(mode.name(), "B♭ mixolydian");
278    }
279
280    #[test]
281    fn test_mode_display() {
282        let mode = Mode::new(D, ModeKind::Dorian);
283        let display = format!("{}", mode);
284        assert!(display.contains("D dorian"));
285        assert!(display.contains("D, E, F, G, A, B, C"));
286        assert!(display.contains("dorian"));
287    }
288
289    #[test]
290    fn test_all_modes_of_c_major() {
291        // All modes of C major scale should contain the same note classes (C, D, E, F, G, A, B)
292        // but starting from different degrees. Notes may be in different octaves.
293
294        let c_ionian = Mode::new(C, ModeKind::Ionian);
295        assert_eq!(c_ionian.notes(), vec![C, D, E, F, G, A, B]);
296
297        let d_dorian = Mode::new(D, ModeKind::Dorian);
298        assert_eq!(d_dorian.notes(), vec![D, E, F, G, A, B, CFive]);
299
300        let e_phrygian = Mode::new(E, ModeKind::Phrygian);
301        assert_eq!(e_phrygian.notes(), vec![E, F, G, A, B, CFive, DFive]);
302
303        let f_lydian = Mode::new(F, ModeKind::Lydian);
304        assert_eq!(f_lydian.notes(), vec![F, G, A, B, CFive, DFive, EFive]);
305
306        let g_mixolydian = Mode::new(G, ModeKind::Mixolydian);
307        assert_eq!(g_mixolydian.notes(), vec![G, A, B, CFive, DFive, EFive, FFive]);
308
309        let a_aeolian = Mode::new(A, ModeKind::Aeolian);
310        assert_eq!(a_aeolian.notes(), vec![A, B, CFive, DFive, EFive, FFive, GFive]);
311
312        let b_locrian = Mode::new(B, ModeKind::Locrian);
313        assert_eq!(b_locrian.notes(), vec![B, CFive, DFive, EFive, FFive, GFive, AFive]);
314    }
315
316    #[test]
317    fn test_mode_characteristic_intervals() {
318        // D Dorian characteristic: major 6th (B) in minor context
319        let mode = Mode::new(D, ModeKind::Dorian);
320        let notes = mode.notes();
321        assert_eq!(notes[5], B); // 6th degree is major 6th
322
323        // E Phrygian characteristic: minor 2nd (F)
324        let mode = Mode::new(E, ModeKind::Phrygian);
325        let notes = mode.notes();
326        assert_eq!(notes[1], F); // 2nd degree is minor 2nd
327
328        // F Lydian characteristic: augmented 4th (B)
329        let mode = Mode::new(F, ModeKind::Lydian);
330        let notes = mode.notes();
331        assert_eq!(notes[3], B); // 4th degree is augmented 4th
332
333        // B Locrian characteristic: diminished 5th (F)
334        let mode = Mode::new(B, ModeKind::Locrian);
335        let notes = mode.notes();
336        assert_eq!(notes[4], FFive); // 5th degree is diminished 5th
337    }
338
339    #[test]
340    fn test_mode_parse() {
341        // Test parsing various modes
342        let mode = Mode::parse("D dorian").unwrap();
343        assert_eq!(mode.root(), D);
344        assert_eq!(mode.kind(), ModeKind::Dorian);
345
346        let mode = Mode::parse("C ionian").unwrap();
347        assert_eq!(mode.root(), C);
348        assert_eq!(mode.kind(), ModeKind::Ionian);
349
350        let mode = Mode::parse("E phrygian").unwrap();
351        assert_eq!(mode.root(), E);
352        assert_eq!(mode.kind(), ModeKind::Phrygian);
353
354        let mode = Mode::parse("F lydian").unwrap();
355        assert_eq!(mode.root(), F);
356        assert_eq!(mode.kind(), ModeKind::Lydian);
357
358        let mode = Mode::parse("G mixolydian").unwrap();
359        assert_eq!(mode.root(), G);
360        assert_eq!(mode.kind(), ModeKind::Mixolydian);
361
362        let mode = Mode::parse("A aeolian").unwrap();
363        assert_eq!(mode.root(), A);
364        assert_eq!(mode.kind(), ModeKind::Aeolian);
365
366        let mode = Mode::parse("B locrian").unwrap();
367        assert_eq!(mode.root(), B);
368        assert_eq!(mode.kind(), ModeKind::Locrian);
369
370        // Test with accidentals
371        let mode = Mode::parse("F# dorian").unwrap();
372        assert_eq!(mode.root(), FSharp);
373        assert_eq!(mode.kind(), ModeKind::Dorian);
374
375        let mode = Mode::parse("Bb lydian").unwrap();
376        assert_eq!(mode.root(), BFlat);
377        assert_eq!(mode.kind(), ModeKind::Lydian);
378    }
379
380    #[test]
381    fn test_harmonic_minor_modes_parse() {
382        // Test harmonic minor modes
383        let mode = Mode::parse("B locrian nat6").unwrap();
384        assert_eq!(mode.kind(), ModeKind::LocrianNatural6);
385
386        let mode = Mode::parse("C ionian #5").unwrap();
387        assert_eq!(mode.kind(), ModeKind::IonianSharp5);
388
389        let mode = Mode::parse("D dorian sharp 4").unwrap();
390        assert_eq!(mode.kind(), ModeKind::DorianSharp4);
391
392        let mode = Mode::parse("E phrygian dominant").unwrap();
393        assert_eq!(mode.kind(), ModeKind::PhrygianDominant);
394
395        let mode = Mode::parse("F lydian #2").unwrap();
396        assert_eq!(mode.kind(), ModeKind::LydianSharp2);
397
398        let mode = Mode::parse("G# ultralocrian").unwrap();
399        assert_eq!(mode.kind(), ModeKind::Ultralocrian);
400    }
401
402    #[test]
403    fn test_melodic_minor_modes_parse() {
404        // Test melodic minor modes
405        let mode = Mode::parse("B dorian b2").unwrap();
406        assert_eq!(mode.kind(), ModeKind::DorianFlat2);
407
408        let mode = Mode::parse("C lydian augmented").unwrap();
409        assert_eq!(mode.kind(), ModeKind::LydianAugmented);
410
411        let mode = Mode::parse("D lydian dominant").unwrap();
412        assert_eq!(mode.kind(), ModeKind::LydianDominant);
413
414        let mode = Mode::parse("E mixolydian b6").unwrap();
415        assert_eq!(mode.kind(), ModeKind::MixolydianFlat6);
416
417        let mode = Mode::parse("F# locrian nat2").unwrap();
418        assert_eq!(mode.kind(), ModeKind::LocrianNatural2);
419
420        let mode = Mode::parse("G# altered").unwrap();
421        assert_eq!(mode.kind(), ModeKind::Altered);
422    }
423
424    #[test]
425    fn test_enharmonic_spelling_diatonic_modes() {
426        // Test all diatonic modes with various root notes to ensure correct enharmonic spelling
427        // Each 7-note mode should use each letter A-G exactly once
428
429        // C Ionian - all natural notes
430        let mode = Mode::new(C, ModeKind::Ionian);
431        mode.validate_spelling().unwrap();
432
433        // F# Dorian - should use sharps, not flats
434        let mode = Mode::new(FSharp, ModeKind::Dorian);
435        mode.validate_spelling().unwrap();
436        let notes = mode.notes();
437        // F# Dorian: F# G# A B C# D E#
438        assert_eq!(notes[0].named_pitch().letter(), "F");
439        assert_eq!(notes[1].named_pitch().letter(), "G");
440        assert_eq!(notes[2].named_pitch().letter(), "A");
441        assert_eq!(notes[3].named_pitch().letter(), "B");
442        assert_eq!(notes[4].named_pitch().letter(), "C");
443        assert_eq!(notes[5].named_pitch().letter(), "D");
444        assert_eq!(notes[6].named_pitch().letter(), "E");
445
446        // Db Lydian - should use flats
447        let mode = Mode::new(DFlat, ModeKind::Lydian);
448        mode.validate_spelling().unwrap();
449        let notes = mode.notes();
450        // Db Lydian: Db Eb F G Ab Bb C
451        assert_eq!(notes[0].named_pitch().letter(), "D");
452        assert_eq!(notes[1].named_pitch().letter(), "E");
453        assert_eq!(notes[2].named_pitch().letter(), "F");
454        assert_eq!(notes[3].named_pitch().letter(), "G");
455        assert_eq!(notes[4].named_pitch().letter(), "A");
456        assert_eq!(notes[5].named_pitch().letter(), "B");
457        assert_eq!(notes[6].named_pitch().letter(), "C");
458
459        // Bb Mixolydian
460        let mode = Mode::new(BFlat, ModeKind::Mixolydian);
461        mode.validate_spelling().unwrap();
462
463        // E Locrian
464        let mode = Mode::new(E, ModeKind::Locrian);
465        mode.validate_spelling().unwrap();
466    }
467
468    #[test]
469    fn test_enharmonic_spelling_harmonic_minor_modes() {
470        // Test harmonic minor modes
471
472        // F# Locrian Natural 6 - should spell with F# G A B C# D E#
473        let mode = Mode::new(FSharp, ModeKind::LocrianNatural6);
474        mode.validate_spelling().unwrap();
475
476        // C Ionian #5
477        let mode = Mode::new(C, ModeKind::IonianSharp5);
478        mode.validate_spelling().unwrap();
479
480        // D Dorian #4
481        let mode = Mode::new(D, ModeKind::DorianSharp4);
482        mode.validate_spelling().unwrap();
483
484        // E Phrygian Dominant
485        let mode = Mode::new(E, ModeKind::PhrygianDominant);
486        mode.validate_spelling().unwrap();
487
488        // F Lydian #2
489        let mode = Mode::new(F, ModeKind::LydianSharp2);
490        mode.validate_spelling().unwrap();
491
492        // G# Ultralocrian
493        let mode = Mode::new(GSharp, ModeKind::Ultralocrian);
494        mode.validate_spelling().unwrap();
495    }
496
497    #[test]
498    fn test_enharmonic_spelling_melodic_minor_modes() {
499        // Test melodic minor modes
500
501        // B Dorian b2
502        let mode = Mode::new(B, ModeKind::DorianFlat2);
503        mode.validate_spelling().unwrap();
504
505        // C Lydian Augmented
506        let mode = Mode::new(C, ModeKind::LydianAugmented);
507        mode.validate_spelling().unwrap();
508
509        // D Lydian Dominant
510        let mode = Mode::new(D, ModeKind::LydianDominant);
511        mode.validate_spelling().unwrap();
512
513        // E Mixolydian b6
514        let mode = Mode::new(E, ModeKind::MixolydianFlat6);
515        mode.validate_spelling().unwrap();
516
517        // F# Locrian natural 2
518        let mode = Mode::new(FSharp, ModeKind::LocrianNatural2);
519        mode.validate_spelling().unwrap();
520
521        // G# Altered
522        let mode = Mode::new(GSharp, ModeKind::Altered);
523        mode.validate_spelling().unwrap();
524    }
525
526    #[test]
527    fn test_enharmonic_spelling_all_roots() {
528        // Test a few modes with all 12 root notes to ensure consistency
529        for root in [C, CSharp, D, DFlat, DSharp, E, EFlat, F, FSharp, G, GFlat, GSharp, A, AFlat, ASharp, B, BFlat] {
530            // Ionian (Major)
531            let mode = Mode::new(root, ModeKind::Ionian);
532            mode.validate_spelling().unwrap_or_else(|e| panic!("Ionian spelling failed for {}: {}", root.static_name(), e));
533
534            // Dorian
535            let mode = Mode::new(root, ModeKind::Dorian);
536            mode.validate_spelling().unwrap_or_else(|e| panic!("Dorian spelling failed for {}: {}", root.static_name(), e));
537
538            // Lydian
539            let mode = Mode::new(root, ModeKind::Lydian);
540            mode.validate_spelling().unwrap_or_else(|e| panic!("Lydian spelling failed for {}: {}", root.static_name(), e));
541        }
542    }
543
544    #[test]
545    fn test_mode_spelling() {
546        let mode = Mode::new(E, ModeKind::PhrygianDominant);
547        assert_eq!(mode.notes(), vec![E, F, GSharp, A, B, CFive, DFive], "E phrygian dominant spelling incorrect");
548        mode.validate_spelling().unwrap();
549
550        let mode = Mode::new(B, ModeKind::LocrianNatural6);
551        assert_eq!(mode.notes(), vec![B, CFive, DFive, EFive, FFive, GSharpFive, AFive], "B locrian nat6 spelling incorrect");
552        mode.validate_spelling().unwrap();
553
554        let mode = Mode::new(D, ModeKind::LydianDominant);
555        assert_eq!(mode.notes(), vec![D, E, FSharp, GSharp, A, B, CFive], "D lydian dominant spelling incorrect");
556        mode.validate_spelling().unwrap();
557
558        assert_eq!(
559            mode.intervals(),
560            &[
561                Interval::PerfectUnison,
562                Interval::MajorSecond,
563                Interval::MajorThird,
564                Interval::AugmentedFourth,
565                Interval::PerfectFifth,
566                Interval::MajorSixth,
567                Interval::MinorSeventh,
568            ]
569        );
570    }
571}