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}