This repository has been archived by the owner on Nov 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
improve xsd::dateTime implementation
- use chrono instead of old crate datetime - do not use an implicit timezone for timezone-less w3c/sparql-query#116
- Loading branch information
Showing
6 changed files
with
188 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
mod datetime; | ||
|
||
use std::{env::args, sync::Arc}; | ||
|
||
use sophia::{ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
//! An XsdDateTime is a dateTime with or without a timezone. | ||
//! See https://www.w3.org/TR/xmlschema-2/#dt-dateTime | ||
//! | ||
//! # Comparing dateTimes | ||
//! | ||
//! According to [Section 3.2.7.4 Order relation on dateTime of XML Schema Part 2](https://www.w3.org/TR/xmlschema-2/#dt-dateTime) | ||
//! dateTimes are incomparable in some circumstances. | ||
//! However, the XPath functions [`op:date-equal`](https://www.w3.org/TR/xpath-functions/#func-date-equal) | ||
//! and [`op:date-less-than`](https://www.w3.org/TR/xpath-functions/#func-date-less-than), | ||
//! which the SPARQL specification refers to, | ||
//! solve this ambiguity with an [implicit timezone], | ||
//! which is [controversial](https://github.com/w3c/sparql-query/issues/116). | ||
//! | ||
//! This implementation uses no implicit timezone. | ||
use std::{cmp::Ordering, fmt::Display, str::FromStr}; | ||
|
||
use chrono::{format::ParseErrorKind, DateTime, FixedOffset, NaiveDateTime}; | ||
|
||
#[derive(Clone, Copy, Debug, PartialEq)] | ||
pub enum XsdDateTime { | ||
Naive(NaiveDateTime), | ||
Timezoned(DateTime<FixedOffset>), | ||
} | ||
|
||
impl FromStr for XsdDateTime { | ||
type Err = chrono::ParseError; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
DateTime::parse_from_rfc3339(s) | ||
.map(Self::Timezoned) | ||
.or_else(|e| { | ||
if e.kind() == ParseErrorKind::TooShort { | ||
s.parse().map(Self::Naive) | ||
} else { | ||
Err(e) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
impl Display for XsdDateTime { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!( | ||
f, | ||
"{}", | ||
match self { | ||
XsdDateTime::Naive(d) => { | ||
let mut out = d.and_utc().to_rfc3339(); | ||
out.truncate(out.len() - 6); // truncate timezone | ||
out | ||
} | ||
XsdDateTime::Timezoned(d) => { | ||
d.to_rfc3339() | ||
} | ||
} | ||
) | ||
} | ||
} | ||
|
||
impl PartialOrd for XsdDateTime { | ||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||
match (self, other) { | ||
(XsdDateTime::Naive(d1), XsdDateTime::Naive(d2)) => d1.partial_cmp(d2), | ||
(XsdDateTime::Naive(d1), XsdDateTime::Timezoned(d2)) => { | ||
heterogeneous_cmp(d2, d1).map(Ordering::reverse) | ||
} | ||
(XsdDateTime::Timezoned(d1), XsdDateTime::Naive(d2)) => heterogeneous_cmp(d1, d2), | ||
(XsdDateTime::Timezoned(d1), XsdDateTime::Timezoned(d2)) => d1.partial_cmp(d2), | ||
} | ||
} | ||
} | ||
|
||
/// Implements https://www.w3.org/TR/xmlschema-2/#dateTime-order | ||
fn heterogeneous_cmp(d1: &DateTime<FixedOffset>, d2: &NaiveDateTime) -> Option<Ordering> { | ||
if d1 < &naive_to_fixed(d2, 14) { | ||
Some(Ordering::Less) | ||
} else if d1 > &naive_to_fixed(d2, -14) { | ||
Some(Ordering::Greater) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
fn naive_to_fixed(d: &NaiveDateTime, offset: i8) -> DateTime<FixedOffset> { | ||
debug_assert!((-14..=14).contains(&offset)); | ||
let fixed_offset = FixedOffset::east_opt(offset as i32 * 3600).unwrap(); | ||
match d.and_local_timezone(fixed_offset) { | ||
chrono::offset::LocalResult::Single(r) => r, | ||
_ => unreachable!(), // FixedOffset has no fold or gap, so there is always a single result | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use test_case::test_case; | ||
|
||
#[test_case("2024-09-17T12:00:00"; "no timezone")] | ||
fn naive(d: &str) { | ||
assert!(matches!( | ||
d.parse::<XsdDateTime>().unwrap(), | ||
XsdDateTime::Naive(_) | ||
)); | ||
} | ||
|
||
#[test_case("2024-09-17T12:00:00Z"; "z")] | ||
#[test_case("2024-09-17T12:00:00+00:00"; "plus 0")] | ||
#[test_case("2024-09-17T12:00:00-00:00"; "minus 0")] | ||
#[test_case("2024-09-17T12:00:00+01:59"; "plus 1:59")] | ||
#[test_case("2024-09-17T12:00:00-01:59"; "minus 1:59")] | ||
#[test_case("2024-09-17T12:00:00+14:00"; "plus 14")] | ||
#[test_case("2024-09-17T12:00:00-14:00"; "minus 14")] | ||
fn timezoned(d: &str) { | ||
assert!(matches!( | ||
d.parse::<XsdDateTime>().unwrap(), | ||
XsdDateTime::Timezoned(_) | ||
)); | ||
} | ||
|
||
#[test_case("2024-09-17T12:00:00"; "no timezone")] | ||
#[test_case("2024-09-17T12:00:00+00:00"; "plus 0")] | ||
#[test_case("2024-09-17T12:00:00+01:59"; "plus 1:59")] | ||
#[test_case("2024-09-17T12:00:00-01:59"; "minus 1:59")] | ||
#[test_case("2024-09-17T12:00:00+14:00"; "plus 14")] | ||
#[test_case("2024-09-17T12:00:00-14:00"; "minus 14")] | ||
#[test_case("2024-09-17T12:00:00.123-14:00"; "with subsec")] | ||
fn to_string(d: &str) { | ||
assert_eq!(d.parse::<XsdDateTime>().unwrap().to_string(), d); | ||
} | ||
|
||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T12:00:00Z" => true; "12z 12z")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T12:00:00+00:00" => true; "12z 12p0")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T13:00:00+01:00" => true; "12z 13p1")] | ||
#[test_case("2024-09-17T12:00:00", "2024-09-17T12:00:00Z" => false; "12 12z")] | ||
#[test_case("2024-09-17T12:00:00", "2024-09-17T13:00:00+01:00" => false; "12 13p1")] | ||
fn equal(d1: &str, d2: &str) -> bool { | ||
d1.parse::<XsdDateTime>().unwrap() == d2.parse::<XsdDateTime>().unwrap() | ||
} | ||
|
||
use Ordering::*; | ||
|
||
#[test_case("2024-09-17T12:00:00", "2024-09-17T12:00:00", Some(Equal); "12 12")] | ||
#[test_case("2024-09-17T12:00:00", "2024-09-17T12:00:00Z", None; "12 12z")] | ||
#[test_case("2024-09-17T12:00:00", "2024-09-17T13:00:00+01:00", None; "12 13p1")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T12:00:00Z", Some(Equal); "12z 12z")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T12:00:00+00:00", Some(Equal); "12z 12p0")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T13:00:00+01:00", Some(Equal); "12z 13p1")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T13:00:00+02:00", Some(Greater); "12z 13p2")] | ||
#[test_case("2024-09-17T12:00:00Z", "2024-09-17T11:00:00-02:00", Some(Less); "12z 11m2")] | ||
#[test_case("2024-09-17T06:00:00", "2024-09-17T19:59:00Z", None; "6 19z")] | ||
#[test_case("2024-09-17T06:00:00", "2024-09-17T20:00:00Z", None; "6 20z")] | ||
#[test_case("2024-09-17T06:00:00", "2024-09-17T20:01:00Z", Some(Less); "6 20:01z")] | ||
#[test_case("2024-09-17T06:00:00Z", "2024-09-17T19:59:00", None; "6z 19")] | ||
#[test_case("2024-09-17T06:00:00Z", "2024-09-17T20:00:00", None; "6z 20")] | ||
#[test_case("2024-09-17T06:00:00Z", "2024-09-17T20:01:00", Some(Less); "6z 20:01")] | ||
fn partial_cmp(d1: &str, d2: &str, exp: Option<Ordering>) { | ||
let d1 = d1.parse::<XsdDateTime>().unwrap(); | ||
let d2 = d2.parse::<XsdDateTime>().unwrap(); | ||
assert_eq!(d1.partial_cmp(&d2), exp); | ||
assert_eq!(d2.partial_cmp(&d1), exp.map(Ordering::reverse)); | ||
} | ||
} |