Skip to content
27 changes: 27 additions & 0 deletions src/engine/sparqlExpressions/DateExpressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ struct ExtractTimeComponentImpl {
}
};

//______________________________________________________________________________
struct ExtractEpoch {
Id operator()(std::optional<DateYearOrDuration> d) const {
#ifndef QLEVER_REDUCED_FEATURE_SET_FOR_CPP17
if (!d.has_value() || !d->isDate()) {
return Id::makeUndefined();
}
Date date = d.value().getDate();

std::optional<int64_t> epoch = date.toEpochInt();
if (!epoch.has_value()) {
return Id::makeUndefined();
}
return Id::makeFromInt(epoch.value());
#else
throw std::runtime_error(
"This QLever server does not support ql:toEpoch because it was "
"compiled with the restricted feature set on C++ 17");
#endif
}
};

//______________________________________________________________________________
using ExtractHours = ExtractTimeComponentImpl<&Date::getHour, &Id::makeFromInt>;
using ExtractMinutes =
Expand All @@ -111,6 +133,7 @@ NARY_EXPRESSION(TimezoneStrExpression, 1,
FV<ExtractStrTimezone, DateValueGetter>);
NARY_EXPRESSION(TimezoneDurationExpression, 1,
FV<ExtractTimezoneDurationFormat, DateValueGetter>);
NARY_EXPRESSION(ToEpochExpression, 1, FV<ExtractEpoch, DateValueGetter>);
NARY_EXPRESSION(HoursExpression, 1, FV<ExtractHours, DateValueGetter>);
NARY_EXPRESSION(MinutesExpression, 1, FV<ExtractMinutes, DateValueGetter>);
NARY_EXPRESSION(SecondsExpression, 1, FV<ExtractSeconds, DateValueGetter>);
Expand Down Expand Up @@ -149,6 +172,10 @@ SparqlExpression::Ptr makeTimezoneExpression(SparqlExpression::Ptr child) {
return std::make_unique<TimezoneDurationExpression>(std::move(child));
}

SparqlExpression::Ptr makeToEpochExpression(SparqlExpression::Ptr child) {
return std::make_unique<ToEpochExpression>(std::move(child));
}

SparqlExpression::Ptr makeMonthExpression(SparqlExpression::Ptr child) {
return std::make_unique<MonthExpression>(std::move(child));
}
Expand Down
1 change: 1 addition & 0 deletions src/engine/sparqlExpressions/NaryExpression.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ SparqlExpression::Ptr makeHoursExpression(SparqlExpression::Ptr child);
SparqlExpression::Ptr makeDayExpression(SparqlExpression::Ptr child);
SparqlExpression::Ptr makeTimezoneStrExpression(SparqlExpression::Ptr child);
SparqlExpression::Ptr makeTimezoneExpression(SparqlExpression::Ptr child);
SparqlExpression::Ptr makeToEpochExpression(SparqlExpression::Ptr child);
SparqlExpression::Ptr makeMonthExpression(SparqlExpression::Ptr child);
SparqlExpression::Ptr makeYearExpression(SparqlExpression::Ptr child);

