packages feed

binrep-0.2.0: src/Binrep/Type/AsciiNat.hs

{-| Naturals represented via ASCII numerals.

A concept which sees occasional use in places where neither speed nor size
efficiency matter.

The tar file format uses it, apparently to sidestep making a decision on byte
ordering. Though digits are encoded "big-endian", so, uh. I don't get it.

I don't really see the usage of these. It seems silly and inefficient, aimed
solely at easing debugging.
-}

{-# LANGUAGE AllowAmbiguousTypes #-}

module Binrep.Type.AsciiNat where

import Binrep
import Binrep.Util ( natVal'' )

import Data.Word ( Word8 )
import Data.List.NonEmpty ( NonEmpty( (:|) ) )
import Mason.Builder qualified as Mason
import Data.ByteString qualified as B
import Data.Semigroup ( sconcat )

import GHC.TypeNats ( Natural, KnownNat )
import GHC.Num.Natural ( naturalSizeInBase#, naturalToWord# )

import GHC.Generics ( Generic )
import Data.Data ( Data )
import Numeric ( showOct, showHex, showBin, showInt )

import FlatParse.Basic qualified as FP

-- | A 'Natural' represented in binary as an ASCII string, where each character
--   a is a digit in the given base (> 1).
--
-- 'Show' instances display the stored number in the given base. If the base has
-- a common prefix (e.g. @0x@ for hex), it is used.
newtype AsciiNat (base :: Natural) = AsciiNat { getAsciiNat :: Natural }
    deriving stock (Generic, Data)
    deriving (Eq, Ord) via Natural

instance Show (AsciiNat 2)  where showsPrec _ n = showString "0b" . showBin (getAsciiNat n)
instance Show (AsciiNat 8)  where showsPrec _ n = showString "0o" . showOct (getAsciiNat n)
instance Show (AsciiNat 10) where showsPrec _ n = showInt (getAsciiNat n)
instance Show (AsciiNat 16) where showsPrec _ n = showString "0x" . showHex (getAsciiNat n)

-- | Compare two 'AsciiNat's with arbitrary bases.
asciiNatCompare :: AsciiNat b1 -> AsciiNat b2 -> Ordering
asciiNatCompare (AsciiNat n1) (AsciiNat n2) = compare n1 n2

-- | The bytelength of an 'AsciiNat' is the number of digits in the number in
--   the given base. We can calculate this generically with great efficiency
--   using GHC primitives.
instance KnownNat base => BLen (AsciiNat base) where
    blen (AsciiNat n) = wordToBLen# (naturalSizeInBase# (naturalToWord# base) n)
      where base = natVal'' @base

--------------------------------------------------------------------------------

instance Put (AsciiNat 8) where
    put = natToAsciiBytes (+ 0x30) 8 . getAsciiNat

instance Get (AsciiNat 8) where
    get = do
        bs <- get
        case asciiBytesToNat octalFromAsciiDigit 8 bs of
          Left bs' -> FP.err $ "TODO " <> show bs'
          Right n  -> return $ AsciiNat n

octalFromAsciiDigit :: Word8 -> Maybe Word8
octalFromAsciiDigit = \case
  0x30 -> Just 0
  0x31 -> Just 1
  0x32 -> Just 2
  0x33 -> Just 3
  0x34 -> Just 4
  0x35 -> Just 5
  0x36 -> Just 6
  0x37 -> Just 7
  _    -> Nothing

--------------------------------------------------------------------------------

natToAsciiBytes :: (Word8 -> Word8) -> Natural -> Natural -> Builder
natToAsciiBytes f base =
    sconcat . fmap (\w -> Mason.word8 w) . fmap f . digits @Word8 base

asciiBytesToNat :: (Word8 -> Maybe Word8) -> Natural -> B.ByteString -> Either Word8 Natural
asciiBytesToNat f base bs =
    case B.foldr go (Right (0, 0)) bs of
      Left w -> Left w
      Right (n, _) -> Right n
  where
    go :: Word8 -> Either Word8 (Natural, Natural) -> Either Word8 (Natural, Natural)
    go _ (Left w) = Left w
    go w (Right (n, expo)) =
        case f w of
          Nothing -> Left w
          Just d  -> Right (n + fromIntegral d * base^expo, expo+1)

digits :: forall b a. (Integral a, Integral b) => a -> a -> NonEmpty b
digits base = go []
  where
    go s x = loop (head' :| s) tail'
      where
        head' = fromIntegral (x `mod` base)
        tail' = x `div` base
    loop s@(r :| rs) = \case
        0 -> s
        x -> go (r : rs) x