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

FontoXML / fontoxpath / 5938885496

22 Aug 2023 12:38PM UTC coverage: 91.55% (+0.05%) from 91.497%
5938885496

Pull #607

github

drex04
Extend format-integer with additional number styles
Pull Request #607: Format integer RTL

4932 of 5732 branches covered (86.04%)

Branch coverage included in aggregate %.

130 of 131 new or added lines in 1 file covered. (99.24%)

11 existing lines in 1 file now uncovered.

10637 of 11274 relevant lines covered (94.35%)

97683.43 hits per line

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

91.47
/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: string, isLowerCase?: boolean) {
52
        let int = parseInt(integer, 10);
15✔
53

54
        const isNegative = int < 0;
15✔
55

56
        int = Math.abs(int);
15✔
57

58
        if (!int) {
15✔
59
                return '-';
6✔
60
        }
61

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

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

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

76
        return romanString;
9✔
77
}
78

79
function convertIntegerToLowerRoman(integer: string) {
80
        return convertIntegerToRoman(integer, true);
9✔
81
}
82

83
function convertIntegerToUpperRoman(integer: string) {
84
        return convertIntegerToRoman(integer, false);
6✔
85
}
86

87
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
9✔
88

89
function convertIntegerToAlphabet(integer: string, isLowerCase?: boolean) {
90
        let int = parseInt(integer, 10);
24✔
91

92
        const isNegative = int < 0;
24✔
93

94
        int = Math.abs(int);
24✔
95

96
        if (!int) {
24✔
97
                return '-';
6✔
98
        }
99

100
        let output = '';
18✔
101
        let digit;
102

103
        while (int > 0) {
18✔
104
                digit = (int - 1) % ALPHABET.length;
27✔
105
                output = ALPHABET[digit] + output;
27✔
106
                int = ((int - digit) / ALPHABET.length) | 0;
27✔
107
        }
108

109
        if (isLowerCase) {
18✔
110
                output = output.toLowerCase();
12✔
111
        }
112

113
        if (isNegative) {
18✔
114
                output = `-${output}`;
6✔
115
        }
116

117
        return output;
18✔
118
}
119

120
function convertIntegerToLowerAlphabet(integer: string) {
121
        return convertIntegerToAlphabet(integer, true);
15✔
122
}
123

124
function convertIntegerToUpperAlphabet(integer: string) {
125
        return convertIntegerToAlphabet(integer, false);
9✔
126
}
127

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

140
const HEBREW_ALPHABET_UNICODE = getUnicodeSubset(
9✔
141
        0x05d0,
142
        27,
143
        [0x05da, 0x05dd, 0x05df, 0x05e3, 0x05e5]
144
);
145

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

153
const ARABIC_ABJADI_ALPHABET = [
9✔
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
        'ظ',
181
        'غ',
182
];
183

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

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

241
function createUnicodeRangeBasedNumberFormatter(
242
        firstCharacterCharCode: number,
243
        numberOfCharacters: number,
244
        charactersToSkip: number[] = []
×
245
): (integer: string) => string {
246
        charactersToSkip.sort((a, b) => a - b);
18✔
247
        numberOfCharacters = numberOfCharacters - charactersToSkip.length;
18✔
248

249
        return function formatAsNumberInUnicodeRange(integer: string) {
18✔
250
                let int = parseInt(integer, 10);
24✔
251

252
                const isNegative = int < 0;
24✔
253

254
                int = Math.abs(int);
24✔
255

256
                if (!int) {
24✔
257
                        return '-';
6✔
258
                }
259

260
                const formattedNumberParts = [];
18✔
261
                while (int > 0) {
18✔
262
                        const digit = (int - 1) % numberOfCharacters;
27✔
263
                        let charCode = firstCharacterCharCode + digit;
27✔
264

265
                        // 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.
266
                        charactersToSkip.forEach((char) => {
27✔
267
                                if (charCode >= char) {
27!
NEW
268
                                        charCode++;
×
269
                                }
270
                        });
271

272
                        formattedNumberParts.unshift(String.fromCodePoint(charCode));
27✔
273
                        int = Math.floor((int - 1) / numberOfCharacters);
27✔
274
                }
275
                let output = formattedNumberParts.join('');
18✔
276
                if (isNegative) {
18✔
277
                        output = `-${output}`;
3✔
278
                }
279

280
                return output;
18✔
281
        };
282
}
283

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

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

