1use std::collections::HashMap;
5
6use crate::{
7 defaults::{FloatType, IntegerType},
8 error::{Error, Result},
9 notation::{Tie, TieType},
10 pitch::Pitch,
11 serial::{IndexedTransformation, ToneRow, TransformationConvention},
12 spanner::Spanner,
13 stream::{Stream, StreamElement, StreamKind},
14};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum Repetitions {
20 #[default]
23 SkipConsecutive,
24 RowsOnly,
27 IncludeAll,
29 IgnoreAll,
32}
33
34#[derive(Clone, Copy, Debug)]
36pub struct SegmentNote<'a> {
37 pub element: &'a StreamElement,
39 pub measure: Option<IntegerType>,
41 pub offset: FloatType,
44 pub leaf: usize,
47}
48
49#[derive(Clone, Debug)]
52pub struct ContiguousSegment<'a> {
53 pub notes: Vec<SegmentNote<'a>>,
55 pub part: Option<usize>,
58 pub active: Vec<u8>,
61 pub matched: Option<Vec<IntegerType>>,
64}
65
66fn pitches_of(element: &StreamElement) -> Vec<Pitch> {
68 match element {
69 StreamElement::ChordSymbol(symbol) => symbol.pitches().unwrap_or_default(),
70 _ => element.pitches(),
71 }
72}
73
74fn pitch_class(pitch: &Pitch) -> u8 {
77 (pitch.ps().round_ties_even() as i64).rem_euclid(12) as u8
78}
79
80impl ContiguousSegment<'_> {
81 pub fn start_measure(&self) -> Option<IntegerType> {
83 self.notes.first().and_then(|note| note.measure)
84 }
85
86 pub fn start_offset(&self) -> Option<FloatType> {
89 self.notes.first().map(|note| note.offset)
90 }
91
92 pub fn pitch_classes_from_bottom(&self) -> Vec<u8> {
95 self.notes
96 .iter()
97 .flat_map(|note| pitches_of(note.element))
98 .map(|pitch| pitch_class(&pitch))
99 .collect()
100 }
101
102 pub fn distinct_pitch_classes(&self) -> Vec<u8> {
105 let mut distinct = Vec::new();
106 for class in self.pitch_classes_from_bottom() {
107 if !distinct.contains(&class) {
108 distinct.push(class);
109 }
110 }
111 distinct
112 }
113
114 fn rows(&self) -> Option<(ToneRow, ToneRow)> {
115 let matched = self.matched.as_ref()?;
116 Some((
117 ToneRow::new(self.active.iter().map(|class| IntegerType::from(*class))),
118 ToneRow::new(matched.iter().copied()),
119 ))
120 }
121
122 pub fn zero_centered_transformations_from_matched(&self) -> Vec<IndexedTransformation> {
126 self.rows()
127 .map(|(active, matched)| matched.find_zero_centered_transformations(&active))
128 .unwrap_or_default()
129 }
130
131 pub fn original_centered_transformations_from_matched(&self) -> Vec<IndexedTransformation> {
134 self.rows()
135 .map(|(active, matched)| matched.find_original_centered_transformations(&active))
136 .unwrap_or_default()
137 }
138}
139
140fn parts_of(stream: &Stream) -> Vec<(Option<usize>, &Stream)> {
143 let parts: Vec<&Stream> = stream
144 .recurse()
145 .into_iter()
146 .filter_map(|(_, element)| match element {
147 StreamElement::Stream(inner)
148 if matches!(inner.kind(), StreamKind::Part | StreamKind::PartStaff) =>
149 {
150 Some(&**inner)
151 }
152 _ => None,
153 })
154 .collect();
155 if parts.is_empty() {
156 vec![(None, stream)]
157 } else {
158 parts
159 .into_iter()
160 .enumerate()
161 .map(|(index, part)| (Some(index), part))
162 .collect()
163 }
164}
165
166fn tie_of(element: &StreamElement) -> Option<&Tie> {
167 match element {
168 StreamElement::Note(note) => note.tie(),
169 StreamElement::Chord(chord) => chord.tie(),
170 StreamElement::Unpitched(stroke) => stroke.written().tie(),
171 StreamElement::PercussionChord(chord) => chord.written().tie(),
172 _ => None,
173 }
174}
175
176fn part_notes(part: &Stream) -> Vec<SegmentNote<'_>> {
180 fn walk<'a>(
181 stream: &'a Stream,
182 measure: Option<IntegerType>,
183 leaf: &mut usize,
184 out: &mut Vec<SegmentNote<'a>>,
185 ) {
186 for event in stream.events() {
187 let element = event.element();
188 if let StreamElement::Stream(inner) = element {
189 let measure = if inner.kind() == StreamKind::Measure {
190 Some(inner.number())
191 } else {
192 measure
193 };
194 walk(inner, measure, leaf, out);
195 continue;
196 }
197 let position = *leaf;
198 *leaf += 1;
199 if !matches!(
200 element,
201 StreamElement::Note(_)
202 | StreamElement::Chord(_)
203 | StreamElement::Unpitched(_)
204 | StreamElement::PercussionChord(_)
205 | StreamElement::ChordSymbol(_)
206 ) {
207 continue;
208 }
209 if tie_of(element).is_some_and(|tie| tie.tie_type() != TieType::Start) {
210 continue;
211 }
212 out.push(SegmentNote {
213 element,
214 measure,
215 offset: event.offset(),
216 leaf: position,
217 });
218 }
219 }
220 let mut out = Vec::new();
221 let measure = (part.kind() == StreamKind::Measure).then(|| part.number());
222 walk(part, measure, &mut 0, &mut out);
223 out
224}
225
226#[derive(Clone, Copy, Debug, Default)]
229pub struct ContiguousSegmentSearcher {
230 pub repetitions: Repetitions,
232 pub include_chords: bool,
234 pub trim_to_shortest_length_fast: bool,
237}
238
239struct Walk<'a> {
241 length: usize,
242 part: Option<usize>,
243 chords: Vec<SegmentNote<'a>>,
244 total: usize,
245 found: Vec<ContiguousSegment<'a>>,
246}
247
248impl<'a> Walk<'a> {
249 fn add(&mut self, notes: Vec<SegmentNote<'a>>) {
250 self.found.push(ContiguousSegment {
251 notes,
252 part: self.part,
253 active: Vec::new(),
254 matched: None,
255 });
256 }
257
258 fn drop_front(&mut self, count: usize) {
259 for removed in self.chords.drain(..count) {
260 self.total -= pitches_of(removed.element).len().min(self.total);
261 }
262 }
263}
264
265fn count(note: &SegmentNote<'_>) -> usize {
266 pitches_of(note.element).len()
267}
268
269fn index_error() -> Error {
270 Error::Search("list index out of range".to_string())
271}
272
273impl ContiguousSegmentSearcher {
274 pub fn new(repetitions: Repetitions, include_chords: bool) -> Self {
276 Self {
277 repetitions,
278 include_chords,
279 trim_to_shortest_length_fast: false,
280 }
281 }
282
283 pub fn by_length<'a>(
291 &self,
292 stream: &'a Stream,
293 length: usize,
294 ) -> Result<Vec<ContiguousSegment<'a>>> {
295 let mut found = Vec::new();
296 for (part, part_stream) in parts_of(stream) {
297 let mut walk = Walk {
298 length,
299 part,
300 chords: Vec::new(),
301 total: 0,
302 found: Vec::new(),
303 };
304 for note in part_notes(part_stream) {
305 match (self.include_chords, self.repetitions) {
306 (false, Repetitions::SkipConsecutive) => {
307 if !same_as_last(&walk, ¬e) {
308 include_all_exclude(&mut walk, note);
309 }
310 }
311 (false, Repetitions::RowsOnly) => rows_only_exclude(&mut walk, note)?,
312 (false, Repetitions::IncludeAll) => include_all_exclude(&mut walk, note),
313 (false, Repetitions::IgnoreAll) => {
314 ignore_all_exclude(&mut walk, note, self.trim_to_shortest_length_fast);
315 }
316 (true, Repetitions::SkipConsecutive) => {
317 if !same_as_last(&walk, ¬e) {
318 include_all_include(&mut walk, note);
319 }
320 }
321 (true, Repetitions::RowsOnly) => rows_only_include(&mut walk, note),
322 (true, Repetitions::IncludeAll) => include_all_include(&mut walk, note),
323 (true, Repetitions::IgnoreAll) => {
324 ignore_all_include(&mut walk, note, self.trim_to_shortest_length_fast)?;
325 }
326 }
327 }
328 found.append(&mut walk.found);
329 }
330 Ok(found)
331 }
332}
333
334fn same_as_last(walk: &Walk<'_>, note: &SegmentNote<'_>) -> bool {
336 walk.chords
337 .last()
338 .is_some_and(|last| pitches_of(last.element) == pitches_of(note.element))
339}
340
341fn include_all_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
343 if count(¬e) > 1 {
344 walk.chords.clear();
345 return;
346 }
347 walk.total += count(¬e);
348 walk.chords.push(note);
349 if walk.chords.len() == walk.length + 1 {
350 walk.chords.remove(0);
351 }
352 if walk.chords.len() == walk.length {
353 let notes = walk.chords.clone();
354 walk.add(notes);
355 }
356}
357
358fn include_all_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
362 walk.total += count(¬e);
363 walk.chords.push(note);
364 let mut active_length = walk.total;
365 let mut to_delete = 0;
366 for start in 0..walk.chords.len() {
367 if start > 0 {
368 active_length -= count(&walk.chords[start - 1]);
369 }
370 let active = &walk.chords[start..];
371 let inner = active_length as isize
372 - (count(&active[0]) + count(&active[active.len() - 1])) as isize;
373 if active_length >= walk.length && inner <= walk.length as isize - 2 {
374 let notes = active.to_vec();
375 walk.add(notes);
376 } else if active_length >= walk.length {
377 to_delete += 1;
378 } else {
379 break;
380 }
381 }
382 walk.drop_front(to_delete);
383}
384
385fn ignore_all_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>, trim: bool) {
387 if count(¬e) > 1 {
388 walk.chords.clear();
389 return;
390 }
391 walk.chords.push(note);
392 let mut to_delete = 0;
393 for start in 0..walk.chords.len() {
394 let active = &walk.chords[start..];
395 let mut classes: Vec<u8> = active
396 .iter()
397 .flat_map(|note| pitches_of(note.element))
398 .map(|pitch| pitch_class(&pitch))
399 .collect();
400 classes.sort_unstable();
401 classes.dedup();
402 if classes.len() == walk.length {
403 let notes = active.to_vec();
404 walk.add(notes);
405 if trim {
406 to_delete += 1;
407 }
408 } else if classes.len() > walk.length {
409 to_delete += 1;
410 }
411 }
412 walk.drop_front(to_delete);
413}
414
415fn ignore_all_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>, trim: bool) -> Result<()> {
417 walk.chords.push(note);
418 let mut to_delete = 0;
419 for start in 0..walk.chords.len() {
420 let active = walk.chords[start..].to_vec();
421 walk.add(active.clone());
422 let mut superset = walk
423 .found
424 .last()
425 .map(ContiguousSegment::pitch_classes_from_bottom)
426 .unwrap_or_default();
427 superset.sort_unstable();
428 superset.dedup();
429 if superset.len() >= walk.length {
430 let first = pitches_of(active[0].element);
431 let last = pitches_of(active[active.len() - 1].element);
432 let mut check: Vec<u8> = active[1..active.len().saturating_sub(1).max(1)]
433 .iter()
434 .flat_map(|note| pitches_of(note.element))
435 .map(|pitch| pitch_class(&pitch))
436 .collect();
437 if active.len() < 2 {
438 check.clear();
439 }
440 check.push(pitch_class(first.last().ok_or_else(index_error)?));
441 check.push(pitch_class(last.first().ok_or_else(index_error)?));
442 check.sort_unstable();
443 check.dedup();
444 if check.len() > walk.length {
445 walk.found.pop();
446 to_delete += 1;
447 } else if trim {
448 to_delete += 1;
449 }
450 } else {
451 walk.found.pop();
452 break;
453 }
454 }
455 walk.drop_front(to_delete);
456 Ok(())
457}
458
459fn rows_only_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) -> Result<()> {
462 if count(¬e) > 1 {
463 walk.chords.clear();
464 return Ok(());
465 }
466 if walk.chords.len() == walk.length && !walk.chords.is_empty() {
467 walk.chords.remove(0);
468 }
469 let pitch_of = |element: &StreamElement| match element {
470 StreamElement::Note(note) => Ok(pitch_class(note.pitch())),
471 _ => Err(Error::Search(
472 "only a note has a single pitch to read".to_string(),
473 )),
474 };
475 let class = pitch_of(note.element)?;
476 let existing = walk
477 .chords
478 .iter()
479 .map(|old| pitch_of(old.element))
480 .collect::<Result<Vec<u8>>>()?;
481 if existing.contains(&class) {
482 walk.chords = vec![note];
483 } else {
484 walk.chords.push(note);
485 }
486 if walk.chords.len() == walk.length {
487 let notes = walk.chords.clone();
488 walk.add(notes);
489 }
490 Ok(())
491}
492
493fn rows_only_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
495 walk.total += count(¬e);
496 walk.chords.push(note);
497 let mut active_length = walk.total;
498 let mut to_delete = 0;
499 for start in 0..walk.chords.len() {
500 if start > 0 {
501 active_length -= count(&walk.chords[start - 1]);
502 }
503 let active = walk.chords[start..].to_vec();
504 let first_count = count(&active[0]);
505 let last_count = count(&active[active.len() - 1]);
506 let inner = active_length as isize - (first_count + last_count) as isize;
507 if active_length >= walk.length && inner <= walk.length as isize - 2 {
508 walk.add(active);
509 let superset = walk
510 .found
511 .last()
512 .map(ContiguousSegment::pitch_classes_from_bottom)
513 .unwrap_or_default();
514 let lower = (superset.len() as isize - walk.length as isize - last_count as isize + 1)
515 .max(0) as usize;
516 let upper =
517 (first_count as isize).min(superset.len() as isize - walk.length as isize + 1);
518 let is_row = (lower as isize..upper).any(|j| {
519 let window = &superset[j as usize..j as usize + walk.length];
520 let mut distinct = window.to_vec();
521 distinct.sort_unstable();
522 distinct.dedup();
523 distinct.len() == walk.length
524 });
525 if !is_row {
526 walk.found.pop();
527 }
528 } else if active_length >= walk.length {
529 to_delete += 1;
530 } else {
531 break;
532 }
533 }
534 walk.drop_front(to_delete);
535}
536
537#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
540pub enum Matching {
541 #[default]
544 Exact,
545 Transposed,
548 Transformed,
551 Multiset,
554 TransposedMultiset,
557 TransposedInvertedMultiset,
560}
561
562#[derive(Clone, Debug, PartialEq)]
564enum Normal {
565 Classes(Vec<u8>),
566 Intervals(String),
567 Row(ToneRow),
568}
569
570impl Normal {
571 fn len(&self) -> usize {
572 match self {
573 Self::Classes(classes) => classes.len(),
574 Self::Intervals(intervals) => intervals.chars().count(),
575 Self::Row(row) => row.len(),
576 }
577 }
578}
579
580fn multiset(classes: impl IntoIterator<Item = i64>) -> Vec<i64> {
582 let mut sorted: Vec<i64> = classes.into_iter().collect();
583 sorted.sort_unstable();
584 sorted
585}
586
587impl Matching {
588 fn normalize(self, segment: &[IntegerType]) -> Normal {
589 let row = ToneRow::new(segment.iter().copied());
590 match self {
591 Self::Transposed => Normal::Intervals(row.intervals_as_string()),
592 Self::Transformed => Normal::Row(row),
593 _ => Normal::Classes(row.pitch_classes().to_vec()),
594 }
595 }
596
597 fn multiset_duplicates(self) -> bool {
598 matches!(
599 self,
600 Self::Multiset | Self::TransposedMultiset | Self::TransposedInvertedMultiset
601 )
602 }
603
604 fn equal(self, search: &Normal, subset: &Normal) -> bool {
605 let classes = |normal: &Normal| -> Vec<i64> {
606 match normal {
607 Normal::Classes(classes) => classes.iter().map(|class| i64::from(*class)).collect(),
608 _ => Vec::new(),
609 }
610 };
611 match self {
612 Self::Exact | Self::Transposed => search == subset,
613 Self::Transformed => match (search, subset) {
614 (Normal::Row(search), Normal::Row(subset)) => {
615 !subset.find_zero_centered_transformations(search).is_empty()
616 }
617 _ => false,
618 },
619 Self::Multiset => multiset(classes(search)) == multiset(classes(subset)),
620 Self::TransposedMultiset | Self::TransposedInvertedMultiset => {
621 let wanted = multiset(classes(subset));
622 let search = classes(search);
623 let transposed = (0..12)
624 .any(|i| multiset(search.iter().map(|p| (p + i).rem_euclid(12))) == wanted);
625 transposed
626 || (self == Self::TransposedInvertedMultiset
627 && (0..12).any(|i| {
628 multiset(search.iter().map(|p| (-(p + i)).rem_euclid(12))) == wanted
629 }))
630 }
631 }
632 }
633}
634
635enum Searched {
637 Normals(Vec<Normal>),
638 Multisets(Vec<Vec<i64>>),
639}
640
641impl Searched {
642 fn already(&mut self, matching: Matching, segment: &[IntegerType]) -> bool {
645 match self {
646 Self::Normals(seen) => {
647 let normal = matching.normalize(segment);
648 let found = if matching == Matching::Transformed {
649 seen.iter().any(|used| match (&normal, used) {
650 (Normal::Row(row), Normal::Row(used)) => {
651 !used.find_zero_centered_transformations(row).is_empty()
652 }
653 _ => false,
654 })
655 } else {
656 seen.contains(&normal)
657 };
658 if !found {
659 seen.push(normal);
660 }
661 found
662 }
663 Self::Multisets(seen) => {
664 let raw: Vec<i64> = segment.iter().map(|p| i64::from(*p)).collect();
665 if matching == Matching::TransposedInvertedMultiset
666 && (0..12).any(|i| {
667 seen.contains(&multiset(raw.iter().map(|p| (-(p + i)).rem_euclid(12))))
668 })
669 {
670 return true;
671 }
672 if (0..12)
673 .any(|i| seen.contains(&multiset(raw.iter().map(|p| (p + i).rem_euclid(12)))))
674 {
675 return true;
676 }
677 seen.push(multiset(raw.iter().map(|p| (p + 11).rem_euclid(12))));
679 false
680 }
681 }
682 }
683}
684
685#[derive(Clone, Copy, Debug, Default)]
689pub struct SegmentMatcher {
690 pub matching: Matching,
692 pub repetitions: Repetitions,
694 pub include_chords: bool,
696}
697
698impl SegmentMatcher {
699 pub fn new(matching: Matching, repetitions: Repetitions, include_chords: bool) -> Self {
702 Self {
703 matching,
704 repetitions,
705 include_chords,
706 }
707 }
708
709 pub fn find<'a>(
719 &self,
720 stream: &'a Stream,
721 search: &[Vec<IntegerType>],
722 ) -> Result<Vec<ContiguousSegment<'a>>> {
723 let searcher = ContiguousSegmentSearcher::new(self.repetitions, self.include_chords);
724 let mut by_length: HashMap<usize, Vec<ContiguousSegment<'a>>> = HashMap::new();
725 let mut matched: Vec<(usize, usize)> = Vec::new();
726 let mut searched = if matches!(
727 self.matching,
728 Matching::TransposedMultiset | Matching::TransposedInvertedMultiset
729 ) {
730 Searched::Multisets(Vec::new())
731 } else {
732 Searched::Normals(Vec::new())
733 };
734 for segment in search {
735 if searched.already(self.matching, segment) {
736 continue;
737 }
738 let normal = self.matching.normalize(segment);
739 let length = segment.len();
740 let candidates = match by_length.entry(length) {
741 std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
742 std::collections::hash_map::Entry::Vacant(entry) => {
743 entry.insert(searcher.by_length(stream, length)?)
744 }
745 };
746 for (index, candidate) in candidates.iter_mut().enumerate() {
747 let found = if self.repetitions == Repetitions::IgnoreAll {
748 self.match_ignore_all(candidate, &normal, length)
749 } else {
750 self.match_other(candidate, &normal, length)
751 };
752 if let Some(active) = found {
753 candidate.active = active;
754 candidate.matched = Some(segment.clone());
755 matched.push((length, index));
756 }
757 }
758 }
759 Ok(matched
760 .into_iter()
761 .map(|(length, index)| by_length[&length][index].clone())
762 .collect())
763 }
764
765 fn match_ignore_all(
768 &self,
769 candidate: &ContiguousSegment<'_>,
770 search: &Normal,
771 length: usize,
772 ) -> Option<Vec<u8>> {
773 let classes = candidate.distinct_pitch_classes();
774 let first: Vec<u8> = pitches_of(candidate.notes.first()?.element)
775 .iter()
776 .map(pitch_class)
777 .collect();
778 let before_last: Vec<u8> = candidate.notes[..candidate.notes.len() - 1]
779 .iter()
780 .flat_map(|note| pitches_of(note.element))
781 .map(|pitch| pitch_class(&pitch))
782 .collect();
783 let starts = classes.len() as isize - search.len() as isize + 1;
784 for start in 0..starts.max(0) as usize {
785 let subset = &classes[start..(start + length).min(classes.len())];
786 let as_ints: Vec<IntegerType> = subset
787 .iter()
788 .map(|class| IntegerType::from(*class))
789 .collect();
790 if !self
791 .matching
792 .equal(search, &self.matching.normalize(&as_ints))
793 {
794 continue;
795 }
796 if !first.contains(&subset[0]) {
797 continue;
798 }
799 if self.matching.multiset_duplicates()
800 || !before_last.contains(&subset[subset.len() - 1])
801 {
802 return Some(subset.to_vec());
803 }
804 }
805 None
806 }
807
808 fn match_other(
810 &self,
811 candidate: &ContiguousSegment<'_>,
812 search: &Normal,
813 length: usize,
814 ) -> Option<Vec<u8>> {
815 let classes = candidate.pitch_classes_from_bottom();
816 let first = count(candidate.notes.first()?);
817 let last = count(candidate.notes.last()?);
818 let lower = (classes.len() as isize - length as isize - last as isize + 1).max(0);
819 let upper = (first as isize).min(classes.len() as isize + 1 - length as isize);
820 for start in lower..upper {
821 let start = start as usize;
822 let subset = &classes[start..(start + length).min(classes.len())];
823 let as_ints: Vec<IntegerType> = subset
824 .iter()
825 .map(|class| IntegerType::from(*class))
826 .collect();
827 if self
828 .matching
829 .equal(search, &self.matching.normalize(&as_ints))
830 {
831 return Some(subset.to_vec());
832 }
833 }
834 None
835 }
836
837 pub fn label(
850 &self,
851 stream: &Stream,
852 named: &[(String, Vec<IntegerType>)],
853 convention: Option<TransformationConvention>,
854 ) -> Result<Stream> {
855 let search: Vec<Vec<IntegerType>> =
856 named.iter().map(|(_, segment)| segment.clone()).collect();
857 let mut found: Vec<(Option<usize>, usize, usize, String)> = Vec::new();
858 {
859 let mut segments = self.find(stream, &search)?;
860 segments.sort_by(|a, b| {
861 (a.part, a.start_measure())
862 .cmp(&(b.part, b.start_measure()))
863 .then_with(|| {
864 a.start_offset()
865 .partial_cmp(&b.start_offset())
866 .unwrap_or(std::cmp::Ordering::Equal)
867 })
868 });
869 for segment in &segments {
870 let (Some(first), Some(last)) = (segment.notes.first(), segment.notes.last())
871 else {
872 continue;
873 };
874 let Some((name, _)) = named
875 .iter()
876 .find(|(_, wanted)| Some(wanted) == segment.matched.as_ref())
877 else {
878 found.push((segment.part, first.leaf, last.leaf, String::new()));
879 continue;
880 };
881 let transformations = match convention {
882 Some(TransformationConvention::OriginalCentered) => {
883 segment.original_centered_transformations_from_matched()
884 }
885 Some(TransformationConvention::ZeroCentered) => {
886 segment.zero_centered_transformations_from_matched()
887 }
888 None => Vec::new(),
889 };
890 let mut label = name.clone();
891 for (transformation, index) in transformations {
892 let convention = convention.unwrap_or(TransformationConvention::ZeroCentered);
893 label.push_str(&format!(" ,{}{index}", transformation.label(convention)));
894 }
895 found.push((segment.part, first.leaf, last.leaf, label));
896 }
897 }
898 let mut labelled = stream.clone();
899 for (part, first, last, label) in found {
900 let target = match part {
901 Some(part) => part_mut(&mut labelled, part).ok_or_else(index_error)?,
902 None => &mut labelled,
903 };
904 let spanned = if first == last {
905 vec![first]
906 } else {
907 vec![first, last]
908 };
909 target.add_spanner(Spanner::line(spanned));
910 if label.is_empty() {
911 continue;
912 }
913 let mut leaf = 0;
914 let mut result = Ok(());
915 target.for_each_mut(&mut |_, element| {
916 if leaf == first {
917 let lyrics = match element {
918 StreamElement::Note(note) => Some(note.lyrics().to_vec()),
919 StreamElement::Chord(chord) => Some(chord.lyrics().to_vec()),
920 StreamElement::ChordSymbol(symbol) => Some(symbol.lyrics().to_vec()),
921 _ => None,
922 };
923 let present = lyrics
924 .is_some_and(|lyrics| lyrics.iter().any(|lyric| lyric.text() == label));
925 if !present {
926 result = match element {
927 StreamElement::Note(note) => note.add_lyric(&label, None, false),
928 StreamElement::Chord(chord) => chord.add_lyric(&label, None, false),
929 StreamElement::ChordSymbol(symbol) => {
930 symbol.add_lyric(&label, None, false);
931 Ok(())
932 }
933 _ => Ok(()),
934 };
935 }
936 }
937 leaf += 1;
938 });
939 result?;
940 }
941 Ok(labelled)
942 }
943}
944
945fn part_mut(stream: &mut Stream, number: usize) -> Option<&mut Stream> {
947 fn walk<'a>(stream: &'a mut Stream, number: usize, seen: &mut usize) -> Option<&'a mut Stream> {
948 for event in stream.events_mut() {
949 if let StreamElement::Stream(inner) = event.element_mut() {
950 if matches!(inner.kind(), StreamKind::Part | StreamKind::PartStaff) {
951 if *seen == number {
952 return Some(inner);
953 }
954 *seen += 1;
955 }
956 if let Some(found) = walk(inner, number, seen) {
957 return Some(found);
958 }
959 }
960 }
961 None
962 }
963 walk(stream, number, &mut 0)
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969 use crate::tinynotation::from_tiny_notation;
970
971 #[test]
972 fn segments_are_found_by_length() -> Result<()> {
973 let line = from_tiny_notation("4/4 c4 d d e f g a b")?;
975 let skip = ContiguousSegmentSearcher::new(Repetitions::SkipConsecutive, true);
976 let found = skip.by_length(&line, 3)?;
977 assert_eq!(found.len(), 5);
978 assert_eq!(found[0].pitch_classes_from_bottom(), [0, 2, 4]);
979 let all = ContiguousSegmentSearcher::new(Repetitions::IncludeAll, true);
980 let found = all.by_length(&line, 3)?;
981 assert_eq!(found.len(), 6);
982 assert_eq!(found[1].pitch_classes_from_bottom(), [2, 2, 4]);
983 Ok(())
984 }
985
986 #[test]
987 fn rows_are_matched_and_labelled() -> Result<()> {
988 let line = from_tiny_notation("4/4 c4 d e f g a b c'")?;
991 let matcher = SegmentMatcher::new(Matching::Transposed, Repetitions::SkipConsecutive, true);
992 let found = matcher.find(&line, &[vec![0, 2, 4]])?;
993 let actives: Vec<&[u8]> = found
994 .iter()
995 .map(|segment| segment.active.as_slice())
996 .collect();
997 assert_eq!(actives, [&[0, 2, 4][..], &[5, 7, 9], &[7, 9, 11]]);
998 let labelled = matcher.label(&line, &[("A".to_string(), vec![0, 2, 4])], None)?;
999 let spanners: usize = labelled.spanners().len()
1000 + labelled
1001 .parts()
1002 .iter()
1003 .map(|part| part.spanners().len())
1004 .sum::<usize>();
1005 assert_eq!(spanners, 3);
1006 let lyrics: Vec<Option<String>> = labelled
1007 .leaves()
1008 .into_iter()
1009 .filter_map(|(_, element)| match element {
1010 StreamElement::Note(note) => Some(note.lyrics().first().map(|lyric| lyric.text())),
1011 _ => None,
1012 })
1013 .collect();
1014 let a = Some("A".to_string());
1015 assert_eq!(
1016 lyrics,
1017 [a.clone(), None, None, a.clone(), a, None, None, None]
1018 );
1019 Ok(())
1020 }
1021}