435 lines
12 KiB
TypeScript
435 lines
12 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import type { KeyboardEvent } from "react";
|
|||
|
|
import { useState } from "react";
|
|||
|
|
import styles from "./CalculatorWidget.module.css";
|
|||
|
|
|
|||
|
|
type Operator = "add" | "subtract" | "multiply" | "divide";
|
|||
|
|
|
|||
|
|
type CalculatorButtonProps = {
|
|||
|
|
label: string;
|
|||
|
|
ariaLabel?: string;
|
|||
|
|
className?: string;
|
|||
|
|
onClick: () => void;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const maxDisplayLength = 16;
|
|||
|
|
|
|||
|
|
function parseDisplay(value: string): number {
|
|||
|
|
const normalizedValue = value.replace(",", ".");
|
|||
|
|
const numberValue = Number(normalizedValue);
|
|||
|
|
|
|||
|
|
if (!Number.isFinite(numberValue)) {
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return numberValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatNumber(value: number): string {
|
|||
|
|
if (!Number.isFinite(value)) {
|
|||
|
|
return "Fehler";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (Object.is(value, -0)) {
|
|||
|
|
return "0";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const absoluteValue = Math.abs(value);
|
|||
|
|
|
|||
|
|
if (absoluteValue !== 0 && (absoluteValue >= 1e15 || absoluteValue < 1e-9)) {
|
|||
|
|
return value.toExponential(8).replace(".", ",");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const roundedValue = Math.round(value * 1e10) / 1e10;
|
|||
|
|
|
|||
|
|
return roundedValue.toLocaleString("de-DE", {
|
|||
|
|
useGrouping: false,
|
|||
|
|
maximumFractionDigits: 10
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function calculate(firstValue: number, secondValue: number, operator: Operator): number {
|
|||
|
|
if (operator === "add") {
|
|||
|
|
return firstValue + secondValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operator === "subtract") {
|
|||
|
|
return firstValue - secondValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operator === "multiply") {
|
|||
|
|
return firstValue * secondValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operator === "divide") {
|
|||
|
|
if (secondValue === 0) {
|
|||
|
|
return Number.NaN;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return firstValue / secondValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return secondValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getOperatorLabel(operator: Operator | null): string {
|
|||
|
|
if (operator === "add") {
|
|||
|
|
return "+";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operator === "subtract") {
|
|||
|
|
return "−";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operator === "multiply") {
|
|||
|
|
return "×";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operator === "divide") {
|
|||
|
|
return "÷";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function CalculatorButton({ label, ariaLabel, className, onClick }: CalculatorButtonProps) {
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className={[styles.button, className].filter(Boolean).join(" ")}
|
|||
|
|
aria-label={ariaLabel ?? label}
|
|||
|
|
onClick={onClick}
|
|||
|
|
>
|
|||
|
|
{label}
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function CalculatorWidget() {
|
|||
|
|
const [display, setDisplay] = useState("0");
|
|||
|
|
const [storedValue, setStoredValue] = useState<number | null>(null);
|
|||
|
|
const [pendingOperator, setPendingOperator] = useState<Operator | null>(null);
|
|||
|
|
const [waitingForOperand, setWaitingForOperand] = useState(false);
|
|||
|
|
const [memory, setMemory] = useState(0);
|
|||
|
|
|
|||
|
|
const displayIsError = display === "Fehler";
|
|||
|
|
|
|||
|
|
function resetIfError(): boolean {
|
|||
|
|
if (!displayIsError) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDisplay("0");
|
|||
|
|
setStoredValue(null);
|
|||
|
|
setPendingOperator(null);
|
|||
|
|
setWaitingForOperand(false);
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function inputDigit(digit: string) {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
setDisplay(digit);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (waitingForOperand) {
|
|||
|
|
setDisplay(digit);
|
|||
|
|
setWaitingForOperand(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDisplay((currentDisplay) => {
|
|||
|
|
if (currentDisplay === "0") {
|
|||
|
|
return digit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (currentDisplay.replace("-", "").replace(",", "").length >= maxDisplayLength) {
|
|||
|
|
return currentDisplay;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `${currentDisplay}${digit}`;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function inputDecimal() {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
setDisplay("0,");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (waitingForOperand) {
|
|||
|
|
setDisplay("0,");
|
|||
|
|
setWaitingForOperand(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDisplay((currentDisplay) => {
|
|||
|
|
if (currentDisplay.includes(",")) {
|
|||
|
|
return currentDisplay;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `${currentDisplay},`;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearEntry() {
|
|||
|
|
setDisplay("0");
|
|||
|
|
setWaitingForOperand(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearAll() {
|
|||
|
|
setDisplay("0");
|
|||
|
|
setStoredValue(null);
|
|||
|
|
setPendingOperator(null);
|
|||
|
|
setWaitingForOperand(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function backspace() {
|
|||
|
|
if (resetIfError() || waitingForOperand) {
|
|||
|
|
setDisplay("0");
|
|||
|
|
setWaitingForOperand(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDisplay((currentDisplay) => {
|
|||
|
|
if (currentDisplay.length <= 1 || (currentDisplay.length === 2 && currentDisplay.startsWith("-"))) {
|
|||
|
|
return "0";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return currentDisplay.slice(0, -1);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleSign() {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDisplay((currentDisplay) => {
|
|||
|
|
if (currentDisplay === "0") {
|
|||
|
|
return currentDisplay;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return currentDisplay.startsWith("-") ? currentDisplay.slice(1) : `-${currentDisplay}`;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyUnary(operation: "percent" | "reciprocal" | "square" | "sqrt") {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const currentValue = parseDisplay(display);
|
|||
|
|
let nextValue = currentValue;
|
|||
|
|
|
|||
|
|
if (operation === "percent") {
|
|||
|
|
if (storedValue !== null && pendingOperator) {
|
|||
|
|
nextValue = (storedValue * currentValue) / 100;
|
|||
|
|
} else {
|
|||
|
|
nextValue = currentValue / 100;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operation === "reciprocal") {
|
|||
|
|
nextValue = currentValue === 0 ? Number.NaN : 1 / currentValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operation === "square") {
|
|||
|
|
nextValue = currentValue * currentValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (operation === "sqrt") {
|
|||
|
|
nextValue = currentValue < 0 ? Number.NaN : Math.sqrt(currentValue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDisplay(formatNumber(nextValue));
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function chooseOperator(operator: Operator) {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const currentValue = parseDisplay(display);
|
|||
|
|
|
|||
|
|
if (storedValue === null) {
|
|||
|
|
setStoredValue(currentValue);
|
|||
|
|
} else if (pendingOperator && !waitingForOperand) {
|
|||
|
|
const result = calculate(storedValue, currentValue, pendingOperator);
|
|||
|
|
setDisplay(formatNumber(result));
|
|||
|
|
setStoredValue(result);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setPendingOperator(operator);
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyEquals() {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (storedValue === null || pendingOperator === null) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const currentValue = parseDisplay(display);
|
|||
|
|
const result = calculate(storedValue, currentValue, pendingOperator);
|
|||
|
|
|
|||
|
|
setDisplay(formatNumber(result));
|
|||
|
|
setStoredValue(null);
|
|||
|
|
setPendingOperator(null);
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function memoryClear() {
|
|||
|
|
setMemory(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function memoryRecall() {
|
|||
|
|
setDisplay(formatNumber(memory));
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function memoryAdd() {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setMemory((currentMemory) => currentMemory + parseDisplay(display));
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function memorySubtract() {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setMemory((currentMemory) => currentMemory - parseDisplay(display));
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function memoryStore() {
|
|||
|
|
if (resetIfError()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setMemory(parseDisplay(display));
|
|||
|
|
setWaitingForOperand(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleKeyboard(event: KeyboardEvent<HTMLDivElement>) {
|
|||
|
|
const key = event.key;
|
|||
|
|
const code = event.code;
|
|||
|
|
let handled = true;
|
|||
|
|
|
|||
|
|
if (/^[0-9]$/.test(key)) {
|
|||
|
|
inputDigit(key);
|
|||
|
|
} else if (key === "," || key === "." || code === "NumpadDecimal") {
|
|||
|
|
inputDecimal();
|
|||
|
|
} else if (key === "+" || code === "NumpadAdd") {
|
|||
|
|
chooseOperator("add");
|
|||
|
|
} else if (key === "-" || code === "NumpadSubtract") {
|
|||
|
|
chooseOperator("subtract");
|
|||
|
|
} else if (key === "*" || code === "NumpadMultiply") {
|
|||
|
|
chooseOperator("multiply");
|
|||
|
|
} else if (key === "/" || code === "NumpadDivide") {
|
|||
|
|
chooseOperator("divide");
|
|||
|
|
} else if (key === "Enter" || key === "=" || code === "NumpadEnter") {
|
|||
|
|
applyEquals();
|
|||
|
|
} else if (key === "Backspace") {
|
|||
|
|
backspace();
|
|||
|
|
} else if (key === "Escape") {
|
|||
|
|
clearAll();
|
|||
|
|
} else if (key === "Delete") {
|
|||
|
|
clearEntry();
|
|||
|
|
} else if (key === "%") {
|
|||
|
|
applyUnary("percent");
|
|||
|
|
} else if (key === "F9") {
|
|||
|
|
toggleSign();
|
|||
|
|
} else {
|
|||
|
|
handled = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (handled) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
className={`${styles.calculator} widgetNoDrag`}
|
|||
|
|
tabIndex={0}
|
|||
|
|
role="application"
|
|||
|
|
aria-label="Taschenrechner"
|
|||
|
|
onKeyDown={handleKeyboard}
|
|||
|
|
>
|
|||
|
|
<output className={styles.display} aria-label="Anzeige">
|
|||
|
|
{display}
|
|||
|
|
</output>
|
|||
|
|
|
|||
|
|
<div className={styles.memoryRow} aria-label="Speicherfunktionen">
|
|||
|
|
<button type="button" className={styles.memoryButton} onClick={memoryClear} disabled={memory === 0}>
|
|||
|
|
MC
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className={styles.memoryButton} onClick={memoryRecall} disabled={memory === 0}>
|
|||
|
|
MR
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className={styles.memoryButton} onClick={memoryAdd}>
|
|||
|
|
M+
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className={styles.memoryButton} onClick={memorySubtract}>
|
|||
|
|
M−
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className={styles.memoryButton} onClick={memoryStore}>
|
|||
|
|
MS
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className={styles.memoryButton} onClick={memoryRecall} disabled={memory === 0}>
|
|||
|
|
M
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className={styles.keypad}>
|
|||
|
|
<CalculatorButton label="%" className={styles.utilityButton} onClick={() => applyUnary("percent")} />
|
|||
|
|
<CalculatorButton label="CE" className={styles.utilityButton} onClick={clearEntry} />
|
|||
|
|
<CalculatorButton label="C" className={styles.utilityButton} onClick={clearAll} />
|
|||
|
|
<CalculatorButton label="⌫" ariaLabel="Rückschritt" className={styles.utilityButton} onClick={backspace} />
|
|||
|
|
|
|||
|
|
<CalculatorButton label="1/x" className={styles.utilityButton} onClick={() => applyUnary("reciprocal")} />
|
|||
|
|
<CalculatorButton label="x²" className={styles.utilityButton} onClick={() => applyUnary("square")} />
|
|||
|
|
<CalculatorButton label="²√x" className={styles.utilityButton} onClick={() => applyUnary("sqrt")} />
|
|||
|
|
<CalculatorButton label="÷" className={styles.operatorButton} onClick={() => chooseOperator("divide")} />
|
|||
|
|
|
|||
|
|
<CalculatorButton label="7" className={styles.numberButton} onClick={() => inputDigit("7")} />
|
|||
|
|
<CalculatorButton label="8" className={styles.numberButton} onClick={() => inputDigit("8")} />
|
|||
|
|
<CalculatorButton label="9" className={styles.numberButton} onClick={() => inputDigit("9")} />
|
|||
|
|
<CalculatorButton label="×" className={styles.operatorButton} onClick={() => chooseOperator("multiply")} />
|
|||
|
|
|
|||
|
|
<CalculatorButton label="4" className={styles.numberButton} onClick={() => inputDigit("4")} />
|
|||
|
|
<CalculatorButton label="5" className={styles.numberButton} onClick={() => inputDigit("5")} />
|
|||
|
|
<CalculatorButton label="6" className={styles.numberButton} onClick={() => inputDigit("6")} />
|
|||
|
|
<CalculatorButton label="−" className={styles.operatorButton} onClick={() => chooseOperator("subtract")} />
|
|||
|
|
|
|||
|
|
<CalculatorButton label="1" className={styles.numberButton} onClick={() => inputDigit("1")} />
|
|||
|
|
<CalculatorButton label="2" className={styles.numberButton} onClick={() => inputDigit("2")} />
|
|||
|
|
<CalculatorButton label="3" className={styles.numberButton} onClick={() => inputDigit("3")} />
|
|||
|
|
<CalculatorButton label="+" className={styles.operatorButton} onClick={() => chooseOperator("add")} />
|
|||
|
|
|
|||
|
|
<CalculatorButton label="+/−" className={styles.numberButton} onClick={toggleSign} />
|
|||
|
|
<CalculatorButton label="0" className={styles.numberButton} onClick={() => inputDigit("0")} />
|
|||
|
|
<CalculatorButton label="," className={styles.numberButton} onClick={inputDecimal} />
|
|||
|
|
<CalculatorButton
|
|||
|
|
label="="
|
|||
|
|
ariaLabel={`Gleich ${getOperatorLabel(pendingOperator)}`}
|
|||
|
|
className={styles.equalsButton}
|
|||
|
|
onClick={applyEquals}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|