@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,206 @@
1
+ /**
2
+ * The date and time types (xs:dateTime, xs:date, xs:time and the five
3
+ * Gregorian "g" types): lexical parsing per XSD 1.1 Part 2, canonical
4
+ * strings per F&O 3.1 19.1.2, and the position on the time line used for
5
+ * comparison and arithmetic.
6
+ *
7
+ * A value is a frozen {@link DateTimeValue} whose absent components are
8
+ * null; `second` is a {@link Decimal}; `timezone` is an offset in minutes
9
+ * or null. Years follow XSD 1.1: year 0000 exists, negative years are
10
+ * astronomical, and more than four digits are allowed up to
11
+ * {@link MAX_YEAR}.
12
+ *
13
+ * @module @tradik/xslt3/xdm/datetime
14
+ */
15
+
16
+ import { XPathError } from "../errors.js";
17
+ import { civilFromDays, daysFromCivil, daysInMonth } from "./calendar.js";
18
+ import { Decimal } from "./decimal.js";
19
+
20
+ /** Largest absolute year supported; beyond it FODT0001 is raised. */
21
+ export const MAX_YEAR = 999999999;
22
+
23
+ /** A date/time value; components that the type does not have are null. */
24
+ export class DateTimeValue {
25
+ /**
26
+ * @param {object} fields
27
+ * @param {number|null} [fields.year]
28
+ * @param {number|null} [fields.month]
29
+ * @param {number|null} [fields.day]
30
+ * @param {number|null} [fields.hour]
31
+ * @param {number|null} [fields.minute]
32
+ * @param {Decimal|null} [fields.second]
33
+ * @param {number|null} [fields.timezone] - Offset in minutes
34
+ */
35
+ constructor({ year, month, day, hour, minute, second, timezone }) {
36
+ this.year = year ?? null;
37
+ this.month = month ?? null;
38
+ this.day = day ?? null;
39
+ this.hour = hour ?? null;
40
+ this.minute = minute ?? null;
41
+ this.second = second ?? null;
42
+ this.timezone = timezone ?? null;
43
+ Object.freeze(this);
44
+ }
45
+ }
46
+
47
+ const Y = "(-?(?:[1-9]\\d{3,}|0\\d{3}))";
48
+ const T = "(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d+)?)";
49
+ const Z = "(Z|[+-]\\d{2}:\\d{2})?";
50
+ const time = ["hour", "minute", "second"];
51
+ /** Lexical pattern and captured component names per primitive type. */
52
+ const formats = {
53
+ dateTime: [`${Y}-(\\d{2})-(\\d{2})T${T}`, ["year", "month", "day", ...time]],
54
+ date: [`${Y}-(\\d{2})-(\\d{2})`, ["year", "month", "day"]],
55
+ time: [T, time],
56
+ gYearMonth: [`${Y}-(\\d{2})`, ["year", "month"]],
57
+ gYear: [Y, ["year"]],
58
+ gMonthDay: ["--(\\d{2})-(\\d{2})", ["month", "day"]],
59
+ gDay: ["---(\\d{2})", ["day"]],
60
+ gMonth: ["--(\\d{2})", ["month"]],
61
+ };
62
+ const patterns = Object.fromEntries(
63
+ Object.entries(formats).map(([kind, [body]]) => [
64
+ kind,
65
+ new RegExp(`^${body}${Z}$`),
66
+ ]),
67
+ );
68
+
69
+ /**
70
+ * Names of the components a date/time type has, in lexical order.
71
+ * @param {string} kind - Primitive local name, e.g. "gYearMonth"
72
+ * @returns {string[]} e.g. ["year", "month"]
73
+ */
74
+ export function componentsOf(kind) {
75
+ return formats[kind][1];
76
+ }
77
+
78
+ /**
79
+ * @param {number} year
80
+ * @returns {number} the year
81
+ * @throws {XPathError} FODT0001 when outside ±MAX_YEAR
82
+ */
83
+ export function checkYear(year) {
84
+ if (Math.abs(year) > MAX_YEAR) {
85
+ throw new XPathError(
86
+ "FODT0001",
87
+ `Year ${year} is out of the supported range`,
88
+ );
89
+ }
90
+ return year;
91
+ }
92
+
93
+ /**
94
+ * Parses a timezone lexical form.
95
+ * @param {string|undefined} text - "Z", "+hh:mm", "-hh:mm" or undefined
96
+ * @returns {number|null|undefined} minutes, null when absent, undefined when invalid
97
+ */
98
+ function parseTimezone(text) {
99
+ if (text === undefined) return null;
100
+ if (text === "Z") return 0;
101
+ const hours = Number(text.slice(1, 3));
102
+ const minutes = Number(text.slice(4));
103
+ if (minutes > 59 || hours > 14 || (hours === 14 && minutes > 0)) {
104
+ return undefined;
105
+ }
106
+ return (text[0] === "-" ? -1 : 1) * (hours * 60 + minutes);
107
+ }
108
+
109
+ /**
110
+ * Whether the parsed components are in range.
111
+ * @param {object} f - Components
112
+ * @returns {boolean}
113
+ */
114
+ function isValid(f) {
115
+ if (f.month !== undefined && (f.month < 1 || f.month > 12)) return false;
116
+ if (f.day !== undefined) {
117
+ if (f.day < 1 || f.day > daysInMonth(f.year ?? 2000, f.month ?? 1)) {
118
+ return false;
119
+ }
120
+ }
121
+ if (f.hour === undefined) return true;
122
+ if (f.minute > 59 || f.second.compare(Decimal.of(60n)) >= 0) return false;
123
+ return (
124
+ f.hour < 24 || (f.hour === 24 && f.minute === 0 && f.second.sign() === 0)
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Parses the lexical form of a date/time type (whitespace already collapsed).
130
+ * @param {string} kind - Primitive local name, e.g. "dateTime", "gMonthDay"
131
+ * @param {string} text
132
+ * @returns {DateTimeValue|null} null when the text is not valid
133
+ * @throws {XPathError} FODT0001 when the year is out of range
134
+ */
135
+ export function parseDateTime(kind, text) {
136
+ const match = patterns[kind].exec(text);
137
+ if (!match) return null;
138
+ const names = componentsOf(kind);
139
+ const fields = {};
140
+ names.forEach((name, i) => {
141
+ const part = match[i + 1];
142
+ // "|| 0" turns the year "-0000" into 0
143
+ fields[name] = name === "second" ? Decimal.parse(part) : Number(part) || 0;
144
+ });
145
+ if (fields.year !== undefined) checkYear(fields.year);
146
+ fields.timezone = parseTimezone(match[names.length + 1]);
147
+ if (fields.timezone === undefined || !isValid(fields)) return null;
148
+ if (fields.hour === 24) {
149
+ fields.hour = 0;
150
+ if (kind === "dateTime") {
151
+ const [year, month, day] = civilFromDays(
152
+ daysFromCivil(fields.year, fields.month, fields.day) + 1,
153
+ );
154
+ Object.assign(fields, { year: checkYear(year), month, day });
155
+ }
156
+ }
157
+ return new DateTimeValue(fields);
158
+ }
159
+
160
+ const pad2 = (n) => String(n).padStart(2, "0");
161
+
162
+ /**
163
+ * @param {number|null} timezone - Minutes
164
+ * @returns {string} "", "Z" or "±hh:mm"
165
+ */
166
+ export function formatTimezone(timezone) {
167
+ if (timezone === null) return "";
168
+ if (timezone === 0) return "Z";
169
+ const abs = Math.abs(timezone);
170
+ return `${timezone < 0 ? "-" : "+"}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`;
171
+ }
172
+
173
+ /** Formatters of the components, by component name. */
174
+ const componentFormats = {
175
+ year: (v) =>
176
+ (v.year < 0 ? "-" : "") + String(Math.abs(v.year)).padStart(4, "0"),
177
+ month: (v) => pad2(v.month),
178
+ day: (v) => pad2(v.day),
179
+ hour: (v) => pad2(v.hour),
180
+ minute: (v) => pad2(v.minute),
181
+ second: (v) => v.second.toString().replace(/^(\d)(?!\d)/, "0$1"),
182
+ };
183
+ const separators = {
184
+ dateTime: ["", "-", "-", "T", ":", ":"],
185
+ date: ["", "-", "-"],
186
+ time: ["", ":", ":"],
187
+ gYearMonth: ["", "-"],
188
+ gYear: [""],
189
+ gMonthDay: ["--", "-"],
190
+ gDay: ["---"],
191
+ gMonth: ["--"],
192
+ };
193
+
194
+ /**
195
+ * Canonical string of a date/time value (the local value with its own
196
+ * timezone, F&O 3.1 19.1.2).
197
+ * @param {string} kind - Primitive local name
198
+ * @param {DateTimeValue} value
199
+ * @returns {string}
200
+ */
201
+ export function formatDateTime(kind, value) {
202
+ const parts = componentsOf(kind).map(
203
+ (name, i) => separators[kind][i] + componentFormats[name](value),
204
+ );
205
+ return parts.join("") + formatTimezone(value.timezone);
206
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Exact arbitrary-precision decimal numbers for xs:decimal.
3
+ *
4
+ * A value is `unscaled × 10^-scale` with a BigInt `unscaled` and a
5
+ * non-negative integer `scale`. Values are kept normalized (no trailing
6
+ * zero digits in the fraction), so equal numbers have equal fields.
7
+ * No floating point is involved anywhere.
8
+ *
9
+ * @module @tradik/xslt3/xdm/decimal
10
+ */
11
+
12
+ /**
13
+ * Number of fraction digits kept by {@link Decimal#div} when both operands
14
+ * have fewer: the precision of xs:decimal division (implementation-defined
15
+ * in F&O 3.1 section 4.2). Results are rounded half-down, like Saxon.
16
+ */
17
+ export const DIVISION_SCALE = 18;
18
+
19
+ const lexicalPattern = /^([+-]?)(\d*)(?:\.(\d*))?$/;
20
+
21
+ /** @param {number} n @returns {bigint} 10^n */
22
+ const pow10 = (n) => 10n ** BigInt(n);
23
+
24
+ /** An immutable exact decimal number. */
25
+ export class Decimal {
26
+ /**
27
+ * Use {@link Decimal.of} or {@link Decimal.parse}; the constructor does
28
+ * not normalize.
29
+ * @param {bigint} unscaled
30
+ * @param {number} scale
31
+ */
32
+ constructor(unscaled, scale) {
33
+ this.unscaled = unscaled;
34
+ this.scale = scale;
35
+ Object.freeze(this);
36
+ }
37
+
38
+ /**
39
+ * Builds a normalized decimal `unscaled × 10^-scale`.
40
+ * @param {bigint} unscaled
41
+ * @param {number} [scale=0] - May be negative (multiplies by 10^-scale)
42
+ * @returns {Decimal}
43
+ */
44
+ static of(unscaled, scale = 0) {
45
+ if (scale < 0) return new Decimal(unscaled * pow10(-scale), 0);
46
+ while (scale > 0 && unscaled % 10n === 0n) {
47
+ unscaled /= 10n;
48
+ scale--;
49
+ }
50
+ return new Decimal(
51
+ unscaled === 0n ? 0n : unscaled,
52
+ unscaled === 0n ? 0 : scale,
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Parses the xs:decimal lexical form `[+-]?(\d+(\.\d*)?|\.\d+)` (no
58
+ * whitespace).
59
+ * @param {string} text
60
+ * @returns {Decimal|null} null when the text is not a decimal literal
61
+ */
62
+ static parse(text) {
63
+ const match = lexicalPattern.exec(text);
64
+ if (!match) return null;
65
+ const [, sign, whole, fraction = ""] = match;
66
+ if (whole === "" && fraction === "") return null;
67
+ const unscaled = BigInt(`${whole}${fraction}`);
68
+ return Decimal.of(sign === "-" ? -unscaled : unscaled, fraction.length);
69
+ }
70
+
71
+ /** @returns {number} -1, 0 or 1 */
72
+ sign() {
73
+ return this.unscaled < 0n ? -1 : this.unscaled > 0n ? 1 : 0;
74
+ }
75
+
76
+ /** @returns {boolean} true when the value has no fraction part */
77
+ isInteger() {
78
+ return this.scale === 0;
79
+ }
80
+
81
+ /** @returns {Decimal} the negated value */
82
+ neg() {
83
+ return new Decimal(-this.unscaled, this.scale);
84
+ }
85
+
86
+ /**
87
+ * Both operands as BigInts at a common scale.
88
+ * @param {Decimal} other
89
+ * @returns {[bigint, bigint, number]} [this, other, scale]
90
+ */
91
+ align(other) {
92
+ const scale = Math.max(this.scale, other.scale);
93
+ return [
94
+ this.unscaled * pow10(scale - this.scale),
95
+ other.unscaled * pow10(scale - other.scale),
96
+ scale,
97
+ ];
98
+ }
99
+
100
+ /** @param {Decimal} other @returns {Decimal} this + other */
101
+ add(other) {
102
+ const [a, b, scale] = this.align(other);
103
+ return Decimal.of(a + b, scale);
104
+ }
105
+
106
+ /** @param {Decimal} other @returns {Decimal} this - other */
107
+ sub(other) {
108
+ return this.add(other.neg());
109
+ }
110
+
111
+ /** @param {Decimal} other @returns {Decimal} this × other */
112
+ mul(other) {
113
+ return Decimal.of(this.unscaled * other.unscaled, this.scale + other.scale);
114
+ }
115
+
116
+ /**
117
+ * Division to `max(DIVISION_SCALE, this.scale, other.scale)` fraction
118
+ * digits, rounded half-down (ties toward zero). The divisor must not be
119
+ * zero.
120
+ * @param {Decimal} other
121
+ * @returns {Decimal}
122
+ */
123
+ div(other) {
124
+ const scale = Math.max(DIVISION_SCALE, this.scale, other.scale);
125
+ // this / other × 10^scale = (u1 × 10^(scale + s2 - s1)) / u2
126
+ let numerator = this.unscaled;
127
+ const shift = scale + other.scale - this.scale;
128
+ numerator *= pow10(shift);
129
+ let denominator = other.unscaled;
130
+ if (denominator < 0n) {
131
+ numerator = -numerator;
132
+ denominator = -denominator;
133
+ }
134
+ let quotient = numerator / denominator;
135
+ const remainder2 = 2n * (numerator % denominator);
136
+ if (remainder2 > denominator) quotient++;
137
+ else if (-remainder2 > denominator) quotient--;
138
+ return Decimal.of(quotient, scale);
139
+ }
140
+
141
+ /**
142
+ * Integer division truncating toward zero; the divisor must not be zero.
143
+ * @param {Decimal} other
144
+ * @returns {bigint}
145
+ */
146
+ idiv(other) {
147
+ const [a, b] = this.align(other);
148
+ return a / b;
149
+ }
150
+
151
+ /**
152
+ * Remainder with the sign of the dividend (`a - b × (a idiv b)`).
153
+ * @param {Decimal} other - Non-zero divisor
154
+ * @returns {Decimal}
155
+ */
156
+ mod(other) {
157
+ const [a, b, scale] = this.align(other);
158
+ return Decimal.of(a % b, scale);
159
+ }
160
+
161
+ /** @param {Decimal} other @returns {number} -1, 0 or 1 */
162
+ compare(other) {
163
+ const [a, b] = this.align(other);
164
+ return a < b ? -1 : a > b ? 1 : 0;
165
+ }
166
+
167
+ /** @returns {bigint} the value with the fraction discarded */
168
+ trunc() {
169
+ return this.unscaled / pow10(this.scale);
170
+ }
171
+
172
+ /** @returns {bigint} the largest integer not above the value */
173
+ floor() {
174
+ const t = this.trunc();
175
+ return this.unscaled < 0n && this.scale > 0 ? t - 1n : t;
176
+ }
177
+
178
+ /** @returns {number} the nearest double (correctly rounded) */
179
+ toNumber() {
180
+ return Number(this.toString());
181
+ }
182
+
183
+ /**
184
+ * Canonical lexical form: no "+" sign, no leading zeros, no trailing
185
+ * fraction zeros, no decimal point for integers ("1", "-0.5", "0").
186
+ * @returns {string}
187
+ */
188
+ toString() {
189
+ const negative = this.unscaled < 0n;
190
+ const digits = (negative ? -this.unscaled : this.unscaled).toString();
191
+ const sign = negative ? "-" : "";
192
+ if (this.scale === 0) return sign + digits;
193
+ const padded = digits.padStart(this.scale + 1, "0");
194
+ const point = padded.length - this.scale;
195
+ return `${sign}${padded.slice(0, point)}.${padded.slice(point)}`;
196
+ }
197
+ }
198
+
199
+ /** The decimal zero. */
200
+ Decimal.ZERO = Decimal.of(0n);
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The duration types (xs:duration, xs:yearMonthDuration,
3
+ * xs:dayTimeDuration): lexical parsing per XSD 1.1 Part 2 and canonical
4
+ * strings per F&O 3.1 19.1.2.
5
+ *
6
+ * A value is a frozen {@link DurationValue} `{ months, seconds }` with an
7
+ * integer number of months and a {@link Decimal} number of seconds of the
8
+ * same sign (XSD 1.1 value space).
9
+ *
10
+ * @module @tradik/xslt3/xdm/duration
11
+ */
12
+
13
+ import { XPathError } from "../errors.js";
14
+ import { Decimal } from "./decimal.js";
15
+
16
+ /** A duration value. */
17
+ export class DurationValue {
18
+ /**
19
+ * @param {number} months - Safe integer
20
+ * @param {Decimal} seconds
21
+ * @throws {XPathError} FODT0002 when the months exceed the safe range
22
+ */
23
+ constructor(months, seconds) {
24
+ if (!Number.isSafeInteger(months)) {
25
+ throw new XPathError(
26
+ "FODT0002",
27
+ "Duration is out of the supported range",
28
+ );
29
+ }
30
+ this.months = months + 0; // no negative zero
31
+ this.seconds = seconds;
32
+ Object.freeze(this);
33
+ }
34
+ }
35
+
36
+ /** The zero duration. */
37
+ export const ZERO_DURATION = new DurationValue(0, Decimal.ZERO);
38
+
39
+ // seconds need digits on both sides of a decimal point, as in XSD 1.0
40
+ // ("PT.5S" and "PT1.S" are errors, qt3tests CastAs688-691)
41
+ const pattern =
42
+ /^(-)?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
43
+
44
+ /** Components allowed per primitive type (indexes into the pattern groups). */
45
+ const allowed = {
46
+ duration: [2, 3, 4, 5, 6, 7],
47
+ yearMonthDuration: [2, 3],
48
+ dayTimeDuration: [4, 5, 6, 7],
49
+ };
50
+
51
+ /**
52
+ * Parses a duration lexical form (whitespace already collapsed).
53
+ * @param {string} kind - "duration", "yearMonthDuration" or "dayTimeDuration"
54
+ * @param {string} text
55
+ * @returns {DurationValue|null} null when invalid
56
+ * @throws {XPathError} FODT0002 when out of the supported range
57
+ */
58
+ export function parseDuration(kind, text) {
59
+ const match = pattern.exec(text);
60
+ if (!match || text.endsWith("T")) return null;
61
+ const present = [2, 3, 4, 5, 6, 7].filter((i) => match[i] !== undefined);
62
+ if (present.length === 0 || present.some((i) => !allowed[kind].includes(i))) {
63
+ return null;
64
+ }
65
+ const [
66
+ ,
67
+ sign,
68
+ years = "0",
69
+ months = "0",
70
+ days = "0",
71
+ hours = "0",
72
+ minutes = "0",
73
+ ] = match;
74
+ const totalMonths = BigInt(years) * 12n + BigInt(months);
75
+ const whole =
76
+ (BigInt(days) * 24n + BigInt(hours)) * 60n * 60n + BigInt(minutes) * 60n;
77
+ let seconds = Decimal.of(whole).add(Decimal.parse(match[7] ?? "0"));
78
+ let monthCount = Number(totalMonths);
79
+ if (totalMonths > BigInt(Number.MAX_SAFE_INTEGER)) monthCount = Infinity;
80
+ if (sign) {
81
+ seconds = seconds.neg();
82
+ monthCount = -monthCount;
83
+ }
84
+ return new DurationValue(monthCount, seconds);
85
+ }
86
+
87
+ /**
88
+ * @param {number} months
89
+ * @returns {string} "nYnM" part without sign, "" for zero
90
+ */
91
+ function yearMonthPart(months) {
92
+ const abs = Math.abs(months);
93
+ const years = Math.floor(abs / 12);
94
+ return (years ? `${years}Y` : "") + (abs % 12 ? `${abs % 12}M` : "");
95
+ }
96
+
97
+ /**
98
+ * @param {Decimal} seconds
99
+ * @returns {string} "nDTnHnMnS" part without sign, "" for zero
100
+ */
101
+ function dayTimePart(seconds) {
102
+ const abs = seconds.sign() < 0 ? seconds.neg() : seconds;
103
+ const whole = abs.trunc();
104
+ const days = whole / 86400n;
105
+ const hours = (whole / 3600n) % 24n;
106
+ const minutes = (whole / 60n) % 60n;
107
+ const secs = abs.sub(Decimal.of(whole - (whole % 60n)));
108
+ const time =
109
+ (hours ? `${hours}H` : "") +
110
+ (minutes ? `${minutes}M` : "") +
111
+ (secs.sign() ? `${secs}S` : "");
112
+ return (days ? `${days}D` : "") + (time ? `T${time}` : "");
113
+ }
114
+
115
+ /**
116
+ * Canonical string of a duration: "P2Y", "P1DT12H", "-PT1.5S"; the zero
117
+ * duration is "P0M" for xs:yearMonthDuration and "PT0S" otherwise.
118
+ * @param {string} kind - "duration", "yearMonthDuration" or "dayTimeDuration"
119
+ * @param {DurationValue} value
120
+ * @returns {string}
121
+ */
122
+ export function formatDuration(kind, value) {
123
+ const months = kind === "dayTimeDuration" ? 0 : value.months;
124
+ const seconds = kind === "yearMonthDuration" ? Decimal.ZERO : value.seconds;
125
+ const body = yearMonthPart(months) + dayTimePart(seconds);
126
+ if (body === "") return kind === "yearMonthDuration" ? "P0M" : "PT0S";
127
+ return `${months < 0 || seconds.sign() < 0 ? "-" : ""}P${body}`;
128
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * General comparisons (=, !=, <, <=, >, >=) of XPath 3.1 section 3.7.2,
3
+ * with the XPath 1.0 compatibility mode rules.
4
+ *
5
+ * @module @tradik/xslt3/xdm/generalCompare
6
+ */
7
+
8
+ import { XPathError } from "../errors.js";
9
+ import { AtomicValue } from "./atomic.js";
10
+ import { cast } from "./cast.js";
11
+ import { orderTests, valueCompare } from "./compare.js";
12
+ import { atomize, effectiveBooleanValue } from "./nodes.js";
13
+ import { compareCodepoints } from "./strings.js";
14
+ import { derivesFrom, isNumericType, types } from "./types.js";
15
+
16
+ const valueOperators = {
17
+ "=": "eq",
18
+ "!=": "ne",
19
+ "<": "lt",
20
+ "<=": "le",
21
+ ">": "gt",
22
+ ">=": "ge",
23
+ };
24
+
25
+ const isUntyped = (item) => item.type === types.untypedAtomic;
26
+ /** xs:untypedAtomic or xs:string itself (not a subtype). */
27
+ const isPlainString = (item) =>
28
+ item.type === types.untypedAtomic || item.type === types.string;
29
+
30
+ const isString = (item) => derivesFrom(item.type, types.string);
31
+
32
+ /**
33
+ * fn:number of an atomic value: its xs:double value, or NaN when it cannot
34
+ * be cast.
35
+ * @param {AtomicValue} item
36
+ * @returns {AtomicValue} an xs:double
37
+ */
38
+ export function toNumber(item) {
39
+ try {
40
+ return cast(item, types.double);
41
+ } catch (error) {
42
+ if (!(error instanceof XPathError)) throw error;
43
+ return new AtomicValue(types.double, NaN);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Converts an untyped operand for comparison with a typed one (XPath 3.1).
49
+ * @param {AtomicValue} untyped
50
+ * @param {AtomicValue} other
51
+ * @returns {AtomicValue}
52
+ */
53
+ function castUntyped(untyped, other) {
54
+ if (isNumericType(other.type)) return cast(untyped, types.double);
55
+ for (const duration of [types.dayTimeDuration, types.yearMonthDuration]) {
56
+ if (derivesFrom(other.type, duration)) return cast(untyped, duration);
57
+ }
58
+ return cast(untyped, other.type.primitive);
59
+ }
60
+
61
+ /**
62
+ * Prepares a pair of atomic values (XPath 3.1 mode). Two untyped values
63
+ * compare as strings, which generalCompare does without converting them.
64
+ * @returns {[AtomicValue, AtomicValue]}
65
+ */
66
+ function convertPair(a, b) {
67
+ if (isUntyped(a)) return [castUntyped(a, b), b];
68
+ if (isUntyped(b)) return [a, castUntyped(b, a)];
69
+ return [a, b];
70
+ }
71
+
72
+ /**
73
+ * Prepares a pair of atomic values (XPath 1.0 compatibility mode).
74
+ * @returns {[AtomicValue, AtomicValue]}
75
+ */
76
+ function convertPairCompatible(a, b) {
77
+ if (isNumericType(a.type) || isNumericType(b.type)) {
78
+ return [toNumber(a), toNumber(b)];
79
+ }
80
+ if (isString(a) || isString(b) || (isUntyped(a) && isUntyped(b))) {
81
+ return [cast(a, types.string), cast(b, types.string)];
82
+ }
83
+ if (isUntyped(a)) return [cast(a, b.type), b];
84
+ if (isUntyped(b)) return [a, cast(b, a.type)];
85
+ return [a, b];
86
+ }
87
+
88
+ /**
89
+ * Whether an operand is a single xs:boolean atomic value.
90
+ * @param {Array} sequence
91
+ * @returns {boolean}
92
+ */
93
+ function isSingleBoolean(sequence) {
94
+ return (
95
+ sequence.length === 1 &&
96
+ sequence[0] instanceof AtomicValue &&
97
+ sequence[0].type.primitive === types.boolean
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Evaluates a general comparison of two sequences: true when some pair of
103
+ * atomized items has the relationship.
104
+ * @param {Array} left - Sequence of items (atomized here)
105
+ * @param {"="|"!="|"<"|"<="|">"|">="} op
106
+ * @param {Array} right
107
+ * @param {import("./compare.js").CompareOptions & {backwardsCompatible?: boolean}} [options]
108
+ * - `backwardsCompatible` enables the XPath 1.0 compatibility mode
109
+ * @returns {boolean}
110
+ * @throws {XPathError} XPTY0004 for incomparable types, FORG0001 when an
111
+ * untyped value cannot be cast
112
+ */
113
+ export function generalCompare(left, op, right, options = {}) {
114
+ const valueOp = valueOperators[op];
115
+ if (!valueOp) {
116
+ throw new TypeError(`Unknown general comparison operator ${op}`);
117
+ }
118
+ let a = left;
119
+ let b = right;
120
+ let convert = convertPair;
121
+ if (options.backwardsCompatible) {
122
+ convert = convertPairCompatible;
123
+ if (isSingleBoolean(a) || isSingleBoolean(b)) {
124
+ const toBoolean = (s) => [
125
+ new AtomicValue(types.boolean, effectiveBooleanValue(s)),
126
+ ];
127
+ a = isSingleBoolean(a) ? a : toBoolean(a);
128
+ b = isSingleBoolean(b) ? b : toBoolean(b);
129
+ }
130
+ }
131
+ a = atomize(a);
132
+ b = atomize(b);
133
+ if (options.backwardsCompatible && valueOp !== "eq" && valueOp !== "ne") {
134
+ a = a.map(toNumber);
135
+ b = b.map(toNumber);
136
+ }
137
+ const collation = options.collation ?? compareCodepoints;
138
+ const holds = orderTests[valueOp];
139
+ for (const x of a) {
140
+ for (const y of b) {
141
+ // Strings and untyped values compare as strings in both modes:
142
+ // skip the casts and the result objects of valueCompare
143
+ if (isPlainString(x) && isPlainString(y)) {
144
+ if (holds(collation(x.value, y.value))) return true;
145
+ continue;
146
+ }
147
+ const [p, q] = convert(x, y);
148
+ if (valueCompare(p, valueOp, q, options)) return true;
149
+ }
150
+ }
151
+ return false;
152
+ }