use std::sync::LazyLock;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Notation {
Decimal,
Hex,
Binary,
Octal,
Scientific,
BigInt,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Literal {
pub(crate) value: f64,
pub(crate) notation: Notation,
}
impl Literal {
pub(crate) const fn decimal(value: f64) -> Self {
Self {
value,
notation: Notation::Decimal,
}
}
}
pub(crate) fn plain_notation(token: &str) -> Notation {
if token.contains(['e', 'E']) {
return Notation::Scientific;
}
Notation::Decimal
}
static STRICT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$").expect("a constant pattern compiles")
});
pub(crate) fn strict_number(raw: &str) -> Option<Literal> {
let trimmed = super::js::trim(raw);
if !STRICT.is_match(trimmed) {
return None;
}
trimmed
.parse::<f64>()
.ok()
.filter(|value| value.is_finite())
.map(|value| Literal {
value,
notation: plain_notation(trimmed),
})
}
pub(crate) fn is_extractable(value: f64) -> bool {
value.is_finite()
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Value {
Number(f64),
Text(String),
Other,
Seq(Vec<Value>),
Map(Vec<Value>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Coercion {
Typed,
Untyped,
}
pub(crate) fn collect(value: &Value, coercion: Coercion) -> Vec<Literal> {
let mut out = Vec::new();
walk(value, coercion, &mut out);
out
}
fn walk(value: &Value, coercion: Coercion, out: &mut Vec<Literal>) {
match value {
Value::Number(number) => {
if is_extractable(*number) {
out.push(Literal::decimal(*number));
}
}
Value::Text(text) => {
if coercion == Coercion::Untyped
&& let Some(number) = strict_number(text)
{
out.push(number);
}
}
Value::Seq(items) | Value::Map(items) => {
for item in items {
walk(item, coercion, out);
}
}
Value::Other => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn values(literals: &[Literal]) -> Vec<f64> {
literals.iter().map(|literal| literal.value).collect()
}
fn number(raw: &str) -> Option<f64> {
strict_number(raw).map(|literal| literal.value)
}
#[test]
fn a_fully_numeric_string_parses() {
assert_eq!(number("42"), Some(42.0));
assert_eq!(number("-1.5e3"), Some(-1500.0));
assert_eq!(number("+7"), Some(7.0));
assert_eq!(number(".5"), Some(0.5));
assert_eq!(number(" 8 "), Some(8.0));
}
#[test]
fn the_trim_is_the_one_the_extension_performs() {
assert_eq!(number("\u{feff}42"), Some(42.0));
assert_eq!(number("42\u{feff}"), Some(42.0));
assert_eq!(number("\u{85}42"), None);
assert_eq!(number("42\u{85}"), None);
}
#[test]
fn a_coerced_string_carries_the_notation_its_text_used() {
assert_eq!(
strict_number("42").expect("a number").notation,
Notation::Decimal
);
assert_eq!(
strict_number("-1.5e3").expect("a number").notation,
Notation::Scientific
);
assert_eq!(
strict_number("1E5").expect("a number").notation,
Notation::Scientific
);
}
#[test]
fn a_partly_numeric_string_is_not_a_number() {
for input in ["12abc", "1.2.3", "0x1A", "1_000", "", "abc", "1,000", "--1"] {
assert_eq!(number(input), None, "{input}");
}
}
#[test]
fn non_finite_values_are_never_extractable() {
assert!(!is_extractable(f64::NAN));
assert!(!is_extractable(f64::INFINITY));
assert!(!is_extractable(f64::NEG_INFINITY));
assert!(is_extractable(0.0));
}
#[test]
fn an_overflowing_literal_is_rejected() {
assert_eq!(number("1e400"), None);
}
#[test]
fn a_typed_format_ignores_numeric_looking_text() {
let document = Value::Map(vec![Value::Number(42.0), Value::Text("7".to_string())]);
assert_eq!(values(&collect(&document, Coercion::Typed)), [42.0]);
}
#[test]
fn an_untyped_format_reads_numeric_looking_text() {
let document = Value::Map(vec![Value::Number(42.0), Value::Text("7".to_string())]);
assert_eq!(values(&collect(&document, Coercion::Untyped)), [42.0, 7.0]);
}
#[test]
fn a_parsed_number_is_reported_as_decimal() {
let document = Value::Seq(vec![Value::Number(1e21)]);
assert_eq!(
collect(&document, Coercion::Typed)[0].notation,
Notation::Decimal
);
}
#[test]
fn nesting_is_walked_in_document_order() {
let document = Value::Map(vec![
Value::Number(1.0),
Value::Seq(vec![Value::Number(2.0), Value::Other, Value::Number(3.0)]),
Value::Map(vec![Value::Number(4.0)]),
]);
assert_eq!(
values(&collect(&document, Coercion::Typed)),
[1.0, 2.0, 3.0, 4.0]
);
}
#[test]
fn a_date_is_never_a_number() {
let document = Value::Map(vec![Value::Other, Value::Number(1.0)]);
assert_eq!(values(&collect(&document, Coercion::Untyped)), [1.0]);
}
#[test]
fn non_finite_numbers_are_dropped_by_the_walk() {
let document = Value::Seq(vec![
Value::Number(f64::INFINITY),
Value::Number(f64::NAN),
Value::Number(1.0),
]);
assert_eq!(values(&collect(&document, Coercion::Typed)), [1.0]);
}
#[test]
fn repeats_are_kept() {
let document = Value::Seq(vec![Value::Number(5.0), Value::Number(5.0)]);
assert_eq!(values(&collect(&document, Coercion::Typed)), [5.0, 5.0]);
}
}