Skip to main content

music21_rs/romantext/
tsv.rs

1//! The tabular format for harmonic analysis the Digital and Cognitive
2//! Musicology Lab annotates its corpora in: music21's
3//! `romanText.tsvConverter`.
4//!
5//! A DCML table is one row a chord: the measure and the beat it falls on,
6//! the global and local keys, and the chord in parts -- the numeral, the
7//! form, the figured bass, the added tones and the degree it is read in --
8//! beside the label the annotator wrote. [`from_tsv`] reads a table into a
9//! score of [`RomanNumeral`](crate::roman::RomanNumeral)s and [`to_tsv`]
10//! writes one back out.
11//!
12//! The two standards the lab has published are both read: version 1 as the
13//! Annotated Beethoven Corpus was first released, and version 2 as it is
14//! now, whose columns are named differently and whose labels carry the
15//! added tones in parentheses. [`Version`] says which.
16//!
17//! The two notations differ in more than their columns, and the swaps are
18//! music21's: a half-diminished chord is `%` there and `ø` here, a seventh
19//! says its quality there and not here, a minor key's degrees are written
20//! in upper case there where music21 writes the mode's own, and the added
21//! tones of version 2 become music21's `[no5][add6]` brackets.
22
23use crate::bar::Ending;
24use crate::chordsymbol::ChordSymbol;
25use crate::defaults::{FloatType, IntegerType};
26use crate::duration::{Duration, limited_fraction};
27use crate::error::{Error, Result};
28use crate::expressions::python_float;
29use crate::key::Key;
30use crate::makenotation::op_frac;
31use crate::meter::TimeSignature;
32use crate::roman::{Minor67Default, RomanNumeral};
33use crate::stream::{Stream, StreamElement, StreamEvent, StreamKind};
34
35use super::music21_key_name;
36
37/// Which of the lab's two standards a table is written in.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum Version {
41    /// The columns of the Annotated Beethoven Corpus as first released.
42    #[default]
43    One,
44    /// The columns as the lab writes them now, the chord label carrying its
45    /// added tones.
46    Two,
47}
48
49impl Version {
50    /// The standard a number names: music21's `dcml_version`.
51    ///
52    /// # Errors
53    ///
54    /// Any number but one or two.
55    pub fn from_number(number: u8) -> Result<Self> {
56        match number {
57            1 => Ok(Self::One),
58            2 => Ok(Self::Two),
59            other => Err(Error::RomanText(format!(
60                "dcml_version {other} is not in (1, 2)"
61            ))),
62        }
63    }
64
65    /// The number music21 calls it by.
66    pub fn number(self) -> u8 {
67        match self {
68            Self::One => 1,
69            Self::Two => 2,
70        }
71    }
72
73    /// The columns this package reads, in the order music21 lists them:
74    /// music21's `V1_HEADERS` and `V2_HEADERS`. A column named here is read
75    /// into the row; any other is kept beside it.
76    pub fn read_columns(self) -> &'static [&'static str] {
77        match self {
78            Self::One => &[
79                "chord",
80                "altchord",
81                "measure",
82                "beat",
83                "totbeat",
84                "timesig",
85                "length",
86                "global_key",
87                "local_key",
88                "pedal",
89                "numeral",
90                "form",
91                "figbass",
92                "changes",
93                "relativeroot",
94                "phraseend",
95            ],
96            Self::Two => &[
97                "chord",
98                "mn",
99                "mn_onset",
100                "timesig",
101                "volta",
102                "globalkey",
103                "localkey",
104                "pedal",
105                "numeral",
106                "form",
107                "figbass",
108                "changes",
109                "relativeroot",
110                "phraseend",
111                "label",
112            ],
113        }
114    }
115
116    /// Every column the lab's standard has, in its own order, which is what
117    /// a table is written with: music21's `DCML_V1_HEADERS` and
118    /// `DCML_V2_HEADERS`.
119    pub fn columns(self) -> &'static [&'static str] {
120        match self {
121            Self::One => &[
122                "chord",
123                "altchord",
124                "measure",
125                "beat",
126                "totbeat",
127                "timesig",
128                "op",
129                "no",
130                "mov",
131                "length",
132                "global_key",
133                "local_key",
134                "pedal",
135                "numeral",
136                "form",
137                "figbass",
138                "changes",
139                "relativeroot",
140                "phraseend",
141            ],
142            Self::Two => &[
143                "mc",
144                "mn",
145                "mc_onset",
146                "mn_onset",
147                "timesig",
148                "staff",
149                "voice",
150                "volta",
151                "label",
152                "globalkey",
153                "localkey",
154                "pedal",
155                "chord",
156                "special",
157                "numeral",
158                "form",
159                "figbass",
160                "changes",
161                "relativeroot",
162                "cadence",
163                "phraseend",
164                "chord_type",
165                "globalkey_is_minor",
166                "localkey_is_minor",
167                "chord_tones",
168                "added_tones",
169                "root",
170                "bass_note",
171            ],
172        }
173    }
174}
175
176/// Which way a label is being read: music21's `direction`.
177#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub enum Direction {
179    /// From the lab's notation into music21's.
180    IntoMusic21,
181    /// From music21's into the lab's.
182    IntoDcml,
183}
184
185/// Whether a key is minor, which the lab says by writing it in lower case:
186/// music21's `isMinor`.
187///
188/// ```
189/// use music21_rs::romantext::tsv::is_minor;
190///
191/// assert!(!is_minor("F"));
192/// assert!(is_minor("f"));
193/// assert!(!is_minor("F#"));
194/// assert!(is_minor("bb"));
195/// ```
196pub fn is_minor(key: &str) -> bool {
197    key == key.to_lowercase()
198}
199
200/// The swaps between the two notations: music21's `characterSwaps`.
201///
202/// A half-diminished chord is `%` in the lab's tables and `ø` in music21's
203/// figures, and the lab says a seventh's quality where music21 does not.
204///
205/// ```
206/// use music21_rs::romantext::tsv::{Direction, character_swaps};
207///
208/// assert_eq!(character_swaps("ii%", Direction::IntoMusic21), "ii\u{f8}");
209/// assert_eq!(character_swaps("ii\u{f8}", Direction::IntoDcml), "ii%");
210/// assert_eq!(character_swaps("VM7", Direction::IntoMusic21), "V7");
211/// ```
212pub fn character_swaps(written: &str, direction: Direction) -> String {
213    match direction {
214        Direction::IntoDcml => written.replace("/o", "%").replace('ø', "%"),
215        Direction::IntoMusic21 => written.replace('%', "ø").replace("M7", "7"),
216    }
217}
218
219/// The local key in its own terms: music21's `getLocalKey`, which reads a
220/// degree against the global key and answers the key it names -- `V` of `G`
221/// is `D`, and `ii` of `C` is `d`.
222///
223/// With `from_dcml` the degree is read the lab's way first, where a minor
224/// key's degrees are written in upper case.
225///
226/// # Errors
227///
228/// A degree that names no major or minor triad.
229pub fn local_key(degree: &str, global_key: &str, from_dcml: bool) -> Result<String> {
230    let degree = if from_dcml {
231        character_swaps(degree, Direction::IntoMusic21)
232    } else {
233        degree.to_string()
234    };
235    let numeral = RomanNumeral::with_minor_defaults(
236        &degree,
237        Key::from_tonic(global_key)?,
238        Minor67Default::Flat,
239        Minor67Default::Flat,
240    )?;
241    let chord = numeral.to_chord()?;
242    let root = chord
243        .root()
244        .ok_or_else(|| Error::RomanText("a local key with no root".to_string()))?
245        .name();
246    // Only the letter takes the case, the accidental having none: a major
247    // key on E double flat is `Ebb` and a minor one `ebb`. music21 writes
248    // the flats `--`, which [`to_tsv`] writes back.
249    let cased = |upper: bool| -> String {
250        let mut letters = root.chars();
251        let step = letters.next().unwrap_or_default();
252        let step = if upper {
253            step.to_ascii_uppercase()
254        } else {
255            step.to_ascii_lowercase()
256        };
257        format!("{step}{}", letters.as_str())
258    };
259    if chord.is_major_triad() {
260        return Ok(cased(true));
261    }
262    if chord.is_minor_triad() {
263        return Ok(cased(false));
264    }
265    Err(Error::RomanText(
266        "local key must be major or minor".to_string(),
267    ))
268}
269
270/// The degree one key stands on in another: music21's `localKeyAsRn`, which
271/// is how a table says where the music has gone -- C major is the `VI` of e
272/// minor.
273///
274/// # Errors
275///
276/// A key whose own tonic triad names no degree of the other.
277pub fn local_key_as_roman(local: &Key, global: &Key) -> Result<String> {
278    let letter = local.tonic_pitch_name_with_case();
279    let figure = if letter.chars().next().is_some_and(char::is_lowercase) {
280        "i"
281    } else {
282        "I"
283    };
284    let tonic = RomanNumeral::new(figure, local.clone())?.to_chord()?;
285    let found = crate::roman::roman_numeral_from_chord(&tonic, Some(global))?.ok_or_else(|| {
286        Error::RomanText(format!(
287            "{letter} names no degree of {}",
288            global.tonic_pitch_name_with_case()
289        ))
290    })?;
291    Ok(found.roman_numeral().to_string())
292}
293
294/// The key a secondary degree establishes: music21's `getSecondaryKey`,
295/// which reads the part after the slash of a `V/vi` in the local key.
296///
297/// # Errors
298///
299/// A degree that names no major or minor triad.
300pub fn secondary_key(numeral: &str, local: &str) -> Result<String> {
301    match numeral.split_once('/') {
302        Some((_, secondary)) => local_key(secondary, local, false),
303        None => Ok(local.to_string()),
304    }
305}
306
307/// The `form` column for a numeral: music21's `getForm`, which reads it off
308/// the figure -- `%` for a half-diminished chord, `o` for a diminished one,
309/// `+M` for an augmented major seventh, `+` for an augmented chord and `M`
310/// for a major seventh.
311pub fn form_of(figure: &str) -> &'static str {
312    if figure.contains('ø') {
313        return "%";
314    }
315    if figure.contains('o') {
316        return "o";
317    }
318    if figure.contains("+M") {
319        return "+M";
320    }
321    if figure.contains('+') {
322        return "+";
323    }
324    if figure.contains('M') {
325        return "M";
326    }
327    ""
328}
329
330/// The figured bass the lab writes, which is music21's figure from its
331/// first digit on: `M43` is `43`, and a figure with no digit in it says
332/// nothing.
333pub fn figured_bass_of(figures_written: &str) -> &str {
334    match figures_written.find(|mark: char| mark.is_ascii_digit()) {
335        Some(at) => &figures_written[at..],
336        None => "",
337    }
338}
339
340/// An added tone as the lab writes one inside the parentheses of a label.
341#[derive(Clone, Copy, Debug, PartialEq, Eq)]
342struct AddedTone<'a> {
343    /// `+` to add without replacing, `-` to take a factor out.
344    added_or_removed: Option<char>,
345    /// `^` to replace the factor above, `v` the one below.
346    above_or_below: Option<char>,
347    /// The sharps or flats written in front of the figure.
348    alteration: &'a str,
349    /// The figure itself, nought to nineteen.
350    factor: &'a str,
351}
352
353/// The added tones written in a label's parentheses, as music21's own
354/// pattern finds them: each an optional `+` or `-`, an optional `^` or `v`,
355/// any number of sharps or flats, and a figure of one or two digits.
356fn added_tones(written: &str) -> Vec<AddedTone<'_>> {
357    let bytes = written.as_bytes();
358    let mut found = Vec::new();
359    let mut at = 0;
360    while at < bytes.len() {
361        let start = at;
362        let mut cursor = at;
363        let added_or_removed = match bytes.get(cursor) {
364            Some(mark @ (b'+' | b'-')) => {
365                cursor += 1;
366                Some(*mark as char)
367            }
368            _ => None,
369        };
370        let above_or_below = match bytes.get(cursor) {
371            Some(mark @ (b'^' | b'v')) => {
372                cursor += 1;
373                Some(*mark as char)
374            }
375            _ => None,
376        };
377        let alteration_start = cursor;
378        if let Some(mark @ (b'#' | b'b')) = bytes.get(cursor) {
379            while bytes.get(cursor) == Some(mark) {
380                cursor += 1;
381            }
382        }
383        let alteration = &written[alteration_start..cursor];
384        // One digit, or two where the first is a one: music21's `1\d|\d`.
385        let digits = match (bytes.get(cursor), bytes.get(cursor + 1)) {
386            (Some(b'1'), Some(second)) if second.is_ascii_digit() => 2,
387            (Some(first), _) if first.is_ascii_digit() => 1,
388            _ => 0,
389        };
390        if digits == 0 {
391            // Nothing here is a tone; music21's own scan moves on a
392            // character and tries again.
393            at = start + 1;
394            continue;
395        }
396        found.push(AddedTone {
397            added_or_removed,
398            above_or_below,
399            alteration,
400            factor: &written[cursor..cursor + digits],
401        });
402        at = cursor + digits;
403    }
404    found
405}
406
407/// A label's parentheses turned into music21's brackets: music21's
408/// `handleAddedTones`.
409///
410/// The lab writes the tones a chord adds in parentheses and leaves what
411/// they replace to be worked out; music21 writes both, so a `V(6)` becomes
412/// `V[no5][add6]`. A tone over a chord already seven deep replaces nothing,
413/// a `+` says to add without replacing, a `-` says to take a factor out,
414/// and `^` or `v` says which neighbour a step replaces. A `V(64)` is the
415/// cadential six-four, which music21 has a figure of its own for.
416///
417/// ```
418/// use music21_rs::romantext::tsv::handle_added_tones;
419///
420/// assert_eq!(handle_added_tones("V(64)"), "Cad64");
421/// assert_eq!(handle_added_tones("i(4+2)"), "i[no3][add4][add2]");
422/// assert_eq!(handle_added_tones("V"), "V");
423/// ```
424pub fn handle_added_tones(label: &str) -> String {
425    // music21's pattern: everything up to the first parenthesis, the tones
426    // inside it, and a secondary degree after it.
427    let Some(open) = label.find('(') else {
428        return label.to_string();
429    };
430    let primary = &label[..open];
431    let rest = &label[open + 1..];
432    // The last parenthesis that leaves either nothing or a secondary
433    // degree behind it, since music21's `.*` takes as much as it can.
434    let Some(close) = rest.rfind(')').filter(|close| {
435        let after = &rest[close + 1..];
436        after.is_empty() || after.starts_with('/')
437    }) else {
438        return label.to_string();
439    };
440    let inside = &rest[..close];
441    let secondary = &rest[close + 1..];
442    if primary == "V" && inside == "64" {
443        return format!("Cad64{secondary}");
444    }
445    // The figure the chord already carries, which says how deep it is: the
446    // digits and slashes the primary ends with.
447    let figure_at = primary
448        .rfind(|mark: char| !mark.is_ascii_digit() && mark != '/')
449        .map_or(0, |at| {
450            at + primary[at..].chars().next().map_or(1, char::len_utf8)
451        });
452    let figure = &primary[figure_at..];
453    let threshold = match figure {
454        "" | "5" | "53" | "5/3" | "3" | "7" => 7,
455        _ => 8,
456    };
457    let mut omissions = String::new();
458    let mut additions = String::new();
459    for tone in added_tones(inside) {
460        if tone.added_or_removed == Some('-') {
461            omissions.push_str(&format!("[no{}]", tone.factor));
462            continue;
463        }
464        let factor: i32 = tone.factor.parse().unwrap_or(0);
465        let replace_above = if tone.added_or_removed == Some('+')
466            || factor >= threshold
467            || matches!(factor, 1 | 3 | 5)
468        {
469            // A tone the chord does not reach, one written `+` to say it
470            // replaces nothing, and a factor the chord already has all
471            // replace nothing.
472            None
473        } else if matches!(factor, 2 | 4 | 6) {
474            // A second, a fourth or a sixth replaces the note below it
475            // unless it is sharpened or marked as replacing the one above.
476            Some(tone.alteration == "#" || tone.above_or_below == Some('^'))
477        } else {
478            Some(tone.alteration != "b" && tone.above_or_below != Some('v'))
479        };
480        if let Some(above) = replace_above {
481            let replaced = if above { factor + 1 } else { factor - 1 };
482            omissions.push_str(&format!("[no{replaced}]"));
483        }
484        // A nought says the seventh replaces the root.
485        let factor = if factor == 0 { 7 } else { factor };
486        additions.push_str(&format!("[add{}{factor}]", tone.alteration));
487    }
488    format!("{primary}{omissions}{additions}{secondary}")
489}
490
491/// Whether a local key is written as a degree rather than as a key: music21
492/// asks whether it holds a roman numeral, which any `i` or `v` in it is.
493fn is_degree(local_key: &str) -> bool {
494    local_key
495        .chars()
496        .any(|mark| matches!(mark.to_ascii_lowercase(), 'i' | 'v'))
497}
498
499/// Every digit of a label prefixed with `d`, as music21 prefixes the
500/// figures of a major-minor seventh on a degree other than the fifth: the
501/// lab writes `II7` where music21 writes `IId7`.
502fn prefix_sevenths(label: &str) -> String {
503    let bytes = label.as_bytes();
504    let mut out = String::with_capacity(label.len() + 2);
505    let mut at = 0;
506    while at < bytes.len() {
507        if !bytes[at].is_ascii_digit() {
508            out.push(bytes[at] as char);
509            at += 1;
510            continue;
511        }
512        let start = at;
513        while bytes.get(at).is_some_and(u8::is_ascii_digit) {
514            at += 1;
515        }
516        // music21 leaves a run alone where a digit or a closing bracket
517        // follows it, so that an `[add13]` does not become `[addd13]`.
518        if bytes.get(at) == Some(&b']') {
519            out.push_str(&label[start..at]);
520        } else {
521            out.push('d');
522            out.push_str(&label[start..at]);
523        }
524    }
525    out
526}
527
528/// A chord of no harmony at all, which the lab writes `@none`: music21's
529/// `NoChord`.
530pub(crate) fn no_chord() -> ChordSymbol {
531    ChordSymbol::no_chord(None)
532}
533
534/// The length a beat stands at, in quarters from the start of its measure:
535/// the lab counts beats from one, in quarter notes.
536pub(crate) fn offset_in_measure(beat: FloatType) -> FloatType {
537    beat - 1.0
538}
539
540/// One row of a DCML table, read: music21's `TabChord` and `TabChordV2`.
541///
542/// The columns of the two standards differ in their names and in what they
543/// carry, and a row of either is read into this one type -- what the lab's
544/// version 1 calls `measure` and `beat` its version 2 calls `mn` and
545/// `mn_onset`, and counts the onset in whole notes rather than in beats.
546/// Every column the standard does not name is kept beside the row, as
547/// music21 keeps its `extra`.
548#[derive(Clone, Debug, Default, PartialEq)]
549#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
550pub struct TabChord {
551    /// Which standard the row was read in.
552    pub version: Version,
553    /// The label the annotator wrote: the lab's `chord`.
554    pub chord: String,
555    /// The degree alone.
556    pub numeral: String,
557    /// The degree the chord is read in, written after a slash.
558    pub relativeroot: Option<String>,
559    /// `o`, `+`, `%` or `M`: how the chord is built over the degree.
560    pub form: Option<String>,
561    /// The figured bass under it.
562    pub figbass: Option<String>,
563    /// The tones it adds or leaves out.
564    pub changes: Option<String>,
565    /// Whether a phrase ends here.
566    pub phraseend: Option<String>,
567    /// The note held under it.
568    pub pedal: Option<String>,
569    /// The key in force, as a degree of the global key or as a key.
570    pub local_key: String,
571    /// The key the piece is in.
572    pub global_key: String,
573    /// The beat of the measure the chord falls on, counted from one.
574    pub beat: FloatType,
575    /// The measure it falls in.
576    pub measure: i32,
577    /// The meter in force, as a ratio.
578    pub timesig: String,
579    /// Which time through a repeat the row belongs to, where the lab's
580    /// version 2 says.
581    pub volta: Option<String>,
582    /// How long the chord sounds, where the lab's version 1 says.
583    pub length: Option<FloatType>,
584    /// Every other column of the row, by name.
585    pub extra: Vec<(String, String)>,
586}
587
588impl TabChord {
589    /// A row read against a table's own header: music21's
590    /// `populateFromRow`, which reads the columns its standard names and
591    /// keeps the rest beside them.
592    pub fn from_row(header: &[String], row: &[String], version: Version) -> Result<Self> {
593        let mut read = Self {
594            version,
595            ..Self::default()
596        };
597        let read_columns = version.read_columns();
598        for (index, name) in header.iter().enumerate() {
599            let Some(cell) = row.get(index) else {
600                continue;
601            };
602            if !read_columns.contains(&name.as_str()) {
603                if !cell.is_empty() {
604                    read.extra.push((name.clone(), cell.clone()));
605                }
606                continue;
607            }
608            let text = cell.clone();
609            match name.as_str() {
610                "chord" => read.chord = text,
611                "numeral" => read.numeral = text,
612                "relativeroot" => read.relativeroot = Some(text),
613                "form" => read.form = Some(text),
614                "figbass" => read.figbass = Some(text),
615                "changes" => read.changes = Some(text),
616                "phraseend" => read.phraseend = Some(text),
617                "pedal" => read.pedal = Some(text),
618                "timesig" => read.timesig = text,
619                "volta" => read.volta = Some(text),
620                "local_key" | "localkey" => read.local_key = text,
621                "global_key" | "globalkey" => read.global_key = text,
622                "measure" | "mn" => {
623                    read.measure = text.parse().map_err(|_| {
624                        Error::RomanText(format!("{name} is not a measure number: {text}"))
625                    })?;
626                }
627                "beat" => {
628                    read.beat = text
629                        .parse()
630                        .map_err(|_| Error::RomanText(format!("beat is not a number: {text}")))?;
631                }
632                // The lab's version 2 counts the onset in whole notes from
633                // the start of the measure, sometimes as a fraction.
634                "mn_onset" => read.beat = float_or_fraction(&text)? * 4.0 + 1.0,
635                // music21 reads the column with `float`, which refuses a
636                // cell saying nothing rather than reading it as no length.
637                "length" => {
638                    read.length = Some(text.parse().map_err(|_| {
639                        Error::RomanText(format!("length is not a number: {text}"))
640                    })?);
641                }
642                // `altchord` and `totbeat` music21 reads onto attributes of
643                // the row that nothing asks again, and `label` onto no
644                // attribute at all, so none of the three is kept beside the
645                // row -- which is what the writer reads for the columns it
646                // cannot work out itself.
647                _ => {}
648            }
649        }
650        Ok(read)
651    }
652
653    /// The label as music21 writes one, which is what a
654    /// [`RomanNumeral`](crate::roman::RomanNumeral) is built from: music21's
655    /// `_changeRepresentation` and the first half of its `tabToM21`.
656    fn music21_label(&self) -> String {
657        // music21's `_changeRepresentation` branches on whether the local
658        // key and the degree it is read in are minor, but every branch
659        // calls one function whose `minor` argument it never reads, so the
660        // swaps are the same either way.
661        let numeral = character_swaps(&self.numeral, Direction::IntoMusic21);
662        let relativeroot = self
663            .relativeroot
664            .as_deref()
665            .filter(|root| !root.is_empty())
666            .map(|root| character_swaps(root, Direction::IntoMusic21));
667        let form = self
668            .form
669            .as_deref()
670            .map(|form| form.replace('%', "ø"))
671            .filter(|form| !form.is_empty());
672        if self.version == Version::Two && !self.chord.is_empty() {
673            let mut label = handle_added_tones(&self.chord.replace('%', "ø"));
674            // A major-minor seventh away from the fifth degree says so in
675            // music21's figure and not in the lab's.
676            if self
677                .extra
678                .iter()
679                .any(|(name, value)| name == "chord_type" && value == "Mm7")
680                && self.numeral != "V"
681            {
682                label = prefix_sevenths(&label);
683            }
684            return label;
685        }
686        let mut label = format!(
687            "{numeral}{}{}",
688            form.unwrap_or_default(),
689            self.figbass.clone().unwrap_or_default()
690        );
691        if let Some(root) = relativeroot.filter(|root| !root.is_empty()) {
692            label.push('/');
693            label.push_str(&root);
694        }
695        label
696    }
697
698    /// The key the chord is read in: the local key as a key of its own,
699    /// where the table wrote it as a degree of the global key.
700    ///
701    /// # Errors
702    ///
703    /// A local key that names no major or minor triad of the global one.
704    pub fn key(&self) -> Result<Key> {
705        let local = character_swaps(&self.local_key, Direction::IntoMusic21);
706        let named = if is_degree(&local) {
707            local_key(&local, &self.global_key, false)?
708        } else {
709            local
710        };
711        Key::from_tonic(&named)
712    }
713
714    /// The chord the row stands for: a numeral in the key in force, or the
715    /// chord of no harmony the lab writes `@none`.
716    ///
717    /// The sixth and the seventh of a minor key are read flat, as music21
718    /// reads a table's.
719    ///
720    /// # Errors
721    ///
722    /// A label or a key the crate cannot read.
723    pub fn element(&self) -> Result<StreamElement> {
724        if self.numeral.is_empty() || self.numeral == "@none" {
725            return Ok(no_chord().into());
726        }
727        let numeral = RomanNumeral::with_minor_defaults(
728            self.music21_label(),
729            self.key()?,
730            Minor67Default::Flat,
731            Minor67Default::Flat,
732        )?;
733        let chord = numeral
734            .to_chord()?
735            .with_duration(Duration::new(0.0)?)
736            .with_numeral(numeral);
737        Ok(chord.into())
738    }
739}
740
741/// A number written plainly or as a fraction: music21's `_float_or_frac`,
742/// which the lab's version 2 needs for an onset of `1/2`.
743fn float_or_fraction(text: &str) -> Result<FloatType> {
744    if let Ok(number) = text.parse::<FloatType>() {
745        return Ok(number);
746    }
747    let (numerator, denominator) = text
748        .split_once('/')
749        .ok_or_else(|| Error::RomanText(format!("not a number: {text}")))?;
750    let numerator: FloatType = numerator
751        .parse()
752        .map_err(|_| Error::RomanText(format!("not a number: {text}")))?;
753    let denominator: FloatType = denominator
754        .parse()
755        .map_err(|_| Error::RomanText(format!("not a number: {text}")))?;
756    if denominator == 0.0 {
757        return Err(Error::RomanText(format!("not a number: {text}")));
758    }
759    Ok(numerator / denominator)
760}
761
762/// The rows of a table, each read: music21's `TsvHandler.tsvToChords`.
763///
764/// # Errors
765///
766/// A table with no header, or a cell the standard cannot read.
767pub fn tsv_to_chords(text: &str, version: Version) -> Result<Vec<TabChord>> {
768    let mut lines = text.lines().filter(|line| !line.trim().is_empty());
769    let header: Vec<String> = match lines.next() {
770        Some(line) => cells(line),
771        None => return Err(Error::RomanText("a table with no header".to_string())),
772    };
773    lines
774        .map(|line| TabChord::from_row(&header, &cells(line), version))
775        .collect()
776}
777
778/// A line of a table, its cells unquoted as a reader of tab-separated
779/// values unquotes them.
780fn cells(line: &str) -> Vec<String> {
781    line.split('\t')
782        .map(|cell| {
783            let cell = cell.trim_end_matches('\r');
784            match cell
785                .strip_prefix('"')
786                .and_then(|rest| rest.strip_suffix('"'))
787            {
788                Some(inside) => inside.replace("\"\"", "\""),
789                None => cell.to_string(),
790            }
791        })
792        .collect()
793}
794
795/// A DCML table read as a score of one part: music21's
796/// `TsvHandler.toM21Stream`, which prepares the measures from the table's
797/// own measure numbers and meters and then puts each chord in the measure
798/// it falls in.
799///
800/// The score is the analysis alone -- the numerals and the keys, with no
801/// notes of the music they describe -- and each numeral lasts until the
802/// next, the last to the end of its measure, which is music21's
803/// `extendDuration`.
804///
805/// ```
806/// use music21_rs::romantext::tsv::{Version, from_tsv};
807///
808/// let table = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n\
809///              .C.I\t1\t1.0\t4/4\tC\tI\tI\n\
810///              V\t2\t1.0\t4/4\tC\tI\tV\n";
811/// let read = from_tsv(table, Version::One)?;
812/// assert_eq!(read.parts().len(), 1);
813/// # Ok::<(), music21_rs::Error>(())
814/// ```
815///
816/// # Errors
817///
818/// A table with no rows, a meter or a key it cannot read, or a label no
819/// numeral can be built from.
820pub fn from_tsv(text: &str, version: Version) -> Result<Stream> {
821    let rows = tsv_to_chords(text, version)?;
822    let Some(first) = rows.first() else {
823        return Err(Error::RomanText("a table with no rows".to_string()));
824    };
825    let mut part = Stream::with_kind(StreamKind::Part);
826    let key = Key::from_tonic(&character_swaps(&first.global_key, Direction::IntoMusic21))?;
827    let mut meter = TimeSignature::from_ratio_string(&first.timesig)?;
828    let mut said_timesig = first.timesig.clone();
829    let mut measure_length = meter.bar_duration().quarter_length();
830    let mut offset = 0.0;
831    let mut previous_measure = first.measure - 1;
832    let mut previous_volta = String::new();
833    // Where each measure of the part stands and what it is numbered, so
834    // that a chord can be put in the measure it falls in.
835    let mut measures: Vec<(String, FloatType, Stream)> = Vec::new();
836    let new_measure = |number: i32, volta: Option<&str>, offset: FloatType| {
837        let mut measure = Stream::with_kind(StreamKind::Measure);
838        measure.set_number(number);
839        let suffix = volta.and_then(volta_suffix);
840        if let Some(suffix) = &suffix {
841            measure.set_number_suffix(Some(suffix.clone()));
842        }
843        let name = format!("{number}{}", suffix.unwrap_or_default());
844        (name, offset, measure)
845    };
846    for row in &rows {
847        let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
848        if version == Version::Two && row.volta.as_deref().unwrap_or("") != previous_volta {
849            previous_volta = row.volta.clone().unwrap_or_default();
850        } else if row.measure == previous_measure {
851            // Only a change of measure, or of the time through a repeat,
852            // makes a measure.
853            continue;
854        }
855        if row.measure > previous_measure + 1 {
856            // Not every measure has a chord of its own.
857            for number in (previous_measure + 1)..=row.measure {
858                offset += measure_length;
859                measures.push(new_measure(number, None, offset));
860                previous_measure = number;
861            }
862            continue;
863        }
864        offset += measure_length;
865        let (name, at, mut measure) = new_measure(row.measure, volta, offset);
866        // A measure played one time through a repeat carries the bracket
867        // drawn over it: music21 keeps a `RepeatBracket` spanning the
868        // measures of each ending, and a measure here says which ending it
869        // is in and that the bracket opens and closes on it, there being
870        // one measure to an ending in a table.
871        if let Some(volta) = volta.and_then(|volta| volta.parse::<u32>().ok()) {
872            measure.set_ending(Some(Ending::new(vec![volta], true, true)));
873        }
874        if row.timesig != said_timesig {
875            let said = TimeSignature::from_ratio_string(&row.timesig)?;
876            measure.insert(offset_in_measure(row.beat), said.clone());
877            said_timesig = row.timesig.clone();
878            measure_length = said.bar_duration().quarter_length();
879            meter = said;
880        }
881        measures.push((name, at, measure));
882        previous_measure = row.measure;
883    }
884    let _ = &meter;
885    // The key and the meter stand at the start of the first measure.
886    if let Some((_, _, measure)) = measures.first_mut() {
887        measure.insert(0.0, key);
888        measure.insert(0.0, TimeSignature::from_ratio_string(&first.timesig)?);
889    }
890    for row in &rows {
891        let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
892        let name = format!(
893            "{}{}",
894            row.measure,
895            volta.and_then(volta_suffix).unwrap_or_default()
896        );
897        let Some((_, _, measure)) = measures.iter_mut().find(|(known, _, _)| *known == name) else {
898            return Err(Error::RomanText(format!("no measure {name} to analyse")));
899        };
900        measure.insert(offset_in_measure(row.beat), row.element()?);
901    }
902    for (_, at, measure) in measures {
903        part.insert(at, measure);
904    }
905    lengthen_harmonies(&mut part)?;
906    let mut score = Stream::with_kind(StreamKind::Score);
907    score.insert(0.0, part);
908    Ok(score)
909}
910
911/// The suffix a volta writes on a measure number: the first time through is
912/// `a`, the second `b`, as music21 letters them.
913fn volta_suffix(volta: &str) -> Option<String> {
914    let number: usize = volta.parse().ok()?;
915    let letter = char::from(b'a' + u8::try_from(number.checked_sub(1)?).ok()?);
916    letter.is_ascii_lowercase().then(|| letter.to_string())
917}
918
919/// Every harmony lasting until the next, and the last to the end of the
920/// part: music21's `extendDuration` over `harmony.Harmony`, and the length
921/// it writes onto the last chord itself afterwards.
922///
923/// music21 extends the durations over the part *flattened*, so a chord
924/// lasts across the barline into the next measure's chord, and then reads
925/// the part's length again -- which the extending has just changed -- to
926/// give the last chord the rest of the piece. Both steps are kept, in that
927/// order, since the second reads what the first wrote.
928fn lengthen_harmonies(part: &mut Stream) -> Result<()> {
929    // Where each harmony stands in the part, measure by measure.
930    let places = harmony_places(part);
931    let total = part.end_offset();
932    for (index, (offset, measure, event)) in places.iter().enumerate() {
933        let until = places.get(index + 1).map_or(total, |(next, _, _)| *next);
934        set_length(part, *measure, *event, op_frac(until - offset))?;
935    }
936    // The part is longer now than it was, and music21 gives the last chord
937    // what is left of it.
938    if let Some((offset, measure, event)) = places.last() {
939        let total = part.end_offset();
940        set_length(part, *measure, *event, op_frac(total - offset))?;
941    }
942    Ok(())
943}
944
945/// Every harmony of a part: where it stands in the part, which measure
946/// holds it, and which of that measure's events it is.
947fn harmony_places(part: &Stream) -> Vec<(FloatType, usize, usize)> {
948    let mut places = Vec::new();
949    for (measure, event) in part.events().iter().enumerate() {
950        let Some(inner) = event.element().as_stream() else {
951            continue;
952        };
953        for (index, held) in inner.events().iter().enumerate() {
954            if held.element().is_instance("Harmony") {
955                places.push((op_frac(event.offset() + held.offset()), measure, index));
956            }
957        }
958    }
959    places.sort_by(|left, right| left.0.total_cmp(&right.0));
960    places
961}
962
963/// Writes one held element's length.
964fn set_length(
965    part: &mut Stream,
966    measure: usize,
967    event: usize,
968    quarter_length: FloatType,
969) -> Result<()> {
970    let Some(StreamElement::Stream(inner)) = part
971        .events_mut()
972        .get_mut(measure)
973        .map(StreamEvent::element_mut)
974    else {
975        return Ok(());
976    };
977    let Some(held) = inner.events_mut().get_mut(event) else {
978        return Ok(());
979    };
980    let duration = Duration::new(quarter_length.max(0.0))?;
981    match held.element_mut() {
982        StreamElement::Chord(chord) => chord.set_duration(duration),
983        StreamElement::ChordSymbol(symbol) => symbol.set_duration(duration),
984        _ => {}
985    }
986    Ok(())
987}
988
989/// A score written as one of the lab's tables: music21's `M21toTSV`, which
990/// writes a row for every numeral of the score, in the order a walk of it
991/// meets them.
992///
993/// Every column the standard names is written, in the standard's own order,
994/// and what the standard has no answer for is written empty, as music21
995/// writes it: where a phrase ends, the note held under a chord, the tones a
996/// chord changes. The columns music21 fills from a numeral's `editorial` --
997/// the cells its reader did not read and kept beside the row -- are empty
998/// here too; [`to_tsv_with_extra`] is how a caller hands them back.
999///
1000/// ```
1001/// use music21_rs::romantext::tsv::{Version, from_tsv, to_tsv};
1002///
1003/// let table = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n\
1004///              I\t1\t1.0\t4/4\tC\tC\tI\n";
1005/// let written = to_tsv(&from_tsv(table, Version::One)?, Version::One)?;
1006/// let mut lines = written.lines();
1007/// assert!(lines.next().is_some_and(|header| header.starts_with("chord\t")));
1008/// assert!(lines.next().is_some_and(|row| row.starts_with("I\t\t1\t1.0")));
1009/// # Ok::<(), music21_rs::Error>(())
1010/// ```
1011///
1012/// # Errors
1013///
1014/// A version 1 table with no row at all -- music21 reads the global key off
1015/// the first numeral and asks its walk for one -- or a numeral standing
1016/// where no meter is in force, which has no beat.
1017pub fn to_tsv(stream: &Stream, version: Version) -> Result<String> {
1018    to_tsv_with_extra(stream, version, &[])
1019}
1020
1021/// A score written as a table, with the cells music21 keeps on each
1022/// numeral's `editorial` given back: one list per chord the walk meets, in
1023/// that order, which for a score [`from_tsv`] read is the order of the
1024/// table's own rows.
1025///
1026/// music21's reader writes every column it does not read onto the numeral
1027/// it made, and its writer reads those cells back for the columns it cannot
1028/// work out itself -- which is how a table's `mc`, `staff`, `chord_tones`
1029/// and the rest survive a round trip. A chord here carries nothing of the
1030/// kind, so they are the caller's to keep: [`TabChord::extra`] is what
1031/// [`tsv_to_chords`] reads them into.
1032///
1033/// # Errors
1034///
1035/// As [`to_tsv`].
1036pub fn to_tsv_with_extra(
1037    stream: &Stream,
1038    version: Version,
1039    extra: &[Vec<(String, String)>],
1040) -> Result<String> {
1041    let written = written_chords(stream, version);
1042    let mut text = String::new();
1043    write_row(
1044        &mut text,
1045        version.columns().iter().map(|&name| name.to_string()),
1046    );
1047    // The global key is the key the first numeral is read in.
1048    let Some(global) = written.iter().find_map(|written| written.numeral) else {
1049        if version == Version::One {
1050            // music21 asks its walk for the next numeral and is answered
1051            // with a raise.
1052            return Err(Error::RomanText(
1053                "a score with no roman numeral to write".to_string(),
1054            ));
1055        }
1056        return Ok(text);
1057    };
1058    let global_key = global.key().clone();
1059    let global_name = music21_key_name(&global_key);
1060    for (index, written) in written.iter().enumerate() {
1061        let attributes = match version {
1062            Version::One => v1_attributes(written, &global_name)?,
1063            Version::Two => v2_attributes(written, &global_name, &global_key)?,
1064        };
1065        let kept = extra.get(index).map(Vec::as_slice).unwrap_or_default();
1066        let cells = version.columns().iter().map(|name| {
1067            // music21 reads a column off the row it built where that row
1068            // has an attribute of the name, and off the numeral's editorial
1069            // where it has none.
1070            cell_of(&attributes, name)
1071                .or_else(|| cell_of(kept, name))
1072                .unwrap_or_default()
1073        });
1074        write_row(&mut text, cells);
1075    }
1076    Ok(text)
1077}
1078
1079/// One cell of a list of them, by column name.
1080fn cell_of<Name: AsRef<str>>(cells: &[(Name, String)], name: &str) -> Option<String> {
1081    cells
1082        .iter()
1083        .find(|(known, _)| known.as_ref() == name)
1084        .map(|(_, value)| value.clone())
1085}
1086
1087/// The columns music21's `TabChord` holds for a numeral: the first half of
1088/// its `_m21ToTsv_v1`.
1089fn v1_attributes(written: &Written<'_>, global_key: &str) -> Result<Vec<(&'static str, String)>> {
1090    let numeral = written
1091        .numeral
1092        .ok_or_else(|| Error::RomanText("a chord of no harmony has no row here".to_string()))?;
1093    // music21 writes the degree again as the `altchord` where the numeral
1094    // standing for it was built in the chord's own key, which a secondary
1095    // numeral always is.
1096    let relativeroot = numeral.secondary().unwrap_or_default().to_string();
1097    Ok(vec![
1098        ("chord", numeral.figure().to_string()),
1099        ("altchord", relativeroot.clone()),
1100        ("measure", written.measure.unwrap_or(1).to_string()),
1101        ("beat", python_float(written.beat()?)),
1102        ("totbeat", String::new()),
1103        ("timesig", written.timesig()),
1104        ("length", python_number(written.length)),
1105        ("global_key", global_key.to_string()),
1106        ("local_key", music21_key_name(numeral.key())),
1107        ("pedal", String::new()),
1108        ("numeral", numeral.roman_numeral()),
1109        ("form", form_of(numeral.figure()).to_string()),
1110        (
1111            "figbass",
1112            figured_bass_of(numeral.figures_written()).to_string(),
1113        ),
1114        ("changes", String::new()),
1115        ("relativeroot", relativeroot),
1116        ("phraseend", String::new()),
1117    ])
1118}
1119
1120/// The columns music21's `TabChordV2` holds: the first half of its
1121/// `_m21ToTsv_v2`, where the label is the whole figure and the chord of no
1122/// harmony is written `@none`.
1123fn v2_attributes(
1124    written: &Written<'_>,
1125    global_key: &str,
1126    global: &Key,
1127) -> Result<Vec<(&'static str, String)>> {
1128    let mut local_key = String::new();
1129    let mut form = String::new();
1130    let mut figbass = String::new();
1131    let mut relativeroot = String::new();
1132    let (chord, numeral_text) = match written.numeral {
1133        None => ("@none".to_string(), "@none".to_string()),
1134        Some(numeral) => {
1135            local_key = local_key_as_roman(numeral.key(), global)?;
1136            if let Some(root) = numeral.secondary() {
1137                relativeroot = character_swaps(root, Direction::IntoDcml);
1138            }
1139            form = form_of(numeral.figure()).to_string();
1140            figbass = figured_bass_of(numeral.figures_written()).to_string();
1141            (
1142                // The lab does not write the `d` music21 puts in front of
1143                // the figures of a major-minor seventh away from the fifth
1144                // degree.
1145                numeral.figure().replacen('d', "", 1),
1146                numeral.roman_numeral(),
1147            )
1148        }
1149    };
1150    Ok(vec![
1151        (
1152            "mn",
1153            written
1154                .measure
1155                .map(|number| number.to_string())
1156                .unwrap_or_default(),
1157        ),
1158        // The lab's version 2 counts the onset in whole notes from the
1159        // start of the measure, which music21 reads off the offset rather
1160        // than off the beat.
1161        ("mn_onset", python_number(written.offset / 4.0)),
1162        ("timesig", written.timesig()),
1163        ("volta", String::new()),
1164        ("globalkey", global_key.to_string()),
1165        ("localkey", local_key),
1166        ("pedal", String::new()),
1167        ("chord", chord),
1168        ("numeral", numeral_text),
1169        ("form", form),
1170        ("figbass", figbass),
1171        ("changes", String::new()),
1172        ("relativeroot", relativeroot),
1173        ("phraseend", String::new()),
1174    ])
1175}
1176
1177/// One chord a table has a row for: the numeral it stands for, or none for
1178/// the chord of no harmony, and where in the score it stands.
1179#[derive(Debug)]
1180struct Written<'a> {
1181    numeral: Option<&'a RomanNumeral>,
1182    measure: Option<IntegerType>,
1183    offset: FloatType,
1184    padding: FloatType,
1185    length: FloatType,
1186    meter: Option<TimeSignature>,
1187}
1188
1189impl Written<'_> {
1190    /// The meter in force as the lab writes it, and nothing where none is.
1191    fn timesig(&self) -> String {
1192        self.meter
1193            .as_ref()
1194            .map(TimeSignature::ratio_string)
1195            .unwrap_or_default()
1196    }
1197
1198    /// The beat of the measure the chord falls on, counted from one and
1199    /// taking the measure's padding in, as music21's `beat` counts it.
1200    ///
1201    /// # Errors
1202    ///
1203    /// A chord standing where no meter is in force, which music21's `beat`
1204    /// raises for.
1205    fn beat(&self) -> Result<FloatType> {
1206        let meter = self.meter.as_ref().ok_or_else(|| {
1207            Error::RomanText("a chord with no meter in force does not have a beat".to_string())
1208        })?;
1209        meter.beat_proportion(op_frac(self.offset + self.padding))
1210    }
1211}
1212
1213/// Every chord a table has a row for, in the order music21's `recurse`
1214/// meets them: a chord standing for a numeral, and under the lab's version
1215/// 2 the chord of no harmony beside it, which its version 1 writes no row
1216/// for.
1217fn written_chords(stream: &Stream, version: Version) -> Vec<Written<'_>> {
1218    let mut found = Vec::new();
1219    let mut meter = None;
1220    read_chords(stream, version, None, &mut meter, &mut found);
1221    found
1222}
1223
1224/// The walk itself, which carries the meter in force and the measure being
1225/// read down into every stream it meets.
1226fn read_chords<'a>(
1227    stream: &'a Stream,
1228    version: Version,
1229    measure: Option<(IntegerType, FloatType)>,
1230    meter: &mut Option<TimeSignature>,
1231    found: &mut Vec<Written<'a>>,
1232) {
1233    for event in stream.events() {
1234        match event.element() {
1235            // A meter sorts before a chord standing at its own offset, so
1236            // the one in force is the last met.
1237            StreamElement::TimeSignature(said) => *meter = Some(said.clone()),
1238            StreamElement::Stream(inner) => {
1239                let inside = if inner.kind() == StreamKind::Measure {
1240                    Some((inner.number(), inner.padding_left()))
1241                } else {
1242                    measure
1243                };
1244                read_chords(inner, version, inside, meter, found);
1245            }
1246            element => {
1247                let numeral = match element {
1248                    StreamElement::Chord(chord) => chord.numeral(),
1249                    _ => None,
1250                };
1251                let no_chord = matches!(
1252                    element,
1253                    StreamElement::ChordSymbol(symbol) if symbol.is_no_chord()
1254                );
1255                if numeral.is_none() && !(version == Version::Two && no_chord) {
1256                    continue;
1257                }
1258                found.push(Written {
1259                    numeral,
1260                    measure: measure.map(|(number, _)| number),
1261                    offset: event.offset(),
1262                    padding: measure.map_or(0.0, |(_, padding)| padding),
1263                    length: element.quarter_length(),
1264                    meter: meter.clone(),
1265                });
1266            }
1267        }
1268    }
1269}
1270
1271/// A row as Python's `csv.writer` writes one: the cells joined by tabs,
1272/// each quoted only where it has to be, and the line ended as that writer
1273/// ends it.
1274fn write_row(text: &mut String, cells: impl Iterator<Item = String>) {
1275    let mut first = true;
1276    for cell in cells {
1277        if !first {
1278            text.push('\t');
1279        }
1280        first = false;
1281        if cell.contains(['\t', '"', '\r', '\n']) {
1282            text.push('"');
1283            text.push_str(&cell.replace('"', "\"\""));
1284            text.push('"');
1285        } else {
1286            text.push_str(&cell);
1287        }
1288    }
1289    text.push_str("\r\n");
1290}
1291
1292/// A length or an onset as Python prints the number music21 holds it as: a
1293/// fraction where `opFrac` could not keep it a float, and otherwise a
1294/// float, which always shows a decimal point.
1295fn python_number(value: FloatType) -> String {
1296    if is_fraction(value)
1297        && let Some((numerator, denominator)) = limited_fraction(value, DENOMINATOR_LIMIT)
1298    {
1299        return format!("{numerator}/{denominator}");
1300    }
1301    python_float(value)
1302}
1303
1304/// Whether music21 holds a number as a `Fraction` rather than as a float:
1305/// `opFrac` keeps a float only where its denominator is a power of two no
1306/// larger than the limit.
1307fn is_fraction(value: FloatType) -> bool {
1308    (value * (DENOMINATOR_LIMIT as FloatType + 1.0)).fract() != 0.0
1309}
1310
1311/// The largest denominator `opFrac` keeps.
1312const DENOMINATOR_LIMIT: i128 = 65535;
1313
1314#[cfg(test)]
1315mod tests {
1316    use super::*;
1317
1318    /// A cell the standard reads as a number and finds nothing in: music21
1319    /// coerces those columns with `int` and `float`, which refuse a cell
1320    /// saying nothing, so a table with one is read by neither side.
1321    #[test]
1322    fn a_number_a_table_leaves_out_is_refused() {
1323        let header = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\tlength\n";
1324        let whole = format!("{header}I\t1\t1.0\t4/4\tC\tC\tI\t4.0\n");
1325        assert!(tsv_to_chords(&whole, Version::One).is_ok());
1326        for missing in [
1327            "I\t\t1.0\t4/4\tC\tC\tI\t4.0\n",
1328            "I\t1\t\t4/4\tC\tC\tI\t4.0\n",
1329            "I\t1\t1.0\t4/4\tC\tC\tI\t\n",
1330        ] {
1331            let table = format!("{header}{missing}");
1332            assert!(
1333                tsv_to_chords(&table, Version::One).is_err(),
1334                "a table with a number left out reads: {missing:?}"
1335            );
1336        }
1337    }
1338
1339    /// Every answer here is music21's own for the same input.
1340    #[test]
1341    fn the_notations_swap_as_music21_swaps_them() {
1342        assert_eq!(character_swaps("ii%", Direction::IntoMusic21), "iiø");
1343        assert_eq!(character_swaps("iiø", Direction::IntoDcml), "ii%");
1344        assert_eq!(character_swaps("ii/o", Direction::IntoDcml), "ii%");
1345        assert_eq!(character_swaps("VM7", Direction::IntoMusic21), "V7");
1346        assert_eq!(character_swaps("viio7", Direction::IntoMusic21), "viio7");
1347        assert!(!is_minor("F"));
1348        assert!(is_minor("f"));
1349        assert!(!is_minor("F#"));
1350        assert!(is_minor("bb"));
1351    }
1352
1353    /// Every form here is music21's own for the same figure.
1354    #[test]
1355    fn the_form_column_reads_off_the_figure() {
1356        for (figure, form) in [
1357            ("V", ""),
1358            ("viio7", "o"),
1359            ("IVM7", "M"),
1360            ("III+", "+"),
1361            ("IV+M7", "+M"),
1362            ("viiø7", "%"),
1363            ("I", ""),
1364            ("V7", ""),
1365            ("viio", "o"),
1366        ] {
1367            assert_eq!(form_of(figure), form, "the form of {figure}");
1368        }
1369        assert_eq!(figured_bass_of("M43"), "43");
1370        assert_eq!(figured_bass_of("43"), "43");
1371        assert_eq!(figured_bass_of("M"), "");
1372        assert_eq!(figured_bass_of(""), "");
1373    }
1374
1375    /// Every label here is music21's own for the same label.
1376    #[test]
1377    fn added_tones_become_music21s_brackets() {
1378        for (label, written) in [
1379            ("V(64)", "Cad64"),
1380            ("i(4+2)", "i[no3][add4][add2]"),
1381            ("Viio7(b4-5)/V", "Viio7[no3][no5][addb4]/V"),
1382            ("vi(#74)", "vi[no3][add#7][add4]"),
1383            ("ii6(11#7b6)", "ii6[no8][no5][add11][add#7][addb6]"),
1384            ("i(#0)", "i[no1][add#7]"),
1385            ("V", "V"),
1386            ("V7(9)", "V7[add9]"),
1387            ("I(+6)", "I[add6]"),
1388            ("V(6)", "V[no5][add6]"),
1389            ("viio(b9)", "viio[addb9]"),
1390            ("I6(4)", "I6[no3][add4]"),
1391            ("V43(2)", "V43[no1][add2]"),
1392            ("i(v4)", "i[no3][add4]"),
1393            ("i(^4)", "i[no5][add4]"),
1394            ("V(13)", "V[add13]"),
1395        ] {
1396            assert_eq!(handle_added_tones(label), written, "the label {label}");
1397        }
1398    }
1399
1400    /// Every key here is music21's own for the same degree.
1401    #[test]
1402    fn a_local_key_is_read_in_its_own_terms() {
1403        for (degree, global, from_dcml, named) in [
1404            ("V", "G", false, "D"),
1405            ("ii", "C", false, "d"),
1406            ("i", "C", false, "c"),
1407            ("#vii", "a", false, "g#"),
1408            ("vii", "a", true, "g"),
1409            // The crate spells a flat `b` where music21 writes `-`, as
1410            // every value here does; `to_tsv` writes music21's spelling.
1411            ("bIII", "c", false, "Ebb"),
1412            ("V", "f", false, "C"),
1413            ("iv", "Eb", false, "ab"),
1414        ] {
1415            assert_eq!(
1416                local_key(degree, global, from_dcml).unwrap(),
1417                named,
1418                "{degree} of {global}"
1419            );
1420        }
1421    }
1422
1423    /// Every degree here is music21's own for the same pair of keys.
1424    #[test]
1425    fn a_key_names_a_degree_of_another() {
1426        for (local, global, degree) in [
1427            ("C", "e", "VI"),
1428            ("C#", "e", "#VI"),
1429            ("e", "C", "iii"),
1430            ("f", "A-", "vi"),
1431            ("g#", "a", "#vii"),
1432        ] {
1433            let local = Key::from_tonic(local).unwrap();
1434            let global = Key::from_tonic(global).unwrap();
1435            assert_eq!(
1436                local_key_as_roman(&local, &global).unwrap(),
1437                degree,
1438                "the degree of the pair"
1439            );
1440        }
1441        assert_eq!(secondary_key("V/vi", "C").unwrap(), "a");
1442        assert_eq!(secondary_key("V", "C").unwrap(), "C");
1443        assert_eq!(secondary_key("viio7/ii", "G").unwrap(), "a");
1444    }
1445
1446    /// music21 prefixes the figures of a major-minor seventh away from the
1447    /// fifth degree, and leaves an added tone's figure alone.
1448    #[test]
1449    fn a_seventh_away_from_the_dominant_says_so() {
1450        assert_eq!(prefix_sevenths("II7"), "IId7");
1451        assert_eq!(prefix_sevenths("II65"), "IId65");
1452        assert_eq!(prefix_sevenths("I[add13]"), "I[add13]");
1453        assert_eq!(prefix_sevenths("II7[add9]"), "IId7[add9]");
1454        assert_eq!(prefix_sevenths("V"), "V");
1455    }
1456
1457    #[test]
1458    fn a_version_is_one_or_two() {
1459        assert_eq!(Version::from_number(1).unwrap(), Version::One);
1460        assert_eq!(Version::from_number(2).unwrap().number(), 2);
1461        assert!(Version::from_number(3).is_err());
1462        assert!(Version::One.columns().contains(&"global_key"));
1463        assert!(Version::Two.columns().contains(&"globalkey"));
1464    }
1465}