use std::sync::LazyLock;
use regex::Regex;
use super::policy::{Literal, plain_notation};
static TEXT_NUMBER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?")
.expect("a constant pattern compiles")
});
pub(crate) fn extract(text: &str) -> Vec<Literal> {
spanned(text)
.into_iter()
.map(|(literal, _)| literal)
.collect()
}
pub(crate) fn spanned(text: &str) -> Vec<(Literal, usize)> {
TEXT_NUMBER
.find_iter(text)
.filter_map(|found| {
found
.as_str()
.parse::<f64>()
.ok()
.filter(|value| value.is_finite())
.map(|value| {
(
Literal {
value,
notation: plain_notation(found.as_str()),
},
found.start(),
)
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::policy::Notation;
fn values(text: &str) -> Vec<f64> {
extract(text)
.into_iter()
.map(|literal| literal.value)
.collect()
}
#[test]
fn plain_numbers_are_found() {
assert_eq!(values("rate 0.0825 on 1000 units"), [0.0825, 1000.0]);
}
#[test]
fn signs_and_exponents_are_read() {
assert_eq!(values("offset -7e2 and +3"), [-700.0, 3.0]);
}
#[test]
fn a_run_carries_the_notation_it_was_written_in() {
let notations: Vec<Notation> = extract("-7e2 and 3")
.into_iter()
.map(|literal| literal.notation)
.collect();
assert_eq!(notations, [Notation::Scientific, Notation::Decimal]);
}
#[test]
fn a_version_string_has_no_grammar_to_protect_it() {
assert_eq!(values("v1.2.3"), [1.2, 0.3]);
assert_eq!(values("3.16.15"), [3.16, 0.15]);
}
#[test]
fn text_without_numbers_yields_nothing() {
assert!(values("no numbers on this line at all").is_empty());
}
#[test]
fn a_span_points_at_the_run() {
let text = "rate 0.0825 here";
let (literal, offset) = spanned(text)[0];
assert_eq!(literal.value, 0.0825);
assert_eq!(&text[offset..offset + 6], "0.0825");
}
#[test]
fn an_overflowing_run_is_dropped() {
assert!(values("1e400").is_empty());
}
}