diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 91a75e10..f0dcc624 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -39,7 +39,7 @@ lazy_static = "1" sysinfo = "0.27" oxrdf = { version = "0.1.1", path="oxrdf", features = ["rdf-star", "oxsdatatypes"] } oxsdatatypes = { version = "0.1.0", path="oxsdatatypes" } -spargebra = { version = "0.2.3", path="spargebra", features = ["rdf-star", "sep-0006"] } +spargebra = { version = "0.2.3", path="spargebra", features = ["rdf-star", "sep-0002", "sep-0006"] } sparesults = { version = "0.1.3", path="sparesults", features = ["rdf-star"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/lib/oxsdatatypes/src/date_time.rs b/lib/oxsdatatypes/src/date_time.rs index 25914fce..403f0bde 100644 --- a/lib/oxsdatatypes/src/date_time.rs +++ b/lib/oxsdatatypes/src/date_time.rs @@ -190,6 +190,14 @@ impl DateTime { } } + // [fn:adjust-dateTime-to-timezone](https://www.w3.org/TR/xpath-functions/#func-adjust-dateTime-to-timezone) + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + Some(Self { + timestamp: self.timestamp.adjust(timezone_offset)?, + }) + } + /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity). #[inline] pub fn is_identical_with(&self, other: &Self) -> bool { @@ -372,6 +380,24 @@ impl Time { .ok() } + // [fn:adjust-time-to-timezone](https://www.w3.org/TR/xpath-functions/#func-adjust-time-to-timezone) + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + DateTime::new( + 1972, + 12, + 31, + self.hour(), + self.minute(), + self.second(), + self.timezone_offset(), + ) + .ok()? + .adjust(timezone_offset)? + .try_into() + .ok() + } + /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity). #[inline] pub fn is_identical_with(&self, other: &Self) -> bool { @@ -543,6 +569,24 @@ impl Date { .ok() } + // [fn:adjust-date-to-timezone](https://www.w3.org/TR/xpath-functions/#func-adjust-date-to-timezone) + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + DateTime::new( + self.year(), + self.month(), + self.day(), + 0, + 0, + Decimal::default(), + self.timezone_offset(), + ) + .ok()? + .adjust(timezone_offset)? + .try_into() + .ok() + } + /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity). #[inline] pub fn is_identical_with(&self, other: &Self) -> bool { @@ -641,6 +685,13 @@ impl GYearMonth { self.timestamp.timezone_offset() } + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + Some(Self { + timestamp: self.timestamp.adjust(timezone_offset)?, + }) + } + #[inline] pub fn to_be_bytes(self) -> [u8; 18] { self.timestamp.to_be_bytes() @@ -747,6 +798,13 @@ impl GYear { self.timestamp.timezone_offset() } + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + Some(Self { + timestamp: self.timestamp.adjust(timezone_offset)?, + }) + } + #[inline] pub fn to_be_bytes(self) -> [u8; 18] { self.timestamp.to_be_bytes() @@ -864,6 +922,13 @@ impl GMonthDay { self.timestamp.timezone_offset() } + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + Some(Self { + timestamp: self.timestamp.adjust(timezone_offset)?, + }) + } + #[inline] pub fn to_be_bytes(self) -> [u8; 18] { self.timestamp.to_be_bytes() @@ -966,6 +1031,13 @@ impl GMonth { self.timestamp.timezone_offset() } + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + Some(Self { + timestamp: self.timestamp.adjust(timezone_offset)?, + }) + } + #[inline] pub fn to_be_bytes(self) -> [u8; 18] { self.timestamp.to_be_bytes() @@ -1082,6 +1154,13 @@ impl GDay { self.timestamp.timezone_offset() } + #[inline] + pub fn adjust(&self, timezone_offset: Option) -> Option { + Some(Self { + timestamp: self.timestamp.adjust(timezone_offset)?, + }) + } + #[inline] pub fn to_be_bytes(self) -> [u8; 18] { self.timestamp.to_be_bytes() @@ -1210,7 +1289,7 @@ impl TryFrom for TimezoneOffset { impl From for DayTimeDuration { #[inline] fn from(value: TimezoneOffset) -> Self { - Self::new(i32::from(value.offset) * 60) + Self::new(i64::from(value.offset) * 60) } } @@ -1451,9 +1530,9 @@ impl Timestamp { } #[inline] - fn checked_add_seconds(&self, seconds: Decimal) -> Option { + fn checked_add_seconds(&self, seconds: impl Into) -> Option { Some(Self { - value: self.value.checked_add(seconds)?, + value: self.value.checked_add(seconds.into())?, timezone_offset: self.timezone_offset, }) } @@ -1476,6 +1555,35 @@ impl Timestamp { }) } + #[inline] + fn adjust(&self, timezone_offset: Option) -> Option { + Some(if let Some(from_timezone) = self.timezone_offset { + if let Some(to_timezone) = timezone_offset { + Self { + value: self.value, // We keep the timestamp + timezone_offset: Some(to_timezone), + } + } else { + Self { + value: self + .value + .checked_add(i64::from(from_timezone.offset) * 60)?, // We keep the literal value + timezone_offset: None, + } + } + } else if let Some(to_timezone) = timezone_offset { + Self { + value: self.value.checked_sub(i64::from(to_timezone.offset) * 60)?, // We keep the literal value + timezone_offset: Some(to_timezone), + } + } else { + Self { + value: self.value, + timezone_offset: None, + } + }) + } + #[inline] fn to_be_bytes(self) -> [u8; 18] { let mut bytes = [0; 18]; @@ -2448,6 +2556,132 @@ mod tests { ); } + #[test] + fn adjust() { + assert_eq!( + DateTime::from_str("2002-03-07T10:00:00-07:00") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("PT10H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + DateTime::from_str("2002-03-08T03:00:00+10:00").unwrap() + ); + assert_eq!( + DateTime::from_str("2002-03-07T00:00:00+01:00") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("-PT8H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + DateTime::from_str("2002-03-06T15:00:00-08:00").unwrap() + ); + assert_eq!( + DateTime::from_str("2002-03-07T10:00:00") + .unwrap() + .adjust(None) + .unwrap(), + DateTime::from_str("2002-03-07T10:00:00").unwrap() + ); + assert_eq!( + DateTime::from_str("2002-03-07T10:00:00-07:00") + .unwrap() + .adjust(None) + .unwrap(), + DateTime::from_str("2002-03-07T10:00:00").unwrap() + ); + + assert_eq!( + Date::from_str("2002-03-07") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("-PT10H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + Date::from_str("2002-03-07-10:00").unwrap() + ); + assert_eq!( + Date::from_str("2002-03-07-07:00") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("-PT10H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + Date::from_str("2002-03-06-10:00").unwrap() + ); + assert_eq!( + Date::from_str("2002-03-07").unwrap().adjust(None).unwrap(), + Date::from_str("2002-03-07").unwrap() + ); + assert_eq!( + Date::from_str("2002-03-07-07:00") + .unwrap() + .adjust(None) + .unwrap(), + Date::from_str("2002-03-07").unwrap() + ); + + assert_eq!( + Time::from_str("10:00:00") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("-PT10H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + Time::from_str("10:00:00-10:00").unwrap() + ); + assert_eq!( + Time::from_str("10:00:00-07:00") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("-PT10H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + Time::from_str("07:00:00-10:00").unwrap() + ); + assert_eq!( + Time::from_str("10:00:00").unwrap().adjust(None).unwrap(), + Time::from_str("10:00:00").unwrap() + ); + assert_eq!( + Time::from_str("10:00:00-07:00") + .unwrap() + .adjust(None) + .unwrap(), + Time::from_str("10:00:00").unwrap() + ); + assert_eq!( + Time::from_str("10:00:00-07:00") + .unwrap() + .adjust(Some( + DayTimeDuration::from_str("PT10H") + .unwrap() + .try_into() + .unwrap() + )) + .unwrap(), + Time::from_str("03:00:00+10:00").unwrap() + ); + } + #[test] fn now() { let now = DateTime::now().unwrap(); diff --git a/lib/spargebra/Cargo.toml b/lib/spargebra/Cargo.toml index 11b4aaa7..9e1b12b9 100644 --- a/lib/spargebra/Cargo.toml +++ b/lib/spargebra/Cargo.toml @@ -16,6 +16,7 @@ rust-version = "1.60" [features] default = [] rdf-star = ["oxrdf/rdf-star"] +sep-0002 = [] sep-0006 = [] [dependencies] diff --git a/lib/spargebra/src/algebra.rs b/lib/spargebra/src/algebra.rs index 65e1dd31..bfebe371 100644 --- a/lib/spargebra/src/algebra.rs +++ b/lib/spargebra/src/algebra.rs @@ -376,6 +376,8 @@ pub enum Function { Object, #[cfg(feature = "rdf-star")] IsTriple, + #[cfg(feature = "sep-0002")] + Adjust, Custom(NamedNode), } @@ -439,6 +441,8 @@ impl Function { Self::Object => write!(f, "object"), #[cfg(feature = "rdf-star")] Self::IsTriple => write!(f, "istriple"), + #[cfg(feature = "sep-0002")] + Self::Adjust => write!(f, "adjust"), Self::Custom(iri) => write!(f, "{iri}"), } } @@ -503,6 +507,8 @@ impl fmt::Display for Function { Self::Object => write!(f, "OBJECT"), #[cfg(feature = "rdf-star")] Self::IsTriple => write!(f, "isTRIPLE"), + #[cfg(feature = "sep-0002")] + Self::Adjust => write!(f, "ADJUST"), Self::Custom(iri) => iri.fmt(f), } } diff --git a/lib/spargebra/src/parser.rs b/lib/spargebra/src/parser.rs index 51318863..1d4358c9 100644 --- a/lib/spargebra/src/parser.rs +++ b/lib/spargebra/src/parser.rs @@ -2109,6 +2109,10 @@ parser! { i("isTriple") "(" _ e:Expression() _ ")" {? #[cfg(feature = "rdf-star")]{Ok(Expression::FunctionCall(Function::IsTriple, vec![e]))} #[cfg(not(feature = "rdf-star"))]{Err("The isTriple function is only available in SPARQL-star")} + } / + i("ADJUST") "(" _ a:Expression() _ "," _ b:Expression() _ ")" {? + #[cfg(feature = "sep-0002")]{Ok(Expression::FunctionCall(Function::Adjust, vec![a, b]))} + #[cfg(not(feature = "sep-0002"))]{Err("The ADJUST function is only available in SPARQL 1.2 SEP 0002")} } //[122] diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index 48cc10aa..3a54e28e 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -1557,6 +1557,38 @@ impl SimpleEvaluator { }) }) } + + PlanExpression::Adjust(dt, tz) => { + let dt = self.expression_evaluator(dt); + let tz = self.expression_evaluator(tz); + Rc::new(move |tuple| { + let timezone_offset = Some( + match tz(tuple)? { + EncodedTerm::DayTimeDurationLiteral(tz) => TimezoneOffset::try_from(tz), + EncodedTerm::DurationLiteral(tz) => TimezoneOffset::try_from(tz), + _ => return None, + } + .ok()?, + ); + Some(match dt(tuple)? { + EncodedTerm::DateTimeLiteral(date_time) => { + date_time.adjust(timezone_offset)?.into() + } + EncodedTerm::TimeLiteral(time) => time.adjust(timezone_offset)?.into(), + EncodedTerm::DateLiteral(date) => date.adjust(timezone_offset)?.into(), + EncodedTerm::GYearMonthLiteral(year_month) => { + year_month.adjust(timezone_offset)?.into() + } + EncodedTerm::GYearLiteral(year) => year.adjust(timezone_offset)?.into(), + EncodedTerm::GMonthDayLiteral(month_day) => { + month_day.adjust(timezone_offset)?.into() + } + EncodedTerm::GDayLiteral(day) => day.adjust(timezone_offset)?.into(), + EncodedTerm::GMonthLiteral(month) => month.adjust(timezone_offset)?.into(), + _ => return None, + }) + }) + } PlanExpression::Now => { let now = self.now; Rc::new(move |_| Some(now.into())) diff --git a/lib/src/sparql/plan.rs b/lib/src/sparql/plan.rs index 6f6d4a06..97dd0d10 100644 --- a/lib/src/sparql/plan.rs +++ b/lib/src/sparql/plan.rs @@ -460,6 +460,7 @@ pub enum PlanExpression { Predicate(Box), Object(Box), IsTriple(Box), + Adjust(Box, Box), BooleanCast(Box), DoubleCast(Box), FloatCast(Box), @@ -557,7 +558,8 @@ impl PlanExpression { | Self::StrDt(a, b) | Self::SameTerm(a, b) | Self::SubStr(a, b, None) - | Self::Regex(a, b, None) => { + | Self::Regex(a, b, None) + | Self::Adjust(a, b) => { a.lookup_used_variables(callback); b.lookup_used_variables(callback); } diff --git a/lib/src/sparql/plan_builder.rs b/lib/src/sparql/plan_builder.rs index 5d555202..57ec123b 100644 --- a/lib/src/sparql/plan_builder.rs +++ b/lib/src/sparql/plan_builder.rs @@ -651,6 +651,10 @@ impl<'a> PlanBuilder<'a> { Function::IsTriple => PlanExpression::IsTriple(Box::new( self.build_for_expression(¶meters[0], variables, graph_name)?, )), + Function::Adjust => PlanExpression::Adjust( + Box::new(self.build_for_expression(¶meters[0], variables, graph_name)?), + Box::new(self.build_for_expression(¶meters[1], variables, graph_name)?), + ), Function::Custom(name) => { if self.custom_functions.contains_key(name) { PlanExpression::CustomFunction(