packages feed

roc-id 0.3.0.0 → 0.4.0.0

raw patch · 14 files changed

+294/−93 lines, 14 filesdep +finitarydep +finite-typelitsdep +microlensPVP ok

version bump matches the API change (PVP)

Dependencies added: finitary, finite-typelits, microlens

API changes (from Hackage documentation)

- ROC.ID: TextTooLong :: FromTextError
- ROC.ID: TextTooShort :: FromTextError
- ROC.ID: checksumDigit :: ID -> Digit
+ ROC.ID: InvalidLength :: FromTextError
+ ROC.ID: checksum :: ID -> Digit
+ ROC.ID: instance Data.Finitary.Finitary ROC.ID.ID
+ ROC.ID: instance GHC.Generics.Generic ROC.ID.ID
+ ROC.ID.Digit: instance Data.Finitary.Finitary ROC.ID.Digit.Digit
+ ROC.ID.Digit1289: instance Data.Finitary.Finitary ROC.ID.Digit1289.Digit1289
+ ROC.ID.Digit1289: instance GHC.Read.Read ROC.ID.Digit1289.Digit1289
+ ROC.ID.Gender: instance Data.Finitary.Finitary ROC.ID.Gender.Gender
+ ROC.ID.Letter: instance Data.Finitary.Finitary ROC.ID.Letter.Letter
+ ROC.ID.Location: instance Data.Finitary.Finitary ROC.ID.Location.Location
+ ROC.ID.Nationality: instance Data.Finitary.Finitary ROC.ID.Nationality.Nationality
+ ROC.ID.Nationality: instance GHC.Generics.Generic ROC.ID.Nationality.Nationality

Files

