Say why the calculator is in an error state
The error was a bool, so the only thing the display could offer was "Error" — true but useless. It is now an enum whose values are their own messages, surfaced in the caption row that was just reserved: the big display still says Error, the line above it says why. That also made a second failure worth naming. A result past what a float64 can hold used to become inf and print as a lone infinity sign; it now reports being out of range, which is the same kind of answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d1e3be7 parent: a561f5a modified
examples/calculator.nim +24 -6 | @@ -12,6 +12,13 @@ type | ||
| 12 | 12 | Op = enum |
| 13 | 13 | opNone = "", opAdd = "+", opSub = "−", opMul = "×", opDiv = "÷" |
| 14 | 14 | |
| 15 | + CalcError* = enum | |
| 16 | + ## The display can only ever say "Error", so the reason rides alongside | |
| 17 | + ## it. Each value is its own message. | |
| 18 | + errNone = "" | |
| 19 | + errDivZero = "Cannot divide by zero" | |
| 20 | + errRange = "Result is out of range" | |
| 21 | + | |
| 15 | 22 | Key = object |
| 16 | 23 | label: string |
| 17 | 24 | id: int32 |
| @@ -22,7 +29,7 @@ type | ||
| 22 | 29 | acc: float |
| 23 | 30 | pending: Op |
| 24 | 31 | startNew: bool ## next digit starts a fresh entry |
| 25 | - error: bool | |
| 32 | + error: CalcError | |
| 26 | 33 | |
| 27 | 34 | const |
| 28 | 35 | Cols = 4 |
| @@ -89,8 +96,14 @@ proc current(c: Calc): float = | ||
| 89 | 96 | ## What the display is showing, as a number. |
| 90 | 97 | if c.entry.len > 0: parseFloat(c.entry) else: c.acc |
| 91 | 98 | |
| 99 | +proc failed*(c: Calc): bool = c.error != errNone | |
| 100 | + | |
| 101 | +proc reason*(c: Calc): string = | |
| 102 | + ## Why the display says Error, or "" when it does not. | |
| 103 | + $c.error | |
| 104 | + | |
| 92 | 105 | proc display*(c: Calc): string = |
| 93 | - if c.error: "Error" | |
| 106 | + if c.failed: "Error" | |
| 94 | 107 | elif c.entry.len > 0: c.entry |
| 95 | 108 | else: format(c.acc) |
| 96 | 109 | |
| @@ -103,8 +116,12 @@ proc resolve(c: var Calc) = | ||
| 103 | 116 | of opSub: c.acc = c.acc - rhs |
| 104 | 117 | of opMul: c.acc = c.acc * rhs |
| 105 | 118 | of opDiv: |
| 106 | - if rhs == 0.0: c.error = true | |
| 119 | + if rhs == 0.0: c.error = errDivZero | |
| 107 | 120 | else: c.acc = c.acc / rhs |
| 121 | + # Catch a result that has run off the end of what a float can say, so it | |
| 122 | + # shows a reason rather than silently becoming inf or NaN. | |
| 123 | + if c.error == errNone and (c.acc != c.acc or abs(c.acc) == Inf): | |
| 124 | + c.error = errRange | |
| 108 | 125 | c.entry = "" |
| 109 | 126 | |
| 110 | 127 | proc digit(c: var Calc; d: int32) = |
| @@ -121,7 +138,7 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} = | ||
| 121 | 138 | if id == IdClear: |
| 122 | 139 | c[] = newCalc() |
| 123 | 140 | return |
| 124 | - if c.error: | |
| 141 | + if c[].failed: | |
| 125 | 142 | return |
| 126 | 143 | |
| 127 | 144 | case id |
| @@ -135,7 +152,7 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} = | ||
| 135 | 152 | if '.' notin c.entry: c.entry.add '.' |
| 136 | 153 | of IdAdd, IdSub, IdMul, IdDiv: |
| 137 | 154 | c[].resolve() |
| 138 | - if not c.error: | |
| 155 | + if not c[].failed: | |
| 139 | 156 | c.pending = case id |
| 140 | 157 | of IdAdd: opAdd |
| 141 | 158 | of IdSub: opSub |
| @@ -215,7 +232,8 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = | ||
| 215 | 232 | # occupy its space either way or the whole keypad shifts. |
| 216 | 233 | b.size(-1.0, CaptionH) |
| 217 | 234 | b.text( |
| 218 | - if c.pending != opNone and not c.error: &"{format(c.acc)} {c.pending}" | |
| 235 | + if c[].failed: c[].reason | |
| 236 | + elif c.pending != opNone: &"{format(c.acc)} {c.pending}" | |
| 219 | 237 | else: "", |
| 220 | 238 | TextCaption) |
| 221 | 239 | |
| @@ -12,6 +12,13 @@ type | |||
| 12 | Op = enum | 12 | Op = enum |
| 13 | opNone = "", opAdd = "+", opSub = "−", opMul = "×", opDiv = "÷" | 13 | opNone = "", opAdd = "+", opSub = "−", opMul = "×", opDiv = "÷" |
| 14 | 14 | ||
| 15 | + CalcError* = enum | ||
| 16 | + ## The display can only ever say "Error", so the reason rides alongside | ||
| 17 | + ## it. Each value is its own message. | ||
| 18 | + errNone = "" | ||
| 19 | + errDivZero = "Cannot divide by zero" | ||
| 20 | + errRange = "Result is out of range" | ||
| 21 | + | ||
| 15 | Key = object | 22 | Key = object |
| 16 | label: string | 23 | label: string |
| 17 | id: int32 | 24 | id: int32 |
| @@ -22,7 +29,7 @@ type | |||
| 22 | acc: float | 29 | acc: float |
| 23 | pending: Op | 30 | pending: Op |
| 24 | startNew: bool ## next digit starts a fresh entry | 31 | startNew: bool ## next digit starts a fresh entry |
| 25 | - error: bool | 32 | + error: CalcError |
| 26 | 33 | ||
| 27 | const | 34 | const |
| 28 | Cols = 4 | 35 | Cols = 4 |
| @@ -89,8 +96,14 @@ proc current(c: Calc): float = | |||
| 89 | ## What the display is showing, as a number. | 96 | ## What the display is showing, as a number. |
| 90 | if c.entry.len > 0: parseFloat(c.entry) else: c.acc | 97 | if c.entry.len > 0: parseFloat(c.entry) else: c.acc |
| 91 | 98 | ||
| 99 | +proc failed*(c: Calc): bool = c.error != errNone | ||
| 100 | + | ||
| 101 | +proc reason*(c: Calc): string = | ||
| 102 | + ## Why the display says Error, or "" when it does not. | ||
| 103 | + $c.error | ||
| 104 | + | ||
| 92 | proc display*(c: Calc): string = | 105 | proc display*(c: Calc): string = |
| 93 | - if c.error: "Error" | 106 | + if c.failed: "Error" |
| 94 | elif c.entry.len > 0: c.entry | 107 | elif c.entry.len > 0: c.entry |
| 95 | else: format(c.acc) | 108 | else: format(c.acc) |
| 96 | 109 | ||
| @@ -103,8 +116,12 @@ proc resolve(c: var Calc) = | |||
| 103 | of opSub: c.acc = c.acc - rhs | 116 | of opSub: c.acc = c.acc - rhs |
| 104 | of opMul: c.acc = c.acc * rhs | 117 | of opMul: c.acc = c.acc * rhs |
| 105 | of opDiv: | 118 | of opDiv: |
| 106 | - if rhs == 0.0: c.error = true | 119 | + if rhs == 0.0: c.error = errDivZero |
| 107 | else: c.acc = c.acc / rhs | 120 | else: c.acc = c.acc / rhs |
| 121 | + # Catch a result that has run off the end of what a float can say, so it | ||
| 122 | + # shows a reason rather than silently becoming inf or NaN. | ||
| 123 | + if c.error == errNone and (c.acc != c.acc or abs(c.acc) == Inf): | ||
| 124 | + c.error = errRange | ||
| 108 | c.entry = "" | 125 | c.entry = "" |
| 109 | 126 | ||
| 110 | proc digit(c: var Calc; d: int32) = | 127 | proc digit(c: var Calc; d: int32) = |
| @@ -121,7 +138,7 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} = | |||
| 121 | if id == IdClear: | 138 | if id == IdClear: |
| 122 | c[] = newCalc() | 139 | c[] = newCalc() |
| 123 | return | 140 | return |
| 124 | - if c.error: | 141 | + if c[].failed: |
| 125 | return | 142 | return |
| 126 | 143 | ||
| 127 | case id | 144 | case id |
| @@ -135,7 +152,7 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} = | |||
| 135 | if '.' notin c.entry: c.entry.add '.' | 152 | if '.' notin c.entry: c.entry.add '.' |
| 136 | of IdAdd, IdSub, IdMul, IdDiv: | 153 | of IdAdd, IdSub, IdMul, IdDiv: |
| 137 | c[].resolve() | 154 | c[].resolve() |
| 138 | - if not c.error: | 155 | + if not c[].failed: |
| 139 | c.pending = case id | 156 | c.pending = case id |
| 140 | of IdAdd: opAdd | 157 | of IdAdd: opAdd |
| 141 | of IdSub: opSub | 158 | of IdSub: opSub |
| @@ -215,7 +232,8 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = | |||
| 215 | # occupy its space either way or the whole keypad shifts. | 232 | # occupy its space either way or the whole keypad shifts. |
| 216 | b.size(-1.0, CaptionH) | 233 | b.size(-1.0, CaptionH) |
| 217 | b.text( | 234 | b.text( |
| 218 | - if c.pending != opNone and not c.error: &"{format(c.acc)} {c.pending}" | 235 | + if c[].failed: c[].reason |
| 236 | + elif c.pending != opNone: &"{format(c.acc)} {c.pending}" | ||
| 219 | else: "", | 237 | else: "", |
| 220 | TextCaption) | 238 | TextCaption) |
| 221 | 239 | ||
modified
tests/tcalculator.nim +19 -0 | @@ -38,10 +38,29 @@ suite "calculator": | ||
| 38 | 38 | test "division by zero errors, and only C recovers": |
| 39 | 39 | var c = fresh() |
| 40 | 40 | check c.press(1, IdDiv, 0, IdEquals) == "Error" |
| 41 | + check c.reason == "Cannot divide by zero" | |
| 41 | 42 | check c.press(5) == "Error" # other keys are ignored |
| 43 | + check c.reason == "Cannot divide by zero" | |
| 42 | 44 | check c.press(IdClear) == "0" |
| 45 | + check c.reason == "" # cleared along with the error | |
| 43 | 46 | check c.press(7) == "7" # usable again |
| 44 | 47 | |
| 48 | + test "a result too large to represent says so, rather than showing inf": | |
| 49 | + var c = fresh() | |
| 50 | + # Each pass multiplies by ~1e18; float64 gives up a little past 1e308. | |
| 51 | + discard c.press(9, IdMul, 9, IdEquals) | |
| 52 | + for _ in 1 .. 20: | |
| 53 | + discard c.press(IdMul, 9, 9, 9, 9, 9, 9, 9, 9, 9, IdEquals) | |
| 54 | + discard c.press(IdMul, 9, 9, 9, 9, 9, 9, 9, 9, 9, IdEquals) | |
| 55 | + check c.display == "Error" | |
| 56 | + check c.reason == "Result is out of range" | |
| 57 | + | |
| 58 | + test "a healthy calculation has no reason": | |
| 59 | + var c = fresh() | |
| 60 | + check c.press(2, IdAdd, 2, IdEquals) == "4" | |
| 61 | + check c.reason == "" | |
| 62 | + check not c.failed | |
| 63 | + | |
| 45 | 64 | test "decimals": |
| 46 | 65 | var c = fresh() |
| 47 | 66 | check c.press(1, IdDot, 5, IdAdd, 2, IdDot, 2, 5, IdEquals) == "3.75" |
| @@ -38,10 +38,29 @@ suite "calculator": | |||
| 38 | test "division by zero errors, and only C recovers": | 38 | test "division by zero errors, and only C recovers": |
| 39 | var c = fresh() | 39 | var c = fresh() |
| 40 | check c.press(1, IdDiv, 0, IdEquals) == "Error" | 40 | check c.press(1, IdDiv, 0, IdEquals) == "Error" |
| 41 | + check c.reason == "Cannot divide by zero" | ||
| 41 | check c.press(5) == "Error" # other keys are ignored | 42 | check c.press(5) == "Error" # other keys are ignored |
| 43 | + check c.reason == "Cannot divide by zero" | ||
| 42 | check c.press(IdClear) == "0" | 44 | check c.press(IdClear) == "0" |
| 45 | + check c.reason == "" # cleared along with the error | ||
| 43 | check c.press(7) == "7" # usable again | 46 | check c.press(7) == "7" # usable again |
| 44 | 47 | ||
| 48 | + test "a result too large to represent says so, rather than showing inf": | ||
| 49 | + var c = fresh() | ||
| 50 | + # Each pass multiplies by ~1e18; float64 gives up a little past 1e308. | ||
| 51 | + discard c.press(9, IdMul, 9, IdEquals) | ||
| 52 | + for _ in 1 .. 20: | ||
| 53 | + discard c.press(IdMul, 9, 9, 9, 9, 9, 9, 9, 9, 9, IdEquals) | ||
| 54 | + discard c.press(IdMul, 9, 9, 9, 9, 9, 9, 9, 9, 9, IdEquals) | ||
| 55 | + check c.display == "Error" | ||
| 56 | + check c.reason == "Result is out of range" | ||
| 57 | + | ||
| 58 | + test "a healthy calculation has no reason": | ||
| 59 | + var c = fresh() | ||
| 60 | + check c.press(2, IdAdd, 2, IdEquals) == "4" | ||
| 61 | + check c.reason == "" | ||
| 62 | + check not c.failed | ||
| 63 | + | ||
| 45 | test "decimals": | 64 | test "decimals": |
| 46 | var c = fresh() | 65 | var c = fresh() |
| 47 | check c.press(1, IdDot, 5, IdAdd, 2, IdDot, 2, 5, IdEquals) == "3.75" | 66 | check c.press(1, IdDot, 5, IdAdd, 2, IdDot, 2, 5, IdEquals) == "3.75" |