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::Left),
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 matches!(child.node_type, ASTNodeType::BinaryOp { .. })
86 } else {
87 if let ASTNodeType::BinaryOp { op: child_op, .. } = &child.node_type {
89 return child_op != parent_op;
90 }
91 false
92 }
93 }
94 }
95}
96
97fn unary_operand_needs_parens(unary_op: &str, operand: &ASTNode) -> bool {
98 match unary_op {
99 "%" | "#" => matches!(operand.node_type, ASTNodeType::BinaryOp { .. }),
100 _ => {
101 let operand_prec = node_precedence(operand);
102 operand_prec < unary_precedence(unary_op)
103 && matches!(operand.node_type, ASTNodeType::BinaryOp { .. })
104 }
105 }
106}
107
108fn pretty_child(
109 child: &ASTNode,
110 parent_op: &str,
111 parent_prec: u8,
112 parent_assoc: Associativity,
113 side: Side,
114 comma_delimited: bool,
115) -> String {
116 if child_needs_parens(child, parent_op, parent_prec, parent_assoc, side) {
117 format!("({})", pretty_print_node(child))
120 } else {
121 pretty_print_node_in_context(child, comma_delimited)
122 }
123}
124
125fn pretty_print_arguments(args: &[ASTNode]) -> String {
126 let mut rendered = String::new();
127 for (index, arg) in args.iter().enumerate() {
128 if index > 0 {
129 rendered.push(',');
130 if !matches!(arg.node_type, ASTNodeType::Omitted) {
131 rendered.push(' ');
132 }
133 }
134 rendered.push_str(&pretty_print_node_in_context(arg, true));
137 }
138 rendered
139}
140
141fn pretty_print_node(ast: &ASTNode) -> String {
142 pretty_print_node_in_context(ast, false)
143}
144
145fn pretty_print_node_in_context(ast: &ASTNode, comma_delimited: bool) -> String {
146 if comma_delimited && matches!(&ast.node_type, ASTNodeType::BinaryOp { op, .. } if op == ",") {
147 return format!("({})", pretty_print_node(ast));
148 }
149 match &ast.node_type {
150 ASTNodeType::Literal(value) => match value {
151 crate::LiteralValue::Text(s) => {
153 let escaped = s.replace('"', "\"\"");
154 format!("\"{escaped}\"")
155 }
156 _ => format!("{value}"),
157 },
158 ASTNodeType::Omitted => String::new(),
159 ASTNodeType::Reference { reference, .. } => reference.normalise(),
160 ASTNodeType::UnaryOp { op, expr } => {
161 let inner = if unary_operand_needs_parens(op, expr) {
162 format!("({})", pretty_print_node(expr))
163 } else {
164 pretty_print_node_in_context(expr, comma_delimited)
165 };
166
167 if op == "%" || op == "#" {
168 format!("{inner}{op}")
169 } else {
170 format!("{op}{inner}")
171 }
172 }
173 ASTNodeType::BinaryOp { op, left, right } => {
174 let (prec, assoc) = infix_info(op);
175 let left_s = pretty_child(left, op, prec, assoc, Side::Left, comma_delimited);
176 let right_s = pretty_child(right, op, prec, assoc, Side::Right, comma_delimited);
177
178 match op.as_str() {
179 ":" => format!("{left_s}:{right_s}"),
182 " " => format!("{left_s} {right_s}"),
183 "," => format!("{left_s}, {right_s}"),
184 _ => format!("{left_s} {op} {right_s}"),
185 }
186 }
187 ASTNodeType::Function { name, args } => {
188 let args_str = pretty_print_arguments(args);
189 format!("{}({})", name.to_uppercase(), args_str)
190 }
191 ASTNodeType::Call { callee, args } => {
192 let callee_str = pretty_print_node(callee);
193 let callee_rendered = match &callee.node_type {
197 ASTNodeType::Function { .. } | ASTNodeType::Call { .. } => callee_str,
198 _ => format!("({callee_str})"),
199 };
200 let args_str = pretty_print_arguments(args);
201 format!("{callee_rendered}({args_str})")
202 }
203 ASTNodeType::Array(rows) => {
204 let rows_str = rows
205 .iter()
206 .map(|row| {
207 row.iter()
208 .map(|cell| pretty_print_node_in_context(cell, true))
209 .collect::<Vec<String>>()
210 .join(", ")
211 })
212 .collect::<Vec<String>>()
213 .join("; ");
214
215 format!("{{{rows_str}}}")
216 }
217 }
218}
219
220pub fn canonical_formula(ast: &ASTNode) -> String {
225 format!("={}", pretty_print(ast))
226}
227
228pub fn pretty_parse_render(formula: &str) -> Result<String, ParserError> {
232 if formula.is_empty() {
234 return Ok(String::new());
235 }
236
237 let needs_equals = !formula.starts_with('=');
239 let formula_to_parse = if needs_equals {
240 format!("={formula}")
241 } else {
242 formula.to_string()
243 };
244
245 let ast = parse(&formula_to_parse)?;
247
248 let pretty_printed = pretty_print(&ast);
250
251 if needs_equals {
253 Ok(pretty_printed)
254 } else {
255 Ok(format!("={pretty_printed}"))
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_pretty_print_validation() {
265 let original = "= sum( a1 ,2 ) ";
266 let pretty = pretty_parse_render(original).unwrap();
267 assert_eq!(pretty, "=SUM(A1, 2)");
268
269 let round = pretty_parse_render(&pretty).unwrap();
270 assert_eq!(pretty, round); }
272
273 #[test]
274 fn test_ast_canonicalization() {
275 let formula = "=sum( a1, b2 )";
277 let pretty = pretty_parse_render(formula).unwrap();
278
279 assert_eq!(pretty, "=SUM(A1, B2)");
281
282 let repretty = pretty_parse_render(&pretty).unwrap();
284 assert_eq!(pretty, repretty);
285 }
286
287 #[test]
288 fn test_pretty_print_operators() {
289 let formula = "=a1+b2*3";
290 let pretty = pretty_parse_render(formula).unwrap();
291 assert_eq!(pretty, "=A1 + B2 * 3");
292
293 let formula = "=a1 + b2 * 3";
294 let pretty = pretty_parse_render(formula).unwrap();
295 assert_eq!(pretty, "=A1 + B2 * 3");
296 }
297
298 #[test]
299 fn test_pretty_print_inserts_parentheses_when_needed() {
300 let formula = "=(a1+b2)*c3";
301 let pretty = pretty_parse_render(formula).unwrap();
302 assert_eq!(pretty, "=(A1 + B2) * C3");
303 }
304
305 #[test]
306 fn test_pretty_print_keeps_same_precedence_right_grouping() {
307 for (formula, expected) in [
308 ("=A1*(B1*C1)", "=A1 * (B1 * C1)"),
309 ("=A1+(B1+C1)", "=A1 + (B1 + C1)"),
310 ("=1=(2=3)", "=1 = (2 = 3)"),
311 ("=\"a\"&(\"b\"&\"c\")", "=\"a\" & (\"b\" & \"c\")"),
312 ("=A1-(B1+C1)", "=A1 - (B1 + C1)"),
313 ("=(A1*B1)*C1", "=A1 * B1 * C1"),
315 ("=(1=2)=3", "=1 = 2 = 3"),
316 ] {
317 let pretty = pretty_parse_render(formula).unwrap();
318 assert_eq!(pretty, expected, "{formula}");
319 assert_eq!(
320 parse(&pretty).unwrap().fingerprint(),
321 parse(formula).unwrap().fingerprint(),
322 "{formula} must re-parse to the same tree"
323 );
324 }
325 }
326
327 #[test]
328 fn test_pretty_print_keeps_union_argument_brackets() {
329 for (formula, expected) in [
330 ("=RANK(A1,(B1,B5))", "=RANK(A1, (B1, B5))"),
331 ("=SUM((A1,B1),C1)", "=SUM((A1, B1), C1)"),
332 ("=SUM((A1,B1,C1))", "=SUM((A1, B1, C1))"),
333 ] {
334 let pretty = pretty_parse_render(formula).unwrap();
335 assert_eq!(pretty, expected, "{formula}");
336 assert_eq!(
337 parse(&pretty).unwrap().fingerprint(),
338 parse(formula).unwrap().fingerprint(),
339 "{formula} must re-parse to the same tree"
340 );
341 }
342 }
343
344 #[test]
345 fn test_pretty_print_function_nesting() {
346 let formula = "=if(a1>0, sum(b1:b10), average(c1:c10))";
347 let pretty = pretty_parse_render(formula).unwrap();
348 assert_eq!(pretty, "=IF(A1 > 0, SUM(B1:B10), AVERAGE(C1:C10))");
349 }
350
351 #[test]
352 fn test_pretty_print_arrays() {
353 let formula = "={1,2;3,4}";
354 let pretty = pretty_parse_render(formula).unwrap();
355 assert_eq!(pretty, "={1, 2; 3, 4}");
356
357 let formula = "={1, 2; 3, 4}";
358 let pretty = pretty_parse_render(formula).unwrap();
359 assert_eq!(pretty, "={1, 2; 3, 4}");
360 }
361
362 #[test]
363 fn test_pretty_print_references() {
364 let formula = "=Sheet1!$a$1:$b$2";
365 let pretty = pretty_parse_render(formula).unwrap();
366 assert_eq!(pretty, "=Sheet1!$A$1:$B$2");
367
368 let formula = "='My Sheet'!a1";
369 let pretty = pretty_parse_render(formula).unwrap();
370 assert_eq!(pretty, "='My Sheet'!A1");
371 }
372
373 #[test]
374 fn test_pretty_print_text_literals_in_functions() {
375 let formula = "=SUMIFS(A:A, B:B, \"*Parking*\")";
377 let pretty = pretty_parse_render(formula).unwrap();
378 assert_eq!(pretty, "=SUMIFS(A:A, B:B, \"*Parking*\")");
379 }
380
381 #[test]
382 fn test_pretty_print_text_concatenation_and_escaping() {
383 let formula = "=\">=\"&DATE(2024,1,1)";
385 let pretty = pretty_parse_render(formula).unwrap();
386 assert_eq!(pretty, "=\">=\" & DATE(2024, 1, 1)");
387
388 let formula = "=\"He said \"\"Hi\"\"\"";
390 let pretty = pretty_parse_render(formula).unwrap();
391 assert_eq!(pretty, "=\"He said \"\"Hi\"\"\"");
392 }
393
394 #[test]
395 fn test_pretty_print_text_in_arrays() {
396 let formula = "={\"A\", \"B\"; \"C\", \"D\"}";
397 let pretty = pretty_parse_render(formula).unwrap();
398 assert_eq!(pretty, "={\"A\", \"B\"; \"C\", \"D\"}");
399 }
400}