1use std::fmt;
12use std::str::FromStr;
13
14use crate::chord::Chord;
15use crate::defaults::{FloatType, IntegerType};
16use crate::error::{Error, Result};
17use crate::interval::Interval;
18use crate::pitch::Pitch;
19
20pub type Shorthand = (&'static str, &'static [&'static str]);
22
23pub const SHORTHAND_DEGREES: [Shorthand; 48] = [
25 ("maj", &["1", "3", "5"]),
26 ("min", &["1", "b3", "5"]),
27 ("aug", &["1", "3", "#5"]),
28 ("dim", &["1", "b3", "b5"]),
29 ("7", &["1", "3", "5", "b7"]),
30 ("maj7", &["1", "3", "5", "7"]),
31 ("minmaj7", &["1", "b3", "5", "7"]),
32 ("min7", &["1", "b3", "5", "b7"]),
33 ("augmaj7", &["1", "3", "#5", "7"]),
34 ("aug7", &["1", "3", "#5", "b7"]),
35 ("hdim7", &["1", "b3", "b5", "b7"]),
36 ("hdim", &["1", "b3", "b5", "b7"]),
37 ("dim7", &["1", "b3", "b5", "bb7"]),
38 ("dom7dim5", &["1", "3", "b5", "b7"]),
39 ("maj6", &["1", "3", "5", "6"]),
40 ("min6", &["1", "b3", "5", "6"]),
41 ("maj9", &["1", "3", "5", "7", "9"]),
42 ("9", &["1", "3", "5", "b7", "9"]),
43 ("minmaj9", &["1", "b3", "5", "7", "9"]),
44 ("min9", &["1", "b3", "5", "b7", "9"]),
45 ("augmaj9", &["1", "3", "#5", "7", "9"]),
46 ("aug9", &["1", "3", "#5", "b7", "9"]),
47 ("hdim9", &["1", "b3", "b5", "b7", "9"]),
48 ("hdimmin9", &["1", "b3", "b5", "b7", "b9"]),
49 ("dim9", &["1", "b3", "b5", "bb7", "9"]),
50 ("dimmin9", &["1", "b3", "b5", "bb7", "b9"]),
51 ("11", &["1", "3", "5", "b7", "9", "11"]),
52 ("maj11", &["1", "3", "5", "7", "9", "11"]),
53 ("minmaj11", &["1", "b3", "5", "7", "9", "11"]),
54 ("min11", &["1", "b3", "5", "b7", "9", "11"]),
55 ("augmaj11", &["1", "3", "#5", "7", "9", "11"]),
56 ("aug11", &["1", "3", "#5", "b7", "9", "11"]),
57 ("hdim11", &["1", "b3", "b5", "b7", "b9", "11"]),
58 ("dim11", &["1", "b3", "b5", "bb7", "b9", "b11"]),
59 ("maj13", &["1", "3", "5", "7", "9", "11", "13"]),
60 ("13", &["1", "3", "5", "b7", "9", "11", "13"]),
61 ("minmaj13", &["1", "b3", "5", "7", "9", "11", "13"]),
62 ("min13", &["1", "b3", "5", "b7", "9", "11", "13"]),
63 ("augmaj13", &["1", "3", "#5", "7", "9", "11", "13"]),
64 ("hdim13", &["1", "b3", "b5", "b7", "9", "11", "13"]),
65 ("sus2", &["1", "2", "5"]),
66 ("sus4", &["1", "4", "5"]),
67 ("7sus4", &["1", "4", "5", "b7"]),
68 ("power", &["1", "5"]),
69 ("pedal", &["1"]),
70 ("1", &["1"]),
71 ("5", &["1", "5"]),
72 ("6", &["1", "3", "5", "6"]),
73];
74
75const DEGREE_SHORTHANDS: &[(&[&str], &str)] = &[
78 (&["3", "5", "b7", "9"], "9"),
79 (&["3", "5", "7", "9"], "maj9"),
80 (&["b3", "5", "b7", "9"], "min9"),
81 (&["3", "5", "b7"], "7"),
82 (&["3", "5", "6"], "maj6"),
83 (&["b3", "5", "6"], "min6"),
84 (&["3", "5", "7"], "maj7"),
85 (&["b3", "b5", "bb7"], "dim7"),
86 (&["b3", "5", "b7"], "min7"),
87 (&["b3", "b5", "b7"], "hdim7"),
88 (&["b3", "5", "7"], "minmaj7"),
89 (&["3", "5"], "maj"),
90 (&["b3", "5"], "min"),
91 (&["b3", "b5"], "dim"),
92 (&["3", "#5"], "aug"),
93 (&["4", "5"], "sus4"),
94];
95
96pub fn shorthand_degrees(shorthand: &str) -> Option<&'static [&'static str]> {
98 SHORTHAND_DEGREES
99 .iter()
100 .find(|(name, _)| *name == shorthand)
101 .map(|(_, degrees)| *degrees)
102}
103
104pub fn convert_interval(harte: &str) -> Result<String> {
109 let cannot = || Error::Harte(format!("The degree {harte} cannot be parsed."));
110 let (sharps, flats, number) = accidentals_and_number(harte).ok_or_else(cannot)?;
111 let base = if number < 8 { number } else { number - 7 };
112 let specifier = if matches!(base, 1 | 4 | 5) {
113 match (sharps, flats) {
114 (0, 0) => "P",
115 (1, 0) => "A",
116 (0, 1) => "d",
117 (2, 0) => return convert_interval(&format!("{}", base + 1)),
118 (0, 2) if base == 1 => return convert_interval("b7"),
119 (0, 2) => return convert_interval(&format!("{}", base - 1)),
120 _ => return Err(cannot()),
121 }
122 } else {
123 match (sharps, flats) {
124 (0, 0) => "M",
125 (1, 0) => "A",
126 (0, 1) => "m",
127 (0, 2) => "d",
128 _ => return Err(cannot()),
129 }
130 };
131 Ok(format!("{specifier}{base}"))
132}
133
134fn accidentals_and_number(degree: &str) -> Option<(usize, usize, IntegerType)> {
138 let text: Vec<char> = degree.chars().collect();
139 (0..text.len()).find_map(|start| {
140 let sharps = text[start..].iter().take_while(|c| **c == '#').count();
141 let flats = text[start + sharps..]
142 .iter()
143 .take_while(|c| **c == 'b')
144 .count();
145 let digits: String = text[start + sharps + flats..]
146 .iter()
147 .take_while(|c| c.is_ascii_digit())
148 .collect();
149 digits.parse().ok().map(|number| (sharps, flats, number))
150 })
151}
152
153pub fn degree_sort_key(degree: &str) -> FloatType {
156 let number: FloatType = degree
157 .chars()
158 .filter(char::is_ascii_digit)
159 .collect::<String>()
160 .parse()
161 .unwrap_or(0.0);
162 if degree.starts_with('b') {
163 number - 0.49
164 } else if degree.starts_with('#') {
165 number + 0.49
166 } else {
167 number
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174#[must_use]
175pub struct HarteInterval {
176 written: String,
177 interval: Interval,
178}
179
180impl HarteInterval {
181 pub fn new(written: &str) -> Result<Self> {
183 let interval = Interval::from_name(&convert_interval(written)?)
184 .map_err(|_| Error::Harte(format!("Harte Interval {written} cannot be converted")))?;
185 Ok(Self {
186 written: written.to_string(),
187 interval,
188 })
189 }
190
191 pub fn written(&self) -> &str {
193 &self.written
194 }
195
196 pub fn interval(&self) -> &Interval {
198 &self.interval
199 }
200
201 pub fn transpose_pitch(&self, root: &Pitch) -> Result<Pitch> {
203 self.interval.transpose_pitch(root)
204 }
205}
206
207impl fmt::Display for HarteInterval {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 f.write_str(&self.written)
210 }
211}
212
213#[derive(Clone, Debug, Default, PartialEq)]
215struct Parsed {
216 root: Option<String>,
217 shorthand: Option<String>,
218 degrees: Vec<String>,
219 bass: Option<String>,
220}
221
222fn parse(label: &str) -> Result<Parsed> {
226 let text: Vec<char> = label.chars().filter(|c| *c != ' ').collect();
227 let error = |at: usize| {
228 Error::Harte(format!(
229 "The input chord {label} is not a valid Harte chord (at character {at})"
230 ))
231 };
232 if text.is_empty() {
233 return Err(error(0));
234 }
235 if text.len() == 1 && matches!(text[0], 'N' | 'X') {
236 return Ok(Parsed::default());
237 }
238 let mut at = 0;
239 if !matches!(text[0], 'A'..='G') {
240 return Err(error(0));
241 }
242 at += 1;
243 while at < text.len() && matches!(text[at], 'b' | '#') {
244 at += 1;
245 }
246 let mut parsed = Parsed {
247 root: Some(text[..at].iter().collect()),
248 ..Parsed::default()
249 };
250
251 let degree = |at: &mut usize| -> Result<String> {
252 let start = *at;
253 if *at < text.len() && text[*at] == '*' {
254 *at += 1;
255 }
256 while *at < text.len() && matches!(text[*at], 'b' | '#') {
257 *at += 1;
258 }
259 let digits = *at;
260 while *at < text.len() && text[*at].is_ascii_digit() {
261 *at += 1;
262 }
263 let number: String = text[digits..*at].iter().collect();
264 if !(1..=13).contains(&number.parse::<u8>().unwrap_or(0)) {
265 return Err(error(digits));
266 }
267 Ok(text[start..*at].iter().collect())
268 };
269 let degree_list = |at: &mut usize| -> Result<Vec<String>> {
270 let mut degrees = vec![degree(at)?];
271 while *at < text.len() && text[*at] == ',' {
272 *at += 1;
273 degrees.push(degree(at)?);
274 }
275 if *at >= text.len() || text[*at] != ')' {
276 return Err(error(*at));
277 }
278 *at += 1;
279 Ok(degrees)
280 };
281
282 if at < text.len() && text[at] == ':' {
283 at += 1;
284 if at < text.len() && text[at] == '(' {
285 at += 1;
286 parsed.degrees = degree_list(&mut at)?;
287 } else {
288 let start = at;
289 while at < text.len() && text[at].is_ascii_alphanumeric() {
290 at += 1;
291 }
292 let shorthand: String = text[start..at].iter().collect();
293 if shorthand_degrees(&shorthand).is_none() {
294 return Err(error(start));
295 }
296 parsed.shorthand = Some(shorthand);
297 if at < text.len() && text[at] == '(' {
298 at += 1;
299 parsed.degrees = degree_list(&mut at)?;
300 }
301 }
302 }
303 if at < text.len() && text[at] == '/' {
304 at += 1;
305 parsed.bass = Some(degree(&mut at)?);
306 }
307 if at != text.len() {
308 return Err(error(at));
309 }
310 Ok(parsed)
311}
312
313#[derive(Clone, Debug)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325#[must_use]
326pub struct Harte {
327 label: String,
328 root: Option<String>,
329 shorthand: Option<String>,
330 degrees: Vec<String>,
331 bass: Option<String>,
332 all_degrees: Vec<String>,
333 chord: Chord,
334}
335
336impl Harte {
337 pub fn new(label: &str) -> Result<Self> {
339 let parsed = parse(label)?;
340 let Some(root) = parsed.root else {
341 return Ok(Self {
342 label: label.to_string(),
343 root: None,
344 shorthand: None,
345 degrees: Vec::new(),
346 bass: None,
347 all_degrees: Vec::new(),
348 chord: Chord::empty(),
349 });
350 };
351 let bass = parsed.bass.unwrap_or_else(|| "1".to_string());
352 let removed: Vec<&str> = parsed
353 .degrees
354 .iter()
355 .filter_map(|degree| degree.strip_prefix('*'))
356 .collect();
357 let mut all: Vec<String> = vec!["1".to_string()];
358 if let Some(shorthand) = &parsed.shorthand {
359 let named = shorthand_degrees(shorthand)
360 .ok_or_else(|| Error::Harte("The Harte shorthand is not valid.".to_string()))?;
361 all.extend(named.iter().map(|degree| (*degree).to_string()));
362 all.extend(parsed.degrees.iter().cloned());
363 } else if parsed.degrees.len() > removed.len() {
364 all.extend(parsed.degrees.iter().cloned());
365 } else {
366 all.extend(["3".to_string(), "5".to_string()]);
367 }
368 all.retain(|degree| !removed.contains(°ree.as_str()) && !degree.starts_with('*'));
369 all.push(bass.clone());
370 all.sort_by(|a, b| {
371 degree_sort_key(a)
372 .partial_cmp(°ree_sort_key(b))
373 .unwrap_or(std::cmp::Ordering::Equal)
374 .then_with(|| a.cmp(b))
375 });
376 all.dedup();
377
378 let root_pitch = Pitch::from_name(format!("{}4", root.replace('b', "-")))?;
380 let mut pitches = Vec::with_capacity(all.len());
381 for degree in &all {
382 pitches.push(HarteInterval::new(degree)?.transpose_pitch(&root_pitch)?);
383 }
384 let bass_pitch = HarteInterval::new(&bass)?.transpose_pitch(&root_pitch)?;
385 let mut chord = Chord::new(pitches.as_slice())?;
386 let bass_pitch = if bass_pitch.name() == root_pitch.name() {
388 root_pitch.clone()
389 } else {
390 let mut lowered = bass_pitch;
391 lowered.set_octave(Some(3));
392 if let Some(note) = chord
393 .notes_mut()
394 .iter_mut()
395 .find(|note| note.pitch().name() == lowered.name())
396 {
397 note.set_pitch(lowered.clone());
398 }
399 lowered
400 };
401 chord.set_root(Some(root_pitch));
402 chord.set_bass(Some(bass_pitch));
403
404 Ok(Self {
405 label: label.to_string(),
406 root: Some(root),
407 shorthand: parsed.shorthand,
408 degrees: parsed.degrees,
409 bass: Some(bass),
410 all_degrees: all,
411 chord,
412 })
413 }
414
415 pub fn label(&self) -> &str {
417 &self.label
418 }
419
420 pub fn is_empty(&self) -> bool {
422 self.root.is_none()
423 }
424
425 pub fn chord(&self) -> &Chord {
427 &self.chord
428 }
429
430 pub fn into_chord(self) -> Chord {
432 self.chord
433 }
434
435 pub fn root_name(&self) -> Option<&str> {
437 self.root.as_deref()
438 }
439
440 pub fn bass_degree(&self) -> Option<&str> {
443 self.bass.as_deref()
444 }
445
446 pub fn shorthand(&self) -> Option<&str> {
449 self.shorthand.as_deref()
450 }
451
452 pub fn degrees(&self) -> Option<&[String]> {
455 (!self.degrees.is_empty()).then_some(self.degrees.as_slice())
456 }
457
458 pub fn unwrap_shorthand(&self) -> Option<&[String]> {
463 if self.shorthand.is_some() {
464 Some(&self.all_degrees)
465 } else {
466 self.degrees()
467 }
468 }
469
470 pub fn sounding_degrees(&self) -> &[String] {
473 &self.all_degrees
474 }
475
476 pub fn bass_is_root(&self) -> bool {
478 self.root.is_some() && self.bass.as_deref() == Some("1")
479 }
480
481 pub fn contains_shorthand(&self) -> bool {
483 self.shorthand.is_some()
484 }
485
486 pub fn midi_pitches(&self) -> Vec<IntegerType> {
488 let mut midi: Vec<IntegerType> = self.chord.pitches().iter().map(Pitch::midi).collect();
489 midi.sort_unstable();
490 midi
491 }
492
493 pub fn multi_hot_encoding(&self, transpose: bool) -> [u8; 12] {
496 let shift = if transpose {
497 self.chord.root().map_or(0, crate::chord::root::pitch_class)
498 } else {
499 0
500 };
501 let mut flags = [0u8; 12];
502 for pitch in self.chord.pitches() {
503 let class = (crate::chord::root::pitch_class(&pitch) + 12 - shift) % 12;
504 flags[usize::from(class)] = 1;
505 }
506 flags
507 }
508
509 pub fn prettify(&self) -> String {
514 let Some(root) = &self.root else {
515 return self.label.clone();
516 };
517 let degrees: Vec<&str> = self
518 .all_degrees
519 .iter()
520 .map(String::as_str)
521 .filter(|degree| *degree != "1")
522 .collect();
523 let Some((grades, shorthand)) = DEGREE_SHORTHANDS
524 .iter()
525 .find(|(grades, _)| grades.iter().all(|grade| degrees.contains(grade)))
526 else {
527 return self.label.clone();
528 };
529 let left: Vec<&str> = degrees
530 .iter()
531 .copied()
532 .filter(|degree| !grades.contains(degree))
533 .collect();
534 let mut pretty = format!("{root}:{shorthand}");
535 if !left.is_empty() {
536 pretty.push('(');
537 pretty.push_str(&left.join(","));
538 pretty.push(')');
539 }
540 if let Some(bass) = self.bass.as_deref().filter(|bass| *bass != "1") {
541 pretty.push('/');
542 pretty.push_str(bass);
543 }
544 pretty
545 }
546}
547
548impl PartialEq for Harte {
549 fn eq(&self, other: &Self) -> bool {
550 self.root == other.root && self.all_degrees == other.all_degrees && self.bass == other.bass
551 }
552}
553
554impl fmt::Display for Harte {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558 let Some(root) = &self.root else {
559 return f.write_str("N");
560 };
561 f.write_str(root)?;
562 if let Some(shorthand) = &self.shorthand {
563 write!(f, ":{shorthand}")?;
564 }
565 if !self.degrees.is_empty() {
566 if self.shorthand.is_none() {
567 f.write_str(":")?;
568 }
569 write!(f, "({})", self.degrees.join(","))?;
570 }
571 if let Some(bass) = self.bass.as_deref().filter(|bass| *bass != "1") {
572 write!(f, "/{bass}")?;
573 }
574 Ok(())
575 }
576}
577
578impl FromStr for Harte {
579 type Err = Error;
580
581 fn from_str(label: &str) -> Result<Self> {
582 Self::new(label)
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
592 fn harte_degrees_convert_to_music21_intervals() {
593 for (harte, music21) in [
594 ("b1", "d1"),
595 ("1", "P1"),
596 ("#1", "A1"),
597 ("b2", "m2"),
598 ("2", "M2"),
599 ("#2", "A2"),
600 ("b3", "m3"),
601 ("3", "M3"),
602 ("#3", "A3"),
603 ("b4", "d4"),
604 ("4", "P4"),
605 ("#4", "A4"),
606 ("b5", "d5"),
607 ("5", "P5"),
608 ("#5", "A5"),
609 ("b6", "m6"),
610 ("6", "M6"),
611 ("#6", "A6"),
612 ("b7", "m7"),
613 ("7", "M7"),
614 ("#7", "A7"),
615 ("b8", "d1"),
616 ("8", "P1"),
617 ("#8", "A1"),
618 ("b9", "m2"),
619 ("9", "M2"),
620 ("#9", "A2"),
621 ("b10", "m3"),
622 ("10", "M3"),
623 ("#10", "A3"),
624 ("b11", "d4"),
625 ("11", "P4"),
626 ("#11", "A4"),
627 ("b12", "d5"),
628 ("12", "P5"),
629 ("#12", "A5"),
630 ("b13", "m6"),
631 ("13", "M6"),
632 ("#13", "A6"),
633 ("b14", "m7"),
634 ("14", "M7"),
635 ("#14", "A7"),
636 ] {
637 assert_eq!(convert_interval(harte).unwrap(), music21, "{harte}");
638 }
639 assert_eq!(convert_interval("bb7").unwrap(), "d7");
640 assert_eq!(convert_interval("##4").unwrap(), "P5");
641 assert_eq!(convert_interval("bb5").unwrap(), "P4");
642 assert_eq!(convert_interval("bb1").unwrap(), "m7");
643 assert_eq!(convert_interval("b#3").unwrap(), "A3");
644 assert_eq!(convert_interval("*3").unwrap(), "M3");
645 assert!(convert_interval("###4").is_err());
646 assert!(convert_interval("x").is_err());
647 }
648
649 #[test]
651 fn the_intervals_of_a_chord_are_read_off_it() {
652 for (label, intervals) in [
653 ("C", vec!["P5", "M3"]),
654 ("A", vec!["P5", "M3"]),
655 ("C:maj", vec!["P5", "M3"]),
656 ("C:min", vec!["P5", "m3"]),
657 ("C:dim", vec!["d5", "m3"]),
658 ("C:aug", vec!["A5", "M3"]),
659 ("N", vec![]),
660 ] {
661 let harte = Harte::new(label).unwrap();
662 let mut annotated = harte.chord().annotate_intervals(false, false).unwrap();
663 annotated.sort();
664 let mut wanted: Vec<String> = intervals.iter().map(|s| s.to_string()).collect();
665 wanted.sort();
666 assert_eq!(annotated, wanted, "{label}");
667 }
668 }
669
670 #[test]
672 fn degrees_sound_in_order_whatever_order_they_were_written_in() {
673 for (label, pitches) in [
674 ("F:(b3, 5, b7, 11)", ["F", "A-", "C", "E-", "B-"]),
675 ("F:(b3, 11, b7, 5)", ["F", "A-", "C", "E-", "B-"]),
676 ("F:maj7(#11)", ["F", "A", "C", "E", "B"]),
677 ] {
678 assert_eq!(
679 Harte::new(label).unwrap().chord().pitch_names(),
680 pitches,
681 "{label}"
682 );
683 }
684 }
685
686 #[test]
687 fn a_label_is_read_into_its_parts_and_the_chord_it_sounds() {
688 let names = |label: &str| -> Vec<String> {
689 Harte::new(label)
690 .unwrap()
691 .chord()
692 .pitches()
693 .iter()
694 .map(Pitch::name_with_octave)
695 .collect()
696 };
697 assert_eq!(names("Ab:maj(9)/9"), ["A-4", "C5", "E-5", "B-3"]);
698 assert_eq!(names("Bb:7/b7"), ["B-4", "D5", "F5", "A-3"]);
699 assert_eq!(names("C:maj(*3)"), ["C4", "G4"]);
700 assert_eq!(names("C:sus4(*3,9)"), ["C4", "F4", "G4", "D4"]);
701 assert_eq!(names("Db"), ["D-4", "F4", "A-4"]);
702 assert_eq!(names("C/5"), ["C4", "E4", "G3"]);
703
704 let harte = Harte::new("Ab:maj(9)/9").unwrap();
705 assert_eq!(harte.root_name(), Some("Ab"));
706 assert_eq!(harte.bass_degree(), Some("9"));
707 assert_eq!(harte.shorthand(), Some("maj"));
708 assert_eq!(harte.degrees().unwrap(), ["9"]);
709 assert_eq!(harte.unwrap_shorthand().unwrap(), ["1", "3", "5", "9"]);
710 assert!(!harte.bass_is_root());
711 assert!(harte.contains_shorthand());
712 assert_eq!(harte.chord().root().unwrap().name_with_octave(), "A-4");
713 assert_eq!(harte.chord().bass().unwrap().name_with_octave(), "B-3");
714 assert_eq!(harte.midi_pitches(), [58, 68, 72, 75]);
715 assert_eq!(harte.to_string(), "Ab:maj(9)/9");
716 assert_eq!(harte.prettify(), "Ab:maj(9)/9");
717
718 let seventh = Harte::new("F:maj7(#11)").unwrap();
719 assert_eq!(seventh.midi_pitches(), [65, 69, 71, 72, 76]);
720 assert_eq!(
721 seventh.multi_hot_encoding(false),
722 [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1]
723 );
724 assert_eq!(
725 seventh.multi_hot_encoding(true),
726 [1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1]
727 );
728 assert_eq!(seventh.prettify(), "F:maj7(#11)");
729
730 let written_out = Harte::new("C:(b3,5)").unwrap();
731 assert_eq!(written_out.prettify(), "C:min");
732 assert_eq!(written_out.unwrap_shorthand().unwrap(), ["b3", "5"]);
733 assert!(!written_out.contains_shorthand());
734 assert_eq!(written_out.to_string(), "C:(b3,5)");
735 assert_eq!(Harte::new("C:sus4(*3,9)").unwrap().prettify(), "C:sus4(9)");
736 assert_eq!(Harte::new("C:maj(*3)").unwrap().prettify(), "C:maj(*3)");
737 assert_eq!(Harte::new("C:maj(*3)").unwrap().degrees().unwrap(), ["*3"]);
738 assert!(Harte::new("C").unwrap().bass_is_root());
739 assert_eq!(Harte::new("C").unwrap().unwrap_shorthand(), None);
740 }
741
742 #[test]
743 fn no_chord_is_empty_and_a_bad_label_is_refused() {
744 for label in ["N", "X"] {
745 let none = Harte::new(label).unwrap();
746 assert!(none.is_empty());
747 assert!(none.chord().is_empty());
748 assert_eq!(none.root_name(), None);
749 assert_eq!(none.bass_degree(), None);
750 assert_eq!(none.degrees(), None);
751 assert_eq!(none.unwrap_shorthand(), None);
752 assert_eq!(none.midi_pitches(), Vec::<IntegerType>::new());
753 assert_eq!(none.multi_hot_encoding(false), [0; 12]);
754 assert_eq!(none.to_string(), "N");
755 assert_eq!(none.prettify(), label);
756 }
757 for label in [
758 "",
759 "H",
760 "C:",
761 "C:foo",
762 "C:maj(",
763 "C:maj(0)",
764 "C:maj(14)",
765 "G:9(113)",
766 "C:maj/",
767 "C x",
768 ] {
769 assert!(Harte::new(label).is_err(), "{label}");
770 }
771 assert_eq!(
772 "C:min7/b3".parse::<Harte>().unwrap().to_string(),
773 "C:min7/b3"
774 );
775 assert_eq!(Harte::new("C:maj").unwrap(), Harte::new("C:(3,5)").unwrap());
776 assert_ne!(Harte::new("C:maj").unwrap(), Harte::new("C:min").unwrap());
777 assert_ne!(Harte::new("C:maj").unwrap(), Harte::new("C:maj/3").unwrap());
778 }
779}