mani-calc 1.2.2 → 2.1.1
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/FRIEND_SETUP.md +132 -0
- package/README.md +373 -493
- package/SETUP.bat +126 -0
- package/bin/overlay.js +87 -19
- package/package.json +14 -3
- package/scripts/install-autostart.js +84 -0
- package/scripts/uninstall-autostart.js +49 -0
- package/src/core/currency-converter.js +191 -0
- package/src/core/date-time-calculator.js +376 -0
- package/src/core/programmer-calc.js +265 -0
- package/src/core/settings-manager.js +179 -0
- package/src/core/utilities.js +402 -0
- package/src/index.js +174 -17
- package/src/ui/floating-search.js +203 -10
- package/src/ui/overlay.html +178 -11
- package/ARCHITECTURE.md +0 -249
- package/CHANGELOG.md +0 -69
- package/COMPLETION_SUMMARY.md +0 -304
- package/CONTRIBUTING.md +0 -110
- package/EXAMPLES.md +0 -220
- package/OVERLAY_MODE.md +0 -364
- package/PUBLISHING_GUIDE.md +0 -291
- package/QUICK_PUBLISH.md +0 -108
- package/RELEASE_NOTES_v1.1.0.md +0 -262
- package/test/test.js +0 -133
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date/Time Calculator Module
|
|
3
|
+
* Handles date arithmetic, countdowns, and world clock
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class DateTimeCalculator {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Timezone offsets (from UTC)
|
|
9
|
+
this.timezones = {
|
|
10
|
+
// Americas
|
|
11
|
+
'new york': -5, 'nyc': -5, 'est': -5, 'eastern': -5,
|
|
12
|
+
'los angeles': -8, 'la': -8, 'pst': -8, 'pacific': -8,
|
|
13
|
+
'chicago': -6, 'cst': -6, 'central': -6,
|
|
14
|
+
'denver': -7, 'mst': -7, 'mountain': -7,
|
|
15
|
+
'toronto': -5, 'vancouver': -8, 'mexico city': -6,
|
|
16
|
+
'sao paulo': -3, 'brazil': -3, 'buenos aires': -3,
|
|
17
|
+
|
|
18
|
+
// Europe
|
|
19
|
+
'london': 0, 'uk': 0, 'gmt': 0, 'utc': 0,
|
|
20
|
+
'paris': 1, 'berlin': 1, 'rome': 1, 'madrid': 1, 'cet': 1,
|
|
21
|
+
'amsterdam': 1, 'brussels': 1, 'vienna': 1, 'zurich': 1,
|
|
22
|
+
'moscow': 3, 'istanbul': 3, 'athens': 2, 'helsinki': 2,
|
|
23
|
+
|
|
24
|
+
// Asia
|
|
25
|
+
'india': 5.5, 'delhi': 5.5, 'mumbai': 5.5, 'ist': 5.5, 'kolkata': 5.5,
|
|
26
|
+
'dubai': 4, 'uae': 4, 'abu dhabi': 4,
|
|
27
|
+
'singapore': 8, 'hong kong': 8, 'hk': 8,
|
|
28
|
+
'tokyo': 9, 'japan': 9, 'jst': 9,
|
|
29
|
+
'seoul': 9, 'korea': 9, 'kst': 9,
|
|
30
|
+
'beijing': 8, 'shanghai': 8, 'china': 8,
|
|
31
|
+
'bangkok': 7, 'jakarta': 7, 'vietnam': 7, 'hanoi': 7,
|
|
32
|
+
'taipei': 8, 'taiwan': 8,
|
|
33
|
+
'manila': 8, 'philippines': 8,
|
|
34
|
+
'kuala lumpur': 8, 'malaysia': 8,
|
|
35
|
+
|
|
36
|
+
// Oceania
|
|
37
|
+
'sydney': 11, 'australia': 11, 'melbourne': 11, 'aest': 11,
|
|
38
|
+
'perth': 8, 'brisbane': 10,
|
|
39
|
+
'auckland': 13, 'new zealand': 13, 'nzst': 13,
|
|
40
|
+
|
|
41
|
+
// Africa/Middle East
|
|
42
|
+
'cairo': 2, 'johannesburg': 2, 'lagos': 1,
|
|
43
|
+
'riyadh': 3, 'saudi': 3, 'tel aviv': 2, 'israel': 2
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Common date patterns
|
|
47
|
+
this.months = {
|
|
48
|
+
'jan': 0, 'january': 0,
|
|
49
|
+
'feb': 1, 'february': 1,
|
|
50
|
+
'mar': 2, 'march': 2,
|
|
51
|
+
'apr': 3, 'april': 3,
|
|
52
|
+
'may': 4,
|
|
53
|
+
'jun': 5, 'june': 5,
|
|
54
|
+
'jul': 6, 'july': 6,
|
|
55
|
+
'aug': 7, 'august': 7,
|
|
56
|
+
'sep': 8, 'sept': 8, 'september': 8,
|
|
57
|
+
'oct': 9, 'october': 9,
|
|
58
|
+
'nov': 10, 'november': 10,
|
|
59
|
+
'dec': 11, 'december': 11
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse date query and return result
|
|
65
|
+
*/
|
|
66
|
+
parse(query) {
|
|
67
|
+
const q = query.toLowerCase().trim();
|
|
68
|
+
|
|
69
|
+
// Time in location: "time in tokyo", "what time in london"
|
|
70
|
+
const timeMatch = q.match(/(?:what(?:'s| is)?\s+)?time\s+(?:in|at)\s+(.+)/i);
|
|
71
|
+
if (timeMatch) {
|
|
72
|
+
return this.getTimeInLocation(timeMatch[1].trim());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Today +/- days: "today + 30 days", "today - 2 weeks"
|
|
76
|
+
const todayMatch = q.match(/today\s*([+-])\s*(\d+)\s*(day|days|week|weeks|month|months|year|years)/i);
|
|
77
|
+
if (todayMatch) {
|
|
78
|
+
return this.addToToday(todayMatch[1], parseInt(todayMatch[2]), todayMatch[3]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Days until/since: "days until dec 25", "days since jan 1"
|
|
82
|
+
const daysMatch = q.match(/(days?|weeks?|months?)\s+(until|since|from|to)\s+(.+)/i);
|
|
83
|
+
if (daysMatch) {
|
|
84
|
+
return this.calculateDuration(daysMatch[1], daysMatch[2], daysMatch[3]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// What day: "what day is dec 25", "what day was jan 1 2020"
|
|
88
|
+
const whatDayMatch = q.match(/what\s+day\s+(?:is|was|will be)\s+(.+)/i);
|
|
89
|
+
if (whatDayMatch) {
|
|
90
|
+
return this.getWeekday(whatDayMatch[1]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Current date/time
|
|
94
|
+
if (q === 'today' || q === 'date' || q === 'now') {
|
|
95
|
+
return this.getCurrentDateTime();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (q === 'time' || q === 'current time') {
|
|
99
|
+
return this.getCurrentTime();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Week number
|
|
103
|
+
if (q === 'week' || q === 'week number' || q === 'what week') {
|
|
104
|
+
return this.getWeekNumber();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get time in a specific location
|
|
112
|
+
*/
|
|
113
|
+
getTimeInLocation(location) {
|
|
114
|
+
const loc = location.toLowerCase().trim();
|
|
115
|
+
const offset = this.timezones[loc];
|
|
116
|
+
|
|
117
|
+
if (offset === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
error: `Unknown location: ${location}`,
|
|
120
|
+
type: 'error'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
|
|
126
|
+
const targetTime = new Date(utc + (offset * 3600000));
|
|
127
|
+
|
|
128
|
+
const hours = targetTime.getHours();
|
|
129
|
+
const minutes = targetTime.getMinutes().toString().padStart(2, '0');
|
|
130
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
131
|
+
const hour12 = hours % 12 || 12;
|
|
132
|
+
|
|
133
|
+
const dayName = targetTime.toLocaleDateString('en-US', { weekday: 'short' });
|
|
134
|
+
const monthDay = targetTime.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
result: `${hour12}:${minutes} ${ampm}`,
|
|
138
|
+
formatted: `${location.charAt(0).toUpperCase() + location.slice(1)}: ${hour12}:${minutes} ${ampm} (${dayName}, ${monthDay})`,
|
|
139
|
+
type: 'time',
|
|
140
|
+
location: location,
|
|
141
|
+
offset: offset
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Add/subtract from today
|
|
147
|
+
*/
|
|
148
|
+
addToToday(operator, amount, unit) {
|
|
149
|
+
const today = new Date();
|
|
150
|
+
let multiplier = operator === '+' ? 1 : -1;
|
|
151
|
+
|
|
152
|
+
switch (unit.toLowerCase().replace(/s$/, '')) {
|
|
153
|
+
case 'day':
|
|
154
|
+
today.setDate(today.getDate() + (amount * multiplier));
|
|
155
|
+
break;
|
|
156
|
+
case 'week':
|
|
157
|
+
today.setDate(today.getDate() + (amount * 7 * multiplier));
|
|
158
|
+
break;
|
|
159
|
+
case 'month':
|
|
160
|
+
today.setMonth(today.getMonth() + (amount * multiplier));
|
|
161
|
+
break;
|
|
162
|
+
case 'year':
|
|
163
|
+
today.setFullYear(today.getFullYear() + (amount * multiplier));
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const dayName = today.toLocaleDateString('en-US', { weekday: 'long' });
|
|
168
|
+
const formatted = today.toLocaleDateString('en-US', {
|
|
169
|
+
weekday: 'long',
|
|
170
|
+
year: 'numeric',
|
|
171
|
+
month: 'long',
|
|
172
|
+
day: 'numeric'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
result: today.toISOString().split('T')[0],
|
|
177
|
+
formatted: formatted,
|
|
178
|
+
type: 'date',
|
|
179
|
+
dayName: dayName
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Calculate days until/since a date
|
|
185
|
+
*/
|
|
186
|
+
calculateDuration(unit, direction, dateStr) {
|
|
187
|
+
const targetDate = this.parseDate(dateStr);
|
|
188
|
+
|
|
189
|
+
if (!targetDate) {
|
|
190
|
+
return {
|
|
191
|
+
error: `Could not parse date: ${dateStr}`,
|
|
192
|
+
type: 'error'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const today = new Date();
|
|
197
|
+
today.setHours(0, 0, 0, 0);
|
|
198
|
+
targetDate.setHours(0, 0, 0, 0);
|
|
199
|
+
|
|
200
|
+
const diffTime = targetDate - today;
|
|
201
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
202
|
+
|
|
203
|
+
let result, formatted;
|
|
204
|
+
const unitLower = unit.toLowerCase().replace(/s$/, '');
|
|
205
|
+
|
|
206
|
+
if (unitLower === 'week') {
|
|
207
|
+
const weeks = Math.abs(Math.floor(diffDays / 7));
|
|
208
|
+
const remainingDays = Math.abs(diffDays % 7);
|
|
209
|
+
result = weeks;
|
|
210
|
+
formatted = `${weeks} week${weeks !== 1 ? 's' : ''}${remainingDays > 0 ? ` and ${remainingDays} day${remainingDays !== 1 ? 's' : ''}` : ''}`;
|
|
211
|
+
} else if (unitLower === 'month') {
|
|
212
|
+
const months = Math.abs(Math.floor(diffDays / 30));
|
|
213
|
+
result = months;
|
|
214
|
+
formatted = `${months} month${months !== 1 ? 's' : ''} (approximately)`;
|
|
215
|
+
} else {
|
|
216
|
+
result = Math.abs(diffDays);
|
|
217
|
+
formatted = `${Math.abs(diffDays)} day${Math.abs(diffDays) !== 1 ? 's' : ''}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const prefix = diffDays > 0 ? 'in' : 'ago';
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
result: result,
|
|
224
|
+
formatted: diffDays === 0 ? 'Today!' : `${formatted} ${diffDays > 0 ? 'from now' : 'ago'}`,
|
|
225
|
+
type: 'duration',
|
|
226
|
+
days: diffDays
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get weekday for a date
|
|
232
|
+
*/
|
|
233
|
+
getWeekday(dateStr) {
|
|
234
|
+
const date = this.parseDate(dateStr);
|
|
235
|
+
|
|
236
|
+
if (!date) {
|
|
237
|
+
return {
|
|
238
|
+
error: `Could not parse date: ${dateStr}`,
|
|
239
|
+
type: 'error'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const dayName = date.toLocaleDateString('en-US', { weekday: 'long' });
|
|
244
|
+
const formatted = date.toLocaleDateString('en-US', {
|
|
245
|
+
weekday: 'long',
|
|
246
|
+
year: 'numeric',
|
|
247
|
+
month: 'long',
|
|
248
|
+
day: 'numeric'
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
result: dayName,
|
|
253
|
+
formatted: formatted,
|
|
254
|
+
type: 'date'
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parse various date formats
|
|
260
|
+
*/
|
|
261
|
+
parseDate(dateStr) {
|
|
262
|
+
const str = dateStr.toLowerCase().trim();
|
|
263
|
+
|
|
264
|
+
// Named dates
|
|
265
|
+
if (str === 'christmas' || str === 'xmas') {
|
|
266
|
+
const year = new Date().getMonth() >= 11 && new Date().getDate() > 25
|
|
267
|
+
? new Date().getFullYear() + 1
|
|
268
|
+
: new Date().getFullYear();
|
|
269
|
+
return new Date(year, 11, 25);
|
|
270
|
+
}
|
|
271
|
+
if (str === 'new year' || str === 'new years' || str === "new year's") {
|
|
272
|
+
return new Date(new Date().getFullYear() + 1, 0, 1);
|
|
273
|
+
}
|
|
274
|
+
if (str === 'halloween') {
|
|
275
|
+
const year = new Date().getMonth() > 9 ? new Date().getFullYear() + 1 : new Date().getFullYear();
|
|
276
|
+
return new Date(year, 9, 31);
|
|
277
|
+
}
|
|
278
|
+
if (str === 'valentine' || str === "valentine's" || str === 'valentines') {
|
|
279
|
+
const year = new Date().getMonth() > 1 ? new Date().getFullYear() + 1 : new Date().getFullYear();
|
|
280
|
+
return new Date(year, 1, 14);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Month day format: "dec 25", "december 25", "25 dec"
|
|
284
|
+
const monthDayMatch = str.match(/(\d{1,2})\s+([a-z]+)|([a-z]+)\s+(\d{1,2})(?:\s+(\d{4}))?/);
|
|
285
|
+
if (monthDayMatch) {
|
|
286
|
+
let day, monthStr, year;
|
|
287
|
+
if (monthDayMatch[1]) {
|
|
288
|
+
day = parseInt(monthDayMatch[1]);
|
|
289
|
+
monthStr = monthDayMatch[2];
|
|
290
|
+
} else {
|
|
291
|
+
monthStr = monthDayMatch[3];
|
|
292
|
+
day = parseInt(monthDayMatch[4]);
|
|
293
|
+
year = monthDayMatch[5] ? parseInt(monthDayMatch[5]) : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const month = this.months[monthStr];
|
|
297
|
+
if (month !== undefined && day >= 1 && day <= 31) {
|
|
298
|
+
const targetYear = year || new Date().getFullYear();
|
|
299
|
+
return new Date(targetYear, month, day);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Standard date formats
|
|
304
|
+
const date = new Date(dateStr);
|
|
305
|
+
if (!isNaN(date.getTime())) {
|
|
306
|
+
return date;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get current date and time
|
|
314
|
+
*/
|
|
315
|
+
getCurrentDateTime() {
|
|
316
|
+
const now = new Date();
|
|
317
|
+
return {
|
|
318
|
+
result: now.toLocaleDateString('en-US', {
|
|
319
|
+
weekday: 'long',
|
|
320
|
+
year: 'numeric',
|
|
321
|
+
month: 'long',
|
|
322
|
+
day: 'numeric'
|
|
323
|
+
}),
|
|
324
|
+
formatted: now.toLocaleString('en-US', {
|
|
325
|
+
weekday: 'long',
|
|
326
|
+
year: 'numeric',
|
|
327
|
+
month: 'long',
|
|
328
|
+
day: 'numeric',
|
|
329
|
+
hour: 'numeric',
|
|
330
|
+
minute: '2-digit',
|
|
331
|
+
hour12: true
|
|
332
|
+
}),
|
|
333
|
+
type: 'datetime'
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get current time
|
|
339
|
+
*/
|
|
340
|
+
getCurrentTime() {
|
|
341
|
+
const now = new Date();
|
|
342
|
+
return {
|
|
343
|
+
result: now.toLocaleTimeString('en-US', {
|
|
344
|
+
hour: 'numeric',
|
|
345
|
+
minute: '2-digit',
|
|
346
|
+
hour12: true
|
|
347
|
+
}),
|
|
348
|
+
formatted: now.toLocaleTimeString('en-US', {
|
|
349
|
+
hour: 'numeric',
|
|
350
|
+
minute: '2-digit',
|
|
351
|
+
second: '2-digit',
|
|
352
|
+
hour12: true
|
|
353
|
+
}),
|
|
354
|
+
type: 'time'
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get current week number
|
|
360
|
+
*/
|
|
361
|
+
getWeekNumber() {
|
|
362
|
+
const now = new Date();
|
|
363
|
+
const start = new Date(now.getFullYear(), 0, 1);
|
|
364
|
+
const diff = now - start;
|
|
365
|
+
const oneWeek = 604800000;
|
|
366
|
+
const weekNum = Math.ceil((diff + start.getDay() * 86400000) / oneWeek);
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
result: weekNum,
|
|
370
|
+
formatted: `Week ${weekNum} of ${now.getFullYear()}`,
|
|
371
|
+
type: 'week'
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = DateTimeCalculator;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmer Calculator Module
|
|
3
|
+
* Handles hex, binary, octal conversions and bitwise operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ProgrammerCalculator {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Regex patterns for different number formats
|
|
9
|
+
this.patterns = {
|
|
10
|
+
hex: /^(?:0x|#)?([0-9a-fA-F]+)$/,
|
|
11
|
+
binary: /^(?:0b)?([01]+)$/,
|
|
12
|
+
octal: /^(?:0o)?([0-7]+)$/,
|
|
13
|
+
decimal: /^(\d+)$/
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse programmer calculation query
|
|
19
|
+
*/
|
|
20
|
+
parse(query) {
|
|
21
|
+
const q = query.toLowerCase().trim();
|
|
22
|
+
|
|
23
|
+
// Convert to decimal: "0xFF to decimal", "hex FF to dec"
|
|
24
|
+
const toDecMatch = q.match(/(?:0x|hex\s*)?([0-9a-f]+)\s+to\s+(?:dec|decimal)/i);
|
|
25
|
+
if (toDecMatch) {
|
|
26
|
+
return this.hexToDecimal(toDecMatch[1]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Convert to hex: "255 to hex", "decimal 255 to hex"
|
|
30
|
+
const toHexMatch = q.match(/(?:decimal\s+)?(\d+)\s+to\s+hex/i);
|
|
31
|
+
if (toHexMatch) {
|
|
32
|
+
return this.decimalToHex(parseInt(toHexMatch[1]));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Convert to binary: "255 to binary", "255 to bin"
|
|
36
|
+
const toBinMatch = q.match(/(?:decimal\s+)?(\d+)\s+to\s+(?:bin|binary)/i);
|
|
37
|
+
if (toBinMatch) {
|
|
38
|
+
return this.decimalToBinary(parseInt(toBinMatch[1]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convert binary to decimal: "0b1111 to decimal", "binary 1111 to dec"
|
|
42
|
+
const binToDecMatch = q.match(/(?:0b|binary\s+)?([01]+)\s+to\s+(?:dec|decimal)/i);
|
|
43
|
+
if (binToDecMatch && q.includes('bin') || q.startsWith('0b')) {
|
|
44
|
+
return this.binaryToDecimal(binToDecMatch[1]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Convert to octal: "255 to octal", "255 to oct"
|
|
48
|
+
const toOctMatch = q.match(/(?:decimal\s+)?(\d+)\s+to\s+(?:oct|octal)/i);
|
|
49
|
+
if (toOctMatch) {
|
|
50
|
+
return this.decimalToOctal(parseInt(toOctMatch[1]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Quick hex prefix: "hex 255", "hex(255)"
|
|
54
|
+
const hexMatch = q.match(/^hex\s*\(?(\d+)\)?$/i);
|
|
55
|
+
if (hexMatch) {
|
|
56
|
+
return this.decimalToHex(parseInt(hexMatch[1]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Quick binary prefix: "bin 255", "binary 255"
|
|
60
|
+
const binMatch = q.match(/^(?:bin|binary)\s*\(?(\d+)\)?$/i);
|
|
61
|
+
if (binMatch) {
|
|
62
|
+
return this.decimalToBinary(parseInt(binMatch[1]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Quick octal prefix: "oct 255", "octal 255"
|
|
66
|
+
const octMatch = q.match(/^(?:oct|octal)\s*\(?(\d+)\)?$/i);
|
|
67
|
+
if (octMatch) {
|
|
68
|
+
return this.decimalToOctal(parseInt(octMatch[1]));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Bitwise operations: "255 AND 128", "255 OR 64", "NOT 255", "255 XOR 128"
|
|
72
|
+
const bitwiseMatch = q.match(/(\d+)\s+(and|or|xor|nand|nor)\s+(\d+)/i);
|
|
73
|
+
if (bitwiseMatch) {
|
|
74
|
+
return this.bitwiseOperation(
|
|
75
|
+
parseInt(bitwiseMatch[1]),
|
|
76
|
+
bitwiseMatch[2].toLowerCase(),
|
|
77
|
+
parseInt(bitwiseMatch[3])
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// NOT operation: "NOT 255", "~255"
|
|
82
|
+
const notMatch = q.match(/(?:not|~)\s*(\d+)/i);
|
|
83
|
+
if (notMatch) {
|
|
84
|
+
return this.bitwiseNot(parseInt(notMatch[1]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Shift operations: "255 << 2", "255 >> 2"
|
|
88
|
+
const shiftMatch = q.match(/(\d+)\s*(<<|>>|>>>)\s*(\d+)/);
|
|
89
|
+
if (shiftMatch) {
|
|
90
|
+
return this.bitShift(
|
|
91
|
+
parseInt(shiftMatch[1]),
|
|
92
|
+
shiftMatch[2],
|
|
93
|
+
parseInt(shiftMatch[3])
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Direct 0x prefix detection
|
|
98
|
+
if (q.startsWith('0x')) {
|
|
99
|
+
return this.hexToDecimal(q.slice(2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Direct 0b prefix detection
|
|
103
|
+
if (q.startsWith('0b')) {
|
|
104
|
+
return this.binaryToDecimal(q.slice(2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Hex to Decimal
|
|
112
|
+
*/
|
|
113
|
+
hexToDecimal(hex) {
|
|
114
|
+
const decimal = parseInt(hex, 16);
|
|
115
|
+
if (isNaN(decimal)) {
|
|
116
|
+
return { error: `Invalid hex: ${hex}`, type: 'error' };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
result: decimal,
|
|
120
|
+
formatted: `0x${hex.toUpperCase()} = ${decimal} (decimal)`,
|
|
121
|
+
type: 'programmer',
|
|
122
|
+
conversions: this.getAllConversions(decimal)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Decimal to Hex
|
|
128
|
+
*/
|
|
129
|
+
decimalToHex(decimal) {
|
|
130
|
+
if (isNaN(decimal)) {
|
|
131
|
+
return { error: `Invalid number: ${decimal}`, type: 'error' };
|
|
132
|
+
}
|
|
133
|
+
const hex = decimal.toString(16).toUpperCase();
|
|
134
|
+
return {
|
|
135
|
+
result: `0x${hex}`,
|
|
136
|
+
formatted: `${decimal} = 0x${hex} (hex)`,
|
|
137
|
+
type: 'programmer',
|
|
138
|
+
conversions: this.getAllConversions(decimal)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Decimal to Binary
|
|
144
|
+
*/
|
|
145
|
+
decimalToBinary(decimal) {
|
|
146
|
+
if (isNaN(decimal)) {
|
|
147
|
+
return { error: `Invalid number: ${decimal}`, type: 'error' };
|
|
148
|
+
}
|
|
149
|
+
const binary = decimal.toString(2);
|
|
150
|
+
// Pad to 8-bit boundaries
|
|
151
|
+
const padded = binary.padStart(Math.ceil(binary.length / 8) * 8, '0');
|
|
152
|
+
const formatted = padded.replace(/(.{4})/g, '$1 ').trim();
|
|
153
|
+
return {
|
|
154
|
+
result: `0b${binary}`,
|
|
155
|
+
formatted: `${decimal} = ${formatted} (binary)`,
|
|
156
|
+
type: 'programmer',
|
|
157
|
+
conversions: this.getAllConversions(decimal)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Binary to Decimal
|
|
163
|
+
*/
|
|
164
|
+
binaryToDecimal(binary) {
|
|
165
|
+
const clean = binary.replace(/\s/g, '');
|
|
166
|
+
const decimal = parseInt(clean, 2);
|
|
167
|
+
if (isNaN(decimal)) {
|
|
168
|
+
return { error: `Invalid binary: ${binary}`, type: 'error' };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
result: decimal,
|
|
172
|
+
formatted: `0b${clean} = ${decimal} (decimal)`,
|
|
173
|
+
type: 'programmer',
|
|
174
|
+
conversions: this.getAllConversions(decimal)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Decimal to Octal
|
|
180
|
+
*/
|
|
181
|
+
decimalToOctal(decimal) {
|
|
182
|
+
if (isNaN(decimal)) {
|
|
183
|
+
return { error: `Invalid number: ${decimal}`, type: 'error' };
|
|
184
|
+
}
|
|
185
|
+
const octal = decimal.toString(8);
|
|
186
|
+
return {
|
|
187
|
+
result: `0o${octal}`,
|
|
188
|
+
formatted: `${decimal} = 0o${octal} (octal)`,
|
|
189
|
+
type: 'programmer',
|
|
190
|
+
conversions: this.getAllConversions(decimal)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Bitwise operations
|
|
196
|
+
*/
|
|
197
|
+
bitwiseOperation(a, op, b) {
|
|
198
|
+
let result;
|
|
199
|
+
switch (op) {
|
|
200
|
+
case 'and': result = a & b; break;
|
|
201
|
+
case 'or': result = a | b; break;
|
|
202
|
+
case 'xor': result = a ^ b; break;
|
|
203
|
+
case 'nand': result = ~(a & b); break;
|
|
204
|
+
case 'nor': result = ~(a | b); break;
|
|
205
|
+
default: return { error: `Unknown operation: ${op}`, type: 'error' };
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
result: result,
|
|
209
|
+
formatted: `${a} ${op.toUpperCase()} ${b} = ${result}`,
|
|
210
|
+
type: 'programmer',
|
|
211
|
+
conversions: this.getAllConversions(result)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Bitwise NOT
|
|
217
|
+
*/
|
|
218
|
+
bitwiseNot(a) {
|
|
219
|
+
// For 32-bit unsigned representation
|
|
220
|
+
const result = ~a >>> 0;
|
|
221
|
+
return {
|
|
222
|
+
result: result,
|
|
223
|
+
formatted: `NOT ${a} = ${result} (32-bit unsigned)`,
|
|
224
|
+
type: 'programmer',
|
|
225
|
+
conversions: this.getAllConversions(result)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Bit shift operations
|
|
231
|
+
*/
|
|
232
|
+
bitShift(a, op, b) {
|
|
233
|
+
let result;
|
|
234
|
+
switch (op) {
|
|
235
|
+
case '<<': result = a << b; break;
|
|
236
|
+
case '>>': result = a >> b; break;
|
|
237
|
+
case '>>>': result = a >>> b; break;
|
|
238
|
+
default: return { error: `Unknown shift: ${op}`, type: 'error' };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
result: result,
|
|
242
|
+
formatted: `${a} ${op} ${b} = ${result}`,
|
|
243
|
+
type: 'programmer',
|
|
244
|
+
conversions: this.getAllConversions(result)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get all conversions for a number
|
|
250
|
+
*/
|
|
251
|
+
getAllConversions(decimal) {
|
|
252
|
+
if (decimal < 0) {
|
|
253
|
+
// Handle negative numbers with 32-bit representation
|
|
254
|
+
decimal = decimal >>> 0;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
decimal: decimal,
|
|
258
|
+
hex: '0x' + decimal.toString(16).toUpperCase(),
|
|
259
|
+
binary: '0b' + decimal.toString(2),
|
|
260
|
+
octal: '0o' + decimal.toString(8)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = ProgrammerCalculator;
|