1use num_bigint::BigInt;
11use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
12use rust_decimal::Decimal;
13
14use crate::types::value::{XmlAtomicValue, XmlValue, XmlValueKind};
15use crate::types::XmlTypeCode;
16use crate::xpath::context::DynamicContext;
17use crate::xpath::error::XPathError;
18use crate::xpath::DomNavigator;
19
20use super::{atomize_to_single_opt, XPathValue};
21
22fn is_integer_type(code: XmlTypeCode) -> bool {
24 matches!(
25 code,
26 XmlTypeCode::Integer
27 | XmlTypeCode::NonPositiveInteger
28 | XmlTypeCode::NegativeInteger
29 | XmlTypeCode::Long
30 | XmlTypeCode::Int
31 | XmlTypeCode::Short
32 | XmlTypeCode::Byte
33 | XmlTypeCode::NonNegativeInteger
34 | XmlTypeCode::UnsignedLong
35 | XmlTypeCode::UnsignedInt
36 | XmlTypeCode::UnsignedShort
37 | XmlTypeCode::UnsignedByte
38 | XmlTypeCode::PositiveInteger
39 )
40}
41
42fn get_float(value: &XmlValue) -> Option<f32> {
44 match &value.value {
45 XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => Some(*f),
46 _ => None,
47 }
48}
49
50pub fn abs<N: DomNavigator>(
58 _context: &mut DynamicContext<'_, N>,
59 mut args: Vec<XPathValue<N>>,
60) -> Result<XPathValue<N>, XPathError> {
61 if args.len() != 1 {
62 return Err(XPathError::wrong_number_of_arguments("abs", 1, args.len()));
63 }
64
65 let arg = args.remove(0);
66 let value = match atomize_to_single_opt(arg)? {
67 None => return Ok(XPathValue::Empty),
68 Some(v) => v,
69 };
70
71 let result = numeric_abs(&value)?;
72 Ok(XPathValue::from_atomic(result))
73}
74
75fn numeric_abs(value: &XmlValue) -> Result<XmlValue, XPathError> {
76 match value.type_code {
77 XmlTypeCode::Double => {
78 let d = value.as_double().ok_or_else(|| XPathError::XPTY0004 {
79 expected: "xs:double".to_string(),
80 found: format!("{:?}", value.type_code),
81 })?;
82 Ok(XmlValue::double(d.abs()))
83 }
84 XmlTypeCode::Float => {
85 let f = get_float(value).ok_or_else(|| XPathError::XPTY0004 {
86 expected: "xs:float".to_string(),
87 found: format!("{:?}", value.type_code),
88 })?;
89 Ok(XmlValue::float(f.abs()))
90 }
91 XmlTypeCode::Decimal => {
92 let d = value.as_decimal().ok_or_else(|| XPathError::XPTY0004 {
93 expected: "xs:decimal".to_string(),
94 found: format!("{:?}", value.type_code),
95 })?;
96 Ok(XmlValue::decimal(d.abs()))
97 }
98 _ if is_integer_type(value.type_code) => {
99 let i = value.as_integer().ok_or_else(|| XPathError::XPTY0004 {
100 expected: "xs:integer".to_string(),
101 found: format!("{:?}", value.type_code),
102 })?;
103 let abs_val = if *i < BigInt::from(0) {
105 -i.clone()
106 } else {
107 i.clone()
108 };
109 Ok(XmlValue::integer(abs_val))
110 }
111 _ => Err(XPathError::XPTY0004 {
112 expected: "xs:numeric".to_string(),
113 found: format!("{:?}", value.type_code),
114 }),
115 }
116}
117
118pub fn ceiling<N: DomNavigator>(
126 _context: &mut DynamicContext<'_, N>,
127 mut args: Vec<XPathValue<N>>,
128) -> Result<XPathValue<N>, XPathError> {
129 if args.len() != 1 {
130 return Err(XPathError::wrong_number_of_arguments(
131 "ceiling",
132 1,
133 args.len(),
134 ));
135 }
136
137 let arg = args.remove(0);
138 let value = match atomize_to_single_opt(arg)? {
139 None => return Ok(XPathValue::Empty),
140 Some(v) => v,
141 };
142
143 let result = numeric_ceiling(&value)?;
144 Ok(XPathValue::from_atomic(result))
145}
146
147fn numeric_ceiling(value: &XmlValue) -> Result<XmlValue, XPathError> {
148 match value.type_code {
149 XmlTypeCode::Double => {
150 let d = value.as_double().ok_or_else(|| XPathError::XPTY0004 {
151 expected: "xs:double".to_string(),
152 found: format!("{:?}", value.type_code),
153 })?;
154 Ok(XmlValue::double(d.ceil()))
155 }
156 XmlTypeCode::Float => {
157 let f = get_float(value).ok_or_else(|| XPathError::XPTY0004 {
158 expected: "xs:float".to_string(),
159 found: format!("{:?}", value.type_code),
160 })?;
161 Ok(XmlValue::float(f.ceil()))
162 }
163 XmlTypeCode::Decimal => {
164 let d = value.as_decimal().ok_or_else(|| XPathError::XPTY0004 {
165 expected: "xs:decimal".to_string(),
166 found: format!("{:?}", value.type_code),
167 })?;
168 let truncated = d.trunc();
170 let result = if d > truncated {
171 truncated + Decimal::ONE
172 } else {
173 truncated
174 };
175 Ok(XmlValue::decimal(result))
176 }
177 _ if is_integer_type(value.type_code) => {
178 Ok(value.clone())
180 }
181 _ => Err(XPathError::XPTY0004 {
182 expected: "xs:numeric".to_string(),
183 found: format!("{:?}", value.type_code),
184 }),
185 }
186}
187
188pub fn floor<N: DomNavigator>(
196 _context: &mut DynamicContext<'_, N>,
197 mut args: Vec<XPathValue<N>>,
198) -> Result<XPathValue<N>, XPathError> {
199 if args.len() != 1 {
200 return Err(XPathError::wrong_number_of_arguments(
201 "floor",
202 1,
203 args.len(),
204 ));
205 }
206
207 let arg = args.remove(0);
208 let value = match atomize_to_single_opt(arg)? {
209 None => return Ok(XPathValue::Empty),
210 Some(v) => v,
211 };
212
213 let result = numeric_floor(&value)?;
214 Ok(XPathValue::from_atomic(result))
215}
216
217fn numeric_floor(value: &XmlValue) -> Result<XmlValue, XPathError> {
218 match value.type_code {
219 XmlTypeCode::Double => {
220 let d = value.as_double().ok_or_else(|| XPathError::XPTY0004 {
221 expected: "xs:double".to_string(),
222 found: format!("{:?}", value.type_code),
223 })?;
224 Ok(XmlValue::double(d.floor()))
225 }
226 XmlTypeCode::Float => {
227 let f = get_float(value).ok_or_else(|| XPathError::XPTY0004 {
228 expected: "xs:float".to_string(),
229 found: format!("{:?}", value.type_code),
230 })?;
231 Ok(XmlValue::float(f.floor()))
232 }
233 XmlTypeCode::Decimal => {
234 let d = value.as_decimal().ok_or_else(|| XPathError::XPTY0004 {
235 expected: "xs:decimal".to_string(),
236 found: format!("{:?}", value.type_code),
237 })?;
238 let truncated = d.trunc();
240 let result = if d < truncated {
241 truncated - Decimal::ONE
242 } else {
243 truncated
244 };
245 Ok(XmlValue::decimal(result))
246 }
247 _ if is_integer_type(value.type_code) => {
248 Ok(value.clone())
250 }
251 _ => Err(XPathError::XPTY0004 {
252 expected: "xs:numeric".to_string(),
253 found: format!("{:?}", value.type_code),
254 }),
255 }
256}
257
258pub fn round<N: DomNavigator>(
267 _context: &mut DynamicContext<'_, N>,
268 mut args: Vec<XPathValue<N>>,
269) -> Result<XPathValue<N>, XPathError> {
270 if args.len() != 1 {
271 return Err(XPathError::wrong_number_of_arguments(
272 "round",
273 1,
274 args.len(),
275 ));
276 }
277
278 let arg = args.remove(0);
279 let value = match atomize_to_single_opt(arg)? {
280 None => return Ok(XPathValue::Empty),
281 Some(v) => v,
282 };
283
284 let result = numeric_round(&value)?;
285 Ok(XPathValue::from_atomic(result))
286}
287
288fn numeric_round(value: &XmlValue) -> Result<XmlValue, XPathError> {
289 match value.type_code {
290 XmlTypeCode::Double => {
291 let d = value.as_double().ok_or_else(|| XPathError::XPTY0004 {
292 expected: "xs:double".to_string(),
293 found: format!("{:?}", value.type_code),
294 })?;
295 Ok(XmlValue::double(round_half_away_from_zero_f64(d)))
297 }
298 XmlTypeCode::Float => {
299 let f = get_float(value).ok_or_else(|| XPathError::XPTY0004 {
300 expected: "xs:float".to_string(),
301 found: format!("{:?}", value.type_code),
302 })?;
303 Ok(XmlValue::float(round_half_away_from_zero_f32(f)))
304 }
305 XmlTypeCode::Decimal => {
306 let d = value.as_decimal().ok_or_else(|| XPathError::XPTY0004 {
307 expected: "xs:decimal".to_string(),
308 found: format!("{:?}", value.type_code),
309 })?;
310 Ok(XmlValue::decimal(round_half_away_from_zero_decimal(d)))
311 }
312 _ if is_integer_type(value.type_code) => {
313 Ok(value.clone())
315 }
316 _ => Err(XPathError::XPTY0004 {
317 expected: "xs:numeric".to_string(),
318 found: format!("{:?}", value.type_code),
319 }),
320 }
321}
322
323fn round_half_away_from_zero_f64(d: f64) -> f64 {
325 if d.is_nan() || d.is_infinite() {
326 return d;
327 }
328 if d >= 0.0 {
331 (d + 0.5).floor()
332 } else {
333 (d - 0.5).ceil()
334 }
335}
336
337fn round_half_away_from_zero_f32(f: f32) -> f32 {
339 if f.is_nan() || f.is_infinite() {
340 return f;
341 }
342 if f >= 0.0 {
343 (f + 0.5).floor()
344 } else {
345 (f - 0.5).ceil()
346 }
347}
348
349fn round_half_away_from_zero_decimal(d: Decimal) -> Decimal {
351 let half = Decimal::new(5, 1); let truncated = d.trunc();
353 let frac = d - truncated;
354
355 if d >= Decimal::ZERO {
356 if frac >= half {
357 truncated + Decimal::ONE
358 } else {
359 truncated
360 }
361 } else if frac <= -half {
362 truncated - Decimal::ONE
363 } else {
364 truncated
365 }
366}
367
368pub fn round_half_to_even<N: DomNavigator>(
377 _context: &mut DynamicContext<'_, N>,
378 mut args: Vec<XPathValue<N>>,
379) -> Result<XPathValue<N>, XPathError> {
380 if args.is_empty() || args.len() > 2 {
381 return Err(XPathError::wrong_number_of_arguments(
382 "round-half-to-even",
383 1,
384 args.len(),
385 ));
386 }
387
388 let precision: i32 = if args.len() == 2 {
390 let prec_arg = args.remove(1);
391 match atomize_to_single_opt(prec_arg)? {
392 None => return Ok(XPathValue::Empty),
393 Some(v) => {
394 v.as_integer()
395 .and_then(|i| i.to_i32())
396 .ok_or_else(|| XPathError::XPTY0004 {
397 expected: "xs:integer".to_string(),
398 found: format!("{:?}", v.type_code),
399 })?
400 }
401 }
402 } else {
403 0
404 };
405
406 let arg = args.remove(0);
407 let value = match atomize_to_single_opt(arg)? {
408 None => return Ok(XPathValue::Empty),
409 Some(v) => v,
410 };
411
412 let result = numeric_round_half_to_even(&value, precision)?;
413 Ok(XPathValue::from_atomic(result))
414}
415
416fn numeric_round_half_to_even(value: &XmlValue, precision: i32) -> Result<XmlValue, XPathError> {
417 match value.type_code {
418 XmlTypeCode::Double => {
419 let d = value.as_double().ok_or_else(|| XPathError::XPTY0004 {
420 expected: "xs:double".to_string(),
421 found: format!("{:?}", value.type_code),
422 })?;
423 Ok(XmlValue::double(round_half_to_even_f64(d, precision)))
424 }
425 XmlTypeCode::Float => {
426 let f = get_float(value).ok_or_else(|| XPathError::XPTY0004 {
427 expected: "xs:float".to_string(),
428 found: format!("{:?}", value.type_code),
429 })?;
430 Ok(XmlValue::float(round_half_to_even_f32(f, precision)))
431 }
432 XmlTypeCode::Decimal => {
433 let d = value.as_decimal().ok_or_else(|| XPathError::XPTY0004 {
434 expected: "xs:decimal".to_string(),
435 found: format!("{:?}", value.type_code),
436 })?;
437 Ok(XmlValue::decimal(round_half_to_even_decimal(d, precision)?))
438 }
439 _ if is_integer_type(value.type_code) => {
440 if precision >= 0 {
442 return Ok(value.clone());
443 }
444
445 let i = value.as_integer().ok_or_else(|| XPathError::XPTY0004 {
447 expected: "xs:integer".to_string(),
448 found: format!("{:?}", value.type_code),
449 })?;
450
451 let result = round_half_to_even_integer(i, precision);
452 Ok(XmlValue::integer(result))
453 }
454 _ => Err(XPathError::XPTY0004 {
455 expected: "xs:numeric".to_string(),
456 found: format!("{:?}", value.type_code),
457 }),
458 }
459}
460
461fn round_half_to_even_f64(d: f64, precision: i32) -> f64 {
463 if d.is_nan() || d.is_infinite() {
464 return d;
465 }
466
467 if precision < 0 {
468 let scale = 10_f64.powi(-precision);
470 let scaled = d / scale;
471 round_ties_even_f64(scaled) * scale
473 } else {
474 let scale = 10_f64.powi(precision);
475 let scaled = d * scale;
476 round_ties_even_f64(scaled) / scale
477 }
478}
479
480fn round_ties_even_f64(d: f64) -> f64 {
482 let floored = d.floor();
483 let frac = d - floored;
484
485 if frac < 0.5 {
486 floored
487 } else if frac > 0.5 {
488 floored + 1.0
489 } else {
490 if floored as i64 % 2 == 0 {
492 floored
493 } else {
494 floored + 1.0
495 }
496 }
497}
498
499fn round_half_to_even_f32(f: f32, precision: i32) -> f32 {
501 if f.is_nan() || f.is_infinite() {
502 return f;
503 }
504
505 if precision < 0 {
506 let scale = 10_f32.powi(-precision);
507 let scaled = f / scale;
508 round_ties_even_f32(scaled) * scale
509 } else {
510 let scale = 10_f32.powi(precision);
511 let scaled = f * scale;
512 round_ties_even_f32(scaled) / scale
513 }
514}
515
516fn round_ties_even_f32(f: f32) -> f32 {
518 let floored = f.floor();
519 let frac = f - floored;
520
521 if frac < 0.5 {
522 floored
523 } else if frac > 0.5 {
524 floored + 1.0
525 } else if floored as i32 % 2 == 0 {
526 floored
527 } else {
528 floored + 1.0
529 }
530}
531
532fn round_half_to_even_decimal(d: Decimal, precision: i32) -> Result<Decimal, XPathError> {
534 if precision < 0 {
535 let abs_precision = (-precision) as u32;
537 let scale = Decimal::from_i64(10_i64.pow(abs_precision))
538 .ok_or_else(|| XPathError::internal("Failed to create decimal scale"))?;
539
540 let scaled = d / scale;
542 let rounded =
543 scaled.round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointNearestEven);
544 Ok(rounded * scale)
545 } else {
546 Ok(d.round_dp_with_strategy(
547 precision as u32,
548 rust_decimal::RoundingStrategy::MidpointNearestEven,
549 ))
550 }
551}
552
553fn round_half_to_even_integer(i: &BigInt, precision: i32) -> BigInt {
555 if precision >= 0 {
556 return i.clone();
557 }
558
559 let abs_precision = (-precision) as u32;
561 let scale = BigInt::from(10).pow(abs_precision);
562 let half_scale = &scale / 2;
563
564 let (quotient, remainder) = (i / &scale, i % &scale);
566 let abs_remainder = if remainder < BigInt::from(0) {
567 -&remainder
568 } else {
569 remainder.clone()
570 };
571
572 let rounded = if abs_remainder < half_scale {
573 quotient.clone()
574 } else if abs_remainder > half_scale {
575 if *i >= BigInt::from(0) {
576 "ient + 1
577 } else {
578 "ient - 1
579 }
580 } else {
581 if "ient % 2 == BigInt::from(0) {
583 quotient.clone()
584 } else if *i >= BigInt::from(0) {
585 "ient + 1
586 } else {
587 "ient - 1
588 }
589 };
590
591 rounded * scale
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::namespace::table::NameTable;
598 use crate::xpath::context::XPathContext;
599 use crate::xpath::RoXmlNavigator;
600
601 fn make_context<'a>() -> DynamicContext<'a, RoXmlNavigator<'a>> {
602 let table = Box::leak(Box::new(NameTable::new()));
603 let xpath_ctx = Box::leak(Box::new(XPathContext::new(table)));
604 DynamicContext::new(xpath_ctx, 0)
605 }
606
607 #[test]
608 fn test_abs_double() {
609 let mut ctx = make_context();
610 let args = vec![XPathValue::double(-3.5)];
611 let result = abs(&mut ctx, args).unwrap();
612 match result {
613 XPathValue::Item(item) => {
614 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
615 assert_eq!(v.as_double().unwrap(), 3.5);
616 } else {
617 panic!("Expected atomic value");
618 }
619 }
620 _ => panic!("Expected single item"),
621 }
622 }
623
624 #[test]
625 fn test_abs_empty() {
626 let mut ctx = make_context();
627 let args = vec![XPathValue::Empty];
628 let result = abs(&mut ctx, args).unwrap();
629 assert!(result.is_empty());
630 }
631
632 #[test]
633 fn test_ceiling_double() {
634 let mut ctx = make_context();
635 let args = vec![XPathValue::double(3.2)];
636 let result = ceiling(&mut ctx, args).unwrap();
637 match result {
638 XPathValue::Item(item) => {
639 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
640 assert_eq!(v.as_double().unwrap(), 4.0);
641 } else {
642 panic!("Expected atomic value");
643 }
644 }
645 _ => panic!("Expected single item"),
646 }
647 }
648
649 #[test]
650 fn test_floor_double() {
651 let mut ctx = make_context();
652 let args = vec![XPathValue::double(3.8)];
653 let result = floor(&mut ctx, args).unwrap();
654 match result {
655 XPathValue::Item(item) => {
656 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
657 assert_eq!(v.as_double().unwrap(), 3.0);
658 } else {
659 panic!("Expected atomic value");
660 }
661 }
662 _ => panic!("Expected single item"),
663 }
664 }
665
666 #[test]
667 fn test_round_double() {
668 let mut ctx = make_context();
669
670 let args = vec![XPathValue::double(2.5)];
672 let result = round(&mut ctx, args).unwrap();
673 match result {
674 XPathValue::Item(item) => {
675 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
676 assert_eq!(v.as_double().unwrap(), 3.0);
677 } else {
678 panic!("Expected atomic value");
679 }
680 }
681 _ => panic!("Expected single item"),
682 }
683
684 let args = vec![XPathValue::double(-2.5)];
686 let result = round(&mut ctx, args).unwrap();
687 match result {
688 XPathValue::Item(item) => {
689 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
690 assert_eq!(v.as_double().unwrap(), -3.0);
691 } else {
692 panic!("Expected atomic value");
693 }
694 }
695 _ => panic!("Expected single item"),
696 }
697 }
698
699 #[test]
700 fn test_round_half_to_even_double() {
701 let mut ctx = make_context();
702
703 let args = vec![XPathValue::double(2.5)];
705 let result = round_half_to_even(&mut ctx, args).unwrap();
706 match result {
707 XPathValue::Item(item) => {
708 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
709 assert_eq!(v.as_double().unwrap(), 2.0);
710 } else {
711 panic!("Expected atomic value");
712 }
713 }
714 _ => panic!("Expected single item"),
715 }
716
717 let args = vec![XPathValue::double(3.5)];
719 let result = round_half_to_even(&mut ctx, args).unwrap();
720 match result {
721 XPathValue::Item(item) => {
722 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
723 assert_eq!(v.as_double().unwrap(), 4.0);
724 } else {
725 panic!("Expected atomic value");
726 }
727 }
728 _ => panic!("Expected single item"),
729 }
730 }
731
732 #[test]
733 fn test_round_half_to_even_with_precision() {
734 let mut ctx = make_context();
735
736 let args = vec![XPathValue::double(3.567), XPathValue::integer(2)];
738 let result = round_half_to_even(&mut ctx, args).unwrap();
739 match result {
740 XPathValue::Item(item) => {
741 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
742 let d = v.as_double().unwrap();
743 assert!((d - 3.57).abs() < 0.001);
744 } else {
745 panic!("Expected atomic value");
746 }
747 }
748 _ => panic!("Expected single item"),
749 }
750 }
751
752 #[test]
753 fn test_round_half_to_even_negative_precision() {
754 let mut ctx = make_context();
755
756 let args = vec![XPathValue::double(35612.0), XPathValue::integer(-2)];
758 let result = round_half_to_even(&mut ctx, args).unwrap();
759 match result {
760 XPathValue::Item(item) => {
761 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
762 let d = v.as_double().unwrap();
763 assert_eq!(d, 35600.0);
764 } else {
765 panic!("Expected atomic value");
766 }
767 }
768 _ => panic!("Expected single item"),
769 }
770 }
771}