1use crate::chord::Chord;
26use crate::defaults::{FloatType, IntegerType};
27use crate::duration::Duration;
28use crate::error::{Error, Result};
29use crate::key::{Key, convert_key_string_to_music21_key_string};
30use crate::makenotation::op_frac;
31use crate::metadata::{Metadata, MetadataValue};
32use crate::meter::TimeSignature;
33use crate::notation::Tie;
34use crate::rest::Rest;
35use crate::roman::RomanNumeral;
36use crate::stream::{Stream, StreamElement, StreamKind};
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct CtLabels {
43 pub roman_numerals: bool,
45 pub subsections: bool,
48}
49
50impl Default for CtLabels {
51 fn default() -> Self {
52 Self {
53 roman_numerals: true,
54 subsections: true,
55 }
56 }
57}
58
59pub fn from_clercq_temperley(text: &str) -> Result<Stream> {
76 CtSong::parse(text).to_part()
77}
78
79pub fn from_clercq_temperley_with(text: &str, labels: CtLabels) -> Result<Stream> {
85 CtSong::parse(text).to_part_with(labels)
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Eq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub struct CtSong {
92 text: String,
93 lines: Vec<String>,
94 rules: Vec<CtRule>,
95}
96
97impl CtSong {
98 pub fn parse(text: &str) -> Self {
105 let lines: Vec<String> = text
106 .split('\n')
107 .filter(|line| !line.is_empty())
108 .map(|line| line.trim().to_string())
109 .collect();
110 let rules = lines
111 .iter()
112 .filter(|line| {
113 line.split_whitespace()
114 .next()
115 .is_some_and(|word| word.ends_with(':'))
116 })
117 .map(|line| CtRule::new(line))
118 .collect();
119 Self {
120 text: lines.join("\n"),
121 lines,
122 rules,
123 }
124 }
125
126 pub fn text(&self) -> &str {
128 &self.text
129 }
130
131 pub fn lines(&self) -> &[String] {
133 &self.lines
134 }
135
136 pub fn title(&self) -> String {
141 self.lines
142 .first()
143 .map(|line| line.replace('%', "").trim().to_string())
144 .unwrap_or_default()
145 }
146
147 pub fn comments(&self) -> Vec<Vec<String>> {
151 let mut comments = Vec::new();
152 for line in self.lines.iter().skip(1) {
153 let Some(at) = line.find('%') else { continue };
154 let said = line[at + 1..].trim().to_string();
155 match line.split_whitespace().next() {
156 Some(word) if word.ends_with(':') => comments.push(vec![word.to_string(), said]),
157 _ => comments.push(vec![said]),
158 }
159 }
160 comments
161 }
162
163 pub fn rules(&self) -> &[CtRule] {
166 &self.rules
167 }
168
169 pub fn rule(&self, name: &str) -> Option<&CtRule> {
171 self.rules.iter().find(|rule| rule.left_hand_side() == name)
172 }
173
174 pub fn home_time_signature(&self) -> Result<Option<TimeSignature>> {
183 for atom in self.song_head() {
184 if !atom.contains('[') {
185 return TimeSignature::from_ratio_string("4/4").map(Some);
186 }
187 if atom.contains('/') {
188 return TimeSignature::from_ratio_string(bracketed(atom)).map(Some);
189 }
190 }
191 Ok(None)
192 }
193
194 pub fn home_key(&self) -> Result<Option<Key>> {
202 for atom in self.song_head() {
203 if !atom.contains('[') {
204 return Key::from_tonic("C").map(Some);
205 }
206 if !atom.contains('/') {
207 let named = convert_key_string_to_music21_key_string(bracketed(atom));
208 return Key::from_tonic(&named).map(Some);
209 }
210 }
211 Ok(None)
212 }
213
214 fn song_head(&self) -> Vec<&str> {
217 self.lines
218 .iter()
219 .find(|line| line.starts_with("S:"))
220 .map(|line| line.split_whitespace().skip(1).take(2).collect())
221 .unwrap_or_default()
222 }
223
224 pub fn to_part(&self) -> Result<Stream> {
231 self.to_part_with(CtLabels::default())
232 }
233
234 pub fn to_part_with(&self, labels: CtLabels) -> Result<Stream> {
240 let mut expansion = Expansion::of(self, labels)?;
241 let song = self
242 .rules
243 .iter()
244 .position(|rule| rule.left_hand_side() == "S")
245 .ok_or_else(|| ct_error("a song with no S: rule to play it through"))?;
246 let places = expansion.expand(song, None, None)?;
247 let mut part = Stream::with_kind(StreamKind::Part);
248 let mut at = 0.0;
249 for (index, place) in places.iter().enumerate() {
250 let measure = expansion.built[*place]
251 .as_measure(IntegerType::try_from(index + 1).unwrap_or(IntegerType::MAX))?;
252 let length = measure.end_offset();
253 part.insert(at, measure);
254 at = op_frac(at + length);
255 }
256 let mut metadata = Metadata::new();
257 metadata.add("title", MetadataValue::new(self.title()));
258 part.set_metadata(Some(metadata));
259 Ok(part)
260 }
261}
262
263#[derive(Clone, Debug, Default, PartialEq, Eq)]
265#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
266pub struct CtRule {
267 text: String,
268 left_hand_side: String,
269 music_text: String,
270}
271
272impl CtRule {
273 pub fn new(text: &str) -> Self {
275 let left_hand_side = match text.split_whitespace().next() {
276 Some(word) if word.ends_with(':') => text
277 .split_once(':')
278 .map_or(String::new(), |(before, _)| before.trim().to_string()),
279 _ => String::new(),
280 };
281 let mut music_text = String::new();
285 if !text.is_empty() {
286 let after = text.get(left_hand_side.len() + 1..).unwrap_or_default();
287 let said = after.split('%').next().unwrap_or(after);
288 music_text = said.trim().to_string();
289 }
290 Self {
291 text: text.to_string(),
292 left_hand_side,
293 music_text,
294 }
295 }
296
297 pub fn text(&self) -> &str {
299 &self.text
300 }
301
302 pub fn left_hand_side(&self) -> &str {
304 &self.left_hand_side
305 }
306
307 pub fn music_text(&self) -> &str {
310 &self.music_text
311 }
312
313 pub fn comment(&self) -> Option<&str> {
315 self.text.find('%').map(|at| self.text[at + 1..].trim())
316 }
317
318 pub fn section_name(&self) -> String {
323 let name = &self.left_hand_side;
324 let rest = |at: usize| name.get(at..).unwrap_or_default();
325 for (abbreviation, said) in [
326 ("In", "Introduction"),
327 ("Br", "Bridge"),
328 ("Vr", "Verse"),
329 ("Ch", "Chorus"),
330 ("Tg", "Tag"),
331 ] {
332 if name.contains(abbreviation) {
333 return format!("{said}{}", rest(2));
334 }
335 }
336 match name.as_str() {
337 "S" => "Song".to_string(),
338 "Fadeout" => "Fadeout".to_string(),
339 other => other.to_string(),
340 }
341 }
342
343 pub fn measure_groups(&self) -> Vec<MeasureGroup> {
347 let split = split_measures(&self.music_text);
350 let mut barred: Vec<(String, String)> = Vec::new();
351 let mut index = 0;
352 while index < split.len() {
353 let content = split[index].trim().to_string();
354 let separator = split.get(index + 1).cloned().unwrap_or_default();
355 if !content.is_empty() || !separator.is_empty() {
356 barred.push((content, separator));
357 }
358 index += 2;
359 }
360 let mut groups: Vec<MeasureGroup> = Vec::new();
363 for (content, separator) in barred {
364 let mut kept: Vec<&str> = Vec::new();
365 for atom in content.split_whitespace() {
366 if let Some(named) = atom.strip_prefix('$') {
367 if !kept.is_empty() {
368 groups.push(MeasureGroup {
369 content: kept.join(" "),
370 kind: GroupKind::Continued,
371 repeats: 1,
372 });
373 kept.clear();
374 }
375 let (repeats, named) = repetition(named);
376 groups.push(MeasureGroup {
377 content: named,
378 kind: GroupKind::Expansion,
379 repeats,
380 });
381 } else {
382 kept.push(atom);
383 }
384 }
385 let (repeats, separator) = repetition(&separator);
386 if !kept.is_empty() || separator == "|" {
387 groups.push(MeasureGroup {
388 content: kept.join(" "),
389 kind: if separator == "|" {
390 GroupKind::Measure
391 } else {
392 GroupKind::Loose
393 },
394 repeats,
395 });
396 }
397 }
398 for group in &mut groups {
402 let atoms: Vec<&str> = group.content.split_whitespace().collect();
403 if group.kind == GroupKind::Measure && atoms.iter().all(|atom| atom.starts_with('[')) {
404 group.content = atoms.join(" ");
405 if !group.content.is_empty() {
406 group.content.push(' ');
407 }
408 group.content.push('.');
409 } else if group.kind == GroupKind::Continued {
410 group.kind = GroupKind::Measure;
411 }
412 }
413 groups
414 }
415
416 pub fn expand(&self) -> Result<Vec<Stream>> {
427 self.expand_in(None, None)
428 }
429
430 pub fn expand_in(&self, meter: Option<TimeSignature>, key: Option<Key>) -> Result<Vec<Stream>> {
436 let alone = [self.clone()];
437 let mut expansion = Expansion {
438 rules: &alone,
439 labels: CtLabels::default(),
440 has_song: false,
441 states: vec![RuleState {
442 meter: match meter {
443 Some(meter) => meter,
444 None => TimeSignature::from_ratio_string("4/4")?,
445 },
446 key: match key {
447 Some(key) => key,
448 None => Key::from_tonic("C")?,
449 },
450 last_regular_atom: String::new(),
451 last_chord: None,
452 }],
453 built: Vec::new(),
454 meters_said: Vec::new(),
455 keys_said: Vec::new(),
456 };
457 let places = expansion.expand(0, None, None)?;
458 places
459 .iter()
460 .enumerate()
461 .map(|(index, place)| {
462 expansion.built[*place]
463 .as_measure(IntegerType::try_from(index + 1).unwrap_or(IntegerType::MAX))
464 })
465 .collect()
466 }
467}
468
469#[derive(Clone, Copy, Debug, PartialEq, Eq)]
471#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
472pub enum GroupKind {
473 Measure,
475 Expansion,
477 Loose,
480 Continued,
484}
485
486#[derive(Clone, Debug, PartialEq, Eq)]
488#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
489pub struct MeasureGroup {
490 pub content: String,
492 pub kind: GroupKind,
494 pub repeats: usize,
496}
497
498pub fn fixup_chord_atom(atom: &str) -> String {
510 let mut atom = atom.to_string();
511 if atom.contains('x') {
512 atom = atom.replace('x', "o");
513 }
514 if atom.contains('h') {
515 atom = atom.replace('h', "/o");
516 }
517 if atom.chars().next().is_some_and(char::is_lowercase) && atom.contains('a') {
518 atom = atom.replace('a', "+");
519 }
520 atom
521}
522
523pub fn is_same(chord: &Chord, last: Option<&Chord>) -> bool {
526 let Some(last) = last else { return false };
527 let names = |chord: &Chord| -> Vec<String> {
528 chord
529 .pitches()
530 .iter()
531 .map(crate::pitch::Pitch::name_with_octave)
532 .collect()
533 };
534 names(chord) == names(last)
535}
536
537fn same_key(one: &Key, other: &Key) -> bool {
542 one.tonic_pitch_name_with_case() == other.tonic_pitch_name_with_case()
543 && one.mode() == other.mode()
544 && one.sharps() == other.sharps()
545}
546
547fn bracketed(atom: &str) -> &str {
549 atom.trim_start_matches('[').trim_end_matches(']')
550}
551
552fn ct_error(said: impl Into<String>) -> Error {
555 Error::RomanText(said.into())
556}
557
558fn split_measures(text: &str) -> Vec<String> {
561 let mut parts = Vec::new();
562 let mut rest = text;
563 while let Some(at) = rest.find('|') {
564 parts.push(rest[..at].to_string());
565 let after = &rest[at + 1..];
566 let bytes = after.as_bytes();
567 let mut walked = 0;
568 let mut separator = String::from("|");
569 if bytes.first() == Some(&b'*') {
570 separator.push('*');
571 walked += 1;
572 }
573 while bytes.get(walked).is_some_and(u8::is_ascii_digit) {
574 separator.push(char::from(bytes[walked]));
575 walked += 1;
576 }
577 parts.push(separator);
578 rest = &after[walked..];
579 }
580 parts.push(rest.to_string());
581 parts
582}
583
584fn repetition(text: &str) -> (usize, String) {
587 let Some(at) = text.find('*') else {
588 return (1, text.to_string());
589 };
590 let digits: String = text[at + 1..]
591 .chars()
592 .take_while(char::is_ascii_digit)
593 .collect();
594 if digits.is_empty() {
595 return (1, text.to_string());
596 }
597 let mut kept = text[..at].to_string();
598 kept.push_str(&text[at + 1 + digits.len()..]);
599 (digits.parse().unwrap_or(1), kept)
600}
601
602#[derive(Clone, Debug, Default)]
605struct Built {
606 meter: Option<TimeSignature>,
607 key: Option<Key>,
608 items: Vec<StreamElement>,
609}
610
611impl Built {
612 fn as_measure(&self, number: IntegerType) -> Result<Stream> {
615 let mut measure = Stream::with_kind(StreamKind::Measure);
616 measure.set_number(number);
617 if let Some(key) = &self.key {
618 measure.insert(0.0, key.clone());
619 }
620 if let Some(meter) = &self.meter {
621 measure.insert(0.0, meter.clone());
622 }
623 let mut at = 0.0;
624 for item in &self.items {
625 let length = item.quarter_length();
626 measure.insert(at, item.clone());
627 at = op_frac(at + length);
628 }
629 Ok(measure)
630 }
631
632 fn chord(&self, item: usize) -> Option<&Chord> {
634 match self.items.get(item) {
635 Some(StreamElement::Chord(chord)) => Some(chord),
636 _ => None,
637 }
638 }
639}
640
641#[derive(Clone, Copy, Debug)]
643struct Place {
644 measure: usize,
645 item: usize,
646}
647
648#[derive(Clone, Debug)]
652struct RuleState {
653 meter: TimeSignature,
654 key: Key,
655 last_regular_atom: String,
656 last_chord: Option<Place>,
657}
658
659struct Expansion<'a> {
662 rules: &'a [CtRule],
663 labels: CtLabels,
664 has_song: bool,
669 states: Vec<RuleState>,
670 built: Vec<Built>,
671 meters_said: Vec<TimeSignature>,
672 keys_said: Vec<Key>,
673}
674
675impl<'a> Expansion<'a> {
676 fn of(song: &'a CtSong, labels: CtLabels) -> Result<Self> {
679 let meter = match song.home_time_signature()? {
680 Some(meter) => meter,
681 None => TimeSignature::from_ratio_string("4/4")?,
682 };
683 let key = match song.home_key()? {
684 Some(key) => key,
685 None => Key::from_tonic("C")?,
686 };
687 let state = RuleState {
688 meter,
689 key,
690 last_regular_atom: String::new(),
691 last_chord: None,
692 };
693 Ok(Self {
694 rules: song.rules(),
695 labels,
696 has_song: true,
697 states: vec![state; song.rules().len()],
698 built: Vec::new(),
699 meters_said: Vec::new(),
700 keys_said: Vec::new(),
701 })
702 }
703
704 fn expand(
708 &mut self,
709 rule: usize,
710 meter: Option<TimeSignature>,
711 key: Option<Key>,
712 ) -> Result<Vec<usize>> {
713 let kept = (
714 self.states[rule].meter.clone(),
715 self.states[rule].key.clone(),
716 );
717 if let Some(meter) = meter {
718 self.states[rule].meter = meter;
719 }
720 if let Some(key) = key {
721 self.states[rule].key = key;
722 }
723 let mut places = Vec::new();
724 for group in self.rules[rule].measure_groups() {
725 match group.kind {
726 GroupKind::Expansion => {
727 self.expand_expansion(rule, &group.content, group.repeats, &mut places)?;
728 }
729 GroupKind::Measure => {
730 self.expand_simple(rule, &group.content, group.repeats, &mut places)?;
731 }
732 GroupKind::Loose | GroupKind::Continued => {}
735 }
736 }
737 self.label_section(rule, &places)?;
738 self.states[rule].meter = kept.0;
739 self.states[rule].key = kept.1;
740 Ok(places)
741 }
742
743 fn expand_expansion(
745 &mut self,
746 rule: usize,
747 content: &str,
748 repeats: usize,
749 places: &mut Vec<usize>,
750 ) -> Result<()> {
751 let named = self
752 .rules
753 .iter()
754 .position(|known| known.left_hand_side() == content)
755 .filter(|_| self.has_song)
756 .ok_or_else(|| {
757 ct_error(format!(
758 "cannot expand rule {content} in {}",
759 self.rules[rule].text()
760 ))
761 })?;
762 for _ in 0..repeats {
763 let meter = self.states[rule].meter.clone();
764 let key = self.states[rule].key.clone();
765 let returned = self.expand(named, Some(meter.clone()), Some(key.clone()))?;
766 let Some(first) = returned.first() else {
767 return Err(ct_error(format!(
768 "rule {content} expands to no measure at all"
769 )));
770 };
771 self.insert_key_and_meter(*first, &meter, &key);
772 places.extend(returned);
773 }
774 Ok(())
775 }
776
777 fn expand_simple(
780 &mut self,
781 rule: usize,
782 content: &str,
783 repeats: usize,
784 places: &mut Vec<usize>,
785 ) -> Result<()> {
786 let mut regular: Vec<String> = Vec::new();
789 for atom in content.split_whitespace() {
790 if atom.starts_with('[') {
791 let inside = bracketed(atom);
792 if inside == "0" {
793 self.states[rule].meter = TimeSignature::from_ratio_string("4/4")?;
796 } else if inside.contains('/') {
797 self.states[rule].meter = TimeSignature::from_ratio_string(inside)?;
798 } else {
799 let named = convert_key_string_to_music21_key_string(inside);
800 self.states[rule].key = Key::from_tonic(&named)?;
801 }
802 } else if atom == "." {
803 if self.states[rule].last_regular_atom.is_empty() {
804 return Err(ct_error(format!(
805 ". with no chord before it: {}",
806 self.rules[rule].text()
807 )));
808 }
809 regular.push(self.states[rule].last_regular_atom.clone());
810 } else {
811 regular.push(atom.to_string());
812 self.states[rule].last_regular_atom = atom.to_string();
813 }
814 }
815 if regular.is_empty() {
816 return Ok(());
818 }
819 let measure = self.built.len();
820 self.built.push(Built::default());
821 let meter = self.states[rule].meter.clone();
822 let key = self.states[rule].key.clone();
823 self.insert_key_and_meter(measure, &meter, &key);
824 let length = op_frac(meter.bar_duration().quarter_length() / regular.len() as FloatType);
825 let mut last_is_in_this_measure = false;
826 for atom in regular {
827 if atom == "R" {
828 self.built[measure]
829 .items
830 .push(Rest::new(Duration::new(length)?).into());
831 self.states[rule].last_chord = None;
832 last_is_in_this_measure = false;
833 continue;
834 }
835 let numeral = RomanNumeral::new(fixup_chord_atom(&atom), key.clone())?;
836 let mut chord = numeral.to_chord()?.with_numeral(numeral);
837 let last = self.states[rule]
838 .last_chord
839 .and_then(|place| self.built[place.measure].chord(place.item))
840 .cloned();
841 if is_same(&chord, last.as_ref()) && last_is_in_this_measure {
842 if let Some(place) = self.states[rule].last_chord
845 && let Some(StreamElement::Chord(held)) =
846 self.built[place.measure].items.get_mut(place.item)
847 {
848 let held_length = held
849 .duration()
850 .map_or(0.0, crate::duration::Duration::quarter_length);
851 held.set_duration(Duration::new(op_frac(held_length + length))?);
852 }
853 continue;
854 }
855 chord.set_duration(Duration::new(length)?);
856 self.tie_and_label(rule, &mut chord, last.as_ref())?;
857 let item = self.built[measure].items.len();
858 self.built[measure].items.push(chord.into());
859 self.states[rule].last_chord = Some(Place { measure, item });
860 last_is_in_this_measure = true;
861 }
862 places.push(measure);
863 for _ in 1..repeats {
866 let mut copy = self.built[measure].clone();
867 copy.meter = None;
868 copy.key = None;
869 places.push(self.built.len());
870 self.built.push(copy);
871 }
872 Ok(())
873 }
874
875 fn tie_and_label(
880 &mut self,
881 rule: usize,
882 chord: &mut Chord,
883 last: Option<&Chord>,
884 ) -> Result<()> {
885 let same = is_same(chord, last);
886 let before = self.states[rule].last_chord;
887 let before_tie = last.and_then(|chord| chord.tie().cloned());
888 if !same {
889 if let Some(mut tie) = before_tie {
890 tie.set_tie_type(crate::notation::TieType::Stop);
891 self.write_tie(before, Some(tie));
892 }
893 if self.labels.roman_numerals {
894 let figure = chord
895 .numeral()
896 .map(|numeral| numeral.figure().to_string())
897 .unwrap_or_default();
898 chord.add_lyric(&figure, Some(1), false)?;
899 }
900 return Ok(());
901 }
902 match before_tie {
903 None => {
904 self.write_tie(before, Some(Tie::new(crate::notation::TieType::Start)));
905 }
906 Some(mut tie) => {
907 tie.set_tie_type(crate::notation::TieType::Continue);
908 self.write_tie(before, Some(tie));
909 }
910 }
911 chord.set_tie(Some(Tie::new(crate::notation::TieType::Stop)));
912 Ok(())
913 }
914
915 fn write_tie(&mut self, place: Option<Place>, tie: Option<Tie>) {
917 if let Some(place) = place
918 && let Some(StreamElement::Chord(held)) =
919 self.built[place.measure].items.get_mut(place.item)
920 {
921 held.set_tie(tie);
922 }
923 }
924
925 fn insert_key_and_meter(&mut self, measure: usize, meter: &TimeSignature, key: &Key) {
928 if !self.has_song {
929 self.built[measure].meter = Some(meter.clone());
931 self.built[measure].key = Some(key.clone());
932 return;
933 }
934 if !self.meters_said.contains(meter) {
935 self.built[measure].meter = Some(meter.clone());
936 self.meters_said.push(meter.clone());
937 }
938 if !self.keys_said.iter().any(|said| same_key(said, key)) {
939 self.built[measure].key = Some(key.clone());
940 self.keys_said.push(key.clone());
941 }
942 }
943
944 fn label_section(&mut self, rule: usize, places: &[usize]) -> Result<()> {
948 if !self.labels.subsections || self.rules[rule].left_hand_side() == "S" {
949 return Ok(());
950 }
951 let name = self.rules[rule].left_hand_side().to_string();
952 for place in places {
953 let Some(item) = self.built[*place]
954 .items
955 .iter()
956 .position(|item| matches!(item, StreamElement::Chord(_)))
957 else {
958 continue;
960 };
961 if let Some(StreamElement::Chord(chord)) = self.built[*place].items.get_mut(item) {
962 let number = IntegerType::try_from(chord.lyrics().len() + 1).unwrap_or(1);
963 chord.add_lyric(&name, Some(number), false)?;
964 }
965 break;
966 }
967 Ok(())
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[test]
979 fn a_rule_is_read_as_the_groups_music21_reads() {
980 let rule = CtRule::new(
981 "In: [A] [4/4] $Vr $BP*3 I IV | I | $BP*3 I IV | I | | R |*4 I |*4 % A comment",
982 );
983 let said: Vec<(String, GroupKind, usize)> = rule
984 .measure_groups()
985 .into_iter()
986 .map(|group| (group.content, group.kind, group.repeats))
987 .collect();
988 let expected: Vec<(String, GroupKind, usize)> = vec![
989 ("[A] [4/4]", GroupKind::Measure, 1),
990 ("Vr", GroupKind::Expansion, 1),
991 ("BP", GroupKind::Expansion, 3),
992 ("I IV", GroupKind::Measure, 1),
993 ("I", GroupKind::Measure, 1),
994 ("BP", GroupKind::Expansion, 3),
995 ("I IV", GroupKind::Measure, 1),
996 ("I", GroupKind::Measure, 1),
997 (".", GroupKind::Measure, 1),
998 ("R", GroupKind::Measure, 4),
999 ("I", GroupKind::Measure, 4),
1000 ]
1001 .into_iter()
1002 .map(|(content, kind, repeats)| (content.to_string(), kind, repeats))
1003 .collect();
1004 assert_eq!(said, expected);
1005 assert_eq!(rule.left_hand_side(), "In");
1006 assert_eq!(rule.comment(), Some("A comment"));
1007 assert_eq!(
1008 rule.music_text(),
1009 "[A] [4/4] $Vr $BP*3 I IV | I | $BP*3 I IV | I | | R |*4 I |*4"
1010 );
1011 }
1012
1013 #[test]
1018 fn an_empty_measure_holds_the_chord_before_it() {
1019 let rule = CtRule::new("Vr: [4/4] bVII | IV | | [2/4] |");
1020 let measures = rule.expand().expect("the rule expands");
1021 assert_eq!(measures.len(), 4);
1022 let described = |measure: &Stream| -> Vec<String> {
1023 measure
1024 .leaves()
1025 .iter()
1026 .map(|(offset, element)| match element {
1027 StreamElement::Chord(chord) => format!(
1028 "{offset} {} {}",
1029 chord.numeral().map_or("", RomanNumeral::figure),
1030 element.quarter_length()
1031 ),
1032 StreamElement::TimeSignature(meter) => meter.ratio_string(),
1033 StreamElement::Key(key) => key.tonic_pitch_name_with_case(),
1034 other => format!("{other:?}"),
1035 })
1036 .collect()
1037 };
1038 assert_eq!(described(&measures[2]), ["C", "4/4", "0 IV 4"]);
1039 assert_eq!(described(&measures[3]), ["C", "2/4", "0 IV 2"]);
1040 }
1041
1042 #[test]
1044 fn blitzkrieg_bop_is_as_long_as_music21_makes_it() {
1045 let song = "% Blitzkrieg Bop\n\
1046 BP: I | IV V | %THIS IS A COMMENT\n\
1047 In: $BP*3 I IV | I | $BP*3 I IV | I | R |*4 I |*4\n\
1048 Vr: $BP*3 I IV | I |\n\
1049 Br: IV | | I | IV I | IV | | ii | IV V |\n\
1050 Co: R |*4 I |*4\n\
1051 S: [A] $In $Vr $Vr $Br $Vr $Vr $Br $Vr $Vr $Co\n";
1052 let part = from_clercq_temperley(song).expect("the song reads");
1053 assert_eq!(part.measures().len(), 96);
1054 assert_eq!(
1055 part.measures().last().map(|measure| measure.number()),
1056 Some(96)
1057 );
1058 assert_eq!(part.end_offset() - 4.0, 380.0);
1061 }
1062
1063 #[test]
1066 fn a_song_says_its_title_and_its_comments() {
1067 let song = CtSong::parse(
1068 "\n% Simple Gifts\n% A wonderful shaker melody\nVr: I | I | %incomplete verse\n\
1069 S: [A] $Vr % Not quite finished!",
1070 );
1071 assert_eq!(song.title(), "Simple Gifts");
1072 assert_eq!(
1073 song.comments(),
1074 vec![
1075 vec!["A wonderful shaker melody".to_string()],
1076 vec!["Vr:".to_string(), "incomplete verse".to_string()],
1077 vec!["S:".to_string(), "Not quite finished!".to_string()],
1078 ]
1079 );
1080 assert_eq!(song.rules().len(), 2);
1081 assert_eq!(
1082 song.home_key()
1083 .expect("a key")
1084 .map(|key| key.tonic_pitch_name_with_case()),
1085 Some("A".to_string())
1086 );
1087 assert_eq!(
1088 song.home_time_signature()
1089 .expect("a meter")
1090 .map(|meter| meter.ratio_string()),
1091 Some("4/4".to_string())
1092 );
1093 }
1094
1095 #[test]
1099 fn a_line_is_read_as_music21_slices_it() {
1100 assert_eq!(CtRule::new("I | IV |").music_text(), "| IV |");
1102 assert_eq!(CtRule::new("I | IV |").left_hand_side(), "");
1103 assert_eq!(CtRule::new("In: $BP*3 I | % c").music_text(), "$BP*3 I |");
1104 let song = CtSong::parse("% 50% More\nVr: I |\nS: [C] $Vr\n");
1106 assert_eq!(song.title(), "50 More");
1107 }
1108
1109 #[test]
1111 fn a_rule_names_the_section_it_stands_for() {
1112 let named = |text: &str| CtRule::new(text).section_name();
1113 assert_eq!(named("Vr2: I |"), "Verse2");
1114 assert_eq!(named("In: I |"), "Introduction");
1115 assert_eq!(named("Br: I |"), "Bridge");
1116 assert_eq!(named("Ch2: I |"), "Chorus2");
1117 assert_eq!(named("Tg: I |"), "Tag");
1118 assert_eq!(named("S: $Vr"), "Song");
1119 assert_eq!(named("Fadeout: I |"), "Fadeout");
1120 assert_eq!(named("Xx: I |"), "Xx");
1121 }
1122}