diff --git a/Semantics.Music/Mode.cs b/Semantics.Music/Mode.cs index 681db41..0ab05e6 100644 --- a/Semantics.Music/Mode.cs +++ b/Semantics.Music/Mode.cs @@ -14,7 +14,6 @@ public sealed record Mode { // Diatonic modes. ["major"] = [0, 2, 4, 5, 7, 9, 11], - ["ionian"] = [0, 2, 4, 5, 7, 9, 11], ["dorian"] = [0, 2, 3, 5, 7, 9, 10], ["phrygian"] = [0, 1, 3, 5, 7, 8, 10], ["lydian"] = [0, 2, 4, 6, 7, 9, 11], @@ -55,6 +54,13 @@ public sealed record Mode ["blues_major"] = [0, 2, 3, 4, 7, 9], }; + // Alternative spellings that name the same mode as a shape above. Parsing maps them to the + // canonical name, so a parsed alias equals the static instance it aliases. + private static readonly Dictionary Aliases = new(StringComparer.OrdinalIgnoreCase) + { + ["ionian"] = "major", + }; + /// Gets the canonical lower-case name of the mode. public string Name { get; private init; } = "major"; @@ -168,11 +174,17 @@ public static Mode Parse(string name) } /// Tries to parse a mode by name, case-insensitively. + /// An alias such as "ionian" parses to its canonical mode (). /// The mode name. /// The matching mode, or null on failure. /// when the name is a known mode. public static bool TryParse(string? name, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Mode? result) { + if (name is not null && Aliases.TryGetValue(name, out string? canonical)) + { + name = canonical; + } + if (name is not null && Shapes.ContainsKey(name)) { result = new() { Name = name.ToLowerInvariant() }; diff --git a/Semantics.Test/Music/ChromaticAnalysisTests.cs b/Semantics.Test/Music/ChromaticAnalysisTests.cs index 05cedee..50360e7 100644 --- a/Semantics.Test/Music/ChromaticAnalysisTests.cs +++ b/Semantics.Test/Music/ChromaticAnalysisTests.cs @@ -48,6 +48,21 @@ public void ChromaticChords_DetectsBorrowedMinorSubdominant() Assert.AreEqual("from parallel minor", analyses[0].Detail); } + [TestMethod] + public void ChromaticChords_ParsedIonianKey_IsAnalyzedAsMajor() + { + Key ionian = Key.Parse("C ionian"); + + System.Collections.Generic.IReadOnlyList secondary = + Progression.Parse("4/4 C | D7 | G7 | C").ChromaticChords(ionian); + Assert.AreEqual("V/V", secondary[0].Detail); + + System.Collections.Generic.IReadOnlyList borrowed = + Progression.Parse("4/4 C | Fm | C").ChromaticChords(ionian); + Assert.AreEqual(ChromaticKind.BorrowedChord, borrowed[0].Kind); + Assert.AreEqual("from parallel minor", borrowed[0].Detail); + } + [TestMethod] public void ChromaticAnalysis_Create_RejectsNegativeIndex() => _ = Assert.ThrowsExactly(() => ChromaticAnalysis.Create(-1, ChromaticKind.Chromatic, null)); diff --git a/Semantics.Test/Music/KeyTests.cs b/Semantics.Test/Music/KeyTests.cs index ed365e3..39de609 100644 --- a/Semantics.Test/Music/KeyTests.cs +++ b/Semantics.Test/Music/KeyTests.cs @@ -96,6 +96,13 @@ public void RoundTrip() Assert.AreEqual(k, Key.Parse(k.ToString())); } + [TestMethod] + public void Parse_IonianSpelling_IsTheSameKeyAsMajor() + { + Assert.AreEqual(Key.Parse("C major"), Key.Parse("C ionian")); + Assert.AreEqual(CMajor, Key.Parse("C ionian")); + } + [TestMethod] public void TryParseFailsOnUnknownMode() { diff --git a/Semantics.Test/Music/ModeTests.cs b/Semantics.Test/Music/ModeTests.cs index bf8ea91..4b0475c 100644 --- a/Semantics.Test/Music/ModeTests.cs +++ b/Semantics.Test/Music/ModeTests.cs @@ -27,6 +27,25 @@ public void Parse_IsCaseInsensitive_AndEqualsStaticInstance() Assert.AreEqual(Mode.Lydian, Mode.Parse("LYDIAN")); } + [TestMethod] + [DataRow("ionian")] + [DataRow("Ionian")] + [DataRow("IONIAN")] + public void Parse_IonianAlias_EqualsMajor(string name) + { + Mode parsed = Mode.Parse(name); + Assert.AreEqual(Mode.Ionian, parsed); + Assert.AreEqual(Mode.Major, parsed); + Assert.AreEqual("major", parsed.ToString()); + } + + [TestMethod] + public void TryParse_IonianAlias_EqualsMajor() + { + Assert.IsTrue(Mode.TryParse("ionian", out Mode? result)); + Assert.AreEqual(Mode.Major, result); + } + [TestMethod] public void Parse_RejectsUnknown() {