use super::temporal::{Duration, Temporal};
use crate::xml::{Document, NodeId};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Value {
NodeSet(Vec<NodeId>),
Boolean(bool),
Number(f64),
String(String),
Sequence(Vec<Item>),
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Item {
Node(NodeId),
String(String),
Number(f64),
Boolean(bool),
Temporal(Temporal),
Duration(Duration),
}
impl Item {
#[must_use]
pub fn to_xpath_string(&self, document: &Document) -> String {
match self {
Item::Node(node) => document.string_value(*node),
Item::String(text) => text.clone(),
Item::Number(number) => format_number(*number),
Item::Boolean(boolean) => if *boolean { "true" } else { "false" }.to_string(),
Item::Temporal(temporal) => temporal.to_lexical(),
Item::Duration(duration) => duration.to_lexical(),
}
}
#[must_use]
pub fn to_number(&self, document: &Document) -> f64 {
match self {
Item::Node(node) => parse_number(&document.string_value(*node)),
Item::String(text) => parse_number(text),
Item::Number(number) => *number,
Item::Boolean(boolean) => f64::from(u8::from(*boolean)),
Item::Temporal(_) | Item::Duration(_) => f64::NAN,
}
}
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Item::Node(_) => "node",
Item::String(_) => "string",
Item::Number(_) => "number",
Item::Boolean(_) => "boolean",
Item::Temporal(temporal) => temporal.kind().as_str(),
Item::Duration(duration) => duration.kind().as_str(),
}
}
#[must_use]
pub const fn as_duration(&self) -> Option<&Duration> {
match self {
Item::Duration(duration) => Some(duration),
_ => None,
}
}
#[must_use]
pub const fn as_temporal(&self) -> Option<&Temporal> {
match self {
Item::Temporal(temporal) => Some(temporal),
_ => None,
}
}
}
#[must_use]
pub fn flatten_into_sequence(values: Vec<Value>) -> Vec<Item> {
let mut items = Vec::new();
for value in values {
match value {
Value::Sequence(inner) => items.extend(inner),
Value::NodeSet(nodes) => items.extend(nodes.into_iter().map(Item::Node)),
Value::Boolean(boolean) => items.push(Item::Boolean(boolean)),
Value::Number(number) => items.push(Item::Number(number)),
Value::String(text) => items.push(Item::String(text)),
}
}
items
}
impl Value {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Value::NodeSet(_) => "node-set",
Value::Boolean(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Sequence(_) => "sequence",
}
}
#[must_use]
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(),
Value::Sequence(items) => match items.as_slice() {
[] => false,
[Item::Boolean(boolean)] => *boolean,
[Item::String(text)] => !text.is_empty(),
[Item::Number(number)] => *number != 0.0 && !number.is_nan(),
_ => true,
},
}
}
pub fn effective_boolean_value(&self) -> Result<bool, String> {
if let Value::Sequence(items) = self {
match items.as_slice() {
[] | [Item::Node(_), ..] | [_] => {}
_ => {
return Err(format!(
"a sequence of {} atomic items has no effective boolean value; \
XPath 2.0 makes this a type error",
items.len()
))
}
}
}
Ok(self.to_boolean())
}
#[must_use]
pub fn to_boolean_scalar(&self) -> bool {
self.to_boolean()
}
#[must_use]
pub fn to_xpath_string(&self, document: &Document) -> String {
match self {
Value::NodeSet(nodes) => nodes
.first()
.map_or_else(String::new, |&n| document.string_value(n)),
Value::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
Value::Number(n) => format_number(*n),
Value::String(s) => s.clone(),
Value::Sequence(items) => items
.first()
.map_or_else(String::new, |item| item.to_xpath_string(document)),
}
}
#[must_use]
pub fn to_xpath_string_scalar(&self) -> String {
match self {
Value::NodeSet(_) | Value::Sequence(_) => String::new(),
Value::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
Value::Number(n) => format_number(*n),
Value::String(s) => s.clone(),
}
}
#[must_use]
pub fn to_number(&self, document: &Document) -> f64 {
match self {
Value::Number(n) => *n,
Value::Boolean(b) => f64::from(u8::from(*b)),
Value::String(s) => parse_number(s),
Value::NodeSet(_) | Value::Sequence(_) => {
parse_number(&self.to_xpath_string(document))
}
}
}
#[must_use]
pub fn as_sequence(&self) -> Option<&[Item]> {
match self {
Value::Sequence(items) => Some(items),
_ => None,
}
}
#[must_use]
pub fn into_items(self) -> Vec<Item> {
flatten_into_sequence(vec![self])
}
#[must_use]
pub fn as_node_set(&self) -> Option<&[NodeId]> {
match self {
Value::NodeSet(nodes) => Some(nodes),
_ => None,
}
}
}
#[must_use]
pub fn parse_number(input: &str) -> f64 {
let trimmed = input.trim_matches(|c: char| c.is_ascii_whitespace());
if trimmed.is_empty() {
return f64::NAN;
}
let body = trimmed.strip_prefix('-').unwrap_or(trimmed);
let valid = !body.is_empty()
&& body.chars().all(|c| c.is_ascii_digit() || c == '.')
&& body.matches('.').count() <= 1
&& body != ".";
if !valid {
return f64::NAN;
}
trimmed.parse::<f64>().unwrap_or(f64::NAN)
}
#[must_use]
pub fn format_number(value: f64) -> String {
if value.is_nan() {
return "NaN".to_string();
}
if value.is_infinite() {
return if value > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
}
if value == 0.0 {
return "0".to_string();
}
if value.fract() == 0.0 && value.abs() < 1e21 {
return format!("{value:.0}");
}
let shortest = format!("{value}");
if !shortest.contains('e') && !shortest.contains('E') {
return shortest;
}
expand_exponential(&shortest)
}
fn expand_exponential(input: &str) -> String {
let (mantissa, exponent) = input
.split_once(['e', 'E'])
.expect("caller checked that an exponent is present");
let exponent: i32 = exponent.parse().unwrap_or(0);
let negative = mantissa.starts_with('-');
let mantissa = mantissa.trim_start_matches('-');
let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let digits: String = format!("{int_part}{frac_part}");
let point = i32::try_from(int_part.len()).unwrap_or(i32::MAX) + exponent;
let mut out = String::new();
if negative {
out.push('-');
}
if point <= 0 {
out.push_str("0.");
for _ in 0..-point {
out.push('0');
}
out.push_str(digits.trim_end_matches('0'));
} else {
let point = usize::try_from(point).unwrap_or(0);
if point >= digits.len() {
out.push_str(&digits);
for _ in 0..point - digits.len() {
out.push('0');
}
} else {
out.push_str(&digits[..point]);
let tail = digits[point..].trim_end_matches('0');
if !tail.is_empty() {
out.push('.');
out.push_str(tail);
}
}
}
if out.is_empty() || out == "-" {
out.push('0');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_integers_without_a_point() {
assert_eq!(format_number(3.0), "3");
assert_eq!(format_number(-3.0), "-3");
assert_eq!(format_number(0.0), "0");
assert_eq!(format_number(-0.0), "0");
}
#[test]
fn format_fractions_shortest_round_trip() {
assert_eq!(format_number(0.5), "0.5");
assert_eq!(format_number(-1.25), "-1.25");
assert_eq!(format_number(1.0 / 3.0), "0.3333333333333333");
}
#[test]
fn format_specials() {
assert_eq!(format_number(f64::NAN), "NaN");
assert_eq!(format_number(f64::INFINITY), "Infinity");
assert_eq!(format_number(f64::NEG_INFINITY), "-Infinity");
}
#[test]
fn format_never_uses_exponential_notation() {
assert_eq!(format_number(1e21), "1000000000000000000000");
assert_eq!(format_number(1e-7), "0.0000001");
assert!(!format_number(1e300).contains('e'));
}
#[test]
fn parse_rejects_what_xpath_rejects() {
assert!(parse_number("1e5").is_nan());
assert!(parse_number("inf").is_nan());
assert!(parse_number("NaN").is_nan());
assert!(parse_number("0x10").is_nan());
assert!(parse_number("").is_nan());
assert!(parse_number("+1").is_nan());
}
#[test]
fn parse_accepts_what_xpath_accepts() {
assert_eq!(parse_number(" 42 "), 42.0);
assert_eq!(parse_number("-1.5"), -1.5);
assert_eq!(parse_number(".5"), 0.5);
assert_eq!(parse_number("5."), 5.0);
}
#[test]
fn boolean_conversion() {
assert!(Value::Number(1.0).to_boolean());
assert!(!Value::Number(0.0).to_boolean());
assert!(!Value::Number(f64::NAN).to_boolean());
assert!(!Value::NodeSet(vec![]).to_boolean());
assert!(Value::String("0".into()).to_boolean());
}
}