packages feed

scrod-0.2026.2.21: source/library/Scrod/Json/Number.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskellQuotes #-}

module Scrod.Json.Number where

import qualified Data.ByteString.Builder as Builder
import qualified Scrod.Decimal as Decimal
import qualified Scrod.Extra.Builder as Builder
import qualified Scrod.Extra.Parsec as Parsec
import qualified Scrod.Extra.Read as Read
import qualified Scrod.Spec as Spec
import qualified Text.Parsec as Parsec

newtype Number = MkNumber
  { unwrap :: Decimal.Decimal
  }
  deriving (Eq, Ord, Show)

decode :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Number
decode = do
  sign <- decodeSign
  intPart <- decodeInt
  (fracPart, fracExp) <- decodeFrac
  expPart <- decodeExp
  pure . MkNumber $
    Decimal.mkDecimal
      (sign $ intPart * (10 ^ abs fracExp) + fracPart)
      (fracExp + expPart)

decodeSign :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m (Integer -> Integer)
decodeSign = Parsec.option id (negate <$ Parsec.char '-')

decodeInt :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Integer
decodeInt =
  Parsec.choice
    [ 0 <$ Parsec.char '0' <* Parsec.notFollowedBy Parsec.digit,
      do
        first <- Parsec.satisfy $ \c -> c >= '1' && c <= '9'
        rest <- Parsec.many Parsec.digit
        maybe (fail "invalid integer") pure . Read.readM $ first : rest
    ]

decodeFrac :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m (Integer, Integer)
decodeFrac = Parsec.option (0, 0) $ do
  digits <- Parsec.char '.' *> Parsec.many1 Parsec.digit
  fracValue <- maybe (fail "invalid fraction") pure $ Read.readM digits
  pure (fracValue, negate . toInteger $ length digits)

decodeExp :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Integer
decodeExp = Parsec.option 0 $ do
  _ <- Parsec.oneOf "eE"
  expSign <-
    Parsec.option id $
      Parsec.choice
        [ id <$ Parsec.char '+',
          negate <$ Parsec.char '-'
        ]
  expDigits <- Parsec.many1 Parsec.digit
  maybe (fail "invalid exponent") (pure . expSign) $ Read.readM expDigits

encode :: Number -> Builder.Builder
encode n =
  let d = unwrap n
   in Builder.integerDec (Decimal.mantissa d)
        <> if Decimal.exponent d == 0
          then mempty
          else Builder.charUtf8 'e' <> Builder.integerDec (Decimal.exponent d)

spec :: (Applicative m, Monad n) => Spec.Spec m n -> n ()
spec s = do
  Spec.named s 'decode $ do
    Spec.it s "parses zero" $ do
      Spec.assertEq s (Parsec.parseString decode "0") . Just . MkNumber $ Decimal.mkDecimal 0 0

    Spec.it s "parses positive integer" $ do
      Spec.assertEq s (Parsec.parseString decode "123") . Just . MkNumber $ Decimal.mkDecimal 123 0

    Spec.it s "parses negative integer" $ do
      Spec.assertEq s (Parsec.parseString decode "-123") . Just . MkNumber $ Decimal.mkDecimal (-123) 0

    Spec.it s "parses decimal with fraction" $ do
      Spec.assertEq s (Parsec.parseString decode "123.45") . Just . MkNumber $ Decimal.mkDecimal 12345 (-2)

    Spec.it s "parses negative decimal with fraction" $ do
      Spec.assertEq s (Parsec.parseString decode "-123.45") . Just . MkNumber $ Decimal.mkDecimal (-12345) (-2)

    Spec.it s "parses with positive exponent" $ do
      Spec.assertEq s (Parsec.parseString decode "123e2") . Just . MkNumber $ Decimal.mkDecimal 123 2

    Spec.it s "parses with negative exponent" $ do
      Spec.assertEq s (Parsec.parseString decode "123e-2") . Just . MkNumber $ Decimal.mkDecimal 123 (-2)

    Spec.it s "parses with uppercase E" $ do
      Spec.assertEq s (Parsec.parseString decode "123E2") . Just . MkNumber $ Decimal.mkDecimal 123 2

    Spec.it s "parses with explicit plus in exponent" $ do
      Spec.assertEq s (Parsec.parseString decode "123e+2") . Just . MkNumber $ Decimal.mkDecimal 123 2

    Spec.it s "parses fraction with exponent" $ do
      Spec.assertEq s (Parsec.parseString decode "1.23e5") . Just . MkNumber $ Decimal.mkDecimal 123 3

    Spec.it s "fails with leading zero" $ do
      Spec.assertEq s (Parsec.parseString decode "01") Nothing

    Spec.it s "fails with just minus" $ do
      Spec.assertEq s (Parsec.parseString decode "-") Nothing

    Spec.it s "fails with trailing fraction" $ do
      Spec.assertEq s (Parsec.parseString decode "123.") Nothing

    Spec.it s "fails with leading fraction" $ do
      Spec.assertEq s (Parsec.parseString decode ".123") Nothing

    Spec.it s "fails with trailing exponent" $ do
      Spec.assertEq s (Parsec.parseString decode "123e") Nothing

    Spec.it s "fails with leading exponent" $ do
      Spec.assertEq s (Parsec.parseString decode "e123") Nothing

  Spec.named s 'encode $ do
    Spec.it s "encodes zero" $ do
      Spec.assertEq s (Builder.toString . encode . MkNumber $ Decimal.mkDecimal 0 0) "0"

    Spec.it s "encodes positive integer" $ do
      Spec.assertEq s (Builder.toString . encode . MkNumber $ Decimal.mkDecimal 123 0) "123"

    Spec.it s "encodes negative integer" $ do
      Spec.assertEq s (Builder.toString . encode . MkNumber $ Decimal.mkDecimal (-123) 0) "-123"

    Spec.it s "encodes with positive exponent" $ do
      Spec.assertEq s (Builder.toString . encode . MkNumber $ Decimal.mkDecimal 123 2) "123e2"

    Spec.it s "encodes with negative exponent" $ do
      Spec.assertEq s (Builder.toString . encode . MkNumber $ Decimal.mkDecimal 12345 (-2)) "12345e-2"

    Spec.it s "encodes small fraction" $ do
      Spec.assertEq s (Builder.toString . encode . MkNumber $ Decimal.mkDecimal 123 (-5)) "123e-5"