• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FontoXML / fontoxpath / 5951693693

23 Aug 2023 01:03PM UTC coverage: 91.547% (+0.05%) from 91.497%
5951693693

Pull #607

github

drex04
PR review fixes
Pull Request #607: Format integer RTL

4932 of 5732 branches covered (86.04%)

Branch coverage included in aggregate %.

137 of 138 new or added lines in 1 file covered. (99.28%)

9 existing lines in 1 file now uncovered.

10630 of 11267 relevant lines covered (94.35%)

97742.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.32
/src/expressions/functions/builtInFunctions_numeric.ts
1
import atomize from '../dataTypes/atomize';
9✔
2
import tryCastToType from '../dataTypes/casting/tryCastToType';
9✔
3
import castToType from '../dataTypes/castToType';
9✔
4
import createAtomicValue from '../dataTypes/createAtomicValue';
9✔
5
import FunctionValue from '../dataTypes/FunctionValue';
9✔
6
import type ISequence from '../dataTypes/ISequence';
7
import isSubtypeOf from '../dataTypes/isSubtypeOf';
9✔
8
import MapValue from '../dataTypes/MapValue';
9✔
9
import sequenceFactory from '../dataTypes/sequenceFactory';
9✔
10
import { SequenceMultiplicity, ValueType } from '../dataTypes/Value';
11
import type DynamicContext from '../DynamicContext';
12
import type ExecutionParameters from '../ExecutionParameters';
13
import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
14
import type StaticContext from '../StaticContext';
15
import { DONE_TOKEN, ready } from '../util/iterators';
9✔
16
import { errXPDY0002 } from '../XPathErrors';
9✔
17
import { performFunctionConversion } from './argumentHelper';
9✔
18
import type { BuiltinDeclarationType } from './builtInFunctions';
19
import type FunctionDefinitionType from './FunctionDefinitionType';
20

21
function createValidNumericType(type: ValueType, transformedValue: number) {
22
        if (isSubtypeOf(type, ValueType.XSINTEGER)) {
6!
23
                return createAtomicValue(transformedValue, ValueType.XSINTEGER);
6✔
24
        }
25
        if (isSubtypeOf(type, ValueType.XSFLOAT)) {
×
26
                return createAtomicValue(transformedValue, ValueType.XSFLOAT);
×
27
        }
28
        if (isSubtypeOf(type, ValueType.XSDOUBLE)) {
×
29
                return createAtomicValue(transformedValue, ValueType.XSDOUBLE);
×
30
        }
31
        // It must be a decimal, only four numeric types
32
        return createAtomicValue(transformedValue, ValueType.XSDECIMAL);
×
33
}
34

35
const ROMAN_NUMBERS = [
9✔
36
        { symbol: 'M', decimal: 1000 },
37
        { symbol: 'CM', decimal: 900 },
38
        { symbol: 'D', decimal: 500 },
39
        { symbol: 'CD', decimal: 400 },
40
        { symbol: 'C', decimal: 100 },
41
        { symbol: 'XC', decimal: 90 },
42
        { symbol: 'L', decimal: 50 },
43
        { symbol: 'XL', decimal: 40 },
44
        { symbol: 'X', decimal: 10 },
45
        { symbol: 'IX', decimal: 9 },
46
        { symbol: 'V', decimal: 5 },
47
        { symbol: 'IV', decimal: 4 },
48
        { symbol: 'I', decimal: 1 },
49
];
50

51
function convertIntegerToRoman(integer: number, isLowerCase?: boolean) {
52
        const isNegative = integer < 0;
15✔
53

54
        integer = Math.abs(integer);
15✔
55

56
        if (!integer) {
15✔
57
                return '-';
6✔
58
        }
59

60
        let romanString = ROMAN_NUMBERS.reduce((str, roman) => {
9✔
61
                const q = Math.floor(integer / roman.decimal);
117✔
62
                integer -= q * roman.decimal;
117✔
63
                return str + roman.symbol.repeat(q);
117✔
64
        }, '');
65

66
        if (isLowerCase) {
9✔
67
                romanString = romanString.toLowerCase();
6✔
68
        }
69

70
        if (isNegative) {
9✔
71
                romanString = `-${romanString}`;
3✔
72
        }
73

74
        return romanString;
9✔
75
}
76

77
function convertIntegerToLowerRoman(integer: number) {
78
        return convertIntegerToRoman(integer, true);
9✔
79
}
80

81
function convertIntegerToUpperRoman(integer: number) {
82
        return convertIntegerToRoman(integer, false);
6✔
83
}
84

85
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
9✔
86