288
// Format as a letter in alphabetical order, using the Hebrew alphabetical order
289
function convertIntegerToHebrewAlefBet(integer: string): string {
290
        let int = parseInt(integer, 10);
105✔
291

292
        const isNegative = int < 0;
105✔
293

294
        int = Math.abs(int);
105✔
295

296
        if (!int) {
105✔
297
                return '-';
3✔
298
        }
299

300
        const multiples = Math.floor((int - 1) / HEBREW_ALPHABET_UNICODE.length);
102✔
301
        // When list runs out of individual characters, repeat the last character in the alphabet
302
        const repeatChar = String.fromCodePoint(0x05ea);
102✔
303
        const formattedNumberParts = Array(multiples).fill(repeatChar);
102✔
304

305
        const digit = (int - 1) % HEBREW_ALPHABET_UNICODE.length;
102✔
306
        const charCode = HEBREW_ALPHABET_UNICODE[digit];
102✔
307
        formattedNumberParts.push(String.fromCodePoint(charCode));
102✔
308

309
        let output = formattedNumberParts.join('');
102✔
310

311
        if (isNegative) {
102✔
312
                output = `-${output}`;
3✔
313
        }
314

315
        return output;
102✔
316
}
317

318
// Format as a letter in alphabetical order, using the Arabic AlifBaTa alphabetical order
319
function convertIntegerToArabicAlifBaTa(integer: string): string {
320
        let int = parseInt(integer, 10);
117✔
321

322
        const isNegative = int < 0;
117✔
323

324
        int = Math.abs(int);
117✔
325

326
        if (!int) {
117✔
327
                return '-';
3✔
328
        }
329

330
        const multiples = Math.floor((int - 1) / ARABIC_ALPHABET_UNICODE.length) + 1;
114✔
331
        const digit = (int - 1) % ARABIC_ALPHABET_UNICODE.length;
114✔
332
        const charCode = ARABIC_ALPHABET_UNICODE[digit];
114✔
333

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

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

340
        if (isNegative) {
114✔
341
                output = `-${output}`;
3✔
342
        }
343

344
        return output;
114✔
345
}
346

347
// Format as a letter in alphabetical order, using the Arabic Abjadi alphabetical order
348
function convertIntegerToArabicAbjadi(integer: string): string {
349
        let int = parseInt(integer, 10);
117✔
350

351
        const isNegative = int < 0;
117✔
352

353
        int = Math.abs(int);
117✔
354

355
        if (!int) {
117✔
356
                return '-';
3✔
357
        }
358

359
        const multiples = Math.floor((int - 1) / ARABIC_ABJADI_ALPHABET.length) + 1;
114✔
360
        const digit = (int - 1) % ARABIC_ABJADI_ALPHABET.length;
114✔
361

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

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

368
        if (isNegative) {
114✔
369
                output = `-${output}`;
3✔
370
        }
371

372
        return output;
114✔
373
}
374

375
function convertIntegerToAbjadNumeral(integer: string): string {
376
        let int = parseInt(integer, 10);
150✔
377

378
        const isNegative = int < 0;
150✔
379

380
        int = Math.abs(int);
150✔
381

382
        if (!int) {
150✔
383
                return '-';
3✔
384
        }
385

386
        const arabicAbjadNumberParts = [];
147✔
387
        let thousands = Math.floor(int / 1000);
147✔
388
        let remainder = int - thousands * 1000;
147✔
389

390
        if (thousands === 1) {
147✔
391
                // Push the 1000 numeral
392
                arabicAbjadNumberParts.push(DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE[0][1]);
9✔
393
        } else if (thousands > 1) {
138✔
394
                // Push the multiplicative thousands place
395
                for (const [val, char] of DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE) {
18✔
396
                        while (thousands >= val) {
504✔
397
                                arabicAbjadNumberParts.push(char);
21✔
398
                                thousands -= val;
21✔
399
                        }
400
                }
401
                // Then push the 1000 numeral
402
                arabicAbjadNumberParts.push(DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE[0][1]);
18✔
403
        }
404

405
        // Then append all numbers lower than 999
406
        for (const [val, char] of DECIMAL_TO_ARABIC_ABJAD_NUMBER_TABLE) {
147✔
407
                while (remainder >= val) {
4,116✔
408
                        remainder -= val;
207✔
409
                        arabicAbjadNumberParts.push(char);
207✔
410
                }
411
        }
412
        let output = arabicAbjadNumberParts.join('');
147✔
413

414
        if (isNegative) {
147✔
415
                output = `-${output}`;
3✔
416
        }
417

418
        return output;
147✔
419
}
420

