Skip to main content

ravenrs_sheet/formula/
input.rs

1//! Localized aliases accepted at user-entry boundaries; file syntax stays English.
2
3use crate::formula::{Expr, ParseError, parser};
4
5/// Formula-entry syntax selected by the interface language. Other interface languages
6/// currently use the canonical English function table until a translation table is supplied.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum FormulaLocale {
9    #[default]
10    En,
11    Es,
12}
13
14const SPANISH: &[(&str, &str)] = &[
15    ("SUM", "SUMA"),
16    ("AVERAGE", "PROMEDIO"),
17    ("IF", "SI"),
18    ("COUNT", "CONTAR"),
19    ("SUMIF", "SUMAR.SI"),
20    ("COUNTIF", "CONTAR.SI"),
21    ("VLOOKUP", "BUSCARV"),
22    ("TODAY", "HOY"),
23    // Financial block (#1986): the loan-schedule and investment staples.
24    ("NPV", "VNA"),
25    ("IRR", "TIR"),
26    ("PMT", "PAGO"),
27    ("PV", "VA"),
28    ("FV", "VF"),
29    ("TRUE", "VERDADERO"),
30    ("FALSE", "FALSO"),
31];
32
33impl FormulaLocale {
34    pub fn list_separator(self) -> char {
35        if self == Self::Es { ';' } else { ',' }
36    }
37    pub fn decimal_separator(self) -> char {
38        if self == Self::Es { ',' } else { '.' }
39    }
40    pub fn array_column_separator(self) -> char {
41        if self == Self::Es { '\\' } else { ',' }
42    }
43    pub fn function_name(self, canonical: &str) -> &str {
44        if self == Self::Es {
45            SPANISH.iter().find(|(en, _)| en.eq_ignore_ascii_case(canonical)).map(|(_, es)| *es).unwrap_or(canonical)
46        } else {
47            canonical
48        }
49    }
50    pub fn canonical_name(self, localized: &str) -> &str {
51        if self == Self::Es {
52            SPANISH.iter().find(|(_, es)| es.eq_ignore_ascii_case(localized)).map(|(en, _)| *en).unwrap_or(localized)
53        } else {
54            localized
55        }
56    }
57}
58
59/// Parses UI input using the selected function table and separators. Existing names and
60/// LET/LAMBDA bindings take precedence over aliases. File and API syntax uses [`crate::formula::parse`].
61pub fn parse_input(src: &str, locale: FormulaLocale, known_name: impl Fn(&str) -> bool) -> Result<Expr, ParseError> {
62    let mut expr = parser::parse_syntax(src, locale)?;
63    if locale == FormulaLocale::Es {
64        normalize(&mut expr, &known_name, &mut Bindings::default(), 0, false)?;
65    }
66    Ok(expr)
67}
68
69/// Prints canonical expressions for editing. Name collisions retain the canonical spelling
70/// so displaying then committing a formula never changes a defined-name reference.
71pub fn print_input(expr: &Expr, locale: FormulaLocale, known_name: impl Fn(&str) -> bool) -> Result<String, ParseError> {
72    // Check before cloning/printing. A flat operator chain such as A1+A2+… nests on its left
73    // without nesting in the text: cloning, printing and `normalize` follow that left spine in
74    // a loop, so it does not count (Excel accepts 4,096 operands, #916). Other nesting does.
75    let mut pending = vec![(expr, 0usize)];
76    while let Some((node, depth)) = pending.pop() {
77        if depth > parser::MAX_DEPTH {
78            return Err(ParseError { msg: "formula is nested too deeply".into(), pos: 0 });
79        }
80        match node {
81            Expr::Unary(_, child) | Expr::Paren(child) => pending.push((child, depth + 1)),
82            Expr::Binary(_, left, right) => {
83                pending.push((left, depth));
84                pending.push((right, depth + 1));
85            }
86            Expr::Call(_, args) => pending.extend(args.iter().map(|e| (e, depth + 1))),
87            Expr::Invoke(callee, args) => {
88                pending.push((callee, depth + 1));
89                pending.extend(args.iter().map(|e| (e, depth + 1)));
90            }
91            Expr::Array(rows) => pending.extend(rows.iter().flatten().map(|e| (e, depth + 1))),
92            _ => {}
93        }
94    }
95    if locale == FormulaLocale::En {
96        return Ok(crate::formula::print(expr));
97    }
98    let mut expr = expr.clone();
99    normalize(&mut expr, &known_name, &mut Bindings::default(), 0, true)?;
100    let canonical = crate::formula::print(&expr);
101    let tokens = crate::formula::lexer::tokenize(&canonical).map_err(|e| ParseError { msg: e.msg, pos: e.pos })?;
102    let mut out = String::new();
103    let mut array_depth = 0usize;
104    for token in tokens {
105        use crate::formula::lexer::Tok;
106        let text = canonical.get(token.start..token.end).unwrap_or("");
107        match token.tok {
108            Tok::LBrace => {
109                array_depth += 1;
110                out.push_str(text);
111            }
112            Tok::RBrace => {
113                array_depth = array_depth.saturating_sub(1);
114                out.push_str(text);
115            }
116            Tok::Number(_) => out.push_str(&text.replace('.', &locale.decimal_separator().to_string())),
117            Tok::Comma => out.push(if array_depth > 0 { locale.array_column_separator() } else { locale.list_separator() }),
118            _ => out.push_str(text),
119        }
120    }
121    Ok(out)
122}
123
124pub(crate) fn boolean_alias(name: &str) -> Option<bool> {
125    if name.eq_ignore_ascii_case("VERDADERO") {
126        Some(true)
127    } else if name.eq_ignore_ascii_case("FALSO") {
128        Some(false)
129    } else {
130        None
131    }
132}
133
134/// The LET/LAMBDA names in scope, with how many times each is bound, so a lookup is one probe
135/// instead of a scan of every binding for each call.
136#[derive(Default)]
137struct Bindings {
138    stack: Vec<String>,
139    counts: std::collections::HashMap<String, usize>,
140}
141
142impl Bindings {
143    fn len(&self) -> usize {
144        self.stack.len()
145    }
146
147    fn push(&mut self, name: &str) {
148        let key = crate::formula::fold_name(name);
149        *self.counts.entry(key.clone()).or_default() += 1;
150        self.stack.push(key);
151    }
152
153    fn truncate(&mut self, len: usize) {
154        while self.stack.len() > len {
155            let Some(key) = self.stack.pop() else { break };
156            if let Some(n) = self.counts.get_mut(&key) {
157                *n -= 1;
158                if *n == 0 {
159                    self.counts.remove(&key);
160                }
161            }
162        }
163    }
164
165    fn contains(&self, name: &str) -> bool {
166        self.counts.contains_key(&crate::formula::fold_name(name))
167    }
168}
169
170fn normalize(expr: &mut Expr, known_name: &impl Fn(&str) -> bool, bindings: &mut Bindings, depth: usize, display: bool) -> Result<(), ParseError> {
171    if depth > parser::MAX_DEPTH {
172        return Err(ParseError { msg: "formula is nested too deeply".into(), pos: 0 });
173    }
174    let bound = |name: &str, bindings: &Bindings| bindings.contains(name) || known_name(name);
175    match expr {
176        Expr::Bool(value) if display => {
177            let alias = if *value { "VERDADERO" } else { "FALSO" };
178            if !bound(alias, bindings) {
179                *expr = Expr::Name(alias.into());
180            }
181        }
182        Expr::Name(name) if !display && !bound(name, bindings) => {
183            if let Some(value) = boolean_alias(name) {
184                *expr = Expr::Bool(value);
185            }
186        }
187        Expr::Call(name, args) if name == "LET" && args.len() >= 3 && !args.len().is_multiple_of(2) => {
188            let saved = bindings.len();
189            if let Some((body, pairs)) = args.split_last_mut() {
190                for pair in pairs.chunks_mut(2) {
191                    if let [declaration, value] = pair {
192                        // The new name is not in scope in its own value expression.
193                        normalize(value, known_name, bindings, depth + 1, display)?;
194                        if let Expr::Name(name) = declaration {
195                            bindings.push(name);
196                        }
197                    }
198                }
199                normalize(body, known_name, bindings, depth + 1, display)?;
200            }
201            bindings.truncate(saved);
202        }
203        Expr::Call(name, args) if name == "LAMBDA" => {
204            let saved = bindings.len();
205            if let Some((body, params)) = args.split_last_mut() {
206                for param in params {
207                    if let Expr::Name(name) = param {
208                        bindings.push(name);
209                    }
210                }
211                normalize(body, known_name, bindings, depth + 1, display)?;
212            }
213            bindings.truncate(saved);
214        }
215        Expr::Call(name, args) => {
216            let alias = if display { FormulaLocale::Es.function_name(name) } else { FormulaLocale::Es.canonical_name(name) };
217            if !bound(name, bindings) && (!display || !bound(alias, bindings)) {
218                *name = alias.to_string();
219            }
220            for arg in args {
221                normalize(arg, known_name, bindings, depth + 1, display)?;
222            }
223        }
224        Expr::Invoke(callee, args) => {
225            normalize(callee, known_name, bindings, depth + 1, display)?;
226            for arg in args {
227                normalize(arg, known_name, bindings, depth + 1, display)?;
228            }
229        }
230        Expr::Unary(_, child) | Expr::Paren(child) => normalize(child, known_name, bindings, depth + 1, display)?,
231        Expr::Binary(..) => {
232            // Follow the left spine of a flat chain (A1+A2+…) in a loop: it is not nesting.
233            // Each call leaves `bindings` as it found them, so the order does not matter.
234            let mut node = expr;
235            while let Expr::Binary(_, left, right) = node {
236                normalize(right, known_name, bindings, depth + 1, display)?;
237                node = left;
238            }
239            normalize(node, known_name, bindings, depth, display)?;
240        }
241        Expr::Array(rows) if display => {
242            for row in rows {
243                for child in row {
244                    normalize(child, known_name, bindings, depth + 1, display)?;
245                }
246            }
247        }
248        // Array elements have already been parsed as literals. Sheet/structured references
249        // contain user-defined labels and must not be translated.
250        _ => {}
251    }
252    Ok(())
253}