packages feed

trade-journal-0.0.1: src/Journal/Amount.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Journal.Amount
  ( Amount (..),
    _Amount,
    rounded,
    thousands,
    renderAmount,
    normalizeAmount,
    spreadAmounts,
    showAmount,
    amountToString,
    mpfr_RNDN,
    mpfr_RNDZ,
    mpfr_RNDU,
    mpfr_RNDD,
    mpfr_RNDA,
    mpfr_RNDF,
    mpfr_RNDNA,
    sign,
  )
where

import Control.Monad
import Data.Aeson
import Data.Char (isDigit)
import Data.Coerce
import Data.Data
import Data.Default
import Data.Function (on)
import Data.Int (Int64)
import Data.List (intercalate)
import Data.List.Split
import Data.Profunctor
import Data.Ratio
import Foreign.C.String
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable
import GHC.Generics
import GHC.TypeLits
import Journal.Utils (Render (..))
import System.IO.Unsafe
import Text.PrettyPrint (text)
import Text.Show.Pretty as P
import Prelude hiding (Double, Float)

mpfr_RNDN, mpfr_RNDZ, mpfr_RNDU, mpfr_RNDD, mpfr_RNDA, mpfr_RNDF :: CUInt

mpfr_RNDNA :: CUInt

mpfr_RNDN = 0 -- round to nearest, with ties to even

mpfr_RNDZ = 1 -- round toward zero

mpfr_RNDU = 2 -- round toward +Inf

mpfr_RNDD = 3 -- round toward -Inf

mpfr_RNDA = 4 -- round away from zero

mpfr_RNDF = 5 -- faithful rounding

mpfr_RNDNA = 6 -- round to nearest, with ties away from zero (mpfr_round)

foreign import ccall unsafe "mpfr_free_str" c'mpfr_free_str :: CString -> IO ()

foreign import ccall unsafe "rational_to_str"
  c'rational_to_str ::
    CLong -> CULong -> CUInt -> CSize -> Ptr CString -> IO ()

newtype Amount (dec :: Nat) = Amount {getAmount :: Ratio Int64}
  deriving
    ( Generic,
      Data,
      Typeable,
      Ord,
      Num,
      Fractional,
      Real,
      RealFrac
    )

instance Default (Amount n) where
  def = 0

instance KnownNat n => PrettyVal (Amount n) where
  prettyVal = P.String . show

showAmount :: forall n. KnownNat n => CUInt -> Amount n -> String
showAmount rnd (Amount r) =
  unsafePerformIO $ alloca $ \bufPtr -> do
    c'rational_to_str
      (CLong (numerator r))
      (CULong (fromIntegral (denominator r)))
      rnd
      (CSize (fromIntegral (natVal (Proxy :: Proxy n))))
      bufPtr
    buf <- peek bufPtr
    str <- peekCString buf
    c'mpfr_free_str buf
    return str

instance KnownNat n => Eq (Amount n) where
  (==) = (==) `on` show

instance KnownNat n => Show (Amount n) where
  show = amountToString 2

instance KnownNat n => Read (Amount n) where
  readsPrec _d = \case
    '-' : xs -> map (\(x, y) -> (negate x, y)) (readNum xs)
    xs -> readNum xs
    where
      readNum r = case takeWhile isDigit r of
        [] -> error $ "Not an amount: " ++ r
        num -> case dropWhile isDigit r of
          ('.' : den) ->
            let den' = takeWhile isDigit den
                rem' = dropWhile isDigit den
             in [(Amount (read (num ++ den') % 10 ^ length den'), rem')]
          xs -> [(Amount (read num % 1), xs)]

instance KnownNat n => Render (Amount n) where
  rendered = text . show

instance KnownNat n => ToJSON (Amount n) where
  toJSON = Number . fromRational . toRational . getAmount

instance KnownNat n => FromJSON (Amount n) where
  parseJSON (Number n) = pure $ Amount (fromRational (toRational n))
  parseJSON v = error $ "Expected Amount, saw: " ++ show v

-- _Amount :: KnownNat n => Prism' String (Amount n)
_Amount ::
  (KnownNat n, Choice p, Applicative f) =>
  p (Amount n) (f (Amount n)) ->
  p String (f String)
_Amount = dimap read (fmap show)

-- rounded :: (KnownNat m, KnownNat n) => Iso' (Amount n) (Amount m)
rounded ::
  (Profunctor p, Functor f) =>
  p (Amount m) (f (Amount m)) ->
  p (Amount n) (f (Amount n))
rounded = dimap coerce (fmap coerce)

amountToString :: KnownNat n => Int -> Amount n -> String
amountToString n = touchup . cleanup n . showAmount mpfr_RNDNA
  where
    touchup s
      | last s == '.' = take (length s - 1) s
      | otherwise = s
    cleanup m t
      | len > m && last t == '0' = cleanup m (take (length t - 1) t)
      | otherwise = t
      where
        len = length (last (splitOn "." t))

thousands :: forall n. KnownNat n => Amount n -> String
thousands d = intercalate "." $ case splitOn "." str of
  x : xs -> (reverse . go . reverse) x
    : case xs of
      y : ys -> expand y : ys
      _ | isInt -> ["00"]
      _ -> []
  xs -> xs
  where
    isInt = case natVal (Proxy :: Proxy n) of
      0 -> True
      _ -> False
    str
      | isInt = show (floor d :: Int)
      | otherwise = amountToString 2 d
    go (x : y : z : []) = x : y : z : []
    go (x : y : z : ['-']) = x : y : z : ['-']
    go (x : y : z : xs) = x : y : z : ',' : go xs
    go xs = xs
    expand [] = "00"
    expand (x : []) = x : "0"
    expand xs = xs

renderAmount :: KnownNat n => Amount n -> String
renderAmount d
  | fromIntegral (floor d :: Int) == d =
    thousands @0 (coerce d)
renderAmount d = thousands d

normalizeAmount :: KnownNat n => CUInt -> Amount n -> Amount n
normalizeAmount = (read .) . showAmount

-- Given a way of project a "count" from an element, an amount, and a list of
-- elements, divide the given amount among the elements each according to its
-- count. Thus, if passed a two element list with counts 60 and 40, the amount
-- would be divided 60% to the first, and 40% to the second.
spreadAmounts ::
  (KnownNat n, KnownNat m) =>
  (a -> Amount m) ->
  Amount n ->
  [a] ->
  [(Amount n, a)]
spreadAmounts f n input = go True input
  where
    diff = n - sum (map sump input)
    per = coerce n / shares
    shares = sum (map f input)
    sump l = coerce (f l * per)
    go _ [] = []
    go b (x : xs) = (sum', x) : go False xs
      where
        sum' = sump x + if b then diff else 0

sign :: (Num a, Ord a, Num b) => a -> b -> b
sign n = (if n < 0 then negate else id) . abs