This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 7a07d1cba13a32346a7a1d8b41b196ec951bf769
Author: Andy Seaborne <[email protected]>
AuthorDate: Tue Nov 4 14:00:45 2025 +0000

    GH-3562: Better NodeValue
---
 .../expr/{NodeValueCmp.java => NVCompare.java}     |   2 +-
 .../org/apache/jena/sparql/expr/NVDatatypes.java   |  95 +++++
 .../org/apache/jena/sparql/expr/NVFactory.java     | 242 +++++++++++
 .../java/org/apache/jena/sparql/expr/NVOps.java    | 154 +++++++
 .../org/apache/jena/sparql/expr/NodeValue.java     | 365 ++++------------
 .../sparql/expr/nodevalue/NodeValueDateTime.java   |  15 -
 .../java/org/apache/jena/sparql/expr/TS_Expr.java  |   3 +-
 .../apache/jena/sparql/expr/TestComparison.java    |   2 +-
 .../org/apache/jena/sparql/expr/TestNVFactory.java | 457 +++++++++++++++++++++
 .../org/apache/jena/sparql/expr/TestNodeValue.java |  32 +-
 .../apache/jena/sparql/expr/TestSortOrdering.java  |  16 +-
 .../querybuilder/rewriters/NodeValueRewriter.java  |   2 +-
 .../rewriters/NodeValueRewriterTest.java           |   3 +-
 13 files changed, 1062 insertions(+), 326 deletions(-)

diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValueCmp.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVCompare.java
similarity index 99%
rename from jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValueCmp.java
rename to jena-arq/src/main/java/org/apache/jena/sparql/expr/NVCompare.java
index eb66c7acae..5f12156779 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValueCmp.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVCompare.java
@@ -39,7 +39,7 @@ import org.apache.jena.sparql.expr.nodevalue.NodeFunctions;
 import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp;
 import org.apache.jena.sparql.util.NodeCmp;
 
