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);
