diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/function/xsd/DateCast.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/function/xsd/DateCast.java
new file mode 100644
index 00000000000..f9205df3b5a
--- /dev/null
+++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/function/xsd/DateCast.java
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Eclipse RDF4J contributors.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ *******************************************************************************/
+package org.eclipse.rdf4j.query.algebra.evaluation.function.xsd;
+
+import static javax.xml.datatype.DatatypeConstants.FIELD_UNDEFINED;
+
+import static org.eclipse.rdf4j.model.datatypes.XMLDatatypeUtil.isValidDate;
+import static org.eclipse.rdf4j.model.datatypes.XMLDatatypeUtil.isValidDateTime;
+import static org.eclipse.rdf4j.model.vocabulary.XSD.DATE;
+import static org.eclipse.rdf4j.model.vocabulary.XSD.DATETIME;
+import static org.eclipse.rdf4j.model.vocabulary.XSD.STRING;
+
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.eclipse.rdf4j.model.IRI;
+import org.eclipse.rdf4j.model.Literal;
+import org.eclipse.rdf4j.model.Value;
+import org.eclipse.rdf4j.model.ValueFactory;
+import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
+
+/**
+ * A {@link org.eclipse.rdf4j.query.algebra.evaluation.function.Function} that tries to cast its argument to an
+ * xsd:date.
+ */
+public class DateCast extends CastFunction {
+
+ private static final String ZERO = "0";
+
+ @Override
+ protected IRI getXsdDatatype() {
+ return DATE;
+ }
+
+ @Override
+ protected boolean isValidForDatatype(String lexicalValue) {
+ return isValidDate(lexicalValue);
+ }
+
+ @Override
+ protected Literal convert(ValueFactory vf, Value value) throws ValueExprEvaluationException {
+ if (value instanceof Literal) {
+ Literal literal = (Literal) value;
+ IRI datatype = literal.getDatatype();
+
+ if (STRING.equals(datatype) || DATETIME.equals(datatype)) {
+ try {
+ XMLGregorianCalendar calValue = literal.calendarValue();
+ int year = calValue.getYear();
+ int month = calValue.getMonth();
+ int day = calValue.getDay();
+ int timezoneOffset = calValue.getTimezone();
+
+ if (FIELD_UNDEFINED != year && FIELD_UNDEFINED != month && FIELD_UNDEFINED != day) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(year).append("-");
+ addZeroIfNeeded(month, builder);
+ builder.append(month).append("-");
+ addZeroIfNeeded(day, builder);
+ builder.append(day);
+
+ if (FIELD_UNDEFINED != timezoneOffset) {
+ int minutes = Math.abs(timezoneOffset);
+ int hours = minutes / 60;
+ minutes = minutes - (hours * 60);
+ builder.append(timezoneOffset > 0 ? "+" : "-");
+ addZeroIfNeeded(hours, builder);
+ builder.append(hours);
+ builder.append(":");
+ addZeroIfNeeded(minutes, builder);
+ builder.append(minutes);
+ }
+
+ return vf.createLiteral(builder.toString(), DATE);
+ } else {
+ throw typeError(literal, null);
+ }
+ } catch (IllegalArgumentException e) {
+ throw typeError(literal, e);
+ }
+ }
+ }
+ throw typeError(value, null);
+ }
+
+ private static void addZeroIfNeeded(int value, StringBuilder builder) {
+ if (value < 10) {
+ builder.append(ZERO);
+ }
+ }
+}
diff --git a/core/queryalgebra/evaluation/src/main/resources/META-INF/services/org.eclipse.rdf4j.query.algebra.evaluation.function.Function b/core/queryalgebra/evaluation/src/main/resources/META-INF/services/org.eclipse.rdf4j.query.algebra.evaluation.function.Function
index bec8f6d9564..a2d70b235ed 100644
--- a/core/queryalgebra/evaluation/src/main/resources/META-INF/services/org.eclipse.rdf4j.query.algebra.evaluation.function.Function
+++ b/core/queryalgebra/evaluation/src/main/resources/META-INF/services/org.eclipse.rdf4j.query.algebra.evaluation.function.Function
@@ -36,6 +36,7 @@ org.eclipse.rdf4j.query.algebra.evaluation.function.string.Substring
org.eclipse.rdf4j.query.algebra.evaluation.function.string.UpperCase
org.eclipse.rdf4j.query.algebra.evaluation.function.xsd.BooleanCast
org.eclipse.rdf4j.query.algebra.evaluation.function.xsd.ByteCast
+org.eclipse.rdf4j.query.algebra.evaluation.function.xsd.DateCast
org.eclipse.rdf4j.query.algebra.evaluation.function.xsd.DateTimeCast
org.eclipse.rdf4j.query.algebra.evaluation.function.xsd.DecimalCast
org.eclipse.rdf4j.query.algebra.evaluation.function.xsd.DoubleCast
@@ -57,4 +58,4 @@ org.eclipse.rdf4j.query.algebra.evaluation.function.triple.IsTripleFunction
org.eclipse.rdf4j.query.algebra.evaluation.function.triple.StatementFunction
org.eclipse.rdf4j.query.algebra.evaluation.function.triple.TripleSubjectFunction
org.eclipse.rdf4j.query.algebra.evaluation.function.triple.TriplePredicateFunction
-org.eclipse.rdf4j.query.algebra.evaluation.function.triple.TripleObjectFunction
\ No newline at end of file
+org.eclipse.rdf4j.query.algebra.evaluation.function.triple.TripleObjectFunction
diff --git a/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/function/xsd/TestDateCast.java b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/function/xsd/TestDateCast.java
new file mode 100644
index 00000000000..e14895d2e52
--- /dev/null
+++ b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/function/xsd/TestDateCast.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Eclipse RDF4J contributors.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Distribution License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ *******************************************************************************/
+package org.eclipse.rdf4j.query.algebra.evaluation.function.xsd;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.eclipse.rdf4j.model.datatypes.XMLDatatypeUtil.parseCalendar;
+import static org.eclipse.rdf4j.model.vocabulary.XSD.DATE;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import org.eclipse.rdf4j.model.Literal;
+import org.eclipse.rdf4j.model.ValueFactory;
+import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
+import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestDateCast {
+
+ private DateCast dateCast;
+ private final ValueFactory vf = SimpleValueFactory.getInstance();
+
+ @Before
+ public void setUp() throws Exception {
+ dateCast = new DateCast();
+ }
+
+ @Test
+ public void testCastPlainLiteral_date() {
+ testDateCast(vf.createLiteral("1999-09-09"), "1999-09-09");
+ }
+
+ @Test
+ public void testCastPlainLiteral_date_withTimeZone_utc() {
+ testDateCast(vf.createLiteral("1999-09-09Z"), "1999-09-09Z");
+ }
+
+ @Test
+ public void testCastPlainLiteral_date_withTimeZone_offset() {
+ testDateCast(vf.createLiteral("1999-09-09-06:00"), "1999-09-09-06:00");
+ }
+
+ @Test
+ public void testCastPlainLiteral_date_invalid() {
+ // Arrange
+ Literal plainLit = vf.createLiteral("1999-09-xx");
+
+ // Act & Assert
+ assertThatExceptionOfType(ValueExprEvaluationException.class).isThrownBy(() -> dateCast.evaluate(vf, plainLit));
+ }
+
+ @Test
+ public void testCastPlainLiteral_dateTime() {
+ testDateCast(vf.createLiteral("1999-09-09T14:45:13"), "1999-09-09");
+ }
+
+ @Test
+ public void testCastPlainLiteral_dateTime_withTimeZone_utc() {
+ testDateCast(vf.createLiteral("1999-09-09T14:45:13Z"), "1999-09-09-00:00");
+ }
+
+ @Test
+ public void testCastPlainLiteral_dateTime_withTimeZone_offset() {
+ testDateCast(vf.createLiteral("1999-09-09T14:45:13-06:00"), "1999-09-09-06:00");
+ }
+
+ @Test
+ public void testCastPlainLiteral_dateTime_invalid() {
+ // Arrange
+ Literal plainLit = vf.createLiteral("1999-09-09T14:45:xx");
+
+ // Act & Assert
+ assertThatExceptionOfType(ValueExprEvaluationException.class).isThrownBy(() -> dateCast.evaluate(vf, plainLit));
+ }
+
+ @Test
+ public void testCastDateLiteral() {
+ testDateCast(vf.createLiteral("2022-11-01Z", DATE), "2022-11-01Z");
+ }
+
+ @Test
+ public void testCastDateTimeLiteral() {
+ testDateCast(vf.createLiteral(parseCalendar("1999-09-09T14:45:13")), "1999-09-09");
+ }
+
+ @Test
+ public void testCastDateTimeLiteral_withTimeZone_utc() {
+ testDateCast(vf.createLiteral(parseCalendar("1999-09-09T14:45:13Z")), "1999-09-09-00:00");
+ }
+
+ @Test
+ public void testCastDateTimeLiteral_withTimeZone_offset() {
+ testDateCast(vf.createLiteral(parseCalendar("1999-09-09T14:45:13+03:00")), "1999-09-09+03:00");
+ }
+
+ private void testDateCast(Literal literal, String expected) {
+ // Arrange
+ Literal result = null;
+
+ // Act
+ try {
+ result = dateCast.evaluate(vf, literal);
+ } catch (ValueExprEvaluationException e) {
+ fail(e.getMessage());
+ }
+
+ // Assert
+ assertNotNull(result);
+ assertThat(result.getLabel()).isEqualTo(expected);
+ assertThat(result.getDatatype()).isEqualTo(DATE);
+ }
+}
diff --git a/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/util/QueryEvaluationUtilityTest.java b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/util/QueryEvaluationUtilityTest.java
index 174e34acc7f..f684f2cf53d 100644
--- a/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/util/QueryEvaluationUtilityTest.java
+++ b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/util/QueryEvaluationUtilityTest.java
@@ -67,6 +67,10 @@ public class QueryEvaluationUtilityTest {
private Literal arg2year;
+ private Literal arg1date;
+
+ private Literal arg2date;
+
private Literal arg1dateTime;
private Literal arg2dateTime;
@@ -100,6 +104,9 @@ public void setUp() {
arg1year = f.createLiteral("2007", XSD.GYEAR);
arg2year = f.createLiteral("2009", XSD.GYEAR);
+ arg1date = f.createLiteral("2009-01-01", XSD.DATE);
+ arg2date = f.createLiteral("2007-01-01", XSD.DATE);
+
arg1dateTime = f.createLiteral("2009-01-01T20:20:20Z", XSD.DATETIME);
arg2dateTime = f.createLiteral("2007-01-01T20:20:20+02:00", XSD.DATETIME);
@@ -161,6 +168,7 @@ public void testCompareEQ() {
assertCompareTrue(arg1string, arg1string, EQ);
assertCompareTrue(arg1int, arg1int, EQ);
assertCompareTrue(arg1year, arg1year, EQ);
+ assertCompareTrue(arg1date, arg1date, EQ);
assertCompareTrue(arg1dateTime, arg1dateTime, EQ);
assertCompareTrue(arg1duration, arg1duration, EQ);
assertCompareTrue(arg1yearMonthDuration, arg1yearMonthDuration, EQ);
@@ -211,14 +219,24 @@ public void testCompareEQ() {
assertCompareException(arg1year, arg2string, EQ);
assertCompareException(arg1year, arg2int, EQ);
assertCompareFalse(arg1year, arg2year, EQ);
+ assertCompareFalse(arg1year, arg2date, EQ);
assertCompareFalse(arg1year, arg2dateTime, EQ);
assertCompareException(arg1year, arg2unknown, EQ);
+ assertCompareException(arg1date, arg2simple, EQ);
+ assertCompareFalse(arg1date, arg2en, EQ);
+ assertCompareException(arg1date, arg2string, EQ);
+ assertCompareException(arg1date, arg2int, EQ);
+ assertCompareFalse(arg1date, arg2year, EQ);
+ assertCompareFalse(arg1date, arg2date, EQ);
+ assertCompareException(arg1date, arg2unknown, EQ);
+
assertCompareException(arg1dateTime, arg2simple, EQ);
assertCompareFalse(arg1dateTime, arg2en, EQ);
assertCompareException(arg1dateTime, arg2string, EQ);
assertCompareException(arg1dateTime, arg2int, EQ);
assertCompareFalse(arg1dateTime, arg2year, EQ);
+ assertCompareFalse(arg1dateTime, arg2date, EQ);
assertCompareFalse(arg1dateTime, arg2dateTime, EQ);
assertCompareException(arg1dateTime, arg2unknown, EQ);
@@ -227,6 +245,7 @@ public void testCompareEQ() {
assertCompareException(arg1duration, arg2string, EQ);
assertCompareException(arg1duration, arg2int, EQ);
assertCompareException(arg1duration, arg2year, EQ);
+ assertCompareException(arg1duration, arg2date, EQ);
assertCompareException(arg1duration, arg2dateTime, EQ);
assertCompareException(arg1duration, arg2duration, EQ);
assertCompareFalse(arg1duration, arg2duration, EQ, false);
@@ -243,6 +262,7 @@ public void testCompareNE() {
assertCompareFalse(arg1string, arg1string, NE);
assertCompareFalse(arg1int, arg1int, NE);
assertCompareFalse(arg1year, arg1year, NE);
+ assertCompareFalse(arg1date, arg1date, NE);
assertCompareFalse(arg1dateTime, arg1dateTime, NE);
assertCompareException(arg1unknown, arg2unknown, NE);
@@ -291,14 +311,24 @@ public void testCompareNE() {
assertCompareException(arg1year, arg2string, NE);
assertCompareException(arg1year, arg2int, NE);
assertCompareTrue(arg1year, arg2year, NE);
+ assertCompareTrue(arg1year, arg2date, NE);
assertCompareTrue(arg1year, arg2dateTime, NE);
assertCompareException(arg1year, arg2unknown, NE);
+ assertCompareException(arg1date, arg2simple, NE);
+ assertCompareTrue(arg1date, arg2en, NE);
+ assertCompareException(arg1date, arg2string, NE);
+ assertCompareException(arg1date, arg2int, NE);
+ assertCompareTrue(arg1date, arg2year, NE);
+ assertCompareTrue(arg1date, arg2date, NE);
+ assertCompareException(arg1date, arg2unknown, NE);
+
assertCompareException(arg1dateTime, arg2simple, NE);
assertCompareTrue(arg1dateTime, arg2en, NE);
assertCompareException(arg1dateTime, arg2string, NE);
assertCompareException(arg1dateTime, arg2int, NE);
assertCompareTrue(arg1dateTime, arg2year, NE);
+ assertCompareTrue(arg1dateTime, arg2date, NE);
assertCompareTrue(arg1dateTime, arg2dateTime, NE);
assertCompareException(arg1dateTime, arg2unknown, NE);
@@ -307,6 +337,7 @@ public void testCompareNE() {
assertCompareException(arg1duration, arg2string, NE);
assertCompareException(arg1duration, arg2int, NE);
assertCompareException(arg1duration, arg2year, NE);
+ assertCompareException(arg1duration, arg2date, NE);
assertCompareException(arg1duration, arg2dateTime, NE);
assertCompareException(arg1duration, arg2duration, NE);
assertCompareTrue(arg1duration, arg2duration, NE, false);
@@ -325,6 +356,7 @@ public void testCompareLT() {
assertCompareFalse(arg1string, arg1string, LT);
assertCompareFalse(arg1int, arg1int, LT);
assertCompareFalse(arg1year, arg1year, LT);
+ assertCompareFalse(arg1date, arg1date, LT);
assertCompareFalse(arg1dateTime, arg1dateTime, LT);
assertCompareException(arg1unknown, arg2unknown, LT);
@@ -361,21 +393,35 @@ public void testCompareLT() {
assertCompareException(arg1year, arg2int, LT);
assertCompareTrue(arg1year, arg2year, LT);
- // comparison between xsd:gYear and xsd:dateTime should raise type error in strict mode
+ // comparison between xsd:gYear and xsd:dateTime & xsd:date should raise type error in strict mode
+ assertCompareException(arg1year, arg1date, LT);
assertCompareException(arg1year, arg1dateTime, LT);
// ... but should succeed in extended mode.
+ assertCompareTrue(arg1year, arg1date, LT, false);
assertCompareTrue(arg1year, arg1dateTime, LT, false);
+ assertCompareException(arg1year, arg2date, LT);
assertCompareException(arg1year, arg2dateTime, LT);
assertCompareException(arg1year, arg2unknown, LT);
+ assertCompareException(arg1date, arg2simple, LT);
+ assertCompareException(arg1date, arg2en, LT);
+ assertCompareException(arg1date, arg2string, LT);
+ assertCompareException(arg1date, arg2int, LT);
+ assertCompareFalse(arg1date, arg1year, LT, false);
+ assertCompareException(arg1date, arg2year, LT);
+ assertCompareFalse(arg1date, arg2date, LT);
+ assertCompareFalse(arg1date, arg2dateTime, LT);
+ assertCompareException(arg1date, arg2unknown, LT);
+
assertCompareException(arg1dateTime, arg2simple, LT);
assertCompareException(arg1dateTime, arg2en, LT);
assertCompareException(arg1dateTime, arg2string, LT);
assertCompareException(arg1dateTime, arg2int, LT);
assertCompareFalse(arg1dateTime, arg1year, LT, false);
assertCompareException(arg1dateTime, arg2year, LT);
+ assertCompareFalse(arg1dateTime, arg2date, LT);
assertCompareFalse(arg1dateTime, arg2dateTime, LT);
assertCompareException(arg1dateTime, arg2unknown, LT);
@@ -384,6 +430,7 @@ public void testCompareLT() {
assertCompareException(arg1duration, arg2string, LT);
assertCompareException(arg1duration, arg2int, LT);
assertCompareException(arg1duration, arg2year, LT);
+ assertCompareException(arg1duration, arg2date, LT);
assertCompareException(arg1duration, arg2dateTime, LT);
assertCompareException(arg1duration, arg2duration, LT);
assertCompareTrue(arg1duration, arg2duration, LT, false);
@@ -396,6 +443,7 @@ public void testCompareLT() {
assertCompareException(arg1yearMonthDuration, arg2string, LT);
assertCompareException(arg1yearMonthDuration, arg2int, LT);
assertCompareException(arg1yearMonthDuration, arg2year, LT);
+ assertCompareException(arg1yearMonthDuration, arg2date, LT);
assertCompareException(arg1yearMonthDuration, arg2dateTime, LT);
assertCompareException(arg1yearMonthDuration, arg2duration, LT);
assertCompareTrue(arg1yearMonthDuration, arg2duration, LT, false);
diff --git a/testsuites/sparql/src/main/java/org/eclipse/rdf4j/testsuite/sparql/tests/BuiltinFunctionTest.java b/testsuites/sparql/src/main/java/org/eclipse/rdf4j/testsuite/sparql/tests/BuiltinFunctionTest.java
index a2341eff23b..886755e90a2 100644
--- a/testsuites/sparql/src/main/java/org/eclipse/rdf4j/testsuite/sparql/tests/BuiltinFunctionTest.java
+++ b/testsuites/sparql/src/main/java/org/eclipse/rdf4j/testsuite/sparql/tests/BuiltinFunctionTest.java
@@ -308,4 +308,57 @@ public void testFilterRegexBoolean() throws Exception {
assertEquals(1, count);
}
}
+
+ @Test
+ public void testDateCastFunction_date() {
+ String qry = "PREFIX xsd: "
+ + "SELECT (xsd:date(\"2022-09-09\") AS ?date) { }";
+
+ try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
+ assertNotNull(result);
+ assertTrue(result.hasNext());
+ assertEquals("2022-09-09", result.next().getValue("date").stringValue());
+ assertFalse(result.hasNext());
+ }
+ }
+
+ @Test
+ public void testDateCastFunction_date_withTimeZone_utc() {
+ String qry = "PREFIX xsd: "
+ + "SELECT (xsd:date(\"2022-09-09Z\") AS ?date) { }";
+
+ try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
+ assertNotNull(result);
+ assertTrue(result.hasNext());
+ assertEquals("2022-09-09Z", result.next().getValue("date").stringValue());
+ assertFalse(result.hasNext());
+ }
+ }
+
+ @Test
+ public void testDateCastFunction_dateTime_withTimeZone_offset() {
+ String qry = "PREFIX xsd: "
+ + "SELECT (xsd:date(\"2022-09-09T14:45:13+03:00\") AS ?date) { }";
+
+ try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
+ assertNotNull(result);
+ assertTrue(result.hasNext());
+ assertEquals("2022-09-09+03:00", result.next().getValue("date").stringValue());
+ assertFalse(result.hasNext());
+ }
+ }
+
+ @Test
+ public void testDateCastFunction_invalidInput() {
+ String qry = "PREFIX xsd: "
+ + "SELECT (xsd:date(\"2022-09-xx\") AS ?date) { }";
+
+ try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
+ assertNotNull(result);
+ assertTrue(result.hasNext());
+ assertFalse("There should be no binding because the cast should have failed.",
+ result.next().hasBinding("date"));
+ assertFalse(result.hasNext());
+ }
+ }
}