• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

albertms10 / music_notes / 16781939945

06 Aug 2025 03:52PM UTC coverage: 99.428% (-0.1%) from 99.551%
16781939945

Pull #617

github

web-flow
Merge 8ef992488 into 892bf89e6
Pull Request #617: refactor(pitch): ♻️ rewrite using `NotationSystem`

73 of 74 new or added lines in 3 files covered. (98.65%)

1 existing line in 1 file now uncovered.

1565 of 1574 relevant lines covered (99.43%)

2.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.55
/lib/src/note/base_note.dart
1
import 'package:collection/collection.dart' show IterableExtension;
2
import 'package:music_notes/utils.dart';
3

4
import '../interval/size.dart';
5
import '../notation_system.dart';
6
import '../tuning/equal_temperament.dart';
7
import 'note.dart';
8

9
/// The base note names of the diatonic scale.
10
///
11
/// ---
12
/// See also:
13
/// * [Note].
14
enum BaseNote implements Comparable<BaseNote> {
15
  /// Note C.
16
  c(0),
17

18
  /// Note D.
19
  d(2),
20

21
  /// Note E.
22
  e(4),
23

24
  /// Note F.
25
  f(5),
26

27
  /// Note G.
28
  g(7),
29

30
  /// Note A.
31
  a(9),
32

33
  /// Note B.
34
  b(11);
35

36
  /// The number of semitones that identify this [BaseNote].
37
  final int semitones;
38

39
  /// Creates a new [BaseNote] from [semitones].
40
  const BaseNote(this.semitones);
41

42
  /// Returns a [BaseNote] that matches with [semitones] as in [BaseNote],
43
  /// otherwise returns `null`.
44
  ///
45
  /// Example:
46
  /// ```dart
47
  /// BaseNote.fromSemitones(2) == BaseNote.d
48
  /// BaseNote.fromSemitones(7) == BaseNote.g
49
  /// BaseNote.fromSemitones(10) == null
50
  /// ```
51
  static BaseNote? fromSemitones(int semitones) => values.firstWhereOrNull(
2✔
52
    (note) => semitones % chromaticDivisions == note.semitones,
4✔
53
  );
54

55
  /// Returns a [BaseNote] that matches with [ordinal].
56
  ///
57
  /// Example:
58
  /// ```dart
59
  /// BaseNote.fromOrdinal(3) == BaseNote.e
60
  /// BaseNote.fromOrdinal(7) == BaseNote.b
61
  /// BaseNote.fromOrdinal(10) == BaseNote.e
62
  /// ```
63
  factory BaseNote.fromOrdinal(int ordinal) =>
1✔
64
      values[ordinal.nonZeroMod(values.length) - 1];
4✔
65

66
  /// Parse [source] as a [BaseNote] and return its value.
67
  ///
68
  /// If the [source] string does not contain a valid [BaseNote], a
69
  /// [FormatException] is thrown.
70
  ///
71
  /// Example:
72
  /// ```dart
73
  /// BaseNote.parse('B') == BaseNote.b
74
  /// BaseNote.parse('a') == BaseNote.a
75
  /// BaseNote.parse('z') // throws a FormatException
76
  /// ```
77
  factory BaseNote.parse(
1✔
78
    String source, {
79
    List<Parser<BaseNote>> chain = const [
80
      EnglishBaseNoteNotation(),
81
      GermanBaseNoteNotation(),
82
      RomanceBaseNoteNotation(),
83
    ],
84
  }) => chain.parse(source);
1✔
85

86
  /// The ordinal number of this [BaseNote].
87
  ///
88
  /// Example:
89
  /// ```dart
90
  /// BaseNote.c.ordinal == 1
91
  /// BaseNote.f.ordinal == 4
92
  /// BaseNote.b.ordinal == 7
93
  /// ```
94
  int get ordinal => values.indexOf(this) + 1;
3✔
95

96
  /// The [Size] that conforms between this [BaseNote] and [other].
97
  ///
98
  /// Example:
99
  /// ```dart
100
  /// BaseNote.d.intervalSize(BaseNote.f) == Size.third
101
  /// BaseNote.a.intervalSize(BaseNote.e) == Size.fifth
102
  /// BaseNote.d.intervalSize(BaseNote.c) == Size.seventh
103
  /// BaseNote.c.intervalSize(BaseNote.a) == Size.sixth
104
  /// ```
105
  Size intervalSize(BaseNote other) => Size(
2✔
106
    other.ordinal - ordinal + (ordinal > other.ordinal ? values.length : 0) + 1,
9✔
107
  );
108

109
  /// The difference in semitones between this [BaseNote] and [other].
110
  ///
111
  /// Example:
112
  /// ```dart
113
  /// BaseNote.c.difference(BaseNote.c) == 0
114
  /// BaseNote.c.difference(BaseNote.e) == 4
115
  /// BaseNote.f.difference(BaseNote.e) == -1
116
  /// BaseNote.a.difference(BaseNote.e) == -5
117
  /// ```
118
  int difference(BaseNote other) => Note(this).difference(Note(other));
4✔
119

120
  /// The positive difference in semitones between this [BaseNote] and [other].
121
  ///
122
  /// When [difference] would return a negative value, this method returns the
123
  /// difference with [other] being in the next octave.
124
  ///
125
  /// Example:
126
  /// ```dart
127
  /// BaseNote.c.positiveDifference(BaseNote.c) == 0
128
  /// BaseNote.c.positiveDifference(BaseNote.e) == 4
129
  /// BaseNote.f.positiveDifference(BaseNote.e) == 11
130
  /// BaseNote.a.positiveDifference(BaseNote.e) == 7
131
  /// ```
132
  int positiveDifference(BaseNote other) {
1✔
133
    final diff = difference(other);
1✔
134

135
    return diff.isNegative ? diff + chromaticDivisions : diff;
2✔
136
  }
137

138
  /// Transposes this [BaseNote] by interval [size].
139
  ///
140
  /// Example:
141
  /// ```dart
142
  /// BaseNote.g.transposeBySize(Size.unison) == BaseNote.g
143
  /// BaseNote.g.transposeBySize(Size.fifth) == BaseNote.d
144
  /// BaseNote.a.transposeBySize(-Size.third) == BaseNote.f
145
  /// ```
146
  BaseNote transposeBySize(Size size) =>
1✔
147
      BaseNote.fromOrdinal(ordinal + size.incrementBy(-1));
5✔
148

149
  /// The next ordinal [BaseNote].
150
  ///
151
  /// Example:
152
  /// ```dart
153
  /// BaseNote.c.next == BaseNote.d
154
  /// BaseNote.f.next == BaseNote.a
155
  /// BaseNote.b.next == BaseNote.c
156
  /// ```
157
  BaseNote get next => transposeBySize(Size.second);
2✔
158

159
  /// The previous ordinal [BaseNote].
160
  ///
161
  /// Example:
162
  /// ```dart
163
  /// BaseNote.e.previous == BaseNote.d
164
  /// BaseNote.g.previous == BaseNote.f
165
  /// BaseNote.c.previous == BaseNote.b
166
  /// ```
167
  BaseNote get previous => transposeBySize(-Size.second);
3✔
168

169
  /// The string representation of this [BaseNote] based on [formatter].
170
  @override
1✔
171
  String toString({
172
    Formatter<BaseNote> formatter = const EnglishBaseNoteNotation(),
173
  }) => formatter.format(this);
1✔
174

175
  @override
1✔
176
  int compareTo(BaseNote other) => semitones.compareTo(other.semitones);
3✔
177
}
178

