use crate::schema::xsd::facets::{FacetConstraints, WhitespaceHandling};
use crate::schema::xsd::primitive::PrimitiveKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NumClass {
Double,
Decimal,
Integer,
}
impl NumClass {
fn from_kind(kind: PrimitiveKind) -> Option<Self> {
match kind {
PrimitiveKind::Double | PrimitiveKind::Float => Some(Self::Double),
PrimitiveKind::Decimal => Some(Self::Decimal),
PrimitiveKind::Integer => Some(Self::Integer),
_ => None,
}
}
#[inline]
fn accepts_token(self, tok: &[u8]) -> bool {
match self {
Self::Double => is_double(tok),
Self::Decimal => is_decimal(tok),
Self::Integer => is_integer(tok),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NumericPlan {
Scalar(NumClass),
List(NumClass),
}
pub(crate) fn classify(constraints: &FacetConstraints) -> Option<NumericPlan> {
if !is_unconstrained(constraints) {
return None;
}
if constraints.is_list {
if constraints.value_kind.is_some() {
return None;
}
let class = NumClass::from_kind(constraints.item_kind?)?;
Some(NumericPlan::List(class))
} else {
let class = NumClass::from_kind(constraints.value_kind?)?;
Some(NumericPlan::Scalar(class))
}
}
fn is_unconstrained(c: &FacetConstraints) -> bool {
c.length.is_none()
&& c.min_length.is_none()
&& c.max_length.is_none()
&& c.min_inclusive.is_none()
&& c.max_inclusive.is_none()
&& c.min_exclusive.is_none()
&& c.max_exclusive.is_none()
&& c.total_digits.is_none()
&& c.fraction_digits.is_none()
&& c.patterns.is_empty()
&& c.enumeration.is_empty()
&& c.explicit_timezone.is_none()
&& c.whitespace == WhitespaceHandling::Collapse
}
#[inline]
fn is_xml_ws(b: u8) -> bool {
b == b' ' || b == b'\t' || b == b'\n' || b == b'\r'
}
#[inline]
pub(crate) fn scan_list(bytes: &[u8], class: NumClass) -> bool {
let mut i = 0;
let n = bytes.len();
while i < n {
while i < n && is_xml_ws(bytes[i]) {
i += 1;
}
if i >= n {
break;
}
let start = i;
while i < n && !is_xml_ws(bytes[i]) {
i += 1;
}
if !class.accepts_token(&bytes[start..i]) {
return false;
}
}
true
}
#[inline]
pub(crate) fn scan_scalar(bytes: &[u8], class: NumClass) -> bool {
let mut start = 0;
let mut end = bytes.len();
while start < end && is_xml_ws(bytes[start]) {
start += 1;
}
while end > start && is_xml_ws(bytes[end - 1]) {
end -= 1;
}
if start == end {
return false;
}
class.accepts_token(&bytes[start..end])
}
#[inline]
fn is_integer(b: &[u8]) -> bool {
let digits = match b.first() {
Some(b'+') | Some(b'-') => &b[1..],
_ => b,
};
!digits.is_empty() && digits.iter().all(u8::is_ascii_digit)
}
#[inline]
fn is_unsigned_mantissa(b: &[u8]) -> bool {
if b.is_empty() {
return false;
}
let mut dot = None;
for (j, &c) in b.iter().enumerate() {
if c == b'.' {
if dot.is_some() {
return false; }
dot = Some(j);
} else if !c.is_ascii_digit() {
return false;
}
}
match dot {
None => true,
Some(d) => {
let before = d;
let after = b.len() - d - 1;
before >= 1 || after >= 1
}
}
}
#[inline]
fn is_decimal(b: &[u8]) -> bool {
let rest = match b.first() {
Some(b'+') | Some(b'-') => &b[1..],
_ => b,
};
is_unsigned_mantissa(rest)
}
#[inline]
fn is_double(b: &[u8]) -> bool {
match b {
b"INF" | b"+INF" | b"-INF" | b"NaN" => return true,
_ => {}
}
let rest = match b.first() {
Some(b'+') | Some(b'-') => &b[1..],
_ => b,
};
match rest.iter().position(|&c| c == b'e' || c == b'E') {
Some(p) => {
if !is_unsigned_mantissa(&rest[..p]) {
return false;
}
let exp = &rest[p + 1..];
let exp_digits = match exp.first() {
Some(b'+') | Some(b'-') => &exp[1..],
_ => exp,
};
!exp_digits.is_empty() && exp_digits.iter().all(u8::is_ascii_digit)
}
None => is_unsigned_mantissa(rest),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn representative_kind(class: NumClass) -> PrimitiveKind {
match class {
NumClass::Double => PrimitiveKind::Double,
NumClass::Decimal => PrimitiveKind::Decimal,
NumClass::Integer => PrimitiveKind::Integer,
}
}
fn assert_token_agrees(class: NumClass, tok: &str) {
assert!(
!tok.bytes().any(is_xml_ws),
"assert_token_agrees is only valid for whitespace-free tokens"
);
let fast = class.accepts_token(tok.as_bytes());
let slow = representative_kind(class).validate(tok).is_ok();
assert_eq!(
fast, slow,
"token {tok:?} for {class:?}: fast={fast} slow={slow}"
);
}
#[test]
fn integer_tokens() {
for t in ["0", "42", "-42", "+42", "000", "-0"] {
assert!(is_integer(t.as_bytes()), "{t}");
assert_token_agrees(NumClass::Integer, t);
}
for t in ["", "+", "-", "1.0", "1e2", "abc", "0x1", "+-1"] {
assert!(!is_integer(t.as_bytes()), "{t}");
assert_token_agrees(NumClass::Integer, t);
}
for t in ["1 2", " 1", "1\t2"] {
assert!(!is_integer(t.as_bytes()), "{t}");
}
}
#[test]
fn decimal_tokens() {
for t in ["0", "1.5", "-1.5", ".5", "1.", "+1.5", "-0.0", "00.00"] {
assert!(is_decimal(t.as_bytes()), "{t}");
assert_token_agrees(NumClass::Decimal, t);
}
for t in ["", ".", "+", "-", "1e2", "1.2.3", "abc", "1..2", "+."] {
assert!(!is_decimal(t.as_bytes()), "{t}");
assert_token_agrees(NumClass::Decimal, t);
}
assert!(!is_decimal(b"1 2"));
}
#[test]
fn double_tokens() {
for t in [
"0", "1.5", "-1.5e-3", "1.2E10", "INF", "-INF", "+INF", "NaN", ".5", "1.", "1e2",
"1E+2", "-0.0e0",
] {
assert!(is_double(t.as_bytes()), "{t}");
assert_token_agrees(NumClass::Double, t);
}
for t in [
"", "abc", "inf", "nan", "+NaN", "-NaN", "1.2.3", "1e", "1e+", "e5", "1ee5", "INFI",
"1.5f",
] {
assert!(!is_double(t.as_bytes()), "{t}");
assert_token_agrees(NumClass::Double, t);
}
assert!(!is_double(b"1 2"));
}
#[test]
fn scan_list_basic() {
assert!(scan_list(b"1.0 2.0 3.0", NumClass::Double));
assert!(scan_list(b" 1.0\t2.0\n3.0 ", NumClass::Double));
assert!(scan_list(b"", NumClass::Double)); assert!(scan_list(b" ", NumClass::Double)); assert!(scan_list(b"42", NumClass::Integer)); assert!(!scan_list(b"1.0 abc 3.0", NumClass::Double));
assert!(!scan_list(b"1 2 3.5", NumClass::Integer)); }
#[test]
fn scan_scalar_basic() {
assert!(scan_scalar(b"42", NumClass::Integer));
assert!(scan_scalar(b" 42 ", NumClass::Integer));
assert!(scan_scalar(b"-1.5e3", NumClass::Double));
assert!(!scan_scalar(b"", NumClass::Integer)); assert!(!scan_scalar(b" ", NumClass::Integer)); assert!(!scan_scalar(b"1 2", NumClass::Integer)); assert!(!scan_scalar(b"abc", NumClass::Double));
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn pick<'a>(&mut self, xs: &[&'a str]) -> &'a str {
xs[(self.next() as usize) % xs.len()]
}
}
fn random_token(rng: &mut Rng) -> String {
let signs = ["", "+", "-"];
let cores = [
"",
"0",
"1",
"42",
"007",
"123456789",
".",
".5",
"5.",
"1.5",
"1.2.3",
"..",
"e",
"E",
"e5",
"1e",
"e+",
"1e2",
"1E-3",
"INF",
"NaN",
"inf",
"nan",
"abc",
"0x1f",
" ",
"1 2",
"+",
"-",
"1.5f",
"9999999999999999999999",
];
let n = 1 + (rng.next() as usize % 3);
let mut s = String::from(rng.pick(&signs));
for _ in 0..n {
s.push_str(rng.pick(&cores));
}
s
}
#[test]
fn property_fast_token_matches_primitive_kind() {
let mut rng = Rng(0x9E3779B97F4A7C15);
for _ in 0..20_000 {
let tok = random_token(&mut rng);
for class in [NumClass::Double, NumClass::Decimal, NumClass::Integer] {
let fast = class.accepts_token(tok.as_bytes());
if tok.bytes().any(is_xml_ws) {
assert!(!fast, "token {tok:?} with ws accepted by {class:?}");
continue;
}
let slow = representative_kind(class).validate(&tok).is_ok();
assert_eq!(
fast, slow,
"token {tok:?} for {class:?}: fast={fast} slow={slow}"
);
}
}
}
#[test]
fn property_scan_list_and_scalar_match_slow_path() {
let mut rng = Rng(0xDEADBEEFCAFEF00D);
let seps = [" ", " ", "\t", "\n", " \t "];
for _ in 0..5_000 {
let count = rng.next() as usize % 6; let mut items = Vec::new();
for _ in 0..count {
items.push(random_token(&mut rng));
}
for class in [NumClass::Double, NumClass::Decimal, NumClass::Integer] {
let kind = representative_kind(class);
let mut joined = String::new();
for (idx, it) in items.iter().enumerate() {
if idx > 0 {
joined.push_str(rng.pick(&seps));
}
joined.push_str(it);
}
let fast_list = scan_list(joined.as_bytes(), class);
let slow_list = joined
.split_whitespace()
.all(|it| kind.validate(it).is_ok());
assert_eq!(
fast_list, slow_list,
"list {joined:?} for {class:?}: fast={fast_list} slow={slow_list}"
);
let core = items.first().cloned().unwrap_or_default();
let padded = format!("{}{}{}", rng.pick(&seps), core, rng.pick(&seps));
let fast_scalar = scan_scalar(padded.as_bytes(), class);
let trimmed =
padded.trim_matches(|c: char| c == ' ' || c == '\t' || c == '\n' || c == '\r');
let slow_scalar = !trimmed.is_empty()
&& !trimmed.bytes().any(is_xml_ws)
&& kind.validate(trimmed).is_ok();
assert_eq!(
fast_scalar, slow_scalar,
"scalar {padded:?} for {class:?}: fast={fast_scalar} slow={slow_scalar}"
);
}
}
}
}