You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
10 KiB

{-# LANGUAGE OverloadedStrings #-}
module PhoneNumber where
import Control.Applicative ((<|>))
6 years ago
import Control.Monad (replicateM)
import Text.Trifecta
import Test.Hspec
6 years ago
import qualified Test.QuickCheck as Q
import qualified Test.QuickCheck.Property as QP
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Gen
import Data.Maybe (isJust)
import Data.Char (isDigit)
-- This is an exercise for the Belgian Phone numbers system
-- From wikipedia
{-
6 years ago
Belgian telephone numbers consist of 3 major parts: First '0', second 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).
6 years ago
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.
6 years ago
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.
6 years ago
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.
6 years ago
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
6 years ago
data ZonePrefix = MobileZP String
| LandOneZP String
| LandTwoZP String
deriving (Eq)
6 years ago
instance Show ZonePrefix where
show (MobileZP s) = "0" ++ s
show (LandOneZP s) = " 0" ++ s
show (LandTwoZP s) = " 0" ++ s
6 years ago
data SubscribersNumber = MobileSN String
| LandOneSN String
| LandTwoSN String
deriving (Eq)
6 years ago
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
6 years ago
instance Show PhoneNumberBE where
6 years ago
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 =
6 years ago
let mobile = do
one <- char '4'
two <- oneOf ['6', '7', '8', '9']
three <- digit
return $ MobileZP [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
6 years ago
in try mobile <|> landOne <|> landTwo
6 years ago
parseSNOfSix :: Parser String
parseSNOfSix = do
let p = count 2 digit
skip = skipOptional $ char ' ' <|> char '.'
p1 <- p
skip
p2 <- p
skip
p3 <- p
6 years ago
return $ p1 ++ p2 ++ p3
6 years ago
parseSNOfSeven :: Parser String
parseSNOfSeven = do
let p = count 2 digit
skip = skipOptional $ char ' ' <|> char '.'
p1 <- count 3 digit
skip
p2 <- p
skip
p3 <- p
6 years ago
return $ p1 ++ p2 ++ p3
parsePhone :: Parser PhoneNumberBE
parsePhone = do
-- Country code
m <- optional $ string "+32"
-- white space
skipMany $ char ' '
-- Leading zero depends on country code
_ <- case m of
Just _ -> do
6 years ago
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
6 years ago
MobileZP _ -> MobileSN <$> parseSNOfSix
LandOneZP _ -> LandOneSN <$> parseSNOfSix
LandTwoZP _ -> LandTwoSN <$> parseSNOfSeven
return $ PhoneNumberBE z s
6 years ago
-- Tests
maybeSuccess :: Result a -> Maybe a
maybeSuccess (Success a) = Just a
maybeSuccess _ = Nothing
6 years ago
-- ZonePrefix
validMobileZonePrefix :: [String]
validMobileZonePrefix =
[(++) "4"] <*> (map ((++) . show) [6..9] <*> map show [0..9])
validLandOneZonePrefix :: [String]
validLandOneZonePrefix = ["2", "3", "4", "9"]
validLandTwoZonePrefix :: [String]
validLandTwoZonePrefix =
map (++) ["1", "5", "6", "7", "8"] <*> map show [0..9]
invalidZonePrefix :: [String]
invalidZonePrefix = ["0", "a"]
validMobileZonePrefixTest :: (String -> ZonePrefix) -> String -> SpecWith ()
validMobileZonePrefixTest zp input = do
it ("Zone prefix: " ++ input) $ do
let m = parseString parseZonePrefix mempty input
r = maybeSuccess m
r `shouldBe` Just (zp input)
invalidZonePrefixTest :: String -> SpecWith ()
invalidZonePrefixTest input = do
it ("Zone prefix: " ++ input) $ do
let m = parseString parseZonePrefix mempty input
r = maybeSuccess m
r `shouldBe` Nothing
zonePrefixTests :: Spec
zonePrefixTests = do
describe "Parsing valid Mobile Zone Prefixes" $ do
mapM_ (validMobileZonePrefixTest MobileZP) validMobileZonePrefix
describe "Parsing valid Land One Zone Prefixes" $ do
mapM_ (validMobileZonePrefixTest LandOneZP) validLandOneZonePrefix
describe "Parsing valid Land Two Zone Prefixes" $ do
mapM_ (validMobileZonePrefixTest LandTwoZP) validLandTwoZonePrefix
describe "Parsing invalid Zone Prefixes" $ do
mapM_ invalidZonePrefixTest invalidZonePrefix
gen_MobileZP :: Gen String
gen_MobileZP = do
c1 <- elements ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
c2 <- elements ['6', '7', '8', '9']
return $ ['4', c2, c1]
gen_LandOneZP :: Gen String
gen_LandOneZP = elements ["2", "3", "4", "9"]
gen_LandTwoZP :: Gen String
gen_LandTwoZP = do
c1 <- elements ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
c2 <- elements ['1', '5', '6', '7', '8']
return $ [c2, c1]
prop_ZP :: Gen String -> (String -> ZonePrefix) -> QP.Property
prop_ZP g c = QP.forAll g $ \input ->
let r = maybeSuccess m
m = parseString parseZonePrefix mempty input
in isJust r
-- Subscriber Number
gen_MobileSN :: Gen String
gen_MobileSN = do
let c = elements ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
p1 <- replicateM 2 c
r <- replicateM 2 $ do
s1 <- elements [" ", "."]
p2 <- replicateM 2 c
return $ s1 ++ p2
return $ p1 ++ (concat r)
gen_LandOneSN :: String -> Gen String
gen_LandOneSN zp = do
let c = elements ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
c1 <- case zp of
"4" -> elements ['0', '1', '2', '3', '4', '5']
otherwise -> c
c2 <- c
r <- replicateM 2 $ do
s1 <- elements [" ", "."]
p2 <- replicateM 2 c
return $ s1 ++ p2
return $ c1:c2:(concat r)
gen_LandOneSN' :: Gen String
gen_LandOneSN' = do
zp <- gen_LandOneZP
gen_LandOneSN zp
gen_LandTwoSN :: Gen String
gen_LandTwoSN = do
let c = elements ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
p1 <- replicateM 3 c
r <- replicateM 2 $ do
s1 <- elements [" ", "."]
p2 <- replicateM 2 c
return $ s1 ++ p2
return $ p1 ++ (concat r)
prop_SN :: Gen String -> Parser String -> QP.Property
prop_SN g p = QP.forAll g $ \input ->
let r = maybeSuccess m
m = parseString p mempty input
in isJust r
-- Phone Number
gen_Spaces :: Gen String
gen_Spaces = listOf $ return ' '
gen_CountryCode :: Gen String
gen_CountryCode = do
sp <- gen_Spaces
return $ "+32" ++ sp
gen_NoCountryCode :: Gen String
gen_NoCountryCode = do
sp <- gen_Spaces
return $ sp ++ "0"
gen_MobileNumber :: Gen String
gen_MobileNumber = do
cc <- oneof [gen_CountryCode, gen_NoCountryCode]
zp <- gen_MobileZP
sep <- elements ["/", " ", ""]
sn <- gen_MobileSN
return $ cc ++ zp ++ sep ++ sn
gen_LandOneNumber :: Gen String
gen_LandOneNumber = do
cc <- oneof [gen_CountryCode, gen_NoCountryCode]
zp <- gen_LandOneZP
sep <- elements ["/", " ", ""]
sn <- gen_LandOneSN zp
return $ cc ++ zp ++ sep ++ sn
gen_LandTwoNumber :: Gen String
gen_LandTwoNumber = do
cc <- oneof [gen_CountryCode, gen_NoCountryCode]
zp <- gen_LandTwoZP
sep <- elements ["/", " ", ""]
sn <- gen_LandTwoSN
return $ cc ++ zp ++ sep ++ sn
prop_PhoneNumberBE :: QP.Property
prop_PhoneNumberBE = QP.forAll g $ \input ->
let r = maybeSuccess m
m = parseString parsePhone mempty input
in isJust r
where g = oneof [gen_MobileNumber, gen_LandOneNumber, gen_LandTwoNumber]
main :: IO ()
6 years ago
main = do
hspec $ do
zonePrefixTests
Q.quickCheck $ prop_ZP gen_MobileZP MobileZP
Q.quickCheck $ prop_ZP gen_LandOneZP LandOneZP
Q.quickCheck $ prop_ZP gen_LandTwoZP LandTwoZP
Q.quickCheck $ prop_SN gen_MobileSN parseSNOfSix
Q.quickCheck $ prop_SN gen_LandOneSN' parseSNOfSix
Q.quickCheck $ prop_SN gen_LandTwoSN parseSNOfSeven
Q.quickCheckWith Q.stdArgs { Q.maxSuccess = 10000 } $ prop_PhoneNumberBE