main
rs 730 lines 23.9 KB
Raw
1 //! Chord symbols: parsing (`F#m7b5`, `Bb13#11/D`, `C6/9`), spelling, and identification
2 //! from a set of notes.
3
4 use anyhow::{Result, bail};
5
6 use super::interval::Interval;
7 use super::pitch::Note;
8
9 #[derive(Debug, Clone, PartialEq)]
10 pub struct Chord {
11 pub root: Note,
12 pub bass: Option<Note>,
13 /// Chord tones above the root, sorted by degree, root (`1`) included.
14 pub intervals: Vec<Interval>,
15 pub symbol: String,
16 }
17
18 #[derive(Default)]
19 struct Spec {
20 major: bool,
21 minor: bool,
22 dim: bool,
23 half_dim: bool,
24 aug: bool,
25 sus: Option<u8>,
26 power: bool,
27 sixth: bool,
28 seventh: bool,
29 nine: bool,
30 eleven: bool,
31 thirteen: bool,
32 adds: Vec<Interval>,
33 no3: bool,
34 no5: bool,
35 alt: bool,
36 /// (accidental, degree number) from tokens such as `b5`, `#9`, `#11`, `b13`.
37 alterations: Vec<(i32, u32)>,
38 seen_number: bool,
39 }
40
41 fn take<'a>(r: &'a str, options: &[&str]) -> Option<&'a str> {
42 options.iter().find_map(|o| r.strip_prefix(o))
43 }
44
45 impl Chord {
46 pub fn parse(symbol: &str) -> Result<Chord> {
47 let original = symbol.trim();
48 let s = original
49 .replace("6/9", "69")
50 .replace(['(', ')', ',', ' '], "");
51 let mut chars = s.chars();
52 let letter = chars.next().filter(|c| matches!(c, 'A'..='G'));
53 let Some(letter) = letter else {
54 bail!(
55 "not a chord symbol: `{original}` (must start with a note A-G, e.g. Cmaj7, F#m7b5)"
56 );
57 };
58 let mut rest = &s[1..];
59 let mut acc = 0i8;
60 if let Some(c) = rest.chars().next() {
61 match c {
62 '#' | '♯' => acc = 1,
63 'b' | '♭' => acc = -1,
64 _ => {}
65 }
66 if acc != 0 {
67 rest = &rest[c.len_utf8()..];
68 }
69 }
70 let root = Note::parse(&format!(
71 "{letter}{}",
72 match acc {
73 1 => "#",
74 -1 => "b",
75 _ => "",
76 }
77 ))?;
78
79 let mut bass = None;
80 if let Some((head, tail)) = rest.rsplit_once('/') {
81 match Note::parse(tail) {
82 Ok(n) => bass = Some(n),
83 Err(_) => bail!("bad bass note `{tail}` in `{original}`"),
84 }
85 rest = head;
86 }
87
88 let spec = parse_suffix(rest, original)?;
89 Ok(Chord {
90 root,
91 bass,
92 intervals: build_intervals(&spec),
93 symbol: original.to_string(),
94 })
95 }
96
97 pub fn notes(&self) -> Vec<Note> {
98 self.intervals
99 .iter()
100 .map(|i| self.root.up(i.steps, i.semis))
101 .collect()
102 }
103
104 pub fn formula(&self) -> String {
105 self.intervals
106 .iter()
107 .map(|i| i.degree_token())
108 .collect::<Vec<_>>()
109 .join(" ")
110 }
111
112 /// Pitch classes of the chord tones, sorted and de-duplicated.
113 pub fn pcs(&self) -> Vec<i32> {
114 let mut v: Vec<i32> = self.notes().iter().map(|n| n.pc()).collect();
115 v.sort_unstable();
116 v.dedup();
117 v
118 }
119
120 pub fn has(&self, steps: usize, semis: i32) -> bool {
121 self.intervals
122 .iter()
123 .any(|i| i.steps == steps && i.semis == semis)
124 }
125
126 pub fn third(&self) -> Option<Interval> {
127 self.intervals.iter().copied().find(|i| i.steps == 2)
128 }
129
130 pub fn fifth(&self) -> Option<Interval> {
131 self.intervals.iter().copied().find(|i| i.steps == 4)
132 }
133
134 pub fn seventh(&self) -> Option<Interval> {
135 self.intervals.iter().copied().find(|i| i.steps == 6)
136 }
137
138 pub fn is_minor_third(&self) -> bool {
139 self.has(2, 3)
140 }
141
142 /// A major third plus a minor seventh: functions as a dominant.
143 pub fn is_dominant(&self) -> bool {
144 self.has(2, 4) && self.has(6, 10)
145 }
146
147 /// Re-spell the chord on a different root (used by transposition).
148 pub fn with_root(&self, root: Note, bass: Option<Note>) -> Chord {
149 Chord {
150 root,
151 bass,
152 intervals: self.intervals.clone(),
153 symbol: String::new(),
154 }
155 }
156
157 /// Canonical symbol, e.g. `Bb13#11/D`.
158 pub fn name(&self) -> String {
159 let mut s = format!("{}{}", self.root.name(), self.suffix());
160 if let Some(b) = self.bass {
161 s.push('/');
162 s.push_str(&b.name());
163 }
164 s
165 }
166
167 /// The quality suffix in a compact canonical form (`m7b5`, `maj9`, `7#9`, `sus4`, ...).
168 pub fn suffix(&self) -> String {
169 let third = self.third().map(|i| i.semis);
170 let fifth = self.fifth().map(|i| i.semis);
171 let seventh = self.seventh().map(|i| i.semis);
172 let nat = |steps: usize, semis: i32| self.has(steps, semis);
173 let sus2 = self.intervals.iter().any(|i| i.steps == 1 && i.semis == 2) && third.is_none();
174 let sus4 = nat(3, 5) && third.is_none();
175 let mut out = String::new();
176 let mut alts: Vec<String> = Vec::new();
177
178 // Highest stacked natural extension replaces the plain 7.
179 let ext = if nat(12, 21) {
180 Some(13)
181 } else if nat(10, 17) && nat(8, 14) {
182 Some(11)
183 } else if nat(8, 14) {
184 Some(9)
185 } else {
186 None
187 };
188 let dim_triad = third == Some(3) && fifth == Some(6);
189 match (third, fifth, seventh) {
190 (Some(3), Some(6), Some(10)) => out.push_str("m7b5"),
191 (Some(3), Some(6), Some(9)) => out.push_str("dim7"),
192 (Some(3), Some(6), None) => out.push_str("dim"),
193 (Some(4), Some(8), None) => out.push_str("aug"),
194 (Some(4), _, Some(11)) => {
195 out.push_str("maj");
196 out.push_str(&ext.map_or("7".into(), |e| e.to_string()));
197 }
198 (Some(3), _, Some(11)) => {
199 out.push_str("mMaj");
200 out.push_str(&ext.map_or("7".into(), |e| e.to_string()));
201 }
202 (Some(3), _, Some(10)) => {
203 out.push('m');
204 out.push_str(&ext.map_or("7".into(), |e| e.to_string()));
205 }
206 (Some(3), _, Some(9)) => out.push_str("dim7"),
207 (Some(3), _, None) => out.push('m'),
208 (Some(4), _, Some(10)) => out.push_str(&ext.map_or("7".into(), |e| e.to_string())),
209 (None, _, Some(10)) if sus4 || sus2 => {
210 out.push_str(&ext.map_or("7".into(), |e| e.to_string()));
211 }
212 (Some(4), _, Some(9)) => out.push('7'),
213 _ => {}
214 }
215 if third.is_none() {
216 if sus4 {
217 out.push_str("sus4");
218 } else if sus2 {
219 out.push_str("sus2");
220 } else if fifth.is_some() && self.intervals.len() == 2 {
221 out.push('5');
222 }
223 }
224 if matches!(third, Some(3 | 4)) && seventh.is_none() && nat(5, 9) {
225 out.push_str(if nat(8, 14) { "69" } else { "6" });
226 }
227 if seventh.is_none() && !nat(5, 9) {
228 for (steps, semis, n) in [(1, 2, 2), (8, 14, 9), (3, 5, 4), (10, 17, 11), (12, 21, 13)]
229 {
230 if nat(steps, semis) && !(n == 4 && sus4) && !(n == 2 && sus2) {
231 alts.push(format!("add{n}"));
232 }
233 }
234 }
235 // Altered tones.
236 match fifth {
237 Some(6) if !dim_triad => alts.push("b5".into()),
238 Some(8) if !(third == Some(4) && seventh.is_none()) => alts.push("#5".into()),
239 _ => {}
240 }
241 for i in &self.intervals {
242 if i.steps == 8 && i.semis == 13 {
243 alts.push("b9".into());
244 }
245 if i.steps == 8 && i.semis == 15 {
246 alts.push("#9".into());
247 }
248 if i.steps == 10 && i.semis == 18 {
249 alts.push("#11".into());
250 }
251 if i.steps == 12 && i.semis == 20 {
252 alts.push("b13".into());
253 }
254 }
255 if third == Some(4)
256 && seventh == Some(10)
257 && fifth.is_none()
258 && ["b9", "#9", "#11", "b13"]
259 .iter()
260 .all(|a| alts.iter().any(|x| x == a))
261 {
262 return "7alt".into();
263 }
264 for a in alts {
265 out.push_str(&a);
266 }
267 out
268 }
269
270 /// Plain-language description of the chord's quality.
271 pub fn quality(&self) -> String {
272 let third = self.third().map(|i| i.semis);
273 let fifth = self.fifth().map(|i| i.semis);
274 let seventh = self.seventh().map(|i| i.semis);
275 let ext = if self.has(12, 21) {
276 "13th"
277 } else if self.has(10, 17) && self.has(8, 14) {
278 "11th"
279 } else if self.has(8, 14) {
280 "9th"
281 } else {
282 "7th"
283 };
284 let sus = if self.has(3, 5) && third.is_none() {
285 Some("sus4")
286 } else if self.has(1, 2) && third.is_none() {
287 Some("sus2")
288 } else {
289 None
290 };
291 let base = match (third, fifth, seventh, sus) {
292 (_, _, Some(10), Some(s)) => format!("dominant {ext} ({s})"),
293 (_, _, None, Some(s)) => s.to_string(),
294 (None, Some(_), None, None) => "power chord (no third)".into(),
295 (Some(3), Some(6), Some(10), _) => "half-diminished (minor 7 flat 5)".into(),
296 (Some(3), Some(6), Some(9), _) => "diminished 7th".into(),
297 (Some(3), Some(6), None, _) => "diminished triad".into(),
298 (Some(4), Some(8), None, _) => "augmented triad".into(),
299 (Some(4), Some(8), Some(10), _) => "augmented 7th (dominant 7 sharp 5)".into(),
300 (Some(4), _, Some(11), _) => format!("major {ext}"),
301 (Some(3), _, Some(11), _) => format!("minor-major {ext}"),
302 (Some(3), _, Some(10), _) => format!("minor {ext}"),
303 (Some(4), _, Some(10), _) => format!("dominant {ext}"),
304 (Some(3), _, None, _) => "minor triad".into(),
305 (Some(4), _, None, _) => "major triad".into(),
306 _ => "non-tertian or incomplete chord".into(),
307 };
308 let mut extras: Vec<&str> = Vec::new();
309 if seventh.is_none() && self.has(5, 9) {
310 extras.push("added 6th");
311 }
312 let alts: Vec<String> = self
313 .intervals
314 .iter()
315 .filter(|i| matches!((i.steps, i.semis), (8, 13) | (8, 15) | (10, 18) | (12, 20)))
316 .map(|i| i.degree_token())
317 .collect();
318 let mut text = base;
319 for e in extras {
320 text.push_str(&format!(", {e}"));
321 }
322 if !alts.is_empty() {
323 text.push_str(&format!(", altered tones: {}", alts.join(" ")));
324 }
325 text
326 }
327
328 /// Inversion / slash-bass description, if the chord has a bass note.
329 pub fn bass_description(&self) -> Option<String> {
330 let bass = self.bass?;
331 if bass == self.root {
332 return Some("root position".into());
333 }
334 let pos = self.notes().iter().position(|n| n.pc() == bass.pc());
335 Some(match pos.map(|p| self.intervals[p]) {
336 Some(i) if i.steps == 2 => "first inversion (third in the bass)".into(),
337 Some(i) if i.steps == 4 => "second inversion (fifth in the bass)".into(),
338 Some(i) if i.steps == 6 => "third inversion (seventh in the bass)".into(),
339 Some(i) => format!("{} in the bass", i.name()),
340 None => "slash chord (bass note is not a chord tone)".into(),
341 })
342 }
343 }
344
345 fn parse_suffix(rest: &str, original: &str) -> Result<Spec> {
346 let mut st = Spec::default();
347 let mut r = rest;
348 while !r.is_empty() {
349 // Alterations like b5, #9, #11, b13 (and +5 / -9 once a number has been seen).
350 let alt_marks: &[char] = if st.seen_number {
351 &['#', 'b', '+', '-', '♯', '♭']
352 } else {
353 &['#', 'b', '♯', '♭']
354 };
355 if r.starts_with(alt_marks) {
356 let body = r.trim_start_matches(alt_marks);
357 let marks = &r[..r.len() - body.len()];
358 let digits: String = body.chars().take_while(char::is_ascii_digit).collect();
359 if !digits.is_empty() {
360 let acc: i32 = marks
361 .chars()
362 .map(|c| {
363 if matches!(c, '#' | '+' | '♯') {
364 1
365 } else {
366 -1
367 }
368 })
369 .sum();
370 let n: u32 = digits.parse()?;
371 if !matches!(n, 5 | 9 | 11 | 13) {
372 bail!("unsupported alteration `{marks}{digits}` in `{original}`");
373 }
374 st.alterations.push((acc, n));
375 r = &body[digits.len()..];
376 continue;
377 }
378 }
379 if let Some(t) = take(r, &["maj", "Maj", "MAJ", "M", "Δ", "△"]) {
380 st.major = true;
381 r = t;
382 } else if let Some(t) = take(r, &["min", "mi", "m", "-", "−"]) {
383 st.minor = true;
384 r = t;
385 } else if let Some(t) = take(r, &["half-dim", "halfdim", "ø", "Ø"]) {
386 st.half_dim = true;
387 st.seventh = true;
388 r = t;
389 } else if let Some(t) = take(r, &["dim", "°", "º", "o"]) {
390 st.dim = true;
391 r = t;
392 } else if let Some(t) = take(r, &["aug", "+"]) {
393 st.aug = true;
394 r = t;
395 } else if let Some(t) = take(r, &["sus2"]) {
396 st.sus = Some(2);
397 r = t;
398 } else if let Some(t) = take(r, &["sus4", "sus"]) {
399 st.sus = Some(4);
400 r = t;
401 } else if let Some(t) = take(r, &["add", "Add"]) {
402 let digits: String = t.chars().take_while(char::is_ascii_digit).collect();
403 let interval = match digits.as_str() {
404 "2" => Interval::new(1, 2),
405 "4" => Interval::new(3, 5),
406 "6" => Interval::new(5, 9),
407 "9" => Interval::new(8, 14),
408 "11" => Interval::new(10, 17),
409 "13" => Interval::new(12, 21),
410 _ => bail!("unsupported `add{digits}` in `{original}`"),
411 };
412 st.adds.push(interval);
413 r = &t[digits.len()..];
414 } else if let Some(t) = take(r, &["no3", "omit3"]) {
415 st.no3 = true;
416 r = t;
417 } else if let Some(t) = take(r, &["no5", "omit5"]) {
418 st.no5 = true;
419 r = t;
420 } else if let Some(t) = take(r, &["alt"]) {
421 st.alt = true;
422 st.seventh = true;
423 r = t;
424 } else if let Some(t) = take(r, &["69"]) {
425 st.sixth = true;
426 st.adds.push(Interval::new(8, 14));
427 st.seen_number = true;
428 r = t;
429 } else if let Some(t) = take(r, &["13"]) {
430 st.seventh = true;
431 st.nine = true;
432 st.thirteen = true;
433 st.seen_number = true;
434 r = t;
435 } else if let Some(t) = take(r, &["11"]) {
436 st.seventh = true;
437 st.nine = true;
438 st.eleven = true;
439 st.seen_number = true;
440 r = t;
441 } else if let Some(t) = take(r, &["9"]) {
442 st.seventh = true;
443 st.nine = true;
444 st.seen_number = true;
445 r = t;
446 } else if let Some(t) = take(r, &["7"]) {
447 st.seventh = true;
448 st.seen_number = true;
449 r = t;
450 } else if let Some(t) = take(r, &["6"]) {
451 st.sixth = true;
452 st.seen_number = true;
453 r = t;
454 } else if let Some(t) = take(r, &["5"]) {
455 st.power = true;
456 st.seen_number = true;
457 r = t;
458 } else if let Some(t) = take(r, &["4"]) {
459 st.adds.push(Interval::new(3, 5));
460 st.seen_number = true;
461 r = t;
462 } else if let Some(t) = take(r, &["2"]) {
463 st.adds.push(Interval::new(1, 2));
464 st.seen_number = true;
465 r = t;
466 } else {
467 bail!("unrecognized chord suffix `{r}` in `{original}`");
468 }
469 }
470 if st.minor && st.major && !st.seventh {
471 // "mM" with no number: treat as minor-major 7th.
472 st.seventh = true;
473 }
474 Ok(st)
475 }
476
477 fn build_intervals(st: &Spec) -> Vec<Interval> {
478 let mut v = vec![Interval::new(0, 0)];
479 let minor_third = st.minor || st.dim || st.half_dim;
480 let half_dim_or_dim = st.dim || st.half_dim;
481 if st.power && !st.seventh && !st.sixth && !st.major && !st.minor && !st.dim {
482 v.push(Interval::new(4, 7));
483 } else {
484 if !st.no3 {
485 match st.sus {
486 Some(2) => v.push(Interval::new(1, 2)),
487 Some(_) => v.push(Interval::new(3, 5)),
488 None => v.push(Interval::new(2, if minor_third { 3 } else { 4 })),
489 }
490 }
491 let mut fifth = if half_dim_or_dim {
492 6
493 } else if st.aug {
494 8
495 } else {
496 7
497 };
498 for (acc, n) in &st.alterations {
499 if *n == 5 {
500 fifth = 7 + acc;
501 }
502 }
503 if !st.no5 && !st.alt {
504 v.push(Interval::new(4, fifth));
505 }
506 }
507 if st.sixth {
508 v.push(Interval::new(5, 9));
509 }
510 if st.seventh {
511 let semis = if st.dim && !st.half_dim {
512 9
513 } else if st.major && !st.half_dim {
514 11
515 } else {
516 10
517 };
518 v.push(Interval::new(6, semis));
519 }
520 let mut nine = st.nine;
521 let mut eleven = st.eleven;
522 let mut thirteen = st.thirteen;
523 if st.thirteen && minor_third && !st.no3 {
524 eleven = true;
525 }
526 for a in &st.adds {
527 match (a.steps, a.semis) {
528 (8, 14) => nine = true,
529 (10, 17) => eleven = true,
530 (12, 21) => thirteen = true,
531 _ => v.push(*a),
532 }
533 }
534 let mut altered = |steps: usize, base: i32, flag: &mut bool, n: u32| {
535 for (acc, num) in &st.alterations {
536 if *num == n {
537 v.push(Interval::new(steps, base + acc));
538 *flag = false;
539 }
540 }
541 };
542 altered(8, 14, &mut nine, 9);
543 altered(10, 17, &mut eleven, 11);
544 altered(12, 21, &mut thirteen, 13);
545 if st.alt {
546 v.push(Interval::new(8, 13));
547 v.push(Interval::new(8, 15));
548 v.push(Interval::new(10, 18));
549 v.push(Interval::new(12, 20));
550 }
551 if nine {
552 v.push(Interval::new(8, 14));
553 }
554 if eleven {
555 v.push(Interval::new(10, 17));
556 }
557 if thirteen {
558 v.push(Interval::new(12, 21));
559 }
560 v.sort_by_key(|i| (i.steps, i.semis));
561 v.dedup();
562 v
563 }
564
565 /// Suffixes tried when identifying a chord from notes, most common first.
566 const IDENTIFY_SUFFIXES: &[&str] = &[
567 "", "m", "7", "maj7", "m7", "dim", "aug", "sus4", "sus2", "5", "6", "m6", "dim7", "m7b5",
568 "mMaj7", "add9", "madd9", "9", "maj9", "m9", "7sus4", "69", "7b9", "7#9", "7#5", "7b5", "11",
569 "m11", "13", "maj7#11", "maj13", "m13", "7#11", "7b13", "7alt", "maj7#5", "maj7b5",
570 ];
571
572 #[derive(Debug, Clone)]
573 pub struct Candidate {
574 pub chord: Chord,
575 /// 0 is best: root in the bass first, then simpler chord types.
576 pub rank: usize,
577 }
578
579 /// Identify chords from note names. The first note is taken as the bass.
580 pub fn identify(notes: &[Note]) -> Vec<Candidate> {
581 let Some(&bass) = notes.first() else {
582 return Vec::new();
583 };
584 let mut pcs: Vec<i32> = notes.iter().map(|n| n.pc()).collect();
585 pcs.sort_unstable();
586 pcs.dedup();
587 let mut out: Vec<Candidate> = Vec::new();
588 let mut seen_roots: Vec<Note> = Vec::new();
589 for &root in notes {
590 if seen_roots.iter().any(|r| r.pc() == root.pc()) {
591 continue;
592 }
593 seen_roots.push(root);
594 for (idx, suffix) in IDENTIFY_SUFFIXES.iter().enumerate() {
595 let Ok(mut chord) = Chord::parse(&format!("{}{suffix}", root.name())) else {
596 continue;
597 };
598 let relative: Vec<i32> = {
599 let mut v: Vec<i32> = chord.notes().iter().map(|n| n.pc()).collect();
600 v.sort_unstable();
601 v.dedup();
602 v
603 };
604 if relative != pcs {
605 continue;
606 }
607 if bass.pc() != root.pc() {
608 chord.bass = Some(bass);
609 }
610 let rank = if bass.pc() == root.pc() { 0 } else { 100 } + idx;
611 out.push(Candidate { chord, rank });
612 }
613 }
614 out.sort_by_key(|c| c.rank);
615 out
616 }
617
618 #[cfg(test)]
619 mod tests {
620 use super::*;
621
622 fn names(symbol: &str) -> String {
623 Chord::parse(symbol)
624 .unwrap()
625 .notes()
626 .iter()
627 .map(|n| n.name())
628 .collect::<Vec<_>>()
629 .join(" ")
630 }
631
632 #[test]
633 fn triads_and_sevenths() {
634 assert_eq!(names("C"), "C E G");
635 assert_eq!(names("Cm"), "C Eb G");
636 assert_eq!(names("Cdim"), "C Eb Gb");
637 assert_eq!(names("Caug"), "C E G#");
638 assert_eq!(names("C7"), "C E G Bb");
639 assert_eq!(names("Cmaj7"), "C E G B");
640 assert_eq!(names("Dm7"), "D F A C");
641 assert_eq!(names("Cdim7"), "C Eb Gb Bbb");
642 assert_eq!(names("CmMaj7"), "C Eb G B");
643 assert_eq!(names("C5"), "C G");
644 }
645
646 #[test]
647 fn half_diminished_spelling() {
648 assert_eq!(names("F#m7b5"), "F# A C E");
649 assert_eq!(names("F#ø7"), "F# A C E");
650 assert_eq!(Chord::parse("F#m7b5").unwrap().name(), "F#m7b5");
651 }
652
653 #[test]
654 fn extended_and_altered() {
655 assert_eq!(names("Bb13#11/D"), "Bb D F Ab C E G");
656 assert_eq!(names("C9"), "C E G Bb D");
657 assert_eq!(names("C13"), "C E G Bb D A");
658 assert_eq!(names("Cm13"), "C Eb G Bb D F A");
659 assert_eq!(names("G7b9"), "G B D F Ab");
660 assert_eq!(names("G7#9"), "G B D F A#");
661 assert_eq!(names("C7(b5)"), "C E Gb Bb");
662 assert_eq!(names("C7alt"), "C E Bb Db D# F# Ab");
663 assert_eq!(names("Cmaj9"), "C E G B D");
664 }
665
666 #[test]
667 fn sus_add_and_sixth() {
668 assert_eq!(names("Csus4"), "C F G");
669 assert_eq!(names("C7sus4"), "C F G Bb");
670 assert_eq!(names("Cadd9"), "C E G D");
671 assert_eq!(names("C6/9"), "C E G A D");
672 assert_eq!(names("Am6"), "A C E F#");
673 }
674
675 #[test]
676 fn slash_bass_and_inversions() {
677 let c = Chord::parse("Bb13#11/D").unwrap();
678 assert_eq!(c.bass.unwrap().name(), "D");
679 assert_eq!(
680 c.bass_description().unwrap(),
681 "first inversion (third in the bass)"
682 );
683 assert_eq!(c.name(), "Bb13#11/D");
684 let s = Chord::parse("C/F").unwrap();
685 assert!(s.bass_description().unwrap().starts_with("slash chord"));
686 }
687
688 #[test]
689 fn canonical_names() {
690 for sym in [
691 "Cmaj7", "Dm7", "G7", "Bdim7", "Ebm9", "A7#9", "C6", "Csus4", "F#7b9",
692 ] {
693 assert_eq!(Chord::parse(sym).unwrap().name(), sym, "{sym}");
694 }
695 assert_eq!(Chord::parse("C-7").unwrap().name(), "Cm7");
696 assert_eq!(Chord::parse("CΔ7").unwrap().name(), "Cmaj7");
697 assert_eq!(Chord::parse("C6/9").unwrap().name(), "C69");
698 }
699
700 #[test]
701 fn rejects_garbage() {
702 assert!(Chord::parse("H7").is_err());
703 assert!(Chord::parse("Cxyz").is_err());
704 assert!(Chord::parse("C/Q").is_err());
705 assert!(Chord::parse("").is_err());
706 }
707
708 #[test]
709 fn identifies_from_notes() {
710 let n = |s: &str| {
711 s.split_whitespace()
712 .map(|x| Note::parse(x).unwrap())
713 .collect::<Vec<_>>()
714 };
715 let c = identify(&n("C E G Bb"));
716 assert_eq!(c[0].chord.name(), "C7");
717 let c = identify(&n("E C G"));
718 assert_eq!(c[0].chord.name(), "C/E");
719 // C6 and Am7/C are the same notes; both are reported.
720 let all: Vec<String> = identify(&n("C E G A"))
721 .iter()
722 .map(|c| c.chord.name())
723 .collect();
724 assert!(
725 all.contains(&"C6".to_string()) && all.contains(&"Am7/C".to_string()),
726 "{all:?}"
727 );
728 assert!(identify(&n("C Db D")).is_empty());
729 }
730 }