#[must_use]
pub fn is_null(value: &str) -> bool {
matches!(value, "null" | "Null" | "NULL" | "~" | "")
}
#[must_use]
pub fn is_bool(value: &str) -> bool {
matches!(
value,
"true" | "True" | "TRUE" | "false" | "False" | "FALSE"
)
}
#[must_use]
pub fn is_integer(value: &str) -> bool {
parse_integer(value).is_some()
}
#[must_use]
pub fn is_float(value: &str) -> bool {
parse_float(value).is_some()
}
#[must_use]
pub fn parse_integer(value: &str) -> Option<i64> {
let (neg, rest) = value.strip_prefix('-').map_or_else(
|| (false, value.strip_prefix('+').unwrap_or(value)),
|r| (true, r),
);
if rest.is_empty() {
return None;
}
let magnitude: i64 = if let Some(oct) = rest.strip_prefix("0o") {
if oct.is_empty() {
return None;
}
i64::from_str_radix(oct, 8).ok()?
} else if let Some(hex) = rest.strip_prefix("0x") {
if hex.is_empty() {
return None;
}
i64::from_str_radix(hex, 16).ok()?
} else {
if rest.len() > 1 && rest.starts_with('0') {
return None;
}
if !rest.chars().all(|c| c.is_ascii_digit()) {
return None;
}
rest.parse::<i64>().ok()?
};
Some(if neg { -magnitude } else { magnitude })
}
#[must_use]
pub fn parse_float(value: &str) -> Option<f64> {
match value {
".inf" | ".Inf" | ".INF" => return Some(f64::INFINITY),
"-.inf" | "-.Inf" | "-.INF" => return Some(f64::NEG_INFINITY),
".nan" | ".NaN" | ".NAN" => return Some(f64::NAN),
_ => {}
}
let stripped = value.strip_prefix('+').unwrap_or(value);
let signed = stripped.strip_prefix('-').unwrap_or(stripped);
if signed.contains('.') || signed.contains('e') || signed.contains('E') {
return value.trim_start_matches('+').parse::<f64>().ok();
}
None
}
#[cfg(test)]
#[allow(clippy::approx_constant, clippy::float_cmp, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn null_lowercase() {
assert!(is_null("null"));
}
#[test]
fn null_titlecase() {
assert!(is_null("Null"));
}
#[test]
fn null_uppercase() {
assert!(is_null("NULL"));
}
#[test]
fn null_tilde() {
assert!(is_null("~"));
}
#[test]
fn null_empty() {
assert!(is_null(""));
}
#[test]
fn not_null_string() {
assert!(!is_null("none"));
assert!(!is_null("nil"));
assert!(!is_null("nUll"));
}
#[test]
fn bool_true_variants() {
assert!(is_bool("true"));
assert!(is_bool("True"));
assert!(is_bool("TRUE"));
}
#[test]
fn bool_false_variants() {
assert!(is_bool("false"));
assert!(is_bool("False"));
assert!(is_bool("FALSE"));
}
#[test]
fn not_bool() {
assert!(!is_bool("yes"));
assert!(!is_bool("no"));
assert!(!is_bool("on"));
assert!(!is_bool("off"));
assert!(!is_bool("tRue"));
}
#[test]
fn integer_decimal() {
assert_eq!(parse_integer("42"), Some(42));
assert_eq!(parse_integer("0"), Some(0));
assert_eq!(parse_integer("-1"), Some(-1));
assert_eq!(parse_integer("+100"), Some(100));
}
#[test]
fn integer_octal() {
assert_eq!(parse_integer("0o17"), Some(15));
assert_eq!(parse_integer("-0o10"), Some(-8));
}
#[test]
fn integer_hex() {
assert_eq!(parse_integer("0xFF"), Some(255));
assert_eq!(parse_integer("-0x1A"), Some(-26));
}
#[test]
fn integer_leading_zeros_rejected() {
assert_eq!(parse_integer("007"), None);
assert_eq!(parse_integer("00"), None);
}
#[test]
fn integer_empty_prefix_rejected() {
assert_eq!(parse_integer("0o"), None);
assert_eq!(parse_integer("0x"), None);
assert_eq!(parse_integer("+"), None);
assert_eq!(parse_integer("-"), None);
assert_eq!(parse_integer(""), None);
}
#[test]
fn is_integer_delegates_to_parse() {
assert!(is_integer("42"));
assert!(!is_integer("3.14"));
assert!(!is_integer("abc"));
}
#[test]
fn float_decimal() {
assert_eq!(parse_float("3.14"), Some(3.14));
assert_eq!(parse_float("-0.5"), Some(-0.5));
assert_eq!(parse_float("+1.0"), Some(1.0));
}
#[test]
fn float_exponent() {
assert_eq!(parse_float("1e10"), Some(1e10));
assert_eq!(parse_float("1.5E-3"), Some(1.5e-3));
}
#[test]
fn float_inf() {
assert_eq!(parse_float(".inf"), Some(f64::INFINITY));
assert_eq!(parse_float(".Inf"), Some(f64::INFINITY));
assert_eq!(parse_float(".INF"), Some(f64::INFINITY));
assert_eq!(parse_float("-.inf"), Some(f64::NEG_INFINITY));
assert_eq!(parse_float("-.Inf"), Some(f64::NEG_INFINITY));
assert_eq!(parse_float("-.INF"), Some(f64::NEG_INFINITY));
}
#[test]
fn float_nan() {
assert!(parse_float(".nan").unwrap().is_nan());
assert!(parse_float(".NaN").unwrap().is_nan());
assert!(parse_float(".NAN").unwrap().is_nan());
}
#[test]
fn not_float() {
assert_eq!(parse_float("42"), None);
assert_eq!(parse_float("abc"), None);
assert_eq!(parse_float(""), None);
}
#[test]
fn is_float_delegates_to_parse() {
assert!(is_float("3.14"));
assert!(is_float(".inf"));
assert!(!is_float("42"));
}
#[test]
fn is_null_returns_false_for_whitespace() {
assert!(!is_null(" "));
assert!(!is_null(" "));
}
#[test]
fn is_integer_returns_true_for_positive_signed() {
assert!(is_integer("+0"));
assert!(is_integer("+42"));
}
#[test]
fn is_integer_returns_false_for_float_looking_strings() {
assert!(!is_integer("1.0"));
assert!(!is_integer("1e5"));
assert!(!is_integer("1.5e3"));
}
#[test]
fn is_integer_returns_false_for_non_numeric_with_letters() {
assert!(!is_integer("abc"));
assert!(!is_integer("1a2"));
}
#[test]
fn is_float_returns_false_for_bare_inf_and_nan() {
assert!(!is_float("inf"));
assert!(!is_float("nan"));
}
#[test]
fn parse_integer_hex_lowercase() {
assert_eq!(parse_integer("0xdeadbeef"), Some(0xdead_beef));
}
#[test]
fn parse_float_positive_signed() {
assert_eq!(parse_float("+1.0"), Some(1.0));
}
}