Skip to main content

klib/core/
notation.rs

1//! A module for working with a unified notation type that can represent a chord, scale, or mode.
2
3use std::fmt::{Display, Error, Formatter};
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use crate::core::{
9    base::{HasDescription, HasName, HasPreciseName, Parsable, Res},
10    chord::{Chord, HasChord, HasRoot},
11    interval::{HasIntervals, Interval},
12    mode::Mode,
13    note::Note,
14    scale::Scale,
15};
16
17// Enum.
18
19/// A unified notation type that can represent a chord, scale, or mode.
20///
21/// This type attempts to parse input in the following order:
22/// 1. Scale (most distinctive keywords like "major pentatonic", "harmonic minor")
23/// 2. Mode (keywords like "dorian", "lydian")
24/// 3. Chord (fallback, most flexible grammar)
25///
26/// # Examples
27///
28/// ```rust
29/// use klib::core::base::Parsable;
30/// use klib::core::notation::Notation;
31///
32/// // Parses as a scale
33/// let notation = Notation::parse("C major pentatonic").unwrap();
34/// assert!(notation.is_scale());
35///
36/// // Parses as a mode
37/// let notation = Notation::parse("D dorian").unwrap();
38/// assert!(notation.is_mode());
39///
40/// // Parses as a chord
41/// let notation = Notation::parse("Cmaj7").unwrap();
42/// assert!(notation.is_chord());
43/// ```
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45#[derive(PartialEq, Eq, Clone, Debug)]
46pub enum Notation {
47    /// A chord notation.
48    Chord(Chord),
49    /// A scale notation.
50    Scale(Scale),
51    /// A mode notation.
52    Mode(Mode),
53}
54
55// Impls.
56
57impl Notation {
58    /// Returns `true` if this notation is a chord.
59    pub fn is_chord(&self) -> bool {
60        matches!(self, Notation::Chord(_))
61    }
62
63    /// Returns `true` if this notation is a scale.
64    pub fn is_scale(&self) -> bool {
65        matches!(self, Notation::Scale(_))
66    }
67
68    /// Returns `true` if this notation is a mode.
69    pub fn is_mode(&self) -> bool {
70        matches!(self, Notation::Mode(_))
71    }
72
73    /// Returns a reference to the inner chord, if this is a chord.
74    pub fn as_chord(&self) -> Option<&Chord> {
75        match self {
76            Notation::Chord(c) => Some(c),
77            _ => None,
78        }
79    }
80
81    /// Returns a reference to the inner scale, if this is a scale.
82    pub fn as_scale(&self) -> Option<&Scale> {
83        match self {
84            Notation::Scale(s) => Some(s),
85            _ => None,
86        }
87    }
88
89    /// Returns a reference to the inner mode, if this is a mode.
90    pub fn as_mode(&self) -> Option<&Mode> {
91        match self {
92            Notation::Mode(m) => Some(m),
93            _ => None,
94        }
95    }
96
97    /// Consumes the notation and returns the inner chord, if this is a chord.
98    pub fn into_chord(self) -> Option<Chord> {
99        match self {
100            Notation::Chord(c) => Some(c),
101            _ => None,
102        }
103    }
104
105    /// Consumes the notation and returns the inner scale, if this is a scale.
106    pub fn into_scale(self) -> Option<Scale> {
107        match self {
108            Notation::Scale(s) => Some(s),
109            _ => None,
110        }
111    }
112
113    /// Consumes the notation and returns the inner mode, if this is a mode.
114    pub fn into_mode(self) -> Option<Mode> {
115        match self {
116            Notation::Mode(m) => Some(m),
117            _ => None,
118        }
119    }
120
121    /// Returns the notes of this notation.
122    ///
123    /// For chords, this returns the chord voicing.
124    /// For scales and modes, this returns the scale/mode degrees.
125    pub fn notes(&self) -> Vec<Note> {
126        match self {
127            Notation::Chord(c) => c.chord(),
128            Notation::Scale(s) => s.notes(),
129            Notation::Mode(m) => m.notes(),
130        }
131    }
132
133    /// Returns a static string describing what kind of notation this is.
134    pub fn kind(&self) -> &'static str {
135        match self {
136            Notation::Chord(_) => "chord",
137            Notation::Scale(_) => "scale",
138            Notation::Mode(_) => "mode",
139        }
140    }
141
142    /// Parses a symbol into a Notation, optionally forcing a specific type.
143    ///
144    /// If `notation_type` is `Some`, it must be one of `"chord"`, `"scale"`, or `"mode"`.
145    /// If `None`, auto-detection is used (scale → mode → chord priority).
146    ///
147    /// # Examples
148    ///
149    /// ```rust
150    /// use klib::core::notation::Notation;
151    ///
152    /// // Auto-detect
153    /// let notation = Notation::parse_with_type("D dorian", None).unwrap();
154    /// assert!(notation.is_mode());
155    ///
156    /// // Force chord interpretation
157    /// let notation = Notation::parse_with_type("C", Some("chord")).unwrap();
158    /// assert!(notation.is_chord());
159    /// ```
160    pub fn parse_with_type(symbol: &str, notation_type: Option<&str>) -> Res<Self> {
161        match notation_type {
162            Some("chord") => Ok(Notation::Chord(Chord::parse(symbol)?)),
163            Some("scale") => Ok(Notation::Scale(Scale::parse(symbol)?)),
164            Some("mode") => Ok(Notation::Mode(Mode::parse(symbol)?)),
165            Some(other) => Err(anyhow::anyhow!("Unknown notation type '{}'. Use 'chord', 'scale', or 'mode'.", other)),
166            None => Notation::parse(symbol),
167        }
168    }
169
170    /// Returns a verbose string representation of this notation.
171    ///
172    /// For chords, this includes recommended scales/modes.
173    /// For scales and modes, this is equivalent to `Display`.
174    pub fn format_verbose(&self) -> String {
175        match self {
176            Notation::Chord(chord) => chord.format_with_scale_candidates(),
177            Notation::Scale(scale) => format!("{}", scale),
178            Notation::Mode(mode) => format!("{}", mode),
179        }
180    }
181}
182
183impl HasRoot for Notation {
184    fn root(&self) -> Note {
185        match self {
186            Notation::Chord(c) => c.root(),
187            Notation::Scale(s) => s.root(),
188            Notation::Mode(m) => m.root(),
189        }
190    }
191}
192
193impl HasIntervals for Notation {
194    fn intervals(&self) -> &'static [Interval] {
195        match self {
196            Notation::Chord(c) => c.intervals(),
197            Notation::Scale(s) => s.intervals(),
198            Notation::Mode(m) => m.intervals(),
199        }
200    }
201}
202
203impl HasName for Notation {
204    fn name(&self) -> String {
205        match self {
206            Notation::Chord(c) => c.name(),
207            Notation::Scale(s) => s.name(),
208            Notation::Mode(m) => m.name(),
209        }
210    }
211}
212
213impl HasPreciseName for Notation {
214    fn precise_name(&self) -> String {
215        match self {
216            Notation::Chord(c) => c.precise_name(),
217            Notation::Scale(s) => s.precise_name(),
218            Notation::Mode(m) => m.precise_name(),
219        }
220    }
221}
222
223impl HasDescription for Notation {
224    fn description(&self) -> &'static str {
225        match self {
226            Notation::Chord(c) => c.description(),
227            Notation::Scale(s) => s.description(),
228            Notation::Mode(m) => m.description(),
229        }
230    }
231}
232
233impl Display for Notation {
234    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
235        match self {
236            Notation::Chord(c) => write!(f, "{}", c),
237            Notation::Scale(s) => write!(f, "{}", s),
238            Notation::Mode(m) => write!(f, "{}", m),
239        }
240    }
241}
242
243impl Parsable for Notation {
244    /// Attempts to parse the input as a scale, then a mode, then a chord.
245    ///
246    /// The order is intentional: scales have the most distinctive keywords,
247    /// modes have specific keywords, and chords are the fallback.
248    fn parse(input: &str) -> Res<Self>
249    where
250        Self: Sized,
251    {
252        // Try scale first (most distinctive keywords).
253        if let Ok(scale) = Scale::parse(input) {
254            return Ok(Notation::Scale(scale));
255        }
256
257        // Try mode second.
258        if let Ok(mode) = Mode::parse(input) {
259            return Ok(Notation::Mode(mode));
260        }
261
262        // Fallback to chord.
263        let chord = Chord::parse(input)?;
264        Ok(Notation::Chord(chord))
265    }
266}
267
268impl From<Chord> for Notation {
269    fn from(chord: Chord) -> Self {
270        Notation::Chord(chord)
271    }
272}
273
274impl From<Scale> for Notation {
275    fn from(scale: Scale) -> Self {
276        Notation::Scale(scale)
277    }
278}
279
280impl From<Mode> for Notation {
281    fn from(mode: Mode) -> Self {
282        Notation::Mode(mode)
283    }
284}
285
286// Tests.
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::core::mode::HasModeKind;
292    use crate::core::mode_kind::ModeKind;
293    use crate::core::note::*;
294    use crate::core::scale::HasScaleKind;
295    use crate::core::scale_kind::ScaleKind;
296    use pretty_assertions::assert_eq;
297
298    #[test]
299    fn test_parse_as_scale() {
300        let notation = Notation::parse("C major pentatonic").unwrap();
301        assert!(notation.is_scale());
302        assert!(!notation.is_chord());
303        assert!(!notation.is_mode());
304        assert_eq!(notation.kind(), "scale");
305
306        let scale = notation.as_scale().unwrap();
307        assert_eq!(scale.root(), C);
308        assert_eq!(scale.kind(), ScaleKind::MajorPentatonic);
309    }
310
311    #[test]
312    fn test_parse_as_mode() {
313        let notation = Notation::parse("D dorian").unwrap();
314        assert!(notation.is_mode());
315        assert!(!notation.is_chord());
316        assert!(!notation.is_scale());
317        assert_eq!(notation.kind(), "mode");
318
319        let mode = notation.as_mode().unwrap();
320        assert_eq!(mode.root(), D);
321        assert_eq!(mode.kind(), ModeKind::Dorian);
322    }
323
324    #[test]
325    fn test_parse_as_chord() {
326        let notation = Notation::parse("Cmaj7").unwrap();
327        assert!(notation.is_chord());
328        assert!(!notation.is_scale());
329        assert!(!notation.is_mode());
330        assert_eq!(notation.kind(), "chord");
331
332        let chord = notation.as_chord().unwrap();
333        assert_eq!(chord.root(), C);
334    }
335
336    #[test]
337    fn test_parse_complex_chord() {
338        let notation = Notation::parse("Em7b9b13").unwrap();
339        assert!(notation.is_chord());
340        assert_eq!(notation.root(), E);
341    }
342
343    #[test]
344    fn test_parse_harmonic_minor() {
345        let notation = Notation::parse("A harmonic minor").unwrap();
346        assert!(notation.is_scale());
347
348        let scale = notation.as_scale().unwrap();
349        assert_eq!(scale.kind(), ScaleKind::HarmonicMinor);
350    }
351
352    #[test]
353    fn test_parse_lydian_mode() {
354        let notation = Notation::parse("F lydian").unwrap();
355        assert!(notation.is_mode());
356
357        let mode = notation.as_mode().unwrap();
358        assert_eq!(mode.kind(), ModeKind::Lydian);
359    }
360
361    #[test]
362    fn test_into_inner() {
363        let notation = Notation::parse("Cm7").unwrap();
364        let chord = notation.into_chord().unwrap();
365        assert_eq!(chord.root(), C);
366
367        let notation = Notation::parse("G mixolydian").unwrap();
368        let mode = notation.into_mode().unwrap();
369        assert_eq!(mode.root(), G);
370
371        let notation = Notation::parse("E blues").unwrap();
372        let scale = notation.into_scale().unwrap();
373        assert_eq!(scale.root(), E);
374    }
375
376    #[test]
377    fn test_notes() {
378        let notation = Notation::parse("C major").unwrap();
379        assert!(notation.is_scale());
380        let notes = notation.notes();
381        assert_eq!(notes.len(), 7);
382        assert_eq!(notes[0], C);
383
384        let notation = Notation::parse("A aeolian").unwrap();
385        assert!(notation.is_mode());
386        let notes = notation.notes();
387        assert_eq!(notes.len(), 7);
388        assert_eq!(notes[0], A);
389    }
390
391    #[test]
392    fn test_intervals() {
393        // Scale intervals
394        let notation = Notation::parse("C major").unwrap();
395        assert!(!notation.intervals().is_empty());
396
397        // Mode intervals
398        let notation = Notation::parse("D dorian").unwrap();
399        assert!(!notation.intervals().is_empty());
400
401        // Chord intervals (chord tones from KnownChord)
402        let notation = Notation::parse("Cmaj7").unwrap();
403        assert!(!notation.intervals().is_empty());
404        assert_eq!(notation.intervals().len(), 4); // Root, 3rd, 5th, 7th
405    }
406
407    #[test]
408    fn test_has_name() {
409        let notation = Notation::parse("C major pentatonic").unwrap();
410        assert_eq!(notation.name(), "C major pentatonic");
411
412        let notation = Notation::parse("D dorian").unwrap();
413        assert_eq!(notation.name(), "D dorian");
414
415        let notation = Notation::parse("Cmaj7").unwrap();
416        assert!(notation.name().contains("C"));
417    }
418
419    #[test]
420    fn test_from_impls() {
421        let chord = Chord::parse("Cm7").unwrap();
422        let notation: Notation = chord.into();
423        assert!(notation.is_chord());
424
425        let scale = Scale::parse("C major").unwrap();
426        let notation: Notation = scale.into();
427        assert!(notation.is_scale());
428
429        let mode = Mode::parse("D dorian").unwrap();
430        let notation: Notation = mode.into();
431        assert!(notation.is_mode());
432    }
433
434    #[test]
435    fn test_display() {
436        let notation = Notation::parse("C major").unwrap();
437        let display = format!("{}", notation);
438        assert!(display.contains("C major"));
439
440        let notation = Notation::parse("D dorian").unwrap();
441        let display = format!("{}", notation);
442        assert!(display.contains("D dorian"));
443    }
444
445    #[test]
446    fn test_priority_scale_over_chord() {
447        // "C major" should parse as scale, not chord
448        let notation = Notation::parse("C major").unwrap();
449        assert!(notation.is_scale(), "Expected 'C major' to parse as scale, got {:?}", notation.kind());
450    }
451
452    #[test]
453    fn test_priority_mode_over_chord() {
454        // "C ionian" should parse as mode, not chord
455        let notation = Notation::parse("C ionian").unwrap();
456        assert!(notation.is_mode(), "Expected 'C ionian' to parse as mode, got {:?}", notation.kind());
457    }
458}