Search code examples
parsinghaskellparsecparser-combinators

How to implement a floating point parser feature in Haskell code for integer processing?


Referring to my assignment's description (I'm student with only basic experience in Haskell), I have to make a simple calculator parser utilizing Text.Parsec. So far that program can read some string input to execute parsing through integer values only, for instance:

parseTest addition "5 + 8 / 4"

There is a complete code of the program I've actually:

import Text.Parsec hiding(digit)
import Data.Functor

type CalcParser a = Parsec String () a

digit :: CalcParser Char
digit = oneOf ['0'..'9']

number :: CalcParser Integer
number = read <$> many1 digit

fp_char :: CalcParser String 
fp_char = many1 digit


applyMany :: a -> [a -> a] -> a
applyMany x [] = x
applyMany x (h:t) = applyMany (h x) t


div_ :: CalcParser (Integer -> Integer -> Integer)
div_= do
    char '/'
    return div

star :: CalcParser (Integer -> Integer -> Integer)
star = do
    char '*'    
    return (*)  

plus :: CalcParser (Integer -> Integer -> Integer)
plus = do
    char '+'    
    return (+)

minus :: CalcParser (Integer -> Integer -> Integer)
minus = do
    char '-'    
    return (-)  

multiply :: CalcParser Integer
multiply = do
    spaces
    lhv <- enclosed
    spaces
    t <- many tail
    return $ applyMany lhv t
    where tail =
                do
                    f <- star <|> div_
                    spaces
                    rhv <- enclosed
                    spaces
                    return (`f` rhv)


add :: CalcParser Integer
add = do
    spaces
    lhv <- multiply <|> fact' <|> negation'
    spaces
    t <- many tail
    return $ applyMany lhv t
    where tail =
                do
                    f <- plus <|> minus
                    spaces
                    rhv <- multiply <|> fact' <|> negation'
                    spaces
                    return (`f` rhv)

enclosed :: CalcParser Integer
enclosed = number <|> do
    char '('
    res <- add
    char ')'
    return res

-- factorial    
fact' :: CalcParser Integer
fact' = do
    spaces
    char '!'
    rhv <- number
    return $ factorial rhv  

factorial :: Integer -> Integer
factorial n
    | n < 0 = error "No factorial exists for negative inputs" 
    | n == 0 || n == 1 = 1
    | otherwise = acc n 1
    where
    acc 0 a = a
    acc b a = acc (b-1) (b * a)  

-- negation 
negation' :: CalcParser Integer
negation' = do
    spaces
    char '~'
    rhv <- enclosed
    spaces
    return $ negate rhv

Listing above includes function definitions for primary operations extended with negation and factorial calculation options. What I need, it's just to make this program sensitive to floating point values as well as to integers in any string input. How could I implement that to start parser by calling an only function (applicable to both Fractional and Integral numbers) as follows (just for example):

parseTest totalCalc "~(8.44 * 12.85 / 3.2) * !4"

I input '!' character in prior to a numeric part of factorial notation, because parser doesn't appear to recognize normal '4!' or similar character sequences as factorial indicators.


Solution

  • Step 1: search+replace all occurrences of Integer to Double. Now your parser still can only read integers, but internally it will represent them as Doubles.

    Step 2: make the number parser parse either a whole number or a floating-point one. The whole number you already got down: it's just a sequence of digits. Let's rename it to better reflect what it's doing:

    parseInt :: CalcParser Double
    parseInt = read <$> many1 digit
    

    The floating-point is not much more difficult: it's a sequence of digits, followed by a dot (period), followed by another sequence of digits:

    parseDouble :: CalcParser Double
    parseDouble = do
        whole <- many1 digit
        char '.'
        fract <- many1 digit
        pure $ read $ whole <> "." <> fract
    

    And then any number would just be "either double or int":

    number :: CalcParser Double
    number = try parseDouble <|> parseInt
    

    Two more notes:

    First, note that you have to try the double first. If you don't, the string "8.32" will be parsed as an int, because the prefix "8" matches the rules for parseInt.

    Second, note that you have to use try with parseDouble. If you don't, whole numbers will fail to parse, because the parseDouble parser will consume the input up to the end of the digits, won't see a dot, will fail, but will not roll back to the beginning of the digits, so that parseInt will not see any digits and will fail too. The try combinator is what does the roll back to the start when the parser fails.