ulid 0.2.0.0 → 0.3.0.0
raw patch · 16 files changed
+987/−388 lines, 16 filesdep −crockfordnew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies removed: crockford
API changes (from Hackage documentation)
- Data.ULID.Crockford: decode :: Int -> ReadS Integer
- Data.ULID.Crockford: encode :: Int -> Integer -> String
+ Data.ULID.Base32: decode :: Integral i => Int -> Text -> [(i, Text)]
+ Data.ULID.Base32: decodeChar :: Integral i => Char -> Maybe i
+ Data.ULID.Base32: encode :: Integral i => Int -> i -> Text
+ Data.ULID.Base32: encodeChar :: Integral i => i -> Char
+ Data.ULID.Digits: digits :: Integral n => n -> n -> [n]
+ Data.ULID.Digits: unDigits :: Integral n => n -> [n] -> n
- Data.ULID: ulidFromInteger :: Integer -> ULID
+ Data.ULID: ulidFromInteger :: Integer -> Either Text ULID
Files
- README.md +60/−34
- app/Main.hs +35/−35
- bench/Main.hs +28/−24
- src/Data/Binary/Roll.hs +31/−28
- src/Data/ULID.hs +144/−119
- src/Data/ULID/Base32.hs +155/−0
- src/Data/ULID/Crockford.hs +0/−20
- src/Data/ULID/Digits.hs +54/−0
- src/Data/ULID/Random.hs +78/−58
- src/Data/ULID/TimeStamp.hs +56/−44
- test/Data/ULID/Base32Spec.hs +43/−0
- test/Data/ULID/RandomSpec.hs +35/−0
- test/Data/ULID/TimeStampSpec.hs +46/−0
- test/Data/ULIDSpec.hs +177/−0
- test/Spec.hs +1/−1
- ulid.cabal +44/−25
README.md view
@@ -1,31 +1,51 @@-# Ulid implementation in Haskell+# ULID Implementation in Haskell -Lexicographically sortable, 128-bit identifier with 48-bit timestamp and 80 random bits.-Canonically encoded as a 26 character string, as opposed to the 36 character UUID.+Lexicographically sortable, 128-bit identifier+with 48-bit timestamp and 80 random bits.+Canonically encoded as a 26 character string,+as opposed to the 36 character UUID. -Original implementation and spec: https://github.com/alizain/ulid/+Original implementation and spec: [github.com/alizain/ulid] +[github.com/alizain/ulid]: https://github.com/alizain/ulid/ ++```txt+ 01an4z07by 79ka1307sr9x4mv3++|----------| |----------------|+ Timestamp Randomness+ 48 bits 80 bits+```++ # Universally Unique Lexicographically Sortable Identifier UUID can be suboptimal for many uses-cases because: - It isn't the most character efficient way of encoding 128 bits of randomness-- UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address-- UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures-- UUID v4 provides no other information than randomness which can cause fragmentation in many data structures+- UUID v1/v2 is impractical in many environments,+ as it requires access to a unique, stable MAC address+- UUID v3/v5 requires a unique seed and produces randomly distributed IDs,+ which can cause fragmentation in many data structures+- UUID v4 provides no other information than randomness,+ which can cause fragmentation in many data structures Instead, herein is proposed ULID: - 128-bit compatibility with UUID - 1.21e+24 unique ULIDs per millisecond-- Lexicographically sortable!-- Canonically encoded as a 26 character string, as opposed to the 36 character UUID-- Uses Crockford's base32 for better efficiency and readability (5 bits per character)+- Lexicographically sortable+- Canonically encoded as a 26 character string,+ as opposed to the 36 character UUID+- Uses [Douglas Crockford's base 32] for better efficiency and readability+ (5 bits per character) - Case insensitive - No special characters (URL safe) +[Douglas Crockford's base 32]: https://www.crockford.com/base32.html + ## Usage A simple usage example:@@ -33,61 +53,67 @@ ````haskell module Main where -import Data.ULID+import Data.ULID main :: IO () main = do- -- Derive a ULID using the current time and default random number generator- ulid1 <- getULID- print ulid1+ -- Derive a ULID using the current time and default random number generator+ ulid1 <- getULID+ print ulid1 - -- Derive a ULID using a specified time and default random number generator- ulid2 <- getULIDTime 1469918176.385 -- POSIX Time, specified to the millisecond- print ulid2+ -- Derive a ULID using a specified time and default random number generator+ ulid2 <- getULIDTime 1469918176.385 -- POSIX Time, millisecond precision+ print ulid2 ```` -As per the spec, it is also possible to use a cryptographically-secure random number generator to contribute the randomness. However, the programmer must manage the generator on their own. Example:+As per the spec, it is also possible to use a cryptographically-secure+random number generator to contribute the randomness.+However, the programmer must manage the generator on their own. +Example: ````haskell module Main where -import Data.ULID+import Data.ULID import qualified Crypto.Random as CR import qualified Data.ULID.Random as UR import qualified Data.ULID.TimeStamp as TS main :: IO ()-main = do - -- This default instantiation may not be sufficiently secure, see the docs - -- https://hackage.haskell.org/package/crypto-api-0.13.2/docs/Crypto-Random.html- g <- (CR.newGenIO :: IO CR.SystemRandom)+main = do+ -- This default instantiation may not be sufficiently secure.+ -- See the docs at+ -- hackage.haskell.org/package/crypto-api-0.13.2/docs/Crypto-Random.html+ g <- (CR.newGenIO :: IO CR.SystemRandom) - -- Generate time stamp from current time- t <- TS.getULIDTimeStamp- - let ulid3 = case UR.mkCryptoULIDRandom g of- Left err -> error $ show err- Right (rnd, g2) -> ULID t rnd -- use g2, etc, to continue generating secure ULIDs- print ulid3-````+ -- Generate timestamp from current time+ t <- TS.getULIDTimeStamp + let ulid3 = case UR.mkCryptoULIDRandom g of+ Left err -> error $ show err+ -- use g2, …, to continue generating secure ULIDs+ Right (rnd, g2) -> ULID t rnd + print ulid3+```` + ## Test Suite -```+```sh stack test ``` + ## Performance -```+```sh stack bench ``` -```+```txt Running 1 benchmarks... Benchmark ulid-bench: RUNNING... 217,868 op/s generate
app/Main.hs view
@@ -1,35 +1,35 @@-module Main where - -import Data.ULID - --- These imports only needed for cryptographically secure ULID -import qualified Crypto.Random as CR -import qualified Data.ULID.Random as UR -import qualified Data.ULID.TimeStamp as TS - -main :: IO () -main = do - -- Derive a ULID using the current time and default random number generator - ulid1 <- getULID - print ulid1 - - -- Derive a ULID using a specified time and default random number generator - ulid2 <- getULIDTime 1469918176.385 -- POSIX Time, specified to the millisecond - print ulid2 - - - -- Below only for crypographically secure ULID example - - -- This default instantiation may not be sufficiently secure, see the docs - -- https://hackage.haskell.org/package/crypto-api-0.13.2/docs/Crypto-Random.html - g <- (CR.newGenIO :: IO CR.SystemRandom) - - -- Generate time stamp from current time - t <- TS.getULIDTimeStamp - - let ulid3 = case UR.mkCryptoULIDRandom g of - Left err -> error $ show err - Right (rnd, g2) -> ULID t rnd -- use g2, etc, to continue generating secure ULIDs - print ulid3 - - +module Main where++import Data.ULID++-- These imports only needed for cryptographically secure ULID+import qualified Crypto.Random as CR+import qualified Data.ULID.Random as UR+import qualified Data.ULID.TimeStamp as TS+++main :: IO ()+main = do+ -- Derive a ULID using the current time and default random number generator+ ulid1 <- getULID+ print ulid1++ -- Derive a ULID using a specified time and default random number generator+ ulid2 <- getULIDTime 1469918176.385 -- ^ POSIX Time in milliseconds+ print ulid2++ -- Below only for cryptographically secure ULID example++ -- This default instantiation may not be sufficiently secure, see the docs+ -- https://hackage.haskell.org/package/crypto-api-0.13.3/docs/Crypto-Random.html+ g <- (CR.newGenIO :: IO CR.SystemRandom)++ -- Generate time stamp from current time+ t <- TS.getULIDTimeStamp++ let ulid3 = case UR.mkCryptoULIDRandom g of+ Left err -> error $ show err+ -- Use g2, etc, to continue generating secure ULIDs+ Right (rnd, g2) -> ULID t rnd++ print ulid3
bench/Main.hs view
@@ -1,24 +1,28 @@-module Main where - -import Data.ULID - -import Control.DeepSeq -import Control.Monad (replicateM) -import qualified Data.Text as T -import qualified Data.Text.Format.Numbers as FN -import qualified Data.Time.Clock.POSIX as PX - -formatTN = T.unpack . (FN.prettyI (Just ',')) - -main :: IO () -main = do - -- Run many iterations of getULID - let ops = 100000 - begin <- PX.getPOSIXTime - ulids <- replicateM ops (getULID >>= return.force) - end <- PX.getPOSIXTime - let elapsed = end - begin - let opsPerSec = (fromIntegral ops) / (realToFrac elapsed) :: Double - putStr $ formatTN (round opsPerSec) - putStrLn " op/s generate" - +module Main where++import Data.ULID++import Control.DeepSeq+import Control.Monad (replicateM)+import Data.Text as T+import Data.Text.IO as T+import Data.Text.Format.Numbers as FN+import Data.Time.Clock.POSIX as PX+++formatTN :: Int -> Text+formatTN =+ FN.prettyI $ Just ','+++main :: IO ()+main = do+ -- Run many iterations of getULID+ let ops = 100000+ begin <- PX.getPOSIXTime+ ulids <- replicateM ops (getULID >>= return.force)+ end <- PX.getPOSIXTime+ let elapsed = end - begin+ let opsPerSec = (fromIntegral ops) / (realToFrac elapsed) :: Double+ T.putStr $ formatTN $ round opsPerSec+ T.putStrLn " op/s generate"
src/Data/Binary/Roll.hs view
@@ -1,28 +1,31 @@-module Data.Binary.Roll where - -import Data.Binary -import Data.Bits -import Data.List (foldl', unfoldr) - - --- | unroll and produce an exact number of bytes (left-pad with 0 bytes) -unroll :: Int -> Integer -> [Word8] -unroll bytes val = replicate (bytes - length xs) 0 ++ xs - where xs = unroll' val - --- source: http://hackage.haskell.org/package/binary-0.8.5.1/docs/src/Data-Binary-Class.html#line-311 --- --- Fold and unfold an Integer to and from a list of its bytes --- --- MOST SIGNIFICANT BYTE FIRST please (this is a change from the default Data.Binary impl) - -unroll' :: Integer -> [Word8] -unroll' = reverse . unfoldr step - where - step 0 = Nothing - step i = Just (fromIntegral i, i `shiftR` 8) - -roll :: [Word8] -> Integer -roll = foldl' unstep 0 - where - unstep a b = a `shiftL` 8 .|. fromIntegral b +module Data.Binary.Roll where++import Data.Binary+import Data.Bits+import Data.List (foldl', unfoldr)+++-- | Unroll and produce an exact number of bytes (left-pad with 0 bytes)+unroll :: Int -> Integer -> [Word8]+unroll bytes val = replicate (bytes - length xs) 0 ++ xs+ where xs = unroll' (abs val)+++-- | Source:+-- https://hackage.haskell.org/package/binary-0.8.5.1/docs/src/Data-Binary-Class.html#line-311+--+-- Fold and unfold an Integer to and from a list of its bytes+--+-- MOST SIGNIFICANT BYTE FIRST please+-- (this is a change from the default `Data.Binary` impl)+unroll' :: Integer -> [Word8]+unroll' = reverse . unfoldr step+ where+ step 0 = Nothing+ step i = Just (fromIntegral i, i `shiftR` 8)+++roll :: [Word8] -> Integer+roll = foldl' unstep 0+ where+ unstep a b = a `shiftL` 8 .|. fromIntegral b
src/Data/ULID.hs view
@@ -1,119 +1,144 @@-{- | -Module : Data.ULID -Copyright : (c) 2017 Steve Kollmansberger - -License : BSD-style - -Maintainer : steve@kolls.net -Stability : experimental -Portability : portable - -This library implements the Universally Unique Lexicographically Sortable Identifier, as described at <https://github.com/alizain/ulid>. - -UUID can be suboptimal for many uses-cases because: - -* It isn't the most character efficient way of encoding 128 bits of randomness -* UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address -* UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures -* UUID v4 provides no other information than randomness which can cause fragmentation in many data structures - -Instead, herein is proposed ULID: - -* 128-bit compatibility with UUID -* 1.21e+24 unique ULIDs per millisecond -* Lexicographically sortable! -* Canonically encoded as a 26 character string, as opposed to the 36 character UUID -* Uses Crockford's base32 for better efficiency and readability (5 bits per character) -* Case insensitive -* No special characters (URL safe) - --} -{-# LANGUAGE DeriveDataTypeable #-} -module Data.ULID ( - ULID(..), - getULIDTime, - getULID, - ulidToInteger, - ulidFromInteger -) where - -import Control.DeepSeq -import Data.Binary -import qualified Data.ByteString.Lazy as LBS -import Data.Data -import Data.Hashable -import Data.Monoid ((<>)) -import Data.Time.Clock.POSIX -import System.IO.Unsafe -import qualified System.Random as R - -import Data.Binary.Roll -import Data.ULID.Random -import Data.ULID.TimeStamp - -data ULID = ULID { - timeStamp :: !ULIDTimeStamp, - random :: !ULIDRandom - } - deriving (Eq, Typeable, Data) - --- | Derive a ULID using a specified time and default random number generator -getULIDTime :: POSIXTime -- ^ The specified UNIX time (seconds) to millisecond precision, e.g. 1469918176.385 - -> IO ULID -getULIDTime t = do - let t' = mkULIDTimeStamp t - r <- getULIDRandom - return $ ULID t' r - --- | Derive a ULID using the current time and default random number generator -getULID :: IO ULID -getULID = do - t <- getULIDTimeStamp - r <- getULIDRandom - return $ ULID t r - --- | Convert a ULID to its corresponding (at most) 128-bit Integer. Integer equivalents retain sortable trait (same sort order). --- This could be useful for storing in a database using a smaller field than storing the Show'n string, but still human-readable unlike the Binary version. -ulidToInteger :: ULID -> Integer -ulidToInteger = roll.(LBS.unpack).encode - --- | Convert a ULID from its corresponding 128-bit Integer. -ulidFromInteger :: Integer -- ^ The ULID's Integer equivalent, as generated by toInteger - -> ULID -ulidFromInteger = decode.(LBS.pack).(unroll 16) -- 16 bytes = 128 bit - -instance Ord ULID where - compare (ULID ts1 _) (ULID ts2 _) = compare ts1 ts2 - -instance Show ULID where - show (ULID ts bytes) = (show ts) ++ (show bytes) - -instance Read ULID where - readsPrec _ str = do - (ts, str2) <- reads str - (rn, str3) <- reads str2 - return (ULID ts rn, str3) - -instance Binary ULID where - put (ULID ts bytes) = put ts <> put bytes - get = do - ts <- get - bytes <- get - return $ ULID ts bytes - --- Because of the strictness annotations, this shouldn't be needed and shouldn't do anything --- I tested and confirmed this in the benchmark, but since I did the work to put it here --- It's no harm to leave it in -instance NFData ULID where - rnf (ULID ts bytes) = rnf ts `seq` (rnf bytes `seq` ()) - -instance R.Random ULID where - randomR _ = R.random -- ignore range - random g = unsafePerformIO $ do - t <- getULIDTimeStamp - let (r, g') = mkULIDRandom g - return (ULID t r, g') - randomIO = getULID - -instance Hashable ULID where - hashWithSalt salt ulid = hashWithSalt salt (encode ulid) +{- |+This library implements the+Universally Unique Lexicographically Sortable Identifier,+as described at https://github.com/alizain/ulid.++UUID can be suboptimal for many uses-cases because:++* It isn't the most character efficient way of encoding 128 bits of randomness+* UUID v1/v2 is impractical in many environments,+ as it requires access to a unique, stable MAC address+* UUID v3/v5 requires a unique seed and produces randomly distributed IDs,+ which can cause fragmentation in many data structures+* UUID v4 provides no other information than randomness,+ which can cause fragmentation in many data structures++Instead, herein is proposed ULID:++* 128-bit compatibility with UUID+* 1.21e+24 unique ULIDs per millisecond+* Lexicographically sortable!+* Canonically encoded as a 26 character text,+ as opposed to the 36 character UUID+* Uses Douglas Crockford's base32 for better efficiency and readability+ (5 bits per character)+* Case insensitive+* No special characters (URL safe)+-}++{-# LANGUAGE DeriveDataTypeable #-}+module Data.ULID (+ ULID(..),+ getULIDTime,+ getULID,+ ulidToInteger,+ ulidFromInteger+) where++import Control.DeepSeq+import Data.Binary+import qualified Data.ByteString.Lazy as LBS+import Data.Data+import Data.Hashable+import Data.Monoid ((<>))+import Data.Text as T+import Data.Time.Clock.POSIX+import System.IO.Unsafe+import qualified System.Random as R++import Data.Binary.Roll+import Data.ULID.Random+import Data.ULID.TimeStamp+++{- |+> t <- getULIDTimeStamp+> r <- getULIDRandom+> pure $ ULID t r+-}+data ULID = ULID+ { timeStamp :: !ULIDTimeStamp+ , random :: !ULIDRandom+ }+ deriving (Eq, Typeable, Data)++instance Ord ULID where+ compare (ULID ts1 _) (ULID ts2 _) = compare ts1 ts2++instance Show ULID where+ show (ULID ts bytes) = (show ts) ++ (show bytes)++instance Read ULID where+ readsPrec _ str = do+ (ts, str2) <- reads str+ (rn, str3) <- reads str2+ return (ULID ts rn, str3)++instance Binary ULID where+ put (ULID ts bytes) = put ts <> put bytes+ get = do+ ts <- get+ bytes <- get+ return $ ULID ts bytes++-- | Because of the strictness annotations,+-- this shouldn't be needed and shouldn't do anything.+-- This is tested and confirmed in the benchmark,+-- but since the work to put it here has already been done+-- it's no harm to leave it in.+instance NFData ULID where+ rnf (ULID ts bytes) = rnf ts `seq` (rnf bytes `seq` ())++instance R.Random ULID where+ randomR _ = R.random -- ignore range+ random g = unsafePerformIO $ do+ t <- getULIDTimeStamp+ let (r, g') = mkULIDRandom g+ return (ULID t r, g')+ randomIO = getULID++instance Hashable ULID where+ hashWithSalt salt ulid = hashWithSalt salt (encode ulid)+++-- | Derive a ULID using a specified time and default random number generator+getULIDTime+ :: POSIXTime -- ^ Specified UNIX time with millisecond precision+ -- (e.g. 1469918176.385)+ -> IO ULID+getULIDTime t = do+ let t' = mkULIDTimeStamp t+ r <- getULIDRandom+ return $ ULID t' r+++-- | Derive a ULID using the current time and default random number generator+getULID :: IO ULID+getULID = do+ t <- getULIDTimeStamp+ r <- getULIDRandom+ return $ ULID t r+++-- | Convert a ULID to its corresponding (at most) 128-bit Integer.+-- Integer equivalents retain sortable trait (same sort order).+-- This could be useful for storing in a database using a smaller field+-- than storing the shown `Text`,+-- but still human-readable unlike the Binary version.+ulidToInteger :: ULID -> Integer+ulidToInteger = roll.(LBS.unpack).encode+++-- | Convert a ULID from its corresponding 128-bit Integer.+ulidFromInteger+ :: Integer -- ^ The ULID's Integer equivalent, as generated by toInteger+ -> Either Text ULID+ulidFromInteger n+ | n < 0 = Left "Value must not be negative"+ | n > maxValidInteger = Left+ "Value must not be larger than the maximum safe Integer size (128 bits)"+ | otherwise = Right+ . decode . LBS.pack . (unroll 16) $ n -- 16 bytes = 128 bit+ where+ maxValidInteger :: Integer+ maxValidInteger = (2 ^ 128) - 1
+ src/Data/ULID/Base32.hs view
@@ -0,0 +1,155 @@+-- | Partly adapted from https://hackage.haskell.org/package/crockford+module Data.ULID.Base32+ ( encode+ , encodeChar+ , decode+ , decodeChar+ )+where++import Data.Char+import Data.Maybe+import Text.Read+import Data.Text as T++import Data.ULID.Digits (digits, unDigits)+++-- | Decodes a Crockford base 32 encoded `Text` into an natural number,+-- if possible. Returns `Nothing` if the `Text` is not a valid encoded value.+decodePlain :: Integral i => Text -> Maybe i+decodePlain base32text = do+ numbers <- mapM decodeChar $ T.unpack base32text+ pure $ unDigits 32 numbers+++-- | Encodes an natural number into a Text,+-- using Douglas Crockford's base 32 encoding.+-- Returns `Nothing` if number is negative.+encodePlain :: Integral i => i -> Text+encodePlain =+ T.pack . fmap encodeChar . digits 32+++-- | Decode a character to its corresponding integer+decodeChar :: Integral i => Char -> Maybe i+decodeChar c = case Data.Char.toUpper c of+ '0' -> Just 0+ 'O' -> Just 0+ '1' -> Just 1+ 'I' -> Just 1+ 'L' -> Just 1+ '2' -> Just 2+ '3' -> Just 3+ '4' -> Just 4+ '5' -> Just 5+ '6' -> Just 6+ '7' -> Just 7+ '8' -> Just 8+ '9' -> Just 9+ 'A' -> Just 10+ 'B' -> Just 11+ 'C' -> Just 12+ 'D' -> Just 13+ 'E' -> Just 14+ 'F' -> Just 15+ 'G' -> Just 16+ 'H' -> Just 17+ 'J' -> Just 18+ 'K' -> Just 19+ 'M' -> Just 20+ 'N' -> Just 21+ 'P' -> Just 22+ 'Q' -> Just 23+ 'R' -> Just 24+ 'S' -> Just 25+ 'T' -> Just 26+ 'V' -> Just 27+ 'W' -> Just 28+ 'X' -> Just 29+ 'Y' -> Just 30+ 'Z' -> Just 31+ _ -> Nothing+++-- | Encode an integer to its corresponding character+encodeChar :: Integral i => i -> Char+encodeChar i = case i of+ 0 -> '0'+ 1 -> '1'+ 2 -> '2'+ 3 -> '3'+ 4 -> '4'+ 5 -> '5'+ 6 -> '6'+ 7 -> '7'+ 8 -> '8'+ 9 -> '9'+ 10 -> 'A'+ 11 -> 'B'+ 12 -> 'C'+ 13 -> 'D'+ 14 -> 'E'+ 15 -> 'F'+ 16 -> 'G'+ 17 -> 'H'+ 18 -> 'J'+ 19 -> 'K'+ 20 -> 'M'+ 21 -> 'N'+ 22 -> 'P'+ 23 -> 'Q'+ 24 -> 'R'+ 25 -> 'S'+ 26 -> 'T'+ 27 -> 'V'+ 28 -> 'W'+ 29 -> 'X'+ 30 -> 'Y'+ 31 -> 'Z'+ _ -> '0'+++-- | Source: https://stackoverflow.com/a/29153602+-- The safety for m > length was removed, because that should never happen.+-- If it does, it should crash.+leftpad :: Int -> Text -> Text+leftpad m xs =+ T.replicate (m - T.length xs) "0" <> xs+++-- | Converts all negative numbers to 0+clampZero :: Integral i => i -> i+clampZero x =+ if x < 0+ then 0+ else x+++-- | >>> encode 5 123+-- "0003V"+--+-- | >>> encode (-5) (-123)+-- ""+encode+ :: Integral i+ => Int -- ^ Overall length of resulting Text+ -> i -- ^ Natural number to encode+ -> Text -- ^ 0 padded, Douglas Crockford's base 32 encoded Text+encode width =+ (leftpad $ clampZero width) . encodePlain . clampZero+++-- | >>> decode 5 "0003V"+-- [(123,"")]+decode+ :: Integral i+ => Int -- ^ Overall length of input Text+ -> Text -- ^ Base 32 encoded Text+ -> [(i, Text)] -- ^ List of possible parses+decode width str | T.length str >= width = let+ (crock, remainder) = T.splitAt width str+ in case decodePlain crock of+ Nothing -> []+ Just c -> [(c, remainder)]+ | otherwise = []
− src/Data/ULID/Crockford.hs
@@ -1,20 +0,0 @@-module Data.ULID.Crockford (encode, decode) where - -import qualified Codec.Crockford as CR -import Text.Read - --- source: https://stackoverflow.com/a/29153602 --- I removed the safety for m > length because that should never happen --- and if it does, I want it to crash! -leftpad m xs = replicate (m - length xs) '0' ++ xs - -encode :: Int -> Integer -> String -encode width = (leftpad width).(CR.encode) - -decode :: Int -> ReadS Integer -decode width str | length str >= width = let - (crock, remainder) = splitAt width str - in case CR.decode crock of - Nothing -> [] - Just c -> [(c, remainder)] - | otherwise = []
+ src/Data/ULID/Digits.hs view
@@ -0,0 +1,54 @@+-- | Adapted from https://hackage.haskell.org/package/digits+module Data.ULID.Digits+ ( digits+ , unDigits+ )+where++import Data.Maybe (fromJust)+import Data.List (genericTake)+++-- | Returns the digits of a positive integer as a Maybe list, in reverse order+-- or Nothing if a zero or negative base is given+-- This is slightly more efficient than in forward order.+mDigitsRev :: Integral n+ => n -- ^ The base to use.+ -> n -- ^ The number to convert to digit form.+ -> Maybe [n] -- ^ Nothing or Just the digits of the number+ -- in list form, in reverse.+mDigitsRev base i = if base < 1+ then Nothing -- We do not support zero or negative bases+ else Just $ dr base i+ where+ dr _ 0 = []+ dr b x = case base of+ 1 -> genericTake x $ repeat 1+ _ -> let (rest, lastDigit) = quotRem x b+ in lastDigit : dr b rest+++-- | Returns the digits of a positive integer as a list, in reverse order.+-- Throws an error if given a zero or negative base.+digitsRev :: Integral n+ => n -- ^ The base to use.+ -> n -- ^ The number to convert to digit from.+ -> [n] -- ^ The digits of the number in list from, in reverse.+digitsRev base = fromJust . mDigitsRev base+++-- | Returns the digits of a positive integer as a list.+-- Throws an error if given a zero or negative base.+digits :: Integral n+ => n -- ^ The base to use (typically 10).+ -> n -- ^ The number to convert to digit form.+ -> [n] -- ^ Either Nothing or the digits of the number in list form.+digits base = reverse . digitsRev base+++-- | Takes a list of digits, and converts them back into a positive integer.+unDigits :: Integral n+ => n -- ^ The base to use.+ -> [n] -- ^ The digits of the number in list form.+ -> n -- ^ The original number.+unDigits base = foldl (\ a b -> a * base + b) 0
src/Data/ULID/Random.hs view
@@ -1,58 +1,78 @@-{-# LANGUAGE DeriveDataTypeable #-} -module Data.ULID.Random ( - ULIDRandom, - mkCryptoULIDRandom, - mkULIDRandom, - getULIDRandom -) where - -import Control.DeepSeq -import Control.Monad -import Crypto.Random -import Data.Binary -import Data.Binary.Roll -import qualified Data.ByteString as BS -import Data.Data -import Data.Word -import System.Random - - -import qualified Data.ULID.Crockford as CR - - -newtype ULIDRandom = ULIDRandom BS.ByteString - deriving (Eq, Typeable, Data) - -numBytes = 10 -- 80 bits - --- | Generate a ULID Random based on a cryptographically secure random number generator. --- | see: https://hackage.haskell.org/package/crypto-api-0.13.2/docs/Crypto-Random.html -mkCryptoULIDRandom :: CryptoRandomGen g => g -> Either GenError (ULIDRandom, g) -mkCryptoULIDRandom g = do - (b, g2) <- genBytes numBytes g - return (ULIDRandom b, g2) - --- | Generate a ULID Random based on a standard random number generator. --- | see: https://hackage.haskell.org/package/random-1.1/docs/System-Random.html -mkULIDRandom :: RandomGen g => g -> (ULIDRandom, g) -mkULIDRandom g = let - (g1, g2) = split g - genbytes = (BS.pack) . take numBytes . randoms - in (ULIDRandom $ genbytes g, g2) - --- | Generate a ULID Random based on the global random number generator. -getULIDRandom :: IO ULIDRandom -getULIDRandom = fst <$> mkULIDRandom <$> newStdGen -- Note: the call to newStdGen splits the generator, so this is safe to call multiple times - -instance Show ULIDRandom where - show (ULIDRandom r) = (CR.encode) 16.roll.(BS.unpack) $ r - -instance Read ULIDRandom where - readsPrec _ = map (\(c,r)->(ULIDRandom $ (BS.pack) $ unroll numBytes c, r)) . (CR.decode) 16 - -instance Binary ULIDRandom where - put (ULIDRandom r) = mapM_ put (BS.unpack $ r) - get = ULIDRandom <$> (BS.pack) <$> replicateM numBytes get - -instance NFData ULIDRandom where - rnf (ULIDRandom r) = rnf r +-- | Helper functions to generate the random part of an ULID+-- either with PRNGs or TRNGs.++{-# LANGUAGE DeriveDataTypeable #-}+module Data.ULID.Random (+ ULIDRandom,+ mkCryptoULIDRandom,+ mkULIDRandom,+ getULIDRandom+) where++import Control.DeepSeq+import Control.Monad+import Crypto.Random+import Data.Binary+import Data.Binary.Roll+import Data.ByteString as BS hiding (split, take)+import Data.Data+import Data.Maybe+import Data.Word+import Data.Text as T hiding (split, take)+import System.Random+++import qualified Data.ULID.Base32 as B32+++-- | Newtype wrapping a `ByteString`+newtype ULIDRandom = ULIDRandom BS.ByteString+ deriving (Eq, Typeable, Data)++instance Show ULIDRandom where+ show (ULIDRandom r) = T.unpack $ B32.encode 16.roll.(BS.unpack) $ r++instance Read ULIDRandom where+ readsPrec _ = fmap+ (\(int, rest) ->+ (ULIDRandom $ BS.pack $ unroll numBytes int, T.unpack rest))+ . (B32.decode $ 16)+ . T.pack++instance Binary ULIDRandom where+ put (ULIDRandom r) = mapM_ put (BS.unpack $ r)+ get = ULIDRandom <$> (BS.pack) <$> replicateM numBytes get++instance NFData ULIDRandom where+ rnf (ULIDRandom r) = rnf r+++numBytes = 10 -- 80 bits+++-- | Generate a `ULIDRandom` based on a cryptographically secure+-- random number generator.+-- See:+-- https://hackage.haskell.org/package/crypto-api-0.13.3/docs/Crypto-Random.html+mkCryptoULIDRandom :: CryptoRandomGen g => g -> Either GenError (ULIDRandom, g)+mkCryptoULIDRandom g = do+ (b, g2) <- genBytes numBytes g+ return (ULIDRandom b, g2)+++-- | Generate a `ULIDRandom` based on a standard random number generator.+-- See:+-- https://hackage.haskell.org/package/random-1.1/docs/System-Random.html+mkULIDRandom :: RandomGen g => g -> (ULIDRandom, g)+mkULIDRandom g = let+ (g1, g2) = split g+ genbytes = (BS.pack) . take numBytes . randoms+ in (ULIDRandom $ genbytes g, g2)+++-- | Generate a ULID Random based on the global random number generator.+getULIDRandom :: IO ULIDRandom+-- | Note: The call to `newStdGen` splits the generator,+-- so this is safe to call multiple times+getULIDRandom =+ fst <$> mkULIDRandom <$> newStdGen
src/Data/ULID/TimeStamp.hs view
@@ -1,44 +1,56 @@-{-# LANGUAGE DeriveDataTypeable #-} -module Data.ULID.TimeStamp ( - ULIDTimeStamp, - mkULIDTimeStamp, - getULIDTimeStamp -) where - -import Control.DeepSeq -import Control.Monad -import Data.Binary -import Data.Binary.Roll -import Data.Data -import Data.Time.Clock -import Data.Time.Clock.POSIX - -import qualified Data.ULID.Crockford as CR - -numBytes = 6 -- 48 bits - --- UNIX time in milliseconds -newtype ULIDTimeStamp = ULIDTimeStamp Integer - deriving (Eq, Ord, Typeable, Data) - --- | Generate a ULID Timestamp based on a specified time -mkULIDTimeStamp :: POSIXTime -- ^ The specified UNIX time (seconds) to millisecond precision, e.g. 1469918176.385 - -> ULIDTimeStamp -mkULIDTimeStamp = ULIDTimeStamp . round . (*1000) - --- | Generate a ULID Timestamp based on current system UNIX time -getULIDTimeStamp :: IO ULIDTimeStamp -getULIDTimeStamp = mkULIDTimeStamp <$> getPOSIXTime - -instance Show ULIDTimeStamp where - show (ULIDTimeStamp i) = CR.encode 10 i - -instance Read ULIDTimeStamp where - readsPrec _ = map (\(c,r)->(ULIDTimeStamp c, r)) . (CR.decode) 10 - -instance Binary ULIDTimeStamp where - put (ULIDTimeStamp i) = mapM_ put (unroll numBytes i) - get = ULIDTimeStamp <$> roll <$> replicateM numBytes get - -instance NFData ULIDTimeStamp where - rnf (ULIDTimeStamp i) = rnf i +-- | Custom data type for timestamps (milliseconds since 1970)++{-# LANGUAGE DeriveDataTypeable #-}+module Data.ULID.TimeStamp (+ ULIDTimeStamp,+ mkULIDTimeStamp,+ getULIDTimeStamp+) where++import Control.DeepSeq+import Control.Monad+import Data.Binary+import Data.Binary.Roll+import Data.Data+import Data.Maybe+import Data.Text as T+import Data.Time.Clock+import Data.Time.Clock.POSIX++import qualified Data.ULID.Base32 as B32+++numBytes = 6 -- 48 bits++-- | UNIX time in milliseconds+newtype ULIDTimeStamp = ULIDTimeStamp Integer+ deriving (Eq, Ord, Typeable, Data)++instance Show ULIDTimeStamp where+ show (ULIDTimeStamp i) = T.unpack $ B32.encode 10 i++instance Read ULIDTimeStamp where+ readsPrec _ = fmap+ (\(int, rest)->(ULIDTimeStamp int, T.unpack rest))+ . (B32.decode) 10+ . T.pack++instance Binary ULIDTimeStamp where+ put (ULIDTimeStamp i) = mapM_ put (unroll numBytes i)+ get = ULIDTimeStamp <$> roll <$> replicateM numBytes get++instance NFData ULIDTimeStamp where+ rnf (ULIDTimeStamp i) = rnf i+++-- | Generate a ULID Timestamp based on a specified time+mkULIDTimeStamp+ :: POSIXTime -- ^ Specified UNIX time with millisecond precision+ -- (e.g. 1469918176.385)+ -> ULIDTimeStamp+mkULIDTimeStamp = ULIDTimeStamp . round . (*1000)+++-- | Generate a ULID Timestamp based on current system UNIX time+getULIDTimeStamp :: IO ULIDTimeStamp+getULIDTimeStamp = mkULIDTimeStamp <$> getPOSIXTime
+ test/Data/ULID/Base32Spec.hs view
@@ -0,0 +1,43 @@+module Data.ULID.Base32Spec where++import Data.ULID.Base32++import Test.Hspec+++-- | Known examples from+-- https://web.archive.org/web/20171219211624/crockfordbase32.codeplex.com+spec :: Spec+spec = do+ describe "encode" $ do+ it "compare to known examples" $ do+ encode 1 1 `shouldBe` "1"+ encode 2 194 `shouldBe` "62"+ encode 11 3838385658376483 `shouldBe` "3D2ZQ6TVC93"+ encode 13 18446744073709551615 `shouldBe` "FZZZZZZZZZZZZ"++ it "compare to known examples (padded)" $ do+ encode 3 1 `shouldBe` "001"+ encode 3 194 `shouldBe` "062"+ encode 15 3838385658376483 `shouldBe` "00003D2ZQ6TVC93"++ describe "decode" $ do+ it "compare to known examples" $ do+ decode 1 "1" `shouldBe` [(1, "")]+ decode 2 "62" `shouldBe` [(194, "")]+ decode 11 "3D2ZQ6TVC93" `shouldBe` [(3838385658376483, "")]+ decode 13 "FZZZZZZZZZZZZ" `shouldBe` [(18446744073709551615, "")]++ it "compare to known examples (padded)" $ do+ decode 3 "001" `shouldBe` [(1, "")]+ decode 3 "062" `shouldBe` [(194, "")]+ decode 15 "00003D2ZQ6TVC93" `shouldBe` [(3838385658376483, "")]++ it "gives remainder text" $ do+ decode 3 "001ABC" `shouldBe` [(1, "ABC")]+ decode 3 "062DEF" `shouldBe` [(194, "DEF")]+ decode 15 "00003D2ZQ6TVC93X1" `shouldBe` [(3838385658376483, "X1")]++ it "gives empty list if invalid" $ do+ decode 3 "U01ABC" `shouldBe` []+ decode 2 "!01DEF" `shouldBe` []
+ test/Data/ULID/RandomSpec.hs view
@@ -0,0 +1,35 @@+module Data.ULID.RandomSpec where++import Data.Binary+import qualified Data.ByteString.Lazy as LBS++import Data.ULID.Random++import Test.Hspec+++spec :: Spec+spec = do+ describe "show/read" $ do+ it "has show/read symmetry" $ do+ a1 <- getULIDRandom+ a2 <- getULIDRandom+ a1 == a2 `shouldBe` False+ read (show a1) `shouldBe` a1+ read (show a2) `shouldBe` a2++ it "has correct show length" $ do+ a1 <- getULIDRandom+ length (show a1) `shouldBe` 16++ describe "encode/decode" $ do+ it "has correct binary length" $ do+ a1 <- getULIDRandom+ LBS.length (encode a1) `shouldBe` 10 -- 80 bit++ it "has encode/decode symmetry" $ do+ a1 <- getULIDRandom+ a2 <- getULIDRandom+ a1 == a2 `shouldBe` False+ decode (encode a1) `shouldBe` a1+ decode (encode a2) `shouldBe` a2
+ test/Data/ULID/TimeStampSpec.hs view
@@ -0,0 +1,46 @@+module Data.ULID.TimeStampSpec where++import Data.Binary+import qualified Data.ByteString.Lazy as LBS++import Data.ULID.TimeStamp++import Test.Hspec+++spec :: Spec+spec = do+ describe "show/read" $ do+ it "works with a known value" $ do+ show (mkULIDTimeStamp 1469918176.385) `shouldBe` "01ARYZ6S41"++ it "has show/read symmetry" $ do+ let a1 = mkULIDTimeStamp 12345+ let a2 = mkULIDTimeStamp 54321+ a1 == a2 `shouldBe` False+ read (show a1) `shouldBe` a1+ read (show a2) `shouldBe` a2++ it "has correct show length" $ do+ let a1 = mkULIDTimeStamp 12345+ length (show a1) `shouldBe` 10++ describe "encode/decode" $ do+ it "has correct binary length" $ do+ let a1 = mkULIDTimeStamp 12345+ LBS.length (encode a1) `shouldBe` 6 -- 48 bit++ it "has encode/decode symmetry" $ do+ let a1 = mkULIDTimeStamp 12345+ let a2 = mkULIDTimeStamp 54321+ a1 == a2 `shouldBe` False+ decode (encode a1) `shouldBe` a1+ decode (encode a2) `shouldBe` a2++ it "encodes MSB first" $ do+ let a1 = mkULIDTimeStamp 12345+ let e1 = encode a1+ -- This works because the value is small,+ -- so the MSB for this value should be 0+ LBS.head e1 `shouldBe` 0+ LBS.last e1 `shouldNotBe` 0
+ test/Data/ULIDSpec.hs view
@@ -0,0 +1,177 @@+module Data.ULIDSpec where++import Control.Concurrent+import Control.Monad (replicateM)+import Data.Binary+import qualified Data.ByteString.Lazy as LBS+import Data.Char+import Data.Hashable+import Data.List (nub, sort)+import qualified System.Random as R++import Data.ULID++import Test.Hspec+++spec :: Spec+spec = do+ describe "ulid capabilities" $ do+ it "binary length 128-bit" $ do+ a1 <- getULID+ LBS.length (encode a1) `shouldBe` 16 -- 128 bit++ it "is lexicographically sortable" $ do+ u1 <- getULID+ threadDelay 1000+ u2 <- getULID+ threadDelay 1000+ u3 <- getULID+ threadDelay 1000+ u4 <- getULID+ threadDelay 1000+ let l = [show u3, show u2, show u4, show u1]+ let l' = sort l+ l' `shouldBe` [show u1, show u2, show u3, show u4]++ -- make sure it works in internal representation too :)+ let ul = [u3, u2, u4, u1]+ let ul' = sort ul+ ul' `shouldBe` [u1, u2, u3, u4]++ it "is encoded as 26 character text" $ do+ u1 <- getULID+ length (show u1) `shouldBe` 26++ it "is case-insensitive" $ do+ u1 <- getULID+ let u2 = read (map toLower (show u1))+ let u3 = read (map toUpper (show u1))+ u1 `shouldBe` u2+ u1 `shouldBe` u3++ it "no special characters" $ do+ u1 <- getULID+ filter (not.isAlphaNum) (show u1) `shouldBe` []++ describe "ulid" $ do+ it "starts with 0 (at least for the foreseeable future)" $ do+ u1 <- getULID+ head (show u1) `shouldBe` '0'++ it "generates unique ulids in default configuration" $ do+ let ops = 1000+ ulids <- replicateM ops getULID+ -- Verify uniqueness+ let n' = length $ nub ulids+ n' `shouldBe` ops++ describe "encode/decode" $ do+ it "has encode/decode symmetry" $ do+ a1 <- getULID+ a2 <- getULID+ a1 == a2 `shouldBe` False+ decode (encode a1) `shouldBe` a1+ decode (encode a2) `shouldBe` a2+ encode a1 `shouldNotBe` encode a2++ it "encodes MSB first" $ do+ a1 <- getULIDTime 12345+ let e1 = encode a1+ -- This works because the time value is small,+ -- and time is sequences first,+ -- so the MSB for this value should be 0+ LBS.head e1 `shouldBe` 0+ LBS.last e1 `shouldNotBe` 0++ describe "random" $ do+ it "works in IO" $ do+ u1 <- (R.randomIO :: IO ULID)+ u2 <- (R.randomIO :: IO ULID)+ u1 `shouldNotBe` u2++ it "works with randomgen" $ do+ g <- R.getStdGen+ let (u1, g') = R.random g :: (ULID, R.StdGen)+ let (u2, _) = R.random g' :: (ULID, R.StdGen)+ u1 `shouldNotBe` u2++ describe "hash" $ do+ -- | The general contract of hashWithSalt is:+ -- If two values are equal according to the == method,+ -- then applying the hashWithSalt method on each of the two values+ -- must produce the same integer result+ -- if the same salt is used in each case.+ it "produces same hash for equal ulids" $ do+ u1 <- getULID+ let u2 = (read (show u1)) :: ULID+ let salt = 12345+ hashWithSalt salt u1 `shouldBe` hashWithSalt salt u2++ -- | It is not required that if two values are unequal+ -- according to the == method,+ -- then applying the hashWithSalt method on each of the two values+ -- must produce distinct integer results.+ -- However, the programmer should be aware+ -- that producing distinct integer results for unequal values+ -- may improve the performance of hashing-based data structures.+ it "produces different hash for nonequals ulids" $ do+ u1 <- getULID+ u2 <- getULID+ let salt = 12345+ -- this could rarely fail due to hash nature+ hashWithSalt salt u1 `shouldNotBe` hashWithSalt salt u2++ -- | This method can be used to compute different hash values+ -- for the same input by providing a different salt+ -- in each application of the method.+ -- This implies that any instance that defines `hashWithSalt` must+ -- make use of the salt in its implementation.+ it "produces different hash for equals ulids with different salt" $ do+ u1 <- getULID+ let u2 = (read (show u1)) :: ULID+ let salt = 12345+ hashWithSalt salt u1 `shouldBe` hashWithSalt salt u2+ let salt2 = 54321+ hashWithSalt salt u1 `shouldNotBe` hashWithSalt salt2 u2++ describe "to/from integer" $ do+ it "is sortable" $ do+ u1 <- getULID+ threadDelay 1000+ u2 <- getULID+ threadDelay 1000+ u3 <- getULID+ threadDelay 1000+ u4 <- getULID+ threadDelay 1000+ let+ l = [ ulidToInteger u3+ , ulidToInteger u2+ , ulidToInteger u4+ , ulidToInteger u1+ ]+ l' = sort l+ l' `shouldBe` [ ulidToInteger u1+ , ulidToInteger u2+ , ulidToInteger u3+ , ulidToInteger u4+ ]++ it "has to/from symmetry" $ do+ a1 <- getULID+ a2 <- getULID+ a1 == a2 `shouldBe` False+ ulidFromInteger (ulidToInteger a1) `shouldBe` (Right a1)+ ulidFromInteger (ulidToInteger a2) `shouldBe` (Right a2)+ ulidToInteger a1 `shouldNotBe` ulidToInteger a2++ it "handles out-of-range integer" $ do+ a1 <- getULID+ ulidFromInteger (negate (ulidToInteger a1))+ `shouldBe` Left "Value must not be negative"+ ulidFromInteger (-1)+ `shouldBe` Left "Value must not be negative"+ ulidFromInteger (2 ^ 128)+ `shouldBe` Left "Value must not be larger than \+ \the maximum safe Integer size (128 bits)"
test/Spec.hs view
@@ -1,3 +1,3 @@ {-# OPTIONS_GHC -F -pgmF hspec-discover #-}--- see https://github.com/hspec/hspec-example/blob/master/strip.cabal+-- See https://github.com/hspec/hspec-example/blob/master/strip.cabal -- runhaskell -isrc -itest test/Spec.hs
ulid.cabal view
@@ -1,50 +1,64 @@ name: ulid-version: 0.2.0.0-synopsis: Implementation of ULID, lexicographically sortable unique identifiers-description: Implementation of alizain's ULID identifier. Canonically encoded as a 26 character string, as opposed to the 36 character UUID. - Uses Crockford's base32 for better efficiency and readability (5 bits per character)-homepage: https://github.com/steven777400/ulid+version: 0.3.0.0+synopsis: Implementation of ULID - Universally Unique+ Lexicographically Sortable Identifier+description: Implementation of Alizain Feerasta's ULID specification.+ A 26 character string identifier,+ as opposed to the 36 character UUID string.+ Uses Douglas Crockford's base 32 encoding+ for better efficiency and readability+ (5 bits per character).+homepage: https://github.com/ad-si/ulid license: BSD3 license-file: LICENSE author: Steve Kollmansberger-maintainer: steve@kolls.net+maintainer: ulid@ad-si.com copyright: 2017 Steve Kollmansberger-category: Data+category: Data, Codec, Database build-type: Simple extra-source-files: README.md cabal-version: >=1.10 + library hs-source-dirs: src exposed-modules: Data.ULID- , Data.ULID.Crockford+ , Data.ULID.Base32+ , Data.ULID.Digits , Data.ULID.Random , Data.ULID.TimeStamp- other-modules: Data.Binary.Roll + other-modules: Data.Binary.Roll build-depends: base >= 4.7 && < 5- , crockford- , time- , crypto-api- , random- , bytestring , binary+ , bytestring+ , crypto-api , deepseq , hashable+ , random+ , text+ , time default-language: Haskell2010+ default-extensions: OverloadedStrings + executable ulid-exe hs-source-dirs: app main-is: Main.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: base , ulid- , crypto-api + , crypto-api default-language: Haskell2010 + test-suite ulid-test type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs+ other-modules: Data.ULID.Base32Spec+ , Data.ULID.RandomSpec+ , Data.ULID.TimeStampSpec+ , Data.ULIDSpec build-depends: base , hspec , ulid@@ -54,18 +68,23 @@ , hashable ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010+ default-extensions: OverloadedStrings + benchmark ulid-bench- type: exitcode-stdio-1.0- hs-source-dirs: bench- main-is: Main.hs- build-depends: base- , ulid - , time- , text- , format-numbers- , deepseq+ type: exitcode-stdio-1.0+ hs-source-dirs: bench+ main-is: Main.hs+ build-depends: base+ , ulid+ , time+ , text+ , format-numbers+ , deepseq+ default-language: Haskell2010+ default-extensions: OverloadedStrings + source-repository head type: git- location: https://github.com/steven777400/ulid.git+ location: https://github.com/ad-si/ulid.git