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

SignpostMarv / Intermediary-Number / 9518994539

14 Jun 2024 04:01PM UTC coverage: 95.423% (+1.2%) from 94.248%
9518994539

push

github

SignpostMarv
satisfying eslint

384 of 398 branches covered (96.48%)

Branch coverage included in aggregate %.

1972 of 2071 relevant lines covered (95.22%)

767.69 hits per line

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

94.72
/lib/IntermediaryNumber.ts
1
import assert from 'assert';
1✔
2
import BigNumber from 'bignumber.js';
1✔
3
import Fraction from 'fraction.js';
1✔
4
import {
1✔
5
        is_string,
1✔
6
} from './Docs.json';
1✔
7
import {
1✔
8
        amount_string,
1✔
9
        NumberStrings,
1✔
10
} from './NumberStrings';
1✔
11
import type {
1✔
12
        input_types,
1✔
13
        type_property_types,
1✔
14
        value_types,
1✔
15
} from './IntermediaryNumberTypes';
1✔
16

1✔
17
//#region Types
1✔
18

1✔
19
export type math_types =
1✔
20
        | operand_types
1✔
21
        | input_types;
1✔
22

1✔
23
export const regex_recurring_number =
1✔
24
        /^-?(\d+\.)(\d+r|\d*\[\d+\]r?|\d*\(\d+\)r?)$/;
1✔
25

1✔
26
export type CanDoMath_result_types =
1✔
27
        | IntermediaryNumber
1✔
28
        | IntermediaryCalculation;
1✔
29

1✔
30
export type operation_types =
1✔
31
        | '+'
1✔
32
        | '-'
1✔
33
        | '*'
1✔
34
        | 'x'
1✔
35
        | '/'
1✔
36
        | '%';
1✔
37

1✔
38
export type operand_type_property_types =
1✔
39
        | type_property_types
1✔
40
        | 'IntermediaryCalculation';
1✔
41

1✔
42
export type CanConvertTypeJson =
1✔
43
        | {
1✔
44
                type: 'IntermediaryNumber',
1✔
45
                value: string,
1✔
46
        }
1✔
47
        | {
1✔
48
                type: 'IntermediaryCalculation',
1✔
49
                left: CanConvertTypeJson,
1✔
50
                operation: operation_types,
1✔
51
                right: CanConvertTypeJson,
1✔
52
        };
1✔
53

1✔
54
export type CanDoMathWithDispose_operator_types =
1✔
55
        | 'divide'
1✔
56
        | 'minus'
1✔
57
        | 'modulo'
1✔
58
        | 'plus'
1✔
59
        | 'times';
1✔
60

1✔
61
export type operand_types =
1✔
62
        | IntermediaryNumber
1✔
63
        | IntermediaryCalculation;
1✔
64

1✔
65
//#region TokenScan types
1✔
66

1✔
67
type TokenSpan_types =
1✔
68
        | 'ignore'
1✔
69
        | 'nesting_open'
1✔
70
        | 'nesting_close'
1✔
71
        | 'numeric'
1✔
72
        | 'operation'
1✔
73

1✔
74
type TokenSpan_types_part_baked = Exclude<
1✔
75
        TokenSpan_types,
1✔
76
        | 'ignore'
1✔
77
>;
1✔
78

1✔
79
type TokenScan_internals = {
1✔
80
        parsed: IntermediaryNumber|IntermediaryCalculation|undefined,
1✔
81
        tokens: (TokenSpan<TokenSpan_types_part_baked>[])|undefined,
1✔
82
        valid: boolean|undefined,
1✔
83
};
1✔
84

1✔
85
type TokenScan_parsing_tokens = Omit<TokenScan, 'is_valid'|'tokens'|'parsed'>;
1✔
86
type TokenScan_parsing_value = Omit<TokenScan, 'is_valid'|'parsed'>;
1✔
87

1✔
88
type TokenScan_tokenizer_operand_buffer =
1✔
89
        | IntermediaryNumber
1✔
90
        | IntermediaryCalculation
1✔
91
        | undefined;
1✔
92

1✔
93
type incomplete_operation = {
1✔
94
        left_operand: Exclude<
1✔
95
                TokenScan_tokenizer_operand_buffer,
1✔
96
                undefined
1✔
97
        >,
1✔
98
        operation: operation_types,
1✔
99
}
1✔
100

1✔
101
type TokenScan_tokenizer = {
1✔
102
        outter_stack: (
1✔
103
                | incomplete_operation
1✔
104
                | TokenSpan<'nesting_open'>
1✔
105
        )[],
1✔
106
        left_operand: TokenScan_tokenizer_operand_buffer,
1✔
107
        right_operand: TokenScan_tokenizer_operand_buffer,
1✔
108
        operation: ''|operation_types,
1✔
109
        operand_mode: 'left'|'right',
1✔
110
}
1✔
111

1✔
112
//#endregion
1✔
113

1✔
114
//#endregion
1✔
115

1✔
116
//#region interfaces
1✔
117

1✔
118
interface HasType
1✔
119
{
1✔
120
        get type(): operand_type_property_types;
1✔
121
}
1✔
122

1✔
123
interface CanDoMath<
1✔
124
        ResultType extends CanDoMath_result_types = CanDoMath_result_types,
1✔
125
        ResolveString extends string = type_property_types
1✔
126
> extends HasType {
1✔
127
        get resolve_type(): ResolveString;
1✔
128

1✔
129
        compare(
1✔
130
                value:math_types
1✔
131
        ): -1|0|1;
1✔
132

1✔
133
        divide(
1✔
134
                value:math_types
1✔
135
        ): ResultType;
1✔
136

1✔
137
        minus(
1✔
138
                value:math_types
1✔
139
        ): ResultType;
1✔
140

1✔
141
        modulo(
1✔
142
                value:math_types
1✔
143
        ): ResultType;
1✔
144

1✔
145
        plus(
1✔
146
                value:math_types
1✔
147
        ): ResultType;
1✔
148

1✔
149
        times(
1✔
150
                value:math_types
1✔
151
        ): ResultType;
1✔
152

1✔
153
        abs(): (
1✔
154
                | IntermediaryCalculation
1✔
155
                | IntermediaryNumber
1✔
156
        );
1✔
157

1✔
158
        max(
1✔
159
                first: math_types,
1✔
160
                ...remaining: math_types[]
1✔
161
        ): math_types;
1✔
162
}
1✔
163

1✔
164
interface CanResolveMath<
1✔
165
        T extends CanDoMath_result_types = CanDoMath_result_types
1✔
166
> extends CanDoMath<
1✔
167
        T,
1✔
168
        string
1✔
169
> {
1✔
170
        resolve(): IntermediaryNumber;
1✔
171
}
1✔
172

1✔
173
export interface CanConvertType extends HasType
1✔
174
{
1✔
175
        toAmountString(): amount_string;
1✔
176

1✔
177
        toBigNumber(): BigNumber;
1✔
178

1✔
179
        toBigNumberOrFraction(): BigNumber|Fraction;
1✔
180

1✔
181
        toFraction(): Fraction;
1✔
182

1✔
183
        toString(): string;
1✔
184

1✔
185
        isLessThan(value:math_types): boolean;
1✔
186

1✔
187
        isGreaterThan(value:math_types): boolean;
1✔
188

1✔
189
        isOne(): boolean;
1✔
190

1✔
191
        isZero(): boolean;
1✔
192

1✔
193
        toJSON(): CanConvertTypeJson;
1✔
194
}
1✔
195

1✔
196
interface CanDoMathWithDispose<
1✔
197
        ResultType extends CanDoMath_result_types = CanDoMath_result_types,
1✔
198
        ResolveString extends string = type_property_types
1✔
199
> extends CanConvertType, CanDoMath<
1✔
200
        ResultType,
1✔
201
        ResolveString
1✔
202
> {
1✔
203
        do_math_then_dispose(
1✔
204
                operator: CanDoMathWithDispose_operator_types,
1✔
205
                right_operand: math_types
1✔
206
        ): ResultType;
1✔
207
}
1✔
208

1✔
209
interface CanResolveMathWithDispose<
1✔
210
        T extends CanDoMath_result_types = CanDoMath_result_types
1✔
211
> extends
1✔
212
        CanResolveMath<T>,
1✔
213
        CanDoMathWithDispose<T, string>
1✔
214
{
1✔
215
}
1✔
216

1✔
217
//#endregion
1✔
218

1✔
219
//#region utility functions
1✔
220

1✔
221
function do_math(
36✔
222
        left_operand: IntermediaryNumber|IntermediaryCalculation,
36✔
223
        operator: operation_types,
36✔
224
        right_operand: math_types
36✔
225
) : operand_types {
36✔
226
        return IntermediaryCalculation.maybe_reduce_operands(
36✔
227
                left_operand,
36✔
228
                operator,
36✔
229
                IntermediaryNumber.reuse_or_create(right_operand)
36✔
230
        );
36✔
231
}
36✔
232

