packages feed

wireform-parquet-0.1.0.0: src/Parquet/Decimal.hs

{-# LANGUAGE BangPatterns #-}

-- | Signed big-endian two's-complement operations for decimal storage.
module Parquet.Decimal (
  compareDecimalBytes,
  decodeDecimalInteger,
  encodeDecimalInteger,
  decimalPrecisionFits,
) where

import Data.Bits (shiftR, testBit, (.&.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Unsafe qualified as BSU
import Data.Int (Int32)


{- | Compare represented integers, including different widths and redundant
sign extension. Offsets stay unboxed after inlining; no slices or Integer
values are allocated by the comparison loop.
-}
compareDecimalBytes :: ByteString -> ByteString -> Ordering
compareDecimalBytes a b
  | BS.null a || BS.null b = compare (BS.length a) (BS.length b)
  | negativeA /= negativeB = if negativeA then LT else GT
  | widthA /= widthB = if negativeA then compare widthB widthA else compare widthA widthB
  | otherwise = bytes 0
  where
    negativeA = testBit (BSU.unsafeIndex a 0) 7
    negativeB = testBit (BSU.unsafeIndex b 0) 7
    startA = significantStart a
    startB = significantStart b
    widthA = BS.length a - startA
    widthB = BS.length b - startB
    bytes !i
      | i == widthA = EQ
      | otherwise = case compare (BSU.unsafeIndex a (startA + i)) (BSU.unsafeIndex b (startB + i)) of
          EQ -> bytes (i + 1)
          result -> result
{-# INLINE compareDecimalBytes #-}


significantStart :: ByteString -> Int
significantStart bytes = go 0
  where
    go !i
      | i >= BS.length bytes - 1 = i
      | otherwise =
          let byte = BSU.unsafeIndex bytes i
              nextNegative = testBit (BSU.unsafeIndex bytes (i + 1)) 7
          in if (byte == 0 && not nextNegative) || (byte == 255 && nextNegative)
               then go (i + 1)
               else i
{-# INLINE significantStart #-}


-- | Metadata-only conversion; the per-value writer uses compareDecimalBytes.
decodeDecimalInteger :: ByteString -> Maybe Integer
decodeDecimalInteger bytes
  | BS.null bytes = Nothing
  | otherwise =
      Just
        ( BS.foldl'
            (\n byte -> n * 256 + fromIntegral byte)
            (if testBit (BSU.unsafeIndex bytes 0) 7 then -1 else 0)
            bytes
        )


encodeDecimalInteger :: Integer -> ByteString
encodeDecimalInteger = BS.reverse . BS.unfoldr step . Just
  where
    step Nothing = Nothing
    step (Just n) =
      let byte = fromIntegral (n .&. 255)
          rest = n `shiftR` 8
          done = (rest == 0 && not (testBit byte 7)) || (rest == -1 && testBit byte 7)
      in Just (byte, if done then Nothing else Just rest)


{- | Whether a positive DECIMAL precision fits signed, fixed-width storage.
All positive Int32 widths/precisions are handled without floating point or
large powers derived from the metadata. BYTE_ARRAY has no such bound.

The capacity is floor((8*width-1)*log10(2)). The rational below is a lower
bound on log10(2), less than 10^-50 below its upper bound. Its floor is exact
for every bit count <=17179869175: the whole log interval is strictly between
adjacent Farey fractions 5107137094/16965542197 and 579001193/1923400330.
Their determinant is 1 and denominator sum 18888942527 exceeds that bit limit;
no rational k/n in this interval can have an admitted denominator n.

scripts/verify_parquet_decimal_capacity.py proves both the logarithm bounds
with exact rational series and the Farey certificate against these constants.
This is metadata validation, outside the per-value comparison path.
-}
decimalPrecisionFits :: Int32 -> Int32 -> Bool
decimalPrecisionFits precision width
  | precision <= 0 || width <= 0 = False
  | otherwise =
      let bits = 8 * toInteger width - 1
          capacity = (bits * log10TwoLowerNumerator) `quot` log10TwoDenominator
      in toInteger precision <= capacity


log10TwoLowerNumerator :: Integer
log10TwoLowerNumerator = 30102999566398119521373889472449302676818988146210


log10TwoDenominator :: Integer
log10TwoDenominator = 100000000000000000000000000000000000000000000000000