diff --git a/lib/model/alphabet.dart b/lib/model/alphabet.dart index 17c9b49..3f7c903 100644 --- a/lib/model/alphabet.dart +++ b/lib/model/alphabet.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:diacritic/diacritic.dart'; import 'package:flutter/material.dart'; @@ -154,8 +155,11 @@ class Alphabet { bool operator ==(Object other) => other is Alphabet && name == other.name && - _letterToNumber == other._letterToNumber; + const MapEquality().equals( + _letterToNumber, + other._letterToNumber, + ); @override - int get hashCode => Object.hash(name, _letterToNumber); + int get hashCode => Object.hash(name, Object.hashAll(_letterToNumber.values)); } diff --git a/lib/model/cryptogram_scheme.dart b/lib/model/cryptogram_scheme.dart index 729b4cb..20ed3fb 100644 --- a/lib/model/cryptogram_scheme.dart +++ b/lib/model/cryptogram_scheme.dart @@ -1,10 +1,20 @@ import 'dart:collection' show SplayTreeMap; +import 'package:collection/collection.dart' show MapEquality; import 'package:music_notes/music_notes.dart'; +import 'note_name.dart'; + class CryptogramScheme { final String name; final Map patterns; + final NoteNotation notationSystem; + + const CryptogramScheme({ + required this.name, + required this.patterns, + this.notationSystem = const EnglishNoteNotation.symbol(), + }); /// Under this scheme the vowel sounds in the text are matched to the vowel /// sounds of the solmization syllables of Guido of Arezzo (where 'ut' is the @@ -13,6 +23,7 @@ class CryptogramScheme { /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Syllables_to_solmization_names). const CryptogramScheme.solmization() : name = 'Solmization', + notationSystem = const RomanceNoteNotation.symbol(), patterns = const { 'do|ut': .c, 're': .d, @@ -40,6 +51,9 @@ class CryptogramScheme { /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#French). const CryptogramScheme.french() : name = 'French', + notationSystem = const RomanceNoteNotation.symbol( + noteNameNotation: FrenchNoteNameNotation(), + ), patterns = const { 'A': .a, 'B': .b, @@ -76,6 +90,9 @@ class CryptogramScheme { /// (Alain). const CryptogramScheme.frenchVariant() : name = 'French variant', + notationSystem = const RomanceNoteNotation.symbol( + noteNameNotation: FrenchNoteNameNotation(), + ), patterns = const { 'A': .a, 'B': .b, @@ -115,6 +132,7 @@ class CryptogramScheme { /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#German). const CryptogramScheme.german() : name = 'German', + notationSystem = const GermanNoteNotation(), patterns = const { 'As': Note(.a, .flat), 'Ais': Note(.a, .sharp), @@ -170,10 +188,10 @@ class CryptogramScheme { /// Example: /// ```dart /// const CryptogramScheme.german().cryptogramOf('Bach') - /// == [Note.b.flat, Note.a, Note.c, Note.b] + /// == [.b.flat, .a, .c, .b] /// /// const CryptogramScheme.frenchVariant().cryptogramOf('Alain') - /// == [Note.a, Note.d, Note.a, Note.a, Note.f] + /// == [.a, .d, .a, .a, .f] /// ``` List cryptogramOf(String name) { final seenMatches = []; @@ -199,8 +217,8 @@ class CryptogramScheme { bool operator ==(Object other) => other is CryptogramScheme && name == other.name && - patterns == other.patterns; + const MapEquality().equals(patterns, other.patterns); @override - int get hashCode => Object.hash(name, patterns); + int get hashCode => Object.hash(name, Object.hashAll(patterns.values)); } diff --git a/lib/model/name.dart b/lib/model/name.dart index a9a15cb..0bedfa6 100644 --- a/lib/model/name.dart +++ b/lib/model/name.dart @@ -29,5 +29,5 @@ class Name { other is Name && name == other.name && alphabet == other.alphabet; @override - int get hashCode => Object.hash(name, alphabet.hashCode); + int get hashCode => Object.hash(name, alphabet); } diff --git a/lib/model/note_name.dart b/lib/model/note_name.dart new file mode 100644 index 0000000..455eb74 --- /dev/null +++ b/lib/model/note_name.dart @@ -0,0 +1,37 @@ +import 'package:music_notes/music_notes.dart'; + +/// The French notation system for [NoteName]. +final class FrenchNoteNameNotation extends StringNotationSystem { + /// Creates a new [FrenchNoteNameNotation]. + const FrenchNoteNameNotation(); + + static final _noteNames = { + .c: 'Do', + .d: 'Ré', + .e: 'Mi', + .f: 'Fa', + .g: 'Sol', + .a: 'La', + .b: 'Si', + }; + + static final _regExp = RegExp( + '(?${_noteNames.values.join('|')})', + caseSensitive: false, + ); + + @override + RegExp get regExp => _regExp; + + @override + NoteName parseMatch(RegExpMatch match) { + final name = match.namedGroup('noteName')!.toLowerCase(); + + return _noteNames.entries + .firstWhere((entry) => entry.value.toLowerCase() == name) + .key; + } + + @override + String format(NoteName noteName) => _noteNames[noteName]!; +} diff --git a/lib/widgets/musical_cryptogram/cryptogram_scheme_dropdown.dart b/lib/widgets/musical_cryptogram/cryptogram_scheme_dropdown.dart index c00e50b..e54cfe8 100644 --- a/lib/widgets/musical_cryptogram/cryptogram_scheme_dropdown.dart +++ b/lib/widgets/musical_cryptogram/cryptogram_scheme_dropdown.dart @@ -64,7 +64,10 @@ class _CryptogramSchemeDropdownMenuItem extends StatelessWidget { ), const SizedBox(width: 14), Text( - scheme.cryptogramOf(name.name).join('–'), + scheme + .cryptogramOf(name.name) + .map((note) => note.format(scheme.notationSystem)) + .join('–'), style: TextStyle( color: theme.colorScheme.primary.withValues(alpha: 0.6), fontSize: 14, diff --git a/lib/widgets/musical_cryptogram/cryptogram_section.dart b/lib/widgets/musical_cryptogram/cryptogram_section.dart index 1e194b3..7923486 100644 --- a/lib/widgets/musical_cryptogram/cryptogram_section.dart +++ b/lib/widgets/musical_cryptogram/cryptogram_section.dart @@ -35,7 +35,7 @@ class _CryptogramSectionState extends State { bottom: BorderSide(color: color.withValues(alpha: 0.4), width: 2), ), ), - clipBehavior: Clip.antiAlias, + clipBehavior: .antiAlias, child: Stack( children: [ Align( diff --git a/lib/widgets/musical_cryptogram/note_card.dart b/lib/widgets/musical_cryptogram/note_card.dart index 309c83f..3453ce5 100644 --- a/lib/widgets/musical_cryptogram/note_card.dart +++ b/lib/widgets/musical_cryptogram/note_card.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:music_notes/music_notes.dart'; class NoteCard extends StatelessWidget { - final Note note; + final String label; - const NoteCard({super.key, required this.note}); + const NoteCard({super.key, required this.label}); @override Widget build(BuildContext context) { @@ -24,7 +23,7 @@ class NoteCard extends StatelessWidget { mainAxisSize: .min, children: [ Text( - '$note', + label, style: theme.textTheme.displayLarge?.copyWith( fontWeight: .w300, color: onPrimary, diff --git a/lib/widgets/musical_cryptogram/note_cards.dart b/lib/widgets/musical_cryptogram/note_cards.dart index 2af1ba5..df0d455 100644 --- a/lib/widgets/musical_cryptogram/note_cards.dart +++ b/lib/widgets/musical_cryptogram/note_cards.dart @@ -15,7 +15,8 @@ class NoteCards extends StatelessWidget { runSpacing: 8, crossAxisAlignment: .end, children: [ - for (final note in name.musicalCryptogram) NoteCard(note: note), + for (final note in name.musicalCryptogram) + NoteCard(label: note.format(name.scheme.notationSystem)), ], ), ); diff --git a/lib/widgets/name_section.dart b/lib/widgets/name_section.dart index f0f039c..e55c0af 100644 --- a/lib/widgets/name_section.dart +++ b/lib/widgets/name_section.dart @@ -50,7 +50,7 @@ class _NameSectionState extends State { ), ), ), - clipBehavior: Clip.antiAlias, + clipBehavior: .antiAlias, child: Stack( children: [ Align( diff --git a/lib/widgets/name_section_value.dart b/lib/widgets/name_section_value.dart index 809d14e..a3dc625 100644 --- a/lib/widgets/name_section_value.dart +++ b/lib/widgets/name_section_value.dart @@ -11,7 +11,7 @@ class NameSectionValue extends StatelessWidget { final color = theme.colorScheme.primary; return FittedBox( - fit: BoxFit.scaleDown, + fit: .scaleDown, alignment: AlignmentDirectional.centerEnd, child: Text( '$value', diff --git a/pubspec.lock b/pubspec.lock index b37de93..0eaff16 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" boolean_selector: dependency: transitive description: @@ -34,7 +34,7 @@ packages: source: hosted version: "1.1.2" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -119,10 +119,10 @@ packages: dependency: "direct main" description: name: music_notes - sha256: "6deb2bbc6bb6490d3323d8782825bb43f168b147ff26d1795101a6578edfe665" + sha256: "41d29c4900d7592e91c95c4dea0f2e75cc46d30f94719ba33f7c9abb0c125cbf" url: "https://pub.dev" source: hosted - version: "0.24.0" + version: "0.25.0" path: dependency: transitive description: @@ -131,6 +131,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" sky_engine: dependency: transitive description: flutter @@ -140,10 +148,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" stack_trace: dependency: transitive description: @@ -196,18 +204,26 @@ packages: dependency: "direct dev" description: name: very_good_analysis - sha256: "96245839dbcc45dfab1af5fa551603b5c7a282028a64746c19c547d21a7f1e3a" + sha256: "27927d1140ce1b140f998b6340f730a626faa5b95110b3e34a238ff254d731d0" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.1.0" vm_service: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "6.6.1" sdks: dart: ">=3.10.0 <4.0.0" flutter: ">=3.38.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index b9e9ff3..52e40fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,10 +9,11 @@ environment: flutter: ^3.38.0 dependencies: + collection: ^1.19.1 diacritic: ^0.1.6 flutter: sdk: flutter - music_notes: ^0.24.0 + music_notes: ^0.25.0 dev_dependencies: flutter_test: diff --git a/test/model/alphabet_test.dart b/test/model/alphabet_test.dart index c8ba591..5202b3c 100644 --- a/test/model/alphabet_test.dart +++ b/test/model/alphabet_test.dart @@ -171,11 +171,15 @@ void main() { // ignore: prefer_const_constructors for testing .latinNaturalOrder(), // ignore: prefer_const_constructors for testing + Alphabet(name: 'A', letterToNumber: {'a': 1, 'b': 2}), + // ignore: prefer_const_constructors for testing .latinNaturalOrder1(), // ignore: prefer_const_constructors for testing + Alphabet(name: 'A', letterToNumber: {'a': 1, 'b': 2}), + // ignore: prefer_const_constructors for testing .latinNaturalOrder(), }; - expect(collection.length, 2); + expect(collection.length, 3); }); }); }); diff --git a/test/model/cryptogram_scheme_test.dart b/test/model/cryptogram_scheme_test.dart index a91d3c2..f6d3cbf 100644 --- a/test/model/cryptogram_scheme_test.dart +++ b/test/model/cryptogram_scheme_test.dart @@ -36,6 +36,7 @@ void main() { final dmitriSchostakowitsch = const CryptogramScheme.german() .cryptogramOf('D. SCH'); expect(dmitriSchostakowitsch, [.d, .e.flat, .c, .b]); + final edvardGrieg = const CryptogramScheme.french().cryptogramOf('EBG'); expect(edvardGrieg, [.e, .b, .g]); @@ -62,6 +63,7 @@ void main() { final johannSebastianBach = const CryptogramScheme.german() .cryptogramOf('BACH'); expect(johannSebastianBach, [.b.flat, .a, .c, .b]); + final johnCage = const CryptogramScheme.german().cryptogramOf('CAGE'); expect(johnCage, [.c, .a, .g, .e]); @@ -100,11 +102,15 @@ void main() { // ignore: prefer_const_constructors for testing .french(), // ignore: prefer_const_constructors for testing + CryptogramScheme(name: 'A', patterns: {'a': .c}), + // ignore: prefer_const_constructors for testing .frenchVariant(), // ignore: prefer_const_constructors for testing .french(), + // ignore: prefer_const_constructors for testing + CryptogramScheme(name: 'A', patterns: {'a': .c}), }; - expect(collection.length, 2); + expect(collection.length, 3); }); }); }); diff --git a/test/model/main.dart b/test/model/main.dart index 7b22f33..da62813 100644 --- a/test/model/main.dart +++ b/test/model/main.dart @@ -1,9 +1,11 @@ import 'alphabet_test.dart' as alphabet_test; import 'cryptogram_scheme_test.dart' as cryptogram_scheme_test; import 'name_test.dart' as name_test; +import 'note_name_test.dart' as note_name_test; void main() { alphabet_test.main(); cryptogram_scheme_test.main(); name_test.main(); + note_name_test.main(); } diff --git a/test/model/note_name_test.dart b/test/model/note_name_test.dart new file mode 100644 index 0000000..d46d477 --- /dev/null +++ b/test/model/note_name_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:music_notes/music_notes.dart'; +import 'package:note_names/model/note_name.dart'; + +void main() { + group('FrenchNoteNameNotation', () { + group('.parse()', () { + const chain = [FrenchNoteNameNotation()]; + + test('throws a FormatException when source is invalid', () { + expect( + () => NoteName.parse('X', chain: chain), + throwsFormatException, + ); + expect(() => NoteName.parse('', chain: chain), throwsFormatException); + expect( + () => NoteName.parse('A', chain: chain), + throwsFormatException, + ); + expect( + () => NoteName.parse('Re', chain: chain), + throwsFormatException, + ); + expect( + () => NoteName.parse('DoRé', chain: chain), + throwsFormatException, + ); + expect( + () => const FrenchNoteNameNotation().parse('x'), + throwsFormatException, + ); + }); + + test('parses source as a NoteName', () { + expect(NoteName.parse('Do', chain: chain), NoteName.c); + expect(NoteName.parse('Ré', chain: chain), NoteName.d); + expect(NoteName.parse('Mi', chain: chain), NoteName.e); + expect(NoteName.parse('Fa', chain: chain), NoteName.f); + expect(NoteName.parse('Sol', chain: chain), NoteName.g); + expect(NoteName.parse('La', chain: chain), NoteName.a); + expect(NoteName.parse('Si', chain: chain), NoteName.b); + + expect(NoteName.parse('do', chain: chain), NoteName.c); + expect(NoteName.parse('DO', chain: chain), NoteName.c); + expect(NoteName.parse('ré', chain: chain), NoteName.d); + expect(NoteName.parse('mi', chain: chain), NoteName.e); + expect(NoteName.parse('fa', chain: chain), NoteName.f); + expect(NoteName.parse('sol', chain: chain), NoteName.g); + expect(NoteName.parse('la', chain: chain), NoteName.a); + expect(NoteName.parse('si', chain: chain), NoteName.b); + }); + + test('.format() and .parse() are inverses', () { + for (final noteName in NoteName.values) { + expect( + noteName, + NoteName.parse( + noteName.format(const FrenchNoteNameNotation()), + chain: chain, + ), + ); + } + }); + }); + }); +}