421
function convertIntegerToHebrewNumeral(integer: string): string {
422
        let int = parseInt(integer, 10);
144✔
423

424
        const isNegative = int < 0;
144✔
425

426
        int = Math.abs(int);
144✔
427

428
        if (!int) {
144✔
429
                return '-';
3✔
430
        }
431

432
        const hebrewNumberParts = [];
141✔
433

434
        // In the number system, 400 is the largest numeral and is repeated when numbers grow larger than 799
435
        const fourHundreds = Math.floor(int / 400);
141✔
436
        let remainder = int - fourHundreds * 400;
141✔
437

438
        // Push a 'ת' character for each additional multiple of 400
439
        for (let i = 0; i < fourHundreds; i++) {
141✔
440
                hebrewNumberParts.push('ת');
33✔
441
        }
442
        // Then add the numbers smaller than 400
443
        for (const [val, char] of DECIMAL_TO_HEBREW_ALEFBET_NUMBER_TABLE) {
141✔
444
                while (remainder >= val) {
3,102✔
445
                        remainder -= val;
231✔
446
                        hebrewNumberParts.push(char);
231✔
447
                }
448
        }
449

450
        // If the last two digits are 15 or 16, substitute these characters because they are an exception in the Hebrew number system
451
        // For 15, substitute (10+5) with (9+6)
452
        const lastTwoChars = hebrewNumberParts.slice(-2).join('');
141✔
453
        if (lastTwoChars === 'יה') {
141✔
454
                hebrewNumberParts.splice(-2, 2, 'ט', 'ו');
12✔
455
        }
456
        // For 16, substitute (10+5) with (9+7)
457
        if (lastTwoChars === 'יו') {
141✔
458
                hebrewNumberParts.splice(-2, 2, 'ט', 'ז');
12✔
459
        }
460

461
        let output = hebrewNumberParts.join('');
141✔
462

463
        if (isNegative) {
141✔
464
                output = `-${output}`;
3✔
465
        }
466

467
        return output;
141✔
468
}
469

470
function convertIntegerToArabicIndicNumeral(integer: string): string {
471
        return new Intl.NumberFormat([], {
90✔
472
                // @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.
473
                numberingSystem: 'arab',
474
                useGrouping: false,
475
        }).format(parseInt(integer, 10));
476
}
477

478
function convertIntegerToPersianNumeral(integer: string): string {
479
        return new Intl.NumberFormat([], {
90✔
480
                // @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.
481
                numberingSystem: 'arabext',
482
                useGrouping: false,
483
        }).format(parseInt(integer, 10));
484
}
485

486
const fnAbs: FunctionDefinitionType = (
9✔
487
        _dynamicContext,
488
        _executionParameters,
489
        _staticContext,
490
        sequence
491
) => {
492
        return sequence.map((onlyValue) =>
9✔
493
                createValidNumericType(onlyValue.type, Math.abs(onlyValue.value))
6✔
494
        );
495
};
496

