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()); + } + } }