Skip to main content

xsd_schema/types/
value.rs

1//! XSD value types for typed atomic values
2//!
3//! This module provides the `XmlValue` type for representing typed XSD values,
4//! integrating with the `xsd-types` crate for atomic value parsing and formatting.
5//!
6//! ## Design
7//!
8//! - `XmlValue` is a typed container for XSD values
9//! - `XmlAtomicValue` holds the actual parsed value
10//! - QName and NOTATION values use `QualifiedName` (namespace-resolved)
11//! - List values store sequences of atomic values with a known item type
12
13use std::fmt;
14
15use num_bigint::BigInt;
16use rust_decimal::prelude::ToPrimitive;
17use rust_decimal::Decimal;
18
19use super::{PrimitiveTypeCode, XmlTypeCode};
20use crate::ids::SimpleTypeKey;
21use crate::namespace::qname::QualifiedName;
22
23/// A typed XSD value with type information.
24///
25/// This is the primary value type for XPath2/XQuery operations.
26/// It carries both the value and its type information.
27#[derive(Debug, Clone, PartialEq)]
28pub struct XmlValue {
29    /// The type code identifying the value's type
30    pub type_code: XmlTypeCode,
31    /// Optional reference to a schema-defined type
32    pub schema_type: Option<SimpleTypeKey>,
33    /// The actual value
34    pub value: XmlValueKind,
35}
36
37impl XmlValue {
38    /// Create a new XmlValue with the given type code and value
39    pub fn new(type_code: XmlTypeCode, value: XmlValueKind) -> Self {
40        Self {
41            type_code,
42            schema_type: None,
43            value,
44        }
45    }
46
47    /// Create a new XmlValue with schema type reference
48    pub fn with_schema_type(
49        type_code: XmlTypeCode,
50        schema_type: SimpleTypeKey,
51        value: XmlValueKind,
52    ) -> Self {
53        Self {
54            type_code,
55            schema_type: Some(schema_type),
56            value,
57        }
58    }
59
60    /// Create an untyped atomic value
61    pub fn untyped(s: impl Into<String>) -> Self {
62        Self {
63            type_code: XmlTypeCode::UntypedAtomic,
64            schema_type: None,
65            value: XmlValueKind::UntypedAtomic(s.into()),
66        }
67    }
68
69    /// Create a string value
70    pub fn string(s: impl Into<String>) -> Self {
71        Self {
72            type_code: XmlTypeCode::String,
73            schema_type: None,
74            value: XmlValueKind::Atomic(XmlAtomicValue::String(s.into())),
75        }
76    }
77
78    /// Create a boolean value
79    pub fn boolean(b: bool) -> Self {
80        Self {
81            type_code: XmlTypeCode::Boolean,
82            schema_type: None,
83            value: XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)),
84        }
85    }
86
87    /// Create a decimal value
88    pub fn decimal(d: Decimal) -> Self {
89        Self {
90            type_code: XmlTypeCode::Decimal,
91            schema_type: None,
92            value: XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)),
93        }
94    }
95
96    /// Create an integer value
97    pub fn integer(i: BigInt) -> Self {
98        Self {
99            type_code: XmlTypeCode::Integer,
100            schema_type: None,
101            value: XmlValueKind::Atomic(XmlAtomicValue::Integer(i)),
102        }
103    }
104
105    /// Create a float value
106    pub fn float(f: f32) -> Self {
107        Self {
108            type_code: XmlTypeCode::Float,
109            schema_type: None,
110            value: XmlValueKind::Atomic(XmlAtomicValue::Float(f)),
111        }
112    }
113
114    /// Create a double value
115    pub fn double(d: f64) -> Self {
116        Self {
117            type_code: XmlTypeCode::Double,
118            schema_type: None,
119            value: XmlValueKind::Atomic(XmlAtomicValue::Double(d)),
120        }
121    }
122
123    /// Check if this is an atomic value
124    pub fn is_atomic(&self) -> bool {
125        matches!(
126            self.value,
127            XmlValueKind::Atomic(_) | XmlValueKind::UntypedAtomic(_)
128        )
129    }
130
131    /// Check if this is a list value
132    pub fn is_list(&self) -> bool {
133        matches!(self.value, XmlValueKind::List { .. })
134    }
135
136    /// Check if this is a union value
137    pub fn is_union(&self) -> bool {
138        matches!(self.value, XmlValueKind::Union(_))
139    }
140
141    /// Check if this is an untyped atomic value
142    pub fn is_untyped(&self) -> bool {
143        matches!(self.value, XmlValueKind::UntypedAtomic(_))
144    }
145
146    /// Get the primitive type code for this value
147    pub fn primitive_type(&self) -> Option<PrimitiveTypeCode> {
148        PrimitiveTypeCode::from_type_code(self.type_code)
149    }
150
151    /// Get the string value (canonical representation)
152    pub fn to_string_value(&self) -> String {
153        match &self.value {
154            XmlValueKind::Atomic(atom) => atom.to_string(),
155            XmlValueKind::List { items, .. } => items
156                .iter()
157                .map(|v| v.to_string())
158                .collect::<Vec<_>>()
159                .join(" "),
160            XmlValueKind::Union(inner) => inner.to_string_value(),
161            XmlValueKind::UntypedAtomic(s) => s.clone(),
162        }
163    }
164
165    /// Try to get as boolean
166    pub fn as_boolean(&self) -> Option<bool> {
167        match &self.value {
168            XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) => Some(*b),
169            _ => None,
170        }
171    }
172
173    /// Try to get as string
174    pub fn as_string(&self) -> Option<&str> {
175        match &self.value {
176            XmlValueKind::Atomic(XmlAtomicValue::String(s)) => Some(s),
177            XmlValueKind::UntypedAtomic(s) => Some(s),
178            _ => None,
179        }
180    }
181
182    /// Try to get as decimal
183    pub fn as_decimal(&self) -> Option<Decimal> {
184        match &self.value {
185            XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => Some(*d),
186            XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => {
187                // Try to convert BigInt to Decimal
188                i.to_string().parse().ok()
189            }
190            _ => None,
191        }
192    }
193
194    /// Try to get as integer
195    pub fn as_integer(&self) -> Option<&BigInt> {
196        match &self.value {
197            XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => Some(i),
198            _ => None,
199        }
200    }
201
202    /// Try to get as double
203    pub fn as_double(&self) -> Option<f64> {
204        match &self.value {
205            XmlValueKind::Atomic(XmlAtomicValue::Double(d)) => Some(*d),
206            XmlValueKind::Atomic(XmlAtomicValue::Float(f)) => Some(*f as f64),
207            XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)) => d.to_string().parse().ok(),
208            XmlValueKind::Atomic(XmlAtomicValue::Integer(i)) => i.to_string().parse().ok(),
209            _ => None,
210        }
211    }
212
213    /// Try to get as QName
214    pub fn as_qname(&self) -> Option<&QualifiedName> {
215        match &self.value {
216            XmlValueKind::Atomic(XmlAtomicValue::QName(qn)) => Some(qn),
217            _ => None,
218        }
219    }
220
221    /// Convert this `XmlValue` to an `XPathValue` for use as `$value` in assertion evaluation.
222    ///
223    /// - **Atomic/UntypedAtomic** → single `XPathValue::Item`
224    /// - **List** → `XPathValue::Sequence` of atomic items, each with `item_schema_type`
225    /// - **Union** → recursively converts the inner value
226    ///
227    /// The `item_schema_type` parameter is needed because `XmlValueKind::List` stores bare
228    /// `XmlAtomicValue` items without per-item `schema_type`. Callers pass it from the
229    /// list type's `resolved_item_type`.
230    #[cfg(feature = "xsd11")]
231    pub fn to_xpath_value<N: crate::xpath::DomNavigator>(
232        &self,
233        item_schema_type: Option<SimpleTypeKey>,
234    ) -> crate::xpath::XPathValue<N> {
235        use crate::xpath::iterator::XmlItem;
236        use crate::xpath::XPathValue;
237
238        match &self.value {
239            XmlValueKind::Atomic(_) | XmlValueKind::UntypedAtomic(_) => {
240                XPathValue::from_atomic(self.clone())
241            }
242            XmlValueKind::List { item_type, items } => {
243                let xml_items: Vec<XmlItem<N>> = items
244                    .iter()
245                    .map(|atom| {
246                        let val = XmlValue {
247                            type_code: atom.type_code(),
248                            schema_type: item_schema_type,
249                            value: XmlValueKind::Atomic(atom.clone()),
250                        };
251                        XmlItem::Atomic(val)
252                    })
253                    .collect();
254                let _ = item_type; // item_type already embedded in each atom's type_code
255                XPathValue::from_sequence(xml_items)
256            }
257            XmlValueKind::Union(inner) => inner.to_xpath_value(item_schema_type),
258        }
259    }
260}
261
262/// Value kind discriminant for XmlValue
263#[derive(Debug, Clone, PartialEq)]
264pub enum XmlValueKind {
265    /// A single atomic value
266    Atomic(XmlAtomicValue),
267    /// A list of atomic values (e.g., NMTOKENS)
268    List {
269        /// The type code of list items
270        item_type: XmlTypeCode,
271        /// The list items
272        items: Vec<XmlAtomicValue>,
273    },
274    /// A union value (actual type determined at runtime)
275    Union(Box<XmlValue>),
276    /// An untyped atomic value (raw string)
277    UntypedAtomic(String),
278}
279
280/// Atomic XSD value types
281///
282/// This enum holds the actual parsed values for atomic XSD types.
283/// For complex types like date/time, we use structured representations.
284#[derive(Debug, Clone, PartialEq)]
285pub enum XmlAtomicValue {
286    // String types
287    /// xs:string and derived types
288    String(String),
289
290    // Boolean type
291    /// xs:boolean
292    Boolean(bool),
293
294    // Numeric types
295    /// xs:decimal
296    Decimal(Decimal),
297    /// xs:integer and derived integer types
298    Integer(BigInt),
299    /// xs:float
300    Float(f32),
301    /// xs:double
302    Double(f64),
303
304    // Date/time types
305    /// xs:dateTime
306    DateTime(DateTimeValue),
307    /// xs:date
308    Date(DateValue),
309    /// xs:time
310    Time(TimeValue),
311    /// xs:duration
312    Duration(DurationValue),
313    /// xs:gYearMonth
314    GYearMonth(GYearMonthValue),
315    /// xs:gYear
316    GYear(GYearValue),
317    /// xs:gMonthDay
318    GMonthDay(GMonthDayValue),
319    /// xs:gDay
320    GDay(GDayValue),
321    /// xs:gMonth
322    GMonth(GMonthValue),
323    /// xs:yearMonthDuration (XSD 1.1)
324    YearMonthDuration(YearMonthDurationValue),
325    /// xs:dayTimeDuration (XSD 1.1)
326    DayTimeDuration(DayTimeDurationValue),
327
328    // Binary types
329    /// xs:hexBinary
330    HexBinary(Vec<u8>),
331    /// xs:base64Binary
332    Base64Binary(Vec<u8>),
333
334    // URI type
335    /// xs:anyURI
336    AnyUri(String),
337
338    // QName types (namespace-resolved)
339    /// xs:QName
340    QName(QualifiedName),
341    /// xs:NOTATION
342    Notation(QualifiedName),
343}
344
345impl XmlAtomicValue {
346    /// Get the type code for this atomic value
347    pub fn type_code(&self) -> XmlTypeCode {
348        match self {
349            Self::String(_) => XmlTypeCode::String,
350            Self::Boolean(_) => XmlTypeCode::Boolean,
351            Self::Decimal(_) => XmlTypeCode::Decimal,
352            Self::Integer(_) => XmlTypeCode::Integer,
353            Self::Float(_) => XmlTypeCode::Float,
354            Self::Double(_) => XmlTypeCode::Double,
355            Self::DateTime(_) => XmlTypeCode::DateTime,
356            Self::Date(_) => XmlTypeCode::Date,
357            Self::Time(_) => XmlTypeCode::Time,
358            Self::Duration(_) => XmlTypeCode::Duration,
359            Self::GYearMonth(_) => XmlTypeCode::GYearMonth,
360            Self::GYear(_) => XmlTypeCode::GYear,
361            Self::GMonthDay(_) => XmlTypeCode::GMonthDay,
362            Self::GDay(_) => XmlTypeCode::GDay,
363            Self::GMonth(_) => XmlTypeCode::GMonth,
364            Self::YearMonthDuration(_) => XmlTypeCode::YearMonthDuration,
365            Self::DayTimeDuration(_) => XmlTypeCode::DayTimeDuration,
366            Self::HexBinary(_) => XmlTypeCode::HexBinary,
367            Self::Base64Binary(_) => XmlTypeCode::Base64Binary,
368            Self::AnyUri(_) => XmlTypeCode::AnyUri,
369            Self::QName(_) => XmlTypeCode::QName,
370            Self::Notation(_) => XmlTypeCode::Notation,
371        }
372    }
373
374    /// Get the primitive type code for this atomic value
375    pub fn primitive_type(&self) -> PrimitiveTypeCode {
376        match self {
377            Self::String(_) => PrimitiveTypeCode::String,
378            Self::Boolean(_) => PrimitiveTypeCode::Boolean,
379            Self::Decimal(_) | Self::Integer(_) => PrimitiveTypeCode::Decimal,
380            Self::Float(_) => PrimitiveTypeCode::Float,
381            Self::Double(_) => PrimitiveTypeCode::Double,
382            Self::DateTime(_) => PrimitiveTypeCode::DateTime,
383            Self::Date(_) => PrimitiveTypeCode::Date,
384            Self::Time(_) => PrimitiveTypeCode::Time,
385            Self::Duration(_) | Self::YearMonthDuration(_) | Self::DayTimeDuration(_) => {
386                PrimitiveTypeCode::Duration
387            }
388            Self::GYearMonth(_) => PrimitiveTypeCode::GYearMonth,
389            Self::GYear(_) => PrimitiveTypeCode::GYear,
390            Self::GMonthDay(_) => PrimitiveTypeCode::GMonthDay,
391            Self::GDay(_) => PrimitiveTypeCode::GDay,
392            Self::GMonth(_) => PrimitiveTypeCode::GMonth,
393            Self::HexBinary(_) => PrimitiveTypeCode::HexBinary,
394            Self::Base64Binary(_) => PrimitiveTypeCode::Base64Binary,
395            Self::AnyUri(_) => PrimitiveTypeCode::AnyUri,
396            Self::QName(_) => PrimitiveTypeCode::QName,
397            Self::Notation(_) => PrimitiveTypeCode::Notation,
398        }
399    }
400}
401
402impl fmt::Display for XmlAtomicValue {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            Self::String(s) => write!(f, "{}", s),
406            Self::Boolean(b) => write!(f, "{}", if *b { "true" } else { "false" }),
407            Self::Decimal(d) => {
408                // XSD canonical form: no trailing zeros for integers
409                if d.fract().is_zero() {
410                    write!(f, "{}", d.trunc())
411                } else {
412                    write!(f, "{}", d.normalize())
413                }
414            }
415            Self::Integer(i) => write!(f, "{}", i),
416            Self::Float(v) => format_float(*v, f),
417            Self::Double(v) => format_double(*v, f),
418            Self::DateTime(dt) => write!(f, "{}", dt),
419            Self::Date(d) => write!(f, "{}", d),
420            Self::Time(t) => write!(f, "{}", t),
421            Self::Duration(d) => write!(f, "{}", d),
422            Self::GYearMonth(v) => write!(f, "{}", v),
423            Self::GYear(v) => write!(f, "{}", v),
424            Self::GMonthDay(v) => write!(f, "{}", v),
425            Self::GDay(v) => write!(f, "{}", v),
426            Self::GMonth(v) => write!(f, "{}", v),
427            Self::YearMonthDuration(v) => write!(f, "{}", v),
428            Self::DayTimeDuration(v) => write!(f, "{}", v),
429            Self::HexBinary(bytes) => {
430                write!(f, "{}", hex::encode_upper(bytes))
431            }
432            Self::Base64Binary(bytes) => {
433                use base64::Engine;
434                write!(
435                    f,
436                    "{}",
437                    base64::engine::general_purpose::STANDARD.encode(bytes)
438                )
439            }
440            Self::AnyUri(uri) => write!(f, "{}", uri),
441            Self::QName(qn) => {
442                // Display with prefix if available
443                write!(f, "QName({:?}:{})", qn.namespace_uri, qn.local_name.0)
444            }
445            Self::Notation(n) => {
446                write!(f, "NOTATION({:?}:{})", n.namespace_uri, n.local_name.0)
447            }
448        }
449    }
450}
451
452/// Format a scientific notation string to ensure the mantissa has a decimal point.
453///
454/// Rust's `{:E}` may produce `1E7` or `-1E7`; XPath 2.0 requires `1.0E7` or `-1.0E7`.
455fn fix_scientific_notation(s: &str) -> String {
456    // Find the position of 'E'
457    if let Some(e_pos) = s.find('E') {
458        let mantissa = &s[..e_pos];
459        let exponent = &s[e_pos..]; // includes 'E'
460        if !mantissa.contains('.') {
461            format!("{}.0{}", mantissa, exponent)
462        } else {
463            s.to_string()
464        }
465    } else {
466        s.to_string()
467    }
468}
469
470/// Format float according to XSD canonical representation
471fn format_float(v: f32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472    if v.is_nan() {
473        write!(f, "NaN")
474    } else if v.is_infinite() {
475        if v.is_sign_positive() {
476            write!(f, "INF")
477        } else {
478            write!(f, "-INF")
479        }
480    } else if v == 0.0 {
481        // Per XPath 2.0: negative zero serializes as "-0", positive zero as "0"
482        if v.is_sign_negative() {
483            write!(f, "-0")
484        } else {
485            write!(f, "0")
486        }
487    } else if v.abs() >= 1e-6 && v.abs() < 1e6 {
488        // Use regular notation for values in this range
489        write!(f, "{}", v)
490    } else {
491        // Use scientific notation with guaranteed decimal point in mantissa
492        let s = format!("{:E}", v);
493        write!(f, "{}", fix_scientific_notation(&s))
494    }
495}
496
497/// Format double according to XSD canonical representation
498fn format_double(v: f64, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499    if v.is_nan() {
500        write!(f, "NaN")
501    } else if v.is_infinite() {
502        if v.is_sign_positive() {
503            write!(f, "INF")
504        } else {
505            write!(f, "-INF")
506        }
507    } else if v == 0.0 {
508        // Per XPath 2.0: negative zero serializes as "-0", positive zero as "0"
509        if v.is_sign_negative() {
510            write!(f, "-0")
511        } else {
512            write!(f, "0")
513        }
514    } else if v.abs() >= 1e-6 && v.abs() < 1e6 {
515        // Use regular notation for values in this range
516        write!(f, "{}", v)
517    } else {
518        // Use scientific notation with guaranteed decimal point in mantissa
519        let s = format!("{:E}", v);
520        write!(f, "{}", fix_scientific_notation(&s))
521    }
522}
523
524// ============================================================================
525// Date/Time Value Types
526// ============================================================================
527
528/// Format a year value according to XPath 2.0 rules.
529///
530/// Negative years must be formatted as sign + 4-digit year (e.g., -12 → "-0012").
531/// Positive years use standard 4-digit zero-padded format.
532fn format_year(year: i32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533    if year < 0 {
534        write!(f, "-{:04}", -year)
535    } else {
536        write!(f, "{:04}", year)
537    }
538}
539
540/// xs:dateTime value
541#[derive(Debug, Clone, PartialEq)]
542pub struct DateTimeValue {
543    pub year: i32,
544    pub month: u8,
545    pub day: u8,
546    pub hour: u8,
547    pub minute: u8,
548    pub second: Decimal,
549    pub timezone: Option<TimezoneOffset>,
550}
551
552impl DateTimeValue {
553    /// Convert to total seconds from a reference epoch for comparison.
554    /// Uses implicit UTC for timezone-unaware values (XSD 1.0 facet semantics).
555    fn to_comparable_instant(&self) -> Decimal {
556        let tz_minutes = self.timezone.map_or(0i64, |tz| tz.0 as i64);
557        let days = date_to_days(self.year, self.month as i32, self.day as i32);
558        Decimal::from(days) * Decimal::from(86400)
559            + Decimal::from(self.hour as i64) * Decimal::from(3600)
560            + Decimal::from(self.minute as i64) * Decimal::from(60)
561            + self.second
562            - Decimal::from(tz_minutes) * Decimal::from(60)
563    }
564}
565
566impl PartialOrd for DateTimeValue {
567    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
568        self.to_comparable_instant()
569            .partial_cmp(&other.to_comparable_instant())
570    }
571}
572
573impl fmt::Display for DateTimeValue {
574    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575        format_year(self.year, f)?;
576        write!(
577            f,
578            "-{:02}-{:02}T{:02}:{:02}:",
579            self.month, self.day, self.hour, self.minute
580        )?;
581        format_seconds(self.second, f)?;
582        if let Some(tz) = &self.timezone {
583            write!(f, "{}", tz)?;
584        }
585        Ok(())
586    }
587}
588
589/// xs:date value
590#[derive(Debug, Clone, PartialEq)]
591pub struct DateValue {
592    pub year: i32,
593    pub month: u8,
594    pub day: u8,
595    pub timezone: Option<TimezoneOffset>,
596}
597
598impl DateValue {
599    fn to_comparable_instant(&self) -> Decimal {
600        let tz_minutes = self.timezone.map_or(0i64, |tz| tz.0 as i64);
601        let days = date_to_days(self.year, self.month as i32, self.day as i32);
602        Decimal::from(days) * Decimal::from(86400) - Decimal::from(tz_minutes) * Decimal::from(60)
603    }
604}
605
606impl PartialOrd for DateValue {
607    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
608        self.to_comparable_instant()
609            .partial_cmp(&other.to_comparable_instant())
610    }
611}
612
613impl fmt::Display for DateValue {
614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
615        format_year(self.year, f)?;
616        write!(f, "-{:02}-{:02}", self.month, self.day)?;
617        if let Some(tz) = &self.timezone {
618            write!(f, "{}", tz)?;
619        }
620        Ok(())
621    }
622}
623
624/// xs:time value
625#[derive(Debug, Clone, PartialEq)]
626pub struct TimeValue {
627    pub hour: u8,
628    pub minute: u8,
629    pub second: Decimal,
630    pub timezone: Option<TimezoneOffset>,
631}
632
633impl TimeValue {
634    fn to_comparable_seconds(&self) -> Decimal {
635        let tz_minutes = self.timezone.map_or(0i64, |tz| tz.0 as i64);
636        Decimal::from(self.hour as i64) * Decimal::from(3600)
637            + Decimal::from(self.minute as i64) * Decimal::from(60)
638            + self.second
639            - Decimal::from(tz_minutes) * Decimal::from(60)
640    }
641}
642
643impl PartialOrd for TimeValue {
644    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
645        self.to_comparable_seconds()
646            .partial_cmp(&other.to_comparable_seconds())
647    }
648}
649
650impl fmt::Display for TimeValue {
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        write!(f, "{:02}:{:02}:", self.hour, self.minute)?;
653        format_seconds(self.second, f)?;
654        if let Some(tz) = &self.timezone {
655            write!(f, "{}", tz)?;
656        }
657        Ok(())
658    }
659}
660
661/// xs:duration value
662#[derive(Debug, Clone, PartialEq)]
663pub struct DurationValue {
664    pub negative: bool,
665    pub years: u32,
666    pub months: u32,
667    pub days: u32,
668    pub hours: u32,
669    pub minutes: u32,
670    pub seconds: Decimal,
671}
672
673impl DurationValue {
674    /// Convert duration to approximate total seconds for comparison.
675    /// Uses average Gregorian month length (365.2425 / 12 = 30.436875 days).
676    /// This provides a total order consistent with the NIST conformance test
677    /// suite expectations. Strict XSD 1.0 §3.2.6.2 uses a partial order via
678    /// 4 reference dates, but most practical implementations use a total order.
679    fn to_approx_total_seconds(&self) -> Decimal {
680        // Average month in seconds: 30.436875 * 86400 = 2629746
681        let month_secs = Decimal::from(2629746i64);
682        let total_months = Decimal::from(self.years as i64) * Decimal::from(12)
683            + Decimal::from(self.months as i64);
684        let day_time_secs = Decimal::from(self.days as i64) * Decimal::from(86400)
685            + Decimal::from(self.hours as i64) * Decimal::from(3600)
686            + Decimal::from(self.minutes as i64) * Decimal::from(60)
687            + self.seconds;
688        let total = total_months * month_secs + day_time_secs;
689        if self.negative {
690            -total
691        } else {
692            total
693        }
694    }
695}
696
697impl PartialOrd for DurationValue {
698    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
699        self.to_approx_total_seconds()
700            .partial_cmp(&other.to_approx_total_seconds())
701    }
702}
703
704impl fmt::Display for DurationValue {
705    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
706        // Normalize year-month part
707        let total_months = self.years * 12 + self.months;
708        let years = total_months / 12;
709        let months = total_months % 12;
710
711        // Normalize day-time part
712        let (days, hours, minutes, seconds) =
713            normalize_day_time(self.days, self.hours, self.minutes, self.seconds);
714
715        if self.negative {
716            write!(f, "-")?;
717        }
718        write!(f, "P")?;
719        if years > 0 {
720            write!(f, "{}Y", years)?;
721        }
722        if months > 0 {
723            write!(f, "{}M", months)?;
724        }
725        if days > 0 {
726            write!(f, "{}D", days)?;
727        }
728        if hours > 0 || minutes > 0 || !seconds.is_zero() {
729            write!(f, "T")?;
730            if hours > 0 {
731                write!(f, "{}H", hours)?;
732            }
733            if minutes > 0 {
734                write!(f, "{}M", minutes)?;
735            }
736            if !seconds.is_zero() {
737                format_duration_seconds(seconds, f)?;
738                write!(f, "S")?;
739            }
740        }
741        // Handle zero duration
742        if years == 0 && months == 0 && days == 0 && hours == 0 && minutes == 0 && seconds.is_zero()
743        {
744            write!(f, "T0S")?;
745        }
746        Ok(())
747    }
748}
749
750/// xs:yearMonthDuration (XSD 1.1)
751#[derive(Debug, Clone, PartialEq)]
752pub struct YearMonthDurationValue {
753    pub negative: bool,
754    pub years: u32,
755    pub months: u32,
756}
757
758impl fmt::Display for YearMonthDurationValue {
759    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
760        // Normalize months → years + months
761        let total_months = self.years * 12 + self.months;
762        let years = total_months / 12;
763        let months = total_months % 12;
764
765        // Negative zero is normalized to positive zero
766        if self.negative && (years > 0 || months > 0) {
767            write!(f, "-")?;
768        }
769        write!(f, "P")?;
770        if years > 0 {
771            write!(f, "{}Y", years)?;
772        }
773        if months > 0 {
774            write!(f, "{}M", months)?;
775        }
776        if years == 0 && months == 0 {
777            write!(f, "0M")?;
778        }
779        Ok(())
780    }
781}
782
783impl PartialOrd for YearMonthDurationValue {
784    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
785        let self_months = if self.negative {
786            -(self.years as i64 * 12 + self.months as i64)
787        } else {
788            self.years as i64 * 12 + self.months as i64
789        };
790        let other_months = if other.negative {
791            -(other.years as i64 * 12 + other.months as i64)
792        } else {
793            other.years as i64 * 12 + other.months as i64
794        };
795        self_months.partial_cmp(&other_months)
796    }
797}
798
799/// xs:dayTimeDuration (XSD 1.1)
800#[derive(Debug, Clone, PartialEq)]
801pub struct DayTimeDurationValue {
802    pub negative: bool,
803    pub days: u32,
804    pub hours: u32,
805    pub minutes: u32,
806    pub seconds: Decimal,
807}
808
809impl fmt::Display for DayTimeDurationValue {
810    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
811        // Normalize seconds → minutes → hours → days
812        let (days, hours, minutes, seconds) =
813            normalize_day_time(self.days, self.hours, self.minutes, self.seconds);
814
815        // Negative zero is normalized to positive zero
816        if self.negative && (days > 0 || hours > 0 || minutes > 0 || !seconds.is_zero()) {
817            write!(f, "-")?;
818        }
819        write!(f, "P")?;
820        if days > 0 {
821            write!(f, "{}D", days)?;
822        }
823        if hours > 0 || minutes > 0 || !seconds.is_zero() {
824            write!(f, "T")?;
825            if hours > 0 {
826                write!(f, "{}H", hours)?;
827            }
828            if minutes > 0 {
829                write!(f, "{}M", minutes)?;
830            }
831            if !seconds.is_zero() {
832                format_duration_seconds(seconds, f)?;
833                write!(f, "S")?;
834            }
835        }
836        if days == 0 && hours == 0 && minutes == 0 && seconds.is_zero() {
837            write!(f, "T0S")?;
838        }
839        Ok(())
840    }
841}
842
843impl PartialOrd for DayTimeDurationValue {
844    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
845        let self_secs = {
846            let s = Decimal::from(self.days as i64) * Decimal::from(86400i64)
847                + Decimal::from(self.hours as i64) * Decimal::from(3600i64)
848                + Decimal::from(self.minutes as i64) * Decimal::from(60i64)
849                + self.seconds;
850            if self.negative {
851                -s
852            } else {
853                s
854            }
855        };
856        let other_secs = {
857            let s = Decimal::from(other.days as i64) * Decimal::from(86400i64)
858                + Decimal::from(other.hours as i64) * Decimal::from(3600i64)
859                + Decimal::from(other.minutes as i64) * Decimal::from(60i64)
860                + other.seconds;
861            if other.negative {
862                -s
863            } else {
864                s
865            }
866        };
867        self_secs.partial_cmp(&other_secs)
868    }
869}
870
871/// xs:gYearMonth value
872#[derive(Debug, Clone, PartialEq)]
873pub struct GYearMonthValue {
874    pub year: i32,
875    pub month: u8,
876    pub timezone: Option<TimezoneOffset>,
877}
878
879impl PartialOrd for GYearMonthValue {
880    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
881        let tz1 = self.timezone.map_or(0i64, |tz| tz.0 as i64);
882        let tz2 = other.timezone.map_or(0i64, |tz| tz.0 as i64);
883        let d1 = date_to_days(self.year, self.month as i32, 1) * 1440 - tz1;
884        let d2 = date_to_days(other.year, other.month as i32, 1) * 1440 - tz2;
885        d1.partial_cmp(&d2)
886    }
887}
888
889impl fmt::Display for GYearMonthValue {
890    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
891        format_year(self.year, f)?;
892        write!(f, "-{:02}", self.month)?;
893        if let Some(tz) = &self.timezone {
894            write!(f, "{}", tz)?;
895        }
896        Ok(())
897    }
898}
899
900/// xs:gYear value
901#[derive(Debug, Clone, PartialEq)]
902pub struct GYearValue {
903    pub year: i32,
904    pub timezone: Option<TimezoneOffset>,
905}
906
907impl PartialOrd for GYearValue {
908    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
909        let tz1 = self.timezone.map_or(0i64, |tz| tz.0 as i64);
910        let tz2 = other.timezone.map_or(0i64, |tz| tz.0 as i64);
911        let d1 = date_to_days(self.year, 1, 1) * 1440 - tz1;
912        let d2 = date_to_days(other.year, 1, 1) * 1440 - tz2;
913        d1.partial_cmp(&d2)
914    }
915}
916
917impl fmt::Display for GYearValue {
918    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919        format_year(self.year, f)?;
920        if let Some(tz) = &self.timezone {
921            write!(f, "{}", tz)?;
922        }
923        Ok(())
924    }
925}
926
927/// xs:gMonthDay value
928#[derive(Debug, Clone, PartialEq)]
929pub struct GMonthDayValue {
930    pub month: u8,
931    pub day: u8,
932    pub timezone: Option<TimezoneOffset>,
933}
934
935impl PartialOrd for GMonthDayValue {
936    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
937        let tz1 = self.timezone.map_or(0i64, |tz| tz.0 as i64);
938        let tz2 = other.timezone.map_or(0i64, |tz| tz.0 as i64);
939        // Use reference year 2000 for gMonthDay comparison
940        let d1 = date_to_days(2000, self.month as i32, self.day as i32) * 1440 - tz1;
941        let d2 = date_to_days(2000, other.month as i32, other.day as i32) * 1440 - tz2;
942        d1.partial_cmp(&d2)
943    }
944}
945
946impl fmt::Display for GMonthDayValue {
947    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948        write!(f, "--{:02}-{:02}", self.month, self.day)?;
949        if let Some(tz) = &self.timezone {
950            write!(f, "{}", tz)?;
951        }
952        Ok(())
953    }
954}
955
956/// xs:gDay value
957#[derive(Debug, Clone, PartialEq)]
958pub struct GDayValue {
959    pub day: u8,
960    pub timezone: Option<TimezoneOffset>,
961}
962
963impl PartialOrd for GDayValue {
964    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
965        let tz1 = self.timezone.map_or(0i64, |tz| tz.0 as i64);
966        let tz2 = other.timezone.map_or(0i64, |tz| tz.0 as i64);
967        let d1 = (self.day as i64) * 1440 - tz1;
968        let d2 = (other.day as i64) * 1440 - tz2;
969        d1.partial_cmp(&d2)
970    }
971}
972
973impl fmt::Display for GDayValue {
974    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
975        write!(f, "---{:02}", self.day)?;
976        if let Some(tz) = &self.timezone {
977            write!(f, "{}", tz)?;
978        }
979        Ok(())
980    }
981}
982
983/// xs:gMonth value
984#[derive(Debug, Clone, PartialEq)]
985pub struct GMonthValue {
986    pub month: u8,
987    pub timezone: Option<TimezoneOffset>,
988}
989
990impl PartialOrd for GMonthValue {
991    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
992        let tz1 = self.timezone.map_or(0i64, |tz| tz.0 as i64);
993        let tz2 = other.timezone.map_or(0i64, |tz| tz.0 as i64);
994        let d1 = (self.month as i64) * 1440 - tz1;
995        let d2 = (other.month as i64) * 1440 - tz2;
996        d1.partial_cmp(&d2)
997    }
998}
999
1000impl fmt::Display for GMonthValue {
1001    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1002        write!(f, "--{:02}", self.month)?;
1003        if let Some(tz) = &self.timezone {
1004            write!(f, "{}", tz)?;
1005        }
1006        Ok(())
1007    }
1008}
1009
1010/// Timezone offset in minutes from UTC
1011#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1012pub struct TimezoneOffset(pub i16);
1013
1014impl TimezoneOffset {
1015    /// UTC timezone
1016    pub const UTC: Self = Self(0);
1017
1018    /// Create a timezone offset from hours and minutes
1019    pub fn from_hm(hours: i8, minutes: i8) -> Self {
1020        Self(hours as i16 * 60 + minutes as i16)
1021    }
1022
1023    /// Get hours component
1024    pub fn hours(&self) -> i8 {
1025        (self.0 / 60) as i8
1026    }
1027
1028    /// Get minutes component
1029    pub fn minutes(&self) -> i8 {
1030        (self.0 % 60).abs() as i8
1031    }
1032}
1033
1034impl fmt::Display for TimezoneOffset {
1035    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1036        if self.0 == 0 {
1037            write!(f, "Z")
1038        } else {
1039            let sign = if self.0 > 0 { '+' } else { '-' };
1040            let hours = (self.0.abs() / 60) as u8;
1041            let minutes = (self.0.abs() % 60) as u8;
1042            write!(f, "{}{:02}:{:02}", sign, hours, minutes)
1043        }
1044    }
1045}
1046
1047/// Normalize day-time duration components.
1048/// Convert a date to total days from a reference epoch (year 1, month 1, day 1).
1049/// Used for comparing date/time values.
1050fn date_to_days(year: i32, month: i32, day: i32) -> i64 {
1051    // Adjust for months < 3 by treating Jan/Feb as months 13/14 of the previous year
1052    let (y, m) = if month <= 2 {
1053        (year as i64 - 1, month as i64 + 12)
1054    } else {
1055        (year as i64, month as i64)
1056    };
1057    // Use a simplified Julian day calculation
1058    365 * y + y / 4 - y / 100 + y / 400 + (153 * (m - 3) + 2) / 5 + day as i64 - 307
1059}
1060
1061///
1062/// Carries over whole seconds into minutes, minutes into hours, hours into days.
1063/// Only the integer part of seconds is carried; the fractional part stays in seconds.
1064fn normalize_day_time(
1065    days: u32,
1066    hours: u32,
1067    minutes: u32,
1068    seconds: Decimal,
1069) -> (u32, u32, u32, Decimal) {
1070    let whole_secs = seconds.trunc();
1071    let frac_secs = seconds - whole_secs;
1072
1073    let total_secs: u64 = whole_secs.to_u64().unwrap_or(0);
1074    let mut mins = minutes as u64 + total_secs / 60;
1075    let rem_secs = (total_secs % 60) as u32;
1076    let mut hrs = hours as u64 + mins / 60;
1077    mins %= 60;
1078    let d = days as u64 + hrs / 24;
1079    hrs %= 24;
1080
1081    let out_seconds = Decimal::from(rem_secs) + frac_secs;
1082    (d as u32, hrs as u32, mins as u32, out_seconds)
1083}
1084
1085/// Format seconds with optional fractional part (zero-padded for time-of-day: HH:MM:SS)
1086fn format_seconds(s: Decimal, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1087    if s.fract().is_zero() {
1088        write!(f, "{:02}", s.trunc())
1089    } else {
1090        // Format with fractional seconds, trimming trailing zeros
1091        let formatted = format!("{}", s);
1092        if let Some(dot_pos) = formatted.find('.') {
1093            let int_part = &formatted[..dot_pos];
1094            let frac_part = formatted[dot_pos + 1..].trim_end_matches('0');
1095            if int_part.len() == 1 {
1096                write!(f, "0{}.{}", int_part, frac_part)
1097            } else {
1098                write!(f, "{}.{}", int_part, frac_part)
1099            }
1100        } else {
1101            write!(f, "{:02}", s)
1102        }
1103    }
1104}
1105
1106/// Format seconds for duration values (no zero-padding)
1107fn format_duration_seconds(s: Decimal, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1108    if s.fract().is_zero() {
1109        write!(f, "{}", s.trunc())
1110    } else {
1111        let formatted = format!("{}", s);
1112        if let Some(dot_pos) = formatted.find('.') {
1113            let int_part = &formatted[..dot_pos];
1114            let frac_part = formatted[dot_pos + 1..].trim_end_matches('0');
1115            write!(f, "{}.{}", int_part, frac_part)
1116        } else {
1117            write!(f, "{}", s)
1118        }
1119    }
1120}
1121
1122// ============================================================================
1123// Tests
1124// ============================================================================
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129
1130    #[test]
1131    fn test_xml_value_string() {
1132        let v = XmlValue::string("hello");
1133        assert_eq!(v.type_code, XmlTypeCode::String);
1134        assert!(v.is_atomic());
1135        assert_eq!(v.to_string_value(), "hello");
1136        assert_eq!(v.as_string(), Some("hello"));
1137    }
1138
1139    #[test]
1140    fn test_xml_value_boolean() {
1141        let v = XmlValue::boolean(true);
1142        assert_eq!(v.type_code, XmlTypeCode::Boolean);
1143        assert_eq!(v.as_boolean(), Some(true));
1144        assert_eq!(v.to_string_value(), "true");
1145
1146        let v = XmlValue::boolean(false);
1147        assert_eq!(v.to_string_value(), "false");
1148    }
1149
1150    #[test]
1151    fn test_xml_value_decimal() {
1152        let v = XmlValue::decimal(Decimal::new(12345, 2));
1153        assert_eq!(v.type_code, XmlTypeCode::Decimal);
1154        assert_eq!(v.as_decimal(), Some(Decimal::new(12345, 2)));
1155    }
1156
1157    #[test]
1158    fn test_xml_value_integer() {
1159        let v = XmlValue::integer(BigInt::from(42));
1160        assert_eq!(v.type_code, XmlTypeCode::Integer);
1161        assert_eq!(v.as_integer(), Some(&BigInt::from(42)));
1162        assert_eq!(v.to_string_value(), "42");
1163    }
1164
1165    #[test]
1166    fn test_xml_value_double() {
1167        let v = XmlValue::double(2.5);
1168        assert_eq!(v.type_code, XmlTypeCode::Double);
1169        assert_eq!(v.as_double(), Some(2.5));
1170    }
1171
1172    #[test]
1173    fn test_xml_value_untyped() {
1174        let v = XmlValue::untyped("raw text");
1175        assert_eq!(v.type_code, XmlTypeCode::UntypedAtomic);
1176        assert!(v.is_untyped());
1177        assert_eq!(v.as_string(), Some("raw text"));
1178    }
1179
1180    #[test]
1181    fn test_xml_atomic_value_type_code() {
1182        assert_eq!(
1183            XmlAtomicValue::String("test".into()).type_code(),
1184            XmlTypeCode::String
1185        );
1186        assert_eq!(
1187            XmlAtomicValue::Boolean(true).type_code(),
1188            XmlTypeCode::Boolean
1189        );
1190        assert_eq!(
1191            XmlAtomicValue::Integer(BigInt::from(1)).type_code(),
1192            XmlTypeCode::Integer
1193        );
1194    }
1195
1196    #[test]
1197    fn test_timezone_display() {
1198        assert_eq!(TimezoneOffset::UTC.to_string(), "Z");
1199        assert_eq!(TimezoneOffset::from_hm(5, 30).to_string(), "+05:30");
1200        assert_eq!(TimezoneOffset::from_hm(-8, 0).to_string(), "-08:00");
1201    }
1202
1203    #[test]
1204    fn test_date_display() {
1205        let d = DateValue {
1206            year: 2024,
1207            month: 3,
1208            day: 15,
1209            timezone: Some(TimezoneOffset::UTC),
1210        };
1211        assert_eq!(d.to_string(), "2024-03-15Z");
1212    }
1213
1214    #[test]
1215    fn test_duration_display() {
1216        let d = DurationValue {
1217            negative: false,
1218            years: 1,
1219            months: 2,
1220            days: 3,
1221            hours: 4,
1222            minutes: 5,
1223            seconds: Decimal::new(65, 1), // 6.5 seconds
1224        };
1225        // Note: format_seconds may zero-pad to 2 digits
1226        assert!(d.to_string().starts_with("P1Y2M3DT4H5M"));
1227        assert!(d.to_string().contains("6.5S"));
1228    }
1229
1230    #[test]
1231    fn test_float_special_values() {
1232        assert_eq!(format!("{}", XmlAtomicValue::Float(f32::INFINITY)), "INF");
1233        assert_eq!(
1234            format!("{}", XmlAtomicValue::Float(f32::NEG_INFINITY)),
1235            "-INF"
1236        );
1237        assert_eq!(format!("{}", XmlAtomicValue::Float(f32::NAN)), "NaN");
1238    }
1239
1240    #[test]
1241    fn test_hex_binary_display() {
1242        let v = XmlAtomicValue::HexBinary(vec![0xDE, 0xAD, 0xBE, 0xEF]);
1243        assert_eq!(format!("{}", v), "DEADBEEF");
1244    }
1245
1246    #[test]
1247    fn test_base64_binary_display() {
1248        let v = XmlAtomicValue::Base64Binary(b"Hello".to_vec());
1249        assert_eq!(format!("{}", v), "SGVsbG8=");
1250    }
1251}