picocalc 0.0.0 → 0.0.1

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 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
@@ -0,0 +1,16 @@
1
+ //#region src/lib/interpreter.d.ts
2
+ interface PrecisionOptions {
3
+ format?: "decimal" | "precise";
4
+ maxDecimals?: number;
5
+ }
6
+ //#endregion
7
+ //#region src/index.d.ts
8
+ interface CalculateOptions extends PrecisionOptions {
9
+ decimalSeparator?: "." | ",";
10
+ }
11
+ /**
12
+ * The main entry point for the library.
13
+ */
14
+ declare function calculate(expression: string, options?: CalculateOptions): string;
15
+ //#endregion
16
+ export { calculate };
package/dist/index.mjs ADDED
@@ -0,0 +1,1071 @@
1
+ //#region src/lib/constants.ts
2
+ const PI = Math.PI.toString();
3
+ const constants = {
4
+ e: Math.E.toString(),
5
+ pi: PI
6
+ };
7
+ //#endregion
8
+ //#region src/lib/errors.ts
9
+ var GenericMathErrror = class extends Error {
10
+ pos;
11
+ constructor(name, message, pos) {
12
+ if (pos === void 0) super(`${name}: ${message}`);
13
+ else super(`${name}: ${message} at position ${pos}`);
14
+ this.pos = pos;
15
+ this.name = name;
16
+ }
17
+ };
18
+ var LexerError = class extends GenericMathErrror {
19
+ constructor(message, pos) {
20
+ super("LexerError", message, pos);
21
+ }
22
+ };
23
+ var InterpreterError = class extends GenericMathErrror {
24
+ constructor(message, pos) {
25
+ super("InterpreterError", message, pos);
26
+ }
27
+ };
28
+ var MaximumPrecisionError = class extends InterpreterError {
29
+ constructor(precision, maxPrecision) {
30
+ super(`Exceeded maximum precision of ${maxPrecision} digits (${precision})`);
31
+ }
32
+ };
33
+ var OverflowError = class extends InterpreterError {
34
+ constructor() {
35
+ super("Overflow");
36
+ }
37
+ };
38
+ var DivisionByZeroError = class extends InterpreterError {
39
+ constructor() {
40
+ super("Division by zero");
41
+ }
42
+ };
43
+ var EmptyExpressionError = class extends InterpreterError {
44
+ constructor() {
45
+ super("Empty expression");
46
+ }
47
+ };
48
+ var UnexpectedEndOfExpressionError = class extends InterpreterError {
49
+ constructor() {
50
+ super("Unexpected end of expression");
51
+ }
52
+ };
53
+ var MismatchedParenthesisError = class extends InterpreterError {
54
+ constructor(pos, message = "Mismatched parenthesis") {
55
+ super(message, pos);
56
+ }
57
+ };
58
+ var InsufficientOperandsError = class extends InterpreterError {
59
+ constructor(pos) {
60
+ super("Insufficient operands for operation", pos);
61
+ }
62
+ };
63
+ var ParserError = class extends GenericMathErrror {
64
+ constructor(message, pos) {
65
+ super("ParserError", message, pos);
66
+ }
67
+ };
68
+ var IncompleteExpressionError = class extends ParserError {
69
+ constructor(message, pos) {
70
+ super(`Incomplete expression: ${message}`, pos);
71
+ }
72
+ };
73
+ //#endregion
74
+ //#region src/lib/utils/gcd.ts
75
+ /**
76
+ * Fast Binary GCD (Stein's 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 < 0n ? -a : a;
84
+ b = b < 0n ? -b : b;
85
+ let shift = 0n;
86
+ while (((a | b) & 1n) === 0n) {
87
+ a >>= 1n;
88
+ b >>= 1n;
89
+ shift++;
90
+ }
91
+ while ((a & 1n) === 0n) a >>= 1n;
92
+ do {
93
+ while ((b & 1n) === 0n) b >>= 1n;
94
+ if (a > b) [a, b] = [b, a];
95
+ b = b - a;
96
+ } while (b !== 0n);
97
+ return a << shift;
98
+ }
99
+ //#endregion
100
+ //#region src/lib/utils/simplify.ts
101
+ /**
102
+ * Reduces a fraction to its simplest form.
103
+ */
104
+ function simplify(v) {
105
+ if (v.d === 0n) throw new DivisionByZeroError();
106
+ if (v.n === 0n) return {
107
+ n: 0n,
108
+ d: 1n,
109
+ c: v.c
110
+ };
111
+ if (v.d === 1n) return v;
112
+ const common = gcd(v.n, v.d);
113
+ const sign = v.d < 0n ? -1n : 1n;
114
+ return {
115
+ n: v.n / common * sign,
116
+ d: v.d / common * sign,
117
+ c: v.c
118
+ };
119
+ }
120
+ //#endregion
121
+ //#region src/lib/utils/ceil.ts
122
+ /**
123
+ * Returns the smallest integer greater than or equal to a Value.
124
+ */
125
+ function ceil(v) {
126
+ const { n, d } = simplify(v);
127
+ if (d === 1n) return n;
128
+ const isPositive = n > 0n;
129
+ const result = n / d;
130
+ const hasRemainder = n % d !== 0n;
131
+ return isPositive && hasRemainder ? result + 1n : result;
132
+ }
133
+ //#endregion
134
+ //#region src/lib/utils/factorial.ts
135
+ function binaryProduct(arr, low, high) {
136
+ if (low > high) return 1n;
137
+ if (low === high) return arr[low];
138
+ const mid = Math.floor((low + high) / 2);
139
+ return binaryProduct(arr, low, mid) * binaryProduct(arr, mid + 1, high);
140
+ }
141
+ function getPrimes(limit) {
142
+ const sieve = new Uint8Array(limit + 1).fill(1);
143
+ sieve[0] = sieve[1] = 0;
144
+ for (let i = 2; i * i <= limit; i++) if (sieve[i]) for (let j = i * i; j <= limit; j += i) sieve[j] = 0;
145
+ const result = [];
146
+ for (let i = 2; i <= limit; i++) if (sieve[i]) result.push(i);
147
+ return result;
148
+ }
149
+ /**
150
+ * Calculates factorial.
151
+ */
152
+ function factorial(n) {
153
+ if (n < 0n) return null;
154
+ if (n === 0n || n === 1n) return 1n;
155
+ if (n < 1000n) {
156
+ let result = 1n;
157
+ for (let i = 2n; i <= n; i++) result *= i;
158
+ return result;
159
+ }
160
+ const primes = getPrimes(Number(n));
161
+ const primePowers = [];
162
+ for (const p of primes) {
163
+ let exponent = 0n;
164
+ let currentN = n;
165
+ const pBig = BigInt(p);
166
+ while (currentN >= pBig) {
167
+ currentN /= pBig;
168
+ exponent += currentN;
169
+ }
170
+ primePowers.push(pBig ** exponent);
171
+ }
172
+ return binaryProduct(primePowers, 0, primePowers.length - 1);
173
+ }
174
+ //#endregion
175
+ //#region src/lib/utils/floor.ts
176
+ /**
177
+ * Returns the largest integer less than or equal to a Value.
178
+ */
179
+ function floor(v) {
180
+ const { n, d } = simplify(v);
181
+ if (d === 1n) return n;
182
+ const isNegative = n < 0n;
183
+ const result = n / d;
184
+ const hasRemainder = n % d !== 0n;
185
+ return isNegative && hasRemainder ? result - 1n : result;
186
+ }
187
+ //#endregion
188
+ //#region src/lib/utils/mod.ts
189
+ /**
190
+ * Calculates remainder of division.
191
+ */
192
+ function mod(a, b) {
193
+ const d = a.d * b.d / gcd(a.d, b.d);
194
+ const n1 = a.n * (d / a.d);
195
+ const n2 = b.n * (d / b.d);
196
+ return {
197
+ n: (n1 % n2 + n2) % n2,
198
+ d
199
+ };
200
+ }
201
+ //#endregion
202
+ //#region src/lib/utils/multiply.ts
203
+ function multiply(a, b) {
204
+ if (a.n === 0n || b.n === 0n) return {
205
+ n: 0n,
206
+ d: 1n
207
+ };
208
+ let n;
209
+ let d;
210
+ let c;
211
+ if (a.d === 1n && b.d === 1n) {
212
+ n = a.n * b.n;
213
+ d = 1n;
214
+ } else {
215
+ const g1 = gcd(a.n, b.d);
216
+ const g2 = gcd(b.n, a.d);
217
+ n = a.n / g1 * (b.n / g2);
218
+ d = a.d / g2 * (b.d / g1);
219
+ }
220
+ if (a.c === void 0 && b.c !== void 0) c = b.c;
221
+ else if (a.c !== void 0 && b.c === void 0) c = a.c;
222
+ return {
223
+ n,
224
+ d,
225
+ c
226
+ };
227
+ }
228
+ //#endregion
229
+ //#region src/lib/utils/nthroot.ts
230
+ /**
231
+ * Calculates the integer nth root of a BigInt.
232
+ */
233
+ function iNthRoot(value, n) {
234
+ if (value < 0n && n % 2n === 0n) throw new InterpreterError("Even root of negative number is not supported yet.");
235
+ if (value < 0n) return -iNthRoot(-value, n);
236
+ if (value < 2n) return value;
237
+ if (n === 1n) return value;
238
+ let x = 1n << BigInt(value.toString(2).length) / n + 1n;
239
+ const nMinus1 = n - 1n;
240
+ while (true) {
241
+ const nextX = (nMinus1 * x + value / x ** nMinus1) / n;
242
+ if (nextX >= x) break;
243
+ x = nextX;
244
+ }
245
+ return x;
246
+ }
247
+ /**
248
+ * Calculates the n-th root of a Value (fraction).
249
+ */
250
+ function nthRoot(v, root, precise = false, precisionDigits = 100) {
251
+ if (root === 0n) throw new DivisionByZeroError();
252
+ if (root === 1n) return v;
253
+ if (v.n < 0n) {
254
+ if (root % 2n === 0n) throw new InterpreterError(`${root}-th root of negative not supported.`);
255
+ const positiveResult = nthRoot({
256
+ n: -v.n,
257
+ d: v.d
258
+ }, root, precise, precisionDigits);
259
+ return {
260
+ n: -positiveResult.n,
261
+ d: positiveResult.d
262
+ };
263
+ }
264
+ if (v.n === 0n) return {
265
+ n: 0n,
266
+ d: 1n
267
+ };
268
+ if (precise) {
269
+ const rootN = iNthRoot(v.n, root);
270
+ const rootD = iNthRoot(v.d, root);
271
+ if (rootN ** root === v.n && rootD ** root === v.d) return simplify({
272
+ n: rootN,
273
+ d: rootD
274
+ });
275
+ }
276
+ const scaleFactor = 10n ** (BigInt(precisionDigits) * root);
277
+ return simplify({
278
+ n: iNthRoot(v.n * scaleFactor / v.d, root),
279
+ d: 10n ** BigInt(precisionDigits)
280
+ });
281
+ }
282
+ //#endregion
283
+ //#region src/lib/utils/sqrt.ts
284
+ /**
285
+ * Calculates the integer square root of a BigInt.
286
+ */
287
+ function isqrt(value) {
288
+ if (value < 0n) throw new InterpreterError("Square root of negative not supported yet.");
289
+ if (value < 2n) return value;
290
+ let res = 0n;
291
+ let bit = 1n << BigInt(value.toString(2).length - 1 & -2);
292
+ while (bit !== 0n) {
293
+ if (value >= res + bit) {
294
+ value -= res + bit;
295
+ res = (res >> 1n) + bit;
296
+ } else res >>= 1n;
297
+ bit >>= 2n;
298
+ }
299
+ return res;
300
+ }
301
+ function isPerfectSquare(val) {
302
+ if (val < 0n) return [false, 0n];
303
+ const root = isqrt(val);
304
+ return [root * root === val, root];
305
+ }
306
+ function sqrt(v, precise = false, precisionDigits = 100) {
307
+ if (v.n < 0n) throw new InterpreterError("Square root of negative not supported yet.");
308
+ if (v.n === 0n) return {
309
+ n: 0n,
310
+ d: 1n
311
+ };
312
+ if (precise) {
313
+ const [nIsSquare, nRoot] = isPerfectSquare(v.n);
314
+ const [dIsSquare, dRoot] = isPerfectSquare(v.d);
315
+ if (nIsSquare && dIsSquare) return simplify({
316
+ n: nRoot,
317
+ d: dRoot
318
+ });
319
+ }
320
+ const scaleFactor = 10n ** BigInt(precisionDigits * 2);
321
+ return simplify({
322
+ n: isqrt(v.n * scaleFactor / v.d),
323
+ d: 10n ** BigInt(precisionDigits)
324
+ });
325
+ }
326
+ //#endregion
327
+ //#region src/lib/interpreter.ts
328
+ const precedence = {
329
+ LPAREN: 0,
330
+ ABS_OPEN: 0,
331
+ ADD: 1,
332
+ SUBTRACT: 1,
333
+ MULTIPLY: 2,
334
+ DIVIDE: 2,
335
+ MOD: 2,
336
+ UNARY_PLUS: 4,
337
+ UNARY_MINUS: 4,
338
+ EXP: 6,
339
+ IMPLICIT_MUL: 6,
340
+ ABS_FN: 8,
341
+ CEIL_FN: 8,
342
+ FLOOR_FN: 8,
343
+ SQRT_FN: 8,
344
+ FACTORIAL: 10
345
+ };
346
+ function isUnaryOperation(op) {
347
+ return op === "UNARY_PLUS" || op === "UNARY_MINUS" || op === "ABS_FN" || op === "CEIL_FN" || op === "FLOOR_FN" || op === "SQRT_FN";
348
+ }
349
+ function getConst(id) {
350
+ const c = constants[id];
351
+ return {
352
+ n: BigInt(c.replace(".", "")),
353
+ d: 10n ** BigInt(c.length - c.indexOf(".") - 1)
354
+ };
355
+ }
356
+ /**
357
+ * Maximum allowed precision
358
+ */
359
+ const MAX_PRECISION = 5e4;
360
+ /**
361
+ * Threshold to prevent denominators from growing infinitely.
362
+ */
363
+ const SIMPLIFY_THRESHOLD = 10n ** 4000n;
364
+ function evaluate(tokens, options = {}) {
365
+ if (tokens.length === 0) throw new EmptyExpressionError();
366
+ const { format } = options;
367
+ const values = [];
368
+ const ops = [];
369
+ const applyOp = (pos) => {
370
+ const op = ops.pop();
371
+ if (!op || op === "LPAREN" || op === "ABS_OPEN") return;
372
+ const right = values.pop();
373
+ if (right === void 0) throw new UnexpectedEndOfExpressionError();
374
+ if (isUnaryOperation(op)) switch (op) {
375
+ case "UNARY_PLUS":
376
+ values.push(right);
377
+ return;
378
+ case "UNARY_MINUS":
379
+ values.push({
380
+ n: -right.n,
381
+ d: right.d,
382
+ c: right.c
383
+ });
384
+ return;
385
+ case "ABS_FN": {
386
+ const rN = right.n;
387
+ values.push({
388
+ n: rN < 0n ? -rN : rN,
389
+ d: right.d,
390
+ c: right.c
391
+ });
392
+ return;
393
+ }
394
+ case "CEIL_FN":
395
+ values.push({
396
+ n: ceil(right),
397
+ d: 1n
398
+ });
399
+ return;
400
+ case "FLOOR_FN":
401
+ values.push({
402
+ n: floor(right),
403
+ d: 1n
404
+ });
405
+ return;
406
+ case "SQRT_FN": {
407
+ if (right.c === void 0) {
408
+ values.push(sqrt(right, format === "precise"));
409
+ return;
410
+ }
411
+ const v = multiply(right, getConst(right.c));
412
+ values.push(sqrt(v, format === "precise"));
413
+ return;
414
+ }
415
+ }
416
+ if (op === "FACTORIAL") {
417
+ const reduced = simplify(right);
418
+ if (reduced.d !== 1n || reduced.n < 0n) throw new InterpreterError("Factorial is only defined for non-negative integers", pos);
419
+ if (reduced.n >= 4e5) throw new OverflowError();
420
+ values.push({
421
+ n: factorial(reduced.n),
422
+ d: 1n
423
+ });
424
+ return;
425
+ }
426
+ const left = values.pop();
427
+ if (left === void 0) throw new InsufficientOperandsError(pos);
428
+ let resN;
429
+ let resD;
430
+ let resC;
431
+ const lN = left.n;
432
+ const lD = left.d;
433
+ const lC = left.c;
434
+ const rN = right.n;
435
+ const rD = right.d;
436
+ const rC = right.c;
437
+ switch (op) {
438
+ case "ADD":
439
+ case "SUBTRACT": {
440
+ const isSub = op === "SUBTRACT";
441
+ if (lN === 0n) {
442
+ resN = isSub ? -rN : rN;
443
+ resD = rD;
444
+ resC = rC;
445
+ break;
446
+ }
447
+ if (rN === 0n) {
448
+ resN = lN;
449
+ resD = lD;
450
+ resC = lC;
451
+ break;
452
+ }
453
+ if (lD === rD) {
454
+ resN = isSub ? lN - rN : lN + rN;
455
+ resD = lD;
456
+ } else {
457
+ const common = gcd(lD, rD);
458
+ if (common === 1n) {
459
+ resN = isSub ? lN * rD - rN * lD : lN * rD + rN * lD;
460
+ resD = lD * rD;
461
+ } else {
462
+ const mLeft = rD / common;
463
+ const mRight = lD / common;
464
+ resN = isSub ? lN * mLeft - rN * mRight : lN * mLeft + rN * mRight;
465
+ resD = lD * mLeft;
466
+ }
467
+ }
468
+ if (lC === rC && resN !== 0n) resC = lC;
469
+ break;
470
+ }
471
+ case "MULTIPLY":
472
+ case "IMPLICIT_MUL": {
473
+ const result = multiply(left, right);
474
+ resN = result.n;
475
+ resD = result.d;
476
+ resC = result.c;
477
+ break;
478
+ }
479
+ case "DIVIDE": {
480
+ const g1 = gcd(lN, rN);
481
+ const g2 = gcd(rD, lD);
482
+ resN = lN / g1 * (rD / g2);
483
+ resD = lD / g2 * (rN / g1);
484
+ if (lC !== void 0 && rC === void 0) resC = lC;
485
+ break;
486
+ }
487
+ case "MOD": {
488
+ if (rN === 0n) throw new DivisionByZeroError();
489
+ const { n, d } = mod({
490
+ n: lN,
491
+ d: lD
492
+ }, {
493
+ n: rN,
494
+ d: rD
495
+ });
496
+ resN = n;
497
+ resD = d;
498
+ break;
499
+ }
500
+ case "EXP": {
501
+ const normalizedExponent = simplify(right);
502
+ let exponent = normalizedExponent.n;
503
+ if (exponent === 0n) {
504
+ resN = 1n;
505
+ resD = 1n;
506
+ break;
507
+ }
508
+ if (lN === 0n) {
509
+ if (rN < 0) throw new DivisionByZeroError();
510
+ resN = 0n;
511
+ resD = 1n;
512
+ break;
513
+ }
514
+ if (lN === lD && lC === void 0) {
515
+ resN = 1n;
516
+ resD = 1n;
517
+ break;
518
+ }
519
+ const exponentD = normalizedExponent.d;
520
+ if (exponentD === 2n) {
521
+ const rootResult = sqrt({
522
+ n: lN ** exponent,
523
+ d: lD ** exponent
524
+ }, format === "precise");
525
+ resN = rootResult.n;
526
+ resD = rootResult.d;
527
+ resC = rootResult.c;
528
+ break;
529
+ }
530
+ if (exponentD !== 1n) {
531
+ const rootResult = nthRoot({
532
+ n: lN ** exponent,
533
+ d: lD ** exponent
534
+ }, exponentD, format === "precise");
535
+ resN = rootResult.n;
536
+ resD = rootResult.d;
537
+ resC = rootResult.c;
538
+ break;
539
+ }
540
+ let baseN = lN;
541
+ let baseD = lD;
542
+ if (exponent < 0n) {
543
+ [baseN, baseD] = [baseD, baseN];
544
+ exponent = -exponent;
545
+ }
546
+ if (exponent === 1n) {
547
+ resN = baseN;
548
+ resD = baseD;
549
+ if (normalizedExponent.c === void 0) resC = lC;
550
+ break;
551
+ }
552
+ if (exponent > 1e4 && (baseN * exponent > 6e6 || baseD * exponent > 6e6)) throw new OverflowError();
553
+ if (!lC) {
554
+ resN = baseN ** exponent;
555
+ resD = baseD ** exponent;
556
+ break;
557
+ }
558
+ const c = getConst(lC);
559
+ resN = (baseN * c.n) ** exponent;
560
+ resD = (baseD * c.d) ** exponent;
561
+ break;
562
+ }
563
+ }
564
+ if (resD > SIMPLIFY_THRESHOLD) values.push(simplify({
565
+ n: resN,
566
+ d: resD,
567
+ c: resC
568
+ }));
569
+ else values.push({
570
+ n: resN,
571
+ d: resD,
572
+ c: resC
573
+ });
574
+ };
575
+ const pushOpWithPrecedence = (currentOp, pos) => {
576
+ const isUnary = isUnaryOperation(currentOp);
577
+ const isRightAssociative = currentOp === "EXP" || currentOp === "IMPLICIT_MUL";
578
+ while (ops.length > 0 && !isUnary && (isRightAssociative ? precedence[ops[ops.length - 1]] > precedence[currentOp] : precedence[ops[ops.length - 1]] >= precedence[currentOp])) applyOp(pos);
579
+ ops.push(currentOp);
580
+ };
581
+ for (let i = 0; i < tokens.length; i++) {
582
+ const token = tokens[i];
583
+ switch (token.type) {
584
+ case "NUMBER": {
585
+ const frac = token.fraction || "";
586
+ const fl = frac.length;
587
+ if (fl > MAX_PRECISION) throw new MaximumPrecisionError(fl, MAX_PRECISION);
588
+ let n = BigInt(token.whole + frac);
589
+ let d = fl === 0 ? 1n : 10n ** BigInt(fl);
590
+ if (token.exponent) {
591
+ if (token.exponent.length > MAX_PRECISION) throw new MaximumPrecisionError(token.exponent.length, MAX_PRECISION);
592
+ const expValue = BigInt(token.exponent);
593
+ if (expValue > 2e6) throw new OverflowError();
594
+ if (expValue >= 0n) n *= 10n ** expValue;
595
+ else d *= 10n ** -expValue;
596
+ }
597
+ if (d === 1n) values.push({
598
+ n,
599
+ d: 1n
600
+ });
601
+ else values.push(simplify({
602
+ n,
603
+ d
604
+ }));
605
+ break;
606
+ }
607
+ case "CONST":
608
+ if (format === "precise") values.push({
609
+ n: 1n,
610
+ d: 1n,
611
+ c: token.id
612
+ });
613
+ else values.push(getConst(token.id));
614
+ break;
615
+ case "LPAREN":
616
+ ops.push("LPAREN");
617
+ break;
618
+ case "RPAREN": {
619
+ let foundMatch = false;
620
+ while (ops.length > 0) {
621
+ if (ops[ops.length - 1] === "LPAREN") {
622
+ foundMatch = true;
623
+ break;
624
+ }
625
+ applyOp(token.pos);
626
+ }
627
+ if (!foundMatch) throw new MismatchedParenthesisError(token.pos);
628
+ ops.pop();
629
+ break;
630
+ }
631
+ case "ABS_OPEN":
632
+ ops.push("ABS_OPEN");
633
+ break;
634
+ case "ABS_CLOSE": {
635
+ let foundMatch = false;
636
+ while (ops.length > 0) {
637
+ if (ops[ops.length - 1] === "ABS_OPEN") {
638
+ foundMatch = true;
639
+ break;
640
+ }
641
+ applyOp(token.pos);
642
+ }
643
+ if (!foundMatch) throw new InterpreterError("Mismatched absolute value pipe", token.pos);
644
+ ops.pop();
645
+ const val = values.pop();
646
+ if (val === void 0) throw new UnexpectedEndOfExpressionError();
647
+ values.push({
648
+ n: val.n < 0n ? -val.n : val.n,
649
+ d: val.d,
650
+ c: val.c
651
+ });
652
+ break;
653
+ }
654
+ case "PLUS":
655
+ pushOpWithPrecedence("ADD", token.pos);
656
+ break;
657
+ case "MINUS":
658
+ pushOpWithPrecedence("SUBTRACT", token.pos);
659
+ break;
660
+ case "MUL":
661
+ pushOpWithPrecedence("MULTIPLY", token.pos);
662
+ break;
663
+ case "DIV":
664
+ pushOpWithPrecedence("DIVIDE", token.pos);
665
+ break;
666
+ case "POW":
667
+ pushOpWithPrecedence("EXP", token.pos);
668
+ break;
669
+ case "UNARY_PLUS":
670
+ pushOpWithPrecedence("UNARY_PLUS", token.pos);
671
+ break;
672
+ case "UNARY_MINUS":
673
+ pushOpWithPrecedence("UNARY_MINUS", token.pos);
674
+ break;
675
+ case "IMPLICIT_MUL":
676
+ pushOpWithPrecedence("IMPLICIT_MUL", token.pos);
677
+ break;
678
+ case "FACTORIAL":
679
+ pushOpWithPrecedence("FACTORIAL", token.pos);
680
+ break;
681
+ case "MOD":
682
+ pushOpWithPrecedence("MOD", token.pos);
683
+ break;
684
+ case "FUNC":
685
+ switch (token.id) {
686
+ case "abs":
687
+ pushOpWithPrecedence("ABS_FN", token.pos);
688
+ break;
689
+ case "ceil":
690
+ pushOpWithPrecedence("CEIL_FN", token.pos);
691
+ break;
692
+ case "floor":
693
+ pushOpWithPrecedence("FLOOR_FN", token.pos);
694
+ break;
695
+ case "sqrt":
696
+ pushOpWithPrecedence("SQRT_FN", token.pos);
697
+ break;
698
+ }
699
+ break;
700
+ }
701
+ }
702
+ while (ops.length > 0) {
703
+ const top = ops[ops.length - 1];
704
+ const lastPos = tokens[tokens.length - 1]?.pos ?? 0;
705
+ if (top === "LPAREN") {
706
+ ops.pop();
707
+ continue;
708
+ }
709
+ applyOp(lastPos);
710
+ }
711
+ const finalValue = values.pop();
712
+ if (finalValue === void 0) throw new UnexpectedEndOfExpressionError();
713
+ return simplify(finalValue);
714
+ }
715
+ //#endregion
716
+ //#region src/lib/lexer.ts
717
+ function isWhitespace(ch) {
718
+ return ch === " ";
719
+ }
720
+ function isDigit(ch) {
721
+ if (ch === void 0) return false;
722
+ return ch >= "0" && ch <= "9";
723
+ }
724
+ function isAlpha(ch) {
725
+ if (ch === void 0) return false;
726
+ return ch >= "a" && ch <= "z";
727
+ }
728
+ /**
729
+ * Simple lexer (tokenizer).
730
+ */
731
+ function tokenize(expression, options = {}) {
732
+ const { decimalSeparator = "." } = options;
733
+ const tokens = [];
734
+ let index = 0;
735
+ const length = expression.length;
736
+ while (index < length) {
737
+ const ch = expression[index];
738
+ const startPos = index;
739
+ if (isWhitespace(ch)) {
740
+ index++;
741
+ continue;
742
+ }
743
+ if (isDigit(ch) || ch === decimalSeparator) {
744
+ let whole = "";
745
+ let fraction = void 0;
746
+ let exponent = void 0;
747
+ if (ch === decimalSeparator) {
748
+ whole = "0";
749
+ index++;
750
+ const fracStart = index;
751
+ let fracBuffer = "";
752
+ while (index < length && isDigit(expression[index])) {
753
+ fracBuffer += expression[index];
754
+ index++;
755
+ }
756
+ if (index === fracStart) throw new LexerError("Expected digit after decimal separator", index);
757
+ fraction = fracBuffer;
758
+ } else {
759
+ while (index < length && isDigit(expression[index])) {
760
+ whole += expression[index];
761
+ index++;
762
+ }
763
+ if (index < length && expression[index] === decimalSeparator) {
764
+ index++;
765
+ let fracBuffer = "";
766
+ const fracStart = index;
767
+ while (index < length && isDigit(expression[index])) {
768
+ fracBuffer += expression[index];
769
+ index++;
770
+ }
771
+ fraction = index === fracStart ? "0" : fracBuffer;
772
+ }
773
+ }
774
+ if (index < length && (expression[index] === "e" || expression[index] === "E")) {
775
+ let lookahead = index + 1;
776
+ let sign = "";
777
+ const next = expression[lookahead];
778
+ if (lookahead < length && (next === "+" || next === "-")) {
779
+ sign = next;
780
+ lookahead++;
781
+ }
782
+ if (lookahead < length && isDigit(expression[lookahead])) {
783
+ index = lookahead;
784
+ let expBuffer = sign;
785
+ while (index < length && isDigit(expression[index])) {
786
+ expBuffer += expression[index];
787
+ index++;
788
+ }
789
+ exponent = expBuffer;
790
+ }
791
+ }
792
+ if (index < length && expression[index] === decimalSeparator) throw new LexerError("Invalid decimal number format", index);
793
+ tokens.push({
794
+ type: "NUMBER",
795
+ whole,
796
+ fraction,
797
+ exponent,
798
+ pos: startPos
799
+ });
800
+ continue;
801
+ }
802
+ if (isAlpha(ch)) {
803
+ let id = "";
804
+ while (index < length && isAlpha(expression[index])) id += expression[index++];
805
+ tokens.push({
806
+ type: "IDENTIFIER",
807
+ id,
808
+ pos: startPos
809
+ });
810
+ continue;
811
+ }
812
+ if (ch === "(") {
813
+ tokens.push({
814
+ type: "LPAREN",
815
+ pos: startPos
816
+ });
817
+ index++;
818
+ continue;
819
+ }
820
+ if (ch === ")") {
821
+ tokens.push({
822
+ type: "RPAREN",
823
+ pos: startPos
824
+ });
825
+ index++;
826
+ continue;
827
+ }
828
+ if (ch === "+") {
829
+ tokens.push({
830
+ type: "PLUS",
831
+ pos: startPos
832
+ });
833
+ index++;
834
+ continue;
835
+ }
836
+ if (ch === "-") {
837
+ tokens.push({
838
+ type: "MINUS",
839
+ pos: startPos
840
+ });
841
+ index++;
842
+ continue;
843
+ }
844
+ if (ch === "*") {
845
+ tokens.push({
846
+ type: "MUL",
847
+ pos: startPos
848
+ });
849
+ index++;
850
+ continue;
851
+ }
852
+ if (ch === "/") {
853
+ tokens.push({
854
+ type: "DIV",
855
+ pos: startPos
856
+ });
857
+ index++;
858
+ continue;
859
+ }
860
+ if (ch === "^") {
861
+ tokens.push({
862
+ type: "POW",
863
+ pos: startPos
864
+ });
865
+ index++;
866
+ continue;
867
+ }
868
+ if (ch === "!") {
869
+ tokens.push({
870
+ type: "FACTORIAL",
871
+ pos: startPos
872
+ });
873
+ index++;
874
+ continue;
875
+ }
876
+ if (ch === "|") {
877
+ tokens.push({
878
+ type: "PIPE",
879
+ pos: startPos
880
+ });
881
+ index++;
882
+ continue;
883
+ }
884
+ if (ch === "%") {
885
+ tokens.push({
886
+ type: "MOD",
887
+ pos: startPos
888
+ });
889
+ index++;
890
+ continue;
891
+ }
892
+ throw new LexerError(`Unexpected character '${ch}'`, index);
893
+ }
894
+ return tokens;
895
+ }
896
+ //#endregion
897
+ //#region src/lib/symbol.ts
898
+ const symbolMap = {
899
+ PLUS: "+",
900
+ UNARY_PLUS: "+",
901
+ MINUS: "-",
902
+ UNARY_MINUS: "-",
903
+ MUL: "*",
904
+ DIV: "/",
905
+ LPAREN: "(",
906
+ RPAREN: ")",
907
+ POW: "^",
908
+ IMPLICIT_MUL: "",
909
+ FACTORIAL: "!",
910
+ ABS_CLOSE: "|",
911
+ ABS_OPEN: "|",
912
+ PIPE: "|",
913
+ MOD: "%"
914
+ };
915
+ const prettifyNumber = (t) => t.fraction ? `${t.whole}.${t.fraction}` : t.whole;
916
+ function getSym(t) {
917
+ if (t.type === "NUMBER") return prettifyNumber(t);
918
+ if (t.type === "IDENTIFIER") return t.id;
919
+ return symbolMap[t.type];
920
+ }
921
+ //#endregion
922
+ //#region src/lib/parser.ts
923
+ /**
924
+ * Helper: Is the last token an "operand" (something that can be followed by a closing pipe or implicit mul)?
925
+ */
926
+ function isOperand(last) {
927
+ if (!last) return false;
928
+ return last.type === "NUMBER" || last.type === "CONST" || last.type === "RPAREN" || last.type === "ABS_CLOSE" || last.type === "FACTORIAL";
929
+ }
930
+ function isBinaryOperator(token) {
931
+ return token.type === "MUL" || token.type === "DIV" || token.type === "POW" || token.type === "MOD";
932
+ }
933
+ /**
934
+ * Helper: Does the current context allow a +/- to be unary?
935
+ */
936
+ function isUnaryContext(last) {
937
+ return !isOperand(last);
938
+ }
939
+ function resolveIdentifier(token) {
940
+ if (token.id === "pi" || token.id === "e") return {
941
+ type: "CONST",
942
+ id: token.id,
943
+ pos: token.pos
944
+ };
945
+ if (token.id === "abs" || token.id === "ceil" || token.id === "floor" || token.id === "sqrt") return {
946
+ type: "FUNC",
947
+ id: token.id,
948
+ pos: token.pos
949
+ };
950
+ throw new ParserError(`Unknown identifier '${token.id}'`, token.pos);
951
+ }
952
+ function parse(tokens) {
953
+ if (tokens.length === 0) return [];
954
+ const result = [];
955
+ let absStack = 0;
956
+ for (let i = 0; i < tokens.length; i++) {
957
+ let token = tokens[i];
958
+ const prev = tokens[i - 1];
959
+ if (token.type === "IDENTIFIER") token = resolveIdentifier(token);
960
+ let prevParsed = result[result.length - 1];
961
+ if (token.type === "PIPE") {
962
+ if (absStack > 0 && isOperand(prevParsed)) {
963
+ result.push({
964
+ type: "ABS_CLOSE",
965
+ pos: token.pos
966
+ });
967
+ absStack--;
968
+ } else {
969
+ if (isOperand(prevParsed)) result.push({
970
+ type: "IMPLICIT_MUL",
971
+ pos: token.pos
972
+ });
973
+ result.push({
974
+ type: "ABS_OPEN",
975
+ pos: token.pos
976
+ });
977
+ absStack++;
978
+ }
979
+ continue;
980
+ }
981
+ if (isOperand(prevParsed)) {
982
+ if (token.type === "LPAREN" || token.type === "FUNC" || token.type === "NUMBER" || token.type === "CONST") result.push({
983
+ type: "IMPLICIT_MUL",
984
+ pos: token.pos
985
+ });
986
+ }
987
+ prevParsed = result[result.length - 1];
988
+ if (isBinaryOperator(token)) {
989
+ if (isUnaryContext(prevParsed)) throw new ParserError(`Unexpected operator '${getSym(token)}'`, token.pos);
990
+ }
991
+ if (token.type === "FACTORIAL") {
992
+ if (!isOperand(prevParsed) && prevParsed?.type !== "RPAREN") throw new ParserError("Unexpected factorial operator", token.pos);
993
+ }
994
+ if (token.type === "RPAREN") {
995
+ if (prevParsed && (prevParsed.type === "PLUS" || prevParsed.type === "MINUS" || isBinaryOperator(prevParsed))) throw new ParserError(`Unexpected ')' after operator '${getSym(prevParsed)}'`, token.pos);
996
+ if (prevParsed?.type === "LPAREN") throw new ParserError("Unexpected ')' after '('", token.pos);
997
+ }
998
+ if (prev && (prev.type === "NUMBER" || prev.type === "IDENTIFIER" && resolveIdentifier(prev).type === "CONST") && token.type === "NUMBER") throw new ParserError("Missing operator between numbers", token.pos);
999
+ if ((token.type === "PLUS" || token.type === "MINUS") && isUnaryContext(prevParsed)) result.push({
1000
+ type: token.type === "PLUS" ? "UNARY_PLUS" : "UNARY_MINUS",
1001
+ pos: token.pos
1002
+ });
1003
+ else result.push(token);
1004
+ }
1005
+ if (absStack > 0) throw new ParserError("Unclosed absolute value '|'", result[result.length - 1]?.pos ?? 0);
1006
+ const last = result[result.length - 1];
1007
+ if (last && last.type !== "CONST" && (isBinaryOperator(last) || [
1008
+ "PLUS",
1009
+ "MINUS",
1010
+ "UNARY_PLUS",
1011
+ "UNARY_MINUS",
1012
+ "FUNC",
1013
+ "ABS_OPEN"
1014
+ ].includes(last.type))) throw new IncompleteExpressionError(`trailing operator '${last.type === "FUNC" ? last.id : getSym(last)}'`, last.pos);
1015
+ return result;
1016
+ }
1017
+ //#endregion
1018
+ //#region src/utils/format-result.ts
1019
+ const getConstantStr = (coeff, c) => {
1020
+ if (!c) return coeff;
1021
+ if (coeff === "0") return "0";
1022
+ if (coeff === "1") return c;
1023
+ if (coeff === "-1") return `-${c}`;
1024
+ return `${coeff}${c}`;
1025
+ };
1026
+ /**
1027
+ * Converts Result to a Decimal or Fraction String.
1028
+ */
1029
+ function formatResult(v, options = {}) {
1030
+ const { n, d, c } = v;
1031
+ if (d === 0n) return "NaN";
1032
+ if (options.format === "precise") {
1033
+ if (d === 1n) return getConstantStr(n.toString(), c);
1034
+ return `${getConstantStr(n.toString(), c)}/${d}`;
1035
+ }
1036
+ const maxDecimals = options.maxDecimals ?? 30;
1037
+ const isNegative = n < 0n;
1038
+ const absN = n < 0n ? -n : n;
1039
+ const integerPart = (absN / d).toString();
1040
+ let remainder = absN % d;
1041
+ if (remainder === 0n) {
1042
+ const res = (isNegative ? "-" : "") + integerPart;
1043
+ return res === "-0" ? "0" : res;
1044
+ }
1045
+ let fractionalPart = "";
1046
+ let count = 0;
1047
+ while (remainder !== 0n && count < maxDecimals) {
1048
+ remainder *= 10n;
1049
+ fractionalPart += (remainder / d).toString();
1050
+ remainder %= d;
1051
+ count++;
1052
+ }
1053
+ let lastNonZero = -1;
1054
+ for (let i = fractionalPart.length - 1; i >= 0; i--) if (fractionalPart[i] !== "0") {
1055
+ lastNonZero = i;
1056
+ break;
1057
+ }
1058
+ const finalFraction = lastNonZero === -1 ? "" : fractionalPart.slice(0, lastNonZero + 1);
1059
+ const result = finalFraction === "" ? integerPart : `${integerPart}.${finalFraction}`;
1060
+ return (isNegative && result !== "0" ? "-" : "") + result;
1061
+ }
1062
+ //#endregion
1063
+ //#region src/index.ts
1064
+ /**
1065
+ * The main entry point for the library.
1066
+ */
1067
+ function calculate(expression, options) {
1068
+ return formatResult(evaluate(parse(tokenize(expression, options)), options), options);
1069
+ }
1070
+ //#endregion
1071
+ export { calculate };
package/package.json CHANGED
@@ -1,4 +1,36 @@
1
1
  {
2
2
  "name": "picocalc",
3
- "version": "0.0.0"
3
+ "version": "0.0.1",
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
+ "#lib/*": "./src/lib/*.ts"
15
+ },
16
+ "exports": {
17
+ ".": "./dist/index.mjs",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "scripts": {
21
+ "test": "bun test",
22
+ "lint": "oxlint",
23
+ "format": "oxfmt",
24
+ "build": "tsdown",
25
+ "prepublishOnly": "bun run build"
26
+ },
27
+ "devDependencies": {
28
+ "@gameroman/config": "0.1.0",
29
+ "@types/bun": "1.3.14",
30
+ "oxfmt": "0.53.0",
31
+ "oxlint": "1.68.0",
32
+ "oxlint-tsgolint": "0.23.0",
33
+ "tsdown": "0.22.2",
34
+ "typescript": "6.0.3"
35
+ }
4
36
  }