Easy edge cases
Nun spielt Tim, der Troublemaker, mit dem Taschenrechner. Er hat dieselben Möglichkeiten wie Mary. Er kann mit einer der folgenden Tasten beginnen:
- mit einer Ziffer
- mit einem Operator
- mit einem Dezimalzeichen
- mit der Istgleich-Taste
- mit der Löschtaste
Number key first
Section titled “Number key first”Wenn Tim auf eine Ziffer klickt, ersetzen wir den Wert im Display mit dieser Ziffer. Nach der Ziffer kann Tim wieder wählen:
- weitere Ziffer (wird vom Happy Path abgefangen)
- Operator (wird vom Happy Path abgefangen)
- Dezimalzeichen (wird vom Happy Path abgefangen)
- Löschtaste (wird vom Happy Path abgefangen)
- Istgleich-Taste
Die ersten vier Möglichkeiten werden vom Happy Path abgefangen. Die Istgleich-Taste macht nach einer Nummer keinen Sinn. Wie reagieren wir sinnvoll darauf?
Number -> Equal
Section titled “Number -> Equal”Machen wir den Test: Wenn Tim ‘5 =’ klickt, sollte im Display ‘5’ angezeigt werden:
const tests = [ // ... { message: 'Number Equal', keys: ['5', 'Equal'], result: '5', },]Oh-oh: Assertion failed: Number Equal - da funktioniert etwas nicht!
Nun verschwindet die Ziffer völlig aus dem Display. Schauen wir uns an, was hier passiert ist:
calculatorButtonsDiv.addEventListener('click', event => { // ... if (buttonType === 'equal') { const firstValue = parseFloat(calculator.dataset.firstValue); const operator = calculator.dataset.operator; const secondValue = parseFloat(result);
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; } // ...})Der equal-Button wurde gedrückt, aber wir haben weder ein firstValue noch einen operator, da diese beiden Werte erst gesetzt werden, wenn ein Operator gedrückt wird. Folglich gibt es kein newResult und das Display ist leer.
Um diesen Fall abzufangen, können wir die Kalkulation einfach überspringen:
// ...if (buttonType === 'equal') { const firstValue = parseFloat(calculator.dataset.firstValue); const operator = calculator.dataset.operator; const secondValue = parseFloat(result);
// skips 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; }}Number -> Decimal -> Equal
Section titled “Number -> Decimal -> Equal”Tim drückt jetzt 2 . 4 5 =. Der Rechner sollte 2.45 anzeigen. Macht er das? Machen wir den Test:
const tests = [ // ... { message: 'Number Decimal Equal', keys: ['2', 'decimal', '4', '5', 'equal'], result: '2.45', },]Der Test hat funktioniert – jetzt testen wir aber auch noch manuell (das sollten wir sowieso immer machen, also den Test testen).
Wir haben jetzt alle Fälle mit Ziffertasten abgedeckt. Tim kann aber noch vier andere Tasten drücken:
- Operator
- Dezimal
- Löschen
- Istgleich
Schauen wir uns zuerst die Dezimaltaste an:
Decimal key first
Section titled “Decimal key first”Wenn das Display 0 zeigt, soll ein Dezimalpunkt angehängt werden. Wir könnten ja 0.1 eintippen wollen. Dieser Fall wird vom Code schon abgedeckt. Trotzdem fügen wir einen Testcase in unser Script ein:
const tests = [ // ... { message: 'Decimal key', keys: ['decimal'], result: '0.', },]Nach der Dezimaltaste hat Tim wieder folgende Möglichkeiten:
- Dezimal -> Ziffer (wird vom Happy Path abgefangen)
- Dezimal -> Operator (wird vom Happy Path abgefangen)
- Dezimal -> Löschen (wird vom Happy Path abgefangen)
- Dezimal -> Dezimal
- Dezimal -> Istgleich
Dezimal -> Dezimal
Section titled “Dezimal -> Dezimal”Wenn Tim 2.. drückt, soll der Rechner 2. anzeigen.
Schreiben wir zuerst einen Test:
const tests = [ // ... { message: 'Decimal Decimal', keys: ['2', 'decimal', 'decimal'], result: '2.', },]Diesen Test besteht der Rechner nicht mehr. Wenn wir 2.. händisch eingeben, dann zeigt der Rechner 2.. an. So steht es auch im Code:
calculatorButtonsDiv.addEventListener('click', e => { // ... if (buttonType === 'decimal') { display.textContent = result + '.'; } // ...})Wir wollen natürlich keine zwei Dezimalpunkte. Was ist also zu tun? Wenn die Anzeige im Display bereits einen Dezimalpunkt beinhaltet, soll der zweite Dezimalpunkt ignoriert werden. Wir können mit includes testen, ob die Anzeige im Display einen Dezimalpunkt beinhaltet oder nicht. includes prüft, ob ein String einen anderen String enthält. Wenn ja, gibt includes ein true zurück, wenn nein, ein false. includes ist case-sensitive!
Beispiel für includes:
Section titled “Beispiel für includes:”const string = 'The hamburgers taste pretty good!';const hasExclamation = string.includes('!');console.log(hasExclamation); // trueWenn das Display schon einen Dezimalpunkt enthält, machen wir gar nichts:
calculatorButtonsDiv.addEventListener('click', e => { // ... if (buttonType === 'decimal') { if (!result.includes('.')) { display.textContent = result + '.'; } }});Gehen wir noch einen Schritt weiter: Wenn Tim 2 . 5 . 5 eintippt, soll im Display 2.55 angezeigt werden. Das funktioniert schon. Sichern wir uns trotzdem mit einem Test ab:
const tests = { // ... { message: 'Decimal Number Decimal', keys: ['2', 'decimal', '5', 'decimal', '5'], result: '2.55', },}Decimal -> Equal
Section titled “Decimal -> Equal”Wenn Tim 2 . = eintippt, sollte das Display 2 anzeigen. Leider sehen wir aber das: 2.. Wir wollen Dezimalpunkte entfernen, wenn danach kein Operator gedrückt wird. Fügen wir zuerst einen Test hinzu:
const tests = { // ... { message: 'Decimal Equal', keys: ['2', 'decimal', 'equal'], result: '2', },}Drücken wir nach einer solchen Eingabe eine Operator-Taste, wird der Dezimalpunkt von JavaScript automatisch gelöscht und das Ergebnis richtig berechnet:
1 . + 5 =der Rechner zeigt 61 . - 5 =der Rechner zeigt -41 . * 5 =der Rechner zeigt 51 . / 5 =der Rechner zeigt 0.2
Dieses Muster können wir uns zunutze machen! Wir können unnötige Dezimalpunkte entfernen, indem wir das Ergebnis mit 1 multiplizieren:
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) { // ... else { // strips unnecessary decimal point display.textContent = parseFloat(result) * 1; } } } // ...});Der Decimal Equal Test sollte jetzt bestanden werden.
Equal key first
Section titled “Equal key first”Wenn Tim als erstes auf die equal-Taste drückt, sollte der Rechner weiterhin 0 anzeigen. Das gibt der Code schon her, wir schreiben trotzdem einen Test:
const tests = { // ... { message: 'Equal key', keys: ['equal'], result: '0', },}Welche Optionen hat Tim jetzt?
- Istgleich -> Ziffer
- Istgleich -> Dezimaltaste
- Istgleich -> Operator
- Istgleich -> Istgleich (wird vom Code oben abgedeckt)
- Istgleich -> Löschen (wird vom Happy Path abgedeckt)
Equal -> Number
Section titled “Equal -> Number”Wenn Tim nach der Istgleich-Taste eine Zifferntaste drückt, können wir annehmen, dass er eine neue Rechnung durchführen will. Diese Rechnung soll keine Werte aus einer früheren Rechnung enthalten. Dafür machen wir zwei neue Tests:
const tests = [ // ... { message: 'Equal Number', keys: ['equal', '3'], return: '3', }, { message: 'Number Equal Number', keys: ['5', 'equal', '3'], return: ['3'], },]Den ersten Test wurde bestanden, der zweite schlägt fehl: Warum? Schauen wir ins Script:
Zuerst hat Tim eine Ziffer eingegeben, die dann im Display angezeigt wurde. Dann ein Istgleich, das, weil weder firstValue noch operator vorhanden war, nur den Wert im Display mit eins multipliziert hat. Anschließend wurde wieder eine Ziffer eingetippt, die, da result zu diesem Zeitpunkt nicht 0war, einfach nur die neue Ziffer an die alte angehängt hat:
// if a number is pressedif (buttonType === 'number') { if (result === '0') { display.textContent = key; } else { display.textContent = result + key; }}Um diesen Fehler zu beheben, müssen wir wissen, ob Tim vorher equal gedrückt hat. Wenn ja, ersetzen wir den Wert im Display mit der gedrückten Ziffer:
// if a number is pressedif (buttonType === 'number') { if (result === '0') { display.textContent = key; } else { display.textContent = result + key; }
if (previousButtonType === 'operator') { display.textContent = key; } // wir ergänzen um diese if-Abfrage: if (previousButtonType === 'equal') { display.textContent = key; }}Dieser Test wird bestanden! Bevor wir weitermachen, wollen wir aber die custom attributes aufräumen, da wir annehmen, dass Tim eine neue Rechnung starten will. In diesem Fall sollten keine alten custom attributes mehr vorhanden sein. Wir könnten wie folgt ergänzen …
// ...if (previousButtontype === 'equal') { display.textContent = key; delete calculator.dataset.firstValue; delete calculator.dataset.operator;}// ...… eleganter wäre hingegen, einfach den Rechner zurückzusetzen, dafür haben wir ja eine Funktion:
// ...if (previousButtonType === 'equal') { resetCalculator(); display.textContent = key;}// ...Equal -> Decimal
Section titled “Equal -> Decimal”Wenn Tim nach dem Istgleich den Dezimalpunkt drückt, können wir auch annehmen, dass er eine neue Rechnung beginnen will. Auch diese neue Rechnung soll keine Werte von vorhergegangenen Berechnungen enthalten. Tim könnte folgende Kombinationen drücken:
= .der Rechner sollte0.anzeigen5 = .der Rechner sollte0.anzeigen
const tests = [ // ... { message: 'Equal Decimal', keys: ['equal', 'decimal'], result: '0.', }, { message: 'Number Equal Decimal', keys: ['5', 'equal', 'decimal'], result: '0.', },]Der erste Test ist bestanden, der zweite nicht. Warum? Schauen wir in den Code: Wenn nach einem Klick auf die Dezimaltaste kein Dezimalpunkt im Display steht, dann fügen wir einen hinzu:
// if decimal is pressedif (buttonType === 'decimal') { if (!result.includes('.')) { display.textContent = result + '.'; }}Zur Behebung dieses Problems müssen wir wieder prüfen, ob der previousButtonType ein Istgleich war und wenn ja, dann setzen wir die Eingabe auf 0. zurück und resetten wieder den ganzen Rechner:
// if decimal is pressedif (buttonType === 'decimal') { if (!result.includes('.')) { display.textContent = result + '.'; }
if (previousButtonType === 'equal') { resetCalculator(); display.textContent = '0.'; }}Equal -> Operator
Section titled “Equal -> Operator”Wenn Tim einen Operator nach einem Istgleich drückt, dann gehen wir davon aus, dass er die Berechnung fortsetzen will. Dafür muss das Resultat der letzten Berechnung als firstValue erhalten bleiben.
Testen wir folgenden Fall:
const test = [ // ... { message: 'Calculator Operator', keys: ['1', 'plus', '1', 'equal', 'plus', '1', 'equal'], result: '3', },]Operator keys first
Section titled “Operator keys first”Wenn eine Operator-Taste gedrückt wird, wollen wir zwei Dinge tun:
- die Taste optisch kennzeichnen
- den Rechner auf die Kalkulation vorbereiten
Beides wird vom Happy Path schon abgedeckt. Wenn das Display
0zeigt und Tim einen Operator drückt, dann wird einfach0alsfirstValueverwendet.
Nach einer Operator-Taste hat Tim wieder folgende Optionen:
- Operator -> Ziffer (wird vom HappyPath abgedeckt)
- Operator -> Operator (wird vom HappyPath abgedeckt)
- Operator -> Decimal
- Operator -> Equal
- Operator -> Clear (wird vom HappyPath abgedeckt)
Operator -> Decimal
Section titled “Operator -> Decimal”Wenn Tim nach einem Dezimalpunkt einen Operator klickt, nehmen wir an, dass er seine nächste Berechnung mit 0. beginnen will. Der Rechner sollte also 0. zeigen. Testen wir das:
const tests = [ // ... { message: 'Operator Decimal', keys: ['times', 'decimal'], result: '0.', }, { message: 'Number Operator Decimal', keys: ['5', 'times', 'decimal'], result: '0.', },]Der erste Test funktioniert, der zweite nicht. Warum? Aus demselben Grund, warum auch die Tastenfolge Number Equal Decimal nicht funktioniert hat: Wenn nach einem Klick auf die Dezimaltaste kein Dezimalpunkt im Display steht, dann fügen wir einen hinzu. Auch hier müssen wir wieder den previousButtonType prüfen, wenn dieser ein Operator war, setzen wir das Display auf 0. zurück. In diesem Fall ist kein Reset nötig, ja sogar kontraproduktiv, weil wir damit unser firstValue löschen würden!
// ...if (buttonType === 'decimal') { if (!result.includes('.')) { display.textContent = result + '.'; }
if (previousButtonType === 'equal') { resetCalculator(); display.textContent = '0.'; } if (previousButtonType === 'operator') { display.textContent = '0.'; }}Operator -> Equal
Section titled “Operator -> Equal”Wenn Tim nach dem Operator auf Istgleich drückt, möchte er eine Rechnung durchführen. Wir nehmen hier einfach an, dass firstValue und secondValue gleich sind:
- Tim drückt
1 + =, wir nehmen es als1 + 1 =an, der Rechner zeigt2 - Tim drückt
2 - =, wir nehmen es als2 - 2 =an, der Rechner zeigt0 - Tim drückt
3 * =, wir nehmen es als3 * 3 =an, der Rechner zeigt9 - Tim drückt
4 / =, wir nehmen es als4 / 4 =an, der Rechner zeigt1
Fügen wir einen Test hinzu:
const tests = [ // ... { message: 'Number Operator Equal', keys: ['7', 'divide', 'equal'], result: '1', },]Das funktioniert jetzt!
Clear key first
Section titled “Clear key first”Das ist bereits erledigt …