2023-01-28 19:32:56 -05:00
|
|
|
module Internal.Utils
|
|
|
|
( compareDate
|
2023-05-29 15:56:15 -04:00
|
|
|
, expandDatePat
|
|
|
|
, askDays
|
2023-05-14 19:20:10 -04:00
|
|
|
, fromWeekday
|
2023-05-29 15:56:15 -04:00
|
|
|
, inDaySpan
|
2023-01-28 19:32:56 -05:00
|
|
|
, fmtRational
|
|
|
|
, matches
|
|
|
|
, fromGregorian'
|
2023-05-29 15:56:15 -04:00
|
|
|
, resolveDaySpan
|
|
|
|
, resolveDaySpan_
|
|
|
|
, intersectDaySpan
|
2023-05-07 20:29:33 -04:00
|
|
|
, liftInner
|
|
|
|
, liftExceptT
|
|
|
|
, liftExcept
|
|
|
|
, liftIOExcept
|
|
|
|
, liftIOExceptT
|
|
|
|
, combineError
|
|
|
|
, combineError_
|
|
|
|
, combineError3
|
|
|
|
, combineErrors
|
|
|
|
, mapErrors
|
|
|
|
, combineErrorM
|
|
|
|
, combineErrorM3
|
|
|
|
, combineErrorIO2
|
|
|
|
, combineErrorIO3
|
|
|
|
, combineErrorIOM2
|
|
|
|
, combineErrorIOM3
|
|
|
|
, collectErrorsIO
|
|
|
|
, mapErrorsIO
|
2023-01-28 19:32:56 -05:00
|
|
|
, parseRational
|
|
|
|
, showError
|
2023-01-28 22:55:07 -05:00
|
|
|
, unlessLeft_
|
|
|
|
, unlessLefts_
|
2023-01-28 19:32:56 -05:00
|
|
|
, unlessLeft
|
|
|
|
, unlessLefts
|
|
|
|
, acntPath2Text
|
2023-01-28 21:13:16 -05:00
|
|
|
, showT
|
2023-01-28 22:55:07 -05:00
|
|
|
, lookupErr
|
2023-02-01 20:56:29 -05:00
|
|
|
, gregorians
|
2023-04-16 20:09:13 -04:00
|
|
|
, uncurry3
|
|
|
|
, fstOf3
|
|
|
|
, sndOf3
|
|
|
|
, thdOf3
|
2023-02-01 20:56:29 -05:00
|
|
|
, xGregToDay
|
2023-02-01 23:02:07 -05:00
|
|
|
, compileMatch
|
2023-02-05 11:34:37 -05:00
|
|
|
, compileOptions
|
2023-02-13 19:57:39 -05:00
|
|
|
, dateMatches
|
|
|
|
, valMatches
|
2023-05-04 21:48:21 -04:00
|
|
|
, roundPrecision
|
|
|
|
, roundPrecisionCur
|
2023-06-24 17:32:43 -04:00
|
|
|
, lookupAccount
|
2023-05-29 15:56:15 -04:00
|
|
|
, lookupAccountKey
|
|
|
|
, lookupAccountSign
|
|
|
|
, lookupAccountType
|
2023-06-24 17:32:43 -04:00
|
|
|
, lookupCurrency
|
2023-05-29 15:56:15 -04:00
|
|
|
, lookupCurrencyKey
|
|
|
|
, lookupCurrencyPrec
|
|
|
|
, lookupTag
|
2023-06-13 20:12:29 -04:00
|
|
|
, mapAdd_
|
2023-06-25 14:26:35 -04:00
|
|
|
, groupKey
|
|
|
|
, groupWith
|
2023-07-01 18:58:15 -04:00
|
|
|
, balanceTxs
|
|
|
|
, expandTransfers
|
|
|
|
, expandTransfer
|
|
|
|
, entryPair
|
2023-01-28 19:32:56 -05:00
|
|
|
)
|
|
|
|
where
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
import Control.Monad.Error.Class
|
|
|
|
import Control.Monad.Except
|
2023-01-25 20:52:27 -05:00
|
|
|
import Data.Time.Format.ISO8601
|
2023-01-05 22:16:06 -05:00
|
|
|
import GHC.Real
|
2023-05-29 14:46:30 -04:00
|
|
|
import Internal.Types.Main
|
2023-01-05 22:16:06 -05:00
|
|
|
import RIO
|
2023-01-25 20:52:27 -05:00
|
|
|
import qualified RIO.List as L
|
2023-01-05 22:16:06 -05:00
|
|
|
import qualified RIO.Map as M
|
2023-02-25 22:56:23 -05:00
|
|
|
import qualified RIO.NonEmpty as NE
|
2023-07-01 18:58:15 -04:00
|
|
|
import RIO.State
|
2023-01-05 22:16:06 -05:00
|
|
|
import qualified RIO.Text as T
|
|
|
|
import RIO.Time
|
2023-07-01 18:58:15 -04:00
|
|
|
import qualified RIO.Vector as V
|
2023-01-05 22:16:06 -05:00
|
|
|
import Text.Regex.TDFA
|
2023-02-05 12:29:43 -05:00
|
|
|
import Text.Regex.TDFA.Text
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-05-29 15:56:15 -04:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- intervals
|
|
|
|
|
|
|
|
expandDatePat :: DaySpan -> DatePat -> InsertExcept [Day]
|
|
|
|
expandDatePat b (Cron cp) = expandCronPat b cp
|
|
|
|
expandDatePat i (Mod mp) = return $ expandModPat mp i
|
|
|
|
|
|
|
|
expandModPat :: ModPat -> DaySpan -> [Day]
|
|
|
|
expandModPat ModPat {mpStart = s, mpBy = b, mpUnit = u, mpRepeats = r} bs =
|
|
|
|
takeWhile (<= upper) $
|
|
|
|
(`addFun` start) . (* b')
|
|
|
|
<$> maybe id (take . fromIntegral) r [0 ..]
|
|
|
|
where
|
|
|
|
(lower, upper) = fromDaySpan bs
|
|
|
|
start = maybe lower fromGregorian' s
|
|
|
|
b' = fromIntegral b
|
|
|
|
addFun = case u of
|
|
|
|
Day -> addDays
|
|
|
|
Week -> addDays . (* 7)
|
|
|
|
Month -> addGregorianMonthsClip
|
|
|
|
Year -> addGregorianYearsClip
|
|
|
|
|
|
|
|
expandCronPat :: DaySpan -> CronPat -> InsertExcept [Day]
|
|
|
|
expandCronPat b CronPat {cpYear, cpMonth, cpDay, cpWeekly} =
|
|
|
|
combineError3 yRes mRes dRes $ \ys ms ds ->
|
|
|
|
filter validWeekday $
|
|
|
|
mapMaybe (uncurry3 toDay) $
|
|
|
|
takeWhile (\((y, _), m, d) -> (y, m, d) <= (yb1, mb1, db1)) $
|
|
|
|
dropWhile (\((y, _), m, d) -> (y, m, d) < (yb0, mb0, db0)) $
|
|
|
|
[(y, m, d) | y <- (\y -> (y, isLeapYear y)) <$> ys, m <- ms, d <- ds]
|
|
|
|
where
|
|
|
|
yRes = case cpYear of
|
|
|
|
Nothing -> return [yb0 .. yb1]
|
|
|
|
Just pat -> do
|
|
|
|
ys <- expandMDYPat (fromIntegral yb0) (fromIntegral yb1) pat
|
|
|
|
return $ dropWhile (< yb0) $ fromIntegral <$> ys
|
|
|
|
mRes = expandMD 12 cpMonth
|
|
|
|
dRes = expandMD 31 cpDay
|
|
|
|
(s, e) = fromDaySpan b
|
|
|
|
(yb0, mb0, db0) = toGregorian s
|
|
|
|
(yb1, mb1, db1) = toGregorian $ addDays (-1) e
|
|
|
|
expandMD lim =
|
|
|
|
fmap (fromIntegral <$>)
|
|
|
|
. maybe (return [1 .. lim]) (expandMDYPat 1 lim)
|
|
|
|
expandW (OnDay x) = [fromEnum x]
|
|
|
|
expandW (OnDays xs) = fromEnum <$> xs
|
|
|
|
ws = maybe [] expandW cpWeekly
|
|
|
|
validWeekday = if null ws then const True else \day -> dayToWeekday day `elem` ws
|
|
|
|
toDay (y, leap) m d
|
|
|
|
| m == 2 && (not leap && d > 28 || leap && d > 29) = Nothing
|
|
|
|
| m `elem` [4, 6, 9, 11] && d > 30 = Nothing
|
|
|
|
| otherwise = Just $ fromGregorian y m d
|
|
|
|
|
|
|
|
expandMDYPat :: Natural -> Natural -> MDYPat -> InsertExcept [Natural]
|
|
|
|
expandMDYPat lower upper (Single x) = return [x | lower <= x && x <= upper]
|
|
|
|
expandMDYPat lower upper (Multi xs) = return $ dropWhile (<= lower) $ takeWhile (<= upper) xs
|
|
|
|
expandMDYPat lower upper (After x) = return [max lower x .. upper]
|
|
|
|
expandMDYPat lower upper (Before x) = return [lower .. min upper x]
|
|
|
|
expandMDYPat lower upper (Between x y) = return [max lower x .. min upper y]
|
|
|
|
expandMDYPat lower upper (Repeat RepeatPat {rpStart = s, rpBy = b, rpRepeats = r})
|
|
|
|
| b < 1 = throwError $ InsertException [PatternError s b r ZeroLength]
|
|
|
|
| otherwise = do
|
|
|
|
k <- limit r
|
|
|
|
return $ dropWhile (<= lower) $ takeWhile (<= k) [s + i * b | i <- [0 ..]]
|
|
|
|
where
|
|
|
|
limit Nothing = return upper
|
|
|
|
limit (Just n)
|
|
|
|
-- this guard not only produces the error for the user but also protects
|
|
|
|
-- from an underflow below it
|
|
|
|
| n < 1 = throwError $ InsertException [PatternError s b r ZeroRepeats]
|
|
|
|
| otherwise = return $ min (s + b * (n - 1)) upper
|
|
|
|
|
|
|
|
dayToWeekday :: Day -> Int
|
|
|
|
dayToWeekday (ModifiedJulianDay d) = mod (fromIntegral d + 2) 7
|
|
|
|
|
|
|
|
askDays
|
|
|
|
:: (MonadFinance m, MonadInsertError m)
|
|
|
|
=> DatePat
|
|
|
|
-> Maybe Interval
|
|
|
|
-> m [Day]
|
|
|
|
askDays dp i = do
|
|
|
|
globalSpan <- askDBState kmBudgetInterval
|
|
|
|
case i of
|
|
|
|
Just i' -> do
|
|
|
|
localSpan <- liftExcept $ resolveDaySpan i'
|
|
|
|
maybe (return []) expand $ intersectDaySpan globalSpan localSpan
|
|
|
|
Nothing -> expand globalSpan
|
|
|
|
where
|
|
|
|
expand = liftExcept . (`expandDatePat` dp)
|
|
|
|
|
2023-01-27 23:33:34 -05:00
|
|
|
--------------------------------------------------------------------------------
|
2023-01-28 19:32:56 -05:00
|
|
|
-- dates
|
|
|
|
|
2023-05-14 19:20:10 -04:00
|
|
|
-- | Lame weekday converter since day of weeks aren't in dhall (yet)
|
|
|
|
fromWeekday :: Weekday -> DayOfWeek
|
|
|
|
fromWeekday Mon = Monday
|
|
|
|
fromWeekday Tue = Tuesday
|
|
|
|
fromWeekday Wed = Wednesday
|
|
|
|
fromWeekday Thu = Thursday
|
|
|
|
fromWeekday Fri = Friday
|
|
|
|
fromWeekday Sat = Saturday
|
|
|
|
fromWeekday Sun = Sunday
|
|
|
|
|
2023-02-01 20:56:29 -05:00
|
|
|
-- | find the next date
|
|
|
|
-- this is meant to go in a very tight loop and be very fast (hence no
|
|
|
|
-- complex date functions, most of which heavily use 'mod' and friends)
|
|
|
|
nextXGreg :: XGregorian -> XGregorian
|
|
|
|
nextXGreg XGregorian {xgYear = y, xgMonth = m, xgDay = d, xgDayOfWeek = w}
|
|
|
|
| m == 12 && d == 31 = XGregorian (y + 1) 1 1 w_
|
|
|
|
| (m == 2 && (not leap && d == 28 || (leap && d == 29)))
|
|
|
|
|| (m `elem` [4, 6, 9, 11] && d == 30)
|
|
|
|
|| (d == 31) =
|
|
|
|
XGregorian y (m + 1) 1 w_
|
|
|
|
| otherwise = XGregorian y m (d + 1) w_
|
|
|
|
where
|
|
|
|
-- don't use DayOfWeek from Data.Time since this uses mod (which uses a
|
|
|
|
-- division opcode) and thus will be slower than just checking for equality
|
|
|
|
-- and adding
|
|
|
|
w_ = if w == 6 then 0 else w + 1
|
|
|
|
leap = isLeapYear $ fromIntegral y
|
|
|
|
|
|
|
|
gregorians :: Day -> [XGregorian]
|
|
|
|
gregorians x = L.iterate nextXGreg $ XGregorian (fromIntegral y) m d w
|
|
|
|
where
|
|
|
|
(y, m, d) = toGregorian x
|
|
|
|
w = fromEnum $ dayOfWeek x
|
|
|
|
|
|
|
|
xGregToDay :: XGregorian -> Day
|
|
|
|
xGregToDay XGregorian {xgYear = y, xgMonth = m, xgDay = d} = fromGregorian (fromIntegral y) m d
|
|
|
|
|
2023-01-28 19:32:56 -05:00
|
|
|
gregTup :: Gregorian -> (Integer, Int, Int)
|
2023-02-12 16:23:32 -05:00
|
|
|
gregTup Gregorian {gYear, gMonth, gDay} =
|
2023-01-28 19:32:56 -05:00
|
|
|
( fromIntegral gYear
|
|
|
|
, fromIntegral gMonth
|
|
|
|
, fromIntegral gDay
|
|
|
|
)
|
|
|
|
|
|
|
|
gregMTup :: GregorianM -> (Integer, Int)
|
2023-02-12 16:23:32 -05:00
|
|
|
gregMTup GregorianM {gmYear, gmMonth} =
|
2023-01-28 19:32:56 -05:00
|
|
|
( fromIntegral gmYear
|
|
|
|
, fromIntegral gmMonth
|
|
|
|
)
|
|
|
|
|
2023-01-30 22:57:42 -05:00
|
|
|
data YMD_ = Y_ !Integer | YM_ !Integer !Int | YMD_ !Integer !Int !Int
|
2023-01-28 19:32:56 -05:00
|
|
|
|
2023-04-30 00:16:06 -04:00
|
|
|
fromYMDMatcher :: YMDMatcher -> YMD_
|
|
|
|
fromYMDMatcher m = case m of
|
2023-01-28 19:32:56 -05:00
|
|
|
Y y -> Y_ $ fromIntegral y
|
|
|
|
YM g -> uncurry YM_ $ gregMTup g
|
|
|
|
YMD g -> uncurry3 YMD_ $ gregTup g
|
|
|
|
|
2023-04-30 00:16:06 -04:00
|
|
|
compareDate :: DateMatcher -> Day -> Ordering
|
2023-01-28 19:32:56 -05:00
|
|
|
compareDate (On md) x =
|
2023-04-30 00:16:06 -04:00
|
|
|
case fromYMDMatcher md of
|
2023-01-25 23:04:54 -05:00
|
|
|
Y_ y' -> compare y y'
|
|
|
|
YM_ y' m' -> compare (y, m) (y', m')
|
|
|
|
YMD_ y' m' d' -> compare (y, m, d) (y', m', d')
|
2022-12-11 17:51:11 -05:00
|
|
|
where
|
2022-12-19 23:13:05 -05:00
|
|
|
(y, m, d) = toGregorian x
|
2023-01-25 23:04:54 -05:00
|
|
|
compareDate (In md offset) x = do
|
2023-04-30 00:16:06 -04:00
|
|
|
case fromYMDMatcher md of
|
2023-01-25 23:04:54 -05:00
|
|
|
Y_ y' -> compareRange y' y
|
|
|
|
YM_ y' m' -> compareRange (toMonth y' m') $ toMonth y m
|
|
|
|
YMD_ y' m' d' ->
|
|
|
|
let s = toModifiedJulianDay $ fromGregorian y' m' d'
|
|
|
|
in compareRange s $ toModifiedJulianDay x
|
2022-12-11 17:51:11 -05:00
|
|
|
where
|
2022-12-19 23:13:05 -05:00
|
|
|
(y, m, _) = toGregorian x
|
|
|
|
compareRange start z
|
2022-12-11 17:51:11 -05:00
|
|
|
| z < start = LT
|
2022-12-22 20:13:03 -05:00
|
|
|
| otherwise = if (start + fromIntegral offset - 1) < z then GT else EQ
|
2022-12-19 23:13:05 -05:00
|
|
|
toMonth year month = (year * 12) + fromIntegral month
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-01-28 19:32:56 -05:00
|
|
|
fromGregorian' :: Gregorian -> Day
|
|
|
|
fromGregorian' = uncurry3 fromGregorian . gregTup
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-05-29 15:56:15 -04:00
|
|
|
inDaySpan :: DaySpan -> Day -> Bool
|
|
|
|
inDaySpan bs = withinDays (fromDaySpan bs)
|
2023-05-29 13:09:17 -04:00
|
|
|
|
|
|
|
withinDays :: (Day, Day) -> Day -> Bool
|
|
|
|
withinDays (d0, d1) x = d0 <= x && x < d1
|
2023-02-05 10:34:26 -05:00
|
|
|
|
2023-05-29 15:56:15 -04:00
|
|
|
resolveDaySpan :: Interval -> InsertExcept DaySpan
|
|
|
|
resolveDaySpan i@Interval {intStart = s} =
|
|
|
|
resolveDaySpan_ (s {gYear = gYear s + 50}) i
|
2023-03-16 23:53:57 -04:00
|
|
|
|
2023-05-29 15:56:15 -04:00
|
|
|
intersectDaySpan :: DaySpan -> DaySpan -> Maybe DaySpan
|
|
|
|
intersectDaySpan a b =
|
|
|
|
if b' > a' then Nothing else Just $ toDaySpan (a', b')
|
2023-05-29 13:09:17 -04:00
|
|
|
where
|
2023-05-29 15:56:15 -04:00
|
|
|
(a0, a1) = fromDaySpan a
|
|
|
|
(b0, b1) = fromDaySpan b
|
2023-05-29 13:09:17 -04:00
|
|
|
a' = max a0 a1
|
|
|
|
b' = min b0 b1
|
|
|
|
|
2023-05-29 15:56:15 -04:00
|
|
|
resolveDaySpan_ :: Gregorian -> Interval -> InsertExcept DaySpan
|
|
|
|
resolveDaySpan_ def Interval {intStart = s, intEnd = e} =
|
2023-07-01 18:32:20 -04:00
|
|
|
-- TODO the default isn't checked here :/
|
2023-02-05 10:34:26 -05:00
|
|
|
case fromGregorian' <$> e of
|
2023-05-29 15:56:15 -04:00
|
|
|
Nothing -> return $ toDaySpan_ $ fromGregorian' def
|
2023-02-05 10:34:26 -05:00
|
|
|
Just e_
|
2023-05-29 15:56:15 -04:00
|
|
|
| s_ < e_ -> return $ toDaySpan_ e_
|
|
|
|
| otherwise -> throwError $ InsertException [DaySpanError s e]
|
2023-01-27 23:33:34 -05:00
|
|
|
where
|
2023-02-05 10:34:26 -05:00
|
|
|
s_ = fromGregorian' s
|
2023-05-29 15:56:15 -04:00
|
|
|
toDaySpan_ end = toDaySpan (s_, end)
|
|
|
|
|
|
|
|
fromDaySpan :: DaySpan -> (Day, Day)
|
|
|
|
fromDaySpan (d, n) = (d, addDays (fromIntegral n + 1) d)
|
2023-02-05 10:34:26 -05:00
|
|
|
|
2023-05-29 15:56:15 -04:00
|
|
|
-- ASSUME a < b
|
|
|
|
toDaySpan :: (Day, Day) -> DaySpan
|
|
|
|
toDaySpan (a, b) = (a, fromIntegral $ diffDays b a - 1)
|
2023-01-27 23:33:34 -05:00
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- matching
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
matches :: MonadFinance m => MatchRe -> TxRecord -> InsertExceptT m (MatchRes (Tx ()))
|
2023-02-12 16:23:32 -05:00
|
|
|
matches
|
2023-04-30 23:28:16 -04:00
|
|
|
StatementParser {spTx, spOther, spVal, spDate, spDesc}
|
2023-02-12 16:23:32 -05:00
|
|
|
r@TxRecord {trDate, trAmount, trDesc, trOther} = do
|
2023-05-07 20:29:33 -04:00
|
|
|
res <- liftInner $
|
|
|
|
combineError3 val other desc $
|
|
|
|
\x y z -> x && y && z && date
|
|
|
|
if res
|
|
|
|
then maybe (return MatchSkip) convert spTx
|
|
|
|
else return MatchFail
|
2023-02-12 16:23:32 -05:00
|
|
|
where
|
2023-04-30 23:28:16 -04:00
|
|
|
val = valMatches spVal trAmount
|
|
|
|
date = maybe True (`dateMatches` trDate) spDate
|
|
|
|
other = foldM (\a o -> (a &&) <$> otherMatches trOther o) True spOther
|
|
|
|
desc = maybe (return True) (matchMaybe trDesc . snd) spDesc
|
2023-06-16 22:05:28 -04:00
|
|
|
convert tg = MatchPass <$> toTx tg r
|
2023-06-10 21:30:30 -04:00
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
toTx :: MonadFinance m => TxGetter -> TxRecord -> InsertExceptT m (Tx ())
|
2023-06-10 21:30:30 -04:00
|
|
|
toTx
|
2023-06-17 00:16:01 -04:00
|
|
|
TxGetter
|
2023-06-19 12:14:18 -04:00
|
|
|
{ tgFrom
|
|
|
|
, tgTo
|
2023-06-17 00:16:01 -04:00
|
|
|
, tgCurrency
|
|
|
|
, tgOtherEntries
|
2023-06-20 22:52:52 -04:00
|
|
|
, tgScale
|
2023-06-17 00:16:01 -04:00
|
|
|
}
|
2023-06-16 22:05:28 -04:00
|
|
|
r@TxRecord {trAmount, trDate, trDesc} = do
|
2023-06-29 21:32:14 -04:00
|
|
|
combineError curRes subRes $ \(cur, f, t) ss ->
|
2023-06-17 00:16:01 -04:00
|
|
|
Tx
|
2023-06-19 12:14:18 -04:00
|
|
|
{ txDate = trDate
|
|
|
|
, txDescr = trDesc
|
2023-06-24 17:32:43 -04:00
|
|
|
, txCommit = ()
|
2023-06-29 21:32:14 -04:00
|
|
|
, txPrimary =
|
2023-07-03 20:27:52 -04:00
|
|
|
Left $
|
|
|
|
EntrySet
|
|
|
|
{ esTotalValue = roundPrecisionCur cur $ tgScale * fromRational trAmount
|
|
|
|
, esCurrency = cur
|
|
|
|
, esFrom = f
|
|
|
|
, esTo = t
|
|
|
|
}
|
|
|
|
, txOther = fmap Left ss
|
2023-06-17 00:16:01 -04:00
|
|
|
}
|
2023-06-16 22:05:28 -04:00
|
|
|
where
|
|
|
|
curRes = do
|
2023-06-29 21:32:14 -04:00
|
|
|
m <- askDBState kmCurrency
|
|
|
|
cur <- liftInner $ resolveCurrency m r tgCurrency
|
2023-07-03 20:27:52 -04:00
|
|
|
let fromRes = liftInner $ resolveHalfEntry resolveFromValue cur r () tgFrom
|
|
|
|
let toRes = liftInner $ resolveHalfEntry resolveToValue cur r () tgTo
|
2023-06-29 21:32:14 -04:00
|
|
|
combineError fromRes toRes (cur,,)
|
2023-06-16 22:05:28 -04:00
|
|
|
subRes = mapErrors (resolveSubGetter r) tgOtherEntries
|
|
|
|
|
|
|
|
resolveSubGetter
|
2023-06-29 21:32:14 -04:00
|
|
|
:: MonadFinance m
|
|
|
|
=> TxRecord
|
2023-06-16 22:05:28 -04:00
|
|
|
-> TxSubGetter
|
2023-07-03 20:27:52 -04:00
|
|
|
-> InsertExceptT m SecondayEntrySet
|
2023-06-19 12:14:18 -04:00
|
|
|
resolveSubGetter r TxSubGetter {tsgFrom, tsgTo, tsgValue, tsgCurrency} = do
|
2023-06-29 21:32:14 -04:00
|
|
|
m <- askDBState kmCurrency
|
|
|
|
cur <- liftInner $ resolveCurrency m r tsgCurrency
|
2023-07-03 20:27:52 -04:00
|
|
|
let toRes = resolveHalfEntry resolveToValue cur r () tsgTo
|
2023-06-29 21:32:14 -04:00
|
|
|
let valRes = liftInner $ fmap (roundPrecisionCur cur) <$> resolveValue r tsgValue
|
2023-07-03 20:27:52 -04:00
|
|
|
liftInner $ combineErrorM toRes valRes $ \t v -> do
|
|
|
|
f <- resolveHalfEntry resolveFromValue cur r v tsgFrom
|
|
|
|
return $
|
|
|
|
EntrySet
|
|
|
|
{ esTotalValue = ()
|
|
|
|
, esCurrency = cur
|
|
|
|
, esFrom = f
|
|
|
|
, esTo = t
|
|
|
|
}
|
2023-06-19 12:14:18 -04:00
|
|
|
|
|
|
|
resolveHalfEntry
|
|
|
|
:: Traversable f
|
|
|
|
=> (TxRecord -> n -> InsertExcept (f Double))
|
2023-06-29 21:32:14 -04:00
|
|
|
-> CurrencyPrec
|
2023-06-19 12:14:18 -04:00
|
|
|
-> TxRecord
|
2023-07-03 20:27:52 -04:00
|
|
|
-> v
|
2023-06-19 12:14:18 -04:00
|
|
|
-> TxHalfGetter (EntryGetter n)
|
2023-07-03 20:27:52 -04:00
|
|
|
-> InsertExcept (HalfEntrySet v (f Rational))
|
|
|
|
resolveHalfEntry f cur r v TxHalfGetter {thgAcnt, thgComment, thgTags, thgEntries} =
|
2023-06-19 12:14:18 -04:00
|
|
|
combineError acntRes esRes $ \a es ->
|
|
|
|
HalfEntrySet
|
|
|
|
{ hesPrimary =
|
|
|
|
Entry
|
|
|
|
{ eAcnt = a
|
2023-07-03 20:27:52 -04:00
|
|
|
, eValue = v
|
2023-06-19 12:14:18 -04:00
|
|
|
, eComment = thgComment
|
|
|
|
, eTags = thgTags
|
|
|
|
}
|
|
|
|
, hesOther = es
|
|
|
|
}
|
|
|
|
where
|
2023-06-29 21:32:14 -04:00
|
|
|
acntRes = resolveAcnt r thgAcnt
|
2023-06-19 12:14:18 -04:00
|
|
|
esRes = mapErrors (resolveEntry f cur r) thgEntries
|
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
valMatches :: ValMatcher -> Rational -> InsertExcept Bool
|
2023-04-30 23:28:16 -04:00
|
|
|
valMatches ValMatcher {vmDen, vmSign, vmNum, vmPrec} x
|
2023-05-07 20:29:33 -04:00
|
|
|
| Just d_ <- vmDen, d_ >= p = throwError $ InsertException [MatchValPrecisionError d_ p]
|
2023-01-28 20:03:58 -05:00
|
|
|
| otherwise =
|
2023-05-07 20:29:33 -04:00
|
|
|
return $
|
2023-04-30 23:28:16 -04:00
|
|
|
checkMaybe (s ==) vmSign
|
|
|
|
&& checkMaybe (n ==) vmNum
|
|
|
|
&& checkMaybe ((d * fromIntegral p ==) . fromIntegral) vmDen
|
2023-01-27 23:33:34 -05:00
|
|
|
where
|
|
|
|
(n, d) = properFraction $ abs x
|
2023-04-30 23:28:16 -04:00
|
|
|
p = 10 ^ vmPrec
|
2023-01-27 23:33:34 -05:00
|
|
|
s = signum x >= 0
|
|
|
|
checkMaybe = maybe True
|
|
|
|
|
2023-04-30 00:16:06 -04:00
|
|
|
dateMatches :: DateMatcher -> Day -> Bool
|
2023-01-28 19:32:56 -05:00
|
|
|
dateMatches md = (EQ ==) . compareDate md
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
otherMatches :: M.Map T.Text T.Text -> FieldMatcherRe -> InsertExcept Bool
|
2023-01-27 23:33:34 -05:00
|
|
|
otherMatches dict m = case m of
|
2023-01-28 20:03:58 -05:00
|
|
|
Val (Field n mv) -> valMatches mv =<< (readRational =<< lookup_ MatchNumeric n)
|
2023-02-01 23:02:07 -05:00
|
|
|
Desc (Field n (_, md)) -> (`matchMaybe` md) =<< lookup_ MatchText n
|
2023-01-27 23:33:34 -05:00
|
|
|
where
|
|
|
|
lookup_ t n = lookupErr (MatchField t) n dict
|
|
|
|
|
2023-06-16 22:05:28 -04:00
|
|
|
resolveEntry
|
2023-06-19 12:14:18 -04:00
|
|
|
:: Traversable f
|
|
|
|
=> (TxRecord -> n -> InsertExcept (f Double))
|
2023-06-29 21:32:14 -04:00
|
|
|
-> CurrencyPrec
|
2023-06-16 22:05:28 -04:00
|
|
|
-> TxRecord
|
2023-06-19 12:14:18 -04:00
|
|
|
-> EntryGetter n
|
2023-06-29 21:32:14 -04:00
|
|
|
-> InsertExcept (Entry AcntID (f Rational) TagID)
|
2023-06-19 12:14:18 -04:00
|
|
|
resolveEntry f cur r s@Entry {eAcnt, eValue} = do
|
2023-06-29 21:32:14 -04:00
|
|
|
combineError acntRes valRes $ \a v ->
|
|
|
|
s {eAcnt = a, eValue = roundPrecisionCur cur <$> v}
|
2023-01-27 23:33:34 -05:00
|
|
|
where
|
2023-05-07 20:29:33 -04:00
|
|
|
acntRes = resolveAcnt r eAcnt
|
2023-06-19 12:14:18 -04:00
|
|
|
valRes = f r eValue
|
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
liftInner :: Monad m => ExceptT e Identity a -> ExceptT e m a
|
|
|
|
liftInner = mapExceptT (return . runIdentity)
|
|
|
|
|
|
|
|
liftExceptT :: MonadError e m => ExceptT e m a -> m a
|
|
|
|
liftExceptT x = runExceptT x >>= either throwError return
|
|
|
|
|
|
|
|
liftExcept :: MonadError e m => Except e a -> m a
|
|
|
|
liftExcept = either throwError return . runExcept
|
|
|
|
|
|
|
|
-- tryError :: MonadError e m => m a -> m (Either e a)
|
|
|
|
-- tryError action = (Right <$> action) `catchError` (pure . Left)
|
|
|
|
|
|
|
|
liftIOExceptT :: MonadIO m => InsertExceptT m a -> m a
|
|
|
|
liftIOExceptT = fromEither <=< runExceptT
|
|
|
|
|
|
|
|
liftIOExcept :: MonadIO m => InsertExcept a -> m a
|
|
|
|
liftIOExcept = fromEither . runExcept
|
|
|
|
|
|
|
|
combineError :: MonadError InsertException m => m a -> m b -> (a -> b -> c) -> m c
|
|
|
|
combineError a b f = combineErrorM a b (\x y -> pure $ f x y)
|
|
|
|
|
|
|
|
combineError_ :: MonadError InsertException m => m a -> m b -> m ()
|
|
|
|
combineError_ a b = do
|
|
|
|
_ <- catchError a $ \e ->
|
|
|
|
throwError =<< catchError (e <$ b) (return . (e <>))
|
|
|
|
_ <- b
|
|
|
|
return ()
|
|
|
|
|
|
|
|
combineErrorM :: MonadError InsertException m => m a -> m b -> (a -> b -> m c) -> m c
|
|
|
|
combineErrorM a b f = do
|
|
|
|
a' <- catchError a $ \e ->
|
|
|
|
throwError =<< catchError (e <$ b) (return . (e <>))
|
|
|
|
f a' =<< b
|
|
|
|
|
|
|
|
combineError3 :: MonadError InsertException m => m a -> m b -> m c -> (a -> b -> c -> d) -> m d
|
|
|
|
combineError3 a b c f =
|
|
|
|
combineError (combineError a b (,)) c $ \(x, y) z -> f x y z
|
|
|
|
|
|
|
|
combineErrorM3 :: MonadError InsertException m => m a -> m b -> m c -> (a -> b -> c -> m d) -> m d
|
|
|
|
combineErrorM3 a b c f = do
|
|
|
|
combineErrorM (combineErrorM a b (curry return)) c $ \(x, y) z -> f x y z
|
|
|
|
|
2023-06-16 22:05:28 -04:00
|
|
|
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)
|
2023-05-07 20:29:33 -04:00
|
|
|
combineErrors = mapErrors id
|
|
|
|
|
2023-06-16 22:05:28 -04:00
|
|
|
enumTraversable :: (Num n, Traversable t) => t a -> t (n, a)
|
|
|
|
enumTraversable = snd . L.mapAccumL go 0
|
2023-05-07 20:29:33 -04:00
|
|
|
where
|
2023-06-16 22:05:28 -04:00
|
|
|
go n x = (n + 1, (n, x))
|
2023-05-07 20:29:33 -04:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
combineErrorIO3 :: MonadUnliftIO m => m a -> m b -> m c -> (a -> b -> c -> d) -> m d
|
|
|
|
combineErrorIO3 a b c f = combineErrorIOM3 a b c (\x y z -> pure $ f x y z)
|
|
|
|
|
|
|
|
combineErrorIOM2 :: MonadUnliftIO m => m a -> m b -> (a -> b -> m c) -> m c
|
|
|
|
combineErrorIOM2 a b f = do
|
|
|
|
a' <- catch a $ \(InsertException es) ->
|
|
|
|
(throwIO . InsertException)
|
|
|
|
=<< catch (es <$ b) (\(InsertException es') -> return (es' ++ es))
|
|
|
|
f a' =<< b
|
|
|
|
|
|
|
|
combineErrorIOM3 :: MonadUnliftIO m => m a -> m b -> m c -> (a -> b -> c -> m d) -> m d
|
|
|
|
combineErrorIOM3 a b c f =
|
|
|
|
combineErrorIOM2 (combineErrorIOM2 a b (curry return)) c $ \(x, y) z -> f x y z
|
|
|
|
|
2023-06-16 22:05:28 -04:00
|
|
|
mapErrorsIO :: (Traversable t, MonadUnliftIO m) => (a -> m b) -> t a -> m (t b)
|
|
|
|
mapErrorsIO f xs = mapM go $ enumTraversable xs
|
2023-05-07 20:29:33 -04:00
|
|
|
where
|
2023-06-16 22:05:28 -04:00
|
|
|
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
|
2023-05-07 20:29:33 -04:00
|
|
|
|
2023-06-16 22:05:28 -04:00
|
|
|
collectErrorsIO :: (Traversable t, MonadUnliftIO m) => t (m a) -> m (t a)
|
2023-05-07 20:29:33 -04:00
|
|
|
collectErrorsIO = mapErrorsIO id
|
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
resolveFromValue :: TxRecord -> EntryNumGetter -> InsertExcept (EntryValue Double)
|
|
|
|
resolveFromValue = resolveValue
|
2023-06-19 12:14:18 -04:00
|
|
|
|
|
|
|
resolveToValue :: TxRecord -> LinkedEntryNumGetter -> InsertExcept (LinkDeferred Double)
|
|
|
|
resolveToValue _ (Linked l) = return $ LinkIndex l
|
2023-06-29 21:32:14 -04:00
|
|
|
resolveToValue r (Getter g) = LinkDeferred <$> resolveValue r g
|
2023-06-19 12:14:18 -04:00
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
resolveValue :: TxRecord -> EntryNumGetter -> InsertExcept (EntryValue Double)
|
2023-06-16 22:05:28 -04:00
|
|
|
resolveValue TxRecord {trOther, trAmount} s = case s of
|
2023-06-29 21:32:14 -04:00
|
|
|
(LookupN t) -> EntryValue TFixed <$> (readDouble =<< lookupErr EntryValField t trOther)
|
|
|
|
(ConstN c) -> return $ EntryValue TFixed c
|
|
|
|
AmountN m -> return $ EntryValue TFixed $ m * fromRational trAmount
|
|
|
|
BalanceN x -> return $ EntryValue TBalance x
|
|
|
|
PercentN x -> return $ EntryValue TPercent x
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-05-29 16:11:19 -04:00
|
|
|
resolveAcnt :: TxRecord -> EntryAcnt -> InsertExcept T.Text
|
|
|
|
resolveAcnt = resolveEntryField AcntField
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
resolveCurrency :: CurrencyMap -> TxRecord -> EntryCur -> InsertExcept CurrencyPrec
|
|
|
|
resolveCurrency m r c = do
|
|
|
|
i <- resolveEntryField CurField r c
|
|
|
|
case M.lookup i m of
|
|
|
|
Just k -> return k
|
|
|
|
-- TODO this should be its own error (I think)
|
|
|
|
Nothing -> throwError $ InsertException [LookupError (EntryIDField CurField) undefined]
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-05-29 16:11:19 -04:00
|
|
|
resolveEntryField :: EntryIDType -> TxRecord -> EntryAcnt -> InsertExcept T.Text
|
|
|
|
resolveEntryField t TxRecord {trOther = o} s = case s of
|
2023-05-07 20:29:33 -04:00
|
|
|
ConstT p -> return p
|
2023-05-04 21:48:21 -04:00
|
|
|
LookupT f -> lookup_ f o
|
|
|
|
MapT (Field f m) -> do
|
2023-01-27 23:33:34 -05:00
|
|
|
k <- lookup_ f o
|
2023-02-05 12:29:43 -05:00
|
|
|
lookup_ k m
|
2023-01-27 23:33:34 -05:00
|
|
|
Map2T (Field (f1, f2) m) -> do
|
2023-05-07 20:29:33 -04:00
|
|
|
(k1, k2) <- combineError (lookup_ f1 o) (lookup_ f2 o) (,)
|
2023-05-04 21:48:21 -04:00
|
|
|
lookup_ (k1, k2) m
|
2023-01-27 23:33:34 -05:00
|
|
|
where
|
2023-05-07 20:29:33 -04:00
|
|
|
lookup_ :: (Ord k, Show k) => k -> M.Map k v -> InsertExcept v
|
2023-05-29 16:11:19 -04:00
|
|
|
lookup_ = lookupErr (EntryIDField t)
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
lookupErr :: (Ord k, Show k) => LookupSuberr -> k -> M.Map k v -> InsertExcept v
|
2023-01-27 23:33:34 -05:00
|
|
|
lookupErr what k m = case M.lookup k m of
|
2023-05-07 20:29:33 -04:00
|
|
|
Just x -> return x
|
|
|
|
_ -> throwError $ InsertException [LookupError what $ showT k]
|
2022-12-11 17:51:11 -05:00
|
|
|
|
2023-02-05 12:29:43 -05:00
|
|
|
parseRational :: MonadFail m => (T.Text, Regex) -> T.Text -> m Rational
|
2023-02-05 11:34:37 -05:00
|
|
|
parseRational (pat, re) s = case matchGroupsMaybe s re of
|
2022-12-11 17:51:11 -05:00
|
|
|
[sign, x, ""] -> uncurry (*) <$> readWhole sign x
|
|
|
|
[sign, x, y] -> do
|
|
|
|
d <- readT "decimal" y
|
2023-02-05 12:29:43 -05:00
|
|
|
let p = 10 ^ T.length y
|
2022-12-11 17:51:11 -05:00
|
|
|
(k, w) <- readWhole sign x
|
|
|
|
return $ k * (w + d % p)
|
2023-01-05 22:16:06 -05:00
|
|
|
_ -> msg "malformed decimal"
|
2022-12-11 17:51:11 -05:00
|
|
|
where
|
2023-02-05 12:29:43 -05:00
|
|
|
readT what t = case readMaybe $ T.unpack t of
|
2023-02-13 18:49:41 -05:00
|
|
|
Just d -> return $ fromInteger d
|
|
|
|
_ -> msg $ T.unwords ["could not parse", what, singleQuote t]
|
2023-02-12 16:23:32 -05:00
|
|
|
msg :: MonadFail m => T.Text -> m a
|
2023-01-05 22:16:06 -05:00
|
|
|
msg m =
|
|
|
|
fail $
|
|
|
|
T.unpack $
|
2023-02-05 12:29:43 -05:00
|
|
|
T.unwords [m, "-", keyVals [("pattern", pat), ("query", s)]]
|
2022-12-11 17:51:11 -05:00
|
|
|
readSign x
|
|
|
|
| x == "-" = return (-1)
|
|
|
|
| x == "+" || x == "" = return 1
|
2023-02-05 12:29:43 -05:00
|
|
|
| otherwise = msg $ T.append "invalid sign: " x
|
2022-12-11 17:51:11 -05:00
|
|
|
readWhole sign x = do
|
|
|
|
w <- readT "whole number" x
|
|
|
|
k <- readSign sign
|
|
|
|
return (k, w)
|
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
readDouble :: T.Text -> InsertExcept Double
|
2023-05-04 21:48:21 -04:00
|
|
|
readDouble s = case readMaybe $ T.unpack s of
|
2023-05-07 20:29:33 -04:00
|
|
|
Just x -> return x
|
|
|
|
Nothing -> throwError $ InsertException [ConversionError s]
|
2023-05-04 21:48:21 -04:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
readRational :: T.Text -> InsertExcept Rational
|
2023-02-05 12:29:43 -05:00
|
|
|
readRational s = case T.split (== '.') s of
|
2023-01-25 23:04:54 -05:00
|
|
|
[x] -> maybe err (return . fromInteger) $ readT x
|
|
|
|
[x, y] -> case (readT x, readT y) of
|
|
|
|
(Just x', Just y') ->
|
2023-02-05 12:29:43 -05:00
|
|
|
let p = 10 ^ T.length y
|
2023-01-25 23:04:54 -05:00
|
|
|
k = if x' >= 0 then 1 else -1
|
|
|
|
in return $ fromInteger x' + k * y' % p
|
|
|
|
_ -> err
|
|
|
|
_ -> err
|
2022-12-11 17:51:11 -05:00
|
|
|
where
|
2023-02-05 12:29:43 -05:00
|
|
|
readT = readMaybe . T.unpack
|
2023-05-07 20:29:33 -04:00
|
|
|
err = throwError $ InsertException [ConversionError s]
|
2022-12-11 17:51:11 -05:00
|
|
|
|
|
|
|
-- TODO smells like a lens
|
2023-01-28 19:32:56 -05:00
|
|
|
-- mapTxSplits :: (a -> b) -> Tx a -> Tx b
|
|
|
|
-- mapTxSplits f t@Tx {txSplits = ss} = t {txSplits = fmap f ss}
|
2022-12-11 17:51:11 -05:00
|
|
|
|
|
|
|
fmtRational :: Natural -> Rational -> T.Text
|
|
|
|
fmtRational precision x = T.concat [s, txt n', ".", pad 2 "0" $ txt d']
|
|
|
|
where
|
|
|
|
s = if x >= 0 then "" else "-"
|
2023-01-05 22:16:06 -05:00
|
|
|
x'@(n :% d) = abs x
|
2022-12-11 17:51:11 -05:00
|
|
|
p = 10 ^ precision
|
2023-01-05 22:16:06 -05:00
|
|
|
n' = div n d
|
|
|
|
d' = (\(a :% b) -> div a b) ((x' - fromIntegral n') * p)
|
2022-12-11 17:51:11 -05:00
|
|
|
txt = T.pack . show
|
|
|
|
pad i c z = T.append (T.replicate (i - T.length z) c) z
|
|
|
|
|
2023-05-04 21:48:21 -04:00
|
|
|
roundPrecision :: Natural -> Double -> Rational
|
|
|
|
roundPrecision n = (% p) . round . (* fromIntegral p) . toRational
|
2022-12-11 17:51:11 -05:00
|
|
|
where
|
2023-05-04 21:48:21 -04:00
|
|
|
p = 10 ^ n
|
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
roundPrecisionCur :: CurrencyPrec -> Double -> Rational
|
|
|
|
roundPrecisionCur (CurrencyPrec _ n) = roundPrecision n
|
2022-12-11 17:51:11 -05:00
|
|
|
|
|
|
|
acntPath2Text :: AcntPath -> T.Text
|
|
|
|
acntPath2Text (AcntPath t cs) = T.intercalate "/" (atName t : cs)
|
2023-01-24 23:24:41 -05:00
|
|
|
|
2023-01-27 23:33:34 -05:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- error display
|
|
|
|
|
2023-01-24 23:24:41 -05:00
|
|
|
showError :: InsertError -> [T.Text]
|
2023-04-16 20:09:13 -04:00
|
|
|
showError other = case other of
|
|
|
|
(StatementError ts ms) -> (showTx <$> ts) ++ (showMatch <$> ms)
|
2023-05-29 15:56:15 -04:00
|
|
|
(DaySpanError a b) ->
|
2023-04-16 20:09:13 -04:00
|
|
|
[T.unwords ["Could not create bounds from", showGregorian_ a, "and", showGreg b]]
|
2023-02-05 10:34:26 -05:00
|
|
|
where
|
|
|
|
showGreg (Just g) = showGregorian_ g
|
|
|
|
showGreg Nothing = "Inf"
|
2023-02-25 22:56:23 -05:00
|
|
|
(AccountError a ts) ->
|
2023-04-16 20:09:13 -04:00
|
|
|
[ T.unwords
|
|
|
|
[ "account type of key"
|
|
|
|
, singleQuote a
|
|
|
|
, "is not one of:"
|
|
|
|
, ts_
|
|
|
|
]
|
|
|
|
]
|
2023-02-25 22:56:23 -05:00
|
|
|
where
|
|
|
|
ts_ = T.intercalate ", " $ NE.toList $ fmap atName ts
|
2023-04-16 20:09:13 -04:00
|
|
|
(PatternError s b r p) -> [T.unwords [msg, "in pattern: ", pat]]
|
2023-02-05 10:34:26 -05:00
|
|
|
where
|
|
|
|
pat =
|
|
|
|
keyVals $
|
|
|
|
[ (k, v)
|
|
|
|
| (k, Just v) <-
|
|
|
|
[ ("start", Just $ showT s)
|
|
|
|
, ("by", Just $ showT b)
|
|
|
|
, ("repeats", showT <$> r)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
msg = case p of
|
|
|
|
ZeroLength -> "Zero repeat length"
|
|
|
|
ZeroRepeats -> "Zero repeats"
|
2023-04-16 20:09:13 -04:00
|
|
|
(RegexError re) -> [T.append "could not make regex from pattern: " re]
|
|
|
|
(ConversionError x) -> [T.append "Could not convert to rational number: " x]
|
|
|
|
(InsertIOError msg) -> [T.append "IO Error: " msg]
|
|
|
|
(ParseError msg) -> [T.append "Parse Error: " msg]
|
2023-01-28 20:03:58 -05:00
|
|
|
(MatchValPrecisionError d p) ->
|
2023-04-16 20:09:13 -04:00
|
|
|
[T.unwords ["Match denominator", showT d, "must be less than", showT p]]
|
2023-01-27 23:33:34 -05:00
|
|
|
(LookupError t f) ->
|
2023-04-16 20:09:13 -04:00
|
|
|
[T.unwords ["Could not find field", f, "when resolving", what]]
|
2023-01-27 23:33:34 -05:00
|
|
|
where
|
|
|
|
what = case t of
|
2023-05-29 16:11:19 -04:00
|
|
|
EntryIDField st -> T.unwords ["entry", idName st, "ID"]
|
|
|
|
EntryValField -> "entry value"
|
2023-01-28 22:55:07 -05:00
|
|
|
MatchField mt -> T.unwords [matchName mt, "match"]
|
|
|
|
DBKey st -> T.unwords ["database", idName st, "ID key"]
|
2023-02-26 22:53:12 -05:00
|
|
|
-- TODO this should be its own function
|
2023-01-28 22:55:07 -05:00
|
|
|
idName AcntField = "account"
|
|
|
|
idName CurField = "currency"
|
2023-02-26 22:53:12 -05:00
|
|
|
idName TagField = "tag"
|
2023-01-28 22:55:07 -05:00
|
|
|
matchName MatchNumeric = "numeric"
|
|
|
|
matchName MatchText = "text"
|
2023-04-30 00:16:06 -04:00
|
|
|
(IncomeError day name balance) ->
|
|
|
|
[ T.unwords
|
|
|
|
[ "Income allocations for budget"
|
|
|
|
, singleQuote name
|
|
|
|
, "exceed total on day"
|
|
|
|
, showT day
|
|
|
|
, "where balance is"
|
2023-05-14 19:20:10 -04:00
|
|
|
, showT (fromRational balance :: Double)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
(PeriodError start next) ->
|
|
|
|
[ T.unwords
|
|
|
|
[ "First pay period on "
|
|
|
|
, singleQuote $ showT start
|
|
|
|
, "must start before first income payment on "
|
|
|
|
, singleQuote $ showT next
|
2023-04-30 00:16:06 -04:00
|
|
|
]
|
|
|
|
]
|
2023-06-19 12:33:50 -04:00
|
|
|
(IndexError Entry {eValue = LinkedNumGetter {lngIndex}, eAcnt} day) ->
|
|
|
|
[ T.unwords
|
|
|
|
[ "No credit entry for index"
|
|
|
|
, singleQuote $ showT lngIndex
|
|
|
|
, "for entry with account"
|
|
|
|
, singleQuote eAcnt
|
|
|
|
, "on"
|
|
|
|
, showT day
|
|
|
|
]
|
|
|
|
]
|
|
|
|
(RoundError cur) ->
|
|
|
|
[ T.unwords
|
|
|
|
[ "Could not look up precision for currency"
|
|
|
|
, singleQuote cur
|
|
|
|
]
|
|
|
|
]
|
2023-01-25 20:52:27 -05:00
|
|
|
|
2023-02-05 10:34:26 -05:00
|
|
|
showGregorian_ :: Gregorian -> T.Text
|
2023-02-12 16:23:32 -05:00
|
|
|
showGregorian_ Gregorian {gYear, gMonth, gDay} = T.intercalate "-" $ showT <$> [gYear, gMonth, gDay]
|
2023-02-05 10:34:26 -05:00
|
|
|
|
2023-01-25 20:52:27 -05:00
|
|
|
showTx :: TxRecord -> T.Text
|
|
|
|
showTx TxRecord {trDate = d, trAmount = v, trDesc = e, trFile = f} =
|
|
|
|
T.append "Unmatched transaction: " $
|
|
|
|
keyVals
|
|
|
|
[ ("path", T.pack f)
|
|
|
|
, ("date", T.pack $ iso8601Show d)
|
|
|
|
, ("value", showT (fromRational v :: Float))
|
2023-02-05 12:29:43 -05:00
|
|
|
, ("description", doubleQuote e)
|
2023-01-25 20:52:27 -05:00
|
|
|
]
|
|
|
|
|
2023-02-01 23:02:07 -05:00
|
|
|
showMatch :: MatchRe -> T.Text
|
2023-04-30 23:28:16 -04:00
|
|
|
showMatch StatementParser {spDate, spVal, spDesc, spOther, spTimes, spPriority} =
|
2023-01-25 20:52:27 -05:00
|
|
|
T.append "Unused match: " $ keyVals [(x, y) | (x, Just y) <- kvs]
|
|
|
|
where
|
|
|
|
kvs =
|
2023-04-30 23:28:16 -04:00
|
|
|
[ ("date", showDateMatcher <$> spDate)
|
|
|
|
, ("val", showValMatcher spVal)
|
|
|
|
, ("desc", fst <$> spDesc)
|
2023-01-25 20:52:27 -05:00
|
|
|
, ("other", others)
|
2023-04-30 23:28:16 -04:00
|
|
|
, ("counter", Just $ maybe "Inf" showT spTimes)
|
|
|
|
, ("priority", Just $ showT spPriority)
|
2023-01-25 20:52:27 -05:00
|
|
|
]
|
2023-04-30 23:28:16 -04:00
|
|
|
others = case spOther of
|
2023-01-25 20:52:27 -05:00
|
|
|
[] -> Nothing
|
2023-01-28 20:03:58 -05:00
|
|
|
xs -> Just $ singleQuote $ T.concat $ showMatchOther <$> xs
|
2023-01-25 20:52:27 -05:00
|
|
|
|
2023-01-28 18:52:28 -05:00
|
|
|
-- | Convert match date to text
|
2023-01-28 19:32:56 -05:00
|
|
|
-- Single date matches will just show the single date, and ranged matches will
|
|
|
|
-- show an interval like [YY-MM-DD, YY-MM-DD)
|
2023-04-30 00:16:06 -04:00
|
|
|
showDateMatcher :: DateMatcher -> T.Text
|
|
|
|
showDateMatcher md = case md of
|
|
|
|
(On x) -> showYMDMatcher x
|
|
|
|
(In start n) -> T.concat ["[", showYMDMatcher start, " ", showYMD_ end, ")"]
|
2023-01-28 18:52:28 -05:00
|
|
|
where
|
2023-01-28 19:32:56 -05:00
|
|
|
-- TODO not DRY (this shifting thing happens during the comparison
|
|
|
|
-- function (kinda)
|
2023-04-30 00:16:06 -04:00
|
|
|
end = case fromYMDMatcher start of
|
2023-01-28 19:32:56 -05:00
|
|
|
Y_ y -> Y_ $ y + fromIntegral n
|
|
|
|
YM_ y m ->
|
|
|
|
let (y_, m_) = divMod (m + fromIntegral n - 1) 12
|
|
|
|
in YM_ (y + fromIntegral y_) (m + m_ + 1)
|
|
|
|
YMD_ y m d ->
|
|
|
|
uncurry3 YMD_ $
|
|
|
|
toGregorian $
|
|
|
|
addDays (fromIntegral n) $
|
|
|
|
fromGregorian y m d
|
2023-01-28 18:52:28 -05:00
|
|
|
|
|
|
|
-- | convert YMD match to text
|
2023-04-30 00:16:06 -04:00
|
|
|
showYMDMatcher :: YMDMatcher -> T.Text
|
|
|
|
showYMDMatcher = showYMD_ . fromYMDMatcher
|
2023-01-28 19:32:56 -05:00
|
|
|
|
|
|
|
showYMD_ :: YMD_ -> T.Text
|
|
|
|
showYMD_ md =
|
2023-01-28 18:52:28 -05:00
|
|
|
T.intercalate "-" $ L.take 3 (fmap showT digits ++ L.repeat "*")
|
|
|
|
where
|
|
|
|
digits = case md of
|
2023-01-28 19:32:56 -05:00
|
|
|
Y_ y -> [fromIntegral y]
|
|
|
|
YM_ y m -> [fromIntegral y, m]
|
|
|
|
YMD_ y m d -> [fromIntegral y, m, d]
|
2023-01-25 20:52:27 -05:00
|
|
|
|
2023-04-30 00:16:06 -04:00
|
|
|
showValMatcher :: ValMatcher -> Maybe T.Text
|
2023-04-30 23:28:16 -04:00
|
|
|
showValMatcher ValMatcher {vmSign = Nothing, vmNum = Nothing, vmDen = Nothing} = Nothing
|
|
|
|
showValMatcher ValMatcher {vmNum, vmDen, vmSign, vmPrec} =
|
2023-02-12 16:23:32 -05:00
|
|
|
Just $ singleQuote $ keyVals [(k, v) | (k, Just v) <- kvs]
|
2023-01-28 18:52:28 -05:00
|
|
|
where
|
2023-01-28 20:03:58 -05:00
|
|
|
kvs =
|
2023-04-30 23:28:16 -04:00
|
|
|
[ ("sign", (\s -> if s then "+" else "-") <$> vmSign)
|
|
|
|
, ("numerator", showT <$> vmNum)
|
|
|
|
, ("denominator", showT <$> vmDen)
|
|
|
|
, ("precision", Just $ showT vmPrec)
|
2023-01-28 20:03:58 -05:00
|
|
|
]
|
2023-01-25 20:52:27 -05:00
|
|
|
|
2023-04-30 23:28:16 -04:00
|
|
|
showMatchOther :: FieldMatcherRe -> T.Text
|
2023-02-01 23:02:07 -05:00
|
|
|
showMatchOther (Desc (Field f (re, _))) =
|
2023-01-28 20:03:58 -05:00
|
|
|
T.unwords ["desc field", singleQuote f, "with re", singleQuote re]
|
|
|
|
showMatchOther (Val (Field f mv)) =
|
|
|
|
T.unwords
|
|
|
|
[ "val field"
|
|
|
|
, singleQuote f
|
|
|
|
, "with match value"
|
2023-04-30 00:16:06 -04:00
|
|
|
, singleQuote $ fromMaybe "*" $ showValMatcher mv
|
2023-01-28 20:03:58 -05:00
|
|
|
]
|
2023-01-25 20:52:27 -05:00
|
|
|
|
|
|
|
singleQuote :: T.Text -> T.Text
|
|
|
|
singleQuote t = T.concat ["'", t, "'"]
|
|
|
|
|
|
|
|
doubleQuote :: T.Text -> T.Text
|
|
|
|
doubleQuote t = T.concat ["'", t, "'"]
|
|
|
|
|
|
|
|
keyVal :: T.Text -> T.Text -> T.Text
|
|
|
|
keyVal a b = T.concat [a, "=", b]
|
|
|
|
|
|
|
|
keyVals :: [(T.Text, T.Text)] -> T.Text
|
|
|
|
keyVals = T.intercalate "; " . fmap (uncurry keyVal)
|
2023-01-26 23:41:45 -05:00
|
|
|
|
2023-01-27 23:33:34 -05:00
|
|
|
showT :: Show a => a -> T.Text
|
|
|
|
showT = T.pack . show
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- pure error processing
|
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
-- concatEither2 :: Either x a -> Either x b -> (a -> b -> c) -> Either [x] c
|
|
|
|
-- concatEither2 a b fun = case (a, b) of
|
|
|
|
-- (Right a_, Right b_) -> Right $ fun a_ b_
|
|
|
|
-- _ -> Left $ catMaybes [leftToMaybe a, leftToMaybe b]
|
|
|
|
|
|
|
|
-- concatEither2M :: Monad m => Either x a -> Either x b -> (a -> b -> m c) -> m (Either [x] c)
|
|
|
|
-- concatEither2M a b fun = case (a, b) of
|
|
|
|
-- (Right a_, Right b_) -> Right <$> fun a_ b_
|
|
|
|
-- _ -> return $ Left $ catMaybes [leftToMaybe a, leftToMaybe b]
|
|
|
|
|
|
|
|
-- concatEither3 :: Either x a -> Either x b -> Either x c -> (a -> b -> c -> d) -> Either [x] d
|
|
|
|
-- concatEither3 a b c fun = case (a, b, c) of
|
|
|
|
-- (Right a_, Right b_, Right c_) -> Right $ fun a_ b_ c_
|
|
|
|
-- _ -> Left $ catMaybes [leftToMaybe a, leftToMaybe b, leftToMaybe c]
|
|
|
|
|
|
|
|
-- concatEithers2 :: Either [x] a -> Either [x] b -> (a -> b -> c) -> Either [x] c
|
|
|
|
-- concatEithers2 a b = merge . concatEither2 a b
|
|
|
|
|
|
|
|
-- concatEithers2M
|
|
|
|
-- :: Monad m
|
|
|
|
-- => Either [x] a
|
|
|
|
-- -> Either [x] b
|
|
|
|
-- -> (a -> b -> m c)
|
|
|
|
-- -> m (Either [x] c)
|
|
|
|
-- concatEithers2M a b = fmap merge . concatEither2M a b
|
|
|
|
|
|
|
|
-- concatEithers3
|
|
|
|
-- :: Either [x] a
|
|
|
|
-- -> Either [x] b
|
|
|
|
-- -> Either [x] c
|
|
|
|
-- -> (a -> b -> c -> d)
|
|
|
|
-- -> Either [x] d
|
|
|
|
-- concatEithers3 a b c = merge . concatEither3 a b c
|
|
|
|
|
|
|
|
-- concatEitherL :: [Either x a] -> Either [x] [a]
|
|
|
|
-- concatEitherL as = case partitionEithers as of
|
|
|
|
-- ([], bs) -> Right bs
|
|
|
|
-- (es, _) -> Left es
|
|
|
|
|
|
|
|
-- concatEithersL :: [Either [x] a] -> Either [x] [a]
|
|
|
|
-- concatEithersL = merge . concatEitherL
|
|
|
|
|
|
|
|
-- leftToMaybe :: Either a b -> Maybe a
|
|
|
|
-- leftToMaybe (Left a) = Just a
|
|
|
|
-- leftToMaybe _ = Nothing
|
2023-01-27 20:31:13 -05:00
|
|
|
|
2023-01-28 22:55:07 -05:00
|
|
|
unlessLeft :: (Monad m, MonadPlus n) => Either a b -> (b -> m (n a)) -> m (n a)
|
2023-01-27 20:31:13 -05:00
|
|
|
unlessLeft (Left es) _ = return (return es)
|
2023-01-28 22:55:07 -05:00
|
|
|
unlessLeft (Right rs) f = f rs
|
2023-01-27 20:31:13 -05:00
|
|
|
|
2023-01-28 22:55:07 -05:00
|
|
|
unlessLefts :: (Monad m) => Either (n a) b -> (b -> m (n a)) -> m (n a)
|
2023-01-27 20:31:13 -05:00
|
|
|
unlessLefts (Left es) _ = return es
|
2023-01-28 22:55:07 -05:00
|
|
|
unlessLefts (Right rs) f = f rs
|
|
|
|
|
|
|
|
unlessLeft_ :: (Monad m, MonadPlus n) => Either a b -> (b -> m ()) -> m (n a)
|
|
|
|
unlessLeft_ e f = unlessLeft e (\x -> void (f x) >> return mzero)
|
|
|
|
|
|
|
|
unlessLefts_ :: (Monad m, MonadPlus n) => Either (n a) b -> (b -> m ()) -> m (n a)
|
|
|
|
unlessLefts_ e f = unlessLefts e (\x -> void (f x) >> return mzero)
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
-- plural :: Either a b -> Either [a] b
|
|
|
|
-- plural = first (: [])
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
-- merge :: Either [[a]] b -> Either [a] b
|
|
|
|
-- merge = first concat
|
2023-01-27 23:33:34 -05:00
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- random functions
|
|
|
|
|
|
|
|
-- when bifunctor fails...
|
2023-01-28 19:32:56 -05:00
|
|
|
-- 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)
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-06-16 22:05:28 -04:00
|
|
|
-- 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)
|
|
|
|
|
2023-07-03 20:27:52 -04:00
|
|
|
groupKey :: Ord c => (a -> c) -> [(a, b)] -> [(a, NonEmpty b)]
|
2023-06-25 14:26:35 -04:00
|
|
|
groupKey f = fmap go . NE.groupAllWith (f . fst)
|
|
|
|
where
|
2023-07-03 20:27:52 -04:00
|
|
|
go xs@((c, _) :| _) = (c, fmap snd xs)
|
2023-06-25 14:26:35 -04:00
|
|
|
|
|
|
|
groupWith :: Ord b => (a -> b) -> [a] -> [(b, [a])]
|
|
|
|
groupWith f = fmap go . NE.groupAllWith fst . fmap (\x -> (f x, x))
|
|
|
|
where
|
|
|
|
go xs@((c, _) :| _) = (c, NE.toList $ fmap snd xs)
|
|
|
|
|
2023-06-13 20:12:29 -04:00
|
|
|
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
|
|
|
|
|
2023-01-27 23:33:34 -05:00
|
|
|
uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
|
|
|
|
uncurry3 f (a, b, c) = f a b c
|
|
|
|
|
2023-04-16 20:09:13 -04:00
|
|
|
fstOf3 :: (a, b, c) -> a
|
|
|
|
fstOf3 (a, _, _) = a
|
|
|
|
|
|
|
|
sndOf3 :: (a, b, c) -> b
|
|
|
|
sndOf3 (_, b, _) = b
|
|
|
|
|
|
|
|
thdOf3 :: (a, b, c) -> c
|
|
|
|
thdOf3 (_, _, c) = c
|
|
|
|
|
2023-01-28 19:32:56 -05:00
|
|
|
-- lpad :: a -> Int -> [a] -> [a]
|
|
|
|
-- lpad c n s = replicate (n - length s) c ++ s
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-01-28 19:32:56 -05:00
|
|
|
-- rpad :: a -> Int -> [a] -> [a]
|
|
|
|
-- rpad c n s = s ++ replicate (n - length s) c
|
2023-01-27 23:33:34 -05:00
|
|
|
|
2023-01-28 20:03:58 -05:00
|
|
|
-- lpadT :: Char -> Int -> T.Text -> T.Text
|
|
|
|
-- lpadT c n s = T.append (T.replicate (n - T.length s) (T.singleton c)) s
|
2023-01-28 18:52:28 -05:00
|
|
|
|
2023-02-01 20:56:29 -05:00
|
|
|
-- TODO this regular expression appears to be compiled each time, which is
|
|
|
|
-- super slow
|
|
|
|
-- NOTE: see https://github.com/haskell-hvr/regex-tdfa/issues/9 - performance
|
|
|
|
-- is likely not going to be optimal for text
|
2023-02-01 23:02:07 -05:00
|
|
|
-- matchMaybe :: T.Text -> T.Text -> EitherErr Bool
|
|
|
|
-- matchMaybe q pat = case compres of
|
|
|
|
-- Right re -> case execute re q of
|
|
|
|
-- Right res -> Right $ isJust res
|
|
|
|
-- Left _ -> Left $ RegexError "this should not happen"
|
|
|
|
-- Left _ -> Left $ RegexError pat
|
|
|
|
-- where
|
|
|
|
-- -- these options barely do anything in terms of performance
|
|
|
|
-- compres = compile (blankCompOpt {newSyntax = True}) (blankExecOpt {captureGroups = False}) pat
|
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
compileOptions :: TxOpts T.Text -> InsertExcept TxOptsRe
|
2023-02-05 11:34:37 -05:00
|
|
|
compileOptions o@TxOpts {toAmountFmt = pat} = do
|
|
|
|
re <- compileRegex True pat
|
|
|
|
return $ o {toAmountFmt = re}
|
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
compileMatch :: StatementParser T.Text -> InsertExcept MatchRe
|
2023-04-30 23:28:16 -04:00
|
|
|
compileMatch m@StatementParser {spDesc, spOther} = do
|
2023-05-07 20:29:33 -04:00
|
|
|
combineError dres ores $ \d os -> m {spDesc = d, spOther = os}
|
2023-02-05 11:34:37 -05:00
|
|
|
where
|
|
|
|
go = compileRegex False
|
2023-05-07 20:29:33 -04:00
|
|
|
dres = mapM go spDesc
|
|
|
|
ores = combineErrors $ fmap (mapM go) spOther
|
2023-02-01 23:02:07 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
compileRegex :: Bool -> T.Text -> InsertExcept (Text, Regex)
|
2023-02-05 11:34:37 -05:00
|
|
|
compileRegex groups pat = case res of
|
2023-05-07 20:29:33 -04:00
|
|
|
Right re -> return (pat, re)
|
|
|
|
Left _ -> throwError $ InsertException [RegexError pat]
|
2023-02-01 23:02:07 -05:00
|
|
|
where
|
|
|
|
res =
|
|
|
|
compile
|
|
|
|
(blankCompOpt {newSyntax = True})
|
2023-02-05 11:34:37 -05:00
|
|
|
(blankExecOpt {captureGroups = groups})
|
2023-02-05 12:29:43 -05:00
|
|
|
pat
|
2023-02-01 23:02:07 -05:00
|
|
|
|
2023-05-07 20:29:33 -04:00
|
|
|
matchMaybe :: T.Text -> Regex -> InsertExcept Bool
|
2023-02-01 23:02:07 -05:00
|
|
|
matchMaybe q re = case execute re q of
|
2023-05-07 20:29:33 -04:00
|
|
|
Right res -> return $ isJust res
|
|
|
|
Left _ -> throwError $ InsertException [RegexError "this should not happen"]
|
2023-02-05 11:34:37 -05:00
|
|
|
|
2023-02-05 12:29:43 -05:00
|
|
|
matchGroupsMaybe :: T.Text -> Regex -> [T.Text]
|
2023-02-05 11:34:37 -05:00
|
|
|
matchGroupsMaybe q re = case regexec re q of
|
|
|
|
Right Nothing -> []
|
|
|
|
Right (Just (_, _, _, xs)) -> xs
|
|
|
|
-- this should never fail as regexec always returns Right
|
|
|
|
Left _ -> []
|
2023-05-29 15:56:15 -04:00
|
|
|
|
|
|
|
lookupAccount :: (MonadInsertError m, MonadFinance m) => AcntID -> m (AccountRId, AcntSign, AcntType)
|
|
|
|
lookupAccount = lookupFinance AcntField kmAccount
|
|
|
|
|
|
|
|
lookupAccountKey :: (MonadInsertError m, MonadFinance m) => AcntID -> m AccountRId
|
|
|
|
lookupAccountKey = fmap fstOf3 . lookupAccount
|
|
|
|
|
|
|
|
lookupAccountSign :: (MonadInsertError m, MonadFinance m) => AcntID -> m AcntSign
|
|
|
|
lookupAccountSign = fmap sndOf3 . lookupAccount
|
|
|
|
|
|
|
|
lookupAccountType :: (MonadInsertError m, MonadFinance m) => AcntID -> m AcntType
|
|
|
|
lookupAccountType = fmap thdOf3 . lookupAccount
|
|
|
|
|
2023-06-29 21:32:14 -04:00
|
|
|
lookupCurrency :: (MonadInsertError m, MonadFinance m) => CurID -> m CurrencyPrec
|
2023-05-29 15:56:15 -04:00
|
|
|
lookupCurrency = lookupFinance CurField kmCurrency
|
|
|
|
|
|
|
|
lookupCurrencyKey :: (MonadInsertError m, MonadFinance m) => AcntID -> m CurrencyRId
|
2023-06-29 21:32:14 -04:00
|
|
|
lookupCurrencyKey = fmap cpID . lookupCurrency
|
2023-05-29 15:56:15 -04:00
|
|
|
|
|
|
|
lookupCurrencyPrec :: (MonadInsertError m, MonadFinance m) => AcntID -> m Natural
|
2023-06-29 21:32:14 -04:00
|
|
|
lookupCurrencyPrec = fmap cpPrec . lookupCurrency
|
2023-05-29 15:56:15 -04:00
|
|
|
|
|
|
|
lookupTag :: (MonadInsertError m, MonadFinance m) => TagID -> m TagRId
|
|
|
|
lookupTag = lookupFinance TagField kmTag
|
|
|
|
|
|
|
|
lookupFinance
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
2023-05-29 16:11:19 -04:00
|
|
|
=> EntryIDType
|
2023-05-29 15:56:15 -04:00
|
|
|
-> (DBState -> M.Map T.Text a)
|
|
|
|
-> T.Text
|
|
|
|
-> m a
|
|
|
|
lookupFinance t f c = (liftExcept . lookupErr (DBKey t) c) =<< askDBState f
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
balanceTxs
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
|
|
|
=> [EntryBin]
|
|
|
|
-> m ([UEBalanced], [InsertTx])
|
|
|
|
balanceTxs ebs =
|
|
|
|
first concat . partitionEithers . catMaybes
|
|
|
|
<$> evalStateT (mapErrors go $ L.sortOn binDate ebs) M.empty
|
|
|
|
where
|
2023-07-03 20:27:52 -04:00
|
|
|
go (ToUpdate utx) =
|
|
|
|
fmap (Just . Left) $
|
|
|
|
liftInnerS $
|
|
|
|
either rebalanceTotalEntrySet rebalanceFullEntrySet utx
|
2023-07-01 18:58:15 -04:00
|
|
|
go (ToRead ReadEntry {reCurrency, reAcnt, reValue}) = do
|
|
|
|
modify $ mapAdd_ (reAcnt, reCurrency) reValue
|
|
|
|
return Nothing
|
|
|
|
go (ToInsert Tx {txPrimary, txOther, txDescr, txCommit, txDate}) = do
|
2023-07-03 20:27:52 -04:00
|
|
|
e <- either balancePrimaryEntrySet balanceSecondaryEntrySet txPrimary
|
|
|
|
let tot = sum $ fmap (eValue . ieEntry) $ NE.toList $ iesFromEntries e
|
|
|
|
es <- mapErrors (either balanceSecondaryEntrySet (balancePrimaryEntrySet . fromShadow tot)) txOther
|
2023-07-01 18:58:15 -04:00
|
|
|
let tx =
|
|
|
|
InsertTx
|
|
|
|
{ itxDescr = txDescr
|
|
|
|
, itxDate = txDate
|
2023-07-03 20:27:52 -04:00
|
|
|
, itxEntrySets = e :| es
|
2023-07-01 18:58:15 -04:00
|
|
|
, itxCommit = txCommit
|
|
|
|
}
|
|
|
|
return $ Just $ Right tx
|
2023-07-03 20:27:52 -04:00
|
|
|
fromShadow tot e@EntrySet {esTotalValue} = e {esTotalValue = tot * toRational esTotalValue}
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
binDate :: EntryBin -> Day
|
2023-07-03 20:27:52 -04:00
|
|
|
binDate (ToUpdate (Right UpdateEntrySet {utDate})) = utDate
|
|
|
|
binDate (ToUpdate (Left UpdateEntrySet {utDate})) = utDate
|
2023-07-01 18:58:15 -04:00
|
|
|
binDate (ToRead ReadEntry {reDate}) = reDate
|
|
|
|
binDate (ToInsert Tx {txDate}) = txDate
|
|
|
|
|
|
|
|
type EntryBals = M.Map (AccountRId, CurrencyRId) Rational
|
|
|
|
|
2023-07-03 20:27:52 -04:00
|
|
|
data UpdateEntryType a b
|
2023-07-01 18:58:15 -04:00
|
|
|
= UET_ReadOnly UE_RO
|
2023-07-03 20:27:52 -04:00
|
|
|
| UET_Unk a
|
|
|
|
| UET_Linked b
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
-- TODO make sure new values are rounded properly here
|
2023-07-03 20:27:52 -04:00
|
|
|
rebalanceTotalEntrySet :: TotalUpdateEntrySet -> State EntryBals [UEBalanced]
|
|
|
|
rebalanceTotalEntrySet
|
2023-07-01 18:58:15 -04:00
|
|
|
UpdateEntrySet
|
2023-07-03 20:27:52 -04:00
|
|
|
{ utFrom0 = (f0, f0links)
|
2023-07-01 18:58:15 -04:00
|
|
|
, utTo0
|
2023-07-03 20:27:52 -04:00
|
|
|
, -- , utPairs
|
|
|
|
utFromUnk
|
2023-07-01 18:58:15 -04:00
|
|
|
, utToUnk
|
|
|
|
, utFromRO
|
|
|
|
, utToRO
|
|
|
|
, utCurrency
|
2023-07-03 20:27:52 -04:00
|
|
|
, -- , utToUnkLink0
|
|
|
|
utTotalValue
|
2023-07-01 18:58:15 -04:00
|
|
|
} =
|
|
|
|
do
|
|
|
|
(f0val, (tpairs, fs)) <-
|
|
|
|
fmap (second partitionEithers) $
|
|
|
|
foldM goFrom (utTotalValue, []) $
|
|
|
|
L.sortOn idx $
|
|
|
|
(UET_ReadOnly <$> utFromRO)
|
2023-07-03 20:27:52 -04:00
|
|
|
++ (UET_Linked <$> utFromUnk)
|
|
|
|
let f0' = f0 {ueValue = StaticValue f0val}
|
|
|
|
let tsLink0 = fmap (unlink (-f0val)) f0links
|
2023-07-01 18:58:15 -04:00
|
|
|
(t0val, tsUnk) <-
|
|
|
|
fmap (second catMaybes) $
|
|
|
|
foldM goTo (-utTotalValue, []) $
|
|
|
|
L.sortOn idx2 $
|
|
|
|
(UET_Linked <$> (tpairs ++ tsLink0))
|
|
|
|
++ (UET_Unk <$> utToUnk)
|
|
|
|
++ (UET_ReadOnly <$> utToRO)
|
|
|
|
let t0 = utTo0 {ueValue = StaticValue t0val}
|
2023-07-03 20:27:52 -04:00
|
|
|
return (f0' : fs ++ (t0 : tsUnk))
|
2023-07-01 18:58:15 -04:00
|
|
|
where
|
|
|
|
project f _ _ (UET_ReadOnly e) = f e
|
|
|
|
project _ f _ (UET_Unk e) = f e
|
|
|
|
project _ _ f (UET_Linked p) = f p
|
|
|
|
idx = project ueIndex ueIndex (ueIndex . fst)
|
|
|
|
idx2 = project ueIndex ueIndex ueIndex
|
|
|
|
-- TODO the sum accumulator thing is kinda awkward
|
|
|
|
goFrom (tot, es) (UET_ReadOnly e) = do
|
|
|
|
v <- updateFixed e
|
|
|
|
return (tot - v, es)
|
|
|
|
goFrom (tot, esPrev) (UET_Unk e) = do
|
|
|
|
v <- updateUnknown e
|
|
|
|
return (tot - v, Right e {ueValue = StaticValue v} : esPrev)
|
|
|
|
goFrom (tot, esPrev) (UET_Linked (e0, es)) = do
|
|
|
|
v <- updateUnknown e0
|
|
|
|
let e0' = Right $ e0 {ueValue = StaticValue v}
|
|
|
|
let es' = fmap (Left . unlink (-v)) es
|
|
|
|
return (tot - v, (e0' : es') ++ esPrev)
|
|
|
|
goTo (tot, esPrev) (UET_ReadOnly e) = do
|
|
|
|
v <- updateFixed e
|
|
|
|
return (tot - v, esPrev)
|
|
|
|
goTo (tot, esPrev) (UET_Linked e) = do
|
|
|
|
v <- updateFixed e
|
|
|
|
return (tot - v, Just e : esPrev)
|
|
|
|
goTo (tot, esPrev) (UET_Unk e) = do
|
|
|
|
v <- updateUnknown e
|
|
|
|
return (tot - v, Just e {ueValue = StaticValue v} : esPrev)
|
|
|
|
updateFixed :: UpdateEntry i StaticValue -> State EntryBals Rational
|
|
|
|
updateFixed e = do
|
|
|
|
let v = unStaticValue $ ueValue e
|
|
|
|
modify $ mapAdd_ (ueAcnt e, utCurrency) v
|
|
|
|
return v
|
|
|
|
updateUnknown e = do
|
|
|
|
let key = (ueAcnt e, utCurrency)
|
|
|
|
curBal <- gets (M.findWithDefault 0 key)
|
|
|
|
let v = case ueValue e of
|
|
|
|
EVPercent p -> p * curBal
|
|
|
|
EVBalance p -> p - curBal
|
|
|
|
modify $ mapAdd_ key v
|
|
|
|
return v
|
|
|
|
unlink v e = e {ueValue = StaticValue $ v * unLinkScale (ueValue e)}
|
|
|
|
|
2023-07-03 20:27:52 -04:00
|
|
|
rebalanceFullEntrySet :: FullUpdateEntrySet -> State EntryBals [UEBalanced]
|
|
|
|
rebalanceFullEntrySet
|
|
|
|
UpdateEntrySet
|
|
|
|
{ utFrom0
|
|
|
|
, utTo0
|
|
|
|
, -- , utPairs
|
|
|
|
utFromUnk
|
|
|
|
, utToUnk
|
|
|
|
, utFromRO
|
|
|
|
, utToRO
|
|
|
|
, utCurrency
|
|
|
|
-- , utToUnkLink0
|
|
|
|
} =
|
|
|
|
do
|
|
|
|
let (f_ro, f_lnkd) = case utFrom0 of
|
|
|
|
Left x -> (x : utFromRO, utFromUnk)
|
|
|
|
Right x -> (utFromRO, x : utFromUnk)
|
|
|
|
(tpairs, fs) <-
|
|
|
|
fmap partitionEithers $
|
|
|
|
foldM goFrom [] $
|
|
|
|
L.sortOn idx $
|
|
|
|
(UET_ReadOnly <$> f_ro)
|
|
|
|
++ (UET_Linked <$> f_lnkd)
|
|
|
|
tsUnk <-
|
|
|
|
fmap catMaybes $
|
|
|
|
foldM goTo [] $
|
|
|
|
L.sortOn idx2 $
|
|
|
|
(UET_Linked <$> tpairs)
|
|
|
|
++ (UET_Unk <$> utToUnk)
|
|
|
|
++ (UET_ReadOnly <$> utToRO)
|
|
|
|
let t0val = -(entrySum fs + entrySum tsUnk)
|
|
|
|
let t0 = utTo0 {ueValue = t0val}
|
|
|
|
return (fs ++ (t0 : tsUnk))
|
|
|
|
where
|
|
|
|
project f _ _ (UET_ReadOnly e) = f e
|
|
|
|
project _ f _ (UET_Unk e) = f e
|
|
|
|
project _ _ f (UET_Linked p) = f p
|
|
|
|
idx = project ueIndex ueIndex (ueIndex . fst)
|
|
|
|
idx2 = project ueIndex ueIndex ueIndex
|
|
|
|
-- TODO the sum accumulator thing is kinda awkward
|
|
|
|
goFrom es (UET_ReadOnly e) = do
|
|
|
|
_ <- updateFixed e
|
|
|
|
return es
|
|
|
|
goFrom esPrev (UET_Unk e) = do
|
|
|
|
v <- updateUnknown e
|
|
|
|
return $ Right e {ueValue = StaticValue v} : esPrev
|
|
|
|
goFrom esPrev (UET_Linked (e0, es)) = do
|
|
|
|
v <- updateUnknown e0
|
|
|
|
let e0' = Right $ e0 {ueValue = StaticValue v}
|
|
|
|
let es' = fmap (Left . unlink (-v)) es
|
|
|
|
return $ (e0' : es') ++ esPrev
|
|
|
|
goTo esPrev (UET_ReadOnly e) = do
|
|
|
|
_ <- updateFixed e
|
|
|
|
return esPrev
|
|
|
|
goTo esPrev (UET_Linked e) = do
|
|
|
|
_ <- updateFixed e
|
|
|
|
return $ Just e : esPrev
|
|
|
|
goTo esPrev (UET_Unk e) = do
|
|
|
|
v <- updateUnknown e
|
|
|
|
return $ Just e {ueValue = StaticValue v} : esPrev
|
|
|
|
updateFixed :: UpdateEntry i StaticValue -> State EntryBals Rational
|
|
|
|
updateFixed e = do
|
|
|
|
let v = unStaticValue $ ueValue e
|
|
|
|
modify $ mapAdd_ (ueAcnt e, utCurrency) v
|
|
|
|
return v
|
|
|
|
updateUnknown e = do
|
|
|
|
let key = (ueAcnt e, utCurrency)
|
|
|
|
curBal <- gets (M.findWithDefault 0 key)
|
|
|
|
let v = case ueValue e of
|
|
|
|
EVPercent p -> p * curBal
|
|
|
|
EVBalance p -> p - curBal
|
|
|
|
modify $ mapAdd_ key v
|
|
|
|
return v
|
|
|
|
unlink v e = e {ueValue = StaticValue $ v * unLinkScale (ueValue e)}
|
|
|
|
entrySum = sum . fmap ueValue
|
|
|
|
|
|
|
|
balanceSecondaryEntrySet
|
2023-07-01 18:58:15 -04:00
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
2023-07-03 20:27:52 -04:00
|
|
|
=> SecondayEntrySet
|
|
|
|
-> StateT EntryBals m InsertEntrySet
|
|
|
|
balanceSecondaryEntrySet
|
|
|
|
EntrySet
|
|
|
|
{ esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs}
|
|
|
|
, esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts}
|
|
|
|
, esCurrency = CurrencyPrec {cpID = curID, cpPrec = precision}
|
|
|
|
} =
|
|
|
|
do
|
|
|
|
fs' <- mapErrors resolveAcntAndTags (f0 :| fs)
|
|
|
|
t0' <- resolveAcntAndTags t0
|
|
|
|
ts' <- mapErrors resolveAcntAndTags ts
|
|
|
|
let balFromEntry = balanceEntry (\a -> liftInnerS . balanceDeferred curID a) curID
|
|
|
|
fs'' <- mapErrors balFromEntry fs'
|
|
|
|
let fv = V.fromList $ NE.toList $ fmap (eValue . ieEntry) fs''
|
|
|
|
let balToEntry = balanceEntry (balanceLinked fv curID precision) curID
|
|
|
|
ts'' <- mapErrors balToEntry ts'
|
|
|
|
-- TODO wet
|
|
|
|
let (acntID, sign) = eAcnt t0'
|
|
|
|
let t0Val = -(entrySum (NE.toList fs'') + entrySum ts'')
|
|
|
|
modify (mapAdd_ (acntID, curID) t0Val)
|
|
|
|
let t0'' =
|
|
|
|
InsertEntry
|
|
|
|
{ ieEntry = t0' {eValue = fromIntegral (sign2Int sign) * t0Val, eAcnt = acntID}
|
|
|
|
, ieDeferred = Nothing
|
|
|
|
}
|
|
|
|
-- TODO don't record index here, just keep them in order and let the
|
|
|
|
-- insertion function deal with assigning the index
|
|
|
|
return $
|
|
|
|
InsertEntrySet
|
|
|
|
{ iesCurrency = curID
|
|
|
|
, iesFromEntries = fs''
|
|
|
|
, iesToEntries = t0'' :| ts''
|
|
|
|
}
|
|
|
|
where
|
|
|
|
entrySum = sum . fmap (eValue . ieEntry)
|
|
|
|
|
|
|
|
balancePrimaryEntrySet
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
|
|
|
=> PrimaryEntrySet
|
|
|
|
-> StateT EntryBals m InsertEntrySet
|
|
|
|
balancePrimaryEntrySet
|
2023-07-01 18:58:15 -04:00
|
|
|
EntrySet
|
|
|
|
{ esFrom = HalfEntrySet {hesPrimary = f0, hesOther = fs}
|
|
|
|
, esTo = HalfEntrySet {hesPrimary = t0, hesOther = ts}
|
|
|
|
, esCurrency = CurrencyPrec {cpID = curID, cpPrec = precision}
|
|
|
|
, esTotalValue
|
|
|
|
} =
|
|
|
|
do
|
|
|
|
let f0res = resolveAcntAndTags f0
|
|
|
|
let t0res = resolveAcntAndTags t0
|
2023-07-03 20:27:52 -04:00
|
|
|
let fsres = mapErrors resolveAcntAndTags fs
|
|
|
|
let tsres = mapErrors resolveAcntAndTags ts
|
|
|
|
combineErrorM (combineError f0res fsres (,)) (combineError t0res tsres (,)) $
|
|
|
|
\(f0', fs') (t0', ts') -> do
|
|
|
|
let balFromEntry = balanceEntry (\a -> liftInnerS . balanceDeferred curID a) curID
|
|
|
|
fs'' <- doEntries balFromEntry curID esTotalValue f0' fs'
|
|
|
|
|
|
|
|
let fv = V.fromList $ NE.toList $ fmap (eValue . ieEntry) fs''
|
|
|
|
|
|
|
|
let balToEntry = balanceEntry (balanceLinked fv curID precision) curID
|
|
|
|
ts'' <- doEntries balToEntry curID (-esTotalValue) t0' ts'
|
|
|
|
return $
|
|
|
|
InsertEntrySet
|
|
|
|
{ iesCurrency = curID
|
|
|
|
, iesFromEntries = fs''
|
|
|
|
, iesToEntries = ts''
|
|
|
|
}
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
doEntries
|
|
|
|
:: (MonadInsertError m)
|
2023-07-03 20:27:52 -04:00
|
|
|
=> (Entry (AccountRId, AcntSign) v TagRId -> StateT EntryBals m (InsertEntry AccountRId CurrencyRId TagRId))
|
2023-07-01 18:58:15 -04:00
|
|
|
-> CurrencyRId
|
|
|
|
-> Rational
|
2023-07-03 20:27:52 -04:00
|
|
|
-> Entry (AccountRId, AcntSign) () TagRId
|
|
|
|
-> [Entry (AccountRId, AcntSign) v TagRId]
|
|
|
|
-> StateT EntryBals m (NonEmpty (InsertEntry AccountRId CurrencyRId TagRId))
|
|
|
|
doEntries f curID tot e@Entry {eAcnt = (acntID, sign)} es = do
|
|
|
|
es' <- mapErrors f es
|
2023-07-01 18:58:15 -04:00
|
|
|
let e0val = tot - entrySum es'
|
|
|
|
-- TODO not dry
|
2023-07-03 20:27:52 -04:00
|
|
|
let s = fromIntegral $ sign2Int sign -- NOTE hack
|
|
|
|
modify (mapAdd_ (acntID, curID) e0val)
|
2023-07-01 18:58:15 -04:00
|
|
|
let e' =
|
|
|
|
InsertEntry
|
2023-07-03 20:27:52 -04:00
|
|
|
{ ieEntry = e {eValue = s * e0val, eAcnt = acntID}
|
|
|
|
, ieDeferred = Nothing
|
2023-07-01 18:58:15 -04:00
|
|
|
}
|
2023-07-03 20:27:52 -04:00
|
|
|
return $ e' :| es'
|
2023-07-01 18:58:15 -04:00
|
|
|
where
|
2023-07-03 20:27:52 -04:00
|
|
|
entrySum = sum . fmap (eValue . ieEntry)
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
liftInnerS :: Monad m => StateT e Identity a -> StateT e m a
|
|
|
|
liftInnerS = mapStateT (return . runIdentity)
|
|
|
|
|
|
|
|
balanceLinked
|
|
|
|
:: MonadInsertError m
|
|
|
|
=> Vector Rational
|
|
|
|
-> CurrencyRId
|
|
|
|
-> Natural
|
|
|
|
-> AccountRId
|
|
|
|
-> LinkDeferred Rational
|
|
|
|
-> StateT EntryBals m (Rational, Maybe DBDeferred)
|
|
|
|
balanceLinked from curID precision acntID lg = case lg of
|
|
|
|
(LinkIndex LinkedNumGetter {lngIndex, lngScale}) -> do
|
|
|
|
let res = fmap (go lngScale) $ from V.!? fromIntegral lngIndex
|
|
|
|
case res of
|
|
|
|
Just v -> return (v, Just $ EntryLinked lngIndex $ toRational lngScale)
|
|
|
|
-- TODO this error would be much more informative if I had access to the
|
|
|
|
-- file from which it came
|
|
|
|
Nothing -> throwError undefined
|
|
|
|
(LinkDeferred d) -> liftInnerS $ balanceDeferred curID acntID d
|
|
|
|
where
|
|
|
|
go s = roundPrecision precision . (* s) . fromRational
|
|
|
|
|
|
|
|
balanceDeferred
|
|
|
|
:: CurrencyRId
|
|
|
|
-> AccountRId
|
|
|
|
-> EntryValue Rational
|
|
|
|
-> State EntryBals (Rational, Maybe DBDeferred)
|
|
|
|
balanceDeferred curID acntID (EntryValue t v) = do
|
|
|
|
newval <- findBalance acntID curID t v
|
|
|
|
let d = case t of
|
|
|
|
TFixed -> Nothing
|
|
|
|
TBalance -> Just $ EntryBalance v
|
|
|
|
TPercent -> Just $ EntryPercent v
|
|
|
|
return (newval, d)
|
|
|
|
|
|
|
|
balanceEntry
|
2023-07-03 20:27:52 -04:00
|
|
|
:: (MonadInsertError m)
|
2023-07-01 18:58:15 -04:00
|
|
|
=> (AccountRId -> v -> StateT EntryBals m (Rational, Maybe DBDeferred))
|
|
|
|
-> CurrencyRId
|
2023-07-03 20:27:52 -04:00
|
|
|
-> Entry (AccountRId, AcntSign) v TagRId
|
2023-07-01 18:58:15 -04:00
|
|
|
-> StateT EntryBals m (InsertEntry AccountRId CurrencyRId TagRId)
|
2023-07-03 20:27:52 -04:00
|
|
|
balanceEntry f curID e@Entry {eValue, eAcnt = (acntID, sign)} = do
|
|
|
|
let s = fromIntegral $ sign2Int sign
|
|
|
|
(newVal, deferred) <- f acntID eValue
|
|
|
|
modify (mapAdd_ (acntID, curID) newVal)
|
|
|
|
return $
|
|
|
|
InsertEntry
|
|
|
|
{ ieEntry = e {eValue = s * newVal, eAcnt = acntID}
|
|
|
|
, ieDeferred = deferred
|
|
|
|
}
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
resolveAcntAndTags
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
|
|
|
=> Entry AcntID v TagID
|
2023-07-03 20:27:52 -04:00
|
|
|
-> m (Entry (AccountRId, AcntSign) v TagRId)
|
2023-07-01 18:58:15 -04:00
|
|
|
resolveAcntAndTags e@Entry {eAcnt, eTags} = do
|
|
|
|
let acntRes = lookupAccount eAcnt
|
|
|
|
let tagRes = mapErrors lookupTag eTags
|
|
|
|
combineError acntRes tagRes $
|
2023-07-03 20:27:52 -04:00
|
|
|
\(acntID, sign, _) tags -> e {eAcnt = (acntID, sign), eTags = tags}
|
2023-07-01 18:58:15 -04:00
|
|
|
|
|
|
|
findBalance
|
|
|
|
:: AccountRId
|
|
|
|
-> CurrencyRId
|
|
|
|
-> TransferType
|
|
|
|
-> Rational
|
|
|
|
-> State EntryBals Rational
|
|
|
|
findBalance acnt cur t v = do
|
|
|
|
curBal <- gets (M.findWithDefault 0 (acnt, cur))
|
|
|
|
return $ case t of
|
|
|
|
TBalance -> v - curBal
|
|
|
|
TPercent -> v * curBal
|
|
|
|
TFixed -> v
|
|
|
|
|
|
|
|
expandTransfers
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
|
|
|
=> TxCommit
|
|
|
|
-> DaySpan
|
|
|
|
-> [PairedTransfer]
|
|
|
|
-> m [Tx TxCommit]
|
|
|
|
expandTransfers tc bounds = fmap concat . mapErrors (expandTransfer tc bounds)
|
|
|
|
|
|
|
|
expandTransfer
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
|
|
|
=> TxCommit
|
|
|
|
-> DaySpan
|
|
|
|
-> PairedTransfer
|
|
|
|
-> m [Tx TxCommit]
|
|
|
|
expandTransfer tc bounds Transfer {transAmounts, transTo, transCurrency, transFrom} = do
|
|
|
|
txs <- mapErrors go transAmounts
|
2023-07-03 20:27:52 -04:00
|
|
|
return $ concat txs
|
2023-07-01 18:58:15 -04:00
|
|
|
where
|
|
|
|
go
|
|
|
|
Amount
|
|
|
|
{ amtWhen = pat
|
|
|
|
, amtValue = TransferValue {tvVal = v, tvType = t}
|
|
|
|
, amtDesc = desc
|
|
|
|
} =
|
2023-07-03 20:27:52 -04:00
|
|
|
withDates bounds pat $ \day -> do
|
|
|
|
p <- entryPair transFrom transTo transCurrency desc () (EntryValue t (toRational (-v)))
|
2023-07-01 18:58:15 -04:00
|
|
|
return
|
|
|
|
Tx
|
|
|
|
{ txCommit = tc
|
|
|
|
, txDate = day
|
2023-07-03 20:27:52 -04:00
|
|
|
, txPrimary = Right p
|
2023-07-01 18:58:15 -04:00
|
|
|
, txOther = []
|
|
|
|
, txDescr = desc
|
|
|
|
}
|
|
|
|
|
|
|
|
entryPair
|
|
|
|
:: (MonadInsertError m, MonadFinance m)
|
|
|
|
=> TaggedAcnt
|
|
|
|
-> TaggedAcnt
|
|
|
|
-> CurID
|
|
|
|
-> T.Text
|
2023-07-03 20:27:52 -04:00
|
|
|
-> v0
|
|
|
|
-> v1
|
|
|
|
-> m (EntrySet v0 v1 v2 v3)
|
|
|
|
entryPair (TaggedAcnt fa fts) (TaggedAcnt ta tts) curid com totval val1 = do
|
2023-07-01 18:58:15 -04:00
|
|
|
cp <- lookupCurrency curid
|
2023-07-03 20:27:52 -04:00
|
|
|
return $
|
|
|
|
EntrySet
|
|
|
|
{ esCurrency = cp
|
|
|
|
, esTotalValue = totval
|
|
|
|
, esFrom = halfEntry fa fts val1
|
|
|
|
, esTo = halfEntry ta tts ()
|
|
|
|
}
|
2023-07-01 18:58:15 -04:00
|
|
|
where
|
2023-07-03 20:27:52 -04:00
|
|
|
halfEntry :: AcntID -> [TagID] -> v -> HalfEntrySet v v0
|
|
|
|
halfEntry a ts v =
|
2023-07-01 18:58:15 -04:00
|
|
|
HalfEntrySet
|
2023-07-03 20:27:52 -04:00
|
|
|
{ hesPrimary = Entry {eAcnt = a, eValue = v, eComment = com, eTags = ts}
|
2023-07-01 18:58:15 -04:00
|
|
|
, hesOther = []
|
|
|
|
}
|
|
|
|
|
|
|
|
withDates
|
|
|
|
:: (MonadFinance m, MonadInsertError m)
|
2023-07-03 20:27:52 -04:00
|
|
|
=> DaySpan
|
|
|
|
-> DatePat
|
2023-07-01 18:58:15 -04:00
|
|
|
-> (Day -> m a)
|
|
|
|
-> m [a]
|
2023-07-03 20:27:52 -04:00
|
|
|
withDates bounds dp f = do
|
2023-07-01 18:58:15 -04:00
|
|
|
days <- liftExcept $ expandDatePat bounds dp
|
|
|
|
combineErrors $ fmap f days
|