Skip to content

Difficult edge cases

Schwieriger wird die Sache, wenn Folgeberechnungen gemacht werden. Schauen wir uns die Sache an:

Wenn wir 9 - 5 - in einen Taschenrechner eintippen, rechnet er und zeigt als Ergebnis 4 an. Warum? Die Antwort ist einfach, hier wird angenommen, dass der Benutzer die Berechnung fortführen möchte. Der Rechner zeigt einfach nur ein Zwischenergebnis an. Unser Rechner macht – gar nichts. Machen wir sicherheitshalber noch einen Test (der fehlschlagen wird):

const tests = [
// ...
{
message: 'Operator Calculation',
keys: ['9', 'minus', '5', 'minus'],
result: '4',
},
];

Wir können nun einfach den Code aus dem Istgleich-Abschnitt in den Operator-Abschnitt kopieren:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'operator') {
button.classList.add('is-pressed');
const firstValue = parseFloat(calculator.dataset.firstValue);
const operator = calculator.dataset.operator;
const secondValue = parseFloat(result);
// Makes a calculation
// skips the calculation if there's no 'firstValue' and 'operator'
if (firstValue && operator) {
let newResult;
if (operator === 'plus') newResult = firstValue + secondValue;
if (operator === 'minus') newResult = firstValue - secondValue;
if (operator === 'times') newResult = firstValue * secondValue;
if (operator === 'divide') newResult = firstValue / secondValue;
display.textContent = newResult;
}
calculator.dataset.firstValue = result;
calculator.dataset.operator = button.dataset.key;
}
});

Der Test läuft durch, allerdings funktioniert jetzt die Kombination Ziffer -> Operator -> Operator nicht mehr (die wir noch nicht getestet haben)!

Wenn Tim einen Operator nach einem Operator drückt, können wir annehmen, dass er zuerst den falschen Operator gedrückt hat. Wir wollen in diesem Fall also nicht die erste Rechenoperation ausführen, sondern nur die zweite. Schreiben wir kurz für diesen Fall auch einen Test:

const tests = [
// ...
{
message: 'Number Operator Operator',
keys: ['9', 'times', 'divide'],
result: '9',
},
]

Die Reparatur ist einfach: wenn der vorhergehende Button ein Operator war, dann wird die Kalkulation gestrichen.

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'operator') {
// ...
if (previousButtonType === 'operator') {
// do nothing
} else {
const firstValue = parseFloat(calculator.dataset.firstValue);
// ...
display.textContent = newResult;
}
}
// ...
});

Allerdings sieht der do nothing Kommentar in der if-Abfrage komisch aus. Wir machen den Code etwas schöner:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'operator') {
// ...
// Reverses the if condition
if (previousButtonType !== 'operator') {
const firstValue = parseFloat(calculator.dataset.firstValue);
const operator = calculator.dataset.operator;
const secondValue = parseFloat(result);
// makes a calculation
if (firstValue && operator) {
let newResult;
if (operator === 'plus') newResult = firstValue + secondValue;
if (operator === 'minus') newResult = firstValue - secondValue;
if (operator === 'times') newResult = firstValue * secondValue;
if (operator === 'divide') newResult = firstValue / secondValue;
display.textContent = newResult;
}
}
// ...
}
// ...
});

Es geht aber noch besser! Da wir die Berechnung nur dann ausführen, wenn beide Bedingungen erfüllt sind, also der vorherige Button kein Operator war und wenn es ein firstValue und einen operator gibt, können wir diese in einer Bedingung zusammenfassen:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'operator') {
// ...
const firstValue = parseFloat(calculator.dataset.firstValue);
const operator = calculator.dataset.operator;
const secondValue = parseFloat(result);
// makes a calculation and combines all conditions into one
if (previousButtonType !== 'operator' && firstValue && operator) {
let newResult;
if (operator === 'plus') newResult = firstValue + secondValue;
if (operator === 'minus') newResult = firstValue - secondValue;
if (operator === 'times') newResult = firstValue * secondValue;
if (operator === 'divide') newResult = firstValue / secondValue;
display.textContent = newResult;
}
// ...
}
// ...
});

Wenn User einen Operator klicken, können sie weitere Berechnungen sowohl mit dem Operator als auch dem Istgleich-Zeichen machen. Das macht die Sache etwas komplizierter.

Hier sind die möglichen Kombinationen:

  • Ziffer -> Operator -> Istgleich -> Istgleich
  • Ziffer -> Operator -> Ziffer -> Istgleich -> Istgleich
  • Ziffer -> Operator -> Ziffer -> Operator -> Ziffer -> Operator ->