87
function convertIntegerToAlphabet(integer: number, isLowerCase?: boolean) {
88
        const isNegative = integer < 0;
24✔
89

90
        integer = Math.abs(integer);
24✔
91

92
        if (!integer) {
24✔
93
                return '-';
6✔
94
        }
95

96
        let output = '';
18✔
97
        let digit;
98

99
        while (integer > 0) {
18✔
100
                digit = (integer - 1) % ALPHABET.length;
27✔
101
                output = ALPHABET[digit] + output;
27✔
102
                integer = ((integer - digit) / ALPHABET.length) | 0;
27✔
103
        }
104

105
        if (isLowerCase) {
18✔
106
                output = output.toLowerCase();
12✔
107
        }
108

109
        if (isNegative) {
18✔
110
                output = `-${output}`;
6✔
111
        }
112

113
        return output;
18✔
114
}
115

116
function convertIntegerToLowerAlphabet(integer: number) {
117
        return convertIntegerToAlphabet(integer, true);
15✔
118
}
119

120
function convertIntegerToUpperAlphabet(integer: number) {
121
        return convertIntegerToAlphabet(integer, false);
9✔
122
}
123

124
function getUnicodeSubset(
125
        firstCharacterCharCode: number,
126
        numberOfCharacters: number,
127
        charactersToSkip: number[] = []
×
128
): number[] {
129
        return Array.from({ length: numberOfCharacters }, (_, i) => i + firstCharacterCharCode).filter(
567✔
130
                (charCode) => {
131
                        return !charactersToSkip.includes(charCode);
567✔
132
                }
133
        );
134
}
135

136
const HEBREW_ALPHABET_UNICODE = getUnicodeSubset(
9✔
137
        0x05d0,
138
        27,
139
        [0x05da, 0x05dd, 0x05df, 0x05e3, 0x05e5]
140
);
141

142
// The Unicode standard follows the modern Arabic Alif-Ba-Ta alphabetical order
143
const ARABIC_ALPHABET_UNICODE = getUnicodeSubset(
9✔
144
        0x0627,
145
        36,
146
        [0x0629, 0x063b, 0x063c, 0x063d, 0x063e, 0x063f, 0x0640, 0x0649]
147
);
148

149
const ARABIC_ABJADI_ALPHABET = [
9✔
150
        'أ',
151
        'ب',
152
        'ج',
153
        'د',
154
        'ه',
155
        'و',
156
        'ز',
157
        'ح',
158
        'ط',
159
        'ي',
160
        'ك',
161
        'ل',
162
        'م',
163
        'ن',
164
        'س',
165
        'ع',
166
        'ف',
167
        'ص',
168
        'ق',
169
        'ر',
170
        'ش',
171
        'ت',
172
        'ث',
173
        'خ',
174
        'ذ',
175
        'ض',
176
        'ظ',
177
        'غ',
178
];
179

180
// The value is the numerical value of the corresponding character in the Abjad numbering system.
181
const DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE: [number, string][] = [
9✔
182
        [1000, 'غ'],
183
        [900, 'ظ'],
184
        [800, 'ض'],
185
        [700, 'ذ'],
186
        [600, 'خ'],
187
        [500, 'ث'],
188
        [400, 'ت'],
189
        [300, 'ش'],
190
        [200, 'ر'],
191
        [100, 'ق'],
192
        [90, 'ص'],
193
        [80, 'ف'],
194
        [70, 'ع'],
195
        [60, 'س'],
196
        [50, 'ن'],
197
        [40, 'م'],
198
        [30, 'ل'],
199
        [20, 'ك'],
200
        [10, 'ي'],
201
        [9, 'ط'],
202
        [8, 'ح'],
203
        [7, 'ز'],
204
        [6, 'و'],
205
        [5, 'ه'],
206
        [4, 'د'],
207
        [3, 'ج'],
208
        [2, 'ب'],
209
        [1, 'أ'],
210
];
211

212
const DECIMAL_TO_HEBREW_ALEFBET_NUMBER_TABLE: [number, string][] = [
9✔
213
        [400, 'ת'],
214
        [300, 'ש'],
215
        [200, 'ר'],
216
        [100, 'ק'],
217
        [90, 'צ'],
218
        [80, 'פ'],
219
        [70, 'ע'],
220
        [60, 'ס'],
221
        [50, 'נ'],
222
        [40, 'מ'],
223
        [30, 'ל'],
224
        [20, 'כ'],
225
        [10, 'י'],
226
        [9, 'ט'],
227
        [8, 'ח'],
228
        [7, 'ז'],
229
        [6, 'ו'],
230
        [5, 'ה'],
231
        [4, 'ד'],
232
        [3, 'ג'],
233
        [2, 'ב'],
234
        [1, 'א'],
235
];
236