1✔
233
function abs(
8✔
234
        value:
8✔
235
                | operand_types
8✔
236
): operand_types {
8✔
237
        if (value.isZero()) {
8✔
238
                return value;
3✔
239
        }
3✔
240

5✔
241
        return (value.isLessThan(0)
5✔
242
                ? IntermediaryNumber.Zero.minus(
8✔
243
                        value
3✔
244
                )
8✔
245
                : value
8✔
246
        );
8✔
247
}
8✔
248

1✔
249
function compare(
236✔
250
        value: math_types,
236✔
251
        to: CanConvertType
236✔
252
): 0|1|-1 {
236✔
253
        const comparable = IntermediaryNumber.reuse_or_create(
236✔
254
                value
236✔
255
        ).toBigNumberOrFraction();
236✔
256

236✔
257
        let result:number|null;
236✔
258

236✔
259
        if (comparable instanceof BigNumber) {
236✔
260
                result = to.toBigNumber().comparedTo(comparable);
232✔
261
        } else {
236✔
262
                result = to.toFraction().compare(comparable);
4✔
263
        }
4✔
264

236✔
265
        assert.strictEqual(
236✔
266
                (
236✔
267
                        -1 === result
236✔
268
                        || 0 === result
236✔
269
                        || 1 === result
236✔
270
                ),
236✔
271
                true,
236✔
272
                `Expecting -1, 0, or 1, receieved ${JSON.stringify(result)}`
236✔
273
        );
236✔
274

236✔
275
        return result as -1|0|1;
236✔
276
}
236✔
277

1✔
278
const conversion_cache = new class {
1✔
279
        private toAmountString_cache:undefined|WeakMap<
5✔
280
                CanConvertType,
5✔
281
                amount_string
5✔
282
        >;
5✔
283
        private toBigNumber_cache:WeakMap<CanConvertType, BigNumber>|undefined;
5✔
284
        private toFraction_cache:WeakMap<CanConvertType, Fraction>|undefined;
5✔
285
        private toString_cache:WeakMap<CanConvertType, string>|undefined;
5✔
286

5✔
287
        get AmountString(): WeakMap<CanConvertType, amount_string>
5✔
288
        {
3✔
289
                if (!this.toAmountString_cache) {
3✔
290
                        this.toAmountString_cache = new WeakMap();
2✔
291
                }
2✔
292

3✔
293
                return this.toAmountString_cache;
3✔
294
        }
3✔
295

5✔
296
        get BigNumber(): WeakMap<CanConvertType, BigNumber>
5✔
297
        {
1,378✔
298
                if (!this.toBigNumber_cache) {
1,378✔
299
                        this.toBigNumber_cache = new WeakMap();
4✔
300
                }
4✔
301

1,378✔
302
                return this.toBigNumber_cache;
1,378✔
303
        }
1,378✔
304

5✔
305
        get Fraction(): WeakMap<CanConvertType, Fraction>
5✔
306
        {
974✔
307
                if (!this.toFraction_cache) {
974✔
308
                        this.toFraction_cache = new WeakMap();
4✔
309
                }
4✔
310

974✔
311
                return this.toFraction_cache;
974✔
312
        }
974✔
313

5✔
314
        get String(): WeakMap<CanConvertType, string>
5✔
315
        {
475✔
316
                if (!this.toString_cache) {
475✔
317
                        this.toString_cache = new WeakMap();
3✔
318
                }
3✔
319

475✔
320
                return this.toString_cache;
475✔
321
        }
475✔
322

5✔
323
        dispose(of:CanConvertType)
5✔
324
        {
3✔
325
                for (const cache of [
3✔
326
                        this.toAmountString_cache,
3✔
327
                        this.toBigNumber_cache,
3✔
328
                        this.toFraction_cache,
3✔
329
                        this.toString_cache,
3✔
330
                ]) {
3✔
331
                        if (cache) {
12✔
332
                                cache.delete(of);
9✔
333
                        }
9✔
334
                }
12✔
335
        }
3✔
336
}
5✔
337

1✔
338
export function dispose(value:operand_types)
1✔
339
{
3✔
340
        conversion_cache.dispose(value);
3✔
341
}
3✔
342

1✔
343
function max(
4✔
344
        first: math_types,
4✔
345
        second: math_types,
4✔
346
        ...remaining: math_types[]
4✔
347
): math_types {
4✔
348
        let max = IntermediaryNumber.reuse_or_create(first);
4✔
349

4✔
350
        for (const entry of [second, ...remaining]) {
4✔
351
                const maybe = IntermediaryNumber.reuse_or_create(entry);
15✔
352
                if (-1 === max.compare(maybe)) {
15✔
353
                        max = maybe;
9✔
354
                }
9✔
355
        }
15✔
356

4✔
357
        return IntermediaryNumber.reuse_or_create(max);
4✔
358
}
4✔
359

1✔
360
//#region TokenScan utility functions
1✔
361

1✔
362
function default_tokenizer_state(): TokenScan_tokenizer {
746✔
363
        return {
746✔
364
                outter_stack: [],
746✔
365
                left_operand: undefined,
746✔
366
                operation: '',
746✔
367
                right_operand: undefined,
746✔
368
                operand_mode: 'left',
746✔
369
        }
746✔
370
}
746✔
371

1✔
372
function is_nesting_open(
7,526✔
373
        maybe: TokenSpan<TokenSpan_types>
7,526✔
374
): maybe is TokenSpan<'nesting_open'> {
7,526✔
375
        return 'nesting_open' === maybe.type;
7,526✔
376
}
7,526✔
377

1✔
378
function is_nesting_close(
5,353✔
379
        maybe: TokenSpan<TokenSpan_types>
5,353✔
380
): maybe is TokenSpan<'nesting_close'> {
5,353✔
381
        return 'nesting_close' === maybe.type;
5,353✔
382
}
5,353✔
383

1✔
384
function is_numeric(
3,180✔
385
        maybe: TokenSpan<TokenSpan_types>
3,180✔
386
): maybe is TokenSpan<'numeric'> {
3,180✔
387
        return 'numeric' === maybe.type;
3,180✔
388
}
3,180✔
389

1✔
390
function is_operation_value(
1,221✔
391
        maybe: string
1,221✔
392
): asserts maybe is operation_types {
1,221✔
393
        if (
1,221✔
394
                ! (
1,221✔
395
                        maybe.length === 1
1,221✔
396
                        && '+-/x*%'.includes(maybe)
1,221✔
397
                )
1,221✔
398
        ) {
1,221!
399
                throw new TokenScanError(
×
400
                        `Expected operation value, found "${maybe}"`
×
401
                )
×
402
        }
×
403
}
1,221✔
404

1✔
405
//#endregion
1✔
406

1✔
407
//#endregion
1✔
408

1✔
409
//#region IntermediaryNumber
1✔
410