497
const integerConverters = new Map([
9✔
498
        ['A', convertIntegerToUpperAlphabet],
499
        ['a', convertIntegerToLowerAlphabet],
500
        ['I', convertIntegerToUpperRoman],
501
        ['i', convertIntegerToLowerRoman],
502
        ['lowerGreek', convertIntegerToLowerGreek],
503
        ['&#x03b1;', convertIntegerToLowerGreek],
504
        ['upperGreek', convertIntegerToUpperGreek],
505
        ['&#x0391;', convertIntegerToUpperGreek],
506
        ['arabicAbjadi', convertIntegerToArabicAbjadi],
507
        ['arabicAbjadNumeral', convertIntegerToAbjadNumeral],
508
        ['arabicAlifBaTa', convertIntegerToArabicAlifBaTa],
509
        ['hebrewAlefBet', convertIntegerToHebrewAlefBet],
510
        ['hebrewNumeral', convertIntegerToHebrewNumeral],
511
        ['arabicIndicNumeral', convertIntegerToArabicIndicNumeral],
512
        ['&#x661;', convertIntegerToArabicIndicNumeral],
513
        ['&#x662;', convertIntegerToArabicIndicNumeral],
514
        ['&#x663;', convertIntegerToArabicIndicNumeral],
515
        ['&#x664;', convertIntegerToArabicIndicNumeral],
516
        ['&#x665;', convertIntegerToArabicIndicNumeral],
517
        ['&#x666;', convertIntegerToArabicIndicNumeral],
518
        ['&#x667;', convertIntegerToArabicIndicNumeral],
519
        ['&#x668;', convertIntegerToArabicIndicNumeral],
520
        ['&#x669;', convertIntegerToArabicIndicNumeral],
521
        ['persianNumeral', convertIntegerToPersianNumeral],
522
        ['&#x6f1;', convertIntegerToPersianNumeral],
523
        ['&#x6f2;', convertIntegerToPersianNumeral],
524
        ['&#x6f3;', convertIntegerToPersianNumeral],
525
        ['&#x6f4;', convertIntegerToPersianNumeral],
526
        ['&#x6f5;', convertIntegerToPersianNumeral],
527
        ['&#x6f6;', convertIntegerToPersianNumeral],
528
        ['&#x6f7;', convertIntegerToPersianNumeral],
529
        ['&#x6f8;', convertIntegerToPersianNumeral],
530
        ['&#x6f9;', convertIntegerToPersianNumeral],
531
]);
532

533
const fnFormatInteger: FunctionDefinitionType = (
9✔
534
        _dynamicContext,
535
        _executionParameters,
536
        _staticContext,
537
        sequence,
538
        pictureSequence
539
) => {
540
        const sequenceValue = sequence.first();
918✔
541
        const pictureValue = pictureSequence.first();
918✔
542

543
        if (sequence.isEmpty()) {
918✔
544
                return sequenceFactory.singleton(createAtomicValue('', ValueType.XSSTRING));
39✔
545
        }
546

547
        const integerConverter = integerConverters.get(pictureValue.value);
879✔
548

549
        if (integerConverter) {
879✔
550
                const convertedString = integerConverter(sequenceValue.value);
876✔
551
                return sequenceFactory.singleton(createAtomicValue(convertedString, ValueType.XSSTRING));
876✔
552
        } else {
553
                const validPictureStrings = Array.from(
3✔
554
                        integerConverters,
555
                        ([pictureString, _]) => pictureString
99✔
556
                );
557
                throw new Error(
3✔
558
                        `Picture: ${
559
                                pictureValue.value
560
                        } is not implemented yet. The supported picture strings are ${
561
                                validPictureStrings.slice(0, -1).join(', ') +
562
                                ' and ' +
563
                                validPictureStrings.slice(-1)
564
                        }.`
565
                );
566
        }
567
};
568

569
const fnCeiling: FunctionDefinitionType = (
9✔
570
        _dynamicContext,
571
        _executionParameters,
572
        _staticContext,
573
        sequence
574
) => {
UNCOV
575
        return sequence.map((onlyValue) =>
×
UNCOV
576
                createValidNumericType(onlyValue.type, Math.ceil(onlyValue.value))
×
577
        );
578
};
579

580
const fnFloor: FunctionDefinitionType = (
9✔
581
        _dynamicContext,
582
        _executionParameters,
583
        _staticContext,
584
        sequence
585
) => {
UNCOV
586
        return sequence.map((onlyValue) =>
×
UNCOV
587
                createValidNumericType(onlyValue.type, Math.floor(onlyValue.value))
×
588
        );
589
};
590

591
function isHalf(value: number, scaling: number) {
592
        return ((value * scaling) % 1) % 0.5 === 0;
39✔
593
}
594

595
function getNumberOfDecimalDigits(value: number) {
596
        if (Math.floor(value) === value || isNaN(value)) {
93✔
597
                return 0;
57✔
598
        }
599

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

603
        if (result[3]) {
36!
UNCOV
604
                if (result[2]) {
×
UNCOV
605
                        return decimals + parseInt(result[3], 10);
×
606
                }
UNCOV
607
                const returnVal = decimals - parseInt(result[3], 10);
×
UNCOV
608
                return returnVal < 0 ? 0 : returnVal;
×
609
        }
610
        return decimals;
36✔
611
}
612

