diff --git a/src/engine/sparqlExpressions/NumericBinaryExpressions.cpp b/src/engine/sparqlExpressions/NumericBinaryExpressions.cpp index 0ff110f6fc..f725f68bee 100644 --- a/src/engine/sparqlExpressions/NumericBinaryExpressions.cpp +++ b/src/engine/sparqlExpressions/NumericBinaryExpressions.cpp @@ -40,8 +40,51 @@ NARY_EXPRESSION(DivideExpressionByZeroIsNan, 2, using Add = MakeNumericExpression>; NARY_EXPRESSION(AddExpression, 2, FV); -using Subtract = MakeNumericExpression>; -NARY_EXPRESSION(SubtractExpression, 2, FV); +// _____________________________________________________________________________ +// Subtract. +struct SubtractImpl { + ValueId operator()(NumericOrDateValue lhs, NumericOrDateValue rhs) const { + return std::visit(SubtractImpl{}, lhs, rhs); + } + + CPP_template(typename L, typename R)( + requires(!std::is_same_v + CPP_and !std::is_same_v)) ValueId + operator()(L lhs, R rhs) const { + using T1 = std::decay_t; + using T2 = std::decay_t; + + if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return Id::makeFromDouble(lhs - rhs); + } else if constexpr (std::is_same_v) { + return Id::makeFromDouble(lhs - static_cast(rhs)); + } + } else if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return Id::makeFromDouble(static_cast(lhs) - rhs); + } else if constexpr (std::is_same_v) { + return Id::makeFromInt(lhs - rhs); + } + } else if constexpr (std::is_same_v && + std::is_same_v) { +#ifndef REDUCED_FEATURE_SET_FOR_CPP17 + // Using - operator implementation in DateYearOrDuration. + auto difference = lhs - rhs; + if (difference.has_value()) { + return Id::makeFromDate(difference.value()); + } else { + return Id::makeUndefined(); + } +#endif + } + // For all other operations returning Undefined + // It is not allowed to use subtractionn between Date and NumericValue + return Id::makeUndefined(); + } +}; +NARY_EXPRESSION(SubtractExpression, 2, + FV); // _____________________________________________________________________________ // Power. diff --git a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp index da21e2a20c..8a77561f3f 100644 --- a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp +++ b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp @@ -46,6 +46,35 @@ NumericValue NumericValueGetter::operator()( AD_FAIL(); } +// _____________________________________________________________________________ +NumericOrDateValue NumericOrDateValueGetter::operator()( + ValueId id, const sparqlExpression::EvaluationContext*) const { + switch (id.getDatatype()) { + case Datatype::Double: + return id.getDouble(); + case Datatype::Int: + return id.getInt(); + case Datatype::Bool: + // TODO Check in the specification what the correct behavior is + // here. They probably should be UNDEF as soon as we have conversion + // functions. + return static_cast(id.getBool()); + case Datatype::Undefined: + case Datatype::EncodedVal: + case Datatype::VocabIndex: + case Datatype::LocalVocabIndex: + case Datatype::TextRecordIndex: + case Datatype::WordVocabIndex: + return NotNumeric{}; + case Datatype::Date: + return id.getDate(); + case Datatype::GeoPoint: + case Datatype::BlankNodeIndex: + return NotNumeric{}; + } + AD_FAIL(); +} + // _____________________________________________________________________________ auto EffectiveBooleanValueGetter::operator()( ValueId id, const EvaluationContext* context) const -> Result { diff --git a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h index 8ff34450b8..8c9b32840e 100644 --- a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h +++ b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h @@ -33,10 +33,17 @@ using Iri = ad_utility::triple_component::Iri; // An empty struct to represent a non-numeric value in a context where only // numeric values make sense. -struct NotNumeric {}; +struct NotNumeric { + bool operator==(const NotNumeric&) const noexcept = default; +}; // The input to an expression that expects a numeric value. using NumericValue = std::variant; using IntOrDouble = std::variant; +// The input to an expression that expects a numeric value or a date. +// Will be used in `NumericBinaryExpressions.cpp` to allow for subtraction of +// Dates. +using NumericOrDateValue = + std::variant; // Return type for `DatatypeValueGetter`. using LiteralOrString = @@ -102,6 +109,19 @@ struct NumericValueGetter : Mixin { NumericValue operator()(ValueId id, const EvaluationContext*) const; }; +// Return `NumericOrDateValue` which is then used as the input to numeric +// expressions. +struct NumericOrDateValueGetter : Mixin { + using Mixin::operator(); + // same as in `NumericValueGetter` + NumericOrDateValue operator()(const LiteralOrIri&, + const EvaluationContext*) const { + return NotNumeric{}; + } + + NumericOrDateValue operator()(ValueId id, const EvaluationContext*) const; +}; + /// Return the type exactly as it was passed in. /// This class is needed for the distinct calculation in the aggregates. struct ActualValueGetter { diff --git a/src/util/DateYearDuration.cpp b/src/util/DateYearDuration.cpp index ce234ceba7..23327978ea 100644 --- a/src/util/DateYearDuration.cpp +++ b/src/util/DateYearDuration.cpp @@ -341,3 +341,136 @@ std::optional DateYearOrDuration::convertToXsdDate( return DateYearOrDuration( Date(date.getYear(), date.getMonth(), date.getDay())); } + +// _____________________________________________________________________________ +#ifndef REDUCED_FEATURE_SET_FOR_CPP17 +void updatePassedTimes(const Date& date1, const Date& date2, long& daysPassed, + int& hoursPassed, int& minutesPassed, + double& secondsPassed) { + // helper function for Subtraction + // this function allows to swap the dates, if daysPassed was negative before + // applying abs. updating daysPassed, hoursPassed, minutesPassed, + // secondsPassed accordingly + if (date1.hasTime()) { + int hour1 = date1.getHour(); + int minute1 = date1.getMinute(); + double second1 = date1.getSecond(); + if (date2.hasTime()) { + int hour2 = date2.getHour(); + int minute2 = date2.getMinute(); + double second2 = date2.getSecond(); + if (hour1 <= hour2) { + if (daysPassed > 0) { + daysPassed--; // counted one day to much + hoursPassed = + 24 - (hour2 - hour1); // total hours of a day - difference + } else { + hoursPassed = (hour2 - hour1); + } + } else { + hoursPassed = (hour1 - hour2); + } + if (minute1 <= minute2) { + if (hoursPassed > 0) { + hoursPassed--; // same as above just one level down + minutesPassed = 60 - (minute2 - minute1); + } else { + minutesPassed = (minute2 - minute1); + } + } else { + minutesPassed = (minute1 - minute2); + } + if (second1 <= second2) { + if (minutesPassed > 0) { + minutesPassed--; + secondsPassed = 60 - (second2 - second1); + } else { + secondsPassed = (second2 - second1); + } + } else { + secondsPassed = (second1 - second2); + } + } else { + // if there is no time given, assume 00:00h 0seconds + hoursPassed = hour1; + minutesPassed = minute1; + secondsPassed = second1; + } + } else { + // date1 has no time, therefore we are assuming time 00:00:00 + if (date2.hasTime()) { + int hour2 = date2.getHour(); + int minute2 = date2.getMinute(); + double second2 = date2.getSecond(); + daysPassed--; + secondsPassed = 60.0 - second2; + minutesPassed = + 60 - + (minute2 + (secondsPassed > 0.0 + ? 1 + : 0)); // we add 1 because the seconds added a minute + hoursPassed = 24 - (hour2 + (minutesPassed > 0 ? 1 : 0)); + } + } +} + +std::optional DateYearOrDuration::operator-( + const DateYearOrDuration& rhs) const { + if (isDate() && rhs.isDate()) { + // Date - Date => Duration | getting time between the two Dates + const Date& ownDate = getDateUnchecked(); + const Date& otherDate = rhs.getDateUnchecked(); + + // Calculate number of days between the two Dates + auto date1 = + std::chrono::year_month_day{std::chrono::year(ownDate.getYear()) / + ownDate.getMonth() / ownDate.getDay()}; + auto date2 = + std::chrono::year_month_day{std::chrono::year(otherDate.getYear()) / + otherDate.getMonth() / otherDate.getDay()}; + + long daysPassed = + (std::chrono::sys_days{date1} - std::chrono::sys_days{date2}).count(); + int hoursPassed = 0; + int minutesPassed = 0; + double secondsPassed = 0.0; + + bool isDaysPassedPos = true; + + if (daysPassed < 0) { + isDaysPassedPos = false; + daysPassed = abs(daysPassed); + } + // Calculate time passed between the two Dates if at least one of them has a + // Time. + if (isDaysPassedPos) { + updatePassedTimes(ownDate, otherDate, daysPassed, hoursPassed, + minutesPassed, secondsPassed); + } else { + updatePassedTimes(otherDate, ownDate, daysPassed, hoursPassed, + minutesPassed, secondsPassed); + } + return DateYearOrDuration(DayTimeDuration(DayTimeDuration::Type::Positive, + daysPassed, hoursPassed, + minutesPassed, secondsPassed)); + } + + if (isDayTimeDuration() && rhs.isDayTimeDuration()) { + // Duration - Duration => Duration | getting new duration that is + // rhs.duration-time smaller return; + // TODO: can be implemented + } + + if (isDate() && rhs.isDayTimeDuration()) { + // Date - Duration => Date | getting new Date from rhs.duration-time earlier + // TODO: can be implemented + } + + // TODO: subtraction with large year can also be implemented + + // Duration - Date is not implemented + + // no viable subtraction + return std::nullopt; +} +#endif diff --git a/src/util/DateYearDuration.h b/src/util/DateYearDuration.h index 49f6a5dbdd..745e7c4423 100644 --- a/src/util/DateYearDuration.h +++ b/src/util/DateYearDuration.h @@ -84,6 +84,12 @@ class DateYearOrDuration { // True iff a complete `Date` is stored and not only a large year. bool isDate() const { return bits_ >> numPayloadDateBits == datetime; } + // True iff a large year is stored. + bool isLongYear() const { + return (bits_ >> numPayloadDateBits == negativeYear) || + (bits_ >> numPayloadDateBits == positiveYear); + } + // True iff constructed with `DayTimeDuration`. bool isDayTimeDuration() const { return bits_ >> numPayloadDurationBits == daytimeDuration; @@ -205,6 +211,13 @@ class DateYearOrDuration { // std::nullopt. static std::optional convertToXsdDate( const DateYearOrDuration& dateValue); + +#ifndef REDUCED_FEATURE_SET_FOR_CPP17 + // Subtraction of two DateYearOrDuration Objects. + // For undefined subtractions `std::nullopt` is returned. + [[nodiscard]] std::optional operator-( + const DateYearOrDuration& rhs) const; +#endif }; #ifdef QLEVER_CPP_17 static_assert(std::is_default_constructible_v); diff --git a/test/DateYearDurationTest.cpp b/test/DateYearDurationTest.cpp index 3f92b1c1e2..6f94940dad 100644 --- a/test/DateYearDurationTest.cpp +++ b/test/DateYearDurationTest.cpp @@ -575,3 +575,118 @@ TEST(DateYearOrDuration, Hashing) { ad_utility::HashSet set{d1, d2}; EXPECT_THAT(set, ::testing::UnorderedElementsAre(d1, d2)); } + +#ifndef REDUCED_FEATURE_SET_FOR_CPP17 +// _____________________________________________________________________________ +TEST(DateYearOrDuration, Subtraction) { + { + // Test for Date Subtraction + DateYearOrDuration test1 = DateYearOrDuration(Date(2012, 12, 24)); + DateYearOrDuration test2 = DateYearOrDuration(Date(2012, 12, 1)); + + DateYearOrDuration result = (test1 - test2).value(); + ASSERT_EQ(true, result.isDayTimeDuration()); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 23)), + result); + result = (test2 - test1).value(); + ASSERT_EQ(true, result.isDayTimeDuration()); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 23)), + result); + + test1 = DateYearOrDuration(Date(2012, 12, 24)); + test2 = DateYearOrDuration(Date(2010, 12, 24)); + result = (test1 - test2).value(); + ASSERT_EQ(true, result.isDayTimeDuration()); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 731)), + result); + result = (test2 - test1).value(); + ASSERT_EQ(true, result.isDayTimeDuration()); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 731)), + result); + + test2 = DateYearOrDuration(Date(1979, 3, 13)); + result = (test1 - test2).value(); + ASSERT_EQ(true, result.isDayTimeDuration()); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 12340)), + result); + + test1 = DateYearOrDuration(Date(1868, 5, 16)); + result = (test1 - test2).value(); + ASSERT_EQ(true, result.isDayTimeDuration()); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 40477)), + result); + } + { + // Test for DateTime Subtraction + // DateTime - DateTime + DateYearOrDuration date1 = + DateYearOrDuration(Date(2012, 12, 22, 12, 6, 12)); + DateYearOrDuration date2 = + DateYearOrDuration(Date(2012, 12, 20, 15, 15, 59)); + // expected duration of 1d20h50min13sec + DateYearOrDuration result = (date1 - date2).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 1, 20, 50, 13)), + result); + result = (date2 - date1).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 1, 20, 50, 13)), + result); + + date2 = DateYearOrDuration(Date(2010, 1, 13, 10, 32, 15)); + // expected duration of 1074d1h33min57sec + result = (date1 - date2).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 1074, 1, 33, 57)), + result); + + // Date - DateTime + date1 = DateYearOrDuration(Date(2012, 12, 22)); + date2 = DateYearOrDuration(Date(2012, 12, 20, 13, 50, 59)); + // expected duration of 1d10h9min1sec + result = (date1 - date2).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 1, 10, 9, 1)), + result); + result = (date2 - date1).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 1, 10, 9, 1)), + result); + } + { + // Test previous bug where days/hours/minutes passed got negative + // daysPassed < 0 + DateYearOrDuration date1 = DateYearOrDuration(Date(2021, 01, 23, 21, 0, 0)); + DateYearOrDuration date2 = DateYearOrDuration(Date(2021, 01, 23, 23, 0, 0)); + // expected duration of 0d2h0min0sec + DateYearOrDuration result = (date1 - date2).value(); + ASSERT_EQ(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 0, 2, 0, 0)), + result); + + // hoursPassed < 0 + date1 = DateYearOrDuration(Date(2021, 01, 23, 22, 10, 0)); + date2 = DateYearOrDuration(Date(2021, 01, 23, 22, 30, 0)); + // expected duration of 0d0h20min0sec + result = (date1 - date2).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 0, 0, 20, 0)), + result); + + // minutesPassed < 0 + date1 = DateYearOrDuration(Date(2021, 01, 23, 22, 10, 03)); + date2 = DateYearOrDuration(Date(2021, 01, 23, 22, 10, 43)); + // expected duration of 0d0h0min40sec + result = (date1 - date2).value(); + ASSERT_EQ(DateYearOrDuration(DayTimeDuration( + DayTimeDuration::Type::Positive, 0, 0, 0, 40)), + result); + } +} +#endif diff --git a/test/SparqlExpressionTest.cpp b/test/SparqlExpressionTest.cpp index c29617b3b8..8b1f25651e 100644 --- a/test/SparqlExpressionTest.cpp +++ b/test/SparqlExpressionTest.cpp @@ -49,6 +49,7 @@ auto D = ad_utility::testing::DoubleId; auto B = ad_utility::testing::BoolId; auto I = ad_utility::testing::IntId; auto Voc = ad_utility::testing::VocabId; +auto Dat = ad_utility::testing::DateId; auto U = Id::makeUndefined(); using Ids = std::vector; @@ -396,6 +397,12 @@ TEST(SparqlExpression, arithmeticOperators) { // `DivideExpression`. // // TODO: Also test `UnaryMinusExpression`. + auto createDat = [](std::string timeString, bool fromDateTime = true) { + return Dat((fromDateTime ? DateYearOrDuration::parseXsdDatetime + : DateYearOrDuration::parseXsdDayTimeDuration), + timeString); + }; + V b{{B(true), B(false), B(false), B(true)}, alloc}; V bAsInt{{I(1), I(0), I(0), I(1)}, alloc}; @@ -403,6 +410,11 @@ TEST(SparqlExpression, arithmeticOperators) { V s{{"true", "", "false", ""}, alloc}; + V dat{ + {createDat("1909-10-10T10:11:23Z"), createDat("2009-09-23T01:01:59Z"), + createDat("1959-03-13T13:13:13Z"), createDat("1889-10-29T00:12:30Z")}, + alloc}; + V allNan{{D(naN), D(naN), D(naN), D(naN)}, alloc}; V i{{I(32), I(-42), I(0), I(5)}, alloc}; @@ -411,6 +423,8 @@ TEST(SparqlExpression, arithmeticOperators) { V bPlusD{{D(2.0), D(-2.0), D(naN), D(1.0)}, alloc}; V bMinusD{{D(0), D(2.0), D(naN), D(1)}, alloc}; V dMinusB{{D(0), D(-2.0), D(naN), D(-1)}, alloc}; + V dMinusDat{{U, U, U, U}, alloc}; + V datMinusD{{U, U, U, U}, alloc}; V bTimesD{{D(1.0), D(-0.0), D(naN), D(0.0)}, alloc}; // Division by zero is `UNDEF`, to change this behavior a runtime parameter // can be set. This is tested explicitly below. @@ -420,6 +434,8 @@ TEST(SparqlExpression, arithmeticOperators) { testPlus(bPlusD, b, d); testMinus(bMinusD, b, d); testMinus(dMinusB, d, b); + testMinus(dMinusDat, d, dat); + testMinus(datMinusD, dat, d); testMultiply(bTimesD, b, d); testDivide(bByD, b, d); testDivide(dByB, d, b); @@ -449,6 +465,19 @@ TEST(SparqlExpression, arithmeticOperators) { testMultiply(times2, mixed, I(2)); testMultiply(times13, mixed, D(1.3)); + // Test for DateTime - DateTime + V minus2000{{createDat("P32954DT13H48M37S", false), + createDat("P3553DT1H1M59S", false), + createDat("P14903DT10H46M47S", false), + createDat("P40239DT23H47M30S", false)}, + alloc}; + testMinus(minus2000, dat, createDat("2000-01-01T00:00:00Z")); + + V mixed2{{B(true), I(250), D(-113.2), Voc(4)}, alloc}; + V mixed2MinusDat{{U, U, U, U}, alloc}; + testMinus(mixed2MinusDat, dat, mixed2); + testMinus(mixed2MinusDat, mixed2, dat); + // For division, all results are doubles, so there is no difference between // int and double inputs. testDivide(by2, mixed, I(2)); diff --git a/test/ValueGetterTest.cpp b/test/ValueGetterTest.cpp index 4bbd68fc1e..08444dfc8b 100644 --- a/test/ValueGetterTest.cpp +++ b/test/ValueGetterTest.cpp @@ -287,4 +287,29 @@ TEST(IntValueGetterTest, OperatorWithLit) { t.checkFromLocalAndNormalVocabAndLiteral("", noInt); } +// _____________________________________________________________________________ +TEST(NumericOrDateValueGetterTest, OperatorWithId) { + NumericOrDateValueGetterTester t; + t.checkFromValueId(ValueId::makeFromInt(-42), + Eq(sparqlExpression::detail::NumericOrDateValue(-42))); + t.checkFromValueId(ValueId::makeFromDouble(50.2), + Optional(VariantWith(DoubleNear(50.2, 0.01)))); + t.checkFromValueId(ValueId::makeFromBool(true), + Eq(sparqlExpression::detail::NumericOrDateValue(1))); + t.checkFromValueId( + ValueId::makeFromDate(DateYearOrDuration(Date(2013, 5, 16))), + Eq(sparqlExpression::detail::NumericOrDateValue( + DateYearOrDuration(Date(2013, 5, 16))))); + t.checkFromValueId( + ValueId::makeFromDate(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 102))), + Eq(sparqlExpression::detail::NumericOrDateValue(DateYearOrDuration( + DayTimeDuration(DayTimeDuration::Type::Positive, 102))))); + t.checkFromValueId(ValueId::makeUndefined(), + Eq(sparqlExpression::detail::NumericOrDateValue( + sparqlExpression::detail::NotNumeric{}))); + t.checkFromValueId(ValueId::makeFromGeoPoint({3, 4}), + Eq(sparqlExpression::detail::NumericOrDateValue( + sparqlExpression::detail::NotNumeric{}))); +} }; // namespace diff --git a/test/ValueGetterTestHelpers.h b/test/ValueGetterTestHelpers.h index 4aa2fb6512..13ad96a6e4 100644 --- a/test/ValueGetterTestHelpers.h +++ b/test/ValueGetterTestHelpers.h @@ -306,6 +306,9 @@ using GeoPointOrWktTester = GeoPointOrWkt>; using IntValueGetterTester = ValueGetterTester; +using NumericOrDateValueGetterTester = + ValueGetterTester; // _____________________________________________________________________________ inline void checkGeoPointOrWktFromLocalAndNormalVocabAndLiteralForValid( std::string wktInput, Loc sourceLocation = AD_CURRENT_SOURCE_LOC()) {