dev: lib, cli, bin: enable/fix name shadowing warnings

And a few other cleanups.
This commit is contained in:
Simon Michael 2022-08-23 11:58:31 +01:00
parent 96db4fe9cc
commit c80c72d7cd
61 changed files with 591 additions and 499 deletions

View File

@ -206,7 +206,6 @@ WARNINGS:=\
-Wall \ -Wall \
-Wno-incomplete-uni-patterns \ -Wno-incomplete-uni-patterns \
-Wno-missing-signatures \ -Wno-missing-signatures \
-Wno-name-shadowing \
-Wno-orphans \ -Wno-orphans \
-Wno-type-defaults \ -Wno-type-defaults \
-Wno-unused-do-bind \ -Wno-unused-do-bind \
@ -418,7 +417,7 @@ ghci-web-test: webdirs $(call def-help,ghci-web-test, start ghci REPL on hledger
# better than stack exec ? # better than stack exec ?
# XXX does not see changes to files # XXX does not see changes to files
ghci-unit-test: $(call def-help,ghci-unit-test, start ghci REPL on hledger-lib + unit test suite) ghci-unit-test: $(call def-help,ghci-unit-test, start ghci REPL on hledger-lib + unit test suite)
$(STACKGHCI) ghci --ghc-options='-rtsopts -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-name-shadowing -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind -ihledger-lib -DDEVELOPMENT -DVERSION="\"1.26.99\""' hledger-lib/test/unittest.hs $(STACKGHCI) ghci --ghc-options='-rtsopts $(WARNINGS) -ihledger-lib -DDEVELOPMENT -DVERSION="\"1.26.99\""' hledger-lib/test/unittest.hs
# ghci-all: $(call def-help,ghci-all, start ghci REPL on all the hledger) # ghci-all: $(call def-help,ghci-all, start ghci REPL on all the hledger)
# $(STACK) exec -- $(GHCI) $(BUILDFLAGS) \ # $(STACK) exec -- $(GHCI) $(BUILDFLAGS) \

View File

@ -3,7 +3,7 @@
-- Run from inside the hledger source tree, or compile with compile.sh. -- Run from inside the hledger source tree, or compile with compile.sh.
-- See hledger-check-fancyassertions.hs. -- See hledger-check-fancyassertions.hs.
-- {-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-} -- {-# OPTIONS_GHC -Wno-missing-signatures #-}
{-| Construct two balance reports for two different time periods and use one of the as "budget" for {-| Construct two balance reports for two different time periods and use one of the as "budget" for
the other, thus comparing them the other, thus comparing them

View File

@ -5,7 +5,7 @@
{- Construct two balance reports for two different time periods and render them side by side -} {- Construct two balance reports for two different time periods and render them side by side -}
{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-} {-# OPTIONS_GHC -Wno-missing-signatures #-}
import System.Environment (getArgs) import System.Environment (getArgs)
import Hledger.Cli import Hledger.Cli

View File

@ -15,7 +15,7 @@
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# OPTIONS_GHC -Wall -Wno-missing-signatures -Wno-name-shadowing #-} {-# OPTIONS_GHC -Wall -Wno-missing-signatures #-}
import Data.List import Data.List
import Data.Maybe import Data.Maybe

View File

@ -3,7 +3,7 @@
-- Run from inside the hledger source tree, or compile with compile.sh. -- Run from inside the hledger source tree, or compile with compile.sh.
-- See hledger-check-fancyassertions.hs. -- See hledger-check-fancyassertions.hs.
{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-} {-# OPTIONS_GHC -Wno-missing-signatures #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}

View File

@ -104,10 +104,10 @@ accountTree :: AccountName -> [AccountName] -> Account
accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m } accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m }
where where
T m = treeFromPaths $ map expandAccountName as :: FastTree AccountName T m = treeFromPaths $ map expandAccountName as :: FastTree AccountName
accountTree' a (T m) = accountTree' a (T m') =
nullacct{ nullacct{
aname=a aname=a
,asubs=map (uncurry accountTree') $ M.assocs m ,asubs=map (uncurry accountTree') $ M.assocs m'
} }
-- | An efficient-to-build tree suggested by Cale Gibbard, probably -- | An efficient-to-build tree suggested by Cale Gibbard, probably
@ -223,7 +223,7 @@ pruneAccounts p = headMay . prune
-- tree's structure remains intact and can still be used. It's a tree/list! -- tree's structure remains intact and can still be used. It's a tree/list!
flattenAccounts :: Account -> [Account] flattenAccounts :: Account -> [Account]
flattenAccounts a = squish a [] flattenAccounts a = squish a []
where squish a as = a : Prelude.foldr squish as (asubs a) where squish a' as = a' : Prelude.foldr squish as (asubs a')
-- | Filter an account tree (to a list). -- | Filter an account tree (to a list).
filterAccounts :: (Account -> Bool) -> Account -> [Account] filterAccounts :: (Account -> Bool) -> Account -> [Account]

View File

@ -177,12 +177,12 @@ concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map a
-- Or, return any error arising from a bad regular expression in the aliases. -- Or, return any error arising from a bad regular expression in the aliases.
accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName
accountNameApplyAliases aliases a = accountNameApplyAliases aliases a =
let (aname,atype) = (accountNameWithoutPostingType a, accountNamePostingType a) let (name,typ) = (accountNameWithoutPostingType a, accountNamePostingType a)
in foldM in foldM
(\acct alias -> dbg6 "result" $ aliasReplace (dbg6 "alias" alias) (dbg6 "account" acct)) (\acct alias -> dbg6 "result" $ aliasReplace (dbg6 "alias" alias) (dbg6 "account" acct))
aname name
aliases aliases
>>= Right . accountNameWithPostingType atype >>= Right . accountNameWithPostingType typ
-- | Memoising version of accountNameApplyAliases, maybe overkill. -- | Memoising version of accountNameApplyAliases, maybe overkill.
accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName
@ -238,7 +238,7 @@ parentAccountNames :: AccountName -> [AccountName]
parentAccountNames a = parentAccountNames' $ parentAccountName a parentAccountNames a = parentAccountNames' $ parentAccountName a
where where
parentAccountNames' "" = [] parentAccountNames' "" = []
parentAccountNames' a = a : parentAccountNames' (parentAccountName a) parentAccountNames' a2 = a2 : parentAccountNames' (parentAccountName a2)
-- | Is the first account a parent or other ancestor of (and not the same as) the second ? -- | Is the first account a parent or other ancestor of (and not the same as) the second ?
isAccountNamePrefixOf :: AccountName -> AccountName -> Bool isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
@ -296,9 +296,9 @@ elideAccountName width s
fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s
where where
elideparts :: Int -> [Text] -> [Text] -> [Text] elideparts :: Int -> [Text] -> [Text] -> [Text]
elideparts width done ss elideparts w done ss
| realLength (accountNameFromComponents $ done++ss) <= width = done++ss | realLength (accountNameFromComponents $ done++ss) <= w = done++ss
| length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss) | length ss > 1 = elideparts w (done++[textTakeWidth 2 $ head ss]) (tail ss)
| otherwise = done++ss | otherwise = done++ss
-- | Keep only the first n components of an account name, where n -- | Keep only the first n components of an account name, where n

View File

@ -255,6 +255,7 @@ instance Num Amount where
(-) = similarAmountsOp (-) (-) = similarAmountsOp (-)
(*) = similarAmountsOp (*) (*) = similarAmountsOp (*)
-- TODO: amount, num are clashy
-- | The empty simple amount. -- | The empty simple amount.
amount, nullamt :: Amount amount, nullamt :: Amount
amount = Amount{acommodity="", aquantity=0, aprice=Nothing, astyle=amountstyle} amount = Amount{acommodity="", aquantity=0, aprice=Nothing, astyle=amountstyle}
@ -314,8 +315,8 @@ amountCost a@Amount{aquantity=q, aprice=mp} =
transformAmount :: (Quantity -> Quantity) -> Amount -> Amount transformAmount :: (Quantity -> Quantity) -> Amount -> Amount
transformAmount f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p} transformAmount f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p}
where where
f' (TotalPrice a@Amount{aquantity=pq}) = TotalPrice a{aquantity = f pq} f' (TotalPrice a1@Amount{aquantity=pq}) = TotalPrice a1{aquantity = f pq}
f' p = p f' p' = p'
-- | Divide an amount's quantity (and its total price, if it has one) by a constant. -- | Divide an amount's quantity (and its total price, if it has one) by a constant.
divideAmount :: Quantity -> Amount -> Amount divideAmount :: Quantity -> Amount -> Amount
@ -522,15 +523,15 @@ showamountquantity amt@Amount{astyle=AmountStyle{asdecimalpoint=mdec, asdigitgro
applyDigitGroupStyle :: Maybe DigitGroupStyle -> Int -> T.Text -> WideBuilder applyDigitGroupStyle :: Maybe DigitGroupStyle -> Int -> T.Text -> WideBuilder
applyDigitGroupStyle Nothing l s = WideBuilder (TB.fromText s) l applyDigitGroupStyle Nothing l s = WideBuilder (TB.fromText s) l
applyDigitGroupStyle (Just (DigitGroups _ [])) l s = WideBuilder (TB.fromText s) l applyDigitGroupStyle (Just (DigitGroups _ [])) l s = WideBuilder (TB.fromText s) l
applyDigitGroupStyle (Just (DigitGroups c (g:gs))) l s = addseps (g:|gs) (toInteger l) s applyDigitGroupStyle (Just (DigitGroups c (g0:gs0))) l0 s0 = addseps (g0:|gs0) (toInteger l0) s0
where where
addseps (g:|gs) l s addseps (g1:|gs1) l1 s1
| l' > 0 = addseps gs' l' rest <> WideBuilder (TB.singleton c <> TB.fromText part) (fromIntegral g + 1) | l2 > 0 = addseps gs2 l2 rest <> WideBuilder (TB.singleton c <> TB.fromText part) (fromIntegral g1 + 1)
| otherwise = WideBuilder (TB.fromText s) (fromInteger l) | otherwise = WideBuilder (TB.fromText s1) (fromInteger l1)
where where
(rest, part) = T.splitAt (fromInteger l') s (rest, part) = T.splitAt (fromInteger l2) s1
gs' = fromMaybe (g:|[]) $ nonEmpty gs gs2 = fromMaybe (g1:|[]) $ nonEmpty gs1
l' = l - toInteger g l2 = l1 - toInteger g1
-- like journalCanonicaliseAmounts -- like journalCanonicaliseAmounts
-- | Canonicalise an amount's display style using the provided commodity style map. -- | Canonicalise an amount's display style using the provided commodity style map.
@ -702,11 +703,11 @@ maCommodities = S.fromList . fmap acommodity . amounts'
unifyMixedAmount :: MixedAmount -> Maybe Amount unifyMixedAmount :: MixedAmount -> Maybe Amount
unifyMixedAmount = foldM combine 0 . amounts unifyMixedAmount = foldM combine 0 . amounts
where where
combine amount result combine amt result
| amountIsZero amount = Just result | amountIsZero amt = Just result
| amountIsZero result = Just amount | amountIsZero result = Just amt
| acommodity amount == acommodity result = Just $ amount + result | acommodity amt == acommodity result = Just $ amt + result
| otherwise = Nothing | otherwise = Nothing
-- | Sum same-commodity amounts in a lossy way, applying the first -- | Sum same-commodity amounts in a lossy way, applying the first
-- price to the result and discarding any other prices. Only used as a -- price to the result and discarding any other prices. Only used as a
@ -839,10 +840,10 @@ showMixedAmountDebug m | m == missingmixedamt = "(missing)"
showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder
showMixedAmountB opts ma showMixedAmountB opts ma
| displayOneLine opts = showMixedAmountOneLineB opts ma | displayOneLine opts = showMixedAmountOneLineB opts ma
| otherwise = WideBuilder (wbBuilder . mconcat $ intersperse sep lines) width | otherwise = WideBuilder (wbBuilder . mconcat $ intersperse sep ls) width
where where
lines = showMixedAmountLinesB opts ma ls = showMixedAmountLinesB opts ma
width = headDef 0 $ map wbWidth lines width = headDef 0 $ map wbWidth ls
sep = WideBuilder (TB.singleton '\n') 0 sep = WideBuilder (TB.singleton '\n') 0
-- | Helper for showMixedAmountB to show a list of Amounts on multiple lines. This returns -- | Helper for showMixedAmountB to show a list of Amounts on multiple lines. This returns
@ -900,7 +901,7 @@ showMixedAmountOneLineB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWi
dropWhileRev p = foldr (\x xs -> if null xs && p x then [] else x:xs) [] dropWhileRev p = foldr (\x xs -> if null xs && p x then [] else x:xs) []
-- Add the elision strings (if any) to each amount -- Add the elision strings (if any) to each amount
withElided = zipWith (\num amt -> (amt, elisionDisplay Nothing (wbWidth sep) num amt)) [n-1,n-2..0] withElided = zipWith (\n2 amt -> (amt, elisionDisplay Nothing (wbWidth sep) n2 amt)) [n-1,n-2..0]
orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount] orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount]
orderedAmounts dopts = maybe id (mapM pad) (displayOrder dopts) . amounts orderedAmounts dopts = maybe id (mapM pad) (displayOrder dopts) . amounts

View File

@ -315,11 +315,11 @@ priceInferrerFor t pt = maybe id inferprice inferFromAndTo
-- For each posting, if the posting type matches, there is only a single amount in the posting, -- For each posting, if the posting type matches, there is only a single amount in the posting,
-- and the commodity of the amount matches the amount we're converting from, -- and the commodity of the amount matches the amount we're converting from,
-- then set its price based on the ratio between fromamount and toamount. -- then set its price based on the ratio between fromamount and toamount.
inferprice (fromamount, toamount) posting inferprice (fromamount, toamount) p
| [a] <- amounts (pamount posting), ptype posting == pt, acommodity a == acommodity fromamount | [a] <- amounts (pamount p), ptype p == pt, acommodity a == acommodity fromamount
= posting{ pamount = mixedAmount a{aprice=Just conversionprice} = p{ pamount = mixedAmount a{aprice=Just conversionprice}
, poriginal = Just $ originalPosting posting } , poriginal = Just $ originalPosting p }
| otherwise = posting | otherwise = p
where where
-- If only one Amount in the posting list matches fromamount we can use TotalPrice. -- If only one Amount in the posting list matches fromamount we can use TotalPrice.
-- Otherwise divide the conversion equally among the Amounts by using a unit price. -- Otherwise divide the conversion equally among the Amounts by using a unit price.

View File

@ -2,7 +2,6 @@
{-# LANGUAGE NoMonoLocalBinds #-} {-# LANGUAGE NoMonoLocalBinds #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-| {-|
@ -348,9 +347,9 @@ latestSpanContaining :: [DateSpan] -> Day -> Maybe DateSpan
latestSpanContaining datespans = go latestSpanContaining datespans = go
where where
go day = do go day = do
span <- Set.lookupLT supSpan spanSet spn <- Set.lookupLT supSpan spanSet
guard $ spanContainsDate span day guard $ spanContainsDate spn day
return span return spn
where where
-- The smallest DateSpan larger than any DateSpan containing day. -- The smallest DateSpan larger than any DateSpan containing day.
supSpan = DateSpan (Just $ addDays 1 day) Nothing supSpan = DateSpan (Just $ addDays 1 day) Nothing
@ -387,18 +386,19 @@ spanFromSmartDate :: Day -> SmartDate -> DateSpan
spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e) spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e)
where where
(ry,rm,_) = toGregorian refdate (ry,rm,_) = toGregorian refdate
(b,e) = span sdate (b,e) = span' sdate
span :: SmartDate -> (Day,Day) where
span (SmartCompleteDate day) = (day, nextday day) span' :: SmartDate -> (Day,Day)
span (SmartAssumeStart y Nothing) = (startofyear day, nextyear day) where day = fromGregorian y 1 1 span' (SmartCompleteDate day) = (day, nextday day)
span (SmartAssumeStart y (Just m)) = (startofmonth day, nextmonth day) where day = fromGregorian y m 1 span' (SmartAssumeStart y Nothing) = (startofyear day, nextyear day) where day = fromGregorian y 1 1
span (SmartFromReference m d) = (day, nextday day) where day = fromGregorian ry (fromMaybe rm m) d span' (SmartAssumeStart y (Just m)) = (startofmonth day, nextmonth day) where day = fromGregorian y m 1
span (SmartMonth m) = (startofmonth day, nextmonth day) where day = fromGregorian ry m 1 span' (SmartFromReference m d) = (day, nextday day) where day = fromGregorian ry (fromMaybe rm m) d
span (SmartRelative n Day) = (addDays n refdate, addDays (n+1) refdate) span' (SmartMonth m) = (startofmonth day, nextmonth day) where day = fromGregorian ry m 1
span (SmartRelative n Week) = (addDays (7*n) d, addDays (7*n+7) d) where d = thisweek refdate span' (SmartRelative n Day) = (addDays n refdate, addDays (n+1) refdate)
span (SmartRelative n Month) = (addGregorianMonthsClip n d, addGregorianMonthsClip (n+1) d) where d = thismonth refdate span' (SmartRelative n Week) = (addDays (7*n) d, addDays (7*n+7) d) where d = thisweek refdate
span (SmartRelative n Quarter) = (addGregorianMonthsClip (3*n) d, addGregorianMonthsClip (3*n+3) d) where d = thisquarter refdate span' (SmartRelative n Month) = (addGregorianMonthsClip n d, addGregorianMonthsClip (n+1) d) where d = thismonth refdate
span (SmartRelative n Year) = (addGregorianYearsClip n d, addGregorianYearsClip (n+1) d) where d = thisyear refdate span' (SmartRelative n Quarter) = (addGregorianMonthsClip (3*n) d, addGregorianMonthsClip (3*n+3) d) where d = thisquarter refdate
span' (SmartRelative n Year) = (addGregorianYearsClip n d, addGregorianYearsClip (n+1) d) where d = thisyear refdate
-- showDay :: Day -> String -- showDay :: Day -> String
-- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day -- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day
@ -541,7 +541,7 @@ thisquarter = startofquarter
startofquarter day = fromGregorian y (firstmonthofquarter m) 1 startofquarter day = fromGregorian y (firstmonthofquarter m) 1
where where
(y,m,_) = toGregorian day (y,m,_) = toGregorian day
firstmonthofquarter m = ((m-1) `div` 3) * 3 + 1 firstmonthofquarter m2 = ((m2-1) `div` 3) * 3 + 1
thisyear = startofyear thisyear = startofyear
prevyear = startofyear . addGregorianYearsClip (-1) prevyear = startofyear . addGregorianYearsClip (-1)
@ -577,14 +577,14 @@ intervalStartBefore int d =
-- >>> nthdayofyearcontaining 1 1 wed22nd -- >>> nthdayofyearcontaining 1 1 wed22nd
-- 2017-01-01 -- 2017-01-01
nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day
nthdayofyearcontaining m md date nthdayofyearcontaining m mdy date
-- PARTIAL: -- PARTIAL:
| not (validMonth m) = error' $ "nthdayofyearcontaining: invalid month "++show m | not (validMonth m) = error' $ "nthdayofyearcontaining: invalid month "++show m
| not (validDay md) = error' $ "nthdayofyearcontaining: invalid day " ++show md | not (validDay mdy) = error' $ "nthdayofyearcontaining: invalid day " ++show mdy
| mmddOfSameYear <= date = mmddOfSameYear | mmddOfSameYear <= date = mmddOfSameYear
| otherwise = mmddOfPrevYear | otherwise = mmddOfPrevYear
where mmddOfSameYear = addDays (toInteger md-1) $ applyN (m-1) nextmonth s where mmddOfSameYear = addDays (toInteger mdy-1) $ applyN (m-1) nextmonth s
mmddOfPrevYear = addDays (toInteger md-1) $ applyN (m-1) nextmonth $ prevyear s mmddOfPrevYear = addDays (toInteger mdy-1) $ applyN (m-1) nextmonth $ prevyear s
s = startofyear date s = startofyear date
-- | For given date d find month-long interval that starts on nth day of month -- | For given date d find month-long interval that starts on nth day of month
@ -606,13 +606,13 @@ nthdayofyearcontaining m md date
-- >>> nthdayofmonthcontaining 30 wed22nd -- >>> nthdayofmonthcontaining 30 wed22nd
-- 2017-10-30 -- 2017-10-30
nthdayofmonthcontaining :: MonthDay -> Day -> Day nthdayofmonthcontaining :: MonthDay -> Day -> Day
nthdayofmonthcontaining md date nthdayofmonthcontaining mdy date
-- PARTIAL: -- PARTIAL:
| not (validDay md) = error' $ "nthdayofmonthcontaining: invalid day " ++show md | not (validDay mdy) = error' $ "nthdayofmonthcontaining: invalid day " ++show mdy
| nthOfSameMonth <= date = nthOfSameMonth | nthOfSameMonth <= date = nthOfSameMonth
| otherwise = nthOfPrevMonth | otherwise = nthOfPrevMonth
where nthOfSameMonth = nthdayofmonth md s where nthOfSameMonth = nthdayofmonth mdy s
nthOfPrevMonth = nthdayofmonth md $ prevmonth s nthOfPrevMonth = nthdayofmonth mdy $ prevmonth s
s = startofmonth date s = startofmonth date
-- | For given date d find week-long interval that starts on nth day of week -- | For given date d find week-long interval that starts on nth day of week
@ -807,8 +807,8 @@ yyyymmdd :: TextParser m SmartDate
yyyymmdd = do yyyymmdd = do
y <- read <$> count 4 digitChar y <- read <$> count 4 digitChar
m <- read <$> count 2 digitChar m <- read <$> count 2 digitChar
md <- optional $ read <$> count 2 digitChar mdy <- optional $ read <$> count 2 digitChar
case md of case mdy of
Nothing -> failIfInvalidDate $ SmartAssumeStart y (Just m) Nothing -> failIfInvalidDate $ SmartAssumeStart y (Just m)
Just d -> maybe (Fail.fail $ showBadDate y m d) (return . SmartCompleteDate) $ Just d -> maybe (Fail.fail $ showBadDate y m d) (return . SmartCompleteDate) $
fromGregorianValid y m d fromGregorianValid y m d
@ -1080,19 +1080,19 @@ tests_Dates = testGroup "Dates"
] ]
, testCase "match dayOfWeek" $ do , testCase "match dayOfWeek" $ do
let dayofweek n s = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1 s let dayofweek n = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1
match ds day = splitSpan (DaysOfWeek [day]) ds @?= dayofweek day ds matchdow ds day = splitSpan (DaysOfWeek [day]) ds @?= dayofweek day ds
ys2021 = fromGregorian 2021 01 01 ys2021 = fromGregorian 2021 01 01
ye2021 = fromGregorian 2021 12 31 ye2021 = fromGregorian 2021 12 31
ys2022 = fromGregorian 2022 01 01 ys2022 = fromGregorian 2022 01 01
mapM_ (match (DateSpan (Just ys2021) (Just ye2021))) [1..7] mapM_ (matchdow (DateSpan (Just ys2021) (Just ye2021))) [1..7]
mapM_ (match (DateSpan (Just ys2021) (Just ys2022))) [1..7] mapM_ (matchdow (DateSpan (Just ys2021) (Just ys2022))) [1..7]
mapM_ (match (DateSpan (Just ye2021) (Just ys2022))) [1..7] mapM_ (matchdow (DateSpan (Just ye2021) (Just ys2022))) [1..7]
mapM_ (match (DateSpan (Just ye2021) Nothing)) [1..7] mapM_ (matchdow (DateSpan (Just ye2021) Nothing)) [1..7]
mapM_ (match (DateSpan (Just ys2022) Nothing)) [1..7] mapM_ (matchdow (DateSpan (Just ys2022) Nothing)) [1..7]
mapM_ (match (DateSpan Nothing (Just ye2021))) [1..7] mapM_ (matchdow (DateSpan Nothing (Just ye2021))) [1..7]
mapM_ (match (DateSpan Nothing (Just ys2022))) [1..7] mapM_ (matchdow (DateSpan Nothing (Just ys2022))) [1..7]
] ]

View File

