Skip to main content

music21_rs/
figuredbass.rs

1//! Figured bass: the numbers written under a bass note, and what they mean.
2//!
3//! A column of figures says how far above the bass each note of the chord
4//! stands, and an accidental beside a number says how that note is spelled.
5//! Most of a column is left out in practice — a bare `7` means a seventh
6//! chord in root position, fifth and third and all — so a column is read in
7//! two forms: as written, and expanded to every note it stands for.
8//!
9//! This is the port of music21's `figuredBass.notation`, and it is what
10//! [`crate::roman::RomanNumeral`] reads its own digits with.
11
12use std::fmt;
13
14use crate::{
15    defaults::IntegerType,
16    error::{Error, Result},
17    pitch::{Accidental, Pitch},
18};
19
20/// The number a figure carries when it is nothing but an extender line:
21/// music21's `EXTENDER_SENTINEL`.
22pub const EXTENDER: IntegerType = -1;
23
24/// The shorthand a column is written in, and every note it stands for.
25///
26/// music21's `shorthandNotation`. A column not listed here is read as it was
27/// written.
28const SHORTHAND: &[(&[Option<IntegerType>], &[IntegerType])] = &[
29    // A column written as nothing at all, or as a bare accidental, is the
30    // triad over its bass.
31    (&[None], &[5, 3]),
32    (&[Some(5)], &[5, 3]),
33    (&[Some(6)], &[6, 3]),
34    (&[Some(7)], &[7, 5, 3]),
35    (&[Some(9)], &[9, 7, 5, 3]),
36    (&[Some(11)], &[11, 9, 7, 5, 3]),
37    (&[Some(13)], &[13, 11, 9, 7, 5, 3]),
38    (&[Some(6), Some(5)], &[6, 5, 3]),
39    (&[Some(4), Some(3)], &[6, 4, 3]),
40    (&[Some(4), Some(2)], &[6, 4, 2]),
41    (&[Some(2)], &[6, 4, 2]),
42];
43
44/// The marks figured bass writes that music21's `Accidental` does not know by
45/// itself, and what each stands for: music21's `specialModifiers`.
46const SPECIAL: &[(&str, &str)] = &[
47    ("+", "#"),
48    ("/", "-"),
49    ("\\", "#"),
50    ("b", "-"),
51    ("bb", "--"),
52    ("bbb", "---"),
53    ("bbbb", "-----"),
54    ("++", "##"),
55    ("+++", "###"),
56    ("++++", "####"),
57    ("\u{266f}", "#"),
58    ("\u{266e}", "n"),
59    ("\u{266d}", "-"),
60];
61
62/// The accidental written beside a figure: music21's `Modifier`.
63///
64/// Figured bass has its own marks — a `+` raises and a `/` lowers — so the
65/// string it was written with is kept alongside the accidental it means.
66#[derive(Clone, Debug, Default, PartialEq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[must_use]
69pub struct Modifier {
70    written: Option<String>,
71    accidental: Option<Accidental>,
72}
73
74impl Modifier {
75    /// Reads one, which may be nothing at all: neither `None` nor the empty
76    /// string says anything about how the note is spelled.
77    pub fn new(written: Option<&str>) -> Result<Self> {
78        let Some(mark) = written.filter(|mark| !mark.is_empty()) else {
79            return Ok(Self {
80                written: written.map(str::to_string),
81                accidental: None,
82            });
83        };
84        let accidental = match Accidental::new(mark) {
85            Ok(accidental) => accidental,
86            Err(_) => {
87                let Some((_, spelled)) = SPECIAL.iter().find(|(special, _)| *special == mark)
88                else {
89                    return Err(Error::Notation(format!(
90                        "Figure modifier unsupported in music21: {mark}"
91                    )));
92                };
93                Accidental::new(*spelled)?
94            }
95        };
96        Ok(Self {
97            written: Some(mark.to_string()),
98            accidental: Some(accidental),
99        })
100    }
101
102    /// The mark as it was written.
103    pub fn written(&self) -> Option<&str> {
104        self.written.as_deref()
105    }
106
107    /// The accidental it stands for, and nothing where nothing was written.
108    pub fn accidental(&self) -> Option<&Accidental> {
109        self.accidental.as_ref()
110    }
111
112    /// The same note spelled as this modifier asks.
113    ///
114    /// A written natural replaces whatever was there; anything else is added
115    /// to it, so a sharp against a flattened note raises it to a natural.
116    pub fn modify(&self, pitch: &Pitch) -> Result<Pitch> {
117        let Some(accidental) = &self.accidental else {
118            return Ok(pitch.clone());
119        };
120        let mut modified = pitch.clone();
121        let alter = if accidental.alter() == 0.0 || !pitch.has_accidental() {
122            accidental.alter()
123        } else {
124            pitch.accidental().alter() + accidental.alter()
125        };
126        modified.set_accidental(Some(Accidental::new(alter)?));
127        Ok(modified)
128    }
129
130    /// The name of a pitch spelled as this modifier asks: music21's
131    /// `modifyPitchName`, so a sharp makes `D` into `D#`.
132    pub fn modify_pitch_name(&self, name: &str) -> Result<String> {
133        Ok(self.modify(&Pitch::from_name(name)?)?.name())
134    }
135}
136
137/// A pitch from its name: music21's `convertToPitch`, which raises a
138/// `ValueError` rather than a pitch error for a name it cannot read.
139pub fn convert_to_pitch(name: &str) -> Result<Pitch> {
140    Pitch::from_name(name)
141        .map_err(|_| Error::Value(format!("Cannot convert string {name} to a music21 Pitch.")))
142}
143
144impl fmt::Display for Modifier {
145    /// music21's `_reprInternal`: the mark and the accidental it means.
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        let written = match &self.written {
148            Some(written) => written.as_str(),
149            None => "None",
150        };
151        match &self.accidental {
152            Some(accidental) => write!(f, "{written} {}", accidental.name()),
153            None => write!(f, "{written} None"),
154        }
155    }
156}
157
158/// One figure of a column: a number above the bass and the accidental written
159/// beside it.
160#[derive(Clone, Debug, PartialEq)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
162#[must_use]
163pub struct Figure {
164    number: Option<IntegerType>,
165    modifier: Modifier,
166    extender: bool,
167}
168
169impl Figure {
170    /// One figure. A number of `None` is a figure written as a bare
171    /// accidental, which stands for the third.
172    pub fn new(number: Option<IntegerType>, modifier: Modifier, extender: bool) -> Self {
173        Self {
174            number,
175            modifier,
176            extender,
177        }
178    }
179
180    /// How far above the bass the note stands, counted inclusively.
181    pub fn number(&self) -> Option<IntegerType> {
182        self.number
183    }
184
185    /// Puts the figure at another number.
186    pub fn set_number(&mut self, number: Option<IntegerType>) {
187        self.number = number;
188    }
189
190    /// The accidental written beside the number.
191    pub fn modifier(&self) -> &Modifier {
192        &self.modifier
193    }
194
195    /// Whether a line carries the figure on from the note before.
196    pub fn has_extender(&self) -> bool {
197        self.extender
198    }
199
200    /// Whether the figure is nothing but that line: music21 reads a number of
201    /// one as no number at all, so an extender on it carries the whole
202    /// column forward.
203    pub fn is_pure_extender(&self) -> bool {
204        self.number == Some(1) && self.extender
205    }
206}
207
208impl fmt::Display for Figure {
209    /// music21's `_reprInternal`.
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        if self.is_pure_extender() {
212            return write!(f, "pure-extender <Modifier {}>", self.modifier);
213        }
214        let number = match self.number {
215            Some(EXTENDER) => "_".to_string(),
216            Some(number) => number.to_string(),
217            None => "None".to_string(),
218        };
219        let extender = if self.extender { "(extender)" } else { "" };
220        write!(f, "{number}{extender} <Modifier {}>", self.modifier)
221    }
222}
223
224/// One column of figured bass, as written and as it stands expanded.
225///
226/// music21's `figuredBass.notation.Notation`. The figures are given as one
227/// string, comma-separated: `'7,5,#3'`, `'6,4'`, `'4+,2'`.
228#[derive(Clone, Debug, Default, PartialEq)]
229#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
230#[must_use]
231pub struct Notation {
232    column: String,
233    figure_strings: Vec<String>,
234    original_numbers: Vec<Option<IntegerType>>,
235    original_modifiers: Vec<Option<String>>,
236    numbers: Vec<Option<IntegerType>>,
237    modifier_strings: Vec<Option<String>>,
238    extenders: Vec<bool>,
239    figures: Vec<Figure>,
240    figures_as_written: Vec<Figure>,
241}
242
243impl Notation {
244    /// Reads a column.
245    pub fn parse(column: &str) -> Result<Self> {
246        let mut notation = Self {
247            column: column.to_string(),
248            ..Self::default()
249        };
250        notation.read_column()?;
251        notation.expand();
252        notation.build_figures()?;
253        Ok(notation)
254    }
255
256    /// music21's `_parseNotationColumn`: every comma-separated figure split
257    /// into its number and the mark beside it.
258    fn read_column(&mut self) -> Result<()> {
259        for written in self.column.split(',') {
260            let written = written.trim();
261            self.figure_strings.push(written.to_string());
262            let digits: String = written
263                .chars()
264                .filter(|letter| letter.is_ascii_digit() || *letter == '_')
265                .collect();
266            let marks: String = written
267                .chars()
268                .filter(|letter| !letter.is_ascii_digit() && *letter != '_')
269                .collect();
270
271            let mut number = None;
272            let mut extender = false;
273            if !digits.is_empty() {
274                if digits == "_" {
275                    number = Some(EXTENDER);
276                    extender = true;
277                } else if digits.contains('_') {
278                    extender = true;
279                    number = digits.trim_matches('_').parse().ok();
280                } else {
281                    number = digits.parse().ok();
282                }
283            }
284            self.original_numbers.push(number);
285            self.original_modifiers
286                .push((!marks.is_empty()).then_some(marks));
287            self.extenders.push(extender);
288        }
289        self.numbers = self.original_numbers.clone();
290        self.modifier_strings = self.original_modifiers.clone();
291        Ok(())
292    }
293
294    /// music21's `_translateToLonghand`: a column written in shorthand stands
295    /// for every note the shorthand leaves out, and each mark stays with the
296    /// number it was written against.
297    fn expand(&mut self) {
298        let longhand = SHORTHAND
299            .iter()
300            .find(|(shorthand, _)| *shorthand == self.numbers.as_slice())
301            .map(|(_, longhand)| longhand.to_vec());
302        let Some(longhand) = longhand else {
303            // A figure written as a bare accidental is the third.
304            self.numbers = self
305                .numbers
306                .iter()
307                .map(|number| Some(number.unwrap_or(3)))
308                .collect();
309            return;
310        };
311        let against: Vec<IntegerType> = self
312            .numbers
313            .iter()
314            .map(|number| number.unwrap_or(3))
315            .collect();
316        self.modifier_strings = longhand
317            .iter()
318            .map(|number| {
319                against
320                    .iter()
321                    .position(|written| written == number)
322                    .and_then(|index| self.modifier_strings[index].clone())
323            })
324            .collect();
325        self.numbers = longhand.into_iter().map(Some).collect();
326    }
327
328    /// music21's `_getFigures`: the numbers and their marks, paired up.
329    fn build_figures(&mut self) -> Result<()> {
330        for (index, number) in self.numbers.iter().enumerate() {
331            let modifier = Modifier::new(self.modifier_strings[index].as_deref())?;
332            let extender = self.extenders.get(index).copied().unwrap_or(false);
333            self.figures.push(Figure::new(*number, modifier, extender));
334        }
335        for (index, number) in self.original_numbers.iter().enumerate() {
336            let modifier = Modifier::new(self.original_modifiers[index].as_deref())?;
337            self.figures_as_written
338                .push(Figure::new(*number, modifier, false));
339        }
340        Ok(())
341    }
342
343    /// The column as it was written.
344    pub fn column(&self) -> &str {
345        &self.column
346    }
347
348    /// Each figure of the written column, as a string.
349    pub fn figure_strings(&self) -> &[String] {
350        &self.figure_strings
351    }
352
353    /// The numbers as written, before the shorthand was expanded.
354    pub fn original_numbers(&self) -> &[Option<IntegerType>] {
355        &self.original_numbers
356    }
357
358    /// The marks as written.
359    pub fn original_modifiers(&self) -> &[Option<String>] {
360        &self.original_modifiers
361    }
362
363    /// The numbers of the expanded column.
364    pub fn numbers(&self) -> &[Option<IntegerType>] {
365        &self.numbers
366    }
367
368    /// The marks of the expanded column.
369    pub fn modifier_strings(&self) -> &[Option<String>] {
370        &self.modifier_strings
371    }
372
373    /// Whether any figure carries a line on from the note before.
374    pub fn has_extenders(&self) -> bool {
375        self.extenders.iter().any(|extender| *extender)
376    }
377
378    /// Which figures carry such a line.
379    pub fn extenders(&self) -> &[bool] {
380        &self.extenders
381    }
382
383    /// The figures of the expanded column.
384    pub fn figures(&self) -> &[Figure] {
385        &self.figures
386    }
387
388    /// The figures of the column as it was written.
389    pub fn figures_as_written(&self) -> &[Figure] {
390        &self.figures_as_written
391    }
392
393    /// The accidentals of the expanded column, one per figure.
394    pub fn modifiers(&self) -> Vec<&Modifier> {
395        self.figures.iter().map(Figure::modifier).collect()
396    }
397}
398
399impl fmt::Display for Notation {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        f.write_str(&self.column)
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn a_column_keeps_what_it_was_written_with_beside_what_it_means() {
411        let notation = Notation::parse("6,#").unwrap();
412        assert_eq!(notation.column(), "6,#");
413        assert_eq!(notation.figures_as_written().len(), 2);
414        assert_eq!(notation.original_modifiers(), [None, Some("#".to_string())]);
415        assert_eq!(notation.extenders(), [false, false]);
416        assert_eq!(notation.modifiers().len(), notation.figures().len());
417        assert_eq!(notation.modifiers()[1].written(), Some("#"));
418        assert_eq!(notation.modifiers()[0].written(), None);
419        assert!(notation.to_string().contains("6,#"));
420
421        let mut figure = Figure::new(Some(6), Modifier::new(None).unwrap(), true);
422        assert!(figure.has_extender());
423        assert!(!figure.is_pure_extender());
424        figure.set_number(Some(1));
425        assert!(figure.is_pure_extender());
426        figure.set_number(None);
427        assert_eq!(figure.number(), None);
428    }
429
430    /// music21's own examples for `modifyPitchName` and `convertToPitch`.
431    #[test]
432    fn a_modifier_respells_a_pitch_name() {
433        assert_eq!(
434            Modifier::new(Some("#"))
435                .unwrap()
436                .modify_pitch_name("D")
437                .unwrap(),
438            "D#"
439        );
440        assert_eq!(
441            Modifier::new(Some("-"))
442                .unwrap()
443                .modify_pitch_name("F")
444                .unwrap(),
445            "F-"
446        );
447        assert_eq!(
448            Modifier::new(Some("n"))
449                .unwrap()
450                .modify_pitch_name("C#")
451                .unwrap(),
452            "C"
453        );
454        assert_eq!(
455            Modifier::new(None)
456                .unwrap()
457                .modify_pitch_name("B-")
458                .unwrap(),
459            "B-"
460        );
461        assert!(
462            Modifier::new(Some("#"))
463                .unwrap()
464                .modify_pitch_name("H")
465                .is_err()
466        );
467
468        assert_eq!(convert_to_pitch("C5").unwrap().to_string(), "C5");
469        assert!(matches!(convert_to_pitch("nonsense"), Err(Error::Value(_))));
470    }
471
472    #[test]
473    fn a_column_expands_out_of_its_shorthand() {
474        let notation = Notation::parse("4+,2").unwrap();
475        assert_eq!(notation.figure_strings(), ["4+", "2"]);
476        assert_eq!(notation.original_numbers(), [Some(4), Some(2)]);
477        assert_eq!(notation.numbers(), [Some(6), Some(4), Some(2)]);
478        assert_eq!(
479            notation.modifier_strings(),
480            [None, Some("+".to_string()), None]
481        );
482        assert_eq!(
483            notation.figures()[1].to_string(),
484            "4 <Modifier + sharp>",
485            "the mark stays with the number it was written against"
486        );
487    }
488
489    #[test]
490    fn a_bare_mark_is_the_third() {
491        let notation = Notation::parse("-6, -").unwrap();
492        assert_eq!(notation.original_numbers(), [Some(6), None]);
493        assert_eq!(notation.numbers(), [Some(6), Some(3)]);
494        assert_eq!(notation.figures()[1].to_string(), "3 <Modifier - flat>");
495    }
496
497    #[test]
498    fn figured_bass_reads_its_own_marks() {
499        // A `+` raises and a `/` lowers, which no accidental name would.
500        assert_eq!(
501            Modifier::new(Some("+"))
502                .unwrap()
503                .accidental()
504                .map(|accidental| accidental.name()),
505            Some("sharp")
506        );
507        assert_eq!(
508            Modifier::new(Some("/"))
509                .unwrap()
510                .accidental()
511                .map(|accidental| accidental.name()),
512            Some("flat")
513        );
514        assert!(Modifier::new(Some("")).unwrap().accidental().is_none());
515        assert!(Modifier::new(None).unwrap().accidental().is_none());
516        assert!(Modifier::new(Some("zzz")).is_err());
517    }
518
519    #[test]
520    fn an_extender_carries_the_figure_on() {
521        let notation = Notation::parse("7_").unwrap();
522        assert!(notation.has_extenders());
523        let pure = Figure::new(Some(1), Modifier::new(Some("#")).unwrap(), true);
524        assert!(pure.is_pure_extender());
525        assert_eq!(pure.to_string(), "pure-extender <Modifier # sharp>");
526    }
527}