@tradik/xslt3 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/LICENSE.md +28 -0
  2. package/README.md +98 -2
  3. package/package.json +43 -4
  4. package/src/errors.js +20 -0
  5. package/src/functions/aggregates.js +144 -0
  6. package/src/functions/arraySort.js +33 -0
  7. package/src/functions/arrays.js +181 -0
  8. package/src/functions/attributeTypes.js +58 -0
  9. package/src/functions/baseUris.js +82 -0
  10. package/src/functions/collations.js +127 -0
  11. package/src/functions/collections.js +84 -0
  12. package/src/functions/context.js +83 -0
  13. package/src/functions/core.js +167 -0
  14. package/src/functions/datetime.js +203 -0
  15. package/src/functions/deepEqual.js +132 -0
  16. package/src/functions/diagnostics.js +88 -0
  17. package/src/functions/focus.js +34 -0
  18. package/src/functions/format/dateComponents.js +176 -0
  19. package/src/functions/format/datePicture.js +115 -0
  20. package/src/functions/format/decimalFormat.js +65 -0
  21. package/src/functions/format/digitPattern.js +145 -0
  22. package/src/functions/format/formatDateTime.js +204 -0
  23. package/src/functions/format/formatInteger.js +109 -0
  24. package/src/functions/format/formatNumber.js +165 -0
  25. package/src/functions/format/fractionFormat.js +80 -0
  26. package/src/functions/format/germanWords.js +89 -0
  27. package/src/functions/format/numberLanguages.js +41 -0
  28. package/src/functions/format/numberPicture.js +202 -0
  29. package/src/functions/format/numberWords.js +155 -0
  30. package/src/functions/format/place.js +74 -0
  31. package/src/functions/format/timezoneFormat.js +89 -0
  32. package/src/functions/higherOrder.js +106 -0
  33. package/src/functions/ids.js +127 -0
  34. package/src/functions/json/index.js +17 -0
  35. package/src/functions/json/jsonChars.js +110 -0
  36. package/src/functions/json/jsonParser.js +203 -0
  37. package/src/functions/json/jsonStrings.js +116 -0
  38. package/src/functions/json/jsonToXml.js +116 -0
  39. package/src/functions/json/parseJson.js +119 -0
  40. package/src/functions/json/xmlRepresentation.js +103 -0
  41. package/src/functions/json/xmlToJson.js +147 -0
  42. package/src/functions/library.js +47 -0
  43. package/src/functions/mapArrayEqual.js +41 -0
  44. package/src/functions/maps.js +152 -0
  45. package/src/functions/math.js +73 -0
  46. package/src/functions/nodePaths.js +181 -0
  47. package/src/functions/nodes.js +199 -0
  48. package/src/functions/numeric.js +148 -0
  49. package/src/functions/options.js +58 -0
  50. package/src/functions/parseIetfDate.js +104 -0
  51. package/src/functions/parseXml.js +77 -0
  52. package/src/functions/qnames.js +68 -0
  53. package/src/functions/random.js +128 -0
  54. package/src/functions/regex/analyzeString.js +144 -0
  55. package/src/functions/regex/charClass.js +112 -0
  56. package/src/functions/regex/compile.js +81 -0
  57. package/src/functions/regex/escapes.js +109 -0
  58. package/src/functions/regex/parser.js +169 -0
  59. package/src/functions/regex/reader.js +37 -0
  60. package/src/functions/regex/regexFunctions.js +140 -0
  61. package/src/functions/regex/translate.js +102 -0
  62. package/src/functions/regex/unicodeBlocks.js +194 -0
  63. package/src/functions/registry.js +140 -0
  64. package/src/functions/sequences.js +173 -0
  65. package/src/functions/serializeFunction.js +53 -0
  66. package/src/functions/signatures.js +105 -0
  67. package/src/functions/sort.js +102 -0
  68. package/src/functions/stringMatch.js +45 -0
  69. package/src/functions/strings.js +194 -0
  70. package/src/functions/stringsCore.js +66 -0
  71. package/src/functions/support.js +106 -0
  72. package/src/functions/textDecoding.js +117 -0
  73. package/src/functions/ucaCollation.js +127 -0
  74. package/src/functions/unparsedText.js +71 -0
  75. package/src/functions/uri.js +166 -0
  76. package/src/functions/xqueryModule.js +24 -0
  77. package/src/index.js +38 -0
  78. package/src/items/array.js +84 -0
  79. package/src/items/function.js +45 -0
  80. package/src/items/map.js +120 -0
  81. package/src/items/mapKey.js +82 -0
  82. package/src/serialize/index.js +129 -0
  83. package/src/serialize/markup/content.js +74 -0
  84. package/src/serialize/markup/dialect.js +178 -0
  85. package/src/serialize/markup/htmlData.js +70 -0
  86. package/src/serialize/markup/namespaces.js +145 -0
  87. package/src/serialize/markup/prolog.js +91 -0
  88. package/src/serialize/markup/writer.js +183 -0
  89. package/src/serialize/methods/adaptive.js +131 -0
  90. package/src/serialize/methods/json.js +158 -0
  91. package/src/serialize/methods/text.js +43 -0
  92. package/src/serialize/output/buffer.js +48 -0
  93. package/src/serialize/output/encoding.js +146 -0
  94. package/src/serialize/output/expander.js +204 -0
  95. package/src/serialize/params/fromElement.js +141 -0
  96. package/src/serialize/params/fromMap.js +128 -0
  97. package/src/serialize/params/names.js +69 -0
  98. package/src/serialize/params/settings.js +153 -0
  99. package/src/serialize/params/values.js +163 -0
  100. package/src/serialize/sequence.js +111 -0
  101. package/src/xdm/arithmetic.js +155 -0
  102. package/src/xdm/atomic.js +142 -0
  103. package/src/xdm/binary.js +92 -0
  104. package/src/xdm/calendar.js +71 -0
  105. package/src/xdm/cast.js +176 -0
  106. package/src/xdm/compare.js +192 -0
  107. package/src/xdm/datetime.js +206 -0
  108. package/src/xdm/decimal.js +200 -0
  109. package/src/xdm/duration.js +128 -0
  110. package/src/xdm/generalCompare.js +152 -0
  111. package/src/xdm/index.js +47 -0
  112. package/src/xdm/lexical.js +126 -0
  113. package/src/xdm/nodes.js +170 -0
  114. package/src/xdm/numeric.js +122 -0
  115. package/src/xdm/qname.js +65 -0
  116. package/src/xdm/strings.js +52 -0
  117. package/src/xdm/temporalArithmetic.js +180 -0
  118. package/src/xdm/timeline.js +68 -0
  119. package/src/xdm/types.js +178 -0
  120. package/src/xpath/eval/atomics.js +45 -0
  121. package/src/xpath/eval/axes.js +212 -0
  122. package/src/xpath/eval/calls.js +168 -0
  123. package/src/xpath/eval/coercion.js +146 -0
  124. package/src/xpath/eval/compiler.js +71 -0
  125. package/src/xpath/eval/constructors.js +129 -0
  126. package/src/xpath/eval/decimalFormats.js +93 -0
  127. package/src/xpath/eval/descendantMemo.js +77 -0
  128. package/src/xpath/eval/documentOrder.js +142 -0
  129. package/src/xpath/eval/domNodes.js +174 -0
  130. package/src/xpath/eval/dynamicCall.js +27 -0
  131. package/src/xpath/eval/dynamicContext.js +161 -0
  132. package/src/xpath/eval/flow.js +115 -0
  133. package/src/xpath/eval/functionItems.js +194 -0
  134. package/src/xpath/eval/groupedChildren.js +88 -0
  135. package/src/xpath/eval/inline.js +71 -0
  136. package/src/xpath/eval/namespaceNodes.js +90 -0
  137. package/src/xpath/eval/nodeTests.js +164 -0
  138. package/src/xpath/eval/operators.js +190 -0
  139. package/src/xpath/eval/paths.js +163 -0
  140. package/src/xpath/eval/positional.js +81 -0
  141. package/src/xpath/eval/predicates.js +56 -0
  142. package/src/xpath/eval/primary.js +99 -0
  143. package/src/xpath/eval/resources.js +149 -0
  144. package/src/xpath/eval/scope.js +108 -0
  145. package/src/xpath/eval/sequenceType.js +193 -0
  146. package/src/xpath/eval/staticContext.js +142 -0
  147. package/src/xpath/eval/steps.js +98 -0
  148. package/src/xpath/eval/subtype.js +154 -0
  149. package/src/xpath/eval/typeExprs.js +147 -0
  150. package/src/xpath/eval/typeNames.js +101 -0
  151. package/src/xpath/eval/uris.js +21 -0
  152. package/src/xpath/eval/values.js +64 -0
  153. package/src/xpath/index.js +217 -0
  154. package/src/xpath/syntax/ast.js +130 -0
  155. package/src/xpath/syntax/chars.js +58 -0
  156. package/src/xpath/syntax/constructors.js +144 -0
  157. package/src/xpath/syntax/expressions.js +107 -0
  158. package/src/xpath/syntax/index.js +44 -0
  159. package/src/xpath/syntax/kindTests.js +134 -0
  160. package/src/xpath/syntax/lexer.js +220 -0
  161. package/src/xpath/syntax/operators.js +111 -0
  162. package/src/xpath/syntax/paths.js +102 -0
  163. package/src/xpath/syntax/postfix.js +91 -0
  164. package/src/xpath/syntax/primaries.js +149 -0
  165. package/src/xpath/syntax/steps.js +146 -0
  166. package/src/xpath/syntax/syntaxError.js +31 -0
  167. package/src/xpath/syntax/tokenStream.js +163 -0
  168. package/src/xpath/syntax/typeAst.js +76 -0
  169. package/src/xpath/syntax/typeOperators.js +127 -0
  170. package/src/xpath/syntax/types.js +158 -0
  171. package/src/xslt/api.js +153 -0
  172. package/src/xslt/compiler/acceptance.js +178 -0
  173. package/src/xslt/compiler/accumulatorDecl.js +146 -0
  174. package/src/xslt/compiler/attributeValues.js +174 -0
  175. package/src/xslt/compiler/attributes.js +211 -0
  176. package/src/xslt/compiler/avt.js +103 -0
  177. package/src/xslt/compiler/body.js +154 -0
  178. package/src/xslt/compiler/children.js +105 -0
  179. package/src/xslt/compiler/compatibility.js +130 -0
  180. package/src/xslt/compiler/components.js +179 -0
  181. package/src/xslt/compiler/contextItem.js +95 -0
  182. package/src/xslt/compiler/decimalFormats.js +132 -0
  183. package/src/xslt/compiler/declarationKinds.js +82 -0
  184. package/src/xslt/compiler/elementInfo.js +246 -0
  185. package/src/xslt/compiler/expressions.js +175 -0
  186. package/src/xslt/compiler/globalContextItem.js +92 -0
  187. package/src/xslt/compiler/globals.js +209 -0
  188. package/src/xslt/compiler/linker.js +134 -0
  189. package/src/xslt/compiler/miscDecl.js +171 -0
  190. package/src/xslt/compiler/modeDecl.js +88 -0
  191. package/src/xslt/compiler/moduleLoading.js +111 -0
  192. package/src/xslt/compiler/modules.js +158 -0
  193. package/src/xslt/compiler/outputDecl.js +209 -0
  194. package/src/xslt/compiler/overrideLinks.js +141 -0
  195. package/src/xslt/compiler/packageChecks.js +84 -0
  196. package/src/xslt/compiler/packageRegistry.js +95 -0
  197. package/src/xslt/compiler/packageTable.js +181 -0
  198. package/src/xslt/compiler/packageVersions.js +143 -0
  199. package/src/xslt/compiler/packages.js +138 -0
  200. package/src/xslt/compiler/registry.js +234 -0
  201. package/src/xslt/compiler/staticStage.js +197 -0
  202. package/src/xslt/compiler/staticVariables.js +93 -0
  203. package/src/xslt/compiler/stylesheet.js +205 -0
  204. package/src/xslt/compiler/tailPosition.js +74 -0
  205. package/src/xslt/compiler/templatesDecl.js +180 -0
  206. package/src/xslt/compiler/useAccumulators.js +76 -0
  207. package/src/xslt/compiler/useWhen.js +94 -0
  208. package/src/xslt/compiler/visibility.js +160 -0
  209. package/src/xslt/index.js +11 -0
  210. package/src/xslt/instructions/analyzeString.js +137 -0
  211. package/src/xslt/instructions/assert.js +72 -0
  212. package/src/xslt/instructions/attributeSets.js +115 -0
  213. package/src/xslt/instructions/conditional.js +99 -0
  214. package/src/xslt/instructions/control.js +123 -0
  215. package/src/xslt/instructions/copy.js +151 -0
  216. package/src/xslt/instructions/elements.js +161 -0
  217. package/src/xslt/instructions/evaluate.js +189 -0
  218. package/src/xslt/instructions/fork.js +35 -0
  219. package/src/xslt/instructions/grouping.js +106 -0
  220. package/src/xslt/instructions/index.js +94 -0
  221. package/src/xslt/instructions/iterate.js +182 -0
  222. package/src/xslt/instructions/literal.js +142 -0
  223. package/src/xslt/instructions/maps.js +72 -0
  224. package/src/xslt/instructions/merge.js +190 -0
  225. package/src/xslt/instructions/number.js +205 -0
  226. package/src/xslt/instructions/output.js +102 -0
  227. package/src/xslt/instructions/params.js +110 -0
  228. package/src/xslt/instructions/resultDocument.js +143 -0
  229. package/src/xslt/instructions/sort.js +114 -0
  230. package/src/xslt/instructions/sourceDocument.js +73 -0
  231. package/src/xslt/instructions/templates.js +153 -0
  232. package/src/xslt/instructions/text.js +151 -0
  233. package/src/xslt/instructions/try.js +186 -0
  234. package/src/xslt/instructions/variables.js +119 -0
  235. package/src/xslt/names.js +204 -0
  236. package/src/xslt/patterns/axisOrigins.js +121 -0
  237. package/src/xslt/patterns/compile.js +151 -0
  238. package/src/xslt/patterns/general.js +132 -0
  239. package/src/xslt/patterns/grammar.js +190 -0
  240. package/src/xslt/patterns/modes.js +153 -0
  241. package/src/xslt/patterns/paths.js +130 -0
  242. package/src/xslt/patterns/patternChecks.js +29 -0
  243. package/src/xslt/patterns/steps.js +162 -0
  244. package/src/xslt/processor.js +178 -0
  245. package/src/xslt/runtime/accumulatorFunctions.js +48 -0
  246. package/src/xslt/runtime/accumulators.js +174 -0
  247. package/src/xslt/runtime/apply.js +153 -0
  248. package/src/xslt/runtime/atomicKeys.js +36 -0
  249. package/src/xslt/runtime/availability.js +162 -0
  250. package/src/xslt/runtime/baseUri.js +64 -0
  251. package/src/xslt/runtime/compilerHook.js +27 -0
  252. package/src/xslt/runtime/conditional.js +110 -0
  253. package/src/xslt/runtime/context.js +106 -0
  254. package/src/xslt/runtime/copy.js +123 -0
  255. package/src/xslt/runtime/copyFunctions.js +71 -0
  256. package/src/xslt/runtime/copyOrigins.js +60 -0
  257. package/src/xslt/runtime/defaultMethod.js +42 -0
  258. package/src/xslt/runtime/documentContent.js +93 -0
  259. package/src/xslt/runtime/dynamicXPath.js +90 -0
  260. package/src/xslt/runtime/functionCache.js +65 -0
  261. package/src/xslt/runtime/functionCall.js +57 -0
  262. package/src/xslt/runtime/functions.js +191 -0
  263. package/src/xslt/runtime/groups.js +108 -0
  264. package/src/xslt/runtime/invocation.js +156 -0
  265. package/src/xslt/runtime/iteration.js +107 -0
  266. package/src/xslt/runtime/keys.js +141 -0
  267. package/src/xslt/runtime/lazy.js +97 -0
  268. package/src/xslt/runtime/locations.js +86 -0
  269. package/src/xslt/runtime/machine.js +137 -0
  270. package/src/xslt/runtime/merging.js +167 -0
  271. package/src/xslt/runtime/namespaceScope.js +141 -0
  272. package/src/xslt/runtime/numberFormat.js +89 -0
  273. package/src/xslt/runtime/numbering.js +168 -0
  274. package/src/xslt/runtime/params.js +59 -0
  275. package/src/xslt/runtime/properties.js +103 -0
  276. package/src/xslt/runtime/rawResults.js +82 -0
  277. package/src/xslt/runtime/sequenceReceiver.js +102 -0
  278. package/src/xslt/runtime/snapshot.js +66 -0
  279. package/src/xslt/runtime/sortKeys.js +119 -0
  280. package/src/xslt/runtime/streamAvailable.js +60 -0
  281. package/src/xslt/runtime/strip.js +102 -0
  282. package/src/xslt/runtime/transformFunction.js +169 -0
  283. package/src/xslt/runtime/transformation.js +168 -0
  284. package/src/xslt/runtime/transformationContext.js +85 -0
  285. package/src/xslt/runtime/treeReceiver.js +194 -0
  286. package/src/xslt/runtime/values.js +92 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * fn:format-number (F&O 3.1 section 4.7): the formatting phase of