Zwei weiterführende Berechnungen benutzen die Istgleich-Taste:

  1. Ziffer -> Operator -> Istgleich -> Istgleich
  2. Ziffer -> Operator -> Ziffer -> Istgleich -> Istgleich

Im ersten Beispiel drückt Tim 9 - =. Wir interpretieren das als 9 - 9. Der Rechner zeigt also 0. Tim drückt nochmal =. Also rechnen wir 0 - 9 und erhalten -9.
Im zweiten Beispiel drückt Tim 8 - 5 =. Der Rechner zeigt 3. Tim drückt nochmal auf =. Wir rechnen 3 - 5, der Rechner zeigt also -2.

Fügen wir die entsprechenden Tests hinzu:

const tests = [
// ...
{
message: 'Number Operator Equal Equal',
keys: ['9', 'minus', 'equal', 'equal'],
result: '-9',
},
{
message: 'Number Operator Number Equal Equal',
keys: ['8', 'minus', '5', 'equal', 'equal'],
result: '-2',
},
];

Diese Tests schlagen fehl.

Im ersten Fall zeigt der Rechner 9. Um herauszufinden, was der Rechner macht, loggen wir firstValue, operator und secondValue in die Konsole:

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
// ...
if (firstValue && operator) {
let newResult;
if (operator === 'plus') newResult = firstValue + secondValue;
if (operator === 'minus') newResult = firstValue - secondValue;
if (operator === 'times') newResult = firstValue * secondValue;
if (operator === 'divide') newResult = firstValue / secondValue;
display.textContent = newResult;
// console.log values.
console.log(firstValue, operator, secondValue, '=', newResult);
} else {
display.textContent = parseFloat(result) * 1;
}
}
// ...
});

Im ersten Schritt sind firstValue und secondValue gleich 9, das ist soweit richtig. Im zweiten Schritt bleibt firstValue allerdings auch 9, weil das immer noch so als custom attribute gespeichert ist. secondValue ist gleich 0, weil wir dafür den angezeigten Wert (also result) definiert haben. Beides ist falsch. Beide Werte müssen nach der ersten Berechnung aktualisiert werden.

firstValue kann einfach nach der Berechnung dem result zugewiesen werden:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue);
const operator = calculator.dataset.operator;
const secondValue = parseFloat(result);
if (firstValue && operator) {
let newResult;
if (operator === 'plus') newResult = firstValue + secondValue;
if (operator === 'minus') newResult = firstValue - secondValue;
if (operator === 'times') newResult = firstValue * secondValue;
if (operator === 'divide') newResult = firstValue / secondValue;
display.textContent = newResult;
// assign results to firstValue
calculator.dataset.firstValue = newResult;
} else {
display.textContent = parseFloat(result) * 1;
}
}
// ...
});

Für secondValue müssen wir das neue Ergebnis speichern. Das ist ein bisschen schwierigier, weil es aus dem Display verschwindet. Wir behelfen uns, indem wir ein neues custom attribute setzen:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'equal') {
// ...
if (firstValue && operator) {
// ...
// assign results to firstValue
calculator.dataset.firstValue = newResult;
// store second value as modifier for followup calculations
calculator.dataset.modifierValue = secondValue;
} else {
display.textContent = parseFloat(result) * 1;
}
}
// ...
});

Bevor wir eine weitere Berechnung durchführen, müssen wir prüfen, ob ein modifierValue vorhanden ist. Wenn ja, setzen wir das modifierValue gleich dem secondValue. Wenn nein, nutzen wir, wie gehabt, das angezeigte Resultat als secondValue:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue);
const operator = calulator.dataset.operator;
// finds modifier value
const modifierValue = parseFloat(calculator.dataset.modifierValue);
// if present, use it as second value
const secondValue = modifierValue || parseFloat(result);
if (firstValue && operator) {
let newResult;
if (operator === 'plus') newResult = firstValue + secondValue;
if (operator === 'minus') newResult = firstValue - secondValue;
if (operator === 'times') newResult = firstValue * secondValue;
if (operator === 'divide') newResult = firstValue / secondValue;
display.textContent = newResult;
calculator.dataset.firstValue = newResult;
calculator.dataset.modifierValue = secondValue;
} else {
display.textContent = parseFloat(result) * 1;
}
}
// ...
});

