Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions xee-interpreter/src/atomic/cast_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ impl atomic::Atomic {
let mut s = String::new();
let offset = date_time.offset;
let date_time = date_time.date_time;
s.push_str(&date_time.format("%Y-%m-%dT%H:%M:%S").to_string());
Self::push_canonical_year(&mut s, date_time.year());
s.push_str(&date_time.format("-%m-%dT%H:%M:%S").to_string());
let millis = date_time.and_utc().timestamp_subsec_millis();
Self::push_millis(&mut s, millis);
if let Some(offset) = offset {
Expand All @@ -75,7 +76,8 @@ impl atomic::Atomic {
date_time: &chrono::DateTime<chrono::FixedOffset>,
) -> String {
let mut s = String::new();
s.push_str(&date_time.format("%Y-%m-%dT%H:%M:%S").to_string());
Self::push_canonical_year(&mut s, date_time.year());
s.push_str(&date_time.format("-%m-%dT%H:%M:%S").to_string());
let millis = date_time.timestamp_subsec_millis();
Self::push_millis(&mut s, millis);
let offset = date_time.offset();
Expand All @@ -100,7 +102,8 @@ impl atomic::Atomic {
let mut s = String::new();
let offset = date.offset;
let date = date.date;
s.push_str(&date.format("%Y-%m-%d").to_string());
Self::push_canonical_year(&mut s, date.year());
s.push_str(&date.format("-%m-%d").to_string());
if let Some(offset) = offset {
Self::push_canonical_time_zone_offset(&mut s, &offset);
}
Expand Down Expand Up @@ -206,6 +209,16 @@ impl atomic::Atomic {
}
}

fn push_canonical_year(s: &mut String, year: i32) {
// chrono's %Y writes years above 9999 with a leading '+', which is
// not part of the XSD lexical space, so write the year ourselves
if year >= 0 {
s.push_str(&format!("{:04}", year));
} else {
s.push_str(&format!("-{:04}", year.abs()));
}
}

fn push_canonical_time_zone_offset(s: &mut String, offset: &chrono::FixedOffset) {
let seconds = offset.local_minus_utc();
if seconds == 0 {
Expand Down
58 changes: 58 additions & 0 deletions xee-xpath/tests/canonical_years.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Canonical serialization of years outside 1..=9999.
//
// chrono's %Y formats years above 9999 with a leading '+', which is not
// valid in XSD lexical space, so the engine's own output failed to cast
// back: xs:dateTime(string(xs:dateTime("10000-01-01T00:00:00Z"))) raised
// FORG0001.

use xee_xpath::{error, Documents, Queries, Query};

fn eval(xpath: &str) -> error::Result<String> {
let mut documents = Documents::new();
let queries = Queries::default();
let q = queries.one(xpath, |_, item| Ok(item.try_into_value::<String>()?))?;
q.execute_build_context(&mut documents, |_| {})
}

#[test]
fn test_five_digit_years_serialize_without_plus() {
assert_eq!(
eval(r#"string(xs:dateTime("10000-01-01T00:00:00Z"))"#).unwrap(),
"10000-01-01T00:00:00Z"
);
assert_eq!(
eval(r#"string(xs:dateTimeStamp("10000-01-01T00:00:00Z"))"#).unwrap(),
"10000-01-01T00:00:00Z"
);
assert_eq!(
eval(r#"string(xs:date("10000-01-01"))"#).unwrap(),
"10000-01-01"
);
}

#[test]
fn test_five_digit_year_round_trips_through_the_engine() {
assert_eq!(
eval(r#"string(xs:dateTime(string(xs:dateTime("10000-01-01T00:00:00Z"))))"#).unwrap(),
"10000-01-01T00:00:00Z"
);
assert_eq!(
eval(r#"string(xs:date(string(xs:date("10000-01-01"))))"#).unwrap(),
"10000-01-01"
);
}

#[test]
fn test_other_year_forms_stay_canonical() {
// negative years and year zero were already canonical
assert_eq!(
eval(r#"string(xs:dateTime("-0005-03-01T12:00:00"))"#).unwrap(),
"-0005-03-01T12:00:00"
);
assert_eq!(
eval(r#"string(xs:date("0000-01-01"))"#).unwrap(),
"0000-01-01"
);
// the gregorian types formatted years manually all along
assert_eq!(eval(r#"string(xs:gYear("10000"))"#).unwrap(), "10000");
}