exprify 1.0.3 → 1.0.6

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.
@@ -1,3003 +0,0 @@
1
- 'use strict';
2
-
3
- function tokenize(expr, context = {}) {
4
- const tokens = [];
5
- let current = "";
6
- let quote = "";
7
-
8
- const operators = ["+", "-", "*", "/", "%", "^", "=", ">", "<", "!", "&", "|"];
9
- const multiOps = [
10
- "==", ">=", "<=", "&&", "||",
11
- "+=", "-=", "*=", "/=", "%=",
12
- "?.", "??", "|>"
13
- ];
14
-
15
- const parentheses = "()";
16
- const comma = ",";
17
- const semicolon = ";";
18
- const keywords = ["to", "in"];
19
- // const functions = context.functions?.getAllFunctionsName?.() || [];
20
- const units = context.units?.getAllUnitsFlat?.() || [];
21
-
22
- const isIdentifier = (s) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s);
23
-
24
- function getContext(str, charIndex) {
25
- // 1. Extract all alphanumeric words into an array
26
- const words = str.match(/[a-z0-9]+/gi) || [];
27
-
28
- // 2. Identify the current character and the one immediately before it
29
- const currentChar = str[charIndex] || null;
30
- const prevChar = charIndex > 0 ? str[charIndex - 1] : null;
31
-
32
- // 3. Find the word that contains the current charIndex
33
- let start = charIndex;
34
- // Move pointer back to the start of the current word
35
- while (start > 0 && /[a-z0-9]/i.test(str[start - 1])) start--;
36
-
37
- let end = charIndex;
38
- // Move pointer forward to the end of the current word
39
- while (end < str.length && /[a-z0-9]/i.test(str[end])) end++;
40
-
41
- const currentWord = str.substring(start, end);
42
-
43
- // 4. Find the word that appears before the currentWord in the sequence
44
- const currentWordIdx = words.indexOf(currentWord);
45
- const prevWord = currentWordIdx > 0 ? words[currentWordIdx - 1] : null;
46
-
47
- // 5. Find the word that appears after the currentWord
48
- const nextWord = (currentWordIdx !== -1 && currentWordIdx < words.length - 1)
49
- ? words[currentWordIdx + 1]
50
- : null;
51
-
52
- return {
53
- prevWord: prevWord,
54
- prevChar: prevChar,
55
- currentWord: currentWord,
56
- currentChar: currentChar,
57
- nextWord: nextWord
58
- };
59
- }
60
-
61
- const isUnaryContext = (prev) =>
62
- !prev ||
63
- prev.type === "Operator" ||
64
- prev.type === "UnaryOperator" ||
65
- (prev.type === "Parenthesis" && prev.value !== ")") ||
66
- prev.type === "ArrayStart" ||
67
- prev.type === "Semicolon" ||
68
- prev.type === "Comma" ||
69
- prev.type === "Ternary";
70
-
71
- const flushCurrent = (nextChar, index) => {
72
- if (!current) return;
73
-
74
- // BOOLEAN
75
- if (/^(true|false)$/i.test(current)) {
76
- tokens.push({ type: "Boolean", value: current.toLowerCase() === "true" });
77
- current = "";
78
- return;
79
- }
80
-
81
- // KEYWORD
82
- if (keywords.includes(current)) {
83
- tokens.push({ type: "Keyword", value: current, pos: index });
84
- current = "";
85
- return;
86
- }
87
-
88
- // BIGINT
89
- if (/^\d+n$/.test(current)) {
90
- tokens.push({ type: "BigInt", value: BigInt(current.slice(0, -1)), pos: index });
91
- current = "";
92
- return;
93
- }
94
-
95
- // HEX
96
- if (/^0x[0-9a-fA-F]+$/.test(current)) {
97
- tokens.push({ type: "Number", value: parseInt(current, 16), pos: index });
98
- current = "";
99
- return;
100
- }
101
-
102
- // BINARY
103
- if (/^0b[01]+$/.test(current)) {
104
- tokens.push({ type: "Number", value: parseInt(current, 2), pos: index });
105
- current = "";
106
- return;
107
- }
108
-
109
- // NUMBER (including scientific)
110
- if (/^[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?$/i.test(current)) {
111
- tokens.push({ type: "Number", value: parseFloat(current), pos: index });
112
- current = "";
113
- return;
114
- }
115
-
116
- // IMAGINARY NUMBER
117
- if (/^[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?i$/i.test(current)) {
118
- tokens.push({
119
- type: "ImaginaryLiteral",
120
- value: parseFloat(current.slice(0, -1)),
121
- pos: index
122
- });
123
- current = "";
124
- return;
125
- }
126
-
127
- // IMAGINARY UNIT
128
- if (/^[+-]?i$/i.test(current)) {
129
- const sign = current[0] === "-" ? -1 : 1;
130
- tokens.push({
131
- type: "ImaginaryLiteral",
132
- value: sign,
133
- pos: index
134
- });
135
- current = "";
136
- return;
137
- }
138
-
139
- // NUMBER + UNIT
140
- const numUnit = current.match(/^([+-]?\d+(\.\d+)?)([a-zA-Z]+)$/);
141
- if (numUnit) {
142
- const value = parseFloat(numUnit[1]);
143
- const unit = numUnit[3];
144
-
145
- tokens.push({
146
- type: units.includes(unit) ? "NumberWithUnit" : "UnknownUnit",
147
- value,
148
- unit,
149
- pos: index
150
- });
151
-
152
- current = "";
153
- return;
154
- }
155
-
156
- // UNIT
157
- if (units.includes(current)) {
158
- const {prevWord} = getContext(expr, index);
159
- if (nextChar !== "(") {
160
- if (prevWord){
161
- if (!isNaN(parseFloat(prevWord)) || prevWord === "to" || prevWord === "in") {
162
- // console.log("Context for unit detection:", {current, prevWord, nextChar});
163
-
164
- tokens.push({ type: "Unit", value: current, pos: index });
165
- current = "";
166
- return;
167
- }
168
- }
169
- }
170
- }
171
-
172
- // IDENTIFIER
173
- if (isIdentifier(current)) {
174
- if (nextChar === "(") {
175
- tokens.push({
176
- type: "Function",
177
- name: current,
178
- pos: index
179
- });
180
- } else {
181
- tokens.push({
182
- type: "Identifier",
183
- name: current,
184
- pos: index
185
- });
186
- }
187
-
188
- current = "";
189
- return;
190
- }
191
-
192
- throw new Error(`Invalid token "${current}" at index ${index}`);
193
- };
194
-
195
-
196
- for (let i = 0; i < expr.length; i++) {
197
- let char = expr[i];
198
- let next = expr[i + 1];
199
-
200
- // comments
201
- if (char === "/" && next === "/") {
202
- while (i < expr.length && expr[i] !== "\n") i++;
203
- continue;
204
- }
205
-
206
- if (char === "/" && next === "*") {
207
- i += 2;
208
- while (i < expr.length && !(expr[i] === "*" && expr[i + 1] === "/")) i++;
209
- i++;
210
- continue;
211
- }
212
-
213
- // string
214
- if (`"'`.includes(char)) {
215
- if (!quote) {
216
- quote = char;
217
- current += char;
218
- } else if (quote === char) {
219
- current += char;
220
- tokens.push({
221
- type: "String",
222
- value: current.slice(1, -1),
223
- pos: i
224
- });
225
- current = "";
226
- quote = "";
227
- } else {
228
- current += char;
229
- }
230
- continue;
231
- }
232
-
233
- if (quote) {
234
- if (char === "\\") {
235
- current += char + expr[++i];
236
- } else {
237
- current += char;
238
- }
239
- continue;
240
- }
241
-
242
- // multi operators
243
- const twoChar = char + next;
244
- if (multiOps.includes(twoChar)) {
245
- flushCurrent(char, i);
246
- tokens.push({ type: "Operator", value: twoChar, pos: i });
247
- i++;
248
- continue;
249
- }
250
-
251
- if (char === "?") {
252
- tokens.push({ type: "Ternary", value: "?" });
253
- continue;
254
- }
255
-
256
- // only treat ':' as ternary IF previous token was '?'
257
- if (char === ":") {
258
- flushCurrent(char, i);
259
- const prev = tokens[tokens.length - 1];
260
-
261
- if (prev && prev.type === "Ternary") {
262
- tokens.push({ type: "Ternary", value: ":" });
263
- } else {
264
- tokens.push({ type: "Colon" });
265
- }
266
- continue;
267
- }
268
-
269
- // dot
270
- if (char === "." && /\d/.test(current) && /\d/.test(next)) {
271
- current += char;
272
- continue;
273
- }
274
-
275
- if (char === ".") {
276
- flushCurrent(char, i);
277
- tokens.push({ type: "Dot", pos: i });
278
- continue;
279
- }
280
-
281
- // operators
282
- if (operators.includes(char)) {
283
- flushCurrent(char, i);
284
-
285
- const prev = tokens[tokens.length - 1];
286
- if ((char === "-" || char === "!") && isUnaryContext(prev)) {
287
- tokens.push({ type: "UnaryOperator", value: char, pos: i });
288
- } else {
289
- tokens.push({ type: "Operator", value: char, pos: i });
290
- }
291
- continue;
292
- }
293
-
294
- // parenthesis
295
- if (parentheses.includes(char)) {
296
- flushCurrent(char, i);
297
- tokens.push({ type: "Parenthesis", value: char, pos: i });
298
- continue;
299
- }
300
-
301
- // array
302
- if (char === "[") {
303
- flushCurrent(char, i);
304
- tokens.push({ type: "ArrayStart", pos: i });
305
- continue;
306
- }
307
-
308
- if (char === "]") {
309
- flushCurrent(char, i);
310
- tokens.push({ type: "ArrayEnd", pos: i });
311
- continue;
312
- }
313
-
314
- // OBJECT START
315
- if (char === "{") {
316
- flushCurrent(char, i);
317
- tokens.push({ type: "BlockStart", pos: i });
318
- continue;
319
- }
320
-
321
- // OBJECT END
322
- if (char === "}") {
323
- flushCurrent(char, i);
324
- tokens.push({ type: "BlockEnd", pos: i });
325
- continue;
326
- }
327
-
328
- // comma
329
- if (char === comma) {
330
- flushCurrent(char, i);
331
- tokens.push({ type: "Comma", pos: i });
332
- continue;
333
- }
334
-
335
- // semicolon
336
- if (char === semicolon) {
337
- flushCurrent(char, i);
338
- tokens.push({ type: "Semicolon", pos: i });
339
- continue;
340
- }
341
-
342
- // space
343
- if (char === " ") {
344
- flushCurrent(next, i);
345
- continue;
346
- }
347
-
348
- // build token
349
- current += char;
350
-
351
- if (i === expr.length - 1) {
352
- flushCurrent(null, i);
353
- }
354
- }
355
-
356
- if (quote) throw new Error("Unclosed string literal");
357
-
358
- // merge number + unit
359
- const merged = [];
360
- for (let i = 0; i < tokens.length; i++) {
361
- const t = tokens[i];
362
- const next = tokens[i + 1];
363
-
364
- if (t?.type === "Number" && next?.type === "Unit") {
365
- merged.push({
366
- type: "NumberWithUnit",
367
- value: t.value,
368
- unit: next.value,
369
- pos: t.pos
370
- });
371
- i++;
372
- continue;
373
- }
374
-
375
- merged.push(t);
376
- }
377
-
378
- // implicit multiplication
379
- const final = [];
380
- for (let i = 0; i < merged.length; i++) {
381
- const a = merged[i];
382
- const b = merged[i + 1];
383
-
384
- final.push(a);
385
-
386
- if (
387
- a && b &&
388
- (
389
- (["Number", "Identifier"].includes(a.type) ||
390
- (a.type === "Parenthesis" && a.value === ")") ||
391
- a.type === "ArrayEnd") &&
392
- (["Identifier", "Function"].includes(b.type) ||
393
- (b.type === "Parenthesis" && b.value === "("))
394
- )
395
- ) {
396
- final.push({ type: "Operator", value: "*", implicit: true });
397
- }
398
- }
399
-
400
- return final;
401
- }
402
-
403
- const isDenseMatrixWrapper = (value) =>
404
- value &&
405
- typeof value === "object" &&
406
- value.exprify === "DenseMatrix" &&
407
- "data" in value &&
408
- "size" in value;
409
-
410
- const cloneMatrixData = (value) => {
411
- if (Array.isArray(value)) {
412
- return value.map(cloneMatrixData);
413
- }
414
-
415
- return value;
416
- };
417
-
418
- const getMatrixSize = (data) => {
419
- if (Array.isArray(data) && data.every(Array.isArray)) {
420
- return [data.length, data[0]?.length || 0];
421
- }
422
-
423
- if (Array.isArray(data)) {
424
- return [data.length];
425
- }
426
-
427
- throw new Error("Matrix data must be an array");
428
- };
429
-
430
- const wrapDenseMatrix = (data) => ({
431
- exprify: "DenseMatrix",
432
- data: cloneMatrixData(data),
433
- size: getMatrixSize(data)
434
- });
435
-
436
- const unwrapDenseMatrix = (value) =>
437
- isDenseMatrixWrapper(value) ? cloneMatrixData(value.data) : value;
438
-
439
- const serializeExprifyValue = (value) => {
440
- if (isDenseMatrixWrapper(value)) {
441
- return JSON.stringify(value);
442
- }
443
-
444
- if (Array.isArray(value) || (value && typeof value === "object")) {
445
- return JSON.stringify(value, (_, current) => {
446
- if (isDenseMatrixWrapper(current)) {
447
- return current;
448
- }
449
-
450
- return current;
451
- });
452
- }
453
-
454
- return value;
455
- };
456
-
457
- function evaluateAST(node, context = {}) {
458
-
459
- const vars = context.variables;
460
- const fns = context.functions;
461
- const units = context.units;
462
-
463
-
464
- const isUnitObj = (v) =>
465
- v && typeof v === "object" && "value" in v && "unit" in v;
466
-
467
- const isComplex = (v) =>
468
- v && typeof v === "object" && "re" in v && "im" in v;
469
-
470
- const isSliceNode = (v) =>
471
- v && typeof v === "object" && v.type === "SliceExpression";
472
-
473
- const isMatrix = (v) =>
474
- Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
475
-
476
- const normalizeMatrix = (value) => {
477
- value = unwrapDenseMatrix(value);
478
- if (isMatrix(value)) return value.map((row) => [...row]);
479
- if (Array.isArray(value)) return [value];
480
- throw new Error("Expected matrix-compatible value");
481
- };
482
-
483
- const toOneBasedIndex = (value) => {
484
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
485
- throw new Error("Matrix indices must be positive integers");
486
- }
487
-
488
- return value - 1;
489
- };
490
-
491
- const resolveSelector = (selector, contextLength) => {
492
- if (isSliceNode(selector)) {
493
- const startValue = selector.start == null ? 1 : evaluateAST(selector.start, context);
494
- const endValue = selector.end == null ? contextLength : evaluateAST(selector.end, context);
495
- const start = toOneBasedIndex(startValue);
496
- const end = toOneBasedIndex(endValue);
497
-
498
- if (end < start) {
499
- return [];
500
- }
501
-
502
- const result = [];
503
- for (let index = start; index <= end; index++) {
504
- result.push(index);
505
- }
506
- return result;
507
- }
508
-
509
- return [toOneBasedIndex(evaluateAST(selector, context))];
510
- };
511
-
512
- const indexMatrix = (matrix, selectors) => {
513
- const target = normalizeMatrix(matrix);
514
-
515
- if (selectors.length === 1) {
516
- const rowIndexes = resolveSelector(selectors[0], target.length);
517
- const rows = rowIndexes.map((rowIndex) => {
518
- if (rowIndex >= target.length) {
519
- throw new Error("Row index out of range");
520
- }
521
- return [...target[rowIndex]];
522
- });
523
-
524
- return rows.length === 1 ? rows[0] : rows;
525
- }
526
-
527
- const rowIndexes = resolveSelector(selectors[0], target.length);
528
- const colIndexes = resolveSelector(selectors[1], target[0]?.length || 0);
529
-
530
- const values = rowIndexes.map((rowIndex) => {
531
- if (rowIndex >= target.length) {
532
- throw new Error("Row index out of range");
533
- }
534
-
535
- return colIndexes.map((colIndex) => {
536
- if (colIndex >= target[rowIndex].length) {
537
- throw new Error("Column index out of range");
538
- }
539
- return target[rowIndex][colIndex];
540
- });
541
- });
542
-
543
- if (rowIndexes.length === 1 && colIndexes.length === 1) {
544
- return values[0][0];
545
- }
546
-
547
- if (rowIndexes.length === 1) {
548
- return values[0];
549
- }
550
-
551
- if (colIndexes.length === 1) {
552
- return values.map((row) => [row[0]]);
553
- }
554
-
555
- return values;
556
- };
557
-
558
- const assignMatrixIndex = (matrix, selectors, value) => {
559
- const target = isMatrix(matrix)
560
- ? matrix.map((row) => [...row])
561
- : Array.isArray(matrix)
562
- ? [matrix.slice()]
563
- : [];
564
-
565
- const rowSelector = selectors[0];
566
- const colSelector = selectors[1];
567
-
568
- if (!rowSelector) {
569
- throw new Error("Matrix assignment requires at least one index");
570
- }
571
-
572
- const rowContextLength = Math.max(target.length, 1);
573
- const rowIndexes = resolveSelector(rowSelector, rowContextLength);
574
-
575
- if (selectors.length === 1) {
576
- const rowsValue = isMatrix(value) ? value : normalizeMatrix(value);
577
-
578
- if (rowsValue.length !== rowIndexes.length) {
579
- throw new Error("Assigned row count does not match slice");
580
- }
581
-
582
- rowIndexes.forEach((rowIndex, index) => {
583
- target[rowIndex] = [...rowsValue[index]];
584
- });
585
-
586
- return {
587
- updatedMatrix: target,
588
- selectionResult: rowIndexes.length === 1 ? [target[rowIndexes[0]]] : rowIndexes.map((rowIndex) => [target[rowIndex]])
589
- };
590
- }
591
-
592
- const maxCols = Math.max(...target.map((row) => row.length), 0, 1);
593
- const colIndexes = resolveSelector(colSelector, maxCols);
594
- const normalizedValue = normalizeMatrix(value);
595
-
596
- if (normalizedValue.length !== rowIndexes.length) {
597
- throw new Error("Assigned row count does not match matrix slice");
598
- }
599
-
600
- normalizedValue.forEach((row, rowOffset) => {
601
- if (row.length !== colIndexes.length) {
602
- throw new Error("Assigned column count does not match matrix slice");
603
- }
604
- });
605
-
606
- rowIndexes.forEach((rowIndex, rowOffset) => {
607
- if (!target[rowIndex]) {
608
- target[rowIndex] = [];
609
- }
610
-
611
- colIndexes.forEach((colIndex, colOffset) => {
612
- target[rowIndex][colIndex] = normalizedValue[rowOffset][colOffset];
613
- });
614
- });
615
-
616
- return {
617
- updatedMatrix: target,
618
- selectionResult: rowIndexes.length === 1
619
- ? [colIndexes.map((colIndex) => target[rowIndexes[0]][colIndex])]
620
- : rowIndexes.map((rowIndex) => colIndexes.map((colIndex) => target[rowIndex][colIndex]))
621
- };
622
- };
623
-
624
- const multiplyMatrices = (left, right) => {
625
- const a = normalizeMatrix(left);
626
- const b = normalizeMatrix(right);
627
-
628
- if (a[0].length !== b.length) {
629
- throw new Error("Matrix dimensions do not allow multiplication");
630
- }
631
-
632
- return a.map((row) =>
633
- b[0].map((_, colIndex) =>
634
- row.reduce((sum, value, rowIndex) => sum + (value * b[rowIndex][colIndex]), 0)
635
- )
636
- );
637
- };
638
-
639
- const toComplex = (value) => {
640
- if (isComplex(value)) return value;
641
- if (typeof value === "number") return { re: value, im: 0 };
642
- throw new Error("Complex arithmetic only supports numbers");
643
- };
644
-
645
- const fromImaginary = (value) => ({ re: 0, im: value });
646
-
647
- const simplifyComplex = (value) =>
648
- value.im === 0 ? value.re : value;
649
-
650
- const createFunctionScope = (params, args) => {
651
- const scopedValues = {};
652
-
653
- params.forEach((param, index) => {
654
- scopedValues[param] = args[index];
655
- });
656
-
657
- return scopedValues;
658
- };
659
-
660
- const evalComplexBinary = (operator, left, right) => {
661
- const a = toComplex(left);
662
- const b = toComplex(right);
663
-
664
- switch (operator) {
665
- case "+":
666
- return simplifyComplex({ re: a.re + b.re, im: a.im + b.im });
667
- case "-":
668
- return simplifyComplex({ re: a.re - b.re, im: a.im - b.im });
669
- case "*":
670
- return simplifyComplex({
671
- re: (a.re * b.re) - (a.im * b.im),
672
- im: (a.re * b.im) + (a.im * b.re)
673
- });
674
- case "/": {
675
- const denominator = (b.re ** 2) + (b.im ** 2);
676
-
677
- if (denominator === 0) {
678
- throw new Error("Division by zero");
679
- }
680
-
681
- return simplifyComplex({
682
- re: ((a.re * b.re) + (a.im * b.im)) / denominator,
683
- im: ((a.im * b.re) - (a.re * b.im)) / denominator
684
- });
685
- }
686
- default:
687
- throw new Error(`Operator ${operator} is not supported for complex numbers`);
688
- }
689
- };
690
-
691
- /* ================= EVALUATOR ================= */
692
-
693
- switch (node.type) {
694
-
695
- /* ===== LITERAL ===== */
696
- case "Literal":
697
- return node.value;
698
-
699
- case "ImaginaryLiteral":
700
- return fromImaginary(node.value);
701
-
702
- case "UnitLiteral":
703
- return { value: node.value, unit: node.unit };
704
-
705
- /* ===== VARIABLE ===== */
706
- case "Identifier":
707
- return vars.get(node.name);
708
-
709
- /* ===== ASSIGNMENT ===== */
710
- case "AssignmentExpression": {
711
- const value = evaluateAST(node.right, context);
712
-
713
- if (node.left.type === "Identifier") {
714
- vars.set(node.left.name, value);
715
- if (node.right.type === "ArrayExpression") {
716
- return wrapDenseMatrix(unwrapDenseMatrix(value));
717
- }
718
- return value;
719
- }
720
-
721
- if (node.left.type === "IndexExpression" && node.left.object.type === "Identifier") {
722
- const currentValue = vars.get(node.left.object.name);
723
- const assigned = assignMatrixIndex(currentValue, node.left.selectors, value);
724
- vars.set(node.left.object.name, assigned.updatedMatrix);
725
- return assigned.selectionResult;
726
- }
727
-
728
- throw new Error("Invalid assignment target");
729
- }
730
-
731
- case "FunctionAssignmentExpression": {
732
- if (node.operator !== "=") {
733
- throw new Error(`Operator ${node.operator} is not supported for function definitions`);
734
- }
735
-
736
- const fn = (...args) => {
737
- const scopedContext = context.withScope(createFunctionScope(node.params, args));
738
- return evaluateAST(node.right, scopedContext);
739
- };
740
-
741
- fns.register(node.left.name, fn);
742
- return fn;
743
- }
744
-
745
- /* ===== UNARY ===== */
746
- case "UnaryExpression": {
747
- const val = evaluateAST(node.argument, context);
748
-
749
- switch (node.operator) {
750
- case "-":
751
- return isComplex(val)
752
- ? simplifyComplex({ re: -val.re, im: -val.im })
753
- : -val;
754
- case "!": return !val;
755
- }
756
-
757
- throw new Error(`Unknown unary operator ${node.operator}`);
758
- }
759
-
760
- /* ===== BINARY ===== */
761
- case "BinaryExpression": {
762
- let left = evaluateAST(node.left, context);
763
- let right = evaluateAST(node.right, context);
764
-
765
- // UNIT handling
766
- if (isUnitObj(left) || isUnitObj(right)) {
767
-
768
- if (!units) throw new Error("Unit system not available");
769
-
770
- return units.compute(node.operator, left, right);
771
- }
772
-
773
- if (node.operator === "*" && (Array.isArray(left) || Array.isArray(right))) {
774
- return multiplyMatrices(left, right);
775
- }
776
-
777
- if (isComplex(left) || isComplex(right)) {
778
- return evalComplexBinary(node.operator, left, right);
779
- }
780
-
781
- switch (node.operator) {
782
- case "+": return left + right;
783
- case "-": return left - right;
784
- case "*": return left * right;
785
- case "/": return left / right;
786
- case "%": return left % right;
787
- case "^": return left ** right;
788
-
789
- case ">": return left > right;
790
- case "<": return left < right;
791
- case ">=": return left >= right;
792
- case "<=": return left <= right;
793
- case "==": return left === right;
794
- }
795
-
796
- throw new Error(`Unknown operator ${node.operator}`);
797
- }
798
-
799
- /* ===== LOGICAL ===== */
800
- case "LogicalExpression": {
801
- const left = evaluateAST(node.left, context);
802
-
803
- if (node.operator === "&&") {
804
- return left && evaluateAST(node.right, context);
805
- }
806
-
807
- if (node.operator === "||") {
808
- return left || evaluateAST(node.right, context);
809
- }
810
-
811
- if (node.operator === "??") {
812
- return left ?? evaluateAST(node.right, context);
813
- }
814
-
815
- throw new Error(`Unknown logical operator ${node.operator}`);
816
- }
817
-
818
- /* ===== FUNCTION CALL ===== */
819
- case "CallExpression": {
820
- const fnName = node.callee.name;
821
- const fn = fns.get(fnName);
822
-
823
- const args = node.arguments.map(arg =>
824
- evaluateAST(arg, context)
825
- );
826
-
827
- return fn(...args);
828
- }
829
-
830
- /* ===== PIPELINE ===== */
831
- case "PipelineExpression": {
832
- const leftVal = evaluateAST(node.left, context);
833
-
834
- // right must be function
835
- if (node.right.type === "CallExpression") {
836
- const fnName = node.right.callee.name;
837
- const fn = fns.get(fnName);
838
-
839
- const args = [
840
- leftVal,
841
- ...node.right.arguments.map(arg =>
842
- evaluateAST(arg, context)
843
- )
844
- ];
845
-
846
- return fn(...args);
847
- }
848
-
849
- if (node.right.type === "Identifier") {
850
- const fn = fns.get(node.right.name);
851
- return fn(leftVal);
852
- }
853
-
854
- throw new Error("Invalid pipeline target");
855
- }
856
-
857
- /* ===== UNIT CONVERSION ===== */
858
- case "UnitConversion": {
859
- const from = evaluateAST(node.from, context);
860
-
861
- if (!isUnitObj(from)) {
862
- throw new Error("Left side must be a unit value");
863
- }
864
-
865
- if (!units) {
866
- throw new Error("Unit system not available");
867
- }
868
-
869
- return units.convert(from.value, from.unit, node.to);
870
- }
871
-
872
- /* ===== ARRAY ===== */
873
- case "ArrayExpression":
874
- return node.elements.map(el => evaluateAST(el, context));
875
-
876
- case "IndexExpression": {
877
- const target = evaluateAST(node.object, context);
878
- return indexMatrix(target, node.selectors);
879
- }
880
-
881
- /* ===== OBJECT ===== */
882
- case "ObjectExpression": {
883
- const obj = {};
884
- for (let p of node.properties) {
885
- obj[p.key] = evaluateAST(p.value, context);
886
- }
887
- return obj;
888
- }
889
-
890
- /* ===== MEMBER ===== */
891
- case "MemberExpression": {
892
- const obj = evaluateAST(node.object, context);
893
-
894
- if (node.optional && obj == null) return undefined;
895
-
896
- return obj[node.property.name];
897
- }
898
-
899
- default:
900
- throw new Error(`Unknown AST node type: ${node.type}`);
901
- }
902
- }
903
-
904
- function createContext({ variables, functions, units, evaluate}) {
905
- if (!variables) throw new Error("Variable store missing");
906
- if (!functions) throw new Error("Function registry missing");
907
- if (!units) throw new Error("Units list missing");
908
- if (!evaluate) throw new Error("evaluate function missing");
909
-
910
- return {
911
- variables: variables,
912
- functions: functions,
913
- units: units,
914
- evaluate,
915
- withScope(scope = {}) {
916
- const tempVars = {
917
- ...variables.all?.(),
918
- ...scope
919
- };
920
- return createContext({
921
- functions: functions,
922
- evaluate,
923
- units,
924
- variables: {
925
- get: (k) => tempVars[k],
926
- set: (k, v) => (tempVars[k] = v),
927
- all: () => tempVars
928
- }
929
- });
930
-
931
- }
932
- };
933
- }
934
-
935
- const isValidNumberPair = (a, b) =>
936
- (typeof a === typeof b) &&
937
- (typeof a === 'number' || typeof a === 'bigint');
938
-
939
- const mathOperations = Object.freeze({
940
- power: function(a, b) {
941
- if (isValidNumberPair(a, b)) return a ** b;
942
- throw new Error("Invalid types for ^");
943
- },
944
-
945
- multiply: function(a, b) {
946
- if (isValidNumberPair(a, b)) return a * b;
947
- throw new Error("Invalid types for *");
948
- },
949
-
950
- divide: function(a, b) {
951
- if (isValidNumberPair(a, b)) {
952
- if (b === 0) throw new Error("Division by zero");
953
- return a / b;
954
- }
955
- throw new Error("Invalid types for /");
956
- },
957
-
958
- add: function(a, b) {
959
- if (isValidNumberPair(a, b)) return a + b;
960
- if (typeof a === 'string' && typeof b === 'string') return a + b;
961
- throw new Error("Invalid types for +");
962
- },
963
- subtract: function(a, b) {
964
- if (isValidNumberPair(a, b)) return a - b;
965
- throw new Error("Invalid types for -");
966
- },
967
-
968
- modulus: function(a, b) {
969
- if (isValidNumberPair(a, b)) return a % b;
970
- throw new Error("Invalid types for %");
971
- }
972
- });
973
-
974
- function createUnitsStore(initial = {}) {
975
- let units = { ...initial};
976
-
977
- // ---------- Helpers ----------
978
-
979
- function getAllUnitsFlat() {
980
- const result = new Set();
981
-
982
- for (const type in units) {
983
- for (const key in units[type]) {
984
- const u = units[type][key];
985
-
986
- const keyLower = key.toLowerCase();
987
- result.add(keyLower);
988
-
989
- // Unit name
990
- if (u.unit) {
991
- const unitLower = u.unit.toLowerCase();
992
-
993
- // Avoid duplicate like "m" vs "meter"
994
- if (unitLower !== keyLower) {
995
- // Optional: only single-word units
996
- if (unitLower.split(/\s+/).length === 1) {
997
- result.add(unitLower);
998
- }
999
- }
1000
- }
1001
-
1002
- // Symbol
1003
- if (u.symbol) {
1004
- const symbolLower = u.symbol.toLowerCase();
1005
-
1006
- // Avoid duplicate with unit name
1007
- if (!u.unit || symbolLower !== u.unit.toLowerCase()) {
1008
- result.add(symbolLower);
1009
- }
1010
- }
1011
- }
1012
- }
1013
-
1014
- return Array.from(result);
1015
- }
1016
-
1017
- function findUnit(input) {
1018
- input = input.toLowerCase();
1019
-
1020
- for (const type in units) {
1021
- for (const key in units[type]) {
1022
- const u = units[type][key];
1023
-
1024
- if (
1025
- key.toLowerCase() === input ||
1026
- u.unit?.toLowerCase() === input ||
1027
- u.symbol?.toLowerCase() === input
1028
- ) {
1029
- return { type, key , data: u};
1030
- }
1031
- }
1032
- }
1033
-
1034
- return null;
1035
- }
1036
-
1037
- // ---------- Core Convert ----------
1038
-
1039
- function convert(value, fromUnit, toUnit) {
1040
- const from = findUnit(fromUnit);
1041
- const to = findUnit(toUnit);
1042
-
1043
- if (!from) throw new Error(`Unknown unit: ${fromUnit}`);
1044
- if (!to) throw new Error(`Unknown unit: ${toUnit}`);
1045
-
1046
- if (from.type !== to.type) {
1047
- throw new Error(`Cannot convert ${fromUnit} to ${toUnit} (${to.data.unit || to.key}). ${from.data.unit || from.key} conversion units like ${Object.keys(units[from.type]).join(", ")}`);
1048
- }
1049
-
1050
- const result = value * (from.data.value / to.data.value);
1051
-
1052
- return { value: result, unit: to.key };
1053
- }
1054
-
1055
- // ---------- Public API ----------
1056
-
1057
- return {
1058
- // Get all units
1059
- getUnits: () => units,
1060
-
1061
- // Replace all units
1062
- setUnits: (newUnits) => {
1063
- units = { ...newUnits };
1064
- },
1065
-
1066
- // Update single type
1067
- updateType: (type, data) => {
1068
- units[type] = { ...units[type], ...data };
1069
- },
1070
-
1071
- // Add new unit
1072
- addUnit: (type, key, unitObj) => {
1073
- if (!units[type]) units[type] = {};
1074
- units[type][key] = unitObj;
1075
- },
1076
- compute(op, left, right) {
1077
-
1078
- const isUnit = (v) =>
1079
- v && typeof v === "object" && "value" in v && "unit" in v;
1080
-
1081
- const apply = (a, b) => {
1082
- switch (op) {
1083
- case "+": return a + b;
1084
- case "-": return a - b;
1085
- case "*": return a * b;
1086
- case "/": return a / b;
1087
- case "%": return a % b;
1088
- case "^": return Math.pow(a, b);
1089
- }
1090
- };
1091
-
1092
- // BOTH UNIT
1093
- if (isUnit(left) && isUnit(right)) {
1094
-
1095
- const from = this.findUnit(right.unit);
1096
- const to = this.findUnit(left.unit);
1097
-
1098
- if (from.type !== to.type) {
1099
- throw new Error(`Cannot operate on different unit types`);
1100
- }
1101
-
1102
- // convert right → left unit
1103
- const r = right.value * (from.data.value / to.data.value);
1104
-
1105
- const result = apply(left.value, r);
1106
-
1107
- // multiplication/division produce compound units
1108
- if (op === "*") {
1109
- return { value: result, unit: left.unit };
1110
- }
1111
-
1112
- if (op === "/") {
1113
- return { value: result, unit: left.unit };
1114
- }
1115
-
1116
- if (op === "^") {
1117
- return { value: result, unit: left.unit };
1118
- }
1119
-
1120
- return { value: result, unit: left.unit };
1121
- }
1122
-
1123
- // ================= LEFT UNIT =================
1124
- if (isUnit(left) && !isUnit(right)) {
1125
- const result = apply(left.value, right);
1126
-
1127
- return { value: result, unit: left.unit };
1128
- }
1129
-
1130
- // ================= RIGHT UNIT =================
1131
- if (!isUnit(left) && isUnit(right)) {
1132
- const result = apply(left, right.value);
1133
-
1134
- if (op === "/") {
1135
- return { value: result, unit: right.unit };
1136
- }
1137
-
1138
- return { value: result, unit: right.unit };
1139
- }
1140
-
1141
- // ================= NORMAL =================
1142
- return apply(left, right);
1143
- },
1144
- // Convert
1145
- convert,
1146
-
1147
- // Search helpers
1148
- getAllUnitsFlat,
1149
- findUnit
1150
- };
1151
- }
1152
-
1153
- const globalUnits = {
1154
- // Length
1155
- length: {
1156
- m: { value: 1, unit: 'meter', symbol: 'm' },
1157
- cm: { value: 0.01, unit: 'centimeter', symbol: 'cm' },
1158
- mm: { value: 0.001, unit: 'millimeter', symbol: 'mm' },
1159
- km: { value: 1000, unit: 'kilometer', symbol: 'km' },
1160
- um: { value: 0.000001, unit: 'micrometer', symbol: 'um', note: 'also called micron' },
1161
- nm: { value: 0.000000001, unit: 'nanometer', symbol: 'nm' },
1162
- px: { value: 0.000264583, unit: 'pixel', symbol: 'px', note: '96dpi standard' },
1163
- em: { value: 0.000264583 * 16, unit: 'em', symbol: 'em', note: '1em = 16px by default' },
1164
- rem: { value: 0.000264583 * 16, unit: 'rem', symbol: 'rem', note: 'root em = 16px by default' },
1165
- pt: { value: 0.000352778, unit: 'point', symbol: 'pt', note: '1pt = 1/72 inch' },
1166
- pc: { value: 0.00423333, unit: 'pica', symbol: 'pc', note: '1pc = 12pt' },
1167
- inch: { value: 0.0254, unit: 'inch', symbol: 'in' },
1168
- ft: { value: 0.3048, unit: 'foot', symbol: 'ft' },
1169
- yd: { value: 0.9144, unit: 'yard', symbol: 'yd' },
1170
- mi: { value: 1609.344, unit: 'mile', symbol: 'mi' },
1171
- thou: { value: 0.0000254, unit: 'mil', symbol: 'thou', note: 'thousandth of an inch' },
1172
- furlong: { value: 201.168, unit: 'furlong', symbol: 'fur', note: '220 yards' },
1173
- nmi: { value: 1852, unit: 'nautical mile', symbol: 'nmi' },
1174
- fathom: { value: 1.8288, unit: 'fathom', symbol: 'fathom' },
1175
- au: { value: 1.496e11, unit: 'astronomical unit', symbol: 'AU' },
1176
- ly: { value: 9.4607e15, unit: 'light year', symbol: 'ly' },
1177
- pc: { value: 3.0857e16, unit: 'parsec', symbol: 'pc' }
1178
- },
1179
-
1180
- // Weight / Mass
1181
- weight: {
1182
- mg: { value: 1e-6, unit: 'milligram', symbol: 'mg' },
1183
- g: { value: 0.001, unit: 'gram', symbol: 'g' },
1184
- kg: { value: 1, unit: 'kilogram', symbol: 'kg' },
1185
- t: { value: 1000, unit: 'tonne', symbol: 't', note: 'metric ton' },
1186
- lb: { value: 0.453592, unit: 'pound', symbol: 'lb' },
1187
- oz: { value: 0.0283495, unit: 'ounce', symbol: 'oz' },
1188
- stone: { value: 6.35029, unit: 'stone', symbol: 'st', note: '1 stone = 14 lb' }
1189
- },
1190
-
1191
- // Time
1192
- time: {
1193
- s: { value: 1, unit: 'second', symbol: 's' },
1194
- min: { value: 60, unit: 'minute', symbol: 'min' },
1195
- h: { value: 3600, unit: 'hour', symbol: 'h' },
1196
- day: { value: 86400, unit: 'day', symbol: 'd' },
1197
- week: { value: 604800, unit: 'week', symbol: 'wk' },
1198
- month: { value: 2629800, unit: 'month', symbol: 'mo', note: 'average month = 30.44 days' },
1199
- year: { value: 31557600, unit: 'year', symbol: 'yr', note: 'average year = 365.25 days' }
1200
- },
1201
-
1202
- // Voltage
1203
- voltage: {
1204
- V: { value: 1, unit: 'volt', symbol: 'V' },
1205
- mV: { value: 0.001, unit: 'millivolt', symbol: 'mV' },
1206
- kV: { value: 1000, unit: 'kilovolt', symbol: 'kV' },
1207
- MV: { value: 1e6, unit: 'megavolt', symbol: 'MV' },
1208
- GV: { value: 1e9, unit: 'gigavolt', symbol: 'GV' },
1209
- statV: { value: 299.792458, unit: 'statvolt', symbol: 'statV', note: 'CGS unit' },
1210
- abV: { value: 1e-8, unit: 'abvolt', symbol: 'abV', note: 'CGS electromagnetic unit' }
1211
- },
1212
-
1213
- // Frequency
1214
- frequency: {
1215
- Hz: { value: 1, unit: 'hertz', symbol: 'Hz', note: '1 cycle per second' },
1216
- kHz: { value: 1e3, unit: 'kilohertz', symbol: 'kHz' },
1217
- MHz: { value: 1e6, unit: 'megahertz', symbol: 'MHz' },
1218
- GHz: { value: 1e9, unit: 'gigahertz', symbol: 'GHz' },
1219
- THz: { value: 1e12, unit: 'terahertz', symbol: 'THz' }
1220
- },
1221
-
1222
- // Power
1223
- power: {
1224
- W: { value: 1, unit: 'watt', symbol: 'W', note: '1 joule per second' },
1225
- mW: { value: 0.001, unit: 'milliwatt', symbol: 'mW' },
1226
- kW: { value: 1000, unit: 'kilowatt', symbol: 'kW' },
1227
- MW: { value: 1e6, unit: 'megawatt', symbol: 'MW' },
1228
- GW: { value: 1e9, unit: 'gigawatt', symbol: 'GW' },
1229
- HP: { value: 745.7, unit: 'horsepower', symbol: 'HP', note: 'mechanical HP = 745.7 W' },
1230
- kcal_per_h: { value: 1.163, unit: 'kilocalorie per hour', symbol: 'kcal/h', note: '= 1.163 W' },
1231
- BTU_per_h: { value: 0.29307107, unit: 'BTU per hour', symbol: 'BTU/h', note: '= 0.293 W' }
1232
- },
1233
-
1234
- // Sound
1235
- sound: {
1236
- dB: { value: 1, unit: 'decibel', symbol: 'dB', note: 'logarithmic unit of sound intensity' },
1237
- dBA: { value: 1, unit: 'A-weighted decibel', symbol: 'dBA', note: 'Adjusted for human hearing' },
1238
- dBC: { value: 1, unit: 'C-weighted decibel', symbol: 'dBC', note: 'Flat weighting for high-level sounds' }
1239
- },
1240
-
1241
- // Temperature
1242
- temperature: {
1243
- K: { value: 1, unit: 'kelvin', symbol: 'K' },
1244
- C: { value: 1, unit: 'Celsius', symbol: '°C', note: '°C → K: add 273.15' },
1245
- F: { value: 1, unit: 'Fahrenheit', symbol: '°F', note: '°F → K: (°F - 32) * 5/9 + 273.15' }
1246
- },
1247
-
1248
- // Pressure
1249
- pressure: {
1250
- Pa: { value: 1, unit: 'pascal', symbol: 'Pa' },
1251
- kPa: { value: 1000, unit: 'kilopascal', symbol: 'kPa' },
1252
- MPa: { value: 1e6, unit: 'megapascal', symbol: 'MPa' },
1253
- bar: { value: 1e5, unit: 'bar', symbol: 'bar' },
1254
- atm: { value: 101325, unit: 'atmosphere', symbol: 'atm' },
1255
- psi: { value: 6894.757, unit: 'pound per square inch', symbol: 'psi' },
1256
- mmHg:{ value: 133.322, unit: 'millimeter of mercury', symbol: 'mmHg' }
1257
- },
1258
-
1259
- // Energy
1260
- energy: {
1261
- J: { value: 1, unit: 'joule', symbol: 'J' },
1262
- kJ: { value: 1000, unit: 'kilojoule', symbol: 'kJ' },
1263
- cal: { value: 4.184, unit: 'calorie', symbol: 'cal' },
1264
- kcal:{ value: 4184, unit: 'kilocalorie', symbol: 'kcal' },
1265
- eV: { value: 1.60218e-19, unit: 'electronvolt', symbol: 'eV' },
1266
- BTU: { value: 1055.06, unit: 'BTU', symbol: 'BTU' }
1267
- },
1268
-
1269
- // Force
1270
- force: {
1271
- N: { value: 1, unit: 'newton', symbol: 'N' },
1272
- kN: { value: 1000, unit: 'kilonewton', symbol: 'kN' },
1273
- lbf: { value: 4.44822, unit: 'pound-force', symbol: 'lbf' },
1274
- kgf: { value: 9.80665, unit: 'kilogram-force', symbol: 'kgf' },
1275
- dyne:{ value: 1e-5, unit: 'dyne', symbol: 'dyn' }
1276
- },
1277
-
1278
- // Area
1279
- area: {
1280
- m2: { value: 1, unit: 'square meter', symbol: 'm²' },
1281
- cm2: { value: 0.0001, unit: 'square centimeter', symbol: 'cm²' },
1282
- km2: { value: 1e6, unit: 'square kilometer', symbol: 'km²' },
1283
- acre: { value: 4046.856, unit: 'acre', symbol: 'acre' },
1284
- hectare:{ value: 10000, unit: 'hectare', symbol: 'ha' },
1285
- ft2: { value: 0.092903, unit: 'square foot', symbol: 'ft²' },
1286
- yd2: { value: 0.836127, unit: 'square yard', symbol: 'yd²' }
1287
- },
1288
-
1289
- // Volume
1290
- volume: {
1291
- m3: { value: 1, unit: 'cubic meter', symbol: 'm³' },
1292
- L: { value: 0.001, unit: 'liter', symbol: 'L' },
1293
- mL: { value: 1e-6, unit: 'milliliter', symbol: 'mL' },
1294
- gallon:{ value: 0.00378541, unit: 'US gallon', symbol: 'gal' },
1295
- pint: { value: 0.000473176, unit: 'US pint', symbol: 'pt' },
1296
- floz: { value: 2.9574e-5, unit: 'US fluid ounce', symbol: 'fl oz' }
1297
- },
1298
-
1299
- // Electrical Current
1300
- current: {
1301
- A: { value: 1, unit: 'ampere', symbol: 'A' },
1302
- mA: { value: 0.001, unit: 'milliampere', symbol: 'mA' },
1303
- uA: { value: 0.000001, unit: 'microampere', symbol: 'uA' },
1304
- kA: { value: 1000, unit: 'kiloampere', symbol: 'kA' }
1305
- },
1306
-
1307
- // Resistance / Conductance
1308
- resistance: {
1309
- ohm: { value: 1, unit: 'ohm' },
1310
- kohm: { value: 1000, unit: 'kiloohm'},
1311
- megaohm: { value: 1e6, unit: 'megaohm'},
1312
- S: { value: 1, unit: 'siemens', symbol: 'S', note: 'conductance' }
1313
- },
1314
-
1315
- // Capacitance / Inductance
1316
- capacitance: {
1317
- F: { value: 1, unit: 'farad', symbol: 'F' },
1318
- mF: { value: 0.001, unit: 'millifarad'},
1319
- uF: { value: 0.000001, unit: 'microfarad' }
1320
- },
1321
- inductance: {
1322
- H: { value: 1, unit: 'henry', symbol: 'H' },
1323
- mH: { value: 0.001, unit: 'millihenry', symbol: 'mH' },
1324
- uH: { value: 0.000001, unit: 'microhenry', symbol: 'uH' }
1325
- },
1326
-
1327
- // Luminous Intensity / Illuminance
1328
- light: {
1329
- cd: { value: 1, unit: 'candela', symbol: 'cd' },
1330
- lm: { value: 1, unit: 'lumen', symbol: 'lm' },
1331
- lx: { value: 1, unit: 'lux', symbol: 'lx' }
1332
- },
1333
-
1334
- // Data / Digital Storage
1335
- data: {
1336
- bit: { value: 1, unit: 'bit', symbol: 'bit' },
1337
- B: { value: 8, unit: 'byte', symbol: 'B' },
1338
- KB: { value: 8e3, unit: 'kilobyte', symbol: 'KB' },
1339
- MB: { value: 8e6, unit: 'megabyte', symbol: 'MB' },
1340
- GB: { value: 8e9, unit: 'gigabyte', symbol: 'GB' },
1341
- TB: { value: 8e12, unit: 'terabyte', symbol: 'TB' }
1342
- },
1343
-
1344
- // Angle
1345
- angle: {
1346
- deg: { value: 1, unit: 'degree', symbol: '°' },
1347
- rad: { value: 57.2958, unit: 'radian', symbol: 'rad', note: '1 rad = 57.2958°' },
1348
- grad:{ value: 0.9, unit: 'grad', symbol: 'grad', note: '1 grad = 0.9°' }
1349
- },
1350
- radiation: {
1351
- // Absorbed Dose
1352
- Gy: { value: 1, unit: 'gray', symbol: 'Gy', note: 'Absorbed dose: 1 Gy = 1 J/kg' },
1353
- mGy: { value: 0.001, unit: 'milligray', symbol: 'mGy' },
1354
- rad: { value: 0.01, unit: 'rad', symbol: 'rad', note: '1 rad = 0.01 Gy' },
1355
-
1356
- // Dose Equivalent
1357
- Sv: { value: 1, unit: 'sievert', symbol: 'Sv', note: 'Biological effect dose equivalent' },
1358
- mSv: { value: 0.001, unit: 'millisievert', symbol: 'mSv' },
1359
- rem: { value: 0.01, unit: 'rem', symbol: 'rem', note: '1 rem = 0.01 Sv' },
1360
-
1361
- // Radioactivity
1362
- Bq: { value: 1, unit: 'becquerel', symbol: 'Bq', note: '1 decay per second' },
1363
- kBq: { value: 1e3, unit: 'kilobecquerel', symbol: 'kBq' },
1364
- MBq: { value: 1e6, unit: 'megabecquerel', symbol: 'MBq' },
1365
- GBq: { value: 1e9, unit: 'gigabecquerel', symbol: 'GBq' },
1366
- Ci: { value: 3.7e10, unit: 'curie', symbol: 'Ci', note: '1 Ci = 3.7 x 10¹⁰ decays per second' },
1367
- mCi: { value: 3.7e7, unit: 'millicurie', symbol: 'mCi' }
1368
- }
1369
- };
1370
-
1371
- const validVarName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
1372
-
1373
- function createVarStore(initial = {}) {
1374
- let store = Object.create(null);
1375
-
1376
-
1377
- for (const key in initial) {
1378
- store[key] = initial[key];
1379
- }
1380
-
1381
- return {
1382
- set(name, value, { override = true } = {}) {
1383
-
1384
- // Name validation
1385
- if (typeof name !== "string" || !name) {
1386
- throw new Error("Variable name must be a non-empty string");
1387
- }
1388
-
1389
- if (!validVarName.test(name)) {
1390
- throw new Error(`Variable Name Error: '${name}' is not a valid variable name`);
1391
- }
1392
-
1393
- // Value validation
1394
- if (value === undefined) {
1395
- throw new Error(`Variable Value Error: '${name}' cannot be undefined`);
1396
- }
1397
-
1398
- // Prevent overwrite (optional)
1399
- if (!override && name in variablesDB) {
1400
- throw new Error(`Variable '${name}' already exists`);
1401
- }
1402
-
1403
- store[name] = value;
1404
- },
1405
-
1406
- //get variable
1407
- get(name) {
1408
- return store[name];
1409
- },
1410
-
1411
- // check existence
1412
- has(name) {
1413
- return Object.prototype.hasOwnProperty.call(store, name);
1414
- },
1415
-
1416
- // remove variable
1417
- remove(name) {
1418
- delete store[name];
1419
- },
1420
-
1421
- // get all variables (snapshot)
1422
- all() {
1423
- return { ...store };
1424
- },
1425
-
1426
- // clear all
1427
- clear() {
1428
- store = Object.create(null);
1429
- },
1430
-
1431
- // merge multiple variables
1432
- merge(obj = {}) {
1433
- for (const key in obj) {
1434
- store[key] = obj[key];
1435
- }
1436
- },
1437
-
1438
- // clone store (for scoped instances)
1439
- clone() {
1440
- return createVarStore(store);
1441
- }
1442
- };
1443
- }
1444
-
1445
- function createFunctionRegistry(initial = {}) {
1446
- const store = Object.create(null);
1447
-
1448
- for (const key in initial) {
1449
- if (typeof initial[key] === "function") {
1450
- store[key] = initial[key];
1451
- }
1452
- }
1453
-
1454
- return {
1455
- getAllFunctionsName() {
1456
- return Object.keys(store);
1457
- },
1458
- // register new formula
1459
- register(name, fn) {
1460
- if (typeof name !== "string" || !name) {
1461
- throw new Error("Formula name must be a non-empty string");
1462
- }
1463
-
1464
- if (typeof fn !== "function") {
1465
- throw new Error(`Formula "${name}" must be callable`);
1466
- }
1467
-
1468
- store[name] = fn;
1469
- },
1470
-
1471
- // get formula
1472
- get(name) {
1473
- return store[name];
1474
- },
1475
-
1476
- // check existence
1477
- has(name) {
1478
- return Object.prototype.hasOwnProperty.call(store, name);
1479
- },
1480
-
1481
- // remove formula
1482
- remove(name) {
1483
- delete store[name];
1484
- },
1485
-
1486
- // list all
1487
- all() {
1488
- return { ...store };
1489
- },
1490
-
1491
- // clear registry
1492
- clear() {
1493
- for (const key in store) {
1494
- delete store[key];
1495
- }
1496
- },
1497
-
1498
- // extend multiple
1499
- extend(formulas = {}) {
1500
- for (const name in formulas) {
1501
- if (typeof formulas[name] === "function") {
1502
- store[name] = formulas[name];
1503
- }
1504
- }
1505
- },
1506
-
1507
- // clone (for scoped instances)
1508
- clone() {
1509
- return createFormulaRegistry(store);
1510
- }
1511
- };
1512
- }
1513
-
1514
- function validateSquareMatrix(matrix) {
1515
- matrix = unwrapDenseMatrix(matrix);
1516
- if (!Array.isArray(matrix) || matrix.length === 0) {
1517
- throw new Error("det() expects a non-empty matrix");
1518
- }
1519
-
1520
- if (!matrix.every(Array.isArray)) {
1521
- throw new Error("det() expects a 2D matrix");
1522
- }
1523
-
1524
- const size = matrix.length;
1525
- if (!matrix.every((row) => row.length === size)) {
1526
- throw new Error("det() expects a square matrix");
1527
- }
1528
-
1529
- for (const row of matrix) {
1530
- for (const value of row) {
1531
- if (typeof value !== "number" && typeof value !== "bigint") {
1532
- throw new Error("det() matrix values must be numeric");
1533
- }
1534
- }
1535
- }
1536
- }
1537
-
1538
- function determinant(matrix) {
1539
- matrix = unwrapDenseMatrix(matrix);
1540
- validateSquareMatrix(matrix);
1541
-
1542
- if (matrix.length === 1) {
1543
- return matrix[0][0];
1544
- }
1545
-
1546
- if (matrix.length === 2) {
1547
- return (matrix[0][0] * matrix[1][1]) - (matrix[0][1] * matrix[1][0]);
1548
- }
1549
-
1550
- return matrix[0].reduce((sum, value, columnIndex) => {
1551
- const minor = matrix.slice(1).map((row) =>
1552
- row.filter((_, index) => index !== columnIndex)
1553
- );
1554
- const cofactor = columnIndex % 2 === 0 ? value : -value;
1555
- return sum + (cofactor * determinant(minor));
1556
- }, 0);
1557
- }
1558
-
1559
- function asMatrixData(value) {
1560
- const data = unwrapDenseMatrix(value);
1561
- if (!Array.isArray(data)) {
1562
- throw new Error("Expected matrix data");
1563
- }
1564
- return data;
1565
- }
1566
-
1567
- function solveLinearSystem(coefficients, constants) {
1568
- const n = coefficients.length;
1569
- const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
1570
-
1571
- for (let pivot = 0; pivot < n; pivot++) {
1572
- let maxRow = pivot;
1573
- let maxValue = Math.abs(augmented[pivot][pivot]);
1574
-
1575
- for (let row = pivot + 1; row < n; row++) {
1576
- const current = Math.abs(augmented[row][pivot]);
1577
- if (current > maxValue) {
1578
- maxValue = current;
1579
- maxRow = row;
1580
- }
1581
- }
1582
-
1583
- if (maxValue === 0) {
1584
- throw new Error("Linear system is singular");
1585
- }
1586
-
1587
- if (maxRow !== pivot) {
1588
- [augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
1589
- }
1590
-
1591
- const pivotValue = augmented[pivot][pivot];
1592
- for (let col = pivot; col <= n; col++) {
1593
- augmented[pivot][col] /= pivotValue;
1594
- }
1595
-
1596
- for (let row = 0; row < n; row++) {
1597
- if (row === pivot) continue;
1598
- const factor = augmented[row][pivot];
1599
- for (let col = pivot; col <= n; col++) {
1600
- augmented[row][col] -= factor * augmented[pivot][col];
1601
- }
1602
- }
1603
- }
1604
-
1605
- return augmented.map((row) => row[n]);
1606
- }
1607
-
1608
- function lupDecomposition(input) {
1609
- const matrix = asMatrixData(input).map((row) => [...row]);
1610
- validateSquareMatrix(matrix);
1611
-
1612
- const n = matrix.length;
1613
- const permutation = Array.from({ length: n }, (_, index) => index);
1614
-
1615
- for (let pivot = 0; pivot < n; pivot++) {
1616
- let maxRow = pivot;
1617
- let maxValue = Math.abs(matrix[pivot][pivot]);
1618
-
1619
- for (let row = pivot + 1; row < n; row++) {
1620
- const current = Math.abs(matrix[row][pivot]);
1621
- if (current > maxValue) {
1622
- maxValue = current;
1623
- maxRow = row;
1624
- }
1625
- }
1626
-
1627
- if (maxValue === 0) {
1628
- throw new Error("Matrix is singular");
1629
- }
1630
-
1631
- if (maxRow !== pivot) {
1632
- [matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
1633
- [permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
1634
- }
1635
-
1636
- for (let row = pivot + 1; row < n; row++) {
1637
- matrix[row][pivot] /= matrix[pivot][pivot];
1638
- for (let col = pivot + 1; col < n; col++) {
1639
- matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
1640
- }
1641
- }
1642
- }
1643
-
1644
- const L = matrix.map((row, rowIndex) =>
1645
- row.map((value, colIndex) => {
1646
- if (rowIndex === colIndex) return 1;
1647
- if (rowIndex > colIndex) return value;
1648
- return 0;
1649
- })
1650
- );
1651
-
1652
- const U = matrix.map((row, rowIndex) =>
1653
- row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
1654
- );
1655
-
1656
- return {
1657
- L: wrapDenseMatrix(L),
1658
- U: wrapDenseMatrix(U),
1659
- p: permutation
1660
- };
1661
- }
1662
-
1663
- function linearSolve(aInput, bInput) {
1664
- const { L, U, p } = lupDecomposition(aInput);
1665
- const a = asMatrixData(aInput);
1666
- const bData = asMatrixData(bInput);
1667
- const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
1668
-
1669
- if (a.length !== bVector.length) {
1670
- throw new Error("Right-hand side dimension mismatch");
1671
- }
1672
-
1673
- const permutedB = p.map((index) => bVector[index]);
1674
- const y = new Array(a.length).fill(0);
1675
-
1676
- for (let row = 0; row < a.length; row++) {
1677
- y[row] = permutedB[row];
1678
- for (let col = 0; col < row; col++) {
1679
- y[row] -= L.data[row][col] * y[col];
1680
- }
1681
- }
1682
-
1683
- const x = new Array(a.length).fill(0);
1684
- for (let row = a.length - 1; row >= 0; row--) {
1685
- x[row] = y[row];
1686
- for (let col = row + 1; col < a.length; col++) {
1687
- x[row] -= U.data[row][col] * x[col];
1688
- }
1689
- x[row] /= U.data[row][row];
1690
- }
1691
-
1692
- return wrapDenseMatrix(x.map((value) => [value]));
1693
- }
1694
-
1695
- function solveLyapunov(aInput, qInput) {
1696
- const A = asMatrixData(aInput).map((row) => [...row]);
1697
- const Q = asMatrixData(qInput).map((row) => [...row]);
1698
- validateSquareMatrix(A);
1699
- validateSquareMatrix(Q);
1700
-
1701
- const n = A.length;
1702
- if (Q.length !== n) {
1703
- throw new Error("A and Q must have the same dimensions");
1704
- }
1705
-
1706
- const coefficients = [];
1707
- const constants = [];
1708
-
1709
- for (let row = 0; row < n; row++) {
1710
- for (let col = 0; col < n; col++) {
1711
- const equation = new Array(n * n).fill(0);
1712
-
1713
- for (let k = 0; k < n; k++) {
1714
- equation[k * n + col] += A[row][k];
1715
- equation[row * n + k] += A[col][k];
1716
- }
1717
-
1718
- coefficients.push(equation);
1719
- constants.push(-Q[row][col]);
1720
- }
1721
- }
1722
-
1723
- const solution = solveLinearSystem(coefficients, constants);
1724
- const X = [];
1725
-
1726
- for (let row = 0; row < n; row++) {
1727
- X.push(solution.slice(row * n, (row + 1) * n));
1728
- }
1729
-
1730
- return wrapDenseMatrix(X);
1731
- }
1732
-
1733
- function evaluatePolynomial(coefficients, x) {
1734
- return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
1735
- }
1736
-
1737
- function syntheticDivide(coefficients, root) {
1738
- const descending = [...coefficients].reverse();
1739
- const quotient = [descending[0]];
1740
-
1741
- for (let index = 1; index < descending.length - 1; index++) {
1742
- quotient.push(descending[index] + (quotient[index - 1] * root));
1743
- }
1744
-
1745
- const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
1746
- return {
1747
- quotient: quotient.reverse(),
1748
- remainder
1749
- };
1750
- }
1751
-
1752
- function solveQuadratic(coefficients) {
1753
- const [c, b, a] = coefficients;
1754
- const discriminant = (b ** 2) - (4 * a * c);
1755
- if (discriminant < 0) {
1756
- throw new Error("Only real roots are supported");
1757
- }
1758
-
1759
- const sqrtDisc = Math.sqrt(discriminant);
1760
- return [
1761
- (-b + sqrtDisc) / (2 * a),
1762
- (-b - sqrtDisc) / (2 * a)
1763
- ];
1764
- }
1765
-
1766
- function polynomialRoots(...coefficients) {
1767
- while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
1768
- coefficients.pop();
1769
- }
1770
-
1771
- const degree = coefficients.length - 1;
1772
- if (degree < 1) {
1773
- throw new Error("polynomialRoot() expects at least a linear polynomial");
1774
- }
1775
-
1776
- if (degree === 1) {
1777
- const [b, a] = coefficients;
1778
- return [-b / a];
1779
- }
1780
-
1781
- if (degree === 2) {
1782
- return solveQuadratic(coefficients);
1783
- }
1784
-
1785
- if (degree === 3) {
1786
- const constant = coefficients[0];
1787
- coefficients[3];
1788
- const candidates = [];
1789
- const limit = Math.abs(constant);
1790
-
1791
- for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
1792
- if (limit % divisor === 0) {
1793
- candidates.push(divisor, -divisor);
1794
- }
1795
- }
1796
-
1797
- for (const candidate of candidates) {
1798
- if (evaluatePolynomial(coefficients, candidate) === 0) {
1799
- const reduced = syntheticDivide(coefficients, candidate);
1800
- const remainingRoots = solveQuadratic(reduced.quotient);
1801
- return [candidate, ...remainingRoots];
1802
- }
1803
- }
1804
- }
1805
-
1806
- throw new Error("polynomialRoot() currently supports degree up to 3");
1807
- }
1808
-
1809
- function dotProduct(a, b) {
1810
- return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
1811
- }
1812
-
1813
- function vectorNorm(vector) {
1814
- return Math.sqrt(dotProduct(vector, vector));
1815
- }
1816
-
1817
- function scaleVector(vector, scalar) {
1818
- return vector.map((value) => value * scalar);
1819
- }
1820
-
1821
- function subtractVectors(a, b) {
1822
- return a.map((value, index) => value - b[index]);
1823
- }
1824
-
1825
- function transpose(matrix) {
1826
- return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
1827
- }
1828
-
1829
- function qrDecomposition(input) {
1830
- const A = asMatrixData(input).map((row) => [...row]);
1831
- if (!A.length || !A.every((row) => row.length === A[0].length)) {
1832
- throw new Error("qr() expects a rectangular matrix");
1833
- }
1834
-
1835
- const rowCount = A.length;
1836
- const colCount = A[0].length;
1837
- const columns = transpose(A);
1838
- const qColumns = [];
1839
-
1840
- for (let col = 0; col < colCount; col++) {
1841
- let vector = [...columns[col]];
1842
-
1843
- for (let existing = 0; existing < qColumns.length; existing++) {
1844
- const projection = dotProduct(qColumns[existing], columns[col]);
1845
- vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
1846
- }
1847
-
1848
- const norm = vectorNorm(vector);
1849
- if (norm === 0) {
1850
- throw new Error("qr() requires linearly independent columns");
1851
- }
1852
-
1853
- qColumns.push(scaleVector(vector, 1 / norm));
1854
- }
1855
-
1856
- for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
1857
- let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
1858
-
1859
- for (const column of qColumns) {
1860
- const projection = dotProduct(column, candidate);
1861
- candidate = subtractVectors(candidate, scaleVector(column, projection));
1862
- }
1863
-
1864
- const norm = vectorNorm(candidate);
1865
- if (norm > 1e-10) {
1866
- qColumns.push(scaleVector(candidate, 1 / norm));
1867
- }
1868
- }
1869
-
1870
- const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
1871
- qColumns.map((column) => column[rowIndex])
1872
- );
1873
-
1874
- const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
1875
- for (let row = 0; row < rowCount; row++) {
1876
- for (let col = 0; col < colCount; col++) {
1877
- fullR[row][col] = dotProduct(qColumns[row], columns[col]);
1878
- }
1879
- }
1880
-
1881
- return {
1882
- Q: wrapDenseMatrix(Q),
1883
- R: wrapDenseMatrix(fullR)
1884
- };
1885
- }
1886
-
1887
- function splitTerms(expression) {
1888
- const normalized = expression.replace(/\s+/g, "");
1889
- if (!normalized) {
1890
- return [];
1891
- }
1892
-
1893
- return normalized
1894
- .replace(/-/g, "+-")
1895
- .split("+")
1896
- .filter(Boolean);
1897
- }
1898
-
1899
- function parsePolynomial(expression, variable) {
1900
- const terms = splitTerms(expression);
1901
- const coefficients = new Map();
1902
-
1903
- for (const term of terms) {
1904
- if (term.includes(variable)) {
1905
- const [rawCoeff, rawPower] = term.split(variable);
1906
- let coefficient;
1907
-
1908
- if (rawCoeff === "" || rawCoeff === "+") coefficient = 1;
1909
- else if (rawCoeff === "-") coefficient = -1;
1910
- else {
1911
- const cleaned = rawCoeff.endsWith("*") ? rawCoeff.slice(0, -1) : rawCoeff;
1912
- coefficient = Number(cleaned);
1913
- }
1914
-
1915
- if (!Number.isFinite(coefficient)) {
1916
- throw new Error("Unsupported algebra term");
1917
- }
1918
-
1919
- let power = 1;
1920
- if (rawPower) {
1921
- if (!rawPower.startsWith("^")) {
1922
- throw new Error("Unsupported algebra term");
1923
- }
1924
-
1925
- power = Number(rawPower.slice(1));
1926
- }
1927
-
1928
- if (!Number.isInteger(power) || power < 0) {
1929
- throw new Error("Only non-negative integer powers are supported");
1930
- }
1931
-
1932
- coefficients.set(power, (coefficients.get(power) || 0) + coefficient);
1933
- } else {
1934
- const constant = Number(term);
1935
- if (!Number.isFinite(constant)) {
1936
- throw new Error("Unsupported algebra term");
1937
- }
1938
- coefficients.set(0, (coefficients.get(0) || 0) + constant);
1939
- }
1940
- }
1941
-
1942
- return coefficients;
1943
- }
1944
-
1945
- function formatPolynomial(coefficients, variable) {
1946
- const ordered = [...coefficients.entries()]
1947
- .filter(([, coefficient]) => coefficient !== 0)
1948
- .sort((a, b) => b[0] - a[0]);
1949
-
1950
- if (!ordered.length) {
1951
- return "0";
1952
- }
1953
-
1954
- return ordered.map(([power, coefficient], index) => {
1955
- const negative = coefficient < 0;
1956
- const absCoeff = Math.abs(coefficient);
1957
- let body;
1958
-
1959
- if (power === 0) {
1960
- body = `${absCoeff}`;
1961
- } else if (power === 1) {
1962
- body = absCoeff === 1 ? variable : `${absCoeff} * ${variable}`;
1963
- } else {
1964
- body = absCoeff === 1
1965
- ? `${variable}^${power}`
1966
- : `${absCoeff} * ${variable}^${power}`;
1967
- }
1968
-
1969
- if (index === 0) {
1970
- return negative ? `-${body}` : body;
1971
- }
1972
-
1973
- return negative ? `- ${body}` : `+ ${body}`;
1974
- }).join(" ");
1975
- }
1976
-
1977
- function simplifyExpression(expression) {
1978
- const compact = expression.replace(/\s+/g, "");
1979
- const variableMatch = compact.match(/[a-zA-Z]+/);
1980
- const variable = variableMatch?.[0] || "x";
1981
- const coefficients = parsePolynomial(expression, variable);
1982
- return formatPolynomial(coefficients, variable);
1983
- }
1984
-
1985
- function derivativeExpression(expression, variable) {
1986
- const coefficients = parsePolynomial(expression, variable);
1987
- const derived = new Map();
1988
-
1989
- for (const [power, coefficient] of coefficients.entries()) {
1990
- if (power === 0) continue;
1991
- derived.set(power - 1, (derived.get(power - 1) || 0) + (coefficient * power));
1992
- }
1993
-
1994
- return formatPolynomial(derived, variable);
1995
- }
1996
-
1997
- const internalFunctions = {
1998
- max: (...args) => {
1999
- if (!args.length) throw new Error("max() requires arguments");
2000
- return Math.max(...args);
2001
- },
2002
-
2003
- min: (...args) => {
2004
- if (!args.length) throw new Error("min() requires arguments");
2005
- return Math.min(...args);
2006
- },
2007
-
2008
- abs: (x) => Math.abs(x),
2009
-
2010
- round: (x) => Math.round(x),
2011
-
2012
- floor: (x) => Math.floor(x),
2013
-
2014
- ceil: (x) => Math.ceil(x),
2015
-
2016
- sqrt: (x) => {
2017
- if (x < 0) throw new Error("sqrt() domain error");
2018
- return Math.sqrt(x);
2019
- },
2020
-
2021
- pow: (a, b) => a ** b,
2022
- det: (matrix) => determinant(matrix),
2023
- polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
2024
- lsolve: (a, b) => linearSolve(a, b),
2025
- lup: (matrix) => lupDecomposition(matrix),
2026
- lyap: (a, q) => solveLyapunov(a, q),
2027
- qr: (matrix) => qrDecomposition(matrix),
2028
- simplify: (expression) => {
2029
- if (typeof expression !== "string") {
2030
- throw new Error("simplify() expects an expression string");
2031
- }
2032
- return simplifyExpression(expression);
2033
- },
2034
- derivative: (expression, variable = "x") => {
2035
- if (typeof expression !== "string" || typeof variable !== "string") {
2036
- throw new Error("derivative() expects expression and variable strings");
2037
- }
2038
- return derivativeExpression(expression, variable);
2039
- },
2040
-
2041
- /* ================= TRIGONOMETRY ================= */
2042
-
2043
- sin: (x) => Math.sin(x),
2044
- cos: (x) => Math.cos(x),
2045
- tan: (x) => Math.tan(x),
2046
-
2047
- asin: (x) => Math.asin(x),
2048
- acos: (x) => Math.acos(x),
2049
- atan: (x) => Math.atan(x),
2050
-
2051
- /* ================= LOG / EXP ================= */
2052
-
2053
- log: (x) => {
2054
- if (x <= 0) throw new Error("log() domain error");
2055
- return Math.log(x);
2056
- },
2057
-
2058
- log10: (x) => {
2059
- if (x <= 0) throw new Error("log10() domain error");
2060
- return Math.log10(x);
2061
- },
2062
-
2063
- exp: (x) => Math.exp(x),
2064
-
2065
- /* ================= RANDOM ================= */
2066
-
2067
- random: () => Math.random(),
2068
-
2069
- /* ================= BOOLEAN / LOGIC ================= */
2070
-
2071
- and: (a, b) => Boolean(a && b),
2072
-
2073
- or: (a, b) => Boolean(a || b),
2074
-
2075
- not: (a) => !a,
2076
- "!": (a) => !a,
2077
-
2078
- /* ================= COMPARISON ================= */
2079
-
2080
- eq: (a, b) => a === b,
2081
-
2082
- neq: (a, b) => a !== b,
2083
- "notEqual": (a, b) => a !== b,
2084
-
2085
- gt: (a, b) => a > b,
2086
- "greaterThan": (a, b) => a > b,
2087
-
2088
- lt: (a, b) => a < b,
2089
- "lessThan": (a, b) => a < b,
2090
-
2091
- gte: (a, b) => a >= b,
2092
- "greaterThanOrEqual": (a, b) => a >= b,
2093
-
2094
- lte: (a, b) => a <= b,
2095
- "lessThanOrEqual": (a, b) => a <= b,
2096
-
2097
- /* ================= UTILITY ================= */
2098
-
2099
- clamp: (x, min, max) => {
2100
- if (min > max) throw new Error("clamp(): min > max");
2101
- return Math.min(Math.max(x, min), max);
2102
- },
2103
-
2104
- if: (condition, a, b) => (condition ? a : b),
2105
-
2106
- /* ================= TYPE ================= */
2107
-
2108
- typeof: (x) => typeof x,
2109
-
2110
- /* ================= STRING ================= */
2111
-
2112
- length: (x) => {
2113
- if (typeof x === "string" || Array.isArray(x)) {
2114
- return x.length;
2115
- }
2116
- throw new Error("length() expects string or array");
2117
- }
2118
- };
2119
-
2120
- function buildAST(tokens) {
2121
- let current = 0;
2122
-
2123
- const peek = () => tokens[current];
2124
- const consume = () => tokens[current++];
2125
-
2126
- const match = (type, value) => {
2127
- const t = peek();
2128
- if (!t) return false;
2129
-
2130
- if (t.type !== type) return false;
2131
-
2132
- if (value !== undefined && t.value !== value) return false;
2133
-
2134
- current++;
2135
- return true;
2136
- };
2137
-
2138
- const parseSliceOrIndex = () => {
2139
- let start = null;
2140
-
2141
- if (!(peek()?.type === "Colon" || peek()?.type === "Comma" || peek()?.type === "ArrayEnd")) {
2142
- start = parseExpression();
2143
- }
2144
-
2145
- if (match("Colon")) {
2146
- let end = null;
2147
-
2148
- if (!(peek()?.type === "Comma" || peek()?.type === "ArrayEnd")) {
2149
- end = parseExpression();
2150
- }
2151
-
2152
- return {
2153
- type: "SliceExpression",
2154
- start,
2155
- end
2156
- };
2157
- }
2158
-
2159
- return start;
2160
- };
2161
-
2162
- /* ================= PRIMARY ================= */
2163
- function parsePrimary() {
2164
- const token = consume();
2165
- if (!token) throw new Error("Unexpected end of input");
2166
-
2167
- switch (token.type) {
2168
- case "Number":
2169
- case "BigInt":
2170
- case "Boolean":
2171
- case "String":
2172
- return { type: "Literal", value: token.value };
2173
-
2174
- case "ImaginaryLiteral":
2175
- return { type: "ImaginaryLiteral", value: token.value };
2176
-
2177
- case "NumberWithUnit":
2178
- return {
2179
- type: "UnitLiteral",
2180
- value: token.value,
2181
- unit: token.unit
2182
- };
2183
-
2184
- case "Identifier":
2185
- return { type: "Identifier", name: token.name };
2186
-
2187
- case "Function":
2188
- return {
2189
- type: "Identifier",
2190
- name: token.name
2191
- };
2192
-
2193
- case "Parenthesis":
2194
- if (token.value === "(") {
2195
- const expr = parseExpression();
2196
-
2197
- if (!match("Parenthesis", ")")) {
2198
- throw new Error(`Expected ')'`);
2199
- }
2200
-
2201
- return expr;
2202
- }
2203
-
2204
- case "ArrayStart": {
2205
- const rows = [];
2206
- let currentRow = [];
2207
-
2208
- if (!match("ArrayEnd")) {
2209
- while (true) {
2210
- currentRow.push(parseExpression());
2211
-
2212
- if (match("Comma")) {
2213
- continue;
2214
- }
2215
-
2216
- if (match("Semicolon")) {
2217
- rows.push(currentRow);
2218
- currentRow = [];
2219
- continue;
2220
- }
2221
-
2222
- if (match("ArrayEnd")) {
2223
- rows.push(currentRow);
2224
- break;
2225
- }
2226
-
2227
- throw new Error(`Expected ',', ';', or ']' at ${current}`);
2228
- }
2229
- }
2230
-
2231
- if (!rows.length) {
2232
- return { type: "ArrayExpression", elements: [] };
2233
- }
2234
-
2235
- if (rows.length === 1) {
2236
- return { type: "ArrayExpression", elements: rows[0] };
2237
- }
2238
-
2239
- return {
2240
- type: "ArrayExpression",
2241
- elements: rows.map((elements) => ({
2242
- type: "ArrayExpression",
2243
- elements
2244
- }))
2245
- };
2246
- }
2247
-
2248
- case "BlockStart": {
2249
- const properties = [];
2250
-
2251
- if (!match("BlockEnd")) {
2252
- do {
2253
- const keyToken = consume();
2254
-
2255
- if (
2256
- keyToken.type !== "Identifier" &&
2257
- keyToken.type !== "String"
2258
- ) {
2259
- throw new Error("Invalid object key");
2260
- }
2261
-
2262
- if (!match("Colon")) {
2263
- throw new Error("Expected ':' after key");
2264
- }
2265
-
2266
- const value = parseExpression();
2267
-
2268
- properties.push({
2269
- key: keyToken.value,
2270
- value
2271
- });
2272
-
2273
- } while (match("Comma"));
2274
-
2275
- if (!match("BlockEnd")) {
2276
- throw new Error(`Expected '}' at ${current}`);
2277
- }
2278
- }
2279
-
2280
- return { type: "ObjectExpression", properties };
2281
- }
2282
- }
2283
-
2284
- throw new Error(`Unexpected token: ${JSON.stringify(token)}`);
2285
- }
2286
-
2287
- /* ================= MEMBER ================= */
2288
- function parseMember() {
2289
- let object = parsePrimary();
2290
-
2291
- while (true) {
2292
- if (match("ArrayStart")) {
2293
- const selectors = [];
2294
-
2295
- if (!match("ArrayEnd")) {
2296
- do {
2297
- selectors.push(parseSliceOrIndex());
2298
- } while (match("Comma"));
2299
-
2300
- if (!match("ArrayEnd")) {
2301
- throw new Error(`Expected ']' at ${current}`);
2302
- }
2303
- }
2304
-
2305
- object = {
2306
- type: "IndexExpression",
2307
- object,
2308
- selectors
2309
- };
2310
- continue;
2311
- }
2312
-
2313
- if (match("Dot")) {
2314
- const property = consume();
2315
-
2316
- if (property.type !== "Identifier") {
2317
- throw new Error("Expected property after '.'");
2318
- }
2319
-
2320
- object = {
2321
- type: "MemberExpression",
2322
- object,
2323
- property: { type: "Identifier", name: property.value },
2324
- optional: false
2325
- };
2326
- continue;
2327
- }
2328
-
2329
- if (match("Operator", "?.")) {
2330
- const property = consume();
2331
-
2332
- object = {
2333
- type: "MemberExpression",
2334
- object,
2335
- property: { type: "Identifier", name: property.value },
2336
- optional: true
2337
- };
2338
- continue;
2339
- }
2340
-
2341
- break;
2342
- }
2343
-
2344
- return object;
2345
- }
2346
-
2347
- /* ================= CALL ================= */
2348
- function parseCallChain() {
2349
- let expr = parseMember();
2350
-
2351
- while (peek()?.type === "Parenthesis" && peek()?.value === "(") {
2352
- consume(); // '('
2353
-
2354
- const args = [];
2355
-
2356
- if (!(peek()?.type === "Parenthesis" && peek()?.value === ")")) {
2357
- do {
2358
- args.push(parseExpression());
2359
- } while (match("Comma"));
2360
- }
2361
-
2362
- if (!match("Parenthesis", ")")) {
2363
- throw new Error(`Expected ')' at ${current}`);
2364
- }
2365
-
2366
- expr = {
2367
- type: "CallExpression",
2368
- callee: expr,
2369
- arguments: args
2370
- };
2371
- }
2372
-
2373
- return expr;
2374
- }
2375
-
2376
- /* ================= UNARY ================= */
2377
- function parseUnary() {
2378
- if (match("UnaryOperator")) {
2379
- const operator = tokens[current - 1].value;
2380
-
2381
- return {
2382
- type: "UnaryExpression",
2383
- operator,
2384
- argument: parseUnary()
2385
- };
2386
- }
2387
-
2388
- return parseCallChain();
2389
- }
2390
-
2391
- /* ================= POWER ================= */
2392
- function parsePower() {
2393
- let left = parseUnary();
2394
-
2395
- if (match("Operator", "^")) {
2396
- const right = parsePower();
2397
- return {
2398
- type: "BinaryExpression",
2399
- operator: "^",
2400
- left,
2401
- right
2402
- };
2403
- }
2404
-
2405
- return left;
2406
- }
2407
-
2408
- /* ================= MULT ================= */
2409
- function parseMultiplication() {
2410
- let left = parsePower();
2411
-
2412
- while (
2413
- match("Operator", "*") ||
2414
- match("Operator", "/") ||
2415
- match("Operator", "%")
2416
- ) {
2417
- const operator = tokens[current - 1].value;
2418
- const right = parsePower();
2419
-
2420
- left = {
2421
- type: "BinaryExpression",
2422
- operator,
2423
- left,
2424
- right
2425
- };
2426
- }
2427
-
2428
- return left;
2429
- }
2430
-
2431
- /* ================= ADD ================= */
2432
- function parseAddition() {
2433
- let left = parseMultiplication();
2434
-
2435
- while (match("Operator", "+") || match("Operator", "-")) {
2436
- const operator = tokens[current - 1].value;
2437
- const right = parseMultiplication();
2438
-
2439
- left = {
2440
- type: "BinaryExpression",
2441
- operator,
2442
- left,
2443
- right
2444
- };
2445
- }
2446
-
2447
- return left;
2448
- }
2449
-
2450
- /* ================= UNIT CONVERSION ================= */
2451
- function parseUnitConversion() {
2452
- let left = parseAddition();
2453
-
2454
- const nextKeyword = peek();
2455
- if (nextKeyword?.type === "Keyword" && ["to", "in"].includes(nextKeyword.value)) {
2456
- consume();
2457
- const next = consume();
2458
-
2459
- if (!next || next.type !== "Unit") {
2460
- throw new Error(`Expected unit after '${nextKeyword.value}'`);
2461
- }
2462
-
2463
- return {
2464
- type: "UnitConversion",
2465
- from: left,
2466
- to: next.value
2467
- };
2468
- }
2469
-
2470
- return left;
2471
- }
2472
-
2473
- /* ================= COMPARISON ================= */
2474
- function parseComparison() {
2475
- let left = parseUnitConversion();
2476
-
2477
- while (
2478
- match("Operator", ">") ||
2479
- match("Operator", "<") ||
2480
- match("Operator", ">=") ||
2481
- match("Operator", "<=") ||
2482
- match("Operator", "==")
2483
- ) {
2484
- const operator = tokens[current - 1].value;
2485
- const right = parseUnitConversion();
2486
-
2487
- left = {
2488
- type: "BinaryExpression",
2489
- operator,
2490
- left,
2491
- right
2492
- };
2493
- }
2494
-
2495
- return left;
2496
- }
2497
-
2498
- /* ================= LOGICAL ================= */
2499
- function parseLogical() {
2500
- let left = parseComparison();
2501
-
2502
- while (
2503
- match("Operator", "&&") ||
2504
- match("Operator", "||")
2505
- ) {
2506
- const operator = tokens[current - 1].value;
2507
- const right = parseComparison();
2508
-
2509
- left = {
2510
- type: "LogicalExpression",
2511
- operator,
2512
- left,
2513
- right
2514
- };
2515
- }
2516
-
2517
- return left;
2518
- }
2519
-
2520
- /* ================= NULLISH ================= */
2521
- function parseNullish() {
2522
- let left = parseLogical();
2523
-
2524
- while (match("Operator", "??")) {
2525
- const right = parseLogical();
2526
-
2527
- left = {
2528
- type: "LogicalExpression",
2529
- operator: "??",
2530
- left,
2531
- right
2532
- };
2533
- }
2534
-
2535
- return left;
2536
- }
2537
-
2538
- /* ================= TERNARY ================= */
2539
- function parseTernary() {
2540
- let test = parseNullish();
2541
-
2542
- if (match("Ternary", "?")) {
2543
- const consequent = parseExpression();
2544
-
2545
- if (!match("Ternary", ":")) {
2546
- throw new Error("Expected ':' in ternary");
2547
- }
2548
-
2549
- const alternate = parseExpression();
2550
-
2551
- return {
2552
- type: "ConditionalExpression",
2553
- test,
2554
- consequent,
2555
- alternate
2556
- };
2557
- }
2558
-
2559
- return test;
2560
- }
2561
-
2562
- /* ================= PIPELINE ================= */
2563
- function parsePipeline() {
2564
- let left = parseTernary();
2565
-
2566
- while (match("Operator", "|>")) {
2567
- const right = parseTernary();
2568
-
2569
- left = {
2570
- type: "PipelineExpression",
2571
- left,
2572
- right
2573
- };
2574
- }
2575
-
2576
- return left;
2577
- }
2578
-
2579
- /* ================= ASSIGNMENT ================= */
2580
- function parseAssignment() {
2581
- let left = parsePipeline();
2582
-
2583
- if (
2584
- match("Operator", "=") ||
2585
- match("Operator", "+=") ||
2586
- match("Operator", "-=") ||
2587
- match("Operator", "*=") ||
2588
- match("Operator", "/=")
2589
- ) {
2590
- const operator = tokens[current - 1].value;
2591
-
2592
- if (left.type === "CallExpression") {
2593
- const isFunctionTarget =
2594
- left.callee?.type === "Identifier" &&
2595
- left.arguments.every((arg) => arg.type === "Identifier");
2596
-
2597
- if (!isFunctionTarget) {
2598
- throw new Error("Invalid function definition");
2599
- }
2600
-
2601
- const right = parseAssignment();
2602
-
2603
- return {
2604
- type: "FunctionAssignmentExpression",
2605
- operator,
2606
- left: {
2607
- type: "Identifier",
2608
- name: left.callee.name
2609
- },
2610
- params: left.arguments.map((arg) => arg.name),
2611
- right
2612
- };
2613
- }
2614
-
2615
- if (
2616
- left.type !== "Identifier" &&
2617
- left.type !== "MemberExpression" &&
2618
- left.type !== "IndexExpression"
2619
- ) {
2620
- throw new Error("Invalid assignment target");
2621
- }
2622
-
2623
- const right = parseAssignment();
2624
-
2625
- return {
2626
- type: "AssignmentExpression",
2627
- operator,
2628
- left,
2629
- right
2630
- };
2631
- }
2632
-
2633
- return left;
2634
- }
2635
-
2636
- /* ================= ENTRY ================= */
2637
- function parseExpression() {
2638
- return parseAssignment();
2639
- }
2640
-
2641
- const ast = parseExpression();
2642
-
2643
- if (current < tokens.length) {
2644
- throw new Error(
2645
- `Unexpected token at end: ${JSON.stringify(peek())}`
2646
- );
2647
- }
2648
-
2649
- return ast;
2650
- }
2651
-
2652
- //
2653
-
2654
- const isComplex = (value) =>
2655
- value && typeof value === "object" && "re" in value && "im" in value;
2656
-
2657
- const isUnitValue = (value) =>
2658
- value && typeof value === "object" && "value" in value && "unit" in value;
2659
-
2660
- const isMatrix = (value) =>
2661
- Array.isArray(value) && value.length > 0 && value.every(Array.isArray);
2662
-
2663
- const formatComplex = (value) => {
2664
- if (!isComplex(value)) return value;
2665
-
2666
- const real = value.re;
2667
- const imaginary = Math.abs(value.im);
2668
- const sign = value.im < 0 ? "-" : "+";
2669
-
2670
- if (real === 0) {
2671
- if (value.im === 1) return "i";
2672
- if (value.im === -1) return "-i";
2673
- return `${value.im}i`;
2674
- }
2675
-
2676
- const imagPart = imaginary === 1 ? "i" : `${imaginary}i`;
2677
- return `${real} ${sign} ${imagPart}`;
2678
- };
2679
-
2680
- const formatScalar = (value) => {
2681
- if (typeof value !== "number") {
2682
- return String(value);
2683
- }
2684
-
2685
- if (Number.isInteger(value)) {
2686
- return String(value);
2687
- }
2688
-
2689
- return Number(value.toFixed(14)).toString();
2690
- };
2691
-
2692
- const formatResult = (value) => {
2693
- if (isComplex(value)) {
2694
- return formatComplex(value);
2695
- }
2696
-
2697
- if (isUnitValue(value)) {
2698
- return `${value.value} ${value.unit}`;
2699
- }
2700
-
2701
- if (isDenseMatrixWrapper(value)) {
2702
- return serializeExprifyValue(value);
2703
- }
2704
-
2705
- if (isMatrix(value)) {
2706
- return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
2707
- }
2708
-
2709
- if (Array.isArray(value)) {
2710
- return JSON.stringify(value);
2711
- }
2712
-
2713
- if (value && typeof value === "object") {
2714
- return serializeExprifyValue(value);
2715
- }
2716
-
2717
- return value;
2718
- };
2719
-
2720
- class exprify {
2721
- constructor() {
2722
- // Shared state
2723
- this.math = mathOperations;
2724
- this.units = createUnitsStore(globalUnits);
2725
- this.functions = createFunctionRegistry(internalFunctions);
2726
- this.variables = createVarStore();
2727
- this._cache = new Map();
2728
- this.variables.set("pi", Math.PI);
2729
- this.variables.set("e", Math.E);
2730
- this.addFunction("parse", (expression) => {
2731
- if (typeof expression !== "string") {
2732
- throw new Error("parse() expects an expression string");
2733
- }
2734
- return expression;
2735
- });
2736
- this.addFunction("leafCount", (value) => {
2737
- const countLeafTokens = (expression) => {
2738
- const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
2739
- const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
2740
- return matches ? matches.length : 0;
2741
- };
2742
-
2743
- let ast = value;
2744
- if (typeof value === "string") {
2745
- try {
2746
- ast = this.parse(value).ast;
2747
- } catch {
2748
- return countLeafTokens(value);
2749
- }
2750
- }
2751
-
2752
- const countLeaves = (node) => {
2753
- if (!node || typeof node !== "object") return 0;
2754
-
2755
- switch (node.type) {
2756
- case "Literal":
2757
- case "ImaginaryLiteral":
2758
- case "UnitLiteral":
2759
- case "Identifier":
2760
- return 1;
2761
- default:
2762
- return Object.values(node).reduce((sum, child) => {
2763
- if (Array.isArray(child)) {
2764
- return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
2765
- }
2766
-
2767
- return sum + countLeaves(child);
2768
- }, 0);
2769
- }
2770
- };
2771
-
2772
- return countLeaves(ast);
2773
- });
2774
- this.addFunction("matrix", (value) => wrapDenseMatrix(value));
2775
- this.addFunction("sparse", (value) => wrapDenseMatrix(value));
2776
- this.addFunction("rationalize", (expression, withDetails = false) => {
2777
- if (typeof expression !== "string") {
2778
- throw new Error("rationalize() expects an expression string");
2779
- }
2780
-
2781
- const normalizedExpression = expression
2782
- .replace(/\s+/g, "")
2783
- .replace(/(\d)([a-zA-Z(])/g, "$1*$2")
2784
- .replace(/([a-zA-Z)])(\d)/g, "$1*$2");
2785
-
2786
- const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
2787
- const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
2788
- const constPoly = (value) => new Map([[polyKey({}), value]]);
2789
- const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
2790
- const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
2791
- const addPoly = (a, b, sign = 1) => {
2792
- const result = new Map(a);
2793
- for (const [key, coeff] of b.entries()) {
2794
- result.set(key, (result.get(key) || 0) + (sign * coeff));
2795
- }
2796
- return cleanPoly(result);
2797
- };
2798
- const multiplyPoly = (a, b) => {
2799
- const result = new Map();
2800
- for (const [keyA, coeffA] of a.entries()) {
2801
- const powersA = keyToPowers(keyA);
2802
- for (const [keyB, coeffB] of b.entries()) {
2803
- const powersB = keyToPowers(keyB);
2804
- const merged = { ...powersA };
2805
- for (const [name, power] of Object.entries(powersB)) {
2806
- merged[name] = (merged[name] || 0) + power;
2807
- }
2808
- const key = polyKey(merged);
2809
- result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
2810
- }
2811
- }
2812
- return cleanPoly(result);
2813
- };
2814
- const powPoly = (poly, exponent) => {
2815
- let result = constPoly(1);
2816
- for (let index = 0; index < exponent; index++) {
2817
- result = multiplyPoly(result, poly);
2818
- }
2819
- return result;
2820
- };
2821
- const rational = (num, den = constPoly(1)) => ({ num, den });
2822
- const addRat = (a, b, sign = 1) => rational(
2823
- addPoly(
2824
- multiplyPoly(a.num, b.den),
2825
- multiplyPoly(b.num, a.den),
2826
- sign
2827
- ),
2828
- multiplyPoly(a.den, b.den)
2829
- );
2830
- const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
2831
- const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
2832
- const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
2833
- const astToRat = (node) => {
2834
- switch (node.type) {
2835
- case "Literal":
2836
- return rational(constPoly(node.value));
2837
- case "Identifier":
2838
- return rational(varPoly(node.name));
2839
- case "UnaryExpression":
2840
- if (node.operator === "-") return negRat(astToRat(node.argument));
2841
- throw new Error("Unsupported unary operator");
2842
- case "BinaryExpression": {
2843
- const left = astToRat(node.left);
2844
- const right = astToRat(node.right);
2845
- switch (node.operator) {
2846
- case "+": return addRat(left, right);
2847
- case "-": return addRat(left, right, -1);
2848
- case "*": return mulRat(left, right);
2849
- case "/": return divRat(left, right);
2850
- case "^": {
2851
- if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
2852
- throw new Error("Unsupported exponent");
2853
- }
2854
- return rational(
2855
- powPoly(left.num, node.right.value),
2856
- powPoly(left.den, node.right.value)
2857
- );
2858
- }
2859
- default:
2860
- throw new Error("Unsupported operator in rationalize()");
2861
- }
2862
- }
2863
- default:
2864
- throw new Error("Unsupported expression in rationalize()");
2865
- }
2866
- };
2867
- const formatPoly = (poly) => {
2868
- const entries = [...poly.entries()]
2869
- .filter(([, coeff]) => coeff !== 0)
2870
- .sort(([keyA], [keyB]) => {
2871
- const powersA = keyToPowers(keyA);
2872
- const powersB = keyToPowers(keyB);
2873
- const firstVarA = Object.keys(powersA).sort()[0] || "";
2874
- const firstVarB = Object.keys(powersB).sort()[0] || "";
2875
-
2876
- if (firstVarA !== firstVarB) {
2877
- return firstVarA.localeCompare(firstVarB);
2878
- }
2879
-
2880
- const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
2881
- const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
2882
- return degreeB - degreeA;
2883
- });
2884
-
2885
- if (!entries.length) return "0";
2886
-
2887
- return entries.map(([key, coeff], index) => {
2888
- const powers = keyToPowers(key);
2889
- const absCoeff = Math.abs(coeff);
2890
- const variablePart = Object.entries(powers)
2891
- .map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
2892
- .join(" * ");
2893
- let body = variablePart;
2894
-
2895
- if (!body) {
2896
- body = `${absCoeff}`;
2897
- } else if (absCoeff !== 1) {
2898
- body = `${absCoeff} * ${body}`;
2899
- }
2900
-
2901
- if (index === 0) {
2902
- return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
2903
- }
2904
-
2905
- return coeff < 0 ? `- ${body}` : `+ ${body}`;
2906
- }).join(" ");
2907
- };
2908
-
2909
- const ast = this.parse(normalizedExpression).ast;
2910
- const result = astToRat(ast);
2911
- const numerator = formatPoly(result.num);
2912
- const denominator = formatPoly(result.den);
2913
- const variableSet = new Set();
2914
-
2915
- for (const poly of [result.num, result.den]) {
2916
- for (const key of poly.keys()) {
2917
- for (const name of Object.keys(keyToPowers(key))) {
2918
- variableSet.add(name);
2919
- }
2920
- }
2921
- }
2922
-
2923
- if (!withDetails) {
2924
- return `(${numerator}) / (${denominator})`;
2925
- }
2926
-
2927
- return {
2928
- numerator,
2929
- denominator,
2930
- coefficients: [],
2931
- variables: [...variableSet].sort(),
2932
- expression: `(${numerator}) / (${denominator})`
2933
- };
2934
- });
2935
- }
2936
-
2937
- setVariable(name, value) {
2938
- this.variables.set(name, value);
2939
- }
2940
-
2941
- getVariable(name) {
2942
- return this.variables.get(name);
2943
- }
2944
-
2945
- addFunction(name, fn) {
2946
- this.functions.register(name, fn);
2947
- }
2948
-
2949
- _createContext() {
2950
- return createContext({
2951
- functions: this.functions,
2952
- variables: this.variables,
2953
- units: this.units,
2954
- evaluate: this.evaluate.bind(this)
2955
- });
2956
- }
2957
-
2958
- tokenize(expr) {
2959
- if (typeof expr !== "string") {
2960
- throw new Error("Expression must be a string");
2961
- }
2962
- return tokenize(expr, this._createContext());
2963
- }
2964
-
2965
- parse(expr) {
2966
- const tokens = this.tokenize(expr);
2967
- const ast = buildAST(tokens);
2968
- return { tokens, ast };
2969
- }
2970
-
2971
- evaluate(expr) {
2972
- const { ast } = this.parse(expr);
2973
- return formatResult(evaluateAST(
2974
- ast,
2975
- this._createContext()
2976
- ));
2977
- }
2978
-
2979
- compile(expr) {
2980
- if (this._cache.has(expr)) {
2981
- return this._cache.get(expr);
2982
- }
2983
-
2984
- const { ast } = this.parse(expr);
2985
-
2986
- const compiledFn = (scope = {}) => {
2987
- const baseContext = this._createContext();
2988
- const scopedContext = baseContext.withScope(scope);
2989
- return formatResult(evaluateAST(ast, scopedContext));
2990
- };
2991
-
2992
- this._cache.set(expr, compiledFn);
2993
- return compiledFn;
2994
- }
2995
-
2996
- clearCache() {
2997
- this._cache.clear();
2998
- }
2999
-
3000
- }
3001
-
3002
- module.exports = exprify;
3003
-