1use std::fmt;
20
21use crate::music::MusicError;
22use crate::music::pitch::{Spelling, class_name, letter_class, read_class};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum Mode {
27 Major,
29 Minor,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub struct Key {
36 pub tonic: u8,
38 pub mode: Mode,
40}
41
42impl Key {
43 #[must_use]
45 pub fn new(tonic: u8, mode: Mode) -> Self {
46 Self {
47 tonic: tonic % 12,
48 mode,
49 }
50 }
51
52 #[must_use]
55 pub fn relative(self) -> Self {
56 match self.mode {
57 Mode::Major => Self::new(self.tonic + 9, Mode::Minor),
58 Mode::Minor => Self::new(self.tonic + 3, Mode::Major),
59 }
60 }
61
62 #[must_use]
64 pub fn parallel(self) -> Self {
65 match self.mode {
66 Mode::Major => Self::new(self.tonic, Mode::Minor),
67 Mode::Minor => Self::new(self.tonic, Mode::Major),
68 }
69 }
70
71 #[must_use]
74 pub fn spelling(self) -> Spelling {
75 let flats: &[u8] = match self.mode {
76 Mode::Major => &[5, 10, 3, 8, 1],
77 Mode::Minor => &[2, 7, 0, 5, 10, 3],
78 };
79 if flats.contains(&self.tonic) {
80 Spelling::Flats
81 } else {
82 Spelling::Sharps
83 }
84 }
85
86 #[must_use]
90 pub fn camelot(self) -> Camelot {
91 let major = match self.mode {
92 Mode::Major => self.tonic,
93 Mode::Minor => self.relative().tonic,
94 };
95 Camelot {
96 number: (7 * major + 7) % 12 + 1,
97 mode: self.mode,
98 }
99 }
100
101 #[must_use]
104 pub fn open_key(self) -> OpenKey {
105 let camelot = self.camelot();
106 OpenKey {
107 number: (camelot.number + 4) % 12 + 1,
108 mode: self.mode,
109 }
110 }
111}
112
113impl fmt::Display for Key {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 let mode = match self.mode {
116 Mode::Major => "major",
117 Mode::Minor => "minor",
118 };
119 write!(f, "{} {mode}", class_name(self.tonic, self.spelling()))
120 }
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
125pub struct Camelot {
126 pub number: u8,
128 pub mode: Mode,
130}
131
132impl Camelot {
133 #[must_use]
135 pub fn key(self) -> Key {
136 let major = Key::new((7 * (self.number + 4)) % 12, Mode::Major);
138 match self.mode {
139 Mode::Major => major,
140 Mode::Minor => major.relative(),
141 }
142 }
143
144 #[must_use]
146 pub fn fifth_up(self) -> Self {
147 Self {
148 number: self.number % 12 + 1,
149 ..self
150 }
151 }
152
153 #[must_use]
155 pub fn fifth_down(self) -> Self {
156 Self {
157 number: (self.number + 10) % 12 + 1,
158 ..self
159 }
160 }
161}
162
163impl fmt::Display for Camelot {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 let letter = match self.mode {
166 Mode::Minor => 'A',
167 Mode::Major => 'B',
168 };
169 write!(f, "{}{letter}", self.number)
170 }
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
176pub struct OpenKey {
177 pub number: u8,
179 pub mode: Mode,
181}
182
183impl OpenKey {
184 #[must_use]
186 pub fn key(self) -> Key {
187 Camelot {
188 number: (self.number + 6) % 12 + 1,
189 mode: self.mode,
190 }
191 .key()
192 }
193}
194
195impl fmt::Display for OpenKey {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 let letter = match self.mode {
198 Mode::Minor => 'm',
199 Mode::Major => 'd',
200 };
201 write!(f, "{}{letter}", self.number)
202 }
203}
204
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub enum KeyAssumption {
208 BareCapital,
210 BareLowercase,
212}
213
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub struct KeyReading {
217 pub key: Key,
219 pub assumption: Option<KeyAssumption>,
221}
222
223pub fn parse_key(text: &str) -> Result<KeyReading, MusicError> {
233 let unknown = || MusicError::UnknownKey(text.to_owned());
234 let trimmed = text.trim();
235 if let Some(key) = code(trimmed) {
236 return Ok(KeyReading {
237 key,
238 assumption: None,
239 });
240 }
241 letter_key(trimmed).ok_or_else(unknown)
242}
243
244fn code(text: &str) -> Option<Key> {
246 let split = text.find(|c: char| !c.is_ascii_digit())?;
247 let (digits, rest) = text.split_at(split);
248 if digits.starts_with('0') {
249 return None;
250 }
251 let number: u8 = digits.parse().ok().filter(|n| (1..=12).contains(n))?;
252 match rest.trim() {
253 "A" | "a" => Some(
254 Camelot {
255 number,
256 mode: Mode::Minor,
257 }
258 .key(),
259 ),
260 "B" | "b" => Some(
261 Camelot {
262 number,
263 mode: Mode::Major,
264 }
265 .key(),
266 ),
267 "m" => Some(
268 OpenKey {
269 number,
270 mode: Mode::Minor,
271 }
272 .key(),
273 ),
274 "d" => Some(
275 OpenKey {
276 number,
277 mode: Mode::Major,
278 }
279 .key(),
280 ),
281 _ => None,
282 }
283}
284
285fn letter_key(text: &str) -> Option<KeyReading> {
287 let first = text.chars().next()?;
288 let german = {
289 let lower = text.to_lowercase();
290 lower.ends_with("dur") || lower.ends_with("moll")
291 };
292 let (class, rest) = german_class(text, german).or_else(|| english_class(text))?;
293 let rest = rest.trim();
294 let rest = match rest.strip_prefix('-') {
295 Some(word) if !word.is_empty() => word,
296 _ => rest,
297 };
298 let mode = match rest {
299 "" => None,
300 "M" => Some(Mode::Major),
301 "m" | "-" | "−" | "–" => Some(Mode::Minor),
302 word => match word.to_lowercase().as_str() {
303 "maj" | "major" | "dur" => Some(Mode::Major),
304 "min" | "minor" | "moll" => Some(Mode::Minor),
305 _ => return None,
306 },
307 };
308 let tonic = u8::try_from(class.rem_euclid(12)).ok()?;
309 let (mode, assumption) = match mode {
310 Some(mode) => (mode, None),
311 None if first.is_ascii_lowercase() => (Mode::Minor, Some(KeyAssumption::BareLowercase)),
312 None => (Mode::Major, Some(KeyAssumption::BareCapital)),
313 };
314 Some(KeyReading {
315 key: Key::new(tonic, mode),
316 assumption,
317 })
318}
319
320fn english_class(text: &str) -> Option<(i16, &str)> {
322 let (class, rest) = read_class(text)?;
323 let trimmed = rest.trim_start();
324 for (word, step) in [("sharp", 1), ("flat", -1)] {
325 if let Some(after) = trimmed.strip_prefix(word)
326 && ends_word(after)
327 {
328 return Some((class + step, after));
329 }
330 }
331 Some((class, rest))
332}
333
334fn ends_word(rest: &str) -> bool {
336 rest.is_empty() || rest.starts_with(|c: char| c.is_whitespace() || c == '-')
337}
338
339fn german_class(text: &str, german: bool) -> Option<(i16, &str)> {
343 let mut chars = text.chars();
344 let letter = chars.next()?.to_ascii_uppercase();
345 let rest = chars.as_str();
346 let class = match letter {
347 'H' => 11,
348 'B' if german => 10,
349 _ => letter_class(letter)?,
350 };
351 let lower = rest.to_ascii_lowercase();
352 let suffix = [("is", 1), ("es", -1), ("s", -1)]
353 .into_iter()
354 .find(|(suffix, _)| lower.starts_with(suffix) && ends_word(&rest[suffix.len()..]));
355 match suffix {
356 Some(("s", _)) if !matches!(letter, 'A' | 'E') => None,
357 Some((_, _)) if letter == 'B' => None,
358 Some((suffix, step)) => Some((class + step, &rest[suffix.len()..])),
359 None if letter == 'H' || (letter == 'B' && german && ends_word(rest)) => {
360 Some((class, rest))
361 }
362 None => None,
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 fn read(text: &str) -> (Key, Option<KeyAssumption>) {
371 let reading = parse_key(text).unwrap_or_else(|error| panic!("{text}: {error}"));
372 (reading.key, reading.assumption)
373 }
374
375 #[test]
376 fn parses_the_key_forms() {
377 let minor = |tonic| Key::new(tonic, Mode::Minor);
378 let major = |tonic| Key::new(tonic, Mode::Major);
379 let cases = [
380 ("Am", minor(9)),
381 ("A min", minor(9)),
382 ("A minor", minor(9)),
383 ("A-", minor(9)),
384 ("A−", minor(9)),
385 ("A maj", major(9)),
386 ("A major", major(9)),
387 ("AM", major(9)),
388 ("A dur", major(9)),
389 ("A# major", major(10)),
390 ("A♯ maj", major(10)),
391 ("Bb major", major(10)),
392 ("B♭ M", major(10)),
393 ("A sharp major", major(10)),
394 ("B flat minor", minor(10)),
395 ("C sharp minor", minor(1)),
396 ("E♭ moll", minor(3)),
397 ("F## major", major(7)),
398 ("Gbb minor", minor(5)),
399 ("H dur", major(11)),
400 ("B moll", minor(10)),
401 ("Fis moll", minor(6)),
402 ("Es dur", major(3)),
403 ("As dur", major(8)),
404 ("Cis-moll", minor(1)),
405 ("8A", minor(9)),
406 ("8B", major(0)),
407 ("1m", minor(9)),
408 ("1d", major(0)),
409 ("12 B", major(4)),
410 ];
411 for (text, key) in cases {
412 assert_eq!(read(text), (key, None), "{text}");
413 }
414 }
415
416 #[test]
417 fn a_bare_letter_is_flagged() {
418 assert_eq!(
419 read("a"),
420 (Key::new(9, Mode::Minor), Some(KeyAssumption::BareLowercase))
421 );
422 assert_eq!(
423 read("A"),
424 (Key::new(9, Mode::Major), Some(KeyAssumption::BareCapital))
425 );
426 assert_eq!(
427 read("f#"),
428 (Key::new(6, Mode::Minor), Some(KeyAssumption::BareLowercase))
429 );
430 for text in ["A#", "A♯", "Bb", "B♭", "A sharp", "B flat", "H"] {
431 assert_eq!(read(text).1, Some(KeyAssumption::BareCapital), "{text}");
432 }
433 }
434
435 #[test]
436 fn rejects_text_outside_the_grammar() {
437 for text in [
438 "13A", "0A", "Z", "Am7", "A7", "", "8C", "13m", "A minorr", "Bes", "A dorian",
439 ] {
440 assert!(parse_key(text).is_err(), "{text}");
441 }
442 }
443
444 #[test]
445 fn camelot_and_open_key_follow_the_formulas() {
446 for number in 1..=12u8 {
447 let major = Camelot {
448 number,
449 mode: Mode::Major,
450 }
451 .key();
452 assert_eq!(u16::from(major.tonic), (7 * (u16::from(number) + 4)) % 12);
453 let minor = Camelot {
454 number,
455 mode: Mode::Minor,
456 }
457 .key();
458 assert_eq!(minor, major.relative());
459 for key in [major, minor] {
460 assert_eq!(key.camelot().number, number);
461 assert_eq!(key.open_key().number, (number + 4) % 12 + 1);
462 assert_eq!(key.open_key().key(), key);
463 }
464 }
465 }
466
467 #[test]
468 fn every_key_round_trips_through_its_codes() {
469 for tonic in 0..12 {
470 for mode in [Mode::Major, Mode::Minor] {
471 let key = Key::new(tonic, mode);
472 assert_eq!(read(&key.camelot().to_string()).0, key);
473 assert_eq!(read(&key.open_key().to_string()).0, key);
474 assert_eq!(read(&key.to_string()).0, key);
475 }
476 }
477 }
478
479 #[test]
480 fn neighbours_are_a_fifth_or_the_relative_key() {
481 for tonic in 0..12 {
482 for mode in [Mode::Major, Mode::Minor] {
483 let key = Key::new(tonic, mode);
484 let code = key.camelot();
485 assert_eq!(code.fifth_up().key(), Key::new(tonic + 7, mode));
486 assert_eq!(code.fifth_down().key(), Key::new(tonic + 5, mode));
487 assert_eq!(code.fifth_up().fifth_down(), code);
488 let other = Camelot {
489 mode: key.relative().mode,
490 ..code
491 };
492 assert_eq!(other.key(), key.relative());
493 assert_eq!(key.parallel().tonic, tonic);
494 }
495 }
496 assert_eq!(
497 Camelot {
498 number: 12,
499 mode: Mode::Major
500 }
501 .fifth_up()
502 .number,
503 1
504 );
505 assert_eq!(
506 Camelot {
507 number: 1,
508 mode: Mode::Major
509 }
510 .fifth_down()
511 .number,
512 12
513 );
514 }
515
516 #[test]
517 fn spells_by_the_display_rule() {
518 assert_eq!(Key::new(10, Mode::Major).to_string(), "Bb major");
519 assert_eq!(Key::new(6, Mode::Minor).to_string(), "F# minor");
520 assert_eq!(Key::new(10, Mode::Minor).to_string(), "Bb minor");
521 assert_eq!(Key::new(8, Mode::Minor).to_string(), "G# minor");
522 assert_eq!(Key::new(3, Mode::Minor).to_string(), "Eb minor");
523 assert_eq!(Key::new(1, Mode::Minor).to_string(), "C# minor");
524 }
525}