1use anyhow::{Context, Result, anyhow};
2use evalexpr::{
3 ContextWithMutableVariables, HashMapContext, Value as EvalValue, eval_with_context,
4};
5
6use crate::data::{Value, normalize_column_name, value_to_evalexpr};
7
8#[derive(Debug, Clone)]
9pub struct DerivedColumn {
10 pub name: String,
11 pub expression: String,
12}
13
14impl DerivedColumn {
15 pub fn parse(spec: &str) -> Result<Self> {
16 let mut parts = spec.splitn(2, '=');
17 let name = parts
18 .next()
19 .map(|s| s.trim())
20 .filter(|s| !s.is_empty())
21 .ok_or_else(|| anyhow!("Derived column is missing a name"))?;
22 let expression = parts
23 .next()
24 .map(|s| s.trim())
25 .filter(|s| !s.is_empty())
26 .ok_or_else(|| anyhow!("Derived column '{name}' is missing an expression"))?;
27 Ok(DerivedColumn {
28 name: name.to_string(),
29 expression: expression.to_string(),
30 })
31 }
32
33 pub fn evaluate(
34 &self,
35 headers: &[String],
36 raw_row: &[String],
37 typed_row: &[Option<Value>],
38 row_number: Option<usize>,
39 ) -> Result<String> {
40 let mut context = HashMapContext::new();
41 for (idx, header) in headers.iter().enumerate() {
42 let canon = normalize_column_name(header);
43 let key_by_index = format!("c{idx}");
44 if let Some(value) = typed_row.get(idx).and_then(|v| v.as_ref()) {
45 context
46 .set_value(canon.clone(), value_to_evalexpr(value))
47 .with_context(|| format!("Binding column '{header}'"))?;
48 context
49 .set_value(key_by_index.clone(), value_to_evalexpr(value))
50 .with_context(|| format!("Binding column index {idx}"))?;
51 } else if let Some(raw) = raw_row.get(idx) {
52 context
53 .set_value(canon.clone(), EvalValue::String(raw.clone()))
54 .with_context(|| format!("Binding raw column '{header}'"))?;
55 context
56 .set_value(key_by_index.clone(), EvalValue::String(raw.clone()))
57 .with_context(|| format!("Binding raw column index {idx}"))?;
58 }
59 }
60 if let Some(row_number) = row_number {
61 context
62 .set_value("row_number".to_string(), EvalValue::Int(row_number as i64))
63 .context("Binding row_number")?;
64 }
65
66 let result = eval_with_context(&self.expression, &context)
67 .with_context(|| format!("Evaluating expression for column '{}'", self.name))?;
68 Ok(match result {
69 EvalValue::String(s) => s,
70 EvalValue::Int(i) => i.to_string(),
71 EvalValue::Float(f) => f.to_string(),
72 EvalValue::Boolean(b) => b.to_string(),
73 EvalValue::Tuple(values) => values
74 .into_iter()
75 .map(|v| v.to_string())
76 .collect::<Vec<_>>()
77 .join("|"),
78 EvalValue::Empty => String::new(),
79 })
80 }
81}
82
83pub fn parse_derived_columns(specs: &[String]) -> Result<Vec<DerivedColumn>> {
84 specs
85 .iter()
86 .map(|spec| DerivedColumn::parse(spec))
87 .collect()
88}