use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::Ordering;
use super::ast::BinaryOp;
use super::float;
use super::value::{Value, string_value};
use crate::tree::{Document, NodeId};
pub(crate) fn ieee_eq(a: f64, b: f64) -> bool {
a.partial_cmp(&b) == Some(Ordering::Equal)
}
pub(crate) fn is_xml_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\r' | '\n')
}
pub(crate) fn xpath_number(s: &str) -> f64 {
let trimmed = s.trim_matches(is_xml_whitespace);
if !is_xpath_number(trimmed) {
return f64::NAN;
}
trimmed.parse().unwrap_or(f64::NAN)
}
fn is_xpath_number(s: &str) -> bool {
let digits = |part: &str| {
!part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())
};
let unsigned = s.strip_prefix('-').unwrap_or(s);
match unsigned.split_once('.') {
None => digits(unsigned),
Some((int, frac)) => {
(digits(int) && (frac.is_empty() || digits(frac)))
|| (int.is_empty() && digits(frac))
}
}
}
pub(crate) fn xpath_round(n: f64) -> f64 {
if !n.is_finite() {
return n;
}
let down = float::floor(n);
let rounded = if n - down >= 0.5 { down + 1.0 } else { down };
if ieee_eq(rounded, 0.0) && n.is_sign_negative() {
return -0.0;
}
rounded
}
pub(crate) fn compare(
doc: &Document,
op: BinaryOp,
l: &Value,
r: &Value,
) -> bool {
match (l, r) {
(Value::NodeSet(a), Value::NodeSet(b)) => compare_sets(doc, op, a, b),
(Value::NodeSet(a), other) => compare_set_with(doc, op, a, other),
(other, Value::NodeSet(b)) => compare_set_with(doc, flip(op), b, other),
_ => compare_scalars(doc, op, l, r),
}
}
fn flip(op: BinaryOp) -> BinaryOp {
match op {
BinaryOp::Lt => BinaryOp::Gt,
BinaryOp::Le => BinaryOp::Ge,
BinaryOp::Gt => BinaryOp::Lt,
BinaryOp::Ge => BinaryOp::Le,
other => other,
}
}
fn compare_scalars(doc: &Document, op: BinaryOp, l: &Value, r: &Value) -> bool {
let equal = match (l, r) {
(Value::Boolean(_), _) | (_, Value::Boolean(_)) => {
l.to_boolean() == r.to_boolean()
}
(Value::Number(_), _) | (_, Value::Number(_)) => {
ieee_eq(l.to_number(doc), r.to_number(doc))
}
_ => l.to_str(doc) == r.to_str(doc),
};
match op {
BinaryOp::Eq => equal,
BinaryOp::Ne => !equal,
_ => compare_numbers(op, l.to_number(doc), r.to_number(doc)),
}
}
fn compare_set_with(
doc: &Document,
op: BinaryOp,
set: &[NodeId],
other: &Value,
) -> bool {
match other {
Value::Boolean(_) => {
compare_scalars(doc, op, &Value::Boolean(!set.is_empty()), other)
}
Value::String(s) if matches!(op, BinaryOp::Eq | BinaryOp::Ne) => {
let want = op == BinaryOp::Eq;
set.iter().any(|&id| (string_value(doc, id) == *s) == want)
}
_ => {
let n = other.to_number(doc);
set.iter().any(|&id| {
compare_numbers(op, xpath_number(&string_value(doc, id)), n)
})
}
}
}
fn compare_sets(
doc: &Document,
op: BinaryOp,
a: &[NodeId],
b: &[NodeId],
) -> bool {
if a.is_empty() || b.is_empty() {
return false;
}
match op {
BinaryOp::Eq => sets_share_a_value(doc, a, b),
BinaryOp::Ne => sets_differ_somewhere(doc, a, b),
_ => sets_compare_at_the_extremes(doc, op, a, b),
}
}
fn sets_share_a_value(doc: &Document, a: &[NodeId], b: &[NodeId]) -> bool {
let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
let values: BTreeSet<String> =
small.iter().map(|&id| string_value(doc, id)).collect();
large
.iter()
.any(|&id| values.contains(&string_value(doc, id)))
}
fn sets_differ_somewhere(doc: &Document, a: &[NodeId], b: &[NodeId]) -> bool {
let va: BTreeSet<String> =
a.iter().map(|&id| string_value(doc, id)).collect();
let vb: BTreeSet<String> =
b.iter().map(|&id| string_value(doc, id)).collect();
va.len() > 1 || vb.len() > 1 || va != vb
}
fn sets_compare_at_the_extremes(
doc: &Document,
op: BinaryOp,
a: &[NodeId],
b: &[NodeId],
) -> bool {
let (min_a, max_a) = extremes(doc, a);
let (min_b, max_b) = extremes(doc, b);
match op {
BinaryOp::Lt => min_a < max_b,
BinaryOp::Le => min_a <= max_b,
BinaryOp::Gt => max_a > min_b,
_ => max_a >= min_b,
}
}
fn extremes(doc: &Document, set: &[NodeId]) -> (f64, f64) {
let numbers: Vec<f64> = set
.iter()
.map(|&id| xpath_number(&string_value(doc, id)))
.filter(|n| !n.is_nan())
.collect();
let min = numbers.iter().copied().fold(f64::NAN, f64::min);
let max = numbers.iter().copied().fold(f64::NAN, f64::max);
(min, max)
}
pub(crate) fn compare_numbers(op: BinaryOp, a: f64, b: f64) -> bool {
match op {
BinaryOp::Eq => ieee_eq(a, b),
BinaryOp::Ne => !ieee_eq(a, b),
BinaryOp::Lt => a < b,
BinaryOp::Le => a <= b,
BinaryOp::Gt => a > b,
BinaryOp::Ge => a >= b,
_ => unreachable!("not a comparison operator"),
}
}