Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Semantics.Music/Arrangement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ other is not null
public double TotalBars => Sections.Sum(section => section.Bars);

/// <summary>Gets the structural form of the arrangement.</summary>
/// <exception cref="ArgumentException">
/// Thrown when the arrangement has more than <see cref="Form.MaxDistinctSections"/> structurally distinct sections.
/// </exception>
public Form Form => Form.Of(this);

/// <summary>Creates an arrangement.</summary>
Expand Down
17 changes: 17 additions & 0 deletions Semantics.Music/Form.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public sealed record Form
[1, 4, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5],
];

/// <summary>The number of structurally distinct sections a form can label, one per letter A-Z.</summary>
public const int MaxDistinctSections = 26;

/// <summary>Gets the section letter-pattern (e.g. "AABA").</summary>
public string Pattern { get; private init; } = "";

Expand All @@ -42,7 +45,14 @@ other is not null
/// <summary>Derives the form of an arrangement from its sections.</summary>
/// <param name="arrangement">The arrangement to analyze.</param>
/// <returns>The derived form.</returns>
/// <remarks>
/// Each structurally distinct section takes the next letter from A to Z, so an arrangement can have at most
/// <see cref="MaxDistinctSections"/> distinct sections. Beyond that there is no letter <see cref="Parse"/> accepts.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="arrangement"/> is null.</exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="arrangement"/> has more than <see cref="MaxDistinctSections"/> structurally distinct sections.
/// </exception>
public static Form Of(Arrangement arrangement)
{
Ensure.NotNull(arrangement);
Expand All @@ -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;
}
Expand Down
29 changes: 29 additions & 0 deletions Semantics.Test/Music/FormTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentException>(() => Form.Of(arrangement));
}

[TestMethod]
public void Of_RepeatsBeyondTwentySixSections_StillLabelsByStructure()
{
List<Section> 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<Section> DistinctSections(int count) =>
[.. Enumerable.Range(1, count).Select(bars => Section(SectionType.Verse, string.Join(" | ", Enumerable.Repeat("C", bars))))];

[TestMethod]
public void Parse_AppliesLetterRecognition()
{
Expand Down
Loading