1use crate::error::{ErrorDomain, ErrorLevel, XmlError};
19use crate::xpath::eval::{Numeric, Value, value_to_number};
20use crate::xpath::index::DocIndexLike;
21
22use super::Result;
23
24fn err(msg: impl Into<String>) -> XmlError {
25 XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
26}
27
28pub fn dispatch<I: DocIndexLike>(
29 name: &str, args: Vec<Value>, idx: &I,
30) -> Option<Result<Value>> {
31 let r = match name {
32 "min" => reduce(&args, idx, f64::INFINITY, |acc, x| if x < acc { x } else { acc }),
34 "max" => reduce(&args, idx, f64::NEG_INFINITY, |acc, x| if x > acc { x } else { acc }),
35 "highest" => extremum(&args, idx, |a, b| a > b),
39 "lowest" => extremum(&args, idx, |a, b| a < b),
40
41 "abs" => one_num(&args, idx, |n| n.abs()),
43 "sqrt" => one_num(&args, idx, |n| n.sqrt()),
44 "exp" => one_num(&args, idx, |n| n.exp()),
45 "log" => one_num(&args, idx, |n| n.ln()),
46 "sin" => one_num(&args, idx, |n| n.sin()),
47 "cos" => one_num(&args, idx, |n| n.cos()),
48 "tan" => one_num(&args, idx, |n| n.tan()),
49 "asin" => one_num(&args, idx, |n| n.asin()),
50 "acos" => one_num(&args, idx, |n| n.acos()),
51 "atan" => one_num(&args, idx, |n| n.atan()),
52
53 "power" => two_num(&args, idx, |a, b| a.powf(b)),
55 "atan2" => two_num(&args, idx, |a, b| a.atan2(b)),
56
57 "constant" => constant(&args, idx),
62
63 "random" => random_value(&args),
65
66 _ => return None,
67 };
68 Some(r)
69}
70
71fn one_num<I: DocIndexLike>(
74 args: &[Value], idx: &I, f: impl Fn(f64) -> f64,
75) -> Result<Value> {
76 if args.len() != 1 {
77 return Err(err("math: function requires 1 argument"));
78 }
79 Ok(Value::Number(Numeric::Double(f(value_to_number(&args[0], idx)))))
80}
81
82fn two_num<I: DocIndexLike>(
83 args: &[Value], idx: &I, f: impl Fn(f64, f64) -> f64,
84) -> Result<Value> {
85 if args.len() != 2 {
86 return Err(err("math: function requires 2 arguments"));
87 }
88 Ok(Value::Number(Numeric::Double(f(
89 value_to_number(&args[0], idx),
90 value_to_number(&args[1], idx),
91 ))))
92}
93
94fn reduce<I: DocIndexLike>(
99 args: &[Value], idx: &I, init: f64, op: impl Fn(f64, f64) -> f64,
100) -> Result<Value> {
101 if args.len() != 1 {
102 return Err(err("math:min/max takes a single nodeset argument"));
103 }
104 let ns = match &args[0] {
105 Value::NodeSet(ns) => ns,
106 _ => return Err(err("math:min/max requires a nodeset argument")),
107 };
108 if ns.is_empty() {
109 return Ok(Value::Number(Numeric::Double(f64::NAN)));
110 }
111 let mut acc = init;
112 for &id in ns {
113 let n: f64 = idx.string_value(id).trim().parse().unwrap_or(f64::NAN);
114 if n.is_nan() {
115 return Ok(Value::Number(Numeric::Double(f64::NAN)));
116 }
117 acc = op(acc, n);
118 }
119 Ok(Value::Number(Numeric::Double(acc)))
120}
121
122fn extremum<I: DocIndexLike>(
127 args: &[Value], idx: &I, better: impl Fn(f64, f64) -> bool,
128) -> Result<Value> {
129 if args.len() != 1 {
130 return Err(err("math:highest/lowest takes a single nodeset argument"));
131 }
132 let ns = match &args[0] {
133 Value::NodeSet(ns) => ns.clone(),
134 _ => return Err(err("math:highest/lowest requires a nodeset argument")),
135 };
136 if ns.is_empty() {
137 return Ok(Value::NodeSet(Vec::new()));
138 }
139 let nums: Vec<f64> = ns.iter()
140 .map(|&id| idx.string_value(id).trim().parse().unwrap_or(f64::NAN))
141 .collect();
142 if nums.iter().any(|n| n.is_nan()) {
143 return Ok(Value::NodeSet(Vec::new()));
146 }
147 let mut best = nums[0];
148 for &n in &nums[1..] {
149 if better(n, best) { best = n; }
150 }
151 let out: Vec<_> = ns.iter().zip(nums.iter())
152 .filter(|(_, n)| **n == best)
153 .map(|(id, _)| *id)
154 .collect();
155 Ok(Value::NodeSet(out))
156}
157
158fn constant<I: DocIndexLike>(args: &[Value], idx: &I) -> Result<Value> {
159 if args.is_empty() || args.len() > 2 {
160 return Err(err("math:constant requires 1 or 2 arguments"));
161 }
162 let name = match &args[0] {
163 Value::String(s) => s.clone(),
164 v => crate::xpath::eval::value_to_string(v, idx),
165 };
166 let raw = match name.as_str() {
179 "PI" => std::f64::consts::PI,
180 "E" => std::f64::consts::E,
181 "SQRRT2" => std::f64::consts::SQRT_2,
182 "LN2" => std::f64::consts::LN_2,
183 "LN10" => std::f64::consts::LN_10,
184 "LOG2E" => std::f64::consts::LOG2_E,
185 "SQRT1_2" => std::f64::consts::FRAC_1_SQRT_2,
186 _ => return Ok(Value::Number(Numeric::Double(f64::NAN))),
187 };
188 if args.len() == 1 {
189 return Ok(Value::Number(Numeric::Double(raw)));
190 }
191 let prec = value_to_number(&args[1], idx);
196 if !prec.is_finite() || prec < 2.0 {
197 return Ok(Value::Number(Numeric::Double(raw)));
200 }
201 let sig_figs = (prec as i32) - 1;
202 let scale = 10f64.powi(sig_figs - 1 - (raw.abs().log10().floor() as i32));
203 Ok(Value::Number(Numeric::Double((raw * scale).round() / scale)))
204}
205
206fn random_value(args: &[Value]) -> Result<Value> {
207 if !args.is_empty() {
208 return Err(err("math:random takes no arguments"));
209 }
210 use std::cell::Cell;
215 std::thread_local! {
216 static STATE: Cell<u64> = const { Cell::new(0x9E37_79B9_7F4A_7C15) };
217 }
218 let v = STATE.with(|s| {
219 let mut x = s.get();
220 if x == 0 {
221 x = (s as *const _ as u64) ^ 0xDEAD_BEEF_DEAD_BEEF;
227 }
228 x ^= x << 13;
229 x ^= x >> 7;
230 x ^= x << 17;
231 s.set(x);
232 x
233 });
234 Ok(Value::Number(Numeric::Double((v >> 11) as f64 / (1u64 << 53) as f64)))
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::xpath::XPathContext;
242 use crate::{parse_str, ParseOptions};
243
244 fn assert_num(r: Result<Value>, expected: f64) {
245 match r.unwrap() {
246 Value::Number(n) if (n.as_f64() - expected).abs() < 1e-12 => {}
247 other => panic!("expected number ~{expected}, got {other:?}"),
248 }
249 }
250
251 fn tiny_doc() -> sup_xml_tree::dom::Document {
252 parse_str("<r/>", &ParseOptions::default()).unwrap()
253 }
254
255 #[test]
256 fn scalar_sqrt() {
257 let doc = tiny_doc();
258 let ctx = XPathContext::new(&doc);
259 assert_num(
260 dispatch("sqrt", vec![Value::Number(Numeric::Double(16.0))], &ctx.index).unwrap(),
261 4.0,
262 );
263 }
264
265 #[test]
266 fn scalar_power() {
267 let doc = tiny_doc();
268 let ctx = XPathContext::new(&doc);
269 assert_num(
270 dispatch("power",
271 vec![Value::Number(Numeric::Double(2.0)), Value::Number(Numeric::Double(10.0))], &ctx.index).unwrap(),
272 1024.0,
273 );
274 }
275
276 #[test]
277 fn scalar_abs() {
278 let doc = tiny_doc();
279 let ctx = XPathContext::new(&doc);
280 assert_num(
281 dispatch("abs", vec![Value::Number(Numeric::Double(-7.5))], &ctx.index).unwrap(),
282 7.5,
283 );
284 }
285
286 #[test]
287 fn constant_pi_with_precision_truncates() {
288 let doc = tiny_doc();
289 let ctx = XPathContext::new(&doc);
290 let r = dispatch("constant",
291 vec![Value::String("PI".into()), Value::Number(Numeric::Double(5.0))],
292 &ctx.index).unwrap().unwrap();
293 match r {
294 Value::Number(n) => assert!((n.as_f64() - 3.1416).abs() < 1e-3),
295 _ => panic!("expected number"),
296 }
297 }
298
299 #[test]
300 fn unknown_returns_none() {
301 let doc = tiny_doc();
302 let ctx = XPathContext::new(&doc);
303 assert!(dispatch("does-not-exist", vec![], &ctx.index).is_none());
304 }
305
306 #[test]
307 fn min_max_over_nodeset() {
308 let doc = parse_str(
309 "<r><i>3</i><i>1</i><i>5</i><i>2</i></r>",
310 &ParseOptions::default(),
311 ).unwrap();
312 let ctx = XPathContext::new(&doc);
313 let ns = ctx.eval("/r/i").unwrap();
314 assert_num(dispatch("max", vec![ns.clone()], &ctx.index).unwrap(), 5.0);
315 assert_num(dispatch("min", vec![ns], &ctx.index).unwrap(), 1.0);
316 }
317
318 #[test]
319 fn highest_returns_nodes_with_max_value() {
320 let doc = parse_str(
321 "<r><i>3</i><i>9</i><i>5</i><i>9</i></r>",
322 &ParseOptions::default(),
323 ).unwrap();
324 let ctx = XPathContext::new(&doc);
325 let ns = ctx.eval("/r/i").unwrap();
326 match dispatch("highest", vec![ns], &ctx.index).unwrap().unwrap() {
327 Value::NodeSet(ids) => assert_eq!(ids.len(), 2),
329 other => panic!("expected nodeset, got {other:?}"),
330 }
331 }
332
333 #[test]
334 fn empty_nodeset_min_is_nan() {
335 let doc = parse_str("<r/>", &ParseOptions::default()).unwrap();
336 let ctx = XPathContext::new(&doc);
337 let r = dispatch("min", vec![Value::NodeSet(vec![])],
338 &ctx.index).unwrap().unwrap();
339 match r {
340 Value::Number(n) => assert!(n.as_f64().is_nan()),
341 _ => panic!("expected NaN"),
342 }
343 }
344
345 #[test]
348 fn scalar_trig_functions() {
349 let doc = tiny_doc();
350 let ctx = XPathContext::new(&doc);
351 let pi = std::f64::consts::PI;
352 assert_num(dispatch("sin", vec![Value::Number(Numeric::Double(0.0))], &ctx.index).unwrap(), 0.0);
353 assert_num(dispatch("cos", vec![Value::Number(Numeric::Double(0.0))], &ctx.index).unwrap(), 1.0);
354 assert_num(dispatch("tan", vec![Value::Number(Numeric::Double(0.0))], &ctx.index).unwrap(), 0.0);
355 assert_num(dispatch("asin", vec![Value::Number(Numeric::Double(0.0))], &ctx.index).unwrap(), 0.0);
356 assert_num(dispatch("acos", vec![Value::Number(Numeric::Double(1.0))], &ctx.index).unwrap(), 0.0);
357 assert_num(dispatch("atan", vec![Value::Number(Numeric::Double(0.0))], &ctx.index).unwrap(), 0.0);
358 assert_num(dispatch("atan2",
359 vec![Value::Number(Numeric::Double(0.0)), Value::Number(Numeric::Double(1.0))], &ctx.index).unwrap(), 0.0);
360 assert_num(dispatch("sin", vec![Value::Number(Numeric::Double(pi / 2.0))], &ctx.index).unwrap(), 1.0);
362 }
363
364 #[test]
365 fn scalar_exp_log() {
366 let doc = tiny_doc();
367 let ctx = XPathContext::new(&doc);
368 assert_num(dispatch("exp", vec![Value::Number(Numeric::Double(0.0))], &ctx.index).unwrap(), 1.0);
369 assert_num(dispatch("log", vec![Value::Number(Numeric::Double(std::f64::consts::E))],
370 &ctx.index).unwrap(), 1.0);
371 }
372
373 #[test]
376 fn one_num_wrong_argc_errors() {
377 let doc = tiny_doc();
378 let ctx = XPathContext::new(&doc);
379 let r = dispatch("sqrt", vec![], &ctx.index).unwrap();
380 assert!(r.is_err());
381 let r = dispatch("sqrt",
382 vec![Value::Number(Numeric::Double(1.0)), Value::Number(Numeric::Double(2.0))], &ctx.index).unwrap();
383 assert!(r.is_err());
384 }
385
386 #[test]
387 fn two_num_wrong_argc_errors() {
388 let doc = tiny_doc();
389 let ctx = XPathContext::new(&doc);
390 let r = dispatch("power", vec![Value::Number(Numeric::Double(1.0))], &ctx.index).unwrap();
391 assert!(r.is_err());
392 }
393
394 #[test]
395 fn reduce_wrong_argc_errors() {
396 let doc = tiny_doc();
397 let ctx = XPathContext::new(&doc);
398 assert!(dispatch("min", vec![], &ctx.index).unwrap().is_err());
399 assert!(dispatch("min",
400 vec![Value::NodeSet(vec![]), Value::NodeSet(vec![])],
401 &ctx.index).unwrap().is_err());
402 }
403
404 #[test]
405 fn reduce_wrong_type_errors() {
406 let doc = tiny_doc();
407 let ctx = XPathContext::new(&doc);
408 let r = dispatch("min", vec![Value::Number(Numeric::Double(3.0))], &ctx.index).unwrap();
409 assert!(r.is_err());
410 }
411
412 #[test]
413 fn reduce_nan_in_nodeset_propagates() {
414 let doc = parse_str(
415 "<r><i>3</i><i>not-a-number</i><i>1</i></r>",
416 &ParseOptions::default(),
417 ).unwrap();
418 let ctx = XPathContext::new(&doc);
419 let ns = ctx.eval("/r/i").unwrap();
420 let r = dispatch("max", vec![ns], &ctx.index).unwrap().unwrap();
421 match r {
422 Value::Number(n) => assert!(n.as_f64().is_nan()),
423 _ => panic!("expected NaN"),
424 }
425 }
426
427 #[test]
428 fn extremum_wrong_argc_errors() {
429 let doc = tiny_doc();
430 let ctx = XPathContext::new(&doc);
431 assert!(dispatch("highest", vec![], &ctx.index).unwrap().is_err());
432 }
433
434 #[test]
435 fn extremum_wrong_type_errors() {
436 let doc = tiny_doc();
437 let ctx = XPathContext::new(&doc);
438 let r = dispatch("lowest", vec![Value::Number(Numeric::Double(3.0))], &ctx.index).unwrap();
439 assert!(r.is_err());
440 }
441
442 #[test]
443 fn extremum_empty_nodeset_returns_empty_nodeset() {
444 let doc = tiny_doc();
445 let ctx = XPathContext::new(&doc);
446 let r = dispatch("highest", vec![Value::NodeSet(vec![])],
447 &ctx.index).unwrap().unwrap();
448 match r {
449 Value::NodeSet(ids) => assert!(ids.is_empty()),
450 _ => panic!("expected nodeset"),
451 }
452 }
453
454 #[test]
455 fn extremum_nan_in_nodeset_returns_empty() {
456 let doc = parse_str(
457 "<r><i>3</i><i>bogus</i><i>5</i></r>",
458 &ParseOptions::default(),
459 ).unwrap();
460 let ctx = XPathContext::new(&doc);
461 let ns = ctx.eval("/r/i").unwrap();
462 let r = dispatch("highest", vec![ns], &ctx.index).unwrap().unwrap();
463 match r {
464 Value::NodeSet(ids) => assert!(ids.is_empty()),
465 _ => panic!("expected empty nodeset"),
466 }
467 }
468
469 #[test]
470 fn lowest_returns_nodes_with_min_value() {
471 let doc = parse_str(
472 "<r><i>3</i><i>1</i><i>5</i><i>1</i></r>",
473 &ParseOptions::default(),
474 ).unwrap();
475 let ctx = XPathContext::new(&doc);
476 let ns = ctx.eval("/r/i").unwrap();
477 match dispatch("lowest", vec![ns], &ctx.index).unwrap().unwrap() {
478 Value::NodeSet(ids) => assert_eq!(ids.len(), 2),
479 other => panic!("expected nodeset, got {other:?}"),
480 }
481 }
482
483 #[test]
486 fn constant_all_named_values() {
487 let doc = tiny_doc();
488 let ctx = XPathContext::new(&doc);
489 let single = |name: &str| -> f64 {
490 match dispatch("constant",
491 vec![Value::String(name.into())], &ctx.index).unwrap().unwrap() {
492 Value::Number(n) => n.as_f64(),
493 _ => panic!(),
494 }
495 };
496 assert!((single("PI") - std::f64::consts::PI).abs() < 1e-12);
497 assert!((single("E") - std::f64::consts::E).abs() < 1e-12);
498 assert!((single("SQRRT2") - std::f64::consts::SQRT_2).abs() < 1e-12);
501 assert!((single("LN2") - std::f64::consts::LN_2).abs() < 1e-12);
502 assert!((single("LN10") - std::f64::consts::LN_10).abs() < 1e-12);
503 assert!((single("LOG2E") - std::f64::consts::LOG2_E).abs() < 1e-12);
504 assert!((single("SQRT1_2") - std::f64::consts::FRAC_1_SQRT_2).abs() < 1e-12);
505 }
506
507 #[test]
508 fn constant_unknown_name_is_nan() {
509 let doc = tiny_doc();
510 let ctx = XPathContext::new(&doc);
511 let r = dispatch("constant",
512 vec![Value::String("BOGUS".into())], &ctx.index).unwrap().unwrap();
513 match r {
514 Value::Number(n) => assert!(n.as_f64().is_nan()),
515 _ => panic!(),
516 }
517 }
518
519 #[test]
520 fn constant_wrong_argc_errors() {
521 let doc = tiny_doc();
522 let ctx = XPathContext::new(&doc);
523 assert!(dispatch("constant", vec![], &ctx.index).unwrap().is_err());
524 assert!(dispatch("constant",
525 vec![Value::String("PI".into()),
526 Value::Number(Numeric::Double(1.0)),
527 Value::Number(Numeric::Double(2.0))],
528 &ctx.index).unwrap().is_err());
529 }
530
531 #[test]
532 fn constant_non_string_first_arg_coerced() {
533 let doc = tiny_doc();
536 let ctx = XPathContext::new(&doc);
537 let r = dispatch("constant",
538 vec![Value::Number(Numeric::Double(1.0))], &ctx.index).unwrap().unwrap();
539 match r {
540 Value::Number(n) => assert!(n.as_f64().is_nan()),
541 _ => panic!(),
542 }
543 }
544
545 #[test]
546 fn constant_non_finite_precision_returns_raw() {
547 let doc = tiny_doc();
549 let ctx = XPathContext::new(&doc);
550 let r = dispatch("constant",
551 vec![Value::String("PI".into()), Value::Number(Numeric::Double(f64::NAN))],
552 &ctx.index).unwrap().unwrap();
553 match r {
554 Value::Number(n) => assert!((n.as_f64() - std::f64::consts::PI).abs() < 1e-12),
555 _ => panic!(),
556 }
557 let r = dispatch("constant",
558 vec![Value::String("PI".into()), Value::Number(Numeric::Double(0.0))],
559 &ctx.index).unwrap().unwrap();
560 match r {
561 Value::Number(n) => assert!((n.as_f64() - std::f64::consts::PI).abs() < 1e-12),
562 _ => panic!(),
563 }
564 }
565
566 #[test]
569 fn random_value_in_unit_interval() {
570 let r = dispatch("random", vec![], &MockIdx).unwrap().unwrap();
574 match r {
575 Value::Number(n) => assert!((0.0..1.0).contains(&n.as_f64()), "got {}", n.as_f64()),
576 _ => panic!(),
577 }
578 let r = dispatch("random", vec![], &MockIdx).unwrap().unwrap();
580 assert!(matches!(r, Value::Number(n) if (0.0..1.0).contains(&n.as_f64())));
581 }
582
583 #[test]
584 fn random_rejects_args() {
585 let r = dispatch("random",
586 vec![Value::Number(Numeric::Double(1.0))], &MockIdx).unwrap();
587 assert!(r.is_err());
588 }
589
590 struct MockIdx;
592 impl DocIndexLike for MockIdx {
593 fn children(&self, _: NodeId) -> &[NodeId] { &[] }
594 fn parent(&self, _: NodeId) -> Option<NodeId> { None }
595 fn attr_range(&self, _: NodeId) -> std::ops::Range<NodeId> { 0..0 }
596 fn kind(&self, _: NodeId) -> crate::xpath::index::XPathNodeKind {
597 crate::xpath::index::XPathNodeKind::Document
598 }
599 fn pi_target(&self, _: NodeId) -> &str { "" }
600 fn string_value(&self, _: NodeId) -> String { String::new() }
601 fn node_name(&self, _: NodeId) -> &str { "" }
602 fn local_name(&self, _: NodeId) -> &str { "" }
603 fn namespace_uri(&self, _: NodeId) -> &str { "" }
604 }
605 use crate::xpath::index::NodeId;
606}