Skip to main content

music21_rs/search/
serial.rs

1//! Rows and sets of pitch classes found in the notes of a stream, and
2//! labelled there: music21's `search.serial`.
3
4use 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/// How a [`ContiguousSegmentSearcher`] reads notes that repeat a pitch
17/// class: music21's `reps`.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum Repetitions {
20    /// A note or chord sounding just what the one before sounded is passed
21    /// over: music21's `skipConsecutive`.
22    #[default]
23    SkipConsecutive,
24    /// Every note counts, and a segment is a run of distinct pitch classes:
25    /// music21's `rowsOnly`.
26    RowsOnly,
27    /// Every note counts: music21's `includeAll`.
28    IncludeAll,
29    /// A segment is as many notes as sound its number of distinct pitch
30    /// classes: music21's `ignoreAll`.
31    IgnoreAll,
32}
33
34/// One note or chord of a [`ContiguousSegment`], and where it stands.
35#[derive(Clone, Copy, Debug)]
36pub struct SegmentNote<'a> {
37    /// The note or chord.
38    pub element: &'a StreamElement,
39    /// The number of the measure it is in, if it is in one.
40    pub measure: Option<IntegerType>,
41    /// Its offset in the stream that holds it, a measure or a voice:
42    /// music21's `offset`.
43    pub offset: FloatType,
44    /// Its position among the leaves of its part, or of the stream searched
45    /// where that has no parts: see [`Stream::leaves`].
46    pub leaf: usize,
47}
48
49/// A run of notes and chords of one part: music21's
50/// `ContiguousSegmentOfNotes`.
51#[derive(Clone, Debug)]
52pub struct ContiguousSegment<'a> {
53    /// The notes and chords, in order.
54    pub notes: Vec<SegmentNote<'a>>,
55    /// The part they are in, counted from nought, or nothing where the
56    /// stream searched has no parts.
57    pub part: Option<usize>,
58    /// The pitch classes a [`SegmentMatcher`] read the run as, where it
59    /// matched one: music21's `activeSegment`, by its pitch classes.
60    pub active: Vec<u8>,
61    /// What the run was matched to, where it was: music21's
62    /// `matchedSegment`.
63    pub matched: Option<Vec<IntegerType>>,
64}
65
66/// The pitches an element sounds, a chord symbol's included.
67fn pitches_of(element: &StreamElement) -> Vec<Pitch> {
68    match element {
69        StreamElement::ChordSymbol(symbol) => symbol.pitches().unwrap_or_default(),
70        _ => element.pitches(),
71    }
72}
73
74/// music21's `pitchClass`: the pitch space rounded half to even, folded
75/// into the octave.
76fn pitch_class(pitch: &Pitch) -> u8 {
77    (pitch.ps().round_ties_even() as i64).rem_euclid(12) as u8
78}
79
80impl ContiguousSegment<'_> {
81    /// The measure of the first note: music21's `startMeasureNumber`.
82    pub fn start_measure(&self) -> Option<IntegerType> {
83        self.notes.first().and_then(|note| note.measure)
84    }
85
86    /// The offset of the first note in the stream holding it: music21's
87    /// `startOffset`.
88    pub fn start_offset(&self) -> Option<FloatType> {
89        self.notes.first().map(|note| note.offset)
90    }
91
92    /// Every pitch class of every note and chord, each chord from its
93    /// bottom: music21's `readPitchClassesFromBottom`.
94    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    /// The same, each pitch class once: music21's
103    /// `getDistinctPitchClasses`.
104    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    /// The zero-centred transformations taking the row matched to the row
123    /// found: music21's `zeroCenteredTransformationsFromMatched`. Nothing
124    /// for a run not matched.
125    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    /// The original-centred transformations taking the row matched to the
132    /// row found: music21's `originalCenteredTransformationsFromMatched`.
133    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
140/// The parts of a stream, nested ones included, or the stream itself where
141/// it has none.
142fn 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
176/// The notes and chords of a part, each with its measure, its own offset
177/// and its leaf position, a note tied on from the one before left out:
178/// what music21's searcher walks.
179fn 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/// Finds every run of a stream's notes and chords that is a segment of a
227/// given length, part by part: music21's `ContiguousSegmentSearcher`.
228#[derive(Clone, Copy, Debug, Default)]
229pub struct ContiguousSegmentSearcher {
230    /// How repeated pitch classes are read.
231    pub repetitions: Repetitions,
232    /// Whether chords may be in a segment; where not, a chord breaks one.
233    pub include_chords: bool,
234    /// With [`Repetitions::IgnoreAll`], whether a run's first note is let go
235    /// as soon as it makes one segment: music21's `trimToShortestLengthFast`.
236    pub trim_to_shortest_length_fast: bool,
237}
238
239/// What a searcher keeps while it walks one part.
240struct 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    /// A searcher reading repetitions so, chords included or not.
275    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    /// Every segment of `length` pitch classes in each part of a stream, in
284    /// order: music21's `byLength`.
285    ///
286    /// # Errors
287    ///
288    /// A note sounding no pitch where music21 reads one, which music21
289    /// cannot read either.
290    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, &note) {
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, &note) {
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
334/// Whether a note sounds just the pitches of the last one kept.
335fn 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
341/// music21's `searchIncludeAllExclude`.
342fn include_all_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
343    if count(&note) > 1 {
344        walk.chords.clear();
345        return;
346    }
347    walk.total += count(&note);
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
358/// music21's `searchIncludeAllInclude`: each run ending on this note whose
359/// inner notes and chords hold fewer pitches than the length, and which
360/// holds at least as many, is a segment.
361fn include_all_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
362    walk.total += count(&note);
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
385/// music21's `searchIgnoreAllExclude`.
386fn ignore_all_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>, trim: bool) {
387    if count(&note) > 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
415/// music21's `searchIgnoreAllInclude`.
416fn 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
459/// music21's `searchRowsOnlyExclude`, which reads the `pitch` of what is
460/// left once chords are passed over, and so refuses anything but a note.
461fn rows_only_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) -> Result<()> {
462    if count(&note) > 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
493/// music21's `searchRowsOnlyInclude`.
494fn rows_only_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
495    walk.total += count(&note);
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/// How a [`SegmentMatcher`] tells a run of pitch classes the same as one
538/// searched for.
539#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
540pub enum Matching {
541    /// The same pitch classes in the same order: music21's
542    /// `SegmentMatcher`.
543    #[default]
544    Exact,
545    /// The same intervals in the same order: music21's
546    /// `TransposedSegmentMatcher`.
547    Transposed,
548    /// A transposition, inversion, retrograde or retrograde inversion:
549    /// music21's `TransformedSegmentMatcher`.
550    Transformed,
551    /// The same pitch classes in any order: music21's
552    /// `MultisetSegmentMatcher`.
553    Multiset,
554    /// The same pitch classes in any order, transposed: music21's
555    /// `TransposedMultisetMatcher`.
556    TransposedMultiset,
557    /// The same pitch classes in any order, transposed or inverted:
558    /// music21's `TransposedInvertedMultisetMatcher`.
559    TransposedInvertedMultiset,
560}
561
562/// A segment as music21 normalizes it to compare.
563#[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
580/// A multiset of pitch classes, as a sorted list.
581fn 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
635/// What a matcher has searched for already in one [`SegmentMatcher::find`].
636enum Searched {
637    Normals(Vec<Normal>),
638    Multisets(Vec<Vec<i64>>),
639}
640
641impl Searched {
642    /// music21's `checkSearchedAlready`: whether a segment is one searched
643    /// for already, noting it if not.
644    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                // music21 keeps the last transposition it tried.
678                seen.push(multiset(raw.iter().map(|p| (p + 11).rem_euclid(12))));
679                false
680            }
681        }
682    }
683}
684
685/// Finds the runs of a stream's notes and chords that are segments or
686/// sets of pitch classes searched for: music21's `SegmentMatcher` and the
687/// matchers built on it, chosen by [`Matching`].
688#[derive(Clone, Copy, Debug, Default)]
689pub struct SegmentMatcher {
690    /// How a run is told the same as a segment searched for.
691    pub matching: Matching,
692    /// How repeated pitch classes are read.
693    pub repetitions: Repetitions,
694    /// Whether chords may be in a run.
695    pub include_chords: bool,
696}
697
698impl SegmentMatcher {
699    /// A matcher of this kind, reading repetitions so, chords included or
700    /// not.
701    pub fn new(matching: Matching, repetitions: Repetitions, include_chords: bool) -> Self {
702        Self {
703            matching,
704            repetitions,
705            include_chords,
706        }
707    }
708
709    /// Each run that matches one of the segments searched for, by the
710    /// first it matches, in the order searched and found: music21's `find`.
711    /// A segment the same as one searched for already is passed over. A run
712    /// matching two segments is listed for each, as music21 lists the one
713    /// run twice, both times as it was matched last.
714    ///
715    /// # Errors
716    ///
717    /// As [`ContiguousSegmentSearcher::by_length`].
718    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    /// music21's `findOneIgnoreAll`: the pitch classes a run is read as,
766    /// where it matches.
767    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    /// music21's `findOneOtherReps`.
809    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    /// A copy of the stream with each run found marked, as music21's
838    /// `labelSegments` and its kin mark them: a line spanner from its first
839    /// note to its last, in its part, and the name of the segment it matched
840    /// as a lyric of its first note, unless the note already carries that
841    /// lyric. Runs are marked part by part, measure by measure, offset by
842    /// offset. With a `convention`, as music21's `labelTransformedSegments`
843    /// takes one, each name is followed by the transformations taking the
844    /// segment to the run, as ` ,T0` or ` ,P0`.
845    ///
846    /// # Errors
847    ///
848    /// As [`SegmentMatcher::find`].
849    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
945/// The part a searcher numbered so, in a stream's walk.
946fn 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        // Each read off music21's ContiguousSegmentSearcher for the line.
974        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        // Each read off music21's TransposedSegmentMatcher and
989        // labelTransposedSegments for the same line.
990        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}