1✔
411
export class IntermediaryNumber implements CanDoMathWithDispose
1✔
412
{
1✔
413
        private readonly value:value_types;
1✔
414

1✔
415
        static readonly One = new this('1');
1✔
416

1✔
417
        static readonly Zero = new this('0');
1✔
418

1✔
419
        protected constructor(value:value_types)
1✔
420
        {
5,029✔
421
                this.value = value;
5,029✔
422
        }
5,029✔
423

1✔
424
        get resolve_type(): type_property_types {
1✔
425
                return this.type;
200✔
426
        }
200✔
427

1✔
428
        get type(): type_property_types
1✔
429
        {
7,022✔
430
                if (this.value instanceof BigNumber) {
7,022✔
431
                        return 'BigNumber';
1,932✔
432
                } else if (this.value instanceof Fraction) {
7,022✔
433
                        return 'Fraction';
1,852✔
434
                } else if (NumberStrings.is_amount_string(this.value)) {
5,090✔
435
                        return 'amount_string';
3,214✔
436
                }
3,214✔
437

24✔
438
                return 'numeric_string';
24✔
439
        }
24✔
440

1✔
441
        abs()
1✔
442
        {
5✔
443
                return abs(this);
5✔
444
        }
5✔
445

1✔
446
        compare(value: math_types): 0 | 1 | -1 {
1✔
447
                return compare(value, this);
211✔
448
        }
211✔
449

1✔
450
        divide(value:math_types)
1✔
451
        {
7✔
452
                return do_math(this, '/', value);
7✔
453
        }
7✔
454

1✔
455
        do_math_then_dispose(
1✔
456
                operator: CanDoMathWithDispose_operator_types,
3✔
457
                right_operand: math_types
3✔
458
        ): CanDoMath_result_types {
3✔
459
                const result = this[operator](right_operand);
3✔
460

3✔
461
                if (result !== this) {
3✔
462
                        dispose(this);
1✔
463
                }
1✔
464

3✔
465
                return result;
3✔
466
        }
3✔
467

1✔
468
        isGreaterThan(value: math_types): boolean {
1✔
469
                return 1 === this.compare(value);
4✔
470
        }
4✔
471

1✔
472
        isLessThan(value: math_types): boolean {
1✔
473
                return -1 === this.compare(value);
4✔
474
        }
4✔
475

1✔
476
        isOne(): boolean
1✔
477
        {
91✔
478
                return 0 === this.compare(1);
91✔
479
        }
91✔
480

1✔
481
        isZero(): boolean {
1✔
482
                return 0 === this.compare(0);
99✔
483
        }
99✔
484

1✔
485
        max(
1✔
486
                first: math_types,
1✔
487
                ...remaining: math_types[]
1✔
488
        ): math_types {
1✔
489
                return max(this, first, ...remaining);
1✔
490
        }
1✔
491

1✔
492
        minus(value:math_types)
1✔
493
        {
8✔
494
                return do_math(this, '-', value);
8✔
495
        }
8✔
496

1✔
497
        modulo(value:math_types)
1✔
498
        {
1✔
499
                return do_math(this, '%', value);
1✔
500
        }
1✔
501

1✔
502
        plus(value:math_types)
1✔
503
        {
6✔
504
                if (this.isZero()) {
6✔
505
                        return IntermediaryNumber.reuse_or_create(value);
2✔
506
                }
2✔
507

4✔
508
                return do_math(this, '+', value);
4✔
509
        }
4✔
510

1✔
511
        times(value:math_types)
1✔
512
        {
5✔
513
                return do_math(this, 'x', value);
5✔
514
        }
5✔
515

1✔
516
        toAmountString(): amount_string
1✔
517
        {
4✔
518
                if (NumberStrings.is_amount_string(this.value)) {
4✔
519
                        return this.value;
1✔
520
                }
1✔
521

3✔
522
                return NumberStrings.round_off(this.toBigNumberOrFraction());
3✔
523
        }
3✔
524

1✔
525
        toBigNumber(): BigNumber
1✔
526
        {
3,409✔
527
                if (this.value instanceof BigNumber) {
3,409✔
528
                        return this.value;
1,962✔
529
                } else if (this.value instanceof Fraction) {
3,409✔
530
                        return BigNumber(this.value.valueOf());
96✔
531
                }
96✔
532

1,351✔
533
                const cache = conversion_cache.BigNumber;
1,351✔
534

1,351✔
535
                if (cache.has(this)) {
3,399✔
536
                        return cache.get(this) as BigNumber;
104✔
537
                }
104✔
538

1,247✔
539
                const value = BigNumber(this.value);
1,247✔
540
                cache.set(this, value);
1,247✔
541

1,247✔
542
                return value;
1,247✔
543
        }
1,247✔
544

1✔
545
        toBigNumberOrFraction(): BigNumber | Fraction {
1✔
546
                return ('Fraction' === this.type)
4,072✔
547
                        ? this.toFraction()
4,072✔
548
                        : this.toBigNumber();
4,072✔
549
        }
4,072✔
550

1✔
551
        toFraction(): Fraction
1✔
552
        {
1,865✔
553
                if (this.value instanceof Fraction) {
1,865✔
554
                        return this.value;
894✔
555
                }
894✔
556

971✔
557
                const cache = conversion_cache.Fraction;
971✔
558

971✔
559
                if (cache.has(this)) {
1,865✔
560
                        return cache.get(this) as Fraction;
20✔
561
                }
20✔
562

951✔
563
                const value = new Fraction(this.toString());
951✔
564
                cache.set(this, value);
951✔
565

951✔
566
                return value;
951✔
567
        }
951✔
568

1✔
569
        toJSON(): CanConvertTypeJson {
1✔
570
                if (this.isOne()) {
67✔
571
                        return {
17✔
572
                                type: 'IntermediaryNumber',
17✔
573
                                value: '1',
17✔
574
                        };
17✔
575
                } else if (this.isZero()) {
67✔
576
                        return {
5✔
577
                                type: 'IntermediaryNumber',
5✔
578
                                value: '0',
5✔
579
                        };
5✔
580
                }
5✔
581

45✔
582
                if (this.value instanceof Fraction) {
64✔
583
                        const [left, right] = this.value.toFraction().split('/');
38✔
584

38✔
585
                        if (undefined === right) {
38✔
586
                                return {
5✔
587
                                        type: 'IntermediaryNumber',
5✔
588
                                        value: left,
5✔
589
                                };
5✔
590
                        }
5✔
591

33✔
592
                        return {
33✔
593
                                type: 'IntermediaryCalculation',
33✔
594
                                left: {
33✔
595
                                        type: 'IntermediaryNumber',
33✔
596
                                        value: left,
33✔
597
                                },
33✔
598
                                operation: '/',
33✔
599
                                right: {
33✔
600
                                        type: 'IntermediaryNumber',
33✔
601
                                        value: right,
33✔
602
                                },
33✔
603
                        };
33✔
604
                } else if (this.value instanceof BigNumber) {
64✔
605
                        return {
6✔
606
                                type: 'IntermediaryNumber',
6✔
607
                                value: this.value.toFixed(),
6✔
608
                        };
6✔
609
                }
6✔
610

1✔
611
                return {
1✔
612
                        type: 'IntermediaryNumber',
1✔
613
                        value: this.value,
1✔
614
                };
1✔
615
        }
1✔
616

1✔
617
        toString()
1✔
618
        {
1,679✔
619
                if (this.value instanceof BigNumber) {
1,679✔
620
                        return this.value.toFixed();
818✔
621
                }
818✔
622

861✔
623
                return this.value.toString();
861✔
624
        }
861✔
625

1✔
626
        static create(
1✔
627
                input: input_types
5,022✔
628
        ): IntermediaryNumber {
5,022✔
629
                if ('' === input) {
5,022✔
630
                        return IntermediaryNumber.Zero;
2✔
631
                }
2✔
632

5,020✔
633
                if (input instanceof Fraction) {
5,022✔
634
                        return new this(input.simplify(1 / (2 ** 52)));
1,039✔
635
                }
1,039✔
636
                if (
3,981✔
637
                        input instanceof BigNumber
3,981✔
638
                        || NumberStrings.is_numeric_string(input)
5,012✔
639
                ) {
5,022✔
640
                        return new this(input);
3,200✔
641
                } else if ('number' === typeof input) {
5,022✔
642
                        return new this(BigNumber(input));
251✔
643
                } else if (is_string(input) && regex_recurring_number.test(input)) {
781✔
644
                        let only_last_digit_recurring = false;
529✔
645
                        if (/^\d*\.\d+r$/.test(input)) {
529✔
646
                                only_last_digit_recurring = true;
100✔
647
                        }
100✔
648

529✔
649
                        if (input.endsWith('r')) {
529✔
650
                                input = input.substring(0, input.length - 1);
204✔
651
                        }
204✔
652

529✔
653
                        if (only_last_digit_recurring) {
529✔
654
                                input = input.replace(/(\d)$/, '($1)');
100✔
655
                        } else if (input.includes('[')) {
529✔
656
                                input = input.replace(/\[(\d+)\]/, '($1)');
104✔
657
                        }
104✔
658

529✔
659
                        return new this(new Fraction(input));
529✔
660
                }
529✔
661

1✔
662
                throw new Error('Unsupported argument specified!');
1✔
663
        }
1✔
664

1✔
665
        static create_if_valid(
1✔
666
                input:string
15✔
667
        ): operand_types|NotValid {
15✔
668
                const maybe = input.trim();
15✔
669

15✔
670
                if (
15✔
671
                        NumberStrings.is_amount_string(maybe)
15✔
672
                        || NumberStrings.is_numeric_string(maybe)
15✔
673
                ) {
15✔
674
                        return IntermediaryNumber.create(maybe)
1✔
675
                } else if (
15✔
676
                        /^(\d+|\d*\.\d+)\s*[+/*x%-]\s*(\d+|\d*\.\d+)$/.test(maybe)
14✔
677
                ) {
14✔
678
                        return (new TokenScan(input)).parsed;
1✔
679
                }
1✔
680

13✔
681
                const scientific = /^(-?\d+(?:\.\d+))e([+-])(\d+)$/.exec(maybe);
13✔
682

13✔
683
                if (scientific) {
15✔
684
                        const calc = new IntermediaryCalculation(
1✔
685
                                IntermediaryNumber.Zero,
1✔
686
                                scientific[2] as '+'|'-',
1✔
687
                                IntermediaryNumber.create(scientific[3]),
1✔
688
                        ).toBigNumber();
1✔
689

1✔
690
                        return IntermediaryNumber.create(scientific[1]).times(
1✔
691
                                (new BigNumber(10)).pow(calc)
1✔
692
                        );
1✔
693
                }
1✔
694

12✔
695
                try {
12✔
696
                        return IntermediaryCalculation.fromString(maybe);
12✔
697
                } catch (err) {
15✔
698
                        return new NotValid(maybe, err);
2✔
699
                }
2✔
700
        }
15✔
701

1✔
702
        static fromJson(json:CanConvertTypeJson): CanDoMath_result_types {
1✔
703
                if ('IntermediaryNumber' === json.type) {
4✔
704
                        return this.create(json.value);
3✔
705
                }
3✔
706

1✔
707
                return new IntermediaryCalculation(
1✔
708
                        this.fromJson(json.left),
1✔
709
                        json.operation,
1✔
710
                        this.fromJson(json.right)
1✔
711
                );
1✔
712
        }
1✔
713

1✔
714
        static reuse_or_create(
1✔
715
                input:
320✔
716
                        | operand_types
320✔
717
                        | input_types
320✔
718
        ): operand_types {
320✔
719
                return (
320✔
720
                        (
320✔
721
                                (input instanceof IntermediaryNumber)
320✔
722
                                || (input instanceof IntermediaryCalculation)
320✔
723
                        )
320✔
724
                                ? input
320✔
725
                                : this.create(input)
320✔
726
                );
320✔
727
        }
320✔
728
}
1✔
729

1✔
730
//#endregion
1✔
731

1✔
732
//#region IntermediaryCalculation
1✔
733

1✔
734
export class NotValid extends Error
1✔
735
{
2✔
736
        readonly reason: unknown;
2✔
737
        readonly value:string;
2✔
738

2✔
739
        constructor(not_valid:string, reason:unknown)
2✔
740
        {
2✔
741
                super('Value given was not valid!');
2✔
742

2✔
743
                this.value = not_valid;
2✔
744
                this.reason = reason;
2✔
745
        }
2✔
746
}
2✔
747

1✔
748
const BigNumber_operation_map:{
1✔
749
        [
1✔
750
                key in Exclude<
1✔
751
                        operation_types,
1✔
752
                        '/'
1✔
753
                >
1✔
754
        ]: ((a: BigNumber, b:BigNumber) => BigNumber)
1✔
755
} = {
1✔
756
        '+': (a, b) => a.plus(b),
1✔
757
        '-': (a, b) => a.minus(b),
1✔
758
        'x': (a, b) => a.times(b),
1✔
759
        '*': (a, b) => a.times(b),
1✔
760
        '%': (a, b) => a.modulo(b),
1✔
761
};
1✔
762

1✔
763
const Fraction_operation_map:{
1✔
764
        [
1✔
765
                key in operation_types
1✔
766
        ]: ((a: Fraction, b:Fraction) => Fraction)
1✔
767
} = {
1✔
768
        '+': (a, b) => a.add(b),
1✔
769
        '-': (a, b) => a.sub(b),
1✔
770
        'x': (a, b) => a.mul(b),
1✔
771
        '*': (a, b) => a.mul(b),
1✔
772
        '/': (a, b) => a.div(b),
1✔
773
        '%': (a, b) => a.mod(b),
1✔
774
};
1✔
775

1✔
776
export class IntermediaryCalculation implements CanResolveMathWithDispose
1✔
777
{
1,265✔
778
        readonly left_operand:operand_types;
1,265✔
779
        readonly operation:operation_types;
1,265✔
780
        readonly right_operand:operand_types;
1,265✔
781

1,265✔
782
        constructor(
1,265✔
783
                left:operand_types,
1,265✔
784
                operation:operation_types,
1,265✔
785
                right:operand_types
1,265✔
786
        ) {
1,265✔
787
                this.left_operand = left;
1,265✔
788
                this.operation = operation;
1,265✔
789
                this.right_operand = right;
1,265✔
790
        }
1,265✔
791

1,265✔
792
        get left_type(): operand_type_property_types
1,265✔
793
        {
460✔
794
                if (this.left_operand instanceof IntermediaryCalculation) {
460✔
795
                        return 'IntermediaryCalculation';
332✔
796
                }
332✔
797

128✔
798
                return this.left_operand.type;
128✔
799
        }
128✔
800

1,265✔
801
        get resolve_type(): string {
1,265✔
802
                return `${this.left_type} ${this.operation} ${this.right_type}`;
460✔
803
        }
460✔
804

1,265✔
805
        get right_type(): operand_type_property_types
1,265✔
806
        {
460✔
807
                if (this.right_operand instanceof IntermediaryCalculation) {
460✔
808
                        return 'IntermediaryCalculation';
64✔
809
                }
64✔
810

396✔
811
                return this.right_operand.type;
396✔
812
        }
396✔
813

1,265✔
814
        get type(): operand_type_property_types
1,265✔
815
        {
3✔
816
                return 'IntermediaryCalculation';
3✔
817
        }
3✔
818

1,265✔
819
        abs()
1,265✔
820
        {
3✔
821
                return abs(this);
3✔
822
        }
3✔
823

1,265✔
824
        compare(value: math_types): 0 | 1 | -1 {
1,265✔
825
                return compare(value, this);
25✔
826
        }
25✔
827

1,265✔
828
        divide(value:math_types)
1,265✔
829
        {
5✔
830
                return do_math(this, '/', value);
5✔
831
        }
5✔
832

1,265✔
833
        do_math_then_dispose(
1,265✔
834
                operator: CanDoMathWithDispose_operator_types,
2✔
835
                right_operand: math_types
2✔
836
        ): CanDoMath_result_types {
2✔
837
                const result = this[operator](right_operand);
2✔
838

2✔
839
                if (result !== this) {
2✔
840
                        dispose(this);
2✔
841
                }
2✔
842

2✔
843
                return result;
2✔
844
        }
2✔
845

1,265✔
846
        isGreaterThan(value: math_types): boolean {
1,265✔
847
                return 1 === this.compare(value);
2✔
848
        }
2✔
849

1,265✔
850
        isLessThan(value: math_types): boolean {
1,265✔
851
                return -1 === this.compare(value);
2✔
852
        }
2✔
853

1,265✔
854
        isOne(): boolean {
1,265✔
855
                return 0 === this.compare(1);
4✔
856
        }
4✔
857

1,265✔
858
        isZero(): boolean {
1,265✔
859
                return 0 === this.compare(0);
13✔
860
        }
13✔
861

1,265✔
862
        max(
1,265✔
863
                first: math_types,
3✔
864
                ...remaining: math_types[]
3✔
865
        ): math_types {
3✔
866
                return max(this, first, ...remaining);
3✔
867
        }
3✔
868

1,265✔
869
        minus(value:math_types)
1,265✔
870
        {
1✔
871
                return do_math(this, '-', value);
1✔
872
        }
1✔
873

1,265✔
874
        modulo(value:math_types)
1,265✔
875
        {
2✔
876
                return do_math(this, '%', value);
2✔
877
        }
2✔
878

1,265✔
879
        plus(value:math_types)
1,265✔
880
        {
3✔
881
                if (this.isZero()) {
3✔
882
                        return IntermediaryNumber.reuse_or_create(value);
1✔
883
                }
1✔
884

2✔
885
                return do_math(this, '+', value);
2✔
886
        }
2✔
887

1,265✔
888
        resolve(): IntermediaryNumber
1,265✔
889
        {
1,274✔
890
                const left_operand = this.operand_to_IntermediaryNumber(
1,274✔
891
                        this.left_operand
1,274✔
892
                );
1,274✔
893
                const right_operand = this.operand_to_IntermediaryNumber(
1,274✔
894
                        this.right_operand
1,274✔
895
                );
1,274✔
896
                const left = left_operand.toBigNumberOrFraction();
1,274✔
897
                const right = right_operand.toBigNumberOrFraction();
1,274✔
898

1,274✔
899
                if (
1,274✔
900
                        '/' === this.operation
1,274✔
901
                        || left instanceof Fraction
1,274✔
902
                        || right instanceof Fraction
1,274✔
903
                ) {
1,274✔
904
                        return IntermediaryNumber.create(
815✔
905
                                Fraction_operation_map[this.operation](
815✔
906
                                        (
815✔
907
                                                (left instanceof BigNumber)
815✔
908
                                                        ? left_operand.toFraction()
815✔
909
                                                        : left
815✔
910
                                        ),
815✔
911
                                        (
815✔
912
                                                (right instanceof BigNumber)
815✔
913
                                                        ? right_operand.toFraction()
815✔
914
                                                        : right
815✔
915
                                        )
815✔
916
                                )
815✔
917
                        );
815✔
918
                }
815✔
919

459✔
920
                return IntermediaryNumber.create(
459✔
921
                        BigNumber_operation_map[this.operation](
459✔
922
                                left,
459✔
923
                                right
459✔
924
                        )
459✔
925
                );
459✔
926
        }
459✔
927

1,265✔
928
        times(value:math_types)
1,265✔
929
        {
1✔
930
                return do_math(this, 'x', value);
1✔
931
        }
1✔
932

1,265✔
933
        toAmountString(): amount_string {
1,265✔
934
                const cache = conversion_cache.AmountString;
3✔
935

3✔
936
                if (cache.has(this)) {
3✔
937
                        return cache.get(this) as amount_string;
1✔
938
                }
1✔
939

2✔
940
                const value = this.resolve().toAmountString();
2✔
941
                cache.set(this, value);
2✔
942

2✔
943
                return value;
2✔
944
        }
2✔
945

1,265✔
946
        toBigNumber(): BigNumber {
1,265✔
947
                const cache = conversion_cache.BigNumber;
27✔
948

27✔
949
                if (cache.has(this)) {
27✔
950
                        return cache.get(this) as BigNumber;
7✔
951
                }
7✔
952

20✔
953
                const value = this.resolve().toBigNumber()
20✔
954
                cache.set(this, value);
20✔
955

20✔
956
                return value;
20✔
957
        }
20✔
958

1,265✔
959
        toBigNumberOrFraction(): BigNumber | Fraction {
1,265✔
960
                return this.resolve().toBigNumberOrFraction();
3✔
961
        }
3✔
962

1,265✔
963
        toFraction(): Fraction {
1,265✔
964
                const cache = conversion_cache.Fraction;
3✔
965

3✔
966
                if (cache.has(this)) {
3✔
967
                        return cache.get(this) as Fraction;
1✔
968
                }
1✔
969

2✔
970
                const value = this.resolve().toFraction();
2✔
971
                cache.set(this, value);
2✔
972

2✔
973
                return value;
2✔
974
        }
2✔
975

1,265✔
976
        toJSON(): CanConvertTypeJson {
1,265✔
977
                const left = this.operand_to_IntermediaryNumber(
10✔
978
                        this.left_operand
10✔
979
                );
10✔
980

10✔
981
                const right = this.operand_to_IntermediaryNumber(
10✔
982
                        this.right_operand
10✔
983
                );
10✔
984

10✔
985
                const maybe = IntermediaryCalculation.maybe_short_circuit(
10✔
986
                        left,
10✔
987
                        this.operation,
10✔
988
                        right
10✔
989
                );
10✔
990

10✔
991
                if (maybe) {
10✔
992
                        return maybe.toJSON();
6✔
993
                }
6✔
994

4✔
995
                return {
4✔
996
                        type: 'IntermediaryCalculation',
4✔
997
                        left: left.toJSON(),
4✔
998
                        operation: this.operation,
4✔
999
                        right: right.toJSON(),
4✔
1000
                }
4✔
1001
        }
4✔
1002

1,265✔
1003
        toString(): string {
1,265✔
1004
                const cache = conversion_cache.String;
475✔
1005

475✔
1006
                if (cache.has(this)) {
475✔
1007
                        return cache.get(this) as string;
5✔
1008
                }
5✔
1009

470✔
1010
                const value = this.resolve().toString();
470✔
1011
                cache.set(this, value);
470✔
1012

470✔
1013
                return value;
470✔
1014
        }
470✔
1015

1,265✔
1016
        toStringCalculation(): string
1,265✔
1017
        {
6✔
1018
                return `${
6✔
1019
                        (this.left_operand instanceof IntermediaryCalculation)
6✔
1020
                                ? `(${this.left_operand.toStringCalculation()})`
6✔
1021
                                : this.left_operand.toString()
6✔
1022
                } ${
6✔
1023
                        this.operation
6✔
1024
                } ${
6✔
1025
                        (this.right_operand instanceof IntermediaryCalculation)
6✔
1026
                                ? `(${this.right_operand.toStringCalculation()})`
6✔
1027
                                : this.right_operand.toString()
6✔
1028
                }`
6✔
1029
        }
6✔
1030

1,265✔
1031
        private operand_to_IntermediaryNumber(
1,265✔
1032
                operand:operand_types
2,568✔
1033
        ) : IntermediaryNumber {
2,568✔
1034
                if (
2,568✔
1035
                        (operand instanceof IntermediaryCalculation)
2,568✔
1036
                ) {
2,568✔
1037
                        return operand.resolve();
737✔
1038
                } else if (
2,568✔
1039
                        'amount_string' === operand.type
1,831✔
1040
                        || 'numeric_string' === operand.type
1,831✔
1041
                ) {
1,831✔
1042
                        return IntermediaryNumber.create(
1,494✔
1043
                                '/' === this.operation
1,494✔
1044
                                        ? operand.toFraction()
1,494✔
1045
                                        : operand.toBigNumberOrFraction()
1,494✔
1046
                        );
1,494✔
1047
                }
1,494✔
1048

337✔
1049
                return operand;
337✔
1050
        }
337✔
1051

1,265✔
1052
        static fromString(
1,265✔
1053
                input:Exclude<string, ''>
52✔
1054
        ): IntermediaryNumber|IntermediaryCalculation {
52✔
1055
                return (new TokenScan(input)).parsed;
52✔
1056
        }
52✔
1057

1,265✔
1058
        static is(maybe: unknown): maybe is IntermediaryCalculation
1,265✔
1059
        {
8✔
1060
                return maybe instanceof this;
8✔
1061
        }
8✔
1062

1,265✔
1063
        static maybe_reduce_operands(
1,265✔
1064
                left:operand_types,
36✔
1065
                operation:operation_types,
36✔
1066
                right:operand_types
36✔
1067
        ) {
36✔
1068
                let value:operand_types|undefined = this.maybe_short_circuit(
36✔
1069
                        left,
36✔
1070
                        operation,
36✔
1071
                        right
36✔
1072
                );
36✔
1073

36✔
1074
                if (undefined === value) {
36✔
1075
                        value = new IntermediaryCalculation(left, operation, right);
31✔
1076
                }
31✔
1077

36✔
1078
                if (value instanceof IntermediaryCalculation) {
36✔
1079
                        return value.resolve();
33✔
1080
                }
33✔
1081

3✔
1082
                return value;
3✔
1083
        }
3✔
1084

1,265✔
1085
        static require_is(maybe: unknown): asserts maybe is IntermediaryCalculation
1,265✔
1086
        {
6✔
1087
                if (!this.is(maybe)) {
6✔
1088
                        throw new Error(
1✔
1089
                                'Argument is not an instanceof IntermediaryCalculation'
1✔
1090
                        );
1✔
1091
                }
1✔
1092
        }
6✔
1093

1,265✔
1094
        private static maybe_short_circuit(
1,265✔
1095
                left:operand_types,
46✔
1096
                operation:operation_types,
46✔
1097
                right:operand_types
46✔
1098
        ) {
46✔
1099
                let value:operand_types|undefined = undefined;
46✔
1100

46✔
1101
                if ('+' === operation) {
46✔
1102
                        if (left.isZero()) {
11✔
1103
                                value = right;
2✔
1104
                        } else if (right.isZero()) {
11✔
1105
                                value = left;
3✔
1106
                        }
3✔
1107
                } else if ('-' === operation && right.isZero()) {
46✔
1108
                        value = left;
1✔
1109
                } else if ('*x'.includes(operation)) {
35✔
1110
                        if (left.isZero() || right.isOne()) {
8✔
1111
                                value = left;
2✔
1112
                        } else if (right.isZero() || left.isOne()) {
8✔
1113
                                value = right;
1✔
1114
                        }
1✔
1115
                } else if ('/' === operation && right.isOne()) {
34✔
1116
                        value = left;
2✔
1117
                }
2✔
1118

46✔
1119
                return value;
46✔
1120
        }
46✔
1121
}
1,265✔
1122

1✔
1123
//#endregion
1✔
1124

1✔
1125
//#region TokenScan
1✔
1126

1✔
1127

1✔
1128

1✔
1129
class TokenSpan<T = TokenSpan_types>
1✔
1130
{
11,159✔
1131
        readonly from:number;
11,159✔
1132
        readonly to:number;
11,159✔
1133
        readonly type:T;
11,159✔
1134

11,159✔
1135
        constructor(from:number, to:number, type:T)
11,159✔
1136
        {
11,159✔
1137
                this.from = from;
11,159✔
1138
                this.to = to;
11,159✔
1139
                this.type = type;
11,159✔
1140
        }
11,159✔
1141
}
11,159✔
1142

1✔
1143
export class TokenScanError extends Error
1✔
1144
{
1✔
1145
}
1✔
1146

1✔
1147
export class TokenScanParseError extends Error
1✔
1148
{
8✔
1149
        readonly current?: TokenSpan<TokenSpan_types>;
8✔
1150
        readonly scan: TokenScan_parsing_value;
8✔
1151
        readonly state?: TokenScan_tokenizer;
8✔
1152

8✔
1153
        constructor(
8✔
1154
                message:string,
8✔
1155
                scan: TokenScan_parsing_value,
8✔
1156
                state: TokenScan_tokenizer,
8✔
1157
                current?: TokenSpan<TokenSpan_types>
8✔
1158
        ) {
8✔
1159
                super(message);
8✔
1160

8✔
1161
                this.scan = scan;
8✔
1162
                this.state = state;
8✔
1163
                this.current = current;
8✔
1164
        }
8✔
1165
}
8✔
1166

1✔
1167
const regex_numeric = (
1✔
1168
        /(?:\d*\.\d*\(\d+\)r?|\d*\.\d*\[\d+\]r?|\d+(?:\.\d+r)?|\.\d+r?)/g
1✔
1169
);
1✔
1170

1✔
1171
// eslint-disable-next-line max-len
1✔
1172
// @todo add TokenScan to CanDoMath_result_types then return a new instance of TokenScan
1✔
1173
export class TokenScan implements CanResolveMathWithDispose
1✔
1174
{
745✔
1175
        private readonly internal:TokenScan_internals = {
745✔
1176
                parsed: undefined,
745✔
1177
                tokens: undefined,
745✔
1178
                valid: undefined,
745✔
1179
        };
745✔
1180

745✔
1181
        readonly value:string;
745✔
1182

745✔
1183
        constructor(value:string)
745✔
1184
        {
745✔
1185
                this.value = value;
745✔
1186
        }
745✔
1187

745✔
1188
        get parsed(): Exclude<TokenScan_internals['parsed'], undefined>
745✔
1189
        {
1,432✔
1190
                if (undefined === this.internal.parsed) {
1,432✔
1191
                        this.internal.parsed = TokenScan.parse_scan(this);
750✔
1192
                }
750✔
1193

1,420✔
1194
                return this.internal.parsed;
1,420✔
1195
        }
1,420✔
1196

745✔
1197
        get resolve_type(): string {
745✔
1198
                return this.parsed.resolve_type;
×
1199
        }
×
1200

745✔
1201
        get tokens(): Exclude<TokenScan_internals['tokens'], undefined>
745✔
1202
        {
843✔
1203
                if (undefined === this.internal.tokens) {
843✔
1204
                        this.internal.tokens = TokenScan.determine_tokens_from_scan(this);
746✔
1205
                }
746✔
1206

839✔
1207
                return this.internal.tokens;
839✔
1208
        }
839✔
1209

745✔
1210
        /**
745✔
1211
         * @todo change to `return 'TokenScan';`
745✔
1212
         */
745✔
1213
        get type(): operand_type_property_types {
745✔
1214
                throw new Error('Method not implemented.');
×
1215
        }
×
1216

745✔
1217
        get valid(): boolean
745✔
1218
        {
692✔
1219
                if (undefined === this.internal.valid) {
692✔
1220
                        try {
692✔
1221
                                this.parsed;
692✔
1222
                                this.internal.valid = true;
692✔
1223
                        } catch (err) {
692✔
1224
                                this.internal.valid = false;
5✔
1225
                        }
5✔
1226
                }
692✔
1227

692✔
1228
                return this.internal.valid;
692✔
1229
        }
692✔
1230

745✔
1231
        abs(): IntermediaryNumber | IntermediaryCalculation {
745✔
1232
                return this.parsed.abs();
1✔
1233
        }
1✔
1234

745✔
1235
        compare(value: math_types): 0 | 1 | -1 {
745✔
1236
                return this.parsed.compare(value);
1✔
1237
        }
1✔
1238

745✔
1239
        divide(value: math_types): CanDoMath_result_types {
745✔
1240
                return this.parsed.divide(value);
1✔
1241
        }
1✔
1242

745✔
1243
        do_math_then_dispose(
745✔
1244
                operator: CanDoMathWithDispose_operator_types,
1✔
1245
                right_operand: math_types
1✔
1246
        ): CanDoMath_result_types {
1✔
1247
                const result = this.parsed.do_math_then_dispose(
1✔
1248
                        operator,
1✔
1249
                        right_operand
1✔
1250
                );
1✔
1251
                this.internal.parsed = undefined;
1✔
1252

1✔
1253
                return result;
1✔
1254
        }
1✔
1255

745✔
1256
        isGreaterThan(value: math_types): boolean {
745✔
1257
                return this.parsed.isGreaterThan(value);
1✔
1258
        }
1✔
1259

745✔
1260
        isLessThan(value: math_types): boolean {
745✔
1261
                return this.parsed.isLessThan(value);
1✔
1262
        }
1✔
1263

745✔
1264
        isOne(): boolean {
745✔
1265
                return (
2✔
1266
                        '1' === this.value.trim()
2✔
1267
                        || this.parsed.isOne()
2✔
1268
                );
2✔
1269
        }
2✔
1270

745✔
1271
        isZero(): boolean {
745✔
1272
                return (
2✔
1273
                        '0' === this.value.trim()
2✔
1274
                        || this.parsed.isZero()
2✔
1275
                );
2✔
1276
        }
2✔
1277

745✔
1278
        max(first: math_types, ...remaining: math_types[]): math_types {
745✔
1279
                return this.parsed.max(first, ...remaining);
1✔
1280
        }
1✔
1281

745✔
1282
        minus(value: math_types): CanDoMath_result_types {
745✔
1283
                return this.parsed.minus(value);
2✔
1284
        }
2✔
1285

745✔
1286
        modulo(value: math_types): CanDoMath_result_types {
745✔
1287
                return this.parsed.modulo(value);
1✔
1288
        }
1✔
1289

745✔
1290
        plus(value: math_types): CanDoMath_result_types {
745✔
1291
                return this.parsed.plus(value);
1✔
1292
        }
1✔
1293

745✔
1294
        resolve(): IntermediaryNumber {
745✔
1295
                const parsed = this.parsed;
2✔
1296

2✔
1297
                return IntermediaryCalculation.is(parsed) ? parsed.resolve() : parsed;
2✔
1298
        }
2✔
1299

745✔
1300
        times(value: math_types): CanDoMath_result_types {
745✔
1301
                return this.parsed.times(value);
1✔
1302
        }
1✔
1303

745✔
1304
        toAmountString(): amount_string {
745✔
1305
                return this.parsed.toAmountString();
1✔
1306
        }
1✔
1307

745✔
1308
        toBigNumber(): BigNumber {
745✔
1309
                return this.parsed.toBigNumber();
1✔
1310
        }
1✔
1311

745✔
1312
        toBigNumberOrFraction(): BigNumber | Fraction {
745✔
1313
                return this.parsed.toBigNumberOrFraction();
1✔
1314
        }
1✔
1315

745✔
1316
        toFraction(): Fraction {
745✔
1317
                return this.parsed.toFraction();
1✔
1318
        }
1✔
1319

745✔
1320
        /**
745✔
1321
         * @todo implement
745✔
1322
         */
745✔
1323
        toJSON(): CanConvertTypeJson {
745✔
1324
                throw new Error('Method not implemented.');
×
1325
        }
×
1326

745✔
1327
        toString(): string {
745✔
1328
                return this.parsed.toString();
2✔
1329
        }
2✔
1330

745✔
1331
        toStringCalculation(): string
745✔
1332
        {
3✔
1333
                return this.value;
3✔
1334
        }
3✔
1335

745✔
1336
        private static determine_tokens_from_scan(
745✔
1337
                scan: TokenScan_parsing_tokens
746✔
1338
        ): Exclude<TokenScan_internals['tokens'], undefined> {
746✔
1339

746✔
1340
                let tokens:TokenSpan<TokenSpan_types>[] = [];
746✔
1341

746✔
1342
                for (const entry of scan.value.matchAll(/([\s]+)/g)) {
746✔
1343
                        tokens.push(new TokenSpan(
2,887✔
1344
                                entry.index,
2,887✔
1345
                                entry.index + entry[0].length,
2,887✔
1346
                                'ignore'
2,887✔
1347
                        ));
2,887✔
1348
                }
2,887✔
1349

746✔
1350
                for (const entry of scan.value.matchAll(regex_numeric)) {
746✔
1351
                        tokens.push(new TokenSpan(
2,006✔
1352
                                entry.index,
2,006✔
1353
                                entry.index + entry[0].length,
2,006✔
1354
                                'numeric'
2,006✔
1355
                        ));
2,006✔
1356
                }
2,006✔
1357

746✔
1358
                for (const entry of scan.value.matchAll(/([+/*x%-])/g)) {
746✔
1359
                        tokens.push(new TokenSpan(
1,229✔
1360
                                entry.index,
1,229✔
1361
                                entry.index + entry[0].length,
1,229✔
1362
                                'operation'
1,229✔
1363
                        ));
1,229✔
1364
                }
1,229✔
1365

746✔
1366
                for (const entry of scan.value.matchAll(/(\()/g)) {
746✔
1367
                        tokens.push(new TokenSpan(
2,490✔
1368
                                entry.index,
2,490✔
1369
                                entry.index + entry[0].length,
2,490✔
1370
                                'nesting_open'
2,490✔
1371
                        ));
2,490✔
1372
                }
2,490✔
1373

746✔
1374
                for (const entry of scan.value.matchAll(/(\))/g)) {
746✔
1375
                        tokens.push(new TokenSpan(
2,490✔
1376
                                entry.index,
2,490✔
1377
                                entry.index + entry[0].length,
2,490✔
1378
                                'nesting_close'
2,490✔
1379
                        ));
2,490✔
1380
                }
2,490✔
1381

746✔
1382
                tokens = tokens.sort((a, b) => {
746✔
1383
                        return a.from - b.from;
31,836✔
1384
                })
746✔
1385

746✔
1386
                const recursive_numerics = tokens.filter(
746✔
1387
                        maybe => (
746✔
1388
                                'numeric' === maybe.type
11,102✔
1389
                                && /[()]/.test(scan.value.substring(maybe.from, maybe.to))
11,102✔
1390
                        )
746✔
1391
                );
746✔
1392

746✔
1393
                tokens = tokens.filter(
746✔
1394
                        (maybe) => {
746✔
1395
                                if (
11,102✔
1396
                                        'nesting_open' === maybe.type
11,102✔
1397
                                        || 'nesting_close' === maybe.type
11,102✔
1398
                                ) {
11,102✔
1399
                                        return !recursive_numerics.find(
4,980✔
1400
                                                maybe_numeric => (
4,980✔
1401
                                                        maybe.from >= maybe_numeric.from
2,394✔
1402
                                                        && maybe.to <= maybe_numeric.to
2,394✔
1403
                                                )
4,980✔
1404
                                        )
4,980✔
1405
                                }
4,980✔
1406

6,122✔
1407
                                return true;
6,122✔
1408
                        }
6,122✔
1409
                );
746✔
1410

746✔
1411
                if (tokens.length < 1) {
746✔
1412
                        throw new TokenScanError('No tokens found!')
4✔
1413
                } else if (0 !== tokens[0].from) {
746!
1414
                        throw new TokenScanError('First token not at index 0!')
×
1415
                } else if (scan.value.length !== tokens[tokens.length - 1].to) {
742!
1416
                        throw new TokenScanError(
×
1417
                                'Last token does not end at end of string!'
×
1418
                        )
×
1419
                }
×
1420

742✔
1421
                let nesting_balance = 0;
742✔
1422

742✔
1423
                for (let index=0; index<tokens.length; ++index) {
746✔
1424
                        const token = tokens[index];
10,460✔
1425
                        if ('nesting_open' === token.type) {
10,460✔
1426
                                nesting_balance += (token.to - token.from);
2,169✔
1427
                        } else if ('nesting_close' === token.type) {
10,460✔
1428
                                nesting_balance -= (token.to - token.from);
2,169✔
1429
                        }
2,169✔
1430

10,460✔
1431
                        if (
10,460✔
1432
                                index > 0
10,460✔
1433
                                && tokens[index - 1].to !== token.from
10,460✔
1434
                        ) {
10,460!
1435
                                console.error(tokens, index);
×
1436
                                throw new TokenScanError(
×
1437
                                        `Token expected to be found at index ${index}`
×
1438
                                )
×
1439
                        }
×
1440
                }
10,460✔
1441

742✔
1442
                if (0 !== nesting_balance) {
746!
1443
                        throw new TokenScanError(
×
1444
                                'Imbalanced nesting in string!'
×
1445
                        );
×
1446
                }
×
1447

742✔
1448
                return this.massage_part_baked_tokens(
742✔
1449
                        scan,
742✔
1450
                        tokens.filter(
742✔
1451
                                (maybe): maybe is TokenSpan<
742✔
1452
                                        TokenSpan_types_part_baked
10,460✔
1453
                                > => 'ignore' !== maybe.type
10,460✔
1454
                        )
742✔
1455
                );
742✔
1456
        }
742✔
1457

745✔
1458
        private static massage_part_baked_tokens(
745✔
1459
                scan: TokenScan_parsing_tokens,
742✔
1460
                tokens: Exclude<TokenScan_internals['tokens'], undefined>
742✔
1461
        ): Exclude<TokenScan_internals['tokens'], undefined> {
742✔
1462
                const smoosh_numerics:number[] = [];
742✔
1463

742✔
1464
                for (
742✔
1465
                        let token_index=tokens.length - 1; token_index > 0; --token_index
742✔
1466
                ) {
742✔
1467
                        const previous = tokens[token_index - 1];
6,831✔
1468
                        const current = tokens[token_index];
6,831✔
1469

6,831✔
1470
                        if ('numeric' === previous.type) {
6,831✔
1471
                                const previous_value = scan.value.substring(
1,633✔
1472
                                        previous.from,
1,633✔
1473
                                        previous.to
1,633✔
1474
                                );
1,633✔
1475
                                const current_value = scan.value.substring(
1,633✔
1476
                                        current.from,
1,633✔
1477
                                        current.to
1,633✔
1478
                                );
1,633✔
1479

1,633✔
1480
                                if (
1,633✔
1481
                                        current_value.startsWith('.')
1,633✔
1482
                                        && /^\d+$/.test(previous_value)
1,633✔
1483
                                ) {
1,633✔
1484
                                        smoosh_numerics.push(token_index);
48✔
1485
                                }
48✔
1486
                        }
1,633✔
1487
                }
6,831✔
1488

742✔
1489
                for (const index of smoosh_numerics) {
742✔
1490
                        tokens.splice(
48✔
1491
                                index - 1,
48✔
1492
                                2,
48✔
1493
                                new TokenSpan(
48✔
1494
                                        tokens[index - 1].from,
48✔
1495
                                        tokens[index].to,
48✔
1496
                                        'numeric'
48✔
1497
                                )
48✔
1498
                        );
48✔
1499
                }
48✔
1500

742✔
1501
                const convert_to_negative:number[] = [];
742✔
1502

742✔
1503
                if (
742✔
1504
                        tokens.length >= 2
742✔
1505
                        && 'operation' === tokens[0].type
742✔
1506
                        && '-' === scan.value[tokens[0].from]
742✔
1507
                        && 'numeric' === tokens[1].type
742✔
1508
                ) {
742✔
1509
                        convert_to_negative.push(0);
8✔
1510
                }
8✔
1511

742✔
1512
                for (
742✔
1513
                        let token_index=0; token_index < tokens.length; ++token_index
742✔
1514
                ) {
742✔
1515
                        const token = tokens[token_index];
7,523✔
1516
                        const next = tokens[token_index + 1];
7,523✔
1517
                        const after = tokens[token_index + 2];
7,523✔
1518

7,523✔
1519
                        if (
7,523✔
1520
                                (
7,523✔
1521
                                        'nesting_open' === token.type
7,523✔
1522
                                        || 'operation' === token.type
7,523✔
1523
                                )
7,523✔
1524
                                && next
7,523✔
1525
                                && after
7,523✔
1526
                                && 'operation' === next.type
7,523✔
1527
                                && '-' === scan.value[next.from]
7,523✔
1528
                                && 'numeric' === after.type
7,523✔
1529
                        ) {
7,523✔
1530
                                convert_to_negative.push(token_index + 1);
1✔
1531
                                token_index += 2;
1✔
1532
                                continue;
1✔
1533
                        }
1✔
1534
                }
7,523✔
1535

742✔
1536
                for (const index of convert_to_negative.reverse()) {
742✔
1537
                        tokens.splice(
9✔
1538
                                index,
9✔
1539
                                2,
9✔
1540
                                new TokenSpan(
9✔
1541
                                        tokens[index].from,
9✔
1542
                                        tokens[index + 1].to,
9✔
1543
                                        'numeric'
9✔
1544
                                )
9✔
1545
                        );
9✔
1546
                }
9✔
1547

742✔
1548
                return tokens;
742✔
1549
        }
742✔
1550

745✔
1551
        private static parse_scan(
745✔
1552
                scan: TokenScan_parsing_value
750✔
1553
        ): IntermediaryNumber|IntermediaryCalculation {
750✔
1554
                const reduced = scan.tokens.reduce(
750✔
1555
                        (
750✔
1556
                                was:TokenScan_tokenizer,
7,526✔
1557
                                is:TokenSpan<TokenSpan_types_part_baked>,
7,526✔
1558
                                index:number,
7,526✔
1559
                        ) => TokenScan.reduce(
7,526✔
1560
                                scan,
7,526✔
1561
                                was,
7,526✔
1562
                                is,
7,526✔
1563
                                index,
7,526✔
1564
                        ),
750✔
1565
                        default_tokenizer_state()
750✔
1566
                );
750✔
1567

750✔
1568
                if (
750✔
1569
                        undefined !== reduced.left_operand
750✔
1570
                        && '' === reduced.operation
750✔
1571
                        && undefined === reduced.right_operand
750✔
1572
                        && 0 === reduced.outter_stack.length
750✔
1573
                ) {
750✔
1574
                        return reduced.left_operand;
738✔
1575
                }
738✔
1576

×
1577
                throw new TokenScanParseError(
×
1578
                        'Parse in unsupported state!',
×
1579
                        scan,
×
1580
                        reduced
×
1581
                );
×
1582
        }
×
1583

745✔
1584
        private static reduce(
745✔
1585
                scan: TokenScan_parsing_value,
7,526✔
1586
                was:TokenScan_tokenizer,
7,526✔
1587
                is:TokenSpan<TokenSpan_types_part_baked>,
7,526✔
1588
                index:number,
7,526✔
1589
        ): TokenScan_tokenizer {
7,526✔
1590
                if (is_nesting_open(is)) {
7,526✔
1591
                        if ('right' === was.operand_mode) {
2,173✔
1592
                                if (undefined === was.left_operand) {
183!
1593
                                        if (
×
1594
                                                ! (
×
1595
                                                        was.outter_stack.length > 0
×
1596
                                                        && ! (
×
1597
                                                                was.outter_stack[
×
1598
                                                                        was.outter_stack.length - 1
×
1599
                                                                ] instanceof TokenSpan
×
1600
                                                        )
×
1601
                                                )
×
1602
                                        ) {
×
1603
                                                throw new TokenScanParseError(
×
1604
                                                        // eslint-disable-next-line max-len
×
1605
                                                        'Nesting opened without left operand to push into stack!',
×
1606
                                                        scan,
×
1607
                                                        was
×
1608
                                                );
×
1609
                                        }
×
1610

×
1611
                                        return was;
×
1612
                                } else if ('' === was.operation) {
183!
1613
                                        throw new TokenScanParseError(
×
1614
                                                'Nesting opened without operation to push into stack!',
×
1615
                                                scan,
×
1616
                                                was,
×
1617
                                                is
×
1618
                                        );
×
1619
                                }
×
1620

183✔
1621
                                was.outter_stack.push({
183✔
1622
                                        left_operand: was.left_operand,
183✔
1623
                                        operation: was.operation,
183✔
1624
                                });
183✔
1625
                                was.left_operand = undefined;
183✔
1626
                                was.operation = '';
183✔
1627
                                was.operand_mode = 'left';
183✔
1628
                        } else {
2,173✔
1629
                                was.outter_stack.push(is);
1,990✔
1630
                        }
1,990✔
1631
                } else if (is_nesting_close(is)) {
7,526✔
1632
                        const popped = was.outter_stack.pop();
2,173✔
1633

2,173✔
1634
                        if (popped instanceof TokenSpan) {
2,173✔
1635
                                if (
1,990✔
1636
                                        'nesting_open' === popped.type
1,990✔
1637
                                        && '' === was.operation
1,990✔
1638
                                        && undefined !== was.left_operand
1,990✔
1639
                                        && undefined === was.right_operand
1,990✔
1640
                                ) {
1,990✔
1641
                                        // no-op, deliberately do nothing
1,984✔
1642
                                } else {
1,990✔
1643
                                        throw new TokenScanParseError(
6✔
1644
                                                // eslint-disable-next-line max-len
6✔
1645
                                                'token span popping in this context not yet implemented',
6✔
1646
                                                scan,
6✔
1647
                                                was,
6✔
1648
                                                is
6✔
1649
                                        );
6✔
1650
                                }
6✔
1651
                        } else if (undefined === popped) {
2,173✔
1652
                                if (
93✔
1653
                                        index !== (scan.tokens.length - 1)
93✔
1654
                                        && (
93✔
1655
                                                '' !== was.operation
12✔
1656
                                                || undefined !== was.right_operand
12✔
1657
                                        )
93✔
1658
                                ) {
93!
1659
                                        throw new TokenScanParseError(
×
1660
                                                'Token scan finished with incomplete parse!',
×
1661
                                                scan,
×
1662
                                                was,
×
1663
                                                is
×
1664
                                        );
×
1665
                                }
×
1666
                        } else {
183✔
1667
                                if (
90✔
1668
                                        '' === was.operation
90✔
1669
                                        && undefined !== was.left_operand
90✔
1670
                                        && undefined === was.right_operand
90✔
1671
                                ) {
90✔
1672
                                        was.left_operand = new IntermediaryCalculation(
88✔
1673
                                                popped.left_operand,
88✔
1674
                                                popped.operation,
88✔
1675
                                                was.left_operand
88✔
1676
                                        );
88✔
1677
                                        was.operation ='';
88✔
1678
                                        was.operand_mode = 'right';
88✔
1679
                                } else {
90✔
1680
                                        throw new TokenScanParseError(
2✔
1681
                                                // eslint-disable-next-line max-len
2✔
1682
                                                'token span popping in this context not yet implemented',
2✔
1683
                                                scan,
2✔
1684
                                                was,
2✔
1685
                                                is
2✔
1686
                                        );
2✔
1687
                                }
2✔
1688
                        }
90✔
1689
                } else if (is_numeric(is)) {
5,353✔
1690
                        if ('left' === was.operand_mode) {
1,959✔
1691
                                was.left_operand = IntermediaryNumber.create(
921✔
1692
                                        scan.value.substring(
921✔
1693
                                                is.from,
921✔
1694
                                                is.to
921✔
1695
                                        )
921✔
1696
                                );
921✔
1697
                                was.operand_mode = 'right';
921✔
1698
                        } else {
1,954✔
1699
                                if ('' === was.operation) {
1,038!
1700
                                        throw new TokenScanParseError(
×
1701
                                                'Right operand detected without operation!',
×
1702
                                                scan,
×
1703
                                                was,
×
1704
                                                is
×
1705
                                        );
×
1706
                                } else if (undefined === was.left_operand) {
1,038!
1707
                                        throw new TokenScanParseError(
×
1708
                                                'Right operand detected without left operand!',
×
1709
                                                scan,
×
1710
                                                was,
×
1711
                                                is
×
1712
                                        );
×
1713
                                }
×
1714

1,038✔
1715
                                let resolved = new IntermediaryCalculation(
1,038✔
1716
                                        was.left_operand,
1,038✔
1717
                                        was.operation,
1,038✔
1718
                                        IntermediaryNumber.create(scan.value.substring(
1,038✔
1719
                                                is.from,
1,038✔
1720
                                                is.to
1,038✔
1721
                                        )),
1,038✔
1722
                                );
1,038✔
1723

1,038✔
1724
                                if (
1,038✔
1725
                                        was.outter_stack.length > 0
1,038✔
1726
                                        && ! (
1,038✔
1727
                                                was.outter_stack[
585✔
1728
                                                        was.outter_stack.length - 1
585✔
1729
                                                ] instanceof TokenSpan
585✔
1730
                                        )
1,038✔
1731
                                ) {
1,038✔
1732
                                        const previous = (
93✔
1733
                                                was.outter_stack.pop()
93✔
1734
                                        ) as incomplete_operation;
93✔
1735

93✔
1736
                                        resolved = new IntermediaryCalculation(
93✔
1737
                                                previous.left_operand,
93✔
1738
                                                previous.operation,
93✔
1739
                                                resolved
93✔
1740
                                        );
93✔
1741
                                }
93✔
1742

1,038✔
1743
                                was.left_operand = resolved;
1,038✔
1744
                                was.operation = '';
1,038✔
1745
                                was.right_operand = undefined;
1,038✔
1746
                        }
1,038✔
1747
                } else if ('operation' === is.type) {
3,180✔
1748
                        if (undefined === was.left_operand) {
1,221!
1749
                                throw new TokenScanParseError(
×
1750
                                        'Operation detected without left operand!',
×
1751
                                        scan,
×
1752
                                        was,
×
1753
                                        is
×
1754
                                );
×
1755
                        } else if ('' !== was.operation) {
1,221!
1756
                                throw new TokenScanParseError(
×
1757
                                        `Cannot set operation when operation already set to "${
×
1758
                                                was.operation
×
1759
                                        }"`,
×
1760
                                        scan,
×
1761
                                        was,
×
1762
                                        is
×
1763
                                )
×
1764
                        }
×
1765
                        const maybe = scan.value.substring(is.from, is.to);
1,221✔
1766
                        is_operation_value(maybe);
1,221✔
1767

1,221✔
1768
                        was.operation = maybe;
1,221✔
1769
                } else {
1,221!
1770
                        throw new TokenScanParseError(
×
1771
                                'not implemented',
×
1772
                                scan,
×
1773
                                was,
×
1774
                                is
×
1775
                        );
×
1776
                }
×
1777

7,518✔
1778
                return was;
7,518✔
1779
        }
7,518✔
1780
}
745✔
1781

1✔
1782
//#endregion
1✔
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