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 d852d700444..77e1f556d5a 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
@@ -192,6 +192,8 @@ imaginaryTokenDefinitions
PREVIOUS_ITEM
NEXT_ITEM
WINDOW_VARS
+ DECIMAL_FORMAT_DECL
+ DEF_DECIMAL_FORMAT_DECL
;
// === XPointer ===
@@ -256,7 +258,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)
@@ -311,6 +313,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" ) =>
@@ -330,9 +335,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" )
@@ -2304,6 +2330,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 20308296806..9781c206c15 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
@@ -210,6 +210,122 @@ options {
return variableName;
}
}
+
+ 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 + "\"");
+ }
+ return value;
+ }
+
+ 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,
+ "The value of decimal-format property 'zero-digit' must be a Unicode digit with numeric value zero, but got: \"" + value + "\"");
+ }
+ }
+
+ 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 };
+ 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":
+ dfRequireSingleChar(child, propName, value);
+ decimalSeparator = value.codePointAt(0);
+ break;
+ case "grouping-separator":
+ dfRequireSingleChar(child, propName, value);
+ groupingSeparator = value.codePointAt(0);
+ break;
+ case "infinity":
+ infinity = value;
+ break;
+ case "minus-sign":
+ dfRequireSingleChar(child, propName, value);
+ minusSign = value.codePointAt(0);
+ break;
+ case "NaN":
+ nan = value;
+ break;
+ case "percent":
+ dfRequireSingleChar(child, propName, value);
+ percent = value.codePointAt(0);
+ break;
+ case "per-mille":
+ dfRequireSingleChar(child, propName, value);
+ perMille = value.codePointAt(0);
+ break;
+ case "zero-digit":
+ dfRequireSingleChar(child, propName, value);
+ dfValidateZeroDigit(child, value);
+ zeroDigit = value.codePointAt(0);
+ break;
+ case "digit":
+ dfRequireSingleChar(child, propName, value);
+ digit = value.codePointAt(0);
+ break;
+ case "pattern-separator":
+ dfRequireSingleChar(child, propName, value);
+ patternSeparator = value.codePointAt(0);
+ break;
+ case "exponent-separator":
+ dfRequireSingleChar(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
+ );
+ dfValidateDistinctPictureChars(parentNode, df);
+ return df;
+ }
}
xpointer [PathExpr path]
@@ -337,6 +453,8 @@ throws PermissionDeniedException, EXistException, XPathException
boolean baseuri = false;
boolean ordering = false;
boolean construction = false;
+ Set declaredDecimalFormats = new HashSet();
+ boolean defaultDecimalFormatDeclared = false;
}:
(
@@ -632,6 +750,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/DecimalFormat.java b/exist-core/src/main/java/org/exist/xquery/DecimalFormat.java
index 46a54962ad5..bc4a7b4add3 100644
--- a/exist-core/src/main/java/org/exist/xquery/DecimalFormat.java
+++ b/exist-core/src/main/java/org/exist/xquery/DecimalFormat.java
@@ -24,7 +24,7 @@
/**
* Data class for a Decimal Format.
*
- * See https://www.w3.org/TR/xpath-31/#dt-static-decimal-formats
+ * See https://www.w3.org/TR/xquery-31/#id-decimal-format-decl
*
* NOTE: UTF-16 characters are stored as code-points!
*
@@ -32,6 +32,11 @@
*/
public class DecimalFormat {
+ /**
+ * The default (unnamed) decimal format as defined by the XQuery 3.1 specification.
+ *
+ * @see XQuery 3.1 §4.10: Decimal Format Declaration
+ */
public static final DecimalFormat UNNAMED = new DecimalFormat(
'.',
'e',
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 23226a155f2..a50e4794086 100644
--- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java
+++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java
@@ -127,6 +127,13 @@ 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 XQDY0101 = new W3CErrorCode("XQDY0101", "An error is raised if a computed namespace constructor attempts to do any of the following:\n" +
"Bind the prefix xml to some namespace URI other than http://www.w3.org/XML/1998/namespace.\n" +
"Bind a prefix other than xml to the namespace URI http://www.w3.org/XML/1998/namespace.\n" +
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 b3721c34179..7c3b84a2ce3 100644
--- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
+++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
@@ -420,6 +420,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));
@@ -2902,6 +2905,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