From 0b2085a02543471d0b4b9ab2e9a6a6ef1dc99058 Mon Sep 17 00:00:00 2001 From: Sebastian Boldt Date: Sat, 8 Feb 2025 22:30:22 +0100 Subject: [PATCH 1/3] improved performance on chords creation for key by creating functions that only create the necessary --- Sources/Tonic/Key.swift | 55 ++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/Sources/Tonic/Key.swift b/Sources/Tonic/Key.swift index 3337384..95c7997 100644 --- a/Sources/Tonic/Key.swift +++ b/Sources/Tonic/Key.swift @@ -15,12 +15,6 @@ public struct Key: Equatable { /// A note set containing all the notes in the key public let noteSet: NoteSet - /// All the traditional triads representable root, third, and fifth from each note in the key - public let primaryTriads: [Chord] - - /// All chords that fit in the key - public let chords: [Chord] - /// Initialize the key /// - Parameters: /// - root: The primary note class of the key, also known as the tonic @@ -36,7 +30,36 @@ public struct Key: Equatable { } } noteSet = NoteSet(notes: r) + } + + /// The type of accidental to use in this key + public var preferredAccidental: Accidental { + if root.accidental == .sharp { + return .sharp + } + if root.accidental == .flat { + return .flat + } + + let naturalKeysWithFlats: [Key] = [.F, .d, .g, .c, .f] + if naturalKeysWithFlats.contains(self) { + return .flat + } + return .sharp + } + /// All chords that fit in the key + public func chords() -> [Chord] { + let table = ChordTable.shared + var chords: [Chord] = [] + for (_, chord) in table.chords where chord.noteClassSet.isSubset(of: noteSet.noteClassSet) { + chords.append(Chord(chord.root, type: chord.type)) + } + return chords + } + + /// All the traditional triads representable root, third, and fifth from each note in the key + public func primaryTriads() -> [Chord] { let table = ChordTable.shared var chords: [Chord] = [] @@ -53,25 +76,7 @@ public struct Key: Equatable { let primaryTriadsStartingWithC = primaryTriads.sorted(by: { $0.root.letter < $1.root.letter }) let rootPosition = primaryTriadsStartingWithC.firstIndex(where: { $0.root == root }) ?? 0 - self.primaryTriads = Array(primaryTriadsStartingWithC.rotatingLeft(positions: rootPosition)) - - self.chords = chords - } - - /// The type of accidental to use in this key - public var preferredAccidental: Accidental { - if root.accidental == .sharp { - return .sharp - } - if root.accidental == .flat { - return .flat - } - - let naturalKeysWithFlats: [Key] = [.F, .d, .g, .c, .f] - if naturalKeysWithFlats.contains(self) { - return .flat - } - return .sharp + return Array(primaryTriadsStartingWithC.rotatingLeft(positions: rootPosition)) } } From 8bd6b7ed9410b1adc5dcfbc8f7ab9d1e409beceb Mon Sep 17 00:00:00 2001 From: Sebastian Boldt Date: Sat, 8 Feb 2025 22:32:44 +0100 Subject: [PATCH 2/3] fixed unit-tests --- Sources/Tonic/Chord.swift | 2 +- Tests/TonicTests/ChordTests.swift | 10 +++++----- Tests/TonicTests/KeyTests.swift | 22 +++++++++++----------- Tests/TonicTests/ReadMeTests.swift | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/Tonic/Chord.swift b/Sources/Tonic/Chord.swift index e1b6eb3..f100f63 100644 --- a/Sources/Tonic/Chord.swift +++ b/Sources/Tonic/Chord.swift @@ -94,7 +94,7 @@ public struct Chord: Equatable, Codable { /// - Returns: Roman Numeral notation public func romanNumeralNotation(in key: Key) -> String? { let capitalRomanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII"] - if let index = key.primaryTriads.firstIndex(where: { $0 == self }) { + if let index = key.primaryTriads().firstIndex(where: { $0 == self }) { let romanNumeral = capitalRomanNumerals[index] switch type { case .major: return romanNumeral diff --git a/Tests/TonicTests/ChordTests.swift b/Tests/TonicTests/ChordTests.swift index 145b9e3..9f74204 100644 --- a/Tests/TonicTests/ChordTests.swift +++ b/Tests/TonicTests/ChordTests.swift @@ -185,15 +185,15 @@ class ChordTests: XCTestCase { } func testRomanNumerals() { - XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.C) ?? "" }, + XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.C) ?? "" }, ["I", "ii", "iii", "IV", "V", "vi", "vii°"]) - XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, + XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, ["III", "iv", "v", "VI", "VII", "i", "ii°"]) - XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.G) ?? "" }, + XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.G) ?? "" }, ["IV", "", "vi", "", "I", "ii", ""]) - XCTAssertEqual(Key.Am.primaryTriads.map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, + XCTAssertEqual(Key.Am.primaryTriads().map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, ["i", "ii°", "III", "iv", "v", "VI", "VII"]) - XCTAssertEqual(Key.Am.primaryTriads.map { $0.romanNumeralNotation(in: Key.C) ?? "" }, + XCTAssertEqual(Key.Am.primaryTriads().map { $0.romanNumeralNotation(in: Key.C) ?? "" }, ["vi", "vii°", "I", "ii", "iii", "IV", "V"]) } diff --git a/Tests/TonicTests/KeyTests.swift b/Tests/TonicTests/KeyTests.swift index 791f650..4f6fbdf 100644 --- a/Tests/TonicTests/KeyTests.swift +++ b/Tests/TonicTests/KeyTests.swift @@ -17,33 +17,33 @@ class KeyTests: XCTestCase { } func testKeyPrimaryTriads() { - XCTAssertEqual(Key.C.primaryTriads.map { $0.description }, + XCTAssertEqual(Key.C.primaryTriads().map { $0.description }, ["C", "Dm", "Em", "F", "G", "Am", "B°"]) - XCTAssertEqual(Key.Am.primaryTriads.map { $0.description }, + XCTAssertEqual(Key.Am.primaryTriads().map { $0.description }, ["Am", "B°", "C", "Dm", "Em", "F", "G"]) - XCTAssertEqual(Key.G.primaryTriads.map { $0.description }, + XCTAssertEqual(Key.G.primaryTriads().map { $0.description }, ["G", "Am", "Bm", "C", "D", "Em", "F♯°"]) - XCTAssertEqual(Key.Cs.primaryTriads.map { $0.description }, + XCTAssertEqual(Key.Cs.primaryTriads().map { $0.description }, ["C♯", "D♯m", "E♯m", "F♯", "G♯", "A♯m", "B♯°"]) - XCTAssertEqual(Key.Cb.primaryTriads.map { $0.description }, + XCTAssertEqual(Key.Cb.primaryTriads().map { $0.description }, ["C♭", "D♭m", "E♭m", "F♭", "G♭", "A♭m", "B♭°"]) } func testScalePrimaryTriads() { - XCTAssertEqual(Key(root: .C, scale: .harmonicMinor).primaryTriads.map { $0.description }, + XCTAssertEqual(Key(root: .C, scale: .harmonicMinor).primaryTriads().map { $0.description }, ["Cm", "D°", "E♭⁺", "Fm", "G", "A♭", "B°"]) - XCTAssertEqual(Key(root: .Db, scale: .phrygian).primaryTriads.map { $0.description }, + XCTAssertEqual(Key(root: .Db, scale: .phrygian).primaryTriads().map { $0.description }, ["D♭m", "E𝄫", "F♭", "G♭m", "A♭°", "B𝄫", "C♭m"]) - XCTAssertEqual(Key(root: .Ds, scale: .harmonicMinor).primaryTriads.map { $0.description }, + XCTAssertEqual(Key(root: .Ds, scale: .harmonicMinor).primaryTriads().map { $0.description }, ["D♯m", "E♯°", "F♯⁺", "G♯m", "A♯", "B", "C𝄪°"]) } func testKeyChords() { - XCTAssertEqual(Key.G.chords.count, 60) // This should only change if new chord types are added - for triad in Key.G.primaryTriads { - XCTAssert(Key.G.chords.contains(triad)) + XCTAssertEqual(Key.G.chords().count, 60) // This should only change if new chord types are added + for triad in Key.G.primaryTriads() { + XCTAssert(Key.G.chords().contains(triad)) } } diff --git a/Tests/TonicTests/ReadMeTests.swift b/Tests/TonicTests/ReadMeTests.swift index 6c71f69..ba3b424 100644 --- a/Tests/TonicTests/ReadMeTests.swift +++ b/Tests/TonicTests/ReadMeTests.swift @@ -16,12 +16,12 @@ final class ReadMeTests: XCTestCase { // What chords are in this key? func testChordsInKey() { - XCTAssertEqual(Key.Cm.chords.count, 60) + XCTAssertEqual(Key.Cm.chords().count, 60) } // What chords in this key contain this note? func testChordsInKeyContainNote() { - XCTAssertEqual(Key.C.chords.filter { $0.noteClasses.contains(.C) }.count, 36) + XCTAssertEqual(Key.C.chords().filter { $0.noteClasses.contains(.C) }.count, 36) } // What notes do these keys have in common? From b4f736a938ec49e144dc5ec293e45a5d6974e4bc Mon Sep 17 00:00:00 2001 From: Sebastian Boldt Date: Sat, 15 Feb 2025 07:25:07 +0100 Subject: [PATCH 3/3] converted functions to computed properties --- Sources/Tonic/Chord.swift | 2 +- Sources/Tonic/Key.swift | 4 ++-- Tests/TonicTests/ChordTests.swift | 10 +++++----- Tests/TonicTests/KeyTests.swift | 22 +++++++++++----------- Tests/TonicTests/ReadMeTests.swift | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/Tonic/Chord.swift b/Sources/Tonic/Chord.swift index f100f63..e1b6eb3 100644 --- a/Sources/Tonic/Chord.swift +++ b/Sources/Tonic/Chord.swift @@ -94,7 +94,7 @@ public struct Chord: Equatable, Codable { /// - Returns: Roman Numeral notation public func romanNumeralNotation(in key: Key) -> String? { let capitalRomanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII"] - if let index = key.primaryTriads().firstIndex(where: { $0 == self }) { + if let index = key.primaryTriads.firstIndex(where: { $0 == self }) { let romanNumeral = capitalRomanNumerals[index] switch type { case .major: return romanNumeral diff --git a/Sources/Tonic/Key.swift b/Sources/Tonic/Key.swift index 95c7997..4baebd1 100644 --- a/Sources/Tonic/Key.swift +++ b/Sources/Tonic/Key.swift @@ -49,7 +49,7 @@ public struct Key: Equatable { } /// All chords that fit in the key - public func chords() -> [Chord] { + public var chords: [Chord] { let table = ChordTable.shared var chords: [Chord] = [] for (_, chord) in table.chords where chord.noteClassSet.isSubset(of: noteSet.noteClassSet) { @@ -59,7 +59,7 @@ public struct Key: Equatable { } /// All the traditional triads representable root, third, and fifth from each note in the key - public func primaryTriads() -> [Chord] { + public var primaryTriads: [Chord] { let table = ChordTable.shared var chords: [Chord] = [] diff --git a/Tests/TonicTests/ChordTests.swift b/Tests/TonicTests/ChordTests.swift index 9f74204..145b9e3 100644 --- a/Tests/TonicTests/ChordTests.swift +++ b/Tests/TonicTests/ChordTests.swift @@ -185,15 +185,15 @@ class ChordTests: XCTestCase { } func testRomanNumerals() { - XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.C) ?? "" }, + XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.C) ?? "" }, ["I", "ii", "iii", "IV", "V", "vi", "vii°"]) - XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, + XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, ["III", "iv", "v", "VI", "VII", "i", "ii°"]) - XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.G) ?? "" }, + XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.G) ?? "" }, ["IV", "", "vi", "", "I", "ii", ""]) - XCTAssertEqual(Key.Am.primaryTriads().map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, + XCTAssertEqual(Key.Am.primaryTriads.map { $0.romanNumeralNotation(in: Key.Am) ?? "" }, ["i", "ii°", "III", "iv", "v", "VI", "VII"]) - XCTAssertEqual(Key.Am.primaryTriads().map { $0.romanNumeralNotation(in: Key.C) ?? "" }, + XCTAssertEqual(Key.Am.primaryTriads.map { $0.romanNumeralNotation(in: Key.C) ?? "" }, ["vi", "vii°", "I", "ii", "iii", "IV", "V"]) } diff --git a/Tests/TonicTests/KeyTests.swift b/Tests/TonicTests/KeyTests.swift index 4f6fbdf..791f650 100644 --- a/Tests/TonicTests/KeyTests.swift +++ b/Tests/TonicTests/KeyTests.swift @@ -17,33 +17,33 @@ class KeyTests: XCTestCase { } func testKeyPrimaryTriads() { - XCTAssertEqual(Key.C.primaryTriads().map { $0.description }, + XCTAssertEqual(Key.C.primaryTriads.map { $0.description }, ["C", "Dm", "Em", "F", "G", "Am", "B°"]) - XCTAssertEqual(Key.Am.primaryTriads().map { $0.description }, + XCTAssertEqual(Key.Am.primaryTriads.map { $0.description }, ["Am", "B°", "C", "Dm", "Em", "F", "G"]) - XCTAssertEqual(Key.G.primaryTriads().map { $0.description }, + XCTAssertEqual(Key.G.primaryTriads.map { $0.description }, ["G", "Am", "Bm", "C", "D", "Em", "F♯°"]) - XCTAssertEqual(Key.Cs.primaryTriads().map { $0.description }, + XCTAssertEqual(Key.Cs.primaryTriads.map { $0.description }, ["C♯", "D♯m", "E♯m", "F♯", "G♯", "A♯m", "B♯°"]) - XCTAssertEqual(Key.Cb.primaryTriads().map { $0.description }, + XCTAssertEqual(Key.Cb.primaryTriads.map { $0.description }, ["C♭", "D♭m", "E♭m", "F♭", "G♭", "A♭m", "B♭°"]) } func testScalePrimaryTriads() { - XCTAssertEqual(Key(root: .C, scale: .harmonicMinor).primaryTriads().map { $0.description }, + XCTAssertEqual(Key(root: .C, scale: .harmonicMinor).primaryTriads.map { $0.description }, ["Cm", "D°", "E♭⁺", "Fm", "G", "A♭", "B°"]) - XCTAssertEqual(Key(root: .Db, scale: .phrygian).primaryTriads().map { $0.description }, + XCTAssertEqual(Key(root: .Db, scale: .phrygian).primaryTriads.map { $0.description }, ["D♭m", "E𝄫", "F♭", "G♭m", "A♭°", "B𝄫", "C♭m"]) - XCTAssertEqual(Key(root: .Ds, scale: .harmonicMinor).primaryTriads().map { $0.description }, + XCTAssertEqual(Key(root: .Ds, scale: .harmonicMinor).primaryTriads.map { $0.description }, ["D♯m", "E♯°", "F♯⁺", "G♯m", "A♯", "B", "C𝄪°"]) } func testKeyChords() { - XCTAssertEqual(Key.G.chords().count, 60) // This should only change if new chord types are added - for triad in Key.G.primaryTriads() { - XCTAssert(Key.G.chords().contains(triad)) + XCTAssertEqual(Key.G.chords.count, 60) // This should only change if new chord types are added + for triad in Key.G.primaryTriads { + XCTAssert(Key.G.chords.contains(triad)) } } diff --git a/Tests/TonicTests/ReadMeTests.swift b/Tests/TonicTests/ReadMeTests.swift index ba3b424..6c71f69 100644 --- a/Tests/TonicTests/ReadMeTests.swift +++ b/Tests/TonicTests/ReadMeTests.swift @@ -16,12 +16,12 @@ final class ReadMeTests: XCTestCase { // What chords are in this key? func testChordsInKey() { - XCTAssertEqual(Key.Cm.chords().count, 60) + XCTAssertEqual(Key.Cm.chords.count, 60) } // What chords in this key contain this note? func testChordsInKeyContainNote() { - XCTAssertEqual(Key.C.chords().filter { $0.noteClasses.contains(.C) }.count, 36) + XCTAssertEqual(Key.C.chords.filter { $0.noteClasses.contains(.C) }.count, 36) } // What notes do these keys have in common?