Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(enharmonic_note): add toClosestNote method #48

Merged
merged 3 commits into from
Apr 14, 2023
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
2 changes: 1 addition & 1 deletion lib/src/interval/int_interval_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension IntIntervalExtension on int {
8: 12,
};

/// Returns an [int] interval that matches [semitones]
/// Returns an [int] interval that matches with [semitones]
/// in [_intervalsToSemitonesDelta], otherwise returns `null`.
///
/// Example:
Expand Down
62 changes: 51 additions & 11 deletions lib/src/note/enharmonic_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,61 @@ class EnharmonicNote extends Enharmonic<Note> {
});
}

/// Returns the [Note] from [semitones] and a [preferredAccidental].
/// Returns the [Note] that matches [withAccidental] from this
/// [EnharmonicNote].
///
/// Throws an [ArgumentError] when [withAccidental] does not match with any
/// possible note.
///
/// Example:
/// ```dart
/// EnharmonicNote.e.toNote() == Note.e
/// EnharmonicNote.dSharp.toNote(Accidental.flat) == Note.eFlat
/// EnharmonicNote.d.toNote() == Note.d
/// EnharmonicNote.fSharp.toNote(Accidental.flat) == Note.gFlat
/// EnharmonicNote.cSharp.toNote(Accidental.natural) // throws
/// ```
Note toNote([Accidental preferredAccidental = Accidental.natural]) {
return items.firstWhereOrNull(
(note) => note.accidental == preferredAccidental,
) ??
items.firstWhereOrNull(
(note) => note.accidental == Accidental.natural,
) ??
items.first;
Note toNote([Accidental? withAccidental]) {
final matchedNote = items.firstWhereOrNull(
(note) => note.accidental == withAccidental,
);
if (matchedNote != null) return matchedNote;

if (withAccidental != null) {
throw ArgumentError.value(
'$withAccidental',
'preferredAccidental',
'Impossible match for',
);
}

return items
// TODO(albertms10): return the note with the closest [withAccidental].
.sorted(
(a, b) => a.accidental.semitones
.abs()
.compareTo(b.accidental.semitones.abs()),
)
.first;
}

/// Returns the [Note] that matches with [preferredAccidental] from this
/// [EnharmonicNote].
///
/// Like [toNote] except that this function returns the closest note where a
/// similar call to [toNote] would throw an [ArgumentError].
///
/// Example:
/// ```dart
/// EnharmonicNote.d.toClosestNote() == Note.d
/// EnharmonicNote.gSharp.toClosestNote(Accidental.flat) == Note.aFlat
/// EnharmonicNote.cSharp.toClosestNote(Accidental.natural) == null
/// ```
Note toClosestNote([Accidental? preferredAccidental]) {
try {
return toNote(preferredAccidental);
// ignore: avoid_catching_errors
} on ArgumentError {
return toNote();
}
}

/// Returns a transposed [EnharmonicNote] by [semitones]
Expand Down
30 changes: 15 additions & 15 deletions lib/src/note/note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,17 @@ class Note implements MusicItem {
Accidental accidental = Accidental.natural,
]) {
final note = Note.fromRawAccidentals(accidentals, accidental);

return mode == Modes.major
? note
: EnharmonicNote(note.semitones)
.transposeBy(
const Interval.imperfect(
3,
ImperfectQuality.minor,
descending: true,
).semitones,
)
.toNote(accidental);
if (mode == Modes.major) return note;

return EnharmonicNote(note.semitones)
.transposeBy(
const Interval.imperfect(
3,
ImperfectQuality.minor,
descending: true,
).semitones,
)
.toClosestNote(accidental);
}

