Skip to content

Commit cd60ba7

Browse files
committed
update
1 parent 20fe200 commit cd60ba7

15 files changed

Lines changed: 2617 additions & 13 deletions

dist/exprify.cjs.js

Lines changed: 622 additions & 2 deletions
Large diffs are not rendered by default.

dist/exprify.cjs.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/exprify.esm.js

Lines changed: 622 additions & 2 deletions
Large diffs are not rendered by default.

dist/exprify.esm.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/exprify.js

Lines changed: 622 additions & 2 deletions
Large diffs are not rendered by default.

dist/exprify.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/exprify.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/exprify.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Exprify Docs
2+
3+
This folder contains supporting documentation for Exprify beyond the main project README.
4+
5+
## Contents
6+
7+
### Getting Started
8+
9+
Exprify is a math expression parser and evaluator for JavaScript with support for:
10+
11+
- arithmetic and precedence
12+
- variables and assignments
13+
- custom JavaScript functions
14+
- inline function definitions
15+
- unit conversion
16+
- matrices
17+
- complex numbers
18+
- algebra helpers
19+
20+
## Quick Example
21+
22+
```js
23+
import Exprify from "exprify";
24+
25+
const expr = new Exprify();
26+
27+
expr.evaluate("hyp(a, b) = sqrt(a ^ 2 + b ^ 2)");
28+
console.log(expr.evaluate("hyp(3, 4)"));
29+
// 5
30+
```
31+
32+
## Main References
33+
34+
- Project overview and API: [`../README.md`](../README.md)
35+
- Token type reference: [`tokenType.txt`](./tokenType.txt)
36+
37+
## Useful Expressions
38+
39+
```txt
40+
x = 10
41+
y = 5
42+
x + y * 2
43+
```
44+
45+
```txt
46+
2 inch to cm
47+
5 cm + 2 inch
48+
```
49+
50+
```txt
51+
m = [1, 2; 3, 4]
52+
det(m)
53+
```
54+
55+
```txt
56+
hyp(a, b) = sqrt(a ^ 2 + b ^ 2)
57+
hyp(6, 8)
58+
```

src/core/Exprify.js

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { globalUnits } from "../utils/globalUnits.js";
1010
import { createVarStore } from "../variables/store.js";
1111
import { createFunctionRegistry } from "../function/registry.js";
1212
import { internalFunctions } from "../function/internal.js";
13+
import { isDenseMatrixWrapper, serializeExprifyValue, wrapDenseMatrix } from "../utils/matrix.js";
1314

1415
import { buildAST } from "../parser/astBuild.js";
1516

@@ -42,6 +43,18 @@ const formatComplex = (value) => {
4243
return `${real} ${sign} ${imagPart}`;
4344
};
4445

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+
4558
const formatResult = (value) => {
4659
if (isComplex(value)) {
4760
return formatComplex(value);
@@ -51,12 +64,20 @@ const formatResult = (value) => {
5164
return `${value.value} ${value.unit}`;
5265
}
5366

67+
if (isDenseMatrixWrapper(value)) {
68+
return serializeExprifyValue(value);
69+
}
70+
5471
if (isMatrix(value)) {
55-
return value.map((row) => row.join("\t")).join("\n");
72+
return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
5673
}
5774

5875
if (Array.isArray(value)) {
59-
return value.join("\n");
76+
return JSON.stringify(value);
77+
}
78+
79+
if (value && typeof value === "object") {
80+
return serializeExprifyValue(value);
6081
}
6182

6283
return value;
@@ -70,6 +91,214 @@ class exprify {
7091
this.functions = createFunctionRegistry(internalFunctions);
7192
this.variables = createVarStore();
7293
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+
});
73302
}
74303

75304
setVariable(name, value) {

0 commit comments

Comments
 (0)