CHANGELOG.md view
@@ -1,3 +1,15 @@+# 0.4.0.0++- Revised the `Show` instance of `ID` to produce a valid Haskell expression+  when applied to nested values of `ID`.+- Revised the `ID.fromSymbol` function so that users are presented with a+  helpful error if they forget to apply the function to a concrete type.+- Simplified the error handling for `ID.fromText` by merging the `TextTooShort`+  and `TextTooLong` errors into a unified `InvalidLength` error.+- Renamed `ID.checksumDigit` to `ID.checksum`.+- Made `ID` an instance of the `Finitary` type class.+- Made `ID` an instance of the `Generic` type class.+ # 0.3.0.0  - Added support for ARC (Alien Residence Certificate) numbers.
LICENSE view
@@ -1,4 +1,4 @@-Copyright Jonathan Knowles (c) 2018–2025+Copyright Jonathan Knowles (c) 2018–2026  All rights reserved. 
README.md view
@@ -1,5 +1,11 @@ # `roc-id`-<a href="https://jonathanknowles.github.io/roc-id/"><img src="https://img.shields.io/badge/API-Documentation-227755" /></a>++[![Latest Release](+  https://img.shields.io/hackage/v/roc-id?label=Latest%20Release&color=227755+)](https://hackage.haskell.org/package/roc-id)+[![Development Branch](+  https://img.shields.io/badge/Development%20Branch-API%20Documentation-225577+)](https://jonathanknowles.github.io/roc-id/)  This package provides a [Haskell](https://www.haskell.org/) implementation of the ROC (Taiwan) Uniform Identification Number (中華民國統一證號) format.
components/roc-id-test/Spec.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-}@@ -17,6 +18,10 @@   ( NonEmpty ((:|)) ) import Data.Text   ( Text )+import Lens.Micro+  ( Lens', lens, set )+import Lens.Micro.Extras+  ( view ) import ROC.ID   ( ID (..) ) import ROC.ID.CharIndex@@ -36,10 +41,11 @@ import ROC.ID.Nationality   ( Nationality (..) ) import Test.Hspec-  ( describe, hspec, it, shouldBe, shouldSatisfy )+  ( Spec, describe, hspec, it, shouldBe, shouldSatisfy ) import Test.QuickCheck   ( Arbitrary (..)   , NonEmptyList (..)+  , Property   , arbitraryBoundedEnum   , choose   , elements@@ -47,12 +53,14 @@   , property   , shrinkBoundedEnum   , shrinkMap+  , (===)   ) import Test.QuickCheck.Classes   ( boundedEnumLaws, eqLaws, numLaws, ordLaws, showLaws, showReadLaws ) import Test.QuickCheck.Classes.Hspec   ( testLawsMany ) +import qualified Data.Finitary as Finitary import qualified Data.Set.NonEmpty as NESet import qualified Data.Text as T import qualified ROC.ID as ID@@ -130,6 +138,48 @@         , showReadLaws         ] +  describe "Finitary instances" $ do++    describe "ID" $ do++      let start    = Finitary.start    @ID+          end      = Finitary.end      @ID+          previous = Finitary.previous @ID+          next     = Finitary.next     @ID++      it "previous start" $+          previous start+            `shouldBe` Nothing++      it "start" $+          start+            `shouldBe` ID.fromSymbol @"A100000001"++      it "next start" $+          next start+            `shouldBe` Just (ID.fromSymbol @"A100000010")++      it "previous end" $+          previous end+            `shouldBe` Just (ID.fromSymbol @"Z999999987")++      it "end" $+          end+            `shouldBe` ID.fromSymbol @"Z999999996"++      it "next end" $+          next end+            `shouldBe` Nothing++  describe "ID attribute getters and setters" $ do++    describe "Gender" $+      checkLensLaws gender+    describe "Location" $+      checkLensLaws location+    describe "Nationality" $+      checkLensLaws nationality+   describe "ID.fromText" $ do      it "successfully parses known-valid identification numbers" $@@ -144,12 +194,12 @@       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+        ID.fromText invalidID `shouldBe` Left ID.InvalidLength      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+        ID.fromText invalidID `shouldBe` Left ID.InvalidLength      it "does not parse identification numbers with invalid location codes" $       property $ \(i :: ID) (c :: Int) -> do@@ -170,7 +220,7 @@     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+              ((c `mod` 9) + fromEnum (ID.checksum i) + 1) `mod` 10         let invalidID =               T.take 9 (ID.toText i) <> T.pack [invalidChecksumDigit]         ID.fromText invalidID `shouldBe` Left ID.InvalidChecksum@@ -193,7 +243,48 @@               replaceCharAt invalidCharIndex 'x' (ID.toText i)               <>               T.pack trailingExcess-        ID.fromText textInvalid `shouldBe` Left ID.TextTooLong+        ID.fromText textInvalid `shouldBe` Left ID.InvalidLength++checkLensLaws+  :: forall i v. (Arbitrary i, Arbitrary v, Eq i, Eq v, Show i, Show v)+  => Lens' i v+  -> Spec+checkLensLaws l =+  do+    it "Finality"+      $ property lensLawFinality+    it "Idempotence"+      $ property lensLawIdempotence+    it "Invertibility"+      $ property lensLawInvertibility+    it "Reversibility"+      $ property lensLawReversibility+    it "Stability"+      $ property lensLawStability+  where+    lensLawFinality :: i -> v -> v -> Property+    lensLawFinality i v1 v2 = set l v2 (set l v1 i) === set l v2 i++    lensLawIdempotence :: i -> v -> Property+    lensLawIdempotence i v = set l v (set l v i) === set l v i++    lensLawInvertibility :: i -> v -> Property+    lensLawInvertibility i v = view l (set l v i) === v++    lensLawReversibility :: i -> v -> Property+    lensLawReversibility i v = set l (view l i) (set l v i) === i++    lensLawStability :: i -> Property+    lensLawStability i = set l (view l i) i === i++gender :: Lens' ID Gender+gender = lens ID.getGender (flip ID.setGender)++location :: Lens' ID Location+location = lens ID.getLocation (flip ID.setLocation)++nationality :: Lens' ID Nationality+nationality = lens ID.getNationality (flip ID.setNationality)  -- | Replaces a character at a specific position. --
components/roc-id/ROC/ID.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -20,7 +23,7 @@   , toText    -- * Validity-  , checksumDigit+  , checksum    -- * Generation   , generate@@ -41,10 +44,14 @@   ( MonadRandom ) import Data.Bifunctor   ( Bifunctor (first) )+import Data.Finitary+  ( Finitary ) import Data.Proxy   ( Proxy (Proxy) ) import Data.Text   ( Text )+import GHC.Generics+  ( Generic ) import GHC.TypeLits   ( Symbol, symbolVal ) import ROC.ID.CharIndex@@ -64,24 +71,20 @@ import ROC.ID.Nationality   ( Nationality (..) ) import ROC.ID.Unchecked-  ( UncheckedID (UncheckedID)-  , ValidID-  )+  ( UncheckedID (UncheckedID), ValidID ) import ROC.ID.Utilities-  ( guard )+  ( guard, randomFinitary ) import Text.Read-  ( Lexeme (Ident, Symbol, Punc), Read (readPrec), lexP, parens )+  ( Lexeme (Ident, Symbol, Punc), Read (readPrec), lexP, parens, prec )  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 -XOverloadedStrings -- >>> :set -XTypeApplications -- >>> import ROC.ID -- >>> import qualified ROC.ID as ID@@ -99,7 +102,7 @@ -- 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'.+-- To compute the checksum digit, use 'checksum'. -- data ID = ID   { c0 :: !Letter@@ -112,10 +115,11 @@   , c7 :: !Digit   , c8 :: !Digit   }-  deriving (Eq, Ord)+  deriving stock (Eq, Ord, Generic)+  deriving anyclass Finitary  instance Read ID where-  readPrec = parens $ do+  readPrec = parens $ prec 10 $ do     Ident  "ID"         <- lexP     Symbol "."          <- lexP     Ident  "fromSymbol" <- lexP@@ -123,8 +127,9 @@     unsafeFromText <$> readPrec  instance Show ID where-  showsPrec _ s =-    showString "ID.fromSymbol @" . shows (toText s)+  showsPrec d s =+    showParen (d > 10) $+      showString "ID.fromSymbol @" . shows (toText s)  -------------------------------------------------------------------------------- -- Construction@@ -187,14 +192,29 @@ --       Character at this position must be a digit in the range [0 .. 9]. -- ... --+-- === Missing or empty symbols+--+-- >>> ID.fromSymbol+-- ...+-- ... Expected a type-level symbol of the form "A123456789".+-- ...+--+-- >>> ID.fromSymbol @""+-- ...+-- ... Expected a type-level symbol of the form "A123456789".+-- ...+-- 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@__.+-- __@A123456789@__: --+-- >>> ID.fromText "A123456789"+-- Right (ID.fromSymbol @"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.@@ -212,12 +232,10 @@   where     fromUncheckedError :: U.FromTextError -> FromTextError     fromUncheckedError = \case-      U.TextTooShort ->-        TextTooShort-      U.TextTooLong ->-        TextTooLong       U.InvalidChar i r ->         InvalidChar i r+      U.InvalidLength ->+        InvalidLength  unsafeFromText :: Text -> ID unsafeFromText t =@@ -229,23 +247,20 @@ -- data FromTextError -  = TextTooShort-  -- ^ Indicates that the input text is too short.--  | TextTooLong-  -- ^ Indicates that the input text is too long.--  | InvalidChar CharIndex CharSet+  = InvalidChar CharIndex CharSet   -- ^ Indicates that the input text contains a character that is not allowed.   ---  --   - `CharIndex` specifies the position of the offending character.+  --   - `CharIndex` specifies the position of the invalid 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)+  | InvalidLength+  -- ^ Indicates that the input text has an invalid length. +  deriving stock (Eq, Ord, Show)+ -------------------------------------------------------------------------------- -- Conversion --------------------------------------------------------------------------------@@ -269,8 +284,8 @@  -- | Computes the terminal checksum digit of an 'ID'. ---checksumDigit :: ID -> Digit-checksumDigit (ID u0 u1 u2 u3 u4 u5 u6 u7 u8) =+checksum :: ID -> Digit+checksum (ID u0 u1 u2 u3 u4 u5 u6 u7 u8) =   negate $ U.checksum (UncheckedID u0 u1 u2 u3 u4 u5 u6 u7 u8 0)  --------------------------------------------------------------------------------@@ -280,17 +295,7 @@ -- | 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+generate = randomFinitary  -------------------------------------------------------------------------------- -- Inspection@@ -358,4 +363,4 @@  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)+  UncheckedID u0 u1 u2 u3 u4 u5 u6 u7 u8 (checksum i)
components/roc-id/ROC/ID/Digit.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}@@ -16,12 +18,14 @@  import Control.Monad.Random   ( MonadRandom )+import Data.Finitary+  ( Finitary ) import GHC.Generics   ( Generic ) import GHC.TypeNats   ( Nat ) import ROC.ID.Utilities-  ( maybeBoundedEnum, randomBoundedEnum )+  ( maybeFinitary, randomFinitary ) import Text.Read   ( Read (readPrec), readMaybe ) @@ -34,7 +38,8 @@ -- data Digit   = D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | D9-  deriving (Bounded, Enum, Eq, Generic, Ord)+  deriving stock (Bounded, Enum, Eq, Generic, Ord)+  deriving anyclass Finitary  -- | Arithmetic modulo 10. --@@ -56,7 +61,7 @@ -- The 'Char' must be a decimal digit in the range @0@ to @9@. -- fromChar :: Char -> Maybe Digit-fromChar c = readMaybe [c] >>= maybeBoundedEnum+fromChar c = readMaybe [c] >>= maybeFinitary  -- | Converts a 'Digit' to a decimal digit character. --@@ -73,7 +78,7 @@ -- | Generates a random digit. -- generate :: MonadRandom m => m Digit-generate = randomBoundedEnum+generate = randomFinitary  type family FromChar (c :: Char) :: Maybe Digit where   FromChar '0' = Just D0
components/roc-id/ROC/ID/Digit1289.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE TypeFamilies #-}@@ -18,6 +20,8 @@  import Control.Monad.Random   ( MonadRandom )+import Data.Finitary+  ( Finitary ) import GHC.Generics   ( Generic ) import GHC.TypeNats@@ -25,16 +29,14 @@ import ROC.ID.Digit   ( Digit (..) ) import ROC.ID.Utilities-  ( randomBoundedEnum )+  ( randomFinitary )  -- | 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+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Read, Show)+  deriving anyclass Finitary  -- | Attempts to parse a 'Digit1289' from a character. --@@ -69,7 +71,7 @@ -- | Generates a random 'Digit1289'. -- generate :: MonadRandom m => m Digit1289-generate = randomBoundedEnum+generate = randomFinitary  type family FromChar (c :: Char) :: Maybe Digit1289 where   FromChar '1' = Just D1289_1
components/roc-id/ROC/ID/Gender.hs view
@@ -1,4 +1,6 @@+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} @@ -10,6 +12,8 @@  import Control.Monad.Random.Class   ( MonadRandom (..) )+import Data.Finitary+  ( Finitary ) import Data.Text   ( Text ) import GHC.Generics@@ -17,12 +21,13 @@ import ROC.ID.Language   ( Language (..) ) import ROC.ID.Utilities-  ( randomBoundedEnum )+  ( randomFinitary )  -- | A person's gender, encodable within an ROC identification number. -- data Gender = Male | Female-  deriving (Bounded, Enum, Eq, Generic, Ord, Read, Show)+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Read, Show)+  deriving anyclass Finitary  -- | Prints the specified 'Gender'. --@@ -44,4 +49,4 @@ -- | Generates a random 'Gender'. -- generate :: MonadRandom m => m Gender-generate = randomBoundedEnum+generate = randomFinitary
components/roc-id/ROC/ID/Letter.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} @@ -13,10 +15,12 @@  import Control.Monad.Random.Class   ( MonadRandom (..) )+import Data.Finitary+  ( Finitary ) import GHC.Generics   ( Generic ) import ROC.ID.Utilities-  ( randomBoundedEnum )+  ( randomFinitary ) import Text.Read   ( readMaybe ) @@ -25,7 +29,8 @@ 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)+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Read, Show)+  deriving anyclass Finitary  -- | Attempts to parse a 'Letter' from a character. --@@ -45,7 +50,7 @@ -- | Generates a random 'Letter'. -- generate :: MonadRandom m => m Letter-generate = randomBoundedEnum+generate = randomFinitary  type family FromChar (c :: Char) :: Maybe Letter where   FromChar 'A' = Just A; FromChar 'N' = Just N
components/roc-id/ROC/ID/Location.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -14,6 +15,8 @@  import Control.Monad.Random.Class   ( MonadRandom (..) )+import Data.Finitary+  ( Finitary ) import Data.Text   ( Text ) import GHC.Generics@@ -22,11 +25,11 @@   ( Language (..) ) import ROC.ID.Letter   ( Letter (..) )+import ROC.ID.Utilities+  ( randomFinitary ) 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@@ -102,6 +105,7 @@ newtype Location = Location Letter   deriving stock (Eq, Generic, Ord)   deriving newtype (Bounded, Enum)+  deriving anyclass Finitary  instance Read Location where   readPrec = parens $ do@@ -191,4 +195,4 @@ -- | Generates a random 'Location'. -- generate :: MonadRandom m => m Location-generate = Location <$> Letter.generate+generate = randomFinitary
components/roc-id/ROC/ID/Nationality.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+ module ROC.ID.Nationality   ( Nationality (..)   , generate@@ -6,15 +10,20 @@  import Control.Monad.Random.Class   ( MonadRandom (..) )+import Data.Finitary+  ( Finitary )+import GHC.Generics+  ( Generic ) import ROC.ID.Utilities-  ( randomBoundedEnum )+  ( randomFinitary )  -- | Specifies a person's nationality. -- data Nationality = National | NonNational-  deriving (Bounded, Enum, Eq, Ord, Read, Show)+  deriving stock (Bounded, Enum, Eq, Ord, Generic, Read, Show)+  deriving anyclass Finitary  -- | Generates a random 'Nationality'. -- generate :: MonadRandom m => m Nationality-generate = randomBoundedEnum+generate = randomFinitary
components/roc-id/ROC/ID/Unchecked.hs view
@@ -31,6 +31,8 @@   ( NonEmpty ((:|)) ) import Data.Text   ( Text )+import Data.Type.Bool+  ( If ) import Data.Type.Equality   ( type (==) ) import GHC.TypeError@@ -38,7 +40,7 @@ import GHC.TypeLits   ( AppendSymbol, KnownSymbol, Symbol ) import GHC.TypeNats-  ( Mod, Nat, type (+) )+  ( CmpNat, Mod, Nat, type (+) ) import ROC.ID.CharIndex   ( CharIndex (..) ) import ROC.ID.CharSet@@ -50,7 +52,7 @@ import ROC.ID.Letter   ( Letter (..) ) import ROC.ID.Utilities-  ( FromJust, Fst, ReplicateChar, Snd, SymbolToCharList, guard )+  ( FromJust, Fst, ReplicateChar, Snd, SymbolLength, SymbolToCharList, guard )  import qualified Data.Set.NonEmpty as NESet import qualified Data.Text as T@@ -88,16 +90,15 @@   )  data FromTextError-  = TextTooShort-  | TextTooLong-  | InvalidChar CharIndex CharSet+  = InvalidChar CharIndex CharSet+  | InvalidLength   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+    when (T.length text0 > 10) $ Left InvalidLength     (text1,  c0                             ) <- parseLetter    text0     (text2,  c1                             ) <- parseDigit1289 text1     (_____, (c2, c3, c4, c5, c6, c7, c8, c9)) <- parseDigits    text2@@ -105,13 +106,13 @@   where     parseLetter :: Parser Letter     parseLetter text = do-      (char, remainder) <- guard TextTooShort $ T.uncons text+      (char, remainder) <- guard InvalidLength $ 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+      (char, remainder) <- guard InvalidLength $ T.uncons text       digit1289 <- guard         (InvalidChar 1 (CharSet $ NESet.fromList $ '1' :| ['2', '8', '9']))         (Digit1289.fromChar char)@@ -121,7 +122,7 @@     parseDigits text = do         let (chars, remainder) = T.splitAt 8 text         digitList <- traverse parseIndexedDigit (zip [2 ..] $ T.unpack chars)-        digitTuple <- guard TextTooShort (listToTuple8 digitList)+        digitTuple <- guard InvalidLength (listToTuple8 digitList)         pure (remainder, digitTuple)       where         parseIndexedDigit (i, c) =@@ -162,7 +163,7 @@ data ChecksumValidity   = ChecksumValid   | ChecksumInvalid-  deriving (Eq, Show)+  deriving stock (Eq, Show)  checksumValidity :: UncheckedID -> ChecksumValidity checksumValidity u =@@ -297,5 +298,18 @@  type ValidID s =   ( KnownSymbol s-  , ChecksumValid (SymbolToId s)+  , ChecksumValid (SymbolToId (ConcreteSymbol s))   ) :: Constraint++-- | Ensures the existence of a concrete symbol, or else raises a type error.+--+type family ConcreteSymbol (s :: Symbol) :: Symbol where+  -- If we can compute the symbol's length, then we have a concrete symbol.+  ConcreteSymbol s =+    If+      (CmpNat (SymbolLength s) 0 == GT)+      s+      (TypeError (TypeError.Text ConcreteSymbolError))++type ConcreteSymbolError =+  "Expected a type-level symbol of the form \"A123456789\"."
components/roc-id/ROC/ID/Utilities.hs view
@@ -1,6 +1,8 @@+{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}@@ -9,24 +11,45 @@  import Control.Monad.Random.Class   ( MonadRandom (..) )+import Data.Finitary+  ( Finitary (fromFinite), Cardinality )+import Data.Finite+  ( packFinite )+import Data.Maybe+  ( fromMaybe )+import Data.Proxy+  ( Proxy (Proxy) ) import GHC.TypeLits-  ( Symbol, UnconsSymbol, ConsSymbol )+  ( Symbol, UnconsSymbol, ConsSymbol, type (<=), type (+) ) import GHC.TypeNats-  ( Nat, type (-) )+  ( Nat, type (-), natVal )+import Numeric.Natural+  ( Natural ) +cardinality :: forall a. Finitary a => Natural+cardinality = natVal $ Proxy @(Cardinality a)+ 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+maybeFinitary :: forall a. Finitary a => Natural -> Maybe a+maybeFinitary = fmap fromFinite . packFinite . fromIntegral @Natural @Integer -randomBoundedEnum :: forall m a. (MonadRandom m, Bounded a, Enum a) => m a-randomBoundedEnum =-  toEnum <$> getRandomR (fromEnum (minBound :: a), fromEnum (maxBound :: a))+randomFinitary+  :: forall m a. (MonadRandom m, Finitary a, 1 <= Cardinality a) => m a+randomFinitary =+    fromMaybe failure . maybeFinitary <$> randomNatural (0, cardinality @a - 1)+  where+    failure = error "randomFinitary: unexpected out-of-bounds value" +randomNatural :: MonadRandom m => (Natural, Natural) -> m Natural+randomNatural (lo, hi) =+  fromIntegral @Integer @Natural <$>+    getRandomR+      ( fromIntegral @Natural @Integer lo+      , fromIntegral @Natural @Integer hi+      )+ type family FromJust (m :: Maybe a) e where   FromJust Nothing e = e   FromJust (Just a) _ = a@@ -36,6 +59,16 @@  type family Snd (t :: (a, a)) :: a where   Snd '(_, y) = y++type family ListLength (as :: [a]) :: Nat where+  ListLength as = ListLengthInner 0 as++type family ListLengthInner (n :: Nat) (as :: [a]) :: Nat where+  ListLengthInner n '[] = n+  ListLengthInner n (a : as) = ListLengthInner (n + 1) as++type family SymbolLength (s :: Symbol) :: Nat where+  SymbolLength s = ListLength (SymbolToCharList s)  type family     SymbolToCharList (s :: Symbol) :: [Char]
roc-id.cabal view
@@ -1,6 +1,6 @@ cabal-version:  3.0 name:           roc-id-version:        0.3.0.0+version:        0.4.0.0 synopsis:       Implementation of the ROC (Taiwan) Uniform ID Number format. category:       Identification homepage:       https://github.com/jonathanknowles/roc-id#readme@@ -43,8 +43,14 @@     build-depends:base                      >= 4.17.2.1     && < 4.22 common dependency-doctest     build-depends:doctest                   >= 0.24.2       && < 0.25+common dependency-finitary+    build-depends:finitary                  >= 2.2.0.0      && < 2.3+common dependency-finite-typelits+    build-depends:finite-typelits           >= 0.2.1.0      && < 0.3 common dependency-hspec     build-depends:hspec                     >= 2.5.5        && < 2.12+common dependency-microlens+    build-depends:microlens                 >= 0.5.0.0      && < 0.6 common dependency-MonadRandom     build-depends:MonadRandom               >= 0.5.1.1      && < 0.7 common dependency-nonempty-containers@@ -68,6 +74,8 @@ library   import:     , dependency-base+    , dependency-finitary+    , dependency-finite-typelits     , dependency-MonadRandom     , dependency-nonempty-containers     , dependency-text@@ -93,8 +101,10 @@ test-suite roc-id-test   import:     , dependency-base+    , dependency-finitary     , dependency-hspec     , dependency-MonadRandom+    , dependency-microlens     , dependency-nonempty-containers     , dependency-QuickCheck     , dependency-quickcheck-classes