packages feed

roc-id 0.2.0.6 → 0.3.0.0

raw patch · 28 files changed

+1702/−542 lines, 28 filesdep +doctestdep +nonempty-containersdep +quickcheck-classesdep −Onlydep −vector-sizeddep ~basebuild-type:Customsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: doctest, nonempty-containers, quickcheck-classes

Dependencies removed: Only, vector-sized

Dependency ranges changed: base

API changes (from Hackage documentation)

- ROC.ID: Identity :: Gender -> Location -> Serial -> Identity
- ROC.ID: InvalidGender :: ParseError
- ROC.ID: InvalidLength :: ParseError
- ROC.ID: InvalidLocation :: ParseError
- ROC.ID: InvalidSerial :: ParseError
- ROC.ID: [identityGender] :: Identity -> Gender
- ROC.ID: [identityLocation] :: Identity -> Location
- ROC.ID: [identitySerial] :: Identity -> Serial
- ROC.ID: data Identity
- ROC.ID: data ParseError
- ROC.ID: identityChecksum :: Identity -> Digit
- ROC.ID: instance GHC.Classes.Eq ROC.ID.Identity
- ROC.ID: instance GHC.Classes.Eq ROC.ID.ParseError
- ROC.ID: instance GHC.Classes.Ord ROC.ID.Identity
- ROC.ID: instance GHC.Generics.Generic ROC.ID.Identity
- ROC.ID: instance GHC.Show.Show ROC.ID.Identity
- ROC.ID: instance GHC.Show.Show ROC.ID.ParseError
- ROC.ID: instance ROC.ID.ToDigits ROC.ID.Gender.Gender 1
- ROC.ID: instance ROC.ID.ToDigits ROC.ID.Location.Location 2
- ROC.ID: instance ROC.ID.ToDigits ROC.ID.Serial.Serial 7
- ROC.ID: parseIdentity :: Text -> Either ParseError Identity
- ROC.ID: randomIdentity :: MonadRandom m => m Identity
- ROC.ID.Digit: parseDigit :: Char -> Maybe Digit
- ROC.ID.Gender: printGender :: Language -> Gender -> Text
- ROC.ID.Gender: randomGender :: MonadRandom m => m Gender
- ROC.ID.Location: A :: Location
- ROC.ID.Location: B :: Location
- ROC.ID.Location: C :: Location
- ROC.ID.Location: D :: Location
- ROC.ID.Location: E :: Location
- ROC.ID.Location: F :: Location
- ROC.ID.Location: G :: Location
- ROC.ID.Location: H :: Location
- ROC.ID.Location: I :: Location
- ROC.ID.Location: J :: Location
- ROC.ID.Location: K :: Location
- ROC.ID.Location: L :: Location
- ROC.ID.Location: M :: Location
- ROC.ID.Location: N :: Location
- ROC.ID.Location: O :: Location
- ROC.ID.Location: P :: Location
- ROC.ID.Location: Q :: Location
- ROC.ID.Location: R :: Location
- ROC.ID.Location: S :: Location
- ROC.ID.Location: T :: Location
- ROC.ID.Location: U :: Location
- ROC.ID.Location: V :: Location
- ROC.ID.Location: W :: Location
- ROC.ID.Location: X :: Location
- ROC.ID.Location: Y :: Location
- ROC.ID.Location: Z :: Location
- ROC.ID.Location: parseLocation :: Char -> Maybe Location
- ROC.ID.Location: printLocation :: Language -> Location -> Text
- ROC.ID.Location: randomLocation :: MonadRandom m => m Location
- ROC.ID.Serial: Serial :: Vector 7 Digit -> Serial
- ROC.ID.Serial: instance GHC.Classes.Eq ROC.ID.Serial.Serial
- ROC.ID.Serial: instance GHC.Classes.Ord ROC.ID.Serial.Serial
- ROC.ID.Serial: instance GHC.Generics.Generic ROC.ID.Serial.Serial
- ROC.ID.Serial: instance GHC.Show.Show ROC.ID.Serial.Serial
- ROC.ID.Serial: newtype Serial
- ROC.ID.Serial: randomSerial :: MonadRandom m => m Serial
+ ROC.ID: ID :: !Letter -> !Digit1289 -> !Digit -> !Digit -> !Digit -> !Digit -> !Digit -> !Digit -> !Digit -> ID
+ ROC.ID: InvalidChar :: CharIndex -> CharSet -> FromTextError
+ ROC.ID: TextTooLong :: FromTextError
+ ROC.ID: TextTooShort :: FromTextError
+ ROC.ID: [c0] :: ID -> !Letter
+ ROC.ID: [c1] :: ID -> !Digit1289
+ ROC.ID: [c2] :: ID -> !Digit
+ ROC.ID: [c3] :: ID -> !Digit
+ ROC.ID: [c4] :: ID -> !Digit
+ ROC.ID: [c5] :: ID -> !Digit
+ ROC.ID: [c6] :: ID -> !Digit
+ ROC.ID: [c7] :: ID -> !Digit
+ ROC.ID: [c8] :: ID -> !Digit
+ ROC.ID: checksumDigit :: ID -> Digit
+ ROC.ID: data FromTextError
+ ROC.ID: data ID
+ ROC.ID: fromSymbol :: forall (s :: Symbol). ValidID s => ID
+ ROC.ID: fromText :: Text -> Either FromTextError ID
+ ROC.ID: generate :: MonadRandom m => m ID
+ ROC.ID: getGender :: ID -> Gender
+ ROC.ID: getLocation :: ID -> Location
+ ROC.ID: getNationality :: ID -> Nationality
+ ROC.ID: instance GHC.Classes.Eq ROC.ID.FromTextError
+ ROC.ID: instance GHC.Classes.Eq ROC.ID.ID
+ ROC.ID: instance GHC.Classes.Ord ROC.ID.FromTextError
+ ROC.ID: instance GHC.Classes.Ord ROC.ID.ID
+ ROC.ID: instance GHC.Read.Read ROC.ID.ID
+ ROC.ID: instance GHC.Show.Show ROC.ID.FromTextError
+ ROC.ID: instance GHC.Show.Show ROC.ID.ID
+ ROC.ID: setGender :: Gender -> ID -> ID
+ ROC.ID: setLocation :: Location -> ID -> ID
+ ROC.ID: setNationality :: Nationality -> ID -> ID
+ ROC.ID: toText :: ID -> Text
+ ROC.ID.CharIndex: CharIndex :: Int -> CharIndex
+ ROC.ID.CharIndex: instance GHC.Classes.Eq ROC.ID.CharIndex.CharIndex
+ ROC.ID.CharIndex: instance GHC.Classes.Ord ROC.ID.CharIndex.CharIndex
+ ROC.ID.CharIndex: instance GHC.Enum.Enum ROC.ID.CharIndex.CharIndex
+ ROC.ID.CharIndex: instance GHC.Num.Num ROC.ID.CharIndex.CharIndex
+ ROC.ID.CharIndex: instance GHC.Read.Read ROC.ID.CharIndex.CharIndex
+ ROC.ID.CharIndex: instance GHC.Show.Show ROC.ID.CharIndex.CharIndex
+ ROC.ID.CharIndex: newtype CharIndex
+ ROC.ID.CharSet: CharRange :: Char -> Char -> CharSet
+ ROC.ID.CharSet: CharSet :: NESet Char -> CharSet
+ ROC.ID.CharSet: data CharSet
+ ROC.ID.CharSet: instance GHC.Classes.Eq ROC.ID.CharSet.CharSet
+ ROC.ID.CharSet: instance GHC.Classes.Ord ROC.ID.CharSet.CharSet
+ ROC.ID.CharSet: instance GHC.Read.Read ROC.ID.CharSet.CharSet
+ ROC.ID.CharSet: instance GHC.Show.Show ROC.ID.CharSet.CharSet
+ ROC.ID.Digit: fromChar :: Char -> Maybe Digit
+ ROC.ID.Digit: generate :: MonadRandom m => m Digit
+ ROC.ID.Digit: instance GHC.Num.Num ROC.ID.Digit.Digit
+ ROC.ID.Digit: instance GHC.Read.Read ROC.ID.Digit.Digit
+ ROC.ID.Digit: toChar :: Digit -> Char
+ ROC.ID.Digit: type family ToNat (d :: Digit) :: Nat
+ ROC.ID.Digit1289: D1289_1 :: Digit1289
+ ROC.ID.Digit1289: D1289_2 :: Digit1289
+ ROC.ID.Digit1289: D1289_8 :: Digit1289
+ ROC.ID.Digit1289: D1289_9 :: Digit1289
+ ROC.ID.Digit1289: data Digit1289
+ ROC.ID.Digit1289: fromChar :: Char -> Maybe Digit1289
+ ROC.ID.Digit1289: generate :: MonadRandom m => m Digit1289
+ ROC.ID.Digit1289: instance GHC.Classes.Eq ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: instance GHC.Classes.Ord ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: instance GHC.Enum.Bounded ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: instance GHC.Enum.Enum ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: instance GHC.Generics.Generic ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: instance GHC.Show.Show ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: toChar :: Digit1289 -> Char
+ ROC.ID.Digit1289: toDigit :: Digit1289 -> Digit
+ ROC.ID.Digit1289: type family ToNat (d :: Digit1289) :: Nat
+ ROC.ID.Gender: generate :: MonadRandom m => m Gender
+ ROC.ID.Gender: instance GHC.Read.Read ROC.ID.Gender.Gender
+ ROC.ID.Gender: toText :: Language -> Gender -> Text
+ ROC.ID.Letter: A :: Letter
+ ROC.ID.Letter: B :: Letter
+ ROC.ID.Letter: C :: Letter
+ ROC.ID.Letter: D :: Letter
+ ROC.ID.Letter: E :: Letter
+ ROC.ID.Letter: F :: Letter
+ ROC.ID.Letter: G :: Letter
+ ROC.ID.Letter: H :: Letter
+ ROC.ID.Letter: I :: Letter
+ ROC.ID.Letter: J :: Letter
+ ROC.ID.Letter: K :: Letter
+ ROC.ID.Letter: L :: Letter
+ ROC.ID.Letter: M :: Letter
+ ROC.ID.Letter: N :: Letter
+ ROC.ID.Letter: O :: Letter
+ ROC.ID.Letter: P :: Letter
+ ROC.ID.Letter: Q :: Letter
+ ROC.ID.Letter: R :: Letter
+ ROC.ID.Letter: S :: Letter
+ ROC.ID.Letter: T :: Letter
+ ROC.ID.Letter: U :: Letter
+ ROC.ID.Letter: V :: Letter
+ ROC.ID.Letter: W :: Letter
+ ROC.ID.Letter: X :: Letter
+ ROC.ID.Letter: Y :: Letter
+ ROC.ID.Letter: Z :: Letter
+ ROC.ID.Letter: data Letter
+ ROC.ID.Letter: fromChar :: Char -> Maybe Letter
+ ROC.ID.Letter: generate :: MonadRandom m => m Letter
+ ROC.ID.Letter: instance GHC.Classes.Eq ROC.ID.Letter.Letter
+ ROC.ID.Letter: instance GHC.Classes.Ord ROC.ID.Letter.Letter
+ ROC.ID.Letter: instance GHC.Enum.Bounded ROC.ID.Letter.Letter
+ ROC.ID.Letter: instance GHC.Enum.Enum ROC.ID.Letter.Letter
+ ROC.ID.Letter: instance GHC.Generics.Generic ROC.ID.Letter.Letter
+ ROC.ID.Letter: instance GHC.Read.Read ROC.ID.Letter.Letter
+ ROC.ID.Letter: instance GHC.Show.Show ROC.ID.Letter.Letter
+ ROC.ID.Letter: toChar :: Letter -> Char
+ ROC.ID.Letter: type family FromChar (c :: Char) :: Maybe Letter
+ ROC.ID.Location: fromLetter :: Letter -> Location
+ ROC.ID.Location: generate :: MonadRandom m => m Location
+ ROC.ID.Location: toLetter :: Location -> Letter
+ ROC.ID.Location: toText :: Language -> Location -> Text
+ ROC.ID.Nationality: National :: Nationality
+ ROC.ID.Nationality: NonNational :: Nationality
+ ROC.ID.Nationality: data Nationality
+ ROC.ID.Nationality: generate :: MonadRandom m => m Nationality
+ ROC.ID.Nationality: instance GHC.Classes.Eq ROC.ID.Nationality.Nationality
+ ROC.ID.Nationality: instance GHC.Classes.Ord ROC.ID.Nationality.Nationality
+ ROC.ID.Nationality: instance GHC.Enum.Bounded ROC.ID.Nationality.Nationality
+ ROC.ID.Nationality: instance GHC.Enum.Enum ROC.ID.Nationality.Nationality
+ ROC.ID.Nationality: instance GHC.Read.Read ROC.ID.Nationality.Nationality
+ ROC.ID.Nationality: instance GHC.Show.Show ROC.ID.Nationality.Nationality
- ROC.ID: InvalidChecksum :: ParseError
+ ROC.ID: InvalidChecksum :: FromTextError

