From 6c38aea1d0f4f544ecdeb279fed4f0c437958c0b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:34:02 +0000 Subject: [PATCH 1/2] [patch] Parse "minor" and "natural_minor" as the Aeolian mode The mode alias table only mapped ionian to major, so the usual spelling of a minor key failed: Key.TryParse("A minor") returned false and Mode.Parse("minor") threw. Map minor and natural_minor to aeolian, so a parsed "A minor" equals A aeolian and still prints as "A aeolian". Fixes #302 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RsRLSwgcnfRH2EGHrQMMxb --- Semantics.Music/Mode.cs | 4 +++- Semantics.Test/Music/KeyTests.cs | 9 +++++++++ Semantics.Test/Music/ModeTests.cs | 11 +++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Semantics.Music/Mode.cs b/Semantics.Music/Mode.cs index 0ab05e6..0b0ac0c 100644 --- a/Semantics.Music/Mode.cs +++ b/Semantics.Music/Mode.cs @@ -59,6 +59,8 @@ public sealed record Mode private static readonly Dictionary Aliases = new(StringComparer.OrdinalIgnoreCase) { ["ionian"] = "major", + ["minor"] = "aeolian", + ["natural_minor"] = "aeolian", }; /// Gets the canonical lower-case name of the mode. @@ -174,7 +176,7 @@ 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 (). + /// An alias parses to its canonical mode: "ionian" to , and "minor" or "natural_minor" to . /// The mode name. /// The matching mode, or null on failure. /// when the name is a known mode. diff --git a/Semantics.Test/Music/KeyTests.cs b/Semantics.Test/Music/KeyTests.cs index 421d505..45a1903 100644 --- a/Semantics.Test/Music/KeyTests.cs +++ b/Semantics.Test/Music/KeyTests.cs @@ -103,6 +103,15 @@ public void Parse_IonianSpelling_IsTheSameKeyAsMajor() Assert.AreEqual(CMajor, Key.Parse("C ionian")); } + [TestMethod] + public void Parse_MinorSpelling_IsTheSameKeyAsAeolian() + { + Key aMinor = Key.Create(PitchClass.Create(NoteLetter.A, Accidental.Natural), Mode.Aeolian); + Assert.AreEqual(aMinor, Key.Parse("A minor")); + Assert.IsTrue(Key.TryParse("A minor", out Key? parsed)); + Assert.AreEqual("A aeolian", parsed!.ToString()); + } + [TestMethod] public void TryParseFailsOnUnknownMode() { diff --git a/Semantics.Test/Music/ModeTests.cs b/Semantics.Test/Music/ModeTests.cs index 4b0475c..f6a27e6 100644 --- a/Semantics.Test/Music/ModeTests.cs +++ b/Semantics.Test/Music/ModeTests.cs @@ -46,6 +46,17 @@ public void TryParse_IonianAlias_EqualsMajor() Assert.AreEqual(Mode.Major, result); } + [TestMethod] + [DataRow("minor")] + [DataRow("Minor")] + [DataRow("natural_minor")] + public void Parse_MinorAlias_EqualsAeolian(string name) + { + Mode parsed = Mode.Parse(name); + Assert.AreEqual(Mode.Aeolian, parsed); + Assert.AreEqual("aeolian", parsed.ToString()); + } + [TestMethod] public void Parse_RejectsUnknown() { From ef6c650d9096a86fb60b50dd9b39252ba0259068 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:49:55 +0000 Subject: [PATCH 2/2] [patch] Name the aeolian mode once The minor aliases made "aeolian" the fourth copy of the same literal in Mode.cs, which SonarCloud's S1192 check failed the quality gate on. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RsRLSwgcnfRH2EGHrQMMxb --- Semantics.Music/Mode.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Semantics.Music/Mode.cs b/Semantics.Music/Mode.cs index 0b0ac0c..2cae038 100644 --- a/Semantics.Music/Mode.cs +++ b/Semantics.Music/Mode.cs @@ -10,6 +10,9 @@ namespace ktsu.Semantics.Music; /// public sealed record Mode { + // Named once because the shape table, its aliases and the static instance all refer to it. + private const string AeolianName = "aeolian"; + private static readonly Dictionary Shapes = new(StringComparer.OrdinalIgnoreCase) { // Diatonic modes. @@ -18,7 +21,7 @@ public sealed record Mode ["phrygian"] = [0, 1, 3, 5, 7, 8, 10], ["lydian"] = [0, 2, 4, 6, 7, 9, 11], ["mixolydian"] = [0, 2, 4, 5, 7, 9, 10], - ["aeolian"] = [0, 2, 3, 5, 7, 8, 10], + [AeolianName] = [0, 2, 3, 5, 7, 8, 10], ["locrian"] = [0, 1, 3, 5, 6, 8, 10], // Minor scales. @@ -59,8 +62,8 @@ public sealed record Mode private static readonly Dictionary Aliases = new(StringComparer.OrdinalIgnoreCase) { ["ionian"] = "major", - ["minor"] = "aeolian", - ["natural_minor"] = "aeolian", + ["minor"] = AeolianName, + ["natural_minor"] = AeolianName, }; /// Gets the canonical lower-case name of the mode. @@ -91,7 +94,7 @@ public sealed record Mode public static Mode Mixolydian => new() { Name = "mixolydian" }; /// The Aeolian (natural minor) mode. - public static Mode Aeolian => new() { Name = "aeolian" }; + public static Mode Aeolian => new() { Name = AeolianName }; /// The Locrian mode. public static Mode Locrian => new() { Name = "locrian" };