Skip to content

Commit 00280e8

Browse files
committed
Add solutions to the parser combinators chapter
1 parent e693f20 commit 00280e8

19 files changed

+769
-1
lines changed

package.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,17 @@ description: Please see the README on GitHub at <https://github.com/Boei
2222
dependencies:
2323
- base >= 4.7 && < 5
2424
- transformers
25+
- containers
26+
- bytestring
27+
- text
28+
- raw-strings-qq
29+
- random
30+
- trifecta
31+
- parsers
2532
- hspec
2633
- QuickCheck
2734
- checkers
2835
- hspec-checkers
29-
- random
3036
- split
3137

3238
library:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module ParserCombinators.Alternative.IntermissionExercise where
2+
3+
import Control.Applicative
4+
import Data.Ratio ((%))
5+
import Text.Trifecta
6+
7+
instance (Eq a) => Eq (Result a) where
8+
Success a == Success b = a == b
9+
Failure a == Failure b = True
10+
_ == _ = False
11+
12+
parseFraction :: Parser Rational
13+
parseFraction = do
14+
numerator <- decimal
15+
char '/'
16+
denominator <- decimal
17+
case denominator of
18+
0 -> fail "Denominator cannot be zero"
19+
_ -> return (numerator % denominator)
20+
21+
type IntegerOrRational = Either Rational Integer
22+
23+
parseIor :: Parser IntegerOrRational
24+
parseIor = try (Left <$> parseFraction) <|> try (Right <$> decimal)
25+
-- This will not work
26+
-- parseIor = (Left <$> parseFraction) <|> (Right <$> decimal)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module ParserCombinators.ChapterExercises.IPV4Addresses where
2+
3+
import Control.Applicative
4+
import Data.Word
5+
import Data.List
6+
import Text.Trifecta
7+
import Test.QuickCheck (Arbitrary, arbitrary)
8+
9+
instance (Eq a) => Eq (Result a) where
10+
Success a == Success b = a == b
11+
Failure a == Failure b = True
12+
_ == _ = False
13+
14+
data IPAddress = IPAddress Word32
15+
deriving (Eq, Ord)
16+
17+
instance Show IPAddress where
18+
show (IPAddress x) = intercalate "." $ map (show . binToDecimal) x''
19+
where x' = pad 32 0 $ decimalToBin x
20+
x'' = separateEvery 8 x'
21+
22+
instance Arbitrary IPAddress where
23+
arbitrary = IPAddress <$> arbitrary
24+
25+
ipAddress :: Parser IPAddress
26+
ipAddress = IPAddress . fromIntegral . binToDecimal . dotDecimalToBin <$> sepBy integer (char '.')
27+
28+
decimalToBin :: (Integral a) => a -> [a]
29+
decimalToBin n
30+
| n <= 1 = [n]
31+
| otherwise = decimalToBin (n `div` 2) ++ [n `mod` 2]
32+
33+
pad :: Int -> a -> [a] -> [a]
34+
pad n a xs = replicate nPad a ++ xs
35+
where nPad = max (n - length xs) 0
36+
37+
dotDecimalToBin :: (Integral a) => [a] -> [a]
38+
dotDecimalToBin = concatMap (pad 8 0 . decimalToBin)
39+
40+
binToDecimal :: (Integral a) => [a] -> a
41+
binToDecimal = foldl' (\acc x -> acc * 2 + x) 0
42+
43+
separateEvery :: Int -> [a] -> [[a]]
44+
separateEvery _ [] = []
45+
separateEvery n xs = take n xs : separateEvery n (drop n xs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module ParserCombinators.ChapterExercises.IPV6Addresses where
2+
3+
import Control.Applicative
4+
import qualified Data.Map as M
5+
import Data.Char
6+
import Data.Word
7+
import Data.List
8+
import Data.Maybe
9+
import Text.Trifecta
10+
import Test.QuickCheck (Arbitrary, arbitrary)
11+
12+
instance (Eq a) => Eq (Result a) where
13+
Success a == Success b = a == b
14+
Failure a == Failure b = True
15+
_ == _ = False
16+
17+
data IPAddress6 = IPAddress6 Word64 Word64
18+
deriving (Eq, Ord)
19+
20+
instance Show IPAddress6 where
21+
show (IPAddress6 x y) = intercalate ":" $ map stripLeadingZeros xy
22+
where x' = pad 16 '0' $ decimalToHex x
23+
y' = pad 16 '0' $ decimalToHex y
24+
xy = separateEvery 4 $ x' ++ y'
25+
26+
instance Arbitrary IPAddress6 where
27+
arbitrary = liftA2 IPAddress6 arbitrary arbitrary
28+
29+
ipAddress6 :: Parser IPAddress6
30+
ipAddress6 = do
31+
xs <- sepBy (many (noneOf ":")) (char ':')
32+
let ys = map (pad 4 '0') $ uncollapse xs
33+
ys1 = fromIntegral $ hexToDecimal $ concat $ take 4 ys
34+
ys2 = fromIntegral $ hexToDecimal $ concat $ drop 4 ys
35+
return $ IPAddress6 ys1 ys2
36+
37+
uncollapse :: [String] -> [String]
38+
uncollapse xs = go xs
39+
where n = 8 - length (filter (/= "") xs)
40+
go [] = []
41+
go (y : ys)
42+
| y == "" = replicate n "0" ++ ys
43+
| otherwise = y : go ys
44+
45+
stripLeadingZeros :: String -> String
46+
stripLeadingZeros "" = ""
47+
stripLeadingZeros [x] = [x]
48+
stripLeadingZeros (x:xs)
49+
| x == '0' = stripLeadingZeros xs
50+
| otherwise = x : xs
51+
52+
pad :: Int -> a -> [a] -> [a]
53+
pad n x xs = replicate (n - length xs) x ++ xs
54+
55+
hexToDecimal :: (Integral a) => String -> a
56+
hexToDecimal = foldl' (\acc x -> acc * 16 + lookup x) 0
57+
where vocab = M.fromList $ zip "0123456789ABCDEF" [0..15]
58+
lookup x = fromJust $ M.lookup (toUpper x) vocab
59+
60+
decimalToHex :: (Integral a) => a -> String
61+
decimalToHex n
62+
| n < 16 = [vocab !! fromIntegral n]
63+
| otherwise = decimalToHex (n `div` 16) ++ [vocab !! fromIntegral (n `mod` 16)]
64+
where vocab = "0123456789ABCDEF"
65+
66+
separateEvery :: Int -> [a] -> [[a]]
67+
separateEvery _ [] = []
68+
separateEvery n xs = take n xs : separateEvery n (drop n xs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
module ParserCombinators.ChapterExercises.LogFile where
2+
3+
import Control.Applicative
4+
import Data.List
5+
import Text.Trifecta
6+
7+
instance (Eq a) => Eq (Result a) where
8+
Success a == Success b = a == b
9+
Failure a == Failure b = True
10+
_ == _ = False
11+
12+
-- Some types
13+
type Year = Integer
14+
type Month = Integer
15+
type Day = Integer
16+
17+
data Date = Date Year Month Day
18+
deriving (Show, Eq)
19+
20+
type Hour = Integer
21+
type Minute = Integer
22+
23+
data Time = Time Hour Minute
24+
deriving (Show, Eq)
25+
26+
type StartTime = Time
27+
type Name = String
28+
type Duration = Double
29+
30+
data Activity = Activity StartTime Name
31+
deriving (Show, Eq)
32+
33+
data Daily = Daily Date [Activity]
34+
deriving (Show, Eq)
35+
36+
-- Parsing
37+
comment :: Parser String
38+
comment = try (spaces >> string "--" >> many (noneOf "\n"))
39+
40+
eol :: Parser ()
41+
eol = (newline >> return ()) <|> (comment >> return ())
42+
43+
-- This function is tricky
44+
-- We have to 1) ignore comments started with -- and all white spaces before
45+
-- 2) handle correctly a single dash
46+
parseName :: Parser String
47+
parseName = (eof >> return "")
48+
<|> (eol >> return "")
49+
<|> (liftA2 (:) anyChar parseName)
50+
51+
parseDate :: Parser Date
52+
parseDate = do
53+
string "# "
54+
year <- integer
55+
char '-'
56+
month <- integer
57+
char '-'
58+
day <- integer
59+
many newline
60+
return $ Date year month day
61+
62+
parseTime :: Parser Time
63+
parseTime = do
64+
hour <- integer
65+
char ':'
66+
minute <- integer
67+
return $ Time hour minute
68+
69+
parseActivity :: Parser Activity
70+
parseActivity = do
71+
startTime <- parseTime
72+
-- integer consumes the trailing zero
73+
-- so `_ <- char ' '` gives an error
74+
skipMany (char ' ')
75+
name <- parseName
76+
skipMany eol
77+
return $ Activity startTime name
78+
79+
parseDaily :: Parser Daily
80+
parseDaily = do
81+
date <- parseDate
82+
skipMany eol
83+
activities <- some parseActivity
84+
return $ Daily date activities
85+
86+
parseLog :: Parser [Daily]
87+
parseLog = skipMany eol >> some parseDaily
88+
89+
-- Group by the first component and aggregate the second component
90+
-- with a binary operator
91+
groupByOp :: Eq a => (b -> b -> b) -> [(a, b)] -> [(a, b)]
92+
groupByOp op = map reduce . groupBy (\a a' -> fst a == fst a')
93+
where reduce = liftA2 (,) (head . map fst) (foldr1 op . map snd)
94+
95+
dailySummary :: Daily -> [(Name, Duration)]
96+
dailySummary (Daily _ activities) = nameDurations
97+
where getDuration (Activity t1 n1) (Activity t2 _) = (n1, timeDiff t1 t2)
98+
timeDiff (Time h1 m1) (Time h2 m2) = fromIntegral $ (h2 * 60 + m2) - (h1 * 60 + m1)
99+
nameDurations = groupByOp (+) $ sortOn fst $ zipWith getDuration activities (tail activities)
100+
101+
summary :: [Daily] -> [(Name, Duration)]
102+
summary = groupByOp (+) . sortOn fst . concatMap dailySummary
103+
104+
-- Average time spent on each activity
105+
avgTime :: [Daily] -> [(Name, Duration)]
106+
avgTime xs = map (\(name, duration) -> (name, duration / len)) $ summary xs
107+
where len = fromIntegral $ length xs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module ParserCombinators.ChapterExercises.ParseDigitAndInteger where
2+
3+
import Control.Applicative
4+
import Data.List
5+
import Text.Trifecta
6+
7+
instance (Eq a) => Eq (Result a) where
8+
Success a == Success b = a == b
9+
Failure a == Failure b = True
10+
_ == _ = False
11+
12+
parseDigit :: Parser Char
13+
parseDigit = oneOf $ concatMap show [0..9]
14+
15+
readChar :: (Read a) => Char -> a
16+
readChar a = read [a]
17+
18+
base10Integer :: Parser Integer
19+
base10Integer = do
20+
-- (<?>) give parser a name
21+
-- See https://hackage.haskell.org/package/parsers-0.12.9/docs/Text-Parser-Combinators.html
22+
digits <- some parseDigit <?> "integer"
23+
return $ (foldl1' (\acc x -> 10 * acc + x) . map readChar) digits
24+
25+
base10Integer' :: Parser Integer
26+
base10Integer' = (char '+' >> base10Integer)
27+
<|> (char '-' >> base10Integer >>= \x -> return (-x))
28+
<|> base10Integer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module ParserCombinators.ChapterExercises.PhoneNumbers where
2+
3+
import Control.Applicative
4+
import Data.List
5+
import Text.Trifecta
6+
7+
instance (Eq a) => Eq (Result a) where
8+
Success a == Success b = a == b
9+
Failure a == Failure b = True
10+
_ == _ = False
11+
12+
type NumberingPlanArea = Int -- aka area code
13+
type Exchange = Int
14+
type LineNumber = Int
15+
16+
data PhoneNumber =
17+
PhoneNumber NumberingPlanArea Exchange LineNumber
18+
deriving (Eq, Show)
19+
20+
number :: String -> Int
21+
number = foldl' (\acc x -> acc * 10 + read [x]) 0
22+
23+
parseNumberingPlanArea :: Parser NumberingPlanArea
24+
parseNumberingPlanArea = do
25+
_ <- optional (string "1-")
26+
numberingPlanArea <- char '(' *> count 3 digit <* char ')' <* char ' '
27+
<|> (count 3 digit <* optional (char '-'))
28+
return $ number numberingPlanArea
29+
30+
parseExchange :: Parser Exchange
31+
parseExchange = number <$> count 3 digit
32+
33+
parseLineNumber :: Parser LineNumber
34+
parseLineNumber = number <$> count 4 digit
35+
36+
parsePhone :: Parser PhoneNumber
37+
parsePhone = do
38+
numberingPlanArea <- parseNumberingPlanArea
39+
exchange <- parseExchange
40+
_ <- optional (char '-')
41+
lineNumber <- parseLineNumber
42+
return $ PhoneNumber numberingPlanArea exchange lineNumber

0 commit comments

Comments
 (0)