613
function determineRoundedNumber(itemAsDecimal: number, halfToEven: boolean, scaling: number) {
614
        if (halfToEven && isHalf(itemAsDecimal, scaling)) {
84✔
615
                if (Math.floor(itemAsDecimal * scaling) % 2 === 0) {
24✔
616
                        return Math.floor(itemAsDecimal * scaling) / scaling;
15✔
617
                }
618
                return Math.ceil(itemAsDecimal * scaling) / scaling;
9✔
619
        }
620
        return Math.round(itemAsDecimal * scaling) / scaling;
60✔
621
}
622

623
function fnRound(
624
        halfToEven: boolean,
625
        _dynamicContext: DynamicContext,
626
        _executionParameters: ExecutionParameters,
627
        _staticContext: StaticContext,
628
        sequence: ISequence,
629
        precision: ISequence
630
): ISequence {
631
        let done = false;
126✔
632
        return sequenceFactory.create({
126✔
633
                next: () => {
634
                        if (done) {
189✔
635
                                return DONE_TOKEN;
72✔
636
                        }
637
                        const firstValue = sequence.first();
117✔
638
                        if (!firstValue) {
117✔
639
                                // Empty sequence
640
                                done = true;
12✔
641
                                return DONE_TOKEN;
12✔
642
                        }
643

644
                        if (
105✔
645
                                (isSubtypeOf(firstValue.type, ValueType.XSFLOAT) ||
420✔
646
                                        isSubtypeOf(firstValue.type, ValueType.XSDOUBLE)) &&
647
                                (firstValue.value === 0 ||
648
                                        isNaN(firstValue.value as number) ||
649
                                        firstValue.value === +Infinity ||
650
                                        firstValue.value === -Infinity)
651
                        ) {
652
                                done = true;
12✔
653
                                return ready(firstValue);
12✔
654
                        }
655
                        let scalingPrecision;
656
                        if (precision) {
93✔
657
                                const sp = precision.first();
21✔
658
                                scalingPrecision = sp.value;
21✔
659
                        } else {
660
                                scalingPrecision = 0;
72✔
661
                        }
662
                        done = true;
93✔
663

664
                        if (getNumberOfDecimalDigits(firstValue.value as number) < scalingPrecision) {
93✔
665
                                return ready(firstValue);
9✔
666
                        }
667

668
                        const originalType = [
84✔
669
                                ValueType.XSINTEGER,
670
                                ValueType.XSDECIMAL,
671
                                ValueType.XSDOUBLE,
672
                                ValueType.XSFLOAT,
673
                        ].find((type: ValueType) => {
674
                                return isSubtypeOf(firstValue.type, type);
189✔
675
                        });
676
                        const itemAsDecimal = castToType(firstValue, ValueType.XSDECIMAL);
84✔
677
                        const scaling = Math.pow(10, scalingPrecision);
84✔
678
                        const roundedNumber = determineRoundedNumber(itemAsDecimal.value, halfToEven, scaling);
84✔
679
                        switch (originalType) {
84!
680
                                case ValueType.XSDECIMAL:
681
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSDECIMAL));
27✔
682
                                case ValueType.XSDOUBLE:
683
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSDOUBLE));
39✔
684
                                case ValueType.XSFLOAT:
UNCOV
685
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSFLOAT));
×
686
                                case ValueType.XSINTEGER:
687
                                        return ready(createAtomicValue(roundedNumber, ValueType.XSINTEGER));
18✔
688
                        }
689
                },
690
        });
691
}
692

693
const fnNumber: FunctionDefinitionType = (
9✔
694
        _dynamicContext,
695
        executionParameters,
696
        _staticContext,
697
        sequence
698
) => {
699
        return atomize(sequence, executionParameters).switchCases({
42✔
700
                empty: () => sequenceFactory.singleton(createAtomicValue(NaN, ValueType.XSDOUBLE)),
9✔
701
                singleton: () => {
702
                        const castResult = tryCastToType(sequence.first(), ValueType.XSDOUBLE);
30✔
703
                        if (castResult.successful) {
30✔
704
                                return sequenceFactory.singleton(castResult.value);
21✔
705
                        }
706
                        return sequenceFactory.singleton(createAtomicValue(NaN, ValueType.XSDOUBLE));
9✔
707
                },
708
                multiple: () => {
UNCOV
709
                        throw new Error('fn:number may only be called with zero or one values');
×
710
                },
711
        });
712
};
713

