Skip to main content

music21/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, FormatErrorKind, Position, Result};
28use crate::expressions::python_float;
29use crate::key::Key;
30use crate::makenotation::{MAX_MEASURES, 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::roman_text(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::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::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::roman_text("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::roman_text(
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::roman_text(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 + written[start..].chars().next().map_or(1, char::len_utf8);
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::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/// A row's label and keys as one of the two standards spells them:
547/// what music21's `_changeRepresentation` writes back onto a row.
548#[derive(Clone, Debug, PartialEq)]
549#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
550pub struct Spelling {
551    /// The whole label, which the lab's version 2 writes in one column.
552    pub chord: String,
553    /// The degree alone.
554    pub numeral: String,
555    /// How the chord is built over the degree.
556    pub form: Option<String>,
557    /// The degree the chord is read in.
558    pub relativeroot: Option<String>,
559    /// The key in force.
560    pub local_key: String,
561}
562
563/// Every column the standard does not name is kept beside the row, as
564/// music21 keeps its `extra`.
565#[derive(Clone, Debug, Default, PartialEq)]
566#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
567pub struct TabChord {
568    /// Which standard the row was read in.
569    pub version: Version,
570    /// The label the annotator wrote: the lab's `chord`.
571    pub chord: String,
572    /// The degree alone.
573    pub numeral: String,
574    /// The degree the chord is read in, written after a slash.
575    pub relativeroot: Option<String>,
576    /// `o`, `+`, `%` or `M`: how the chord is built over the degree.
577    pub form: Option<String>,
578    /// The figured bass under it.
579    pub figbass: Option<String>,
580    /// The tones it adds or leaves out.
581    pub changes: Option<String>,
582    /// Whether a phrase ends here.
583    pub phraseend: Option<String>,
584    /// The note held under it.
585    pub pedal: Option<String>,
586    /// The key in force, as a degree of the global key or as a key.
587    pub local_key: String,
588    /// The key the piece is in.
589    pub global_key: String,
590    /// The beat of the measure the chord falls on, counted from one.
591    pub beat: FloatType,
592    /// The measure it falls in.
593    pub measure: i32,
594    /// The meter in force, as a ratio.
595    pub timesig: String,
596    /// Which time through a repeat the row belongs to, where the lab's
597    /// version 2 says.
598    pub volta: Option<String>,
599    /// How long the chord sounds, where the lab's version 1 says.
600    pub length: Option<FloatType>,
601    /// Every other column of the row, by name.
602    pub extra: Vec<(String, String)>,
603}
604
605impl TabChord {
606    /// A row read against a table's own header: music21's
607    /// `populateFromRow`, which reads the columns its standard names and
608    /// keeps the rest beside them.
609    pub fn from_row(header: &[String], row: &[String], version: Version) -> Result<Self> {
610        let mut read = Self {
611            version,
612            ..Self::default()
613        };
614        let read_columns = version.read_columns();
615        for (index, name) in header.iter().enumerate() {
616            let Some(cell) = row.get(index) else {
617                continue;
618            };
619            if !read_columns.contains(&name.as_str()) {
620                if !cell.is_empty() {
621                    read.extra.push((name.clone(), cell.clone()));
622                }
623                continue;
624            }
625            let text = cell.clone();
626            match name.as_str() {
627                "chord" => read.chord = text,
628                "numeral" => read.numeral = text,
629                "relativeroot" => read.relativeroot = Some(text),
630                "form" => read.form = Some(text),
631                "figbass" => read.figbass = Some(text),
632                "changes" => read.changes = Some(text),
633                "phraseend" => read.phraseend = Some(text),
634                "pedal" => read.pedal = Some(text),
635                "timesig" => read.timesig = text,
636                "volta" => read.volta = Some(text),
637                "local_key" | "localkey" => read.local_key = text,
638                "global_key" | "globalkey" => read.global_key = text,
639                "measure" | "mn" => {
640                    read.measure = text.parse().map_err(|_| {
641                        Error::roman_text(format!("{name} is not a measure number: {text}"))
642                    })?;
643                }
644                "beat" => {
645                    read.beat = text
646                        .parse()
647                        .map_err(|_| Error::roman_text(format!("beat is not a number: {text}")))?;
648                }
649                // The lab's version 2 counts the onset in whole notes from
650                // the start of the measure, sometimes as a fraction.
651                "mn_onset" => read.beat = float_or_fraction(&text)? * 4.0 + 1.0,
652                // music21 reads the column with `float`, which refuses a
653                // cell saying nothing rather than reading it as no length.
654                "length" => {
655                    read.length = Some(text.parse().map_err(|_| {
656                        Error::roman_text(format!("length is not a number: {text}"))
657                    })?);
658                }
659                // `altchord` and `totbeat` music21 reads onto attributes of
660                // the row that nothing asks again, and `label` onto no
661                // attribute at all, so none of the three is kept beside the
662                // row -- which is what the writer reads for the columns it
663                // cannot work out itself.
664                _ => {}
665            }
666        }
667        Ok(read)
668    }
669
670    /// The label and the keys as one of the two standards spells them:
671    /// music21's `_changeRepresentation`, which writes all five back onto
672    /// the row.
673    ///
674    /// Only the reading *into* music21 touches the label: music21 leaves
675    /// a row's chord and form alone going the other way.
676    pub fn spelled(&self, direction: Direction) -> Spelling {
677        // music21 branches on whether the local key and the degree it is
678        // read in are minor, but every branch calls one function whose
679        // `minor` argument it never reads, so the swaps are the same
680        // either way.
681        let numeral = character_swaps(&self.numeral, direction);
682        let relativeroot = self
683            .relativeroot
684            .as_deref()
685            .filter(|root| !root.is_empty())
686            .map(|root| character_swaps(root, direction));
687        let local_key = character_swaps(&self.local_key, direction);
688        if direction == Direction::IntoDcml {
689            return Spelling {
690                chord: self.chord.clone(),
691                numeral,
692                form: self.form.clone(),
693                relativeroot,
694                local_key,
695            };
696        }
697        let form = self.form.as_deref().map(|form| form.replace('%', "ø"));
698        let mut chord = self.chord.clone();
699        if self.version == Version::Two && !chord.is_empty() {
700            chord = handle_added_tones(&chord.replace('%', "ø"));
701            // A major-minor seventh away from the fifth degree says so in
702            // music21's figure and not in the lab's. music21 reads the
703            // degree here before it has swapped it.
704            if self
705                .extra
706                .iter()
707                .any(|(name, value)| name == "chord_type" && value == "Mm7")
708                && self.numeral != "V"
709            {
710                chord = prefix_sevenths(&chord);
711            }
712        }
713        Spelling {
714            chord,
715            numeral,
716            form,
717            relativeroot,
718            local_key,
719        }
720    }
721
722    /// The row with that spelling written onto it.
723    fn spelled_row(&self, direction: Direction) -> Self {
724        let spelling = self.spelled(direction);
725        Self {
726            chord: spelling.chord,
727            numeral: spelling.numeral,
728            form: spelling.form,
729            relativeroot: spelling.relativeroot,
730            local_key: spelling.local_key,
731            ..self.clone()
732        }
733    }
734
735    /// The figure the row's fields spell *as they stand*: the
736    /// figure-building half of music21's `tabToM21`, which reads the row
737    /// after `_changeRepresentation` has had its way with it.
738    ///
739    /// The lab's version 2 writes the whole label in one column, which is
740    /// the figure; version 1 writes the degree, how the chord is built
741    /// over it, the figured bass and the degree it is read in, which this
742    /// puts together.
743    pub fn label(&self) -> String {
744        if self.version == Version::Two && !self.chord.is_empty() {
745            return self.chord.clone();
746        }
747        let mut label = format!(
748            "{}{}{}",
749            self.numeral,
750            self.form
751                .as_deref()
752                .filter(|form| !form.is_empty())
753                .unwrap_or_default(),
754            self.figbass.as_deref().unwrap_or_default()
755        );
756        if let Some(root) = self.relativeroot.as_deref().filter(|root| !root.is_empty()) {
757            label.push('/');
758            label.push_str(root);
759        }
760        label
761    }
762
763    /// The key the chord is read in: the local key as a key of its own,
764    /// where the table wrote it as a degree of the global key.
765    ///
766    /// # Errors
767    ///
768    /// A local key that names no major or minor triad of the global one.
769    pub fn key(&self) -> Result<Key> {
770        let local = character_swaps(&self.local_key, Direction::IntoMusic21);
771        let named = if is_degree(&local) {
772            local_key(&local, &self.global_key, false)?
773        } else {
774            local
775        };
776        Key::from_tonic(&named)
777    }
778
779    /// The chord the row stands for: a numeral in the key in force, or the
780    /// chord of no harmony the lab writes `@none`.
781    ///
782    /// The sixth and the seventh of a minor key are read flat, as music21
783    /// reads a table's.
784    ///
785    /// # Errors
786    ///
787    /// A label or a key the crate cannot read.
788    pub fn element(&self) -> Result<StreamElement> {
789        self.spelled_row(Direction::IntoMusic21)
790            .element_as_spelled()
791    }
792
793    /// The same of a row *as it stands*, which is music21's `tabToM21`
794    /// once `_changeRepresentation` has had its way with the row.
795    ///
796    /// # Errors
797    ///
798    /// A label or a key the crate cannot read.
799    pub fn element_as_spelled(&self) -> Result<StreamElement> {
800        if self.numeral.is_empty() || self.numeral == "@none" {
801            return Ok(no_chord().into());
802        }
803        let numeral = RomanNumeral::with_minor_defaults(
804            self.label(),
805            self.key()?,
806            Minor67Default::Flat,
807            Minor67Default::Flat,
808        )?;
809        let chord = numeral
810            .to_chord()?
811            .with_duration(Duration::new(0.0)?)
812            .with_numeral(numeral);
813        Ok(chord.into())
814    }
815}
816
817/// A number written plainly or as a fraction: music21's `_float_or_frac`,
818/// which the lab's version 2 needs for an onset of `1/2`.
819fn float_or_fraction(text: &str) -> Result<FloatType> {
820    if let Ok(number) = text.parse::<FloatType>() {
821        return Ok(number);
822    }
823    let (numerator, denominator) = text
824        .split_once('/')
825        .ok_or_else(|| Error::roman_text(format!("not a number: {text}")))?;
826    let numerator: FloatType = numerator
827        .parse()
828        .map_err(|_| Error::roman_text(format!("not a number: {text}")))?;
829    let denominator: FloatType = denominator
830        .parse()
831        .map_err(|_| Error::roman_text(format!("not a number: {text}")))?;
832    if denominator == 0.0 {
833        return Err(Error::roman_text(format!("not a number: {text}")));
834    }
835    Ok(numerator / denominator)
836}
837
838/// The rows of a table, each read: music21's `TsvHandler.tsvToChords`.
839///
840/// # Errors
841///
842/// A table with no header, or a cell the standard cannot read.
843pub fn tsv_to_chords(text: &str, version: Version) -> Result<Vec<TabChord>> {
844    let mut lines = text.lines().filter(|line| !line.trim().is_empty());
845    let header: Vec<String> = match lines.next() {
846        Some(line) => cells(line),
847        None => return Err(Error::roman_text("a table with no header".to_string())),
848    };
849    lines
850        .map(|line| TabChord::from_row(&header, &cells(line), version))
851        .collect()
852}
853
854/// A line of a table, its cells unquoted as a reader of tab-separated
855/// values unquotes them.
856fn cells(line: &str) -> Vec<String> {
857    line.split('\t')
858        .map(|cell| {
859            let cell = cell.trim_end_matches('\r');
860            match cell
861                .strip_prefix('"')
862                .and_then(|rest| rest.strip_suffix('"'))
863            {
864                Some(inside) => inside.replace("\"\"", "\""),
865                None => cell.to_string(),
866            }
867        })
868        .collect()
869}
870
871/// A DCML table read as a score of one part: music21's
872/// `TsvHandler.toM21Stream`, which prepares the measures from the table's
873/// own measure numbers and meters and then puts each chord in the measure
874/// it falls in.
875///
876/// The score is the analysis alone -- the numerals and the keys, with no
877/// notes of the music they describe -- and each numeral lasts until the
878/// next, the last to the end of its measure, which is music21's
879/// `extendDuration`.
880///
881/// ```
882/// use music21::romantext::tsv::{Version, from_tsv};
883///
884/// let table = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n\
885///              .C.I\t1\t1.0\t4/4\tC\tI\tI\n\
886///              V\t2\t1.0\t4/4\tC\tI\tV\n";
887/// let read = from_tsv(table, Version::One)?;
888/// assert_eq!(read.parts().len(), 1);
889/// # Ok::<(), music21::Error>(())
890/// ```
891///
892/// # Errors
893///
894/// A table with no rows, a meter or a key it cannot read, or a label no
895/// numeral can be built from.
896pub fn from_tsv(text: &str, version: Version) -> Result<Stream> {
897    let rows = tsv_to_chords(text, version)?;
898    let Some(first) = rows.first() else {
899        return Err(Error::roman_text("a table with no rows".to_string()));
900    };
901    let mut part = Stream::with_kind(StreamKind::Part);
902    let key = Key::from_tonic(&character_swaps(&first.global_key, Direction::IntoMusic21))?;
903    let mut meter = TimeSignature::from_ratio_string(&first.timesig)?;
904    let mut said_timesig = first.timesig.clone();
905    let mut measure_length = meter.bar_duration().quarter_length();
906    let mut offset = 0.0;
907    // Counted wider than a measure number, which the first may be the
908    // least of.
909    let mut previous_measure = i64::from(first.measure) - 1;
910    let mut previous_volta = String::new();
911    // Where each measure of the part stands and what it is numbered, so
912    // that a chord can be put in the measure it falls in.
913    let mut measures: Vec<(String, FloatType, Stream)> = Vec::new();
914    let new_measure = |number: i32, volta: Option<&str>, offset: FloatType| {
915        let mut measure = Stream::with_kind(StreamKind::Measure);
916        measure.set_number(number);
917        let suffix = volta.and_then(volta_suffix);
918        if let Some(suffix) = &suffix {
919            measure.set_number_suffix(Some(suffix.clone()));
920        }
921        let name = format!("{number}{}", suffix.unwrap_or_default());
922        (name, offset, measure)
923    };
924    for row in &rows {
925        let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
926        if version == Version::Two && row.volta.as_deref().unwrap_or("") != previous_volta {
927            previous_volta = row.volta.clone().unwrap_or_default();
928        } else if i64::from(row.measure) == previous_measure {
929            // Only a change of measure, or of the time through a repeat,
930            // makes a measure.
931            continue;
932        }
933        if i64::from(row.measure) > previous_measure + 1 {
934            let made = measures.len() as i64 + i64::from(row.measure) - previous_measure;
935            if made > MAX_MEASURES as i64 {
936                return Err(Error::roman_text(format!(
937                    "measure {} would make more than {MAX_MEASURES} measures",
938                    row.measure
939                ))
940                .of_kind(FormatErrorKind::TooLarge)
941                .placed(|| Position::measure(row.measure.to_string())));
942            }
943            // Not every measure has a chord of its own.
944            for number in (previous_measure + 1)..=i64::from(row.measure) {
945                offset += measure_length;
946                let number = IntegerType::try_from(number).unwrap_or(row.measure);
947                measures.push(new_measure(number, None, offset));
948            }
949            previous_measure = i64::from(row.measure);
950            continue;
951        }
952        offset += measure_length;
953        let (name, at, mut measure) = new_measure(row.measure, volta, offset);
954        // A measure played one time through a repeat carries the bracket
955        // drawn over it: music21 keeps a `RepeatBracket` spanning the
956        // measures of each ending, and a measure here says which ending it
957        // is in and that the bracket opens and closes on it, there being
958        // one measure to an ending in a table.
959        if let Some(volta) = volta.and_then(|volta| volta.parse::<u32>().ok()) {
960            measure.set_ending(Some(Ending::new(vec![volta], true, true)));
961        }
962        if row.timesig != said_timesig {
963            let said = TimeSignature::from_ratio_string(&row.timesig)?;
964            measure.insert(offset_in_measure(row.beat), said.clone());
965            said_timesig = row.timesig.clone();
966            measure_length = said.bar_duration().quarter_length();
967            meter = said;
968        }
969        measures.push((name, at, measure));
970        previous_measure = i64::from(row.measure);
971    }
972    let _ = &meter;
973    // The key and the meter stand at the start of the first measure.
974    if let Some((_, _, measure)) = measures.first_mut() {
975        measure.insert(0.0, key);
976        measure.insert(0.0, TimeSignature::from_ratio_string(&first.timesig)?);
977    }
978    for row in &rows {
979        let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
980        let name = format!(
981            "{}{}",
982            row.measure,
983            volta.and_then(volta_suffix).unwrap_or_default()
984        );
985        let Some((_, _, measure)) = measures.iter_mut().find(|(known, _, _)| *known == name) else {
986            return Err(Error::roman_text(format!("no measure {name} to analyse")));
987        };
988        measure.insert(offset_in_measure(row.beat), row.element()?);
989    }
990    // All at once: sorting the part again for each measure is quadratic.
991    part.insert_sorted(
992        measures
993            .into_iter()
994            .map(|(_, at, measure)| StreamEvent::new(at, measure))
995            .collect(),
996    );
997    lengthen_harmonies(&mut part)?;
998    let mut score = Stream::with_kind(StreamKind::Score);
999    score.insert(0.0, part);
1000    Ok(score)
1001}
1002
1003/// The suffix a volta writes on a measure number: the first time through is
1004/// `a`, the second `b`, as music21 letters them.
1005fn volta_suffix(volta: &str) -> Option<String> {
1006    let number: usize = volta.parse().ok()?;
1007    let letter = char::from(b'a'.checked_add(u8::try_from(number.checked_sub(1)?).ok()?)?);
1008    letter.is_ascii_lowercase().then(|| letter.to_string())
1009}
1010
1011/// Every harmony lasting until the next, and the last to the end of the
1012/// part: music21's `extendDuration` over `harmony.Harmony`, and the length
1013/// it writes onto the last chord itself afterwards.
1014///
1015/// music21 extends the durations over the part *flattened*, so a chord
1016/// lasts across the barline into the next measure's chord, and then reads
1017/// the part's length again -- which the extending has just changed -- to
1018/// give the last chord the rest of the piece. Both steps are kept, in that
1019/// order, since the second reads what the first wrote.
1020fn lengthen_harmonies(part: &mut Stream) -> Result<()> {
1021    // Where each harmony stands in the part, measure by measure.
1022    let places = harmony_places(part);
1023    let total = part.end_offset();
1024    for (index, (offset, measure, event)) in places.iter().enumerate() {
1025        let until = places.get(index + 1).map_or(total, |(next, _, _)| *next);
1026        set_length(part, *measure, *event, op_frac(until - offset))?;
1027    }
1028    // The part is longer now than it was, and music21 gives the last chord
1029    // what is left of it.
1030    if let Some((offset, measure, event)) = places.last() {
1031        let total = part.end_offset();
1032        set_length(part, *measure, *event, op_frac(total - offset))?;
1033    }
1034    Ok(())
1035}
1036
1037/// Every harmony of a part: where it stands in the part, which measure
1038/// holds it, and which of that measure's events it is.
1039fn harmony_places(part: &Stream) -> Vec<(FloatType, usize, usize)> {
1040    let mut places = Vec::new();
1041    for (measure, event) in part.events().iter().enumerate() {
1042        let Some(inner) = event.element().as_stream() else {
1043            continue;
1044        };
1045        for (index, held) in inner.events().iter().enumerate() {
1046            if held.element().is_instance("Harmony") {
1047                places.push((op_frac(event.offset() + held.offset()), measure, index));
1048            }
1049        }
1050    }
1051    places.sort_by(|left, right| left.0.total_cmp(&right.0));
1052    places
1053}
1054
1055/// Writes one held element's length.
1056fn set_length(
1057    part: &mut Stream,
1058    measure: usize,
1059    event: usize,
1060    quarter_length: FloatType,
1061) -> Result<()> {
1062    let Some(StreamElement::Stream(inner)) = part
1063        .events_mut()
1064        .get_mut(measure)
1065        .map(StreamEvent::element_mut)
1066    else {
1067        return Ok(());
1068    };
1069    let Some(held) = inner.events_mut().get_mut(event) else {
1070        return Ok(());
1071    };
1072    let duration = Duration::new(quarter_length.max(0.0))?;
1073    match held.element_mut() {
1074        StreamElement::Chord(chord) => chord.set_duration(duration),
1075        StreamElement::ChordSymbol(symbol) => symbol.set_duration(duration),
1076        _ => {}
1077    }
1078    Ok(())
1079}
1080
1081/// A score written as one of the lab's tables: music21's `M21toTSV`, which
1082/// writes a row for every numeral of the score, in the order a walk of it
1083/// meets them.
1084///
1085/// Every column the standard names is written, in the standard's own order,
1086/// and what the standard has no answer for is written empty, as music21
1087/// writes it: where a phrase ends, the note held under a chord, the tones a
1088/// chord changes. The columns music21 fills from a numeral's `editorial` --
1089/// the cells its reader did not read and kept beside the row -- are empty
1090/// here too; [`to_tsv_with_extra`] is how a caller hands them back.
1091///
1092/// ```
1093/// use music21::romantext::tsv::{Version, from_tsv, to_tsv};
1094///
1095/// let table = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n\
1096///              I\t1\t1.0\t4/4\tC\tC\tI\n";
1097/// let written = to_tsv(&from_tsv(table, Version::One)?, Version::One)?;
1098/// let mut lines = written.lines();
1099/// assert!(lines.next().is_some_and(|header| header.starts_with("chord\t")));
1100/// assert!(lines.next().is_some_and(|row| row.starts_with("I\t\t1\t1.0")));
1101/// # Ok::<(), music21::Error>(())
1102/// ```
1103///
1104/// # Errors
1105///
1106/// A version 1 table with no row at all -- music21 reads the global key off
1107/// the first numeral and asks its walk for one -- or a numeral standing
1108/// where no meter is in force, which has no beat.
1109pub fn to_tsv(stream: &Stream, version: Version) -> Result<String> {
1110    to_tsv_with_extra(stream, version, &[])
1111}
1112
1113/// A score written as a table, with the cells music21 keeps on each
1114/// numeral's `editorial` given back: one list per chord the walk meets, in
1115/// that order, which for a score [`from_tsv`] read is the order of the
1116/// table's own rows.
1117///
1118/// music21's reader writes every column it does not read onto the numeral
1119/// it made, and its writer reads those cells back for the columns it cannot
1120/// work out itself -- which is how a table's `mc`, `staff`, `chord_tones`
1121/// and the rest survive a round trip. A chord here carries nothing of the
1122/// kind, so they are the caller's to keep: [`TabChord::extra`] is what
1123/// [`tsv_to_chords`] reads them into.
1124///
1125/// # Errors
1126///
1127/// As [`to_tsv`].
1128pub fn to_tsv_with_extra(
1129    stream: &Stream,
1130    version: Version,
1131    extra: &[Vec<(String, String)>],
1132) -> Result<String> {
1133    let written = written_chords(stream, version);
1134    let mut text = String::new();
1135    write_row(
1136        &mut text,
1137        version.columns().iter().map(|&name| name.to_string()),
1138    );
1139    // The global key is the key the first numeral is read in.
1140    let Some(global) = written.iter().find_map(|written| written.numeral) else {
1141        if version == Version::One {
1142            // music21 asks its walk for the next numeral and is answered
1143            // with a raise.
1144            return Err(Error::roman_text(
1145                "a score with no roman numeral to write".to_string(),
1146            ));
1147        }
1148        return Ok(text);
1149    };
1150    let global_key = global.key().clone();
1151    let global_name = music21_key_name(&global_key);
1152    for (index, written) in written.iter().enumerate() {
1153        let attributes = match version {
1154            Version::One => v1_attributes(written, &global_name)?,
1155            Version::Two => v2_attributes(written, &global_name, &global_key)?,
1156        };
1157        let kept = extra.get(index).map(Vec::as_slice).unwrap_or_default();
1158        let cells = version.columns().iter().map(|name| {
1159            // music21 reads a column off the row it built where that row
1160            // has an attribute of the name, and off the numeral's editorial
1161            // where it has none.
1162            cell_of(&attributes, name)
1163                .or_else(|| cell_of(kept, name))
1164                .unwrap_or_default()
1165        });
1166        write_row(&mut text, cells);
1167    }
1168    Ok(text)
1169}
1170
1171/// One cell of a list of them, by column name.
1172fn cell_of<Name: AsRef<str>>(cells: &[(Name, String)], name: &str) -> Option<String> {
1173    cells
1174        .iter()
1175        .find(|(known, _)| known.as_ref() == name)
1176        .map(|(_, value)| value.clone())
1177}
1178
1179/// The columns music21's `TabChord` holds for a numeral: the first half of
1180/// its `_m21ToTsv_v1`.
1181fn v1_attributes(written: &Written<'_>, global_key: &str) -> Result<Vec<(&'static str, String)>> {
1182    let numeral = written
1183        .numeral
1184        .ok_or_else(|| Error::roman_text("a chord of no harmony has no row here".to_string()))?;
1185    // music21 writes the degree again as the `altchord` where the numeral
1186    // standing for it was built in the chord's own key, which a secondary
1187    // numeral always is.
1188    let relativeroot = numeral.secondary().unwrap_or_default().to_string();
1189    Ok(vec![
1190        ("chord", numeral.figure().to_string()),
1191        ("altchord", relativeroot.clone()),
1192        ("measure", written.measure.unwrap_or(1).to_string()),
1193        ("beat", python_float(written.beat()?)),
1194        ("totbeat", String::new()),
1195        ("timesig", written.timesig()),
1196        ("length", python_number(written.length)),
1197        ("global_key", global_key.to_string()),
1198        ("local_key", music21_key_name(numeral.key())),
1199        ("pedal", String::new()),
1200        ("numeral", numeral.roman_numeral()),
1201        ("form", form_of(numeral.figure()).to_string()),
1202        (
1203            "figbass",
1204            figured_bass_of(numeral.figures_written()).to_string(),
1205        ),
1206        ("changes", String::new()),
1207        ("relativeroot", relativeroot),
1208        ("phraseend", String::new()),
1209    ])
1210}
1211
1212/// The columns music21's `TabChordV2` holds: the first half of its
1213/// `_m21ToTsv_v2`, where the label is the whole figure and the chord of no
1214/// harmony is written `@none`.
1215fn v2_attributes(
1216    written: &Written<'_>,
1217    global_key: &str,
1218    global: &Key,
1219) -> Result<Vec<(&'static str, String)>> {
1220    let mut local_key = String::new();
1221    let mut form = String::new();
1222    let mut figbass = String::new();
1223    let mut relativeroot = String::new();
1224    let (chord, numeral_text) = match written.numeral {
1225        None => ("@none".to_string(), "@none".to_string()),
1226        Some(numeral) => {
1227            local_key = local_key_as_roman(numeral.key(), global)?;
1228            if let Some(root) = numeral.secondary() {
1229                relativeroot = character_swaps(root, Direction::IntoDcml);
1230            }
1231            form = form_of(numeral.figure()).to_string();
1232            figbass = figured_bass_of(numeral.figures_written()).to_string();
1233            (
1234                // The lab does not write the `d` music21 puts in front of
1235                // the figures of a major-minor seventh away from the fifth
1236                // degree.
1237                numeral.figure().replacen('d', "", 1),
1238                numeral.roman_numeral(),
1239            )
1240        }
1241    };
1242    Ok(vec![
1243        (
1244            "mn",
1245            written
1246                .measure
1247                .map(|number| number.to_string())
1248                .unwrap_or_default(),
1249        ),
1250        // The lab's version 2 counts the onset in whole notes from the
1251        // start of the measure, which music21 reads off the offset rather
1252        // than off the beat.
1253        ("mn_onset", python_number(written.offset / 4.0)),
1254        ("timesig", written.timesig()),
1255        ("volta", String::new()),
1256        ("globalkey", global_key.to_string()),
1257        ("localkey", local_key),
1258        ("pedal", String::new()),
1259        ("chord", chord),
1260        ("numeral", numeral_text),
1261        ("form", form),
1262        ("figbass", figbass),
1263        ("changes", String::new()),
1264        ("relativeroot", relativeroot),
1265        ("phraseend", String::new()),
1266    ])
1267}
1268
1269/// One chord a table has a row for: the numeral it stands for, or none for
1270/// the chord of no harmony, and where in the score it stands.
1271#[derive(Debug)]
1272struct Written<'a> {
1273    numeral: Option<&'a RomanNumeral>,
1274    measure: Option<IntegerType>,
1275    offset: FloatType,
1276    padding: FloatType,
1277    length: FloatType,
1278    meter: Option<TimeSignature>,
1279}
1280
1281impl Written<'_> {
1282    /// The meter in force as the lab writes it, and nothing where none is.
1283    fn timesig(&self) -> String {
1284        self.meter
1285            .as_ref()
1286            .map(TimeSignature::ratio_string)
1287            .unwrap_or_default()
1288    }
1289
1290    /// The beat of the measure the chord falls on, counted from one and
1291    /// taking the measure's padding in, as music21's `beat` counts it.
1292    ///
1293    /// # Errors
1294    ///
1295    /// A chord standing where no meter is in force, which music21's `beat`
1296    /// raises for.
1297    fn beat(&self) -> Result<FloatType> {
1298        let meter = self.meter.as_ref().ok_or_else(|| {
1299            Error::roman_text("a chord with no meter in force does not have a beat".to_string())
1300        })?;
1301        meter.beat_proportion(op_frac(self.offset + self.padding))
1302    }
1303}
1304
1305/// Every chord a table has a row for, in the order music21's `recurse`
1306/// meets them: a chord standing for a numeral, and under the lab's version
1307/// 2 the chord of no harmony beside it, which its version 1 writes no row
1308/// for.
1309fn written_chords(stream: &Stream, version: Version) -> Vec<Written<'_>> {
1310    let mut found = Vec::new();
1311    let mut meter = None;
1312    read_chords(stream, version, None, &mut meter, &mut found);
1313    found
1314}
1315
1316/// The walk itself, which carries the meter in force and the measure being
1317/// read down into every stream it meets.
1318fn read_chords<'a>(
1319    stream: &'a Stream,
1320    version: Version,
1321    measure: Option<(IntegerType, FloatType)>,
1322    meter: &mut Option<TimeSignature>,
1323    found: &mut Vec<Written<'a>>,
1324) {
1325    for event in stream.events() {
1326        match event.element() {
1327            // A meter sorts before a chord standing at its own offset, so
1328            // the one in force is the last met.
1329            StreamElement::TimeSignature(said) => *meter = Some(said.clone()),
1330            StreamElement::Stream(inner) => {
1331                let inside = if inner.kind() == StreamKind::Measure {
1332                    Some((inner.number(), inner.padding_left()))
1333                } else {
1334                    measure
1335                };
1336                read_chords(inner, version, inside, meter, found);
1337            }
1338            element => {
1339                let numeral = match element {
1340                    StreamElement::Chord(chord) => chord.numeral(),
1341                    _ => None,
1342                };
1343                let no_chord = matches!(
1344                    element,
1345                    StreamElement::ChordSymbol(symbol) if symbol.is_no_chord()
1346                );
1347                if numeral.is_none() && !(version == Version::Two && no_chord) {
1348                    continue;
1349                }
1350                found.push(Written {
1351                    numeral,
1352                    measure: measure.map(|(number, _)| number),
1353                    offset: event.offset(),
1354                    padding: measure.map_or(0.0, |(_, padding)| padding),
1355                    length: element.quarter_length(),
1356                    meter: meter.clone(),
1357                });
1358            }
1359        }
1360    }
1361}
1362
1363/// A row as Python's `csv.writer` writes one: the cells joined by tabs,
1364/// each quoted only where it has to be, and the line ended as that writer
1365/// ends it.
1366fn write_row(text: &mut String, cells: impl Iterator<Item = String>) {
1367    let mut first = true;
1368    for cell in cells {
1369        if !first {
1370            text.push('\t');
1371        }
1372        first = false;
1373        if cell.contains(['\t', '"', '\r', '\n']) {
1374            text.push('"');
1375            text.push_str(&cell.replace('"', "\"\""));
1376            text.push('"');
1377        } else {
1378            text.push_str(&cell);
1379        }
1380    }
1381    text.push_str("\r\n");
1382}
1383
1384/// A length or an onset as Python prints the number music21 holds it as: a
1385/// fraction where `opFrac` could not keep it a float, and otherwise a
1386/// float, which always shows a decimal point.
1387fn python_number(value: FloatType) -> String {
1388    if is_fraction(value)
1389        && let Some((numerator, denominator)) = limited_fraction(value, DENOMINATOR_LIMIT)
1390    {
1391        return format!("{numerator}/{denominator}");
1392    }
1393    python_float(value)
1394}
1395
1396/// Whether music21 holds a number as a `Fraction` rather than as a float:
1397/// `opFrac` keeps a float only where its denominator is a power of two no
1398/// larger than the limit.
1399fn is_fraction(value: FloatType) -> bool {
1400    (value * (DENOMINATOR_LIMIT as FloatType + 1.0)).fract() != 0.0
1401}
1402
1403/// The largest denominator `opFrac` keeps.
1404const DENOMINATOR_LIMIT: i128 = 65535;
1405
1406#[cfg(test)]
1407mod tests {
1408    use super::*;
1409
1410    /// A cell the standard reads as a number and finds nothing in: music21
1411    /// coerces those columns with `int` and `float`, which refuse a cell
1412    /// saying nothing, so a table with one is read by neither side.
1413    #[test]
1414    fn a_number_a_table_leaves_out_is_refused() {
1415        let header = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\tlength\n";
1416        let whole = format!("{header}I\t1\t1.0\t4/4\tC\tC\tI\t4.0\n");
1417        assert!(tsv_to_chords(&whole, Version::One).is_ok());
1418        for missing in [
1419            "I\t\t1.0\t4/4\tC\tC\tI\t4.0\n",
1420            "I\t1\t\t4/4\tC\tC\tI\t4.0\n",
1421            "I\t1\t1.0\t4/4\tC\tC\tI\t\n",
1422        ] {
1423            let table = format!("{header}{missing}");
1424            assert!(
1425                tsv_to_chords(&table, Version::One).is_err(),
1426                "a table with a number left out reads: {missing:?}"
1427            );
1428        }
1429    }
1430
1431    /// Every answer here is music21's own for the same input.
1432    #[test]
1433    fn the_notations_swap_as_music21_swaps_them() {
1434        assert_eq!(character_swaps("ii%", Direction::IntoMusic21), "iiø");
1435        assert_eq!(character_swaps("iiø", Direction::IntoDcml), "ii%");
1436        assert_eq!(character_swaps("ii/o", Direction::IntoDcml), "ii%");
1437        assert_eq!(character_swaps("VM7", Direction::IntoMusic21), "V7");
1438        assert_eq!(character_swaps("viio7", Direction::IntoMusic21), "viio7");
1439        assert!(!is_minor("F"));
1440        assert!(is_minor("f"));
1441        assert!(!is_minor("F#"));
1442        assert!(is_minor("bb"));
1443    }
1444
1445    /// Every form here is music21's own for the same figure.
1446    #[test]
1447    fn the_form_column_reads_off_the_figure() {
1448        for (figure, form) in [
1449            ("V", ""),
1450            ("viio7", "o"),
1451            ("IVM7", "M"),
1452            ("III+", "+"),
1453            ("IV+M7", "+M"),
1454            ("viiø7", "%"),
1455            ("I", ""),
1456            ("V7", ""),
1457            ("viio", "o"),
1458        ] {
1459            assert_eq!(form_of(figure), form, "the form of {figure}");
1460        }
1461        assert_eq!(figured_bass_of("M43"), "43");
1462        assert_eq!(figured_bass_of("43"), "43");
1463        assert_eq!(figured_bass_of("M"), "");
1464        assert_eq!(figured_bass_of(""), "");
1465    }
1466
1467    /// Every label here is music21's own for the same label.
1468    #[test]
1469    fn added_tones_become_music21s_brackets() {
1470        for (label, written) in [
1471            ("V(64)", "Cad64"),
1472            ("i(4+2)", "i[no3][add4][add2]"),
1473            ("Viio7(b4-5)/V", "Viio7[no3][no5][addb4]/V"),
1474            ("vi(#74)", "vi[no3][add#7][add4]"),
1475            ("ii6(11#7b6)", "ii6[no8][no5][add11][add#7][addb6]"),
1476            ("i(#0)", "i[no1][add#7]"),
1477            ("V", "V"),
1478            ("V7(9)", "V7[add9]"),
1479            ("I(+6)", "I[add6]"),
1480            ("V(6)", "V[no5][add6]"),
1481            ("viio(b9)", "viio[addb9]"),
1482            ("I6(4)", "I6[no3][add4]"),
1483            ("V43(2)", "V43[no1][add2]"),
1484            ("i(v4)", "i[no3][add4]"),
1485            ("i(^4)", "i[no5][add4]"),
1486            ("V(13)", "V[add13]"),
1487            // What is no tone is passed over a character at a time.
1488            ("V(\u{f8}13)", "V[add13]"),
1489        ] {
1490            assert_eq!(handle_added_tones(label), written, "the label {label}");
1491        }
1492    }
1493
1494    /// Every key here is music21's own for the same degree.
1495    #[test]
1496    fn a_local_key_is_read_in_its_own_terms() {
1497        for (degree, global, from_dcml, named) in [
1498            ("V", "G", false, "D"),
1499            ("ii", "C", false, "d"),
1500            ("i", "C", false, "c"),
1501            ("#vii", "a", false, "g#"),
1502            ("vii", "a", true, "g"),
1503            // The crate spells a flat `b` where music21 writes `-`, as
1504            // every value here does; `to_tsv` writes music21's spelling.
1505            ("bIII", "c", false, "Ebb"),
1506            ("V", "f", false, "C"),
1507            ("iv", "Eb", false, "ab"),
1508        ] {
1509            assert_eq!(
1510                local_key(degree, global, from_dcml).unwrap(),
1511                named,
1512                "{degree} of {global}"
1513            );
1514        }
1515    }
1516
1517    /// Every degree here is music21's own for the same pair of keys.
1518    #[test]
1519    fn a_key_names_a_degree_of_another() {
1520        for (local, global, degree) in [
1521            ("C", "e", "VI"),
1522            ("C#", "e", "#VI"),
1523            ("e", "C", "iii"),
1524            ("f", "A-", "vi"),
1525            ("g#", "a", "#vii"),
1526        ] {
1527            let local = Key::from_tonic(local).unwrap();
1528            let global = Key::from_tonic(global).unwrap();
1529            assert_eq!(
1530                local_key_as_roman(&local, &global).unwrap(),
1531                degree,
1532                "the degree of the pair"
1533            );
1534        }
1535        assert_eq!(secondary_key("V/vi", "C").unwrap(), "a");
1536        assert_eq!(secondary_key("V", "C").unwrap(), "C");
1537        assert_eq!(secondary_key("viio7/ii", "G").unwrap(), "a");
1538    }
1539
1540    /// music21 prefixes the figures of a major-minor seventh away from the
1541    /// fifth degree, and leaves an added tone's figure alone.
1542    #[test]
1543    fn a_seventh_away_from_the_dominant_says_so() {
1544        assert_eq!(prefix_sevenths("II7"), "IId7");
1545        assert_eq!(prefix_sevenths("II65"), "IId65");
1546        assert_eq!(prefix_sevenths("I[add13]"), "I[add13]");
1547        assert_eq!(prefix_sevenths("II7[add9]"), "IId7[add9]");
1548        assert_eq!(prefix_sevenths("V"), "V");
1549    }
1550
1551    #[test]
1552    fn a_version_is_one_or_two() {
1553        assert_eq!(Version::from_number(1).unwrap(), Version::One);
1554        assert_eq!(Version::from_number(2).unwrap().number(), 2);
1555        assert!(Version::from_number(3).is_err());
1556        assert!(Version::One.columns().contains(&"global_key"));
1557        assert!(Version::Two.columns().contains(&"globalkey"));
1558    }
1559
1560    /// A table skipping thousands of measures, each of which is made: put
1561    /// in the part one at a time, they took minutes.
1562    #[test]
1563    fn a_table_skipping_many_measures_is_read_quickly() {
1564        let table = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n\
1565                     .C.I\t1\t1.0\t4/4\tC\tI\tI\n\
1566                     V\t20000\t1.0\t4/4\tC\tI\tV\n";
1567        let started = std::time::Instant::now();
1568        let read = from_tsv(table, Version::One).unwrap();
1569        assert_eq!(read.parts()[0].measures().len(), 20000);
1570        assert!(started.elapsed() < std::time::Duration::from_secs(10));
1571    }
1572
1573    #[test]
1574    fn a_table_of_more_measures_than_a_stream_takes_is_refused() {
1575        let header = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n";
1576        let gap = crate::makenotation::MAX_MEASURES + 2;
1577        for (first, last) in [(1, gap as i64), (-2_147_483_648, 1), (1, 2_147_483_647)] {
1578            let table =
1579                format!("{header}.C.I\t{first}\t1.0\t4/4\tC\tI\tI\nV\t{last}\t1.0\t4/4\tC\tI\tV\n");
1580            assert!(from_tsv(&table, Version::One).is_err(), "{first} {last}");
1581        }
1582    }
1583
1584    #[test]
1585    fn a_volta_past_z_has_no_letter() {
1586        assert_eq!(volta_suffix("2").as_deref(), Some("b"));
1587        assert_eq!(volta_suffix("26").as_deref(), Some("z"));
1588        assert_eq!(volta_suffix("27"), None);
1589        assert_eq!(volta_suffix("200"), None);
1590    }
1591}