From 96d022e4a26051422a85045ab2bdf3fe42468134 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:26:09 +0000 Subject: [PATCH] Refuse a form with more distinct sections than letters [patch] Form.Of labelled the 27th structurally distinct section '[', which Form.Parse rejects, so Parse(ToString()) failed for a Form the library produced. Of now throws ArgumentException past 26 distinct sections, exposed as Form.MaxDistinctSections. Fixes ktsu-dev/Semantics#314 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SWJoWcEC3BuVS7pSPco7mA --- Semantics.Music/Arrangement.cs | 3 +++ Semantics.Music/Form.cs | 17 +++++++++++++++++ Semantics.Test/Music/FormTests.cs | 29 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/Semantics.Music/Arrangement.cs b/Semantics.Music/Arrangement.cs index f778c1ea..1a14c682 100644 --- a/Semantics.Music/Arrangement.cs +++ b/Semantics.Music/Arrangement.cs @@ -33,6 +33,9 @@ other is not null public double TotalBars => Sections.Sum(section => section.Bars); /// Gets the structural form of the arrangement. + /// + /// Thrown when the arrangement has more than structurally distinct sections. + /// public Form Form => Form.Of(this); /// Creates an arrangement. diff --git a/Semantics.Music/Form.cs b/Semantics.Music/Form.cs index eb30ec0d..a5abdc94 100644 --- a/Semantics.Music/Form.cs +++ b/Semantics.Music/Form.cs @@ -17,6 +17,9 @@ public sealed record Form [1, 4, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5], ]; + /// The number of structurally distinct sections a form can label, one per letter A-Z. + public const int MaxDistinctSections = 26; + /// Gets the section letter-pattern (e.g. "AABA"). public string Pattern { get; private init; } = ""; @@ -42,7 +45,14 @@ other is not null /// Derives the form of an arrangement from its sections. /// The arrangement to analyze. /// The derived form. + /// + /// Each structurally distinct section takes the next letter from A to Z, so an arrangement can have at most + /// distinct sections. Beyond that there is no letter accepts. + /// /// Thrown when is null. + /// + /// Thrown when has more than structurally distinct sections. + /// public static Form Of(Arrangement arrangement) { Ensure.NotNull(arrangement); @@ -55,6 +65,13 @@ public static Form Of(Arrangement arrangement) int index = representatives.FindIndex(representative => representative.IsSameStructure(section)); if (index < 0) { + if (representatives.Count == MaxDistinctSections) + { + throw new ArgumentException( + $"A form can label at most {MaxDistinctSections} structurally distinct sections (A-Z).", + nameof(arrangement)); + } + representatives.Add(section); index = representatives.Count - 1; } diff --git a/Semantics.Test/Music/FormTests.cs b/Semantics.Test/Music/FormTests.cs index a4221874..242f47ad 100644 --- a/Semantics.Test/Music/FormTests.cs +++ b/Semantics.Test/Music/FormTests.cs @@ -40,6 +40,35 @@ public void Of_RecognizesTwelveBarBlues() Assert.AreEqual(NamedForm.TwelveBarBlues, arrangement.Form.Name); } + [TestMethod] + public void Of_TwentySixDistinctSections_RoundTripsThroughParse() + { + Arrangement arrangement = Arrangement.Create(CMajor, DistinctSections(Form.MaxDistinctSections)); + Form form = arrangement.Form; + Assert.AreEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZ", form.Pattern); + Assert.AreEqual(form, Form.Parse(form.ToString())); + } + + [TestMethod] + public void Of_MoreDistinctSectionsThanLetters_Throws() + { + Arrangement arrangement = Arrangement.Create(CMajor, DistinctSections(Form.MaxDistinctSections + 1)); + _ = Assert.ThrowsExactly(() => Form.Of(arrangement)); + } + + [TestMethod] + public void Of_RepeatsBeyondTwentySixSections_StillLabelsByStructure() + { + List
sections = DistinctSections(Form.MaxDistinctSections); + sections.Add(sections[0]); + Form form = Arrangement.Create(CMajor, sections).Form; + Assert.AreEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZA", form.Pattern); + } + + // Sections with different chord counts never share a structure, so section i has i + 1 bars of C. + private static List
DistinctSections(int count) => + [.. Enumerable.Range(1, count).Select(bars => Section(SectionType.Verse, string.Join(" | ", Enumerable.Repeat("C", bars))))]; + [TestMethod] public void Parse_AppliesLetterRecognition() {