-- |Hascal is a simple, minimalistic, tiny calculator library and program. -- -- * Hascal only understands single-character operators. -- * Hascal only understands infix operators. -- * Hascal does not understand parantheses. -- * @_@ is hard-coded as single prefix for negating numbers. -- * By default, Hascal understands the operators @+@, @-@, @*@, @/@, @^@, and -- @?@ (logarithm). -- * By default, Hascal understands the constants @pi@, @e@, and @i@. -- * Using Hascal as a library, you can add new operators and new constants -- using a configuration data type. -- -- The hascal executable program is easy to use. In a shell, type: -- -- >>> hascal 1+2-3*4/5^6?7 -- -1.7263530417152033 -- -- Given a configuration, the 'hascal' function similarly evaluates an -- expression of type 'String'. In a Haskell interpreter like GHCI, type: -- -- >>> hascal def "1+2-3*4/5^6?7" -- Right (0.2736469582847967 :+ 0.0) -- -- >>> hascal def "1++2" -- Left "Error at \"\"." -------------------------------------------------------------------------------- module Hascal ( Config(..) , Data.Default.Default(..) , hascal , showHascal , showNumber ) where -------------------------------------------------------------------------------- import Data.Complex import Data.Default import Data.List import Data.List.Split -------------------------------------------------------------------------------- -- | The result of an evaluation of a string is either a String containing an -- error message or a complex number. type Result t = Either String (Complex t) -------------------------------------------------------------------------------- -- | An operator is a pair of -- * a 'Char' containing its identifier; and -- * a binary numeral function. type Operator t = (Char, Complex t -> Complex t -> Complex t) -------------------------------------------------------------------------------- -- | A constant is a pair of -- * a 'String' containing its identifier; and -- * a 'Complex' number containing its value. type Constant t = (String, Complex t) -------------------------------------------------------------------------------- -- | A configuration. 'def' is the default configuration. data Config t = Config { operators :: [Operator t] -- ^ A list of operators. The order in this list also determines the order of evaluation. , constants :: [Constant t] -- ^ A list of constants. } instance (Read t, RealFloat t) => Default (Config t) where def = Config [ ('+', (+)) , ('-', (-)) , ('*', (*)) , ('/', (/)) , ('^', (**)) , ('?', flip logBase) ] [ ("pi", pi :+0) , ("e" , exp 1:+0) , ("i" , 0 :+1) ] -------------------------------------------------------------------------------- -- | Given a configuration and a 'String'-expression, returns a 'String' -- containing the error message or the resulting complex number. showHascal :: (Show t, Read t, RealFloat t) => Config t -> String -> String showHascal conf s = either id showNumber (hascal conf s) -------------------------------------------------------------------------------- -- | Given a configuration and a 'String'-expression, returns 'Either' a -- 'String' containing an error message; or a 'Complex' number. hascal :: (Read t, RealFloat t) => Config t -> String -> Result t hascal conf = calc (operators conf) (constants conf) -------------------------------------------------------------------------------- -- Given a list of operators, a list of constants, and a 'String'-expression, -- returns 'Either' a 'String' containing an error message; or a 'Complex' -- number. calc :: (Read t, RealFloat t) => [Operator t] -> [Constant t] -> String -> Result t calc [] cs s = readNumber cs s calc ((c, f) : ops) cs s = foldl1 apply (map (calc ops cs) (splitOn [c] s)) where apply x y = case (x, y) of (Right rx, Right ry) -> Right (f rx ry) (Left rx, _ ) -> Left rx (_ , Left ry) -> Left ry -------------------------------------------------------------------------------- -- 'Either' returns a 'String' containing an error message or returns a -- complex number represented in the given 'String' as a 'Constant' or as a -- number. It’s a wrapper around 'findOrRead', adding the underscore prefix -- operator for negation. readNumber :: (Read t, RealFloat t) => [Constant t] -> String -> Result t readNumber cs ('_':s) = fmap negate (findOrRead cs s) readNumber cs s = findOrRead cs s -------------------------------------------------------------------------------- -- 'Either' returns a 'String' containing an error message or returns a -- complex number represented in the given 'String' as a 'Constant' or as a -- number. findOrRead :: (Read t, Floating t) => [Constant t] -> String -> Result t findOrRead cs s = maybe (maybeRead s) (Right . snd) (find ((==s) . fst) cs) where maybeRead s | any (null . snd) (reads s :: [(Double,String)]) -- TODO: solve this better = Right (read s:+0) | otherwise = Left ("Error at \"" ++ s ++ "\".") -------------------------------------------------------------------------------- -- | Show a 'Complex' number a little bit more human-readable by matching both -- the real and the imaginery part against zero and one. showNumber :: (Show t, RealFloat t) => Complex t -> String showNumber (r:+0) = show r showNumber (r:+1) = show r ++ " + i" showNumber (0:+i) = show i ++ " * i" showNumber (r:+i) = show r ++ " + " ++ show i ++ " * i"