use crate::document::Node;
#[derive(Clone, PartialEq)]
pub enum Value<N> {
NodeSet(Vec<N>),
Boolean(bool),
Number(f64),
String(String),
}
impl<N: std::fmt::Debug> std::fmt::Debug for Value<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::NodeSet(nodes) => f.debug_tuple("NodeSet").field(nodes).finish(),
Value::Boolean(b) => f.debug_tuple("Boolean").field(b).finish(),
Value::Number(n) => f.debug_tuple("Number").field(n).finish(),
Value::String(s) => f.debug_tuple("String").field(s).finish(),
}
}
}
impl<'a, N: Node<'a>> Value<N> {
pub fn to_boolean(&self) -> bool {
match self {
Value::NodeSet(nodes) => !nodes.is_empty(),
Value::Boolean(b) => *b,
Value::Number(n) => *n != 0.0 && !n.is_nan(),
Value::String(s) => !s.is_empty(),
}
}
pub fn to_number(&self) -> f64 {
match self {
Value::NodeSet(_) => string_to_number(&self.to_xpath_string()),
Value::Boolean(b) => {
if *b {
1.0
} else {
0.0
}
}
Value::Number(n) => *n,
Value::String(s) => string_to_number(s),
}
}
pub fn to_xpath_string(&self) -> String {
match self {
Value::NodeSet(nodes) => first_in_document_order(nodes)
.map(|n| n.string_value())
.unwrap_or_default(),
Value::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
Value::Number(n) => number_to_string(*n),
Value::String(s) => s.clone(),
}
}
}
fn first_in_document_order<'a, N: Node<'a>>(nodes: &[N]) -> Option<N> {
nodes.iter().copied().min_by(|a, b| a.document_order(*b))
}
fn number_to_string(n: f64) -> String {
if n.is_nan() {
return "NaN".to_string();
}
if n.is_infinite() {
return if n > 0.0 {
"Infinity".to_string()
} else {
"-Infinity".to_string()
};
}
if n == 0.0 {
return "0".to_string();
}
n.to_string()
}
pub(crate) fn string_to_number(s: &str) -> f64 {
let trimmed = s.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\r' | '\n'));
let (negative, rest) = match trimmed.strip_prefix('-') {
Some(r) => (true, r),
None => (false, trimmed),
};
let b = rest.as_bytes();
let mut pos = 0;
while pos < b.len() && b[pos].is_ascii_digit() {
pos += 1;
}
let has_int_digits = pos > 0;
let mut has_frac_digits = false;
if pos < b.len() && b[pos] == b'.' {
pos += 1;
let frac_start = pos;
while pos < b.len() && b[pos].is_ascii_digit() {
pos += 1;
}
has_frac_digits = pos > frac_start;
}
if pos != b.len() || !(has_int_digits || has_frac_digits) {
return f64::NAN;
}
let digits = &rest[..pos];
let full = if negative {
format!("-{digits}")
} else {
digits.to_string()
};
full.parse::<f64>().unwrap_or(f64::NAN)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::fixture::build;
#[test]
fn boolean_number_rules() {
assert!(!Value::<crate::document::fixture::TestNode>::Number(0.0).to_boolean());
assert!(!Value::<crate::document::fixture::TestNode>::Number(-0.0).to_boolean());
assert!(!Value::<crate::document::fixture::TestNode>::Number(f64::NAN).to_boolean());
assert!(Value::<crate::document::fixture::TestNode>::Number(1.0).to_boolean());
assert!(Value::<crate::document::fixture::TestNode>::Number(-1.0).to_boolean());
}
#[test]
fn boolean_string_rules() {
assert!(!Value::<crate::document::fixture::TestNode>::String(String::new()).to_boolean());
assert!(Value::<crate::document::fixture::TestNode>::String("x".into()).to_boolean());
}
#[test]
fn boolean_node_set_rules() {
let f = build();
assert!(!Value::NodeSet::<crate::document::fixture::TestNode>(Vec::new()).to_boolean());
assert!(Value::NodeSet(vec![f.doc.node(f.span)]).to_boolean());
}
#[test]
fn number_boolean_rules() {
assert_eq!(
Value::<crate::document::fixture::TestNode>::Boolean(true).to_number(),
1.0
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Boolean(false).to_number(),
0.0
);
}
#[test]
fn number_string_valid_forms() {
assert_eq!(string_to_number("42"), 42.0);
assert_eq!(string_to_number("42.5"), 42.5);
assert_eq!(string_to_number(".5"), 0.5);
assert_eq!(string_to_number("5."), 5.0);
assert_eq!(string_to_number("-42"), -42.0);
assert_eq!(string_to_number(" 42 "), 42.0);
assert_eq!(string_to_number("\t-3.5\n"), -3.5);
}
#[test]
fn number_string_malformed_is_nan() {
assert!(string_to_number("abc").is_nan());
assert!(string_to_number("").is_nan());
assert!(string_to_number("--5").is_nan());
assert!(string_to_number("1.2.3").is_nan());
assert!(
string_to_number("1e5").is_nan(),
"exponent notation is not XPath Number syntax"
);
assert!(
string_to_number("+5").is_nan(),
"leading plus is not permitted"
);
assert!(
string_to_number("5 6").is_nan(),
"embedded whitespace is not permitted"
);
assert!(string_to_number("Infinity").is_nan());
assert!(string_to_number("NaN").is_nan());
}
#[test]
fn number_node_set_uses_first_in_document_order_string_value() {
let f = build();
let value = Value::NodeSet(vec![f.doc.node(f.span_attr)]);
assert_eq!(value.to_number(), 1.0);
let value = Value::NodeSet(vec![f.doc.node(f.text2), f.doc.node(f.comment)]);
assert!(
value.to_number().is_nan(),
"comment's string-value ('note') is not a Number"
);
}
#[test]
fn number_empty_node_set_is_nan() {
let value = Value::NodeSet::<crate::document::fixture::TestNode>(Vec::new());
assert!(value.to_number().is_nan());
}
#[test]
fn string_boolean_rules() {
assert_eq!(
Value::<crate::document::fixture::TestNode>::Boolean(true).to_xpath_string(),
"true"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Boolean(false).to_xpath_string(),
"false"
);
}
#[test]
fn string_number_special_values() {
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(f64::NAN).to_xpath_string(),
"NaN"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(0.0).to_xpath_string(),
"0"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(-0.0).to_xpath_string(),
"0"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(f64::INFINITY).to_xpath_string(),
"Infinity"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(f64::NEG_INFINITY)
.to_xpath_string(),
"-Infinity"
);
}
#[test]
#[allow(clippy::approx_constant)] fn string_number_integer_vs_decimal_formatting() {
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(3.0).to_xpath_string(),
"3"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(-3.0).to_xpath_string(),
"-3"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(100.0).to_xpath_string(),
"100"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(3.14).to_xpath_string(),
"3.14"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(0.5).to_xpath_string(),
"0.5"
);
assert_eq!(
Value::<crate::document::fixture::TestNode>::Number(-0.5).to_xpath_string(),
"-0.5"
);
}
#[test]
fn string_node_set_rules() {
let f = build();
let value = Value::NodeSet::<crate::document::fixture::TestNode>(Vec::new());
assert_eq!(value.to_xpath_string(), "");
let value = Value::NodeSet(vec![f.doc.node(f.text0)]);
assert_eq!(value.to_xpath_string(), "Hello ");
let value = Value::NodeSet(vec![f.doc.node(f.text2), f.doc.node(f.text0)]);
assert_eq!(value.to_xpath_string(), "Hello ");
}
}