-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathCalc.hs
188 lines (169 loc) · 6.28 KB
/
Calc.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
{-# OPTIONS_GHC -fwarn-incomplete-patterns #-} -- ensure that all possible Commands are covered in pattern matching
module Calc (
State(..),
processCommand, parseInput, populate, toString, initialState, Operation(..), Command(..), Digit(..), lbl
) where
-- | a data type representing all possible Commands that can be executed by the Calculator
data Command = Digit Digit -- ^ a Digit
| Dot -- ^ a Dot
| Operation Operation -- ^ an arithmetic operation
| Flush -- ^ evaluate the current operation (i.e. pressing '=')
| Clear -- ^ clear the current calculator state
| ClearError -- ^ clear the last error
deriving (Show, Eq, Ord)
-- | a data type representing all digits
data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine deriving (Show, Eq, Ord)
-- | a data type representing all possible arithmetic operations
data Operation = Add | Sub | Mul | Div deriving (Show, Eq, Ord)
-- | a data type representing all possible states of the calculator (see [calculator section](README.md#the-calculator))
data State = EnteringA Entering -- ^ entering register A
| EnteredAandOp Double Operation -- ^ A, Op
| EnteringB Double Operation Entering -- ^ A, Op, entering register B
| Calculated Double Operation Double -- ^ A, Op, B
| Error Double String -- ^ A, Message
deriving (Show, Eq)
-- | Entering is a tuple used while entering numbers. It consists of
type Entering = (String, Bool) -- A tuple of the String representation of the entered digits
-- and a flag signalling that Dot was already pressed.
-- | the initial state of the calculator
initialState :: State
initialState = EnteringA ("0", False)
-- | returns the String representation of a State
toString :: State -> String
toString s =
case s of
EnteringA a -> trim $ fst a
EnteredAandOp a _ -> show a
EnteringB _ _ b -> trim $ fst b
Calculated a _ _ -> show a
Error _ msg -> msg
-- | handle leading zero characters
trim :: String -> String
trim "0" = "0"
trim s@('0':'.':_) = s
trim ('0':tl) = tl
trim x = x
-- | convert an Entering tuple to a Double
fromEntering :: Entering -> Double
fromEntering = read . fst
-- | convert a Double to an Entering tuple
asEntering :: Double -> Entering
asEntering x = (show x, x /= (fromInteger . truncate) x)
-- | parse a Command from an input string
parseInput :: String -> Command
parseInput x = case x of
"0" -> Digit Zero
"1" -> Digit One
"2" -> Digit Two
"3" -> Digit Three
"4" -> Digit Four
"5" -> Digit Five
"6" -> Digit Six
"7" -> Digit Seven
"8" -> Digit Eight
"9" -> Digit Nine
"." -> Dot
"+" -> Operation Add
"-" -> Operation Sub
"*" -> Operation Mul
"/" -> Operation Div
"=" -> Flush
"C" -> Clear
"CE" -> ClearError
_ -> undefined
-- | return the button label for a calculator Command
lbl :: Command -> String
lbl c = case c of
Digit Zero -> "0"
Digit One -> "1"
Digit Two -> "2"
Digit Three -> "3"
Digit Four -> "4"
Digit Five -> "5"
Digit Six -> "6"
Digit Seven -> "7"
Digit Eight -> "8"
Digit Nine -> "9"
Dot -> "."
Operation Add -> "+"
Operation Sub -> "-"
Operation Mul -> "*"
Operation Div -> "/"
Flush -> "="
Clear -> "C"
ClearError -> "CE"
-- | process a calculator command. That is: compute a calculator state transition
processCommand :: Command -> State -> State
processCommand cmd = case cmd of
Digit x -> addDigit x
Dot -> addDot
Operation op -> applyOp op
command -> applyCmd command
-- | a helper function that allows to parse a command from a string and then execute it
populate :: String -> State -> State
populate = processCommand . parseInput
-- | add a digit
addDigit :: Digit -> State -> State
addDigit x s =
case s of
(EnteringA a) -> EnteringA (update a)
(EnteringB a op b) -> EnteringB a op (update b)
(EnteredAandOp a op) -> EnteringB a op (num x, False)
Calculated {} -> EnteringA (num x, False)
_ -> s
where
update (a, False) = (ccc a (num x), False)
update (a, True) = (ccc a (num x), True)
num i = lbl (Digit i)
ccc "0" "0" = "0" -- avoid to create leading 0 digits
ccc a b = a ++ b
-- | add a dot
addDot :: State -> State
addDot s =
case s of
(EnteringA a) -> EnteringA (dotted a)
(EnteringB a op b) -> EnteringB a op (dotted b)
_ -> s
where
dotted (a, False) = (a ++ ".", True)
dotted (a, True) = (a, True)
-- | try to evaluate the current operation
tryToCalc :: Double -> Operation -> Double -- A op B
-> (String -> a) -- error handler
-> (Double -> a) -- result handler
-> a
tryToCalc _ Div b mkError _ | b == 0 = mkError "Division by Zero!"
tryToCalc a op b _ mkResult =
let f = case op of
Add -> (+)
Sub -> (-)
Mul -> (*)
Div -> (/)
in mkResult $ f a b
-- | apply an arithmetic operation
applyOp :: Operation -> State -> State
applyOp op s =
case s of
(EnteringA a) -> EnteredAandOp (fromEntering a) op
(EnteringB a op' b) -> tryToCalc a op' (fromEntering b)
(Error a)
(`EnteredAandOp` op)
(EnteredAandOp a _) -> Error a "Can't do this!"
(Calculated a _ _) -> EnteredAandOp a op
_ -> s
-- | apply a unary command (C, CE, or =)
applyCmd :: Command -> State -> State
applyCmd cmd s =
case (cmd, s) of
(ClearError, Error a _) -> EnteringA (asEntering a)
(Clear, _) -> initialState
(_, Error _ _) -> s
(Flush, EnteringA _) -> s
(Flush, EnteredAandOp a _) -> Error a "Can't do this!"
(Flush, EnteringB a op b) -> calc a op (fromEntering b)
(Flush, Calculated a op b) -> calc a op b
_ -> s
where
calc a op b = tryToCalc a op b
(Error a)
(\a' -> Calculated a' op b)