So, jetzt schlagen alle Tests fehl! Das ist aber normal, wenn man Features einbaut. Wir haben modifierValue benutzt, aber es wird nicht zurückgesetzt, wenn wir den Rechner zurücksetzen. Fügen wir das also ein:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'clear') {
if (button.textContent === 'AC') {
delete calculator.dataset.firstValue;
delete calculator.dataset.operator;
// clearing the modifier value
delete calculator.dataset.modifierValue;
}
display.textContent = '0';
button.textContent = 'AC';
}
// ...
});

Wir fügen außerdem resetCalculator() noch eine Zeile hinzu, um sicherzustellen, dass modifierValue auch zurückgesetzt wird:

function resetCalculator() {
// ...
console.assert(!calculator.dataset.modifierValue, 'No modifier value');
}

Wenn wir jetzt testen, funktionieren die alten Tests wieder! Auch der neue Test Number -> Operator -> Number -> Equal -> Equal funktioniert. Number -> Operator -> Equal -> Equal wirft aber weiterhin eine Fehlermeldung. Warum? Loggen wir wieder firstValue, operator, secondValue und newResult und schauen wir uns die Sache an:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'equal') {
// ...
if (firstValue && operator) {
// ...
// console.log values
console.log(firstValue, operator, secondValue, '=', newResult);
} else {
display.textContent = parseFloat(result)* 1;
}
}
});

Der Rechner führt die erste Berechnung aus, aber die zweite nicht mehr, egal, wie oft wir auf Istgleich drücken. Warum? Schauen wir auf die Konsole:

  • firstValue = 9
  • operator = minus
  • secondValue = 9
  • newResult = 0

Für die zweite Berechnung wird firstValue der Wert von newResult aus der ersten Berechnung zugewiesen. Damit wird firstValue zu 0 und damit falsey – und somit wird auch unsere Bedingung nicht mehr erfüllt:

if (firstValue && operator) {
// ...
}
// Example of how typeof works
console.log(typeof 42); // number
console.log(typeof 42 === number); // true

Bauen wir nun die richtige Prüfung ein:

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'equal') {
// ...
if (typeof firstValue === 'number' && operator) {
// make calculation
} else {
// ...
}
}
// ...
});

Wir können folgende Berechnung mit Operatoren machen:

  • Ziffer -> Operator -> Ziffer -> Operator Wir sollten aber auch das ermöglichen:
  • Ziffer -> Operator -> Ziffer -> Operator -> Ziffer -> Operator etc.

Angenommen, Tim drückt folgende Tasten: 1 + 2 + 3 + 4 + 5 +. Das sollte passieren:

  1. 1 + 2 + -> Der Rechner zeigt 3
  2. 3 + -> Der Rechner zeigt 6
  3. 4 + -> Der Rechner zeigt 10
  4. 5 + -> Der Rechner zeigt 15

Machen wir den Test:

const tests = [
// ...
{
message: 'Operator follow-up calculation',
keys: ['1', 'plus', '2', 'plus', '3', 'plus', '4', 'plus', '5', 'plus'],
result: '15',
},
]

Der Test schlägt natürlich fehl.

Wir müssen

  1. firstValue mit dem berechneten Ergebnis ersetzen
  2. secondValue aus dem Display holen

Letzteres tun wir schon, ersteres geht so:

calculator.buttonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'operator') {
// ...
if (
previousButtonType !== 'operator' &&
typeof firstValue === 'number' &&
operator
) {
// ...
display.textContent = newResult;
// if there's a calculation, we change firstValue
calculator.dataset.firstValue = newResult;
} else {
// otherwise, we set the displayed result to firstValue
calculator.dataset.firstValue = result;
}
calculator.dataset.operator = key;
}
// ...
});

Jetzt sollten wir follow-up Berechnungen mit Operatoren machen können.

Blöderweise schlägt jetzt wieder ein anderer Test fehl: Equal Operator.

Was passiert hier? Schauen wir nochmal kurz den Test an:

const tests = [
// ...
{
message: 'Equal Operator',
keys: ['1', 'plus', '1', 'equal', 'plus', '1', 'equal'],
result: '3',
},
]

Der Rechner rechnet, wenn wir Equal -> Operator klicken. Das sollte nicht passieren. Wir können das ganz einfach lösen, indem wir prüfen, ob der vorherige Button equal war.

calculatorButtonsDiv.addEventListener('click', e => {
// ...
if (buttonType === 'operator') {
// ...
if (
previousButtonType !== 'operator' &&
// checks if previous button is equal key
previousButtonType !== 'equal' &&
typeof firstValue === 'number' &&
operator
) {
// ...
} else {
// ...
}
calculator.dataset.operator = key;
}
});