packages feed

ascii-numbers-1.2.0.0: library/ASCII/Hexadecimal.hs

module ASCII.Hexadecimal
    (
    {- * Read/show for numeric strings -}
    {- ** Natural  -} showNatural,  readNatural,
    {- ** Integer  -} showInteger,  readInteger,
    {- ** Integral -} showIntegral, readIntegral,

    {- * Various digit types -} HexChar (..), HexLetter (..), HexCharBreakdown (..),

    {- * Monomorphic character conversions -}
    {- ** HexLetter ↔ D16              -} hexLetterD16, d16HexLetter,
    {- ** HexLetter ↔ HexChar          -} letterHexChar, hexCharLetter,
    {- ** HexChar   ↔ ASCII Char       -} hexAsciiChar, asciiCharHex,
    {- ** HexChar   ↔ D16              -} d16HexChar, hexCharD16,
    {- ** HexChar   ↔ HexCharBreakdown -} breakDownHexChar, assembleHexChar,

    {- * Hexadecimal character superset classes -}
    {- * Of hex character       -} HexCharSuperset (..),
    {- * Of hex character lists -} HexStringSuperset (..),

    {- * Character/number conversions -}
    {- ** Natural ↔ HexChar -} naturalHexCharMaybe, hexCharNatural, naturalHexCharUnsafe,
    {- ** Natural ↔ D16     -} naturalD16Maybe,     d16Natural,     naturalD16Unsafe,
    {- ** Integer ↔ HexChar -} integerHexCharMaybe, hexCharInteger, integerHexCharUnsafe,
    {- ** Integer ↔ D16     -} integerD16Maybe,     d16Integer,     integerD16Unsafe

    ) where

import ASCII.Case (Case (..))
import qualified ASCII.Char as ASCII
import qualified ASCII.Decimal as Dec
import ASCII.Refinement (ASCII, asciiUnsafe, lift)
import ASCII.Superset (StringSuperset, fromChar, fromCharList, toCharListMaybe,
                       toCharMaybe)
import ASCII.Word4 (Word4)
import qualified ASCII.Word4 as Word4

import Control.Monad (guard, (<=<), (=<<))
import Data.Bifoldable (bifoldMap)
import Data.Bits (Bits, toIntegralSized)
import Data.Bool (Bool, (&&))
import Data.Data (Data)
import Data.Eq (Eq)
import Data.Function (id, ($), (.))
import Data.Functor (fmap)
import Data.Hashable (Hashable)
import Data.Maybe (Maybe (Just, Nothing), fromJust, isJust)
import Data.Monoid (mempty)
import Data.Ord (Ord (..))
import Data.Word (Word8)
import GHC.Generics (Generic)
import Numeric.Natural (Natural)
import Prelude (Bounded (..), Enum (..), Integer, Integral, abs, fromEnum,
                fromInteger, fromIntegral, negate, quotRem, toEnum, toInteger,
                (*), (+), (-))
import Text.Show (Show)

import qualified Data.Bool as Bool
import qualified Data.Char as Unicode
import qualified Data.List as List

import Data.List.NonEmpty (NonEmpty, nonEmpty)

import qualified DList

import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Lazy as LBS

import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as TB

---  Types  ---

-- | Letters used as hexadecimal digits above 9, without a notion of case.
data HexLetter =
    LetterA -- ^ 10
  | LetterB -- ^ 11
  | LetterC -- ^ 12
  | LetterD -- ^ 13
  | LetterE -- ^ 14
  | LetterF -- ^ 15
    deriving stock (Bounded, Enum, Eq, Ord, Show, Data, Generic)
    deriving anyclass Hashable