/// Returns the [Note] from the [Tonality] given its [accidentals] number
Expand All @@ -73,7 +72,7 @@ class Note implements MusicItem {

return EnharmonicNote(
(fifthInterval.semitones * accidentals + 1).chromaticModExcludeZero,
).toNote(
).toClosestNote(
(accidental == Accidental.flat && accidentals > 8) ||
(accidental == Accidental.sharp && accidentals > 10)
? Accidental(accidental.semitones + 1)
Expand Down Expand Up @@ -149,13 +148,14 @@ class Note implements MusicItem {
var distance = 0;
var currentPitch = this.semitones;

var tempNote = EnharmonicNote(currentPitch).toNote(preferredAccidental);
var tempNote =
EnharmonicNote(currentPitch).toClosestNote(preferredAccidental);

while (tempNote != other && distance < chromaticDivisions) {
distance++;
currentPitch += semitones;
tempNote = EnharmonicNote(currentPitch.chromaticModExcludeZero)
.toNote(preferredAccidental);
.toClosestNote(preferredAccidental);
}

return distance;
Expand Down
4 changes: 2 additions & 2 deletions lib/src/note/notes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum Notes {

const Notes(this.value);

/// Returns a [Notes] enum item that matches [value]
/// Returns a [Notes] enum item that matches with [value]
/// as in [Notes], otherwise returns `null`.
///
/// Example:
Expand All @@ -26,7 +26,7 @@ enum Notes {
(note) => value.chromaticModExcludeZero == note.value,
);

/// Returns a [Notes] enum item that matches [ordinal].
/// Returns a [Notes] enum item that matches with [ordinal].
///
/// Example:
/// ```dart
Expand Down
160 changes: 160 additions & 0 deletions test/src/note/enharmonic_note_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,166 @@ void main() {
});
});

group('.toNote()', () {
test(
'should return the Note that matches with the accidental',
() {
expect(EnharmonicNote.c.toNote(), Note.c);
expect(
EnharmonicNote.c.toNote(Accidental.sharp),
const Note(Notes.b, Accidental.sharp),
);
expect(
EnharmonicNote.c.toNote(Accidental.doubleFlat),
const Note(Notes.d, Accidental.doubleFlat),
);

expect(EnharmonicNote.cSharp.toNote(), Note.cSharp);
expect(EnharmonicNote.cSharp.toNote(Accidental.flat), Note.dFlat);

expect(EnharmonicNote.d.toNote(), Note.d);
expect(
EnharmonicNote.d.toNote(Accidental.doubleSharp),
const Note(Notes.c, Accidental.doubleSharp),
);
expect(
EnharmonicNote.d.toNote(Accidental.doubleFlat),
const Note(Notes.e, Accidental.doubleFlat),
);

expect(EnharmonicNote.dSharp.toNote(), Note.dSharp);
expect(EnharmonicNote.dSharp.toNote(Accidental.flat), Note.eFlat);

expect(EnharmonicNote.e.toNote(), Note.e);
expect(
EnharmonicNote.e.toNote(Accidental.doubleSharp),
const Note(Notes.d, Accidental.doubleSharp),
);
expect(
EnharmonicNote.e.toNote(Accidental.flat),
const Note(Notes.f, Accidental.flat),
);

expect(EnharmonicNote.f.toNote(), Note.f);
expect(
EnharmonicNote.f.toNote(Accidental.sharp),
const Note(Notes.e, Accidental.sharp),
);
expect(
EnharmonicNote.f.toNote(Accidental.doubleFlat),
const Note(Notes.g, Accidental.doubleFlat),
);

expect(EnharmonicNote.fSharp.toNote(), Note.fSharp);
expect(EnharmonicNote.fSharp.toNote(Accidental.flat), Note.gFlat);

expect(EnharmonicNote.g.toNote(), Note.g);
expect(
EnharmonicNote.g.toNote(Accidental.doubleSharp),
const Note(Notes.f, Accidental.doubleSharp),
);
expect(
EnharmonicNote.g.toNote(Accidental.doubleFlat),
const Note(Notes.a, Accidental.doubleFlat),
);

expect(EnharmonicNote.gSharp.toNote(), Note.gSharp);
expect(EnharmonicNote.gSharp.toNote(Accidental.flat), Note.aFlat);

expect(EnharmonicNote.a.toNote(), Note.a);
expect(
EnharmonicNote.a.toNote(Accidental.doubleSharp),
const Note(Notes.g, Accidental.doubleSharp),
);
expect(
EnharmonicNote.a.toNote(Accidental.doubleFlat),
const Note(Notes.b, Accidental.doubleFlat),
);

expect(EnharmonicNote.aSharp.toNote(), Note.aSharp);
expect(EnharmonicNote.aSharp.toNote(Accidental.flat), Note.bFlat);

expect(EnharmonicNote.b.toNote(), Note.b);
expect(
EnharmonicNote.b.toNote(Accidental.doubleSharp),
const Note(Notes.a, Accidental.doubleSharp),
);
expect(
EnharmonicNote.b.toNote(Accidental.flat),
const Note(Notes.c, Accidental.flat),
);
},
);

test(
'should throw an ArgumentError when withAccidental does not match with '
'any Note',
() {
expect(
() => EnharmonicNote.cSharp.toNote(Accidental.natural),
throwsArgumentError,
);
expect(
() => EnharmonicNote.c.toNote(Accidental.flat),
throwsArgumentError,
);
expect(
() => EnharmonicNote.d.toNote(Accidental.sharp),
throwsArgumentError,
);
expect(
() => EnharmonicNote.a.toNote(Accidental.tripleFlat),
throwsArgumentError,
);
},
);
});

group('.toClosestNote()', () {
test(
'should return the Note that matches with the preferred accidental',
() {
expect(EnharmonicNote.c.toClosestNote(), Note.c);
expect(
EnharmonicNote.c.toClosestNote(Accidental.sharp),
const Note(Notes.b, Accidental.sharp),
);
expect(
EnharmonicNote.c.toClosestNote(Accidental.doubleFlat),
const Note(Notes.d, Accidental.doubleFlat),
);

expect(EnharmonicNote.cSharp.toClosestNote(), Note.cSharp);
expect(
EnharmonicNote.cSharp.toClosestNote(Accidental.flat),
Note.dFlat,
);

// ... Similar to `.toNote()`.
},
);

test(
'should return the closest Note where a similar call to .toNote() '
'would throw',
() {
expect(
EnharmonicNote.cSharp.toClosestNote(Accidental.natural),
Note.cSharp,
);
expect(EnharmonicNote.c.toClosestNote(Accidental.flat), Note.c);
expect(
EnharmonicNote.d.toClosestNote(Accidental.sharp),
Note.d,
);
expect(
EnharmonicNote.a.toClosestNote(Accidental.tripleFlat),
Note.a,
);
},
);
});

group('.shortestFifthsDistance()', () {
test('should return the shortest fifths distance from other', () {
expect(EnharmonicNote.c.shortestFifthsDistance(EnharmonicNote.c), 0);
Expand Down