libbarto/realtime/
cv.rs

1// Copyright (c) 2025 barto developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use std::{collections::HashSet, hash::Hash, str::FromStr};
10
11use anyhow::{Error, Result};
12use num_traits::{Bounded, FromPrimitive, NumOps, ToPrimitive, Zero};
13use regex::Regex;
14
15use crate::utils::until_err;
16
17/// A value constrained by specific rules (such as the day of the month)
18#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub enum ConstrainedValue<T>
20where
21    T: Constrainable,
22{
23    /// Every value
24    All,
25    /// A range of values
26    Range(T, T),
27    /// A repetition of values
28    ///
29    /// This is a sequence of values: start, start + rep, start + 2*rep
30    /// up to the optional end value.  If no end value is given, it continues
31    /// to the maximum `Bounded` value for the type.
32    Repetition {
33        /// The value to start
34        start: T,
35        /// An optional end value
36        end: Option<T>,
37        /// The repetition value
38        rep: u8,
39    },
40    /// Specific values
41    Specific(Vec<T>),
42}
43
44impl<T> ConstrainedValueMatcher<T> for ConstrainedValue<T>
45where
46    T: Constrainable + NumOps + Zero + Copy + FromPrimitive,
47{
48    fn matches(&self, value: T) -> bool {
49        match self {
50            Self::All => true,
51            Self::Range(first, second) => value >= *first && value <= *second,
52            Self::Repetition { start, end, rep } => {
53                if value < *start {
54                    return false;
55                }
56                if let Some(end_value) = end
57                    && value > *end_value
58                {
59                    return false;
60                }
61                let diff = value - *start;
62                diff % T::from_u8(*rep).unwrap() == T::zero()
63            }
64            Self::Specific(values) => values.contains(&value),
65        }
66    }
67}
68
69/// A trait for types that can be constrained
70pub trait Constrainable:
71    Bounded + Copy + Eq + FromStr + Hash + Ord + PartialEq + PartialOrd + ToPrimitive
72{
73}
74
75impl<T> Constrainable for T where
76    T: Bounded + Copy + Eq + FromStr + Hash + Ord + PartialEq + PartialOrd + ToPrimitive
77{
78}
79
80/// A trait for parsing constrained values
81pub trait ConstrainedValueParser<'a, T>:
82    FromStr<Err = Error> + TryFrom<&'a str, Error = Error>
83where
84    T: Constrainable,
85{
86    /// The error to return for an invalid parse
87    fn invalid(s: &str) -> Error;
88
89    /// The regex to match repetitions
90    fn repetition_regex() -> Regex;
91
92    /// The regex to match ranges
93    fn range_regex() -> Regex;
94
95    /// Whether to allow 'R' for random value
96    #[must_use]
97    fn allow_rand() -> bool {
98        false
99    }
100
101    /// The 'all' constrained value
102    fn all() -> Self;
103
104    /// The 'rand' constrained value
105    fn rand() -> Self;
106
107    /// The 'repetition' constrained value
108    fn rep(start: T, end: Option<T>, rep: u8) -> Self;
109
110    /// The 'range' constrained value
111    fn range(first: T, second: T) -> Self;
112
113    /// The 'specific' constrained value
114    fn specific(values: Vec<T>) -> Self;
115
116    /// Parse a constrained value from a string
117    ///
118    /// # Errors
119    ///
120    fn parse(s: &str) -> Result<Self> {
121        if s.is_empty() {
122            Err(Self::invalid(s))
123        } else if s == "*" {
124            Ok(Self::all())
125        } else if s == "R" && Self::allow_rand() {
126            Ok(Self::rand())
127        } else if Self::repetition_regex().is_match(s) {
128            Self::parse_repetition(s)
129        } else if Self::range_regex().is_match(s) {
130            Self::parse_range(s)
131        } else {
132            Self::parse_specific(s)
133        }
134    }
135
136    /// Parse a range constrained value from a string
137    ///
138    /// # Errors
139    ///
140    fn parse_range(s: &str) -> Result<Self> {
141        if let Some(caps) = Self::range_regex().captures(s) {
142            let first = caps[1].parse::<T>().map_err(|_| Self::invalid(s))?;
143            let second = caps[2].parse::<T>().map_err(|_| Self::invalid(s))?;
144            if (first < T::min_value() || first > T::max_value())
145                || (second < T::min_value() || second > T::max_value())
146                || (first > second)
147            {
148                Err(Self::invalid(s))
149            } else {
150                Ok(Self::range(first, second))
151            }
152        } else {
153            Err(Self::invalid(s))
154        }
155    }
156
157    /// Parse a repetition constrained value from a string
158    ///
159    /// # Errors
160    ///
161    fn parse_repetition(s: &str) -> Result<Self> {
162        if let Some(caps) = Self::repetition_regex().captures(s) {
163            let start = caps[1].parse::<T>().map_err(|_| Self::invalid(s))?;
164            let end = if let Some(end_match) = caps.get(3) {
165                Some(
166                    end_match
167                        .as_str()
168                        .parse::<T>()
169                        .map_err(|_| Self::invalid(s))?,
170                )
171            } else {
172                None
173            };
174            let rep = caps[4].parse::<u8>().map_err(|_| Self::invalid(s))?;
175            if rep == 0 || start < T::min_value() || start > T::max_value() {
176                Err(Self::invalid(s))
177            } else if let Some(end_val) = end {
178                if end_val < start || end_val < T::min_value() || end_val > T::max_value() {
179                    Err(Self::invalid(s))
180                } else {
181                    Ok(Self::rep(start, Some(end_val), rep))
182                }
183            } else {
184                Ok(Self::rep(start, None, rep))
185            }
186        } else {
187            Err(Self::invalid(s))
188        }
189    }
190
191    /// Parse a specific constrained value from a string
192    ///
193    /// # Errors
194    ///
195    fn parse_specific(s: &str) -> Result<Self> {
196        let mut err = Ok(());
197        let mut values: Vec<T> = s
198            .split(',')
199            .map(|part| part.parse::<T>().map_err(|_| Self::invalid(s)))
200            .scan(&mut err, until_err)
201            .collect::<HashSet<_>>()
202            .into_iter()
203            .collect();
204        err?;
205        values.sort_unstable();
206        Ok(Self::specific(values))
207    }
208}
209
210/// A trait for matching constrained values
211pub trait ConstrainedValueMatcher<T>
212where
213    T: Constrainable,
214{
215    /// Check if the constrained value matches the given value
216    fn matches(&self, value: T) -> bool;
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    type Test = ConstrainedValue<u8>;
224
225    impl TryFrom<&str> for Test {
226        type Error = Error;
227
228        #[cfg_attr(coverage_nightly, coverage(off))]
229        fn try_from(s: &str) -> Result<Self> {
230            Test::parse(s)
231        }
232    }
233
234    impl FromStr for Test {
235        type Err = Error;
236
237        #[cfg_attr(coverage_nightly, coverage(off))]
238        fn from_str(s: &str) -> Result<Self> {
239            Test::try_from(s)
240        }
241    }
242
243    #[cfg_attr(coverage_nightly, coverage(off))]
244    impl ConstrainedValueParser<'_, u8> for Test {
245        fn invalid(s: &str) -> Error {
246            Error::msg(format!("invalid constrained value: {s}"))
247        }
248
249        fn all() -> Self {
250            Test::All
251        }
252
253        fn rand() -> Self {
254            Test::All
255        }
256
257        fn repetition_regex() -> Regex {
258            Regex::new(r"^(\d{1,3})(-(\d{1,3}))?/(\d{1,3})$").unwrap()
259        }
260
261        fn range_regex() -> Regex {
262            Regex::new(r"^(\d{1,3})-(\d{1,3})$").unwrap()
263        }
264
265        fn rep(start: u8, end: Option<u8>, rep: u8) -> Self {
266            Test::Repetition { start, end, rep }
267        }
268
269        fn range(first: u8, second: u8) -> Self {
270            Test::Range(first, second)
271        }
272
273        fn specific(values: Vec<u8>) -> Self {
274            Test::Specific(values)
275        }
276    }
277
278    #[test]
279    fn parse_range_errors() {
280        assert!(Test::parse_range("").is_err());
281    }
282
283    #[test]
284    fn parse_repetition_errors() {
285        assert!(Test::parse_repetition("").is_err());
286    }
287}