ENH use sane types to simplify tx balancing

This commit is contained in:
Nathan Dwarshuis 2023-06-16 22:05:28 -04:00
parent 6926003c46
commit ad0975aba7
6 changed files with 411 additions and 264 deletions

View File

@ -396,31 +396,21 @@ let FieldMatcher_ =
let FieldMatcher = FieldMatcher_ Text let FieldMatcher = FieldMatcher_ Text
let FromEntryNumGetter = let EntryNumGetter =
{- {-
Means to get a numeric value from a statement row. Means to get a numeric value from a statement row.
FLookupN: lookup the value from a field LookupN: lookup the value from a field
FConstN: a constant value ConstN: a constant value
FAmountN: the value of the 'Amount' column AmountN: the value of the 'Amount' column
FBalanceN: the amount required to make the target account reach a balance BalanceN: the amount required to make the target account reach a balance
-} -}
< FLookupN : Text < LookupN : Text
| FConstN : Double | ConstN : Double
| FAmountN : Double | AmountN : Double
| FBalanceN : Double | BalanceN : Double
> >
let ToEntryNumGetter =
{-
Means to get a numeric value from a statement row.
TLookupN: lookup the value from a field
TConstN: a constant value
TAmountN: the value of the 'Amount' column
-}
< TLookupN : Text | TConstN : Double | TAmountN : Double >
let EntryTextGetter = let EntryTextGetter =
{- {-
Means to get a textual value from a statement row. Means to get a textual value from a statement row.
@ -458,7 +448,6 @@ let Entry =
-} -}
\(a : Type) -> \(a : Type) ->
\(v : Type) -> \(v : Type) ->
\(c : Type) ->
\(t : Type) -> \(t : Type) ->
{ eAcnt : { eAcnt :
{- {-
@ -470,11 +459,6 @@ let Entry =
Pertains to value for this entry. Pertains to value for this entry.
-} -}
v v
, eCurrency :
{-
Pertains to value for this entry.
-}
c
, eComment : , eComment :
{- {-
A short description of this entry (if none, use a blank string) A short description of this entry (if none, use a blank string)
@ -487,31 +471,65 @@ let Entry =
List t List t
} }
let FromEntryGetter = let EntryGetter =
{- {-
Means for getting an entry from a given row in a statement to apply to the Means for getting an entry from a given row in a statement
credit side of the transaction.
-} -}
{ Type = Entry EntryAcntGetter FromEntryNumGetter EntryCurGetter TagID { Type = Entry EntryAcntGetter EntryNumGetter TagID
, default = { eValue = None FromEntryNumGetter, eComment = "" } , default = { eValue = None EntryNumGetter, eComment = "" }
} }
let ToEntryGetter = let TxSubGetter =
{- {-
Means for getting an entry from a given row in a statement to apply to the A means for transforming one row in a statement to a transaction
debit side of the transaction.
-} -}
{ Type = { tsgFromAcnt :
Entry EntryAcntGetter (Optional ToEntryNumGetter) EntryCurGetter TagID {-
, default = { eValue = None ToEntryNumGetter, eComment = "" } -}
EntryAcntGetter
, tsgToAcnt :
{-
-}
EntryAcntGetter
, tsgValue :
{-
-}
EntryNumGetter
, tsgCurrency :
{-
-}
EntryCurGetter
, tsgFromEntries :
{-
-}
List EntryGetter.Type
, tsgToEntries :
{-
A means of getting entries for this transaction (minimum 1)
-}
List EntryGetter.Type
} }
let TxGetter = let TxGetter =
{- {-
A means for transforming one row in a statement to a transaction A means for transforming one row in a statement to a transaction
Note that N-1 entries need to be specified to make a transaction, as the At least two entries must be made for any given transaction. Below these
Nth entry will be balanced with the others. correspond to the 'from' and 'to' accounts, which will share a single value
(whose positive is added to 'to' and negative is added to 'from' accounts)
given by the record (ie one row in a statement) denominated in the given
currency.
Optionally, both sides of the from/to flow of value can be split with other
accounts (given by 'tgFromEntries' and 'tgToEntries'). In either case, the
amount actually transferred between the 'from' and 'to' accounts above
will be the difference after considering these additional account entries.
Furthermore, additionally entries denominated in different currencies
may be specified via 'tgOtherEntries'. Each member in this list corresponds
to a different currency (and associated entries) governed by most of the
rules for this type regarding balancing and splitting value.
-} -}
{ tgFromAcnt : { tgFromAcnt :
{- {-
@ -520,7 +538,14 @@ let TxGetter =
entries are specified (see below). entries are specified (see below).
-} -}
EntryAcntGetter EntryAcntGetter
, tgFromCurrency : , tgToAcnt :
{-
Account from which this transaction will be balanced. The value of
the transaction will be assigned to this account unless other from
entries are specified (see below).
-}
EntryAcntGetter
, tgCurrency :
{- {-
Currency to assign to the account/value denoted by 'tgFromAcnt' Currency to assign to the account/value denoted by 'tgFromAcnt'
above. above.
@ -538,12 +563,16 @@ let TxGetter =
This is useful for situations where a particular transaction denotes This is useful for situations where a particular transaction denotes
values that come from multiple subaccounts. values that come from multiple subaccounts.
-} -}
List FromEntryGetter.Type List EntryGetter.Type
, tgToEntries : , tgToEntries :
{- {-
A means of getting entries for this transaction (minimum 1) A means of getting entries for this transaction
-} -}
List ToEntryGetter.Type List EntryGetter.Type
, tgOtherEntries :
{-
-}
List TxSubGetter
} }
let StatementParser_ = let StatementParser_ =
@ -1088,13 +1117,11 @@ in { CurID
, DateMatcher , DateMatcher
, FieldMatcher , FieldMatcher
, FieldMatcher_ , FieldMatcher_
, FromEntryNumGetter , EntryNumGetter
, ToEntryNumGetter
, Field , Field
, FieldMap , FieldMap
, Entry , Entry
, FromEntryGetter , EntryGetter
, ToEntryGetter
, EntryTextGetter , EntryTextGetter
, EntryCurGetter , EntryCurGetter
, EntryAcntGetter , EntryAcntGetter

View File

@ -11,7 +11,9 @@ module Internal.Database
, whenHash , whenHash
, whenHash_ , whenHash_
, insertEntry , insertEntry
-- , insertEntrySet
, resolveEntry , resolveEntry
-- , resolveEntrySet
) )
where where
@ -393,15 +395,15 @@ whenHash_ t o f = do
if h `elem` hs then Just . (c,) <$> f else return Nothing if h `elem` hs then Just . (c,) <$> f else return Nothing
insertEntry :: MonadSqlQuery m => TransactionRId -> KeyEntry -> m EntryRId insertEntry :: MonadSqlQuery m => TransactionRId -> KeyEntry -> m EntryRId
insertEntry t Entry {eAcnt, eCurrency, eValue, eComment, eTags} = do insertEntry t FullEntry {feEntry = Entry {eValue, eTags, eAcnt, eComment}, feCurrency} = do
k <- insert $ EntryR t eCurrency eAcnt eComment eValue k <- insert $ EntryR t feCurrency eAcnt eComment eValue
mapM_ (insert_ . TagRelationR k) eTags mapM_ (insert_ . TagRelationR k) eTags
return k return k
resolveEntry :: (MonadInsertError m, MonadFinance m) => BalEntry -> m KeyEntry resolveEntry :: (MonadInsertError m, MonadFinance m) => BalEntry -> m KeyEntry
resolveEntry s@Entry {eAcnt, eCurrency, eValue, eTags} = do resolveEntry s@FullEntry {feEntry = e@Entry {eValue, eTags, eAcnt}, feCurrency} = do
let aRes = lookupAccountKey eAcnt let aRes = lookupAccountKey eAcnt
let cRes = lookupCurrencyKey eCurrency let cRes = lookupCurrencyKey feCurrency
let sRes = lookupAccountSign eAcnt let sRes = lookupAccountSign eAcnt
let tagRes = combineErrors $ fmap lookupTag eTags let tagRes = combineErrors $ fmap lookupTag eTags
-- TODO correct sign here? -- TODO correct sign here?
@ -409,8 +411,6 @@ resolveEntry s@Entry {eAcnt, eCurrency, eValue, eTags} = do
combineError (combineError3 aRes cRes sRes (,,)) tagRes $ combineError (combineError3 aRes cRes sRes (,,)) tagRes $
\(aid, cid, sign) tags -> \(aid, cid, sign) tags ->
s s
{ eAcnt = aid { feCurrency = cid
, eCurrency = cid , feEntry = e {eAcnt = aid, eValue = fromIntegral (sign2Int sign) * eValue, eTags = tags}
, eValue = fromIntegral (sign2Int sign) * eValue
, eTags = tags
} }

View File

@ -8,7 +8,8 @@ where
import Control.Monad.Except import Control.Monad.Except
import Data.Csv import Data.Csv
import Database.Persist.Monad import Data.Foldable
import Database.Persist.Monad hiding (get)
import Internal.Database import Internal.Database
import Internal.Types.Main import Internal.Types.Main
import Internal.Utils import Internal.Utils
@ -18,6 +19,7 @@ import RIO.FilePath
import qualified RIO.List as L import qualified RIO.List as L
import qualified RIO.Map as M import qualified RIO.Map as M
import qualified RIO.NonEmpty as NE import qualified RIO.NonEmpty as NE
import RIO.State
import qualified RIO.Text as T import qualified RIO.Text as T
import RIO.Time import RIO.Time
import qualified RIO.Vector as V import qualified RIO.Vector as V
@ -26,7 +28,7 @@ import qualified RIO.Vector as V
-- :: (MonadInsertError m, MonadFinance m, MonadUnliftIO m) -- :: (MonadInsertError m, MonadFinance m, MonadUnliftIO m)
-- => FilePath -- => FilePath
-- -> [History] -- -> [History]
-- -> m [(CommitR, [RawTx])] -- -> m [(CommitR, [DeferredTx])]
-- readHistory root hs = do -- readHistory root hs = do
-- let (ts, ss) = splitHistory hs -- let (ts, ss) = splitHistory hs
-- ts' <- catMaybes <$> mapErrorsIO readHistTransfer ts -- ts' <- catMaybes <$> mapErrorsIO readHistTransfer ts
@ -36,7 +38,7 @@ import qualified RIO.Vector as V
readHistTransfer readHistTransfer
:: (MonadInsertError m, MonadFinance m) :: (MonadInsertError m, MonadFinance m)
=> HistTransfer => HistTransfer
-> m (Maybe (CommitR, [RawTx])) -> m (Maybe (CommitR, [DeferredTx]))
readHistTransfer readHistTransfer
m@Transfer m@Transfer
{ transFrom = from { transFrom = from
@ -63,11 +65,11 @@ readHistStmt
:: (MonadUnliftIO m, MonadFinance m) :: (MonadUnliftIO m, MonadFinance m)
=> FilePath => FilePath
-> Statement -> Statement
-> m (Maybe (CommitR, [RawTx])) -> m (Maybe (CommitR, [DeferredTx]))
readHistStmt root i = whenHash_ CTImport i $ do readHistStmt root i = whenHash_ CTImport i $ do
bs <- readImport root i bs <- readImport root i
bounds <- askDBState kmStatementInterval bounds <- askDBState kmStatementInterval
return $ filter (inDaySpan bounds . txDate) bs return $ filter (inDaySpan bounds . dtxDate) bs
splitHistory :: [History] -> ([HistTransfer], [Statement]) splitHistory :: [History] -> ([HistTransfer], [Statement])
splitHistory = partitionEithers . fmap go splitHistory = partitionEithers . fmap go
@ -77,7 +79,7 @@ splitHistory = partitionEithers . fmap go
insertHistory insertHistory
:: (MonadInsertError m, MonadSqlQuery m, MonadFinance m) :: (MonadInsertError m, MonadSqlQuery m, MonadFinance m)
=> [(CommitR, [RawTx])] => [(CommitR, [DeferredTx])]
-> m () -> m ()
insertHistory hs = do insertHistory hs = do
bs <- balanceTxs $ concatMap (\(c, xs) -> fmap (c,) xs) hs bs <- balanceTxs $ concatMap (\(c, xs) -> fmap (c,) xs) hs
@ -96,30 +98,37 @@ txPair
-> CurID -> CurID
-> Rational -> Rational
-> T.Text -> T.Text
-> RawTx -> DeferredTx
txPair day from to cur val desc = tx txPair day from to cur val desc =
Tx
{ dtxDescr = desc
, dtxDate = day
, dtxEntries =
[ EntrySet
{ desTotalValue = val
, desCurrency = cur
, desFromEntry0 = entry from
, desToEntryBal = entry to
, desFromEntries = []
, desToEntries = []
}
]
}
where where
split a v = entry a =
Entry Entry
{ eAcnt = a { eAcnt = a
, eValue = ConstD v , eValue = ()
, eComment = "" , eComment = ""
, eCurrency = cur
, eTags = [] , eTags = []
} }
tx =
Tx
{ txDescr = desc
, txDate = day
, txEntries = [split from (-val), split to val]
}
resolveTx :: (MonadInsertError m, MonadFinance m) => BalTx -> m KeyTx resolveTx :: (MonadInsertError m, MonadFinance m) => BalTx -> m KeyTx
resolveTx t@Tx {txEntries = ss} = resolveTx t@Tx {dtxEntries = ss} =
fmap (\kss -> t {txEntries = kss}) $ combineErrors $ fmap resolveEntry ss (\kss -> t {dtxEntries = kss}) <$> mapErrors resolveEntry ss
insertTx :: MonadSqlQuery m => CommitRId -> KeyTx -> m () insertTx :: MonadSqlQuery m => CommitRId -> KeyTx -> m ()
insertTx c Tx {txDate = d, txDescr = e, txEntries = ss} = do insertTx c Tx {dtxDate = d, dtxDescr = e, dtxEntries = ss} = do
k <- insert $ TransactionR c d e k <- insert $ TransactionR c d e
mapM_ (insertEntry k) ss mapM_ (insertEntry k) ss
@ -127,7 +136,7 @@ insertTx c Tx {txDate = d, txDescr = e, txEntries = ss} = do
-- Statements -- Statements
-- TODO this probably won't scale well (pipes?) -- TODO this probably won't scale well (pipes?)
readImport :: (MonadUnliftIO m, MonadFinance m) => FilePath -> Statement -> m [RawTx] readImport :: (MonadUnliftIO m, MonadFinance m) => FilePath -> Statement -> m [DeferredTx]
readImport root Statement {stmtTxOpts, stmtParsers, stmtSkipLines, stmtDelim, stmtPaths} = do readImport root Statement {stmtTxOpts, stmtParsers, stmtSkipLines, stmtDelim, stmtPaths} = do
let ores = compileOptions stmtTxOpts let ores = compileOptions stmtTxOpts
let cres = combineErrors $ compileMatch <$> stmtParsers let cres = combineErrors $ compileMatch <$> stmtParsers
@ -175,7 +184,7 @@ parseTxRecord p TxOpts {toDate, toAmountFmt, toDesc, toAmount, toOther, toDateFm
-- TODO need to somehow balance temporally here (like I do in the budget for -- TODO need to somehow balance temporally here (like I do in the budget for
-- directives that "pay off" a balance) -- directives that "pay off" a balance)
matchRecords :: [MatchRe] -> [TxRecord] -> InsertExceptT CurrencyM [RawTx] matchRecords :: [MatchRe] -> [TxRecord] -> InsertExceptT CurrencyM [DeferredTx]
matchRecords ms rs = do matchRecords ms rs = do
(matched, unmatched, notfound) <- matchAll (matchPriorities ms) rs (matched, unmatched, notfound) <- matchAll (matchPriorities ms) rs
case (matched, unmatched, notfound) of case (matched, unmatched, notfound) of
@ -236,7 +245,7 @@ zipperSlice f x = go
zipperMatch zipperMatch
:: Unzipped MatchRe :: Unzipped MatchRe
-> TxRecord -> TxRecord
-> InsertExceptT CurrencyM (Zipped MatchRe, MatchRes RawTx) -> InsertExceptT CurrencyM (Zipped MatchRe, MatchRes DeferredTx)
zipperMatch (Unzipped bs cs as) x = go [] cs zipperMatch (Unzipped bs cs as) x = go [] cs
where where
go _ [] = return (Zipped bs $ cs ++ as, MatchFail) go _ [] = return (Zipped bs $ cs ++ as, MatchFail)
@ -252,7 +261,7 @@ zipperMatch (Unzipped bs cs as) x = go [] cs
zipperMatch' zipperMatch'
:: Zipped MatchRe :: Zipped MatchRe
-> TxRecord -> TxRecord
-> InsertExceptT CurrencyM (Zipped MatchRe, MatchRes RawTx) -> InsertExceptT CurrencyM (Zipped MatchRe, MatchRes DeferredTx)
zipperMatch' z x = go z zipperMatch' z x = go z
where where
go (Zipped bs (a : as)) = do go (Zipped bs (a : as)) = do
@ -269,7 +278,7 @@ matchDec m = case spTimes m of
Just n -> Just $ m {spTimes = Just $ n - 1} Just n -> Just $ m {spTimes = Just $ n - 1}
Nothing -> Just m Nothing -> Just m
matchAll :: [MatchGroup] -> [TxRecord] -> InsertExceptT CurrencyM ([RawTx], [TxRecord], [MatchRe]) matchAll :: [MatchGroup] -> [TxRecord] -> InsertExceptT CurrencyM ([DeferredTx], [TxRecord], [MatchRe])
matchAll = go ([], []) matchAll = go ([], [])
where where
go (matched, unused) gs rs = case (gs, rs) of go (matched, unused) gs rs = case (gs, rs) of
@ -279,13 +288,13 @@ matchAll = go ([], [])
(ts, unmatched, us) <- matchGroup g rs (ts, unmatched, us) <- matchGroup g rs
go (ts ++ matched, us ++ unused) gs' unmatched go (ts ++ matched, us ++ unused) gs' unmatched
matchGroup :: MatchGroup -> [TxRecord] -> InsertExceptT CurrencyM ([RawTx], [TxRecord], [MatchRe]) matchGroup :: MatchGroup -> [TxRecord] -> InsertExceptT CurrencyM ([DeferredTx], [TxRecord], [MatchRe])
matchGroup MatchGroup {mgDate = ds, mgNoDate = ns} rs = do matchGroup MatchGroup {mgDate = ds, mgNoDate = ns} rs = do
(md, rest, ud) <- matchDates ds rs (md, rest, ud) <- matchDates ds rs
(mn, unmatched, un) <- matchNonDates ns rest (mn, unmatched, un) <- matchNonDates ns rest
return (md ++ mn, unmatched, filter ((/= Nothing) . spTimes) $ ud ++ un) return (md ++ mn, unmatched, filter ((/= Nothing) . spTimes) $ ud ++ un)
matchDates :: [MatchRe] -> [TxRecord] -> InsertExceptT CurrencyM ([RawTx], [TxRecord], [MatchRe]) matchDates :: [MatchRe] -> [TxRecord] -> InsertExceptT CurrencyM ([DeferredTx], [TxRecord], [MatchRe])
matchDates ms = go ([], [], initZipper ms) matchDates ms = go ([], [], initZipper ms)
where where
go (matched, unmatched, z) [] = go (matched, unmatched, z) [] =
@ -306,7 +315,7 @@ matchDates ms = go ([], [], initZipper ms)
go (m, u, z') rs go (m, u, z') rs
findDate m r = maybe EQ (`compareDate` trDate r) $ spDate m findDate m r = maybe EQ (`compareDate` trDate r) $ spDate m
matchNonDates :: [MatchRe] -> [TxRecord] -> InsertExceptT CurrencyM ([RawTx], [TxRecord], [MatchRe]) matchNonDates :: [MatchRe] -> [TxRecord] -> InsertExceptT CurrencyM ([DeferredTx], [TxRecord], [MatchRe])
matchNonDates ms = go ([], [], initZipper ms) matchNonDates ms = go ([], [], initZipper ms)
where where
go (matched, unmatched, z) [] = go (matched, unmatched, z) [] =
@ -323,63 +332,74 @@ matchNonDates ms = go ([], [], initZipper ms)
MatchFail -> (matched, r : unmatched) MatchFail -> (matched, r : unmatched)
in go (m, u, resetZipper z') rs in go (m, u, resetZipper z') rs
-- TDOO should use a better type here to squish down all the entry sets
-- which at this point in the chain should not be necessary
balanceTxs balanceTxs
:: (MonadInsertError m, MonadFinance m) :: (MonadInsertError m, MonadFinance m)
=> [(CommitR, RawTx)] => [(CommitR, DeferredTx)]
-> m [(CommitR, KeyTx)] -> m [(CommitR, KeyTx)]
balanceTxs ts = do balanceTxs ts = do
bs <- mapErrors balanceTx $ snd $ L.mapAccumR balanceTxTargets M.empty ts' keyts <- mapErrors resolveTx balTs
return $ zip cs bs return $ zip cs keyts
where where
(cs, ts') = L.unzip $ L.sortOn (txDate . snd) ts (cs, ts') = L.unzip $ L.sortOn (dtxDate . snd) ts
go bals t@Tx {dtxEntries} =
second (\es -> t {dtxEntries = concat es}) $
L.mapAccumL balanceEntrySet bals dtxEntries
balTs = snd $ L.mapAccumL go M.empty ts'
balanceTxTargets type EntryBals = M.Map (AcntID, CurID) Rational
:: (Ord a, Ord c)
=> M.Map (a, c) Rational
-> Tx (Entry a (Deferred Rational) c t)
-> (M.Map (a, c) Rational, Tx (Entry a (Maybe Rational) c t))
balanceTxTargets bals t@Tx {txEntries} = (bals', t {txEntries = es})
where
(bals', es) = L.mapAccumR balanceEntryTargets bals txEntries
balanceEntryTargets -- TODO might be faster to also do all the key stuff here since currency
:: (Ord a, Ord c) -- will be looked up for every entry rather then the entire entry set
=> M.Map (a, c) Rational balanceEntrySet :: EntryBals -> DeferredEntrySet -> (EntryBals, [BalEntry])
-> Entry a (Deferred Rational) c t balanceEntrySet
-> (M.Map (a, c) Rational, Entry a (Maybe Rational) c t) bals
balanceEntryTargets bals e@Entry {eValue, eAcnt, eCurrency} = (bals', e {eValue = v}) EntrySet
{ desFromEntry0
, desFromEntries
, desToEntryBal
, desToEntries
, desCurrency
, desTotalValue
} = flipTup $ runState doBalAll bals
where
flipTup (a, b) = (b, a)
doEntries es tot e0 = do
es' <- state (\b -> flipTup $ L.mapAccumL (balanceEntry desCurrency) b es)
let val0 = tot - entrySum es'
modify $ mapAdd_ (eAcnt e0, desCurrency) val0
return $ e0 {eValue = val0} : es'
doBalAll = do
fes <- doEntries desFromEntries desTotalValue desFromEntry0
tes <- doEntries desToEntries (-desTotalValue) desToEntryBal
return $ toFull <$> fes ++ tes
toFull e = FullEntry {feEntry = e, feCurrency = desCurrency}
entrySum :: Num v => [Entry a v t] -> v
entrySum = sum . fmap eValue
balanceEntry
:: CurID
-> EntryBals
-> Entry AcntID (Deferred Rational) TagID
-> (EntryBals, Entry AcntID Rational TagID)
balanceEntry curID bals e@Entry {eValue = Deferred toBal v, eAcnt}
| toBal = (bals, e {eValue = v})
| otherwise = (bals', e {eValue = newVal})
where where
key = (eAcnt, eCurrency) key = (eAcnt, curID)
curBal = M.findWithDefault 0 key bals curBal = M.findWithDefault 0 key bals
v = case eValue of newVal = v - curBal
ConstD x -> Just x bals' = mapAdd_ key newVal bals
Target x -> Just $ x - curBal
Derive -> Nothing
bals' = maybe bals (\y -> mapAdd_ key y bals) v
balanceTx -- -- reimplementation from future version :/
:: (MonadInsertError m, MonadFinance m) -- mapAccumM
=> Tx (Entry AcntID (Maybe Rational) CurID TagID) -- :: Monad m
-> m KeyTx -- => (s -> a -> m (s, b))
balanceTx t@Tx {txEntries = ss} = do -- -> s
bs <- liftExcept $ balanceEntries ss -- -> [a]
resolveTx $ t {txEntries = bs} -- -> m (s, [b])
-- mapAccumM f s xs = foldrM go (s, []) xs
balanceEntries :: [Entry AcntID (Maybe Rational) CurID TagID] -> InsertExcept [BalEntry] -- where
balanceEntries ss = -- go x (s', acc) = second (: acc) <$> f s' x
fmap concat
<$> mapM (uncurry bal)
$ groupByKey
$ fmap (\s -> (eCurrency s, s)) ss
where
haeValue s@Entry {eValue = Just v} = Right s {eValue = v}
haeValue s = Left s
bal cur rss
| length rss < 2 = throwError $ InsertException [BalanceError TooFewEntries cur rss]
| otherwise = case partitionEithers $ fmap haeValue rss of
([noVal], val) -> return $ noVal {eValue = foldr (\s x -> x - eValue s) 0 val} : val
([], val) -> return val
_ -> throwError $ InsertException [BalanceError NotOneBlank cur rss]
groupByKey :: Ord k => [(k, v)] -> [(k, [v])]
groupByKey = M.toList . M.fromListWith (++) . fmap (second (: []))

View File

@ -19,7 +19,7 @@ import Language.Haskell.TH.Syntax (Lift)
import RIO import RIO
import qualified RIO.Map as M import qualified RIO.Map as M
import qualified RIO.Text as T import qualified RIO.Text as T
import RIO.Time -- import RIO.Time
import Text.Regex.TDFA import Text.Regex.TDFA
makeHaskellTypesWith makeHaskellTypesWith
@ -32,8 +32,7 @@ makeHaskellTypesWith
, MultipleConstructors "DatePat" "(./dhall/Types.dhall).DatePat" , MultipleConstructors "DatePat" "(./dhall/Types.dhall).DatePat"
, MultipleConstructors "YMDMatcher" "(./dhall/Types.dhall).YMDMatcher" , MultipleConstructors "YMDMatcher" "(./dhall/Types.dhall).YMDMatcher"
, MultipleConstructors "DateMatcher" "(./dhall/Types.dhall).DateMatcher" , MultipleConstructors "DateMatcher" "(./dhall/Types.dhall).DateMatcher"
, MultipleConstructors "ToEntryNumGetter" "(./dhall/Types.dhall).ToEntryNumGetter" , MultipleConstructors "EntryNumGetter" "(./dhall/Types.dhall).EntryNumGetter"
, MultipleConstructors "FromEntryNumGetter" "(./dhall/Types.dhall).FromEntryNumGetter"
, MultipleConstructors "BudgetCurrency" "(./dhall/Types.dhall).BudgetCurrency" , MultipleConstructors "BudgetCurrency" "(./dhall/Types.dhall).BudgetCurrency"
, MultipleConstructors "BudgetTransferType" "(./dhall/Types.dhall).BudgetTransferType" , MultipleConstructors "BudgetTransferType" "(./dhall/Types.dhall).BudgetTransferType"
, MultipleConstructors "TaxMethod" "(./dhall/Types.dhall).TaxMethod" , MultipleConstructors "TaxMethod" "(./dhall/Types.dhall).TaxMethod"
@ -98,8 +97,7 @@ deriveProduct
, "YMDMatcher" , "YMDMatcher"
, "BudgetCurrency" , "BudgetCurrency"
, "Exchange" , "Exchange"
, "FromEntryNumGetter" , "EntryNumGetter"
, "ToEntryNumGetter"
, "TemporalScope" , "TemporalScope"
, "SqlConfig" , "SqlConfig"
, "PretaxValue" , "PretaxValue"
@ -340,9 +338,7 @@ instance Ord DateMatcher where
compare (On d) (In d' _) = compare d d' <> LT compare (On d) (In d' _) = compare d d' <> LT
compare (In d _) (On d') = compare d d' <> GT compare (In d _) (On d') = compare d d' <> GT
deriving instance Hashable FromEntryNumGetter deriving instance Hashable EntryNumGetter
deriving instance Hashable ToEntryNumGetter
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- top level type with fixed account tree to unroll the recursion in the dhall -- top level type with fixed account tree to unroll the recursion in the dhall
@ -425,28 +421,17 @@ data History
| HistStatement !Statement | HistStatement !Statement
deriving (Eq, Generic, Hashable, FromDhall) deriving (Eq, Generic, Hashable, FromDhall)
type ToEntryGetter = Entry EntryAcnt (Maybe ToEntryNumGetter) EntryCur TagID type EntryGetter = Entry EntryAcnt EntryNumGetter TagID
type FromEntryGetter = Entry EntryAcnt FromEntryNumGetter EntryCur TagID instance FromDhall EntryGetter
instance FromDhall ToEntryGetter deriving instance (Show a, Show v, Show t) => Show (Entry a v t)
instance FromDhall FromEntryGetter deriving instance Generic (Entry a v t)
deriving instance (Show a, Show c, Show v, Show t) => Show (Entry a v c t) deriving instance (Hashable a, Hashable v, Hashable t) => Hashable (Entry a v t)
deriving instance Generic (Entry a v c t) deriving instance (Eq a, Eq v, Eq t) => Eq (Entry a v t)
deriving instance (Hashable a, Hashable v, Hashable c, Hashable t) => Hashable (Entry a v c t)
deriving instance (Eq a, Eq v, Eq c, Eq t) => Eq (Entry a v c t)
data Tx s = Tx
{ txDescr :: !T.Text
, txDate :: !Day
, txEntries :: ![s]
}
deriving (Generic)
data TxOpts re = TxOpts data TxOpts re = TxOpts
{ toDate :: !T.Text { toDate :: !T.Text
@ -507,11 +492,23 @@ data FieldMatcher re
deriving instance Show (FieldMatcher T.Text) deriving instance Show (FieldMatcher T.Text)
data TxSubGetter = TxSubGetter
{ tsgFromAcnt :: !EntryAcnt
, tsgToAcnt :: !EntryAcnt
, tsgValue :: !EntryNumGetter
, tsgCurrency :: !EntryCur
, tsgFromEntries :: ![EntryGetter]
, tsgToEntries :: ![EntryGetter]
}
deriving (Eq, Generic, Hashable, Show, FromDhall)
data TxGetter = TxGetter data TxGetter = TxGetter
{ tgFromAcnt :: !EntryAcnt { tgFromAcnt :: !EntryAcnt
, tgFromCurrency :: !EntryCur , tgToAcnt :: !EntryAcnt
, tgFromEntries :: ![FromEntryGetter] , tgCurrency :: !EntryCur
, tgToEntries :: ![ToEntryGetter] , tgFromEntries :: ![EntryGetter]
, tgToEntries :: ![EntryGetter]
, tgOtherEntries :: ![TxSubGetter]
} }
deriving (Eq, Generic, Hashable, Show, FromDhall) deriving (Eq, Generic, Hashable, Show, FromDhall)

View File

@ -59,13 +59,20 @@ data DBUpdates = DBUpdates
type CurrencyM = Reader CurrencyMap type CurrencyM = Reader CurrencyMap
type DeferredKeyEntry = Entry AccountRId (Deferred Rational) CurrencyRId TagRId -- type DeferredKeyEntry = Entry AccountRId (Deferred Rational) CurrencyRId TagRId
type KeyEntry = Entry AccountRId Rational CurrencyRId TagRId data FullEntry a c t = FullEntry
{ feCurrency :: !c
, feEntry :: !(Entry a Rational t)
}
type DeferredKeyTx = Tx DeferredKeyEntry type KeyEntry = FullEntry AccountRId CurrencyRId TagRId
type KeyTx = Tx KeyEntry type BalEntry = FullEntry AcntID CurID TagID
-- type DeferredKeyTx = Tx DeferredKeyEntry
-- type KeyTx = Tx KeyEntry
type TreeR = Tree ([T.Text], AccountRId) type TreeR = Tree ([T.Text], AccountRId)
@ -131,18 +138,46 @@ accountSign IncomeT = Credit
accountSign LiabilityT = Credit accountSign LiabilityT = Credit
accountSign EquityT = Credit accountSign EquityT = Credit
data Deferred a = ConstD a | Target a | Derive data EntrySet a c t v = EntrySet
{ desTotalValue :: !Rational
, desCurrency :: !c
, desFromEntry0 :: !(Entry a () t)
, desFromEntries :: ![Entry a v t]
, desToEntries :: ![Entry a v t]
, desToEntryBal :: !(Entry a () t)
}
data Tx e = Tx
{ dtxDescr :: !T.Text
, dtxDate :: !Day
, dtxEntries :: !e
}
deriving (Generic)
type DeferredEntrySet = EntrySet AcntID CurID TagID (Deferred Rational)
type BalEntrySet = EntrySet AcntID CurID TagID Rational
type KeyEntrySet = EntrySet AccountRId CurrencyRId TagRId Rational
type DeferredTx = Tx [DeferredEntrySet]
type BalTx = Tx [BalEntry]
type KeyTx = Tx [KeyEntry]
data Deferred a = Deferred Bool a
deriving (Show, Functor, Foldable, Traversable) deriving (Show, Functor, Foldable, Traversable)
type RawEntry = Entry AcntID (Deferred Rational) CurID TagID -- type RawEntry = Entry AcntID (Deferred Rational) CurID TagID
-- type RawFromEntry = Entry AcntID (Deferred Rational) CurID TagID -- type RawFromEntry = Entry AcntID (Deferred Rational) CurID TagID
type BalEntry = Entry AcntID Rational CurID TagID -- type BalEntry = Entry AcntID Rational CurID TagID
type RawTx = Tx RawEntry -- type RawTx = Tx RawEntry
type BalTx = Tx BalEntry -- type BalTx = Tx BalEntry
data MatchRes a = MatchPass !a | MatchFail | MatchSkip data MatchRes a = MatchPass !a | MatchFail | MatchSkip
@ -179,8 +214,8 @@ data InsertError
| ParseError !T.Text | ParseError !T.Text
| ConversionError !T.Text | ConversionError !T.Text
| LookupError !LookupSuberr !T.Text | LookupError !LookupSuberr !T.Text
| BalanceError !BalanceType !CurID ![Entry AcntID (Maybe Rational) CurID TagID] | -- | BalanceError !BalanceType !CurID ![Entry AcntID (Maybe Rational) CurID TagID]
| IncomeError !Day !T.Text !Rational IncomeError !Day !T.Text !Rational
| PatternError !Natural !Natural !(Maybe Natural) !PatternSuberr | PatternError !Natural !Natural !(Maybe Natural) !PatternSuberr
| DaySpanError !Gregorian !(Maybe Gregorian) | DaySpanError !Gregorian !(Maybe Gregorian)
| StatementError ![TxRecord] ![MatchRe] | StatementError ![TxRecord] ![MatchRe]

View File

@ -290,7 +290,7 @@ toDaySpan (a, b) = (a, fromIntegral $ diffDays b a - 1)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- matching -- matching
matches :: MatchRe -> TxRecord -> InsertExceptT CurrencyM (MatchRes RawTx) matches :: MatchRe -> TxRecord -> InsertExceptT CurrencyM (MatchRes DeferredTx)
matches matches
StatementParser {spTx, spOther, spVal, spDate, spDesc} StatementParser {spTx, spOther, spVal, spDate, spDesc}
r@TxRecord {trDate, trAmount, trDesc, trOther} = do r@TxRecord {trDate, trAmount, trDesc, trOther} = do
@ -305,39 +305,108 @@ matches
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
convert TxGetter {tgFromAcnt, tgFromCurrency, tgFromEntries, tgToEntries} = convert tg = MatchPass <$> toTx tg r
MatchPass <$> toTx tgFromCurrency tgFromAcnt tgFromEntries tgToEntries r
toTx :: TxGetter -> TxRecord -> InsertExceptT CurrencyM DeferredTx
toTx toTx
:: EntryCur TxGetter {tgFromAcnt, tgToAcnt, tgCurrency, tgFromEntries, tgToEntries, tgOtherEntries}
-> EntryAcnt r@TxRecord {trAmount, trDate, trDesc} = do
-> [FromEntryGetter] combineError3 acntRes curRes subRes $ \(fa, ta) (cur, fe, te) ss -> do
-> [ToEntryGetter]
-> TxRecord
-> InsertExceptT CurrencyM RawTx
toTx sc sa fromEntries toEntries r@TxRecord {trAmount, trDate, trDesc} = do
combineError
(combineError acntRes curRes (,))
(combineError fromRes toRes (,))
$ \(a, c) (fs, ts) ->
let fromEntry = let fromEntry =
Entry Entry
{ eAcnt = a { eAcnt = fa
, eCurrency = c , eValue = ()
, eValue = ConstD trAmount
, eComment = "" -- TODO actually fill this in , eComment = "" -- TODO actually fill this in
, eTags = [] -- TODO what goes here? , eTags = [] -- TODO what goes here?
} }
toEntry =
Entry
{ eAcnt = ta
, eValue = ()
, eComment = ""
, eTags = []
}
in Tx in Tx
{ txDate = trDate { dtxDate = trDate
, txDescr = trDesc , dtxDescr = trDesc
, txEntries = fromEntry : fs ++ ts , dtxEntries =
EntrySet
{ desTotalValue = trAmount
, desCurrency = cur
, desFromEntry0 = fromEntry
, desFromEntries = fe
, desToEntries = te
, desToEntryBal = toEntry
}
: ss
} }
where where
acntRes = liftInner $ resolveAcnt r sa resolveAcnt_ = liftInner . resolveAcnt r
curRes = liftInner $ resolveCurrency r sc acntRes =
fromRes = combineErrors $ fmap (resolveFromEntry r) fromEntries combineError
toRes = combineErrors $ fmap (resolveToEntry r) toEntries (resolveAcnt_ tgFromAcnt)
(resolveAcnt_ tgToAcnt)
(,)
curRes = do
cur <- liftInner $ resolveCurrency r tgCurrency
let feRes = mapErrors (resolveEntry cur r) tgFromEntries
let teRes = mapErrors (resolveEntry cur r) tgToEntries
combineError feRes teRes (cur,,)
subRes = mapErrors (resolveSubGetter r) tgOtherEntries
resolveSubGetter
:: TxRecord
-> TxSubGetter
-> InsertExceptT CurrencyM DeferredEntrySet
resolveSubGetter
r
TxSubGetter
{ tsgFromAcnt
, tsgToAcnt
, tsgValue
, tsgCurrency
, tsgFromEntries
, tsgToEntries
} = combineErrorM acntRes curRes $ \(fa, ta) (cur, fe, te) ->
do
m <- ask
-- TODO laaaaame...
(Deferred _ val) <- liftInner $ mapM (roundPrecisionCur cur m) =<< resolveValue r tsgValue
let fromEntry =
Entry
{ eAcnt = fa
, eValue = ()
, eComment = "" -- TODO actually fill this in
, eTags = [] -- TODO what goes here?
}
let toEntry =
Entry
{ eAcnt = ta
, eValue = ()
, eComment = ""
, eTags = []
}
return
EntrySet
{ desTotalValue = val
, desCurrency = cur
, desFromEntry0 = fromEntry
, desFromEntries = fe
, desToEntries = te
, desToEntryBal = toEntry
}
where
resolveAcnt_ = liftInner . resolveAcnt r
acntRes =
combineError
(resolveAcnt_ tsgFromAcnt)
(resolveAcnt_ tsgToAcnt)
(,)
curRes = do
cur <- liftInner $ resolveCurrency r tsgCurrency
let feRes = mapErrors (resolveEntry cur r) tsgFromEntries
let teRes = mapErrors (resolveEntry cur r) tsgToEntries
combineError feRes teRes (cur,,)
valMatches :: ValMatcher -> Rational -> InsertExcept Bool valMatches :: ValMatcher -> Rational -> InsertExcept Bool
valMatches ValMatcher {vmDen, vmSign, vmNum, vmPrec} x valMatches ValMatcher {vmDen, vmSign, vmNum, vmPrec} x
@ -363,28 +432,34 @@ otherMatches dict m = case m of
where where
lookup_ t n = lookupErr (MatchField t) n dict lookup_ t n = lookupErr (MatchField t) n dict
resolveFromEntry :: TxRecord -> FromEntryGetter -> InsertExceptT CurrencyM RawEntry -- TODO this should be more general?
resolveFromEntry r s@Entry {eAcnt, eValue, eCurrency} = do resolveEntry
:: CurID
-> TxRecord
-> EntryGetter
-> InsertExceptT CurrencyM (Entry AcntID (Deferred Rational) TagID)
resolveEntry cur r s@Entry {eAcnt, eValue} = do
m <- ask m <- ask
liftInner $ combineErrorM3 acntRes curRes valRes $ \a c v -> do liftInner $ combineErrorM acntRes valRes $ \a v -> do
v' <- mapM (roundPrecisionCur c m) v v' <- mapM (roundPrecisionCur cur m) v
return $ s {eAcnt = a, eValue = v', eCurrency = c} return $ s {eAcnt = a, eValue = v'}
where where
acntRes = resolveAcnt r eAcnt acntRes = resolveAcnt r eAcnt
curRes = resolveCurrency r eCurrency valRes = resolveValue r eValue
valRes = resolveFromValue r eValue
-- TODO wet code (kinda, not sure if it's worth combining with above) -- curRes = resolveCurrency r eCurrency
resolveToEntry :: TxRecord -> ToEntryGetter -> InsertExceptT CurrencyM RawEntry
resolveToEntry r s@Entry {eAcnt, eValue, eCurrency} = do -- -- TODO wet code (kinda, not sure if it's worth combining with above)
m <- ask -- resolveToEntry :: TxRecord -> EntryGetter -> InsertExceptT CurrencyM RawEntry
liftInner $ combineErrorM3 acntRes curRes valRes $ \a c v -> do -- resolveToEntry r s@Entry {eAcnt, eValue, eCurrency} = do
v' <- mapM (roundPrecisionCur c m) v -- m <- ask
return $ s {eAcnt = a, eValue = maybe Derive Target v', eCurrency = c} -- liftInner $ combineErrorM3 acntRes curRes valRes $ \a c v -> do
where -- v' <- mapM (roundPrecisionCur c m) v
acntRes = resolveAcnt r eAcnt -- return $ s {eAcnt = a, eValue = maybe Derive (ConstD False) v', eCurrency = c}
curRes = resolveCurrency r eCurrency -- where
valRes = mapM (resolveToValue r) eValue -- acntRes = resolveAcnt r eAcnt
-- curRes = resolveCurrency r eCurrency
-- valRes = mapM (resolveToValue r) eValue
liftInner :: Monad m => ExceptT e Identity a -> ExceptT e m a liftInner :: Monad m => ExceptT e Identity a -> ExceptT e m a
liftInner = mapExceptT (return . runIdentity) liftInner = mapExceptT (return . runIdentity)
@ -428,17 +503,28 @@ combineErrorM3 :: MonadError InsertException m => m a -> m b -> m c -> (a -> b -
combineErrorM3 a b c f = do combineErrorM3 a b c f = do
combineErrorM (combineErrorM a b (curry return)) c $ \(x, y) z -> f x y z combineErrorM (combineErrorM a b (curry return)) c $ \(x, y) z -> f x y z
combineErrors :: MonadError InsertException m => [m a] -> m [a] mapErrors
:: (Traversable t, MonadError InsertException m)
=> (a -> m b)
-> t a
-> m (t b)
-- First, record number of each action. Then try each action. On first failure,
-- note it's position in the sequence, skip ahead to the untried actions,
-- collect failures and add to the first failure.
mapErrors f xs = mapM go $ enumTraversable xs
where
go (n, x) = catchError (f x) $ \e -> do
es <- fmap catMaybes $ mapM (err . f) $ drop (n + 1) $ toList xs
throwError $ foldr (<>) e es
err x = catchError (Nothing <$ x) (pure . Just)
combineErrors :: (Traversable t, MonadError InsertException m) => t (m a) -> m (t a)
combineErrors = mapErrors id combineErrors = mapErrors id
mapErrors :: MonadError InsertException m => (a -> m b) -> [a] -> m [b] enumTraversable :: (Num n, Traversable t) => t a -> t (n, a)
mapErrors f xs = do enumTraversable = snd . L.mapAccumL go 0
ys <- mapM (go . f) xs
case partitionEithers ys of
([], zs) -> return zs
(e : es, _) -> throwError $ foldr (<>) e es
where where
go x = catchError (Right <$> x) (pure . Left) go n x = (n + 1, (n, x))
combineErrorIO2 :: MonadUnliftIO m => m a -> m b -> (a -> b -> c) -> m c combineErrorIO2 :: MonadUnliftIO m => m a -> m b -> (a -> b -> c) -> m c
combineErrorIO2 a b f = combineErrorIOM2 a b (\x y -> pure $ f x y) combineErrorIO2 a b f = combineErrorIOM2 a b (\x y -> pure $ f x y)
@ -457,31 +543,30 @@ combineErrorIOM3 :: MonadUnliftIO m => m a -> m b -> m c -> (a -> b -> c -> m d)
combineErrorIOM3 a b c f = combineErrorIOM3 a b c f =
combineErrorIOM2 (combineErrorIOM2 a b (curry return)) c $ \(x, y) z -> f x y z combineErrorIOM2 (combineErrorIOM2 a b (curry return)) c $ \(x, y) z -> f x y z
mapErrorsIO :: MonadUnliftIO m => (a -> m b) -> [a] -> m [b] mapErrorsIO :: (Traversable t, MonadUnliftIO m) => (a -> m b) -> t a -> m (t b)
mapErrorsIO f xs = do mapErrorsIO f xs = mapM go $ enumTraversable xs
ys <- mapM (go . f) xs
case partitionEithers ys of
([], zs) -> return zs
(es, _) -> throwIO $ InsertException $ concat es
where where
go x = catch (Right <$> x) $ \(InsertException es) -> pure $ Left es go (n, x) = catch (f x) $ \(InsertException e) -> do
es <- fmap catMaybes $ mapM (err . f) $ drop (n + 1) $ toList xs
throwIO $ InsertException $ foldr (<>) e es
err x = catch (Nothing <$ x) $ \(InsertException es) -> pure $ Just es
collectErrorsIO :: MonadUnliftIO m => [m a] -> m [a] collectErrorsIO :: (Traversable t, MonadUnliftIO m) => t (m a) -> m (t a)
collectErrorsIO = mapErrorsIO id collectErrorsIO = mapErrorsIO id
resolveFromValue :: TxRecord -> FromEntryNumGetter -> InsertExcept (Deferred Double) resolveValue :: TxRecord -> EntryNumGetter -> InsertExcept (Deferred Double)
resolveFromValue TxRecord {trOther, trAmount} s = case s of resolveValue TxRecord {trOther, trAmount} s = case s of
(FLookupN t) -> ConstD <$> (readDouble =<< lookupErr EntryValField t trOther) (LookupN t) -> Deferred False <$> (readDouble =<< lookupErr EntryValField t trOther)
(FConstN c) -> return $ ConstD c (ConstN c) -> return $ Deferred False c
FAmountN m -> return $ ConstD $ (* m) $ fromRational trAmount AmountN m -> return $ Deferred False $ (* m) $ fromRational trAmount
FBalanceN x -> return $ Target x BalanceN x -> return $ Deferred True x
-- TODO not DRY -- -- TODO not DRY
resolveToValue :: TxRecord -> ToEntryNumGetter -> InsertExcept Double -- resolveToValue :: TxRecord -> ToEntryNumGetter -> InsertExcept Double
resolveToValue TxRecord {trOther, trAmount} s = case s of -- resolveToValue TxRecord {trOther, trAmount} s = case s of
(TLookupN t) -> readDouble =<< lookupErr EntryValField t trOther -- (TLookupN t) -> readDouble =<< lookupErr EntryValField t trOther
(TConstN c) -> return c -- (TConstN c) -> return c
TAmountN m -> return $ (* m) $ fromRational trAmount -- TAmountN m -> return $ (* m) $ fromRational trAmount
resolveAcnt :: TxRecord -> EntryAcnt -> InsertExcept T.Text resolveAcnt :: TxRecord -> EntryAcnt -> InsertExcept T.Text
resolveAcnt = resolveEntryField AcntField resolveAcnt = resolveEntryField AcntField
@ -656,20 +741,6 @@ showError other = case other of
, singleQuote $ showT next , singleQuote $ showT next
] ]
] ]
(BalanceError t cur rss) ->
[ T.unwords
[ msg
, "for currency"
, singleQuote cur
, "and for entries"
, entries
]
]
where
msg = case t of
TooFewEntries -> "Need at least two entries to balance"
NotOneBlank -> "Exactly one entries must be blank"
entries = T.intercalate ", " $ fmap (singleQuote . showEntry) rss
showGregorian_ :: Gregorian -> T.Text showGregorian_ :: Gregorian -> T.Text
showGregorian_ Gregorian {gYear, gMonth, gDay} = T.intercalate "-" $ showT <$> [gYear, gMonth, gDay] showGregorian_ Gregorian {gYear, gMonth, gDay} = T.intercalate "-" $ showT <$> [gYear, gMonth, gDay]
@ -757,14 +828,6 @@ showMatchOther (Val (Field f mv)) =
, singleQuote $ fromMaybe "*" $ showValMatcher mv , singleQuote $ fromMaybe "*" $ showValMatcher mv
] ]
showEntry :: Entry AcntID (Maybe Rational) CurID TagID -> T.Text
showEntry Entry {eAcnt, eValue, eComment} =
keyVals
[ ("account", eAcnt)
, ("value", T.pack $ show ((fromRational <$> eValue) :: Maybe Float))
, ("comment", doubleQuote eComment)
]
singleQuote :: T.Text -> T.Text singleQuote :: T.Text -> T.Text
singleQuote t = T.concat ["'", t, "'"] singleQuote t = T.concat ["'", t, "'"]
@ -856,6 +919,11 @@ unlessLefts_ e f = unlessLefts e (\x -> void (f x) >> return mzero)
-- thrice :: (a -> d) -> (b -> e) -> (c -> f) -> (a, b, c) -> (d, e, f) -- thrice :: (a -> d) -> (b -> e) -> (c -> f) -> (a, b, c) -> (d, e, f)
-- thrice f1 f2 f3 (x, y, z) = (f1 x, f2 y, f3 z) -- thrice f1 f2 f3 (x, y, z) = (f1 x, f2 y, f3 z)
-- groupKey :: Ord c => (a -> c) -> [(a, b)] -> [(a, [b])]
-- groupKey f = fmap go . NE.groupAllWith (f . fst)
-- where
-- go xs@((c, _) :| _) = (c, NE.toList $ fmap snd xs)
mapAdd_ :: (Ord k, Num v) => k -> v -> M.Map k v -> M.Map k v mapAdd_ :: (Ord k, Num v) => k -> v -> M.Map k v -> M.Map k v
mapAdd_ k v = M.alter (maybe (Just v) (Just . (+ v))) k mapAdd_ k v = M.alter (maybe (Just v) (Just . (+ v))) k