Skip to main content

visual_cortex_vision/
patterns.rs

1//! Text parsers for OCR output. Used with `WatcherBuilder::ocr`.
2
3/// Parse the first numeric token out of recognized text (`"HP 1,234/5,000"`
4/// -> `1234.0`). Scans for the first run of ASCII digits, treating embedded
5/// commas as ignorable OCR noise (any count or position) and one embedded
6/// `.` as a decimal point; a leading `.` is not part of the number
7/// (`".5"` -> `5.0`). Malformed sequences (e.g. multi-dot tokens like
8/// "1.2.3") fail to parse and are skipped rather than aborting the scan, so
9/// a later well-formed token can still match. Returns `None` when nothing
10/// in the whole text parses to a finite value; never returns NaN/inf.
11pub fn number() -> impl Fn(&str) -> Option<f64> + Send + 'static {
12    |text: &str| {
13        let mut token = String::new();
14        let mut chars = text.chars();
15        loop {
16            match chars.next() {
17                Some(c) if c.is_ascii_digit() || (!token.is_empty() && (c == ',' || c == '.')) => {
18                    if c != ',' {
19                        token.push(c);
20                    }
21                }
22                other => {
23                    if token.chars().any(|c| c.is_ascii_digit()) {
24                        // Trim a trailing '.' ("300." -> "300")
25                        let trimmed = token.trim_end_matches('.');
26                        if let Ok(n) = trimmed.parse::<f64>() {
27                            if n.is_finite() {
28                                return Some(n);
29                            }
30                        }
31                        // Malformed token (e.g. multi-dot "1.2.3"): drop it
32                        // and keep scanning instead of aborting.
33                    }
34                    token.clear();
35                    other?;
36                }
37            }
38        }
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn number_parses_first_numeric_token() {
48        let p = number();
49        assert_eq!(p("300"), Some(300.0));
50        assert_eq!(p("HP 1,234/5,000"), Some(1234.0));
51        assert_eq!(p("Level 5 HP 300"), Some(5.0));
52        assert_eq!(p("12.5%"), Some(12.5));
53        assert_eq!(p(".5"), Some(5.0));
54        assert_eq!(p("1,,234"), Some(1234.0));
55        // A malformed token (multi-dot) must not abort the scan.
56        assert_eq!(p("v1.2.3 HP 500"), Some(500.0));
57    }
58
59    #[test]
60    fn number_rejects_non_numeric() {
61        let p = number();
62        assert_eq!(p("abc"), None);
63        assert_eq!(p(""), None);
64        assert_eq!(p("..,,.."), None);
65    }
66
67    #[test]
68    fn number_never_returns_non_finite() {
69        let p = number();
70        // A pathological run of digits large enough to overflow f64 parses to
71        // infinity; the parser must reject it.
72        let huge = "9".repeat(400);
73        assert_eq!(p(&huge), None);
74    }
75}