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
Covers:#300, #322, #336, #340, #341, #344, #345, #346, #358. These are one bug cluster with shared root causes, so fix them through this design, not one at a time. Each covered issue is linked as a sub-issue here; close them from the PR(s) that implement the matching step.
Decision (maintainer, 2026-09-30)
The design below is approved as written, with these calls:
7M reads as maj7 (C7M = Cmaj7), as §3.2 proposes.
Keep the small legacy reader so previously printed text such as C79, Cmaj79, Csus47 and C6add9 still parses. That keeps this release [minor] rather than [major].
The mmaj7 spelling stays as proposed.
The new gap found while designing (Csus4#5 silently drops the #5) is fixed here too; no separate issue.
1. Scope
In scope. All of these are about how a chord symbol is read or written.
#
Bug
300
C7add13 prints C713 and reparses with an extra 9 and 11 (a seventh chord's addN is lost)
322
CM9/CM11/CM13 read as dominant, and CmM9 reads as Cm9 (M is only recognised as maj7 when a 7 follows it)
336
C(#11) prints C#11, which reparses with a C# root (an altered token is printed straight after the root)
340
Cm7#5/Cm#5/Cm+ read as augmented with a major third
341
C13#11/C13b9 get the natural and the altered degree
344
Text after the slash bass is dropped: C/Gm, C/E/G, C/Bb7
345
-/+ alteration signs read as a quality: C7-9 becomes Cm9, C7+9 becomes Caug9
346
Quality characters are accepted in any order and any number: C7M, C57, C77, C7m, Cmm7
358
The canonical text is not what anyone writes: C79, Cmaj79, Csus47, C6add9
This design also turns up a gap no issue records yet: Csus4#5 parses as Csus4 and drops the #5, because DetermineQuality returns Sus4 before it looks at the fifth. The grammar rejects it.
Out of scope. Each of these has a different root cause and should be fixed on its own.
Replace the body of TryParse with a left-to-right recursive-descent reader over a cursor, and
replace ToString with a writer that emits exactly the productions below, in canonical order.
There is no Take, no Contains and no pre-rewrite. Every character is consumed by exactly one
production, or the parse fails.
3.1 Grammar (the whole symbol, with no whitespace)
symbol = root body [ "/" bass ] EOF
root, bass = letter { accidental } ; exactly what PitchClass.TryParse accepts; bass must reach EOF (#344)
accidental = "#" | "b" | "♯" | "♭"
body = [ quality ] [ ext ] [ sus ] { modifier } ; see 3.2 for the `sus` placement
quality = minor [ augMark ] ; minor + raised fifth -> MinorSharpFive (#340)
| "dim" | "°" | "ø" | augMark | majorWord-alone
minor = "m" | "min" | "-" ; "-" is minor only here, at the body start (#345)
augMark = "aug" | "+"
ext = [ majorWord ] number ; majorWord + number -> Seventh.Major (#322)
| number "M" ; "7M" = maj7, the Brazilian/French spelling (#346)
| "Δ" ; alone = maj7
| "6" [ "/9" | "9" ] ; six / six-nine (replaces both 6/9 rewrites)
| "b6" | "♭6" ; minor-sixth colour; after the root it must be parenthesised
| "5" ; power; nothing else may follow except "/" bass
majorWord = "maj" | "Maj" | "M" | "Δ" ; "Cmaj"/"CM" alone = major triad (kept)
number = "7" | "9" | "11" | "13"
sus = "sus2" | "sus4" | "sus" ; "sus" = sus4
modifier = alteration | add | omit | "(" modifier { [","] modifier } ")"
alteration = flat ( "5" | "9" | "13" ) | sharp ( "5" | "9" | "11" )
flat = "b" | "♭" | "-" ; "-" as an alteration only after the body start (#345)
sharp = "#" | "♯" | "+" ; "+" followed by a digit; a trailing bare "+" is augMark
add = "add" ( "9" | "11" | "13" | "6" | "b6" )
omit = "no3" | "no5"
3.2 Reading rules (grammar to model)
Quality. Empty or majorWord gives Major. minor gives Minor. dim/° gives Diminished. augMark, or #5 on a major third, gives Augmented. minor+augMark, or minor+#5, gives
the new MinorSharpFive. b5 on a major third gives MajorFlatFive, and b5 on a minor third
gives Diminished (m7b5). ø/ø7 give Diminished with a Dominant seventh. sus/5 give Sus2/Sus4/Power. sus combined with any quality prefix is rejected, and so is b5/#5 on sus or power (the model cannot hold it).
Seventh. A number implies a seventh: Major if a majorWord/M marks it, Diminished if the
quality is Diminished via dim/°, and otherwise Dominant.
Order. The order of modifiers is free on input, because real charts write both C7#9b13 and C7b13#9. Quality, ext and sus positions are fixed. sus may come before ext only to accept the
legacy Csus47 (see compat below).
Six-nine.6/9 is read inside ext, before a / is ever considered as a bass. So C6/9/G
needs no rewrite.
Legacy compat. This covers only the text the 5.x printer emitted, pinned by one test and to be
dropped at the next [major]. 7 or maj7 followed directly by 9|11|13 reads as that number
(C79 reads as C9, Cmaj713 as Cmaj13), and sus before ext is allowed (Csus47). Without
this, a chart saved by today's library would stop loading.
Quality text: Major "", Minor m, Augmented aug, MinorSharpFive m with #5 in the
alteration slot, MajorFlatFive as "" with b5, Diminished+Dominant as m with b5. Diminished
with a Diminished or no seventh is dim/dim7, and Diminished+Major is dimmaj7 (as today). The
major seventh word is maj, so the output is Cmaj9, Cmmaj9, Caugmaj7.
Round trip.Parse(c.ToString()) == c for every c with IsExpressible(c). That is an
internal predicate: Power has no seventh, sixth, tensions or omissions, and a Diminished SeventhType only appears on Diminished quality. Every chord Parse returns is expressible.
Canonical text.Parse(s).ToString() == s for every string in the canonical corpus.
4. Public API impact
The only surface change is that ChordQuality.MinorSharpFive (third 3, fifth 8) is appended at the
end of the enum, which is additive. Consumers to update are ChordTones, Key.QualitySuffix/RomanNumeralOf (lower-case numeral, suffix #5), KeyInference
(non-comparable, like MajorFlatFive) and Progression.Chromatic.
Tag: [minor]. This follows the precedent set by fecd433 for MajorFlatFive and by the
rejection fixes, which shipped as patches. The legacy-compat production is what keeps previously
saved text loadable, so the release does not have to be [major].
Alternative, not recommended now. Split Quality into Third (None/Sus2/Minor/Major/Sus4)
and Fifth (None/Flat/Perfect/Sharp) axes. That is cleaner, but it is [major] for every package
(the repo ships one version), and the grammar above does not need it.
5. Test plan
New Semantics.Test/Music/ChordGrammarTests.cs: one [DataRow] table per concern, each row checking (input, expected ToString, expected ChordTones) or (input, rejected). Every case asserts the tones, because a round-trip-only check misses #341.
Add a text assertion over a canonical corpus (all existing entries in canonical form, plus every
"prints as" value above).
Add EveryExpressibleChordRoundTrips. It enumerates roots {C, F#} × 9 qualities × 4 sevenths ×
3 sixths × 128 tension sets × 4 omissions × bass {none, G} (about 220k values; each check is
microseconds), filters on IsExpressible, and asserts Parse(ToString(c)) == c.
Keep DiminishedMajorSeventhRoundTrips.
Run ProgressionRoundTripTests, SectionRoundTripTests and ArrangementRoundTripTests unchanged.
Update any expected strings to the new canonical text.
6. Implementation order
Write ChordGrammarTests and the enumerated round-trip test first, and confirm they fail for the
issues above.
Append ChordQuality.MinorSharpFive and handle it in ChordTones, Key.QualitySuffix, RomanNumeralOf, Progression.KeyInference and Progression.Chromatic.
Add internal sealed class ChordSymbolReader in Semantics.Music/ChordSymbolReader.cs: a cursor
with Peek/TryTake(string), and one method per production in §3.1, in grammar order. Read
root/bass through Notation and require the bass to reach EOF. Collect raw facts into a builder
(quality marks, ext number, major marker, sus, alterations, adds, omits), then apply §3.2 once at
the end.
Point Chord.TryParse at the reader. Delete TryReadRoot, TryRewriteSixNine, RewriteSixNine, RewriteHalfDiminished, IsQualityVocabulary, ConsumeModifiers/Tensions/FifthAlteration/ Omissions, DetermineQuality/Seventh, IsMinor, IsMajorSeventhWord, ApplyExtensions and Take.
Add internal static class ChordSymbolWriter implementing §3.3, plus internal static bool IsExpressible(Chord). Chord.ToString() delegates to it. Delete the Append* helpers.
Update the XML docs on Parse/ToString to state the grammar and both invariants. Add the
grammar to Semantics.Music/README.md.
Run the full suite in Release. Fix expected strings in the Progression, Section and Arrangement
tests only where the new canonical text is the intended output.
Covers: #300, #322, #336, #340, #341, #344, #345, #346, #358. These are one bug cluster with shared root causes, so fix them through this design, not one at a time. Each covered issue is linked as a sub-issue here; close them from the PR(s) that implement the matching step.
Decision (maintainer, 2026-09-30)
The design below is approved as written, with these calls:
7Mreads as maj7 (C7M=Cmaj7), as §3.2 proposes.C79,Cmaj79,Csus47andC6add9still parses. That keeps this release[minor]rather than[major].mmaj7spelling stays as proposed.Csus4#5silently drops the#5) is fixed here too; no separate issue.1. Scope
In scope. All of these are about how a chord symbol is read or written.
C7add13printsC713and reparses with an extra 9 and 11 (a seventh chord'saddNis lost)CM9/CM11/CM13read as dominant, andCmM9reads asCm9(Mis only recognised as maj7 when a7follows it)C(#11)printsC#11, which reparses with a C# root (an altered token is printed straight after the root)Cm7#5/Cm#5/Cm+read as augmented with a major thirdC13#11/C13b9get the natural and the altered degreeC/Gm,C/E/G,C/Bb7-/+alteration signs read as a quality:C7-9becomes Cm9,C7+9becomes Caug9C7M,C57,C77,C7m,Cmm7C79,Cmaj79,Csus47,C6add9This design also turns up a gap no issue records yet:
Csus4#5parses asCsus4and drops the#5, becauseDetermineQualityreturnsSus4before it looks at the fifth. The grammar rejects it.Out of scope. Each of these has a different root cause and should be fixed on its own.
ChromaticChords: this is key analysis (harmonic-minor V), shared with Key.RomanNumeralOf labels the minor-key leading-tone chord as a flattened tonic: G#dim in A minor is "bi°", not "#vii°" #304 and Secondary dominants in non-major modes get minor-key numerals ("V/ii°" in G mixolydian), and octatonic keys wrap to "V/i" #301. The chord is read correctly.Arrangementblock splitting: this is the chart layout, not a symbol. It is a two-line fix inArrangement.TryParse.Durationaccepts zero and negative lengths, soNote,Rest,ChordEventandProgression.Parseaccept"C4:-1/4:v64","R:0/4","C@-1/4"#353Durationaccepting ≤ 0: this is a value-type invariant. It also coversNote,RestandChordEvent.Progression.Parsesilently drops beat slashes after aChord@n/dcell:"4/4 C@1/2 / / | G"parses as C for 2 beats with G on beat 3 #328Progressionbeat slashes after@n/d: this is the chart-cell grammar. It has the same "no grammar" problem one level up and would be a good follow-up.Voiceinversions, Form.Of labels the 27th distinct section '[' so Form.Parse(form.ToString()) fails to round-trip #314/Section.Parseaccepts numbers and comma lists as section types ([Verse, Chorus]parses as Chorus,[42]as undefined 42) #318/Pitch.TryParse("C1073741823") succeeds as C-1: octave arithmetic wraps, and "C 4 " is accepted too #307 (Form/Section/Pitch parsers): these are unrelated.Bbprints asA#) is aPitchClassquestion. The model holds pitch classes, not note names.2. Root causes
Take()removes the firstoccurrence of a token wherever it sits, and
Contains/StartsWithread the quality fromwhatever characters happen to be left. That is why position means nothing (Chord.Parse accepts quality characters in any order and any number: "C7M", "C57", "C77" and "C7m" all parse as C7 #346), why
-and+anywhere change the quality (Chord.Parse reads "-"/"+" alteration signs as chord quality: "C7-9" becomes Cm9, "C7-5" becomes Cm7, "Cmaj7-5" becomes CmMaj7, "C7+9" becomes Caug9 #345), and why leftover text after the bass is never checked (Chord.Parse drops everything after the slash-bass note: "C/Gm", "C/E/G" and "C/Bb7" parse as "C/G", "C/E" and "C/A#" #344).
Each fix so far has added a rewrite (
RewriteSixNine,RewriteHalfDiminished,TryRewriteSixNine) or a special case (IsMajorSeventhWord), and each one leaves the nextspelling uncovered (Chord.Parse reads "CM9", "CM11" and "CM13" as dominant chords (Bb, not B), and "CmM9" as a plain Cm9 #322).
ApplyExtensionsturns13into 9+11+13 unconditionally (Chord.Parse stacks the natural 9th/11th under an altered one: "C13#11" sounds both F and F#, "C13b9" both Db and D #341). The printer inverts this with "highest flag wins", whichcannot express a gap in the stack (Chord round-trip breaks for a seventh with add11/add13: "C7add13" prints as "C713", which reparses as C13 with a 9th and 11th #300) and writes the number after
7instead of in its place(Chord.ToString() writes non-standard symbols: G9 → "G79", Gmaj9 → "Gmaj79", G13 → "G713", G7sus4 → "Gsus47", C6/9 → "C6add9" #358).
Append*pieces, so it can write text the parser reads differently: an accidental after the root (Chord.ToString() writes an altered tension straight after the root: "C(#11)" prints as "C#11" and reparses as a C# dominant eleventh #336),
and
susbefore the number (Chord.ToString() writes non-standard symbols: G9 → "G79", Gmaj9 → "Gmaj79", G13 → "G713", G7sus4 → "Gsus47", C6/9 → "C6add9" #358). The round-trip test checks only chords that came fromParse, so a stable round trip of the wrong chord passes (Chord.Parse stacks the natural 9th/11th under an altered one: "C13#11" sounds both F and F#, "C13b9" both Db and D #341).ChordQuality.MajorFlatFiveexists but itsMinorSharpFivecounterpart does not, som#5has nowhere to go (Chord.Parse reads "Cm7#5", "Cm#5" and "Cm+" as augmented chords with a major third (Cm7#5 comes back as Caug7) #340).3. Design
Replace the body of
TryParsewith a left-to-right recursive-descent reader over a cursor, andreplace
ToStringwith a writer that emits exactly the productions below, in canonical order.There is no
Take, noContainsand no pre-rewrite. Every character is consumed by exactly oneproduction, or the parse fails.
3.1 Grammar (the whole symbol, with no whitespace)
3.2 Reading rules (grammar to model)
majorWordgives Major.minorgives Minor.dim/°gives Diminished.augMark, or#5on a major third, gives Augmented.minor+augMark, orminor+#5, givesthe new
MinorSharpFive.b5on a major third gives MajorFlatFive, andb5on a minor thirdgives Diminished (
m7b5).ø/ø7give Diminished with a Dominant seventh.sus/5give Sus2/Sus4/Power.suscombined with any quality prefix is rejected, and so isb5/#5on sus or power (the model cannot hold it).numberimplies a seventh: Major if amajorWord/Mmarks it, Diminished if thequality is Diminished via
dim/°, and otherwise Dominant.numberN ≥ 9, add the natural degree for each of 9..N unless the same degree isaltered:
b9/#9suppress 9, and#11suppresses 11 (Chord.Parse stacks the natural 9th/11th under an altered one: "C13#11" sounds both F and F#, "C13b9" both Db and D #341).13withb13is rejected.addadds exactly one natural degree (Chord round-trip breaks for a seventh with add11/add13: "C7add13" prints as "C713", which reparses as C13 with a 9th and 11th #300).b9b9),b5+#5,a repeated omission, a second
sus, and a secondext(C77,C57,C7m,Cmm7) (Chord.Parse accepts quality characters in any order and any number: "C7M", "C57", "C77" and "C7m" all parse as C7 #346).modifiers is free on input, because real charts write bothC7#9b13andC7b13#9. Quality, ext and sus positions are fixed.susmay come beforeextonly to accept thelegacy
Csus47(see compat below).6/9is read insideext, before a/is ever considered as a bass. SoC6/9/Gneeds no rewrite.
dropped at the next
[major].7ormaj7followed directly by9|11|13reads as that number(
C79reads asC9,Cmaj713asCmaj13), andsusbeforeextis allowed (Csus47). Withoutthis, a chart saved by today's library would stop loading.
3.3 Canonical
ToString(model to grammar)Token order:
root,quality,ext,sus,alterations(b5 #5 b9 #9 #11 b13),adds,omits,/bass."", Minorm, Augmentedaug, MinorSharpFivemwith#5in thealteration slot, MajorFlatFive as
""withb5, Diminished+Dominant asmwithb5. Diminishedwith a Diminished or no seventh is
dim/dim7, and Diminished+Major isdimmaj7(as today). Themajor seventh word is
maj, so the output isCmaj9,Cmmaj9,Caugmaj7.N= the highest of 9/11/13 whose natural is present and every degree from 9 up to N ispresent, whether natural or altered. Print
N(ormajN) in place of7. Examples:C13b9,C9#11,C13#11, andC7#9because there is no natural 9. Any remaining natural degree prints asaddN, which givesC7add13andC9add13(Chord round-trip breaks for a seventh with add11/add13: "C7add13" prints as "C713", which reparses as C13 with a 9th and 11th #300, Chord.ToString() writes non-standard symbols: G9 → "G79", Gmaj9 → "Gmaj79", G13 → "G713", G7sus4 → "Gsus47", C6/9 → "C6add9" #358).C7sus4,C9sus4,Csus2(Chord.ToString() writes non-standard symbols: G9 → "G79", Gmaj9 → "Gmaj79", G13 → "G713", G7sus4 → "Gsus47", C6/9 → "C6add9" #358). Sixth without a seventh is6,6/9,m6/9orb6. With a seventh it isadd6/addb6. This is the only reason those twoadds exist:it makes the writer total.
(
b5,b6,b9,#9,#11,b13), wrap the alteration run in parentheses:C(b5),C(#11),C(b9#11),C(b6). Alterations that follow a word or digit need no parentheses.5. Bass prints/+Bass.Name.3.4 Invariants
Parse(c.ToString()) == cfor everycwithIsExpressible(c). That is aninternal predicate: Power has no seventh, sixth, tensions or omissions, and a Diminished
SeventhTypeonly appears on Diminished quality. Every chordParsereturns is expressible.Parse(s).ToString() == sfor every string in the canonical corpus.4. Public API impact
ChordQuality.MinorSharpFive(third 3, fifth 8) is appended at theend of the enum, which is additive. Consumers to update are
ChordTones,Key.QualitySuffix/RomanNumeralOf(lower-case numeral, suffix#5),KeyInference(non-comparable, like MajorFlatFive) and
Progression.Chromatic.C13b13,Csus4#5,Cmsus4and duplicatealterations.
ToStringoutput changes (Chord.ToString() writes non-standard symbols: G9 → "G79", Gmaj9 → "Gmaj79", G13 → "G713", G7sus4 → "Gsus47", C6/9 → "C6add9" #358), andProgression,SectionandArrangementToStringchange with it.[minor]. This follows the precedent set by fecd433 for MajorFlatFive and by therejection fixes, which shipped as patches. The legacy-compat production is what keeps previously
saved text loadable, so the release does not have to be
[major].QualityintoThird(None/Sus2/Minor/Major/Sus4)and
Fifth(None/Flat/Perfect/Sharp) axes. That is cleaner, but it is[major]for every package(the repo ships one version), and the grammar above does not need it.
5. Test plan
New
Semantics.Test/Music/ChordGrammarTests.cs: one[DataRow]table per concern, each row checking(input, expected ToString, expected ChordTones)or(input, rejected). Every case asserts thetones, because a round-trip-only check misses #341.
C7add13→C7add13[0,4,7,10,21];C7add11;C9add13→[0,4,7,10,14,21];Cmaj7add13CM9→Cmaj9[0,4,7,11,14];CM11;CM13;CmM9→Cmmaj9[0,3,7,11,14];CM7,CΔ9,CMunchangedC(#11)→C(#11)[0,4,7,18];C(b9),C(#9),C(b13),C(b6),B(b9);Cadd9(#11)→C(#11)add9Cm7#5→Cm7#5[0,3,8,10];Cm#5,Cm+,Cmaug7→Cm7#5;C7#5,Caug,C+unchangedC13b9→[0,4,7,10,13,17,21];C13#9,C13#11,C11b9,C13b9#11;C13/C11/C9unchanged;C13b13rejectedC/Gm,C/E/G,Am/G/F,C7/G#m7b5,C/Bb7,C/Gxyz; accepted:C/G,Dm7/G,C6/9,Cm6/9,C6/9/G,Db/CbC7-9,C7(-9)→C7b9[0,4,7,10,13];C7-5→C7b5;Cmaj7-5;C7-13;C7+9→C7#9; unchanged:C-,C-7,C-9,C-7b5,C+,C7+,C7+5C57,C77,C7m,Cmm7;C7M→Cmaj7C9,Cm9,Cmaj9,Caug9,C11,C13,Cmaj13,C7sus4,C9sus4,C6/9,Cm6/9print as themselvesCsus4#5rejected;Cmsus4rejected;C7b9b9rejected;C7b5#5rejectedC79→C9,Cmaj79→Cmaj9,C713→C13,Csus47→C7sus4,C6add9→C6/9,Cmmaj7readIn
ChordRoundTripTests:"prints as" value above).
EveryExpressibleChordRoundTrips. It enumerates roots {C, F#} × 9 qualities × 4 sevenths ×3 sixths × 128 tension sets × 4 omissions × bass {none, G} (about 220k values; each check is
microseconds), filters on
IsExpressible, and assertsParse(ToString(c)) == c.DiminishedMajorSeventhRoundTrips.ProgressionRoundTripTests,SectionRoundTripTestsandArrangementRoundTripTestsunchanged.Update any expected strings to the new canonical text.
6. Implementation order
ChordGrammarTestsand the enumerated round-trip test first, and confirm they fail for theissues above.
ChordQuality.MinorSharpFiveand handle it inChordTones,Key.QualitySuffix,RomanNumeralOf,Progression.KeyInferenceandProgression.Chromatic.internal sealed class ChordSymbolReaderinSemantics.Music/ChordSymbolReader.cs: a cursorwith
Peek/TryTake(string), and one method per production in §3.1, in grammar order. Readroot/bass through
Notationand require the bass to reach EOF. Collect raw facts into a builder(quality marks, ext number, major marker, sus, alterations, adds, omits), then apply §3.2 once at
the end.
Chord.TryParseat the reader. DeleteTryReadRoot,TryRewriteSixNine,RewriteSixNine,RewriteHalfDiminished,IsQualityVocabulary,ConsumeModifiers/Tensions/FifthAlteration/Omissions,DetermineQuality/Seventh,IsMinor,IsMajorSeventhWord,ApplyExtensionsandTake.internal static class ChordSymbolWriterimplementing §3.3, plusinternal static bool IsExpressible(Chord).Chord.ToString()delegates to it. Delete theAppend*helpers.Parse/ToStringto state the grammar and both invariants. Add thegrammar to
Semantics.Music/README.md.tests only where the new canonical text is the intended output.
-p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props).Commit as
[minor], referencing Chord round-trip breaks for a seventh with add11/add13: "C7add13" prints as "C713", which reparses as C13 with a 9th and 11th #300 Chord.Parse reads "CM9", "CM11" and "CM13" as dominant chords (Bb, not B), and "CmM9" as a plain Cm9 #322 Chord.ToString() writes an altered tension straight after the root: "C(#11)" prints as "C#11" and reparses as a C# dominant eleventh #336 Chord.Parse reads "Cm7#5", "Cm#5" and "Cm+" as augmented chords with a major third (Cm7#5 comes back as Caug7) #340 Chord.Parse stacks the natural 9th/11th under an altered one: "C13#11" sounds both F and F#, "C13b9" both Db and D #341 Chord.Parse drops everything after the slash-bass note: "C/Gm", "C/E/G" and "C/Bb7" parse as "C/G", "C/E" and "C/A#" #344 Chord.Parse reads "-"/"+" alteration signs as chord quality: "C7-9" becomes Cm9, "C7-5" becomes Cm7, "Cmaj7-5" becomes CmMaj7, "C7+9" becomes Caug9 #345 Chord.Parse accepts quality characters in any order and any number: "C7M", "C57", "C77" and "C7m" all parse as C7 #346 Chord.ToString() writes non-standard symbols: G9 → "G79", Gmaj9 → "Gmaj79", G13 → "G713", G7sus4 → "Gsus47", C6/9 → "C6add9" #358.