Skip to main content

music21_rs/romantext/
mod.rs

1//! RomanText: music21's `romanText`, a harmonic analysis written measure by
2//! measure as roman numerals.
3//!
4//! ```text
5//! Composer: J. S. Bach
6//! Time Signature: 4/4
7//! m1 C: I b3 V6
8//! m2 I b2 ii6/5 b3 V7 b4 vi
9//! m3 = m1
10//! ```
11//!
12//! A line starting `m` and a number is a measure; in it a key is a note name
13//! and a colon, `b` and a number is the beat the next chord stands on, and
14//! anything else is a roman numeral. The other lines are a tag, a colon and
15//! what it says.
16
17pub mod clercq_temperley;
18pub mod tsv;
19
20use crate::bar::{Barline, Ending, RepeatDirection};
21use crate::chord::Chord;
22use crate::chordsymbol::ChordSymbol;
23use crate::defaults::{FloatType, IntegerType};
24use crate::duration::Duration;
25use crate::error::{Error, Result};
26use crate::key::{Key, KeySignature, convert_key_string_to_music21_key_string};
27use crate::makenotation::{make_accidentals, make_beams, op_frac, sorted_events};
28use crate::metadata::{Metadata, MetadataValue};
29use crate::meter::TimeSignature;
30use crate::notation::{Tie, TieType};
31use crate::rest::Rest;
32use crate::roman::{Minor67Default, RomanNumeral};
33use crate::stream::{Stream, StreamElement, StreamEvent, StreamKind};
34
35fn roman_text_error(message: impl Into<String>) -> Error {
36    Error::RomanText(message.into())
37}
38
39/// One word of a measure line: music21's `RTAtom` and its kinds.
40#[derive(Clone, Debug, PartialEq)]
41enum Atom {
42    /// `b2.5`: the beat what follows stands on.
43    Beat(String),
44    /// `G;:` sets the key and its signature.
45    Key(String),
46    /// `G:` sets the key chords are read in.
47    AnalyticKey(String),
48    /// `KS-2`: a key signature of so many sharps.
49    KeySignature(String),
50    RepeatStart,
51    RepeatStop,
52    NoChord,
53    Chord(String),
54    /// A phrase mark, a parenthesis or an optional key, none of which says
55    /// anything a score holds.
56    Other,
57}
58
59/// One line: music21's `RTTagged` and `RTMeasure`.
60#[derive(Clone, Debug)]
61enum Token {
62    Tagged {
63        tag: String,
64        data: String,
65    },
66    Measure {
67        numbers: Vec<IntegerType>,
68        letters: Vec<String>,
69        variant: bool,
70        data: String,
71        atoms: Vec<Atom>,
72    },
73}
74
75impl Token {
76    fn is_movement(&self) -> bool {
77        matches!(self, Self::Tagged { tag, .. } if tag.eq_ignore_ascii_case("movement"))
78    }
79}
80
81/// How far a measure tag reaches into a line: `m`, digits, letters `a` to
82/// `h`, hyphens, digits and letters again.
83fn measure_tag_end(line: &str) -> Option<usize> {
84    let bytes = line.as_bytes();
85    if bytes.first() != Some(&b'm') {
86        return None;
87    }
88    let mut end = 1;
89    let run = |from: usize, test: fn(&u8) -> bool| -> usize {
90        from + bytes[from..].iter().take_while(|byte| test(byte)).count()
91    };
92    let digits = run(end, u8::is_ascii_digit);
93    if digits == end {
94        return None;
95    }
96    end = digits;
97    let letter: fn(&u8) -> bool = |byte| (b'a'..=b'h').contains(byte);
98    end = run(end, letter);
99    end = run(end, |byte| *byte == b'-');
100    end = run(end, u8::is_ascii_digit);
101    end = run(end, letter);
102    Some(end)
103}
104
105/// A measure number and its repeat letter, or two of each for a range:
106/// music21's `_getMeasureNumberData`.
107fn measure_numbers(text: &str) -> Result<(Vec<IntegerType>, Vec<String>)> {
108    let pieces: Vec<&str> = if text.contains('-') {
109        let pieces: Vec<&str> = text.split('-').collect();
110        if pieces.len() != 2 {
111            return Err(roman_text_error(format!(
112                "cannot read the measure numbers {text:?}"
113            )));
114        }
115        pieces
116    } else {
117        vec![text]
118    };
119    let mut numbers = Vec::new();
120    let mut letters = Vec::new();
121    for piece in pieces {
122        let digits: String = piece.chars().filter(char::is_ascii_digit).collect();
123        let other: String = piece.chars().filter(|c| !c.is_ascii_digit()).collect();
124        numbers.push(digits.parse::<IntegerType>().map_err(|_| {
125            roman_text_error(format!("cannot read the measure number in {piece:?}"))
126        })?);
127        letters.push(other.replace('m', ""));
128    }
129    Ok((numbers, letters))
130}
131
132/// Whether a word starts with a key written as music21's patterns have it:
133/// note letters, flats and sharps, then the ending asked for.
134fn starts_with_key(word: &str, ending: &str) -> bool {
135    let letters = word
136        .chars()
137        .take_while(|c| matches!(c, 'A'..='G' | 'a'..='g'))
138        .count();
139    if letters == 0 {
140        return false;
141    }
142    let rest = &word[letters..];
143    let signs = rest.chars().take_while(|c| matches!(c, 'b' | '#')).count();
144    rest[signs..].starts_with(ending)
145}
146
147/// music21's `tokenizeAtoms`.
148fn atoms_of(data: &str) -> Vec<Atom> {
149    let mut atoms = Vec::new();
150    for word in data.split(' ') {
151        let word = word.trim();
152        let chars: Vec<char> = word.chars().collect();
153        if word.is_empty() {
154            continue;
155        }
156        if word == "=" {
157            break;
158        }
159        let no_chord = word.starts_with("NC")
160            || word.starts_with("nc")
161            || (chars.len() >= 4 && chars[0] == 'N' && chars[2] == 'C');
162        let atom = if matches!(word, "||" | "(" | ")") {
163            Atom::Other
164        } else if chars[0] == 'b' && chars.get(1).is_some_and(|c| matches!(c, '1'..='9' | '.')) {
165            Atom::Beat(word.to_string())
166        } else if word
167            .strip_prefix("?(")
168            .or_else(|| word.strip_prefix("?)"))
169            .is_some_and(|rest| starts_with_key(rest, ""))
170            && (word.starts_with("?)") || starts_with_key(&word[2..], ":"))
171        {
172            Atom::Other
173        } else if starts_with_key(word, ";:") {
174            Atom::Key(word.to_string())
175        } else if starts_with_key(word, ":") {
176            Atom::AnalyticKey(word.to_string())
177        } else if word.strip_prefix("KS").is_some_and(|rest| {
178            rest.strip_prefix('-')
179                .unwrap_or(rest)
180                .starts_with(|c: char| ('0'..='7').contains(&c))
181        }) {
182            Atom::KeySignature(word.to_string())
183        } else if word.starts_with("||:") {
184            Atom::RepeatStart
185        } else if word.starts_with(":||") {
186            Atom::RepeatStop
187        } else if no_chord {
188            Atom::NoChord
189        } else {
190            Atom::Chord(word.to_string())
191        };
192        atoms.push(atom);
193    }
194    atoms
195}
196
197fn tagged(line: &str) -> Token {
198    match line.split_once(':') {
199        Some((tag, data)) => Token::Tagged {
200            tag: tag.trim().to_string(),
201            data: data.trim().to_string(),
202        },
203        None => Token::Tagged {
204            tag: String::new(),
205            data: line.to_string(),
206        },
207    }
208}
209
210/// music21's `RTHandler.tokenize`: the header's lines, then the body's.
211fn tokenize(text: &str) -> Result<Vec<Token>> {
212    let lines: Vec<&str> = text.split('\n').collect();
213    let body = lines
214        .iter()
215        .position(|line| measure_tag_end(line.trim()).is_some())
216        .ok_or_else(|| {
217            roman_text_error("Cannot find the first measure definition in this file.")
218        })?;
219    let mut tokens = Vec::new();
220    for line in &lines[..body] {
221        let line = line.trim();
222        if !line.is_empty() {
223            tokens.push(tagged(line));
224        }
225    }
226    for line in &lines[body..] {
227        let line = line.trim();
228        if line.is_empty() {
229            continue;
230        }
231        let Some(end) = measure_tag_end(line) else {
232            tokens.push(tagged(line));
233            continue;
234        };
235        let (numbers, letters) = measure_numbers(line[..end].trim())?;
236        let raw = line[end..].trim();
237        // `var1` and `varA` mark a variant reading, which is passed over.
238        let variant = raw.strip_prefix("var").is_some_and(|rest| {
239            rest.starts_with(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
240        });
241        tokens.push(Token::Measure {
242            numbers,
243            letters,
244            variant,
245            data: raw.to_string(),
246            atoms: atoms_of(raw),
247        });
248    }
249    Ok(tokens)
250}
251
252/// The key a word names, and the words it puts in front of the next chord:
253/// music21's `_getKeyAndPrefix`.
254fn key_and_prefix(word: &str) -> Result<(Key, String)> {
255    let name = convert_key_string_to_music21_key_string(word.trim_end_matches([';', ':']));
256    let key = Key::from_tonic(&name)?;
257    // The lyric is music21's `tonicPitchNameWithCase`: the text a score
258    // carries is music21's, not a name of the crate's.
259    let named = music21_key_name(&key);
260    Ok((key, format!("{named}: ")))
261}
262
263/// A key as music21 names it: its `tonicPitchNameWithCase`, which spells a
264/// flat `-` where the crate spells one `b`.
265pub(super) fn music21_key_name(key: &Key) -> String {
266    let mut name = key.tonic_pitch_name_with_case();
267    // The step itself may be a `B`, so only what follows it is a flat.
268    let accidental = name.split_off(1);
269    name.push_str(&accidental.replace('b', "-"));
270    name
271}
272
273/// music21's `getBeatFloatOrFrac`: `b2.5` is beat two and a half, `b1.66`
274/// beat one and two thirds, and `b1.66.5` half a third further on.
275fn beat_of(word: &str) -> Result<FloatType> {
276    let text = word.replace('b', "");
277    let parts: Vec<&str> = text.split('.').collect();
278    if parts.len() > 4 {
279        return Err(roman_text_error(format!(
280            "cannot handle specification: {word}"
281        )));
282    }
283    let read = |text: &str| -> Result<FloatType> {
284        text.parse::<FloatType>()
285            .map_err(|_| roman_text_error(format!("cannot read the beat {word:?}")))
286    };
287    let main = read(parts[0])?;
288    // A third or a sixth written to two places is the fraction itself.
289    let mut fraction = 0.0;
290    let mut denominator = 1.0;
291    if parts.len() > 1 {
292        fraction = read(&format!(".{}", parts[1]))?;
293        for (numerator, over) in [(1.0, 3.0), (2.0, 3.0), (1.0, 6.0), (5.0, 6.0)] {
294            if (fraction - numerator / over).abs() <= 1e-2 {
295                fraction = numerator / over;
296            }
297        }
298        denominator = crate::duration::limited_fraction(fraction, 1_000_000)
299            .map_or(1.0, |(_, denominator)| denominator as FloatType);
300    }
301    let further = if parts.len() > 2 {
302        op_frac(1.0 / (denominator / read(&format!(".{}", parts[2]))?))
303    } else {
304        0.0
305    };
306    Ok(op_frac(main + fraction + further))
307}
308
309/// What a sounding element is.
310// Small and short-lived: boxing the larger variant would buy nothing.
311#[allow(clippy::large_enum_variant)]
312#[derive(Clone, Debug)]
313enum Sound {
314    Numeral {
315        figure: String,
316        key: Key,
317        sixth: Minor67Default,
318        seventh: Minor67Default,
319        follows_key_change: bool,
320        /// The key of the chord this one pivots to.
321        pivot: Option<Key>,
322    },
323    Rest,
324}
325
326/// A chord or a rest of the analysis, kept apart from the measures so that
327/// a later line can lengthen or tie one in an earlier measure.
328#[derive(Clone, Debug)]
329struct Sounding {
330    sound: Sound,
331    quarter_length: FloatType,
332    /// The verses, a line each; none where there are no lyrics at all.
333    lyric: Option<String>,
334    tie: Option<TieType>,
335}
336
337impl Sounding {
338    fn is_numeral(&self) -> bool {
339        matches!(self.sound, Sound::Numeral { .. })
340    }
341
342    fn element(&self) -> Result<StreamElement> {
343        let duration = Duration::new(self.quarter_length)?;
344        match &self.sound {
345            Sound::Rest => {
346                let mut rest = Rest::new(duration);
347                if let Some(lyric) = &self.lyric {
348                    rest.set_lyric(Some(lyric));
349                }
350                rest.set_tie(self.tie.map(Tie::new));
351                Ok(rest.into())
352            }
353            Sound::Numeral {
354                figure,
355                key,
356                sixth,
357                seventh,
358                ..
359            } => {
360                let numeral =
361                    RomanNumeral::with_minor_defaults(figure, key.clone(), *sixth, *seventh)?;
362                let mut chord: Chord = numeral
363                    .to_chord()?
364                    .with_duration(duration)
365                    .with_numeral(numeral);
366                if let (Some(lyric), Some(singer)) = (&self.lyric, chord.notes_mut().first_mut()) {
367                    singer.set_lyric(Some(lyric))?;
368                }
369                chord.set_tie(self.tie.map(Tie::new));
370                Ok(chord.into())
371            }
372        }
373    }
374}
375
376/// What a measure holds at an offset.
377// Small and short-lived: boxing the larger variant would buy nothing.
378#[allow(clippy::large_enum_variant)]
379#[derive(Clone, Debug)]
380enum Held {
381    /// A chord or rest, by its place among the soundings.
382    Sounding(usize),
383    Element(StreamElement),
384}
385
386#[derive(Clone, Debug, Default)]
387struct Measure {
388    number: IntegerType,
389    suffix: Option<String>,
390    items: Vec<(FloatType, Held)>,
391    left: Option<Barline>,
392    right: Option<Barline>,
393    /// What a pickup leaves out before its first chord, and the last
394    /// measure after its last.
395    padding_left: FloatType,
396    padding_right: FloatType,
397}
398
399/// music21's `PartTranslator`.
400struct Translator {
401    metadata: Metadata,
402    measures: Vec<Measure>,
403    soundings: Vec<Sounding>,
404    meter: TimeSignature,
405    meter_at_last_chord: TimeSignature,
406    meter_set: bool,
407    last_measure_number: IntegerType,
408    last_letters: Vec<String>,
409    previous: Option<usize>,
410    signature: Option<KeySignature>,
411    signature_placed: bool,
412    found_signature: bool,
413    key: Key,
414    prefix: String,
415    sixth: Minor67Default,
416    seventh: Minor67Default,
417    /// The measures under each ending, by the number music21 counts them
418    /// under, in the order they were met.
419    endings: Vec<(u32, Vec<(IntegerType, usize)>)>,
420    // What is true while one measure is read.
421    previous_in_measure: Option<usize>,
422    pivot_possible: bool,
423    key_change: bool,
424    offset: FloatType,
425}
426
427impl Translator {
428    fn new() -> Result<Self> {
429        Ok(Self {
430            metadata: Metadata::new(),
431            measures: Vec::new(),
432            soundings: Vec::new(),
433            meter: TimeSignature::new(4, 4)?,
434            meter_at_last_chord: TimeSignature::new(4, 4)?,
435            meter_set: false,
436            last_measure_number: 0,
437            last_letters: Vec::new(),
438            previous: None,
439            signature: None,
440            signature_placed: true,
441            found_signature: false,
442            key: Key::from_tonic("C")?,
443            prefix: String::new(),
444            sixth: Minor67Default::Cautionary,
445            seventh: Minor67Default::Cautionary,
446            endings: Vec::new(),
447            previous_in_measure: None,
448            pivot_possible: false,
449            key_change: false,
450            offset: 0.0,
451        })
452    }
453
454    fn add(&mut self, sounding: Sounding) -> usize {
455        self.soundings.push(sounding);
456        self.soundings.len() - 1
457    }
458
459    /// Ties the last chord on to a copy of it, which is handed back.
460    fn continued(&mut self, quarter_length: FloatType) -> Option<usize> {
461        let previous = self.previous?;
462        let mut copy = self.soundings[previous].clone();
463        copy.lyric = Some(String::new());
464        copy.quarter_length = quarter_length;
465        copy.tie = Some(TieType::Stop);
466        let held = &mut self.soundings[previous];
467        held.tie = Some(if held.tie.is_none() {
468            TieType::Start
469        } else {
470            TieType::Continue
471        });
472        let made = self.add(copy);
473        self.previous = Some(made);
474        Some(made)
475    }
476
477    /// music21's `appendMeasureToRepeatEndingsDict`.
478    fn note_ending(
479        &mut self,
480        letters: &[String],
481        measure: &mut Measure,
482        place: usize,
483    ) -> Result<()> {
484        if letters.is_empty() {
485            return Ok(());
486        }
487        measure.suffix = Some(letters[0].clone()).filter(|letter| !letter.is_empty());
488        for letter in letters {
489            if letter.is_empty() {
490                continue;
491            }
492            let number = match letter.as_str() {
493                "a" => 1,
494                "b" => 2,
495                "c" => 3,
496                "d" => 4,
497                "e" => 5,
498                "f" => 6,
499                "g" => 7,
500                "h" => 8,
501                _ => {
502                    return Err(roman_text_error(format!(
503                        "Improper repeat letter: {letter}"
504                    )));
505                }
506            };
507            let entry = (measure.number, place);
508            match self.endings.iter_mut().find(|(held, _)| *held == number) {
509                Some((_, measures)) => measures.push(entry),
510                None => self.endings.push((number, vec![entry])),
511            }
512        }
513        Ok(())
514    }
515
516    /// music21's `fillMeasureFromPreviousRn`.
517    fn fill_from_previous(&mut self, measure: &mut Measure) {
518        let length = self.meter_at_last_chord.bar_quarter_length();
519        if let Some(made) = self.continued(length) {
520            let at = measure
521                .items
522                .iter()
523                .map(|(offset, held)| match held {
524                    Held::Sounding(index) => offset + self.soundings[*index].quarter_length,
525                    Held::Element(_) => *offset,
526                })
527                .fold(0.0, FloatType::max);
528            measure.items.push((at, Held::Sounding(made)));
529        }
530    }
531
532    fn measure_length(&self, measure: &Measure) -> FloatType {
533        measure
534            .items
535            .iter()
536            .map(|(offset, held)| match held {
537                Held::Sounding(index) => offset + self.soundings[*index].quarter_length,
538                Held::Element(_) => *offset,
539            })
540            .fold(0.0, FloatType::max)
541    }
542
543    /// A copy of a measure already read, its chords read again in the key
544    /// in force: music21's `_copySingleMeasure`.
545    fn copied(&mut self, source: usize, number: IntegerType) -> Measure {
546        let mut measure = self.measures[source].clone();
547        measure.number = number;
548        for (_, held) in &mut measure.items {
549            let Held::Sounding(index) = held else {
550                continue;
551            };
552            let mut copy = self.soundings[*index].clone();
553            if let Sound::Numeral {
554                figure,
555                key,
556                sixth,
557                seventh,
558                follows_key_change,
559                pivot,
560            } = &mut copy.sound
561            {
562                if *follows_key_change {
563                    self.key = key.clone();
564                } else if let Some(pivot) = pivot {
565                    self.key = pivot.clone();
566                } else if key.tonic().name() != self.key.tonic().name()
567                    || key.mode() != self.key.mode()
568                {
569                    // Read in another key the chord is built afresh, and
570                    // the ties its notes carried are gone.
571                    *key = self.key.clone();
572                    copy.tie = None;
573                }
574                // A chord of another chord is read afresh, with nothing of
575                // what the first reading was told.
576                if figure.contains('/')
577                    && RomanNumeral::new(figure.as_str(), key.clone())
578                        .is_ok_and(|numeral| numeral.secondary().is_some())
579                {
580                    *key = self.key.clone();
581                    *sixth = Minor67Default::default();
582                    *seventh = Minor67Default::default();
583                    *follows_key_change = false;
584                    *pivot = None;
585                    copy.tie = None;
586                }
587            }
588            self.soundings.push(copy);
589            *index = self.soundings.len() - 1;
590        }
591        measure
592    }
593
594    fn last_numeral(&self, measure: &Measure) -> Option<usize> {
595        let mut items: Vec<&(FloatType, Held)> = measure.items.iter().collect();
596        items.sort_by(|left, right| left.0.total_cmp(&right.0));
597        items.into_iter().rev().find_map(|(_, held)| match held {
598            Held::Sounding(index) if self.soundings[*index].is_numeral() => Some(*index),
599            _ => None,
600        })
601    }
602
603    /// music21's `translateMeasureLineToken`.
604    fn measure(
605        &mut self,
606        numbers: &[IntegerType],
607        letters: &[String],
608        variant: bool,
609        data: &str,
610        atoms: &[Atom],
611    ) -> Result<()> {
612        if variant {
613            return Ok(());
614        }
615        let is_copy = data.starts_with('=');
616        if numbers[0] > self.last_measure_number + 1 && self.previous.is_some() {
617            // Measures the file leaves out hold the last chord still.
618            for number in self.last_measure_number + 1..numbers[0] {
619                let mut fill = Measure {
620                    number,
621                    ..Measure::default()
622                };
623                self.fill_from_previous(&mut fill);
624                let letters = self.last_letters.clone();
625                let place = self.measures.len();
626                self.note_ending(&letters, &mut fill, place)?;
627                fill.number = number;
628                self.measures.push(fill);
629            }
630            self.last_measure_number = numbers[0] - 1;
631            self.last_letters = letters.to_vec();
632        }
633
634        if numbers.len() == 1 && is_copy {
635            let (targets, _) = measure_numbers(data.replace('=', "").trim())?;
636            if targets.len() > 1 {
637                return Err(roman_text_error(
638                    "a single measure cannot define a copy operation for multiple measures",
639                ));
640            }
641            let source = self
642                .measures
643                .iter()
644                .position(|measure| measure.number == targets[0])
645                .ok_or_else(|| {
646                    roman_text_error(format!(
647                        "Could not find measure {} to copy from",
648                        targets[0]
649                    ))
650                })?;
651            let measure = self.copied(source, numbers[0]);
652            if let Some(last) = self.last_numeral(&measure) {
653                self.previous = Some(last);
654            }
655            self.last_measure_number = measure.number;
656            self.last_letters = letters.to_vec();
657            self.measures.push(measure);
658        } else if numbers.len() > 1 {
659            let (targets, _) = measure_numbers(data.replace('=', "").trim())?;
660            if targets.len() == 1 {
661                return Err(roman_text_error(
662                    "a multiple measure range cannot copy a single measure",
663                ));
664            }
665            let (start, end) = (targets[0], targets[1]);
666            if numbers[1] - numbers[0] != end - start {
667                return Err(roman_text_error(
668                    "both the source and destination sections need to have the same number of \
669                     measures",
670                ));
671            }
672            if numbers[0] < end {
673                return Err(roman_text_error(
674                    "the source section cannot overlap with the destination section",
675                ));
676            }
677            let mut made: Vec<Measure> = Vec::new();
678            for source in 0..self.measures.len() {
679                let number = self.measures[source].number;
680                if (start..=end).contains(&number) {
681                    made.push(self.copied(source, numbers[0] + number - start));
682                }
683                if number == end {
684                    break;
685                }
686            }
687            let Some(last) = made.last() else {
688                return Err(roman_text_error(format!(
689                    "Could not find measures {start}-{end} to copy from"
690                )));
691            };
692            if let Some(last) = self.last_numeral(last) {
693                self.previous = Some(last);
694            }
695            self.last_measure_number = last.number;
696            self.last_letters = letters.to_vec();
697            self.measures.extend(made);
698        } else {
699            let mut measure = self.single(numbers[0], letters, atoms)?;
700            if self.measure_length(&measure) == 0.0 {
701                self.fill_from_previous(&mut measure);
702            }
703            self.measures.push(measure);
704        }
705        Ok(())
706    }
707
708    /// music21's `translateSingleMeasure`.
709    fn single(
710        &mut self,
711        number: IntegerType,
712        letters: &[String],
713        atoms: &[Atom],
714    ) -> Result<Measure> {
715        let mut measure = Measure {
716            number,
717            ..Measure::default()
718        };
719        let place = self.measures.len();
720        self.note_ending(letters, &mut measure, place)?;
721        self.last_measure_number = number;
722        self.last_letters = letters.to_vec();
723        if !self.meter_set {
724            measure
725                .items
726                .push((0.0, Held::Element(self.meter.clone().into())));
727            self.meter_set = true;
728        }
729        if !self.signature_placed
730            && let Some(signature) = &self.signature
731        {
732            measure
733                .items
734                .push((0.0, Held::Element(signature.clone().into())));
735            self.signature_placed = true;
736        }
737        self.offset = 0.0;
738        self.previous_in_measure = None;
739        self.pivot_possible = false;
740        self.key_change = false;
741        for (index, atom) in atoms.iter().enumerate() {
742            self.atom(atom, &mut measure, index + 1 == atoms.len())?;
743        }
744        // The last chord read, in whatever measure it stands, lasts what is
745        // left of this bar.
746        if let Some(previous) = self.previous {
747            self.soundings[previous].quarter_length = self.meter.bar_quarter_length() - self.offset;
748        }
749        Ok(measure)
750    }
751
752    /// Gives the chord before this one in the measure the length up to
753    /// here.
754    fn end_previous(&mut self, measure: &Measure) -> Result<()> {
755        let Some(previous) = self.previous_in_measure else {
756            return Ok(());
757        };
758        let start = measure
759            .items
760            .iter()
761            .find_map(|(offset, held)| match held {
762                Held::Sounding(index) if *index == previous => Some(*offset),
763                _ => None,
764            })
765            .unwrap_or(0.0);
766        let length = self.offset - start;
767        if length <= 0.0 {
768            return Err(roman_text_error("too many notes in this measure"));
769        }
770        self.soundings[previous].quarter_length = length;
771        Ok(())
772    }
773
774    /// music21's `translateSingleMeasureAtom`.
775    fn atom(&mut self, atom: &Atom, measure: &mut Measure, is_last: bool) -> Result<()> {
776        let at_start = if measure.number <= 1 {
777            0.0
778        } else {
779            self.offset
780        };
781        match atom {
782            Atom::Key(word) => {
783                self.set_key(word)?;
784                measure.items.push((
785                    at_start,
786                    Held::Element(StreamElement::Key(self.key.clone())),
787                ));
788                self.found_signature = true;
789            }
790            Atom::AnalyticKey(word) if !self.found_signature => {
791                self.set_key(word)?;
792                measure.items.push((
793                    at_start,
794                    Held::Element(StreamElement::Key(self.key.clone())),
795                ));
796                self.found_signature = true;
797            }
798            Atom::AnalyticKey(word) => self.set_key(word)?,
799            Atom::KeySignature(word) => {
800                let sharps = word[2..].parse::<IntegerType>().map_err(|_| {
801                    roman_text_error(format!("cannot get a key signature from {word}"))
802                })?;
803                measure
804                    .items
805                    .push((at_start, Held::Element(KeySignature::new(sharps).into())));
806                self.found_signature = true;
807            }
808            Atom::Beat(word) => {
809                let offset = self.meter.offset_from_beat(beat_of(word)?).unwrap_or(0.0);
810                // A measure that opens after its first beat holds the last
811                // chord until then.
812                if self.previous_in_measure.is_none()
813                    && offset > 0.0
814                    && let Some(made) = self.continued(offset)
815                {
816                    self.previous_in_measure = Some(made);
817                    measure.items.push((0.0, Held::Sounding(made)));
818                }
819                self.pivot_possible = false;
820                self.offset = offset;
821            }
822            Atom::NoChord => {
823                self.meter_at_last_chord = self.meter.clone();
824                measure.items.push((
825                    self.offset,
826                    Held::Element(ChordSymbol::no_chord(None).into()),
827                ));
828                if !self.pivot_possible {
829                    self.end_previous(measure)?;
830                    self.prefix.clear();
831                    let made = self.add(Sounding {
832                        sound: Sound::Rest,
833                        quarter_length: 1.0,
834                        lyric: None,
835                        tie: None,
836                    });
837                    measure.items.push((self.offset, Held::Sounding(made)));
838                    self.previous_in_measure = Some(made);
839                    self.previous = Some(made);
840                }
841            }
842            Atom::Chord(figure) => {
843                self.meter_at_last_chord = self.meter.clone();
844                // Read now, so that a figure that cannot be read is refused
845                // where it stands.
846                let _ = RomanNumeral::with_minor_defaults(
847                    figure.as_str(),
848                    self.key.clone(),
849                    self.sixth,
850                    self.seventh,
851                )?;
852                let follows_key_change = std::mem::take(&mut self.key_change);
853                if self.pivot_possible {
854                    // Two chords with no beat between are one chord read in
855                    // two keys.
856                    if let Some(previous) = self.previous_in_measure {
857                        let prefix = std::mem::take(&mut self.prefix);
858                        let held = &mut self.soundings[previous];
859                        held.lyric = Some(format!(
860                            "{}//{prefix}{figure}",
861                            held.lyric.clone().unwrap_or_default()
862                        ));
863                        if let Sound::Numeral { pivot, .. } = &mut held.sound {
864                            *pivot = Some(self.key.clone());
865                        }
866                    }
867                    self.pivot_possible = false;
868                } else {
869                    self.end_previous(measure)?;
870                    let prefix = std::mem::take(&mut self.prefix);
871                    let made = self.add(Sounding {
872                        sound: Sound::Numeral {
873                            figure: figure.clone(),
874                            key: self.key.clone(),
875                            sixth: self.sixth,
876                            seventh: self.seventh,
877                            follows_key_change,
878                            pivot: None,
879                        },
880                        quarter_length: 1.0,
881                        lyric: Some(format!("{prefix}{figure}")),
882                        tie: None,
883                    });
884                    measure.items.push((self.offset, Held::Sounding(made)));
885                    self.previous_in_measure = Some(made);
886                    self.previous = Some(made);
887                    self.pivot_possible = true;
888                }
889            }
890            Atom::RepeatStart | Atom::RepeatStop => {
891                let stop = *atom == Atom::RepeatStop;
892                if (stop && is_last)
893                    || (stop
894                        && self.offset != 0.0
895                        && self.meter.bar_quarter_length() == self.offset)
896                {
897                    measure.right = Some(Barline::repeat(RepeatDirection::End, None));
898                } else if self.offset == 0.0 && !stop {
899                    measure.left = Some(Barline::repeat(RepeatDirection::Start, None));
900                }
901            }
902            Atom::Other => {}
903        }
904        Ok(())
905    }
906
907    fn set_key(&mut self, word: &str) -> Result<()> {
908        let (key, prefix) = key_and_prefix(word)?;
909        self.key = key;
910        self.prefix.push_str(&prefix);
911        self.key_change = true;
912        Ok(())
913    }
914
915    /// music21's `translateOneLineToken`.
916    fn token(&mut self, token: &Token) -> Result<()> {
917        let (tag, data) = match token {
918            Token::Measure {
919                numbers,
920                letters,
921                variant,
922                data,
923                atoms,
924            } => return self.measure(numbers, letters, *variant, data, atoms),
925            Token::Tagged { tag, data } => (tag.to_lowercase(), data),
926        };
927        let text = |metadata: &mut Metadata, name: &str| {
928            metadata.add(name, MetadataValue::new(data.clone()));
929        };
930        match tag.as_str() {
931            "title" => text(&mut self.metadata, "title"),
932            "work" | "madrigal" | "piece" => text(&mut self.metadata, "alternativeTitle"),
933            "composer" => self.metadata.add_contributor("composer", data.clone()),
934            "movement" => text(&mut self.metadata, "movementNumber"),
935            "analyst" => self.metadata.add_contributor("analyst", data.clone()),
936            "proofreader" | "proof reader" => {
937                self.metadata.add_contributor("proofreader", data.clone());
938            }
939            "timesignature" | "time signature" => {
940                let meter = match data.as_str() {
941                    "C" | "c" => Some(TimeSignature::common()),
942                    "C|" | "Cut" | "cut" => Some(TimeSignature::cut()),
943                    other => TimeSignature::from_ratio_string(other).ok(),
944                };
945                // A meter that cannot be read leaves the one in force.
946                if let Some(meter) = meter {
947                    self.meter = meter;
948                    self.meter_set = false;
949                }
950            }
951            "keysignature" | "key signature" => {
952                let sharps = match data.as_str() {
953                    "" => 0,
954                    "Bb" => -1,
955                    other => other.parse::<IntegerType>().map_err(|_| {
956                        roman_text_error(format!("Cannot parse key signature: {other:?}"))
957                    })?,
958                };
959                self.signature = Some(KeySignature::new(sharps));
960                self.signature_placed = false;
961                self.found_signature = true;
962            }
963            "sixthminor" | "sixth minor" | "seventhminor" | "seventh minor" => {
964                let sixth = tag.starts_with("sixth");
965                let reading = match data.to_lowercase().as_str() {
966                    "flat" => Minor67Default::Flat,
967                    "sharp" => Minor67Default::Sharp,
968                    "quality" => Minor67Default::Quality,
969                    "courtesy" | "cautionary" => Minor67Default::Cautionary,
970                    "harmonic" if sixth => Minor67Default::Flat,
971                    "harmonic" => Minor67Default::Sharp,
972                    other => {
973                        return Err(roman_text_error(format!(
974                            "Cannot parse setting vi or vii parsing: {other:?}"
975                        )));
976                    }
977                };
978                if sixth {
979                    self.sixth = reading;
980                } else {
981                    self.seventh = reading;
982                }
983            }
984            _ => {}
985        }
986        Ok(())
987    }
988
989    /// music21's `fixPickupMeasure`: a measure numbered nought that opens
990    /// after its first beat starts where its first chord does, and the last
991    /// measure is cut short by as much.
992    fn fix_pickup(&mut self) {
993        let Some(first) = self.measures.iter().position(|measure| measure.number == 0) else {
994            return;
995        };
996        let is_chord = |held: &Held, soundings: &[Sounding]| match held {
997            Held::Sounding(index) => soundings[*index].is_numeral(),
998            Held::Element(element) => matches!(element, StreamElement::ChordSymbol(_)),
999        };
1000        let mut chords: Vec<FloatType> = self.measures[first]
1001            .items
1002            .iter()
1003            .filter(|(_, held)| is_chord(held, &self.soundings))
1004            .map(|(offset, _)| *offset)
1005            .collect();
1006        chords.sort_by(FloatType::total_cmp);
1007        let Some(padding) = chords.first().copied().filter(|offset| *offset != 0.0) else {
1008            return;
1009        };
1010        for (offset, _) in &mut self.measures[first].items {
1011            if *offset >= padding {
1012                *offset -= padding;
1013            }
1014        }
1015        self.measures[first].padding_left = padding;
1016        let last = self.measures.len() - 1;
1017        if last == first {
1018            return;
1019        }
1020        let mut sorted: Vec<&(FloatType, Held)> = self.measures[last].items.iter().collect();
1021        sorted.sort_by(|left, right| left.0.total_cmp(&right.0));
1022        let closing = sorted.into_iter().rev().find_map(|(_, held)| match held {
1023            Held::Sounding(index) if self.soundings[*index].is_numeral() => Some(Some(*index)),
1024            Held::Element(StreamElement::ChordSymbol(_)) => Some(None),
1025            _ => None,
1026        });
1027        if let Some(Some(index)) = closing
1028            && self.soundings[index].quarter_length > padding
1029        {
1030            let length = self.measure_length(&self.measures[last]);
1031            self.soundings[index].quarter_length -= length - padding;
1032            self.measures[last].padding_right = length - padding;
1033        }
1034    }
1035
1036    /// The part, once every line is read: music21's `translateTokens`.
1037    fn part(mut self) -> Result<(Stream, Metadata)> {
1038        self.fix_pickup();
1039
1040        // music21's `_consolidateRepeatEndings`: each run of measures in a
1041        // row under one ending is a bracket.
1042        let mut brackets: Vec<(Vec<usize>, u32)> = Vec::new();
1043        for (number, measures) in &self.endings {
1044            let mut run: Vec<usize> = Vec::new();
1045            let mut last: Option<IntegerType> = None;
1046            for (measure_number, place) in measures {
1047                if last.is_some_and(|last| *measure_number > last + 1) {
1048                    brackets.push((std::mem::take(&mut run), *number));
1049                }
1050                run.push(*place);
1051                last = Some(*measure_number);
1052            }
1053            if !run.is_empty() {
1054                brackets.push((run, *number));
1055            }
1056        }
1057        let mut endings: Vec<Option<Ending>> = vec![None; self.measures.len()];
1058        for (places, number) in &brackets {
1059            for (index, place) in places.iter().enumerate() {
1060                if let Some(slot) = endings.get_mut(*place) {
1061                    *slot = Some(Ending::new(
1062                        vec![*number],
1063                        index == 0,
1064                        index + 1 == places.len(),
1065                    ));
1066                }
1067            }
1068            if *number == 1
1069                && let Some(measure) = places
1070                    .last()
1071                    .and_then(|place| self.measures.get_mut(*place))
1072                && measure.right.is_none()
1073            {
1074                measure.right = Some(Barline::repeat(RepeatDirection::End, None));
1075            }
1076        }
1077
1078        let mut events = Vec::new();
1079        let mut offset = 0.0;
1080        for (measure, ending) in self.measures.iter().zip(endings) {
1081            let mut held = Vec::new();
1082            for (at, item) in &measure.items {
1083                let element = match item {
1084                    Held::Sounding(index) => self.soundings[*index].element()?,
1085                    Held::Element(element) => element.clone(),
1086                };
1087                held.push(StreamEvent::new(*at, element));
1088            }
1089            let mut stream =
1090                Stream::with_kind(StreamKind::Measure).with_events(sorted_events(held));
1091            stream.set_number(measure.number);
1092            stream.set_number_suffix(measure.suffix.clone());
1093            stream.set_padding_left(measure.padding_left);
1094            stream.set_padding_right(measure.padding_right);
1095            stream.set_left_barline(measure.left.clone());
1096            stream.set_right_barline(measure.right.clone());
1097            stream.set_ending(ending);
1098            let length = stream.end_offset();
1099            events.push(StreamEvent::new(offset, stream));
1100            offset = op_frac(offset + length);
1101        }
1102        let mut part = Stream::with_kind(StreamKind::Part).with_events(events);
1103        // music21 goes on where a measure cannot be beamed.
1104        let _ = make_beams(&mut part);
1105        make_accidentals(&mut part);
1106        Ok((part, self.metadata))
1107    }
1108}
1109
1110fn score_of(tokens: &[Token]) -> Result<Stream> {
1111    let mut translator = Translator::new()?;
1112    for token in tokens {
1113        translator.token(token)?;
1114    }
1115    let (part, metadata) = translator.part()?;
1116    let mut score =
1117        Stream::with_kind(StreamKind::Score).with_events(vec![StreamEvent::new(0.0, part)]);
1118    score.set_metadata(Some(metadata));
1119    Ok(score)
1120}
1121
1122/// Reads a RomanText analysis into a score: music21's
1123/// `converter.parse` of one.
1124///
1125/// The score holds one part of measures, and in each the chords the
1126/// numerals stand for, in the key they were written in, each carrying its
1127/// figure as its lyric -- the key too where it has just changed, as
1128/// `G: V7`. A chord lasts until the next, or to the end of its bar; one
1129/// held into a measure that opens after its first beat is tied there. `NC`
1130/// is a rest under a no-chord symbol. A measure written `m5 = m1` is a copy
1131/// of another, its chords read in the key now in force.
1132///
1133/// A file of several `Movement:` lines comes back as an opus of scores, the
1134/// lines before the first heading each of them.
1135///
1136/// ```
1137/// use music21_rs::romantext::from_roman_text;
1138///
1139/// let score = from_roman_text("Time Signature: 3/4\nm1 G: I b3 V6\nm2 I\n")?;
1140/// let part = score.parts()[0];
1141/// assert_eq!(part.measures().len(), 2);
1142/// let names: Vec<String> = part.measures()[0]
1143///     .pitches()
1144///     .iter()
1145///     .map(|pitch| pitch.name())
1146///     .collect();
1147/// assert_eq!(names, ["G", "B", "D", "F#", "A", "D"]);
1148/// # Ok::<(), music21_rs::Error>(())
1149/// ```
1150pub fn from_roman_text(text: &str) -> Result<Stream> {
1151    let tokens = tokenize(text)?;
1152    let movements = tokens.iter().filter(|token| token.is_movement()).count();
1153    if movements < 2 {
1154        return score_of(&tokens);
1155    }
1156    // Each movement takes the lines before the first heading as its own.
1157    let mut pieces: Vec<Vec<Token>> = Vec::new();
1158    let mut current: Vec<Token> = Vec::new();
1159    for token in tokens {
1160        if token.is_movement() {
1161            pieces.push(std::mem::take(&mut current));
1162        }
1163        current.push(token);
1164    }
1165    if !current.is_empty() {
1166        pieces.push(current);
1167    }
1168    let head = pieces.remove(0);
1169    let mut events = Vec::new();
1170    let mut offset = 0.0;
1171    for piece in pieces {
1172        let tokens: Vec<Token> = head.iter().cloned().chain(piece).collect();
1173        let score = score_of(&tokens)?;
1174        let length = score.end_offset();
1175        events.push(StreamEvent::new(offset, score));
1176        offset = op_frac(offset + length);
1177    }
1178    Ok(Stream::with_kind(StreamKind::Opus).with_events(events))
1179}
1180
1181/// Writes a score's roman numerals as a RomanText analysis, as music21's
1182/// `romanText.writeRoman.RnWriter` writes one, a line after each line.
1183///
1184/// The header names the composer, the title (with the movement's number and
1185/// name), the analyst and the proofreader, as the score's metadata gives
1186/// them. Then measure by measure: a `Time Signature:` line where a meter
1187/// stands, and a line of the measure's numerals -- each chord standing for
1188/// one ([`Chord::numeral`]) at its beat, `b2.5`, beat one left unsaid, the
1189/// key written before a numeral where it is not the key of the one before,
1190/// and a chord tied from the one before passed over -- with `||:` and
1191/// `:||` where a repeat starts and ends. A score is written from its first
1192/// part, an opus score by score.
1193///
1194/// ```
1195/// use music21_rs::romantext::{from_roman_text, to_roman_text};
1196///
1197/// let score = from_roman_text("Time Signature: 3/4\nm1 G: I b3 V6\nm2 I\n")?;
1198/// let written = to_roman_text(&score)?;
1199/// assert!(written.ends_with("Time Signature: 3/4\nm1 G: I b3 V6\nm2 I\n"));
1200/// # Ok::<(), music21_rs::Error>(())
1201/// ```
1202///
1203/// # Errors
1204///
1205/// A numeral standing where a meter has no beat.
1206pub fn to_roman_text(stream: &Stream) -> Result<String> {
1207    let mut lines = Vec::new();
1208    write_lines(stream, &mut lines)?;
1209    Ok(lines.iter().map(|line| format!("{line}\n")).collect())
1210}
1211
1212/// `RnWriter`'s `combinedList` for one stream.
1213fn write_lines(stream: &Stream, lines: &mut Vec<String>) -> Result<()> {
1214    if stream.kind() == StreamKind::Opus {
1215        for event in stream.events() {
1216            if let StreamElement::Stream(score) = event.element() {
1217                write_lines(score, lines)?;
1218                lines.push("\n".to_string());
1219            }
1220        }
1221        return Ok(());
1222    }
1223    let wrapped;
1224    let container: &Stream = match stream.kind() {
1225        StreamKind::Score => stream.parts().first().copied().unwrap_or(stream),
1226        StreamKind::Part | StreamKind::PartStaff => stream,
1227        StreamKind::Measure => {
1228            let mut part = Stream::with_kind(StreamKind::Part);
1229            part.insert(0.0, stream.clone());
1230            wrapped = part;
1231            &wrapped
1232        }
1233        _ => {
1234            // music21 appends everything the stream holds to one measure.
1235            let mut measure = Stream::with_kind(StreamKind::Measure);
1236            for event in stream.events() {
1237                measure.push(event.element().clone());
1238            }
1239            let mut part = Stream::with_kind(StreamKind::Part);
1240            part.insert(0.0, measure);
1241            wrapped = part;
1242            &wrapped
1243        }
1244    };
1245
1246    let mut composer = "Composer unknown".to_string();
1247    let mut title = "Title unknown".to_string();
1248    let mut analyst = String::new();
1249    let mut proofreader = String::new();
1250    if let Some(metadata) = stream.metadata() {
1251        if let Some(prepared) = written_title(metadata) {
1252            title = prepared;
1253        }
1254        if let Some(name) = contributor(metadata, "composer") {
1255            composer = name;
1256        }
1257        if let Some(name) = contributor(metadata, "analyst") {
1258            analyst = name;
1259        }
1260        if let Some(name) = contributor(metadata, "proofreader") {
1261            proofreader = name;
1262        }
1263    }
1264    lines.push(format!("Composer: {composer}"));
1265    lines.push(format!("Title: {title}"));
1266    lines.push(format!("Analyst: {analyst}"));
1267    lines.push(format!("Proofreader: {proofreader}"));
1268    lines.push(String::new());
1269
1270    // A meter standing in the part itself is in force from its start; with
1271    // none anywhere, music21 puts a 4/4 there.
1272    let mut meter = container
1273        .events()
1274        .iter()
1275        .find_map(|event| match event.element() {
1276            StreamElement::TimeSignature(meter) => Some(meter.clone()),
1277            _ => None,
1278        });
1279    let any_meter = container
1280        .leaves()
1281        .iter()
1282        .any(|(_, element)| matches!(element, StreamElement::TimeSignature(_)));
1283    if !any_meter {
1284        meter = Some(TimeSignature::new(4, 4)?);
1285    }
1286
1287    let mut key_string = String::new();
1288    for measure in container.measures() {
1289        let meters: Vec<(FloatType, &TimeSignature)> = measure
1290            .events()
1291            .iter()
1292            .filter_map(|event| match event.element() {
1293                StreamElement::TimeSignature(meter) => Some((event.offset(), meter)),
1294                _ => None,
1295            })
1296            .collect();
1297        if let Some((_, first)) = meters.first() {
1298            lines.push(format!("Time Signature: {}", first.ratio_string()));
1299            if meters.len() > 1 {
1300                let rest: Vec<String> = meters[1..]
1301                    .iter()
1302                    .map(|(_, meter)| format!("'{}'", meter.ratio_string()))
1303                    .collect();
1304                lines.push(format!(
1305                    "Note: further time signature change(s) unprocessed: [{}]",
1306                    rest.join(", ")
1307                ));
1308            }
1309        }
1310        let number = format!(
1311            "{}{}",
1312            measure.number(),
1313            measure.number_suffix().unwrap_or("")
1314        );
1315        let mut line = String::new();
1316        if measure
1317            .left_barline()
1318            .is_some_and(|barline| barline.repeat_direction() == Some(RepeatDirection::Start))
1319        {
1320            push_chord_string(&mut line, &number, 1.0, "||:");
1321        }
1322        let mut last_beat = None;
1323        for event in measure.events() {
1324            let StreamElement::Chord(chord) = event.element() else {
1325                continue;
1326            };
1327            let Some(numeral) = chord.numeral() else {
1328                continue;
1329            };
1330            let at = event.offset();
1331            // The meter in force: the last standing at or before the
1332            // numeral in this measure, else the one carried in.
1333            let in_force = meters
1334                .iter()
1335                .rev()
1336                .find(|(offset, _)| *offset <= at)
1337                .map(|(_, meter)| (*meter).clone())
1338                .or_else(|| meter.clone())
1339                .ok_or_else(|| roman_text_error("a roman numeral with no meter in force"))?;
1340            let beat = in_force.beat_proportion(op_frac(at + measure.padding_left()))?;
1341            last_beat = Some(beat);
1342            if chord
1343                .tie()
1344                .is_some_and(|tie| tie.tie_type() != TieType::Start)
1345            {
1346                continue;
1347            }
1348            let key = numeral.key().tonic_pitch_name_with_case().replace('-', "b");
1349            let written = if key == key_string {
1350                numeral.figure().to_string()
1351            } else {
1352                key_string = key.clone();
1353                format!("{key}: {}", numeral.figure())
1354            };
1355            push_chord_string(&mut line, &number, beat, &written);
1356        }
1357        if measure
1358            .right_barline()
1359            .is_some_and(|barline| barline.repeat_direction() == Some(RepeatDirection::End))
1360        {
1361            push_chord_string(&mut line, &number, last_beat.unwrap_or(1.0), ":||");
1362        }
1363        if !line.is_empty() {
1364            lines.push(line);
1365        }
1366        if let Some((_, last)) = meters.last() {
1367            meter = Some((*last).clone());
1368        }
1369    }
1370    Ok(())
1371}
1372
1373/// `rnString`: a chord string added to a measure's line, its beat said
1374/// where it is not the first.
1375fn push_chord_string(line: &mut String, number: &str, beat: FloatType, chord: &str) {
1376    if line.is_empty() {
1377        line.push('m');
1378        line.push_str(number);
1379    }
1380    let beat = int_beat(beat);
1381    if beat == "1" {
1382        line.push_str(&format!(" {chord}"));
1383    } else {
1384        line.push_str(&format!(" b{beat} {chord}"));
1385    }
1386}
1387
1388/// `intBeat`: a beat as a whole number where it is one, else rounded to two
1389/// places, written as Python writes a float.
1390fn int_beat(beat: FloatType) -> String {
1391    if beat.fract() == 0.0 {
1392        return format!("{}", beat as i64);
1393    }
1394    let rounded = (beat * 100.0).round() / 100.0;
1395    let written = format!("{rounded}");
1396    if written.contains('.') {
1397        written
1398    } else {
1399        format!("{written}.0")
1400    }
1401}
1402
1403/// `prepTitle`: the best title, the movement's number and its name where
1404/// that is not the title.
1405fn written_title(metadata: &Metadata) -> Option<String> {
1406    let mut parts = Vec::new();
1407    if let Some(title) = ["title", "popularTitle", "alternativeTitle", "movementName"]
1408        .iter()
1409        .find_map(|name| metadata.first_text(name))
1410    {
1411        parts.push(title.to_string());
1412    }
1413    if let Some(number) = metadata.first_text("movementNumber") {
1414        parts.push(format!("- No.{number}:"));
1415    }
1416    if let Some(name) = metadata.first_text("movementName")
1417        && metadata.first_text("title") != Some(name)
1418    {
1419        parts.push(name.to_string());
1420    }
1421    (!parts.is_empty()).then(|| parts.join(" "))
1422}
1423
1424/// The first contributor in a role, filed under the role or among the
1425/// other contributors.
1426fn contributor(metadata: &Metadata, role: &str) -> Option<String> {
1427    metadata
1428        .first_text(role)
1429        .or_else(|| {
1430            metadata
1431                .get("otherContributor")
1432                .iter()
1433                .find(|value| value.role() == Some(role))
1434                .map(MetadataValue::text)
1435        })
1436        .filter(|name| !name.is_empty())
1437        .map(str::to_string)
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442    use super::*;
1443
1444    fn lengths(measure: &Stream) -> Vec<(FloatType, FloatType)> {
1445        measure
1446            .events()
1447            .iter()
1448            .filter(|event| event.element().is_note_or_chord())
1449            .map(|event| (event.offset(), event.element().quarter_length()))
1450            .collect()
1451    }
1452
1453    #[test]
1454    fn words_of_a_measure_are_told_apart() {
1455        assert_eq!(
1456            atoms_of("C: I b2.5 V6 || Bb;: KS-2 NC ||: :|| ?(d: ii"),
1457            [
1458                Atom::AnalyticKey("C:".to_string()),
1459                Atom::Chord("I".to_string()),
1460                Atom::Beat("b2.5".to_string()),
1461                Atom::Chord("V6".to_string()),
1462                Atom::Other,
1463                Atom::Key("Bb;:".to_string()),
1464                Atom::KeySignature("KS-2".to_string()),
1465                Atom::NoChord,
1466                Atom::RepeatStart,
1467                Atom::RepeatStop,
1468                Atom::Other,
1469                Atom::Chord("ii".to_string()),
1470            ]
1471        );
1472    }
1473
1474    #[test]
1475    fn a_beat_may_be_a_third_written_to_two_places() {
1476        assert_eq!(beat_of("b2").unwrap(), 2.0);
1477        assert_eq!(beat_of("b2.5").unwrap(), 2.5);
1478        assert!((beat_of("b1.66").unwrap() - 5.0 / 3.0).abs() < 1e-9);
1479        assert!((beat_of("b1.66.5").unwrap() - 11.0 / 6.0).abs() < 1e-9);
1480    }
1481
1482    #[test]
1483    fn a_chord_lasts_until_the_next_or_the_barline() {
1484        let score = from_roman_text("Time Signature: 4/4\nm1 C: I b3 V\nm2 I\n").unwrap();
1485        let part = score.parts()[0];
1486        let measures = part.measures();
1487        assert_eq!(lengths(measures[0]), [(0.0, 2.0), (2.0, 2.0)]);
1488        assert_eq!(lengths(measures[1]), [(0.0, 4.0)]);
1489    }
1490
1491    #[test]
1492    fn a_measure_opening_late_holds_the_last_chord_tied() {
1493        let score = from_roman_text("m1 C: I\nm2 b3 V\n").unwrap();
1494        let part = score.parts()[0];
1495        let second = part.measures()[1];
1496        assert_eq!(lengths(second), [(0.0, 2.0), (2.0, 2.0)]);
1497        let StreamElement::Chord(held) = second.events()[0].element() else {
1498            panic!("the measure opens with the chord held over");
1499        };
1500        assert_eq!(held.tie().map(Tie::tie_type), Some(TieType::Stop));
1501    }
1502
1503    #[test]
1504    fn a_measure_may_copy_another() {
1505        let score = from_roman_text("m1 C: I b3 V\nm2 IV\nm3 = m1\n").unwrap();
1506        let part = score.parts()[0];
1507        let measures = part.measures();
1508        assert_eq!(measures.len(), 3);
1509        assert_eq!(measures[2].number(), 3);
1510        assert_eq!(measures[2].pitches(), measures[0].pitches());
1511    }
1512
1513    #[test]
1514    fn text_with_no_measure_is_refused() {
1515        assert!(from_roman_text("Composer: nobody\n").is_err());
1516    }
1517}