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 8756c9389349864abf95a86dd3300da8d7678931
Author: Andy Seaborne <[email protected]>
AuthorDate: Mon Sep 8 14:59:17 2025 +0100

    GH-3404: Tidy up NodeId and DecimalNode56
---
 .../org/apache/jena/sparql/util/XSDNumUtils.java   | 42 +++++++++-----
 .../java/org/apache/jena/sparql/util/TS_Util.java  |  3 +-
 .../apache/jena/sparql/util/TestXSDNumUtils.java   | 65 ++++++++++++++++++++++
 .../java/org/apache/jena/tdb2/store/NodeId.java    | 24 ++++++--
 .../org/apache/jena/tdb2/store/NodeIdFactory.java  |  2 +-
 .../org/apache/jena/tdb2/store/NodeIdInline.java   |  5 +-
 .../apache/jena/tdb2/store/value/DecimalNode.java  |  2 +-
 .../jena/tdb2/store/value/DecimalNode56.java       | 48 +++++++---------
 .../jena/tdb2/store/value/TestNodeIdInline.java    | 41 +++++++++++++-
 9 files changed, 179 insertions(+), 53 deletions(-)

diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/util/XSDNumUtils.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/util/XSDNumUtils.java
index 389fa11135..aa884ce0ca 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/util/XSDNumUtils.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/util/XSDNumUtils.java
@@ -70,8 +70,9 @@ public class XSDNumUtils {
     }
 
     /**
-     * Produce a lexical form for {@link BigDecimal} that is compatible with
-     * Turtle syntax (i.e it has a decimal point).
+     * Produce a lexical form for {@link BigDecimal} that is compatible with 
Turtle
+     * syntax (i.e it has a decimal point). This is also the function used by 
TDB2 to
+     * turn decimal NodeId values into the lexical form of an xsd:decimal.
      */
     public static String stringForm(BigDecimal decimal) {
         return XSDNumUtils.canonicalDecimalStrWithDot(decimal);
@@ -164,18 +165,6 @@ public class XSDNumUtils {
         return bd.stripTrailingZeros().toPlainString();
     }
 
-    /** Return a canonical decimal with a trailing ".0". */
-    public static BigDecimal canonicalDecimalWithDot(BigDecimal decimal) {
-        BigDecimal result = decimal;
-        if (result.scale() > 1) {
-            result = decimal.stripTrailingZeros();
-        }
-        if (result.scale() <= 0) {
-            result = result.setScale(1);
-        }
-        return result;
-    }
-
     /**
      * Integer-valued decimals have a trailing ".0".
      * (In XML Schema Datatype 1.1 they did not have a ".0".)
@@ -195,4 +184,29 @@ public class XSDNumUtils {
             str = str + ".0";
         return str;
     }
+
+    /**
+     * Return a canonical decimal with a trailing ".0".
+     * This is canonicalizing the value/scale.
+     * <p>
+     * This is the {@link BigDecimal} form used to encode into NodeIds in TDB2.
+     * <p>
+     * It has a trailing ".0" for integer values so it is Turtle compatible, 
but
+     * otherwise has no trailing zeros.
+     * <p>
+     * For TDB2, we require a consistent, fixed value/scale form for any value 
to be
+     * encoded in a TDB2 NodeId and when reconstructed to get the same lexical 
form.
+     *
+     * @see BigDecimal
+     */
+    public static BigDecimal canonicalDecimal(BigDecimal decimal) {
+        BigDecimal result = decimal;
+        if (result.scale() > 1) {
+            result = decimal.stripTrailingZeros();
+        }
+        if (result.scale() <= 0) {
+            result = result.setScale(1);
+        }
+        return result;
+    }
 }
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/util/TS_Util.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/util/TS_Util.java
index 6ffd38dd57..1cb984e6d2 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/util/TS_Util.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/util/TS_Util.java
@@ -28,7 +28,8 @@ import org.junit.platform.suite.api.Suite;
     TestFmtUtils.class,
     TestContextUtils.class,
     TestGraphUtils.class,
