From 53ec28d22b62dec2d44226741c98882605298fb6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 13:29:27 +0000 Subject: [PATCH] =?UTF-8?q?Parse=20CMaj7,=20CmMaj7=20and=20C=C2=B07=20as?= =?UTF-8?q?=20the=20sevenths=20they=20name=20[patch]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DetermineSeventh matched "maj", "M7" and 'Δ' but not "Maj", so CMaj7 and CmMaj7 fell through to a dominant seventh. Its diminished-seventh branch checked only "dim", although DetermineQuality also accepts '°', so C°7 became the half-diminished chord. Accept both spellings, and assert the seventh type and pitches directly, since the misparsed chords round-trip. Fixes #281 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VeSBKr7Q99CTQ5BUJMKEct --- Semantics.Music/Chord.cs | 9 +++++++-- Semantics.Test/Music/ChordTests.cs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Semantics.Music/Chord.cs b/Semantics.Music/Chord.cs index 3c7e49d..274be80 100644 --- a/Semantics.Music/Chord.cs +++ b/Semantics.Music/Chord.cs @@ -367,7 +367,10 @@ private static bool IsMinor(string body) => private static SeventhType DetermineSeventh(string body, ChordQuality quality) { bool hasSeven = body.Contains('7'); - bool hasMaj7 = body.Contains("maj", StringComparison.Ordinal) || body.Contains("M7", StringComparison.Ordinal) || body.Contains('Δ'); + bool hasMaj7 = body.Contains("maj", StringComparison.Ordinal) + || body.Contains("Maj", StringComparison.Ordinal) + || body.Contains("M7", StringComparison.Ordinal) + || body.Contains('Δ'); // The major seventh is tested first because one body can carry both spellings: "dimmaj7" // is the diminished-major seventh, a chord distinct from "dim7", and it is what the @@ -379,7 +382,9 @@ private static SeventhType DetermineSeventh(string body, ChordQuality quality) return SeventhType.Major; } - if (quality == ChordQuality.Diminished && body.Contains("dim", StringComparison.Ordinal) && hasSeven) + // '°' is accepted here as it is for the triad, so "C°7" is the diminished seventh rather than + // falling through to a dominant seventh on a diminished triad, which is the half-diminished chord. + if (quality == ChordQuality.Diminished && (body.Contains("dim", StringComparison.Ordinal) || body.Contains('°')) && hasSeven) { return SeventhType.Diminished; } diff --git a/Semantics.Test/Music/ChordTests.cs b/Semantics.Test/Music/ChordTests.cs index f7d142f..b243300 100644 --- a/Semantics.Test/Music/ChordTests.cs +++ b/Semantics.Test/Music/ChordTests.cs @@ -69,6 +69,20 @@ public void ChordTones_DiminishedMajorSeventh_HasTheMajorSeventhNotTheDoubleFlat Assert.AreSequenceEqual([0, 3, 6, 11], actual, "Cdimmaj7 should be a diminished triad with a major seventh."); } + [TestMethod] + [DataRow("CmMaj7", ChordQuality.Minor, SeventhType.Major, new[] { 0, 3, 7, 11 })] + [DataRow("CMaj7", ChordQuality.Major, SeventhType.Major, new[] { 0, 4, 7, 11 })] + [DataRow("C°7", ChordQuality.Diminished, SeventhType.Diminished, new[] { 0, 3, 6, 9 })] + public void Parse_AlternativeSeventhSpellings_GiveTheSeventhTheyName(string symbol, ChordQuality quality, SeventhType seventh, int[] tones) + { + // A round-trip alone cannot catch these: the misparsed chord formats and re-parses + // consistently, so the seventh type and the pitches have to be asserted directly. + Chord c = Chord.Parse(symbol); + Assert.AreEqual(quality, c.Quality, symbol); + Assert.AreEqual(seventh, c.Seventh, symbol); + Assert.AreSequenceEqual(tones, [.. c.ChordTones()], symbol); + } + [TestMethod] public void Parse_HalfDiminished_IsDiminishedTriadWithDominantSeventh() {