exprify 1.0.4 → 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.
- package/HISTORY.md +49 -0
- package/README.md +100 -180
- package/SECURITY.md +18 -0
- package/bin/cli.mjs +234 -0
- package/dist/exprify.cjs.cjs +3558 -1220
- package/dist/exprify.cjs.cjs.map +1 -1
- package/dist/exprify.esm.js +3558 -1220
- package/dist/exprify.esm.js.map +1 -1
- package/dist/exprify.js +3560 -1222
- package/dist/exprify.js.map +1 -1
- package/dist/exprify.min.js +2 -2
- package/dist/exprify.min.js.map +1 -1
- package/package.json +44 -17
- package/src/core/context.js +35 -27
- package/src/core/exprify.js +880 -0
- package/src/function/executor.js +29 -20
- package/src/function/internal.js +1150 -153
- package/src/function/registry.js +23 -16
- package/src/index.js +1 -1
- package/src/math/bignumber.js +31 -0
- package/src/math/fraction.js +112 -0
- package/src/math/operations.js +38 -24
- package/src/parser/astBuild.js +276 -214
- package/src/parser/evaluator.js +431 -171
- package/src/parser/tokenizer.js +179 -146
- package/src/utils/decimal.js +264 -0
- package/src/utils/globalUnits.js +43 -35
- package/src/utils/matrix.js +14 -14
- package/src/utils/store.js +69 -47
- package/src/variables/store.js +18 -15
- package/src/core/Exprify.js +0 -369
package/src/utils/store.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export function createUnitsStore(initial = {}) {
|
|
2
|
-
let units = { ...initial};
|
|
3
|
-
|
|
4
|
-
// ---------- Helpers ----------
|
|
2
|
+
let units = { ...initial };
|
|
5
3
|
|
|
4
|
+
// Helpers
|
|
6
5
|
function getAllUnitsFlat() {
|
|
7
6
|
const result = new Set();
|
|
8
7
|
|
|
@@ -19,7 +18,6 @@ export function createUnitsStore(initial = {}) {
|
|
|
19
18
|
|
|
20
19
|
// Avoid duplicate like "m" vs "meter"
|
|
21
20
|
if (unitLower !== keyLower) {
|
|
22
|
-
// Optional: only single-word units
|
|
23
21
|
if (unitLower.split(/\s+/).length === 1) {
|
|
24
22
|
result.add(unitLower);
|
|
25
23
|
}
|
|
@@ -41,6 +39,9 @@ export function createUnitsStore(initial = {}) {
|
|
|
41
39
|
return Array.from(result);
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} input
|
|
44
|
+
*/
|
|
44
45
|
function findUnit(input) {
|
|
45
46
|
input = input.toLowerCase();
|
|
46
47
|
|
|
@@ -53,7 +54,7 @@ export function createUnitsStore(initial = {}) {
|
|
|
53
54
|
u.unit?.toLowerCase() === input ||
|
|
54
55
|
u.symbol?.toLowerCase() === input
|
|
55
56
|
) {
|
|
56
|
-
return { type, key
|
|
57
|
+
return { type, key, data: u };
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -61,68 +62,89 @@ export function createUnitsStore(initial = {}) {
|
|
|
61
62
|
return null;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* @param {number} value
|
|
67
|
+
* @param {any} fromUnit
|
|
68
|
+
* @param {any} toUnit
|
|
69
|
+
*/
|
|
66
70
|
function convert(value, fromUnit, toUnit) {
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const from = findUnit(fromUnit);
|
|
72
|
+
const to = findUnit(toUnit);
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const result = value * (from.data.value / to.data.value);
|
|
74
|
+
if (!from) {
|
|
75
|
+
throw new Error(`Unknown unit: ${fromUnit}`);
|
|
76
|
+
}
|
|
77
|
+
if (!to) {
|
|
78
|
+
throw new Error(`Unknown unit: ${toUnit}`);
|
|
79
|
+
}
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
if (from.type !== to.type) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Cannot convert ${fromUnit} to ${toUnit} (${to.data.unit || to.key}). ${from.data.unit || from.key} conversion units like ${Object.keys(units[from.type]).join(', ')}`
|
|
84
|
+
);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
const result = value * (from.data.value / to.data.value);
|
|
83
88
|
|
|
89
|
+
return { value: result, unit: to.key };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Public API
|
|
84
93
|
return {
|
|
85
94
|
// Get all units
|
|
86
95
|
getUnits: () => units,
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
setUnits: (newUnits) => {
|
|
97
|
+
setUnits: (/** @type {{}} */ newUnits) => {
|
|
90
98
|
units = { ...newUnits };
|
|
91
99
|
},
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
updateType: (type, data) => {
|
|
101
|
+
updateType: (/** @type {string | number} */ type, /** @type {any} */ data) => {
|
|
95
102
|
units[type] = { ...units[type], ...data };
|
|
96
103
|
},
|
|
97
104
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
addUnit: (
|
|
106
|
+
/** @type {string | number} */ type,
|
|
107
|
+
/** @type {string | number} */ key,
|
|
108
|
+
/** @type {any} */ unitObj
|
|
109
|
+
) => {
|
|
110
|
+
if (!units[type]) {
|
|
111
|
+
units[type] = {};
|
|
112
|
+
}
|
|
101
113
|
units[type][key] = unitObj;
|
|
102
114
|
},
|
|
115
|
+
// Unit-aware arithmetic: unify operands to same unit type, then apply operator
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} op
|
|
118
|
+
* @param {{ unit: any; value: any; }} left
|
|
119
|
+
* @param {{ unit: any; value: number; }} right
|
|
120
|
+
*/
|
|
103
121
|
compute(op, left, right) {
|
|
122
|
+
const isUnit = (/** @type {any} */ v) =>
|
|
123
|
+
v && typeof v === 'object' && 'value' in v && 'unit' in v;
|
|
104
124
|
|
|
105
|
-
const
|
|
106
|
-
v && typeof v === "object" && "value" in v && "unit" in v;
|
|
107
|
-
|
|
108
|
-
const apply = (a, b) => {
|
|
125
|
+
const apply = (/** @type {any} */ a, /** @type {any} */ b) => {
|
|
109
126
|
switch (op) {
|
|
110
|
-
case
|
|
111
|
-
|
|
112
|
-
case
|
|
113
|
-
|
|
114
|
-
case
|
|
115
|
-
|
|
127
|
+
case '+':
|
|
128
|
+
return a + b;
|
|
129
|
+
case '-':
|
|
130
|
+
return a - b;
|
|
131
|
+
case '*':
|
|
132
|
+
return a * b;
|
|
133
|
+
case '/':
|
|
134
|
+
return a / b;
|
|
135
|
+
case '%':
|
|
136
|
+
return a % b;
|
|
137
|
+
case '^':
|
|
138
|
+
return Math.pow(a, b);
|
|
116
139
|
}
|
|
117
140
|
};
|
|
118
141
|
|
|
119
142
|
// BOTH UNIT
|
|
120
143
|
if (isUnit(left) && isUnit(right)) {
|
|
121
|
-
|
|
122
144
|
const from = this.findUnit(right.unit);
|
|
123
145
|
const to = this.findUnit(left.unit);
|
|
124
146
|
|
|
125
|
-
if (from.type !== to.type) {
|
|
147
|
+
if (!from || !to || from.type !== to.type) {
|
|
126
148
|
throw new Error(`Cannot operate on different unit types`);
|
|
127
149
|
}
|
|
128
150
|
|
|
@@ -132,47 +154,47 @@ export function createUnitsStore(initial = {}) {
|
|
|
132
154
|
const result = apply(left.value, r);
|
|
133
155
|
|
|
134
156
|
// multiplication/division produce compound units
|
|
135
|
-
if (op ===
|
|
157
|
+
if (op === '*') {
|
|
136
158
|
return { value: result, unit: left.unit };
|
|
137
159
|
}
|
|
138
160
|
|
|
139
|
-
if (op ===
|
|
161
|
+
if (op === '/') {
|
|
140
162
|
return { value: result, unit: left.unit };
|
|
141
163
|
}
|
|
142
164
|
|
|
143
|
-
if (op ===
|
|
165
|
+
if (op === '^') {
|
|
144
166
|
return { value: result, unit: left.unit };
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
return { value: result, unit: left.unit };
|
|
148
170
|
}
|
|
149
171
|
|
|
150
|
-
//
|
|
172
|
+
// LEFT UNIT
|
|
151
173
|
if (isUnit(left) && !isUnit(right)) {
|
|
152
174
|
const result = apply(left.value, right);
|
|
153
175
|
|
|
154
176
|
return { value: result, unit: left.unit };
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
//
|
|
179
|
+
// RIGHT UNIT
|
|
158
180
|
if (!isUnit(left) && isUnit(right)) {
|
|
159
181
|
const result = apply(left, right.value);
|
|
160
182
|
|
|
161
|
-
if (op ===
|
|
183
|
+
if (op === '/') {
|
|
162
184
|
return { value: result, unit: right.unit };
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
return { value: result, unit: right.unit };
|
|
166
188
|
}
|
|
167
189
|
|
|
168
|
-
//
|
|
190
|
+
// NORMAL
|
|
169
191
|
return apply(left, right);
|
|
170
192
|
},
|
|
171
|
-
|
|
193
|
+
|
|
172
194
|
convert,
|
|
173
195
|
|
|
174
196
|
// Search helpers
|
|
175
197
|
getAllUnitsFlat,
|
|
176
|
-
findUnit
|
|
198
|
+
findUnit,
|
|
177
199
|
};
|
|
178
200
|
}
|
package/src/variables/store.js
CHANGED
|
@@ -2,18 +2,19 @@ const validVarName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
|
2
2
|
|
|
3
3
|
export function createVarStore(initial = {}) {
|
|
4
4
|
let store = Object.create(null);
|
|
5
|
-
|
|
6
5
|
|
|
7
6
|
for (const key in initial) {
|
|
8
7
|
store[key] = initial[key];
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
return {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} name
|
|
13
|
+
* @param {number | undefined} value
|
|
14
|
+
*/
|
|
12
15
|
set(name, value, { override = true } = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (typeof name !== "string" || !name) {
|
|
16
|
-
throw new Error("Variable name must be a non-empty string");
|
|
16
|
+
if (typeof name !== 'string' || !name) {
|
|
17
|
+
throw new Error('Variable name must be a non-empty string');
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
if (!validVarName.test(name)) {
|
|
@@ -26,50 +27,52 @@ export function createVarStore(initial = {}) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// Prevent overwrite (optional)
|
|
29
|
-
if (!override && name in
|
|
30
|
+
if (!override && name in store) {
|
|
30
31
|
throw new Error(`Variable '${name}' already exists`);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
store[name] = value;
|
|
34
35
|
},
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* @param {string | number} name
|
|
39
|
+
*/
|
|
37
40
|
get(name) {
|
|
38
41
|
return store[name];
|
|
39
42
|
},
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
/**
|
|
45
|
+
* @param {any} name
|
|
46
|
+
*/
|
|
42
47
|
has(name) {
|
|
43
48
|
return Object.prototype.hasOwnProperty.call(store, name);
|
|
44
49
|
},
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @param {string | number} name
|
|
53
|
+
*/
|
|
47
54
|
remove(name) {
|
|
48
55
|
delete store[name];
|
|
49
56
|
},
|
|
50
57
|
|
|
51
|
-
// get all variables (snapshot)
|
|
52
58
|
all() {
|
|
53
59
|
return { ...store };
|
|
54
60
|
},
|
|
55
61
|
|
|
56
|
-
// clear all
|
|
57
62
|
clear() {
|
|
58
63
|
store = Object.create(null);
|
|
59
64
|
},
|
|
60
65
|
|
|
61
|
-
// merge multiple variables
|
|
62
66
|
merge(obj = {}) {
|
|
63
67
|
for (const key in obj) {
|
|
64
68
|
store[key] = obj[key];
|
|
65
69
|
}
|
|
66
70
|
},
|
|
67
71
|
|
|
68
|
-
// clone store (for scoped instances)
|
|
69
72
|
clone() {
|
|
70
73
|
return createVarStore(store);
|
|
71
|
-
}
|
|
74
|
+
},
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
export default { createVarStore };
|
|
78
|
+
export default { createVarStore };
|
package/src/core/Exprify.js
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { tokenize } from "../parser/tokenizer.js";
|
|
2
|
-
// import { infixToPostfix } from "../parser/infixToPostfix.js";
|
|
3
|
-
import { evaluateAST } from "../parser/evaluator.js";
|
|
4
|
-
import { createContext } from "./context.js";
|
|
5
|
-
import { mathOperations } from "../math/operations.js";
|
|
6
|
-
|
|
7
|
-
import { createUnitsStore } from "../utils/store.js";
|
|
8
|
-
import { globalUnits } from "../utils/globalUnits.js";
|
|
9
|
-
|
|
10
|
-
import { createVarStore } from "../variables/store.js";
|
|
11
|
-
import { createFunctionRegistry } from "../function/registry.js";
|
|
12
|
-
import { internalFunctions } from "../function/internal.js";
|
|
13
|
-
import { isDenseMatrixWrapper, serializeExprifyValue, wrapDenseMatrix } from "../utils/matrix.js";
|
|
14
|
-
|
|
15
|
-
import { buildAST } from "../parser/astBuild.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
const isComplex = (value) =>
|
|
21
|
-
value && typeof value === "object" && "re" in value && "im" in value;
|
|
22
|
-
|
|
23
|
-
const isUnitValue = (value) =>
|
|
24
|
-
value && typeof value === "object" && "value" in value && "unit" in value;
|
|
25
|
-
|
|
26
|
-
const isMatrix = (value) =>
|
|
27
|
-
Array.isArray(value) && value.length > 0 && value.every(Array.isArray);
|
|
28
|
-
|
|
29
|
-
const formatComplex = (value) => {
|
|
30
|
-
if (!isComplex(value)) return value;
|
|
31
|
-
|
|
32
|
-
const real = value.re;
|
|
33
|
-
const imaginary = Math.abs(value.im);
|
|
34
|
-
const sign = value.im < 0 ? "-" : "+";
|
|
35
|
-
|
|
36
|
-
if (real === 0) {
|
|
37
|
-
if (value.im === 1) return "i";
|
|
38
|
-
if (value.im === -1) return "-i";
|
|
39
|
-
return `${value.im}i`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const imagPart = imaginary === 1 ? "i" : `${imaginary}i`;
|
|
43
|
-
return `${real} ${sign} ${imagPart}`;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const formatScalar = (value) => {
|
|
47
|
-
if (typeof value !== "number") {
|
|
48
|
-
return String(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (Number.isInteger(value)) {
|
|
52
|
-
return String(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return Number(value.toFixed(14)).toString();
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const formatResult = (value) => {
|
|
59
|
-
if (isComplex(value)) {
|
|
60
|
-
return formatComplex(value);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (isUnitValue(value)) {
|
|
64
|
-
return `${value.value} ${value.unit}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (isDenseMatrixWrapper(value)) {
|
|
68
|
-
return serializeExprifyValue(value);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (isMatrix(value)) {
|
|
72
|
-
return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (Array.isArray(value)) {
|
|
76
|
-
return JSON.stringify(value);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (value && typeof value === "object") {
|
|
80
|
-
return serializeExprifyValue(value);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return value;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
class exprify {
|
|
87
|
-
constructor() {
|
|
88
|
-
// Shared state
|
|
89
|
-
this.math = mathOperations;
|
|
90
|
-
this.units = createUnitsStore(globalUnits);
|
|
91
|
-
this.functions = createFunctionRegistry(internalFunctions);
|
|
92
|
-
this.variables = createVarStore();
|
|
93
|
-
this._cache = new Map();
|
|
94
|
-
this.variables.set("pi", Math.PI);
|
|
95
|
-
this.variables.set("e", Math.E);
|
|
96
|
-
this.addFunction("parse", (expression) => {
|
|
97
|
-
if (typeof expression !== "string") {
|
|
98
|
-
throw new Error("parse() expects an expression string");
|
|
99
|
-
}
|
|
100
|
-
return expression;
|
|
101
|
-
});
|
|
102
|
-
this.addFunction("leafCount", (value) => {
|
|
103
|
-
const countLeafTokens = (expression) => {
|
|
104
|
-
const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
|
|
105
|
-
const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
|
|
106
|
-
return matches ? matches.length : 0;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
let ast = value;
|
|
110
|
-
if (typeof value === "string") {
|
|
111
|
-
try {
|
|
112
|
-
ast = this.parse(value).ast;
|
|
113
|
-
} catch {
|
|
114
|
-
return countLeafTokens(value);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const countLeaves = (node) => {
|
|
119
|
-
if (!node || typeof node !== "object") return 0;
|
|
120
|
-
|
|
121
|
-
switch (node.type) {
|
|
122
|
-
case "Literal":
|
|
123
|
-
case "ImaginaryLiteral":
|
|
124
|
-
case "UnitLiteral":
|
|
125
|
-
case "Identifier":
|
|
126
|
-
return 1;
|
|
127
|
-
default:
|
|
128
|
-
return Object.values(node).reduce((sum, child) => {
|
|
129
|
-
if (Array.isArray(child)) {
|
|
130
|
-
return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return sum + countLeaves(child);
|
|
134
|
-
}, 0);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return countLeaves(ast);
|
|
139
|
-
});
|
|
140
|
-
this.addFunction("matrix", (value) => wrapDenseMatrix(value));
|
|
141
|
-
this.addFunction("sparse", (value) => wrapDenseMatrix(value));
|
|
142
|
-
this.addFunction("rationalize", (expression, withDetails = false) => {
|
|
143
|
-
if (typeof expression !== "string") {
|
|
144
|
-
throw new Error("rationalize() expects an expression string");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const normalizedExpression = expression
|
|
148
|
-
.replace(/\s+/g, "")
|
|
149
|
-
.replace(/(\d)([a-zA-Z(])/g, "$1*$2")
|
|
150
|
-
.replace(/([a-zA-Z)])(\d)/g, "$1*$2");
|
|
151
|
-
|
|
152
|
-
const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
|
|
153
|
-
const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
|
|
154
|
-
const makePoly = (terms = new Map()) => terms;
|
|
155
|
-
const constPoly = (value) => new Map([[polyKey({}), value]]);
|
|
156
|
-
const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
|
|
157
|
-
const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
|
|
158
|
-
const addPoly = (a, b, sign = 1) => {
|
|
159
|
-
const result = new Map(a);
|
|
160
|
-
for (const [key, coeff] of b.entries()) {
|
|
161
|
-
result.set(key, (result.get(key) || 0) + (sign * coeff));
|
|
162
|
-
}
|
|
163
|
-
return cleanPoly(result);
|
|
164
|
-
};
|
|
165
|
-
const multiplyPoly = (a, b) => {
|
|
166
|
-
const result = new Map();
|
|
167
|
-
for (const [keyA, coeffA] of a.entries()) {
|
|
168
|
-
const powersA = keyToPowers(keyA);
|
|
169
|
-
for (const [keyB, coeffB] of b.entries()) {
|
|
170
|
-
const powersB = keyToPowers(keyB);
|
|
171
|
-
const merged = { ...powersA };
|
|
172
|
-
for (const [name, power] of Object.entries(powersB)) {
|
|
173
|
-
merged[name] = (merged[name] || 0) + power;
|
|
174
|
-
}
|
|
175
|
-
const key = polyKey(merged);
|
|
176
|
-
result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return cleanPoly(result);
|
|
180
|
-
};
|
|
181
|
-
const powPoly = (poly, exponent) => {
|
|
182
|
-
let result = constPoly(1);
|
|
183
|
-
for (let index = 0; index < exponent; index++) {
|
|
184
|
-
result = multiplyPoly(result, poly);
|
|
185
|
-
}
|
|
186
|
-
return result;
|
|
187
|
-
};
|
|
188
|
-
const rational = (num, den = constPoly(1)) => ({ num, den });
|
|
189
|
-
const addRat = (a, b, sign = 1) => rational(
|
|
190
|
-
addPoly(
|
|
191
|
-
multiplyPoly(a.num, b.den),
|
|
192
|
-
multiplyPoly(b.num, a.den),
|
|
193
|
-
sign
|
|
194
|
-
),
|
|
195
|
-
multiplyPoly(a.den, b.den)
|
|
196
|
-
);
|
|
197
|
-
const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
|
|
198
|
-
const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
|
|
199
|
-
const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
|
|
200
|
-
const astToRat = (node) => {
|
|
201
|
-
switch (node.type) {
|
|
202
|
-
case "Literal":
|
|
203
|
-
return rational(constPoly(node.value));
|
|
204
|
-
case "Identifier":
|
|
205
|
-
return rational(varPoly(node.name));
|
|
206
|
-
case "UnaryExpression":
|
|
207
|
-
if (node.operator === "-") return negRat(astToRat(node.argument));
|
|
208
|
-
throw new Error("Unsupported unary operator");
|
|
209
|
-
case "BinaryExpression": {
|
|
210
|
-
const left = astToRat(node.left);
|
|
211
|
-
const right = astToRat(node.right);
|
|
212
|
-
switch (node.operator) {
|
|
213
|
-
case "+": return addRat(left, right);
|
|
214
|
-
case "-": return addRat(left, right, -1);
|
|
215
|
-
case "*": return mulRat(left, right);
|
|
216
|
-
case "/": return divRat(left, right);
|
|
217
|
-
case "^": {
|
|
218
|
-
if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
|
|
219
|
-
throw new Error("Unsupported exponent");
|
|
220
|
-
}
|
|
221
|
-
return rational(
|
|
222
|
-
powPoly(left.num, node.right.value),
|
|
223
|
-
powPoly(left.den, node.right.value)
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
default:
|
|
227
|
-
throw new Error("Unsupported operator in rationalize()");
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
default:
|
|
231
|
-
throw new Error("Unsupported expression in rationalize()");
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
const formatPoly = (poly) => {
|
|
235
|
-
const entries = [...poly.entries()]
|
|
236
|
-
.filter(([, coeff]) => coeff !== 0)
|
|
237
|
-
.sort(([keyA], [keyB]) => {
|
|
238
|
-
const powersA = keyToPowers(keyA);
|
|
239
|
-
const powersB = keyToPowers(keyB);
|
|
240
|
-
const firstVarA = Object.keys(powersA).sort()[0] || "";
|
|
241
|
-
const firstVarB = Object.keys(powersB).sort()[0] || "";
|
|
242
|
-
|
|
243
|
-
if (firstVarA !== firstVarB) {
|
|
244
|
-
return firstVarA.localeCompare(firstVarB);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
|
|
248
|
-
const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
|
|
249
|
-
return degreeB - degreeA;
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (!entries.length) return "0";
|
|
253
|
-
|
|
254
|
-
return entries.map(([key, coeff], index) => {
|
|
255
|
-
const powers = keyToPowers(key);
|
|
256
|
-
const absCoeff = Math.abs(coeff);
|
|
257
|
-
const variablePart = Object.entries(powers)
|
|
258
|
-
.map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
|
|
259
|
-
.join(" * ");
|
|
260
|
-
let body = variablePart;
|
|
261
|
-
|
|
262
|
-
if (!body) {
|
|
263
|
-
body = `${absCoeff}`;
|
|
264
|
-
} else if (absCoeff !== 1) {
|
|
265
|
-
body = `${absCoeff} * ${body}`;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (index === 0) {
|
|
269
|
-
return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return coeff < 0 ? `- ${body}` : `+ ${body}`;
|
|
273
|
-
}).join(" ");
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const ast = this.parse(normalizedExpression).ast;
|
|
277
|
-
const result = astToRat(ast);
|
|
278
|
-
const numerator = formatPoly(result.num);
|
|
279
|
-
const denominator = formatPoly(result.den);
|
|
280
|
-
const variableSet = new Set();
|
|
281
|
-
|
|
282
|
-
for (const poly of [result.num, result.den]) {
|
|
283
|
-
for (const key of poly.keys()) {
|
|
284
|
-
for (const name of Object.keys(keyToPowers(key))) {
|
|
285
|
-
variableSet.add(name);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!withDetails) {
|
|
291
|
-
return `(${numerator}) / (${denominator})`;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
numerator,
|
|
296
|
-
denominator,
|
|
297
|
-
coefficients: [],
|
|
298
|
-
variables: [...variableSet].sort(),
|
|
299
|
-
expression: `(${numerator}) / (${denominator})`
|
|
300
|
-
};
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
setVariable(name, value) {
|
|
305
|
-
this.variables.set(name, value);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
getVariable(name) {
|
|
309
|
-
return this.variables.get(name);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
addFunction(name, fn) {
|
|
313
|
-
this.functions.register(name, fn);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
_createContext() {
|
|
317
|
-
return createContext({
|
|
318
|
-
functions: this.functions,
|
|
319
|
-
variables: this.variables,
|
|
320
|
-
units: this.units,
|
|
321
|
-
evaluate: this.evaluate.bind(this)
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
tokenize(expr) {
|
|
326
|
-
if (typeof expr !== "string") {
|
|
327
|
-
throw new Error("Expression must be a string");
|
|
328
|
-
}
|
|
329
|
-
return tokenize(expr, this._createContext());
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
parse(expr) {
|
|
333
|
-
const tokens = this.tokenize(expr);
|
|
334
|
-
const ast = buildAST(tokens);
|
|
335
|
-
return { tokens, ast };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
evaluate(expr) {
|
|
339
|
-
const { ast } = this.parse(expr);
|
|
340
|
-
return formatResult(evaluateAST(
|
|
341
|
-
ast,
|
|
342
|
-
this._createContext()
|
|
343
|
-
));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
compile(expr) {
|
|
347
|
-
if (this._cache.has(expr)) {
|
|
348
|
-
return this._cache.get(expr);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const { ast } = this.parse(expr);
|
|
352
|
-
|
|
353
|
-
const compiledFn = (scope = {}) => {
|
|
354
|
-
const baseContext = this._createContext();
|
|
355
|
-
const scopedContext = baseContext.withScope(scope);
|
|
356
|
-
return formatResult(evaluateAST(ast, scopedContext));
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
this._cache.set(expr, compiledFn);
|
|
360
|
-
return compiledFn;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
clearCache() {
|
|
364
|
-
this._cache.clear();
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export default exprify;
|