237
function createUnicodeRangeBasedNumberFormatter(
238
        firstCharacterCharCode: number,
239
        numberOfCharacters: number,
240
        charactersToSkip: number[] = []
×
241
): (integer: number) => string {
242
        charactersToSkip.sort((a, b) => a - b);
18✔
243
        numberOfCharacters = numberOfCharacters - charactersToSkip.length;
18✔
244

245
        return function formatAsNumberInUnicodeRange(integer: number) {
18✔
246
                const isNegative = integer < 0;
24✔
247

248
                integer = Math.abs(integer);
24✔
249

250
                if (!integer) {
24✔
251
                        return '-';
6✔
252
                }
253

254
                const formattedNumberParts = [];
18✔
255
                while (integer > 0) {
18✔
256
                        const digit = (integer - 1) % numberOfCharacters;
27✔
257
                        let charCode = firstCharacterCharCode + digit;
27✔
258

259
                        // We should skip all skippable characters, because they are in order, we can loop over them and increment our pointer until we have a character we can use.
260
                        charactersToSkip.forEach((char) => {
27✔
261
                                if (charCode >= char) {
27!
NEW
262
                                        charCode++;
×
263
                                }
264
                        });
265

266
                        formattedNumberParts.unshift(String.fromCodePoint(charCode));
27✔
267
                        integer = Math.floor((integer - 1) / numberOfCharacters);
27✔
268
                }
269
                let output = formattedNumberParts.join('');
18✔
270
                if (isNegative) {
18✔
271
                        output = `-${output}`;
3✔
272
                }
273

274
                return output;
18✔
275
        };
276
}
277

278
const convertIntegerToLowerGreek = createUnicodeRangeBasedNumberFormatter(0x03b1, 25, [0x03c2]);
9✔
279

280
const convertIntegerToUpperGreek = createUnicodeRangeBasedNumberFormatter(0x0391, 25, [0x03a2]);
9✔
281

282
// Format as a letter in alphabetical order, using the Hebrew alphabetical order
283
function convertIntegerToHebrewAlefBet(integer: number): string {
284
        const isNegative = integer < 0;
105✔
285

286
        integer = Math.abs(integer);
105✔
287

288
        if (!integer) {
105✔
289
                return '-';
3✔
290
        }
291

292
        const multiples = Math.floor((integer - 1) / HEBREW_ALPHABET_UNICODE.length);
102✔
293
        // When list runs out of individual characters, repeat the last character in the alphabet
294
        const repeatChar = String.fromCodePoint(0x05ea);
102✔
295
        const formattedNumberParts = Array(multiples).fill(repeatChar);
102✔
296

297
        const digit = (integer - 1) % HEBREW_ALPHABET_UNICODE.length;
102✔
298
        const charCode = HEBREW_ALPHABET_UNICODE[digit];
102✔
299
        formattedNumberParts.push(String.fromCodePoint(charCode));
102✔
300

301
        let output = formattedNumberParts.join('');
102✔
302

303
        if (isNegative) {
102✔
304
                output = `-${output}`;
3✔
305
        }
306

307
        return output;
102✔
308
}
309

310
// Format as a letter in alphabetical order, using the Arabic AlifBaTa alphabetical order
311
function convertIntegerToArabicAlifBaTa(integer: number): string {
312
        const isNegative = integer < 0;
117✔
313

314
        integer = Math.abs(integer);
117✔
315

316
        if (!integer) {
117✔
317
                return '-';
3✔
318
        }
319

320
        const multiples = Math.floor((integer - 1) / ARABIC_ALPHABET_UNICODE.length) + 1;
114✔
321
        const digit = (integer - 1) % ARABIC_ALPHABET_UNICODE.length;
114✔
322
        const charCode = ARABIC_ALPHABET_UNICODE[digit];
114✔
323

324
        // When list runs out of individual characters, double each character, then triple, etc.
325
        const formattedNumberParts = Array(multiples).fill(String.fromCodePoint(charCode));
114✔
326

327
        // Add non-joiner separator between characters to prevent ligatures
328
        let output = formattedNumberParts.join(String.fromCodePoint(0x200c));
114✔
329

330
        if (isNegative) {
114✔
331
                output = `-${output}`;
3✔
332
        }
333

334
        return output;
114✔
335
}
336