714
// This is the djb2 algorithm, taken from http://www.cse.yorku.ca/~oz/hash.html
715
function createSeedFromString(seed: string): number {
716
        let hash = 5381;
3✔
717
        for (let i = 0; i < seed.length; ++i) {
3✔
718
                const c = seed.charCodeAt(i);
9✔
719
                hash = hash * 33 + c;
9✔
720
                // Make sure we overflow gracefully
721
                hash = hash % Number.MAX_SAFE_INTEGER;
9✔
722
        }
723
        return hash;
3✔
724
}
725

726
const fnRandomNumberGenerator: FunctionDefinitionType = (
9✔
727
        dynamicContext: DynamicContext,
728
        _executionParameters,
729
        _staticContext,
730
        sequence = sequenceFactory.empty()
906✔
731
) => {
732
        const iseed = sequence.isEmpty()
909✔
733
                ? dynamicContext.getRandomNumber()
734
                : dynamicContext.getRandomNumber(
735
                                createSeedFromString(castToType(sequence.first(), ValueType.XSSTRING).value)
736
                  );
737

738
        /**
739
         * Create the random-number-generator object. The seed _must_ be an integer
740
         */
741
        function fnRandomImplementation(iseed: number): ISequence {
742
                const permute: FunctionDefinitionType = (
1,209✔
743
                        _dynamicContext,
744
                        _executionParameters,
745
                        _staticContext,
746
                        sequence
747
                ) => {
748
                        if (sequence.isEmpty() || sequence.isSingleton()) {
300!
UNCOV
749
                                return sequence;
×
750
                        }
751

752
                        const allValues = sequence.getAllValues();
300✔
753
                        let permuteCurrent = iseed;
300✔
754
                        // Use Durstenfeld shuffle to randomize the list
755
                        for (let i = allValues.length - 1; i > 1; i--) {
300✔
756
                                permuteCurrent = dynamicContext.getRandomNumber(permuteCurrent).currentInt;
600✔
757
                                const j = permuteCurrent % i;
600✔
758
                                const t = allValues[j];
600✔
759
                                allValues[j] = allValues[i];
600✔
760
                                allValues[i] = t;
600✔
761
                        }
762

763
                        return sequenceFactory.create(allValues);
300✔
764
                };
765

766
                return sequenceFactory.singleton(
1,209✔
767
                        new MapValue([
768
                                {
769
                                        key: createAtomicValue('number', ValueType.XSSTRING),
770
                                        value: () =>
771
                                                sequenceFactory.singleton(
600✔
772
                                                        createAtomicValue(
773
                                                                dynamicContext.getRandomNumber(iseed).currentDecimal,
774
                                                                ValueType.XSDOUBLE
775
                                                        )
776
                                                ),
777
                                },
778
                                {
779
                                        key: createAtomicValue('next', ValueType.XSSTRING),
780
                                        value: () =>
781
                                                sequenceFactory.singleton(
303✔
782
                                                        new FunctionValue({
783
                                                                value: () =>
784
                                                                        fnRandomImplementation(
300✔
785
                                                                                dynamicContext.getRandomNumber(iseed).currentInt
786
                                                                        ),
787
                                                                isAnonymous: true,
788
                                                                localName: '',
789
                                                                namespaceURI: '',
790
                                                                argumentTypes: [],
791
                                                                arity: 0,
792
                                                                returnType: {
793
                                                                        type: ValueType.MAP,
794
                                                                        mult: SequenceMultiplicity.EXACTLY_ONE,
795
                                                                },
796
                                                        })
797
                                                ),
798
                                },
799
                                {
800
                                        key: createAtomicValue('permute', ValueType.XSSTRING),
801
                                        value: () =>
802
                                                sequenceFactory.singleton(
303✔
803
                                                        new FunctionValue({
804
                                                                value: permute,
805
                                                                isAnonymous: true,
806
                                                                localName: '',
807
                                                                namespaceURI: '',
808
                                                                argumentTypes: [
809
                                                                        {
810
                                                                                type: ValueType.ITEM,
811
                                                                                mult: SequenceMultiplicity.ZERO_OR_MORE,
812
                                                                        },
813
                                                                ],
814
                                                                arity: 1,
815
                                                                returnType: {
816
                                                                        type: ValueType.ITEM,
817
                                                                        mult: SequenceMultiplicity.ZERO_OR_MORE,
818
                                                                },
819
                                                        })
820
                                                ),
821
                                },
822
                        ])
823
                );
824
        }
825
        return fnRandomImplementation(iseed.currentInt);
909✔
826
};
827