@ -111,24 +111,24 @@ makePostingAccountErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe I
makePostingAccountErrorExcerpt p = makePostingErrorExcerpt p finderrcols makePostingAccountErrorExcerpt p = makePostingErrorExcerpt p finderrcols
where where
-- Calculate columns suitable for highlighting the synthetic excerpt. -- Calculate columns suitable for highlighting the synthetic excerpt.
finderrcols p _ _ = Just (col, Just col2) finderrcols p' _ _ = Just (col, Just col2)
where where
col = 5 + if isVirtual p then 1 else 0 col = 5 + if isVirtual p' then 1 else 0
col2 = col + T.length (paccount p) - 1 col2 = col + T.length (paccount p') - 1
-- | From the given posting, make an error excerpt showing the transaction with -- | From the given posting, make an error excerpt showing the transaction with
-- the balance assertion highlighted. -- the balance assertion highlighted.
makeBalanceAssertionErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe Int), Text) makeBalanceAssertionErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe Int), Text)
makeBalanceAssertionErrorExcerpt p = makePostingErrorExcerpt p finderrcols makeBalanceAssertionErrorExcerpt p = makePostingErrorExcerpt p finderrcols
where where
finderrcols p t trendered = Just (col, Just col2) finderrcols p' t trendered = Just (col, Just col2)
where where
-- Analyse the rendering to find the columns to highlight. -- Analyse the rendering to find the columns to highlight.
tlines = dbg5 "tlines" $ max 1 $ length $ T.lines $ tcomment t -- transaction comment can generate extra lines tlines = dbg5 "tlines" $ max 1 $ length $ T.lines $ tcomment t -- transaction comment can generate extra lines
(col, col2) = (col, col2) =
let def = (5, maximum (map T.length $ T.lines trendered)) -- fallback: underline whole posting. Shouldn't happen. let def = (5, maximum (map T.length $ T.lines trendered)) -- fallback: underline whole posting. Shouldn't happen.
in in
case transactionFindPostingIndex (==p) t of case transactionFindPostingIndex (==p') t of
Nothing -> def Nothing -> def
Just idx -> fromMaybe def $ do Just idx -> fromMaybe def $ do
let let
@ -136,9 +136,9 @@ makeBalanceAssertionErrorExcerpt p = makePostingErrorExcerpt p finderrcols
beforepslines = dbg5 "beforepslines" $ sum $ map (max 1 . length . T.lines . pcomment) beforeps -- posting comment can generate extra lines (assume only one commodity shown) beforepslines = dbg5 "beforepslines" $ sum $ map (max 1 . length . T.lines . pcomment) beforeps -- posting comment can generate extra lines (assume only one commodity shown)
assertionline <- dbg5 "assertionline" $ headMay $ drop (tlines + beforepslines) $ T.lines trendered assertionline <- dbg5 "assertionline" $ headMay $ drop (tlines + beforepslines) $ T.lines trendered
let let
col2 = T.length assertionline col2' = T.length assertionline
l = dropWhile (/= '=') $ reverse $ T.unpack assertionline l = dropWhile (/= '=') $ reverse $ T.unpack assertionline
l' = dropWhile (`elem` ['=','*']) l l' = dropWhile (`elem` ['=','*']) l
col = length l' + 1 col' = length l' + 1
return (col, col2) return (col', col2')

View File

@ -252,14 +252,14 @@ dbgJournalAcctDeclOrder prefix
where where
showAcctDeclsSummary :: [(AccountName,AccountDeclarationInfo)] -> String showAcctDeclsSummary :: [(AccountName,AccountDeclarationInfo)] -> String
showAcctDeclsSummary adis showAcctDeclsSummary adis
| length adis < (2*num+2) = "[" <> showadis adis <> "]" | length adis < (2*n+2) = "[" <> showadis adis <> "]"
| otherwise = | otherwise =
"[" <> showadis (take num adis) <> " ... " <> showadis (takelast num adis) <> "]" "[" <> showadis (take n adis) <> " ... " <> showadis (takelast n adis) <> "]"
where where
num = 3 n = 3
showadis = intercalate ", " . map showadi showadis = intercalate ", " . map showadi
showadi (a,adi) = "("<>show (adideclarationorder adi)<>","<>T.unpack a<>")" showadi (a,adi) = "("<>show (adideclarationorder adi)<>","<>T.unpack a<>")"
takelast n = reverse . take n . reverse takelast n' = reverse . take n' . reverse
instance Default Journal where instance Default Journal where
def = nulljournal def = nulljournal
@ -405,7 +405,7 @@ journalAccountTags Journal{jdeclaredaccounttags} a = M.findWithDefault [] a jdec
-- | Which tags are in effect for this account, including tags inherited from parent accounts ? -- | Which tags are in effect for this account, including tags inherited from parent accounts ?
journalInheritedAccountTags :: Journal -> AccountName -> [Tag] journalInheritedAccountTags :: Journal -> AccountName -> [Tag]
journalInheritedAccountTags j a = journalInheritedAccountTags j a =
foldl' (\ts a -> ts `union` journalAccountTags j a) [] as foldl' (\ts a' -> ts `union` journalAccountTags j a') [] as
where where
as = a : parentAccountNames a as = a : parentAccountNames a
-- PERF: cache in journal ? -- PERF: cache in journal ?

View File

@ -116,15 +116,15 @@ journalCheckCommodities j = mapM_ checkcommodities (journalPostings j)
-- assets "C $" -1 @ $ 2 -- assets "C $" -1 @ $ 2
-- ^^^^^^^^^^^^^^ -- ^^^^^^^^^^^^^^
-- XXX refine this region when it's easy -- XXX refine this region when it's easy
finderrcols p t txntxt = finderrcols p' t txntxt =
case transactionFindPostingIndex (==p) t of case transactionFindPostingIndex (==p') t of
Nothing -> Nothing Nothing -> Nothing
Just pindex -> Just (amtstart, Just amtend) Just pindex -> Just (amtstart, Just amtend)
where where
tcommentlines = max 0 (length (T.lines $ tcomment t) - 1) tcommentlines = max 0 (length (T.lines $ tcomment t) - 1)
errrelline = 1 + tcommentlines + pindex -- XXX doesn't count posting coment lines errrelline = 1 + tcommentlines + pindex -- XXX doesn't count posting coment lines
errline = fromMaybe "" (T.lines txntxt `atMay` (errrelline-1)) errline = fromMaybe "" (T.lines txntxt `atMay` (errrelline-1))
acctend = 4 + T.length (paccount p) + if isVirtual p then 2 else 0 acctend = 4 + T.length (paccount p') + if isVirtual p' then 2 else 0
amtstart = acctend + (T.length $ T.takeWhile isSpace $ T.drop acctend errline) + 1 amtstart = acctend + (T.length $ T.takeWhile isSpace $ T.drop acctend errline) + 1
amtend = amtstart + (T.length $ T.stripEnd $ T.takeWhile (/=';') $ T.drop amtstart errline) amtend = amtstart + (T.length $ T.stripEnd $ T.takeWhile (/=';') $ T.drop amtstart errline)
@ -151,10 +151,10 @@ journalCheckPayees j = mapM_ checkpayee (jtxns j)
-- Calculate columns suitable for highlighting the excerpt. -- Calculate columns suitable for highlighting the excerpt.
-- We won't show these in the main error line as they aren't -- We won't show these in the main error line as they aren't
-- accurate for the actual data. -- accurate for the actual data.
finderrcols t = Just (col, Just col2) finderrcols t' = Just (col, Just col2)
where where
col = T.length (showTransactionLineFirstPart t) + 2 col = T.length (showTransactionLineFirstPart t') + 2
col2 = col + T.length (transactionPayee t) - 1 col2 = col + T.length (transactionPayee t') - 1
---------- ----------

View File

@ -15,20 +15,19 @@ import Hledger.Utils (textChomp)
journalCheckOrdereddates :: WhichDate -> Journal -> Either String () journalCheckOrdereddates :: WhichDate -> Journal -> Either String ()
journalCheckOrdereddates whichdate j = do journalCheckOrdereddates whichdate j = do
let let
-- we check date ordering within each file, not across files -- we check date ordering within each file, not across files
-- note, relying on txns always being sorted by file here -- note, relying on txns always being sorted by file here
txnsbyfile = groupBy (\t1 t2 -> transactionFile t1 == transactionFile t2) $ jtxns j txnsbyfile = groupBy (\t1 t2 -> transactionFile t1 == transactionFile t2) $ jtxns j
getdate = transactionDateOrDate2 whichdate getdate = transactionDateOrDate2 whichdate
compare a b = getdate a <= getdate b compare' a b = getdate a <= getdate b
either Left (const $ Right ()) $ (const $ Right ()) =<< (forM txnsbyfile $ \ts ->
forM txnsbyfile $ \ts -> case checkTransactions compare' ts of
case checkTransactions compare ts of
FoldAcc{fa_previous=Nothing} -> Right () FoldAcc{fa_previous=Nothing} -> Right ()
FoldAcc{fa_error=Nothing} -> Right () FoldAcc{fa_error=Nothing} -> Right ()
FoldAcc{fa_error=Just t, fa_previous=Just tprev} -> Left $ printf FoldAcc{fa_error=Just t, fa_previous=Just tprev} -> Left $ printf
("%s:%d:\n%s\nOrdered dates checking is enabled, and this transaction's\n" ("%s:%d:\n%s\nOrdered dates checking is enabled, and this transaction's\n"
++ "date%s (%s) is out of order with the previous transaction.\n" ++ "date%s (%s) is out of order with the previous transaction.\n"
++ "Consider moving this entry into date order, or adjusting its date.") ++ "Consider moving this entry into date order, or adjusting its date.")
f l ex datenum (show $ getdate t) f l ex datenum (show $ getdate t)
where where
@ -37,7 +36,7 @@ journalCheckOrdereddates whichdate j = do
-- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them -- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them
ex = T.unlines [textChomp ex1, T.pack " ", textChomp ex2] ex = T.unlines [textChomp ex1, T.pack " ", textChomp ex2]
finderrcols _t = Just (1, Just 10) finderrcols _t = Just (1, Just 10)
datenum = if whichdate==SecondaryDate then "2" else "" datenum = if whichdate==SecondaryDate then "2" else "")
data FoldAcc a b = FoldAcc data FoldAcc a b = FoldAcc
{ fa_error :: Maybe a { fa_error :: Maybe a
@ -46,11 +45,11 @@ data FoldAcc a b = FoldAcc
checkTransactions :: (Transaction -> Transaction -> Bool) checkTransactions :: (Transaction -> Transaction -> Bool)
-> [Transaction] -> FoldAcc Transaction Transaction -> [Transaction] -> FoldAcc Transaction Transaction
checkTransactions compare = foldWhile f FoldAcc{fa_error=Nothing, fa_previous=Nothing} checkTransactions compare' = foldWhile f FoldAcc{fa_error=Nothing, fa_previous=Nothing}
where where
f current acc@FoldAcc{fa_previous=Nothing} = acc{fa_previous=Just current} f current acc@FoldAcc{fa_previous=Nothing} = acc{fa_previous=Just current}
f current acc@FoldAcc{fa_previous=Just previous} = f current acc@FoldAcc{fa_previous=Just previous} =
if compare previous current if compare' previous current
then acc{fa_previous=Just current} then acc{fa_previous=Just current}
else acc{fa_error=Just current} else acc{fa_error=Just current}
@ -58,5 +57,5 @@ foldWhile :: (a -> FoldAcc a b -> FoldAcc a b) -> FoldAcc a b -> [a] -> FoldAcc
foldWhile _ acc [] = acc foldWhile _ acc [] = acc
foldWhile fold acc (a:as) = foldWhile fold acc (a:as) =
case fold a acc of case fold a acc of
acc@FoldAcc{fa_error=Just _} -> acc acc'@FoldAcc{fa_error=Just _} -> acc'
acc -> foldWhile fold acc as acc' -> foldWhile fold acc' as

View File

@ -1,4 +1,3 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Hledger.Data.JournalChecks.Uniqueleafnames ( module Hledger.Data.JournalChecks.Uniqueleafnames (
@ -43,12 +42,12 @@ journalCheckUniqueleafnames j = do
(f,l,_,ex2) = makePostingErrorExcerpt p2 finderrcols (f,l,_,ex2) = makePostingErrorExcerpt p2 finderrcols
-- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them -- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them
ex = T.unlines [textChomp ex1, T.pack " ...", textChomp ex2] ex = T.unlines [textChomp ex1, T.pack " ...", textChomp ex2]
finderrcols p _ _ = Just (col, Just col2) finderrcols p' _ _ = Just (col, Just col2)
where where
a = paccount p a = paccount p'
alen = T.length a alen = T.length a
llen = T.length $ accountLeafName a llen = T.length $ accountLeafName a
col = 5 + (if isVirtual p then 1 else 0) + alen - llen col = 5 + (if isVirtual p' then 1 else 0) + alen - llen
col2 = col + llen - 1 col2 = col + llen - 1
accts = T.unlines fulls accts = T.unlines fulls

View File

@ -59,7 +59,7 @@ periodAsDateSpan (QuarterPeriod y q) = DateSpan (Just $ fromGregorian y m 1) (Ju
where where
(y', q') | q==4 = (y+1,1) (y', q') | q==4 = (y+1,1)
| otherwise = (y,q+1) | otherwise = (y,q+1)
quarterAsMonth q = (q-1) * 3 + 1 quarterAsMonth q2 = (q2-1) * 3 + 1
m = quarterAsMonth q m = quarterAsMonth q
m' = quarterAsMonth q' m' = quarterAsMonth q'
periodAsDateSpan (YearPeriod y) = DateSpan (Just $ fromGregorian y 1 1) (Just $ fromGregorian (y+1) 1 1) periodAsDateSpan (YearPeriod y) = DateSpan (Just $ fromGregorian y 1 1) (Just $ fromGregorian (y+1) 1 1)

View File

@ -41,7 +41,7 @@ _ptgen str = do
nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }
nulldatespan nulldatespan
_ptgenspan str span = do _ptgenspan str spn = do
let let
t = T.pack str t = T.pack str
(i,s) = parsePeriodExpr' nulldate t (i,s) = parsePeriodExpr' nulldate t
@ -51,7 +51,7 @@ _ptgenspan str span = do
mapM_ (T.putStr . showTransaction) $ mapM_ (T.putStr . showTransaction) $
runPeriodicTransaction runPeriodicTransaction
nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }
span spn
--deriving instance Show PeriodicTransaction --deriving instance Show PeriodicTransaction
-- for better pretty-printing: -- for better pretty-printing:

View File

@ -86,7 +86,7 @@ import Data.Time.Calendar (Day)
import Safe (maximumBound) import Safe (maximumBound)
import Text.DocLayout (realLength) import Text.DocLayout (realLength)
import Text.Tabular.AsciiWide import Text.Tabular.AsciiWide hiding (render)
import Hledger.Utils import Hledger.Utils
import Hledger.Data.Types import Hledger.Data.Types
@ -396,7 +396,7 @@ postingApplyAliases aliases p@Posting{paccount} =
Right a -> Right p{paccount=a} Right a -> Right p{paccount=a}
Left e -> Left err Left e -> Left err
where where
err = "problem while applying account aliases:\n" ++ pshow aliases err = "problem while applying account aliases:\n" ++ pshow aliases
++ "\n to account name: "++T.unpack paccount++"\n "++e ++ "\n to account name: "++T.unpack paccount++"\n "++e
-- | Choose and apply a consistent display style to the posting -- | Choose and apply a consistent display style to the posting
@ -427,7 +427,7 @@ postingToCost styles ToCost p
| "_conversion-matched" `elem` map fst (ptags p) && noCost = Nothing | "_conversion-matched" `elem` map fst (ptags p) && noCost = Nothing
| otherwise = Just $ postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p | otherwise = Just $ postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p
where where
noCost = null . filter (isJust . aprice) . amountsRaw $ pamount p noCost = (not . any (isJust . aprice) . amountsRaw) $ pamount p
-- | Generate inferred equity postings from a 'Posting' using transaction prices. -- | Generate inferred equity postings from a 'Posting' using transaction prices.
-- Make sure not to generate equity postings when there are already matched -- Make sure not to generate equity postings when there are already matched
@ -497,7 +497,7 @@ commentAddTag c (t,v)
-- A space is inserted following the colon, before the value. -- A space is inserted following the colon, before the value.
commentAddTagNextLine :: Text -> Tag -> Text commentAddTagNextLine :: Text -> Tag -> Text
commentAddTagNextLine cmt (t,v) = commentAddTagNextLine cmt (t,v) =
cmt <> (if "\n" `T.isSuffixOf` cmt then "" else "\n") <> t <> ": " <> v cmt <> (if "\n" `T.isSuffixOf` cmt then "" else "\n") <> t <> ": " <> v
-- tests -- tests

View File

@ -4,7 +4,6 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
module Hledger.Data.StringFormat ( module Hledger.Data.StringFormat (
@ -154,8 +153,8 @@ fieldp = do
formatStringTester fs value expected = actual @?= expected formatStringTester fs value expected = actual @?= expected
where where
actual = case fs of actual = case fs of
FormatLiteral l -> formatText False Nothing Nothing l FormatLiteral l -> formatText False Nothing Nothing l
FormatField leftJustify min max _ -> formatText leftJustify min max value FormatField leftJustify mn mx _ -> formatText leftJustify mn mx value
tests_StringFormat = testGroup "StringFormat" [ tests_StringFormat = testGroup "StringFormat" [

View File

@ -139,8 +139,8 @@ entryFromTimeclockInOut i o
-- since otherwise it will often have large recurring decimal parts which (since 1.21) -- since otherwise it will often have large recurring decimal parts which (since 1.21)
-- print would display all 255 digits of. timeclock amounts have one second resolution, -- print would display all 255 digits of. timeclock amounts have one second resolution,
-- so two decimal places is precise enough (#1527). -- so two decimal places is precise enough (#1527).
amount = mixedAmount $ setAmountInternalPrecision 2 $ hrs hours amt = mixedAmount $ setAmountInternalPrecision 2 $ hrs hours
ps = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}] ps = [posting{paccount=acctname, pamount=amt, ptype=VirtualPosting, ptransaction=Just t}]
-- tests -- tests

View File

@ -10,7 +10,6 @@ tags.
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Hledger.Data.Transaction module Hledger.Data.Transaction
( -- * Transaction ( -- * Transaction
@ -250,8 +249,8 @@ transactionAddPricesFromEquity acctTypes t = first (annotateErrorWithTransaction
| isConversion p = Right ((cs, others), Just np) | isConversion p = Right ((cs, others), Just np)
| hasPrice p = Right ((cs, (np:ps, os)), Nothing) | hasPrice p = Right ((cs, (np:ps, os)), Nothing)
| otherwise = Right ((cs, (ps, np:os)), Nothing) | otherwise = Right ((cs, (ps, np:os)), Nothing)
select np@(_, p) ((cs, others), Just last) select np@(_, p) ((cs, others), Just lst)
| isConversion p = Right (((last, np):cs, others), Nothing) | isConversion p = Right (((lst, np):cs, others), Nothing)
| otherwise = Left "Conversion postings must occur in adjacent pairs" | otherwise = Left "Conversion postings must occur in adjacent pairs"
-- Given a pair of indexed conversion postings, and a state consisting of lists of -- Given a pair of indexed conversion postings, and a state consisting of lists of
@ -267,10 +266,10 @@ transactionAddPricesFromEquity acctTypes t = first (annotateErrorWithTransaction
ca1 <- postingAmountNoPrice cp1 ca1 <- postingAmountNoPrice cp1
ca2 <- postingAmountNoPrice cp2 ca2 <- postingAmountNoPrice cp2
let -- The function to add transaction prices and tag postings in the indexed list of postings let -- The function to add transaction prices and tag postings in the indexed list of postings
transformPostingF np pricep = \(n, p) -> transformPostingF np pricep (n,p) =
(n, if | n == np -> pricep `postingAddTags` [("_price-matched","")] (n, if | n == np -> pricep `postingAddTags` [("_price-matched","")]
| n == n1 || n == n2 -> p `postingAddTags` [("_conversion-matched","")] | n == n1 || n == n2 -> p `postingAddTags` [("_conversion-matched","")]
| otherwise -> p) | otherwise -> p)
-- All priced postings which match the conversion posting pair -- All priced postings which match the conversion posting pair
matchingPricePs = mapMaybe (mapM $ pricedPostingIfMatchesBothAmounts ca1 ca2) priceps matchingPricePs = mapMaybe (mapM $ pricedPostingIfMatchesBothAmounts ca1 ca2) priceps
-- All other postings which match at least one of the conversion posting pair -- All other postings which match at least one of the conversion posting pair

View File

@ -67,7 +67,7 @@ module Hledger.Query (
matchesTags, matchesTags,
matchesPriceDirective, matchesPriceDirective,
words'', words'',
prefixes, queryprefixes,
-- * tests -- * tests
tests_Query tests_Query
) )
@ -167,7 +167,7 @@ data QueryOpt = QueryOptInAcctOnly AccountName -- ^ show an account register fo
-- >>> parseQuery nulldate "\"expenses:dining out\"" -- >>> parseQuery nulldate "\"expenses:dining out\""
-- Right (Acct (RegexpCI "expenses:dining out"),[]) -- Right (Acct (RegexpCI "expenses:dining out"),[])
parseQuery :: Day -> T.Text -> Either String (Query,[QueryOpt]) parseQuery :: Day -> T.Text -> Either String (Query,[QueryOpt])
parseQuery d = parseQueryList d . words'' prefixes parseQuery d = parseQueryList d . words'' queryprefixes
-- | Convert a list of query expression containing to a query and zero -- | Convert a list of query expression containing to a query and zero
-- or more query options; or return an error message if query parsing fails. -- or more query options; or return an error message if query parsing fails.
@ -234,8 +234,8 @@ words'' prefixes = fromparse . parsewith maybeprefixedquotedphrases -- XXX
-- XXX -- XXX
-- keep synced with patterns below, excluding "not" -- keep synced with patterns below, excluding "not"
prefixes :: [T.Text] queryprefixes :: [T.Text]
prefixes = map (<>":") [ queryprefixes = map (<>":") [
"inacctonly" "inacctonly"
,"inacct" ,"inacct"
,"amt" ,"amt"
@ -285,10 +285,10 @@ parseQueryTerm _ (T.stripPrefix "note:" -> Just s) = Left <$> noteTag (Just s)
parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left . Acct <$> toRegexCI s parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left . Acct <$> toRegexCI s
parseQueryTerm d (T.stripPrefix "date2:" -> Just s) = parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =
case parsePeriodExpr d s of Left e -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e case parsePeriodExpr d s of Left e -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e
Right (_,span) -> Right $ Left $ Date2 span Right (_,spn) -> Right $ Left $ Date2 spn
parseQueryTerm d (T.stripPrefix "date:" -> Just s) = parseQueryTerm d (T.stripPrefix "date:" -> Just s) =
case parsePeriodExpr d s of Left e -> Left $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e case parsePeriodExpr d s of Left e -> Left $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e
Right (_,span) -> Right $ Left $ Date span Right (_,spn) -> Right $ Left $ Date spn
parseQueryTerm _ (T.stripPrefix "status:" -> Just s) = parseQueryTerm _ (T.stripPrefix "status:" -> Just s) =
case parseStatus s of Left e -> Left $ "\"status:"++T.unpack s++"\" gave a parse error: " ++ e case parseStatus s of Left e -> Left $ "\"status:"++T.unpack s++"\" gave a parse error: " ++ e
Right st -> Right $ Left $ StatusQ st Right st -> Right $ Left $ StatusQ st
@ -412,9 +412,9 @@ truestrings = ["1"]
-- * modifying -- * modifying
simplifyQuery :: Query -> Query simplifyQuery :: Query -> Query
simplifyQuery q = simplifyQuery q0 =
let q' = simplify q let q1 = simplify q0
in if q' == q then q else simplifyQuery q' in if q1 == q0 then q0 else simplifyQuery q1
where where
simplify (And []) = Any simplify (And []) = Any
simplify (And [q]) = simplify q simplify (And [q]) = simplify q
@ -455,7 +455,7 @@ filterQuery' p q = if p q then q else Any
-- (Since 1.24.1, might be merged into filterQuery in future.) -- (Since 1.24.1, might be merged into filterQuery in future.)
-- XXX Semantics not completely clear. -- XXX Semantics not completely clear.
filterQueryOrNotQuery :: (Query -> Bool) -> Query -> Query filterQueryOrNotQuery :: (Query -> Bool) -> Query -> Query
filterQueryOrNotQuery p = simplifyQuery . filterQueryOrNotQuery' p filterQueryOrNotQuery p0 = simplifyQuery . filterQueryOrNotQuery' p0
where where
filterQueryOrNotQuery' :: (Query -> Bool) -> Query -> Query filterQueryOrNotQuery' :: (Query -> Bool) -> Query -> Query
filterQueryOrNotQuery' p (And qs) = And $ map (filterQueryOrNotQuery p) qs filterQueryOrNotQuery' p (And qs) = And $ map (filterQueryOrNotQuery p) qs
@ -584,7 +584,7 @@ queryEndDate False (Date (DateSpan _ (Just d))) = Just d
queryEndDate True (Date2 (DateSpan _ (Just d))) = Just d queryEndDate True (Date2 (DateSpan _ (Just d))) = Just d
queryEndDate _ _ = Nothing queryEndDate _ _ = Nothing
queryTermDateSpan (Date span) = Just span queryTermDateSpan (Date spn) = Just spn
queryTermDateSpan _ = Nothing queryTermDateSpan _ = Nothing
-- | What date span (or with a true argument, what secondary date span) does this query specify ? -- | What date span (or with a true argument, what secondary date span) does this query specify ?
@ -594,8 +594,8 @@ queryTermDateSpan _ = Nothing
queryDateSpan :: Bool -> Query -> DateSpan queryDateSpan :: Bool -> Query -> DateSpan
queryDateSpan secondary (Or qs) = spansUnion $ map (queryDateSpan secondary) qs queryDateSpan secondary (Or qs) = spansUnion $ map (queryDateSpan secondary) qs
queryDateSpan secondary (And qs) = spansIntersect $ map (queryDateSpan secondary) qs queryDateSpan secondary (And qs) = spansIntersect $ map (queryDateSpan secondary) qs
queryDateSpan _ (Date span) = span queryDateSpan _ (Date spn) = spn
queryDateSpan True (Date2 span) = span queryDateSpan True (Date2 spn) = spn
queryDateSpan _ _ = nulldatespan queryDateSpan _ _ = nulldatespan
-- | What date span does this query specify, treating primary and secondary dates as equivalent ? -- | What date span does this query specify, treating primary and secondary dates as equivalent ?
@ -605,8 +605,8 @@ queryDateSpan _ _ = nulldatespan
queryDateSpan' :: Query -> DateSpan queryDateSpan' :: Query -> DateSpan
queryDateSpan' (Or qs) = spansUnion $ map queryDateSpan' qs queryDateSpan' (Or qs) = spansUnion $ map queryDateSpan' qs
queryDateSpan' (And qs) = spansIntersect $ map queryDateSpan' qs queryDateSpan' (And qs) = spansIntersect $ map queryDateSpan' qs
queryDateSpan' (Date span) = span queryDateSpan' (Date spn) = spn
queryDateSpan' (Date2 span) = span queryDateSpan' (Date2 spn) = spn
queryDateSpan' _ = nulldatespan queryDateSpan' _ = nulldatespan
-- | What is the earliest of these dates, where Nothing is earliest ? -- | What is the earliest of these dates, where Nothing is earliest ?
@ -732,16 +732,16 @@ matchesPosting (And qs) p = all (`matchesPosting` p) qs
matchesPosting (Code r) p = maybe False (regexMatchText r . tcode) $ ptransaction p matchesPosting (Code r) p = maybe False (regexMatchText r . tcode) $ ptransaction p
matchesPosting (Desc r) p = maybe False (regexMatchText r . tdescription) $ ptransaction p matchesPosting (Desc r) p = maybe False (regexMatchText r . tdescription) $ ptransaction p
matchesPosting (Acct r) p = matches p || maybe False matches (poriginal p) where matches = regexMatchText r . paccount matchesPosting (Acct r) p = matches p || maybe False matches (poriginal p) where matches = regexMatchText r . paccount
matchesPosting (Date span) p = span `spanContainsDate` postingDate p matchesPosting (Date spn) p = spn `spanContainsDate` postingDate p
matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p matchesPosting (Date2 spn) p = spn `spanContainsDate` postingDate2 p
matchesPosting (StatusQ s) p = postingStatus p == s matchesPosting (StatusQ s) p = postingStatus p == s
matchesPosting (Real v) p = v == isReal p matchesPosting (Real v) p = v == isReal p
matchesPosting q@(Depth _) Posting{paccount=a} = q `matchesAccount` a matchesPosting q@(Depth _) Posting{paccount=a} = q `matchesAccount` a
matchesPosting q@(Amt _ _) Posting{pamount=as} = q `matchesMixedAmount` as matchesPosting q@(Amt _ _) Posting{pamount=as} = q `matchesMixedAmount` as
matchesPosting (Sym r) Posting{pamount=as} = any (matchesCommodity (Sym r) . acommodity) $ amountsRaw as matchesPosting (Sym r) Posting{pamount=as} = any (matchesCommodity (Sym r) . acommodity) $ amountsRaw as
matchesPosting (Tag n v) p = case (reString n, v) of matchesPosting (Tag n v) p = case (reString n, v) of
("payee", Just v) -> maybe False (regexMatchText v . transactionPayee) $ ptransaction p ("payee", Just v') -> maybe False (regexMatchText v' . transactionPayee) $ ptransaction p
("note", Just v) -> maybe False (regexMatchText v . transactionNote) $ ptransaction p ("note", Just v') -> maybe False (regexMatchText v' . transactionNote) $ ptransaction p
(_, mv) -> matchesTags n mv $ postingAllTags p (_, mv) -> matchesTags n mv $ postingAllTags p
matchesPosting (Type _) _ = False matchesPosting (Type _) _ = False
@ -765,17 +765,17 @@ matchesTransaction (And qs) t = all (`matchesTransaction` t) qs
matchesTransaction (Code r) t = regexMatchText r $ tcode t matchesTransaction (Code r) t = regexMatchText r $ tcode t
matchesTransaction (Desc r) t = regexMatchText r $ tdescription t matchesTransaction (Desc r) t = regexMatchText r $ tdescription t
matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t
matchesTransaction (Date span) t = spanContainsDate span $ tdate t matchesTransaction (Date spn) t = spanContainsDate spn $ tdate t
matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t matchesTransaction (Date2 spn) t = spanContainsDate spn $ transactionDate2 t
matchesTransaction (StatusQ s) t = tstatus t == s matchesTransaction (StatusQ s) t = tstatus t == s
matchesTransaction (Real v) t = v == hasRealPostings t matchesTransaction (Real v) t = v == hasRealPostings t
matchesTransaction q@(Amt _ _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction q@(Amt _ _) t = any (q `matchesPosting`) $ tpostings t
matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t
matchesTransaction q@(Sym _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction q@(Sym _) t = any (q `matchesPosting`) $ tpostings t
matchesTransaction (Tag n v) t = case (reString n, v) of matchesTransaction (Tag n v) t = case (reString n, v) of
("payee", Just v) -> regexMatchText v $ transactionPayee t ("payee", Just v') -> regexMatchText v' $ transactionPayee t
("note", Just v) -> regexMatchText v $ transactionNote t ("note", Just v') -> regexMatchText v' $ transactionNote t
(_, v) -> matchesTags n v $ transactionAllTags t (_, v') -> matchesTags n v' $ transactionAllTags t
matchesTransaction (Type _) _ = False matchesTransaction (Type _) _ = False
-- | Like matchesTransaction, but if the journal's account types are provided, -- | Like matchesTransaction, but if the journal's account types are provided,
@ -821,7 +821,7 @@ matchesPriceDirective (Or qs) p = any (`matchesPriceDirective` p) qs
matchesPriceDirective (And qs) p = all (`matchesPriceDirective` p) qs matchesPriceDirective (And qs) p = all (`matchesPriceDirective` p) qs
matchesPriceDirective q@(Amt _ _) p = matchesAmount q (pdamount p) matchesPriceDirective q@(Amt _ _) p = matchesAmount q (pdamount p)
matchesPriceDirective q@(Sym _) p = matchesCommodity q (pdcommodity p) matchesPriceDirective q@(Sym _) p = matchesCommodity q (pdcommodity p)
matchesPriceDirective (Date span) p = spanContainsDate span (pddate p) matchesPriceDirective (Date spn) p = spanContainsDate spn (pddate p)
matchesPriceDirective _ _ = True matchesPriceDirective _ _ = True
@ -854,8 +854,8 @@ tests_Query = testGroup "Query" [
(words'' [] "not:'a b'") @?= ["not:a b"] (words'' [] "not:'a b'") @?= ["not:a b"]
(words'' [] "'not:a b'") @?= ["not:a b"] (words'' [] "'not:a b'") @?= ["not:a b"]
(words'' ["desc:"] "not:desc:'a b'") @?= ["not:desc:a b"] (words'' ["desc:"] "not:desc:'a b'") @?= ["not:desc:a b"]
(words'' prefixes "\"acct:expenses:autres d\233penses\"") @?= ["acct:expenses:autres d\233penses"] (words'' queryprefixes "\"acct:expenses:autres d\233penses\"") @?= ["acct:expenses:autres d\233penses"]
(words'' prefixes "\"") @?= ["\""] (words'' queryprefixes "\"") @?= ["\""]
,testCase "filterQuery" $ do ,testCase "filterQuery" $ do
filterQuery queryIsDepth Any @?= Any filterQuery queryIsDepth Any @?= Any

View File

@ -108,15 +108,15 @@ defaultJournal = defaultJournalPath >>= runExceptT . readJournalFile definputopt
defaultJournalPath :: IO String defaultJournalPath :: IO String
defaultJournalPath = do defaultJournalPath = do
s <- envJournalPath s <- envJournalPath
if null s then defaultJournalPath else return s if null s then defpath else return s
where where
envJournalPath = envJournalPath =
getEnv journalEnvVar getEnv journalEnvVar
`C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2 `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2
`C.catch` (\(_::C.IOException) -> return "")) `C.catch` (\(_::C.IOException) -> return ""))
defaultJournalPath = do defpath = do
home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "") home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")
return $ home </> journalDefaultFilename return $ home </> journalDefaultFilename
-- | A file path optionally prefixed by a reader name and colon -- | A file path optionally prefixed by a reader name and colon
-- (journal:, csv:, timedot:, etc.). -- (journal:, csv:, timedot:, etc.).

View File

@ -195,7 +195,7 @@ rawOptsToInputOpts day rawopts =
argsquery = lefts . rights . map (parseQueryTerm day) $ querystring_ ropts argsquery = lefts . rights . map (parseQueryTerm day) $ querystring_ ropts
datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery
commodity_styles = either err id $ commodityStyleFromRawOpts rawopts styles = either err id $ commodityStyleFromRawOpts rawopts
where err e = error' $ "could not parse commodity-style: '" ++ e ++ "'" -- PARTIAL: where err e = error' $ "could not parse commodity-style: '" ++ e ++ "'" -- PARTIAL:
in definputopts{ in definputopts{
@ -215,7 +215,7 @@ rawOptsToInputOpts day rawopts =
,balancingopts_ = defbalancingopts{ ,balancingopts_ = defbalancingopts{
ignore_assertions_ = boolopt "ignore-assertions" rawopts ignore_assertions_ = boolopt "ignore-assertions" rawopts
, infer_transaction_prices_ = not noinferprice , infer_transaction_prices_ = not noinferprice
, commodity_styles_ = Just commodity_styles , commodity_styles_ = Just styles
} }
,strict_ = boolopt "strict" rawopts ,strict_ = boolopt "strict" rawopts
,_ioDay = day ,_ioDay = day
@ -446,8 +446,8 @@ journalAddFile f j@Journal{jfiles=fs} = j{jfiles=fs++[f]}
-- A version of `match` that is strict in the returned text -- A version of `match` that is strict in the returned text
match' :: TextParser m a -> TextParser m (Text, a) match' :: TextParser m a -> TextParser m (Text, a)
match' p = do match' p = do
(!txt, p) <- match p (!txt, p') <- match p
pure (txt, p) pure (txt, p')
--- ** parsers --- ** parsers
--- *** transaction bits --- *** transaction bits
@ -514,9 +514,9 @@ datep' mYear = do
Just date -> pure $! date Just date -> pure $! date
partialDate :: Int -> Maybe Year -> Month -> Char -> MonthDay -> TextParser m Day partialDate :: Int -> Maybe Year -> Month -> Char -> MonthDay -> TextParser m Day
partialDate startOffset mYear month sep day = do partialDate startOffset myr month sep day = do
endOffset <- getOffset endOffset <- getOffset
case mYear of case myr of
Just year -> Just year ->
case fromGregorianValid year month day of case fromGregorianValid year month day of
Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $ Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $
@ -611,12 +611,12 @@ yearorintp = do
modifiedaccountnamep :: JournalParser m AccountName modifiedaccountnamep :: JournalParser m AccountName
modifiedaccountnamep = do modifiedaccountnamep = do
parent <- getParentAccount parent <- getParentAccount
aliases <- getAccountAliases als <- getAccountAliases
-- off1 <- getOffset -- off1 <- getOffset
a <- lift accountnamep a <- lift accountnamep
-- off2 <- getOffset -- off2 <- getOffset
-- XXX or accountNameApplyAliasesMemo ? doesn't seem to make a difference (retest that function) -- XXX or accountNameApplyAliasesMemo ? doesn't seem to make a difference (retest that function)
case accountNameApplyAliases aliases $ joinAccountNames parent a of case accountNameApplyAliases als $ joinAccountNames parent a of
Right a' -> return $! a' Right a' -> return $! a'
-- should not happen, regexaliasp will have displayed a better error already: -- should not happen, regexaliasp will have displayed a better error already:
-- (XXX why does customFailure cause error to be displayed there, but not here ?) -- (XXX why does customFailure cause error to be displayed there, but not here ?)
@ -660,12 +660,12 @@ singlespacednoncommenttext1p = singlespacedtextsatisfying1p (not . isSameLineCom
-- | Parse non-empty, single-spaced text starting and ending with non-whitespace, -- | Parse non-empty, single-spaced text starting and ending with non-whitespace,
-- where all characters satisfy the given predicate. -- where all characters satisfy the given predicate.
singlespacedtextsatisfying1p :: (Char -> Bool) -> TextParser m T.Text singlespacedtextsatisfying1p :: (Char -> Bool) -> TextParser m T.Text
singlespacedtextsatisfying1p pred = do singlespacedtextsatisfying1p f = do
firstPart <- partp firstPart <- partp
otherParts <- many $ try $ singlespacep *> partp otherParts <- many $ try $ singlespacep *> partp
pure $! T.unwords $ firstPart : otherParts pure $! T.unwords $ firstPart : otherParts
where where
partp = takeWhile1P Nothing (\c -> pred c && not (isSpace c)) partp = takeWhile1P Nothing (\c -> f c && not (isSpace c))
-- | Parse one non-newline whitespace character that is not followed by another one. -- | Parse one non-newline whitespace character that is not followed by another one.
singlespacep :: TextParser m () singlespacep :: TextParser m ()
@ -708,20 +708,20 @@ amountp = amountpwithmultiplier False
amountpwithmultiplier :: Bool -> JournalParser m Amount amountpwithmultiplier :: Bool -> JournalParser m Amount
amountpwithmultiplier mult = label "amount" $ do amountpwithmultiplier mult = label "amount" $ do
let spaces = lift $ skipNonNewlineSpaces let spaces = lift $ skipNonNewlineSpaces
amount <- amountwithoutpricep mult <* spaces amt <- amountwithoutpricep mult <* spaces
(mprice, _elotprice, _elotdate) <- runPermutation $ (mprice, _elotprice, _elotdate) <- runPermutation $
(,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amount <* spaces) (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amt <* spaces)
<*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces) <*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces)
<*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces) <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces)
pure $ amount { aprice = mprice } pure $ amt { aprice = mprice }
amountpnolotpricesp :: JournalParser m Amount amountpnolotpricesp :: JournalParser m Amount
amountpnolotpricesp = label "amount" $ do amountpnolotpricesp = label "amount" $ do
let spaces = lift $ skipNonNewlineSpaces let spaces = lift $ skipNonNewlineSpaces
amount <- amountwithoutpricep False amt <- amountwithoutpricep False
spaces spaces
mprice <- optional $ priceamountp amount <* spaces mprice <- optional $ priceamountp amt <* spaces
pure $ amount { aprice = mprice } pure $ amt { aprice = mprice }
amountwithoutpricep :: Bool -> JournalParser m Amount amountwithoutpricep :: Bool -> JournalParser m Amount
amountwithoutpricep mult = do amountwithoutpricep mult = do
@ -1094,8 +1094,8 @@ data DigitGrp = DigitGrp {
-- | A custom show instance, showing digit groups as the parser saw them. -- | A custom show instance, showing digit groups as the parser saw them.
instance Show DigitGrp where instance Show DigitGrp where
show (DigitGrp len num) = "\"" ++ padding ++ numStr ++ "\"" show (DigitGrp len n) = "\"" ++ padding ++ numStr ++ "\""
where numStr = show num where numStr = show n
padding = genericReplicate (toInteger len - toInteger (length numStr)) '0' padding = genericReplicate (toInteger len - toInteger (length numStr)) '0'
instance Sem.Semigroup DigitGrp where instance Sem.Semigroup DigitGrp where

View File

@ -13,9 +13,7 @@ A reader for CSV data, using an extra rules file to help interpret the data.
--- ** language --- ** language
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
@ -38,7 +36,7 @@ where
--- ** imports --- ** imports
import Control.Applicative (liftA2) import Control.Applicative (liftA2)
import Control.Monad (unless, when) import Control.Monad (unless, when, void)
import Control.Monad.Except (ExceptT(..), liftEither, throwError) import Control.Monad.Except (ExceptT(..), liftEither, throwError)
import qualified Control.Monad.Fail as Fail import qualified Control.Monad.Fail as Fail
import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.IO.Class (MonadIO, liftIO)
@ -104,13 +102,13 @@ parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
parse iopts f t = do parse iopts f t = do
let rulesfile = mrules_file_ iopts let rulesfile = mrules_file_ iopts
readJournalFromCsv rulesfile f t readJournalFromCsv rulesfile f t
-- journalFinalise assumes the journal's items are
-- reversed, as produced by JournalReader's parser.
-- But here they are already properly ordered. So we'd
-- better preemptively reverse them once more. XXX inefficient
<&> journalReverse
-- apply any command line account aliases. Can fail with a bad replacement pattern. -- apply any command line account aliases. Can fail with a bad replacement pattern.
>>= liftEither . journalApplyAliases (aliasesFromOpts iopts) >>= liftEither . journalApplyAliases (aliasesFromOpts iopts)
-- journalFinalise assumes the journal's items are
-- reversed, as produced by JournalReader's parser.
-- But here they are already properly ordered. So we'd
-- better preemptively reverse them once more. XXX inefficient
. journalReverse
>>= journalFinalise iopts{balancingopts_=(balancingopts_ iopts){ignore_assertions_=True}} f t >>= journalFinalise iopts{balancingopts_=(balancingopts_ iopts){ignore_assertions_=True}} f t
--- ** reading rules files --- ** reading rules files
@ -193,14 +191,14 @@ instance ShowErrorComponent String where
-- Included file paths may be relative to the directory of the provided file path. -- Included file paths may be relative to the directory of the provided file path.
-- This is done as a pre-parse step to simplify the CSV rules parser. -- This is done as a pre-parse step to simplify the CSV rules parser.
expandIncludes :: FilePath -> Text -> IO Text expandIncludes :: FilePath -> Text -> IO Text
expandIncludes dir content = mapM (expandLine dir) (T.lines content) >>= return . T.unlines expandIncludes dir0 content = mapM (expandLine dir0) (T.lines content) <&> T.unlines
where where
expandLine dir line = expandLine dir1 line =
case line of case line of
(T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< T.readFile f' (T.stripPrefix "include " -> Just f) -> expandIncludes dir2 =<< T.readFile f'
where where
f' = dir </> T.unpack (T.dropWhile isSpace f) f' = dir1 </> T.unpack (T.dropWhile isSpace f)
dir' = takeDirectory f' dir2 = takeDirectory f'
_ -> return line _ -> return line
-- | An error-throwing IO action that parses this text as CSV conversion rules -- | An error-throwing IO action that parses this text as CSV conversion rules
@ -257,7 +255,7 @@ type CsvRules = CsvRules' (Text -> [ConditionalBlock])
instance Eq CsvRules where instance Eq CsvRules where
r1 == r2 = (rdirectives r1, rcsvfieldindexes r1, rassignments r1) == r1 == r2 = (rdirectives r1, rcsvfieldindexes r1, rassignments r1) ==
(rdirectives r2, rcsvfieldindexes r2, rassignments r2) (rdirectives r2, rcsvfieldindexes r2, rassignments r2)
-- Custom Show instance used for debug output: omit the rblocksassigning field, which isn't showable. -- Custom Show instance used for debug output: omit the rblocksassigning field, which isn't showable.
instance Show CsvRules where instance Show CsvRules where
@ -582,7 +580,7 @@ conditionaltablep = do
newline newline
body <- flip manyTill (lift eolof) $ do body <- flip manyTill (lift eolof) $ do
off <- getOffset off <- getOffset
m <- matcherp' (char sep >> return ()) m <- matcherp' $ void $ char sep
vs <- T.split (==sep) . T.pack <$> lift restofline vs <- T.split (==sep) . T.pack <$> lift restofline
if (length vs /= length fields) if (length vs /= length fields)
then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d" (length fields) (length vs)) :: String) then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d" (length fields) (length vs)) :: String)
@ -745,8 +743,8 @@ readJournalFromCsv mrulesfile csvfile csvdata = do
-- than one date and the first date is more recent than the last): -- than one date and the first date is more recent than the last):
-- reverse them to get same-date transactions ordered chronologically. -- reverse them to get same-date transactions ordered chronologically.
txns' = txns' =
(if newestfirst || mdataseemsnewestfirst == Just True (if newestfirst || mdataseemsnewestfirst == Just True
then dbg7 "reversed csv txns" . reverse else id) then dbg7 "reversed csv txns" . reverse else id)
txns txns
where where
newestfirst = dbg6 "newestfirst" $ isJust $ getDirective "newest-first" rules newestfirst = dbg6 "newestfirst" $ isJust $ getDirective "newest-first" rules
@ -757,7 +755,7 @@ readJournalFromCsv mrulesfile csvfile csvdata = do
-- Second, sort by date. -- Second, sort by date.
txns'' = dbg7 "date-sorted csv txns" $ sortBy (comparing tdate) txns' txns'' = dbg7 "date-sorted csv txns" $ sortBy (comparing tdate) txns'
liftIO $ when (not rulesfileexists) $ do liftIO $ unless rulesfileexists $ do
dbg1IO "creating conversion rules file" rulesfile dbg1IO "creating conversion rules file" rulesfile
T.writeFile rulesfile rulestext T.writeFile rulesfile rulestext
@ -804,7 +802,7 @@ validateCsv :: CsvRules -> Int -> CSV -> Either String [CsvRecord]
validateCsv rules numhdrlines = validate . applyConditionalSkips . drop numhdrlines . filternulls validateCsv rules numhdrlines = validate . applyConditionalSkips . drop numhdrlines . filternulls
where where
filternulls = filter (/=[""]) filternulls = filter (/=[""])
skipCount r = skipnum r =
case (getEffectiveAssignment rules r "end", getEffectiveAssignment rules r "skip") of case (getEffectiveAssignment rules r "end", getEffectiveAssignment rules r "skip") of
(Nothing, Nothing) -> Nothing (Nothing, Nothing) -> Nothing
(Just _, _) -> Just maxBound (Just _, _) -> Just maxBound
@ -812,7 +810,7 @@ validateCsv rules numhdrlines = validate . applyConditionalSkips . drop numhdrli
(Nothing, Just x) -> Just (read $ T.unpack x) (Nothing, Just x) -> Just (read $ T.unpack x)
applyConditionalSkips [] = [] applyConditionalSkips [] = []
applyConditionalSkips (r:rest) = applyConditionalSkips (r:rest) =
case skipCount r of case skipnum r of
Nothing -> r:(applyConditionalSkips rest) Nothing -> r:(applyConditionalSkips rest)
Just cnt -> applyConditionalSkips (drop (cnt-1) rest) Just cnt -> applyConditionalSkips (drop (cnt-1) rest)
validate [] = Right [] validate [] = Right []
@ -869,15 +867,15 @@ transactionFromCsvRecord sourcepos rules record = t
field = hledgerField rules record :: HledgerFieldName -> Maybe FieldTemplate field = hledgerField rules record :: HledgerFieldName -> Maybe FieldTemplate
fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text
parsedate = parseDateWithCustomOrDefaultFormats (rule "date-format") parsedate = parseDateWithCustomOrDefaultFormats (rule "date-format")
mkdateerror datefield datevalue mdateformat = T.unpack $ T.unlines mkdateerror datefield datevalue mdateformat' = T.unpack $ T.unlines
["error: could not parse \""<>datevalue<>"\" as a date using date format " ["error: could not parse \""<>datevalue<>"\" as a date using date format "
<>maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" (T.pack . show) mdateformat <>maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" (T.pack . show) mdateformat'
,showRecord record ,showRecord record
,"the "<>datefield<>" rule is: "<>(fromMaybe "required, but missing" $ field datefield) ,"the "<>datefield<>" rule is: "<>(fromMaybe "required, but missing" $ field datefield)
,"the date-format is: "<>fromMaybe "unspecified" mdateformat ,"the date-format is: "<>fromMaybe "unspecified" mdateformat'
,"you may need to " ,"you may need to "
<>"change your "<>datefield<>" rule, " <>"change your "<>datefield<>" rule, "
<>maybe "add a" (const "change your") mdateformat<>" date-format rule, " <>maybe "add a" (const "change your") mdateformat'<>" date-format rule, "
<>"or "<>maybe "add a" (const "change your") mskip<>" skip rule" <>"or "<>maybe "add a" (const "change your") mskip<>" skip rule"
,"for m/d/y or d/m/y dates, use date-format %-m/%-d/%Y or date-format %-d/%-m/%Y" ,"for m/d/y or d/m/y dates, use date-format %-m/%-d/%Y or date-format %-d/%-m/%Y"
] ]
@ -894,7 +892,7 @@ transactionFromCsvRecord sourcepos rules record = t
-- PARTIAL: -- PARTIAL:
date' = fromMaybe (error' $ mkdateerror "date" date mdateformat) $ parsedate date date' = fromMaybe (error' $ mkdateerror "date" date mdateformat) $ parsedate date
mdate2 = fieldval "date2" mdate2 = fieldval "date2"
mdate2' = maybe Nothing (maybe (error' $ mkdateerror "date2" (fromMaybe "" mdate2) mdateformat) Just . parsedate) mdate2 mdate2' = (maybe (error' $ mkdateerror "date2" (fromMaybe "" mdate2) mdateformat) Just . parsedate) =<< mdate2
status = status =
case fieldval "status" of case fieldval "status" of
Nothing -> Unmarked Nothing -> Unmarked
@ -904,12 +902,12 @@ transactionFromCsvRecord sourcepos rules record = t
["error: could not parse \""<>s<>"\" as a cleared status (should be *, ! or empty)" ["error: could not parse \""<>s<>"\" as a cleared status (should be *, ! or empty)"
,"the parse error is: "<>T.pack (customErrorBundlePretty err) ,"the parse error is: "<>T.pack (customErrorBundlePretty err)
] ]
code = maybe "" singleline $ fieldval "code" code = maybe "" singleline' $ fieldval "code"
description = maybe "" singleline $ fieldval "description" description = maybe "" singleline' $ fieldval "description"
comment = maybe "" unescapeNewlines $ fieldval "comment" comment = maybe "" unescapeNewlines $ fieldval "comment"
precomment = maybe "" unescapeNewlines $ fieldval "precomment" precomment = maybe "" unescapeNewlines $ fieldval "precomment"
singleline = T.unwords . filter (not . T.null) . map T.strip . T.lines singleline' = T.unwords . filter (not . T.null) . map T.strip . T.lines
unescapeNewlines = T.intercalate "\n" . T.splitOn "\\n" unescapeNewlines = T.intercalate "\n" . T.splitOn "\\n"
---------------------------------------------------------------------- ----------------------------------------------------------------------
@ -918,7 +916,7 @@ transactionFromCsvRecord sourcepos rules record = t
p1IsVirtual = (accountNamePostingType <$> fieldval "account1") == Just VirtualPosting p1IsVirtual = (accountNamePostingType <$> fieldval "account1") == Just VirtualPosting
ps = [p | n <- [1..maxpostings] ps = [p | n <- [1..maxpostings]
,let comment = maybe "" unescapeNewlines $ fieldval ("comment"<> T.pack (show n)) ,let cmt = maybe "" unescapeNewlines $ fieldval ("comment"<> T.pack (show n))
,let currency = fromMaybe "" (fieldval ("currency"<> T.pack (show n)) <|> fieldval "currency") ,let currency = fromMaybe "" (fieldval ("currency"<> T.pack (show n)) <|> fieldval "currency")
,let mamount = getAmount rules record currency p1IsVirtual n ,let mamount = getAmount rules record currency p1IsVirtual n
,let mbalance = getBalance rules record currency n ,let mbalance = getBalance rules record currency n
@ -930,7 +928,7 @@ transactionFromCsvRecord sourcepos rules record = t
,pamount = fromMaybe missingmixedamt mamount ,pamount = fromMaybe missingmixedamt mamount
,ptransaction = Just t ,ptransaction = Just t
,pbalanceassertion = mkBalanceAssertion rules record <$> mbalance ,pbalanceassertion = mkBalanceAssertion rules record <$> mbalance
,pcomment = comment ,pcomment = cmt
,ptype = accountNamePostingType acct ,ptype = accountNamePostingType acct
} }
] ]
@ -967,7 +965,7 @@ getAmount rules record currency p1IsVirtual n =
unnumberedfieldnames = ["amount","amount-in","amount-out"] unnumberedfieldnames = ["amount","amount-in","amount-out"]
-- amount field names which can affect this posting -- amount field names which can affect this posting
fieldnames = map (("amount"<> T.pack(show n))<>) ["","-in","-out"] fieldnames = map (("amount"<> T.pack (show n))<>) ["","-in","-out"]
-- For posting 1, also recognise the old amount/amount-in/amount-out names. -- For posting 1, also recognise the old amount/amount-in/amount-out names.
-- For posting 2, the same but only if posting 1 needs balancing. -- For posting 2, the same but only if posting 1 needs balancing.
++ if n==1 || n==2 && not p1IsVirtual then unnumberedfieldnames else [] ++ if n==1 || n==2 && not p1IsVirtual then unnumberedfieldnames else []
@ -1000,6 +998,37 @@ getAmount rules record currency p1IsVirtual n =
[] -> Nothing [] -> Nothing
[(f,a)] -> Just $ negateIfOut f a [(f,a)] -> Just $ negateIfOut f a
fs -> error' . T.unpack . textChomp . T.unlines $ -- PARTIAL: fs -> error' . T.unpack . textChomp . T.unlines $ -- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
["in CSV rules:" ["in CSV rules:"
,"While processing " <> showRecord record ,"While processing " <> showRecord record
,"while calculating amount for posting " <> T.pack (show n) ,"while calculating amount for posting " <> T.pack (show n)
@ -1038,6 +1067,37 @@ getBalance rules record currency n = do
parseAmount :: CsvRules -> CsvRecord -> Text -> Text -> MixedAmount parseAmount :: CsvRules -> CsvRecord -> Text -> Text -> MixedAmount
parseAmount rules record currency s = parseAmount rules record currency s =
either mkerror mixedAmount $ -- PARTIAL: either mkerror mixedAmount $ -- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
-- PARTIAL:
runParser (evalStateT (amountp <* eof) journalparsestate) "" $ runParser (evalStateT (amountp <* eof) journalparsestate) "" $
currency <> simplifySign s currency <> simplifySign s
where where
@ -1068,8 +1128,8 @@ parseBalanceAmount rules record currency n s =
-- the csv record's line number would be good -- the csv record's line number would be good
where where
journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules} journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules}
mkerror n s e = error' . T.unpack $ T.unlines mkerror n' s' e = error' . T.unpack $ T.unlines
["error: could not parse \"" <> s <> "\" as balance"<> T.pack (show n) <> " amount" ["error: could not parse \"" <> s' <> "\" as balance"<> T.pack (show n') <> " amount"
,showRecord record ,showRecord record
,showRules rules record ,showRules rules record
-- ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency -- ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency
@ -1252,17 +1312,19 @@ replaceCsvFieldReference rules record s = case T.uncons s of
-- column number, ("date" or "1"), from the given CSV record, if such a field exists. -- column number, ("date" or "1"), from the given CSV record, if such a field exists.
csvFieldValue :: CsvRules -> CsvRecord -> CsvFieldName -> Maybe Text csvFieldValue :: CsvRules -> CsvRecord -> CsvFieldName -> Maybe Text
csvFieldValue rules record fieldname = do csvFieldValue rules record fieldname = do
fieldindex <- if | T.all isDigit fieldname -> readMay $ T.unpack fieldname fieldindex <-
| otherwise -> lookup (T.toLower fieldname) $ rcsvfieldindexes rules if T.all isDigit fieldname
then readMay $ T.unpack fieldname
else lookup (T.toLower fieldname) $ rcsvfieldindexes rules
T.strip <$> atMay record (fieldindex-1) T.strip <$> atMay record (fieldindex-1)
-- | Parse the date string using the specified date-format, or if unspecified -- | Parse the date string using the specified date-format, or if unspecified
-- the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, leading -- the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, leading
-- zeroes optional). -- zeroes optional).
parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> Text -> Maybe Day parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> Text -> Maybe Day
parseDateWithCustomOrDefaultFormats mformat s = asum $ map parsewith formats parseDateWithCustomOrDefaultFormats mformat s = asum $ map parsewith' formats
where where
parsewith = flip (parseTimeM True defaultTimeLocale) (T.unpack s) parsewith' = flip (parseTimeM True defaultTimeLocale) (T.unpack s)
formats = map T.unpack $ maybe formats = map T.unpack $ maybe
["%Y/%-m/%-d" ["%Y/%-m/%-d"
,"%Y-%-m-%-d" ,"%Y-%-m-%-d"
@ -1299,6 +1361,37 @@ tests_CsvReader = testGroup "CsvReader" [
] ]
,testGroup "conditionalblockp" [ ,testGroup "conditionalblockp" [
testCase "space after conditional" $ -- #1120 testCase "space after conditional" $ -- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
-- #1120
parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?= parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=
(Right $ CB{cbMatchers=[RecordMatcher None $ toRegexCI' "a"],cbAssignments=[("account2","b")]}) (Right $ CB{cbMatchers=[RecordMatcher None $ toRegexCI' "a"],cbAssignments=[("account2","b")]})

View File

@ -261,8 +261,8 @@ includedirectivep = do
prefixedglob <- T.unpack <$> takeWhileP Nothing (/= '\n') -- don't consume newline yet prefixedglob <- T.unpack <$> takeWhileP Nothing (/= '\n') -- don't consume newline yet
parentoff <- getOffset parentoff <- getOffset
parentpos <- getSourcePos parentpos <- getSourcePos
let (mprefix,glob) = splitReaderPrefix prefixedglob let (mprefix,glb) = splitReaderPrefix prefixedglob
paths <- getFilePaths parentoff parentpos glob paths <- getFilePaths parentoff parentpos glb
let prefixedpaths = case mprefix of let prefixedpaths = case mprefix of
Nothing -> paths Nothing -> paths
Just fmt -> map ((fmt++":")++) paths Just fmt -> map ((fmt++":")++) paths
@ -460,8 +460,8 @@ commoditydirectiveonelinep = do
string "commodity" string "commodity"
lift skipNonNewlineSpaces1 lift skipNonNewlineSpaces1
off <- getOffset off <- getOffset
amount <- amountp amt <- amountp
pure $ (off, amount) pure $ (off, amt)
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
_ <- lift followingcommentp _ <- lift followingcommentp
let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg6 "style from commodity directive" astyle} let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg6 "style from commodity directive" astyle}
@ -489,8 +489,8 @@ commoditydirectivemultilinep = do
lift skipNonNewlineSpaces1 lift skipNonNewlineSpaces1
sym <- lift commoditysymbolp sym <- lift commoditysymbolp
_ <- lift followingcommentp _ <- lift followingcommentp
mformat <- lastMay <$> many (indented $ formatdirectivep sym) mfmt <- lastMay <$> many (indented $ formatdirectivep sym)
let comm = Commodity{csymbol=sym, cformat=mformat} let comm = Commodity{csymbol=sym, cformat=mfmt}
modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j}) modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j})
where where
indented = (lift skipNonNewlineSpaces1 >>) indented = (lift skipNonNewlineSpaces1 >>)
@ -674,7 +674,7 @@ periodictransactionp = do
-- first parsing with 'singlespacedtextp', then "re-parsing" with -- first parsing with 'singlespacedtextp', then "re-parsing" with
-- 'periodexprp' saves 'periodexprp' from having to respect the single- -- 'periodexprp' saves 'periodexprp' from having to respect the single-
-- and double-space parsing rules -- and double-space parsing rules
(interval, span) <- lift $ reparseExcerpt periodExcerpt $ do (interval, spn) <- lift $ reparseExcerpt periodExcerpt $ do
pexp <- periodexprp refdate pexp <- periodexprp refdate
(<|>) eof $ do (<|>) eof $ do
offset1 <- getOffset offset1 <- getOffset
@ -687,7 +687,7 @@ periodictransactionp = do
pure pexp pure pexp
-- In periodic transactions, the period expression has an additional constraint: -- In periodic transactions, the period expression has an additional constraint:
case checkPeriodicTransactionStartDate interval span periodtxt of case checkPeriodicTransactionStartDate interval spn periodtxt of
Just e -> customFailure $ parseErrorAt off e Just e -> customFailure $ parseErrorAt off e
Nothing -> pure () Nothing -> pure ()
@ -701,7 +701,7 @@ periodictransactionp = do
return $ nullperiodictransaction{ return $ nullperiodictransaction{
ptperiodexpr=periodtxt ptperiodexpr=periodtxt
,ptinterval=interval ,ptinterval=interval
,ptspan=span ,ptspan=spn
,ptstatus=status ,ptstatus=status
,ptcode=code ,ptcode=code
,ptdescription=description ,ptdescription=description
@ -767,7 +767,7 @@ postingphelper isPostingRule mTransactionYear = do
let (ptype, account') = (accountNamePostingType account, textUnbracket account) let (ptype, account') = (accountNamePostingType account, textUnbracket account)
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
mult <- if isPostingRule then multiplierp else pure False mult <- if isPostingRule then multiplierp else pure False
amount <- optional $ amountpwithmultiplier mult amt <- optional $ amountpwithmultiplier mult
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
massertion <- optional balanceassertionp massertion <- optional balanceassertionp
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
@ -777,7 +777,7 @@ postingphelper isPostingRule mTransactionYear = do
, pdate2=mdate2 , pdate2=mdate2
, pstatus=status , pstatus=status
, paccount=account' , paccount=account'
, pamount=maybe missingmixedamt mixedAmount amount , pamount=maybe missingmixedamt mixedAmount amt
, pcomment=comment , pcomment=comment
, ptype=ptype , ptype=ptype
, ptags=tags , ptags=tags

View File

@ -28,7 +28,6 @@ inc.client1 .... .... ..
--- ** language --- ** language
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
--- ** exports --- ** exports
module Hledger.Read.TimedotReader ( module Hledger.Read.TimedotReader (
@ -173,7 +172,7 @@ entryp = do
lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1] lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1]
a <- modifiedaccountnamep a <- modifiedaccountnamep
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
hrs <- hours <-
try (lift followingcommentp >> return 0) try (lift followingcommentp >> return 0)
<|> (lift durationp <* <|> (lift durationp <*
(try (lift followingcommentp) <|> (newline >> return ""))) (try (lift followingcommentp) <|> (newline >> return "")))
@ -187,7 +186,7 @@ entryp = do
tstatus = Cleared, tstatus = Cleared,
tpostings = [ tpostings = [
nullposting{paccount=a nullposting{paccount=a
,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hrs, astyle=s} ,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hours, astyle=s}
,ptype=VirtualPosting ,ptype=VirtualPosting
,ptransaction=Just t ,ptransaction=Just t
} }

View File

@ -245,8 +245,8 @@ accountTransactionsReportByCommodity tr =
-- balance amount) components that don't involve the specified -- balance amount) components that don't involve the specified
-- commodity. Other item fields such as the transaction are left unchanged. -- commodity. Other item fields such as the transaction are left unchanged.
filterAccountTransactionsReportByCommodity :: CommoditySymbol -> AccountTransactionsReport -> AccountTransactionsReport filterAccountTransactionsReportByCommodity :: CommoditySymbol -> AccountTransactionsReport -> AccountTransactionsReport
filterAccountTransactionsReportByCommodity c = filterAccountTransactionsReportByCommodity comm =
fixTransactionsReportItemBalances . concatMap (filterTransactionsReportItemByCommodity c) fixTransactionsReportItemBalances . concatMap (filterTransactionsReportItemByCommodity comm)
where where
filterTransactionsReportItemByCommodity c (t,t2,s,o,a,bal) filterTransactionsReportItemByCommodity c (t,t2,s,o,a,bal)
| c `elem` cs = [item'] | c `elem` cs = [item']
@ -261,9 +261,9 @@ filterAccountTransactionsReportByCommodity c =
fixTransactionsReportItemBalances items = reverse $ i:(go startbal is) fixTransactionsReportItemBalances items = reverse $ i:(go startbal is)
where where
i:is = reverse items i:is = reverse items
startbal = filterMixedAmountByCommodity c $ triBalance i startbal = filterMixedAmountByCommodity comm $ triBalance i
go _ [] = [] go _ [] = []
go bal ((t,t2,s,o,amt,_):is) = (t,t2,s,o,amt,bal'):go bal' is go bal ((t,t2,s,o,amt,_):is') = (t,t2,s,o,amt,bal'):go bal' is'
where bal' = bal `maPlus` amt where bal' = bal `maPlus` amt
-- tests -- tests

View File

@ -133,17 +133,17 @@ journalAddBudgetGoalTransactions bopts ropts reportspan j =
Just d -> Just d' Just d -> Just d'
where where
-- the interval and any date span of the periodic transaction with longest period -- the interval and any date span of the periodic transaction with longest period
(interval, span) = (intervl, spn) =
case budgetpts of case budgetpts of
[] -> (Days 1, nulldatespan) [] -> (Days 1, nulldatespan)
pts -> (ptinterval pt, ptspan pt) pts -> (ptinterval pt, ptspan pt)
where pt = maximumBy (comparing ptinterval) pts -- PARTIAL: maximumBy won't fail where pt = maximumBy (comparing ptinterval) pts -- PARTIAL: maximumBy won't fail
-- the natural start of this interval on or before the journal/report start -- the natural start of this interval on or before the journal/report start
intervalstart = intervalStartBefore interval d intervalstart = intervalStartBefore intervl d
-- the natural interval start before the journal/report start, -- the natural interval start before the journal/report start,
-- or the rule-specified start if later, -- or the rule-specified start if later,
-- but no later than the journal/report start. -- but no later than the journal/report start.
d' = min d $ maybe intervalstart (max intervalstart) $ spanStart span d' = min d $ maybe intervalstart (max intervalstart) $ spanStart spn
-- select periodic transactions matching a pattern -- select periodic transactions matching a pattern
-- (the argument of the (final) --budget option). -- (the argument of the (final) --budget option).
@ -308,11 +308,11 @@ budgetReportAsTable
| transpose_ = \(Tab.Table rh ch vals) -> Tab.Table ch rh (transpose vals) | transpose_ = \(Tab.Table rh ch vals) -> Tab.Table ch rh (transpose vals)
| otherwise = id | otherwise = id
(accts, rows, totalrows) = (accts, prependcs itemscs (padcells texts), prependcs trcs (padtr trtexts)) (accts, rows, totalrows) = (accts', prependcs itemscs (padcells texts), prependcs trcs (padtr trtexts))
where where
shownitems :: [[(AccountName, WideBuilder, BudgetDisplayRow)]] shownitems :: [[(AccountName, WideBuilder, BudgetDisplayRow)]]
shownitems = (fmap (\i -> fmap (\(cs, cvals) -> (renderacct i, cs, cvals)) . showrow $ rowToBudgetCells i) items) shownitems = (fmap (\i -> fmap (\(cs, cvals) -> (renderacct i, cs, cvals)) . showrow $ rowToBudgetCells i) items)
(accts, itemscs, texts) = unzip3 $ concat shownitems (accts', itemscs, texts) = unzip3 $ concat shownitems
showntr :: [[(WideBuilder, BudgetDisplayRow)]] showntr :: [[(WideBuilder, BudgetDisplayRow)]]
showntr = [showrow $ rowToBudgetCells tr] showntr = [showrow $ rowToBudgetCells tr]
@ -381,10 +381,8 @@ budgetReportAsTable
where where
actual' = fromMaybe nullmixedamt actual actual' = fromMaybe nullmixedamt actual
budgetAndPerc b = uncurry zip budgetAndPerc b =
( showmixed b zip (showmixed b) (fmap (wbFromText . T.pack . show . roundTo 0) <$> percbudget actual' b)
, fmap (wbFromText . T.pack . show . roundTo 0) <$> percbudget actual' b
)
full full
| Just b <- mbudget = Just <$> budgetAndPerc b | Just b <- mbudget = Just <$> budgetAndPerc b
@ -397,9 +395,9 @@ budgetReportAsTable
(TB.fromText . flip T.replicate " " $ actualwidth - w) <> b (TB.fromText . flip T.replicate " " $ actualwidth - w) <> b
(totalpercentwidth, totalbudgetwidth) = (totalpercentwidth, totalbudgetwidth) =
let totalpercentwidth = if percentwidth == 0 then 0 else percentwidth + 5 let totalpercentwidth' = if percentwidth == 0 then 0 else percentwidth + 5
in ( totalpercentwidth in ( totalpercentwidth'
, if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth + 3 , if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth' + 3
) )
-- | Display a padded budget string -- | Display a padded budget string
@ -446,14 +444,20 @@ budgetReportAsCsv
(PeriodicReport colspans items tr) (PeriodicReport colspans items tr)
= (if transpose_ then transpose else id) $ = (if transpose_ then transpose else id) $
-- heading row
-- heading row -- heading row
("Account" : ("Account" :
["Commodity" | layout_ == LayoutBare ] ["Commodity" | layout_ == LayoutBare ]
++ concatMap (\span -> [showDateSpan span, "budget"]) colspans ++ concatMap (\spn -> [showDateSpan spn, "budget"]) colspans
++ concat [["Total" ,"budget"] | row_total_] ++ concat [["Total" ,"budget"] | row_total_]
++ concat [["Average","budget"] | average_] ++ concat [["Average","budget"] | average_]
) : ) :
-- account rows
-- account rows -- account rows
concatMap (rowAsTexts prrFullName) items concatMap (rowAsTexts prrFullName) items
@ -461,23 +465,23 @@ budgetReportAsCsv
++ concat [ rowAsTexts (const "Total:") tr | not no_total_ ] ++ concat [ rowAsTexts (const "Total:") tr | not no_total_ ]
where where
flattentuples abs = concat [[a,b] | (a,b) <- abs] flattentuples tups = concat [[a,b] | (a,b) <- tups]
showNorm = maybe "" (wbToText . showMixedAmountB oneLine) showNorm = maybe "" (wbToText . showMixedAmountB oneLine)
rowAsTexts :: (PeriodicReportRow a BudgetCell -> Text) rowAsTexts :: (PeriodicReportRow a BudgetCell -> Text)
-> PeriodicReportRow a BudgetCell -> PeriodicReportRow a BudgetCell
-> [[Text]] -> [[Text]]
rowAsTexts render row@(PeriodicReportRow _ as (rowtot,budgettot) (rowavg, budgetavg)) rowAsTexts render row@(PeriodicReportRow _ as (rowtot,budgettot) (rowavg, budgetavg))
| layout_ /= LayoutBare = [render row : fmap showNorm all] | layout_ /= LayoutBare = [render row : fmap showNorm vals]
| otherwise = | otherwise =
joinNames . zipWith (:) cs -- add symbols and names joinNames . zipWith (:) cs -- add symbols and names
. transpose -- each row becomes a list of Text quantities . transpose -- each row becomes a list of Text quantities
. fmap (fmap wbToText . showMixedAmountLinesB oneLine{displayOrder=Just cs, displayMinWidth=Nothing} . fmap (fmap wbToText . showMixedAmountLinesB oneLine{displayOrder=Just cs, displayMinWidth=Nothing}
.fromMaybe nullmixedamt) .fromMaybe nullmixedamt)
$ all $ vals
where where
cs = S.toList . foldl' S.union mempty . fmap maCommodities $ catMaybes all cs = S.toList . foldl' S.union mempty . fmap maCommodities $ catMaybes vals
all = flattentuples as vals = flattentuples as
++ concat [[rowtot, budgettot] | row_total_] ++ concat [[rowtot, budgettot] | row_total_]
++ concat [[rowavg, budgetavg] | average_] ++ concat [[rowavg, budgetavg] | average_]

View File

@ -343,7 +343,7 @@ calculateReportMatrix rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle startb
avalue = acctApplyBoth . mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle avalue = acctApplyBoth . mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle
acctApplyBoth f a = a{aibalance = f $ aibalance a, aebalance = f $ aebalance a} acctApplyBoth f a = a{aibalance = f $ aibalance a, aebalance = f $ aebalance a}
historicalDate = minimumMay $ mapMaybe spanStart colspans historicalDate = minimumMay $ mapMaybe spanStart colspans
zeros = M.fromList [(span, nullacct) | span <- colspans] zeros = M.fromList [(spn, nullacct) | spn <- colspans]
colspans = map fst colps colspans = map fst colps
@ -406,11 +406,11 @@ displayedAccounts :: ReportSpec
-> HashMap AccountName (Map DateSpan Account) -> HashMap AccountName (Map DateSpan Account)
-> HashMap AccountName DisplayName -> HashMap AccountName DisplayName
displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts valuedaccts displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts valuedaccts
| depth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1 | qdepth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1
| otherwise = HM.mapWithKey (\a _ -> displayedName a) displayedAccts | otherwise = HM.mapWithKey (\a _ -> displayedName a) displayedAccts
where where
-- Accounts which are to be displayed -- Accounts which are to be displayed
displayedAccts = (if depth == 0 then id else HM.filterWithKey keep) valuedaccts displayedAccts = (if qdepth == 0 then id else HM.filterWithKey keep) valuedaccts
where where
keep name amts = isInteresting name amts || name `HM.member` interestingParents keep name amts = isInteresting name amts || name `HM.member` interestingParents
@ -429,7 +429,7 @@ displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts
-- Accounts interesting for their own sake -- Accounts interesting for their own sake
isInteresting name amts = isInteresting name amts =
d <= depth -- Throw out anything too deep d <= qdepth -- Throw out anything too deep
&& ( name `Set.member` unelidableaccts -- Unelidable accounts should be kept unless too deep && ( name `Set.member` unelidableaccts -- Unelidable accounts should be kept unless too deep
||(empty_ ropts && keepWhenEmpty amts) -- Keep empty accounts when called with --empty ||(empty_ ropts && keepWhenEmpty amts) -- Keep empty accounts when called with --empty
|| not (isZeroRow balance amts) -- Keep everything with a non-zero balance in the row || not (isZeroRow balance amts) -- Keep everything with a non-zero balance in the row
@ -440,8 +440,8 @@ displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts
ALFlat -> const True -- Keep all empty accounts in flat mode ALFlat -> const True -- Keep all empty accounts in flat mode
ALTree -> all (null . asubs) -- Keep only empty leaves in tree mode ALTree -> all (null . asubs) -- Keep only empty leaves in tree mode
balance = maybeStripPrices . case accountlistmode_ ropts of balance = maybeStripPrices . case accountlistmode_ ropts of
ALTree | d == depth -> aibalance ALTree | d == qdepth -> aibalance
_ -> aebalance _ -> aebalance
where maybeStripPrices = if conversionop_ ropts == Just NoConversionOp then id else mixedAmountStripPrices where maybeStripPrices = if conversionop_ ropts == Just NoConversionOp then id else mixedAmountStripPrices
-- Accounts interesting because they are a fork for interesting subaccounts -- Accounts interesting because they are a fork for interesting subaccounts
@ -453,7 +453,7 @@ displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts
minSubs = if no_elide_ ropts then 1 else 2 minSubs = if no_elide_ ropts then 1 else 2
isZeroRow balance = all (mixedAmountLooksZero . balance) isZeroRow balance = all (mixedAmountLooksZero . balance)
depth = fromMaybe maxBound $ queryDepth query qdepth = fromMaybe maxBound $ queryDepth query
numSubs = subaccountTallies . HM.keys $ HM.filterWithKey isInteresting valuedaccts numSubs = subaccountTallies . HM.keys $ HM.filterWithKey isInteresting valuedaccts
-- | Sort the rows by amount or by account declaration order. -- | Sort the rows by amount or by account declaration order.
@ -534,10 +534,10 @@ transposeMap :: [(DateSpan, HashMap AccountName a)]
-> HashMap AccountName (Map DateSpan a) -> HashMap AccountName (Map DateSpan a)
transposeMap = foldr (uncurry addSpan) mempty transposeMap = foldr (uncurry addSpan) mempty
where where
addSpan span acctmap seen = HM.foldrWithKey (addAcctSpan span) seen acctmap addSpan spn acctmap seen = HM.foldrWithKey (addAcctSpan spn) seen acctmap
addAcctSpan span acct a = HM.alter f acct addAcctSpan spn acct a = HM.alter f acct
where f = Just . M.insert span a . fromMaybe mempty where f = Just . M.insert spn a . fromMaybe mempty
-- | A sorting helper: sort a list of things (eg report rows) keyed by account name -- | A sorting helper: sort a list of things (eg report rows) keyed by account name
-- to match the provided ordering of those same account names. -- to match the provided ordering of those same account names.

View File

@ -73,8 +73,8 @@ postingsReport rspec@ReportSpec{_rsReportOpts=ropts@ReportOpts{..}} j = items
-- Postings, or summary postings with their subperiod's end date, to be displayed. -- Postings, or summary postings with their subperiod's end date, to be displayed.
displayps :: [(Posting, Maybe Period)] displayps :: [(Posting, Maybe Period)]
| multiperiod = [(p, Just period) | (p, period) <- summariseps reportps] | multiperiod = [(p', Just period') | (p', period') <- summariseps reportps]
| otherwise = [(p, Nothing) | p <- reportps] | otherwise = [(p', Nothing) | p' <- reportps]
where where
summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans
showempty = empty_ || average_ showempty = empty_ || average_
@ -189,9 +189,9 @@ summarisePostingsByInterval wd mdepth showempty colspans =
-- with 0 amount. -- with 0 amount.
-- --
summarisePostingsInDateSpan :: DateSpan -> WhichDate -> Maybe Int -> Bool -> [Posting] -> [SummaryPosting] summarisePostingsInDateSpan :: DateSpan -> WhichDate -> Maybe Int -> Bool -> [Posting] -> [SummaryPosting]
summarisePostingsInDateSpan span@(DateSpan b e) wd mdepth showempty ps summarisePostingsInDateSpan spn@(DateSpan b e) wd mdepth showempty ps
| null ps && (isNothing b || isNothing e) = [] | null ps && (isNothing b || isNothing e) = []
| null ps && showempty = [(summaryp, dateSpanAsPeriod span)] | null ps && showempty = [(summaryp, dateSpanAsPeriod spn)]
| otherwise = summarypes | otherwise = summarypes
where where
postingdate = if wd == PrimaryDate then postingDate else postingDate2 postingdate = if wd == PrimaryDate then postingDate else postingDate2
@ -200,14 +200,14 @@ summarisePostingsInDateSpan span@(DateSpan b e) wd mdepth showempty ps
clippedanames = nub $ map (clipAccountName mdepth) anames clippedanames = nub $ map (clipAccountName mdepth) anames
summaryps | mdepth == Just 0 = [summaryp{paccount="...",pamount=sumPostings ps}] summaryps | mdepth == Just 0 = [summaryp{paccount="...",pamount=sumPostings ps}]
| otherwise = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames] | otherwise = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]
summarypes = map (, dateSpanAsPeriod span) $ (if showempty then id else filter (not . mixedAmountLooksZero . pamount)) summaryps summarypes = map (, dateSpanAsPeriod spn) $ (if showempty then id else filter (not . mixedAmountLooksZero . pamount)) summaryps
anames = nubSort $ map paccount ps anames = nubSort $ map paccount ps
-- aggregate balances by account, like ledgerFromJournal, then do depth-clipping -- aggregate balances by account, like ledgerFromJournal, then do depth-clipping
accts = accountsFromPostings ps accts = accountsFromPostings ps
balance a = maybe nullmixedamt bal $ lookupAccount a accts balance a = maybe nullmixedamt bal $ lookupAccount a accts
where where
bal = if isclipped a then aibalance else aebalance bal = if isclipped a then aibalance else aebalance
isclipped a = maybe False (accountNameLevel a >=) mdepth isclipped a' = maybe False (accountNameLevel a' >=) mdepth
negatePostingAmount :: Posting -> Posting negatePostingAmount :: Posting -> Posting
negatePostingAmount = postingTransformAmount negate negatePostingAmount = postingTransformAmount negate

View File

@ -292,8 +292,8 @@ defreportspec = ReportSpec
-- | Set the default ConversionOp. -- | Set the default ConversionOp.
setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec
setDefaultConversionOp def rspec@ReportSpec{_rsReportOpts=ropts} = setDefaultConversionOp defop rspec@ReportSpec{_rsReportOpts=ropts} =
rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just def}} rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just defop}}
accountlistmodeopt :: RawOpts -> AccountListMode accountlistmodeopt :: RawOpts -> AccountListMode
accountlistmodeopt = accountlistmodeopt =
@ -360,7 +360,7 @@ layoutopt rawopts = fromMaybe (LayoutWide Nothing) $ layout <|> column
(s,n) = break (==',') $ map toLower opt (s,n) = break (==',') $ map toLower opt
w = case drop 1 n of w = case drop 1 n of
"" -> Nothing "" -> Nothing
c | Just w <- readMay c -> Just w c | Just w' <- readMay c -> Just w'
_ -> usageError "width in --layout=wide,WIDTH must be an integer" _ -> usageError "width in --layout=wide,WIDTH must be an integer"
err = usageError "--layout's argument should be \"wide[,WIDTH]\", \"tall\", \"bare\", or \"tidy\"" err = usageError "--layout's argument should be \"wide[,WIDTH]\", \"tall\", \"bare\", or \"tidy\""
@ -390,14 +390,14 @@ periodFromRawOpts d rawopts =
beginDatesFromRawOpts :: Day -> RawOpts -> [Day] beginDatesFromRawOpts :: Day -> RawOpts -> [Day]
beginDatesFromRawOpts d = collectopts (begindatefromrawopt d) beginDatesFromRawOpts d = collectopts (begindatefromrawopt d)
where where
begindatefromrawopt d (n,v) begindatefromrawopt d' (n,v)
| n == "begin" = | n == "begin" =
either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $ either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
fixSmartDateStrEither' d (T.pack v) fixSmartDateStrEither' d' (T.pack v)
| n == "period" = | n == "period" =
case case
either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $ either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
parsePeriodExpr d (stripquotes $ T.pack v) parsePeriodExpr d' (stripquotes $ T.pack v)
of of
(_, DateSpan (Just b) _) -> Just b (_, DateSpan (Just b) _) -> Just b
_ -> Nothing _ -> Nothing
@ -408,14 +408,14 @@ beginDatesFromRawOpts d = collectopts (begindatefromrawopt d)
endDatesFromRawOpts :: Day -> RawOpts -> [Day] endDatesFromRawOpts :: Day -> RawOpts -> [Day]
endDatesFromRawOpts d = collectopts (enddatefromrawopt d) endDatesFromRawOpts d = collectopts (enddatefromrawopt d)
where where
enddatefromrawopt d (n,v) enddatefromrawopt d' (n,v)
| n == "end" = | n == "end" =
either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $ either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
fixSmartDateStrEither' d (T.pack v) fixSmartDateStrEither' d' (T.pack v)
| n == "period" = | n == "period" =
case case
either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $ either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
parsePeriodExpr d (stripquotes $ T.pack v) parsePeriodExpr d' (stripquotes $ T.pack v)
of of
(_, DateSpan _ (Just e)) -> Just e (_, DateSpan _ (Just e)) -> Just e
_ -> Nothing _ -> Nothing
@ -589,12 +589,12 @@ journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceo
CalcGain -> journalMapPostings (\p -> postingTransformAmount (gain p) p) j CalcGain -> journalMapPostings (\p -> postingTransformAmount (gain p) p) j
_ -> journalMapPostings (\p -> postingTransformAmount (valuation p) p) $ costing j _ -> journalMapPostings (\p -> postingTransformAmount (valuation p) p) $ costing j
where where
valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts) valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)
gain p = maybe id (mixedAmountApplyGain priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts) gain p = maybe id (mixedAmountApplyGain priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)
costing = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts) costing = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts)
-- Find the end of the period containing this posting -- Find the end of the period containing this posting
periodEnd = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts) postingperiodend = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts)
mPeriodEnd = case interval_ ropts of mPeriodEnd = case interval_ ropts of
NoInterval -> const . spanEnd . fst $ reportSpan j rspec NoInterval -> const . spanEnd . fst $ reportSpan j rspec
_ -> spanEnd <=< latestSpanContaining (historical : spans) _ -> spanEnd <=< latestSpanContaining (historical : spans)
@ -611,11 +611,11 @@ mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle =
case valuationAfterSum ropts of case valuationAfterSum ropts of
Just mc -> case balancecalc_ ropts of Just mc -> case balancecalc_ ropts of
CalcGain -> gain mc CalcGain -> gain mc
_ -> \span -> valuation mc span . costing _ -> \spn -> valuation mc spn . costing
Nothing -> const id Nothing -> const id
where where
valuation mc span = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span) valuation mc spn = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)
gain mc span = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span) gain mc spn = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)
costing = case fromMaybe NoConversionOp $ conversionop_ ropts of costing = case fromMaybe NoConversionOp $ conversionop_ ropts of
NoConversionOp -> id NoConversionOp -> id
ToCost -> styleMixedAmount styles . mixedAmountCost ToCost -> styleMixedAmount styles . mixedAmountCost
@ -809,6 +809,8 @@ class HasReportOptsNoUpdate a => HasReportOpts a where
reportOpts = reportOptsNoUpdate reportOpts = reportOptsNoUpdate
{-# INLINE reportOpts #-} {-# INLINE reportOpts #-}
-- XXX these names are a bit clashy
period :: ReportableLens' a Period period :: ReportableLens' a Period
period = reportOpts.periodNoUpdate period = reportOpts.periodNoUpdate
{-# INLINE period #-} {-# INLINE period #-}

View File

@ -183,7 +183,7 @@ readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably
where where
openFileOrStdin :: String -> IOMode -> IO Handle openFileOrStdin :: String -> IOMode -> IO Handle
openFileOrStdin "-" _ = return stdin openFileOrStdin "-" _ = return stdin
openFileOrStdin f m = openFile f m openFileOrStdin f' m = openFile f' m
readHandlePortably :: Handle -> IO Text readHandlePortably :: Handle -> IO Text
readHandlePortably h = do readHandlePortably h = do
@ -225,9 +225,9 @@ sequence' ms = do
return (h []) return (h [])
where where
go h [] = return h go h [] = return h
go h (m:ms) = do go h (m:ms') = do
x <- m x <- m
go (h . (x :)) ms go (h . (x :)) ms'
-- | Like mapM but uses sequence'. -- | Like mapM but uses sequence'.
{-# INLINABLE mapM' #-} {-# INLINABLE mapM' #-}
@ -339,7 +339,7 @@ makeHledgerClassyLenses x = flip makeLensesWith x $ classyRules
-- HasReportOpts class with some special behaviour. We therefore give the -- HasReportOpts class with some special behaviour. We therefore give the
-- basic lenses a special NoUpdate name to avoid conflicts. -- basic lenses a special NoUpdate name to avoid conflicts.
className "ReportOpts" = Just (mkName "HasReportOptsNoUpdate", mkName "reportOptsNoUpdate") className "ReportOpts" = Just (mkName "HasReportOptsNoUpdate", mkName "reportOptsNoUpdate")
className (x:xs) = Just (mkName ("Has" ++ x:xs), mkName (toLower x : xs)) className (x':xs) = Just (mkName ("Has" ++ x':xs), mkName (toLower x' : xs))
className [] = Nothing className [] = Nothing
-- Fields of ReportOpts which need to update the Query when they are updated. -- Fields of ReportOpts which need to update the Query when they are updated.

View File

@ -99,11 +99,11 @@ instance Show Regexp where
RegexpCI _ _ -> showString "RegexpCI " RegexpCI _ _ -> showString "RegexpCI "
instance Read Regexp where instance Read Regexp where
readsPrec d r = readParen (d > app_prec) (\r -> [(toRegexCI' m,t) | readsPrec d r = readParen (d > app_prec) (\r' -> [(toRegexCI' m,t) |
("RegexCI",s) <- lex r, ("RegexCI",s) <- lex r',
(m,t) <- readsPrec (app_prec+1) s]) r (m,t) <- readsPrec (app_prec+1) s]) r
++ readParen (d > app_prec) (\r -> [(toRegex' m, t) | ++ readParen (d > app_prec) (\r' -> [(toRegex' m, t) |
("Regex",s) <- lex r, ("Regex",s) <- lex r',
(m,t) <- readsPrec (app_prec+1) s]) r (m,t) <- readsPrec (app_prec+1) s]) r
where app_prec = 10 where app_prec = 10
@ -186,7 +186,7 @@ regexReplace re repl = memo $ regexReplaceUnmemo re repl
-- but there can still be a runtime error from the replacement -- but there can still be a runtime error from the replacement
-- pattern, eg a backreference referring to a nonexistent match group.) -- pattern, eg a backreference referring to a nonexistent match group.)
regexReplaceUnmemo :: Regexp -> Replacement -> String -> Either RegexError String regexReplaceUnmemo :: Regexp -> Replacement -> String -> Either RegexError String
regexReplaceUnmemo re repl s = foldM (replaceMatch repl) s (reverse $ match (reCompiled re) s :: [MatchText String]) regexReplaceUnmemo re repl str = foldM (replaceMatch repl) str (reverse $ match (reCompiled re) str :: [MatchText String])
where where
-- Replace one match within the string with the replacement text -- Replace one match within the string with the replacement text
-- appropriate for this match. Or return an error message. -- appropriate for this match. Or return an error message.
@ -195,22 +195,22 @@ regexReplaceUnmemo re repl s = foldM (replaceMatch repl) s (reverse $ match (reC
case elems matchgroups of case elems matchgroups of
[] -> Right s [] -> Right s
((_,(off,len)):_) -> -- groups should have 0-based indexes, and there should always be at least one, since this is a match ((_,(off,len)):_) -> -- groups should have 0-based indexes, and there should always be at least one, since this is a match
erepl >>= \repl -> Right $ pre ++ repl ++ post erpl >>= \rpl -> Right $ pre ++ rpl ++ post
where where
(pre, post') = splitAt off s (pre, post') = splitAt off s
post = drop len post' post = drop len post'
-- The replacement text: the replacement pattern with all -- The replacement text: the replacement pattern with all
-- numeric backreferences replaced by the appropriate groups -- numeric backreferences replaced by the appropriate groups
-- from this match. Or an error message. -- from this match. Or an error message.
erepl = regexReplaceAllByM backrefRegex (lookupMatchGroup matchgroups) replpat erpl = regexReplaceAllByM backrefRegex (lookupMatchGroup matchgroups) replpat
where where
-- Given some match groups and a numeric backreference, -- Given some match groups and a numeric backreference,
-- return the referenced group text, or an error message. -- return the referenced group text, or an error message.
lookupMatchGroup :: MatchText String -> String -> Either RegexError String lookupMatchGroup :: MatchText String -> String -> Either RegexError String
lookupMatchGroup grps ('\\':s@(_:_)) | all isDigit s = lookupMatchGroup grps ('\\':s2@(_:_)) | all isDigit s2 =
case read s of n | n `elem` indices grps -> Right $ fst (grps ! n) -- PARTIAL: should not fail, all digits case read s2 of n | n `elem` indices grps -> Right $ fst (grps ! n) -- PARTIAL: should not fail, all digits
_ -> Left $ "no match group exists for backreference \"\\"++s++"\"" _ -> Left $ "no match group exists for backreference \"\\"++s++"\""
lookupMatchGroup _ s = Left $ "lookupMatchGroup called on non-numeric-backreference \""++s++"\", shouldn't happen" lookupMatchGroup _ s2 = Left $ "lookupMatchGroup called on non-numeric-backreference \""++s2++"\", shouldn't happen"
backrefRegex = toRegex' "\\\\[0-9]+" -- PARTIAL: should not fail backrefRegex = toRegex' "\\\\[0-9]+" -- PARTIAL: should not fail
-- regexReplace' :: Regexp -> Replacement -> String -> String -- regexReplace' :: Regexp -> Replacement -> String -> String
@ -249,8 +249,8 @@ regexReplaceAllBy re transform s = prependdone rest
go :: (Int,String,String->String) -> (Int,Int) -> (Int,String,String->String) go :: (Int,String,String->String) -> (Int,Int) -> (Int,String,String->String)
go (pos,todo,prepend) (off,len) = go (pos,todo,prepend) (off,len) =
let (prematch, matchandrest) = splitAt (off - pos) todo let (prematch, matchandrest) = splitAt (off - pos) todo
(matched, rest) = splitAt len matchandrest (matched, rest2) = splitAt len matchandrest
in (off + len, rest, prepend . (prematch++) . (transform matched ++)) in (off + len, rest2, prepend . (prematch++) . (transform matched ++))
-- Replace all occurrences of a regexp in a string, transforming each match -- Replace all occurrences of a regexp in a string, transforming each match
-- with the given monadic function. Eg if the monad is Either, a Left result -- with the given monadic function. Eg if the monad is Either, a Left result

View File

@ -371,9 +371,9 @@ attachSource filePath sourceText finalParseError = case finalParseError of
-- A parse error thrown directly with the 'FinalError' constructor -- A parse error thrown directly with the 'FinalError' constructor
-- requires both source and filepath. -- requires both source and filepath.
FinalError parseError -> FinalError err ->
let bundle = ParseErrorBundle let bundle = ParseErrorBundle
{ bundleErrors = parseError NE.:| [] { bundleErrors = err NE.:| []
, bundlePosState = initialPosState filePath sourceText } , bundlePosState = initialPosState filePath sourceText }
in FinalParseErrorBundle' in FinalParseErrorBundle'
{ finalErrorBundle = bundle { finalErrorBundle = bundle

View File

@ -211,11 +211,11 @@ renderHLine _ _ _ _ _ NoLine = []
renderHLine vpos borders pretty w h prop = [renderHLine' vpos borders pretty prop w h] renderHLine vpos borders pretty w h prop = [renderHLine' vpos borders pretty prop w h]
renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> Builder renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> Builder
renderHLine' vpos borders pretty prop is h = addBorders $ sep <> coreLine <> sep renderHLine' vpos borders pretty prop is hdr = addBorders $ sep <> coreLine <> sep
where where
addBorders xs = if borders then edge HL <> xs <> edge HR else xs addBorders xs = if borders then edge HL <> xs <> edge HR else xs
edge hpos = boxchar vpos hpos SingleLine prop pretty edge hpos = boxchar vpos hpos SingleLine prop pretty
coreLine = foldMap helper $ flattenHeader $ zipHeader 0 is h coreLine = foldMap helper $ flattenHeader $ zipHeader 0 is hdr
helper = either vsep dashes helper = either vsep dashes
dashes (i,_) = stimesMonoid i sep dashes (i,_) = stimesMonoid i sep
sep = boxchar vpos HM NoLine prop pretty sep = boxchar vpos HM NoLine prop pretty

View File

@ -83,7 +83,6 @@ ghc-options:
- -Wall - -Wall
- -Wno-incomplete-uni-patterns - -Wno-incomplete-uni-patterns
- -Wno-missing-signatures - -Wno-missing-signatures
- -Wno-name-shadowing
- -Wno-orphans - -Wno-orphans
- -Wno-type-defaults - -Wno-type-defaults
- -Wno-unused-do-bind - -Wno-unused-do-bind

View File

@ -210,7 +210,7 @@ updateReportPeriod updatePeriod = fromRight err . overEither period updatePeriod
-- | Apply a new filter query, or return the failing query. -- | Apply a new filter query, or return the failing query.
setFilter :: String -> UIState -> Either String UIState setFilter :: String -> UIState -> Either String UIState
setFilter s = first (const s) . setEither querystring (words'' prefixes $ T.pack s) setFilter s = first (const s) . setEither querystring (words'' queryprefixes $ T.pack s)
-- | Reset some filters & toggles. -- | Reset some filters & toggles.
resetFilter :: UIState -> UIState resetFilter :: UIState -> UIState

View File

@ -118,14 +118,14 @@ removeDates =
map quoteIfSpaced . map quoteIfSpaced .
filter (\term -> filter (\term ->
not $ T.isPrefixOf "date:" term || T.isPrefixOf "date2:" term) . not $ T.isPrefixOf "date:" term || T.isPrefixOf "date2:" term) .
Query.words'' Query.prefixes Query.words'' queryprefixes
removeInacct :: Text -> [Text] removeInacct :: Text -> [Text]
removeInacct = removeInacct =
map quoteIfSpaced . map quoteIfSpaced .
filter (\term -> filter (\term ->
not $ T.isPrefixOf "inacct:" term || T.isPrefixOf "inacctonly:" term) . not $ T.isPrefixOf "inacct:" term || T.isPrefixOf "inacctonly:" term) .
Query.words'' Query.prefixes Query.words'' queryprefixes
replaceInacct :: Text -> Text -> Text replaceInacct :: Text -> Text -> Text
replaceInacct q acct = T.unwords $ acct : removeInacct q replaceInacct q acct = T.unwords $ acct : removeInacct q

View File

@ -323,7 +323,7 @@ defCommandMode names = defMode {
-- given name, providing hledger's common input/reporting/help flags. -- given name, providing hledger's common input/reporting/help flags.
-- Just used when invoking addons. -- Just used when invoking addons.
addonCommandMode :: Name -> Mode RawOpts addonCommandMode :: Name -> Mode RawOpts
addonCommandMode name = (defCommandMode [name]) { addonCommandMode nam = (defCommandMode [nam]) {
modeHelp = "" modeHelp = ""
-- XXX not needed ? -- XXX not needed ?
-- fromMaybe "" $ lookup (stripAddonExtension name) [ -- fromMaybe "" $ lookup (stripAddonExtension name) [
@ -539,10 +539,10 @@ rawOptsToCliOpts rawopts = do
-- add a space character to preserve them. -- add a space character to preserve them.
-- --
getHledgerCliOpts' :: Mode RawOpts -> [String] -> IO CliOpts getHledgerCliOpts' :: Mode RawOpts -> [String] -> IO CliOpts
getHledgerCliOpts' mode' args' = do getHledgerCliOpts' mode' args0 = do
let rawopts = either usageError id $ process mode' args' let rawopts = either usageError id $ process mode' args0
opts <- rawOptsToCliOpts rawopts opts <- rawOptsToCliOpts rawopts
debugArgs args' opts debugArgs args0 opts
when ("help" `inRawOpts` rawopts_ opts) $ putStr shorthelp >> exitSuccess when ("help" `inRawOpts` rawopts_ opts) $ putStr shorthelp >> exitSuccess
-- when ("help" `inRawOpts` rawopts_ opts) $ putStr longhelp >> exitSuccess -- when ("help" `inRawOpts` rawopts_ opts) $ putStr longhelp >> exitSuccess
return opts return opts
@ -557,11 +557,11 @@ getHledgerCliOpts' mode' args' = do
] ]
-- | Print debug info about arguments and options if --debug is present. -- | Print debug info about arguments and options if --debug is present.
debugArgs :: [String] -> CliOpts -> IO () debugArgs :: [String] -> CliOpts -> IO ()
debugArgs args' opts = debugArgs args1 opts =
when ("--debug" `elem` args') $ do when ("--debug" `elem` args1) $ do
progname' <- getProgName progname' <- getProgName
putStrLn $ "running: " ++ progname' putStrLn $ "running: " ++ progname'
putStrLn $ "raw args: " ++ show args' putStrLn $ "raw args: " ++ show args1
putStrLn $ "processed opts:\n" ++ show opts putStrLn $ "processed opts:\n" ++ show opts
putStrLn $ "search query: " ++ show (_rsQuery $ reportspec_ opts) putStrLn $ "search query: " ++ show (_rsQuery $ reportspec_ opts)
@ -590,7 +590,7 @@ expandPathPreservingPrefix d prefixedf = do
let (p,f) = splitReaderPrefix prefixedf let (p,f) = splitReaderPrefix prefixedf
f' <- expandPath d f f' <- expandPath d f
return $ case p of return $ case p of
Just p -> p ++ ":" ++ f' Just p' -> p' ++ ":" ++ f'
Nothing -> f' Nothing -> f'
-- | Get the expanded, absolute output file path specified by an -- | Get the expanded, absolute output file path specified by an

View File

@ -51,25 +51,25 @@ accounts CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query,_rsRepo
-- 1. identify the accounts we'll show -- 1. identify the accounts we'll show
let tree = tree_ ropts let tree = tree_ ropts
declared = boolopt "declared" rawopts decl = boolopt "declared" rawopts
used = boolopt "used" rawopts used = boolopt "used" rawopts
types = boolopt "types" rawopts types = boolopt "types" rawopts
positions = boolopt "positions" rawopts positions = boolopt "positions" rawopts
directives = boolopt "directives" rawopts directives = boolopt "directives" rawopts
-- a depth limit will clip and exclude account names later, but we don't want to exclude accounts at this stage -- a depth limit will clip and exclude account names later, but we don't want to exclude accounts at this stage
nodepthq = dbg4 "nodepthq" $ filterQuery (not . queryIsDepth) query nodepthq = dbg4 "nodepthq" $ filterQuery (not . queryIsDepth) query
-- just the acct: part of the query will be reapplied later, after clipping -- just the acct: part of the query will be reapplied later, after clipping
acctq = dbg4 "acctq" $ filterQuery queryIsAcct query acctq = dbg4 "acctq" $ filterQuery queryIsAcct query
depth = dbg4 "depth" $ queryDepth $ filterQuery queryIsDepth query dep = dbg4 "depth" $ queryDepth $ filterQuery queryIsDepth query
matcheddeclaredaccts = matcheddeclaredaccts =
dbg4 "matcheddeclaredaccts" $ dbg4 "matcheddeclaredaccts" $
filter (matchesAccountExtra (journalAccountType j) (journalInheritedAccountTags j) nodepthq) filter (matchesAccountExtra (journalAccountType j) (journalInheritedAccountTags j) nodepthq)
$ map fst $ jdeclaredaccounts j $ map fst $ jdeclaredaccounts j
matchedusedaccts = dbg5 "matchedusedaccts" $ map paccount $ journalPostings $ filterJournalPostings nodepthq j matchedusedaccts = dbg5 "matchedusedaccts" $ map paccount $ journalPostings $ filterJournalPostings nodepthq j
accts = dbg5 "accts to show" $ accts = dbg5 "accts to show" $
if | declared && not used -> matcheddeclaredaccts if | decl && not used -> matcheddeclaredaccts
| not declared && used -> matchedusedaccts | not decl && used -> matchedusedaccts
| otherwise -> matcheddeclaredaccts ++ matchedusedaccts | otherwise -> matcheddeclaredaccts ++ matchedusedaccts
-- 2. sort them by declaration order (then undeclared accounts alphabetically) -- 2. sort them by declaration order (then undeclared accounts alphabetically)
-- within each group of siblings -- within each group of siblings
@ -78,10 +78,10 @@ accounts CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query,_rsRepo
-- 3. if there's a depth limit, depth-clip and remove any no longer useful items -- 3. if there's a depth limit, depth-clip and remove any no longer useful items
clippedaccts = clippedaccts =
dbg4 "clippedaccts" $ dbg4 "clippedaccts" $
filter (matchesAccount acctq) $ -- clipping can leave accounts that no longer match the query, remove such filter (matchesAccount acctq) $ -- clipping can leave accounts that no longer match the query, remove such
nub $ -- clipping can leave duplicates (adjacent, hopefully) nub $ -- clipping can leave duplicates (adjacent, hopefully)
filter (not . T.null) $ -- depth:0 can leave nulls filter (not . T.null) $ -- depth:0 can leave nulls
map (clipAccountName depth) $ -- clip at depth if specified map (clipAccountName dep) $ -- clip at depth if specified
sortedaccts sortedaccts
-- 4. print what remains as a list or tree, maybe applying --drop in the former case. -- 4. print what remains as a list or tree, maybe applying --drop in the former case.

View File

@ -189,7 +189,7 @@ confirmedTransactionWizard prevInput es@EntryState{..} stack@(currentStage : _)
Nothing -> Nothing ->
confirmedTransactionWizard prevInput es (drop 1 stack) confirmedTransactionWizard prevInput es (drop 1 stack)
EnterNewPosting txnParams@TxnParams{..} posting -> case (esPostings, posting) of EnterNewPosting txnParams@TxnParams{..} p -> case (esPostings, p) of
([], Nothing) -> ([], Nothing) ->
confirmedTransactionWizard prevInput es (EnterAccount txnParams : stack) confirmedTransactionWizard prevInput es (EnterAccount txnParams : stack)
(_, Just _) -> (_, Just _) ->
@ -230,15 +230,15 @@ confirmedTransactionWizard prevInput es@EntryState{..} stack@(currentStage : _)
confirmedTransactionWizard prevInput es{esPostings=init esPostings} (dropWhile notPrevAmountAndNotEnterDesc stack) confirmedTransactionWizard prevInput es{esPostings=init esPostings} (dropWhile notPrevAmountAndNotEnterDesc stack)
EnterAmountAndComment txnParams account -> amountAndCommentWizard prevInput es >>= \case EnterAmountAndComment txnParams account -> amountAndCommentWizard prevInput es >>= \case
Just (amount, comment) -> do Just (amt, comment) -> do
let posting = nullposting{paccount=T.pack $ stripbrackets account let p = nullposting{paccount=T.pack $ stripbrackets account
,pamount=mixedAmount amount ,pamount=mixedAmount amt
,pcomment=comment ,pcomment=comment
,ptype=accountNamePostingType $ T.pack account ,ptype=accountNamePostingType $ T.pack account
} }
amountAndCommentString = showAmount amount ++ T.unpack (if T.null comment then "" else " ;" <> comment) amountAndCommentString = showAmount amt ++ T.unpack (if T.null comment then "" else " ;" <> comment)
prevAmountAndCmnt' = replaceNthOrAppend (length esPostings) amountAndCommentString (prevAmountAndCmnt prevInput) prevAmountAndCmnt' = replaceNthOrAppend (length esPostings) amountAndCommentString (prevAmountAndCmnt prevInput)
es' = es{esPostings=esPostings++[posting], esArgs=drop 2 esArgs} es' = es{esPostings=esPostings++[p], esArgs=drop 2 esArgs}
confirmedTransactionWizard prevInput{prevAmountAndCmnt=prevAmountAndCmnt'} es' (EnterNewPosting txnParams (Just posting) : stack) confirmedTransactionWizard prevInput{prevAmountAndCmnt=prevAmountAndCmnt'} es' (EnterNewPosting txnParams (Just posting) : stack)
Nothing -> confirmedTransactionWizard prevInput es (drop 1 stack) Nothing -> confirmedTransactionWizard prevInput es (drop 1 stack)
@ -310,18 +310,18 @@ accountWizard PrevInput{..} EntryState{..} = do
where where
canfinish = not (null esPostings) && postingsBalanced esPostings canfinish = not (null esPostings) && postingsBalanced esPostings
parseAccountOrDotOrNull :: String -> Bool -> String -> Maybe (Maybe String) parseAccountOrDotOrNull :: String -> Bool -> String -> Maybe (Maybe String)
parseAccountOrDotOrNull _ _ "<" = dbg1 $ Just Nothing parseAccountOrDotOrNull _ _ "<" = dbg' $ Just Nothing
parseAccountOrDotOrNull _ _ "." = dbg1 $ Just $ Just "." -- . always signals end of txn parseAccountOrDotOrNull _ _ "." = dbg' $ Just $ Just "." -- . always signals end of txn
parseAccountOrDotOrNull "" True "" = dbg1 $ Just $ Just "" -- when there's no default and txn is balanced, "" also signals end of txn parseAccountOrDotOrNull "" True "" = dbg' $ Just $ Just "" -- when there's no default and txn is balanced, "" also signals end of txn
parseAccountOrDotOrNull def@(_:_) _ "" = dbg1 $ Just $ Just def -- when there's a default, "" means use that parseAccountOrDotOrNull def@(_:_) _ "" = dbg' $ Just $ Just def -- when there's a default, "" means use that
parseAccountOrDotOrNull _ _ s = dbg1 $ fmap (Just . T.unpack) $ parseAccountOrDotOrNull _ _ s = dbg' $ fmap (Just . T.unpack) $
either (const Nothing) validateAccount $ either (const Nothing) validateAccount $
flip evalState esJournal $ runParserT (accountnamep <* eof) "" (T.pack s) -- otherwise, try to parse the input as an accountname flip evalState esJournal $ runParserT (accountnamep <* eof) "" (T.pack s) -- otherwise, try to parse the input as an accountname
where where
validateAccount :: Text -> Maybe Text validateAccount :: Text -> Maybe Text
validateAccount t | no_new_accounts_ esOpts && notElem t (journalAccountNamesDeclaredOrImplied esJournal) = Nothing validateAccount t | no_new_accounts_ esOpts && notElem t (journalAccountNamesDeclaredOrImplied esJournal) = Nothing
| otherwise = Just t | otherwise = Just t
dbg1 = id -- strace dbg' = id -- strace
amountAndCommentWizard PrevInput{..} EntryState{..} = do amountAndCommentWizard PrevInput{..} EntryState{..} = do
let pnum = length esPostings + 1 let pnum = length esPostings + 1

View File

@ -31,7 +31,7 @@ import Hledger
import Hledger.Read.CsvReader (CSV, CsvRecord, printCSV) import Hledger.Read.CsvReader (CSV, CsvRecord, printCSV)
import Hledger.Cli.CliOptions import Hledger.Cli.CliOptions
import Hledger.Cli.Utils import Hledger.Cli.Utils
import Text.Tabular.AsciiWide import Text.Tabular.AsciiWide hiding (render)
aregistermode = hledgerCommandMode aregistermode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Aregister.txt") $(embedFileRelative "Hledger/Cli/Commands/Aregister.txt")
@ -71,7 +71,7 @@ aregister :: CliOpts -> Journal -> IO ()
aregister opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do aregister opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do
-- the first argument specifies the account, any remaining arguments are a filter query -- the first argument specifies the account, any remaining arguments are a filter query
let help = "aregister needs an ACCTPAT argument to select an account" let help = "aregister needs an ACCTPAT argument to select an account"
(apat,querystring) <- case listofstringopt "args" rawopts of (apat,querystr) <- case listofstringopt "args" rawopts of
[] -> error' $ help <> ".\nPlease provide an account name or a (case-insensitive, infix, regexp) pattern." [] -> error' $ help <> ".\nPlease provide an account name or a (case-insensitive, infix, regexp) pattern."
(a:as) -> return (a, map T.pack as) (a:as) -> return (a, map T.pack as)
let let
@ -88,7 +88,7 @@ aregister opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do
depth_=Nothing depth_=Nothing
-- always show historical balance -- always show historical balance
, balanceaccum_= Historical , balanceaccum_= Historical
, querystring_ = querystring , querystring_ = querystr
} }
wd = whichDate ropts' wd = whichDate ropts'
-- and regenerate the ReportSpec, making sure to use the above -- and regenerate the ReportSpec, making sure to use the above
@ -184,8 +184,8 @@ accountTransactionsReportItemAsText
] ]
spacerCell = Cell BottomLeft [WideBuilder (TB.singleton ' ') 1] spacerCell = Cell BottomLeft [WideBuilder (TB.singleton ' ') 1]
spacerCell2 = Cell BottomLeft [WideBuilder (TB.fromString " ") 2] spacerCell2 = Cell BottomLeft [WideBuilder (TB.fromString " ") 2]
pad fullwidth amt = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt pad fullwidth amt1 = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt1
where w = fullwidth - wbWidth amt where w = fullwidth - wbWidth amt1
-- calculate widths -- calculate widths
(totalwidth,mdescwidth) = registerWidthsFromOpts copts (totalwidth,mdescwidth) = registerWidthsFromOpts copts
(datewidth, date) = (10, showDate $ transactionRegisterDate wd reportq thisacctq t) (datewidth, date) = (10, showDate $ transactionRegisterDate wd reportq thisacctq t)

View File

@ -267,7 +267,7 @@ import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import Data.Time (addDays, fromGregorian) import Data.Time (addDays, fromGregorian)
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import Lucid as L import Lucid as L hiding (value_)
import Safe (headMay, maximumMay) import Safe (headMay, maximumMay)
import Text.Tabular.AsciiWide import Text.Tabular.AsciiWide
(Align(..), Cell(..), Table(..), TableOpts(..), cellWidth, concatTables, (Align(..), Cell(..), Table(..), TableOpts(..), cellWidth, concatTables,
@ -340,8 +340,8 @@ balancemode = hledgerCommandMode
balance :: CliOpts -> Journal -> IO () balance :: CliOpts -> Journal -> IO ()
balance opts@CliOpts{reportspec_=rspec} j = case balancecalc_ of balance opts@CliOpts{reportspec_=rspec} j = case balancecalc_ of
CalcBudget -> do -- single or multi period budget report CalcBudget -> do -- single or multi period budget report
let reportspan = fst $ reportSpan j rspec let rspan = fst $ reportSpan j rspec
budgetreport = budgetReport rspec (balancingopts_ $ inputopts_ opts) reportspan j budgetreport = budgetReport rspec (balancingopts_ $ inputopts_ opts) rspan j
render = case fmt of render = case fmt of
"txt" -> budgetReportAsText ropts "txt" -> budgetReportAsText ropts
"json" -> (<>"\n") . toJsonText "json" -> (<>"\n") . toJsonText
@ -362,8 +362,8 @@ balance opts@CliOpts{reportspec_=rspec} j = case balancecalc_ of
_ -> do -- single period simple balance report _ -> do -- single period simple balance report
let report = balanceReport rspec j -- simple Ledger-style balance report let report = balanceReport rspec j -- simple Ledger-style balance report
render = case fmt of render = case fmt of
"txt" -> \ropts -> TB.toLazyText . balanceReportAsText ropts "txt" -> \ropts1 -> TB.toLazyText . balanceReportAsText ropts1
"csv" -> \ropts -> printCSV . balanceReportAsCsv ropts "csv" -> \ropts1 -> printCSV . balanceReportAsCsv ropts1
-- "html" -> \ropts -> (<>"\n") . L.renderText . multiBalanceReportAsHtml ropts . balanceReportAsMultiBalanceReport ropts -- "html" -> \ropts -> (<>"\n") . L.renderText . multiBalanceReportAsHtml ropts . balanceReportAsMultiBalanceReport ropts
"json" -> const $ (<>"\n") . toJsonText "json" -> const $ (<>"\n") . toJsonText
_ -> error' $ unsupportedOutputFormatError fmt -- PARTIAL: _ -> error' $ unsupportedOutputFormatError fmt -- PARTIAL:
@ -430,9 +430,9 @@ balanceReportAsText :: ReportOpts -> BalanceReport -> TB.Builder
balanceReportAsText opts ((items, total)) = case layout_ opts of balanceReportAsText opts ((items, total)) = case layout_ opts of
LayoutBare | iscustom -> error' "Custom format not supported with commodity columns" -- PARTIAL: LayoutBare | iscustom -> error' "Custom format not supported with commodity columns" -- PARTIAL:
LayoutBare -> balanceReportAsText' opts ((items, total)) LayoutBare -> balanceReportAsText' opts ((items, total))
_ -> unlinesB lines <> unlinesB (if no_total_ opts then [] else [overline, totalLines]) _ -> unlinesB ls <> unlinesB (if no_total_ opts then [] else [overline, totalLines])
where where
(lines, sizes) = unzip $ map (balanceReportItemAsText opts) items (ls, sizes) = unzip $ map (balanceReportItemAsText opts) items
-- abuse renderBalanceReportItem to render the total with similar format -- abuse renderBalanceReportItem to render the total with similar format
(totalLines, _) = renderBalanceReportItem opts ("",0,total) (totalLines, _) = renderBalanceReportItem opts ("",0,total)
-- with a custom format, extend the line to the full report width; -- with a custom format, extend the line to the full report width;
@ -449,20 +449,20 @@ balanceReportAsText opts ((items, total)) = case layout_ opts of
balanceReportAsText' :: ReportOpts -> BalanceReport -> TB.Builder balanceReportAsText' :: ReportOpts -> BalanceReport -> TB.Builder
balanceReportAsText' opts ((items, total)) = balanceReportAsText' opts ((items, total)) =
unlinesB . fmap (renderColumns def{tableBorders=False} sizes . Tab.Group Tab.NoLine . fmap Tab.Header) $ unlinesB . fmap (renderColumns def{tableBorders=False} sizes . Tab.Group Tab.NoLine . fmap Tab.Header) $
lines ++ concat [[[overline], totalline] | not (no_total_ opts)] ls ++ concat [[[overline], totalline] | not (no_total_ opts)]
where where
render (_, acctname, depth, amt) = render (_, acctname, dep, amt) =
[ Cell TopRight damts [ Cell TopRight damts
, Cell TopLeft (fmap wbFromText cs) , Cell TopLeft (fmap wbFromText cs)
, Cell TopLeft (replicate (length damts - 1) mempty ++ [wbFromText dispname]) ] , Cell TopLeft (replicate (length damts - 1) mempty ++ [wbFromText dispname]) ]
where dopts = oneLine{displayColour=color_ opts, displayOrder=Just cs} where dopts = oneLine{displayColour=color_ opts, displayOrder=Just cs}
cs = if mixedAmountLooksZero amt then [""] else S.toList $ maCommodities amt cs = if mixedAmountLooksZero amt then [""] else S.toList $ maCommodities amt
dispname = T.replicate ((depth - 1) * 2) " " <> acctname dispname = T.replicate ((dep - 1) * 2) " " <> acctname
damts = showMixedAmountLinesB dopts amt damts = showMixedAmountLinesB dopts amt
lines = fmap render items ls = fmap render items
totalline = render ("", "", 0, total) totalline = render ("", "", 0, total)
sizes = fromMaybe 0 . maximumMay . map cellWidth <$> sizes = fromMaybe 0 . maximumMay . map cellWidth <$>
transpose ([totalline | not (no_total_ opts)] ++ lines) transpose ([totalline | not (no_total_ opts)] ++ ls)
overline = Cell TopLeft . pure . wbFromText . flip T.replicate "-" . fromMaybe 0 $ headMay sizes overline = Cell TopLeft . pure . wbFromText . flip T.replicate "-" . fromMaybe 0 $ headMay sizes
{- {-
@ -481,12 +481,12 @@ This implementation turned out to be a bit convoluted but implements the followi
-- differently-priced quantities of the same commodity will appear merged. -- differently-priced quantities of the same commodity will appear merged.
-- The output will be one or more lines depending on the format and number of commodities. -- The output will be one or more lines depending on the format and number of commodities.
balanceReportItemAsText :: ReportOpts -> BalanceReportItem -> (TB.Builder, [Int]) balanceReportItemAsText :: ReportOpts -> BalanceReportItem -> (TB.Builder, [Int])
balanceReportItemAsText opts (_, accountName, depth, amt) = balanceReportItemAsText opts (_, accountName, dep, amt) =
renderBalanceReportItem opts (accountName, depth, amt) renderBalanceReportItem opts (accountName, dep, amt)
-- | Render a balance report item using the given StringFormat, generating one or more lines of text. -- | Render a balance report item using the given StringFormat, generating one or more lines of text.
renderBalanceReportItem :: ReportOpts -> (AccountName, Int, MixedAmount) -> (TB.Builder, [Int]) renderBalanceReportItem :: ReportOpts -> (AccountName, Int, MixedAmount) -> (TB.Builder, [Int])
renderBalanceReportItem opts (acctname, depth, total) = renderBalanceReportItem opts (acctname, dep, total) =
case format_ opts of case format_ opts of
OneLine comps -> renderRow' $ render True True comps OneLine comps -> renderRow' $ render True True comps
TopAligned comps -> renderRow' $ render True False comps TopAligned comps -> renderRow' $ render True False comps
@ -496,14 +496,14 @@ renderBalanceReportItem opts (acctname, depth, total) =
. Tab.Group Tab.NoLine $ map Tab.Header is . Tab.Group Tab.NoLine $ map Tab.Header is
, map cellWidth is ) , map cellWidth is )
render topaligned oneline = map (renderComponent topaligned oneline opts (acctname, depth, total)) render topaligned oneline = map (renderComponent topaligned oneline opts (acctname, dep, total))
-- | Render one StringFormat component for a balance report item. -- | Render one StringFormat component for a balance report item.
renderComponent :: Bool -> Bool -> ReportOpts -> (AccountName, Int, MixedAmount) -> StringFormatComponent -> Cell renderComponent :: Bool -> Bool -> ReportOpts -> (AccountName, Int, MixedAmount) -> StringFormatComponent -> Cell
renderComponent _ _ _ _ (FormatLiteral s) = textCell TopLeft s renderComponent _ _ _ _ (FormatLiteral s) = textCell TopLeft s
renderComponent topaligned oneline opts (acctname, depth, total) (FormatField ljust mmin mmax field) = case field of renderComponent topaligned oneline opts (acctname, dep, total) (FormatField ljust mmin mmax field) = case field of
DepthSpacerField -> Cell align [WideBuilder (TB.fromText $ T.replicate d " ") d] DepthSpacerField -> Cell align [WideBuilder (TB.fromText $ T.replicate d " ") d]
where d = maybe id min mmax $ depth * fromMaybe 1 mmin where d = maybe id min mmax $ dep * fromMaybe 1 mmin
AccountField -> textCell align $ formatText ljust mmin mmax acctname AccountField -> textCell align $ formatText ljust mmin mmax acctname
TotalField -> Cell align . pure $ showMixedAmountB dopts total TotalField -> Cell align . pure $ showMixedAmountB dopts total
_ -> Cell align [mempty] _ -> Cell align [mempty]
@ -721,13 +721,13 @@ multiBalanceRowAsWbs bopts ReportOpts{..} colspans (PeriodicReportRow _ as rowto
totalscolumn = row_total_ && balanceaccum_ `notElem` [Cumulative, Historical] totalscolumn = row_total_ && balanceaccum_ `notElem` [Cumulative, Historical]
cs = if all mixedAmountLooksZero allamts then [""] else S.toList $ foldMap maCommodities allamts cs = if all mixedAmountLooksZero allamts then [""] else S.toList $ foldMap maCommodities allamts
allamts = as ++ [rowtot | totalscolumn && not (null as)] ++ [rowavg | average_ && not (null as)] allamts = as ++ [rowtot | totalscolumn && not (null as)] ++ [rowavg | average_ && not (null as)]
addDateColumns span@(DateSpan s e) = (wbFromText (showDateSpan span) :) addDateColumns spn@(DateSpan s e) = (wbFromText (showDateSpan spn) :)
. (wbFromText (maybe "" showDate s) :) . (wbFromText (maybe "" showDate s) :)
. (wbFromText (maybe "" (showDate . addDays (-1)) e) :) . (wbFromText (maybe "" (showDate . addDays (-1)) e) :)
paddedTranspose :: a -> [[a]] -> [[a]] paddedTranspose :: a -> [[a]] -> [[a]]
paddedTranspose _ [] = [[]] paddedTranspose _ [] = [[]]
paddedTranspose n as = take (maximum . map length $ as) . trans $ as paddedTranspose n as1 = take (maximum . map length $ as1) . trans $ as1
where where
trans ([] : xss) = (n : map h xss) : trans ([n] : map t xss) trans ([] : xss) = (n : map h xss) : trans ([n] : map t xss)
trans ((x : xs) : xss) = (x : map h xss) : trans (m xs : map t xss) trans ((x : xs) : xss) = (x : map h xss) : trans (m xs : map t xss)

View File

@ -96,10 +96,10 @@ parseCheckArgument s =
-- | Run the named error check, possibly with some arguments, -- | Run the named error check, possibly with some arguments,
-- on this journal with these options. -- on this journal with these options.
runCheck :: CliOpts -> Journal -> (Check,[String]) -> IO () runCheck :: CliOpts -> Journal -> (Check,[String]) -> IO ()
runCheck CliOpts{reportspec_=ReportSpec{_rsReportOpts=ropts}} j (check,_) = do runCheck CliOpts{reportspec_=ReportSpec{_rsReportOpts=ropts}} j (chck,_) = do
d <- getCurrentDay d <- getCurrentDay
let let
results = case check of results = case chck of
Accounts -> journalCheckAccounts j Accounts -> journalCheckAccounts j
Commodities -> journalCheckCommodities j Commodities -> journalCheckCommodities j
Ordereddates -> journalCheckOrdereddates (whichDate ropts) j Ordereddates -> journalCheckOrdereddates (whichDate ropts) j

View File

@ -135,11 +135,11 @@ close copts@CliOpts{rawopts_=rawopts, reportspec_=rspec'} j = do
| -- get the balances for each commodity and transaction price | -- get the balances for each commodity and transaction price
(a,mb) <- acctbals (a,mb) <- acctbals
, let bs = amounts mb , let bs0 = amounts mb
-- mark the last balance in each commodity with True -- mark the last balance in each commodity with True
, let bs' = concat [reverse $ zip (reverse bs) (True : repeat False) , let bs2 = concat [reverse $ zip (reverse bs1) (True : repeat False)
| bs <- groupBy ((==) `on` acommodity) bs] | bs1 <- groupBy ((==) `on` acommodity) bs0]
, (b, islast) <- bs' , (b, islast) <- bs2
] ]
-- or a final multicommodity posting transferring all balances to equity -- or a final multicommodity posting transferring all balances to equity
@ -160,12 +160,12 @@ close copts@CliOpts{rawopts_=rawopts, reportspec_=rspec'} j = do
: [posting{paccount=openingacct, pamount=mixedAmount . precise $ negate b} | interleaved] : [posting{paccount=openingacct, pamount=mixedAmount . precise $ negate b} | interleaved]
| (a,mb) <- acctbals | (a,mb) <- acctbals
, let bs = amounts mb , let bs0 = amounts mb
-- mark the last balance in each commodity with the unpriced sum in that commodity (for a balance assertion) -- mark the last balance in each commodity with the unpriced sum in that commodity (for a balance assertion)
, let bs' = concat [reverse $ zip (reverse bs) (Just commoditysum : repeat Nothing) , let bs2 = concat [reverse $ zip (reverse bs1) (Just commoditysum : repeat Nothing)
| bs <- groupBy ((==) `on` acommodity) bs | bs1 <- groupBy ((==) `on` acommodity) bs0
, let commoditysum = (sum bs)] , let commoditysum = (sum bs1)]
, (b, mcommoditysum) <- bs' , (b, mcommoditysum) <- bs2
] ]
++ [posting{paccount=openingacct, pamount=if explicit then mixedAmountSetFullPrecision (maNegate totalamt) else missingmixedamt} | not interleaved] ++ [posting{paccount=openingacct, pamount=if explicit then mixedAmountSetFullPrecision (maNegate totalamt) else missingmixedamt} | not interleaved]

View File

@ -34,6 +34,6 @@ codesmode = hledgerCommandMode
codes :: CliOpts -> Journal -> IO () codes :: CliOpts -> Journal -> IO ()
codes CliOpts{reportspec_=rspec} j = do codes CliOpts{reportspec_=rspec} j = do
let ts = entriesReport rspec j let ts = entriesReport rspec j
codes = (if empty_ (_rsReportOpts rspec) then id else filter (not . T.null)) $ codes' = (if empty_ (_rsReportOpts rspec) then id else filter (not . T.null)) $
map tcode ts map tcode ts
mapM_ T.putStrLn codes mapM_ T.putStrLn codes'

View File

@ -33,6 +33,6 @@ descriptionsmode = hledgerCommandMode
descriptions :: CliOpts -> Journal -> IO () descriptions :: CliOpts -> Journal -> IO ()
descriptions CliOpts{reportspec_=rspec} j = do descriptions CliOpts{reportspec_=rspec} j = do
let ts = entriesReport rspec j let ts = entriesReport rspec j
descriptions = nubSort $ map tdescription ts descs = nubSort $ map tdescription ts
mapM_ T.putStrLn descriptions mapM_ T.putStrLn descs

View File

@ -31,7 +31,7 @@ files :: CliOpts -> Journal -> IO ()
files CliOpts{rawopts_=rawopts} j = do files CliOpts{rawopts_=rawopts} j = do
let args = listofstringopt "args" rawopts let args = listofstringopt "args" rawopts
regex <- mapM (either fail pure . toRegex . T.pack) $ headMay args regex <- mapM (either fail pure . toRegex . T.pack) $ headMay args
let files = maybe id (filter . regexMatch) regex let fs = maybe id (filter . regexMatch) regex
$ map fst $ map fst
$ jfiles j $ jfiles j
mapM_ putStrLn files mapM_ putStrLn fs

View File

@ -34,5 +34,5 @@ notesmode = hledgerCommandMode
notes :: CliOpts -> Journal -> IO () notes :: CliOpts -> Journal -> IO ()
notes CliOpts{reportspec_=rspec} j = do notes CliOpts{reportspec_=rspec} j = do
let ts = entriesReport rspec j let ts = entriesReport rspec j
notes = nubSort $ map transactionNote ts notes' = nubSort $ map transactionNote ts
mapM_ T.putStrLn notes mapM_ T.putStrLn notes'

View File

@ -36,13 +36,13 @@ payeesmode = hledgerCommandMode
payees :: CliOpts -> Journal -> IO () payees :: CliOpts -> Journal -> IO ()
payees CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query}} j = do payees CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query}} j = do
let let
declared = boolopt "declared" rawopts decl = boolopt "declared" rawopts
used = boolopt "used" rawopts used = boolopt "used" rawopts
-- XXX matchesPayee is currently an alias for matchesDescription, not sure if it matters -- XXX matchesPayee is currently an alias for matchesDescription, not sure if it matters
matcheddeclaredpayees = S.fromList . filter (matchesPayeeWIP query) $ journalPayeesDeclared j matcheddeclaredpayees = S.fromList . filter (matchesPayeeWIP query) $ journalPayeesDeclared j
matchedusedpayees = S.fromList . map transactionPayee $ filter (matchesTransaction query) $ jtxns j matchedusedpayees = S.fromList . map transactionPayee $ filter (matchesTransaction query) $ jtxns j
payees = payees' =
if | declared && not used -> matcheddeclaredpayees if | decl && not used -> matcheddeclaredpayees
| not declared && used -> matchedusedpayees | not decl && used -> matchedusedpayees
| otherwise -> matcheddeclaredpayees <> matchedusedpayees | otherwise -> matcheddeclaredpayees <> matchedusedpayees
mapM_ T.putStrLn payees mapM_ T.putStrLn payees'

View File

@ -168,13 +168,13 @@ entriesReportAsCsv txns =
-- The txnidx field (transaction index) allows postings to be grouped back into transactions. -- The txnidx field (transaction index) allows postings to be grouped back into transactions.
transactionToCSV :: Transaction -> CSV transactionToCSV :: Transaction -> CSV
transactionToCSV t = transactionToCSV t =
map (\p -> T.pack (show idx):date:date2:status:code:description:comment:p) map (\p -> T.pack (show idx):d:d2:status:code:description:comment:p)
(concatMap postingToCSV $ tpostings t) (concatMap postingToCSV $ tpostings t)
where where
idx = tindex t idx = tindex t
description = tdescription t description = tdescription t
date = showDate (tdate t) d = showDate (tdate t)
date2 = maybe "" showDate $ tdate2 t d2 = maybe "" showDate $ tdate2 t
status = T.pack . show $ tstatus t status = T.pack . show $ tstatus t
code = tcode t code = tcode t
comment = T.strip $ tcomment t comment = T.strip $ tcomment t
@ -186,10 +186,10 @@ postingToCSV p =
-- separators and prices -- separators and prices
let a_ = amountStripPrices a{acommodity=""} in let a_ = amountStripPrices a{acommodity=""} in
let showamt = wbToText . showAmountB csvDisplay in let showamt = wbToText . showAmountB csvDisplay in
let amount = showamt a_ in let amt = showamt a_ in
let credit = if q < 0 then showamt $ negate a_ else "" in let credit = if q < 0 then showamt $ negate a_ else "" in
let debit = if q >= 0 then showamt a_ else "" in let debit = if q >= 0 then showamt a_ else "" in
[account, amount, c, credit, debit, status, comment]) [account, amt, c, credit, debit, status, comment])
. amounts $ pamount p . amounts $ pamount p
where where
status = T.pack . show $ pstatus p status = T.pack . show $ pstatus p

View File

@ -25,11 +25,11 @@ import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import System.Console.CmdArgs.Explicit (flagNone, flagReq) import System.Console.CmdArgs.Explicit (flagNone, flagReq)
import Hledger import Hledger hiding (per)
import Hledger.Read.CsvReader (CSV, CsvRecord, printCSV) import Hledger.Read.CsvReader (CSV, CsvRecord, printCSV)
import Hledger.Cli.CliOptions import Hledger.Cli.CliOptions
import Hledger.Cli.Utils import Hledger.Cli.Utils
import Text.Tabular.AsciiWide import Text.Tabular.AsciiWide hiding (render)
registermode = hledgerCommandMode registermode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Register.txt") $(embedFileRelative "Hledger/Cli/Commands/Register.txt")
@ -144,14 +144,14 @@ postingsReportItemAsText opts preferredamtwidth preferredbalwidth ((mdate, mperi
] ]
spacerCell = Cell BottomLeft [WideBuilder (TB.singleton ' ') 1] spacerCell = Cell BottomLeft [WideBuilder (TB.singleton ' ') 1]
spacerCell2 = Cell BottomLeft [WideBuilder (TB.fromString " ") 2] spacerCell2 = Cell BottomLeft [WideBuilder (TB.fromString " ") 2]
pad fullwidth amt = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt pad fullwidth amt' = WideBuilder (TB.fromText $ T.replicate w " ") w <> amt'
where w = fullwidth - wbWidth amt where w = fullwidth - wbWidth amt'
-- calculate widths -- calculate widths
(totalwidth,mdescwidth) = registerWidthsFromOpts opts (totalwidth,mdescwidth) = registerWidthsFromOpts opts
datewidth = maybe 10 periodTextWidth mperiod datewidth = maybe 10 periodTextWidth mperiod
date = case mperiod of date = case mperiod of
Just period -> if isJust mdate then showPeriod period else "" Just per -> if isJust mdate then showPeriod per else ""
Nothing -> maybe "" showDate mdate Nothing -> maybe "" showDate mdate
(amtwidth, balwidth) (amtwidth, balwidth)
| shortfall <= 0 = (preferredamtwidth, preferredbalwidth) | shortfall <= 0 = (preferredamtwidth, preferredbalwidth)
| otherwise = (adjustedamtwidth, adjustedbalwidth) | otherwise = (adjustedamtwidth, adjustedbalwidth)

View File

@ -20,7 +20,7 @@ import Hledger.Cli.CliOptions
import Hledger.Cli.Commands.Print import Hledger.Cli.Commands.Print
import System.Console.CmdArgs.Explicit import System.Console.CmdArgs.Explicit
import Text.Printf import Text.Printf
import Text.Megaparsec import Text.Megaparsec hiding (pos1)
import qualified Data.Algorithm.Diff as D import qualified Data.Algorithm.Diff as D
rewritemode = hledgerCommandMode rewritemode = hledgerCommandMode
@ -101,11 +101,11 @@ renderPatch = go Nothing . sortOn fst where
go _ [] = "" go _ [] = ""
go Nothing cs@((SourcePos fp _ _, _):_) = fileHeader fp <> go (Just (fp, 0)) cs go Nothing cs@((SourcePos fp _ _, _):_) = fileHeader fp <> go (Just (fp, 0)) cs
go (Just (fp, _)) cs@((SourcePos fp' _ _, _):_) | fp /= fp' = go Nothing cs go (Just (fp, _)) cs@((SourcePos fp' _ _, _):_) | fp /= fp' = go Nothing cs
go (Just (fp, offs)) ((SourcePos _ lineno _, diffs):cs) = chunkHeader <> chunk <> go (Just (fp, offs + adds - dels)) cs go (Just (fp, offs)) ((SourcePos _ lineno _, diffs):cs) = chunkHeader <> chnk <> go (Just (fp, offs + adds - dels)) cs
where where
chunkHeader = T.pack $ printf "@@ -%d,%d +%d,%d @@\n" (unPos lineno) dels (unPos lineno+offs) adds chunkHeader = T.pack $ printf "@@ -%d,%d +%d,%d @@\n" (unPos lineno) dels (unPos lineno+offs) adds
(dels, adds) = foldl' countDiff (0, 0) diffs (dels, adds) = foldl' countDiff (0, 0) diffs
chunk = foldMap renderLine diffs chnk = foldMap renderLine diffs
fileHeader fp = "--- " <> T.pack fp <> "\n+++ " <> T.pack fp <> "\n" fileHeader fp = "--- " <> T.pack fp <> "\n+++ " <> T.pack fp <> "\n"
countDiff (dels, adds) = \case countDiff (dels, adds) = \case

View File

@ -92,45 +92,45 @@ roi CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsReportOpts=ReportO
let priceDirectiveDates = dbg3 "priceDirectiveDates" $ map pddate $ jpricedirectives j let priceDirectiveDates = dbg3 "priceDirectiveDates" $ map pddate $ jpricedirectives j
tableBody <- forM spans $ \span@(DateSpan (Just spanBegin) (Just spanEnd)) -> do tableBody <- forM spans $ \spn@(DateSpan (Just begin) (Just end)) -> do
-- Spans are [spanBegin,spanEnd), and spanEnd is 1 day after then actual end date we are interested in -- Spans are [begin,end), and end is 1 day after the actual end date we are interested in
let let
cashFlowApplyCostValue = map (\(d,amt) -> (d,mixedAmountValue spanEnd d amt)) cashFlowApplyCostValue = map (\(d,amt) -> (d,mixedAmountValue end d amt))
valueBefore = valueBefore =
mixedAmountValue spanEnd spanBegin $ mixedAmountValue end begin $
total trans (And [ investmentsQuery total trans (And [ investmentsQuery
, Date (DateSpan Nothing (Just spanBegin))]) , Date (DateSpan Nothing (Just begin))])
valueAfter = valueAfter =
mixedAmountValue spanEnd spanEnd $ mixedAmountValue end end $
total trans (And [investmentsQuery total trans (And [investmentsQuery
, Date (DateSpan Nothing (Just spanEnd))]) , Date (DateSpan Nothing (Just end))])
priceDates = dbg3 "priceDates" $ nub $ filter (spanContainsDate span) priceDirectiveDates priceDates = dbg3 "priceDates" $ nub $ filter (spanContainsDate spn) priceDirectiveDates
cashFlow = cashFlow =
((map (,nullmixedamt) priceDates)++) $ ((map (,nullmixedamt) priceDates)++) $
cashFlowApplyCostValue $ cashFlowApplyCostValue $
calculateCashFlow wd trans (And [ Not investmentsQuery calculateCashFlow wd trans (And [ Not investmentsQuery
, Not pnlQuery , Not pnlQuery
, Date span ] ) , Date spn ] )
pnl = pnl =
cashFlowApplyCostValue $ cashFlowApplyCostValue $
calculateCashFlow wd trans (And [ Not investmentsQuery calculateCashFlow wd trans (And [ Not investmentsQuery
, pnlQuery , pnlQuery
, Date span ] ) , Date spn ] )
thisSpan = dbg3 "processing span" $ thisSpan = dbg3 "processing span" $
OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow pnl OneSpan begin end valueBefore valueAfter cashFlow pnl
irr <- internalRateOfReturn showCashFlow prettyTables thisSpan irr <- internalRateOfReturn showCashFlow prettyTables thisSpan
twr <- timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountValue thisSpan twr <- timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountValue thisSpan
let cashFlowAmt = maNegate . maSum $ map snd cashFlow let cashFlowAmt = maNegate . maSum $ map snd cashFlow
let smallIsZero x = if abs x < 0.01 then 0.0 else x let smallIsZero x = if abs x < 0.01 then 0.0 else x
return [ showDate spanBegin return [ showDate begin
, showDate (addDays (-1) spanEnd) , showDate (addDays (-1) end)
, T.pack $ showMixedAmount valueBefore , T.pack $ showMixedAmount valueBefore
, T.pack $ showMixedAmount cashFlowAmt , T.pack $ showMixedAmount cashFlowAmt
, T.pack $ showMixedAmount valueAfter , T.pack $ showMixedAmount valueAfter
@ -148,7 +148,7 @@ roi CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsReportOpts=ReportO
TL.putStrLn $ Tab.render prettyTables id id id table TL.putStrLn $ Tab.render prettyTables id id id table
timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountValue (OneSpan spanBegin spanEnd valueBeforeAmt valueAfter cashFlow pnl) = do timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountValue (OneSpan begin end valueBeforeAmt valueAfter cashFlow pnl) = do
let valueBefore = unMix valueBeforeAmt let valueBefore = unMix valueBeforeAmt
let initialUnitPrice = 100 :: Decimal let initialUnitPrice = 100 :: Decimal
let initialUnits = valueBefore / initialUnitPrice let initialUnits = valueBefore / initialUnitPrice
@ -169,17 +169,17 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountV
$ sort $ sort
$ datedCashflows ++ datedPnls $ datedCashflows ++ datedPnls
where where
zeroUnitsNeedsCashflowAtTheFront changes = zeroUnitsNeedsCashflowAtTheFront changes1 =
if initialUnits > 0 then changes if initialUnits > 0 then changes1
else else
let (leadingEmptyCashFlows, rest) = span isEmptyCashflow changes let (leadingEmptyCashFlows, rest) = span isEmptyCashflow changes1
(leadingPnls, rest') = span (isLeft . snd) rest (leadingPnls, rest') = span (isLeft . snd) rest
(firstCashflow, rest'') = splitAt 1 rest' (firstCashflow, rest'') = splitAt 1 rest'
in leadingEmptyCashFlows ++ firstCashflow ++ leadingPnls ++ rest'' in leadingEmptyCashFlows ++ firstCashflow ++ leadingPnls ++ rest''
isEmptyCashflow (_date, amt) = case amt of isEmptyCashflow (_date, amt) = case amt of
Right amt -> mixedAmountIsZero amt Right amt' -> mixedAmountIsZero amt'
Left _ -> False Left _ -> False
datedPnls = map (second Left) $ aggregateByDate pnl datedPnls = map (second Left) $ aggregateByDate pnl
@ -198,16 +198,16 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountV
tail $ tail $
scanl scanl
(\(_, _, unitPrice, unitBalance) (date, amt) -> (\(_, _, unitPrice, unitBalance) (date, amt) ->
let valueOnDate = unMix $ mixedAmountValue spanEnd date $ total trans (And [investmentsQuery, Date (DateSpan Nothing (Just date))]) let valueOnDate = unMix $ mixedAmountValue end date $ total trans (And [investmentsQuery, Date (DateSpan Nothing (Just date))])
in in
case amt of case amt of
Right amt -> Right amt' ->
-- we are buying or selling -- we are buying or selling
let unitsBoughtOrSold = unMix amt / unitPrice let unitsBoughtOrSold = unMix amt' / unitPrice
in (valueOnDate, unitsBoughtOrSold, unitPrice, unitBalance + unitsBoughtOrSold) in (valueOnDate, unitsBoughtOrSold, unitPrice, unitBalance + unitsBoughtOrSold)
Left pnl -> Left pnl' ->
-- PnL change -- PnL change
let valueAfterDate = valueOnDate + unMix pnl let valueAfterDate = valueOnDate + unMix pnl'
unitPrice' = valueAfterDate/unitBalance unitPrice' = valueAfterDate/unitBalance
in (valueOnDate, 0, unitPrice', unitBalance)) in (valueOnDate, 0, unitPrice', unitBalance))
(0, 0, initialUnitPrice, initialUnits) (0, 0, initialUnitPrice, initialUnits)
@ -220,17 +220,17 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountV
else (unMix valueAfter) / finalUnitBalance else (unMix valueAfter) / finalUnitBalance
-- Technically, totalTWR should be (100*(finalUnitPrice - initialUnitPrice) / initialUnitPrice), but initalUnitPrice is 100, so 100/100 == 1 -- Technically, totalTWR should be (100*(finalUnitPrice - initialUnitPrice) / initialUnitPrice), but initalUnitPrice is 100, so 100/100 == 1
totalTWR = roundTo 2 $ (finalUnitPrice - initialUnitPrice) totalTWR = roundTo 2 $ (finalUnitPrice - initialUnitPrice)
years = fromIntegral (diffDays spanEnd spanBegin) / 365 :: Double years = fromIntegral (diffDays end begin) / 365 :: Double
annualizedTWR = 100*((1+(realToFrac totalTWR/100))**(1/years)-1) :: Double annualizedTWR = 100*((1+(realToFrac totalTWR/100))**(1/years)-1) :: Double
when showCashFlow $ do when showCashFlow $ do
printf "\nTWR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd)) printf "\nTWR cash flow for %s - %s\n" (showDate begin) (showDate (addDays (-1) end))
let (dates', amounts) = unzip changes let (dates', amts) = unzip changes
cashflows' = map (fromRight nullmixedamt) amounts cashflows' = map (fromRight nullmixedamt) amts
pnls = map (fromLeft nullmixedamt) amounts pnls = map (fromLeft nullmixedamt) amts
(valuesOnDate,unitsBoughtOrSold', unitPrices', unitBalances') = unzip4 units (valuesOnDate,unitsBoughtOrSold', unitPrices', unitBalances') = unzip4 units
add x lst = if valueBefore/=0 then x:lst else lst add x lst = if valueBefore/=0 then x:lst else lst
dates = add spanBegin dates' dates = add begin dates'
cashflows = add valueBeforeAmt cashflows' cashflows = add valueBeforeAmt cashflows'
unitsBoughtOrSold = add initialUnits unitsBoughtOrSold' unitsBoughtOrSold = add initialUnits unitsBoughtOrSold'
unitPrices = add initialUnitPrice unitPrices' unitPrices = add initialUnitPrice unitPrices'
@ -242,11 +242,11 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountV
(Tab.Group DoubleLine [ Tab.Group Tab.SingleLine [Tab.Header "Portfolio value", Tab.Header "Unit balance"] (Tab.Group DoubleLine [ Tab.Group Tab.SingleLine [Tab.Header "Portfolio value", Tab.Header "Unit balance"]
, Tab.Group Tab.SingleLine [Tab.Header "Pnl", Tab.Header "Cashflow", Tab.Header "Unit price", Tab.Header "Units"] , Tab.Group Tab.SingleLine [Tab.Header "Pnl", Tab.Header "Cashflow", Tab.Header "Unit price", Tab.Header "Units"]
, Tab.Group Tab.SingleLine [Tab.Header "New Unit Balance"]]) , Tab.Group Tab.SingleLine [Tab.Header "New Unit Balance"]])
[ [value, oldBalance, pnl, cashflow, prc, udelta, balance] [ [val, oldBalance, pnl', cashflow, prc, udelta, balance]
| value <- map showDecimal valuesOnDate | val <- map showDecimal valuesOnDate
| oldBalance <- map showDecimal (0:unitBalances) | oldBalance <- map showDecimal (0:unitBalances)
| balance <- map showDecimal unitBalances | balance <- map showDecimal unitBalances
| pnl <- map showMixedAmount pnls | pnl' <- map showMixedAmount pnls
| cashflow <- map showMixedAmount cashflows | cashflow <- map showMixedAmount cashflows
| prc <- map showDecimal unitPrices | prc <- map showDecimal unitPrices
| udelta <- map showDecimal unitsBoughtOrSold ]) | udelta <- map showDecimal unitsBoughtOrSold ])
@ -256,28 +256,28 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountV
return annualizedTWR return annualizedTWR
internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow _pnl) = do internalRateOfReturn showCashFlow prettyTables (OneSpan begin end valueBefore valueAfter cashFlow _pnl) = do
let prefix = (spanBegin, maNegate valueBefore) let prefix = (begin, maNegate valueBefore)
postfix = (spanEnd, valueAfter) postfix = (end, valueAfter)
totalCF = filter (maIsNonZero . snd) $ prefix : (sortOn fst cashFlow) ++ [postfix] totalCF = filter (maIsNonZero . snd) $ prefix : (sortOn fst cashFlow) ++ [postfix]
when showCashFlow $ do when showCashFlow $ do
printf "\nIRR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd)) printf "\nIRR cash flow for %s - %s\n" (showDate begin) (showDate (addDays (-1) end))
let (dates, amounts) = unzip totalCF let (dates, amts) = unzip totalCF
TL.putStrLn $ Tab.render prettyTables id id id TL.putStrLn $ Tab.render prettyTables id id id
(Table (Table
(Tab.Group Tab.NoLine (map (Header . showDate) dates)) (Tab.Group Tab.NoLine (map (Header . showDate) dates))
(Tab.Group Tab.SingleLine [Header "Amount"]) (Tab.Group Tab.SingleLine [Header "Amount"])
(map ((:[]) . T.pack . showMixedAmount) amounts)) (map ((:[]) . T.pack . showMixedAmount) amts))
-- 0% is always a solution, so require at least something here -- 0% is always a solution, so require at least something here
case totalCF of case totalCF of
[] -> return 0 [] -> return 0
_ -> case ridders (RiddersParam 100 (AbsTol 0.00001)) _ -> case ridders (RiddersParam 100 (AbsTol 0.00001))
(0.000000000001,10000) (0.000000000001,10000)
(interestSum spanEnd totalCF) of (interestSum end totalCF) of
Root rate -> return ((rate-1)*100) Root rate -> return ((rate-1)*100)
NotBracketed -> error' $ "Error (NotBracketed): No solution for Internal Rate of Return (IRR).\n" NotBracketed -> error' $ "Error (NotBracketed): No solution for Internal Rate of Return (IRR).\n"
++ " Possible causes: IRR is huge (>1000000%), balance of investment becomes negative at some point in time." ++ " Possible causes: IRR is huge (>1000000%), balance of investment becomes negative at some point in time."
@ -301,7 +301,7 @@ total trans query = sumPostings . filter (matchesPosting query) $ concatMap real
unMix :: MixedAmount -> Quantity unMix :: MixedAmount -> Quantity
unMix a = unMix a =
case (unifyMixedAmount $ mixedAmountCost a) of case (unifyMixedAmount $ mixedAmountCost a) of
Just a -> aquantity a Just a' -> aquantity a'
Nothing -> error' $ "Amounts could not be converted to a single cost basis: " ++ show (map showAmount $ amounts a) ++ Nothing -> error' $ "Amounts could not be converted to a single cost basis: " ++ show (map showAmount $ amounts a) ++
"\nConsider using --value to force all costs to be in a single commodity." ++ "\nConsider using --value to force all costs to be in a single commodity." ++
"\nFor example, \"--cost --value=end,<commodity> --infer-market-prices\", where commodity is the one that was used to pay for the investment." "\nFor example, \"--cost --value=end,<commodity> --infer-market-prices\", where commodity is the one that was used to pay for the investment."

View File

@ -60,17 +60,17 @@ stats opts@CliOpts{reportspec_=rspec, progstarttime_} j = do
(realToFrac dt :: Float) (fromIntegral numtxns / realToFrac dt :: Float) (realToFrac dt :: Float) (fromIntegral numtxns / realToFrac dt :: Float)
showLedgerStats :: Ledger -> Day -> DateSpan -> (TB.Builder, Int) showLedgerStats :: Ledger -> Day -> DateSpan -> (TB.Builder, Int)
showLedgerStats l today span = showLedgerStats l today spn =
(unlinesB $ map (renderRowB def{tableBorders=False, borderSpaces=False} . showRow) stats (unlinesB $ map (renderRowB def{tableBorders=False, borderSpaces=False} . showRow) stts
,tnum) ,tnum)
where where
showRow (label, value) = Group NoLine $ map (Header . textCell TopLeft) showRow (label, val) = Group NoLine $ map (Header . textCell TopLeft)
[fitText (Just w1) (Just w1) False True label `T.append` ": ", T.pack value] [fitText (Just w1) (Just w1) False True label `T.append` ": ", T.pack val]
w1 = maximum $ map (T.length . fst) stats w1 = maximum $ map (T.length . fst) stts
(stats, tnum) = ([ (stts, tnum) = ([
("Main file", path) -- ++ " (from " ++ source ++ ")") ("Main file", path) -- ++ " (from " ++ source ++ ")")
,("Included files", unlines $ drop 1 $ journalFilePaths j) ,("Included files", unlines $ drop 1 $ journalFilePaths j)
,("Transactions span", printf "%s to %s (%d days)" (start span) (end span) days) ,("Transactions span", printf "%s to %s (%d days)" (start spn) (end spn) days)
,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed) ,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed)
,("Transactions", printf "%d (%0.1f per day)" tnum txnrate) ,("Transactions", printf "%d (%0.1f per day)" tnum txnrate)
,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30) ,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30)
@ -84,29 +84,29 @@ showLedgerStats l today span =
-- Days since reconciliation : %(reconcileelapsed)s -- Days since reconciliation : %(reconcileelapsed)s
-- Days since last transaction : %(recentelapsed)s -- Days since last transaction : %(recentelapsed)s
] ]
,tnum) ,tnum1)
where where
j = ljournal l j = ljournal l
path = journalFilePath j path = journalFilePath j
ts = sortOn tdate $ filter (spanContainsDate span . tdate) $ jtxns j ts = sortOn tdate $ filter (spanContainsDate spn . tdate) $ jtxns j
as = nub $ map paccount $ concatMap tpostings ts as = nub $ map paccount $ concatMap tpostings ts
cs = either error' Map.keys $ commodityStylesFromAmounts $ concatMap (amountsRaw . pamount) $ concatMap tpostings ts -- PARTIAL: cs = either error' Map.keys $ commodityStylesFromAmounts $ concatMap (amountsRaw . pamount) $ concatMap tpostings ts -- PARTIAL:
lastdate | null ts = Nothing lastdate | null ts = Nothing
| otherwise = Just $ tdate $ last ts | otherwise = Just $ tdate $ last ts
lastelapsed = fmap (diffDays today) lastdate lastelapsed = fmap (diffDays today) lastdate
showelapsed Nothing = "" showelapsed Nothing = ""
showelapsed (Just days) = printf " (%d %s)" days' direction showelapsed (Just dys) = printf " (%d %s)" dys' direction
where days' = abs days where dys' = abs dys
direction | days >= 0 = "days ago" :: String direction | dys >= 0 = "days ago" :: String
| otherwise = "days from now" | otherwise = "days from now"
tnum = length ts -- Integer would be better tnum1 = length ts -- Integer would be better
start (DateSpan (Just d) _) = show d start (DateSpan (Just d) _) = show d
start _ = "" start _ = ""
end (DateSpan _ (Just d)) = show d end (DateSpan _ (Just d)) = show d
end _ = "" end _ = ""
days = fromMaybe 0 $ daysInSpan span days = fromMaybe 0 $ daysInSpan spn
txnrate | days==0 = 0 txnrate | days==0 = 0
| otherwise = fromIntegral tnum / fromIntegral days :: Double | otherwise = fromIntegral tnum1 / fromIntegral days :: Double
tnum30 = length $ filter withinlast30 ts tnum30 = length $ filter withinlast30 ts
withinlast30 t = d >= addDays (-30) today && (d<=today) where d = tdate t withinlast30 t = d >= addDays (-30) today && (d<=today) where d = tdate t
txnrate30 = fromIntegral tnum30 / 30 :: Double txnrate30 = fromIntegral tnum30 / 30 :: Double

View File

@ -22,7 +22,7 @@ import Data.Time.Calendar (Day, addDays)
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import Hledger.Read.CsvReader (CSV, printCSV) import Hledger.Read.CsvReader (CSV, printCSV)
import Lucid as L hiding (value_) import Lucid as L hiding (value_)
import Text.Tabular.AsciiWide as Tab import Text.Tabular.AsciiWide as Tab hiding (render)
import Hledger import Hledger
import Hledger.Cli.Commands.Balance import Hledger.Cli.Commands.Balance
@ -174,11 +174,11 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportspec_=r
-- render appropriately -- render appropriately
render = case outputFormatFromOpts opts of render = case outputFormatFromOpts opts of
"txt" -> compoundBalanceReportAsText ropts' "txt" -> compoundBalanceReportAsText ropts'
"csv" -> printCSV . compoundBalanceReportAsCsv ropts' "csv" -> printCSV . compoundBalanceReportAsCsv ropts'
"html" -> L.renderText . compoundBalanceReportAsHtml ropts' "html" -> L.renderText . compoundBalanceReportAsHtml ropts'
"json" -> toJsonText "json" -> toJsonText
x -> error' $ unsupportedOutputFormatError x x -> error' $ unsupportedOutputFormatError x
-- | Summarise one or more (inclusive) end dates, in a way that's -- | Summarise one or more (inclusive) end dates, in a way that's
-- visually different from showDateSpan, suggesting discrete end dates -- visually different from showDateSpan, suggesting discrete end dates
@ -232,12 +232,12 @@ compoundBalanceReportAsText ropts
-- | Convert a named multi balance report to a table suitable for -- | Convert a named multi balance report to a table suitable for
-- concatenating with others to make a compound balance report table. -- concatenating with others to make a compound balance report table.
subreportAsTable ropts (title, r, _) = t subreportAsTable ropts1 (title1, r, _) = t
where where
-- convert to table -- convert to table
Table lefthdrs tophdrs cells = balanceReportAsTable ropts r Table lefthdrs tophdrs cells = balanceReportAsTable ropts1 r
-- tweak the layout -- tweak the layout
t = Table (Tab.Group Tab.SingleLine [Tab.Header title, lefthdrs]) tophdrs ([]:cells) t = Table (Tab.Group Tab.SingleLine [Tab.Header title1, lefthdrs]) tophdrs ([]:cells)
-- | Render a compound balance report as CSV. -- | Render a compound balance report as CSV.
-- Subreports' CSV is concatenated, with the headings rows replaced by a -- Subreports' CSV is concatenated, with the headings rows replaced by a
@ -256,9 +256,9 @@ compoundBalanceReportAsCsv ropts (CompoundPeriodicReport title colspans subrepor
: concatMap (subreportAsCsv ropts) subreports : concatMap (subreportAsCsv ropts) subreports
where where
-- | Add a subreport title row and drop the heading row. -- | Add a subreport title row and drop the heading row.
subreportAsCsv ropts (subreporttitle, multibalreport, _) = subreportAsCsv ropts1 (subreporttitle, multibalreport, _) =
padRow subreporttitle : padRow subreporttitle :
tail (multiBalanceReportAsCsv ropts multibalreport) tail (multiBalanceReportAsCsv ropts1 multibalreport)
padRow s = take numcols $ s : repeat "" padRow s = take numcols $ s : repeat ""
where where
numcols numcols

View File

@ -97,7 +97,7 @@ mainmode addons = defMode {
-- | Let's go! -- | Let's go!
main :: IO () main :: IO ()
main = do main = do
progstarttime <- getPOSIXTime starttime <- getPOSIXTime
-- Choose and run the appropriate internal or external command based -- Choose and run the appropriate internal or external command based
-- on the raw command-line arguments, cmdarg's interpretation of -- on the raw command-line arguments, cmdarg's interpretation of
@ -132,7 +132,7 @@ main = do
-- parse arguments with cmdargs -- parse arguments with cmdargs
opts' <- argsToCliOpts args addons opts' <- argsToCliOpts args addons
let opts = opts'{progstarttime_=progstarttime} let opts = opts'{progstarttime_=starttime}
-- select an action and run it. -- select an action and run it.
let let
@ -143,13 +143,13 @@ main = do
hasVersion = ("--version" `elem`) hasVersion = ("--version" `elem`)
printUsage = putStr $ showModeUsage $ mainmode addons printUsage = putStr $ showModeUsage $ mainmode addons
badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure -- PARTIAL: badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure -- PARTIAL:
hasHelpFlag args = any (`elem` args) ["-h","--help"] hasHelpFlag args1 = any (`elem` args1) ["-h","--help"]
hasManFlag args = (`elem` args) "--man" hasManFlag args1 = (`elem` args1) "--man"
hasInfoFlag args = (`elem` args) "--info" hasInfoFlag args1 = (`elem` args1) "--info"
f `orShowHelp` mode f `orShowHelp` mode1
| hasHelpFlag args = putStr $ showModeUsage mode | hasHelpFlag args = putStr $ showModeUsage mode1
| hasInfoFlag args = runInfoForTopic "hledger" (headMay $ modeNames mode) | hasInfoFlag args = runInfoForTopic "hledger" (headMay $ modeNames mode1)
| hasManFlag args = runManForTopic "hledger" (headMay $ modeNames mode) | hasManFlag args = runManForTopic "hledger" (headMay $ modeNames mode1)
| otherwise = f | otherwise = f
-- where -- where
-- lastdocflag -- lastdocflag
@ -237,7 +237,7 @@ moveFlagsAfterCommand args = moveArgs $ ensureDebugHasArg args
(bs,["--debug"]) -> bs++["--debug=1"] (bs,["--debug"]) -> bs++["--debug=1"]
_ -> as _ -> as
moveArgs args = insertFlagsAfterCommand $ moveArgs' (args, []) moveArgs args1 = insertFlagsAfterCommand $ moveArgs' (args1, [])
where where
-- -f FILE ..., --alias ALIAS ... -- -f FILE ..., --alias ALIAS ...
moveArgs' ((f:v:a:as), flags) | isMovableReqArgFlag f, isValue v = moveArgs' (a:as, flags ++ [f,v]) moveArgs' ((f:v:a:as), flags) | isMovableReqArgFlag f, isValue v = moveArgs' (a:as, flags ++ [f,v])
@ -251,7 +251,7 @@ moveFlagsAfterCommand args = moveArgs $ ensureDebugHasArg args
moveArgs' (as, flags) = (as, flags) moveArgs' (as, flags) = (as, flags)
insertFlagsAfterCommand ([], flags) = flags insertFlagsAfterCommand ([], flags) = flags
insertFlagsAfterCommand (command:args, flags) = [command] ++ flags ++ args insertFlagsAfterCommand (command1:args2, flags) = [command1] ++ flags ++ args2
isMovableNoArgFlag a = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` optargflagstomove ++ noargflagstomove isMovableNoArgFlag a = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` optargflagstomove ++ noargflagstomove

View File

@ -181,16 +181,16 @@ maybeFileModificationTime f = do
-- | Attempt to open a web browser on the given url, all platforms. -- | Attempt to open a web browser on the given url, all platforms.
openBrowserOn :: String -> IO ExitCode openBrowserOn :: String -> IO ExitCode
openBrowserOn u = trybrowsers browsers u openBrowserOn = trybrowsers browsers
where where
trybrowsers (b:bs) u = do trybrowsers (b:bs) u1 = do
(e,_,_) <- readProcessWithExitCode b [u] "" (e,_,_) <- readProcessWithExitCode b [u1] ""
case e of case e of
ExitSuccess -> return ExitSuccess ExitSuccess -> return ExitSuccess
ExitFailure _ -> trybrowsers bs u ExitFailure _ -> trybrowsers bs u1
trybrowsers [] u = do trybrowsers [] u1 = do
putStrLn $ printf "Could not start a web browser (tried: %s)" $ intercalate ", " browsers putStrLn $ printf "Could not start a web browser (tried: %s)" $ intercalate ", " browsers
putStrLn $ printf "Please open your browser and visit %s" u putStrLn $ printf "Please open your browser and visit %s" u1
return $ ExitFailure 127 return $ ExitFailure 127
browsers | os=="darwin" = ["open"] browsers | os=="darwin" = ["open"]
| os=="mingw32" = ["c:/Program Files/Mozilla Firefox/firefox.exe"] | os=="mingw32" = ["c:/Program Files/Mozilla Firefox/firefox.exe"]
@ -270,11 +270,11 @@ postingsOrTransactionsReportAsText alignAll opts itemAsText itemamt itembal repo
minWidth = 12 minWidth = 12
chunkSize = 1000 chunkSize = 1000
renderItem (amtWidth, balWidth) item@(_, amt, bal) = ((amtWidth', balWidth'), itemBuilder) renderItem (amtWidth, balWidth) item@(_, amt1, bal1) = ((amtWidth', balWidth'), itemBuilder)
where where
itemBuilder = itemAsText amtWidth' balWidth' item itemBuilder = itemAsText amtWidth' balWidth' item
amtWidth' = if alignAll then amtWidth else maximumStrict $ amtWidth : map wbWidth amt amtWidth' = if alignAll then amtWidth else maximumStrict $ amtWidth : map wbWidth amt1
balWidth' = if alignAll then balWidth else maximumStrict $ balWidth : map wbWidth bal balWidth' = if alignAll then balWidth else maximumStrict $ balWidth : map wbWidth bal1
startWidth f = maximum $ minWidth : map wbWidth (concatMap f startAlign) startWidth f = maximum $ minWidth : map wbWidth (concatMap f startAlign)
where where

View File

@ -57,19 +57,19 @@ progname = "hledger"
-- so that must not be overridden by a log.date git config variable. -- so that must not be overridden by a log.date git config variable.
-- --
versionStringWith :: Either String GitInfo -> ProgramName -> PackageVersion -> VersionString versionStringWith :: Either String GitInfo -> ProgramName -> PackageVersion -> VersionString
versionStringWith egitinfo progname packageversion = versionStringWith egitinfo prognam packagever =
concat [ progname , " " , version , ", " , os' , "-" , arch ] concat [ prognam , " " , version , ", " , os' , "-" , arch ]
where where
os' | os == "darwin" = "mac" os' | os == "darwin" = "mac"
| os == "mingw32" = "windows" | os == "mingw32" = "windows"
| otherwise = os | otherwise = os
version = case egitinfo of version = case egitinfo of
Left _err -> packageversion Left _err -> packagever
Right gitinfo -> Right gitinfo ->
case words $ giCommitDate gitinfo of case words $ giCommitDate gitinfo of
-- git log's date format is normally --date=default ("similar to --date=rfc2822") -- git log's date format is normally --date=default ("similar to --date=rfc2822")
_weekday:mon:day:_localtime:year:_offset:_ -> _weekday:mon:day:_localtime:year:_offset:_ ->
intercalate "-" [packageversion , hash, date] intercalate "-" [packagever , hash, date]
where where
hash = 'g' : take 9 (giHash gitinfo) -- like git describe hash = 'g' : take 9 (giHash gitinfo) -- like git describe
date = concat [year,mm,dd] date = concat [year,mm,dd]

View File

@ -89,7 +89,6 @@ ghc-options:
- -Wall - -Wall
- -Wno-incomplete-uni-patterns - -Wno-incomplete-uni-patterns
- -Wno-missing-signatures - -Wno-missing-signatures
- -Wno-name-shadowing
- -Wno-orphans - -Wno-orphans
- -Wno-type-defaults - -Wno-type-defaults
- -Wno-unused-do-bind - -Wno-unused-do-bind