ascii-numbers-1.2.0.0: library/ASCII/Decimal.hs
module ASCII.Decimal
(
{- * Read/show for numeric strings -}
{- ** Natural -} showNatural, readNatural,
{- ** Integer -} showInteger, readInteger,
{- ** Integral -} showIntegral, readIntegral,
{- * The digit type -} Digit (..),
{- * Decimal digit superset classes -}
{- ** Of digit -} DigitSuperset (..),
{- ** Of digit lists -} DigitStringSuperset (..),
{- * Character/number conversions -}
{- ** Natural -} naturalDigitMaybe, digitNatural,
{- ** Integer -} integerDigitMaybe, digitInteger
) where
import qualified ASCII.Char as ASCII
import qualified ASCII.Refinement
import ASCII.Refinement (ASCII, asciiUnsafe)
import ASCII.Superset (StringSuperset, fromChar, fromCharList, toCharListMaybe,
toCharMaybe)
import Control.Monad ((<=<), (=<<))
import Data.Bifoldable (bifoldMap)
import Data.Bits (Bits, toIntegralSized)
import Data.Bool (Bool, (&&), (||))
import Data.Data (Data)
import Data.Eq (Eq)
import Data.Function (id, (.))
import Data.Functor (fmap)
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Data.Maybe (Maybe (..), fromJust, isJust)
import Data.Monoid (mempty)
import Data.Ord (Ord (..))
import Data.Word (Word8)
import GHC.Generics (Generic)
import Numeric.Natural (Natural)
import Prelude (Bounded (..), Enum (..), Integer, Integral, abs, fromEnum,
fromInteger, fromIntegral, negate, quotRem, toEnum, toInteger,
(*), (+), (-))
import Text.Show (Show)
import qualified Data.Bool as Bool
import qualified Data.Char as Unicode
import qualified Data.List as List
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as TB
import DList (DList)
import qualified DList
{- |
The subset of ASCII used to represent unsigned decimal numbers:
* 'ASCII.Char.Digit0' to 'ASCII.Char.Digit9'
-}
data Digit
= Digit0 -- ^ Zero
| Digit1 -- ^ One
| Digit2 -- ^ Two
| Digit3 -- ^ Three
| Digit4 -- ^ Four
| Digit5 -- ^ Five
| Digit6 -- ^ Six
| Digit7 -- ^ Seven
| Digit8 -- ^ Eight
| Digit9 -- ^ Nine
deriving stock (Bounded, Enum, Eq, Ord, Show, Data, Generic)
deriving anyclass Hashable
--- Show functions ---
{- |
Examples:
* @showNatural 0@ = @"0"@
* @showNatural 268@ = @"268"@
-}
showNatural :: DigitStringSuperset string => Natural -> string
showNatural =
\case
0 -> fromDigitList [ Digit0 ]
n -> fromDigitList (naturalList n)
where
naturalList :: Natural -> [Digit]
naturalList = DList.toList . r
where
r :: Natural -> DList Digit
r = \case
0 -> mempty
n ->
bifoldMap
r
(DList.singleton . toEnum . fromIntegral)
(quotRem n 10)
{- |
Examples:
* @showInteger 0@ = @"0"@
* @showInteger 12@ = @"12"@
* @showInteger (negate 12)@ = @"-12"@
-}
showInteger :: StringSuperset string => Integer -> string
showInteger = fromCharList . integerList
where
integerList :: Integer -> [ASCII.Char]
integerList =
\case
0 -> [ ASCII.Digit0 ]
n | n < 0 -> ASCII.HyphenMinus : nonNegativeIntegerList (abs n)
n -> nonNegativeIntegerList n
nonNegativeIntegerList :: Integer -> [ASCII.Char]
nonNegativeIntegerList = DList.toList . r
where
r :: Integer -> DList ASCII.Char
r = \case
0 -> mempty
n ->
bifoldMap
r
(DList.singleton . fromDigit . toEnum . fromInteger)
(quotRem n 10)
showIntegral :: (Integral n, StringSuperset string) => n -> string
showIntegral = showInteger . toInteger
--- Read functions ---
{- |
Examples:
* @readNatural "0"@ = @Just 0@
* @readNatural "268"@ = @Just 268@
* @readNatural "0004"@ = @Just 4@
* @readNatural ""@ = @Nothing@
* @readNatural "-4"@ = @Nothing@
* @readNatural "12345678901234567890"@ = @Just 12345678901234567890@
-}
readNatural :: DigitStringSuperset string => string -> Maybe Natural
readNatural = (Just . readNaturalDigits) <=< nonEmpty <=< toDigitListMaybe
where
readNaturalDigits :: NonEmpty Digit -> Natural
readNaturalDigits = List.foldl' (\total x -> (10 * total) + fromIntegral (fromEnum x)) 0
{- |
Examples:
* @readInteger "0"@ = @Just 0@
* @readInteger "268"@ = @Just 268@
* @readInteger "0004"@ = @Just 4@
* @readInteger ""@ = @Nothing@
* @readInteger "-4"@ = @Just (-4)@
* @readInteger "12345678901234567890"@ = @Just 12345678901234567890@
-}
readInteger :: StringSuperset string => string -> Maybe Integer
readInteger = readIntegerCharList <=< toCharListMaybe
where
readIntegerCharList :: [ASCII.Char] -> Maybe Integer
readIntegerCharList =
\case
ASCII.HyphenMinus : xs -> fmap negate (readNonNegative xs)
xs -> readNonNegative xs
readNonNegative :: [ASCII.Char] -> Maybe Integer
readNonNegative = (Just . toInteger . readIntegerDigits) <=< nonEmpty <=< toDigitListMaybe
readIntegerDigits :: NonEmpty Digit -> Integer
readIntegerDigits = List.foldl' (\total x -> (10 * total) + fromIntegral (fromEnum x)) 0
{- |
Examples:
* @readIntegral "0"@ = @Just (0 :: Word8)@
* @readIntegral "175"@ = @Just (175 :: Word8)@
* @readIntegral "268"@ = @(Nothing :: Maybe Word8)@
* @readIntegral "0004"@ = @Just (4 :: Word8)@
* @readIntegral ""@ = @(Nothing :: Maybe Word8)@
* @readIntegral "-4"@ = @(Nothing :: Maybe Word8)@
* @readIntegral "12345678901234567890"@ = @(Nothing :: Maybe Word8)@
-}
readIntegral :: (StringSuperset string, Integral num, Bits num) => string -> Maybe num
readIntegral = toIntegralSized <=< readInteger
--- Uninteresting monomorphic specializations of polymorphic functions ---
{- |
Examples:
* @naturalDigitMaybe 5@ = @Just Digit5@
* @naturalDigitMaybe 12@ = @Nothing@
-}
naturalDigitMaybe :: Natural -> Maybe Digit
naturalDigitMaybe n = if n > 9 then Nothing else Just (toEnum (fromIntegral n))
{- |
Examples:
* @integerDigitMaybe 5@ = @Just Digit5@
* @integerDigitMaybe 12@ = @Nothing@
-}
integerDigitMaybe :: Integer -> Maybe Digit
integerDigitMaybe n = if (n < 0 || n > 9) then Nothing else Just (toEnum (fromInteger n))
digitNatural :: Digit -> Natural
digitNatural = fromIntegral . fromEnum
digitInteger :: Digit -> Integer
digitInteger = fromIntegral . fromEnum
--- Classes ---
class DigitSuperset char
where
fromDigit :: Digit -> char
isDigit :: char -> Bool
isDigit = isJust . toDigitMaybe
toDigitUnsafe :: char -> Digit
toDigitUnsafe = fromJust . toDigitMaybe
toDigitMaybe :: char -> Maybe Digit
toDigitMaybe x = if isDigit x then Just (toDigitUnsafe x) else Nothing
{-# minimal fromDigit, ((isDigit, toDigitUnsafe) | toDigitMaybe) #-}
class DigitStringSuperset string
where
fromDigitList :: [Digit] -> string
isDigitString :: string -> Bool
isDigitString = isJust . toDigitListMaybe
toDigitListUnsafe :: string -> [Digit]
toDigitListUnsafe = fromJust . toDigitListMaybe
toDigitListMaybe :: string -> Maybe [Digit]
toDigitListMaybe x = if isDigitString x then Just (toDigitListUnsafe x) else Nothing
{-# minimal fromDigitList, ((isDigitString, toDigitListUnsafe) | toDigitListMaybe) #-}
--- DigitSuperset instances ---
instance DigitSuperset Digit
where
isDigit _ = Bool.True
fromDigit = id
toDigitUnsafe = id
toDigitMaybe = Just
instance DigitSuperset ASCII.Char
where
isDigit x = x >= ASCII.Digit0 && x <= ASCII.Digit9
fromDigit = toEnum . (\x -> x + fromEnum ASCII.Digit0) . fromEnum
toDigitUnsafe = toEnum . (\x -> x - fromEnum ASCII.Digit0) . fromEnum
instance DigitSuperset Unicode.Char
where
isDigit x = x >= '0' && x <= '9'
fromDigit = Unicode.chr . (\x -> x + Unicode.ord '0') . fromEnum
toDigitUnsafe = toEnum . (\x -> x - Unicode.ord '0') . Unicode.ord
instance DigitSuperset Word8
where
fromDigit x = fromChar (fromDigit x :: ASCII.Char)
toDigitMaybe w = toDigitMaybe =<< (toCharMaybe w :: Maybe ASCII.Char)
instance DigitSuperset char => DigitSuperset (ASCII char)
where
isDigit = isDigit . ASCII.Refinement.lift
fromDigit = asciiUnsafe . fromDigit
toDigitUnsafe = toDigitUnsafe . ASCII.Refinement.lift
toDigitMaybe = toDigitMaybe . ASCII.Refinement.lift
--- DigitStringSuperset instances ---
instance DigitStringSuperset [Digit]
where
isDigitString _ = Bool.True
fromDigitList = id
toDigitListUnsafe = id
toDigitListMaybe = Just
instance DigitStringSuperset [ASCII.Char]
where
isDigitString = List.all isDigit
fromDigitList = List.map fromDigit
toDigitListUnsafe = List.map toDigitUnsafe
instance DigitStringSuperset [Unicode.Char]
where
isDigitString = List.all isDigit
fromDigitList = List.map fromDigit
toDigitListUnsafe = List.map toDigitUnsafe
instance DigitStringSuperset T.Text
where
isDigitString = T.all isDigit
fromDigitList = T.pack . List.map fromDigit
toDigitListUnsafe = List.map toDigitUnsafe . T.unpack
instance DigitStringSuperset LT.Text
where
isDigitString = LT.all isDigit
fromDigitList = LT.pack . List.map fromDigit
toDigitListUnsafe = List.map toDigitUnsafe . LT.unpack
instance DigitStringSuperset TB.Builder
where
fromDigitList = TB.fromLazyText . fromDigitList
toDigitListMaybe = toDigitListMaybe . TB.toLazyText
instance DigitStringSuperset BS.ByteString
where
isDigitString = BS.all isDigit
fromDigitList = BS.pack . List.map fromDigit
toDigitListUnsafe = List.map toDigitUnsafe . BS.unpack
instance DigitStringSuperset LBS.ByteString
where
isDigitString = LBS.all isDigit
fromDigitList = LBS.pack . List.map fromDigit
toDigitListUnsafe = List.map toDigitUnsafe . LBS.unpack
instance DigitStringSuperset BSB.Builder
where
fromDigitList = BSB.lazyByteString . fromDigitList
toDigitListMaybe = toDigitListMaybe . BSB.toLazyByteString
instance DigitStringSuperset char => DigitStringSuperset (ASCII char)
where
isDigitString = isDigitString . ASCII.Refinement.lift
fromDigitList = asciiUnsafe . fromDigitList
toDigitListUnsafe = toDigitListUnsafe . ASCII.Refinement.lift
toDigitListMaybe = toDigitListMaybe . ASCII.Refinement.lift