{- | The subset of ASCII used to represent hexadecimal numbers:

* 'ASCII.Char.Digit0' to 'ASCII.Char.Digit9'
* 'ASCII.Char.CapitalLetterA' to 'ASCII.Char.CapitalLetterF'
* 'ASCII.Char.SmallLetterA' to 'ASCII.Char.SmallLetterF'
-}
data HexChar =
      Digit0 | Digit1 | Digit2 | Digit3 | Digit4
    | Digit5 | Digit6 | Digit7 | Digit8 | Digit9
    | CapitalLetterA | CapitalLetterB | CapitalLetterC
    | CapitalLetterD | CapitalLetterE | CapitalLetterF
    | SmallLetterA | SmallLetterB | SmallLetterC
    | SmallLetterD | SmallLetterE | SmallLetterF
    deriving stock (Bounded, Enum, Eq, Ord, Show, Data, Generic)
    deriving anyclass Hashable

data HexCharBreakdown = HexDigit Dec.Digit | HexLetter Case HexLetter
    deriving stock (Eq, Ord, Show, Data, Generic)
    deriving anyclass Hashable

-- | Behaves the same as 'HexChar'
instance Enum HexCharBreakdown
  where
    fromEnum = fromEnum . assembleHexChar
    toEnum = breakDownHexChar . toEnum

-- | Behaves the same as 'HexChar'
instance Bounded HexCharBreakdown
  where
    minBound = breakDownHexChar minBound
    maxBound = breakDownHexChar maxBound


---  Monomorphic conversions between the character types  ---

breakDownHexChar :: HexChar -> HexCharBreakdown
breakDownHexChar =
    \case
        x | x <= Digit9          ->  HexDigit            (toEnum (fromEnum x))
        x | x <= CapitalLetterF  ->  HexLetter UpperCase (toEnum (fromEnum x - 10))
        x                        ->  HexLetter LowerCase (toEnum (fromEnum x - 16))

assembleHexChar :: HexCharBreakdown -> HexChar
assembleHexChar =
    \case
        HexDigit x             ->  toEnum (fromEnum x)
        HexLetter UpperCase x  ->  toEnum (fromEnum x + 10)
        HexLetter LowerCase x  ->  toEnum (fromEnum x + 16)

d16HexChar :: Case -> Word4 -> HexChar
d16HexChar c =
    toEnum
    .
    case c of
        UpperCase -> fromEnum
        LowerCase -> \case
            x | x <= Word4.Number9  ->  fromEnum x
            x                       ->  fromEnum x + 6

hexCharD16 :: HexChar -> Word4
hexCharD16 =
    \case
        x | x < SmallLetterA  ->  toEnum (fromEnum x)
        x                     ->  toEnum (fromEnum x - 6)

hexAsciiChar :: HexChar -> ASCII.Char
hexAsciiChar =
    toEnum
    .
    \case
        x | x <= Digit9          ->  fromEnum ASCII.Digit0         + fromEnum x
        x | x <= CapitalLetterF  ->  fromEnum ASCII.CapitalLetterA + fromEnum x - 10
        x                        ->  fromEnum ASCII.SmallLetterA   + fromEnum x - 16

asciiCharHex :: ASCII.Char -> Maybe HexChar
asciiCharHex =
    \case
        x | x >= ASCII.Digit0 && x <= ASCII.Digit9 ->
            Just $ toEnum $
                fromEnum x - fromEnum ASCII.Digit0

        x | x >= ASCII.CapitalLetterA && x <= ASCII.CapitalLetterF ->
            Just $ toEnum $
                fromEnum x + 10 - fromEnum ASCII.CapitalLetterA

        x | x >= ASCII.SmallLetterA && x <= ASCII.SmallLetterF ->
            Just $ toEnum $
                fromEnum x + 16 - fromEnum ASCII.SmallLetterA

        _ -> Nothing

hexLetterD16 :: HexLetter -> Word4
hexLetterD16 = toEnum . (\x -> x + 10) . fromEnum

d16HexLetter :: Word4 -> Maybe HexLetter
d16HexLetter x =
  do
    guard (x >= Word4.Number10)
    Just (toEnum (fromEnum x - 10))

letterHexChar :: Case -> HexLetter -> HexChar
letterHexChar = \case
    UpperCase -> toEnum . (\x -> x + 10) . fromEnum
    LowerCase -> toEnum . (\x -> x + 16) . fromEnum

hexCharLetter :: HexChar -> Maybe HexLetter
hexCharLetter = \case
    x | x <= Digit9          ->  Nothing
    x | x <= CapitalLetterF  ->  Just (toEnum (fromEnum x - 10))
    x                        ->  Just (toEnum (fromEnum x - 16))


---  Monomorphic character/number conversions  ---

naturalHexCharMaybe :: Case -> Natural -> Maybe HexChar
naturalHexCharMaybe =
    \case
        UpperCase -> \case
            x | x <= 15 -> Just (toEnum (fromIntegral x))
            _           -> Nothing
        LowerCase -> \case
            x | x <= 9  -> Just (toEnum (fromIntegral x))
            x | x <= 15 -> Just (toEnum (fromIntegral x + 6))
            _           -> Nothing

naturalHexCharUnsafe :: Case -> Natural -> HexChar
naturalHexCharUnsafe =
    \case
        UpperCase -> \x -> toEnum (fromIntegral x)
        LowerCase -> \case
            x | x <= 9  -> toEnum (fromIntegral x)
            x           -> toEnum (fromIntegral x + 6)

hexCharNatural :: HexChar -> Natural
hexCharNatural =
    fromIntegral
    .
    \case
        x | x > 15  ->  x - 6
        x           ->  x
    .
    fromEnum

integerHexCharMaybe :: Case -> Integer -> Maybe HexChar
integerHexCharMaybe =
    \case
        UpperCase -> \case
            x | x < 0   -> Nothing
            x | x <= 15 -> Just (toEnum (fromIntegral x))
            _           -> Nothing
        LowerCase -> \case
            x | x < 0   -> Nothing
            x | x <= 9  -> Just (toEnum (fromIntegral x))
            x | x <= 15 -> Just (toEnum (fromIntegral x + 6))
            _           -> Nothing

integerHexCharUnsafe :: Case -> Integer -> HexChar
integerHexCharUnsafe =
    \case
        UpperCase -> \x -> toEnum (fromIntegral x)
        LowerCase -> \case
            x | x <= 9  -> toEnum (fromIntegral x)
            x           -> toEnum (fromIntegral x + 6)

hexCharInteger :: HexChar -> Integer
hexCharInteger =
    fromIntegral
    .
    \case
        x | x > 15  ->  x - 6
        x           ->  x
    .
    fromEnum

naturalD16Maybe :: Natural -> Maybe Word4
naturalD16Maybe =
    \case
        x | x <= 15  ->  Just (toEnum (fromIntegral x))
        _            ->  Nothing

d16Natural :: Word4 -> Natural
d16Natural = fromIntegral . fromEnum

integerD16Maybe :: Integer -> Maybe Word4
integerD16Maybe =
    \case
        x | x < 0    ->  Nothing
        x | x <= 15  ->  Just (toEnum (fromInteger x))
        _            ->  Nothing

d16Integer :: Word4 -> Integer
d16Integer = toInteger . fromEnum

naturalD16Unsafe :: Natural -> Word4
naturalD16Unsafe = toEnum . fromIntegral

integerD16Unsafe :: Integer -> Word4
integerD16Unsafe = toEnum . fromIntegral


---  Classes  ---

