use chrono::Local;
use num_bigint::BigInt;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use crate::types::value::{
DateTimeValue, DateValue, DayTimeDurationValue, DurationValue, TimeValue, TimezoneOffset,
XmlAtomicValue, XmlValue, XmlValueKind, YearMonthDurationValue,
};
use crate::types::XmlTypeCode;
use crate::xpath::context::DynamicContext;
use crate::xpath::error::XPathError;
use crate::xpath::DomNavigator;
use super::{atomize_to_single_opt, XPathValue};
fn get_implicit_timezone_offset() -> TimezoneOffset {
let seconds = Local::now().offset().local_minus_utc();
TimezoneOffset((seconds / 60) as i16)
}
fn timezone_to_day_time_duration(tz: TimezoneOffset) -> DayTimeDurationValue {
let total_minutes = tz.0.unsigned_abs() as u32;
let hours = total_minutes / 60;
let minutes = total_minutes % 60;
DayTimeDurationValue {
negative: tz.0 < 0,
days: 0,
hours,
minutes,
seconds: Decimal::ZERO,
}
}
fn day_time_duration_to_timezone(dur: &DayTimeDurationValue) -> Result<TimezoneOffset, XPathError> {
if dur.days != 0 {
return Err(XPathError::FODT0003 {
value: format_day_time_duration(dur),
});
}
if dur.seconds != Decimal::ZERO {
return Err(XPathError::FODT0003 {
value: format_day_time_duration(dur),
});
}
let total_minutes = dur.hours as i64 * 60 + dur.minutes as i64;
let signed_minutes = if dur.negative {
-total_minutes
} else {
total_minutes
};
if !(-840..=840).contains(&signed_minutes) {
return Err(XPathError::FODT0003 {
value: format_day_time_duration(dur),
});
}
Ok(TimezoneOffset(signed_minutes as i16))
}
fn format_day_time_duration(dur: &DayTimeDurationValue) -> String {
let mut s = String::new();
if dur.negative {
s.push('-');
}
s.push_str("PT");
if dur.days != 0 {
s.push_str(&format!("{}D", dur.days));
}
if dur.hours != 0 {
s.push_str(&format!("{}H", dur.hours));
}
if dur.minutes != 0 {
s.push_str(&format!("{}M", dur.minutes));
}
if dur.seconds != Decimal::ZERO {
s.push_str(&format!("{}S", dur.seconds));
}
if s.len() == 2 || (s.len() == 3 && s.starts_with('-')) {
s.push_str("0S");
}
s
}
fn validate_timezone_offset(minutes: i16) -> Result<(), XPathError> {
if !(-840..=840).contains(&minutes) {
return Err(XPathError::FODT0003 {
value: format!("{}:{:02}", minutes / 60, (minutes % 60).abs()),
});
}
Ok(())
}
fn as_datetime(value: &XmlValue) -> Option<&DateTimeValue> {
match &value.value {
XmlValueKind::Atomic(XmlAtomicValue::DateTime(v)) => Some(v),
_ => None,
}
}
fn as_date(value: &XmlValue) -> Option<&DateValue> {
match &value.value {
XmlValueKind::Atomic(XmlAtomicValue::Date(v)) => Some(v),
_ => None,
}
}
fn as_time(value: &XmlValue) -> Option<&TimeValue> {
match &value.value {
XmlValueKind::Atomic(XmlAtomicValue::Time(v)) => Some(v),
_ => None,
}
}
fn as_duration(value: &XmlValue) -> Option<&DurationValue> {
match &value.value {
XmlValueKind::Atomic(XmlAtomicValue::Duration(v)) => Some(v),
_ => None,
}
}
fn as_year_month_duration(value: &XmlValue) -> Option<&YearMonthDurationValue> {
match &value.value {
XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(v)) => Some(v),
_ => None,
}
}
fn as_day_time_duration(value: &XmlValue) -> Option<&DayTimeDurationValue> {
match &value.value {
XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(v)) => Some(v),
_ => None,
}
}
fn is_duration_type(code: XmlTypeCode) -> bool {
matches!(
code,
XmlTypeCode::Duration | XmlTypeCode::YearMonthDuration | XmlTypeCode::DayTimeDuration
)
}
fn xml_integer(i: i64) -> XmlValue {
XmlValue {
type_code: XmlTypeCode::Integer,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(i))),
}
}
fn xml_decimal(d: Decimal) -> XmlValue {
XmlValue {
type_code: XmlTypeCode::Decimal,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::Decimal(d)),
}
}
fn xml_day_time_duration(value: DayTimeDurationValue) -> XmlValue {
XmlValue {
type_code: XmlTypeCode::DayTimeDuration,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(value)),
}
}
fn xml_datetime(value: DateTimeValue) -> XmlValue {
XmlValue {
type_code: XmlTypeCode::DateTime,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::DateTime(value)),
}
}
fn xml_date(value: DateValue) -> XmlValue {
XmlValue {
type_code: XmlTypeCode::Date,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::Date(value)),
}
}
fn xml_time(value: TimeValue) -> XmlValue {
XmlValue {
type_code: XmlTypeCode::Time,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::Time(value)),
}
}
pub fn current_datetime<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if !args.is_empty() {
return Err(XPathError::wrong_number_of_arguments(
"current-dateTime",
0,
args.len(),
));
}
let dt = if let Some(ref cached) = context.current_datetime {
cached.clone()
} else {
let now = Local::now();
let tz_offset = context
.implicit_timezone
.unwrap_or_else(get_implicit_timezone_offset);
let secs = now.format("%S").to_string().parse::<u32>().unwrap_or(0);
let nanos = now.timestamp_subsec_nanos();
let second = Decimal::from(secs) + Decimal::from(nanos) / Decimal::from(1_000_000_000u64);
let dt = DateTimeValue {
year: now.format("%Y").to_string().parse().unwrap_or(2000),
month: now.format("%m").to_string().parse().unwrap_or(1),
day: now.format("%d").to_string().parse().unwrap_or(1),
hour: now.format("%H").to_string().parse().unwrap_or(0),
minute: now.format("%M").to_string().parse().unwrap_or(0),
second,
timezone: Some(tz_offset),
};
context.current_datetime = Some(dt.clone());
dt
};
Ok(XPathValue::from_atomic(xml_datetime(dt)))
}
pub fn current_date<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if !args.is_empty() {
return Err(XPathError::wrong_number_of_arguments(
"current-date",
0,
args.len(),
));
}
let dt_value = current_datetime(context, vec![])?;
let dt = match dt_value {
XPathValue::Item(item) => {
if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
as_datetime(&v).cloned()
} else {
None
}
}
_ => None,
};
let dt = dt.ok_or_else(|| XPathError::internal("Failed to get current dateTime"))?;
let date = DateValue {
year: dt.year,
month: dt.month,
day: dt.day,
timezone: dt.timezone,
};
Ok(XPathValue::from_atomic(xml_date(date)))
}
pub fn current_time<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if !args.is_empty() {
return Err(XPathError::wrong_number_of_arguments(
"current-time",
0,
args.len(),
));
}
let dt_value = current_datetime(context, vec![])?;
let dt = match dt_value {
XPathValue::Item(item) => {
if let crate::xpath::iterator::XmlItem::Atomic(v) = item {
as_datetime(&v).cloned()
} else {
None
}
}
_ => None,
};
let dt = dt.ok_or_else(|| XPathError::internal("Failed to get current dateTime"))?;
let time = TimeValue {
hour: dt.hour,
minute: dt.minute,
second: dt.second,
timezone: dt.timezone,
};
Ok(XPathValue::from_atomic(xml_time(time)))
}
pub fn implicit_timezone<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if !args.is_empty() {
return Err(XPathError::wrong_number_of_arguments(
"implicit-timezone",
0,
args.len(),
));
}
let tz = context
.implicit_timezone
.unwrap_or_else(get_implicit_timezone_offset);
let duration = timezone_to_day_time_duration(tz);
Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
}
fn normalized_years_from_ym(years: u32, months: u32, negative: bool) -> i64 {
let total_months = years as i64 * 12 + months as i64;
let years_component = total_months / 12;
if negative {
-years_component
} else {
years_component
}
}
fn normalized_months_from_ym(years: u32, months: u32, negative: bool) -> i64 {
let total_months = years as i64 * 12 + months as i64;
let months_component = total_months % 12;
if negative {
-months_component
} else {
months_component
}
}
fn normalized_days_from_dt(
days: u32,
hours: u32,
minutes: u32,
seconds: Decimal,
negative: bool,
) -> i64 {
let total_seconds = Decimal::from(days) * Decimal::from(86400)
+ Decimal::from(hours) * Decimal::from(3600)
+ Decimal::from(minutes) * Decimal::from(60)
+ seconds;
let days_component = (total_seconds / Decimal::from(86400))
.floor()
.to_i64()
.unwrap_or(0);
if negative {
-days_component
} else {
days_component
}
}
fn normalized_hours_from_dt(
days: u32,
hours: u32,
minutes: u32,
seconds: Decimal,
negative: bool,
) -> i64 {
let total_seconds = Decimal::from(days) * Decimal::from(86400)
+ Decimal::from(hours) * Decimal::from(3600)
+ Decimal::from(minutes) * Decimal::from(60)
+ seconds;
let remainder_after_days = total_seconds % Decimal::from(86400);
let hours_component = (remainder_after_days / Decimal::from(3600))
.floor()
.to_i64()
.unwrap_or(0);
if negative {
-hours_component
} else {
hours_component
}
}
fn normalized_minutes_from_dt(
days: u32,
hours: u32,
minutes: u32,
seconds: Decimal,
negative: bool,
) -> i64 {
let total_seconds = Decimal::from(days) * Decimal::from(86400)
+ Decimal::from(hours) * Decimal::from(3600)
+ Decimal::from(minutes) * Decimal::from(60)
+ seconds;
let remainder_after_hours = total_seconds % Decimal::from(3600);
let minutes_component = (remainder_after_hours / Decimal::from(60))
.floor()
.to_i64()
.unwrap_or(0);
if negative {
-minutes_component
} else {
minutes_component
}
}
fn normalized_seconds_from_dt(
days: u32,
hours: u32,
minutes: u32,
seconds: Decimal,
negative: bool,
) -> Decimal {
let total_seconds = Decimal::from(days) * Decimal::from(86400)
+ Decimal::from(hours) * Decimal::from(3600)
+ Decimal::from(minutes) * Decimal::from(60)
+ seconds;
let seconds_component = total_seconds % Decimal::from(60);
if negative {
-seconds_component
} else {
seconds_component
}
}
pub fn years_from_duration<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"years-from-duration",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
if !is_duration_type(value.type_code) {
return Err(XPathError::XPTY0004 {
expected: "xs:duration".to_string(),
found: format!("{:?}", value.type_code),
});
}
let result = if let Some(dur) = as_duration(&value) {
normalized_years_from_ym(dur.years, dur.months, dur.negative)
} else if let Some(ymd) = as_year_month_duration(&value) {
normalized_years_from_ym(ymd.years, ymd.months, ymd.negative)
} else if as_day_time_duration(&value).is_some() {
0
} else {
return Err(XPathError::internal("Unexpected duration type"));
};
Ok(XPathValue::from_atomic(xml_integer(result)))
}
pub fn months_from_duration<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"months-from-duration",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
if !is_duration_type(value.type_code) {
return Err(XPathError::XPTY0004 {
expected: "xs:duration".to_string(),
found: format!("{:?}", value.type_code),
});
}
let result = if let Some(dur) = as_duration(&value) {
normalized_months_from_ym(dur.years, dur.months, dur.negative)
} else if let Some(ymd) = as_year_month_duration(&value) {
normalized_months_from_ym(ymd.years, ymd.months, ymd.negative)
} else if as_day_time_duration(&value).is_some() {
0
} else {
return Err(XPathError::internal("Unexpected duration type"));
};
Ok(XPathValue::from_atomic(xml_integer(result)))
}
pub fn days_from_duration<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"days-from-duration",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
if !is_duration_type(value.type_code) {
return Err(XPathError::XPTY0004 {
expected: "xs:duration".to_string(),
found: format!("{:?}", value.type_code),
});
}
let result = if let Some(dur) = as_duration(&value) {
normalized_days_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
} else if as_year_month_duration(&value).is_some() {
0
} else if let Some(dtd) = as_day_time_duration(&value) {
normalized_days_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
} else {
return Err(XPathError::internal("Unexpected duration type"));
};
Ok(XPathValue::from_atomic(xml_integer(result)))
}
pub fn hours_from_duration<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"hours-from-duration",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
if !is_duration_type(value.type_code) {
return Err(XPathError::XPTY0004 {
expected: "xs:duration".to_string(),
found: format!("{:?}", value.type_code),
});
}
let result = if let Some(dur) = as_duration(&value) {
normalized_hours_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
} else if as_year_month_duration(&value).is_some() {
0
} else if let Some(dtd) = as_day_time_duration(&value) {
normalized_hours_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
} else {
return Err(XPathError::internal("Unexpected duration type"));
};
Ok(XPathValue::from_atomic(xml_integer(result)))
}
pub fn minutes_from_duration<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"minutes-from-duration",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
if !is_duration_type(value.type_code) {
return Err(XPathError::XPTY0004 {
expected: "xs:duration".to_string(),
found: format!("{:?}", value.type_code),
});
}
let result = if let Some(dur) = as_duration(&value) {
normalized_minutes_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
} else if as_year_month_duration(&value).is_some() {
0
} else if let Some(dtd) = as_day_time_duration(&value) {
normalized_minutes_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
} else {
return Err(XPathError::internal("Unexpected duration type"));
};
Ok(XPathValue::from_atomic(xml_integer(result)))
}
pub fn seconds_from_duration<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"seconds-from-duration",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
if !is_duration_type(value.type_code) {
return Err(XPathError::XPTY0004 {
expected: "xs:duration".to_string(),
found: format!("{:?}", value.type_code),
});
}
let result = if let Some(dur) = as_duration(&value) {
normalized_seconds_from_dt(dur.days, dur.hours, dur.minutes, dur.seconds, dur.negative)
} else if as_year_month_duration(&value).is_some() {
Decimal::ZERO
} else if let Some(dtd) = as_day_time_duration(&value) {
normalized_seconds_from_dt(dtd.days, dtd.hours, dtd.minutes, dtd.seconds, dtd.negative)
} else {
return Err(XPathError::internal("Unexpected duration type"));
};
Ok(XPathValue::from_atomic(xml_decimal(result)))
}
pub fn year_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"year-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(dt.year as i64)))
}
pub fn month_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"month-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(dt.month as i64)))
}
pub fn day_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"day-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(dt.day as i64)))
}
pub fn hours_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"hours-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(dt.hour as i64)))
}
pub fn minutes_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"minutes-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(dt.minute as i64)))
}
pub fn seconds_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"seconds-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_decimal(dt.second)))
}
pub fn timezone_from_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"timezone-from-dateTime",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", value.type_code),
})?;
match dt.timezone {
Some(tz) => {
let duration = timezone_to_day_time_duration(tz);
Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
}
None => Ok(XPathValue::Empty),
}
}
pub fn year_from_date<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"year-from-date",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:date".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(date.year as i64)))
}
pub fn month_from_date<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"month-from-date",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:date".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(date.month as i64)))
}
pub fn day_from_date<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"day-from-date",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:date".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(date.day as i64)))
}
pub fn timezone_from_date<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"timezone-from-date",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let date = as_date(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:date".to_string(),
found: format!("{:?}", value.type_code),
})?;
match date.timezone {
Some(tz) => {
let duration = timezone_to_day_time_duration(tz);
Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
}
None => Ok(XPathValue::Empty),
}
}
pub fn hours_from_time<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"hours-from-time",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:time".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(time.hour as i64)))
}
pub fn minutes_from_time<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"minutes-from-time",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:time".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_integer(time.minute as i64)))
}
pub fn seconds_from_time<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"seconds-from-time",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:time".to_string(),
found: format!("{:?}", value.type_code),
})?;
Ok(XPathValue::from_atomic(xml_decimal(time.second)))
}
pub fn timezone_from_time<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 1 {
return Err(XPathError::wrong_number_of_arguments(
"timezone-from-time",
1,
args.len(),
));
}
let arg = args.remove(0);
let value = match atomize_to_single_opt(arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let time = as_time(&value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:time".to_string(),
found: format!("{:?}", value.type_code),
})?;
match time.timezone {
Some(tz) => {
let duration = timezone_to_day_time_duration(tz);
Ok(XPathValue::from_atomic(xml_day_time_duration(duration)))
}
None => Ok(XPathValue::Empty),
}
}
pub fn create_datetime<N: DomNavigator>(
_context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() != 2 {
return Err(XPathError::wrong_number_of_arguments(
"dateTime",
2,
args.len(),
));
}
let time_arg = args.remove(1);
let date_arg = args.remove(0);
let date_value = match atomize_to_single_opt(date_arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let time_value = match atomize_to_single_opt(time_arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let date = as_date(&date_value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:date".to_string(),
found: format!("{:?}", date_value.type_code),
})?;
let time = as_time(&time_value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:time".to_string(),
found: format!("{:?}", time_value.type_code),
})?;
let timezone = match (date.timezone, time.timezone) {
(Some(date_tz), Some(time_tz)) => {
if date_tz.0 != time_tz.0 {
return Err(XPathError::FORG0008);
}
Some(date_tz)
}
(Some(tz), None) => Some(tz),
(None, Some(tz)) => Some(tz),
(None, None) => None,
};
let hour = if time.hour == 24 { 0 } else { time.hour };
let result = DateTimeValue {
year: date.year,
month: date.month,
day: date.day,
hour,
minute: time.minute,
second: time.second,
timezone,
};
Ok(XPathValue::from_atomic(xml_datetime(result)))
}
pub fn adjust_datetime_to_timezone<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.is_empty() || args.len() > 2 {
return Err(XPathError::wrong_number_of_arguments(
"adjust-dateTime-to-timezone",
1,
args.len(),
));
}
let tz_arg = if args.len() == 2 {
Some(args.remove(1))
} else {
None
};
let dt_arg = args.remove(0);
let dt_value = match atomize_to_single_opt(dt_arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let dt = as_datetime(&dt_value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dateTime".to_string(),
found: format!("{:?}", dt_value.type_code),
})?;
let target_tz = if let Some(tz_val) = tz_arg {
match atomize_to_single_opt(tz_val)? {
None => {
let result = DateTimeValue {
year: dt.year,
month: dt.month,
day: dt.day,
hour: dt.hour,
minute: dt.minute,
second: dt.second,
timezone: None,
};
return Ok(XPathValue::from_atomic(xml_datetime(result)));
}
Some(v) => {
let duration = as_day_time_duration(&v).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dayTimeDuration".to_string(),
found: format!("{:?}", v.type_code),
})?;
day_time_duration_to_timezone(duration)?
}
}
} else {
context
.implicit_timezone
.unwrap_or_else(get_implicit_timezone_offset)
};
validate_timezone_offset(target_tz.0)?;
let result = match dt.timezone {
None => {
DateTimeValue {
year: dt.year,
month: dt.month,
day: dt.day,
hour: dt.hour,
minute: dt.minute,
second: dt.second,
timezone: Some(target_tz),
}
}
Some(source_tz) => {
let offset_diff = target_tz.0 - source_tz.0;
adjust_datetime_by_minutes(dt, offset_diff, target_tz)?
}
};
Ok(XPathValue::from_atomic(xml_datetime(result)))
}
pub fn adjust_date_to_timezone<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.is_empty() || args.len() > 2 {
return Err(XPathError::wrong_number_of_arguments(
"adjust-date-to-timezone",
1,
args.len(),
));
}
let tz_arg = if args.len() == 2 {
Some(args.remove(1))
} else {
None
};
let date_arg = args.remove(0);
let date_value = match atomize_to_single_opt(date_arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let date = as_date(&date_value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:date".to_string(),
found: format!("{:?}", date_value.type_code),
})?;
let target_tz = if let Some(tz_val) = tz_arg {
match atomize_to_single_opt(tz_val)? {
None => {
let result = DateValue {
year: date.year,
month: date.month,
day: date.day,
timezone: None,
};
return Ok(XPathValue::from_atomic(xml_date(result)));
}
Some(v) => {
let duration = as_day_time_duration(&v).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dayTimeDuration".to_string(),
found: format!("{:?}", v.type_code),
})?;
day_time_duration_to_timezone(duration)?
}
}
} else {
context
.implicit_timezone
.unwrap_or_else(get_implicit_timezone_offset)
};
validate_timezone_offset(target_tz.0)?;
let result = match date.timezone {
None => {
DateValue {
year: date.year,
month: date.month,
day: date.day,
timezone: Some(target_tz),
}
}
Some(source_tz) => {
let offset_diff = target_tz.0 - source_tz.0;
adjust_date_by_minutes(date, offset_diff, target_tz)?
}
};
Ok(XPathValue::from_atomic(xml_date(result)))
}
pub fn adjust_time_to_timezone<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.is_empty() || args.len() > 2 {
return Err(XPathError::wrong_number_of_arguments(
"adjust-time-to-timezone",
1,
args.len(),
));
}
let tz_arg = if args.len() == 2 {
Some(args.remove(1))
} else {
None
};
let time_arg = args.remove(0);
let time_value = match atomize_to_single_opt(time_arg)? {
None => return Ok(XPathValue::Empty),
Some(v) => v,
};
let time = as_time(&time_value).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:time".to_string(),
found: format!("{:?}", time_value.type_code),
})?;
let target_tz = if let Some(tz_val) = tz_arg {
match atomize_to_single_opt(tz_val)? {
None => {
let result = TimeValue {
hour: time.hour,
minute: time.minute,
second: time.second,
timezone: None,
};
return Ok(XPathValue::from_atomic(xml_time(result)));
}
Some(v) => {
let duration = as_day_time_duration(&v).ok_or_else(|| XPathError::XPTY0004 {
expected: "xs:dayTimeDuration".to_string(),
found: format!("{:?}", v.type_code),
})?;
day_time_duration_to_timezone(duration)?
}
}
} else {
context
.implicit_timezone
.unwrap_or_else(get_implicit_timezone_offset)
};
validate_timezone_offset(target_tz.0)?;
let result = match time.timezone {
None => {
TimeValue {
hour: time.hour,
minute: time.minute,
second: time.second,
timezone: Some(target_tz),
}
}
Some(source_tz) => {
let offset_diff = target_tz.0 - source_tz.0;
adjust_time_by_minutes(time, offset_diff, target_tz)?
}
};
Ok(XPathValue::from_atomic(xml_time(result)))
}
fn adjust_datetime_by_minutes(
dt: &DateTimeValue,
offset_minutes: i16,
target_tz: TimezoneOffset,
) -> Result<DateTimeValue, XPathError> {
let mut total_minutes = dt.hour as i32 * 60 + dt.minute as i32 + offset_minutes as i32;
let mut day_delta = 0i32;
while total_minutes < 0 {
total_minutes += 1440;
day_delta -= 1;
}
while total_minutes >= 1440 {
total_minutes -= 1440;
day_delta += 1;
}
let new_hour = (total_minutes / 60) as u8;
let new_minute = (total_minutes % 60) as u8;
let (new_year, new_month, new_day) = add_days_to_date(dt.year, dt.month, dt.day, day_delta)?;
Ok(DateTimeValue {
year: new_year,
month: new_month,
day: new_day,
hour: new_hour,
minute: new_minute,
second: dt.second,
timezone: Some(target_tz),
})
}
fn adjust_date_by_minutes(
date: &DateValue,
offset_minutes: i16,
target_tz: TimezoneOffset,
) -> Result<DateValue, XPathError> {
let total_minutes = offset_minutes as i32;
let resulting_time = total_minutes;
let day_delta = if resulting_time >= 0 {
resulting_time / 1440
} else {
(resulting_time - 1439) / 1440
};
let (new_year, new_month, new_day) =
add_days_to_date(date.year, date.month, date.day, day_delta)?;
Ok(DateValue {
year: new_year,
month: new_month,
day: new_day,
timezone: Some(target_tz),
})
}
fn adjust_time_by_minutes(
time: &TimeValue,
offset_minutes: i16,
target_tz: TimezoneOffset,
) -> Result<TimeValue, XPathError> {
let mut total_minutes = time.hour as i32 * 60 + time.minute as i32 + offset_minutes as i32;
while total_minutes < 0 {
total_minutes += 1440;
}
while total_minutes >= 1440 {
total_minutes -= 1440;
}
let new_hour = (total_minutes / 60) as u8;
let new_minute = (total_minutes % 60) as u8;
Ok(TimeValue {
hour: new_hour,
minute: new_minute,
second: time.second,
timezone: Some(target_tz),
})
}
fn add_days_to_date(
year: i32,
month: u8,
day: u8,
delta: i32,
) -> Result<(i32, u8, u8), XPathError> {
if delta == 0 {
return Ok((year, month, day));
}
let mut y = year;
let mut m = month as i32;
let mut d = day as i32 + delta;
loop {
let days_in_current_month = days_in_month(y, m as u8)?;
if d > days_in_current_month as i32 {
d -= days_in_current_month as i32;
m += 1;
if m > 12 {
m = 1;
y += 1;
}
} else if d < 1 {
m -= 1;
if m < 1 {
m = 12;
y -= 1;
}
let days_in_prev_month = days_in_month(y, m as u8)?;
d += days_in_prev_month as i32;
} else {
break;
}
}
Ok((y, m as u8, d as u8))
}
fn days_in_month(year: i32, month: u8) -> Result<u8, XPathError> {
let days = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => return Err(XPathError::internal("Invalid month value")),
};
Ok(days)
}
fn is_leap_year(year: i32) -> bool {
let year = year as i64;
year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::namespace::table::NameTable;
use crate::xpath::context::XPathContext;
use crate::xpath::iterator::XmlItem;
use crate::xpath::RoXmlNavigator;
fn make_context<'a>() -> DynamicContext<'a, RoXmlNavigator<'a>> {
let table = Box::leak(Box::new(NameTable::new()));
let xpath_ctx = Box::leak(Box::new(XPathContext::new(table)));
DynamicContext::new(xpath_ctx, 0)
}
fn make_datetime_value(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: Decimal,
timezone: Option<TimezoneOffset>,
) -> XPathValue<RoXmlNavigator<'static>> {
let dt = DateTimeValue {
year,
month,
day,
hour,
minute,
second,
timezone,
};
XPathValue::from_atomic(xml_datetime(dt))
}
fn make_date_value(
year: i32,
month: u8,
day: u8,
timezone: Option<TimezoneOffset>,
) -> XPathValue<RoXmlNavigator<'static>> {
let d = DateValue {
year,
month,
day,
timezone,
};
XPathValue::from_atomic(xml_date(d))
}
fn make_time_value(
hour: u8,
minute: u8,
second: Decimal,
timezone: Option<TimezoneOffset>,
) -> XPathValue<RoXmlNavigator<'static>> {
let t = TimeValue {
hour,
minute,
second,
timezone,
};
XPathValue::from_atomic(xml_time(t))
}
fn make_duration_value(
negative: bool,
years: u32,
months: u32,
days: u32,
hours: u32,
minutes: u32,
seconds: Decimal,
) -> XPathValue<RoXmlNavigator<'static>> {
let d = DurationValue {
negative,
years,
months,
days,
hours,
minutes,
seconds,
};
XPathValue::from_atomic(XmlValue {
type_code: XmlTypeCode::Duration,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::Duration(d)),
})
}
fn make_year_month_duration(
negative: bool,
years: u32,
months: u32,
) -> XPathValue<RoXmlNavigator<'static>> {
let d = YearMonthDurationValue {
negative,
years,
months,
};
XPathValue::from_atomic(XmlValue {
type_code: XmlTypeCode::YearMonthDuration,
schema_type: None,
value: XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(d)),
})
}
fn make_day_time_duration(
negative: bool,
days: u32,
hours: u32,
minutes: u32,
seconds: Decimal,
) -> XPathValue<RoXmlNavigator<'static>> {
let d = DayTimeDurationValue {
negative,
days,
hours,
minutes,
seconds,
};
XPathValue::from_atomic(xml_day_time_duration(d))
}
fn get_integer_result<N: DomNavigator>(result: &XPathValue<N>) -> Option<i64> {
match result {
XPathValue::Item(XmlItem::Atomic(v)) => v.as_integer().and_then(|i| i.to_i64()),
_ => None,
}
}
fn get_decimal_result<N: DomNavigator>(result: &XPathValue<N>) -> Option<Decimal> {
match result {
XPathValue::Item(XmlItem::Atomic(v)) => v.as_decimal(),
_ => None,
}
}
#[test]
fn test_current_datetime_returns_value() {
let mut ctx = make_context();
let result = current_datetime(&mut ctx, vec![]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_current_date_returns_value() {
let mut ctx = make_context();
let result = current_date(&mut ctx, vec![]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_current_time_returns_value() {
let mut ctx = make_context();
let result = current_time(&mut ctx, vec![]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_implicit_timezone_returns_value() {
let mut ctx = make_context();
let result = implicit_timezone(&mut ctx, vec![]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_years_from_duration() {
let mut ctx = make_context();
let dur = make_duration_value(false, 1, 2, 3, 4, 5, Decimal::from(6));
let result = years_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(1));
}
#[test]
fn test_years_from_duration_negative() {
let mut ctx = make_context();
let dur = make_duration_value(true, 1, 2, 0, 0, 0, Decimal::ZERO);
let result = years_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(-1));
}
#[test]
fn test_months_from_duration() {
let mut ctx = make_context();
let dur = make_duration_value(false, 1, 14, 0, 0, 0, Decimal::ZERO);
let result = months_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(2));
}
#[test]
fn test_days_from_duration() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 5, 0, 0, Decimal::ZERO);
let result = days_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(5));
}
#[test]
fn test_hours_from_duration() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 0, 10, 0, Decimal::ZERO);
let result = hours_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(10));
}
#[test]
fn test_minutes_from_duration() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 0, 0, 45, Decimal::ZERO);
let result = minutes_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(45));
}
#[test]
fn test_seconds_from_duration() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 0, 0, 0, Decimal::new(305, 1));
let result = seconds_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_decimal_result(&result), Some(Decimal::new(305, 1)));
}
#[test]
fn test_year_from_datetime() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = year_from_datetime(&mut ctx, vec![dt]).unwrap();
assert_eq!(get_integer_result(&result), Some(2024));
}
#[test]
fn test_month_from_datetime() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = month_from_datetime(&mut ctx, vec![dt]).unwrap();
assert_eq!(get_integer_result(&result), Some(3));
}
#[test]
fn test_day_from_datetime() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = day_from_datetime(&mut ctx, vec![dt]).unwrap();
assert_eq!(get_integer_result(&result), Some(15));
}
#[test]
fn test_hours_from_datetime() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = hours_from_datetime(&mut ctx, vec![dt]).unwrap();
assert_eq!(get_integer_result(&result), Some(10));
}
#[test]
fn test_minutes_from_datetime() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = minutes_from_datetime(&mut ctx, vec![dt]).unwrap();
assert_eq!(get_integer_result(&result), Some(30));
}
#[test]
fn test_seconds_from_datetime() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::new(455, 1), None);
let result = seconds_from_datetime(&mut ctx, vec![dt]).unwrap();
assert_eq!(get_decimal_result(&result), Some(Decimal::new(455, 1)));
}
#[test]
fn test_timezone_from_datetime_with_tz() {
let mut ctx = make_context();
let dt = make_datetime_value(
2024,
3,
15,
10,
30,
Decimal::from(0),
Some(TimezoneOffset(-300)),
);
let result = timezone_from_datetime(&mut ctx, vec![dt]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_timezone_from_datetime_without_tz() {
let mut ctx = make_context();
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = timezone_from_datetime(&mut ctx, vec![dt]).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_year_from_date() {
let mut ctx = make_context();
let d = make_date_value(2024, 6, 20, None);
let result = year_from_date(&mut ctx, vec![d]).unwrap();
assert_eq!(get_integer_result(&result), Some(2024));
}
#[test]
fn test_month_from_date() {
let mut ctx = make_context();
let d = make_date_value(2024, 6, 20, None);
let result = month_from_date(&mut ctx, vec![d]).unwrap();
assert_eq!(get_integer_result(&result), Some(6));
}
#[test]
fn test_day_from_date() {
let mut ctx = make_context();
let d = make_date_value(2024, 6, 20, None);
let result = day_from_date(&mut ctx, vec![d]).unwrap();
assert_eq!(get_integer_result(&result), Some(20));
}
#[test]
fn test_hours_from_time() {
let mut ctx = make_context();
let t = make_time_value(14, 35, Decimal::from(0), None);
let result = hours_from_time(&mut ctx, vec![t]).unwrap();
assert_eq!(get_integer_result(&result), Some(14));
}
#[test]
fn test_minutes_from_time() {
let mut ctx = make_context();
let t = make_time_value(14, 35, Decimal::from(0), None);
let result = minutes_from_time(&mut ctx, vec![t]).unwrap();
assert_eq!(get_integer_result(&result), Some(35));
}
#[test]
fn test_seconds_from_time() {
let mut ctx = make_context();
let t = make_time_value(14, 35, Decimal::new(125, 1), None);
let result = seconds_from_time(&mut ctx, vec![t]).unwrap();
assert_eq!(get_decimal_result(&result), Some(Decimal::new(125, 1)));
}
#[test]
fn test_create_datetime_no_tz() {
let mut ctx = make_context();
let d = make_date_value(2024, 3, 15, None);
let t = make_time_value(10, 30, Decimal::from(0), None);
let result = create_datetime(&mut ctx, vec![d, t]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_create_datetime_with_matching_tz() {
let mut ctx = make_context();
let tz = TimezoneOffset::from_hm(5, 0);
let d = make_date_value(2024, 3, 15, Some(tz));
let t = make_time_value(10, 30, Decimal::from(0), Some(tz));
let result = create_datetime(&mut ctx, vec![d, t]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_create_datetime_mismatched_tz() {
let mut ctx = make_context();
let d = make_date_value(2024, 3, 15, Some(TimezoneOffset::from_hm(5, 0)));
let t = make_time_value(
10,
30,
Decimal::from(0),
Some(TimezoneOffset::from_hm(-5, 0)),
);
let result = create_datetime(&mut ctx, vec![d, t]);
assert!(matches!(result, Err(XPathError::FORG0008)));
}
#[test]
fn test_adjust_datetime_to_timezone_no_input_tz() {
let mut ctx = make_context();
ctx.implicit_timezone = Some(TimezoneOffset::from_hm(-5, 0));
let dt = make_datetime_value(2024, 3, 15, 10, 30, Decimal::from(0), None);
let result = adjust_datetime_to_timezone(&mut ctx, vec![dt]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_adjust_datetime_to_timezone_strip_tz() {
let mut ctx = make_context();
let dt = make_datetime_value(
2024,
3,
15,
10,
30,
Decimal::from(0),
Some(TimezoneOffset::UTC),
);
let result = adjust_datetime_to_timezone(&mut ctx, vec![dt, XPathValue::Empty]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_adjust_time_to_timezone() {
let mut ctx = make_context();
ctx.implicit_timezone = Some(TimezoneOffset::from_hm(0, 0));
let t = make_time_value(10, 30, Decimal::from(0), None);
let result = adjust_time_to_timezone(&mut ctx, vec![t]).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_component_functions_with_empty() {
let mut ctx = make_context();
let result = years_from_duration(&mut ctx, vec![XPathValue::Empty]).unwrap();
assert!(result.is_empty());
let result = year_from_datetime(&mut ctx, vec![XPathValue::Empty]).unwrap();
assert!(result.is_empty());
let result = year_from_date(&mut ctx, vec![XPathValue::Empty]).unwrap();
assert!(result.is_empty());
let result = hours_from_time(&mut ctx, vec![XPathValue::Empty]).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_duration_normalization_p14m() {
let mut ctx = make_context();
let dur = make_year_month_duration(false, 0, 14);
let result = years_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
assert_eq!(get_integer_result(&result), Some(1));
let result = months_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(2));
}
#[test]
fn test_duration_normalization_pt30h() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 0, 30, 0, Decimal::ZERO);
let result = days_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
assert_eq!(get_integer_result(&result), Some(1));
let result = hours_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(6));
}
#[test]
fn test_duration_normalization_pt90m() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 0, 0, 90, Decimal::ZERO);
let result = hours_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
assert_eq!(get_integer_result(&result), Some(1));
let result = minutes_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_integer_result(&result), Some(30));
}
#[test]
fn test_duration_normalization_pt3665s() {
let mut ctx = make_context();
let dur = make_day_time_duration(false, 0, 0, 0, Decimal::from(3665));
let result = hours_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
assert_eq!(get_integer_result(&result), Some(1));
let result = minutes_from_duration(&mut ctx, vec![dur.clone()]).unwrap();
assert_eq!(get_integer_result(&result), Some(1));
let result = seconds_from_duration(&mut ctx, vec![dur]).unwrap();
assert_eq!(get_decimal_result(&result), Some(Decimal::from(5)));
}
#[test]
fn test_timezone_offset_with_days_rejected() {
let dur = DayTimeDurationValue {
negative: false,
days: 1,
hours: 0,
minutes: 0,
seconds: Decimal::ZERO,
};
let result = day_time_duration_to_timezone(&dur);
assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
}
#[test]
fn test_timezone_offset_with_seconds_rejected() {
let dur = DayTimeDurationValue {
negative: false,
days: 0,
hours: 5,
minutes: 0,
seconds: Decimal::from(30),
};
let result = day_time_duration_to_timezone(&dur);
assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
}
#[test]
fn test_timezone_offset_with_fractional_seconds_rejected() {
let dur = DayTimeDurationValue {
negative: false,
days: 0,
hours: 5,
minutes: 0,
seconds: Decimal::new(5, 1), };
let result = day_time_duration_to_timezone(&dur);
assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
}
#[test]
fn test_timezone_offset_valid() {
let dur = DayTimeDurationValue {
negative: false,
days: 0,
hours: 5,
minutes: 30,
seconds: Decimal::ZERO,
};
let result = day_time_duration_to_timezone(&dur).unwrap();
assert_eq!(result.0, 330); }
#[test]
fn test_timezone_offset_out_of_range_rejected() {
let dur = DayTimeDurationValue {
negative: false,
days: 0,
hours: 15,
minutes: 0,
seconds: Decimal::ZERO,
};
let result = day_time_duration_to_timezone(&dur);
assert!(matches!(result, Err(XPathError::FODT0003 { .. })));
}
}