{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE UndecidableInstances #-} module Literate.Accounting.Math where import Control.Applicative (Applicative (..)) import Control.Exception (Exception, throw) import Control.Monad.Classes qualified as MC import Control.Monad.Classes.Run qualified as MC import Control.Monad.Trans.Except as MT import Control.Monad.Trans.Reader as MT import Data.Bool import Data.Data (Data) import Data.Either (Either (..), either) import Data.Eq (Eq (..)) import Data.Foldable (Foldable, foldl') import Data.Function (flip, id, ($), (.)) import Data.Functor ((<$>)) import Data.Functor.Identity (Identity (..)) import Data.Int (Int) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Maybe (Maybe (..)) import Data.Monoid (Endo (..)) import Data.Ord (Ord (..), Ordering (..)) import Data.Proxy (Proxy (..)) import Data.Ratio (Rational) import Data.String (String) import Data.Text (Text) import Data.Typeable (Typeable) import Data.Word import GHC.Generics (Generic) import GHC.Real (FractionalExponentBase (Base10), Ratio ((:%)), (%)) import GHC.Stack (HasCallStack) import GHC.TypeLits (KnownNat, Nat, Symbol, natVal, type (<=)) import Literate.Prelude import Numeric.Natural import Text.Read (Read) import Text.Show (Show (..), ShowS, showParen, showString, showsPrec) import Prelude (Integer, Integral, error, fromIntegral, maxBound) import Prelude qualified -- import Data.Decimal (Decimal, DecimalRaw (..), roundTo) -- import Data.Validity qualified as Validity -- import Data.Reflection (Reifies(..), reify) -- import Data.Modular -- import Literate.Accounting.Rebindable (fromInteger, FromRational(..)) errorWithStack :: HasCallStack => String -> a errorWithStack = error -- (msg <> "\n" <> prettyCallStack callStack) -- * Class 'Zeroable' class Zeroable a where zero :: a isZero :: a -> Bool default isZero :: Zeroable a => Eq a => a -> Bool isZero = (== zero) instance Zeroable String where zero = "" isZero = null instance Zeroable Word32 where zero = (0 :: Int) & fromIntegral instance Zeroable Word64 where zero = (0 :: Int) & fromIntegral -- instance Zeroable Decimal where -- zero = 0 instance Zeroable (Map.Map k a) where zero = Map.empty isZero = Map.null {- instance Zeroable Decimal where zero = 0 instance Zeroable (Map k a) where zero = Map.empty -} -- * Class 'Signable' class Signable a where sign :: a -> Ordering default sign :: Zeroable a => Ord a => a -> Ordering sign a = case () of _ | isZero a -> EQ _ | a < zero -> LT _ -> GT -- instance Signable Decimal -- * Class 'Addable' -- | Can be added. class Addable a where (+) :: HasCallStack => a -> a -> a infixl 6 + default (+) :: Prelude.Num a => HasCallStack => a -> a -> a (+) = (Prelude.+) -- | For @'Addable' ('Map' k ())@. instance Addable () where (+) () () = () instance (Ord k, Addable a) => Addable (Map k a) where (+) = Map.unionWith (flip (+)) instance Addable a => Addable (Maybe a) where Nothing + Nothing = Nothing Just x + Nothing = Just x Nothing + Just y = Just y Just x + Just y = Just (x + y) {- instance Addable Decimal where (+) x y = Decimal e (fromIntegral (nx Prelude.+ ny)) where (e, nx, ny) = roundMinDecimal x y -- | Round the two 'DecimalRaw' values to the smallest exponent. roundMinDecimal :: Integral i => DecimalRaw i -> DecimalRaw i -> (Word8, i, i) roundMinDecimal d1@(Decimal e1 _) d2@(Decimal e2 _) = (e, n1, n2) where e = min e1 e2 Decimal _ n1 = roundTo e d1 Decimal _ n2 = roundTo e d2 -} -- * Class 'Negable' class Negable a where negate :: a -> a default negate :: Prelude.Num a => a -> a negate = Prelude.negate -- | For @'Negable' ('Map' k ())@. instance Negable () where negate () = () instance Negable Int instance Negable Integer -- instance Negable Decimal instance Negable a => Negable (Map k a) where negate = Map.map negate instance Negable a => Negable (Endo a) where negate (Endo f) = Endo (f . negate) instance Negable a => Negable [a] where negate = (negate <$>) -- * Class 'Substractable' class Substractable a where (-) :: a -> a -> a infixl 6 - default (-) :: Prelude.Num a => a -> a -> a (-) = (Prelude.-) -- | For @'Substractable' ('Map' k ())@. instance Substractable () where (-) () () = () instance Substractable Int instance Substractable Integer -- instance Substractable Decimal instance (Ord k, Addable a, Negable a) => Substractable (Map k a) where (-) x y = Map.unionWith (flip (+)) x (negate y) -- * Constraint 'MinQty' type QuantFact n = (KnownNat n, 1 <= n, n <= 4294967295 {-Word32-}) quantisationFactor :: forall qf. QuantFact qf => Word32 quantisationFactor = Prelude.fromIntegral (natVal (Proxy @qf)) data Unit = UnitName Symbol | (:*:) Unit Unit | (:/:) Unit Unit class UnitShowS (u :: Unit) where unitShowS :: Int -> ShowS instance KnownSymbol u => UnitShowS (UnitName u) where unitShowS _prec = showString (symbolVal (Proxy @u)) instance (UnitShowS x, UnitShowS y) => UnitShowS (x :*: y) where unitShowS p = showParen (7 <= p) $ unitShowS @x 7 . showString "\x202F*\x202F" . unitShowS @y 7 instance (UnitShowS x, UnitShowS y) => UnitShowS (x :/: y) where unitShowS p = showParen (7 <= p) $ unitShowS @x 7 . showString "\x202F/\x202F" . unitShowS @y 7 unitShow :: forall u. UnitShowS u => String unitShow = unitShowS @u 0 "" -- * Type 'Amount' newtype Amount (qf :: Nat) (unit :: Unit) = Amount { amountQuantity :: Quantity qf } deriving (Show, Read, Eq, Ord, Data, Typeable, Generic) instance QuantFact qf => FromInteger (MT.Except ErrorQuantity (Amount qf unit)) where fromInteger i = fromRational (i :% 1) -- * Type 'Quantity' newtype Quantity qf = Quantity { unQuantity :: Word64 } deriving (Show, Read, Eq, Ord, Data, Typeable, Generic) instance Zeroable (Quantity qf) where zero = Quantity 0 isZero (Quantity q) = q == 0 -- instance Validity Quantity -- instance NFData Quantity instance QuantFact qf => FromInteger (MT.Except ErrorQuantity (Quantity qf)) where fromInteger n = fromRational (n :% 1) instance QuantFact qf => FromInteger (Quantity qf) where fromInteger n = fromRational (n :% 1) instance QuantFact qf => FromInteger (Amount qf unit) where fromInteger = fromInteger >>> Amount instance Integral a => Addable (Ratio a) where x + y = x Prelude.+ y instance Addable (Maybe (Quantity qf)) where (+) mx my = do Quantity x <- mx Quantity y <- my let res = fromIntegral x Prelude.+ fromIntegral y if res > fromIntegral @Word64 maxBound then Nothing else Just (Quantity (Prelude.fromInteger res)) instance Addable (Quantity qf) where (+) x y = do let res = fromIntegral (unQuantity x) Prelude.+ fromIntegral (unQuantity y) if res > fromIntegral (maxBound @Word64) then errorWithStack "Quantity overflow" else Quantity (Prelude.fromInteger res) instance Addable (Amount qf unit) where Amount x + Amount y = Amount (x + y) {- sum :: forall f. Foldable f => f Amount -> Maybe Amount sum l = let maxBoundI :: Integer maxBoundI = fromIntegral (maxBound :: Word64) r :: Integer r = foldl' (\acc a -> (toInteger :: Word64 -> Integer) (amountQuantity a) + acc) 0 l in if r > maxBoundI then Nothing else Just (Amount ((fromInteger :: Integer -> Word64) r)) -} instance Substractable (Maybe (Quantity qf)) where (-) mx my = do Quantity x <- mx Quantity y <- my let res = fromIntegral x - fromIntegral y if res < 0 then Nothing else Just (Quantity (Prelude.fromInteger res)) sumAmounts :: forall qf unit f. Functor f => Foldable f => f (Amount qf unit) -> Maybe (Amount qf unit) sumAmounts l = l <&> amountQuantity & sumQuantities <&> Amount sumQuantities :: forall qf f. Foldable f => f (Quantity qf) -> Maybe (Quantity qf) sumQuantities l = let res = foldl' (\acc a -> Prelude.toInteger (unQuantity a) Prelude.+ acc) 0 l in if res > fromIntegral (maxBound @Word64) then Nothing else Just (Quantity (Prelude.fromInteger res)) multiply :: Word32 -> Quantity qf -> Maybe (Quantity qf) multiply c (Quantity qty) = let res :: Integer res = Prelude.fromIntegral c Prelude.* Prelude.fromIntegral qty in if res > fromIntegral (maxBound @Word64) then Nothing else Just (Quantity (Prelude.fromInteger res)) -- * Type 'CurrencyEnv' newtype CurrencyEnv = CurrencyEnv { currencyDefault :: Text } -- newtype CurrencyT repr a = CurrencyT { unCurrencyT :: MT.ReaderT CurrencyEnv repr a } -- * Class 'CurrencyUSD' class CurrencyUSD repr where usd :: repr a -> repr a instance CurrencyUSD (MT.ReaderT CurrencyEnv repr) where usd = MT.local (\c -> c{currencyDefault = "$"}) -- * Type 'ErrorQuantity' data ErrorQuantity = ErrorQuantityNaN | ErrorQuantityInfinite | ErrorQuantityNotNormalised | ErrorQuantityOverflow Natural | ErrorQuantityNegative | ErrorQuantityNotMultipleOfMinimalQuantity (Rational) (Word32) Natural Natural deriving (Eq, Show) instance Exception ErrorQuantity newtype MinimalQuantity = MinimalQuantity Word32 {- quantityFromRational :: forall qf m. MC.MonadExcept ErrorQuantity m => MC.MonadReader MinimalQuantity m => --Reifies qf MinimalQuantity => Rational -> m (Quantity qf) -} instance QuantFact qf => FromRational (MT.Except ErrorQuantity (Quantity qf)) where fromRational r@(rn :% rd) -- ToDo: replace with isValid | rd == 0 = MC.throw $ if rn == 0 then ErrorQuantityNaN else ErrorQuantityInfinite | gcd < 0 || Prelude.quot rn gcd :% Prelude.quot rd gcd /= rn :% rd = MC.throw ErrorQuantityNotNormalised | r < 0 = MC.throw ErrorQuantityNegative | (fromIntegral :: Word64 -> Natural) (maxBound :: Word64) < ceiled = MC.throw $ ErrorQuantityOverflow ceiled | floored == ceiled = pure $ Quantity $ fromIntegral floored | otherwise = MC.throw $ ErrorQuantityNotMultipleOfMinimalQuantity r minQty floored ceiled where gcd = Prelude.gcd rn rd minQty = quantisationFactor @qf qty :: Rational qty = r Prelude.* fromIntegral minQty ceiled :: Natural ceiled = Prelude.ceiling qty floored :: Natural floored = Prelude.floor qty instance QuantFact qf => FromRational (MT.Except ErrorQuantity (Amount qf unit)) where fromRational r = Amount <$> fromRational r -- | Warning(functional/completeness/partial) instance QuantFact qf => FromRational (Quantity qf) where fromRational = fromRational >>> MT.runExcept >>> either (throw @_ @_ @ErrorQuantity) id instance QuantFact qf => FromRational (Amount qf unit) where fromRational = Amount . fromRational -- | Turn an amount of money into a 'Ratio'. -- -- WARNING: that the result will be @Quantity :% 0@ if the quantisation factor is @0@. quantityToRatio :: forall qf. QuantFact qf => Quantity qf -> Ratio Natural quantityToRatio (Quantity q) = -- \| isZero qf = Prelude.fromIntegral q :% 0 -- \| otherwise = (Prelude.fromIntegral :: Word64 -> Natural) q % (Prelude.fromIntegral :: Word32 -> Natural) qf where qf = quantisationFactor @qf -- | Turn an amount of money into a 'Rational'. -- -- WARNING: that the result will be @Quantity :% 0@ if the quantisation factor is @0@. quantityToRational :: QuantFact qf => Quantity qf -> Rational quantityToRational q = q & quantityToRatio & Prelude.toRational -- instance QuantFact qf => FromRational (Amount qf unit) where -- fromRational = either (throw @_ @_ @ErrorQuantity) id . MT.runExcept . fromRational {- validateNotNaN :: RealFloat a => a -> Validation validateNotNaN x | isNaN x = validateNotInfinite :: RealFloat a => a -> Validation validateNotInfinite d = declare "The RealFloat is not infinite." $ not (isInfinite d) validateRatioNotNaN :: Integral a => Ratio a -> Validation validateRatioNotNaN r = declare "The Ratio is not NaN." $ case r of (0 :% 0) -> False _ -> True validateRatioNotInfinite :: Integral a => Ratio a -> Validation validateRatioNotInfinite r = declare "The Ratio is not infinite." $ case r of (1 :% 0) -> False ((-1) :% 0) -> False _ -> True validateRatioNormalised :: Integral a => Ratio a -> Validation validateRatioNormalised (n :% d) = declare "The Ratio is normalised." $ case d of 0 -> False _ -> let g = gcd n d gcdOverflows = g < 0 n' :% d' = (n `quot` g) :% (d `quot` g) valueIsNormalised = n' :% d' == n :% d in not gcdOverflows && valueIsNormalised -} {- -- | Turn a 'Ratio' into an amount of money. -- -- This function will fail if the 'Ratio': -- -- * Is NaN (0 :% 0) -- * Is infinite (1 :% 0) or (-1 :% 0) -- * Is non-normalised (5 :% 5) -- * Does represent an integer number of minimal quantisations. fromRatio :: Word32 -> Ratio Natural -> Maybe Quantity fromRatio quantisationFactor r = quantityFromRational quantisationFactor (Prelude.quantityToRational r) -- | Distribute an amount of money into chunks that are as evenly distributed as possible. distribute :: Quantity -> Word32 -> QuantityDistribution distribute (Quantity 0) _ = DistributedZeroQuantity distribute _ 0 = DistributedIntoZeroChunks distribute (Quantity a) f = let smallerChunkSize, rest :: Word64 (smallerChunkSize, rest) = divMod a ((fromIntegral :: Word32 -> Word64) f) smallerChunk :: Quantity smallerChunk = Quantity smallerChunkSize in if rest == 0 then DistributedIntoEqualChunks f smallerChunk else let -- This 'fromIntegral' is theoretically not safe, but it's -- necessarily smaller than f so it will fit. numberOfLargerChunks :: Word32 numberOfLargerChunks = (fromIntegral :: Word64 -> Word32) rest numberOfSmallerChunks :: Word32 numberOfSmallerChunks = f - numberOfLargerChunks largerChunk :: Quantity largerChunk = Quantity $ succ smallerChunkSize in DistributedIntoUnequalChunks numberOfLargerChunks largerChunk numberOfSmallerChunks smallerChunk -- | The result of 'distribute' data QuantityDistribution = -- | The second argument was zero. DistributedIntoZeroChunks | -- | The first argument was a zero amount. DistributedZeroQuantity | -- | Distributed into this many equal chunks of this amount DistributedIntoEqualChunks !Word32 !Quantity | -- | Distributed into unequal chunks, this many of the first (larger) amount, and this many of the second (slightly smaller) amount. DistributedIntoUnequalChunks !Word32 !Quantity !Word32 !Quantity deriving (Show, Read, Eq, Generic) instance Validity QuantityDistribution where validate ad = mconcat [ genericValidate ad, case ad of DistributedIntoUnequalChunks _ a1 _ a2 -> declare "The larger chunks are larger" $ a1 > a2 _ -> valid ] instance NFData QuantityDistribution -- | Validate that an 'Quantity' is strictly positive. I.e. not 'zero'. validateStrictlyPositive :: Quantity -> Validation validateStrictlyPositive amount = declare "The Quantity is strictly positive" $ amount > zero -} -- | Fractional multiplication fraction :: Zeroable (Quantity qf) => Ratio Natural -> Quantity qf -> (Quantity qf, Ratio Natural) fraction frac (Quantity 0) = (zero, frac) fraction 0 _ = (zero, 0) fraction frac (Quantity a) = let theoreticalResult :: Ratio Natural theoreticalResult = (fromIntegral :: Word64 -> Ratio Natural) a Prelude.* frac roundedResult :: Word64 roundedResult = (Prelude.round :: Ratio Natural -> Word64) theoreticalResult actualRate :: Ratio Natural actualRate = (fromIntegral :: Word64 -> Natural) roundedResult % (fromIntegral :: Word64 -> Natural) a in (Quantity roundedResult, actualRate)