@signpostmarv/intermediary-number 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +4 -0
- package/index.js +4 -0
- package/lib/Docs.json.js +0 -1
- package/lib/IntermediaryNumber.d.ts +95 -16
- package/lib/IntermediaryNumber.js +457 -6
- package/lib/IntermediaryNumberTypes.js +0 -1
- package/lib/NumberStrings.js +0 -1
- package/lib/Numbers.js +0 -1
- package/package.json +10 -9
- package/data/.gitkeep +0 -0
- package/lib/TokenScan.d.ts +0 -48
- package/lib/TokenScan.js +0 -302
package/index.d.ts
ADDED
package/index.js
ADDED
package/lib/Docs.json.js
CHANGED
|
@@ -4,7 +4,43 @@ import { amount_string } from './NumberStrings';
|
|
|
4
4
|
import type { input_types, type_property_types, value_types } from './IntermediaryNumberTypes';
|
|
5
5
|
export type math_types = operand_types | input_types;
|
|
6
6
|
export declare const regex_recurring_number: RegExp;
|
|
7
|
-
export type CanDoMath_result_types = IntermediaryNumber | IntermediaryCalculation;
|
|
7
|
+
export type CanDoMath_result_types = IntermediaryNumber | IntermediaryCalculation | TokenScan;
|
|
8
|
+
export type operation_types = '+' | '-' | '*' | 'x' | '/' | '%';
|
|
9
|
+
export type operand_type_property_types = type_property_types | 'IntermediaryCalculation' | 'TokenScan';
|
|
10
|
+
export type CanConvertTypeJson = {
|
|
11
|
+
type: 'IntermediaryNumber';
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'IntermediaryCalculation';
|
|
15
|
+
left: CanConvertTypeJson;
|
|
16
|
+
operation: operation_types;
|
|
17
|
+
right: CanConvertTypeJson;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'TokenScan';
|
|
20
|
+
value: string;
|
|
21
|
+
};
|
|
22
|
+
export type CanDoMathWithDispose_operator_types = 'divide' | 'minus' | 'modulo' | 'plus' | 'times';
|
|
23
|
+
export type operand_types = IntermediaryNumber | IntermediaryCalculation | TokenScan;
|
|
24
|
+
type TokenSpan_types = 'ignore' | 'nesting_open' | 'nesting_close' | 'numeric' | 'operation';
|
|
25
|
+
type TokenSpan_types_part_baked = Exclude<TokenSpan_types, 'ignore'>;
|
|
26
|
+
type TokenScan_internals = {
|
|
27
|
+
parsed: IntermediaryNumber | IntermediaryCalculation | undefined;
|
|
28
|
+
tokens: (TokenSpan<TokenSpan_types_part_baked>[]) | undefined;
|
|
29
|
+
valid: boolean | undefined;
|
|
30
|
+
};
|
|
31
|
+
type TokenScan_parsing_value = Omit<TokenScan, 'is_valid' | 'parsed'>;
|
|
32
|
+
type TokenScan_tokenizer_operand_buffer = IntermediaryNumber | IntermediaryCalculation | undefined;
|
|
33
|
+
type incomplete_operation = {
|
|
34
|
+
left_operand: Exclude<TokenScan_tokenizer_operand_buffer, undefined>;
|
|
35
|
+
operation: operation_types;
|
|
36
|
+
};
|
|
37
|
+
type TokenScan_tokenizer = {
|
|
38
|
+
outter_stack: (incomplete_operation | TokenSpan<'nesting_open'>)[];
|
|
39
|
+
left_operand: TokenScan_tokenizer_operand_buffer;
|
|
40
|
+
right_operand: TokenScan_tokenizer_operand_buffer;
|
|
41
|
+
operation: '' | operation_types;
|
|
42
|
+
operand_mode: 'left' | 'right';
|
|
43
|
+
};
|
|
8
44
|
interface HasType {
|
|
9
45
|
get type(): operand_type_property_types;
|
|
10
46
|
}
|
|
@@ -16,21 +52,12 @@ interface CanDoMath<ResultType extends CanDoMath_result_types = CanDoMath_result
|
|
|
16
52
|
modulo(value: math_types): ResultType;
|
|
17
53
|
plus(value: math_types): ResultType;
|
|
18
54
|
times(value: math_types): ResultType;
|
|
19
|
-
abs(): (
|
|
55
|
+
abs(): (operand_types);
|
|
20
56
|
max(first: math_types, ...remaining: math_types[]): math_types;
|
|
21
57
|
}
|
|
22
58
|
interface CanResolveMath<T extends CanDoMath_result_types = CanDoMath_result_types> extends CanDoMath<T, string> {
|
|
23
59
|
resolve(): IntermediaryNumber;
|
|
24
60
|
}
|
|
25
|
-
export type CanConvertTypeJson = {
|
|
26
|
-
type: 'IntermediaryNumber';
|
|
27
|
-
value: string;
|
|
28
|
-
} | {
|
|
29
|
-
type: 'IntermediaryCalculation';
|
|
30
|
-
left: CanConvertTypeJson;
|
|
31
|
-
operation: operation_types;
|
|
32
|
-
right: CanConvertTypeJson;
|
|
33
|
-
};
|
|
34
61
|
export interface CanConvertType extends HasType {
|
|
35
62
|
toAmountString(): amount_string;
|
|
36
63
|
toBigNumber(): BigNumber;
|
|
@@ -43,7 +70,6 @@ export interface CanConvertType extends HasType {
|
|
|
43
70
|
isZero(): boolean;
|
|
44
71
|
toJSON(): CanConvertTypeJson;
|
|
45
72
|
}
|
|
46
|
-
type CanDoMathWithDispose_operator_types = 'divide' | 'minus' | 'modulo' | 'plus' | 'times';
|
|
47
73
|
interface CanDoMathWithDispose<ResultType extends CanDoMath_result_types = CanDoMath_result_types, ResolveString extends string = type_property_types> extends CanConvertType, CanDoMath<ResultType, ResolveString> {
|
|
48
74
|
do_math_then_dispose(operator: CanDoMathWithDispose_operator_types, right_operand: math_types): ResultType;
|
|
49
75
|
}
|
|
@@ -76,6 +102,7 @@ export declare class IntermediaryNumber implements CanDoMathWithDispose {
|
|
|
76
102
|
toFraction(): Fraction;
|
|
77
103
|
toJSON(): CanConvertTypeJson;
|
|
78
104
|
toString(): string;
|
|
105
|
+
toStringCalculation(): string;
|
|
79
106
|
static create(input: input_types): IntermediaryNumber;
|
|
80
107
|
static create_if_valid(input: string): operand_types | NotValid;
|
|
81
108
|
static fromJson(json: CanConvertTypeJson): CanDoMath_result_types;
|
|
@@ -86,9 +113,6 @@ export declare class NotValid extends Error {
|
|
|
86
113
|
readonly value: string;
|
|
87
114
|
constructor(not_valid: string, reason: unknown);
|
|
88
115
|
}
|
|
89
|
-
export type operand_types = IntermediaryNumber | IntermediaryCalculation;
|
|
90
|
-
export type operation_types = '+' | '-' | '*' | 'x' | '/' | '%';
|
|
91
|
-
export type operand_type_property_types = type_property_types | 'IntermediaryCalculation';
|
|
92
116
|
export declare class IntermediaryCalculation implements CanResolveMathWithDispose {
|
|
93
117
|
readonly left_operand: operand_types;
|
|
94
118
|
readonly operation: operation_types;
|
|
@@ -118,9 +142,64 @@ export declare class IntermediaryCalculation implements CanResolveMathWithDispos
|
|
|
118
142
|
toFraction(): Fraction;
|
|
119
143
|
toJSON(): CanConvertTypeJson;
|
|
120
144
|
toString(): string;
|
|
145
|
+
toStringCalculation(): string;
|
|
121
146
|
private operand_to_IntermediaryNumber;
|
|
122
147
|
static fromString(input: Exclude<string, ''>): IntermediaryNumber | IntermediaryCalculation;
|
|
123
|
-
static
|
|
148
|
+
static is(maybe: unknown): maybe is IntermediaryCalculation;
|
|
149
|
+
static maybe_reduce_operands(left: operand_types, operation: operation_types, right: operand_types): IntermediaryNumber | TokenScan;
|
|
150
|
+
static require_is(maybe: unknown): asserts maybe is IntermediaryCalculation;
|
|
124
151
|
private static maybe_short_circuit;
|
|
125
152
|
}
|
|
153
|
+
declare class TokenSpan<T = TokenSpan_types> {
|
|
154
|
+
readonly from: number;
|
|
155
|
+
readonly to: number;
|
|
156
|
+
readonly type: T;
|
|
157
|
+
constructor(from: number, to: number, type: T);
|
|
158
|
+
}
|
|
159
|
+
export declare class TokenScanError extends Error {
|
|
160
|
+
}
|
|
161
|
+
export declare class TokenScanParseError extends Error {
|
|
162
|
+
readonly current?: TokenSpan<TokenSpan_types>;
|
|
163
|
+
readonly scan: TokenScan_parsing_value;
|
|
164
|
+
readonly state?: TokenScan_tokenizer;
|
|
165
|
+
constructor(message: string, scan: TokenScan_parsing_value, state: TokenScan_tokenizer, current?: TokenSpan<TokenSpan_types>);
|
|
166
|
+
}
|
|
167
|
+
export declare class TokenScan implements CanResolveMathWithDispose {
|
|
168
|
+
private readonly internal;
|
|
169
|
+
readonly value: string | [TokenScan, operation_types, math_types];
|
|
170
|
+
private constructor();
|
|
171
|
+
get parsed(): Exclude<TokenScan_internals['parsed'], undefined>;
|
|
172
|
+
get resolve_type(): string;
|
|
173
|
+
get tokens(): Exclude<TokenScan_internals['tokens'], undefined>;
|
|
174
|
+
get type(): operand_type_property_types;
|
|
175
|
+
get valid(): boolean;
|
|
176
|
+
abs(): operand_types;
|
|
177
|
+
compare(value: math_types): 0 | 1 | -1;
|
|
178
|
+
divide(value: math_types): TokenScan;
|
|
179
|
+
do_math_then_dispose(operator: CanDoMathWithDispose_operator_types, right_operand: math_types): CanDoMath_result_types;
|
|
180
|
+
isGreaterThan(value: math_types): boolean;
|
|
181
|
+
isLessThan(value: math_types): boolean;
|
|
182
|
+
isOne(): boolean;
|
|
183
|
+
isZero(): boolean;
|
|
184
|
+
max(first: math_types, ...remaining: math_types[]): math_types;
|
|
185
|
+
minus(value: math_types): TokenScan;
|
|
186
|
+
modulo(value: math_types): TokenScan;
|
|
187
|
+
plus(value: math_types): TokenScan;
|
|
188
|
+
resolve(): IntermediaryNumber;
|
|
189
|
+
times(value: math_types): TokenScan;
|
|
190
|
+
toAmountString(): amount_string;
|
|
191
|
+
toBigNumber(): BigNumber;
|
|
192
|
+
toBigNumberOrFraction(): BigNumber | Fraction;
|
|
193
|
+
toFraction(): Fraction;
|
|
194
|
+
toJSON(): CanConvertTypeJson;
|
|
195
|
+
toString(): string;
|
|
196
|
+
toStringCalculation(): string;
|
|
197
|
+
static create(value: string): TokenScan;
|
|
198
|
+
static is(maybe: unknown): maybe is TokenScan;
|
|
199
|
+
static require_is(maybe: unknown): asserts maybe is TokenScan;
|
|
200
|
+
private static determine_tokens_from_scan;
|
|
201
|
+
private static massage_part_baked_tokens;
|
|
202
|
+
private static parse_scan;
|
|
203
|
+
private static reduce;
|
|
204
|
+
}
|
|
126
205
|
export {};
|
|
@@ -3,8 +3,9 @@ import BigNumber from 'bignumber.js';
|
|
|
3
3
|
import Fraction from 'fraction.js';
|
|
4
4
|
import { is_string, } from './Docs.json';
|
|
5
5
|
import { NumberStrings, } from './NumberStrings';
|
|
6
|
-
import { TokenScan, } from './TokenScan';
|
|
7
6
|
export const regex_recurring_number = /^-?(\d+\.)(\d+r|\d*\[\d+\]r?|\d*\(\d+\)r?)$/;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region utility functions
|
|
8
9
|
function do_math(left_operand, operator, right_operand) {
|
|
9
10
|
return IntermediaryCalculation.maybe_reduce_operands(left_operand, operator, IntermediaryNumber.reuse_or_create(right_operand));
|
|
10
11
|
}
|
|
@@ -85,6 +86,34 @@ function max(first, second, ...remaining) {
|
|
|
85
86
|
}
|
|
86
87
|
return IntermediaryNumber.reuse_or_create(max);
|
|
87
88
|
}
|
|
89
|
+
//#region TokenScan utility functions
|
|
90
|
+
function default_tokenizer_state() {
|
|
91
|
+
return {
|
|
92
|
+
outter_stack: [],
|
|
93
|
+
left_operand: undefined,
|
|
94
|
+
operation: '',
|
|
95
|
+
right_operand: undefined,
|
|
96
|
+
operand_mode: 'left',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function is_nesting_open(maybe) {
|
|
100
|
+
return 'nesting_open' === maybe.type;
|
|
101
|
+
}
|
|
102
|
+
function is_nesting_close(maybe) {
|
|
103
|
+
return 'nesting_close' === maybe.type;
|
|
104
|
+
}
|
|
105
|
+
function is_numeric(maybe) {
|
|
106
|
+
return 'numeric' === maybe.type;
|
|
107
|
+
}
|
|
108
|
+
function is_operation_value(maybe) {
|
|
109
|
+
if (!(maybe.length === 1
|
|
110
|
+
&& '+-/x*%'.includes(maybe))) {
|
|
111
|
+
throw new TokenScanError(`Expected operation value, found "${maybe}"`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region IntermediaryNumber
|
|
88
117
|
export class IntermediaryNumber {
|
|
89
118
|
value;
|
|
90
119
|
static One = new this('1');
|
|
@@ -242,6 +271,9 @@ export class IntermediaryNumber {
|
|
|
242
271
|
}
|
|
243
272
|
return this.value.toString();
|
|
244
273
|
}
|
|
274
|
+
toStringCalculation() {
|
|
275
|
+
return this.toString();
|
|
276
|
+
}
|
|
245
277
|
static create(input) {
|
|
246
278
|
if ('' === input) {
|
|
247
279
|
return IntermediaryNumber.Zero;
|
|
@@ -281,7 +313,7 @@ export class IntermediaryNumber {
|
|
|
281
313
|
return IntermediaryNumber.create(maybe);
|
|
282
314
|
}
|
|
283
315
|
else if (/^(\d+|\d*\.\d+)\s*[+/*x%-]\s*(\d+|\d*\.\d+)$/.test(maybe)) {
|
|
284
|
-
return
|
|
316
|
+
return TokenScan.create(input).parsed;
|
|
285
317
|
}
|
|
286
318
|
const scientific = /^(-?\d+(?:\.\d+))e([+-])(\d+)$/.exec(maybe);
|
|
287
319
|
if (scientific) {
|
|
@@ -299,15 +331,21 @@ export class IntermediaryNumber {
|
|
|
299
331
|
if ('IntermediaryNumber' === json.type) {
|
|
300
332
|
return this.create(json.value);
|
|
301
333
|
}
|
|
334
|
+
else if ('TokenScan' === json.type) {
|
|
335
|
+
return TokenScan.create(json.value);
|
|
336
|
+
}
|
|
302
337
|
return new IntermediaryCalculation(this.fromJson(json.left), json.operation, this.fromJson(json.right));
|
|
303
338
|
}
|
|
304
339
|
static reuse_or_create(input) {
|
|
305
340
|
return (((input instanceof IntermediaryNumber)
|
|
306
|
-
|| (input instanceof IntermediaryCalculation)
|
|
341
|
+
|| (input instanceof IntermediaryCalculation)
|
|
342
|
+
|| (input instanceof TokenScan))
|
|
307
343
|
? input
|
|
308
344
|
: this.create(input));
|
|
309
345
|
}
|
|
310
346
|
}
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region IntermediaryCalculation
|
|
311
349
|
export class NotValid extends Error {
|
|
312
350
|
reason;
|
|
313
351
|
value;
|
|
@@ -474,8 +512,16 @@ export class IntermediaryCalculation {
|
|
|
474
512
|
cache.set(this, value);
|
|
475
513
|
return value;
|
|
476
514
|
}
|
|
515
|
+
toStringCalculation() {
|
|
516
|
+
return `${(this.left_operand instanceof IntermediaryCalculation)
|
|
517
|
+
? `(${this.left_operand.toStringCalculation()})`
|
|
518
|
+
: this.left_operand.toString()} ${this.operation} ${(this.right_operand instanceof IntermediaryCalculation)
|
|
519
|
+
? `(${this.right_operand.toStringCalculation()})`
|
|
520
|
+
: this.right_operand.toString()}`;
|
|
521
|
+
}
|
|
477
522
|
operand_to_IntermediaryNumber(operand) {
|
|
478
|
-
if ((operand instanceof IntermediaryCalculation)
|
|
523
|
+
if ((operand instanceof IntermediaryCalculation)
|
|
524
|
+
|| (operand instanceof TokenScan)) {
|
|
479
525
|
return operand.resolve();
|
|
480
526
|
}
|
|
481
527
|
else if ('amount_string' === operand.type
|
|
@@ -487,7 +533,10 @@ export class IntermediaryCalculation {
|
|
|
487
533
|
return operand;
|
|
488
534
|
}
|
|
489
535
|
static fromString(input) {
|
|
490
|
-
return
|
|
536
|
+
return TokenScan.create(input).parsed;
|
|
537
|
+
}
|
|
538
|
+
static is(maybe) {
|
|
539
|
+
return maybe instanceof this;
|
|
491
540
|
}
|
|
492
541
|
static maybe_reduce_operands(left, operation, right) {
|
|
493
542
|
let value = this.maybe_short_circuit(left, operation, right);
|
|
@@ -499,6 +548,11 @@ export class IntermediaryCalculation {
|
|
|
499
548
|
}
|
|
500
549
|
return value;
|
|
501
550
|
}
|
|
551
|
+
static require_is(maybe) {
|
|
552
|
+
if (!this.is(maybe)) {
|
|
553
|
+
throw new Error('Argument is not an instanceof IntermediaryCalculation');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
502
556
|
static maybe_short_circuit(left, operation, right) {
|
|
503
557
|
let value = undefined;
|
|
504
558
|
if ('+' === operation) {
|
|
@@ -526,4 +580,401 @@ export class IntermediaryCalculation {
|
|
|
526
580
|
return value;
|
|
527
581
|
}
|
|
528
582
|
}
|
|
529
|
-
//#
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region TokenScan
|
|
585
|
+
class TokenSpan {
|
|
586
|
+
from;
|
|
587
|
+
to;
|
|
588
|
+
type;
|
|
589
|
+
constructor(from, to, type) {
|
|
590
|
+
this.from = from;
|
|
591
|
+
this.to = to;
|
|
592
|
+
this.type = type;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
export class TokenScanError extends Error {
|
|
596
|
+
}
|
|
597
|
+
export class TokenScanParseError extends Error {
|
|
598
|
+
current;
|
|
599
|
+
scan;
|
|
600
|
+
state;
|
|
601
|
+
constructor(message, scan, state, current) {
|
|
602
|
+
super(message);
|
|
603
|
+
this.scan = scan;
|
|
604
|
+
this.state = state;
|
|
605
|
+
this.current = current;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const regex_numeric = (/(?:\d*\.\d*\(\d+\)r?|\d*\.\d*\[\d+\]r?|\d+(?:\.\d+r)?|\.\d+r?)/g);
|
|
609
|
+
export class TokenScan {
|
|
610
|
+
internal = {
|
|
611
|
+
parsed: undefined,
|
|
612
|
+
tokens: undefined,
|
|
613
|
+
valid: undefined,
|
|
614
|
+
};
|
|
615
|
+
value;
|
|
616
|
+
constructor(value) {
|
|
617
|
+
this.value = value;
|
|
618
|
+
}
|
|
619
|
+
get parsed() {
|
|
620
|
+
if (undefined === this.internal.parsed) {
|
|
621
|
+
this.internal.parsed = TokenScan.parse_scan(this);
|
|
622
|
+
}
|
|
623
|
+
return this.internal.parsed;
|
|
624
|
+
}
|
|
625
|
+
get resolve_type() {
|
|
626
|
+
return this.parsed.resolve_type;
|
|
627
|
+
}
|
|
628
|
+
get tokens() {
|
|
629
|
+
if (undefined === this.internal.tokens) {
|
|
630
|
+
this.internal.tokens = TokenScan.determine_tokens_from_scan(this);
|
|
631
|
+
}
|
|
632
|
+
return this.internal.tokens;
|
|
633
|
+
}
|
|
634
|
+
get type() {
|
|
635
|
+
return 'TokenScan';
|
|
636
|
+
}
|
|
637
|
+
get valid() {
|
|
638
|
+
if (undefined === this.internal.valid) {
|
|
639
|
+
try {
|
|
640
|
+
this.parsed;
|
|
641
|
+
this.internal.valid = true;
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
this.internal.valid = false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return this.internal.valid;
|
|
648
|
+
}
|
|
649
|
+
abs() {
|
|
650
|
+
return this.parsed.abs();
|
|
651
|
+
}
|
|
652
|
+
compare(value) {
|
|
653
|
+
return this.parsed.compare(value);
|
|
654
|
+
}
|
|
655
|
+
divide(value) {
|
|
656
|
+
return new TokenScan([
|
|
657
|
+
this,
|
|
658
|
+
'/',
|
|
659
|
+
value,
|
|
660
|
+
]);
|
|
661
|
+
}
|
|
662
|
+
do_math_then_dispose(operator, right_operand) {
|
|
663
|
+
const result = this.parsed.do_math_then_dispose(operator, right_operand);
|
|
664
|
+
this.internal.parsed = undefined;
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
isGreaterThan(value) {
|
|
668
|
+
return this.parsed.isGreaterThan(value);
|
|
669
|
+
}
|
|
670
|
+
isLessThan(value) {
|
|
671
|
+
return this.parsed.isLessThan(value);
|
|
672
|
+
}
|
|
673
|
+
isOne() {
|
|
674
|
+
return ((is_string(this.value)
|
|
675
|
+
&& '1' === this.value.trim())
|
|
676
|
+
|| this.parsed.isOne());
|
|
677
|
+
}
|
|
678
|
+
isZero() {
|
|
679
|
+
return ((is_string(this.value)
|
|
680
|
+
&& '0' === this.value.trim())
|
|
681
|
+
|| this.parsed.isZero());
|
|
682
|
+
}
|
|
683
|
+
max(first, ...remaining) {
|
|
684
|
+
return this.parsed.max(first, ...remaining);
|
|
685
|
+
}
|
|
686
|
+
minus(value) {
|
|
687
|
+
return new TokenScan([
|
|
688
|
+
this,
|
|
689
|
+
'-',
|
|
690
|
+
value,
|
|
691
|
+
]);
|
|
692
|
+
}
|
|
693
|
+
modulo(value) {
|
|
694
|
+
return new TokenScan([
|
|
695
|
+
this,
|
|
696
|
+
'%',
|
|
697
|
+
value,
|
|
698
|
+
]);
|
|
699
|
+
}
|
|
700
|
+
plus(value) {
|
|
701
|
+
return new TokenScan([
|
|
702
|
+
this,
|
|
703
|
+
'+',
|
|
704
|
+
value,
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
resolve() {
|
|
708
|
+
const parsed = this.parsed;
|
|
709
|
+
return IntermediaryCalculation.is(parsed) ? parsed.resolve() : parsed;
|
|
710
|
+
}
|
|
711
|
+
times(value) {
|
|
712
|
+
return new TokenScan([
|
|
713
|
+
this,
|
|
714
|
+
'x',
|
|
715
|
+
value,
|
|
716
|
+
]);
|
|
717
|
+
}
|
|
718
|
+
toAmountString() {
|
|
719
|
+
return this.parsed.toAmountString();
|
|
720
|
+
}
|
|
721
|
+
toBigNumber() {
|
|
722
|
+
return this.parsed.toBigNumber();
|
|
723
|
+
}
|
|
724
|
+
toBigNumberOrFraction() {
|
|
725
|
+
return this.parsed.toBigNumberOrFraction();
|
|
726
|
+
}
|
|
727
|
+
toFraction() {
|
|
728
|
+
return this.parsed.toFraction();
|
|
729
|
+
}
|
|
730
|
+
toJSON() {
|
|
731
|
+
return {
|
|
732
|
+
type: 'TokenScan',
|
|
733
|
+
value: this.toStringCalculation(),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
toString() {
|
|
737
|
+
return this.parsed.toString();
|
|
738
|
+
}
|
|
739
|
+
toStringCalculation() {
|
|
740
|
+
if (this.value instanceof Array) {
|
|
741
|
+
const left_operand = this.value[0];
|
|
742
|
+
return `${(left_operand.parsed instanceof IntermediaryNumber)
|
|
743
|
+
? left_operand.toString()
|
|
744
|
+
: `(${left_operand.toStringCalculation()})`} ${this.value[1]} ${IntermediaryNumber.reuse_or_create(this.value[2]).toStringCalculation()}`;
|
|
745
|
+
}
|
|
746
|
+
return this.value;
|
|
747
|
+
}
|
|
748
|
+
static create(value) {
|
|
749
|
+
return new TokenScan(value);
|
|
750
|
+
}
|
|
751
|
+
static is(maybe) {
|
|
752
|
+
return maybe instanceof TokenScan;
|
|
753
|
+
}
|
|
754
|
+
static require_is(maybe) {
|
|
755
|
+
if (!this.is(maybe)) {
|
|
756
|
+
throw new Error('Argument is not an instanceof TokenScan');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
static determine_tokens_from_scan(scan) {
|
|
760
|
+
let tokens = [];
|
|
761
|
+
const value = scan.toStringCalculation();
|
|
762
|
+
for (const entry of value.matchAll(/([\s]+)/g)) {
|
|
763
|
+
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'ignore'));
|
|
764
|
+
}
|
|
765
|
+
for (const entry of value.matchAll(regex_numeric)) {
|
|
766
|
+
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'numeric'));
|
|
767
|
+
}
|
|
768
|
+
for (const entry of value.matchAll(/([+/*x%-])/g)) {
|
|
769
|
+
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'operation'));
|
|
770
|
+
}
|
|
771
|
+
for (const entry of value.matchAll(/(\()/g)) {
|
|
772
|
+
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'nesting_open'));
|
|
773
|
+
}
|
|
774
|
+
for (const entry of value.matchAll(/(\))/g)) {
|
|
775
|
+
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'nesting_close'));
|
|
776
|
+
}
|
|
777
|
+
tokens = tokens.sort((a, b) => {
|
|
778
|
+
return a.from - b.from;
|
|
779
|
+
});
|
|
780
|
+
const recursive_numerics = tokens.filter(maybe => ('numeric' === maybe.type
|
|
781
|
+
&& /[()]/.test(value.substring(maybe.from, maybe.to))));
|
|
782
|
+
tokens = tokens.filter((maybe) => {
|
|
783
|
+
if ('nesting_open' === maybe.type
|
|
784
|
+
|| 'nesting_close' === maybe.type) {
|
|
785
|
+
return !recursive_numerics.find(maybe_numeric => (maybe.from >= maybe_numeric.from
|
|
786
|
+
&& maybe.to <= maybe_numeric.to));
|
|
787
|
+
}
|
|
788
|
+
return true;
|
|
789
|
+
});
|
|
790
|
+
if (tokens.length < 1) {
|
|
791
|
+
throw new TokenScanError('No tokens found!');
|
|
792
|
+
}
|
|
793
|
+
else if (0 !== tokens[0].from) {
|
|
794
|
+
throw new TokenScanError('First token not at index 0!');
|
|
795
|
+
}
|
|
796
|
+
else if (value.length !== tokens[tokens.length - 1].to) {
|
|
797
|
+
throw new TokenScanError('Last token does not end at end of string!');
|
|
798
|
+
}
|
|
799
|
+
let nesting_balance = 0;
|
|
800
|
+
for (let index = 0; index < tokens.length; ++index) {
|
|
801
|
+
const token = tokens[index];
|
|
802
|
+
if ('nesting_open' === token.type) {
|
|
803
|
+
nesting_balance += (token.to - token.from);
|
|
804
|
+
}
|
|
805
|
+
else if ('nesting_close' === token.type) {
|
|
806
|
+
nesting_balance -= (token.to - token.from);
|
|
807
|
+
}
|
|
808
|
+
if (index > 0
|
|
809
|
+
&& tokens[index - 1].to !== token.from) {
|
|
810
|
+
console.error(tokens, index);
|
|
811
|
+
throw new TokenScanError(`Token expected to be found at index ${index}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (0 !== nesting_balance) {
|
|
815
|
+
throw new TokenScanError('Imbalanced nesting in string!');
|
|
816
|
+
}
|
|
817
|
+
return this.massage_part_baked_tokens(scan, tokens.filter((maybe) => 'ignore' !== maybe.type));
|
|
818
|
+
}
|
|
819
|
+
static massage_part_baked_tokens(scan, tokens) {
|
|
820
|
+
const smoosh_numerics = [];
|
|
821
|
+
const value = scan.toStringCalculation();
|
|
822
|
+
for (let token_index = tokens.length - 1; token_index > 0; --token_index) {
|
|
823
|
+
const previous = tokens[token_index - 1];
|
|
824
|
+
const current = tokens[token_index];
|
|
825
|
+
if ('numeric' === previous.type) {
|
|
826
|
+
const previous_value = value.substring(previous.from, previous.to);
|
|
827
|
+
const current_value = value.substring(current.from, current.to);
|
|
828
|
+
if (current_value.startsWith('.')
|
|
829
|
+
&& /^\d+$/.test(previous_value)) {
|
|
830
|
+
smoosh_numerics.push(token_index);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
for (const index of smoosh_numerics) {
|
|
835
|
+
tokens.splice(index - 1, 2, new TokenSpan(tokens[index - 1].from, tokens[index].to, 'numeric'));
|
|
836
|
+
}
|
|
837
|
+
const convert_to_negative = [];
|
|
838
|
+
if (tokens.length >= 2
|
|
839
|
+
&& 'operation' === tokens[0].type
|
|
840
|
+
&& '-' === value[tokens[0].from]
|
|
841
|
+
&& 'numeric' === tokens[1].type) {
|
|
842
|
+
convert_to_negative.push(0);
|
|
843
|
+
}
|
|
844
|
+
for (let token_index = 0; token_index < tokens.length; ++token_index) {
|
|
845
|
+
const token = tokens[token_index];
|
|
846
|
+
const next = tokens[token_index + 1];
|
|
847
|
+
const after = tokens[token_index + 2];
|
|
848
|
+
if (('nesting_open' === token.type
|
|
849
|
+
|| 'operation' === token.type)
|
|
850
|
+
&& next
|
|
851
|
+
&& after
|
|
852
|
+
&& 'operation' === next.type
|
|
853
|
+
&& '-' === value[next.from]
|
|
854
|
+
&& 'numeric' === after.type) {
|
|
855
|
+
convert_to_negative.push(token_index + 1);
|
|
856
|
+
token_index += 2;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const index of convert_to_negative.reverse()) {
|
|
861
|
+
tokens.splice(index, 2, new TokenSpan(tokens[index].from, tokens[index + 1].to, 'numeric'));
|
|
862
|
+
}
|
|
863
|
+
return tokens;
|
|
864
|
+
}
|
|
865
|
+
static parse_scan(scan) {
|
|
866
|
+
const reduced = scan.tokens.reduce((was, is, index) => TokenScan.reduce(scan, was, is, index), default_tokenizer_state());
|
|
867
|
+
if (undefined !== reduced.left_operand
|
|
868
|
+
&& '' === reduced.operation
|
|
869
|
+
&& undefined === reduced.right_operand
|
|
870
|
+
&& 0 === reduced.outter_stack.length) {
|
|
871
|
+
return reduced.left_operand;
|
|
872
|
+
}
|
|
873
|
+
throw new TokenScanParseError('Parse in unsupported state!', scan, reduced);
|
|
874
|
+
}
|
|
875
|
+
static reduce(scan, was, is, index) {
|
|
876
|
+
const value = scan.toStringCalculation();
|
|
877
|
+
if (is_nesting_open(is)) {
|
|
878
|
+
if ('right' === was.operand_mode) {
|
|
879
|
+
if (undefined === was.left_operand) {
|
|
880
|
+
if (!(was.outter_stack.length > 0
|
|
881
|
+
&& !(was.outter_stack[was.outter_stack.length - 1] instanceof TokenSpan))) {
|
|
882
|
+
throw new TokenScanParseError(
|
|
883
|
+
// eslint-disable-next-line max-len
|
|
884
|
+
'Nesting opened without left operand to push into stack!', scan, was);
|
|
885
|
+
}
|
|
886
|
+
return was;
|
|
887
|
+
}
|
|
888
|
+
else if ('' === was.operation) {
|
|
889
|
+
throw new TokenScanParseError('Nesting opened without operation to push into stack!', scan, was, is);
|
|
890
|
+
}
|
|
891
|
+
was.outter_stack.push({
|
|
892
|
+
left_operand: was.left_operand,
|
|
893
|
+
operation: was.operation,
|
|
894
|
+
});
|
|
895
|
+
was.left_operand = undefined;
|
|
896
|
+
was.operation = '';
|
|
897
|
+
was.operand_mode = 'left';
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
was.outter_stack.push(is);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else if (is_nesting_close(is)) {
|
|
904
|
+
const popped = was.outter_stack.pop();
|
|
905
|
+
if (popped instanceof TokenSpan) {
|
|
906
|
+
if ('nesting_open' === popped.type
|
|
907
|
+
&& '' === was.operation
|
|
908
|
+
&& undefined !== was.left_operand
|
|
909
|
+
&& undefined === was.right_operand) {
|
|
910
|
+
// no-op, deliberately do nothing
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
throw new TokenScanParseError(
|
|
914
|
+
// eslint-disable-next-line max-len
|
|
915
|
+
'token span popping in this context not yet implemented', scan, was, is);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else if (undefined === popped) {
|
|
919
|
+
if (index !== (scan.tokens.length - 1)
|
|
920
|
+
&& ('' !== was.operation
|
|
921
|
+
|| undefined !== was.right_operand)) {
|
|
922
|
+
throw new TokenScanParseError('Token scan finished with incomplete parse!', scan, was, is);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
if ('' === was.operation
|
|
927
|
+
&& undefined !== was.left_operand
|
|
928
|
+
&& undefined === was.right_operand) {
|
|
929
|
+
was.left_operand = new IntermediaryCalculation(popped.left_operand, popped.operation, was.left_operand);
|
|
930
|
+
was.operation = '';
|
|
931
|
+
was.operand_mode = 'right';
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
throw new TokenScanParseError(
|
|
935
|
+
// eslint-disable-next-line max-len
|
|
936
|
+
'token span popping in this context not yet implemented', scan, was, is);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
else if (is_numeric(is)) {
|
|
941
|
+
if ('left' === was.operand_mode) {
|
|
942
|
+
was.left_operand = IntermediaryNumber.create(value.substring(is.from, is.to));
|
|
943
|
+
was.operand_mode = 'right';
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
if ('' === was.operation) {
|
|
947
|
+
throw new TokenScanParseError('Right operand detected without operation!', scan, was, is);
|
|
948
|
+
}
|
|
949
|
+
else if (undefined === was.left_operand) {
|
|
950
|
+
throw new TokenScanParseError('Right operand detected without left operand!', scan, was, is);
|
|
951
|
+
}
|
|
952
|
+
let resolved = new IntermediaryCalculation(was.left_operand, was.operation, IntermediaryNumber.create(value.substring(is.from, is.to)));
|
|
953
|
+
if (was.outter_stack.length > 0
|
|
954
|
+
&& !(was.outter_stack[was.outter_stack.length - 1] instanceof TokenSpan)) {
|
|
955
|
+
const previous = (was.outter_stack.pop());
|
|
956
|
+
resolved = new IntermediaryCalculation(previous.left_operand, previous.operation, resolved);
|
|
957
|
+
}
|
|
958
|
+
was.left_operand = resolved;
|
|
959
|
+
was.operation = '';
|
|
960
|
+
was.right_operand = undefined;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else if ('operation' === is.type) {
|
|
964
|
+
if (undefined === was.left_operand) {
|
|
965
|
+
throw new TokenScanParseError('Operation detected without left operand!', scan, was, is);
|
|
966
|
+
}
|
|
967
|
+
else if ('' !== was.operation) {
|
|
968
|
+
throw new TokenScanParseError(`Cannot set operation when operation already set to "${was.operation}"`, scan, was, is);
|
|
969
|
+
}
|
|
970
|
+
const maybe = value.substring(is.from, is.to);
|
|
971
|
+
is_operation_value(maybe);
|
|
972
|
+
was.operation = maybe;
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
throw new TokenScanParseError('not implemented', scan, was, is);
|
|
976
|
+
}
|
|
977
|
+
return was;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
//#endregion
|
package/lib/NumberStrings.js
CHANGED
package/lib/Numbers.js
CHANGED
package/package.json
CHANGED
|
@@ -8,24 +8,25 @@
|
|
|
8
8
|
"url": "git+https://github.com/SignpostMarv/Intermediary-Number.git"
|
|
9
9
|
},
|
|
10
10
|
"funding": "https://github.com/SignpostMarv/Intermediary-Number?sponsor=1",
|
|
11
|
+
"exports": "./index.js",
|
|
11
12
|
"devDependencies": {
|
|
12
|
-
"@eslint/js": "^9.
|
|
13
|
+
"@eslint/js": "^9.4.0",
|
|
13
14
|
"@stdlib/types": "^0.3.2",
|
|
14
|
-
"@types/eslint": "^8.56.
|
|
15
|
-
"@types/node": "^20.
|
|
15
|
+
"@types/eslint": "^8.56.10",
|
|
16
|
+
"@types/node": "^20.14.2",
|
|
16
17
|
"c8": "^9.1.0",
|
|
17
18
|
"eslint": "^8.57.0",
|
|
18
|
-
"glob": "^10.
|
|
19
|
-
"prettier": "^3.2
|
|
19
|
+
"glob": "^10.4.1",
|
|
20
|
+
"prettier": "^3.3.2",
|
|
20
21
|
"ts-node": "^10.9.2",
|
|
21
|
-
"tslib": "^2.6.
|
|
22
|
+
"tslib": "^2.6.3",
|
|
22
23
|
"typescript": "^5.4.5",
|
|
23
|
-
"typescript-eslint": "^7.
|
|
24
|
+
"typescript-eslint": "^7.13.0"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@satisfactory-clips-archive/custom-assert": "^0.1.
|
|
27
|
+
"@satisfactory-clips-archive/custom-assert": "^0.1.2",
|
|
27
28
|
"bignumber.js": "^9.1.2",
|
|
28
29
|
"fraction.js": "^4.3.7"
|
|
29
30
|
},
|
|
30
|
-
"version": "0.
|
|
31
|
+
"version": "0.2.0"
|
|
31
32
|
}
|
package/data/.gitkeep
DELETED
|
File without changes
|
package/lib/TokenScan.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { IntermediaryCalculation, IntermediaryNumber, operation_types } from './IntermediaryNumber';
|
|
2
|
-
type TokenSpan_types = 'ignore' | 'nesting_open' | 'nesting_close' | 'numeric' | 'operation';
|
|
3
|
-
type TokenSpan_types_part_baked = Exclude<TokenSpan_types, 'ignore'>;
|
|
4
|
-
declare class TokenSpan<T = TokenSpan_types> {
|
|
5
|
-
readonly from: number;
|
|
6
|
-
readonly to: number;
|
|
7
|
-
readonly type: T;
|
|
8
|
-
constructor(from: number, to: number, type: T);
|
|
9
|
-
}
|
|
10
|
-
export declare class TokenScanError extends Error {
|
|
11
|
-
}
|
|
12
|
-
export declare class TokenScanParseError extends Error {
|
|
13
|
-
readonly current?: TokenSpan<TokenSpan_types>;
|
|
14
|
-
readonly scan: TokenScan_parsing_value;
|
|
15
|
-
readonly state?: TokenScan_tokenizer;
|
|
16
|
-
constructor(message: string, scan: TokenScan_parsing_value, state: TokenScan_tokenizer, current?: TokenSpan<TokenSpan_types>);
|
|
17
|
-
}
|
|
18
|
-
type TokenScan_internals = {
|
|
19
|
-
parsed: IntermediaryNumber | IntermediaryCalculation | undefined;
|
|
20
|
-
tokens: (TokenSpan<TokenSpan_types_part_baked>[]) | undefined;
|
|
21
|
-
valid: boolean | undefined;
|
|
22
|
-
};
|
|
23
|
-
type TokenScan_parsing_value = Omit<TokenScan, 'is_valid' | 'parsed'>;
|
|
24
|
-
export declare class TokenScan {
|
|
25
|
-
private readonly internal;
|
|
26
|
-
readonly value: string;
|
|
27
|
-
constructor(value: string);
|
|
28
|
-
get parsed(): Exclude<TokenScan_internals['parsed'], undefined>;
|
|
29
|
-
get tokens(): Exclude<TokenScan_internals['tokens'], undefined>;
|
|
30
|
-
get valid(): boolean;
|
|
31
|
-
private static determine_tokens_from_scan;
|
|
32
|
-
private static massage_part_baked_tokens;
|
|
33
|
-
private static parse_scan;
|
|
34
|
-
private static reduce;
|
|
35
|
-
}
|
|
36
|
-
type TokenScan_tokenizer_operand_buffer = IntermediaryNumber | IntermediaryCalculation | undefined;
|
|
37
|
-
type incomplete_operation = {
|
|
38
|
-
left_operand: Exclude<TokenScan_tokenizer_operand_buffer, undefined>;
|
|
39
|
-
operation: operation_types;
|
|
40
|
-
};
|
|
41
|
-
type TokenScan_tokenizer = {
|
|
42
|
-
outter_stack: (incomplete_operation | TokenSpan<'nesting_open'>)[];
|
|
43
|
-
left_operand: TokenScan_tokenizer_operand_buffer;
|
|
44
|
-
right_operand: TokenScan_tokenizer_operand_buffer;
|
|
45
|
-
operation: '' | operation_types;
|
|
46
|
-
operand_mode: 'left' | 'right';
|
|
47
|
-
};
|
|
48
|
-
export {};
|
package/lib/TokenScan.js
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { IntermediaryCalculation, IntermediaryNumber, } from './IntermediaryNumber';
|
|
2
|
-
class TokenSpan {
|
|
3
|
-
from;
|
|
4
|
-
to;
|
|
5
|
-
type;
|
|
6
|
-
constructor(from, to, type) {
|
|
7
|
-
this.from = from;
|
|
8
|
-
this.to = to;
|
|
9
|
-
this.type = type;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export class TokenScanError extends Error {
|
|
13
|
-
}
|
|
14
|
-
export class TokenScanParseError extends Error {
|
|
15
|
-
current;
|
|
16
|
-
scan;
|
|
17
|
-
state;
|
|
18
|
-
constructor(message, scan, state, current) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.scan = scan;
|
|
21
|
-
this.state = state;
|
|
22
|
-
this.current = current;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const regex_numeric = (/(?:\d*\.\d*\(\d+\)r?|\d*\.\d*\[\d+\]r?|\d+(?:\.\d+r)?|\.\d+r?)/g);
|
|
26
|
-
export class TokenScan {
|
|
27
|
-
internal = {
|
|
28
|
-
parsed: undefined,
|
|
29
|
-
tokens: undefined,
|
|
30
|
-
valid: undefined,
|
|
31
|
-
};
|
|
32
|
-
value;
|
|
33
|
-
constructor(value) {
|
|
34
|
-
this.value = value;
|
|
35
|
-
}
|
|
36
|
-
get parsed() {
|
|
37
|
-
if (undefined === this.internal.parsed) {
|
|
38
|
-
this.internal.parsed = TokenScan.parse_scan(this);
|
|
39
|
-
}
|
|
40
|
-
return this.internal.parsed;
|
|
41
|
-
}
|
|
42
|
-
get tokens() {
|
|
43
|
-
if (undefined === this.internal.tokens) {
|
|
44
|
-
this.internal.tokens = TokenScan.determine_tokens_from_scan(this);
|
|
45
|
-
}
|
|
46
|
-
return this.internal.tokens;
|
|
47
|
-
}
|
|
48
|
-
get valid() {
|
|
49
|
-
if (undefined === this.internal.valid) {
|
|
50
|
-
try {
|
|
51
|
-
this.parsed;
|
|
52
|
-
this.internal.valid = true;
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
this.internal.valid = false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return this.internal.valid;
|
|
59
|
-
}
|
|
60
|
-
static determine_tokens_from_scan(scan) {
|
|
61
|
-
let tokens = [];
|
|
62
|
-
for (const entry of scan.value.matchAll(/([\s]+)/g)) {
|
|
63
|
-
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'ignore'));
|
|
64
|
-
}
|
|
65
|
-
for (const entry of scan.value.matchAll(regex_numeric)) {
|
|
66
|
-
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'numeric'));
|
|
67
|
-
}
|
|
68
|
-
for (const entry of scan.value.matchAll(/([+/*x%-])/g)) {
|
|
69
|
-
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'operation'));
|
|
70
|
-
}
|
|
71
|
-
for (const entry of scan.value.matchAll(/(\()/g)) {
|
|
72
|
-
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'nesting_open'));
|
|
73
|
-
}
|
|
74
|
-
for (const entry of scan.value.matchAll(/(\))/g)) {
|
|
75
|
-
tokens.push(new TokenSpan(entry.index, entry.index + entry[0].length, 'nesting_close'));
|
|
76
|
-
}
|
|
77
|
-
tokens = tokens.sort((a, b) => {
|
|
78
|
-
return a.from - b.from;
|
|
79
|
-
});
|
|
80
|
-
const recursive_numerics = tokens.filter(maybe => ('numeric' === maybe.type
|
|
81
|
-
&& /[()]/.test(scan.value.substring(maybe.from, maybe.to))));
|
|
82
|
-
tokens = tokens.filter((maybe) => {
|
|
83
|
-
if ('nesting_open' === maybe.type
|
|
84
|
-
|| 'nesting_close' === maybe.type) {
|
|
85
|
-
return !recursive_numerics.find(maybe_numeric => (maybe.from >= maybe_numeric.from
|
|
86
|
-
&& maybe.to <= maybe_numeric.to));
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
});
|
|
90
|
-
if (tokens.length < 1) {
|
|
91
|
-
throw new TokenScanError('No tokens found!');
|
|
92
|
-
}
|
|
93
|
-
else if (0 !== tokens[0].from) {
|
|
94
|
-
throw new TokenScanError('First token not at index 0!');
|
|
95
|
-
}
|
|
96
|
-
else if (scan.value.length !== tokens[tokens.length - 1].to) {
|
|
97
|
-
throw new TokenScanError('Last token does not end at end of string!');
|
|
98
|
-
}
|
|
99
|
-
let nesting_balance = 0;
|
|
100
|
-
for (let index = 0; index < tokens.length; ++index) {
|
|
101
|
-
const token = tokens[index];
|
|
102
|
-
if ('nesting_open' === token.type) {
|
|
103
|
-
nesting_balance += (token.to - token.from);
|
|
104
|
-
}
|
|
105
|
-
else if ('nesting_close' === token.type) {
|
|
106
|
-
nesting_balance -= (token.to - token.from);
|
|
107
|
-
}
|
|
108
|
-
if (index > 0
|
|
109
|
-
&& tokens[index - 1].to !== token.from) {
|
|
110
|
-
console.error(tokens, index);
|
|
111
|
-
throw new TokenScanError(`Token expected to be found at index ${index}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (0 !== nesting_balance) {
|
|
115
|
-
throw new TokenScanError('Imbalanced nesting in string!');
|
|
116
|
-
}
|
|
117
|
-
return this.massage_part_baked_tokens(scan, tokens.filter((maybe) => 'ignore' !== maybe.type));
|
|
118
|
-
}
|
|
119
|
-
static massage_part_baked_tokens(scan, tokens) {
|
|
120
|
-
const smoosh_numerics = [];
|
|
121
|
-
for (let token_index = tokens.length - 1; token_index > 0; --token_index) {
|
|
122
|
-
const previous = tokens[token_index - 1];
|
|
123
|
-
const current = tokens[token_index];
|
|
124
|
-
if ('numeric' === previous.type) {
|
|
125
|
-
const previous_value = scan.value.substring(previous.from, previous.to);
|
|
126
|
-
const current_value = scan.value.substring(current.from, current.to);
|
|
127
|
-
if (current_value.startsWith('.')
|
|
128
|
-
&& /^\d+$/.test(previous_value)) {
|
|
129
|
-
smoosh_numerics.push(token_index);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
for (const index of smoosh_numerics) {
|
|
134
|
-
tokens.splice(index - 1, 2, new TokenSpan(tokens[index - 1].from, tokens[index].to, 'numeric'));
|
|
135
|
-
}
|
|
136
|
-
const convert_to_negative = [];
|
|
137
|
-
if (tokens.length >= 2
|
|
138
|
-
&& 'operation' === tokens[0].type
|
|
139
|
-
&& '-' === scan.value[tokens[0].from]
|
|
140
|
-
&& 'numeric' === tokens[1].type) {
|
|
141
|
-
convert_to_negative.push(0);
|
|
142
|
-
}
|
|
143
|
-
for (let token_index = 0; token_index < tokens.length; ++token_index) {
|
|
144
|
-
const token = tokens[token_index];
|
|
145
|
-
const next = tokens[token_index + 1];
|
|
146
|
-
const after = tokens[token_index + 2];
|
|
147
|
-
if (('nesting_open' === token.type
|
|
148
|
-
|| 'operation' === token.type)
|
|
149
|
-
&& next
|
|
150
|
-
&& after
|
|
151
|
-
&& 'operation' === next.type
|
|
152
|
-
&& '-' === scan.value[next.from]
|
|
153
|
-
&& 'numeric' === after.type) {
|
|
154
|
-
convert_to_negative.push(token_index + 1);
|
|
155
|
-
token_index += 2;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
for (const index of convert_to_negative.reverse()) {
|
|
160
|
-
tokens.splice(index, 2, new TokenSpan(tokens[index].from, tokens[index + 1].to, 'numeric'));
|
|
161
|
-
}
|
|
162
|
-
return tokens;
|
|
163
|
-
}
|
|
164
|
-
static parse_scan(scan) {
|
|
165
|
-
const reduced = scan.tokens.reduce((was, is, index) => TokenScan.reduce(scan, was, is, index), default_tokenizer_state());
|
|
166
|
-
if (undefined !== reduced.left_operand
|
|
167
|
-
&& '' === reduced.operation
|
|
168
|
-
&& undefined === reduced.right_operand
|
|
169
|
-
&& 0 === reduced.outter_stack.length) {
|
|
170
|
-
return reduced.left_operand;
|
|
171
|
-
}
|
|
172
|
-
throw new TokenScanParseError('Parse in unsupported state!', scan, reduced);
|
|
173
|
-
}
|
|
174
|
-
static reduce(scan, was, is, index) {
|
|
175
|
-
if (is_nesting_open(is)) {
|
|
176
|
-
if ('right' === was.operand_mode) {
|
|
177
|
-
if (undefined === was.left_operand) {
|
|
178
|
-
if (!(was.outter_stack.length > 0
|
|
179
|
-
&& !(was.outter_stack[was.outter_stack.length - 1] instanceof TokenSpan))) {
|
|
180
|
-
throw new TokenScanParseError(
|
|
181
|
-
// eslint-disable-next-line max-len
|
|
182
|
-
'Nesting opened without left operand to push into stack!', scan, was);
|
|
183
|
-
}
|
|
184
|
-
return was;
|
|
185
|
-
}
|
|
186
|
-
else if ('' === was.operation) {
|
|
187
|
-
throw new TokenScanParseError('Nesting opened without operation to push into stack!', scan, was, is);
|
|
188
|
-
}
|
|
189
|
-
was.outter_stack.push({
|
|
190
|
-
left_operand: was.left_operand,
|
|
191
|
-
operation: was.operation,
|
|
192
|
-
});
|
|
193
|
-
was.left_operand = undefined;
|
|
194
|
-
was.operation = '';
|
|
195
|
-
was.operand_mode = 'left';
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
was.outter_stack.push(is);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else if (is_nesting_close(is)) {
|
|
202
|
-
const popped = was.outter_stack.pop();
|
|
203
|
-
if (popped instanceof TokenSpan) {
|
|
204
|
-
if ('nesting_open' === popped.type
|
|
205
|
-
&& '' === was.operation
|
|
206
|
-
&& undefined !== was.left_operand
|
|
207
|
-
&& undefined === was.right_operand) {
|
|
208
|
-
// no-op, deliberately do nothing
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
throw new TokenScanParseError(
|
|
212
|
-
// eslint-disable-next-line max-len
|
|
213
|
-
'token span popping in this context not yet implemented', scan, was, is);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
else if (undefined === popped) {
|
|
217
|
-
if (index !== (scan.tokens.length - 1)
|
|
218
|
-
&& ('' !== was.operation
|
|
219
|
-
|| undefined !== was.right_operand)) {
|
|
220
|
-
throw new TokenScanParseError('Token scan finished with incomplete parse!', scan, was, is);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
if ('' === was.operation
|
|
225
|
-
&& undefined !== was.left_operand
|
|
226
|
-
&& undefined === was.right_operand) {
|
|
227
|
-
was.left_operand = new IntermediaryCalculation(popped.left_operand, popped.operation, was.left_operand);
|
|
228
|
-
was.operation = '';
|
|
229
|
-
was.operand_mode = 'right';
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
throw new TokenScanParseError(
|
|
233
|
-
// eslint-disable-next-line max-len
|
|
234
|
-
'token span popping in this context not yet implemented', scan, was, is);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
else if (is_numeric(is)) {
|
|
239
|
-
if ('left' === was.operand_mode) {
|
|
240
|
-
was.left_operand = IntermediaryNumber.create(scan.value.substring(is.from, is.to));
|
|
241
|
-
was.operand_mode = 'right';
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
if ('' === was.operation) {
|
|
245
|
-
throw new TokenScanParseError('Right operand detected without operation!', scan, was, is);
|
|
246
|
-
}
|
|
247
|
-
else if (undefined === was.left_operand) {
|
|
248
|
-
throw new TokenScanParseError('Right operand detected without left operand!', scan, was, is);
|
|
249
|
-
}
|
|
250
|
-
let resolved = new IntermediaryCalculation(was.left_operand, was.operation, IntermediaryNumber.create(scan.value.substring(is.from, is.to)));
|
|
251
|
-
if (was.outter_stack.length > 0
|
|
252
|
-
&& !(was.outter_stack[was.outter_stack.length - 1] instanceof TokenSpan)) {
|
|
253
|
-
const previous = (was.outter_stack.pop());
|
|
254
|
-
resolved = new IntermediaryCalculation(previous.left_operand, previous.operation, resolved);
|
|
255
|
-
}
|
|
256
|
-
was.left_operand = resolved;
|
|
257
|
-
was.operation = '';
|
|
258
|
-
was.right_operand = undefined;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
else if ('operation' === is.type) {
|
|
262
|
-
if (undefined === was.left_operand) {
|
|
263
|
-
throw new TokenScanParseError('Operation detected without left operand!', scan, was, is);
|
|
264
|
-
}
|
|
265
|
-
else if ('' !== was.operation) {
|
|
266
|
-
throw new TokenScanParseError(`Cannot set operation when operation already set to "${was.operation}"`, scan, was, is);
|
|
267
|
-
}
|
|
268
|
-
const maybe = scan.value.substring(is.from, is.to);
|
|
269
|
-
is_operation_value(maybe);
|
|
270
|
-
was.operation = maybe;
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
throw new TokenScanParseError('not implemented', scan, was, is);
|
|
274
|
-
}
|
|
275
|
-
return was;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function default_tokenizer_state() {
|
|
279
|
-
return {
|
|
280
|
-
outter_stack: [],
|
|
281
|
-
left_operand: undefined,
|
|
282
|
-
operation: '',
|
|
283
|
-
right_operand: undefined,
|
|
284
|
-
operand_mode: 'left',
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
function is_nesting_open(maybe) {
|
|
288
|
-
return 'nesting_open' === maybe.type;
|
|
289
|
-
}
|
|
290
|
-
function is_nesting_close(maybe) {
|
|
291
|
-
return 'nesting_close' === maybe.type;
|
|
292
|
-
}
|
|
293
|
-
function is_numeric(maybe) {
|
|
294
|
-
return 'numeric' === maybe.type;
|
|
295
|
-
}
|
|
296
|
-
function is_operation_value(maybe) {
|
|
297
|
-
if (!(maybe.length === 1
|
|
298
|
-
&& '+-/x*%'.includes(maybe))) {
|
|
299
|
-
throw new TokenScanError(`Expected operation value, found "${maybe}"`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
//# sourceMappingURL=TokenScan.js.map
|