Skip to main content

ries_rs/
match_summary.rs

1//! Shared match presentation fields for binding surfaces.
2
3use crate::search::Match;
4use crate::solver::{canonical_expression_key, solve_for_x_rhs_expression};
5use crate::thresholds::EXACT_MATCH_TOLERANCE;
6
7/// Presentation-ready fields derived from a search [`Match`].
8///
9/// Binding surfaces (WASM, Python) convert this into their own types with
10/// trivial field copies, keeping derived-field logic in one place.
11#[derive(Clone, Debug)]
12pub struct MatchSummary {
13    /// Left-hand side expression in infix notation
14    pub lhs: String,
15    /// Right-hand side expression in infix notation
16    pub rhs: String,
17    /// Left-hand side expression in postfix notation
18    pub lhs_postfix: String,
19    /// Right-hand side expression in postfix notation
20    pub rhs_postfix: String,
21    /// Solved `x = expression` in infix notation, when analytically solvable
22    pub solve_for_x: Option<String>,
23    /// Solved `x = expression` in postfix notation
24    pub solve_for_x_postfix: Option<String>,
25    /// Canonical key for deduplication
26    pub canonical_key: String,
27    /// Solved value of x
28    pub x_value: f64,
29    /// Error (x_value - target)
30    pub error: f64,
31    /// Complexity score
32    pub complexity: u32,
33    /// Number of operators in the equation
34    pub operator_count: usize,
35    /// Maximum tree depth of the equation
36    pub tree_depth: usize,
37    /// Whether this is an exact match
38    pub is_exact: bool,
39}
40
41impl From<Match> for MatchSummary {
42    fn from(m: Match) -> Self {
43        Self::from_match(&m)
44    }
45}
46
47impl MatchSummary {
48    /// Build presentation fields from a search match.
49    pub fn from_match(m: &Match) -> Self {
50        let lhs_infix = m.lhs.expr.to_infix_or_postfix();
51        let rhs_infix = m.rhs.expr.to_infix_or_postfix();
52
53        let solved = solve_for_x_rhs_expression(&m.lhs.expr, &m.rhs.expr);
54        let solve_for_x = solved
55            .as_ref()
56            .map(|e| format!("x = {}", e.to_infix_or_postfix()));
57        let solve_for_x_postfix = solved.as_ref().map(|e| e.to_postfix());
58
59        let canonical_key = canonical_expression_key(&m.lhs.expr)
60            .zip(canonical_expression_key(&m.rhs.expr))
61            .map(|(l, r)| format!("{l}={r}"))
62            .unwrap_or_else(|| format!("{}={}", m.lhs.expr.to_postfix(), m.rhs.expr.to_postfix()));
63
64        Self {
65            lhs: lhs_infix,
66            rhs: rhs_infix,
67            lhs_postfix: m.lhs.expr.to_postfix(),
68            rhs_postfix: m.rhs.expr.to_postfix(),
69            solve_for_x,
70            solve_for_x_postfix,
71            canonical_key,
72            x_value: m.x_value,
73            error: m.error,
74            complexity: m.complexity,
75            operator_count: m.lhs.expr.operator_count() + m.rhs.expr.operator_count(),
76            tree_depth: m.lhs.expr.tree_depth().max(m.rhs.expr.tree_depth()),
77            is_exact: m.error.abs() < EXACT_MATCH_TOLERANCE,
78        }
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::expr::{EvaluatedExpr, Expression};
86    use crate::symbol::NumType;
87
88    fn make_match(lhs: &str, rhs: &str, error: f64) -> Match {
89        let lhs_expr = Expression::parse(lhs).unwrap();
90        let rhs_expr = Expression::parse(rhs).unwrap();
91        let complexity = lhs_expr.complexity() + rhs_expr.complexity();
92        Match {
93            lhs: EvaluatedExpr::new(lhs_expr, 0.0, 1.0, NumType::Integer),
94            rhs: EvaluatedExpr::new(rhs_expr, 0.0, 0.0, NumType::Integer),
95            x_value: 2.5,
96            error,
97            complexity,
98        }
99    }
100
101    #[test]
102    fn match_summary_populates_core_fields() {
103        let summary = MatchSummary::from_match(&make_match("x1+", "3", 0.0));
104        assert_eq!(summary.lhs_postfix, "x1+");
105        assert_eq!(summary.rhs_postfix, "3");
106        assert!(summary.is_exact);
107        assert!(summary.operator_count > 0);
108        assert!(summary.tree_depth > 0);
109        assert!(!summary.canonical_key.is_empty());
110    }
111}