You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Chord.Parse rejects Cmaj7sus4 / Cmaj9sus4 / Cmaj7sus2, yet Chord.ToString() prints exactly those symbols, so a Progression holding one cannot be re-parsed #368
A suspended chord with a major seventh is a valid Chord: new Chord { Root = C, Quality = Sus4, Seventh = Major } has tones [0,5,7,11]. ToString() prints it as Cmaj7sus4, but Chord.Parse rejects that text and every other spelling of the chord. These inputs all make TryParse return false: Cmaj7sus4, CM7sus4, Cmaj7sus, Cmaj9sus4, Cmaj7sus2 and Cmaj7(sus4). C7sus4 is accepted, so only the major-seventh case is missing.
Confirmed by compiling Semantics.Music at 8d68f2a into a scratch console app:
The roman-numeral path is broken the same way. In C major, Key.RomanNumeralOf(Cmaj7sus4) returns "Isus4maj7", and Key.ChordFromRomanNumeral("Isus4maj7") throws FormatException: Invalid chord symbol 'Csus4maj7'.
Why it matters
Cmaj7sus4 is an ordinary symbol in jazz and pop charts, and a chart containing it cannot be loaded.
A Progression, Section or Arrangement built in code with this chord produces ToString() text that its own Parse rejects, which breaks the advertised round-trip contract.
Semantics.Music/ChordSymbolReader.cs:601-602: HasInvalidSuspension() includes MajorMarker, so it rejects maj7sus4.
Semantics.Music/ChordSymbolWriter.cs:127-130: HasSupportedStructure treats Sus2/Sus4 with SeventhType.Major as inexpressible. That is why EveryExpressibleChordRoundTrips (Semantics.Test/Music/ChordRoundTripTests.cs) skips this case instead of catching it.
Semantics.Music/Key.cs:232-259: QualitySuffix writes sus4 before maj7 (Isus4maj7).
Suggested fix / acceptance criteria
Drop MajorMarker from HasInvalidSuspension(), while still rejecting m/dim/aug/+ with sus.
Remove the Sus + Major-seventh exclusion from ChordSymbolWriter.HasSupportedStructure, so the exhaustive round-trip test covers it.
These parse to the right tones and round-trip through ToString():
Chord.Parse("Cmaj7sus4") → Sus4 + Major seventh, [0,5,7,11]
Cmaj9sus4 → [0,5,7,11,14]
CM7sus2 → [0,2,7,11]
Key.QualitySuffix emits the seventh before the suspension (Imaj7sus4), so ChordFromRomanNumeral(RomanNumeralOf(c)) works for these chords.
New cases are added to ChordGrammarTests (with tones asserted) and to the ChordRoundTripTests corpus.
Priority: Medium. Cmaj7sus4 is a common chart symbol, and a Progression holding one cannot be re-parsed from its own ToString().
Area / suggested owner:Semantics.Music chord grammar: ChordSymbolReader.HasInvalidSuspension, ChordSymbolWriter.HasSupportedStructure, and Key.QualitySuffix.
Next step: Drop the MajorMarker exclusion and the writer-side skip, so that EveryExpressibleChordRoundTrips covers this case. Then reorder QualitySuffix to maj7sus4.
What's wrong
A suspended chord with a major seventh is a valid
Chord:new Chord { Root = C, Quality = Sus4, Seventh = Major }has tones[0,5,7,11].ToString()prints it asCmaj7sus4, butChord.Parserejects that text and every other spelling of the chord. These inputs all makeTryParsereturn false:Cmaj7sus4,CM7sus4,Cmaj7sus,Cmaj9sus4,Cmaj7sus2andCmaj7(sus4).C7sus4is accepted, so only the major-seventh case is missing.Confirmed by compiling Semantics.Music at 8d68f2a into a scratch console app:
The roman-numeral path is broken the same way. In C major,
Key.RomanNumeralOf(Cmaj7sus4)returns"Isus4maj7", andKey.ChordFromRomanNumeral("Isus4maj7")throwsFormatException: Invalid chord symbol 'Csus4maj7'.Why it matters
Cmaj7sus4is an ordinary symbol in jazz and pop charts, and a chart containing it cannot be loaded.Progression,SectionorArrangementbuilt in code with this chord producesToString()text that its ownParserejects, which breaks the advertised round-trip contract.Semantics.Music/README.md(lines 90–98) givesbody = [ quality ] [ extension ] [ suspension ]withextension = [ majorWord ] number, which acceptsmaj7followed bysus4. Redesign chord symbols: one grammar for Chord.Parse and Chord.ToString (fixes #300 #322 #336 #340 #341 #344 #345 #346 #358) #360 rejected sus only when combined with a quality (m/dim/aug), andmajbelongs to the extension, not the quality.Where
Semantics.Music/ChordSymbolReader.cs:601-602:HasInvalidSuspension()includesMajorMarker, so it rejectsmaj7sus4.Semantics.Music/ChordSymbolWriter.cs:127-130:HasSupportedStructuretreats Sus2/Sus4 withSeventhType.Majoras inexpressible. That is whyEveryExpressibleChordRoundTrips(Semantics.Test/Music/ChordRoundTripTests.cs) skips this case instead of catching it.Semantics.Music/Key.cs:232-259:QualitySuffixwritessus4beforemaj7(Isus4maj7).Suggested fix / acceptance criteria
MajorMarkerfromHasInvalidSuspension(), while still rejectingm/dim/aug/+with sus.ChordSymbolWriter.HasSupportedStructure, so the exhaustive round-trip test covers it.ToString():Chord.Parse("Cmaj7sus4")→ Sus4 + Major seventh,[0,5,7,11]Cmaj9sus4→[0,5,7,11,14]CM7sus2→[0,2,7,11]Key.QualitySuffixemits the seventh before the suspension (Imaj7sus4), soChordFromRomanNumeral(RomanNumeralOf(c))works for these chords.ChordGrammarTests(with tones asserted) and to theChordRoundTripTestscorpus.