179
/// The English notation system for [BaseNote
180
final class EnglishBaseNoteNotation extends NotationSystem<BaseNote> {
181
  /// Creates a new [EnglishBaseNoteNotation].
182
  const EnglishBaseNoteNotation();
3✔
183

184
  @override
1✔
185
  String format(BaseNote baseNote) => baseNote.name.toUpperCase();
2✔
186

187
  @override
1✔
188
  bool matches(String source) {
189
    if (source.toLowerCase()
1✔
190
        case 'c' || 'd' || 'e' || 'f' || 'g' || 'a' || 'b') {
7✔
191
      return true;
192
    }
193

194
    return false;
195
  }
196

197
  @override
1✔
198
  BaseNote parse(String source) => BaseNote.values.byName(source.toLowerCase());
2✔
199
}
200

201
/// The German notation system for [BaseNote
202
final class GermanBaseNoteNotation extends NotationSystem<BaseNote> {
203
  /// Creates a new [GermanBaseNoteNotation].
204
  const GermanBaseNoteNotation();
3✔
205

206
  @override
1✔
207
  String format(BaseNote baseNote) => switch (baseNote) {
208
    BaseNote.b => 'H',
1✔
209
    BaseNote(:final name) => name.toUpperCase(),
2✔
210
  };
211

212
  @override
1✔
213
  bool matches(String source) {
214
    if (source.toLowerCase()
1✔
215
        case 'c' || 'd' || 'e' || 'f' || 'g' || 'a' || 'h') {
7✔
216
      return true;
217
    }
218

219
    return false;
220
  }
221

222
  @override
1✔
223
  BaseNote parse(String source) => switch (source.toLowerCase()) {
1✔
224
    'c' => BaseNote.c,
1✔
225
    'd' => BaseNote.d,
1✔
226
    'e' => BaseNote.e,
1✔
227
    'f' => BaseNote.f,
1✔
228
    'g' => BaseNote.g,
1✔
229
    'a' => BaseNote.a,
1✔
230
    'h' => BaseNote.b,
1✔
231
    'b' => BaseNote.b,
1✔
232
    _ => throw FormatException('Invalid BaseNote', source),
1✔
233
  };
234
}
235

236
/// The Romance notation system for [BaseNote
237
final class RomanceBaseNoteNotation extends NotationSystem<BaseNote> {
238
  /// Creates a new [RomanceBaseNoteNotation].
239
  const RomanceBaseNoteNotation();
3✔
240

241
  @override
1✔
242
  String format(BaseNote baseNote) => switch (baseNote) {
243
    BaseNote.c => 'Do',
1✔
244
    BaseNote.d => 'Re',
1✔
245
    BaseNote.e => 'Mi',
1✔
246
    BaseNote.f => 'Fa',
1✔
247
    BaseNote.g => 'Sol',
1✔
248
    BaseNote.a => 'La',
1✔
249
    BaseNote.b => 'Si',
1✔
250
  };
251

252
  @override
1✔
253
  bool matches(String source) {
254
    if (source.toLowerCase()
1✔
255
        case 'do' || 're' || 'mi' || 'fa' || 'sol' || 'la' || 'si') {
7✔
256
      return true;
257
    }
258

259
    return false;
260
  }
261

262
  @override
1✔
263
  BaseNote parse(String source) => switch (source.toLowerCase()) {
1✔
264
    'do' => BaseNote.c,
1✔
265
    're' => BaseNote.d,
1✔
266
    'mi' => BaseNote.e,
1✔
267
    'fa' => BaseNote.f,
1✔
268
    'sol' => BaseNote.g,
1✔
269
    'la' => BaseNote.a,
1✔
270
    'si' => BaseNote.b,
1✔
UNCOV
271
    _ => throw FormatException('Invalid BaseNote', source),
×
272
  };
273
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc