-- | 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 = []