337
// Format as a letter in alphabetical order, using the Arabic Abjadi alphabetical order
338
function convertIntegerToArabicAbjadi(integer: number): string {
339
        const isNegative = integer < 0;
117✔
340

341
        integer = Math.abs(integer);
117✔
342

343
        if (!integer) {
117✔
344
                return '-';
3✔
345
        }
346

347
        const multiples = Math.floor((integer - 1) / ARABIC_ABJADI_ALPHABET.length) + 1;
114✔
348
        const digit = (integer - 1) % ARABIC_ABJADI_ALPHABET.length;
114✔
349

350
        // When list runs out of individual characters, double each character, then triple, etc.
351
        const formattedNumberParts = Array(multiples).fill(ARABIC_ABJADI_ALPHABET[digit]);
114✔
352

353
        // Add non-joiner separator between characters to prevent ligatures
354
        let output = formattedNumberParts.join(String.fromCodePoint(0x200c));
114✔
355

356
        if (isNegative) {
114✔
357
                output = `-${output}`;
3✔
358
        }
359

360
        return output;
114✔
361
}
362

363
function convertIntegerToAbjadNumeral(integer: number): string {
364
        const isNegative = integer < 0;
150✔
365

366
        integer = Math.abs(integer);
150✔
367

368
        if (!integer) {
150✔
369
                return '-';
3✔
370
        }
371

372
        const arabicAbjadNumberParts = [];
147✔
373
        let thousands = Math.floor(integer / 1000);
147✔
374
        let remainder = integer - thousands * 1000;
147✔
375

376
        if (thousands === 1) {
147✔
377
                // Push the 1000 numeral
378
                arabicAbjadNumberParts.push(DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE[0][1]);
9✔
379
        } else if (thousands > 1) {
138✔
380
                // Push the multiplicative thousands place
381
                for (const [val, char] of DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE) {
18✔
382
                        while (thousands >= val) {
504✔
383
                                arabicAbjadNumberParts.push(char);
21✔
384
                                thousands -= val;
21✔
385
                        }
386
                }
387
                // Then push the 1000 numeral
388
                arabicAbjadNumberParts.push(DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE[0][1]);
18✔
389
        }
390

391
        // Then append all numbers lower than 999
392
        for (const [val, char] of DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE) {
147✔
393
                while (remainder >= val) {
4,116✔
394
                        remainder -= val;
207✔
395
                        arabicAbjadNumberParts.push(char);
207✔
396
                }
397
        }
398
        let output = arabicAbjadNumberParts.join('');
147✔
399

400
        if (isNegative) {
147✔
401
                output = `-${output}`;
3✔
402
        }
403

404
        return output;
147✔
405
}
406

407
function convertIntegerToHebrewNumeral(integer: number): string {
408
        const isNegative = integer < 0;
144✔
409

410
        integer = Math.abs(integer);
144✔
411

412
        if (!integer) {
144✔
413
                return '-';
3✔
414
        }
415

416
        const hebrewNumberParts = [];
141✔
417

418
        // In the number system, 400 is the largest numeral and is repeated when numbers grow larger than 799
419
        const fourHundreds = Math.floor(integer / 400);
141✔
420
        let remainder = integer - fourHundreds * 400;
141✔
421

422
        // Push a 'ת' character for each additional multiple of 400
423
        for (let i = 0; i < fourHundreds; i++) {
141✔
424
                hebrewNumberParts.push('ת');
33✔
425
        }
426
        // Then add the numbers smaller than 400
427
        for (const [val, char] of DECIMAL_TO_HEBREW_ALEFBET_NUMBER_TABLE) {
141✔
428
                while (remainder >= val) {
3,102✔
429
                        remainder -= val;
231✔
430
                        hebrewNumberParts.push(char);
231✔
431
                }
432
        }
433

434
        // If the last two digits are 15 or 16, substitute these characters because they are an exception in the Hebrew number system
435
        // For 15, substitute (10+5) with (9+6)
436
        const lastTwoChars = hebrewNumberParts.slice(-2).join('');
141✔
437
        if (lastTwoChars === 'יה') {
141✔
438
                hebrewNumberParts.splice(-2, 2, 'ט', 'ו');
12✔
439
        }
440
        // For 16, substitute (10+5) with (9+7)
441
        if (lastTwoChars === 'יו') {
141✔
442
                hebrewNumberParts.splice(-2, 2, 'ט', 'ז');
12✔
443
        }
444

445
        let output = hebrewNumberParts.join('');
141✔
446

447
        if (isNegative) {
141✔
448
                output = `-${output}`;
3✔
449
        }
450

451
        return output;
141✔
452
}
453

454
function convertIntegerToArabicIndicNumeral(integer: number): string {
455
        return new Intl.NumberFormat([], {
90✔
456
                // @ts-ignore TS does not recognize this NumberFormatOption because it is still an experimental part of the spec. However it is supported by all major browsers.
457
                numberingSystem: 'arab',
458
                useGrouping: false,
459
        }).format(integer);
460
}
461

462
function convertIntegerToPersianNumeral(integer: number): string {
463
        return new Intl.NumberFormat([], {
90✔
464
                // @ts-ignore TS does not recognize this NumberFormatOption because it is still an experimental part of the spec. However it is supported by all major browsers.
465
                numberingSystem: 'arabext',
466
                useGrouping: false,
467
        }).format(integer);
468
}
469

