Skip to main content

sim_lib_music_serial/
time_point.rs

1//! Exact time-point systems with onset-class order kept separate from duration series.
2
3use std::num::NonZeroU16;
4
5use sim_lib_music_core::Time;
6use sim_lib_serial_core::{
7    AggregateRule, AlphabetError, AlphabetId, FiniteAlphabet, SerialAlphabet, Series, SeriesError,
8};
9use thiserror::Error;
10
11use crate::rotate_sequence_left;
12
13/// Finite onset-class alphabet for one exact time-point system.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct TimePointAlphabet {
16    modulus: NonZeroU16,
17    inner: FiniteAlphabet<u16>,
18}
19
20impl TimePointAlphabet {
21    /// Constructs the canonical onset-class alphabet `0..modulus`.
22    pub fn try_new(modulus: NonZeroU16) -> Result<Self, TimePointError> {
23        let symbols = (0..modulus.get()).collect::<Vec<_>>();
24        let inner = FiniteAlphabet::try_new(
25            AlphabetId::try_new(format!("time-point/{}-v1", modulus.get()))?,
26            symbols,
27        )?;
28        Ok(Self { modulus, inner })
29    }
30
31    /// Returns the system modulus retained by this alphabet.
32    pub const fn modulus(&self) -> NonZeroU16 {
33        self.modulus
34    }
35
36    /// Returns the canonical onset classes in system order.
37    pub fn classes(&self) -> &[u16] {
38        self.inner.symbols()
39    }
40}
41
42impl SerialAlphabet for TimePointAlphabet {
43    type Symbol = u16;
44
45    fn id(&self) -> &AlphabetId {
46        self.inner.id()
47    }
48
49    fn symbols(&self) -> &[Self::Symbol] {
50        self.inner.symbols()
51    }
52}
53
54/// Exact time-point system with one finite modulus and one unit duration.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct TimePointSystem {
57    /// Finite onset-class modulus.
58    pub modulus: NonZeroU16,
59    /// Exact unit mapped to one onset-class step.
60    pub unit: Time,
61}
62
63impl TimePointSystem {
64    /// Returns the canonical onset-class alphabet for this system.
65    pub fn alphabet(&self) -> Result<TimePointAlphabet, TimePointError> {
66        TimePointAlphabet::try_new(self.modulus)
67    }
68
69    /// Converts one onset class to an exact onset measured in this system's unit.
70    pub fn onset_for(&self, point: u16) -> Option<Time> {
71        (point < self.modulus.get()).then(|| self.unit * i64::from(point))
72    }
73}
74
75/// One ordered onset-class series over a [`TimePointSystem`].
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct TimePointRow {
78    /// Validated onset-class order over the system alphabet.
79    pub points: Series<TimePointAlphabet>,
80}
81
82impl TimePointRow {
83    /// Constructs an exhaustive onset-class row for `system`.
84    pub fn try_new(system: &TimePointSystem, points: Vec<u16>) -> Result<Self, TimePointError> {
85        Ok(Self {
86            points: Series::try_new(
87                system.alphabet()?,
88                AggregateRule::exhaustive_exactly_once(),
89                points,
90            )?,
91        })
92    }
93
94    /// Returns the system modulus inferred from the retained alphabet.
95    pub fn modulus(&self) -> NonZeroU16 {
96        self.points.alphabet().modulus()
97    }
98
99    /// Returns the retained onset classes in serial order.
100    pub fn order(&self) -> &[u16] {
101        self.points.order()
102    }
103
104    /// Returns exact onset positions under `system`, without implying any durations.
105    pub fn onsets(&self, system: &TimePointSystem) -> Result<Vec<Time>, TimePointError> {
106        if system.modulus != self.modulus() {
107            return Err(TimePointError::SystemMismatch {
108                expected: self.modulus(),
109                found: system.modulus,
110            });
111        }
112        self.order()
113            .iter()
114            .map(|&point| {
115                system
116                    .onset_for(point)
117                    .ok_or(TimePointError::PointOutsideSystem {
118                        point,
119                        modulus: system.modulus,
120                    })
121            })
122            .collect()
123    }
124
125    /// Returns a left-rotated onset-class row reduced modulo the system modulus.
126    pub fn rotate(&self, steps: usize) -> Result<Self, TimePointError> {
127        Ok(Self {
128            points: Series::try_new(
129                self.points.alphabet().clone(),
130                self.points.rule().clone(),
131                rotate_sequence_left(self.points.order(), steps),
132            )?,
133        })
134    }
135}
136
137/// Failure while constructing or realizing exact time-point data.
138#[derive(Clone, Debug, PartialEq, Eq, Error)]
139pub enum TimePointError {
140    /// The canonical onset-class alphabet was invalid.
141    #[error(transparent)]
142    Alphabet(#[from] AlphabetError),
143    /// The caller-supplied onset-class order violated the system aggregate contract.
144    #[error(transparent)]
145    Series(#[from] SeriesError),
146    /// The caller tried to realize a row under another modulus.
147    #[error("time-point row expects modulus {expected}, got {found}")]
148    SystemMismatch {
149        /// Row modulus retained by the row alphabet.
150        expected: NonZeroU16,
151        /// Realization modulus supplied by the caller.
152        found: NonZeroU16,
153    },
154    /// One onset class lay outside the selected finite system.
155    #[error("time-point class {point} lies outside modulus {modulus}")]
156    PointOutsideSystem {
157        /// Rejected onset class.
158        point: u16,
159        /// Finite system modulus.
160        modulus: NonZeroU16,
161    },
162}