picocalc 0.0.2 → 0.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/LICENSE +21 -21
- package/dist/index.d.mts +10 -3
- package/dist/index.mjs +103 -219
- package/package.json +6 -6
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "bigint.bitlength";
|
|
1
2
|
//#region src/lib/types.d.ts
|
|
2
3
|
interface PrecisionOptions {
|
|
3
4
|
readonly format?: "decimal" | "precise";
|
|
@@ -5,13 +6,19 @@ interface PrecisionOptions {
|
|
|
5
6
|
readonly roundingMode?: "round" | "truncate";
|
|
6
7
|
}
|
|
7
8
|
//#endregion
|
|
9
|
+
//#region src/lib/errors.d.ts
|
|
10
|
+
declare class PicocalcError extends Error {
|
|
11
|
+
pos?: number;
|
|
12
|
+
constructor(name: string, message: string, pos?: number);
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
8
15
|
//#region src/index.d.ts
|
|
9
16
|
interface CalculateOptions extends PrecisionOptions {
|
|
10
17
|
decimalSeparator?: "." | ",";
|
|
11
18
|
}
|
|
12
19
|
/**
|
|
13
|
-
* The main entry point for the library.
|
|
14
|
-
*/
|
|
20
|
+
* The main entry point for the library.
|
|
21
|
+
*/
|
|
15
22
|
declare function calculate(expression: string, options?: CalculateOptions): string;
|
|
16
23
|
//#endregion
|
|
17
|
-
export { calculate };
|
|
24
|
+
export { PicocalcError, calculate };
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import "bigint.bitlength";
|
|
2
2
|
//#region src/lib/errors.ts
|
|
3
|
-
var
|
|
3
|
+
var PicocalcError = class extends Error {
|
|
4
4
|
pos;
|
|
5
5
|
constructor(name, message, pos) {
|
|
6
|
-
if (pos === void 0) super(
|
|
7
|
-
else super(`${
|
|
6
|
+
if (pos === void 0) super(message);
|
|
7
|
+
else super(`${message} at position ${pos}`);
|
|
8
8
|
this.pos = pos;
|
|
9
9
|
this.name = name;
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
-
var InterpreterError = class extends
|
|
12
|
+
var InterpreterError = class extends PicocalcError {
|
|
13
13
|
constructor(message, pos) {
|
|
14
14
|
super("InterpreterError", message, pos);
|
|
15
15
|
}
|
|
@@ -403,7 +403,7 @@ function exponentiate(left, right, precision) {
|
|
|
403
403
|
if (lN === lD && c === void 0) return ONE;
|
|
404
404
|
if (rN === "OVERFLOW") return right;
|
|
405
405
|
if (lN === -lD && c === void 0) {
|
|
406
|
-
if (
|
|
406
|
+
if (simplify(right).n % 2n) return {
|
|
407
407
|
n: -1n,
|
|
408
408
|
d: 1n
|
|
409
409
|
};
|
|
@@ -552,8 +552,11 @@ const precedence = {
|
|
|
552
552
|
SQRT_FN: 8,
|
|
553
553
|
FACTORIAL: 10
|
|
554
554
|
};
|
|
555
|
+
function isFunctionOp(op) {
|
|
556
|
+
return op === "ABS_FN" || op === "CEIL_FN" || op === "FLOOR_FN" || op === "SQRT_FN";
|
|
557
|
+
}
|
|
555
558
|
function isUnaryOperation(op) {
|
|
556
|
-
return op === "UNARY_PLUS" || op === "UNARY_MINUS" || op
|
|
559
|
+
return op === "UNARY_PLUS" || op === "UNARY_MINUS" || isFunctionOp(op);
|
|
557
560
|
}
|
|
558
561
|
/**
|
|
559
562
|
* Maximum allowed precision
|
|
@@ -673,7 +676,7 @@ function evaluate(tokens, options = {}) {
|
|
|
673
676
|
const pushOpWithPrecedence = (currentOp, pos) => {
|
|
674
677
|
const isUnary = isUnaryOperation(currentOp);
|
|
675
678
|
const isRightAssociative = currentOp === "EXP" || currentOp === "IMPLICIT_MUL";
|
|
676
|
-
while (ops.length > 0 && !isUnary && (isRightAssociative ? precedence[ops
|
|
679
|
+
while (ops.length > 0 && !isUnary && (isRightAssociative ? precedence[ops.at(-1)] > precedence[currentOp] : precedence[ops.at(-1)] >= precedence[currentOp])) applyOp(pos);
|
|
677
680
|
ops.push(currentOp);
|
|
678
681
|
};
|
|
679
682
|
for (const token of tokens) switch (token.type) {
|
|
@@ -712,12 +715,13 @@ function evaluate(tokens, options = {}) {
|
|
|
712
715
|
else values.push(getConst(token.id));
|
|
713
716
|
break;
|
|
714
717
|
case "LPAREN":
|
|
715
|
-
|
|
718
|
+
case "ABS_OPEN":
|
|
719
|
+
ops.push(token.type);
|
|
716
720
|
break;
|
|
717
721
|
case "RPAREN": {
|
|
718
722
|
let foundMatch = false;
|
|
719
723
|
while (ops.length > 0) {
|
|
720
|
-
if (ops
|
|
724
|
+
if (ops.at(-1) === "LPAREN") {
|
|
721
725
|
foundMatch = true;
|
|
722
726
|
break;
|
|
723
727
|
}
|
|
@@ -725,14 +729,14 @@ function evaluate(tokens, options = {}) {
|
|
|
725
729
|
}
|
|
726
730
|
if (!foundMatch) throw new MismatchedParenthesisError(token.pos);
|
|
727
731
|
ops.pop();
|
|
732
|
+
const nextOp = ops.at(-1);
|
|
733
|
+
if (!nextOp) break;
|
|
734
|
+
if (isFunctionOp(nextOp)) applyOp(token.pos);
|
|
728
735
|
break;
|
|
729
736
|
}
|
|
730
|
-
case "ABS_OPEN":
|
|
731
|
-
ops.push("ABS_OPEN");
|
|
732
|
-
break;
|
|
733
737
|
case "ABS_CLOSE": {
|
|
734
738
|
while (ops.length > 0) {
|
|
735
|
-
if (ops
|
|
739
|
+
if (ops.at(-1) === "ABS_OPEN") break;
|
|
736
740
|
applyOp(token.pos);
|
|
737
741
|
}
|
|
738
742
|
ops.pop();
|
|
@@ -764,19 +768,11 @@ function evaluate(tokens, options = {}) {
|
|
|
764
768
|
pushOpWithPrecedence("EXP", token.pos);
|
|
765
769
|
break;
|
|
766
770
|
case "UNARY_PLUS":
|
|
767
|
-
pushOpWithPrecedence("UNARY_PLUS", token.pos);
|
|
768
|
-
break;
|
|
769
771
|
case "UNARY_MINUS":
|
|
770
|
-
pushOpWithPrecedence("UNARY_MINUS", token.pos);
|
|
771
|
-
break;
|
|
772
772
|
case "IMPLICIT_MUL":
|
|
773
|
-
pushOpWithPrecedence("IMPLICIT_MUL", token.pos);
|
|
774
|
-
break;
|
|
775
773
|
case "FACTORIAL":
|
|
776
|
-
pushOpWithPrecedence("FACTORIAL", token.pos);
|
|
777
|
-
break;
|
|
778
774
|
case "MOD":
|
|
779
|
-
pushOpWithPrecedence(
|
|
775
|
+
pushOpWithPrecedence(token.type, token.pos);
|
|
780
776
|
break;
|
|
781
777
|
case "FUNC":
|
|
782
778
|
switch (token.id) {
|
|
@@ -796,8 +792,8 @@ function evaluate(tokens, options = {}) {
|
|
|
796
792
|
break;
|
|
797
793
|
}
|
|
798
794
|
while (ops.length > 0) {
|
|
799
|
-
const top = ops
|
|
800
|
-
const lastPos = tokens
|
|
795
|
+
const top = ops.at(-1);
|
|
796
|
+
const lastPos = tokens.at(-1)?.pos ?? 0;
|
|
801
797
|
if (top === "LPAREN") {
|
|
802
798
|
ops.pop();
|
|
803
799
|
continue;
|
|
@@ -810,14 +806,11 @@ function evaluate(tokens, options = {}) {
|
|
|
810
806
|
}
|
|
811
807
|
//#endregion
|
|
812
808
|
//#region src/lib/lexer.ts
|
|
813
|
-
var LexerError = class extends
|
|
809
|
+
var LexerError = class extends PicocalcError {
|
|
814
810
|
constructor(message, pos) {
|
|
815
811
|
super("LexerError", message, pos);
|
|
816
812
|
}
|
|
817
813
|
};
|
|
818
|
-
function isWhitespace(ch) {
|
|
819
|
-
return ch === " ";
|
|
820
|
-
}
|
|
821
814
|
function isDigit(ch) {
|
|
822
815
|
if (ch === void 0) return false;
|
|
823
816
|
return ch >= "0" && ch <= "9";
|
|
@@ -830,42 +823,49 @@ function isAlpha(ch) {
|
|
|
830
823
|
* Maximum allowed expression length
|
|
831
824
|
*/
|
|
832
825
|
const MAX_EXPRESSION_LENGTH = 75e6;
|
|
826
|
+
const CHAR_MAP = {
|
|
827
|
+
"(": "LPAREN",
|
|
828
|
+
")": "RPAREN",
|
|
829
|
+
"+": "PLUS",
|
|
830
|
+
"-": "MINUS",
|
|
831
|
+
"*": "MUL",
|
|
832
|
+
"/": "DIV",
|
|
833
|
+
"^": "POW",
|
|
834
|
+
"!": "FACTORIAL",
|
|
835
|
+
"|": "PIPE",
|
|
836
|
+
"%": "MOD"
|
|
837
|
+
};
|
|
833
838
|
/**
|
|
834
839
|
* Simple lexer (tokenizer).
|
|
835
840
|
*/
|
|
836
841
|
function tokenize(expression, options = {}) {
|
|
837
842
|
const { decimalSeparator = "." } = options;
|
|
838
843
|
const tokens = [];
|
|
839
|
-
let index = 0;
|
|
840
844
|
const length = expression.length;
|
|
841
845
|
if (length > MAX_EXPRESSION_LENGTH) throw new LexerError(`Expression length (${length}) exceeds the maximum allowed limit of ${MAX_EXPRESSION_LENGTH}`, 0);
|
|
842
|
-
|
|
846
|
+
for (let index = 0; index < length; index++) {
|
|
843
847
|
const ch = expression[index];
|
|
844
|
-
const
|
|
845
|
-
if (
|
|
846
|
-
index++;
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
848
|
+
const pos = index;
|
|
849
|
+
if (ch === " ") continue;
|
|
849
850
|
if (isDigit(ch) || ch === decimalSeparator) {
|
|
850
851
|
let whole = "";
|
|
851
852
|
let fraction = void 0;
|
|
852
853
|
let exponent = void 0;
|
|
853
|
-
const
|
|
854
|
+
const consumeDigits = () => {
|
|
855
|
+
const start = index;
|
|
856
|
+
while (index < length && isDigit(expression[index])) index++;
|
|
857
|
+
return expression.slice(start, index);
|
|
858
|
+
};
|
|
854
859
|
if (ch === decimalSeparator) {
|
|
855
860
|
whole = "0";
|
|
856
861
|
index++;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if (index === fracStart) throw new LexerError("Expected digit after decimal separator", index);
|
|
860
|
-
fraction = expression.slice(fracStart, index);
|
|
862
|
+
fraction = consumeDigits();
|
|
863
|
+
if (fraction.length === 0) throw new LexerError("Expected digit after decimal separator", index);
|
|
861
864
|
} else {
|
|
862
|
-
|
|
863
|
-
whole = expression.slice(startIdx, index);
|
|
865
|
+
whole = consumeDigits();
|
|
864
866
|
if (index < length && expression[index] === decimalSeparator) {
|
|
865
867
|
index++;
|
|
866
|
-
|
|
867
|
-
while (index < length && isDigit(expression[index])) index++;
|
|
868
|
-
fraction = expression.slice(fracStart, index);
|
|
868
|
+
fraction = consumeDigits();
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
871
|
if (index < length && (expression[index] === "e" || expression[index] === "E")) {
|
|
@@ -878,115 +878,37 @@ function tokenize(expression, options = {}) {
|
|
|
878
878
|
}
|
|
879
879
|
if (lookahead < length && isDigit(expression[lookahead])) {
|
|
880
880
|
index = lookahead;
|
|
881
|
-
|
|
882
|
-
while (index < length && isDigit(expression[index])) {
|
|
883
|
-
expBuffer += expression[index];
|
|
884
|
-
index++;
|
|
885
|
-
}
|
|
886
|
-
exponent = expBuffer;
|
|
881
|
+
exponent = sign + consumeDigits();
|
|
887
882
|
}
|
|
888
883
|
}
|
|
889
884
|
if (index < length && expression[index] === decimalSeparator) throw new LexerError("Invalid decimal number format", index);
|
|
885
|
+
index--;
|
|
890
886
|
tokens.push({
|
|
891
887
|
type: "NUMBER",
|
|
892
888
|
whole,
|
|
893
889
|
fraction,
|
|
894
890
|
exponent,
|
|
895
|
-
pos
|
|
891
|
+
pos
|
|
896
892
|
});
|
|
897
893
|
continue;
|
|
898
894
|
}
|
|
899
895
|
if (isAlpha(ch)) {
|
|
900
896
|
let id = "";
|
|
901
897
|
while (index < length && isAlpha(expression[index])) id += expression[index++];
|
|
898
|
+
index--;
|
|
902
899
|
tokens.push({
|
|
903
900
|
type: "IDENTIFIER",
|
|
904
901
|
id,
|
|
905
|
-
pos
|
|
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
|
|
902
|
+
pos
|
|
961
903
|
});
|
|
962
|
-
index++;
|
|
963
904
|
continue;
|
|
964
905
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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);
|
|
906
|
+
const type = CHAR_MAP[ch];
|
|
907
|
+
if (!type) throw new LexerError(`Unexpected character '${ch}'`, index);
|
|
908
|
+
tokens.push({
|
|
909
|
+
type,
|
|
910
|
+
pos
|
|
911
|
+
});
|
|
990
912
|
}
|
|
991
913
|
return tokens;
|
|
992
914
|
}
|
|
@@ -1012,7 +934,7 @@ const symbolMap = {
|
|
|
1012
934
|
const prettifyNumber = (t) => t.fraction ? `${t.whole}.${t.fraction}` : t.whole;
|
|
1013
935
|
function getSym(t) {
|
|
1014
936
|
if (t.type === "NUMBER") return prettifyNumber(t);
|
|
1015
|
-
if (t.type === "IDENTIFIER") return t.id;
|
|
937
|
+
if (t.type === "IDENTIFIER" || t.type === "FUNC" || t.type === "CONST") return t.id;
|
|
1016
938
|
return symbolMap[t.type];
|
|
1017
939
|
}
|
|
1018
940
|
//#endregion
|
|
@@ -1027,13 +949,10 @@ function isOperand(last) {
|
|
|
1027
949
|
function isBinaryOperator(token) {
|
|
1028
950
|
return token.type === "MUL" || token.type === "DIV" || token.type === "POW" || token.type === "MOD";
|
|
1029
951
|
}
|
|
1030
|
-
/**
|
|
1031
|
-
* Helper: Does the current context allow a +/- to be unary?
|
|
1032
|
-
*/
|
|
1033
952
|
function isUnaryContext(last) {
|
|
1034
953
|
return !isOperand(last);
|
|
1035
954
|
}
|
|
1036
|
-
var ParserError = class extends
|
|
955
|
+
var ParserError = class extends PicocalcError {
|
|
1037
956
|
constructor(message, pos) {
|
|
1038
957
|
super("ParserError", message, pos);
|
|
1039
958
|
}
|
|
@@ -1049,15 +968,16 @@ var UnexpectedOperatorError = class extends ParserError {
|
|
|
1049
968
|
}
|
|
1050
969
|
};
|
|
1051
970
|
function resolveIdentifier(token) {
|
|
971
|
+
const pos = token.pos;
|
|
1052
972
|
if (token.id === "pi" || token.id === "e") return {
|
|
1053
973
|
type: "CONST",
|
|
1054
974
|
id: token.id,
|
|
1055
|
-
pos
|
|
975
|
+
pos
|
|
1056
976
|
};
|
|
1057
977
|
if (token.id === "abs" || token.id === "ceil" || token.id === "floor" || token.id === "sqrt") return {
|
|
1058
978
|
type: "FUNC",
|
|
1059
979
|
id: token.id,
|
|
1060
|
-
pos
|
|
980
|
+
pos
|
|
1061
981
|
};
|
|
1062
982
|
throw new ParserError(`Unknown identifier '${token.id}'`, token.pos);
|
|
1063
983
|
}
|
|
@@ -1069,53 +989,47 @@ function parse(tokens) {
|
|
|
1069
989
|
let token = tokens[i];
|
|
1070
990
|
const prev = tokens[i - 1];
|
|
1071
991
|
if (token.type === "IDENTIFIER") token = resolveIdentifier(token);
|
|
1072
|
-
|
|
992
|
+
const pos = token.pos;
|
|
993
|
+
const isPrevOperand = isOperand(result.at(-1));
|
|
1073
994
|
if (token.type === "PIPE") {
|
|
1074
|
-
if (absStack > 0 &&
|
|
995
|
+
if (absStack > 0 && isPrevOperand) {
|
|
1075
996
|
result.push({
|
|
1076
997
|
type: "ABS_CLOSE",
|
|
1077
|
-
pos
|
|
998
|
+
pos
|
|
1078
999
|
});
|
|
1079
1000
|
absStack--;
|
|
1080
|
-
|
|
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++;
|
|
1001
|
+
continue;
|
|
1090
1002
|
}
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1093
|
-
if (isOperand(prevParsed)) {
|
|
1094
|
-
if (token.type === "LPAREN" || token.type === "FUNC" || token.type === "NUMBER" || token.type === "CONST") result.push({
|
|
1003
|
+
if (isPrevOperand) result.push({
|
|
1095
1004
|
type: "IMPLICIT_MUL",
|
|
1096
|
-
pos
|
|
1005
|
+
pos
|
|
1097
1006
|
});
|
|
1007
|
+
result.push({
|
|
1008
|
+
type: "ABS_OPEN",
|
|
1009
|
+
pos
|
|
1010
|
+
});
|
|
1011
|
+
absStack++;
|
|
1012
|
+
continue;
|
|
1098
1013
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (prevParsed?.type === "LPAREN") throw new UnexpectedOperatorError("Unexpected ')' after '('", token.pos);
|
|
1014
|
+
if ((token.type === "LPAREN" || token.type === "FUNC" || token.type === "NUMBER" || token.type === "CONST") && isPrevOperand) result.push({
|
|
1015
|
+
type: "IMPLICIT_MUL",
|
|
1016
|
+
pos
|
|
1017
|
+
});
|
|
1018
|
+
const prevParsed = result.at(-1);
|
|
1019
|
+
if (isBinaryOperator(token) && isUnaryContext(prevParsed)) throw new UnexpectedOperatorError(`Unexpected operator '${getSym(token)}'`, token.pos);
|
|
1020
|
+
if ((token.type === "FACTORIAL" || token.type === "RPAREN") && isUnaryContext(prevParsed)) {
|
|
1021
|
+
if (!prevParsed) throw new UnexpectedOperatorError(`Unexpected '${getSym(token)}' at the beginning of expression`, token.pos);
|
|
1022
|
+
throw new UnexpectedOperatorError(`Unexpected '${getSym(token)}' after '${getSym(prevParsed)}'`, token.pos);
|
|
1109
1023
|
}
|
|
1110
1024
|
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
1025
|
if ((token.type === "PLUS" || token.type === "MINUS") && isUnaryContext(prevParsed)) result.push({
|
|
1112
1026
|
type: token.type === "PLUS" ? "UNARY_PLUS" : "UNARY_MINUS",
|
|
1113
|
-
pos
|
|
1027
|
+
pos
|
|
1114
1028
|
});
|
|
1115
1029
|
else result.push(token);
|
|
1116
1030
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1031
|
+
const last = result.at(-1);
|
|
1032
|
+
if (absStack > 0) throw new ParserError("Unclosed absolute value '|'", last?.pos ?? 0);
|
|
1119
1033
|
if (last && last.type !== "CONST" && (isBinaryOperator(last) || [
|
|
1120
1034
|
"PLUS",
|
|
1121
1035
|
"MINUS",
|
|
@@ -1132,16 +1046,15 @@ function parse(tokens) {
|
|
|
1132
1046
|
function getConstantStr(coeff, c, e) {
|
|
1133
1047
|
if (coeff === 0n) return "0";
|
|
1134
1048
|
if (!c) return `${coeff}`;
|
|
1135
|
-
|
|
1136
|
-
if (
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1049
|
+
let constantStr = c;
|
|
1050
|
+
if (e) {
|
|
1051
|
+
const num = e.n;
|
|
1052
|
+
const den = e.d ?? 1n;
|
|
1053
|
+
if (num !== den) {
|
|
1054
|
+
const expStr = den === 1n ? `${num}` : `(${num}/${den})`;
|
|
1055
|
+
constantStr = den === 2n * num ? `sqrt(${c})` : `${c}^${expStr}`;
|
|
1056
|
+
}
|
|
1140
1057
|
}
|
|
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
1058
|
if (coeff === 1n) return constantStr;
|
|
1146
1059
|
if (coeff === -1n) return `-${constantStr}`;
|
|
1147
1060
|
return `${coeff}${constantStr}`;
|
|
@@ -1165,46 +1078,17 @@ function formatPrecise(v) {
|
|
|
1165
1078
|
*/
|
|
1166
1079
|
function formatResult(v, options = {}) {
|
|
1167
1080
|
const { n, d } = v;
|
|
1168
|
-
if (d === 0n) return "NaN";
|
|
1169
1081
|
if (options.format === "precise") return formatPrecise(v);
|
|
1170
1082
|
const { maxDecimals = 30, roundingMode = "round" } = options;
|
|
1171
1083
|
const isNegative = n < 0;
|
|
1172
|
-
const
|
|
1173
|
-
let
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
|
|
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;
|
|
1084
|
+
const factor = 10n ** BigInt(maxDecimals);
|
|
1085
|
+
let resultStr = (((isNegative ? -n : n) * factor + (roundingMode === "round" ? d / 2n : 0n)) / d).toString();
|
|
1086
|
+
if (resultStr.length <= maxDecimals) resultStr = resultStr.padStart(maxDecimals + 1, "0");
|
|
1087
|
+
const splitIdx = resultStr.length - maxDecimals;
|
|
1088
|
+
const integerPart = resultStr.slice(0, splitIdx);
|
|
1089
|
+
const fractionalPart = resultStr.slice(splitIdx).replace(/0+$/, "");
|
|
1090
|
+
const finalResult = fractionalPart ? `${integerPart}.${fractionalPart}` : integerPart;
|
|
1091
|
+
return (isNegative && finalResult !== "0" ? "-" : "") + finalResult;
|
|
1208
1092
|
}
|
|
1209
1093
|
//#endregion
|
|
1210
1094
|
//#region src/index.ts
|
|
@@ -1215,4 +1099,4 @@ function calculate(expression, options) {
|
|
|
1215
1099
|
return formatResult(evaluate(parse(tokenize(expression, options)), options), options);
|
|
1216
1100
|
}
|
|
1217
1101
|
//#endregion
|
|
1218
|
-
export { calculate };
|
|
1102
|
+
export { PicocalcError, calculate };
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocalc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/picocalc/
|
|
7
|
+
"url": "git+https://github.com/picocalc/picocalc.git"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"bigint.bitlength": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@gameroman/config": "0.2.
|
|
33
|
+
"@gameroman/config": "0.2.5",
|
|
34
34
|
"@types/bun": "1.3.14",
|
|
35
|
-
"oxfmt": "0.
|
|
36
|
-
"oxlint": "1.
|
|
35
|
+
"oxfmt": "0.58.0",
|
|
36
|
+
"oxlint": "1.73.0",
|
|
37
37
|
"oxlint-tsgolint": "0.24.0",
|
|
38
|
-
"tsdown": "0.22.
|
|
38
|
+
"tsdown": "0.22.7",
|
|
39
39
|
"typescript": "6.0.3"
|
|
40
40
|
}
|
|
41
41
|
}
|