Skip to main content

xsd_schema/xpath/
cast.rs

1//! Type casting operations for XPath evaluation.
2//!
3//! This module implements XPath 2.0 type casting rules for converting
4//! values between different types.
5//!
6//! ## Casting Rules
7//!
8//! - `cast_to`: Explicit cast expression (`value cast as type`)
9//! - `treat_as`: Type assertion without conversion (`value treat as type`)
10//! - `instance_of`: Type test (`value instance of type`)
11//! - `castable`: Castability test (`value castable as type`)
12
13use num_bigint::BigInt;
14use rust_decimal::Decimal;
15
16use super::error::XPathError;
17use crate::namespace::qname::QualifiedName;
18use crate::namespace::table::{well_known, NameTable};
19use crate::types::value::{
20    DateTimeValue, DateValue, DayTimeDurationValue, DurationValue, GDayValue, GMonthDayValue,
21    GMonthValue, GYearMonthValue, GYearValue, TimeValue, XmlAtomicValue, XmlValue, XmlValueKind,
22    YearMonthDurationValue,
23};
24use crate::types::{XmlTypeCode, VALIDATOR_REGISTRY};
25use crate::xpath::ast::OccurrenceIndicator;
26
27/// Check if a cast from `source` to `target` is allowed by the XPath 2.0 casting table.
28///
29/// Implements the casting rules from XPath Functions and Operators Section 17.1.
30/// Returns `true` if the cast is permitted, `false` if it should raise XPTY0004.
31fn can_cast(source: XmlTypeCode, target: XmlTypeCode) -> bool {
32    // Same type is always allowed
33    if source == target {
34        return true;
35    }
36
37    // Casting to NOTATION is never allowed
38    if target == XmlTypeCode::Notation {
39        return false;
40    }
41
42    // untypedAtomic and string can cast to anything (except NOTATION, handled above)
43    if source == XmlTypeCode::UntypedAtomic
44        || source == XmlTypeCode::String
45        || source.is_string_derived()
46    {
47        return true;
48    }
49
50    // Determine the effective source category (map integer subtypes to numeric)
51    let source_numeric = source.is_numeric();
52    let source_boolean = source == XmlTypeCode::Boolean;
53    let source_duration = matches!(
54        source,
55        XmlTypeCode::Duration | XmlTypeCode::YearMonthDuration | XmlTypeCode::DayTimeDuration
56    );
57    let source_datetime = source == XmlTypeCode::DateTime || source == XmlTypeCode::DateTimeStamp;
58    let source_date = source == XmlTypeCode::Date;
59    let source_time = source == XmlTypeCode::Time;
60    let source_gyearmonth = source == XmlTypeCode::GYearMonth;
61    let source_gyear = source == XmlTypeCode::GYear;
62    let source_gmonthday = source == XmlTypeCode::GMonthDay;
63    let source_gday = source == XmlTypeCode::GDay;
64    let source_gmonth = source == XmlTypeCode::GMonth;
65    let source_binary = matches!(source, XmlTypeCode::Base64Binary | XmlTypeCode::HexBinary);
66    let source_anyuri = source == XmlTypeCode::AnyUri;
67
68    let target_ua_or_string = target == XmlTypeCode::UntypedAtomic
69        || target == XmlTypeCode::String
70        || target.is_string_derived();
71    let target_numeric = target.is_numeric() || target == XmlTypeCode::Boolean;
72
73    // numeric types → uA, string, float, double, decimal, integer (subtypes), boolean
74    if source_numeric {
75        return target_ua_or_string || target_numeric;
76    }
77
78    // boolean → uA, string, float, double, decimal, integer (subtypes), boolean
79    if source_boolean {
80        return target_ua_or_string || target_numeric;
81    }
82
83    // duration types → uA, string, duration, yearMonthDuration, dayTimeDuration
84    if source_duration {
85        return target_ua_or_string
86            || matches!(
87                target,
88                XmlTypeCode::Duration
89                    | XmlTypeCode::YearMonthDuration
90                    | XmlTypeCode::DayTimeDuration
91            );
92    }
93
94    // dateTime/dateTimeStamp → uA, string, dateTime, dateTimeStamp, date, time, gYearMonth, gYear, gMonthDay, gDay, gMonth
95    if source_datetime {
96        return target_ua_or_string
97            || matches!(
98                target,
99                XmlTypeCode::DateTime
100                    | XmlTypeCode::DateTimeStamp
101                    | XmlTypeCode::Date
102                    | XmlTypeCode::Time
103                    | XmlTypeCode::GYearMonth
104                    | XmlTypeCode::GYear
105                    | XmlTypeCode::GMonthDay
106                    | XmlTypeCode::GDay
107                    | XmlTypeCode::GMonth
108            );
109    }
110
111    // date → uA, string, dateTime, date, gYearMonth, gYear, gMonthDay, gDay, gMonth
112    if source_date {
113        return target_ua_or_string
114            || matches!(
115                target,
116                XmlTypeCode::DateTime
117                    | XmlTypeCode::Date
118                    | XmlTypeCode::GYearMonth
119                    | XmlTypeCode::GYear
120                    | XmlTypeCode::GMonthDay
121                    | XmlTypeCode::GDay
122                    | XmlTypeCode::GMonth
123            );
124    }
125
126    // time → uA, string, time
127    if source_time {
128        return target_ua_or_string || target == XmlTypeCode::Time;
129    }
130
131    // gYearMonth → uA, string, gYearMonth
132    if source_gyearmonth {
133        return target_ua_or_string || target == XmlTypeCode::GYearMonth;
134    }
135
136    // gYear → uA, string, gYear
137    if source_gyear {
138        return target_ua_or_string || target == XmlTypeCode::GYear;
139    }
140
141    // gMonthDay → uA, string, gMonthDay
142    if source_gmonthday {
143        return target_ua_or_string || target == XmlTypeCode::GMonthDay;
144    }
145
146    // gDay → uA, string, gDay
147    if source_gday {
148        return target_ua_or_string || target == XmlTypeCode::GDay;
149    }
150
151    // gMonth → uA, string, gMonth
152    if source_gmonth {
153        return target_ua_or_string || target == XmlTypeCode::GMonth;
154    }
155
156    // base64Binary / hexBinary → uA, string, base64Binary, hexBinary
157    if source_binary {
158        return target_ua_or_string
159            || matches!(target, XmlTypeCode::Base64Binary | XmlTypeCode::HexBinary);
160    }
161
162    // anyURI → uA, string, anyURI
163    if source_anyuri {
164        return target_ua_or_string || target == XmlTypeCode::AnyUri;
165    }
166
167    false
168}
169
170/// Cast an atomic value to a target type.
171///
172/// This implements the XPath `cast as` expression for atomic values.
173///
174/// # Arguments
175///
176/// * `value` - The value to cast
177/// * `target_type` - The target type code
178///
179/// # Returns
180///
181/// * `Ok(XmlValue)` - The cast value
182/// * `Err(XPathError)` - If casting fails (FORG0001) or types are incompatible (XPTY0004)
183pub fn cast_to(value: &XmlValue, target_type: XmlTypeCode) -> Result<XmlValue, XPathError> {
184    // Same type - no conversion needed
185    if value.type_code == target_type {
186        return Ok(value.clone());
187    }
188
189    // Check the XPath 2.0 casting table
190    if !can_cast(value.type_code, target_type) {
191        return Err(XPathError::type_mismatch(
192            format!("{:?}", value.type_code),
193            format!("{:?}", target_type),
194        ));
195    }
196
197    let string_val = value.to_string_value();
198
199    match target_type {
200        XmlTypeCode::String => Ok(XmlValue::string(string_val)),
201
202        XmlTypeCode::Boolean => cast_to_boolean(value, &string_val),
203
204        XmlTypeCode::Decimal => cast_to_decimal(value, &string_val),
205
206        XmlTypeCode::Integer => cast_to_integer(value, &string_val),
207
208        XmlTypeCode::Float => cast_to_float(value, &string_val),
209
210        XmlTypeCode::Double => cast_to_double(value, &string_val),
211
212        XmlTypeCode::UntypedAtomic => Ok(XmlValue::untyped(string_val)),
213
214        // Integer-derived types: cast to integer first, then validate range
215        target if is_integer_derived(target) => cast_to_integer_subtype(value, target),
216
217        // Date/time types: try direct cross-casting first, then parse via VALIDATOR_REGISTRY
218        XmlTypeCode::DateTime
219        | XmlTypeCode::Date
220        | XmlTypeCode::Time
221        | XmlTypeCode::Duration
222        | XmlTypeCode::YearMonthDuration
223        | XmlTypeCode::DayTimeDuration
224        | XmlTypeCode::GYearMonth
225        | XmlTypeCode::GYear
226        | XmlTypeCode::GMonthDay
227        | XmlTypeCode::GDay
228        | XmlTypeCode::GMonth
229        | XmlTypeCode::DateTimeStamp => {
230            // Try direct cross-casting between date/time/duration types
231            if let Some(result) = cast_datetime_cross(value, target_type) {
232                return result;
233            }
234            let type_name = target_type.local_name().unwrap_or("unknown");
235            VALIDATOR_REGISTRY
236                .validate(target_type, string_val.trim())
237                .map_err(|_| {
238                    XPathError::invalid_cast_value(&string_val, format!("xs:{}", type_name))
239                })
240        }
241
242        // Binary and URI types: try direct binary cross-casting, then parse via VALIDATOR_REGISTRY
243        XmlTypeCode::AnyUri | XmlTypeCode::HexBinary | XmlTypeCode::Base64Binary => {
244            // Direct binary cross-casting (base64Binary ↔ hexBinary)
245            if let Some(result) = cast_binary_cross(value, target_type) {
246                return result;
247            }
248            let type_name = target_type.local_name().unwrap_or("unknown");
249            VALIDATOR_REGISTRY
250                .validate(target_type, string_val.trim())
251                .map_err(|_| {
252                    XPathError::invalid_cast_value(&string_val, format!("xs:{}", type_name))
253                })
254        }
255
256        // String-derived types: parse via VALIDATOR_REGISTRY
257        XmlTypeCode::NormalizedString
258        | XmlTypeCode::Token
259        | XmlTypeCode::Language
260        | XmlTypeCode::NmToken
261        | XmlTypeCode::Name
262        | XmlTypeCode::NCName
263        | XmlTypeCode::Id
264        | XmlTypeCode::IdRef
265        | XmlTypeCode::Entity => {
266            let type_name = target_type.local_name().unwrap_or("unknown");
267            VALIDATOR_REGISTRY
268                .validate(target_type, &string_val)
269                .map_err(|_| {
270                    XPathError::invalid_cast_value(&string_val, format!("xs:{}", type_name))
271                })
272        }
273
274        // QName and NOTATION require special handling (not castable from string)
275        XmlTypeCode::QName | XmlTypeCode::Notation => Err(XPathError::type_mismatch(
276            format!("{:?}", value.type_code),
277            format!("{:?}", target_type),
278        )),
279
280        // Unsupported cast
281        _ => Err(XPathError::type_mismatch(
282            format!("{:?}", value.type_code),
283            format!("{:?}", target_type),
284        )),
285    }
286}
287
288/// Cast a value to boolean.
289fn cast_to_boolean(value: &XmlValue, string_val: &str) -> Result<XmlValue, XPathError> {
290    let result = match &value.value {
291        XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) => *b,
292        XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => *i != BigInt::from(0),
293        XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => !d.is_zero(),
294        XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => *f != 0.0 && !f.is_nan(),
295        XmlValueKind::Atomic(XmlAtomicValue::Double(d)) => *d != 0.0 && !d.is_nan(),
296        _ => {
297            let s = string_val.trim();
298            match s {
299                "true" | "1" => true,
300                "false" | "0" => false,
301                _ => {
302                    return Err(XPathError::invalid_cast_value(string_val, "xs:boolean"));
303                }
304            }
305        }
306    };
307    Ok(XmlValue::boolean(result))
308}
309
310/// Cast a value to decimal.
311fn cast_to_decimal(value: &XmlValue, string_val: &str) -> Result<XmlValue, XPathError> {
312    let result = match &value.value {
313        XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => *d,
314        XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => i
315            .to_string()
316            .parse::<Decimal>()
317            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:decimal"))?,
318        XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => {
319            if f.is_nan() || f.is_infinite() {
320                return Err(XPathError::invalid_cast_value(string_val, "xs:decimal"));
321            }
322            if *f == 0.0 {
323                Decimal::ZERO
324            } else {
325                Decimal::try_from(*f)
326                    .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:decimal"))?
327            }
328        }
329        XmlValueKind::Atomic(XmlAtomicValue::Double(d)) => {
330            if d.is_nan() || d.is_infinite() {
331                return Err(XPathError::invalid_cast_value(string_val, "xs:decimal"));
332            }
333            if *d == 0.0 {
334                Decimal::ZERO
335            } else {
336                Decimal::try_from(*d)
337                    .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:decimal"))?
338            }
339        }
340        XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) => {
341            if *b {
342                Decimal::ONE
343            } else {
344                Decimal::ZERO
345            }
346        }
347        _ => string_val
348            .trim()
349            .parse::<Decimal>()
350            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:decimal"))?,
351    };
352    Ok(XmlValue::decimal(result))
353}
354
355/// Cast a value to integer.
356fn cast_to_integer(value: &XmlValue, string_val: &str) -> Result<XmlValue, XPathError> {
357    let result = match &value.value {
358        XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => i.clone(),
359        XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => {
360            // Truncate decimal to integer
361            let truncated = d.trunc();
362            truncated
363                .to_string()
364                .parse::<BigInt>()
365                .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:integer"))?
366        }
367        XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => {
368            if f.is_nan() || f.is_infinite() {
369                return Err(XPathError::invalid_cast_value(string_val, "xs:integer"));
370            }
371            let truncated = f.trunc() as f64;
372            // Use string round-trip to handle values outside i64 range
373            let s = format!("{:.0}", truncated);
374            s.parse::<BigInt>().map_err(|_| XPathError::FOCA0003 {
375                message: format!("Value {} is too large for xs:integer", string_val),
376            })?
377        }
378        XmlValueKind::Atomic(XmlAtomicValue::Double(d)) => {
379            if d.is_nan() || d.is_infinite() {
380                return Err(XPathError::invalid_cast_value(string_val, "xs:integer"));
381            }
382            // Use string round-trip to handle values outside i64 range
383            let s = format!("{:.0}", d.trunc());
384            s.parse::<BigInt>().map_err(|_| XPathError::FOCA0003 {
385                message: format!("Value {} is too large for xs:integer", string_val),
386            })?
387        }
388        XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) => BigInt::from(if *b { 1 } else { 0 }),
389        _ => string_val
390            .trim()
391            .parse::<BigInt>()
392            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:integer"))?,
393    };
394    Ok(XmlValue::integer(result))
395}
396
397/// Cast a value to float.
398fn cast_to_float(value: &XmlValue, string_val: &str) -> Result<XmlValue, XPathError> {
399    let result = match &value.value {
400        XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => *f,
401        XmlValueKind::Atomic(XmlAtomicValue::Double(d)) => *d as f32,
402        XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => d
403            .to_string()
404            .parse::<f32>()
405            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:float"))?,
406        XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => i
407            .to_string()
408            .parse::<f32>()
409            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:float"))?,
410        XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) => {
411            if *b {
412                1.0
413            } else {
414                0.0
415            }
416        }
417        _ => parse_float_with_special(string_val.trim())
418            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:float"))?,
419    };
420    Ok(XmlValue::float(result))
421}
422
423/// Cast a value to double.
424fn cast_to_double(value: &XmlValue, string_val: &str) -> Result<XmlValue, XPathError> {
425    let result = match &value.value {
426        XmlValueKind::Atomic(XmlAtomicValue::Double(d)) => *d,
427        XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => *f as f64,
428        XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => d
429            .to_string()
430            .parse::<f64>()
431            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:double"))?,
432        XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => i
433            .to_string()
434            .parse::<f64>()
435            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:double"))?,
436        XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) => {
437            if *b {
438                1.0
439            } else {
440                0.0
441            }
442        }
443        _ => parse_double_with_special(string_val.trim())
444            .map_err(|_| XPathError::invalid_cast_value(string_val, "xs:double"))?,
445    };
446    Ok(XmlValue::double(result))
447}
448
449/// Parse a float string, handling special values like INF and NaN.
450fn parse_float_with_special(s: &str) -> Result<f32, ()> {
451    match s {
452        "INF" => Ok(f32::INFINITY),
453        "-INF" => Ok(f32::NEG_INFINITY),
454        "NaN" => Ok(f32::NAN),
455        _ => s.parse::<f32>().map_err(|_| ()),
456    }
457}
458
459/// Parse a double string, handling special values like INF and NaN.
460fn parse_double_with_special(s: &str) -> Result<f64, ()> {
461    match s {
462        "INF" => Ok(f64::INFINITY),
463        "-INF" => Ok(f64::NEG_INFINITY),
464        "NaN" => Ok(f64::NAN),
465        _ => s.parse::<f64>().map_err(|_| ()),
466    }
467}
468
469/// Treat a value as a specific type (type assertion without conversion).
470///
471/// This implements the XPath `treat as` expression. Unlike `cast`, this
472/// does not perform any conversion - it just validates that the value
473/// already has the expected type.
474///
475/// # Arguments
476///
477/// * `value` - The value to check
478/// * `target_type` - The expected type code
479///
480/// # Returns
481///
482/// * `Ok(XmlValue)` - The original value if it matches
483/// * `Err(XPathError)` - XPTY0004 if type doesn't match
484pub fn treat_as(value: &XmlValue, target_type: XmlTypeCode) -> Result<XmlValue, XPathError> {
485    if type_matches(value.type_code, target_type) {
486        Ok(value.clone())
487    } else {
488        Err(XPathError::type_mismatch(
489            format!("{:?}", target_type),
490            format!("{:?}", value.type_code),
491        ))
492    }
493}
494
495/// Check if a value is an instance of a type.
496///
497/// This implements the XPath `instance of` expression.
498///
499/// # Arguments
500///
501/// * `value` - The value to check
502/// * `target_type` - The type to check against
503///
504/// # Returns
505///
506/// `true` if the value matches the type, `false` otherwise
507pub fn instance_of(value: &XmlValue, target_type: XmlTypeCode) -> bool {
508    type_matches(value.type_code, target_type)
509}
510
511/// Check if a value is an instance of a type (optional value version).
512///
513/// Returns true for None if target type allows empty sequence.
514pub fn instance_of_opt(
515    value: Option<&XmlValue>,
516    target_type: XmlTypeCode,
517    allow_empty: bool,
518) -> bool {
519    match value {
520        None => allow_empty,
521        Some(v) => instance_of(v, target_type),
522    }
523}
524
525/// Check if a value can be cast to a type.
526///
527/// This implements the XPath `castable as` expression.
528///
529/// # Arguments
530///
531/// * `value` - The value to check
532/// * `target_type` - The target type
533///
534/// # Returns
535///
536/// `true` if the cast would succeed, `false` otherwise
537pub fn castable(value: &XmlValue, target_type: XmlTypeCode) -> bool {
538    cast_to(value, target_type).is_ok()
539}
540
541/// Check if a value can be cast to a type (optional value version).
542pub fn castable_opt(value: Option<&XmlValue>, target_type: XmlTypeCode, allow_empty: bool) -> bool {
543    match value {
544        None => allow_empty,
545        Some(v) => castable(v, target_type),
546    }
547}
548
549/// Check if a type code is an integer-derived type.
550fn is_integer_derived(code: XmlTypeCode) -> bool {
551    matches!(
552        code,
553        XmlTypeCode::Integer
554            | XmlTypeCode::NonPositiveInteger
555            | XmlTypeCode::NegativeInteger
556            | XmlTypeCode::Long
557            | XmlTypeCode::Int
558            | XmlTypeCode::Short
559            | XmlTypeCode::Byte
560            | XmlTypeCode::NonNegativeInteger
561            | XmlTypeCode::UnsignedLong
562            | XmlTypeCode::UnsignedInt
563            | XmlTypeCode::UnsignedShort
564            | XmlTypeCode::UnsignedByte
565            | XmlTypeCode::PositiveInteger
566    )
567}
568
569/// Check if a source type matches a target type for type checking.
570///
571/// This handles type compatibility rules including:
572/// - Exact type match
573/// - anyAtomicType matches any atomic type
574/// - String derived types match string
575/// - Integer derived types match integer
576pub fn type_matches(source: XmlTypeCode, target: XmlTypeCode) -> bool {
577    if source == target {
578        return true;
579    }
580
581    // anyAtomicType matches any atomic type
582    if target == XmlTypeCode::AnyAtomicType {
583        return source.is_atomic();
584    }
585
586    // Item matches everything
587    if target == XmlTypeCode::Item {
588        return true;
589    }
590
591    // String type hierarchy
592    if target == XmlTypeCode::String {
593        return source.is_string_derived() || source == XmlTypeCode::UntypedAtomic;
594    }
595
596    // Integer type hierarchy
597    if target == XmlTypeCode::Integer {
598        return is_integer_derived(source);
599    }
600
601    // Decimal type hierarchy (includes integer)
602    if target == XmlTypeCode::Decimal {
603        return source == XmlTypeCode::Decimal || is_integer_derived(source);
604    }
605
606    false
607}
608
609/// Convert a resolved atomic type QualifiedName to XmlTypeCode.
610///
611/// The QualifiedName should have been resolved during binding phase
612/// and must be in the XS (XML Schema) namespace.
613///
614/// # Errors
615///
616/// Returns XPST0051 if the type name is not a known atomic type.
617pub fn resolved_type_to_type_code(
618    qname: &QualifiedName,
619    names: &NameTable,
620) -> Result<XmlTypeCode, XPathError> {
621    // Check namespace is XS_NAMESPACE
622    match qname.namespace_uri {
623        Some(ns_id) if ns_id == well_known::XS_NAMESPACE => {}
624        _ => {
625            let local = names.resolve(qname.local_name);
626            return Err(XPathError::XPST0051 {
627                type_name: local.to_string(),
628            });
629        }
630    }
631
632    // Get local name and convert to type code
633    let local_name = names.resolve(qname.local_name);
634    XmlTypeCode::from_local_name(&local_name).ok_or_else(|| XPathError::XPST0051 {
635        type_name: local_name.to_string(),
636    })
637}
638
639/// Check if an occurrence indicator allows the given item count.
640///
641/// This implements XPath 2.0 sequence type cardinality matching:
642/// - `One` (no indicator): exactly 1 item
643/// - `ZeroOrOne` (`?`): 0 or 1 items
644/// - `ZeroOrMore` (`*`): any count
645/// - `OneOrMore` (`+`): at least 1 item
646pub fn occurrence_allows_count(occ: OccurrenceIndicator, count: usize) -> bool {
647    match occ {
648        OccurrenceIndicator::One => count == 1,
649        OccurrenceIndicator::ZeroOrOne => count <= 1,
650        OccurrenceIndicator::ZeroOrMore => true,
651        OccurrenceIndicator::OneOrMore => count >= 1,
652    }
653}
654
655/// Cross-cast between date/time/duration types by directly extracting fields.
656/// Returns None if no direct cross-cast applies (fall through to string parsing).
657fn cast_datetime_cross(
658    value: &XmlValue,
659    target: XmlTypeCode,
660) -> Option<Result<XmlValue, XPathError>> {
661    match (&value.value, target) {
662        // DateTime → Date
663        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::Date) => {
664            Some(Ok(XmlValue::new(
665                XmlTypeCode::Date,
666                XmlValueKind::Atomic(XmlAtomicValue::Date(DateValue {
667                    year: dt.year,
668                    month: dt.month,
669                    day: dt.day,
670                    timezone: dt.timezone,
671                })),
672            )))
673        }
674        // DateTime → Time
675        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::Time) => {
676            Some(Ok(XmlValue::new(
677                XmlTypeCode::Time,
678                XmlValueKind::Atomic(XmlAtomicValue::Time(TimeValue {
679                    hour: dt.hour,
680                    minute: dt.minute,
681                    second: dt.second,
682                    timezone: dt.timezone,
683                })),
684            )))
685        }
686        // Date → DateTime (time defaults to 00:00:00)
687        (XmlValueKind::Atomic(XmlAtomicValue::Date(d)), XmlTypeCode::DateTime) => {
688            Some(Ok(XmlValue::new(
689                XmlTypeCode::DateTime,
690                XmlValueKind::Atomic(XmlAtomicValue::DateTime(DateTimeValue {
691                    year: d.year,
692                    month: d.month,
693                    day: d.day,
694                    hour: 0,
695                    minute: 0,
696                    second: Decimal::ZERO,
697                    timezone: d.timezone,
698                })),
699            )))
700        }
701        // DateTime → gYearMonth
702        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::GYearMonth) => {
703            Some(Ok(XmlValue::new(
704                XmlTypeCode::GYearMonth,
705                XmlValueKind::Atomic(XmlAtomicValue::GYearMonth(GYearMonthValue {
706                    year: dt.year,
707                    month: dt.month,
708                    timezone: dt.timezone,
709                })),
710            )))
711        }
712        // DateTime → gYear
713        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::GYear) => {
714            Some(Ok(XmlValue::new(
715                XmlTypeCode::GYear,
716                XmlValueKind::Atomic(XmlAtomicValue::GYear(GYearValue {
717                    year: dt.year,
718                    timezone: dt.timezone,
719                })),
720            )))
721        }
722        // DateTime → gMonthDay
723        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::GMonthDay) => {
724            Some(Ok(XmlValue::new(
725                XmlTypeCode::GMonthDay,
726                XmlValueKind::Atomic(XmlAtomicValue::GMonthDay(GMonthDayValue {
727                    month: dt.month,
728                    day: dt.day,
729                    timezone: dt.timezone,
730                })),
731            )))
732        }
733        // DateTime → gDay
734        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::GDay) => {
735            Some(Ok(XmlValue::new(
736                XmlTypeCode::GDay,
737                XmlValueKind::Atomic(XmlAtomicValue::GDay(GDayValue {
738                    day: dt.day,
739                    timezone: dt.timezone,
740                })),
741            )))
742        }
743        // DateTime → gMonth
744        (XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)), XmlTypeCode::GMonth) => {
745            Some(Ok(XmlValue::new(
746                XmlTypeCode::GMonth,
747                XmlValueKind::Atomic(XmlAtomicValue::GMonth(GMonthValue {
748                    month: dt.month,
749                    timezone: dt.timezone,
750                })),
751            )))
752        }
753        // Date → gYearMonth
754        (XmlValueKind::Atomic(XmlAtomicValue::Date(d)), XmlTypeCode::GYearMonth) => {
755            Some(Ok(XmlValue::new(
756                XmlTypeCode::GYearMonth,
757                XmlValueKind::Atomic(XmlAtomicValue::GYearMonth(GYearMonthValue {
758                    year: d.year,
759                    month: d.month,
760                    timezone: d.timezone,
761                })),
762            )))
763        }
764        // Date → gYear
765        (XmlValueKind::Atomic(XmlAtomicValue::Date(d)), XmlTypeCode::GYear) => {
766            Some(Ok(XmlValue::new(
767                XmlTypeCode::GYear,
768                XmlValueKind::Atomic(XmlAtomicValue::GYear(GYearValue {
769                    year: d.year,
770                    timezone: d.timezone,
771                })),
772            )))
773        }
774        // Date → gMonthDay
775        (XmlValueKind::Atomic(XmlAtomicValue::Date(d)), XmlTypeCode::GMonthDay) => {
776            Some(Ok(XmlValue::new(
777                XmlTypeCode::GMonthDay,
778                XmlValueKind::Atomic(XmlAtomicValue::GMonthDay(GMonthDayValue {
779                    month: d.month,
780                    day: d.day,
781                    timezone: d.timezone,
782                })),
783            )))
784        }
785        // Date → gDay
786        (XmlValueKind::Atomic(XmlAtomicValue::Date(d)), XmlTypeCode::GDay) => {
787            Some(Ok(XmlValue::new(
788                XmlTypeCode::GDay,
789                XmlValueKind::Atomic(XmlAtomicValue::GDay(GDayValue {
790                    day: d.day,
791                    timezone: d.timezone,
792                })),
793            )))
794        }
795        // Date → gMonth
796        (XmlValueKind::Atomic(XmlAtomicValue::Date(d)), XmlTypeCode::GMonth) => {
797            Some(Ok(XmlValue::new(
798                XmlTypeCode::GMonth,
799                XmlValueKind::Atomic(XmlAtomicValue::GMonth(GMonthValue {
800                    month: d.month,
801                    timezone: d.timezone,
802                })),
803            )))
804        }
805        // Duration → YearMonthDuration (extract year/month parts)
806        (XmlValueKind::Atomic(XmlAtomicValue::Duration(d)), XmlTypeCode::YearMonthDuration) => {
807            Some(Ok(XmlValue::new(
808                XmlTypeCode::YearMonthDuration,
809                XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(YearMonthDurationValue {
810                    negative: d.negative,
811                    years: d.years,
812                    months: d.months,
813                })),
814            )))
815        }
816        // Duration → DayTimeDuration (extract day/time parts)
817        (XmlValueKind::Atomic(XmlAtomicValue::Duration(d)), XmlTypeCode::DayTimeDuration) => {
818            Some(Ok(XmlValue::new(
819                XmlTypeCode::DayTimeDuration,
820                XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(DayTimeDurationValue {
821                    negative: d.negative,
822                    days: d.days,
823                    hours: d.hours,
824                    minutes: d.minutes,
825                    seconds: d.seconds,
826                })),
827            )))
828        }
829        // YearMonthDuration → Duration
830        (XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(ym)), XmlTypeCode::Duration) => {
831            Some(Ok(XmlValue::new(
832                XmlTypeCode::Duration,
833                XmlValueKind::Atomic(XmlAtomicValue::Duration(DurationValue {
834                    negative: ym.negative,
835                    years: ym.years,
836                    months: ym.months,
837                    days: 0,
838                    hours: 0,
839                    minutes: 0,
840                    seconds: Decimal::ZERO,
841                })),
842            )))
843        }
844        // DayTimeDuration → Duration
845        (XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(dt)), XmlTypeCode::Duration) => {
846            Some(Ok(XmlValue::new(
847                XmlTypeCode::Duration,
848                XmlValueKind::Atomic(XmlAtomicValue::Duration(DurationValue {
849                    negative: dt.negative,
850                    years: 0,
851                    months: 0,
852                    days: dt.days,
853                    hours: dt.hours,
854                    minutes: dt.minutes,
855                    seconds: dt.seconds,
856                })),
857            )))
858        }
859        // YearMonthDuration → DayTimeDuration: yields zero per XPath 2.0 F&O §17.1.5.
860        // Cast goes through xs:duration as intermediate; yearMonthDuration has no day/time
861        // components, so extracting the day-time part always produces PT0S.
862        (
863            XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(_)),
864            XmlTypeCode::DayTimeDuration,
865        ) => Some(Ok(XmlValue::new(
866            XmlTypeCode::DayTimeDuration,
867            XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(DayTimeDurationValue {
868                negative: false,
869                days: 0,
870                hours: 0,
871                minutes: 0,
872                seconds: Decimal::ZERO,
873            })),
874        ))),
875        // DayTimeDuration → YearMonthDuration: yields zero per XPath 2.0 F&O §17.1.5.
876        // Cast goes through xs:duration as intermediate; dayTimeDuration has no year/month
877        // components, so extracting the year-month part always produces P0M.
878        (
879            XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(_)),
880            XmlTypeCode::YearMonthDuration,
881        ) => Some(Ok(XmlValue::new(
882            XmlTypeCode::YearMonthDuration,
883            XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(YearMonthDurationValue {
884                negative: false,
885                years: 0,
886                months: 0,
887            })),
888        ))),
889        _ => None,
890    }
891}
892
893/// Cross-cast between binary types (base64Binary ↔ hexBinary).
894fn cast_binary_cross(
895    value: &XmlValue,
896    target: XmlTypeCode,
897) -> Option<Result<XmlValue, XPathError>> {
898    match (&value.value, target) {
899        (XmlValueKind::Atomic(XmlAtomicValue::Base64Binary(bytes)), XmlTypeCode::HexBinary) => {
900            Some(Ok(XmlValue::new(
901                XmlTypeCode::HexBinary,
902                XmlValueKind::Atomic(XmlAtomicValue::HexBinary(bytes.clone())),
903            )))
904        }
905        (XmlValueKind::Atomic(XmlAtomicValue::HexBinary(bytes)), XmlTypeCode::Base64Binary) => {
906            Some(Ok(XmlValue::new(
907                XmlTypeCode::Base64Binary,
908                XmlValueKind::Atomic(XmlAtomicValue::Base64Binary(bytes.clone())),
909            )))
910        }
911        _ => None,
912    }
913}
914
915/// Cast a numeric value to a specific integer subtype.
916///
917/// This handles casting to types like xs:int, xs:short, xs:byte, etc.
918/// with range checking.
919pub fn cast_to_integer_subtype(
920    value: &XmlValue,
921    target_type: XmlTypeCode,
922) -> Result<XmlValue, XPathError> {
923    // First cast to integer
924    let int_val = cast_to(value, XmlTypeCode::Integer)?;
925    let bigint = int_val
926        .as_integer()
927        .ok_or_else(|| XPathError::internal("Expected integer after cast"))?;
928
929    // Then validate range for the specific subtype
930    let (min, max): (i128, i128) = match target_type {
931        XmlTypeCode::Byte => (i8::MIN as i128, i8::MAX as i128),
932        XmlTypeCode::Short => (i16::MIN as i128, i16::MAX as i128),
933        XmlTypeCode::Int => (i32::MIN as i128, i32::MAX as i128),
934        XmlTypeCode::Long => (i64::MIN as i128, i64::MAX as i128),
935        XmlTypeCode::UnsignedByte => (0, u8::MAX as i128),
936        XmlTypeCode::UnsignedShort => (0, u16::MAX as i128),
937        XmlTypeCode::UnsignedInt => (0, u32::MAX as i128),
938        XmlTypeCode::UnsignedLong => (0, u64::MAX as i128),
939        XmlTypeCode::PositiveInteger => (1, i128::MAX),
940        XmlTypeCode::NonNegativeInteger => (0, i128::MAX),
941        XmlTypeCode::NegativeInteger => (i128::MIN, -1),
942        XmlTypeCode::NonPositiveInteger => (i128::MIN, 0),
943        XmlTypeCode::Integer => return Ok(int_val),
944        _ => {
945            return Err(XPathError::type_mismatch(
946                format!("{:?}", value.type_code),
947                format!("{:?}", target_type),
948            ))
949        }
950    };
951
952    // Check range
953    let val_i128: i128 = bigint.to_string().parse().map_err(|_| {
954        XPathError::invalid_cast_value(bigint.to_string(), format!("{:?}", target_type))
955    })?;
956
957    if val_i128 < min || val_i128 > max {
958        return Err(XPathError::invalid_cast_value(
959            bigint.to_string(),
960            format!("{:?}", target_type),
961        ));
962    }
963
964    Ok(XmlValue::new(
965        target_type,
966        XmlValueKind::Atomic(XmlAtomicValue::Integer(bigint.clone())),
967    ))
968}
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973
974    #[test]
975    fn test_cast_string_to_integer() {
976        let value = XmlValue::string("42");
977        let result = cast_to(&value, XmlTypeCode::Integer).unwrap();
978        assert_eq!(result.type_code, XmlTypeCode::Integer);
979        assert_eq!(result.as_integer().unwrap(), &BigInt::from(42));
980    }
981
982    #[test]
983    fn test_cast_string_to_decimal() {
984        let value = XmlValue::string("2.5");
985        let result = cast_to(&value, XmlTypeCode::Decimal).unwrap();
986        assert_eq!(result.type_code, XmlTypeCode::Decimal);
987    }
988
989    #[test]
990    fn test_cast_string_to_boolean() {
991        assert_eq!(
992            cast_to(&XmlValue::string("true"), XmlTypeCode::Boolean)
993                .unwrap()
994                .as_boolean(),
995            Some(true)
996        );
997        assert_eq!(
998            cast_to(&XmlValue::string("false"), XmlTypeCode::Boolean)
999                .unwrap()
1000                .as_boolean(),
1001            Some(false)
1002        );
1003        assert_eq!(
1004            cast_to(&XmlValue::string("1"), XmlTypeCode::Boolean)
1005                .unwrap()
1006                .as_boolean(),
1007            Some(true)
1008        );
1009        assert_eq!(
1010            cast_to(&XmlValue::string("0"), XmlTypeCode::Boolean)
1011                .unwrap()
1012                .as_boolean(),
1013            Some(false)
1014        );
1015    }
1016
1017    #[test]
1018    fn test_cast_invalid_string_to_boolean() {
1019        let result = cast_to(&XmlValue::string("yes"), XmlTypeCode::Boolean);
1020        assert!(result.is_err());
1021    }
1022
1023    #[test]
1024    fn test_cast_integer_to_double() {
1025        let value = XmlValue::integer(BigInt::from(42));
1026        let result = cast_to(&value, XmlTypeCode::Double).unwrap();
1027        assert_eq!(result.as_double(), Some(42.0));
1028    }
1029
1030    #[test]
1031    fn test_cast_double_to_integer() {
1032        let value = XmlValue::double(42.7);
1033        let result = cast_to(&value, XmlTypeCode::Integer).unwrap();
1034        assert_eq!(result.as_integer().unwrap(), &BigInt::from(42)); // Truncated
1035    }
1036
1037    #[test]
1038    fn test_cast_nan_to_integer_fails() {
1039        let value = XmlValue::double(f64::NAN);
1040        let result = cast_to(&value, XmlTypeCode::Integer);
1041        assert!(result.is_err());
1042    }
1043
1044    #[test]
1045    fn test_cast_inf_to_decimal_fails() {
1046        let value = XmlValue::double(f64::INFINITY);
1047        let result = cast_to(&value, XmlTypeCode::Decimal);
1048        assert!(result.is_err());
1049    }
1050
1051    #[test]
1052    fn test_cast_same_type() {
1053        let value = XmlValue::string("hello");
1054        let result = cast_to(&value, XmlTypeCode::String).unwrap();
1055        assert_eq!(result.to_string_value(), "hello");
1056    }
1057
1058    #[test]
1059    fn test_instance_of() {
1060        assert!(instance_of(&XmlValue::string("test"), XmlTypeCode::String));
1061        assert!(instance_of(
1062            &XmlValue::integer(BigInt::from(1)),
1063            XmlTypeCode::Integer
1064        ));
1065        assert!(!instance_of(
1066            &XmlValue::string("test"),
1067            XmlTypeCode::Integer
1068        ));
1069
1070        // anyAtomicType should match any atomic
1071        assert!(instance_of(
1072            &XmlValue::string("test"),
1073            XmlTypeCode::AnyAtomicType
1074        ));
1075    }
1076
1077    #[test]
1078    fn test_castable() {
1079        assert!(castable(&XmlValue::string("42"), XmlTypeCode::Integer));
1080        assert!(!castable(
1081            &XmlValue::string("not a number"),
1082            XmlTypeCode::Integer
1083        ));
1084    }
1085
1086    #[test]
1087    fn test_treat_as_matching() {
1088        let value = XmlValue::string("test");
1089        let result = treat_as(&value, XmlTypeCode::String);
1090        assert!(result.is_ok());
1091    }
1092
1093    #[test]
1094    fn test_treat_as_non_matching() {
1095        let value = XmlValue::string("test");
1096        let result = treat_as(&value, XmlTypeCode::Integer);
1097        assert!(result.is_err());
1098    }
1099
1100    #[test]
1101    fn test_cast_special_float_values() {
1102        let inf = XmlValue::string("INF");
1103        let result = cast_to(&inf, XmlTypeCode::Float).unwrap();
1104        assert!(result.as_double().unwrap().is_infinite());
1105
1106        let nan = XmlValue::string("NaN");
1107        let result = cast_to(&nan, XmlTypeCode::Double).unwrap();
1108        assert!(result.as_double().unwrap().is_nan());
1109    }
1110
1111    #[test]
1112    fn test_cast_to_integer_subtype() {
1113        let value = XmlValue::string("100");
1114
1115        // Should succeed for byte
1116        let result = cast_to_integer_subtype(&value, XmlTypeCode::Byte).unwrap();
1117        assert_eq!(result.type_code, XmlTypeCode::Byte);
1118
1119        // Should fail for byte (out of range)
1120        let big = XmlValue::string("500");
1121        let result = cast_to_integer_subtype(&big, XmlTypeCode::Byte);
1122        assert!(result.is_err());
1123    }
1124
1125    #[test]
1126    fn test_cast_numeric_to_boolean() {
1127        // Integer non-zero → true
1128        assert_eq!(
1129            cast_to(&XmlValue::integer(BigInt::from(10)), XmlTypeCode::Boolean)
1130                .unwrap()
1131                .as_boolean(),
1132            Some(true)
1133        );
1134        // Integer zero → false
1135        assert_eq!(
1136            cast_to(&XmlValue::integer(BigInt::from(0)), XmlTypeCode::Boolean)
1137                .unwrap()
1138                .as_boolean(),
1139            Some(false)
1140        );
1141        // Double NaN → false
1142        assert_eq!(
1143            cast_to(&XmlValue::double(f64::NAN), XmlTypeCode::Boolean)
1144                .unwrap()
1145                .as_boolean(),
1146            Some(false)
1147        );
1148        // Double -0 → false
1149        assert_eq!(
1150            cast_to(&XmlValue::double(-0.0), XmlTypeCode::Boolean)
1151                .unwrap()
1152                .as_boolean(),
1153            Some(false)
1154        );
1155        // Float non-zero → true
1156        assert_eq!(
1157            cast_to(&XmlValue::float(1.5), XmlTypeCode::Boolean)
1158                .unwrap()
1159                .as_boolean(),
1160            Some(true)
1161        );
1162        // Decimal → true
1163        assert_eq!(
1164            cast_to(
1165                &XmlValue::decimal(Decimal::new(-11234, 4)),
1166                XmlTypeCode::Boolean
1167            )
1168            .unwrap()
1169            .as_boolean(),
1170            Some(true)
1171        );
1172    }
1173
1174    #[test]
1175    fn test_cast_datetime_to_date() {
1176        let dt = XmlValue::new(
1177            XmlTypeCode::DateTime,
1178            XmlValueKind::Atomic(XmlAtomicValue::DateTime(DateTimeValue {
1179                year: 1999,
1180                month: 5,
1181                day: 31,
1182                hour: 13,
1183                minute: 20,
1184                second: Decimal::ZERO,
1185                timezone: Some(crate::types::value::TimezoneOffset(-300)),
1186            })),
1187        );
1188        let result = cast_to(&dt, XmlTypeCode::Date).unwrap();
1189        assert_eq!(result.type_code, XmlTypeCode::Date);
1190        assert_eq!(result.to_string_value(), "1999-05-31-05:00");
1191    }
1192
1193    #[test]
1194    fn test_cast_datetime_to_time() {
1195        let dt = XmlValue::new(
1196            XmlTypeCode::DateTime,
1197            XmlValueKind::Atomic(XmlAtomicValue::DateTime(DateTimeValue {
1198                year: 1999,
1199                month: 5,
1200                day: 31,
1201                hour: 13,
1202                minute: 20,
1203                second: Decimal::ZERO,
1204                timezone: Some(crate::types::value::TimezoneOffset(-300)),
1205            })),
1206        );
1207        let result = cast_to(&dt, XmlTypeCode::Time).unwrap();
1208        assert_eq!(result.type_code, XmlTypeCode::Time);
1209        assert_eq!(result.to_string_value(), "13:20:00-05:00");
1210    }
1211
1212    #[test]
1213    fn test_cast_date_to_datetime() {
1214        let d = XmlValue::new(
1215            XmlTypeCode::Date,
1216            XmlValueKind::Atomic(XmlAtomicValue::Date(DateValue {
1217                year: 1999,
1218                month: 5,
1219                day: 31,
1220                timezone: Some(crate::types::value::TimezoneOffset::UTC),
1221            })),
1222        );
1223        let result = cast_to(&d, XmlTypeCode::DateTime).unwrap();
1224        assert_eq!(result.type_code, XmlTypeCode::DateTime);
1225        assert_eq!(result.to_string_value(), "1999-05-31T00:00:00Z");
1226    }
1227
1228    #[test]
1229    fn test_cast_datetime_to_gyear() {
1230        let dt = XmlValue::new(
1231            XmlTypeCode::DateTime,
1232            XmlValueKind::Atomic(XmlAtomicValue::DateTime(DateTimeValue {
1233                year: 1999,
1234                month: 5,
1235                day: 31,
1236                hour: 13,
1237                minute: 20,
1238                second: Decimal::ZERO,
1239                timezone: None,
1240            })),
1241        );
1242        let result = cast_to(&dt, XmlTypeCode::GYear).unwrap();
1243        assert_eq!(result.to_string_value(), "1999");
1244
1245        let result = cast_to(&dt, XmlTypeCode::GMonth).unwrap();
1246        assert_eq!(result.to_string_value(), "--05");
1247
1248        let result = cast_to(&dt, XmlTypeCode::GDay).unwrap();
1249        assert_eq!(result.to_string_value(), "---31");
1250
1251        let result = cast_to(&dt, XmlTypeCode::GMonthDay).unwrap();
1252        assert_eq!(result.to_string_value(), "--05-31");
1253
1254        let result = cast_to(&dt, XmlTypeCode::GYearMonth).unwrap();
1255        assert_eq!(result.to_string_value(), "1999-05");
1256    }
1257
1258    #[test]
1259    fn test_cast_duration_to_yearmonth() {
1260        let dur = XmlValue::new(
1261            XmlTypeCode::Duration,
1262            XmlValueKind::Atomic(XmlAtomicValue::Duration(DurationValue {
1263                negative: false,
1264                years: 1,
1265                months: 2,
1266                days: 3,
1267                hours: 10,
1268                minutes: 30,
1269                seconds: Decimal::new(23, 0),
1270            })),
1271        );
1272        let result = cast_to(&dur, XmlTypeCode::YearMonthDuration).unwrap();
1273        assert_eq!(result.to_string_value(), "P1Y2M");
1274
1275        let result = cast_to(&dur, XmlTypeCode::DayTimeDuration).unwrap();
1276        assert_eq!(result.to_string_value(), "P3DT10H30M23S");
1277    }
1278
1279    #[test]
1280    fn test_cast_binary_cross() {
1281        // base64Binary → hexBinary
1282        let b64 = XmlValue::new(
1283            XmlTypeCode::Base64Binary,
1284            XmlValueKind::Atomic(XmlAtomicValue::Base64Binary(vec![0xAB, 0xCD])),
1285        );
1286        let result = cast_to(&b64, XmlTypeCode::HexBinary).unwrap();
1287        assert_eq!(result.type_code, XmlTypeCode::HexBinary);
1288        assert_eq!(result.to_string_value(), "ABCD");
1289
1290        // hexBinary → base64Binary
1291        let hex = XmlValue::new(
1292            XmlTypeCode::HexBinary,
1293            XmlValueKind::Atomic(XmlAtomicValue::HexBinary(vec![0xFF, 0x00])),
1294        );
1295        let result = cast_to(&hex, XmlTypeCode::Base64Binary).unwrap();
1296        assert_eq!(result.type_code, XmlTypeCode::Base64Binary);
1297    }
1298
1299    #[test]
1300    fn test_cast_string_to_time_with_timezone() {
1301        let value = XmlValue::string("13:20:00-05:00");
1302        let result = cast_to(&value, XmlTypeCode::Time).unwrap();
1303        assert_eq!(result.type_code, XmlTypeCode::Time);
1304        assert_eq!(result.to_string_value(), "13:20:00-05:00");
1305    }
1306
1307    #[test]
1308    fn test_cast_string_to_yearmonth_duration_zero() {
1309        let value = XmlValue::string("P0Y0M");
1310        let result = cast_to(&value, XmlTypeCode::YearMonthDuration).unwrap();
1311        assert_eq!(result.to_string_value(), "P0M");
1312    }
1313}