Skip to main content

xsd_schema/xpath/functions/
numeric.rs

1//! XPath 2.0 numeric functions.
2//!
3//! This module implements numeric functions from the XPath 2.0 specification:
4//! - fn:abs
5//! - fn:ceiling
6//! - fn:floor
7//! - fn:round
8//! - fn:round-half-to-even
9
10use 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
22/// Check if a type code is an integer-derived type.
23fn 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
42/// Extract float value from XmlValue.
43fn get_float(value: &XmlValue) -> Option<f32> {
44    match &value.value {
45        XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => Some(*f),
46        _ => None,
47    }
48}
49
50// ============================================================================
51// fn:abs($arg as numeric?) as numeric?
52// ============================================================================
53
54/// Implements fn:abs - returns the absolute value of the argument.
55///
56/// The function preserves the numeric type of the input.
57pub 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            // For BigInt, we need to handle negative numbers
104            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
118// ============================================================================
119// fn:ceiling($arg as numeric?) as numeric?
120// ============================================================================
121
122/// Implements fn:ceiling - returns the smallest integer greater than or equal to the argument.
123///
124/// The function preserves the numeric type of the input.
125pub 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            // Decimal doesn't have ceil(), use manual calculation
169            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            // For integers, ceiling is identity
179            Ok(value.clone())
180        }
181        _ => Err(XPathError::XPTY0004 {
182            expected: "xs:numeric".to_string(),
183            found: format!("{:?}", value.type_code),
184        }),
185    }
186}
187
188// ============================================================================
189// fn:floor($arg as numeric?) as numeric?
190// ============================================================================
191
192/// Implements fn:floor - returns the largest integer less than or equal to the argument.
193///
194/// The function preserves the numeric type of the input.
195pub 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            // Decimal doesn't have floor(), use manual calculation
239            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            // For integers, floor is identity
249            Ok(value.clone())
250        }
251        _ => Err(XPathError::XPTY0004 {
252            expected: "xs:numeric".to_string(),
253            found: format!("{:?}", value.type_code),
254        }),
255    }
256}
257
258// ============================================================================
259// fn:round($arg as numeric?) as numeric?
260// ============================================================================
261
262/// Implements fn:round - returns the nearest integer to the argument.
263///
264/// Rounds half values away from zero (e.g., 0.5 -> 1, -0.5 -> -1).
265/// The function preserves the numeric type of the input.
266pub 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            // XPath round() rounds half away from zero
296            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            // For integers, round is identity
314            Ok(value.clone())
315        }
316        _ => Err(XPathError::XPTY0004 {
317            expected: "xs:numeric".to_string(),
318            found: format!("{:?}", value.type_code),
319        }),
320    }
321}
322
323/// Round half away from zero for f64 (XPath round semantics).
324fn round_half_away_from_zero_f64(d: f64) -> f64 {
325    if d.is_nan() || d.is_infinite() {
326        return d;
327    }
328    // For positive numbers: floor(x + 0.5)
329    // For negative numbers: ceil(x - 0.5)
330    if d >= 0.0 {
331        (d + 0.5).floor()
332    } else {
333        (d - 0.5).ceil()
334    }
335}
336
337/// Round half away from zero for f32.
338fn 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
349/// Round half away from zero for Decimal.
350fn round_half_away_from_zero_decimal(d: Decimal) -> Decimal {
351    let half = Decimal::new(5, 1); // 0.5
352    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
368// ============================================================================
369// fn:round-half-to-even($arg as numeric?, $precision as integer?) as numeric?
370// ============================================================================
371
372/// Implements fn:round-half-to-even - banker's rounding.
373///
374/// Rounds to the specified precision using half-to-even rounding mode.
375/// If precision is omitted, rounds to the nearest integer.
376pub 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    // Get precision (default 0)
389    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            // For integers with non-negative precision, return as-is
441            if precision >= 0 {
442                return Ok(value.clone());
443            }
444
445            // For negative precision, round to powers of 10
446            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
461/// Round half to even for f64 with given precision.
462fn 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        // Round to powers of 10 (e.g., precision -1 rounds to nearest 10)
469        let scale = 10_f64.powi(-precision);
470        let scaled = d / scale;
471        // Use round_ties_even
472        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
480/// Round ties to even for f64 (banker's rounding).
481fn 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        // Exactly 0.5 - round to even
491        if floored as i64 % 2 == 0 {
492            floored
493        } else {
494            floored + 1.0
495        }
496    }
497}
498
499/// Round half to even for f32 with given precision.
500fn 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
516/// Round ties to even for f32.
517fn 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
532/// Round half to even for Decimal with given precision.
533fn round_half_to_even_decimal(d: Decimal, precision: i32) -> Result<Decimal, XPathError> {
534    if precision < 0 {
535        // For negative precision, we need to round to powers of 10
536        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        // Divide, round, multiply
541        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
553/// Round half to even for BigInt with negative precision.
554fn round_half_to_even_integer(i: &BigInt, precision: i32) -> BigInt {
555    if precision >= 0 {
556        return i.clone();
557    }
558
559    // For negative precision, round to powers of 10
560    let abs_precision = (-precision) as u32;
561    let scale = BigInt::from(10).pow(abs_precision);
562    let half_scale = &scale / 2;
563
564    // Compute: round(i / scale) * scale using half-to-even
565    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            &quotient + 1
577        } else {
578            &quotient - 1
579        }
580    } else {
581        // Exactly half - round to even
582        if &quotient % 2 == BigInt::from(0) {
583            quotient.clone()
584        } else if *i >= BigInt::from(0) {
585            &quotient + 1
586        } else {
587            &quotient - 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        // Test 2.5 -> 3 (round half away from zero)
671        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        // Test -2.5 -> -3 (round half away from zero)
685        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        // Test 2.5 -> 2 (half to even)
704        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        // Test 3.5 -> 4 (half to even)
718        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        // Test 3.567 with precision 2 -> 3.57
737        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        // Test 35612 with precision -2 -> 35600
757        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}