packages feed

slackblocks-3.1.0: src/Slackblocks/Internal/Decimal.hs

-- | Compare finite decimal strings without expanding their exponents.
module Slackblocks.Internal.Decimal (decimalParts, compareDecimal) where

import Data.Char (isAscii, isDigit)
import Data.Text (Text)
import Data.Text qualified as T
import Text.Read (readMaybe)

asciiDigits :: Text -> Bool
asciiDigits text = not (T.null text) && T.all (\c -> isAscii c && isDigit c) text

unsigned :: Text -> Text
unsigned value = case T.uncons value of
  Just (c, rest) | c == '+' || c == '-' -> rest
  _ -> value

signedInteger :: Text -> Maybe Integer
signedInteger text
  | asciiDigits (unsigned text) = readMaybe (T.unpack (if T.isPrefixOf "+" text then T.drop 1 text else text))
  | otherwise = Nothing

decimalParts :: Text -> Maybe (Int, Text, Integer)
decimalParts value = do
  (mantissa, exponentValue) <- case T.split (\c -> c == 'e' || c == 'E') value of
    [m] -> Just (m, 0)
    [m, e] -> (m,) <$> signedInteger e
    _ -> Nothing
  let pieces = T.splitOn "." (unsigned mantissa)
  fraction <- case pieces of
    [_] -> Just 0
    [_, f] -> Just (T.length f)
    _ -> Nothing
  let combined = T.concat pieces
  if not (asciiDigits combined)
    then Nothing
    else do
      let digits = T.dropWhile (== '0') combined
          sign
            | T.null digits = 0
            | T.isPrefixOf "-" mantissa = -1
            | otherwise = 1
          rank = exponentValue + fromIntegral (T.length digits) - fromIntegral fraction
      pure (sign, digits, rank)

compareDecimal :: Text -> Text -> Maybe Ordering
compareDecimal left right = do
  (a, x, rankX) <- decimalParts left
  (b, y, rankY) <- decimalParts right
  let width = max (T.length x) (T.length y)
      magnitude = compare rankX rankY <> compare (T.justifyLeft width '0' x) (T.justifyLeft width '0' y)
  pure $ if a /= b then compare a b else if a == 0 then EQ else if a < 0 then invert magnitude else magnitude
  where
    invert LT = GT; invert GT = LT; invert EQ = EQ