470
const fnAbs: FunctionDefinitionType = (
9✔
471
        _dynamicContext,
472
        _executionParameters,
473
        _staticContext,
474
        sequence
475
) => {
476
        return sequence.map((onlyValue) =>
9✔
477
                createValidNumericType(onlyValue.type, Math.abs(onlyValue.value))
6✔
478
        );
479
};
480

481
const integerConverters = new Map([
9✔
482
        ['A', convertIntegerToUpperAlphabet],
483
        ['a', convertIntegerToLowerAlphabet],
484
        ['I', convertIntegerToUpperRoman],
485
        ['i', convertIntegerToLowerRoman],
486
        ['lowerGreek', convertIntegerToLowerGreek],
487
        ['\u03b1', convertIntegerToLowerGreek],
488
        ['upperGreek', convertIntegerToUpperGreek],
489
        ['\u0391', convertIntegerToUpperGreek],
490
        ['arabicAbjadi', convertIntegerToArabicAbjadi],
491
        ['arabicAbjadNumeral', convertIntegerToAbjadNumeral],
492
        ['arabicAlifBaTa', convertIntegerToArabicAlifBaTa],
493
        ['hebrewAlefBet', convertIntegerToHebrewAlefBet],
494
        ['hebrewNumeral', convertIntegerToHebrewNumeral],
495
        ['arabicIndicNumeral', convertIntegerToArabicIndicNumeral],
496
        ['\u0661', convertIntegerToArabicIndicNumeral],
497
        ['\u0662', convertIntegerToArabicIndicNumeral],
498
        ['\u0663', convertIntegerToArabicIndicNumeral],
499
        ['\u0664', convertIntegerToArabicIndicNumeral],
500
        ['\u0665', convertIntegerToArabicIndicNumeral],
501
        ['\u0666', convertIntegerToArabicIndicNumeral],
502
        ['\u0667', convertIntegerToArabicIndicNumeral],
503
        ['\u0668', convertIntegerToArabicIndicNumeral],
504
        ['\u0669', convertIntegerToArabicIndicNumeral],
505
        ['persianNumeral', convertIntegerToPersianNumeral],
506
        ['\u06f1', convertIntegerToPersianNumeral],
507
        ['\u06f2', convertIntegerToPersianNumeral],
508
        ['\u06f3', convertIntegerToPersianNumeral],
509
        ['\u06f4', convertIntegerToPersianNumeral],
510
        ['\u06f5', convertIntegerToPersianNumeral],
511
        ['\u06f6', convertIntegerToPersianNumeral],
512
        ['\u06f7', convertIntegerToPersianNumeral],
513
        ['\u06f8', convertIntegerToPersianNumeral],
514
        ['\u06f9', convertIntegerToPersianNumeral],
515
]);
516

517
const fnFormatInteger: FunctionDefinitionType = (
9✔
518
        _dynamicContext,
519
        _executionParameters,
520
        _staticContext,
521
        sequence,
522
        pictureSequence
523
) => {
524
        const sequenceValue = sequence.first();
918✔
525
        const pictureValue = pictureSequence.first();
918✔
526

527
        if (sequence.isEmpty()) {
918✔
528
                return sequenceFactory.singleton(createAtomicValue('', ValueType.XSSTRING));
39✔
529
        }
530

531
        const integerConverter = integerConverters.get(pictureValue.value);
879✔
532

533
        if (integerConverter) {
879✔
534
                const integer = sequenceValue.value as number;
876✔
535
                const convertedString = integerConverter(integer);
876✔
536
                return sequenceFactory.singleton(createAtomicValue(convertedString, ValueType.XSSTRING));
876✔
537
        } else {
538
                const validPictureStrings = Array.from(
3✔
539
                        integerConverters,
540
                        ([pictureString, _]) => pictureString
99✔
541
                );
542
                throw new Error(
3✔
543
                        `Picture: ${
544
                                pictureValue.value
545
                        } is not implemented yet. The supported picture strings are ${
546
                                validPictureStrings.slice(0, -1).join(', ') +
547
                                ' and ' +
548
                                validPictureStrings.slice(-1)
549
                        }.`
550
                );
551
        }
552
};
553

554
const fnCeiling: FunctionDefinitionType = (
9✔
555
        _dynamicContext,
556
        _executionParameters,
557
        _staticContext,
558
        sequence
559
) => {
560
        return sequence.map((onlyValue) =>
×
561
                createValidNumericType(onlyValue.type, Math.ceil(onlyValue.value))
×
562
        );
563
};
564

