#[inline]
pub(crate) fn continues_unit(ch: char) -> bool {
!ch.is_ascii() || continues_unit_byte(ch as u8)
}
#[inline]
const fn continues_unit_byte(b: u8) -> bool {
b >= 0x80 || b.is_ascii_alphabetic() || matches!(b, b'_' | b'-' | b'\\')
}
pub(crate) fn exponent_len(s: &str) -> usize {
let bytes = s.as_bytes();
if bytes.is_empty() || (bytes[0] != b'e' && bytes[0] != b'E') {
return 0;
}
let mut i = 1;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
if i >= bytes.len() || !bytes[i].is_ascii_digit() {
return 0;
}
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
i
}
#[inline]
fn opens_number_digits(bytes: &[u8]) -> bool {
match bytes.first() {
Some(b) if b.is_ascii_digit() => true,
Some(b'.') => bytes.get(1).is_some_and(u8::is_ascii_digit),
_ => false,
}
}
#[inline]
pub(crate) fn starts_number(s: &str) -> bool {
let bytes = s.as_bytes();
let sign = usize::from(matches!(bytes.first(), Some(b'+' | b'-')));
let answer = opens_number_digits(&bytes[sign..]);
debug_assert_eq!(answer, number_part_len(s) > 0, "starts_number({s:?})");
answer
}
#[inline]
pub(crate) fn sign_starts_number(sign: u8, after: &str) -> bool {
debug_assert!(matches!(sign, b'+' | b'-'), "a sign, not {sign:?}");
opens_number_digits(after.as_bytes())
}
pub(crate) fn number_part_len(s: &str) -> usize {
let bytes = s.as_bytes();
let mut i = 0;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let int_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let has_integer_digits = i > int_start;
let mut has_fraction_digits = false;
if i < bytes.len() && bytes[i] == b'.' {
let after_dot = &s[i + 1..];
if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
has_fraction_digits = true;
} else if exponent_len(after_dot) > 0 {
i += 1;
} else if has_integer_digits
&& after_dot
.as_bytes()
.first()
.is_none_or(|&b| !continues_unit_byte(b) && b != b'.')
{
i += 1;
}
}
if !has_integer_digits && !has_fraction_digits {
return 0; }
i + exponent_len(&s[i..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_number_is_number_part_len_nonzero() {
let alphabet: Vec<char> = (0u8..0x80)
.map(char::from)
.chain(['\u{a0}', '\u{e9}', '\u{3000}', '\u{1f4a9}'])
.collect();
let mut s = String::new();
let check = |s: &str| {
let expected = number_part_len(s) > 0;
assert_eq!(starts_number(s), expected, "starts_number({s:?})");
if let Some(rest) = s.strip_prefix(['+', '-']) {
let sign = s.as_bytes()[0];
assert_eq!(sign_starts_number(sign, rest), expected, "sign {s:?}");
}
};
check("");
for &a in &alphabet {
for &b in &alphabet {
for &c in &alphabet {
s.clear();
s.extend([a, b, c]);
check(&s);
}
s.clear();
s.extend([a, b]);
check(&s);
}
s.clear();
s.push(a);
check(&s);
}
}
#[test]
fn continues_unit_is_its_unicode_reading() {
for ch in (0..=0x10_FFFF).filter_map(char::from_u32) {
let unicode =
ch.is_alphabetic() || ch == '_' || ch == '-' || ch == '\\' || !ch.is_ascii();
assert_eq!(continues_unit(ch), unicode, "{ch:?}");
let mut lead = [0u8; 4];
let lead = ch.encode_utf8(&mut lead).as_bytes()[0];
assert_eq!(continues_unit_byte(lead), unicode, "{ch:?}");
}
}
#[test]
fn number_part_len_basics() {
assert_eq!(number_part_len("1"), 1);
assert_eq!(number_part_len("123"), 3);
assert_eq!(number_part_len("1.5"), 3);
assert_eq!(number_part_len(".5"), 2);
assert_eq!(number_part_len("-1.5"), 4);
assert_eq!(number_part_len("+.5"), 3);
assert_eq!(number_part_len("1.50"), 4);
}
#[test]
fn number_part_len_excludes_unit() {
assert_eq!(number_part_len("1px"), 1);
assert_eq!(number_part_len("1.5px"), 3);
assert_eq!(number_part_len("1em"), 1);
assert_eq!(number_part_len("2ex"), 1);
}
#[test]
fn number_part_len_exponents() {
assert_eq!(number_part_len("1e1"), 3);
assert_eq!(number_part_len("1e+1"), 4);
assert_eq!(number_part_len("1.5e10"), 6);
assert_eq!(number_part_len("1.5E10"), 6);
assert_eq!(number_part_len("1.5e-0010"), 9);
assert_eq!(number_part_len("1.e1"), 4);
assert_eq!(number_part_len("1e3px"), 3);
}
#[test]
fn number_part_len_trailing_dot() {
assert_eq!(number_part_len("1."), 2);
assert_eq!(number_part_len("10."), 3);
assert_eq!(number_part_len("1.px"), 1);
assert_eq!(number_part_len("1.foo"), 1);
}
#[test]
fn number_part_len_non_numbers() {
assert_eq!(number_part_len(""), 0);
assert_eq!(number_part_len("abc"), 0);
assert_eq!(number_part_len("."), 0);
assert_eq!(number_part_len("+"), 0);
assert_eq!(number_part_len("-px"), 0);
}
#[test]
fn exponent_len_distinguishes_units() {
assert_eq!(exponent_len("e1"), 2);
assert_eq!(exponent_len("E+10"), 4);
assert_eq!(exponent_len("e-5"), 3);
assert_eq!(exponent_len("em"), 0); assert_eq!(exponent_len("e"), 0);
assert_eq!(exponent_len("px"), 0);
}
#[test]
fn number_part_len_malformed_boundaries() {
assert_eq!(number_part_len("1.2.3"), 3);
assert_eq!(number_part_len("1.."), 1);
assert_eq!(number_part_len("1..e-10"), 1);
assert_eq!(number_part_len("1..5"), 1);
assert_eq!(number_part_len("1."), 2);
assert_eq!(number_part_len("1.e1"), 4);
assert_eq!(number_part_len("5e10e10"), 4);
assert_eq!(number_part_len(".5e3"), 4);
assert_eq!(number_part_len(".e3"), 0);
assert_eq!(number_part_len("1e+"), 1);
assert_eq!(number_part_len("1_000"), 1);
}
}