Skip to main content

music21/features/
mod.rs

1//! Features of a piece of music, extracted for machine learning and corpus
2//! study: music21's `features` package.
3//!
4//! A [`DataInstance`] prepares a stream once -- ties stripped, every part
5//! and voice prepared on its own -- and works out each representation an
6//! extractor asks for the first time one does: its pitches, its pitch and
7//! interval histograms, its melodic contour. An [`Extractor`] reads those
8//! and answers a [`Feature`], a vector of numbers of a fixed length, each a
9//! [`Value`] that is a whole number or a measurement as music21's is.
10//! [`jsymbolic::JSYMBOLIC`] holds the extractors music21 ports from Cory
11//! McKay's jSymbolic, and [`native::NATIVE`] those music21 adds of its own;
12//! [`extractors_by_id`] finds them by id. A [`DataSet`] reads features of
13//! many pieces into one table and writes it for a machine-learning tool.
14//!
15//! ```
16//! use music21::features::{DataInstance, jsymbolic};
17//! use music21::tinynotation::from_tiny_notation;
18//!
19//! let line = from_tiny_notation("4/4 c4 d e f g a b c'")?;
20//! let data = DataInstance::new(&line)?;
21//! let range = jsymbolic::extractor("P10").expect("the range extractor");
22//! assert_eq!(range.extract(&data)?.vector(), [12.0]);
23//! # Ok::<(), music21::Error>(())
24//! ```
25
26mod dataset;
27pub mod jsymbolic;
28/// Which language a text is in: music21's `Trigram`, and its
29/// `LanguageDetector` with the `language-detection` feature.
30pub mod language;
31pub mod native;
32
33pub use dataset::{
34    Cell, DataSet, Failure, Library, OutputFormat, all_features_as_list, extractor_by_id,
35    extractors_by_id, index_of, vector_by_id,
36};
37
38use std::cell::OnceCell;
39
40use crate::{
41    analysis::{self, KeyEstimate, KeyProfile, pitch_analysis},
42    chord::Chord,
43    defaults::{FloatType, IntegerType},
44    error::{Error, Result},
45    meter::TimeSignature,
46    pitch::Pitch,
47    stream::{ConsecutiveOptions, Stream, StreamElement, StreamEvent, StreamKind},
48    tempo::MetronomeMark,
49};
50
51/// One value of a [`Feature`]: a whole number, as a count or an index is,
52/// or a float, as a share or a measurement is. music21 keeps the two apart,
53/// a vector starting as whole noughts, and writes them apart -- `4` and
54/// `4.0` -- in a data set.
55#[derive(Clone, Copy, Debug, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub enum Value {
58    /// A whole number.
59    Integer(i64),
60    /// A float.
61    Float(FloatType),
62}
63
64impl Value {
65    /// The value as a float, whichever it is.
66    pub fn as_float(self) -> FloatType {
67        match self {
68            Self::Integer(value) => value as FloatType,
69            Self::Float(value) => value,
70        }
71    }
72
73    /// Whether it is a whole number rather than a float.
74    pub fn is_integer(self) -> bool {
75        matches!(self, Self::Integer(_))
76    }
77}
78
79impl Default for Value {
80    fn default() -> Self {
81        Self::Integer(0)
82    }
83}
84
85/// Written as Python writes it: `4` for the whole number, `4.0` for the
86/// float, the fewest digits that read back as the float.
87impl std::fmt::Display for Value {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Self::Integer(value) => write!(f, "{value}"),
91            Self::Float(value) => f.write_str(&crate::statistics::python_repr(*value)),
92        }
93    }
94}
95
96macro_rules! whole_values {
97    ($($kind:ty),*) => {$(
98        impl From<$kind> for Value {
99            fn from(value: $kind) -> Self {
100                Self::Integer(value as i64)
101            }
102        }
103    )*};
104}
105
106whole_values!(i32, i64, u8, u32, usize);
107
108impl From<FloatType> for Value {
109    fn from(value: FloatType) -> Self {
110        Self::Float(value)
111    }
112}
113
114/// What an [`Extractor`] answers: its name and the vector it found.
115#[derive(Clone, Debug, PartialEq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct Feature {
118    id: String,
119    name: String,
120    discrete: bool,
121    values: Vec<Value>,
122}
123
124impl Feature {
125    /// The extractor's id: jSymbolic's code for it, such as `P10`.
126    pub fn id(&self) -> &str {
127        &self.id
128    }
129
130    /// The feature's name.
131    pub fn name(&self) -> &str {
132        &self.name
133    }
134
135    /// Whether the values are counts rather than measurements.
136    pub fn discrete(&self) -> bool {
137        self.discrete
138    }
139
140    /// The values as floats, as many as the extractor has dimensions.
141    pub fn vector(&self) -> Vec<FloatType> {
142        self.values.iter().map(|value| value.as_float()).collect()
143    }
144
145    /// The values, each a whole number or a float as music21's is.
146    pub fn values(&self) -> &[Value] {
147        &self.values
148    }
149
150    /// The labels music21 gives the values in a data set: the name with
151    /// underscores for spaces, numbered where there is more than one.
152    pub fn attribute_labels(&self) -> Vec<String> {
153        attribute_labels(&self.name, self.values.len())
154    }
155}
156
157/// music21's `getAttributeLabels`: the name with underscores for spaces,
158/// numbered where there is more than one value.
159fn attribute_labels(name: &str, dimensions: usize) -> Vec<String> {
160    let name = name.replace(' ', "_");
161    if dimensions == 1 {
162        vec![name]
163    } else {
164        (0..dimensions)
165            .map(|index| format!("{name}_{index}"))
166            .collect()
167    }
168}
169
170/// One way of reading a feature out of a piece: music21's
171/// `FeatureExtractor`, with what its class says about the feature.
172#[derive(Clone, Copy)]
173pub struct Extractor {
174    id: &'static str,
175    name: &'static str,
176    description: &'static str,
177    dimensions: usize,
178    discrete: bool,
179    normalize: bool,
180    process: fn(&DataInstance, &mut [Value]) -> Result<()>,
181}
182
183impl std::fmt::Debug for Extractor {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.debug_struct("Extractor")
186            .field("id", &self.id)
187            .field("name", &self.name)
188            .field("dimensions", &self.dimensions)
189            .finish()
190    }
191}
192
193impl Extractor {
194    /// The extractor's id.
195    pub fn id(&self) -> &'static str {
196        self.id
197    }
198
199    /// What the feature is called.
200    pub fn name(&self) -> &'static str {
201        self.name
202    }
203
204    /// What the feature measures.
205    pub fn description(&self) -> &'static str {
206        self.description
207    }
208
209    /// How many values the feature has.
210    pub fn dimensions(&self) -> usize {
211        self.dimensions
212    }
213
214    /// Whether the values are counts rather than measurements.
215    pub fn discrete(&self) -> bool {
216        self.discrete
217    }
218
219    /// Whether the values are scaled to sum to one.
220    pub fn normalize(&self) -> bool {
221        self.normalize
222    }
223
224    /// The labels of the values in a data set: the name with underscores
225    /// for spaces, numbered where there is more than one value.
226    pub fn attribute_labels(&self) -> Vec<String> {
227        attribute_labels(self.name, self.dimensions)
228    }
229
230    /// The feature as it stands before anything is read: every value a
231    /// whole nought. music21's `getBlankFeature`, which a data set stands
232    /// in for a feature that cannot be read.
233    pub fn blank(&self) -> Feature {
234        Feature {
235            id: self.id.to_string(),
236            name: self.name.to_string(),
237            discrete: self.discrete,
238            values: vec![Value::Integer(0); self.dimensions],
239        }
240    }
241
242    /// The feature of a prepared piece: music21's `extract`.
243    ///
244    /// # Errors
245    ///
246    /// A piece the feature cannot be read from -- one with no notes, for
247    /// most of them -- or a normalized feature whose values are all nought.
248    pub fn extract(&self, data: &DataInstance) -> Result<Feature> {
249        let mut feature = self.blank();
250        (self.process)(data, &mut feature.values)?;
251        if self.normalize {
252            let sum = python_sum_of(&feature.values);
253            if sum == 0.0 {
254                return Err(Error::Feature("cannot normalize zero vector".to_string()));
255            }
256            // music21 multiplies by the reciprocal rather than dividing.
257            let scalar = 1.0 / sum;
258            for value in &mut feature.values {
259                *value = Value::Float(value.as_float() * scalar);
260            }
261        }
262        Ok(feature)
263    }
264}
265
266/// The sum of some values as Python's `sum` makes it: the whole numbers
267/// before the first float added exactly, the rest by compensated
268/// summation.
269fn python_sum_of(values: &[Value]) -> FloatType {
270    let whole = values.iter().take_while(|value| value.is_integer()).count();
271    let exact: i64 = values[..whole]
272        .iter()
273        .map(|value| match value {
274            Value::Integer(value) => *value,
275            Value::Float(_) => 0,
276        })
277        .sum();
278    let mut rest = vec![exact as FloatType];
279    rest.extend(values[whole..].iter().map(|value| value.as_float()));
280    crate::statistics::python_sum(&rest)
281}
282
283/// A piece prepared for feature extraction, with each representation of it
284/// worked out once: music21's `DataInstance` and the `StreamForms` it keeps.
285#[derive(Debug)]
286pub struct DataInstance {
287    prepared: Stream,
288    /// The parts of a score and then every voice anywhere in the piece,
289    /// each prepared on its own, as music21 lists them.
290    parts: Vec<DataInstance>,
291    parts_count: usize,
292    pitches: OnceCell<Vec<Pitch>>,
293    midi_pitch_histogram: OnceCell<Vec<(IntegerType, usize)>>,
294    pitch_class_histogram: OnceCell<[usize; 12]>,
295    midi_interval_histogram: OnceCell<Vec<usize>>,
296    contour: OnceCell<Vec<IntegerType>>,
297    seconds: OnceCell<std::result::Result<Vec<Seconds>, Error>>,
298    chordified: OnceCell<std::result::Result<Chordified, Error>>,
299    quarter_length_histogram: OnceCell<Vec<(FloatType, usize)>>,
300    analyzed_key: OnceCell<Option<Vec<KeyEstimate>>>,
301}
302
303/// The chords of a piece chordified, and how many parts sound in each.
304#[derive(Debug)]
305struct Chordified {
306    chords: Vec<Chord>,
307    parts_sounding: Vec<usize>,
308}
309
310/// Where a note sounds in time, in seconds at the tempi of the piece:
311/// music21's `secondsMap` entry.
312#[derive(Clone, Copy, Debug, PartialEq)]
313#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
314pub struct Seconds {
315    /// When the note starts.
316    pub offset: FloatType,
317    /// How long it sounds.
318    pub duration: FloatType,
319    /// When it stops.
320    pub end: FloatType,
321}
322
323impl DataInstance {
324    /// Prepares a piece: its ties stripped, and each part of a score and
325    /// each voice anywhere in it prepared on its own.
326    ///
327    /// # Errors
328    ///
329    /// A tie the piece cannot be stripped of.
330    pub fn new(stream: &Stream) -> Result<Self> {
331        let mut instance = Self::forms(stream)?;
332        if stream.kind() == StreamKind::Score {
333            let parts = stream.parts();
334            instance.parts_count = parts.len();
335            for part in parts {
336                instance.parts.push(Self::forms(part)?);
337            }
338        }
339        for (_, element) in stream.recurse() {
340            if let StreamElement::Stream(voice) = element
341                && voice.kind() == StreamKind::Voice
342            {
343                instance.parts.push(Self::forms(voice)?);
344            }
345        }
346        Ok(instance)
347    }
348
349    /// music21's `StreamForms`: the stream with its ties stripped.
350    fn forms(stream: &Stream) -> Result<Self> {
351        let mut prepared = stream.clone();
352        prepared.strip_ties(true)?;
353        Ok(Self {
354            prepared,
355            parts: Vec::new(),
356            parts_count: 0,
357            pitches: OnceCell::new(),
358            midi_pitch_histogram: OnceCell::new(),
359            pitch_class_histogram: OnceCell::new(),
360            midi_interval_histogram: OnceCell::new(),
361            contour: OnceCell::new(),
362            seconds: OnceCell::new(),
363            chordified: OnceCell::new(),
364            quarter_length_histogram: OnceCell::new(),
365            analyzed_key: OnceCell::new(),
366        })
367    }
368
369    /// The piece with its ties stripped.
370    pub fn prepared(&self) -> &Stream {
371        &self.prepared
372    }
373
374    /// Each part of a score prepared on its own, then each voice: music21's
375    /// `data['parts']`.
376    pub fn parts(&self) -> &[DataInstance] {
377        &self.parts
378    }
379
380    /// How many parts a score has: music21's `partsCount`, nought for a
381    /// stream that is not a score.
382    pub fn parts_count(&self) -> usize {
383        self.parts_count
384    }
385
386    /// Every pitch of the piece's notes, chords and chord symbols: music21's
387    /// `Stream.pitches`.
388    pub fn pitches(&self) -> &[Pitch] {
389        self.pitches
390            .get_or_init(|| pitch_analysis::music21_pitches(&self.prepared).unwrap_or_default())
391    }
392
393    /// How often each MIDI note number sounds, in the order each is first
394    /// met: music21's `midiPitchHistogram`, a `Counter`.
395    pub fn midi_pitch_histogram(&self) -> &[(IntegerType, usize)] {
396        self.midi_pitch_histogram.get_or_init(|| {
397            let mut counts: Vec<(IntegerType, usize)> = Vec::new();
398            for pitch in self.pitches() {
399                let midi = pitch.midi();
400                match counts.iter_mut().find(|(known, _)| *known == midi) {
401                    Some((_, count)) => *count += 1,
402                    None => counts.push((midi, 1)),
403                }
404            }
405            counts
406        })
407    }
408
409    /// How often each pitch class sounds: music21's `pitchClassHistogram`.
410    pub fn pitch_class_histogram(&self) -> &[usize; 12] {
411        self.pitch_class_histogram.get_or_init(|| {
412            let mut counts = [0; 12];
413            for pitch in self.pitches() {
414                // music21's `pitchClass`: the pitch space rounded half to
415                // even, folded into the octave.
416                counts[(pitch.ps().round_ties_even() as i64).rem_euclid(12) as usize] += 1;
417            }
418            counts
419        })
420    }
421
422    /// How often each melodic interval, in semitones either way, comes
423    /// between neighbouring notes of each part, chords and rests passed
424    /// over: music21's `midiIntervalHistogram`, 128 bins.
425    pub fn midi_interval_histogram(&self) -> &[usize] {
426        self.midi_interval_histogram.get_or_init(|| {
427            let mut counts = vec![0; 128];
428            let options = ConsecutiveOptions {
429                skip_rests: true,
430                skip_chords: true,
431                skip_gaps: true,
432                no_none: true,
433                ..ConsecutiveOptions::default()
434            };
435            for part in self.melodic_parts(self.prepared.kind() == StreamKind::Score) {
436                let midis = consecutive_midis(part, &options, false);
437                for pair in midis.windows(2) {
438                    let size = (pair[0] - pair[1]).unsigned_abs() as usize;
439                    if let Some(count) = counts.get_mut(size) {
440                        *count += 1;
441                    }
442                }
443            }
444            counts
445        })
446    }
447
448    /// Every melodic step of each part in semitones, rising steps positive,
449    /// a chord read as its highest note: music21's `contourList`.
450    pub fn contour_list(&self) -> &[IntegerType] {
451        self.contour.get_or_init(|| {
452            let options = ConsecutiveOptions {
453                skip_rests: true,
454                skip_gaps: true,
455                no_none: true,
456                ..ConsecutiveOptions::default()
457            };
458            let mut contour = Vec::new();
459            for part in self.melodic_parts(self.prepared.has_part_like_streams()) {
460                let midis = consecutive_midis(part, &options, true);
461                contour.extend(midis.windows(2).map(|pair| pair[1] - pair[0]));
462            }
463            contour
464        })
465    }
466
467    /// The stretches of the piece each tempo holds, start, end and the mark
468    /// sounding there: [`Stream::metronome_mark_boundaries`] of the piece as
469    /// this instance prepared it.
470    ///
471    /// # Errors
472    ///
473    /// As [`Stream::metronome_mark_boundaries`].
474    pub fn metronome_mark_boundaries(&self) -> Result<Vec<(FloatType, FloatType, MetronomeMark)>> {
475        self.prepared.metronome_mark_boundaries()
476    }
477
478    /// When each note, chord and chord symbol of the piece sounds, in
479    /// seconds, in the order the piece holds them: the notes of music21's
480    /// `secondsMap`, read over the piece *flattened*, where music21's own
481    /// `secondsMap` reads only what a stream holds directly. The features
482    /// that read it want every note of the piece.
483    ///
484    /// # Errors
485    ///
486    /// A tempo that says no number, or as
487    /// [`DataInstance::metronome_mark_boundaries`].
488    pub fn seconds_map(&self) -> Result<&[Seconds]> {
489        self.seconds
490            .get_or_init(|| self.work_out_seconds())
491            .as_deref()
492            .map_err(Clone::clone)
493    }
494
495    fn work_out_seconds(&self) -> Result<Vec<Seconds>> {
496        let boundaries = self.metronome_mark_boundaries()?;
497        let flat = self.prepared.flatten();
498        let lowest = flat.events().first().map_or(0.0, StreamEvent::offset);
499        let mut seconds = Vec::new();
500        for event in flat.events() {
501            let element = event.element();
502            if !matches!(
503                element,
504                StreamElement::Note(_)
505                    | StreamElement::Chord(_)
506                    | StreamElement::Unpitched(_)
507                    | StreamElement::PercussionChord(_)
508                    | StreamElement::ChordSymbol(_)
509            ) {
510                continue;
511            }
512            // music21 rounds the offset to eight places first.
513            let offset = crate::statistics::python_round(event.offset(), 8);
514            let start = accumulated_seconds(&boundaries, lowest, offset)?;
515            let duration =
516                accumulated_seconds(&boundaries, offset, offset + element.quarter_length())?;
517            seconds.push(Seconds {
518                offset: start,
519                duration,
520                end: start + duration,
521            });
522        }
523        Ok(seconds)
524    }
525
526    /// How many parts sound in each chord of the piece chordified, in order:
527    /// music21's `chordify(addPartIdAsGroup=True)`, read as the number of
528    /// part ids on each chord's pitches. A piece that is not a score is not
529    /// chordified, and its own chords carry no part ids, so each counts
530    /// nought.
531    ///
532    /// # Errors
533    ///
534    /// A score that cannot be chordified.
535    pub fn parts_sounding(&self) -> Result<&[usize]> {
536        Ok(&self.chordified()?.parts_sounding)
537    }
538
539    /// The chords of the piece chordified with every pitch kept: music21's
540    /// `chordify.flat.getElementsByClass(Chord)`. A piece that is not a
541    /// score is not chordified, so these are its own chords and chord
542    /// symbols, a symbol that sounds nothing an empty chord.
543    ///
544    /// # Errors
545    ///
546    /// A score that cannot be chordified.
547    pub fn chordified_chords(&self) -> Result<&[Chord]> {
548        Ok(&self.chordified()?.chords)
549    }
550
551    fn chordified(&self) -> Result<&Chordified> {
552        self.chordified
553            .get_or_init(|| {
554                if self.prepared.kind() == StreamKind::Score {
555                    let (chordified, parts_sounding) = self.prepared.chordify_parts_sounding()?;
556                    Ok(Chordified {
557                        chords: chords_of(&chordified)?,
558                        parts_sounding,
559                    })
560                } else {
561                    let chords = chords_of(&self.prepared)?;
562                    Ok(Chordified {
563                        parts_sounding: vec![0; chords.len()],
564                        chords,
565                    })
566                }
567            })
568            .as_ref()
569            .map_err(Clone::clone)
570    }
571
572    /// How often each length in quarters comes among the piece's notes, in
573    /// the order each is first met: music21's
574    /// `flat.notes.quarterLengthHistogram`, a `Counter`.
575    pub fn quarter_length_histogram(&self) -> &[(FloatType, usize)] {
576        self.quarter_length_histogram.get_or_init(|| {
577            let mut counts: Vec<(FloatType, usize)> = Vec::new();
578            for event in self.prepared.flatten().events() {
579                let element = event.element();
580                if !matches!(
581                    element,
582                    StreamElement::Note(_)
583                        | StreamElement::Chord(_)
584                        | StreamElement::Unpitched(_)
585                        | StreamElement::PercussionChord(_)
586                        | StreamElement::ChordSymbol(_)
587                ) {
588                    continue;
589                }
590                let length = element.quarter_length();
591                match counts.iter_mut().find(|(known, _)| *known == length) {
592                    Some((_, count)) => *count += 1,
593                    None => counts.push((length, 1)),
594                }
595            }
596            counts
597        })
598    }
599
600    /// The keys the piece is likely in, best first, by Aarden and Essen's
601    /// weights: music21's `flat.analyzedKey`, the key and its
602    /// `alternateInterpretations`. Nothing for a piece with no pitched
603    /// notes.
604    pub fn analyzed_key(&self) -> Option<&[KeyEstimate]> {
605        self.analyzed_key
606            .get_or_init(|| {
607                analysis::estimate_key_of_stream(KeyProfile::AardenEssen, &self.prepared)
608            })
609            .as_deref()
610    }
611
612    /// The meters the piece states, in order.
613    fn time_signatures(&self) -> Vec<TimeSignature> {
614        self.prepared
615            .flatten()
616            .events()
617            .iter()
618            .filter_map(|event| match event.element() {
619                StreamElement::TimeSignature(meter) => Some(meter.clone()),
620                _ => None,
621            })
622            .collect()
623    }
624
625    /// The modes of the keys the piece states, in order.
626    fn key_modes(&self) -> Vec<String> {
627        self.prepared
628            .flatten()
629            .events()
630            .iter()
631            .filter_map(|event| match event.element() {
632                StreamElement::Key(key) => Some(key.mode().to_string()),
633                _ => None,
634            })
635            .collect()
636    }
637
638    fn melodic_parts(&self, by_part: bool) -> Vec<&Stream> {
639        if by_part {
640            self.prepared.parts()
641        } else {
642            vec![&self.prepared]
643        }
644    }
645}
646
647/// The chords and chord symbols of a stream, nested ones included.
648fn chords_of(stream: &Stream) -> Result<Vec<Chord>> {
649    stream
650        .flatten()
651        .events()
652        .iter()
653        .filter_map(|event| match event.element() {
654            StreamElement::Chord(chord) => Some(Ok(chord.clone())),
655            StreamElement::ChordSymbol(symbol) if symbol.is_no_chord() => {
656                Some(Chord::new(&[] as &[Pitch]))
657            }
658            StreamElement::ChordSymbol(symbol) => Some(symbol.to_chord()),
659            _ => None,
660        })
661        .collect()
662}
663
664/// The MIDI numbers of the notes [`Stream::find_consecutive_notes`] finds,
665/// a chord's highest on the staff where chords are kept.
666fn consecutive_midis(
667    part: &Stream,
668    options: &ConsecutiveOptions,
669    chords: bool,
670) -> Vec<IntegerType> {
671    let leaves = part.leaves();
672    let found = part.find_consecutive_notes(options).unwrap_or_default();
673    found
674        .into_iter()
675        .flatten()
676        .filter_map(|position| match leaves[position].1 {
677            StreamElement::Note(note) => Some(note.pitch().midi()),
678            StreamElement::Chord(chord) if chords => chord
679                .sort_diatonic_ascending()
680                .pitches()
681                .last()
682                .map(Pitch::midi),
683            StreamElement::ChordSymbol(symbol) if chords => {
684                let mut pitches = symbol.pitches().ok()?;
685                pitches.sort_by(|left, right| {
686                    (left.diatonic_note_number(), left.ps())
687                        .partial_cmp(&(right.diatonic_note_number(), right.ps()))
688                        .unwrap_or(std::cmp::Ordering::Equal)
689                });
690                pitches.last().map(Pitch::midi)
691            }
692            _ => None,
693        })
694        .collect()
695}
696
697/// How long the stretch from one offset to another lasts in seconds, at the
698/// tempo holding each part of it: music21's `_accumulatedSeconds`.
699fn accumulated_seconds(
700    boundaries: &[(FloatType, FloatType, MetronomeMark)],
701    start: FloatType,
702    end: FloatType,
703) -> Result<FloatType> {
704    let mut total = 0.0;
705    let mut active_start = start;
706    for (from, to, mark) in boundaries {
707        if !(*from <= active_start && active_start < *to) {
708            continue;
709        }
710        let active_end = if end < *to { end } else { *to };
711        total += mark.seconds_per_quarter()? * (active_end - active_start);
712        if active_end == end {
713            break;
714        }
715        active_start = active_end;
716    }
717    Ok(total)
718}