Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
596
examples/scipix/tests/unit/math_tests.rs
Normal file
596
examples/scipix/tests/unit/math_tests.rs
Normal file
@@ -0,0 +1,596 @@
|
||||
// Math parsing tests for ruvector-scipix
|
||||
//
|
||||
// Tests symbol recognition, AST construction, and LaTeX/MathML/AsciiMath generation
|
||||
// for various mathematical expressions including fractions, roots, matrices, integrals, etc.
|
||||
// Target: 90%+ coverage of math parsing module
|
||||
|
||||
#[cfg(test)]
|
||||
mod math_tests {
|
||||
// Mock math structures for testing
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum MathSymbol {
|
||||
// Numbers
|
||||
Digit(char),
|
||||
|
||||
// Variables
|
||||
Variable(char),
|
||||
|
||||
// Greek letters
|
||||
Alpha,
|
||||
Beta,
|
||||
Gamma,
|
||||
Delta,
|
||||
Epsilon,
|
||||
Pi,
|
||||
Sigma,
|
||||
Omega,
|
||||
|
||||
// Operators
|
||||
Plus,
|
||||
Minus,
|
||||
Times,
|
||||
Divide,
|
||||
Equals,
|
||||
|
||||
// Relations
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
LessEqual,
|
||||
GreaterEqual,
|
||||
NotEqual,
|
||||
|
||||
// Special symbols
|
||||
Infinity,
|
||||
Partial,
|
||||
Nabla,
|
||||
Integral,
|
||||
Sum,
|
||||
Product,
|
||||
Root,
|
||||
Sqrt,
|
||||
|
||||
// Brackets
|
||||
LeftParen,
|
||||
RightParen,
|
||||
LeftBracket,
|
||||
RightBracket,
|
||||
LeftBrace,
|
||||
RightBrace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum MathNode {
|
||||
Number(String),
|
||||
Variable(String),
|
||||
Symbol(MathSymbol),
|
||||
BinaryOp {
|
||||
op: String,
|
||||
left: Box<MathNode>,
|
||||
right: Box<MathNode>,
|
||||
},
|
||||
Fraction {
|
||||
numerator: Box<MathNode>,
|
||||
denominator: Box<MathNode>,
|
||||
},
|
||||
Superscript {
|
||||
base: Box<MathNode>,
|
||||
exponent: Box<MathNode>,
|
||||
},
|
||||
Subscript {
|
||||
base: Box<MathNode>,
|
||||
index: Box<MathNode>,
|
||||
},
|
||||
Root {
|
||||
degree: Option<Box<MathNode>>,
|
||||
radicand: Box<MathNode>,
|
||||
},
|
||||
Matrix {
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
elements: Vec<Vec<MathNode>>,
|
||||
},
|
||||
Integral {
|
||||
lower: Option<Box<MathNode>>,
|
||||
upper: Option<Box<MathNode>>,
|
||||
integrand: Box<MathNode>,
|
||||
},
|
||||
Summation {
|
||||
lower: Option<Box<MathNode>>,
|
||||
upper: Option<Box<MathNode>>,
|
||||
term: Box<MathNode>,
|
||||
},
|
||||
}
|
||||
|
||||
impl MathNode {
|
||||
fn to_latex(&self) -> String {
|
||||
match self {
|
||||
Self::Number(n) => n.clone(),
|
||||
Self::Variable(v) => v.clone(),
|
||||
Self::Symbol(MathSymbol::Plus) => "+".to_string(),
|
||||
Self::Symbol(MathSymbol::Minus) => "-".to_string(),
|
||||
Self::Symbol(MathSymbol::Times) => r"\times".to_string(),
|
||||
Self::Symbol(MathSymbol::Divide) => r"\div".to_string(),
|
||||
Self::Symbol(MathSymbol::Pi) => r"\pi".to_string(),
|
||||
Self::Symbol(MathSymbol::Alpha) => r"\alpha".to_string(),
|
||||
Self::Symbol(MathSymbol::Infinity) => r"\infty".to_string(),
|
||||
Self::BinaryOp { op, left, right } => {
|
||||
format!("{} {} {}", left.to_latex(), op, right.to_latex())
|
||||
}
|
||||
Self::Fraction {
|
||||
numerator,
|
||||
denominator,
|
||||
} => {
|
||||
format!(r"\frac{{{}}}{{{}}}", numerator.to_latex(), denominator.to_latex())
|
||||
}
|
||||
Self::Superscript { base, exponent } => {
|
||||
format!("{}^{{{}}}", base.to_latex(), exponent.to_latex())
|
||||
}
|
||||
Self::Subscript { base, index } => {
|
||||
format!("{}_{{{}}}", base.to_latex(), index.to_latex())
|
||||
}
|
||||
Self::Root { degree: None, radicand } => {
|
||||
format!(r"\sqrt{{{}}}", radicand.to_latex())
|
||||
}
|
||||
Self::Root { degree: Some(n), radicand } => {
|
||||
format!(r"\sqrt[{{{}}}]{{{}}}", n.to_latex(), radicand.to_latex())
|
||||
}
|
||||
Self::Matrix { elements, .. } => {
|
||||
let mut result = r"\begin{bmatrix}".to_string();
|
||||
for (i, row) in elements.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push_str(r" \\ ");
|
||||
}
|
||||
for (j, elem) in row.iter().enumerate() {
|
||||
if j > 0 {
|
||||
result.push_str(" & ");
|
||||
}
|
||||
result.push_str(&elem.to_latex());
|
||||
}
|
||||
}
|
||||
result.push_str(r" \end{bmatrix}");
|
||||
result
|
||||
}
|
||||
Self::Integral { lower, upper, integrand } => {
|
||||
let mut result = r"\int".to_string();
|
||||
if let Some(l) = lower {
|
||||
result.push_str(&format!("_{{{}}}", l.to_latex()));
|
||||
}
|
||||
if let Some(u) = upper {
|
||||
result.push_str(&format!("^{{{}}}", u.to_latex()));
|
||||
}
|
||||
result.push_str(&format!(" {} dx", integrand.to_latex()));
|
||||
result
|
||||
}
|
||||
Self::Summation { lower, upper, term } => {
|
||||
let mut result = r"\sum".to_string();
|
||||
if let Some(l) = lower {
|
||||
result.push_str(&format!("_{{{}}}", l.to_latex()));
|
||||
}
|
||||
if let Some(u) = upper {
|
||||
result.push_str(&format!("^{{{}}}", u.to_latex()));
|
||||
}
|
||||
result.push_str(&format!(" {}", term.to_latex()));
|
||||
result
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_mathml(&self) -> String {
|
||||
match self {
|
||||
Self::Number(n) => format!("<mn>{}</mn>", n),
|
||||
Self::Variable(v) => format!("<mi>{}</mi>", v),
|
||||
Self::BinaryOp { op, left, right } => {
|
||||
format!(
|
||||
"<mrow>{}<mo>{}</mo>{}</mrow>",
|
||||
left.to_mathml(),
|
||||
op,
|
||||
right.to_mathml()
|
||||
)
|
||||
}
|
||||
Self::Fraction {
|
||||
numerator,
|
||||
denominator,
|
||||
} => {
|
||||
format!(
|
||||
"<mfrac>{}{}</mfrac>",
|
||||
numerator.to_mathml(),
|
||||
denominator.to_mathml()
|
||||
)
|
||||
}
|
||||
Self::Superscript { base, exponent } => {
|
||||
format!(
|
||||
"<msup>{}{}</msup>",
|
||||
base.to_mathml(),
|
||||
exponent.to_mathml()
|
||||
)
|
||||
}
|
||||
Self::Root { degree: None, radicand } => {
|
||||
format!("<msqrt>{}</msqrt>", radicand.to_mathml())
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_asciimath(&self) -> String {
|
||||
match self {
|
||||
Self::Number(n) => n.clone(),
|
||||
Self::Variable(v) => v.clone(),
|
||||
Self::BinaryOp { op, left, right } => {
|
||||
format!("{} {} {}", left.to_asciimath(), op, right.to_asciimath())
|
||||
}
|
||||
Self::Fraction {
|
||||
numerator,
|
||||
denominator,
|
||||
} => {
|
||||
format!("({})/({})", numerator.to_asciimath(), denominator.to_asciimath())
|
||||
}
|
||||
Self::Superscript { base, exponent } => {
|
||||
format!("{}^{}", base.to_asciimath(), exponent.to_asciimath())
|
||||
}
|
||||
Self::Root { degree: None, radicand } => {
|
||||
format!("sqrt({})", radicand.to_asciimath())
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol_recognition_numbers() {
|
||||
let symbols = vec![
|
||||
MathSymbol::Digit('0'),
|
||||
MathSymbol::Digit('1'),
|
||||
MathSymbol::Digit('9'),
|
||||
];
|
||||
|
||||
for symbol in symbols {
|
||||
assert!(matches!(symbol, MathSymbol::Digit(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol_recognition_variables() {
|
||||
let symbols = vec![
|
||||
MathSymbol::Variable('x'),
|
||||
MathSymbol::Variable('y'),
|
||||
MathSymbol::Variable('z'),
|
||||
];
|
||||
|
||||
for symbol in symbols {
|
||||
assert!(matches!(symbol, MathSymbol::Variable(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol_recognition_greek() {
|
||||
let greeks = vec![
|
||||
(MathSymbol::Alpha, "α"),
|
||||
(MathSymbol::Beta, "β"),
|
||||
(MathSymbol::Gamma, "γ"),
|
||||
(MathSymbol::Delta, "δ"),
|
||||
(MathSymbol::Pi, "π"),
|
||||
(MathSymbol::Sigma, "Σ"),
|
||||
(MathSymbol::Omega, "Ω"),
|
||||
];
|
||||
|
||||
assert_eq!(greeks.len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol_recognition_operators() {
|
||||
let ops = vec![
|
||||
MathSymbol::Plus,
|
||||
MathSymbol::Minus,
|
||||
MathSymbol::Times,
|
||||
MathSymbol::Divide,
|
||||
MathSymbol::Equals,
|
||||
];
|
||||
|
||||
assert_eq!(ops.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ast_construction_simple_addition() {
|
||||
let expr = MathNode::BinaryOp {
|
||||
op: "+".to_string(),
|
||||
left: Box::new(MathNode::Variable("x".to_string())),
|
||||
right: Box::new(MathNode::Variable("y".to_string())),
|
||||
};
|
||||
|
||||
assert!(matches!(expr, MathNode::BinaryOp { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ast_construction_simple_multiplication() {
|
||||
let expr = MathNode::BinaryOp {
|
||||
op: "*".to_string(),
|
||||
left: Box::new(MathNode::Number("2".to_string())),
|
||||
right: Box::new(MathNode::Variable("x".to_string())),
|
||||
};
|
||||
|
||||
match expr {
|
||||
MathNode::BinaryOp { op, .. } => assert_eq!(op, "*"),
|
||||
_ => panic!("Expected BinaryOp"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_simple_addition() {
|
||||
let expr = MathNode::BinaryOp {
|
||||
op: "+".to_string(),
|
||||
left: Box::new(MathNode::Variable("x".to_string())),
|
||||
right: Box::new(MathNode::Variable("y".to_string())),
|
||||
};
|
||||
|
||||
let latex = expr.to_latex();
|
||||
assert_eq!(latex, "x + y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_fraction_simple() {
|
||||
let frac = MathNode::Fraction {
|
||||
numerator: Box::new(MathNode::Number("1".to_string())),
|
||||
denominator: Box::new(MathNode::Number("2".to_string())),
|
||||
};
|
||||
|
||||
let latex = frac.to_latex();
|
||||
assert_eq!(latex, r"\frac{1}{2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_fraction_variables() {
|
||||
let frac = MathNode::Fraction {
|
||||
numerator: Box::new(MathNode::Variable("a".to_string())),
|
||||
denominator: Box::new(MathNode::Variable("b".to_string())),
|
||||
};
|
||||
|
||||
let latex = frac.to_latex();
|
||||
assert_eq!(latex, r"\frac{a}{b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_fraction_complex() {
|
||||
let numerator = MathNode::BinaryOp {
|
||||
op: "+".to_string(),
|
||||
left: Box::new(MathNode::Variable("a".to_string())),
|
||||
right: Box::new(MathNode::Number("1".to_string())),
|
||||
};
|
||||
|
||||
let frac = MathNode::Fraction {
|
||||
numerator: Box::new(numerator),
|
||||
denominator: Box::new(MathNode::Variable("b".to_string())),
|
||||
};
|
||||
|
||||
let latex = frac.to_latex();
|
||||
assert_eq!(latex, r"\frac{a + 1}{b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_root_square() {
|
||||
let root = MathNode::Root {
|
||||
degree: None,
|
||||
radicand: Box::new(MathNode::Variable("x".to_string())),
|
||||
};
|
||||
|
||||
let latex = root.to_latex();
|
||||
assert_eq!(latex, r"\sqrt{x}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_root_nth() {
|
||||
let root = MathNode::Root {
|
||||
degree: Some(Box::new(MathNode::Number("3".to_string()))),
|
||||
radicand: Box::new(MathNode::Variable("x".to_string())),
|
||||
};
|
||||
|
||||
let latex = root.to_latex();
|
||||
assert_eq!(latex, r"\sqrt[{3}]{x}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_superscript() {
|
||||
let power = MathNode::Superscript {
|
||||
base: Box::new(MathNode::Variable("x".to_string())),
|
||||
exponent: Box::new(MathNode::Number("2".to_string())),
|
||||
};
|
||||
|
||||
let latex = power.to_latex();
|
||||
assert_eq!(latex, "x^{2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_subscript() {
|
||||
let sub = MathNode::Subscript {
|
||||
base: Box::new(MathNode::Variable("x".to_string())),
|
||||
index: Box::new(MathNode::Number("1".to_string())),
|
||||
};
|
||||
|
||||
let latex = sub.to_latex();
|
||||
assert_eq!(latex, "x_{1}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_subscript_and_superscript() {
|
||||
let base = MathNode::Variable("x".to_string());
|
||||
let with_sub = MathNode::Subscript {
|
||||
base: Box::new(base),
|
||||
index: Box::new(MathNode::Number("1".to_string())),
|
||||
};
|
||||
let with_both = MathNode::Superscript {
|
||||
base: Box::new(with_sub),
|
||||
exponent: Box::new(MathNode::Number("2".to_string())),
|
||||
};
|
||||
|
||||
let latex = with_both.to_latex();
|
||||
assert_eq!(latex, "x_{1}^{2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_matrix_2x2() {
|
||||
let matrix = MathNode::Matrix {
|
||||
rows: 2,
|
||||
cols: 2,
|
||||
elements: vec![
|
||||
vec![
|
||||
MathNode::Number("1".to_string()),
|
||||
MathNode::Number("2".to_string()),
|
||||
],
|
||||
vec![
|
||||
MathNode::Number("3".to_string()),
|
||||
MathNode::Number("4".to_string()),
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
let latex = matrix.to_latex();
|
||||
assert!(latex.contains(r"\begin{bmatrix}"));
|
||||
assert!(latex.contains(r"\end{bmatrix}"));
|
||||
assert!(latex.contains("1 & 2"));
|
||||
assert!(latex.contains("3 & 4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_matrix_3x3() {
|
||||
let matrix = MathNode::Matrix {
|
||||
rows: 3,
|
||||
cols: 3,
|
||||
elements: vec![
|
||||
vec![
|
||||
MathNode::Number("1".to_string()),
|
||||
MathNode::Number("2".to_string()),
|
||||
MathNode::Number("3".to_string()),
|
||||
],
|
||||
vec![
|
||||
MathNode::Number("4".to_string()),
|
||||
MathNode::Number("5".to_string()),
|
||||
MathNode::Number("6".to_string()),
|
||||
],
|
||||
vec![
|
||||
MathNode::Number("7".to_string()),
|
||||
MathNode::Number("8".to_string()),
|
||||
MathNode::Number("9".to_string()),
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
let latex = matrix.to_latex();
|
||||
assert!(latex.contains(r"\begin{bmatrix}"));
|
||||
assert!(latex.contains("1 & 2 & 3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_integral_simple() {
|
||||
let integral = MathNode::Integral {
|
||||
lower: None,
|
||||
upper: None,
|
||||
integrand: Box::new(MathNode::Variable("x".to_string())),
|
||||
};
|
||||
|
||||
let latex = integral.to_latex();
|
||||
assert!(latex.contains(r"\int"));
|
||||
assert!(latex.contains("x dx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_integral_with_limits() {
|
||||
let integral = MathNode::Integral {
|
||||
lower: Some(Box::new(MathNode::Number("0".to_string()))),
|
||||
upper: Some(Box::new(MathNode::Number("1".to_string()))),
|
||||
integrand: Box::new(MathNode::Variable("x".to_string())),
|
||||
};
|
||||
|
||||
let latex = integral.to_latex();
|
||||
assert!(latex.contains(r"\int_{0}^{1}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latex_generation_summation() {
|
||||
let sum = MathNode::Summation {
|
||||
lower: Some(Box::new(MathNode::BinaryOp {
|
||||
op: "=".to_string(),
|
||||
left: Box::new(MathNode::Variable("i".to_string())),
|
||||
right: Box::new(MathNode::Number("1".to_string())),
|
||||
})),
|
||||
upper: Some(Box::new(MathNode::Variable("n".to_string()))),
|
||||
term: Box::new(MathNode::Variable("i".to_string())),
|
||||
};
|
||||
|
||||
let latex = sum.to_latex();
|
||||
assert!(latex.contains(r"\sum"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mathml_generation_number() {
|
||||
let num = MathNode::Number("42".to_string());
|
||||
let mathml = num.to_mathml();
|
||||
assert_eq!(mathml, "<mn>42</mn>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mathml_generation_variable() {
|
||||
let var = MathNode::Variable("x".to_string());
|
||||
let mathml = var.to_mathml();
|
||||
assert_eq!(mathml, "<mi>x</mi>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mathml_generation_fraction() {
|
||||
let frac = MathNode::Fraction {
|
||||
numerator: Box::new(MathNode::Number("1".to_string())),
|
||||
denominator: Box::new(MathNode::Number("2".to_string())),
|
||||
};
|
||||
|
||||
let mathml = frac.to_mathml();
|
||||
assert!(mathml.contains("<mfrac>"));
|
||||
assert!(mathml.contains("<mn>1</mn>"));
|
||||
assert!(mathml.contains("<mn>2</mn>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mathml_generation_superscript() {
|
||||
let power = MathNode::Superscript {
|
||||
base: Box::new(MathNode::Variable("x".to_string())),
|
||||
exponent: Box::new(MathNode::Number("2".to_string())),
|
||||
};
|
||||
|
||||
let mathml = power.to_mathml();
|
||||
assert!(mathml.contains("<msup>"));
|
||||
assert!(mathml.contains("<mi>x</mi>"));
|
||||
assert!(mathml.contains("<mn>2</mn>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asciimath_generation_simple() {
|
||||
let expr = MathNode::BinaryOp {
|
||||
op: "+".to_string(),
|
||||
left: Box::new(MathNode::Variable("x".to_string())),
|
||||
right: Box::new(MathNode::Number("1".to_string())),
|
||||
};
|
||||
|
||||
let ascii = expr.to_asciimath();
|
||||
assert_eq!(ascii, "x + 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asciimath_generation_fraction() {
|
||||
let frac = MathNode::Fraction {
|
||||
numerator: Box::new(MathNode::Variable("a".to_string())),
|
||||
denominator: Box::new(MathNode::Variable("b".to_string())),
|
||||
};
|
||||
|
||||
let ascii = frac.to_asciimath();
|
||||
assert_eq!(ascii, "(a)/(b)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asciimath_generation_power() {
|
||||
let power = MathNode::Superscript {
|
||||
base: Box::new(MathNode::Variable("x".to_string())),
|
||||
exponent: Box::new(MathNode::Number("2".to_string())),
|
||||
};
|
||||
|
||||
let ascii = power.to_asciimath();
|
||||
assert_eq!(ascii, "x^2");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user