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()) {
let trimmed = token.trim_end_matches('.');
if let Ok(n) = trimmed.parse::<f64>() {
if n.is_finite() {
return Some(n);
}
}
}
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));
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();
let huge = "9".repeat(400);
assert_eq!(p(&huge), None);
}
}