Skip to content

Instantly share code, notes, and snippets.

@rmkane
Created August 3, 2024 13:34
Show Gist options
  • Save rmkane/835835b315f47b10e4f127bd309cd046 to your computer and use it in GitHub Desktop.
Save rmkane/835835b315f47b10e4f127bd309cd046 to your computer and use it in GitHub Desktop.
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>Calculator</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/[email protected]/reset.min.css"
/>
<!-- Calculator Base Style -->
<style type="text/css">
:root {
--calc-bg: #ddd;
--calc-text: #111;
--calc-border: #ccc;
--calc-btn-bg: #eee;
--calc-btn-hover-bg: #fff;
--calc-btn-hover-text: var(--calc-text);
--calc-disp-bg: #fff;
}
.calculator {
display: flex;
flex-direction: column;
padding: 0.5rem;
background: var(--calc-bg);
color: var(--calc-text);
gap: 1rem;
border: thin solid var(--calc-border);
box-shadow: 0 0 0.5rem 0.25rem rgba(0, 0, 0, 0.5);
border-radius: 0.25rem;
}
.calculator-display {
display: flex;
flex-direction: column;
gap: 0.5rem;
border: thin solid var(--calc-border);
padding: 0.5rem;
background: var(--calc-disp-bg);
font-family: monospace;
border-radius: 0.25rem;
}
.calculator-status {
display: flex;
flex-direction: row;
justify-content: space-between;
min-height: 1rem;
user-select: none;
}
.calculator-memory {
display: flex;
font-size: 0.8rem;
}
.calculator-buffer {
display: flex;
font-size: 0.8rem;
}
.calculator-value {
min-height: 2rem;
font-size: 2rem;
text-align: right;
}
.calculator-button-grid {
display: grid;
grid-template-columns: repeat(5, auto);
gap: 0.5rem;
}
.calculator-button {
border: thin solid var(--calc-border);
background: var(--calc-btn-bg);
color: var(--calc-text);
min-width: 2rem;
min-height: 2rem;
font-family: Arial;
border-radius: 0.25rem;
}
.calculator-button:hover {
background: var(--calc-btn-hover-bg);
color: var(--calc-btn-hover-text);
cursor: pointer;
}
.calculator-button[data-name="OPERATOR_EQUALS"] {
grid-row: span 2;
}
.calculator-button[data-name="DIGIT_0"] {
grid-column: span 2;
}
</style>
<!-- Calculator Themes -->
<style>
.calculator[data-theme="light"] {
--calc-bg: #ddd;
--calc-text: #111;
--calc-border: #ccc;
--calc-btn-bg: #eee;
--calc-btn-hover-bg: #fff;
--calc-btn-hover-text: var(--calc-text);
--calc-disp-bg: #fff;
}
.calculator[data-theme="dark"] {
--calc-bg: #111;
--calc-text: #eee;
--calc-border: #333;
--calc-btn-bg: #222;
--calc-btn-hover-bg: #333;
--calc-disp-bg: #000;
}
.calculator[data-theme="contrast"] {
--calc-bg: #000;
--calc-text: #fff;
--calc-border: #fff;
--calc-btn-bg: #000;
--calc-btn-hover-bg: #fff;
--calc-btn-hover-text: #000;
--calc-disp-bg: #000;
}
.calculator[data-theme="red"] {
--calc-bg: #211;
--calc-text: #fee;
--calc-border: #433;
--calc-btn-bg: #322;
--calc-btn-hover-bg: #433;
--calc-disp-bg: #100;
}
.calculator[data-theme="green"] {
--calc-bg: #121;
--calc-text: #efe;
--calc-border: #343;
--calc-btn-bg: #232;
--calc-btn-hover-bg: #343;
--calc-disp-bg: #010;
}
.calculator[data-theme="blue"] {
--calc-bg: #112;
--calc-text: #eef;
--calc-border: #334;
--calc-btn-bg: #223;
--calc-btn-hover-bg: #334;
--calc-disp-bg: #001;
}
.calculator[data-theme="cyan"] {
--calc-bg: #122;
--calc-text: #eff;
--calc-border: #344;
--calc-btn-bg: #233;
--calc-btn-hover-bg: #344;
--calc-disp-bg: #011;
}
.calculator[data-theme="magenta"] {
--calc-bg: #212;
--calc-text: #fef;
--calc-border: #434;
--calc-btn-bg: #323;
--calc-btn-hover-bg: #434;
--calc-disp-bg: #101;
}
.calculator[data-theme="yellow"] {
--calc-bg: #221;
--calc-text: #ffe;
--calc-border: #443;
--calc-btn-bg: #332;
--calc-btn-hover-bg: #443;
--calc-disp-bg: #110;
}
</style>
<!-- Demo Style -->
<style type="text/css">
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: #222;
}
#calculators-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 2rem;
padding: 2rem;
}
</style>
<!-- Calculator -->
<script type="text/javascript">
// Math Functions Section
const BinaryFn = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) throw new Error("DIVISION_BY_ZERO");
return a / b;
},
percentage: (a, b) => (a / 100) * b,
};
const UnaryFn = {
negate: (n) => -n,
sqrt: (n) => {
if (n < 0) throw new Error("NEGATIVE_SQRT");
return Math.sqrt(n);
},
reciprocal: (n) => {
if (n === 0) throw new Error("DIVISION_BY_ZERO");
return 1 / n;
},
};
// Constants Section
const ACTION_TYPES = {
DIGIT: "digit",
FUNCTION: "function",
MEMORY: "memory",
OPERATOR: "operator",
};
const OPERATOR_TYPES = {
ADDITION: "OPERATOR_ADDITION",
DIVISION: "OPERATOR_DIVISION",
EQUALS: "OPERATOR_EQUALS",
MULTIPLICATION: "OPERATOR_MULTIPLICATION",
PERCENTAGE: "OPERATOR_PERCENTAGE",
SUBTRACTION: "OPERATOR_SUBTRACTION",
};
const FUNCTION_TYPES = {
BACKSPACE: "FUNCTION_BACKSPACE",
CLEAR: "FUNCTION_CLEAR",
CLEAR_ENTRY: "FUNCTION_CLEAR_ENTRY",
DECIMAL: "FUNCTION_DECIMAL",
NEGATE: "FUNCTION_NEGATE",
RECIPROCAL: "FUNCTION_RECIPROCAL",
SQUARE_ROOT: "FUNCTION_SQUARE_ROOT",
};
const MEMORY_TYPES = {
ADD: "MEMORY_ADD",
CLEAR: "MEMORY_CLEAR",
RECALL: "MEMORY_RECALL",
STORE: "MEMORY_STORE",
SUBTRACT: "MEMORY_SUBTRACT",
};
const DIGIT_TYPES = [
"DIGIT_0",
"DIGIT_1",
"DIGIT_2",
"DIGIT_3",
"DIGIT_4",
"DIGIT_5",
"DIGIT_6",
"DIGIT_7",
"DIGIT_8",
"DIGIT_9",
];
const BUTTON_TYPES = {
OPERATOR_ADDITION: OPERATOR_TYPES.ADDITION,
OPERATOR_DIVISION: OPERATOR_TYPES.DIVISION,
OPERATOR_EQUALS: OPERATOR_TYPES.EQUALS,
OPERATOR_MULTIPLICATION: OPERATOR_TYPES.MULTIPLICATION,
OPERATOR_PERCENTAGE: OPERATOR_TYPES.PERCENTAGE,
OPERATOR_SUBTRACTION: OPERATOR_TYPES.SUBTRACTION,
FUNCTION_BACKSPACE: FUNCTION_TYPES.BACKSPACE,
FUNCTION_CLEAR: FUNCTION_TYPES.CLEAR,
FUNCTION_CLEAR_ENTRY: FUNCTION_TYPES.CLEAR_ENTRY,
FUNCTION_DECIMAL: FUNCTION_TYPES.DECIMAL,
FUNCTION_NEGATE: FUNCTION_TYPES.NEGATE,
FUNCTION_RECIPROCAL: FUNCTION_TYPES.RECIPROCAL,
FUNCTION_SQUARE_ROOT: FUNCTION_TYPES.SQUARE_ROOT,
MEMORY_ADD: MEMORY_TYPES.ADD,
MEMORY_CLEAR: MEMORY_TYPES.CLEAR,
MEMORY_RECALL: MEMORY_TYPES.RECALL,
MEMORY_STORE: MEMORY_TYPES.STORE,
MEMORY_SUBTRACT: MEMORY_TYPES.SUBTRACT,
DIGIT_0: DIGIT_TYPES[0],
DIGIT_1: DIGIT_TYPES[1],
DIGIT_2: DIGIT_TYPES[2],
DIGIT_3: DIGIT_TYPES[3],
DIGIT_4: DIGIT_TYPES[4],
DIGIT_5: DIGIT_TYPES[5],
DIGIT_6: DIGIT_TYPES[6],
DIGIT_7: DIGIT_TYPES[7],
DIGIT_8: DIGIT_TYPES[8],
DIGIT_9: DIGIT_TYPES[9],
};
const Buttons = [
{ name: BUTTON_TYPES.DIGIT_0, label: "0", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_1, label: "1", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_2, label: "2", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_3, label: "3", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_4, label: "4", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_5, label: "5", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_6, label: "6", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_7, label: "7", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_8, label: "8", type: ACTION_TYPES.DIGIT },
{ name: BUTTON_TYPES.DIGIT_9, label: "9", type: ACTION_TYPES.DIGIT },
{
name: BUTTON_TYPES.FUNCTION_BACKSPACE,
label: "←",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.FUNCTION_CLEAR_ENTRY,
label: "CE",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.FUNCTION_CLEAR,
label: "C",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.FUNCTION_DECIMAL,
label: ".",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.FUNCTION_NEGATE,
label: "\xb1",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.FUNCTION_SQUARE_ROOT,
label: "√",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.FUNCTION_RECIPROCAL,
label: "1/x",
type: ACTION_TYPES.FUNCTION,
},
{
name: BUTTON_TYPES.MEMORY_CLEAR,
label: "MC",
type: ACTION_TYPES.MEMORY,
},
{
name: BUTTON_TYPES.MEMORY_RECALL,
label: "MR",
type: ACTION_TYPES.MEMORY,
},
{
name: BUTTON_TYPES.MEMORY_STORE,
label: "MS",
type: ACTION_TYPES.MEMORY,
},
{
name: BUTTON_TYPES.MEMORY_ADD,
label: "M+",
type: ACTION_TYPES.MEMORY,
},
{
name: BUTTON_TYPES.MEMORY_SUBTRACT,
label: "M-",
type: ACTION_TYPES.MEMORY,
},
{
name: BUTTON_TYPES.OPERATOR_ADDITION,
label: "+",
type: ACTION_TYPES.OPERATOR,
},
{
name: BUTTON_TYPES.OPERATOR_DIVISION,
label: "\xf7",
type: ACTION_TYPES.OPERATOR,
},
{
name: BUTTON_TYPES.OPERATOR_EQUALS,
label: "=",
type: ACTION_TYPES.OPERATOR,
},
{
name: BUTTON_TYPES.OPERATOR_MULTIPLICATION,
label: "\xd7",
type: ACTION_TYPES.OPERATOR,
},
{
name: BUTTON_TYPES.OPERATOR_PERCENTAGE,
label: "%",
type: ACTION_TYPES.OPERATOR,
},
{
name: BUTTON_TYPES.OPERATOR_SUBTRACTION,
label: "-",
type: ACTION_TYPES.OPERATOR,
},
];
const MathSymbols = {
ADDITION: "+",
DIVISION: "÷",
EQUALS: "=",
MULTIPLICATION: "×",
NEGATE: "±",
PERCENTAGE: "%",
SQUARE_ROOT: "√",
SUBTRACTION: "-",
};
const SupportedThemes = [
"light",
"dark",
"contrast",
"red",
"green",
"blue",
"cyan",
"magenta",
"yellow",
];
const errorMessages = {
DIVISION_BY_ZERO: "DIV BY 0",
NEGATIVE_SQRT: "NEG SQRT",
};
// Order of buttons in the grid
const buttonOrder = [
BUTTON_TYPES.MEMORY_CLEAR,
BUTTON_TYPES.MEMORY_RECALL,
BUTTON_TYPES.MEMORY_STORE,
BUTTON_TYPES.MEMORY_ADD,
BUTTON_TYPES.MEMORY_SUBTRACT,
BUTTON_TYPES.FUNCTION_BACKSPACE,
BUTTON_TYPES.FUNCTION_CLEAR_ENTRY,
BUTTON_TYPES.FUNCTION_CLEAR,
BUTTON_TYPES.FUNCTION_NEGATE,
BUTTON_TYPES.FUNCTION_SQUARE_ROOT,
BUTTON_TYPES.DIGIT_7,
BUTTON_TYPES.DIGIT_8,
BUTTON_TYPES.DIGIT_9,
BUTTON_TYPES.OPERATOR_DIVISION,
BUTTON_TYPES.OPERATOR_PERCENTAGE,
BUTTON_TYPES.DIGIT_4,
BUTTON_TYPES.DIGIT_5,
BUTTON_TYPES.DIGIT_6,
BUTTON_TYPES.OPERATOR_MULTIPLICATION,
BUTTON_TYPES.FUNCTION_RECIPROCAL,
BUTTON_TYPES.DIGIT_1,
BUTTON_TYPES.DIGIT_2,
BUTTON_TYPES.DIGIT_3,
BUTTON_TYPES.OPERATOR_SUBTRACTION,
BUTTON_TYPES.OPERATOR_EQUALS,
BUTTON_TYPES.DIGIT_0,
BUTTON_TYPES.FUNCTION_DECIMAL,
BUTTON_TYPES.OPERATOR_ADDITION,
];
// Map button order to buttons
const buttons = buttonOrder.map((name) =>
Buttons.find((button) => button.name === name)
);
const operations = {
[OPERATOR_TYPES.ADDITION]: {
symbol: MathSymbols.ADDITION,
perform: BinaryFn.add,
},
[OPERATOR_TYPES.SUBTRACTION]: {
symbol: MathSymbols.SUBTRACTION,
perform: BinaryFn.subtract,
},
[OPERATOR_TYPES.MULTIPLICATION]: {
symbol: MathSymbols.MULTIPLICATION,
perform: BinaryFn.multiply,
},
[OPERATOR_TYPES.DIVISION]: {
symbol: MathSymbols.DIVISION,
perform: BinaryFn.divide,
},
[OPERATOR_TYPES.PERCENTAGE]: {
symbol: MathSymbols.PERCENTAGE,
perform: BinaryFn.percentage,
},
[OPERATOR_TYPES.EQUALS]: {
symbol: MathSymbols.EQUALS,
},
};
const customFunctions = {
[FUNCTION_TYPES.CLEAR]: clear,
[FUNCTION_TYPES.CLEAR_ENTRY]: clearEntry,
[FUNCTION_TYPES.BACKSPACE]: backspace,
[FUNCTION_TYPES.NEGATE]: negate,
[FUNCTION_TYPES.SQUARE_ROOT]: squareRoot,
[FUNCTION_TYPES.RECIPROCAL]: reciprocal,
[FUNCTION_TYPES.DECIMAL]: addDecimal,
};
const memoryFunctions = {
[MEMORY_TYPES.CLEAR]: memoryClear,
[MEMORY_TYPES.RECALL]: memoryRecall,
[MEMORY_TYPES.STORE]: memoryStore,
[MEMORY_TYPES.ADD]: memoryAdd,
[MEMORY_TYPES.SUBTRACT]: memorySubtract,
};
const keyMapping = {
// Digits
0: DIGIT_TYPES[0],
1: DIGIT_TYPES[1],
2: DIGIT_TYPES[2],
3: DIGIT_TYPES[3],
4: DIGIT_TYPES[4],
5: DIGIT_TYPES[5],
6: DIGIT_TYPES[6],
7: DIGIT_TYPES[7],
8: DIGIT_TYPES[8],
9: DIGIT_TYPES[9],
// Operators
"+": OPERATOR_TYPES.ADDITION,
"-": OPERATOR_TYPES.SUBTRACTION,
"*": OPERATOR_TYPES.MULTIPLICATION,
"/": OPERATOR_TYPES.DIVISION,
"%": OPERATOR_TYPES.PERCENTAGE,
Enter: OPERATOR_TYPES.EQUALS,
"=": OPERATOR_TYPES.EQUALS,
// Functions
Backspace: FUNCTION_TYPES.BACKSPACE,
c: FUNCTION_TYPES.CLEAR_ENTRY,
C: FUNCTION_TYPES.CLEAR,
Escape: FUNCTION_TYPES.CLEAR,
".": FUNCTION_TYPES.DECIMAL,
};
const initialState = {
currentValue: 0,
previousValue: null,
operator: null,
waitingForSecondOperand: false,
memory: 0,
error: null,
floatMode: false,
};
// Utility Functions Section
function hasOperator(state) {
return state.operator && state.waitingForSecondOperand;
}
function getButtonType(buttonName) {
if (buttonName.startsWith("DIGIT")) return ACTION_TYPES.DIGIT;
if (buttonName.startsWith("OPERATOR")) return ACTION_TYPES.OPERATOR;
if (buttonName.startsWith("FUNCTION")) return ACTION_TYPES.FUNCTION;
if (buttonName.startsWith("MEMORY")) return ACTION_TYPES.MEMORY;
}
function getDigit(name) {
return name.replace(/^DIGIT_/, "");
}
function getOperatorSymbol(name) {
return operations[name]?.symbol ?? "";
}
function applyStateTransformation(state, lookup, name) {
return lookup[name]?.(state) ?? state;
}
function formatValue(value) {
if (value === "0.") return value;
const n = parseFloat(value);
return Number.isInteger(n) ? n.toString() : formatFloat(n);
}
function formatFloat(value) {
return parseFloat(value)
.toFixed(4)
.replace(/\.?0+$/, "");
}
// Core Functions Section
function handleDigit(name, state) {
if (state.error) return state; // Prevent actions if there is an error
const digit = getDigit(name);
if (state.waitingForSecondOperand) {
return {
...state,
currentValue: parseFloat(digit),
floatMode: false,
waitingForSecondOperand: false,
};
}
let newValue;
if (state.floatMode) {
newValue = state.currentValue.toString() + digit;
} else {
newValue =
state.currentValue === 0
? digit
: state.currentValue.toString() + digit;
}
return { ...state, currentValue: parseFloat(newValue) };
}
function handleOperator(name, state) {
if (state.error) return state; // Prevent actions if there is an error
if (state.currentValue === null || isNaN(state.currentValue))
return state;
if (name === OPERATOR_TYPES.EQUALS && state.previousValue == null)
return state;
const inputValue = state.currentValue;
if (hasOperator(state)) {
return { ...state, operator: name };
}
if (state.previousValue == null) {
return {
...state,
previousValue: inputValue,
operator: name,
waitingForSecondOperand: true,
};
}
if (state.operator) {
try {
const result = operations[state.operator].perform(
state.previousValue,
inputValue
);
return {
...state,
currentValue: result,
previousValue: result,
operator: name,
waitingForSecondOperand: true,
};
} catch (error) {
return {
...state,
error: error.message,
};
}
}
return state;
}
function handleFunction(name, state) {
if (state.error && name !== FUNCTION_TYPES.CLEAR) return state; // Only allow clearing the error
return applyStateTransformation(state, customFunctions, name);
}
function handleMemory(name, state) {
if (state.error) return state; // Prevent actions if there is an error
return applyStateTransformation(state, memoryFunctions, name);
}
function performAction(action, ref, state) {
const newState = handleType(action.type, action.name, state);
Object.assign(state, newState);
render(ref, state);
}
function handleType(type, name, state) {
switch (type) {
case ACTION_TYPES.DIGIT:
return handleDigit(name, state);
case ACTION_TYPES.OPERATOR:
return handleOperator(name, state);
case ACTION_TYPES.FUNCTION:
return handleFunction(name, state);
case ACTION_TYPES.MEMORY:
return handleMemory(name, state);
}
}
// Event Listeners Section
function addListeners(ref, state) {
ref.addEventListener("click", (event) =>
handleClick(event, ref, state)
);
ref.addEventListener("focus", () => {
activeCalculator = { ref, state };
});
ref.addEventListener("blur", () => {
activeCalculator = null;
});
document.addEventListener("keydown", handleKeyDown);
}
function handleClick(event, ref, state) {
// Focus on calculator
if (!activeCalculator || activeCalculator.ref !== ref) {
activeCalculator = { ref, state };
}
// Handle the button logic
const button = event.target;
if (button.classList.contains("calculator-button")) {
const action = {
name: button.dataset.name,
type: getButtonType(button.dataset.name),
};
performAction(action, ref, state);
}
}
function handleKeyDown(event) {
if (!activeCalculator) return;
const buttonName = keyMapping[event.key];
if (!buttonName) return;
const action = {
name: buttonName,
type: getButtonType(buttonName),
};
performAction(action, activeCalculator.ref, activeCalculator.state);
event.preventDefault();
}
// Initialization Section
function addButtons(ref) {
const gridEl = ref.querySelector(".calculator-button-grid");
buttons.forEach(({ label, name, type }) => {
gridEl.insertAdjacentHTML(
"beforeend",
`
<button
type="button"
class="calculator-button"
title="${name.toLowerCase().replace(/_/g, " ")}"
data-name="${name}"
data-type="${type}"
>
${label}
</button>
`
);
});
}
function initCalculator(ref, state) {
addButtons(ref);
addListeners(ref, state);
render(ref, state);
}
function CalculatorApp(elementOrSelector) {
if (!isElementOrSelector(elementOrSelector)) {
throw new Error(
`${typeof elementOrSelector} must be an element or selector`
);
}
const ref = isSelector(elementOrSelector)
? document.querySelector(elementOrSelector)
: elementOrSelector;
let state = structuredClone(initialState);
ref.classList.add("calculator");
ref.innerHTML = `
<div class="calculator-display">
<div class="calculator-status">
<div class="calculator-memory"></div>
<div class="calculator-buffer"></div>
</div>
<div class="calculator-value">0</div>
</div>
<div class="calculator-button-grid"></div>
`;
ref.tabIndex = 0;
initCalculator(ref, state);
}
let activeCalculator;
// Rendering Section
function render(ref, state) {
const displayEl = ref.querySelector(".calculator-display");
const bufferEl = displayEl.querySelector(".calculator-buffer");
const valueEl = displayEl.querySelector(".calculator-value");
const memoryEl = displayEl.querySelector(".calculator-memory");
bufferEl.textContent = renderBufferDisplay(state);
valueEl.textContent = renderCurrentDisplay(state);
memoryEl.textContent = renderMemoryDisplay(state);
}
function renderBufferDisplay(state) {
if (state.error) return "";
if (state.operator === OPERATOR_TYPES.EQUALS) return "";
const operator = getOperatorSymbol(state.operator);
const previousValue =
state.previousValue !== null ? formatValue(state.previousValue) : "";
return [previousValue, operator].filter(Boolean).join(" ");
}
function renderCurrentDisplay(state) {
if (state.error) return errorMessages[state.error] || "Error";
// Convert the current value to a string and ensure it displays correctly
let displayValue = state.currentValue.toString();
// If in float mode and there's no decimal point, add a trailing dot
if (state.floatMode && !displayValue.includes(".")) {
displayValue += ".";
}
return formatValue(displayValue);
}
function renderMemoryDisplay(state) {
if (state.memory === null || state.memory === 0) return "";
return `M = ${formatValue(state.memory)}`;
}
// Operations and Functions Section
function clear(state) {
return {
...state,
currentValue: 0,
previousValue: null,
operator: null,
waitingForSecondOperand: false,
error: null,
floatMode: false,
};
}
function applyMath(state, fn) {
try {
const result = fn(parseFloat(state.currentValue));
return {
...state,
currentValue: result,
floatMode: !Number.isInteger(result),
};
} catch (error) {
return { ...state, error: error.message, currentValue: 0 };
}
}
function clearEntry(state) {
return { ...state, currentValue: 0, error: null, floatMode: false };
}
function backspace(state) {
if (state.error) return state; // Prevent actions if there is an error
let currentValueStr = state.currentValue.toString();
if (currentValueStr.length === 1 || currentValueStr === "-0") {
return { ...state, currentValue: 0, floatMode: false };
}
const newValueStr = currentValueStr.slice(0, -1);
const newFloatMode = newValueStr.includes(".");
const newValue = newValueStr === "-" ? 0 : parseFloat(newValueStr) || 0;
return {
...state,
currentValue: newValue,
floatMode: newFloatMode,
};
}
function negate(state) {
if (isEmpty(state.currentValue)) return state;
return applyMath(state, UnaryFn.negate);
}
function squareRoot(state) {
if (isEmpty(state.currentValue)) return state;
return applyMath(state, UnaryFn.sqrt);
}
function reciprocal(state) {
if (isEmpty(state.currentValue)) return state;
return applyMath(state, UnaryFn.reciprocal);
}
function addDecimal(state) {
if (state.error) return state; // Prevent actions if there is an error
// If the user was waiting for the second operand, reset the current value to '0.'
if (state.waitingForSecondOperand) {
return {
...state,
currentValue: "0.",
floatMode: true,
waitingForSecondOperand: false,
};
}
// If the current value already contains a decimal point, return the current state
if (state.floatMode) {
return state;
}
// Append the decimal point to the current value
return {
...state,
currentValue: state.currentValue.toString() + ".",
floatMode: true,
};
}
function memoryClear(state) {
return { ...state, memory: 0, error: null };
}
function memoryRecall(state) {
if (state.memory === null) return state;
return {
...state,
currentValue: state.memory,
waitingForSecondOperand: false,
error: null,
};
}
function memoryStore(state) {
return { ...state, memory: state.currentValue, error: null };
}
function memoryAdd(state) {
const newValue =
parseFloat(state.memory) + parseFloat(state.currentValue);
return { ...state, memory: newValue, error: null };
}
function memorySubtract(state) {
const newValue =
parseFloat(state.memory) - parseFloat(state.currentValue);
return { ...state, memory: newValue, error: null };
}
// General functions
function isEmpty(v) {
return v === "";
}
function isElement(v) {
return v instanceof HTMLElement;
}
function isSelector(v) {
return typeof v === "string";
}
function isElementOrSelector(v) {
return isElement(v) || isSelector(v);
}
</script>
</head>
<body>
<div id="calculators-container"></div>
<!-- Demo -->
<script>
document.addEventListener("DOMContentLoaded", loadDemo);
function loadDemo() {
const containerEl = document.getElementById("calculators-container");
SupportedThemes.forEach(
(theme) => new CalculatorApp(spawn(theme, containerEl))
);
}
function spawn(theme, containerEl) {
const el = document.createElement("div");
el.id = `calculator-${theme}`;
el.dataset.theme = theme;
containerEl.appendChild(el);
return el;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment