packages feed

scientific-notation 0.1.3.0 → 0.1.4.0

raw patch · 4 files changed

+337/−16 lines, 4 filesdep +text-shortdep ~byteslicedep ~bytestringdep ~primitive

Dependencies added: text-short

Dependency ranges changed: byteslice, bytestring, primitive

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Revision history for scientific-notation +## 0.1.4.0 -- 2022-03-16++* Add `fromWord8`, `fromWord16`, and `fromWord32`.+* Add `roundShiftedToInt64`.+* Add `encode`.+* Change `builderUtf8` to present numbers in decimal notation without exponent+  in many common cases. This is not considered a breaking change.+ ## 0.1.3.0 -- 2021-02-23  * Add `greaterThanInt64`.
scientific-notation.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: scientific-notation-version: 0.1.3.0+version: 0.1.4.0 synopsis: Scientific notation intended for tokenization description:   This library provides a type used to represent a number in@@ -51,7 +51,11 @@     , base >=4.12 && <5     , bytebuild >=0.3.5 && <0.4     , bytesmith >=0.3 && <0.4+    , byteslice >=0.2.6 && <0.3     , natural-arithmetic >=0.1.1 && <0.2+    , text-short >=0.1.3+    , primitive >=0.7.1+    , bytestring >=0.10.12   hs-source-dirs: src   ghc-options: -O2 -Wall   default-language: Haskell2010
src/Data/Number/Scientific.hs view
@@ -1,5 +1,7 @@ {-# language BangPatterns #-}+{-# language DuplicateRecordFields #-} {-# language LambdaCase #-}+{-# language NumericUnderscores #-} {-# language TypeApplications #-} {-# language MultiWayIf #-} {-# language MagicHash #-}@@ -12,6 +14,9 @@   , small   , large   , fromFixed+  , fromWord8+  , fromWord16+  , fromWord32   , fromWord64     -- * Consume   , toWord@@ -23,6 +28,8 @@   , toInt32   , toInt64   , withExposed+    -- * Scale and Consume+  , roundShiftedToInt64     -- * Compare   , greaterThanInt64     -- * Decode@@ -37,25 +44,37 @@   , parserNegatedUtf8Bytes#   , parserNegatedTrailingUtf8Bytes#     -- * Encode+  , encode   , builderUtf8   ) where  import Prelude hiding (negate) +import Control.Monad.ST (runST) import GHC.Exts (Int#,Word#,Int(I#),(+#)) import GHC.Word (Word(W#),Word8(W8#),Word16(W16#),Word32(W32#),Word64(W64#)) import GHC.Int (Int64(I64#),Int32(I32#)) import Data.Bytes.Builder (Builder) import Data.Bytes.Parser.Unsafe (Parser(..)) import Data.Fixed (Fixed(MkFixed),HasResolution)+import Data.Primitive (ByteArray(ByteArray))+import Data.Text.Short (ShortText)+import Data.ByteString.Short.Internal (ShortByteString(SBS))+import Data.Bytes.Types (Bytes(Bytes))  import qualified Arithmetic.Nat as Nat import qualified Data.Fixed as Fixed+import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Types as BT import qualified Data.Bytes.Builder as Builder import qualified Data.Bytes.Builder.Bounded as BB+import qualified Data.Bytes.Builder.Bounded.Unsafe as BBU+import qualified Data.Bytes.Chunks as Chunks import qualified Data.Bytes.Parser as Parser import qualified Data.Bytes.Parser.Latin as Latin import qualified Data.Bytes.Parser.Unsafe as Unsafe+import qualified Data.Primitive as PM+import qualified Data.Text.Short.Unsafe as TS import qualified GHC.Exts as Exts import qualified Prelude as Prelude @@ -193,6 +212,23 @@   (# (# #) | #) -> Nothing   (# | i #) -> Just (I64# i) +-- | This works even if the number has a fractional component. For example:+--+-- >>> roundShiftedToInt64 2 (fromFixed @E3 1.037)+-- 103+--+-- The shift amount should be a small constant between -100 and 100.+-- The behavior of a shift outside this range is undefined.+roundShiftedToInt64 ::+     Int -- ^ Exponent @e@, @n@ is multiplied by @10^e@ before rounding+  -> Scientific -- ^ Number @n@+  -> Maybe Int64+{-# inline roundShiftedToInt64 #-}+roundShiftedToInt64 (I# adj) (Scientific (I# coeff) (I# e) largeNum) =+  case roundToInt# coeff e adj largeNum of+     (# (# #) | #) -> Nothing+     (# | i #) -> Just (I64# i)+ -- | Convert a 64-bit unsigned word to a 'Scientific'. fromWord64 :: Word64 -> Scientific fromWord64 !w = if w <= 9223372036854775807@@ -201,6 +237,21 @@     let !b = LargeScientific (fromIntegral w) 0      in Scientific 0 minBound b +-- | Convert an 8-bit unsigned word to a 'Scientific'.+fromWord8 :: Word8 -> Scientific+{-# inline fromWord8 #-}+fromWord8 !w = Scientific (fromIntegral w) 0 zeroLarge++-- | Convert a 16-bit unsigned word to a 'Scientific'.+fromWord16 :: Word16 -> Scientific+{-# inline fromWord16 #-}+fromWord16 !w = Scientific (fromIntegral w) 0 zeroLarge++-- | Convert a 32-bit unsigned word to a 'Scientific'.+fromWord32 :: Word32 -> Scientific+{-# inline fromWord32 #-}+fromWord32 !w = Scientific (fromIntegral w) 0 zeroLarge+ -- | Is the number represented in scientific notation greater than the -- 64-bit integer argument? greaterThanInt64 :: Scientific -> Int64 -> Bool@@ -330,6 +381,24 @@   toSmallIntHelper smallToInt largeToInt     coefficient0# exponent0# largeNum +roundToInt# :: Int# -> Int# -> Int# -> LargeScientific -> (# (# #) | Int# #)+{-# noinline roundToInt# #-}+roundToInt# coefficient0# exponent0# adjustment0# largeNum =+  if exponent0 /= minBound+    then+      if | coefficient0 == 0 -> (# | 0# #)+         | exponent0 > (maxBound - 200) -> (# (# #) | #)+         | exponent0 < (minBound + 200) -> (# (# #) | #)+         | adjustment0 > 100 -> (# (# #) | #)+         | adjustment0 < (-100) -> (# (# #) | #)+         | otherwise ->+             roundSmallToInt coefficient0 (I# (exponent0# +# adjustment0#))+    else roundLargeToInt adjustment0 largeNum+  where+  coefficient0 = I# coefficient0#+  exponent0 = I# exponent0#+  adjustment0 = I# adjustment0#+ -- Arguments are non-normalized coefficient and exponent. -- We cannot use the same trick that we use for Word8 and -- Word16.@@ -376,6 +445,24 @@         else negIntExp10 coefficient expon   | otherwise = (# (# #) | #) +-- Arguments are non-normalized coefficient and exponent.+-- This is similar to smallToInt except that we round numbers with fractional+-- parts. And by round, I actually mean truncate. Fractional parts only show+-- up when the exponent is negative.+roundSmallToInt :: Int -> Int -> (# (# #) | Int# #)+roundSmallToInt !coefficient0 !exponent0+  | coefficient0 == 0 = (# | 0# #)+  | (coefficient@(I# coefficient# ),expon) <- incrementNegativeExp coefficient0 exponent0+  , expon < 30 = case compare expon 0 of+      EQ -> (# | coefficient# #)+      GT -> if coefficient >= 0+        then posIntExp10 coefficient expon+        else negIntExp10 coefficient expon+      LT -> if coefficient >= 0+        then (# | roundPosIntNegExp10 coefficient expon #)+        else (# | roundNegIntNegExp10 coefficient expon #)+  | otherwise = (# (# #) | #)+ -- Arguments are non-normalized coefficient and exponent -- With Word16, we can do a neat little trick where we -- cap the coefficient at 65536 and the exponent at 5. This@@ -474,6 +561,33 @@         else negIntExp10 (fromIntegral @Integer @Int coefficient) (fromIntegral @Integer @Int expon)   | otherwise = (# (# #) | #) +-- Arguments are non-normalized, this targets the native word size+roundLargeToInt :: Int -> LargeScientific -> (# (# #) | Int# #)+roundLargeToInt !adj (LargeScientific coefficient0 exponent0)+  | coefficient0 == 0 = (# | 0# #)+  | (coefficient,expon) <- largeIncrementNegativeExp coefficient0 exponent1+  , expon < 30+    = case compare expon 0 of+        EQ -> case fromIntegral @Integer @Int coefficient of+          I# r -> (# | r #)+        GT ->+          if coefficient >= (fromIntegral @Int @Integer minBound) && coefficient <= (fromIntegral @Int @Integer maxBound)+            then if coefficient >= 0+              then posIntExp10 (fromIntegral @Integer @Int coefficient) (fromIntegral @Integer @Int expon)+              else negIntExp10 (fromIntegral @Integer @Int coefficient) (fromIntegral @Integer @Int expon)+            else (# (# #) | #)+        LT -> if expon < (-100_000_000_000)+          then -- Due to the realities of hardward, a negative exponent with high+               -- magnitude is guaranteed to produce a zero result. A coefficient+               -- large enough to resist the zero result would consume all memory.+               (# | 0# #)+          else if coefficient >= 0+            then roundPosIntegerNegExp10 coefficient (fromInteger expon)+            else roundNegIntegerNegExp10 coefficient (fromInteger expon)+  | otherwise = (# (# #) | #)+  where+  exponent1 = exponent0 + toInteger adj+ -- Precondition: the exponent is non-negative. This returns -- an unboxed Nothing on overflow. This implementation should -- work even on a 32-bit platform.@@ -533,6 +647,27 @@         else (# (# #) | #)     else (# (# #) | #) +-- Precondition: The exponent is non-positive, and the+-- coefficient is non-negative. This returns an unboxed+-- Nothing on overflow.+roundPosIntNegExp10 :: Int -> Int -> Int#+roundPosIntNegExp10 !a@(I# a# ) !e = case e of+  0 -> a#+  _ -> roundPosIntNegExp10 (quot a 10) (e + 1)++-- Precondition: The exponent is non-positive, and the+-- coefficient is non-negative. This returns an unboxed+-- Nothing on overflow.+roundPosIntegerNegExp10 :: Integer -> Int -> (# (# #) | Int# #)+roundPosIntegerNegExp10 !a !e = case e of+  0 -> if a > fromIntegral @Int @Integer maxBound+    then (# (# #) | #)+    else case fromInteger a of+      I# a# -> (# | a# #)+  _ -> case a of+    0 -> (# | 0# #)+    _ -> roundPosIntegerNegExp10 (quot a 10) (e + 1)+ -- Precondition: The exponent is non-negative, and the -- coefficient is non-positive. This returns an unboxed -- Nothing on overflow.@@ -546,6 +681,27 @@         else (# (# #) | #)     else (# (# #) | #) +-- Precondition: The exponent is non-position, and the+-- coefficient is non-positive. This returns an unboxed+-- Nothing on overflow.+roundNegIntNegExp10 :: Int -> Int -> Int#+roundNegIntNegExp10 !a@(I# a# ) !e = case e of+  0 -> a#+  _ -> roundNegIntNegExp10 (quot a 10) (e + 1)++-- Precondition: The exponent is non-position, and the+-- coefficient is non-positive. This returns an unboxed+-- Nothing on overflow.+roundNegIntegerNegExp10 :: Integer -> Int -> (# (# #) | Int# #)+roundNegIntegerNegExp10 !a !e = case e of+  0 -> if a > fromIntegral @Int @Integer maxBound+    then (# (# #) | #)+    else case fromInteger a of+      I# a# -> (# | a# #)+  _ -> case a of+    0 -> (# | 0# #)+    _ -> roundNegIntegerNegExp10 (quot a 10) (e + 1)+ -- What are these lower and upper bounds? The problem that -- we are trying to solve is that overflow is tricky to detect -- when we multiply by ten. By putting an upper (or lower)@@ -610,6 +766,7 @@  -- If the exponent is negative, increase it as long as the -- coefficient divides ten evenly.+-- This only ever causes the coefficient to decrease, never increase. incrementNegativeExp# :: Int# -> Int# -> (# Int#, Int# #) {-# noinline incrementNegativeExp# #-} incrementNegativeExp# w# e# = if I# e# >= 0@@ -937,21 +1094,102 @@         runParser (g y) (# arr, b, c #) s1   ) +-- | Encode a number as text. If the exponent is between -50 and +50 (exclusive),+-- this represents the number without any exponent. For example:+--+-- >>> encode (small 87654321 (-3))+-- "87654.321"+-- >>> encode (small 5000 (-3))+-- "-5000"+--+-- The decision of when to use an exponent is not considered stable part of+-- this library\'s API. Check the test suite for examples of what to expect,+-- and feel free to open an issue or contribute if the output of this function+-- is unsightly in certain situations.+encode :: Scientific -> ShortText+encode s = case Chunks.concatU (Builder.run 128 (builderUtf8 s)) of+  ByteArray x -> TS.fromShortByteStringUnsafe (SBS x)++-- | Variant of 'encode' that provides a builder instead. builderUtf8 :: Scientific -> Builder builderUtf8 (Scientific coeff e big)   | e == 0 = Builder.intDec coeff   | e == minBound = let LargeScientific coeff' e' = big in-      case e' of-        0 -> Builder.integerDec coeff'-        _ -> -          Builder.integerDec coeff'-          <>-          Builder.ascii 'e'-          <>-          Builder.integerDec e'-  | otherwise = Builder.fromBounded Nat.constant $-      BB.intDec coeff-      `BB.append`-      BB.ascii 'e'-      `BB.append`-      BB.intDec e+      if | coeff' == 0 -> Builder.ascii '0'+         | e' == 0 -> Builder.integerDec coeff'+         | e' > 0 && e' < 50 ->+             -- TODO: Add a replicate function to builder to improve this.+             Builder.integerDec coeff' <> Builder.bytes (Bytes.replicate (fromInteger e') 0x30)+         | e' < 0, e' > (-50), coeff' > 0, coeff' < 18446744073709551616 ->+             let coeff'' = fromInteger coeff' :: Word+                 e'' = fromInteger e' :: Int+              in Builder.bytes (encodePosCoeffNegExp coeff'' e'')+         | e' < 0, e' > (-50), coeff' < 0, coeff' > (-18446744073709551616) ->+             let coeff'' = fromInteger (Prelude.negate coeff') :: Word+                 e'' = fromInteger e' :: Int+              in Builder.bytes (encodeNegCoeffNegExp coeff'' e'')+         | otherwise ->+             Builder.integerDec coeff'+             <>+             Builder.ascii 'e'+             <>+             Builder.integerDec e'+  | otherwise =+      if | coeff == 0 -> Builder.ascii '0'+         | e > 0 && e < 50 ->+             -- TODO: Add a replicate function to builder to improve this.+             Builder.intDec coeff <> Builder.bytes (Bytes.replicate e 0x30)+         | e < 0 && e > (-50) -> if coeff > 0+             then Builder.bytes (encodePosCoeffNegExp (fromIntegral @Int @Word coeff) e)+             else Builder.bytes (encodeNegCoeffNegExp (fromIntegral @Int @Word (Prelude.negate coeff)) e)+         | otherwise -> Builder.fromBounded Nat.constant $+             BB.intDec coeff+             `BB.append`+             BB.ascii 'e'+             `BB.append`+             BB.intDec e++-- Precondition: exponent is negative.+-- This is convoluted, so if a reader of this code thinks of a better+-- way to do this, feel free to PR a more simple replacement. +encodePosCoeffNegExp :: Word -> Int -> Bytes+encodePosCoeffNegExp !w !e = runST $ do+  dst <- PM.newByteArray 128+  PM.setByteArray dst 0 128 (0x30 :: Word8)+  end <- BBU.pasteST (BB.wordDec w) dst 100+  let dotIx = end + e+  let coeffMag = end - 100+  let extra = if coeffMag > Prelude.negate e+        then (coeffMag - Prelude.negate e) - 1+        else 0+  PM.moveByteArray dst 0 dst 1 dotIx+  PM.writeByteArray dst (dotIx - 1) (0x2E :: Word8)+  dst' <- PM.unsafeFreezeByteArray dst+  pure Bytes+    { BT.array=dst'+    , BT.offset=dotIx - 2 - extra+    , BT.length=Prelude.negate e + 2 + extra+    }++-- Precondition: exponent is negative.+-- This is convoluted, so if a reader of this code thinks of a better+-- way to do this, feel free to PR a more simple replacement. +encodeNegCoeffNegExp :: Word -> Int -> Bytes+encodeNegCoeffNegExp !w !e = runST $ do+  dst <- PM.newByteArray 128+  PM.setByteArray dst 0 128 (0x30 :: Word8)+  end <- BBU.pasteST (BB.wordDec w) dst 100+  let dotIx = end + e+  let coeffMag = end - 100+  let extra = if coeffMag > Prelude.negate e+        then (coeffMag - Prelude.negate e) - 1+        else 0+  PM.moveByteArray dst 0 dst 1 dotIx+  PM.writeByteArray dst (dotIx - 1) (0x2E :: Word8)+  PM.writeByteArray dst (dotIx - 3 - extra) (0x2D :: Word8)+  dst' <- PM.unsafeFreezeByteArray dst+  pure Bytes+    { BT.array=dst'+    , BT.offset=dotIx - 3 - extra+    , BT.length=Prelude.negate e + 3 + extra+    }
test/Main.hs view
@@ -1,4 +1,5 @@ {-# language BangPatterns #-}+{-# language NumericUnderscores #-} {-# language ScopedTypeVariables #-} {-# language TypeApplications #-} {-# language OverloadedStrings #-}@@ -11,7 +12,7 @@ import Data.Fixed (Fixed,E12) import Data.Int (Int64) import Data.Number.Scientific (large,small,toWord8,toWord16,toWord32,toWord64)-import Data.Number.Scientific (toInt64,toInt32)+import Data.Number.Scientific (toInt64,toInt32,roundShiftedToInt64) import Data.Primitive (ByteArray) import Data.Word (Word8) import Test.Tasty (defaultMain,testGroup,TestTree)@@ -127,6 +128,16 @@     , THU.testCase "T" $ Nothing @=? toInt64 (large 93 17)     , THU.testCase "U" $ Just (-9.3e17) @=? toInt64 (small (-93) 16)     , THU.testCase "V" $ Nothing @=? toInt64 (large 922337203685477581 1)+    , THU.testCase "W" $ Just 12 @=? roundShiftedToInt64 1 (small 129 (-2))+    , THU.testCase "X" $ Just (-12) @=? roundShiftedToInt64 1 (small (-129) (-2))+    , THU.testCase "Y" $ Nothing @=? roundShiftedToInt64 31 (small 129 (-2))+    , THU.testCase "Z" $ Just (1.29e18) @=? roundShiftedToInt64 18 (small 129 (-2))+    , THU.testCase "AA" $ Just 9223372 @=? roundShiftedToInt64 (-26) (large 9223372036854775817425364203 5)+    , THU.testCase "AB" $ Just (-9223372) @=? roundShiftedToInt64 (-26) (large (-9223372036854775817425364203) 5)+    , THU.testCase "AC" $ Just 0 @=? roundShiftedToInt64 0 (large (-9223372036854775817425364203) (-1_000_000_000))+    , THU.testCase "AD" $ Just 0 @=? roundShiftedToInt64 0 (large (50000000000000000000000000000) (-1_000_000_000))+    , THU.testCase "AE" $ Just 2 @=? toInt64 (small 2 0)+    , THU.testCase "AF" $ Just 2 @=? toInt64 (large 2 0)     ]   , testGroup "Compare"     [ THU.testCase "A" $ SCI.greaterThanInt64 (small 300 (-2)) 2 @=? True@@ -182,6 +193,66 @@           P.Success (P.Slice (length str + 1) 0 (large i j))           ===           P.parseBytes (SCI.parserSignedUtf8Bytes ()) (bytes str)+      ]+    ]+  , testGroup "Encode"+    [ testGroup "small"+      [ THU.testCase "A" $ "5000" @=? SCI.encode (small 5 3)+      , THU.testCase "B" $ "-5000" @=? SCI.encode (small (-5) 3)+      , THU.testCase "C" $ "0.0006" @=? SCI.encode (small 6 (-4))+      , THU.testCase "D" $ "0.087654321" @=? SCI.encode (small 87654321 (-9))+      , THU.testCase "E" $ "0.87654321" @=? SCI.encode (small 87654321 (-8))+      , THU.testCase "F" $ "8.7654321" @=? SCI.encode (small 87654321 (-7))+      , THU.testCase "G" $ "87.654321" @=? SCI.encode (small 87654321 (-6))+      , THU.testCase "H" $ "876.54321" @=? SCI.encode (small 87654321 (-5))+      , THU.testCase "I" $ "8765.4321" @=? SCI.encode (small 87654321 (-4))+      , THU.testCase "J" $ "87654.321" @=? SCI.encode (small 87654321 (-3))+      , THU.testCase "K" $ "876543.21" @=? SCI.encode (small 87654321 (-2))+      , THU.testCase "L" $ "8765432.1" @=? SCI.encode (small 87654321 (-1))+      , THU.testCase "M" $ "87654321" @=? SCI.encode (small 87654321 0)+      , THU.testCase "N" $ "876543210" @=? SCI.encode (small 87654321 1)+      , THU.testCase "O" $ "87654321.0" @=? SCI.encode (small 876543210 (-1))+      , THU.testCase "P" $ "-0.087654321" @=? SCI.encode (small (-87654321) (-9))+      , THU.testCase "Q" $ "-0.87654321" @=? SCI.encode (small (-87654321) (-8))+      , THU.testCase "R" $ "-8.7654321" @=? SCI.encode (small (-87654321) (-7))+      , THU.testCase "S" $ "-87.654321" @=? SCI.encode (small (-87654321) (-6))+      , THU.testCase "T" $ "-876.54321" @=? SCI.encode (small (-87654321) (-5))+      , THU.testCase "U" $ "-8765.4321" @=? SCI.encode (small (-87654321) (-4))+      , THU.testCase "V" $ "-87654.321" @=? SCI.encode (small (-87654321) (-3))+      , THU.testCase "W" $ "-876543.21" @=? SCI.encode (small (-87654321) (-2))+      , THU.testCase "X" $ "-8765432.1" @=? SCI.encode (small (-87654321) (-1))+      , THU.testCase "Y" $ "-87654321" @=? SCI.encode (small (-87654321) 0)+      , THU.testCase "Z" $ "-876543210" @=? SCI.encode (small (-87654321) 1)+      , THU.testCase "AA" $ "-87654321.0" @=? SCI.encode (small (-876543210) (-1))+      ]+    , testGroup "large"+      [ THU.testCase "A" $ "5000" @=? SCI.encode (large 5 3)+      , THU.testCase "B" $ "-5000" @=? SCI.encode (large (-5) 3)+      , THU.testCase "C" $ "0.0006" @=? SCI.encode (large 6 (-4))+      , THU.testCase "D" $ "0.087654321" @=? SCI.encode (large 87654321 (-9))+      , THU.testCase "E" $ "0.87654321" @=? SCI.encode (large 87654321 (-8))+      , THU.testCase "F" $ "8.7654321" @=? SCI.encode (large 87654321 (-7))+      , THU.testCase "G" $ "87.654321" @=? SCI.encode (large 87654321 (-6))+      , THU.testCase "H" $ "876.54321" @=? SCI.encode (large 87654321 (-5))+      , THU.testCase "I" $ "8765.4321" @=? SCI.encode (large 87654321 (-4))+      , THU.testCase "J" $ "87654.321" @=? SCI.encode (large 87654321 (-3))+      , THU.testCase "K" $ "876543.21" @=? SCI.encode (large 87654321 (-2))+      , THU.testCase "L" $ "8765432.1" @=? SCI.encode (large 87654321 (-1))+      , THU.testCase "M" $ "87654321" @=? SCI.encode (large 87654321 0)+      , THU.testCase "N" $ "876543210" @=? SCI.encode (large 87654321 1)+      , THU.testCase "O" $ "87654321.0" @=? SCI.encode (large 876543210 (-1))+      , THU.testCase "P" $ "-0.087654321" @=? SCI.encode (large (-87654321) (-9))+      , THU.testCase "Q" $ "-0.87654321" @=? SCI.encode (large (-87654321) (-8))+      , THU.testCase "R" $ "-8.7654321" @=? SCI.encode (large (-87654321) (-7))+      , THU.testCase "S" $ "-87.654321" @=? SCI.encode (large (-87654321) (-6))+      , THU.testCase "T" $ "-876.54321" @=? SCI.encode (large (-87654321) (-5))+      , THU.testCase "U" $ "-8765.4321" @=? SCI.encode (large (-87654321) (-4))+      , THU.testCase "V" $ "-87654.321" @=? SCI.encode (large (-87654321) (-3))+      , THU.testCase "W" $ "-876543.21" @=? SCI.encode (large (-87654321) (-2))+      , THU.testCase "X" $ "-8765432.1" @=? SCI.encode (large (-87654321) (-1))+      , THU.testCase "Y" $ "-87654321" @=? SCI.encode (large (-87654321) 0)+      , THU.testCase "Z" $ "-876543210" @=? SCI.encode (large (-87654321) 1)+      , THU.testCase "AA" $ "-87654321.0" @=? SCI.encode (large (-876543210) (-1))       ]     ]   ]