@jacksontian/equation-resolver 1.0.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/.github/workflows/nodejs.yml +31 -0
- package/README.md +114 -0
- package/bin/cli.js +64 -0
- package/eslint.config.js +63 -0
- package/lib/evaluator.js +308 -0
- package/lib/fraction.js +123 -0
- package/lib/index.js +6 -0
- package/lib/lexer.js +115 -0
- package/lib/parser.js +118 -0
- package/lib/semantic-checker.js +56 -0
- package/package.json +34 -0
- package/test/evaluator.test.js +230 -0
- package/test/fraction.test.js +435 -0
- package/test/lexer.test.js +418 -0
- package/test/parser.test.js +264 -0
- package/test/semantic-checker.test.js +45 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { Lexer } from '../lib/lexer.js';
|
|
4
|
+
|
|
5
|
+
describe('Lexer', () => {
|
|
6
|
+
describe('数字识别', () => {
|
|
7
|
+
it('应该识别整数', () => {
|
|
8
|
+
const lexer = new Lexer('123');
|
|
9
|
+
const token = lexer.getNextToken();
|
|
10
|
+
assert.strictEqual(token.type, 'NUMBER');
|
|
11
|
+
assert.strictEqual(token.value, '123');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('应该识别小数', () => {
|
|
15
|
+
const lexer = new Lexer('3.14');
|
|
16
|
+
const token = lexer.getNextToken();
|
|
17
|
+
assert.strictEqual(token.type, 'NUMBER');
|
|
18
|
+
assert.strictEqual(token.value, '3.14');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('应该拒绝以小数点开头的数字(需要显式写 0.5)', () => {
|
|
22
|
+
const lexer = new Lexer('.5');
|
|
23
|
+
assert.throws(
|
|
24
|
+
() => lexer.getNextToken(),
|
|
25
|
+
(error) => {
|
|
26
|
+
assert.strictEqual(error.message, 'Unexpected character: .');
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('应该识别多个数字', () => {
|
|
33
|
+
const lexer = new Lexer('123 456');
|
|
34
|
+
const token1 = lexer.getNextToken();
|
|
35
|
+
assert.strictEqual(token1.type, 'NUMBER');
|
|
36
|
+
assert.strictEqual(token1.value, '123');
|
|
37
|
+
|
|
38
|
+
const token2 = lexer.getNextToken();
|
|
39
|
+
assert.strictEqual(token2.type, 'NUMBER');
|
|
40
|
+
assert.strictEqual(token2.value, '456');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('变量识别', () => {
|
|
45
|
+
it('应该识别单个字母变量', () => {
|
|
46
|
+
const lexer = new Lexer('x');
|
|
47
|
+
const token = lexer.getNextToken();
|
|
48
|
+
assert.strictEqual(token.type, 'VARIABLE');
|
|
49
|
+
assert.strictEqual(token.value, 'x');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('应该识别多字母变量', () => {
|
|
53
|
+
const lexer = new Lexer('abc');
|
|
54
|
+
const token = lexer.getNextToken();
|
|
55
|
+
assert.strictEqual(token.type, 'VARIABLE');
|
|
56
|
+
assert.strictEqual(token.value, 'a');
|
|
57
|
+
const token2 = lexer.getNextToken();
|
|
58
|
+
assert.strictEqual(token2.type, 'VARIABLE');
|
|
59
|
+
assert.strictEqual(token2.value, 'b');
|
|
60
|
+
const token3 = lexer.getNextToken();
|
|
61
|
+
assert.strictEqual(token3.type, 'VARIABLE');
|
|
62
|
+
assert.strictEqual(token3.value, 'c');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('应该识别大写变量', () => {
|
|
66
|
+
const lexer = new Lexer('XYZ');
|
|
67
|
+
const token = lexer.getNextToken();
|
|
68
|
+
assert.strictEqual(token.type, 'VARIABLE');
|
|
69
|
+
assert.strictEqual(token.value, 'X');
|
|
70
|
+
const token2 = lexer.getNextToken();
|
|
71
|
+
assert.strictEqual(token2.type, 'VARIABLE');
|
|
72
|
+
assert.strictEqual(token2.value, 'Y');
|
|
73
|
+
const token3 = lexer.getNextToken();
|
|
74
|
+
assert.strictEqual(token3.type, 'VARIABLE');
|
|
75
|
+
assert.strictEqual(token3.value, 'Z');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('应该识别混合大小写变量', () => {
|
|
79
|
+
const lexer = new Lexer('xYz');
|
|
80
|
+
const token = lexer.getNextToken();
|
|
81
|
+
assert.strictEqual(token.type, 'VARIABLE');
|
|
82
|
+
assert.strictEqual(token.value, 'x');
|
|
83
|
+
const token2 = lexer.getNextToken();
|
|
84
|
+
assert.strictEqual(token2.type, 'VARIABLE');
|
|
85
|
+
assert.strictEqual(token2.value, 'Y');
|
|
86
|
+
const token3 = lexer.getNextToken();
|
|
87
|
+
assert.strictEqual(token3.type, 'VARIABLE');
|
|
88
|
+
assert.strictEqual(token3.value, 'z');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('应该识别多个变量', () => {
|
|
92
|
+
const lexer = new Lexer('x y z');
|
|
93
|
+
const token1 = lexer.getNextToken();
|
|
94
|
+
assert.strictEqual(token1.type, 'VARIABLE');
|
|
95
|
+
assert.strictEqual(token1.value, 'x');
|
|
96
|
+
|
|
97
|
+
const token2 = lexer.getNextToken();
|
|
98
|
+
assert.strictEqual(token2.type, 'VARIABLE');
|
|
99
|
+
assert.strictEqual(token2.value, 'y');
|
|
100
|
+
|
|
101
|
+
const token3 = lexer.getNextToken();
|
|
102
|
+
assert.strictEqual(token3.type, 'VARIABLE');
|
|
103
|
+
assert.strictEqual(token3.value, 'z');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('运算符识别', () => {
|
|
108
|
+
it('应该识别加号', () => {
|
|
109
|
+
const lexer = new Lexer('+');
|
|
110
|
+
const token = lexer.getNextToken();
|
|
111
|
+
assert.strictEqual(token.type, 'PLUS');
|
|
112
|
+
assert.strictEqual(token.value, '+');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('应该识别减号', () => {
|
|
116
|
+
const lexer = new Lexer('-');
|
|
117
|
+
const token = lexer.getNextToken();
|
|
118
|
+
assert.strictEqual(token.type, 'MINUS');
|
|
119
|
+
assert.strictEqual(token.value, '-');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('应该识别乘号', () => {
|
|
123
|
+
const lexer = new Lexer('*');
|
|
124
|
+
const token = lexer.getNextToken();
|
|
125
|
+
assert.strictEqual(token.type, 'MULTIPLY');
|
|
126
|
+
assert.strictEqual(token.value, '*');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('应该识别除号', () => {
|
|
130
|
+
const lexer = new Lexer('/');
|
|
131
|
+
const token = lexer.getNextToken();
|
|
132
|
+
assert.strictEqual(token.type, 'DIVIDE');
|
|
133
|
+
assert.strictEqual(token.value, '/');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('应该识别多个运算符', () => {
|
|
137
|
+
const lexer = new Lexer('+-*/');
|
|
138
|
+
const tokens = [];
|
|
139
|
+
for (let i = 0; i < 4; i++) {
|
|
140
|
+
tokens.push(lexer.getNextToken());
|
|
141
|
+
}
|
|
142
|
+
assert.strictEqual(tokens[0].type, 'PLUS');
|
|
143
|
+
assert.strictEqual(tokens[1].type, 'MINUS');
|
|
144
|
+
assert.strictEqual(tokens[2].type, 'MULTIPLY');
|
|
145
|
+
assert.strictEqual(tokens[3].type, 'DIVIDE');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('括号识别', () => {
|
|
150
|
+
it('应该识别左括号', () => {
|
|
151
|
+
const lexer = new Lexer('(');
|
|
152
|
+
const token = lexer.getNextToken();
|
|
153
|
+
assert.strictEqual(token.type, 'LPAREN');
|
|
154
|
+
assert.strictEqual(token.value, '(');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('应该识别右括号', () => {
|
|
158
|
+
const lexer = new Lexer(')');
|
|
159
|
+
const token = lexer.getNextToken();
|
|
160
|
+
assert.strictEqual(token.type, 'RPAREN');
|
|
161
|
+
assert.strictEqual(token.value, ')');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('应该识别括号对', () => {
|
|
165
|
+
const lexer = new Lexer('()');
|
|
166
|
+
const token1 = lexer.getNextToken();
|
|
167
|
+
assert.strictEqual(token1.type, 'LPAREN');
|
|
168
|
+
|
|
169
|
+
const token2 = lexer.getNextToken();
|
|
170
|
+
assert.strictEqual(token2.type, 'RPAREN');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('等号和分号识别', () => {
|
|
175
|
+
it('应该识别等号', () => {
|
|
176
|
+
const lexer = new Lexer('=');
|
|
177
|
+
const token = lexer.getNextToken();
|
|
178
|
+
assert.strictEqual(token.type, 'EQUALS');
|
|
179
|
+
assert.strictEqual(token.value, '=');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('应该识别分号', () => {
|
|
183
|
+
const lexer = new Lexer(';');
|
|
184
|
+
const token = lexer.getNextToken();
|
|
185
|
+
assert.strictEqual(token.type, 'SEMICOLON');
|
|
186
|
+
assert.strictEqual(token.value, ';');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('应该识别多个分号', () => {
|
|
190
|
+
const lexer = new Lexer(';;');
|
|
191
|
+
const token1 = lexer.getNextToken();
|
|
192
|
+
assert.strictEqual(token1.type, 'SEMICOLON');
|
|
193
|
+
|
|
194
|
+
const token2 = lexer.getNextToken();
|
|
195
|
+
assert.strictEqual(token2.type, 'SEMICOLON');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('空白字符处理', () => {
|
|
200
|
+
it('应该跳过空格', () => {
|
|
201
|
+
const lexer = new Lexer(' x');
|
|
202
|
+
const token = lexer.getNextToken();
|
|
203
|
+
assert.strictEqual(token.type, 'VARIABLE');
|
|
204
|
+
assert.strictEqual(token.value, 'x');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('应该跳过多个空格', () => {
|
|
208
|
+
const lexer = new Lexer(' 123');
|
|
209
|
+
const token = lexer.getNextToken();
|
|
210
|
+
assert.strictEqual(token.type, 'NUMBER');
|
|
211
|
+
assert.strictEqual(token.value, '123');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('应该处理数字和变量之间的空格', () => {
|
|
215
|
+
const lexer = new Lexer('2 x');
|
|
216
|
+
const token1 = lexer.getNextToken();
|
|
217
|
+
assert.strictEqual(token1.type, 'NUMBER');
|
|
218
|
+
assert.strictEqual(token1.value, '2');
|
|
219
|
+
|
|
220
|
+
const token2 = lexer.getNextToken();
|
|
221
|
+
assert.strictEqual(token2.type, 'VARIABLE');
|
|
222
|
+
assert.strictEqual(token2.value, 'x');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('复杂表达式', () => {
|
|
227
|
+
it('应该正确解析简单方程', () => {
|
|
228
|
+
const lexer = new Lexer('2x = 4');
|
|
229
|
+
const tokens = [];
|
|
230
|
+
let token;
|
|
231
|
+
do {
|
|
232
|
+
token = lexer.getNextToken();
|
|
233
|
+
tokens.push(token);
|
|
234
|
+
} while (token.type !== 'EOF');
|
|
235
|
+
|
|
236
|
+
assert.strictEqual(tokens[0].type, 'NUMBER');
|
|
237
|
+
assert.strictEqual(tokens[0].value, '2');
|
|
238
|
+
assert.strictEqual(tokens[1].type, 'VARIABLE');
|
|
239
|
+
assert.strictEqual(tokens[1].value, 'x');
|
|
240
|
+
assert.strictEqual(tokens[2].type, 'EQUALS');
|
|
241
|
+
assert.strictEqual(tokens[3].type, 'NUMBER');
|
|
242
|
+
assert.strictEqual(tokens[3].value, '4');
|
|
243
|
+
assert.strictEqual(tokens[4].type, 'EOF');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('应该正确解析带括号的表达式', () => {
|
|
247
|
+
const lexer = new Lexer('(x + 1)');
|
|
248
|
+
const tokens = [];
|
|
249
|
+
let token;
|
|
250
|
+
do {
|
|
251
|
+
token = lexer.getNextToken();
|
|
252
|
+
tokens.push(token);
|
|
253
|
+
} while (token.type !== 'EOF');
|
|
254
|
+
|
|
255
|
+
assert.strictEqual(tokens[0].type, 'LPAREN');
|
|
256
|
+
assert.strictEqual(tokens[1].type, 'VARIABLE');
|
|
257
|
+
assert.strictEqual(tokens[1].value, 'x');
|
|
258
|
+
assert.strictEqual(tokens[2].type, 'PLUS');
|
|
259
|
+
assert.strictEqual(tokens[3].type, 'NUMBER');
|
|
260
|
+
assert.strictEqual(tokens[3].value, '1');
|
|
261
|
+
assert.strictEqual(tokens[4].type, 'RPAREN');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('应该正确解析方程组', () => {
|
|
265
|
+
const lexer = new Lexer('x + y = 5; x - y = 1');
|
|
266
|
+
const tokens = [];
|
|
267
|
+
let token;
|
|
268
|
+
do {
|
|
269
|
+
token = lexer.getNextToken();
|
|
270
|
+
tokens.push(token);
|
|
271
|
+
} while (token.type !== 'EOF');
|
|
272
|
+
|
|
273
|
+
// 检查第一个方程
|
|
274
|
+
assert.strictEqual(tokens[0].type, 'VARIABLE');
|
|
275
|
+
assert.strictEqual(tokens[0].value, 'x');
|
|
276
|
+
assert.strictEqual(tokens[1].type, 'PLUS');
|
|
277
|
+
assert.strictEqual(tokens[2].type, 'VARIABLE');
|
|
278
|
+
assert.strictEqual(tokens[2].value, 'y');
|
|
279
|
+
assert.strictEqual(tokens[3].type, 'EQUALS');
|
|
280
|
+
assert.strictEqual(tokens[4].type, 'NUMBER');
|
|
281
|
+
assert.strictEqual(tokens[4].value, '5');
|
|
282
|
+
|
|
283
|
+
// 检查分号
|
|
284
|
+
assert.strictEqual(tokens[5].type, 'SEMICOLON');
|
|
285
|
+
|
|
286
|
+
// 检查第二个方程
|
|
287
|
+
assert.strictEqual(tokens[6].type, 'VARIABLE');
|
|
288
|
+
assert.strictEqual(tokens[6].value, 'x');
|
|
289
|
+
assert.strictEqual(tokens[7].type, 'MINUS');
|
|
290
|
+
assert.strictEqual(tokens[8].type, 'VARIABLE');
|
|
291
|
+
assert.strictEqual(tokens[8].value, 'y');
|
|
292
|
+
assert.strictEqual(tokens[9].type, 'EQUALS');
|
|
293
|
+
assert.strictEqual(tokens[10].type, 'NUMBER');
|
|
294
|
+
assert.strictEqual(tokens[10].value, '1');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('EOF 处理', () => {
|
|
299
|
+
it('应该在输入结束时返回 EOF', () => {
|
|
300
|
+
const lexer = new Lexer('');
|
|
301
|
+
const token = lexer.getNextToken();
|
|
302
|
+
assert.strictEqual(token.type, 'EOF');
|
|
303
|
+
assert.strictEqual(token.value, null);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('应该在解析完所有 token 后返回 EOF', () => {
|
|
307
|
+
const lexer = new Lexer('x');
|
|
308
|
+
const token1 = lexer.getNextToken();
|
|
309
|
+
assert.strictEqual(token1.type, 'VARIABLE');
|
|
310
|
+
|
|
311
|
+
const token2 = lexer.getNextToken();
|
|
312
|
+
assert.strictEqual(token2.type, 'EOF');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('应该连续返回 EOF', () => {
|
|
316
|
+
const lexer = new Lexer('x');
|
|
317
|
+
lexer.getNextToken(); // 跳过 x
|
|
318
|
+
const token1 = lexer.getNextToken();
|
|
319
|
+
assert.strictEqual(token1.type, 'EOF');
|
|
320
|
+
|
|
321
|
+
const token2 = lexer.getNextToken();
|
|
322
|
+
assert.strictEqual(token2.type, 'EOF');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('错误处理', () => {
|
|
327
|
+
it('应该对不支持的字符抛出错误', () => {
|
|
328
|
+
const lexer = new Lexer('@');
|
|
329
|
+
assert.throws(
|
|
330
|
+
() => lexer.getNextToken(),
|
|
331
|
+
(error) => {
|
|
332
|
+
assert.strictEqual(error.message, 'Unexpected character: @');
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('应该对特殊字符抛出错误', () => {
|
|
339
|
+
const lexer = new Lexer('#');
|
|
340
|
+
assert.throws(
|
|
341
|
+
() => lexer.getNextToken(),
|
|
342
|
+
(error) => {
|
|
343
|
+
assert.strictEqual(error.message, 'Unexpected character: #');
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('应该在遇到错误字符时包含字符信息', () => {
|
|
350
|
+
const lexer = new Lexer('$');
|
|
351
|
+
assert.throws(
|
|
352
|
+
() => lexer.getNextToken(),
|
|
353
|
+
(error) => {
|
|
354
|
+
assert.strictEqual(error.message, 'Unexpected character: $');
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('边界情况', () => {
|
|
362
|
+
it('应该处理只有运算符的输入', () => {
|
|
363
|
+
const lexer = new Lexer('++--');
|
|
364
|
+
const tokens = [];
|
|
365
|
+
for (let i = 0; i < 4; i++) {
|
|
366
|
+
tokens.push(lexer.getNextToken());
|
|
367
|
+
}
|
|
368
|
+
assert.strictEqual(tokens[0].type, 'PLUS');
|
|
369
|
+
assert.strictEqual(tokens[1].type, 'PLUS');
|
|
370
|
+
assert.strictEqual(tokens[2].type, 'MINUS');
|
|
371
|
+
assert.strictEqual(tokens[3].type, 'MINUS');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('应该处理只有数字的输入', () => {
|
|
375
|
+
const lexer = new Lexer('123.456');
|
|
376
|
+
const token = lexer.getNextToken();
|
|
377
|
+
assert.strictEqual(token.type, 'NUMBER');
|
|
378
|
+
assert.strictEqual(token.value, '123.456');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('应该处理只有变量的输入', () => {
|
|
382
|
+
const lexer = new Lexer('xyz');
|
|
383
|
+
const token = lexer.getNextToken();
|
|
384
|
+
assert.strictEqual(token.type, 'VARIABLE');
|
|
385
|
+
assert.strictEqual(token.value, 'x');
|
|
386
|
+
const token2 = lexer.getNextToken();
|
|
387
|
+
assert.strictEqual(token2.type, 'VARIABLE');
|
|
388
|
+
assert.strictEqual(token2.value, 'y');
|
|
389
|
+
const token3 = lexer.getNextToken();
|
|
390
|
+
assert.strictEqual(token3.type, 'VARIABLE');
|
|
391
|
+
assert.strictEqual(token3.value, 'z');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('应该处理数字和运算符混合', () => {
|
|
395
|
+
const lexer = new Lexer('1+2-3*4/5');
|
|
396
|
+
const tokens = [];
|
|
397
|
+
let token;
|
|
398
|
+
do {
|
|
399
|
+
token = lexer.getNextToken();
|
|
400
|
+
if (token.type !== 'EOF') {
|
|
401
|
+
tokens.push(token);
|
|
402
|
+
}
|
|
403
|
+
} while (token.type !== 'EOF');
|
|
404
|
+
|
|
405
|
+
assert.strictEqual(tokens.length, 9);
|
|
406
|
+
assert.strictEqual(tokens[0].type, 'NUMBER');
|
|
407
|
+
assert.strictEqual(tokens[1].type, 'PLUS');
|
|
408
|
+
assert.strictEqual(tokens[2].type, 'NUMBER');
|
|
409
|
+
assert.strictEqual(tokens[3].type, 'MINUS');
|
|
410
|
+
assert.strictEqual(tokens[4].type, 'NUMBER');
|
|
411
|
+
assert.strictEqual(tokens[5].type, 'MULTIPLY');
|
|
412
|
+
assert.strictEqual(tokens[6].type, 'NUMBER');
|
|
413
|
+
assert.strictEqual(tokens[7].type, 'DIVIDE');
|
|
414
|
+
assert.strictEqual(tokens[8].type, 'NUMBER');
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { Parser } from '../lib/parser.js';
|
|
4
|
+
import { Lexer } from '../lib/lexer.js';
|
|
5
|
+
|
|
6
|
+
function parse(input) {
|
|
7
|
+
const lexer = new Lexer(input);
|
|
8
|
+
const parser = new Parser(lexer);
|
|
9
|
+
return parser.parse();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('Parser', () => {
|
|
13
|
+
|
|
14
|
+
describe('equation parsing', () => {
|
|
15
|
+
it('should parse simple equation', () => {
|
|
16
|
+
const ast = parse('x = 5');
|
|
17
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
18
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
19
|
+
const equation = ast.equations[0];
|
|
20
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
21
|
+
assert.strictEqual(equation.left.type, 'VARIABLE');
|
|
22
|
+
assert.strictEqual(equation.left.name, 'x');
|
|
23
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
24
|
+
assert.strictEqual(equation.right.value, '5');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should parse simple equation, semicolon is optional', () => {
|
|
28
|
+
const ast = parse('x = 5;');
|
|
29
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
30
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
31
|
+
const equation = ast.equations[0];
|
|
32
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
33
|
+
assert.strictEqual(equation.left.type, 'VARIABLE');
|
|
34
|
+
assert.strictEqual(equation.left.name, 'x');
|
|
35
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
36
|
+
assert.strictEqual(equation.right.value, '5');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should parse two equations equation system', () => {
|
|
40
|
+
const ast = parse('x = 5; y = 1');
|
|
41
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
42
|
+
assert.strictEqual(ast.equations.length, 2);
|
|
43
|
+
assert.strictEqual(ast.equations[0].type, 'EQUATION');
|
|
44
|
+
assert.strictEqual(ast.equations[0].left.type, 'VARIABLE');
|
|
45
|
+
assert.strictEqual(ast.equations[0].left.name, 'x');
|
|
46
|
+
assert.strictEqual(ast.equations[0].right.type, 'NUMBER');
|
|
47
|
+
assert.strictEqual(ast.equations[0].right.value, '5');
|
|
48
|
+
assert.strictEqual(ast.equations[1].type, 'EQUATION');
|
|
49
|
+
assert.strictEqual(ast.equations[1].left.type, 'VARIABLE');
|
|
50
|
+
assert.strictEqual(ast.equations[1].left.name, 'y');
|
|
51
|
+
assert.strictEqual(ast.equations[1].right.type, 'NUMBER');
|
|
52
|
+
assert.strictEqual(ast.equations[1].right.value, '1');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should support + in equations', () => {
|
|
56
|
+
const ast = parse('x + 1 = 5');
|
|
57
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
58
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
59
|
+
const equation = ast.equations[0];
|
|
60
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
61
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
62
|
+
assert.strictEqual(equation.left.op, '+');
|
|
63
|
+
assert.strictEqual(equation.left.left.type, 'VARIABLE');
|
|
64
|
+
assert.strictEqual(equation.left.left.name, 'x');
|
|
65
|
+
assert.strictEqual(equation.left.right.type, 'NUMBER');
|
|
66
|
+
assert.strictEqual(equation.left.right.value, '1');
|
|
67
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
68
|
+
assert.strictEqual(equation.right.value, '5');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should support - in equations', () => {
|
|
72
|
+
const ast = parse('x - 1 = 5');
|
|
73
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
74
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
75
|
+
const equation = ast.equations[0];
|
|
76
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should support * in equations', () => {
|
|
80
|
+
const ast = parse('x * 1 = 5');
|
|
81
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
82
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
83
|
+
const equation = ast.equations[0];
|
|
84
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
85
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
86
|
+
assert.strictEqual(equation.left.op, '*');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should support / in equations', () => {
|
|
90
|
+
const ast = parse('x / 1 = 5');
|
|
91
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
92
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
93
|
+
const equation = ast.equations[0];
|
|
94
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should support implicit multiplication in equations', () => {
|
|
98
|
+
const ast = parse('1x = 5');
|
|
99
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
100
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
101
|
+
const equation = ast.equations[0];
|
|
102
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
103
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
104
|
+
assert.strictEqual(equation.left.op, '*');
|
|
105
|
+
assert.strictEqual(equation.left.left.type, 'NUMBER');
|
|
106
|
+
assert.strictEqual(equation.left.left.value, '1');
|
|
107
|
+
assert.strictEqual(equation.left.right.type, 'VARIABLE');
|
|
108
|
+
assert.strictEqual(equation.left.right.name, 'x');
|
|
109
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
110
|
+
assert.strictEqual(equation.right.value, '5');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should support parentheses in equations', () => {
|
|
114
|
+
const ast = parse('(x + 1) = 5');
|
|
115
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
116
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
117
|
+
const equation = ast.equations[0];
|
|
118
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
119
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
120
|
+
assert.strictEqual(equation.left.op, '+');
|
|
121
|
+
assert.strictEqual(equation.left.left.type, 'VARIABLE');
|
|
122
|
+
assert.strictEqual(equation.left.left.name, 'x');
|
|
123
|
+
assert.strictEqual(equation.left.right.type, 'NUMBER');
|
|
124
|
+
assert.strictEqual(equation.left.right.value, '1');
|
|
125
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
126
|
+
assert.strictEqual(equation.right.value, '5');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should support parentheses in equations', () => {
|
|
130
|
+
const ast = parse('(x + 1)y = 5');
|
|
131
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
132
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
133
|
+
const equation = ast.equations[0];
|
|
134
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
135
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
136
|
+
assert.strictEqual(equation.left.op, '*');
|
|
137
|
+
assert.strictEqual(equation.left.left.type, 'BINARY_OP');
|
|
138
|
+
assert.strictEqual(equation.left.left.op, '+');
|
|
139
|
+
assert.strictEqual(equation.left.left.left.type, 'VARIABLE');
|
|
140
|
+
assert.strictEqual(equation.left.left.left.name, 'x');
|
|
141
|
+
assert.strictEqual(equation.left.left.right.type, 'NUMBER');
|
|
142
|
+
assert.strictEqual(equation.left.left.right.value, '1');
|
|
143
|
+
assert.strictEqual(equation.left.right.type, 'VARIABLE');
|
|
144
|
+
assert.strictEqual(equation.left.right.name, 'y');
|
|
145
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
146
|
+
assert.strictEqual(equation.right.value, '5');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should support parentheses in equations', () => {
|
|
150
|
+
const ast = parse('1 + 3 * 5 = x');
|
|
151
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
152
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
153
|
+
const equation = ast.equations[0];
|
|
154
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
155
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
156
|
+
assert.strictEqual(equation.left.op, '+');
|
|
157
|
+
assert.strictEqual(equation.left.left.type, 'NUMBER');
|
|
158
|
+
assert.strictEqual(equation.left.left.value, '1');
|
|
159
|
+
assert.strictEqual(equation.left.right.type, 'BINARY_OP');
|
|
160
|
+
assert.strictEqual(equation.left.right.op, '*');
|
|
161
|
+
assert.strictEqual(equation.left.right.left.type, 'NUMBER');
|
|
162
|
+
assert.strictEqual(equation.left.right.left.value, '3');
|
|
163
|
+
assert.strictEqual(equation.left.right.right.type, 'NUMBER');
|
|
164
|
+
assert.strictEqual(equation.left.right.right.value, '5');
|
|
165
|
+
assert.strictEqual(equation.right.type, 'VARIABLE');
|
|
166
|
+
assert.strictEqual(equation.right.name, 'x');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should support parentheses in equations', () => {
|
|
170
|
+
const ast = parse('xy = 1');
|
|
171
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
172
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
173
|
+
const equation = ast.equations[0];
|
|
174
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
175
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
176
|
+
assert.strictEqual(equation.left.op, '*');
|
|
177
|
+
assert.strictEqual(equation.left.left.type, 'VARIABLE');
|
|
178
|
+
assert.strictEqual(equation.left.left.name, 'x');
|
|
179
|
+
assert.strictEqual(equation.left.right.type, 'VARIABLE');
|
|
180
|
+
assert.strictEqual(equation.left.right.name, 'y');
|
|
181
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
182
|
+
assert.strictEqual(equation.right.value, '1');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should support parentheses in equations', () => {
|
|
186
|
+
const ast = parse('x(y)z = 1');
|
|
187
|
+
assert.strictEqual(ast.type, 'EQUATION_SYSTEM');
|
|
188
|
+
assert.strictEqual(ast.equations.length, 1);
|
|
189
|
+
const equation = ast.equations[0];
|
|
190
|
+
assert.strictEqual(equation.type, 'EQUATION');
|
|
191
|
+
assert.strictEqual(equation.left.type, 'BINARY_OP');
|
|
192
|
+
assert.strictEqual(equation.left.op, '*');
|
|
193
|
+
assert.strictEqual(equation.left.left.type, 'VARIABLE');
|
|
194
|
+
assert.strictEqual(equation.left.left.name, 'x');
|
|
195
|
+
assert.strictEqual(equation.left.right.type, 'BINARY_OP');
|
|
196
|
+
assert.strictEqual(equation.left.right.op, '*');
|
|
197
|
+
assert.strictEqual(equation.left.right.left.type, 'VARIABLE');
|
|
198
|
+
assert.strictEqual(equation.left.right.left.name, 'y');
|
|
199
|
+
assert.strictEqual(equation.left.right.right.type, 'VARIABLE');
|
|
200
|
+
assert.strictEqual(equation.left.right.right.name, 'z');
|
|
201
|
+
assert.strictEqual(equation.right.type, 'NUMBER');
|
|
202
|
+
assert.strictEqual(equation.right.value, '1');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Exceptions', () => {
|
|
207
|
+
it('should throw error when unexpected token is encountered', () => {
|
|
208
|
+
assert.throws(
|
|
209
|
+
() => parse(')'),
|
|
210
|
+
(error) => {
|
|
211
|
+
assert.strictEqual(error.message, 'Unexpected token: RPAREN, expected NUMBER, VARIABLE, LPAREN');
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should throw error when missing equals sign', () => {
|
|
218
|
+
assert.throws(
|
|
219
|
+
() => parse('x + 5'),
|
|
220
|
+
(error) => {
|
|
221
|
+
assert.strictEqual(error.message, 'Expected EQUALS,but got EOF');
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
assert.throws(
|
|
227
|
+
() => parse('2x'),
|
|
228
|
+
(error) => {
|
|
229
|
+
assert.strictEqual(error.message, 'Expected EQUALS,but got EOF');
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should throw error when parentheses are not matched', () => {
|
|
236
|
+
assert.throws(
|
|
237
|
+
() => parse('(x + 1'),
|
|
238
|
+
(error) => {
|
|
239
|
+
assert.strictEqual(error.message, 'Expected RPAREN,but got EOF');
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should throw error when missing variable', () => {
|
|
246
|
+
assert.throws(
|
|
247
|
+
() => parse('2x ='),
|
|
248
|
+
(error) => {
|
|
249
|
+
assert.strictEqual(error.message, 'Unexpected token: EOF, expected NUMBER, VARIABLE, LPAREN');
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should throw error when missing left side', () => {
|
|
256
|
+
assert.throws(
|
|
257
|
+
() => parse('= 5'),
|
|
258
|
+
(error) => {
|
|
259
|
+
assert.strictEqual(error.message, 'Unexpected token: EQUALS, expected NUMBER, VARIABLE, LPAREN');
|
|
260
|
+
return true;
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|