@goadminuiux/calculator 0.0.0-stage → 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.
@@ -0,0 +1,641 @@
1
+ /**
2
+ * @adminuiux/calculator - Core Calculator Engine
3
+ * Framework-independent mathematical evaluation engine and stream calculator state machine.
4
+ *
5
+ * @license MIT
6
+ * @copyright AdminUIUX
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ import { Formatter } from './formatter.js';
12
+
13
+ export class StreamCalculator {
14
+ constructor(config = {}) {
15
+ this.config = Object.assign({
16
+ precision: 8,
17
+ currency: '',
18
+ taxRate: 0.18,
19
+ tipRates: [10, 15, 20],
20
+ angleMode: 'deg',
21
+ onCalculate: null
22
+ }, config);
23
+
24
+ this.reset();
25
+ }
26
+
27
+ reset() {
28
+ this.currentInput = '0';
29
+ this.previousValue = null;
30
+ this.pendingOperator = null;
31
+ this.historyTokens = [];
32
+ this.hasJustCalculated = false;
33
+ this.hasPercentApplied = false;
34
+ this.rawPercent = null;
35
+ this.memory = 0;
36
+ this.tapeHistory = [];
37
+ }
38
+
39
+ inputDigit(digit) {
40
+ if (this.hasJustCalculated) {
41
+ this.currentInput = digit === '.' ? '0.' : String(digit);
42
+ this.previousValue = null;
43
+ this.pendingOperator = null;
44
+ this.historyTokens = [];
45
+ this.hasJustCalculated = false;
46
+ this.hasPercentApplied = false;
47
+ this.rawPercent = null;
48
+ return;
49
+ }
50
+
51
+ this.hasPercentApplied = false;
52
+ this.rawPercent = null;
53
+
54
+ if (digit === '.') {
55
+ if (!this.currentInput.includes('.')) {
56
+ this.currentInput = (this.currentInput === '' || this.currentInput === '0') ? '0.' : this.currentInput + '.';
57
+ }
58
+ } else if (digit === '00') {
59
+ if (this.currentInput === '0' || this.currentInput === '') {
60
+ this.currentInput = '0';
61
+ } else {
62
+ this.currentInput += '00';
63
+ }
64
+ } else {
65
+ if (this.currentInput === '0' || this.currentInput === '') {
66
+ this.currentInput = String(digit);
67
+ } else {
68
+ this.currentInput += String(digit);
69
+ }
70
+ }
71
+ }
72
+
73
+ inputOperator(op) {
74
+ const symbol = Formatter.getOperatorSymbol(op);
75
+
76
+ if (this.hasJustCalculated) {
77
+ const currentNum = this.parseCurrent();
78
+ this.previousValue = currentNum;
79
+ this.pendingOperator = op;
80
+ this.historyTokens = [Formatter.format(currentNum), symbol];
81
+ this.currentInput = '';
82
+ this.hasJustCalculated = false;
83
+ return;
84
+ }
85
+
86
+ if (this.previousValue === null) {
87
+ const currentNum = this.parseCurrent();
88
+ this.previousValue = currentNum;
89
+ this.pendingOperator = op;
90
+ this.historyTokens = [Formatter.format(currentNum), symbol];
91
+ this.currentInput = '';
92
+ return;
93
+ }
94
+
95
+ if (this.currentInput !== '') {
96
+ const nextNum = parseFloat(this.currentInput);
97
+ let calculated = 0;
98
+
99
+ if (this.hasPercentApplied) {
100
+ const prevOp = this.pendingOperator;
101
+ if (prevOp === '+') calculated = this.previousValue + nextNum;
102
+ else if (prevOp === '-') calculated = this.previousValue - nextNum;
103
+ else if (prevOp === '*' || prevOp === 'x' || prevOp === '×') calculated = nextNum;
104
+ else if (prevOp === '/' || prevOp === '÷') {
105
+ const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
106
+ calculated = frac === 0 ? 0 : this.previousValue / frac;
107
+ } else {
108
+ calculated = this.calculate(this.previousValue, prevOp, nextNum);
109
+ }
110
+ calculated = Formatter.round(calculated, this.config.precision);
111
+ this.hasPercentApplied = false;
112
+ this.rawPercent = null;
113
+ } else {
114
+ calculated = this.calculate(this.previousValue, this.pendingOperator, nextNum);
115
+ }
116
+
117
+ if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
118
+ this.historyTokens.push(symbol);
119
+ } else {
120
+ this.historyTokens.push(Formatter.format(nextNum), symbol);
121
+ }
122
+ this.previousValue = calculated;
123
+ this.pendingOperator = op;
124
+ this.currentInput = '';
125
+ return;
126
+ }
127
+
128
+ this.pendingOperator = op;
129
+ if (this.historyTokens.length > 0) {
130
+ this.historyTokens[this.historyTokens.length - 1] = symbol;
131
+ }
132
+ }
133
+
134
+ inputEquals() {
135
+ if (this.pendingOperator === null || this.previousValue === null) return this.parseCurrent();
136
+
137
+ const nextNum = this.currentInput !== '' ? parseFloat(this.currentInput) : this.previousValue;
138
+ let result = 0;
139
+
140
+ if (this.hasPercentApplied) {
141
+ const op = this.pendingOperator;
142
+ if (op === '+') result = this.previousValue + nextNum;
143
+ else if (op === '-') result = this.previousValue - nextNum;
144
+ else if (op === '*' || op === 'x' || op === '×') result = nextNum;
145
+ else if (op === '/' || op === '÷') {
146
+ const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
147
+ result = frac === 0 ? 0 : this.previousValue / frac;
148
+ } else {
149
+ result = this.calculate(this.previousValue, op, nextNum);
150
+ }
151
+ result = Formatter.round(result, this.config.precision);
152
+ this.hasPercentApplied = false;
153
+ this.rawPercent = null;
154
+ } else {
155
+ result = this.calculate(this.previousValue, this.pendingOperator, nextNum);
156
+ }
157
+
158
+ if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
159
+ this.historyTokens.push('=');
160
+ } else {
161
+ this.historyTokens.push(Formatter.format(nextNum), '=');
162
+ }
163
+
164
+ const historyStr = this.historyTokens.join(' ');
165
+ this.tapeHistory.unshift(`${historyStr} ${Formatter.format(result)}`);
166
+
167
+ if (typeof this.config.onCalculate === 'function') {
168
+ this.config.onCalculate(result, historyStr);
169
+ }
170
+
171
+ this.previousValue = result;
172
+ this.currentInput = String(result);
173
+ this.pendingOperator = null;
174
+ this.hasJustCalculated = true;
175
+ return result;
176
+ }
177
+
178
+ calculate(a, op, b) {
179
+ let res = 0;
180
+ switch (op) {
181
+ case '+': res = a + b; break;
182
+ case '-': res = a - b; break;
183
+ case '*':
184
+ case 'x':
185
+ case '×':
186
+ res = a * b; break;
187
+ case '/':
188
+ case '÷':
189
+ res = b === 0 ? 0 : a / b; break;
190
+ case '%':
191
+ case 'mod':
192
+ res = a % b; break;
193
+ case '^':
194
+ case 'pow':
195
+ res = Math.pow(a, b); break;
196
+ default: res = b;
197
+ }
198
+ return Formatter.round(res, this.config.precision);
199
+ }
200
+
201
+ toggleSign() {
202
+ if (this.currentInput !== '' && this.currentInput !== '0') {
203
+ if (this.currentInput.startsWith('-')) {
204
+ this.currentInput = this.currentInput.slice(1);
205
+ } else {
206
+ this.currentInput = '-' + this.currentInput;
207
+ }
208
+ } else if (this.previousValue !== null) {
209
+ this.previousValue = -this.previousValue;
210
+ this.currentInput = String(this.previousValue);
211
+ }
212
+ }
213
+
214
+ applyPercent() {
215
+ const current = this.parseCurrent();
216
+ if (this.previousValue !== null && this.pendingOperator !== null) {
217
+ const percentVal = (this.previousValue * current) / 100;
218
+ this.currentInput = String(Formatter.round(percentVal, this.config.precision));
219
+ this.hasPercentApplied = true;
220
+ this.rawPercent = current;
221
+ this.historyTokens = [
222
+ Formatter.format(this.previousValue),
223
+ Formatter.getOperatorSymbol(this.pendingOperator),
224
+ `${Formatter.format(current)}%`
225
+ ];
226
+ } else {
227
+ const percentVal = current / 100;
228
+ this.currentInput = String(Formatter.round(percentVal, this.config.precision));
229
+ this.hasPercentApplied = false;
230
+ this.rawPercent = null;
231
+ this.historyTokens = [`${Formatter.format(current)}%`];
232
+ }
233
+ }
234
+
235
+ backspace() {
236
+ if (this.hasJustCalculated) {
237
+ this.clearAll();
238
+ return;
239
+ }
240
+ if (this.currentInput.length > 1) {
241
+ this.currentInput = this.currentInput.slice(0, -1);
242
+ } else {
243
+ this.currentInput = '0';
244
+ }
245
+ }
246
+
247
+ clearAll() {
248
+ this.currentInput = '0';
249
+ this.previousValue = null;
250
+ this.pendingOperator = null;
251
+ this.historyTokens = [];
252
+ this.hasJustCalculated = false;
253
+ this.hasPercentApplied = false;
254
+ this.rawPercent = null;
255
+ }
256
+
257
+ parseCurrent() {
258
+ return parseFloat(this.currentInput) || 0;
259
+ }
260
+
261
+ getDisplayValue() {
262
+ let displayVal = this.currentInput;
263
+ if (displayVal === '' && this.previousValue !== null) {
264
+ displayVal = String(this.previousValue);
265
+ }
266
+ if (displayVal === '') displayVal = '0';
267
+ return displayVal;
268
+ }
269
+
270
+ getFormattedDisplay() {
271
+ return Formatter.format(this.getDisplayValue(), { currency: this.config.currency });
272
+ }
273
+
274
+ getHistoryString() {
275
+ return this.historyTokens.join(' ');
276
+ }
277
+ }
278
+
279
+ export const Calculator = {
280
+ precision: 8,
281
+
282
+ add(a, b, precision = this.precision) {
283
+ return Formatter.round(Number(a) + Number(b), precision);
284
+ },
285
+
286
+ subtract(a, b, precision = this.precision) {
287
+ return Formatter.round(Number(a) - Number(b), precision);
288
+ },
289
+
290
+ multiply(a, b, precision = this.precision) {
291
+ return Formatter.round(Number(a) * Number(b), precision);
292
+ },
293
+
294
+ divide(a, b, precision = this.precision) {
295
+ const divisor = Number(b);
296
+ if (divisor === 0) return 0;
297
+ return Formatter.round(Number(a) / divisor, precision);
298
+ },
299
+
300
+ modulo(a, b, precision = this.precision) {
301
+ const divisor = Number(b);
302
+ if (divisor === 0) return 0;
303
+ return Formatter.round(Number(a) % divisor, precision);
304
+ },
305
+
306
+ power(a, b, precision = this.precision) {
307
+ return Formatter.round(Math.pow(Number(a), Number(b)), precision);
308
+ },
309
+
310
+ percent(value, percentage, precision = this.precision) {
311
+ return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
312
+ },
313
+
314
+ addPercent(value, percentage, precision = this.precision) {
315
+ const delta = (Number(value) * Number(percentage)) / 100;
316
+ return Formatter.round(Number(value) + delta, precision);
317
+ },
318
+
319
+ subtractPercent(value, percentage, precision = this.precision) {
320
+ const delta = (Number(value) * Number(percentage)) / 100;
321
+ return Formatter.round(Number(value) - delta, precision);
322
+ },
323
+
324
+ multiplyPercent(value, percentage, precision = this.precision) {
325
+ return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
326
+ },
327
+
328
+ dividePercent(value, percentage, precision = this.precision) {
329
+ const frac = Number(percentage) / 100;
330
+ if (frac === 0) return 0;
331
+ return Formatter.round(Number(value) / frac, precision);
332
+ },
333
+
334
+ sin(val, angleMode = 'deg', precision = this.precision) {
335
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
336
+ return Formatter.round(Math.sin(rad), precision);
337
+ },
338
+
339
+ cos(val, angleMode = 'deg', precision = this.precision) {
340
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
341
+ return Formatter.round(Math.cos(rad), precision);
342
+ },
343
+
344
+ tan(val, angleMode = 'deg', precision = this.precision) {
345
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
346
+ return Formatter.round(Math.tan(rad), precision);
347
+ },
348
+
349
+ asin(val, angleMode = 'deg', precision = this.precision) {
350
+ const rad = Math.asin(Number(val));
351
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
352
+ return Formatter.round(res, precision);
353
+ },
354
+
355
+ acos(val, angleMode = 'deg', precision = this.precision) {
356
+ const rad = Math.acos(Number(val));
357
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
358
+ return Formatter.round(res, precision);
359
+ },
360
+
361
+ atan(val, angleMode = 'deg', precision = this.precision) {
362
+ const rad = Math.atan(Number(val));
363
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
364
+ return Formatter.round(res, precision);
365
+ },
366
+
367
+ sqrt(val, precision = this.precision) {
368
+ const n = Number(val);
369
+ return n >= 0 ? Formatter.round(Math.sqrt(n), precision) : 0;
370
+ },
371
+
372
+ sqr(val, precision = this.precision) {
373
+ return Formatter.round(Number(val) * Number(val), precision);
374
+ },
375
+
376
+ cube(val, precision = this.precision) {
377
+ return Formatter.round(Number(val) * Number(val) * Number(val), precision);
378
+ },
379
+
380
+ log(val, precision = this.precision) {
381
+ const n = Number(val);
382
+ return n > 0 ? Formatter.round(Math.log10(n), precision) : 0;
383
+ },
384
+
385
+ ln(val, precision = this.precision) {
386
+ const n = Number(val);
387
+ return n > 0 ? Formatter.round(Math.log(n), precision) : 0;
388
+ },
389
+
390
+ exp(val, precision = this.precision) {
391
+ return Formatter.round(Math.exp(Number(val)), precision);
392
+ },
393
+
394
+ reciprocal(val, precision = this.precision) {
395
+ const n = Number(val);
396
+ return n !== 0 ? Formatter.round(1 / n, precision) : 0;
397
+ },
398
+
399
+ abs(val) {
400
+ return Math.abs(Number(val));
401
+ },
402
+
403
+ factorial(n) {
404
+ const num = Math.floor(Number(n));
405
+ if (num < 0) return 0;
406
+ if (num === 0 || num === 1) return 1;
407
+ let r = 1;
408
+ for (let i = 2; i <= Math.min(num, 170); i++) r *= i;
409
+ return r;
410
+ },
411
+
412
+ tax(amount, rate = 0.18, isAdd = true, precision = this.precision) {
413
+ const amt = Number(amount);
414
+ const res = isAdd ? amt * (1 + rate) : amt / (1 + rate);
415
+ return Formatter.round(res, precision);
416
+ },
417
+
418
+ tip(amount, percent = 15, precision = this.precision) {
419
+ const amt = Number(amount);
420
+ const tipVal = (amt * percent) / 100;
421
+ return Formatter.round(amt + tipVal, precision);
422
+ },
423
+
424
+ discount(amount, percent = 10, precision = this.precision) {
425
+ const amt = Number(amount);
426
+ const discVal = (amt * percent) / 100;
427
+ return Formatter.round(amt - discVal, precision);
428
+ },
429
+
430
+ format(value, options) {
431
+ return Formatter.format(value, options);
432
+ },
433
+
434
+ executePerform(currentVal, expr) {
435
+ if (!expr) return { value: currentVal, history: '' };
436
+ const str = String(expr).trim();
437
+ const val = Number(currentVal) || 0;
438
+
439
+ const pctMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)\s*%$/);
440
+ if (pctMatch) {
441
+ const sign = pctMatch[1] || '+';
442
+ const pct = parseFloat(pctMatch[2]);
443
+ let result = val;
444
+ if (sign === '+' || !pctMatch[1]) result = this.addPercent(val, pct);
445
+ else if (sign === '-') result = this.subtractPercent(val, pct);
446
+ else if (sign === '*') result = this.multiplyPercent(val, pct);
447
+ else if (sign === '/') result = this.dividePercent(val, pct);
448
+ const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
449
+ return {
450
+ value: result,
451
+ history: `${Formatter.format(val)} ${opDisplay} ${pct}% =`
452
+ };
453
+ }
454
+
455
+ const numMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)$/);
456
+ if (numMatch) {
457
+ const sign = numMatch[1] || '+';
458
+ const num = parseFloat(numMatch[2]);
459
+ let result = val;
460
+ if (sign === '+') result = this.add(val, num);
461
+ else if (sign === '-') result = this.subtract(val, num);
462
+ else if (sign === '*') result = this.multiply(val, num);
463
+ else if (sign === '/') result = this.divide(val, num);
464
+ const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
465
+ return {
466
+ value: result,
467
+ history: `${Formatter.format(val)} ${opDisplay} ${Formatter.format(num)} =`
468
+ };
469
+ }
470
+
471
+ return { value: val, history: '' };
472
+ },
473
+
474
+ evaluate(expression, options = {}) {
475
+ if (!expression || typeof expression !== 'string') return 0;
476
+ const precision = options.precision ?? this.precision;
477
+
478
+ let clean = expression
479
+ .replace(/×/g, '*')
480
+ .replace(/÷/g, '/')
481
+ .replace(/−/g, '-')
482
+ .replace(/,/g, '')
483
+ .trim();
484
+
485
+ if (!clean) return 0;
486
+
487
+ const tokens = [];
488
+ let i = 0;
489
+ let lastToken = null;
490
+
491
+ while (i < clean.length) {
492
+ const char = clean[i];
493
+
494
+ if (/\s/.test(char)) {
495
+ i++;
496
+ continue;
497
+ }
498
+
499
+ if (/\d/.test(char) || (char === '.' && i + 1 < clean.length && /\d/.test(clean[i + 1]))) {
500
+ let numStr = '';
501
+ while (i < clean.length && (/[\d.]/.test(clean[i]))) {
502
+ numStr += clean[i];
503
+ i++;
504
+ }
505
+ let isPct = false;
506
+ while (i < clean.length && /\s/.test(clean[i])) i++;
507
+ if (i < clean.length && clean[i] === '%') {
508
+ isPct = true;
509
+ i++;
510
+ }
511
+ const numVal = parseFloat(numStr);
512
+ tokens.push({ type: 'number', value: numVal, isPercent: isPct });
513
+ lastToken = 'number';
514
+ continue;
515
+ }
516
+
517
+ if (char === '+' || char === '-' || char === '*' || char === '/' || char === '^' || char === '(' || char === ')') {
518
+ const isUnary = (char === '+' || char === '-') &&
519
+ (lastToken === null || lastToken === 'operator' || lastToken === 'lparen');
520
+
521
+ if (isUnary) {
522
+ tokens.push({ type: 'unary', value: char });
523
+ lastToken = 'unary';
524
+ } else if (char === '(') {
525
+ tokens.push({ type: 'lparen', value: '(' });
526
+ lastToken = 'lparen';
527
+ } else if (char === ')') {
528
+ tokens.push({ type: 'rparen', value: ')' });
529
+ lastToken = 'rparen';
530
+ } else {
531
+ tokens.push({ type: 'operator', value: char });
532
+ lastToken = 'operator';
533
+ }
534
+ i++;
535
+ continue;
536
+ }
537
+
538
+ i++;
539
+ }
540
+
541
+ if (tokens.length === 0) return 0;
542
+
543
+ const precedence = {
544
+ '+': 1,
545
+ '-': 1,
546
+ '*': 2,
547
+ '/': 2,
548
+ '^': 3,
549
+ 'u+': 4,
550
+ 'u-': 4
551
+ };
552
+
553
+ const outputQueue = [];
554
+ const opStack = [];
555
+
556
+ for (let t = 0; t < tokens.length; t++) {
557
+ const token = tokens[t];
558
+
559
+ if (token.type === 'number') {
560
+ outputQueue.push(token);
561
+ } else if (token.type === 'unary') {
562
+ opStack.push({ type: 'unary', op: 'u' + token.value });
563
+ } else if (token.type === 'operator') {
564
+ const op = token.value;
565
+ while (
566
+ opStack.length > 0 &&
567
+ opStack[opStack.length - 1].type !== 'lparen' &&
568
+ ((precedence[opStack[opStack.length - 1].op] > precedence[op]) ||
569
+ (precedence[opStack[opStack.length - 1].op] === precedence[op] && op !== '^'))
570
+ ) {
571
+ outputQueue.push(opStack.pop());
572
+ }
573
+ opStack.push({ type: 'operator', op: op });
574
+ } else if (token.type === 'lparen') {
575
+ opStack.push(token);
576
+ } else if (token.type === 'rparen') {
577
+ while (opStack.length > 0 && opStack[opStack.length - 1].type !== 'lparen') {
578
+ outputQueue.push(opStack.pop());
579
+ }
580
+ if (opStack.length > 0 && opStack[opStack.length - 1].type === 'lparen') {
581
+ opStack.pop();
582
+ }
583
+ }
584
+ }
585
+
586
+ while (opStack.length > 0) {
587
+ const op = opStack.pop();
588
+ if (op.type !== 'lparen') {
589
+ outputQueue.push(op);
590
+ }
591
+ }
592
+
593
+ const evalStack = [];
594
+ for (let k = 0; k < outputQueue.length; k++) {
595
+ const item = outputQueue[k];
596
+
597
+ if (item.type === 'number') {
598
+ evalStack.push(item);
599
+ } else if (item.type === 'unary') {
600
+ const operand = evalStack.pop();
601
+ const val = typeof operand === 'object' ? operand.value : operand;
602
+ const res = item.op === 'u-' ? -val : val;
603
+ evalStack.push({ type: 'number', value: res });
604
+ } else if (item.type === 'operator') {
605
+ const rightToken = evalStack.pop();
606
+ const leftToken = evalStack.pop();
607
+
608
+ const left = typeof leftToken === 'object' ? leftToken.value : Number(leftToken);
609
+ let right = typeof rightToken === 'object' ? rightToken.value : Number(rightToken);
610
+
611
+ if (rightToken && rightToken.isPercent) {
612
+ if (item.op === '+' || item.op === '-') {
613
+ right = (left * right) / 100;
614
+ } else if (item.op === '*' || item.op === '/') {
615
+ right = right / 100;
616
+ }
617
+ }
618
+
619
+ let res = 0;
620
+ switch (item.op) {
621
+ case '+': res = left + right; break;
622
+ case '-': res = left - right; break;
623
+ case '*': res = left * right; break;
624
+ case '/': res = right === 0 ? 0 : left / right; break;
625
+ case '^': res = Math.pow(left, right); break;
626
+ }
627
+ evalStack.push({ type: 'number', value: res });
628
+ }
629
+ }
630
+
631
+ if (evalStack.length === 0) return 0;
632
+ const finalResult = evalStack[0].value ?? evalStack[0];
633
+ return Formatter.round(Number(finalResult), precision);
634
+ },
635
+
636
+ createStream(config = {}) {
637
+ return new StreamCalculator(config);
638
+ }
639
+ };
640
+
641
+ export default Calculator;