Files

CHANGELOG.md view
@@ -1,3 +1,9 @@+# 0.3.0.0++- Added support for ARC (Alien Residence Certificate) numbers.+- Added support for type-checked ID literals.+- Refined and reorganised the public API.+ # 0.2.0.6  - Revised version bounds of dependencies.
LICENSE view
@@ -1,4 +1,4 @@-Copyright Jonathan Knowles (c) 2018–2023+Copyright Jonathan Knowles (c) 2018–2025  All rights reserved. 
README.md view
@@ -1,7 +1,21 @@+# `roc-id`+<a href="https://jonathanknowles.github.io/roc-id/"><img src="https://img.shields.io/badge/API-Documentation-227755" /></a>+ This package provides a [Haskell](https://www.haskell.org/) implementation of-the ROC (Taiwan) Identification Number (中華民國身分證號碼) standard.+the ROC (Taiwan) Uniform Identification Number (中華民國統一證號) format. -For more details of the standard, see:+This format is used by both National Identification Cards (國民身分證) and+Alien Resident Certificates (居留證). Each identification number consists of a+single uppercase letter followed by nine decimal digits, with the final digit+serving as a checksum, calculated according to a standard algorithm. +Example: `A123456789`++This package offers functions for validating, decoding, and encoding these+numbers.++For more details of the Uniform Identification Number format, see:+ * https://zh.wikipedia.org/wiki/中華民國國民身分證-* https://en.wikipedia.org/wiki/National_Identification_Card_(Republic_of_China)+* https://en.wikipedia.org/wiki/National_identification_card_(Taiwan)+* https://en.wikipedia.org/wiki/Resident_certificate
+ Setup.hs view
@@ -0,0 +1,5 @@+import Distribution.Extra.Doctest+  ( defaultMainWithDoctests )++main :: IO ()+main = defaultMainWithDoctests "roc-id-test-docs"
+ components/roc-id-test-docs/Main.hs view
@@ -0,0 +1,9 @@+module Main where++import Build_doctests+  ( flags, pkgs, module_sources )+import Test.DocTest+  ( doctest )++main :: IO ()+main = doctest (flags ++ pkgs ++ module_sources)
+ components/roc-id-test/Spec.hs view
@@ -0,0 +1,249 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{- HLINT ignore "Redundant bracket" -}++module Main where++import Data.Bifunctor+  ( Bifunctor (second) )+import Data.Char+  ( intToDigit )+import Data.List.NonEmpty+  ( NonEmpty ((:|)) )+import Data.Text+  ( Text )+import ROC.ID+  ( ID (..) )+import ROC.ID.CharIndex+  ( CharIndex (CharIndex) )+import ROC.ID.CharSet+  ( CharSet (..) )+import ROC.ID.Digit+  ( Digit (..) )+import ROC.ID.Digit1289+  ( Digit1289 (..) )+import ROC.ID.Gender+  ( Gender (..) )+import ROC.ID.Letter+  ( Letter (..) )+import ROC.ID.Location+  ( Location )+import ROC.ID.Nationality+  ( Nationality (..) )+import Test.Hspec+  ( describe, hspec, it, shouldBe, shouldSatisfy )+import Test.QuickCheck+  ( Arbitrary (..)+  , NonEmptyList (..)+  , arbitraryBoundedEnum+  , choose+  , elements+  , forAll+  , property+  , shrinkBoundedEnum+  , shrinkMap+  )+import Test.QuickCheck.Classes+  ( boundedEnumLaws, eqLaws, numLaws, ordLaws, showLaws, showReadLaws )+import Test.QuickCheck.Classes.Hspec+  ( testLawsMany )++import qualified Data.Set.NonEmpty as NESet+import qualified Data.Text as T+import qualified ROC.ID as ID++instance Arbitrary Digit where+  arbitrary = arbitraryBoundedEnum+  shrink = shrinkBoundedEnum++instance Arbitrary Digit1289 where+  arbitrary = arbitraryBoundedEnum+  shrink = shrinkBoundedEnum++instance Arbitrary Gender where+  arbitrary = arbitraryBoundedEnum+  shrink = shrinkBoundedEnum++instance Arbitrary Letter where+  arbitrary = arbitraryBoundedEnum+  shrink = shrinkBoundedEnum++instance Arbitrary ID where+  arbitrary = idFromTuple <$> arbitrary+  shrink = shrinkMap idFromTuple idToTuple++instance Arbitrary Location where+  arbitrary = arbitraryBoundedEnum+  shrink = shrinkBoundedEnum++instance Arbitrary Nationality where+  arbitrary = arbitraryBoundedEnum+  shrink = shrinkBoundedEnum++main :: IO ()+main = hspec $ do++  describe "Class laws" $ do++    testLawsMany @Digit+        [ boundedEnumLaws+        , eqLaws+        , numLaws+        , ordLaws+        , showLaws+        , showReadLaws+        ]++    testLawsMany @Gender+        [ boundedEnumLaws+        , eqLaws+        , ordLaws+        , showLaws+        , showReadLaws+        ]++    testLawsMany @ID+        [ eqLaws+        , ordLaws+        , showLaws+        , showReadLaws+        ]++    testLawsMany @Location+        [ boundedEnumLaws+        , eqLaws+        , ordLaws+        , showLaws+        , showReadLaws+        ]++    testLawsMany @Nationality+        [ boundedEnumLaws+        , eqLaws+        , ordLaws+        , showLaws+        , showReadLaws+        ]++  describe "ID.fromText" $ do++    it "successfully parses known-valid identification numbers" $+      forAll (elements knownValidIDs) $ \i ->+        ID.fromText (ID.toText i) `shouldBe` Right i++    it "successfully parses valid identification numbers" $+      property $ \(i :: ID) ->+        ID.fromText (ID.toText i) `shouldBe` Right i++    it "does not parse identification numbers that are too short" $+      property $ \(i :: ID) n -> do+        let newLength = n `mod` 10+        let invalidID = T.take newLength $ ID.toText i+        ID.fromText invalidID `shouldBe` Left ID.TextTooShort++    it "does not parse identification numbers that are too long" $+      property $ \(i :: ID) (NonEmpty s) -> do+        let invalidID = ID.toText i <> T.pack s+        ID.fromText invalidID `shouldBe` Left ID.TextTooLong++    it "does not parse identification numbers with invalid location codes" $+      property $ \(i :: ID) (c :: Int) -> do+        let invalidLocationCode = intToDigit $ c `mod` 10+        let invalidID = replaceCharAt 0 invalidLocationCode $ ID.toText i+        ID.fromText invalidID `shouldBe`+          Left (ID.InvalidChar 0 (CharRange 'A' 'Z'))++    it "does not parse identification numbers with invalid initial digits" $+      property $ \(i :: ID) ->+        forAll (elements ['0', '3', '4', '5', '6', '7']) $ \initialDigit -> do+          let invalidID = replaceCharAt 1 initialDigit (ID.toText i)+          let expectedError =+                ID.InvalidChar 1+                  (CharSet $ NESet.fromList $ '1' :| ['2', '8', '9'])+          ID.fromText invalidID `shouldBe` Left expectedError++    it "does not parse identification numbers with invalid checksums" $+      property $ \(i :: ID) (c :: Int) -> do+        let invalidChecksumDigit = intToDigit $+              ((c `mod` 9) + fromEnum (ID.checksumDigit i) + 1) `mod` 10+        let invalidID =+              T.take 9 (ID.toText i) <> T.pack [invalidChecksumDigit]+        ID.fromText invalidID `shouldBe` Left ID.InvalidChecksum++    it "reports invalid characters even when input is too short" $+      property $ \(i :: ID) ->+      forAll (choose (1, 9)) $ \truncatedLength ->+      forAll (choose (0, truncatedLength - 1)) $ \invalidCharIndex -> do+        let textTruncated = T.take truncatedLength (ID.toText i)+        let textInvalid = replaceCharAt invalidCharIndex 'x' textTruncated+        ID.fromText textInvalid `shouldSatisfy` \case+          Left (ID.InvalidChar (CharIndex index) _)+            | index == invalidCharIndex -> True+          _ -> False++    it "does not report invalid characters if input is too long" $+      property $ \(i :: ID) (NonEmpty trailingExcess) ->+      forAll (choose (0, 9)) $ \invalidCharIndex -> do+        let textInvalid =+              replaceCharAt invalidCharIndex 'x' (ID.toText i)+              <>+              T.pack trailingExcess+        ID.fromText textInvalid `shouldBe` Left ID.TextTooLong++-- | Replaces a character at a specific position.+--+replaceCharAt :: Int -> Char -> Text -> Text+replaceCharAt i c t+    | i < 0 || i >= T.length t = error "replaceCharAt: invalid index"+    | otherwise = prefix <> T.singleton c <> suffix+  where+    (prefix, suffix) = second (T.drop 1) (T.splitAt i t)++-- | A set of known-valid ID numbers.+--+-- Generated with 身分證字號產生器.+--+-- See: https://www.csie.ntu.edu.tw/~b90057/use/ROCid.html+--+knownValidIDs :: [ID]+knownValidIDs =+  [ ID.fromSymbol @"A123961383"+  , ID.fromSymbol @"B210742224"+  , ID.fromSymbol @"C120930548"+  , ID.fromSymbol @"D257991149"+  , ID.fromSymbol @"E127379116"+  , ID.fromSymbol @"F235628112"+  , ID.fromSymbol @"G105851924"+  , ID.fromSymbol @"H247910878"+  , ID.fromSymbol @"I118949082"+  , ID.fromSymbol @"J218475156"+  , ID.fromSymbol @"K150252170"+  , ID.fromSymbol @"L298479266"+  , ID.fromSymbol @"M114415878"+  , ID.fromSymbol @"N242846162"+  , ID.fromSymbol @"O184333688"+  , ID.fromSymbol @"P257366789"+  , ID.fromSymbol @"Q163999855"+  , ID.fromSymbol @"R275744925"+  , ID.fromSymbol @"S158047168"+  , ID.fromSymbol @"T296696104"+  , ID.fromSymbol @"U108929984"+  , ID.fromSymbol @"V245356279"+  , ID.fromSymbol @"W127612989"+  , ID.fromSymbol @"X234128072"+  , ID.fromSymbol @"Y140531128"+  , ID.fromSymbol @"Z250358466"+  ]++idFromTuple :: Digit ~ d => (Letter, Digit1289, d, d, d, d, d, d, d) -> ID+idFromTuple (x0, x1, x2, x3, x4, x5, x6, x7, x8) =+  ID x0 x1 x2 x3 x4 x5 x6 x7 x8++idToTuple :: Digit ~ d => ID -> (Letter, Digit1289, d, d, d, d, d, d, d)+idToTuple (ID x0 x1 x2 x3 x4 x5 x6 x7 x8) =+  (x0, x1, x2, x3, x4, x5, x6, x7, x8)
+ components/roc-id-test/Test/QuickCheck/Classes/Hspec.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++-- | Provides testing functions to check that type class instances obey laws.+module Test.QuickCheck.Classes.Hspec+    ( testLaws+    , testLawsMany+    ) where++import Prelude++import Control.Monad+    ( forM_+    )+import Data.Proxy+    ( Proxy (..)+    )+import Data.Typeable+    ( Typeable+    , typeRep+    )+import Test.Hspec+    ( Spec+    , describe+    , it+    , parallel+    )+import Test.QuickCheck.Classes+    ( Laws (..)+    )++-- | Constructs a test to check that the given type class instance obeys the+--   given set of laws.+--+-- Example usage:+--+-- >>> testLaws @Natural ordLaws+-- >>> testLaws @(Map Int) functorLaws+testLaws+    :: forall a+     . Typeable a+    => (Proxy a -> Laws)+    -> Spec+testLaws getLaws =+    parallel+        $ describe description+        $ forM_ (lawsProperties laws)+        $ uncurry it+    where+        description =+            mconcat+                [ "Testing "+                , lawsTypeclass laws+                , " laws for type "+                , show (typeRep $ Proxy @a)+                ]+        laws = getLaws $ Proxy @a++-- | Calls `testLaws` with multiple sets of laws.+--+-- Example usage:+--+-- >>> testLawsMany @Natural [eqLaws, ordLaws]+-- >>> testLawsMany @(Map Int) [foldableLaws, functorLaws]+testLawsMany+    :: forall a+     . Typeable a+    => [Proxy a -> Laws]+    -> Spec+testLawsMany getLawsMany =+    testLaws @a `mapM_` getLawsMany
+ components/roc-id/ROC/ID.hs view
@@ -0,0 +1,361 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++module ROC.ID+  (+  -- * Type+    ID (..)++  -- * Construction+  , fromSymbol+  , fromText+  , FromTextError (..)++  -- * Conversion+  , toText++  -- * Validity+  , checksumDigit++  -- * Generation+  , generate++  -- * Inspection+  , getGender+  , getLocation+  , getNationality++  -- * Modification+  , setGender+  , setLocation+  , setNationality+  )+  where++import Control.Monad.Random.Class+  ( MonadRandom )+import Data.Bifunctor+  ( Bifunctor (first) )+import Data.Proxy+  ( Proxy (Proxy) )+import Data.Text+  ( Text )+import GHC.TypeLits+  ( Symbol, symbolVal )+import ROC.ID.CharIndex+  ( CharIndex (..) )+import ROC.ID.CharSet+  ( CharSet (..) )+import ROC.ID.Digit+  ( Digit (..) )+import ROC.ID.Digit1289+  ( Digit1289 (..) )+import ROC.ID.Gender+  ( Gender (..) )+import ROC.ID.Letter+  ( Letter (..) )+import ROC.ID.Location+  ( Location )+import ROC.ID.Nationality+  ( Nationality (..) )+import ROC.ID.Unchecked+  ( UncheckedID (UncheckedID)+  , ValidID+  )+import ROC.ID.Utilities+  ( guard )+import Text.Read+  ( Lexeme (Ident, Symbol, Punc), Read (readPrec), lexP, parens )++import qualified Data.Text as T+import qualified ROC.ID.Digit as Digit+import qualified ROC.ID.Digit1289 as Digit1289+import qualified ROC.ID.Letter as Letter+import qualified ROC.ID.Location as Location+import qualified ROC.ID.Unchecked as U++-- |+-- $setup+-- >>> :set -XDataKinds+-- >>> :set -XTypeApplications+-- >>> import ROC.ID+-- >>> import qualified ROC.ID as ID++--------------------------------------------------------------------------------+-- Type+--------------------------------------------------------------------------------++-- | Represents a __valid__ 10-digit ROC (Taiwan) Uniform Identification Number+-- (中華民國統一證號) of the form __@A123456789@__.+--+-- By construction, invalid identification numbers are __not representable__ by+-- this data type.+--+-- To guarantee correctness, an 'ID' value does __not__ store the terminal+-- checksum digit of the identification number it represents.+--+-- To compute the checksum digit, use 'checksumDigit'.+--+data ID = ID+  { c0 :: !Letter+  , c1 :: !Digit1289+  , c2 :: !Digit+  , c3 :: !Digit+  , c4 :: !Digit+  , c5 :: !Digit+  , c6 :: !Digit+  , c7 :: !Digit+  , c8 :: !Digit+  }+  deriving (Eq, Ord)++instance Read ID where+  readPrec = parens $ do+    Ident  "ID"         <- lexP+    Symbol "."          <- lexP+    Ident  "fromSymbol" <- lexP+    Punc   "@"          <- lexP+    unsafeFromText <$> readPrec++instance Show ID where+  showsPrec _ s =+    showString "ID.fromSymbol @" . shows (toText s)++--------------------------------------------------------------------------------+-- Construction+--------------------------------------------------------------------------------++-- | Constructs an 'ID' from a type-level textual symbol.+--+-- The symbol must be exactly 10 characters in length and of the form+-- __@A123456789@__:+--+-- >>> ID.fromSymbol @"A123456789"+-- ID.fromSymbol @"A123456789"+--+-- More precisely:+--+--  - the symbol must match the regular expression __@^[A-Z][1289][0-9]{8}$@__+--  - the resultant ID must have a valid checksum.+--+-- Failure to satisfy these constraints will result in one of the following+-- __type errors__:+--+-- === Invalid lengths+--+-- >>> ID.fromSymbol @"A12345678"+-- ...+-- ... An ID must have exactly 10 characters.+-- ...+--+-- === Invalid checksums+--+-- >>> ID.fromSymbol @"A123456780"+-- ...+-- ... ID has invalid checksum.+-- ...+--+-- === Invalid characters+--+-- On detection of an invalid character, the resulting type error reports+-- both the position of the character and the set of characters permitted+-- at that position.+--+-- >>> ID.fromSymbol @"_123456789"+-- ...+--     • "_123456789"+--        ^+--       Character at this position must be an uppercase letter.+-- ...+--+-- >>> ID.fromSymbol @"A_23456789"+-- ...+--     • "A_23456789"+--         ^+--       Character at this position must be a digit from the set {1, 2, 8, 9}.+-- ...+--+-- >>> ID.fromSymbol @"A1_3456789"+-- ...+--     • "A1_3456789"+--          ^+--       Character at this position must be a digit in the range [0 .. 9].+-- ...+--+fromSymbol :: forall (s :: Symbol). ValidID s => ID+fromSymbol = unsafeFromText $ T.pack $ symbolVal $ Proxy @s++-- | Attempts to construct an 'ID' from 'Text'.+--+-- The input must be exactly 10 characters in length and of the form+-- __@A123456789@__.+--+-- More precisely, the input must match the regular expression+-- __@^[A-Z][1289][0-9]{8}$@__, and the resultant ID must have a valid+-- checksum.+--+-- This function satisfies the following law:+--+-- @+-- 'fromText' ('toText' i) '==' 'Right' i+-- @+--+fromText :: Text -> Either FromTextError ID+fromText text = do+    unchecked <- first fromUncheckedError $ U.fromText text+    guard InvalidChecksum $ fromUnchecked unchecked+  where+    fromUncheckedError :: U.FromTextError -> FromTextError+    fromUncheckedError = \case+      U.TextTooShort ->+        TextTooShort+      U.TextTooLong ->+        TextTooLong+      U.InvalidChar i r ->+        InvalidChar i r++unsafeFromText :: Text -> ID+unsafeFromText t =+  case fromText t of+    Left _ -> error "unsafeFromText"+    Right i -> i++-- | Indicates an error that occurred while constructing an 'ID' from 'Text'.+--+data FromTextError++  = TextTooShort+  -- ^ Indicates that the input text is too short.++  | TextTooLong+  -- ^ Indicates that the input text is too long.++  | InvalidChar CharIndex CharSet+  -- ^ Indicates that the input text contains a character that is not allowed.+  --+  --   - `CharIndex` specifies the position of the offending character.+  --   - `CharSet` specifies the set of characters allowed at that position.++  | InvalidChecksum+  -- ^ Indicates that the parsed identification number has an invalid checksum.++  deriving (Eq, Ord, Show)++--------------------------------------------------------------------------------+-- Conversion+--------------------------------------------------------------------------------++-- | Converts an 'ID' to 'Text'.+--+-- The output is of the form __@A123456789@__.+--+-- This function satisfies the following law:+--+-- @+-- 'fromText' ('toText' i) '==' 'Right' i+-- @+--+toText :: ID -> Text+toText = U.toText . toUnchecked++--------------------------------------------------------------------------------+-- Validity+--------------------------------------------------------------------------------++-- | Computes the terminal checksum digit of an 'ID'.+--+checksumDigit :: ID -> Digit+checksumDigit (ID u0 u1 u2 u3 u4 u5 u6 u7 u8) =+  negate $ U.checksum (UncheckedID u0 u1 u2 u3 u4 u5 u6 u7 u8 0)++--------------------------------------------------------------------------------+-- Generation+--------------------------------------------------------------------------------++-- | Generates a random 'ID'.+--+generate :: MonadRandom m => m ID+generate =+  ID+    <$> Letter.generate+    <*> Digit1289.generate+    <*> Digit.generate+    <*> Digit.generate+    <*> Digit.generate+    <*> Digit.generate+    <*> Digit.generate+    <*> Digit.generate+    <*> Digit.generate++--------------------------------------------------------------------------------+-- Inspection+--------------------------------------------------------------------------------++-- | Decodes the 'Gender' component of an 'ID'.+--+getGender :: ID -> Gender+getGender ID {c1} = fst $ decodeC1 c1++-- | Decodes the 'Location' component of an 'ID'.+--+getLocation :: ID -> Location+getLocation ID {c0} = Location.fromLetter c0++-- | Decodes the 'Nationality' component of an 'ID'.+--+getNationality :: ID -> Nationality+getNationality ID {c1} = snd $ decodeC1 c1++--------------------------------------------------------------------------------+-- Modification+--------------------------------------------------------------------------------++-- | Updates the 'Gender' component of an 'ID'.+--+setGender :: Gender -> ID -> ID+setGender gender i = i {c1 = encodeC1 (gender, getNationality i)}++-- | Updates the 'Location' component of an 'ID'.+--+setLocation :: Location -> ID -> ID+setLocation location i = i {c0 = Location.toLetter location}++-- | Updates the 'Nationality' component of an 'ID'.+--+setNationality :: Nationality -> ID -> ID+setNationality nationality i = i {c1 = encodeC1 (getGender i, nationality)}++--------------------------------------------------------------------------------+-- Internal+--------------------------------------------------------------------------------++decodeC1 :: Digit1289 -> (Gender, Nationality)+decodeC1 = \case+  D1289_1 -> (  Male,    National)+  D1289_2 -> (Female,    National)+  D1289_8 -> (  Male, NonNational)+  D1289_9 -> (Female, NonNational)++encodeC1 :: (Gender, Nationality) -> Digit1289+encodeC1 = \case+  (  Male,    National) -> D1289_1+  (Female,    National) -> D1289_2+  (  Male, NonNational) -> D1289_8+  (Female, NonNational) -> D1289_9++fromUnchecked :: UncheckedID -> Maybe ID+fromUnchecked u@(UncheckedID u0 u1 u2 u3 u4 u5 u6 u7 u8 _) =+    case U.checksumValidity u of+      U.ChecksumValid   -> Just i+      U.ChecksumInvalid -> Nothing+  where+    i = ID u0 u1 u2 u3 u4 u5 u6 u7 u8++toUnchecked :: ID -> UncheckedID+toUnchecked i@(ID u0 u1 u2 u3 u4 u5 u6 u7 u8) =+  UncheckedID u0 u1 u2 u3 u4 u5 u6 u7 u8 (checksumDigit i)
+ components/roc-id/ROC/ID/CharIndex.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module ROC.ID.CharIndex+  ( CharIndex (..)+  )+  where++-- | Specifies the zero-based position of a character within a string.+--+newtype CharIndex = CharIndex Int+  deriving stock (Read, Show)+  deriving newtype (Enum, Eq, Num, Ord)
+ components/roc-id/ROC/ID/CharSet.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE DerivingStrategies #-}++module ROC.ID.CharSet+  ( CharSet (..)+  )+  where++import Data.Set.NonEmpty+  ( NESet )++-- | Specifies a set of characters.+--+data CharSet+  = CharSet (NESet Char)+  -- ^ An explicit set of characters.+  | CharRange Char Char+  -- ^ An inclusive range of characters.+  deriving stock (Eq, Ord, Read, Show)
+ components/roc-id/ROC/ID/Digit.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module ROC.ID.Digit+  ( Digit (..)+  , fromChar+  , toChar+  , generate+  , FromChar+  , FromNat+  , ToNat+  ) where++import Control.Monad.Random+  ( MonadRandom )+import GHC.Generics+  ( Generic )+import GHC.TypeNats+  ( Nat )+import ROC.ID.Utilities+  ( maybeBoundedEnum, randomBoundedEnum )+import Text.Read+  ( Read (readPrec), readMaybe )++import Prelude hiding+  ( fromIntegral )++import qualified Prelude++-- | Represents a single decimal digit in the range @0@ to @9@.+--+data Digit+  = D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | D9+  deriving (Bounded, Enum, Eq, Generic, Ord)++-- | Arithmetic modulo 10.+--+instance Num Digit where+  a + b = fromIntegral (fromEnum a + fromEnum b)+  a * b = fromIntegral (fromEnum a * fromEnum b)+  a - b = fromIntegral (fromEnum a - fromEnum b)+  abs a = a+  fromInteger = fromIntegral+  signum D0 = D0+  signum __ = D1++instance Read Digit where readPrec = fromInteger <$> readPrec++instance Show Digit where show = show . fromEnum++-- | Attempts to parse a 'Digit' from a character.+--+-- The 'Char' must be a decimal digit in the range @0@ to @9@.+--+fromChar :: Char -> Maybe Digit+fromChar c = readMaybe [c] >>= maybeBoundedEnum++-- | Converts a 'Digit' to a decimal digit character.+--+toChar :: Digit -> Char+toChar digit = case show digit of+  [c] -> c+  _ -> error "toChar"++-- | Creates a 'Digit' from an integral number (modulo 10).+--+fromIntegral :: Integral i => i -> Digit+fromIntegral i = toEnum (Prelude.fromIntegral (i `mod` 10))++-- | Generates a random digit.+--+generate :: MonadRandom m => m Digit+generate = randomBoundedEnum++type family FromChar (c :: Char) :: Maybe Digit where+  FromChar '0' = Just D0+  FromChar '1' = Just D1+  FromChar '2' = Just D2+  FromChar '3' = Just D3+  FromChar '4' = Just D4+  FromChar '5' = Just D5+  FromChar '6' = Just D6+  FromChar '7' = Just D7+  FromChar '8' = Just D8+  FromChar '9' = Just D9+  FromChar _   = Nothing++type family FromNat (n :: Nat) :: Maybe Digit where+  FromNat 0 = Just D0+  FromNat 1 = Just D1+  FromNat 2 = Just D2+  FromNat 3 = Just D3+  FromNat 4 = Just D4+  FromNat 5 = Just D5+  FromNat 6 = Just D6+  FromNat 7 = Just D7+  FromNat 8 = Just D8+  FromNat 9 = Just D9+  FromNat _ = Nothing++type family ToNat (d :: Digit) :: Nat where+  ToNat D0 = 0+  ToNat D1 = 1+  ToNat D2 = 2+  ToNat D3 = 3+  ToNat D4 = 4+  ToNat D5 = 5+  ToNat D6 = 6+  ToNat D7 = 7+  ToNat D8 = 8+  ToNat D9 = 9
+ components/roc-id/ROC/ID/Digit1289.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module ROC.ID.Digit1289+  ( Digit1289 (..)+  , fromChar+  , toChar+  , toDigit+  , generate+  , FromChar+  , FromNat+  , ToNat+  ) where++import Control.Monad.Random+  ( MonadRandom )+import GHC.Generics+  ( Generic )+import GHC.TypeNats+  ( Nat )+import ROC.ID.Digit+  ( Digit (..) )+import ROC.ID.Utilities+  ( randomBoundedEnum )++-- | Represents a single decimal digit from the set {@1@, @2@, @8@, @9@}.+--+data Digit1289+  = D1289_1 | D1289_2 | D1289_8 | D1289_9+  deriving (Bounded, Enum, Eq, Generic, Ord)++instance Show Digit1289 where+  show = (: []) . toChar++-- | Attempts to parse a 'Digit1289' from a character.+--+-- Only the characters @'1'@, @'2'@, @'8'@, and @'9'@ are accepted.+--+fromChar :: Char -> Maybe Digit1289+fromChar = \case+  '1' -> Just D1289_1+  '2' -> Just D1289_2+  '8' -> Just D1289_8+  '9' -> Just D1289_9+  _   -> Nothing++-- | Converts a 'Digit1289' to a decimal digit character.+--+toChar :: Digit1289 -> Char+toChar = \case+  D1289_1 -> '1'+  D1289_2 -> '2'+  D1289_8 -> '8'+  D1289_9 -> '9'++-- | Converts a 'Digit1289' to an ordinary 'Digit'.+--+toDigit :: Digit1289 -> Digit+toDigit = \case+  D1289_1 -> D1+  D1289_2 -> D2+  D1289_8 -> D8+  D1289_9 -> D9++-- | Generates a random 'Digit1289'.+--+generate :: MonadRandom m => m Digit1289+generate = randomBoundedEnum++type family FromChar (c :: Char) :: Maybe Digit1289 where+  FromChar '1' = Just D1289_1+  FromChar '2' = Just D1289_2+  FromChar '8' = Just D1289_8+  FromChar '9' = Just D1289_9+  FromChar _   = Nothing++type family FromNat (n :: Nat) :: Maybe Digit1289 where+  FromNat 1 = Just D1289_1+  FromNat 2 = Just D1289_2+  FromNat 8 = Just D1289_8+  FromNat 9 = Just D1289_9+  FromNat _ = Nothing++type family ToNat (d :: Digit1289) :: Nat where+  ToNat D1289_1 = 1+  ToNat D1289_2 = 2+  ToNat D1289_8 = 8+  ToNat D1289_9 = 9
+ components/roc-id/ROC/ID/Gender.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module ROC.ID.Gender+  ( Gender (..)+  , toText+  , generate+  ) where++import Control.Monad.Random.Class+  ( MonadRandom (..) )+import Data.Text+  ( Text )+import GHC.Generics+  ( Generic )+import ROC.ID.Language+  ( Language (..) )+import ROC.ID.Utilities+  ( randomBoundedEnum )++-- | A person's gender, encodable within an ROC identification number.+--+data Gender = Male | Female+  deriving (Bounded, Enum, Eq, Generic, Ord, Read, Show)++-- | Prints the specified 'Gender'.+--+toText :: Language -> Gender -> Text+toText = \case+  English -> toTextEnglish+  Chinese -> toTextChinese++toTextEnglish :: Gender -> Text+toTextEnglish = \case+  Male   -> "Male"+  Female -> "Female"++toTextChinese :: Gender -> Text+toTextChinese = \case+  Male   -> "男性"+  Female -> "女性"++-- | Generates a random 'Gender'.+--+generate :: MonadRandom m => m Gender+generate = randomBoundedEnum
+ components/roc-id/ROC/ID/Language.hs view
@@ -0,0 +1,5 @@+module ROC.ID.Language where++-- | A language into which values can be localized when pretty printing.+--+data Language = English | Chinese
+ components/roc-id/ROC/ID/Letter.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module ROC.ID.Letter+  ( Letter (..)+  , fromChar+  , toChar+  , generate+  , FromChar+  ) where++import Control.Monad.Random.Class+  ( MonadRandom (..) )+import GHC.Generics+  ( Generic )+import ROC.ID.Utilities+  ( randomBoundedEnum )+import Text.Read+  ( readMaybe )++-- | Represents a letter in the latin alphabet.+--+data Letter+  = A | B | C | D | E | F | G | H | I | J | K | L | M+  | N | O | P | Q | R | S | T | U | V | W | X | Y | Z+  deriving (Bounded, Enum, Eq, Generic, Ord, Read, Show)++-- | Attempts to parse a 'Letter' from a character.+--+-- Returns 'Nothing' if the specified character is not an uppercase alphabetic+-- character.+--+fromChar :: Char -> Maybe Letter+fromChar c = readMaybe [c]++-- | Converts the specified 'Letter' to a character.+--+toChar :: Letter -> Char+toChar letter = case show letter of+  [c] -> c+  _ -> error "toChar"++-- | Generates a random 'Letter'.+--+generate :: MonadRandom m => m Letter+generate = randomBoundedEnum++type family FromChar (c :: Char) :: Maybe Letter where+  FromChar 'A' = Just A; FromChar 'N' = Just N+  FromChar 'B' = Just B; FromChar 'O' = Just O+  FromChar 'C' = Just C; FromChar 'P' = Just P+  FromChar 'D' = Just D; FromChar 'Q' = Just Q+  FromChar 'E' = Just E; FromChar 'R' = Just R+  FromChar 'F' = Just F; FromChar 'S' = Just S+  FromChar 'G' = Just G; FromChar 'T' = Just T+  FromChar 'H' = Just H; FromChar 'U' = Just U+  FromChar 'I' = Just I; FromChar 'V' = Just V+  FromChar 'J' = Just J; FromChar 'W' = Just W+  FromChar 'K' = Just K; FromChar 'X' = Just X+  FromChar 'L' = Just L; FromChar 'Y' = Just Y+  FromChar 'M' = Just M; FromChar 'Z' = Just Z+  FromChar _ = Nothing
+ components/roc-id/ROC/ID/Location.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module ROC.ID.Location+  ( Location+  , fromLetter+  , toLetter+  , toText+  , generate+  ) where++import Control.Monad.Random.Class+  ( MonadRandom (..) )+import Data.Text+  ( Text )+import GHC.Generics+  ( Generic )+import ROC.ID.Language+  ( Language (..) )+import ROC.ID.Letter+  ( Letter (..) )+import Text.Read+  ( Lexeme (Ident, Symbol), Read (readPrec), lexP, parens )++import qualified ROC.ID.Letter as Letter++-- | Represents a location, encodable within an ROC identification number.+--+-- == Location codes+--+-- Every location that can be represented in an ROC identification number has a+-- unique code:+--+-- +------+---------+-------------------++-- | Code | Chinese | English           |+-- +======+=========+===================++-- | @A@  | 臺北市     | Taipei City       |+-- +------+---------+-------------------++-- | @B@  | 臺中市     | Taichung City     |+-- +------+---------+-------------------++-- | @C@  | 基隆市     | Keelung City      |+-- +------+---------+-------------------++-- | @D@  | 臺南市     | Tainan City       |+-- +------+---------+-------------------++-- | @E@  | 高雄市     | Kaohsiung City    |+-- +------+---------+-------------------++-- | @F@  | 新北市     | New Taipei City   |+-- +------+---------+-------------------++-- | @G@  | 宜蘭縣     | Yilan County      |+-- +------+---------+-------------------++-- | @H@  | 桃園市     | Taoyuan City      |+-- +------+---------+-------------------++-- | @I@  | 嘉義市     | Chiayi City       |+-- +------+---------+-------------------++-- | @J@  | 新竹縣     | Hsinchu County    |+-- +------+---------+-------------------++-- | @K@  | 苗栗縣     | Miaoli County     |+-- +------+---------+-------------------++-- | @L@  | 臺中縣     | Taichung County   |+-- +------+---------+-------------------++-- | @M@  | 南投縣     | Nantou County     |+-- +------+---------+-------------------++-- | @N@  | 彰化縣     | Changhua County   |+-- +------+---------+-------------------++-- | @O@  | 新竹市     | Hsinchu City      |+-- +------+---------+-------------------++-- | @P@  | 雲林縣     | Yunlin County     |+-- +------+---------+-------------------++-- | @Q@  | 嘉義縣     | Chiayi County     |+-- +------+---------+-------------------++-- | @R@  | 臺南縣     | Tainan County     |+-- +------+---------+-------------------++-- | @S@  | 高雄縣     | Kaohsiung County  |+-- +------+---------+-------------------++-- | @T@  | 屏東縣     | Pingtung County   |+-- +------+---------+-------------------++-- | @U@  | 花蓮縣     | Hualien County    |+-- +------+---------+-------------------++-- | @V@  | 臺東縣     | Taitung County    |+-- +------+---------+-------------------++-- | @W@  | 金門縣     | Kinmen County     |+-- +------+---------+-------------------++-- | @X@  | 澎湖縣     | Penghu County     |+-- +------+---------+-------------------++-- | @Y@  | 陽明山     | Yangmingshan      |+-- +------+---------+-------------------++-- | @Z@  | 連江縣     | Lienchiang County |+-- +------+---------+-------------------++--+-- == Usage+--+-- To construct a 'Location' from its letter code, use the 'fromLetter'+-- function.+--+-- To print the full name of a 'Location', use the 'toText' function.+--+-- To generate a random 'Location', use the 'generate' function.+--+newtype Location = Location Letter+  deriving stock (Eq, Generic, Ord)+  deriving newtype (Bounded, Enum)++instance Read Location where+  readPrec = parens $ do+    Ident "Location"   <- lexP+    Symbol "."         <- lexP+    Ident "fromLetter" <- lexP+    fromLetter <$> readPrec++instance Show Location where+  showsPrec _ s =+    showString "Location.fromLetter " . shows (toLetter s)++-- | Constructs a 'Location' from its corresponding letter code.+--+fromLetter :: Letter -> Location+fromLetter = Location++-- | Converts a 'Location' to its corresponding letter code.+--+toLetter :: Location -> Letter+toLetter (Location letter) = letter++-- | Prints the specified 'Location'.+toText :: Language -> Location -> Text+toText = \case+  English -> toTextEnglish+  Chinese -> toTextChinese++toTextChinese :: Location -> Text+toTextChinese (Location letter) = case letter of+  A -> "臺北市"+  B -> "臺中市"+  C -> "基隆市"+  D -> "臺南市"+  E -> "高雄市"+  F -> "新北市"+  G -> "宜蘭縣"+  H -> "桃園市"+  I -> "嘉義市"+  J -> "新竹縣"+  K -> "苗栗縣"+  L -> "臺中縣"+  M -> "南投縣"+  N -> "彰化縣"+  O -> "新竹市"+  P -> "雲林縣"+  Q -> "嘉義縣"+  R -> "臺南縣"+  S -> "高雄縣"+  T -> "屏東縣"+  U -> "花蓮縣"+  V -> "臺東縣"+  W -> "金門縣"+  X -> "澎湖縣"+  Y -> "陽明山"+  Z -> "連江縣"++toTextEnglish :: Location -> Text+toTextEnglish (Location letter) = case letter of+  A -> "Taipei City"+  B -> "Taichung City"+  C -> "Keelung City"+  D -> "Tainan City"+  E -> "Kaohsiung City"+  F -> "New Taipei City"+  G -> "Yilan County"+  H -> "Taoyuan City"+  I -> "Chiayi City"+  J -> "Hsinchu County"+  K -> "Miaoli County"+  L -> "Taichung County"+  M -> "Nantou County"+  N -> "Changhua County"+  O -> "Hsinchu City"+  P -> "Yunlin County"+  Q -> "Chiayi County"+  R -> "Tainan County"+  S -> "Kaohsiung County"+  T -> "Pingtung County"+  U -> "Hualien County"+  V -> "Taitung County"+  W -> "Kinmen County"+  X -> "Penghu County"+  Y -> "Yangmingshan"+  Z -> "Lienchiang County"++-- | Generates a random 'Location'.+--+generate :: MonadRandom m => m Location+generate = Location <$> Letter.generate
+ components/roc-id/ROC/ID/Nationality.hs view
@@ -0,0 +1,20 @@+module ROC.ID.Nationality+  ( Nationality (..)+  , generate+  )+  where++import Control.Monad.Random.Class+  ( MonadRandom (..) )+import ROC.ID.Utilities+  ( randomBoundedEnum )++-- | Specifies a person's nationality.+--+data Nationality = National | NonNational+  deriving (Bounded, Enum, Eq, Ord, Read, Show)++-- | Generates a random 'Nationality'.+--+generate :: MonadRandom m => m Nationality+generate = randomBoundedEnum
+ components/roc-id/ROC/ID/Unchecked.hs view
@@ -0,0 +1,301 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}++module ROC.ID.Unchecked+  ( UncheckedID (..)+  , UncheckedIDTuple+  , FromTextError (..)+  , fromText+  , toText+  , checksum+  , checksumValidity+  , ChecksumValidity (..)+  , ValidID+  )+  where++import Control.Monad+  ( when )+import Data.Kind+  ( Constraint )+import Data.List.NonEmpty+  ( NonEmpty ((:|)) )+import Data.Text+  ( Text )+import Data.Type.Equality+  ( type (==) )+import GHC.TypeError+  ( Assert, TypeError )+import GHC.TypeLits+  ( AppendSymbol, KnownSymbol, Symbol )+import GHC.TypeNats+  ( Mod, Nat, type (+) )+import ROC.ID.CharIndex+  ( CharIndex (..) )+import ROC.ID.CharSet+  ( CharSet (..) )+import ROC.ID.Digit+  ( Digit (..) )+import ROC.ID.Digit1289+  ( Digit1289 (..) )+import ROC.ID.Letter+  ( Letter (..) )+import ROC.ID.Utilities+  ( FromJust, Fst, ReplicateChar, Snd, SymbolToCharList, guard )++import qualified Data.Set.NonEmpty as NESet+import qualified Data.Text as T+import qualified GHC.TypeError as TypeError+import qualified GHC.TypeNats as N+import qualified ROC.ID.Digit as Digit+import qualified ROC.ID.Digit1289 as Digit1289+import qualified ROC.ID.Letter as Letter++data UncheckedID = UncheckedID+  { c0 :: !Letter+  , c1 :: !Digit1289+  , c2 :: !Digit+  , c3 :: !Digit+  , c4 :: !Digit+  , c5 :: !Digit+  , c6 :: !Digit+  , c7 :: !Digit+  , c8 :: !Digit+  , c9 :: !Digit+  }+  deriving stock (Eq, Ord, Show)++type UncheckedIDTuple =+  ( Letter+  , Digit1289+  , Digit+  , Digit+  , Digit+  , Digit+  , Digit+  , Digit+  , Digit+  , Digit+  )++data FromTextError+  = TextTooShort+  | TextTooLong+  | InvalidChar CharIndex CharSet+  deriving stock (Eq, Ord, Read, Show)++type Parser a = Text -> Either FromTextError (Text, a)++fromText :: Text -> Either FromTextError UncheckedID+fromText text0 = do+    when (T.length text0 > 10) $ Left TextTooLong+    (text1,  c0                             ) <- parseLetter    text0+    (text2,  c1                             ) <- parseDigit1289 text1+    (_____, (c2, c3, c4, c5, c6, c7, c8, c9)) <- parseDigits    text2+    pure UncheckedID {c0, c1, c2, c3, c4, c5, c6, c7, c8, c9}+  where+    parseLetter :: Parser Letter+    parseLetter text = do+      (char, remainder) <- guard TextTooShort $ T.uncons text+      letter <- guard (InvalidChar 0 (CharRange 'A' 'Z')) (Letter.fromChar char)+      pure (remainder, letter)++    parseDigit1289 :: Parser Digit1289+    parseDigit1289 text = do+      (char, remainder) <- guard TextTooShort $ T.uncons text+      digit1289 <- guard+        (InvalidChar 1 (CharSet $ NESet.fromList $ '1' :| ['2', '8', '9']))+        (Digit1289.fromChar char)+      pure (remainder, digit1289)++    parseDigits :: d ~ Digit => Parser (d, d, d, d, d, d, d, d)+    parseDigits text = do+        let (chars, remainder) = T.splitAt 8 text+        digitList <- traverse parseIndexedDigit (zip [2 ..] $ T.unpack chars)+        digitTuple <- guard TextTooShort (listToTuple8 digitList)+        pure (remainder, digitTuple)+      where+        parseIndexedDigit (i, c) =+          guard (InvalidChar i (CharRange '0' '9')) (Digit.fromChar c)++toText :: UncheckedID -> Text+toText UncheckedID {c0, c1, c2, c3, c4, c5, c6, c7, c8, c9} =+  T.pack+    ( Letter.toChar c0+    : Digit1289.toChar c1+    : fmap Digit.toChar [c2, c3, c4, c5, c6, c7, c8, c9]+    )++checksum :: UncheckedID -> Digit+checksum (UncheckedID u0 (Digit1289.toDigit -> u1) u2 u3 u4 u5 u6 u7 u8 u9) =+  sum $ zipWith (*)+    [ 1,  9,  8,  7,  6,  5,  4,  3,  2,  1,  1]+    [a0, a1, u1, u2, u3, u4, u5, u6, u7, u8, u9]+  where+    (a0, a1) = checksumLetterToDigitPair u0++checksumLetterToDigitPair :: Letter -> (Digit, Digit)+checksumLetterToDigitPair = \case+  A -> (1, 0); N -> (2, 2)+  B -> (1, 1); O -> (3, 5)+  C -> (1, 2); P -> (2, 3)+  D -> (1, 3); Q -> (2, 4)+  E -> (1, 4); R -> (2, 5)+  F -> (1, 5); S -> (2, 6)+  G -> (1, 6); T -> (2, 7)+  H -> (1, 7); U -> (2, 8)+  I -> (3, 4); V -> (2, 9)+  J -> (1, 8); W -> (3, 2)+  K -> (1, 9); X -> (3, 0)+  L -> (2, 0); Y -> (3, 1)+  M -> (2, 1); Z -> (3, 3)++data ChecksumValidity+  = ChecksumValid+  | ChecksumInvalid+  deriving (Eq, Show)++checksumValidity :: UncheckedID -> ChecksumValidity+checksumValidity u =+  case checksum u of+    0 -> ChecksumValid+    _ -> ChecksumInvalid++listToTuple8 :: [a] -> Maybe (a, a, a, a, a, a, a, a)+listToTuple8 = \case+  (a0 : a1 : a2 : a3 : a4 : a5 : a6 : a7 : _) ->+    Just (a0, a1, a2, a3, a4, a5, a6, a7)+  _ ->+    Nothing++type family ChecksumValid (id :: UncheckedIDTuple) :: Constraint where+  ChecksumValid id =+    Assert+      (ChecksumDigit id == D0)+      (TypeError (TypeError.Text "ID has invalid checksum."))++type family ChecksumDigit (id :: UncheckedIDTuple) :: Digit where+  ChecksumDigit id = ChecksumDigitFromNat (Mod (Checksum id) 10)++type family ChecksumDigitFromNat (n :: Nat) :: Digit where+  ChecksumDigitFromNat n =+    FromJust+      (Digit.FromNat n)+      (TypeError (TypeError.Text "Expected natural number between 0 and 9."))++type family Checksum (id :: UncheckedIDTuple) :: Nat where+  Checksum '(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9) =+    (     (ChecksumLetterToNat0 c0 N.* 1)+      N.+ (ChecksumLetterToNat1 c0 N.* 9)+      N.+ (Digit1289.ToNat      c1 N.* 8)+      N.+ (Digit.ToNat          c2 N.* 7)+      N.+ (Digit.ToNat          c3 N.* 6)+      N.+ (Digit.ToNat          c4 N.* 5)+      N.+ (Digit.ToNat          c5 N.* 4)+      N.+ (Digit.ToNat          c6 N.* 3)+      N.+ (Digit.ToNat          c7 N.* 2)+      N.+ (Digit.ToNat          c8 N.* 1)+      N.+ (Digit.ToNat          c9 N.* 1)+    )++type family ChecksumLetterToNat0 (letter :: Letter) :: Nat where+  ChecksumLetterToNat0 x = Fst (ChecksumLetterToNatPair x)++type family ChecksumLetterToNat1 (letter :: Letter) :: Nat where+  ChecksumLetterToNat1 x = Snd (ChecksumLetterToNatPair x)++type family ChecksumLetterToNatPair (l :: Letter) :: (Nat, Nat) where+  ChecksumLetterToNatPair A = '(1, 0); ChecksumLetterToNatPair B = '(1, 1)+  ChecksumLetterToNatPair C = '(1, 2); ChecksumLetterToNatPair D = '(1, 3)+  ChecksumLetterToNatPair E = '(1, 4); ChecksumLetterToNatPair F = '(1, 5)+  ChecksumLetterToNatPair G = '(1, 6); ChecksumLetterToNatPair H = '(1, 7)+  ChecksumLetterToNatPair I = '(3, 4); ChecksumLetterToNatPair J = '(1, 8)+  ChecksumLetterToNatPair K = '(1, 9); ChecksumLetterToNatPair L = '(2, 0)+  ChecksumLetterToNatPair M = '(2, 1); ChecksumLetterToNatPair N = '(2, 2)+  ChecksumLetterToNatPair O = '(3, 5); ChecksumLetterToNatPair P = '(2, 3)+  ChecksumLetterToNatPair Q = '(2, 4); ChecksumLetterToNatPair R = '(2, 5)+  ChecksumLetterToNatPair S = '(2, 6); ChecksumLetterToNatPair T = '(2, 7)+  ChecksumLetterToNatPair U = '(2, 8); ChecksumLetterToNatPair V = '(2, 9)+  ChecksumLetterToNatPair W = '(3, 2); ChecksumLetterToNatPair X = '(3, 0)+  ChecksumLetterToNatPair Y = '(3, 1); ChecksumLetterToNatPair Z = '(3, 3)++type family SymbolToId (s :: Symbol) :: UncheckedIDTuple where+  SymbolToId s =+    IdFromCharList s (SymbolToCharList s)++type family+    IdFromCharList (id :: Symbol) (xs :: [Char]) :: UncheckedIDTuple+  where+    IdFromCharList id '[c0, c1, c2, c3, c4, c5, c6, c7, c8, c9] =+      '( LetterFromChar    id 0 c0+       , Digit1289FromChar id 1 c1+       , DigitFromChar     id 2 c2+       , DigitFromChar     id 3 c3+       , DigitFromChar     id 4 c4+       , DigitFromChar     id 5 c5+       , DigitFromChar     id 6 c6+       , DigitFromChar     id 7 c7+       , DigitFromChar     id 8 c8+       , DigitFromChar     id 9 c9+       )+    IdFromCharList _ _ =+      TypeError (TypeError.Text "An ID must have exactly 10 characters.")++type family+    DigitFromChar (id :: Symbol) (index :: Nat) (c :: Char) :: Digit+  where+    DigitFromChar id index c =+      FromJust+        (Digit.FromChar c)+        (InvalidCharError id index DigitTypeError)++type family+    Digit1289FromChar (id :: Symbol) (index :: Nat) (c :: Char) :: Digit1289+  where+    Digit1289FromChar id index c =+      FromJust+        (Digit1289.FromChar c)+        (InvalidCharError id index Digit1289TypeError)++type family+    LetterFromChar (id :: Symbol) (index :: Nat) (c :: Char) :: Letter+  where+    LetterFromChar id index c =+      FromJust+        (Letter.FromChar c)+        (InvalidCharError id index LetterTypeError)++type DigitTypeError =+  "Character at this position must be a digit in the range [0 .. 9]."++type Digit1289TypeError =+  "Character at this position must be a digit from the set {1, 2, 8, 9}."++type LetterTypeError =+  "Character at this position must be an uppercase letter."++type family InvalidCharError+    (invalidId :: Symbol) (charIndex :: Nat) (message :: Symbol)+  where+    InvalidCharError invalidId charIndex message =+      TypeError+        ( TypeError.ShowType invalidId+          TypeError.:$$:+          TypeError.Text (ReplicateChar (charIndex + 1) ' ' `AppendSymbol` "^")+          TypeError.:$$:+          TypeError.Text message+        )++type ValidID s =+  ( KnownSymbol s+  , ChecksumValid (SymbolToId s)+  ) :: Constraint
+ components/roc-id/ROC/ID/Utilities.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module ROC.ID.Utilities where++import Control.Monad.Random.Class+  ( MonadRandom (..) )+import GHC.TypeLits+  ( Symbol, UnconsSymbol, ConsSymbol )+import GHC.TypeNats+  ( Nat, type (-) )++guard :: x -> Maybe y -> Either x y+guard x = maybe (Left x) Right++maybeBoundedEnum :: forall a. (Bounded a, Enum a) => Int -> Maybe a+maybeBoundedEnum i+  | i < fromEnum (minBound :: a) = Nothing+  | i > fromEnum (maxBound :: a) = Nothing+  | otherwise                    = pure $ toEnum i++randomBoundedEnum :: forall m a. (MonadRandom m, Bounded a, Enum a) => m a+randomBoundedEnum =+  toEnum <$> getRandomR (fromEnum (minBound :: a), fromEnum (maxBound :: a))++type family FromJust (m :: Maybe a) e where+  FromJust Nothing e = e+  FromJust (Just a) _ = a++type family Fst (t :: (a, a)) :: a where+  Fst '(x, _) = x++type family Snd (t :: (a, a)) :: a where+  Snd '(_, y) = y++type family+    SymbolToCharList (s :: Symbol) :: [Char]+  where+    SymbolToCharList s = MaybeCharSymbolToCharList (UnconsSymbol s)++type family+  MaybeCharSymbolToCharList+    (m :: Maybe (Char, Symbol)) :: [Char]+  where+    MaybeCharSymbolToCharList ('Just '(c, rest)) = c ': SymbolToCharList rest+    MaybeCharSymbolToCharList 'Nothing = '[]++type family ReplicateChar (n :: Nat) (c :: Char) :: Symbol where+  ReplicateChar n c = ReplicateChar' "" n c++type family ReplicateChar' (s :: Symbol) (n :: Nat) (c :: Char) :: Symbol where+  ReplicateChar' s 0 _ = s+  ReplicateChar' s n c = ReplicateChar' (ConsSymbol c s) (n - 1) c
− library/ROC/ID.hs
@@ -1,160 +0,0 @@-{-# 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---- Types:---- | Represents a __valid__ 10-digit ROC national identification number--- (中華民國身份證號碼) of the form __@A123456789@__.------ By construction, invalid values are __not representable__ by this type.------ An identification number encodes a person's 'Gender', the 'Location' in--- which they first registered for an identification card, and a unique 'Serial'--- number.----data Identity = Identity-  { identityGender   :: Gender-  -- ^ The gender of the person to whom this ID number belongs.-  , identityLocation :: Location-  -- ^ The location in which the person first registered for an ID card.-  , identitySerial   :: Serial-  -- ^ The serial number portion of this ID number.-  } deriving (Eq, Generic, Ord)--instance Show Identity where-  show i@Identity {..} = ""-    <> show identityLocation-    <> foldMap show (toDigits identityGender)-    <> foldMap show (toDigits identitySerial)-    <> show (identityChecksum i)---- | Calculate the checksum of the specified 'Identity'.----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---- | Attempt to parse an 'Identity' using the specified 'Text' as input.------ The input must be of the form __@A123456789@__.----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---- | An error produced when parsing an 'Identity' with the 'parseIdentity'---   function.----data ParseError-  = InvalidLength-    -- ^ The input was either too short or too long.-  | InvalidGender-    -- ^ The gender portion of the input was invalid.-  | InvalidLocation-    -- ^ The location portion of the input included non-alphabetic characters.-  | InvalidSerial-    -- ^ The serial number portion of the input included non-numeric characters.-  | InvalidChecksum-    -- ^ The computed checksum did not match the checksum portion of the input.-  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---- | Generate a random 'Identity'.----randomIdentity :: MonadRandom m => m Identity-randomIdentity = Identity <$> randomGender-                          <*> randomLocation-                          <*> randomSerial-
− library/ROC/ID/Digit.hs
@@ -1,23 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}--module ROC.ID.Digit-  ( Digit (..)-  , parseDigit-  ) where--import GHC.Generics-    ( Generic )--import ROC.ID.Utilities---- | Represents a single decimal digit in the range 0 to 9.----data Digit-  = D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | D9-  deriving (Bounded, Enum, Eq, Generic, Ord)--instance Show Digit where show = show . fromEnum--parseDigit :: Char -> Maybe Digit-parseDigit c = maybeRead [c] >>= maybeToEnum-
− library/ROC/ID/Gender.hs
@@ -1,47 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}--module ROC.ID.Gender-  ( Gender (..)-  , printGender-  , randomGender-  ) where--import Control.Monad.Random.Class-    ( MonadRandom (..) )-import Data.Text-    ( Text )-import GHC.Generics-    ( Generic )--import ROC.ID.Language-import ROC.ID.Utilities---- | A person's gender, encodable within an ROC identification number.----data Gender = Male | Female-  deriving (Bounded, Enum, Eq, Generic, Ord, Show)---- | Pretty-print the specified 'Gender'.----printGender :: Language -> Gender -> Text-printGender = \case-  English -> printGenderEnglish-  Chinese -> printGenderChinese--printGenderEnglish :: Gender -> Text-printGenderEnglish = \case-  Male   -> "Male"-  Female -> "Female"--printGenderChinese :: Gender -> Text-printGenderChinese = \case-  Male   -> "男性"-  Female -> "女性"---- | Generate a random 'Gender'.----randomGender :: MonadRandom m => m Gender-randomGender = randomBoundedEnum-
− library/ROC/ID/Language.hs
@@ -1,6 +0,0 @@-module ROC.ID.Language where---- | A language into which values can be localized when pretty printing.----data Language = English | Chinese-
− library/ROC/ID/Location.hs
@@ -1,135 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}--module ROC.ID.Location-  ( Location (..)-  , parseLocation-  , printLocation-  , randomLocation-  ) where--import Control.Monad.Random.Class-    ( MonadRandom (..) )-import Data.Text-    ( Text )-import GHC.Generics-    ( Generic )-import ROC.ID.Language-    ( Language (..) )-import ROC.ID.Utilities---- | A location, encodable within an ROC identification number.------ To generate the name of a 'Location', use the 'printLocation' function.------ To parse a 'Location', use the 'parseLocation' function.------ To generate a random 'Location', use the 'randomLocation' function.----data Location-  = A -- ^ 臺北市 Taipei City-  | B -- ^ 臺中市 Taichung City-  | C -- ^ 基隆市 Keelung City-  | D -- ^ 臺南市 Tainan City-  | E -- ^ 高雄市 Kaohsiung City-  | F -- ^ 新北市 New Taipei City-  | G -- ^ 宜蘭縣 Yilan County-  | H -- ^ 桃園市 Taoyuan City-  | I -- ^ 嘉義市 Chiayi City-  | J -- ^ 新竹縣 Hsinchu County-  | K -- ^ 苗栗縣 Miaoli County-  | L -- ^ 臺中縣 Taichung County-  | M -- ^ 南投縣 Nantou County-  | N -- ^ 彰化縣 Changhua County-  | O -- ^ 新竹市 Hsinchu City-  | P -- ^ 雲林縣 Yunlin County-  | Q -- ^ 嘉義縣 Chiayi County-  | R -- ^ 臺南縣 Pingtung County-  | S -- ^ 高雄縣 Kaohsiung County-  | T -- ^ 屏東縣 Pingtung County-  | U -- ^ 花蓮縣 Hualien County-  | V -- ^ 臺東縣 Taitung County-  | W -- ^ 金門縣 Kinmen County-  | X -- ^ 澎湖縣 Penghu County-  | Y -- ^ 陽明山 Yangmingshan-  | Z -- ^ 連江縣 Lienchiang County-  deriving (Bounded, Enum, Eq, Generic, Ord, Read, Show)---- | Parse the specified uppercase alphabetic character as a 'Location'.------ Returns 'Nothing' if the specified character is not an uppercase alphabetic--- character.----parseLocation :: Char -> Maybe Location-parseLocation c = maybeRead [c]---- | Pretty-print the specified 'Location'.-printLocation :: Language -> Location -> Text-printLocation = \case-  English -> printLocationEnglish-  Chinese -> printLocationChinese--printLocationChinese :: Location -> Text-printLocationChinese = \case-  A -> "臺北市"-  B -> "臺中市"-  C -> "基隆市"-  D -> "臺南市"-  E -> "高雄市"-  F -> "新北市"-  G -> "宜蘭縣"-  H -> "桃園市"-  I -> "嘉義市"-  J -> "新竹縣"-  K -> "苗栗縣"-  L -> "臺中縣"-  M -> "南投縣"-  N -> "彰化縣"-  O -> "新竹市"-  P -> "雲林縣"-  Q -> "嘉義縣"-  R -> "臺南縣"-  S -> "高雄縣"-  T -> "屏東縣"-  U -> "花蓮縣"-  V -> "臺東縣"-  W -> "金門縣"-  X -> "澎湖縣"-  Y -> "陽明山"-  Z -> "連江縣"--printLocationEnglish :: Location -> Text-printLocationEnglish = \case-  A -> "Taipei City"-  B -> "Taichung City"-  C -> "Keelung City"-  D -> "Tainan City"-  E -> "Kaohsiung City"-  F -> "New Taipei City"-  G -> "Yilan County"-  H -> "Taoyuan City"-  I -> "Chiayi City"-  J -> "Hsinchu County"-  K -> "Miaoli County"-  L -> "Taichung County"-  M -> "Nantou County"-  N -> "Changhua County"-  O -> "Hsinchu City"-  P -> "Yunlin County"-  Q -> "Chiayi County"-  R -> "Pingtung County"-  S -> "Kaohsiung County"-  T -> "Pingtung County"-  U -> "Hualien County"-  V -> "Taitung County"-  W -> "Kinmen County"-  X -> "Penghu County"-  Y -> "Yangmingshan"-  Z -> "Lienchiang County"---- | Generate a random 'Location'.----randomLocation :: MonadRandom m => m Location-randomLocation = randomBoundedEnum-
− library/ROC/ID/Serial.hs
@@ -1,34 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveGeneric #-}--module ROC.ID.Serial-  ( Serial (..)-  , randomSerial-  ) where--import Control.Monad.Random.Class-    ( MonadRandom (..) )-import Data.Vector.Sized-    ( Vector )-import GHC.Generics-    ( Generic )--import ROC.ID.Digit-import ROC.ID.Utilities--import qualified Data.Vector.Sized as V---- | A 7-digit serial number, as found within an ROC identification number.------ A serial number is unique for a gender and location.------ To generate a random 'Serial' number, use the 'randomSerial' function.----newtype Serial = Serial (Vector 7 Digit)-  deriving (Eq, Generic, Ord, Show)---- | Generate a random 'Serial' number.----randomSerial :: MonadRandom m => m Serial-randomSerial = Serial <$> V.replicateM randomBoundedEnum-
− library/ROC/ID/Utilities.hs
@@ -1,24 +0,0 @@-{-# LANGUAGE ScopedTypeVariables #-}--module ROC.ID.Utilities where--import Control.Monad.Random.Class-    ( MonadRandom (..) )-import Data.Maybe-    ( listToMaybe )--guard :: x -> Maybe y -> Either x y-guard x = maybe (Left x) Right--maybeRead :: Read a => String -> Maybe a-maybeRead = fmap fst . listToMaybe . reads--maybeToEnum :: forall a . Bounded a => Enum a => Int -> Maybe a-maybeToEnum i-  | i < fromEnum (minBound :: a) = Nothing-  | i > fromEnum (maxBound :: a) = Nothing-  | otherwise                    = pure $ toEnum i--randomBoundedEnum :: forall a m . MonadRandom m => Bounded a => Enum a => m a-randomBoundedEnum =-  toEnum <$> getRandomR (fromEnum (minBound :: a), fromEnum (maxBound :: a))
roc-id.cabal view
@@ -1,7 +1,7 @@ cabal-version:  3.0 name:           roc-id-version:        0.2.0.6-synopsis:       Implementation of the ROC (Taiwan) National ID standard.+version:        0.3.0.0+synopsis:       Implementation of the ROC (Taiwan) Uniform ID Number format. category:       Identification homepage:       https://github.com/jonathanknowles/roc-id#readme bug-reports:    https://github.com/jonathanknowles/roc-id/issues@@ -10,41 +10,57 @@ copyright:      Jonathan Knowles license:        BSD-3-Clause license-file:   LICENSE-build-type:     Simple+build-type:     Custom  description:-  This package provides an implementation of the ROC (Taiwan) National-  Identification Number (中華民國身分證號碼) standard. -  In particular, it provides functions for parsing and validating-  identification numbers from textual input.+  This package provides an implementation of the ROC (Taiwan) Uniform+  Identification Number (中華民國統一證號) format. +  This format is used by both National Identification Cards (國民身分證) and+  Alien Resident Certificates (居留證). Each identification number consists of+  a single uppercase letter followed by nine decimal digits, with the final+  digit serving as a checksum, calculated according to a standard algorithm.++  Example: `A123456789`++  This package offers functions for validating, decoding, and encoding these+  numbers.+   See the "ROC.ID" module to get started. -  For more details of the standard on which this package is based, see:+  For more details of the Uniform Identification Number format, see:    * https://zh.wikipedia.org/wiki/中華民國國民身分證-  * https://en.wikipedia.org/wiki/National_Identification_Card_(Republic_of_China)+  * https://en.wikipedia.org/wiki/National_identification_card_(Taiwan)+  * https://en.wikipedia.org/wiki/Resident_certificate  extra-doc-files:   CHANGELOG.md   README.md  common dependency-base-    build-depends:base                      >= 4.7          && < 4.22+    build-depends:base                      >= 4.17.2.1     && < 4.22+common dependency-doctest+    build-depends:doctest                   >= 0.24.2       && < 0.25 common dependency-hspec     build-depends:hspec                     >= 2.5.5        && < 2.12 common dependency-MonadRandom     build-depends:MonadRandom               >= 0.5.1.1      && < 0.7-common dependency-Only-    build-depends:Only                      >= 0.1          && < 0.2+common dependency-nonempty-containers+    build-depends:nonempty-containers       >= 0.3.5.0      && < 0.4 common dependency-QuickCheck     build-depends:QuickCheck                >= 2.13.2       && < 2.18+common dependency-quickcheck-classes+    build-depends:quickcheck-classes        >= 0.6.5.0      && < 0.7 common dependency-text     build-depends:text                      >= 1.2.3.1      && < 2.2-common dependency-vector-sized-    build-depends:vector-sized              >= 1.0.4.0      && < 1.7 +custom-setup+  setup-depends:+    , base                                  >= 4.17.2.1     && < 4.22+    , cabal-doctest                         >= 1.0.12       && < 1.1+ source-repository head   type: git   location: https://github.com/jonathanknowles/roc-id@@ -53,20 +69,24 @@   import:     , dependency-base     , dependency-MonadRandom-    , dependency-Only+    , dependency-nonempty-containers     , dependency-text-    , dependency-vector-sized   exposed-modules:       ROC.ID+      ROC.ID.CharIndex+      ROC.ID.CharSet       ROC.ID.Digit+      ROC.ID.Digit1289       ROC.ID.Gender       ROC.ID.Language+      ROC.ID.Letter       ROC.ID.Location-      ROC.ID.Serial+      ROC.ID.Nationality   other-modules:+      ROC.ID.Unchecked       ROC.ID.Utilities   hs-source-dirs:-      library+      components/roc-id   ghc-options: -Wall   default-language: Haskell2010 @@ -75,15 +95,31 @@     , dependency-base     , dependency-hspec     , dependency-MonadRandom-    , dependency-Only+    , dependency-nonempty-containers     , dependency-QuickCheck+    , dependency-quickcheck-classes     , dependency-text-    , dependency-vector-sized   type: exitcode-stdio-1.0   main-is: Spec.hs+  other-modules:+      Test.QuickCheck.Classes.Hspec   hs-source-dirs:-      test+      components/roc-id-test   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N   build-depends:     , roc-id+  default-language: Haskell2010++test-suite roc-id-test-docs+  import:+    , dependency-base+    , dependency-doctest+  build-depends:+    , roc-id+  x-doctest-options: --verbose+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+    components/roc-id-test-docs+  ghc-options: -threaded   default-language: Haskell2010
− test/Spec.hs
@@ -1,88 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}--{-# LANGUAGE ScopedTypeVariables #-}--module Main where--import ROC.ID-import ROC.ID.Digit-import ROC.ID.Gender-import ROC.ID.Location-import ROC.ID.Serial--import Data.Char-    ( intToDigit )-import Test.Hspec-    ( describe, hspec, it, shouldBe )-import Test.QuickCheck-    ( Arbitrary (..)-    , NonEmptyList (..)-    , applyArbitrary3-    , arbitraryBoundedEnum-    , genericShrink-    , property-    )--import qualified Data.Text as T-import qualified Data.Vector.Sized as V--instance Arbitrary Digit where-  arbitrary = arbitraryBoundedEnum-  shrink = genericShrink--instance Arbitrary Gender where-  arbitrary = arbitraryBoundedEnum-  shrink = genericShrink--instance Arbitrary Identity where-  arbitrary = applyArbitrary3 Identity-  shrink = genericShrink--instance Arbitrary Location where-  arbitrary = arbitraryBoundedEnum-  shrink = genericShrink--instance Arbitrary Serial where-  arbitrary = Serial . V.fromTuple <$> arbitrary-  shrink (Serial v) = Serial <$> traverse shrink v--main :: IO ()-main = hspec $--  describe "parseIdentity" $ do--    it "successfully parses valid identification numbers" $-      property $ \(i :: Identity) ->-        parseIdentity (T.pack $ show i) `shouldBe` Right i--    it "does not parse identification numbers that are too short" $-      property $ \(i :: Identity) n -> do-        let newLength = n `mod` 10-        let invalidIdentity = T.pack $ take newLength $ show i-        parseIdentity invalidIdentity `shouldBe` Left InvalidLength--    it "does not parse identification numbers that are too long" $-      property $ \(i :: Identity) (NonEmpty s) -> do-        let invalidIdentity = T.pack $ show i <> s-        parseIdentity invalidIdentity `shouldBe` Left InvalidLength--    it "does not parse identification numbers with invalid gender codes" $-      property $ \(i :: Identity) (c :: Int) -> do-        let invalidGenderCode = intToDigit $ ((c `mod` 8) + 3) `mod` 10-        let invalidIdentity = T.pack $-              take 1 (show i) <> [invalidGenderCode] <> drop 2 (show i)-        parseIdentity invalidIdentity `shouldBe` Left InvalidGender--    it "does not parse identification numbers with invalid location codes" $-      property $ \(i :: Identity) (c :: Int) -> do-        let invalidLocationCode = intToDigit $ c `mod` 10-        let invalidIdentity = T.pack $ invalidLocationCode : drop 1 (show i)-        parseIdentity invalidIdentity `shouldBe` Left InvalidLocation--    it "does not parse identification numbers with invalid checksums" $-      property $ \(i :: Identity) (c :: Int) -> do-        let invalidChecksum = intToDigit $-              ((c `mod` 9) + fromEnum (identityChecksum i) + 1) `mod` 10-        let invalidIdentity = T.pack $ take 9 (show i) <> [invalidChecksum]-        parseIdentity invalidIdentity `shouldBe` Left InvalidChecksum-