Skip to main content

xsd_schema/xpath/functions/
sequence.rs

1//! XPath 2.0 sequence functions.
2//!
3//! This module implements sequence functions from the XPath 2.0 specification:
4//! - fn:index-of
5//! - fn:remove
6//! - fn:insert-before
7//! - fn:subsequence
8//! - fn:unordered
9//! - fn:deep-equal
10
11use num_bigint::BigInt;
12use rust_decimal::prelude::ToPrimitive;
13
14use crate::types::value::{XmlAtomicValue, XmlValue};
15use crate::types::XmlTypeCode;
16use crate::xpath::context::DynamicContext;
17use crate::xpath::error::XPathError;
18use crate::xpath::iterator::{VecNodeIterator, XmlItem};
19use crate::xpath::tree_comparer::TreeComparer;
20use crate::xpath::DomNavigator;
21
22use super::{
23    atomize_sequence, atomize_to_double, atomize_to_single, atomize_to_single_opt,
24    atomize_to_string_opt, materialize, XPathValue,
25};
26
27/// Default collation URI (codepoint collation).
28const DEFAULT_COLLATION: &str = "http://www.w3.org/2005/xpath-functions/collation/codepoint";
29
30/// Validate collation URI - only default collation is supported.
31/// Returns Ok(()) if collation is valid (default or empty), FOCH0002 otherwise.
32fn validate_collation(collation: Option<&str>) -> Result<(), XPathError> {
33    match collation {
34        None => Ok(()),
35        Some(c) if c.is_empty() || c == DEFAULT_COLLATION => Ok(()),
36        Some(c) => Err(XPathError::unknown_collation(c)),
37    }
38}
39
40// ============================================================================
41// fn:index-of($seq as xs:anyAtomicType*, $search as xs:anyAtomicType,
42//             $collation as xs:string?) as xs:integer*
43// ============================================================================
44
45/// Implements fn:index-of - returns positions of matching items in a sequence.
46///
47/// Returns a sequence of positive integers giving the positions of items in $seq
48/// that are equal to $search.
49pub fn index_of<N: DomNavigator>(
50    _context: &mut DynamicContext<'_, N>,
51    mut args: Vec<XPathValue<N>>,
52) -> Result<XPathValue<N>, XPathError> {
53    if args.len() < 2 || args.len() > 3 {
54        return Err(XPathError::wrong_number_of_arguments(
55            "index-of",
56            2,
57            args.len(),
58        ));
59    }
60
61    // Get the sequence (arg 0) and search value (arg 1)
62    let seq = args.remove(0);
63    let search_arg = args.remove(0);
64    // Collation (arg 2) is ignored for now
65
66    // Atomize both
67    let seq_values = atomize_sequence(seq)?;
68    let search_value = match atomize_to_single_opt(search_arg)? {
69        None => return Ok(XPathValue::Empty),
70        Some(value) => value,
71    };
72
73    // Find matching positions (1-based)
74    let mut positions = Vec::new();
75    for (idx, item) in seq_values.iter().enumerate() {
76        if values_equal(item, &search_value) {
77            positions.push(XmlItem::Atomic(XmlValue::integer(BigInt::from(idx + 1))));
78        }
79    }
80
81    Ok(XPathValue::from_sequence(positions))
82}
83
84/// Compare two atomic values for equality (used by index-of and distinct-values).
85/// Normalizes UntypedAtomic and AnyUri to string for comparison.
86/// Applies numeric type promotion for comparing different numeric types.
87fn values_equal(left: &XmlValue, right: &XmlValue) -> bool {
88    let left_norm = normalize_for_comparison(left);
89    let right_norm = normalize_for_comparison(right);
90
91    // Numeric type promotion: compare numerics as doubles
92    if left_norm.type_code.is_numeric() && right_norm.type_code.is_numeric() {
93        return numeric_values_equal(&left_norm, &right_norm);
94    }
95
96    // Use value equality for non-numeric types
97    left_norm == right_norm
98}
99
100/// Compare two numeric values for equality using XPath 2.0 type promotion.
101/// Promotion: decimal→float→double. Uses the least common type.
102/// NaN is not equal to NaN for value comparison per XPath spec.
103fn numeric_values_equal(left: &XmlValue, right: &XmlValue) -> bool {
104    numeric_values_equal_inner(left, right, false)
105}
106
107/// Inner implementation for numeric equality comparison.
108/// If `nan_equal` is true, NaN == NaN (used by fn:distinct-values).
109fn numeric_values_equal_inner(left: &XmlValue, right: &XmlValue, nan_equal: bool) -> bool {
110    let lt = left.type_code;
111    let rt = right.type_code;
112    let is_decimal_or_int =
113        |tc: XmlTypeCode| tc.is_numeric() && tc != XmlTypeCode::Float && tc != XmlTypeCode::Double;
114
115    // If either operand is xs:double, promote both to double
116    if lt == XmlTypeCode::Double || rt == XmlTypeCode::Double {
117        if let (Some(l), Some(r)) = (left.as_double(), right.as_double()) {
118            if nan_equal && l.is_nan() && r.is_nan() {
119                return true;
120            }
121            if l.is_nan() || r.is_nan() {
122                return false;
123            }
124            return l == r;
125        }
126        return false;
127    }
128
129    // If either operand is xs:float, promote the other to float
130    if lt == XmlTypeCode::Float || rt == XmlTypeCode::Float {
131        let lf = match &left.value {
132            crate::types::value::XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => Some(*f),
133            _ => left.as_decimal().and_then(|d| d.to_f32()),
134        };
135        let rf = match &right.value {
136            crate::types::value::XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => Some(*f),
137            _ => right.as_decimal().and_then(|d| d.to_f32()),
138        };
139        if let (Some(l), Some(r)) = (lf, rf) {
140            if nan_equal && l.is_nan() && r.is_nan() {
141                return true;
142            }
143            if l.is_nan() || r.is_nan() {
144                return false;
145            }
146            return l == r;
147        }
148        return false;
149    }
150
151    // Both are decimal/integer — compare as Decimal (exact)
152    if is_decimal_or_int(lt) && is_decimal_or_int(rt) {
153        if let (Some(l), Some(r)) = (left.as_decimal(), right.as_decimal()) {
154            return l == r;
155        }
156    }
157
158    false
159}
160
161/// Compare two values for equality for fn:distinct-values.
162/// Like values_equal but treats NaN as equal to NaN per XPath 2.0 spec.
163fn distinct_values_equal(left: &XmlValue, right: &XmlValue) -> bool {
164    let left_norm = normalize_for_comparison(left);
165    let right_norm = normalize_for_comparison(right);
166
167    // Numeric type promotion with NaN == NaN for fn:distinct-values
168    if left_norm.type_code.is_numeric() && right_norm.type_code.is_numeric() {
169        return numeric_values_equal_inner(&left_norm, &right_norm, true);
170    }
171
172    // Duration cross-type comparison: P0M == PT0S (both are zero duration)
173    if is_duration_code(left_norm.type_code) && is_duration_code(right_norm.type_code) {
174        return durations_equal(&left_norm, &right_norm);
175    }
176
177    // Use value equality for non-numeric types
178    left_norm == right_norm
179}
180
181fn is_duration_code(code: XmlTypeCode) -> bool {
182    matches!(
183        code,
184        XmlTypeCode::Duration | XmlTypeCode::YearMonthDuration | XmlTypeCode::DayTimeDuration
185    )
186}
187
188/// Compare two duration values for equality.
189/// Handles cross-type comparison (YearMonthDuration vs DayTimeDuration).
190fn durations_equal(left: &XmlValue, right: &XmlValue) -> bool {
191    // Same type: use regular equality
192    if left.type_code == right.type_code {
193        return left == right;
194    }
195    // Cross-type: only zero durations are comparable and equal
196    is_zero_duration(left) && is_zero_duration(right)
197}
198
199/// Check if a duration value is zero.
200fn is_zero_duration(value: &XmlValue) -> bool {
201    match &value.value {
202        crate::types::value::XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(d)) => {
203            d.years == 0 && d.months == 0
204        }
205        crate::types::value::XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(d)) => {
206            d.days == 0 && d.hours == 0 && d.minutes == 0 && d.seconds.is_zero()
207        }
208        _ => false,
209    }
210}
211
212/// Normalize a value for comparison (UntypedAtomic and AnyUri become string).
213fn normalize_for_comparison(value: &XmlValue) -> XmlValue {
214    match value.type_code {
215        XmlTypeCode::UntypedAtomic | XmlTypeCode::AnyUri => {
216            XmlValue::string(value.to_string_value())
217        }
218        _ => value.clone(),
219    }
220}
221
222// ============================================================================
223// fn:reverse($arg as item()*) as item()*
224// ============================================================================
225
226/// Implements fn:reverse - reverses the order of items in a sequence.
227pub fn reverse<N: DomNavigator>(
228    _context: &mut DynamicContext<'_, N>,
229    mut args: Vec<XPathValue<N>>,
230) -> Result<XPathValue<N>, XPathError> {
231    if args.len() != 1 {
232        return Err(XPathError::wrong_number_of_arguments(
233            "reverse",
234            1,
235            args.len(),
236        ));
237    }
238
239    let mut items = materialize(args.remove(0));
240    items.reverse();
241    Ok(XPathValue::from_sequence(items))
242}
243
244// ============================================================================
245// fn:zero-or-one($arg as item()*) as item()?
246// ============================================================================
247
248/// Implements fn:zero-or-one - returns the argument if it contains zero or one items.
249///
250/// Raises FORG0003 if the argument contains more than one item.
251pub fn zero_or_one<N: DomNavigator>(
252    _context: &mut DynamicContext<'_, N>,
253    mut args: Vec<XPathValue<N>>,
254) -> Result<XPathValue<N>, XPathError> {
255    if args.len() != 1 {
256        return Err(XPathError::wrong_number_of_arguments(
257            "zero-or-one",
258            1,
259            args.len(),
260        ));
261    }
262
263    let arg = args.remove(0);
264    if arg.len() > 1 {
265        return Err(XPathError::FORG0003);
266    }
267    Ok(arg)
268}
269
270// ============================================================================
271// fn:one-or-more($arg as item()*) as item()+
272// ============================================================================
273
274/// Implements fn:one-or-more - returns the argument if it contains one or more items.
275///
276/// Raises FORG0004 if the argument is an empty sequence.
277pub fn one_or_more<N: DomNavigator>(
278    _context: &mut DynamicContext<'_, N>,
279    mut args: Vec<XPathValue<N>>,
280) -> Result<XPathValue<N>, XPathError> {
281    if args.len() != 1 {
282        return Err(XPathError::wrong_number_of_arguments(
283            "one-or-more",
284            1,
285            args.len(),
286        ));
287    }
288
289    let arg = args.remove(0);
290    if arg.is_empty() {
291        return Err(XPathError::FORG0004);
292    }
293    Ok(arg)
294}
295
296// ============================================================================
297// fn:exactly-one($arg as item()*) as item()
298// ============================================================================
299
300/// Implements fn:exactly-one - returns the argument if it contains exactly one item.
301///
302/// Raises FORG0005 if the argument does not contain exactly one item.
303pub fn exactly_one<N: DomNavigator>(
304    _context: &mut DynamicContext<'_, N>,
305    mut args: Vec<XPathValue<N>>,
306) -> Result<XPathValue<N>, XPathError> {
307    if args.len() != 1 {
308        return Err(XPathError::wrong_number_of_arguments(
309            "exactly-one",
310            1,
311            args.len(),
312        ));
313    }
314
315    let arg = args.remove(0);
316    if arg.len() != 1 {
317        return Err(XPathError::FORG0005);
318    }
319    Ok(arg)
320}
321
322// ============================================================================
323// fn:distinct-values($arg as xs:anyAtomicType*, $collation as xs:string?) as xs:anyAtomicType*
324// ============================================================================
325
326/// Implements fn:distinct-values - returns unique values from a sequence.
327///
328/// Returns the values that appear in the argument with duplicates removed.
329/// Uses value equality with numeric type promotion.
330pub fn distinct_values<N: DomNavigator>(
331    _context: &mut DynamicContext<'_, N>,
332    mut args: Vec<XPathValue<N>>,
333) -> Result<XPathValue<N>, XPathError> {
334    if args.is_empty() || args.len() > 2 {
335        return Err(XPathError::wrong_number_of_arguments(
336            "distinct-values",
337            1,
338            args.len(),
339        ));
340    }
341
342    let seq = args.remove(0);
343    // Collation (arg 1) is ignored for now
344
345    // Atomize the sequence
346    let values = atomize_sequence(seq)?;
347
348    if values.is_empty() {
349        return Ok(XPathValue::Empty);
350    }
351
352    // Remove duplicates using distinct_values_equal for comparison
353    // (treats NaN as equal to NaN, unlike value comparison)
354    let mut distinct: Vec<XmlValue> = Vec::new();
355    for value in values {
356        let is_duplicate = distinct
357            .iter()
358            .any(|existing| distinct_values_equal(existing, &value));
359        if !is_duplicate {
360            distinct.push(value);
361        }
362    }
363
364    // Convert back to XPathValue
365    let items: Vec<XmlItem<N>> = distinct.into_iter().map(XmlItem::Atomic).collect();
366
367    Ok(XPathValue::from_sequence(items))
368}
369
370// ============================================================================
371// fn:remove($target as item()*, $position as xs:integer) as item()*
372// ============================================================================
373
374/// Implements fn:remove - removes an item from a sequence at a given position.
375///
376/// Returns a new sequence with the item at the specified position removed.
377/// If $position is less than 1 or greater than the length of $target,
378/// the sequence is returned unchanged.
379pub fn remove<N: DomNavigator>(
380    _context: &mut DynamicContext<'_, N>,
381    mut args: Vec<XPathValue<N>>,
382) -> Result<XPathValue<N>, XPathError> {
383    if args.len() != 2 {
384        return Err(XPathError::wrong_number_of_arguments(
385            "remove",
386            2,
387            args.len(),
388        ));
389    }
390
391    let target = args.remove(0);
392    let position_arg = args.remove(0);
393
394    // Get position as integer
395    let position_value = atomize_to_single(position_arg)?;
396    let position = position_value
397        .as_integer()
398        .and_then(|i| i.to_i64())
399        .ok_or_else(|| XPathError::XPTY0004 {
400            expected: "xs:integer".to_string(),
401            found: format!("{:?}", position_value.type_code),
402        })?;
403
404    // Materialize target sequence
405    let mut items = materialize(target);
406
407    // If position is out of range, return sequence unchanged
408    if position < 1 || position as usize > items.len() {
409        return Ok(XPathValue::from_sequence(items));
410    }
411
412    // Remove item at position (convert 1-based to 0-based)
413    items.remove((position - 1) as usize);
414
415    Ok(XPathValue::from_sequence(items))
416}
417
418// ============================================================================
419// fn:insert-before($target as item()*, $position as xs:integer,
420//                  $inserts as item()*) as item()*
421// ============================================================================
422
423/// Implements fn:insert-before - inserts items into a sequence.
424///
425/// Returns a new sequence with $inserts inserted before the item at $position.
426/// If $position < 1, inserts at the beginning.
427/// If $position > length + 1, inserts at the end.
428pub fn insert_before<N: DomNavigator>(
429    _context: &mut DynamicContext<'_, N>,
430    mut args: Vec<XPathValue<N>>,
431) -> Result<XPathValue<N>, XPathError> {
432    if args.len() != 3 {
433        return Err(XPathError::wrong_number_of_arguments(
434            "insert-before",
435            3,
436            args.len(),
437        ));
438    }
439
440    let target = args.remove(0);
441    let position_arg = args.remove(0);
442    let inserts = args.remove(0);
443
444    // Get position as integer
445    let position_value = atomize_to_single(position_arg)?;
446    let position = position_value
447        .as_integer()
448        .and_then(|i| i.to_i64())
449        .ok_or_else(|| XPathError::XPTY0004 {
450            expected: "xs:integer".to_string(),
451            found: format!("{:?}", position_value.type_code),
452        })?;
453
454    // Materialize both sequences
455    let mut target_items = materialize(target);
456    let insert_items = materialize(inserts);
457
458    // Adjust position: if < 1, use 1; if > len+1, use len+1
459    let len = target_items.len();
460    let adjusted_pos = if position < 1 {
461        0
462    } else if position as usize > len {
463        len
464    } else {
465        (position - 1) as usize
466    };
467
468    // Build result: items[0..pos] + inserts + items[pos..]
469    let mut result = Vec::with_capacity(target_items.len() + insert_items.len());
470    result.extend(target_items.drain(..adjusted_pos));
471    result.extend(insert_items);
472    result.extend(target_items);
473
474    Ok(XPathValue::from_sequence(result))
475}
476
477// ============================================================================
478// fn:subsequence($sourceSeq as item()*, $startingLoc as xs:double,
479//                $length as xs:double?) as item()*
480// ============================================================================
481
482/// Implements fn:subsequence - returns a contiguous subsequence.
483///
484/// Returns items from $sourceSeq starting at position $startingLoc
485/// and continuing for $length items (or to the end if $length is omitted).
486///
487/// Uses XPath 2.0 rounding rules:
488/// - Positions are doubles, rounded to integers
489/// - NaN startingLoc or length -> empty
490/// - +Infinity startingLoc -> empty
491/// - -Infinity length -> empty
492pub fn subsequence<N: DomNavigator>(
493    _context: &mut DynamicContext<'_, N>,
494    mut args: Vec<XPathValue<N>>,
495) -> Result<XPathValue<N>, XPathError> {
496    if args.is_empty() || args.len() > 3 {
497        return Err(XPathError::wrong_number_of_arguments(
498            "subsequence",
499            2,
500            args.len(),
501        ));
502    }
503
504    let source = args.remove(0);
505    let starting_loc_arg = args.remove(0);
506    let length_arg = if !args.is_empty() {
507        Some(args.remove(0))
508    } else {
509        None
510    };
511
512    // Get starting location as double
513    let starting_loc = atomize_to_double(starting_loc_arg)?;
514
515    // Get optional length as double
516    let length = match length_arg {
517        Some(arg) => Some(atomize_to_double(arg)?),
518        None => None,
519    };
520
521    // Handle NaN cases
522    if starting_loc.is_nan() {
523        return Ok(XPathValue::Empty);
524    }
525    if let Some(len) = length {
526        if len.is_nan() {
527            return Ok(XPathValue::Empty);
528        }
529    }
530
531    // Handle infinity cases
532    if starting_loc.is_infinite() && starting_loc.is_sign_positive() {
533        return Ok(XPathValue::Empty);
534    }
535    if let Some(len) = length {
536        if len.is_infinite() && len.is_sign_negative() {
537            return Ok(XPathValue::Empty);
538        }
539    }
540
541    // Materialize source sequence
542    let items = materialize(source);
543
544    // Round starting location (XPath uses round-half-to-even, but round() is close enough)
545    let start_rounded = round_half_away_from_zero(starting_loc);
546
547    // Calculate effective start and end positions
548    let (start_idx, end_idx) = match length {
549        Some(len) => {
550            let len_rounded = round_half_away_from_zero(len);
551            // Per spec: items where round(startingLoc) <= position < round(startingLoc) + round(length)
552            // Note: position is 1-based, so item at position p has index p-1
553
554            // Handle negative start adjusting length
555            let effective_start = if start_rounded < 1.0 {
556                // If start is negative, we skip fewer items but the length is reduced
557                1.0
558            } else {
559                start_rounded
560            };
561
562            // Calculate length adjustment for negative start
563            let adjusted_len = if start_rounded < 1.0 {
564                len_rounded + start_rounded - 1.0
565            } else {
566                len_rounded
567            };
568
569            if adjusted_len <= 0.0 {
570                return Ok(XPathValue::Empty);
571            }
572
573            let start = (effective_start - 1.0).max(0.0) as usize;
574            let end = (effective_start - 1.0 + adjusted_len).min(items.len() as f64) as usize;
575            (start, end)
576        }
577        None => {
578            // No length specified - go to end
579            if start_rounded < 1.0 {
580                (0, items.len())
581            } else {
582                let start = (start_rounded - 1.0).max(0.0) as usize;
583                (start, items.len())
584            }
585        }
586    };
587
588    // Handle out of range
589    if start_idx >= items.len() {
590        return Ok(XPathValue::Empty);
591    }
592
593    // Extract subsequence
594    let result: Vec<XmlItem<N>> = items
595        .into_iter()
596        .skip(start_idx)
597        .take(end_idx.saturating_sub(start_idx))
598        .collect();
599
600    Ok(XPathValue::from_sequence(result))
601}
602
603/// Round half away from zero (XPath round semantics).
604fn round_half_away_from_zero(d: f64) -> f64 {
605    if d.is_nan() || d.is_infinite() {
606        return d;
607    }
608    if d >= 0.0 {
609        (d + 0.5).floor()
610    } else {
611        (d - 0.5).ceil()
612    }
613}
614
615// ============================================================================
616// fn:unordered($sourceSeq as item()*) as item()*
617// ============================================================================
618
619/// Implements fn:unordered - returns the sequence in implementation-defined order.
620///
621/// This is an optimization hint; implementations may return items in any order.
622/// Our implementation simply returns the input unchanged.
623pub fn unordered<N: DomNavigator>(
624    _context: &mut DynamicContext<'_, N>,
625    mut args: Vec<XPathValue<N>>,
626) -> Result<XPathValue<N>, XPathError> {
627    if args.len() != 1 {
628        return Err(XPathError::wrong_number_of_arguments(
629            "unordered",
630            1,
631            args.len(),
632        ));
633    }
634
635    // Simply return the input unchanged
636    Ok(args.remove(0))
637}
638
639// ============================================================================
640// fn:deep-equal($parameter1 as item()*, $parameter2 as item()*,
641//               $collation as xs:string?) as xs:boolean
642// ============================================================================
643
644/// Implements fn:deep-equal - tests whether two sequences are deep-equal.
645///
646/// Two sequences are deep-equal if they have the same length and each pair
647/// of corresponding items are deep-equal.
648pub fn deep_equal<N: DomNavigator>(
649    _context: &mut DynamicContext<'_, N>,
650    mut args: Vec<XPathValue<N>>,
651) -> Result<XPathValue<N>, XPathError> {
652    if args.len() < 2 || args.len() > 3 {
653        return Err(XPathError::wrong_number_of_arguments(
654            "deep-equal",
655            2,
656            args.len(),
657        ));
658    }
659
660    // Validate collation if provided (third argument)
661    if args.len() == 3 {
662        let collation_arg = args.pop().unwrap();
663        let collation = atomize_to_string_opt(collation_arg)?;
664        validate_collation(collation.as_deref())?;
665    }
666
667    let param1 = args.remove(0);
668    let param2 = args.remove(0);
669
670    // Materialize both sequences
671    let items1 = materialize(param1);
672    let items2 = materialize(param2);
673
674    // Create VecNodeIterators for comparison
675    let iter1: VecNodeIterator<N> = VecNodeIterator::new(items1);
676    let iter2: VecNodeIterator<N> = VecNodeIterator::new(items2);
677
678    // Use TreeComparer for deep equality
679    let comparer = TreeComparer::default();
680    let result = comparer.deep_equal_iter(&iter1, &iter2)?;
681
682    Ok(XPathValue::boolean(result))
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    use crate::namespace::table::NameTable;
689    use crate::xpath::context::XPathContext;
690    use crate::xpath::iterator::XmlItem;
691    use crate::xpath::RoXmlNavigator;
692
693    fn make_context<'a>() -> DynamicContext<'a, RoXmlNavigator<'a>> {
694        let table = Box::leak(Box::new(NameTable::new()));
695        let xpath_ctx = Box::leak(Box::new(XPathContext::new(table)));
696        DynamicContext::new(xpath_ctx, 0)
697    }
698
699    fn integer_seq<N: DomNavigator>(values: &[i64]) -> XPathValue<N> {
700        let items: Vec<XmlItem<N>> = values
701            .iter()
702            .map(|&v| XmlItem::Atomic(XmlValue::integer(BigInt::from(v))))
703            .collect();
704        XPathValue::from_sequence(items)
705    }
706
707    fn extract_integers<N: DomNavigator>(value: XPathValue<N>) -> Vec<i64> {
708        match value {
709            XPathValue::Empty => vec![],
710            XPathValue::Item(item) => {
711                if let XmlItem::Atomic(v) = item {
712                    vec![v.as_integer().and_then(|i| i.to_i64()).unwrap()]
713                } else {
714                    vec![]
715                }
716            }
717            XPathValue::Sequence(items) => items
718                .into_iter()
719                .filter_map(|item| {
720                    if let XmlItem::Atomic(v) = item {
721                        v.as_integer().and_then(|i| i.to_i64())
722                    } else {
723                        None
724                    }
725                })
726                .collect(),
727        }
728    }
729
730    fn extract_bool<N: DomNavigator>(value: XPathValue<N>) -> bool {
731        match value {
732            XPathValue::Item(XmlItem::Atomic(v)) => v.as_boolean().unwrap_or(false),
733            _ => false,
734        }
735    }
736
737    // ========== index-of tests ==========
738
739    #[test]
740    fn test_index_of_multiple_matches() {
741        let mut ctx = make_context();
742        let seq = integer_seq::<RoXmlNavigator>(&[10, 20, 30, 20]);
743        let search = XPathValue::integer(20);
744        let args = vec![seq, search];
745        let result = index_of(&mut ctx, args).unwrap();
746        assert_eq!(extract_integers(result), vec![2, 4]);
747    }
748
749    #[test]
750    fn test_index_of_no_match() {
751        let mut ctx = make_context();
752        let seq = integer_seq::<RoXmlNavigator>(&[10, 20, 30]);
753        let search = XPathValue::integer(40);
754        let args = vec![seq, search];
755        let result = index_of(&mut ctx, args).unwrap();
756        assert_eq!(extract_integers(result), Vec::<i64>::new());
757    }
758
759    #[test]
760    fn test_index_of_string_matches() {
761        let mut ctx = make_context();
762        let items: Vec<XmlItem<RoXmlNavigator>> = vec!["a", "b", "c", "b"]
763            .into_iter()
764            .map(|s| XmlItem::Atomic(XmlValue::string(s)))
765            .collect();
766        let seq = XPathValue::from_sequence(items);
767        let search = XPathValue::string("b");
768        let args = vec![seq, search];
769        let result = index_of(&mut ctx, args).unwrap();
770        assert_eq!(extract_integers(result), vec![2, 4]);
771    }
772
773    #[test]
774    fn test_index_of_empty_sequence() {
775        let mut ctx = make_context();
776        let seq = XPathValue::<RoXmlNavigator>::Empty;
777        let search = XPathValue::integer(1);
778        let args = vec![seq, search];
779        let result = index_of(&mut ctx, args).unwrap();
780        assert!(result.is_empty());
781    }
782
783    // ========== remove tests ==========
784
785    #[test]
786    fn test_remove_middle() {
787        let mut ctx = make_context();
788        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
789        let pos = XPathValue::integer(2);
790        let args = vec![seq, pos];
791        let result = remove(&mut ctx, args).unwrap();
792        assert_eq!(extract_integers(result), vec![1, 3]);
793    }
794
795    #[test]
796    fn test_remove_out_of_range_low() {
797        let mut ctx = make_context();
798        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
799        let pos = XPathValue::integer(0);
800        let args = vec![seq, pos];
801        let result = remove(&mut ctx, args).unwrap();
802        assert_eq!(extract_integers(result), vec![1, 2, 3]);
803    }
804
805    #[test]
806    fn test_remove_out_of_range_high() {
807        let mut ctx = make_context();
808        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
809        let pos = XPathValue::integer(10);
810        let args = vec![seq, pos];
811        let result = remove(&mut ctx, args).unwrap();
812        assert_eq!(extract_integers(result), vec![1, 2, 3]);
813    }
814
815    // ========== insert-before tests ==========
816
817    #[test]
818    fn test_insert_before_middle() {
819        let mut ctx = make_context();
820        let target = integer_seq::<RoXmlNavigator>(&[1, 3]);
821        let pos = XPathValue::integer(2);
822        let inserts = XPathValue::integer(2);
823        let args = vec![target, pos, inserts];
824        let result = insert_before(&mut ctx, args).unwrap();
825        assert_eq!(extract_integers(result), vec![1, 2, 3]);
826    }
827
828    #[test]
829    fn test_insert_before_position_less_than_one() {
830        let mut ctx = make_context();
831        let target = integer_seq::<RoXmlNavigator>(&[2, 3]);
832        let pos = XPathValue::integer(0);
833        let inserts = XPathValue::integer(1);
834        let args = vec![target, pos, inserts];
835        let result = insert_before(&mut ctx, args).unwrap();
836        assert_eq!(extract_integers(result), vec![1, 2, 3]);
837    }
838
839    #[test]
840    fn test_insert_before_position_beyond_end() {
841        let mut ctx = make_context();
842        let target = integer_seq::<RoXmlNavigator>(&[1, 2]);
843        let pos = XPathValue::integer(10);
844        let inserts = XPathValue::integer(3);
845        let args = vec![target, pos, inserts];
846        let result = insert_before(&mut ctx, args).unwrap();
847        assert_eq!(extract_integers(result), vec![1, 2, 3]);
848    }
849
850    // ========== subsequence tests ==========
851
852    #[test]
853    fn test_subsequence_with_length() {
854        let mut ctx = make_context();
855        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3, 4, 5]);
856        let start = XPathValue::double(2.0);
857        let len = XPathValue::double(3.0);
858        let args = vec![seq, start, len];
859        let result = subsequence(&mut ctx, args).unwrap();
860        assert_eq!(extract_integers(result), vec![2, 3, 4]);
861    }
862
863    #[test]
864    fn test_subsequence_without_length() {
865        let mut ctx = make_context();
866        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3, 4, 5]);
867        let start = XPathValue::double(3.0);
868        let args = vec![seq, start];
869        let result = subsequence(&mut ctx, args).unwrap();
870        assert_eq!(extract_integers(result), vec![3, 4, 5]);
871    }
872
873    #[test]
874    fn test_subsequence_negative_start() {
875        let mut ctx = make_context();
876        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
877        let start = XPathValue::double(-1.0);
878        let len = XPathValue::double(4.0);
879        let args = vec![seq, start, len];
880        let result = subsequence(&mut ctx, args).unwrap();
881        // start=-1, len=4: positions where -1 <= pos < 3, i.e., pos 1 and 2
882        assert_eq!(extract_integers(result), vec![1, 2]);
883    }
884
885    #[test]
886    fn test_subsequence_nan_start() {
887        let mut ctx = make_context();
888        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
889        let start = XPathValue::double(f64::NAN);
890        let len = XPathValue::double(2.0);
891        let args = vec![seq, start, len];
892        let result = subsequence(&mut ctx, args).unwrap();
893        assert!(result.is_empty());
894    }
895
896    #[test]
897    fn test_subsequence_rounding() {
898        let mut ctx = make_context();
899        // subsequence((1,2,3,4,5), 1.5, 2.6) should round to start=2, len=3 -> items 2,3,4
900        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3, 4, 5]);
901        let start = XPathValue::double(1.5);
902        let len = XPathValue::double(2.6);
903        let args = vec![seq, start, len];
904        let result = subsequence(&mut ctx, args).unwrap();
905        assert_eq!(extract_integers(result), vec![2, 3, 4]);
906    }
907
908    // ========== unordered tests ==========
909
910    #[test]
911    fn test_unordered_passthrough() {
912        let mut ctx = make_context();
913        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
914        let args = vec![seq];
915        let result = unordered(&mut ctx, args).unwrap();
916        assert_eq!(extract_integers(result), vec![1, 2, 3]);
917    }
918
919    // ========== index-of with numeric type promotion tests ==========
920
921    #[test]
922    fn test_index_of_integer_matches_double() {
923        let mut ctx = make_context();
924        // Sequence of integers
925        let seq = integer_seq::<RoXmlNavigator>(&[10, 20, 30]);
926        // Search for 20.0 (double)
927        let search = XPathValue::double(20.0);
928        let args = vec![seq, search];
929        let result = index_of(&mut ctx, args).unwrap();
930        // Should find 20 at position 2
931        assert_eq!(extract_integers(result), vec![2]);
932    }
933
934    #[test]
935    fn test_index_of_double_matches_integer() {
936        let mut ctx = make_context();
937        // Sequence with a double
938        let items: Vec<XmlItem<RoXmlNavigator>> = vec![
939            XmlItem::Atomic(XmlValue::double(10.0)),
940            XmlItem::Atomic(XmlValue::double(20.0)),
941            XmlItem::Atomic(XmlValue::double(30.0)),
942        ];
943        let seq = XPathValue::from_sequence(items);
944        // Search for 20 (integer)
945        let search = XPathValue::integer(20);
946        let args = vec![seq, search];
947        let result = index_of(&mut ctx, args).unwrap();
948        // Should find 20.0 at position 2
949        assert_eq!(extract_integers(result), vec![2]);
950    }
951
952    #[test]
953    fn test_index_of_nan_not_equal() {
954        let mut ctx = make_context();
955        // Sequence with NaN
956        let items: Vec<XmlItem<RoXmlNavigator>> = vec![
957            XmlItem::Atomic(XmlValue::double(f64::NAN)),
958            XmlItem::Atomic(XmlValue::double(1.0)),
959        ];
960        let seq = XPathValue::from_sequence(items);
961        // Search for NaN
962        let search = XPathValue::double(f64::NAN);
963        let args = vec![seq, search];
964        let result = index_of(&mut ctx, args).unwrap();
965        // NaN should not match NaN for value comparison
966        assert!(result.is_empty());
967    }
968
969    // ========== reverse tests ==========
970
971    #[test]
972    fn test_reverse_sequence() {
973        let mut ctx = make_context();
974        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3, 4, 5]);
975        let args = vec![seq];
976        let result = reverse(&mut ctx, args).unwrap();
977        assert_eq!(extract_integers(result), vec![5, 4, 3, 2, 1]);
978    }
979
980    #[test]
981    fn test_reverse_empty() {
982        let mut ctx = make_context();
983        let seq = XPathValue::<RoXmlNavigator>::Empty;
984        let args = vec![seq];
985        let result = reverse(&mut ctx, args).unwrap();
986        assert!(result.is_empty());
987    }
988
989    #[test]
990    fn test_reverse_single() {
991        let mut ctx = make_context();
992        let seq = XPathValue::integer(42);
993        let args = vec![seq];
994        let result = reverse(&mut ctx, args).unwrap();
995        assert_eq!(extract_integers(result), vec![42]);
996    }
997
998    // ========== zero-or-one tests ==========
999
1000    #[test]
1001    fn test_zero_or_one_empty() {
1002        let mut ctx = make_context();
1003        let seq = XPathValue::<RoXmlNavigator>::Empty;
1004        let args = vec![seq];
1005        let result = zero_or_one(&mut ctx, args).unwrap();
1006        assert!(result.is_empty());
1007    }
1008
1009    #[test]
1010    fn test_zero_or_one_single() {
1011        let mut ctx = make_context();
1012        let seq = XPathValue::integer(42);
1013        let args = vec![seq];
1014        let result = zero_or_one(&mut ctx, args).unwrap();
1015        assert_eq!(extract_integers(result), vec![42]);
1016    }
1017
1018    #[test]
1019    fn test_zero_or_one_multiple_fails() {
1020        let mut ctx = make_context();
1021        let seq = integer_seq::<RoXmlNavigator>(&[1, 2]);
1022        let args = vec![seq];
1023        let result = zero_or_one(&mut ctx, args);
1024        match result {
1025            Err(e) => assert_eq!(e.error_code(), Some("FORG0003")),
1026            Ok(_) => panic!("Expected FORG0003 error"),
1027        }
1028    }
1029
1030    // ========== one-or-more tests ==========
1031
1032    #[test]
1033    fn test_one_or_more_single() {
1034        let mut ctx = make_context();
1035        let seq = XPathValue::integer(42);
1036        let args = vec![seq];
1037        let result = one_or_more(&mut ctx, args).unwrap();
1038        assert_eq!(extract_integers(result), vec![42]);
1039    }
1040
1041    #[test]
1042    fn test_one_or_more_multiple() {
1043        let mut ctx = make_context();
1044        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
1045        let args = vec![seq];
1046        let result = one_or_more(&mut ctx, args).unwrap();
1047        assert_eq!(extract_integers(result), vec![1, 2, 3]);
1048    }
1049
1050    #[test]
1051    fn test_one_or_more_empty_fails() {
1052        let mut ctx = make_context();
1053        let seq = XPathValue::<RoXmlNavigator>::Empty;
1054        let args = vec![seq];
1055        let result = one_or_more(&mut ctx, args);
1056        match result {
1057            Err(e) => assert_eq!(e.error_code(), Some("FORG0004")),
1058            Ok(_) => panic!("Expected FORG0004 error"),
1059        }
1060    }
1061
1062    // ========== exactly-one tests ==========
1063
1064    #[test]
1065    fn test_exactly_one_single() {
1066        let mut ctx = make_context();
1067        let seq = XPathValue::integer(42);
1068        let args = vec![seq];
1069        let result = exactly_one(&mut ctx, args).unwrap();
1070        assert_eq!(extract_integers(result), vec![42]);
1071    }
1072
1073    #[test]
1074    fn test_exactly_one_empty_fails() {
1075        let mut ctx = make_context();
1076        let seq = XPathValue::<RoXmlNavigator>::Empty;
1077        let args = vec![seq];
1078        let result = exactly_one(&mut ctx, args);
1079        match result {
1080            Err(e) => assert_eq!(e.error_code(), Some("FORG0005")),
1081            Ok(_) => panic!("Expected FORG0005 error"),
1082        }
1083    }
1084
1085    #[test]
1086    fn test_exactly_one_multiple_fails() {
1087        let mut ctx = make_context();
1088        let seq = integer_seq::<RoXmlNavigator>(&[1, 2]);
1089        let args = vec![seq];
1090        let result = exactly_one(&mut ctx, args);
1091        match result {
1092            Err(e) => assert_eq!(e.error_code(), Some("FORG0005")),
1093            Ok(_) => panic!("Expected FORG0005 error"),
1094        }
1095    }
1096
1097    // ========== distinct-values tests ==========
1098
1099    #[test]
1100    fn test_distinct_values_integers() {
1101        let mut ctx = make_context();
1102        let seq = integer_seq::<RoXmlNavigator>(&[1, 2, 1, 3, 2, 1]);
1103        let args = vec![seq];
1104        let result = distinct_values(&mut ctx, args).unwrap();
1105        assert_eq!(extract_integers(result), vec![1, 2, 3]);
1106    }
1107
1108    #[test]
1109    fn test_distinct_values_empty() {
1110        let mut ctx = make_context();
1111        let seq = XPathValue::<RoXmlNavigator>::Empty;
1112        let args = vec![seq];
1113        let result = distinct_values(&mut ctx, args).unwrap();
1114        assert!(result.is_empty());
1115    }
1116
1117    #[test]
1118    fn test_distinct_values_mixed_numeric() {
1119        let mut ctx = make_context();
1120        // Mix of integers and doubles that are equal
1121        let items: Vec<XmlItem<RoXmlNavigator>> = vec![
1122            XmlItem::Atomic(XmlValue::integer(BigInt::from(1))),
1123            XmlItem::Atomic(XmlValue::double(2.0)),
1124            XmlItem::Atomic(XmlValue::integer(BigInt::from(1))), // duplicate of 1
1125            XmlItem::Atomic(XmlValue::double(2.0)),              // duplicate of 2.0
1126            XmlItem::Atomic(XmlValue::integer(BigInt::from(3))),
1127        ];
1128        let seq = XPathValue::from_sequence(items);
1129        let args = vec![seq];
1130        let result = distinct_values(&mut ctx, args).unwrap();
1131        // Should have 3 distinct values: 1, 2.0, 3
1132        assert_eq!(result.len(), 3);
1133    }
1134
1135    #[test]
1136    fn test_distinct_values_strings() {
1137        let mut ctx = make_context();
1138        let items: Vec<XmlItem<RoXmlNavigator>> = vec!["a", "b", "a", "c", "b"]
1139            .into_iter()
1140            .map(|s| XmlItem::Atomic(XmlValue::string(s)))
1141            .collect();
1142        let seq = XPathValue::from_sequence(items);
1143        let args = vec![seq];
1144        let result = distinct_values(&mut ctx, args).unwrap();
1145        // Should have 3 distinct values: a, b, c
1146        assert_eq!(result.len(), 3);
1147    }
1148
1149    // ========== deep-equal tests ==========
1150
1151    #[test]
1152    fn test_deep_equal_same_integers() {
1153        let mut ctx = make_context();
1154        let seq1 = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
1155        let seq2 = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
1156        let args = vec![seq1, seq2];
1157        let result = deep_equal(&mut ctx, args).unwrap();
1158        assert!(extract_bool(result));
1159    }
1160
1161    #[test]
1162    fn test_deep_equal_different_integers() {
1163        let mut ctx = make_context();
1164        let seq1 = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
1165        let seq2 = integer_seq::<RoXmlNavigator>(&[1, 2, 4]);
1166        let args = vec![seq1, seq2];
1167        let result = deep_equal(&mut ctx, args).unwrap();
1168        assert!(!extract_bool(result));
1169    }
1170
1171    #[test]
1172    fn test_deep_equal_nan() {
1173        let mut ctx = make_context();
1174        let seq1: XPathValue<RoXmlNavigator> = XPathValue::double(f64::NAN);
1175        let seq2: XPathValue<RoXmlNavigator> = XPathValue::double(f64::NAN);
1176        let args = vec![seq1, seq2];
1177        let result = deep_equal(&mut ctx, args).unwrap();
1178        // XPath deep-equal treats NaN as equal to NaN
1179        assert!(extract_bool(result));
1180    }
1181
1182    #[test]
1183    fn test_deep_equal_empty_sequences() {
1184        let mut ctx = make_context();
1185        let seq1 = XPathValue::<RoXmlNavigator>::Empty;
1186        let seq2 = XPathValue::<RoXmlNavigator>::Empty;
1187        let args = vec![seq1, seq2];
1188        let result = deep_equal(&mut ctx, args).unwrap();
1189        assert!(extract_bool(result));
1190    }
1191
1192    #[test]
1193    fn test_deep_equal_different_lengths() {
1194        let mut ctx = make_context();
1195        let seq1 = integer_seq::<RoXmlNavigator>(&[1, 2]);
1196        let seq2 = integer_seq::<RoXmlNavigator>(&[1, 2, 3]);
1197        let args = vec![seq1, seq2];
1198        let result = deep_equal(&mut ctx, args).unwrap();
1199        assert!(!extract_bool(result));
1200    }
1201}