-public class NodeValueCmp {
+class NVCompare {
 
     // ----------------------------------------------------------------
     // ---- sameValueAs
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVDatatypes.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVDatatypes.java
new file mode 100644
index 0000000000..6cd527a772
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVDatatypes.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.expr;
+
+import java.util.Set;
+
+import org.apache.jena.datatypes.RDFDatatype;
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.vocabulary.RDF;
+
+/** Constants related to datatypes with values support in ARQ/SPARQL. */
+class NVDatatypes {
+
+    /*
+     * Datatype representing xsd:precisionDecimal 
https://www.w3.org/TR/xsd-precisionDecimal/
+     */
+    // Not a derived type of xsd:decimal.
+    //public static final RDFDatatype XSDprecisionDecimal = new 
XSDPRecisionDecimal("precisionDecimal", BigDecimal.class);
+
+    public static final RDFDatatype XSDdecimal = XSDDatatype.XSDdecimal;
+    public static final RDFDatatype XSDfloat = XSDDatatype.XSDfloat;
+    public static final RDFDatatype XSDdouble = XSDDatatype.XSDdouble;
+
+    public static final RDFDatatype XSDinteger = XSDDatatype.XSDinteger;
+    public static final RDFDatatype XSDpositiveInteger = 
XSDDatatype.XSDpositiveInteger;
+    public static final RDFDatatype XSDnegativeInteger = 
XSDDatatype.XSDnegativeInteger;
+    public static final RDFDatatype XSDnonPositiveInteger = 
XSDDatatype.XSDnonPositiveInteger;
+    public static final RDFDatatype XSDnonNegativeInteger = 
XSDDatatype.XSDnonNegativeInteger;
+
+    public static final RDFDatatype XSDlong = XSDDatatype.XSDlong;
+    public static final RDFDatatype XSDint = XSDDatatype.XSDint;
+    public static final RDFDatatype XSDshort = XSDDatatype.XSDshort;
+    public static final RDFDatatype XSDbyte = XSDDatatype.XSDbyte;
+
+    public static final RDFDatatype XSDunsignedLong = 
XSDDatatype.XSDunsignedLong;
+    public static final RDFDatatype XSDunsignedInt = 
XSDDatatype.XSDunsignedInt;
+    public static final RDFDatatype XSDunsignedShort = 
XSDDatatype.XSDunsignedShort;
+    public static final RDFDatatype XSDunsignedByte = 
XSDDatatype.XSDunsignedByte;
+
+    public static final RDFDatatype XSDboolean = XSDDatatype.XSDboolean;
+
+    public static final RDFDatatype XSDstring = XSDDatatype.XSDstring;
+    public static final RDFDatatype langString = RDF.dtLangString;
+    public static final RDFDatatype dirLangString = RDF.dtDirLangString;
+
+
+    public static final RDFDatatype XSDnormalizedString = 
XSDDatatype.XSDnormalizedString;
+//    public static final RDFDatatype XSDtoken = XSDDatatype.XSDtoken;
+//    public static final RDFDatatype XSDlanguage = XSDDatatype.XSDlanguage;
+
+//    public static final RDFDatatype XSDhexBinary = XSDDatatype.XSDhexBinary;
+//    public static final RDFDatatype XSDbase64Binary = 
XSDDatatype.XSDbase64Binary;
+
+    public static final RDFDatatype XSDdateTime = XSDDatatype.XSDdateTime;
+    public static final RDFDatatype XSDdateTimeStamp = 
XSDDatatype.XSDdateTimeStamp;
+    public static final RDFDatatype XSDdate = XSDDatatype.XSDdate;
+    public static final RDFDatatype XSDtime = XSDDatatype.XSDtime;
+
+    public static final RDFDatatype XSDduration = XSDDatatype.XSDduration;
+    public static final RDFDatatype XSDdayTimeDuration = 
XSDDatatype.XSDdayTimeDuration;
+    public static final RDFDatatype XSDyearMonthDuration = 
XSDDatatype.XSDyearMonthDuration;
+
+    public static final RDFDatatype XSDgYear = XSDDatatype.XSDgYear;
+    public static final RDFDatatype XSDgMonth = XSDDatatype.XSDgMonth;
+    public static final RDFDatatype XSDgDay = XSDDatatype.XSDgDay;
+    public static final RDFDatatype XSDgYearMonth = XSDDatatype.XSDgYearMonth;
+    public static final RDFDatatype XSDgMonthDay = XSDDatatype.XSDgMonthDay;
+
+    public static final Set<RDFDatatype> numerics = Set.of(XSDdecimal, 
XSDfloat, XSDdouble, XSDinteger,
+                                                           XSDpositiveInteger, 
XSDnegativeInteger, XSDnonPositiveInteger, XSDnonNegativeInteger,
+                                                           XSDlong, XSDint, 
XSDshort, XSDbyte,
+                                                           XSDunsignedLong, 
XSDunsignedInt, XSDunsignedShort, XSDunsignedByte);
+
+    public static final Set<RDFDatatype> durations = Set.of(XSDduration, 
XSDdayTimeDuration, XSDyearMonthDuration);
+
+    public static final Set<RDFDatatype> temporal = Set.of(XSDdateTime, 
XSDdateTimeStamp,XSDdate, XSDtime,
+                                                           XSDgYear, 
XSDgMonth, XSDgDay,
+                                                           XSDgYearMonth, 
XSDgMonthDay);
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVFactory.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVFactory.java
new file mode 100644
index 0000000000..5da176ba7d
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVFactory.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.expr;
+
+import static org.apache.jena.sparql.expr.NVDatatypes.*;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.datatype.Duration;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.apache.jena.datatypes.RDFDatatype;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.impl.LiteralLabel;
+import org.apache.jena.sparql.ARQInternalErrorException;
+import org.apache.jena.sparql.SystemARQ;
+import org.apache.jena.sparql.expr.nodevalue.*;
+import org.apache.jena.sparql.util.RomanNumeral;
+import org.apache.jena.sparql.util.RomanNumeralDatatype;
+import org.apache.jena.vocabulary.RDF;
+
+class NVFactory {
+    @FunctionalInterface
+    interface ToNodeValue { NodeValue create(RDFDatatype datatype, Node node); 
}
+
+    private static Map<RDFDatatype, ToNodeValue> mapper = dtSetup();
+
+    public static NodeValue create(Node node) {
+        RDFDatatype datatype = node.getLiteralDatatype();
+        ToNodeValue function = mapper.get(datatype);
+        if ( function == null )
+            return new NodeValueNode(node);
+        NodeValue nv = function.create(datatype, node);
+        if ( nv == null )
+            return new NodeValueNode(node);
+        return nv;
+    }
+
+    public static NodeValue create(RDFDatatype datatype, Node node) {
+        return mapper.get(datatype).create(datatype, node);
+    }
+
+    /** Create an immutable map of datatype to NodeValue maker */
+    private static Map<RDFDatatype, ToNodeValue> dtSetup() {
+        Map<RDFDatatype, ToNodeValue> mapper = new HashMap<>();
+
+        entry(mapper, XSDdecimal,            NVFactory::decimalMaker);
+        entry(mapper, XSDfloat,              NVFactory::floatMaker);
+        entry(mapper, XSDdouble,             NVFactory::doubleMaker);
+
+        entry(mapper, XSDinteger,            NVFactory::integerMaker);
+        entry(mapper, XSDnonPositiveInteger, NVFactory::integerMaker);
+        entry(mapper, XSDnonNegativeInteger, NVFactory::integerMaker);
+        entry(mapper, XSDpositiveInteger,    NVFactory::integerMaker);
+        entry(mapper, XSDnegativeInteger,    NVFactory::integerMaker);
+
+        entry(mapper, XSDbyte,               NVFactory::integerMaker);
+        entry(mapper, XSDshort,              NVFactory::integerMaker);
+        entry(mapper, XSDint,                NVFactory::integerMaker);
+        entry(mapper, XSDlong,               NVFactory::integerMaker);
+
+        entry(mapper, XSDunsignedByte,       NVFactory::integerMaker);
+        entry(mapper, XSDunsignedShort,      NVFactory::integerMaker);
+        entry(mapper, XSDunsignedInt,        NVFactory::integerMaker);
+        entry(mapper, XSDunsignedLong,       NVFactory::integerMaker);
+
+        entry(mapper, XSDboolean,            NVFactory::booleanMaker);
+
+        entry(mapper, XSDstring,             NVFactory::stringMaker);
+        //[DT] XXX needs validation
+        entry(mapper, XSDnormalizedString,  NVFactory::stringMaker);
+        //[DT] XXX xsd;token, xsd:language
+
+        entry(mapper, RDF.dtLangString,      NVFactory::langStringMaker);
+        entry(mapper, RDF.dtDirLangString,   NVFactory::dirLangStringMaker);
+
+//        entry(mapper, XSDhexBinary, null);
+//        entry(mapper, XSDbase64Binary, null);
+
+        entry(mapper, XSDdate,               NVFactory::dateTimeMaker);
+        entry(mapper, XSDtime,               NVFactory::dateTimeMaker);
+        entry(mapper, XSDdateTime,           NVFactory::dateTimeMaker);
+        entry(mapper, XSDdateTimeStamp,      NVFactory::dateTimeMaker);
+
+        entry(mapper, XSDgDay,               NVFactory::dateTimeMaker);
+        entry(mapper, XSDgMonth,             NVFactory::dateTimeMaker);
+        entry(mapper, XSDgYear,              NVFactory::dateTimeMaker);
+        entry(mapper, XSDgYearMonth,         NVFactory::dateTimeMaker);
+        entry(mapper, XSDgMonthDay,          NVFactory::dateTimeMaker);
+
+        entry(mapper, XSDduration,           NVFactory::durationMaker);
+        entry(mapper, XSDdayTimeDuration,    NVFactory::durationMaker);
+        entry(mapper, XSDyearMonthDuration,  NVFactory::durationMaker);
+
+        if ( SystemARQ.EnableRomanNumerals )
+            entry(mapper, RomanNumeralDatatype.get(), 
NVFactory::romanNumeralMaker);
+
+        return Map.copyOf(mapper);
+    }
+
+    private static void entry( Map<RDFDatatype, ToNodeValue> map, RDFDatatype 
rdfDatatype, ToNodeValue toNodeValue) {
+        map.put(rdfDatatype, toNodeValue);
+    }
+
+    private static NodeValue integerMaker(RDFDatatype datatype, Node node) {
+        if ( ! node.getLiteral().isWellFormed() )
+            return null;
+        String trimmedLexical = node.getLiteralLexicalForm().trim();
+        if ( ! datatype.isValid(trimmedLexical) )
+            return null;
+        BigInteger bigInteger = new BigInteger(trimmedLexical);
+        return new NodeValueInteger(bigInteger, node);
+    }
+
+    private static NodeValue floatMaker(RDFDatatype datatype, Node node) {
+        if ( ! node.getLiteral().isWellFormed() )
+            return null;
+        // Uses getValue - no harm using isWellformed.
+        LiteralLabel lit = node.getLiteral();
+        float f = ((Number)lit.getValue()).floatValue();
+        return new NodeValueFloat(f, node);
+    }
+
+    private static NodeValue doubleMaker(RDFDatatype datatype, Node node) {
+        LiteralLabel lit = node.getLiteral();
+        if ( ! lit.isWellFormed() )
+            return null;
+        double d = ((Number)lit.getValue()).doubleValue();
+        return new NodeValueDouble(d, node);
+    }
+
+    private static NodeValue decimalMaker(RDFDatatype datatype, Node node) {
+        LiteralLabel lit = node.getLiteral();
+        if ( ! lit.isWellFormed() )
+            return null;
+        String trimmedLexical = node.getLiteralLexicalForm().trim();
+        // jena-core narrows dataypes.
+        BigDecimal decimal = new BigDecimal(trimmedLexical);
+        return new NodeValueDecimal(decimal, node);
+    }
+
+    private static NodeValue booleanMaker(RDFDatatype datatype, Node node) {
+        LiteralLabel lit = node.getLiteral();
+        if ( ! lit.isWellFormed() )
+            return null;
+        boolean b = (Boolean) lit.getValue();
+        return new NodeValueBoolean(b, node);
+    }
+
+    private static NodeValue stringMaker(RDFDatatype datatype, Node node) {
+        return new NodeValueString(node.getLiteralLexicalForm(), node);
+    }
+
+    private static NodeValue langStringMaker(RDFDatatype datatype, Node node) {
+        return new NodeValueLang(node);
+    }
+
+    private static NodeValue dirLangStringMaker(RDFDatatype datatype, Node 
node) {
+        return new NodeValueLangDir(node);
+    }
+
+    private static NodeValue dateTimeMaker(RDFDatatype datatype, Node node) {
+        String trimmedLexical = node.getLiteralLexicalForm().trim();
+        try {
+            XMLGregorianCalendar gCal = 
createXMLGregorianCalendar(trimmedLexical, node);
+            // Check the expected fields.
+            boolean isCorrect = NVOps.checkCalendarInstance(gCal, datatype);
+            if (! isCorrect )
+                return null;
+            return new NodeValueDateTime(gCal, node);
+        } catch (IllegalArgumentException ex) {
+            return null;
+        }
+    }
+
+    // Fixup
+    private static XMLGregorianCalendar createXMLGregorianCalendar(String lex, 
Node n) {
+        // Java bug : gMonth with a timezone of Z causes 
IllegalArgumentException
+        if ( XSDgMonth.equals(n.getLiteralDatatype()) ) {
+            if ( lex.endsWith("Z") ) {
+                String lex2 = lex.substring(0, lex.length() - 1);
+                XMLGregorianCalendar gCal = 
NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex2);
+                gCal.setTimezone(0);
+                return gCal;
+            }
+        }
+        XMLGregorianCalendar gCal = 
NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex);
+        return gCal;
+    }
+
+    private static NodeValue durationMaker(RDFDatatype datatype, Node node) {
+        String trimmedLexical = node.getLiteralLexicalForm().trim();
+        try {
+            Duration duration = 
NodeValue.xmlDatatypeFactory.newDuration(trimmedLexical);
+            boolean isCorrect = NVOps.checkDurationInstance(duration, 
datatype);
+            if (! isCorrect )
+                return null;
+            return new NodeValueDuration(duration, node);
+        } catch (IllegalArgumentException ex) {
+            return null;
+        }
+    }
+
+    // Test extension type!
+    private static NodeValue romanNumeralMaker(RDFDatatype datatype, Node 
node) {
+        LiteralLabel lit = node.getLiteral();
+        Object obj = RomanNumeralDatatype.get().parse(lit.getLexicalForm());
+        if ( obj instanceof Integer )
+            return new NodeValueInteger(((Integer)obj).longValue());
+        if ( obj instanceof RomanNumeral )
+            return new NodeValueInteger( ((RomanNumeral)obj).intValue() );
+        throw new ARQInternalErrorException("DatatypeFormatException: Roman 
numeral is unknown class");
+    }
+
+    /**
+     * Converts a hexBinary literal node to a NodeValueNode.
+     * Assumes the node is a valid hexBinary literal.
+     */
+    private static NodeValue hexBinaryToNodeValue(RDFDatatype datatype, Node 
node) {
+        // No conversion, just wrap the node as a NodeValueNode
+        return new NodeValueNode(node);
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVOps.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVOps.java
new file mode 100644
index 0000000000..4c86f3469c
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVOps.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.expr;
+
+import static org.apache.jena.sparql.expr.NVDatatypes.*;
+import static java.util.Map.entry;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import javax.xml.datatype.DatatypeConstants;
+import javax.xml.datatype.Duration;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.apache.jena.datatypes.RDFDatatype;
+
+/** Operations related to data/times */
+class NVOps {
+
+    // ---- Data/time with XMLGregorianCalendar
+
+    private static final Map<RDFDatatype, Predicate<XMLGregorianCalendar>> 
TEMPORAL_VALIDATE = Map.ofEntries(
+             // date/time
+             entry(XSDdateTime,
+                       gCal -> checkDateFields(gCal, true) && 
checkTimeFields(gCal, true)),
+             entry(XSDdateTimeStamp,
+                       gCal -> checkDateFields(gCal, true) && 
checkTimeFields(gCal, true) && checkTimezoneField(gCal, true)),
+             entry(XSDdate,
+                       gCal -> checkDateFields(gCal, true) && 
checkTimeFields(gCal, false)),
+             entry(XSDtime,
+                       gCal -> checkDateFields(gCal, false) && 
checkTimeFields(gCal, true)),
+             // g*
+             entry(XSDgYear,
+                       gCal -> checkDateFields(gCal, true, false, false) && 
checkTimeFields(gCal, false)),
+             entry(XSDgYearMonth,
+                       gCal -> checkDateFields(gCal, true, true, false) && 
checkTimeFields(gCal, false)),
+             entry(XSDgMonth,
+                       gCal -> checkDateFields(gCal, false, true, false) && 
checkTimeFields(gCal, false)),
+             entry(XSDgMonthDay,
+                       gCal -> checkDateFields(gCal, false, true, true) && 
checkTimeFields(gCal, false)),
+             entry(XSDgDay,
+                       gCal -> checkDateFields(gCal, false, false, true) && 
checkTimeFields(gCal, false))
+            );
+
+    static boolean checkCalendarInstance(XMLGregorianCalendar gCal, 
RDFDatatype xsdDatatype) {
+        Objects.requireNonNull(xsdDatatype);
+        var predicate = TEMPORAL_VALIDATE.get(xsdDatatype);
+        return predicate.test(gCal);
+    }
+
+    // No need to check getEonAndYear or getFractionalSecond.
+    // These can not be set unless their companion field is set.
+
+    /** Check the set/undefined status of the 6 fields (not timezone, which is 
optional except in xsd:dateTimeStamp) */
+    private static boolean checkFields(XMLGregorianCalendar gCal, boolean 
yearField, boolean monthField, boolean dayField, boolean hourField, boolean 
minuteField, boolean secondField) {
+        return checkDateFields(gCal, yearField, monthField, dayField) && 
checkTimeFields(gCal, hourField, minuteField, secondField);
+    }
+
+    /** Check the set/undefined status of the date fields. */
+    private static boolean checkDateFields(XMLGregorianCalendar gCal, boolean 
present) {
+        return checkDateFields(gCal, present, present, present);
+    }
+
+    /** Check the set/undefined status of the 3 date fields. */
+    private static boolean checkDateFields(XMLGregorianCalendar gCal, boolean 
yearField, boolean monthField, boolean dayField) {
+        if ( ! checkTemporalField(gCal.getYear(), yearField) )
+            return false;
+        // No need to check getEonAndYear -- getYear will be set.
+        //gCal.getEonAndYear()
+        if ( ! checkTemporalField(gCal.getMonth(),monthField ) )
+            return false;
+        if ( ! checkTemporalField(gCal.getDay() , dayField ) )
+            return false;
+        return true;
+    }
+
+    /** Check the set/undefined status of the time fields. */
+    private static boolean checkTimeFields(XMLGregorianCalendar gCal, boolean 
present) {
+        return checkTimeFields(gCal, present, present, present);
+    }
+
+    /** Check the set/undefined status of the 3 time fields. */
+    private static boolean checkTimeFields(XMLGregorianCalendar gCal, boolean 
hourField, boolean minuteField, boolean secondField) {
+        if ( ! checkTemporalField(gCal.getHour(), hourField ) )
+            return false;
+        if ( ! checkTemporalField(gCal.getMinute(), minuteField ) )
+            return false;
+        if ( ! checkTemporalField(gCal.getSecond(), secondField ) )
+            return false;
+        // No need to check getFractionalSecond -- getSecond will be set.
+        //gCal.getFractionalSecond();
+        return true;
+    }
+
+    /** Check the set/undefined status of the timezone field. */
+    private static boolean checkTimezoneField(XMLGregorianCalendar gCal, 
boolean timezonePresent) {
+        return checkTemporalField(gCal.getTimezone(), timezonePresent);
+    }
+
+    /** Check the set/undefined status of a field value in an 
XMLGregorialCalendar. */
+    private static boolean checkTemporalField(int fieldValue, boolean isSet) {
+        return ( fieldValue != DatatypeConstants.FIELD_UNDEFINED ) == isSet;
+    }
+
+    // ---- Duration
+    // A Duration of nothing set is illegal so we only need to test of missing
+
+    private static final Map<RDFDatatype, Predicate<Duration>> 
DURATION_VALIDATE = Map.ofEntries
+            (
+             entry(XSDduration,          dur -> true),
+             entry(XSDdayTimeDuration,   dur ->isDayTimeDuration(dur)),
+             entry(XSDyearMonthDuration, dur -> isYearMonthDuration(dur))
+            );
+
+    static boolean checkDurationInstance(Duration duration, RDFDatatype 
xsdDatatype) {
+        Objects.requireNonNull(xsdDatatype);
+        var predicate = DURATION_VALIDATE.get(xsdDatatype);
+        return predicate.test(duration);
+    }
+
+    static boolean isDayTimeDuration(Duration duration) {
+        return checkDurationField(duration, DatatypeConstants.YEARS, false) &&
+               checkDurationField(duration, DatatypeConstants.MONTHS, false) ;
+    }
+
+    static boolean isYearMonthDuration(Duration duration) {
+        return checkDurationField(duration, DatatypeConstants.DAYS, false) &&
+               checkDurationField(duration, DatatypeConstants.HOURS, false) &&
+               checkDurationField(duration, DatatypeConstants.MINUTES, false) 
&&
+               checkDurationField(duration, DatatypeConstants.SECONDS, false) ;
+    }
+
+    /** Check the set/undefined status of a field value in a Duration. */
+    private static boolean checkDurationField(Duration duration, 
DatatypeConstants.Field field, boolean isSet) {
+        return duration.isSet(field) == isSet;
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java
index 877007806a..0e619b453a 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java
@@ -18,7 +18,6 @@
 
 package org.apache.jena.sparql.expr;
 
-import static javax.xml.datatype.DatatypeConstants.*;
 import static org.apache.jena.datatypes.xsd.XSDDatatype.*;
 import static org.apache.jena.sparql.expr.ValueSpace.VSPACE_DIFFERENT;
 import static org.apache.jena.sparql.expr.ValueSpace.VSPACE_UNKNOWN;
@@ -34,17 +33,12 @@ import javax.xml.datatype.XMLGregorianCalendar;
 
 import org.apache.jena.atlas.lib.DateTimeUtils;
 import org.apache.jena.atlas.logging.Log;
-import org.apache.jena.datatypes.DatatypeFormatException;
 import org.apache.jena.datatypes.RDFDatatype;
 import org.apache.jena.datatypes.TypeMapper;
-import org.apache.jena.datatypes.xsd.XSDDatatype;
 import org.apache.jena.ext.xerces.DatatypeFactoryInst;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.graph.TextDirection;
-import org.apache.jena.graph.impl.LiteralLabel;
-import org.apache.jena.sparql.ARQInternalErrorException;
-import org.apache.jena.sparql.SystemARQ;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.binding.Binding;
@@ -53,7 +47,11 @@ import org.apache.jena.sparql.function.FunctionEnv;
 import org.apache.jena.sparql.graph.NodeConst;
 import org.apache.jena.sparql.graph.NodeTransform;
 import org.apache.jena.sparql.serializer.SerializationContext;
-import org.apache.jena.sparql.util.*;
+import org.apache.jena.sparql.sse.SSE;
+import org.apache.jena.sparql.util.FmtUtils;
+import org.apache.jena.sparql.util.NodeFactoryExtra;
+import org.apache.jena.sparql.util.NodeUtils;
+import org.apache.jena.sparql.util.XSDNumUtils;
 import org.apache.jena.sys.JenaSystem;
 import org.apache.jena.vocabulary.RDF;
 import org.slf4j.Logger;
@@ -63,56 +61,34 @@ public abstract class NodeValue extends ExprNode
 {
     static { JenaSystem.init(); }
 
-    // Maybe:: NodeValueStringLang - strings with language tag
-
-    /* Naming:
-     * getXXX => plain accessor
-     * asXXX =>  force to the required thing if necessary.
-     *
+    /*
      * Implementation notes:
      *
-     * 1. There is little point delaying turning a node into its value
-     *    because it has to be verified anyway (e.g. illegal literals).
-     *    Because a NodeValue is being created, it is reasonably likely it
-     *    is going to be used for it's value, so processing the datatype
-     *    can be done at creation time where it is clearer.
+     * Delaying turning a value into a graph Node is
+     * valuable because intermediates, like the result of 2+3, will not
+     * be needed as nodes unless used for assignment.
      *
-     * 2. Conversely, delaying turning a value into a graph node is
-     *    valuable because intermediates, like the result of 2+3, will not
-     *    be needed as nodes unless assignment (and there is no assignment
-     *    in SPARQL even if there is for ARQ).
-     *    Node level operations like str() don't need a full node.
+     * Operations:
+     * See also NV* for NodeValue code.
+     * See also NodeValueCmp for comparison operations.
+     * See also XSDFuncOp for XQuery/XPath functions.
+     * See also NodeFunctions for RDF Term related functions.
      *
-     * 3. nodevalue.NodeFunctions contains the SPARQL builtin implementations.
-     *    nodevalue.XSDFuncOp contains the implementation of the XQuery/Xpath
-     *    functions and operations.
-     *    See also NodeUtils.
      *
-     * 4. Note that SPARQL "=" is "known to be sameValueAs". Similarly "!=" is
-     *    known to be different.
-     *
-     * 5. To add a new number type:
-     *    Add sub type into nodevalue.NodeValueXXX
-     *      Must implement .hashCode() and .equals() based on value.
-     *    Add Functions.add/subtract/etc code and compareNumeric
-     *    Add to compare code
-     *    Fix TestExprNumeric
-     *    Write lots of tests.
-     *    Library code Maths1 and Maths2 for maths functions
+     * Note that SPARQL "=" is "known to be sameValueAs".
+     * Similarly "!=" is known to be different.
      */
 
     /*
      * Effective boolean value rules.
      *    boolean: value of the boolean
      *    string: length(string) > 0 is true
-     *    numeric: number != Nan && number != 0 is true
+     *    numeric: number != NaN && number != 0 is true
      * ref:  http://www.w3.org/TR/xquery/#dt-ebv
      */
 
     private static Logger log = LoggerFactory.getLogger(NodeValue.class);
 
-    // ---- Constants and initializers / public
-
     public static boolean VerboseWarnings = true;
     public static boolean VerboseExceptions = false;
 
@@ -146,7 +122,6 @@ public abstract class NodeValue extends ExprNode
 
     private Node node = null;     // Null used when a value has not been 
turned into a Node.
 
-    // Don't create direct - the static builders manage the value/node 
relationship
     protected NodeValue() { super(); }
     protected NodeValue(Node n) { super(); node = n; }
 
@@ -155,24 +130,16 @@ public abstract class NodeValue extends ExprNode
         return Set.of();
     }
 
-//    protected makeNodeValue(NodeValue nv)
-//    {
-//        if ( v.isNode() )    { ... }
-//        if ( v.isBoolean() ) { ... }
-//        if ( v.isInteger() ) { ... }
-//        if ( v.isDouble() )  { ... }
-//        if ( v.isDecimal() ) { ... }
-//        if ( v.isString() )  { ... }
-//        if ( v.isDate() )    { ... }
-//    }
-
-    // ----------------------------------------------------------------
-    // ---- Construct NodeValue without a graph node.
-
-    /** Convenience operation - parse a string to produce a NodeValue - common 
namespaces like xsd: are built-in */
+    /**
+     * Convenience operation,primarily for tests.
+     * Parse a string (using {@link SSE}) to produce a NodeValue - common 
namespaces like xsd: are built-in
+     */
     public static NodeValue parse(String string)
     { return makeNode(NodeFactoryExtra.parseNode(string)); }
 
+    // ----------------------------------------------------------------
+    // ---- Construct NodeValue without a graph node (calculated later if 
needed)
+
     public static NodeValue makeInteger(long i)
     { return new NodeValueInteger(BigInteger.valueOf(i)); }
 
@@ -221,6 +188,8 @@ public abstract class NodeValue extends ExprNode
     public static NodeValue makeDate(String lexicalForm)
     { return NodeValue.makeNode(lexicalForm, XSDdate); }
 
+    /** @deprecated Use a XMLGregorianCalendar */
+    @Deprecated(forRemoval=true)
     public static NodeValue makeDateTime(Calendar cal) {
         String lex = DateTimeUtils.calendarToXSDDateTimeString(cal);
         return NodeValue.makeNode(lex, XSDdateTime);
@@ -229,9 +198,11 @@ public abstract class NodeValue extends ExprNode
     public static NodeValue makeDateTime(XMLGregorianCalendar cal) {
         String lex = cal.toXMLFormat();
         Node node = NodeFactory.createLiteralDT(lex, XSDdateTime);
-        return NodeValueDateTime.create(lex, node);
+        return new NodeValueDateTime(cal, node);
     }
 
+    /** @deprecated Use a XMLGregorianCalendar */
+    @Deprecated(forRemoval=true)
     public static NodeValue makeDate(Calendar cal) {
         String lex = DateTimeUtils.calendarToXSDDateString(cal);
         return NodeValue.makeNode(lex, XSDdate);
@@ -240,7 +211,7 @@ public abstract class NodeValue extends ExprNode
     public static NodeValue makeDate(XMLGregorianCalendar cal) {
         String lex = cal.toXMLFormat();
         Node node = NodeFactory.createLiteralDT(lex, XSDdate);
-        return NodeValueDateTime.create(lex, node);
+        return new NodeValueDateTime(cal, node);
     }
 
     public static NodeValue makeDuration(String lexicalForm)
@@ -249,9 +220,6 @@ public abstract class NodeValue extends ExprNode
     public static NodeValue makeDuration(Duration duration)
     { return new NodeValueDuration(duration); }
 
-    public static NodeValue makeNodeDuration(Duration duration, Node node)
-    { return new NodeValueDuration(duration, node); }
-
     public static NodeValue makeBoolean(boolean b)
     { return b ? NodeValue.TRUE : NodeValue.FALSE; }
 
@@ -267,7 +235,7 @@ public abstract class NodeValue extends ExprNode
 
     public static NodeValue makeNode(String lexicalForm, RDFDatatype dtype) {
         Node n = NodeFactory.createLiteralDT(lexicalForm, dtype);
-        return NodeValue.makeNode(n);
+        return makeNode(n);
     }
 
     // Convenience - knows that lang tags aren't allowed with datatypes.
@@ -302,64 +270,46 @@ public abstract class NodeValue extends ExprNode
     public static NodeValue makeNodeBoolean(boolean b)
     { return b ? NodeValue.TRUE : NodeValue.FALSE; }
 
-    public static NodeValue makeNodeBoolean(String lexicalForm) {
-        return makeNode(lexicalForm, null, XSDboolean.getURI());
-    }
+    public static NodeValue makeNodeBoolean(String lexicalForm)
+    { return makeNode(lexicalForm, XSDboolean); }
 
-    public static NodeValue makeNodeInteger(long v) {
-        return makeNode(Long.toString(v), null, XSDinteger.getURI());
-    }
+    public static NodeValue makeNodeInteger(long v)
+    { return makeNode(Long.toString(v), XSDinteger); }
 
-    public static NodeValue makeNodeInteger(String lexicalForm) {
-        return makeNode(lexicalForm, null, XSDinteger.getURI());
-    }
+    public static NodeValue makeNodeInteger(String lexicalForm)
+    { return makeNode(lexicalForm, XSDinteger); }
 
-    public static NodeValue makeNodeFloat(float f) {
-        return makeNode(XSDNumUtils.stringForm(f), null, XSDfloat.getURI());
-    }
+    public static NodeValue makeNodeFloat(float f)
+    { return makeNode(XSDNumUtils.stringForm(f), XSDfloat); }
 
-    public static NodeValue makeNodeFloat(String lexicalForm) {
-        return makeNode(lexicalForm, null, XSDfloat.getURI());
-    }
+    public static NodeValue makeNodeFloat(String lexicalForm)
+    { return makeNode(lexicalForm, XSDfloat); }
 
-    public static NodeValue makeNodeDouble(double v) {
-        return makeNode(XSDNumUtils.stringForm(v), null, XSDdouble.getURI());
-    }
+    public static NodeValue makeNodeDouble(double v)
+    { return makeNode(XSDNumUtils.stringForm(v), XSDdouble); }
 
-    public static NodeValue makeNodeDouble(String lexicalForm) {
-        return makeNode(lexicalForm, null, XSDdouble.getURI());
-    }
+    public static NodeValue makeNodeDouble(String lexicalForm)
+    { return makeNode(lexicalForm, XSDdouble); }
 
     public static NodeValue makeNodeDecimal(BigDecimal decimal) {
         String lex = XSDNumUtils.stringFormatARQ(decimal);
-        return makeNode(lex, XSDDatatype.XSDdecimal);
+        return makeNode(lex, XSDdecimal);
     }
 
-    public static NodeValue makeNodeDecimal(String lexicalForm) {
-        return makeNode(lexicalForm, null, XSDdecimal.getURI());
-    }
-
-    public static NodeValue makeNodeString(String string) {
-        return makeNode(string, null, (String)null);
-    }
+    public static NodeValue makeNodeDecimal(String lexicalForm)
+    { return makeNode(lexicalForm, XSDdecimal); }
 
-    public static NodeValue makeNodeDateTime(Calendar date) {
-        String lex = DateTimeUtils.calendarToXSDDateTimeString(date);
-        return makeNode(lex, XSDdateTime);
-    }
+    public static NodeValue makeNodeString(String string)
+    { return makeNode(string, XSDstring); }
 
-    public static NodeValue makeNodeDateTime(String lexicalForm) {
-        return makeNode(lexicalForm, XSDdateTime);
-    }
+    public static NodeValue makeNodeDateTime(String lexicalForm)
+    { return makeNode(lexicalForm, XSDdateTime); }
 
-    public static NodeValue makeNodeDate(Calendar date) {
-        String lex = DateTimeUtils.calendarToXSDDateString(date);
-        return makeNode(lex, XSDdate);
-    }
+    public static NodeValue makeNodeDate(String lexicalForm)
+    { return makeNode(lexicalForm, XSDdate); }
 
-    public static NodeValue makeNodeDate(String lexicalForm) {
-        return makeNode(lexicalForm, XSDdate);
-    }
+    public static NodeValue makeNodeDuration(Duration duration, Node node)
+    { return new NodeValueDuration(duration, node); }
 
     // ----------------------------------------------------------------
     // ---- Expr interface
@@ -437,7 +387,7 @@ public abstract class NodeValue extends ExprNode
      * if known to be different values, throw ExprEvalException otherwise
      */
     public static boolean sameValueAs(NodeValue nv1, NodeValue nv2) {
-        return NodeValueCmp.sameValueAs(nv1, nv2);
+        return NVCompare.sameValueAs(nv1, nv2);
     }
 
     /**
@@ -453,7 +403,7 @@ public abstract class NodeValue extends ExprNode
      * the two NodeValues are known to be the same, else throw 
ExprEvalException
      */
     public static boolean notSameValueAs(NodeValue nv1, NodeValue nv2) {
-        return NodeValueCmp.notSameValueAs(nv1, nv2);
+        return NVCompare.notSameValueAs(nv1, nv2);
     }
 
     // ----------------------------------------------------------------
@@ -468,7 +418,7 @@ public abstract class NodeValue extends ExprNode
      */
     public static int compare(NodeValue nv1, NodeValue nv2) {
         //return NodeValueCompare.compare(nv1, nv2);
-        int x = NodeValueCmp.compareByValue(nv1, nv2);
+        int x = NVCompare.compareByValue(nv1, nv2);
         if ( x == Expr.CMP_INDETERMINATE || x == Expr.CMP_UNEQUAL )
             throw new ExprNotComparableException(null);
         return x;
@@ -484,7 +434,7 @@ public abstract class NodeValue extends ExprNode
      */
     public static int compareAlways(NodeValue nv1, NodeValue nv2) {
         //return NodeValueCompare.compareAlways(nv1, nv2);
-        return NodeValueCmp.compareAlways(nv1, nv2);
+        return NVCompare.compareAlways(nv1, nv2);
     }
 
     // ----------------------------------------------------------------
@@ -514,6 +464,10 @@ public abstract class NodeValue extends ExprNode
 
     // ----------------------------------------------------------------
     // ---- Subclass operations
+    // "isX" means "can it be used where X is expected", according to the 
XPath/XQuery Functions and Operator rules.
+    // e.g. NodeValueFloat.isDouble is true.
+
+    public boolean isLiteral()      { return getNode() == null || 
getNode().isLiteral(); }
 
     public boolean isBoolean()      { return false; }
     public boolean isString()       { return false; }
@@ -529,23 +483,11 @@ public abstract class NodeValue extends ExprNode
     public boolean hasDateTime()    { return isDateTime() || isDate() || 
isTime() || isGYear() || isGYearMonth() || isGMonth() || isGMonthDay() || 
isGDay(); }
     public boolean isDateTime()     { return false; }
     public boolean isDate()         { return false; }
-    public boolean isLiteral()      { return getNode() == null || 
getNode().isLiteral(); }
     public boolean isTime()         { return false; }
-    public boolean isDuration()     { return false; }
 
-    public boolean isYearMonthDuration() {
-        if ( ! isDuration() ) return false;
-        Duration dur = getDuration();
-        return ( dur.isSet(YEARS) || dur.isSet(MONTHS) ) &&
-               ! dur.isSet(DAYS) && ! dur.isSet(HOURS) && ! dur.isSet(MINUTES) 
&& ! dur.isSet(SECONDS);
-    }
-
-    public boolean isDayTimeDuration() {
-        if ( ! isDuration() ) return false;
-        Duration dur = getDuration();
-        return !dur.isSet(YEARS) && ! dur.isSet(MONTHS) &&
-            ( dur.isSet(DAYS) || dur.isSet(HOURS) || dur.isSet(MINUTES) || 
dur.isSet(SECONDS) );
-    }
+    public boolean isDuration()     { return false; }
+    public boolean isYearMonthDuration() { return isDuration() && 
NVOps.isYearMonthDuration(getDuration()); }
+    public boolean isDayTimeDuration()   { return isDuration() && 
NVOps.isDayTimeDuration(getDuration()); }
 
     public boolean isGYear()        { return false; }
     public boolean isGYearMonth()   { return false; }
@@ -608,175 +550,14 @@ public abstract class NodeValue extends ExprNode
             return new NodeValueLang(node);
         }
 
-        NodeValue nv = _setByValue(node);
-        if ( nv != null )
-            return nv;
-        return new NodeValueNode(node);
-    }
-
-    // Jena code does not have these types (yet)
-    private static final String dtXSDprecisionDecimal   = 
XSD+"#precisionDecimal";
-
-    // Returns null for unrecognized literal.
-    private static NodeValue _setByValue(Node node) {
-        // This should not happen.
-        // nodeToNodeValue should have been dealt with it.
-        if ( NodeUtils.hasLang(node) ) {
-            if ( NodeUtils.hasLangDir(node) )
-                return new NodeValueLangDir(node);
-            return new NodeValueLang(node);
-        }
-        LiteralLabel lit = node.getLiteral();
-        RDFDatatype datatype = lit.getDatatype();
-
-        // Quick check.
-        // Only XSD supported.
-        // And (for testing) roman numerals.
-        String datatypeURI = datatype.getURI();
-        if ( !datatypeURI.startsWith(xsdNamespace) && 
!SystemARQ.EnableRomanNumerals ) {
-            // Not XSD.
-            return null;
-        }
-
-        String lex = lit.getLexicalForm();
-
-        try { // DatatypeFormatException - should not happen
-            if ( XSDstring.isValidLiteral(lit) )
-                // String - plain or xsd:string, or derived datatype.
-                return new NodeValueString(lex, node);
-
-            // Otherwise xsd:string is like any other unknown datatype.
-            // Ditto literals with language tags (which are handled by 
nodeToNodeValue)
-
-            // isValidLiteral is a value test - not a syntactic test.
-            // This makes a difference in that "1"^^xsd:decimal" is a
-            // valid literal for xsd:integer (all other cases are subtypes of 
xsd:integer)
-            // which we want to become integer anyway).
-
-            // Order here is promotion order integer-decimal-float-double
-
-            // XSD allows whitespace. Java String.trim removes too much
-            // so must test for validity on the untrimmed lexical form.
-            String lexTrimmed = lex.trim();
-
-            if ( ! datatype.equals(XSDdecimal) ) { // decimal covers integer, 
and all derived types, lexical forms
-                // XSD integer and derived types
-                if ( XSDinteger.isValidLiteral(lit) ) {
-                    if ( ! lit.isWellFormed() )
-                        return null;
-                    // BigInteger does not accept such whitespace.
-                    String s = lexTrimmed;
-                    if ( s.startsWith("+") )
-                        // BigInteger does not accept leading "+"
-                        s = s.substring(1);
-                    // Includes subtypes (int, byte, postiveInteger etc).
-                    // NB Known to be valid for type by now
-                    BigInteger integer = new BigInteger(s);
-                    return new NodeValueInteger(integer, node);
-                }
-            }
-
-            if ( datatype.equals(XSDdecimal) && XSDdecimal.isValidLiteral(lit) 
) {
-                BigDecimal decimal = new BigDecimal(lexTrimmed);
-                return new NodeValueDecimal(decimal, node);
-            }
-
-            if ( datatype.equals(XSDfloat) && XSDfloat.isValidLiteral(lit) ) {
-                // NB If needed, call to floatValue, then assign to double.
-                // Gets 1.3f != 1.3d right
-                float f = ((Number)lit.getValue()).floatValue();
-                return new NodeValueFloat(f, node);
-            }
-
-            if ( datatype.equals(XSDdouble) && XSDdouble.isValidLiteral(lit) ) 
{
-                double d = ((Number)lit.getValue()).doubleValue();
-                return new NodeValueDouble(d, node);
-            }
-
-            if ( datatype.equals(XSDboolean) && XSDboolean.isValidLiteral(lit) 
) {
-                boolean b = (Boolean) lit.getValue();
-                return new NodeValueBoolean(b, node);
-            }
-
-            if ( datatype.equals(XSDdateTime) || 
datatype.equals(XSDdateTimeStamp) ) {
-                if ( ! XSDdateTime.isValid(lex) )
-                    return null;
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-
-            if ( datatype.equals(XSDdate) && XSDdate.isValidLiteral(lit) ) {
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-
-            if ( datatype.equals(XSDtime) && XSDtime.isValidLiteral(lit) ) {
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-
-            if ( datatype.equals(XSDgYear) && XSDgYear.isValidLiteral(lit) ) {
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-            if ( datatype.equals(XSDgYearMonth) && 
XSDgYearMonth.isValidLiteral(lit) ) {
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-            if ( datatype.equals(XSDgMonth) && XSDgMonth.isValidLiteral(lit) ) 
{
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-
-            if ( datatype.equals(XSDgMonthDay) && 
XSDgMonthDay.isValidLiteral(lit) ) {
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-            if ( datatype.equals(XSDgDay) && XSDgDay.isValidLiteral(lit) ) {
-                return NodeValueDateTime.create(lexTrimmed, node);
-            }
-
-            // -- Duration
-
-            if ( datatype.equals(XSDduration) && XSDduration.isValid(lex) ) {
-                Duration duration = xmlDatatypeFactory.newDuration(lexTrimmed);
-                return new NodeValueDuration(duration, node);
-            }
-
-            if ( datatype.equals(XSDyearMonthDuration) && 
XSDyearMonthDuration.isValid(lex) ) {
-                Duration duration = xmlDatatypeFactory.newDuration(lexTrimmed);
-                return new NodeValueDuration(duration, node);
-            }
-            if ( datatype.equals(XSDdayTimeDuration) && 
XSDdayTimeDuration.isValid(lex) ) {
-                Duration duration = xmlDatatypeFactory.newDuration(lexTrimmed);
-                return new NodeValueDuration(duration, node);
-            }
-
-            // If wired into the TypeMapper via 
RomanNumeralDatatype.enableAsFirstClassDatatype
-//            if ( RomanNumeralDatatype.get().isValidLiteral(lit) )
-//            {
-//                int i = ((RomanNumeral)lit.getValue()).intValue();
-//                return new NodeValueInteger(i);
-//            }
-
-            // Not wired in
-            if ( SystemARQ.EnableRomanNumerals )
-            {
-                if ( 
lit.getDatatypeURI().equals(RomanNumeralDatatype.get().getURI()) )
-                {
-                    Object obj = RomanNumeralDatatype.get().parse(lexTrimmed);
-                    if ( obj instanceof Integer )
-                        return new 
NodeValueInteger(((Integer)obj).longValue());
-                    if ( obj instanceof RomanNumeral )
-                        return new NodeValueInteger( 
((RomanNumeral)obj).intValue() );
-                    throw new 
ARQInternalErrorException("DatatypeFormatException: Roman numeral is unknown 
class");
-                }
-            }
-
-        } catch (DatatypeFormatException ex)
-        {
-            // Should have been caught earlier by special test in 
nodeToNodeValue
-            throw new ARQInternalErrorException("DatatypeFormatException: 
"+lit, ex);
-        }
-        return null;
+        // Includes creating NodeValueNode for ill-formed literals.
+        NodeValue nv = NVFactory.create(node);
+        return nv;
     }
 
     // ----------------------------------------------------------------
 
-    // Point to catch all exceptions.
+    /** Common point for exceptions during evaluation. */
     public static void raise(ExprException ex) {
         throw ex;
     }
@@ -787,7 +568,6 @@ public abstract class NodeValue extends ExprNode
     private void forceToNode() {
         if ( node == null )
             node = asNode();
-
         if ( node == null )
             raise(new ExprEvalException("Not a node: " + this));
     }
@@ -813,7 +593,6 @@ public abstract class NodeValue extends ExprNode
 
     // Convert to a string - usually overridden.
     public String asString() {
-        // Do not call .toString()
         forceToNode();
         return NodeFunctions.str(node);
     }
@@ -825,13 +604,13 @@ public abstract class NodeValue extends ExprNode
 
     @Override
     public boolean equals(Expr other, boolean bySyntax) {
+        // Java equals, not "same value" or "same term"
         if ( other == null ) return false;
         if ( this == other ) return true;
         // This is the equality condition Jena uses - lang tags are different 
by case.
         if ( ! ( other instanceof NodeValue nv) )
             return false;
         return asNode().equals(nv.asNode());
-        // Not NodeFunctions.sameTerm (which smooshes language tags by case)
     }
 
     public abstract void visit(NodeValueVisitor visitor);
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java
index 06850afdd6..503153eb37 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java
@@ -30,21 +30,6 @@ public class NodeValueDateTime extends NodeValue
 {
     final private XMLGregorianCalendar datetime;
 
-    /** Lex - caller removes leading and trailing whitespace. */
-    public static NodeValueDateTime create(String lex, Node n) {
-        // Java bug : gMonth with a timezone of Z causes 
IllegalArgumentException
-        if ( XSDgMonth.equals(n.getLiteralDatatype()) ) {
-            if ( lex.endsWith("Z") ) {
-                lex = lex.substring(0, lex.length() - 1);
-                XMLGregorianCalendar datetime = 
NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex);
-                datetime.setTimezone(0);
-                return new NodeValueDateTime(datetime, n);
-            }
-        }
-        XMLGregorianCalendar datetime = 
NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex);
-        return new NodeValueDateTime(datetime, n);
-    }
-
     public NodeValueDateTime(XMLGregorianCalendar datetime, Node n) {
         super(n);
         this.datetime = datetime;
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java
index c89caac691..2d70825d94 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java
@@ -27,7 +27,8 @@ import 
org.apache.jena.sparql.expr.nodevalue.TestNodeValueSortKey;
 
 @Suite
 @SelectClasses({
-    TestNodeValue.class
+    TestNVFactory.class
+    , TestNodeValue.class
     , TestExpressions.class
     , TestExpressions2.class
     , TestExpressions3.class
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java
index 01dda93ac2..c9a53aaa71 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java
@@ -77,7 +77,7 @@ public class TestComparison {
 
     private static void compare(NodeValue nv1, NodeValue nv2, int expected) {
         try {
-            int cmp = NodeValueCmp.compareByValue(nv1, nv2);
+            int cmp = NVCompare.compareByValue(nv1, nv2);
             assertEquals(expected, cmp);
         } catch (ExprNotComparableException ex) {
             throw ex;
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNVFactory.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNVFactory.java
new file mode 100644
index 0000000000..35c330e357
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNVFactory.java
@@ -0,0 +1,457 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.expr;
+
+import static java.lang.String.format;
+import static org.apache.jena.datatypes.xsd.XSDDatatype.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import org.apache.jena.datatypes.RDFDatatype;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.sparql.expr.nodevalue.*;
+import org.apache.jena.vocabulary.RDF;
+
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestNVFactory {
+
+    // basic testing
+    private <T extends NodeValue> NodeValue test(String lex, RDFDatatype dt, 
Class<T> expectedClass) {
+        Node n = NodeFactory.createLiteralDT(lex, dt);
+        NodeValue nv = NVFactory.create(n);
+        assertTrue(expectedClass.isInstance(nv),
+                  format("Expected instance of %s but got %s", 
expectedClass.getSimpleName(), nv.getClass().getSimpleName()));
+        assertEquals(lex, nv.asNode().getLiteralLexicalForm());
+        return nv;
+    }
+
+    @Test
+    public void testDecimal_1() {
+        NodeValue nv = test("12.34", XSDdecimal, NodeValueDecimal.class);
+        assertTrue(nv.isDecimal());
+        assertNotNull(nv.getDecimal());
+        assertTrue(nv.isFloat());
+        assertTrue(nv.isDouble());
+        assertFalse(nv.isInteger());
+        assertTrue(nv.isDecimal());
+    }
+
+    @Test
+    public void testDecimal_2() {
+        NodeValue nv = test("12", XSDdecimal, NodeValueDecimal.class);
+        assertTrue(nv.isDecimal());
+        assertNotNull(nv.getDecimal());
+    }
+
+    @Test
+    public void testDecimal_3() {
+        NodeValue nv = test("-12", XSDdecimal, NodeValueDecimal.class);
+        assertTrue(nv.isDecimal());
+        assertNotNull(nv.getDecimal());
+    }
+
+    @Test
+    public void testFloat() {
+        NodeValue nv = test("12.34", XSDfloat, NodeValueFloat.class);
+        assertTrue(nv.isFloat());
+        assertTrue(nv.isDouble());
+        assertFalse(nv.isInteger());
+        assertFalse(nv.isDecimal());
+    }
+
+    @Test
+    public void testDouble() {
+        NodeValue nv = test("12.34", XSDdouble, NodeValueDouble.class);
+        assertFalse(nv.isFloat());
+        assertTrue(nv.isDouble());
+        assertFalse(nv.isInteger());
+        assertFalse(nv.isDecimal());
+    }
+
+    @Test
+    public void testInteger() {
+        NodeValue nv = test("123", XSDinteger, NodeValueInteger.class);
+        assertTrue(nv.isFloat());
+        assertTrue(nv.isDouble());
+        assertTrue(nv.isInteger());
+        assertTrue(nv.isDecimal());
+    }
+
+    @Test
+    public void testBoolean() {
+        NodeValue nv1 = test("true", XSDboolean, NodeValueBoolean.class);
+        NodeValue nv2 = test("false", XSDboolean, NodeValueBoolean.class);
+    }
+
+    @Test
+    public void testString() {
+        NodeValue nv = test("test string", XSDstring, NodeValueString.class);
+    }
+
+    // Keep original language string tests as they have special handling
+    @Test
+    public void testLangString() {
+        String lex = "hello";
+        String lang = "en";
+        Node n = NodeFactory.createLiteralLang(lex, lang);
+        NodeValue nv = NVFactory.create(n);
+        assertTrue(NodeValueLang.class.isInstance(nv));
+        assertEquals(lex, nv.getString());
+        assertEquals(lang, nv.getLang());
+    }
+
+    @Test
+    public void testDirLangString() {
+        String lex = "hello";
+        String lang = "en-GB";
+        String ltr = "ltr";
+        Node n = NodeFactory.createLiteralDirLang(lex, lang, ltr);
+        NodeValue nv = NVFactory.create(RDF.dtDirLangString, n);
+        assertTrue(NodeValueLangDir.class.isInstance(nv));
+        assertEquals(lex, nv.getString());
+        assertEquals(lang, nv.getLang());
+    }
+
+    @Test
+    public void testInvalidIntegerLiteral() {
+        NodeValue nv = test("not-an-integer", XSDinteger, NodeValueNode.class);
+        assertNotNull(nv);
+        assertFalse(nv.isNumber());
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedInt() {
+        NodeValue nv = test("123", XSDint, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedLong() {
+        NodeValue nv = test("123", XSDlong, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedShort() {
+        NodeValue nv = test("123", XSDshort, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedByte() {
+        NodeValue nv = test("123", XSDbyte, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedUnsignedByte() {
+        NodeValue nv = test("123", XSDunsignedByte, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedUnsignedShort() {
+        NodeValue nv = test("123", XSDunsignedShort, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedUnsignedInt() {
+        NodeValue nv = test("123", XSDunsignedInt, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedUnsignedLong() {
+        NodeValue nv = test("123", XSDunsignedLong, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedNonPositiveInteger() {
+        NodeValue nv = test("-123", XSDnonPositiveInteger, 
NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedNonNegativeInteger() {
+        NodeValue nv = test("123", XSDnonNegativeInteger, 
NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedPositiveInteger() {
+        NodeValue nv = test("123", XSDpositiveInteger, NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    @Test
+    public void testDerivedNegativeInteger() {
+        NodeValue nv = test("-123", XSDnegativeInteger, 
NodeValueInteger.class);
+        assertTrue(nv.isInteger());
+    }
+
+    // Date and time types
+
+
+    @Test
+    public void testDateTimeType() {
+        NodeValue nv = test("2025-11-03T12:00:00", XSDdateTime, 
NodeValueDateTime.class);
+        assertTrue(nv.isDateTime());
+        assertFalse(nv.isDate());
+        assertFalse(nv.isTime());
+    }
+
+    @Test
+    public void testDate() {
+        NodeValue nv = test("2025-11-03", XSDdate, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertTrue(nv.isDate());
+        assertFalse(nv.isTime());
+    }
+
+    @Test
+    public void testTime() {
+        NodeValue nv = test("12:00:00", XSDtime, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertFalse(nv.isDate());
+        assertTrue(nv.isTime());
+    }
+
+    @Test
+    public void testDateTimeStamp() {
+        NodeValue nv = test("2025-11-03T12:00:00Z", XSDdateTimeStamp, 
NodeValueDateTime.class);
+        assertTrue(nv.isDateTime());
+    }
+
+    @Test
+    public void testGDay() {
+        NodeValue nv = test("---03", XSDgDay, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertTrue(nv.isGDay());
+        assertFalse(nv.isGMonthDay());
+    }
+
+    @Test
+    public void testGMonth() {
+        NodeValue nv = test("--11", XSDgMonth, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertTrue(nv.isGMonth());
+        assertFalse(nv.isGMonthDay());
+    }
+
+    @Test
+    public void testGMonthDay() {
+        NodeValue nv = test("--11-03", XSDgMonthDay, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertFalse(nv.isGMonth());
+        assertTrue(nv.isGMonthDay());
+    }
+
+    @Test
+    public void testGYear() {
+        NodeValue nv = test("2025", XSDgYear, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertTrue(nv.isGYear());
+        assertFalse(nv.isGYearMonth());
+    }
+
+    @Test
+    public void testGYearMonth() {
+        NodeValue nv = test("2025-11", XSDgYearMonth, NodeValueDateTime.class);
+        assertFalse(nv.isDateTime());
+        assertFalse(nv.isGYear());
+        assertTrue(nv.isGYearMonth());
+    }
+
+    // Keep original duration types test as it has special assertions
+    @Test
+    public void testDuration() {
+        NodeValue nv = test("P1Y2M3DT4H5M6S", XSDduration, 
NodeValueDuration.class);
+        assertTrue(nv.isDuration());
+        assertFalse(nv.isDayTimeDuration());
+        assertFalse(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testDuration_2() {
+        NodeValue nv = test("P3DT4H5M6S", XSDduration, 
NodeValueDuration.class);
+        assertTrue(nv.isDuration());
+        assertTrue(nv.isDayTimeDuration());
+        assertFalse(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testDuration_3() {
+        NodeValue nv = test("P1Y2M", XSDduration, NodeValueDuration.class);
+        assertTrue(nv.isDuration());
+        assertFalse(nv.isDayTimeDuration());
+        assertTrue(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testDayTimeDuration() {
+        NodeValue nv = test("P3DT4H5M6S", XSDdayTimeDuration, 
NodeValueDuration.class);
+        assertTrue(nv.isDuration());
+        assertTrue(nv.isDayTimeDuration());
+        assertFalse(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testYearMonthDuration() {
+        NodeValue nv = test("P1Y2M", XSDyearMonthDuration, 
NodeValueDuration.class);
+        assertTrue(nv.isDuration());
+        assertFalse(nv.isDayTimeDuration());
+        assertTrue(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testInvalidDayTimeDuration() {
+        NodeValue nv = test("P1Y", XSDdayTimeDuration, NodeValueNode.class);
+        assertFalse(nv.isDuration());
+        assertFalse(nv.isDayTimeDuration());
+        assertFalse(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testInvalidYearMonthDuration() {
+        NodeValue nv = test("PT3H", XSDyearMonthDuration, NodeValueNode.class);
+        assertFalse(nv.isDuration());
+        assertFalse(nv.isDayTimeDuration());
+        assertFalse(nv.isYearMonthDuration());
+    }
+
+    @Test
+    public void testInvalidDateFormat_1() {
+        test("not-a-date", XSDdate, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidDateFormat_2() {
+        // Invalid for xsd:date
+        test("2025-11-03T15:04:30", XSDdate, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidTimeFormat() {
+        // Invalid hours
+        test("25:00:00", XSDtime, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidDateTimeFormat_1() {
+        // Invalid valid for month, day, and hour
+        NodeValue nv = test("2025-13-45T25:00:00", XSDdateTime, 
NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidDateTimeFormat_2() {
+        // Invalid for xsd:dateTime
+        NodeValue nv = test("2025-12-25", XSDdateTime, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidDecimalFormat() {
+        // Multiple decimal points
+        NodeValue nv = test("12.34.56", XSDdecimal, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidFloatFormat() {
+        // Incomplete exponent
+        NodeValue nv = test("12.34e", XSDfloat, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidIntegerFormat() {
+        // Decimal not allowed for integer
+        NodeValue nv = test("123.45", XSDinteger, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidDurationFormat() {
+        // Invalid duration designator
+        test("P1X", XSDduration, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidGMonthFormat() {
+        // Invalid month value
+        test("--13", XSDgMonth, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidGDayFormat() {
+        // Invalid day value
+        test("---32", XSDgDay, NodeValueNode.class);
+    }
+
+    @Test
+    public void testInvalidPositiveIntegerNegativeValue() {
+        // Negative value not allowed for positive integer
+        NodeValue nv = test("-123", XSDpositiveInteger, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidUnsignedIntValue() {
+        // Negative value not allowed for unsigned
+        NodeValue nv = test("-1", XSDunsignedInt, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidByteRange() {
+        // Outside byte range (-128 to 127)
+        NodeValue nv = test("128", XSDbyte, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidUnsignedByteRange() {
+        // Outside unsigned byte range (0 to 255)
+        NodeValue nv = test("256", XSDunsignedByte, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidNonNegativeInteger() {
+        // Negative value not allowed
+        NodeValue nv = test("-1", XSDnonNegativeInteger, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+
+    @Test
+    public void testInvalidNonPositiveInteger() {
+        // Positive value not allowed
+        NodeValue nv = test("1", XSDnonPositiveInteger, NodeValueNode.class);
+        assertFalse(nv.isInteger());
+    }
+}
\ No newline at end of file
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java
index 802f6784d1..ce093cc27c 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java
@@ -23,6 +23,8 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.math.BigDecimal;
 import java.util.*;
 
+import javax.xml.datatype.XMLGregorianCalendar;
+
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -163,19 +165,32 @@ public class TestNodeValue
     }
 
     @Test
-    public void testDateTime1() {
+    public void testDateTime1x() {
+        // Legacy
+        // Better to use XMLGregorianCalendar
         Calendar cal = new GregorianCalendar();
         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
         // Clear/ set all fields (milliseconds included).
         cal.setTimeInMillis(0);
         cal.set(2005, 01, 18, 20, 39, 10); // NB Months from 0, not 1
 
+        @SuppressWarnings("removal")
         NodeValue v = NodeValue.makeDateTime(cal);
         assertTrue(v.isDateTime(), ()->"Not a dateTime: " + v);
         assertFalse(v.isDate(), ()->"A date: " + v);
         // DateTimes always have nodes because we used that to parse the thing.
     }
 
+    @Test
+    public void testDateTime1() {
+        // Better to use XMLGregorianCalendar
+        XMLGregorianCalendar cal = 
NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar("2025-11-05T12:08:30.5Z");
+        NodeValue v = NodeValue.makeDateTime(cal);
+        assertTrue(v.isDateTime(), ()->"Not a dateTime: " + v);
+        assertFalse(v.isDate(), ()->"A date: " + v);
+    }
+
+
     @Test
     public void testDateTime2() {
         NodeValue v = NodeValue.makeNodeDateTime("2005-02-18T20:39:10Z");
@@ -262,19 +277,26 @@ public class TestNodeValue
     }
 
     @Test
-    public void testDate1() {
+    public void testDate1x() {
+        // Legacy
         Calendar cal = new GregorianCalendar();
         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
         // Clear/ set all fields (milliseconds included).
         cal.setTimeInMillis(0);
-        // NB Months from 0, not 1
-        // For a date, must be time = 00:00:00
         cal.set(2005, 01, 18, 0, 0, 0);
+        @SuppressWarnings("removal")
+        NodeValue v = NodeValue.makeDate(cal);
+        assertTrue(v.isDate(), ()->"Not a date: " + v);
+        assertFalse(v.isDateTime(), ()->"A dateTime: " + v);
+    }
 
+    @Test
+    public void testDate1() {
+        // Better to use XMLGregorianCalendar
+        XMLGregorianCalendar cal = 
NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar("2025-11-05");
         NodeValue v = NodeValue.makeDate(cal);
         assertTrue(v.isDate(), ()->"Not a date: " + v);
         assertFalse(v.isDateTime(), ()->"A dateTime: " + v);
-        // DateTimes always have nodes because we used that to parse the thing.
     }
 
     @Test
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java
index 09c817fdd7..dc9c449923 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java
@@ -181,9 +181,9 @@ public class TestSortOrdering {
         test(nv1, nv2, nv3);
 
         // And this should be <= order
-        int x12 = NodeValueCmp.compareWithOrdering(nv1, nv2);
-        int x23 = NodeValueCmp.compareWithOrdering(nv2, nv3);
-        int x13 = NodeValueCmp.compareWithOrdering(nv1, nv3);
+        int x12 = NVCompare.compareWithOrdering(nv1, nv2);
+        int x23 = NVCompare.compareWithOrdering(nv2, nv3);
+        int x13 = NVCompare.compareWithOrdering(nv1, nv3);
 
         boolean isTransitiveLE = isLE(x12) && isLE(x23) && isLE(x13);
 
@@ -234,9 +234,9 @@ public class TestSortOrdering {
      * Test, return true if a transitive order of arguments holds (either 
"less than or equals" or "greater than or equal").
      */
     static boolean testWorker(NodeValue nv1, NodeValue nv2, NodeValue nv3) {
-        int x12 = NodeValueCmp.compareWithOrdering(nv1, nv2);
-        int x23 = NodeValueCmp.compareWithOrdering(nv2, nv3);
-        int x13 = NodeValueCmp.compareWithOrdering(nv1, nv3);
+        int x12 = NVCompare.compareWithOrdering(nv1, nv2);
+        int x23 = NVCompare.compareWithOrdering(nv2, nv3);
+        int x13 = NVCompare.compareWithOrdering(nv1, nv3);
 
         if ( isLE(x12) ) {
             if ( isLE(x23) ) {
@@ -256,11 +256,11 @@ public class TestSortOrdering {
     }
 
     private static boolean isLE(NodeValue nv1, NodeValue nv2) {
-        return isLE(NodeValueCmp.compareWithOrdering(nv1, nv2));
+        return isLE(NVCompare.compareWithOrdering(nv1, nv2));
     }
 
     private static boolean isGE(NodeValue nv1, NodeValue nv2) {
-        return isGE(NodeValueCmp.compareWithOrdering(nv1, nv2));
+        return isGE(NVCompare.compareWithOrdering(nv1, nv2));
     }
 
     private static boolean isLE(int x) {
diff --git 
a/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java
 
b/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java
index e157e33635..da807b3030 100644
--- 
a/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java
+++ 
b/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java
@@ -81,7 +81,7 @@ class NodeValueRewriter extends AbstractRewriter<NodeValue> 
implements NodeValue
 
     @Override
     public void visit(NodeValueDateTime nv) {
-        push(NodeValueDateTime.create(nv.getDateTime().toXMLFormat(), 
changeNode(nv.asNode())));
+        push(new NodeValueDateTime(nv.getDateTime(), changeNode(nv.asNode())));
     }
 
     @Override
diff --git 
a/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java
 
b/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java
index f1f4ad16aa..3a3dfd32e9 100644
--- 
a/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java
+++ 
b/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java
@@ -188,7 +188,8 @@ public class NodeValueRewriterTest {
         // 2001-10-26T21:32:52
         GregorianCalendar cal = new GregorianCalendar(2001, 10, 26, 21, 32, 
52);
         Node n = NodeFactory.createLiteralByValue(cal, 
XSDDatatype.XSDdateTime);
-        NodeValue nv = NodeValueDateTime.create("2001-10-26T21:32:52", n);
+        @SuppressWarnings("removal")
+        NodeValue nv = NodeValue.makeDateTime(cal);
         nv.visit(rewriter);
         NodeValue result = rewriter.getResult();
         assertEquals(nv, result);

Reply via email to