visual-cortex-vision 0.10.0

Detectors for visual-cortex: pixel/color conditions, template matching, the OcrEngine contract, and OCR text parsers.
Documentation
//! Text parsers for OCR output. Used with `WatcherBuilder::ocr`.

/// Parse the first numeric token out of recognized text (`"HP 1,234/5,000"`
/// -> `1234.0`). Scans for the first run of ASCII digits, treating embedded
/// commas as ignorable OCR noise (any count or position) and one embedded
/// `.` as a decimal point; a leading `.` is not part of the number
/// (`".5"` -> `5.0`). Malformed sequences (e.g. multi-dot tokens like
/// "1.2.3") fail to parse and are skipped rather than aborting the scan, so
/// a later well-formed token can still match. Returns `None` when nothing
/// in the whole text parses to a finite value; never returns NaN/inf.
pub fn number() -> impl Fn(&str) -> Option<f64> + Send + 'static {
    |text: &str| {
        let mut token = String::new();
        let mut chars = text.chars();
        loop {
            match chars.next() {
                Some(c) if c.is_ascii_digit() || (!token.is_empty() && (c == ',' || c == '.')) => {
                    if c != ',' {
                        token.push(c);
                    }
                }
                other => {
                    if token.chars().any(|c| c.is_ascii_digit()) {
                        // Trim a trailing '.' ("300." -> "300")
                        let trimmed = token.trim_end_matches('.');
                        if let Ok(n) = trimmed.parse::<f64>() {
                            if n.is_finite() {
                                return Some(n);
                            }
                        }
                        // Malformed token (e.g. multi-dot "1.2.3"): drop it
                        // and keep scanning instead of aborting.
                    }
                    token.clear();
                    other?;
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn number_parses_first_numeric_token() {
        let p = number();
        assert_eq!(p("300"), Some(300.0));
        assert_eq!(p("HP 1,234/5,000"), Some(1234.0));
        assert_eq!(p("Level 5 HP 300"), Some(5.0));
        assert_eq!(p("12.5%"), Some(12.5));
        assert_eq!(p(".5"), Some(5.0));
        assert_eq!(p("1,,234"), Some(1234.0));
        // A malformed token (multi-dot) must not abort the scan.
        assert_eq!(p("v1.2.3 HP 500"), Some(500.0));
    }

    #[test]
    fn number_rejects_non_numeric() {
        let p = number();
        assert_eq!(p("abc"), None);
        assert_eq!(p(""), None);
        assert_eq!(p("..,,.."), None);
    }

    #[test]
    fn number_never_returns_non_finite() {
        let p = number();
        // A pathological run of digits large enough to overflow f64 parses to
        // infinity; the parser must reject it.
        let huge = "9".repeat(400);
        assert_eq!(p(&huge), None);
    }
}