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
314 lines
10 KiB
{-# LANGUAGE OverloadedStrings #-} |
|
|
|
module PhoneNumber where |
|
|
|
import Control.Applicative ((<|>)) |
|
import Control.Monad (replicateM) |
|
|
|
import Text.Trifecta |
|
import Test.Hspec |
|
|
|
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 |
|
{- |
|
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). |
|
|
|
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 = 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 |
|
in try mobile <|> landOne <|> landTwo |
|
|
|
parseSNOfSix :: Parser String |
|
parseSNOfSix = do |
|
let p = count 2 digit |
|
skip = skipOptional $ char ' ' <|> char '.' |
|
p1 <- p |
|
skip |
|
p2 <- p |
|
skip |
|
p3 <- p |
|
return $ p1 ++ p2 ++ p3 |
|
|
|
parseSNOfSeven :: Parser String |
|
parseSNOfSeven = do |
|
let p = count 2 digit |
|
skip = skipOptional $ char ' ' <|> char '.' |
|
p1 <- count 3 digit |
|
skip |
|
p2 <- p |
|
skip |
|
p3 <- p |
|
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 |
|
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 _ -> MobileSN <$> parseSNOfSix |
|
LandOneZP _ -> LandOneSN <$> parseSNOfSix |
|
LandTwoZP _ -> LandTwoSN <$> parseSNOfSeven |
|
return $ PhoneNumberBE z s |
|
|
|
-- Tests |
|
|
|
maybeSuccess :: Result a -> Maybe a |
|
maybeSuccess (Success a) = Just a |
|
maybeSuccess _ = Nothing |
|
|
|
-- 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 () |
|
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 |