ENH use decimals to round

This commit is contained in:
Nathan Dwarshuis 2023-07-08 00:52:40 -04:00
parent c886c53f17
commit 46decdc4de
7 changed files with 277 additions and 265 deletions

View File

@ -75,7 +75,8 @@ library
ViewPatterns ViewPatterns
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -Wpartial-fields -Werror -O2 ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -Wpartial-fields -Werror -O2
build-depends: build-depends:
base >=4.12 && <10 Decimal >=0.5.2
, base >=4.12 && <10
, cassava , cassava
, conduit >=1.3.4.2 , conduit >=1.3.4.2
, containers >=0.6.4.1 , containers >=0.6.4.1
@ -144,7 +145,8 @@ executable pwncash
ViewPatterns ViewPatterns
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -Wpartial-fields -Werror -O2 -threaded ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -Wpartial-fields -Werror -O2 -threaded
build-depends: build-depends:
base >=4.12 && <10 Decimal >=0.5.2
, base >=4.12 && <10
, budget , budget
, cassava , cassava
, conduit >=1.3.4.2 , conduit >=1.3.4.2

View File

@ -1,6 +1,7 @@
module Internal.Budget (readBudget) where module Internal.Budget (readBudget) where
import Control.Monad.Except import Control.Monad.Except
import Data.Decimal hiding (allocate)
import Data.Foldable import Data.Foldable
import Internal.Database import Internal.Database
import Internal.Types.Main import Internal.Types.Main
@ -105,7 +106,7 @@ readIncome
(combineError incRes nonIncRes (,)) (combineError incRes nonIncRes (,))
(combineError cpRes dayRes (,)) (combineError cpRes dayRes (,))
$ \_ (cp, days) -> do $ \_ (cp, days) -> do
let gross = roundPrecisionCur cp incGross let gross = realFracToDecimal (cpPrec cp) incGross
foldDays (allocate cp gross) start days foldDays (allocate cp gross) start days
where where
incRes = isIncomeAcnt srcAcnt incRes = isIncomeAcnt srcAcnt
@ -156,7 +157,7 @@ readIncome
let primary = let primary =
EntrySet EntrySet
{ esTotalValue = gross { esTotalValue = gross
, esCurrency = cp , esCurrency = cpID cp
, esFrom = HalfEntrySet {hesPrimary = src, hesOther = []} , esFrom = HalfEntrySet {hesPrimary = src, hesOther = []}
, esTo = HalfEntrySet {hesPrimary = dest, hesOther = allos} , esTo = HalfEntrySet {hesPrimary = dest, hesOther = allos}
} }
@ -178,18 +179,16 @@ periodScaler
-> InsertExcept PeriodScaler -> InsertExcept PeriodScaler
periodScaler pt prev cur = return scale periodScaler pt prev cur = return scale
where where
n = fromIntegral $ workingDays wds prev cur n = workingDays wds prev cur
wds = case pt of wds = case pt of
Hourly HourlyPeriod {hpWorkingDays} -> hpWorkingDays Hourly HourlyPeriod {hpWorkingDays} -> hpWorkingDays
Daily ds -> ds Daily ds -> ds
scale precision x = case pt of scale prec x = case pt of
Hourly HourlyPeriod {hpAnnualHours, hpDailyHours} -> Hourly HourlyPeriod {hpAnnualHours, hpDailyHours} ->
fromRational (rnd $ x / fromIntegral hpAnnualHours) realFracToDecimal prec (x / fromIntegral hpAnnualHours)
* fromIntegral hpDailyHours * fromIntegral hpDailyHours
* n * fromIntegral n
Daily _ -> x * n / 365.25 Daily _ -> realFracToDecimal prec (x * fromIntegral n / 365.25)
where
rnd = roundPrecision precision
-- ASSUME start < end -- ASSUME start < end
workingDays :: [Weekday] -> Day -> Day -> Natural workingDays :: [Weekday] -> Day -> Day -> Natural
@ -267,49 +266,44 @@ selectAllos day Allocation {alloAmts, alloTo} =
, faDesc = amtDesc , faDesc = amtDesc
} }
allo2Trans :: FlatAllocation Rational -> Entry AcntID (LinkDeferred Rational) TagID allo2Trans :: FlatAllocation Decimal -> Entry AcntID LinkDeferred TagID
allo2Trans FlatAllocation {faValue, faTo = TaggedAcnt {taAcnt, taTags}, faDesc} = allo2Trans FlatAllocation {faValue, faTo = TaggedAcnt {taAcnt, taTags}, faDesc} =
Entry Entry
{ eValue = LinkDeferred (EntryValue TFixed faValue) { eValue = LinkDeferred (EntryFixed faValue)
, eComment = faDesc , eComment = faDesc
, eAcnt = taAcnt , eAcnt = taAcnt
, eTags = taTags , eTags = taTags
} }
allocatePre allocatePre
:: Natural :: Precision
-> Rational -> Decimal
-> [FlatAllocation PretaxValue] -> [FlatAllocation PretaxValue]
-> (M.Map T.Text Rational, [FlatAllocation Rational]) -> (M.Map T.Text Decimal, [FlatAllocation Decimal])
allocatePre precision gross = L.mapAccumR go M.empty allocatePre precision gross = L.mapAccumR go M.empty
where where
go m f@FlatAllocation {faValue} = go m f@FlatAllocation {faValue = PretaxValue {preCategory, preValue, prePercent}} =
let c = preCategory faValue let v =
p = preValue faValue if prePercent
v = then gross *. (preValue / 100)
if prePercent faValue else realFracToDecimal precision preValue
then (roundPrecision 3 p / 100) * gross in (mapAdd_ preCategory v m, f {faValue = v})
else roundPrecision precision p
in (mapAdd_ c v m, f {faValue = v})
allocateTax allocateTax
:: Natural :: Precision
-> Rational -> Decimal
-> M.Map T.Text Rational -> M.Map T.Text Decimal
-> PeriodScaler -> PeriodScaler
-> [FlatAllocation TaxValue] -> [FlatAllocation TaxValue]
-> [FlatAllocation Rational] -> [FlatAllocation Decimal]
allocateTax precision gross preDeds f = fmap (fmap go) allocateTax precision gross preDeds f = fmap (fmap go)
where where
go TaxValue {tvCategories, tvMethod} = go TaxValue {tvCategories, tvMethod} =
let agi = gross - sum (mapMaybe (`M.lookup` preDeds) tvCategories) let agi = gross - sum (mapMaybe (`M.lookup` preDeds) tvCategories)
in case tvMethod of in case tvMethod of
TMPercent p -> TMPercent p -> agi *. p / 100
roundPrecision precision $
fromRational $
roundPrecision 3 p / 100 * agi
TMBracket TaxProgression {tpDeductible, tpBrackets} -> TMBracket TaxProgression {tpDeductible, tpBrackets} ->
let taxDed = roundPrecision precision $ f precision tpDeductible let taxDed = f precision tpDeductible
in foldBracket f precision (agi - taxDed) tpBrackets in foldBracket f precision (agi - taxDed) tpBrackets
-- | Compute effective tax percentage of a bracket -- | Compute effective tax percentage of a bracket
@ -323,26 +317,25 @@ allocateTax precision gross preDeds f = fmap (fmap go)
-- --
-- In reality, this can all be done with one loop, but it isn't clear these -- In reality, this can all be done with one loop, but it isn't clear these
-- three steps are implemented from this alone. -- three steps are implemented from this alone.
foldBracket :: PeriodScaler -> Natural -> Rational -> [TaxBracket] -> Rational foldBracket :: PeriodScaler -> Precision -> Decimal -> [TaxBracket] -> Decimal
foldBracket f precision agi bs = fst $ foldr go (0, agi) $ L.sortOn tbLowerLimit bs foldBracket f prec agi bs = fst $ foldr go (0, agi) $ L.sortOn tbLowerLimit bs
where where
go TaxBracket {tbLowerLimit, tbPercent} a@(acc, remain) = go TaxBracket {tbLowerLimit, tbPercent} a@(acc, remain) =
let l = roundPrecision precision $ f precision tbLowerLimit let l = f prec tbLowerLimit
p = roundPrecision 3 tbPercent / 100 in if remain >= l
in if remain >= l then (acc + p * (remain - l), l) else a then (acc + (remain - l) *. (tbPercent / 100), l)
else a
allocatePost allocatePost
:: Natural :: Precision
-> Rational -> Decimal
-> [FlatAllocation PosttaxValue] -> [FlatAllocation PosttaxValue]
-> [FlatAllocation Rational] -> [FlatAllocation Decimal]
allocatePost precision aftertax = fmap (fmap go) allocatePost prec aftertax = fmap (fmap go)
where where
go PosttaxValue {postValue, postPercent} = go PosttaxValue {postValue, postPercent}
let v = postValue | postPercent = aftertax *. (postValue / 100)
in if postPercent | otherwise = realFracToDecimal prec postValue
then aftertax * roundPrecision 3 v / 100
else roundPrecision precision v
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- shadow transfers -- shadow transfers
@ -365,8 +358,9 @@ fromShadow
-> ShadowTransfer -> ShadowTransfer
-> m (Maybe ShadowEntrySet) -> m (Maybe ShadowEntrySet)
fromShadow tx ShadowTransfer {stFrom, stTo, stDesc, stRatio, stCurrency, stMatch} = do fromShadow tx ShadowTransfer {stFrom, stTo, stDesc, stRatio, stCurrency, stMatch} = do
cp <- lookupCurrency stCurrency
res <- liftExcept $ shadowMatches stMatch tx res <- liftExcept $ shadowMatches stMatch tx
es <- entryPair stFrom stTo stCurrency stDesc stRatio () let es = entryPair stFrom stTo (cpID cp) stDesc stRatio ()
return $ if not res then Nothing else Just es return $ if not res then Nothing else Just es
shadowMatches :: TransferMatcher -> Tx CommitR -> InsertExcept Bool shadowMatches :: TransferMatcher -> Tx CommitR -> InsertExcept Bool
@ -374,7 +368,7 @@ shadowMatches TransferMatcher {tmFrom, tmTo, tmDate, tmVal} Tx {txPrimary, txDat
-- NOTE this will only match against the primary entry set since those -- NOTE this will only match against the primary entry set since those
-- are what are guaranteed to exist from a transfer -- are what are guaranteed to exist from a transfer
valRes <- case txPrimary of valRes <- case txPrimary of
Left es -> valMatches tmVal $ esTotalValue es Left es -> valMatches tmVal $ toRational $ esTotalValue es
Right _ -> return True Right _ -> return True
return $ return $
memberMaybe fa tmFrom memberMaybe fa tmFrom
@ -404,7 +398,7 @@ type IntAllocations =
type DaySpanAllocation = Allocation DaySpan type DaySpanAllocation = Allocation DaySpan
type PeriodScaler = Natural -> Double -> Double type PeriodScaler = Precision -> Double -> Decimal
data FlatAllocation v = FlatAllocation data FlatAllocation v = FlatAllocation
{ faValue :: !v { faValue :: !v

View File

@ -22,6 +22,7 @@ where
import Conduit import Conduit
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.Logger import Control.Monad.Logger
import Data.Decimal
import Data.Hashable import Data.Hashable
import Database.Esqueleto.Experimental ((:&) (..), (==.), (^.)) import Database.Esqueleto.Experimental ((:&) (..), (==.), (^.))
import qualified Database.Esqueleto.Experimental as E import qualified Database.Esqueleto.Experimental as E
@ -461,6 +462,7 @@ readUpdates hashes = do
return (makeRE . snd <$> toRead, toUpdate') return (makeRE . snd <$> toRead, toUpdate')
where where
makeUES ((_, day, name, pri, (curID, prec)), es) = do makeUES ((_, day, name, pri, (curID, prec)), es) = do
let prec' = fromIntegral $ E.unValue prec
let res = let res =
bimap NE.nonEmpty NE.nonEmpty $ bimap NE.nonEmpty NE.nonEmpty $
NE.partition ((< 0) . entryRIndex . snd) $ NE.partition ((< 0) . entryRIndex . snd) $
@ -469,22 +471,22 @@ readUpdates hashes = do
case res of case res of
(Just froms, Just tos) -> do (Just froms, Just tos) -> do
let tot = sum $ fmap (entryRValue . snd) froms let tot = sum $ fmap (entryRValue . snd) froms
(from0, fromRO, fromUnkVec) <- splitFrom $ NE.reverse froms (from0, fromRO, fromUnkVec) <- splitFrom prec' $ NE.reverse froms
(from0', fromUnk, to0, toRO, toUnk) <- splitTo from0 fromUnkVec tos (from0', fromUnk, to0, toRO, toUnk) <- splitTo prec' from0 fromUnkVec tos
-- TODO WAP (wet ass programming) -- TODO WAP (wet ass programming)
return $ case from0' of return $ case from0' of
Left x -> Left x ->
Left $ Left $
UpdateEntrySet UpdateEntrySet
{ utDate = E.unValue day { utDate = E.unValue day
, utCurrency = (E.unValue curID, fromIntegral $ E.unValue prec) , utCurrency = E.unValue curID
, utFrom0 = x , utFrom0 = x
, utTo0 = to0 , utTo0 = to0
, utFromRO = fromRO , utFromRO = fromRO
, utToRO = toRO , utToRO = toRO
, utFromUnk = fromUnk , utFromUnk = fromUnk
, utToUnk = toUnk , utToUnk = toUnk
, utTotalValue = tot , utTotalValue = realFracToDecimal prec' tot
, utBudget = E.unValue name , utBudget = E.unValue name
, utPriority = E.unValue pri , utPriority = E.unValue pri
} }
@ -492,7 +494,7 @@ readUpdates hashes = do
Right $ Right $
UpdateEntrySet UpdateEntrySet
{ utDate = E.unValue day { utDate = E.unValue day
, utCurrency = (E.unValue curID, fromIntegral $ E.unValue prec) , utCurrency = E.unValue curID
, utFrom0 = x , utFrom0 = x
, utTo0 = to0 , utTo0 = to0
, utFromRO = fromRO , utFromRO = fromRO
@ -504,32 +506,34 @@ readUpdates hashes = do
, utPriority = E.unValue pri , utPriority = E.unValue pri
} }
_ -> throwError undefined _ -> throwError undefined
makeRE ((_, day, name, pri, (curID, _)), entry) = makeRE ((_, day, name, pri, (curID, prec)), entry) =
let e = entityVal entry let e = entityVal entry
in ReadEntry in ReadEntry
{ reDate = E.unValue day { reDate = E.unValue day
, reCurrency = E.unValue curID , reCurrency = E.unValue curID
, reAcnt = entryRAccount e , reAcnt = entryRAccount e
, reValue = entryRValue e , reValue = realFracToDecimal (fromIntegral $ E.unValue prec) (entryRValue e)
, reBudget = E.unValue name , reBudget = E.unValue name
, rePriority = E.unValue pri , rePriority = E.unValue pri
} }
splitFrom splitFrom
:: NonEmpty (EntryRId, EntryR) :: Precision
-> NonEmpty (EntryRId, EntryR)
-> InsertExcept (Either UEBlank (Either UE_RO UEUnk), [UE_RO], [UEUnk]) -> InsertExcept (Either UEBlank (Either UE_RO UEUnk), [UE_RO], [UEUnk])
splitFrom (f0 :| fs) = do splitFrom prec (f0 :| fs) = do
-- ASSUME entries are sorted by index -- ASSUME entries are sorted by index
-- TODO combine errors here -- TODO combine errors here
let f0Res = readDeferredValue f0 let f0Res = readDeferredValue prec f0
let fsRes = mapErrors splitDeferredValue fs let fsRes = mapErrors (splitDeferredValue prec) fs
combineErrorM f0Res fsRes $ \f0' fs' -> do combineErrorM f0Res fsRes $ \f0' fs' -> do
let (ro, unk) = partitionEithers fs' let (ro, unk) = partitionEithers fs'
-- let idxVec = V.fromList $ fmap (either (const Nothing) Just) fs' -- let idxVec = V.fromList $ fmap (either (const Nothing) Just) fs'
return (f0', ro, unk) return (f0', ro, unk)
splitTo splitTo
:: Either UEBlank (Either UE_RO UEUnk) :: Precision
-> Either UEBlank (Either UE_RO UEUnk)
-> [UEUnk] -> [UEUnk]
-> NonEmpty (EntryRId, EntryR) -> NonEmpty (EntryRId, EntryR)
-> InsertExcept -> InsertExcept
@ -539,7 +543,7 @@ splitTo
, [UE_RO] , [UE_RO]
, [UEUnk] , [UEUnk]
) )
splitTo from0 fromUnk (t0 :| ts) = do splitTo prec from0 fromUnk (t0 :| ts) = do
-- How to split the credit side of the database transaction in 1024 easy -- How to split the credit side of the database transaction in 1024 easy
-- steps: -- steps:
-- --
@ -547,7 +551,7 @@ splitTo from0 fromUnk (t0 :| ts) = do
let (unlinked, linked) = partitionEithers $ fmap splitLinked ts let (unlinked, linked) = partitionEithers $ fmap splitLinked ts
-- 2. For unlinked entries, split into read-only and unknown entries -- 2. For unlinked entries, split into read-only and unknown entries
let unlinkedRes = partitionEithers <$> mapErrors splitDeferredValue unlinked let unlinkedRes = partitionEithers <$> mapErrors (splitDeferredValue prec) unlinked
-- 3. For linked entries, split into those that link to the primary debit -- 3. For linked entries, split into those that link to the primary debit
-- entry and not -- entry and not
@ -557,7 +561,7 @@ splitTo from0 fromUnk (t0 :| ts) = do
-- into those that link to an unknown debit entry or not. Those that -- into those that link to an unknown debit entry or not. Those that
-- are not will be read-only and those that are will be collected with -- are not will be read-only and those that are will be collected with
-- their linked debit entry -- their linked debit entry
let linkedRes = zipPaired fromUnk linkedN let linkedRes = zipPaired prec fromUnk linkedN
-- 5. For entries linked to the primary debit entry, turn them into linked -- 5. For entries linked to the primary debit entry, turn them into linked
-- entries (lazily only used when needed later) -- entries (lazily only used when needed later)
@ -571,7 +575,7 @@ splitTo from0 fromUnk (t0 :| ts) = do
\from0Links (fromUnk', toROLinkedN) (toROUnlinked, toUnk) -> do \from0Links (fromUnk', toROLinkedN) (toROUnlinked, toUnk) -> do
let (from0', toROLinked0) = case from0 of let (from0', toROLinked0) = case from0 of
Left blnk -> (Left (blnk, from0Links), []) Left blnk -> (Left (blnk, from0Links), [])
Right (Left ro) -> (Right $ Left ro, makeRoUE . snd . snd <$> linked0) Right (Left ro) -> (Right $ Left ro, makeRoUE prec . snd . snd <$> linked0)
Right (Right unk) -> (Right $ Right (unk, from0Links), []) Right (Right unk) -> (Right $ Right (unk, from0Links), [])
return (from0', fromUnk', primary, toROLinked0 ++ toROLinkedN ++ toROUnlinked, toUnk) return (from0', fromUnk', primary, toROLinked0 ++ toROLinkedN ++ toROUnlinked, toUnk)
where where
@ -583,10 +587,11 @@ splitTo from0 fromUnk (t0 :| ts) = do
-- sorted according to index and 'fst' respectively. NOTE the output will NOT be -- sorted according to index and 'fst' respectively. NOTE the output will NOT be
-- sorted. -- sorted.
zipPaired zipPaired
:: [UEUnk] :: Precision
-> [UEUnk]
-> [(Int, NonEmpty (EntryRId, EntryR))] -> [(Int, NonEmpty (EntryRId, EntryR))]
-> InsertExcept ([(UEUnk, [UELink])], [UE_RO]) -> InsertExcept ([(UEUnk, [UELink])], [UE_RO])
zipPaired = go ([], []) zipPaired prec = go ([], [])
where where
nolinks = ((,[]) <$>) nolinks = ((,[]) <$>)
go acc fs [] = return $ first (nolinks fs ++) acc go acc fs [] = return $ first (nolinks fs ++) acc
@ -599,7 +604,7 @@ zipPaired = go ([], [])
| otherwise -> (Nothing, rest) | otherwise -> (Nothing, rest)
_ -> (Nothing, rest) _ -> (Nothing, rest)
let acc' = (nolinks lesser ++ facc, tacc) let acc' = (nolinks lesser ++ facc, tacc)
let ros = NE.toList $ makeRoUE . snd <$> tls let ros = NE.toList $ makeRoUE prec . snd <$> tls
let f = maybe (second (++ ros)) (\u -> first (u :)) nextLink let f = maybe (second (++ ros)) (\u -> first (u :)) nextLink
go (f acc') fs' ts go (f acc') fs' ts
@ -619,30 +624,30 @@ makeLinkUnk (k, e) =
maybe maybe
(throwError $ InsertException undefined) (throwError $ InsertException undefined)
(return . makeUE k e . LinkScale) (return . makeUE k e . LinkScale)
$ entryRCachedValue e $ fromRational <$> entryRCachedValue e
splitDeferredValue :: (EntryRId, EntryR) -> InsertExcept (Either UE_RO UEUnk) splitDeferredValue :: Precision -> (EntryRId, EntryR) -> InsertExcept (Either UE_RO UEUnk)
splitDeferredValue p = do splitDeferredValue prec p = do
res <- readDeferredValue p res <- readDeferredValue prec p
case res of case res of
Left _ -> throwError $ InsertException undefined Left _ -> throwError $ InsertException undefined
Right x -> return x Right x -> return x
readDeferredValue :: (EntryRId, EntryR) -> InsertExcept (Either UEBlank (Either UE_RO UEUnk)) readDeferredValue :: Precision -> (EntryRId, EntryR) -> InsertExcept (Either UEBlank (Either UE_RO UEUnk))
readDeferredValue (k, e) = case (entryRCachedValue e, entryRCachedType e) of readDeferredValue prec (k, e) = case (entryRCachedValue e, entryRCachedType e) of
(Nothing, Just TFixed) -> return $ Right $ Left $ makeRoUE e (Nothing, Just TFixed) -> return $ Right $ Left $ makeRoUE prec e
(Just v, Just TBalance) -> go EVBalance v (Just v, Just TBalance) -> go $ fmap EVBalance $ makeUE k e $ realFracToDecimal prec v
(Just v, Just TPercent) -> go EVPercent v (Just v, Just TPercent) -> go $ fmap EVPercent $ makeUE k e $ fromRational v
(Nothing, Nothing) -> return $ Left $ makeUnkUE k e (Nothing, Nothing) -> return $ Left $ makeUnkUE k e
_ -> throwError $ InsertException undefined _ -> throwError $ InsertException undefined
where where
go c = return . Right . Right . fmap c . makeUE k e go = return . Right . Right
makeUE :: i -> EntryR -> v -> UpdateEntry i v makeUE :: i -> EntryR -> v -> UpdateEntry i v
makeUE k e v = UpdateEntry k (entryRAccount e) v (entryRIndex e) makeUE k e v = UpdateEntry k (entryRAccount e) v (entryRIndex e)
makeRoUE :: EntryR -> UpdateEntry () StaticValue makeRoUE :: Precision -> EntryR -> UpdateEntry () StaticValue
makeRoUE e = makeUE () e $ StaticValue (entryRValue e) makeRoUE prec e = makeUE () e $ StaticValue (realFracToDecimal prec $ entryRValue e)
makeUnkUE :: EntryRId -> EntryR -> UpdateEntry EntryRId () makeUnkUE :: EntryRId -> EntryR -> UpdateEntry EntryRId ()
makeUnkUE k e = makeUE k e () makeUnkUE k e = makeUE k e ()
@ -689,15 +694,17 @@ insertEntry
, ieDeferred , ieDeferred
} = } =
do do
ek <- insert $ EntryR k eAcnt eComment eValue i cval ctype deflink ek <- insert $ EntryR k eAcnt eComment (toRational eValue) i cval ctype deflink
mapM_ (insert_ . TagRelationR ek) eTags mapM_ (insert_ . TagRelationR ek) eTags
return ek return ek
where where
(cval, ctype, deflink) = case ieDeferred of (cval, ctype, deflink) = case ieDeferred of
(Just (EntryLinked index scale)) -> (Just scale, Nothing, Just $ fromIntegral index) (Just (DBEntryLinked x s)) -> (Just (toRational s), Nothing, Just $ fromIntegral x)
(Just (EntryBalance target)) -> (Just target, Just TBalance, Nothing) (Just (DBEntryBalance b)) -> (Just (toRational b), Just TBalance, Nothing)
(Just (EntryPercent target)) -> (Just target, Just TPercent, Nothing) (Just (DBEntryPercent p)) -> (Just (toRational p), Just TPercent, Nothing)
Nothing -> (Nothing, Just TFixed, Nothing) Nothing -> (Nothing, Just TFixed, Nothing)
updateTx :: MonadSqlQuery m => UEBalanced -> m () updateTx :: MonadSqlQuery m => UEBalanced -> m ()
updateTx UpdateEntry {ueID, ueValue} = update ueID [EntryRValue =. unStaticValue ueValue] updateTx UpdateEntry {ueID, ueValue} = update ueID [EntryRValue =. v]
where
v = toRational $ unStaticValue ueValue

View File

@ -7,6 +7,7 @@ where
import Control.Monad.Except import Control.Monad.Except
import Data.Csv import Data.Csv
import Data.Decimal
import Data.Foldable import Data.Foldable
import GHC.Real import GHC.Real
import Internal.Database import Internal.Database
@ -93,7 +94,7 @@ parseTxRecord p TxOpts {toDate, toAmountFmt, toDesc, toAmount, toOther, toDateFm
if d == "" if d == ""
then return Nothing then return Nothing
else do else do
a <- parseRational toAmountFmt =<< r .: T.encodeUtf8 toAmount a <- parseDecimal toAmountFmt =<< r .: T.encodeUtf8 toAmount
e <- r .: T.encodeUtf8 toDesc e <- r .: T.encodeUtf8 toDesc
os <- M.fromList <$> mapM (\n -> (n,) <$> r .: T.encodeUtf8 n) toOther os <- M.fromList <$> mapM (\n -> (n,) <$> r .: T.encodeUtf8 n) toOther
d' <- parseTimeM True defaultTimeLocale (T.unpack toDateFmt) d d' <- parseTimeM True defaultTimeLocale (T.unpack toDateFmt) d
@ -276,7 +277,7 @@ matches
then maybe (return MatchSkip) convert spTx then maybe (return MatchSkip) convert spTx
else return MatchFail else return MatchFail
where where
val = valMatches spVal trAmount val = valMatches spVal $ toRational trAmount
date = maybe True (`dateMatches` trDate) spDate date = maybe True (`dateMatches` trDate) spDate
other = foldM (\a o -> (a &&) <$> otherMatches trOther o) True spOther other = foldM (\a o -> (a &&) <$> otherMatches trOther o) True spOther
desc = maybe (return True) (matchMaybe trDesc . snd) spDesc desc = maybe (return True) (matchMaybe trDesc . snd) spDesc
@ -301,8 +302,8 @@ toTx
, txPrimary = , txPrimary =
Left $ Left $
EntrySet EntrySet
{ esTotalValue = roundPrecisionCur cur $ tgScale * fromRational trAmount { esTotalValue = roundTo (cpPrec cur) trAmount *. tgScale
, esCurrency = cur , esCurrency = cpID cur
, esFrom = f , esFrom = f
, esTo = t , esTo = t
} }
@ -314,8 +315,9 @@ toTx
curRes = do curRes = do
m <- askDBState kmCurrency m <- askDBState kmCurrency
cur <- liftInner $ resolveCurrency m r tgCurrency cur <- liftInner $ resolveCurrency m r tgCurrency
let fromRes = liftInner $ resolveHalfEntry resolveFromValue cur r () tgFrom let prec = cpPrec cur
let toRes = liftInner $ resolveHalfEntry resolveToValue cur r () tgTo let fromRes = liftInner $ resolveHalfEntry resolveFromValue prec r () tgFrom
let toRes = liftInner $ resolveHalfEntry resolveToValue prec r () tgTo
combineError fromRes toRes (cur,,) combineError fromRes toRes (cur,,)
subRes = mapErrors (resolveSubGetter r) tgOtherEntries subRes = mapErrors (resolveSubGetter r) tgOtherEntries
@ -327,27 +329,27 @@ resolveSubGetter
resolveSubGetter r TxSubGetter {tsgFrom, tsgTo, tsgValue, tsgCurrency} = do resolveSubGetter r TxSubGetter {tsgFrom, tsgTo, tsgValue, tsgCurrency} = do
m <- askDBState kmCurrency m <- askDBState kmCurrency
cur <- liftInner $ resolveCurrency m r tsgCurrency cur <- liftInner $ resolveCurrency m r tsgCurrency
let toRes = resolveHalfEntry resolveToValue cur r () tsgTo let prec = cpPrec cur
let valRes = liftInner $ fmap (roundPrecisionCur cur) <$> resolveValue r tsgValue let toRes = resolveHalfEntry resolveToValue prec r () tsgTo
let valRes = liftInner $ resolveValue prec r tsgValue
liftInner $ combineErrorM toRes valRes $ \t v -> do liftInner $ combineErrorM toRes valRes $ \t v -> do
f <- resolveHalfEntry resolveFromValue cur r v tsgFrom f <- resolveHalfEntry resolveFromValue prec r v tsgFrom
return $ return $
EntrySet EntrySet
{ esTotalValue = () { esTotalValue = ()
, esCurrency = cur , esCurrency = cpID cur
, esFrom = f , esFrom = f
, esTo = t , esTo = t
} }
resolveHalfEntry resolveHalfEntry
:: Traversable f :: (Precision -> TxRecord -> n -> InsertExcept v')
=> (TxRecord -> n -> InsertExcept (f Double)) -> Precision
-> CurrencyPrec
-> TxRecord -> TxRecord
-> v -> v
-> TxHalfGetter (EntryGetter n) -> TxHalfGetter (EntryGetter n)
-> InsertExcept (HalfEntrySet v (f Rational)) -> InsertExcept (HalfEntrySet v v')
resolveHalfEntry f cur r v TxHalfGetter {thgAcnt, thgComment, thgTags, thgEntries} = resolveHalfEntry f prec r v TxHalfGetter {thgAcnt, thgComment, thgTags, thgEntries} =
combineError acntRes esRes $ \a es -> combineError acntRes esRes $ \a es ->
HalfEntrySet HalfEntrySet
{ hesPrimary = { hesPrimary =
@ -361,7 +363,7 @@ resolveHalfEntry f cur r v TxHalfGetter {thgAcnt, thgComment, thgTags, thgEntrie
} }
where where
acntRes = resolveAcnt r thgAcnt acntRes = resolveAcnt r thgAcnt
esRes = mapErrors (resolveEntry f cur r) thgEntries esRes = mapErrors (resolveEntry f prec r) thgEntries
otherMatches :: M.Map T.Text T.Text -> FieldMatcherRe -> InsertExcept Bool otherMatches :: M.Map T.Text T.Text -> FieldMatcherRe -> InsertExcept Bool
otherMatches dict m = case m of otherMatches dict m = case m of
@ -371,33 +373,33 @@ otherMatches dict m = case m of
lookup_ t n = lookupErr (MatchField t) n dict lookup_ t n = lookupErr (MatchField t) n dict
resolveEntry resolveEntry
:: Traversable f :: (Precision -> TxRecord -> n -> InsertExcept v)
=> (TxRecord -> n -> InsertExcept (f Double)) -> Precision
-> CurrencyPrec
-> TxRecord -> TxRecord
-> EntryGetter n -> EntryGetter n
-> InsertExcept (Entry AcntID (f Rational) TagID) -> InsertExcept (Entry AcntID v TagID)
resolveEntry f cur r s@Entry {eAcnt, eValue} = do resolveEntry f prec r s@Entry {eAcnt, eValue} =
combineError acntRes valRes $ \a v -> combineError acntRes valRes $ \a v -> s {eAcnt = a, eValue = v}
s {eAcnt = a, eValue = roundPrecisionCur cur <$> v}
where where
acntRes = resolveAcnt r eAcnt acntRes = resolveAcnt r eAcnt
valRes = f r eValue valRes = f prec r eValue
resolveFromValue :: TxRecord -> EntryNumGetter -> InsertExcept (EntryValue Double) resolveFromValue :: Precision -> TxRecord -> EntryNumGetter -> InsertExcept EntryValue
resolveFromValue = resolveValue resolveFromValue = resolveValue
resolveToValue :: TxRecord -> LinkedEntryNumGetter -> InsertExcept (LinkDeferred Double) resolveToValue :: Precision -> TxRecord -> LinkedEntryNumGetter -> InsertExcept LinkDeferred
resolveToValue _ (Linked l) = return $ LinkIndex l resolveToValue _ _ (Linked l) = return $ LinkIndex l
resolveToValue r (Getter g) = LinkDeferred <$> resolveValue r g resolveToValue prec r (Getter g) = LinkDeferred <$> resolveValue prec r g
resolveValue :: TxRecord -> EntryNumGetter -> InsertExcept (EntryValue Double) resolveValue :: Precision -> TxRecord -> EntryNumGetter -> InsertExcept EntryValue
resolveValue TxRecord {trOther, trAmount} s = case s of resolveValue prec TxRecord {trOther, trAmount} s = case s of
(LookupN t) -> EntryValue TFixed <$> (readDouble =<< lookupErr EntryValField t trOther) (LookupN t) -> EntryFixed . go <$> (readDouble =<< lookupErr EntryValField t trOther)
(ConstN c) -> return $ EntryValue TFixed c (ConstN c) -> return $ EntryFixed $ go c
AmountN m -> return $ EntryValue TFixed $ m * fromRational trAmount AmountN m -> return $ EntryFixed $ trAmount *. m
BalanceN x -> return $ EntryValue TBalance x BalanceN x -> return $ EntryBalance $ go x
PercentN x -> return $ EntryValue TPercent x PercentN x -> return $ EntryPercent x
where
go = realFracToDecimal prec
resolveAcnt :: TxRecord -> EntryAcnt -> InsertExcept T.Text resolveAcnt :: TxRecord -> EntryAcnt -> InsertExcept T.Text
resolveAcnt = resolveEntryField AcntField resolveAcnt = resolveEntryField AcntField
@ -479,14 +481,41 @@ matchGroupsMaybe q re = case regexec re q of
-- this should never fail as regexec always returns Right -- this should never fail as regexec always returns Right
Left _ -> [] Left _ -> []
parseRational :: MonadFail m => (T.Text, Regex) -> T.Text -> m Rational -- parseRational :: MonadFail m => (T.Text, Regex) -> T.Text -> m Rational
parseRational (pat, re) s = case matchGroupsMaybe s re of -- parseRational (pat, re) s = case matchGroupsMaybe s re of
[sign, x, ""] -> uncurry (*) <$> readWhole sign x -- [sign, x, ""] -> uncurry (*) <$> readWhole sign x
-- [sign, x, y] -> do
-- d <- readT "decimal" y
-- let p = 10 ^ T.length y
-- (k, w) <- readWhole sign x
-- return $ k * (w + d % p)
-- _ -> msg "malformed decimal"
-- where
-- readT what t = case readMaybe $ T.unpack t of
-- Just d -> return $ fromInteger d
-- _ -> msg $ T.unwords ["could not parse", what, singleQuote t]
-- msg :: MonadFail m => T.Text -> m a
-- msg m =
-- fail $
-- T.unpack $
-- T.unwords [m, "-", keyVals [("pattern", pat), ("query", s)]]
-- readSign x
-- | x == "-" = return (-1)
-- | x == "+" || x == "" = return 1
-- | otherwise = msg $ T.append "invalid sign: " x
-- readWhole sign x = do
-- w <- readT "whole number" x
-- k <- readSign sign
-- return (k, w)
parseDecimal :: MonadFail m => (T.Text, Regex) -> T.Text -> m Decimal
parseDecimal (pat, re) s = case matchGroupsMaybe s re of
[sign, x, ""] -> Decimal 0 . uncurry (*) <$> readWhole sign x
[sign, x, y] -> do [sign, x, y] -> do
d <- readT "decimal" y d <- readT "decimal" y
let p = 10 ^ T.length y let p = T.length y
(k, w) <- readWhole sign x (k, w) <- readWhole sign x
return $ k * (w + d % p) return $ Decimal (fromIntegral p) (k * (w * (10 ^ p) + d))
_ -> msg "malformed decimal" _ -> msg "malformed decimal"
where where
readT what t = case readMaybe $ T.unpack t of readT what t = case readMaybe $ T.unpack t of

View File

@ -12,6 +12,7 @@ module Internal.Types.Main
where where
import Control.Monad.Except import Control.Monad.Except
import Data.Decimal
import Database.Persist.Sql hiding (Desc, In, Statement) import Database.Persist.Sql hiding (Desc, In, Statement)
import Dhall hiding (embed, maybe) import Dhall hiding (embed, maybe)
import Internal.Types.Database import Internal.Types.Database
@ -36,7 +37,7 @@ data ConfigHashes = ConfigHashes
type AccountMap = M.Map AcntID (AccountRId, AcntSign, AcntType) type AccountMap = M.Map AcntID (AccountRId, AcntSign, AcntType)
data CurrencyPrec = CurrencyPrec {cpID :: !CurrencyRId, cpPrec :: !Natural} data CurrencyPrec = CurrencyPrec {cpID :: !CurrencyRId, cpPrec :: !Precision}
deriving (Show) deriving (Show)
type CurrencyMap = M.Map CurID CurrencyPrec type CurrencyMap = M.Map CurID CurrencyPrec
@ -64,17 +65,15 @@ data DBUpdates = DBUpdates
type CurrencyM = Reader CurrencyMap type CurrencyM = Reader CurrencyMap
-- type DeferredKeyEntry = Entry AccountRId (Deferred Rational) CurrencyRId TagRId
data DBDeferred data DBDeferred
= EntryLinked Natural Rational = DBEntryLinked Natural Double
| EntryBalance Rational | DBEntryBalance Decimal
| EntryPercent Rational | DBEntryPercent Double
data ReadEntry = ReadEntry data ReadEntry = ReadEntry
{ reCurrency :: !CurrencyRId { reCurrency :: !CurrencyRId
, reAcnt :: !AccountRId , reAcnt :: !AccountRId
, reValue :: !Rational , reValue :: !Decimal
, reDate :: !Day , reDate :: !Day
, rePriority :: !Int , rePriority :: !Int
, reBudget :: !T.Text , reBudget :: !T.Text
@ -93,16 +92,15 @@ data CurrencyRound = CurrencyRound CurID Natural
deriving instance Functor (UpdateEntry i) deriving instance Functor (UpdateEntry i)
newtype LinkScale = LinkScale {unLinkScale :: Rational} type Precision = Word8
newtype LinkScale = LinkScale {unLinkScale :: Decimal}
deriving newtype (Num, Show) deriving newtype (Num, Show)
-- newtype BalanceTarget = BalanceTarget {unBalanceTarget :: Rational} newtype StaticValue = StaticValue {unStaticValue :: Decimal}
-- deriving newtype (Num)
newtype StaticValue = StaticValue {unStaticValue :: Rational}
deriving newtype (Num, Show) deriving newtype (Num, Show)
data EntryValueUnk = EVBalance Rational | EVPercent Rational deriving (Show) data EntryValueUnk = EVBalance Decimal | EVPercent Double deriving (Show)
type UEUnk = UpdateEntry EntryRId EntryValueUnk type UEUnk = UpdateEntry EntryRId EntryValueUnk
@ -121,7 +119,7 @@ data UpdateEntrySet f t = UpdateEntrySet
, utToUnk :: ![UEUnk] , utToUnk :: ![UEUnk]
, utFromRO :: ![UE_RO] , utFromRO :: ![UE_RO]
, utToRO :: ![UE_RO] , utToRO :: ![UE_RO]
, utCurrency :: !(CurrencyRId, Natural) , utCurrency :: !CurrencyRId
, utDate :: !Day , utDate :: !Day
, utTotalValue :: !t , utTotalValue :: !t
, utBudget :: !T.Text , utBudget :: !T.Text
@ -129,7 +127,7 @@ data UpdateEntrySet f t = UpdateEntrySet
} }
deriving (Show) deriving (Show)
type TotalUpdateEntrySet = UpdateEntrySet (UEBlank, [UELink]) Rational type TotalUpdateEntrySet = UpdateEntrySet (UEBlank, [UELink]) Decimal
type FullUpdateEntrySet = UpdateEntrySet (Either UE_RO (UEUnk, [UELink])) () type FullUpdateEntrySet = UpdateEntrySet (Either UE_RO (UEUnk, [UELink])) ()
@ -171,7 +169,7 @@ data AcntPath = AcntPath
data TxRecord = TxRecord data TxRecord = TxRecord
{ trDate :: !Day { trDate :: !Day
, trAmount :: !Rational , trAmount :: !Decimal
, trDesc :: !T.Text , trDesc :: !T.Text
, trOther :: !(M.Map T.Text T.Text) , trOther :: !(M.Map T.Text T.Text)
, trFile :: !FilePath , trFile :: !FilePath
@ -211,7 +209,7 @@ data HalfEntrySet v0 vN = HalfEntrySet
data EntrySet v0 vp0 vpN vtN = EntrySet data EntrySet v0 vp0 vpN vtN = EntrySet
{ esTotalValue :: !v0 { esTotalValue :: !v0
, esCurrency :: !CurrencyPrec , esCurrency :: !CurrencyRId
, esFrom :: !(HalfEntrySet vp0 vpN) , esFrom :: !(HalfEntrySet vp0 vpN)
, esTo :: !(HalfEntrySet () vtN) , esTo :: !(HalfEntrySet () vtN)
} }
@ -221,25 +219,13 @@ type TotalEntrySet v0 vpN vtN = EntrySet v0 () vpN vtN
type FullEntrySet vp0 vpN vtN = EntrySet () vp0 vpN vtN type FullEntrySet vp0 vpN vtN = EntrySet () vp0 vpN vtN
type PrimaryEntrySet = type PrimaryEntrySet = TotalEntrySet Decimal EntryValue LinkDeferred
TotalEntrySet
Rational
(EntryValue Rational)
(LinkDeferred Rational)
type SecondayEntrySet = type SecondayEntrySet = FullEntrySet EntryValue EntryValue LinkDeferred
FullEntrySet
(EntryValue Rational)
(EntryValue Rational)
(LinkDeferred Rational)
type TransferEntrySet = SecondayEntrySet type TransferEntrySet = SecondayEntrySet
type ShadowEntrySet = type ShadowEntrySet = TotalEntrySet Double EntryValue LinkDeferred
TotalEntrySet
Double
(EntryValue Rational)
(LinkDeferred Rational)
data TxCommit = HistoryCommit CommitR | BudgetCommit CommitR T.Text data TxCommit = HistoryCommit CommitR | BudgetCommit CommitR T.Text
deriving (Eq, Ord, Show) deriving (Eq, Ord, Show)
@ -257,7 +243,7 @@ data Tx k = Tx
data InsertEntry = InsertEntry data InsertEntry = InsertEntry
{ ieDeferred :: !(Maybe DBDeferred) { ieDeferred :: !(Maybe DBDeferred)
, ieEntry :: !(Entry AccountRId Rational TagRId) , ieEntry :: !(Entry AccountRId Decimal TagRId)
} }
data InsertEntrySet = InsertEntrySet data InsertEntrySet = InsertEntrySet
@ -279,17 +265,16 @@ data InsertTx = InsertTx
data Deferred a = Deferred Bool a data Deferred a = Deferred Bool a
deriving (Show, Functor, Foldable, Traversable) deriving (Show, Functor, Foldable, Traversable)
data EntryValue a = EntryValue TransferType a data EntryValue_ a = EntryValue_ TransferType a
deriving (Show, Functor, Foldable, Traversable) deriving (Show, Functor, Foldable, Traversable)
data LinkDeferred a data EntryValue = EntryFixed Decimal | EntryPercent Double | EntryBalance Decimal
= LinkDeferred (EntryValue a) deriving (Show, Eq, Ord)
data LinkDeferred
= LinkDeferred EntryValue
| LinkIndex LinkedNumGetter | LinkIndex LinkedNumGetter
deriving (Show, Functor, Traversable, Foldable) deriving (Show)
-- type RawEntry = Entry AcntID (Deferred Rational) CurID TagID
-- type RawFromEntry = Entry AcntID (Deferred Rational) CurID TagID
data MatchRes a = MatchPass !a | MatchFail | MatchSkip data MatchRes a = MatchPass !a | MatchFail | MatchSkip

View File

@ -39,8 +39,6 @@ module Internal.Utils
, xGregToDay , xGregToDay
, dateMatches , dateMatches
, valMatches , valMatches
, roundPrecision
, roundPrecisionCur
, lookupAccount , lookupAccount
, lookupAccountKey , lookupAccountKey
, lookupAccountSign , lookupAccountSign
@ -63,6 +61,7 @@ where
import Control.Monad.Error.Class import Control.Monad.Error.Class
import Control.Monad.Except import Control.Monad.Except
import Data.Decimal
import Data.Time.Format.ISO8601 import Data.Time.Format.ISO8601
import GHC.Real import GHC.Real
import Internal.Types.Main import Internal.Types.Main
@ -415,13 +414,13 @@ fmtRational precision x = T.concat [s, txt n', ".", pad 2 "0" $ txt d']
txt = T.pack . show txt = T.pack . show
pad i c z = T.append (T.replicate (i - T.length z) c) z pad i c z = T.append (T.replicate (i - T.length z) c) z
roundPrecision :: Natural -> Double -> Rational -- roundPrecision :: Natural -> Double -> Rational
roundPrecision n = (% p) . round . (* fromIntegral p) . toRational -- roundPrecision n = (% p) . round . (* fromIntegral p) . toRational
where -- where
p = 10 ^ n -- p = 10 ^ n
roundPrecisionCur :: CurrencyPrec -> Double -> Rational -- roundPrecisionCur :: CurrencyPrec -> Double -> Rational
roundPrecisionCur (CurrencyPrec _ n) = roundPrecision n -- roundPrecisionCur (CurrencyPrec _ n) = roundPrecision n
acntPath2Text :: AcntPath -> T.Text acntPath2Text :: AcntPath -> T.Text
acntPath2Text (AcntPath t cs) = T.intercalate "/" (atName t : cs) acntPath2Text (AcntPath t cs) = T.intercalate "/" (atName t : cs)
@ -525,7 +524,7 @@ showTx TxRecord {trDate = d, trAmount = v, trDesc = e, trFile = f} =
keyVals keyVals
[ ("path", T.pack f) [ ("path", T.pack f)
, ("date", T.pack $ iso8601Show d) , ("date", T.pack $ iso8601Show d)
, ("value", showT (fromRational v :: Float)) , ("value", showT v)
, ("description", doubleQuote e) , ("description", doubleQuote e)
] ]
@ -663,7 +662,7 @@ lookupCurrency = lookupFinance CurField kmCurrency
lookupCurrencyKey :: (MonadInsertError m, MonadFinance m) => AcntID -> m CurrencyRId lookupCurrencyKey :: (MonadInsertError m, MonadFinance m) => AcntID -> m CurrencyRId
lookupCurrencyKey = fmap cpID . lookupCurrency lookupCurrencyKey = fmap cpID . lookupCurrency
lookupCurrencyPrec :: (MonadInsertError m, MonadFinance m) => AcntID -> m Natural lookupCurrencyPrec :: (MonadInsertError m, MonadFinance m) => AcntID -> m Precision
lookupCurrencyPrec = fmap cpPrec . lookupCurrency lookupCurrencyPrec = fmap cpPrec . lookupCurrency
lookupTag :: (MonadInsertError m, MonadFinance m) => TagID -> m TagRId lookupTag :: (MonadInsertError m, MonadFinance m) => TagID -> m TagRId
@ -695,7 +694,7 @@ balanceTxs ebs =
go (ToInsert Tx {txPrimary, txOther, txDescr, txCommit, txDate, txBudget, txPriority}) = do go (ToInsert Tx {txPrimary, txOther, txDescr, txCommit, txDate, txBudget, txPriority}) = do
e <- either (balancePrimaryEntrySet txBudget) (balanceSecondaryEntrySet txBudget) txPrimary e <- either (balancePrimaryEntrySet txBudget) (balanceSecondaryEntrySet txBudget) txPrimary
let tot = sum $ fmap (eValue . ieEntry) $ NE.toList $ iesFromEntries e let tot = sum $ fmap (eValue . ieEntry) $ NE.toList $ iesFromEntries e
es <- mapErrors (either (balanceSecondaryEntrySet txBudget) (balancePrimaryEntrySet txBudget . fromShadow tot)) txOther es <- mapErrors (goOther tot) txOther
let tx = let tx =
-- TODO this is lame -- TODO this is lame
InsertTx InsertTx
@ -707,7 +706,12 @@ balanceTxs ebs =
, itxPriority = txPriority , itxPriority = txPriority
} }
return $ Just $ Right tx return $ Just $ Right tx
fromShadow tot e@EntrySet {esTotalValue} = e {esTotalValue = tot * toRational esTotalValue} where
goOther tot =
either
(balanceSecondaryEntrySet txBudget)
(balancePrimaryEntrySet txBudget . fromShadow tot)
fromShadow tot e@EntrySet {esTotalValue} = e {esTotalValue = tot *. esTotalValue}
binDate :: EntryBin -> (Day, Int) binDate :: EntryBin -> (Day, Int)
binDate (ToRead ReadEntry {reDate, rePriority}) = (reDate, rePriority) binDate (ToRead ReadEntry {reDate, rePriority}) = (reDate, rePriority)
@ -720,7 +724,7 @@ type BCKey = (CurrencyRId, Text)
type ABCKey = (AccountRId, BCKey) type ABCKey = (AccountRId, BCKey)
type EntryBals = M.Map ABCKey Rational type EntryBals = M.Map ABCKey Decimal
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- rebalancing -- rebalancing
@ -735,19 +739,19 @@ rebalanceTotalEntrySet
, utToUnk , utToUnk
, utFromRO , utFromRO
, utToRO , utToRO
, utCurrency = (curID, precision) , utCurrency
, utTotalValue , utTotalValue
, utBudget , utBudget
} = } =
do do
(fval, fs, tpairs) <- rebalanceDebit bc precision utFromRO utFromUnk (fval, fs, tpairs) <- rebalanceDebit bc utFromRO utFromUnk
let f0val = utTotalValue - fval let f0val = utTotalValue - fval
modify $ mapAdd_ (f0Acnt, bc) f0val modify $ mapAdd_ (f0Acnt, bc) f0val
let tsLinked = tpairs ++ (unlink f0val <$> f0links) let tsLinked = tpairs ++ (unlink f0val <$> f0links)
ts <- rebalanceCredit bc precision utTotalValue utTo0 utToUnk utToRO tsLinked ts <- rebalanceCredit bc utTotalValue utTo0 utToUnk utToRO tsLinked
return (f0 {ueValue = StaticValue f0val} : fs ++ ts) return (f0 {ueValue = StaticValue f0val} : fs ++ ts)
where where
bc = (curID, utBudget) bc = (utCurrency, utBudget)
rebalanceFullEntrySet :: FullUpdateEntrySet -> State EntryBals [UEBalanced] rebalanceFullEntrySet :: FullUpdateEntrySet -> State EntryBals [UEBalanced]
rebalanceFullEntrySet rebalanceFullEntrySet
@ -758,26 +762,25 @@ rebalanceFullEntrySet
, utToUnk , utToUnk
, utFromRO , utFromRO
, utToRO , utToRO
, utCurrency = (curID, precision) , utCurrency
, utBudget , utBudget
} = } =
do do
(ftot, fs, tpairs) <- rebalanceDebit bc precision rs ls (ftot, fs, tpairs) <- rebalanceDebit bc rs ls
ts <- rebalanceCredit bc precision ftot utTo0 utToUnk utToRO tpairs ts <- rebalanceCredit bc ftot utTo0 utToUnk utToRO tpairs
return (fs ++ ts) return (fs ++ ts)
where where
(rs, ls) = case utFrom0 of (rs, ls) = case utFrom0 of
Left x -> (x : utFromRO, utFromUnk) Left x -> (x : utFromRO, utFromUnk)
Right x -> (utFromRO, x : utFromUnk) Right x -> (utFromRO, x : utFromUnk)
bc = (curID, utBudget) bc = (utCurrency, utBudget)
rebalanceDebit rebalanceDebit
:: BCKey :: BCKey
-> Natural
-> [UE_RO] -> [UE_RO]
-> [(UEUnk, [UELink])] -> [(UEUnk, [UELink])]
-> State EntryBals (Rational, [UEBalanced], [UEBalanced]) -> State EntryBals (Decimal, [UEBalanced], [UEBalanced])
rebalanceDebit k precision ro linked = do rebalanceDebit k ro linked = do
(tot, (tpairs, fs)) <- (tot, (tpairs, fs)) <-
fmap (second (partitionEithers . concat)) $ fmap (second (partitionEithers . concat)) $
sumM goFrom $ sumM goFrom $
@ -788,24 +791,23 @@ rebalanceDebit k precision ro linked = do
idx = either ueIndex (ueIndex . fst) idx = either ueIndex (ueIndex . fst)
goFrom (Left e) = (,[]) <$> updateFixed k e goFrom (Left e) = (,[]) <$> updateFixed k e
goFrom (Right (e0, es)) = do goFrom (Right (e0, es)) = do
v <- updateUnknown precision k e0 v <- updateUnknown k e0
let e0' = Right $ e0 {ueValue = StaticValue v} let e0' = Right $ e0 {ueValue = StaticValue v}
let es' = Left . unlink v <$> es let es' = Left . unlink v <$> es
return (v, e0' : es') return (v, e0' : es')
unlink :: Rational -> UELink -> UEBalanced unlink :: Decimal -> UELink -> UEBalanced
unlink v e = e {ueValue = StaticValue $ (-v) * unLinkScale (ueValue e)} unlink v e = e {ueValue = StaticValue $ (-v) * unLinkScale (ueValue e)}
rebalanceCredit rebalanceCredit
:: BCKey :: BCKey
-> Natural -> Decimal
-> Rational
-> UEBlank -> UEBlank
-> [UEUnk] -> [UEUnk]
-> [UE_RO] -> [UE_RO]
-> [UEBalanced] -> [UEBalanced]
-> State EntryBals [UEBalanced] -> State EntryBals [UEBalanced]
rebalanceCredit k precision tot t0 us rs bs = do rebalanceCredit k tot t0 us rs bs = do
(tval, ts) <- (tval, ts) <-
fmap (second catMaybes) $ fmap (second catMaybes) $
sumM goTo $ sumM goTo $
@ -819,7 +821,7 @@ rebalanceCredit k precision tot t0 us rs bs = do
goTo (UETReadOnly e) = (,Nothing) <$> updateFixed k e goTo (UETReadOnly e) = (,Nothing) <$> updateFixed k e
goTo (UETLinked e) = (,Just e) <$> updateFixed k e goTo (UETLinked e) = (,Just e) <$> updateFixed k e
goTo (UETUnk e) = do goTo (UETUnk e) = do
v <- updateUnknown precision k e v <- updateUnknown k e
return (v, Just $ e {ueValue = StaticValue v}) return (v, Just $ e {ueValue = StaticValue v})
data UpdateEntryType a b data UpdateEntryType a b
@ -832,18 +834,18 @@ projectUET f _ _ (UETReadOnly e) = f e
projectUET _ f _ (UETUnk e) = f e projectUET _ f _ (UETUnk e) = f e
projectUET _ _ f (UETLinked p) = f p projectUET _ _ f (UETLinked p) = f p
updateFixed :: BCKey -> UpdateEntry i StaticValue -> State EntryBals Rational updateFixed :: BCKey -> UpdateEntry i StaticValue -> State EntryBals Decimal
updateFixed k e = do updateFixed k e = do
let v = unStaticValue $ ueValue e let v = unStaticValue $ ueValue e
modify $ mapAdd_ (ueAcnt e, k) v modify $ mapAdd_ (ueAcnt e, k) v
return v return v
updateUnknown :: Natural -> BCKey -> UpdateEntry i EntryValueUnk -> State EntryBals Rational updateUnknown :: BCKey -> UpdateEntry i EntryValueUnk -> State EntryBals Decimal
updateUnknown precision k e = do updateUnknown k e = do
let key = (ueAcnt e, k) let key = (ueAcnt e, k)
curBal <- gets (M.findWithDefault 0 key) curBal <- gets (M.findWithDefault 0 key)
let v = roundPrecision precision $ fromRational $ case ueValue e of let v = case ueValue e of
EVPercent p -> p * curBal EVPercent p -> curBal *. p
EVBalance p -> p - curBal EVBalance p -> p - curBal
modify $ mapAdd_ key v modify $ mapAdd_ key v
return v return v
@ -861,7 +863,7 @@ balancePrimaryEntrySet
EntrySet EntrySet
{ esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs} { esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs}
, esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts} , esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts}
, esCurrency = CurrencyPrec {cpID = curID, cpPrec = precision} , esCurrency
, esTotalValue , esTotalValue
} = } =
do do
@ -869,12 +871,12 @@ balancePrimaryEntrySet
let t0res = resolveAcntAndTags t0 let t0res = resolveAcntAndTags t0
let fsres = mapErrors resolveAcntAndTags fs let fsres = mapErrors resolveAcntAndTags fs
let tsres = mapErrors resolveAcntAndTags ts let tsres = mapErrors resolveAcntAndTags ts
let bc = (curID, budgetName) let bc = (esCurrency, budgetName)
combineErrorM (combineError f0res fsres (,)) (combineError t0res tsres (,)) $ combineErrorM (combineError f0res fsres (,)) (combineError t0res tsres (,)) $
\(f0', fs') (t0', ts') -> do \(f0', fs') (t0', ts') -> do
let balFrom = fmap liftInnerS . balanceDeferred precision let balFrom = fmap liftInnerS . balanceDeferred
fs'' <- doEntries balFrom bc esTotalValue f0' fs' fs'' <- doEntries balFrom bc esTotalValue f0' fs'
balanceFinal bc (-esTotalValue) precision fs'' t0' ts' balanceFinal bc (-esTotalValue) fs'' t0' ts'
balanceSecondaryEntrySet balanceSecondaryEntrySet
:: (MonadInsertError m, MonadFinance m) :: (MonadInsertError m, MonadFinance m)
@ -886,7 +888,7 @@ balanceSecondaryEntrySet
EntrySet EntrySet
{ esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs} { esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs}
, esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts} , esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts}
, esCurrency = CurrencyPrec {cpID = curID, cpPrec = precision} , esCurrency
} = } =
do do
let fsRes = mapErrors resolveAcntAndTags (f0 :| fs) let fsRes = mapErrors resolveAcntAndTags (f0 :| fs)
@ -895,24 +897,23 @@ balanceSecondaryEntrySet
combineErrorM fsRes (combineError t0Res tsRes (,)) $ \fs' (t0', ts') -> do combineErrorM fsRes (combineError t0Res tsRes (,)) $ \fs' (t0', ts') -> do
fs'' <- mapErrors balFrom fs' fs'' <- mapErrors balFrom fs'
let tot = entrySum (NE.toList fs'') let tot = entrySum (NE.toList fs'')
balanceFinal bc (-tot) precision fs'' t0' ts' balanceFinal bc (-tot) fs'' t0' ts'
where where
entrySum = sum . fmap (eValue . ieEntry) entrySum = sum . fmap (eValue . ieEntry)
balFrom = balanceEntry (fmap liftInnerS . balanceDeferred precision) bc balFrom = balanceEntry (fmap liftInnerS . balanceDeferred) bc
bc = (curID, budgetName) bc = (esCurrency, budgetName)
balanceFinal balanceFinal
:: (MonadInsertError m) :: (MonadInsertError m)
=> BCKey => BCKey
-> Rational -> Decimal
-> Natural
-> NonEmpty InsertEntry -> NonEmpty InsertEntry
-> Entry (AccountRId, AcntSign) () TagRId -> Entry (AccountRId, AcntSign) () TagRId
-> [Entry (AccountRId, AcntSign) (LinkDeferred Rational) TagRId] -> [Entry (AccountRId, AcntSign) LinkDeferred TagRId]
-> StateT EntryBals m InsertEntrySet -> StateT EntryBals m InsertEntrySet
balanceFinal k@(curID, _) tot precision fs t0 ts = do balanceFinal k@(curID, _) tot fs t0 ts = do
let fv = V.fromList $ NE.toList $ fmap (eValue . ieEntry) fs let fv = V.fromList $ NE.toList $ fmap (eValue . ieEntry) fs
let balTo = balanceLinked fv precision let balTo = balanceLinked fv
ts' <- doEntries balTo k tot t0 ts ts' <- doEntries balTo k tot t0 ts
return $ return $
InsertEntrySet InsertEntrySet
@ -923,9 +924,9 @@ balanceFinal k@(curID, _) tot precision fs t0 ts = do
doEntries doEntries
:: (MonadInsertError m) :: (MonadInsertError m)
=> (ABCKey -> v -> StateT EntryBals m (Rational, Maybe DBDeferred)) => (ABCKey -> v -> StateT EntryBals m (Decimal, Maybe DBDeferred))
-> BCKey -> BCKey
-> Rational -> Decimal
-> Entry (AccountRId, AcntSign) () TagRId -> Entry (AccountRId, AcntSign) () TagRId
-> [Entry (AccountRId, AcntSign) v TagRId] -> [Entry (AccountRId, AcntSign) v TagRId]
-> StateT EntryBals m (NonEmpty InsertEntry) -> StateT EntryBals m (NonEmpty InsertEntry)
@ -949,39 +950,34 @@ liftInnerS = mapStateT (return . runIdentity)
balanceLinked balanceLinked
:: MonadInsertError m :: MonadInsertError m
=> Vector Rational => Vector Decimal
-> Natural
-> ABCKey -> ABCKey
-> LinkDeferred Rational -> LinkDeferred
-> StateT EntryBals m (Rational, Maybe DBDeferred) -> StateT EntryBals m (Decimal, Maybe DBDeferred)
balanceLinked from precision k lg = case lg of balanceLinked from k lg = case lg of
(LinkIndex LinkedNumGetter {lngIndex, lngScale}) -> do (LinkIndex LinkedNumGetter {lngIndex, lngScale}) -> do
let res = fmap (go lngScale) $ from V.!? fromIntegral lngIndex let res = fmap (go lngScale) $ from V.!? fromIntegral lngIndex
case res of case res of
Just v -> return (v, Just $ EntryLinked lngIndex $ toRational lngScale) Just v -> return (v, Just $ DBEntryLinked lngIndex lngScale)
-- TODO this error would be much more informative if I had access to the -- TODO this error would be much more informative if I had access to the
-- file from which it came -- file from which it came
Nothing -> throwError undefined Nothing -> throwError undefined
(LinkDeferred d) -> liftInnerS $ balanceDeferred precision k d (LinkDeferred d) -> liftInnerS $ balanceDeferred k d
where where
go s = negate . roundPrecision precision . (* s) . fromRational go s = negate . (*. s)
balanceDeferred balanceDeferred :: ABCKey -> EntryValue -> State EntryBals (Decimal, Maybe DBDeferred)
:: Natural balanceDeferred k e = do
-> ABCKey newval <- findBalance k e
-> EntryValue Rational let d = case e of
-> State EntryBals (Rational, Maybe DBDeferred) EntryFixed _ -> Nothing
balanceDeferred prec k (EntryValue t v) = do EntryBalance v -> Just $ DBEntryBalance v
newval <- findBalance prec k t v EntryPercent v -> Just $ DBEntryPercent v
let d = case t of
TFixed -> Nothing
TBalance -> Just $ EntryBalance v
TPercent -> Just $ EntryPercent v
return (newval, d) return (newval, d)
balanceEntry balanceEntry
:: (MonadInsertError m) :: (MonadInsertError m)
=> (ABCKey -> v -> StateT EntryBals m (Rational, Maybe DBDeferred)) => (ABCKey -> v -> StateT EntryBals m (Decimal, Maybe DBDeferred))
-> BCKey -> BCKey
-> Entry (AccountRId, AcntSign) v TagRId -> Entry (AccountRId, AcntSign) v TagRId
-> StateT EntryBals m InsertEntry -> StateT EntryBals m InsertEntry
@ -1005,18 +1001,13 @@ resolveAcntAndTags e@Entry {eAcnt, eTags} = do
combineError acntRes tagRes $ combineError acntRes tagRes $
\(acntID, sign, _) tags -> e {eAcnt = (acntID, sign), eTags = tags} \(acntID, sign, _) tags -> e {eAcnt = (acntID, sign), eTags = tags}
findBalance findBalance :: ABCKey -> EntryValue -> State EntryBals Decimal
:: Natural findBalance k e = do
-> ABCKey
-> TransferType
-> Rational
-> State EntryBals Rational
findBalance prec k t v = do
curBal <- gets (M.findWithDefault 0 k) curBal <- gets (M.findWithDefault 0 k)
return $ roundPrecision prec $ fromRational $ case t of return $ case e of
TBalance -> v - curBal EntryBalance b -> b - curBal
TPercent -> v * curBal EntryPercent p -> curBal *. p
TFixed -> v EntryFixed v -> v
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- transfers -- transfers
@ -1047,14 +1038,20 @@ expandTransfer tc name bounds Transfer {transAmounts, transTo, transCurrency, tr
, amtValue = TransferValue {tvVal = v, tvType = t} , amtValue = TransferValue {tvVal = v, tvType = t}
, amtDesc = desc , amtDesc = desc
, amtPriority = pri , amtPriority = pri
} = } = do
withDates bounds pat $ \day -> do cp <- lookupCurrency transCurrency
p <- entryPair transFrom transTo transCurrency desc () (EntryValue t (toRational (-v))) let v' = (-v)
let dec = realFracToDecimal (cpPrec cp) v'
let v'' = case t of
TFixed -> EntryFixed dec
TPercent -> EntryPercent v'
TBalance -> EntryBalance dec
withDates bounds pat $ \day ->
return return
Tx Tx
{ txCommit = tc { txCommit = tc
, txDate = day , txDate = day
, txPrimary = Right p , txPrimary = Right $ entryPair transFrom transTo (cpID cp) desc () v''
, txOther = [] , txOther = []
, txDescr = desc , txDescr = desc
, txBudget = name , txBudget = name
@ -1062,23 +1059,20 @@ expandTransfer tc name bounds Transfer {transAmounts, transTo, transCurrency, tr
} }
entryPair entryPair
:: (MonadInsertError m, MonadFinance m) :: TaggedAcnt
=> TaggedAcnt
-> TaggedAcnt -> TaggedAcnt
-> CurID -> CurrencyRId
-> T.Text -> T.Text
-> v0 -> v0
-> v1 -> v1
-> m (EntrySet v0 v1 v2 v3) -> EntrySet v0 v1 v2 v3
entryPair (TaggedAcnt fa fts) (TaggedAcnt ta tts) curid com totval val1 = do entryPair (TaggedAcnt fa fts) (TaggedAcnt ta tts) curid com totval val1 =
cp <- lookupCurrency curid EntrySet
return $ { esCurrency = curid
EntrySet , esTotalValue = totval
{ esCurrency = cp , esFrom = halfEntry fa fts val1
, esTotalValue = totval , esTo = halfEntry ta tts ()
, esFrom = halfEntry fa fts val1 }
, esTo = halfEntry ta tts ()
}
where where
halfEntry :: AcntID -> [TagID] -> v -> HalfEntrySet v v0 halfEntry :: AcntID -> [TagID] -> v -> HalfEntrySet v v0
halfEntry a ts v = halfEntry a ts v =

View File

@ -87,6 +87,7 @@ dependencies:
- filepath - filepath
- mtl - mtl
- persistent-mtl >= 0.3.0.0 - persistent-mtl >= 0.3.0.0
- Decimal >= 0.5.2
library: library:
source-dirs: lib/ source-dirs: lib/