-    TestQueryCheckRW.class
+    TestQueryCheckRW.class,
+    TestXSDNumUtils.class
 })
 public class TS_Util
 { }
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/util/TestXSDNumUtils.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/util/TestXSDNumUtils.java
new file mode 100644
index 0000000000..cf84ce6b33
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/util/TestXSDNumUtils.java
@@ -0,0 +1,65 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.junit.jupiter.api.Test;
+
+public class TestXSDNumUtils {
+
+    @Test public void canonicalDecimal_01() {
+        test(0, 0, "0.0", 0, 1);
+    }
+
+    @Test public void canonicalDecimal_02() {
+        test(0, 1, "0.0", 0, 1);
+    }
+
+    // Positive scale = digits after decimal point of the value.
+    // Negative scale = digits extra before decimal point
+
+    @Test public void canonicalDecimal_03() {
+        test(101, 0, "101.0", 1010, 1);
+    }
+
+    @Test public void canonicalDecimal_04() {
+        test(101, 1, "10.1", 101, 1);
+    }
+
+    @Test public void canonicalDecimal_05() {
+        test(101, 2, "1.01", 101, 2);
+    }
+
+    @Test public void canonicalDecimal_06() {
+        test(-101, -2, "-10100.0", -101000, 1);
+    }
+
+    private static void test(long inputValue, int inputScale, String expected, 
long expectedValue, int expectedScale) {
+        BigDecimal bd1 = new BigDecimal(BigInteger.valueOf(inputValue), 
inputScale);
+        BigDecimal bdActual = XSDNumUtils.canonicalDecimal(bd1);
+        String actual = XSDNumUtils.stringForm(bdActual);
+        assertEquals(expected, actual, "String");
+        assertEquals(expectedValue, bdActual.unscaledValue().longValue(), 
"value");
+        assertEquals(expectedScale, bdActual.scale(), "scale");
+    }
+}
diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeId.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeId.java
index 508fe0e7c6..d6bef2b7d9 100644
--- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeId.java
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeId.java
@@ -104,15 +104,24 @@ public class NodeId implements Comparable<NodeId>
 //    public long getPtrLo() { return value2; }
 //    public int  getPtrHi() { return value1 & 0x00FFFFFF; }
 
-    // 64 bit
+    /** The pointer part of a NodeId reference. */
     public long getPtrLocation()    { return value2; }
-    // Long.
+
+    // 96 bit
 //    public long getPtrLo()          { return value2; }
 //    public int  getPtrHi()          { return value1; }
 
-    public int getTypeValue()       { return type.type(); }
+    // 64 bit.
+    public int getTypeValue() {
+        return type.type();
+    }
+
+    /** The value (encoding) part of an inline literal (56 bits) */
+    public long getValue56() {
+        return value2;
+    }
 
-    public boolean isInline() {
+    public boolean isInline()       {
         return isInline(this);
     }
 
@@ -141,8 +150,11 @@ public class NodeId implements Comparable<NodeId>
 //    public static boolean isDefined(NodeId nodeId)             { return 
nodeId == NodeIdDefined; }
 //    public static boolean isUndefined(NodeId nodeId)           { return 
nodeId == NodeIdUndefined; }
 
-    /** Create from a long-encoded value */
-    /*package*/ static NodeId createRaw(NodeIdType type, long value) {
+    /**
+     * Create from a long-encoded value.
+     * Caution: an illegal value for the long argument will cause serious 
problems.
+     */
+    public static NodeId createRaw(NodeIdType type, long value) {
         return new NodeId(type, 0, value);
     }
 
diff --git 
a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdFactory.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdFactory.java
index cfd8acaedb..04c165d28a 100644
--- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdFactory.java
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdFactory.java
@@ -68,7 +68,7 @@ public class NodeIdFactory
         return createNew(PTR, 0, lo);
     }
 
