sci-calc-pro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/bin/cli.js +240 -0
- package/lib/calculator.js +135 -0
- package/lib/parser.js +251 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# sci-calc-pro
|
|
2
|
+
|
|
3
|
+
Professional scientific calculator CLI for Node.js. Zero dependencies, pure JavaScript.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g sci-calc-pro
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use directly in a project:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx sci-calc-pro "sin(45)"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### One-liner
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sci-calc "2^10"
|
|
23
|
+
sci-calc "sin(45) + cos(60)"
|
|
24
|
+
sci-calc "sqrt(144)"
|
|
25
|
+
sci-calc "fact(10)"
|
|
26
|
+
sci-calc "log(1000)"
|
|
27
|
+
sci-calc "C(10,3)"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Interactive REPL
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
sci-calc --repl
|
|
34
|
+
sci-calc
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
sci> 2 + 3
|
|
39
|
+
= 5
|
|
40
|
+
sci> sin(45)
|
|
41
|
+
= 0.707106781187
|
|
42
|
+
sci> set x = 10
|
|
43
|
+
sci> x^2
|
|
44
|
+
= 100
|
|
45
|
+
sci> help
|
|
46
|
+
sci> exit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Functions
|
|
50
|
+
|
|
51
|
+
| Category | Functions |
|
|
52
|
+
| -------------- | --------------------------------------------------- |
|
|
53
|
+
| Basic | `sqrt`, `cbrt`, `abs`, `pow`, `mod` |
|
|
54
|
+
| Trigonometric | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` |
|
|
55
|
+
| Hyperbolic | `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh` |
|
|
56
|
+
| Logarithmic | `log` (base 10), `ln` (natural), `log2`, `exp`, `exp2`, `exp10` |
|
|
57
|
+
| Combinatorics | `fact`/`factorial`, `perm`/`P`, `comb`/`C` |
|
|
58
|
+
| Rounding | `round`, `floor`, `ceil`, `trunc` |
|
|
59
|
+
| Aggregation | `max`, `min` |
|
|
60
|
+
|
|
61
|
+
## Constants
|
|
62
|
+
|
|
63
|
+
- `pi` - 3.14159265358979...
|
|
64
|
+
- `e` - 2.71828182845904...
|
|
65
|
+
- `phi` - Golden ratio (1.61803398874989...)
|
|
66
|
+
|
|
67
|
+
## REPL Commands
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
| ----------------- | ------------------------------ |
|
|
71
|
+
| `help` | Show help |
|
|
72
|
+
| `list` | List all functions |
|
|
73
|
+
| `vars` | Show stored variables |
|
|
74
|
+
| `set x = expr` | Store a variable |
|
|
75
|
+
| `history` | Show calculation history |
|
|
76
|
+
| `unit deg`/`rad` | Set angle unit |
|
|
77
|
+
| `ms expr` | Store to memory |
|
|
78
|
+
| `mr` | Recall memory |
|
|
79
|
+
| `mc` | Clear memory |
|
|
80
|
+
| `clear` | Clear screen |
|
|
81
|
+
| `exit` | Quit |
|
|
82
|
+
|
|
83
|
+
## Flags
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
sci-calc <expression> Evaluate expression
|
|
87
|
+
sci-calc --repl Start interactive mode
|
|
88
|
+
sci-calc --help Show help
|
|
89
|
+
sci-calc --version Show version
|
|
90
|
+
sci-calc --list List functions
|
|
91
|
+
sci-calc --unit <deg|rad> Set angle unit for evaluation
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Examples
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Trig in degrees
|
|
98
|
+
sci-calc "sin(30)"
|
|
99
|
+
sci-calc "atan2(1,1)"
|
|
100
|
+
|
|
101
|
+
# Factorials and combinations
|
|
102
|
+
sci-calc "fact(20)"
|
|
103
|
+
sci-calc "C(52,5)" # 5-card poker hands
|
|
104
|
+
|
|
105
|
+
# Complex expressions
|
|
106
|
+
sci-calc "sqrt(e^2 + pi^2)"
|
|
107
|
+
sci-calc "log(1000) + ln(e^5)"
|
|
108
|
+
|
|
109
|
+
# Powers and roots
|
|
110
|
+
sci-calc "2^128"
|
|
111
|
+
sci-calc "cbrt(27)"
|
|
112
|
+
|
|
113
|
+
# With angle unit
|
|
114
|
+
sci-calc --unit rad "sin(1.5708)"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const Parser = require('../lib/parser');
|
|
6
|
+
const SciCalculator = require('../lib/calculator');
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.0.0';
|
|
9
|
+
const BANNER = `
|
|
10
|
+
╔═══════════════════════════════════════════╗
|
|
11
|
+
║ SCI-CALC PRO v${VERSION} ║
|
|
12
|
+
║ Professional Scientific Calculator ║
|
|
13
|
+
╚═══════════════════════════════════════════╝
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const HELP = `
|
|
17
|
+
USAGE:
|
|
18
|
+
sci-calc <expression> Evaluate an expression
|
|
19
|
+
sci-calc --repl Start interactive mode
|
|
20
|
+
sci-calc --help Show this help
|
|
21
|
+
sci-calc --version Show version
|
|
22
|
+
sci-calc --list List available functions
|
|
23
|
+
sci-calc --unit <deg|rad> Set angle unit
|
|
24
|
+
|
|
25
|
+
FUNCTIONS:
|
|
26
|
+
Trig: sin, cos, tan, asin, acos, atan, atan2
|
|
27
|
+
Hyperbolic: sinh, cosh, tanh, asinh, acosh, atanh
|
|
28
|
+
Log: log (base10), ln (natural), log2, exp, exp2, exp10
|
|
29
|
+
Root: sqrt, cbrt
|
|
30
|
+
Combin: fact (factorial), perm, comb
|
|
31
|
+
Round: round, floor, ceil, trunc
|
|
32
|
+
Other: abs, pow, mod, max, min
|
|
33
|
+
|
|
34
|
+
CONSTANTS:
|
|
35
|
+
pi, e, phi
|
|
36
|
+
|
|
37
|
+
EXAMPLES:
|
|
38
|
+
sci-calc "sin(45)"
|
|
39
|
+
sci-calc "2^10"
|
|
40
|
+
sci-calc "sqrt(144) + ln(100)"
|
|
41
|
+
sci-calc "fact(10)"
|
|
42
|
+
sci-calc "C(10,3)"
|
|
43
|
+
sci-calc --repl
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
function printResult(value, calc) {
|
|
47
|
+
console.log(` = ${calc.format(value)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printError(msg) {
|
|
51
|
+
console.error(` \x1b[31mError: ${msg}\x1b[0m`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Single expression mode ──
|
|
55
|
+
function evaluateExpression(expr, parser) {
|
|
56
|
+
try {
|
|
57
|
+
const result = parser.parse(expr);
|
|
58
|
+
printResult(result, parser.calc);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
printError(err.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── REPL mode ──
|
|
66
|
+
function startREPL(angleUnit) {
|
|
67
|
+
console.log(BANNER);
|
|
68
|
+
console.log(' Type "help" for commands, "exit" to quit.\n');
|
|
69
|
+
|
|
70
|
+
const parser = new Parser();
|
|
71
|
+
parser.setAngleUnit(angleUnit);
|
|
72
|
+
|
|
73
|
+
const rl = readline.createInterface({
|
|
74
|
+
input: process.stdin,
|
|
75
|
+
output: process.stdout,
|
|
76
|
+
prompt: `\x1b[36msci>\x1b[0m `,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
rl.prompt();
|
|
80
|
+
|
|
81
|
+
rl.on('line', (line) => {
|
|
82
|
+
const input = line.trim();
|
|
83
|
+
if (!input) { rl.prompt(); return; }
|
|
84
|
+
|
|
85
|
+
const lower = input.toLowerCase();
|
|
86
|
+
|
|
87
|
+
if (lower === 'exit' || lower === 'quit' || lower === 'q') {
|
|
88
|
+
console.log(' Goodbye!');
|
|
89
|
+
rl.close();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (lower === 'help' || lower === 'h' || lower === '?') {
|
|
94
|
+
console.log(HELP);
|
|
95
|
+
rl.prompt();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (lower === 'list') {
|
|
100
|
+
console.log(HELP);
|
|
101
|
+
rl.prompt();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (lower === 'clear' || lower === 'cls') {
|
|
106
|
+
console.clear();
|
|
107
|
+
rl.prompt();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (lower === 'vars') {
|
|
112
|
+
console.log(' Variables:', JSON.stringify(parser.vars, null, 2));
|
|
113
|
+
rl.prompt();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (lower.startsWith('set ')) {
|
|
118
|
+
const parts = input.slice(4).split('=');
|
|
119
|
+
if (parts.length === 2) {
|
|
120
|
+
const varName = parts[0].trim();
|
|
121
|
+
const val = parser.parse(parts[1].trim());
|
|
122
|
+
parser.vars[varName] = val;
|
|
123
|
+
console.log(` ${varName} = ${parser.calc.format(val)}`);
|
|
124
|
+
} else {
|
|
125
|
+
printError('Usage: set varname = expression');
|
|
126
|
+
}
|
|
127
|
+
rl.prompt();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (lower.startsWith('unit ')) {
|
|
132
|
+
const unit = lower.slice(5).trim();
|
|
133
|
+
try {
|
|
134
|
+
parser.setAngleUnit(unit);
|
|
135
|
+
console.log(` Angle unit set to: ${unit}`);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
printError(e.message);
|
|
138
|
+
}
|
|
139
|
+
rl.prompt();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (lower === 'history') {
|
|
144
|
+
const hist = parser.calc.history;
|
|
145
|
+
if (hist.length === 0) {
|
|
146
|
+
console.log(' No history yet.');
|
|
147
|
+
} else {
|
|
148
|
+
hist.slice(-20).forEach((h, i) => {
|
|
149
|
+
console.log(` ${String(i + 1).padStart(3)}. ${h.expr} = ${parser.calc.format(h.result)}`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
rl.prompt();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (lower === 'mem' || lower === 'memory') {
|
|
157
|
+
console.log(` Memory = ${parser.calc.format(parser.calc.memory)}`);
|
|
158
|
+
rl.prompt();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (lower.startsWith('ms ')) {
|
|
163
|
+
try {
|
|
164
|
+
const val = parser.parse(input.slice(3));
|
|
165
|
+
parser.calc.memStore(val);
|
|
166
|
+
console.log(` Memory = ${parser.calc.format(val)}`);
|
|
167
|
+
} catch (e) { printError(e.message); }
|
|
168
|
+
rl.prompt();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (lower === 'mr') {
|
|
173
|
+
console.log(` ${parser.calc.format(parser.calc.memRecall())}`);
|
|
174
|
+
rl.prompt();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (lower === 'mc') {
|
|
179
|
+
parser.calc.memClear();
|
|
180
|
+
console.log(' Memory cleared.');
|
|
181
|
+
rl.prompt();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
evaluateExpression(input, parser);
|
|
186
|
+
rl.prompt();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
rl.on('close', () => process.exit(0));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── CLI entry ──
|
|
193
|
+
function main() {
|
|
194
|
+
const args = process.argv.slice(2);
|
|
195
|
+
|
|
196
|
+
if (args.length === 0) {
|
|
197
|
+
startREPL('deg');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const flag = args[0].toLowerCase();
|
|
202
|
+
|
|
203
|
+
if (flag === '--help' || flag === '-h') {
|
|
204
|
+
console.log(HELP);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (flag === '--version' || flag === '-v') {
|
|
209
|
+
console.log(`sci-calc-pro v${VERSION}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (flag === '--list') {
|
|
214
|
+
console.log(HELP);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (flag === '--repl' || flag === '-r') {
|
|
219
|
+
const unit = args.includes('--rad') ? 'rad' : 'deg';
|
|
220
|
+
startREPL(unit);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (flag === '--unit') {
|
|
225
|
+
const unit = (args[1] || 'deg').toLowerCase();
|
|
226
|
+
const expression = args.slice(2).join(' ');
|
|
227
|
+
const parser = new Parser();
|
|
228
|
+
parser.setAngleUnit(unit);
|
|
229
|
+
if (!expression) { startREPL(unit); return; }
|
|
230
|
+
evaluateExpression(expression, parser);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Direct evaluation
|
|
235
|
+
const expression = args.join(' ');
|
|
236
|
+
const parser = new Parser();
|
|
237
|
+
evaluateExpression(expression, parser);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
main();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
4
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
5
|
+
|
|
6
|
+
class SciCalculator {
|
|
7
|
+
constructor(angleUnit = 'deg') {
|
|
8
|
+
this.angleUnit = angleUnit;
|
|
9
|
+
this.memory = 0;
|
|
10
|
+
this.history = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_toRad(angle) {
|
|
14
|
+
return this.angleUnit === 'deg' ? angle * DEG_TO_RAD : angle;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_fromRad(rad) {
|
|
18
|
+
return this.angleUnit === 'deg' ? rad * RAD_TO_DEG : rad;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_record(expr, result) {
|
|
22
|
+
this.history.push({ expr, result, time: new Date() });
|
|
23
|
+
if (this.history.length > 100) this.history.shift();
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Basic arithmetic
|
|
28
|
+
add(a, b) { return this._record(`${a} + ${b}`, a + b); }
|
|
29
|
+
sub(a, b) { return this._record(`${a} - ${b}`, a - b); }
|
|
30
|
+
mul(a, b) { return this._record(`${a} × ${b}`, a * b); }
|
|
31
|
+
div(a, b) {
|
|
32
|
+
if (b === 0) throw new Error('Division by zero');
|
|
33
|
+
return this._record(`${a} ÷ ${b}`, a / b);
|
|
34
|
+
}
|
|
35
|
+
mod(a, b) { return this._record(`${a} mod ${b}`, a % b); }
|
|
36
|
+
pow(a, b) { return this._record(`${a} ^ ${b}`, Math.pow(a, b)); }
|
|
37
|
+
sqrt(a) {
|
|
38
|
+
if (a < 0) throw new Error('Square root of negative number');
|
|
39
|
+
return this._record(`√(${a})`, Math.sqrt(a));
|
|
40
|
+
}
|
|
41
|
+
cbrt(a) { return this._record(`∛(${a})`, Math.cbrt(a)); }
|
|
42
|
+
abs(a) { return this._record(`|${a}|`, Math.abs(a)); }
|
|
43
|
+
factorial(a) {
|
|
44
|
+
if (a < 0 || !Number.isInteger(a)) throw new Error('Factorial requires a non-negative integer');
|
|
45
|
+
if (a > 170) throw new Error('Factorial overflow (max 170)');
|
|
46
|
+
let result = 1;
|
|
47
|
+
for (let i = 2; i <= a; i++) result *= i;
|
|
48
|
+
return this._record(`${a}!`, result);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Trigonometric functions
|
|
52
|
+
sin(a) { return this._record(`sin(${a})`, Math.sin(this._toRad(a))); }
|
|
53
|
+
cos(a) { return this._record(`cos(${a})`, Math.cos(this._toRad(a))); }
|
|
54
|
+
tan(a) {
|
|
55
|
+
const rad = this._toRad(a);
|
|
56
|
+
if (Math.abs(Math.cos(rad)) < 1e-15) throw new Error('tan: undefined (asymptote)');
|
|
57
|
+
return this._record(`tan(${a})`, Math.tan(rad));
|
|
58
|
+
}
|
|
59
|
+
asin(a) {
|
|
60
|
+
if (a < -1 || a > 1) throw new Error('asin: domain error [-1, 1]');
|
|
61
|
+
return this._record(`asin(${a})`, this._fromRad(Math.asin(a)));
|
|
62
|
+
}
|
|
63
|
+
acos(a) {
|
|
64
|
+
if (a < -1 || a > 1) throw new Error('acos: domain error [-1, 1]');
|
|
65
|
+
return this._record(`acos(${a})`, this._fromRad(Math.acos(a)));
|
|
66
|
+
}
|
|
67
|
+
atan(a) { return this._record(`atan(${a})`, this._fromRad(Math.atan(a))); }
|
|
68
|
+
atan2(y, x) { return this._record(`atan2(${y}, ${x})`, this._fromRad(Math.atan2(y, x))); }
|
|
69
|
+
|
|
70
|
+
// Hyperbolic functions
|
|
71
|
+
sinh(a) { return this._record(`sinh(${a})`, Math.sinh(this._toRad(a))); }
|
|
72
|
+
cosh(a) { return this._record(`cosh(${a})`, Math.cosh(this._toRad(a))); }
|
|
73
|
+
tanh(a) { return this._record(`tanh(${a})`, Math.tanh(this._toRad(a))); }
|
|
74
|
+
asinh(a) { return this._record(`asinh(${a})`, this._fromRad(Math.asinh(a))); }
|
|
75
|
+
acosh(a) {
|
|
76
|
+
if (a < 1) throw new Error('acosh: domain error [1, ∞)');
|
|
77
|
+
return this._record(`acosh(${a})`, this._fromRad(Math.acosh(a)));
|
|
78
|
+
}
|
|
79
|
+
atanh(a) {
|
|
80
|
+
if (a <= -1 || a >= 1) throw new Error('atanh: domain error (-1, 1)');
|
|
81
|
+
return this._record(`atanh(${a})`, this._fromRad(Math.atanh(a)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Logarithmic & exponential
|
|
85
|
+
log(a) { return this._record(`log(${a})`, Math.log10(a)); }
|
|
86
|
+
ln(a) { return this._record(`ln(${a})`, Math.log(a)); }
|
|
87
|
+
log2(a) { return this._record(`log2(${a})`, Math.log2(a)); }
|
|
88
|
+
exp(a) { return this._record(`exp(${a})`, Math.exp(a)); }
|
|
89
|
+
exp2(a) { return this._record(`2^${a}`, Math.pow(2, a)); }
|
|
90
|
+
exp10(a) { return this._record(`10^${a}`, Math.pow(10, a)); }
|
|
91
|
+
|
|
92
|
+
// Combinatorics
|
|
93
|
+
perm(n, r) {
|
|
94
|
+
if (n < 0 || r < 0 || r > n) throw new Error('P(n,r): invalid arguments');
|
|
95
|
+
let result = 1;
|
|
96
|
+
for (let i = 0; i < r; i++) result *= (n - i);
|
|
97
|
+
return this._record(`P(${n},${r})`, result);
|
|
98
|
+
}
|
|
99
|
+
comb(n, r) {
|
|
100
|
+
if (n < 0 || r < 0 || r > n) throw new Error('C(n,r): invalid arguments');
|
|
101
|
+
let result = 1;
|
|
102
|
+
for (let i = 0; i < r; i++) result = result * (n - i) / (i + 1);
|
|
103
|
+
return this._record(`C(${n},${r})`, Math.round(result));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Constants
|
|
107
|
+
pi() { return Math.PI; }
|
|
108
|
+
e() { return Math.E; }
|
|
109
|
+
phi() { return (1 + Math.sqrt(5)) / 2; }
|
|
110
|
+
|
|
111
|
+
// Memory
|
|
112
|
+
memStore(v) { this.memory = v; return this.memory; }
|
|
113
|
+
memRecall() { return this.memory; }
|
|
114
|
+
memAdd(v) { this.memory += v; return this.memory; }
|
|
115
|
+
memSub(v) { this.memory -= v; return this.memory; }
|
|
116
|
+
memClear() { this.memory = 0; return 0; }
|
|
117
|
+
|
|
118
|
+
// Rounding
|
|
119
|
+
round(a, d = 0) { return this._record(`round(${a},${d})`, Number(a.toFixed(d))); }
|
|
120
|
+
floor(a) { return this._record(`floor(${a})`, Math.floor(a)); }
|
|
121
|
+
ceil(a) { return this._record(`ceil(${a})`, Math.ceil(a)); }
|
|
122
|
+
truncate(a) { return this._record(`trunc(${a})`, Math.trunc(a)); }
|
|
123
|
+
|
|
124
|
+
// Format result
|
|
125
|
+
format(value) {
|
|
126
|
+
if (typeof value !== 'number' || isNaN(value)) return 'NaN';
|
|
127
|
+
if (!isFinite(value)) return value > 0 ? 'Infinity' : '-Infinity';
|
|
128
|
+
if (Number.isInteger(value) && Math.abs(value) < 1e15) return value.toString();
|
|
129
|
+
if (Math.abs(value) < 1e-10 || Math.abs(value) >= 1e15) return value.toExponential(10);
|
|
130
|
+
const str = value.toPrecision(12);
|
|
131
|
+
return parseFloat(str).toString();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = SciCalculator;
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SciCalculator = require('./calculator');
|
|
4
|
+
|
|
5
|
+
class Parser {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.calc = new SciCalculator();
|
|
8
|
+
this.vars = { pi: Math.PI, e: Math.E, phi: (1 + Math.sqrt(5)) / 2 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setAngleUnit(unit) {
|
|
12
|
+
if (unit !== 'deg' && unit !== 'rad') throw new Error('Angle unit must be "deg" or "rad"');
|
|
13
|
+
this.calc.angleUnit = unit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
parse(input) {
|
|
17
|
+
const cleaned = input
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/×/g, '*')
|
|
20
|
+
.replace(/÷/g, '/')
|
|
21
|
+
.replace(/\^/g, '**')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.replace(/√\(/g, 'sqrt(')
|
|
24
|
+
.replace(/∛\(/g, 'cbrt(');
|
|
25
|
+
|
|
26
|
+
const result = this._eval(cleaned, { pos: 0 });
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_eval(input, state) {
|
|
31
|
+
let result = this._parseAddSub(input, state);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_parseAddSub(input, state) {
|
|
36
|
+
let left = this._parseMulDiv(input, state);
|
|
37
|
+
while (state.pos < input.length) {
|
|
38
|
+
this._skipSpaces(input, state);
|
|
39
|
+
const ch = input[state.pos];
|
|
40
|
+
if (ch === '+') {
|
|
41
|
+
state.pos++;
|
|
42
|
+
const right = this._parseMulDiv(input, state);
|
|
43
|
+
left = left + right;
|
|
44
|
+
} else if (ch === '-' && (state.pos === 0 || '+-*/(^,'.includes(input[state.pos - 1]))) {
|
|
45
|
+
state.pos++;
|
|
46
|
+
const right = this._parseMulDiv(input, state);
|
|
47
|
+
left = left - right;
|
|
48
|
+
} else {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return left;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_parseMulDiv(input, state) {
|
|
56
|
+
let left = this._parsePower(input, state);
|
|
57
|
+
while (state.pos < input.length) {
|
|
58
|
+
this._skipSpaces(input, state);
|
|
59
|
+
const ch = input[state.pos];
|
|
60
|
+
if (ch === '*') {
|
|
61
|
+
state.pos++;
|
|
62
|
+
if (input[state.pos] === '*') { state.pos++; }
|
|
63
|
+
const right = this._parsePower(input, state);
|
|
64
|
+
left = left * right;
|
|
65
|
+
} else if (ch === '/') {
|
|
66
|
+
state.pos++;
|
|
67
|
+
const right = this._parsePower(input, state);
|
|
68
|
+
left = left / right;
|
|
69
|
+
} else if (ch === '%') {
|
|
70
|
+
state.pos++;
|
|
71
|
+
const right = this._parsePower(input, state);
|
|
72
|
+
left = left % right;
|
|
73
|
+
} else {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return left;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_parsePower(input, state) {
|
|
81
|
+
let base = this._parseUnary(input, state);
|
|
82
|
+
this._skipSpaces(input, state);
|
|
83
|
+
if (state.pos < input.length && input[state.pos] === '*') {
|
|
84
|
+
if (input[state.pos + 1] === '*') {
|
|
85
|
+
state.pos += 2;
|
|
86
|
+
const exp = this._parseUnary(input, state);
|
|
87
|
+
return Math.pow(base, exp);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return base;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_parseUnary(input, state) {
|
|
94
|
+
this._skipSpaces(input, state);
|
|
95
|
+
if (state.pos < input.length && input[state.pos] === '-') {
|
|
96
|
+
state.pos++;
|
|
97
|
+
return -this._parseAtom(input, state);
|
|
98
|
+
}
|
|
99
|
+
if (state.pos < input.length && input[state.pos] === '+') {
|
|
100
|
+
state.pos++;
|
|
101
|
+
}
|
|
102
|
+
return this._parseAtom(input, state);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_parseAtom(input, state) {
|
|
106
|
+
this._skipSpaces(input, state);
|
|
107
|
+
|
|
108
|
+
if (state.pos >= input.length) throw new Error('Unexpected end of expression');
|
|
109
|
+
|
|
110
|
+
// Parentheses
|
|
111
|
+
if (input[state.pos] === '(') {
|
|
112
|
+
state.pos++;
|
|
113
|
+
const result = this._eval(input, state);
|
|
114
|
+
this._skipSpaces(input, state);
|
|
115
|
+
if (state.pos < input.length && input[state.pos] === ')') state.pos++;
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Number
|
|
120
|
+
if (this._isDigit(input[state.pos]) || input[state.pos] === '.') {
|
|
121
|
+
return this._parseNumber(input, state);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Function or variable
|
|
125
|
+
if (this._isAlpha(input[state.pos])) {
|
|
126
|
+
return this._parseFunctionOrVar(input, state);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(`Unexpected character: '${input[state.pos]}' at position ${state.pos}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_parseNumber(input, state) {
|
|
133
|
+
const start = state.pos;
|
|
134
|
+
while (state.pos < input.length && (this._isDigit(input[state.pos]) || input[state.pos] === '.')) {
|
|
135
|
+
state.pos++;
|
|
136
|
+
}
|
|
137
|
+
// Scientific notation: 1e10, 1E-5
|
|
138
|
+
if (state.pos < input.length && (input[state.pos] === 'e' || input[state.pos] === 'E')) {
|
|
139
|
+
if (state.pos + 1 < input.length && (input[state.pos + 1] === '-' || input[state.pos + 1] === '+')) {
|
|
140
|
+
state.pos += 2;
|
|
141
|
+
while (state.pos < input.length && this._isDigit(input[state.pos])) state.pos++;
|
|
142
|
+
} else if (state.pos + 1 < input.length && this._isDigit(input[state.pos + 1])) {
|
|
143
|
+
state.pos += 2;
|
|
144
|
+
while (state.pos < input.length && this._isDigit(input[state.pos])) state.pos++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const numStr = input.slice(start, state.pos);
|
|
148
|
+
const num = parseFloat(numStr);
|
|
149
|
+
if (isNaN(num)) throw new Error(`Invalid number: ${numStr}`);
|
|
150
|
+
return num;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_parseFunctionOrVar(input, state) {
|
|
154
|
+
const start = state.pos;
|
|
155
|
+
while (state.pos < input.length && this._isAlphaNum(input[state.pos])) state.pos++;
|
|
156
|
+
const name = input.slice(start, state.pos);
|
|
157
|
+
|
|
158
|
+
this._skipSpaces(input, state);
|
|
159
|
+
|
|
160
|
+
// Function call
|
|
161
|
+
if (state.pos < input.length && input[state.pos] === '(') {
|
|
162
|
+
state.pos++;
|
|
163
|
+
const args = this._parseArgs(input, state);
|
|
164
|
+
this._skipSpaces(input, state);
|
|
165
|
+
if (state.pos < input.length && input[state.pos] === ')') state.pos++;
|
|
166
|
+
return this._callFunction(name, args);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Variable
|
|
170
|
+
if (name in this.vars) return this.vars[name];
|
|
171
|
+
throw new Error(`Unknown variable: ${name}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_parseArgs(input, state) {
|
|
175
|
+
const args = [];
|
|
176
|
+
this._skipSpaces(input, state);
|
|
177
|
+
if (state.pos < input.length && input[state.pos] === ')') return args;
|
|
178
|
+
|
|
179
|
+
args.push(this._eval(input, state));
|
|
180
|
+
while (state.pos < input.length && input[state.pos] === ',') {
|
|
181
|
+
state.pos++;
|
|
182
|
+
args.push(this._eval(input, state));
|
|
183
|
+
}
|
|
184
|
+
return args;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_callFunction(name, args) {
|
|
188
|
+
const c = this.calc;
|
|
189
|
+
switch (name.toLowerCase()) {
|
|
190
|
+
// Basic
|
|
191
|
+
case 'sqrt': return c.sqrt(args[0]);
|
|
192
|
+
case 'cbrt': return c.cbrt(args[0]);
|
|
193
|
+
case 'abs': return c.abs(args[0]);
|
|
194
|
+
case 'fact': case 'factorial': return c.factorial(args[0]);
|
|
195
|
+
case 'mod': return c.mod(args[0], args[1]);
|
|
196
|
+
|
|
197
|
+
// Trig
|
|
198
|
+
case 'sin': return c.sin(args[0]);
|
|
199
|
+
case 'cos': return c.cos(args[0]);
|
|
200
|
+
case 'tan': return c.tan(args[0]);
|
|
201
|
+
case 'asin': return c.asin(args[0]);
|
|
202
|
+
case 'acos': return c.acos(args[0]);
|
|
203
|
+
case 'atan': return c.atan(args[0]);
|
|
204
|
+
case 'atan2': return c.atan2(args[0], args[1]);
|
|
205
|
+
|
|
206
|
+
// Hyperbolic
|
|
207
|
+
case 'sinh': return c.sinh(args[0]);
|
|
208
|
+
case 'cosh': return c.cosh(args[0]);
|
|
209
|
+
case 'tanh': return c.tanh(args[0]);
|
|
210
|
+
case 'asinh': return c.asinh(args[0]);
|
|
211
|
+
case 'acosh': return c.acosh(args[0]);
|
|
212
|
+
case 'atanh': return c.atanh(args[0]);
|
|
213
|
+
|
|
214
|
+
// Log
|
|
215
|
+
case 'log': case 'log10': return c.log(args[0]);
|
|
216
|
+
case 'ln': return c.ln(args[0]);
|
|
217
|
+
case 'log2': return c.log2(args[0]);
|
|
218
|
+
case 'exp': return c.exp(args[0]);
|
|
219
|
+
case 'exp2': return c.exp2(args[0]);
|
|
220
|
+
case 'exp10': return c.exp10(args[0]);
|
|
221
|
+
|
|
222
|
+
// Combinatorics
|
|
223
|
+
case 'perm': case 'permutation': case 'p': return c.perm(args[0], args[1]);
|
|
224
|
+
case 'comb': case 'combination': case 'choose': case 'c': return c.comb(args[0], args[1]);
|
|
225
|
+
|
|
226
|
+
// Rounding
|
|
227
|
+
case 'round': return c.round(args[0], args[1] || 0);
|
|
228
|
+
case 'floor': return c.floor(args[0]);
|
|
229
|
+
case 'ceil': return c.ceil(args[0]);
|
|
230
|
+
case 'trunc': return c.truncate(args[0]);
|
|
231
|
+
|
|
232
|
+
// Misc
|
|
233
|
+
case 'pow': return c.pow(args[0], args[1]);
|
|
234
|
+
case 'max': return Math.max(...args);
|
|
235
|
+
case 'min': return Math.min(...args);
|
|
236
|
+
|
|
237
|
+
default:
|
|
238
|
+
throw new Error(`Unknown function: ${name}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_skipSpaces(input, state) {
|
|
243
|
+
while (state.pos < input.length && input[state.pos] === ' ') state.pos++;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_isDigit(ch) { return ch >= '0' && ch <= '9'; }
|
|
247
|
+
_isAlpha(ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_'; }
|
|
248
|
+
_isAlphaNum(ch) { return this._isAlpha(ch) || this._isDigit(ch); }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = Parser;
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sci-calc-pro",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Professional scientific calculator CLI for Node.js",
|
|
5
|
+
"main": "lib/calculator.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sci-calc": "bin/cli.js",
|
|
8
|
+
"sci-calc-pro": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/cli.js --repl",
|
|
12
|
+
"calc": "node bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"calculator",
|
|
16
|
+
"scientific",
|
|
17
|
+
"cli",
|
|
18
|
+
"math",
|
|
19
|
+
"trigonometry",
|
|
20
|
+
"logarithm",
|
|
21
|
+
"algebra"
|
|
22
|
+
],
|
|
23
|
+
"author": "IIEE",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=14.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|