music21_rs/humdrum/harm.rs
1//! The harmonic annotations a `**harm` spine carries: music21's
2//! `humdrum.harmParser`.
3//!
4//! A `**harm` token is a scale degree with what is written round it --
5//! `V7b`, `#IVo7`, `Gnd`, `V/V/V`, `(I)`, `I[V]` -- and the two notations
6//! are not the same, so music21 converts one into the other: a `V7b` there
7//! is a `V65` to a [`RomanNumeral`](crate::roman::RomanNumeral), the
8//! inversion letter standing for the figured bass. The representation is
9//! described at <https://www.humdrum.org/rep/harm/>.
10//!
11//! music21 reads a token with a regular expression; the crate has none, so
12//! [`Harm::parse`] reads it by hand in the same order -- the accidental,
13//! the degree, the quality, the intervals, the inversion, an alternative
14//! reading in brackets, and a secondary function after a slash. The two
15//! agree on every token music21's own tests and docstrings name, and on the
16//! ones they do not: `harm::tests` holds forty more read off music21.
17//!
18//! The degree is matched longest first where music21's alternation tries
19//! the shortest and backtracks. The two come to the same thing, since no
20//! prefix of a degree (`i`, `v`, `I`, `V`) is a quality, an interval, an
21//! inversion, a bracket or a slash, so a shorter degree never leaves
22//! something the rest of the token can read.
23
24/// The degrees and special chords a `**harm` token may name, longest first.
25const ROOTS: [&str; 19] = [
26 "iii", "vii", "III", "VII", "iv", "vi", "ii", "IV", "VI", "II", "Gn", "Lt", "Fr", "Tr", "i",
27 "v", "I", "V", "N",
28];
29
30/// A `**harm` annotation read: music21's `HarmParser.parse`, whose answer is
31/// a dictionary of these same parts.
32#[derive(Clone, Debug, PartialEq, Eq, Default)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[must_use]
35pub struct Harm {
36 accidental: String,
37 root: String,
38 attribute: Option<char>,
39 intervals: Option<String>,
40 inversion: Option<char>,
41 alternative: Option<Box<Harm>>,
42 secondary: Option<Box<Harm>>,
43 implied: bool,
44}
45
46impl Harm {
47 /// Reads a `**harm` token, or nothing where it is not one: music21's
48 /// `HarmParser.parse`, which answers an empty dictionary there.
49 ///
50 /// ```
51 /// use music21_rs::humdrum::harm::Harm;
52 ///
53 /// let read = Harm::parse("#IV7b").expect("a harm token");
54 /// assert_eq!(read.accidental(), "#");
55 /// assert_eq!(read.root(), "IV");
56 /// assert_eq!(read.intervals(), Some("7"));
57 /// assert_eq!(read.inversion(), Some('b'));
58 /// assert_eq!(read.roman_figure(), "#IV65");
59 /// assert_eq!(Harm::parse("VIII"), None);
60 /// ```
61 pub fn parse(token: &str) -> Option<Self> {
62 // Harmony the annotator thought implied is written in parentheses
63 // round the whole token, and nothing else may be: music21's own
64 // pattern matches the whole string or fails.
65 if let Some(inner) = token
66 .strip_prefix('(')
67 .and_then(|rest| rest.strip_suffix(')'))
68 && !inner.is_empty()
69 && !inner.contains('(')
70 && !inner.contains(')')
71 {
72 let mut found = Self::parse(inner)?;
73 found.implied = true;
74 return Some(found);
75 }
76
77 let mut rest = token;
78 let mut read = Self::default();
79
80 // The accidental: up to two of `#` or `-`, as many as are written.
81 // Taking both can cost nothing, since no degree begins with either.
82 while read.accidental.len() < 2 {
83 let Some(mark) = rest
84 .chars()
85 .next()
86 .filter(|mark| *mark == '#' || *mark == '-')
87 else {
88 break;
89 };
90 read.accidental.push(mark);
91 rest = &rest[mark.len_utf8()..];
92 }
93
94 let root = ROOTS.iter().find(|root| rest.starts_with(**root))?;
95 read.root = (*root).to_string();
96 rest = &rest[root.len()..];
97
98 // Diminished or augmented, where the token says so.
99 if let Some(quality) = rest
100 .chars()
101 .next()
102 .filter(|mark| *mark == 'o' || *mark == '+')
103 {
104 read.attribute = Some(quality);
105 rest = &rest[quality.len_utf8()..];
106 }
107
108 // The intervals written over the degree, as many as the annotator
109 // wrote. music21 keeps the last of them, its group having matched
110 // once per interval, and that is what it reads the seventh off.
111 while let Some((interval, after)) = next_interval(rest) {
112 read.intervals = Some(interval.to_string());
113 rest = after;
114 }
115
116 if let Some(inversion) = rest
117 .chars()
118 .next()
119 .filter(|mark| ('b'..='d').contains(mark))
120 {
121 read.inversion = Some(inversion);
122 rest = &rest[inversion.len_utf8()..];
123 }
124
125 // An alternative reading in brackets, which music21 reads and the
126 // figure then says nothing of.
127 if let Some(after) = rest.strip_prefix('[') {
128 let (inner, after) = after.split_once(']')?;
129 if inner.is_empty() || inner.contains('[') {
130 return None;
131 }
132 read.alternative = Self::parse(inner).map(Box::new);
133 rest = after;
134 }
135
136 // A secondary function after a slash: the degree the one before it
137 // is read in. One that cannot be read is kept as nothing, which is
138 // the empty dictionary music21 keeps and then passes over.
139 if let Some(after) = rest.strip_prefix('/') {
140 if after.is_empty() {
141 return None;
142 }
143 read.secondary = Self::parse(after).map(Box::new);
144 rest = "";
145 }
146
147 rest.is_empty().then_some(read)
148 }
149
150 /// The accidental written in front of the degree: `#`, `--`, or nothing.
151 pub fn accidental(&self) -> &str {
152 &self.accidental
153 }
154
155 /// The degree or special chord: `V`, `ii`, `Gn`, `N`.
156 pub fn root(&self) -> &str {
157 &self.root
158 }
159
160 /// `o` for a diminished reading, `+` for an augmented one.
161 pub fn attribute(&self) -> Option<char> {
162 self.attribute
163 }
164
165 /// The last of the intervals written over the degree, as music21 keeps
166 /// them: `7` of `V7`, `AA6` of `VAA6`, `9` of `V79`.
167 pub fn intervals(&self) -> Option<&str> {
168 self.intervals.as_deref()
169 }
170
171 /// The inversion letter: `b` for the first inversion, `c` for the
172 /// second, `d` for the third.
173 pub fn inversion(&self) -> Option<char> {
174 self.inversion
175 }
176
177 /// The reading written in brackets beside this one, which the figure
178 /// says nothing of.
179 pub fn alternative(&self) -> Option<&Harm> {
180 self.alternative.as_deref()
181 }
182
183 /// The degree this one is read in, written after a slash.
184 pub fn secondary(&self) -> Option<&Harm> {
185 self.secondary.as_deref()
186 }
187
188 /// Whether the annotator wrote the token in parentheses, meaning the
189 /// harmony is implied rather than sounded.
190 pub fn implied(&self) -> bool {
191 self.implied
192 }
193
194 /// The figure a [`RomanNumeral`](crate::roman::RomanNumeral) is built
195 /// from: music21's `convertHarmToRoman`.
196 ///
197 /// The inversion letter becomes the figured bass it stands for -- a
198 /// seventh chord's `b` is `65` and a triad's is `6` -- a German or
199 /// French sixth is read as a seventh chord whatever intervals were
200 /// written, and only the *degrees* of the secondary functions are
201 /// carried across, as music21 carries them.
202 pub fn roman_figure(&self) -> String {
203 let (degree, intervals) = match self.root.as_str() {
204 "Gn" => ("Ger", Some("7")),
205 "Lt" => ("It", self.intervals()),
206 "Fr" => ("Fr", Some("7")),
207 root => (root, self.intervals()),
208 };
209 let seventh = intervals.is_some_and(|intervals| intervals.contains('7'));
210 let inversion = match self.inversion {
211 None => {
212 if seventh {
213 "7"
214 } else {
215 ""
216 }
217 }
218 Some('b') => {
219 if seventh {
220 "65"
221 } else {
222 "6"
223 }
224 }
225 Some('c') => {
226 if seventh {
227 "43"
228 } else {
229 "64"
230 }
231 }
232 // A third inversion is a seventh chord's, or a special chord's.
233 _ => "2",
234 };
235 let mut figure = format!(
236 "{}{degree}{}{inversion}",
237 self.accidental,
238 self.attribute.map(String::from).unwrap_or_default()
239 );
240 let mut secondary = self.secondary();
241 while let Some(found) = secondary {
242 figure.push('/');
243 figure.push_str(&found.root);
244 secondary = found.secondary();
245 }
246 figure
247 }
248}
249
250/// The interval at the front of what is left, if one is written there:
251/// `AA6`, `DD7`, `M7` or a plain `7`, as music21's own alternation reads
252/// them.
253fn next_interval(rest: &str) -> Option<(&str, &str)> {
254 let digits = |from: usize| -> Option<usize> {
255 let count = rest[from..]
256 .chars()
257 .take_while(char::is_ascii_digit)
258 .count();
259 (count > 0).then_some(from + count)
260 };
261 for prefix in ["AA", "DD"] {
262 if rest.starts_with(prefix)
263 && let Some(end) = digits(prefix.len())
264 {
265 return Some((&rest[..end], &rest[end..]));
266 }
267 }
268 if rest
269 .chars()
270 .next()
271 .is_some_and(|first| "mMPAD".contains(first))
272 && let Some(end) = digits(1)
273 {
274 return Some((&rest[..end], &rest[end..]));
275 }
276 digits(0).map(|end| (&rest[..end], &rest[end..]))
277}
278
279/// The figure a `**harm` token stands for, or nothing where it is no token:
280/// music21's `convertHarmToRoman`.
281///
282/// ```
283/// use music21_rs::humdrum::harm::convert_harm_to_roman;
284///
285/// assert_eq!(convert_harm_to_roman("V7b").as_deref(), Some("V65"));
286/// assert_eq!(convert_harm_to_roman("Gnd").as_deref(), Some("Ger2"));
287/// assert_eq!(convert_harm_to_roman("x"), None);
288/// ```
289pub fn convert_harm_to_roman(token: &str) -> Option<String> {
290 Harm::parse(token).map(|harm| harm.roman_figure())
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 /// Every figure here is music21's own for the same token: the triads,
298 /// sevenths, special chords and alterations of its own tests, the
299 /// docstrings' examples, and forty more read off music21 beside them.
300 #[test]
301 fn a_harm_token_reads_as_music21_reads_it() {
302 for (token, figure) in [
303 // The docstrings' own.
304 ("I", Some("I")),
305 ("Vc", Some("V64")),
306 ("Ib", Some("I6")),
307 ("iib", Some("ii6")),
308 ("V", Some("V")),
309 ("viiob", Some("viio6")),
310 ("vib", Some("vi6")),
311 ("V7", Some("V7")),
312 ("viio7c", Some("viio43")),
313 ("V7d", Some("V2")),
314 ("viio7b", Some("viio65")),
315 ("V7c", Some("V43")),
316 ("Lt", Some("It")),
317 ("Ltb", Some("It6")),
318 ("Ltc", Some("It64")),
319 ("Fr", Some("Fr7")),
320 ("Frb", Some("Fr65")),
321 ("Frc", Some("Fr43")),
322 ("Frd", Some("Fr2")),
323 ("Gn", Some("Ger7")),
324 ("Gnb", Some("Ger65")),
325 ("Gnc", Some("Ger43")),
326 ("Gnd", Some("Ger2")),
327 // Its unit tests' special chords and alterations.
328 ("N", Some("N")),
329 ("Nb", Some("N6")),
330 ("Nc", Some("N64")),
331 ("-IIb", Some("-II6")),
332 ("--IIc", Some("--II64")),
333 ("#IV7", Some("#IV7")),
334 ("##IV7b", Some("##IV65")),
335 // Every degree, in both cases.
336 ("i", Some("i")),
337 ("iv", Some("iv")),
338 ("vii", Some("vii")),
339 ("III", Some("III")),
340 ("VI", Some("VI")),
341 ("IV", Some("IV")),
342 ("ib", Some("i6")),
343 ("ivc", Some("iv64")),
344 ("viid", Some("vii2")),
345 ("I7", Some("I7")),
346 ("I7b", Some("I65")),
347 ("I7c", Some("I43")),
348 ("I7d", Some("I2")),
349 ("i7", Some("i7")),
350 // The qualities.
351 ("viio", Some("viio")),
352 ("viio7", Some("viio7")),
353 ("V+", Some("V+")),
354 ("ii+7", Some("ii+7")),
355 ("iiio", Some("iiio")),
356 ("iio", Some("iio")),
357 ("iio7", Some("iio7")),
358 ("iiiob", Some("iiio6")),
359 ("Io7", Some("Io7")),
360 ("V+7b", Some("V+65")),
361 ("vii+", Some("vii+")),
362 // The intervals: only a seventh among them changes the figure,
363 // and the quality letter in front of one is not carried over.
364 ("VM7", Some("V7")),
365 ("Vm7", Some("V7")),
366 ("VD7", Some("V7")),
367 ("VA6", Some("V")),
368 ("VAA6", Some("V")),
369 ("VDD7", Some("V7")),
370 ("V9", Some("V")),
371 ("V11", Some("V")),
372 ("V13", Some("V")),
373 ("V79", Some("V7")),
374 ("V0", Some("V")),
375 ("I8", Some("I")),
376 ("IM7b", Some("I65")),
377 ("Vm7c", Some("V43")),
378 ("TrD7", Some("Tr7")),
379 ("N7", Some("N7")),
380 ("Lt7", Some("It7")),
381 ("Ltd", Some("It2")),
382 // A German or French sixth is a seventh chord whatever is
383 // written over it.
384 ("Gn7b", Some("Ger65")),
385 ("Fr7b", Some("Fr65")),
386 // The secondary functions, of which only the degrees carry.
387 ("V/V", Some("V/V")),
388 ("V7/IV", Some("V7/IV")),
389 ("viio/ii", Some("viio/ii")),
390 ("V/V/V", Some("V/V/V")),
391 ("viioD7/iv/v", Some("viio7/iv/v")),
392 ("V7b/ii", Some("V65/ii")),
393 ("V/#iv", Some("V/iv")),
394 ("V/ivb", Some("V/iv")),
395 ("V/iv7", Some("V/iv")),
396 ("V7/V7", Some("V7/V")),
397 ("V/N", Some("V/N")),
398 ("V/Gn", Some("V/Gn")),
399 // A secondary that cannot be read is passed over, as music21
400 // passes over the empty dictionary its own parse answers.
401 ("V//V", Some("V")),
402 // Implied harmony, and an alternative reading.
403 ("(I)", Some("I")),
404 ("(V/V)", Some("V/V")),
405 ("(Ib)", Some("I6")),
406 ("I[V]", Some("I")),
407 ("I[V/IV]", Some("I")),
408 ("V7[viio]", Some("V7")),
409 ("I[V]/V", Some("I/V")),
410 // Both accidentals, in either order, and on a special chord.
411 ("#-I", Some("#-I")),
412 ("-#I", Some("-#I")),
413 ("#N", Some("#N")),
414 ("-Lt", Some("-It")),
415 ("Tr", Some("Tr")),
416 ("Trb", Some("Tr6")),
417 // What is no token at all.
418 ("", None),
419 (".", None),
420 ("x", None),
421 ("VIII", None),
422 ("I/", None),
423 ("/V", None),
424 ("Ie", None),
425 ("I!", None),
426 ("I-", None),
427 ("I7o", None),
428 ("iib7", None),
429 ("I#", None),
430 ("I(", None),
431 ("#", None),
432 ("-", None),
433 ("o", None),
434 ("+", None),
435 ("(I)/V", None),
436 ] {
437 assert_eq!(
438 convert_harm_to_roman(token).as_deref(),
439 figure,
440 "**harm token {token}"
441 );
442 }
443 }
444
445 /// The parts music21's own `parse` answers with, for the tokens whose
446 /// dictionary its docstrings show.
447 #[test]
448 fn the_parts_of_a_token_are_music21s() {
449 let read = Harm::parse("iib").expect("a token");
450 assert_eq!((read.root(), read.inversion()), ("ii", Some('b')));
451 assert!(!read.implied());
452
453 let read = Harm::parse("#IV7").expect("a token");
454 assert_eq!(read.accidental(), "#");
455 assert_eq!(read.intervals(), Some("7"));
456
457 let read = Harm::parse("VAA6").expect("a token");
458 assert_eq!(read.intervals(), Some("AA6"));
459
460 let read = Harm::parse("(I)").expect("a token");
461 assert!(read.implied());
462 assert_eq!(read.root(), "I");
463
464 let read = Harm::parse("I[V]").expect("a token");
465 assert_eq!(read.alternative().map(Harm::root), Some("V"));
466 assert!(read.secondary().is_none());
467
468 // The chain of secondary functions, which the figure walks: a
469 // degree read in a degree read in the key.
470 let read = Harm::parse("V/V/V").expect("a token");
471 assert_eq!(read.secondary().map(Harm::root), Some("V"));
472 assert_eq!(
473 read.secondary().and_then(Harm::secondary).map(Harm::root),
474 Some("V")
475 );
476 assert!(
477 read.secondary()
478 .and_then(Harm::secondary)
479 .and_then(Harm::secondary)
480 .is_none()
481 );
482 }
483}