class HexCharSuperset char
  where

    fromHexChar :: HexChar -> char

    isHexChar :: char -> Bool
    isHexChar = isJust . toHexCharMaybe

    toHexCharUnsafe :: char -> HexChar
    toHexCharUnsafe = fromJust . toHexCharMaybe

    toHexCharMaybe :: char -> Maybe HexChar
    toHexCharMaybe x = if isHexChar x then Just (toHexCharUnsafe x) else Nothing

    {-# minimal fromHexChar, ((isHexChar, toHexCharUnsafe) | toHexCharMaybe) #-}

class HexStringSuperset string
  where

    fromHexCharList :: [HexChar] -> string

    isHexString :: string -> Bool
    isHexString = isJust . toHexCharListMaybe

    toHexCharListUnsafe :: string -> [HexChar]
    toHexCharListUnsafe = fromJust . toHexCharListMaybe

    toHexCharListMaybe :: string -> Maybe [HexChar]
    toHexCharListMaybe x = if isHexString x then Just (toHexCharListUnsafe x) else Nothing

    {-# minimal fromHexCharList, ((isHexString, toHexCharListUnsafe) | toHexCharListMaybe) #-}


---  Show functions  ---

{- |

Examples:

* @showNatural LowerCase 12@ = @"c"@
* @showNatural UpperCase (256 + 12)@ = @"10C"@
* @showNatural UpperCase 0@ = @"0"@

-}
showNatural :: HexStringSuperset string => Case -> Natural -> string
showNatural =
    \c -> \case
        0 -> fromHexCharList [ Digit0 ]
        n -> fromHexCharList (fmap (d16HexChar c) (naturalList n))
  where
    naturalList :: Natural -> [Word4]
    naturalList = DList.toList . r
      where
        r = \case
            0 -> mempty
            n ->
                bifoldMap
                    r
                    (DList.singleton . naturalD16Unsafe)
                    (quotRem n 16)

{- |

Examples:

* @showInteger LowerCase 12@ = @"c"@
* @showInteger LowerCase (negate 12)@ = @"-c"@
* @showInteger UpperCase (256 + 12)@ = @"10C"@
* @showInteger UpperCase (negate (256 + 12))@ = @"-10C"@
* @showInteger UpperCase 0@ = @"0"@

-}
showInteger :: StringSuperset string => Case -> Integer -> string
showInteger = \c -> fromCharList . integerList c
  where
    integerList :: Case -> Integer -> [ASCII.Char]
    integerList c =
        \case
            0           ->  [ ASCII.Digit0 ]
            n | n < 0   ->  ASCII.HyphenMinus : nonNegativeIntegerList c (abs n)
            n           ->  nonNegativeIntegerList c n

    nonNegativeIntegerList :: Case -> Integer -> [ASCII.Char]
    nonNegativeIntegerList c = DList.toList . r
      where
        r = \case
            0 -> mempty
            n ->
                bifoldMap
                    r
                    (DList.singleton . fromHexChar . integerHexCharUnsafe c)
                    (quotRem n 16)

showIntegral :: (StringSuperset string, Integral number) => Case -> number -> string
showIntegral c = showInteger c . toInteger


---  Read functions  ---

{- |

Examples:

* @readNatural "5"@ = @Just 5@
* @readNatural "-5"@ = @Nothing@
* @readNatural "1f"@ = @Just 31@
* @readNatural "1F"@ = @Just 31@
* @readNatural "xa"@ = @Nothing@
* @readNatural ""@ = @Nothing@

-}
readNatural :: HexStringSuperset string => string -> Maybe Natural
readNatural = (Just . readNaturalDigits) <=< nonEmpty <=< (Just . fmap hexCharD16) <=< toHexCharListMaybe
  where
    readNaturalDigits :: NonEmpty Word4 -> Natural
    readNaturalDigits = List.foldl' (\total x -> (16 * total) + d16Natural x) 0

{- |

Examples:

* @readInteger "5"@ = @Just 5@
* @readInteger "-5"@ = @Just (-5)@
* @readInteger "1f"@ = @Just 31@
* @readInteger "1F"@ = @Just 31@
* @readInteger "xa"@ = @Nothing@
* @readInteger ""@ = @Nothing@
* @readInteger "-"@ = @Nothing@

-}
readInteger :: StringSuperset string => string -> Maybe Integer
readInteger = readIntegerCharList <=< toCharListMaybe
  where
    readIntegerCharList :: [ASCII.Char] -> Maybe Integer
    readIntegerCharList =
        \case
            ASCII.HyphenMinus : xs  ->  fmap negate (readNonNegative xs)
            xs                      ->  readNonNegative xs

    readNonNegative :: [ASCII.Char] -> Maybe Integer
    readNonNegative = (Just . toInteger . readIntegerDigits) <=< nonEmpty <=< (Just . fmap hexCharD16) <=< toHexCharListMaybe

    readIntegerDigits :: NonEmpty Word4 -> Integer
    readIntegerDigits = List.foldl' (\total x -> (16 * total) + d16Integer x) 0

{- |

Examples:

* @readIntegral "0014"@ = @Just (20 :: Word8)@
* @readIntegral ""@ = @(Nothing :: Maybe Word8)@
* @readIntegral "-4"@ = @(Nothing :: Maybe Word8)@
* @readIntegral "1234"@ = @(Nothing :: Maybe Word8)@

-}
readIntegral :: (StringSuperset string, Integral number, Bits number) => string -> Maybe number
readIntegral = toIntegralSized <=< readInteger


---  HexCharSuperset instances  ---

instance HexCharSuperset HexChar
  where
    isHexChar _ = Bool.True
    fromHexChar = id
    toHexCharUnsafe = id
    toHexCharMaybe = Just

instance HexCharSuperset ASCII.Char
  where
    fromHexChar = hexAsciiChar
    toHexCharMaybe = asciiCharHex

instance HexCharSuperset Unicode.Char
  where
    fromHexChar = fromChar . fromHexChar
    toHexCharMaybe = toHexCharMaybe <=< toCharMaybe

instance HexCharSuperset Word8
  where
    fromHexChar x = fromChar (fromHexChar x :: ASCII.Char)
    toHexCharMaybe w = toHexCharMaybe =<< (toCharMaybe w :: Maybe ASCII.Char)

instance HexCharSuperset char => HexCharSuperset (ASCII char)
  where
    isHexChar = isHexChar . ASCII.Refinement.lift
    fromHexChar = asciiUnsafe . fromHexChar
    toHexCharUnsafe = toHexCharUnsafe . ASCII.Refinement.lift
    toHexCharMaybe = toHexCharMaybe . ASCII.Refinement.lift


---  HexStringSuperset instances  ---

instance HexStringSuperset [HexChar]
  where
    fromHexCharList = id
    isHexString _ = Bool.True
    toHexCharListUnsafe = id
    toHexCharListMaybe = Just

instance HexStringSuperset [ASCII.Char]
  where
    fromHexCharList = List.map fromHexChar
    isHexString = List.all isHexChar
    toHexCharListUnsafe = List.map toHexCharUnsafe

instance HexStringSuperset [Unicode.Char]
  where
    fromHexCharList = List.map fromHexChar
    isHexString = List.all isHexChar
    toHexCharListUnsafe = List.map toHexCharUnsafe

instance HexStringSuperset T.Text
  where
    fromHexCharList = T.pack . List.map fromHexChar
    isHexString = T.all isHexChar
    toHexCharListUnsafe = List.map toHexCharUnsafe . T.unpack

instance HexStringSuperset LT.Text
  where
    fromHexCharList = LT.pack . List.map fromHexChar
    isHexString = LT.all isHexChar
    toHexCharListUnsafe = List.map toHexCharUnsafe . LT.unpack

instance HexStringSuperset TB.Builder
  where
    fromHexCharList = TB.fromLazyText . fromHexCharList
    toHexCharListMaybe = toHexCharListMaybe . TB.toLazyText

instance HexStringSuperset BS.ByteString
  where
    fromHexCharList = BS.pack . List.map fromHexChar
    isHexString = BS.all isHexChar
    toHexCharListUnsafe = List.map toHexCharUnsafe . BS.unpack

instance HexStringSuperset LBS.ByteString
  where
    fromHexCharList = LBS.pack . List.map fromHexChar
    isHexString = LBS.all isHexChar
    toHexCharListUnsafe = List.map toHexCharUnsafe . LBS.unpack

instance HexStringSuperset BSB.Builder
  where
    fromHexCharList = BSB.lazyByteString . fromHexCharList
    toHexCharListMaybe = toHexCharListMaybe . BSB.toLazyByteString

instance HexStringSuperset char => HexStringSuperset (ASCII char)
  where
    isHexString = isHexString . ASCII.Refinement.lift
    fromHexCharList = asciiUnsafe . fromHexCharList
    toHexCharListUnsafe = toHexCharListUnsafe . ASCII.Refinement.lift
    toHexCharListMaybe = toHexCharListMaybe . ASCII.Refinement.lift