Skip to main content

klib/core/
known_chord.rs

1//! A module for working with known chords.
2
3use crate::core::{
4    base::{HasDescription, HasName, HasStaticName},
5    interval::{HasIntervals, Interval},
6    mode::Mode,
7    mode_kind::ModeKind,
8    modifier::Degree,
9    note::Note,
10    scale::Scale,
11    scale_kind::ScaleKind,
12};
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17// Traits.
18
19/// A trait for types that have a relative scale.
20pub trait HasRelativeScale {
21    /// Returns the relative scale of the type (usually a [`Chord`]).
22    ///
23    /// The relative scale is the scale that the chord is built on, using
24    /// only the intervals, without any need for notes; e.g., a major chord
25    /// is built with all the "major" and "perfect" intervals.
26    fn relative_scale(&self) -> Vec<Interval>;
27}
28
29/// A trait for types that have a relative chord.
30pub trait HasRelativeChord {
31    /// Returns the relative chord of the type (usually a [`Chord`]).
32    ///
33    /// The relative chord is the chord that the chord is built on, using
34    /// only the intervals, without any need for notes; e.g., a major chord
35    /// is built with the major third and perfect fifth intervals.
36    fn relative_chord(&self) -> Vec<Interval>;
37}
38
39/// A trait for types that can enumerate recommended scales and modes.
40pub trait HasScaleCandidates {
41    /// Returns a list of recommended scale/mode candidates for this chord,
42    /// ranked by relevance.
43    fn scale_candidates(&self) -> Vec<ScaleCandidate>;
44}
45
46// Structures.
47
48/// Represents a recommended scale or mode for a chord.
49#[derive(Debug, Clone, PartialEq)]
50#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
51pub enum ScaleCandidate {
52    /// A mode candidate
53    Mode {
54        /// The mode kind
55        kind: ModeKind,
56        /// Ranking (1 = most relevant)
57        rank: u8,
58        /// Reason why this mode fits the chord
59        reason: &'static str,
60    },
61    /// A scale candidate
62    Scale {
63        /// The scale kind
64        kind: ScaleKind,
65        /// Ranking (1 = most relevant)
66        rank: u8,
67        /// Reason why this scale fits the chord
68        reason: &'static str,
69    },
70}
71
72impl ScaleCandidate {
73    /// Returns the rank of this candidate
74    pub fn rank(&self) -> u8 {
75        match self {
76            ScaleCandidate::Mode { rank, .. } => *rank,
77            ScaleCandidate::Scale { rank, .. } => *rank,
78        }
79    }
80
81    /// Returns the reason for this candidate
82    pub fn reason(&self) -> &'static str {
83        match self {
84            ScaleCandidate::Mode { reason, .. } => reason,
85            ScaleCandidate::Scale { reason, .. } => reason,
86        }
87    }
88
89    /// Returns the name of this candidate
90    pub fn name(&self) -> String {
91        match self {
92            ScaleCandidate::Mode { kind, .. } => kind.name(),
93            ScaleCandidate::Scale { kind, .. } => kind.name(),
94        }
95    }
96
97    /// Returns the description of this candidate
98    pub fn description(&self) -> &'static str {
99        match self {
100            ScaleCandidate::Mode { kind, .. } => kind.description(),
101            ScaleCandidate::Scale { kind, .. } => kind.description(),
102        }
103    }
104
105    /// Returns the notes of this candidate rooted at the given note
106    pub fn notes(&self, root: Note) -> Vec<Note> {
107        match self {
108            ScaleCandidate::Mode { kind, .. } => Mode::new(root, *kind).notes(),
109            ScaleCandidate::Scale { kind, .. } => Scale::new(root, *kind).notes(),
110        }
111    }
112}
113
114/// Represents the kind of interval collection (mode or scale)
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub enum IntervalCollectionKind {
118    /// A mode
119    Mode(ModeKind),
120    /// A scale
121    Scale(ScaleKind),
122}
123
124/// Represents a scale or mode candidate in static storage format
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct IntervalCandidate {
127    /// The kind of interval collection
128    pub kind: IntervalCollectionKind,
129    /// Ranking (1 = most relevant)
130    pub rank: u8,
131    /// Reason why this fits the chord
132    pub reason: &'static str,
133}
134
135impl IntervalCandidate {
136    /// Converts this interval candidate to a rooted scale candidate
137    pub fn to_scale_candidate(&self) -> ScaleCandidate {
138        match self.kind {
139            IntervalCollectionKind::Mode(kind) => ScaleCandidate::Mode {
140                kind,
141                rank: self.rank,
142                reason: self.reason,
143            },
144            IntervalCollectionKind::Scale(kind) => ScaleCandidate::Scale {
145                kind,
146                rank: self.rank,
147                reason: self.reason,
148            },
149        }
150    }
151}
152
153// Enum.
154
155/// An enum representing a known chord.
156#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug, Ord, PartialOrd)]
157#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
158#[repr(u8)]
159pub enum KnownChord {
160    /// An unknown chord.
161    Unknown,
162    /// A major chord.
163    Major,
164    /// A minor chord.
165    Minor,
166    /// A major 7 chord.
167    Major7,
168    /// A dominant chord.
169    Dominant(Degree),
170    /// A minor major 7 chord.
171    MinorMajor7,
172    /// A minor dominant chord with degree.
173    MinorDominant(Degree),
174    /// A dominant sharp 11 chord with degree.
175    DominantSharp11(Degree),
176    /// An augmented chord.
177    Augmented,
178    /// An augmented major 7 chord.
179    AugmentedMajor7,
180    /// An augmented chord.
181    AugmentedDominant(Degree),
182    /// A half diminished chord.
183    HalfDiminished(Degree),
184    /// A diminished chord.
185    Diminished,
186    /// A dominant flat 9 chord.
187    DominantFlat9(Degree),
188    /// A dominant sharp 9 chord.
189    DominantSharp9(Degree),
190    /// An augmented dominant flat 9 chord.
191    AugmentedDominantFlat9(Degree),
192    /// A minor dominant flat 13 chord.
193    MinorDominantFlat13(Degree),
194    /// A minor dominant flat 9 flat 13 chord.
195    MinorDominantFlat9Flat13(Degree),
196    /// A sharp 11 chord.
197    Sharp11,
198}
199
200// Impls.
201
202impl HasDescription for KnownChord {
203    fn description(&self) -> &'static str {
204        match self {
205            KnownChord::Unknown => panic!("KnownChord::Unknown should never be used in description()"),
206            KnownChord::Major => "major",
207            KnownChord::Minor => "minor",
208            KnownChord::Major7 => "major 7",
209            KnownChord::Dominant(_) => "dominant",
210            KnownChord::MinorMajor7 => "minor major 7",
211            KnownChord::MinorDominant(_) => "minor 7",
212            KnownChord::DominantSharp11(_) => "dominant sharp 11",
213            KnownChord::Augmented => "augmented",
214            KnownChord::AugmentedMajor7 => "augmented major 7",
215            KnownChord::AugmentedDominant(_) => "augmented dominant",
216            KnownChord::HalfDiminished(_) => "half diminished",
217            KnownChord::Diminished => "diminished",
218            KnownChord::DominantFlat9(_) => "dominant flat 9",
219            KnownChord::DominantSharp9(_) => "dominant sharp 9",
220            KnownChord::AugmentedDominantFlat9(_) => "augmented dominant flat 9",
221            KnownChord::MinorDominantFlat13(_) => "minor dominant flat 13",
222            KnownChord::MinorDominantFlat9Flat13(_) => "minor dominant flat 9 flat 13",
223            KnownChord::Sharp11 => "sharp 11",
224        }
225    }
226}
227
228impl HasRelativeScale for KnownChord {
229    fn relative_scale(&self) -> Vec<Interval> {
230        match self {
231            KnownChord::Unknown => panic!("KnownChord::Unknown should never be used in relative_scale()"),
232            KnownChord::Major => vec![
233                Interval::PerfectUnison,
234                Interval::MajorSecond,
235                Interval::MajorThird,
236                Interval::PerfectFourth,
237                Interval::PerfectFifth,
238                Interval::MajorSixth,
239                Interval::MajorSeventh,
240            ],
241            KnownChord::Minor => vec![
242                Interval::PerfectUnison,
243                Interval::MajorSecond,
244                Interval::MinorThird,
245                Interval::PerfectFourth,
246                Interval::PerfectFifth,
247                Interval::MinorSixth,
248                Interval::MinorSeventh,
249            ],
250            KnownChord::Major7 => vec![
251                Interval::PerfectUnison,
252                Interval::MajorSecond,
253                Interval::MajorThird,
254                Interval::PerfectFourth,
255                Interval::PerfectFifth,
256                Interval::MajorSixth,
257                Interval::MajorSeventh,
258            ],
259            KnownChord::Dominant(_) => vec![
260                Interval::PerfectUnison,
261                Interval::MajorSecond,
262                Interval::MajorThird,
263                Interval::PerfectFourth,
264                Interval::PerfectFifth,
265                Interval::MajorSixth,
266                Interval::MinorSeventh,
267            ],
268            KnownChord::MinorMajor7 => vec![
269                Interval::PerfectUnison,
270                Interval::MajorSecond,
271                Interval::MinorThird,
272                Interval::PerfectFourth,
273                Interval::PerfectFifth,
274                Interval::MajorSixth,
275                Interval::MajorSeventh,
276            ],
277            KnownChord::MinorDominant(_) => vec![
278                Interval::PerfectUnison,
279                Interval::MajorSecond,
280                Interval::MinorThird,
281                Interval::PerfectFourth,
282                Interval::PerfectFifth,
283                Interval::MajorSixth,
284                Interval::MinorSeventh,
285            ],
286            KnownChord::DominantSharp11(_) => vec![
287                Interval::PerfectUnison,
288                Interval::MajorSecond,
289                Interval::MajorThird,
290                Interval::AugmentedFourth,
291                Interval::PerfectFifth,
292                Interval::MajorSixth,
293                Interval::MinorSeventh,
294            ],
295            KnownChord::Augmented => vec![
296                Interval::PerfectUnison,
297                Interval::MajorSecond,
298                Interval::MajorThird,
299                Interval::PerfectFourth,
300                Interval::AugmentedFifth,
301                Interval::MajorSixth,
302                Interval::MajorSeventh,
303            ],
304            KnownChord::AugmentedMajor7 => vec![
305                Interval::PerfectUnison,
306                Interval::MajorSecond,
307                Interval::MajorThird,
308                Interval::AugmentedFourth,
309                Interval::AugmentedFifth,
310                Interval::MajorSixth,
311                Interval::MajorSeventh,
312            ],
313            KnownChord::AugmentedDominant(_) => vec![
314                Interval::PerfectUnison,
315                Interval::MajorSecond,
316                Interval::MajorThird,
317                Interval::AugmentedFourth,
318                Interval::AugmentedFifth,
319                Interval::AugmentedSixth,
320            ],
321            KnownChord::HalfDiminished(_) => vec![
322                Interval::PerfectUnison,
323                Interval::MinorSecond,
324                Interval::MinorThird,
325                Interval::PerfectFourth,
326                Interval::DiminishedFifth,
327                Interval::MinorSixth,
328                Interval::MinorSeventh,
329            ],
330            KnownChord::Diminished => vec![
331                Interval::PerfectUnison,
332                Interval::MajorSecond,
333                Interval::MinorThird,
334                Interval::PerfectFourth,
335                Interval::DiminishedFifth,
336                Interval::MinorSixth,
337                Interval::DiminishedSeventh,
338                Interval::MajorSeventh,
339            ],
340            KnownChord::DominantFlat9(_) => vec![
341                Interval::PerfectUnison,
342                Interval::MinorSecond,
343                Interval::MinorThird,
344                Interval::MajorThird,
345                Interval::AugmentedFourth,
346                Interval::PerfectFifth,
347                Interval::MajorSixth,
348                Interval::MinorSeventh,
349            ],
350            KnownChord::DominantSharp9(_) => vec![
351                Interval::PerfectUnison,
352                Interval::MinorSecond,
353                Interval::MinorThird,
354                Interval::DiminishedFourth,
355                Interval::DiminishedFifth,
356                Interval::MinorSixth,
357                Interval::MinorSeventh,
358            ],
359            KnownChord::AugmentedDominantFlat9(_) => vec![
360                Interval::PerfectUnison,
361                Interval::MinorSecond,
362                Interval::MinorThird,
363                Interval::MajorThird,
364                Interval::AugmentedFourth,
365                Interval::AugmentedFifth,
366                Interval::MinorSeventh,
367            ],
368            KnownChord::MinorDominantFlat13(_) => vec![
369                Interval::PerfectUnison,
370                Interval::MajorSecond,
371                Interval::MinorThird,
372                Interval::PerfectFourth,
373                Interval::PerfectFifth,
374                Interval::MinorSixth,
375                Interval::MinorSeventh,
376            ],
377            KnownChord::MinorDominantFlat9Flat13(_) => vec![
378                Interval::PerfectUnison,
379                Interval::MinorSecond,
380                Interval::MinorThird,
381                Interval::PerfectFourth,
382                Interval::PerfectFifth,
383                Interval::MinorSixth,
384                Interval::MinorSeventh,
385            ],
386            KnownChord::Sharp11 => vec![
387                Interval::PerfectUnison,
388                Interval::MajorSecond,
389                Interval::MajorThird,
390                Interval::AugmentedFourth,
391                Interval::PerfectFifth,
392                Interval::MajorSixth,
393                Interval::MajorSeventh,
394            ],
395        }
396    }
397}
398
399impl HasRelativeChord for KnownChord {
400    fn relative_chord(&self) -> Vec<Interval> {
401        self.intervals().to_vec()
402    }
403}
404
405impl HasName for KnownChord {
406    fn name(&self) -> String {
407        match self {
408            KnownChord::Unknown => panic!("KnownChord::Unknown should never be used in name()"),
409            KnownChord::Major => "".to_owned(),
410            KnownChord::Minor => "m".to_owned(),
411            KnownChord::Major7 => "maj7".to_owned(),
412            KnownChord::Dominant(d) => d.static_name().to_owned(),
413            KnownChord::MinorMajor7 => "m(maj7)".to_owned(),
414            KnownChord::MinorDominant(d) => format!("m{}", d.static_name()),
415            KnownChord::DominantSharp11(d) => format!("{}(♯11)", d.static_name()),
416            KnownChord::Augmented => "+".to_owned(),
417            KnownChord::AugmentedMajor7 => "+(maj7)".to_owned(),
418            KnownChord::AugmentedDominant(d) => format!("+{}", d.static_name()),
419            KnownChord::HalfDiminished(d) => format!("m{}(♭5)", d.static_name()),
420            KnownChord::Diminished => "dim".to_owned(),
421            KnownChord::DominantFlat9(d) => format!("{}(♭9)", d.static_name()),
422            KnownChord::DominantSharp9(d) => format!("{}(♯9)", d.static_name()),
423            KnownChord::AugmentedDominantFlat9(d) => format!("+{}(♭9)", d.static_name()),
424            KnownChord::MinorDominantFlat13(d) => format!("m{}(♭13)", d.static_name()),
425            KnownChord::MinorDominantFlat9Flat13(d) => format!("{}(♭9)(♭13)", d.static_name()),
426            KnownChord::Sharp11 => "(♯11)".to_owned(),
427        }
428    }
429}
430
431impl KnownChord {
432    /// Returns the static interval candidates for this chord
433    pub fn scale_interval_candidates(&self) -> &'static [IntervalCandidate] {
434        match self {
435            KnownChord::Unknown => &[],
436            KnownChord::Major => MAJOR_CANDIDATES,
437            KnownChord::Minor => MINOR_CANDIDATES,
438            KnownChord::Major7 => MAJOR7_CANDIDATES,
439            KnownChord::Dominant(_) => DOMINANT_CANDIDATES,
440            KnownChord::MinorMajor7 => MINOR_MAJOR7_CANDIDATES,
441            KnownChord::MinorDominant(_) => MINOR_DOMINANT_CANDIDATES,
442            KnownChord::DominantSharp11(_) => DOMINANT_SHARP11_CANDIDATES,
443            KnownChord::Augmented => AUGMENTED_CANDIDATES,
444            KnownChord::AugmentedMajor7 => AUGMENTED_MAJOR7_CANDIDATES,
445            KnownChord::AugmentedDominant(_) => AUGMENTED_DOMINANT_CANDIDATES,
446            KnownChord::HalfDiminished(_) => HALF_DIMINISHED_CANDIDATES,
447            KnownChord::Diminished => DIMINISHED_CANDIDATES,
448            KnownChord::DominantFlat9(_) => DOMINANT_FLAT9_CANDIDATES,
449            KnownChord::DominantSharp9(_) => DOMINANT_SHARP9_CANDIDATES,
450            KnownChord::AugmentedDominantFlat9(_) => AUGMENTED_DOMINANT_FLAT9_CANDIDATES,
451            KnownChord::MinorDominantFlat13(_) => MINOR_DOMINANT_FLAT13_CANDIDATES,
452            KnownChord::MinorDominantFlat9Flat13(_) => MINOR_DOMINANT_FLAT9_FLAT13_CANDIDATES,
453            KnownChord::Sharp11 => SHARP11_CANDIDATES,
454        }
455    }
456}
457
458impl HasScaleCandidates for KnownChord {
459    fn scale_candidates(&self) -> Vec<ScaleCandidate> {
460        self.scale_interval_candidates().iter().map(IntervalCandidate::to_scale_candidate).collect()
461    }
462}
463
464impl HasIntervals for KnownChord {
465    fn intervals(&self) -> &'static [Interval] {
466        match self {
467            KnownChord::Unknown => &[],
468            KnownChord::Major => MAJOR_INTERVALS,
469            KnownChord::Minor => MINOR_INTERVALS,
470            KnownChord::Major7 => MAJOR7_INTERVALS,
471            KnownChord::Dominant(_) => DOMINANT_INTERVALS,
472            KnownChord::MinorMajor7 => MINOR_MAJOR7_INTERVALS,
473            KnownChord::MinorDominant(_) => MINOR_DOMINANT_INTERVALS,
474            KnownChord::DominantSharp11(_) => DOMINANT_SHARP11_INTERVALS,
475            KnownChord::Augmented => AUGMENTED_INTERVALS,
476            KnownChord::AugmentedMajor7 => AUGMENTED_MAJOR7_INTERVALS,
477            KnownChord::AugmentedDominant(_) => AUGMENTED_DOMINANT_INTERVALS,
478            KnownChord::AugmentedDominantFlat9(_) => AUGMENTED_DOMINANT_FLAT9_INTERVALS,
479            KnownChord::HalfDiminished(_) => HALF_DIMINISHED_INTERVALS,
480            KnownChord::Diminished => DIMINISHED_INTERVALS,
481            KnownChord::DominantFlat9(_) => DOMINANT_FLAT9_INTERVALS,
482            KnownChord::DominantSharp9(_) => DOMINANT_SHARP9_INTERVALS,
483            KnownChord::MinorDominantFlat13(_) => MINOR_DOMINANT_FLAT13_INTERVALS,
484            KnownChord::MinorDominantFlat9Flat13(_) => MINOR_DOMINANT_FLAT9_FLAT13_INTERVALS,
485            KnownChord::Sharp11 => SHARP11_INTERVALS,
486        }
487    }
488}
489
490// Static chord intervals for each KnownChord variant (chord tones only)
491
492static MAJOR_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth];
493static MINOR_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth];
494static MAJOR7_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MajorSeventh];
495static DOMINANT_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MinorSeventh];
496static MINOR_MAJOR7_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth, Interval::MajorSeventh];
497static MINOR_DOMINANT_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth, Interval::MinorSeventh];
498static DOMINANT_SHARP11_INTERVALS: &[Interval] = &[
499    Interval::PerfectUnison,
500    Interval::MajorThird,
501    Interval::PerfectFifth,
502    Interval::MinorSeventh,
503    Interval::AugmentedEleventh,
504];
505static AUGMENTED_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth];
506static AUGMENTED_MAJOR7_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth, Interval::MajorSeventh];
507static AUGMENTED_DOMINANT_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth, Interval::MinorSeventh];
508static AUGMENTED_DOMINANT_FLAT9_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::AugmentedFifth, Interval::MinorSeventh, Interval::MinorNinth];
509static HALF_DIMINISHED_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::DiminishedFifth, Interval::MinorSeventh];
510static DIMINISHED_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::DiminishedFifth, Interval::DiminishedSeventh];
511static DOMINANT_FLAT9_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MinorSeventh, Interval::MinorNinth];
512static DOMINANT_SHARP9_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MajorThird, Interval::PerfectFifth, Interval::MinorSeventh, Interval::AugmentedNinth];
513static MINOR_DOMINANT_FLAT13_INTERVALS: &[Interval] = &[Interval::PerfectUnison, Interval::MinorThird, Interval::PerfectFifth, Interval::MinorSeventh, Interval::MinorThirteenth];
514static MINOR_DOMINANT_FLAT9_FLAT13_INTERVALS: &[Interval] = &[
515    Interval::PerfectUnison,
516    Interval::MinorThird,
517    Interval::PerfectFifth,
518    Interval::MinorSeventh,
519    Interval::MinorNinth,
520    Interval::MinorThirteenth,
521];
522static SHARP11_INTERVALS: &[Interval] = &[
523    Interval::PerfectUnison,
524    Interval::MajorThird,
525    Interval::PerfectFifth,
526    Interval::MajorSeventh,
527    Interval::AugmentedEleventh,
528];
529
530// Static interval candidates for each KnownChord variant
531
532static MAJOR_CANDIDATES: &[IntervalCandidate] = &[
533    IntervalCandidate {
534        kind: IntervalCollectionKind::Mode(ModeKind::Ionian),
535        rank: 1,
536        reason: "Default diatonic major scale for functional harmony in tonal contexts; works over I, IV, V progressions; avoid dwelling on the 4th scale degree as it can clash with the major 3rd in the triad",
537    },
538    IntervalCandidate {
539        kind: IntervalCollectionKind::Scale(ScaleKind::MajorPentatonic),
540        rank: 2,
541        reason: "Safe major melody that avoids the 4th and 7th scale degrees completely; eliminates avoid-note concerns; perfect for pop, rock, and country hooks where you want guaranteed consonance",
542    },
543    IntervalCandidate {
544        kind: IntervalCollectionKind::Mode(ModeKind::Lydian),
545        rank: 3,
546        reason: "Bright major sound with ♯11 sheen; major with ♯4 (4th mode of major scale); common in modern jazz and film scoring; lean into the ♯4/♯11 as the characteristic color tone that distinguishes it from Ionian",
547    },
548    IntervalCandidate {
549        kind: IntervalCollectionKind::Mode(ModeKind::Mixolydian),
550        rank: 4,
551        reason: "Adds ♭7 over a major triad for blues, rock, and modal flavor; creates a dominant-like quality without needing to resolve; stylistic choice for a looser, groovier feel",
552    },
553];
554
555static MINOR_CANDIDATES: &[IntervalCandidate] = &[
556    IntervalCandidate {
557        kind: IntervalCollectionKind::Mode(ModeKind::Aeolian),
558        rank: 1,
559        reason: "Default natural minor scale for functional harmony; 6th mode of major scale; provides the classic melancholic, sad tonality with ♭6; contains ♭3, ♭6, and ♭7 for full minor character",
560    },
561    IntervalCandidate {
562        kind: IntervalCollectionKind::Scale(ScaleKind::MinorPentatonic),
563        rank: 2,
564        reason: "Safe minor melody that avoids the ♭2 and ♭6 scale degrees; the go-to scale for blues, rock, and pentatonic-based improvisation; simple and effective with no avoid-note concerns",
565    },
566    IntervalCandidate {
567        kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
568        rank: 3,
569        reason: "Essential blues vocabulary built on minor pentatonic with added ♯4 blue note; phrasing-driven rather than theoretical; creates that characteristic blues 'bend' and expressive quality",
570    },
571    IntervalCandidate {
572        kind: IntervalCollectionKind::Mode(ModeKind::Dorian),
573        rank: 4,
574        reason: "Minor mode with raised 6th (♮6) that provides a brighter, jazzier lift compared to Aeolian; 2nd mode of major scale; less sad than natural minor; common choice for jazz and funk minor sounds",
575    },
576    IntervalCandidate {
577        kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
578        rank: 5,
579        reason: "Minor mode with a lowered 2nd (♭2) that creates an exotic, Spanish, or Flamenco flavor; 3rd mode of major scale; dark and modal with a distinctly non-functional character",
580    },
581    IntervalCandidate {
582        kind: IntervalCollectionKind::Scale(ScaleKind::HarmonicMinor),
583        rank: 6,
584        reason: "Classical minor scale with raised 7th (♮7) for strong V-i resolution; the ♭6 creates tension and augmented-second interval; essential for traditional minor key harmony",
585    },
586    IntervalCandidate {
587        kind: IntervalCollectionKind::Scale(ScaleKind::MelodicMinor),
588        rank: 7,
589        reason: "Modern jazz minor with both ♮6 and ♮7 for smooth ascending melodic motion; eliminates the augmented second of harmonic minor; bright and sophisticated minor color",
590    },
591];
592
593static MAJOR7_CANDIDATES: &[IntervalCandidate] = &[
594    IntervalCandidate {
595        kind: IntervalCollectionKind::Mode(ModeKind::Ionian),
596        rank: 1,
597        reason: "Default major 7 scale from functional harmony; Ionian mode provides the natural maj7 chord tones; watch out for the 4th scale degree which can clash as an avoid-note over the maj7 chord",
598    },
599    IntervalCandidate {
600        kind: IntervalCollectionKind::Mode(ModeKind::Lydian),
601        rank: 2,
602        reason: "Bright major 7 sound with ♯11 sheen; major 7 with ♯4 (4th mode of major scale); the modern jazz choice for maj7 chords; the ♯4/♯11 is the color tone that makes this sparkle; eliminates the avoid-note issue of the natural 4th",
603    },
604    IntervalCandidate {
605        kind: IntervalCollectionKind::Scale(ScaleKind::MajorPentatonic),
606        rank: 3,
607        reason: "Safe major 7 melody that completely avoids both the 4th and 7th scale degrees; provides consonant, hook-friendly melodic material with zero avoid-note concerns",
608    },
609];
610
611static DOMINANT_CANDIDATES: &[IntervalCandidate] = &[
612    IntervalCandidate {
613        kind: IntervalCollectionKind::Mode(ModeKind::Mixolydian),
614        rank: 1,
615        reason: "Default dominant 7 scale; 5th mode of major scale; provides major 3rd with ♭7 for functional V chord resolution; the bread-and-butter choice for tonal dominant chords in ii-V-I progressions",
616    },
617    IntervalCandidate {
618        kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
619        rank: 2,
620        reason: "Blues vocabulary over dominant 7 chords; includes the ♯4 blue note for characteristic blues phrasing; phrasing-driven rather than theoretical; perfect for blues turnarounds and rock contexts",
621    },
622    IntervalCandidate {
623        kind: IntervalCollectionKind::Mode(ModeKind::LydianDominant),
624        rank: 3,
625        reason: "Dominant 7 with ♯11 color (Mixolydian with ♯4); 4th mode of melodic minor; sophisticated modern jazz sound; use when you want bright ♯11 upper-structure tension on your V chord; also called Acoustic scale",
626    },
627    IntervalCandidate {
628        kind: IntervalCollectionKind::Mode(ModeKind::MixolydianFlat6),
629        rank: 4,
630        reason: "Dominant scale with ♭13 (♭6) for darker tension; 5th mode of melodic minor; creates a minor-leaning dominant sound; useful when the V chord needs to lean toward a minor resolution",
631    },
632    IntervalCandidate {
633        kind: IntervalCollectionKind::Scale(ScaleKind::WholeTone),
634        rank: 5,
635        reason: "Symmetrical whole tone scale for augmented dominant (#5) color; creates dreamy, floating, harmonically ambiguous sound; every note is a whole step apart; use for suspended, unresolved V7#5 or V+7 sounds",
636    },
637    IntervalCandidate {
638        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
639        rank: 6,
640        reason: "Classic dominant diminished (half-whole octatonic); supports ♭9, ♯9, ♯11, and 13 simultaneously; use when the V chord feels 'hot' and needs maximum upper-structure tension; distinct from Altered mode",
641    },
642    IntervalCandidate {
643        kind: IntervalCollectionKind::Mode(ModeKind::Altered),
644        rank: 7,
645        reason: "The ultimate altered dominant tool (7th mode of melodic minor); provides maximum tension with ♭9, ♯9, ♭5, and ♯5 (♭13) all available; strongest when resolving to minor i or in modern jazz contexts needing maximum harmonic pull",
646    },
647];
648
649static MINOR_MAJOR7_CANDIDATES: &[IntervalCandidate] = &[
650    IntervalCandidate {
651        kind: IntervalCollectionKind::Scale(ScaleKind::MelodicMinor),
652        rank: 1,
653        reason: "Primary source scale for minor-major 7 chords; melodic minor provides the ♮6 and ♮7 needed for this chord quality; creates smooth ascending melodic lines in classical and jazz contexts",
654    },
655    IntervalCandidate {
656        kind: IntervalCollectionKind::Scale(ScaleKind::HarmonicMinor),
657        rank: 2,
658        reason: "Alternative source for mMaj7 with a more exotic flavor; harmonic minor provides the ♮7 but retains the ♭6 creating characteristic tension and the augmented second interval between ♭6 and ♮7",
659    },
660];
661
662static MINOR_DOMINANT_CANDIDATES: &[IntervalCandidate] = &[
663    IntervalCandidate {
664        kind: IntervalCollectionKind::Mode(ModeKind::Dorian),
665        rank: 1,
666        reason: "Default minor 7 scale with raised 6th (♮6) that lifts and brightens the sound; 2nd mode of major scale; the classic jazz ii chord sound in ii-V-I progressions; more optimistic than Aeolian",
667    },
668    IntervalCandidate {
669        kind: IntervalCollectionKind::Scale(ScaleKind::MinorPentatonic),
670        rank: 2,
671        reason: "Safe minor 7 melody that avoids the ♭2 and ♭6 scale degrees for guaranteed consonance; simple and effective choice; the go-to for straightforward, bluesy minor 7 improvisation",
672    },
673    IntervalCandidate {
674        kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
675        rank: 3,
676        reason: "Blues vocabulary over minor 7 chords with the characteristic ♯4 blue note; stylistic phrasing choice for blues, rock, and R&B contexts; phrasing-driven rather than purely harmonic",
677    },
678    IntervalCandidate {
679        kind: IntervalCollectionKind::Mode(ModeKind::Aeolian),
680        rank: 4,
681        reason: "Natural minor over m7 chords; includes the ♭6 for darker, more melancholic color compared to Dorian; functional and traditional; creates a more classically minor feel",
682    },
683    IntervalCandidate {
684        kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
685        rank: 5,
686        reason: "Modal minor 7 with lowered 2nd (♭2) for exotic, Spanish, or Flamenco flavor; 3rd mode of major scale; creates a distinctly modal, non-functional character; dark and mysterious",
687    },
688];
689
690static DOMINANT_SHARP11_CANDIDATES: &[IntervalCandidate] = &[
691    IntervalCandidate {
692        kind: IntervalCollectionKind::Mode(ModeKind::LydianDominant),
693        rank: 1,
694        reason: "The defining scale for V7♯11 chords; dominant 7 with ♯11 (Mixolydian with ♯4); 4th mode of melodic minor; sophisticated modern jazz sound for dominant chords with upper-structure tension; also called Acoustic scale",
695    },
696    IntervalCandidate {
697        kind: IntervalCollectionKind::Mode(ModeKind::Mixolydian),
698        rank: 2,
699        reason: "Basic dominant fallback that de-emphasizes the ♯11 extension; provides functional V7 sound (dominant 7 with ♭7); when you want simpler, more traditional dominant approach without the ♯11 color",
700    },
701    IntervalCandidate {
702        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
703        rank: 3,
704        reason: "Adds even more tension options on top of the V7♯11; half-whole diminished (dominant diminished) provides ♭9, ♯9, ♯11, and 13 simultaneously; use for hot, dense dominant sound with maximum upper-structure complexity",
705    },
706];
707
708static AUGMENTED_CANDIDATES: &[IntervalCandidate] = &[
709    IntervalCandidate {
710        kind: IntervalCollectionKind::Mode(ModeKind::IonianSharp5),
711        rank: 1,
712        reason: "Major scale with raised 5th (♯5); 3rd mode of harmonic minor; provides a functional augmented triad sound while maintaining major scale characteristics; the classic augmented triad source",
713    },
714    IntervalCandidate {
715        kind: IntervalCollectionKind::Scale(ScaleKind::WholeTone),
716        rank: 2,
717        reason: "Symmetrical whole tone scale where every augmented triad shares the same notes; creates a dreamy, floating, harmonically ambiguous quality; all notes are whole steps apart; perfect for suspended augmented sounds",
718    },
719    IntervalCandidate {
720        kind: IntervalCollectionKind::Mode(ModeKind::LydianAugmented),
721        rank: 3,
722        reason: "Major scale with both ♯4 and ♯5; 3rd mode of melodic minor; provides a bright, exotic augmented color with Lydian characteristics; sophisticated choice for modern jazz augmented triads",
723    },
724];
725
726static AUGMENTED_MAJOR7_CANDIDATES: &[IntervalCandidate] = &[
727    IntervalCandidate {
728        kind: IntervalCollectionKind::Mode(ModeKind::LydianAugmented),
729        rank: 1,
730        reason: "3rd mode of melodic minor; provides major 7 with both ♯4/♯11 and ♯5; bright, exotic, and sophisticated augmented major 7 sound; the modern jazz choice for this chord quality",
731    },
732    IntervalCandidate {
733        kind: IntervalCollectionKind::Mode(ModeKind::IonianSharp5),
734        rank: 2,
735        reason: "Major 7 with raised 5th from harmonic minor; more functional and traditional approach to augmented major 7; retains natural 4th unlike Lydian Augmented",
736    },
737];
738
739static AUGMENTED_DOMINANT_CANDIDATES: &[IntervalCandidate] = &[
740    IntervalCandidate {
741        kind: IntervalCollectionKind::Scale(ScaleKind::WholeTone),
742        rank: 1,
743        reason: "Primary scale for augmented dominant 7 (V+7 or V7♯5) chords; symmetrical whole tone scale naturally provides the ♯5 augmented quality; creates dreamy, floating dominant color; perfect for unresolved, suspended V+7 sounds",
744    },
745    IntervalCandidate {
746        kind: IntervalCollectionKind::Mode(ModeKind::LydianDominant),
747        rank: 2,
748        reason: "Dominant 7 with ♯11 (Mixolydian with ♯4) that can accommodate or bend toward ♯5; flexible modern dominant sound from melodic minor; use when you want V7 character with option to lean into augmented implications",
749    },
750];
751
752static HALF_DIMINISHED_CANDIDATES: &[IntervalCandidate] = &[
753    IntervalCandidate {
754        kind: IntervalCollectionKind::Mode(ModeKind::Locrian),
755        rank: 1,
756        reason: "Default half-diminished (m7♭5) scale; 7th mode of major scale; provides the functional ii°7 sound in minor key ii°-V-i progressions; classic diminished 5th with minor 7th",
757    },
758    IntervalCandidate {
759        kind: IntervalCollectionKind::Mode(ModeKind::LocrianNatural2),
760        rank: 2,
761        reason: "Half-diminished with raised 2nd (♮2) for smoother melodic motion; 6th mode of melodic minor; eliminates the difficult ♭2 interval; modern jazz choice for m7♭5 chords",
762    },
763    IntervalCandidate {
764        kind: IntervalCollectionKind::Mode(ModeKind::LocrianNatural6),
765        rank: 3,
766        reason: "Half-diminished with raised 6th (♮6 or ♮13) for a brighter color; 2nd mode of harmonic minor; provides major 6th/13th extension while maintaining the ♭5; exotic flavor",
767    },
768];
769
770static DIMINISHED_CANDIDATES: &[IntervalCandidate] = &[
771    IntervalCandidate {
772        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedWholeHalf),
773        rank: 1,
774        reason: "Symmetrical diminished 7 scale with whole-half step pattern; primary choice for fully diminished 7 chords; every diminished 7 chord shares three others a minor third apart in this scale",
775    },
776    IntervalCandidate {
777        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
778        rank: 2,
779        reason: "Alternative diminished scale with half-whole step pattern; more commonly used for dominant 7 chords but can work as a passing chord option for dim7; provides different color than W-H pattern",
780    },
781];
782
783static DOMINANT_FLAT9_CANDIDATES: &[IntervalCandidate] = &[
784    IntervalCandidate {
785        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
786        rank: 1,
787        reason: "Primary scale for V7♭9 chords using symmetrical half-whole diminished (dominant diminished) pattern; provides ♭9, ♯9, ♯11, and 13 simultaneously; rich dominant tension palette for modern jazz and classical harmony",
788    },
789    IntervalCandidate {
790        kind: IntervalCollectionKind::Mode(ModeKind::PhrygianDominant),
791        rank: 2,
792        reason: "Phrygian with major 3rd creates Spanish or Phrygian dominant sound; 5th mode of harmonic minor; characteristic ♭2/♭9 with major 3rd fingerprint; exotic, Middle Eastern, or Flamenco flavor; strong V in minor keys",
793    },
794];
795
796static DOMINANT_SHARP9_CANDIDATES: &[IntervalCandidate] = &[
797    IntervalCandidate {
798        kind: IntervalCollectionKind::Mode(ModeKind::Altered),
799        rank: 1,
800        reason: "The altered dominant scale (7th mode of melodic minor); provides all alterations including ♭9, ♯9, ♭5, and ♯5 (♭13); maximum tension with strongest resolution pull; the ultimate V7♯9 choice for modern jazz and chromatic harmony",
801    },
802    IntervalCandidate {
803        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
804        rank: 2,
805        reason: "Symmetrical half-whole diminished (dominant diminished) providing ♭9, ♯9, ♯11, and 13; comprehensive dominant tension palette; hot V7 sound with dense upper-structure options; works when you need maximum harmonic complexity",
806    },
807    IntervalCandidate {
808        kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
809        rank: 3,
810        reason: "Blues vocabulary that naturally includes the ♯9 sound through the ♯4 blue note; phrasing-driven stylistic choice rather than theoretical; perfect for blues and rock contexts where ♯9 is part of the blues language",
811    },
812];
813
814static AUGMENTED_DOMINANT_FLAT9_CANDIDATES: &[IntervalCandidate] = &[
815    IntervalCandidate {
816        kind: IntervalCollectionKind::Mode(ModeKind::Altered),
817        rank: 1,
818        reason: "Primary scale for V7(♭9)(♯5) chords; the altered dominant scale (7th mode of melodic minor) provides all necessary alterations including ♭9, ♯5/♭13, and ♭5; maximum tension with both flat 9 and sharp 5 present; creates the strongest resolution pull in modern jazz and chromatic harmony",
819    },
820    IntervalCandidate {
821        kind: IntervalCollectionKind::Scale(ScaleKind::DiminishedHalfWhole),
822        rank: 2,
823        reason: "Symmetrical half-whole diminished (dominant diminished) that supports V7(♭9)(♯5); provides ♭9, ♯9, ♯11, and 13 simultaneously; rich dominant tension palette; works well when you need the ♭9 with option to imply or approach the ♯5",
824    },
825    IntervalCandidate {
826        kind: IntervalCollectionKind::Mode(ModeKind::PhrygianDominant),
827        rank: 3,
828        reason: "Spanish or Phrygian dominant sound (5th mode of harmonic minor) emphasizing the characteristic ♭9; exotic, Middle Eastern, or Flamenco flavor; while it has natural 5th, it can approach or bend toward ♯5 in phrasing; strong V in minor keys",
829    },
830];
831
832static MINOR_DOMINANT_FLAT13_CANDIDATES: &[IntervalCandidate] = &[
833    IntervalCandidate {
834        kind: IntervalCollectionKind::Mode(ModeKind::Aeolian),
835        rank: 1,
836        reason:
837            "Natural minor scale over minor 7 chords; provides the ♭6/♭13 extension naturally; functional sound for minor dominant chords where you need the flat 13 characteristic clearly present",
838    },
839    IntervalCandidate {
840        kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
841        rank: 2,
842        reason: "Modal minor 7 with both ♭2 and ♭6/♭13; 3rd mode of major scale; creates an exotic, darker minor dominant with Spanish or modal flavor; both characteristic tensions present",
843    },
844];
845
846static MINOR_DOMINANT_FLAT9_FLAT13_CANDIDATES: &[IntervalCandidate] = &[
847    IntervalCandidate {
848        kind: IntervalCollectionKind::Mode(ModeKind::Phrygian),
849        rank: 1,
850        reason: "Phrygian mode provides both characteristic tensions: ♭2/♭9 and ♭6/♭13; 3rd mode of major scale; creates exotic, modal, Spanish or Flamenco flavor; dark and mysterious minor dominant sound",
851    },
852    IntervalCandidate {
853        kind: IntervalCollectionKind::Scale(ScaleKind::Blues),
854        rank: 2,
855        reason: "Blues scale offers simpler, more phrasing-driven vocabulary; stylistic choice rather than literal interval match; use when you want blues vocabulary over this complex minor dominant chord symbol",
856    },
857];
858
859static SHARP11_CANDIDATES: &[IntervalCandidate] = &[
860    IntervalCandidate {
861        kind: IntervalCollectionKind::Mode(ModeKind::Lydian),
862        rank: 1,
863        reason: "Major 7 with ♯11 extension; major with ♯4 (4th mode of major scale); the bright, modern sound where ♯4/♯11 is the defining color tone; sophisticated jazz and film score choice; eliminates the avoid-note issue of natural 4th",
864    },
865    IntervalCandidate {
866        kind: IntervalCollectionKind::Mode(ModeKind::Ionian),
867        rank: 2,
868        reason: "Major 7 fallback that de-emphasizes the ♯11 extension; returns to functional harmony; use when you want to downplay the ♯11 color or need a simpler, more traditional major 7 approach",
869    },
870    IntervalCandidate {
871        kind: IntervalCollectionKind::Scale(ScaleKind::MajorPentatonic),
872        rank: 3,
873        reason: "Safe major 7 melody that avoids both the 4th and 7th scale degrees; provides consonant melodic material with zero avoid-note concerns; perfect for hooks and melodic lines over maj7♯11 chords",
874    },
875];
876
877// Tests.
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882    use crate::core::modifier::Degree;
883    use pretty_assertions::assert_eq;
884
885    #[test]
886    fn test_dominant_chord_candidates() {
887        let candidates = KnownChord::Dominant(Degree::Seven).scale_candidates();
888        assert!(!candidates.is_empty(), "G7 should have scale candidates");
889
890        match &candidates[0] {
891            ScaleCandidate::Mode { kind, rank, .. } => {
892                assert_eq!(*kind, ModeKind::Mixolydian);
893                assert_eq!(*rank, 1);
894            }
895            _ => panic!("First candidate for G7 should be a Mode"),
896        }
897
898        match &candidates[1] {
899            ScaleCandidate::Scale { kind, rank, .. } => {
900                assert_eq!(*kind, ScaleKind::Blues);
901                assert_eq!(*rank, 2);
902            }
903            _ => panic!("Second candidate for G7 should be a Scale"),
904        }
905
906        match &candidates[2] {
907            ScaleCandidate::Mode { kind, rank, .. } => {
908                assert_eq!(*kind, ModeKind::LydianDominant);
909                assert_eq!(*rank, 3);
910            }
911            _ => panic!("Third candidate for G7 should be a Mode"),
912        }
913    }
914
915    #[test]
916    fn test_dominant_sharp11_candidates() {
917        let candidates = KnownChord::DominantSharp11(Degree::Seven).scale_candidates();
918        assert!(!candidates.is_empty(), "G7#11 should have scale candidates");
919
920        match &candidates[0] {
921            ScaleCandidate::Mode { kind, rank, .. } => {
922                assert_eq!(*kind, ModeKind::LydianDominant);
923                assert_eq!(*rank, 1);
924            }
925            _ => panic!("First candidate for G7#11 should be a Mode"),
926        }
927    }
928
929    #[test]
930    fn test_dominant_flat9_candidates() {
931        let candidates = KnownChord::DominantFlat9(Degree::Seven).scale_candidates();
932        assert!(!candidates.is_empty(), "G7b9 should have scale candidates");
933
934        match &candidates[0] {
935            ScaleCandidate::Scale { kind, rank, .. } => {
936                assert_eq!(*kind, ScaleKind::DiminishedHalfWhole);
937                assert_eq!(*rank, 1);
938            }
939            ScaleCandidate::Mode { kind, rank, .. } => {
940                assert_eq!(*kind, ModeKind::PhrygianDominant);
941                assert_eq!(*rank, 1);
942            }
943        }
944    }
945
946    #[test]
947    fn test_dominant_sharp9_candidates() {
948        let candidates = KnownChord::DominantSharp9(Degree::Seven).scale_candidates();
949        assert!(!candidates.is_empty(), "G7#9 should have scale candidates");
950
951        match &candidates[0] {
952            ScaleCandidate::Mode { kind, rank, .. } => {
953                assert_eq!(*kind, ModeKind::Altered);
954                assert_eq!(*rank, 1);
955            }
956            _ => panic!("First candidate for G7#9 should be a Mode"),
957        }
958    }
959
960    #[test]
961    fn test_augmented_dominant_flat9_candidates() {
962        let candidates = KnownChord::AugmentedDominantFlat9(Degree::Seven).scale_candidates();
963        assert!(!candidates.is_empty(), "G7(b9)(#5) should have scale candidates");
964
965        match &candidates[0] {
966            ScaleCandidate::Mode { kind, rank, .. } => {
967                assert_eq!(*kind, ModeKind::Altered);
968                assert_eq!(*rank, 1);
969            }
970            _ => panic!("First candidate for G7(b9)(#5) should be a Mode"),
971        }
972
973        match &candidates[1] {
974            ScaleCandidate::Scale { kind, rank, .. } => {
975                assert_eq!(*kind, ScaleKind::DiminishedHalfWhole);
976                assert_eq!(*rank, 2);
977            }
978            _ => panic!("Second candidate for G7(b9)(#5) should be a Scale"),
979        }
980    }
981
982    #[test]
983    fn test_half_diminished_candidates() {
984        let candidates = KnownChord::HalfDiminished(Degree::Seven).scale_candidates();
985        assert!(!candidates.is_empty(), "Cm7b5 should have scale candidates");
986
987        match &candidates[0] {
988            ScaleCandidate::Mode { kind, rank, .. } => {
989                assert_eq!(*kind, ModeKind::Locrian);
990                assert_eq!(*rank, 1);
991            }
992            _ => panic!("First candidate for Cm7b5 should be a Mode"),
993        }
994    }
995
996    #[test]
997    fn test_augmented_dominant_candidates() {
998        let candidates = KnownChord::AugmentedDominant(Degree::Seven).scale_candidates();
999        assert!(!candidates.is_empty(), "Augmented dominant should have scale candidates");
1000
1001        match &candidates[0] {
1002            ScaleCandidate::Scale { kind, rank, .. } => {
1003                assert_eq!(*kind, ScaleKind::WholeTone);
1004                assert_eq!(*rank, 1);
1005            }
1006            _ => panic!("First candidate for augmented dominant should be a Scale"),
1007        }
1008    }
1009
1010    #[test]
1011    fn test_major_chord_candidates() {
1012        let candidates = KnownChord::Major.scale_candidates();
1013        assert!(candidates.len() >= 3, "Major chord should have at least 3 candidates");
1014
1015        match &candidates[0] {
1016            ScaleCandidate::Mode { kind, rank, .. } => {
1017                assert_eq!(*kind, ModeKind::Ionian);
1018                assert_eq!(*rank, 1);
1019            }
1020            _ => panic!("First candidate for Major should be Ionian mode"),
1021        }
1022
1023        match &candidates[1] {
1024            ScaleCandidate::Scale { kind, rank, .. } => {
1025                assert_eq!(*kind, ScaleKind::MajorPentatonic);
1026                assert_eq!(*rank, 2);
1027            }
1028            _ => panic!("Second candidate for Major should be MajorPentatonic scale"),
1029        }
1030
1031        match &candidates[2] {
1032            ScaleCandidate::Mode { kind, rank, .. } => {
1033                assert_eq!(*kind, ModeKind::Lydian);
1034                assert_eq!(*rank, 3);
1035            }
1036            _ => panic!("Third candidate for Major should be Lydian mode"),
1037        }
1038    }
1039
1040    #[test]
1041    fn test_minor_chord_candidates() {
1042        let candidates = KnownChord::Minor.scale_candidates();
1043        assert!(candidates.len() >= 3, "Minor chord should have at least 3 candidates");
1044
1045        match &candidates[0] {
1046            ScaleCandidate::Mode { kind, rank, .. } => {
1047                assert_eq!(*kind, ModeKind::Aeolian);
1048                assert_eq!(*rank, 1);
1049            }
1050            _ => panic!("First candidate for Minor should be Aeolian mode"),
1051        }
1052
1053        match &candidates[1] {
1054            ScaleCandidate::Scale { kind, rank, .. } => {
1055                assert_eq!(*kind, ScaleKind::MinorPentatonic);
1056                assert_eq!(*rank, 2);
1057            }
1058            _ => panic!("Second candidate for Minor should be MinorPentatonic scale"),
1059        }
1060
1061        match &candidates[2] {
1062            ScaleCandidate::Scale { kind, rank, .. } => {
1063                assert_eq!(*kind, ScaleKind::Blues);
1064                assert_eq!(*rank, 3);
1065            }
1066            _ => panic!("Third candidate for Minor should be Blues scale"),
1067        }
1068    }
1069
1070    #[test]
1071    fn test_minor_dominant_candidates() {
1072        let candidates = KnownChord::MinorDominant(Degree::Seven).scale_candidates();
1073        assert!(candidates.len() >= 3, "Minor dominant should have at least 3 candidates");
1074
1075        match &candidates[0] {
1076            ScaleCandidate::Mode { kind, rank, .. } => {
1077                assert_eq!(*kind, ModeKind::Dorian);
1078                assert_eq!(*rank, 1);
1079            }
1080            _ => panic!("First candidate for Minor dominant should be Dorian mode"),
1081        }
1082
1083        match &candidates[1] {
1084            ScaleCandidate::Scale { kind, rank, .. } => {
1085                assert_eq!(*kind, ScaleKind::MinorPentatonic);
1086                assert_eq!(*rank, 2);
1087            }
1088            _ => panic!("Second candidate for Minor dominant should be MinorPentatonic scale"),
1089        }
1090
1091        match &candidates[2] {
1092            ScaleCandidate::Scale { kind, rank, .. } => {
1093                assert_eq!(*kind, ScaleKind::Blues);
1094                assert_eq!(*rank, 3);
1095            }
1096            _ => panic!("Third candidate for Minor dominant should be Blues scale"),
1097        }
1098    }
1099
1100    #[test]
1101    fn test_interval_candidates_kinds_and_order() {
1102        // Test that all KnownChord variants return properly ordered interval candidates
1103        let all_variants = vec![
1104            KnownChord::Unknown,
1105            KnownChord::Major,
1106            KnownChord::Minor,
1107            KnownChord::Major7,
1108            KnownChord::Dominant(Degree::Seven),
1109            KnownChord::MinorMajor7,
1110            KnownChord::MinorDominant(Degree::Seven),
1111            KnownChord::DominantSharp11(Degree::Seven),
1112            KnownChord::Augmented,
1113            KnownChord::AugmentedMajor7,
1114            KnownChord::AugmentedDominant(Degree::Seven),
1115            KnownChord::HalfDiminished(Degree::Seven),
1116            KnownChord::Diminished,
1117            KnownChord::DominantFlat9(Degree::Seven),
1118            KnownChord::DominantSharp9(Degree::Seven),
1119            KnownChord::AugmentedDominantFlat9(Degree::Seven),
1120            KnownChord::MinorDominantFlat13(Degree::Seven),
1121            KnownChord::MinorDominantFlat9Flat13(Degree::Seven),
1122            KnownChord::Sharp11,
1123        ];
1124
1125        for known_chord in all_variants {
1126            let candidates = known_chord.scale_interval_candidates();
1127
1128            // Verify ranks are sequential starting at 1
1129            for (i, candidate) in candidates.iter().enumerate() {
1130                let expected_rank = (i + 1) as u8;
1131                assert_eq!(
1132                    candidate.rank, expected_rank,
1133                    "{:?}: Expected rank {} at position {}, got {}",
1134                    known_chord, expected_rank, i, candidate.rank
1135                );
1136
1137                // Verify reason is non-empty
1138                assert!(!candidate.reason.is_empty(), "{:?}: Candidate at position {} has empty reason", known_chord, i);
1139
1140                // Verify kind matches what scale_candidates() returns
1141                let scale_candidates = known_chord.scale_candidates();
1142                if i < scale_candidates.len() {
1143                    match (&candidate.kind, &scale_candidates[i]) {
1144                        (IntervalCollectionKind::Mode(mk), ScaleCandidate::Mode { kind, .. }) => {
1145                            assert_eq!(mk, kind, "{:?}: Mode kind mismatch at position {}", known_chord, i);
1146                        }
1147                        (IntervalCollectionKind::Scale(sk), ScaleCandidate::Scale { kind, .. }) => {
1148                            assert_eq!(sk, kind, "{:?}: Scale kind mismatch at position {}", known_chord, i);
1149                        }
1150                        _ => panic!("{:?}: Kind type mismatch at position {}", known_chord, i),
1151                    }
1152                }
1153            }
1154
1155            // Verify scale_candidates() matches interval_candidates()
1156            let scale_candidates = known_chord.scale_candidates();
1157            assert_eq!(candidates.len(), scale_candidates.len(), "{:?}: Mismatch in candidate count", known_chord);
1158        }
1159    }
1160}