improv_duckdb_model/
calc.rs1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14use improv_core_model::{parser, Expr, Measure, MeasureId, MeasureKind, Model, Name, ValueType};
15use improv_engine::compiler::{infer, CompileContext};
16use improv_engine::{scalar, CellValue};
17
18#[derive(Debug)]
20pub struct Compiled {
21 names: Vec<String>,
23 expr: Expr,
24}
25
26type Input = Option<CellValue>;
28
29pub type Inputs = Vec<(String, Input)>;
31
32pub fn parse_inputs(json: Option<&str>) -> Result<Inputs, String> {
35 use serde_json::Value as J;
36 let Some(text) = json.map(str::trim).filter(|t| !t.is_empty()) else {
37 return Ok(Vec::new());
38 };
39 let J::Object(map) = serde_json::from_str::<J>(text)
40 .map_err(|e| format!("improv_calc: inputs are not valid JSON: {e}"))?
41 else {
42 return Err("improv_calc: inputs must be a JSON object like {\"rate\": 0.05}".into());
43 };
44 map.into_iter()
45 .map(|(k, v)| {
46 let v = match v {
47 J::Null => None,
48 J::Bool(b) => Some(CellValue::Bool(b)),
49 J::Number(n) => Some(CellValue::num(n.as_f64().unwrap_or(f64::NAN))),
50 J::String(s) => Some(CellValue::Text(s)),
51 J::Array(_) | J::Object(_) => {
52 return Err(format!(
53 "improv_calc: input {k:?} is a JSON array or object; inputs are scalars"
54 ))
55 }
56 };
57 Ok((k, v))
58 })
59 .collect()
60}
61
62fn input_type(v: &Input) -> ValueType {
65 match v {
66 Some(CellValue::Bool(_)) => ValueType::Boolean,
67 Some(CellValue::Text(_)) => ValueType::Text,
68 Some(CellValue::Date(_)) => ValueType::DateTime,
69 _ => ValueType::Number,
70 }
71}
72
73pub fn compile(formula: &str, inputs: &Inputs) -> Result<Compiled, String> {
75 let mut model = Model::new();
76 for (i, (name, v)) in inputs.iter().enumerate() {
77 model.add_measure(Measure {
78 id: MeasureId(i as u32 + 1),
79 name: Name(name.clone()),
80 value_type: input_type(v),
81 categories: vec![],
82 kind: MeasureKind::Input,
83 description: None,
84 });
85 }
86 let text = formula.trim();
87 let text = text.strip_prefix('=').unwrap_or(text);
88 let expr = parser::parse_expr(&model, text)
89 .map_err(|e| format!("improv_calc: {e}"))?
90 .expr;
91 scalar::check(&expr).map_err(|e| format!("improv_calc: {e}"))?;
92 infer(&CompileContext::new(&model.measures), MeasureId(0), &expr)
93 .map_err(|e| format!("improv_calc: {e}"))?;
94 Ok(Compiled {
95 names: inputs.iter().map(|(n, _)| n.clone()).collect(),
96 expr,
97 })
98}
99
100impl Compiled {
101 pub fn eval(&self, inputs: &Inputs) -> Result<Option<CellValue>, String> {
104 debug_assert_eq!(inputs.len(), self.names.len());
105 let env: HashMap<MeasureId, CellValue> = (1..)
106 .zip(inputs)
107 .filter_map(|(i, (_, v))| v.clone().map(|v| (MeasureId(i), v)))
108 .collect();
109 scalar::eval(&self.expr, &env).map_err(|e| format!("improv_calc: {e}"))
110 }
111}
112
113type Key = (String, Vec<(String, &'static str)>);
115
116#[derive(Debug)]
118pub struct Cache {
119 max: usize,
120 map: Mutex<HashMap<Key, Arc<Compiled>>>,
121}
122
123impl Default for Cache {
124 fn default() -> Self {
125 Cache::new(1024)
126 }
127}
128
129impl Cache {
130 pub fn new(max: usize) -> Self {
132 Cache {
133 max,
134 map: Mutex::new(HashMap::new()),
135 }
136 }
137
138 pub fn len(&self) -> usize {
140 self.lock().len()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.len() == 0
146 }
147
148 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<Key, Arc<Compiled>>> {
149 self.map.lock().unwrap_or_else(|e| e.into_inner())
150 }
151
152 pub fn get(&self, formula: &str, inputs: &Inputs) -> Result<Arc<Compiled>, String> {
155 let key: Key = (
156 formula.to_string(),
157 inputs
158 .iter()
159 .map(|(n, v)| (n.clone(), improv_pg_model::value_type_name(input_type(v))))
160 .collect(),
161 );
162 if let Some(c) = self.lock().get(&key) {
163 return Ok(c.clone());
164 }
165 let c = Arc::new(compile(formula, inputs)?);
167 let mut map = self.lock();
168 if map.len() >= self.max {
171 map.clear();
172 }
173 map.insert(key, c.clone());
174 Ok(c)
175 }
176
177 pub fn calc(&self, formula: &str, inputs: Option<&str>) -> Result<Option<CellValue>, String> {
180 let inputs = parse_inputs(inputs)?;
181 self.get(formula, &inputs)?.eval(&inputs)
182 }
183}
184
185pub fn number(v: &CellValue) -> Option<f64> {
188 const MS_PER_DAY: f64 = 86_400_000.0;
189 const EPOCH_SERIAL: f64 = 25_569.0; match v {
191 CellValue::Num(_) => v.as_num(),
192 CellValue::Bool(b) => Some(f64::from(u8::from(*b))),
193 CellValue::Date(ms) => Some(*ms as f64 / MS_PER_DAY + EPOCH_SERIAL),
194 CellValue::Text(_) | CellValue::Err(_) => None,
195 }
196}