Compare commits

..

77 Commits

Author SHA1 Message Date
tanner 0ea5212778 test: Add tests for leading zero visual input handling
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 5dd0272350 fix: Prevent multiple leading zeros in number input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 8de4d70c8b test: Add chaos tests for number input, decimals, and leading zeros
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 7a3e5cd868 fix: Prevent multiple decimal points in number input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 8ed03143bd fix: Set haptic feedback duration for button presses to 30ms 2026-06-13 16:13:48 -06:00
tanner 95cc91781d feat: Add haptic feedback for number, decimal, and backspace
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 1a2fa25ea7 fix: Use buttonPress() for trig functions in tests
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner df0a2c03fc test: Expect 0 for sin/tan(PI) results due to float correction
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner d2c4be15d0 test: Expand Pi button test coverage
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3937109edb feat: Replace percent button with Pi button, update logic and tests
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 98fdddac5d fix: Correct negative zero logic for +/- and update test
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 73cd819e94 fix: Convert scientific notation to decimal for test input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner abe931d61d style: Improve readability of edge case numbers in tests 2026-06-13 16:13:48 -06:00
tanner 0bf1220c62 test: Add extensive edge case tests and expand number generation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner e32b274e5c fix: Update checkDisplay logic and backspace/trig test assertions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2088081bb8 test: Improve output validation by spying on raw display numbers
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2754d4f52a test: Improve display truncation handling for large numbers
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 04efe1b4d9 fix: Adjust test stringWidth mock to prevent truncation issues
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 728687ab37 fix: Adjust test stringWidth mock for display precision
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 77288c92af fix: Prevent displayOutput from clearing state on Infinity/NaN
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner e8a361210d test: Correct infinity string comparison in checkDisplay
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 255984e52f fix: Apply negation to current number when new number is pressed
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 20ce84ed89 test: Replace static tests with comprehensive programmatic test suite
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 31531ff118 fix: Increase test tolerance for float comparisons
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 37867a095b fix: Improve display comparison for floating point and truncation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 83a50fa06a Revert to calc before tests 2026-06-13 16:13:48 -06:00
tanner ab8f7e78b8 fix: Round floats for improved precision in fixFloat
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6fc30311f3 fix: Correct process mock and use buttonPress for multi-char commands
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a59b92572b fix: Reset angle mode on AC and parse float for square root
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2c91af6759 fix: Parse numeric input for scientific ops and expose operators to tests
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6ae1b173bd feat: Update logging endpoint and add buzz on success 2026-06-13 16:13:48 -06:00
tanner f94d5c2105 fix: Capture correct display output in g.drawString mock
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 913156ca3c fix: Expose buttonPress function for testing
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a5adeb379c fix: Mock FontDylex7x13 module in Node.js test runner
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 281187fd91 test: Add full simulation tests for calculator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner f5abf6eb48 test: Add calculator tests 2026-06-13 16:13:48 -06:00
tanner 810e43bcfd feat: Add HTTP POST for calculation logs with error display
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 1c511d3441 fix: Correct expression display on repeated '=' press
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3b66810ba1 feat: Add send function to log calculation history and result
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner ebb1550fc7 fix: Relax scientific notation display trigger
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6b7285a017 refactor: Remove unnecessary number display formatting 2026-06-13 16:13:48 -06:00
tanner 85185dbbaa fix: Replace regex with manual thousands separator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 5cb5ee604b fix: Ensure thousands separators display without premature scientific notation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner e57e09eb17 style: Use comma as thousands separator 2026-06-13 16:13:48 -06:00
tanner f3fdf4e5ab feat: Dynamically adjust result display width based on font
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2106a4298e refactor: Use Dylex7x13 font for calculator UI 2026-06-13 16:13:48 -06:00
tanner ca2265e8aa feat: Add thousands separator to calculator output
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner b86f6720bc fix: Correctly parse leading zeros for decimal input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 8c7c488967 refactor: Simplify math operations, remove custom precision functions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3507d8938e fix: Parse scientific notation in getIntWithPrecision
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 9738c76140 fix: Parse numbers as decimal in getIntWithPrecision
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner f19a77d06f fix: Dynamically calculate precision for scientific notation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 739b337135 fix: Adjust precision for scientific notation display 2026-06-13 16:13:48 -06:00
tanner 68aa187f71 fix: Prevent extra space in scientific notation exponent
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner b369cb0c3b fix: Fix Math.log10 not found error using Math.log equivalent
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 212cbdb9b7 fix: Implement toExponential polyfill for broader compatibility
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a714c57b2b fix: Display small and large numbers in scientific notation
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 6ac882699a fix: Correct floating point precision for trig functions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 9b48f7342a fix: Set default angle mode to degrees and simplify error messages 2026-06-13 16:13:48 -06:00
tanner 8b5ac36284 feat: Add angle mode toggle and apply to trig functions
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 3c18e0326d feat: Add scientific operators page (trig, log, exp)
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2400ebd145 style: Set button text color to black
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 22e36c447d Use load() instead of Bangle.load() for calculator
see: https://www.espruino.com/ReferenceBANGLEJS2#l_Bangle_load
2026-06-13 16:13:48 -06:00
tanner 37de23ecfe feat: Launch calculator app via menu command
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 2a9f8ce857 feat: Add 1/x inversion function to operators menu
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner a113e10ae5 fix: Prevent leading zero when backspacing a decimal
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 197d393553 chore: Update square root operator display to 'sqrt' 2026-06-13 16:13:48 -06:00
tanner 8fd4b6e45a feat: Add square and square root functions to calculator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 36120b877f feat: Add backspace button to keypad
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-06-13 16:13:48 -06:00
tanner 69330e03cb feat: Add calculator app 2026-06-13 16:13:48 -06:00
Tanner 4c506c7913 Make weather or temp data grey if it's old 2026-06-13 11:24:21 -06:00
tanner 8275abdb57 Swap stop watch positions 2026-03-12 12:58:52 -06:00
tanner 1c433fc56b Display both feels like and outdoor temperatures 2026-03-12 12:52:51 -06:00
tanner 14bda5c29f Use clearInterval to clear the stopwatch interval 2026-03-10 16:58:57 -06:00
tanner ea58f2101f Increase buzz duration 2026-03-10 16:54:44 -06:00
tanner 07bb582ddd feat: Initialize buzz state for stopwatches
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-10 16:22:44 -06:00
tanner 6d965dd016 feat: Add timed buzzing alerts to stopwatches
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-10 16:22:18 -06:00
3 changed files with 1456 additions and 17 deletions
+51 -17
View File
@@ -11,14 +11,17 @@
let watchState = STATE_IDLE;
let stopWatch = require("Storage").readJSON("mystopwatch.json", true) || {start1: null, elapsed1: null, start2: null, elapsed2: null};
let stopWatch = require("Storage").readJSON("mystopwatch.json", true) || {start1: null, elapsed1: null, buzz1: null, start2: null, elapsed2: null, buzzed2: null};
let saveStopWatch = function() {
require("Storage").writeJSON("mystopwatch.json", stopWatch);
}
let stopWatchTimer = null;
let myMessage = "";
let temperature = "";
let temperature = "?";
let temp_old = true;
let feels_like = "?";
let weather_old = true;
let drawTimer = null;
@@ -37,24 +40,39 @@
let Tt1 = (stopWatch.elapsed1 || 0);
if (stopWatch.start1) {
Tt1 += Date.now() - stopWatch.start1;
const fifteenMinutes = 15 * 60 * 1000;
let intervals = Math.floor(Tt1 / fifteenMinutes);
if (intervals > (stopWatch.buzz1 || 0)) {
stopWatch.buzz1 = intervals;
Bangle.buzz(500);
saveStopWatch();
}
}
let Ttxt1 = timeToText(Tt1);
g.clearRect(0, y-60, w, y-34);
g.clearRect(0, y+61, w, y+88);
g.setColor(g.theme.fg);
g.setFontAlign(0, 0).setFont("Vector", 26).drawString("S1: " + Ttxt1, x, y-45);
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(Ttxt1, x, y+74);
}
if (stopWatch.start2 || stopWatch.elapsed2) {
let Tt2 = (stopWatch.elapsed2 || 0);
if (stopWatch.start2) {
Tt2 += Date.now() - stopWatch.start2;
const oneMinute = 60 * 1000;
if (!stopWatch.buzzed2 && Tt2 >= oneMinute) {
stopWatch.buzzed2 = true;
Bangle.buzz(500);
saveStopWatch();
}
}
let Ttxt2 = timeToText(Tt2);
g.clearRect(0, y+61, w, y+88);
g.clearRect(0, y-60, w, y-34);
g.setColor(g.theme.fg);
g.setFontAlign(0, 0).setFont("Vector", 26).drawString("S2: " + Ttxt2, x, y+76);
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(Ttxt2, x, y-45);
}
}
@@ -75,25 +93,32 @@
var utcHour = Math.floor(utc / 3600);
var utcMinute = Math.floor((utc % 3600) / 60);
var utcStr = utcHour.toString().padStart(2, '0') + ":" + utcMinute.toString().padStart(2, '0');
g.setFontAlign(0, 0).setFont("Vector", 36).drawString(utcStr, x-25, y+43);
g.setFontAlign(0, 0).setFont("Vector", 36).drawString(utcStr, x-32, y+43);
var tz_offset = date.toString().indexOf("GMT");
var tz = date.toString().substring(tz_offset+3, tz_offset+6);
g.setFontAlign(0, 0).setFont("Vector", 24).drawString(tz, x+60, y+43);
//var tz_offset = date.toString().indexOf("GMT");
//var tz = date.toString().substring(tz_offset+3, tz_offset+6);
//g.setFontAlign(0, 0).setFont("Vector", 24).drawString(tz, x+60, y+43);
if (temp_old) g.setColor("#888");
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(temperature, x+53, y+43);
g.setColor(g.theme.fg);
// Show date and day of week
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var dateStr = date.getDate() + " " + days[date.getDay()] + " " + temperature;
var dateStr = date.getDate() + " " + days[date.getDay()];
// don't draw date string if stopwatch 2 is running
if (!stopWatch.start2 && !stopWatch.elapsed2) {
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(dateStr, x, y+76);
// don't draw date string if stopwatch 1 is running
if (!stopWatch.start1 && !stopWatch.elapsed1) {
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(dateStr, x-32, y+74);
if (weather_old) g.setColor("#888");
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(feels_like, x+53, y+74);
g.setColor(g.theme.fg);
}
//var wrapped = g.wrapString(myMessage, g.getWidth()-10).join("\n");
// don't draw message if stopwatch 1 is running
if (!stopWatch.start1 && !stopWatch.elapsed1) {
// don't draw message if stopwatch 2 is running
if (!stopWatch.start2 && !stopWatch.elapsed2) {
g.setFontAlign(0, 0).setFont("Vector", 26).drawString(myMessage, x, y-45);
}
@@ -107,6 +132,9 @@
let result = JSON.parse(event.resp);
myMessage = result.context;
temperature = result.temperature;
temp_old = result.temp_old;
feels_like = result.feels_like;
weather_old = result.weather_old;
if (watchState == STATE_IDLE) {
if (paintFace) paintFace();
}
@@ -193,6 +221,7 @@
stopWatch.start1 = Date.now();
if (!stopWatch.elapsed1) {
stopWatch.elapsed1 = 0;
stopWatch.buzz1 = 0;
}
saveStopWatch();
@@ -207,6 +236,7 @@
stopWatch.start2 = Date.now();
if (!stopWatch.elapsed2) {
stopWatch.elapsed2 = 0;
stopWatch.buzzed2 = false;
}
saveStopWatch();
@@ -233,6 +263,7 @@
let stopSW1 = function() {
stopWatch.start1 = null;
stopWatch.elapsed1 = null;
stopWatch.buzz1 = null;
saveStopWatch();
if (!stopWatch.start2) {
@@ -260,6 +291,7 @@
let stopSW2 = function() {
stopWatch.start2 = null;
stopWatch.elapsed2 = null;
stopWatch.buzzed2 = null;
saveStopWatch();
if (!stopWatch.start1) {
@@ -334,6 +366,8 @@
console.log("Pulling new menu...");
menu = null;
require("Storage").writeJSON("menu.json", menu);
} else if (menuCommand == "commands,util,calc,") {
load("calculator.app.js");
} else if (menuCommand == "states,stop watch,start1,") {
startSW1();
} else if (menuCommand == "states,stop watch,start2,") {
@@ -403,7 +437,7 @@
Bangle.removeListener('twist', handleTwist);
if (drawTimer) clearTimeout(drawTimer);
drawTimer = undefined;
if (stopWatchTimer) clearTimeout(stopWatchTimer);
if (stopWatchTimer) clearInterval(stopWatchTimer);
stopWatchTimer = undefined;
paintFace = undefined; // http request may resolve after font's been unloaded, so unset
+736
View File
@@ -0,0 +1,736 @@
/**
* BangleJS Calculator
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*
* Contributors: thyttan https://github.com/thyttan
*/
g.clear();
require("FontDylex7x13").add(Graphics);
var DEFAULT_SELECTION_NUMBERS = '5';
var RESULT_HEIGHT = 40;
var COLORS = {
// [normal, selected]
DEFAULT: ['#7F8183', '#A6A6A7'],
OPERATOR: ['#F99D1C', '#CA7F2A'],
SPECIAL: ['#65686C', '#7F8183']
};
var KEY_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()];
var screen, screenColor;
var globalGrid = [4, 5];
var swipeEnabled;
var numbersGrid = [3, 4];
var numbers = {
'0': {grid: [1, 3], globalGrid: [1, 4], trbl: '2.0B'},
'.': {grid: [2, 3], globalGrid: [2, 4], trbl: '3=.0'},
'1': {grid: [0, 2], globalGrid: [0, 3], trbl: '42B1'},
'2': {grid: [1, 2], globalGrid: [1, 3], trbl: '5301'},
'3': {grid: [2, 2], globalGrid: [2, 3], trbl: '6+.2'},
'4': {grid: [0, 1], globalGrid: [0, 2], trbl: '7514'},
'5': {grid: [1, 1], globalGrid: [1, 2], trbl: '8624'},
'6': {grid: [2, 1], globalGrid: [2, 2], trbl: '9-35'},
'7': {grid: [0, 0], globalGrid: [0, 1], trbl: 'R847'},
'8': {grid: [1, 0], globalGrid: [1, 1], trbl: 'N957'},
'9': {grid: [2, 0], globalGrid: [2, 1], trbl: 'p*68'},
'B': {grid: [0, 3], globalGrid: [0, 4], trbl: '10BB', val: '<-', color: COLORS.SPECIAL},
};
var operatorsGrid = [2, 3];
var operators = {
'+': {grid: [0, 0], globalGrid: [3, 3], trbl: '-+=3'},
'-': {grid: [1, 0], globalGrid: [3, 2], trbl: '*-+6'},
'*': {grid: [0, 1], globalGrid: [3, 1], trbl: '/*-9'},
'/': {grid: [1, 1], globalGrid: [3, 0], trbl: '//*p'},
'=': {grid: [1, 2], globalGrid: [3, 4], trbl: '+==.'},
};
if (process.env.HWVERSION!=1) {
operatorsGrid = [2, 4];
operators['='].grid = [1, 3];
operators.r = {grid: [0, 2], val: 'sqrt'};
operators.s = {grid: [1, 2], val: 'x^2'};
operators.i = {grid: [0, 3], val: '1/x'};
}
var scientificOperatorsGrid = [2, 4];
var scientificOperators = {
'sin': {grid: [0, 0], val: 'sin'},
'cos': {grid: [1, 0], val: 'cos'},
'tan': {grid: [0, 1], val: 'tan'},
'angleMode': {grid: [1, 1], val: 'deg'},
'log': {grid: [0, 2], val: 'log'},
'tenpow': {grid: [1, 2], val: '10^x'},
'ln': {grid: [0, 3], val: 'ln'},
'epow': {grid: [1, 3], val: 'e^x'},
};
var specialsGrid = [2, 2];
var specials = {
'R': {grid: [0, 0], globalGrid: [0, 0], trbl: 'RN7R', val: 'AC'},
'N': {grid: [1, 0], globalGrid: [1, 0], trbl: 'Np8R', val: '+/-'},
'p': {grid: [0, 1], globalGrid: [2, 0], trbl: 'p/9N', val: 'Pi'},
'send': {grid: [1, 1], val: 'send'},
};
var selected = DEFAULT_SELECTION_NUMBERS;
var prevSelected = DEFAULT_SELECTION_NUMBERS;
var prevNumber = null;
var currNumber = null;
var operator = null;
var results = null;
var isDecimal = false;
var hasPressedEquals = false;
var angleMode = 'deg';
var prevExpression = null;
var currExpression = "";
function prepareScreen(screen, grid, defaultColor) {
for (var k in screen) {
if (screen.hasOwnProperty(k)) {
screen[k].color = screen[k].color || defaultColor;
var position = [];
var xGrid = (KEY_AREA[2]-KEY_AREA[0])/grid[0];
var yGrid = (KEY_AREA[3]-KEY_AREA[1])/grid[1];
if (swipeEnabled) {
position[0] = KEY_AREA[0]+xGrid*screen[k].grid[0];
position[1] = KEY_AREA[1]+yGrid*screen[k].grid[1];
} else {
position[0] = KEY_AREA[0]+xGrid*screen[k].globalGrid[0];
position[1] = KEY_AREA[1]+yGrid*screen[k].globalGrid[1];
}
position[2] = position[0]+xGrid-1;
position[3] = position[1]+yGrid-1;
screen[k].xy = position;
}
}
}
function drawKey(name, k, selected) {
var color = k.color || COLORS.DEFAULT;
g.setColor(color[selected ? 1 : 0]);
g.setFont('Dylex7x13', 2).setFontAlign(0,0);
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
g.setColor(0);
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
}
function drawKeys() {
g.setColor(screenColor[0]);
g.fillRect(KEY_AREA[0], KEY_AREA[1], KEY_AREA[2], KEY_AREA[3]);
for (var k in screen) {
if (screen.hasOwnProperty(k)) {
drawKey(k, screen[k], k == selected);
}
}
}
function drawGlobal() {
screen = {};
screenColor = COLORS.DEFAULT;
prepareScreen(numbers, globalGrid, COLORS.DEFAULT);
for (var k in numbers) {
screen[k] = numbers[k];
}
prepareScreen(operators, globalGrid, COLORS.OPERATOR);
for (var k in operators) {
screen[k] = operators[k];
}
prepareScreen(specials, globalGrid, COLORS.SPECIAL);
for (var k in specials) {
screen[k] = specials[k];
}
drawKeys();
}
function drawNumbers() {
screen = numbers;
screenColor = COLORS.DEFAULT;
drawKeys();
}
function drawOperators() {
screen = operators;
screenColor =COLORS.OPERATOR;
drawKeys();
}
function drawScientificOperators() {
screen = scientificOperators;
screenColor =COLORS.OPERATOR;
drawKeys();
}
function drawSpecials() {
screen = specials;
screenColor = COLORS.SPECIAL;
drawKeys();
}
function toExponential(num, precision) {
if (num === 0) {
let z = "0";
if (precision > 0) z += "." + "0".repeat(precision);
return z + "e+0";
}
var sign = "";
if (num < 0) {
sign = "-";
num = -num;
}
var exp = Math.floor(Math.log(num) / Math.LN10);
var mantissa = num / Math.pow(10, exp);
var mantissaFixed = mantissa.toFixed(precision);
if (parseFloat(mantissaFixed) >= 10) {
mantissaFixed = (mantissa/10).toFixed(precision);
exp++;
}
return sign + mantissaFixed + "e" + (exp >= 0 ? "+" : "") + exp;
}
function fixFloat(n) {
if (Math.abs(n) < 1e-10) return 0;
return n;
}
function addSeparators(s) {
var parts = s.split(".");
var intPart = parts[0];
var sign = "";
if (intPart[0] === "-") {
sign = "-";
intPart = intPart.slice(1);
}
var result = "";
while (intPart.length > 3) {
result = "," + intPart.slice(-3) + result;
intPart = intPart.slice(0, -3);
}
result = intPart + result;
parts[0] = sign + result;
return parts.join(".");
}
function doMath(x, y, operator) {
x = parseFloat(x);
y = parseFloat(y);
switch (operator) {
case '/':
return x / y;
case '*':
return x * y;
case '+':
return x + y;
case '-':
return x - y;
}
}
function displayOutput(num) {
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
g.setColor(-1);
g.setFont('Dylex7x13', 2);
if (num === Infinity || num === -Infinity || isNaN(num)) {
// handle division by 0
if (num === Infinity) {
num = 'INF';
} else if (num === -Infinity) {
num = '-INF';
} else {
num = 'NaN';
}
specials.R.val = 'AC';
if (!swipeEnabled) drawKey('R', specials.R);
} else {
// might not be a number due to display of dot "."
var numNumeric = Number(num);
if (typeof num === 'string') {
if (num.indexOf('.') !== -1) {
// display a 0 before a lonely dot
if (numNumeric == 0) {
num = '0.';
}
} else {
// remove preceding 0
while (num.length > 1 && num[0] === '0')
num = num.substr(1);
}
}
var numStr = num.toString();
var displayStr;
if (typeof num === 'number' && (numStr.indexOf('e') > -1 || (num !== 0 && Math.abs(num) < 1e-6))) {
// try to format as scientific notation
let precision = 10; // start with high precision
while (precision >= 0) {
let scientificStr = toExponential(num, precision).replace("e", "E");
if (g.stringWidth(scientificStr) <= g.getWidth() - 20) {
displayStr = scientificStr;
break;
}
precision--;
}
if (precision < 0) { // if it still doesn't fit
displayStr = toExponential(num, 0).replace("e", "E");
}
} else {
displayStr = addSeparators(numStr);
}
num = displayStr;
// final check for truncation
if (g.stringWidth(num) > g.getWidth() - 20) {
while(g.stringWidth(num+'...') > g.getWidth() - 20 && num.length > 1) {
num = num.slice(0,-1);
}
num += '...';
}
}
g.setFontAlign(1,0);
g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2);
if (operator) {
g.setFont('Dylex7x13', 2).setFontAlign(1,0);
g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2);
}
}
var wasPressedEquals = false;
var hasPressedNumber = false;
function calculatorLogic(x) {
if (wasPressedEquals && hasPressedNumber !== false) {
prevNumber = null;
currNumber = hasPressedNumber;
wasPressedEquals = false;
hasPressedNumber = false;
return;
}
if (hasPressedEquals) {
if (hasPressedNumber) {
prevNumber = null;
hasPressedNumber = false;
operator = null;
} else {
currNumber = null;
prevNumber = results;
}
hasPressedEquals = false;
wasPressedEquals = true;
}
if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) {
operator = x;
displayOutput(prevNumber);
} else if (prevNumber != null && currNumber != null && operator != null) {
// we execute the calculus only when there was a previous number entered before and an operator
let oldOperator = operator;
results = doMath(prevNumber, currNumber, operator);
operator = x;
prevNumber = results;
prevExpression = "(" + prevExpression + " " + oldOperator + " " + currExpression + ")";
currNumber = null;
currExpression = "";
displayOutput(results);
} else if (prevNumber == null && currNumber != null && operator == null) {
// no operator yet, save the current number for later use when an operator is pressed
operator = x;
prevNumber = currNumber;
prevExpression = currExpression;
currNumber = null;
currExpression = "";
displayOutput(prevNumber);
} else if (prevNumber == null && currNumber == null && operator == null) {
displayOutput(0);
}
}
function buttonPress(val) {
switch (val) {
case 'R':
currNumber = null;
results = null;
isDecimal = false;
hasPressedEquals = false;
if (specials.R.val == 'AC') {
prevNumber = null;
operator = null;
prevExpression = null;
currExpression = "";
} else {
specials.R.val = 'AC';
drawKey('R', specials.R, true);
}
wasPressedEquals = false;
hasPressedNumber = false;
displayOutput(0);
break;
case 'p':
specials.R.val = 'C';
if (!swipeEnabled) drawKey('R', specials.R);
currNumber = Math.PI;
if (hasPressedEquals === 1) {
hasPressedEquals = 2;
}
hasPressedNumber = currNumber;
currExpression = currNumber.toString();
displayOutput(currNumber);
break;
case 'r':
if (results != null) {
results = Math.sqrt(results);
displayOutput(results);
prevExpression = "sqrt(" + prevExpression + ")";
} else if (currNumber != null) {
currNumber = Math.sqrt(currNumber);
displayOutput(currNumber);
currExpression = "sqrt(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 's':
if (results != null) {
results = results * results;
displayOutput(results);
prevExpression = "(" + prevExpression + ")^2";
} else if (currNumber != null) {
var num = parseFloat(currNumber);
currNumber = num * num;
displayOutput(currNumber);
currExpression = "(" + currExpression + ")^2";
}
hasPressedNumber = false;
break;
case 'sin':
if (results != null) {
let angle = results;
if (angleMode === 'deg') {
angle = angle * Math.PI / 180;
}
results = fixFloat(Math.sin(angle));
displayOutput(results);
prevExpression = "sin(" + prevExpression + ")";
} else if (currNumber != null) {
let angle = currNumber;
if (angleMode === 'deg') {
angle = angle * Math.PI / 180;
}
currNumber = fixFloat(Math.sin(angle));
displayOutput(currNumber);
currExpression = "sin(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'cos':
if (results != null) {
let angle = results;
if (angleMode === 'deg') {
angle = angle * Math.PI / 180;
}
results = fixFloat(Math.cos(angle));
displayOutput(results);
prevExpression = "cos(" + prevExpression + ")";
} else if (currNumber != null) {
let angle = currNumber;
if (angleMode === 'deg') {
angle = angle * Math.PI / 180;
}
currNumber = fixFloat(Math.cos(angle));
displayOutput(currNumber);
currExpression = "cos(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'tan':
if (results != null) {
let angle = results;
if (angleMode === 'deg') {
angle = angle * Math.PI / 180;
}
results = fixFloat(Math.tan(angle));
displayOutput(results);
prevExpression = "tan(" + prevExpression + ")";
} else if (currNumber != null) {
let angle = currNumber;
if (angleMode === 'deg') {
angle = angle * Math.PI / 180;
}
currNumber = fixFloat(Math.tan(angle));
displayOutput(currNumber);
currExpression = "tan(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'log':
if (results != null) {
results = Math.log(results) / Math.LN10;
displayOutput(results);
prevExpression = "log(" + prevExpression + ")";
} else if (currNumber != null) {
currNumber = Math.log(currNumber) / Math.LN10;
displayOutput(currNumber);
currExpression = "log(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'tenpow':
if (results != null) {
results = Math.pow(10, results);
displayOutput(results);
prevExpression = "10^(" + prevExpression + ")";
} else if (currNumber != null) {
currNumber = Math.pow(10, currNumber);
displayOutput(currNumber);
currExpression = "10^(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'ln':
if (results != null) {
results = Math.log(results);
displayOutput(results);
prevExpression = "ln(" + prevExpression + ")";
} else if (currNumber != null) {
currNumber = Math.log(currNumber);
displayOutput(currNumber);
currExpression = "ln(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'epow':
if (results != null) {
results = Math.exp(results);
displayOutput(results);
prevExpression = "e^(" + prevExpression + ")";
} else if (currNumber != null) {
currNumber = Math.exp(currNumber);
displayOutput(currNumber);
currExpression = "e^(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'angleMode':
if (angleMode === 'rad') {
angleMode = 'deg';
} else {
angleMode = 'rad';
}
scientificOperators.angleMode.val = angleMode;
drawKey('angleMode', scientificOperators.angleMode);
break;
case 'i':
if (results != null) {
results = 1 / results;
displayOutput(results);
prevExpression = "1/(" + prevExpression + ")";
} else if (currNumber != null) {
currNumber = 1 / parseFloat(currNumber);
displayOutput(currNumber);
currExpression = "1/(" + currExpression + ")";
}
hasPressedNumber = false;
break;
case 'N':
if (results != null && !hasPressedNumber) {
if (results === 0) {
results = (1/results === -Infinity || Object.is(results, -0)) ? 0 : -0;
} else {
results *= -1;
}
displayOutput(results);
prevExpression = "-(" + prevExpression + ")";
} else {
if (currNumber === null) currNumber = '0';
var num = parseFloat(currNumber);
if (num === 0) {
// Toggle between 0 and -0
currNumber = (1/currNumber === -Infinity || Object.is(currNumber, -0)) ? '0' : -0;
} else {
currNumber = num * -1;
}
displayOutput(currNumber);
currExpression = "-(" + currExpression + ")";
}
break;
case 'B':
Bangle.buzz(30);
if (isDecimal) {
isDecimal = false;
displayOutput(currNumber);
break;
}
if (currNumber != null) {
currNumber = currNumber.toString();
if (currNumber.length > 1) {
currNumber = currNumber.slice(0, -1);
} else {
currNumber = '0';
}
// if we removed a decimal point
if (currNumber.indexOf('.') === -1) {
isDecimal = false;
}
hasPressedNumber = currNumber;
currExpression = currNumber;
displayOutput(currNumber);
}
break;
case '/':
case '*':
case '-':
case '+':
calculatorLogic(val);
hasPressedNumber = false;
if (swipeEnabled) drawNumbers();
break;
case '.':
Bangle.buzz(30);
if (currNumber != null && currNumber.toString().indexOf('.') !== -1) {
break;
}
specials.R.val = 'C';
if (!swipeEnabled) drawKey('R', specials.R);
isDecimal = true;
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
break;
case '=':
if (prevNumber != null && currNumber != null && operator != null) {
let cExpr = currExpression;
if (hasPressedEquals && cExpr === "") {
cExpr = currNumber.toString();
}
prevExpression = prevExpression + " " + operator + " " + cExpr;
results = doMath(prevNumber, currNumber, operator);
prevNumber = results;
currExpression = "";
displayOutput(results);
hasPressedEquals = 1;
}
hasPressedNumber = false;
break;
case 'send':
let logStr;
if (hasPressedEquals) {
logStr = prevExpression + " = " + results;
} else if (operator && prevExpression) { // Incomplete binary operation, e.g. "5 +" or "5 + 3"
logStr = prevExpression + " " + operator;
if (currExpression) {
logStr += " " + currExpression;
}
} else if (results) { // Result of a unary op on a previous result
logStr = prevExpression + " = " + results;
} else if (currNumber) { // A number has been entered, or a unary op on it
if (currExpression && currExpression !== currNumber.toString()) {
logStr = currExpression + " = " + currNumber;
} else {
logStr = currNumber.toString();
}
} else {
logStr = results ? results.toString() : "0";
}
console.log(logStr);
if (Bangle.http){
const options = {timeout:3000, method: "post", body: logStr};
Bangle.http("https://tbot.tannercollin.com/banglecalc", options).then(event => {
Bangle.buzz();
console.log("Successfully sent");
}).catch((e)=>{
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
g.setColor(-1);
g.setFont('Dylex7x13', 2);
g.setFontAlign(1,0);
g.drawString('ERROR', g.getWidth()-20, RESULT_HEIGHT/2);
if (operator) {
g.setFont('Dylex7x13', 2).setFontAlign(1,0);
g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2);
}
});
}
break;
default: {
Bangle.buzz(30);
specials.R.val = 'C';
if (!swipeEnabled) drawKey('R', specials.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
if (isDecimal) {
currNumber = currNumber == null || hasPressedEquals === 1 ? '0.' + val : parseInt(currNumber, 10) + '.' + val;
isDecimal = false;
} else {
currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val);
// remove preceding 0 on integers
if (typeof currNumber === 'string' && currNumber.length > 1 && currNumber[0] === '0' && currNumber.indexOf('.') === -1) {
currNumber = currNumber.substr(1);
}
}
if (hasPressedEquals === 1) {
hasPressedEquals = 2;
}
hasPressedNumber = currNumber;
currExpression = currNumber;
displayOutput(currNumber);
break;
}
}
}
function moveDirection(d) {
drawKey(selected, screen[selected]);
prevSelected = selected;
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : screen[selected].trbl[d];
drawKey(selected, screen[selected], true);
}
if (process.env.HWVERSION==1) {
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
swipeEnabled = false;
drawGlobal();
} else { // touchscreen?
selected = "NONE";
swipeEnabled = true;
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
prepareScreen(scientificOperators, scientificOperatorsGrid, COLORS.OPERATOR);
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
drawNumbers();
Bangle.setUI({
mode : 'custom',
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
touch : (n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
//print("Press "+key);
buttonPress(""+key);
}
}
},
swipe : (LR, UD) => {
if (UD !== 0) {
drawNumbers();
return;
}
if (LR === 1) { // right
if (screen === scientificOperators) drawOperators();
else if (screen === operators) drawNumbers();
else if (screen === numbers) drawSpecials();
else if (screen === specials) drawNumbers();
}
if (LR === -1) { // left
if (screen === numbers) drawOperators();
else if (screen === operators) drawScientificOperators();
else if (screen === specials) drawNumbers();
else if (screen === scientificOperators) drawNumbers();
}
}
});
}
displayOutput(0);
+669
View File
@@ -0,0 +1,669 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
// --- Mock Bangle.js environment ---
let mock_display_str = "";
let mock_ui_callbacks = {};
let drawn_since_clear = false;
global.last_display_num = null;
// Mock 'g' (graphics)
global.g = {
_col: 0,
_bg: 0,
_font: "6x8",
_font_size: 1,
clear: () => { drawn_since_clear = false; },
clearRect: () => { drawn_since_clear = false; },
setColor: (c) => { global.g._col = c; return global.g; },
setBgColor: (c) => { global.g._bg = c; return global.g; },
fillRect: () => {},
setFont: function(font, size) {
if (font && font.includes(":")) {
const parts = font.split(":");
this._font = parts[0];
this._font_size = parseInt(parts[1], 10);
} else {
this._font = font;
if (size) this._font_size = size;
}
return this;
},
setFontAlign: () => {},
stringWidth: (s) => s.length * 4.7 * (global.g._font_size || 1), // approximation to match watch
getWidth: () => 176,
getHeight: () => 176,
drawString: (s) => {
if (!drawn_since_clear) {
mock_display_str = String(s);
drawn_since_clear = true;
}
},
getFontHeight: () => 8 * (global.g._font_size || 1)
};
global.Graphics = {};
// Mock Bangle.js-specific process.env properties
process.env.HWVERSION = 2; // Simulate Bangle.js 2
// Mock 'Bangle'
global.Bangle = {
setUI: (callbacks) => { mock_ui_callbacks = callbacks; },
http: () => new Promise(resolve => resolve({resp: "{}"})),
buzz: () => {},
};
// Mock 'load' (to exit app)
global.load = () => {};
// --- End Mock ---
// --- Load calculator app ---
// We inject a "spy" to capture the raw value passed to displayOutput,
// making our tests independent of UI formatting.
let calculatorCode = fs.readFileSync(path.join(__dirname, 'calculator.app.js'), 'utf8');
// 1. Rename the original function so we can call it.
calculatorCode = calculatorCode.replace(
'function displayOutput(num)',
'function _original_displayOutput(num)'
);
// 2. Prepend our spy that captures the value and then calls the original.
const spyCode = `
var displayOutput = function(num) {
global.last_display_num = num;
_original_displayOutput(num);
};
`;
const processedCode = spyCode + calculatorCode;
const wrappedCode = `(function(require) { ${processedCode}; return { buttonPress, scientificOperators }; })`;
const getAppFns = eval(wrappedCode);
const { buttonPress, scientificOperators } = getAppFns((name) => {
if (name === "FontDylex7x13") {
return { add: () => {} };
}
return require(name);
});
// --- End Load ---
// --- Test Framework ---
const test_suite = [];
let tests_passed = 0;
let tests_failed = 0;
function test(name, fn) {
test_suite.push({ name, fn });
}
function runTests() {
test_suite.forEach(t => {
// Reset state before each test
buttonPress('R'); // C
buttonPress('R'); // AC
if (global.last_display_num !== 0) {
console.error(`[FAIL] ${t.name} - Failed to reset state. Last number is: "${global.last_display_num}"`);
tests_failed++;
return;
}
try {
t.fn();
console.log(`[PASS] ${t.name}`);
tests_passed++;
} catch (e) {
console.error(`[FAIL] ${t.name}`);
console.error(e);
tests_failed++;
}
});
console.log(`\nTests finished. Passed: ${tests_passed}, Failed: ${tests_failed}.`);
if (tests_failed > 0) {
process.exit(1);
}
}
function press(buttons) {
for (const button of buttons) {
buttonPress(button);
}
}
function checkDisplay(expected, message) {
const actual = global.last_display_num;
if (typeof expected === 'number' && isNaN(expected)) {
assert.ok(typeof actual === 'number' && isNaN(actual), message || `Expected NaN, got ${actual}`);
return;
}
// Coerce to numbers for comparison to handle string vs number differences ('5' vs 5)
// and use a tolerance for floating point values.
const actualNum = parseFloat(actual);
const expectedNum = parseFloat(expected);
if (!isNaN(actualNum) && !isNaN(expectedNum)) {
const tolerance = 1e-9;
if (Math.abs(expectedNum - actualNum) < tolerance || actualNum === expectedNum) { // second part handles Infinity
return; // The numbers are close enough.
}
}
// Fallback to strict equality for non-numeric strings or when numeric check fails
assert.strictEqual(actual, expected, message);
}
// --- End Test Framework ---
// --- Test Cases ---
const basicNumbers = [
0, 1, -1, 2, -2, 5, -5, 10, -10, 0.1, -0.1, 0.2, -0.2, 0.5, -0.5,
123, -123, 1.23, -1.23,
1234567, -1234567, 99999999, -99999999,
];
const edgeCaseNumbers = [
-0,
Number.MAX_SAFE_INTEGER,
Number.MIN_SAFE_INTEGER,
Math.PI, Math.E,
0.000001, -0.000001, 0.00000001, -0.00000001,
1000000, -1000000, 100000000, -100000000,
0.1+0.2, // floating point fun
1/3, 2/3, 0.9999999, 1.0000001
];
// Generate some more numbers to reach the target count
const generatedNumbers = [];
for (let i = 1; i <= 20; i++) {
// integers
generatedNumbers.push(i * 123);
generatedNumbers.push(i * -123);
// floats
generatedNumbers.push(i / 10);
generatedNumbers.push(i / -10);
// small floats
generatedNumbers.push(i / 1000);
generatedNumbers.push(i / -1000);
}
const testNumbers = [...new Set([...basicNumbers, ...edgeCaseNumbers, ...generatedNumbers])];
// We need to filter out Infinity/-Infinity for pressNumber helper as they can't be typed.
// They can only appear as results, which we test for separately.
const finiteTestNumbers = testNumbers.filter(n => isFinite(n));
// Helper to press a number, handling negatives
function pressNumber(n) {
let s = String(n);
// If string representation is in scientific notation, convert to decimal string
if (s.includes('e')) {
s = n.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
}
if (s.startsWith('-')) {
press(s.substring(1));
buttonPress('N');
} else {
press(s);
}
}
// --- Generated Tests: Binary Operations ---
const binaryOps = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => a / b,
};
for (const op in binaryOps) {
for (const a of finiteTestNumbers) {
for (const b of finiteTestNumbers) {
if (op === '/' && b === 0) {
test(`Binary Edge Case: ${a} / 0`, () => {
pressNumber(a);
buttonPress('/');
pressNumber(0);
buttonPress('=');
checkDisplay(a === 0 ? NaN : (a > 0 ? Infinity : -Infinity));
});
continue;
}
test(`Binary: ${a} ${op} ${b}`, () => {
pressNumber(a);
buttonPress(op);
pressNumber(b);
buttonPress('=');
const expected = binaryOps[op](a, b);
checkDisplay(expected);
});
}
}
}
// --- Generated Tests: Operations with non-finite results ---
const smallFiniteTestNumbers = [1, -1, 5, -5, 0, 100, -100]; // for speed
// Test operations that result in Infinity, -Infinity, or NaN, and then continue with another operation
for (const a of smallFiniteTestNumbers) {
// a / 0 -> Infinity/-Infinity/NaN
test(`Non-finite chaining: (${a} / 0) + 5`, () => {
pressNumber(a);
press('/0=');
const intermediate = a/0;
checkDisplay(intermediate);
press('+5=');
checkDisplay(intermediate + 5);
});
test(`Non-finite chaining: (${a} / 0) * 5`, () => {
pressNumber(a);
press('/0=');
const intermediate = a/0;
checkDisplay(intermediate);
press('*5=');
checkDisplay(intermediate * 5);
});
test(`Non-finite chaining: (${a} / 0) * 0`, () => {
pressNumber(a);
press('/0=');
const intermediate = a/0;
checkDisplay(intermediate);
press('*0=');
checkDisplay(intermediate * 0); // Should be NaN
});
test(`Non-finite chaining: (${a} / 0) / 5`, () => {
pressNumber(a);
press('/0=');
const intermediate = a/0;
checkDisplay(intermediate);
press('/5=');
checkDisplay(intermediate / 5);
});
test(`Non-finite chaining: (${a} / 0) / 0`, () => {
pressNumber(a);
press('/0=');
const intermediate = a/0;
checkDisplay(intermediate);
press('/0=');
checkDisplay(intermediate / 0);
});
}
// 0/0 = NaN. Test operations on NaN
test(`Non-finite chaining: (0 / 0) + 5`, () => {
press('0/0=');
checkDisplay(NaN);
press('+5=');
checkDisplay(NaN + 5); // anything + NaN is NaN
});
test(`Non-finite chaining: (0 / 0) * 5`, () => {
press('0/0=');
checkDisplay(NaN);
press('*5=');
checkDisplay(NaN * 5);
});
test(`Non-finite chaining: (0 / 0) / 5`, () => {
press('0/0=');
checkDisplay(NaN);
press('/5=');
checkDisplay(NaN / 5);
});
test(`Non-finite chaining: sqrt(negative) then op`, () => {
press('9N'); // -9
press('r'); // sqrt
checkDisplay(NaN);
press('+1=');
checkDisplay(NaN);
});
// --- Generated Tests: Unary Operations ---
const unaryOps = {
'r': { name: 'sqrt', fn: Math.sqrt },
's': { name: 'x^2', fn: (a) => a * a },
'i': { name: '1/x', fn: (a) => 1 / a },
};
for (const op in unaryOps) {
for (const a of finiteTestNumbers) {
if (op === 'r' && a < 0) {
test(`Unary Edge Case: sqrt(${a})`, () => {
pressNumber(a);
buttonPress('r');
checkDisplay(NaN);
});
continue;
}
test(`Unary: ${unaryOps[op].name}(${a})`, () => {
pressNumber(a);
buttonPress(op);
const expected = unaryOps[op].fn(a);
checkDisplay(expected);
});
}
}
// --- Generated Tests: Chaining Operations ---
// To keep runtime sane, we'll pick a smaller subset for chaining
const chainNumbers = [0, 1, -1, 5, -5, 0.5, -0.5, 10, -10, 123, -123, 1.23];
const chainOps = ['+', '-', '*', '/'];
for (const a of chainNumbers) {
for (const b of chainNumbers) {
for (const c of chainNumbers) {
for (const op1 of chainOps) {
for (const op2 of chainOps) {
test(`Chaining: ${a} ${op1} ${b} ${op2} ${c}`, () => {
pressNumber(a);
buttonPress(op1);
pressNumber(b);
buttonPress(op2); // This will execute the first operation
pressNumber(c);
buttonPress('='); // This will execute the second operation
// Calculator does sequential evaluation: (a op1 b) op2 c
const expected = binaryOps[op2](binaryOps[op1](a, b), c);
checkDisplay(expected);
});
}
}
}
}
}
// --- Generated Tests: Trigonometry ---
const trigOps = {
'sin': { fn: Math.sin, name: 'sin' },
'cos': { fn: Math.cos, name: 'cos' },
'tan': { fn: Math.tan, name: 'tan' },
};
const trigAngles = [0, 15, 30, 45, 60, 75, 90, 180, 270, 360, -30, -45, -90, -180, 450, 720, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, Math.PI, 3*Math.PI/2, 2*Math.PI, -Math.PI/2, -Math.PI];
// Test in DEG mode (default)
for (const op in trigOps) {
for (const angle of trigAngles) {
test(`Trig (deg): ${trigOps[op].name}(${angle})`, () => {
pressNumber(angle);
buttonPress(op);
const expected = trigOps[op].fn(angle * Math.PI / 180);
checkDisplay(expected);
});
}
}
// Test in RAD mode
test('Switch to RAD mode', () => {
buttonPress('angleMode');
assert.strictEqual(scientificOperators.angleMode.val, 'rad');
});
for (const op in trigOps) {
for (const angle of trigAngles) {
test(`Trig (rad): ${trigOps[op].name}(${angle})`, () => {
pressNumber(angle);
buttonPress(op);
const expected = trigOps[op].fn(angle);
checkDisplay(expected);
});
}
}
test('Switch back to DEG mode for subsequent tests', () => {
buttonPress('angleMode');
assert.strictEqual(scientificOperators.angleMode.val, 'deg');
});
// --- Specific Functionality Tests ---
test('Clear and All Clear', () => {
press('123+');
checkDisplay('123');
press('R'); // Clear
checkDisplay('0');
press('456');
checkDisplay('456');
press('=');
checkDisplay('579'); // 123 + 456
press('R'); // Clear
checkDisplay('0');
buttonPress('R'); // All Clear
press('+1=');
checkDisplay('1');
});
test('Backspace', () => {
press('123.45B');
checkDisplay('123.4');
press('B');
checkDisplay('123.');
press('B');
checkDisplay('123');
press('B');
checkDisplay('12');
press('B');
checkDisplay('1');
press('B');
checkDisplay('0');
press('B');
checkDisplay('0');
});
test('Number input chaos: decimals and backspace', () => {
// Multiple decimals
press('1.2.3');
checkDisplay('1.23', 'Second decimal point should be ignored');
press('R'); press('R');
// Multiple leading decimals
press('..123');
checkDisplay('0.123', 'Multiple leading decimal points should result in 0.123');
press('R'); press('R');
// Backspace over decimal and re-add
press('1.23B');
checkDisplay('1.2', 'Backspace once');
press('B');
checkDisplay('1.', 'Backspace twice');
press('.');
checkDisplay('1.', 'Adding decimal again should have no effect');
press('B');
checkDisplay('1', 'Backspace a third time');
press('.');
checkDisplay('1.', 'Should be able to add decimal back');
press('45');
checkDisplay('1.45', 'Can add numbers after new decimal');
press('.');
checkDisplay('1.45', 'Another decimal should be ignored');
});
test('Number input chaos: leading zeros', () => {
press('01');
checkDisplay('1', 'Leading zero on integer should be replaced');
press('R'); press('R');
press('000123');
checkDisplay('123', 'Multiple leading zeros on integer should be replaced');
press('R'); press('R');
press('0.123');
checkDisplay('0.123', 'Single zero before decimal is kept');
press('R'); press('R');
press('00.123');
checkDisplay('0.123', 'Multiple zeros before decimal are collapsed to one');
press('R'); press('R');
press('123');
press('R'); // C
checkDisplay(0, 'Clear should result in 0');
press('007');
checkDisplay('7', 'Can input number with leading zeros after a clear');
});
test('Leading zeros are visually removed during input', () => {
press('0');
assert.strictEqual(global.last_display_num, '0', 'Input: 0');
press('0');
assert.strictEqual(global.last_display_num, '0', 'Second 0 should not create "00"');
press('7');
assert.strictEqual(global.last_display_num, '7', '0 should be replaced by 7');
press('R'); press('R');
press('0');
press('0');
press('.');
assert.strictEqual(global.last_display_num, '0.', 'Input: 00. -> "0."');
press('4');
assert.strictEqual(global.last_display_num, '0.4', 'Input: 00.4 -> "0.4"');
press('R'); press('R');
// After a calculation, starting a new number
press('1+2=');
checkDisplay(3);
press('0');
assert.strictEqual(global.last_display_num, '0', 'Post-calc: 0');
press('0');
assert.strictEqual(global.last_display_num, '0', 'Post-calc: 00 -> "0"');
press('5');
assert.strictEqual(global.last_display_num, '5', 'Post-calc: 005 -> "5"');
});
test('Repeated equals', () => {
press('2+3=');
checkDisplay('5');
press('=');
checkDisplay('8');
press('=');
checkDisplay('11');
press('R'); press('R');
press('10-2=');
checkDisplay('8');
press('=');
checkDisplay('6');
});
test('Operator change', () => {
press('10+-*/2=');
checkDisplay('5'); // 10/2=5
});
test('Long number input', () => {
press('1234567');
checkDisplay('1234567');
});
test('Input starting with decimal point', () => {
press('.5');
checkDisplay('0.5');
press('*2=');
checkDisplay('1');
});
test('Negative zero handling', () => {
press('0N'); // get -0
checkDisplay(-0);
press('*5');
checkDisplay('5'); // after typing 5, display should be 5
press('=');
checkDisplay(-0); // -0 * 5 = -0
buttonPress('R'); buttonPress('R');
press('5*0N='); // 5 * -0 = -0
checkDisplay(-0);
});
test('Pi button', () => {
press('p');
checkDisplay(Math.PI);
press('*2=');
checkDisplay(Math.PI * 2);
});
test('Pi replaces current number entry', () => {
press('123p');
checkDisplay(Math.PI);
});
test('Operation with Pi', () => {
press('5*p=');
checkDisplay(5 * Math.PI);
});
test('Pi as second operand', () => {
press('2+p=');
checkDisplay(2 + Math.PI);
});
test('Chaining operations with Pi', () => {
press('p*2+3=');
checkDisplay(Math.PI * 2 + 3);
});
test('Unary operations on Pi', () => {
press('p');
checkDisplay(Math.PI);
press('r'); // sqrt
checkDisplay(Math.sqrt(Math.PI));
press('R'); press('R'); // AC
press('p');
press('s'); // x^2
checkDisplay(Math.PI * Math.PI);
press('R'); press('R'); // AC
press('p');
press('i'); // 1/x
checkDisplay(1 / Math.PI);
});
test('Trig functions with Pi (rad)', () => {
buttonPress('angleMode'); // switch to RAD
assert.strictEqual(scientificOperators.angleMode.val, 'rad');
press('p');
buttonPress('sin');
checkDisplay(0); // Math.sin(Math.PI) is ~0, calculator snaps to 0
press('R'); press('R');
press('p');
buttonPress('cos');
checkDisplay(-1);
press('R'); press('R');
press('p');
buttonPress('tan');
checkDisplay(0); // Math.tan(Math.PI) is ~0, calculator snaps to 0
buttonPress('angleMode'); // switch back to DEG
assert.strictEqual(scientificOperators.angleMode.val, 'deg');
});
test('Pi after equals', () => {
press('1+2=');
checkDisplay(3);
press('p');
checkDisplay(Math.PI);
press('+1=');
checkDisplay(Math.PI + 1);
});
test('Pi with negative operator', () => {
press('pN');
checkDisplay(-Math.PI);
press('*2=');
checkDisplay(-Math.PI * 2);
});
test('Negative number times Pi', () => {
press('2N*p=');
checkDisplay(-2 * Math.PI);
});
// Run all the defined tests
runTests();