565
const fnFloor: FunctionDefinitionType = (
9✔
566
        _dynamicContext,
567
        _executionParameters,
568
        _staticContext,
569
        sequence
570
) => {
UNCOV
571
        return sequence.map((onlyValue) =>
×
UNCOV
572
                createValidNumericType(onlyValue.type, Math.floor(onlyValue.value))
×
573
        );
574
};
575

576
function isHalf(value: number, scaling: number) {
577
        return ((value * scaling) % 1) % 0.5 === 0;
39✔
578
}
579

580
function getNumberOfDecimalDigits(value: number) {
581
        if (Math.floor(value) === value || isNaN(value)) {
93✔
582
                return 0;
57✔
583
        }
584

585
        const result = /\d+(?:\.(\d*))?(?:[Ee](-)?(\d+))*/.exec(`${value}`);
36✔
586
        const decimals = result[1] ? result[1].length : 0;
36!
587

588
        if (result[3]) {
36!
UNCOV
589
                if (result[2]) {
×
UNCOV
590
                        return decimals + parseInt(result[3], 10);
×
591
                }
UNCOV
592
                const returnVal = decimals - parseInt(result[3], 10);
×
UNCOV
593
                return returnVal < 0 ? 0 : returnVal;
×
594
        }
595
        return decimals;
36✔
596
}
597

598
function determineRoundedNumber(itemAsDecimal: number, halfToEven: boolean, scaling: number) {
599
        if (halfToEven && isHalf(itemAsDecimal, scaling)) {
84✔
600
                if (Math.floor(itemAsDecimal * scaling) % 2 === 0) {
24✔
601
                        return Math.floor(itemAsDecimal * scaling) / scaling;
15✔
602
                }
603
                return Math.ceil(itemAsDecimal * scaling) / scaling;
9✔
604
        }
605
        return Math.round(itemAsDecimal * scaling) / scaling;
60✔
606
}
607

608
function fnRound(
609
        halfToEven: boolean,
610
        _dynamicContext: DynamicContext,
611
        _executionParameters: ExecutionParameters,
612
        _staticContext: StaticContext,
613
        sequence: ISequence,
614
        precision: ISequence
615
): ISequence {
616
        let done = false;
126✔
617
        return sequenceFactory.create({
126✔
618
                next: () => {
619
                        if (done) {
189✔
620
                                return DONE_TOKEN;
72✔
621
                        }
622
                        const firstValue = sequence.first();
117✔
623
                        if (!firstValue) {
117✔
624
                                // Empty sequence
625
                                done = true;
12✔
626
                                return DONE_TOKEN;
12✔
627
                        }
628

629
                        if (
105✔
630
                                (isSubtypeOf(firstValue.type, ValueType.XSFLOAT) ||
420✔
631
                                        isSubtypeOf(firstValue.type, ValueType.XSDOUBLE)) &&
632
                                (firstValue.value === 0 ||
633
                                        isNaN(firstValue.value as number) ||
634
                                        firstValue.value === +Infinity ||
635
                                        firstValue.value === -Infinity)
636
                        ) {
637
                                done = true;
12✔
638
                                return ready(firstValue);
12✔
639
                        }
640
                        let scalingPrecision;
641
                        if (precision) {
93✔
642
                                const sp = precision.first();
21✔
643
                                scalingPrecision = sp.value;
21✔
644
                        } else {
645
                                scalingPrecision = 0;
72✔
646
                        }
647
                        done = true;
93✔
648

649
                        if (getNumberOfDecimalDigits(firstValue.value as number) < scalingPrecision) {
93✔
650
                                return ready(firstValue);
9✔
651
                        }
652

653
                        const originalType = [
84✔
654
                                ValueType.XSINTEGER,
655
                                ValueType.XSDECIMAL,
656
                                ValueType.XSDOUBLE,
657
                                ValueType.XSFLOAT,
658
                        ].find((type: ValueType) => {
659
                                return isSubtypeOf(firstValue.type, type);
189✔
660
                        });
661
                        const itemAsDecimal = castToType(firstValue, ValueType.XSDECIMAL);
84✔
662
                        const scaling = Math.pow(10, scalingPrecision);
84✔
663
                        const roundedNumber = determineRoundedNumber(itemAsDecimal.value, halfToEven, scaling);
84✔
664
                        switch (originalType) {
84!
665
                                case ValueType.XSDECIMAL:
666
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSDECIMAL));
27✔
667
                                case ValueType.XSDOUBLE:
668
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSDOUBLE));
39✔
669
                                case ValueType.XSFLOAT:
UNCOV
670
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSFLOAT));
×
671
                                case ValueType.XSINTEGER:
672
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSINTEGER));
18✔
673
                        }
674
                },