-    /*package*/ /*long*/ static NodeId createPtrLong(int hi, long lo) {
+    /*package*/ static NodeId createPtrLong(int hi, long lo) {
         return create(PTR, hi, lo);
     }
 
diff --git 
a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdInline.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdInline.java
index f28da5354c..c3a7452977 100644
--- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdInline.java
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/NodeIdInline.java
@@ -162,8 +162,11 @@ public class NodeIdInline {
             // .trim is how Jena does it but it rather savage. spc, \n \r \t.
             // But at this point we know it's a valid literal so the excessive
             // chopping by .trim is safe.
+
+            // Jena 5 and before
             BigDecimal decimal = new BigDecimal(lit.getLexicalForm().trim());
-            decimal = XSDNumUtils.canonicalDecimalWithDot(decimal);
+            // Jena6: Adjust to canonicalize the value/scale.
+            decimal = XSDNumUtils.canonicalDecimal(decimal);
 
             // Does range checking.
             DecimalNode56 dn = DecimalNode56.valueOf(decimal);
diff --git 
a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode.java
index acb99de2c5..b821142678 100644
--- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode.java
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode.java
@@ -18,6 +18,6 @@
 
 package org.apache.jena.tdb2.store.value;
 
-/** Placeholder for a full length decimal , using int+long.*/
+/** Placeholder for a full length decimal, using int+long.*/
 public class DecimalNode {
 }
diff --git 
a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode56.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode56.java
index 55c5448e38..61f477f9c6 100644
--- 
a/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode56.java
+++ 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/store/value/DecimalNode56.java
@@ -23,49 +23,44 @@ import java.math.BigInteger;
 
 import org.apache.jena.atlas.lib.BitsLong;
 
-
 // Decimal packed into 56 bits.
 public class DecimalNode56
 {
     //private static Logger log = LoggerFactory.getLogger(DecimalNode.class);
 
-    BigDecimal decimal = null;
-
-    // signed 8 bits of scale, signed 48 bits of value.
-    // Decimal precision is 47 bits (it's signed) or around 14 places.
-    // Not finance industry accuracy nor XSD (18 places minimum) but still 
useful.
+    // 56 bits signed 8 bits of scale, signed 48 bits of value.
+    // Decimal precision is 47 bits (it's signed) or around 13 places 
(canonical form).
+    // This is not finance industry accuracy nor XSD (18 places minimum) but 
still useful.
 
-    static final int        SCALE_LEN = 8;
-    static final int        VALUE_LEN = 48;
-    static final int        ENC_LEN   = 48 + SCALE_LEN;
+    private static final int SCALE_LEN      = 8;
+    private static final int VALUE_LEN      = 48;
+    private static final int ENC_LEN        = 48 + SCALE_LEN;
 
-    static final long       MAX_VALUE = (1L << (VALUE_LEN - 1)) - 1;
-    static final long       MIN_VALUE = -(1L << (VALUE_LEN - 1));
+    private static final long MAX_VALUE     = (1L << (VALUE_LEN - 1)) - 1;
+    private static final long MIN_VALUE     = -(1L << (VALUE_LEN - 1));
 
-    static final int        MAX_SCALE = (1 << (SCALE_LEN - 1)) - 1;
-    static final int        MIN_SCALE = -(1 << (SCALE_LEN - 1));
+    private static final int MAX_SCALE      = (1 << (SCALE_LEN - 1)) - 1;
+    private static final int MIN_SCALE      = -(1 << (SCALE_LEN - 1));
 
-    static final BigInteger MAX_I     = BigInteger.valueOf(MAX_VALUE);
-    static final BigInteger MIN_I     = BigInteger.valueOf(MIN_VALUE);
+    private static final BigInteger MAX_I   = BigInteger.valueOf(MAX_VALUE);
+    private static final BigInteger MIN_I   = BigInteger.valueOf(MIN_VALUE);
 
-    // Bits counts
-    static private int      SCALE_LO  = 56 - SCALE_LEN;
-    static private int      SCALE_HI  = 56;                               // 
Exclusive
-                                                                          // 
index
+    // Bits positions [LO, HI)
+    private static final int SCALE_LO      = ENC_LEN - SCALE_LEN;
+    private static final int SCALE_HI      = ENC_LEN;
 
-    static private int      VALUE_LO  = 0;
-    static private int      VALUE_HI  = VALUE_LO + VALUE_LEN;
+    private static final int VALUE_LO       = 0;
+    private static final int VALUE_HI       = VALUE_LO + VALUE_LEN;
 
-    private int             scale;
-    private long            value;
+    // Object fields.
+    private BigDecimal decimal = null;
+    private int   scale;
+    private long  value;
 
     public static DecimalNode56 valueOf(BigDecimal decimal) {
         int scale = decimal.scale();
         BigInteger bigInt = decimal.unscaledValue();
 
-        //decimal.longValueExact(); // Throws exception
-        //new BigDecimal(long);
-
         if ( bigInt.compareTo(MAX_I) > 0 || bigInt.compareTo(MIN_I) < 0 )
             // This check makes sure that bigInt.longValue() is safe
             return null;
@@ -82,7 +77,6 @@ public class DecimalNode56
             // log.warn("Value out of range: ("+binValue+","+scale+")");
             return null;
         }
-
         return new DecimalNode56(binValue, scale);
     }
 
diff --git 
a/jena-tdb2/src/test/java/org/apache/jena/tdb2/store/value/TestNodeIdInline.java
 
b/jena-tdb2/src/test/java/org/apache/jena/tdb2/store/value/TestNodeIdInline.java
index 61792b0cc9..70d168b093 100644
--- 
a/jena-tdb2/src/test/java/org/apache/jena/tdb2/store/value/TestNodeIdInline.java
+++ 
b/jena-tdb2/src/test/java/org/apache/jena/tdb2/store/value/TestNodeIdInline.java
@@ -153,6 +153,30 @@ public class TestNodeIdInline
     @Test public void nodeId_decimal_19()
     { testNodeIdRoundtripDecimal("18.000"); }
 
+    @Test public void nodeId_decimal_30()
+    { testNodeIdLexicalDecimal("0.0", "0.0"); }
+
+    @Test public void nodeId_decimal_31()
+    { testNodeIdLexicalDecimal("+00.000", "0.0"); }
+
+    @Test public void nodeId_decimal_32()
+    { testNodeIdLexicalDecimal("-00.000", "0.0"); }
+
+    @Test public void nodeId_decimal_40()
+    { testNodeIdLexicalDecimal("1", "1.0"); }
+
+    @Test public void nodeId_decimal_41()
+    { testNodeIdLexicalDecimal("-1", "-1.0"); }
+
+    @Test public void nodeId_decimal_42()
+    { testNodeIdLexicalDecimal("+1", "1.0"); }
+
+    @Test public void nodeId_decimal_43()
+    { testNodeIdLexicalDecimal("100.000", "100.0"); }
+
+    @Test public void nodeId_decimal_44()
+    { testNodeIdLexicalDecimal("00.000000100", "0.0000001"); }
+
     @Test public void nodeId_dateTime_01()
     { test("'2008-04-28T15:36:15+01:00'^^xsd:dateTime"); }
 
@@ -340,18 +364,31 @@ public class TestNodeIdInline
         assertEquals(correct, n2, ()->"Not same term");
     }
 
+    // Check: lexical form to recovered lexical form.
+    private static void testNodeIdLexicalDecimal(String decimalStr, String 
expectedLexicalForm) {
+        Node node = NodeFactory.createLiteralDT(decimalStr, 
XSDDatatype.XSDdecimal);
+        NodeId nodeId = NodeId.inline(node);
+        Node recoveredNode = NodeId.extract(nodeId);
+
+        // Check lexical form
+        String lex = recoveredNode.getLiteralLexicalForm();
+        assertEquals(expectedLexicalForm, lex, "Recovered lexical form");
+    }
+
+    // Check: lexical form to recovered node
     private static void testNodeIdRoundtripDecimal(String decimalStr) {
         Node node = NodeFactory.createLiteralDT(decimalStr, 
XSDDatatype.XSDdecimal);
         testNodeIdRoundtrip(node);
     }
 
-    /** For a Node n assert: nodeId(n) == nodeId(extract(nodeId(n)) */
+    /** For a Node n -- assert :: nodeId(n) == nodeId(extract(nodeId(n)) */
+    // Check: node to recovered node
     private static void testNodeIdRoundtrip(Node node) {
         NodeId nodeId = NodeId.inline(node);
         testNodeIdRoundtrip(nodeId);
     }
 
-    /** For a NodeId n assert: n == nodeId(extract(n)) */
+    /** For a NodeId n -- assert :: n == nodeId(extract(n)) */
     private static void testNodeIdRoundtrip(NodeId expected) {
         Node extractedNode = NodeId.extract(expected);
         NodeId actual = NodeId.inline(extractedNode);

Reply via email to