use formulaa::ast::{Node, Row, normalize, strip_spacers};
use formulaa::latex::row_to_latex;
use formulaa::parse::parse;
use formulaa::render::{RenderCtx, absorb_spacers, render_root};
use formulaa::symbols::Radical;
use formulaa::symbols::{Accent, Arrow};
fn s(t: &str) -> Row {
t.chars().map(Node::Sym).collect()
}
fn n(node: Node) -> Row {
vec![node]
}
fn frac(num: Row, den: Row) -> Node {
Node::Frac { num, den }
}
fn sqrt(arg: Row) -> Node {
Node::Sqrt {
arg,
index: Radical::Sqrt,
}
}
fn cbrt(arg: Row) -> Node {
Node::Sqrt {
arg,
index: Radical::Cbrt,
}
}
fn sup(arg: Row) -> Node {
Node::Sup { arg }
}
fn sub(arg: Row) -> Node {
Node::Sub { arg }
}
fn delim(left: char, right: char, mids: Vec<char>, segs: Vec<Row>) -> Node {
use formulaa::symbols::Delim;
Node::Delim {
left: Delim::of_spec_side(left, true).unwrap(),
right: Delim::of_spec_side(right, false).unwrap(),
mids: mids.len(),
segs,
}
}
fn paren(inner: Row) -> Node {
delim('(', ')', vec![], vec![inner])
}
fn bigop(op: char, lower: Row, upper: Row) -> Node {
Node::BigOpSym { op, lower, upper }
}
fn opname(name: &str, lower: Row, upper: Row) -> Node {
Node::BigOp {
name: name.into(),
lower,
upper,
}
}
fn func(name: &str) -> Node {
Node::Func(name.into())
}
fn acc(accent: Accent, base: char) -> Node {
if accent.under() {
Node::Accent {
overs: vec![],
unders: vec![accent],
base,
}
} else {
Node::Accent {
overs: vec![accent],
unders: vec![],
base,
}
}
}
fn array(rows: usize, cols: usize, cells: Vec<Row>) -> Node {
assert_eq!(cells.len(), rows * cols);
Node::Array { rows, cols, cells }
}
fn mat(rows: usize, cols: usize, cells: Vec<Row>) -> Node {
delim('[', ']', vec![], vec![vec![array(rows, cols, cells)]])
}
fn cat(parts: &[Row]) -> Row {
parts.concat()
}
fn roundtrip(name: &str, row: &Row) {
let row = normalize(row);
let ctx = RenderCtx::canonical();
let aa = render_root(&row, None, &ctx).to_text();
let expected = normalize(&absorb_spacers(&row));
assert_eq!(
normalize(&expected),
expected,
"[{}] normalize is not idempotent",
name
);
let parsed = parse(&aa).unwrap_or_else(|e| {
panic!(
"[{}] parse failed: {}\n--- AA ---\n{}\n--- AST ---\n{:?}",
name, e, aa, row
)
});
assert_eq!(
parsed,
expected,
"[{}] AST mismatch\n--- AA ---\n{}\n--- LaTeX (expected) ---\n{}\n--- LaTeX (parsed) ---\n{}",
name,
aa,
row_to_latex(&expected),
row_to_latex(&parsed)
);
let aa2 = render_root(&parsed, None, &ctx).to_text();
let reparsed = parse(&aa2)
.unwrap_or_else(|e| panic!("[{}] re-parse failed: {}\n--- AA ---\n{}", name, e, aa2));
assert_eq!(reparsed, parsed, "[{}] re-render mismatch", name);
if !expected.is_empty() {
assert!(!row_to_latex(&expected).is_empty());
}
let tex = row_to_latex(&expected);
let from_tex = normalize(&formulaa::from_latex::row_from_latex(&tex));
assert_eq!(
from_tex,
normalize(&strip_spacers(&expected)),
"[{}] LaTeX roundtrip mismatch\n--- LaTeX ---\n{}",
name,
tex
);
let exported = formulaa::render::export_aa(&row);
let reparsed = parse(&exported).unwrap_or_else(|e| {
panic!(
"[{}] export parse failed: {}\n--- AA ---\n{}",
name, e, exported
)
});
assert_eq!(
reparsed, expected,
"[{}] export mismatch\n--- AA ---\n{}",
name, exported
);
}
#[test]
fn malformed_pictures_do_not_panic() {
for aa in [
"├ ┤\n⎝ 𝑏 ⎠",
"⎛ 𝑎 ⎞\n├ ┤",
" ┌\n𝑥│\n √",
" ┌\n │\n √",
"┌\n√",
"⎡\n⎣",
"┬",
"╱\n╲",
] {
let got = parse(aa);
assert!(got.is_ok() || got.is_err(), "{:?}", aa);
}
}
#[test]
fn foreign_latex_reads_right() {
use formulaa::from_latex::row_from_latex;
let tex = |t: &str| row_to_latex(&normalize(&row_from_latex(t)));
assert_eq!(tex(r"{1 \over 2} + x"), r"\frac{1}{2}+x");
assert_eq!(tex(r"a \atop b"), r"\frac{a}{b}");
assert_eq!(tex(r"a \not= b"), r"a\ne b");
assert_eq!(tex(r"a \not\in B"), r"a\notin B");
assert_eq!(tex(r"a \not\leq b"), r"a\nleq b");
assert_eq!(tex(r"a \not{=} b"), r"a\ne b");
assert_eq!(tex(r"a \not\equiv b"), r"a\not\equiv b");
assert_eq!(tex(r"a \not\perp b"), r"a\perp b");
assert_eq!(tex(r"\cancel{\frac{a}{b}}"), r"\frac{a}{b}");
assert_eq!(tex(r"\cancel{x}+y"), r"x+y");
assert_eq!(
tex(r"\sum\limits_{i=1}^{n} a"),
tex(r"\sum_{i=1}^{n} a"),
"\\limits keeps the band"
);
assert_eq!(
tex(r"\begin{array} a & b \\ c \end{array}"),
tex(r"\begin{array}{cc} a & b \\ c \end{array}"),
"a spec-less array keeps its first cell"
);
}
#[test]
fn normalize_is_idempotent() {
use formulaa::ast::{Field, Node};
let cases: Vec<Node> = vec![
Node::BigOp {
name: "T".into(),
lower: vec![],
upper: vec![],
},
Node::BigOp {
name: "lim".into(),
lower: vec![],
upper: vec![],
},
Node::Func("T".into()),
Node::Func("".into()),
Node::Roman('1'),
Node::Roman('.'),
Node::WideAccent {
overs: vec![],
unders: vec![],
base: vec![Node::Sym('x')],
},
Node::WideAccent {
overs: vec![],
unders: vec![],
base: vec![],
},
Node::Sup { arg: vec![] },
];
for n in cases {
let once = normalize(&vec![n.clone()]);
assert_eq!(normalize(&once), once, "not idempotent: {:?}", n);
let mut host = Node::Sqrt {
index: formulaa::symbols::Radical::Sqrt,
arg: vec![],
};
*host.field_mut(Field::SqrtArg) = vec![n.clone()];
let once = normalize(&vec![host.clone()]);
assert_eq!(
normalize(&once),
once,
"not idempotent in an inset: {:?}",
n
);
}
}
#[test]
fn export_blanks_slot_marks_when_safe() {
use formulaa::render::export_aa;
let row = vec![frac(vec![], s("2"))];
let ex = export_aa(&row);
assert!(!ex.contains('⬚'), "{}", ex);
assert_eq!(parse(&ex).unwrap(), normalize(&row));
assert!(!export_aa(&s("x+1")).contains('⬚'));
let row = vec![sup(n(frac(s("1"), s("2"))))];
let ex = export_aa(&row);
assert!(ex.contains('⬚'), "{}", ex);
assert_eq!(parse(&ex).unwrap(), normalize(&row));
}
#[test]
fn stray_stacked_content_is_an_error() {
assert!(parse("1 y\n─ + x\n2").is_err());
assert!(parse("1\n─ + x\n2 y").is_err());
}
#[test]
fn operatorname_star_band() {
let row = cat(&[
n(opname("esssup", s("x"), vec![])),
s("f"),
n(paren(s("x"))),
]);
roundtrip("operatorname-star", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\operatorname*{esssup}_{x}f\\left(x\\right)"
);
let row = cat(&[n(opname("esssup", vec![], vec![])), s("f")]);
roundtrip("operatorname-star-bandless-limits", &row);
assert_eq!(normalize(&row)[0], func("esssup"));
assert_eq!(row_to_latex(&normalize(&row)), "\\operatorname{esssup}f");
}
#[test]
fn word_operators() {
let row = n(opname("limsup", s("n"), vec![]));
roundtrip("limsup", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert!(aa.contains("┈limsup┈"), "{}", aa);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\operatorname*{lim\\,sup}_{n}"
);
let row = n(opname("argmax", s("x"), vec![]));
roundtrip("argmax", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\operatorname*{arg\\,max}_{x}"
);
}
#[test]
fn roman_differential_quotes() {
let d = || Node::Roman('d');
let row = cat(&[n(d()), s("x")]);
roundtrip("dx", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "d𝑥");
assert_eq!(row_to_latex(&normalize(&row)), "\\mathrm{d}x");
let row = cat(&[n(d())]);
roundtrip("d-alone", &row);
assert_eq!(
render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text(),
"'d'"
);
let row = cat(&[n(d()), s("+"), s("x")]);
roundtrip("d-plus", &row);
assert!(
render_root(&normalize(&row), None, &RenderCtx::canonical())
.to_text()
.starts_with("'d'")
);
let row = cat(&[n(d()), n(d())]);
roundtrip("d-d", &row);
}
#[test]
fn dotted_roman_runs() {
let t = |s: &str| Node::Func(s.into());
let aa = |row: &Row| render_root(&normalize(row), None, &RenderCtx::canonical()).to_text();
let row = cat(&[n(t("i.i.d.")), s("x")]);
roundtrip("iid", &row);
assert_eq!(aa(&row), "i.i.d.𝑥");
assert_eq!(row_to_latex(&normalize(&row)), "\\operatorname{i.i.d.}x");
let row = cat(&[n(t("i.i.d.")), n(t("ab"))]);
roundtrip("iid-ab", &row);
let row = cat(&[n(t("i.i")), n(Node::Sym('.'))]);
roundtrip("iid-dot", &row);
let row = cat(&[n(func("sin")), n(Node::Sym('.'))]);
roundtrip("sin-dot", &row);
assert_eq!(aa(&row), "sin.");
let row = cat(&[n(t("x..y"))]);
roundtrip("double-dot", &row);
assert_eq!(aa(&row), "'x..y'");
let row = cat(&[s("x"), n(Node::Sym('′')), n(Node::Sym('′'))]);
roundtrip("primes", &row);
assert_eq!(aa(&row), "𝑥′′");
}
#[test]
fn wide_accents() {
let wa = |over: Option<Accent>, under: Option<Accent>, base: Row| Node::WideAccent {
overs: over.into_iter().collect(),
unders: under.into_iter().collect(),
base,
};
let row = n(wa(Some(Accent::Hat), None, s("abc")));
roundtrip("widehat", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "┈┈˰┈┈\n 𝑎𝑏𝑐");
assert_eq!(row_to_latex(&normalize(&row)), "\\widehat{abc}");
let row = n(wa(Some(Accent::Vec), Some(Accent::Underline), s("AB")));
roundtrip("vec-underline", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\overrightarrow{\\underline{AB}}"
);
let row = n(wa(None, Some(Accent::Utilde), s("AB")));
roundtrip("wide-utilde", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, " 𝐴𝐵\n┈˜˜┈");
assert_eq!(row_to_latex(&normalize(&row)), "\\utilde{AB}");
let row = n(wa(Some(Accent::Tilde), Some(Accent::Utilde), s("xy")));
roundtrip("tilde-utilde", &row);
let inner = wa(Some(Accent::Hat), Some(Accent::Underline), s("Aβ"));
let row = n(wa(Some(Accent::Hat), None, cat(&[n(inner), s("1")])));
roundtrip("stacked-hats", &row);
let row = n(wa(Some(Accent::Hat), None, s("x")));
assert_eq!(
normalize(&row),
vec![Node::Accent {
overs: vec![Accent::Hat],
unders: vec![],
base: 'x'
}]
);
let row = cat(&[
n(sup(n(wa(None, Some(Accent::Underline), s("px"))))),
n(wa(Some(Accent::Tilde), None, s("bc"))),
]);
roundtrip("band-next-to-band", &row);
let row = n(wa(None, None, s("ab")));
assert_eq!(normalize(&row), s("ab"));
}
#[test]
fn box_drawing_delim_forms() {
let arr = |rows: usize, cols: usize, cells: Vec<Row>| Node::Array { rows, cols, cells };
let row = n(delim(
'(',
')',
vec![],
vec![n(arr(2, 1, vec![s("a"), s("b")]))],
));
roundtrip("fused-grid-1col", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "⎛ 𝑎 ⎞\n├ ┤\n⎝ 𝑏 ⎠");
let row = n(delim(
'(',
')',
vec![],
vec![n(arr(2, 2, vec![s("a"), s("b"), s("c"), s("d")]))],
));
roundtrip("fused-grid-2x2", &row);
let row = n(delim(
'(',
')',
vec![],
vec![n(arr(1, 2, vec![s("a"), s("b")]))],
));
roundtrip("fused-grid-1row", &row);
let row = n(delim(
'(',
']',
vec![],
vec![n(arr(2, 1, vec![s("a"), s("b")]))],
));
roundtrip("fused-grid-mixed", &row);
let row = n(delim(
'[',
']',
vec![],
vec![n(arr(2, 1, vec![s("a"), s("b")]))],
));
roundtrip("fused-grid-bracket", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "⎡ 𝑎 ⎤\n├ ┤\n⎣ 𝑏 ⎦");
let row = n(delim(
'{',
'}',
vec![],
vec![n(arr(2, 1, vec![s("a"), s("b")]))],
));
roundtrip("curly-wraps-lattice", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert!(
aa.contains('┌') && aa.contains('⎨'),
"no fusion for curly:\n{}",
aa
);
let row = n(delim(
'{',
'}',
vec![],
vec![cat(&[s("a"), n(sup(s("α+")))])],
));
roundtrip("curly-min-height", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "⎧ α+⎫\n⎨𝑎 ⎬\n⎩ ⎭");
let row = n(delim('.', ')', vec![], vec![n(frac(s("1"), s("2")))]));
roundtrip("null-left", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert!(aa.contains('┆') && aa.contains('⎞'), "{}", aa);
let row = n(Node::Norm {
arg: n(frac(s("1"), s("2"))),
});
roundtrip("tall-norm-stacked", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(
aa.matches('‖').count(),
6,
"stacked ‖ on both sides:\n{}",
aa
);
}
#[test]
fn sqrt_box_overline() {
let row = n(sqrt(cat(&[s("1+"), n(frac(s("1"), s("2")))])));
roundtrip("sqrt-box", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert!(aa.starts_with("┌────"), "{}", aa);
let row = cat(&[
n(sqrt(s("2"))),
n(sup(n(Node::Arrow {
op: Arrow::To,
over: vec![],
under: vec![],
}))),
]);
roundtrip("sqrt-then-arrow-sup", &row);
}
#[test]
fn ceil_floor_norm() {
let row = cat(&[n(delim(
'⌈',
'⌉',
vec![],
vec![cat(&[s("x"), n(frac(s("1"), s("2")))])],
))]);
roundtrip("ceil", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\left\\lceil x\\frac{1}{2}\\right\\rceil "
);
let row = cat(&[n(delim('⌊', '⌋', vec![], vec![s("n")]))]);
roundtrip("floor", &row);
let row = cat(&[n(Node::Norm {
arg: cat(&[s("v"), n(frac(s("a"), s("b")))]),
})]);
roundtrip("norm", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\left\\|v\\frac{a}{b}\\right\\|"
);
let row = cat(&[
n(Node::Norm { arg: s("v") }),
s("+"),
n(Node::Norm { arg: s("w") }),
]);
roundtrip("norm-siblings", &row);
}
#[test]
fn text_spaces_and_operator_names() {
let row = cat(&[n(Node::Text("if x holds".into()))]);
roundtrip("text-spaces", &row);
assert_eq!(
render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text(),
"\"if x holds\""
);
assert_eq!(row_to_latex(&normalize(&row)), "\\text{if x holds}");
let row = cat(&[n(Node::Text("a\"b\\c".into()))]);
roundtrip("text-escapes", &row);
assert_eq!(
render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text(),
"\"a\\\"b\\\\c\""
);
let row = cat(&[n(Node::Text("50% {of} $x_1$".into()))]);
roundtrip("text-syntax-chars", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\text{50\\% \\{of\\} \\$x\\_1\\$}"
);
assert_eq!(
row_to_latex(&vec![func("arcctg")]),
"\\operatorname{arcctg}"
);
assert_eq!(row_to_latex(&vec![func("Tr")]), "\\operatorname{Tr}");
assert_eq!(row_to_latex(&vec![func("Re")]), "\\operatorname{Re}");
roundtrip("tr-run", &cat(&[n(func("Tr")), n(paren(s("A")))]));
let row = cat(&[n(Node::BigOp {
name: "plim".into(),
lower: s("n"),
upper: vec![],
})]);
roundtrip("plim", &row);
assert_eq!(row_to_latex(&normalize(&row)), "\\operatorname*{plim}_{n}");
}
#[test]
fn rcases_grid() {
let row = n(delim(
'.',
'}',
vec![],
vec![vec![array(2, 2, vec![s("a"), s("b"), s("c"), s("d")])]],
));
roundtrip("rcases", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\begin{rcases} a & b \\\\ c & d \\end{rcases}"
);
}
#[test]
fn tall_middle_braket() {
let row = cat(&[n(delim(
'⟨',
'⟩',
vec!['|', '|'],
vec![
s("ψ"),
vec![Node::Frac {
num: s("H"),
den: s("2"),
}],
s("ψ"),
],
))]);
roundtrip("tall-middle", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\left\\langle \\psi \\middle|\\frac{H}{2}\\middle|\\psi \\right\\rangle "
);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, " ╱ │ 𝐻 │ ╲\n╱ ψ│───│ψ ╲\n╲ │ 2 │ ╱\n ╲ │ │ ╱");
for old in [
"╱ ╲\n⟨𝑥 + 𝑦⟩\n╲ ╱",
"╱ 1 ╲\n⟨𝑥 + ───⟩\n╲ 2 ╱",
] {
assert!(parse(old).is_err(), "read as an angle: {:?}", parse(old));
}
}
#[test]
fn multi_line_formula() {
let row = cat(&[
s("y="),
n(paren(cat(&[s("x+1")]))),
n(sup(s("2"))),
n(Node::Break),
s("=x"),
n(sup(s("2"))),
s("+2x+1"),
n(Node::Break),
n(frac(s("a"), s("b"))),
]);
roundtrip("multi-line", &row);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(
aa.lines().filter(|l| l.trim_end() == "┈").count(),
2,
"two separator rows:\n{}",
aa
);
assert!(
row_to_latex(&normalize(&row)).contains("\\\\"),
"latex line break"
);
roundtrip(
"empty-line",
&cat(&[s("a"), n(Node::Break), n(Node::Break), s("b")]),
);
}
#[test]
fn cardano_formula() {
let discriminant = sqrt(cat(&[
n(frac(cat(&[s("q"), n(sup(s("2")))]), s("4"))),
s("+"),
n(frac(cat(&[s("p"), n(sup(s("3")))]), s("27"))),
]));
let half_q = |sign: &str| {
cat(&[
s(sign),
n(frac(s("q"), s("2"))),
s(if sign == "-" { "+" } else { "-" }),
n(discriminant.clone()),
])
};
let row = cat(&[s("t="), n(cbrt(half_q("-"))), s("+"), n(cbrt(half_q("")))]);
roundtrip("cardano", &row);
}
#[test]
fn cauchy_schwarz_inequality() {
let sum = |body: Row| {
n(bigop('∑', s("k=1"), s("n")))
.into_iter()
.chain(body)
.collect::<Row>()
};
let row = cat(&[
n(paren(sum(cat(&[
s("u"),
n(sub(s("k"))),
n(acc(Accent::Bar, 'v')),
n(sub(s("k"))),
])))),
n(sup(s("2"))),
s("≤"),
n(paren(sum(cat(&[s("u"), n(sub(s("k"))), n(sup(s("2")))])))),
n(paren(sum(cat(&[s("v"), n(sub(s("k"))), n(sup(s("2")))])))),
]);
roundtrip("cauchy-schwarz", &row);
}
#[test]
fn vandermonde_determinant() {
let x = |i: &str, p: Option<&str>| -> Row {
let mut row = cat(&[s("x"), n(sub(s(i)))]);
if let Some(p) = p {
row.push(sup(s(p)));
}
row
};
let m = mat(
4,
5,
vec![
s("1"),
x("1", None),
x("1", Some("2")),
s("⋯"),
x("1", Some("n-1")),
s("1"),
x("2", None),
x("2", Some("2")),
s("⋯"),
x("2", Some("n-1")),
s("⋮"),
s("⋮"),
s("⋮"),
s("⋱"),
s("⋮"),
s("1"),
x("n", None),
x("n", Some("2")),
s("⋯"),
x("n", Some("n-1")),
],
);
let row = cat(&[
n(m),
s("="),
n(bigop('∏', s("1≤i<j≤n"), vec![])),
n(paren(cat(&[
s("x"),
n(sub(s("j"))),
s("-"),
s("x"),
n(sub(s("i"))),
]))),
]);
roundtrip("vandermonde", &row);
}
#[test]
fn gaussian_integral() {
let row = cat(&[
n(bigop('∫', s("-∞"), s("∞"))),
s("e"),
n(sup(cat(&[s("-x"), n(sup(s("2")))]))),
s("dx="),
n(sqrt(s("π"))),
]);
roundtrip("gaussian", &row);
}
#[test]
fn schroedinger_equation() {
let row = cat(&[
s("iℏ"),
n(frac(s("∂Ψ"), s("∂t"))),
s("=-"),
n(frac(cat(&[s("ℏ"), n(sup(s("2")))]), s("2m"))),
n(frac(
cat(&[s("∂"), n(sup(s("2"))), s("Ψ")]),
cat(&[s("∂x"), n(sup(s("2")))]),
)),
s("+V"),
n(paren(s("x"))),
s("Ψ"),
]);
roundtrip("schroedinger", &row);
}
#[test]
fn gauss_law() {
let row = cat(&[
n(bigop('∮', vec![], vec![])),
n(acc(Accent::Vec, 'E')),
s("⋅d"),
n(acc(Accent::Vec, 'A')),
s("="),
n(frac(s("Q"), cat(&[s("ε"), n(sub(s("0")))]))),
]);
roundtrip("gauss-law", &row);
}
#[test]
fn cauchy_integral_formula() {
let row = cat(&[
s("f"),
n(paren(s("a"))),
s("="),
n(frac(s("1"), s("2πi"))),
n(bigop('∮', vec![], vec![])),
n(frac(cat(&[s("f"), n(paren(s("z")))]), cat(&[s("z-a")]))),
s("dz"),
]);
roundtrip("cauchy-integral", &row);
}
#[test]
fn euler_identity() {
let row = cat(&[s("e"), n(sup(s("iπ"))), s("+1=0")]);
roundtrip("euler", &row);
}
#[test]
fn triple_integral_band() {
let row = cat(&[n(bigop('∭', s("Ω"), vec![])), s("∇⋅F␣dV")]);
roundtrip("triple-integral", &row);
}
#[test]
fn norm_grid_and_bare_array_roundtrip() {
let row = vec![Node::Norm {
arg: vec![array(2, 2, vec![s("a"), s("b"), vec![], s("d")])],
}];
roundtrip("vmatrix", &row);
assert_eq!(
row_to_latex(&normalize(&row)),
"\\begin{Vmatrix} a & b \\\\ & d \\end{Vmatrix}"
);
let row = cat(&[s("x+"), n(array(1, 2, vec![s("a"), s("b")])), s("=y")]);
roundtrip("bare-array-mid-row", &row);
}
#[test]
fn amssymb_tier_atoms() {
roundtrip("amssymb-relations", &s("a≼b⪅c⋆d⊓e⟹a≺e"));
roundtrip("greek-variants", &s("ϵ≠ε␣ϕ≠φ␣ϰ≠κ"));
}
#[test]
fn normal_pdf() {
let row = cat(&[
s("f"),
n(paren(s("x"))),
s("="),
n(frac(s("1"), n(sqrt(cat(&[s("2πσ"), n(sup(s("2")))]))))),
s("e"),
n(sup(cat(&[
s("-"),
n(frac(
cat(&[n(paren(s("x-μ"))), n(sup(s("2")))]),
cat(&[s("2σ"), n(sup(s("2")))]),
)),
]))),
]);
roundtrip("normal-pdf", &row);
}
#[test]
fn variance() {
let row = cat(&[
s("σ"),
n(sup(s("2"))),
s("="),
n(frac(s("1"), s("n"))),
n(bigop('∑', s("i=1"), s("n"))),
n(paren(cat(&[s("x"), n(sub(s("i"))), s("-μ")]))),
n(sup(s("2"))),
]);
roundtrip("variance", &row);
}
#[test]
fn bayes_theorem() {
let p = |arg: &str| cat(&[s("P"), n(paren(s(arg)))]);
let row = cat(&[p("A|B"), s("="), n(frac(cat(&[p("B|A"), p("A")]), p("B")))]);
roundtrip("bayes", &row);
}
#[test]
fn rotation_matrix() {
let row = cat(&[
s("R="),
n(mat(
2,
2,
vec![
cat(&[n(func("cos")), s("θ")]),
cat(&[s("-"), n(func("sin")), s("θ")]),
cat(&[n(func("sin")), s("θ")]),
cat(&[n(func("cos")), s("θ")]),
],
)),
]);
roundtrip("rotation", &row);
}
#[test]
fn matrix_exponential() {
let row = cat(&[
s("e"),
n(sup(cat(&[
n(mat(2, 2, vec![s("0"), s("1"), s("-1"), s("0")])),
s("t"),
]))),
]);
roundtrip("matrix-exponential", &row);
}
#[test]
fn nested_matrices() {
let inner = mat(2, 2, vec![s("a"), s("b"), s("c"), s("d")]);
let row = n(mat(
2,
2,
vec![
n(frac(s("1"), s("2"))),
s("0"),
n(inner),
cat(&[s("x"), n(sup(s("2")))]),
],
));
roundtrip("nested-matrices", &row);
}
#[test]
fn strike_overlays_are_rejected() {
for aa in ["x\u{338}", "\u{338}", "a\u{336}b", "\"x\u{338}\""] {
let e = parse(aa).expect_err("overlay must be rejected");
assert!(
e.to_string().contains("strike overlays"),
"pointed message for {:?}: {}",
aa,
e
);
}
}
#[test]
fn bare_one_char_func_is_final() {
let row = vec![Node::Func("1".into()), Node::Func(".".into())];
roundtrip("bare-one-char-func", &row);
}
#[test]
fn delimiter_blocks() {
let abs = |r: Row| delim('|', '|', vec![], vec![r]);
roundtrip("abs", &cat(&[n(abs(s("-x"))), s("="), n(abs(s("x")))]));
roundtrip(
"braket",
&n(delim(
'⟨',
'⟩',
vec!['|', '|'],
vec![s("ψ"), s("H"), s("ψ")],
)),
);
roundtrip(
"set-builder",
&n(delim(
'{',
'}',
vec!['|'],
vec![
s("x"),
cat(&[s("x"), n(sup(s("2"))), s(">"), n(frac(s("1"), s("2")))]),
],
)),
);
roundtrip(
"cases",
&cat(&[
n(abs(s("x"))),
s("="),
n(delim(
'{',
'.',
vec![],
vec![n(array(2, 2, vec![s("x"), s("x≥0"), s("-x"), s("x<0")]))],
)),
]),
);
roundtrip(
"bare-array",
&n(array(2, 2, vec![s("a"), s("b"), s("c"), s("d")])),
);
roundtrip(
"adjacent-lattices",
&cat(&[
n(array(2, 1, vec![s("a"), s("b")])),
s("x"),
n(array(2, 1, vec![s("c"), s("d")])),
]),
);
roundtrip(
"lattice-in-sup",
&cat(&[s("e"), n(sup(n(array(1, 2, vec![s("0"), s("t")]))))]),
);
roundtrip(
"null-delim-grid",
&n(delim(
'.',
'.',
vec![],
vec![n(array(2, 2, vec![s("a"), s("b"), s("c"), s("d")]))],
)),
);
roundtrip(
"pmatrix",
&n(delim(
'(',
')',
vec![],
vec![n(array(1, 2, vec![s("a+b"), s("c")]))],
)),
);
roundtrip("interval", &n(delim('(', ']', vec![], vec![s("0,1")])));
roundtrip(
"nested-delims",
&n(delim(
'{',
'}',
vec![],
vec![n(delim('⟨', '⟩', vec!['|'], vec![s("u"), n(abs(s("v")))]))],
)),
);
}
#[test]
fn explicit_space_atoms() {
let row = cat(&[s("f"), s("␣"), n(paren(s("x"))), s("␣␣"), s("dx")]);
roundtrip("space-atoms", &row);
let row = cat(&[
n(mat(1, 2, vec![cat(&[s("a"), s("␣"), s("b")]), s("c")])),
s("␣"),
n(frac(s("␣"), s("x"))),
]);
roundtrip("space-in-matrix", &row);
}
#[test]
fn braces_over_under() {
let brace = |over, arg: Row, label: Row| Node::Brace { over, arg, label };
roundtrip(
"overbrace",
&cat(&[
n(brace(true, cat(&[s("a"), s("+"), s("b")]), s("n"))),
s("+"),
n(brace(false, s("c"), s("m"))),
]),
);
roundtrip(
"brace-tall",
&cat(&[
s("x"),
s("<"),
n(brace(
true,
cat(&[n(frac(s("1"), s("2"))), s("+y")]),
vec![],
)),
]),
);
}
#[test]
fn text_runs() {
roundtrip(
"mathrm-dx",
&cat(&[
n(bigop('∫', vec![], vec![])),
s("f"),
n(paren(s("x"))),
n(Node::Func("dx".into())),
]),
);
roundtrip(
"text-otherwise",
&n(delim(
'{',
'.',
vec![],
vec![n(array(
2,
2,
vec![
s("x"),
s("x≥0"),
s("-x"),
n(Node::Text("other wise".into())),
],
))],
)),
);
}
#[test]
fn labeled_arrows() {
let arrow = |op: Arrow, over: Row, under: Row| Node::Arrow { op, over, under };
roundtrip(
"xrightarrow",
&cat(&[s("A"), n(arrow(Arrow::To, s("f"), vec![])), s("B")]),
);
roundtrip(
"xarrow-both",
&cat(&[
s("X"),
n(arrow(Arrow::To, cat(&[s("g"), n(sup(s("2")))]), s("n→∞"))),
s("Y"),
n(arrow(Arrow::From, vec![], s("h"))),
s("Z"),
]),
);
roundtrip(
"double-arrows",
&cat(&[
n(arrow(Arrow::DoubleTo, s("f"), vec![])),
n(arrow(Arrow::DoubleFrom, vec![], s("g"))),
]),
);
roundtrip(
"frac-then-arrow-atom",
&cat(&[n(frac(s("1"), s("2"))), s("→"), n(frac(s("3"), s("4")))]),
);
roundtrip(
"adjacent-arrows",
&cat(&[
n(arrow(Arrow::From, s("a"), vec![])),
n(arrow(Arrow::To, s("b"), vec![])),
]),
);
}
#[test]
fn stacked_accents() {
let row = cat(&[
n(Node::Accent {
overs: vec![Accent::Vec, Accent::Hat],
unders: vec![],
base: 'a',
}),
s("+"),
n(Node::Accent {
overs: vec![Accent::Bar],
unders: vec![Accent::Underline],
base: 'x',
}),
]);
roundtrip("stacked-accents", &row);
let row = cat(&[
n(Node::Accent {
overs: vec![Accent::Dot, Accent::Bar, Accent::Hat],
unders: vec![],
base: 'v',
}),
n(frac(s("1"), s("2"))),
]);
roundtrip("triple-accent", &row);
let row = cat(&[
n(Node::Accent {
overs: vec![Accent::Bar],
unders: vec![],
base: 'x',
}),
n(Node::Accent {
overs: vec![Accent::Hat],
unders: vec![],
base: 'v',
}),
n(Node::Accent {
overs: vec![Accent::Tilde],
unders: vec![],
base: 'w',
}),
n(Node::Accent {
overs: vec![Accent::Check],
unders: vec![],
base: 'c',
}),
n(Node::Accent {
overs: vec![Accent::Ring],
unders: vec![],
base: 'r',
}),
n(Node::Accent {
overs: vec![Accent::Dot],
unders: vec![],
base: 'd',
}),
n(Node::Accent {
overs: vec![Accent::Ddot],
unders: vec![],
base: 'e',
}),
n(Node::Accent {
overs: vec![Accent::Vec],
unders: vec![],
base: 'u',
}),
n(Node::Accent {
overs: vec![],
unders: vec![Accent::Underline],
base: 'y',
}),
]);
let aa = render_root(&normalize(&row), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "_˰˷˯˳․․․⇾\n𝑥𝑣𝑤𝑐𝑟𝑑𝑒 𝑢𝑦\n ¯");
roundtrip("hugging-marks", &row);
let row = cat(&[
n(Node::Accent {
overs: vec![Accent::Ddot, Accent::Vec],
unders: vec![],
base: 'E',
}),
n(Node::Roman('e')),
]);
roundtrip("ddot-then-mathrm", &row);
let row = cat(&[
n(Node::Accent {
overs: vec![Accent::Ddot],
unders: vec![],
base: 'x',
}),
n(Node::Accent {
overs: vec![Accent::Ddot],
unders: vec![],
base: 'y',
}),
n(Node::Sup {
arg: vec![Node::Sym('.')],
}),
]);
roundtrip("ddot-chain-sup-dot", &row);
let row = cat(&[
n(Node::Sqrt {
arg: vec![],
index: Radical::Sqrt,
}),
n(Node::Accent {
overs: vec![Accent::Bar],
unders: vec![],
base: 'x',
}),
]);
roundtrip("sqrt-then-bar", &row);
}
#[test]
fn formatting_spacers() {
let sp = || Node::Spacer;
let row = cat(&[
s("f"),
n(sp()),
n(paren(s("x"))),
n(sp()),
n(sp()),
n(frac(cat(&[s("1"), n(sp()), s("+"), s("x")]), s("2"))),
]);
roundtrip("spacers", &row);
let row = cat(&[
n(sp()),
s("x"),
n(sup(s("a"))),
n(sp()),
n(sup(s("b"))),
n(sp()),
]);
roundtrip("spacers-scripts", &row);
}
#[test]
fn spacing_survives_the_roundtrip() {
for aa in ["𝑎 + 𝑏", "𝑎 + 𝑏", "𝑓(𝑥) = 𝑎𝑥 + 𝑏", "𝑥² + 2𝑥 + 1"]
{
let row = parse(aa).unwrap_or_else(|e| panic!("parse failed: {} ({})", e, aa));
let back = render_root(&row, None, &RenderCtx::canonical()).to_text();
assert_eq!(back, aa, "spacing was rewritten");
}
let glued = cat(&[n(func("sin")), n(Node::Spacer), n(func("cos"))]);
let aa = render_root(&normalize(&glued), None, &RenderCtx::canonical()).to_text();
assert_eq!(aa, "sin cos");
assert_eq!(
parse(&aa).unwrap(),
vec![Node::Func("sin".into()), Node::Func("cos".into())],
"the separating blank came back as a spacer too"
);
}
#[test]
fn continued_fraction() {
let mut row = s("x");
for _ in 0..4 {
row = cat(&[s("1+"), n(frac(s("1"), row))]);
}
roundtrip("continued-fraction", &row);
}
#[test]
fn limit_functions() {
let row = cat(&[
n(Node::BigOp {
name: "lim".into(),
lower: cat(&[s("x"), s("→"), s("0")]),
upper: vec![],
}),
s("f"),
n(paren(s("x"))),
]);
roundtrip("lim", &row);
let row = cat(&[
n(Node::BigOp {
name: "argmax".into(),
lower: s("x∈S"),
upper: vec![],
}),
s("f"),
n(paren(s("x"))),
]);
roundtrip("argmax", &row);
let row = vec![Node::BigOpSym {
op: '∮',
lower: vec![],
upper: vec![],
}];
roundtrip("bare-op", &row);
}
#[test]
fn nested_limits() {
let row = cat(&[
n(bigop(
'∑',
cat(&[
s("i∈"),
n(bigop('⋃', s("k"), vec![])),
s("S"),
n(sub(s("k"))),
]),
n(frac(s("n"), s("2"))),
)),
s("a"),
n(sub(s("i"))),
]);
roundtrip("nested-limits", &row);
}
#[test]
fn parses_handwritten_2d_input() {
let aa = r#"
2
x + 1
x = ────────
√2π
"#;
let aa = aa.replace("√2π", "2π"); let row = parse(&aa).unwrap();
assert_eq!(row_to_latex(&row), "x=\\frac{x^{2}+1}{2\\pi }");
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545F4914F6CDD1D)
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
}
const ATOMS: &[char] = &[
'a', 'b', 'c', 'x', 'y', 'z', 'A', 'B', 'N', '0', '1', '2', '7', '+', '-', '=', '<', 'α', 'β',
'π', 'λ', '∞', '∂', '⋅', '±', '∈', '→', '␣', '∼', '′', '.', '%', '&',
];
fn gen_row(rng: &mut Rng, depth: usize, max_len: usize) -> Row {
let len = rng.below(max_len + 1);
(0..len).map(|_| gen_node(rng, depth)).collect()
}
fn gen_node(rng: &mut Rng, depth: usize) -> Node {
let structural = depth > 0 && rng.below(100) < 45;
if !structural {
return match rng.below(10) {
0 => Node::Func(["sin", "cos", "log", "exp"][rng.below(4)].into()),
1 => {
let marks = [
Accent::Hat,
Accent::Bar,
Accent::Dot,
Accent::Ddot,
Accent::Vec,
Accent::Tilde,
Accent::Underline,
Accent::Utilde,
];
let base = ['x', 'v', 'a', 'E'][rng.below(4)];
let (mut overs, mut unders) = (vec![], vec![]);
for _ in 0..1 + rng.below(2) {
let m = marks[rng.below(marks.len())];
if m.under() {
unders.push(m)
} else {
overs.push(m)
}
}
Node::Accent {
overs,
unders,
base,
}
}
2 => Node::Spacer,
3 => match rng.below(6) {
0 => Node::Roman('d'),
1 => Node::Roman('e'),
2 => Node::Roman('D'),
3 => Node::Func("i.i.d.".into()),
4 => Node::Func("w.r.t".into()),
_ => Node::Func("a.e".into()),
},
_ => Node::Sym(ATOMS[rng.below(ATOMS.len())]),
};
}
let d = depth - 1;
match rng.below(14) {
0 => Node::Frac {
num: gen_row(rng, d, 3),
den: gen_row(rng, d, 3),
},
1 => Node::Sqrt {
arg: gen_row(rng, d, 3),
index: [Radical::Sqrt, Radical::Sqrt, Radical::Cbrt, Radical::Qdrt][rng.below(4)],
},
2 => Node::Sup {
arg: gen_row(rng, d, 2),
},
3 => Node::Sub {
arg: gen_row(rng, d, 2),
},
4 => {
let (lower, upper) = (gen_row(rng, d, 3), gen_row(rng, d, 2));
match rng.below(5) {
0 => opname(["lim", "Tr"][rng.below(2)], lower, upper),
1 => opname("max", lower, upper),
2 => opname("argmax", lower, upper),
3 => opname("esssup", lower, upper),
_ => Node::BigOpSym {
op: ['∑', '∏', '∫', '⋃'][rng.below(4)],
lower,
upper,
},
}
}
5 => {
let pairs = [
('(', ')'),
('[', ']'),
('{', '}'),
('⟨', '⟩'),
('|', '|'),
('.', '.'),
('(', ']'),
('{', '.'),
('.', '}'),
('⌈', '⌉'),
('⌊', '⌋'),
];
let (l, r) = pairs[rng.below(pairs.len())];
let nsegs = 1 + rng.below(2); let segs = (0..nsegs).map(|_| gen_row(rng, d, 3)).collect::<Vec<_>>();
delim(l, r, vec!['|'; nsegs - 1], segs)
}
7 => Node::Norm {
arg: gen_row(rng, d, 3),
},
13 => {
let overs = [
Accent::Hat,
Accent::Tilde,
Accent::Bar,
Accent::Vec,
Accent::Dot,
Accent::Ddot,
Accent::Check,
Accent::Ring,
];
let base: Row = if rng.below(3) == 0 {
gen_row(rng, d, 3)
} else {
(0..1 + rng.below(3))
.map(|_| Node::Sym(ATOMS[rng.below(ATOMS.len())]))
.collect()
};
let unders = [Accent::Underline, Accent::Utilde];
let pick = |rng: &mut Rng, pool: &[Accent], n: usize| -> Vec<Accent> {
let mut v = Vec::new();
for _ in 0..n {
v.push(pool[rng.below(pool.len())]);
}
v
};
let (no, nu) = match rng.below(4) {
0 => (1, 0),
1 => (0, 1),
2 => (1, 1),
_ => (1 + rng.below(2), rng.below(2)),
};
Node::WideAccent {
overs: pick(rng, &overs, no),
unders: pick(rng, &unders, nu),
base,
}
}
6 => {
let pairs = [('[', ']'), ('[', ']'), ('(', ')'), ('.', '.'), ('{', '.')];
let (l, r) = pairs[rng.below(pairs.len())];
let (rows, cols) = [(2, 2), (1, 2), (2, 1), (1, 1)][rng.below(4)];
let cells = (0..rows * cols).map(|_| gen_row(rng, d, 2)).collect();
delim(l, r, vec![], vec![vec![Node::Array { rows, cols, cells }]])
}
8 => {
let (rows, cols) = [(2, 2), (1, 2), (1, 1), (3, 2), (1, 3)][rng.below(5)];
let cells = (0..rows * cols).map(|_| gen_row(rng, d, 2)).collect();
Node::Array { rows, cols, cells }
}
10 => {
if rng.below(2) == 0 {
Node::Func(["dx", "abc", "T", "sin"][rng.below(4)].into())
} else {
Node::Text(["if", "if x", "a\"b", "x\\y"][rng.below(4)].into())
}
}
11 => Node::Brace {
over: rng.below(2) == 0,
arg: gen_row(rng, d, 3),
label: gen_row(rng, d, 2),
},
9 => Node::Arrow {
op: Arrow::ALL[rng.below(Arrow::ALL.len())],
over: gen_row(rng, d, 3),
under: gen_row(rng, d, 2),
},
_ => Node::Sym(ATOMS[rng.below(ATOMS.len())]),
}
}
#[test]
fn property_random_asts_roundtrip() {
let n: usize = std::env::var("FORMULAA_PROP_N")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2000);
let seed: u64 = std::env::var("FORMULAA_PROP_SEED")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0x8bad_f00d_dead_beef);
let mut rng = Rng(seed);
for i in 0..n {
let depth = 1 + rng.below(4);
let mut row = gen_row(&mut rng, depth, 5);
for _ in 0..rng.below(3) {
row.push(Node::Break);
let d = 1 + rng.below(3);
row.extend(gen_row(&mut rng, d, 4));
}
if i == 4201 {
eprintln!("RAW-4201 {:?}", row);
}
roundtrip(&format!("random-{}", i), &row);
}
}