use std::fmt::Debug;
#[derive(Debug, Clone)]
enum Phase<T> {
MonotonicallyRising,
MonotonicallyFalling,
AtValue { value: T },
}
pub struct TimeSeriesPattern<T> {
phases: Vec<Phase<T>>,
}
impl<T> TimeSeriesPattern<T>
where
T: Copy + Ord + Debug,
{
pub fn new() -> Self {
Self { phases: Vec::new() }
}
pub fn monotonically_rising(mut self) -> Self {
self.phases.push(Phase::MonotonicallyRising);
self
}
pub fn monotonically_falling(mut self) -> Self {
self.phases.push(Phase::MonotonicallyFalling);
self
}
pub fn at_value(mut self, value: T) -> Self {
self.phases.push(Phase::AtValue { value });
self
}
pub fn matches(&self, samples: &[T]) -> Result<(), String> {
let mut view_start = 0;
let mut view_end = 0;
assert!(self.phases.len() > 0);
for (phase_idx, phase) in self.phases.iter().enumerate() {
let last_phase = match phase_idx {
0 => None,
x => self.phases.get(x - 1),
};
macro_rules! fail {
($($arg:tt)*) => {
{
let msg = format!($($arg)*);
return Err(format!("@{phase:?}{phase_idx:?} {msg}"));
}
};
}
use Phase::*;
match (phase, last_phase) {
(AtValue { value }, None) => {
view_start = 0;
view_end = view_start
+ samples[view_start..]
.iter()
.take_while(|x| *x == value)
.count()
- 1;
if view_end - view_start == 0 {
fail!("samples do not start with {:?}", value)
}
}
(MonotonicallyRising | MonotonicallyFalling, None)
| (MonotonicallyFalling, Some(AtValue { value: _ } | MonotonicallyRising))
| (MonotonicallyRising, Some(AtValue { value: _ } | MonotonicallyFalling)) => {
let is_rise = matches!(phase, MonotonicallyRising);
view_start = view_start + 1;
view_end = view_start
+ samples[view_start..]
.iter()
.enumerate()
.take_while(|(i, x)| match (i, is_rise) {
(0, _) => true,
(_, true) => *x >= &samples[view_start..][*i - 1],
(_, false) => *x <= &samples[view_start..][*i - 1],
})
.count()
- 1;
if view_end - view_start == 0 {
fail!("couldn't find a {phase:?} pattern")
}
}
(MonotonicallyRising, Some(MonotonicallyRising)) => {
fail!("Rising after Rising pattern doesn't make sense")
}
(MonotonicallyFalling, Some(MonotonicallyFalling)) => {
fail!("Falling after Falling pattern doesn't make sense")
}
(AtValue { value }, Some(_)) => {
view_start = view_start + 1;
view_start += match samples[view_start..=view_end]
.iter()
.position(|x| *x == *value)
{
Some(offset) => offset,
None => fail!("can't find value {value:?}"),
};
view_end = view_start
+ samples[view_start..]
.iter()
.take_while(|x| *x == value)
.count()
- 1;
}
}
}
if view_end < samples.len() - 1 {
return Err(format!(
"{} remaining for pattern match",
samples.len() - view_end - 1
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_envelope_pattern() {
let samples1 = vec![
0, 0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 7, 0,
0, 0, 0,
];
let samples2 = vec![
0, 0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 7, 0,
0, 0, 0,
];
let samples3 = vec![
0, 0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 7, 7,
7, 0, 0, 0, 0,
];
let samples4 = vec![
0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 7, 7,
3, 0, 0, 0, 0,
];
let samples5 = vec![
0, 0, 0, 9, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 11, 7, 7, 3, 0, 0, 0, 0,
];
let samples10 = vec![0, 0, 12, 12, 11, 12, 7, 8, 0];
let samples11 = vec![0, 0, 12, 12, 11, 11, 12, 7, 8, 0];
let pattern = TimeSeriesPattern::new()
.at_value(0) .monotonically_rising() .at_value(12) .monotonically_falling() .at_value(0);
pattern.matches(&samples1).expect("Pattern should match");
pattern.matches(&samples2).expect("Pattern should match");
pattern.matches(&samples3).expect("Pattern should match");
pattern.matches(&samples4).expect("Pattern should match");
pattern.matches(&samples5).expect("Pattern should match");
assert!(!pattern.matches(&samples10).is_ok());
assert!(!pattern.matches(&samples11).is_ok());
}
}