3
+ * section 4.7.5 over the analysis of numberPicture.js, with decimal
4
+ * formats from decimalFormat.js. Numbers are formatted exactly: doubles
5
+ * and floats through the shortest decimal that reads back as them.
6
+ *
7
+ * @module @tradik/xslt3/functions/format/formatNumber
8
+ */
9
+
10
+ import { Decimal } from "../../xdm/decimal.js";
11
+ import { numberToDecimal } from "../../xdm/numeric.js";
12
+ import { types } from "../../xdm/types.js";
13
+ import { roundDecimal } from "../numeric.js";
14
+ import { define, stringItem } from "../support.js";
15
+ import { getDecimalFormat } from "./decimalFormat.js";
16
+ import { analyzePicture } from "./numberPicture.js";
17
+
18
+ /**
19
+ * Splits a decimal into mantissa and exponent with
20
+ * 10^(scaling-1) <= |mantissa| < 10^scaling.
21
+ * @param {Decimal} value - Non-negative
22
+ * @param {number} scaling
23
+ * @returns {{mantissa: Decimal, exponent: number}}
24
+ */
25
+ function scale(value, scaling) {
26
+ if (value.sign() === 0) return { mantissa: value, exponent: 0 };
27
+ const length = value.unscaled.toString().length;
28
+ const exponent = length - value.scale - scaling;
29
+ return {
30
+ mantissa: Decimal.of(value.unscaled, value.scale + exponent),
31
+ exponent,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Inserts grouping separators into the digits of the integer part.
37
+ * @param {string} digits
38
+ * @param {import("./numberPicture.js").SubPicture} sub
39
+ * @param {string} separator
40
+ * @returns {string}
41
+ */
42
+ function groupInteger(digits, sub, separator) {
43
+ let result = "";
44
+ for (let i = digits.length - 1, n = 0; i >= 0; i--, n++) {
45
+ const grouped = sub.grouping
46
+ ? n > 0 && n % sub.grouping === 0
47
+ : sub.integerGroups.includes(n) && n > 0;
48
+ result = digits[i] + (grouped ? separator : "") + result;
49
+ }
50
+ return result;
51
+ }
52
+
53
+ /**
54
+ * Formats the absolute value of a finite number with a sub-picture.
55
+ * @param {Decimal} value - Non-negative, already multiplied
56
+ * @param {import("./numberPicture.js").SubPicture} sub
57
+ * @param {import("./decimalFormat.js").DecimalFormat} format
58
+ * @returns {string}
59
+ */
60
+ function formatAbsolute(value, sub, format) {
61
+ const { mantissa: unrounded, exponent } = sub.minExponent
62
+ ? scale(value, sub.scaling)
63
+ : { mantissa: value, exponent: 0 };
64
+ const mantissa = roundDecimal(unrounded, sub.maxFraction, true);
65
+ const [whole, fraction = ""] = mantissa.toString().split(".");
66
+ const integer = (whole === "0" ? "" : whole).padStart(sub.minInteger, "0");
67
+ let fractionDigits = fraction.padEnd(sub.minFraction, "0");
68
+ for (const position of [...sub.fractionGroups].reverse()) {
69
+ if (position < fractionDigits.length) {
70
+ fractionDigits =
71
+ fractionDigits.slice(0, position) +
72
+ format.groupingSeparator +
73
+ fractionDigits.slice(position);
74
+ }
75
+ }
76
+ let text = groupInteger(integer, sub, format.groupingSeparator);
77
+ // the separator also stays without one in the picture when the
78
+ // adjustments of 4.7.4 asked for fraction digits ("#e0" gives "0.2e0")
79
+ if (fractionDigits !== "") {
80
+ text += format.decimalSeparator + fractionDigits;
81
+ }
82
+ if (sub.minExponent) {
83
+ text +=
84
+ format.exponentSeparator +
85
+ (exponent < 0 ? format.minusSign : "") +
86
+ String(Math.abs(exponent)).padStart(sub.minExponent, "0");
87
+ }
88
+ const zero = format.zeroDigit.codePointAt(0);
89
+ return text.replace(/[0-9]/g, (d) => String.fromCodePoint(zero + Number(d)));
90
+ }
91
+
92
+ /**
93
+ * fn:format-number on an atomic numeric value.
94
+ * @param {import("../../xdm/atomic.js").AtomicValue|undefined} item -
95
+ * Undefined for the empty sequence (formatted as NaN)
96
+ * @param {string} picture
97
+ * @param {import("./decimalFormat.js").DecimalFormat} format
98
+ * @returns {string}
99
+ */
100
+ export function formatNumber(item, picture, format) {
101
+ const { positive, negative } = analyzePicture(picture, format);
102
+ const value = item?.value ?? NaN;
103
+ if (typeof value === "number" && Number.isNaN(value)) return format.nan;
104
+ const isNegative =
105
+ typeof value === "number"
106
+ ? value < 0 || Object.is(value, -0)
107
+ : typeof value === "bigint"
108
+ ? value < 0n
109
+ : value.sign() < 0;
110
+ const sub = isNegative ? negative : positive;
111
+ return sub.prefix + adjusted(item, sub, format) + sub.suffix;
112
+ }
113
+
114
+ /**
115
+ * The absolute value multiplied for percent or per-mille in the value's
116
+ * own type (doubles may overflow to infinity), formatted.
117
+ * @param {import("../../xdm/atomic.js").AtomicValue} item - Not NaN
118
+ * @param {import("./numberPicture.js").SubPicture} sub
119
+ * @param {import("./decimalFormat.js").DecimalFormat} format
120
+ * @returns {string}
121
+ */
122
+ function adjusted(item, sub, format) {
123
+ const { value } = item;
124
+ let exact;
125
+ if (typeof value === "number") {
126
+ const isFloat = item.type.primitive === types.float;
127
+ const product = isFloat
128
+ ? Math.fround(value * sub.multiplier)
129
+ : value * sub.multiplier;
130
+ if (!Number.isFinite(product)) return format.infinity;
131
+ exact = numberToDecimal(Math.abs(product), isFloat);
132
+ } else {
133
+ exact = (value instanceof Decimal ? value : Decimal.of(value)).mul(
134
+ Decimal.of(BigInt(sub.multiplier)),
135
+ );
136
+ if (exact.sign() < 0) exact = exact.neg();
137
+ }
138
+ return formatAbsolute(exact, sub, format);
139
+ }
140
+
141
+ /**
142
+ * @param {Array<*>[]} args - [$value, $picture, $decimal-format-name?]
143
+ * @param {object} context
144
+ * @returns {Array<*>}
145
+ */
146
+ const impl = ([value, [picture], name], context) => [
147
+ stringItem(
148
+ formatNumber(
149
+ value[0],
150
+ picture.value,
151
+ getDecimalFormat(name?.[0]?.value, context),
152
+ ),
153
+ ),
154
+ ];
155
+
156
+ /** @type {import("../support.js").FunctionDefinition[]} */
157
+ export const formatNumberFunctions = [
158
+ define("format-number", ["xs:numeric?", "xs:string"], "xs:string", impl),
159
+ define(
160
+ "format-number",
161
+ ["xs:numeric?", "xs:string", "xs:string?"],
162
+ "xs:string",
163
+ impl,
164
+ ),
165
+ ];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The fractional seconds component [f] of the format-dateTime family
3
+ * (F&O 3.1 section 9.8.4.5): the digit pattern is applied to the reversed
4
+ * fraction digits, so grouping separators count from the decimal point,
5
+ * and excess digits are truncated, not rounded.
6
+ *
7
+ * @module @tradik/xslt3/functions/format/fractionFormat
8
+ */
9
+
10
+ import { formatDigits, parseDigitPattern, zeroOf } from "./digitPattern.js";
11
+
12
+ const isDigit = (c) => /^\p{Nd}$/u.test(c);
13
+ const reverse = (s) => Array.from(s).reverse().join("");
14
+
15
+ /**
16
+ * Applies the width modifier to a fractional seconds picture.
17
+ * @param {string[]} chars - Characters of the picture
18
+ * @param {number|null} minWidth
19
+ * @param {number|null} maxWidth
20
+ * @returns {string[]}
21
+ */
22
+ function widen(chars, minWidth, maxWidth) {
23
+ const zero = String.fromCodePoint(zeroOf(chars.find(isDigit)));
24
+ const result = [...chars];
25
+ let mandatory = result.filter(isDigit).length;
26
+ for (let i = 0; i < result.length && mandatory < (minWidth ?? 0); i++) {
27
+ if (result[i] === "#") {
28
+ result[i] = zero;
29
+ mandatory++;
30
+ }
31
+ }
32
+ while (mandatory < (minWidth ?? 0)) {
33
+ result.push(zero);
34
+ mandatory++;
35
+ }
36
+ if (maxWidth !== null) {
37
+ let signs = result.filter((c) => c === "#" || isDigit(c)).length;
38
+ for (; signs < maxWidth; signs++) result.push("#");
39
+ }
40
+ return result;
41
+ }
42
+
43
+ /**
44
+ * Formats fractional seconds.
45
+ * @param {import("../../xdm/decimal.js").Decimal} second - Seconds value
46
+ * @param {import("./datePicture.js").Marker} marker - Component f
47
+ * @returns {string}
48
+ */
49
+ export function formatFraction(second, marker) {
50
+ const { minWidth, maxWidth } = marker;
51
+ const token = Array.from(marker.first ?? "").some(isDigit)
52
+ ? marker.first
53
+ : "1";
54
+ const digits = (second.toString().split(".")[1] ?? "").replace(/0+$/, "");
55
+ const chars = Array.from(token);
56
+ if (maxWidth === null && chars.length === 1) {
57
+ // a single digit keeps all the digits of the value
58
+ const zero = zeroOf(token);
59
+ return (digits || "0")
60
+ .padEnd(minWidth ?? 1, "0")
61
+ .replace(/[0-9]/g, (d) => String.fromCodePoint(zero + Number(d)));
62
+ }
63
+ const pattern = parseDigitPattern(
64
+ reverse(widen(chars, minWidth, maxWidth).join("")),
65
+ );
66
+ const out = Array.from(
67
+ reverse(formatDigits(BigInt(reverse(digits) || "0"), pattern)),
68
+ );
69
+ const zero = String.fromCodePoint(pattern.zero);
70
+ let count = out.filter(isDigit).length;
71
+ while (
72
+ count > pattern.digits ||
73
+ (count > pattern.mandatory && out.at(-1) === zero)
74
+ ) {
75
+ out.pop();
76
+ count--;
77
+ while (!isDigit(out.at(-1))) out.pop();
78
+ }
79
+ return out.join("");
80
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * German numbers in words for format-integer and xsl:number with
3
+ * language "de" (F&O 3.1 section 4.6.1): cardinals ("einundzwanzig",
4
+ * "zweihunderteins", "eine Million") and ordinals with an ending
5
+ * ("dritte", "zehnter", "zwanzigsten"); digits take "." as ordinal mark.
6
+ *
7
+ * @module @tradik/xslt3/functions/format/germanWords
8
+ */
9
+
10
+ const ONES = (
11
+ "null eins zwei drei vier fünf sechs sieben acht neun zehn elf zwölf " +
12
+ "dreizehn vierzehn fünfzehn sechzehn siebzehn achtzehn neunzehn"
13
+ ).split(" ");
14
+ const TENS =
15
+ " zwanzig dreißig vierzig fünfzig sechzig siebzig achtzig neunzig".split(
16
+ " ",
17
+ );
18
+ const SCALES = [
19
+ [10n ** 18n, "Trillion", "Trillionen"],
20
+ [10n ** 15n, "Billiarde", "Billiarden"],
21
+ [10n ** 12n, "Billion", "Billionen"],
22
+ [10n ** 9n, "Milliarde", "Milliarden"],
23
+ [10n ** 6n, "Million", "Millionen"],
24
+ ];
25
+
26
+ /**
27
+ * A number below one million as the first part of a compound: "ein"
28
+ * rather than "eins" at the end ("einundzwanzigtausend").
29
+ * @param {bigint} n - 1 to 999,999
30
+ * @returns {string}
31
+ */
32
+ const prefixWords = (n) => cardinalBelowMillion(n).replace(/eins$/, "ein");
33
+
34
+ /**
35
+ * German cardinal words below one million.
36
+ * @param {bigint} n - Non-negative
37
+ * @returns {string}
38
+ */
39
+ function cardinalBelowMillion(n) {
40
+ if (n < 20n) return ONES[Number(n)];
41
+ if (n < 100n) {
42
+ const units = n % 10n;
43
+ const tens = TENS[Number(n / 10n)];
44
+ return units ? `${prefixWords(units)}und${tens}` : tens;
45
+ }
46
+ if (n < 1000n) {
47
+ const rest = n % 100n;
48
+ return `${prefixWords(n / 100n)}hundert${rest ? cardinalBelowMillion(rest) : ""}`;
49
+ }
50
+ const rest = n % 1000n;
51
+ return `${prefixWords(n / 1000n)}tausend${rest ? cardinalBelowMillion(rest) : ""}`;
52
+ }
53
+
54
+ /**
55
+ * German cardinal words, e.g. "einhundertvierunddreißig".
56
+ * @param {bigint} n - Non-negative
57
+ * @returns {string}
58
+ */
59
+ export function germanCardinal(n) {
60
+ const scale = SCALES.find(([size]) => n >= size);
61
+ if (!scale) return cardinalBelowMillion(n);
62
+ const [size, one, many] = scale;
63
+ const count = n / size;
64
+ const rest = n % size;
65
+ const head =
66
+ count === 1n ? `eine ${one}` : `${germanCardinal(count)} ${many}`;
67
+ return rest ? `${head} ${germanCardinal(rest)}` : head;
68
+ }
69
+
70
+ /** Ordinal stems of the numbers whose ordinal is irregular. */
71
+ const IRREGULAR = { 1: "erst", 3: "dritt", 7: "siebt", 8: "acht" };
72
+
73
+ /**
74
+ * German ordinal words, e.g. "dritte", "zehnter", "zwanzigsten".
75
+ * @param {bigint} n - Non-negative
76
+ * @param {string} [form] - "-er", "-es"...: the ending (default "e");
77
+ * other forms (such as "%spellout-ordinal") take the default
78
+ * @returns {string}
79
+ */
80
+ export function germanOrdinal(n, form = "") {
81
+ const ending = form.startsWith("-") ? form.slice(1) : "e";
82
+ const last = n % 100n;
83
+ if (last === 0n || last >= 20n || n >= 10n ** 6n) {
84
+ return `${germanCardinal(n)}st${ending}`;
85
+ }
86
+ const head = n === last ? "" : germanCardinal(n - last);
87
+ const stem = IRREGULAR[Number(last)] ?? `${ONES[Number(last)]}t`;
88
+ return `${head}${stem}${ending}`;
89
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The languages numbers are written in by format-integer, the format-date
3
+ * family and xsl:number: words (cardinal and ordinal) and the ordinal mark
4
+ * of digits. A language is chosen by the primary subtag of its tag
5
+ * ("de-AT" is "de"); others are English. Add a language by adding an
6
+ * entry to {@link NUMBER_LANGUAGES}.
7
+ *
8
+ * @module @tradik/xslt3/functions/format/numberLanguages
9
+ */
10
+
11
+ import { germanCardinal, germanOrdinal } from "./germanWords.js";
12
+ import { cardinalWords, ordinalSuffix, ordinalWords } from "./numberWords.js";
13
+
14
+ /**
15
+ * How a language writes numbers.
16
+ * @typedef {object} NumberLanguage
17
+ * @property {(n: bigint) => string} cardinal - Lower-case words
18
+ * @property {(n: bigint, form: string) => string} ordinal - Lower-case
19
+ * words; `form` is the text in parentheses of the "o" modifier, or the
20
+ * ordinal attribute of xsl:number ("-er", "%spellout-ordinal"...)
21
+ * @property {(n: bigint) => string} suffix - Mark after ordinal digits
22
+ */
23
+
24
+ /** @type {Readonly<Record<string, NumberLanguage>>} */
25
+ export const NUMBER_LANGUAGES = Object.freeze({
26
+ en: { cardinal: cardinalWords, ordinal: ordinalWords, suffix: ordinalSuffix },
27
+ de: { cardinal: germanCardinal, ordinal: germanOrdinal, suffix: () => "." },
28
+ });
29
+
30
+ /**
31
+ * The number language of a language tag.
32
+ * @param {string|null} [tag] - e.g. "de", "en-GB"; English when absent
33
+ * or not supported
34
+ * @returns {NumberLanguage}
35
+ */
36
+ export function numberLanguage(tag) {
37
+ const primary = (tag ?? "").trim().toLowerCase().split("-")[0];
38
+ return Object.hasOwn(NUMBER_LANGUAGES, primary)
39
+ ? NUMBER_LANGUAGES[primary]
40
+ : NUMBER_LANGUAGES.en;
41
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Analysis of fn:format-number picture strings (F&O 3.1 sections 4.7.3
3
+ * and 4.7.4): syntax checks (FODF1310) and the variables of each
4
+ * sub-picture.
5
+ *
6
+ * @module @tradik/xslt3/functions/format/numberPicture
7
+ */
8
+
9
+ import { pictureError, regularGrouping } from "./digitPattern.js";
10
+
11
+ /**
12
+ * Variables of one sub-picture.
13
+ * @typedef {object} SubPicture
14
+ * @property {string} prefix
15
+ * @property {string} suffix
16
+ * @property {number[]} integerGroups - Grouping positions, from the
17
+ * decimal separator leftwards
18
+ * @property {number} grouping - Regular grouping size, 0 when irregular
19
+ * @property {number[]} fractionGroups
20
+ * @property {number} minInteger
21
+ * @property {number} scaling
22
+ * @property {number} minFraction
23
+ * @property {number} maxFraction
24
+ * @property {number} minExponent - 0 without exponent
25
+ * @property {boolean} hasDecimal - Whether the decimal separator occurs
26
+ * @property {number} multiplier - 1, 100 (percent) or 1000 (per-mille)
27
+ */
28
+
29
+ /**
30
+ * Classifies the characters of a sub-picture.
31
+ * @param {string[]} chars
32
+ * @param {import("./decimalFormat.js").DecimalFormat} format
33
+ * @returns {string[]} "digit", "optional", "decimal", "grouping",
34
+ * "exponent" (candidate) or "passive" per character
35
+ */
36
+ function classify(chars, format) {
37
+ const zero = format.zeroDigit.codePointAt(0);
38
+ return chars.map((c) => {
39
+ const cp = c.codePointAt(0);
40
+ if (cp >= zero && cp <= zero + 9) return "digit";
41
+ if (c === format.digit) return "optional";
42
+ if (c === format.decimalSeparator) return "decimal";
43
+ if (c === format.groupingSeparator) return "grouping";
44
+ if (c === format.exponentSeparator) return "exponent";
45
+ return "passive";
46
+ });
47
+ }
48
+
49
+ const count = (kinds, ...wanted) =>
50
+ kinds.filter((k) => wanted.includes(k)).length;
51
+
52
+ /**
53
+ * Grouping positions of a part, counted from its decimal-separator side.
54
+ * @param {string[]} kinds - The part, ordered away from the separator
55
+ * @returns {number[]}
56
+ */
57
+ function groupPositions(kinds) {
58
+ const positions = [];
59
+ let digits = 0;
60
+ for (const kind of kinds) {
61
+ if (kind === "grouping") positions.push(digits);
62
+ else digits++;
63
+ }
64
+ return positions;
65
+ }
66
+
67
+ /**
68
+ * Checks the syntax rules on the mantissa part.
69
+ * @param {string[]} integer - Kinds of the integer part
70
+ * @param {string[]} fraction - Kinds of the fractional part
71
+ * @param {boolean} hasDecimal
72
+ */
73
+ function checkMantissa(integer, fraction, hasDecimal) {
74
+ const mantissa = [...integer, ...fraction];
75
+ if (count(mantissa, "digit", "optional") === 0) pictureError("no digit");
76
+ if (
77
+ integer.at(-1) === "grouping" ||
78
+ (hasDecimal && fraction[0] === "grouping") ||
79
+ mantissa.some((k, i) => k === "grouping" && mantissa[i + 1] === "grouping")
80
+ ) {
81
+ pictureError("misplaced grouping separator");
82
+ }
83
+ const firstDigit = integer.indexOf("digit");
84
+ if (firstDigit >= 0 && integer.indexOf("optional", firstDigit) >= 0) {
85
+ pictureError("optional digit after a mandatory digit");
86
+ }
87
+ const firstOptional = fraction.indexOf("optional");
88
+ if (firstOptional >= 0 && fraction.indexOf("digit", firstOptional) >= 0) {
89
+ pictureError("mandatory digit after an optional digit");
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Analyzes one sub-picture.
95
+ * @param {string} picture
96
+ * @param {import("./decimalFormat.js").DecimalFormat} format
97
+ * @returns {SubPicture}
98
+ */
99
+ export function analyzeSubPicture(picture, format) {
100
+ const chars = Array.from(picture);
101
+ const kinds = classify(chars, format);
102
+ const isActive = (k) => k !== "passive" && k !== "exponent";
103
+ const first = kinds.findIndex(isActive);
104
+ const last = kinds.findLastIndex(isActive);
105
+ const exponents = kinds
106
+ .map((k, i) => (k === "exponent" && i > first && i < last ? i : -1))
107
+ .filter((i) => i >= 0);
108
+ const marks = chars.filter(
109
+ (c) => c === format.percent || c === format.perMille,
110
+ );
111
+ if (first < 0) pictureError("no digit");
112
+ if (count(kinds, "decimal") > 1) pictureError("several decimal separators");
113
+ if (
114
+ marks.length > 1 ||
115
+ exponents.length > 1 ||
116
+ (marks.length && exponents.length)
117
+ ) {
118
+ pictureError("several percent, per-mille or exponent signs");
119
+ }
120
+ const exponent = exponents[0] ?? -1;
121
+ kinds.forEach((k, i) => {
122
+ if (!isActive(k) && i > first && i < last && i !== exponent) {
123
+ pictureError("passive character between active characters");
124
+ }
125
+ });
126
+ const mantissaEnd = exponent < 0 ? last + 1 : exponent;
127
+ const mantissa = kinds.slice(first, mantissaEnd);
128
+ const exponentPart = exponent < 0 ? [] : kinds.slice(exponent + 1, last + 1);
129
+ if (exponentPart.some((k) => k !== "digit")) {
130
+ pictureError("non-digit in the exponent");
131
+ }
132
+ const decimal = mantissa.indexOf("decimal");
133
+ const hasDecimal = decimal >= 0;
134
+ const integer = hasDecimal ? mantissa.slice(0, decimal) : mantissa;
135
+ const fraction = hasDecimal ? mantissa.slice(decimal + 1) : [];
136
+ checkMantissa(integer, fraction, hasDecimal);
137
+ return variables({
138
+ integer,
139
+ fraction,
140
+ hasDecimal,
141
+ minExponent: exponentPart.length,
142
+ prefix: chars.slice(0, first).join(""),
143
+ suffix: chars.slice(last + 1).join(""),
144
+ multiplier: marks[0] === format.percent ? 100 : marks.length ? 1000 : 1,
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Computes the variables of section 4.7.4 from the parts.
150
+ * @param {object} parts
151
+ * @returns {SubPicture}
152
+ */
153
+ function variables({ integer, fraction, hasDecimal, minExponent, ...rest }) {
154
+ const integerGroups = groupPositions([...integer].reverse());
155
+ const integerDigits = count(integer, "digit", "optional");
156
+ const separators = new Map(integerGroups.map((p) => [p, ","]));
157
+ let minInteger = count(integer, "digit");
158
+ let minFraction = count(fraction, "digit");
159
+ let maxFraction = count(fraction, "digit", "optional");
160
+ if (minInteger === 0 && maxFraction === 0) {
161
+ if (minExponent > 0) {
162
+ minFraction = 1;
163
+ maxFraction = 1;
164
+ } else {
165
+ minInteger = 1;
166
+ }
167
+ }
168
+ if (minExponent > 0 && minInteger === 0 && integer.includes("optional")) {
169
+ minInteger = 1;
170
+ }
171
+ if (minInteger === 0 && minFraction === 0) minFraction = 1;
172
+ return {
173
+ ...rest,
174
+ integerGroups,
175
+ grouping: regularGrouping(separators, integerDigits),
176
+ fractionGroups: groupPositions(fraction),
177
+ minInteger,
178
+ scaling: count(integer, "digit"),
179
+ minFraction,
180
+ maxFraction,
181
+ minExponent,
182
+ hasDecimal,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Analyzes a picture string into its positive and negative sub-pictures.
188
+ * @param {string} picture
189
+ * @param {import("./decimalFormat.js").DecimalFormat} format
190
+ * @returns {{positive: SubPicture, negative: SubPicture}}
191
+ * @throws {XPathError} FODF1310 for an invalid picture
192
+ */
193
+ export function analyzePicture(picture, format) {
194
+ const parts = picture.split(format.patternSeparator);
195
+ if (parts.length > 2) pictureError("several pattern separators");
196
+ const positive = analyzeSubPicture(parts[0], format);
197
+ const negative =
198
+ parts.length === 2
199
+ ? analyzeSubPicture(parts[1], format)
200
+ : { ...positive, prefix: format.minusSign + positive.prefix };
201
+ return { positive, negative };
202
+ }