675
        });
676
}
677

678
const fnNumber: FunctionDefinitionType = (
9✔
679
        _dynamicContext,
680
        executionParameters,
681
        _staticContext,
682
        sequence
683
) => {
684
        return atomize(sequence, executionParameters).switchCases({
42✔
685
                empty: () => sequenceFactory.singleton(createAtomicValue(NaN, ValueType.XSDOUBLE)),
9✔
686
                singleton: () => {
687
                        const castResult = tryCastToType(sequence.first(), ValueType.XSDOUBLE);
30✔
688
                        if (castResult.successful) {
30✔
689
                                return sequenceFactory.singleton(castResult.value);
21✔
690
                        }
691
                        return sequenceFactory.singleton(createAtomicValue(NaN, ValueType.XSDOUBLE));
9✔
692
                },
693
                multiple: () => {
UNCOV
694
                        throw new Error('fn:number may only be called with zero or one values');
×
695
                },
696
        });
697
};
698

699
// This is the djb2 algorithm, taken from http://www.cse.yorku.ca/~oz/hash.html
700
function createSeedFromString(seed: string): number {
701
        let hash = 5381;
3✔
702
        for (let i = 0; i < seed.length; ++i) {
3✔
703
                const c = seed.charCodeAt(i);
9✔
704
                hash = hash * 33 + c;
9✔
705
                // Make sure we overflow gracefully
706
                hash = hash % Number.MAX_SAFE_INTEGER;
9✔
707
        }
708
        return hash;
3✔
709
}
710

711
const fnRandomNumberGenerator: FunctionDefinitionType = (
9✔
712
        dynamicContext: DynamicContext,
713
        _executionParameters,
714
        _staticContext,
715
        sequence = sequenceFactory.empty()
906✔
716
) => {
717
        const iseed = sequence.isEmpty()
909✔
718
                ? dynamicContext.getRandomNumber()
719
                : dynamicContext.getRandomNumber(
720
                                createSeedFromString(castToType(sequence.first(), ValueType.XSSTRING).value)
721
                  );
722

723
        /**
724
         * Create the random-number-generator object. The seed _must_ be an integer
725
         */
726
        function fnRandomImplementation(iseed: number): ISequence {
727
                const permute: FunctionDefinitionType = (
1,209✔
728
                        _dynamicContext,
729
                        _executionParameters,
730
                        _staticContext,
731
                        sequence
732
                ) => {
733
                        if (sequence.isEmpty() || sequence.isSingleton()) {
300!
UNCOV
734
                                return sequence;
×
735
                        }
736

737
                        const allValues = sequence.getAllValues();
300✔
738
                        let permuteCurrent = iseed;
300✔
739
                        // Use Durstenfeld shuffle to randomize the list
740
                        for (let i = allValues.length - 1; i > 1; i--) {
300✔
741
                                permuteCurrent = dynamicContext.getRandomNumber(permuteCurrent).currentInt;
600✔
742
                                const j = permuteCurrent % i;
600✔
743
                                const t = allValues[j];
600✔
744
                                allValues[j] = allValues[i];
600✔
745
                                allValues[i] = t;
600✔
746
                        }
747

748
                        return sequenceFactory.create(allValues);
300✔
749
                };
750

751
                return sequenceFactory.singleton(
1,209✔
752
                        new MapValue([
753
                                {
754
                                        key: createAtomicValue('number', ValueType.XSSTRING),
755
                                        value: () =>
756
                                                sequenceFactory.singleton(
600✔
757
                                                        createAtomicValue(
758
                                                                dynamicContext.getRandomNumber(iseed).currentDecimal,
759
                                                                ValueType.XSDOUBLE
760
                                                        )
761
                                                ),
762
                                },
763
                                {
764
                                        key: createAtomicValue('next', ValueType.XSSTRING),
765
                                        value: () =>
766
                                                sequenceFactory.singleton(
303✔
767
                                                        new FunctionValue({
768
                                                                value: () =>
769
                                                                        fnRandomImplementation(
300✔
770
                                                                                dynamicContext.getRandomNumber(iseed).currentInt
771
                                                                        ),
772
                                                                isAnonymous: true,
773
                                                                localName: '',
774
                                                                namespaceURI: '',
775
                                                                argumentTypes: [],
776
                                                                arity: 0,
777
                                                                returnType: {
778
                                                                        type: ValueType.MAP,
779
                                                                        mult: SequenceMultiplicity.EXACTLY_ONE,
780
                                                                },
781
                                                        })
782
                                                ),
783
                                },
784
                                {
785
                                        key: createAtomicValue('permute', ValueType.XSSTRING),
786
                                        value: () =>
787
                                                sequenceFactory.singleton(
303✔
788
                                                        new FunctionValue({
789
                                                                value: permute,
790
                                                                isAnonymous: true,
791
                                                                localName: '',
792
                                                                namespaceURI: '',
793
                                                                argumentTypes: [
794
                                                                        {
795
                                                                                type: ValueType.ITEM,
796
                                                                                mult: SequenceMultiplicity.ZERO_OR_MORE,
797
                                                                        },
798
                                                                ],
799
                                                                arity: 1,
800
                                                                returnType: {
801
                                                                        type: ValueType.ITEM,
802
                                                                        mult: SequenceMultiplicity.ZERO_OR_MORE,
803
                                                                },
804
                                                        })
805
                                                ),
806
                                },
807
                        ])
808
                );
809
        }
810
        return fnRandomImplementation(iseed.currentInt);
909✔
811
};
812

