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 +8/−0
- scientific-notation.cabal +5/−1
- src/Data/Number/Scientific.hs +252/−14
- test/Main.hs +72/−1
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)) ] ] ]