Expand Down
10 changes: 5 additions & 5 deletions src/parser/sparqlParser/SparqlQleverVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,15 @@ ExpressionPtr Visitor::processIriFunctionCall(
// QLever-internal functions.
//
// NOTE: Predicates like `ql:has-predicate` etc. are handled elsewhere.
static const UnaryFuncTable customGeoUnaryFuncs{
static const UnaryFuncTable unaryInternalFuncs{
{"envelopeLowerLeft", &makeEnvelopeLowerLeftExpression},
{"envelopeUpperRight", &makeEnvelopeUpperRightExpression},
{"isGeoPoint", &makeIsGeoPointExpression},
{"toEpoch", &makeToEpochExpression},
};
if (checkPrefix(QL_PREFIX)) {
if (functionName == "isGeoPoint") {
return createUnary(&makeIsGeoPointExpression);
} else if (ad_utility::contains(customGeoUnaryFuncs, functionName)) {
return createUnary(customGeoUnaryFuncs.at(functionName));
if (ad_utility::contains(unaryInternalFuncs, functionName)) {
return createUnary(unaryInternalFuncs.at(functionName));
} else if (functionName == "prefix-match") {
return createBinary(&makePrefixMatchExpression);
}
Expand Down
20 changes: 19 additions & 1 deletion src/util/Date.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ std::optional<Date::Nanoseconds> Date::toEpoch() const {
if (date.ok()) {
// Build timestamp from `Date`.
auto second = duration<double>{getSecond()};
// If getHour returns -1 the date does not specify time, therefore just
// assume 0 hours.
auto hour = std::max(getHour(), 0);
Date::Nanoseconds result =
sys_days(date) + hours{getHour() - getTimeZoneOffsetToUTCInHours()} +
sys_days(date) + hours{hour - getTimeZoneOffsetToUTCInHours()} +
minutes{getMinute()} +
duration_cast<nanoseconds>(
second); // Here all times are converted to a UTC time.
Expand All @@ -126,6 +129,20 @@ std::optional<Date::Nanoseconds> Date::toEpoch() const {
}
}

// _____________________________________________________________________________
std::optional<int64_t> Date::toEpochInt() const {
std::optional<Date::Nanoseconds> result = toEpoch();
if (!result.has_value()) {
return std::nullopt; // Invalid date.
} else {
// First convert the timepoint to its duration representation and then cast
// to total seconds.
return std::chrono::duration_cast<std::chrono::seconds>(
result.value().time_since_epoch())
.count();
}
}

// _____________________________________________________________________________
Date Date::makeFromEpoch(Nanoseconds timestamp, TimeZone tz) {
int8_t offset = Date::getTimeZoneOffsetToUTCInHours(tz);
Expand Down Expand Up @@ -154,6 +171,7 @@ Date Date::makeFromEpoch(Nanoseconds timestamp, TimeZone tz) {
}

#endif

// _____________________________________________________________________________
int8_t Date::getTimeZoneOffsetToUTCInHours(TimeZone tz) {
// Handle different types contained in variant `TimeZone`.
Expand Down
3 changes: 3 additions & 0 deletions src/util/Date.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,13 @@ class Date {
// If `Date` is valid, convert it to Unix Epoch timestamp. ToEpoch always
// returns a UTC timestamp.
std::optional<Nanoseconds> toEpoch() const;
// Uses `toEpoch` to return the Epoch time in seconds.
std::optional<int64_t> toEpochInt() const;

// From a Unix Epoch timestamp, construct the corresponding `Date`.
static Date makeFromEpoch(Nanoseconds timestamp, TimeZone tz);
#endif

static int8_t getTimeZoneOffsetToUTCInHours(TimeZone tz);
int8_t getTimeZoneOffsetToUTCInHours() const;
};
Expand Down
18 changes: 18 additions & 0 deletions test/DateYearDurationTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,24 @@ TEST(Date, Subtraction) {
}
#endif

// _____________________________________________________________________________
TEST(Date, toEpochInt) {
using namespace testing;
Date date = Date(1970, 1, 1, 0, 0, 0);
auto result = date.toEpochInt();
EXPECT_THAT(result, Optional(Eq(0)));
date = Date(1999, 1, 1, 10, 12, 0);
result = date.toEpochInt();
EXPECT_THAT(result, Optional(Eq(915'185'520)));
date = Date(1949, 2, 11, 10, 12, 0);
result = date.toEpochInt();
EXPECT_THAT(result, Optional(Eq(-659'108'880)));
// Invalid date
date = Date(1998, 2, 30, 10, 12, 0);
result = date.toEpochInt();
ASSERT_FALSE(result.has_value());
}

// _____________________________________________________________________________
TEST(Date, getTimeZoneOffsetToUTCInHours) {
Date date = Date(1970, 1, 1, 0, 0, 0); // No `TimeZone` given.
Expand Down
68 changes: 54 additions & 14 deletions test/SparqlExpressionTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,8 @@ auto testUnaryExpression = [](VectorOrExpressionResult auto const& operand,

TEST(SparqlExpression, dateOperators) {
// Test `YearExpression`, `MonthExpression`, `DayExpression`,
// `HoursExpression`, `MinutesExpression`, and `SecondsExpression`.
// `HoursExpression`, `MinutesExpression`, `SecondsExpression` and
// `ToEpochExpression`.
// Helper function that asserts that the date operators give the expected
// result on the given date.
auto checkYear = testUnaryExpression<&makeYearExpression>;
Expand All @@ -607,15 +608,17 @@ TEST(SparqlExpression, dateOperators) {
auto checkHours = testUnaryExpression<&makeHoursExpression>;
auto checkMinutes = testUnaryExpression<&makeMinutesExpression>;
auto checkSeconds = testUnaryExpression<&makeSecondsExpression>;
auto checkEpoch = testUnaryExpression<&makeToEpochExpression>;
auto check = [&checkYear, &checkMonth, &checkDay, &checkHours, &checkMinutes,
&checkSeconds](
&checkSeconds, &checkEpoch](
const DateYearOrDuration& date,
std::optional<int> expectedYear,
std::optional<int> expectedMonth = std::nullopt,
std::optional<int> expectedDay = std::nullopt,
std::optional<int> expectedHours = std::nullopt,
std::optional<int> expectedMinutes = std::nullopt,
std::optional<double> expectedSeconds = std::nullopt,
std::optional<int> expectedEpoch = std::nullopt,
ad_utility::source_location l = AD_CURRENT_SOURCE_LOC()) {
auto trace = generateLocationTrace(l);
auto optToIdInt = [](const auto& opt) {
Expand All @@ -639,27 +642,59 @@ TEST(SparqlExpression, dateOperators) {
checkMinutes(Ids{Id::makeFromDate(date)}, Ids{optToIdInt(expectedMinutes)});
checkSeconds(Ids{Id::makeFromDate(date)},
Ids{optToIdDouble(expectedSeconds)});
checkEpoch(Ids{Id::makeFromDate(date)}, Ids{optToIdInt(expectedEpoch)});
};

using D = DateYearOrDuration;
// Now the checks for dates with varying level of detail.
// Now the checks for dates with varying level of detail.
#ifndef QLEVER_REDUCED_FEATURE_SET_FOR_CPP17
// `ToEpochExpression` works with `std::chrono`.
check(D::parseXsdDatetime("1970-04-22T11:53:42.25"), 1970, 4, 22, 11, 53,
42.25);
check(D::parseXsdDate("1970-04-22"), 1970, 4, 22);
check(D::parseXsdDate("1970-04-22"), 1970, 4, 22);
check(D::parseXsdDate("0042-12-24"), 42, 12, 24);
check(D::parseXsdDate("-0099-07-01"), -99, 7, 1);
check(D::parseGYear("-1234"), -1234, std::nullopt, std::nullopt);
check(D::parseXsdDate("0321-07-01"), 321, 7, 1);
check(D::parseXsdDate("2321-07-01"), 2321, 7, 1);

// Test behavior of the `largeYear` representation that doesn't store the
// actual date.
42.25, 9'633'222);
check(D::parseXsdDate("1970-04-22"), 1970, 4, 22, std::nullopt, std::nullopt,
std::nullopt, 9'590'400);
check(D::parseXsdDate("1970-04-22"), 1970, 4, 22, std::nullopt, std::nullopt,
std::nullopt, 9'590'400);
// TODO<yarox-1> Currently not working, but will be working after change of
// `toEpoch` from Nanoseconds to Milliseconds.
// check(D::parseXsdDate("0042-12-24"), 42, 12, 24, std::nullopt, std::nullopt,
// std::nullopt, -852'768'000); check(D::parseXsdDate("-0099-07-01"), -99, 7, 1,
// std::nullopt, std::nullopt, std::nullopt, -65'275'718'400);
// check(D::parseGYear("-1234"), -1234, std::nullopt, std::nullopt,
// std::nullopt, std::nullopt, std::nullopt, -101'108'476'800);
// check(D::parseXsdDate("0321-07-01"), 321, 7, 1, std::nullopt, std::nullopt,
// std::nullopt, -52'021'785'600); check(D::parseXsdDate("2321-07-01"), 2321, 7,
// 1, std::nullopt, std::nullopt, std::nullopt, 11'092'118'400);
#else
AD_EXPECT_THROW_WITH_MESSAGE(
check(D::parseXsdDatetime("1970-04-22T11:53:42.25"), 1970, 4, 22, 11, 53,
42.25, 9'633'222),
::testing::HasSubstr("does not support ql:toEpoch"));
AD_EXPECT_THROW_WITH_MESSAGE(
check(D::parseXsdDate("1970-04-22"), 1970, 4, 22, std::nullopt,
std::nullopt, std::nullopt, 9'590'400),
::testing::HasSubstr("does not support ql:toEpoch"));
AD_EXPECT_THROW_WITH_MESSAGE(
check(D::parseGYear("-1234"), -1234, std::nullopt, std::nullopt,
std::nullopt, std::nullopt, std::nullopt, -101'108'476'800),
::testing::HasSubstr("does not support ql:toEpoch"));
#endif
// Test behavior of the `largeYear` representation that doesn't store the
// actual date.
#ifndef QLEVER_REDUCED_FEATURE_SET_FOR_CPP17
check(D::parseGYear("123456"), 123456);
check(D::parseGYearMonth("-12345-01"), -12345, 1);
check(D::parseGYearMonth("-12345-03"), -12345, 1);
check(D::parseXsdDate("-12345-01-01"), -12345, 1, 1);
check(D::parseXsdDate("-12345-03-04"), -12345, 1, 1);
#else
AD_EXPECT_THROW_WITH_MESSAGE(
check(D::parseGYear("123456"), 123456),
::testing::HasSubstr("does not support ql:toEpoch"));
AD_EXPECT_THROW_WITH_MESSAGE(
check(D::parseXsdDate("-12345-01-01"), -12345, 1, 1),
::testing::HasSubstr("does not support ql:toEpoch"));
#endif

// Invalid inputs for date expressions.
checkYear(Ids{Id::makeFromInt(42)}, Ids{Id::makeUndefined()});
Expand All @@ -668,11 +703,16 @@ TEST(SparqlExpression, dateOperators) {
checkHours(Ids{Id::makeFromInt(42)}, Ids{Id::makeUndefined()});
checkMinutes(Ids{Id::makeFromInt(84)}, Ids{Id::makeUndefined()});
checkSeconds(Ids{Id::makeFromDouble(120.0123)}, Ids{Id::makeUndefined()});
checkEpoch(Ids{Id::makeFromInt(84)}, Ids{Id::makeUndefined()});
auto testYear = testUnaryExpression<&makeYearExpression>;
testYear(Ids{Id::makeFromDouble(42.0)}, Ids{U});
testYear(Ids{Id::makeFromBool(false)}, Ids{U});
testYear(IdOrLocalVocabEntryVec{lit("noDate")}, Ids{U});

// Test epoch for invalid dates.
checkEpoch(Ids{Id::makeFromDate(D::parseXsdDate("1970-02-30"))},
Ids{Id::makeUndefined()});

// test makeTimezoneStrExpression / makeTimezoneExpression
auto positive = DayTimeDuration::Type::Positive;
auto negative = DayTimeDuration::Type::Negative;
Expand Down
2 changes: 2 additions & 0 deletions test/parser/SparqlAntlrParserExpressionTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ TEST(SparqlParser, FunctionCall) {
matchUnary(&makeCentroidExpression));
expectFunctionCall(absl::StrCat(ql, "isGeoPoint>(?x)"),
matchUnary(&makeIsGeoPointExpression));
expectFunctionCall(absl::StrCat(ql, "toEpoch>(?x)"),
matchUnary(&makeToEpochExpression));
expectFunctionCall(absl::StrCat(ql, "envelopeLowerLeft>(?x)"),
matchUnary(&makeEnvelopeLowerLeftExpression));
expectFunctionCall(absl::StrCat(ql, "envelopeUpperRight>(?x)"),
Expand Down
Loading