use crate::ast::{Expression, Variable};
use serde_json::{json, Value as JsonValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Verbosity {
Minimal,
#[default]
Standard,
Detailed,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PathStatistics {
pub total_steps: usize,
pub operation_counts: OperationCounts,
pub unique_operations: usize,
pub uses_advanced_methods: bool,
pub uses_calculus: bool,
pub uses_matrix_operations: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OperationCounts {
pub both_sides: usize,
pub transformations: usize,
pub variable_ops: usize,
pub identities: usize,
pub advanced: usize,
pub calculus: usize,
pub matrix: usize,
pub custom: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResolutionPath {
pub initial: Expression,
pub steps: Vec<ResolutionStep>,
pub result: Expression,
}
impl ResolutionPath {
pub fn new(initial: Expression) -> Self {
Self {
initial: initial.clone(),
steps: Vec::new(),
result: initial,
}
}
pub fn add_step(&mut self, step: ResolutionStep) {
self.steps.push(step);
}
pub fn set_result(&mut self, result: Expression) {
self.result = result;
}
pub fn step_count(&self) -> usize {
self.steps.len()
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn explain(&self) -> String {
format!("Solution path with {} steps", self.steps.len())
}
pub fn to_text(&self, verbosity: Verbosity) -> String {
let mut output = String::new();
match verbosity {
Verbosity::Minimal => {
output.push_str(&format!("Start: {:?}\n", self.initial));
for (i, step) in self.steps.iter().enumerate() {
if step.operation.is_key_operation() {
output.push_str(&format!(
"Step {}: {}\n",
i + 1,
step.operation.describe()
));
}
}
output.push_str(&format!("Result: {:?}\n", self.result));
}
Verbosity::Standard => {
output.push_str(&format!("Initial: {:?}\n\n", self.initial));
for (i, step) in self.steps.iter().enumerate() {
output.push_str(&format!("Step {}: {}\n", i + 1, step.operation.describe()));
output.push_str(&format!(" → {:?}\n\n", step.result));
}
output.push_str(&format!("Final result: {:?}\n", self.result));
}
Verbosity::Detailed => {
output.push_str(&format!("=== Solution Path ===\n\n"));
output.push_str(&format!("Starting expression: {:?}\n\n", self.initial));
for (i, step) in self.steps.iter().enumerate() {
output.push_str(&format!("--- Step {} ---\n", i + 1));
output.push_str(&format!("Operation: {}\n", step.operation.describe()));
output.push_str(&format!("Explanation: {}\n", step.explanation));
output.push_str(&format!("Result: {:?}\n\n", step.result));
}
output.push_str(&format!("=== Final Result ===\n"));
output.push_str(&format!("{:?}\n", self.result));
}
}
output
}
pub fn to_latex(&self, verbosity: Verbosity) -> String {
let mut output = String::new();
output.push_str("\\begin{align*}\n");
match verbosity {
Verbosity::Minimal => {
output.push_str(&format!(" & {} \\\\\n", self.initial.to_latex()));
for step in &self.steps {
if step.operation.is_key_operation() {
output.push_str(&format!(
" &\\quad \\text{{{}}} \\\\\n",
step.operation.describe_latex()
));
output.push_str(&format!(" &= {} \\\\\n", step.result.to_latex()));
}
}
}
Verbosity::Standard => {
output.push_str(&format!(" & {} \\\\\n", self.initial.to_latex()));
for step in &self.steps {
output.push_str(&format!(
" &\\quad \\text{{{}}} \\\\\n",
step.operation.describe_latex()
));
output.push_str(&format!(" &= {} \\\\\n", step.result.to_latex()));
}
}
Verbosity::Detailed => {
output.push_str(&format!(
" & \\text{{Initial: }} {} \\\\\n",
self.initial.to_latex()
));
for (i, step) in self.steps.iter().enumerate() {
output.push_str(&format!(
" &\\quad \\text{{Step {}: {}}} \\\\\n",
i + 1,
step.operation.describe_latex()
));
output.push_str(&format!(
" &\\quad \\text{{({}}})\\\\\n",
escape_latex_text(&step.explanation)
));
output.push_str(&format!(" &= {} \\\\\n", step.result.to_latex()));
}
}
}
output.push_str("\\end{align*}\n");
output
}
pub fn to_json(&self) -> JsonValue {
let steps: Vec<JsonValue> = self
.steps
.iter()
.enumerate()
.map(|(i, step)| {
json!({
"step_number": i + 1,
"operation": step.operation.describe(),
"operation_category": step.operation.category(),
"explanation": step.explanation,
"result": format!("{:?}", step.result),
"result_latex": step.result.to_latex(),
})
})
.collect();
let stats = self.statistics();
json!({
"initial": format!("{:?}", self.initial),
"initial_latex": self.initial.to_latex(),
"steps": steps,
"result": format!("{:?}", self.result),
"result_latex": self.result.to_latex(),
"step_count": self.steps.len(),
"statistics": {
"total_steps": stats.total_steps,
"unique_operations": stats.unique_operations,
"uses_advanced_methods": stats.uses_advanced_methods,
"uses_calculus": stats.uses_calculus,
"uses_matrix_operations": stats.uses_matrix_operations,
"operation_counts": {
"both_sides": stats.operation_counts.both_sides,
"transformations": stats.operation_counts.transformations,
"variable_ops": stats.operation_counts.variable_ops,
"identities": stats.operation_counts.identities,
"advanced": stats.operation_counts.advanced,
"calculus": stats.operation_counts.calculus,
"matrix": stats.operation_counts.matrix,
"custom": stats.operation_counts.custom,
}
}
})
}
pub fn statistics(&self) -> PathStatistics {
let mut counts = OperationCounts::default();
let mut unique_ops: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut uses_advanced = false;
let mut uses_calculus = false;
let mut uses_matrix = false;
for step in &self.steps {
let category = step.operation.category();
unique_ops.insert(step.operation.describe());
match category.as_str() {
"both_sides" => counts.both_sides += 1,
"transformation" => counts.transformations += 1,
"variable" => counts.variable_ops += 1,
"identity" => counts.identities += 1,
"advanced" => {
counts.advanced += 1;
uses_advanced = true;
}
"calculus" => {
counts.calculus += 1;
uses_calculus = true;
}
"matrix" => {
counts.matrix += 1;
uses_matrix = true;
}
"custom" => counts.custom += 1,
_ => {}
}
}
PathStatistics {
total_steps: self.steps.len(),
operation_counts: counts,
unique_operations: unique_ops.len(),
uses_advanced_methods: uses_advanced,
uses_calculus,
uses_matrix_operations: uses_matrix,
}
}
}
fn escape_latex_text(text: &str) -> String {
text.replace('\\', "\\textbackslash{}")
.replace('{', "\\{")
.replace('}', "\\}")
.replace('$', "\\$")
.replace('%', "\\%")
.replace('&', "\\&")
.replace('#', "\\#")
.replace('_', "\\_")
.replace('^', "\\^{}")
.replace('~', "\\~{}")
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResolutionStep {
pub operation: Operation,
pub explanation: String,
pub result: Expression,
}
impl ResolutionStep {
pub fn new(operation: Operation, explanation: String, result: Expression) -> Self {
Self {
operation,
explanation,
result,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Operation {
AddBothSides(Expression),
SubtractBothSides(Expression),
MultiplyBothSides(Expression),
DivideBothSides(Expression),
PowerBothSides(Expression),
RootBothSides(Expression),
ApplyFunction(String),
Simplify,
Expand,
Factor,
CombineFractions,
Cancel,
Substitute {
variable: Variable,
value: Expression,
},
Isolate(Variable),
MoveTerm(Expression),
ApplyIdentity(String),
ApplyTrigIdentity(String),
ApplyLogProperty(String),
QuadraticFormula,
CompleteSquare,
NumericalApproximation,
Differentiate {
variable: Variable,
rule: String,
},
Integrate {
variable: Variable,
technique: String,
},
EvaluateLimit {
variable: Variable,
approaches: Expression,
method: String,
},
IntegrationByParts {
u: Expression,
dv: Expression,
},
USubstitution {
substitution: Expression,
},
SolveODE {
method: String,
},
MatrixOperation {
operation: String,
},
GaussianElimination,
ComputeDeterminant {
method: String,
},
ApproximationSubstitution {
original: Expression,
approximation: Expression,
error_bound: f64,
},
Custom(String),
}
impl Operation {
pub fn describe(&self) -> String {
match self {
Operation::AddBothSides(expr) => format!("Add {:?} to both sides", expr),
Operation::SubtractBothSides(expr) => format!("Subtract {:?} from both sides", expr),
Operation::MultiplyBothSides(expr) => format!("Multiply both sides by {:?}", expr),
Operation::DivideBothSides(expr) => format!("Divide both sides by {:?}", expr),
Operation::PowerBothSides(expr) => format!("Raise both sides to power {:?}", expr),
Operation::RootBothSides(expr) => format!("Take {:?} root of both sides", expr),
Operation::ApplyFunction(func) => format!("Apply {} to both sides", func),
Operation::Simplify => "Simplify expression".to_string(),
Operation::Expand => "Expand expression".to_string(),
Operation::Factor => "Factor expression".to_string(),
Operation::CombineFractions => "Combine fractions".to_string(),
Operation::Cancel => "Cancel common factors".to_string(),
Operation::Substitute { variable, value } => {
format!("Substitute {} = {:?}", variable, value)
}
Operation::Isolate(var) => format!("Isolate {}", var),
Operation::MoveTerm(expr) => format!("Move {:?} to other side", expr),
Operation::ApplyIdentity(name) => format!("Apply identity: {}", name),
Operation::ApplyTrigIdentity(name) => format!("Apply trig identity: {}", name),
Operation::ApplyLogProperty(name) => format!("Apply log property: {}", name),
Operation::QuadraticFormula => "Apply quadratic formula".to_string(),
Operation::CompleteSquare => "Complete the square".to_string(),
Operation::NumericalApproximation => "Use numerical approximation".to_string(),
Operation::Differentiate { variable, rule } => {
format!("Differentiate with respect to {} ({})", variable, rule)
}
Operation::Integrate {
variable,
technique,
} => {
format!("Integrate with respect to {} ({})", variable, technique)
}
Operation::EvaluateLimit {
variable,
approaches,
method,
} => {
format!(
"Evaluate limit as {} → {:?} ({})",
variable, approaches, method
)
}
Operation::IntegrationByParts { u, dv } => {
format!("Integration by parts: u = {:?}, dv = {:?}", u, dv)
}
Operation::USubstitution { substitution } => {
format!("U-substitution: u = {:?}", substitution)
}
Operation::SolveODE { method } => format!("Solve ODE ({})", method),
Operation::MatrixOperation { operation } => {
format!("Matrix operation: {}", operation)
}
Operation::GaussianElimination => "Apply Gaussian elimination".to_string(),
Operation::ComputeDeterminant { method } => {
format!("Compute determinant ({})", method)
}
Operation::ApproximationSubstitution {
original,
approximation,
error_bound,
} => {
format!(
"Approximate {:?} ≈ {:?} (error bound: {:.2e})",
original, approximation, error_bound
)
}
Operation::Custom(desc) => desc.clone(),
}
}
pub fn describe_latex(&self) -> String {
match self {
Operation::AddBothSides(expr) => {
format!("Add {} to both sides", expr.to_latex())
}
Operation::SubtractBothSides(expr) => {
format!("Subtract {} from both sides", expr.to_latex())
}
Operation::MultiplyBothSides(expr) => {
format!("Multiply both sides by {}", expr.to_latex())
}
Operation::DivideBothSides(expr) => {
format!("Divide both sides by {}", expr.to_latex())
}
Operation::PowerBothSides(expr) => {
format!("Raise both sides to power {}", expr.to_latex())
}
Operation::RootBothSides(expr) => {
format!("Take {} root of both sides", expr.to_latex())
}
Operation::Substitute { variable, value } => {
format!("Substitute {} = {}", variable, value.to_latex())
}
Operation::MoveTerm(expr) => {
format!("Move {} to other side", expr.to_latex())
}
Operation::IntegrationByParts { u, dv } => {
format!(
"Integration by parts: u = {}, dv = {}",
u.to_latex(),
dv.to_latex()
)
}
Operation::USubstitution { substitution } => {
format!("U-substitution: u = {}", substitution.to_latex())
}
Operation::EvaluateLimit {
variable,
approaches,
method,
} => {
format!(
"Evaluate limit as {} \\to {} ({})",
variable,
approaches.to_latex(),
method
)
}
_ => self.describe(),
}
}
pub fn category(&self) -> String {
match self {
Operation::AddBothSides(_)
| Operation::SubtractBothSides(_)
| Operation::MultiplyBothSides(_)
| Operation::DivideBothSides(_)
| Operation::PowerBothSides(_)
| Operation::RootBothSides(_)
| Operation::ApplyFunction(_) => "both_sides".to_string(),
Operation::Simplify
| Operation::Expand
| Operation::Factor
| Operation::CombineFractions
| Operation::Cancel => "transformation".to_string(),
Operation::Substitute { .. } | Operation::Isolate(_) | Operation::MoveTerm(_) => {
"variable".to_string()
}
Operation::ApplyIdentity(_)
| Operation::ApplyTrigIdentity(_)
| Operation::ApplyLogProperty(_) => "identity".to_string(),
Operation::QuadraticFormula
| Operation::CompleteSquare
| Operation::NumericalApproximation => "advanced".to_string(),
Operation::Differentiate { .. }
| Operation::Integrate { .. }
| Operation::EvaluateLimit { .. }
| Operation::IntegrationByParts { .. }
| Operation::USubstitution { .. }
| Operation::SolveODE { .. } => "calculus".to_string(),
Operation::MatrixOperation { .. }
| Operation::GaussianElimination
| Operation::ComputeDeterminant { .. } => "matrix".to_string(),
Operation::ApproximationSubstitution { .. } => "approximation".to_string(),
Operation::Custom(_) => "custom".to_string(),
}
}
pub fn is_key_operation(&self) -> bool {
match self {
Operation::Simplify | Operation::Cancel => false,
_ => true,
}
}
}
#[derive(Clone)]
pub struct ResolutionPathBuilder {
path: ResolutionPath,
}
impl ResolutionPathBuilder {
pub fn new(initial: Expression) -> Self {
Self {
path: ResolutionPath::new(initial),
}
}
pub fn step(mut self, operation: Operation, explanation: String, result: Expression) -> Self {
self.path
.add_step(ResolutionStep::new(operation, explanation, result));
self
}
pub fn simplify(self, explanation: String, result: Expression) -> Self {
self.step(Operation::Simplify, explanation, result)
}
pub fn isolate(self, variable: Variable, explanation: String, result: Expression) -> Self {
self.step(Operation::Isolate(variable), explanation, result)
}
pub fn finish(mut self, result: Expression) -> ResolutionPath {
self.path.set_result(result);
self.path
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Expression;
#[test]
fn test_verbosity_default() {
let verbosity = Verbosity::default();
assert_eq!(verbosity, Verbosity::Standard);
}
#[test]
fn test_to_text_minimal() {
let mut path = ResolutionPath::new(Expression::Integer(10));
path.add_step(ResolutionStep::new(
Operation::Simplify,
"Simplify".to_string(),
Expression::Integer(10),
));
path.add_step(ResolutionStep::new(
Operation::DivideBothSides(Expression::Integer(2)),
"Divide by 2".to_string(),
Expression::Integer(5),
));
path.set_result(Expression::Integer(5));
let text = path.to_text(Verbosity::Minimal);
assert!(text.contains("Start:"));
assert!(text.contains("Result:"));
assert!(text.contains("Divide"));
assert!(!text.contains("Simplify"));
}
#[test]
fn test_to_text_standard() {
let mut path = ResolutionPath::new(Expression::Integer(10));
path.add_step(ResolutionStep::new(
Operation::SubtractBothSides(Expression::Integer(3)),
"Subtract 3".to_string(),
Expression::Integer(7),
));
path.set_result(Expression::Integer(7));
let text = path.to_text(Verbosity::Standard);
assert!(text.contains("Initial:"));
assert!(text.contains("Step 1:"));
assert!(text.contains("Subtract"));
assert!(text.contains("Final result:"));
}
#[test]
fn test_to_text_detailed() {
let mut path = ResolutionPath::new(Expression::Integer(10));
path.add_step(ResolutionStep::new(
Operation::AddBothSides(Expression::Integer(5)),
"Add 5 to isolate the term".to_string(),
Expression::Integer(15),
));
path.set_result(Expression::Integer(15));
let text = path.to_text(Verbosity::Detailed);
assert!(text.contains("=== Solution Path ==="));
assert!(text.contains("Starting expression:"));
assert!(text.contains("--- Step 1 ---"));
assert!(text.contains("Operation:"));
assert!(text.contains("Explanation:"));
assert!(text.contains("Add 5 to isolate the term"));
assert!(text.contains("=== Final Result ==="));
}
#[test]
fn test_to_latex_standard() {
let mut path = ResolutionPath::new(Expression::Integer(10));
path.add_step(ResolutionStep::new(
Operation::DivideBothSides(Expression::Integer(2)),
"Divide by 2".to_string(),
Expression::Integer(5),
));
path.set_result(Expression::Integer(5));
let latex = path.to_latex(Verbosity::Standard);
assert!(latex.contains("\\begin{align*}"));
assert!(latex.contains("\\end{align*}"));
assert!(latex.contains("\\text{"));
}
#[test]
fn test_to_json() {
let mut path = ResolutionPath::new(Expression::Integer(20));
path.add_step(ResolutionStep::new(
Operation::Simplify,
"Combine like terms".to_string(),
Expression::Integer(20),
));
path.add_step(ResolutionStep::new(
Operation::QuadraticFormula,
"Apply quadratic formula".to_string(),
Expression::Integer(5),
));
path.set_result(Expression::Integer(5));
let json = path.to_json();
assert!(json["steps"].is_array());
assert_eq!(json["step_count"], 2);
assert_eq!(json["statistics"]["total_steps"], 2);
assert_eq!(json["statistics"]["uses_advanced_methods"], true);
assert_eq!(json["statistics"]["operation_counts"]["transformations"], 1);
assert_eq!(json["statistics"]["operation_counts"]["advanced"], 1);
}
#[test]
fn test_statistics() {
let mut path = ResolutionPath::new(Expression::Integer(100));
path.add_step(ResolutionStep::new(
Operation::AddBothSides(Expression::Integer(5)),
"Add".to_string(),
Expression::Integer(105),
));
path.add_step(ResolutionStep::new(
Operation::Simplify,
"Simplify".to_string(),
Expression::Integer(105),
));
path.add_step(ResolutionStep::new(
Operation::Factor,
"Factor".to_string(),
Expression::Integer(105),
));
path.add_step(ResolutionStep::new(
Operation::Isolate(Variable::new("x")),
"Isolate x".to_string(),
Expression::Integer(21),
));
path.add_step(ResolutionStep::new(
Operation::QuadraticFormula,
"Apply quadratic formula".to_string(),
Expression::Integer(7),
));
let stats = path.statistics();
assert_eq!(stats.total_steps, 5);
assert_eq!(stats.operation_counts.both_sides, 1);
assert_eq!(stats.operation_counts.transformations, 2); assert_eq!(stats.operation_counts.variable_ops, 1);
assert_eq!(stats.operation_counts.advanced, 1);
assert!(stats.uses_advanced_methods);
assert!(!stats.uses_calculus);
assert!(!stats.uses_matrix_operations);
}
#[test]
fn test_statistics_calculus() {
let mut path = ResolutionPath::new(Expression::Integer(0));
path.add_step(ResolutionStep::new(
Operation::Differentiate {
variable: Variable::new("x"),
rule: "power rule".to_string(),
},
"Differentiate".to_string(),
Expression::Integer(0),
));
path.add_step(ResolutionStep::new(
Operation::Integrate {
variable: Variable::new("x"),
technique: "substitution".to_string(),
},
"Integrate".to_string(),
Expression::Integer(0),
));
let stats = path.statistics();
assert!(stats.uses_calculus);
assert_eq!(stats.operation_counts.calculus, 2);
}
#[test]
fn test_statistics_matrix() {
let mut path = ResolutionPath::new(Expression::Integer(0));
path.add_step(ResolutionStep::new(
Operation::GaussianElimination,
"Apply Gaussian elimination".to_string(),
Expression::Integer(0),
));
path.add_step(ResolutionStep::new(
Operation::ComputeDeterminant {
method: "cofactor expansion".to_string(),
},
"Compute determinant".to_string(),
Expression::Integer(0),
));
let stats = path.statistics();
assert!(stats.uses_matrix_operations);
assert_eq!(stats.operation_counts.matrix, 2);
}
#[test]
fn test_operation_category() {
assert_eq!(
Operation::AddBothSides(Expression::Integer(1)).category(),
"both_sides"
);
assert_eq!(Operation::Simplify.category(), "transformation");
assert_eq!(
Operation::Isolate(Variable::new("x")).category(),
"variable"
);
assert_eq!(
Operation::ApplyIdentity("difference of squares".to_string()).category(),
"identity"
);
assert_eq!(Operation::QuadraticFormula.category(), "advanced");
assert_eq!(
Operation::Differentiate {
variable: Variable::new("x"),
rule: "power".to_string()
}
.category(),
"calculus"
);
assert_eq!(Operation::GaussianElimination.category(), "matrix");
assert_eq!(
Operation::Custom("custom op".to_string()).category(),
"custom"
);
}
#[test]
fn test_is_key_operation() {
assert!(!Operation::Simplify.is_key_operation());
assert!(!Operation::Cancel.is_key_operation());
assert!(Operation::QuadraticFormula.is_key_operation());
assert!(Operation::DivideBothSides(Expression::Integer(2)).is_key_operation());
assert!(Operation::Factor.is_key_operation());
assert!(Operation::GaussianElimination.is_key_operation());
}
#[test]
fn test_describe_latex() {
let op = Operation::AddBothSides(Expression::Integer(5));
let latex = op.describe_latex();
assert!(latex.contains("Add"));
assert!(latex.contains("5"));
assert!(latex.contains("both sides"));
let op2 = Operation::QuadraticFormula;
assert_eq!(op2.describe_latex(), "Apply quadratic formula");
}
#[test]
fn test_operation_counts_default() {
let counts = OperationCounts::default();
assert_eq!(counts.both_sides, 0);
assert_eq!(counts.transformations, 0);
assert_eq!(counts.variable_ops, 0);
assert_eq!(counts.identities, 0);
assert_eq!(counts.advanced, 0);
assert_eq!(counts.calculus, 0);
assert_eq!(counts.matrix, 0);
assert_eq!(counts.custom, 0);
}
#[test]
fn test_empty_path_statistics() {
let path = ResolutionPath::new(Expression::Integer(42));
let stats = path.statistics();
assert_eq!(stats.total_steps, 0);
assert_eq!(stats.unique_operations, 0);
assert!(!stats.uses_advanced_methods);
assert!(!stats.uses_calculus);
assert!(!stats.uses_matrix_operations);
}
#[test]
fn test_unique_operations_count() {
let mut path = ResolutionPath::new(Expression::Integer(10));
path.add_step(ResolutionStep::new(
Operation::Simplify,
"First simplify".to_string(),
Expression::Integer(10),
));
path.add_step(ResolutionStep::new(
Operation::Simplify,
"Second simplify".to_string(),
Expression::Integer(10),
));
path.add_step(ResolutionStep::new(
Operation::Factor,
"Factor".to_string(),
Expression::Integer(10),
));
let stats = path.statistics();
assert_eq!(stats.total_steps, 3);
assert_eq!(stats.unique_operations, 2);
}
#[test]
fn test_escape_latex_text() {
let text = "Use $x_1$ and {braces} with 50% & 100#";
let escaped = escape_latex_text(text);
assert!(escaped.contains("\\$"));
assert!(escaped.contains("\\_"));
assert!(escaped.contains("\\{"));
assert!(escaped.contains("\\}"));
assert!(escaped.contains("\\%"));
assert!(escaped.contains("\\&"));
assert!(escaped.contains("\\#"));
}
}