exprify 1.0.2 → 1.0.4
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/README.md +34 -1
- package/dist/{exprify.cjs.js → exprify.cjs.cjs} +1 -1
- package/dist/exprify.cjs.cjs.map +1 -0
- package/dist/exprify.js +2 -2
- package/dist/exprify.min.js +1 -1
- package/package.json +15 -6
- package/src/core/Exprify.js +369 -0
- package/src/core/context.js +30 -0
- package/src/function/executor.js +64 -0
- package/src/function/internal.js +612 -0
- package/src/function/registry.js +68 -0
- package/src/index.js +2 -0
- package/src/math/operations.js +38 -0
- package/src/parser/astBuild.js +531 -0
- package/src/parser/evaluator.js +460 -0
- package/src/parser/tokenizer.js +399 -0
- package/src/utils/globalUnits.js +217 -0
- package/src/utils/matrix.js +53 -0
- package/src/utils/store.js +178 -0
- package/src/variables/store.js +75 -0
- package/dist/exprify.cjs.js.map +0 -1
- package/docs/README.md +0 -34
- package/docs/assets/css/style.scss +0 -4
- package/docs/tokenType.txt +0 -21
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { unwrapDenseMatrix, wrapDenseMatrix } from "../utils/matrix.js";
|
|
2
|
+
|
|
3
|
+
function validateSquareMatrix(matrix) {
|
|
4
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
5
|
+
if (!Array.isArray(matrix) || matrix.length === 0) {
|
|
6
|
+
throw new Error("det() expects a non-empty matrix");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!matrix.every(Array.isArray)) {
|
|
10
|
+
throw new Error("det() expects a 2D matrix");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const size = matrix.length;
|
|
14
|
+
if (!matrix.every((row) => row.length === size)) {
|
|
15
|
+
throw new Error("det() expects a square matrix");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const row of matrix) {
|
|
19
|
+
for (const value of row) {
|
|
20
|
+
if (typeof value !== "number" && typeof value !== "bigint") {
|
|
21
|
+
throw new Error("det() matrix values must be numeric");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function determinant(matrix) {
|
|
28
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
29
|
+
validateSquareMatrix(matrix);
|
|
30
|
+
|
|
31
|
+
if (matrix.length === 1) {
|
|
32
|
+
return matrix[0][0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (matrix.length === 2) {
|
|
36
|
+
return (matrix[0][0] * matrix[1][1]) - (matrix[0][1] * matrix[1][0]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return matrix[0].reduce((sum, value, columnIndex) => {
|
|
40
|
+
const minor = matrix.slice(1).map((row) =>
|
|
41
|
+
row.filter((_, index) => index !== columnIndex)
|
|
42
|
+
);
|
|
43
|
+
const cofactor = columnIndex % 2 === 0 ? value : -value;
|
|
44
|
+
return sum + (cofactor * determinant(minor));
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toLinearArray(value) {
|
|
49
|
+
const unwrapped = unwrapDenseMatrix(value);
|
|
50
|
+
return Array.isArray(unwrapped) ? unwrapped : value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function asMatrixData(value) {
|
|
54
|
+
const data = unwrapDenseMatrix(value);
|
|
55
|
+
if (!Array.isArray(data)) {
|
|
56
|
+
throw new Error("Expected matrix data");
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function solveLinearSystem(coefficients, constants) {
|
|
62
|
+
const n = coefficients.length;
|
|
63
|
+
const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
|
|
64
|
+
|
|
65
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
66
|
+
let maxRow = pivot;
|
|
67
|
+
let maxValue = Math.abs(augmented[pivot][pivot]);
|
|
68
|
+
|
|
69
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
70
|
+
const current = Math.abs(augmented[row][pivot]);
|
|
71
|
+
if (current > maxValue) {
|
|
72
|
+
maxValue = current;
|
|
73
|
+
maxRow = row;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (maxValue === 0) {
|
|
78
|
+
throw new Error("Linear system is singular");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (maxRow !== pivot) {
|
|
82
|
+
[augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pivotValue = augmented[pivot][pivot];
|
|
86
|
+
for (let col = pivot; col <= n; col++) {
|
|
87
|
+
augmented[pivot][col] /= pivotValue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (let row = 0; row < n; row++) {
|
|
91
|
+
if (row === pivot) continue;
|
|
92
|
+
const factor = augmented[row][pivot];
|
|
93
|
+
for (let col = pivot; col <= n; col++) {
|
|
94
|
+
augmented[row][col] -= factor * augmented[pivot][col];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return augmented.map((row) => row[n]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function lupDecomposition(input) {
|
|
103
|
+
const matrix = asMatrixData(input).map((row) => [...row]);
|
|
104
|
+
validateSquareMatrix(matrix);
|
|
105
|
+
|
|
106
|
+
const n = matrix.length;
|
|
107
|
+
const permutation = Array.from({ length: n }, (_, index) => index);
|
|
108
|
+
|
|
109
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
110
|
+
let maxRow = pivot;
|
|
111
|
+
let maxValue = Math.abs(matrix[pivot][pivot]);
|
|
112
|
+
|
|
113
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
114
|
+
const current = Math.abs(matrix[row][pivot]);
|
|
115
|
+
if (current > maxValue) {
|
|
116
|
+
maxValue = current;
|
|
117
|
+
maxRow = row;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (maxValue === 0) {
|
|
122
|
+
throw new Error("Matrix is singular");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (maxRow !== pivot) {
|
|
126
|
+
[matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
|
|
127
|
+
[permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
131
|
+
matrix[row][pivot] /= matrix[pivot][pivot];
|
|
132
|
+
for (let col = pivot + 1; col < n; col++) {
|
|
133
|
+
matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const L = matrix.map((row, rowIndex) =>
|
|
139
|
+
row.map((value, colIndex) => {
|
|
140
|
+
if (rowIndex === colIndex) return 1;
|
|
141
|
+
if (rowIndex > colIndex) return value;
|
|
142
|
+
return 0;
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const U = matrix.map((row, rowIndex) =>
|
|
147
|
+
row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
L: wrapDenseMatrix(L),
|
|
152
|
+
U: wrapDenseMatrix(U),
|
|
153
|
+
p: permutation
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function linearSolve(aInput, bInput) {
|
|
158
|
+
const { L, U, p } = lupDecomposition(aInput);
|
|
159
|
+
const a = asMatrixData(aInput);
|
|
160
|
+
const bData = asMatrixData(bInput);
|
|
161
|
+
const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
|
|
162
|
+
|
|
163
|
+
if (a.length !== bVector.length) {
|
|
164
|
+
throw new Error("Right-hand side dimension mismatch");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const permutedB = p.map((index) => bVector[index]);
|
|
168
|
+
const y = new Array(a.length).fill(0);
|
|
169
|
+
|
|
170
|
+
for (let row = 0; row < a.length; row++) {
|
|
171
|
+
y[row] = permutedB[row];
|
|
172
|
+
for (let col = 0; col < row; col++) {
|
|
173
|
+
y[row] -= L.data[row][col] * y[col];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const x = new Array(a.length).fill(0);
|
|
178
|
+
for (let row = a.length - 1; row >= 0; row--) {
|
|
179
|
+
x[row] = y[row];
|
|
180
|
+
for (let col = row + 1; col < a.length; col++) {
|
|
181
|
+
x[row] -= U.data[row][col] * x[col];
|
|
182
|
+
}
|
|
183
|
+
x[row] /= U.data[row][row];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return wrapDenseMatrix(x.map((value) => [value]));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function solveLyapunov(aInput, qInput) {
|
|
190
|
+
const A = asMatrixData(aInput).map((row) => [...row]);
|
|
191
|
+
const Q = asMatrixData(qInput).map((row) => [...row]);
|
|
192
|
+
validateSquareMatrix(A);
|
|
193
|
+
validateSquareMatrix(Q);
|
|
194
|
+
|
|
195
|
+
const n = A.length;
|
|
196
|
+
if (Q.length !== n) {
|
|
197
|
+
throw new Error("A and Q must have the same dimensions");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const coefficients = [];
|
|
201
|
+
const constants = [];
|
|
202
|
+
|
|
203
|
+
for (let row = 0; row < n; row++) {
|
|
204
|
+
for (let col = 0; col < n; col++) {
|
|
205
|
+
const equation = new Array(n * n).fill(0);
|
|
206
|
+
|
|
207
|
+
for (let k = 0; k < n; k++) {
|
|
208
|
+
equation[k * n + col] += A[row][k];
|
|
209
|
+
equation[row * n + k] += A[col][k];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
coefficients.push(equation);
|
|
213
|
+
constants.push(-Q[row][col]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const solution = solveLinearSystem(coefficients, constants);
|
|
218
|
+
const X = [];
|
|
219
|
+
|
|
220
|
+
for (let row = 0; row < n; row++) {
|
|
221
|
+
X.push(solution.slice(row * n, (row + 1) * n));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return wrapDenseMatrix(X);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function evaluatePolynomial(coefficients, x) {
|
|
228
|
+
return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function syntheticDivide(coefficients, root) {
|
|
232
|
+
const descending = [...coefficients].reverse();
|
|
233
|
+
const quotient = [descending[0]];
|
|
234
|
+
|
|
235
|
+
for (let index = 1; index < descending.length - 1; index++) {
|
|
236
|
+
quotient.push(descending[index] + (quotient[index - 1] * root));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
|
|
240
|
+
return {
|
|
241
|
+
quotient: quotient.reverse(),
|
|
242
|
+
remainder
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function solveQuadratic(coefficients) {
|
|
247
|
+
const [c, b, a] = coefficients;
|
|
248
|
+
const discriminant = (b ** 2) - (4 * a * c);
|
|
249
|
+
if (discriminant < 0) {
|
|
250
|
+
throw new Error("Only real roots are supported");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
254
|
+
return [
|
|
255
|
+
(-b + sqrtDisc) / (2 * a),
|
|
256
|
+
(-b - sqrtDisc) / (2 * a)
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function polynomialRoots(...coefficients) {
|
|
261
|
+
while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
|
|
262
|
+
coefficients.pop();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const degree = coefficients.length - 1;
|
|
266
|
+
if (degree < 1) {
|
|
267
|
+
throw new Error("polynomialRoot() expects at least a linear polynomial");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (degree === 1) {
|
|
271
|
+
const [b, a] = coefficients;
|
|
272
|
+
return [-b / a];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (degree === 2) {
|
|
276
|
+
return solveQuadratic(coefficients);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (degree === 3) {
|
|
280
|
+
const constant = coefficients[0];
|
|
281
|
+
const leading = coefficients[3];
|
|
282
|
+
const candidates = [];
|
|
283
|
+
const limit = Math.abs(constant);
|
|
284
|
+
|
|
285
|
+
for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
|
|
286
|
+
if (limit % divisor === 0) {
|
|
287
|
+
candidates.push(divisor, -divisor);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const candidate of candidates) {
|
|
292
|
+
if (evaluatePolynomial(coefficients, candidate) === 0) {
|
|
293
|
+
const reduced = syntheticDivide(coefficients, candidate);
|
|
294
|
+
const remainingRoots = solveQuadratic(reduced.quotient);
|
|
295
|
+
return [candidate, ...remainingRoots];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new Error("polynomialRoot() currently supports degree up to 3");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function dotProduct(a, b) {
|
|
304
|
+
return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function vectorNorm(vector) {
|
|
308
|
+
return Math.sqrt(dotProduct(vector, vector));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function scaleVector(vector, scalar) {
|
|
312
|
+
return vector.map((value) => value * scalar);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function subtractVectors(a, b) {
|
|
316
|
+
return a.map((value, index) => value - b[index]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function transpose(matrix) {
|
|
320
|
+
return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function qrDecomposition(input) {
|
|
324
|
+
const A = asMatrixData(input).map((row) => [...row]);
|
|
325
|
+
if (!A.length || !A.every((row) => row.length === A[0].length)) {
|
|
326
|
+
throw new Error("qr() expects a rectangular matrix");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const rowCount = A.length;
|
|
330
|
+
const colCount = A[0].length;
|
|
331
|
+
const columns = transpose(A);
|
|
332
|
+
const qColumns = [];
|
|
333
|
+
|
|
334
|
+
for (let col = 0; col < colCount; col++) {
|
|
335
|
+
let vector = [...columns[col]];
|
|
336
|
+
|
|
337
|
+
for (let existing = 0; existing < qColumns.length; existing++) {
|
|
338
|
+
const projection = dotProduct(qColumns[existing], columns[col]);
|
|
339
|
+
vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const norm = vectorNorm(vector);
|
|
343
|
+
if (norm === 0) {
|
|
344
|
+
throw new Error("qr() requires linearly independent columns");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
qColumns.push(scaleVector(vector, 1 / norm));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
|
|
351
|
+
let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
|
|
352
|
+
|
|
353
|
+
for (const column of qColumns) {
|
|
354
|
+
const projection = dotProduct(column, candidate);
|
|
355
|
+
candidate = subtractVectors(candidate, scaleVector(column, projection));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const norm = vectorNorm(candidate);
|
|
359
|
+
if (norm > 1e-10) {
|
|
360
|
+
qColumns.push(scaleVector(candidate, 1 / norm));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
|
|
365
|
+
qColumns.map((column) => column[rowIndex])
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
|
|
369
|
+
for (let row = 0; row < rowCount; row++) {
|
|
370
|
+
for (let col = 0; col < colCount; col++) {
|
|
371
|
+
fullR[row][col] = dotProduct(qColumns[row], columns[col]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
Q: wrapDenseMatrix(Q),
|
|
377
|
+
R: wrapDenseMatrix(fullR)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function splitTerms(expression) {
|
|
382
|
+
const normalized = expression.replace(/\s+/g, "");
|
|
383
|
+
if (!normalized) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return normalized
|
|
388
|
+
.replace(/-/g, "+-")
|
|
389
|
+
.split("+")
|
|
390
|
+
.filter(Boolean);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function parsePolynomial(expression, variable) {
|
|
394
|
+
const terms = splitTerms(expression);
|
|
395
|
+
const coefficients = new Map();
|
|
396
|
+
|
|
397
|
+
for (const term of terms) {
|
|
398
|
+
if (term.includes(variable)) {
|
|
399
|
+
const [rawCoeff, rawPower] = term.split(variable);
|
|
400
|
+
let coefficient;
|
|
401
|
+
|
|
402
|
+
if (rawCoeff === "" || rawCoeff === "+") coefficient = 1;
|
|
403
|
+
else if (rawCoeff === "-") coefficient = -1;
|
|
404
|
+
else {
|
|
405
|
+
const cleaned = rawCoeff.endsWith("*") ? rawCoeff.slice(0, -1) : rawCoeff;
|
|
406
|
+
coefficient = Number(cleaned);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!Number.isFinite(coefficient)) {
|
|
410
|
+
throw new Error("Unsupported algebra term");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let power = 1;
|
|
414
|
+
if (rawPower) {
|
|
415
|
+
if (!rawPower.startsWith("^")) {
|
|
416
|
+
throw new Error("Unsupported algebra term");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
power = Number(rawPower.slice(1));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!Number.isInteger(power) || power < 0) {
|
|
423
|
+
throw new Error("Only non-negative integer powers are supported");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
coefficients.set(power, (coefficients.get(power) || 0) + coefficient);
|
|
427
|
+
} else {
|
|
428
|
+
const constant = Number(term);
|
|
429
|
+
if (!Number.isFinite(constant)) {
|
|
430
|
+
throw new Error("Unsupported algebra term");
|
|
431
|
+
}
|
|
432
|
+
coefficients.set(0, (coefficients.get(0) || 0) + constant);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return coefficients;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function formatPolynomial(coefficients, variable) {
|
|
440
|
+
const ordered = [...coefficients.entries()]
|
|
441
|
+
.filter(([, coefficient]) => coefficient !== 0)
|
|
442
|
+
.sort((a, b) => b[0] - a[0]);
|
|
443
|
+
|
|
444
|
+
if (!ordered.length) {
|
|
445
|
+
return "0";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return ordered.map(([power, coefficient], index) => {
|
|
449
|
+
const negative = coefficient < 0;
|
|
450
|
+
const absCoeff = Math.abs(coefficient);
|
|
451
|
+
let body;
|
|
452
|
+
|
|
453
|
+
if (power === 0) {
|
|
454
|
+
body = `${absCoeff}`;
|
|
455
|
+
} else if (power === 1) {
|
|
456
|
+
body = absCoeff === 1 ? variable : `${absCoeff} * ${variable}`;
|
|
457
|
+
} else {
|
|
458
|
+
body = absCoeff === 1
|
|
459
|
+
? `${variable}^${power}`
|
|
460
|
+
: `${absCoeff} * ${variable}^${power}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (index === 0) {
|
|
464
|
+
return negative ? `-${body}` : body;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return negative ? `- ${body}` : `+ ${body}`;
|
|
468
|
+
}).join(" ");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function simplifyExpression(expression) {
|
|
472
|
+
const compact = expression.replace(/\s+/g, "");
|
|
473
|
+
const variableMatch = compact.match(/[a-zA-Z]+/);
|
|
474
|
+
const variable = variableMatch?.[0] || "x";
|
|
475
|
+
const coefficients = parsePolynomial(expression, variable);
|
|
476
|
+
return formatPolynomial(coefficients, variable);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function derivativeExpression(expression, variable) {
|
|
480
|
+
const coefficients = parsePolynomial(expression, variable);
|
|
481
|
+
const derived = new Map();
|
|
482
|
+
|
|
483
|
+
for (const [power, coefficient] of coefficients.entries()) {
|
|
484
|
+
if (power === 0) continue;
|
|
485
|
+
derived.set(power - 1, (derived.get(power - 1) || 0) + (coefficient * power));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return formatPolynomial(derived, variable);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export const internalFunctions = {
|
|
492
|
+
max: (...args) => {
|
|
493
|
+
if (!args.length) throw new Error("max() requires arguments");
|
|
494
|
+
return Math.max(...args);
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
min: (...args) => {
|
|
498
|
+
if (!args.length) throw new Error("min() requires arguments");
|
|
499
|
+
return Math.min(...args);
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
abs: (x) => Math.abs(x),
|
|
503
|
+
|
|
504
|
+
round: (x) => Math.round(x),
|
|
505
|
+
|
|
506
|
+
floor: (x) => Math.floor(x),
|
|
507
|
+
|
|
508
|
+
ceil: (x) => Math.ceil(x),
|
|
509
|
+
|
|
510
|
+
sqrt: (x) => {
|
|
511
|
+
if (x < 0) throw new Error("sqrt() domain error");
|
|
512
|
+
return Math.sqrt(x);
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
pow: (a, b) => a ** b,
|
|
516
|
+
det: (matrix) => determinant(matrix),
|
|
517
|
+
polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
|
|
518
|
+
lsolve: (a, b) => linearSolve(a, b),
|
|
519
|
+
lup: (matrix) => lupDecomposition(matrix),
|
|
520
|
+
lyap: (a, q) => solveLyapunov(a, q),
|
|
521
|
+
qr: (matrix) => qrDecomposition(matrix),
|
|
522
|
+
simplify: (expression) => {
|
|
523
|
+
if (typeof expression !== "string") {
|
|
524
|
+
throw new Error("simplify() expects an expression string");
|
|
525
|
+
}
|
|
526
|
+
return simplifyExpression(expression);
|
|
527
|
+
},
|
|
528
|
+
derivative: (expression, variable = "x") => {
|
|
529
|
+
if (typeof expression !== "string" || typeof variable !== "string") {
|
|
530
|
+
throw new Error("derivative() expects expression and variable strings");
|
|
531
|
+
}
|
|
532
|
+
return derivativeExpression(expression, variable);
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/* ================= TRIGONOMETRY ================= */
|
|
536
|
+
|
|
537
|
+
sin: (x) => Math.sin(x),
|
|
538
|
+
cos: (x) => Math.cos(x),
|
|
539
|
+
tan: (x) => Math.tan(x),
|
|
540
|
+
|
|
541
|
+
asin: (x) => Math.asin(x),
|
|
542
|
+
acos: (x) => Math.acos(x),
|
|
543
|
+
atan: (x) => Math.atan(x),
|
|
544
|
+
|
|
545
|
+
/* ================= LOG / EXP ================= */
|
|
546
|
+
|
|
547
|
+
log: (x) => {
|
|
548
|
+
if (x <= 0) throw new Error("log() domain error");
|
|
549
|
+
return Math.log(x);
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
log10: (x) => {
|
|
553
|
+
if (x <= 0) throw new Error("log10() domain error");
|
|
554
|
+
return Math.log10(x);
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
exp: (x) => Math.exp(x),
|
|
558
|
+
|
|
559
|
+
/* ================= RANDOM ================= */
|
|
560
|
+
|
|
561
|
+
random: () => Math.random(),
|
|
562
|
+
|
|
563
|
+
/* ================= BOOLEAN / LOGIC ================= */
|
|
564
|
+
|
|
565
|
+
and: (a, b) => Boolean(a && b),
|
|
566
|
+
|
|
567
|
+
or: (a, b) => Boolean(a || b),
|
|
568
|
+
|
|
569
|
+
not: (a) => !a,
|
|
570
|
+
"!": (a) => !a,
|
|
571
|
+
|
|
572
|
+
/* ================= COMPARISON ================= */
|
|
573
|
+
|
|
574
|
+
eq: (a, b) => a === b,
|
|
575
|
+
|
|
576
|
+
neq: (a, b) => a !== b,
|
|
577
|
+
"notEqual": (a, b) => a !== b,
|
|
578
|
+
|
|
579
|
+
gt: (a, b) => a > b,
|
|
580
|
+
"greaterThan": (a, b) => a > b,
|
|
581
|
+
|
|
582
|
+
lt: (a, b) => a < b,
|
|
583
|
+
"lessThan": (a, b) => a < b,
|
|
584
|
+
|
|
585
|
+
gte: (a, b) => a >= b,
|
|
586
|
+
"greaterThanOrEqual": (a, b) => a >= b,
|
|
587
|
+
|
|
588
|
+
lte: (a, b) => a <= b,
|
|
589
|
+
"lessThanOrEqual": (a, b) => a <= b,
|
|
590
|
+
|
|
591
|
+
/* ================= UTILITY ================= */
|
|
592
|
+
|
|
593
|
+
clamp: (x, min, max) => {
|
|
594
|
+
if (min > max) throw new Error("clamp(): min > max");
|
|
595
|
+
return Math.min(Math.max(x, min), max);
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
if: (condition, a, b) => (condition ? a : b),
|
|
599
|
+
|
|
600
|
+
/* ================= TYPE ================= */
|
|
601
|
+
|
|
602
|
+
typeof: (x) => typeof x,
|
|
603
|
+
|
|
604
|
+
/* ================= STRING ================= */
|
|
605
|
+
|
|
606
|
+
length: (x) => {
|
|
607
|
+
if (typeof x === "string" || Array.isArray(x)) {
|
|
608
|
+
return x.length;
|
|
609
|
+
}
|
|
610
|
+
throw new Error("length() expects string or array");
|
|
611
|
+
}
|
|
612
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function createFunctionRegistry(initial = {}) {
|
|
2
|
+
const store = Object.create(null);
|
|
3
|
+
|
|
4
|
+
for (const key in initial) {
|
|
5
|
+
if (typeof initial[key] === "function") {
|
|
6
|
+
store[key] = initial[key];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
getAllFunctionsName() {
|
|
12
|
+
return Object.keys(store);
|
|
13
|
+
},
|
|
14
|
+
// register new formula
|
|
15
|
+
register(name, fn) {
|
|
16
|
+
if (typeof name !== "string" || !name) {
|
|
17
|
+
throw new Error("Formula name must be a non-empty string");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof fn !== "function") {
|
|
21
|
+
throw new Error(`Formula "${name}" must be callable`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
store[name] = fn;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// get formula
|
|
28
|
+
get(name) {
|
|
29
|
+
return store[name];
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// check existence
|
|
33
|
+
has(name) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(store, name);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// remove formula
|
|
38
|
+
remove(name) {
|
|
39
|
+
delete store[name];
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// list all
|
|
43
|
+
all() {
|
|
44
|
+
return { ...store };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// clear registry
|
|
48
|
+
clear() {
|
|
49
|
+
for (const key in store) {
|
|
50
|
+
delete store[key];
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// extend multiple
|
|
55
|
+
extend(formulas = {}) {
|
|
56
|
+
for (const name in formulas) {
|
|
57
|
+
if (typeof formulas[name] === "function") {
|
|
58
|
+
store[name] = formulas[name];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// clone (for scoped instances)
|
|
64
|
+
clone() {
|
|
65
|
+
return createFormulaRegistry(store);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const isValidNumberPair = (a, b) =>
|
|
2
|
+
(typeof a === typeof b) &&
|
|
3
|
+
(typeof a === 'number' || typeof a === 'bigint');
|
|
4
|
+
|
|
5
|
+
export const mathOperations = Object.freeze({
|
|
6
|
+
power: function(a, b) {
|
|
7
|
+
if (isValidNumberPair(a, b)) return a ** b;
|
|
8
|
+
throw new Error("Invalid types for ^");
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
multiply: function(a, b) {
|
|
12
|
+
if (isValidNumberPair(a, b)) return a * b;
|
|
13
|
+
throw new Error("Invalid types for *");
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
divide: function(a, b) {
|
|
17
|
+
if (isValidNumberPair(a, b)) {
|
|
18
|
+
if (b === 0) throw new Error("Division by zero");
|
|
19
|
+
return a / b;
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Invalid types for /");
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
add: function(a, b) {
|
|
25
|
+
if (isValidNumberPair(a, b)) return a + b;
|
|
26
|
+
if (typeof a === 'string' && typeof b === 'string') return a + b;
|
|
27
|
+
throw new Error("Invalid types for +");
|
|
28
|
+
},
|
|
29
|
+
subtract: function(a, b) {
|
|
30
|
+
if (isValidNumberPair(a, b)) return a - b;
|
|
31
|
+
throw new Error("Invalid types for -");
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
modulus: function(a, b) {
|
|
35
|
+
if (isValidNumberPair(a, b)) return a % b;
|
|
36
|
+
throw new Error("Invalid types for %");
|
|
37
|
+
}
|
|
38
|
+
});
|