@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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +223 -2
- package/dist/calculator.esm.js +1049 -0
- package/dist/calculator.js +1234 -0
- package/dist/calculator.min.js +18 -0
- package/package.json +50 -4
- package/src/calculator.js +641 -0
- package/src/formatter.js +88 -0
- package/src/index.js +434 -0
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adminuiux/calculator
|
|
3
|
+
* AdminUIUX - Production-Ready Universal Calculator Package
|
|
4
|
+
*
|
|
5
|
+
* Lightweight, zero-dependency vanilla JavaScript calculator engine & DOM component.
|
|
6
|
+
* Works seamlessly in Browser (direct <script> / CDN), CommonJS (Node.js), and ES Modules.
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @author AdminUIUX (adminuiux.com)
|
|
10
|
+
* @license MIT
|
|
11
|
+
*
|
|
12
|
+
* Public APIs:
|
|
13
|
+
* - Calculator: Pure mathematical calculation & evaluation engine (framework-independent)
|
|
14
|
+
* - Formatter: High-precision number rounding, currency, and thousand-separator formatting
|
|
15
|
+
* - StreamCalculator: Decoupled desk/stream calculator state machine (digits, chained ops, memory, tape)
|
|
16
|
+
* - AdminUIUXCalculator: DOM-binding component connecting HTML buttons, displays, outputs & keyboard
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
(function (global, factory) {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
23
|
+
// CommonJS / Node.js
|
|
24
|
+
module.exports = factory();
|
|
25
|
+
} else if (typeof define === 'function' && define.amd) {
|
|
26
|
+
// AMD
|
|
27
|
+
define([], factory);
|
|
28
|
+
} else {
|
|
29
|
+
// Browser Global / CDN script
|
|
30
|
+
const exportsObj = factory();
|
|
31
|
+
global.AdminUIUXCalculator = exportsObj.AdminUIUXCalculator;
|
|
32
|
+
global.Calculator = exportsObj.Calculator;
|
|
33
|
+
global.Formatter = exportsObj.Formatter;
|
|
34
|
+
global.StreamCalculator = exportsObj.StreamCalculator;
|
|
35
|
+
}
|
|
36
|
+
})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this, function () {
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
/* ==========================================================================
|
|
40
|
+
1. FORMATTER UTILITIES
|
|
41
|
+
========================================================================== */
|
|
42
|
+
const Formatter = {
|
|
43
|
+
/**
|
|
44
|
+
* Round a number to a fixed decimal precision avoiding IEEE 754 floating point artifacts.
|
|
45
|
+
* @param {number} value
|
|
46
|
+
* @param {number} [precision=8]
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
round(value, precision = 8) {
|
|
50
|
+
if (!isFinite(value) || isNaN(value)) return 0;
|
|
51
|
+
const p = Math.pow(10, precision);
|
|
52
|
+
return Math.round((value + Number.EPSILON) * p) / p;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a number with thousand commas and optional currency prefix.
|
|
57
|
+
* @param {number|string} num
|
|
58
|
+
* @param {Object} [options={}]
|
|
59
|
+
* @param {string} [options.currency=''] - Currency prefix (e.g. '$', '€')
|
|
60
|
+
* @param {number} [options.precision] - Optional fixed decimal precision to round to
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
format(num, options = {}) {
|
|
64
|
+
if (num === null || num === undefined || num === '') return '';
|
|
65
|
+
const parsed = typeof num === 'number' ? num : parseFloat(num);
|
|
66
|
+
if (isNaN(parsed)) return 'Error';
|
|
67
|
+
|
|
68
|
+
let targetNum = num;
|
|
69
|
+
if (typeof options.precision === 'number') {
|
|
70
|
+
targetNum = this.round(parsed, options.precision);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const str = String(targetNum);
|
|
74
|
+
if (str.includes('e') || str.includes('E')) return str;
|
|
75
|
+
|
|
76
|
+
const parts = str.split('.');
|
|
77
|
+
const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
78
|
+
const formatted = parts.length > 1 ? `${intPart}.${parts[1]}` : intPart;
|
|
79
|
+
|
|
80
|
+
if (options.currency) {
|
|
81
|
+
return `${options.currency} ${formatted}`;
|
|
82
|
+
}
|
|
83
|
+
return formatted;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse formatted numeric string into clean number.
|
|
88
|
+
* @param {string|number} str
|
|
89
|
+
* @returns {number}
|
|
90
|
+
*/
|
|
91
|
+
parse(str) {
|
|
92
|
+
if (typeof str === 'number') return isNaN(str) ? 0 : str;
|
|
93
|
+
if (!str || typeof str !== 'string') return 0;
|
|
94
|
+
const clean = str.replace(/[^0-9.-]/g, '');
|
|
95
|
+
const val = parseFloat(clean);
|
|
96
|
+
return isNaN(val) ? 0 : val;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert standard math code operators into display symbols.
|
|
101
|
+
* @param {string} op
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
getOperatorSymbol(op) {
|
|
105
|
+
switch (op) {
|
|
106
|
+
case '*':
|
|
107
|
+
case 'x':
|
|
108
|
+
return '×';
|
|
109
|
+
case '/':
|
|
110
|
+
return '÷';
|
|
111
|
+
case '-':
|
|
112
|
+
return '−';
|
|
113
|
+
default:
|
|
114
|
+
return op;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/* ==========================================================================
|
|
120
|
+
2. CORE PURE CALCULATOR ENGINE (Framework & DOM Independent)
|
|
121
|
+
========================================================================== */
|
|
122
|
+
const Calculator = {
|
|
123
|
+
precision: 8,
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Basic Arithmetic Operations
|
|
127
|
+
*/
|
|
128
|
+
add(a, b, precision = this.precision) {
|
|
129
|
+
return Formatter.round(Number(a) + Number(b), precision);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
subtract(a, b, precision = this.precision) {
|
|
133
|
+
return Formatter.round(Number(a) - Number(b), precision);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
multiply(a, b, precision = this.precision) {
|
|
137
|
+
return Formatter.round(Number(a) * Number(b), precision);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
divide(a, b, precision = this.precision) {
|
|
141
|
+
const divisor = Number(b);
|
|
142
|
+
if (divisor === 0) return 0;
|
|
143
|
+
return Formatter.round(Number(a) / divisor, precision);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
modulo(a, b, precision = this.precision) {
|
|
147
|
+
const divisor = Number(b);
|
|
148
|
+
if (divisor === 0) return 0;
|
|
149
|
+
return Formatter.round(Number(a) % divisor, precision);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
power(a, b, precision = this.precision) {
|
|
153
|
+
return Formatter.round(Math.pow(Number(a), Number(b)), precision);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Percentage Calculations
|
|
158
|
+
*/
|
|
159
|
+
percent(value, percentage, precision = this.precision) {
|
|
160
|
+
return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
addPercent(value, percentage, precision = this.precision) {
|
|
164
|
+
const delta = (Number(value) * Number(percentage)) / 100;
|
|
165
|
+
return Formatter.round(Number(value) + delta, precision);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
subtractPercent(value, percentage, precision = this.precision) {
|
|
169
|
+
const delta = (Number(value) * Number(percentage)) / 100;
|
|
170
|
+
return Formatter.round(Number(value) - delta, precision);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
multiplyPercent(value, percentage, precision = this.precision) {
|
|
174
|
+
return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
dividePercent(value, percentage, precision = this.precision) {
|
|
178
|
+
const frac = Number(percentage) / 100;
|
|
179
|
+
if (frac === 0) return 0;
|
|
180
|
+
return Formatter.round(Number(value) / frac, precision);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Scientific & Math Functions
|
|
185
|
+
*/
|
|
186
|
+
sin(val, angleMode = 'deg', precision = this.precision) {
|
|
187
|
+
const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
|
|
188
|
+
return Formatter.round(Math.sin(rad), precision);
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
cos(val, angleMode = 'deg', precision = this.precision) {
|
|
192
|
+
const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
|
|
193
|
+
return Formatter.round(Math.cos(rad), precision);
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
tan(val, angleMode = 'deg', precision = this.precision) {
|
|
197
|
+
const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
|
|
198
|
+
return Formatter.round(Math.tan(rad), precision);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
asin(val, angleMode = 'deg', precision = this.precision) {
|
|
202
|
+
const rad = Math.asin(Number(val));
|
|
203
|
+
const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
|
|
204
|
+
return Formatter.round(res, precision);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
acos(val, angleMode = 'deg', precision = this.precision) {
|
|
208
|
+
const rad = Math.acos(Number(val));
|
|
209
|
+
const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
|
|
210
|
+
return Formatter.round(res, precision);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
atan(val, angleMode = 'deg', precision = this.precision) {
|
|
214
|
+
const rad = Math.atan(Number(val));
|
|
215
|
+
const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
|
|
216
|
+
return Formatter.round(res, precision);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
sqrt(val, precision = this.precision) {
|
|
220
|
+
const n = Number(val);
|
|
221
|
+
return n >= 0 ? Formatter.round(Math.sqrt(n), precision) : 0;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
sqr(val, precision = this.precision) {
|
|
225
|
+
return Formatter.round(Number(val) * Number(val), precision);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
cube(val, precision = this.precision) {
|
|
229
|
+
return Formatter.round(Number(val) * Number(val) * Number(val), precision);
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
log(val, precision = this.precision) {
|
|
233
|
+
const n = Number(val);
|
|
234
|
+
return n > 0 ? Formatter.round(Math.log10(n), precision) : 0;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
ln(val, precision = this.precision) {
|
|
238
|
+
const n = Number(val);
|
|
239
|
+
return n > 0 ? Formatter.round(Math.log(n), precision) : 0;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
exp(val, precision = this.precision) {
|
|
243
|
+
return Formatter.round(Math.exp(Number(val)), precision);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
reciprocal(val, precision = this.precision) {
|
|
247
|
+
const n = Number(val);
|
|
248
|
+
return n !== 0 ? Formatter.round(1 / n, precision) : 0;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
abs(val) {
|
|
252
|
+
return Math.abs(Number(val));
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
factorial(n) {
|
|
256
|
+
const num = Math.floor(Number(n));
|
|
257
|
+
if (num < 0) return 0;
|
|
258
|
+
if (num === 0 || num === 1) return 1;
|
|
259
|
+
let r = 1;
|
|
260
|
+
for (let i = 2; i <= Math.min(num, 170); i++) r *= i;
|
|
261
|
+
return r;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Financial Helpers
|
|
266
|
+
*/
|
|
267
|
+
tax(amount, rate = 0.18, isAdd = true, precision = this.precision) {
|
|
268
|
+
const amt = Number(amount);
|
|
269
|
+
const res = isAdd ? amt * (1 + rate) : amt / (1 + rate);
|
|
270
|
+
return Formatter.round(res, precision);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
tip(amount, percent = 15, precision = this.precision) {
|
|
274
|
+
const amt = Number(amount);
|
|
275
|
+
const tipVal = (amt * percent) / 100;
|
|
276
|
+
return Formatter.round(amt + tipVal, precision);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
discount(amount, percent = 10, precision = this.precision) {
|
|
280
|
+
const amt = Number(amount);
|
|
281
|
+
const discVal = (amt * percent) / 100;
|
|
282
|
+
return Formatter.round(amt - discVal, precision);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Format shorthand helper
|
|
287
|
+
*/
|
|
288
|
+
format(value, options) {
|
|
289
|
+
return Formatter.format(value, options);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Execute action expression string (e.g. "+10%", "-15%", "+500", "*2")
|
|
294
|
+
* @param {number} currentVal
|
|
295
|
+
* @param {string} expr
|
|
296
|
+
* @returns {{ value: number, history: string }}
|
|
297
|
+
*/
|
|
298
|
+
executePerform(currentVal, expr) {
|
|
299
|
+
if (!expr) return { value: currentVal, history: '' };
|
|
300
|
+
const str = String(expr).trim();
|
|
301
|
+
const val = Number(currentVal) || 0;
|
|
302
|
+
|
|
303
|
+
// Percentage modifier (e.g. "+15%", "-10%", "20%")
|
|
304
|
+
const pctMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)\s*%$/);
|
|
305
|
+
if (pctMatch) {
|
|
306
|
+
const sign = pctMatch[1] || '+';
|
|
307
|
+
const pct = parseFloat(pctMatch[2]);
|
|
308
|
+
let result = val;
|
|
309
|
+
if (sign === '+' || !pctMatch[1]) {
|
|
310
|
+
result = this.addPercent(val, pct);
|
|
311
|
+
} else if (sign === '-') {
|
|
312
|
+
result = this.subtractPercent(val, pct);
|
|
313
|
+
} else if (sign === '*') {
|
|
314
|
+
result = this.multiplyPercent(val, pct);
|
|
315
|
+
} else if (sign === '/') {
|
|
316
|
+
result = this.dividePercent(val, pct);
|
|
317
|
+
}
|
|
318
|
+
const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
|
|
319
|
+
return {
|
|
320
|
+
value: result,
|
|
321
|
+
history: `${Formatter.format(val)} ${opDisplay} ${pct}% =`
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Numeric offset/multiplier (e.g. "+100", "-50", "*2", "/4")
|
|
326
|
+
const numMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)$/);
|
|
327
|
+
if (numMatch) {
|
|
328
|
+
const sign = numMatch[1] || '+';
|
|
329
|
+
const num = parseFloat(numMatch[2]);
|
|
330
|
+
let result = val;
|
|
331
|
+
if (sign === '+') result = this.add(val, num);
|
|
332
|
+
else if (sign === '-') result = this.subtract(val, num);
|
|
333
|
+
else if (sign === '*') result = this.multiply(val, num);
|
|
334
|
+
else if (sign === '/') result = this.divide(val, num);
|
|
335
|
+
const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
|
|
336
|
+
return {
|
|
337
|
+
value: result,
|
|
338
|
+
history: `${Formatter.format(val)} ${opDisplay} ${Formatter.format(num)} =`
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { value: val, history: '' };
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Mathematical Expression Parser & Evaluator (BODMAS / Operator Precedence)
|
|
347
|
+
* Evaluates strings like "100 + 25 * 2", "(10 + 20) * 3", "100 - 10%", etc.
|
|
348
|
+
* Pure tokenizer and Shunting-Yard evaluator with zero eval() or Function() calls.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} expression
|
|
351
|
+
* @param {Object} [options={}]
|
|
352
|
+
* @returns {number}
|
|
353
|
+
*/
|
|
354
|
+
evaluate(expression, options = {}) {
|
|
355
|
+
if (!expression || typeof expression !== 'string') return 0;
|
|
356
|
+
const precision = options.precision ?? this.precision;
|
|
357
|
+
|
|
358
|
+
// Normalize symbols: '×' -> '*', '÷' -> '/', '−' -> '-'
|
|
359
|
+
let clean = expression
|
|
360
|
+
.replace(/×/g, '*')
|
|
361
|
+
.replace(/÷/g, '/')
|
|
362
|
+
.replace(/−/g, '-')
|
|
363
|
+
.replace(/,/g, '')
|
|
364
|
+
.trim();
|
|
365
|
+
|
|
366
|
+
if (!clean) return 0;
|
|
367
|
+
|
|
368
|
+
// Tokenize expression
|
|
369
|
+
const tokens = [];
|
|
370
|
+
let i = 0;
|
|
371
|
+
let lastToken = null;
|
|
372
|
+
|
|
373
|
+
while (i < clean.length) {
|
|
374
|
+
const char = clean[i];
|
|
375
|
+
|
|
376
|
+
if (/\s/.test(char)) {
|
|
377
|
+
i++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Numbers (including decimals)
|
|
382
|
+
if (/\d/.test(char) || (char === '.' && i + 1 < clean.length && /\d/.test(clean[i + 1]))) {
|
|
383
|
+
let numStr = '';
|
|
384
|
+
while (i < clean.length && (/[\d.]/.test(clean[i]))) {
|
|
385
|
+
numStr += clean[i];
|
|
386
|
+
i++;
|
|
387
|
+
}
|
|
388
|
+
// Check if followed by %
|
|
389
|
+
let isPct = false;
|
|
390
|
+
while (i < clean.length && /\s/.test(clean[i])) i++;
|
|
391
|
+
if (i < clean.length && clean[i] === '%') {
|
|
392
|
+
isPct = true;
|
|
393
|
+
i++;
|
|
394
|
+
}
|
|
395
|
+
const numVal = parseFloat(numStr);
|
|
396
|
+
tokens.push({ type: 'number', value: numVal, isPercent: isPct });
|
|
397
|
+
lastToken = 'number';
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Operators & Parentheses
|
|
402
|
+
if (char === '+' || char === '-' || char === '*' || char === '/' || char === '^' || char === '(' || char === ')') {
|
|
403
|
+
// Check for unary plus/minus
|
|
404
|
+
const isUnary = (char === '+' || char === '-') &&
|
|
405
|
+
(lastToken === null || lastToken === 'operator' || lastToken === 'lparen');
|
|
406
|
+
|
|
407
|
+
if (isUnary) {
|
|
408
|
+
tokens.push({ type: 'unary', value: char });
|
|
409
|
+
lastToken = 'unary';
|
|
410
|
+
} else if (char === '(') {
|
|
411
|
+
tokens.push({ type: 'lparen', value: '(' });
|
|
412
|
+
lastToken = 'lparen';
|
|
413
|
+
} else if (char === ')') {
|
|
414
|
+
tokens.push({ type: 'rparen', value: ')' });
|
|
415
|
+
lastToken = 'rparen';
|
|
416
|
+
} else {
|
|
417
|
+
tokens.push({ type: 'operator', value: char });
|
|
418
|
+
lastToken = 'operator';
|
|
419
|
+
}
|
|
420
|
+
i++;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Skip any unrecognized characters
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (tokens.length === 0) return 0;
|
|
429
|
+
|
|
430
|
+
// Operator Precedence Map
|
|
431
|
+
const precedence = {
|
|
432
|
+
'+': 1,
|
|
433
|
+
'-': 1,
|
|
434
|
+
'*': 2,
|
|
435
|
+
'/': 2,
|
|
436
|
+
'^': 3,
|
|
437
|
+
'u+': 4,
|
|
438
|
+
'u-': 4
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const outputQueue = [];
|
|
442
|
+
const opStack = [];
|
|
443
|
+
|
|
444
|
+
// Shunting-Yard Algorithm: infix to postfix (RPN)
|
|
445
|
+
for (let t = 0; t < tokens.length; t++) {
|
|
446
|
+
const token = tokens[t];
|
|
447
|
+
|
|
448
|
+
if (token.type === 'number') {
|
|
449
|
+
outputQueue.push(token);
|
|
450
|
+
} else if (token.type === 'unary') {
|
|
451
|
+
opStack.push({ type: 'unary', op: 'u' + token.value });
|
|
452
|
+
} else if (token.type === 'operator') {
|
|
453
|
+
const op = token.value;
|
|
454
|
+
while (
|
|
455
|
+
opStack.length > 0 &&
|
|
456
|
+
opStack[opStack.length - 1].type !== 'lparen' &&
|
|
457
|
+
((precedence[opStack[opStack.length - 1].op] > precedence[op]) ||
|
|
458
|
+
(precedence[opStack[opStack.length - 1].op] === precedence[op] && op !== '^'))
|
|
459
|
+
) {
|
|
460
|
+
outputQueue.push(opStack.pop());
|
|
461
|
+
}
|
|
462
|
+
opStack.push({ type: 'operator', op: op });
|
|
463
|
+
} else if (token.type === 'lparen') {
|
|
464
|
+
opStack.push(token);
|
|
465
|
+
} else if (token.type === 'rparen') {
|
|
466
|
+
while (opStack.length > 0 && opStack[opStack.length - 1].type !== 'lparen') {
|
|
467
|
+
outputQueue.push(opStack.pop());
|
|
468
|
+
}
|
|
469
|
+
if (opStack.length > 0 && opStack[opStack.length - 1].type === 'lparen') {
|
|
470
|
+
opStack.pop();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
while (opStack.length > 0) {
|
|
476
|
+
const op = opStack.pop();
|
|
477
|
+
if (op.type !== 'lparen') {
|
|
478
|
+
outputQueue.push(op);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Postfix Evaluator
|
|
483
|
+
const evalStack = [];
|
|
484
|
+
for (let k = 0; k < outputQueue.length; k++) {
|
|
485
|
+
const item = outputQueue[k];
|
|
486
|
+
|
|
487
|
+
if (item.type === 'number') {
|
|
488
|
+
evalStack.push(item);
|
|
489
|
+
} else if (item.type === 'unary') {
|
|
490
|
+
const operand = evalStack.pop();
|
|
491
|
+
const val = typeof operand === 'object' ? operand.value : operand;
|
|
492
|
+
const res = item.op === 'u-' ? -val : val;
|
|
493
|
+
evalStack.push({ type: 'number', value: res });
|
|
494
|
+
} else if (item.type === 'operator') {
|
|
495
|
+
const rightToken = evalStack.pop();
|
|
496
|
+
const leftToken = evalStack.pop();
|
|
497
|
+
|
|
498
|
+
const left = typeof leftToken === 'object' ? leftToken.value : Number(leftToken);
|
|
499
|
+
let right = typeof rightToken === 'object' ? rightToken.value : Number(rightToken);
|
|
500
|
+
|
|
501
|
+
// Smart percentage handling
|
|
502
|
+
if (rightToken && rightToken.isPercent) {
|
|
503
|
+
if (item.op === '+' || item.op === '-') {
|
|
504
|
+
right = (left * right) / 100;
|
|
505
|
+
} else if (item.op === '*' || item.op === '/') {
|
|
506
|
+
right = right / 100;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let res = 0;
|
|
511
|
+
switch (item.op) {
|
|
512
|
+
case '+': res = left + right; break;
|
|
513
|
+
case '-': res = left - right; break;
|
|
514
|
+
case '*': res = left * right; break;
|
|
515
|
+
case '/': res = right === 0 ? 0 : left / right; break;
|
|
516
|
+
case '^': res = Math.pow(left, right); break;
|
|
517
|
+
}
|
|
518
|
+
evalStack.push({ type: 'number', value: res });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (evalStack.length === 0) return 0;
|
|
523
|
+
const finalResult = evalStack[0].value ?? evalStack[0];
|
|
524
|
+
return Formatter.round(Number(finalResult), precision);
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Factory to instantiate a decoupled Stream Calculator state machine
|
|
529
|
+
* @param {Object} [config={}]
|
|
530
|
+
* @returns {StreamCalculator}
|
|
531
|
+
*/
|
|
532
|
+
createStream(config = {}) {
|
|
533
|
+
return new StreamCalculator(config);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
/* ==========================================================================
|
|
538
|
+
3. STREAM CALCULATOR STATE MACHINE (Desk / Chained Calculator)
|
|
539
|
+
========================================================================== */
|
|
540
|
+
class StreamCalculator {
|
|
541
|
+
constructor(config = {}) {
|
|
542
|
+
this.config = Object.assign({
|
|
543
|
+
precision: 8,
|
|
544
|
+
currency: '',
|
|
545
|
+
taxRate: 0.18,
|
|
546
|
+
tipRates: [10, 15, 20],
|
|
547
|
+
angleMode: 'deg',
|
|
548
|
+
onCalculate: null
|
|
549
|
+
}, config);
|
|
550
|
+
|
|
551
|
+
this.reset();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
reset() {
|
|
555
|
+
this.currentInput = '0';
|
|
556
|
+
this.previousValue = null;
|
|
557
|
+
this.pendingOperator = null;
|
|
558
|
+
this.historyTokens = [];
|
|
559
|
+
this.hasJustCalculated = false;
|
|
560
|
+
this.hasPercentApplied = false;
|
|
561
|
+
this.rawPercent = null;
|
|
562
|
+
this.memory = 0;
|
|
563
|
+
this.tapeHistory = [];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
inputDigit(digit) {
|
|
567
|
+
if (this.hasJustCalculated) {
|
|
568
|
+
this.currentInput = digit === '.' ? '0.' : digit;
|
|
569
|
+
this.previousValue = null;
|
|
570
|
+
this.pendingOperator = null;
|
|
571
|
+
this.historyTokens = [];
|
|
572
|
+
this.hasJustCalculated = false;
|
|
573
|
+
this.hasPercentApplied = false;
|
|
574
|
+
this.rawPercent = null;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.hasPercentApplied = false;
|
|
579
|
+
this.rawPercent = null;
|
|
580
|
+
|
|
581
|
+
if (digit === '.') {
|
|
582
|
+
if (!this.currentInput.includes('.')) {
|
|
583
|
+
this.currentInput = this.currentInput === '' || this.currentInput === '0' ? '0.' : this.currentInput + '.';
|
|
584
|
+
}
|
|
585
|
+
} else if (digit === '00') {
|
|
586
|
+
if (this.currentInput === '0' || this.currentInput === '') {
|
|
587
|
+
this.currentInput = '0';
|
|
588
|
+
} else {
|
|
589
|
+
this.currentInput += '00';
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
if (this.currentInput === '0' || this.currentInput === '') {
|
|
593
|
+
this.currentInput = String(digit);
|
|
594
|
+
} else {
|
|
595
|
+
this.currentInput += String(digit);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
inputOperator(op) {
|
|
601
|
+
const symbol = Formatter.getOperatorSymbol(op);
|
|
602
|
+
|
|
603
|
+
if (this.hasJustCalculated) {
|
|
604
|
+
const currentNum = this.parseCurrent();
|
|
605
|
+
this.previousValue = currentNum;
|
|
606
|
+
this.pendingOperator = op;
|
|
607
|
+
this.historyTokens = [Formatter.format(currentNum), symbol];
|
|
608
|
+
this.currentInput = '';
|
|
609
|
+
this.hasJustCalculated = false;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (this.previousValue === null) {
|
|
614
|
+
const currentNum = this.parseCurrent();
|
|
615
|
+
this.previousValue = currentNum;
|
|
616
|
+
this.pendingOperator = op;
|
|
617
|
+
this.historyTokens = [Formatter.format(currentNum), symbol];
|
|
618
|
+
this.currentInput = '';
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (this.currentInput !== '') {
|
|
623
|
+
const nextNum = parseFloat(this.currentInput);
|
|
624
|
+
let calculated = 0;
|
|
625
|
+
|
|
626
|
+
if (this.hasPercentApplied) {
|
|
627
|
+
const prevOp = this.pendingOperator;
|
|
628
|
+
if (prevOp === '+') calculated = this.previousValue + nextNum;
|
|
629
|
+
else if (prevOp === '-') calculated = this.previousValue - nextNum;
|
|
630
|
+
else if (prevOp === '*' || prevOp === 'x' || prevOp === '×') calculated = nextNum;
|
|
631
|
+
else if (prevOp === '/' || prevOp === '÷') {
|
|
632
|
+
const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
|
|
633
|
+
calculated = frac === 0 ? 0 : this.previousValue / frac;
|
|
634
|
+
} else {
|
|
635
|
+
calculated = this.calculate(this.previousValue, prevOp, nextNum);
|
|
636
|
+
}
|
|
637
|
+
calculated = Formatter.round(calculated, this.config.precision);
|
|
638
|
+
this.hasPercentApplied = false;
|
|
639
|
+
this.rawPercent = null;
|
|
640
|
+
} else {
|
|
641
|
+
calculated = this.calculate(this.previousValue, this.pendingOperator, nextNum);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
|
|
645
|
+
this.historyTokens.push(symbol);
|
|
646
|
+
} else {
|
|
647
|
+
this.historyTokens.push(Formatter.format(nextNum), symbol);
|
|
648
|
+
}
|
|
649
|
+
this.previousValue = calculated;
|
|
650
|
+
this.pendingOperator = op;
|
|
651
|
+
this.currentInput = '';
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Change pending operator
|
|
656
|
+
this.pendingOperator = op;
|
|
657
|
+
if (this.historyTokens.length > 0) {
|
|
658
|
+
this.historyTokens[this.historyTokens.length - 1] = symbol;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
inputEquals() {
|
|
663
|
+
if (this.pendingOperator === null || this.previousValue === null) return this.parseCurrent();
|
|
664
|
+
|
|
665
|
+
const nextNum = this.currentInput !== '' ? parseFloat(this.currentInput) : this.previousValue;
|
|
666
|
+
let result = 0;
|
|
667
|
+
|
|
668
|
+
if (this.hasPercentApplied) {
|
|
669
|
+
const op = this.pendingOperator;
|
|
670
|
+
if (op === '+') result = this.previousValue + nextNum;
|
|
671
|
+
else if (op === '-') result = this.previousValue - nextNum;
|
|
672
|
+
else if (op === '*' || op === 'x' || op === '×') result = nextNum;
|
|
673
|
+
else if (op === '/' || op === '÷') {
|
|
674
|
+
const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
|
|
675
|
+
result = frac === 0 ? 0 : this.previousValue / frac;
|
|
676
|
+
} else {
|
|
677
|
+
result = this.calculate(this.previousValue, op, nextNum);
|
|
678
|
+
}
|
|
679
|
+
result = Formatter.round(result, this.config.precision);
|
|
680
|
+
this.hasPercentApplied = false;
|
|
681
|
+
this.rawPercent = null;
|
|
682
|
+
} else {
|
|
683
|
+
result = this.calculate(this.previousValue, this.pendingOperator, nextNum);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
|
|
687
|
+
this.historyTokens.push('=');
|
|
688
|
+
} else {
|
|
689
|
+
this.historyTokens.push(Formatter.format(nextNum), '=');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const historyStr = this.historyTokens.join(' ');
|
|
693
|
+
this.tapeHistory.unshift(`${historyStr} ${Formatter.format(result)}`);
|
|
694
|
+
|
|
695
|
+
if (typeof this.config.onCalculate === 'function') {
|
|
696
|
+
this.config.onCalculate(result, historyStr);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
this.previousValue = result;
|
|
700
|
+
this.currentInput = String(result);
|
|
701
|
+
this.pendingOperator = null;
|
|
702
|
+
this.hasJustCalculated = true;
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
calculate(a, op, b) {
|
|
707
|
+
let res = 0;
|
|
708
|
+
switch (op) {
|
|
709
|
+
case '+': res = a + b; break;
|
|
710
|
+
case '-': res = a - b; break;
|
|
711
|
+
case '*':
|
|
712
|
+
case 'x':
|
|
713
|
+
case '×':
|
|
714
|
+
res = a * b; break;
|
|
715
|
+
case '/':
|
|
716
|
+
case '÷':
|
|
717
|
+
res = b === 0 ? 0 : a / b; break;
|
|
718
|
+
case '%':
|
|
719
|
+
case 'mod':
|
|
720
|
+
res = a % b; break;
|
|
721
|
+
case '^':
|
|
722
|
+
case 'pow':
|
|
723
|
+
res = Math.pow(a, b); break;
|
|
724
|
+
default: res = b;
|
|
725
|
+
}
|
|
726
|
+
return Formatter.round(res, this.config.precision);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
toggleSign() {
|
|
730
|
+
if (this.currentInput !== '' && this.currentInput !== '0') {
|
|
731
|
+
if (this.currentInput.startsWith('-')) {
|
|
732
|
+
this.currentInput = this.currentInput.slice(1);
|
|
733
|
+
} else {
|
|
734
|
+
this.currentInput = '-' + this.currentInput;
|
|
735
|
+
}
|
|
736
|
+
} else if (this.previousValue !== null) {
|
|
737
|
+
this.previousValue = -this.previousValue;
|
|
738
|
+
this.currentInput = String(this.previousValue);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
applyPercent() {
|
|
743
|
+
const current = this.parseCurrent();
|
|
744
|
+
if (this.previousValue !== null && this.pendingOperator !== null) {
|
|
745
|
+
const percentVal = (this.previousValue * current) / 100;
|
|
746
|
+
this.currentInput = String(Formatter.round(percentVal, this.config.precision));
|
|
747
|
+
this.hasPercentApplied = true;
|
|
748
|
+
this.rawPercent = current;
|
|
749
|
+
this.historyTokens = [
|
|
750
|
+
Formatter.format(this.previousValue),
|
|
751
|
+
Formatter.getOperatorSymbol(this.pendingOperator),
|
|
752
|
+
`${Formatter.format(current)}%`
|
|
753
|
+
];
|
|
754
|
+
} else {
|
|
755
|
+
const percentVal = current / 100;
|
|
756
|
+
this.currentInput = String(Formatter.round(percentVal, this.config.precision));
|
|
757
|
+
this.hasPercentApplied = false;
|
|
758
|
+
this.rawPercent = null;
|
|
759
|
+
this.historyTokens = [`${Formatter.format(current)}%`];
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
backspace() {
|
|
764
|
+
if (this.hasJustCalculated) {
|
|
765
|
+
this.clearAll();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (this.currentInput.length > 1) {
|
|
769
|
+
this.currentInput = this.currentInput.slice(0, -1);
|
|
770
|
+
} else {
|
|
771
|
+
this.currentInput = '0';
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
clearAll() {
|
|
776
|
+
this.currentInput = '0';
|
|
777
|
+
this.previousValue = null;
|
|
778
|
+
this.pendingOperator = null;
|
|
779
|
+
this.historyTokens = [];
|
|
780
|
+
this.hasJustCalculated = false;
|
|
781
|
+
this.hasPercentApplied = false;
|
|
782
|
+
this.rawPercent = null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
parseCurrent() {
|
|
786
|
+
return parseFloat(this.currentInput) || 0;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
getDisplayValue() {
|
|
790
|
+
let displayVal = this.currentInput;
|
|
791
|
+
if (displayVal === '' && this.previousValue !== null) {
|
|
792
|
+
displayVal = String(this.previousValue);
|
|
793
|
+
}
|
|
794
|
+
if (displayVal === '') displayVal = '0';
|
|
795
|
+
return displayVal;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
getFormattedDisplay() {
|
|
799
|
+
return Formatter.format(this.getDisplayValue(), { currency: this.config.currency });
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
getHistoryString() {
|
|
803
|
+
return this.historyTokens.join(' ');
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* ==========================================================================
|
|
808
|
+
4. ADMINUIUX CALCULATOR (DOM Binding Layer)
|
|
809
|
+
========================================================================== */
|
|
810
|
+
class AdminUIUXCalculator {
|
|
811
|
+
constructor(element, config = {}) {
|
|
812
|
+
if (typeof element === 'string') {
|
|
813
|
+
this.wrapper = document.querySelector(element);
|
|
814
|
+
} else {
|
|
815
|
+
this.wrapper = element;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (!this.wrapper) return;
|
|
819
|
+
if (this.wrapper._calculatorInstance) return;
|
|
820
|
+
this.wrapper._calculatorInstance = this;
|
|
821
|
+
|
|
822
|
+
// Extract configuration from data attributes or arguments
|
|
823
|
+
this.config = Object.assign({
|
|
824
|
+
type: this.wrapper.getAttribute('data-calculator-type') || this.wrapper.getAttribute('data-calendar-type') || this.wrapper.getAttribute('data-calc-mode') || 'basic',
|
|
825
|
+
precision: parseInt(this.wrapper.getAttribute('data-calc-precision'), 10) || (config.precision ?? 8),
|
|
826
|
+
angleMode: 'deg',
|
|
827
|
+
currency: this.wrapper.getAttribute('data-calc-currency') || config.currency || '',
|
|
828
|
+
taxRate: config.taxRate ?? 0.18,
|
|
829
|
+
tipRates: config.tipRates ?? [10, 15, 20],
|
|
830
|
+
quickPresets: config.quickPresets ?? [50, 100, 500, 1000],
|
|
831
|
+
enableKeyboard: config.enableKeyboard ?? true,
|
|
832
|
+
showTape: config.showTape ?? false,
|
|
833
|
+
onCalculate: config.onCalculate || null
|
|
834
|
+
}, config);
|
|
835
|
+
|
|
836
|
+
// Instantiate Decoupled Stream Calculator Engine
|
|
837
|
+
this.engine = new StreamCalculator(this.config);
|
|
838
|
+
|
|
839
|
+
this.hasInteracted = false;
|
|
840
|
+
this.memory = 0;
|
|
841
|
+
|
|
842
|
+
// Cache DOM Elements
|
|
843
|
+
this.displayEl = this.wrapper.querySelector('.calculator-display, [data-calc-display]');
|
|
844
|
+
this.historyEl = this.wrapper.querySelector('.calculator-history, [data-calc-history]');
|
|
845
|
+
this.memoryBadge = this.wrapper.querySelector('[data-calc-memory-badge]');
|
|
846
|
+
this.angleModeBtn = this.wrapper.querySelector('[data-calc-angle-toggle]');
|
|
847
|
+
this.tapeListEl = this.wrapper.querySelector('.calculator-tape, [data-calc-tape]');
|
|
848
|
+
|
|
849
|
+
this.initEventListeners();
|
|
850
|
+
this.updateDisplay();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
initEventListeners() {
|
|
854
|
+
this.wrapper.querySelectorAll('[data-calc-action], [data-calculator-perfom], [data-calculator-perform]').forEach(btn => {
|
|
855
|
+
btn.addEventListener('click', (e) => {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
const performExpr = btn.getAttribute('data-calculator-perfom') || btn.getAttribute('data-calculator-perform');
|
|
858
|
+
if (performExpr) {
|
|
859
|
+
this.executePerform(performExpr);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const action = btn.getAttribute('data-calc-action');
|
|
863
|
+
const val = btn.getAttribute('data-val');
|
|
864
|
+
this.handleAction(action, val);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
if (this.config.enableKeyboard) {
|
|
869
|
+
this.wrapper.addEventListener('keydown', (e) => {
|
|
870
|
+
this.handleKeyDown(e);
|
|
871
|
+
});
|
|
872
|
+
if (!this.wrapper.hasAttribute('tabindex')) {
|
|
873
|
+
this.wrapper.setAttribute('tabindex', '0');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
handleKeyDown(e) {
|
|
879
|
+
if (['input', 'textarea'].includes(document.activeElement?.tagName?.toLowerCase())) return;
|
|
880
|
+
|
|
881
|
+
const key = e.key;
|
|
882
|
+
if (!isNaN(key)) {
|
|
883
|
+
e.preventDefault();
|
|
884
|
+
this.handleAction('num', key);
|
|
885
|
+
} else if (key === '.') {
|
|
886
|
+
e.preventDefault();
|
|
887
|
+
this.handleAction('num', '.');
|
|
888
|
+
} else if (key === '+' || key === '-' || key === '*' || key === '/') {
|
|
889
|
+
e.preventDefault();
|
|
890
|
+
this.handleAction('operator', key);
|
|
891
|
+
} else if (key === 'Enter' || key === '=') {
|
|
892
|
+
e.preventDefault();
|
|
893
|
+
this.handleAction('equals');
|
|
894
|
+
} else if (key === 'Backspace') {
|
|
895
|
+
e.preventDefault();
|
|
896
|
+
this.handleAction('backspace');
|
|
897
|
+
} else if (key === 'Escape') {
|
|
898
|
+
e.preventDefault();
|
|
899
|
+
this.handleAction('clear');
|
|
900
|
+
} else if (key === '%') {
|
|
901
|
+
e.preventDefault();
|
|
902
|
+
this.handleAction('percent');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
handleAction(action, val) {
|
|
907
|
+
this.hasInteracted = true;
|
|
908
|
+
switch (action) {
|
|
909
|
+
case 'num':
|
|
910
|
+
this.engine.inputDigit(val);
|
|
911
|
+
break;
|
|
912
|
+
case 'operator':
|
|
913
|
+
this.engine.inputOperator(val);
|
|
914
|
+
break;
|
|
915
|
+
case 'equals':
|
|
916
|
+
this.engine.inputEquals();
|
|
917
|
+
this.syncTape();
|
|
918
|
+
break;
|
|
919
|
+
case 'clear':
|
|
920
|
+
this.engine.clearAll();
|
|
921
|
+
break;
|
|
922
|
+
case 'clear-entry':
|
|
923
|
+
case 'backspace':
|
|
924
|
+
this.engine.backspace();
|
|
925
|
+
break;
|
|
926
|
+
case 'negate':
|
|
927
|
+
this.engine.toggleSign();
|
|
928
|
+
break;
|
|
929
|
+
case 'percent':
|
|
930
|
+
this.engine.applyPercent();
|
|
931
|
+
break;
|
|
932
|
+
case 'scientific':
|
|
933
|
+
this.applyScientific(val);
|
|
934
|
+
break;
|
|
935
|
+
case 'memory':
|
|
936
|
+
this.handleMemory(val);
|
|
937
|
+
break;
|
|
938
|
+
case 'angle-toggle':
|
|
939
|
+
this.toggleAngleMode();
|
|
940
|
+
break;
|
|
941
|
+
case 'preset':
|
|
942
|
+
this.applyPreset(parseFloat(val));
|
|
943
|
+
break;
|
|
944
|
+
case 'tax-add':
|
|
945
|
+
this.applyTax(true);
|
|
946
|
+
break;
|
|
947
|
+
case 'tax-sub':
|
|
948
|
+
this.applyTax(false);
|
|
949
|
+
break;
|
|
950
|
+
case 'tip':
|
|
951
|
+
this.applyTip(parseFloat(val));
|
|
952
|
+
break;
|
|
953
|
+
case 'discount':
|
|
954
|
+
this.applyDiscount(parseFloat(val));
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
this.updateDisplay();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
applyScientific(func) {
|
|
961
|
+
const val = this.engine.parseCurrent();
|
|
962
|
+
let res = val;
|
|
963
|
+
switch (func) {
|
|
964
|
+
case 'sin': res = Calculator.sin(val, this.config.angleMode, this.config.precision); break;
|
|
965
|
+
case 'cos': res = Calculator.cos(val, this.config.angleMode, this.config.precision); break;
|
|
966
|
+
case 'tan': res = Calculator.tan(val, this.config.angleMode, this.config.precision); break;
|
|
967
|
+
case 'asin': res = Calculator.asin(val, this.config.angleMode, this.config.precision); break;
|
|
968
|
+
case 'acos': res = Calculator.acos(val, this.config.angleMode, this.config.precision); break;
|
|
969
|
+
case 'atan': res = Calculator.atan(val, this.config.angleMode, this.config.precision); break;
|
|
970
|
+
case 'sqrt': res = Calculator.sqrt(val, this.config.precision); break;
|
|
971
|
+
case 'sqr': res = Calculator.sqr(val, this.config.precision); break;
|
|
972
|
+
case 'cube': res = Calculator.cube(val, this.config.precision); break;
|
|
973
|
+
case 'log': res = Calculator.log(val, this.config.precision); break;
|
|
974
|
+
case 'ln': res = Calculator.ln(val, this.config.precision); break;
|
|
975
|
+
case 'exp': res = Calculator.exp(val, this.config.precision); break;
|
|
976
|
+
case '1/x': res = Calculator.reciprocal(val, this.config.precision); break;
|
|
977
|
+
case 'abs': res = Calculator.abs(val); break;
|
|
978
|
+
case 'pi': res = Formatter.round(Math.PI, this.config.precision); break;
|
|
979
|
+
case 'e': res = Formatter.round(Math.E, this.config.precision); break;
|
|
980
|
+
case 'fact': res = Calculator.factorial(val); break;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
this.engine.currentInput = String(res);
|
|
984
|
+
if (this.engine.hasJustCalculated) {
|
|
985
|
+
this.engine.historyTokens = [`${func}(${Formatter.format(val)}) =`];
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
handleMemory(action) {
|
|
990
|
+
const val = this.engine.parseCurrent();
|
|
991
|
+
switch (action) {
|
|
992
|
+
case 'mc': this.memory = 0; break;
|
|
993
|
+
case 'mr':
|
|
994
|
+
this.engine.currentInput = String(this.memory);
|
|
995
|
+
this.engine.hasJustCalculated = false;
|
|
996
|
+
break;
|
|
997
|
+
case 'm+': this.memory += val; break;
|
|
998
|
+
case 'm-': this.memory -= val; break;
|
|
999
|
+
case 'ms': this.memory = val; break;
|
|
1000
|
+
}
|
|
1001
|
+
if (this.memoryBadge) {
|
|
1002
|
+
this.memoryBadge.style.display = this.memory !== 0 ? 'inline-block' : 'none';
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
toggleAngleMode() {
|
|
1007
|
+
this.config.angleMode = this.config.angleMode === 'deg' ? 'rad' : 'deg';
|
|
1008
|
+
if (this.angleModeBtn) {
|
|
1009
|
+
this.angleModeBtn.textContent = this.config.angleMode.toUpperCase();
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
applyPreset(amount) {
|
|
1014
|
+
const current = this.engine.parseCurrent();
|
|
1015
|
+
this.engine.currentInput = String(current + amount);
|
|
1016
|
+
this.engine.hasJustCalculated = false;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
applyTax(isAdd) {
|
|
1020
|
+
const current = this.engine.parseCurrent();
|
|
1021
|
+
const rate = this.config.taxRate;
|
|
1022
|
+
const result = Calculator.tax(current, rate, isAdd, this.config.precision);
|
|
1023
|
+
this.engine.currentInput = String(result);
|
|
1024
|
+
this.engine.historyTokens = [`${Formatter.format(current)} ${isAdd ? '+' : '-'} ${Math.round(rate * 100)}% Tax =`];
|
|
1025
|
+
this.engine.hasJustCalculated = true;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
applyTip(percent) {
|
|
1029
|
+
const current = this.engine.parseCurrent();
|
|
1030
|
+
const result = Calculator.tip(current, percent, this.config.precision);
|
|
1031
|
+
this.engine.currentInput = String(result);
|
|
1032
|
+
this.engine.historyTokens = [`${Formatter.format(current)} + ${percent}% Tip =`];
|
|
1033
|
+
this.engine.hasJustCalculated = true;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
applyDiscount(percent) {
|
|
1037
|
+
const current = this.engine.parseCurrent();
|
|
1038
|
+
const result = Calculator.discount(current, percent, this.config.precision);
|
|
1039
|
+
this.engine.currentInput = String(result);
|
|
1040
|
+
this.engine.historyTokens = [`${Formatter.format(current)} - ${percent}% Off =`];
|
|
1041
|
+
this.engine.hasJustCalculated = true;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
executePerform(expr) {
|
|
1045
|
+
if (!expr) return;
|
|
1046
|
+
this.hasInteracted = true;
|
|
1047
|
+
const current = this.engine.parseCurrent();
|
|
1048
|
+
const res = Calculator.executePerform(current, expr);
|
|
1049
|
+
this.engine.currentInput = String(res.value);
|
|
1050
|
+
if (res.history) {
|
|
1051
|
+
this.engine.historyTokens = [res.history];
|
|
1052
|
+
}
|
|
1053
|
+
this.engine.hasJustCalculated = true;
|
|
1054
|
+
this.updateDisplay();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
getOutputTargets() {
|
|
1058
|
+
const selector = this.wrapper.getAttribute('data-calculator-output') ||
|
|
1059
|
+
this.wrapper.getAttribute('data-output-target') ||
|
|
1060
|
+
this.config.outputTarget ||
|
|
1061
|
+
this.config.output;
|
|
1062
|
+
if (!selector) return [];
|
|
1063
|
+
try {
|
|
1064
|
+
return Array.from(document.querySelectorAll(selector));
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
getCalculationTargets() {
|
|
1071
|
+
const selector = this.wrapper.getAttribute('data-calculator-calculation') ||
|
|
1072
|
+
this.wrapper.getAttribute('data-calculation-target') ||
|
|
1073
|
+
this.config.calculationTarget ||
|
|
1074
|
+
this.config.calculation;
|
|
1075
|
+
if (!selector) return [];
|
|
1076
|
+
try {
|
|
1077
|
+
return Array.from(document.querySelectorAll(selector));
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
return [];
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
updateOutputTargets(displayStr, rawVal) {
|
|
1084
|
+
const targets = this.getOutputTargets();
|
|
1085
|
+
if (targets.length === 0) return;
|
|
1086
|
+
|
|
1087
|
+
targets.forEach(target => {
|
|
1088
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
1089
|
+
target.value = displayStr;
|
|
1090
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1091
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1092
|
+
} else {
|
|
1093
|
+
target.textContent = displayStr;
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
this.wrapper.dispatchEvent(new CustomEvent('calculator:change', {
|
|
1098
|
+
bubbles: true,
|
|
1099
|
+
detail: {
|
|
1100
|
+
value: rawVal,
|
|
1101
|
+
formatted: displayStr,
|
|
1102
|
+
history: this.engine.getHistoryString(),
|
|
1103
|
+
calculator: this
|
|
1104
|
+
}
|
|
1105
|
+
}));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
updateCalculationTargets(calculationStr) {
|
|
1109
|
+
const targets = this.getCalculationTargets();
|
|
1110
|
+
if (targets.length === 0) return;
|
|
1111
|
+
|
|
1112
|
+
targets.forEach(target => {
|
|
1113
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
1114
|
+
target.value = calculationStr;
|
|
1115
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1116
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1117
|
+
} else {
|
|
1118
|
+
target.textContent = calculationStr;
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
updateDisplay() {
|
|
1124
|
+
const currency = this.config.currency ? `${this.config.currency} ` : '';
|
|
1125
|
+
const rawVal = this.engine.parseCurrent();
|
|
1126
|
+
let finalDisplay = this.engine.getDisplayValue();
|
|
1127
|
+
|
|
1128
|
+
// Format integer with commas
|
|
1129
|
+
const parts = finalDisplay.split('.');
|
|
1130
|
+
const formattedInt = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
1131
|
+
finalDisplay = parts.length > 1 ? `${formattedInt}.${parts[1]}` : formattedInt;
|
|
1132
|
+
|
|
1133
|
+
if (this.displayEl) {
|
|
1134
|
+
this.displayEl.textContent = `${currency}${finalDisplay}`;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const historyStr = this.engine.getHistoryString();
|
|
1138
|
+
if (this.historyEl) {
|
|
1139
|
+
if (historyStr) {
|
|
1140
|
+
this.historyEl.textContent = historyStr;
|
|
1141
|
+
} else {
|
|
1142
|
+
this.historyEl.innerHTML = ' ';
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
this.scrollToEnd(this.historyEl);
|
|
1147
|
+
this.scrollToEnd(this.displayEl);
|
|
1148
|
+
|
|
1149
|
+
if (this.hasInteracted) {
|
|
1150
|
+
this.updateOutputTargets(`${currency}${finalDisplay}`, rawVal);
|
|
1151
|
+
this.updateCalculationTargets(historyStr || 'Result:');
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
isRTL() {
|
|
1156
|
+
if (typeof document === 'undefined') return false;
|
|
1157
|
+
return (
|
|
1158
|
+
document.documentElement.getAttribute('dir') === 'rtl' ||
|
|
1159
|
+
document.documentElement.getAttribute('data-bs-direction') === 'rtl' ||
|
|
1160
|
+
document.body?.getAttribute('dir') === 'rtl' ||
|
|
1161
|
+
this.wrapper?.getAttribute('dir') === 'rtl' ||
|
|
1162
|
+
getComputedStyle(this.wrapper || document.documentElement).direction === 'rtl'
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
scrollToEnd(element) {
|
|
1167
|
+
if (!element) return;
|
|
1168
|
+
const rtl = this.isRTL();
|
|
1169
|
+
element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
|
|
1170
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
1171
|
+
requestAnimationFrame(() => {
|
|
1172
|
+
if (element) {
|
|
1173
|
+
element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
syncTape() {
|
|
1180
|
+
if (!this.tapeListEl) return;
|
|
1181
|
+
this.tapeListEl.innerHTML = '';
|
|
1182
|
+
this.engine.tapeHistory.forEach((entry, idx) => {
|
|
1183
|
+
const item = document.createElement('div');
|
|
1184
|
+
item.className = 'd-flex justify-content-between py-1 border-bottom border-light-subtle';
|
|
1185
|
+
item.innerHTML = `<span class="text-secondary">${this.engine.tapeHistory.length - idx}.</span> <span>${entry}</span>`;
|
|
1186
|
+
this.tapeListEl.appendChild(item);
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Static Factory Methods
|
|
1191
|
+
static create(target, options = {}) {
|
|
1192
|
+
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
1193
|
+
if (!el) return null;
|
|
1194
|
+
return new AdminUIUXCalculator(el, options);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
static init(target, options = {}) {
|
|
1198
|
+
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
1199
|
+
if (!el) return null;
|
|
1200
|
+
return new AdminUIUXCalculator(el, options);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
static initAll() {
|
|
1204
|
+
if (typeof document === 'undefined') return;
|
|
1205
|
+
document.querySelectorAll('.calculator, [data-calculator-type], [data-calendar-type]').forEach(el => {
|
|
1206
|
+
if (!el._calculatorInstance) {
|
|
1207
|
+
new AdminUIUXCalculator(el);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Attach static namespaces to AdminUIUXCalculator for convenience
|
|
1214
|
+
AdminUIUXCalculator.Calculator = Calculator;
|
|
1215
|
+
AdminUIUXCalculator.Formatter = Formatter;
|
|
1216
|
+
AdminUIUXCalculator.StreamCalculator = StreamCalculator;
|
|
1217
|
+
|
|
1218
|
+
// Automatic initialization in browser DOM
|
|
1219
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
1220
|
+
if (document.readyState === 'loading') {
|
|
1221
|
+
document.addEventListener('DOMContentLoaded', () => AdminUIUXCalculator.initAll());
|
|
1222
|
+
} else {
|
|
1223
|
+
AdminUIUXCalculator.initAll();
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return {
|
|
1228
|
+
AdminUIUXCalculator: AdminUIUXCalculator,
|
|
1229
|
+
Calculator: Calculator,
|
|
1230
|
+
Formatter: Formatter,
|
|
1231
|
+
StreamCalculator: StreamCalculator,
|
|
1232
|
+
default: AdminUIUXCalculator
|
|
1233
|
+
};
|
|
1234
|
+
});
|