nandi/cosmicnimpublic Fork 0
d1e3be7
Commits
Clone
git clone https://git.rickub.com/nandi/cosmicnim.git
git clone ssh://git@rickub.com/nandi/cosmicnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandithebull committed 2026-09-20T12:04:15-07:00 Browse files
d1e3be7 parent: a561f5a
modified examples/calculator.nim +24 -6
@@ -12,6 +12,13 @@ type
1212 Op = enum
1313 opNone = "", opAdd = "+", opSub = "−", opMul = "×", opDiv = "÷"
1414
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+
1522 Key = object
1623 label: string
1724 id: int32
@@ -22,7 +29,7 @@ type
2229 acc: float
2330 pending: Op
2431 startNew: bool ## next digit starts a fresh entry
25- error: bool
32+ error: CalcError
2633
2734 const
2835 Cols = 4
@@ -89,8 +96,14 @@ proc current(c: Calc): float =
8996 ## What the display is showing, as a number.
9097 if c.entry.len > 0: parseFloat(c.entry) else: c.acc
9198
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+
92105 proc display*(c: Calc): string =
93- if c.error: "Error"
106+ if c.failed: "Error"
94107 elif c.entry.len > 0: c.entry
95108 else: format(c.acc)
96109
@@ -103,8 +116,12 @@ proc resolve(c: var Calc) =
103116 of opSub: c.acc = c.acc - rhs
104117 of opMul: c.acc = c.acc * rhs
105118 of opDiv:
106- if rhs == 0.0: c.error = true
119+ if rhs == 0.0: c.error = errDivZero
107120 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
108125 c.entry = ""
109126
110127 proc digit(c: var Calc; d: int32) =
@@ -121,7 +138,7 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} =
121138 if id == IdClear:
122139 c[] = newCalc()
123140 return
124- if c.error:
141+ if c[].failed:
125142 return
126143
127144 case id
@@ -135,7 +152,7 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} =
135152 if '.' notin c.entry: c.entry.add '.'
136153 of IdAdd, IdSub, IdMul, IdDiv:
137154 c[].resolve()
138- if not c.error:
155+ if not c[].failed:
139156 c.pending = case id
140157 of IdAdd: opAdd
141158 of IdSub: opSub
@@ -215,7 +232,8 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =
215232 # occupy its space either way or the whole keypad shifts.
216233 b.size(-1.0, CaptionH)
217234 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}"
219237 else: "",
220238 TextCaption)
221239
@@ -12,6 +12,13 @@ type
12 Op = enum12 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 = object22 Key = object
16 label: string23 label: string
17 id: int3224 id: int32
@@ -22,7 +29,7 @@ type
22 acc: float29 acc: float
23 pending: Op30 pending: Op
24 startNew: bool ## next digit starts a fresh entry31 startNew: bool ## next digit starts a fresh entry
25- error: bool32+ error: CalcError
26 33
27 const34 const
28 Cols = 435 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.acc97 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.entry107 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 - rhs116 of opSub: c.acc = c.acc - rhs
104 of opMul: c.acc = c.acc * rhs117 of opMul: c.acc = c.acc * rhs
105 of opDiv:118 of opDiv:
106- if rhs == 0.0: c.error = true119+ if rhs == 0.0: c.error = errDivZero
107 else: c.acc = c.acc / rhs120 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 return140 return
124- if c.error:141+ if c[].failed:
125 return142 return
126 143
127 case id144 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 id156 c.pending = case id
140 of IdAdd: opAdd157 of IdAdd: opAdd
141 of IdSub: opSub158 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":
3838 test "division by zero errors, and only C recovers":
3939 var c = fresh()
4040 check c.press(1, IdDiv, 0, IdEquals) == "Error"
41+ check c.reason == "Cannot divide by zero"
4142 check c.press(5) == "Error" # other keys are ignored
43+ check c.reason == "Cannot divide by zero"
4244 check c.press(IdClear) == "0"
45+ check c.reason == "" # cleared along with the error
4346 check c.press(7) == "7" # usable again
4447
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+
4564 test "decimals":
4665 var c = fresh()
4766 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 ignored42 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 again46 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"