From 13c4b316fa9995680ce7e84d18ee2a758ead7138 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 2 Mar 2026 20:05:10 -0500 Subject: [PATCH 1/3] [feature] Implement declare decimal-format for XQuery 3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parser support for the XQuery 3.1 `declare decimal-format` and `declare default decimal-format` prolog declarations (spec section 4.10), enabling users to customize number formatting via fn:format-number. The runtime infrastructure (DecimalFormat class, XQueryContext storage, FnFormatNumbers 3-arg support) was already in place — this adds the missing parser recognition and tree walker processing. Changes: - XQuery.g: Add DECIMAL_FORMAT_DECL/DEF_DECIMAL_FORMAT_DECL tokens, grammar rules for named and default forms, property keywords - XQueryTree.g: Walk AST, validate properties (single-char, zero-digit, distinctness), register formats in XQueryContext - ErrorCodes.java: Add XQST0097 (duplicate) and XQST0098 (invalid) - XQueryContext.java: Add setDefaultStaticDecimalFormat() convenience - format-numbers.xql: Add tests for named/default formats, custom NaN/infinity, and error cases Closes #56 Co-Authored-By: Claude Opus 4.6 --- .../antlr/org/exist/xquery/parser/XQuery.g | 52 ++++++- .../org/exist/xquery/parser/XQueryTree.g | 147 ++++++++++++++++++ .../java/org/exist/xquery/ErrorCodes.java | 8 + .../java/org/exist/xquery/XQueryContext.java | 4 + .../test/xquery/numbers/format-numbers.xql | 100 ++++++++++++ 5 files changed, 310 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index 39b3992d7f5..4c2805dc663 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -219,6 +219,8 @@ imaginaryTokenDefinitions PREVIOUS_ITEM NEXT_ITEM WINDOW_VARS + DECIMAL_FORMAT_DECL + DEF_DECIMAL_FORMAT_DECL ; // === XPointer === @@ -283,7 +285,7 @@ prolog throws XPathException ( importDecl | - ( "declare" ( "default" | "boundary-space" | "ordering" | "construction" | "base-uri" | "copy-namespaces" | "namespace" ) ) => + ( "declare" ( "default" | "boundary-space" | "ordering" | "construction" | "base-uri" | "copy-namespaces" | "namespace" | "decimal-format" ) ) => s:setter { if(!inSetters) @@ -338,6 +340,9 @@ setter { #setter= #(#[DEF_FUNCTION_NS_DECL, "defaultFunctionNSDecl"], deff); } | "order"^ "empty"! ( "greatest" | "least" ) + | + "decimal-format"! ( dfDefProperty )* + { #setter = #(#[DEF_DECIMAL_FORMAT_DECL, "defaultDecimalFormatDecl"], #setter); } ) | ( "declare" "boundary-space" ) => @@ -357,9 +362,30 @@ setter | ( "declare" "namespace" ) => namespaceDecl + | + ( "declare" "decimal-format" ) => + decimalFormatDecl ) ; +decimalFormatDecl +{ String eq = null; } +: + decl:"declare"! "decimal-format"! eq=eqName! ( dfDefProperty )* + { + #decimalFormatDecl = #(#[DECIMAL_FORMAT_DECL, eq], #decimalFormatDecl); + #decimalFormatDecl.copyLexInfo(#decl); + } + ; + +dfDefProperty +: + ( "decimal-separator"^ | "grouping-separator"^ | "infinity"^ | "minus-sign"^ + | "NaN"^ | "percent"^ | "per-mille"^ | "zero-digit"^ | "digit"^ + | "pattern-separator"^ | "exponent-separator"^ ) + EQ! STRING_LITERAL + ; + preserveMode : ( "preserve" | "no-preserve" ) @@ -2367,6 +2393,30 @@ reservedKeywords returns [String name] "next" { name = "next"; } | "when" { name = "when"; } + | + "decimal-format" { name = "decimal-format"; } + | + "decimal-separator" { name = "decimal-separator"; } + | + "grouping-separator" { name = "grouping-separator"; } + | + "infinity" { name = "infinity"; } + | + "minus-sign" { name = "minus-sign"; } + | + "NaN" { name = "NaN"; } + | + "percent" { name = "percent"; } + | + "per-mille" { name = "per-mille"; } + | + "zero-digit" { name = "zero-digit"; } + | + "digit" { name = "digit"; } + | + "pattern-separator" { name = "pattern-separator"; } + | + "exponent-separator" { name = "exponent-separator"; } ; diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 98520186e65..8e7d00e6706 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -236,6 +236,122 @@ options { return variableName; } } + + private static String requireSingleChar(final AST node, final String propName, final String value) throws XPathException { + if (value.codePointCount(0, value.length()) != 1) { + throw new XPathException(node.getLine(), node.getColumn(), ErrorCodes.XQST0098, + "The value of decimal-format property '" + propName + "' must be a single character, but got: \"" + value + "\""); + } + return value; + } + + private static void validateZeroDigit(final AST node, final String value) throws XPathException { + final int cp = value.codePointAt(0); + if (Character.getType(cp) != Character.DECIMAL_DIGIT_NUMBER || Character.getNumericValue(cp) != 0) { + throw new XPathException(node.getLine(), node.getColumn(), ErrorCodes.XQST0098, + "The value of decimal-format property 'zero-digit' must be a Unicode digit with numeric value zero, but got: \"" + value + "\""); + } + } + + private static void validateDistinctPictureChars(final AST node, final DecimalFormat df) throws XPathException { + // The 8 single-character picture-string properties must all have distinct values + final int[] chars = { df.decimalSeparator, df.groupingSeparator, df.percent, df.perMille, + df.zeroDigit, df.digit, df.patternSeparator, df.exponentSeparator }; + final String[] names = { "decimal-separator", "grouping-separator", "percent", "per-mille", + "zero-digit", "digit", "pattern-separator", "exponent-separator" }; + for (int i = 0; i < chars.length; i++) { + for (int j = i + 1; j < chars.length; j++) { + if (chars[i] == chars[j]) { + throw new XPathException(node.getLine(), node.getColumn(), ErrorCodes.XQST0098, + "Decimal-format properties '" + names[i] + "' and '" + names[j] + + "' must have distinct values, but both are: '" + new String(Character.toChars(chars[i])) + "'"); + } + } + } + } + + private DecimalFormat processDecimalFormatProperties(final AST parentNode) throws XPathException { + // Start with UNNAMED defaults + int decimalSeparator = DecimalFormat.UNNAMED.decimalSeparator; + int exponentSeparator = DecimalFormat.UNNAMED.exponentSeparator; + int groupingSeparator = DecimalFormat.UNNAMED.groupingSeparator; + int percent = DecimalFormat.UNNAMED.percent; + int perMille = DecimalFormat.UNNAMED.perMille; + int zeroDigit = DecimalFormat.UNNAMED.zeroDigit; + int digit = DecimalFormat.UNNAMED.digit; + int patternSeparator = DecimalFormat.UNNAMED.patternSeparator; + String infinity = DecimalFormat.UNNAMED.infinity; + String nan = DecimalFormat.UNNAMED.NaN; + int minusSign = DecimalFormat.UNNAMED.minusSign; + + AST child = parentNode.getFirstChild(); + while (child != null) { + final String propName = child.getText(); + final AST valueNode = child.getFirstChild(); + if (valueNode == null) { + child = child.getNextSibling(); + continue; + } + final String value = valueNode.getText(); + + switch (propName) { + case "decimal-separator": + requireSingleChar(child, propName, value); + decimalSeparator = value.codePointAt(0); + break; + case "grouping-separator": + requireSingleChar(child, propName, value); + groupingSeparator = value.codePointAt(0); + break; + case "infinity": + infinity = value; + break; + case "minus-sign": + requireSingleChar(child, propName, value); + minusSign = value.codePointAt(0); + break; + case "NaN": + nan = value; + break; + case "percent": + requireSingleChar(child, propName, value); + percent = value.codePointAt(0); + break; + case "per-mille": + requireSingleChar(child, propName, value); + perMille = value.codePointAt(0); + break; + case "zero-digit": + requireSingleChar(child, propName, value); + validateZeroDigit(child, value); + zeroDigit = value.codePointAt(0); + break; + case "digit": + requireSingleChar(child, propName, value); + digit = value.codePointAt(0); + break; + case "pattern-separator": + requireSingleChar(child, propName, value); + patternSeparator = value.codePointAt(0); + break; + case "exponent-separator": + requireSingleChar(child, propName, value); + exponentSeparator = value.codePointAt(0); + break; + default: + break; + } + child = child.getNextSibling(); + } + + final DecimalFormat df = new DecimalFormat( + decimalSeparator, exponentSeparator, groupingSeparator, + percent, perMille, zeroDigit, digit, + patternSeparator, infinity, nan, minusSign + ); + validateDistinctPictureChars(parentNode, df); + return df; + } } xpointer [PathExpr path] @@ -367,6 +483,8 @@ throws PermissionDeniedException, EXistException, XPathException boolean baseuri = false; boolean ordering = false; boolean construction = false; + Set declaredDecimalFormats = new HashSet(); + boolean defaultDecimalFormatDeclared = false; }: ( @@ -665,6 +783,35 @@ throws PermissionDeniedException, EXistException, XPathException ) ) | + #( + dfDecl:DECIMAL_FORMAT_DECL (.)* + { + final QName dfQName; + try { + dfQName = QName.parse(staticContext, dfDecl.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(dfDecl.getLine(), dfDecl.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix in decimal format name: " + dfDecl.getText()); + } + final String dfKey = dfQName.getNamespaceURI() + ":" + dfQName.getLocalPart(); + if (declaredDecimalFormats.contains(dfKey)) + throw new XPathException(dfDecl, ErrorCodes.XQST0097, "Duplicate decimal format declaration: " + dfDecl.getText()); + declaredDecimalFormats.add(dfKey); + final DecimalFormat df = processDecimalFormatProperties(dfDecl); + context.setStaticDecimalFormat(dfQName, df); + } + ) + | + #( + defDfDecl:DEF_DECIMAL_FORMAT_DECL (.)* + { + if (defaultDecimalFormatDeclared) + throw new XPathException(defDfDecl, ErrorCodes.XQST0097, "Duplicate default decimal format declaration."); + defaultDecimalFormatDeclared = true; + final DecimalFormat df = processDecimalFormatProperties(defDfDecl); + context.setDefaultStaticDecimalFormat(df); + } + ) + | functionDecl [path] | importDecl [path] diff --git a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java index 903edab957a..331d2ff7880 100644 --- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java +++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java @@ -129,6 +129,14 @@ public class ErrorCodes { public static final ErrorCode XQST0094 = new W3CErrorCode("XQST0094", "The name of each grouping variable must be equal (by the eq operator on expanded QNames) to the name of a variable in the input tuple stream."); + public static final ErrorCode XQST0097 = new W3CErrorCode("XQST0097", + """ + It is a static error to have more than one decimal-format declaration with the same name, \ + or more than one default decimal-format declaration, in the same module."""); + public static final ErrorCode XQST0098 = new W3CErrorCode("XQST0098", + """ + It is a static error if the properties representing characters used in a picture string \ + do not each have distinct values, or if a property value is not valid for its property."""); public static final ErrorCode XQST0118 = new W3CErrorCode("XQST0118", "It is a static error if an element constructor uses a name in the end tag that does not match the name in the start tag."); public static final ErrorCode XQST0125 = new W3CErrorCode("XQST0125", "It is a static error if an inline function expression is annotated as %public or %private."); diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index 9f5e0bbf7ab..cd21364036f 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -2993,6 +2993,10 @@ public void setStaticDecimalFormat(final QName qnDecimalFormat, final DecimalFor staticDecimalFormats.put(qnDecimalFormat, decimalFormat); } + public void setDefaultStaticDecimalFormat(final DecimalFormat decimalFormat) { + staticDecimalFormats.put(UNNAMED_DECIMAL_FORMAT, decimalFormat); + } + public Map getCachedUriCollectionResults() { return cachedUriCollectionResults; } diff --git a/exist-core/src/test/xquery/numbers/format-numbers.xql b/exist-core/src/test/xquery/numbers/format-numbers.xql index 23080940618..8746b4bf059 100644 --- a/exist-core/src/test/xquery/numbers/format-numbers.xql +++ b/exist-core/src/test/xquery/numbers/format-numbers.xql @@ -316,4 +316,104 @@ declare %test:assertEquals("1.235e-10") function fd:exponent-fails($number as xs:double, $picture as xs:string) { format-number($number, $picture) +}; + +(:~ + : Named decimal-format with European conventions (comma as decimal separator, dot as grouping separator). + :) +declare + %test:assertEquals("1.234,50") +function fd:named-decimal-format-eu() { + util:eval(' + declare namespace local = "http://local"; + declare decimal-format local:eu decimal-separator = "," grouping-separator = "."; + format-number(1234.5, "#.##0,00", "local:eu") + ') +}; + +(:~ + : Default decimal-format with European conventions. + :) +declare + %test:assertEquals("1.234,50") +function fd:default-decimal-format-eu() { + util:eval(' + declare default decimal-format decimal-separator = "," grouping-separator = "."; + format-number(1234.5, "#.##0,00") + ') +}; + +(:~ + : Custom NaN property. + :) +declare + %test:assertEquals("not a number") +function fd:custom-nan() { + util:eval(' + declare default decimal-format NaN = "not a number"; + format-number(number(''NaN''), "#.00") + ') +}; + +(:~ + : Custom infinity property. + :) +declare + %test:assertEquals("INFINITY") +function fd:custom-infinity() { + util:eval(' + declare default decimal-format infinity = "INFINITY"; + format-number(1 div 0e0, "#.00") + ') +}; + +(:~ + : Error: duplicate named decimal-format declaration. + :) +declare + %test:assertError("XQST0097") +function fd:error-duplicate-named-decimal-format() { + util:eval(' + declare namespace local = "http://local"; + declare decimal-format local:eu decimal-separator = "," grouping-separator = "."; + declare decimal-format local:eu decimal-separator = "," grouping-separator = "."; + format-number(1234.5, "#.##0,00", "local:eu") + ') +}; + +(:~ + : Error: duplicate default decimal-format declaration. + :) +declare + %test:assertError("XQST0097") +function fd:error-duplicate-default-decimal-format() { + util:eval(' + declare default decimal-format decimal-separator = "," grouping-separator = "."; + declare default decimal-format decimal-separator = "," grouping-separator = "."; + format-number(1234.5, "#.##0,00") + ') +}; + +(:~ + : Error: non-distinct property values (decimal-separator and grouping-separator are the same). + :) +declare + %test:assertError("XQST0098") +function fd:error-non-distinct-properties() { + util:eval(' + declare default decimal-format decimal-separator = "." grouping-separator = "."; + format-number(1234.5, "#.##0.00") + ') +}; + +(:~ + : Error: invalid zero-digit value. + :) +declare + %test:assertError("XQST0098") +function fd:error-invalid-zero-digit() { + util:eval(' + declare default decimal-format zero-digit = "A"; + format-number(1234.5, "#,##0.00") + ') }; \ No newline at end of file From c3082d92a43e70717fcf00ed5d923e9f3bcfa4f5 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Thu, 5 Mar 2026 09:55:06 -0500 Subject: [PATCH 2/3] [refactor] Address review: add df prefix to decimal-format helpers, add Javadoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the three decimal-format validation helper methods in XQueryTree.g with a `df` prefix to clarify their scope: - requireSingleChar → dfRequireSingleChar - validateZeroDigit → dfValidateZeroDigit - validateDistinctPictureChars → dfValidateDistinctPictureChars Add Javadoc comments on DecimalFormat.UNNAMED and UNNAMED_DECIMAL_FORMAT explaining the XPath 3.1 spec origin of the "unnamed" terminology. Co-Authored-By: Claude Opus 4.6 --- .../org/exist/xquery/parser/XQueryTree.g | 28 +++++++++---------- .../java/org/exist/xquery/XQueryContext.java | 3 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 8e7d00e6706..158f6c33db8 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -237,7 +237,7 @@ options { } } - private static String requireSingleChar(final AST node, final String propName, final String value) throws XPathException { + private static String dfRequireSingleChar(final AST node, final String propName, final String value) throws XPathException { if (value.codePointCount(0, value.length()) != 1) { throw new XPathException(node.getLine(), node.getColumn(), ErrorCodes.XQST0098, "The value of decimal-format property '" + propName + "' must be a single character, but got: \"" + value + "\""); @@ -245,7 +245,7 @@ options { return value; } - private static void validateZeroDigit(final AST node, final String value) throws XPathException { + private static void dfValidateZeroDigit(final AST node, final String value) throws XPathException { final int cp = value.codePointAt(0); if (Character.getType(cp) != Character.DECIMAL_DIGIT_NUMBER || Character.getNumericValue(cp) != 0) { throw new XPathException(node.getLine(), node.getColumn(), ErrorCodes.XQST0098, @@ -253,7 +253,7 @@ options { } } - private static void validateDistinctPictureChars(final AST node, final DecimalFormat df) throws XPathException { + private static void dfValidateDistinctPictureChars(final AST node, final DecimalFormat df) throws XPathException { // The 8 single-character picture-string properties must all have distinct values final int[] chars = { df.decimalSeparator, df.groupingSeparator, df.percent, df.perMille, df.zeroDigit, df.digit, df.patternSeparator, df.exponentSeparator }; @@ -296,46 +296,46 @@ options { switch (propName) { case "decimal-separator": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); decimalSeparator = value.codePointAt(0); break; case "grouping-separator": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); groupingSeparator = value.codePointAt(0); break; case "infinity": infinity = value; break; case "minus-sign": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); minusSign = value.codePointAt(0); break; case "NaN": nan = value; break; case "percent": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); percent = value.codePointAt(0); break; case "per-mille": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); perMille = value.codePointAt(0); break; case "zero-digit": - requireSingleChar(child, propName, value); - validateZeroDigit(child, value); + dfRequireSingleChar(child, propName, value); + dfValidateZeroDigit(child, value); zeroDigit = value.codePointAt(0); break; case "digit": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); digit = value.codePointAt(0); break; case "pattern-separator": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); patternSeparator = value.codePointAt(0); break; case "exponent-separator": - requireSingleChar(child, propName, value); + dfRequireSingleChar(child, propName, value); exponentSeparator = value.codePointAt(0); break; default: @@ -349,7 +349,7 @@ options { percent, perMille, zeroDigit, digit, patternSeparator, infinity, nan, minusSign ); - validateDistinctPictureChars(parentNode, df); + dfValidateDistinctPictureChars(parentNode, df); return df; } } diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index cd21364036f..341dc70c671 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -419,6 +419,9 @@ public class XQueryContext implements BinaryValueManager, Context { * HTTP context. */ private @Nullable HttpContext httpContext = null; + /** + * Sentinel QName for the default (unnamed) decimal format per XQuery 3.1 §4.10. + */ private static final QName UNNAMED_DECIMAL_FORMAT = new QName("__UNNAMED__", Function.BUILTIN_FUNCTION_NS); private final Map staticDecimalFormats = hashMap(Tuple(UNNAMED_DECIMAL_FORMAT, DecimalFormat.UNNAMED)); From fda2cc91f36a4e23e13783fa4fba29db5d56a5aa Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Tue, 5 May 2026 20:01:12 -0400 Subject: [PATCH 3/3] [refactor] Use switch expression for decimal-format property dispatch Address @reinhapa's review comment on PR #6217: convert the property-name dispatch in the decimal-format AST handler from a traditional switch/break statement into arrow-syntax cases. No behavioural change; NumberTests still pass (161 run, 0 failures). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../org/exist/xquery/parser/XQueryTree.g | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 158f6c33db8..f8962bf90df 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -295,51 +295,46 @@ options { final String value = valueNode.getText(); switch (propName) { - case "decimal-separator": + case "decimal-separator" -> { dfRequireSingleChar(child, propName, value); decimalSeparator = value.codePointAt(0); - break; - case "grouping-separator": + } + case "grouping-separator" -> { dfRequireSingleChar(child, propName, value); groupingSeparator = value.codePointAt(0); - break; - case "infinity": - infinity = value; - break; - case "minus-sign": + } + case "infinity" -> infinity = value; + case "minus-sign" -> { dfRequireSingleChar(child, propName, value); minusSign = value.codePointAt(0); - break; - case "NaN": - nan = value; - break; - case "percent": + } + case "NaN" -> nan = value; + case "percent" -> { dfRequireSingleChar(child, propName, value); percent = value.codePointAt(0); - break; - case "per-mille": + } + case "per-mille" -> { dfRequireSingleChar(child, propName, value); perMille = value.codePointAt(0); - break; - case "zero-digit": + } + case "zero-digit" -> { dfRequireSingleChar(child, propName, value); dfValidateZeroDigit(child, value); zeroDigit = value.codePointAt(0); - break; - case "digit": + } + case "digit" -> { dfRequireSingleChar(child, propName, value); digit = value.codePointAt(0); - break; - case "pattern-separator": + } + case "pattern-separator" -> { dfRequireSingleChar(child, propName, value); patternSeparator = value.codePointAt(0); - break; - case "exponent-separator": + } + case "exponent-separator" -> { dfRequireSingleChar(child, propName, value); exponentSeparator = value.codePointAt(0); - break; - default: - break; + } + default -> { } } child = child.getNextSibling(); }