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
14 changes: 13 additions & 1 deletion Semantics.Music/Mode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
private static readonly Dictionary<string, int[]> Shapes = new(StringComparer.OrdinalIgnoreCase)
{
// Diatonic modes.
["major"] = [0, 2, 4, 5, 7, 9, 11],

Check warning on line 16 in Semantics.Music/Mode.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'major' 4 times.

Check warning on line 16 in Semantics.Music/Mode.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'major' 4 times.

Check warning on line 16 in Semantics.Music/Mode.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal 'major' 4 times.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Semantics&issues=AaDmTk_NAduN7hzMoBrg&open=AaDmTk_NAduN7hzMoBrg&pullRequest=295
["ionian"] = [0, 2, 4, 5, 7, 9, 11],
["dorian"] = [0, 2, 3, 5, 7, 9, 10],
["phrygian"] = [0, 1, 3, 5, 7, 8, 10],
["lydian"] = [0, 2, 4, 6, 7, 9, 11],
Expand Down Expand Up @@ -55,6 +54,13 @@
["blues_major"] = [0, 2, 3, 4, 7, 9],
};

// Alternative spellings that name the same mode as a shape above. Parsing maps them to the
// canonical name, so a parsed alias equals the static instance it aliases.
private static readonly Dictionary<string, string> Aliases = new(StringComparer.OrdinalIgnoreCase)
{
["ionian"] = "major",
};

/// <summary>Gets the canonical lower-case name of the mode.</summary>
public string Name { get; private init; } = "major";

Expand Down Expand Up @@ -168,11 +174,17 @@
}

/// <summary>Tries to parse a mode by name, case-insensitively.</summary>
/// <remarks>An alias such as "ionian" parses to its canonical mode (<see cref="Major"/>).</remarks>
/// <param name="name">The mode name.</param>
/// <param name="result">The matching mode, or null on failure.</param>
/// <returns><see langword="true"/> when the name is a known mode.</returns>
public static bool TryParse(string? name, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Mode? result)
{
if (name is not null && Aliases.TryGetValue(name, out string? canonical))
{
name = canonical;
}

if (name is not null && Shapes.ContainsKey(name))
{
result = new() { Name = name.ToLowerInvariant() };
Expand Down
15 changes: 15 additions & 0 deletions Semantics.Test/Music/ChromaticAnalysisTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ public void ChromaticChords_DetectsBorrowedMinorSubdominant()
Assert.AreEqual("from parallel minor", analyses[0].Detail);
}

[TestMethod]
public void ChromaticChords_ParsedIonianKey_IsAnalyzedAsMajor()
{
Key ionian = Key.Parse("C ionian");

System.Collections.Generic.IReadOnlyList<ChromaticAnalysis> secondary =
Progression.Parse("4/4 C | D7 | G7 | C").ChromaticChords(ionian);
Assert.AreEqual("V/V", secondary[0].Detail);

System.Collections.Generic.IReadOnlyList<ChromaticAnalysis> borrowed =
Progression.Parse("4/4 C | Fm | C").ChromaticChords(ionian);
Assert.AreEqual(ChromaticKind.BorrowedChord, borrowed[0].Kind);
Assert.AreEqual("from parallel minor", borrowed[0].Detail);
}

[TestMethod]
public void ChromaticAnalysis_Create_RejectsNegativeIndex() =>
_ = Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => ChromaticAnalysis.Create(-1, ChromaticKind.Chromatic, null));
Expand Down
7 changes: 7 additions & 0 deletions Semantics.Test/Music/KeyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ public void RoundTrip()
Assert.AreEqual(k, Key.Parse(k.ToString()));
}

[TestMethod]
public void Parse_IonianSpelling_IsTheSameKeyAsMajor()
{
Assert.AreEqual(Key.Parse("C major"), Key.Parse("C ionian"));
Assert.AreEqual(CMajor, Key.Parse("C ionian"));
}

[TestMethod]
public void TryParseFailsOnUnknownMode()
{
Expand Down
19 changes: 19 additions & 0 deletions Semantics.Test/Music/ModeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ public void Parse_IsCaseInsensitive_AndEqualsStaticInstance()
Assert.AreEqual(Mode.Lydian, Mode.Parse("LYDIAN"));
}

[TestMethod]
[DataRow("ionian")]
[DataRow("Ionian")]
[DataRow("IONIAN")]
public void Parse_IonianAlias_EqualsMajor(string name)
{
Mode parsed = Mode.Parse(name);
Assert.AreEqual(Mode.Ionian, parsed);
Assert.AreEqual(Mode.Major, parsed);
Assert.AreEqual("major", parsed.ToString());
}

[TestMethod]
public void TryParse_IonianAlias_EqualsMajor()
{
Assert.IsTrue(Mode.TryParse("ionian", out Mode? result));
Assert.AreEqual(Mode.Major, result);
}

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