csv_managed/
derive.rs

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}