828
const declarations: BuiltinDeclarationType[] = [
9✔
829
        {
830
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
831
                localName: 'abs',
832
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
833
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
834
                callFunction: fnAbs,
835
        },
836

837
        {
838
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
839
                localName: 'format-integer',
840
                argumentTypes: [
841
                        { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.ZERO_OR_ONE },
842
                        { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
843
                ],
844
                returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
845
                callFunction: fnFormatInteger,
846
        },
847

848
        {
849
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
850
                localName: 'ceiling',
851
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
852
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
853
                callFunction: fnCeiling,
854
        },
855

856
        {
857
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
858
                localName: 'floor',
859
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
860
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
861
                callFunction: fnFloor,
862
        },
863

864
        {
865
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
866
                localName: 'round',
867
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
868
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
869
                callFunction: fnRound.bind(null, false),
870
        },
871

872
        {
873
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
874
                localName: 'round',
875
                argumentTypes: [
876
                        { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
877
                        { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.EXACTLY_ONE },
878
                ],
879
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
880
                callFunction: fnRound.bind(null, false),
881
        },
882

883
        {
884
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
885
                localName: 'round-half-to-even',
886
                argumentTypes: [{ type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE }],
887
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
888
                callFunction: fnRound.bind(null, true),
889
        },
890

891
        {
892
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
893
                localName: 'round-half-to-even',
894
                argumentTypes: [
895
                        { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
896
                        { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.EXACTLY_ONE },
897
                ],
898
                returnType: { type: ValueType.XSNUMERIC, mult: SequenceMultiplicity.ZERO_OR_ONE },
899
                callFunction: fnRound.bind(null, true),
900
        },
901

902
        {
903
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
904
                localName: 'number',
905
                argumentTypes: [
906
                        { type: ValueType.XSANYATOMICTYPE, mult: SequenceMultiplicity.ZERO_OR_ONE },
907
                ],
908
                returnType: { type: ValueType.XSDOUBLE, mult: SequenceMultiplicity.EXACTLY_ONE },
909
                callFunction: fnNumber,
910
        },
911

912
        {
913
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
914
                localName: 'number',
915
                argumentTypes: [],
916
                returnType: { type: ValueType.XSDOUBLE, mult: SequenceMultiplicity.EXACTLY_ONE },
917
                callFunction: (dynamicContext, executionParameters, staticContext) => {
918
                        const atomizedContextItem =
919
                                dynamicContext.contextItem &&
9✔
920
                                performFunctionConversion(
921
                                        { type: ValueType.XSANYATOMICTYPE, mult: SequenceMultiplicity.ZERO_OR_ONE },
922
                                        sequenceFactory.singleton(dynamicContext.contextItem),
923
                                        executionParameters,
924
                                        'fn:number',
925
                                        false
926
                                );
927
                        if (!atomizedContextItem) {
9✔
928
                                throw errXPDY0002('fn:number needs an atomizable context item.');
3✔
929
                        }
930
                        return fnNumber(
6✔
931
                                dynamicContext,
932
                                executionParameters,
933
                                staticContext,
934
                                atomizedContextItem
935
                        );
936
                },
937
        },
938

939
        {
940
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
941
                localName: 'random-number-generator',
942
                argumentTypes: [],
943
                returnType: { type: ValueType.MAP, mult: SequenceMultiplicity.EXACTLY_ONE },
944
                callFunction: fnRandomNumberGenerator,
945
        },
946

947
        {
948
                namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
949
                localName: 'random-number-generator',
950
                argumentTypes: [
951
                        {
952
                                type: ValueType.XSANYATOMICTYPE,
953
                                mult: SequenceMultiplicity.ZERO_OR_ONE,
954
                        },
955
                ],
956
                returnType: { type: ValueType.MAP, mult: SequenceMultiplicity.EXACTLY_ONE },
957
                callFunction: fnRandomNumberGenerator,
958
        },
959
];
960

961
export default {
9✔
962
        declarations,
963
        functions: {
964
                ['number']: fnNumber,
965
                round: fnRound,
966
        },
967
};
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