1use crate::parser::{ASTNode, ASTNodeType, ParserError, parse};
2use crate::tokenizer::Associativity;
3
4pub fn pretty_print(ast: &ASTNode) -> String {
14 pretty_print_node(ast)
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18enum Side {
19 Left,
20 Right,
21}
22
23fn infix_info(op: &str) -> (u8, Associativity) {
24 match op {
25 ":" => (10, Associativity::Left),
26 " " => (9, Associativity::Left),
27 "," => (8, Associativity::Left),
28 "^" => (5, Associativity::Right),
29 "*" | "/" => (4, Associativity::Left),
30 "+" | "-" => (3, Associativity::Left),
31 "&" => (2, Associativity::Left),
32 "=" | "<" | ">" | "<=" | ">=" | "<>" => (1, Associativity::Left),
33 _ => (0, Associativity::Left),
34 }
35}
36
37fn unary_precedence(op: &str) -> u8 {
38 match op {
39 "#" => 11,
40 "%" => 7,
41 _ => 6,
42 }
43}
44
45fn node_precedence(ast: &ASTNode) -> u8 {
46 match &ast.node_type {
47 ASTNodeType::BinaryOp { op, .. } => infix_info(op).0,
48 ASTNodeType::UnaryOp { op, .. } => unary_precedence(op),
49 _ => 10,
51 }
52}
53
54fn child_needs_parens(
55 child: &ASTNode,
56 parent_op: &str,
57 parent_prec: u8,
58 parent_assoc: Associativity,
59 side: Side,
60) -> bool {
61 let child_prec = node_precedence(child);
62 if child_prec < parent_prec {
63 return true;
64 }
65 if child_prec > parent_prec {
66 return false;
67 }
68
69 match side {
71 Side::Left => {
72 if parent_assoc == Associativity::Right {
73 matches!(child.node_type, ASTNodeType::BinaryOp { .. })
75 } else {
76 false
77 }
78 }
79 Side::Right => {
80 if parent_assoc == Associativity::Left {
81 if let ASTNodeType::BinaryOp { op: child_op, .. } = &child.node_type {
82 if child_op != parent_op {
83 return true;
84 }
85
86 if parent_op == "-" || parent_op == "/" {
88 return true;
89 }
90 }
91 false
92 } else {
93 if let ASTNodeType::BinaryOp { op: child_op, .. } = &child.node_type {
95 return child_op != parent_op;
96 }
97 false
98 }
99 }
100 }
101}
102
103fn unary_operand_needs_parens(unary_op: &str, operand: &ASTNode) -> bool {
104 match unary_op {
105 "%" | "#" => matches!(operand.node_type, ASTNodeType::BinaryOp { .. }),
106 _ => {
107 let operand_prec = node_precedence(operand);
108 operand_prec < unary_precedence(unary_op)
109 && matches!(operand.node_type, ASTNodeType::BinaryOp { .. })
110 }
111 }
112}
113
114fn pretty_child(
115 child: &ASTNode,
116 parent_op: &str,
117 parent_prec: u8,
118 parent_assoc: Associativity,
119 side: Side,
120) -> String {
121 let s = pretty_print_node(child);
122 if child_needs_parens(child, parent_op, parent_prec, parent_assoc, side) {
123 format!("({s})")
124 } else {
125 s
126 }
127}
128
129fn pretty_print_arguments(args: &[ASTNode]) -> String {
130 let mut rendered = String::new();
131 for (index, arg) in args.iter().enumerate() {
132 if index > 0 {
133 rendered.push(',');
134 if !matches!(arg.node_type, ASTNodeType::Omitted) {
135 rendered.push(' ');
136 }
137 }
138 rendered.push_str(&pretty_print_node(arg));
139 }
140 rendered
141}
142
143fn pretty_print_node(ast: &ASTNode) -> String {
144 match &ast.node_type {
145 ASTNodeType::Literal(value) => match value {
146 crate::LiteralValue::Text(s) => {
148 let escaped = s.replace('"', "\"\"");
149 format!("\"{escaped}\"")
150 }
151 _ => format!("{value}"),
152 },
153 ASTNodeType::Omitted => String::new(),
154 ASTNodeType::Reference { reference, .. } => reference.normalise(),
155 ASTNodeType::UnaryOp { op, expr } => {
156 let inner = pretty_print_node(expr);
157 let inner = if unary_operand_needs_parens(op, expr) {
158 format!("({inner})")
159 } else {
160 inner
161 };
162
163 if op == "%" || op == "#" {
164 format!("{inner}{op}")
165 } else {
166 format!("{op}{inner}")
167 }
168 }
169 ASTNodeType::BinaryOp { op, left, right } => {
170 let (prec, assoc) = infix_info(op);
171 let left_s = pretty_child(left, op, prec, assoc, Side::Left);
172 let right_s = pretty_child(right, op, prec, assoc, Side::Right);
173
174 match op.as_str() {
175 ":" => format!("{left_s}:{right_s}"),
178 " " => format!("{left_s} {right_s}"),
179 "," => format!("{left_s}, {right_s}"),
180 _ => format!("{left_s} {op} {right_s}"),
181 }
182 }
183 ASTNodeType::Function { name, args } => {
184 let args_str = pretty_print_arguments(args);
185 format!("{}({})", name.to_uppercase(), args_str)
186 }
187 ASTNodeType::Call { callee, args } => {
188 let callee_str = pretty_print_node(callee);
189 let callee_rendered = match &callee.node_type {
193 ASTNodeType::Function { .. } | ASTNodeType::Call { .. } => callee_str,
194 _ => format!("({callee_str})"),
195 };
196 let args_str = pretty_print_arguments(args);
197 format!("{callee_rendered}({args_str})")
198 }
199 ASTNodeType::Array(rows) => {
200 let rows_str = rows
201 .iter()
202 .map(|row| {
203 row.iter()
204 .map(pretty_print_node)
205 .collect::<Vec<String>>()
206 .join(", ")
207 })
208 .collect::<Vec<String>>()
209 .join("; ");
210
211 format!("{{{rows_str}}}")
212 }
213 }
214}
215
216pub fn canonical_formula(ast: &ASTNode) -> String {
221 format!("={}", pretty_print(ast))
222}
223
224pub fn pretty_parse_render(formula: &str) -> Result<String, ParserError> {
228 if formula.is_empty() {
230 return Ok(String::new());
231 }
232
233 let needs_equals = !formula.starts_with('=');
235 let formula_to_parse = if needs_equals {
236 format!("={formula}")
237 } else {
238 formula.to_string()
239 };
240
241 let ast = parse(&formula_to_parse)?;
243
244 let pretty_printed = pretty_print(&ast);
246
247 if needs_equals {
249 Ok(pretty_printed)
250 } else {
251 Ok(format!("={pretty_printed}"))
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_pretty_print_validation() {
261 let original = "= sum( a1 ,2 ) ";
262 let pretty = pretty_parse_render(original).unwrap();
263 assert_eq!(pretty, "=SUM(A1, 2)");
264
265 let round = pretty_parse_render(&pretty).unwrap();
266 assert_eq!(pretty, round); }
268
269 #[test]
270 fn test_ast_canonicalization() {
271 let formula = "=sum( a1, b2 )";
273 let pretty = pretty_parse_render(formula).unwrap();
274
275 assert_eq!(pretty, "=SUM(A1, B2)");
277
278 let repretty = pretty_parse_render(&pretty).unwrap();
280 assert_eq!(pretty, repretty);
281 }
282
283 #[test]
284 fn test_pretty_print_operators() {
285 let formula = "=a1+b2*3";
286 let pretty = pretty_parse_render(formula).unwrap();
287 assert_eq!(pretty, "=A1 + B2 * 3");
288
289 let formula = "=a1 + b2 * 3";
290 let pretty = pretty_parse_render(formula).unwrap();
291 assert_eq!(pretty, "=A1 + B2 * 3");
292 }
293
294 #[test]
295 fn test_pretty_print_inserts_parentheses_when_needed() {
296 let formula = "=(a1+b2)*c3";
297 let pretty = pretty_parse_render(formula).unwrap();
298 assert_eq!(pretty, "=(A1 + B2) * C3");
299 }
300
301 #[test]
302 fn test_pretty_print_function_nesting() {
303 let formula = "=if(a1>0, sum(b1:b10), average(c1:c10))";
304 let pretty = pretty_parse_render(formula).unwrap();
305 assert_eq!(pretty, "=IF(A1 > 0, SUM(B1:B10), AVERAGE(C1:C10))");
306 }
307
308 #[test]
309 fn test_pretty_print_arrays() {
310 let formula = "={1,2;3,4}";
311 let pretty = pretty_parse_render(formula).unwrap();
312 assert_eq!(pretty, "={1, 2; 3, 4}");
313
314 let formula = "={1, 2; 3, 4}";
315 let pretty = pretty_parse_render(formula).unwrap();
316 assert_eq!(pretty, "={1, 2; 3, 4}");
317 }
318
319 #[test]
320 fn test_pretty_print_references() {
321 let formula = "=Sheet1!$a$1:$b$2";
322 let pretty = pretty_parse_render(formula).unwrap();
323 assert_eq!(pretty, "=Sheet1!$A$1:$B$2");
324
325 let formula = "='My Sheet'!a1";
326 let pretty = pretty_parse_render(formula).unwrap();
327 assert_eq!(pretty, "='My Sheet'!A1");
328 }
329
330 #[test]
331 fn test_pretty_print_text_literals_in_functions() {
332 let formula = "=SUMIFS(A:A, B:B, \"*Parking*\")";
334 let pretty = pretty_parse_render(formula).unwrap();
335 assert_eq!(pretty, "=SUMIFS(A:A, B:B, \"*Parking*\")");
336 }
337
338 #[test]
339 fn test_pretty_print_text_concatenation_and_escaping() {
340 let formula = "=\">=\"&DATE(2024,1,1)";
342 let pretty = pretty_parse_render(formula).unwrap();
343 assert_eq!(pretty, "=\">=\" & DATE(2024, 1, 1)");
344
345 let formula = "=\"He said \"\"Hi\"\"\"";
347 let pretty = pretty_parse_render(formula).unwrap();
348 assert_eq!(pretty, "=\"He said \"\"Hi\"\"\"");
349 }
350
351 #[test]
352 fn test_pretty_print_text_in_arrays() {
353 let formula = "={\"A\", \"B\"; \"C\", \"D\"}";
354 let pretty = pretty_parse_render(formula).unwrap();
355 assert_eq!(pretty, "={\"A\", \"B\"; \"C\", \"D\"}");
356 }
357}