use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyntheticSubject {
pub name_pattern: String,
pub rules: HashMap<String, Aggregate>,
pub default_rule: Option<Aggregate>,
pub group_by_quality: Option<String>,
pub group_by_component: Option<String>,
pub component_filters: HashMap<String, String>,
}
impl SyntheticSubject {
pub fn new(name: impl Into<String>) -> Self {
Self {
name_pattern: name.into(),
rules: HashMap::new(),
default_rule: None,
group_by_quality: None,
group_by_component: None,
component_filters: HashMap::new(),
}
}
pub fn mean_all(name: impl Into<String>) -> Self {
Self {
name_pattern: name.into(),
rules: HashMap::new(),
default_rule: Some(Aggregate::Mean),
group_by_quality: None,
group_by_component: None,
component_filters: HashMap::new(),
}
}
pub fn sum_all(name: impl Into<String>) -> Self {
Self {
name_pattern: name.into(),
rules: HashMap::new(),
default_rule: Some(Aggregate::Sum),
group_by_quality: None,
group_by_component: None,
component_filters: HashMap::new(),
}
}
pub fn min_all(name: impl Into<String>) -> Self {
Self {
name_pattern: name.into(),
rules: HashMap::new(),
default_rule: Some(Aggregate::Min),
group_by_quality: None,
group_by_component: None,
component_filters: HashMap::new(),
}
}
pub fn max_all(name: impl Into<String>) -> Self {
Self {
name_pattern: name.into(),
rules: HashMap::new(),
default_rule: Some(Aggregate::Max),
group_by_quality: None,
group_by_component: None,
component_filters: HashMap::new(),
}
}
pub fn auto(name: impl Into<String>) -> Self {
Self {
name_pattern: name.into(),
rules: HashMap::new(),
default_rule: Some(Aggregate::Auto),
group_by_quality: None,
group_by_component: None,
component_filters: HashMap::new(),
}
}
pub fn rule(mut self, measurement: impl Into<String>, aggregate: Aggregate) -> Self {
self.rules.insert(measurement.into(), aggregate);
self
}
pub fn group_by(mut self, quality: impl Into<String>) -> Self {
self.group_by_quality = Some(quality.into());
self
}
pub fn group_by_component(mut self, component: impl Into<String>) -> Self {
self.group_by_component = Some(component.into());
self
}
pub fn where_component(
mut self,
component: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.component_filters
.insert(component.into(), value.into());
self
}
pub fn get_aggregate(&self, measurement: &str) -> Option<&Aggregate> {
self.rules.get(measurement).or(self.default_rule.as_ref())
}
pub fn is_grouped(&self) -> bool {
self.group_by_quality.is_some() || self.group_by_component.is_some()
}
pub fn has_component_filters(&self) -> bool {
!self.component_filters.is_empty()
}
pub fn expand_name(
&self,
quality_value: Option<&str>,
component_value: Option<&str>,
) -> String {
let mut name = self.name_pattern.clone();
if let Some(qv) = quality_value {
name = name.replace("{quality}", qv);
if let Some(ref q) = self.group_by_quality {
name = name.replace(&format!("{{{}}}", q), qv);
}
}
if let Some(cv) = component_value {
name = name.replace("{component}", cv);
if let Some(ref c) = self.group_by_component {
name = name.replace(&format!("{{{}}}", c), cv);
}
}
name
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Aggregate {
Mean,
Sum,
Min,
Max,
Any,
All,
Count,
First,
Last,
MostRecent,
LeastRecent,
LinearTrend,
Auto,
}
impl Aggregate {
pub fn as_str(&self) -> &'static str {
match self {
Aggregate::Mean => "mean",
Aggregate::Sum => "sum",
Aggregate::Min => "min",
Aggregate::Max => "max",
Aggregate::Any => "max", Aggregate::All => "min", Aggregate::Count => "count",
Aggregate::First => "first",
Aggregate::Last => "last",
Aggregate::MostRecent => "most_recent",
Aggregate::LeastRecent => "least_recent",
Aggregate::LinearTrend => "linear_trend",
Aggregate::Auto => "auto",
}
}
pub fn resolve_auto(kind: crate::unit::MeasurementKind) -> Self {
use crate::unit::MeasurementKind;
match kind {
MeasurementKind::Measure => Aggregate::Mean,
MeasurementKind::Categorical => Aggregate::Last,
MeasurementKind::Count => Aggregate::Sum,
MeasurementKind::Average => Aggregate::Mean,
MeasurementKind::Binary => Aggregate::Any,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_synthetic_subject_mean_all() {
let s = SyntheticSubject::mean_all("Fleet Average");
assert_eq!(s.name_pattern, "Fleet Average");
assert_eq!(s.default_rule, Some(Aggregate::Mean));
assert!(s.rules.is_empty());
}
#[test]
fn test_synthetic_subject_with_rules() {
let s = SyntheticSubject::new("Custom")
.rule("fuel", Aggregate::Mean)
.rule("units", Aggregate::Sum)
.rule("any_engine", Aggregate::Any);
assert_eq!(s.rules.len(), 3);
assert_eq!(s.get_aggregate("fuel"), Some(&Aggregate::Mean));
assert_eq!(s.get_aggregate("units"), Some(&Aggregate::Sum));
assert_eq!(s.get_aggregate("unknown"), None);
}
#[test]
fn test_synthetic_subject_grouped() {
let s = SyntheticSubject::mean_all("{zone} Average").group_by("zone");
assert!(s.is_grouped());
assert_eq!(s.group_by_quality, Some("zone".to_string()));
let name = s.expand_name(Some("North"), None);
assert_eq!(name, "North Average");
}
#[test]
fn test_synthetic_subject_component_filter() {
let s = SyntheticSubject::sum_all("Blue Large Total")
.where_component("color", "blue")
.where_component("size", "L");
assert!(s.has_component_filters());
assert_eq!(s.component_filters.len(), 2);
}
#[test]
fn test_synthetic_subject_grouped_by_component() {
let s = SyntheticSubject::sum_all("{color} Total").group_by_component("color");
assert!(s.is_grouped());
let name = s.expand_name(None, Some("blue"));
assert_eq!(name, "blue Total");
}
#[test]
fn test_aggregate_auto_resolve() {
use crate::unit::MeasurementKind;
assert_eq!(
Aggregate::resolve_auto(MeasurementKind::Measure),
Aggregate::Mean
);
assert_eq!(
Aggregate::resolve_auto(MeasurementKind::Categorical),
Aggregate::Last
);
assert_eq!(
Aggregate::resolve_auto(MeasurementKind::Count),
Aggregate::Sum
);
}
}