formualizer_parse/
pretty.rs

1use crate::parser::{ASTNode, ASTNodeType, Parser, ParserError};
2use crate::tokenizer::Tokenizer;
3
4/// Pretty-prints an AST node according to canonical formatting rules.
5///
6/// Rules:
7/// - All functions upper-case, no spaces before '('
8/// - Commas followed by single space; no space before ','
9/// - Binary operators surrounded by single spaces
10/// - No superfluous parentheses (keeps semantics)
11/// - References printed via .normalise()
12/// - Array literals: {1, 2; 3, 4}
13pub fn pretty_print(ast: &ASTNode) -> String {
14    match &ast.node_type {
15        ASTNodeType::Literal(value) => match value {
16            // Quote and escape text literals to preserve Excel semantics
17            crate::LiteralValue::Text(s) => {
18                let escaped = s.replace('"', "\"\"");
19                format!("\"{escaped}\"")
20            }
21            _ => format!("{value}"),
22        },
23        ASTNodeType::Reference { reference, .. } => reference.normalise(),
24        ASTNodeType::UnaryOp { op, expr } => {
25            format!("{}{}", op, pretty_print(expr))
26        }
27        ASTNodeType::BinaryOp { op, left, right } => {
28            // Special handling for range operator ':'
29            if op == ":" {
30                format!("{}:{}", pretty_print(left), pretty_print(right))
31            } else {
32                format!("{} {} {}", pretty_print(left), op, pretty_print(right))
33            }
34        }
35        ASTNodeType::Function { name, args } => {
36            let args_str = args
37                .iter()
38                .map(pretty_print)
39                .collect::<Vec<String>>()
40                .join(", ");
41
42            format!("{}({})", name.to_uppercase(), args_str)
43        }
44        ASTNodeType::Array(rows) => {
45            let rows_str = rows
46                .iter()
47                .map(|row| {
48                    row.iter()
49                        .map(pretty_print)
50                        .collect::<Vec<String>>()
51                        .join(", ")
52                })
53                .collect::<Vec<String>>()
54                .join("; ");
55
56            format!("{{{rows_str}}}")
57        }
58    }
59}
60
61/// Produce a canonical Excel formula string for an AST, prefixed with '='.
62///
63/// This is the single entry-point that UI layers should use when displaying
64/// a formula reconstructed from an AST.
65pub fn canonical_formula(ast: &ASTNode) -> String {
66    format!("={}", pretty_print(ast))
67}
68
69/// Tokenizes and parses a formula, then pretty-prints it.
70///
71/// Returns a Result with the pretty-printed formula or a parser error.
72pub fn pretty_parse_render(formula: &str) -> Result<String, ParserError> {
73    // Handle empty formula case
74    if formula.is_empty() {
75        return Ok(String::new());
76    }
77
78    // If formula doesn't start with '=', add it before parsing and remove it after
79    let needs_equals = !formula.starts_with('=');
80    let formula_to_parse = if needs_equals {
81        format!("={formula}")
82    } else {
83        formula.to_string()
84    };
85
86    // Tokenize, parse, and pretty-print
87    let tokenizer = match Tokenizer::new(&formula_to_parse) {
88        Ok(t) => t,
89        Err(e) => {
90            return Err(ParserError {
91                message: format!("Tokenizer error: {}", e.message),
92                position: None,
93            });
94        }
95    };
96
97    let mut parser = Parser::new(tokenizer.items, false);
98    let ast = parser.parse()?;
99
100    // Format the result with '=' prefix
101    let pretty_printed = pretty_print(&ast);
102
103    // Return the result with appropriate '=' prefix
104    if needs_equals {
105        Ok(pretty_printed)
106    } else {
107        Ok(format!("={pretty_printed}"))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_pretty_print_validation() {
117        let original = "= sum(  a1 ,2 ) ";
118        let pretty = pretty_parse_render(original).unwrap();
119        assert_eq!(pretty, "=SUM(A1, 2)");
120
121        let round = pretty_parse_render(&pretty).unwrap();
122        assert_eq!(pretty, round); // idempotent
123    }
124
125    #[test]
126    fn test_ast_canonicalization() {
127        // Test that our pretty printer produces canonical form
128        let formula = "=sum(  a1, b2  )";
129        let pretty = pretty_parse_render(formula).unwrap();
130
131        // Check that the pretty printed version is canonicalized
132        assert_eq!(pretty, "=SUM(A1, B2)");
133
134        // Test round-trip consistency
135        let repretty = pretty_parse_render(&pretty).unwrap();
136        assert_eq!(pretty, repretty);
137    }
138
139    #[test]
140    fn test_pretty_print_operators() {
141        let formula = "=a1+b2*3";
142        let pretty = pretty_parse_render(formula).unwrap();
143        assert_eq!(pretty, "=A1 + B2 * 3");
144
145        let formula = "=a1 + b2 *     3";
146        let pretty = pretty_parse_render(formula).unwrap();
147        assert_eq!(pretty, "=A1 + B2 * 3");
148    }
149
150    #[test]
151    fn test_pretty_print_function_nesting() {
152        let formula = "=if(a1>0, sum(b1:b10), average(c1:c10))";
153        let pretty = pretty_parse_render(formula).unwrap();
154        assert_eq!(pretty, "=IF(A1 > 0, SUM(B1:B10), AVERAGE(C1:C10))");
155    }
156
157    #[test]
158    fn test_pretty_print_arrays() {
159        let formula = "={1,2;3,4}";
160        let pretty = pretty_parse_render(formula).unwrap();
161        assert_eq!(pretty, "={1, 2; 3, 4}");
162
163        let formula = "={1, 2; 3, 4}";
164        let pretty = pretty_parse_render(formula).unwrap();
165        assert_eq!(pretty, "={1, 2; 3, 4}");
166    }
167
168    #[test]
169    fn test_pretty_print_references() {
170        let formula = "=Sheet1!$a$1:$b$2";
171        let pretty = pretty_parse_render(formula).unwrap();
172        assert_eq!(pretty, "=Sheet1!A1:B2");
173
174        let formula = "='My Sheet'!a1";
175        let pretty = pretty_parse_render(formula).unwrap();
176        assert_eq!(pretty, "='My Sheet'!A1");
177    }
178
179    #[test]
180    fn test_pretty_print_text_literals_in_functions() {
181        // Should preserve quotes around text literals
182        let formula = "=SUMIFS(A:A, B:B, \"*Parking*\")";
183        let pretty = pretty_parse_render(formula).unwrap();
184        assert_eq!(pretty, "=SUMIFS(A:A, B:B, \"*Parking*\")");
185    }
186
187    #[test]
188    fn test_pretty_print_text_concatenation_and_escaping() {
189        // Operators as text must stay quoted, and spacing around '&' is canonical
190        let formula = "=\">=\"&DATE(2024,1,1)";
191        let pretty = pretty_parse_render(formula).unwrap();
192        assert_eq!(pretty, "=\">=\" & DATE(2024, 1, 1)");
193
194        // Embedded quotes should be doubled
195        let formula = "=\"He said \"\"Hi\"\"\"";
196        let pretty = pretty_parse_render(formula).unwrap();
197        assert_eq!(pretty, "=\"He said \"\"Hi\"\"\"");
198    }
199
200    #[test]
201    fn test_pretty_print_text_in_arrays() {
202        let formula = "={\"A\", \"B\"; \"C\", \"D\"}";
203        let pretty = pretty_parse_render(formula).unwrap();
204        assert_eq!(pretty, "={\"A\", \"B\"; \"C\", \"D\"}");
205    }
206}