Skip to main content

music21/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 no_pitches() -> Error {
270    Error::Search("a chord at the end of the segment holds no pitches".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(no_pitches)?));
441            check.push(pitch_class(last.first().ok_or_else(no_pitches)?));
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    /// Whether a run's pitch classes are the segment searched for, as
605    /// this matcher tells the two the same: music21's `equalSubset`, given
606    /// the classes each reading was made from rather than the readings.
607    ///
608    /// The order is music21's: the segment searched for first, which is
609    /// the one a transposing matcher moves.
610    pub fn equal_segments(self, search: &[IntegerType], subset: &[IntegerType]) -> bool {
611        self.equal(&self.normalize(search), &self.normalize(subset))
612    }
613
614    fn equal(self, search: &Normal, subset: &Normal) -> bool {
615        let classes = |normal: &Normal| -> Vec<i64> {
616            match normal {
617                Normal::Classes(classes) => classes.iter().map(|class| i64::from(*class)).collect(),
618                _ => Vec::new(),
619            }
620        };
621        match self {
622            Self::Exact | Self::Transposed => search == subset,
623            Self::Transformed => match (search, subset) {
624                (Normal::Row(search), Normal::Row(subset)) => {
625                    !subset.find_zero_centered_transformations(search).is_empty()
626                }
627                _ => false,
628            },
629            Self::Multiset => multiset(classes(search)) == multiset(classes(subset)),
630            Self::TransposedMultiset | Self::TransposedInvertedMultiset => {
631                let wanted = multiset(classes(subset));
632                let search = classes(search);
633                let transposed = (0..12)
634                    .any(|i| multiset(search.iter().map(|p| (p + i).rem_euclid(12))) == wanted);
635                transposed
636                    || (self == Self::TransposedInvertedMultiset
637                        && (0..12).any(|i| {
638                            multiset(search.iter().map(|p| (-(p + i)).rem_euclid(12))) == wanted
639                        }))
640            }
641        }
642    }
643}
644
645/// What a matcher has searched for already in one [`SegmentMatcher::find`].
646enum Searched {
647    Normals(Vec<Normal>),
648    Multisets(Vec<Vec<i64>>),
649}
650
651impl Searched {
652    /// music21's `checkSearchedAlready`: whether a segment is one searched
653    /// for already, noting it if not.
654    fn already(&mut self, matching: Matching, segment: &[IntegerType]) -> bool {
655        match self {
656            Self::Normals(seen) => {
657                let normal = matching.normalize(segment);
658                let found = if matching == Matching::Transformed {
659                    seen.iter().any(|used| match (&normal, used) {
660                        (Normal::Row(row), Normal::Row(used)) => {
661                            !used.find_zero_centered_transformations(row).is_empty()
662                        }
663                        _ => false,
664                    })
665                } else {
666                    seen.contains(&normal)
667                };
668                if !found {
669                    seen.push(normal);
670                }
671                found
672            }
673            Self::Multisets(seen) => {
674                let raw: Vec<i64> = segment.iter().map(|p| i64::from(*p)).collect();
675                if matching == Matching::TransposedInvertedMultiset
676                    && (0..12).any(|i| {
677                        seen.contains(&multiset(raw.iter().map(|p| (-(p + i)).rem_euclid(12))))
678                    })
679                {
680                    return true;
681                }
682                if (0..12)
683                    .any(|i| seen.contains(&multiset(raw.iter().map(|p| (p + i).rem_euclid(12)))))
684                {
685                    return true;
686                }
687                // music21 keeps the last transposition it tried.
688                seen.push(multiset(raw.iter().map(|p| (p + 11).rem_euclid(12))));
689                false
690            }
691        }
692    }
693}
694
695/// Finds the runs of a stream's notes and chords that are segments or
696/// sets of pitch classes searched for: music21's `SegmentMatcher` and the
697/// matchers built on it, chosen by [`Matching`].
698#[derive(Clone, Copy, Debug, Default)]
699pub struct SegmentMatcher {
700    /// How a run is told the same as a segment searched for.
701    pub matching: Matching,
702    /// How repeated pitch classes are read.
703    pub repetitions: Repetitions,
704    /// Whether chords may be in a run.
705    pub include_chords: bool,
706}
707
708impl SegmentMatcher {
709    /// A matcher of this kind, reading repetitions so, chords included or
710    /// not.
711    pub fn new(matching: Matching, repetitions: Repetitions, include_chords: bool) -> Self {
712        Self {
713            matching,
714            repetitions,
715            include_chords,
716        }
717    }
718
719    /// Each run that matches one of the segments searched for, by the
720    /// first it matches, in the order searched and found: music21's `find`.
721    /// A segment the same as one searched for already is passed over. A run
722    /// matching two segments is listed for each, as music21 lists the one
723    /// run twice, both times as it was matched last.
724    ///
725    /// # Errors
726    ///
727    /// As [`ContiguousSegmentSearcher::by_length`].
728    pub fn find<'a>(
729        &self,
730        stream: &'a Stream,
731        search: &[Vec<IntegerType>],
732    ) -> Result<Vec<ContiguousSegment<'a>>> {
733        let searcher = ContiguousSegmentSearcher::new(self.repetitions, self.include_chords);
734        let mut by_length: HashMap<usize, Vec<ContiguousSegment<'a>>> = HashMap::new();
735        let mut matched: Vec<(usize, usize)> = Vec::new();
736        let mut searched = if matches!(
737            self.matching,
738            Matching::TransposedMultiset | Matching::TransposedInvertedMultiset
739        ) {
740            Searched::Multisets(Vec::new())
741        } else {
742            Searched::Normals(Vec::new())
743        };
744        for segment in search {
745            if searched.already(self.matching, segment) {
746                continue;
747            }
748            let normal = self.matching.normalize(segment);
749            let length = segment.len();
750            let candidates = match by_length.entry(length) {
751                std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
752                std::collections::hash_map::Entry::Vacant(entry) => {
753                    entry.insert(searcher.by_length(stream, length)?)
754                }
755            };
756            for (index, candidate) in candidates.iter_mut().enumerate() {
757                let found = if self.repetitions == Repetitions::IgnoreAll {
758                    self.match_ignore_all(candidate, &normal, length)
759                } else {
760                    self.match_other(candidate, &normal, length)
761                };
762                if let Some(active) = found {
763                    candidate.active = active;
764                    candidate.matched = Some(segment.clone());
765                    matched.push((length, index));
766                }
767            }
768        }
769        Ok(matched
770            .into_iter()
771            .map(|(length, index)| by_length[&length][index].clone())
772            .collect())
773    }
774
775    /// music21's `findOneIgnoreAll`: the pitch classes a run is read as,
776    /// where it matches.
777    fn match_ignore_all(
778        &self,
779        candidate: &ContiguousSegment<'_>,
780        search: &Normal,
781        length: usize,
782    ) -> Option<Vec<u8>> {
783        let classes = candidate.distinct_pitch_classes();
784        let first: Vec<u8> = pitches_of(candidate.notes.first()?.element)
785            .iter()
786            .map(pitch_class)
787            .collect();
788        let before_last: Vec<u8> = candidate.notes[..candidate.notes.len() - 1]
789            .iter()
790            .flat_map(|note| pitches_of(note.element))
791            .map(|pitch| pitch_class(&pitch))
792            .collect();
793        let starts = classes.len() as isize - search.len() as isize + 1;
794        for start in 0..starts.max(0) as usize {
795            let subset = &classes[start..(start + length).min(classes.len())];
796            let as_ints: Vec<IntegerType> = subset
797                .iter()
798                .map(|class| IntegerType::from(*class))
799                .collect();
800            if !self
801                .matching
802                .equal(search, &self.matching.normalize(&as_ints))
803            {
804                continue;
805            }
806            if !first.contains(&subset[0]) {
807                continue;
808            }
809            if self.matching.multiset_duplicates()
810                || !before_last.contains(&subset[subset.len() - 1])
811            {
812                return Some(subset.to_vec());
813            }
814        }
815        None
816    }
817
818    /// music21's `findOneOtherReps`.
819    fn match_other(
820        &self,
821        candidate: &ContiguousSegment<'_>,
822        search: &Normal,
823        length: usize,
824    ) -> Option<Vec<u8>> {
825        let classes = candidate.pitch_classes_from_bottom();
826        let first = count(candidate.notes.first()?);
827        let last = count(candidate.notes.last()?);
828        let lower = (classes.len() as isize - length as isize - last as isize + 1).max(0);
829        let upper = (first as isize).min(classes.len() as isize + 1 - length as isize);
830        for start in lower..upper {
831            let start = start as usize;
832            let subset = &classes[start..(start + length).min(classes.len())];
833            let as_ints: Vec<IntegerType> = subset
834                .iter()
835                .map(|class| IntegerType::from(*class))
836                .collect();
837            if self
838                .matching
839                .equal(search, &self.matching.normalize(&as_ints))
840            {
841                return Some(subset.to_vec());
842            }
843        }
844        None
845    }
846
847    /// Where each run found is to be marked and with what: the
848    /// by-position twin of [`SegmentMatcher::label`], which is built on
849    /// it, for a caller marking the objects the stream was read from
850    /// rather than a stream of the crate's.
851    ///
852    /// Runs come part by part, measure by measure, offset by offset, as
853    /// music21 marks them, and a run matched to nothing is named nothing.
854    ///
855    /// # Errors
856    ///
857    /// As [`SegmentMatcher::find`].
858    pub fn label_places(
859        &self,
860        stream: &Stream,
861        named: &[(String, Vec<IntegerType>)],
862        convention: Option<TransformationConvention>,
863    ) -> Result<Vec<SegmentLabel>> {
864        let search: Vec<Vec<IntegerType>> =
865            named.iter().map(|(_, segment)| segment.clone()).collect();
866        let mut segments = self.find(stream, &search)?;
867        segments.sort_by(|a, b| {
868            (a.part, a.start_measure())
869                .cmp(&(b.part, b.start_measure()))
870                .then_with(|| {
871                    a.start_offset()
872                        .partial_cmp(&b.start_offset())
873                        .unwrap_or(std::cmp::Ordering::Equal)
874                })
875        });
876        let mut found = Vec::new();
877        for segment in &segments {
878            let (Some(first), Some(last)) = (segment.notes.first(), segment.notes.last()) else {
879                continue;
880            };
881            let Some((name, _)) = named
882                .iter()
883                .find(|(_, wanted)| Some(wanted) == segment.matched.as_ref())
884            else {
885                found.push(SegmentLabel {
886                    part: segment.part,
887                    first: first.leaf,
888                    last: last.leaf,
889                    label: String::new(),
890                });
891                continue;
892            };
893            let transformations = match convention {
894                Some(TransformationConvention::OriginalCentered) => {
895                    segment.original_centered_transformations_from_matched()
896                }
897                Some(TransformationConvention::ZeroCentered) => {
898                    segment.zero_centered_transformations_from_matched()
899                }
900                None => Vec::new(),
901            };
902            let mut label = name.clone();
903            for (transformation, index) in transformations {
904                let convention = convention.unwrap_or(TransformationConvention::ZeroCentered);
905                label.push_str(&format!(" ,{}{index}", transformation.label(convention)));
906            }
907            found.push(SegmentLabel {
908                part: segment.part,
909                first: first.leaf,
910                last: last.leaf,
911                label,
912            });
913        }
914        Ok(found)
915    }
916
917    /// A copy of the stream with each run found marked, as music21's
918    /// `labelSegments` and its kin mark them: a line spanner from its first
919    /// note to its last, in its part, and the name of the segment it matched
920    /// as a lyric of its first note, unless the note already carries that
921    /// lyric. Runs are marked part by part, measure by measure, offset by
922    /// offset. With a `convention`, as music21's `labelTransformedSegments`
923    /// takes one, each name is followed by the transformations taking the
924    /// segment to the run, as ` ,T0` or ` ,P0`.
925    ///
926    /// # Errors
927    ///
928    /// As [`SegmentMatcher::find`].
929    pub fn label(
930        &self,
931        stream: &Stream,
932        named: &[(String, Vec<IntegerType>)],
933        convention: Option<TransformationConvention>,
934    ) -> Result<Stream> {
935        let found = self.label_places(stream, named, convention)?;
936        let mut labelled = stream.clone();
937        for SegmentLabel {
938            part,
939            first,
940            last,
941            label,
942        } in found
943        {
944            let target = match part {
945                Some(part) => part_mut(&mut labelled, part)
946                    .ok_or_else(|| Error::Search(format!("the stream has no part {part}")))?,
947                None => &mut labelled,
948            };
949            let spanned = if first == last {
950                vec![first]
951            } else {
952                vec![first, last]
953            };
954            target.add_spanner(Spanner::line(spanned));
955            if label.is_empty() {
956                continue;
957            }
958            let mut leaf = 0;
959            let mut result = Ok(());
960            target.for_each_mut(&mut |_, element| {
961                if leaf == first {
962                    let lyrics = match element {
963                        StreamElement::Note(note) => Some(note.lyrics().to_vec()),
964                        StreamElement::Chord(chord) => Some(chord.lyrics().to_vec()),
965                        StreamElement::ChordSymbol(symbol) => Some(symbol.lyrics().to_vec()),
966                        _ => None,
967                    };
968                    let present = lyrics
969                        .is_some_and(|lyrics| lyrics.iter().any(|lyric| lyric.text() == label));
970                    if !present {
971                        result = match element {
972                            StreamElement::Note(note) => note.add_lyric(&label, None, false),
973                            StreamElement::Chord(chord) => chord.add_lyric(&label, None, false),
974                            StreamElement::ChordSymbol(symbol) => {
975                                symbol.add_lyric(&label, None, false);
976                                Ok(())
977                            }
978                            _ => Ok(()),
979                        };
980                    }
981                }
982                leaf += 1;
983            });
984            result?;
985        }
986        Ok(labelled)
987    }
988}
989
990/// Where one run found is to be marked, and with what: the part it is in,
991/// the places of its first and last notes among that part's leaves, and
992/// the name it is marked with, which is empty for a run matched to
993/// nothing.
994#[derive(Clone, Debug, PartialEq, Eq)]
995pub struct SegmentLabel {
996    /// The part the run is in, counted from nought, or nothing where the
997    /// stream searched has no parts.
998    pub part: Option<usize>,
999    /// Where its first note stands among the part's leaves.
1000    pub first: usize,
1001    /// Where its last note stands.
1002    pub last: usize,
1003    /// The name to write as a lyric of the first note.
1004    pub label: String,
1005}
1006
1007/// The part a searcher numbered so, in a stream's walk.
1008fn part_mut(stream: &mut Stream, number: usize) -> Option<&mut Stream> {
1009    fn walk<'a>(stream: &'a mut Stream, number: usize, seen: &mut usize) -> Option<&'a mut Stream> {
1010        for event in stream.events_mut() {
1011            if let StreamElement::Stream(inner) = event.element_mut() {
1012                if matches!(inner.kind(), StreamKind::Part | StreamKind::PartStaff) {
1013                    if *seen == number {
1014                        return Some(inner);
1015                    }
1016                    *seen += 1;
1017                }
1018                if let Some(found) = walk(inner, number, seen) {
1019                    return Some(found);
1020                }
1021            }
1022        }
1023        None
1024    }
1025    walk(stream, number, &mut 0)
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030    use super::*;
1031    use crate::tinynotation::from_tiny_notation;
1032
1033    #[test]
1034    fn segments_are_found_by_length() -> Result<()> {
1035        // Each read off music21's ContiguousSegmentSearcher for the line.
1036        let line = from_tiny_notation("4/4 c4 d d e f g a b")?;
1037        let skip = ContiguousSegmentSearcher::new(Repetitions::SkipConsecutive, true);
1038        let found = skip.by_length(&line, 3)?;
1039        assert_eq!(found.len(), 5);
1040        assert_eq!(found[0].pitch_classes_from_bottom(), [0, 2, 4]);
1041        let all = ContiguousSegmentSearcher::new(Repetitions::IncludeAll, true);
1042        let found = all.by_length(&line, 3)?;
1043        assert_eq!(found.len(), 6);
1044        assert_eq!(found[1].pitch_classes_from_bottom(), [2, 2, 4]);
1045        Ok(())
1046    }
1047
1048    #[test]
1049    fn rows_are_matched_and_labelled() -> Result<()> {
1050        // Each read off music21's TransposedSegmentMatcher and
1051        // labelTransposedSegments for the same line.
1052        let line = from_tiny_notation("4/4 c4 d e f g a b c'")?;
1053        let matcher = SegmentMatcher::new(Matching::Transposed, Repetitions::SkipConsecutive, true);
1054        let found = matcher.find(&line, &[vec![0, 2, 4]])?;
1055        let actives: Vec<&[u8]> = found
1056            .iter()
1057            .map(|segment| segment.active.as_slice())
1058            .collect();
1059        assert_eq!(actives, [&[0, 2, 4][..], &[5, 7, 9], &[7, 9, 11]]);
1060        let labelled = matcher.label(&line, &[("A".to_string(), vec![0, 2, 4])], None)?;
1061        let spanners: usize = labelled.spanners().len()
1062            + labelled
1063                .parts()
1064                .iter()
1065                .map(|part| part.spanners().len())
1066                .sum::<usize>();
1067        assert_eq!(spanners, 3);
1068        let lyrics: Vec<Option<String>> = labelled
1069            .leaves()
1070            .into_iter()
1071            .filter_map(|(_, element)| match element {
1072                StreamElement::Note(note) => Some(note.lyrics().first().map(|lyric| lyric.text())),
1073                _ => None,
1074            })
1075            .collect();
1076        let a = Some("A".to_string());
1077        assert_eq!(
1078            lyrics,
1079            [a.clone(), None, None, a.clone(), a, None, None, None]
1080        );
1081        Ok(())
1082    }
1083}