Skip to main content

shape_runtime/
fuzzy_property.rs

1//! Property-specific fuzzy matching for Shape
2//!
3//! This module provides fine-grained fuzzy matching that allows different
4//! tolerances for different row properties.
5
6use shape_ast::error::{Result, ShapeError};
7use std::collections::HashMap;
8
9/// Property-specific tolerance configuration
10#[derive(Debug, Clone)]
11pub struct PropertyTolerances {
12    /// Default tolerance for unspecified properties
13    pub default: f64,
14    /// Property-specific tolerances
15    pub properties: HashMap<String, f64>,
16}
17
18impl Default for PropertyTolerances {
19    fn default() -> Self {
20        let mut properties = HashMap::new();
21
22        // Default tolerances for common row properties
23        properties.insert("body".to_string(), 0.02); // 2% for body size
24        properties.insert("upper_wick".to_string(), 0.05); // 5% for upper wick
25        properties.insert("lower_wick".to_string(), 0.05); // 5% for lower wick
26        properties.insert("range".to_string(), 0.03); // 3% for high-low range
27        properties.insert("open".to_string(), 0.01); // 1% for open price
28        properties.insert("close".to_string(), 0.01); // 1% for close price
29        properties.insert("high".to_string(), 0.01); // 1% for high price
30        properties.insert("low".to_string(), 0.01); // 1% for low price
31        properties.insert("volume".to_string(), 0.10); // 10% for volume
32
33        Self {
34            default: 0.02,
35            properties,
36        }
37    }
38}
39
40impl PropertyTolerances {
41    /// Get tolerance for a specific property
42    pub fn get(&self, property: &str) -> f64 {
43        self.properties
44            .get(property)
45            .copied()
46            .unwrap_or(self.default)
47    }
48
49    /// Set tolerance for a specific property
50    pub fn set(&mut self, property: String, tolerance: f64) -> Result<()> {
51        if !(0.0..=1.0).contains(&tolerance) {
52            return Err(ShapeError::RuntimeError {
53                message: format!("Tolerance must be between 0.0 and 1.0, got {}", tolerance),
54                location: None,
55            });
56        }
57        self.properties.insert(property, tolerance);
58        Ok(())
59    }
60
61    /// Parse from annotation arguments
62    /// @fuzzy(0.02) - default tolerance
63    /// @fuzzy(body: 0.02, wick: 0.05) - property-specific
64    pub fn from_annotation_args(args: &[shape_ast::ast::Expr]) -> Result<Self> {
65        let mut tolerances = Self::default();
66
67        if args.is_empty() {
68            return Ok(tolerances);
69        }
70
71        // Check if it's a single number (default tolerance)
72        if args.len() == 1 {
73            if let shape_ast::ast::Expr::Literal(shape_ast::ast::Literal::Number(n), _) = &args[0] {
74                tolerances.default = *n;
75                // Apply to all properties
76                for (_, tol) in tolerances.properties.iter_mut() {
77                    *tol = *n;
78                }
79                return Ok(tolerances);
80            }
81        }
82
83        // Otherwise parse property: value pairs
84        // This would need more sophisticated parsing, for now use defaults
85        Ok(tolerances)
86    }
87}
88
89/// Enhanced fuzzy matcher with property-specific tolerances
90pub struct PropertyFuzzyMatcher {
91    tolerances: PropertyTolerances,
92}
93
94impl PropertyFuzzyMatcher {
95    pub fn new(tolerances: PropertyTolerances) -> Self {
96        Self { tolerances }
97    }
98
99    /// Fuzzy compare with property context
100    pub fn fuzzy_compare(&self, property: &str, actual: f64, expected: f64, op: FuzzyOp) -> bool {
101        let tolerance = self.tolerances.get(property);
102
103        match op {
104            FuzzyOp::Equal => {
105                let diff = (actual - expected).abs();
106                let avg = (actual.abs() + expected.abs()) / 2.0;
107                if avg == 0.0 {
108                    diff == 0.0
109                } else {
110                    diff / avg <= tolerance
111                }
112            }
113            FuzzyOp::Greater => actual > expected * (1.0 - tolerance),
114            FuzzyOp::Less => actual < expected * (1.0 + tolerance),
115            FuzzyOp::GreaterEq => actual >= expected * (1.0 - tolerance),
116            FuzzyOp::LessEq => actual <= expected * (1.0 + tolerance),
117        }
118    }
119}
120
121#[derive(Debug, Clone, Copy)]
122pub enum FuzzyOp {
123    Equal,
124    Greater,
125    Less,
126    GreaterEq,
127    LessEq,
128}