Skip to main content

music21_rs/
harte.rs

1//! Harte chord notation, as read by
2//! [harte-library](https://github.com/andreamust/harte-library): `C:maj7/3`,
3//! `Bb:(b3,5,b7,9)`, `F:sus4(*3,9)`, `N`.
4//!
5//! A label is a root, an optional shorthand, an optional list of degrees in
6//! parentheses and an optional bass degree after a slash. `*3` in the
7//! degrees removes the third. A degree is written the Harte way, `b3`, `#11`,
8//! `bb7`; [`convert_interval`] gives the music21 interval it stands for, and
9//! [`Harte`] builds the chord the label sounds.
10
11use 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
20/// A shorthand beside the degrees it stands for.
21pub type Shorthand = (&'static str, &'static [&'static str]);
22
23/// The degrees each Harte shorthand stands for, the root included.
24pub 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
75/// The shorthands [`Harte::prettify`] folds a set of degrees into, tried in
76/// this order, so a ninth is written as one before its seventh is.
77const 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
96/// The degrees a shorthand stands for, or `None` for a name that is not one.
97pub 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
104/// The music21 interval a Harte degree stands for: `b3` is `m3`, `#11` is
105/// `A4`, `bb7` is `d7`. Compound degrees fold to simple ones, and a double
106/// sharp or flat on a perfect degree is read as the neighbouring degree with
107/// one accidental fewer, as harte-library reads them.
108pub 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
134/// The first run of sharps, then flats, then digits in a degree, read the
135/// way the library's `([#]+)?([b]+)?(\d+)` search reads it: a run that ends
136/// without a digit is skipped over, so `b#3` is one sharp above a third.
137fn 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
153/// Where a degree sorts among the others: by its number, a flat just below
154/// it and a sharp just above, so `b3` comes after `2` and before `3`.
155pub 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/// A Harte degree beside the music21 interval it stands for.
172#[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    /// Reads a Harte degree such as `b3`.
182    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    /// The degree as written.
192    pub fn written(&self) -> &str {
193        &self.written
194    }
195
196    /// The interval it stands for.
197    pub fn interval(&self) -> &Interval {
198        &self.interval
199    }
200
201    /// The pitch this degree names above a root.
202    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/// The parts of a label as written.
214#[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
222/// Reads a label the way harte-library's grammar does: spaces are ignored
223/// anywhere, `N` and `X` are no chord, and a degree is a number from 1 to
224/// 13 with any accidentals in front of it and an optional `*` before those.
225fn 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/// A chord read from a Harte label, with the [`Chord`] it sounds.
314///
315/// The chord's pitches stand in degree order above the root in octave 4,
316/// the bass an octave lower where it is not the root, and the root and bass
317/// are fixed on the chord as the label names them. A label of `N` or `X` is
318/// no chord at all: it has no root and an empty chord.
319///
320/// Two labels are equal when they sound the same chord: the same root, the
321/// same degrees once the shorthand is unwrapped, and the same bass. `C:maj`
322/// equals `C:(3,5)`.
323#[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    /// Reads a label and builds the chord it names.
338    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(&degree.as_str()) && !degree.starts_with('*'));
369        all.push(bass.clone());
370        all.sort_by(|a, b| {
371            degree_sort_key(a)
372                .partial_cmp(&degree_sort_key(b))
373                .unwrap_or(std::cmp::Ordering::Equal)
374                .then_with(|| a.cmp(b))
375        });
376        all.dedup();
377
378        // music21 writes a flat as `-`, and several flats as several.
379        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        // The bass sounds an octave below the rest, unless it is the root.
387        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    /// The label as it was given.
416    pub fn label(&self) -> &str {
417        &self.label
418    }
419
420    /// Whether the label names no chord, `N` or `X`.
421    pub fn is_empty(&self) -> bool {
422        self.root.is_none()
423    }
424
425    /// The chord the label sounds.
426    pub fn chord(&self) -> &Chord {
427        &self.chord
428    }
429
430    /// The chord, handed over.
431    pub fn into_chord(self) -> Chord {
432        self.chord
433    }
434
435    /// The root as written, `Bb`; harte-library's `get_root`.
436    pub fn root_name(&self) -> Option<&str> {
437        self.root.as_deref()
438    }
439
440    /// The bass as a degree above the root, `1` when none was written;
441    /// harte-library's `get_bass`.
442    pub fn bass_degree(&self) -> Option<&str> {
443        self.bass.as_deref()
444    }
445
446    /// The shorthand, where the label carries one; harte-library's
447    /// `get_shorthand`.
448    pub fn shorthand(&self) -> Option<&str> {
449        self.shorthand.as_deref()
450    }
451
452    /// The degrees written in parentheses, `*3` included, or `None` when
453    /// there were none; harte-library's `get_degrees`.
454    pub fn degrees(&self) -> Option<&[String]> {
455        (!self.degrees.is_empty()).then_some(self.degrees.as_slice())
456    }
457
458    /// Every degree the chord sounds, the shorthand's and the written ones
459    /// together with the root and the bass, in order; harte-library's
460    /// `unwrap_shorthand`, which answers the written degrees alone for a
461    /// label with no shorthand and nothing for one with neither.
462    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    /// Every degree the chord sounds, the root and the bass among them, in
471    /// order; empty for no chord.
472    pub fn sounding_degrees(&self) -> &[String] {
473        &self.all_degrees
474    }
475
476    /// Whether the bass is the root.
477    pub fn bass_is_root(&self) -> bool {
478        self.root.is_some() && self.bass.as_deref() == Some("1")
479    }
480
481    /// Whether the label carries a shorthand.
482    pub fn contains_shorthand(&self) -> bool {
483        self.shorthand.is_some()
484    }
485
486    /// The MIDI numbers of the pitches, lowest first.
487    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    /// Which of the twelve pitch classes the chord sounds, one flag each,
494    /// counted from C, or from the root with `transpose`.
495    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    /// The label rewritten with the largest shorthand its degrees contain,
510    /// the degrees left over in parentheses and the bass after a slash:
511    /// `C:(b3,5)` is `C:min`. A label whose degrees fit no shorthand comes
512    /// back as it was written; so does `N`.
513    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    /// The label in its canonical spelling: root, shorthand, degrees and
556    /// bass, without spaces; `N` for no chord.
557    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    /// harte-library's `test_harte_interval`, every case.
591    #[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    /// harte-library's `test_interval_extraction`.
650    #[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    /// harte-library's `test_ordering_of_degrees`.
671    #[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}