@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,1049 @@
1
+ /**
2
+ * @adminuiux/calculator (ES Module Bundle)
3
+ * AdminUIUX - Production-Ready Universal Calculator Package
4
+ *
5
+ * @version 1.0.0
6
+ * @author AdminUIUX (adminuiux.com)
7
+ * @license MIT
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ export const Formatter = {
13
+ round(value, precision = 8) {
14
+ if (!isFinite(value) || isNaN(value)) return 0;
15
+ const p = Math.pow(10, precision);
16
+ return Math.round((value + Number.EPSILON) * p) / p;
17
+ },
18
+
19
+ format(num, options = {}) {
20
+ if (num === null || num === undefined || num === '') return '';
21
+ const parsed = typeof num === 'number' ? num : parseFloat(num);
22
+ if (isNaN(parsed)) return 'Error';
23
+
24
+ let targetNum = num;
25
+ if (typeof options.precision === 'number') {
26
+ targetNum = this.round(parsed, options.precision);
27
+ }
28
+
29
+ const str = String(targetNum);
30
+ if (str.includes('e') || str.includes('E')) return str;
31
+
32
+ const parts = str.split('.');
33
+ const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
34
+ const formatted = parts.length > 1 ? `${intPart}.${parts[1]}` : intPart;
35
+
36
+ if (options.currency) {
37
+ return `${options.currency} ${formatted}`;
38
+ }
39
+ return formatted;
40
+ },
41
+
42
+ parse(str) {
43
+ if (typeof str === 'number') return isNaN(str) ? 0 : str;
44
+ if (!str || typeof str !== 'string') return 0;
45
+ const clean = str.replace(/[^0-9.-]/g, '');
46
+ const val = parseFloat(clean);
47
+ return isNaN(val) ? 0 : val;
48
+ },
49
+
50
+ getOperatorSymbol(op) {
51
+ switch (op) {
52
+ case '*':
53
+ case 'x': return '×';
54
+ case '/': return '÷';
55
+ case '-': return '−';
56
+ default: return op;
57
+ }
58
+ }
59
+ };
60
+
61
+ export class StreamCalculator {
62
+ constructor(config = {}) {
63
+ this.config = Object.assign({
64
+ precision: 8,
65
+ currency: '',
66
+ taxRate: 0.18,
67
+ tipRates: [10, 15, 20],
68
+ angleMode: 'deg',
69
+ onCalculate: null
70
+ }, config);
71
+
72
+ this.reset();
73
+ }
74
+
75
+ reset() {
76
+ this.currentInput = '0';
77
+ this.previousValue = null;
78
+ this.pendingOperator = null;
79
+ this.historyTokens = [];
80
+ this.hasJustCalculated = false;
81
+ this.hasPercentApplied = false;
82
+ this.rawPercent = null;
83
+ this.memory = 0;
84
+ this.tapeHistory = [];
85
+ }
86
+
87
+ inputDigit(digit) {
88
+ if (this.hasJustCalculated) {
89
+ this.currentInput = digit === '.' ? '0.' : String(digit);
90
+ this.previousValue = null;
91
+ this.pendingOperator = null;
92
+ this.historyTokens = [];
93
+ this.hasJustCalculated = false;
94
+ this.hasPercentApplied = false;
95
+ this.rawPercent = null;
96
+ return;
97
+ }
98
+
99
+ this.hasPercentApplied = false;
100
+ this.rawPercent = null;
101
+
102
+ if (digit === '.') {
103
+ if (!this.currentInput.includes('.')) {
104
+ this.currentInput = (this.currentInput === '' || this.currentInput === '0') ? '0.' : this.currentInput + '.';
105
+ }
106
+ } else if (digit === '00') {
107
+ if (this.currentInput === '0' || this.currentInput === '') {
108
+ this.currentInput = '0';
109
+ } else {
110
+ this.currentInput += '00';
111
+ }
112
+ } else {
113
+ if (this.currentInput === '0' || this.currentInput === '') {
114
+ this.currentInput = String(digit);
115
+ } else {
116
+ this.currentInput += String(digit);
117
+ }
118
+ }
119
+ }
120
+
121
+ inputOperator(op) {
122
+ const symbol = Formatter.getOperatorSymbol(op);
123
+
124
+ if (this.hasJustCalculated) {
125
+ const currentNum = this.parseCurrent();
126
+ this.previousValue = currentNum;
127
+ this.pendingOperator = op;
128
+ this.historyTokens = [Formatter.format(currentNum), symbol];
129
+ this.currentInput = '';
130
+ this.hasJustCalculated = false;
131
+ return;
132
+ }
133
+
134
+ if (this.previousValue === null) {
135
+ const currentNum = this.parseCurrent();
136
+ this.previousValue = currentNum;
137
+ this.pendingOperator = op;
138
+ this.historyTokens = [Formatter.format(currentNum), symbol];
139
+ this.currentInput = '';
140
+ return;
141
+ }
142
+
143
+ if (this.currentInput !== '') {
144
+ const nextNum = parseFloat(this.currentInput);
145
+ let calculated = 0;
146
+
147
+ if (this.hasPercentApplied) {
148
+ const prevOp = this.pendingOperator;
149
+ if (prevOp === '+') calculated = this.previousValue + nextNum;
150
+ else if (prevOp === '-') calculated = this.previousValue - nextNum;
151
+ else if (prevOp === '*' || prevOp === 'x' || prevOp === '×') calculated = nextNum;
152
+ else if (prevOp === '/' || prevOp === '÷') {
153
+ const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
154
+ calculated = frac === 0 ? 0 : this.previousValue / frac;
155
+ } else {
156
+ calculated = this.calculate(this.previousValue, prevOp, nextNum);
157
+ }
158
+ calculated = Formatter.round(calculated, this.config.precision);
159
+ this.hasPercentApplied = false;
160
+ this.rawPercent = null;
161
+ } else {
162
+ calculated = this.calculate(this.previousValue, this.pendingOperator, nextNum);
163
+ }
164
+
165
+ if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
166
+ this.historyTokens.push(symbol);
167
+ } else {
168
+ this.historyTokens.push(Formatter.format(nextNum), symbol);
169
+ }
170
+ this.previousValue = calculated;
171
+ this.pendingOperator = op;
172
+ this.currentInput = '';
173
+ return;
174
+ }
175
+
176
+ this.pendingOperator = op;
177
+ if (this.historyTokens.length > 0) {
178
+ this.historyTokens[this.historyTokens.length - 1] = symbol;
179
+ }
180
+ }
181
+
182
+ inputEquals() {
183
+ if (this.pendingOperator === null || this.previousValue === null) return this.parseCurrent();
184
+
185
+ const nextNum = this.currentInput !== '' ? parseFloat(this.currentInput) : this.previousValue;
186
+ let result = 0;
187
+
188
+ if (this.hasPercentApplied) {
189
+ const op = this.pendingOperator;
190
+ if (op === '+') result = this.previousValue + nextNum;
191
+ else if (op === '-') result = this.previousValue - nextNum;
192
+ else if (op === '*' || op === 'x' || op === '×') result = nextNum;
193
+ else if (op === '/' || op === '÷') {
194
+ const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
195
+ result = frac === 0 ? 0 : this.previousValue / frac;
196
+ } else {
197
+ result = this.calculate(this.previousValue, op, nextNum);
198
+ }
199
+ result = Formatter.round(result, this.config.precision);
200
+ this.hasPercentApplied = false;
201
+ this.rawPercent = null;
202
+ } else {
203
+ result = this.calculate(this.previousValue, this.pendingOperator, nextNum);
204
+ }
205
+
206
+ if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
207
+ this.historyTokens.push('=');
208
+ } else {
209
+ this.historyTokens.push(Formatter.format(nextNum), '=');
210
+ }
211
+
212
+ const historyStr = this.historyTokens.join(' ');
213
+ this.tapeHistory.unshift(`${historyStr} ${Formatter.format(result)}`);
214
+
215
+ if (typeof this.config.onCalculate === 'function') {
216
+ this.config.onCalculate(result, historyStr);
217
+ }
218
+
219
+ this.previousValue = result;
220
+ this.currentInput = String(result);
221
+ this.pendingOperator = null;
222
+ this.hasJustCalculated = true;
223
+ return result;
224
+ }
225
+
226
+ calculate(a, op, b) {
227
+ let res = 0;
228
+ switch (op) {
229
+ case '+': res = a + b; break;
230
+ case '-': res = a - b; break;
231
+ case '*':
232
+ case 'x':
233
+ case '×': res = a * b; break;
234
+ case '/':
235
+ case '÷': res = b === 0 ? 0 : a / b; break;
236
+ case '%':
237
+ case 'mod': res = a % b; break;
238
+ case '^':
239
+ case 'pow': res = Math.pow(a, b); break;
240
+ default: res = b;
241
+ }
242
+ return Formatter.round(res, this.config.precision);
243
+ }
244
+
245
+ toggleSign() {
246
+ if (this.currentInput !== '' && this.currentInput !== '0') {
247
+ if (this.currentInput.startsWith('-')) {
248
+ this.currentInput = this.currentInput.slice(1);
249
+ } else {
250
+ this.currentInput = '-' + this.currentInput;
251
+ }
252
+ } else if (this.previousValue !== null) {
253
+ this.previousValue = -this.previousValue;
254
+ this.currentInput = String(this.previousValue);
255
+ }
256
+ }
257
+
258
+ applyPercent() {
259
+ const current = this.parseCurrent();
260
+ if (this.previousValue !== null && this.pendingOperator !== null) {
261
+ const percentVal = (this.previousValue * current) / 100;
262
+ this.currentInput = String(Formatter.round(percentVal, this.config.precision));
263
+ this.hasPercentApplied = true;
264
+ this.rawPercent = current;
265
+ this.historyTokens = [
266
+ Formatter.format(this.previousValue),
267
+ Formatter.getOperatorSymbol(this.pendingOperator),
268
+ `${Formatter.format(current)}%`
269
+ ];
270
+ } else {
271
+ const percentVal = current / 100;
272
+ this.currentInput = String(Formatter.round(percentVal, this.config.precision));
273
+ this.hasPercentApplied = false;
274
+ this.rawPercent = null;
275
+ this.historyTokens = [`${Formatter.format(current)}%`];
276
+ }
277
+ }
278
+
279
+ backspace() {
280
+ if (this.hasJustCalculated) {
281
+ this.clearAll();
282
+ return;
283
+ }
284
+ if (this.currentInput.length > 1) {
285
+ this.currentInput = this.currentInput.slice(0, -1);
286
+ } else {
287
+ this.currentInput = '0';
288
+ }
289
+ }
290
+
291
+ clearAll() {
292
+ this.currentInput = '0';
293
+ this.previousValue = null;
294
+ this.pendingOperator = null;
295
+ this.historyTokens = [];
296
+ this.hasJustCalculated = false;
297
+ this.hasPercentApplied = false;
298
+ this.rawPercent = null;
299
+ }
300
+
301
+ parseCurrent() {
302
+ return parseFloat(this.currentInput) || 0;
303
+ }
304
+
305
+ getDisplayValue() {
306
+ let displayVal = this.currentInput;
307
+ if (displayVal === '' && this.previousValue !== null) {
308
+ displayVal = String(this.previousValue);
309
+ }
310
+ if (displayVal === '') displayVal = '0';
311
+ return displayVal;
312
+ }
313
+
314
+ getFormattedDisplay() {
315
+ return Formatter.format(this.getDisplayValue(), { currency: this.config.currency });
316
+ }
317
+
318
+ getHistoryString() {
319
+ return this.historyTokens.join(' ');
320
+ }
321
+ }
322
+
323
+ export const Calculator = {
324
+ precision: 8,
325
+
326
+ add(a, b, precision = this.precision) {
327
+ return Formatter.round(Number(a) + Number(b), precision);
328
+ },
329
+
330
+ subtract(a, b, precision = this.precision) {
331
+ return Formatter.round(Number(a) - Number(b), precision);
332
+ },
333
+
334
+ multiply(a, b, precision = this.precision) {
335
+ return Formatter.round(Number(a) * Number(b), precision);
336
+ },
337
+
338
+ divide(a, b, precision = this.precision) {
339
+ const divisor = Number(b);
340
+ if (divisor === 0) return 0;
341
+ return Formatter.round(Number(a) / divisor, precision);
342
+ },
343
+
344
+ modulo(a, b, precision = this.precision) {
345
+ const divisor = Number(b);
346
+ if (divisor === 0) return 0;
347
+ return Formatter.round(Number(a) % divisor, precision);
348
+ },
349
+
350
+ power(a, b, precision = this.precision) {
351
+ return Formatter.round(Math.pow(Number(a), Number(b)), precision);
352
+ },
353
+
354
+ percent(value, percentage, precision = this.precision) {
355
+ return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
356
+ },
357
+
358
+ addPercent(value, percentage, precision = this.precision) {
359
+ const delta = (Number(value) * Number(percentage)) / 100;
360
+ return Formatter.round(Number(value) + delta, precision);
361
+ },
362
+
363
+ subtractPercent(value, percentage, precision = this.precision) {
364
+ const delta = (Number(value) * Number(percentage)) / 100;
365
+ return Formatter.round(Number(value) - delta, precision);
366
+ },
367
+
368
+ multiplyPercent(value, percentage, precision = this.precision) {
369
+ return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
370
+ },
371
+
372
+ dividePercent(value, percentage, precision = this.precision) {
373
+ const frac = Number(percentage) / 100;
374
+ if (frac === 0) return 0;
375
+ return Formatter.round(Number(value) / frac, precision);
376
+ },
377
+
378
+ sin(val, angleMode = 'deg', precision = this.precision) {
379
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
380
+ return Formatter.round(Math.sin(rad), precision);
381
+ },
382
+
383
+ cos(val, angleMode = 'deg', precision = this.precision) {
384
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
385
+ return Formatter.round(Math.cos(rad), precision);
386
+ },
387
+
388
+ tan(val, angleMode = 'deg', precision = this.precision) {
389
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
390
+ return Formatter.round(Math.tan(rad), precision);
391
+ },
392
+
393
+ asin(val, angleMode = 'deg', precision = this.precision) {
394
+ const rad = Math.asin(Number(val));
395
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
396
+ return Formatter.round(res, precision);
397
+ },
398
+
399
+ acos(val, angleMode = 'deg', precision = this.precision) {
400
+ const rad = Math.acos(Number(val));
401
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
402
+ return Formatter.round(res, precision);
403
+ },
404
+
405
+ atan(val, angleMode = 'deg', precision = this.precision) {
406
+ const rad = Math.atan(Number(val));
407
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
408
+ return Formatter.round(res, precision);
409
+ },
410
+
411
+ sqrt(val, precision = this.precision) {
412
+ const n = Number(val);
413
+ return n >= 0 ? Formatter.round(Math.sqrt(n), precision) : 0;
414
+ },
415
+
416
+ sqr(val, precision = this.precision) {
417
+ return Formatter.round(Number(val) * Number(val), precision);
418
+ },
419
+
420
+ cube(val, precision = this.precision) {
421
+ return Formatter.round(Number(val) * Number(val) * Number(val), precision);
422
+ },
423
+
424
+ log(val, precision = this.precision) {
425
+ const n = Number(val);
426
+ return n > 0 ? Formatter.round(Math.log10(n), precision) : 0;
427
+ },
428
+
429
+ ln(val, precision = this.precision) {
430
+ const n = Number(val);
431
+ return n > 0 ? Formatter.round(Math.log(n), precision) : 0;
432
+ },
433
+
434
+ exp(val, precision = this.precision) {
435
+ return Formatter.round(Math.exp(Number(val)), precision);
436
+ },
437
+
438
+ reciprocal(val, precision = this.precision) {
439
+ const n = Number(val);
440
+ return n !== 0 ? Formatter.round(1 / n, precision) : 0;
441
+ },
442
+
443
+ abs(val) {
444
+ return Math.abs(Number(val));
445
+ },
446
+
447
+ factorial(n) {
448
+ const num = Math.floor(Number(n));
449
+ if (num < 0) return 0;
450
+ if (num === 0 || num === 1) return 1;
451
+ let r = 1;
452
+ for (let i = 2; i <= Math.min(num, 170); i++) r *= i;
453
+ return r;
454
+ },
455
+
456
+ tax(amount, rate = 0.18, isAdd = true, precision = this.precision) {
457
+ const amt = Number(amount);
458
+ const res = isAdd ? amt * (1 + rate) : amt / (1 + rate);
459
+ return Formatter.round(res, precision);
460
+ },
461
+
462
+ tip(amount, percent = 15, precision = this.precision) {
463
+ const amt = Number(amount);
464
+ const tipVal = (amt * percent) / 100;
465
+ return Formatter.round(amt + tipVal, precision);
466
+ },
467
+
468
+ discount(amount, percent = 10, precision = this.precision) {
469
+ const amt = Number(amount);
470
+ const discVal = (amt * percent) / 100;
471
+ return Formatter.round(amt - discVal, precision);
472
+ },
473
+
474
+ format(value, options) {
475
+ return Formatter.format(value, options);
476
+ },
477
+
478
+ executePerform(currentVal, expr) {
479
+ if (!expr) return { value: currentVal, history: '' };
480
+ const str = String(expr).trim();
481
+ const val = Number(currentVal) || 0;
482
+
483
+ const pctMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)\s*%$/);
484
+ if (pctMatch) {
485
+ const sign = pctMatch[1] || '+';
486
+ const pct = parseFloat(pctMatch[2]);
487
+ let result = val;
488
+ if (sign === '+' || !pctMatch[1]) result = this.addPercent(val, pct);
489
+ else if (sign === '-') result = this.subtractPercent(val, pct);
490
+ else if (sign === '*') result = this.multiplyPercent(val, pct);
491
+ else if (sign === '/') result = this.dividePercent(val, pct);
492
+ const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
493
+ return {
494
+ value: result,
495
+ history: `${Formatter.format(val)} ${opDisplay} ${pct}% =`
496
+ };
497
+ }
498
+
499
+ const numMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)$/);
500
+ if (numMatch) {
501
+ const sign = numMatch[1] || '+';
502
+ const num = parseFloat(numMatch[2]);
503
+ let result = val;
504
+ if (sign === '+') result = this.add(val, num);
505
+ else if (sign === '-') result = this.subtract(val, num);
506
+ else if (sign === '*') result = this.multiply(val, num);
507
+ else if (sign === '/') result = this.divide(val, num);
508
+ const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
509
+ return {
510
+ value: result,
511
+ history: `${Formatter.format(val)} ${opDisplay} ${Formatter.format(num)} =`
512
+ };
513
+ }
514
+
515
+ return { value: val, history: '' };
516
+ },
517
+
518
+ evaluate(expression, options = {}) {
519
+ if (!expression || typeof expression !== 'string') return 0;
520
+ const precision = options.precision ?? this.precision;
521
+
522
+ let clean = expression
523
+ .replace(/×/g, '*')
524
+ .replace(/÷/g, '/')
525
+ .replace(/−/g, '-')
526
+ .replace(/,/g, '')
527
+ .trim();
528
+
529
+ if (!clean) return 0;
530
+
531
+ const tokens = [];
532
+ let i = 0;
533
+ let lastToken = null;
534
+
535
+ while (i < clean.length) {
536
+ const char = clean[i];
537
+
538
+ if (/\s/.test(char)) {
539
+ i++;
540
+ continue;
541
+ }
542
+
543
+ if (/\d/.test(char) || (char === '.' && i + 1 < clean.length && /\d/.test(clean[i + 1]))) {
544
+ let numStr = '';
545
+ while (i < clean.length && (/[\d.]/.test(clean[i]))) {
546
+ numStr += clean[i];
547
+ i++;
548
+ }
549
+ let isPct = false;
550
+ while (i < clean.length && /\s/.test(clean[i])) i++;
551
+ if (i < clean.length && clean[i] === '%') {
552
+ isPct = true;
553
+ i++;
554
+ }
555
+ const numVal = parseFloat(numStr);
556
+ tokens.push({ type: 'number', value: numVal, isPercent: isPct });
557
+ lastToken = 'number';
558
+ continue;
559
+ }
560
+
561
+ if (char === '+' || char === '-' || char === '*' || char === '/' || char === '^' || char === '(' || char === ')') {
562
+ const isUnary = (char === '+' || char === '-') &&
563
+ (lastToken === null || lastToken === 'operator' || lastToken === 'lparen');
564
+
565
+ if (isUnary) {
566
+ tokens.push({ type: 'unary', value: char });
567
+ lastToken = 'unary';
568
+ } else if (char === '(') {
569
+ tokens.push({ type: 'lparen', value: '(' });
570
+ lastToken = 'lparen';
571
+ } else if (char === ')') {
572
+ tokens.push({ type: 'rparen', value: ')' });
573
+ lastToken = 'rparen';
574
+ } else {
575
+ tokens.push({ type: 'operator', value: char });
576
+ lastToken = 'operator';
577
+ }
578
+ i++;
579
+ continue;
580
+ }
581
+
582
+ i++;
583
+ }
584
+
585
+ if (tokens.length === 0) return 0;
586
+
587
+ const precedence = { '+': 1, '-': 1, '*': 2, '/': 2, '^': 3, 'u+': 4, 'u-': 4 };
588
+ const outputQueue = [];
589
+ const opStack = [];
590
+
591
+ for (let t = 0; t < tokens.length; t++) {
592
+ const token = tokens[t];
593
+
594
+ if (token.type === 'number') {
595
+ outputQueue.push(token);
596
+ } else if (token.type === 'unary') {
597
+ opStack.push({ type: 'unary', op: 'u' + token.value });
598
+ } else if (token.type === 'operator') {
599
+ const op = token.value;
600
+ while (
601
+ opStack.length > 0 &&
602
+ opStack[opStack.length - 1].type !== 'lparen' &&
603
+ ((precedence[opStack[opStack.length - 1].op] > precedence[op]) ||
604
+ (precedence[opStack[opStack.length - 1].op] === precedence[op] && op !== '^'))
605
+ ) {
606
+ outputQueue.push(opStack.pop());
607
+ }
608
+ opStack.push({ type: 'operator', op: op });
609
+ } else if (token.type === 'lparen') {
610
+ opStack.push(token);
611
+ } else if (token.type === 'rparen') {
612
+ while (opStack.length > 0 && opStack[opStack.length - 1].type !== 'lparen') {
613
+ outputQueue.push(opStack.pop());
614
+ }
615
+ if (opStack.length > 0 && opStack[opStack.length - 1].type === 'lparen') {
616
+ opStack.pop();
617
+ }
618
+ }
619
+ }
620
+
621
+ while (opStack.length > 0) {
622
+ const op = opStack.pop();
623
+ if (op.type !== 'lparen') {
624
+ outputQueue.push(op);
625
+ }
626
+ }
627
+
628
+ const evalStack = [];
629
+ for (let k = 0; k < outputQueue.length; k++) {
630
+ const item = outputQueue[k];
631
+
632
+ if (item.type === 'number') {
633
+ evalStack.push(item);
634
+ } else if (item.type === 'unary') {
635
+ const operand = evalStack.pop();
636
+ const val = typeof operand === 'object' ? operand.value : operand;
637
+ const res = item.op === 'u-' ? -val : val;
638
+ evalStack.push({ type: 'number', value: res });
639
+ } else if (item.type === 'operator') {
640
+ const rightToken = evalStack.pop();
641
+ const leftToken = evalStack.pop();
642
+
643
+ const left = typeof leftToken === 'object' ? leftToken.value : Number(leftToken);
644
+ let right = typeof rightToken === 'object' ? rightToken.value : Number(rightToken);
645
+
646
+ if (rightToken && rightToken.isPercent) {
647
+ if (item.op === '+' || item.op === '-') {
648
+ right = (left * right) / 100;
649
+ } else if (item.op === '*' || item.op === '/') {
650
+ right = right / 100;
651
+ }
652
+ }
653
+
654
+ let res = 0;
655
+ switch (item.op) {
656
+ case '+': res = left + right; break;
657
+ case '-': res = left - right; break;
658
+ case '*': res = left * right; break;
659
+ case '/': res = right === 0 ? 0 : left / right; break;
660
+ case '^': res = Math.pow(left, right); break;
661
+ }
662
+ evalStack.push({ type: 'number', value: res });
663
+ }
664
+ }
665
+
666
+ if (evalStack.length === 0) return 0;
667
+ const finalResult = evalStack[0].value ?? evalStack[0];
668
+ return Formatter.round(Number(finalResult), precision);
669
+ },
670
+
671
+ createStream(config = {}) {
672
+ return new StreamCalculator(config);
673
+ }
674
+ };
675
+
676
+ export class AdminUIUXCalculator {
677
+ constructor(element, config = {}) {
678
+ if (typeof element === 'string') {
679
+ this.wrapper = document.querySelector(element);
680
+ } else {
681
+ this.wrapper = element;
682
+ }
683
+
684
+ if (!this.wrapper) return;
685
+ if (this.wrapper._calculatorInstance) return;
686
+ this.wrapper._calculatorInstance = this;
687
+
688
+ this.config = Object.assign({
689
+ type: this.wrapper.getAttribute('data-calculator-type') || this.wrapper.getAttribute('data-calendar-type') || this.wrapper.getAttribute('data-calc-mode') || 'basic',
690
+ precision: parseInt(this.wrapper.getAttribute('data-calc-precision'), 10) || (config.precision ?? 8),
691
+ angleMode: 'deg',
692
+ currency: this.wrapper.getAttribute('data-calc-currency') || config.currency || '',
693
+ taxRate: config.taxRate ?? 0.18,
694
+ tipRates: config.tipRates ?? [10, 15, 20],
695
+ quickPresets: config.quickPresets ?? [50, 100, 500, 1000],
696
+ enableKeyboard: config.enableKeyboard ?? true,
697
+ showTape: config.showTape ?? false,
698
+ onCalculate: config.onCalculate || null
699
+ }, config);
700
+
701
+ this.engine = new StreamCalculator(this.config);
702
+ this.hasInteracted = false;
703
+ this.memory = 0;
704
+
705
+ this.displayEl = this.wrapper.querySelector('.calculator-display, [data-calc-display]');
706
+ this.historyEl = this.wrapper.querySelector('.calculator-history, [data-calc-history]');
707
+ this.memoryBadge = this.wrapper.querySelector('[data-calc-memory-badge]');
708
+ this.angleModeBtn = this.wrapper.querySelector('[data-calc-angle-toggle]');
709
+ this.tapeListEl = this.wrapper.querySelector('.calculator-tape, [data-calc-tape]');
710
+
711
+ this.initEventListeners();
712
+ this.updateDisplay();
713
+ }
714
+
715
+ initEventListeners() {
716
+ this.wrapper.querySelectorAll('[data-calc-action], [data-calculator-perfom], [data-calculator-perform]').forEach(btn => {
717
+ btn.addEventListener('click', (e) => {
718
+ e.preventDefault();
719
+ const performExpr = btn.getAttribute('data-calculator-perfom') || btn.getAttribute('data-calculator-perform');
720
+ if (performExpr) {
721
+ this.executePerform(performExpr);
722
+ return;
723
+ }
724
+ const action = btn.getAttribute('data-calc-action');
725
+ const val = btn.getAttribute('data-val');
726
+ this.handleAction(action, val);
727
+ });
728
+ });
729
+
730
+ if (this.config.enableKeyboard) {
731
+ this.wrapper.addEventListener('keydown', (e) => {
732
+ this.handleKeyDown(e);
733
+ });
734
+ if (!this.wrapper.hasAttribute('tabindex')) {
735
+ this.wrapper.setAttribute('tabindex', '0');
736
+ }
737
+ }
738
+ }
739
+
740
+ handleKeyDown(e) {
741
+ if (['input', 'textarea'].includes(document.activeElement?.tagName?.toLowerCase())) return;
742
+
743
+ const key = e.key;
744
+ if (!isNaN(key)) {
745
+ e.preventDefault();
746
+ this.handleAction('num', key);
747
+ } else if (key === '.') {
748
+ e.preventDefault();
749
+ this.handleAction('num', '.');
750
+ } else if (key === '+' || key === '-' || key === '*' || key === '/') {
751
+ e.preventDefault();
752
+ this.handleAction('operator', key);
753
+ } else if (key === 'Enter' || key === '=') {
754
+ e.preventDefault();
755
+ this.handleAction('equals');
756
+ } else if (key === 'Backspace') {
757
+ e.preventDefault();
758
+ this.handleAction('backspace');
759
+ } else if (key === 'Escape') {
760
+ e.preventDefault();
761
+ this.handleAction('clear');
762
+ } else if (key === '%') {
763
+ e.preventDefault();
764
+ this.handleAction('percent');
765
+ }
766
+ }
767
+
768
+ handleAction(action, val) {
769
+ this.hasInteracted = true;
770
+ switch (action) {
771
+ case 'num': this.engine.inputDigit(val); break;
772
+ case 'operator': this.engine.inputOperator(val); break;
773
+ case 'equals':
774
+ this.engine.inputEquals();
775
+ this.syncTape();
776
+ break;
777
+ case 'clear': this.engine.clearAll(); break;
778
+ case 'clear-entry':
779
+ case 'backspace': this.engine.backspace(); break;
780
+ case 'negate': this.engine.toggleSign(); break;
781
+ case 'percent': this.engine.applyPercent(); break;
782
+ case 'scientific': this.applyScientific(val); break;
783
+ case 'memory': this.handleMemory(val); break;
784
+ case 'angle-toggle': this.toggleAngleMode(); break;
785
+ case 'preset': this.applyPreset(parseFloat(val)); break;
786
+ case 'tax-add': this.applyTax(true); break;
787
+ case 'tax-sub': this.applyTax(false); break;
788
+ case 'tip': this.applyTip(parseFloat(val)); break;
789
+ case 'discount': this.applyDiscount(parseFloat(val)); break;
790
+ }
791
+ this.updateDisplay();
792
+ }
793
+
794
+ applyScientific(func) {
795
+ const val = this.engine.parseCurrent();
796
+ let res = val;
797
+ switch (func) {
798
+ case 'sin': res = Calculator.sin(val, this.config.angleMode, this.config.precision); break;
799
+ case 'cos': res = Calculator.cos(val, this.config.angleMode, this.config.precision); break;
800
+ case 'tan': res = Calculator.tan(val, this.config.angleMode, this.config.precision); break;
801
+ case 'asin': res = Calculator.asin(val, this.config.angleMode, this.config.precision); break;
802
+ case 'acos': res = Calculator.acos(val, this.config.angleMode, this.config.precision); break;
803
+ case 'atan': res = Calculator.atan(val, this.config.angleMode, this.config.precision); break;
804
+ case 'sqrt': res = Calculator.sqrt(val, this.config.precision); break;
805
+ case 'sqr': res = Calculator.sqr(val, this.config.precision); break;
806
+ case 'cube': res = Calculator.cube(val, this.config.precision); break;
807
+ case 'log': res = Calculator.log(val, this.config.precision); break;
808
+ case 'ln': res = Calculator.ln(val, this.config.precision); break;
809
+ case 'exp': res = Calculator.exp(val, this.config.precision); break;
810
+ case '1/x': res = Calculator.reciprocal(val, this.config.precision); break;
811
+ case 'abs': res = Calculator.abs(val); break;
812
+ case 'pi': res = Formatter.round(Math.PI, this.config.precision); break;
813
+ case 'e': res = Formatter.round(Math.E, this.config.precision); break;
814
+ case 'fact': res = Calculator.factorial(val); break;
815
+ }
816
+
817
+ this.engine.currentInput = String(res);
818
+ if (this.engine.hasJustCalculated) {
819
+ this.engine.historyTokens = [`${func}(${Formatter.format(val)}) =`];
820
+ }
821
+ }
822
+
823
+ handleMemory(action) {
824
+ const val = this.engine.parseCurrent();
825
+ switch (action) {
826
+ case 'mc': this.memory = 0; break;
827
+ case 'mr':
828
+ this.engine.currentInput = String(this.memory);
829
+ this.engine.hasJustCalculated = false;
830
+ break;
831
+ case 'm+': this.memory += val; break;
832
+ case 'm-': this.memory -= val; break;
833
+ case 'ms': this.memory = val; break;
834
+ }
835
+ if (this.memoryBadge) {
836
+ this.memoryBadge.style.display = this.memory !== 0 ? 'inline-block' : 'none';
837
+ }
838
+ }
839
+
840
+ toggleAngleMode() {
841
+ this.config.angleMode = this.config.angleMode === 'deg' ? 'rad' : 'deg';
842
+ if (this.angleModeBtn) {
843
+ this.angleModeBtn.textContent = this.config.angleMode.toUpperCase();
844
+ }
845
+ }
846
+
847
+ applyPreset(amount) {
848
+ const current = this.engine.parseCurrent();
849
+ this.engine.currentInput = String(current + amount);
850
+ this.engine.hasJustCalculated = false;
851
+ }
852
+
853
+ applyTax(isAdd) {
854
+ const current = this.engine.parseCurrent();
855
+ const rate = this.config.taxRate;
856
+ const result = Calculator.tax(current, rate, isAdd, this.config.precision);
857
+ this.engine.currentInput = String(result);
858
+ this.engine.historyTokens = [`${Formatter.format(current)} ${isAdd ? '+' : '-'} ${Math.round(rate * 100)}% Tax =`];
859
+ this.engine.hasJustCalculated = true;
860
+ }
861
+
862
+ applyTip(percent) {
863
+ const current = this.engine.parseCurrent();
864
+ const result = Calculator.tip(current, percent, this.config.precision);
865
+ this.engine.currentInput = String(result);
866
+ this.engine.historyTokens = [`${Formatter.format(current)} + ${percent}% Tip =`];
867
+ this.engine.hasJustCalculated = true;
868
+ }
869
+
870
+ applyDiscount(percent) {
871
+ const current = this.engine.parseCurrent();
872
+ const result = Calculator.discount(current, percent, this.config.precision);
873
+ this.engine.currentInput = String(result);
874
+ this.engine.historyTokens = [`${Formatter.format(current)} - ${percent}% Off =`];
875
+ this.engine.hasJustCalculated = true;
876
+ }
877
+
878
+ executePerform(expr) {
879
+ if (!expr) return;
880
+ this.hasInteracted = true;
881
+ const current = this.engine.parseCurrent();
882
+ const res = Calculator.executePerform(current, expr);
883
+ this.engine.currentInput = String(res.value);
884
+ if (res.history) {
885
+ this.engine.historyTokens = [res.history];
886
+ }
887
+ this.engine.hasJustCalculated = true;
888
+ this.updateDisplay();
889
+ }
890
+
891
+ getOutputTargets() {
892
+ const selector = this.wrapper.getAttribute('data-calculator-output') ||
893
+ this.wrapper.getAttribute('data-output-target') ||
894
+ this.config.outputTarget ||
895
+ this.config.output;
896
+ if (!selector) return [];
897
+ try {
898
+ return Array.from(document.querySelectorAll(selector));
899
+ } catch (e) {
900
+ return [];
901
+ }
902
+ }
903
+
904
+ getCalculationTargets() {
905
+ const selector = this.wrapper.getAttribute('data-calculator-calculation') ||
906
+ this.wrapper.getAttribute('data-calculation-target') ||
907
+ this.config.calculationTarget ||
908
+ this.config.calculation;
909
+ if (!selector) return [];
910
+ try {
911
+ return Array.from(document.querySelectorAll(selector));
912
+ } catch (e) {
913
+ return [];
914
+ }
915
+ }
916
+
917
+ updateOutputTargets(displayStr, rawVal) {
918
+ const targets = this.getOutputTargets();
919
+ if (targets.length === 0) return;
920
+
921
+ targets.forEach(target => {
922
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
923
+ target.value = displayStr;
924
+ target.dispatchEvent(new Event('input', { bubbles: true }));
925
+ target.dispatchEvent(new Event('change', { bubbles: true }));
926
+ } else {
927
+ target.textContent = displayStr;
928
+ }
929
+ });
930
+
931
+ this.wrapper.dispatchEvent(new CustomEvent('calculator:change', {
932
+ bubbles: true,
933
+ detail: {
934
+ value: rawVal,
935
+ formatted: displayStr,
936
+ history: this.engine.getHistoryString(),
937
+ calculator: this
938
+ }
939
+ }));
940
+ }
941
+
942
+ updateCalculationTargets(calculationStr) {
943
+ const targets = this.getCalculationTargets();
944
+ if (targets.length === 0) return;
945
+
946
+ targets.forEach(target => {
947
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
948
+ target.value = calculationStr;
949
+ target.dispatchEvent(new Event('input', { bubbles: true }));
950
+ target.dispatchEvent(new Event('change', { bubbles: true }));
951
+ } else {
952
+ target.textContent = calculationStr;
953
+ }
954
+ });
955
+ }
956
+
957
+ updateDisplay() {
958
+ const currency = this.config.currency ? `${this.config.currency} ` : '';
959
+ const rawVal = this.engine.parseCurrent();
960
+ let finalDisplay = this.engine.getDisplayValue();
961
+
962
+ const parts = finalDisplay.split('.');
963
+ const formattedInt = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
964
+ finalDisplay = parts.length > 1 ? `${formattedInt}.${parts[1]}` : formattedInt;
965
+
966
+ if (this.displayEl) {
967
+ this.displayEl.textContent = `${currency}${finalDisplay}`;
968
+ }
969
+
970
+ const historyStr = this.engine.getHistoryString();
971
+ if (this.historyEl) {
972
+ if (historyStr) {
973
+ this.historyEl.textContent = historyStr;
974
+ } else {
975
+ this.historyEl.innerHTML = '&nbsp;';
976
+ }
977
+ }
978
+
979
+ this.scrollToEnd(this.historyEl);
980
+ this.scrollToEnd(this.displayEl);
981
+
982
+ if (this.hasInteracted) {
983
+ this.updateOutputTargets(`${currency}${finalDisplay}`, rawVal);
984
+ this.updateCalculationTargets(historyStr || 'Result:');
985
+ }
986
+ }
987
+
988
+ isRTL() {
989
+ if (typeof document === 'undefined') return false;
990
+ return (
991
+ document.documentElement.getAttribute('dir') === 'rtl' ||
992
+ document.documentElement.getAttribute('data-bs-direction') === 'rtl' ||
993
+ document.body?.getAttribute('dir') === 'rtl' ||
994
+ this.wrapper?.getAttribute('dir') === 'rtl' ||
995
+ getComputedStyle(this.wrapper || document.documentElement).direction === 'rtl'
996
+ );
997
+ }
998
+
999
+ scrollToEnd(element) {
1000
+ if (!element) return;
1001
+ const rtl = this.isRTL();
1002
+ element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
1003
+ if (typeof requestAnimationFrame === 'function') {
1004
+ requestAnimationFrame(() => {
1005
+ if (element) {
1006
+ element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
1007
+ }
1008
+ });
1009
+ }
1010
+ }
1011
+
1012
+ syncTape() {
1013
+ if (!this.tapeListEl) return;
1014
+ this.tapeListEl.innerHTML = '';
1015
+ this.engine.tapeHistory.forEach((entry, idx) => {
1016
+ const item = document.createElement('div');
1017
+ item.className = 'd-flex justify-content-between py-1 border-bottom border-light-subtle';
1018
+ item.innerHTML = `<span class="text-secondary">${this.engine.tapeHistory.length - idx}.</span> <span>${entry}</span>`;
1019
+ this.tapeListEl.appendChild(item);
1020
+ });
1021
+ }
1022
+
1023
+ static create(target, options = {}) {
1024
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1025
+ if (!el) return null;
1026
+ return new AdminUIUXCalculator(el, options);
1027
+ }
1028
+
1029
+ static init(target, options = {}) {
1030
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1031
+ if (!el) return null;
1032
+ return new AdminUIUXCalculator(el, options);
1033
+ }
1034
+
1035
+ static initAll() {
1036
+ if (typeof document === 'undefined') return;
1037
+ document.querySelectorAll('.calculator, [data-calculator-type], [data-calendar-type]').forEach(el => {
1038
+ if (!el._calculatorInstance) {
1039
+ new AdminUIUXCalculator(el);
1040
+ }
1041
+ });
1042
+ }
1043
+ }
1044
+
1045
+ AdminUIUXCalculator.Calculator = Calculator;
1046
+ AdminUIUXCalculator.Formatter = Formatter;
1047
+ AdminUIUXCalculator.StreamCalculator = StreamCalculator;
1048
+
1049
+ export default AdminUIUXCalculator;