Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
465
vendor/ruvector/examples/scipix/src/math/asciimath.rs
vendored
Normal file
465
vendor/ruvector/examples/scipix/src/math/asciimath.rs
vendored
Normal file
@@ -0,0 +1,465 @@
|
||||
//! AsciiMath generation from mathematical AST
|
||||
//!
|
||||
//! This module converts mathematical AST nodes to AsciiMath notation,
|
||||
//! a simplified plain-text format for mathematical expressions.
|
||||
|
||||
use crate::math::ast::{BinaryOp, BracketType, LargeOpType, MathExpr, MathNode, UnaryOp};
|
||||
|
||||
/// AsciiMath generator for mathematical expressions
|
||||
pub struct AsciiMathGenerator {
|
||||
/// Use Unicode symbols (true) or ASCII approximations (false)
|
||||
unicode: bool,
|
||||
}
|
||||
|
||||
impl AsciiMathGenerator {
|
||||
/// Create a new AsciiMath generator with Unicode support
|
||||
pub fn new() -> Self {
|
||||
Self { unicode: true }
|
||||
}
|
||||
|
||||
/// Create an ASCII-only generator
|
||||
pub fn ascii_only() -> Self {
|
||||
Self { unicode: false }
|
||||
}
|
||||
|
||||
/// Generate AsciiMath string from a mathematical expression
|
||||
pub fn generate(&self, expr: &MathExpr) -> String {
|
||||
self.generate_node(&expr.root, None)
|
||||
}
|
||||
|
||||
/// Generate AsciiMath for a single node
|
||||
fn generate_node(&self, node: &MathNode, parent_precedence: Option<u8>) -> String {
|
||||
match node {
|
||||
MathNode::Symbol { value, .. } => value.clone(),
|
||||
|
||||
MathNode::Number { value, .. } => value.clone(),
|
||||
|
||||
MathNode::Binary { op, left, right } => {
|
||||
let precedence = op.precedence();
|
||||
let needs_parens = parent_precedence.map_or(false, |p| precedence < p);
|
||||
|
||||
let left_str = self.generate_node(left, Some(precedence));
|
||||
let right_str = self.generate_node(
|
||||
right,
|
||||
Some(if op.is_left_associative() {
|
||||
precedence
|
||||
} else {
|
||||
precedence + 1
|
||||
}),
|
||||
);
|
||||
|
||||
let op_str = self.binary_op_to_asciimath(op);
|
||||
|
||||
let result = format!("{} {} {}", left_str, op_str, right_str);
|
||||
|
||||
if needs_parens {
|
||||
format!("({})", result)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::Unary { op, operand } => {
|
||||
let op_str = self.unary_op_to_asciimath(op);
|
||||
let operand_str = self.generate_node(operand, Some(70));
|
||||
format!("{}{}", op_str, operand_str)
|
||||
}
|
||||
|
||||
MathNode::Fraction {
|
||||
numerator,
|
||||
denominator,
|
||||
} => {
|
||||
let num_str = self.generate_node(numerator, None);
|
||||
let den_str = self.generate_node(denominator, None);
|
||||
format!("({})/({})", num_str, den_str)
|
||||
}
|
||||
|
||||
MathNode::Radical { index, radicand } => {
|
||||
let rad_str = self.generate_node(radicand, None);
|
||||
if let Some(idx) = index {
|
||||
let idx_str = self.generate_node(idx, None);
|
||||
format!("root({})({} )", idx_str, rad_str)
|
||||
} else {
|
||||
format!("sqrt({})", rad_str)
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::Script {
|
||||
base,
|
||||
subscript,
|
||||
superscript,
|
||||
} => {
|
||||
let base_str = self.generate_node(base, Some(65));
|
||||
let mut result = base_str;
|
||||
|
||||
if let Some(sub) = subscript {
|
||||
let sub_str = self.generate_node(sub, None);
|
||||
result.push_str(&format!("_{{{}}}", sub_str));
|
||||
}
|
||||
|
||||
if let Some(sup) = superscript {
|
||||
let sup_str = self.generate_node(sup, None);
|
||||
result.push_str(&format!("^{{{}}}", sup_str));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
MathNode::Function { name, argument } => {
|
||||
let arg_str = self.generate_node(argument, None);
|
||||
format!("{}({})", name, arg_str)
|
||||
}
|
||||
|
||||
MathNode::Matrix { rows, .. } => {
|
||||
let mut content = String::new();
|
||||
content.push('[');
|
||||
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
if i > 0 {
|
||||
content.push_str("; ");
|
||||
}
|
||||
for (j, elem) in row.iter().enumerate() {
|
||||
if j > 0 {
|
||||
content.push_str(", ");
|
||||
}
|
||||
content.push_str(&self.generate_node(elem, None));
|
||||
}
|
||||
}
|
||||
|
||||
content.push(']');
|
||||
content
|
||||
}
|
||||
|
||||
MathNode::Group {
|
||||
content,
|
||||
bracket_type,
|
||||
} => {
|
||||
let content_str = self.generate_node(content, None);
|
||||
let (open, close) = match bracket_type {
|
||||
BracketType::Parentheses => ("(", ")"),
|
||||
BracketType::Brackets => ("[", "]"),
|
||||
BracketType::Braces => ("{", "}"),
|
||||
BracketType::AngleBrackets => {
|
||||
if self.unicode {
|
||||
("⟨", "⟩")
|
||||
} else {
|
||||
("<", ">")
|
||||
}
|
||||
}
|
||||
BracketType::Vertical => ("|", "|"),
|
||||
BracketType::DoubleVertical => {
|
||||
if self.unicode {
|
||||
("‖", "‖")
|
||||
} else {
|
||||
("||", "||")
|
||||
}
|
||||
}
|
||||
BracketType::Floor => {
|
||||
if self.unicode {
|
||||
("⌊", "⌋")
|
||||
} else {
|
||||
("|_", "_|")
|
||||
}
|
||||
}
|
||||
BracketType::Ceiling => {
|
||||
if self.unicode {
|
||||
("⌈", "⌉")
|
||||
} else {
|
||||
("|^", "^|")
|
||||
}
|
||||
}
|
||||
BracketType::None => ("", ""),
|
||||
};
|
||||
|
||||
format!("{}{}{}", open, content_str, close)
|
||||
}
|
||||
|
||||
MathNode::LargeOp {
|
||||
op_type,
|
||||
lower,
|
||||
upper,
|
||||
content,
|
||||
} => {
|
||||
let op_str = self.large_op_to_asciimath(op_type);
|
||||
let content_str = self.generate_node(content, None);
|
||||
|
||||
let mut result = op_str.to_string();
|
||||
|
||||
if let Some(low) = lower {
|
||||
let low_str = self.generate_node(low, None);
|
||||
result.push_str(&format!("_{{{}}}", low_str));
|
||||
}
|
||||
|
||||
if let Some(up) = upper {
|
||||
let up_str = self.generate_node(up, None);
|
||||
result.push_str(&format!("^{{{}}}", up_str));
|
||||
}
|
||||
|
||||
format!("{} {}", result, content_str)
|
||||
}
|
||||
|
||||
MathNode::Sequence { elements } => elements
|
||||
.iter()
|
||||
.map(|e| self.generate_node(e, None))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
|
||||
MathNode::Text { content } => {
|
||||
format!("\"{}\"", content)
|
||||
}
|
||||
|
||||
MathNode::Empty => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert binary operator to AsciiMath
|
||||
fn binary_op_to_asciimath<'a>(&self, op: &'a BinaryOp) -> &'a str {
|
||||
if self.unicode {
|
||||
match op {
|
||||
BinaryOp::Add => "+",
|
||||
BinaryOp::Subtract => "-",
|
||||
BinaryOp::Multiply => "×",
|
||||
BinaryOp::Divide => "÷",
|
||||
BinaryOp::Power => "^",
|
||||
BinaryOp::Equal => "=",
|
||||
BinaryOp::NotEqual => "≠",
|
||||
BinaryOp::Less => "<",
|
||||
BinaryOp::Greater => ">",
|
||||
BinaryOp::LessEqual => "≤",
|
||||
BinaryOp::GreaterEqual => "≥",
|
||||
BinaryOp::ApproxEqual => "≈",
|
||||
BinaryOp::Equivalent => "≡",
|
||||
BinaryOp::Similar => "∼",
|
||||
BinaryOp::Congruent => "≅",
|
||||
BinaryOp::Proportional => "∝",
|
||||
BinaryOp::Custom(s) => s,
|
||||
}
|
||||
} else {
|
||||
match op {
|
||||
BinaryOp::Add => "+",
|
||||
BinaryOp::Subtract => "-",
|
||||
BinaryOp::Multiply => "*",
|
||||
BinaryOp::Divide => "/",
|
||||
BinaryOp::Power => "^",
|
||||
BinaryOp::Equal => "=",
|
||||
BinaryOp::NotEqual => "!=",
|
||||
BinaryOp::Less => "<",
|
||||
BinaryOp::Greater => ">",
|
||||
BinaryOp::LessEqual => "<=",
|
||||
BinaryOp::GreaterEqual => ">=",
|
||||
BinaryOp::ApproxEqual => "~~",
|
||||
BinaryOp::Equivalent => "-=",
|
||||
BinaryOp::Similar => "~",
|
||||
BinaryOp::Congruent => "~=",
|
||||
BinaryOp::Proportional => "prop",
|
||||
BinaryOp::Custom(s) => s.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert unary operator to AsciiMath
|
||||
fn unary_op_to_asciimath<'a>(&self, op: &'a UnaryOp) -> &'a str {
|
||||
match op {
|
||||
UnaryOp::Plus => "+",
|
||||
UnaryOp::Minus => "-",
|
||||
UnaryOp::Not => {
|
||||
if self.unicode {
|
||||
"¬"
|
||||
} else {
|
||||
"not "
|
||||
}
|
||||
}
|
||||
UnaryOp::Custom(s) => s.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert large operator to AsciiMath
|
||||
fn large_op_to_asciimath(&self, op: &LargeOpType) -> &str {
|
||||
if self.unicode {
|
||||
match op {
|
||||
LargeOpType::Sum => "∑",
|
||||
LargeOpType::Product => "∏",
|
||||
LargeOpType::Integral => "∫",
|
||||
LargeOpType::DoubleIntegral => "∬",
|
||||
LargeOpType::TripleIntegral => "∭",
|
||||
LargeOpType::ContourIntegral => "∮",
|
||||
LargeOpType::Union => "⋃",
|
||||
LargeOpType::Intersection => "⋂",
|
||||
LargeOpType::Coproduct => "∐",
|
||||
LargeOpType::DirectSum => "⊕",
|
||||
LargeOpType::Custom(_) => "sum",
|
||||
}
|
||||
} else {
|
||||
match op {
|
||||
LargeOpType::Sum => "sum",
|
||||
LargeOpType::Product => "prod",
|
||||
LargeOpType::Integral => "int",
|
||||
LargeOpType::DoubleIntegral => "iint",
|
||||
LargeOpType::TripleIntegral => "iiint",
|
||||
LargeOpType::ContourIntegral => "oint",
|
||||
LargeOpType::Union => "cup",
|
||||
LargeOpType::Intersection => "cap",
|
||||
LargeOpType::Coproduct => "coprod",
|
||||
LargeOpType::DirectSum => "oplus",
|
||||
LargeOpType::Custom(_) => "sum",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AsciiMathGenerator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_number() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Number {
|
||||
value: "42".to_string(),
|
||||
is_decimal: false,
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = AsciiMathGenerator::new();
|
||||
assert_eq!(gen.generate(&expr), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addition() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Binary {
|
||||
op: BinaryOp::Add,
|
||||
left: Box::new(MathNode::Number {
|
||||
value: "1".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
right: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = AsciiMathGenerator::new();
|
||||
assert_eq!(gen.generate(&expr), "1 + 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Fraction {
|
||||
numerator: Box::new(MathNode::Number {
|
||||
value: "1".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
denominator: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = AsciiMathGenerator::new();
|
||||
assert_eq!(gen.generate(&expr), "(1)/(2)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sqrt() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Radical {
|
||||
index: None,
|
||||
radicand: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = AsciiMathGenerator::new();
|
||||
assert_eq!(gen.generate(&expr), "sqrt(2)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superscript() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Script {
|
||||
base: Box::new(MathNode::Symbol {
|
||||
value: "x".to_string(),
|
||||
unicode: Some('x'),
|
||||
}),
|
||||
subscript: None,
|
||||
superscript: Some(Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
})),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = AsciiMathGenerator::new();
|
||||
assert_eq!(gen.generate(&expr), "x^{2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_vs_ascii() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Binary {
|
||||
op: BinaryOp::Multiply,
|
||||
left: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
right: Box::new(MathNode::Number {
|
||||
value: "3".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
|
||||
let gen_unicode = AsciiMathGenerator::new();
|
||||
assert_eq!(gen_unicode.generate(&expr), "2 × 3");
|
||||
|
||||
let gen_ascii = AsciiMathGenerator::ascii_only();
|
||||
assert_eq!(gen_ascii.generate(&expr), "2 * 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matrix() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Matrix {
|
||||
rows: vec![
|
||||
vec![
|
||||
MathNode::Number {
|
||||
value: "1".to_string(),
|
||||
is_decimal: false,
|
||||
},
|
||||
MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
MathNode::Number {
|
||||
value: "3".to_string(),
|
||||
is_decimal: false,
|
||||
},
|
||||
MathNode::Number {
|
||||
value: "4".to_string(),
|
||||
is_decimal: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
bracket_type: BracketType::Brackets,
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = AsciiMathGenerator::new();
|
||||
assert_eq!(gen.generate(&expr), "[1, 2; 3, 4]");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user