1use chrono::Local;
13use num_bigint::BigInt;
14use rust_decimal::prelude::ToPrimitive;
15use rust_decimal::Decimal;
16
17use crate::types::value::{
18 DateTimeValue, DateValue, DayTimeDurationValue, DurationValue, TimeValue, TimezoneOffset,
19 XmlAtomicValue, XmlValue, XmlValueKind, YearMonthDurationValue,
20};
21use crate::types::XmlTypeCode;
22use crate::xpath::context::DynamicContext;
23use crate::xpath::error::XPathError;
24use crate::xpath::DomNavigator;
25
26use super::{atomize_to_single_opt, XPathValue};
27
28fn get_implicit_timezone_offset() -> TimezoneOffset {
34 let seconds = Local::now().offset().local_minus_utc();
35 TimezoneOffset((seconds / 60) as i16)
36}
37
38fn timezone_to_day_time_duration(tz: TimezoneOffset) -> DayTimeDurationValue {
40 let total_minutes = tz.0.unsigned_abs() as u32;
41 let hours = total_minutes / 60;
42 let minutes = total_minutes % 60;
43 DayTimeDurationValue {
44 negative: tz.0 < 0,
45 days: 0,
46 hours,
47 minutes,
48 seconds: Decimal::ZERO,
49 }
50}
51
52fn day_time_duration_to_timezone(dur: &DayTimeDurationValue) -> Result<TimezoneOffset, XPathError> {
58 if dur.days != 0 {
63 return Err(XPathError::FODT0003 {
64 value: format_day_time_duration(dur),
65 });
66 }
67
68 if dur.seconds != Decimal::ZERO {
70 return Err(XPathError::FODT0003 {
71 value: format_day_time_duration(dur),
72 });
73 }
74
75 let total_minutes = dur.hours as i64 * 60 + dur.minutes as i64;
77 let signed_minutes = if dur.negative {
78 -total_minutes
79 } else {
80 total_minutes
81 };
82
83 if !(-840..=840).contains(&signed_minutes) {
85 return Err(XPathError::FODT0003 {
86 value: format_day_time_duration(dur),
87 });
88 }
89
90 Ok(TimezoneOffset(signed_minutes as i16))
91}
92
93fn format_day_time_duration(dur: &DayTimeDurationValue) -> String {
95 let mut s = String::new();
96 if dur.negative {
97 s.push('-');
98 }
99 s.push_str("PT");
100 if dur.days != 0 {
101 s.push_str(&format!("{}D", dur.days));
102 }
103 if dur.hours != 0 {
104 s.push_str(&format!("{}H", dur.hours));
105 }
106 if dur.minutes != 0 {
107 s.push_str(&format!("{}M", dur.minutes));
108 }
109 if dur.seconds != Decimal::ZERO {
110 s.push_str(&format!("{}S", dur.seconds));
111 }
112 if s.len() == 2 || (s.len() == 3 && s.starts_with('-')) {
113 s.push_str("0S");
114 }
115 s
116}
117
118fn validate_timezone_offset(minutes: i16) -> Result<(), XPathError> {
120 if !(-840..=840).contains(&minutes) {
121 return Err(XPathError::FODT0003 {
122 value: format!("{}:{:02}", minutes / 60, (minutes % 60).abs()),
123 });
124 }
125 Ok(())
126}
127
128fn as_datetime(value: &XmlValue) -> Option<&DateTimeValue> {
130 match &value.value {
131 XmlValueKind::Atomic(XmlAtomicValue::DateTime(v)) => Some(v),
132 _ => None,
133 }
134}
135
136fn as_date(value: &XmlValue) -> Option<&DateValue> {
138 match &value.value {
139 XmlValueKind::Atomic(XmlAtomicValue::Date(v)) => Some(v),
140 _ => None,
141 }
142}
143
144fn as_time(value: &XmlValue) -> Option<&TimeValue> {
146 match &value.value {
147 XmlValueKind::Atomic(XmlAtomicValue::Time(v)) => Some(v),
148 _ => None,
149 }
150}
151
152fn as_duration(value: &XmlValue) -> Option<&DurationValue> {
154 match &value.value {
155 XmlValueKind::Atomic(XmlAtomicValue::Duration(v)) => Some(v),
156 _ => None,
157 }
158}
159
160fn as_year_month_duration(value: &XmlValue) -> Option<&YearMonthDurationValue> {
162 match &value.value {
163 XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(v)) => Some(v),
164 _ => None,
165 }
166}
167
168fn as_day_time_duration(value: &XmlValue) -> Option<&DayTimeDurationValue> {
170 match &value.value {
171 XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(v)) => Some(v),
172 _ => None,
173 }
174}
175
176fn is_duration_type(code: XmlTypeCode) -> bool {
178 matches!(
179 code,
180 XmlTypeCode::Duration | XmlTypeCode::YearMonthDuration | XmlTypeCode::DayTimeDuration
181 )
182}
183
184fn xml_integer(i: i64) -> XmlValue {
186 XmlValue {
187 type_code: XmlTypeCode::Integer,
188 schema_type: None,
189 value: XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(i))),
190 }
191}
192
193fn xml_decimal(d: Decimal) -> XmlValue {
195 XmlValue {
196 type_code: XmlTypeCode::Decimal,
197 schema_type: None,
198 value: XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)),
199 }
200}
201
202fn xml_day_time_duration(value: DayTimeDurationValue) -> XmlValue {
204 XmlValue {
205 type_code: XmlTypeCode::DayTimeDuration,
206 schema_type: None,
207 value: XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(value)),
208 }
209}
210
211fn xml_datetime(value: DateTimeValue) -> XmlValue {
213 XmlValue {
214 type_code: XmlTypeCode::DateTime,
215 schema_type: None,
216 value: XmlValueKind::Atomic(XmlAtomicValue::DateTime(value)),
217 }
218}
219
220fn xml_date(value: DateValue) -> XmlValue {
222 XmlValue {
223 type_code: XmlTypeCode::Date,
224 schema_type: None,
225 value: XmlValueKind::Atomic(XmlAtomicValue::Date(value)),
226 }
227}
228
229fn xml_time(value: TimeValue) -> XmlValue {
231 XmlValue {
232 type_code: XmlTypeCode::Time,
233 schema_type: None,
234 value: XmlValueKind::Atomic(XmlAtomicValue::Time(value)),
235 }
236}
237
238pub fn current_datetime<N: DomNavigator>(
248 context: &mut DynamicContext<'_, N>,
249 args: Vec<XPathValue<N>>,
250) -> Result<XPathValue<N>, XPathError> {
251 if !args.is_empty() {
252 return Err(XPathError::wrong_number_of_arguments(
253 "current-dateTime",
254 0,
255 args.len(),
256 ));
257 }
258
259 let dt = if let Some(ref cached) = context.current_datetime {
261 cached.clone()
262 } else {
263 let now = Local::now();
264
265 let tz_offset = context
267 .implicit_timezone
268 .unwrap_or_else(get_implicit_timezone_offset);
269
270 let secs = now.format("%S").to_string().parse::<u32>().unwrap_or(0);
273 let nanos = now.timestamp_subsec_nanos();
274 let second = Decimal::from(secs) + Decimal::from(nanos) / Decimal::from(1_000_000_000u64);
276
277 let dt = DateTimeValue {
278 year: now.format("%Y").to_string().parse().unwrap_or(2000),
279 month: now.format("%m").to_string().parse().unwrap_or(1),
280 day: now.format("%d").to_string().parse().unwrap_or(1),
281 hour: now.format("%H").to_string().parse().unwrap_or(0),
282 minute: now.format("%M").to_string().parse().unwrap_or(0),
283 second,
284 timezone: Some(tz_offset),
285 };
286 context.current_datetime = Some(dt.clone());
287 dt
288 };
289
290 Ok(XPathValue::from_atomic(xml_datetime(dt)))
291}
292
293pub fn current_date<N: DomNavigator>(
295 context: &mut DynamicContext<'_, N>,
296 args: Vec<XPathValue<N>>,
297) -> Result<XPathValue<N>, XPathError> {
298 if !args.is_empty() {
299 return Err(XPathError::wrong_number_of_arguments(
300 "current-date",
301 0,
302 args.len(),
303 ));
304 }
305
306 let dt_value = current_datetime(context, vec![])?;
308 let dt = match dt_value {
309 XPathValue::Item(item) => {
310 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
311 as_datetime(&v).cloned()
312 } else {
313 None
314 }
315 }
316 _ => None,
317 };
318
319 let dt = dt.ok_or_else(|| XPathError::internal("Failed to get current dateTime"))?;
320
321 let date = DateValue {
322 year: dt.year,
323 month: dt.month,
324 day: dt.day,
325 timezone: dt.timezone,
326 };
327
328 Ok(XPathValue::from_atomic(xml_date(date)))
329}
330
331pub fn current_time<N: DomNavigator>(
333 context: &mut DynamicContext<'_, N>,
334 args: Vec<XPathValue<N>>,
335) -> Result<XPathValue<N>, XPathError> {
336 if !args.is_empty() {
337 return Err(XPathError::wrong_number_of_arguments(
338 "current-time",
339 0,
340 args.len(),
341 ));
342 }
343
344 let dt_value = current_datetime(context, vec![])?;
346 let dt = match dt_value {
347 XPathValue::Item(item) => {
348 if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
349 as_datetime(&v).cloned()
350 } else {
351 None
352 }
353 }
354 _ => None,
355 };
356
357 let dt = dt.ok_or_else(|| XPathError::internal("Failed to get current dateTime"))?;
358
359 let time = TimeValue {
360 hour: dt.hour,
361 minute: dt.minute,
362 second: dt.second,
363 timezone: dt.timezone,
364 };
365
366 Ok(XPathValue::from_atomic(xml_time(time)))
367}
368
369pub fn implicit_timezone<N: DomNavigator>(
371 context: &mut DynamicContext<'_, N>,
372 args: Vec<XPathValue<N>>,
373) -> Result<XPathValue<N>, XPathError> {
374 if !args.is_empty() {
375 return Err(XPathError::wrong_number_of_arguments(
376 "implicit-timezone",
377 0,
378 args.len(),
379 ));
380 }
381
382 let tz = context
383 .implicit_timezone
384 .unwrap_or_else(get_implicit_timezone_offset);
385
386 let duration = timezone_to_day_time_duration(tz);
387 Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
388}
389
390fn normalized_years_from_ym(years: u32, months: u32, negative: bool) -> i64 {
397 let total_months = years as i64 * 12 + months as i64;
398 let years_component = total_months / 12;
399 if negative {
400 -years_component
401 } else {
402 years_component
403 }
404}
405
406fn normalized_months_from_ym(years: u32, months: u32, negative: bool) -> i64 {
409 let total_months = years as i64 * 12 + months as i64;
410 let months_component = total_months % 12;
411 if negative {
412 -months_component
413 } else {
414 months_component
415 }
416}
417
418fn normalized_days_from_dt(
420 days: u32,
421 hours: u32,
422 minutes: u32,
423 seconds: Decimal,
424 negative: bool,
425) -> i64 {
426 let total_seconds = Decimal::from(days) * Decimal::from(86400)
427 + Decimal::from(hours) * Decimal::from(3600)
428 + Decimal::from(minutes) * Decimal::from(60)
429 + seconds;
430 let days_component = (total_seconds / Decimal::from(86400))
432 .floor()
433 .to_i64()
434 .unwrap_or(0);
435 if negative {
436 -days_component
437 } else {
438 days_component
439 }
440}
441
442fn normalized_hours_from_dt(
444 days: u32,
445 hours: u32,
446 minutes: u32,
447 seconds: Decimal,
448 negative: bool,
449) -> i64 {
450 let total_seconds = Decimal::from(days) * Decimal::from(86400)
451 + Decimal::from(hours) * Decimal::from(3600)
452 + Decimal::from(minutes) * Decimal::from(60)
453 + seconds;
454 let remainder_after_days = total_seconds % Decimal::from(86400);
456 let hours_component = (remainder_after_days / Decimal::from(3600))
457 .floor()
458 .to_i64()
459 .unwrap_or(0);
460 if negative {
461 -hours_component
462 } else {
463 hours_component
464 }
465}
466
467fn normalized_minutes_from_dt(
469 days: u32,
470 hours: u32,
471 minutes: u32,
472 seconds: Decimal,
473 negative: bool,
474) -> i64 {
475 let total_seconds = Decimal::from(days) * Decimal::from(86400)
476 + Decimal::from(hours) * Decimal::from(3600)
477 + Decimal::from(minutes) * Decimal::from(60)
478 + seconds;
479 let remainder_after_hours = total_seconds % Decimal::from(3600);
481 let minutes_component = (remainder_after_hours / Decimal::from(60))
482 .floor()
483 .to_i64()
484 .unwrap_or(0);
485 if negative {
486 -minutes_component
487 } else {
488 minutes_component
489 }
490}
491
492fn normalized_seconds_from_dt(
494 days: u32,
495 hours: u32,
496 minutes: u32,
497 seconds: Decimal,
498 negative: bool,
499) -> Decimal {
500 let total_seconds = Decimal::from(days) * Decimal::from(86400)
501 + Decimal::from(hours) * Decimal::from(3600)
502 + Decimal::from(minutes) * Decimal::from(60)
503 + seconds;
504 let seconds_component = total_seconds % Decimal::from(60);
506 if negative {
507 -seconds_component
508 } else {
509 seconds_component
510 }
511}
512
513pub fn years_from_duration<N: DomNavigator>(
515 _context: &mut DynamicContext<'_, N>,
516 mut args: Vec<XPathValue<N>>,
517) -> Result<XPathValue<N>, XPathError> {
518 if args.len() != 1 {
519 return Err(XPathError::wrong_number_of_arguments(
520 "years-from-duration",
521 1,
522 args.len(),
523 ));
524 }
525
526 let arg = args.remove(0);
527 let value = match atomize_to_single_opt(arg)? {
528 None => return Ok(XPathValue::Empty),
529 Some(v) => v,
530 };
531
532 if !is_duration_type(value.type_code) {
533 return Err(XPathError::XPTY0004 {
534 expected: "xs:duration".to_string(),
535 found: format!("{:?}", value.type_code),
536 });
537 }
538
539 let result = if let Some(dur) = as_duration(&value) {
541 normalized_years_from_ym(dur.years, dur.months, dur.negative)
543 } else if let Some(ymd) = as_year_month_duration(&value) {
544 normalized_years_from_ym(ymd.years, ymd.months, ymd.negative)
545 } else if as_day_time_duration(&value).is_some() {
546 0
548 } else {
549 return Err(XPathError::internal("Unexpected duration type"));
550 };
551
552 Ok(XPathValue::from_atomic(xml_integer(result)))
553}
554
555pub fn months_from_duration<N: DomNavigator>(
557 _context: &mut DynamicContext<'_, N>,
558 mut args: Vec<XPathValue<N>>,
559) -> Result<XPathValue<N>, XPathError> {
560 if args.len() != 1 {
561 return Err(XPathError::wrong_number_of_arguments(
562 "months-from-duration",
563 1,
564 args.len(),
565 ));
566 }
567
568 let arg = args.remove(0);
569 let value = match atomize_to_single_opt(arg)? {
570 None => return Ok(XPathValue::Empty),
571 Some(v) => v,
572 };
573
574 if !is_duration_type(value.type_code) {
575 return Err(XPathError::XPTY0004 {
576 expected: "xs:duration".to_string(),
577 found: format!("{:?}", value.type_code),
578 });
579 }
580
581 let result = if let Some(dur) = as_duration(&value) {
583 normalized_months_from_ym(dur.years, dur.months, dur.negative)
584 } else if let Some(ymd) = as_year_month_duration(&value) {
585 normalized_months_from_ym(ymd.years, ymd.months, ymd.negative)
586 } else if as_day_time_duration(&value).is_some() {
587 0
589 } else {
590 return Err(XPathError::internal("Unexpected duration type"));
591 };
592
593 Ok(XPathValue::from_atomic(xml_integer(result)))
594}
595
596pub fn days_from_duration<N: DomNavigator>(
598 _context: &mut DynamicContext<'_, N>,
599 mut args: Vec<XPathValue<N>>,
600) -> Result<XPathValue<N>, XPathError> {
601 if args.len() != 1 {
602 return Err(XPathError::wrong_number_of_arguments(
603 "days-from-duration",
604 1,
605 args.len(),
606 ));
607 }
608
609 let arg = args.remove(0);
610 let value = match atomize_to_single_opt(arg)? {
611 None => return Ok(XPathValue::Empty),
612 Some(v) => v,
613 };
614
615 if !is_duration_type(value.type_code) {
616 return Err(XPathError::XPTY0004 {
617 expected: "xs:duration".to_string(),
618 found: format!("{:?}", value.type_code),
619 });
620 }
621
622 let result = if let Some(dur) = as_duration(&value) {
624 normalized_days_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
625 } else if as_year_month_duration(&value).is_some() {
626 0
628 } else if let Some(dtd) = as_day_time_duration(&value) {
629 normalized_days_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
630 } else {
631 return Err(XPathError::internal("Unexpected duration type"));
632 };
633
634 Ok(XPathValue::from_atomic(xml_integer(result)))
635}
636
637pub fn hours_from_duration<N: DomNavigator>(
639 _context: &mut DynamicContext<'_, N>,
640 mut args: Vec<XPathValue<N>>,
641) -> Result<XPathValue<N>, XPathError> {
642 if args.len() != 1 {
643 return Err(XPathError::wrong_number_of_arguments(
644 "hours-from-duration",
645 1,
646 args.len(),
647 ));
648 }
649
650 let arg = args.remove(0);
651 let value = match atomize_to_single_opt(arg)? {
652 None => return Ok(XPathValue::Empty),
653 Some(v) => v,
654 };
655
656 if !is_duration_type(value.type_code) {
657 return Err(XPathError::XPTY0004 {
658 expected: "xs:duration".to_string(),
659 found: format!("{:?}", value.type_code),
660 });
661 }
662
663 let result = if let Some(dur) = as_duration(&value) {
665 normalized_hours_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
666 } else if as_year_month_duration(&value).is_some() {
667 0
669 } else if let Some(dtd) = as_day_time_duration(&value) {
670 normalized_hours_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
671 } else {
672 return Err(XPathError::internal("Unexpected duration type"));
673 };
674
675 Ok(XPathValue::from_atomic(xml_integer(result)))
676}
677
678pub fn minutes_from_duration<N: DomNavigator>(
680 _context: &mut DynamicContext<'_, N>,
681 mut args: Vec<XPathValue<N>>,
682) -> Result<XPathValue<N>, XPathError> {
683 if args.len() != 1 {
684 return Err(XPathError::wrong_number_of_arguments(
685 "minutes-from-duration",
686 1,
687 args.len(),
688 ));
689 }
690
691 let arg = args.remove(0);
692 let value = match atomize_to_single_opt(arg)? {
693 None => return Ok(XPathValue::Empty),
694 Some(v) => v,
695 };
696
697 if !is_duration_type(value.type_code) {
698 return Err(XPathError::XPTY0004 {
699 expected: "xs:duration".to_string(),
700 found: format!("{:?}", value.type_code),
701 });
702 }
703
704 let result = if let Some(dur) = as_duration(&value) {
706 normalized_minutes_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
707 } else if as_year_month_duration(&value).is_some() {
708 0
710 } else if let Some(dtd) = as_day_time_duration(&value) {
711 normalized_minutes_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
712 } else {
713 return Err(XPathError::internal("Unexpected duration type"));
714 };
715
716 Ok(XPathValue::from_atomic(xml_integer(result)))
717}
718
719pub fn seconds_from_duration<N: DomNavigator>(
721 _context: &mut DynamicContext<'_, N>,
722 mut args: Vec<XPathValue<N>>,
723) -> Result<XPathValue<N>, XPathError> {
724 if args.len() != 1 {
725 return Err(XPathError::wrong_number_of_arguments(
726 "seconds-from-duration",
727 1,
728 args.len(),
729 ));
730 }
731
732 let arg = args.remove(0);
733 let value = match atomize_to_single_opt(arg)? {
734 None => return Ok(XPathValue::Empty),
735 Some(v) => v,
736 };
737
738 if !is_duration_type(value.type_code) {
739 return Err(XPathError::XPTY0004 {
740 expected: "xs:duration".to_string(),
741 found: format!("{:?}", value.type_code),
742 });
743 }
744
745 let result = if let Some(dur) = as_duration(&value) {
747 normalized_seconds_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
748 } else if as_year_month_duration(&value).is_some() {
749 Decimal::ZERO
751 } else if let Some(dtd) = as_day_time_duration(&value) {
752 normalized_seconds_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
753 } else {
754 return Err(XPathError::internal("Unexpected duration type"));
755 };
756
757 Ok(XPathValue::from_atomic(xml_decimal(result)))
758}
759
760pub fn year_from_datetime<N: DomNavigator>(
766 _context: &mut DynamicContext<'_, N>,
767 mut args: Vec<XPathValue<N>>,
768) -> Result<XPathValue<N>, XPathError> {
769 if args.len() != 1 {
770 return Err(XPathError::wrong_number_of_arguments(
771 "year-from-dateTime",
772 1,
773 args.len(),
774 ));
775 }
776
777 let arg = args.remove(0);
778 let value = match atomize_to_single_opt(arg)? {
779 None => return Ok(XPathValue::Empty),
780 Some(v) => v,
781 };
782
783 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
784 expected: "xs:dateTime".to_string(),
785 found: format!("{:?}", value.type_code),
786 })?;
787
788 Ok(XPathValue::from_atomic(xml_integer(dt.year as i64)))
789}
790
791pub fn month_from_datetime<N: DomNavigator>(
793 _context: &mut DynamicContext<'_, N>,
794 mut args: Vec<XPathValue<N>>,
795) -> Result<XPathValue<N>, XPathError> {
796 if args.len() != 1 {
797 return Err(XPathError::wrong_number_of_arguments(
798 "month-from-dateTime",
799 1,
800 args.len(),
801 ));
802 }
803
804 let arg = args.remove(0);
805 let value = match atomize_to_single_opt(arg)? {
806 None => return Ok(XPathValue::Empty),
807 Some(v) => v,
808 };
809
810 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
811 expected: "xs:dateTime".to_string(),
812 found: format!("{:?}", value.type_code),
813 })?;
814
815 Ok(XPathValue::from_atomic(xml_integer(dt.month as i64)))
816}
817
818pub fn day_from_datetime<N: DomNavigator>(
820 _context: &mut DynamicContext<'_, N>,
821 mut args: Vec<XPathValue<N>>,
822) -> Result<XPathValue<N>, XPathError> {
823 if args.len() != 1 {
824 return Err(XPathError::wrong_number_of_arguments(
825 "day-from-dateTime",
826 1,
827 args.len(),
828 ));
829 }
830
831 let arg = args.remove(0);
832 let value = match atomize_to_single_opt(arg)? {
833 None => return Ok(XPathValue::Empty),
834 Some(v) => v,
835 };
836
837 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
838 expected: "xs:dateTime".to_string(),
839 found: format!("{:?}", value.type_code),
840 })?;
841
842 Ok(XPathValue::from_atomic(xml_integer(dt.day as i64)))
843}
844
845pub fn hours_from_datetime<N: DomNavigator>(
847 _context: &mut DynamicContext<'_, N>,
848 mut args: Vec<XPathValue<N>>,
849) -> Result<XPathValue<N>, XPathError> {
850 if args.len() != 1 {
851 return Err(XPathError::wrong_number_of_arguments(
852 "hours-from-dateTime",
853 1,
854 args.len(),
855 ));
856 }
857
858 let arg = args.remove(0);
859 let value = match atomize_to_single_opt(arg)? {
860 None => return Ok(XPathValue::Empty),
861 Some(v) => v,
862 };
863
864 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
865 expected: "xs:dateTime".to_string(),
866 found: format!("{:?}", value.type_code),
867 })?;
868
869 Ok(XPathValue::from_atomic(xml_integer(dt.hour as i64)))
870}
871
872pub fn minutes_from_datetime<N: DomNavigator>(
874 _context: &mut DynamicContext<'_, N>,
875 mut args: Vec<XPathValue<N>>,
876) -> Result<XPathValue<N>, XPathError> {
877 if args.len() != 1 {
878 return Err(XPathError::wrong_number_of_arguments(
879 "minutes-from-dateTime",
880 1,
881 args.len(),
882 ));
883 }
884
885 let arg = args.remove(0);
886 let value = match atomize_to_single_opt(arg)? {
887 None => return Ok(XPathValue::Empty),
888 Some(v) => v,
889 };
890
891 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
892 expected: "xs:dateTime".to_string(),
893 found: format!("{:?}", value.type_code),
894 })?;
895
896 Ok(XPathValue::from_atomic(xml_integer(dt.minute as i64)))
897}
898
899pub fn seconds_from_datetime<N: DomNavigator>(
901 _context: &mut DynamicContext<'_, N>,
902 mut args: Vec<XPathValue<N>>,
903) -> Result<XPathValue<N>, XPathError> {
904 if args.len() != 1 {
905 return Err(XPathError::wrong_number_of_arguments(
906 "seconds-from-dateTime",
907 1,
908 args.len(),
909 ));
910 }
911
912 let arg = args.remove(0);
913 let value = match atomize_to_single_opt(arg)? {
914 None => return Ok(XPathValue::Empty),
915 Some(v) => v,
916 };
917
918 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
919 expected: "xs:dateTime".to_string(),
920 found: format!("{:?}", value.type_code),
921 })?;
922
923 Ok(XPathValue::from_atomic(xml_decimal(dt.second)))
924}
925
926pub fn timezone_from_datetime<N: DomNavigator>(
928 _context: &mut DynamicContext<'_, N>,
929 mut args: Vec<XPathValue<N>>,
930) -> Result<XPathValue<N>, XPathError> {
931 if args.len() != 1 {
932 return Err(XPathError::wrong_number_of_arguments(
933 "timezone-from-dateTime",
934 1,
935 args.len(),
936 ));
937 }
938
939 let arg = args.remove(0);
940 let value = match atomize_to_single_opt(arg)? {
941 None => return Ok(XPathValue::Empty),
942 Some(v) => v,
943 };
944
945 let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
946 expected: "xs:dateTime".to_string(),
947 found: format!("{:?}", value.type_code),
948 })?;
949
950 match dt.timezone {
951 Some(tz) => {
952 let duration = timezone_to_day_time_duration(tz);
953 Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
954 }
955 None => Ok(XPathValue::Empty),
956 }
957}
958
959pub fn year_from_date<N: DomNavigator>(
965 _context: &mut DynamicContext<'_, N>,
966 mut args: Vec<XPathValue<N>>,
967) -> Result<XPathValue<N>, XPathError> {
968 if args.len() != 1 {
969 return Err(XPathError::wrong_number_of_arguments(
970 "year-from-date",
971 1,
972 args.len(),
973 ));
974 }
975
976 let arg = args.remove(0);
977 let value = match atomize_to_single_opt(arg)? {
978 None => return Ok(XPathValue::Empty),
979 Some(v) => v,
980 };
981
982 let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
983 expected: "xs:date".to_string(),
984 found: format!("{:?}", value.type_code),
985 })?;
986
987 Ok(XPathValue::from_atomic(xml_integer(date.year as i64)))
988}
989
990pub fn month_from_date<N: DomNavigator>(
992 _context: &mut DynamicContext<'_, N>,
993 mut args: Vec<XPathValue<N>>,
994) -> Result<XPathValue<N>, XPathError> {
995 if args.len() != 1 {
996 return Err(XPathError::wrong_number_of_arguments(
997 "month-from-date",
998 1,
999 args.len(),
1000 ));
1001 }
1002
1003 let arg = args.remove(0);
1004 let value = match atomize_to_single_opt(arg)? {
1005 None => return Ok(XPathValue::Empty),
1006 Some(v) => v,
1007 };
1008
1009 let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
1010 expected: "xs:date".to_string(),
1011 found: format!("{:?}", value.type_code),
1012 })?;
1013
1014 Ok(XPathValue::from_atomic(xml_integer(date.month as i64)))
1015}
1016
1017pub fn day_from_date<N: DomNavigator>(
1019 _context: &mut DynamicContext<'_, N>,
1020 mut args: Vec<XPathValue<N>>,
1021) -> Result<XPathValue<N>, XPathError> {
1022 if args.len() != 1 {
1023 return Err(XPathError::wrong_number_of_arguments(
1024 "day-from-date",
1025 1,
1026 args.len(),
1027 ));
1028 }
1029
1030 let arg = args.remove(0);
1031 let value = match atomize_to_single_opt(arg)? {
1032 None => return Ok(XPathValue::Empty),
1033 Some(v) => v,
1034 };
1035
1036 let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
1037 expected: "xs:date".to_string(),
1038 found: format!("{:?}", value.type_code),
1039 })?;
1040
1041 Ok(XPathValue::from_atomic(xml_integer(date.day as i64)))
1042}
1043
1044pub fn timezone_from_date<N: DomNavigator>(
1046 _context: &mut DynamicContext<'_, N>,
1047 mut args: Vec<XPathValue<N>>,
1048) -> Result<XPathValue<N>, XPathError> {
1049 if args.len() != 1 {
1050 return Err(XPathError::wrong_number_of_arguments(
1051 "timezone-from-date",
1052 1,
1053 args.len(),
1054 ));
1055 }
1056
1057 let arg = args.remove(0);
1058 let value = match atomize_to_single_opt(arg)? {
1059 None => return Ok(XPathValue::Empty),
1060 Some(v) => v,
1061 };
1062
1063 let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
1064 expected: "xs:date".to_string(),
1065 found: format!("{:?}", value.type_code),
1066 })?;
1067
1068 match date.timezone {
1069 Some(tz) => {
1070 let duration = timezone_to_day_time_duration(tz);
1071 Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
1072 }
1073 None => Ok(XPathValue::Empty),
1074 }
1075}
1076
1077pub fn hours_from_time<N: DomNavigator>(
1083 _context: &mut DynamicContext<'_, N>,
1084 mut args: Vec<XPathValue<N>>,
1085) -> Result<XPathValue<N>, XPathError> {
1086 if args.len() != 1 {
1087 return Err(XPathError::wrong_number_of_arguments(
1088 "hours-from-time",
1089 1,
1090 args.len(),
1091 ));
1092 }
1093
1094 let arg = args.remove(0);
1095 let value = match atomize_to_single_opt(arg)? {
1096 None => return Ok(XPathValue::Empty),
1097 Some(v) => v,
1098 };
1099
1100 let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
1101 expected: "xs:time".to_string(),
1102 found: format!("{:?}", value.type_code),
1103 })?;
1104
1105 Ok(XPathValue::from_atomic(xml_integer(time.hour as i64)))
1106}
1107
1108pub fn minutes_from_time<N: DomNavigator>(
1110 _context: &mut DynamicContext<'_, N>,
1111 mut args: Vec<XPathValue<N>>,
1112) -> Result<XPathValue<N>, XPathError> {
1113 if args.len() != 1 {
1114 return Err(XPathError::wrong_number_of_arguments(
1115 "minutes-from-time",
1116 1,
1117 args.len(),
1118 ));
1119 }
1120
1121 let arg = args.remove(0);
1122 let value = match atomize_to_single_opt(arg)? {
1123 None => return Ok(XPathValue::Empty),
1124 Some(v) => v,
1125 };
1126
1127 let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
1128 expected: "xs:time".to_string(),
1129 found: format!("{:?}", value.type_code),
1130 })?;
1131
1132 Ok(XPathValue::from_atomic(xml_integer(time.minute as i64)))
1133}
1134
1135pub fn seconds_from_time<N: DomNavigator>(
1137 _context: &mut DynamicContext<'_, N>,
1138 mut args: Vec<XPathValue<N>>,
1139) -> Result<XPathValue<N>, XPathError> {
1140 if args.len() != 1 {
1141 return Err(XPathError::wrong_number_of_arguments(
1142 "seconds-from-time",
1143 1,
1144 args.len(),
1145 ));
1146 }
1147
1148 let arg = args.remove(0);
1149 let value = match atomize_to_single_opt(arg)? {
1150 None => return Ok(XPathValue::Empty),
1151 Some(v) => v,
1152 };
1153
1154 let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
1155 expected: "xs:time".to_string(),
1156 found: format!("{:?}", value.type_code),
1157 })?;
1158
1159 Ok(XPathValue::from_atomic(xml_decimal(time.second)))
1160}
1161
1162pub fn timezone_from_time<N: DomNavigator>(
1164 _context: &mut DynamicContext<'_, N>,
1165 mut args: Vec<XPathValue<N>>,
1166) -> Result<XPathValue<N>, XPathError> {
1167 if args.len() != 1 {
1168 return Err(XPathError::wrong_number_of_arguments(
1169 "timezone-from-time",
1170 1,
1171 args.len(),
1172 ));
1173 }
1174
1175 let arg = args.remove(0);
1176 let value = match atomize_to_single_opt(arg)? {
1177 None => return Ok(XPathValue::Empty),
1178 Some(v) => v,
1179 };
1180
1181 let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
1182 expected: "xs:time".to_string(),
1183 found: format!("{:?}", value.type_code),
1184 })?;
1185
1186 match time.timezone {
1187 Some(tz) => {
1188 let duration = timezone_to_day_time_duration(tz);
1189 Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
1190 }
1191 None => Ok(XPathValue::Empty),
1192 }
1193}
1194
1195pub fn create_datetime<N: DomNavigator>(
1203 _context: &mut DynamicContext<'_, N>,
1204 mut args: Vec<XPathValue<N>>,
1205) -> Result<XPathValue<N>, XPathError> {
1206 if args.len() != 2 {
1207 return Err(XPathError::wrong_number_of_arguments(
1208 "dateTime",
1209 2,
1210 args.len(),
1211 ));
1212 }
1213
1214 let time_arg = args.remove(1);
1215 let date_arg = args.remove(0);
1216
1217 let date_value = match atomize_to_single_opt(date_arg)? {
1219 None => return Ok(XPathValue::Empty),
1220 Some(v) => v,
1221 };
1222
1223 let time_value = match atomize_to_single_opt(time_arg)? {
1225 None => return Ok(XPathValue::Empty),
1226 Some(v) => v,
1227 };
1228
1229 let date = as_date(&date_value).ok_or_else(|| XPathError::XPTY0004 {
1230 expected: "xs:date".to_string(),
1231 found: format!("{:?}", date_value.type_code),
1232 })?;
1233
1234 let time = as_time(&time_value).ok_or_else(|| XPathError::XPTY0004 {
1235 expected: "xs:time".to_string(),
1236 found: format!("{:?}", time_value.type_code),
1237 })?;
1238
1239 let timezone = match (date.timezone, time.timezone) {
1241 (Some(date_tz), Some(time_tz)) => {
1242 if date_tz.0 != time_tz.0 {
1244 return Err(XPathError::FORG0008);
1245 }
1246 Some(date_tz)
1247 }
1248 (Some(tz), None) => Some(tz),
1249 (None, Some(tz)) => Some(tz),
1250 (None, None) => None,
1251 };
1252
1253 let hour = if time.hour == 24 { 0 } else { time.hour };
1255
1256 let result = DateTimeValue {
1257 year: date.year,
1258 month: date.month,
1259 day: date.day,
1260 hour,
1261 minute: time.minute,
1262 second: time.second,
1263 timezone,
1264 };
1265
1266 Ok(XPathValue::from_atomic(xml_datetime(result)))
1267}
1268
1269pub fn adjust_datetime_to_timezone<N: DomNavigator>(
1275 context: &mut DynamicContext<'_, N>,
1276 mut args: Vec<XPathValue<N>>,
1277) -> Result<XPathValue<N>, XPathError> {
1278 if args.is_empty() || args.len() > 2 {
1279 return Err(XPathError::wrong_number_of_arguments(
1280 "adjust-dateTime-to-timezone",
1281 1,
1282 args.len(),
1283 ));
1284 }
1285
1286 let tz_arg = if args.len() == 2 {
1288 Some(args.remove(1))
1289 } else {
1290 None
1291 };
1292
1293 let dt_arg = args.remove(0);
1294 let dt_value = match atomize_to_single_opt(dt_arg)? {
1295 None => return Ok(XPathValue::Empty),
1296 Some(v) => v,
1297 };
1298
1299 let dt = as_datetime(&dt_value).ok_or_else(|| XPathError::XPTY0004 {
1300 expected: "xs:dateTime".to_string(),
1301 found: format!("{:?}", dt_value.type_code),
1302 })?;
1303
1304 let target_tz = if let Some(tz_val) = tz_arg {
1306 match atomize_to_single_opt(tz_val)? {
1307 None => {
1308 let result = DateTimeValue {
1310 year: dt.year,
1311 month: dt.month,
1312 day: dt.day,
1313 hour: dt.hour,
1314 minute: dt.minute,
1315 second: dt.second,
1316 timezone: None,
1317 };
1318 return Ok(XPathValue::from_atomic(xml_datetime(result)));
1319 }
1320 Some(v) => {
1321 let duration = as_day_time_duration(&v).ok_or_else(|| XPathError::XPTY0004 {
1322 expected: "xs:dayTimeDuration".to_string(),
1323 found: format!("{:?}", v.type_code),
1324 })?;
1325 day_time_duration_to_timezone(duration)?
1326 }
1327 }
1328 } else {
1329 context
1331 .implicit_timezone
1332 .unwrap_or_else(get_implicit_timezone_offset)
1333 };
1334
1335 validate_timezone_offset(target_tz.0)?;
1336
1337 let result = match dt.timezone {
1339 None => {
1340 DateTimeValue {
1342 year: dt.year,
1343 month: dt.month,
1344 day: dt.day,
1345 hour: dt.hour,
1346 minute: dt.minute,
1347 second: dt.second,
1348 timezone: Some(target_tz),
1349 }
1350 }
1351 Some(source_tz) => {
1352 let offset_diff = target_tz.0 - source_tz.0;
1354 adjust_datetime_by_minutes(dt, offset_diff, target_tz)?
1355 }
1356 };
1357
1358 Ok(XPathValue::from_atomic(xml_datetime(result)))
1359}
1360
1361pub fn adjust_date_to_timezone<N: DomNavigator>(
1363 context: &mut DynamicContext<'_, N>,
1364 mut args: Vec<XPathValue<N>>,
1365) -> Result<XPathValue<N>, XPathError> {
1366 if args.is_empty() || args.len() > 2 {
1367 return Err(XPathError::wrong_number_of_arguments(
1368 "adjust-date-to-timezone",
1369 1,
1370 args.len(),
1371 ));
1372 }
1373
1374 let tz_arg = if args.len() == 2 {
1376 Some(args.remove(1))
1377 } else {
1378 None
1379 };
1380
1381 let date_arg = args.remove(0);
1382 let date_value = match atomize_to_single_opt(date_arg)? {
1383 None => return Ok(XPathValue::Empty),
1384 Some(v) => v,
1385 };
1386
1387 let date = as_date(&date_value).ok_or_else(|| XPathError::XPTY0004 {
1388 expected: "xs:date".to_string(),
1389 found: format!("{:?}", date_value.type_code),
1390 })?;
1391
1392 let target_tz = if let Some(tz_val) = tz_arg {
1394 match atomize_to_single_opt(tz_val)? {
1395 None => {
1396 let result = DateValue {
1398 year: date.year,
1399 month: date.month,
1400 day: date.day,
1401 timezone: None,
1402 };
1403 return Ok(XPathValue::from_atomic(xml_date(result)));
1404 }
1405 Some(v) => {
1406 let duration = as_day_time_duration(&v).ok_or_else(|| XPathError::XPTY0004 {
1407 expected: "xs:dayTimeDuration".to_string(),
1408 found: format!("{:?}", v.type_code),
1409 })?;
1410 day_time_duration_to_timezone(duration)?
1411 }
1412 }
1413 } else {
1414 context
1416 .implicit_timezone
1417 .unwrap_or_else(get_implicit_timezone_offset)
1418 };
1419
1420 validate_timezone_offset(target_tz.0)?;
1421
1422 let result = match date.timezone {
1424 None => {
1425 DateValue {
1427 year: date.year,
1428 month: date.month,
1429 day: date.day,
1430 timezone: Some(target_tz),
1431 }
1432 }
1433 Some(source_tz) => {
1434 let offset_diff = target_tz.0 - source_tz.0;
1437 adjust_date_by_minutes(date, offset_diff, target_tz)?
1438 }
1439 };
1440
1441 Ok(XPathValue::from_atomic(xml_date(result)))
1442}
1443
1444pub fn adjust_time_to_timezone<N: DomNavigator>(
1446 context: &mut DynamicContext<'_, N>,
1447 mut args: Vec<XPathValue<N>>,
1448) -> Result<XPathValue<N>, XPathError> {
1449 if args.is_empty() || args.len() > 2 {
1450 return Err(XPathError::wrong_number_of_arguments(
1451 "adjust-time-to-timezone",
1452 1,
1453 args.len(),
1454 ));
1455 }
1456
1457 let tz_arg = if args.len() == 2 {
1459 Some(args.remove(1))
1460 } else {
1461 None
1462 };
1463
1464 let time_arg = args.remove(0);
1465 let time_value = match atomize_to_single_opt(time_arg)? {
1466 None => return Ok(XPathValue::Empty),
1467 Some(v) => v,
1468 };
1469
1470 let time = as_time(&time_value).ok_or_else(|| XPathError::XPTY0004 {
1471 expected: "xs:time".to_string(),
1472 found: format!("{:?}", time_value.type_code),
1473 })?;
1474
1475 let target_tz = if let Some(tz_val) = tz_arg {
1477 match atomize_to_single_opt(tz_val)? {
1478 None => {
1479 let result = TimeValue {
1481 hour: time.hour,
1482 minute: time.minute,
1483 second: time.second,
1484 timezone: None,
1485 };
1486 return Ok(XPathValue::from_atomic(xml_time(result)));
1487 }
1488 Some(v) => {
1489 let duration = as_day_time_duration(&v).ok_or_else(|| XPathError::XPTY0004 {
1490 expected: "xs:dayTimeDuration".to_string(),
1491 found: format!("{:?}", v.type_code),
1492 })?;
1493 day_time_duration_to_timezone(duration)?
1494 }
1495 }
1496 } else {
1497 context
1499 .implicit_timezone
1500 .unwrap_or_else(get_implicit_timezone_offset)
1501 };
1502
1503 validate_timezone_offset(target_tz.0)?;
1504
1505 let result = match time.timezone {
1507 None => {
1508 TimeValue {
1510 hour: time.hour,
1511 minute: time.minute,
1512 second: time.second,
1513 timezone: Some(target_tz),
1514 }
1515 }
1516 Some(source_tz) => {
1517 let offset_diff = target_tz.0 - source_tz.0;
1519 adjust_time_by_minutes(time, offset_diff, target_tz)?
1520 }
1521 };
1522
1523 Ok(XPathValue::from_atomic(xml_time(result)))
1524}
1525
1526fn adjust_datetime_by_minutes(
1532 dt: &DateTimeValue,
1533 offset_minutes: i16,
1534 target_tz: TimezoneOffset,
1535) -> Result<DateTimeValue, XPathError> {
1536 let mut total_minutes = dt.hour as i32 * 60 + dt.minute as i32 + offset_minutes as i32;
1538
1539 let mut day_delta = 0i32;
1541 while total_minutes < 0 {
1542 total_minutes += 1440;
1543 day_delta -= 1;
1544 }
1545 while total_minutes >= 1440 {
1546 total_minutes -= 1440;
1547 day_delta += 1;
1548 }
1549
1550 let new_hour = (total_minutes / 60) as u8;
1551 let new_minute = (total_minutes % 60) as u8;
1552
1553 let (new_year, new_month, new_day) = add_days_to_date(dt.year, dt.month, dt.day, day_delta)?;
1555
1556 Ok(DateTimeValue {
1557 year: new_year,
1558 month: new_month,
1559 day: new_day,
1560 hour: new_hour,
1561 minute: new_minute,
1562 second: dt.second,
1563 timezone: Some(target_tz),
1564 })
1565}
1566
1567fn adjust_date_by_minutes(
1571 date: &DateValue,
1572 offset_minutes: i16,
1573 target_tz: TimezoneOffset,
1574) -> Result<DateValue, XPathError> {
1575 let total_minutes = offset_minutes as i32;
1580
1581 let resulting_time = total_minutes;
1584
1585 let day_delta = if resulting_time >= 0 {
1587 resulting_time / 1440
1589 } else {
1590 (resulting_time - 1439) / 1440
1593 };
1594
1595 let (new_year, new_month, new_day) =
1596 add_days_to_date(date.year, date.month, date.day, day_delta)?;
1597
1598 Ok(DateValue {
1599 year: new_year,
1600 month: new_month,
1601 day: new_day,
1602 timezone: Some(target_tz),
1603 })
1604}
1605
1606fn adjust_time_by_minutes(
1608 time: &TimeValue,
1609 offset_minutes: i16,
1610 target_tz: TimezoneOffset,
1611) -> Result<TimeValue, XPathError> {
1612 let mut total_minutes = time.hour as i32 * 60 + time.minute as i32 + offset_minutes as i32;
1613
1614 while total_minutes < 0 {
1616 total_minutes += 1440;
1617 }
1618 while total_minutes >= 1440 {
1619 total_minutes -= 1440;
1620 }
1621
1622 let new_hour = (total_minutes / 60) as u8;
1623 let new_minute = (total_minutes % 60) as u8;
1624
1625 Ok(TimeValue {
1626 hour: new_hour,
1627 minute: new_minute,
1628 second: time.second,
1629 timezone: Some(target_tz),
1630 })
1631}
1632
1633fn add_days_to_date(
1635 year: i32,
1636 month: u8,
1637 day: u8,
1638 delta: i32,
1639) -> Result<(i32, u8, u8), XPathError> {
1640 if delta == 0 {
1641 return Ok((year, month, day));
1642 }
1643
1644 let mut y = year;
1645 let mut m = month as i32;
1646 let mut d = day as i32 + delta;
1647
1648 loop {
1650 let days_in_current_month = days_in_month(y, m as u8)?;
1651
1652 if d > days_in_current_month as i32 {
1653 d -= days_in_current_month as i32;
1654 m += 1;
1655 if m > 12 {
1656 m = 1;
1657 y += 1;
1658 }
1659 } else if d < 1 {
1660 m -= 1;
1661 if m < 1 {
1662 m = 12;
1663 y -= 1;
1664 }
1665 let days_in_prev_month = days_in_month(y, m as u8)?;
1666 d += days_in_prev_month as i32;
1667 } else {
1668 break;
1669 }
1670 }
1671
1672 Ok((y, m as u8, d as u8))
1673}
1674
1675fn days_in_month(year: i32, month: u8) -> Result<u8, XPathError> {
1677 let days = match month {
1678 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1679 4 | 6 | 9 | 11 => 30,
1680 2 => {
1681 if is_leap_year(year) {
1682 29
1683 } else {
1684 28
1685 }
1686 }
1687 _ => return Err(XPathError::internal("Invalid month value")),
1688 };
1689 Ok(days)
1690}
1691
1692fn is_leap_year(year: i32) -> bool {
1694 let year = year as i64;
1695 year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
1696}
1697
1698#[cfg(test)]
1703mod tests {
1704 use super::*;
1705 use crate::namespace::table::NameTable;
1706 use crate::xpath::context::XPathContext;
1707 use crate::xpath::iterator::XmlItem;
1708 use crate::xpath::RoXmlNavigator;
1709
1710 fn make_context<'a>() -> DynamicContext<'a, RoXmlNavigator<'a>> {
1711 let table = Box::leak(Box::new(NameTable::new()));
1712 let xpath_ctx = Box::leak(Box::new(XPathContext::new(table)));
1713 DynamicContext::new(xpath_ctx, 0)
1714 }
1715
1716 fn make_datetime_value(
1717 year: i32,
1718 month: u8,
1719 day: u8,
1720 hour: u8,
1721 minute: u8,
1722 second: Decimal,
1723 timezone: Option<TimezoneOffset>,
1724 ) -> XPathValue<RoXmlNavigator<'static>> {
1725 let dt = DateTimeValue {
1726 year,
1727 month,
1728 day,
1729 hour,
1730 minute,
1731 second,
1732 timezone,
1733 };
1734 XPathValue::from_atomic(xml_datetime(dt))
1735 }
1736
1737 fn make_date_value(
1738 year: i32,
1739 month: u8,
1740 day: u8,
1741 timezone: Option<TimezoneOffset>,
1742 ) -> XPathValue<RoXmlNavigator<'static>> {
1743 let d = DateValue {
1744 year,
1745 month,
1746 day,
1747 timezone,
1748 };
1749 XPathValue::from_atomic(xml_date(d))
1750 }
1751
1752 fn make_time_value(
1753 hour: u8,
1754 minute: u8,
1755 second: Decimal,
1756 timezone: Option<TimezoneOffset>,
1757 ) -> XPathValue<RoXmlNavigator<'static>> {
1758 let t = TimeValue {
1759 hour,
1760 minute,
1761 second,
1762 timezone,
1763 };
1764 XPathValue::from_atomic(xml_time(t))
1765 }
1766
1767 fn make_duration_value(
1768 negative: bool,
1769 years: u32,
1770 months: u32,
1771 days: u32,
1772 hours: u32,
1773 minutes: u32,
1774 seconds: Decimal,
1775 ) -> XPathValue<RoXmlNavigator<'static>> {
1776 let d = DurationValue {
1777 negative,
1778 years,
1779 months,
1780 days,
1781 hours,
1782 minutes,
1783 seconds,
1784 };
1785 XPathValue::from_atomic(XmlValue {
1786 type_code: XmlTypeCode::Duration,
1787 schema_type: None,
1788 value: XmlValueKind::Atomic(XmlAtomicValue::Duration(d)),
1789 })
1790 }
1791
1792 fn make_year_month_duration(
1793 negative: bool,
1794 years: u32,
1795 months: u32,
1796 ) -> XPathValue<RoXmlNavigator<'static>> {
1797 let d = YearMonthDurationValue {
1798 negative,
1799 years,
1800 months,
1801 };
1802 XPathValue::from_atomic(XmlValue {
1803 type_code: XmlTypeCode::YearMonthDuration,
1804 schema_type: None,
1805 value: XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(d)),
1806 })
1807 }
1808
1809 fn make_day_time_duration(
1810 negative: bool,
1811 days: u32,
1812 hours: u32,
1813 minutes: u32,
1814 seconds: Decimal,
1815 ) -> XPathValue<RoXmlNavigator<'static>> {
1816 let d = DayTimeDurationValue {
1817 negative,
1818 days,
1819 hours,
1820 minutes,
1821 seconds,
1822 };
1823 XPathValue::from_atomic(xml_day_time_duration(d))
1824 }
1825
1826 fn get_integer_result<N: DomNavigator>(result: &XPathValue<N>) -> Option<i64> {
1827 match result {
1828 XPathValue::Item(XmlItem::Atomic(v)) => v.as_integer().and_then(|i| i.to_i64()),
1829 _ => None,
1830 }
1831 }
1832
1833 fn get_decimal_result<N: DomNavigator>(result: &XPathValue<N>) -> Option<Decimal> {
1834 match result {
1835 XPathValue::Item(XmlItem::Atomic(v)) => v.as_decimal(),
1836 _ => None,
1837 }
1838 }
1839
1840 #[test]
1845 fn test_current_datetime_returns_value() {
1846 let mut ctx = make_context();
1847 let result = current_datetime(&mut ctx, vec![]).unwrap();
1848 assert!(!result.is_empty());
1849 }
1850
1851 #[test]
1852 fn test_current_date_returns_value() {
1853 let mut ctx = make_context();
1854 let result = current_date(&mut ctx, vec![]).unwrap();
1855 assert!(!result.is_empty());
1856 }
1857
1858 #[test]
1859 fn test_current_time_returns_value() {
1860 let mut ctx = make_context();
1861 let result = current_time(&mut ctx, vec![]).unwrap();
1862 assert!(!result.is_empty());
1863 }
1864
1865 #[test]
1866 fn test_implicit_timezone_returns_value() {
1867 let mut ctx = make_context();
1868 let result = implicit_timezone(&mut ctx, vec![]).unwrap();
1869 assert!(!result.is_empty());
1870 }
1871
1872 #[test]
1877 fn test_years_from_duration() {
1878 let mut ctx = make_context();
1879 let dur = make_duration_value(false, 1, 2, 3, 4, 5, Decimal::from(6));
1881 let result = years_from_duration(&mut ctx, vec![dur]).unwrap();
1882 assert_eq!(get_integer_result(&result), Some(1));
1883 }
1884
1885 #[test]
1886 fn test_years_from_duration_negative() {
1887 let mut ctx = make_context();
1888 let dur = make_duration_value(true, 1, 2, 0, 0, 0, Decimal::ZERO);
1890 let result = years_from_duration(&mut ctx, vec![dur]).unwrap();
1891 assert_eq!(get_integer_result(&result), Some(-1));
1892 }
1893
1894 #[test]
1895 fn test_months_from_duration() {
1896 let mut ctx = make_context();
1897 let dur = make_duration_value(false, 1, 14, 0, 0, 0, Decimal::ZERO);
1899 let result = months_from_duration(&mut ctx, vec![dur]).unwrap();
1900 assert_eq!(get_integer_result(&result), Some(2));
1901 }
1902
1903 #[test]
1904 fn test_days_from_duration() {
1905 let mut ctx = make_context();
1906 let dur = make_day_time_duration(false, 5, 0, 0, Decimal::ZERO);
1908 let result = days_from_duration(&mut ctx, vec![dur]).unwrap();
1909 assert_eq!(get_integer_result(&result), Some(5));
1910 }
1911
1912 #[test]
1913 fn test_hours_from_duration() {
1914 let mut ctx = make_context();
1915 let dur = make_day_time_duration(false, 0, 10, 0, Decimal::ZERO);
1917 let result = hours_from_duration(&mut ctx, vec![dur]).unwrap();
1918 assert_eq!(get_integer_result(&result), Some(10));
1919 }
1920
1921 #[test]
1922 fn test_minutes_from_duration() {
1923 let mut ctx = make_context();
1924 let dur = make_day_time_duration(false, 0, 0, 45, Decimal::ZERO);
1926 let result = minutes_from_duration(&mut ctx, vec![dur]).unwrap();
1927 assert_eq!(get_integer_result(&result), Some(45));
1928 }
1929
1930 #[test]
1931 fn test_seconds_from_duration() {
1932 let mut ctx = make_context();
1933 let dur = make_day_time_duration(false, 0, 0, 0, Decimal::new(305, 1));
1935 let result = seconds_from_duration(&mut ctx, vec![dur]).unwrap();
1936 assert_eq!(get_decimal_result(&result), Some(Decimal::new(305, 1)));
1937 }
1938
1939 #[test]
1944 fn test_year_from_datetime() {
1945 let mut ctx = make_context();
1946 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
1947 let result = year_from_datetime(&mut ctx, vec![dt]).unwrap();
1948 assert_eq!(get_integer_result(&result), Some(2024));
1949 }
1950
1951 #[test]
1952 fn test_month_from_datetime() {
1953 let mut ctx = make_context();
1954 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
1955 let result = month_from_datetime(&mut ctx, vec![dt]).unwrap();
1956 assert_eq!(get_integer_result(&result), Some(3));
1957 }
1958
1959 #[test]
1960 fn test_day_from_datetime() {
1961 let mut ctx = make_context();
1962 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
1963 let result = day_from_datetime(&mut ctx, vec![dt]).unwrap();
1964 assert_eq!(get_integer_result(&result), Some(15));
1965 }
1966
1967 #[test]
1968 fn test_hours_from_datetime() {
1969 let mut ctx = make_context();
1970 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
1971 let result = hours_from_datetime(&mut ctx, vec![dt]).unwrap();
1972 assert_eq!(get_integer_result(&result), Some(10));
1973 }
1974
1975 #[test]
1976 fn test_minutes_from_datetime() {
1977 let mut ctx = make_context();
1978 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
1979 let result = minutes_from_datetime(&mut ctx, vec![dt]).unwrap();
1980 assert_eq!(get_integer_result(&result), Some(30));
1981 }
1982
1983 #[test]
1984 fn test_seconds_from_datetime() {
1985 let mut ctx = make_context();
1986 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::new(455, 1), None);
1987 let result = seconds_from_datetime(&mut ctx, vec![dt]).unwrap();
1988 assert_eq!(get_decimal_result(&result), Some(Decimal::new(455, 1)));
1989 }
1990
1991 #[test]
1992 fn test_timezone_from_datetime_with_tz() {
1993 let mut ctx = make_context();
1994 let dt = make_datetime_value(
1995 2024,
1996 3,
1997 15,
1998 10,
1999 30,
2000 Decimal::from(0),
2001 Some(TimezoneOffset(-300)),
2002 );
2003 let result = timezone_from_datetime(&mut ctx, vec![dt]).unwrap();
2004 assert!(!result.is_empty());
2005 }
2006
2007 #[test]
2008 fn test_timezone_from_datetime_without_tz() {
2009 let mut ctx = make_context();
2010 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
2011 let result = timezone_from_datetime(&mut ctx, vec![dt]).unwrap();
2012 assert!(result.is_empty());
2013 }
2014
2015 #[test]
2020 fn test_year_from_date() {
2021 let mut ctx = make_context();
2022 let d = make_date_value(2024, 6, 20, None);
2023 let result = year_from_date(&mut ctx, vec![d]).unwrap();
2024 assert_eq!(get_integer_result(&result), Some(2024));
2025 }
2026
2027 #[test]
2028 fn test_month_from_date() {
2029 let mut ctx = make_context();
2030 let d = make_date_value(2024, 6, 20, None);
2031 let result = month_from_date(&mut ctx, vec![d]).unwrap();
2032 assert_eq!(get_integer_result(&result), Some(6));
2033 }
2034
2035 #[test]
2036 fn test_day_from_date() {
2037 let mut ctx = make_context();
2038 let d = make_date_value(2024, 6, 20, None);
2039 let result = day_from_date(&mut ctx, vec![d]).unwrap();
2040 assert_eq!(get_integer_result(&result), Some(20));
2041 }
2042
2043 #[test]
2048 fn test_hours_from_time() {
2049 let mut ctx = make_context();
2050 let t = make_time_value(14, 35, Decimal::from(0), None);
2051 let result = hours_from_time(&mut ctx, vec![t]).unwrap();
2052 assert_eq!(get_integer_result(&result), Some(14));
2053 }
2054
2055 #[test]
2056 fn test_minutes_from_time() {
2057 let mut ctx = make_context();
2058 let t = make_time_value(14, 35, Decimal::from(0), None);
2059 let result = minutes_from_time(&mut ctx, vec![t]).unwrap();
2060 assert_eq!(get_integer_result(&result), Some(35));
2061 }
2062
2063 #[test]
2064 fn test_seconds_from_time() {
2065 let mut ctx = make_context();
2066 let t = make_time_value(14, 35, Decimal::new(125, 1), None);
2067 let result = seconds_from_time(&mut ctx, vec![t]).unwrap();
2068 assert_eq!(get_decimal_result(&result), Some(Decimal::new(125, 1)));
2069 }
2070
2071 #[test]
2076 fn test_create_datetime_no_tz() {
2077 let mut ctx = make_context();
2078 let d = make_date_value(2024, 3, 15, None);
2079 let t = make_time_value(10, 30, Decimal::from(0), None);
2080 let result = create_datetime(&mut ctx, vec![d, t]).unwrap();
2081 assert!(!result.is_empty());
2082 }
2083
2084 #[test]
2085 fn test_create_datetime_with_matching_tz() {
2086 let mut ctx = make_context();
2087 let tz = TimezoneOffset::from_hm(5, 0);
2088 let d = make_date_value(2024, 3, 15, Some(tz));
2089 let t = make_time_value(10, 30, Decimal::from(0), Some(tz));
2090 let result = create_datetime(&mut ctx, vec![d, t]).unwrap();
2091 assert!(!result.is_empty());
2092 }
2093
2094 #[test]
2095 fn test_create_datetime_mismatched_tz() {
2096 let mut ctx = make_context();
2097 let d = make_date_value(2024, 3, 15, Some(TimezoneOffset::from_hm(5, 0)));
2098 let t = make_time_value(
2099 10,
2100 30,
2101 Decimal::from(0),
2102 Some(TimezoneOffset::from_hm(-5, 0)),
2103 );
2104 let result = create_datetime(&mut ctx, vec![d, t]);
2105 assert!(matches!(result, Err(XPathError::FORG0008)));
2106 }
2107
2108 #[test]
2113 fn test_adjust_datetime_to_timezone_no_input_tz() {
2114 let mut ctx = make_context();
2115 ctx.implicit_timezone = Some(TimezoneOffset::from_hm(-5, 0));
2116 let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
2117 let result = adjust_datetime_to_timezone(&mut ctx, vec![dt]).unwrap();
2118 assert!(!result.is_empty());
2120 }
2121
2122 #[test]
2123 fn test_adjust_datetime_to_timezone_strip_tz() {
2124 let mut ctx = make_context();
2125 let dt = make_datetime_value(
2126 2024,
2127 3,
2128 15,
2129 10,
2130 30,
2131 Decimal::from(0),
2132 Some(TimezoneOffset::UTC),
2133 );
2134 let result = adjust_datetime_to_timezone(&mut ctx, vec![dt, XPathValue::Empty]).unwrap();
2135 assert!(!result.is_empty());
2137 }
2138
2139 #[test]
2140 fn test_adjust_time_to_timezone() {
2141 let mut ctx = make_context();
2142 ctx.implicit_timezone = Some(TimezoneOffset::from_hm(0, 0));
2143 let t = make_time_value(10, 30, Decimal::from(0), None);
2144 let result = adjust_time_to_timezone(&mut ctx, vec![t]).unwrap();
2145 assert!(!result.is_empty());
2146 }
2147
2148 #[test]
2153 fn test_component_functions_with_empty() {
2154 let mut ctx = make_context();
2155
2156 let result = years_from_duration(&mut ctx, vec![XPathValue::Empty]).unwrap();
2157 assert!(result.is_empty());
2158
2159 let result = year_from_datetime(&mut ctx, vec![XPathValue::Empty]).unwrap();
2160 assert!(result.is_empty());
2161
2162 let result = year_from_date(&mut ctx, vec![XPathValue::Empty]).unwrap();
2163 assert!(result.is_empty());
2164
2165 let result = hours_from_time(&mut ctx, vec![XPathValue::Empty]).unwrap();
2166 assert!(result.is_empty());
2167 }
2168
2169 #[test]
2174 fn test_duration_normalization_p14m() {
2175 let mut ctx = make_context();
2177 let dur = make_year_month_duration(false, 0, 14);
2178
2179 let result = years_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
2180 assert_eq!(get_integer_result(&result), Some(1));
2181
2182 let result = months_from_duration(&mut ctx, vec![dur]).unwrap();
2183 assert_eq!(get_integer_result(&result), Some(2));
2184 }
2185
2186 #[test]
2187 fn test_duration_normalization_pt30h() {
2188 let mut ctx = make_context();
2190 let dur = make_day_time_duration(false, 0, 30, 0, Decimal::ZERO);
2191
2192 let result = days_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
2193 assert_eq!(get_integer_result(&result), Some(1));
2194
2195 let result = hours_from_duration(&mut ctx, vec![dur]).unwrap();
2196 assert_eq!(get_integer_result(&result), Some(6));
2197 }
2198
2199 #[test]
2200 fn test_duration_normalization_pt90m() {
2201 let mut ctx = make_context();
2203 let dur = make_day_time_duration(false, 0, 0, 90, Decimal::ZERO);
2204
2205 let result = hours_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
2206 assert_eq!(get_integer_result(&result), Some(1));
2207
2208 let result = minutes_from_duration(&mut ctx, vec![dur]).unwrap();
2209 assert_eq!(get_integer_result(&result), Some(30));
2210 }
2211
2212 #[test]
2213 fn test_duration_normalization_pt3665s() {
2214 let mut ctx = make_context();
2216 let dur = make_day_time_duration(false, 0, 0, 0, Decimal::from(3665));
2217
2218 let result = hours_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
2219 assert_eq!(get_integer_result(&result), Some(1));
2220
2221 let result = minutes_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
2222 assert_eq!(get_integer_result(&result), Some(1));
2223
2224 let result = seconds_from_duration(&mut ctx, vec![dur]).unwrap();
2225 assert_eq!(get_decimal_result(&result), Some(Decimal::from(5)));
2226 }
2227
2228 #[test]
2233 fn test_timezone_offset_with_days_rejected() {
2234 let dur = DayTimeDurationValue {
2236 negative: false,
2237 days: 1,
2238 hours: 0,
2239 minutes: 0,
2240 seconds: Decimal::ZERO,
2241 };
2242 let result = day_time_duration_to_timezone(&dur);
2243 assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
2244 }
2245
2246 #[test]
2247 fn test_timezone_offset_with_seconds_rejected() {
2248 let dur = DayTimeDurationValue {
2250 negative: false,
2251 days: 0,
2252 hours: 5,
2253 minutes: 0,
2254 seconds: Decimal::from(30),
2255 };
2256 let result = day_time_duration_to_timezone(&dur);
2257 assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
2258 }
2259
2260 #[test]
2261 fn test_timezone_offset_with_fractional_seconds_rejected() {
2262 let dur = DayTimeDurationValue {
2264 negative: false,
2265 days: 0,
2266 hours: 5,
2267 minutes: 0,
2268 seconds: Decimal::new(5, 1), };
2270 let result = day_time_duration_to_timezone(&dur);
2271 assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
2272 }
2273
2274 #[test]
2275 fn test_timezone_offset_valid() {
2276 let dur = DayTimeDurationValue {
2278 negative: false,
2279 days: 0,
2280 hours: 5,
2281 minutes: 30,
2282 seconds: Decimal::ZERO,
2283 };
2284 let result = day_time_duration_to_timezone(&dur).unwrap();
2285 assert_eq!(result.0, 330); }
2287
2288 #[test]
2289 fn test_timezone_offset_out_of_range_rejected() {
2290 let dur = DayTimeDurationValue {
2292 negative: false,
2293 days: 0,
2294 hours: 15,
2295 minutes: 0,
2296 seconds: Decimal::ZERO,
2297 };
2298 let result = day_time_duration_to_timezone(&dur);
2299 assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
2300 }
2301}