1use crate::types::*;
8use std::fmt;
9
10const ELEMENTS: &[&str; 118] = &[
12 "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl",
13 "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As",
14 "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In",
15 "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb",
16 "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl",
17 "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk",
18 "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh",
19 "Fl", "Mc", "Lv", "Ts", "Og",
20];
21
22const UNICODE_SUBSCRIPTS: [char; 10] = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
24
25const UNICODE_SUPERSCRIPTS: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
27
28const PRECEDENCE: &[(&str, i32)] = &[
33 ("or", 1),
34 ("and", 2),
35 ("==", 3),
36 ("!=", 3),
37 ("=", 3),
38 ("<", 3),
39 (">", 3),
40 ("<=", 3),
41 (">=", 3),
42 ("+", 4),
43 ("-", 4),
44 ("*", 5),
45 ("/", 5),
46 ("not", 6),
47 ("^", 7),
48];
49
50const ASSOCIATIVE_OPS: &[&str] = &["+", "*", "and", "or"];
56
57const UMINUS_OPERAND_PARENT_PREC: i32 = 4;
65
66fn render_operand(op: &str, op_prec: i32, arg: &Expr, is_right: bool, fmt: Fmt) -> String {
73 if !is_right {
74 return render_at(arg, fmt, op_prec - 1);
75 }
76 let bare = match arg {
77 Expr::Operator(n) => {
78 (n.op == op && ASSOCIATIVE_OPS.contains(&op))
79 || (fmt == Fmt::Latex && op == "*" && n.op == "/" && n.args.len() == 2)
80 }
81 _ => true,
82 };
83 render_at(arg, fmt, if bare { op_prec - 1 } else { op_prec })
84}
85
86fn starts_with_literal_power(expr: &Expr) -> bool {
91 let Expr::Operator(n) = expr else {
92 return false;
93 };
94 match n.op.as_str() {
95 "^" => matches!(
96 n.args.first(),
97 Some(Expr::Integer(_)) | Some(Expr::Number(_))
98 ),
99 "*" | "/" => n.args.first().is_some_and(starts_with_literal_power),
100 _ => false,
101 }
102}
103
104fn get_precedence(op: &str) -> i32 {
106 for (operator, prec) in PRECEDENCE {
107 if operator == &op {
108 return *prec;
109 }
110 }
111 0 }
113
114const GREEK_TABLE: &[(&str, char, bool)] = &[
124 ("alpha", 'α', false),
125 ("beta", 'β', false),
126 ("gamma", 'γ', true),
127 ("delta", 'δ', true),
128 ("epsilon", 'ε', false),
129 ("zeta", 'ζ', false),
130 ("eta", 'η', false),
131 ("theta", 'θ', true),
132 ("iota", 'ι', false),
133 ("kappa", 'κ', false),
134 ("lambda", 'λ', true),
135 ("mu", 'μ', false),
136 ("nu", 'ν', false),
137 ("xi", 'ξ', true),
138 ("omicron", 'ο', false),
139 ("pi", 'π', true),
140 ("rho", 'ρ', false),
141 ("sigma", 'σ', true),
142 ("tau", 'τ', false),
143 ("upsilon", 'υ', true),
144 ("phi", 'φ', true),
145 ("chi", 'χ', false),
146 ("psi", 'ψ', true),
147 ("omega", 'ω', true),
148];
149
150fn greek_name_to_char(name: &str) -> Option<char> {
153 GREEK_TABLE
154 .iter()
155 .find(|(n, _, _)| *n == name)
156 .map(|&(_, c, _)| c)
157}
158
159fn greek_char_to_name(c: char) -> Option<&'static str> {
162 GREEK_TABLE
163 .iter()
164 .find(|&&(_, ch, _)| ch == c)
165 .map(|&(n, _, _)| n)
166}
167
168fn format_chemical_subscripts(s: &str) -> String {
170 if let Some(c) = greek_name_to_char(s) {
172 return c.to_string();
173 }
174
175 let (body, charge) = split_charge(s);
178
179 let mut result = String::new();
180 let chars: Vec<char> = body.chars().collect();
181 let mut i = 0;
182
183 while i < chars.len() {
184 let ch = chars[i];
185
186 if ch.is_alphabetic() {
187 if i + 1 < chars.len() {
189 let two_letter = format!("{}{}", ch, chars[i + 1]);
190 if ELEMENTS.contains(&two_letter.as_str()) {
191 result.push_str(&two_letter);
192 i += 2;
193
194 while i < chars.len() && chars[i].is_ascii_digit() {
196 let digit = chars[i].to_digit(10).unwrap();
197 result.push(UNICODE_SUBSCRIPTS[digit as usize]);
198 i += 1;
199 }
200 continue;
201 }
202 }
203
204 if ELEMENTS.contains(&ch.to_string().as_str()) {
206 result.push(ch);
207 i += 1;
208
209 while i < chars.len() && chars[i].is_ascii_digit() {
211 let digit = chars[i].to_digit(10).unwrap();
212 result.push(UNICODE_SUBSCRIPTS[digit as usize]);
213 i += 1;
214 }
215 continue;
216 }
217
218 result.push(ch);
220 i += 1;
221 } else {
222 result.push(ch);
225 i += 1;
226 if ch == ')' || ch == ']' {
227 while i < chars.len() && chars[i].is_ascii_digit() {
228 let digit = chars[i].to_digit(10).unwrap();
229 result.push(UNICODE_SUBSCRIPTS[digit as usize]);
230 i += 1;
231 }
232 }
233 }
234 }
235
236 result.push_str(&to_superscript_str(&charge));
237 result
238}
239
240fn split_charge(s: &str) -> (&str, String) {
247 let last = match s.chars().last() {
248 Some(c) => c,
249 None => return (s, String::new()),
250 };
251 if last == '+' || last == '-' {
253 let without_sign = &s[..s.len() - 1];
254 let mag: String = without_sign
255 .chars()
256 .rev()
257 .take_while(|c| c.is_ascii_digit())
258 .collect::<Vec<_>>()
259 .into_iter()
260 .rev()
261 .collect();
262 let body = &without_sign[..without_sign.len() - mag.len()];
263 if !mag.is_empty() && is_chemical_formula(body) {
264 return (body, format!("{mag}{last}"));
265 }
266 }
267 if last.is_ascii_digit() {
269 let mag: String = s
270 .chars()
271 .rev()
272 .take_while(|c| c.is_ascii_digit())
273 .collect::<Vec<_>>()
274 .into_iter()
275 .rev()
276 .collect();
277 let before = &s[..s.len() - mag.len()];
278 if let Some(sign) = before.chars().last()
279 && (sign == '+' || sign == '-')
280 {
281 let body = &before[..before.len() - 1];
282 if is_chemical_formula(body) {
283 return (body, format!("{mag}{sign}"));
284 }
285 }
286 }
287 (s, String::new())
288}
289
290enum FloatParts {
298 Plain(String),
300 Scientific { mantissa: String, exponent: String },
304 Inf(bool),
306 NaN,
308}
309
310const SCI_NOTATION_MIN: f64 = 0.01;
314const SCI_NOTATION_MAX: f64 = 10000.0;
315
316fn format_display_float(n: f64) -> FloatParts {
320 if n.is_nan() {
321 return FloatParts::NaN;
322 }
323 if n.is_infinite() {
324 return FloatParts::Inf(n < 0.0);
325 }
326 if n == 0.0 {
328 return FloatParts::Plain("0".to_string());
329 }
330
331 let abs_n = n.abs();
332 if !(SCI_NOTATION_MIN..SCI_NOTATION_MAX).contains(&abs_n) {
333 let sci = shortest_sci(n);
338 if let Some(e_pos) = sci.find('e') {
339 let mut mantissa = sci[..e_pos].to_string();
340 if !mantissa.contains('.') {
342 mantissa.push_str(".0");
343 }
344 return FloatParts::Scientific {
345 mantissa,
346 exponent: sci[e_pos + 1..].to_string(),
347 };
348 }
349 return FloatParts::Plain(sci);
350 }
351
352 if n.fract() == 0.0 {
354 return FloatParts::Plain(format!("{}", n as i64));
355 }
356
357 let (digits, exp) = shortest_digit_count(n);
361 let frac = usize::try_from(digits as i64 - 1 - exp).unwrap_or(0);
362 FloatParts::Plain(format!("{n:.frac$}"))
363}
364
365fn shortest_digit_count(n: f64) -> (usize, i64) {
368 let s = format!("{n:e}");
369 let e_pos = s.find('e').expect("`{:e}` always has an exponent");
370 let digits = s[..e_pos].trim_start_matches('-').replace('.', "").len();
371 let exp = s[e_pos + 1..]
372 .parse()
373 .expect("`{:e}` exponent is an integer");
374 (digits, exp)
375}
376
377fn shortest_sci(n: f64) -> String {
387 let (digits, _) = shortest_digit_count(n);
388 let prec = digits - 1;
389 format!("{n:.prec$e}")
390}
391
392fn unicode_minus(s: String) -> String {
395 match s.strip_prefix('-') {
396 Some(rest) => format!("−{rest}"),
397 None => s,
398 }
399}
400
401fn format_number_unicode(n: f64) -> String {
403 match format_display_float(n) {
404 FloatParts::Plain(s) => unicode_minus(s),
405 FloatParts::Scientific { mantissa, exponent } => {
406 format!(
407 "{}×10{}",
408 unicode_minus(mantissa),
409 to_superscript_str(&exponent)
410 )
411 }
412 FloatParts::Inf(neg) => if neg { "−∞" } else { "∞" }.to_string(),
413 FloatParts::NaN => "NaN".to_string(),
414 }
415}
416
417fn format_number_latex(n: f64) -> String {
419 match format_display_float(n) {
420 FloatParts::Plain(s) => s,
421 FloatParts::Scientific { mantissa, exponent } => {
422 format!("{mantissa} \\times 10^{{{exponent}}}")
423 }
424 FloatParts::Inf(neg) => if neg { "-\\infty" } else { "\\infty" }.to_string(),
425 FloatParts::NaN => "\\text{NaN}".to_string(),
426 }
427}
428
429fn format_number_ascii(n: f64) -> String {
431 match format_display_float(n) {
432 FloatParts::Plain(s) => s,
433 FloatParts::Scientific { mantissa, exponent } => format!("{mantissa}e{exponent}"),
434 FloatParts::Inf(neg) => if neg { "-inf" } else { "inf" }.to_string(),
435 FloatParts::NaN => "NaN".to_string(),
436 }
437}
438
439fn nonfinite_string(name: &str, fmt: Fmt) -> Option<String> {
444 let n = match name {
445 "Infinity" => f64::INFINITY,
446 "-Infinity" => f64::NEG_INFINITY,
447 "NaN" => f64::NAN,
448 _ => return None,
449 };
450 Some(match fmt {
451 Fmt::Unicode => format_number_unicode(n),
452 Fmt::Latex => format_number_latex(n),
453 Fmt::Ascii => format_number_ascii(n),
454 })
455}
456
457impl fmt::Display for Expr {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 write!(f, "{}", self.to_unicode())
460 }
461}
462
463impl Expr {
464 pub fn to_unicode(&self) -> String {
466 self.to_unicode_with_precedence(0)
467 }
468
469 fn to_unicode_with_precedence(&self, parent_prec: i32) -> String {
470 match self {
471 Expr::Number(n) => format_number_unicode(*n),
474 Expr::Integer(n) => format_number_unicode(*n as f64),
475 Expr::Variable(name) => nonfinite_string(name, Fmt::Unicode)
476 .unwrap_or_else(|| format_chemical_subscripts(name)),
477 Expr::Operator(node) => format_operator(node, Fmt::Unicode, parent_prec),
478 }
479 }
480}
481
482fn call_form(op_symbol: &str, args: &[Expr], render: impl Fn(&Expr) -> String) -> String {
489 format!(
490 "{}({})",
491 op_symbol,
492 args.iter().map(render).collect::<Vec<_>>().join(", ")
493 )
494}
495
496fn pick(fmt: Fmt, unicode: &'static str, latex: &'static str, ascii: &'static str) -> &'static str {
500 match fmt {
501 Fmt::Unicode => unicode,
502 Fmt::Latex => latex,
503 Fmt::Ascii => ascii,
504 }
505}
506
507fn comparison_or_call(
512 op: &str,
513 op_prec: i32,
514 args: &[Expr],
515 infix: &str,
516 call: &str,
517 fmt: Fmt,
518) -> String {
519 if args.len() == 2 {
520 format!(
521 "{} {} {}",
522 render_operand(op, op_prec, &args[0], false, fmt),
523 infix,
524 render_operand(op, op_prec, &args[1], true, fmt)
525 )
526 } else {
527 call_form(call, args, |a| render_at(a, fmt, 0))
528 }
529}
530
531fn infix_chain(op: &str, op_prec: i32, args: &[Expr], sep: &str, fmt: Fmt) -> String {
534 args.iter()
535 .enumerate()
536 .map(|(i, a)| render_operand(op, op_prec, a, i > 0, fmt))
537 .collect::<Vec<_>>()
538 .join(sep)
539}
540
541#[derive(Clone, Copy, PartialEq, Eq)]
545enum Fmt {
546 Unicode,
547 Latex,
548 Ascii,
549}
550
551fn render_fmt(expr: &Expr, fmt: Fmt) -> String {
553 match fmt {
554 Fmt::Unicode => to_unicode(expr),
555 Fmt::Latex => to_latex(expr),
556 Fmt::Ascii => to_ascii(expr),
557 }
558}
559
560fn render_operator(node: &ExpressionNode, fmt: Fmt) -> String {
563 format_operator(node, fmt, 0)
564}
565
566fn latex_name(name: &str) -> String {
568 name.replace('_', "\\_")
569}
570
571fn integer_exponent(e: &Expr) -> Option<i64> {
574 match e {
575 Expr::Integer(n) => Some(*n),
576 Expr::Number(n) if n.fract() == 0.0 => Some(*n as i64),
577 _ => None,
578 }
579}
580
581fn to_superscript_str(s: &str) -> String {
583 s.chars()
584 .map(|c| match c {
585 '0'..='9' => UNICODE_SUPERSCRIPTS[c.to_digit(10).unwrap() as usize],
586 '-' => '⁻',
587 '+' => '⁺',
588 other => other,
589 })
590 .collect()
591}
592
593fn greek_to_ascii(s: &str) -> String {
596 let mut out = String::new();
597 for c in s.chars() {
598 match greek_char_to_name(c) {
599 Some(name) => out.push_str(name),
600 None => out.push(c),
601 }
602 }
603 out
604}
605
606fn greek_to_latex(name: &str) -> Option<String> {
612 for &(n, c, capital_has_latex) in GREEK_TABLE {
613 if name == n || name.chars().eq(std::iter::once(c)) {
615 return Some(format!("\\{n}"));
616 }
617 let initial = n.chars().next().expect("table names are non-empty");
619 if capital_has_latex && name.strip_prefix(initial.to_ascii_uppercase()) == Some(&n[1..]) {
620 return Some(format!("\\{name}"));
621 }
622 }
623 None
624}
625
626fn expr_raw_string(e: &Expr) -> String {
629 match e {
630 Expr::Variable(s) => s.clone(),
631 Expr::Integer(n) => n.to_string(),
632 Expr::Number(n) => n.to_string(),
633 Expr::Operator(node) => node.op.clone(),
634 }
635}
636
637fn wrap_if_op(expr: &Expr, fmt: Fmt) -> String {
640 let s = render_fmt(expr, fmt);
641 if matches!(expr, Expr::Operator(_)) {
642 format!("({s})")
643 } else {
644 s
645 }
646}
647
648fn render_at(expr: &Expr, fmt: Fmt, prec: i32) -> String {
650 match fmt {
651 Fmt::Unicode => expr.to_unicode_with_precedence(prec),
652 Fmt::Latex => to_latex_prec(expr, prec),
653 Fmt::Ascii => to_ascii_prec(expr, prec),
654 }
655}
656
657fn sum_as_difference(args: &[Expr], op_prec: i32, fmt: Fmt) -> Option<String> {
660 if args.len() != 2 {
661 return None;
662 }
663 let Expr::Operator(n) = &args[1] else {
664 return None;
665 };
666 if n.op != "-" || n.args.len() != 1 {
667 return None;
668 }
669 let minus = if fmt == Fmt::Unicode { "−" } else { "-" };
670 Some(format!(
671 "{} {minus} {}",
672 render_at(&args[0], fmt, op_prec - 1),
673 render_at(&n.args[0], fmt, op_prec)
674 ))
675}
676
677fn latex_mul_juxtapose(args: &[Expr]) -> bool {
682 args.iter()
683 .any(|a| matches!(a, Expr::Variable(v) if v.contains('\\')))
684}
685
686fn format_region_bound(bound: &crate::types::RegionBound, fmt: Fmt) -> String {
691 match bound {
692 crate::types::RegionBound::Int(i) => i.to_string(),
693 crate::types::RegionBound::Expr(Expr::Integer(i)) => i.to_string(),
694 crate::types::RegionBound::Expr(e) => render_fmt(e, fmt),
695 }
696}
697
698fn format_const_value(value: &serde_json::Value, fmt: Fmt) -> String {
702 match value {
703 serde_json::Value::Array(arr) => {
704 let inner = arr
705 .iter()
706 .map(|v| format_const_value(v, fmt))
707 .collect::<Vec<_>>()
708 .join(", ");
709 format!("[{inner}]")
710 }
711 serde_json::Value::Number(num) => {
712 if let Some(i) = num.as_i64() {
713 i.to_string()
714 } else if let Some(u) = num.as_u64() {
715 u.to_string()
716 } else if let Some(f) = num.as_f64() {
717 format_number_fmt(f, fmt)
718 } else {
719 num.to_string()
720 }
721 }
722 serde_json::Value::String(s) => s.clone(),
723 serde_json::Value::Bool(b) => b.to_string(),
724 other => other.to_string(),
725 }
726}
727
728fn json_scalar_string(v: &serde_json::Value) -> String {
730 match v {
731 serde_json::Value::String(s) => s.clone(),
732 serde_json::Value::Number(n) => n.to_string(),
733 serde_json::Value::Bool(b) => b.to_string(),
734 other => other.to_string(),
735 }
736}
737
738fn aggregate_symbol(semiring: Option<&str>, reduce: &str, fmt: Fmt) -> &'static str {
741 enum Fam {
742 Plus,
743 Times,
744 Max,
745 Min,
746 Bool,
747 }
748 let fam = if let Some(sr) = semiring {
749 match sr {
750 "max_product" | "max_sum" => Fam::Max,
751 "min_sum" => Fam::Min,
752 "bool_and_or" => Fam::Bool,
753 _ => Fam::Plus,
754 }
755 } else {
756 match reduce {
757 "*" => Fam::Times,
758 "max" => Fam::Max,
759 "min" => Fam::Min,
760 _ => Fam::Plus,
761 }
762 };
763 let (u, l, a) = match fam {
764 Fam::Plus => ("Σ", "\\sum", "sum"),
765 Fam::Times => ("Π", "\\prod", "prod"),
766 Fam::Max => ("max", "\\max", "max"),
767 Fam::Min => ("min", "\\min", "min"),
768 Fam::Bool => ("⋁", "\\bigvee", "any"),
769 };
770 match fmt {
771 Fmt::Unicode => u,
772 Fmt::Latex => l,
773 Fmt::Ascii => a,
774 }
775}
776
777fn format_range_spec(rng: &RangeSpec) -> String {
779 match rng {
780 RangeSpec::Interval(iv) => format!("{}:{}", iv[0], iv[1]),
781 RangeSpec::Strided(iv) => format!("{}:{}:{}", iv[0], iv[1], iv[2]),
782 RangeSpec::IndexSetRef { from, of } => match of {
783 Some(of) if !of.is_empty() => format!("{}({})", from, of.join(", ")),
784 _ => from.clone(),
785 },
786 RangeSpec::RaggedDyn { offsets, of } => format!("{}({})", offsets, of.join(", ")),
787 RangeSpec::DerivedDyn { from_faq } => from_faq.clone(),
788 }
789}
790
791fn format_ranges_clause(ranges: &std::collections::HashMap<String, RangeSpec>, fmt: Fmt) -> String {
794 let in_sym = match fmt {
795 Fmt::Latex => " \\in ",
796 Fmt::Unicode => "∈",
797 Fmt::Ascii => " in ",
798 };
799 let mut keys: Vec<&String> = ranges.keys().collect();
800 keys.sort();
801 let parts = keys
802 .iter()
803 .map(|k| format!("{}{}{}", k, in_sym, format_range_spec(&ranges[*k])))
804 .collect::<Vec<_>>()
805 .join(", ");
806 match fmt {
807 Fmt::Latex => format!(" \\text{{ where }} \\{{{parts}\\}}"),
808 _ => format!(" where {{{parts}}}"),
809 }
810}
811
812fn format_join_clause(join: Option<&[JoinClause]>, fmt: Fmt) -> String {
819 let Some(join) = join.filter(|j| !j.is_empty()) else {
820 return String::new();
821 };
822 let clauses = join
823 .iter()
824 .map(|c| {
825 if let Some(ov) = &c.overlap {
826 let eps = match ov.eps {
827 Some(e) => format!(", eps={}", format_number_fmt(e, fmt)),
828 None => String::new(),
829 };
830 return format!(
831 "overlap(src=[{}], tgt=[{}]{eps})",
832 ov.src_env.join(", "),
833 ov.tgt_env.join(", ")
834 );
835 }
836 let mut parts: Vec<String> =
837 c.on.iter().map(|p| format!("{}={}", p[0], p[1])).collect();
838 if let Some(syms) = &c.syms {
839 parts.push(format!("syms=[{}]", syms.join(", ")));
840 }
841 parts.join(", ")
842 })
843 .collect::<Vec<_>>()
844 .join("; ");
845 format!(" join({clauses})")
846}
847
848fn format_number_fmt(n: f64, fmt: Fmt) -> String {
850 match fmt {
851 Fmt::Unicode => format_number_unicode(n),
852 Fmt::Latex => format_number_latex(n),
853 Fmt::Ascii => format_number_ascii(n),
854 }
855}
856
857fn format_aggregate(node: &ExpressionNode, fmt: Fmt) -> String {
859 let out_idx = node
860 .output_idx
861 .as_ref()
862 .map(|v| v.join(", "))
863 .unwrap_or_default();
864 let expr_str = node
865 .expr
866 .as_deref()
867 .map(|e| render_fmt(e, fmt))
868 .unwrap_or_default();
869 let semiring = node.semiring.as_deref();
870 let reduce = node.reduce.as_deref().unwrap_or("+");
871 let sym = aggregate_symbol(semiring, reduce, fmt);
872 let idx_part = if fmt == Fmt::Latex {
873 format!("_{{{out_idx}}}")
874 } else {
875 format!("[{out_idx}]")
876 };
877 let mut out = format!("{sym}{idx_part} ({expr_str})");
878 if let Some(ranges) = &node.ranges
879 && !ranges.is_empty()
880 {
881 out.push_str(&format_ranges_clause(ranges, fmt));
882 }
883 out.push_str(&format_join_clause(node.join.as_deref(), fmt));
884 if let Some(filter) = node.filter.as_deref() {
885 out.push_str(&format!(" if {}", render_fmt(filter, fmt)));
886 }
887 if node.distinct == Some(true) {
888 out.push_str(" distinct");
889 }
890 if let Some(key) = node.key.as_deref() {
891 out.push_str(&format!(" key={}", render_fmt(key, fmt)));
892 }
893 if let Some(id) = node.id.as_deref() {
896 out.push_str(&format!(" id={id}"));
897 }
898 if let Some(sr) = semiring
899 && sr != "sum_product"
900 {
901 out.push_str(&format!(" [semiring={sr}]"));
902 }
903 out
904}
905
906fn format_arg_witness(node: &ExpressionNode, fmt: Fmt) -> String {
908 let arg = node.arg.as_deref().unwrap_or("");
909 let expr_str = node
910 .expr
911 .as_deref()
912 .map(|e| render_fmt(e, fmt))
913 .unwrap_or_default();
914 let idx_part = if fmt == Fmt::Latex {
915 format!("_{{{arg}}}")
916 } else {
917 format!("[{arg}]")
918 };
919 let name = if fmt == Fmt::Latex {
920 format!("\\mathrm{{{}}}", node.op)
921 } else {
922 node.op.clone()
923 };
924 let mut out = format!("{name}{idx_part} ({expr_str})");
925 if let Some(ranges) = &node.ranges
926 && !ranges.is_empty()
927 {
928 out.push_str(&format_ranges_clause(ranges, fmt));
929 }
930 out.push_str(&format_join_clause(node.join.as_deref(), fmt));
931 if let Some(filter) = node.filter.as_deref() {
932 out.push_str(&format!(" if {}", render_fmt(filter, fmt)));
933 }
934 if let Some(id) = node.id.as_deref() {
935 out.push_str(&format!(" id={id}"));
936 }
937 out
938}
939
940fn format_structural_op(node: &ExpressionNode, fmt: Fmt) -> Option<String> {
947 let op = node.op.as_str();
948 let args = node.args.as_slice();
949 let eq = if fmt == Fmt::Latex { " = " } else { "=" };
950
951 match op {
952 "const" => Some(format_const_value(
953 node.value.as_ref().unwrap_or(&serde_json::Value::Null),
954 fmt,
955 )),
956
957 "true" | "false" => Some(op.to_string()),
958
959 "fn" => {
960 let name = node.name.as_deref().unwrap_or("");
961 let inner = args
962 .iter()
963 .map(|a| render_fmt(a, fmt))
964 .collect::<Vec<_>>()
965 .join(", ");
966 Some(match fmt {
967 Fmt::Latex => format!("\\mathrm{{{}}}({})", latex_name(name), inner),
968 _ => format!("{name}({inner})"),
969 })
970 }
971
972 "enum" => {
973 let a0 = args.first().map(expr_raw_string).unwrap_or_default();
974 let a1 = args.get(1).map(expr_raw_string).unwrap_or_default();
975 let label = format!("{a0}.{a1}");
976 Some(match fmt {
977 Fmt::Latex => format!("\\mathrm{{{}}}", latex_name(&label)),
978 _ => label,
979 })
980 }
981
982 "index" => {
983 if args.is_empty() {
984 return None;
985 }
986 let idx = args[1..]
987 .iter()
988 .map(|a| render_fmt(a, fmt))
989 .collect::<Vec<_>>()
990 .join(", ");
991 Some(format!("{}[{}]", wrap_if_op(&args[0], fmt), idx))
992 }
993
994 "broadcast" => {
995 let fn_name = node.broadcast_fn.as_deref()?;
996 let synth = ExpressionNode {
997 op: fn_name.to_string(),
998 args: node.args.clone(),
999 ..Default::default()
1000 };
1001 Some(render_operator(&synth, fmt))
1002 }
1003
1004 "integral" => {
1005 if args.is_empty() {
1006 return None;
1007 }
1008 let f = render_fmt(&args[0], fmt);
1009 let v = node.int_var.as_deref().unwrap_or("x");
1010 let lo = node
1011 .lower
1012 .as_deref()
1013 .map(|e| render_fmt(e, fmt))
1014 .unwrap_or_default();
1015 let hi = node
1016 .upper
1017 .as_deref()
1018 .map(|e| render_fmt(e, fmt))
1019 .unwrap_or_default();
1020 Some(match fmt {
1021 Fmt::Latex => format!("\\int_{{{lo}}}^{{{hi}}} {f} \\, d{v}"),
1022 Fmt::Unicode => format!("∫[{lo}, {hi}] {f} d{v}"),
1023 Fmt::Ascii => format!("integral({f}, {v}, {lo}, {hi})"),
1024 })
1025 }
1026
1027 "table_lookup" => {
1028 let table = node.table.as_deref().unwrap_or("");
1029 let bindings = node
1030 .axes
1031 .as_ref()
1032 .map(|axes| {
1033 let mut keys: Vec<&String> = axes.keys().collect();
1034 keys.sort();
1035 keys.iter()
1036 .map(|k| format!("{}{}{}", k, eq, render_fmt(&axes[*k], fmt)))
1037 .collect::<Vec<_>>()
1038 .join(", ")
1039 })
1040 .unwrap_or_default();
1041 let out_str = match &node.output {
1042 Some(v) if !v.is_null() => format!(":{}", json_scalar_string(v)),
1043 _ => String::new(),
1044 };
1045 let name = if fmt == Fmt::Latex {
1046 format!("\\mathrm{{{}}}", latex_name(table))
1047 } else {
1048 table.to_string()
1049 };
1050 Some(format!("{name}[{bindings}]{out_str}"))
1051 }
1052
1053 "apply_expression_template" => {
1054 let name = node.name.as_deref().unwrap_or("");
1055 let inner = node
1056 .bindings
1057 .as_ref()
1058 .map(|b| {
1059 let mut keys: Vec<&String> = b.keys().collect();
1060 keys.sort();
1061 keys.iter()
1062 .map(|k| format!("{}{}{}", k, eq, render_fmt(&b[*k], fmt)))
1063 .collect::<Vec<_>>()
1064 .join(", ")
1065 })
1066 .unwrap_or_default();
1067 Some(match fmt {
1068 Fmt::Latex => format!(
1069 "\\mathrm{{{}}}\\langle {} \\rangle",
1070 latex_name(name),
1071 inner
1072 ),
1073 Fmt::Unicode => format!("{name}⟨{inner}⟩"),
1074 Fmt::Ascii => format!("{name}<{inner}>"),
1075 })
1076 }
1077
1078 "makearray" => {
1079 let values = node.values.as_ref();
1080 let parts = node
1081 .regions
1082 .as_ref()
1083 .map(|regions| {
1084 regions
1085 .iter()
1086 .enumerate()
1087 .map(|(i, region)| {
1088 let reg_str = region
1089 .iter()
1090 .map(|dim| {
1091 format!(
1092 "{}:{}",
1093 format_region_bound(&dim[0], fmt),
1094 format_region_bound(&dim[1], fmt)
1095 )
1096 })
1097 .collect::<Vec<_>>()
1098 .join(", ");
1099 let val = values
1100 .and_then(|vs| vs.get(i))
1101 .map(|v| render_fmt(v, fmt))
1102 .unwrap_or_else(|| "?".to_string());
1103 format!("[{reg_str}] = {val}")
1104 })
1105 .collect::<Vec<_>>()
1106 .join(", ")
1107 })
1108 .unwrap_or_default();
1109 let name = if fmt == Fmt::Latex {
1110 "\\mathrm{makearray}"
1111 } else {
1112 "makearray"
1113 };
1114 Some(format!("{name}({parts})"))
1115 }
1116
1117 "reshape" => {
1118 if args.is_empty() {
1119 return None;
1120 }
1121 let shape = node
1122 .shape
1123 .as_ref()
1124 .map(|s| {
1125 s.iter()
1126 .map(|x| x.to_string())
1127 .collect::<Vec<_>>()
1128 .join(", ")
1129 })
1130 .unwrap_or_default();
1131 let name = if fmt == Fmt::Latex {
1132 "\\mathrm{reshape}"
1133 } else {
1134 "reshape"
1135 };
1136 Some(format!(
1137 "{}({}, [{}])",
1138 name,
1139 render_fmt(&args[0], fmt),
1140 shape
1141 ))
1142 }
1143
1144 "transpose" => {
1145 if args.is_empty() {
1146 return None;
1147 }
1148 if let Some(perm) = &node.perm
1149 && !perm.is_empty()
1150 {
1151 let p = perm
1152 .iter()
1153 .map(|x| x.to_string())
1154 .collect::<Vec<_>>()
1155 .join(", ");
1156 let name = if fmt == Fmt::Latex {
1157 "\\mathrm{transpose}"
1158 } else {
1159 "transpose"
1160 };
1161 return Some(format!("{}({}, [{}])", name, render_fmt(&args[0], fmt), p));
1162 }
1163 let a = wrap_if_op(&args[0], fmt);
1164 Some(match fmt {
1165 Fmt::Latex => format!("{a}^{{T}}"),
1166 Fmt::Unicode => format!("{a}ᵀ"),
1167 Fmt::Ascii => format!("transpose({})", render_fmt(&args[0], fmt)),
1168 })
1169 }
1170
1171 "concat" => {
1172 let inner = args
1173 .iter()
1174 .map(|a| render_fmt(a, fmt))
1175 .collect::<Vec<_>>()
1176 .join(", ");
1177 let name = if fmt == Fmt::Latex {
1178 "\\mathrm{concat}"
1179 } else {
1180 "concat"
1181 };
1182 let axis = node.axis.unwrap_or(0);
1183 Some(format!("{name}({inner}, axis={axis})"))
1184 }
1185
1186 "intersect_polygon" | "polygon_intersection_area" => {
1187 let inner = args
1188 .iter()
1189 .map(|a| render_fmt(a, fmt))
1190 .collect::<Vec<_>>()
1191 .join(", ");
1192 let name = if fmt == Fmt::Latex {
1193 format!("\\mathrm{{{}}}", latex_name(op))
1194 } else {
1195 op.to_string()
1196 };
1197 let manifold = node.manifold.as_deref().unwrap_or("");
1198 let id_part = match node.id.as_deref() {
1203 Some(id) => format!(", id={id}"),
1204 None => String::new(),
1205 };
1206 Some(format!("{name}({inner}, manifold={manifold}{id_part})"))
1207 }
1208
1209 "skolem" => {
1213 let label = node.label.as_deref()?;
1214 let inner = args
1215 .iter()
1216 .map(|a| render_fmt(a, fmt))
1217 .collect::<Vec<_>>()
1218 .join(", ");
1219 let name = if fmt == Fmt::Latex {
1220 "\\mathrm{skolem}"
1221 } else {
1222 "skolem"
1223 };
1224 Some(format!("{name}({inner}, label={label})"))
1225 }
1226
1227 "faq" => Some(format_aggregate(node, fmt)),
1228
1229 "argmin" | "argmax" => Some(format_arg_witness(node, fmt)),
1230
1231 _ => None,
1232 }
1233}
1234
1235fn format_operator(node: &ExpressionNode, fmt: Fmt, parent_prec: i32) -> String {
1243 if let Some(s) = format_structural_op(node, fmt) {
1246 return s;
1247 }
1248
1249 let op = node.op.as_str();
1250 let args = node.args.as_slice();
1251 let wrt = &node.wrt;
1252 let op_prec = get_precedence(op);
1253 let needs_parens = op_prec > 0 && op_prec <= parent_prec;
1254 let r0 = |arg: &Expr| render_at(arg, fmt, 0);
1256
1257 if fmt == Fmt::Latex && op == "/" && args.len() == 2 {
1263 let frac = format!("\\frac{{{}}}{{{}}}", r0(&args[0]), r0(&args[1]));
1264 return if needs_parens {
1265 format!("({frac})")
1266 } else {
1267 frac
1268 };
1269 }
1270
1271 let result = match op {
1272 "+" => {
1273 if let Some(s) = sum_as_difference(args, op_prec, fmt) {
1274 s
1275 } else if args.len() >= 2 {
1276 infix_chain(op, op_prec, args, " + ", fmt)
1277 } else {
1278 call_form("+", args, r0)
1279 }
1280 }
1281 "-" => {
1282 let minus = pick(fmt, "−", "-", "-");
1283 if args.len() == 1 {
1284 let operand = render_at(&args[0], fmt, UMINUS_OPERAND_PARENT_PREC);
1290 if starts_with_literal_power(&args[0]) {
1291 format!("{minus}({operand})")
1292 } else {
1293 format!("{minus}{operand}")
1294 }
1295 } else if args.len() == 2 {
1296 format!(
1304 "{} {minus} {}",
1305 render_at(&args[0], fmt, op_prec - 1),
1306 render_at(&args[1], fmt, op_prec)
1307 )
1308 } else {
1309 call_form(minus, args, r0)
1310 }
1311 }
1312 "*" => {
1313 if args.len() >= 2 {
1314 let sep: &str = match fmt {
1317 Fmt::Unicode => "·",
1318 Fmt::Ascii => " * ",
1319 Fmt::Latex if latex_mul_juxtapose(args) => " ",
1320 Fmt::Latex => " \\cdot ",
1321 };
1322 infix_chain(op, op_prec, args, sep, fmt)
1323 } else {
1324 call_form(pick(fmt, "·", "\\cdot", "*"), args, r0)
1325 }
1326 }
1327 "/" => {
1328 if args.len() == 2 {
1330 format!(
1334 "{}{}{}",
1335 render_at(&args[0], fmt, op_prec - 1),
1336 pick(fmt, "/", "/", " / "),
1337 render_at(&args[1], fmt, op_prec)
1338 )
1339 } else {
1340 call_form(pick(fmt, "÷", "\\div", "/"), args, r0)
1341 }
1342 }
1343 "^" => {
1344 if args.len() == 2 {
1345 match fmt {
1346 Fmt::Unicode => match integer_exponent(&args[1]) {
1348 Some(n) => format!(
1349 "{}{}",
1350 render_at(&args[0], fmt, op_prec),
1351 to_superscript_str(&n.to_string())
1352 ),
1353 None => format!(
1354 "{}^{}",
1355 render_at(&args[0], fmt, op_prec),
1356 render_at(&args[1], fmt, op_prec)
1357 ),
1358 },
1359 Fmt::Latex => {
1362 format!("{}^{{{}}}", render_at(&args[0], fmt, op_prec), r0(&args[1]))
1363 }
1364 Fmt::Ascii => format!(
1365 "{}^{}",
1366 render_at(&args[0], fmt, op_prec),
1367 render_at(&args[1], fmt, op_prec)
1368 ),
1369 }
1370 } else {
1371 call_form("^", args, r0)
1372 }
1373 }
1374 "D" => {
1375 let wrt_default = wrt
1383 .as_deref()
1384 .unwrap_or(crate::op_registry::STRUCTURAL_DERIVATIVE_WRT);
1385 if let [arg] = args {
1386 let wrt_var = wrt_default;
1387 match fmt {
1388 Fmt::Unicode => format!(
1389 "∂{}/∂{}",
1390 wrap_if_op(arg, fmt),
1391 format_chemical_subscripts(wrt_var)
1392 ),
1393 Fmt::Latex => format!(
1394 "\\frac{{\\partial {}}}{{\\partial {}}}",
1395 wrap_if_op(arg, fmt),
1396 wrt_var
1397 ),
1398 Fmt::Ascii => format!("D({})/D{wrt_var}", r0(arg)),
1400 }
1401 } else {
1402 call_form("D", args, r0)
1403 }
1404 }
1405 ">" => comparison_or_call(op, op_prec, args, ">", ">", fmt),
1406 "<" => comparison_or_call(op, op_prec, args, "<", "<", fmt),
1407 ">=" => comparison_or_call(
1408 op,
1409 op_prec,
1410 args,
1411 pick(fmt, "≥", "\\geq", ">="),
1412 pick(fmt, ">=", "\\geq", ">="),
1413 fmt,
1414 ),
1415 "<=" => comparison_or_call(
1416 op,
1417 op_prec,
1418 args,
1419 pick(fmt, "≤", "\\leq", "<="),
1420 pick(fmt, "<=", "\\leq", "<="),
1421 fmt,
1422 ),
1423 "=" | "==" => comparison_or_call(
1424 op,
1425 op_prec,
1426 args,
1427 pick(fmt, "=", "=", "=="),
1428 pick(fmt, "=", "=", "=="),
1429 fmt,
1430 ),
1431 "!=" => comparison_or_call(
1432 op,
1433 op_prec,
1434 args,
1435 pick(fmt, "≠", "\\neq", "!="),
1436 pick(fmt, "!=", "\\neq", "!="),
1437 fmt,
1438 ),
1439 "and" => {
1440 if args.len() >= 2 {
1441 infix_chain(
1442 op,
1443 op_prec,
1444 args,
1445 pick(fmt, " ∧ ", " \\land ", " and "),
1446 fmt,
1447 )
1448 } else {
1449 call_form(pick(fmt, "and", "\\land", "and"), args, r0)
1450 }
1451 }
1452 "or" => {
1453 if args.len() >= 2 {
1454 infix_chain(op, op_prec, args, pick(fmt, " ∨ ", " \\lor ", " or "), fmt)
1455 } else {
1456 call_form(pick(fmt, "or", "\\lor", "or"), args, r0)
1457 }
1458 }
1459 "not" => {
1460 if args.len() == 1 {
1461 let prefix = pick(fmt, "¬", "\\neg ", "not ");
1463 if matches!(&args[0], Expr::Operator(_)) {
1464 format!("{prefix}({})", r0(&args[0]))
1465 } else {
1466 format!("{prefix}{}", r0(&args[0]))
1467 }
1468 } else {
1469 call_form(pick(fmt, "not", "\\neg", "not"), args, r0)
1470 }
1471 }
1472 "exp" => match fmt {
1473 Fmt::Latex => {
1475 if let [arg] = args {
1476 if matches!(arg, Expr::Operator(_)) {
1477 format!("\\exp\\left({}\\right)", r0(arg))
1478 } else {
1479 format!("\\exp({})", r0(arg))
1480 }
1481 } else {
1482 call_form("\\exp", args, r0)
1483 }
1484 }
1485 _ => call_form("exp", args, r0),
1486 },
1487 "ifelse" => match fmt {
1488 Fmt::Latex if args.len() == 3 => format!(
1489 "\\begin{{cases}} {} & \\text{{if }} {} \\\\ {} & \\text{{otherwise}} \\end{{cases}}",
1490 r0(&args[1]),
1491 r0(&args[0]),
1492 r0(&args[2])
1493 ),
1494 Fmt::Latex => call_form("\\mathrm{ifelse}", args, r0),
1495 _ => call_form("ifelse", args, r0),
1496 },
1497 "log" => {
1498 if args.len() == 1 {
1499 call_form(pick(fmt, "ln", "\\ln", "log"), args, r0)
1500 } else {
1501 call_form(pick(fmt, "log", "\\log", "log"), args, r0)
1502 }
1503 }
1504 "log10" => {
1505 let sym = if args.len() == 1 {
1506 pick(fmt, "log₁₀", "\\log_{10}", "log10")
1507 } else {
1508 pick(fmt, "log10", "\\log_{10}", "log10")
1509 };
1510 call_form(sym, args, r0)
1511 }
1512 "sqrt" => match fmt {
1513 Fmt::Unicode => {
1514 if let [arg] = args {
1515 if matches!(arg, Expr::Operator(_)) {
1517 format!("√({})", r0(arg))
1518 } else {
1519 format!("√{}", r0(arg))
1520 }
1521 } else {
1522 call_form("sqrt", args, r0)
1523 }
1524 }
1525 Fmt::Latex => format!(
1526 "\\sqrt{{{}}}",
1527 args.iter().map(&r0).collect::<Vec<_>>().join(", ")
1528 ),
1529 Fmt::Ascii => call_form("sqrt", args, r0),
1530 },
1531 "asin" => {
1532 let sym = if args.len() == 1 {
1533 pick(fmt, "arcsin", "\\arcsin", "asin")
1534 } else {
1535 pick(fmt, "asin", "\\arcsin", "asin")
1536 };
1537 call_form(sym, args, r0)
1538 }
1539 "acos" => {
1540 let sym = if args.len() == 1 {
1541 pick(fmt, "arccos", "\\arccos", "acos")
1542 } else {
1543 pick(fmt, "acos", "\\arccos", "acos")
1544 };
1545 call_form(sym, args, r0)
1546 }
1547 "atan" => {
1548 let sym = if args.len() == 1 {
1549 pick(fmt, "arctan", "\\arctan", "atan")
1550 } else {
1551 pick(fmt, "atan", "\\arctan", "atan")
1552 };
1553 call_form(sym, args, r0)
1554 }
1555 "abs" => match fmt {
1556 Fmt::Unicode => {
1557 if args.len() == 1 {
1558 format!("|{}|", r0(&args[0]))
1559 } else {
1560 call_form("abs", args, r0)
1561 }
1562 }
1563 Fmt::Latex => format!("|{}|", args.iter().map(&r0).collect::<Vec<_>>().join(", ")),
1564 Fmt::Ascii => call_form("abs", args, r0),
1565 },
1566 "sign" => call_form(pick(fmt, "sgn", "\\mathrm{sgn}", "sign"), args, r0),
1567 "floor" => match fmt {
1568 Fmt::Unicode => {
1569 if args.len() == 1 {
1570 format!("⌊{}⌋", r0(&args[0]))
1571 } else {
1572 call_form("floor", args, r0)
1573 }
1574 }
1575 Fmt::Latex => format!(
1576 "\\lfloor {} \\rfloor",
1577 args.iter().map(&r0).collect::<Vec<_>>().join(", ")
1578 ),
1579 Fmt::Ascii => call_form("floor", args, r0),
1580 },
1581 "ceil" => match fmt {
1582 Fmt::Unicode => {
1583 if args.len() == 1 {
1584 format!("⌈{}⌉", r0(&args[0]))
1585 } else {
1586 call_form("ceil", args, r0)
1587 }
1588 }
1589 Fmt::Latex => format!(
1590 "\\lceil {} \\rceil",
1591 args.iter().map(&r0).collect::<Vec<_>>().join(", ")
1592 ),
1593 Fmt::Ascii => call_form("ceil", args, r0),
1594 },
1595 "asinh" => {
1596 let sym = if args.len() == 1 {
1597 pick(fmt, "sinh⁻¹", "\\sinh^{-1}", "asinh")
1598 } else {
1599 pick(fmt, "asinh", "\\sinh^{-1}", "asinh")
1600 };
1601 call_form(sym, args, r0)
1602 }
1603 "acosh" => {
1604 let sym = if args.len() == 1 {
1605 pick(fmt, "cosh⁻¹", "\\cosh^{-1}", "acosh")
1606 } else {
1607 pick(fmt, "acosh", "\\cosh^{-1}", "acosh")
1608 };
1609 call_form(sym, args, r0)
1610 }
1611 "atanh" => {
1612 let sym = if args.len() == 1 {
1613 pick(fmt, "tanh⁻¹", "\\tanh^{-1}", "atanh")
1614 } else {
1615 pick(fmt, "atanh", "\\tanh^{-1}", "atanh")
1616 };
1617 call_form(sym, args, r0)
1618 }
1619 "min" => call_form(pick(fmt, "min", "\\min", "min"), args, r0),
1620 "max" => call_form(pick(fmt, "max", "\\max", "max"), args, r0),
1621 "atan2" => call_form(pick(fmt, "atan2", "\\mathrm{atan2}", "atan2"), args, r0),
1622 "sin" | "cos" | "tan" | "sinh" | "cosh" | "tanh" => {
1623 let latex = format!("\\{op}");
1625 call_form(
1626 if fmt == Fmt::Latex {
1627 latex.as_str()
1628 } else {
1629 op
1630 },
1631 args,
1632 r0,
1633 )
1634 }
1635 "Pre" => call_form(pick(fmt, "Pre", "\\mathrm{Pre}", "Pre"), args, r0),
1637 _ => {
1640 let latex = format!("\\mathrm{{{}}}", latex_name(op));
1641 call_form(
1642 if fmt == Fmt::Latex {
1643 latex.as_str()
1644 } else {
1645 op
1646 },
1647 args,
1648 r0,
1649 )
1650 }
1651 };
1652
1653 if needs_parens {
1654 format!("({result})")
1655 } else {
1656 result
1657 }
1658}
1659
1660pub trait TextRenderable {
1672 fn render_unicode(&self) -> String;
1674 fn render_latex(&self) -> String;
1676 fn render_ascii(&self) -> String;
1678}
1679
1680impl TextRenderable for Expr {
1681 fn render_unicode(&self) -> String {
1682 self.to_unicode()
1683 }
1684 fn render_latex(&self) -> String {
1685 to_latex_prec(self, 0)
1686 }
1687 fn render_ascii(&self) -> String {
1688 to_ascii_prec(self, 0)
1689 }
1690}
1691
1692impl TextRenderable for Equation {
1693 fn render_unicode(&self) -> String {
1694 format!(
1695 "{} = {}",
1696 self.lhs.render_unicode(),
1697 self.rhs.render_unicode()
1698 )
1699 }
1700 fn render_latex(&self) -> String {
1701 format!("{} = {}", self.lhs.render_latex(), self.rhs.render_latex())
1702 }
1703 fn render_ascii(&self) -> String {
1704 format!("{} = {}", self.lhs.render_ascii(), self.rhs.render_ascii())
1705 }
1706}
1707
1708macro_rules! summary_renderable {
1713 ($t:ty) => {
1714 impl TextRenderable for $t {
1715 fn render_unicode(&self) -> String {
1716 self.to_string()
1717 }
1718 fn render_latex(&self) -> String {
1719 self.to_string()
1720 }
1721 fn render_ascii(&self) -> String {
1722 self.to_string()
1723 }
1724 }
1725 };
1726}
1727
1728summary_renderable!(Model);
1729summary_renderable!(ReactionSystem);
1730summary_renderable!(EsmFile);
1731
1732pub fn to_unicode<T: TextRenderable + ?Sized>(target: &T) -> String {
1737 target.render_unicode()
1738}
1739
1740pub fn to_latex<T: TextRenderable + ?Sized>(target: &T) -> String {
1742 target.render_latex()
1743}
1744
1745fn to_latex_prec(expr: &Expr, parent_prec: i32) -> String {
1751 match expr {
1752 Expr::Integer(n) => format_number_latex(*n as f64),
1753 Expr::Number(n) => format_number_latex(*n),
1754 Expr::Variable(name) => {
1755 nonfinite_string(name, Fmt::Latex).unwrap_or_else(|| format_variable_latex(name))
1756 }
1757 Expr::Operator(node) => format_operator(node, Fmt::Latex, parent_prec),
1758 }
1759}
1760
1761fn format_variable_latex(name: &str) -> String {
1762 if is_preformatted_latex(name) {
1764 return name.to_string();
1765 }
1766 if let Some(cmd) = greek_to_latex(name) {
1768 return cmd;
1769 }
1770 format_chemical_latex(name)
1771}
1772
1773fn is_preformatted_latex(v: &str) -> bool {
1780 if v.starts_with("\\mathrm{") {
1781 return true;
1782 }
1783 if v.contains('\\') {
1784 return !v.contains('{');
1785 }
1786 v.contains('{') || v.contains('}')
1787}
1788
1789fn has_element_pattern(variable: &str) -> bool {
1793 let chars: Vec<char> = variable.chars().filter(|&c| c != '_').collect();
1794 let mut has_element = false;
1795 let mut i = 0;
1796 while i < chars.len() {
1797 let mut sym_len = 0;
1798 if i + 1 < chars.len() {
1799 let two: String = [chars[i], chars[i + 1]].iter().collect();
1800 if ELEMENTS.contains(&two.as_str()) {
1801 sym_len = 2;
1802 }
1803 }
1804 if sym_len == 0 && ELEMENTS.contains(&chars[i].to_string().as_str()) {
1805 sym_len = 1;
1806 }
1807 if sym_len > 0 {
1808 has_element = true;
1809 i += sym_len;
1810 while i < chars.len() && chars[i].is_ascii_digit() {
1811 i += 1;
1812 }
1813 } else {
1814 if chars[i].is_ascii_alphabetic() {
1816 return false;
1817 }
1818 i += 1;
1819 }
1820 }
1821 has_element
1822}
1823
1824fn latex_chemical_inner(formula: &str) -> String {
1827 let chars: Vec<char> = formula.chars().collect();
1828 let mut out = String::new();
1829 let mut i = 0;
1830 while i < chars.len() {
1831 if chars[i].is_ascii_digit() {
1832 let mut digits = String::new();
1833 while i < chars.len() && chars[i].is_ascii_digit() {
1834 digits.push(chars[i]);
1835 i += 1;
1836 }
1837 if digits.chars().count() == 1 {
1838 out.push('_');
1839 out.push_str(&digits);
1840 } else {
1841 out.push_str(&format!("_{{{digits}}}"));
1842 }
1843 } else {
1844 out.push(chars[i]);
1845 i += 1;
1846 }
1847 }
1848 out
1849}
1850
1851fn strip_outer_mathrm(s: &str) -> String {
1853 let inner = s.strip_prefix("\\mathrm{").unwrap_or(s);
1854 inner.strip_suffix('}').unwrap_or(inner).to_string()
1855}
1856
1857fn get_chemical_suffix(variable: &str) -> Option<(String, String)> {
1860 if variable.contains('_') {
1861 let parts: Vec<&str> = variable.split('_').collect();
1862 if parts.len() == 2 && has_element_pattern(parts[1]) && !has_element_pattern(parts[0]) {
1863 return Some((parts[0].to_string(), parts[1].to_string()));
1864 }
1865 if parts.len() == 3 {
1866 let suffix = parts[1..].join("_");
1867 if has_element_pattern(&suffix) && !has_element_pattern(parts[0]) {
1868 return Some((parts[0].to_string(), suffix));
1869 }
1870 }
1871 }
1872 let chars: Vec<char> = variable.chars().collect();
1873 for i in 1..chars.len() {
1874 let prefix: String = chars[..i].iter().collect();
1875 let suffix: String = chars[i..].iter().collect();
1876 if has_element_pattern(&suffix) && !has_element_pattern(&prefix) {
1877 return Some((prefix, suffix));
1878 }
1879 }
1880 None
1881}
1882
1883fn format_chemical_suffix_inner(variable: &str) -> String {
1886 if get_chemical_suffix(variable).is_some() {
1887 return strip_outer_mathrm(&format_chemical_latex(variable));
1888 }
1889 if ELEMENTS.contains(&variable) && !variable.chars().any(|c| c.is_ascii_digit()) {
1890 return variable.to_string();
1891 }
1892 latex_chemical_inner(variable)
1893}
1894
1895fn single_letter_digits(name: &str) -> Option<(String, String)> {
1897 let chars: Vec<char> = name.chars().collect();
1898 if chars.len() >= 2
1899 && chars[0].is_ascii_alphabetic()
1900 && chars[1..].iter().all(|c| c.is_ascii_digit())
1901 {
1902 return Some((chars[0].to_string(), chars[1..].iter().collect()));
1903 }
1904 None
1905}
1906
1907fn is_chemical_formula(s: &str) -> bool {
1908 if s.is_empty() {
1909 return false;
1910 }
1911
1912 let first_char = s.chars().next().unwrap();
1914 if !first_char.is_ascii_uppercase() {
1915 return false;
1916 }
1917
1918 if s.len() >= 2 {
1920 let two_letter = &s[..2];
1921 if ELEMENTS.contains(&two_letter) {
1922 return true;
1923 }
1924 }
1925
1926 let one_letter = &s[..1];
1927 ELEMENTS.contains(&one_letter)
1928}
1929
1930fn format_chemical_latex(variable: &str) -> String {
1933 let (charge_body, charge) = split_charge(variable);
1936 if !charge.is_empty() {
1937 return format!("{}^{{{charge}}}", format_chemical_latex(charge_body));
1938 }
1939
1940 if let Some((prefix, suffix)) = get_chemical_suffix(variable) {
1942 let prefix_multi = prefix.chars().count() > 1;
1943 if suffix.contains('_') {
1944 let segments: Vec<&str> = suffix.split('_').collect();
1945 let should_split = segments[0]
1949 .chars()
1950 .last()
1951 .is_some_and(|c| c.is_ascii_digit())
1952 || prefix_multi;
1953 if should_split {
1954 let mut result = if prefix_multi {
1955 format!("\\mathrm{{{prefix}}}")
1956 } else {
1957 prefix.clone()
1958 };
1959 for seg in &segments {
1960 if has_element_pattern(seg) {
1961 result.push_str(&format!("_{{\\mathrm{{{}}}}}", latex_chemical_inner(seg)));
1962 } else {
1963 result.push_str(&format!("_\\mathrm{{{seg}}}"));
1964 }
1965 }
1966 return result;
1967 }
1968 }
1969 let inner = format_chemical_suffix_inner(&suffix);
1970 let formatted_prefix = if prefix_multi {
1971 format!("\\mathrm{{{prefix}}}")
1972 } else {
1973 prefix
1974 };
1975 return format!("{formatted_prefix}_{{\\mathrm{{{inner}}}}}");
1976 }
1977
1978 if has_element_pattern(variable) {
1979 if ELEMENTS.contains(&variable) && !variable.chars().any(|c| c.is_ascii_digit()) {
1981 return variable.to_string();
1982 }
1983 return format!("\\mathrm{{{}}}", latex_chemical_inner(variable));
1985 }
1986
1987 if let Some((letter, digits)) = single_letter_digits(variable) {
1989 return if digits.chars().count() == 1 {
1990 format!("{letter}_{digits}")
1991 } else {
1992 format!("{letter}_{{{digits}}}")
1993 };
1994 }
1995 if variable.chars().count() == 1 {
1996 return variable.to_string();
1997 }
1998 if variable.contains('_') {
1999 let parts: Vec<&str> = variable.split('_').collect();
2000 if parts.iter().any(|p| has_element_pattern(p)) {
2001 let base = parts[0];
2002 let mut result =
2003 if base.chars().count() == 1 && base.chars().all(|c| c.is_ascii_alphabetic()) {
2004 base.to_string()
2005 } else if has_element_pattern(base) {
2006 format_chemical_latex(base)
2007 } else {
2008 format!("\\mathrm{{{base}}}")
2009 };
2010 for part in &parts[1..] {
2011 if has_element_pattern(part) {
2012 result.push_str(&format!("_{{\\mathrm{{{}}}}}", latex_chemical_inner(part)));
2013 } else {
2014 result.push_str(&format!("_\\mathrm{{{part}}}"));
2015 }
2016 }
2017 return result;
2018 }
2019 return format!("\\mathrm{{{}}}", variable.replace('_', "\\_"));
2021 }
2022 if !variable.chars().any(|c| c.is_ascii_lowercase()) {
2025 return variable.to_string();
2026 }
2027 format!("\\mathrm{{{variable}}}")
2028}
2029
2030pub fn to_ascii<T: TextRenderable + ?Sized>(target: &T) -> String {
2032 target.render_ascii()
2033}
2034
2035fn to_ascii_prec(expr: &Expr, parent_prec: i32) -> String {
2041 match expr {
2042 Expr::Integer(n) => format_number_ascii(*n as f64),
2043 Expr::Number(n) => format_number_ascii(*n),
2044 Expr::Variable(name) => {
2046 nonfinite_string(name, Fmt::Ascii).unwrap_or_else(|| greek_to_ascii(name))
2047 }
2048 Expr::Operator(node) => format_operator(node, Fmt::Ascii, parent_prec),
2049 }
2050}
2051
2052impl fmt::Display for Model {
2053 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2054 let name = self.name.as_deref().unwrap_or("Unnamed");
2055 let param_count = self
2056 .variables
2057 .iter()
2058 .filter(|(_, v)| matches!(v.var_type, VariableType::Parameter))
2059 .count();
2060 let eq_count = self.equations.len();
2061
2062 writeln!(
2063 f,
2064 " {} ({} parameters, {} equation{})",
2065 name,
2066 param_count,
2067 eq_count,
2068 if eq_count == 1 { "" } else { "s" }
2069 )?;
2070
2071 for eq in &self.equations {
2073 writeln!(f, " {} = {}", eq.lhs, eq.rhs)?;
2074 }
2075
2076 Ok(())
2077 }
2078}
2079
2080impl fmt::Display for ReactionSystem {
2081 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2082 let species_count = self.species.len();
2083
2084 let param_count = self.parameters.len();
2086
2087 let reaction_count = self.reactions.len();
2088
2089 writeln!(
2090 f,
2091 " ReactionSystem ({} species, {} parameters, {} reaction{})",
2092 species_count,
2093 param_count,
2094 reaction_count,
2095 if reaction_count == 1 { "" } else { "s" }
2096 )?;
2097
2098 for (i, reaction) in self.reactions.iter().enumerate() {
2100 let default_name = format!("R{}", i + 1);
2101 let reaction_name = reaction
2102 .id
2103 .as_deref()
2104 .or(reaction.name.as_deref())
2105 .unwrap_or(&default_name);
2106
2107 let substrates = reaction
2109 .substrates
2110 .iter()
2111 .flatten()
2112 .map(|s| {
2113 if s.coefficient == 1.0 {
2114 format_chemical_subscripts(&s.species)
2115 } else {
2116 format!(
2117 "{}·{}",
2118 format_number_unicode(s.coefficient),
2119 format_chemical_subscripts(&s.species)
2120 )
2121 }
2122 })
2123 .collect::<Vec<_>>()
2124 .join(" + ");
2125
2126 let products = reaction
2128 .products
2129 .iter()
2130 .flatten()
2131 .map(|p| {
2132 if p.coefficient == 1.0 {
2133 format_chemical_subscripts(&p.species)
2134 } else {
2135 format!(
2136 "{}·{}",
2137 format_number_unicode(p.coefficient),
2138 format_chemical_subscripts(&p.species)
2139 )
2140 }
2141 })
2142 .collect::<Vec<_>>()
2143 .join(" + ");
2144
2145 writeln!(
2146 f,
2147 " {}: {} → {} rate: {}",
2148 reaction_name, substrates, products, reaction.rate
2149 )?;
2150 }
2151
2152 Ok(())
2153 }
2154}
2155
2156impl fmt::Display for EsmFile {
2157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2158 let name = self.metadata.name.as_deref().unwrap_or("Unnamed");
2159 let description = self.metadata.description.as_deref().unwrap_or("");
2160 let authors = self
2161 .metadata
2162 .authors
2163 .as_ref()
2164 .map(|a| a.join(", "))
2165 .unwrap_or_else(|| "Unknown".to_string());
2166
2167 writeln!(f, "ESM v{}: {}", self.esm, name)?;
2168 if !description.is_empty() {
2169 writeln!(f, " \"{description}\"")?;
2170 }
2171 writeln!(f, " Authors: {authors}")?;
2172 writeln!(f)?;
2173
2174 if let Some(ref reaction_systems) = self.reaction_systems
2176 && !reaction_systems.is_empty()
2177 {
2178 writeln!(f, " Reaction Systems:")?;
2179 for system in reaction_systems.values() {
2180 write!(f, "{system}")?;
2181 writeln!(f)?;
2182 }
2183 }
2184
2185 if let Some(ref models) = self.models
2187 && !models.is_empty()
2188 {
2189 writeln!(f, " Models:")?;
2190 for model in models.values() {
2191 write!(f, "{model}")?;
2192 writeln!(f)?;
2193 }
2194 }
2195
2196 if let Some(ref data_sources) = self.data_sources
2200 && !data_sources.is_empty()
2201 {
2202 writeln!(f, " Data Sources:")?;
2203 for (name, source) in data_sources {
2204 let kind = match source.kind {
2205 crate::DataSourceKind::Grid => "grid",
2206 crate::DataSourceKind::Points => "points",
2207 crate::DataSourceKind::Static => "static",
2208 };
2209 writeln!(
2210 f,
2211 " {}: [{}] {}{}",
2212 name,
2213 kind,
2214 source.source.url_template,
2215 if source.temporal.is_some() {
2216 " (temporal)"
2217 } else {
2218 ""
2219 },
2220 )?;
2221 }
2222 writeln!(f)?;
2223 }
2224
2225 if let Some(ref coupling) = self.coupling
2227 && !coupling.is_empty()
2228 {
2229 writeln!(f, " Coupling:")?;
2230 for (i, entry) in coupling.iter().enumerate() {
2231 match entry {
2232 CouplingEntry::OperatorCompose { systems, .. } => {
2233 if systems.len() >= 2 {
2234 writeln!(
2235 f,
2236 " {}. operator_compose: {} + {}",
2237 i + 1,
2238 systems[0],
2239 systems[1]
2240 )?;
2241 }
2242 }
2243 CouplingEntry::VariableMap { from, to, .. } => {
2244 writeln!(f, " {}. variable_map: {} → {}", i + 1, from, to)?;
2245 }
2246 _ => {
2247 writeln!(f, " {}. {:?}", i + 1, entry)?;
2248 }
2249 }
2250 }
2251 writeln!(f)?;
2252 }
2253
2254 if let Some(ref domain) = self.domain {
2256 write!(f, " Domain: ")?;
2257
2258 let mut domain_parts = Vec::new();
2259 if domain.temporal.is_some() {
2260 domain_parts.push("temporal".to_string());
2261 }
2262
2263 if domain_parts.is_empty() {
2264 writeln!(f, "[Domain information]")?;
2265 } else {
2266 writeln!(f, "{}", domain_parts.join(", "))?;
2267 }
2268 }
2269
2270 Ok(())
2271 }
2272}
2273
2274#[cfg(test)]
2275mod tests {
2276 use super::*;
2277 use indexmap::IndexMap;
2278
2279 #[test]
2280 fn test_chemical_subscripts() {
2281 assert_eq!(format_chemical_subscripts("O3"), "O₃");
2282 assert_eq!(format_chemical_subscripts("NO2"), "NO₂");
2283 assert_eq!(format_chemical_subscripts("CH4"), "CH₄");
2284 assert_eq!(format_chemical_subscripts("C2H6"), "C₂H₆");
2285 assert_eq!(format_chemical_subscripts("H2O2"), "H₂O₂");
2286 }
2287
2288 #[test]
2289 fn test_number_formatting_unicode() {
2290 assert_eq!(format_number_unicode(42.0), "42");
2291 assert_eq!(format_number_unicode(3.15), "3.15");
2292 assert_eq!(format_number_unicode(1.8e-12), "1.8×10⁻¹²");
2294 assert_eq!(format_number_unicode(2.46e19), "2.46×10¹⁹");
2295
2296 assert_eq!(format_number_unicode(0.0), "0");
2298 assert_eq!(format_number_unicode(-0.0), "0");
2299
2300 assert_eq!(format_number_unicode(1.0), "1");
2302 assert_eq!(format_number_unicode(-1.0), "−1");
2303 }
2304
2305 #[test]
2306 fn test_zero_formatting_all_formats() {
2307 let zero_expr = Expr::Number(0.0);
2309 let one_expr = Expr::Number(1.0);
2310 let neg_zero_expr = Expr::Number(-0.0);
2311
2312 assert_eq!(to_unicode(&zero_expr), "0");
2314 assert_eq!(to_unicode(&neg_zero_expr), "0");
2315 assert_eq!(to_unicode(&one_expr), "1");
2316
2317 assert_eq!(to_latex(&zero_expr), "0");
2319 assert_eq!(to_latex(&neg_zero_expr), "0");
2320 assert_eq!(to_latex(&one_expr), "1");
2321
2322 assert_eq!(to_ascii(&zero_expr), "0");
2324 assert_eq!(to_ascii(&neg_zero_expr), "0");
2325 assert_eq!(to_ascii(&one_expr), "1");
2326 }
2327
2328 #[test]
2334 fn the_greek_table_drives_all_three_views() {
2335 let names: Vec<&str> = GREEK_TABLE.iter().map(|&(n, _, _)| n).collect();
2336 assert_eq!(
2337 names,
2338 [
2339 "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota",
2340 "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi", "rho", "sigma", "tau",
2341 "upsilon", "phi", "chi", "psi", "omega",
2342 ]
2343 );
2344 for &(n, c, _) in GREEK_TABLE {
2345 assert_eq!(greek_name_to_char(n), Some(c), "name → char for {n}");
2346 assert_eq!(greek_char_to_name(c), Some(n), "char → name for {c}");
2347 assert_eq!(greek_to_latex(n).unwrap(), format!("\\{n}"));
2348 assert_eq!(greek_to_latex(&c.to_string()).unwrap(), format!("\\{n}"));
2349 }
2350 let capitals: Vec<&str> = GREEK_TABLE
2351 .iter()
2352 .filter(|&&(_, _, cap)| cap)
2353 .map(|&(n, _, _)| n)
2354 .collect();
2355 assert_eq!(
2356 capitals,
2357 [
2358 "gamma", "delta", "theta", "lambda", "xi", "pi", "sigma", "upsilon", "phi", "psi",
2359 "omega",
2360 ]
2361 );
2362 assert_eq!(greek_to_latex("Theta").as_deref(), Some("\\Theta"));
2363 assert_eq!(greek_to_latex("Omega").as_deref(), Some("\\Omega"));
2364 for miss in ["Alpha", "Δ", "GAMMA", "Deltas", "x"] {
2368 assert_eq!(greek_to_latex(miss), None, "{miss} must not match");
2369 }
2370 assert_eq!(greek_to_ascii("θ_Δx"), "theta_Δx");
2371 assert_eq!(format_chemical_subscripts("Delta"), "Delta");
2372 }
2373
2374 #[test]
2375 fn test_scientific_all_backends() {
2376 assert_eq!(format_number_unicode(1.8e-12), "1.8×10⁻¹²");
2378 assert_eq!(format_number_unicode(2.46e19), "2.46×10¹⁹");
2379 assert_eq!(format_number_latex(1.8e-12), "1.8 \\times 10^{-12}");
2380 assert_eq!(format_number_ascii(1.8e-12), "1.8e-12");
2381 assert_eq!(format_number_unicode(f64::INFINITY), "∞");
2383 assert_eq!(format_number_latex(f64::NEG_INFINITY), "-\\infty");
2384 assert_eq!(format_number_ascii(f64::NAN), "NaN");
2385 }
2386
2387 #[test]
2388 fn test_expr_display() {
2389 let expr = Expr::Variable("O3".to_string());
2390 assert_eq!(format!("{expr}"), "O₃");
2391
2392 let expr = Expr::Number(1.8e-12);
2393 assert_eq!(format!("{expr}"), "1.8×10⁻¹²");
2394 }
2395
2396 #[test]
2397 fn test_operator_precedence() {
2398 let add = Expr::operator(ExpressionNode {
2399 op: "+".to_string(),
2400 args: vec![
2401 Expr::operator(ExpressionNode {
2402 op: "*".to_string(),
2403 args: vec![
2404 Expr::Variable("a".to_string()),
2405 Expr::Variable("b".to_string()),
2406 ],
2407 wrt: None,
2408 dim: None,
2409 ..Default::default()
2410 }),
2411 Expr::Variable("c".to_string()),
2412 ],
2413 wrt: None,
2414 dim: None,
2415 ..Default::default()
2416 });
2417 assert_eq!(format!("{add}"), "a·b + c");
2418
2419 let mul = Expr::operator(ExpressionNode {
2420 op: "*".to_string(),
2421 args: vec![
2422 Expr::operator(ExpressionNode {
2423 op: "+".to_string(),
2424 args: vec![
2425 Expr::Variable("a".to_string()),
2426 Expr::Variable("b".to_string()),
2427 ],
2428 wrt: None,
2429 dim: None,
2430 ..Default::default()
2431 }),
2432 Expr::Variable("c".to_string()),
2433 ],
2434 wrt: None,
2435 dim: None,
2436 ..Default::default()
2437 });
2438 assert_eq!(format!("{mul}"), "(a + b)·c");
2439 }
2440
2441 #[test]
2442 fn test_model_summary_display() {
2443 let metadata = Metadata {
2445 name: Some("TestModel".to_string()),
2446 description: Some("Test description".to_string()),
2447 authors: Some(vec!["Test Author".to_string()]),
2448 ..Default::default()
2449 };
2450
2451 let mut parameters = IndexMap::new();
2453 parameters.insert(
2454 "k1".to_string(),
2455 Parameter {
2456 default: Some(1.8e-12),
2457 units: Some("cm3/molec/s".to_string()),
2458 description: Some("Rate constant".to_string()),
2459 ..Default::default()
2460 },
2461 );
2462
2463 let reactions = vec![Reaction {
2464 id: Some("R1".to_string()),
2465 name: Some("R1".to_string()),
2466 substrates: Some(vec![StoichiometricEntry {
2467 species: "A".to_string(),
2468 coefficient: 1.0,
2469 }]),
2470 products: Some(vec![StoichiometricEntry {
2471 species: "B".to_string(),
2472 coefficient: 1.0,
2473 }]),
2474 rate: Expr::Variable("k1".to_string()),
2475 reference: None,
2476 }];
2477
2478 let mut species = IndexMap::new();
2479 species.insert(
2480 "A".to_string(),
2481 Species {
2482 default: Some(1e-9),
2483 units: Some("molec/cm3".to_string()),
2484 description: None,
2485 constant: None,
2486 ..Default::default()
2487 },
2488 );
2489 species.insert(
2490 "B".to_string(),
2491 Species {
2492 default: Some(0.0),
2493 units: Some("molec/cm3".to_string()),
2494 description: None,
2495 constant: None,
2496 ..Default::default()
2497 },
2498 );
2499
2500 let reaction_system = ReactionSystem {
2501 species,
2502 parameters,
2503 reactions,
2504 ..Default::default()
2505 };
2506
2507 let mut reaction_systems = IndexMap::new();
2508 reaction_systems.insert("TestReactions".to_string(), reaction_system);
2509
2510 let esm_file = EsmFile {
2512 esm: "0.1.0".to_string(),
2513 metadata,
2514 reaction_systems: Some(reaction_systems),
2515 ..Default::default()
2516 };
2517
2518 let output = format!("{esm_file}");
2520
2521 assert!(output.contains("ESM v0.1.0: TestModel"));
2523 assert!(output.contains("\"Test description\""));
2524 assert!(output.contains("Authors: Test Author"));
2525 assert!(output.contains("Reaction Systems:"));
2526 assert!(output.contains("(2 species, 1 parameters, 1 reaction)"));
2527 assert!(output.contains("R1: A → B rate: k1"));
2528
2529 assert_eq!(to_unicode(&esm_file), output);
2534 assert_eq!(to_latex(&esm_file), output);
2535 assert_eq!(to_ascii(&esm_file), output);
2536
2537 let rs = &esm_file.reaction_systems.as_ref().unwrap()["TestReactions"];
2538 assert_eq!(to_unicode(rs), rs.to_string());
2539 assert_eq!(to_ascii(rs), rs.to_string());
2540 }
2541
2542 #[test]
2545 fn equations_render_in_all_three_formats() {
2546 let eq = Equation {
2547 lhs: Expr::Variable("alpha".to_string()),
2548 rhs: Expr::operator(ExpressionNode {
2549 op: "*".to_string(),
2550 args: vec![Expr::Variable("beta".to_string()), Expr::Number(2.0)],
2551 ..Default::default()
2552 }),
2553 ..Default::default()
2554 };
2555 assert_eq!(to_unicode(&eq), "α = β·2");
2556 assert_eq!(to_ascii(&eq), "alpha = beta * 2");
2557 assert_eq!(to_latex(&eq), "\\alpha = \\beta \\cdot 2");
2558 }
2559
2560 #[test]
2561 fn test_pre_operator_formatting() {
2562 use crate::types::*;
2563
2564 let pre_expr = Expr::operator(ExpressionNode {
2566 op: "Pre".to_string(),
2567 args: vec![Expr::Variable("x".to_string())],
2568 wrt: None,
2569 dim: None,
2570 ..Default::default()
2571 });
2572
2573 assert_eq!(to_unicode(&pre_expr), "Pre(x)");
2575 assert_eq!(to_latex(&pre_expr), "\\mathrm{Pre}(x)");
2576 assert_eq!(to_ascii(&pre_expr), "Pre(x)");
2577
2578 let complex_pre = Expr::operator(ExpressionNode {
2580 op: "Pre".to_string(),
2581 args: vec![Expr::operator(ExpressionNode {
2582 op: "+".to_string(),
2583 args: vec![
2584 Expr::Variable("a".to_string()),
2585 Expr::Variable("b".to_string()),
2586 ],
2587 wrt: None,
2588 dim: None,
2589 ..Default::default()
2590 })],
2591 wrt: None,
2592 dim: None,
2593 ..Default::default()
2594 });
2595
2596 assert_eq!(to_unicode(&complex_pre), "Pre(a + b)");
2597 assert_eq!(to_latex(&complex_pre), "\\mathrm{Pre}(a + b)");
2598 assert_eq!(to_ascii(&complex_pre), "Pre(a + b)");
2599
2600 let multi_arg_pre = Expr::operator(ExpressionNode {
2602 op: "Pre".to_string(),
2603 args: vec![
2604 Expr::Variable("x".to_string()),
2605 Expr::Variable("y".to_string()),
2606 ],
2607 wrt: None,
2608 dim: None,
2609 ..Default::default()
2610 });
2611
2612 assert_eq!(to_unicode(&multi_arg_pre), "Pre(x, y)");
2613 assert_eq!(to_latex(&multi_arg_pre), "\\mathrm{Pre}(x, y)");
2614 assert_eq!(to_ascii(&multi_arg_pre), "Pre(x, y)");
2615 }
2616
2617 fn op_node(op: &str, args: Vec<Expr>) -> Expr {
2619 Expr::operator(ExpressionNode {
2620 op: op.to_string(),
2621 args,
2622 wrt: None,
2623 dim: None,
2624 ..Default::default()
2625 })
2626 }
2627
2628 fn var(name: &str) -> Expr {
2629 Expr::Variable(name.to_string())
2630 }
2631
2632 #[test]
2633 fn test_precedence_parens_all_printers() {
2634 let mul = op_node("*", vec![op_node("+", vec![var("a"), var("b")]), var("c")]);
2638 assert_eq!(to_unicode(&mul), "(a + b)·c");
2639 assert_eq!(to_latex(&mul), "(a + b) \\cdot c");
2640 assert_eq!(to_ascii(&mul), "(a + b) * c");
2641
2642 let add = op_node("+", vec![var("a"), op_node("*", vec![var("b"), var("c")])]);
2644 assert_eq!(to_unicode(&add), "a + b·c");
2645 assert_eq!(to_latex(&add), "a + b \\cdot c");
2646 assert_eq!(to_ascii(&add), "a + b * c");
2647
2648 let sub = op_node("-", vec![var("a"), op_node("-", vec![var("b"), var("c")])]);
2650 assert_eq!(to_unicode(&sub), "a − (b − c)");
2651 assert_eq!(to_latex(&sub), "a - (b - c)");
2652 assert_eq!(to_ascii(&sub), "a - (b - c)");
2653
2654 let pow = op_node(
2657 "^",
2658 vec![op_node("+", vec![var("a"), var("b")]), Expr::Number(2.0)],
2659 );
2660 assert_eq!(to_unicode(&pow), "(a + b)²");
2661 assert_eq!(to_latex(&pow), "(a + b)^{2}");
2662 assert_eq!(to_ascii(&pow), "(a + b)^2");
2663
2664 let frac_mul = op_node("*", vec![var("a"), op_node("/", vec![var("b"), var("c")])]);
2669 assert_eq!(to_unicode(&frac_mul), "a·(b/c)");
2670 assert_eq!(to_latex(&frac_mul), "a \\cdot \\frac{b}{c}");
2671 assert_eq!(to_ascii(&frac_mul), "a * (b / c)");
2672
2673 let ind = op_node(
2676 "*",
2677 vec![
2678 op_node("<=", vec![var("a"), var("b")]),
2679 op_node("<", vec![var("c"), var("d")]),
2680 ],
2681 );
2682 assert_eq!(to_ascii(&ind), "(a <= b) * (c < d)");
2683 let lt_plus = op_node("+", vec![op_node("<", vec![var("a"), var("b")]), var("c")]);
2684 assert_eq!(to_ascii(<_plus), "(a < b) + c");
2685 let and_mul = op_node(
2686 "*",
2687 vec![op_node("and", vec![var("a"), var("b")]), var("c")],
2688 );
2689 assert_eq!(to_ascii(&and_mul), "(a and b) * c");
2690 let and3 = op_node(
2693 "and",
2694 vec![
2695 op_node("<=", vec![var("a"), var("b")]),
2696 op_node("<", vec![var("b"), var("c")]),
2697 op_node("<", vec![var("c"), var("d")]),
2698 ],
2699 );
2700 assert_eq!(to_ascii(&and3), "a <= b and b < c and c < d");
2701 assert_eq!(to_unicode(&and3), "a ≤ b ∧ b < c ∧ c < d");
2702 let eq_lt = op_node("==", vec![var("a"), op_node("<", vec![var("b"), var("c")])]);
2703 assert_eq!(to_ascii(&eq_lt), "a == (b < c)");
2704 let neg_lt = op_node("-", vec![op_node("<", vec![var("a"), var("b")])]);
2706 assert_eq!(to_ascii(&neg_lt), "-(a < b)");
2707 let neg_sum = op_node("-", vec![op_node("+", vec![var("a"), var("b")])]);
2708 assert_eq!(to_ascii(&neg_sum), "-(a + b)");
2709 let neg_prod = op_node("-", vec![op_node("*", vec![var("a"), var("b")])]);
2710 assert_eq!(to_ascii(&neg_prod), "-a * b");
2711 }
2712
2713 #[test]
2714 fn test_scientific_notation_unified_across_printers() {
2715 let large = Expr::Number(2.46e19);
2719 assert_eq!(to_unicode(&large), "2.46×10¹⁹");
2720 assert_eq!(to_latex(&large), "2.46 \\times 10^{19}");
2721 assert_eq!(to_ascii(&large), "2.46e19");
2722
2723 let small = Expr::Number(1.8e-12);
2724 assert_eq!(to_unicode(&small), "1.8×10⁻¹²");
2725 assert_eq!(to_latex(&small), "1.8 \\times 10^{-12}");
2726 assert_eq!(to_ascii(&small), "1.8e-12");
2727
2728 let big_int = Expr::Integer(15000);
2730 assert_eq!(to_unicode(&big_int), "1.5×10⁴");
2731 assert_eq!(to_ascii(&big_int), "1.5e4");
2732 }
2733
2734 #[test]
2740 fn test_operator_matrix_all_backends() {
2741 fn opn(op: &str, args: Vec<Expr>) -> Expr {
2742 Expr::operator(ExpressionNode {
2743 op: op.to_string(),
2744 args,
2745 ..Default::default()
2746 })
2747 }
2748 fn dop(wrt: &str, arg: Expr) -> Expr {
2749 Expr::operator(ExpressionNode {
2750 op: "D".to_string(),
2751 args: vec![arg],
2752 wrt: Some(wrt.to_string()),
2753 ..Default::default()
2754 })
2755 }
2756 let a = || var("a");
2757 let b = || var("b");
2758 let c = || var("c");
2759 let ab = || opn("+", vec![a(), b()]);
2760 let chk = |e: Expr, u: &str, l: &str, s: &str| {
2763 assert_eq!(to_unicode(&e), u, "unicode");
2764 assert_eq!(to_latex(&e), l, "latex");
2765 assert_eq!(to_ascii(&e), s, "ascii");
2766 };
2767
2768 chk(opn("*", vec![a(), b()]), "a·b", r"a \cdot b", "a * b");
2770 chk(
2771 opn("*", vec![ab(), c()]),
2772 "(a + b)·c",
2773 r"(a + b) \cdot c",
2774 "(a + b) * c",
2775 );
2776 chk(
2777 opn("*", vec![var("\\mathrm{O_3}"), var("\\mathrm{N_2}")]),
2778 r"\mathrm{O_3}·\mathrm{N_2}",
2779 r"\mathrm{O_3} \mathrm{N_2}",
2780 r"\mathrm{O_3} * \mathrm{N_2}",
2781 );
2782 chk(opn("/", vec![a(), b()]), "a/b", r"\frac{a}{b}", "a / b");
2784 chk(
2785 opn("/", vec![ab(), c()]),
2786 "(a + b)/c",
2787 r"\frac{a + b}{c}",
2788 "(a + b) / c",
2789 );
2790 chk(
2794 opn("*", vec![a(), opn("/", vec![b(), c()])]),
2795 "a·(b/c)",
2796 r"a \cdot \frac{b}{c}",
2797 "a * (b / c)",
2798 );
2799 chk(opn("^", vec![a(), b()]), "a^b", r"a^{b}", "a^b");
2801 chk(opn("^", vec![a(), Expr::Integer(2)]), "a²", r"a^{2}", "a^2");
2802 chk(
2803 opn("^", vec![a(), Expr::Number(2.0)]),
2804 "a²",
2805 r"a^{2}",
2806 "a^2",
2807 );
2808 chk(
2809 opn("^", vec![a(), Expr::Number(2.5)]),
2810 "a^2.5",
2811 r"a^{2.5}",
2812 "a^2.5",
2813 );
2814 chk(
2815 opn("^", vec![ab(), Expr::Integer(2)]),
2816 "(a + b)²",
2817 r"(a + b)^{2}",
2818 "(a + b)^2",
2819 );
2820 chk(
2821 opn("^", vec![a(), Expr::Integer(-3)]),
2822 "a⁻³",
2823 r"a^{-3}",
2824 "a^-3",
2825 );
2826 chk(opn("-", vec![a()]), "−a", "-a", "-a");
2828 chk(
2829 opn("-", vec![a(), opn("-", vec![b(), c()])]),
2830 "a − (b − c)",
2831 "a - (b - c)",
2832 "a - (b - c)",
2833 );
2834 chk(
2835 opn("+", vec![a(), opn("-", vec![b()])]),
2836 "a − b",
2837 "a - b",
2838 "a - b",
2839 );
2840 chk(
2842 dop("t", a()),
2843 "∂a/∂t",
2844 r"\frac{\partial a}{\partial t}",
2845 "D(a)/Dt",
2846 );
2847 chk(
2848 dop("t", ab()),
2849 "∂(a + b)/∂t",
2850 r"\frac{\partial (a + b)}{\partial t}",
2851 "D(a + b)/Dt",
2852 );
2853 chk(
2859 opn("D", vec![a()]),
2860 "∂a/∂t",
2861 r"\frac{\partial a}{\partial t}",
2862 "D(a)/Dt",
2863 );
2864 chk(opn("D", vec![a(), b()]), "D(a, b)", "D(a, b)", "D(a, b)");
2866 chk(opn(">", vec![a(), b()]), "a > b", "a > b", "a > b");
2868 chk(opn(">=", vec![a(), b()]), "a ≥ b", r"a \geq b", "a >= b");
2869 chk(opn(">=", vec![a()]), ">=(a)", r"\geq(a)", ">=(a)");
2870 chk(opn("<=", vec![a(), b()]), "a ≤ b", r"a \leq b", "a <= b");
2871 chk(opn("=", vec![a(), b()]), "a = b", "a = b", "a == b");
2872 chk(opn("==", vec![a()]), "=(a)", "=(a)", "==(a)");
2873 chk(opn("!=", vec![a(), b()]), "a ≠ b", r"a \neq b", "a != b");
2874 chk(opn("!=", vec![a()]), "!=(a)", r"\neq(a)", "!=(a)");
2875 chk(opn("and", vec![a(), b()]), "a ∧ b", r"a \land b", "a and b");
2877 chk(opn("or", vec![a(), b()]), "a ∨ b", r"a \lor b", "a or b");
2878 chk(opn("not", vec![a()]), "¬a", r"\neg a", "not a");
2879 chk(
2880 opn("not", vec![opn("==", vec![a(), b()])]),
2881 "¬(a = b)",
2882 r"\neg (a = b)",
2883 "not (a == b)",
2884 );
2885 chk(
2886 opn("not", vec![a(), b()]),
2887 "not(a, b)",
2888 r"\neg(a, b)",
2889 "not(a, b)",
2890 );
2891 chk(opn("log", vec![a()]), "ln(a)", r"\ln(a)", "log(a)");
2893 chk(
2894 opn("log", vec![a(), b()]),
2895 "log(a, b)",
2896 r"\log(a, b)",
2897 "log(a, b)",
2898 );
2899 chk(
2900 opn("log10", vec![a()]),
2901 "log₁₀(a)",
2902 r"\log_{10}(a)",
2903 "log10(a)",
2904 );
2905 chk(
2906 opn("log10", vec![a(), b()]),
2907 "log10(a, b)",
2908 r"\log_{10}(a, b)",
2909 "log10(a, b)",
2910 );
2911 chk(opn("sqrt", vec![a()]), "√a", r"\sqrt{a}", "sqrt(a)");
2912 chk(
2913 opn("sqrt", vec![a(), b()]),
2914 "sqrt(a, b)",
2915 r"\sqrt{a, b}",
2916 "sqrt(a, b)",
2917 );
2918 chk(
2919 opn("sqrt", vec![ab()]),
2920 "√(a + b)",
2921 r"\sqrt{a + b}",
2922 "sqrt(a + b)",
2923 );
2924 chk(
2925 opn("asin", vec![a()]),
2926 "arcsin(a)",
2927 r"\arcsin(a)",
2928 "asin(a)",
2929 );
2930 chk(opn("abs", vec![a()]), "|a|", "|a|", "abs(a)");
2931 chk(
2932 opn("abs", vec![a(), b()]),
2933 "abs(a, b)",
2934 "|a, b|",
2935 "abs(a, b)",
2936 );
2937 chk(
2938 opn("sign", vec![a()]),
2939 "sgn(a)",
2940 r"\mathrm{sgn}(a)",
2941 "sign(a)",
2942 );
2943 chk(
2944 opn("floor", vec![a()]),
2945 "⌊a⌋",
2946 r"\lfloor a \rfloor",
2947 "floor(a)",
2948 );
2949 chk(
2950 opn("floor", vec![a(), b()]),
2951 "floor(a, b)",
2952 r"\lfloor a, b \rfloor",
2953 "floor(a, b)",
2954 );
2955 chk(opn("ceil", vec![a()]), "⌈a⌉", r"\lceil a \rceil", "ceil(a)");
2956 chk(
2957 opn("asinh", vec![a()]),
2958 "sinh⁻¹(a)",
2959 r"\sinh^{-1}(a)",
2960 "asinh(a)",
2961 );
2962 chk(opn("exp", vec![a()]), "exp(a)", r"\exp(a)", "exp(a)");
2963 chk(
2964 opn("exp", vec![ab()]),
2965 "exp(a + b)",
2966 r"\exp\left(a + b\right)",
2967 "exp(a + b)",
2968 );
2969 chk(opn("min", vec![a()]), "min(a)", r"\min(a)", "min(a)");
2970 chk(
2971 opn("atan2", vec![a()]),
2972 "atan2(a)",
2973 r"\mathrm{atan2}(a)",
2974 "atan2(a)",
2975 );
2976 chk(opn("sin", vec![a()]), "sin(a)", r"\sin(a)", "sin(a)");
2977 chk(
2978 opn("Pre", vec![a()]),
2979 "Pre(a)",
2980 r"\mathrm{Pre}(a)",
2981 "Pre(a)",
2982 );
2983 chk(
2985 opn("ifelse", vec![a(), b(), c()]),
2986 "ifelse(a, b, c)",
2987 r"\begin{cases} b & \text{if } a \\ c & \text{otherwise} \end{cases}",
2988 "ifelse(a, b, c)",
2989 );
2990 chk(
2991 opn("ifelse", vec![a(), b()]),
2992 "ifelse(a, b)",
2993 r"\mathrm{ifelse}(a, b)",
2994 "ifelse(a, b)",
2995 );
2996 chk(
2998 opn("grad", vec![a()]),
2999 "grad(a)",
3000 r"\mathrm{grad}(a)",
3001 "grad(a)",
3002 );
3003 chk(
3004 opn("my_op", vec![a(), b()]),
3005 "my_op(a, b)",
3006 r"\mathrm{my\_op}(a, b)",
3007 "my_op(a, b)",
3008 );
3009 }
3010}