From ffc93eddbb237af34f49309495d4c0a0068f2a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:13:21 +0200 Subject: [PATCH 1/9] feat(model): add `CryptogramScheme` and `toCryptogram` extension method --- .vscode/settings.json | 3 +- lib/model/cryptogram_scheme.dart | 195 +++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 lib/model/cryptogram_scheme.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 58677e5..5ff2d1e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "cSpell.words": ["latin"] + "cSpell.ignoreWords": ["music"], + "cSpell.words": ["cryptogrammatic", "latin"] } diff --git a/lib/model/cryptogram_scheme.dart b/lib/model/cryptogram_scheme.dart new file mode 100644 index 0000000..098b429 --- /dev/null +++ b/lib/model/cryptogram_scheme.dart @@ -0,0 +1,195 @@ +import 'dart:collection' show SplayTreeMap; + +import 'package:music_notes/music_notes.dart'; + +class CryptogramScheme { + final String name; + final Map patterns; + + /// Creates a new [CryptogramScheme] from [name] and [patterns]. + const CryptogramScheme(this.name, {required this.patterns}); + + /// 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 + /// root, which we now call 'do', [Note.c]). + /// + /// See [Musical Cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Syllables_to_solmization_names). + CryptogramScheme.solmization() + : name = 'Solmization', + patterns = { + RegExp('do|ut', caseSensitive: false): Note.c, + RegExp('re', caseSensitive: false): Note.d, + RegExp('mi', caseSensitive: false): Note.e, + RegExp('fa', caseSensitive: false): Note.f, + RegExp('sol?', caseSensitive: false): Note.g, + RegExp('la', caseSensitive: false): Note.a, + RegExp('(t|s)i', caseSensitive: false): Note.b, + RegExp('a', caseSensitive: false): Note.a, + RegExp('d', caseSensitive: false): Note.c, + RegExp('r', caseSensitive: false): Note.d, + RegExp('m', caseSensitive: false): Note.e, + RegExp('f', caseSensitive: false): Note.f, + }; + + /// Arisen late in the 19th century, it was more akin to normal encipherment. + /// The most popular version involved writing out the letters H–N, O–U and + /// V–Z in lines under the original diatonic notes A–G, as follows: + /// + /// | A | B | C | D | E | F | G | + /// |:-:|:-:|:-:|:-:|:-:|:-:|:-:| + /// | H | I | J | K | L | M | N | + /// | O | P | Q | R | S | T | U | + /// | V | W | X | Y | Z | | | + /// + /// See [Musical Cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#French). + CryptogramScheme.french() + : name = 'French', + patterns = { + RegExp('A', caseSensitive: false): Note.a, + RegExp('B', caseSensitive: false): Note.b, + RegExp('C', caseSensitive: false): Note.c, + RegExp('D', caseSensitive: false): Note.d, + RegExp('E', caseSensitive: false): Note.e, + RegExp('F', caseSensitive: false): Note.f, + RegExp('G', caseSensitive: false): Note.g, + RegExp('H', caseSensitive: false): Note.a, + RegExp('I', caseSensitive: false): Note.b, + RegExp('J', caseSensitive: false): Note.c, + RegExp('K', caseSensitive: false): Note.d, + RegExp('L', caseSensitive: false): Note.e, + RegExp('M', caseSensitive: false): Note.f, + RegExp('N', caseSensitive: false): Note.g, + RegExp('O', caseSensitive: false): Note.a, + RegExp('P', caseSensitive: false): Note.b, + RegExp('Q', caseSensitive: false): Note.c, + RegExp('R', caseSensitive: false): Note.d, + RegExp('S', caseSensitive: false): Note.e, + RegExp('T', caseSensitive: false): Note.f, + RegExp('U', caseSensitive: false): Note.g, + RegExp('V', caseSensitive: false): Note.a, + RegExp('W', caseSensitive: false): Note.b, + RegExp('X', caseSensitive: false): Note.c, + RegExp('Y', caseSensitive: false): Note.d, + RegExp('Z', caseSensitive: false): Note.e, + }; + + /// Derived on the [CryptogramScheme.french] but leaving H = [Note.b] and + /// starting the second line with 'I'. + /// + /// See [Musical Cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Summary_of_signature_motifs) + /// (Alain). + CryptogramScheme.frenchVariant() + : name = 'French variant', + patterns = { + RegExp('A', caseSensitive: false): Note.a, + RegExp('B', caseSensitive: false): Note.b, + RegExp('C', caseSensitive: false): Note.c, + RegExp('D', caseSensitive: false): Note.d, + RegExp('E', caseSensitive: false): Note.e, + RegExp('F', caseSensitive: false): Note.f, + RegExp('G', caseSensitive: false): Note.g, + RegExp('H', caseSensitive: false): Note.b, + RegExp('I', caseSensitive: false): Note.a, + RegExp('J', caseSensitive: false): Note.b, + RegExp('K', caseSensitive: false): Note.c, + RegExp('L', caseSensitive: false): Note.d, + RegExp('M', caseSensitive: false): Note.e, + RegExp('N', caseSensitive: false): Note.f, + RegExp('O', caseSensitive: false): Note.g, + RegExp('P', caseSensitive: false): Note.a, + RegExp('Q', caseSensitive: false): Note.b, + RegExp('R', caseSensitive: false): Note.c, + RegExp('S', caseSensitive: false): Note.d, + RegExp('T', caseSensitive: false): Note.e, + RegExp('U', caseSensitive: false): Note.f, + RegExp('V', caseSensitive: false): Note.g, + RegExp('W', caseSensitive: false): Note.a, + RegExp('X', caseSensitive: false): Note.b, + RegExp('Y', caseSensitive: false): Note.c, + RegExp('Z', caseSensitive: false): Note.d, + }; + + /// Because the development of note names took place within the framework of + /// [Mode]s, in the German-speaking world [Note.b.flat] was named 'B' and + /// [Note.b] (natural) was named 'H'. + /// + /// Similarly, other note names were derived by sound, e.g., [Note.e.flat], + /// 'Es' in German, could represent 'S' and [Note.a.flat] the digraph 'As'. + /// + /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#German). + CryptogramScheme.german() + : name = 'German', + patterns = { + RegExp('As', caseSensitive: false): Note.a.flat, + RegExp('Ais', caseSensitive: false): Note.a.sharp, + RegExp('A', caseSensitive: false): Note.a, + RegExp('B', caseSensitive: false): Note.b.flat, + RegExp('Ces', caseSensitive: false): Note.c.flat, + RegExp('Cis', caseSensitive: false): Note.c.sharp, + RegExp('C', caseSensitive: false): Note.c, + RegExp('Des', caseSensitive: false): Note.d.flat, + RegExp('Dis', caseSensitive: false): Note.d.sharp, + RegExp('D', caseSensitive: false): Note.d, + RegExp('Es', caseSensitive: false): Note.e.flat, + RegExp('Eis', caseSensitive: false): Note.e.sharp, + RegExp('Fes', caseSensitive: false): Note.f.flat, + RegExp('Fis', caseSensitive: false): Note.f.sharp, + RegExp('F', caseSensitive: false): Note.f, + RegExp('Ges', caseSensitive: false): Note.g.flat, + RegExp('Gis', caseSensitive: false): Note.g.sharp, + RegExp('G', caseSensitive: false): Note.g, + RegExp('E', caseSensitive: false): Note.e, + RegExp('H', caseSensitive: false): Note.b, + RegExp('I', caseSensitive: false): Note.b, + RegExp('J', caseSensitive: false): Note.c, + RegExp('K', caseSensitive: false): Note.d, + RegExp('L', caseSensitive: false): Note.e, + RegExp('M', caseSensitive: false): Note.f, + RegExp('N', caseSensitive: false): Note.g, + RegExp('O', caseSensitive: false): Note.a, + RegExp('P', caseSensitive: false): Note.b, + RegExp('Q', caseSensitive: false): Note.c, + RegExp('R', caseSensitive: false): Note.d, + RegExp('S', caseSensitive: false): Note.e.flat, + RegExp('T', caseSensitive: false): Note.f, + RegExp('U', caseSensitive: false): Note.g, + RegExp('V', caseSensitive: false): Note.a, + RegExp('W', caseSensitive: false): Note.b, + RegExp('X', caseSensitive: false): Note.c, + RegExp('Y', caseSensitive: false): Note.d, + RegExp('Z', caseSensitive: false): Note.e, + }; +} + +extension CryptogramStringExtension on String { + /// Returns the cryptogrammatic sequence of [Note]s, a sequence which can be + /// taken to refer to an extra-musical text by some 'logical' relationship, + /// usually between note names and letters. + /// + /// Example: + /// ```dart + /// 'Bach'.toCryptogram(CryptogramScheme.german()) + /// == [Note.b.flat, Note.a, Note.c, Note.b] + /// + /// 'Alain'.toCryptogram(CryptogramScheme.frenchVariant()) + /// == [Note.a, Note.d, Note.a, Note.a, Note.f] + /// ``` + List toCryptogram(CryptogramScheme scheme) { + final seenMatches = []; + final notes = SplayTreeMap(); + + for (final pattern in scheme.patterns.keys) { + for (final match in pattern.allMatches(this)) { + final isMatchSeen = seenMatches.any( + (element) => match.start >= element.start && match.end <= element.end, + ); + if (!isMatchSeen) { + seenMatches.add(match); + notes[match.start] = scheme.patterns[pattern]!; + } + } + } + + return notes.values.toList(); + } +} From 2d224e0908b02249b129e05d7ea545c46b4e8ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:24:36 +0200 Subject: [PATCH 2/9] chore(pubspec): remove `characters` dependency --- pubspec.lock | 2 +- pubspec.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e147a6b..bbfd368 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,7 +58,7 @@ packages: source: hosted version: "2.1.1" characters: - dependency: "direct main" + dependency: transitive description: name: characters sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" diff --git a/pubspec.yaml b/pubspec.yaml index 9c75e71..78e5086 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,6 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - characters: ^1.3.0 diacritic: ^0.1.3 flutter: sdk: flutter From 3d06e5bb55aac4a0465325041e72e38d785ec562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:34:23 +0200 Subject: [PATCH 3/9] test(cryptogram_string_extension): add test cases for extension method --- .../cryptogram_string_extension_test.dart | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 test/model/cryptogram_string_extension_test.dart diff --git a/test/model/cryptogram_string_extension_test.dart b/test/model/cryptogram_string_extension_test.dart new file mode 100644 index 0000000..ff18fc0 --- /dev/null +++ b/test/model/cryptogram_string_extension_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:music_notes/music_notes.dart'; +import 'package:note_names/model/cryptogram_scheme.dart'; + +void main() { + group('CryptogramStringExtension', () { + group('.toCryptogram', () { + test('should return the musical cryptogram of this String', () { + final arnoldSchonberg = + 'A. SCHBEG'.toCryptogram(CryptogramScheme.german()); + expect( + arnoldSchonberg, + [Note.a, Note.e.flat, Note.c, Note.b, Note.b.flat, Note.e, Note.g], + ); + + final belaBartok = 'BE BA'.toCryptogram(CryptogramScheme.french()); + expect(belaBartok, [Note.b, Note.e, Note.b, Note.a]); + + final carlPhilipEmmanuelBach = + 'CFE BACH'.toCryptogram(CryptogramScheme.german()); + expect( + carlPhilipEmmanuelBach, + [Note.c, Note.f, Note.e, Note.b.flat, Note.a, Note.c, Note.b], + ); + + final dimitriSchostakowitsch = + 'D. SCH'.toCryptogram(CryptogramScheme.german()); + expect(dimitriSchostakowitsch, [Note.d, Note.e.flat, Note.c, Note.b]); + + final edvardGried = 'EBG'.toCryptogram(CryptogramScheme.french()); + expect(edvardGried, [Note.e, Note.b, Note.g]); + + final franzSchubert = 'F. SCH'.toCryptogram(CryptogramScheme.german()); + expect(franzSchubert, [Note.f, Note.e.flat, Note.c, Note.b]); + + final gustavHolst = 'GSAH'.toCryptogram(CryptogramScheme.german()); + expect(gustavHolst, [Note.g, Note.e.flat, Note.a, Note.b]); + + final jehanAlain = + 'ALAIN'.toCryptogram(CryptogramScheme.frenchVariant()); + expect(jehanAlain, [Note.a, Note.d, Note.a, Note.a, Note.f]); + + final johannesBrahms = 'BAHS'.toCryptogram(CryptogramScheme.german()); + expect(johannesBrahms, [Note.b.flat, Note.a, Note.b, Note.e.flat]); + + final johannSebastianBach = + 'BACH'.toCryptogram(CryptogramScheme.german()); + expect(johannSebastianBach, [Note.b.flat, Note.a, Note.c, Note.b]); + + final johnCage = 'CAGE'.toCryptogram(CryptogramScheme.german()); + expect(johnCage, [Note.c, Note.a, Note.g, Note.e]); + + final josephHaydn = 'HAYDN'.toCryptogram(CryptogramScheme.german()); + expect(josephHaydn, [Note.b, Note.a, Note.d, Note.d, Note.g]); + + final metaAbegg = 'ABEGG'.toCryptogram(CryptogramScheme.german()); + expect(metaAbegg, [Note.a, Note.b.flat, Note.e, Note.g, Note.g]); + + final nielsGade = 'GADE'.toCryptogram(CryptogramScheme.german()); + expect(nielsGade, [Note.g, Note.a, Note.d, Note.e]); + + final paulSacher = 'SACHER'.toCryptogram(CryptogramScheme.german()); + expect( + paulSacher, + [Note.e.flat, Note.a, Note.c, Note.b, Note.e, Note.d], + ); + + final robertSchumann = 'SCHA'.toCryptogram(CryptogramScheme.german()); + expect(robertSchumann, [Note.e.flat, Note.c, Note.b, Note.a]); + }); + }); + }); +} From 283f7f52c7d83225374d5aee35a50de0b52d053a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:35:49 +0200 Subject: [PATCH 4/9] chore(gitignore): ignore `bin` directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 24476c5..7b659e3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ migrate_working_dir/ .pub-cache/ .pub/ /build/ +/bin/ # Symbolication related app.*.symbols From f0dd25122d567a3e4298b1d3725fbd9358fefac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:39:00 +0200 Subject: [PATCH 5/9] test(main): add `main` test call --- test/model/main.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/model/main.dart b/test/model/main.dart index 0948c9d..2378e01 100644 --- a/test/model/main.dart +++ b/test/model/main.dart @@ -1,7 +1,10 @@ import 'alphabet_test.dart' as alphabet_test; +import 'cryptogram_string_extension_test.dart' + as cryptogram_string_extension_test; import 'name_test.dart' as name_test; void main() { alphabet_test.main(); + cryptogram_string_extension_test.main(); name_test.main(); } From 0aab48bb8d228eca922c89ef569cb175c90a5ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:40:52 +0200 Subject: [PATCH 6/9] test(cryptogram_string_extension): add missing `()` to group name --- test/model/cryptogram_string_extension_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model/cryptogram_string_extension_test.dart b/test/model/cryptogram_string_extension_test.dart index ff18fc0..cc7d097 100644 --- a/test/model/cryptogram_string_extension_test.dart +++ b/test/model/cryptogram_string_extension_test.dart @@ -4,7 +4,7 @@ import 'package:note_names/model/cryptogram_scheme.dart'; void main() { group('CryptogramStringExtension', () { - group('.toCryptogram', () { + group('.toCryptogram()', () { test('should return the musical cryptogram of this String', () { final arnoldSchonberg = 'A. SCHBEG'.toCryptogram(CryptogramScheme.german()); From bb28392844a3845a750fbe6230cb5f8cfee12f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:51:35 +0200 Subject: [PATCH 7/9] test(cryptogram_string_extension): add test case for solmization --- lib/model/cryptogram_scheme.dart | 9 ++++----- test/model/cryptogram_string_extension_test.dart | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/model/cryptogram_scheme.dart b/lib/model/cryptogram_scheme.dart index 098b429..bad6568 100644 --- a/lib/model/cryptogram_scheme.dart +++ b/lib/model/cryptogram_scheme.dart @@ -24,11 +24,10 @@ class CryptogramScheme { RegExp('sol?', caseSensitive: false): Note.g, RegExp('la', caseSensitive: false): Note.a, RegExp('(t|s)i', caseSensitive: false): Note.b, - RegExp('a', caseSensitive: false): Note.a, - RegExp('d', caseSensitive: false): Note.c, - RegExp('r', caseSensitive: false): Note.d, - RegExp('m', caseSensitive: false): Note.e, - RegExp('f', caseSensitive: false): Note.f, + RegExp('a', caseSensitive: false): Note.f, + RegExp('e', caseSensitive: false): Note.d, + RegExp('i', caseSensitive: false): Note.e, + RegExp('u', caseSensitive: false): Note.c, }; /// Arisen late in the 19th century, it was more akin to normal encipherment. diff --git a/test/model/cryptogram_string_extension_test.dart b/test/model/cryptogram_string_extension_test.dart index cc7d097..14b499e 100644 --- a/test/model/cryptogram_string_extension_test.dart +++ b/test/model/cryptogram_string_extension_test.dart @@ -6,6 +6,13 @@ void main() { group('CryptogramStringExtension', () { group('.toCryptogram()', () { test('should return the musical cryptogram of this String', () { + final herculesDuxFerrarie = 'Hercules Dux Ferrarie' + .toCryptogram(CryptogramScheme.solmization()); + expect( + herculesDuxFerrarie, + [Note.d, Note.c, Note.d, Note.c, Note.d, Note.f, Note.e, Note.d], + ); + final arnoldSchonberg = 'A. SCHBEG'.toCryptogram(CryptogramScheme.german()); expect( From 444830837403cfd6c19500ed89a45c02a81e4525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:54:13 +0200 Subject: [PATCH 8/9] docs(cryptogram_string_extension): fix reference caps --- lib/model/cryptogram_scheme.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/model/cryptogram_scheme.dart b/lib/model/cryptogram_scheme.dart index bad6568..340c12f 100644 --- a/lib/model/cryptogram_scheme.dart +++ b/lib/model/cryptogram_scheme.dart @@ -13,7 +13,7 @@ class CryptogramScheme { /// sounds of the solmization syllables of Guido of Arezzo (where 'ut' is the /// root, which we now call 'do', [Note.c]). /// - /// See [Musical Cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Syllables_to_solmization_names). + /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Syllables_to_solmization_names). CryptogramScheme.solmization() : name = 'Solmization', patterns = { @@ -40,7 +40,7 @@ class CryptogramScheme { /// | O | P | Q | R | S | T | U | /// | V | W | X | Y | Z | | | /// - /// See [Musical Cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#French). + /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#French). CryptogramScheme.french() : name = 'French', patterns = { @@ -75,7 +75,7 @@ class CryptogramScheme { /// Derived on the [CryptogramScheme.french] but leaving H = [Note.b] and /// starting the second line with 'I'. /// - /// See [Musical Cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Summary_of_signature_motifs) + /// See [Musical cryptogram](https://en.wikipedia.org/wiki/Musical_cryptogram#Summary_of_signature_motifs) /// (Alain). CryptogramScheme.frenchVariant() : name = 'French variant', From 53dc8ffd4cc92ece1df7fe21a6c4e0e69ab7d8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Jun 2023 01:54:53 +0200 Subject: [PATCH 9/9] refactor(cryptogram_string_extension): remove unused const constructor --- lib/model/cryptogram_scheme.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/model/cryptogram_scheme.dart b/lib/model/cryptogram_scheme.dart index 340c12f..1d49bee 100644 --- a/lib/model/cryptogram_scheme.dart +++ b/lib/model/cryptogram_scheme.dart @@ -6,9 +6,6 @@ class CryptogramScheme { final String name; final Map patterns; - /// Creates a new [CryptogramScheme] from [name] and [patterns]. - const CryptogramScheme(this.name, {required this.patterns}); - /// 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 /// root, which we now call 'do', [Note.c]).