The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

module Pugs.Parser.Number (
    parseNatOrRat,
    naturalOrRat,
    signedNaturalOrRat,
) where
import Pugs.Internals
import Pugs.Rule
import Text.ParserCombinators.Parsec.Char

parseNatOrRat :: String -> Either ParseError (Either Integer (Ratio Integer))
parseNatOrRat s = runParser signedNaturalOrRat () "" s

signedNaturalOrRat :: GenParser Char st (Either Integer (Ratio Integer))
signedNaturalOrRat = do
    sig <- sign
    if sig then naturalOrRat else do
        num <- naturalOrRat
        return $ case num of
            Left i  -> Left (-i)
            Right d -> Right (-d)

naturalOrRat :: GenParser Char st (Either Integer (Ratio Integer))
naturalOrRat = (<?> "number") $ do
        try (char '0' >> zeroNumRat)
    <|> decimalRat
    <|> fractRatOnly
    where
    zeroNumRat = do
            n <- hexadecimal <|> decimal <|> octalBad <|> octal <|> binary
            return (Left n)
        <|> decimalRat
        <|> fractRat 0
        <|> return (Left 0)

    decimalRat = do
        n <- decimalLiteral
        option (Left n) (try $ fractRat n)

    fractRatOnly = do
        fract <- try fraction
        expo  <- option (1%1) expo
        return (Right $ fract * expo) -- Right is Rat

    fractRat n = do
            fract <- try fraction
            expo  <- option (1%1) expo
            return (Right $ ((n % 1) + fract) * expo) -- Right is Rat
        <|> do
            expo <- expo
            if expo < 1
                then return (Right $ (n % 1) * expo)
                else return (Right $ (n % 1) * expo)

    fraction = do
            char '.'
            digit  <- satisfy isDigit
            digits <- many (satisfy isWordDigit) <?> "fraction"
            return (digitsToRat $ filter (/= '_') (digit:digits))
        <?> "fraction"
        where
        digitsToRat d = digitsNum d % (10 ^ length d)
        digitsNum d = foldl (\x y -> x * 10 + (toInteger $ digitToInt y)) 0 d
        isWordDigit x = (isDigit x || x == '_')

    expo :: GenParser Char st Rational
    expo = do
            oneOf "eE"
            f <- sign
            e <- decimalLiteral <?> "exponent"
            return (power (if f then e else -e))
        <?> "exponent"
        where
        power e | e < 0      = 1 % (10^abs(e))
                | otherwise  = (10^e) % 1

    decimalLiteral         = number 10
    hexadecimal     = do{ char 'x'; number 16  }
    decimal         = do{ oneOf "_d"; number 10  }
    octal           = do{ char 'o'; number 8 }
    octalBad        = do{ many1 octDigit ; fail "0100 is not octal in perl6 any more, use 0o100 instead." }
    binary          = do{ char 'b'; number 2  }

    number base = do
        d   <- baseDigit base
        ds  <- many (baseDigit base <|> 
                do { char '_'; lookAhead (baseDigit base); return '_' })
        let n = foldl (\x d -> base*x + b36DigitToInteger d) 0 digits
            digits = (d : filter (/= '_') ds)
        seq n (return n)
        where
        baseDigit                   = baseDigitInt . fromIntegral
        baseDigitInt b 
            | b <= 10               = oneOf $ take b ['0'..'9']
            | b >  10 && b <= 36    = oneOf $ ['0'..'9'] 
                                    ++ take (b - 10) ['a'..'z'] 
                                    ++ take (b - 10) ['A'..'Z']
            | otherwise             = error "baseDigitInt: base too large"
        b36DigitToInteger           = toInteger . b36DigitToInt
        b36DigitToInt c
            | isDigit c             = fromEnum c - fromEnum '0'
            | c >= 'a' && c <= 'z'  = fromEnum c - fromEnum 'a' + 10
            | c >= 'A' && c <= 'Z'  = fromEnum c - fromEnum 'A' + 10
            | otherwise             = error "b36DigitToInt: not a base 36 digit"

sign :: GenParser Char st Bool
sign = (char '-' >> return False)
   <|> (char '+' >> return True)
   <|> return True