From 8eb33b2ac447d27e885f80703fc520beeaadcc53 Mon Sep 17 00:00:00 2001 From: Gunther Rademacher Date: Tue, 26 May 2026 15:06:35 +0200 Subject: [PATCH] add support for option `number-format` of `fn:parse-json` --- .../basex/build/json/JsonParserOptions.java | 17 +++++++- .../basex/io/parse/json/JsonConverter.java | 3 ++ .../basex/io/parse/json/JsonW3Converter.java | 43 ++++++++++++++++++- .../org/basex/query/func/fn/ParseJson.java | 8 ++++ .../org/basex/query/func/FnModuleTest.java | 26 +++++++++++ .../org/basex/query/func/JsonModuleTest.java | 2 +- 6 files changed, 95 insertions(+), 4 deletions(-) diff --git a/basex-core/src/main/java/org/basex/build/json/JsonParserOptions.java b/basex-core/src/main/java/org/basex/build/json/JsonParserOptions.java index d6ebc837c9..caf86d5298 100644 --- a/basex-core/src/main/java/org/basex/build/json/JsonParserOptions.java +++ b/basex-core/src/main/java/org/basex/build/json/JsonParserOptions.java @@ -19,8 +19,11 @@ public final class JsonParserOptions extends JsonOptions { public static final BooleanOption LIBERAL = new BooleanOption("liberal", false); /** Option: fallback function (parse-json, json-to-xml). */ public static final ValueOption FALLBACK = new ValueOption("fallback", FUNCTION_ZO); - /** Option: number-parser function (parse-json, json-to-xml). */ + /** Option: number-parser function (json-to-xml). */ public static final ValueOption NUMBER_PARSER = new ValueOption("number-parser", FUNCTION_ZO); + /** Option: number format (parse-json). */ + public static final EnumOption NUMBER_FORMAT = + new EnumOption<>("number-format", JsonNumberFormat.DOUBLE); /** Option: handle duplicates (parse-json, json-to-xml). */ public static final EnumOption DUPLICATES = new EnumOption<>("duplicates", JsonDuplicates.class); @@ -31,6 +34,18 @@ public final class JsonParserOptions extends JsonOptions { /** Option: encoding (custom). */ public static final StringOption ENCODING = CommonOptions.ENCODING; + /** Number format. */ + public enum JsonNumberFormat { + /** Double. */ DOUBLE, + /** Decimal. */ DECIMAL, + /** Adaptive. */ ADAPTIVE; + + @Override + public String toString() { + return Enums.string(this); + } + } + /** Duplicate handling. */ public enum JsonDuplicates { /** Reject. */ REJECT, diff --git a/basex-core/src/main/java/org/basex/io/parse/json/JsonConverter.java b/basex-core/src/main/java/org/basex/io/parse/json/JsonConverter.java index ba60815219..2ed7043013 100644 --- a/basex-core/src/main/java/org/basex/io/parse/json/JsonConverter.java +++ b/basex-core/src/main/java/org/basex/io/parse/json/JsonConverter.java @@ -31,6 +31,8 @@ public abstract class JsonConverter extends Job { protected QueryFunction numberParser; /** Null value. */ protected Value nullValue = Empty.VALUE; + /** Input info. */ + protected InputInfo info; /** Interruptible job. */ protected Job job; @@ -110,6 +112,7 @@ public final Value convert(final IO input) throws QueryException, IOException { public final Value convert(final TextInput input, final String uri, final InputInfo ii, final Job jb) throws QueryException, IOException { job = jb; + info = ii; init(uri); new JsonParser(input, jopts, this).parse(ii); return finish(); diff --git a/basex-core/src/main/java/org/basex/io/parse/json/JsonW3Converter.java b/basex-core/src/main/java/org/basex/io/parse/json/JsonW3Converter.java index 52b1f25fb5..561cf64f24 100644 --- a/basex-core/src/main/java/org/basex/io/parse/json/JsonW3Converter.java +++ b/basex-core/src/main/java/org/basex/io/parse/json/JsonW3Converter.java @@ -2,6 +2,7 @@ import static org.basex.query.QueryError.*; +import java.math.*; import java.util.*; import org.basex.build.json.*; @@ -34,12 +35,19 @@ * @author Leo Woerteler */ public final class JsonW3Converter extends JsonConverter { + /** Maximum long value as BigDecimal. */ + private static final BigDecimal MAX_LONG_BD = BigDecimal.valueOf(Long.MAX_VALUE); + /** Minimum long value as BigDecimal. */ + private static final BigDecimal MIN_LONG_BD = BigDecimal.valueOf(Long.MIN_VALUE); + /** Stack for intermediate values. */ private final Stack stack = new Stack<>(); /** Stack for intermediate arrays. */ private final Stack arrays = new Stack<>(); /** Stack for intermediate maps. */ private final Stack maps = new Stack<>(); + /** Number format. */ + private JsonNumberFormat fmt; /** * Constructor. @@ -48,6 +56,7 @@ public final class JsonW3Converter extends JsonConverter { */ JsonW3Converter(final JsonParserOptions opts) throws QueryException { super(opts); + fmt = jopts.get(JsonParserOptions.NUMBER_FORMAT); final JsonDuplicates dupl = jopts.get(JsonParserOptions.DUPLICATES); if(dupl == JsonDuplicates.RETAIN) { throw OPTION_JSON_X.get(null, JsonParserOptions.DUPLICATES.name(), dupl); @@ -106,8 +115,38 @@ protected void closeArray() { } @Override - public void numberLit(final byte[] value) throws QueryException { - stack.push(numberParser != null ? numberParser.apply(value) : Dbl.get(Dbl.parse(value, null))); + public void numberLit(final byte[] string) throws QueryException { + Value value = null; + if(fmt != JsonNumberFormat.DOUBLE) { + final BigDecimal bd = Dec.parse(string, null, false); + if(bd != null) { + value = decimalOrInteger(bd); + } else if(fmt == JsonNumberFormat.DECIMAL) { + try { + value = decimalOrInteger(new BigDecimal(Dbl.parse(string, null))); + } catch(final NumberFormatException ex) { + Util.debug(ex); + throw FUNCCAST_X_X.get(info, "xs:decimal", string); + } + } + } + if(value == null) value = Dbl.get(Dbl.parse(string, null)); + stack.push(value); + } + + /** + * Converts a decimal to an integer if possible. + * @param bd decimal value + * @return integer or decimal item + */ + private static Item decimalOrInteger(final BigDecimal bd) { + final BigDecimal normalized = bd.stripTrailingZeros(); + if(normalized.scale() <= 0 && + normalized.compareTo(MIN_LONG_BD) >= 0 && + normalized.compareTo(MAX_LONG_BD) <= 0) { + return Itr.get(normalized.longValue()); + } + return Dec.get(bd); } @Override diff --git a/basex-core/src/main/java/org/basex/query/func/fn/ParseJson.java b/basex-core/src/main/java/org/basex/query/func/fn/ParseJson.java index 7f9ccdd738..acb1a1a3e1 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/ParseJson.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/ParseJson.java @@ -6,6 +6,7 @@ import org.basex.build.json.*; import org.basex.build.json.JsonOptions.*; +import org.basex.build.json.JsonParserOptions.*; import org.basex.io.in.*; import org.basex.io.parse.json.*; import org.basex.query.*; @@ -66,9 +67,16 @@ final Value parse(final TextInput ti, final Options options, final QueryContext } final Value numberParser = options.get(JsonParserOptions.NUMBER_PARSER); if(!numberParser.isEmpty()) { + if(jf == JsonFormat.W3 || jf == JsonFormat.XQUERY) { + throw INVALIDOPTION_X.get(info, Options.unknown(JsonParserOptions.NUMBER_PARSER)); + } final FItem np = toFunction(numberParser, 1, qc); converter.numberParser(s -> np.invoke(qc, info, Atm.get(s)).item(qc, info)); } + final JsonNumberFormat fmt = options.get(JsonParserOptions.NUMBER_FORMAT); + if(fmt != JsonNumberFormat.DOUBLE && jf != JsonFormat.W3 && jf != JsonFormat.XQUERY) { + throw INVALIDOPTION_X.get(info, Options.unknown(JsonParserOptions.NUMBER_FORMAT)); + } final Value nll = options.get(JsonParserOptions.NULL); if(nll != Empty.VALUE && jf != JsonFormat.W3 && jf != JsonFormat.XQUERY) { throw INVALIDOPTION_X.get(info, Options.unknown(JsonParserOptions.NULL)); diff --git a/basex-core/src/test/java/org/basex/query/func/FnModuleTest.java b/basex-core/src/test/java/org/basex/query/func/FnModuleTest.java index 4acfef84c5..df4b00c0d9 100644 --- a/basex-core/src/test/java/org/basex/query/func/FnModuleTest.java +++ b/basex-core/src/test/java/org/basex/query/func/FnModuleTest.java @@ -1988,6 +1988,11 @@ public final class FnModuleTest extends SandboxTest { final Function func = JSON_DOC; query(func.args("src/test/resources/example.json") + "('address')('state')", "NY"); query(func.args("src/test/resources/example.json") + "?address?state", "NY"); + query(func.args("src/test/resources/example.json", " { 'number-format': 'adaptive' }") + + "//postalCode/data() => type-of()", "xs:integer"); + + error(func.args("src/test/resources/example.json", " { 'number-parser': xs:decimal#1 }"), + INVALIDOPTION_X); } /** Test method. */ @@ -2805,6 +2810,27 @@ public final class FnModuleTest extends SandboxTest { "a[\\b]b\uD801\uDC02c[\\uD803]d[\\uDC04]e[\\uD805][\\uD806]"); query("try {" + func.args("nvll") + "} catch * { $err:description }", "(1:1): Unexpected JSON value: 'nvll'."); + + query(func.args("1") + " => type-of()", "xs:double"); + query(func.args("1", " { 'number-format': 'double' }") + " => type-of()", "xs:double"); + query(func.args("1e6", " { 'number-format': 'double' }") + " => type-of()", "xs:double"); + + query(func.args("1", " { 'number-format': 'decimal' }") + " => type-of()", "xs:integer"); + query(func.args("-5", " { 'number-format': 'decimal' }") + " => type-of()", "xs:integer"); + query(func.args("2.0", " { 'number-format': 'decimal' }") + " => type-of()", "xs:integer"); + query(func.args("1.5", " { 'number-format': 'decimal' }") + " => type-of()", "xs:decimal"); + query(func.args("1e6", " { 'number-format': 'decimal' }") + " => type-of()", "xs:integer"); + query(func.args("1.5e0", " { 'number-format': 'decimal' }") + " => type-of()", "xs:decimal"); + query(func.args("99999999999999999999", " { 'number-format': 'decimal' }") + + " => type-of()", "xs:decimal"); + + query(func.args("1", " { 'number-format': 'adaptive' }") + " => type-of()", "xs:integer"); + query(func.args("2.0", " { 'number-format': 'adaptive' }") + " => type-of()", "xs:integer"); + query(func.args("1.5", " { 'number-format': 'adaptive' }") + " => type-of()", "xs:decimal"); + query(func.args("1e6", " { 'number-format': 'adaptive' }") + " => type-of()", "xs:double"); + + error(func.args("1", " { 'number-parser': xs:decimal#1 }"), INVALIDOPTION_X); + error(func.args("1e9999", " { 'number-format': 'decimal' }"), FUNCCAST_X_X); } /** Test method. */ diff --git a/basex-core/src/test/java/org/basex/query/func/JsonModuleTest.java b/basex-core/src/test/java/org/basex/query/func/JsonModuleTest.java index 179115c698..ed784dc87a 100644 --- a/basex-core/src/test/java/org/basex/query/func/JsonModuleTest.java +++ b/basex-core/src/test/java/org/basex/query/func/JsonModuleTest.java @@ -128,7 +128,7 @@ public final class JsonModuleTest extends SandboxTest { query(func.args("-123.456E0001", options), "-1234.56"); query(func.args("[ -123.456E0001, 0 ]", options), "[-1234.56,0]"); - options = " { 'format': 'xquery', 'number-parser': xs:decimal#1 }"; + options = " { 'format': 'xquery', 'number-format': 'decimal' }"; String input = "1234567890123456789012345678901234567890"; query(func.args(input, options), input); input = "1234567890123456789012345678901234567890.123456789012345678901234567890123456789";