@signpostmarv/intermediary-number 0.1.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.
@@ -0,0 +1,48 @@
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 {};
@@ -0,0 +1,302 @@
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
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@signpostmarv/intermediary-number",
4
+ "description": "intermediary number & calculator classes for SignpostMarv's Docs.json.ts-driven production planner",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/SignpostMarv/Intermediary-Number.git"
9
+ },
10
+ "funding": "https://github.com/SignpostMarv/Intermediary-Number?sponsor=1",
11
+ "devDependencies": {
12
+ "@eslint/js": "^9.0.0",
13
+ "@stdlib/types": "^0.3.2",
14
+ "@types/eslint": "^8.56.9",
15
+ "@types/node": "^20.12.7",
16
+ "c8": "^9.1.0",
17
+ "eslint": "^8.57.0",
18
+ "glob": "^10.3.12",
19
+ "prettier": "^3.2.5",
20
+ "ts-node": "^10.9.2",
21
+ "tslib": "^2.6.2",
22
+ "typescript": "^5.4.5",
23
+ "typescript-eslint": "^7.7.0"
24
+ },
25
+ "dependencies": {
26
+ "@satisfactory-clips-archive/custom-assert": "^0.1.0",
27
+ "bignumber.js": "^9.1.2",
28
+ "fraction.js": "^4.3.7"
29
+ },
30
+ "version": "0.1.0"
31
+ }