primus-0.3.0.0: src/Primus/Enum.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
{- |
Module : Primus.Enum
Description : methods for safe enumeration and enumeration on containers
Copyright : (c) Grant Weyburne, 2022
License : BSD-3
-}
module Primus.Enum (
-- * enumerations
universe1,
universe1R,
enumFrom1,
enumFrom1R,
enumTo1,
enumFromThen1,
enumFromTo1,
enumFromThenTo1,
-- * converters
predSafe,
succSafe,
integerToEnumSafe,
integerToIntSafe,
-- * container enums
-- ** enumerations
toEnumList,
toEnumList1,
universeTraversable,
toEnumTraversable,
-- calcNextEnum,
-- minMax,
-- zerolr
-- ** converters
succTraversable,
predTraversable,
fromEnumFoldable,
fromEnumFoldable1,
-- ** capacity
capacity,
) where
import Control.Arrow
import Data.Foldable
import Data.Function
import Data.Functor
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty (..))
import Data.Ord
import Data.Semigroup.Foldable
import Primus.Error
import Primus.Fold
-- | create a nonempty list of all the values for an 'Enum'
universe1 :: forall a. (Bounded a, Enum a) => NonEmpty a
universe1 = enumFrom1 minBound
-- | create a nonempty list of all the values for an 'Enum' in reverse order
universe1R :: forall a. (Bounded a, Enum a) => NonEmpty a
universe1R = enumFrom1R maxBound
-- | create a nonempty list of values starting at "a"
enumFrom1 :: (Bounded a, Enum a) => a -> NonEmpty a
enumFrom1 a = a :| drop 1 [a .. maxBound]
-- | create a nonempty list of values starting at "a"
enumTo1 :: (Bounded a, Enum a) => a -> NonEmpty a
enumTo1 a = minBound :| drop 1 [minBound .. a]
-- | create a nonempty list of values starting at "a" and skipping "b"
enumFromThen1 :: (Bounded a, Enum a) => a -> a -> NonEmpty a
enumFromThen1 a b =
case comparing fromEnum a b of
LT -> a :| drop 1 [a, b .. maxBound]
EQ -> a :| [] -- diverges from enumFromThen by returning one value instead of cycling
GT -> a :| drop 1 [a, b .. minBound]
-- | 'enumFromThenTo' for nonempty lists
enumFromThenTo1 :: Enum a => a -> a -> a -> NonEmpty a
enumFromThenTo1 a b c =
if comparing fromEnum a b == EQ
then a :| [] -- diverges from enumFromThenTo by returning one value instead of cycling
else a :| drop 1 [a, b .. c]
-- | create a nonempty list of values starting at "a" and skipping "b"
enumFromTo1 :: Enum a => a -> a -> NonEmpty a
enumFromTo1 a b =
case comparing fromEnum a b of
LT -> a :| drop 1 [a .. b]
EQ -> a :| [] -- diverges from enumFromTo by returning one value instead of cycling
GT -> a :| drop 1 [a, pred a .. b] -- diverges from enumFromTo by going backwards instead of returning nothing
-- pred has to exist: a > b => pred a >= b unless float ...
-- | create a nonempty list of "a" in reverse order
enumFrom1R :: forall a. (Bounded a, Enum a) => a -> NonEmpty a
enumFrom1R a
| Just prv <- predSafe a = a :| drop 1 [a, prv .. minBound]
| otherwise = a :| []
-- | safe 'pred' for a bounded 'Enum'
predSafe :: (Bounded a, Enum a) => a -> Maybe a
predSafe a
| on (==) fromEnum a minBound = Nothing
| otherwise = Just (pred a)
-- | safe 'succ' for a bounded 'Enum'
succSafe :: (Bounded a, Enum a) => a -> Maybe a
succSafe a
| on (==) fromEnum a maxBound = Nothing
| otherwise = Just (succ a)
-- | load a given container with "a"s using the relative position "i"
toEnumTraversable ::
forall a f z.
(Traversable f, Enum a, Bounded a) =>
f z ->
Integer ->
Either String (f a)
toEnumTraversable tz i = do
lst <- toEnumList @a i
z <- zerolr
c <- capacity @a tz
lmsg ("cap=" ++ show c) $ padL (z <$ tz) lst
zerolr :: forall a. (Bounded a, Enum a) => Either String a
zerolr = left (const "zerolr: not defined at zero") $ integerToEnumSafe @a 0
-- | calculates the minimum and maximum range of enumerations that can be stored in a container of the given size
capacity :: forall a t z. (Bounded a, Enum a, Foldable t) => t z -> Either String (Integer, Integer)
capacity (length -> len) = lmsg "capacity" $ do
let z@(mn, mx) = minMax @a
lhs <- case compare mn 0 of
LT -> Right (-(-mn + 1) ^ len + 1)
EQ -> Right 0
GT -> Left $ "unsupported mn > 0: " ++ show z
rhs <- case compare 0 mx of
LT -> Right ((mx + 1) ^ len - 1)
EQ -> Right 0
GT -> Left $ "unsupported mx < 0: " ++ show z
pure (lhs, rhs)
{- | convert toEnum of "a" into a list containing "a"s
zero is the empty list: see 'toEnumList'
-}
toEnumList :: forall a. (Enum a, Bounded a) => Integer -> Either String [a]
toEnumList i
| i == 0 = [] <$ zerolr @a
| otherwise =
let f :: Integer -> Either String (Maybe (a, Integer))
f s
| s == 0 = pure Nothing
| otherwise =
calcNextEnum s <&> \(s', a) ->
if abs s' < abs s
then Just (a, s')
else
if s' == 0
then Nothing
else programmError "toEnumList"
in unfoldlM f i
-- | calculate the next enum
calcNextEnum :: forall a. (Enum a, Bounded a) => Integer -> Either String (Integer, a)
calcNextEnum i = lmsg "calcNextEnum" $
case compare i 0 of
GT
| mx > 0 ->
let (a, b) = divMod i (mx + 1)
in case integerToEnumSafe b of
Left e -> Left $ "out of range(GT): " ++ show i ++ " mod " ++ show (mx + 1) ++ " == " ++ show b ++ "(undefined) e=" ++ e
Right c -> Right (a, c)
| otherwise -> Left "not defined for positive numbers"
EQ -> (0,) <$> zerolr
LT
| mn < 0 ->
let (a, b) = quotRem i (mn - 1)
in case integerToEnumSafe b of
Left e -> Left $ "out of range(LT): " ++ show i ++ " mod " ++ show (mn - 1) ++ " == " ++ show b ++ "(undefined) e=" ++ e
Right c -> Right (-a, c)
| otherwise -> Left "not defined for negative numbers"
where
(mn, mx) = minMax @a
-- | return the min and max of a bounded enum
minMax :: forall a. (Enum a, Bounded a) => (Integer, Integer)
minMax = on (,) (toInteger . fromEnum @a) minBound maxBound
-- | concrete safe conversion of Integer to Int
integerToIntSafe :: Integer -> Either String Int
integerToIntSafe = integerToEnumSafe
-- | safe 'toEnum'
integerToEnumSafe :: forall a. (Enum a, Bounded a) => Integer -> Either String a
integerToEnumSafe i
| i < mn = Left $ msg "underflow"
| i > mx = Left $ msg "overflow"
| otherwise = pure $ toEnum @a $ fromInteger @Int i -- i <= maxBound && maxBound :: Int so cant fail on fromInteger
where
(mn, mx) = minMax @a
msg s = "integerToEnumSafe:" ++ s ++ " where " ++ show i ++ " not in range [" ++ show mn ++ ".." ++ show mx ++ "]"
-- | convert toEnum of "a" into a nonempty list containing "a"s
toEnumList1 :: forall a. (Enum a, Bounded a) => Integer -> Either String (NonEmpty a)
toEnumList1 i =
toEnumList i <&> \case
[] -> minBound :| []
a : as -> a :| as
-- | reverse of 'toEnumList' [can fail if xs is null and toEnum 0 is not defined]
fromEnumFoldable ::
forall a t.
(Foldable t, Enum a, Bounded a) =>
t a ->
Either String Integer
fromEnumFoldable xs =
case toList xs of
[] -> 0 <$ zerolr @a
a : as -> pure $ fromEnumFoldable1 (a :| as)
-- | reverse of 'toEnumList1' [cant fail]
fromEnumFoldable1 ::
forall a t.
(Foldable1 t, Enum a, Bounded a) =>
t a ->
Integer
fromEnumFoldable1 xs =
let (mn, mx) = minMax @a
nn, pp :: Maybe Integer
nn = if mn < 0 then Just (-1) else Nothing
pp = if mx > 0 then Just 1 else Nothing
f ::
a ->
(Integer, (Maybe Integer, Maybe Integer)) ->
(Integer, (Maybe Integer, Maybe Integer))
f a (b, (n, p)) =
let v = toInteger (fromEnum a)
w = case (compare v 0, n, p) of
(LT, Just x, _) -> b - v * x
(EQ, _, _) -> b
(GT, _, Just y) -> b + v * y
o -> programmError $ "fromEnumFoldable1 " ++ show o
in (w, ((\x -> -(mn - 1) * x) <$> n, (\x -> (mx + 1) * x) <$> p))
in fst $ foldr f (0, (nn, pp)) xs
-- | 'succ' for a traversable container
succTraversable ::
forall a t.
(Traversable t, Enum a, Bounded a) =>
t a ->
Either String (t a)
succTraversable xs =
let f :: Bool -> a -> (Bool, a)
f b a =
case (b, succSafe a) of
(True, Just a') -> (False, a')
(True, Nothing) -> (True, minBound)
_o -> (b, a)
(lft, ret) = L.mapAccumR f True xs
in if lft
then Left "succTraversable: over maxbound"
else Right ret
-- | 'pred' for a traversable container
predTraversable ::
forall a t.
(Traversable t, Enum a, Bounded a) =>
t a ->
Either String (t a)
predTraversable xs =
let f :: Bool -> a -> (Bool, a)
f b a =
case (b, predSafe a) of
(True, Just a') -> (False, a')
(True, Nothing) -> (True, maxBound)
_o -> (b, a)
(lft, ret) = L.mapAccumR f True xs
in if lft
then Left "predTraversable: below minbound"
else Right ret
{- | generate all the possible enum combinations for the given container in ascending order
useful for creating all the valid indices for matrices
-}
universeTraversable ::
forall f a.
(Traversable f, Enum a, Bounded a) =>
f a ->
Either String (NonEmpty (f a))
universeTraversable ta = do
(mn, mx) <- capacity @a ta
traverse (toEnumTraversable ta) (mn :| drop 1 [mn .. mx])