1use crate::parser::{ASTNode, ASTNodeType, ParserError, parse};
2use crate::tokenizer::Associativity;
3
4pub fn pretty_print(ast: &ASTNode) -> String {
23 pretty_print_node(ast)
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27enum Side {
28 Left,
29 Right,
30}
31
32fn infix_info(op: &str) -> (u8, Associativity) {
33 match op {
34 ":" => (10, Associativity::Left),
35 " " => (9, Associativity::Left),
36 "," => (8, Associativity::Left),
37 "^" => (5, Associativity::Left),
38 "*" | "/" => (4, Associativity::Left),
39 "+" | "-" => (3, Associativity::Left),
40 "&" => (2, Associativity::Left),
41 "=" | "<" | ">" | "<=" | ">=" | "<>" => (1, Associativity::Left),
42 _ => (0, Associativity::Left),
43 }
44}
45
46fn unary_precedence(op: &str) -> u8 {
47 match op {
48 "#" => 11,
49 "%" => 7,
50 _ => 6,
51 }
52}
53
54fn node_precedence(ast: &ASTNode) -> u8 {
55 match &ast.node_type {
56 ASTNodeType::BinaryOp { op, .. } => infix_info(op.as_str()).0,
57 ASTNodeType::UnaryOp { op, .. } => unary_precedence(op.as_str()),
58 _ => 10,
60 }
61}
62
63fn child_needs_parens(
64 child: &ASTNode,
65 parent_op: &str,
66 parent_prec: u8,
67 parent_assoc: Associativity,
68 side: Side,
69) -> bool {
70 let child_prec = node_precedence(child);
71 if child_prec < parent_prec {
72 return true;
73 }
74 if child_prec > parent_prec {
75 return false;
76 }
77
78 match side {
80 Side::Left => {
81 if parent_assoc == Associativity::Right {
82 matches!(child.node_type, ASTNodeType::BinaryOp { .. })
84 } else {
85 false
86 }
87 }
88 Side::Right => {
89 if parent_assoc == Associativity::Left {
90 matches!(child.node_type, ASTNodeType::BinaryOp { .. })
95 } else {
96 if let ASTNodeType::BinaryOp { op: child_op, .. } = &child.node_type {
98 return child_op.as_str() != parent_op;
99 }
100 false
101 }
102 }
103 }
104}
105
106fn unary_operand_needs_parens(unary_op: &str, operand: &ASTNode) -> bool {
107 match unary_op {
108 "%" | "#" => matches!(operand.node_type, ASTNodeType::BinaryOp { .. }),
109 _ => {
110 let operand_prec = node_precedence(operand);
111 operand_prec < unary_precedence(unary_op)
112 && matches!(operand.node_type, ASTNodeType::BinaryOp { .. })
113 }
114 }
115}
116
117fn pretty_child(
118 child: &ASTNode,
119 parent_op: &str,
120 parent_prec: u8,
121 parent_assoc: Associativity,
122 side: Side,
123 comma_delimited: bool,
124) -> String {
125 if child_needs_parens(child, parent_op, parent_prec, parent_assoc, side) {
126 format!("({})", pretty_print_node(child))
129 } else {
130 pretty_print_node_in_context(child, comma_delimited)
131 }
132}
133
134fn pretty_print_arguments(args: &[ASTNode]) -> String {
135 let mut rendered = String::new();
136 for (index, arg) in args.iter().enumerate() {
137 if index > 0 {
138 rendered.push(',');
139 if !matches!(arg.node_type, ASTNodeType::Omitted) {
140 rendered.push(' ');
141 }
142 }
143 rendered.push_str(&pretty_print_node_in_context(arg, true));
146 }
147 rendered
148}
149
150fn pretty_print_node(ast: &ASTNode) -> String {
151 pretty_print_node_in_context(ast, false)
152}
153
154fn pretty_print_node_in_context(ast: &ASTNode, comma_delimited: bool) -> String {
155 if comma_delimited
156 && matches!(&ast.node_type, ASTNodeType::BinaryOp { op, .. } if op.as_str() == ",")
157 {
158 return format!("({})", pretty_print_node(ast));
159 }
160 match &ast.node_type {
161 ASTNodeType::Literal(value) => match value {
162 crate::LiteralValue::Text(s) => {
164 let escaped = s.replace('"', "\"\"");
165 format!("\"{escaped}\"")
166 }
167 _ => format!("{value}"),
168 },
169 ASTNodeType::Omitted => String::new(),
170 ASTNodeType::Reference { reference, .. } => reference.normalise(),
171 ASTNodeType::UnaryOp { op, expr } => {
172 let inner = if unary_operand_needs_parens(op.as_str(), expr) {
173 format!("({})", pretty_print_node(expr))
174 } else {
175 pretty_print_node_in_context(expr, comma_delimited)
176 };
177
178 if op.as_str() == "%" || op.as_str() == "#" {
179 format!("{inner}{op}")
180 } else {
181 format!("{op}{inner}")
182 }
183 }
184 ASTNodeType::BinaryOp { op, left, right } => {
185 let (prec, assoc) = infix_info(op.as_str());
186 let left_s = pretty_child(left, op.as_str(), prec, assoc, Side::Left, comma_delimited);
187 let right_s = pretty_child(
188 right,
189 op.as_str(),
190 prec,
191 assoc,
192 Side::Right,
193 comma_delimited,
194 );
195
196 match op.as_str() {
197 ":" => format!("{left_s}:{right_s}"),
200 " " => format!("{left_s} {right_s}"),
201 "," => format!("{left_s}, {right_s}"),
202 _ => format!("{left_s} {op} {right_s}"),
203 }
204 }
205 ASTNodeType::Function { name, args } => {
206 let args_str = pretty_print_arguments(args);
207 format!("{}({})", name.to_uppercase(), args_str)
208 }
209 ASTNodeType::Call { callee, args } => {
210 let callee_str = pretty_print_node(callee);
211 let callee_rendered = match &callee.node_type {
215 ASTNodeType::Function { .. } | ASTNodeType::Call { .. } => callee_str,
216 _ => format!("({callee_str})"),
217 };
218 let args_str = pretty_print_arguments(args);
219 format!("{callee_rendered}({args_str})")
220 }
221 ASTNodeType::Array(rows) => {
222 let rows_str = rows
223 .iter()
224 .map(|row| {
225 row.iter()
226 .map(|cell| pretty_print_node_in_context(cell, true))
227 .collect::<Vec<String>>()
228 .join(", ")
229 })
230 .collect::<Vec<String>>()
231 .join("; ");
232
233 format!("{{{rows_str}}}")
234 }
235 }
236}
237
238pub fn canonical_formula(ast: &ASTNode) -> String {
252 format!("={}", pretty_print(ast))
253}
254
255pub fn pretty_parse_render(formula: &str) -> Result<String, ParserError> {
259 if formula.is_empty() {
261 return Ok(String::new());
262 }
263
264 let needs_equals = !formula.starts_with('=');
266 let formula_to_parse = if needs_equals {
267 format!("={formula}")
268 } else {
269 formula.to_string()
270 };
271
272 let ast = parse(&formula_to_parse)?;
274
275 let pretty_printed = pretty_print(&ast);
277
278 if needs_equals {
280 Ok(pretty_printed)
281 } else {
282 Ok(format!("={pretty_printed}"))
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_pretty_print_validation() {
292 let original = "= sum( a1 ,2 ) ";
293 let pretty = pretty_parse_render(original).unwrap();
294 assert_eq!(pretty, "=SUM(A1, 2)");
295
296 let round = pretty_parse_render(&pretty).unwrap();
297 assert_eq!(pretty, round); }
299
300 #[test]
301 fn test_ast_canonicalization() {
302 let formula = "=sum( a1, b2 )";
304 let pretty = pretty_parse_render(formula).unwrap();
305
306 assert_eq!(pretty, "=SUM(A1, B2)");
308
309 let repretty = pretty_parse_render(&pretty).unwrap();
311 assert_eq!(pretty, repretty);
312 }
313
314 #[test]
315 fn test_pretty_print_operators() {
316 let formula = "=a1+b2*3";
317 let pretty = pretty_parse_render(formula).unwrap();
318 assert_eq!(pretty, "=A1 + B2 * 3");
319
320 let formula = "=a1 + b2 * 3";
321 let pretty = pretty_parse_render(formula).unwrap();
322 assert_eq!(pretty, "=A1 + B2 * 3");
323 }
324
325 #[test]
326 fn test_pretty_print_inserts_parentheses_when_needed() {
327 let formula = "=(a1+b2)*c3";
328 let pretty = pretty_parse_render(formula).unwrap();
329 assert_eq!(pretty, "=(A1 + B2) * C3");
330 }
331
332 #[test]
333 fn test_pretty_print_keeps_same_precedence_right_grouping() {
334 for (formula, expected) in [
335 ("=A1*(B1*C1)", "=A1 * (B1 * C1)"),
336 ("=A1+(B1+C1)", "=A1 + (B1 + C1)"),
337 ("=1=(2=3)", "=1 = (2 = 3)"),
338 ("=\"a\"&(\"b\"&\"c\")", "=\"a\" & (\"b\" & \"c\")"),
339 ("=A1-(B1+C1)", "=A1 - (B1 + C1)"),
340 ("=(A1*B1)*C1", "=A1 * B1 * C1"),
342 ("=(1=2)=3", "=1 = 2 = 3"),
343 ] {
344 let pretty = pretty_parse_render(formula).unwrap();
345 assert_eq!(pretty, expected, "{formula}");
346 assert_eq!(
347 parse(&pretty).unwrap().fingerprint(),
348 parse(formula).unwrap().fingerprint(),
349 "{formula} must re-parse to the same tree"
350 );
351 }
352 }
353
354 #[test]
355 fn test_pretty_print_keeps_union_argument_brackets() {
356 for (formula, expected) in [
357 ("=RANK(A1,(B1,B5))", "=RANK(A1, (B1, B5))"),
358 ("=SUM((A1,B1),C1)", "=SUM((A1, B1), C1)"),
359 ("=SUM((A1,B1,C1))", "=SUM((A1, B1, C1))"),
360 ] {
361 let pretty = pretty_parse_render(formula).unwrap();
362 assert_eq!(pretty, expected, "{formula}");
363 assert_eq!(
364 parse(&pretty).unwrap().fingerprint(),
365 parse(formula).unwrap().fingerprint(),
366 "{formula} must re-parse to the same tree"
367 );
368 }
369 }
370
371 #[test]
372 fn test_pretty_print_function_nesting() {
373 let formula = "=if(a1>0, sum(b1:b10), average(c1:c10))";
374 let pretty = pretty_parse_render(formula).unwrap();
375 assert_eq!(pretty, "=IF(A1 > 0, SUM(B1:B10), AVERAGE(C1:C10))");
376 }
377
378 #[test]
379 fn test_pretty_print_arrays() {
380 let formula = "={1,2;3,4}";
381 let pretty = pretty_parse_render(formula).unwrap();
382 assert_eq!(pretty, "={1, 2; 3, 4}");
383
384 let formula = "={1, 2; 3, 4}";
385 let pretty = pretty_parse_render(formula).unwrap();
386 assert_eq!(pretty, "={1, 2; 3, 4}");
387 }
388
389 #[test]
390 fn test_pretty_print_references() {
391 let formula = "=Sheet1!$a$1:$b$2";
392 let pretty = pretty_parse_render(formula).unwrap();
393 assert_eq!(pretty, "=Sheet1!$A$1:$B$2");
394
395 let formula = "='My Sheet'!a1";
396 let pretty = pretty_parse_render(formula).unwrap();
397 assert_eq!(pretty, "='My Sheet'!A1");
398 }
399
400 #[test]
401 fn test_pretty_print_text_literals_in_functions() {
402 let formula = "=SUMIFS(A:A, B:B, \"*Parking*\")";
404 let pretty = pretty_parse_render(formula).unwrap();
405 assert_eq!(pretty, "=SUMIFS(A:A, B:B, \"*Parking*\")");
406 }
407
408 #[test]
409 fn test_pretty_print_text_concatenation_and_escaping() {
410 let formula = "=\">=\"&DATE(2024,1,1)";
412 let pretty = pretty_parse_render(formula).unwrap();
413 assert_eq!(pretty, "=\">=\" & DATE(2024, 1, 1)");
414
415 let formula = "=\"He said \"\"Hi\"\"\"";
417 let pretty = pretty_parse_render(formula).unwrap();
418 assert_eq!(pretty, "=\"He said \"\"Hi\"\"\"");
419 }
420
421 #[test]
422 fn test_pretty_print_text_in_arrays() {
423 let formula = "={\"A\", \"B\"; \"C\", \"D\"}";
424 let pretty = pretty_parse_render(formula).unwrap();
425 assert_eq!(pretty, "={\"A\", \"B\"; \"C\", \"D\"}");
426 }
427}