813
const declarations: BuiltinDeclarationType[] = [
9✔
814
        {
815
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
816
                localName: 'abs',
817
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
818
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
819
                callFunction: fnAbs,
820
        },
821

822
        {
823
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
824
                localName: 'format-integer',
825
                argumentTypes: [
826
                        { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.ZERO_OR_ONE },
827
                        { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
828
                ],
829
                returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
830
                callFunction: fnFormatInteger,
831
        },
832

833
        {
834
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
835
                localName: 'ceiling',
836
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
837
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
838
                callFunction: fnCeiling,
839
        },
840

841
        {
842
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
843
                localName: 'floor',
844
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
845
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
846
                callFunction: fnFloor,
847
        },
848

849
        {
850
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
851
                localName: 'round',
852
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
853
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
854
                callFunction: fnRound.bind(null, false),
855
        },
856

857
        {
858
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
859
                localName: 'round',
860
                argumentTypes: [
861
                        { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
862
                        { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.EXACTLY_ONE },
863
                ],
864
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
865
                callFunction: fnRound.bind(null, false),
866
        },
867

868
        {
869
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
870
                localName: 'round-half-to-even',
871
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
872
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
873
                callFunction: fnRound.bind(null, true),
874
        },
875

876
        {
877
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
878
                localName: 'round-half-to-even',
879
                argumentTypes: [
880
                        { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
881
                        { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.EXACTLY_ONE },
882
                ],
883
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
884
                callFunction: fnRound.bind(null, true),
885
        },
886

887
        {
888
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
889
                localName: 'number',
890
                argumentTypes: [
891
                        { type: ValueType.XSANYATOMICTYPE, mult: SequenceMultiplicity.ZERO_OR_ONE },
892
                ],
893
                returnType: { type: ValueType.XSDOUBLE, mult: SequenceMultiplicity.EXACTLY_ONE },
894
                callFunction: fnNumber,
895
        },
896

897
        {
898
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
899
                localName: 'number',
900
                argumentTypes: [],
901
                returnType: { type: ValueType.XSDOUBLE, mult: SequenceMultiplicity.EXACTLY_ONE },
902
                callFunction: (dynamicContext, executionParameters, staticContext) => {
903
                        const atomizedContextItem =
904
                                dynamicContext.contextItem &&
9✔
905
                                performFunctionConversion(
906
                                        { type: ValueType.XSANYATOMICTYPE, mult: SequenceMultiplicity.ZERO_OR_ONE },
907
                                        sequenceFactory.singleton(dynamicContext.contextItem),
908
                                        executionParameters,
909
                                        'fn:number',
910
                                        false
911
                                );
912
                        if (!atomizedContextItem) {
9✔
913
                                throw errXPDY0002('fn:number needs an atomizable context item.');
3✔
914
                        }
915
                        return fnNumber(
6✔
916
                                dynamicContext,
917
                                executionParameters,
918
                                staticContext,
919
                                atomizedContextItem
920
                        );
921
                },
922
        },
923

924
        {
925
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
926
                localName: 'random-number-generator',
927
                argumentTypes: [],
928
                returnType: { type: ValueType.MAP, mult: SequenceMultiplicity.EXACTLY_ONE },
929
                callFunction: fnRandomNumberGenerator,
930
        },
931

932
        {
933
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
934
                localName: 'random-number-generator',
935
                argumentTypes: [
936
                        {
937
                                type: ValueType.XSANYATOMICTYPE,
938
                                mult: SequenceMultiplicity.ZERO_OR_ONE,
939
                        },
940
                ],
941
                returnType: { type: ValueType.MAP, mult: SequenceMultiplicity.EXACTLY_ONE },
942
                callFunction: fnRandomNumberGenerator,
943
        },
944
];
945

946
export default {
9✔
947
        declarations,
948
        functions: {
949
                ['number']: fnNumber,
950
                round: fnRound,
951
        },
952
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc