1use chrono::{
19 DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime,
20 TimeZone, Timelike, Utc,
21};
22
23use crate::error::{ErrorDomain, ErrorLevel, XmlError};
24use crate::xpath::eval::{Numeric, Value, value_to_string};
25use crate::xpath::index::DocIndexLike;
26
27use super::Result;
28
29fn err(msg: impl Into<String>) -> XmlError {
30 XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
31}
32
33#[derive(Clone, Copy, Debug)]
43struct Dt {
44 naive: NaiveDateTime,
45 offset: Option<FixedOffset>,
46}
47
48impl Dt {
49 fn to_utc(self) -> DateTime<Utc> {
53 let off = self.offset.unwrap_or(FixedOffset::east_opt(0).unwrap());
54 off.from_local_datetime(&self.naive).single()
56 .map(|dt| dt.with_timezone(&Utc))
57 .unwrap_or_else(|| Utc.from_utc_datetime(&self.naive))
58 }
59}
60
61fn parse_xsd_datetime(s: &str) -> Option<Dt> {
66 let (s, neg) = strip_neg(s);
67 let (year_part, rest) = take_year(s)?;
68 let rest = expect_char(rest, '-')?;
69 let (month, rest) = take_n_digits(rest, 2)?;
70 let rest = expect_char(rest, '-')?;
71 let (day, rest) = take_n_digits(rest, 2)?;
72 let rest = expect_char(rest, 'T')?;
73 let (hour, rest) = take_n_digits(rest, 2)?;
74 let rest = expect_char(rest, ':')?;
75 let (minute, rest) = take_n_digits(rest, 2)?;
76 let rest = expect_char(rest, ':')?;
77 let (second, rest) = take_n_digits(rest, 2)?;
78 let (nano, rest) = parse_optional_fraction(rest);
79 let (offset, rest) = parse_optional_tz(rest);
80 if !rest.is_empty() { return None; }
81
82 let year = signed(year_part, neg)?;
83 let date = NaiveDate::from_ymd_opt(year, month as u32, day as u32)?;
84 let time = NaiveTime::from_hms_nano_opt(hour as u32, minute as u32, second as u32, nano)?;
85 Some(Dt { naive: date.and_time(time), offset })
86}
87
88fn parse_xsd_date(s: &str) -> Option<Dt> {
90 let (s, neg) = strip_neg(s);
91 let (year_part, rest) = take_year(s)?;
92 let rest = expect_char(rest, '-')?;
93 let (month, rest) = take_n_digits(rest, 2)?;
94 let rest = expect_char(rest, '-')?;
95 let (day, rest) = take_n_digits(rest, 2)?;
96 let (offset, rest) = parse_optional_tz(rest);
97 if !rest.is_empty() { return None; }
98 let year = signed(year_part, neg)?;
99 let date = NaiveDate::from_ymd_opt(year, month as u32, day as u32)?;
100 Some(Dt { naive: date.and_time(NaiveTime::MIN), offset })
101}
102
103fn parse_xsd_time(s: &str) -> Option<Dt> {
105 let (hour, rest) = take_n_digits(s, 2)?;
106 let rest = expect_char(rest, ':')?;
107 let (minute, rest) = take_n_digits(rest, 2)?;
108 let rest = expect_char(rest, ':')?;
109 let (second, rest) = take_n_digits(rest, 2)?;
110 let (nano, rest) = parse_optional_fraction(rest);
111 let (offset, rest) = parse_optional_tz(rest);
112 if !rest.is_empty() { return None; }
113 let time = NaiveTime::from_hms_nano_opt(hour as u32, minute as u32, second as u32, nano)?;
114 let date = NaiveDate::from_ymd_opt(1970, 1, 1)?;
118 Some(Dt { naive: date.and_time(time), offset })
119}
120
121fn parse_any(s: &str) -> Option<Dt> {
123 parse_xsd_datetime(s).or_else(|| parse_xsd_date(s)).or_else(|| parse_xsd_time(s))
124}
125
126#[derive(Clone, Copy, Debug)]
136struct XsdDuration {
137 negative: bool,
139 months: i64, days: i64,
141 seconds: i64,
142 nanos: i64,
143}
144
145impl XsdDuration {
146 fn months_part(&self) -> i64 {
148 if self.negative { -self.months } else { self.months }
149 }
150 fn duration_part(&self) -> chrono::Duration {
152 let total_nanos =
153 (self.days as i128) * 86_400 * 1_000_000_000
154 + (self.seconds as i128) * 1_000_000_000
155 + self.nanos as i128;
156 let signed = if self.negative { -total_nanos } else { total_nanos };
157 chrono::Duration::nanoseconds(signed.clamp(i64::MIN as i128, i64::MAX as i128) as i64)
158 }
159}
160
161fn parse_xsd_duration(s: &str) -> Option<XsdDuration> {
164 let (mut s, neg) = strip_neg(s);
165 s = s.strip_prefix('P')?;
166 let mut years = 0i64;
167 let mut months = 0i64;
168 let mut days = 0i64;
169 let mut hours = 0i64;
170 let mut minutes = 0i64;
171 let mut seconds = 0i64;
172 let mut nanos = 0i64;
173 let mut any = false;
174
175 while let Some(c) = s.chars().next() {
177 if c == 'T' { break; }
178 let (n, rest) = take_unsigned_int(s)?;
179 let unit = rest.chars().next()?;
180 s = &rest[unit.len_utf8()..];
181 match unit {
182 'Y' => years = n as i64,
183 'M' => months = n as i64,
184 'D' => days = n as i64,
185 _ => return None,
186 }
187 any = true;
188 }
189 if let Some(rest) = s.strip_prefix('T') {
191 s = rest;
192 if s.is_empty() { return None; }
193 loop {
194 if s.is_empty() { break; }
195 let (n, mut rest) = take_unsigned_int(s)?;
196 let mut frac_nanos = 0i64;
198 if let Some(after_dot) = rest.strip_prefix('.') {
199 let (digits, after) = take_digits(after_dot);
200 if digits.is_empty() { return None; }
201 frac_nanos = digits_to_nanos(digits);
202 rest = after;
203 }
204 let unit = rest.chars().next()?;
205 s = &rest[unit.len_utf8()..];
206 match unit {
207 'H' => hours = n as i64,
208 'M' => minutes = n as i64,
209 'S' => { seconds = n as i64; nanos = frac_nanos; }
210 _ => return None,
211 }
212 any = true;
213 }
214 }
215 if !any || !s.is_empty() { return None; }
216 let total_months = years.checked_mul(12)?.checked_add(months)?;
217 let total_seconds = hours.checked_mul(3600)?
218 .checked_add(minutes.checked_mul(60)?)?
219 .checked_add(seconds)?;
220 Some(XsdDuration {
221 negative: neg, months: total_months, days, seconds: total_seconds, nanos,
222 })
223}
224
225fn format_xsd_datetime(dt: &Dt) -> String {
228 let mut s = format_year(dt.naive.year());
229 s.push_str(&format!(
230 "-{:02}-{:02}T{:02}:{:02}:{:02}",
231 dt.naive.month(), dt.naive.day(),
232 dt.naive.hour(), dt.naive.minute(), dt.naive.second(),
233 ));
234 let nanos = dt.naive.nanosecond();
235 if nanos != 0 {
236 let frac = format!(".{:09}", nanos);
237 s.push_str(frac.trim_end_matches('0'));
238 }
239 if let Some(off) = dt.offset { s.push_str(&format_offset(off)); }
240 s
241}
242
243fn format_xsd_date(dt: &Dt) -> String {
244 let mut s = format_year(dt.naive.year());
245 s.push_str(&format!("-{:02}-{:02}", dt.naive.month(), dt.naive.day()));
246 if let Some(off) = dt.offset { s.push_str(&format_offset(off)); }
247 s
248}
249
250fn format_xsd_time(dt: &Dt) -> String {
251 let mut s = format!(
252 "{:02}:{:02}:{:02}",
253 dt.naive.hour(), dt.naive.minute(), dt.naive.second(),
254 );
255 let nanos = dt.naive.nanosecond();
256 if nanos != 0 {
257 let frac = format!(".{:09}", nanos);
258 s.push_str(frac.trim_end_matches('0'));
259 }
260 if let Some(off) = dt.offset { s.push_str(&format_offset(off)); }
261 s
262}
263
264fn format_year(y: i32) -> String {
265 if y < 0 { format!("-{:04}", -y) } else { format!("{:04}", y) }
266}
267
268fn format_offset(off: FixedOffset) -> String {
269 let secs = off.local_minus_utc();
270 if secs == 0 { return "Z".to_string(); }
271 let (sign, mag) = if secs >= 0 { ('+', secs) } else { ('-', -secs) };
272 format!("{sign}{:02}:{:02}", mag / 3600, (mag % 3600) / 60)
273}
274
275fn format_xsd_duration(d: &XsdDuration) -> String {
276 let mut s = String::new();
279 if d.negative { s.push('-'); }
280 s.push('P');
281 let mut any_date = false;
282 let years = d.months / 12;
283 let months = d.months % 12;
284 if years != 0 { s.push_str(&format!("{years}Y")); any_date = true; }
285 if months != 0 { s.push_str(&format!("{months}M")); any_date = true; }
286 if d.days != 0 { s.push_str(&format!("{}D", d.days)); any_date = true; }
287 let has_time = d.seconds != 0 || d.nanos != 0;
288 if has_time {
289 s.push('T');
290 let hours = d.seconds / 3600;
291 let minutes = (d.seconds % 3600) / 60;
292 let secs = d.seconds % 60;
293 if hours != 0 { s.push_str(&format!("{hours}H")); }
294 if minutes != 0 { s.push_str(&format!("{minutes}M")); }
295 if secs != 0 || d.nanos != 0 {
296 if d.nanos == 0 {
297 s.push_str(&format!("{secs}S"));
298 } else {
299 let frac = format!(".{:09}", d.nanos);
300 s.push_str(&format!("{secs}{}S", frac.trim_end_matches('0')));
301 }
302 }
303 } else if !any_date {
304 s.push_str("PT0S".strip_prefix('P').unwrap());
305 }
306 s
307}
308
309fn strip_neg(s: &str) -> (&str, bool) {
312 match s.strip_prefix('-') { Some(rest) => (rest, true), None => (s, false) }
313}
314
315fn take_year(s: &str) -> Option<(&str, &str)> {
317 let bytes = s.as_bytes();
318 let mut i = 0;
319 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
320 if i < 4 { return None; }
321 Some((&s[..i], &s[i..]))
322}
323
324fn take_n_digits(s: &str, n: usize) -> Option<(u32, &str)> {
325 if s.len() < n { return None; }
326 let head = &s[..n];
327 if !head.bytes().all(|b| b.is_ascii_digit()) { return None; }
328 Some((head.parse().ok()?, &s[n..]))
329}
330
331fn take_digits(s: &str) -> (&str, &str) {
332 let bytes = s.as_bytes();
333 let mut i = 0;
334 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
335 (&s[..i], &s[i..])
336}
337
338fn take_unsigned_int(s: &str) -> Option<(u64, &str)> {
339 let (head, rest) = take_digits(s);
340 if head.is_empty() { return None; }
341 Some((head.parse().ok()?, rest))
342}
343
344fn expect_char(s: &str, c: char) -> Option<&str> {
345 s.strip_prefix(c)
346}
347
348fn signed(year_part: &str, neg: bool) -> Option<i32> {
349 let y: i32 = year_part.parse().ok()?;
350 if y == 0 { return None; } Some(if neg { -y } else { y })
352}
353
354fn parse_optional_fraction(s: &str) -> (u32, &str) {
355 match s.strip_prefix('.') {
356 Some(rest) => {
357 let (digits, after) = take_digits(rest);
358 if digits.is_empty() { return (0, s); }
359 (digits_to_nanos(digits) as u32, after)
360 }
361 None => (0, s),
362 }
363}
364
365fn digits_to_nanos(digits: &str) -> i64 {
369 let mut buf = String::with_capacity(9);
370 for (i, c) in digits.chars().enumerate() {
371 if i >= 9 { break; }
372 buf.push(c);
373 }
374 while buf.len() < 9 { buf.push('0'); }
375 buf.parse().unwrap_or(0)
376}
377
378fn parse_optional_tz(s: &str) -> (Option<FixedOffset>, &str) {
381 if let Some(rest) = s.strip_prefix('Z') {
382 return (FixedOffset::east_opt(0), rest);
383 }
384 let sign = match s.as_bytes().first() {
385 Some(b'+') => 1,
386 Some(b'-') => -1,
387 _ => return (None, s),
388 };
389 let rest = &s[1..];
390 let Some((h, r)) = take_n_digits(rest, 2) else { return (None, s); };
391 let Some(r) = expect_char(r, ':') else { return (None, s); };
392 let Some((m, r)) = take_n_digits(r, 2) else { return (None, s); };
393 let total = sign * ((h as i32) * 3600 + (m as i32) * 60);
394 (FixedOffset::east_opt(total), r)
395}
396
397fn now_dt() -> Dt {
400 let dur = std::time::SystemTime::now()
403 .duration_since(std::time::UNIX_EPOCH)
404 .unwrap_or_default();
405 let secs = dur.as_secs() as i64;
406 let nanos = dur.subsec_nanos();
407 let dt = DateTime::<Utc>::from_timestamp(secs, nanos)
408 .unwrap_or_else(|| Utc.timestamp_opt(0, 0).unwrap());
409 Dt {
410 naive: dt.naive_utc(),
411 offset: Some(FixedOffset::east_opt(0).unwrap()),
412 }
413}
414
415fn arg_str<I: DocIndexLike>(args: &[Value], n: usize, idx: &I) -> Option<String> {
416 args.get(n).map(|v| value_to_string(v, idx))
417}
418
419fn dt_arg_or_now<I: DocIndexLike>(args: &[Value], idx: &I) -> Option<Dt> {
423 if args.is_empty() {
424 return Some(now_dt());
425 }
426 parse_any(&arg_str(args, 0, idx)?)
427}
428
429pub fn dispatch<I: DocIndexLike>(
430 name: &str, args: Vec<Value>, idx: &I,
431) -> Option<Result<Value>> {
432 let r: Result<Value> = match name {
433 "date-time" => {
434 if !args.is_empty() {
435 return Some(Err(err("date:date-time takes no arguments")));
436 }
437 Ok(Value::String(format_xsd_datetime(&now_dt())))
438 }
439 "date" => Ok(extract_part(&args, idx, |dt| format_xsd_date(&dt))),
440 "time" => Ok(extract_part(&args, idx, |dt| format_xsd_time(&dt))),
441
442 "year" => Ok(num_field(&args, idx, |dt| dt.naive.year() as f64)),
443 "month-in-year" => Ok(num_field(&args, idx, |dt| dt.naive.month() as f64)),
444 "day-in-month" => Ok(num_field(&args, idx, |dt| dt.naive.day() as f64)),
445 "hour-in-day" => Ok(num_field(&args, idx, |dt| dt.naive.hour() as f64)),
446 "minute-in-hour" => Ok(num_field(&args, idx, |dt| dt.naive.minute() as f64)),
447 "second-in-minute"=> Ok(num_field(&args, idx, |dt| {
448 dt.naive.second() as f64 + dt.naive.nanosecond() as f64 / 1e9
450 })),
451
452 "day-in-week" => Ok(num_field(&args, idx, |dt| {
454 dt.naive.weekday().number_from_sunday() as f64
457 })),
458 "day-name" => Ok(str_field(&args, idx, |dt| match dt.naive.weekday() {
459 chrono::Weekday::Mon => "Monday", chrono::Weekday::Tue => "Tuesday",
460 chrono::Weekday::Wed => "Wednesday",chrono::Weekday::Thu => "Thursday",
461 chrono::Weekday::Fri => "Friday", chrono::Weekday::Sat => "Saturday",
462 chrono::Weekday::Sun => "Sunday",
463 })),
464 "day-abbreviation" => Ok(str_field(&args, idx, |dt| match dt.naive.weekday() {
465 chrono::Weekday::Mon => "Mon", chrono::Weekday::Tue => "Tue",
466 chrono::Weekday::Wed => "Wed", chrono::Weekday::Thu => "Thu",
467 chrono::Weekday::Fri => "Fri", chrono::Weekday::Sat => "Sat",
468 chrono::Weekday::Sun => "Sun",
469 })),
470 "month-name" => Ok(str_field(&args, idx, |dt| MONTH_NAMES[(dt.naive.month() - 1) as usize])),
471 "month-abbreviation" => Ok(str_field(&args, idx, |dt| MONTH_ABBR[(dt.naive.month() - 1) as usize])),
472
473 "day-of-week-in-month" => Ok(num_field(&args, idx, |dt| {
476 ((dt.naive.day() - 1) / 7 + 1) as f64
477 })),
478
479 "day-in-year" => Ok(num_field(&args, idx, |dt| dt.naive.ordinal() as f64)),
482 "week-in-year" => Ok(num_field(&args, idx, |dt| dt.naive.iso_week().week() as f64)),
486 "leap-year" => Ok(bool_field(&args, idx, |dt| {
487 let y = dt.naive.year();
490 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
491 })),
492 "seconds" => Ok(seconds_fn(&args, idx)),
498
499 "add" => Ok(add_fn(&args, idx)),
500 "add-duration" => Ok(add_duration_fn(&args, idx)),
501 "difference" => Ok(difference_fn(&args, idx)),
502 "duration" => Ok(duration_from_seconds(&args, idx)),
503
504 _ => return None,
505 };
506 Some(r)
507}
508
509const MONTH_NAMES: [&str; 12] = [
510 "January", "February", "March", "April", "May", "June",
511 "July", "August", "September", "October", "November", "December",
512];
513const MONTH_ABBR: [&str; 12] = [
514 "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec",
515];
516
517fn extract_part<I: DocIndexLike>(
520 args: &[Value], idx: &I, fmt: impl FnOnce(Dt) -> String,
521) -> Value {
522 Value::String(dt_arg_or_now(args, idx).map(fmt).unwrap_or_default())
523}
524
525fn num_field<I: DocIndexLike>(
526 args: &[Value], idx: &I, f: impl FnOnce(Dt) -> f64,
527) -> Value {
528 Value::Number(Numeric::Double(dt_arg_or_now(args, idx).map(f).unwrap_or(f64::NAN)))
529}
530
531fn str_field<I: DocIndexLike>(
532 args: &[Value], idx: &I, f: impl FnOnce(Dt) -> &'static str,
533) -> Value {
534 Value::String(dt_arg_or_now(args, idx).map(f).unwrap_or("").to_string())
535}
536
537fn bool_field<I: DocIndexLike>(
538 args: &[Value], idx: &I, f: impl FnOnce(Dt) -> bool,
539) -> Value {
540 Value::Boolean(dt_arg_or_now(args, idx).map(f).unwrap_or(false))
541}
542
543fn seconds_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Value {
560 if args.is_empty() {
561 return Value::Number(Numeric::Double(now_dt().to_utc().timestamp() as f64));
562 }
563 let s = value_to_string(&args[0], idx);
564 if let Some(dt) = parse_any(&s) {
565 let utc = dt.to_utc();
566 return Value::Number(Numeric::Double(
567 utc.timestamp() as f64 + utc.timestamp_subsec_nanos() as f64 / 1e9
568 ));
569 }
570 if let Some(dur) = parse_xsd_duration(&s) {
571 const SECONDS_PER_DAY: f64 = 86400.0;
577 const DAYS_PER_MONTH: f64 = 30.4368;
578 let total =
579 dur.months as f64 * DAYS_PER_MONTH * SECONDS_PER_DAY
580 + dur.days as f64 * SECONDS_PER_DAY
581 + dur.seconds as f64
582 + dur.nanos as f64 / 1e9;
583 return Value::Number(Numeric::Double(if dur.negative { -total } else { total }));
584 }
585 Value::Number(Numeric::Double(f64::NAN))
586}
587
588fn add_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Value {
590 if args.len() != 2 { return Value::String(String::new()); }
591 let Some(date) = parse_any(&value_to_string(&args[0], idx)) else {
592 return Value::String(String::new());
593 };
594 let Some(dur) = parse_xsd_duration(&value_to_string(&args[1], idx)) else {
595 return Value::String(String::new());
596 };
597 let m = dur.months_part();
598 let mut nd = date.naive;
600 if m != 0 {
601 let months = chrono::Months::new(m.unsigned_abs() as u32);
602 nd = if m > 0 {
603 nd.checked_add_months(months)
604 } else {
605 nd.checked_sub_months(months)
606 }
607 .unwrap_or(nd); }
609 let new_dt = nd.checked_add_signed(dur.duration_part()).unwrap_or(nd);
610 let result = Dt { naive: new_dt, offset: date.offset };
611 let s = value_to_string(&args[0], idx);
614 let out = if parse_xsd_date(&s).is_some() && !s.contains('T') {
615 format_xsd_date(&result)
616 } else {
617 format_xsd_datetime(&result)
618 };
619 Value::String(out)
620}
621
622fn add_duration_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Value {
627 if args.len() != 2 { return Value::String(String::new()); }
628 let Some(a) = parse_xsd_duration(&value_to_string(&args[0], idx)) else {
629 return Value::String(String::new());
630 };
631 let Some(b) = parse_xsd_duration(&value_to_string(&args[1], idx)) else {
632 return Value::String(String::new());
633 };
634 let sa = if a.negative { -1i64 } else { 1 };
635 let sb = if b.negative { -1i64 } else { 1 };
636 let months = sa * a.months + sb * b.months;
637 let days = sa * a.days + sb * b.days;
638 let seconds = sa * a.seconds + sb * b.seconds;
639 let nanos = sa * a.nanos + sb * b.nanos;
640 let negative = months < 0 || (months == 0 && (days < 0 || (days == 0 && (seconds < 0 || nanos < 0))));
641 let abs_months = months.abs();
642 let abs_days = days.abs();
643 let abs_seconds = seconds.abs();
644 let abs_nanos = nanos.abs();
645 Value::String(format_xsd_duration(&XsdDuration {
646 negative, months: abs_months, days: abs_days, seconds: abs_seconds, nanos: abs_nanos,
647 }))
648}
649
650fn difference_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Value {
653 if args.len() != 2 { return Value::String(String::new()); }
654 let Some(a) = parse_any(&value_to_string(&args[0], idx)) else {
655 return Value::String(String::new());
656 };
657 let Some(b) = parse_any(&value_to_string(&args[1], idx)) else {
658 return Value::String(String::new());
659 };
660 let delta_secs = b.to_utc().signed_duration_since(a.to_utc()).num_seconds();
661 let negative = delta_secs < 0;
662 Value::String(format_xsd_duration(&XsdDuration {
663 negative,
664 months: 0,
665 days: delta_secs.unsigned_abs() as i64 / 86_400,
666 seconds: delta_secs.unsigned_abs() as i64 % 86_400,
667 nanos: 0,
668 }))
669}
670
671fn duration_from_seconds<I: DocIndexLike>(args: &[Value], idx: &I) -> Value {
674 if args.len() != 1 { return Value::String(String::new()); }
675 let n = crate::xpath::eval::value_to_number(&args[0], idx);
676 if !n.is_finite() { return Value::String(String::new()); }
677 let neg = n < 0.0;
678 let abs = n.abs();
679 let secs = abs.trunc() as i64;
680 let nanos = (abs.fract() * 1e9).round() as i64;
681 Value::String(format_xsd_duration(&XsdDuration {
682 negative: neg,
683 months: 0,
684 days: secs / 86_400,
685 seconds: secs % 86_400,
686 nanos,
687 }))
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use crate::xpath::XPathContext;
694 use crate::{parse_str, ParseOptions};
695
696 fn tiny() -> sup_xml_tree::dom::Document {
697 parse_str("<r/>", &ParseOptions::default()).unwrap()
698 }
699 fn s(v: &Value) -> String {
700 if let Value::String(s) = v { s.clone() } else { panic!("expected string, got {v:?}") }
701 }
702 fn n(v: &Value) -> f64 {
703 if let Value::Number(n) = v { n.as_f64() } else { panic!("expected number, got {v:?}") }
704 }
705 fn b(v: &Value) -> bool {
706 if let Value::Boolean(b) = v { *b } else { panic!("expected boolean, got {v:?}") }
707 }
708
709 #[test]
712 fn datetime_parse_basic() {
713 let dt = parse_xsd_datetime("2024-03-15T14:30:45Z").unwrap();
714 assert_eq!(dt.naive.year(), 2024);
715 assert_eq!(dt.naive.month(), 3);
716 assert_eq!(dt.naive.day(), 15);
717 assert_eq!(dt.naive.hour(), 14);
718 assert_eq!(dt.offset, Some(FixedOffset::east_opt(0).unwrap()));
719 }
720
721 #[test]
722 fn datetime_parse_with_offset() {
723 let dt = parse_xsd_datetime("2024-03-15T14:30:45+05:30").unwrap();
724 assert_eq!(dt.offset.unwrap().local_minus_utc(), 5*3600 + 30*60);
725 }
726
727 #[test]
728 fn datetime_parse_fractional_seconds() {
729 let dt = parse_xsd_datetime("2024-03-15T14:30:45.5Z").unwrap();
730 assert_eq!(dt.naive.nanosecond(), 500_000_000);
731 }
732
733 #[test]
734 fn datetime_rejects_year_zero() {
735 assert!(parse_xsd_datetime("0000-01-01T00:00:00Z").is_none());
736 }
737
738 #[test]
739 fn date_parse_negative_year() {
740 let dt = parse_xsd_date("-0044-03-15").unwrap();
741 assert_eq!(dt.naive.year(), -44);
742 }
743
744 #[test]
745 fn duration_parse_full() {
746 let d = parse_xsd_duration("P1Y2M3DT4H5M6S").unwrap();
747 assert_eq!(d.months, 14);
748 assert_eq!(d.days, 3);
749 assert_eq!(d.seconds, 4*3600 + 5*60 + 6);
750 }
751
752 #[test]
753 fn duration_parse_negative_with_fraction() {
754 let d = parse_xsd_duration("-PT1.5S").unwrap();
755 assert!(d.negative);
756 assert_eq!(d.seconds, 1);
757 assert_eq!(d.nanos, 500_000_000);
758 }
759
760 #[test]
761 fn duration_rejects_empty() {
762 assert!(parse_xsd_duration("P").is_none());
763 assert!(parse_xsd_duration("PT").is_none());
764 }
765
766 #[test]
769 fn year_extracts_year_field() {
770 let doc = tiny();
771 let ctx = XPathContext::new(&doc);
772 let v = dispatch("year",
773 vec![Value::String("2024-03-15T00:00:00Z".into())],
774 &ctx.index).unwrap().unwrap();
775 assert_eq!(n(&v), 2024.0);
776 }
777
778 #[test]
779 fn day_in_week_uses_sunday_one_convention() {
780 let doc = tiny();
781 let ctx = XPathContext::new(&doc);
782 let v = dispatch("day-in-week",
784 vec![Value::String("2024-03-17".into())], &ctx.index).unwrap().unwrap();
785 assert_eq!(n(&v), 1.0);
786 let v = dispatch("day-in-week",
788 vec![Value::String("2024-03-16".into())], &ctx.index).unwrap().unwrap();
789 assert_eq!(n(&v), 7.0);
790 }
791
792 #[test]
793 fn day_name_returns_full_name() {
794 let doc = tiny();
795 let ctx = XPathContext::new(&doc);
796 let v = dispatch("day-name",
797 vec![Value::String("2024-03-15".into())], &ctx.index).unwrap().unwrap();
798 assert_eq!(s(&v), "Friday");
799 }
800
801 #[test]
802 fn month_name_returns_full_name() {
803 let doc = tiny();
804 let ctx = XPathContext::new(&doc);
805 let v = dispatch("month-name",
806 vec![Value::String("2024-03-15".into())], &ctx.index).unwrap().unwrap();
807 assert_eq!(s(&v), "March");
808 }
809
810 #[test]
813 fn day_in_year_jan_first_is_one() {
814 let doc = tiny();
815 let ctx = XPathContext::new(&doc);
816 let v = dispatch("day-in-year",
817 vec![Value::String("2024-01-01".into())], &ctx.index).unwrap().unwrap();
818 assert_eq!(n(&v), 1.0);
819 }
820
821 #[test]
822 fn day_in_year_leap_year_dec_31_is_366() {
823 let doc = tiny();
824 let ctx = XPathContext::new(&doc);
825 let v = dispatch("day-in-year",
826 vec![Value::String("2024-12-31".into())], &ctx.index).unwrap().unwrap();
827 assert_eq!(n(&v), 366.0);
828 let v = dispatch("day-in-year",
829 vec![Value::String("2023-12-31".into())], &ctx.index).unwrap().unwrap();
830 assert_eq!(n(&v), 365.0);
831 }
832
833 #[test]
834 fn week_in_year_iso_8601_numbering() {
835 let doc = tiny();
836 let ctx = XPathContext::new(&doc);
837 let v = dispatch("week-in-year",
839 vec![Value::String("2024-01-01".into())], &ctx.index).unwrap().unwrap();
840 assert_eq!(n(&v), 1.0);
841 let v = dispatch("week-in-year",
843 vec![Value::String("2023-01-01".into())], &ctx.index).unwrap().unwrap();
844 assert_eq!(n(&v), 52.0);
845 }
846
847 #[test]
848 fn leap_year_handles_century_exception() {
849 let doc = tiny();
850 let ctx = XPathContext::new(&doc);
851 let v = dispatch("leap-year",
852 vec![Value::String("2024-06-15".into())], &ctx.index).unwrap().unwrap();
853 assert!(b(&v));
854 let v = dispatch("leap-year",
855 vec![Value::String("2023-06-15".into())], &ctx.index).unwrap().unwrap();
856 assert!(!b(&v));
857 let v = dispatch("leap-year",
859 vec![Value::String("1900-06-15".into())], &ctx.index).unwrap().unwrap();
860 assert!(!b(&v));
861 let v = dispatch("leap-year",
863 vec![Value::String("2000-06-15".into())], &ctx.index).unwrap().unwrap();
864 assert!(b(&v));
865 }
866
867 #[test]
868 fn seconds_from_datetime_is_unix_epoch_seconds() {
869 let doc = tiny();
870 let ctx = XPathContext::new(&doc);
871 let v = dispatch("seconds",
872 vec![Value::String("1970-01-01T00:00:00Z".into())], &ctx.index).unwrap().unwrap();
873 assert_eq!(n(&v), 0.0);
874 let v = dispatch("seconds",
875 vec![Value::String("1970-01-01T00:01:30Z".into())], &ctx.index).unwrap().unwrap();
876 assert_eq!(n(&v), 90.0);
877 }
878
879 #[test]
880 fn seconds_from_date_treats_as_midnight_utc() {
881 let doc = tiny();
882 let ctx = XPathContext::new(&doc);
883 let v = dispatch("seconds",
884 vec![Value::String("1970-01-02".into())], &ctx.index).unwrap().unwrap();
885 assert_eq!(n(&v), 86400.0);
886 }
887
888 #[test]
889 fn seconds_from_duration_sums_components() {
890 let doc = tiny();
891 let ctx = XPathContext::new(&doc);
892 let v = dispatch("seconds",
893 vec![Value::String("PT1H30M".into())], &ctx.index).unwrap().unwrap();
894 assert_eq!(n(&v), 5400.0);
895 let v = dispatch("seconds",
896 vec![Value::String("-PT10S".into())], &ctx.index).unwrap().unwrap();
897 assert_eq!(n(&v), -10.0);
898 }
899
900 #[test]
901 fn seconds_bad_input_is_nan() {
902 let doc = tiny();
903 let ctx = XPathContext::new(&doc);
904 let v = dispatch("seconds",
905 vec![Value::String("not a date".into())], &ctx.index).unwrap().unwrap();
906 assert!(n(&v).is_nan());
907 }
908
909 #[test]
912 fn add_one_month_to_jan_31_clips_to_feb_29_in_leap_year() {
913 let doc = tiny();
914 let ctx = XPathContext::new(&doc);
915 let v = dispatch("add",
916 vec![Value::String("2024-01-31".into()), Value::String("P1M".into())],
917 &ctx.index).unwrap().unwrap();
918 assert_eq!(s(&v), "2024-02-29");
919 }
920
921 #[test]
922 fn add_one_month_to_jan_31_clips_to_feb_28_in_non_leap_year() {
923 let doc = tiny();
924 let ctx = XPathContext::new(&doc);
925 let v = dispatch("add",
926 vec![Value::String("2023-01-31".into()), Value::String("P1M".into())],
927 &ctx.index).unwrap().unwrap();
928 assert_eq!(s(&v), "2023-02-28");
929 }
930
931 #[test]
932 fn add_negative_duration_subtracts() {
933 let doc = tiny();
934 let ctx = XPathContext::new(&doc);
935 let v = dispatch("add",
936 vec![Value::String("2024-03-15".into()), Value::String("-P1D".into())],
937 &ctx.index).unwrap().unwrap();
938 assert_eq!(s(&v), "2024-03-14");
939 }
940
941 #[test]
942 fn difference_returns_xsd_duration() {
943 let doc = tiny();
944 let ctx = XPathContext::new(&doc);
945 let v = dispatch("difference",
946 vec![
947 Value::String("2024-01-01T00:00:00Z".into()),
948 Value::String("2024-01-02T00:00:00Z".into()),
949 ], &ctx.index).unwrap().unwrap();
950 assert_eq!(s(&v), "P1D");
951 }
952
953 #[test]
954 fn duration_from_seconds_formats_correctly() {
955 let doc = tiny();
956 let ctx = XPathContext::new(&doc);
957 let v = dispatch("duration",
958 vec![Value::Number(Numeric::Double(90061.5))], &ctx.index).unwrap().unwrap();
959 assert_eq!(s(&v), "P1DT1H1M1.5S");
961 }
962
963 #[test]
966 fn malformed_input_returns_empty_not_error() {
967 let doc = tiny();
968 let ctx = XPathContext::new(&doc);
969 let v = dispatch("year",
970 vec![Value::String("not-a-date".into())], &ctx.index).unwrap().unwrap();
971 assert!(n(&v).is_nan());
973 }
974
975 #[test]
976 fn unknown_function_returns_none() {
977 let doc = tiny();
978 let ctx = XPathContext::new(&doc);
979 assert!(dispatch("nonsense-name", vec![], &ctx.index).is_none());
980 }
981
982 #[test]
985 fn parse_xsd_time_basic() {
986 let dt = parse_xsd_time("14:30:45Z").unwrap();
987 assert_eq!(dt.naive.hour(), 14);
988 assert_eq!(dt.naive.minute(), 30);
989 assert_eq!(dt.naive.second(), 45);
990 assert_eq!(dt.naive.year(), 1970);
992 assert_eq!(dt.naive.month(), 1);
993 assert_eq!(dt.naive.day(), 1);
994 }
995
996 #[test]
997 fn parse_xsd_time_with_fractional_and_offset() {
998 let dt = parse_xsd_time("14:30:45.25+05:30").unwrap();
999 assert_eq!(dt.naive.nanosecond(), 250_000_000);
1000 assert_eq!(dt.offset.unwrap().local_minus_utc(), 5*3600 + 30*60);
1001 }
1002
1003 #[test]
1004 fn parse_xsd_time_no_offset() {
1005 let dt = parse_xsd_time("14:30:45").unwrap();
1006 assert!(dt.offset.is_none());
1007 }
1008
1009 #[test]
1010 fn parse_xsd_time_rejects_garbage() {
1011 assert!(parse_xsd_time("14:30").is_none());
1012 assert!(parse_xsd_time("14:30:45Zextra").is_none());
1013 assert!(parse_xsd_time("99:30:45").is_none());
1014 }
1015
1016 #[test]
1019 fn parse_xsd_duration_rejects_bad_designator_in_date_part() {
1020 assert!(parse_xsd_duration("P1X").is_none());
1022 }
1023
1024 #[test]
1025 fn parse_xsd_duration_rejects_bad_designator_in_time_part() {
1026 assert!(parse_xsd_duration("PT1X").is_none());
1028 }
1029
1030 #[test]
1033 fn format_xsd_datetime_round_trip() {
1034 let dt = parse_xsd_datetime("2024-03-15T14:30:45Z").unwrap();
1036 assert_eq!(format_xsd_datetime(&dt), "2024-03-15T14:30:45Z");
1037 }
1038
1039 #[test]
1040 fn format_xsd_datetime_with_fraction_and_offset() {
1041 let dt = parse_xsd_datetime("2024-03-15T14:30:45.5+05:30").unwrap();
1042 let out = format_xsd_datetime(&dt);
1043 assert!(out.starts_with("2024-03-15T14:30:45.5"));
1044 assert!(out.ends_with("+05:30"));
1045 }
1046
1047 #[test]
1048 fn format_xsd_datetime_no_offset() {
1049 let dt = parse_xsd_datetime("2024-03-15T14:30:45").unwrap();
1050 let out = format_xsd_datetime(&dt);
1052 assert_eq!(out, "2024-03-15T14:30:45");
1053 }
1054
1055 #[test]
1056 fn format_xsd_time_via_dispatch() {
1057 let doc = tiny();
1058 let ctx = XPathContext::new(&doc);
1059 let v = dispatch("time",
1060 vec![Value::String("2024-03-15T14:30:45.5Z".into())],
1061 &ctx.index).unwrap().unwrap();
1062 let out = s(&v);
1063 assert!(out.starts_with("14:30:45"));
1064 assert!(out.contains('Z'));
1065 }
1066
1067 #[test]
1068 fn format_offset_zero_is_z() {
1069 let off = FixedOffset::east_opt(0).unwrap();
1070 assert_eq!(format_offset(off), "Z");
1071 }
1072
1073 #[test]
1074 fn format_offset_negative() {
1075 let off = FixedOffset::west_opt(5 * 3600).unwrap();
1076 assert_eq!(format_offset(off), "-05:00");
1077 }
1078
1079 #[test]
1080 fn format_offset_positive_non_aligned() {
1081 let off = FixedOffset::east_opt(5 * 3600 + 30 * 60).unwrap();
1082 assert_eq!(format_offset(off), "+05:30");
1083 }
1084
1085 #[test]
1086 fn format_xsd_duration_zero_emits_pt0s() {
1087 let d = XsdDuration {
1088 negative: false, months: 0, days: 0, seconds: 0, nanos: 0,
1089 };
1090 let out = format_xsd_duration(&d);
1091 assert_eq!(out, "PT0S");
1092 }
1093
1094 #[test]
1095 fn format_xsd_duration_seconds_only() {
1096 let d = XsdDuration {
1097 negative: false, months: 0, days: 0, seconds: 45, nanos: 0,
1098 };
1099 assert_eq!(format_xsd_duration(&d), "PT45S");
1100 }
1101
1102 #[test]
1105 fn parse_optional_tz_negative_offset() {
1106 let dt = parse_xsd_datetime("2024-03-15T14:30:45-08:00").unwrap();
1107 assert_eq!(dt.offset.unwrap().local_minus_utc(), -8 * 3600);
1108 }
1109
1110 #[test]
1113 fn date_time_dispatch_with_no_args_returns_current_time() {
1114 let doc = tiny();
1115 let ctx = XPathContext::new(&doc);
1116 let v = dispatch("date-time", vec![], &ctx.index).unwrap().unwrap();
1117 let out = s(&v);
1118 assert!(parse_xsd_datetime(&out).is_some(), "got {out:?}");
1120 }
1121
1122 #[test]
1123 fn date_time_dispatch_rejects_args() {
1124 let doc = tiny();
1125 let ctx = XPathContext::new(&doc);
1126 let r = dispatch("date-time",
1127 vec![Value::String("ignored".into())], &ctx.index).unwrap();
1128 assert!(r.is_err());
1129 }
1130
1131 #[test]
1134 fn year_with_no_args_returns_current_year() {
1135 let doc = tiny();
1136 let ctx = XPathContext::new(&doc);
1137 let v = dispatch("year", vec![], &ctx.index).unwrap().unwrap();
1138 let y = n(&v);
1139 assert!(y >= 2020.0 && y <= 3000.0, "got {y}");
1141 }
1142
1143 #[test]
1146 fn second_in_minute_includes_fraction() {
1147 let doc = tiny();
1148 let ctx = XPathContext::new(&doc);
1149 let v = dispatch("second-in-minute",
1150 vec![Value::String("2024-03-15T14:30:45.25Z".into())],
1151 &ctx.index).unwrap().unwrap();
1152 let got = n(&v);
1153 assert!((got - 45.25).abs() < 1e-9, "got {got}");
1154 }
1155
1156 #[test]
1159 fn all_weekday_names() {
1160 let doc = tiny();
1161 let ctx = XPathContext::new(&doc);
1162 let cases = [
1164 ("2024-03-11", "Monday", "Mon"),
1165 ("2024-03-12", "Tuesday", "Tue"),
1166 ("2024-03-13", "Wednesday", "Wed"),
1167 ("2024-03-14", "Thursday", "Thu"),
1168 ("2024-03-15", "Friday", "Fri"),
1169 ("2024-03-16", "Saturday", "Sat"),
1170 ("2024-03-17", "Sunday", "Sun"),
1171 ];
1172 for (date, full, abbr) in cases {
1173 let v = dispatch("day-name",
1174 vec![Value::String(date.into())], &ctx.index).unwrap().unwrap();
1175 assert_eq!(s(&v), full, "day-name for {date}");
1176 let v = dispatch("day-abbreviation",
1177 vec![Value::String(date.into())], &ctx.index).unwrap().unwrap();
1178 assert_eq!(s(&v), abbr, "day-abbreviation for {date}");
1179 }
1180 }
1181
1182 #[test]
1183 fn all_month_names() {
1184 let doc = tiny();
1185 let ctx = XPathContext::new(&doc);
1186 let names = ["January","February","March","April","May","June",
1187 "July","August","September","October","November","December"];
1188 let abbrs = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
1189 for (i, (name, abbr)) in names.iter().zip(abbrs.iter()).enumerate() {
1190 let date = format!("2024-{:02}-15", i + 1);
1191 let v = dispatch("month-name",
1192 vec![Value::String(date.clone())], &ctx.index).unwrap().unwrap();
1193 assert_eq!(s(&v), *name, "month {} ({date})", i + 1);
1194 let v = dispatch("month-abbreviation",
1195 vec![Value::String(date)], &ctx.index).unwrap().unwrap();
1196 assert_eq!(s(&v), *abbr);
1197 }
1198 }
1199
1200 #[test]
1203 fn day_of_week_in_month_counts_weekday_occurrences() {
1204 let doc = tiny();
1205 let ctx = XPathContext::new(&doc);
1206 let cases = [
1207 ("2024-03-01", 1.0), ("2024-03-07", 1.0), ("2024-03-08", 2.0), ("2024-03-15", 3.0), ("2024-03-22", 4.0), ("2024-03-29", 5.0), ];
1214 for (date, expected) in cases {
1215 let v = dispatch("day-of-week-in-month",
1216 vec![Value::String(date.into())], &ctx.index).unwrap().unwrap();
1217 assert_eq!(n(&v), expected, "{date}");
1218 }
1219 }
1220
1221 #[test]
1224 fn add_duration_combines_two_positive_durations() {
1225 let doc = tiny();
1226 let ctx = XPathContext::new(&doc);
1227 let v = dispatch("add-duration",
1228 vec![Value::String("P1Y".into()), Value::String("P6M".into())],
1229 &ctx.index).unwrap().unwrap();
1230 assert_eq!(s(&v), "P1Y6M");
1231 }
1232
1233 #[test]
1234 fn add_duration_with_negative_yields_partial_cancellation() {
1235 let doc = tiny();
1236 let ctx = XPathContext::new(&doc);
1237 let v = dispatch("add-duration",
1239 vec![Value::String("P1Y".into()), Value::String("-P6M".into())],
1240 &ctx.index).unwrap().unwrap();
1241 assert_eq!(s(&v), "P6M");
1242 }
1243
1244 #[test]
1245 fn add_duration_rejects_non_duration_args() {
1246 let doc = tiny();
1247 let ctx = XPathContext::new(&doc);
1248 let v = dispatch("add-duration",
1249 vec![Value::String("not-a-duration".into()), Value::String("P1M".into())],
1250 &ctx.index).unwrap().unwrap();
1251 assert_eq!(s(&v), "");
1252 }
1253
1254 #[test]
1255 fn add_duration_wrong_argc_returns_empty() {
1256 let doc = tiny();
1257 let ctx = XPathContext::new(&doc);
1258 let v = dispatch("add-duration",
1259 vec![Value::String("P1Y".into())], &ctx.index).unwrap().unwrap();
1260 assert_eq!(s(&v), "");
1261 }
1262
1263 #[test]
1266 fn add_emits_datetime_when_input_has_time_part() {
1267 let doc = tiny();
1268 let ctx = XPathContext::new(&doc);
1269 let v = dispatch("add",
1270 vec![Value::String("2024-03-15T12:00:00Z".into()),
1271 Value::String("P1D".into())],
1272 &ctx.index).unwrap().unwrap();
1273 let out = s(&v);
1274 assert!(out.contains('T'), "got {out}");
1275 }
1276
1277 #[test]
1278 fn add_wrong_argc_returns_empty() {
1279 let doc = tiny();
1280 let ctx = XPathContext::new(&doc);
1281 let v = dispatch("add",
1282 vec![Value::String("2024-01-01".into())], &ctx.index).unwrap().unwrap();
1283 assert_eq!(s(&v), "");
1284 }
1285
1286 #[test]
1287 fn add_with_malformed_date_returns_empty() {
1288 let doc = tiny();
1289 let ctx = XPathContext::new(&doc);
1290 let v = dispatch("add",
1291 vec![Value::String("not-a-date".into()),
1292 Value::String("P1D".into())],
1293 &ctx.index).unwrap().unwrap();
1294 assert_eq!(s(&v), "");
1295 }
1296
1297 #[test]
1298 fn add_with_malformed_duration_returns_empty() {
1299 let doc = tiny();
1300 let ctx = XPathContext::new(&doc);
1301 let v = dispatch("add",
1302 vec![Value::String("2024-01-01".into()),
1303 Value::String("not-a-duration".into())],
1304 &ctx.index).unwrap().unwrap();
1305 assert_eq!(s(&v), "");
1306 }
1307
1308 #[test]
1311 fn difference_negative_when_end_before_start() {
1312 let doc = tiny();
1313 let ctx = XPathContext::new(&doc);
1314 let v = dispatch("difference",
1315 vec![Value::String("2024-01-02T00:00:00Z".into()),
1316 Value::String("2024-01-01T00:00:00Z".into())],
1317 &ctx.index).unwrap().unwrap();
1318 assert!(s(&v).starts_with('-'));
1319 }
1320
1321 #[test]
1322 fn difference_wrong_argc_returns_empty() {
1323 let doc = tiny();
1324 let ctx = XPathContext::new(&doc);
1325 let v = dispatch("difference",
1326 vec![Value::String("2024-01-01".into())], &ctx.index).unwrap().unwrap();
1327 assert_eq!(s(&v), "");
1328 }
1329
1330 #[test]
1331 fn difference_malformed_args_return_empty() {
1332 let doc = tiny();
1333 let ctx = XPathContext::new(&doc);
1334 let v = dispatch("difference",
1335 vec![Value::String("oops".into()),
1336 Value::String("2024-01-01T00:00:00Z".into())],
1337 &ctx.index).unwrap().unwrap();
1338 assert_eq!(s(&v), "");
1339 let v = dispatch("difference",
1340 vec![Value::String("2024-01-01T00:00:00Z".into()),
1341 Value::String("oops".into())],
1342 &ctx.index).unwrap().unwrap();
1343 assert_eq!(s(&v), "");
1344 }
1345
1346 #[test]
1349 fn duration_negative_seconds() {
1350 let doc = tiny();
1351 let ctx = XPathContext::new(&doc);
1352 let v = dispatch("duration",
1353 vec![Value::Number(Numeric::Double(-90.0))], &ctx.index).unwrap().unwrap();
1354 assert!(s(&v).starts_with('-'));
1355 }
1356
1357 #[test]
1358 fn duration_wrong_argc_returns_empty() {
1359 let doc = tiny();
1360 let ctx = XPathContext::new(&doc);
1361 let v = dispatch("duration", vec![], &ctx.index).unwrap().unwrap();
1362 assert_eq!(s(&v), "");
1363 }
1364
1365 #[test]
1366 fn duration_non_finite_returns_empty() {
1367 let doc = tiny();
1368 let ctx = XPathContext::new(&doc);
1369 let v = dispatch("duration",
1370 vec![Value::Number(Numeric::Double(f64::NAN))], &ctx.index).unwrap().unwrap();
1371 assert_eq!(s(&v), "");
1372 let v = dispatch("duration",
1373 vec![Value::Number(Numeric::Double(f64::INFINITY))], &ctx.index).unwrap().unwrap();
1374 assert_eq!(s(&v), "");
1375 }
1376
1377 #[test]
1380 fn extract_individual_fields() {
1381 let doc = tiny();
1382 let ctx = XPathContext::new(&doc);
1383 let dt = "2024-03-15T14:30:45Z";
1384 assert_eq!(n(&dispatch("month-in-year",
1385 vec![Value::String(dt.into())], &ctx.index).unwrap().unwrap()), 3.0);
1386 assert_eq!(n(&dispatch("day-in-month",
1387 vec![Value::String(dt.into())], &ctx.index).unwrap().unwrap()), 15.0);
1388 assert_eq!(n(&dispatch("hour-in-day",
1389 vec![Value::String(dt.into())], &ctx.index).unwrap().unwrap()), 14.0);
1390 assert_eq!(n(&dispatch("minute-in-hour",
1391 vec![Value::String(dt.into())], &ctx.index).unwrap().unwrap()), 30.0);
1392 }
1393
1394 #[test]
1397 fn date_dispatch_extracts_date_part_from_datetime() {
1398 let doc = tiny();
1399 let ctx = XPathContext::new(&doc);
1400 let v = dispatch("date",
1401 vec![Value::String("2024-03-15T14:30:45Z".into())],
1402 &ctx.index).unwrap().unwrap();
1403 assert!(s(&v).starts_with("2024-03-15"));
1404 }
1405}