1use crate::document::Node;
6
7#[derive(Clone, PartialEq)]
17pub enum Value<N> {
18 NodeSet(Vec<N>),
19 Boolean(bool),
20 Number(f64),
22 String(String),
23}
24
25impl<N: std::fmt::Debug> std::fmt::Debug for Value<N> {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Value::NodeSet(nodes) => f.debug_tuple("NodeSet").field(nodes).finish(),
32 Value::Boolean(b) => f.debug_tuple("Boolean").field(b).finish(),
33 Value::Number(n) => f.debug_tuple("Number").field(n).finish(),
34 Value::String(s) => f.debug_tuple("String").field(s).finish(),
35 }
36 }
37}
38
39impl<'a, N: Node<'a>> Value<N> {
40 pub fn to_boolean(&self) -> bool {
45 match self {
46 Value::NodeSet(nodes) => !nodes.is_empty(),
47 Value::Boolean(b) => *b,
48 Value::Number(n) => *n != 0.0 && !n.is_nan(),
49 Value::String(s) => !s.is_empty(),
50 }
51 }
52
53 pub fn to_number(&self) -> f64 {
57 match self {
58 Value::NodeSet(_) => string_to_number(&self.to_xpath_string()),
59 Value::Boolean(b) => {
60 if *b {
61 1.0
62 } else {
63 0.0
64 }
65 }
66 Value::Number(n) => *n,
67 Value::String(s) => string_to_number(s),
68 }
69 }
70
71 pub fn to_xpath_string(&self) -> String {
81 match self {
82 Value::NodeSet(nodes) => first_in_document_order(nodes)
83 .map(|n| n.string_value())
84 .unwrap_or_default(),
85 Value::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
86 Value::Number(n) => number_to_string(*n),
87 Value::String(s) => s.clone(),
88 }
89 }
90}
91
92fn first_in_document_order<'a, N: Node<'a>>(nodes: &[N]) -> Option<N> {
96 nodes.iter().copied().min_by(|a, b| a.document_order(*b))
97}
98
99fn number_to_string(n: f64) -> String {
117 if n.is_nan() {
118 return "NaN".to_string();
119 }
120 if n.is_infinite() {
121 return if n > 0.0 {
122 "Infinity".to_string()
123 } else {
124 "-Infinity".to_string()
125 };
126 }
127 if n == 0.0 {
128 return "0".to_string();
131 }
132 n.to_string()
133}
134
135pub(crate) fn string_to_number(s: &str) -> f64 {
149 let trimmed = s.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\r' | '\n'));
150 let (negative, rest) = match trimmed.strip_prefix('-') {
151 Some(r) => (true, r),
152 None => (false, trimmed),
153 };
154 let b = rest.as_bytes();
155 let mut pos = 0;
156 while pos < b.len() && b[pos].is_ascii_digit() {
157 pos += 1;
158 }
159 let has_int_digits = pos > 0;
160 let mut has_frac_digits = false;
161 if pos < b.len() && b[pos] == b'.' {
162 pos += 1;
163 let frac_start = pos;
164 while pos < b.len() && b[pos].is_ascii_digit() {
165 pos += 1;
166 }
167 has_frac_digits = pos > frac_start;
168 }
169 if pos != b.len() || !(has_int_digits || has_frac_digits) {
172 return f64::NAN;
173 }
174 let digits = &rest[..pos];
175 let full = if negative {
176 format!("-{digits}")
177 } else {
178 digits.to_string()
179 };
180 full.parse::<f64>().unwrap_or(f64::NAN)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::document::fixture::build;
187
188 #[test]
191 fn boolean_number_rules() {
192 assert!(!Value::<crate::document::fixture::TestNode>::Number(0.0).to_boolean());
193 assert!(!Value::<crate::document::fixture::TestNode>::Number(-0.0).to_boolean());
194 assert!(!Value::<crate::document::fixture::TestNode>::Number(f64::NAN).to_boolean());
195 assert!(Value::<crate::document::fixture::TestNode>::Number(1.0).to_boolean());
196 assert!(Value::<crate::document::fixture::TestNode>::Number(-1.0).to_boolean());
197 }
198
199 #[test]
200 fn boolean_string_rules() {
201 assert!(!Value::<crate::document::fixture::TestNode>::String(String::new()).to_boolean());
202 assert!(Value::<crate::document::fixture::TestNode>::String("x".into()).to_boolean());
203 }
204
205 #[test]
206 fn boolean_node_set_rules() {
207 let f = build();
208 assert!(!Value::NodeSet::<crate::document::fixture::TestNode>(Vec::new()).to_boolean());
209 assert!(Value::NodeSet(vec![f.doc.node(f.span)]).to_boolean());
210 }
211
212 #[test]
215 fn number_boolean_rules() {
216 assert_eq!(
217 Value::<crate::document::fixture::TestNode>::Boolean(true).to_number(),
218 1.0
219 );
220 assert_eq!(
221 Value::<crate::document::fixture::TestNode>::Boolean(false).to_number(),
222 0.0
223 );
224 }
225
226 #[test]
227 fn number_string_valid_forms() {
228 assert_eq!(string_to_number("42"), 42.0);
229 assert_eq!(string_to_number("42.5"), 42.5);
230 assert_eq!(string_to_number(".5"), 0.5);
231 assert_eq!(string_to_number("5."), 5.0);
232 assert_eq!(string_to_number("-42"), -42.0);
233 assert_eq!(string_to_number(" 42 "), 42.0);
234 assert_eq!(string_to_number("\t-3.5\n"), -3.5);
235 }
236
237 #[test]
238 fn number_string_malformed_is_nan() {
239 assert!(string_to_number("abc").is_nan());
240 assert!(string_to_number("").is_nan());
241 assert!(string_to_number("--5").is_nan());
242 assert!(string_to_number("1.2.3").is_nan());
243 assert!(
244 string_to_number("1e5").is_nan(),
245 "exponent notation is not XPath Number syntax"
246 );
247 assert!(
248 string_to_number("+5").is_nan(),
249 "leading plus is not permitted"
250 );
251 assert!(
252 string_to_number("5 6").is_nan(),
253 "embedded whitespace is not permitted"
254 );
255 assert!(string_to_number("Infinity").is_nan());
256 assert!(string_to_number("NaN").is_nan());
257 }
258
259 #[test]
260 fn number_node_set_uses_first_in_document_order_string_value() {
261 let f = build();
262 let value = Value::NodeSet(vec![f.doc.node(f.span_attr)]);
264 assert_eq!(value.to_number(), 1.0);
265 let value = Value::NodeSet(vec![f.doc.node(f.text2), f.doc.node(f.comment)]);
269 assert!(
270 value.to_number().is_nan(),
271 "comment's string-value ('note') is not a Number"
272 );
273 }
274
275 #[test]
276 fn number_empty_node_set_is_nan() {
277 let value = Value::NodeSet::<crate::document::fixture::TestNode>(Vec::new());
278 assert!(value.to_number().is_nan());
279 }
280
281 #[test]
284 fn string_boolean_rules() {
285 assert_eq!(
286 Value::<crate::document::fixture::TestNode>::Boolean(true).to_xpath_string(),
287 "true"
288 );
289 assert_eq!(
290 Value::<crate::document::fixture::TestNode>::Boolean(false).to_xpath_string(),
291 "false"
292 );
293 }
294
295 #[test]
296 fn string_number_special_values() {
297 assert_eq!(
298 Value::<crate::document::fixture::TestNode>::Number(f64::NAN).to_xpath_string(),
299 "NaN"
300 );
301 assert_eq!(
302 Value::<crate::document::fixture::TestNode>::Number(0.0).to_xpath_string(),
303 "0"
304 );
305 assert_eq!(
306 Value::<crate::document::fixture::TestNode>::Number(-0.0).to_xpath_string(),
307 "0"
308 );
309 assert_eq!(
310 Value::<crate::document::fixture::TestNode>::Number(f64::INFINITY).to_xpath_string(),
311 "Infinity"
312 );
313 assert_eq!(
314 Value::<crate::document::fixture::TestNode>::Number(f64::NEG_INFINITY)
315 .to_xpath_string(),
316 "-Infinity"
317 );
318 }
319
320 #[test]
321 #[allow(clippy::approx_constant)] fn string_number_integer_vs_decimal_formatting() {
323 assert_eq!(
324 Value::<crate::document::fixture::TestNode>::Number(3.0).to_xpath_string(),
325 "3"
326 );
327 assert_eq!(
328 Value::<crate::document::fixture::TestNode>::Number(-3.0).to_xpath_string(),
329 "-3"
330 );
331 assert_eq!(
332 Value::<crate::document::fixture::TestNode>::Number(100.0).to_xpath_string(),
333 "100"
334 );
335 assert_eq!(
336 Value::<crate::document::fixture::TestNode>::Number(3.14).to_xpath_string(),
337 "3.14"
338 );
339 assert_eq!(
340 Value::<crate::document::fixture::TestNode>::Number(0.5).to_xpath_string(),
341 "0.5"
342 );
343 assert_eq!(
344 Value::<crate::document::fixture::TestNode>::Number(-0.5).to_xpath_string(),
345 "-0.5"
346 );
347 }
348
349 #[test]
350 fn string_node_set_rules() {
351 let f = build();
352 let value = Value::NodeSet::<crate::document::fixture::TestNode>(Vec::new());
353 assert_eq!(value.to_xpath_string(), "");
354 let value = Value::NodeSet(vec![f.doc.node(f.text0)]);
355 assert_eq!(value.to_xpath_string(), "Hello ");
356 let value = Value::NodeSet(vec![f.doc.node(f.text2), f.doc.node(f.text0)]);
358 assert_eq!(value.to_xpath_string(), "Hello ");
359 }
360}