parent
36ee6214c2
commit
da33ab0f18
18 changed files with 1513 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||||||
|
# Chapter Exercises |
||||||
|
1. see src/SemVer.hs |
||||||
|
2. see src/PosInt.hs |
||||||
|
3. see src/PosInt.hs |
||||||
|
4. see src/PhoneNumber.hs |
||||||
|
5. see src/ParseLog.hs |
||||||
|
6. see src/IPAddress.hs |
||||||
|
7. see src/IPAddress.hs |
||||||
|
8. see src/IPAddress.hs |
||||||
|
9. see src/IPAddress.hs |
||||||
|
10. Not done (yet). |
@ -0,0 +1,2 @@ |
|||||||
|
# Exercises: Parsing Practise |
||||||
|
see src/LearnParsers.hs |
@ -0,0 +1,4 @@ |
|||||||
|
# Exercise: Unit of Success |
||||||
|
`(>>)` is actually `(*>)` from `Applicative` and `Applicative` also has `(<*)` which has type `Applicative f => f a -> f b -> f a` |
||||||
|
|
||||||
|
So the solution here is: `integer <* eof` |
@ -0,0 +1,2 @@ |
|||||||
|
# Exercise: Try Try |
||||||
|
see src/TryTry.hs |
@ -0,0 +1,41 @@ |
|||||||
|
{-# LANGUAGE QuasiQuotes #-} |
||||||
|
module AltParsing where |
||||||
|
|
||||||
|
import Control.Applicative |
||||||
|
import Text.RawString.QQ |
||||||
|
import Text.Trifecta |
||||||
|
|
||||||
|
type NumberOrString = Either Integer String |
||||||
|
|
||||||
|
a :: String |
||||||
|
a = "blah" |
||||||
|
b :: String |
||||||
|
b = "123" |
||||||
|
c :: String |
||||||
|
c = "123blah789" |
||||||
|
|
||||||
|
eitherOr :: String |
||||||
|
eitherOr = [r| |
||||||
|
123 |
||||||
|
abc |
||||||
|
456 |
||||||
|
def |
||||||
|
|] |
||||||
|
|
||||||
|
parseNos :: Parser NumberOrString |
||||||
|
parseNos = do |
||||||
|
skipMany (oneOf "\n") |
||||||
|
v <- (Left <$> integer) <|> (Right <$> some letter) |
||||||
|
skipMany (oneOf "\n") |
||||||
|
return v |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = do |
||||||
|
let p f i = parseString f mempty i |
||||||
|
print $ p (some letter) a |
||||||
|
print $ p integer b |
||||||
|
print $ p parseNos a |
||||||
|
print $ p parseNos b |
||||||
|
print $ p (many parseNos) c |
||||||
|
print $ p (some parseNos) c |
||||||
|
print $ p (some parseNos) eitherOr |
@ -0,0 +1,48 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
module BT where |
||||||
|
|
||||||
|
import Control.Applicative |
||||||
|
import qualified Data.Attoparsec.ByteString as A |
||||||
|
import Data.Attoparsec.ByteString (parseOnly) |
||||||
|
import Data.ByteString (ByteString) |
||||||
|
import Text.Trifecta hiding (parseTest) |
||||||
|
import Text.Parsec (Parsec, parseTest) |
||||||
|
|
||||||
|
-- Helper function to run a trifecta parser and print the result |
||||||
|
trifP :: Show a => Parser a -> String -> IO () |
||||||
|
trifP p i = print $ parseString p mempty i |
||||||
|
|
||||||
|
-- Helper function to run a parsec parser and print the result |
||||||
|
parsecP :: Show a => Parsec String () a -> String -> IO () |
||||||
|
parsecP = parseTest |
||||||
|
|
||||||
|
-- Helper function to runan attoparsec and print the result |
||||||
|
attoP :: Show a => A.Parser a -> ByteString -> IO () |
||||||
|
attoP p i = print $ parseOnly p i |
||||||
|
|
||||||
|
-- Parsers |
||||||
|
nobackParse :: (Monad f, CharParsing f) => f Char |
||||||
|
nobackParse = (char '1' >> char '2') <|> char '3' |
||||||
|
|
||||||
|
tryParse :: (Monad f, CharParsing f) => f Char |
||||||
|
tryParse = try (char '1' >> char '2') <|> char '3' |
||||||
|
|
||||||
|
tryAnnot :: (Monad f, CharParsing f) => f Char |
||||||
|
tryAnnot = (try (char '1' >> char '2') <?> "Tried 12") |
||||||
|
<|> (char '3' <?> "Tried 3") |
||||||
|
|
||||||
|
-- Main |
||||||
|
main :: IO () |
||||||
|
main = do |
||||||
|
-- trifecta |
||||||
|
trifP nobackParse "13" |
||||||
|
trifP tryParse "13" |
||||||
|
trifP tryAnnot "13" |
||||||
|
-- parsec |
||||||
|
parsecP nobackParse "13" |
||||||
|
parsecP tryParse "13" |
||||||
|
parsecP tryAnnot "13" |
||||||
|
-- attoparsec |
||||||
|
attoP nobackParse "13" |
||||||
|
attoP tryParse "13" |
||||||
|
attoP tryAnnot "13" |
@ -0,0 +1,70 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
module Text.Fractions where |
||||||
|
|
||||||
|
import Data.Attoparsec.Text (parseOnly) |
||||||
|
import Data.Ratio ((%)) |
||||||
|
import Data.String (IsString) |
||||||
|
import Text.Trifecta |
||||||
|
|
||||||
|
badFraction :: IsString a => a |
||||||
|
badFraction = "1/0" |
||||||
|
alsoBad :: IsString a => a |
||||||
|
alsoBad = "10" |
||||||
|
shouldWork :: IsString a => a |
||||||
|
shouldWork = "1/2" |
||||||
|
shouldAlsoWork :: IsString a => a |
||||||
|
shouldAlsoWork = "2/1" |
||||||
|
|
||||||
|
-- parseFraction :: Parser Rational |
||||||
|
-- parseFraction = do |
||||||
|
-- numerator <- decimal |
||||||
|
-- char '/' |
||||||
|
-- denominator <- decimal |
||||||
|
-- return (numerator % denominator) |
||||||
|
|
||||||
|
-- virtuousFraction :: Parser Rational |
||||||
|
-- virtuousFraction = do |
||||||
|
-- numerator <- decimal |
||||||
|
-- char '/' |
||||||
|
-- denominator <- decimal |
||||||
|
-- case denominator of |
||||||
|
-- 0 -> fail "Denominator cannot be zero" |
||||||
|
-- _ -> return (numerator % denominator) |
||||||
|
|
||||||
|
parseFraction :: (Monad m, TokenParsing m) => m Rational |
||||||
|
parseFraction = do |
||||||
|
numerator <- decimal |
||||||
|
char '/' |
||||||
|
denominator <- decimal |
||||||
|
case denominator of |
||||||
|
0 -> fail "Denominator cannot be zero" |
||||||
|
_ -> return (numerator % denominator) |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = do |
||||||
|
-- parseOnly is Attoparsec |
||||||
|
let attoP = parseOnly parseFraction |
||||||
|
print $ attoP badFraction |
||||||
|
print $ attoP shouldWork |
||||||
|
print $ attoP shouldAlsoWork |
||||||
|
print $ attoP alsoBad |
||||||
|
|
||||||
|
-- parseString is Trifecta |
||||||
|
let p = parseString parseFraction mempty |
||||||
|
print $ p badFraction |
||||||
|
print $ p shouldWork |
||||||
|
print $ p shouldAlsoWork |
||||||
|
print $ p alsoBad |
||||||
|
-- let p = parseString parseFraction mempty |
||||||
|
-- print $ p shouldWork |
||||||
|
-- print $ p shouldAlsoWork |
||||||
|
-- print $ p alsoBad |
||||||
|
-- print $ p badFraction |
||||||
|
|
||||||
|
-- testVirtuous :: IO () |
||||||
|
-- testVirtuous = do |
||||||
|
-- let virtuousFraction' = parseString virtuousFraction mempty |
||||||
|
-- print $ virtuousFraction' shouldWork |
||||||
|
-- print $ virtuousFraction' shouldAlsoWork |
||||||
|
-- print $ virtuousFraction' alsoBad |
||||||
|
-- print $ virtuousFraction' badFraction |
@ -0,0 +1,4 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
|
||||||
|
module GraphViz where |
||||||
|
|
@ -0,0 +1,274 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
|
||||||
|
module IPAddress where |
||||||
|
|
||||||
|
import Text.Trifecta hiding (span) |
||||||
|
import Data.Word |
||||||
|
import Data.Bits |
||||||
|
import Data.List (splitAt) |
||||||
|
import Numeric (readHex, showHex) |
||||||
|
import Control.Monad.State |
||||||
|
import Data.Maybe (fromMaybe) |
||||||
|
-- import Data.List (group, span) |
||||||
|
import Data.Either (lefts) |
||||||
|
-- import qualified Data.Sequence as S |
||||||
|
|
||||||
|
import Test.Hspec |
||||||
|
|
||||||
|
-- Exercise 6 |
||||||
|
data IPAddress = IPAddress Word32 deriving (Eq, Ord) |
||||||
|
instance Show IPAddress where |
||||||
|
show (IPAddress w32) = |
||||||
|
let n = toInteger w32 |
||||||
|
in show ((n `shiftR` 24) .&. 0xFF) ++ "." ++ |
||||||
|
show ((n `shiftR` 16) .&. 0xFF) ++ "." ++ |
||||||
|
show ((n `shiftR` 8) .&. 0xFF) ++ "." ++ |
||||||
|
show (n .&. 0xFF) |
||||||
|
|
||||||
|
parseValidIPByte :: Parser Word8 |
||||||
|
parseValidIPByte = do |
||||||
|
i <- decimal |
||||||
|
let max' = toInteger (maxBound :: Word8) |
||||||
|
min' = toInteger (minBound :: Word8) |
||||||
|
if i <= max' && i >= min' |
||||||
|
then return (fromInteger i) |
||||||
|
else unexpected "Invalid byte" |
||||||
|
|
||||||
|
parseIPv4Address :: Parser IPAddress |
||||||
|
parseIPv4Address = do |
||||||
|
b1 <- toInteger <$> parseValidIPByte |
||||||
|
_ <- char '.' |
||||||
|
b2 <- toInteger <$> parseValidIPByte |
||||||
|
_ <- char '.' |
||||||
|
b3 <- toInteger <$> parseValidIPByte |
||||||
|
_ <- char '.' |
||||||
|
b4 <- toInteger <$> parseValidIPByte |
||||||
|
return $ IPAddress . fromInteger $ (b1 `shiftL` 24) + |
||||||
|
(b2 `shiftL` 16) + |
||||||
|
(b3 `shiftL` 8) + |
||||||
|
b4 |
||||||
|
|
||||||
|
maybeSuccess :: Result a -> Maybe a |
||||||
|
maybeSuccess (Success a) = Just a |
||||||
|
maybeSuccess _ = Nothing |
||||||
|
|
||||||
|
testByteHelper :: String -> Maybe Word8 -> Expectation |
||||||
|
testByteHelper s r = do |
||||||
|
let m = parseString parseValidIPByte mempty s |
||||||
|
r' = maybeSuccess m |
||||||
|
r' `shouldBe` r |
||||||
|
|
||||||
|
testIPv4Helper :: String -> Maybe IPAddress -> Expectation |
||||||
|
testIPv4Helper s r = do |
||||||
|
let m = parseString parseIPv4Address mempty s |
||||||
|
r' = maybeSuccess m |
||||||
|
r' `shouldBe` r |
||||||
|
|
||||||
|
testByte :: IO () |
||||||
|
testByte = hspec $ do |
||||||
|
describe "Test parsing of byte" $ do |
||||||
|
it "Can parse 0" $ testByteHelper "0" (Just 0) |
||||||
|
it "Can parse 255" $ testByteHelper "255" (Just 255) |
||||||
|
it "Can't parse 256" $ testByteHelper "256" Nothing |
||||||
|
it "Can't parse -1" $ testByteHelper "-1" Nothing |
||||||
|
it "Can't parse empty" $ testByteHelper "" Nothing |
||||||
|
|
||||||
|
testIPv4 :: IO () |
||||||
|
testIPv4 = hspec $ do |
||||||
|
describe "Test parsing of IPv4" $ do |
||||||
|
it "Can parse 0.0.0.0" $ do |
||||||
|
testIPv4Helper "0.0.0.0" (Just (IPAddress 0)) |
||||||
|
it "Can parse 255.255.255.255" $ do |
||||||
|
testIPv4Helper "255.255.255.255" (Just (IPAddress (2^32 - 1))) |
||||||
|
it "Can parse 0.0.0.255.1" $ do |
||||||
|
testIPv4Helper "0.0.0.255.1" (Just (IPAddress 255)) |
||||||
|
it "Can't parse .0.0.0.0" $ do |
||||||
|
testIPv4Helper ".0.0.0.0" Nothing |
||||||
|
it "Can't parse 0..0.0.0" $ do |
||||||
|
testIPv4Helper "0..0.0.0" Nothing |
||||||
|
|
||||||
|
toIPAddress6 :: IPAddress -> IPAddress6 |
||||||
|
toIPAddress6 (IPAddress w32) = IPAddress6 0 (fromIntegral w32) |
||||||
|
|
||||||
|
-- Exercise 7 |
||||||
|
|
||||||
|
-- Datatypes: |
||||||
|
data IPAddress6 = IPAddress6 Word64 Word64 deriving (Eq, Ord) |
||||||
|
-- An IPv6Piece is the part between ':', it can be either |
||||||
|
-- "::" or "ABCD" |
||||||
|
type IPv6Piece = Either () Word16 |
||||||
|
|
||||||
|
-- |
||||||
|
toArray :: IPAddress6 -> [Word16] |
||||||
|
toArray (IPAddress6 w64 w64') = |
||||||
|
let n = toInteger w64 |
||||||
|
m = toInteger w64' |
||||||
|
mask = fromInteger . (\a -> (.&.) a 0xFFFF) |
||||||
|
in mask (n `shiftR` 48) : |
||||||
|
mask (n `shiftR` 32) : |
||||||
|
mask (n `shiftR` 16) : |
||||||
|
mask n : |
||||||
|
mask (m `shiftR` 48) : |
||||||
|
mask (m `shiftR` 32) : |
||||||
|
mask (m `shiftR` 16) : |
||||||
|
mask m : [] |
||||||
|
|
||||||
|
toArray' :: [Word16] -> [Either Int Word16] |
||||||
|
toArray' [] = [] |
||||||
|
toArray' s@(0:0:_) = (Left $ length ys) : (toArray' zs) |
||||||
|
where (ys, zs) = span (==0) s |
||||||
|
toArray' (x:xs) = (Right x) : toArray' xs |
||||||
|
|
||||||
|
findMax :: [Either Int Word16] -> Maybe Int |
||||||
|
findMax xs = go $ lefts xs |
||||||
|
where go [] = Nothing |
||||||
|
go ys = Just $ maximum ys |
||||||
|
|
||||||
|
replaceLeft :: Int -> [Either Int Word16] -> [IPv6Piece] |
||||||
|
replaceLeft m xs = go m xs False |
||||||
|
where go _ [] _ = [] |
||||||
|
go i ((Right c):ys) b = (Right c) : (go i ys b) |
||||||
|
go i ((Left c):ys) False = |
||||||
|
if (c == i) |
||||||
|
then (Left ()) : (go i ys True) |
||||||
|
else (replicate c (Right 0)) ++ (go i ys False) |
||||||
|
go i ((Left c):ys) True = (replicate c (Right 0)) ++ (go i ys True) |
||||||
|
|
||||||
|
showPieces :: [IPv6Piece] -> String |
||||||
|
showPieces [] = "" |
||||||
|
showPieces ((Right c):ys) = (showHex c "") ++ (go ys) |
||||||
|
where go [] = [] |
||||||
|
go (Right c':zs) = ":" ++ (showHex c' "") ++ (go zs) |
||||||
|
go zs = showPieces zs |
||||||
|
showPieces ((Left ():ys)) = "::" ++ (showPieces ys) |
||||||
|
|
||||||
|
|
||||||
|
instance Show IPAddress6 where |
||||||
|
show p = let xs = toArray' (toArray p) |
||||||
|
in showPieces $ (case findMax xs of |
||||||
|
Nothing -> replaceLeft 0 xs |
||||||
|
(Just i) -> replaceLeft i xs) |
||||||
|
|
||||||
|
validHex :: String |
||||||
|
validHex = "abcdefABCDEF0123456789" |
||||||
|
|
||||||
|
-- Helper functions |
||||||
|
-- only parses [0-9A-Fa-f]{1,4} |
||||||
|
readWord16 :: String -> Maybe Word16 |
||||||
|
readWord16 s = |
||||||
|
let r = readHex (take 4 s) :: [(Word16, String)] |
||||||
|
in case r of |
||||||
|
((w,s'):[]) -> if s' == "" then Just w else Nothing |
||||||
|
_ -> Nothing |
||||||
|
|
||||||
|
mkWord64 :: Word16 -> Word16 -> Word16 -> Word16 -> Word64 |
||||||
|
mkWord64 a b c d = (fromIntegral a `shiftL` 48) + |
||||||
|
(fromIntegral b `shiftL` 32) + |
||||||
|
(fromIntegral c `shiftL` 16) + |
||||||
|
(fromIntegral d) |
||||||
|
|
||||||
|
mkIPAddress6 :: [Word16] -> Maybe IPAddress6 |
||||||
|
mkIPAddress6 xs = |
||||||
|
case length xs of |
||||||
|
8 -> let (a,b) = splitAt 4 xs |
||||||
|
w1 = (mkWord64 (a !! 0) (a !! 1) (a !! 2) (a !! 3)) |
||||||
|
w2 = (mkWord64 (b !! 0) (b !! 1) (b !! 2) (b !! 3)) |
||||||
|
in Just $ IPAddress6 w1 w2 |
||||||
|
_ -> Nothing |
||||||
|
|
||||||
|
expandIPv6Pieces :: [IPv6Piece] -> Maybe [Word16] |
||||||
|
expandIPv6Pieces xs = |
||||||
|
case length $ filter (==(Left ())) xs of |
||||||
|
0 -> Just $ foldr f [] xs |
||||||
|
1 -> Just $ foldr g [] xs |
||||||
|
_ -> Nothing |
||||||
|
where f (Left _) s = s |
||||||
|
f (Right c) s = c:s |
||||||
|
n = max (9 - (length xs)) 0 |
||||||
|
g (Left _) s = (take n $ repeat 0) ++ s |
||||||
|
g (Right c) s = c:s |
||||||
|
|
||||||
|
getIPAddress6 :: [IPv6Piece] -> Maybe IPAddress6 |
||||||
|
getIPAddress6 xs = expandIPv6Pieces xs >>= mkIPAddress6 |
||||||
|
|
||||||
|
type IPv6State = Bool |
||||||
|
type IPv6Parser a = StateT IPv6State Parser a |
||||||
|
|
||||||
|
-- Parsing the individual pieces: |
||||||
|
-- hex, 1-4 hexes, ':', '::' |
||||||
|
parseSingleHex :: IPv6Parser Char |
||||||
|
parseSingleHex = do |
||||||
|
c <- oneOf validHex |
||||||
|
return c |
||||||
|
|
||||||
|
parseIPv6Hex :: IPv6Parser Word16 |
||||||
|
parseIPv6Hex = do |
||||||
|
c1 <- parseSingleHex <?> "At least one hex" |
||||||
|
o <- mapStateT f $ count 3 $ optional parseSingleHex |
||||||
|
let s = c1 : (fromMaybe "" o) |
||||||
|
case readWord16 s of |
||||||
|
Just w -> return w |
||||||
|
Nothing -> unexpected $ "Invalid piece: " ++ s |
||||||
|
where f :: Parser ([Maybe Char], IPv6State) |
||||||
|
-> Parser (Maybe [Char], IPv6State) |
||||||
|
f = fmap (\(c,b) -> (sequence c, b)) |
||||||
|
|
||||||
|
parseSep :: IPv6Parser String |
||||||
|
parseSep = do |
||||||
|
c <- string ":" |
||||||
|
return c |
||||||
|
|
||||||
|
parseCons :: IPv6Parser (Maybe IPv6Piece) |
||||||
|
parseCons = do |
||||||
|
b <- get |
||||||
|
c <- optional $ string "::" |
||||||
|
case c of |
||||||
|
Nothing -> return Nothing |
||||||
|
Just _ -> if b |
||||||
|
then unexpected "Only one '::' allowed" |
||||||
|
else do |
||||||
|
put True |
||||||
|
return (Just $ Left ()) |
||||||
|
|
||||||
|
-- Parses: |
||||||
|
-- ::ABCD |
||||||
|
-- :ABCD |
||||||
|
parseInnerParts :: IPv6Parser [IPv6Piece] |
||||||
|
parseInnerParts = do |
||||||
|
c <- parseCons |
||||||
|
case c of |
||||||
|
Nothing -> do |
||||||
|
parseSep |
||||||
|
w <- parseIPv6Hex |
||||||
|
return [Right w] |
||||||
|
Just _ -> do |
||||||
|
w <- parseIPv6Hex |
||||||
|
return [Left (), Right w] |
||||||
|
|
||||||
|
|
||||||
|
parseIPv6Address :: IPv6Parser IPAddress6 |
||||||
|
parseIPv6Address = do |
||||||
|
c <- parseCons |
||||||
|
w <- parseIPv6Hex |
||||||
|
a <- case c of |
||||||
|
Nothing -> do |
||||||
|
xs <- mapStateT f $ many parseInnerParts |
||||||
|
c' <- parseCons |
||||||
|
case c' of |
||||||
|
Nothing -> return ([Right w] ++ xs) |
||||||
|
Just _ -> return ([Right w] ++ xs ++ [Left ()]) |
||||||
|
Just _ -> do |
||||||
|
xs <- mapStateT f $ many parseInnerParts |
||||||
|
return $ ([Left ()] ++ [Right w] ++ xs) |
||||||
|
case getIPAddress6 a of |
||||||
|
Nothing -> unexpected "Invalid format" |
||||||
|
Just a' -> return a' |
||||||
|
where f :: Parser ([[IPv6Piece]], IPv6State) |
||||||
|
-> Parser ([IPv6Piece], IPv6State) |
||||||
|
f = fmap (\(xs, b) -> (concat xs, b)) |
||||||
|
|
||||||
|
toIPAddress :: IPAddress6 -> Maybe IPAddress |
||||||
|
toIPAddress (IPAddress6 w64 w64') |
||||||
|
| w64 /= 0 = Nothing |
||||||
|
| w64' > (fromIntegral $ (maxBound :: Word32)) = Nothing |
||||||
|
| otherwise = Just $ IPAddress (fromIntegral w64') |
@ -0,0 +1,152 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
{-# LANGUAGE QuasiQuotes #-} |
||||||
|
|
||||||
|
module Data.Ini where |
||||||
|
|
||||||
|
import Control.Applicative |
||||||
|
import Data.ByteString (ByteString) |
||||||
|
import Data.Char (isAlpha) |
||||||
|
import Data.Map (Map) |
||||||
|
import qualified Data.Map as M |
||||||
|
import Data.Text (Text) |
||||||
|
import qualified Data.Text.IO as TIO |
||||||
|
import Test.Hspec |
||||||
|
import Text.RawString.QQ |
||||||
|
import Text.Trifecta |
||||||
|
|
||||||
|
headerEx :: ByteString |
||||||
|
headerEx = "[blah]" |
||||||
|
|
||||||
|
newtype Header = Header String deriving (Eq, Ord, Show) |
||||||
|
|
||||||
|
parseBracketPair :: Parser a -> Parser a |
||||||
|
parseBracketPair p = char '[' *> p <* char ']' |
||||||
|
|
||||||
|
parseHeader :: Parser Header |
||||||
|
parseHeader = parseBracketPair (Header <$> some letter) |
||||||
|
|
||||||
|
assignmentEx :: ByteString |
||||||
|
assignmentEx = "woot=1" |
||||||
|
|
||||||
|
type Name = String |
||||||
|
type Value = String |
||||||
|
type Assignments = Map Name Value |
||||||
|
|
||||||
|
parseAssignment :: Parser (Name, Value) |
||||||
|
parseAssignment = do |
||||||
|
name <- some letter |
||||||
|
_ <- char '=' |
||||||
|
val <- some (noneOf "\n") |
||||||
|
skipEOL -- important! |
||||||
|
return (name, val) |
||||||
|
|
||||||
|
-- | Skip end of lien and whitespace beyond. |
||||||
|
skipEOL :: Parser () |
||||||
|
skipEOL = skipMany (oneOf "\n") |
||||||
|
|
||||||
|
commentEx :: ByteString |
||||||
|
commentEx = "; last modified 11 November 2017 by John Doe" |
||||||
|
|
||||||
|
commentEx' :: ByteString |
||||||
|
commentEx' = "; blah\n; woot\n \n; hah" |
||||||
|
|
||||||
|
-- | Skip comments starting at the beginning of the line |
||||||
|
skipComments :: Parser () |
||||||
|
skipComments = skipMany $ do |
||||||
|
_ <- char ';' <|> char '#' |
||||||
|
skipMany (noneOf "\n") |
||||||
|
skipEOL |
||||||
|
|
||||||
|
sectionEx :: ByteString |
||||||
|
sectionEx = "; ignore me\n[languages]\nGael=Haskell" |
||||||
|
|
||||||
|
sectionEx' :: ByteString |
||||||
|
sectionEx' = [r| |
||||||
|
; ignore me |
||||||
|
[languages] |
||||||
|
Gael=Haskell |
||||||
|
|] |
||||||
|
|
||||||
|
sectionEx'' :: ByteString |
||||||
|
sectionEx'' = [r| |
||||||
|
; comment |
||||||
|
[section] |
||||||
|
host=wikipedia.org |
||||||
|
alias=claw |
||||||
|
|
||||||
|
[whatisit] |
||||||
|
red=intoothandclaw |
||||||
|
|] |
||||||
|
|
||||||
|
data Section = Section Header Assignments deriving (Eq, Show) |
||||||
|
|
||||||
|
newtype Config = Config (Map Header Assignments) deriving (Eq, Show) |
||||||
|
|
||||||
|
skipWhitespace :: Parser () |
||||||
|
skipWhitespace = skipMany (char ' ' <|> char '\n') |
||||||
|
|
||||||
|
parseSection :: Parser Section |
||||||
|
parseSection = do |
||||||
|
skipWhitespace |
||||||
|
skipComments |
||||||
|
h <- parseHeader |
||||||
|
skipEOL |
||||||
|
assignments <- some parseAssignment |
||||||
|
return $ Section h (M.fromList assignments) |
||||||
|
|
||||||
|
rollup :: Section -> Map Header Assignments -> Map Header Assignments |
||||||
|
rollup (Section h a) m = M.insert h a m |
||||||
|
|
||||||
|
parseIni :: Parser Config |
||||||
|
parseIni = do |
||||||
|
sections <- some parseSection |
||||||
|
let mapOfSections = foldr rollup M.empty sections |
||||||
|
return $ Config mapOfSections |
||||||
|
|
||||||
|
maybeSuccess :: Result a -> Maybe a |
||||||
|
maybeSuccess (Success a) = Just a |
||||||
|
maybeSuccess _ = Nothing |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = hspec $ do |
||||||
|
describe "Assignment Parsing" $ |
||||||
|
it "can parse a simple assignment" $ do |
||||||
|
let m = parseByteString parseAssignment mempty assignmentEx |
||||||
|
r' = maybeSuccess m |
||||||
|
print m |
||||||
|
r' `shouldBe` Just ("woot", "1") |
||||||
|
describe "Header Parsing" $ |
||||||
|
it "can parse a simple header" $ do |
||||||
|
let m = parseByteString parseHeader mempty headerEx |
||||||
|
r' = maybeSuccess m |
||||||
|
print m |
||||||
|
r' `shouldBe` Just (Header "blah") |
||||||
|
describe "Comment parsing" $ |
||||||
|
it "Skips comment before header" $ do |
||||||
|
let p = skipComments >> parseHeader |
||||||
|
i = "; woot\n[blah]" |
||||||
|
m = parseByteString p mempty i |
||||||
|
r' = maybeSuccess m |
||||||
|
print m |
||||||
|
r' `shouldBe` Just (Header "blah") |
||||||
|
describe "Section parsing" $ |
||||||
|
it "can parse a simple section" $ do |
||||||
|
let m = parseByteString parseSection mempty sectionEx |
||||||
|
r' = maybeSuccess m |
||||||
|
languages = M.fromList [("Gael", "Haskell")] |
||||||
|
expected' = Just (Section (Header "languages") languages) |
||||||
|
print m |
||||||
|
r' `shouldBe` expected' |
||||||
|
describe "INI parsing" $ |
||||||
|
it "Can parse multiple sections" $ do |
||||||
|
let m = parseByteString parseIni mempty sectionEx'' |
||||||
|
r' = maybeSuccess m |
||||||
|
sectionValues = M.fromList |
||||||
|
[ ("alias", "claw") , ("host", "wikipedia.org")] |
||||||
|
whatisitValues = M.fromList [("red", "intoothandclaw")] |
||||||
|
expected' = Just (Config (M.fromList [ (Header "section" |
||||||
|
, sectionValues) |
||||||
|
, (Header "whatisit" |
||||||
|
, whatisitValues)])) |
||||||
|
print m |
||||||
|
r' `shouldBe` expected' |
@ -0,0 +1,81 @@ |
|||||||
|
module LearnParsers where |
||||||
|
|
||||||
|
import Text.Trifecta |
||||||
|
import Text.Parser.Combinators (eof) |
||||||
|
|
||||||
|
import Control.Applicative ((<|>)) |
||||||
|
|
||||||
|
stop :: Parser a |
||||||
|
stop = unexpected "stop" |
||||||
|
|
||||||
|
one :: Parser Char |
||||||
|
one = char '1' |
||||||
|
|
||||||
|
one' :: Parser Char |
||||||
|
one' = one >> stop |
||||||
|
|
||||||
|
oneTwo :: Parser Char |
||||||
|
oneTwo = char '1' >> char '2' |
||||||
|
|
||||||
|
oneTwo' :: Parser Char |
||||||
|
oneTwo' = oneTwo >> stop |
||||||
|
|
||||||
|
testParse :: Show a => Parser a -> IO () |
||||||
|
testParse p = print $ parseString p mempty "123" |
||||||
|
|
||||||
|
-- testEOFParse :: Parser () -> IO () |
||||||
|
-- testEOFParse p = print $ parseString p mempty "123" |
||||||
|
|
||||||
|
pNL :: String -> IO () |
||||||
|
pNL s = putStrLn ('\n' : s) |
||||||
|
|
||||||
|
-- 1 |
||||||
|
one'' :: Parser () |
||||||
|
one'' = one >> eof |
||||||
|
|
||||||
|
oneTwo'' :: Parser () |
||||||
|
oneTwo'' = oneTwo >> eof |
||||||
|
|
||||||
|
-- 2 |
||||||
|
-- Looking at the documentation you can combine strings with <|> |
||||||
|
oneTwoThree :: Parser String |
||||||
|
-- Put the biggest string first as it matches from from left to right |
||||||
|
oneTwoThree = string "123" <|> string "12" <|> string "1" |
||||||
|
|
||||||
|
-- Other solution is to use choice to create a parser from other parser |
||||||
|
-- choice :: [Parser a] -> Parser a |
||||||
|
oneTwoThree' :: Parser String |
||||||
|
oneTwoThree' = choice [string "1", string "12", string "123"] |
||||||
|
|
||||||
|
-- 3 |
||||||
|
-- Using foldr: |
||||||
|
string' :: String -> Parser String |
||||||
|
string' = foldr (\c p -> (:) c <$> p) (pure []) |
||||||
|
|
||||||
|
-- We haven't used the `char` function, if we do want to use it, we have |
||||||
|
-- to use <*>: |
||||||
|
string'' :: String -> Parser String |
||||||
|
string'' = foldr (\c p -> (:) <$> char c <*> p) (pure []) |
||||||
|
|
||||||
|
-- Which is basically: |
||||||
|
string''' :: String -> Parser String |
||||||
|
string''' = traverse char |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = do |
||||||
|
pNL "stop:" |
||||||
|
testParse (stop :: Parser Char) |
||||||
|
pNL "one:" |
||||||
|
testParse one |
||||||
|
pNL "one':" |
||||||
|
testParse one' |
||||||
|
pNL "oneTwo:" |
||||||
|
testParse oneTwo |
||||||
|
pNL "oneTwo'" |
||||||
|
testParse oneTwo' |
||||||
|
pNL "one'':" |
||||||
|
testParse one'' |
||||||
|
pNL "oneTwo'':" |
||||||
|
testParse oneTwo'' |
@ -0,0 +1,53 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
{-# LANGUAGE QuasiQuotes #-} |
||||||
|
|
||||||
|
module Marshalling where |
||||||
|
|
||||||
|
import Control.Applicative |
||||||
|
import Data.Aeson |
||||||
|
import Data.ByteString.Lazy (ByteString) |
||||||
|
import qualified Data.Text as T |
||||||
|
import Data.Text (Text) |
||||||
|
import Text.RawString.QQ |
||||||
|
|
||||||
|
sectionJson :: ByteString |
||||||
|
sectionJson = [r| |
||||||
|
{ "section": {"host": "wikipedia.org"}, |
||||||
|
"whatisit": {"red": "intoothandclaw"} |
||||||
|
} |
||||||
|
|] |
||||||
|
|
||||||
|
data TestData = TestData { section :: Host, what :: Color } deriving (Eq, Show) |
||||||
|
|
||||||
|
instance FromJSON TestData where |
||||||
|
parseJSON (Object v) = |
||||||
|
TestData <$> v .: "section" |
||||||
|
<*> v .: "whatisit" |
||||||
|
parseJSON _ = fail "Expected an object for TestData" |
||||||
|
|
||||||
|
newtype Host = Host String deriving (Eq, Show) |
||||||
|
|
||||||
|
instance FromJSON Host where |
||||||
|
parseJSON (Object v) = |
||||||
|
Host <$> v .: "host" |
||||||
|
parseJSON _ = fail "Expected an object for Host" |
||||||
|
|
||||||
|
type Annotation = String |
||||||
|
|
||||||
|
data Color = Red Annotation |
||||||
|
| Blue Annotation |
||||||
|
| Yellow Annotation |
||||||
|
deriving (Eq, Show) |
||||||
|
|
||||||
|
instance FromJSON Color where |
||||||
|
parseJSON (Object v) = |
||||||
|
(Red <$> v .: "red") <|> |
||||||
|
(Blue <$> v .: "blue") <|> |
||||||
|
(Yellow <$> v .: "yellow") |
||||||
|
parseJSON _ = fail "Expected an object for Color" |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = do |
||||||
|
let d :: Maybe TestData |
||||||
|
d = decode sectionJson |
||||||
|
print d |
@ -0,0 +1,31 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
{-# LANGUAGE QuasiQuotes #-} |
||||||
|
|
||||||
|
module NumberOrString where |
||||||
|
|
||||||
|
import Control.Applicative |
||||||
|
import Data.Aeson |
||||||
|
import Data.ByteString.Lazy (ByteString) |
||||||
|
import qualified Data.Text as T |
||||||
|
import Data.Text (Text) |
||||||
|
import Text.RawString.QQ |
||||||
|
import Data.Scientific (floatingOrInteger) |
||||||
|
|
||||||
|
data NumberOrString = Numba Integer| Stringy Text deriving (Eq, Show) |
||||||
|
instance FromJSON NumberOrString where |
||||||
|
parseJSON (Number i) = |
||||||
|
case floatingOrInteger i of |
||||||
|
(Left _) -> fail "Must be integral number" |
||||||
|
(Right integer) -> return $ Numba integer |
||||||
|
parseJSON (String s) = return $ Stringy s |
||||||
|
parseJSON _ = fail "NumberOrString must be number or string" |
||||||
|
|
||||||
|
dec :: ByteString -> Maybe NumberOrString |
||||||
|
dec = decode |
||||||
|
|
||||||
|
eitherDec :: ByteString -> Either String NumberOrString |
||||||
|
eitherDec = eitherDecode |
||||||
|
|
||||||
|
main = do |
||||||
|
print $ dec "blah" |
||||||
|
print $ eitherDec "blah" |
@ -0,0 +1,195 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
{-# LANGUAGE QuasiQuotes #-} |
||||||
|
|
||||||
|
module ParseLog where |
||||||
|
|
||||||
|
import Control.Applicative ((<|>), liftA2) |
||||||
|
import Data.Monoid ((<>)) |
||||||
|
import Text.Trifecta |
||||||
|
import Data.Time.Format |
||||||
|
import Data.Time |
||||||
|
import Data.List (intersperse) |
||||||
|
import Text.RawString.QQ |
||||||
|
|
||||||
|
import Test.QuickCheck |
||||||
|
|
||||||
|
data LogEntry = LogEntry TimeOfDay String deriving Eq |
||||||
|
instance Show LogEntry where |
||||||
|
show (LogEntry t s) = |
||||||
|
let time = formatTime defaultTimeLocale "%H:%M" t |
||||||
|
in time ++ " " ++ s |
||||||
|
instance Arbitrary LogEntry where |
||||||
|
arbitrary = do |
||||||
|
tod <- TimeOfDay <$> choose (0,23) <*> choose (0,59) <*> pure 0 |
||||||
|
s <- liftA2 (++) |
||||||
|
arbitrary |
||||||
|
(oneof [((++) "--") <$> arbitrary, arbitrary]) |
||||||
|
return $ LogEntry tod (filter (\c -> c /= '\n') s) |
||||||
|
|
||||||
|
data DayEntry = DayEntry Day [LogEntry] deriving Eq |
||||||
|
instance Show DayEntry where |
||||||
|
show (DayEntry d es) = |
||||||
|
let day = formatTime defaultTimeLocale "%Y-%m-%d" d |
||||||
|
entries = concat . intersperse "\n" . map show $ es |
||||||
|
in "# " ++ day ++ "\n" ++ entries |
||||||
|
instance Arbitrary DayEntry where |
||||||
|
arbitrary = do |
||||||
|
day <- ModifiedJulianDay <$> (2000 +) <$> arbitrary |
||||||
|
es <- listOf arbitrary |
||||||
|
return $ DayEntry day es |
||||||
|
|
||||||
|
data Log = Log [DayEntry] deriving Eq |
||||||
|
instance Show Log where |
||||||
|
show (Log ds) = concat . intersperse "\n\n" . map show $ ds |
||||||
|
instance Arbitrary Log where |
||||||
|
arbitrary = do |
||||||
|
es <- listOf arbitrary |
||||||
|
return $ Log es |
||||||
|
|
||||||
|
countTimeSpentDay :: [LogEntry] -> DiffTime |
||||||
|
countTimeSpentDay [] = 0 |
||||||
|
countTimeSpentDay (_:[]) = 0 |
||||||
|
countTimeSpentDay (x:y:xs) = |
||||||
|
let (LogEntry t1 _) = x |
||||||
|
(LogEntry t2 _) = y |
||||||
|
in (timeOfDayToTime t2) |
||||||
|
- (timeOfDayToTime t1) |
||||||
|
+ (countTimeSpentDay (y:xs)) |
||||||
|
|
||||||
|
countDay :: DayEntry -> DiffTime |
||||||
|
countDay (DayEntry _ e) = countTimeSpentDay e |
||||||
|
|
||||||
|
countTime :: Log -> DiffTime |
||||||
|
countTime (Log ds) = sum $ map countDay ds |
||||||
|
|
||||||
|
skipBlankLines :: Parser () |
||||||
|
skipBlankLines = |
||||||
|
skipMany $ (skipWhiteSpace >> skipComment >> newline) <|> |
||||||
|
(skipWhiteSpace >> newline) |
||||||
|
|
||||||
|
skipComment :: Parser () |
||||||
|
skipComment = skipMany $ do |
||||||
|
_ <- count 2 $ char '-' |
||||||
|
skipMany $ notChar '\n' |
||||||
|
|
||||||
|
skipWhiteSpace :: Parser () |
||||||
|
skipWhiteSpace = skipMany $ oneOf "\t " |
||||||
|
|
||||||
|
removeComment :: String -> String |
||||||
|
removeComment [] = [] |
||||||
|
removeComment (x:xs) = go "" x False xs |
||||||
|
where go s '-' True _ = s -- skip comment |
||||||
|
go s '-' False [] = s ++ "-" -- ended with '-' |
||||||
|
go s c _ [] = s ++ [c] -- ended without comment |
||||||
|
go s '-' False (y:ys) = go s y True ys -- possible start of comment |
||||||
|
go s c _ (y:ys) = go (s ++ [c]) y False ys |
||||||
|
|
||||||
|
|
||||||
|
parseLogEntry :: Parser LogEntry |
||||||
|
parseLogEntry = do |
||||||
|
t <- count 2 digit <> string ":" <> count 2 digit |
||||||
|
let m = parseTimeM True defaultTimeLocale "%H:%M" t |
||||||
|
timeOfDay <- case m of |
||||||
|
Just time -> return time |
||||||
|
Nothing -> unexpected "Incorrect time format" |
||||||
|
_ <- space |
||||||
|
s <- some $ notChar '\n' |
||||||
|
_ <- newline |
||||||
|
return $ LogEntry timeOfDay (removeComment s) |
||||||
|
|
||||||
|
parseDayEntry :: Parser DayEntry |
||||||
|
parseDayEntry = do |
||||||
|
_ <- char '#' |
||||||
|
_ <- space |
||||||
|
d <- some digit <> string "-" |
||||||
|
<> count 2 digit <> string "-" |
||||||
|
<> count 2 digit |
||||||
|
let m = parseTimeM True defaultTimeLocale "%Y-%m-%d" d |
||||||
|
day <- case m of |
||||||
|
Just day' -> return day' |
||||||
|
Nothing -> unexpected "Incorrect day format" |
||||||
|
-- only thing allowed after day is whitespace and comments |
||||||
|
skipWhiteSpace |
||||||
|
skipComment |
||||||
|
_ <- newline |
||||||
|
-- Followed by a list of paserLogEntries |
||||||
|
logEntries <- many parseLogEntry |
||||||
|
skipBlankLines |
||||||
|
return $ DayEntry day logEntries |
||||||
|
|
||||||
|
parseLog :: Parser Log |
||||||
|
parseLog = do |
||||||
|
skipBlankLines |
||||||
|
dayEntries <- many parseDayEntry |
||||||
|
return $ Log dayEntries |
||||||
|
|
||||||
|
testLog :: String |
||||||
|
testLog = [r|-- wheee a comment |
||||||
|
|
||||||
|
|
||||||
|
# 2025-02-05 |
||||||
|
08:00 Breakfast |
||||||
|
09:00 Sanitizing moisture collector |
||||||
|
11:00 Exercising in high-grav gym |
||||||
|
12:00 Lunch |
||||||
|
13:00 Programming |
||||||
|
17:00 Commuting home in rover |
||||||
|
17:30 R&R |
||||||
|
19:00 Dinner |
||||||
|
21:00 Shower |
||||||
|
21:15 Read |
||||||
|
22:00 Sleep |
||||||
|
|
||||||
|
# 2025-02-07 -- dates not nececessarily sequential |
||||||
|
08:00 Breakfast -- should I try skippin bfast? |
||||||
|
09:00 Bumped head, passed out |
||||||
|
13:36 Wake up, headache |
||||||
|
13:37 Go to medbay |
||||||
|
13:40 Patch self up |
||||||
|
13:45 Commute home for rest |
||||||
|
14:15 Read |
||||||
|
21:00 Dinner |
||||||
|
21:15 Read |
||||||
|
22:00 Sleep |
||||||
|
|] |
||||||
|
|
||||||
|
-- Generator for Log files |
||||||
|
genBlankLine :: Gen String |
||||||
|
genBlankLine = oneof [return "\t -- comment!\n", return " \t\n"] |
||||||
|
genDayLine :: Gen String |
||||||
|
genDayLine = do |
||||||
|
day <- ModifiedJulianDay <$> (2000 +) <$> arbitrary |
||||||
|
let day_string = formatTime defaultTimeLocale "%Y-%m-%d" day |
||||||
|
oneof [ return $ "# " ++ day_string ++ "\t \n" |
||||||
|
, return $ "# " ++ day_string ++ "\t -- comment!\n" ] |
||||||
|
genLogEntry :: Gen String |
||||||
|
genLogEntry = do |
||||||
|
time <- TimeOfDay <$> choose (0,23) <*> choose (0,59) <*> pure 0 |
||||||
|
let time_string = formatTime defaultTimeLocale "%H:%M" time |
||||||
|
oneof [ return $ time_string ++ " some description \t \n" |
||||||
|
, return $ time_string ++ " some description \t -- with comment\n"] |
||||||
|
genDayEntry :: Gen String |
||||||
|
genDayEntry = do |
||||||
|
day <- genDayLine |
||||||
|
logEntry <- genLogEntry -- at least one log entry |
||||||
|
logEntryList <- concat <$> listOf genLogEntry |
||||||
|
blankLines <- concat <$> listOf genBlankLine |
||||||
|
return $ day ++ logEntry ++ logEntryList ++ blankLines |
||||||
|
genLog :: Gen String |
||||||
|
genLog = do |
||||||
|
blankLines <- concat <$> listOf genBlankLine |
||||||
|
dayEntries <- concat <$> listOf genDayEntry |
||||||
|
return $ blankLines ++ dayEntries |
||||||
|
|
||||||
|
newtype LogExample = LogExample String deriving (Show) |
||||||
|
instance Arbitrary LogExample where |
||||||
|
arbitrary = LogExample <$> genLog |
||||||
|
|
||||||
|
propLog :: LogExample -> Bool |
||||||
|
propLog (LogExample s) = case parseString parseLog mempty s of |
||||||
|
Text.Trifecta.Success _ -> True |
||||||
|
_ -> False |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = do |
||||||
|
quickCheck propLog |
@ -0,0 +1,172 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
|
||||||
|
module PhoneNumber where |
||||||
|
|
||||||
|
import Control.Applicative ((<|>)) |
||||||
|
import Text.Trifecta |
||||||
|
import Test.Hspec |
||||||
|
|
||||||
|
-- This is an exercise for the Belgian Phone numbers system |
||||||
|
-- From wikipedia |
||||||
|
{- |
||||||
|
Belgian telephone numbers consist of two major parts: Firstly '0', secondly the |
||||||
|
"zone prefix" (A) which is 1 or 2 digits long for landlines and 3 digits long |
||||||
|
for mobile phones and thirdly the "subscriber's number" (B). |
||||||
|
|
||||||
|
Land lines are always 9 digits long. They are prefixed by a zero, followed by |
||||||
|
the zone prefix. Depending on the length of the zone prefix, the subscriber's |
||||||
|
number consists of either 6 or 7 digits. Hence land line numbers are written |
||||||
|
either 0AA BB BB BB or 0A BBB BB BB. |
||||||
|
|
||||||
|
Mobile Phone numbers always consist of 10 digits. The first digit of the "zone |
||||||
|
prefix" of a mobile number is always '4'. Then follows 2 digits indicating to |
||||||
|
which Mobile Operator's pool the number originally belonged when it was taken |
||||||
|
into usage. The fourth digit represents a "sub-group" of this pool and has no |
||||||
|
additional meaning other than increasing the amount of possible numbers. The |
||||||
|
subscriber's number consists of 6 digits. Hence, mobile phone numbers are |
||||||
|
written 04AA BB BB BB. Sometimes, the last 6 digits are written in two groups |
||||||
|
of 3 digits to increase readability: 04AA BBB BBB. |
||||||
|
|
||||||
|
Numbers are sometimes written with a slash in between the zone prefix and the |
||||||
|
subscriber's number. This is the case for both land lines and mobile phone |
||||||
|
numbers. Sometimes, dots are written between the blocks of the subscriber's |
||||||
|
number. Examples: 0AA/BB BB BB, 0AA/BB.BB.BB; for mobile numbers: |
||||||
|
04AA/BB BB BB, 04AA/BB.BB.BB or 04AA/BBB.BBB. |
||||||
|
|
||||||
|
The international country code prefix for Belgium is "+32". When dialing a |
||||||
|
number with the prefix, the 0 can be dropped, e.g.: +32 4AA BB BB BB. |
||||||
|
-} |
||||||
|
|
||||||
|
-- see also https://en.wikipedia.org/wiki/Telephone_numbers_in_Belgium |
||||||
|
|
||||||
|
|
||||||
|
-- Will only look at landlines and phonelines |
||||||
|
-- not special numbers or non-geographic numbers |
||||||
|
data ZonePrefix = MobileZP String |
||||||
|
| LandOneZP String |
||||||
|
| LandTwoZP String |
||||||
|
deriving (Eq) |
||||||
|
instance Show ZonePrefix where |
||||||
|
show (MobileZP s) = "0" ++ s |
||||||
|
show (LandOneZP s) = " 0" ++ s |
||||||
|
show (LandTwoZP s) = " 0" ++ s |
||||||
|
|
||||||
|
data SubscribersNumber = MobileSN String |
||||||
|
| LandOneSN String |
||||||
|
| LandTwoSN String |
||||||
|
deriving (Eq) |
||||||
|
instance Show SubscribersNumber where |
||||||
|
show (LandOneSN (a:b:c:d:e:f:g:[])) = a:b:c:" " ++ d:e:" " ++ f:g:[] |
||||||
|
show (LandOneSN s) = "Invalid format: " ++ s |
||||||
|
show (MobileSN (a:b:c:d:e:f:[])) = " " ++ a:b:" " ++ c:d:" " ++ e:f:[] |
||||||
|
show (MobileSN s) = "Invalid format: " ++ s |
||||||
|
show (LandTwoSN (a:b:c:d:e:f:[])) = " " ++ a:b:" " ++ c:d:" " ++ e:f:[] |
||||||
|
show (LandTwoSN s) = "Invalid format: " ++ s |
||||||
|
|
||||||
|
data PhoneNumberBE = PhoneNumberBE ZonePrefix SubscribersNumber deriving Eq |
||||||
|
instance Show PhoneNumberBE where |
||||||
|
show (PhoneNumberBE z s) = "(PhoneNumberBE " ++ show z |
||||||
|
++ " " |
||||||
|
++ show s |
||||||
|
++ ")" |
||||||
|
|
||||||
|
|
||||||
|
-- Mobile numbers are: 046x 047x 048x 049x |
||||||
|
-- Land lines are most other things where: |
||||||
|
-- 02,03,04 and 09 are single digit land line area codes |
||||||
|
-- the rest are two digit land line phone |
||||||
|
parseZonePrefix :: Parser ZonePrefix |
||||||
|
parseZonePrefix = |
||||||
|
let mobile = MobileZP <$> do |
||||||
|
one <- char '4' |
||||||
|
two <- oneOf ['6', '7', '8', '9'] |
||||||
|
three <- digit |
||||||
|
return $ [one, two, three] |
||||||
|
landOne = LandOneZP . (:[]) <$> oneOf ['2', '3', '4', '9'] |
||||||
|
landTwo = LandTwoZP <$> count 2 digit |
||||||
|
-- Order is important, we want to check mobile first, followed by |
||||||
|
-- landOne. Everything else is assumed to be land two |
||||||
|
in mobile <|> landOne <|> landTwo |
||||||
|
|
||||||
|
parseMobileSN :: Parser SubscribersNumber |
||||||
|
parseMobileSN = do |
||||||
|
let p = count 2 digit |
||||||
|
skip = skipOptional $ char ' ' <|> char '.' |
||||||
|
p1 <- p |
||||||
|
skip |
||||||
|
p2 <- p |
||||||
|
skip |
||||||
|
p3 <- p |
||||||
|
return . MobileSN $ p1 ++ p2 ++ p3 |
||||||
|
|
||||||
|
parseLandTwoSN :: Parser SubscribersNumber |
||||||
|
parseLandTwoSN = do |
||||||
|
let p = count 2 digit |
||||||
|
skip = skipOptional $ char ' ' <|> char '.' |
||||||
|
p1 <- p |
||||||
|
skip |
||||||
|
p2 <- p |
||||||
|
skip |
||||||
|
p3 <- p |
||||||
|
return . LandTwoSN $ p1 ++ p2 ++ p3 |
||||||
|
|
||||||
|
parseLandOneSN :: Parser SubscribersNumber |
||||||
|
parseLandOneSN = do |
||||||
|
let p = count 2 digit |
||||||
|
skip = skipOptional $ char ' ' <|> char '.' |
||||||
|
p1 <- count 3 digit |
||||||
|
skip |
||||||
|
p2 <- p |
||||||
|
skip |
||||||
|
p3 <- p |
||||||
|
return . LandTwoSN $ p1 ++ p2 ++ p3 |
||||||
|
|
||||||
|
parsePhone :: Parser PhoneNumberBE |
||||||
|
parsePhone = do |
||||||
|
-- Country code |
||||||
|
m <- optional $ string "+32" |
||||||
|
-- white space |
||||||
|
skipMany $ char ' ' |
||||||
|
-- Leading zero depends on country code |
||||||
|
skipOptional $ char ' ' |
||||||
|
_ <- case m of |
||||||
|
Just _ -> do |
||||||
|
a <- optional $ char '0' |
||||||
|
case a of |
||||||
|
Nothing -> return Nothing |
||||||
|
Just _ -> unexpected "0 not expected here" |
||||||
|
Nothing -> Just <$> char '0' |
||||||
|
z <- parseZonePrefix |
||||||
|
skipOptional $ char ' ' <|> char '/' |
||||||
|
s <- case z of |
||||||
|
MobileZP _ -> parseMobileSN |
||||||
|
LandOneZP _ -> parseLandOneSN |
||||||
|
LandTwoZP _ -> parseLandTwoSN |
||||||
|
return $ PhoneNumberBE z s |
||||||
|
|
||||||
|
maybeSuccess :: Result a -> Maybe a |
||||||
|
maybeSuccess (Success a) = Just a |
||||||
|
maybeSuccess _ = Nothing |
||||||
|
|
||||||
|
-- Should test all options for mobile and single-digit land lines, |
||||||
|
-- but that is just copy-paste... QuickCheck could also help. |
||||||
|
main :: IO () |
||||||
|
main = hspec $ do |
||||||
|
describe "Parse Zone Prefix" $ do |
||||||
|
it "Mobile starts with 4[6-9]x " $ do |
||||||
|
let m = parseString parseZonePrefix mempty "472" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just (MobileZP "472") |
||||||
|
it "Incorrect mobile zone prefix should fail" $ do |
||||||
|
let m = parseString parseZonePrefix mempty "47" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "Land line with single digit" $ do |
||||||
|
let m = parseString parseZonePrefix mempty "2" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just (LandOneZP "2") |
||||||
|
it "Land line with double digit" $ do |
||||||
|
let m = parseString parseZonePrefix mempty "55" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just (LandTwoZP "55") |
||||||
|
-- ... |
@ -0,0 +1,121 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
|
||||||
|
module PosInt where |
||||||
|
|
||||||
|
import Text.Trifecta |
||||||
|
import Test.Hspec |
||||||
|
|
||||||
|
parseDigit :: Parser Char |
||||||
|
parseDigit = oneOf ['0'..'9'] |
||||||
|
|
||||||
|
base10Integer :: Parser Integer |
||||||
|
base10Integer = do |
||||||
|
xs <- some $ (\a -> read (a:"")) <$> parseDigit |
||||||
|
return $ |
||||||
|
fst $ foldr (\a (b,i) -> (a * i + b, i*10)) (0,1) xs |
||||||
|
|
||||||
|
parseSign :: Parser Char |
||||||
|
parseSign = oneOf ['+', '-'] |
||||||
|
|
||||||
|
base10Integer' :: Parser Integer |
||||||
|
base10Integer' = do |
||||||
|
c <- option '+' parseSign |
||||||
|
let f = if c == '-' then negate else id |
||||||
|
f <$> base10Integer |
||||||
|
|
||||||
|
maybeSuccess :: Result a -> Maybe a |
||||||
|
maybeSuccess (Success a) = Just a |
||||||
|
maybeSuccess _ = Nothing |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = hspec $ do |
||||||
|
describe "Parse Digit" $ do |
||||||
|
it "can parse a 0" $ do |
||||||
|
let m = parseString parseDigit mempty "0" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '0' |
||||||
|
it "can parse a 1" $ do |
||||||
|
let m = parseString parseDigit mempty "1" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '1' |
||||||
|
it "can parse a 2" $ do |
||||||
|
let m = parseString parseDigit mempty "2" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '2' |
||||||
|
it "can parse a 3" $ do |
||||||
|
let m = parseString parseDigit mempty "3" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '3' |
||||||
|
it "can parse a 4" $ do |
||||||
|
let m = parseString parseDigit mempty "4" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '4' |
||||||
|
it "can parse a 5" $ do |
||||||
|
let m = parseString parseDigit mempty "5" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '5' |
||||||
|
it "can parse a 6" $ do |
||||||
|
let m = parseString parseDigit mempty "6" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '6' |
||||||
|
it "can parse a 7" $ do |
||||||
|
let m = parseString parseDigit mempty "7" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '7' |
||||||
|
it "can parse a 8" $ do |
||||||
|
let m = parseString parseDigit mempty "8" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '8' |
||||||
|
it "can parse a 9" $ do |
||||||
|
let m = parseString parseDigit mempty "9" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '9' |
||||||
|
it "other values fail" $ do |
||||||
|
let m = parseString parseDigit mempty "a" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "Parse Integer" $ do |
||||||
|
it "can parse single digit" $ do |
||||||
|
let m = parseString base10Integer mempty "3" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just 3 |
||||||
|
it "can parse multiple digits" $ do |
||||||
|
let m = parseString base10Integer mempty "123" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just 123 |
||||||
|
it "fails when no digit" $ do |
||||||
|
let m = parseString base10Integer mempty "x23" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "Parse Sign" $ do |
||||||
|
it "Minus" $ do |
||||||
|
let m = parseString parseSign mempty "-" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '-' |
||||||
|
it "Plus" $ do |
||||||
|
let m = parseString parseSign mempty "+" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just '+' |
||||||
|
it "other" $ do |
||||||
|
let m = parseString parseSign mempty "1" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "Parse Positive Integer" $ do |
||||||
|
it "positive without +" $ do |
||||||
|
let m = parseString base10Integer' mempty "123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just 123 |
||||||
|
it "positive with -" $ do |
||||||
|
let m = parseString base10Integer' mempty "+123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just 123 |
||||||
|
|
||||||
|
describe "Parse negative integer" $ do |
||||||
|
it "negative" $ do |
||||||
|
let m = parseString base10Integer' mempty "-123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
r `shouldBe` Just (-123) |
||||||
|
|
@ -0,0 +1,229 @@ |
|||||||
|
{-# LANGUAGE OverloadedStrings #-} |
||||||
|
|
||||||
|
module SemVer where |
||||||
|
|
||||||
|
import Data.Char (isDigit) |
||||||
|
import Text.Trifecta |
||||||
|
import Test.Hspec |
||||||
|
|
||||||
|
data NumberOrString = NOSS String | NOSI Integer deriving (Eq, Show) |
||||||
|
|
||||||
|
type Major = Integer |
||||||
|
type Minor = Integer |
||||||
|
type Patch = Integer |
||||||
|
type Release = [NumberOrString] |
||||||
|
type Metadata = [NumberOrString] |
||||||
|
|
||||||
|
data SemVer = SemVer Major Minor Patch Release Metadata deriving (Eq, Show) |
||||||
|
|
||||||
|
-- Probably nicer ways of doing this? |
||||||
|
instance Ord SemVer where |
||||||
|
compare (SemVer major minor patch _ _) |
||||||
|
(SemVer major' minor' patch' _ _) = |
||||||
|
-- case compare major major' of |
||||||
|
-- GT -> GT |
||||||
|
-- LT -> LT |
||||||
|
-- EQ -> |
||||||
|
-- case compare minor minor' of |
||||||
|
-- GT -> GT |
||||||
|
-- LT -> LT |
||||||
|
-- EQ -> |
||||||
|
-- case compare patch patch' of |
||||||
|
-- GT -> GT |
||||||
|
-- LT -> LT |
||||||
|
-- EQ -> EQ |
||||||
|
mconcat $ |
||||||
|
zipWith compare [major, minor, patch] [major', minor', patch'] |
||||||
|
|
||||||
|
validChars :: [Char] |
||||||
|
validChars = ['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z'] |
||||||
|
|
||||||
|
-- According to SemVer identifiers comprise of [0-9a-zA-Z] |
||||||
|
-- Looks like the exercises wants us to differentiate between numbers |
||||||
|
-- and strings, even though it can be mixed: e.g 1.0.0-beta+exp.sha.5114f85 |
||||||
|
-- Therefore we will only treat it as a number if the entire identifier can |
||||||
|
-- be identified as a number |
||||||
|
parseNumberOrString :: Parser NumberOrString |
||||||
|
parseNumberOrString = do |
||||||
|
s <- some $ oneOf validChars |
||||||
|
return $ if all isDigit s |
||||||
|
then NOSI (read s) |
||||||
|
else NOSS s |
||||||
|
|
||||||
|
parserNumberOrStringList :: Parser [NumberOrString] |
||||||
|
parserNumberOrStringList = do |
||||||
|
x <- parseNumberOrString |
||||||
|
xs <- many $ char '.' >> parseNumberOrString |
||||||
|
return $ x : xs |
||||||
|
|
||||||
|
parseRelease :: Parser Release |
||||||
|
parseRelease = char '-' >> parserNumberOrStringList |
||||||
|
|
||||||
|
parseMetadata :: Parser Metadata |
||||||
|
parseMetadata = char '+' >> parserNumberOrStringList |
||||||
|
|
||||||
|
parseSemVer :: Parser SemVer |
||||||
|
parseSemVer = do |
||||||
|
major <- decimal |
||||||
|
_ <- char '.' |
||||||
|
minor <- decimal |
||||||
|
_ <- char '.' |
||||||
|
patch <- decimal |
||||||
|
rel <- option [] parseRelease |
||||||
|
metadata <- option [] parseMetadata |
||||||
|
_ <- eof -- not really necessary, but just makes it nice to test :) |
||||||
|
return $ SemVer major minor patch rel metadata |
||||||
|
|
||||||
|
maybeSuccess :: Result a -> Maybe a |
||||||
|
maybeSuccess (Success a) = Just a |
||||||
|
maybeSuccess _ = Nothing |
||||||
|
|
||||||
|
main :: IO () |
||||||
|
main = hspec $ do |
||||||
|
describe "Number or String parsing" $ do |
||||||
|
it "can parse string" $ do |
||||||
|
let m = parseString parseNumberOrString mempty "abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (NOSS "abc") |
||||||
|
it "can parse number" $ do |
||||||
|
let m = parseString parseNumberOrString mempty "123" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (NOSI 123) |
||||||
|
it "can parse mixed number and string" $ do |
||||||
|
let m = parseString parseNumberOrString mempty "123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (NOSS "123abc") |
||||||
|
it "fails on non alphanumeric" $ do |
||||||
|
let m = parseString parseNumberOrString mempty "+" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "Number or String parsing" $ do |
||||||
|
it "can parse one number or string" $ do |
||||||
|
let m = parseString parserNumberOrStringList mempty "123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just [NOSS "123abc"] |
||||||
|
it "can parse more than one number or string" $ do |
||||||
|
let m = parseString parserNumberOrStringList mempty "123.abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just [NOSI 123, NOSS "abc"] |
||||||
|
it "fails when no number or string" $ do |
||||||
|
let m = parseString parserNumberOrStringList mempty "+" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "Release parsing" $ do |
||||||
|
it "can parse release" $ do |
||||||
|
let m = parseString parseRelease mempty "-123.abc.123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just [NOSI 123, NOSS "abc", NOSS "123abc"] |
||||||
|
it "fails when doesn't start with '-'" $ do |
||||||
|
let m = parseString parseRelease mempty "123.abc.123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "Metadata parsing" $ do |
||||||
|
it "can parse metadata" $ do |
||||||
|
let m = parseString parseMetadata mempty "+123.abc.123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just [NOSI 123, NOSS "abc", NOSS "123abc"] |
||||||
|
it "fails when doesn't start with '+'" $ do |
||||||
|
let m = parseString parseRelease mempty "123.abc.123abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
|
||||||
|
describe "SemVer parsing" $ do |
||||||
|
it "can parse just version" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (SemVer 1 2 3 [] []) |
||||||
|
it "fails on invalid version" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "fails with invalid data after version" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3[" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "can parse with release" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3-abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (SemVer 1 2 3 [NOSS "abc"] []) |
||||||
|
it "fails with more than one release" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3-abc-abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "fails with invalid data after release" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3-abc]" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "can parse with metadata" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3+abc" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (SemVer 1 2 3 [] [NOSS "abc"]) |
||||||
|
it "fails with more than one metadata" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3+abc+123" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "fails with non number or string after release" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3+abc[" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Nothing |
||||||
|
it "can parse with release and metadata" $ do |
||||||
|
let m = parseString parseSemVer mempty "1.2.3-abc+123" |
||||||
|
r = maybeSuccess m |
||||||
|
print m |
||||||
|
r `shouldBe` Just (SemVer 1 2 3 [NOSS "abc"] [NOSI 123]) |
||||||
|
|
||||||
|
describe "Comparing SemVer" $ do |
||||||
|
it "Bigger Major" $ do |
||||||
|
let a = SemVer 2 1 0 [] [] |
||||||
|
b = SemVer 1 2 1 [] [] |
||||||
|
compare a b `shouldBe` GT |
||||||
|
it "Smaller Major" $ do |
||||||
|
let a = SemVer 1 2 1 [] [] |
||||||
|
b = SemVer 2 1 0 [] [] |
||||||
|
compare a b `shouldBe` LT |
||||||
|
it "Bigger Minor" $ do |
||||||
|
let a = SemVer 1 2 0 [] [] |
||||||
|
b = SemVer 1 1 1 [] [] |
||||||
|
compare a b `shouldBe` GT |
||||||
|
it "Smaller Major" $ do |
||||||
|
let a = SemVer 1 1 1 [] [] |
||||||
|
b = SemVer 1 2 0 [] [] |
||||||
|
compare a b `shouldBe` LT |
||||||
|
it "Bigger Patch" $ do |
||||||
|
let a = SemVer 1 1 2 [] [] |
||||||
|
b = SemVer 1 1 1 [] [] |
||||||
|
compare a b `shouldBe` GT |
||||||
|
it "Smaller Patch" $ do |
||||||
|
let a = SemVer 1 1 1 [] [] |
||||||
|
b = SemVer 1 1 2 [] [] |
||||||
|
compare a b `shouldBe` LT |
||||||
|
it "Equal Patch" $ do |
||||||
|
let a = SemVer 1 1 1 [] [] |
||||||
|
compare a a `shouldBe` EQ |
||||||
|
it "Release and Major don't matter" $ do |
||||||
|
let a = SemVer 1 1 1 [NOSI 3] [NOSI 3] |
||||||
|
b = SemVer 1 1 1 [NOSI 2, NOSI 1] [NOSI 2, NOSI 1] |
||||||
|
compare a b `shouldBe` EQ |
@ -0,0 +1,23 @@ |
|||||||
|
module TryTry where |
||||||
|
|
||||||
|
import Control.Applicative ((<|>)) |
||||||
|
import Text.Trifecta |
||||||
|
import Data.Ratio ((%)) |
||||||
|
|
||||||
|
parseFraction :: Parser Rational |
||||||
|
parseFraction = do |
||||||
|
numerator <- decimal |
||||||
|
char '/' |
||||||
|
denominator <- decimal |
||||||
|
case denominator of |
||||||
|
0 -> fail "Denominator cannot be zero" |
||||||
|
_ -> return (numerator % denominator) |
||||||
|
|
||||||
|
type DecimalOrFraction = Either Integer Rational |
||||||
|
|
||||||
|
parseDecimalOrFraction :: Parser DecimalOrFraction |
||||||
|
parseDecimalOrFraction = (Right <$> (try parseFraction)) |
||||||
|
<|> (Left <$> decimal) |
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in new issue