1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum Note {
18 C,
20 D,
22 E,
24 F,
26 G,
28 A,
30 B,
32}
33
34impl Note {
35 fn from_char(c: char) -> Option<Self> {
37 match c {
38 'C' => Some(Self::C),
39 'D' => Some(Self::D),
40 'E' => Some(Self::E),
41 'F' => Some(Self::F),
42 'G' => Some(Self::G),
43 'A' => Some(Self::A),
44 'B' => Some(Self::B),
45 _ => None,
46 }
47 }
48}
49
50impl core::fmt::Display for Note {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 let s = match self {
53 Self::C => "C",
54 Self::D => "D",
55 Self::E => "E",
56 Self::F => "F",
57 Self::G => "G",
58 Self::A => "A",
59 Self::B => "B",
60 };
61 f.write_str(s)
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum Accidental {
72 Sharp,
74 Flat,
76}
77
78impl core::fmt::Display for Accidental {
79 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80 match self {
81 Self::Sharp => f.write_str("#"),
82 Self::Flat => f.write_str("b"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97pub enum ChordQuality {
98 Major,
100 Minor,
102 Diminished,
104 Augmented,
106}
107
108impl core::fmt::Display for ChordQuality {
109 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110 match self {
111 Self::Major => Ok(()),
112 Self::Minor => f.write_str("m"),
113 Self::Diminished => f.write_str("dim"),
114 Self::Augmented => f.write_str("aug"),
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
143pub struct ChordDetail {
144 pub root: Note,
146 pub root_accidental: Option<Accidental>,
148 pub quality: ChordQuality,
150 pub extension: Option<String>,
157 pub bass_note: Option<(Note, Option<Accidental>)>,
159}
160
161impl core::fmt::Display for ChordDetail {
162 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
163 write!(f, "{}", self.root)?;
164 if let Some(ref acc) = self.root_accidental {
165 write!(f, "{acc}")?;
166 }
167 write!(f, "{}", self.quality)?;
168 if let Some(ref ext) = self.extension {
169 f.write_str(ext)?;
170 }
171 if let Some((ref bass, ref bass_acc)) = self.bass_note {
172 write!(f, "/{bass}")?;
173 if let Some(acc) = bass_acc {
174 write!(f, "{acc}")?;
175 }
176 }
177 Ok(())
178 }
179}
180
181#[must_use]
213pub fn parse_chord(input: &str) -> Option<ChordDetail> {
214 let mut chars = input.chars().peekable();
215
216 let root = Note::from_char(*chars.peek()?)?;
218 chars.next();
219
220 let root_accidental = match chars.peek() {
222 Some('#') => {
223 chars.next();
224 Some(Accidental::Sharp)
225 }
226 Some('b') => {
227 chars.next();
234 Some(Accidental::Flat)
235 }
236 _ => None,
237 };
238
239 let rest: String = chars.collect();
241
242 let (quality_ext_str, bass_str) = if let Some(slash_pos) = rest.find('/') {
244 let (before, after) = rest.split_at(slash_pos);
245 (before, Some(&after[1..]))
247 } else {
248 (rest.as_str(), None)
249 };
250
251 let bass_note = if let Some(bass) = bass_str {
253 parse_note_with_accidental(bass)
254 } else {
255 None
256 };
257
258 if bass_str.is_some() && bass_note.is_none() {
260 return None;
263 }
264
265 let (quality, extension) = parse_quality_and_extension(quality_ext_str);
267
268 Some(ChordDetail {
269 root,
270 root_accidental,
271 quality,
272 extension,
273 bass_note,
274 })
275}
276
277fn parse_note_with_accidental(s: &str) -> Option<(Note, Option<Accidental>)> {
279 let mut chars = s.chars();
280 let note = Note::from_char(chars.next()?)?;
281 let accidental = match chars.next() {
282 Some('#') => Some(Accidental::Sharp),
283 Some('b') => Some(Accidental::Flat),
284 Some(_) => return None, None => None,
286 };
287 if chars.next().is_some() {
289 return None;
290 }
291 Some((note, accidental))
292}
293
294fn parse_quality_and_extension(s: &str) -> (ChordQuality, Option<String>) {
299 if s.is_empty() {
300 return (ChordQuality::Major, None);
301 }
302
303 if let Some(rest) = s.strip_prefix("dim") {
309 let ext = non_empty_string(rest);
310 return (ChordQuality::Diminished, ext);
311 }
312
313 if let Some(rest) = s.strip_prefix("aug") {
315 let ext = non_empty_string(rest);
316 return (ChordQuality::Augmented, ext);
317 }
318
319 if let Some(rest) = s.strip_prefix('+') {
321 let ext = non_empty_string(rest);
322 return (ChordQuality::Augmented, ext);
323 }
324
325 if let Some(rest) = s.strip_prefix("min") {
327 let ext = non_empty_string(rest);
328 return (ChordQuality::Minor, ext);
329 }
330
331 if let Some(rest) = s.strip_prefix("maj") {
333 let ext = if rest.is_empty() {
335 Some("maj".to_string())
336 } else {
337 Some(format!("maj{rest}"))
338 };
339 return (ChordQuality::Major, ext);
340 }
341
342 if let Some(rest) = s.strip_prefix('m') {
344 let ext = non_empty_string(rest);
347 return (ChordQuality::Minor, ext);
348 }
349
350 if s.starts_with("sus") {
352 return (ChordQuality::Major, Some(s.to_string()));
353 }
354
355 if s.starts_with("add") {
357 return (ChordQuality::Major, Some(s.to_string()));
358 }
359
360 if s.starts_with(|c: char| c.is_ascii_digit()) {
362 return (ChordQuality::Major, Some(s.to_string()));
363 }
364
365 if let Some(rest) = s.strip_prefix('°') {
367 let ext = non_empty_string(rest);
368 return (ChordQuality::Diminished, ext);
369 }
370
371 (ChordQuality::Major, Some(s.to_string()))
374}
375
376fn non_empty_string(s: &str) -> Option<String> {
378 if s.is_empty() {
379 None
380 } else {
381 Some(s.to_string())
382 }
383}
384
385#[cfg(test)]
390mod tests {
391 use super::*;
392
393 fn pd(input: &str) -> ChordDetail {
397 parse_chord(input).unwrap_or_else(|| panic!("expected Some for chord '{input}'"))
398 }
399
400 #[test]
403 fn basic_major_chords() {
404 for (input, expected_root) in [
405 ("C", Note::C),
406 ("D", Note::D),
407 ("E", Note::E),
408 ("F", Note::F),
409 ("G", Note::G),
410 ("A", Note::A),
411 ("B", Note::B),
412 ] {
413 let detail = pd(input);
414 assert_eq!(detail.root, expected_root, "root for '{input}'");
415 assert_eq!(detail.root_accidental, None, "accidental for '{input}'");
416 assert_eq!(detail.quality, ChordQuality::Major, "quality for '{input}'");
417 assert_eq!(detail.extension, None, "extension for '{input}'");
418 assert_eq!(detail.bass_note, None, "bass for '{input}'");
419 }
420 }
421
422 #[test]
425 fn minor_chords() {
426 let detail = pd("Am");
427 assert_eq!(detail.root, Note::A);
428 assert_eq!(detail.quality, ChordQuality::Minor);
429 assert_eq!(detail.extension, None);
430
431 let detail = pd("Em");
432 assert_eq!(detail.root, Note::E);
433 assert_eq!(detail.quality, ChordQuality::Minor);
434
435 let detail = pd("Dm");
436 assert_eq!(detail.root, Note::D);
437 assert_eq!(detail.quality, ChordQuality::Minor);
438 }
439
440 #[test]
441 fn minor_with_min_suffix() {
442 let detail = pd("Amin");
443 assert_eq!(detail.root, Note::A);
444 assert_eq!(detail.quality, ChordQuality::Minor);
445 assert_eq!(detail.extension, None);
446 }
447
448 #[test]
451 fn sharp_major() {
452 let detail = pd("C#");
453 assert_eq!(detail.root, Note::C);
454 assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
455 assert_eq!(detail.quality, ChordQuality::Major);
456 }
457
458 #[test]
459 fn flat_major() {
460 let detail = pd("Db");
461 assert_eq!(detail.root, Note::D);
462 assert_eq!(detail.root_accidental, Some(Accidental::Flat));
463 assert_eq!(detail.quality, ChordQuality::Major);
464 }
465
466 #[test]
467 fn sharp_minor() {
468 let detail = pd("F#m");
469 assert_eq!(detail.root, Note::F);
470 assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
471 assert_eq!(detail.quality, ChordQuality::Minor);
472 }
473
474 #[test]
475 fn flat_minor() {
476 let detail = pd("Bbm");
477 assert_eq!(detail.root, Note::B);
478 assert_eq!(detail.root_accidental, Some(Accidental::Flat));
479 assert_eq!(detail.quality, ChordQuality::Minor);
480 }
481
482 #[test]
483 fn bb_flat() {
484 let detail = pd("Bb");
485 assert_eq!(detail.root, Note::B);
486 assert_eq!(detail.root_accidental, Some(Accidental::Flat));
487 assert_eq!(detail.quality, ChordQuality::Major);
488 }
489
490 #[test]
493 fn major_seventh() {
494 let detail = pd("Cmaj7");
495 assert_eq!(detail.root, Note::C);
496 assert_eq!(detail.quality, ChordQuality::Major);
497 assert_eq!(detail.extension.as_deref(), Some("maj7"));
498 }
499
500 #[test]
501 fn minor_seventh() {
502 let detail = pd("Am7");
503 assert_eq!(detail.root, Note::A);
504 assert_eq!(detail.quality, ChordQuality::Minor);
505 assert_eq!(detail.extension.as_deref(), Some("7"));
506 }
507
508 #[test]
509 fn dominant_seventh() {
510 let detail = pd("G7");
511 assert_eq!(detail.root, Note::G);
512 assert_eq!(detail.quality, ChordQuality::Major);
513 assert_eq!(detail.extension.as_deref(), Some("7"));
514 }
515
516 #[test]
517 fn ninth_chord() {
518 let detail = pd("G9");
519 assert_eq!(detail.root, Note::G);
520 assert_eq!(detail.quality, ChordQuality::Major);
521 assert_eq!(detail.extension.as_deref(), Some("9"));
522 }
523
524 #[test]
525 fn sus4() {
526 let detail = pd("Dsus4");
527 assert_eq!(detail.root, Note::D);
528 assert_eq!(detail.quality, ChordQuality::Major);
529 assert_eq!(detail.extension.as_deref(), Some("sus4"));
530 }
531
532 #[test]
533 fn sus2() {
534 let detail = pd("Asus2");
535 assert_eq!(detail.root, Note::A);
536 assert_eq!(detail.quality, ChordQuality::Major);
537 assert_eq!(detail.extension.as_deref(), Some("sus2"));
538 }
539
540 #[test]
541 fn add9() {
542 let detail = pd("Cadd9");
543 assert_eq!(detail.root, Note::C);
544 assert_eq!(detail.quality, ChordQuality::Major);
545 assert_eq!(detail.extension.as_deref(), Some("add9"));
546 }
547
548 #[test]
549 fn minor_major_seventh() {
550 let detail = pd("Cmmaj7");
554 assert_eq!(detail.root, Note::C);
555 assert_eq!(detail.quality, ChordQuality::Minor);
556 assert_eq!(detail.extension.as_deref(), Some("maj7"));
558 }
559
560 #[test]
561 fn seventh_sus4() {
562 let detail = pd("G7sus4");
563 assert_eq!(detail.root, Note::G);
564 assert_eq!(detail.quality, ChordQuality::Major);
565 assert_eq!(detail.extension.as_deref(), Some("7sus4"));
566 }
567
568 #[test]
569 fn sixth_chord() {
570 let detail = pd("C6");
571 assert_eq!(detail.root, Note::C);
572 assert_eq!(detail.quality, ChordQuality::Major);
573 assert_eq!(detail.extension.as_deref(), Some("6"));
574 }
575
576 #[test]
577 fn minor_sixth() {
578 let detail = pd("Am6");
579 assert_eq!(detail.root, Note::A);
580 assert_eq!(detail.quality, ChordQuality::Minor);
581 assert_eq!(detail.extension.as_deref(), Some("6"));
582 }
583
584 #[test]
585 fn eleventh_chord() {
586 let detail = pd("G11");
587 assert_eq!(detail.root, Note::G);
588 assert_eq!(detail.quality, ChordQuality::Major);
589 assert_eq!(detail.extension.as_deref(), Some("11"));
590 }
591
592 #[test]
593 fn thirteenth_chord() {
594 let detail = pd("C13");
595 assert_eq!(detail.root, Note::C);
596 assert_eq!(detail.quality, ChordQuality::Major);
597 assert_eq!(detail.extension.as_deref(), Some("13"));
598 }
599
600 #[test]
603 fn diminished() {
604 let detail = pd("Bdim");
605 assert_eq!(detail.root, Note::B);
606 assert_eq!(detail.quality, ChordQuality::Diminished);
607 assert_eq!(detail.extension, None);
608 }
609
610 #[test]
611 fn diminished_seventh() {
612 let detail = pd("Cdim7");
613 assert_eq!(detail.root, Note::C);
614 assert_eq!(detail.quality, ChordQuality::Diminished);
615 assert_eq!(detail.extension.as_deref(), Some("7"));
616 }
617
618 #[test]
619 fn diminished_symbol() {
620 let detail = pd("B°");
621 assert_eq!(detail.root, Note::B);
622 assert_eq!(detail.quality, ChordQuality::Diminished);
623 assert_eq!(detail.extension, None);
624 }
625
626 #[test]
627 fn augmented() {
628 let detail = pd("Faug");
629 assert_eq!(detail.root, Note::F);
630 assert_eq!(detail.quality, ChordQuality::Augmented);
631 assert_eq!(detail.extension, None);
632 }
633
634 #[test]
635 fn augmented_plus_symbol() {
636 let detail = pd("C+");
637 assert_eq!(detail.root, Note::C);
638 assert_eq!(detail.quality, ChordQuality::Augmented);
639 assert_eq!(detail.extension, None);
640 }
641
642 #[test]
643 fn augmented_seventh() {
644 let detail = pd("Caug7");
645 assert_eq!(detail.root, Note::C);
646 assert_eq!(detail.quality, ChordQuality::Augmented);
647 assert_eq!(detail.extension.as_deref(), Some("7"));
648 }
649
650 #[test]
653 fn slash_chord_simple() {
654 let detail = pd("G/B");
655 assert_eq!(detail.root, Note::G);
656 assert_eq!(detail.quality, ChordQuality::Major);
657 assert_eq!(detail.bass_note, Some((Note::B, None)));
658 }
659
660 #[test]
661 fn slash_chord_minor() {
662 let detail = pd("Am/E");
663 assert_eq!(detail.root, Note::A);
664 assert_eq!(detail.quality, ChordQuality::Minor);
665 assert_eq!(detail.bass_note, Some((Note::E, None)));
666 }
667
668 #[test]
669 fn slash_chord_with_accidental_bass() {
670 let detail = pd("C/Bb");
671 assert_eq!(detail.root, Note::C);
672 assert_eq!(detail.bass_note, Some((Note::B, Some(Accidental::Flat))));
673 }
674
675 #[test]
676 fn slash_chord_with_sharp_bass() {
677 let detail = pd("Am/G#");
678 assert_eq!(detail.root, Note::A);
679 assert_eq!(detail.quality, ChordQuality::Minor);
680 assert_eq!(detail.bass_note, Some((Note::G, Some(Accidental::Sharp))));
681 }
682
683 #[test]
684 fn slash_chord_extended() {
685 let detail = pd("Am7/G");
686 assert_eq!(detail.root, Note::A);
687 assert_eq!(detail.quality, ChordQuality::Minor);
688 assert_eq!(detail.extension.as_deref(), Some("7"));
689 assert_eq!(detail.bass_note, Some((Note::G, None)));
690 }
691
692 #[test]
693 fn slash_chord_sharp_root() {
694 let detail = pd("F#m/E");
695 assert_eq!(detail.root, Note::F);
696 assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
697 assert_eq!(detail.quality, ChordQuality::Minor);
698 assert_eq!(detail.bass_note, Some((Note::E, None)));
699 }
700
701 #[test]
704 fn empty_string() {
705 assert!(parse_chord("").is_none());
706 }
707
708 #[test]
709 fn lowercase_root() {
710 assert!(parse_chord("am").is_none());
712 }
713
714 #[test]
715 fn non_note_root() {
716 assert!(parse_chord("Hm").is_none());
717 assert!(parse_chord("X").is_none());
718 }
719
720 #[test]
721 fn numeric_only() {
722 assert!(parse_chord("7").is_none());
723 }
724
725 #[test]
726 fn slash_with_invalid_bass() {
727 assert!(parse_chord("G/X").is_none());
728 assert!(parse_chord("G/").is_none());
729 }
730
731 #[test]
732 fn slash_bass_too_long() {
733 assert!(parse_chord("G/Bm").is_none());
735 }
736
737 #[test]
738 fn multi_slash_is_invalid() {
739 assert!(parse_chord("C/D/E").is_none());
742 }
743
744 #[test]
747 fn display_basic_major() {
748 assert_eq!(pd("C").to_string(), "C");
749 }
750
751 #[test]
752 fn display_minor() {
753 assert_eq!(pd("Am").to_string(), "Am");
754 }
755
756 #[test]
757 fn display_sharp_minor_seventh() {
758 assert_eq!(pd("C#m7").to_string(), "C#m7");
759 }
760
761 #[test]
762 fn display_slash_chord() {
763 assert_eq!(pd("G/B").to_string(), "G/B");
764 }
765
766 #[test]
767 fn display_flat_chord() {
768 assert_eq!(pd("Bb").to_string(), "Bb");
769 }
770
771 #[test]
772 fn display_diminished() {
773 assert_eq!(pd("Bdim").to_string(), "Bdim");
774 }
775
776 #[test]
777 fn display_augmented() {
778 assert_eq!(pd("Faug").to_string(), "Faug");
779 }
780
781 #[test]
782 fn display_sus4() {
783 assert_eq!(pd("Dsus4").to_string(), "Dsus4");
784 }
785
786 #[test]
787 fn display_slash_with_accidental() {
788 assert_eq!(pd("C/Bb").to_string(), "C/Bb");
789 }
790
791 #[test]
792 fn display_complex_chord() {
793 assert_eq!(pd("F#m7/E").to_string(), "F#m7/E");
794 }
795
796 #[test]
799 fn maj_alone() {
800 let detail = pd("Cmaj");
802 assert_eq!(detail.root, Note::C);
803 assert_eq!(detail.quality, ChordQuality::Major);
804 assert_eq!(detail.extension.as_deref(), Some("maj"));
805 }
806
807 #[test]
808 fn maj9() {
809 let detail = pd("Cmaj9");
810 assert_eq!(detail.root, Note::C);
811 assert_eq!(detail.quality, ChordQuality::Major);
812 assert_eq!(detail.extension.as_deref(), Some("maj9"));
813 }
814
815 #[test]
816 fn sharp_augmented() {
817 let detail = pd("G#+");
818 assert_eq!(detail.root, Note::G);
819 assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
820 assert_eq!(detail.quality, ChordQuality::Augmented);
821 }
822
823 #[test]
824 fn flat_diminished() {
825 let detail = pd("Ebdim");
826 assert_eq!(detail.root, Note::E);
827 assert_eq!(detail.root_accidental, Some(Accidental::Flat));
828 assert_eq!(detail.quality, ChordQuality::Diminished);
829 }
830
831 #[test]
832 fn minor_add9() {
833 let detail = pd("Amadd9");
834 assert_eq!(detail.root, Note::A);
835 assert_eq!(detail.quality, ChordQuality::Minor);
836 assert_eq!(detail.extension.as_deref(), Some("add9"));
838 }
839
840 #[test]
841 fn empty_bass_string_is_invalid() {
842 assert!(parse_chord("G/").is_none());
844 }
845}