picocalc 0.0.0 → 0.0.2
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/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.mjs +1218 -0
- package/package.json +38 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roman A
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
File without changes
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/lib/types.d.ts
|
|
2
|
+
interface PrecisionOptions {
|
|
3
|
+
readonly format?: "decimal" | "precise";
|
|
4
|
+
readonly maxDecimals?: number;
|
|
5
|
+
readonly roundingMode?: "round" | "truncate";
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/index.d.ts
|
|
9
|
+
interface CalculateOptions extends PrecisionOptions {
|
|
10
|
+
decimalSeparator?: "." | ",";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The main entry point for the library.
|
|
14
|
+
*/
|
|
15
|
+
declare function calculate(expression: string, options?: CalculateOptions): string;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { calculate };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import "bigint.bitlength";
|
|
2
|
+
//#region src/lib/errors.ts
|
|
3
|
+
var GenericMathErrror = class extends Error {
|
|
4
|
+
pos;
|
|
5
|
+
constructor(name, message, pos) {
|
|
6
|
+
if (pos === void 0) super(`${name}: ${message}`);
|
|
7
|
+
else super(`${name}: ${message} at position ${pos}`);
|
|
8
|
+
this.pos = pos;
|
|
9
|
+
this.name = name;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var InterpreterError = class extends GenericMathErrror {
|
|
13
|
+
constructor(message, pos) {
|
|
14
|
+
super("InterpreterError", message, pos);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var MaximumPrecisionError = class extends InterpreterError {
|
|
18
|
+
constructor(precision, maxPrecision) {
|
|
19
|
+
super(`Exceeded maximum precision of ${maxPrecision} digits (${precision})`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var NotImplementedError = class extends InterpreterError {
|
|
23
|
+
constructor(message, pos) {
|
|
24
|
+
super(message, pos);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var OverflowError = class extends InterpreterError {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("Overflow");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var DivisionByZeroError = class extends InterpreterError {
|
|
33
|
+
constructor() {
|
|
34
|
+
super("Division by zero");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var EmptyExpressionError = class extends InterpreterError {
|
|
38
|
+
constructor() {
|
|
39
|
+
super("Empty expression");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var MismatchedParenthesisError = class extends InterpreterError {
|
|
43
|
+
constructor(pos, message = "Mismatched parenthesis") {
|
|
44
|
+
super(message, pos);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/lib/types.ts
|
|
49
|
+
const OverflowValue = { n: "OVERFLOW" };
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/lib/utils/constants.ts
|
|
52
|
+
const PI = Math.PI.toString();
|
|
53
|
+
const E = Math.E.toString();
|
|
54
|
+
const ZERO = {
|
|
55
|
+
n: 0n,
|
|
56
|
+
d: 1n
|
|
57
|
+
};
|
|
58
|
+
const ONE = {
|
|
59
|
+
n: 1n,
|
|
60
|
+
d: 1n
|
|
61
|
+
};
|
|
62
|
+
const constants = {
|
|
63
|
+
e: E,
|
|
64
|
+
pi: PI
|
|
65
|
+
};
|
|
66
|
+
function getConst(id) {
|
|
67
|
+
const c = constants[id];
|
|
68
|
+
return {
|
|
69
|
+
n: BigInt(c.replace(".", "")),
|
|
70
|
+
d: 10n ** BigInt(c.length - c.indexOf(".") - 1)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/lib/utils/gcd.ts
|
|
75
|
+
/**
|
|
76
|
+
* The Euclidean GCD Algorithm for BigInt.
|
|
77
|
+
*/
|
|
78
|
+
function gcd(a, b) {
|
|
79
|
+
if (a === b) return a;
|
|
80
|
+
if (a === 1n || b === 1n) return 1n;
|
|
81
|
+
if (a === 0n) return b;
|
|
82
|
+
if (b === 0n) return a;
|
|
83
|
+
a = a < 0 ? -a : a;
|
|
84
|
+
b = b < 0 ? -b : b;
|
|
85
|
+
while (b > 0) {
|
|
86
|
+
a %= b;
|
|
87
|
+
[a, b] = [b, a];
|
|
88
|
+
}
|
|
89
|
+
return a;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/lib/utils/multiply.ts
|
|
93
|
+
function multiply(a, b) {
|
|
94
|
+
const aN = a.n;
|
|
95
|
+
const bN = b.n;
|
|
96
|
+
if (aN === 0n || bN === 0n) return ZERO;
|
|
97
|
+
if (aN === "OVERFLOW") return a;
|
|
98
|
+
if (bN === "OVERFLOW") return b;
|
|
99
|
+
const { d: aD, c: aC, e: aE } = a;
|
|
100
|
+
const { d: bD, c: bC, e: bE } = b;
|
|
101
|
+
const g1 = gcd(aN, bD);
|
|
102
|
+
const g2 = gcd(bN, aD);
|
|
103
|
+
const n = aN / g1 * (bN / g2);
|
|
104
|
+
const d = aD / g2 * (bD / g1);
|
|
105
|
+
if (aC === void 0 && bC === void 0) return {
|
|
106
|
+
n,
|
|
107
|
+
d
|
|
108
|
+
};
|
|
109
|
+
if (aC === bC) return {
|
|
110
|
+
n,
|
|
111
|
+
d,
|
|
112
|
+
c: aC,
|
|
113
|
+
e: simplify(add({
|
|
114
|
+
n: aE?.n ?? 1n,
|
|
115
|
+
d: aE?.d ?? 1n
|
|
116
|
+
}, {
|
|
117
|
+
n: bE?.n ?? 1n,
|
|
118
|
+
d: bE?.d ?? 1n
|
|
119
|
+
}))
|
|
120
|
+
};
|
|
121
|
+
if (aC !== void 0 && bC !== void 0) return multiply(toSimpleFraction(a), toSimpleFraction(b));
|
|
122
|
+
return {
|
|
123
|
+
n,
|
|
124
|
+
d,
|
|
125
|
+
c: aC ?? bC,
|
|
126
|
+
e: aE ?? bE
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/lib/utils/nthroot.ts
|
|
131
|
+
/**
|
|
132
|
+
* Calculates the integer nth root of a BigInt.
|
|
133
|
+
*/
|
|
134
|
+
function iNthRoot(value, n) {
|
|
135
|
+
if (value < 0 && n % 2n === 0n) throw new NotImplementedError(`Even (${n}th) root of negative numbers is not implemented yet.`);
|
|
136
|
+
if (value < 0) return -iNthRoot(-value, n);
|
|
137
|
+
if (value < 2n) return value;
|
|
138
|
+
if (n === 1n) return value;
|
|
139
|
+
let x = 1n << BigInt.bitLength(value) / n + 1n;
|
|
140
|
+
const nMinus1 = n - 1n;
|
|
141
|
+
while (true) {
|
|
142
|
+
const nextX = (nMinus1 * x + value / x ** nMinus1) / n;
|
|
143
|
+
if (nextX >= x) break;
|
|
144
|
+
x = nextX;
|
|
145
|
+
}
|
|
146
|
+
return x;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Calculates the n-th root of a fraction.
|
|
150
|
+
*/
|
|
151
|
+
function nthRoot(v, n, precision) {
|
|
152
|
+
if (n === 0n) throw new DivisionByZeroError();
|
|
153
|
+
if (n === 1n) return v;
|
|
154
|
+
if (v.n < 0) {
|
|
155
|
+
if (n % 2n === 0n) throw new NotImplementedError(`Even (${n}th) root of negative numbers is not implemented yet.`);
|
|
156
|
+
const positiveResult = nthRoot({
|
|
157
|
+
n: -v.n,
|
|
158
|
+
d: v.d
|
|
159
|
+
}, n, precision);
|
|
160
|
+
return {
|
|
161
|
+
n: -positiveResult.n,
|
|
162
|
+
d: positiveResult.d
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (v.n === 0n) return ZERO;
|
|
166
|
+
if (precision.format === "precise") {
|
|
167
|
+
const rootN = iNthRoot(v.n, n);
|
|
168
|
+
const rootD = iNthRoot(v.d, n);
|
|
169
|
+
if (rootN ** n === v.n && rootD ** n === v.d) {
|
|
170
|
+
const e = simplify({
|
|
171
|
+
n: v.e?.n ?? 1n,
|
|
172
|
+
d: (v.e?.d ?? 1n) * n
|
|
173
|
+
});
|
|
174
|
+
return simplify({
|
|
175
|
+
n: rootN,
|
|
176
|
+
d: rootD,
|
|
177
|
+
c: v.c,
|
|
178
|
+
e
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const precisionDigits = Math.max(10, (precision.maxDecimals ?? 30) * 3);
|
|
183
|
+
const scaleFactor = 10n ** (BigInt(precisionDigits) * n);
|
|
184
|
+
return simplify({
|
|
185
|
+
n: iNthRoot(v.n * scaleFactor / v.d, n),
|
|
186
|
+
d: 10n ** BigInt(precisionDigits)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/lib/utils/simplify.ts
|
|
191
|
+
/**
|
|
192
|
+
* Reduces a fraction to its simplest form.
|
|
193
|
+
*/
|
|
194
|
+
function simplify(v) {
|
|
195
|
+
if (v.d === 0n) throw new DivisionByZeroError();
|
|
196
|
+
if (v.n === 0n) return ZERO;
|
|
197
|
+
if (v.d === 1n) {
|
|
198
|
+
if (v.e?.n === 0n) return {
|
|
199
|
+
n: v.n,
|
|
200
|
+
d: v.d
|
|
201
|
+
};
|
|
202
|
+
return v;
|
|
203
|
+
}
|
|
204
|
+
const common = gcd(v.n, v.d);
|
|
205
|
+
const sign = v.d < 0 ? -1n : 1n;
|
|
206
|
+
const n = v.n / common * sign;
|
|
207
|
+
const d = v.d / common * sign;
|
|
208
|
+
if (v.e?.n === 0n) return {
|
|
209
|
+
n,
|
|
210
|
+
d
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
n,
|
|
214
|
+
d,
|
|
215
|
+
c: v.c,
|
|
216
|
+
e: v.e
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolves any constants and exponents to return a simple fraction.
|
|
221
|
+
*/
|
|
222
|
+
function toSimpleFraction(val) {
|
|
223
|
+
if (!val.c) return val;
|
|
224
|
+
const c = getConst(val.c);
|
|
225
|
+
const expN = val.e?.n ?? 1n;
|
|
226
|
+
const expD = val.e?.d ?? 1n;
|
|
227
|
+
const absExpN = expN < 0 ? -expN : expN;
|
|
228
|
+
let poweredConstant = {
|
|
229
|
+
n: expN < 0 ? c.d ** absExpN : c.n ** absExpN,
|
|
230
|
+
d: expN < 0 ? c.n ** absExpN : c.d ** absExpN
|
|
231
|
+
};
|
|
232
|
+
if (expD !== 1n) poweredConstant = nthRoot(poweredConstant, expD, { format: "precise" });
|
|
233
|
+
return multiply(poweredConstant, {
|
|
234
|
+
n: val.n,
|
|
235
|
+
d: val.d
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/lib/utils/add.ts
|
|
240
|
+
function add(left, right, subtract = false) {
|
|
241
|
+
const lN = left.n;
|
|
242
|
+
const rN = right.n;
|
|
243
|
+
if (lN === "OVERFLOW") return left;
|
|
244
|
+
if (rN === "OVERFLOW") return right;
|
|
245
|
+
const { d: lD, c, e } = left;
|
|
246
|
+
const { d: rD, c: rC, e: rE } = right;
|
|
247
|
+
if (lN === 0n) return {
|
|
248
|
+
n: subtract ? -rN : rN,
|
|
249
|
+
d: rD,
|
|
250
|
+
c: rC,
|
|
251
|
+
e: rE
|
|
252
|
+
};
|
|
253
|
+
if (rN === 0n) return {
|
|
254
|
+
n: lN,
|
|
255
|
+
d: lD,
|
|
256
|
+
c,
|
|
257
|
+
e
|
|
258
|
+
};
|
|
259
|
+
let n;
|
|
260
|
+
let d;
|
|
261
|
+
if (lD === rD) {
|
|
262
|
+
n = subtract ? lN - rN : lN + rN;
|
|
263
|
+
d = lD;
|
|
264
|
+
} else {
|
|
265
|
+
const common = gcd(lD, rD);
|
|
266
|
+
const mLeft = rD / common;
|
|
267
|
+
const mRight = lD / common;
|
|
268
|
+
n = subtract ? lN * mLeft - rN * mRight : lN * mLeft + rN * mRight;
|
|
269
|
+
d = lD * mLeft;
|
|
270
|
+
}
|
|
271
|
+
if (n === 0n) return ZERO;
|
|
272
|
+
if (c !== rC || e?.n !== rE?.n) return add(toSimpleFraction(left), toSimpleFraction(right), subtract);
|
|
273
|
+
return {
|
|
274
|
+
n,
|
|
275
|
+
d,
|
|
276
|
+
c,
|
|
277
|
+
e
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/lib/utils/ceil.ts
|
|
282
|
+
/**
|
|
283
|
+
* Returns the smallest integer greater than or equal to the value.
|
|
284
|
+
*/
|
|
285
|
+
function ceil(v) {
|
|
286
|
+
const { n, d } = simplify(v);
|
|
287
|
+
if (d === 1n) return n;
|
|
288
|
+
const isPositive = n > 0;
|
|
289
|
+
const result = n / d;
|
|
290
|
+
const hasRemainder = n % d !== 0n;
|
|
291
|
+
return isPositive && hasRemainder ? result + 1n : result;
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/lib/utils/divide.ts
|
|
295
|
+
function divide(left, right) {
|
|
296
|
+
const lN = left.n;
|
|
297
|
+
const rN = right.n;
|
|
298
|
+
if (rN === 0n) throw new DivisionByZeroError();
|
|
299
|
+
if (lN === 0n) return ZERO;
|
|
300
|
+
if (lN === "OVERFLOW" || rN === "OVERFLOW") return OverflowValue;
|
|
301
|
+
const { d: lD, c: lC, e: lE } = left;
|
|
302
|
+
const { d: rD, c: rC, e: rE } = right;
|
|
303
|
+
const g1 = gcd(lN, rN);
|
|
304
|
+
const g2 = gcd(rD, lD);
|
|
305
|
+
const n = lN / g1 * (rD / g2);
|
|
306
|
+
const d = lD / g2 * (rN / g1);
|
|
307
|
+
if (lC !== void 0 && lC === rC) return {
|
|
308
|
+
n,
|
|
309
|
+
d,
|
|
310
|
+
c: lC,
|
|
311
|
+
e: simplify(add({
|
|
312
|
+
n: lE?.n ?? 1n,
|
|
313
|
+
d: lE?.d ?? 1n
|
|
314
|
+
}, {
|
|
315
|
+
n: rE?.n ?? 1n,
|
|
316
|
+
d: rE?.d ?? 1n
|
|
317
|
+
}, true))
|
|
318
|
+
};
|
|
319
|
+
const c = lC ?? rC;
|
|
320
|
+
let e;
|
|
321
|
+
if (lC !== void 0) e = lE ? {
|
|
322
|
+
n: lE.n,
|
|
323
|
+
d: lE.d ?? 1n
|
|
324
|
+
} : {
|
|
325
|
+
n: 1n,
|
|
326
|
+
d: 1n
|
|
327
|
+
};
|
|
328
|
+
else if (rC !== void 0) e = {
|
|
329
|
+
n: -(rE?.n ?? 1n),
|
|
330
|
+
d: rE?.d ?? 1n
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
n,
|
|
334
|
+
d,
|
|
335
|
+
c,
|
|
336
|
+
e
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/lib/utils/sqrt.ts
|
|
341
|
+
/**
|
|
342
|
+
* Calculates the integer square root of a BigInt.
|
|
343
|
+
*/
|
|
344
|
+
function isqrt(value) {
|
|
345
|
+
if (value < 0) throw new NotImplementedError("Square root of negative numbers is not implemented yet.");
|
|
346
|
+
if (value < 2n) return value;
|
|
347
|
+
let res = 0n;
|
|
348
|
+
let bit = 1n << (BigInt.bitLength(value) & -2n);
|
|
349
|
+
while (bit !== 0n) {
|
|
350
|
+
if (value >= res + bit) {
|
|
351
|
+
value -= res + bit;
|
|
352
|
+
res = (res >> 1n) + bit;
|
|
353
|
+
} else res >>= 1n;
|
|
354
|
+
bit >>= 2n;
|
|
355
|
+
}
|
|
356
|
+
return res;
|
|
357
|
+
}
|
|
358
|
+
function isPerfectSquare(val) {
|
|
359
|
+
if (val < 0) return [false, 0n];
|
|
360
|
+
const root = isqrt(val);
|
|
361
|
+
return [root * root === val, root];
|
|
362
|
+
}
|
|
363
|
+
function sqrt(v, precision) {
|
|
364
|
+
if (v.n === "OVERFLOW") return v;
|
|
365
|
+
if (v.n < 0) throw new NotImplementedError("Square root of negative numbers is not implemented yet.");
|
|
366
|
+
if (v.n === 0n) return ZERO;
|
|
367
|
+
if (precision.format === "precise") {
|
|
368
|
+
const [nIsSquare, n] = isPerfectSquare(v.n);
|
|
369
|
+
const [dIsSquare, d] = isPerfectSquare(v.d);
|
|
370
|
+
if (nIsSquare && dIsSquare) {
|
|
371
|
+
const e = simplify({
|
|
372
|
+
n: v.e?.n ?? 1n,
|
|
373
|
+
d: (v.e?.d ?? 1n) * 2n
|
|
374
|
+
});
|
|
375
|
+
return simplify({
|
|
376
|
+
n,
|
|
377
|
+
d,
|
|
378
|
+
c: v.c,
|
|
379
|
+
e
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const precisionDigits = Math.max(10, (precision.maxDecimals ?? 30) * 3);
|
|
384
|
+
const scaleFactor = 10n ** BigInt(precisionDigits * 2);
|
|
385
|
+
return simplify({
|
|
386
|
+
n: isqrt(v.n * scaleFactor / v.d),
|
|
387
|
+
d: 10n ** BigInt(precisionDigits)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/lib/utils/exponentiate.ts
|
|
392
|
+
/**
|
|
393
|
+
* Maximum digits allowed for the result of exponentiation
|
|
394
|
+
*/
|
|
395
|
+
const MAX_EXPONENT_RESULT_DIGITS = 10000000n;
|
|
396
|
+
function exponentiate(left, right, precision) {
|
|
397
|
+
const rN = right.n;
|
|
398
|
+
if (rN === 0n) return ONE;
|
|
399
|
+
const lN = left.n;
|
|
400
|
+
if (lN === "OVERFLOW") return left;
|
|
401
|
+
const lD = left.d;
|
|
402
|
+
const c = left.c;
|
|
403
|
+
if (lN === lD && c === void 0) return ONE;
|
|
404
|
+
if (rN === "OVERFLOW") return right;
|
|
405
|
+
if (lN === -lD && c === void 0) {
|
|
406
|
+
if (rN % 2n) return {
|
|
407
|
+
n: -1n,
|
|
408
|
+
d: 1n
|
|
409
|
+
};
|
|
410
|
+
return ONE;
|
|
411
|
+
}
|
|
412
|
+
const normalizedExp = toSimpleFraction(simplify(right));
|
|
413
|
+
let exponent = normalizedExp.n;
|
|
414
|
+
if (lN === 0n) {
|
|
415
|
+
if (exponent < 0) throw new DivisionByZeroError();
|
|
416
|
+
return ZERO;
|
|
417
|
+
}
|
|
418
|
+
const dExp = normalizedExp.d;
|
|
419
|
+
if (dExp >= 100) {
|
|
420
|
+
let baseFloat = Number(lN) / Number(lD);
|
|
421
|
+
if (c) baseFloat *= Number(constants[c]);
|
|
422
|
+
const expFloat = Number(exponent) / Number(dExp);
|
|
423
|
+
const resultFloat = baseFloat ** expFloat;
|
|
424
|
+
if (!Number.isFinite(resultFloat)) return OverflowValue;
|
|
425
|
+
const [integerPart, fractionalPart] = resultFloat.toString().split(".");
|
|
426
|
+
if (!fractionalPart) return simplify({
|
|
427
|
+
n: BigInt(integerPart),
|
|
428
|
+
d: 1n
|
|
429
|
+
});
|
|
430
|
+
const denominator = 10n ** BigInt(fractionalPart.length);
|
|
431
|
+
return simplify({
|
|
432
|
+
n: BigInt(integerPart + fractionalPart),
|
|
433
|
+
d: denominator
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const eN = (left.e?.n ?? 1n) * exponent;
|
|
437
|
+
let baseN = lN;
|
|
438
|
+
let baseD = lD;
|
|
439
|
+
if (exponent < 0) {
|
|
440
|
+
exponent = -exponent;
|
|
441
|
+
[baseN, baseD] = [baseD, baseN];
|
|
442
|
+
}
|
|
443
|
+
const baseNDigits = BigInt.bitLength(baseN);
|
|
444
|
+
const baseDDigits = BigInt.bitLength(baseD);
|
|
445
|
+
if (baseNDigits * exponent > MAX_EXPONENT_RESULT_DIGITS || baseDDigits * exponent > MAX_EXPONENT_RESULT_DIGITS || (baseNDigits > 5e3 || baseDDigits > 5e3) && exponent > 10) return OverflowValue;
|
|
446
|
+
const e = c ? simplify({
|
|
447
|
+
n: eN,
|
|
448
|
+
d: left.e?.d ?? 1n
|
|
449
|
+
}) : void 0;
|
|
450
|
+
if (dExp === exponent) return {
|
|
451
|
+
n: baseN,
|
|
452
|
+
d: baseD,
|
|
453
|
+
c,
|
|
454
|
+
e
|
|
455
|
+
};
|
|
456
|
+
const v = {
|
|
457
|
+
n: baseN ** exponent,
|
|
458
|
+
d: baseD ** exponent,
|
|
459
|
+
c,
|
|
460
|
+
e
|
|
461
|
+
};
|
|
462
|
+
if (dExp === 1n) return v;
|
|
463
|
+
if (dExp === 2n) return sqrt(v, precision);
|
|
464
|
+
return nthRoot(v, dExp, precision);
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/lib/utils/factorial.ts
|
|
468
|
+
function binaryProduct(arr, low, high) {
|
|
469
|
+
if (low > high) return 1n;
|
|
470
|
+
if (low === high) return arr[low];
|
|
471
|
+
const mid = Math.floor((low + high) / 2);
|
|
472
|
+
return binaryProduct(arr, low, mid) * binaryProduct(arr, mid + 1, high);
|
|
473
|
+
}
|
|
474
|
+
function getPrimes(limit) {
|
|
475
|
+
const sieve = new Uint8Array(limit + 1).fill(1);
|
|
476
|
+
sieve[0] = sieve[1] = 0;
|
|
477
|
+
for (let i = 2; i * i <= limit; i++) if (sieve[i]) for (let j = i * i; j <= limit; j += i) sieve[j] = 0;
|
|
478
|
+
const result = [];
|
|
479
|
+
for (let i = 2; i <= limit; i++) if (sieve[i]) result.push(i);
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Calculates factorial.
|
|
484
|
+
*/
|
|
485
|
+
function factorial(n) {
|
|
486
|
+
if (n < 0) return null;
|
|
487
|
+
if (n === 0n || n === 1n) return 1n;
|
|
488
|
+
if (n < 1000n) {
|
|
489
|
+
let result = 1n;
|
|
490
|
+
for (let i = 2n; i <= n; i++) result *= i;
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
const primes = getPrimes(Number(n));
|
|
494
|
+
const primePowers = [];
|
|
495
|
+
for (const p of primes) {
|
|
496
|
+
let exponent = 0n;
|
|
497
|
+
let currentN = n;
|
|
498
|
+
const pBig = BigInt(p);
|
|
499
|
+
while (currentN >= pBig) {
|
|
500
|
+
currentN /= pBig;
|
|
501
|
+
exponent += currentN;
|
|
502
|
+
}
|
|
503
|
+
primePowers.push(pBig ** exponent);
|
|
504
|
+
}
|
|
505
|
+
return binaryProduct(primePowers, 0, primePowers.length - 1);
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/lib/utils/floor.ts
|
|
509
|
+
/**
|
|
510
|
+
* Returns the largest integer less than or equal to the value.
|
|
511
|
+
*/
|
|
512
|
+
function floor(v) {
|
|
513
|
+
const { n, d } = simplify(v);
|
|
514
|
+
if (d === 1n) return n;
|
|
515
|
+
const isNegative = n < 0;
|
|
516
|
+
const result = n / d;
|
|
517
|
+
const hasRemainder = n % d !== 0n;
|
|
518
|
+
return isNegative && hasRemainder ? result - 1n : result;
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/lib/utils/mod.ts
|
|
522
|
+
/**
|
|
523
|
+
* Calculates remainder of division.
|
|
524
|
+
*/
|
|
525
|
+
function mod(a, b) {
|
|
526
|
+
if (b.n === 0n) throw new DivisionByZeroError();
|
|
527
|
+
const d = a.d * b.d / gcd(a.d, b.d);
|
|
528
|
+
const n1 = a.n * (d / a.d);
|
|
529
|
+
const n2 = b.n * (d / b.d);
|
|
530
|
+
return {
|
|
531
|
+
n: (n1 % n2 + n2) % n2,
|
|
532
|
+
d
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/lib/interpreter.ts
|
|
537
|
+
const precedence = {
|
|
538
|
+
LPAREN: 0,
|
|
539
|
+
ABS_OPEN: 0,
|
|
540
|
+
ADD: 1,
|
|
541
|
+
SUBTRACT: 1,
|
|
542
|
+
MULTIPLY: 2,
|
|
543
|
+
DIVIDE: 2,
|
|
544
|
+
MOD: 2,
|
|
545
|
+
UNARY_PLUS: 4,
|
|
546
|
+
UNARY_MINUS: 4,
|
|
547
|
+
EXP: 6,
|
|
548
|
+
IMPLICIT_MUL: 6,
|
|
549
|
+
ABS_FN: 8,
|
|
550
|
+
CEIL_FN: 8,
|
|
551
|
+
FLOOR_FN: 8,
|
|
552
|
+
SQRT_FN: 8,
|
|
553
|
+
FACTORIAL: 10
|
|
554
|
+
};
|
|
555
|
+
function isUnaryOperation(op) {
|
|
556
|
+
return op === "UNARY_PLUS" || op === "UNARY_MINUS" || op === "ABS_FN" || op === "CEIL_FN" || op === "FLOOR_FN" || op === "SQRT_FN";
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Maximum allowed precision
|
|
560
|
+
*/
|
|
561
|
+
const MAX_PRECISION = 5e4;
|
|
562
|
+
/**
|
|
563
|
+
* Maximum allowed factorial
|
|
564
|
+
*/
|
|
565
|
+
const MAX_FACTORIAL = 2e4;
|
|
566
|
+
/**
|
|
567
|
+
* Maximum allowed exponent for scientific number notation
|
|
568
|
+
*/
|
|
569
|
+
const MAX_SCIENTIFIC_NUMBER_EXPONENT = 3e5;
|
|
570
|
+
function evaluate(tokens, options = {}) {
|
|
571
|
+
if (tokens.length === 0) throw new EmptyExpressionError();
|
|
572
|
+
const values = [];
|
|
573
|
+
const ops = [];
|
|
574
|
+
const applyOp = (pos) => {
|
|
575
|
+
const op = ops.pop();
|
|
576
|
+
if (!op || op === "LPAREN" || op === "ABS_OPEN") return;
|
|
577
|
+
const right = values.pop();
|
|
578
|
+
if (isUnaryOperation(op)) switch (op) {
|
|
579
|
+
case "UNARY_PLUS":
|
|
580
|
+
values.push(right);
|
|
581
|
+
return;
|
|
582
|
+
case "UNARY_MINUS": {
|
|
583
|
+
const rN = right.n;
|
|
584
|
+
if (rN === "OVERFLOW") {
|
|
585
|
+
values.push(OverflowValue);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
values.push({
|
|
589
|
+
...right,
|
|
590
|
+
n: -rN
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
case "ABS_FN": {
|
|
595
|
+
const rN = right.n;
|
|
596
|
+
if (rN === "OVERFLOW") {
|
|
597
|
+
values.push(OverflowValue);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const n = rN < 0 ? -rN : rN;
|
|
601
|
+
values.push({
|
|
602
|
+
...right,
|
|
603
|
+
n
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
case "CEIL_FN":
|
|
608
|
+
if (right.n === "OVERFLOW") {
|
|
609
|
+
values.push(OverflowValue);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
values.push({
|
|
613
|
+
n: ceil(toSimpleFraction(right)),
|
|
614
|
+
d: 1n
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
case "FLOOR_FN":
|
|
618
|
+
if (right.n === "OVERFLOW") {
|
|
619
|
+
values.push(OverflowValue);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
values.push({
|
|
623
|
+
n: floor(toSimpleFraction(right)),
|
|
624
|
+
d: 1n
|
|
625
|
+
});
|
|
626
|
+
return;
|
|
627
|
+
case "SQRT_FN":
|
|
628
|
+
values.push(sqrt(right, options));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (op === "FACTORIAL") {
|
|
632
|
+
if (right.n === "OVERFLOW") {
|
|
633
|
+
values.push(OverflowValue);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const reduced = simplify(right);
|
|
637
|
+
if (reduced.d !== 1n || reduced.n < 0) throw new NotImplementedError("Factorial is only implemented for non-negative integers.", pos);
|
|
638
|
+
if (reduced.n >= MAX_FACTORIAL) {
|
|
639
|
+
values.push(OverflowValue);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
values.push({
|
|
643
|
+
n: factorial(reduced.n),
|
|
644
|
+
d: 1n
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const left = values.pop();
|
|
649
|
+
switch (op) {
|
|
650
|
+
case "ADD":
|
|
651
|
+
case "SUBTRACT":
|
|
652
|
+
values.push(add(left, right, op === "SUBTRACT"));
|
|
653
|
+
return;
|
|
654
|
+
case "MULTIPLY":
|
|
655
|
+
case "IMPLICIT_MUL":
|
|
656
|
+
values.push(multiply(left, right));
|
|
657
|
+
return;
|
|
658
|
+
case "DIVIDE":
|
|
659
|
+
values.push(divide(left, right));
|
|
660
|
+
return;
|
|
661
|
+
case "MOD":
|
|
662
|
+
if (left.n === "OVERFLOW" || right.n === "OVERFLOW") {
|
|
663
|
+
values.push(OverflowValue);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
values.push(mod(left, right));
|
|
667
|
+
return;
|
|
668
|
+
case "EXP":
|
|
669
|
+
values.push(exponentiate(left, right, options));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const pushOpWithPrecedence = (currentOp, pos) => {
|
|
674
|
+
const isUnary = isUnaryOperation(currentOp);
|
|
675
|
+
const isRightAssociative = currentOp === "EXP" || currentOp === "IMPLICIT_MUL";
|
|
676
|
+
while (ops.length > 0 && !isUnary && (isRightAssociative ? precedence[ops[ops.length - 1]] > precedence[currentOp] : precedence[ops[ops.length - 1]] >= precedence[currentOp])) applyOp(pos);
|
|
677
|
+
ops.push(currentOp);
|
|
678
|
+
};
|
|
679
|
+
for (const token of tokens) switch (token.type) {
|
|
680
|
+
case "NUMBER": {
|
|
681
|
+
const frac = token.fraction || "";
|
|
682
|
+
const fl = frac.length;
|
|
683
|
+
if (fl > MAX_PRECISION) throw new MaximumPrecisionError(fl, MAX_PRECISION);
|
|
684
|
+
let n = BigInt(token.whole + frac);
|
|
685
|
+
let d = fl === 0 ? 1n : 10n ** BigInt(fl);
|
|
686
|
+
if (token.exponent) {
|
|
687
|
+
if (token.exponent.length > MAX_PRECISION) throw new MaximumPrecisionError(token.exponent.length, MAX_PRECISION);
|
|
688
|
+
const expValue = BigInt(token.exponent);
|
|
689
|
+
if (expValue > MAX_SCIENTIFIC_NUMBER_EXPONENT) {
|
|
690
|
+
values.push(OverflowValue);
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
if (expValue >= 0) n *= 10n ** expValue;
|
|
694
|
+
else d *= 10n ** -expValue;
|
|
695
|
+
}
|
|
696
|
+
if (d === 1n) values.push({
|
|
697
|
+
n,
|
|
698
|
+
d: 1n
|
|
699
|
+
});
|
|
700
|
+
else values.push({
|
|
701
|
+
n,
|
|
702
|
+
d
|
|
703
|
+
});
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case "CONST":
|
|
707
|
+
if (options.format === "precise") values.push({
|
|
708
|
+
n: 1n,
|
|
709
|
+
d: 1n,
|
|
710
|
+
c: token.id
|
|
711
|
+
});
|
|
712
|
+
else values.push(getConst(token.id));
|
|
713
|
+
break;
|
|
714
|
+
case "LPAREN":
|
|
715
|
+
ops.push("LPAREN");
|
|
716
|
+
break;
|
|
717
|
+
case "RPAREN": {
|
|
718
|
+
let foundMatch = false;
|
|
719
|
+
while (ops.length > 0) {
|
|
720
|
+
if (ops[ops.length - 1] === "LPAREN") {
|
|
721
|
+
foundMatch = true;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
applyOp(token.pos);
|
|
725
|
+
}
|
|
726
|
+
if (!foundMatch) throw new MismatchedParenthesisError(token.pos);
|
|
727
|
+
ops.pop();
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
case "ABS_OPEN":
|
|
731
|
+
ops.push("ABS_OPEN");
|
|
732
|
+
break;
|
|
733
|
+
case "ABS_CLOSE": {
|
|
734
|
+
while (ops.length > 0) {
|
|
735
|
+
if (ops[ops.length - 1] === "ABS_OPEN") break;
|
|
736
|
+
applyOp(token.pos);
|
|
737
|
+
}
|
|
738
|
+
ops.pop();
|
|
739
|
+
const val = values.pop();
|
|
740
|
+
if (val.n === "OVERFLOW") {
|
|
741
|
+
values.push(OverflowValue);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
values.push({
|
|
745
|
+
n: val.n < 0 ? -val.n : val.n,
|
|
746
|
+
d: val.d,
|
|
747
|
+
c: val.c
|
|
748
|
+
});
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case "PLUS":
|
|
752
|
+
pushOpWithPrecedence("ADD", token.pos);
|
|
753
|
+
break;
|
|
754
|
+
case "MINUS":
|
|
755
|
+
pushOpWithPrecedence("SUBTRACT", token.pos);
|
|
756
|
+
break;
|
|
757
|
+
case "MUL":
|
|
758
|
+
pushOpWithPrecedence("MULTIPLY", token.pos);
|
|
759
|
+
break;
|
|
760
|
+
case "DIV":
|
|
761
|
+
pushOpWithPrecedence("DIVIDE", token.pos);
|
|
762
|
+
break;
|
|
763
|
+
case "POW":
|
|
764
|
+
pushOpWithPrecedence("EXP", token.pos);
|
|
765
|
+
break;
|
|
766
|
+
case "UNARY_PLUS":
|
|
767
|
+
pushOpWithPrecedence("UNARY_PLUS", token.pos);
|
|
768
|
+
break;
|
|
769
|
+
case "UNARY_MINUS":
|
|
770
|
+
pushOpWithPrecedence("UNARY_MINUS", token.pos);
|
|
771
|
+
break;
|
|
772
|
+
case "IMPLICIT_MUL":
|
|
773
|
+
pushOpWithPrecedence("IMPLICIT_MUL", token.pos);
|
|
774
|
+
break;
|
|
775
|
+
case "FACTORIAL":
|
|
776
|
+
pushOpWithPrecedence("FACTORIAL", token.pos);
|
|
777
|
+
break;
|
|
778
|
+
case "MOD":
|
|
779
|
+
pushOpWithPrecedence("MOD", token.pos);
|
|
780
|
+
break;
|
|
781
|
+
case "FUNC":
|
|
782
|
+
switch (token.id) {
|
|
783
|
+
case "abs":
|
|
784
|
+
pushOpWithPrecedence("ABS_FN", token.pos);
|
|
785
|
+
break;
|
|
786
|
+
case "ceil":
|
|
787
|
+
pushOpWithPrecedence("CEIL_FN", token.pos);
|
|
788
|
+
break;
|
|
789
|
+
case "floor":
|
|
790
|
+
pushOpWithPrecedence("FLOOR_FN", token.pos);
|
|
791
|
+
break;
|
|
792
|
+
case "sqrt":
|
|
793
|
+
pushOpWithPrecedence("SQRT_FN", token.pos);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
while (ops.length > 0) {
|
|
799
|
+
const top = ops[ops.length - 1];
|
|
800
|
+
const lastPos = tokens[tokens.length - 1]?.pos ?? 0;
|
|
801
|
+
if (top === "LPAREN") {
|
|
802
|
+
ops.pop();
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
applyOp(lastPos);
|
|
806
|
+
}
|
|
807
|
+
const finalValue = values.pop();
|
|
808
|
+
if (finalValue.n === "OVERFLOW") throw new OverflowError();
|
|
809
|
+
return simplify(finalValue);
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/lib/lexer.ts
|
|
813
|
+
var LexerError = class extends GenericMathErrror {
|
|
814
|
+
constructor(message, pos) {
|
|
815
|
+
super("LexerError", message, pos);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
function isWhitespace(ch) {
|
|
819
|
+
return ch === " ";
|
|
820
|
+
}
|
|
821
|
+
function isDigit(ch) {
|
|
822
|
+
if (ch === void 0) return false;
|
|
823
|
+
return ch >= "0" && ch <= "9";
|
|
824
|
+
}
|
|
825
|
+
function isAlpha(ch) {
|
|
826
|
+
if (ch === void 0) return false;
|
|
827
|
+
return ch >= "a" && ch <= "z";
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Maximum allowed expression length
|
|
831
|
+
*/
|
|
832
|
+
const MAX_EXPRESSION_LENGTH = 75e6;
|
|
833
|
+
/**
|
|
834
|
+
* Simple lexer (tokenizer).
|
|
835
|
+
*/
|
|
836
|
+
function tokenize(expression, options = {}) {
|
|
837
|
+
const { decimalSeparator = "." } = options;
|
|
838
|
+
const tokens = [];
|
|
839
|
+
let index = 0;
|
|
840
|
+
const length = expression.length;
|
|
841
|
+
if (length > MAX_EXPRESSION_LENGTH) throw new LexerError(`Expression length (${length}) exceeds the maximum allowed limit of ${MAX_EXPRESSION_LENGTH}`, 0);
|
|
842
|
+
while (index < length) {
|
|
843
|
+
const ch = expression[index];
|
|
844
|
+
const startPos = index;
|
|
845
|
+
if (isWhitespace(ch)) {
|
|
846
|
+
index++;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (isDigit(ch) || ch === decimalSeparator) {
|
|
850
|
+
let whole = "";
|
|
851
|
+
let fraction = void 0;
|
|
852
|
+
let exponent = void 0;
|
|
853
|
+
const startIdx = index;
|
|
854
|
+
if (ch === decimalSeparator) {
|
|
855
|
+
whole = "0";
|
|
856
|
+
index++;
|
|
857
|
+
const fracStart = index;
|
|
858
|
+
while (index < length && isDigit(expression[index])) index++;
|
|
859
|
+
if (index === fracStart) throw new LexerError("Expected digit after decimal separator", index);
|
|
860
|
+
fraction = expression.slice(fracStart, index);
|
|
861
|
+
} else {
|
|
862
|
+
while (index < length && isDigit(expression[index])) index++;
|
|
863
|
+
whole = expression.slice(startIdx, index);
|
|
864
|
+
if (index < length && expression[index] === decimalSeparator) {
|
|
865
|
+
index++;
|
|
866
|
+
const fracStart = index;
|
|
867
|
+
while (index < length && isDigit(expression[index])) index++;
|
|
868
|
+
fraction = expression.slice(fracStart, index);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (index < length && (expression[index] === "e" || expression[index] === "E")) {
|
|
872
|
+
let lookahead = index + 1;
|
|
873
|
+
let sign = "";
|
|
874
|
+
const next = expression[lookahead];
|
|
875
|
+
if (lookahead < length && (next === "+" || next === "-")) {
|
|
876
|
+
sign = next;
|
|
877
|
+
lookahead++;
|
|
878
|
+
}
|
|
879
|
+
if (lookahead < length && isDigit(expression[lookahead])) {
|
|
880
|
+
index = lookahead;
|
|
881
|
+
let expBuffer = sign;
|
|
882
|
+
while (index < length && isDigit(expression[index])) {
|
|
883
|
+
expBuffer += expression[index];
|
|
884
|
+
index++;
|
|
885
|
+
}
|
|
886
|
+
exponent = expBuffer;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (index < length && expression[index] === decimalSeparator) throw new LexerError("Invalid decimal number format", index);
|
|
890
|
+
tokens.push({
|
|
891
|
+
type: "NUMBER",
|
|
892
|
+
whole,
|
|
893
|
+
fraction,
|
|
894
|
+
exponent,
|
|
895
|
+
pos: startPos
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (isAlpha(ch)) {
|
|
900
|
+
let id = "";
|
|
901
|
+
while (index < length && isAlpha(expression[index])) id += expression[index++];
|
|
902
|
+
tokens.push({
|
|
903
|
+
type: "IDENTIFIER",
|
|
904
|
+
id,
|
|
905
|
+
pos: startPos
|
|
906
|
+
});
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (ch === "(") {
|
|
910
|
+
tokens.push({
|
|
911
|
+
type: "LPAREN",
|
|
912
|
+
pos: startPos
|
|
913
|
+
});
|
|
914
|
+
index++;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (ch === ")") {
|
|
918
|
+
tokens.push({
|
|
919
|
+
type: "RPAREN",
|
|
920
|
+
pos: startPos
|
|
921
|
+
});
|
|
922
|
+
index++;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (ch === "+") {
|
|
926
|
+
tokens.push({
|
|
927
|
+
type: "PLUS",
|
|
928
|
+
pos: startPos
|
|
929
|
+
});
|
|
930
|
+
index++;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (ch === "-") {
|
|
934
|
+
tokens.push({
|
|
935
|
+
type: "MINUS",
|
|
936
|
+
pos: startPos
|
|
937
|
+
});
|
|
938
|
+
index++;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (ch === "*") {
|
|
942
|
+
tokens.push({
|
|
943
|
+
type: "MUL",
|
|
944
|
+
pos: startPos
|
|
945
|
+
});
|
|
946
|
+
index++;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (ch === "/") {
|
|
950
|
+
tokens.push({
|
|
951
|
+
type: "DIV",
|
|
952
|
+
pos: startPos
|
|
953
|
+
});
|
|
954
|
+
index++;
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (ch === "^") {
|
|
958
|
+
tokens.push({
|
|
959
|
+
type: "POW",
|
|
960
|
+
pos: startPos
|
|
961
|
+
});
|
|
962
|
+
index++;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (ch === "!") {
|
|
966
|
+
tokens.push({
|
|
967
|
+
type: "FACTORIAL",
|
|
968
|
+
pos: startPos
|
|
969
|
+
});
|
|
970
|
+
index++;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (ch === "|") {
|
|
974
|
+
tokens.push({
|
|
975
|
+
type: "PIPE",
|
|
976
|
+
pos: startPos
|
|
977
|
+
});
|
|
978
|
+
index++;
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (ch === "%") {
|
|
982
|
+
tokens.push({
|
|
983
|
+
type: "MOD",
|
|
984
|
+
pos: startPos
|
|
985
|
+
});
|
|
986
|
+
index++;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
throw new LexerError(`Unexpected character '${ch}'`, index);
|
|
990
|
+
}
|
|
991
|
+
return tokens;
|
|
992
|
+
}
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/lib/symbol.ts
|
|
995
|
+
const symbolMap = {
|
|
996
|
+
PLUS: "+",
|
|
997
|
+
UNARY_PLUS: "+",
|
|
998
|
+
MINUS: "-",
|
|
999
|
+
UNARY_MINUS: "-",
|
|
1000
|
+
MUL: "*",
|
|
1001
|
+
DIV: "/",
|
|
1002
|
+
LPAREN: "(",
|
|
1003
|
+
RPAREN: ")",
|
|
1004
|
+
POW: "^",
|
|
1005
|
+
IMPLICIT_MUL: "",
|
|
1006
|
+
FACTORIAL: "!",
|
|
1007
|
+
ABS_CLOSE: "|",
|
|
1008
|
+
ABS_OPEN: "|",
|
|
1009
|
+
PIPE: "|",
|
|
1010
|
+
MOD: "%"
|
|
1011
|
+
};
|
|
1012
|
+
const prettifyNumber = (t) => t.fraction ? `${t.whole}.${t.fraction}` : t.whole;
|
|
1013
|
+
function getSym(t) {
|
|
1014
|
+
if (t.type === "NUMBER") return prettifyNumber(t);
|
|
1015
|
+
if (t.type === "IDENTIFIER") return t.id;
|
|
1016
|
+
return symbolMap[t.type];
|
|
1017
|
+
}
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/lib/parser.ts
|
|
1020
|
+
/**
|
|
1021
|
+
* Helper: Is the last token an "operand" (something that can be followed by a closing pipe or implicit mul)?
|
|
1022
|
+
*/
|
|
1023
|
+
function isOperand(last) {
|
|
1024
|
+
if (!last) return false;
|
|
1025
|
+
return last.type === "NUMBER" || last.type === "CONST" || last.type === "RPAREN" || last.type === "ABS_CLOSE" || last.type === "FACTORIAL";
|
|
1026
|
+
}
|
|
1027
|
+
function isBinaryOperator(token) {
|
|
1028
|
+
return token.type === "MUL" || token.type === "DIV" || token.type === "POW" || token.type === "MOD";
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Helper: Does the current context allow a +/- to be unary?
|
|
1032
|
+
*/
|
|
1033
|
+
function isUnaryContext(last) {
|
|
1034
|
+
return !isOperand(last);
|
|
1035
|
+
}
|
|
1036
|
+
var ParserError = class extends GenericMathErrror {
|
|
1037
|
+
constructor(message, pos) {
|
|
1038
|
+
super("ParserError", message, pos);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
var IncompleteExpressionError = class extends ParserError {
|
|
1042
|
+
constructor(message, pos) {
|
|
1043
|
+
super(`Incomplete expression: ${message}`, pos);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
var UnexpectedOperatorError = class extends ParserError {
|
|
1047
|
+
constructor(message, pos) {
|
|
1048
|
+
super(`Unexpected operator: ${message}`, pos);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
function resolveIdentifier(token) {
|
|
1052
|
+
if (token.id === "pi" || token.id === "e") return {
|
|
1053
|
+
type: "CONST",
|
|
1054
|
+
id: token.id,
|
|
1055
|
+
pos: token.pos
|
|
1056
|
+
};
|
|
1057
|
+
if (token.id === "abs" || token.id === "ceil" || token.id === "floor" || token.id === "sqrt") return {
|
|
1058
|
+
type: "FUNC",
|
|
1059
|
+
id: token.id,
|
|
1060
|
+
pos: token.pos
|
|
1061
|
+
};
|
|
1062
|
+
throw new ParserError(`Unknown identifier '${token.id}'`, token.pos);
|
|
1063
|
+
}
|
|
1064
|
+
function parse(tokens) {
|
|
1065
|
+
if (tokens.length === 0) return [];
|
|
1066
|
+
const result = [];
|
|
1067
|
+
let absStack = 0;
|
|
1068
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1069
|
+
let token = tokens[i];
|
|
1070
|
+
const prev = tokens[i - 1];
|
|
1071
|
+
if (token.type === "IDENTIFIER") token = resolveIdentifier(token);
|
|
1072
|
+
let prevParsed = result[result.length - 1];
|
|
1073
|
+
if (token.type === "PIPE") {
|
|
1074
|
+
if (absStack > 0 && isOperand(prevParsed)) {
|
|
1075
|
+
result.push({
|
|
1076
|
+
type: "ABS_CLOSE",
|
|
1077
|
+
pos: token.pos
|
|
1078
|
+
});
|
|
1079
|
+
absStack--;
|
|
1080
|
+
} else {
|
|
1081
|
+
if (isOperand(prevParsed)) result.push({
|
|
1082
|
+
type: "IMPLICIT_MUL",
|
|
1083
|
+
pos: token.pos
|
|
1084
|
+
});
|
|
1085
|
+
result.push({
|
|
1086
|
+
type: "ABS_OPEN",
|
|
1087
|
+
pos: token.pos
|
|
1088
|
+
});
|
|
1089
|
+
absStack++;
|
|
1090
|
+
}
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (isOperand(prevParsed)) {
|
|
1094
|
+
if (token.type === "LPAREN" || token.type === "FUNC" || token.type === "NUMBER" || token.type === "CONST") result.push({
|
|
1095
|
+
type: "IMPLICIT_MUL",
|
|
1096
|
+
pos: token.pos
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
prevParsed = result[result.length - 1];
|
|
1100
|
+
if (isBinaryOperator(token)) {
|
|
1101
|
+
if (isUnaryContext(prevParsed)) throw new UnexpectedOperatorError(`Unexpected operator '${getSym(token)}'`, token.pos);
|
|
1102
|
+
}
|
|
1103
|
+
if (token.type === "FACTORIAL") {
|
|
1104
|
+
if (!isOperand(prevParsed) && prevParsed?.type !== "RPAREN") throw new UnexpectedOperatorError("Unexpected factorial operator", token.pos);
|
|
1105
|
+
}
|
|
1106
|
+
if (token.type === "RPAREN") {
|
|
1107
|
+
if (prevParsed && (prevParsed.type === "PLUS" || prevParsed.type === "MINUS" || isBinaryOperator(prevParsed))) throw new UnexpectedOperatorError(`Unexpected ')' after operator '${getSym(prevParsed)}'`, token.pos);
|
|
1108
|
+
if (prevParsed?.type === "LPAREN") throw new UnexpectedOperatorError("Unexpected ')' after '('", token.pos);
|
|
1109
|
+
}
|
|
1110
|
+
if (prev && (prev.type === "NUMBER" || prev.type === "IDENTIFIER" && resolveIdentifier(prev).type === "CONST") && token.type === "NUMBER") throw new ParserError("Missing operator between numbers", token.pos);
|
|
1111
|
+
if ((token.type === "PLUS" || token.type === "MINUS") && isUnaryContext(prevParsed)) result.push({
|
|
1112
|
+
type: token.type === "PLUS" ? "UNARY_PLUS" : "UNARY_MINUS",
|
|
1113
|
+
pos: token.pos
|
|
1114
|
+
});
|
|
1115
|
+
else result.push(token);
|
|
1116
|
+
}
|
|
1117
|
+
if (absStack > 0) throw new ParserError("Unclosed absolute value '|'", result[result.length - 1]?.pos ?? 0);
|
|
1118
|
+
const last = result[result.length - 1];
|
|
1119
|
+
if (last && last.type !== "CONST" && (isBinaryOperator(last) || [
|
|
1120
|
+
"PLUS",
|
|
1121
|
+
"MINUS",
|
|
1122
|
+
"UNARY_PLUS",
|
|
1123
|
+
"UNARY_MINUS",
|
|
1124
|
+
"FUNC",
|
|
1125
|
+
"ABS_OPEN",
|
|
1126
|
+
"LPAREN"
|
|
1127
|
+
].includes(last.type))) throw new IncompleteExpressionError(`trailing operator '${last.type === "FUNC" ? last.id : getSym(last)}'`, last.pos);
|
|
1128
|
+
return result;
|
|
1129
|
+
}
|
|
1130
|
+
//#endregion
|
|
1131
|
+
//#region src/utils/format-result.ts
|
|
1132
|
+
function getConstantStr(coeff, c, e) {
|
|
1133
|
+
if (coeff === 0n) return "0";
|
|
1134
|
+
if (!c) return `${coeff}`;
|
|
1135
|
+
const isOne = e && e.n === 1n && (e.d === void 0 || e.d === 1n);
|
|
1136
|
+
if (!e || isOne) {
|
|
1137
|
+
if (coeff === 1n) return c;
|
|
1138
|
+
if (coeff === -1n) return `-${c}`;
|
|
1139
|
+
return `${coeff}${c}`;
|
|
1140
|
+
}
|
|
1141
|
+
const num = e.n;
|
|
1142
|
+
const den = e.d ?? 1n;
|
|
1143
|
+
const expStr = den === 1n ? `${num}` : `(${num}/${den})`;
|
|
1144
|
+
const constantStr = den === 2n * num ? `sqrt(${c})` : `${c}^${expStr}`;
|
|
1145
|
+
if (coeff === 1n) return constantStr;
|
|
1146
|
+
if (coeff === -1n) return `-${constantStr}`;
|
|
1147
|
+
return `${coeff}${constantStr}`;
|
|
1148
|
+
}
|
|
1149
|
+
function formatPrecise(v) {
|
|
1150
|
+
const { n, d, c, e } = v;
|
|
1151
|
+
if (n === 0n) return "0";
|
|
1152
|
+
const isNegative = n < 0;
|
|
1153
|
+
const absN = isNegative ? -n : n;
|
|
1154
|
+
const numSign = isNegative ? "-" : "";
|
|
1155
|
+
if (c && e && e.n < 0n) return `${numSign}${absN}/${getConstantStr(d, c, {
|
|
1156
|
+
n: -e.n,
|
|
1157
|
+
d: e.d
|
|
1158
|
+
})}`;
|
|
1159
|
+
const numeratorStr = getConstantStr(absN, c, e);
|
|
1160
|
+
if (d === 1n) return `${numSign}${numeratorStr}`;
|
|
1161
|
+
return `${numSign}${numeratorStr}/${d}`;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Converts Result to a Decimal or Fraction String.
|
|
1165
|
+
*/
|
|
1166
|
+
function formatResult(v, options = {}) {
|
|
1167
|
+
const { n, d } = v;
|
|
1168
|
+
if (d === 0n) return "NaN";
|
|
1169
|
+
if (options.format === "precise") return formatPrecise(v);
|
|
1170
|
+
const { maxDecimals = 30, roundingMode = "round" } = options;
|
|
1171
|
+
const isNegative = n < 0;
|
|
1172
|
+
const absN = isNegative ? -n : n;
|
|
1173
|
+
let integerPart = (absN / d).toString();
|
|
1174
|
+
let remainder = absN % d;
|
|
1175
|
+
if (remainder === 0n) {
|
|
1176
|
+
const res = (isNegative ? "-" : "") + integerPart;
|
|
1177
|
+
return res === "-0" ? "0" : res;
|
|
1178
|
+
}
|
|
1179
|
+
let fractionalPart = "";
|
|
1180
|
+
let count = 0;
|
|
1181
|
+
const targetLength = maxDecimals + 1;
|
|
1182
|
+
while (remainder !== 0n && count < targetLength) {
|
|
1183
|
+
remainder *= 10n;
|
|
1184
|
+
fractionalPart += (remainder / d).toString();
|
|
1185
|
+
remainder %= d;
|
|
1186
|
+
count++;
|
|
1187
|
+
}
|
|
1188
|
+
const nexDigitStr = fractionalPart[maxDecimals];
|
|
1189
|
+
if (nexDigitStr) {
|
|
1190
|
+
const nextDigit = parseInt(nexDigitStr, 10);
|
|
1191
|
+
fractionalPart = fractionalPart.slice(0, maxDecimals);
|
|
1192
|
+
if (roundingMode === "round" && nextDigit >= 5) {
|
|
1193
|
+
const incremented = (BigInt(fractionalPart) + 1n).toString();
|
|
1194
|
+
if (incremented.length > fractionalPart.length) {
|
|
1195
|
+
integerPart = (BigInt(integerPart) + 1n).toString();
|
|
1196
|
+
fractionalPart = incremented.slice(1);
|
|
1197
|
+
} else fractionalPart = incremented.padStart(fractionalPart.length, "0");
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
let lastNonZero = -1;
|
|
1201
|
+
for (let i = fractionalPart.length - 1; i >= 0; i--) if (fractionalPart[i] !== "0") {
|
|
1202
|
+
lastNonZero = i;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
const finalFraction = lastNonZero === -1 ? "" : fractionalPart.slice(0, lastNonZero + 1);
|
|
1206
|
+
const result = finalFraction === "" ? integerPart : `${integerPart}.${finalFraction}`;
|
|
1207
|
+
return (isNegative && result !== "0" ? "-" : "") + result;
|
|
1208
|
+
}
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region src/index.ts
|
|
1211
|
+
/**
|
|
1212
|
+
* The main entry point for the library.
|
|
1213
|
+
*/
|
|
1214
|
+
function calculate(expression, options) {
|
|
1215
|
+
return formatResult(evaluate(parse(tokenize(expression, options)), options), options);
|
|
1216
|
+
}
|
|
1217
|
+
//#endregion
|
|
1218
|
+
export { calculate };
|
package/package.json
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocalc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/picocalc/math-parser.git"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"imports": {
|
|
14
|
+
"#src": "./src/index.ts",
|
|
15
|
+
"#lib/*": "./src/lib/*.ts"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.mjs",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"bench": "bun --cpu-prof-md --cpu-prof-dir ./bench/profiles bench/index.ts",
|
|
23
|
+
"test": "bun test --dots",
|
|
24
|
+
"lint": "oxlint",
|
|
25
|
+
"format": "oxfmt",
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"prepublishOnly": "bun run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"bigint.bitlength": "0.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@gameroman/config": "0.2.2",
|
|
34
|
+
"@types/bun": "1.3.14",
|
|
35
|
+
"oxfmt": "0.57.0",
|
|
36
|
+
"oxlint": "1.72.0",
|
|
37
|
+
"oxlint-tsgolint": "0.24.0",
|
|
38
|
+
"tsdown": "0.22.3",
|
|
39
|
+
"typescript": "6.0.3"
|
|
40
|
+
}
|
|
4
41
|
}
|