valiant-0.1.0.0: src/Valiant/Binary/Scientific.hs
{-# OPTIONS_GHC -Wno-orphans #-}
-- | Binary encode/decode for PostgreSQL @numeric@ type.
--
-- PG stores numeric in a base-10000 representation:
-- 2 bytes: ndigits (number of base-10000 digits)
-- 2 bytes: weight (exponent of first digit, in base-10000)
-- 2 bytes: sign (0x0000 = positive, 0x4000 = negative, 0xC000 = NaN)
-- 2 bytes: dscale (number of digits after decimal point)
-- ndigits * 2 bytes: base-10000 digits (0..9999 each, big-endian Int16)
module Valiant.Binary.Scientific
() where
import Data.Bits (shiftL, (.|.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as B
import Data.ByteString.Char8 qualified as BS8
import Data.ByteString.Lazy qualified as LBS
import Data.Int (Int16)
import Data.Scientific (Scientific)
import Data.Scientific qualified as Sci
import PgWire.Binary.Types (PgDecode (..), PgEncode (..))
import PgWire.Protocol.Oid (oidNumeric)
-- Constants
signPositive, signNegative, signNaN :: Int16
signPositive = 0x0000
signNegative = 0x4000
signNaN = fromIntegral (0xC000 :: Int)
-- Encoding ------------------------------------------------------------------
instance PgEncode Scientific where
pgEncode = encodeNumeric
pgOid _ = oidNumeric
encodeNumeric :: Scientific -> ByteString
encodeNumeric sci
| sci == 0 =
LBS.toStrict . B.toLazyByteString $
int16 0 <> int16 0 <> int16 signPositive <> int16 0
| otherwise =
let (isNeg, absSci) = if sci < 0 then (True, negate sci) else (False, sci)
!sign = if isNeg then signNegative else signPositive
c = Sci.coefficient absSci
e = Sci.base10Exponent absSci
-- Digit bytes of the absolute coefficient
!coeffBs = BS8.pack (show (abs c))
!coeffLen = BS.length coeffBs
!digitsBeforeDecimal = max 0 (coeffLen + e)
!digitsAfterDecimal = max 0 (negate e)
-- Number of base-10000 groups
!groupsBefore = (digitsBeforeDecimal + 3) `div` 4
!padBefore = groupsBefore * 4 - digitsBeforeDecimal
!groupsAfter = (digitsAfterDecimal + 3) `div` 4
!padAfter = groupsAfter * 4 - digitsAfterDecimal
-- Build padded digit bytes: [padBefore zeros] [intPart] [fracPart] [padAfter zeros]
(!intPart, !fracPart)
| e >= 0 = (coeffBs <> BS.replicate e 0x30, BS.empty) -- 0x30 = '0'
| otherwise =
let splitPos = coeffLen + e
in if splitPos > 0
then (BS.take splitPos coeffBs, BS.drop splitPos coeffBs)
else (BS.empty, BS.replicate (negate splitPos) 0x30 <> coeffBs)
!allDigits = BS.replicate padBefore 0x30 <> intPart <> fracPart <> BS.replicate padAfter 0x30
-- Convert to base-10000 groups
!groups = toBase10000Groups allDigits
-- Strip trailing zero groups
!groups' = reverse (dropWhile (== 0) (reverse groups))
!ndigits' = length groups'
!weight = fromIntegral (groupsBefore - 1) :: Int16
!dscale = fromIntegral digitsAfterDecimal :: Int16
in LBS.toStrict . B.toLazyByteString $
int16 (fromIntegral ndigits')
<> int16 weight
<> int16 sign
<> int16 dscale
<> mconcat (map (int16 . fromIntegral) groups')
-- | Convert a ByteString of ASCII digit bytes to base-10000 groups.
-- Processes 4 bytes at a time.
toBase10000Groups :: ByteString -> [Int]
toBase10000Groups bs
| BS.null bs = []
| otherwise =
let (!chunk, !rest) = BS.splitAt 4 bs
!val = BS.foldl' (\acc w -> acc * 10 + fromIntegral (w - 0x30)) 0 chunk
in val : toBase10000Groups rest
int16 :: Int16 -> B.Builder
int16 = B.int16BE
{-# INLINE int16 #-}
-- Decoding ------------------------------------------------------------------
instance PgDecode Scientific where
pgDecode = decodeNumeric
decodeNumeric :: ByteString -> Either String Scientific
decodeNumeric bs
| BS.length bs < 8 = Left "numeric: header too short (need 8 bytes)"
| otherwise = do
ndigits <- getInt16 bs 0
weight <- getInt16 bs 2
sign <- getInt16 bs 4
_dscale <- getInt16 bs 6
let expectedLen = 8 + fromIntegral ndigits * 2
if BS.length bs < expectedLen
then Left $ "numeric: payload too short (need " <> show expectedLen <> " bytes)"
else do
when (sign == signNaN) $ Left "numeric: NaN not representable as Scientific"
digits <- mapM (\i -> getInt16 bs (8 + i * 2)) [0 .. fromIntegral ndigits - 1]
let !isNeg = sign == signNegative
!intVal = foldlStrict (\acc d -> acc * 10000 + fromIntegral d) (0 :: Integer) digits
!expo = 4 * (fromIntegral weight - fromIntegral ndigits + 1)
!sci = Sci.scientific (if isNeg then negate intVal else intVal) expo
Right sci
-- | Strict left fold over a list (avoids importing Data.List just for foldl').
foldlStrict :: (b -> a -> b) -> b -> [a] -> b
foldlStrict _ !z [] = z
foldlStrict f !z (x : xs) = foldlStrict f (f z x) xs
{-# INLINE foldlStrict #-}
getInt16 :: ByteString -> Int -> Either String Int16
getInt16 bs off
| off + 2 > BS.length bs = Left "numeric: getInt16 out of bounds"
| otherwise =
let hi = fromIntegral (BS.index bs off) :: Int16
lo = fromIntegral (BS.index bs (off + 1)) :: Int16
in Right (hi `shiftL` 8 .|. lo)
{-# INLINE getInt16 #-}
when :: Bool -> Either String () -> Either String ()
when True e = e
when False _ = Right ()
{-# INLINE when #-}