1use crate::base::{CellError, CellRef, Value};
11use crate::formula::Expr;
12use crate::model::{PivotTable, Workbook};
13
14pub const MAX_FORMULA_LEN: usize = 8_192;
16pub const MAX_CALC_FIELDS: usize = 256;
18const MAX_DEPTH: usize = 64;
20const PREFIX: &str = "__pcf";
21
22fn placeholder(i: usize) -> String {
23 format!("{PREFIX}{i}")
24}
25
26#[derive(Clone, Debug)]
29pub struct Compiled {
30 pub body: Expr,
31 pub refs: Vec<String>,
33}
34
35pub fn compile(formula: &str, field: &dyn Fn(&str) -> Option<String>) -> Result<Compiled, String> {
38 let text = formula.trim();
39 let text = text.strip_prefix('=').unwrap_or(text).trim();
40 if text.is_empty() {
41 return Err("The formula is empty.".into());
42 }
43 if text.len() > MAX_FORMULA_LEN {
44 return Err("The formula is too long.".into());
45 }
46 let chars: Vec<char> = text.chars().collect();
47 let at = |i: usize| chars.get(i).copied();
48 let mut out = String::with_capacity(text.len() + 16);
49 let mut refs: Vec<String> = Vec::new();
50 let slot = |name: String, refs: &mut Vec<String>| -> String {
51 let i = refs.iter().position(|r| r.eq_ignore_ascii_case(&name)).unwrap_or_else(|| {
52 refs.push(name);
53 refs.len() - 1
54 });
55 placeholder(i)
56 };
57 let mut i = 0;
58 while let Some(c) = at(i) {
59 match c {
60 '"' => {
61 out.push('"');
62 i += 1;
63 loop {
64 match at(i) {
65 None => return Err("The formula has an unterminated text value.".into()),
66 Some('"') if at(i + 1) == Some('"') => {
67 out.push_str("\"\"");
68 i += 2;
69 }
70 Some('"') => {
71 out.push('"');
72 i += 1;
73 break;
74 }
75 Some(x) => {
76 out.push(x);
77 i += 1;
78 }
79 }
80 }
81 }
82 '\'' => {
83 let mut name = String::new();
84 i += 1;
85 loop {
86 match at(i) {
87 None => return Err("The formula has an unterminated field name.".into()),
88 Some('\'') if at(i + 1) == Some('\'') => {
89 name.push('\'');
90 i += 2;
91 }
92 Some('\'') => {
93 i += 1;
94 break;
95 }
96 Some(x) => {
97 name.push(x);
98 i += 1;
99 }
100 }
101 }
102 let canon = field(&name).ok_or_else(|| format!("There's no field named '{name}'."))?;
103 out.push_str(&slot(canon, &mut refs));
104 }
105 c if c.is_ascii_digit() || (c == '.' && at(i + 1).is_some_and(|d| d.is_ascii_digit())) => {
106 while at(i).is_some_and(|d| d.is_ascii_digit() || d == '.') {
107 out.push(at(i).unwrap_or('0'));
108 i += 1;
109 }
110 if matches!(at(i), Some('e' | 'E')) {
111 let sign = usize::from(matches!(at(i + 1), Some('+' | '-')));
112 if at(i + 1 + sign).is_some_and(|d| d.is_ascii_digit()) {
113 for _ in 0..=sign {
114 out.push(at(i).unwrap_or('0'));
115 i += 1;
116 }
117 while at(i).is_some_and(|d| d.is_ascii_digit()) {
118 out.push(at(i).unwrap_or('0'));
119 i += 1;
120 }
121 }
122 }
123 }
124 c if c.is_alphabetic() || c == '_' => {
125 let start = i;
126 while at(i).is_some_and(|d| d.is_alphanumeric() || d == '_' || d == '.') {
127 i += 1;
128 }
129 let word: String = chars.get(start..i).map(|w| w.iter().collect()).unwrap_or_default();
130 let mut j = i;
131 while at(j).is_some_and(char::is_whitespace) {
132 j += 1;
133 }
134 let is_call = at(j) == Some('(');
135 match field(&word) {
136 Some(canon) if !is_call => out.push_str(&slot(canon, &mut refs)),
137 _ => out.push_str(&word),
138 }
139 }
140 c => {
141 out.push(c);
142 i += 1;
143 }
144 }
145 }
146 let body = crate::formula::parse(&out).map_err(|_| "The formula contains an error.".to_string())?;
147 let mut problem: Option<String> = None;
148 body.walk(&mut |e| {
149 if problem.is_some() {
150 return;
151 }
152 match e {
153 Expr::Name(n) if n.starts_with(PREFIX) => {}
154 Expr::Name(n) => problem = Some(format!("There's no field named '{n}'.")),
155 Expr::Ref(_) | Expr::Struct(_) => problem = Some("A calculated field can't refer to cells or tables; use field names.".into()),
156 _ => {}
157 }
158 });
159 match problem {
160 Some(p) => Err(p),
161 None => Ok(Compiled { body, refs }),
162 }
163}
164
165fn literal(v: &Value) -> Expr {
166 match v {
167 Value::Number(n) => Expr::Number(*n),
168 Value::Error(e) => Expr::Error(*e),
169 Value::Text(t) => Expr::Text(t.clone()),
170 Value::Bool(b) => Expr::Bool(*b),
171 Value::Empty => Expr::Number(0.0),
172 Value::Array(_) => literal(&v.scalar()),
173 }
174}
175
176pub fn eval(wb: &Workbook, c: &Compiled, args: &[Value]) -> Value {
178 let expr = if c.refs.is_empty() {
179 c.body.clone()
180 } else {
181 let mut call = Vec::with_capacity(c.refs.len() * 2 + 1);
182 for i in 0..c.refs.len() {
183 call.push(Expr::Name(placeholder(i)));
184 call.push(literal(args.get(i).unwrap_or(&Value::Empty)));
185 }
186 call.push(c.body.clone());
187 Expr::Call("LET".into(), call)
188 };
189 match crate::calc::recalc::evaluate_expr(wb, 0, CellRef::default(), &expr).scalar() {
190 Value::Empty => Value::Number(0.0),
191 v => v,
192 }
193}
194
195pub struct CalcSet {
197 names: Vec<String>,
198 compiled: Vec<Result<Compiled, String>>,
199}
200
201impl CalcSet {
202 pub fn new(pt: &PivotTable, headers: &[String]) -> CalcSet {
203 let names: Vec<String> = pt.calculated_fields.iter().take(MAX_CALC_FIELDS).map(|c| c.name.clone()).collect();
204 let lookup = |n: &str| -> Option<String> {
205 let n = n.trim();
206 headers.iter().chain(names.iter()).find(|h| h.trim().eq_ignore_ascii_case(n)).cloned()
207 };
208 let compiled = pt.calculated_fields.iter().take(MAX_CALC_FIELDS).map(|c| compile(&c.formula, &lookup)).collect();
209 CalcSet { names, compiled }
210 }
211
212 pub fn index(&self, name: &str) -> Option<usize> {
213 self.names.iter().position(|n| n.eq_ignore_ascii_case(name.trim()))
214 }
215
216 pub fn error(&self, i: usize) -> Option<&str> {
218 self.compiled.get(i).and_then(|c| c.as_ref().err()).map(String::as_str)
219 }
220
221 pub fn source_refs(&self, i: usize) -> Vec<String> {
223 let mut out: Vec<String> = Vec::new();
224 let mut seen = vec![false; self.names.len()];
225 let mut stack = vec![i];
226 while let Some(j) = stack.pop() {
227 if !seen.get(j).copied().unwrap_or(true) {
228 if let Some(s) = seen.get_mut(j) {
229 *s = true;
230 }
231 if let Some(Ok(c)) = self.compiled.get(j) {
232 for r in &c.refs {
233 match self.index(r) {
234 Some(k) => stack.push(k),
235 None => {
236 if !out.iter().any(|o| o.eq_ignore_ascii_case(r)) {
237 out.push(r.clone());
238 }
239 }
240 }
241 }
242 }
243 }
244 }
245 out
246 }
247
248 pub fn is_cyclic(&self, i: usize) -> bool {
250 let mut seen = vec![false; self.names.len()];
251 let mut stack: Vec<usize> = match self.compiled.get(i) {
252 Some(Ok(c)) => c.refs.iter().filter_map(|r| self.index(r)).collect(),
253 _ => vec![],
254 };
255 while let Some(j) = stack.pop() {
256 if j == i {
257 return true;
258 }
259 if seen.get(j).copied().unwrap_or(true) {
260 continue;
261 }
262 if let Some(s) = seen.get_mut(j) {
263 *s = true;
264 }
265 if let Some(Ok(c)) = self.compiled.get(j) {
266 stack.extend(c.refs.iter().filter_map(|r| self.index(r)));
267 }
268 }
269 false
270 }
271
272 pub fn value(&self, wb: &Workbook, i: usize, sum: &dyn Fn(&str) -> Value) -> Value {
275 let mut memo: Vec<Option<Value>> = vec![None; self.names.len()];
276 let mut active = vec![false; self.names.len()];
277 self.value_in(wb, i, sum, &mut memo, &mut active, 0)
278 }
279
280 fn value_in(&self, wb: &Workbook, i: usize, sum: &dyn Fn(&str) -> Value, memo: &mut [Option<Value>], active: &mut [bool], depth: usize) -> Value {
281 if let Some(Some(v)) = memo.get(i) {
282 return v.clone();
283 }
284 if depth > MAX_DEPTH || active.get(i).copied().unwrap_or(true) {
285 return Value::Error(CellError::Ref);
286 }
287 let Some(Ok(c)) = self.compiled.get(i) else { return Value::Error(CellError::Name) };
288 if let Some(a) = active.get_mut(i) {
289 *a = true;
290 }
291 let args: Vec<Value> = c
292 .refs
293 .iter()
294 .map(|r| match self.index(r) {
295 Some(j) => self.value_in(wb, j, sum, memo, active, depth + 1),
296 None => sum(r),
297 })
298 .collect();
299 let v = eval(wb, c, &args);
300 if let Some(a) = active.get_mut(i) {
301 *a = false;
302 }
303 if let Some(m) = memo.get_mut(i) {
304 *m = Some(v.clone());
305 }
306 v
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn fields(n: &str) -> Option<String> {
315 ["Sales", "Units", "Unit Price", "Q1"].iter().find(|f| f.eq_ignore_ascii_case(n)).map(|f| f.to_string())
316 }
317
318 #[test]
319 fn compiles_field_names() {
320 let c = compile("= Sales*0.1 + 'Unit Price'/units - Sales", &fields).unwrap();
321 assert_eq!(c.refs, vec!["Sales", "Unit Price", "Units"]);
322 let c = compile("=ROUND(Q1*1.5E2,0)", &fields).unwrap();
324 assert_eq!(c.refs, vec!["Q1"]);
325 let wb = Workbook::new();
326 assert_eq!(eval(&wb, &c, &[Value::Number(2.0)]), Value::Number(300.0));
327 assert!(compile("=Sales*Bogus", &fields).unwrap_err().contains("Bogus"));
328 assert!(compile("=A1*2", &fields).is_err());
329 assert!(compile("='Nope'*2", &fields).is_err());
330 assert!(compile("=Sales*(", &fields).is_err());
331 assert!(compile("=", &fields).is_err());
332 assert!(compile(&format!("={}", "1+".repeat(MAX_FORMULA_LEN)), &fields).is_err());
333 }
334
335 #[test]
336 fn deep_and_cyclic_chains_are_bounded() {
337 let mut pt = PivotTable::default();
339 pt.calculated_fields.push(crate::model::PivotCalcField { name: "C0".into(), formula: "=Sales+1".into() });
340 for i in 1..300 {
341 pt.calculated_fields.push(crate::model::PivotCalcField { name: format!("C{i}"), formula: format!("=C{}+1", i - 1) });
342 }
343 pt.calculated_fields.push(crate::model::PivotCalcField { name: "P".into(), formula: "=Q".into() });
344 pt.calculated_fields.push(crate::model::PivotCalcField { name: "Q".into(), formula: "=P*2".into() });
345 let set = CalcSet::new(&pt, &["Sales".to_string()]);
346 let wb = Workbook::new();
347 let sum = |_: &str| Value::Number(10.0);
348 assert_eq!(set.value(&wb, 0, &sum), Value::Number(11.0));
349 assert_eq!(set.value(&wb, 10, &sum), Value::Number(21.0));
350 assert_eq!(set.value(&wb, 200, &sum), Value::Error(CellError::Ref));
352 assert_eq!(set.index("P"), None, "P is past the cap of {MAX_CALC_FIELDS} fields");
353 let mut small = PivotTable::default();
355 small.calculated_fields.push(crate::model::PivotCalcField { name: "P".into(), formula: "=Q".into() });
356 small.calculated_fields.push(crate::model::PivotCalcField { name: "Q".into(), formula: "=P*2".into() });
357 let set = CalcSet::new(&small, &["Sales".to_string()]);
358 assert!(set.is_cyclic(0));
359 assert_eq!(set.value(&wb, 0, &sum), Value::Error(CellError::Ref));
360 assert_eq!(set.source_refs(0), Vec::<String>::new());
361 }
362}