{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module ROC.ID
( Identity (..)
, identityChecksum
, parseIdentity
, ParseError (..)
, randomIdentity
) where
import Control.Monad.Random.Class (MonadRandom (..))
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Tuple.Only (Only (..))
import Data.Vector.Sized (Vector)
import GHC.Generics (Generic)
import ROC.ID.Digit
import ROC.ID.Gender
import ROC.ID.Location
import ROC.ID.Serial
import ROC.ID.Utilities
import qualified Data.Text as T
import qualified Data.Vector.Sized as V
data Identity = Identity
{ identityGender :: Gender
, identityLocation :: Location
, identitySerial :: Serial
} deriving (Eq, Generic, Ord)
instance Show Identity where
show i@Identity {..} = ""
<> show identityLocation
<> foldMap show (toDigits identityGender)
<> foldMap show (toDigits identitySerial)
<> show (identityChecksum i)
identityChecksum :: Identity -> Digit
identityChecksum Identity {..} = toEnum $ negate total `mod` 10
where
total = 1 * p 0 + 9 * p 1 + 8 * g 0 + 7 * s 0 + 6 * s 1
+ 5 * s 2 + 4 * s 3 + 3 * s 4 + 2 * s 5 + 1 * s 6
g = index identityGender
p = index identityLocation
s = index identitySerial
index x = fromEnum . V.index e
where
e = toDigits x
class ToDigits t n | t -> n where
toDigits :: t -> Vector n Digit
instance ToDigits Gender 1 where
toDigits = V.fromTuple . Only . \case
Male -> D1
Female -> D2
instance ToDigits Location 2 where
toDigits = V.fromTuple . \case
A -> (D1, D0); N -> (D2, D2)
B -> (D1, D1); O -> (D3, D5)
C -> (D1, D2); P -> (D2, D3)
D -> (D1, D3); Q -> (D2, D4)
E -> (D1, D4); R -> (D2, D5)
F -> (D1, D5); S -> (D2, D6)
G -> (D1, D6); T -> (D2, D7)
H -> (D1, D7); U -> (D2, D8)
I -> (D3, D4); V -> (D2, D9)
J -> (D1, D8); W -> (D3, D2)
K -> (D1, D9); X -> (D3, D0)
L -> (D2, D0); Y -> (D3, D1)
M -> (D2, D1); Z -> (D3, D3)
instance ToDigits Serial 7 where
toDigits (Serial c) = c
parseIdentity :: Text -> Either ParseError Identity
parseIdentity t = do
v <- guard InvalidLength (parseRaw t)
i <- Identity <$> guard InvalidGender (parseGender $ readGender v)
<*> guard InvalidLocation (parseLocation $ readLocation v)
<*> guard InvalidSerial (parseSerial $ readSerial v)
c <- guard InvalidChecksum (parseDigit $ readChecksum v)
if c == identityChecksum i then pure i else Left InvalidChecksum
where
readSerial = V.slice (Proxy :: Proxy 2)
readLocation = flip V.index 0
readGender = flip V.index 1
readChecksum = flip V.index 9
data ParseError
= InvalidLength
| InvalidGender
| InvalidLocation
| InvalidSerial
| InvalidChecksum
deriving (Eq, Show)
parseRaw :: Text -> Maybe (Vector 10 Char)
parseRaw = V.fromList . T.unpack
parseGender :: Char -> Maybe Gender
parseGender = \case
'1' -> pure Male
'2' -> pure Female
_ -> Nothing
parseSerial :: Vector 7 Char -> Maybe Serial
parseSerial a = Serial <$> traverse parseDigit a
randomIdentity :: MonadRandom m => m Identity
randomIdentity = Identity <$> randomGender
<*> randomLocation
<*> randomSerial