code: Strip extraneous trailing whitespace from Haskell sources
This commit is contained in:
		
							parent
							
								
									7e332fda20
								
							
						
					
					
						commit
						11d9e5eb6a
					
				
							
								
								
									
										6
									
								
								Shake.hs
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Shake.hs
									
									
									
									
									
								
							| @ -497,7 +497,7 @@ main = do | ||||
|       | pkg <- packages ] | ||||
| 
 | ||||
|     phony "commandhelp" $ need commandtxts | ||||
|      | ||||
| 
 | ||||
|     commandtxts |%> \out -> do | ||||
|       let src = out -<.> "md" | ||||
|       need [src] | ||||
| @ -695,7 +695,7 @@ main = do | ||||
|       -- tagrelease: \ | ||||
|       --   $(call def-help,tagrelease, commit a release tag based on $(VERSIONFILE) for each package ) | ||||
|       --   for p in $(PACKAGES); do git tag -f $$p-$(VERSION); done | ||||
|         | ||||
| 
 | ||||
|     -- MISC | ||||
| 
 | ||||
|     -- Generate the web manuals based on the current checkout and save | ||||
| @ -777,7 +777,7 @@ wikiLink :: Markdown -> Markdown | ||||
| wikiLink = | ||||
|   replaceBy wikilinkre         wikilinkReplace         . | ||||
|   replaceBy labelledwikilinkre labelledwikilinkReplace | ||||
|    | ||||
| 
 | ||||
| -- regex stuff | ||||
| 
 | ||||
| -- couldn't figure out how to use match subgroups, so we don't | ||||
|  | ||||
| @ -56,7 +56,7 @@ cmdmode = hledgerCommandMode | ||||
|   [here| chart | ||||
| Generate a pie chart for the top account balances with the same sign, | ||||
| in SVG format. | ||||
|   | ||||
| 
 | ||||
| Based on the old hledger-chart package, this is not yet useful. | ||||
| It's supposed to show only balances of one sign, but this might be broken. | ||||
|   |] | ||||
|  | ||||
| @ -45,7 +45,7 @@ hledger smooth revenues:consulting | hledger -f- incomestatement -W | ||||
| 
 | ||||
| FLAGS | ||||
|   |] | ||||
|   []  | ||||
|   [] | ||||
|   [generalflagsgroup1] | ||||
|   [] | ||||
|   ([], Just $ argsFlag "ACCT") | ||||
| @ -64,7 +64,7 @@ main = do | ||||
|       q = queryFromOpts today ropts | ||||
|       acct = T.pack $ headDef (error' "Please provide an account name argument") args | ||||
|       pr = postingsReport ropts (And [Acct $ accountNameToAccountRegex acct, q]) j | ||||
|        | ||||
| 
 | ||||
|       -- dates of postings to acct (in report) | ||||
|       pdates = map (postingDate . fourth5) (snd pr) | ||||
|       -- the specified report end date or today's date | ||||
|  | ||||
| @ -76,8 +76,8 @@ accountsFromPostings ps = | ||||
|   in | ||||
|     acctsflattened | ||||
| 
 | ||||
| -- | Convert a list of account names to a tree of Account objects,  | ||||
| -- with just the account names filled in.  | ||||
| -- | Convert a list of account names to a tree of Account objects, | ||||
| -- with just the account names filled in. | ||||
| -- A single root account with the given name is added. | ||||
| accountTree :: AccountName -> [AccountName] -> Account | ||||
| accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m } | ||||
| @ -193,7 +193,7 @@ filterAccounts p a | ||||
|     | otherwise = concatMap (filterAccounts p) (asubs a) | ||||
| 
 | ||||
| -- | Sort each group of siblings in an account tree by inclusive amount, | ||||
| -- so that the accounts with largest normal balances are listed first.   | ||||
| -- so that the accounts with largest normal balances are listed first. | ||||
| -- The provided normal balance sign determines whether normal balances | ||||
| -- are negative or positive, affecting the sort order. Ie, | ||||
| -- if balances are normally negative, then the most negative balances | ||||
| @ -217,10 +217,10 @@ accountSetDeclarationInfo j a@Account{..} = | ||||
| -- | Sort account names by the order in which they were declared in | ||||
| -- the journal, at each level of the account tree (ie within each | ||||
| -- group of siblings). Undeclared accounts are sorted last and | ||||
| -- alphabetically.  | ||||
| -- alphabetically. | ||||
| -- This is hledger's default sort for reports organised by account. | ||||
| -- The account list is converted to a tree temporarily, adding any | ||||
| -- missing parents; these can be kept (suitable for a tree-mode report)  | ||||
| -- missing parents; these can be kept (suitable for a tree-mode report) | ||||
| -- or removed (suitable for a flat-mode report). | ||||
| -- | ||||
| sortAccountNamesByDeclaration :: Journal -> Bool -> [AccountName] -> [AccountName] | ||||
| @ -235,14 +235,14 @@ sortAccountNamesByDeclaration j keepparents as = | ||||
|   as | ||||
| 
 | ||||
| -- | Sort each group of siblings in an account tree by declaration order, then account name. | ||||
| -- So each group will contain first the declared accounts,  | ||||
| -- in the same order as their account directives were parsed,  | ||||
| -- and then the undeclared accounts, sorted by account name.  | ||||
| -- So each group will contain first the declared accounts, | ||||
| -- in the same order as their account directives were parsed, | ||||
| -- and then the undeclared accounts, sorted by account name. | ||||
| sortAccountTreeByDeclaration :: Account -> Account | ||||
| sortAccountTreeByDeclaration a | ||||
|   | null $ asubs a = a | ||||
|   | otherwise      = a{asubs= | ||||
|       sortOn accountDeclarationOrderAndName $  | ||||
|       sortOn accountDeclarationOrderAndName $ | ||||
|       map sortAccountTreeByDeclaration $ asubs a | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -29,7 +29,7 @@ module Hledger.Data.AccountName ( | ||||
|   ,expandAccountName | ||||
|   ,expandAccountNames | ||||
|   ,isAccountNamePrefixOf | ||||
| --  ,isAccountRegex  | ||||
| --  ,isAccountRegex | ||||
|   ,isSubAccountNameOf | ||||
|   ,parentAccountName | ||||
|   ,parentAccountNames | ||||
| @ -50,7 +50,7 @@ import Data.Tree | ||||
| import Text.Printf | ||||
| 
 | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| 
 | ||||
| -- $setup | ||||
| -- >>> :set -XOverloadedStrings | ||||
| @ -88,13 +88,13 @@ accountNameLevel "" = 0 | ||||
| accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1 | ||||
| 
 | ||||
| -- | A top-level account prefixed to some accounts in budget reports. | ||||
| -- Defined here so it can be ignored by accountNameDrop.  | ||||
| -- Defined here so it can be ignored by accountNameDrop. | ||||
| unbudgetedAccountName :: T.Text | ||||
| unbudgetedAccountName = "<unbudgeted>" | ||||
| 
 | ||||
| -- | Remove some number of account name components from the front of the account name. | ||||
| -- If the special "<unbudgeted>" top-level account is present, it is preserved and | ||||
| -- dropping affects the rest of the account name.  | ||||
| -- dropping affects the rest of the account name. | ||||
| accountNameDrop :: Int -> AccountName -> AccountName | ||||
| accountNameDrop n a | ||||
|   | a == unbudgetedAccountName = a | ||||
| @ -103,7 +103,7 @@ accountNameDrop n a | ||||
|         "" -> unbudgetedAccountName | ||||
|         a' -> unbudgetedAccountAndSep <> a' | ||||
|   | otherwise = accountNameFromComponents $ drop n $ accountNameComponents a | ||||
|   where  | ||||
|   where | ||||
|     unbudgetedAccountAndSep = unbudgetedAccountName <> acctsep | ||||
| 
 | ||||
| -- | Sorted unique account names implied by these account names, | ||||
|  | ||||
| @ -139,7 +139,7 @@ import Text.Printf | ||||
| 
 | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Data.Commodity | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| 
 | ||||
| 
 | ||||
| deriving instance Show MarketPrice | ||||
| @ -148,7 +148,7 @@ deriving instance Show MarketPrice | ||||
| ------------------------------------------------------------------------------- | ||||
| -- Amount styles | ||||
| 
 | ||||
| -- | Default amount style  | ||||
| -- | Default amount style | ||||
| amountstyle = AmountStyle L False 0 (Just '.') Nothing | ||||
| 
 | ||||
| 
 | ||||
| @ -222,10 +222,10 @@ amountToCost styles = styleAmount styles . costOfAmount | ||||
| -- | Replace an amount's TotalPrice, if it has one, with an equivalent UnitPrice. | ||||
| -- Has no effect on amounts without one. | ||||
| -- Also increases the unit price's display precision to show one extra decimal place, | ||||
| -- to help keep transaction amounts balancing.  | ||||
| -- to help keep transaction amounts balancing. | ||||
| -- Does Decimal division, might be some rounding/irrational number issues. | ||||
| amountTotalPriceToUnitPrice :: Amount -> Amount | ||||
| amountTotalPriceToUnitPrice  | ||||
| amountTotalPriceToUnitPrice | ||||
|   a@Amount{aquantity=q, aprice=Just (TotalPrice pa@Amount{aquantity=pq, astyle=ps@AmountStyle{asprecision=pp}})} | ||||
|   = a{aprice = Just $ UnitPrice pa{aquantity=abs (pq/q), astyle=ps{asprecision=pp+1}}} | ||||
| amountTotalPriceToUnitPrice a = a | ||||
| @ -317,20 +317,20 @@ showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice | ||||
| showAmountWithoutPrice :: Amount -> String | ||||
| showAmountWithoutPrice a = showAmount a{aprice=Nothing} | ||||
| 
 | ||||
| -- | Set an amount's internal precision, ie rounds the Decimal representing  | ||||
| -- | Set an amount's internal precision, ie rounds the Decimal representing | ||||
| -- the amount's quantity to some number of decimal places. | ||||
| -- Rounding is done with Data.Decimal's default roundTo function: | ||||
| -- "If the value ends in 5 then it is rounded to the nearest even value (Banker's Rounding)". | ||||
| -- Does not change the amount's display precision. | ||||
| -- Intended only for internal use, eg when comparing amounts in tests.  | ||||
| -- Intended only for internal use, eg when comparing amounts in tests. | ||||
| setAmountInternalPrecision :: Int -> Amount -> Amount | ||||
| setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{  | ||||
|    astyle=s{asprecision=p}  | ||||
| setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{ | ||||
|    astyle=s{asprecision=p} | ||||
|   ,aquantity=roundTo (fromIntegral p) q | ||||
|   } | ||||
| 
 | ||||
| -- | Set an amount's internal precision, flipped. | ||||
| -- Intended only for internal use, eg when comparing amounts in tests.  | ||||
| -- Intended only for internal use, eg when comparing amounts in tests. | ||||
| withInternalPrecision :: Amount -> Int -> Amount | ||||
| withInternalPrecision = flip setAmountInternalPrecision | ||||
| 
 | ||||
| @ -366,7 +366,7 @@ styleAmount :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount | ||||
| styleAmount styles a = | ||||
|   case M.lookup (acommodity a) styles of | ||||
|     Just s  -> a{astyle=s} | ||||
|     Nothing -> a  | ||||
|     Nothing -> a | ||||
| 
 | ||||
| -- | Get the string representation of an amount, based on its | ||||
| -- commodity's display settings. String representations equivalent to | ||||
| @ -375,7 +375,7 @@ styleAmount styles a = | ||||
| showAmount :: Amount -> String | ||||
| showAmount = showAmountHelper False | ||||
| 
 | ||||
| -- | Colour version. For a negative amount, adds ANSI codes to change the colour,  | ||||
| -- | Colour version. For a negative amount, adds ANSI codes to change the colour, | ||||
| -- currently to hard-coded red. | ||||
| cshowAmount :: Amount -> String | ||||
| cshowAmount a = | ||||
| @ -589,7 +589,7 @@ multiplyMixedAmountAndPrice n = mapMixedAmount (multiplyAmountAndPrice n) | ||||
| -- | Calculate the average of some mixed amounts. | ||||
| averageMixedAmounts :: [MixedAmount] -> MixedAmount | ||||
| averageMixedAmounts [] = 0 | ||||
| averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` sum as  | ||||
| averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` sum as | ||||
| 
 | ||||
| -- | Is this mixed amount negative, if it can be normalised to a single commodity ? | ||||
| isNegativeMixedAmount :: MixedAmount -> Maybe Bool | ||||
| @ -620,7 +620,7 @@ isReallyZeroMixedAmountCost = isReallyZeroMixedAmount . costOfMixedAmount | ||||
| 
 | ||||
| -- | Given a map of standard amount display styles, apply the appropriate ones to each individual amount. | ||||
| styleMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount | ||||
| styleMixedAmount styles (Mixed as) = Mixed $ map (styleAmount styles) as  | ||||
| styleMixedAmount styles (Mixed as) = Mixed $ map (styleAmount styles) as | ||||
| 
 | ||||
| -- | Get the string representation of a mixed amount, after | ||||
| -- normalising it to one amount per commodity. Assumes amounts have | ||||
| @ -713,7 +713,7 @@ canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> M | ||||
| canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as | ||||
| 
 | ||||
| -- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice. | ||||
| -- Has no effect on amounts without one.  | ||||
| -- Has no effect on amounts without one. | ||||
| -- Does Decimal division, might be some rounding/irrational number issues. | ||||
| mixedAmountTotalPriceToUnitPrice :: MixedAmount -> MixedAmount | ||||
| mixedAmountTotalPriceToUnitPrice (Mixed as) = Mixed $ map amountTotalPriceToUnitPrice as | ||||
| @ -731,17 +731,17 @@ tests_Amount = tests "Amount" [ | ||||
|       ,costOfAmount (eur 1){aprice=Just $ TotalPrice $ usd 2} `is` usd 2 | ||||
|       ,costOfAmount (eur (-1)){aprice=Just $ TotalPrice $ usd 2} `is` usd (-2) | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "isZeroAmount" [ | ||||
|        expect $ isZeroAmount amount | ||||
|       ,expect $ isZeroAmount $ usd 0 | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "negating amounts" [ | ||||
|        negate (usd 1) `is` (usd 1){aquantity= -1} | ||||
|       ,let b = (usd 1){aprice=Just $ UnitPrice $ eur 2} in negate b `is` b{aquantity= -1} | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "adding amounts without prices" [ | ||||
|        (usd 1.23 + usd (-1.23)) `is` usd 0 | ||||
|       ,(usd 1.23 + usd (-1.23)) `is` usd 0 | ||||
| @ -753,7 +753,7 @@ tests_Amount = tests "Amount" [ | ||||
|       -- adding different commodities assumes conversion rate 1 | ||||
|       ,expect $ isZeroAmount (usd 1.23 - eur 1.23) | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "showAmount" [ | ||||
|       showAmount (usd 0 + gbp 0) `is` "0" | ||||
|     ] | ||||
| @ -770,7 +770,7 @@ tests_Amount = tests "Amount" [ | ||||
|                ]) | ||||
|         `is` Mixed [usd 0 `withPrecision` 3] | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "adding mixed amounts with total prices" [ | ||||
|       sum (map (Mixed . (:[])) | ||||
|        [usd 1 @@ eur 1 | ||||
| @ -780,7 +780,7 @@ tests_Amount = tests "Amount" [ | ||||
|                    ,usd (-2) @@ eur 1 | ||||
|                    ] | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "showMixedAmount" [ | ||||
|        showMixedAmount (Mixed [usd 1]) `is` "$1.00" | ||||
|       ,showMixedAmount (Mixed [usd 1 `at` eur 2]) `is` "$1.00 @ €2.00" | ||||
| @ -788,27 +788,27 @@ tests_Amount = tests "Amount" [ | ||||
|       ,showMixedAmount (Mixed []) `is` "0" | ||||
|       ,showMixedAmount missingmixedamt `is` "" | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "showMixedAmountWithoutPrice" $ | ||||
|       let a = usd 1 `at` eur 2 in  | ||||
|       let a = usd 1 `at` eur 2 in | ||||
|     [ | ||||
|         showMixedAmountWithoutPrice (Mixed [a]) `is` "$1.00" | ||||
|        ,showMixedAmountWithoutPrice (Mixed [a, -a]) `is` "0" | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "normaliseMixedAmount" [ | ||||
|        test "a missing amount overrides any other amounts" $  | ||||
|        test "a missing amount overrides any other amounts" $ | ||||
|         normaliseMixedAmount (Mixed [usd 1, missingamt]) `is` missingmixedamt | ||||
|       ,test "unpriced same-commodity amounts are combined" $  | ||||
|       ,test "unpriced same-commodity amounts are combined" $ | ||||
|         normaliseMixedAmount (Mixed [usd 0, usd 2]) `is` Mixed [usd 2] | ||||
|       ,test "amounts with same unit price are combined" $  | ||||
|       ,test "amounts with same unit price are combined" $ | ||||
|         normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) `is` Mixed [usd 2 `at` eur 1] | ||||
|       ,test "amounts with different unit prices are not combined" $  | ||||
|       ,test "amounts with different unit prices are not combined" $ | ||||
|         normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) `is` Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2] | ||||
|       ,test "amounts with total prices are not combined" $ | ||||
|         normaliseMixedAmount (Mixed  [usd 1 @@ eur 1, usd 1 @@ eur 1]) `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1] | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|     ,tests "normaliseMixedAmountSquashPricesForDisplay" [ | ||||
|        normaliseMixedAmountSquashPricesForDisplay (Mixed []) `is` Mixed [nullamt] | ||||
|       ,expect $ isZeroMixedAmount $ normaliseMixedAmountSquashPricesForDisplay | ||||
|  | ||||
| @ -156,7 +156,7 @@ spansSpan spans = DateSpan (maybe Nothing spanStart $ headMay spans) (maybe Noth | ||||
| -- If no interval is specified, the original span is returned. | ||||
| -- If the original span is the null date span, ie unbounded, the null date span is returned. | ||||
| -- If the original span is empty, eg if the end date is <= the start date, no spans are returned. | ||||
| --  | ||||
| -- | ||||
| -- | ||||
| -- ==== Examples: | ||||
| -- >>> let t i d1 d2 = splitSpan i $ mkdatespan d1 d2 | ||||
| @ -531,19 +531,19 @@ startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day | ||||
| -- Examples: lets take 2017-11-22. Year-long intervals covering it that | ||||
| -- starts before Nov 22 will start in 2017. However | ||||
| -- intervals that start after Nov 23rd should start in 2016: | ||||
| -- >>> let wed22nd = parsedate "2017-11-22"           | ||||
| -- >>> let wed22nd = parsedate "2017-11-22" | ||||
| -- >>> nthdayofyearcontaining 11 21 wed22nd | ||||
| -- 2017-11-21           | ||||
| -- 2017-11-21 | ||||
| -- >>> nthdayofyearcontaining 11 22 wed22nd | ||||
| -- 2017-11-22           | ||||
| -- 2017-11-22 | ||||
| -- >>> nthdayofyearcontaining 11 23 wed22nd | ||||
| -- 2016-11-23           | ||||
| -- 2016-11-23 | ||||
| -- >>> nthdayofyearcontaining 12 02 wed22nd | ||||
| -- 2016-12-02           | ||||
| -- 2016-12-02 | ||||
| -- >>> nthdayofyearcontaining 12 31 wed22nd | ||||
| -- 2016-12-31           | ||||
| -- 2016-12-31 | ||||
| -- >>> nthdayofyearcontaining 1 1 wed22nd | ||||
| -- 2017-01-01           | ||||
| -- 2017-01-01 | ||||
| nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day | ||||
| nthdayofyearcontaining m md date | ||||
|   | not (validMonth $ show m)  = error' $ "nthdayofyearcontaining: invalid month "++show m | ||||
| @ -555,23 +555,23 @@ nthdayofyearcontaining m md date | ||||
|         s = startofyear date | ||||
| 
 | ||||
| -- | For given date d find month-long interval that starts on nth day of month | ||||
| -- and covers it.  | ||||
| -- and covers it. | ||||
| -- The given day of month should be basically valid (1-31), or an error is raised. | ||||
| -- | ||||
| -- Examples: lets take 2017-11-22. Month-long intervals covering it that | ||||
| -- start on 1st-22nd of month will start in Nov. However | ||||
| -- intervals that start on 23rd-30th of month should start in Oct: | ||||
| -- >>> let wed22nd = parsedate "2017-11-22"           | ||||
| -- >>> let wed22nd = parsedate "2017-11-22" | ||||
| -- >>> nthdayofmonthcontaining 1 wed22nd | ||||
| -- 2017-11-01           | ||||
| -- 2017-11-01 | ||||
| -- >>> nthdayofmonthcontaining 12 wed22nd | ||||
| -- 2017-11-12           | ||||
| -- 2017-11-12 | ||||
| -- >>> nthdayofmonthcontaining 22 wed22nd | ||||
| -- 2017-11-22           | ||||
| -- 2017-11-22 | ||||
| -- >>> nthdayofmonthcontaining 23 wed22nd | ||||
| -- 2017-10-23           | ||||
| -- 2017-10-23 | ||||
| -- >>> nthdayofmonthcontaining 30 wed22nd | ||||
| -- 2017-10-30           | ||||
| -- 2017-10-30 | ||||
| nthdayofmonthcontaining :: MonthDay -> Day -> Day | ||||
| nthdayofmonthcontaining md date | ||||
|   | not (validDay $ show md) = error' $ "nthdayofmonthcontaining: invalid day "  ++show md | ||||
| @ -582,22 +582,22 @@ nthdayofmonthcontaining md date | ||||
|         s = startofmonth date | ||||
| 
 | ||||
| -- | For given date d find week-long interval that starts on nth day of week | ||||
| -- and covers it.  | ||||
| -- and covers it. | ||||
| -- | ||||
| -- Examples: 2017-11-22 is Wed. Week-long intervals that cover it and | ||||
| -- start on Mon, Tue or Wed will start in the same week. However | ||||
| -- intervals that start on Thu or Fri should start in prev week:           | ||||
| -- >>> let wed22nd = parsedate "2017-11-22"           | ||||
| -- intervals that start on Thu or Fri should start in prev week: | ||||
| -- >>> let wed22nd = parsedate "2017-11-22" | ||||
| -- >>> nthdayofweekcontaining 1 wed22nd | ||||
| -- 2017-11-20           | ||||
| -- 2017-11-20 | ||||
| -- >>> nthdayofweekcontaining 2 wed22nd | ||||
| -- 2017-11-21 | ||||
| -- >>> nthdayofweekcontaining 3 wed22nd | ||||
| -- 2017-11-22           | ||||
| -- 2017-11-22 | ||||
| -- >>> nthdayofweekcontaining 4 wed22nd | ||||
| -- 2017-11-16           | ||||
| -- 2017-11-16 | ||||
| -- >>> nthdayofweekcontaining 5 wed22nd | ||||
| -- 2017-11-17           | ||||
| -- 2017-11-17 | ||||
| nthdayofweekcontaining :: WeekDay -> Day -> Day | ||||
| nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek | ||||
|                            | otherwise = nthOfPrevWeek | ||||
| @ -606,12 +606,12 @@ nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek | ||||
|           s = startofweek d | ||||
| 
 | ||||
| -- | For given date d find month-long interval that starts on nth weekday of month | ||||
| -- and covers it.  | ||||
| -- and covers it. | ||||
| -- | ||||
| -- Examples: 2017-11-22 is 3rd Wed of Nov. Month-long intervals that cover it and | ||||
| -- start on 1st-4th Wed will start in Nov. However | ||||
| -- intervals that start on 4th Thu or Fri or later should start in Oct:           | ||||
| -- >>> let wed22nd = parsedate "2017-11-22"           | ||||
| -- intervals that start on 4th Thu or Fri or later should start in Oct: | ||||
| -- >>> let wed22nd = parsedate "2017-11-22" | ||||
| -- >>> nthweekdayofmonthcontaining 1 3 wed22nd | ||||
| -- 2017-11-01 | ||||
| -- >>> nthweekdayofmonthcontaining 3 2 wed22nd | ||||
| @ -630,12 +630,12 @@ nthweekdayofmonthcontaining n wd d | nthWeekdaySameMonth <= d  = nthWeekdaySameM | ||||
| 
 | ||||
| -- | Advance to nth weekday wd after given start day s | ||||
| advancetonthweekday :: Int -> WeekDay -> Day -> Day | ||||
| advancetonthweekday n wd s =  | ||||
| advancetonthweekday n wd s = | ||||
|   maybe err (addWeeks (n-1)) $ firstMatch (>=s) $ iterate (addWeeks 1) $ firstweekday s | ||||
|   where | ||||
|     err = error' "advancetonthweekday: should not happen" | ||||
|     addWeeks k = addDays (7 * fromIntegral k) | ||||
|     firstMatch p = headMay . dropWhile (not . p)  | ||||
|     firstMatch p = headMay . dropWhile (not . p) | ||||
|     firstweekday = addDays (fromIntegral wd-1) . startofweek | ||||
| 
 | ||||
| ---------------------------------------------------------------------- | ||||
| @ -834,7 +834,7 @@ md = do | ||||
|   failIfInvalidDay d | ||||
|   return ("",m,d) | ||||
| 
 | ||||
| -- These are compared case insensitively, and should all be kept lower case.  | ||||
| -- These are compared case insensitively, and should all be kept lower case. | ||||
| months         = ["january","february","march","april","may","june", | ||||
|                   "july","august","september","october","november","december"] | ||||
| monthabbrevs   = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"] | ||||
| @ -864,8 +864,8 @@ weekday = do | ||||
|   wday <- T.toLower <$> (choice . map string' $ weekdays ++ weekdayabbrevs) | ||||
|   case catMaybes $ [wday `elemIndex` weekdays, wday `elemIndex` weekdayabbrevs] of | ||||
|     (i:_) -> return (i+1) | ||||
|     []    -> fail  $ "weekday: should not happen: attempted to find " <>  | ||||
|                       show wday <> " in " <> show (weekdays ++ weekdayabbrevs)  | ||||
|     []    -> fail  $ "weekday: should not happen: attempted to find " <> | ||||
|                       show wday <> " in " <> show (weekdays ++ weekdayabbrevs) | ||||
| 
 | ||||
| today,yesterday,tomorrow :: TextParser m SmartDate | ||||
| today     = string' "today"     >> return ("","","today") | ||||
| @ -909,7 +909,7 @@ lastthisnextthing = do | ||||
| -- >>> p "every 2nd day" | ||||
| -- Right (DayOfMonth 2,DateSpan -) | ||||
| -- >>> p "every 2nd day 2009-" | ||||
| -- Right (DayOfMonth 2,DateSpan 2009/01/01-)   | ||||
| -- Right (DayOfMonth 2,DateSpan 2009/01/01-) | ||||
| -- >>> p "every 29th Nov" | ||||
| -- Right (DayOfYear 11 29,DateSpan -) | ||||
| -- >>> p "every 29th nov -2009" | ||||
| @ -1007,9 +1007,9 @@ reportingintervalp = choice' [ | ||||
|         string' "of" | ||||
|         skipMany spacenonewline | ||||
|         string' period | ||||
|          | ||||
| 
 | ||||
|       optOf_ period = optional $ try $ of_ period | ||||
|        | ||||
| 
 | ||||
|       nth = do n <- some digitChar | ||||
|                choice' $ map string' ["st","nd","rd","th"] | ||||
|                return $ read n | ||||
|  | ||||
| @ -111,7 +111,7 @@ import Data.Tree | ||||
| import System.Time (ClockTime(TOD)) | ||||
| import Text.Printf | ||||
| 
 | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Data.AccountName | ||||
| import Hledger.Data.Amount | ||||
| @ -160,12 +160,12 @@ instance Show Journal where | ||||
| --                      ] | ||||
| 
 | ||||
| -- The monoid instance for Journal is useful for two situations. | ||||
| --  | ||||
| -- | ||||
| -- 1. concatenating finalised journals, eg with multiple -f options: | ||||
| -- FIRST <> SECOND. The second's list fields are appended to the | ||||
| -- first's, map fields are combined, transaction counts are summed, | ||||
| -- the parse state of the second is kept. | ||||
| --  | ||||
| -- | ||||
| -- 2. merging a child parsed journal, eg with the include directive: | ||||
| -- CHILD <> PARENT. A parsed journal's data is in reverse order, so | ||||
| -- this gives what we want. | ||||
| @ -268,7 +268,7 @@ journalPostings = concatMap tpostings . jtxns | ||||
| journalAccountNamesUsed :: Journal -> [AccountName] | ||||
| journalAccountNamesUsed = accountNamesFromPostings . journalPostings | ||||
| 
 | ||||
| -- | Sorted unique account names implied by this journal's transactions -  | ||||
| -- | Sorted unique account names implied by this journal's transactions - | ||||
| -- accounts posted to and all their implied parent accounts. | ||||
| journalAccountNamesImplied :: Journal -> [AccountName] | ||||
| journalAccountNamesImplied = expandAccountNames . journalAccountNamesUsed | ||||
| @ -289,31 +289,31 @@ journalAccountNamesDeclaredOrImplied j = nub $ sort $ journalAccountNamesDeclare | ||||
| 
 | ||||
| -- | Convenience/compatibility alias for journalAccountNamesDeclaredOrImplied. | ||||
| journalAccountNames :: Journal -> [AccountName] | ||||
| journalAccountNames = journalAccountNamesDeclaredOrImplied  | ||||
| journalAccountNames = journalAccountNamesDeclaredOrImplied | ||||
| 
 | ||||
| journalAccountNameTree :: Journal -> Tree AccountName | ||||
| journalAccountNameTree = accountNameTreeFrom . journalAccountNames | ||||
| 
 | ||||
| -- queries for standard account types | ||||
| 
 | ||||
| -- | Get a query for accounts of a certain type (Asset, Liability..) in this journal.   | ||||
| -- The query will match all accounts which were declared as that type by account directives,  | ||||
| -- plus all their subaccounts which have not been declared as a different type.  | ||||
| -- If no accounts were declared as this type, the query will instead match accounts  | ||||
| -- | Get a query for accounts of a certain type (Asset, Liability..) in this journal. | ||||
| -- The query will match all accounts which were declared as that type by account directives, | ||||
| -- plus all their subaccounts which have not been declared as a different type. | ||||
| -- If no accounts were declared as this type, the query will instead match accounts | ||||
| -- with names matched by the provided case-insensitive regular expression. | ||||
| journalAccountTypeQuery :: AccountType -> Regexp -> Journal -> Query | ||||
| journalAccountTypeQuery atype fallbackregex j = | ||||
|   case M.lookup atype (jdeclaredaccounttypes j) of | ||||
|     Nothing -> Acct fallbackregex | ||||
|     Just as -> | ||||
|       -- XXX Query isn't able to match account type since that requires extra info from the journal.  | ||||
|       -- XXX Query isn't able to match account type since that requires extra info from the journal. | ||||
|       -- So we do a hacky search by name instead. | ||||
|       And [  | ||||
|       And [ | ||||
|          Or $ map (Acct . accountNameToAccountRegex) as | ||||
|         ,Not $ Or $ map (Acct . accountNameToAccountRegex) differentlytypedsubs | ||||
|         ] | ||||
|       where | ||||
|         differentlytypedsubs = concat  | ||||
|         differentlytypedsubs = concat | ||||
|           [subs | (t,bs) <- M.toList (jdeclaredaccounttypes j) | ||||
|               , t /= atype | ||||
|               , let subs = [b | b <- bs, any (`isAccountNamePrefixOf` b) as] | ||||
| @ -321,35 +321,35 @@ journalAccountTypeQuery atype fallbackregex j = | ||||
| 
 | ||||
| -- | A query for accounts in this journal which have been | ||||
| -- declared as Asset by account directives, or otherwise for | ||||
| -- accounts with names matched by the case-insensitive regular expression  | ||||
| -- accounts with names matched by the case-insensitive regular expression | ||||
| -- @^assets?(:|$)@. | ||||
| journalAssetAccountQuery :: Journal -> Query | ||||
| journalAssetAccountQuery = journalAccountTypeQuery Asset "^assets?(:|$)" | ||||
| 
 | ||||
| -- | A query for accounts in this journal which have been | ||||
| -- declared as Liability by account directives, or otherwise for | ||||
| -- accounts with names matched by the case-insensitive regular expression  | ||||
| -- accounts with names matched by the case-insensitive regular expression | ||||
| -- @^(debts?|liabilit(y|ies))(:|$)@. | ||||
| journalLiabilityAccountQuery :: Journal -> Query | ||||
| journalLiabilityAccountQuery = journalAccountTypeQuery Liability "^(debts?|liabilit(y|ies))(:|$)" | ||||
| 
 | ||||
| -- | A query for accounts in this journal which have been | ||||
| -- declared as Equity by account directives, or otherwise for | ||||
| -- accounts with names matched by the case-insensitive regular expression  | ||||
| -- accounts with names matched by the case-insensitive regular expression | ||||
| -- @^equity(:|$)@. | ||||
| journalEquityAccountQuery :: Journal -> Query | ||||
| journalEquityAccountQuery = journalAccountTypeQuery Equity "^equity(:|$)" | ||||
| 
 | ||||
| -- | A query for accounts in this journal which have been | ||||
| -- declared as Revenue by account directives, or otherwise for | ||||
| -- accounts with names matched by the case-insensitive regular expression  | ||||
| -- accounts with names matched by the case-insensitive regular expression | ||||
| -- @^(income|revenue)s?(:|$)@. | ||||
| journalRevenueAccountQuery :: Journal -> Query | ||||
| journalRevenueAccountQuery = journalAccountTypeQuery Revenue "^(income|revenue)s?(:|$)" | ||||
| 
 | ||||
| -- | A query for accounts in this journal which have been | ||||
| -- declared as Expense by account directives, or otherwise for | ||||
| -- accounts with names matched by the case-insensitive regular expression  | ||||
| -- accounts with names matched by the case-insensitive regular expression | ||||
| -- @^(income|revenue)s?(:|$)@. | ||||
| journalExpenseAccountQuery  :: Journal -> Query | ||||
| journalExpenseAccountQuery = journalAccountTypeQuery Expense "^expenses?(:|$)" | ||||
| @ -371,7 +371,7 @@ journalProfitAndLossAccountQuery j = Or [journalRevenueAccountQuery j | ||||
| 
 | ||||
| -- | A query for Cash (-equivalent) accounts in this journal (ie, | ||||
| -- accounts which appear on the cashflow statement.)  This is currently | ||||
| -- hard-coded to be all the Asset accounts except for those with names  | ||||
| -- hard-coded to be all the Asset accounts except for those with names | ||||
| -- containing the case-insensitive regular expression @(receivable|:A/R|:fixed)@. | ||||
| journalCashAccountQuery  :: Journal -> Query | ||||
| journalCashAccountQuery j = And [journalAssetAccountQuery j, Not $ Acct "(receivable|:A/R|:fixed)"] | ||||
| @ -579,7 +579,7 @@ journalTieTransactions j@Journal{jtxns=ts} = j{jtxns=map txnTieKnot ts} | ||||
| journalUntieTransactions :: Transaction -> Transaction | ||||
| journalUntieTransactions t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps} | ||||
| 
 | ||||
| -- | Apply any transaction modifier rules in the journal  | ||||
| -- | Apply any transaction modifier rules in the journal | ||||
| -- (adding automated postings to transactions, eg). | ||||
| journalModifyTransactions :: Journal -> Journal | ||||
| journalModifyTransactions j = j{ jtxns = modifyTransactions (jtxnmodifiers j) (jtxns j) } | ||||
| @ -591,7 +591,7 @@ journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTran | ||||
| 
 | ||||
| -- "Transaction balancing" - inferring missing amounts and checking transaction balancedness and balance assertions | ||||
| 
 | ||||
| -- | Monad used for statefully balancing/amount-inferring/assertion-checking  | ||||
| -- | Monad used for statefully balancing/amount-inferring/assertion-checking | ||||
| -- a sequence of transactions. | ||||
| -- Perhaps can be simplified, or would a different ordering of layers make sense ? | ||||
| -- If you see a way, let us know. | ||||
| @ -613,9 +613,9 @@ data BalancingState s = BalancingState { | ||||
| withB :: (BalancingState s -> ST s a) -> Balancing s a | ||||
| withB f = ask >>= lift . lift . f | ||||
| 
 | ||||
| -- | Get an account's running balance so far.  | ||||
| -- | Get an account's running balance so far. | ||||
| getAmountB :: AccountName -> Balancing s MixedAmount | ||||
| getAmountB acc = withB $ \BalancingState{bsBalances} -> do  | ||||
| getAmountB acc = withB $ \BalancingState{bsBalances} -> do | ||||
|   fromMaybe 0 <$> H.lookup bsBalances acc | ||||
| 
 | ||||
| -- | Add an amount to an account's running balance, and return the new running balance. | ||||
| @ -626,7 +626,7 @@ addAmountB acc amt = withB $ \BalancingState{bsBalances} -> do | ||||
|   H.insert bsBalances acc new | ||||
|   return new | ||||
| 
 | ||||
| -- | Set an account's running balance to this amount, and return the difference from the old.  | ||||
| -- | Set an account's running balance to this amount, and return the difference from the old. | ||||
| setAmountB :: AccountName -> MixedAmount -> Balancing s MixedAmount | ||||
| setAmountB acc amt = withB $ \BalancingState{bsBalances} -> do | ||||
|   old <- fromMaybe 0 <$> H.lookup bsBalances acc | ||||
| @ -639,15 +639,15 @@ storeTransactionB t = withB $ \BalancingState{bsTransactions}  -> | ||||
|   void $ writeArray bsTransactions (tindex t) t | ||||
| 
 | ||||
| -- | Infer any missing amounts (to satisfy balance assignments and | ||||
| -- to balance transactions) and check that all transactions balance  | ||||
| -- to balance transactions) and check that all transactions balance | ||||
| -- and (optional) all balance assertions pass. Or return an error message | ||||
| -- (just the first error encountered). | ||||
| -- | ||||
| -- Assumes journalInferCommodityStyles has been called, since those affect transaction balancing. | ||||
| -- | ||||
| -- This does multiple things because amount inferring, balance assignments,  | ||||
| -- This does multiple things because amount inferring, balance assignments, | ||||
| -- balance assertions and posting dates are interdependent. | ||||
| --  | ||||
| -- | ||||
| -- This can be simplified further. Overview as of 20190219: | ||||
| -- @ | ||||
| -- ****** parseAndFinaliseJournal['] (Cli/Utils.hs), journalAddForecast (Common.hs), budgetJournal (BudgetReport.hs), tests (BalanceReport.hs) | ||||
| @ -670,19 +670,19 @@ storeTransactionB t = withB $ \BalancingState{bsTransactions}  -> | ||||
| journalBalanceTransactions :: Bool -> Journal -> Either String Journal | ||||
| journalBalanceTransactions assrt j' = | ||||
|   let | ||||
|     -- ensure transactions are numbered, so we can store them by number  | ||||
|     -- ensure transactions are numbered, so we can store them by number | ||||
|     j@Journal{jtxns=ts} = journalNumberTransactions j' | ||||
|     -- display precisions used in balanced checking | ||||
|     styles = Just $ journalCommodityStyles j | ||||
|     -- balance assignments will not be allowed on these | ||||
|     txnmodifieraccts = S.fromList $ map paccount $ concatMap tmpostingrules $ jtxnmodifiers j  | ||||
|   in  | ||||
|     runST $ do  | ||||
|     txnmodifieraccts = S.fromList $ map paccount $ concatMap tmpostingrules $ jtxnmodifiers j | ||||
|   in | ||||
|     runST $ do | ||||
|       -- We'll update a mutable array of transactions as we balance them, | ||||
|       -- not strictly necessary but avoids a sort at the end I think. | ||||
|       balancedtxns <- newListArray (1, genericLength ts) ts | ||||
| 
 | ||||
|       -- Infer missing posting amounts, check transactions are balanced,  | ||||
|       -- Infer missing posting amounts, check transactions are balanced, | ||||
|       -- and check balance assertions. This is done in two passes: | ||||
|       runExceptT $ do | ||||
| 
 | ||||
| @ -691,14 +691,14 @@ journalBalanceTransactions assrt j' = | ||||
|         -- The postings and not-yet-balanced transactions remain in the same relative order. | ||||
|         psandts :: [Either Posting Transaction] <- fmap concat $ forM ts $ \case | ||||
|           t | null $ assignmentPostings t -> case balanceTransaction styles t of | ||||
|               Left  e  -> throwError e  | ||||
|               Left  e  -> throwError e | ||||
|               Right t' -> do | ||||
|                 lift $ writeArray balancedtxns (tindex t') t' | ||||
|                 return $ map Left $ tpostings t' | ||||
|           t -> return [Right t] | ||||
| 
 | ||||
|         -- 2. Sort these items by date, preserving the order of same-day items, | ||||
|         -- and step through them while keeping running account balances,  | ||||
|         -- and step through them while keeping running account balances, | ||||
|         runningbals <- lift $ H.newSized (length $ journalAccountNamesUsed j) | ||||
|         flip runReaderT (BalancingState styles txnmodifieraccts assrt runningbals balancedtxns) $ do | ||||
|           -- performing balance assignments in, and balancing, the remaining transactions, | ||||
| @ -706,17 +706,17 @@ journalBalanceTransactions assrt j' = | ||||
|           void $ mapM' balanceTransactionAndCheckAssertionsB $ sortOn (either postingDate tdate) psandts | ||||
| 
 | ||||
|         ts' <- lift $ getElems balancedtxns | ||||
|         return j{jtxns=ts'}  | ||||
|         return j{jtxns=ts'} | ||||
| 
 | ||||
| -- | This function is called statefully on each of a date-ordered sequence of  | ||||
| -- 1. fully explicit postings from already-balanced transactions and  | ||||
| -- | This function is called statefully on each of a date-ordered sequence of | ||||
| -- 1. fully explicit postings from already-balanced transactions and | ||||
| -- 2. not-yet-balanced transactions containing balance assignments. | ||||
| -- It executes balance assignments and finishes balancing the transactions,  | ||||
| -- It executes balance assignments and finishes balancing the transactions, | ||||
| -- and checks balance assertions on each posting as it goes. | ||||
| -- An error will be thrown if a transaction can't be balanced  | ||||
| -- An error will be thrown if a transaction can't be balanced | ||||
| -- or if an illegal balance assignment is found (cf checkIllegalBalanceAssignment). | ||||
| -- Transaction prices are removed, which helps eg balance-assertions.test: 15. Mix different commodities and assignments. | ||||
| -- This stores the balanced transactions in case 2 but not in case 1.   | ||||
| -- This stores the balanced transactions in case 2 but not in case 1. | ||||
| balanceTransactionAndCheckAssertionsB :: Either Posting Transaction -> Balancing s () | ||||
| 
 | ||||
| balanceTransactionAndCheckAssertionsB (Left p@Posting{}) = | ||||
| @ -726,28 +726,28 @@ balanceTransactionAndCheckAssertionsB (Left p@Posting{}) = | ||||
| balanceTransactionAndCheckAssertionsB (Right t@Transaction{tpostings=ps}) = do | ||||
|   -- make sure we can handle the balance assignments | ||||
|   mapM_ checkIllegalBalanceAssignmentB ps | ||||
|   -- for each posting, infer its amount from the balance assignment if applicable,  | ||||
|   -- for each posting, infer its amount from the balance assignment if applicable, | ||||
|   -- update the account's running balance and check the balance assertion if any | ||||
|   ps' <- forM ps $ \p -> pure (removePrices p) >>= addOrAssignAmountAndCheckAssertionB | ||||
|   -- infer any remaining missing amounts, and make sure the transaction is now fully balanced  | ||||
|   -- infer any remaining missing amounts, and make sure the transaction is now fully balanced | ||||
|   styles <- R.reader bsStyles | ||||
|   case balanceTransactionHelper styles t{tpostings=ps'} of | ||||
|     Left err -> throwError err  | ||||
|     Left err -> throwError err | ||||
|     Right (t', inferredacctsandamts) -> do | ||||
|       -- for each amount just inferred, update the running balance  | ||||
|       -- for each amount just inferred, update the running balance | ||||
|       mapM_ (uncurry addAmountB) inferredacctsandamts | ||||
|       -- and save the balanced transaction. | ||||
|       storeTransactionB t'  | ||||
|       storeTransactionB t' | ||||
| 
 | ||||
| -- | If this posting has an explicit amount, add it to the account's running balance. | ||||
| -- If it has a missing amount and a balance assignment, infer the amount from, and  | ||||
| -- If it has a missing amount and a balance assignment, infer the amount from, and | ||||
| -- reset the running balance to, the assigned balance. | ||||
| -- If it has a missing amount and no balance assignment, leave it for later. | ||||
| -- Then test the balance assertion if any. | ||||
| addOrAssignAmountAndCheckAssertionB :: Posting -> Balancing s Posting | ||||
| addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanceassertion=mba} | ||||
|   | hasAmount p = do | ||||
|       newbal <- addAmountB acc amt  | ||||
|       newbal <- addAmountB acc amt | ||||
|       whenM (R.reader bsAssrt) $ checkBalanceAssertionB p newbal | ||||
|       return p | ||||
|   | Just BalanceAssertion{baamount,batotal} <- mba = do | ||||
| @ -760,8 +760,8 @@ addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanc | ||||
|         False -> do | ||||
|           -- a partial balance assignment | ||||
|           oldbalothercommodities <- filterMixedAmount ((acommodity baamount /=) . acommodity) <$> getAmountB acc | ||||
|           let assignedbalthiscommodity = Mixed [baamount]  | ||||
|               newbal = oldbalothercommodities + assignedbalthiscommodity    | ||||
|           let assignedbalthiscommodity = Mixed [baamount] | ||||
|               newbal = oldbalothercommodities + assignedbalthiscommodity | ||||
|           diff <- setAmountB acc newbal | ||||
|           return (diff,newbal) | ||||
|       let p' = p{pamount=diff, poriginal=Just $ originalPosting p} | ||||
| @ -774,7 +774,7 @@ addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanc | ||||
| -- optionally check the posting's balance assertion if any. | ||||
| -- The posting is expected to have an explicit amount (otherwise this does nothing). | ||||
| -- Adding and checking balance assertions are tightly paired because we | ||||
| -- need to see the balance as it stands after each individual posting.  | ||||
| -- need to see the balance as it stands after each individual posting. | ||||
| addAmountAndCheckAssertionB :: Posting -> Balancing s Posting | ||||
| addAmountAndCheckAssertionB p | hasAmount p = do | ||||
|   newbal <- addAmountB (paccount p) (pamount p) | ||||
| @ -806,17 +806,17 @@ checkBalanceAssertionB _ _ = return () | ||||
| checkBalanceAssertionOneCommodityB :: Posting -> Amount -> MixedAmount -> Balancing s () | ||||
| checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt actualbal = do | ||||
|   let isinclusive = maybe False bainclusive $ pbalanceassertion p | ||||
|   actualbal' <-  | ||||
|     if isinclusive  | ||||
|     then  | ||||
|       -- sum the running balances of this account and any of its subaccounts seen so far  | ||||
|       withB $ \BalancingState{bsBalances} ->  | ||||
|         H.foldM  | ||||
|           (\ibal (acc, amt) -> return $ ibal +  | ||||
|   actualbal' <- | ||||
|     if isinclusive | ||||
|     then | ||||
|       -- sum the running balances of this account and any of its subaccounts seen so far | ||||
|       withB $ \BalancingState{bsBalances} -> | ||||
|         H.foldM | ||||
|           (\ibal (acc, amt) -> return $ ibal + | ||||
|             if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc then amt else 0) | ||||
|           0  | ||||
|           0 | ||||
|           bsBalances | ||||
|     else return actualbal   | ||||
|     else return actualbal | ||||
|   let | ||||
|     assertedcomm    = acommodity assertedamt | ||||
|     actualbalincomm = headDef 0 $ amounts $ filterMixedAmountByCommodity assertedcomm $ actualbal' | ||||
| @ -863,17 +863,17 @@ checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt | ||||
| 
 | ||||
| -- | Throw an error if this posting is trying to do an illegal balance assignment. | ||||
| checkIllegalBalanceAssignmentB :: Posting -> Balancing s () | ||||
| checkIllegalBalanceAssignmentB p = do  | ||||
| checkIllegalBalanceAssignmentB p = do | ||||
|   checkBalanceAssignmentPostingDateB p | ||||
|   checkBalanceAssignmentUnassignableAccountB p | ||||
|    | ||||
| 
 | ||||
| -- XXX these should show position. annotateErrorWithTransaction t ? | ||||
| 
 | ||||
| -- | Throw an error if this posting is trying to do a balance assignment and | ||||
| -- has a custom posting date (which makes amount inference too hard/impossible). | ||||
| checkBalanceAssignmentPostingDateB :: Posting -> Balancing s () | ||||
| checkBalanceAssignmentPostingDateB p = | ||||
|   when (hasBalanceAssignment p && isJust (pdate p)) $  | ||||
|   when (hasBalanceAssignment p && isJust (pdate p)) $ | ||||
|     throwError $ unlines $ | ||||
|       ["postings which are balance assignments may not have a custom date." | ||||
|       ,"Please write the posting amount explicitly, or remove the posting date:" | ||||
| @ -918,8 +918,8 @@ journalApplyCommodityStyles j@Journal{jtxns=ts, jpricedirectives=pds} = j'' | ||||
|       fixbalanceassertion ba = ba{baamount=styleAmount styles $ baamount ba} | ||||
|       fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmount styles a} | ||||
| 
 | ||||
| -- | Get all the amount styles defined in this journal, either declared by  | ||||
| -- a commodity directive or inferred from amounts, as a map from symbol to style.  | ||||
| -- | Get all the amount styles defined in this journal, either declared by | ||||
| -- a commodity directive or inferred from amounts, as a map from symbol to style. | ||||
| -- Styles declared by commodity directives take precedence, and these also are | ||||
| -- guaranteed to know their decimal point character. | ||||
| journalCommodityStyles :: Journal -> M.Map CommoditySymbol AmountStyle | ||||
| @ -1078,23 +1078,23 @@ journalPivot fieldortagname j = j{jtxns = map (transactionPivot fieldortagname) | ||||
| 
 | ||||
| -- | Replace this transaction's postings' account names with the value | ||||
| -- of the given field or tag, if any. | ||||
| transactionPivot :: Text -> Transaction -> Transaction          | ||||
| transactionPivot :: Text -> Transaction -> Transaction | ||||
| transactionPivot fieldortagname t = t{tpostings = map (postingPivot fieldortagname) . tpostings $ t} | ||||
| 
 | ||||
| -- | Replace this posting's account name with the value | ||||
| -- of the given field or tag, if any, otherwise the empty string. | ||||
| postingPivot :: Text -> Posting -> Posting          | ||||
| postingPivot :: Text -> Posting -> Posting | ||||
| postingPivot fieldortagname p = p{paccount = pivotedacct, poriginal = Just $ originalPosting p} | ||||
|   where | ||||
|     pivotedacct | ||||
|       | Just t <- ptransaction p, fieldortagname == "code"        = tcode t   | ||||
|       | Just t <- ptransaction p, fieldortagname == "description" = tdescription t   | ||||
|       | Just t <- ptransaction p, fieldortagname == "payee"       = transactionPayee t   | ||||
|       | Just t <- ptransaction p, fieldortagname == "note"        = transactionNote t   | ||||
|       | Just t <- ptransaction p, fieldortagname == "code"        = tcode t | ||||
|       | Just t <- ptransaction p, fieldortagname == "description" = tdescription t | ||||
|       | Just t <- ptransaction p, fieldortagname == "payee"       = transactionPayee t | ||||
|       | Just t <- ptransaction p, fieldortagname == "note"        = transactionNote t | ||||
|       | Just (_, value) <- postingFindTag fieldortagname p        = value | ||||
|       | otherwise                                                 = "" | ||||
| 
 | ||||
| postingFindTag :: TagName -> Posting -> Maybe (TagName, TagValue)          | ||||
| postingFindTag :: TagName -> Posting -> Maybe (TagName, TagValue) | ||||
| postingFindTag tagname p = find ((tagname==) . fst) $ postingAllTags p | ||||
| 
 | ||||
| -- -- | Build a database of market prices in effect on the given date, | ||||
| @ -1333,8 +1333,8 @@ tests_Journal = tests "Journal" [ | ||||
|             nulljournal{ jtxns = [ | ||||
|                transaction "2019/01/01" [ vpost' "a" (num 2)    (balassert (num 2)) ] | ||||
|               ,transaction "2019/01/01" [ | ||||
|                  post' "b" (num 1)     Nothing   | ||||
|                 ,post' "a"  missingamt Nothing   | ||||
|                  post' "b" (num 1)     Nothing | ||||
|                 ,post' "a"  missingamt Nothing | ||||
|               ] | ||||
|               ,transaction "2019/01/01" [ post' "a" (num 0)     (balassert (num 1)) ] | ||||
|             ]} | ||||
|  | ||||
| @ -31,7 +31,7 @@ import qualified Data.Text as T | ||||
| import Safe (headDef) | ||||
| import Text.Printf | ||||
| 
 | ||||
| import Hledger.Utils.Test  | ||||
| import Hledger.Utils.Test | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Data.Account | ||||
| import Hledger.Data.Journal | ||||
|  | ||||
| @ -166,7 +166,7 @@ showPeriod (PeriodFrom b)      = formatTime defaultTimeLocale "%0C%y/%m/%d-" b | ||||
| showPeriod (PeriodTo e)        = formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e)    -- -INCLUSIVEENDDATE | ||||
| showPeriod PeriodAll           = "-" | ||||
| 
 | ||||
| -- | Like showPeriod, but if it's a month period show just  | ||||
| -- | Like showPeriod, but if it's a month period show just | ||||
| -- the 3 letter month name abbreviation for the current locale. | ||||
| showPeriodMonthAbbrev (MonthPeriod _ m)                           -- Jan | ||||
|   | m > 0 && m <= length monthnames = snd $ monthnames !! (m-1) | ||||
|  | ||||
| @ -34,7 +34,7 @@ import Hledger.Utils.UTF8IOCompat (error') | ||||
| -- doctest helper, too much hassle to define in the comment | ||||
| -- XXX duplicates some logic in periodictransactionp | ||||
| _ptgen str = do | ||||
|   let  | ||||
|   let | ||||
|     t = T.pack str | ||||
|     (i,s) = parsePeriodExpr' nulldate t | ||||
|   case checkPeriodicTransactionStartDate i s t of | ||||
| @ -42,7 +42,7 @@ _ptgen str = do | ||||
|     Nothing -> | ||||
|       mapM_ (putStr . showTransaction) $ | ||||
|         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] } | ||||
|           nulldatespan | ||||
| 
 | ||||
| 
 | ||||
| @ -184,13 +184,13 @@ instance Show PeriodicTransaction where | ||||
| -- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the week | ||||
| -- | ||||
| -- >>> _ptgen "monthly from 2017/5/4" | ||||
| -- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month         | ||||
| -- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month | ||||
| -- | ||||
| -- >>> _ptgen "every quarter from 2017/1/2" | ||||
| -- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter         | ||||
| -- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter | ||||
| -- | ||||
| -- >>> _ptgen "yearly from 2017/1/14" | ||||
| -- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year         | ||||
| -- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year | ||||
| -- | ||||
| -- >>> let reportperiod="daily from 2018/01/03" in let (i,s) = parsePeriodExpr' nulldate reportperiod in runPeriodicTransaction (nullperiodictransaction{ptperiodexpr=reportperiod, ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1]}) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03")) | ||||
| -- [] | ||||
| @ -203,28 +203,28 @@ runPeriodicTransaction PeriodicTransaction{..} requestedspan = | ||||
|     t = nulltransaction{ | ||||
|            tstatus      = ptstatus | ||||
|           ,tcode        = ptcode | ||||
|           ,tdescription = ptdescription  | ||||
|           ,tdescription = ptdescription | ||||
|           ,tcomment     = (if T.null ptcomment then "\n" else ptcomment) <> "recur: " <> ptperiodexpr | ||||
|           ,ttags        = ("recur", ptperiodexpr) : pttags  | ||||
|           ,ttags        = ("recur", ptperiodexpr) : pttags | ||||
|           ,tpostings    = ptpostings | ||||
|           } | ||||
| 
 | ||||
| -- | Check that this date span begins at a boundary of this interval,  | ||||
| -- | Check that this date span begins at a boundary of this interval, | ||||
| -- or return an explanatory error message including the provided period expression | ||||
| -- (from which the span and interval are derived). | ||||
| checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String  | ||||
| checkPeriodicTransactionStartDate i s periodexpr =  | ||||
| checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String | ||||
| checkPeriodicTransactionStartDate i s periodexpr = | ||||
|   case (i, spanStart s) of | ||||
|     (Weeks _,    Just d) -> checkStart d "week" | ||||
|     (Months _,   Just d) -> checkStart d "month" | ||||
|     (Quarters _, Just d) -> checkStart d "quarter" | ||||
|     (Years _,    Just d) -> checkStart d "year" | ||||
|     _                    -> Nothing  | ||||
|     _                    -> Nothing | ||||
|     where | ||||
|       checkStart d x = | ||||
|         let firstDate = fixSmartDate d ("","this",x)  | ||||
|         in    | ||||
|          if d == firstDate  | ||||
|         let firstDate = fixSmartDate d ("","this",x) | ||||
|         in | ||||
|          if d == firstDate | ||||
|          then Nothing | ||||
|          else Just $ | ||||
|           "Unable to generate transactions according to "++show (T.unpack periodexpr) | ||||
|  | ||||
| @ -78,7 +78,7 @@ import qualified Data.Text as T | ||||
| import Data.Time.Calendar | ||||
| import Safe | ||||
| 
 | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Data.Amount | ||||
| import Hledger.Data.AccountName | ||||
| @ -221,7 +221,7 @@ postingDate2 p = headDef nulldate $ catMaybes dates | ||||
| -- | Get a posting's status. This is cleared or pending if those are | ||||
| -- explicitly set on the posting, otherwise the status of its parent | ||||
| -- transaction, or unmarked if there is no parent transaction. (Note | ||||
| -- the ambiguity, unmarked can mean "posting and transaction are both  | ||||
| -- the ambiguity, unmarked can mean "posting and transaction are both | ||||
| -- unmarked" or "posting is unmarked and don't know about the transaction". | ||||
| postingStatus :: Posting -> Status | ||||
| postingStatus Posting{pstatus=s, ptransaction=mt} | ||||
|  | ||||
| @ -137,7 +137,7 @@ fieldp = do | ||||
| 
 | ||||
| ---------------------------------------------------------------------- | ||||
| 
 | ||||
| formatStringTester fs value expected = actual `is` expected  | ||||
| formatStringTester fs value expected = actual `is` expected | ||||
|   where | ||||
|     actual = case fs of | ||||
|       FormatLiteral l                   -> formatString False Nothing Nothing l | ||||
|  | ||||
| @ -26,7 +26,7 @@ import System.Locale (defaultTimeLocale) | ||||
| #endif | ||||
| import Text.Printf | ||||
| 
 | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Data.Dates | ||||
| import Hledger.Data.Amount | ||||
| @ -130,10 +130,10 @@ tests_Timeclock = tests "Timeclock" [ | ||||
|                   parseTime defaultTimeLocale "%H:%M:%S" | ||||
| #endif | ||||
|        showtime = formatTime defaultTimeLocale "%H:%M" | ||||
|        txndescs = map (T.unpack . tdescription) . timeclockEntriesToTransactions now  | ||||
|        txndescs = map (T.unpack . tdescription) . timeclockEntriesToTransactions now | ||||
|        future = utcToLocalTime tz $ addUTCTime 100 now' | ||||
|        futurestr = showtime future | ||||
|    tests "timeclockEntriesToTransactions" [  | ||||
|    tests "timeclockEntriesToTransactions" [ | ||||
|      test "started yesterday, split session at midnight" $ | ||||
|       txndescs [clockin (mktime yesterday "23:00:00") "" ""] `is` ["23:00-23:59","00:00-"++nowstr] | ||||
|      ,test "split multi-day sessions at each midnight" $ | ||||
|  | ||||
| @ -62,7 +62,7 @@ import Data.Time.Calendar | ||||
| import Text.Printf | ||||
| import qualified Data.Map as Map | ||||
| 
 | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Data.Types | ||||
| import Hledger.Data.Dates | ||||
| import Hledger.Data.Posting | ||||
| @ -101,7 +101,7 @@ nulltransaction = Transaction { | ||||
|                   } | ||||
| 
 | ||||
| -- | Make a simple transaction with the given date and postings. | ||||
| transaction :: String -> [Posting] -> Transaction  | ||||
| transaction :: String -> [Posting] -> Transaction | ||||
| transaction datestr ps = txnTieKnot $ nulltransaction{tdate=parsedate datestr, tpostings=ps} | ||||
| 
 | ||||
| transactionPayee :: Transaction -> Text | ||||
| @ -122,7 +122,7 @@ payeeAndNoteFromDescription t | ||||
|     (p, n) = T.span (/= '|') t | ||||
| 
 | ||||
| {-| | ||||
| Render a journal transaction as text in the style of Ledger's print command.  | ||||
| Render a journal transaction as text in the style of Ledger's print command. | ||||
| 
 | ||||
| Ledger 2.x's standard format looks like this: | ||||
| 
 | ||||
| @ -139,7 +139,7 @@ pcommentwidth = no limit -- 22 | ||||
| @ | ||||
| 
 | ||||
| The output will be parseable journal syntax. | ||||
| To facilitate this, postings with explicit multi-commodity amounts  | ||||
| To facilitate this, postings with explicit multi-commodity amounts | ||||
| are displayed as multiple similar postings, one per commodity. | ||||
| (Normally does not happen with this function). | ||||
| 
 | ||||
| @ -148,8 +148,8 @@ and the transaction appears obviously balanced | ||||
| (postings sum to 0, without needing to infer conversion prices), | ||||
| the last posting's amount will not be shown. | ||||
| -} | ||||
| -- XXX why that logic ?  | ||||
| -- XXX where is/should this be still used ?  | ||||
| -- XXX why that logic ? | ||||
| -- XXX where is/should this be still used ? | ||||
| -- XXX rename these, after amount expressions/mixed posting amounts lands | ||||
| --     eg showTransactionSimpleAmountsElidingLast, showTransactionSimpleAmounts, showTransaction | ||||
| showTransaction :: Transaction -> String | ||||
| @ -158,19 +158,19 @@ showTransaction = showTransactionHelper True False | ||||
| -- | Like showTransaction, but does not change amounts' explicitness. | ||||
| -- Explicit amounts are shown and implicit amounts are not. | ||||
| -- The output will be parseable journal syntax. | ||||
| -- To facilitate this, postings with explicit multi-commodity amounts  | ||||
| -- To facilitate this, postings with explicit multi-commodity amounts | ||||
| -- are displayed as multiple similar postings, one per commodity. | ||||
| -- Most often, this is the one you want to use. | ||||
| showTransactionUnelided :: Transaction -> String | ||||
| showTransactionUnelided = showTransactionHelper False False | ||||
| 
 | ||||
| -- | Like showTransactionUnelided, but explicit multi-commodity amounts  | ||||
| -- are shown on one line, comma-separated. In this case the output will  | ||||
| -- | Like showTransactionUnelided, but explicit multi-commodity amounts | ||||
| -- are shown on one line, comma-separated. In this case the output will | ||||
| -- not be parseable journal syntax. | ||||
| showTransactionUnelidedOneLineAmounts :: Transaction -> String | ||||
| showTransactionUnelidedOneLineAmounts = showTransactionHelper False True | ||||
| 
 | ||||
| -- | Helper for showTransaction*.  | ||||
| -- | Helper for showTransaction*. | ||||
| showTransactionHelper :: Bool -> Bool -> Transaction -> String | ||||
| showTransactionHelper elide onelineamounts t = | ||||
|     unlines $ [descriptionline] | ||||
| @ -205,7 +205,7 @@ renderCommentLines t = | ||||
| -- for `print` output. Normally this output will be valid journal syntax which | ||||
| -- hledger can reparse (though it may include no-longer-valid balance assertions). | ||||
| -- | ||||
| -- Explicit amounts are shown, any implicit amounts are not.  | ||||
| -- Explicit amounts are shown, any implicit amounts are not. | ||||
| -- | ||||
| -- Setting elide to true forces the last posting's amount to be implicit, if: | ||||
| -- there are other postings, all with explicit amounts, and the transaction | ||||
| @ -215,36 +215,36 @@ renderCommentLines t = | ||||
| -- if onelineamounts is true, these amounts are shown on one line, | ||||
| -- comma-separated, and the output will not be valid journal syntax. | ||||
| -- Otherwise, they are shown as several similar postings, one per commodity. | ||||
| --  | ||||
| -- | ||||
| -- The output will appear to be a balanced transaction. | ||||
| -- Amounts' display precisions, which may have been limited by commodity | ||||
| -- directives, will be increased if necessary to ensure this. | ||||
| -- | ||||
| -- Posting amounts will be aligned with each other, starting about 4 columns | ||||
| -- beyond the widest account name (see postingAsLines for details). | ||||
| --  | ||||
| -- | ||||
| postingsAsLines :: Bool -> Bool -> Transaction -> [Posting] -> [String] | ||||
| postingsAsLines elide onelineamounts t ps | ||||
|   | elide && length ps > 1 && all hasAmount ps && isTransactionBalanced Nothing t -- imprecise balanced check | ||||
|    = concatMap (postingAsLines False onelineamounts ps) (init ps) ++ postingAsLines True onelineamounts ps (last ps) | ||||
|   | otherwise = concatMap (postingAsLines False onelineamounts ps) ps | ||||
| 
 | ||||
| -- | Render one posting, on one or more lines, suitable for `print` output.   | ||||
| -- | Render one posting, on one or more lines, suitable for `print` output. | ||||
| -- There will be an indented account name, plus one or more of status flag, | ||||
| -- posting amount, balance assertion, same-line comment, next-line comments. | ||||
| --  | ||||
| -- | ||||
| -- If the posting's amount is implicit or if elideamount is true, no amount is shown. | ||||
| -- | ||||
| -- If the posting's amount is explicit and multi-commodity, multiple similar  | ||||
| -- If the posting's amount is explicit and multi-commodity, multiple similar | ||||
| -- postings are shown, one for each commodity, to help produce parseable journal syntax. | ||||
| -- Or if onelineamounts is true, such amounts are shown on one line, comma-separated | ||||
| -- (and the output will not be valid journal syntax). | ||||
| -- | ||||
| -- By default, 4 spaces (2 if there's a status flag) are shown between  | ||||
| -- By default, 4 spaces (2 if there's a status flag) are shown between | ||||
| -- account name and start of amount area, which is typically 12 chars wide | ||||
| -- and contains a right-aligned amount (so 10-12 visible spaces between  | ||||
| -- and contains a right-aligned amount (so 10-12 visible spaces between | ||||
| -- account name and amount is typical). | ||||
| -- When given a list of postings to be aligned with, the whitespace will be  | ||||
| -- When given a list of postings to be aligned with, the whitespace will be | ||||
| -- increased if needed to match the posting with the longest account name. | ||||
| -- This is used to align the amounts of a transaction's postings. | ||||
| -- | ||||
| @ -255,10 +255,10 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [ | ||||
|     | postingblock <- postingblocks] | ||||
|   where | ||||
|     postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, "  ", amount, assertion, samelinecomment] | amount <- shownAmounts] | ||||
|     assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p  | ||||
|     assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p | ||||
|     statusandaccount = lineIndent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p | ||||
|         where | ||||
|           -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned   | ||||
|           -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned | ||||
|           minwidth = maximum $ map ((2+) . textWidth . T.pack . pacctstr) pstoalignwith | ||||
|           pstatusandacct p' = pstatusprefix p' ++ pacctstr p' | ||||
|           pstatusprefix p' | null s    = "" | ||||
| @ -279,8 +279,8 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [ | ||||
|       case renderCommentLines (pcomment p) of []   -> ("",[]) | ||||
|                                               c:cs -> (c,cs) | ||||
| 
 | ||||
| -- | Render a balance assertion, as the =[=][*] symbol and expected amount.  | ||||
| showBalanceAssertion BalanceAssertion{..} =  | ||||
| -- | Render a balance assertion, as the =[=][*] symbol and expected amount. | ||||
| showBalanceAssertion BalanceAssertion{..} = | ||||
|   "=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount | ||||
| 
 | ||||
| -- | Render a posting, simply. Used in balance assertion errors. | ||||
| @ -296,7 +296,7 @@ showBalanceAssertion BalanceAssertion{..} = | ||||
| --     assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p | ||||
| 
 | ||||
| -- | Render a posting, at the appropriate width for aligning with | ||||
| -- its siblings if any. Used by the rewrite command.  | ||||
| -- its siblings if any. Used by the rewrite command. | ||||
| showPostingLines :: Posting -> [String] | ||||
| showPostingLines p = postingAsLines False False ps p where | ||||
|     ps | Just t <- ptransaction p = tpostings t | ||||
| @ -366,14 +366,14 @@ isTransactionBalanced styles t = | ||||
|       bvsum' = canonicalise $ costOfMixedAmount bvsum | ||||
|       canonicalise = maybe id canonicaliseMixedAmount styles | ||||
| 
 | ||||
| -- | Balance this transaction, ensuring that its postings  | ||||
| -- | Balance this transaction, ensuring that its postings | ||||
| -- (and its balanced virtual postings) sum to 0, | ||||
| -- by inferring a missing amount or conversion price(s) if needed.  | ||||
| -- by inferring a missing amount or conversion price(s) if needed. | ||||
| -- Or if balancing is not possible, because the amounts don't sum to 0 or | ||||
| -- because there's more than one missing amount, return an error message. | ||||
| -- | ||||
| -- Transactions with balance assignments can have more than one | ||||
| -- missing amount; to balance those you should use the more powerful   | ||||
| -- missing amount; to balance those you should use the more powerful | ||||
| -- journalBalanceTransactions. | ||||
| -- | ||||
| -- The "sum to 0" test is done using commodity display precisions, | ||||
| @ -383,18 +383,18 @@ balanceTransaction :: | ||||
|      Maybe (Map.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles | ||||
|   -> Transaction | ||||
|   -> Either String Transaction | ||||
| balanceTransaction mstyles = fmap fst . balanceTransactionHelper mstyles  | ||||
| balanceTransaction mstyles = fmap fst . balanceTransactionHelper mstyles | ||||
| 
 | ||||
| -- | Helper used by balanceTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB; | ||||
| -- use one of those instead. It also returns a list of accounts  | ||||
| -- use one of those instead. It also returns a list of accounts | ||||
| -- and amounts that were inferred. | ||||
| balanceTransactionHelper :: | ||||
|      Maybe (Map.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles | ||||
|   -> Transaction | ||||
|   -> Either String (Transaction, [(AccountName, MixedAmount)]) | ||||
| balanceTransactionHelper mstyles t = do | ||||
|   (t', inferredamtsandaccts) <-  | ||||
|     inferBalancingAmount (fromMaybe Map.empty mstyles) $ inferBalancingPrices t  | ||||
|   (t', inferredamtsandaccts) <- | ||||
|     inferBalancingAmount (fromMaybe Map.empty mstyles) $ inferBalancingPrices t | ||||
|   if isTransactionBalanced mstyles t' | ||||
|   then Right (txnTieKnot t', inferredamtsandaccts) | ||||
|   else Left $ annotateErrorWithTransaction t' $ nonzerobalanceerror t' | ||||
| @ -413,7 +413,7 @@ balanceTransactionHelper mstyles t = do | ||||
|           sep = if not (null rmsg) && not (null bvmsg) then "; " else "" :: String | ||||
| 
 | ||||
| annotateErrorWithTransaction :: Transaction -> String -> String | ||||
| annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsourcepos t, s, showTransactionUnelided t]     | ||||
| annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsourcepos t, s, showTransactionUnelided t] | ||||
| 
 | ||||
| -- | Infer up to one missing amount for this transactions's real postings, and | ||||
| -- likewise for its balanced virtual postings, if needed; or return an error | ||||
| @ -423,7 +423,7 @@ annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsou | ||||
| -- We can infer a missing amount when there are multiple postings and exactly | ||||
| -- one of them is amountless. If the amounts had price(s) the inferred amount | ||||
| -- have the same price(s), and will be converted to the price commodity. | ||||
| inferBalancingAmount ::  | ||||
| inferBalancingAmount :: | ||||
|      Map.Map CommoditySymbol AmountStyle -- ^ commodity display styles | ||||
|   -> Transaction | ||||
|   -> Either String (Transaction, [(AccountName, MixedAmount)]) | ||||
| @ -446,16 +446,16 @@ inferBalancingAmount styles t@Transaction{tpostings=ps} | ||||
|     inferamount p = | ||||
|       let | ||||
|         minferredamt = case ptype p of | ||||
|           RegularPosting         | not (hasAmount p) -> Just realsum  | ||||
|           BalancedVirtualPosting | not (hasAmount p) -> Just bvsum  | ||||
|           _                                          -> Nothing  | ||||
|           RegularPosting         | not (hasAmount p) -> Just realsum | ||||
|           BalancedVirtualPosting | not (hasAmount p) -> Just bvsum | ||||
|           _                                          -> Nothing | ||||
|       in | ||||
|         case minferredamt of | ||||
|           Nothing -> (p, Nothing) | ||||
|           Just a  -> (p{pamount=a', poriginal=Just $ originalPosting p}, Just a')  | ||||
|           Just a  -> (p{pamount=a', poriginal=Just $ originalPosting p}, Just a') | ||||
|             where | ||||
|               -- Inferred amounts are converted to cost. | ||||
|               -- Also ensure the new amount has the standard style for its commodity   | ||||
|               -- Also ensure the new amount has the standard style for its commodity | ||||
|               -- (since the main amount styling pass happened before this balancing pass); | ||||
|               a' = styleMixedAmount styles $ normaliseMixedAmount $ costOfMixedAmount (-a) | ||||
| 
 | ||||
| @ -613,7 +613,7 @@ tests_Transaction = | ||||
|               ] | ||||
|         ] | ||||
|    -- postingsAsLines | ||||
|     -- one implicit amount  | ||||
|     -- one implicit amount | ||||
|     , let timp = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` missingamt]} | ||||
|     -- explicit amounts, balanced | ||||
|           texp = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` usd (-1)]} | ||||
| @ -659,7 +659,7 @@ tests_Transaction = | ||||
|             , test "one-explicit-amount-elide-true" $ | ||||
|               let t = texp1 | ||||
|                in postingsAsLines True False t (tpostings t) `is` | ||||
|                   [ "    (a)           $1.00" -- explicit amount remains explicit since only one posting  | ||||
|                   [ "    (a)           $1.00" -- explicit amount remains explicit since only one posting | ||||
|                   ] | ||||
|             , test "explicit-amounts-two-commodities-elide-true" $ | ||||
|               let t = texp2 | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| {-| | ||||
| 
 | ||||
| A 'TransactionModifier' is a rule that modifies certain 'Transaction's, | ||||
| typically adding automated postings to them.  | ||||
| typically adding automated postings to them. | ||||
| 
 | ||||
| -} | ||||
| module Hledger.Data.TransactionModifier ( | ||||
| @ -41,7 +41,7 @@ modifyTransactions tmods = map applymods | ||||
| -- | Converts a 'TransactionModifier' to a 'Transaction'-transforming function, | ||||
| -- which applies the modification(s) specified by the TransactionModifier. | ||||
| -- Currently this means adding automated postings when certain other postings are present. | ||||
| -- The postings of the transformed transaction will reference it in the usual  | ||||
| -- The postings of the transformed transaction will reference it in the usual | ||||
| -- way (ie, 'txnTieKnot' is called). | ||||
| -- | ||||
| -- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]} | ||||
| @ -60,16 +60,16 @@ modifyTransactions tmods = map applymods | ||||
| -- <BLANKLINE> | ||||
| -- | ||||
| transactionModifierToFunction :: TransactionModifier -> (Transaction -> Transaction) | ||||
| transactionModifierToFunction mt =  | ||||
| transactionModifierToFunction mt = | ||||
|   \t@(tpostings -> ps) -> txnTieKnot t{ tpostings=generatePostings ps } -- TODO add modifier txn comment/tags ? | ||||
|   where | ||||
|     q = simplifyQuery $ tmParseQuery mt (error' "a transaction modifier's query cannot depend on current date") | ||||
|     mods = map tmPostingRuleToFunction $ tmpostingrules mt | ||||
|     generatePostings ps = [p' | p <- ps | ||||
|                               , p' <- if q `matchesPosting` p then p:[ m p | m <- mods] else [p]] | ||||
|      | ||||
| -- | Parse the 'Query' from a 'TransactionModifier's 'tmquerytxt',  | ||||
| -- and return it as a function requiring the current date.  | ||||
| 
 | ||||
| -- | Parse the 'Query' from a 'TransactionModifier's 'tmquerytxt', | ||||
| -- and return it as a function requiring the current date. | ||||
| -- | ||||
| -- >>> tmParseQuery (TransactionModifier "" []) undefined | ||||
| -- Any | ||||
| @ -85,9 +85,9 @@ tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt) | ||||
| -- | Converts a 'TransactionModifier''s posting rule to a 'Posting'-generating function, | ||||
| -- which will be used to make a new posting based on the old one (an "automated posting"). | ||||
| -- The new posting's amount can optionally be the old posting's amount multiplied by a constant. | ||||
| -- If the old posting had a total-priced amount, the new posting's multiplied amount will be unit-priced.  | ||||
| -- If the old posting had a total-priced amount, the new posting's multiplied amount will be unit-priced. | ||||
| tmPostingRuleToFunction :: TMPostingRule -> (Posting -> Posting) | ||||
| tmPostingRuleToFunction pr =  | ||||
| tmPostingRuleToFunction pr = | ||||
|   \p -> renderPostingCommentDates $ pr | ||||
|       { pdate = pdate p | ||||
|       , pdate2 = pdate2 p | ||||
| @ -103,15 +103,15 @@ tmPostingRuleToFunction pr = | ||||
|             matchedamount = dbg6 "matchedamount" $ pamount p | ||||
|             -- Handle a matched amount with a total price carefully so as to keep the transaction balanced (#928). | ||||
|             -- Approach 1: convert to a unit price and increase the display precision slightly | ||||
|             -- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount  | ||||
|             -- Approach 2: multiply the total price (keeping it positive) as well as the quantity  | ||||
|             Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount  | ||||
|             -- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount | ||||
|             -- Approach 2: multiply the total price (keeping it positive) as well as the quantity | ||||
|             Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount | ||||
|           in | ||||
|             case acommodity pramount of | ||||
|               "" -> Mixed as | ||||
|               -- TODO multipliers with commodity symbols are not yet a documented feature. | ||||
|               -- For now: in addition to multiplying the quantity, it also replaces the  | ||||
|               -- matched amount's commodity, display style, and price with those of the posting rule.    | ||||
|               -- For now: in addition to multiplying the quantity, it also replaces the | ||||
|               -- matched amount's commodity, display style, and price with those of the posting rule. | ||||
|               c  -> Mixed [a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount} | a <- as] | ||||
| 
 | ||||
| postingRuleMultiplier :: TMPostingRule -> Maybe Quantity | ||||
|  | ||||
| @ -30,7 +30,7 @@ import Data.Functor (($>)) | ||||
| import Data.Graph.Inductive (Gr,Node,NodeMap) | ||||
| import Data.List (intercalate) | ||||
| import Text.Blaze (ToMarkup(..)) | ||||
| --XXX https://hackage.haskell.org/package/containers/docs/Data-Map.html  | ||||
| --XXX https://hackage.haskell.org/package/containers/docs/Data-Map.html | ||||
| --Note: You should use Data.Map.Strict instead of this module if: | ||||
| --You will eventually need all the values stored. | ||||
| --The stored values don't represent large virtual data structures to be lazily computed. | ||||
| @ -158,7 +158,7 @@ instance ToMarkup Quantity | ||||
| -- | An amount's per-unit or total cost/selling price in another | ||||
| -- commodity, as recorded in the journal entry eg with @ or @@. | ||||
| -- Docs call this "transaction price". The amount is always positive. | ||||
| data AmountPrice = UnitPrice Amount | TotalPrice Amount  | ||||
| data AmountPrice = UnitPrice Amount | TotalPrice Amount | ||||
|   deriving (Eq,Ord,Typeable,Data,Generic,Show) | ||||
| 
 | ||||
| instance NFData AmountPrice | ||||
| @ -301,7 +301,7 @@ data Posting = Posting { | ||||
|                                                     --   Tying this knot gets tedious, Maybe makes it easier/optional. | ||||
|       poriginal         :: Maybe Posting            -- ^ When this posting has been transformed in some way | ||||
|                                                     --   (eg its amount or price was inferred, or the account name was | ||||
|                                                     --   changed by a pivot or budget report), this references the original  | ||||
|                                                     --   changed by a pivot or budget report), this references the original | ||||
|                                                     --   untransformed posting (which will have Nothing in this field). | ||||
|     } deriving (Typeable,Data,Generic) | ||||
| 
 | ||||
| @ -358,10 +358,10 @@ data Transaction = Transaction { | ||||
| instance NFData Transaction | ||||
| 
 | ||||
| -- | A transaction modifier rule. This has a query which matches postings | ||||
| -- in the journal, and a list of transformations to apply to those  | ||||
| -- in the journal, and a list of transformations to apply to those | ||||
| -- postings or their transactions. Currently there is one kind of transformation: | ||||
| -- the TMPostingRule, which adds a posting ("auto posting") to the transaction,  | ||||
| -- optionally setting its amount to the matched posting's amount multiplied by a constant.  | ||||
| -- the TMPostingRule, which adds a posting ("auto posting") to the transaction, | ||||
| -- optionally setting its amount to the matched posting's amount multiplied by a constant. | ||||
| data TransactionModifier = TransactionModifier { | ||||
|       tmquerytxt :: Text, | ||||
|       tmpostingrules :: [TMPostingRule] | ||||
| @ -383,8 +383,8 @@ type TMPostingRule = Posting | ||||
| -- | A periodic transaction rule, describing a transaction that recurs. | ||||
| data PeriodicTransaction = PeriodicTransaction { | ||||
|       ptperiodexpr   :: Text,     -- ^ the period expression as written | ||||
|       ptinterval     :: Interval, -- ^ the interval at which this transaction recurs  | ||||
|       ptspan         :: DateSpan, -- ^ the (possibly unbounded) period during which this transaction recurs. Contains a whole number of intervals.  | ||||
|       ptinterval     :: Interval, -- ^ the interval at which this transaction recurs | ||||
|       ptspan         :: DateSpan, -- ^ the (possibly unbounded) period during which this transaction recurs. Contains a whole number of intervals. | ||||
|       -- | ||||
|       ptstatus       :: Status,   -- ^ some of Transaction's fields | ||||
|       ptcode         :: Text, | ||||
| @ -496,8 +496,8 @@ data Journal = Journal { | ||||
|   ,jparsetimeclockentries :: [TimeclockEntry]                       -- ^ timeclock sessions which have not been clocked out | ||||
|   ,jincludefilestack      :: [FilePath] | ||||
|   -- principal data | ||||
|   ,jdeclaredaccounts      :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)  | ||||
|   ,jdeclaredaccounttypes  :: M.Map AccountType [AccountName]        -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts)  | ||||
|   ,jdeclaredaccounts      :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation) | ||||
|   ,jdeclaredaccounttypes  :: M.Map AccountType [AccountName]        -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts) | ||||
|   ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives | ||||
|   ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts  TODO misnamed - jusedstyles | ||||
|   ,jpricedirectives       :: [PriceDirective]                       -- ^ All market price declarations (P directives), in parse order (after journal finalisation). | ||||
| @ -558,12 +558,12 @@ data Account = Account { | ||||
|   ,aibalance                 :: MixedAmount    -- ^ this account's balance, including subaccounts | ||||
|   } deriving (Typeable, Data, Generic) | ||||
| 
 | ||||
| -- | Whether an account's balance is normally a positive number (in  | ||||
| -- accounting terms, a debit balance) or a negative number (credit balance).  | ||||
| -- | Whether an account's balance is normally a positive number (in | ||||
| -- accounting terms, a debit balance) or a negative number (credit balance). | ||||
| -- Assets and expenses are normally positive (debit), while liabilities, equity | ||||
| -- and income are normally negative (credit). | ||||
| -- https://en.wikipedia.org/wiki/Normal_balance | ||||
| data NormalSign = NormallyPositive | NormallyNegative deriving (Show, Data, Eq)  | ||||
| data NormalSign = NormallyPositive | NormallyNegative deriving (Show, Data, Eq) | ||||
| 
 | ||||
| -- | A Ledger has the journal it derives from, and the accounts | ||||
| -- derived from that. Accounts are accessible both list-wise and | ||||
|  | ||||
| @ -46,9 +46,9 @@ tests_Valuation = tests "Valuation" [ | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| -- Valuation | ||||
|                       | ||||
| 
 | ||||
| -- Apply a specified valuation to this mixed amount, using the provided | ||||
| -- prices db, commodity styles, period-end/current dates,  | ||||
| -- prices db, commodity styles, period-end/current dates, | ||||
| -- and whether this is for a multiperiod report or not. | ||||
| mixedAmountApplyValuation :: [PriceDirective] -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> ValuationType -> MixedAmount -> MixedAmount | ||||
| mixedAmountApplyValuation prices styles periodend today ismultiperiod v (Mixed as) = | ||||
| @ -63,7 +63,7 @@ mixedAmountValueAtDate :: [PriceDirective] -> M.Map CommoditySymbol AmountStyle | ||||
| mixedAmountValueAtDate prices styles mc d (Mixed as) = Mixed $ map (amountValueAtDate prices styles mc d) as | ||||
| 
 | ||||
| -- | Apply a specified valuation to this amount, using the provided | ||||
| -- prices db, commodity styles, period-end/current dates,  | ||||
| -- prices db, commodity styles, period-end/current dates, | ||||
| -- and whether this is for a multiperiod report or not. | ||||
| amountApplyValuation :: [PriceDirective] -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> ValuationType -> Amount -> Amount | ||||
| amountApplyValuation prices styles periodend today ismultiperiod v a = | ||||
| @ -101,7 +101,7 @@ amountValueAtDate pricedirectives styles mto d a = | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| -- Building a price graph | ||||
|                       | ||||
| 
 | ||||
| -- | Convert a list of market price directives in parse order to a | ||||
| -- graph of all prices in effect on a given day, allowing efficient | ||||
| -- lookup of exchange rates between commodity pairs. | ||||
| @ -148,7 +148,7 @@ marketPriceReverse mp@MarketPrice{..} = mp{mpfrom=mpto, mpto=mpfrom, mprate=1/mp | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| -- Market price lookup | ||||
|                       | ||||
| 
 | ||||
| tests_priceLookup = | ||||
|   let | ||||
|     d = parsedate | ||||
| @ -214,7 +214,7 @@ priceLookup pricedirectives d from mto = | ||||
|       where | ||||
|         -- If to is unspecified, try to pick a default valuation commodity from declared prices (only). | ||||
|         -- XXX how to choose ? Take lowest sorted ? | ||||
|         -- Take first, hoping current order is useful ?       <-  | ||||
|         -- Take first, hoping current order is useful ?       <- | ||||
|         -- Keep parse order in label and take latest parsed ? | ||||
|         mdefaultto = | ||||
|           dbg4 ("default valuation commodity for "++T.unpack from) $ | ||||
| @ -257,7 +257,7 @@ node m = fst . fst . mkNode m | ||||
| pathEdgeLabels :: (Show b, Ord b) => Gr a b -> [Node] -> [b] | ||||
| pathEdgeLabels g = map frommaybe . map (nodesEdgeLabel g) . pathEdges | ||||
|   where frommaybe = fromMaybe (error' "pathEdgeLabels: expected no Nothings here") | ||||
|    | ||||
| 
 | ||||
| -- | Convert a path to node pairs representing the path's edges. | ||||
| pathEdges :: [Node] -> [(Node,Node)] | ||||
| pathEdges p = [(f,t) | f:t:_ <- tails p] | ||||
|  | ||||
| @ -654,7 +654,7 @@ matchesPriceDirective _ _           = True | ||||
| 
 | ||||
| tests_Query = tests "Query" [ | ||||
|    tests "simplifyQuery" [ | ||||
|      | ||||
| 
 | ||||
|      (simplifyQuery $ Or [Acct "a"])      `is` (Acct "a") | ||||
|     ,(simplifyQuery $ Or [Any,None])      `is` (Any) | ||||
|     ,(simplifyQuery $ And [Any,None])     `is` (None) | ||||
| @ -665,7 +665,7 @@ tests_Query = tests "Query" [ | ||||
|       `is` (Date (DateSpan (Just $ parsedate "2012-01-01") (Just $ parsedate "2013-01-01"))) | ||||
|     ,(simplifyQuery $ And [Or [],Or [Desc "b b"]]) `is` (Desc "b b") | ||||
|    ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "parseQuery" [ | ||||
|      (parseQuery nulldate "acct:'expenses:autres d\233penses' desc:b") `is` (And [Acct "expenses:autres d\233penses", Desc "b"], []) | ||||
|     ,parseQuery nulldate "inacct:a desc:\"b b\""                     `is` (Desc "b b", [QueryOptInAcct "a"]) | ||||
| @ -674,18 +674,18 @@ tests_Query = tests "Query" [ | ||||
|     ,parseQuery nulldate "'a a' 'b"                                  `is` (Or [Acct "a a",Acct "'b"], []) | ||||
|     ,parseQuery nulldate "\""                                        `is` (Acct "\"", []) | ||||
|    ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "words''" [ | ||||
|       (words'' [] "a b")                   `is` ["a","b"]         | ||||
|     , (words'' [] "'a b'")                 `is` ["a b"]           | ||||
|     , (words'' [] "not:a b")               `is` ["not:a","b"]     | ||||
|     , (words'' [] "not:'a b'")             `is` ["not:a b"]       | ||||
|     , (words'' [] "'not:a b'")             `is` ["not:a b"]       | ||||
|     , (words'' ["desc:"] "not:desc:'a b'") `is` ["not:desc:a b"]  | ||||
|       (words'' [] "a b")                   `is` ["a","b"] | ||||
|     , (words'' [] "'a b'")                 `is` ["a b"] | ||||
|     , (words'' [] "not:a b")               `is` ["not:a","b"] | ||||
|     , (words'' [] "not:'a b'")             `is` ["not:a b"] | ||||
|     , (words'' [] "'not:a b'")             `is` ["not:a b"] | ||||
|     , (words'' ["desc:"] "not:desc:'a b'") `is` ["not:desc:a b"] | ||||
|     , (words'' prefixes "\"acct:expenses:autres d\233penses\"") `is` ["acct:expenses:autres d\233penses"] | ||||
|     , (words'' prefixes "\"")              `is` ["\""] | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "filterQuery" [ | ||||
|      filterQuery queryIsDepth Any       `is` Any | ||||
|     ,filterQuery queryIsDepth (Depth 1) `is` Depth 1 | ||||
| @ -714,7 +714,7 @@ tests_Query = tests "Query" [ | ||||
|     ,parseQueryTerm nulldate "amt:<0"                           `is` (Left $ Amt Lt 0) | ||||
|     ,parseQueryTerm nulldate "amt:>10000.10"                    `is` (Left $ Amt AbsGt 10000.1) | ||||
|    ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "parseAmountQueryTerm" [ | ||||
|      parseAmountQueryTerm "<0"        `is` (Lt,0) -- special case for convenience, since AbsLt 0 would be always false | ||||
|     ,parseAmountQueryTerm ">0"        `is` (Gt,0) -- special case for convenience and consistency with above | ||||
| @ -725,7 +725,7 @@ tests_Query = tests "Query" [ | ||||
|     ,parseAmountQueryTerm "-0.23"     `is` (Eq,(-0.23)) | ||||
|     ,_test "number beginning with decimal mark" $ parseAmountQueryTerm "=.23" `is` (AbsEq,0.23)  -- XXX | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "matchesAccount" [ | ||||
|      expect $ (Acct "b:c") `matchesAccount` "a:bb:c:d" | ||||
|     ,expect $ not $ (Acct "^a:b") `matchesAccount` "c:a:b" | ||||
| @ -736,7 +736,7 @@ tests_Query = tests "Query" [ | ||||
|     ,expect $ Date2 nulldatespan `matchesAccount` "a" | ||||
|     ,expect $ not $ (Tag "a" Nothing) `matchesAccount` "a" | ||||
|   ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "matchesPosting" [ | ||||
|      test "positive match on cleared posting status"  $ | ||||
|       expect $ (StatusQ Cleared)  `matchesPosting` nullposting{pstatus=Cleared} | ||||
| @ -766,7 +766,7 @@ tests_Query = tests "Query" [ | ||||
|     ,test "l" $ expect $ (Sym "shekels") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]} | ||||
|     ,test "m" $ expect $ not $ (Sym "shek") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]} | ||||
|   ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "matchesTransaction" [ | ||||
|      expect $ Any `matchesTransaction` nulltransaction | ||||
|     ,expect $ not $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x"} | ||||
|  | ||||
| @ -184,7 +184,7 @@ findReader Nothing (Just path) = | ||||
| -- Combining Journals means concatenating them, basically. | ||||
| -- The parse state resets at the start of each file, which means that | ||||
| -- directives & aliases do not affect subsequent sibling or parent files. | ||||
| -- They do affect included child files though.  | ||||
| -- They do affect included child files though. | ||||
| -- Also the final parse state saved in the Journal does span all files. | ||||
| readJournalFiles :: InputOpts -> [PrefixedFilePath] -> IO (Either String Journal) | ||||
| readJournalFiles iopts = | ||||
| @ -207,7 +207,7 @@ readJournalFiles iopts = | ||||
| -- generation, a rules file for converting CSV data, etc. | ||||
| readJournalFile :: InputOpts -> PrefixedFilePath -> IO (Either String Journal) | ||||
| readJournalFile iopts prefixedfile = do | ||||
|   let  | ||||
|   let | ||||
|     (mfmt, f) = splitReaderPrefix prefixedfile | ||||
|     iopts' = iopts{mformat_=firstJust [mfmt, mformat_ iopts]} | ||||
|   requireJournalFileExists f | ||||
| @ -235,13 +235,13 @@ latestDates = headDef [] . take 1 . group . reverse . sort | ||||
| 
 | ||||
| -- | Remember that these transaction dates were the latest seen when | ||||
| -- reading this journal file. | ||||
| saveLatestDates :: LatestDates -> FilePath -> IO ()  | ||||
| saveLatestDates :: LatestDates -> FilePath -> IO () | ||||
| saveLatestDates dates f = writeFile (latestDatesFileFor f) $ unlines $ map showDate dates | ||||
| 
 | ||||
| -- | What were the latest transaction dates seen the last time this  | ||||
| -- | What were the latest transaction dates seen the last time this | ||||
| -- journal file was read ? If there were multiple transactions on the | ||||
| -- latest date, that number of dates is returned, otherwise just one. | ||||
| -- Or none if no transactions were read, or if latest dates info is not  | ||||
| -- Or none if no transactions were read, or if latest dates info is not | ||||
| -- available for this file. | ||||
| previousLatestDates :: FilePath -> IO LatestDates | ||||
| previousLatestDates f = do | ||||
| @ -299,7 +299,7 @@ readJournal iopts mfile txt = | ||||
| -- | ||||
| -- Try to parse the given text to a Journal using each reader in turn, | ||||
| -- returning the first success, or if all of them fail, the first error message. | ||||
| --     | ||||
| -- | ||||
| -- Input ioptions (@iopts@) specify CSV conversion rules file to help convert CSV data, | ||||
| -- enable or disable balance assertion checking and automated posting generation. | ||||
| -- | ||||
|  | ||||
| @ -163,12 +163,12 @@ data InputOpts = InputOpts { | ||||
|     ,mrules_file_       :: Maybe FilePath       -- ^ a conversion rules file to use (when reading CSV) | ||||
|     ,separator_         :: Char                 -- ^ the separator to use (when reading CSV) | ||||
|     ,aliases_           :: [String]             -- ^ account name aliases to apply | ||||
|     ,anon_              :: Bool                 -- ^ do light anonymisation/obfuscation of the data  | ||||
|     ,anon_              :: Bool                 -- ^ do light anonymisation/obfuscation of the data | ||||
|     ,ignore_assertions_ :: Bool                 -- ^ don't check balance assertions | ||||
|     ,new_               :: Bool                 -- ^ read only new transactions since this file was last read | ||||
|     ,new_save_          :: Bool                 -- ^ save latest new transactions state for next time | ||||
|     ,pivot_             :: String               -- ^ use the given field's value as the account name  | ||||
|     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed      | ||||
|     ,pivot_             :: String               -- ^ use the given field's value as the account name | ||||
|     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed | ||||
|  } deriving (Show, Data) --, Typeable) | ||||
| 
 | ||||
| instance Default InputOpts where def = definputopts | ||||
| @ -188,7 +188,7 @@ rawOptsToInputOpts rawopts = InputOpts{ | ||||
|   ,new_               = boolopt "new" rawopts | ||||
|   ,new_save_          = True | ||||
|   ,pivot_             = stringopt "pivot" rawopts | ||||
|   ,auto_              = boolopt "auto" rawopts                         | ||||
|   ,auto_              = boolopt "auto" rawopts | ||||
|   } | ||||
| 
 | ||||
| --- * parsing utilities | ||||
| @ -219,7 +219,7 @@ rejp = runErroringJournalParser | ||||
| genericSourcePos :: SourcePos -> GenericSourcePos | ||||
| genericSourcePos p = GenericSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p) (fromIntegral . unPos $ sourceColumn p) | ||||
| 
 | ||||
| -- | Construct a generic start & end line parse position from start and end megaparsec SourcePos's.  | ||||
| -- | Construct a generic start & end line parse position from start and end megaparsec SourcePos's. | ||||
| journalSourcePos :: SourcePos -> SourcePos -> GenericSourcePos | ||||
| journalSourcePos p p' = JournalSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p, fromIntegral $ line') | ||||
|     where line' | ||||
| @ -355,7 +355,7 @@ getAmountStyle commodity = do | ||||
|     return effectiveStyle | ||||
| 
 | ||||
| addDeclaredAccountType :: AccountName -> AccountType -> JournalParser m () | ||||
| addDeclaredAccountType acct atype =  | ||||
| addDeclaredAccountType acct atype = | ||||
|   modify' (\j -> j{jdeclaredaccounttypes = M.insertWith (++) atype [acct] (jdeclaredaccounttypes j)}) | ||||
| 
 | ||||
| pushParentAccount :: AccountName -> JournalParser m () | ||||
| @ -542,7 +542,7 @@ secondarydatep primaryDate = char '=' *> datep' (Just primaryYear) | ||||
| 
 | ||||
| --- ** account names | ||||
| 
 | ||||
| -- | Parse an account name (plus one following space if present),  | ||||
| -- | Parse an account name (plus one following space if present), | ||||
| -- then apply any parent account prefix and/or account aliases currently in effect, | ||||
| -- in that order. (Ie first add the parent account prefix, then rewrite with aliases). | ||||
| modifiedaccountnamep :: JournalParser m AccountName | ||||
| @ -556,9 +556,9 @@ modifiedaccountnamep = do | ||||
|     joinAccountNames parent | ||||
|     a | ||||
| 
 | ||||
| -- | Parse an account name, plus one following space if present.  | ||||
| -- | Parse an account name, plus one following space if present. | ||||
| -- Account names have one or more parts separated by the account separator character, | ||||
| -- and are terminated by two or more spaces (or end of input).  | ||||
| -- and are terminated by two or more spaces (or end of input). | ||||
| -- Each part is at least one character long, may have single spaces inside it, | ||||
| -- and starts with a non-whitespace. | ||||
| -- Note, this means "{account}", "%^!" and ";comment" are all accepted | ||||
| @ -791,7 +791,7 @@ exponentp = char' 'e' *> signp <*> decimal <?> "exponent" | ||||
| -- | ||||
| -- Returns: | ||||
| -- - the decimal number | ||||
| -- - the precision (number of digits after the decimal point)   | ||||
| -- - the precision (number of digits after the decimal point) | ||||
| -- - the decimal point character, if any | ||||
| -- - the digit group style, if any (digit group character and sizes of digit groups) | ||||
| fromRawNumber | ||||
| @ -811,7 +811,7 @@ fromRawNumber raw mExp = case raw of | ||||
|     in  Right (quantity, precision, mDecPt, Nothing) | ||||
| 
 | ||||
|   WithSeparators digitSep digitGrps mDecimals -> case mExp of | ||||
|     Nothing ->  | ||||
|     Nothing -> | ||||
|       let mDecPt = fmap fst mDecimals | ||||
|           decimalGrp = maybe mempty snd mDecimals | ||||
|           digitGroupStyle = DigitGroups digitSep (groupSizes digitGrps) | ||||
| @ -1038,7 +1038,7 @@ followingcommentp' contentp = do | ||||
|     -- if there's just a next-line comment, insert an empty same-line comment | ||||
|     -- so the next-line comment doesn't get rendered as a same-line comment. | ||||
|     sameLine' | null sameLine && not (null nextLines) = [("",mempty)] | ||||
|               | otherwise = sameLine  | ||||
|               | otherwise = sameLine | ||||
|     (texts, contents) = unzip $ sameLine' ++ nextLines | ||||
|     strippedCommentText = T.unlines $ map T.strip texts | ||||
|     commentContent = mconcat contents | ||||
| @ -1306,32 +1306,32 @@ tests_Common = tests "Common" [ | ||||
|    tests "amountp" [ | ||||
|     test "basic"                  $ expectParseEq amountp "$47.18"     (usd 47.18) | ||||
|    ,test "ends with decimal mark" $ expectParseEq amountp "$1."        (usd 1  `withPrecision` 0) | ||||
|    ,test "unit price"             $ expectParseEq amountp "$10 @ €0.5"  | ||||
|    ,test "unit price"             $ expectParseEq amountp "$10 @ €0.5" | ||||
|       -- not precise enough: | ||||
|       -- (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1)) -- `withStyle` asdecimalpoint=Just '.' | ||||
|       amount{ | ||||
|          acommodity="$" | ||||
|         ,aquantity=10 -- need to test internal precision with roundTo ? I think not  | ||||
|         ,aquantity=10 -- need to test internal precision with roundTo ? I think not | ||||
|         ,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing} | ||||
|         ,aprice=Just $ UnitPrice $ | ||||
|           amount{ | ||||
|              acommodity="€" | ||||
|             ,aquantity=0.5 | ||||
|             ,astyle=amountstyle{asprecision=1, asdecimalpoint=Just '.'} | ||||
|             }  | ||||
|         }  | ||||
|             } | ||||
|         } | ||||
|    ,test "total price"            $ expectParseEq amountp "$10 @@ €5" | ||||
|       amount{ | ||||
|          acommodity="$" | ||||
|         ,aquantity=10  | ||||
|         ,aquantity=10 | ||||
|         ,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing} | ||||
|         ,aprice=Just $ TotalPrice $ | ||||
|           amount{ | ||||
|              acommodity="€" | ||||
|             ,aquantity=5 | ||||
|             ,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing} | ||||
|             }  | ||||
|         }  | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| 
 | ||||
|   ,let p = lift (numberp Nothing) :: JournalParser IO (Quantity, Int, Maybe Char, Maybe DigitGroupStyle) in | ||||
| @ -1355,7 +1355,7 @@ tests_Common = tests "Common" [ | ||||
|     ,test "." $ expectParseError p ".1," "" | ||||
|     ,test "." $ expectParseError p ",1." "" | ||||
|     ] | ||||
|    | ||||
| 
 | ||||
|   ,tests "spaceandamountormissingp" [ | ||||
|      test "space and amount" $ expectParseEq spaceandamountormissingp " $47.18" (Mixed [usd 47.18]) | ||||
|     ,test "empty string" $ expectParseEq spaceandamountormissingp "" missingmixedamt | ||||
|  | ||||
| @ -107,9 +107,9 @@ reader = Reader | ||||
| parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal | ||||
| parse iopts = parseAndFinaliseJournal journalp' iopts | ||||
|   where | ||||
|     journalp' = do  | ||||
|     journalp' = do | ||||
|       -- reverse parsed aliases to ensure that they are applied in order given on commandline | ||||
|       mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts)  | ||||
|       mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts) | ||||
|       journalp | ||||
| 
 | ||||
| -- | Get the account name aliases from options, if any. | ||||
| @ -267,12 +267,12 @@ accountdirectivep = do | ||||
|   -- maybe an account type code (ALERX) after two or more spaces | ||||
|   -- XXX added in 1.11, deprecated in 1.13, remove in 1.14 | ||||
|   mtypecode :: Maybe Char <- lift $ optional $ try $ do | ||||
|     skipSome spacenonewline -- at least one more space in addition to the one consumed by modifiedaccountp  | ||||
|     skipSome spacenonewline -- at least one more space in addition to the one consumed by modifiedaccountp | ||||
|     choice $ map char "ALERX" | ||||
| 
 | ||||
|   -- maybe a comment, on this and/or following lines | ||||
|   (cmt, tags) <- lift transactioncommentp | ||||
|    | ||||
| 
 | ||||
|   -- maybe Ledger-style subdirectives (ignored) | ||||
|   skipMany indentedlinep | ||||
| 
 | ||||
| @ -386,7 +386,7 @@ formatdirectivep expectedsym = do | ||||
|   Amount{acommodity,astyle} <- amountp | ||||
|   _ <- lift followingcommentp | ||||
|   if acommodity==expectedsym | ||||
|     then  | ||||
|     then | ||||
|       if asdecimalpoint astyle == Nothing | ||||
|       then customFailure $ parseErrorAt off pleaseincludedecimalpoint | ||||
|       else return $ dbg2 "style from format subdirective" astyle | ||||
| @ -532,7 +532,7 @@ transactionmodifierp = do | ||||
| -- | Parse a periodic transaction | ||||
| -- | ||||
| -- This reuses periodexprp which parses period expressions on the command line. | ||||
| -- This is awkward because periodexprp supports relative and partial dates,  | ||||
| -- This is awkward because periodexprp supports relative and partial dates, | ||||
| -- which we don't really need here, and it doesn't support the notion of a | ||||
| -- default year set by a Y directive, which we do need to consider here. | ||||
| -- We resolve it as follows: in periodic transactions' period expressions, | ||||
| @ -546,12 +546,12 @@ periodictransactionp = do | ||||
|   lift $ skipMany spacenonewline | ||||
|   -- a period expression | ||||
|   off <- getOffset | ||||
|    | ||||
| 
 | ||||
|   -- if there's a default year in effect, use Y/1/1 as base for partial/relative dates | ||||
|   today <- liftIO getCurrentDay | ||||
|   mdefaultyear <- getYear | ||||
|   let refdate = case mdefaultyear of | ||||
|                   Nothing -> today  | ||||
|                   Nothing -> today | ||||
|                   Just y  -> fromGregorian y 1 1 | ||||
|   periodExcerpt <- lift $ excerpt_ $ | ||||
|                     singlespacedtextsatisfyingp (\c -> c /= ';' && c /= '\n') | ||||
| @ -576,7 +576,7 @@ periodictransactionp = do | ||||
|   case checkPeriodicTransactionStartDate interval span periodtxt of | ||||
|     Just e -> customFailure $ parseErrorAt off e | ||||
|     Nothing -> pure () | ||||
|    | ||||
| 
 | ||||
|   status <- lift statusp <?> "cleared status" | ||||
|   code <- lift codep <?> "transaction code" | ||||
|   description <- lift $ T.strip <$> descriptionp | ||||
| @ -678,7 +678,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|     test "YYYY-MM-DD" $ expectParse datep "2018-01-01" | ||||
|     test "YYYY.MM.DD" $ expectParse datep "2018.01.01" | ||||
|     test "yearless date with no default year" $ expectParseError datep "1/1" "current year is unknown" | ||||
|     test "yearless date with default year" $ do  | ||||
|     test "yearless date with default year" $ do | ||||
|       let s = "1/1" | ||||
|       ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep s | ||||
|       either (fail.("parse error at "++).customErrorBundlePretty) (const ok) ep | ||||
| @ -703,7 +703,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|   ,tests "periodictransactionp" [ | ||||
| 
 | ||||
|     test "more period text in comment after one space" $ expectParseEq periodictransactionp | ||||
|       "~ monthly from 2018/6 ;In 2019 we will change this\n"  | ||||
|       "~ monthly from 2018/6 ;In 2019 we will change this\n" | ||||
|       nullperiodictransaction { | ||||
|          ptperiodexpr  = "monthly from 2018/6" | ||||
|         ,ptinterval    = Months 1 | ||||
| @ -713,7 +713,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|         } | ||||
| 
 | ||||
|     ,test "more period text in description after two spaces" $ expectParseEq periodictransactionp | ||||
|       "~ monthly from 2018/6   In 2019 we will change this\n"  | ||||
|       "~ monthly from 2018/6   In 2019 we will change this\n" | ||||
|       nullperiodictransaction { | ||||
|          ptperiodexpr  = "monthly from 2018/6" | ||||
|         ,ptinterval    = Months 1 | ||||
| @ -748,16 +748,16 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|     ] | ||||
| 
 | ||||
|   ,tests "postingp" [ | ||||
|      test "basic" $ expectParseEq (postingp Nothing)  | ||||
|      test "basic" $ expectParseEq (postingp Nothing) | ||||
|       "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n" | ||||
|       posting{ | ||||
|         paccount="expenses:food:dining",  | ||||
|         pamount=Mixed [usd 10],  | ||||
|         pcomment="a: a a\nb: b b\n",  | ||||
|         paccount="expenses:food:dining", | ||||
|         pamount=Mixed [usd 10], | ||||
|         pcomment="a: a a\nb: b b\n", | ||||
|         ptags=[("a","a a"), ("b","b b")] | ||||
|         } | ||||
| 
 | ||||
|     ,test "posting dates" $ expectParseEq (postingp Nothing)  | ||||
|     ,test "posting dates" $ expectParseEq (postingp Nothing) | ||||
|       " a  1. ; date:2012/11/28, date2=2012/11/29,b:b\n" | ||||
|       nullposting{ | ||||
|          paccount="a" | ||||
| @ -768,14 +768,14 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|         ,pdate2=Nothing  -- Just $ fromGregorian 2012 11 29 | ||||
|         } | ||||
| 
 | ||||
|     ,test "posting dates bracket syntax" $ expectParseEq (postingp Nothing)  | ||||
|     ,test "posting dates bracket syntax" $ expectParseEq (postingp Nothing) | ||||
|       " a  1. ; [2012/11/28=2012/11/29]\n" | ||||
|       nullposting{ | ||||
|          paccount="a" | ||||
|         ,pamount=Mixed [num 1] | ||||
|         ,pcomment="[2012/11/28=2012/11/29]\n" | ||||
|         ,ptags=[] | ||||
|         ,pdate= Just $ fromGregorian 2012 11 28  | ||||
|         ,pdate= Just $ fromGregorian 2012 11 28 | ||||
|         ,pdate2=Just $ fromGregorian 2012 11 29 | ||||
|         } | ||||
| 
 | ||||
| @ -788,7 +788,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
| 
 | ||||
|   ,tests "transactionmodifierp" [ | ||||
| 
 | ||||
|     test "basic" $ expectParseEq transactionmodifierp  | ||||
|     test "basic" $ expectParseEq transactionmodifierp | ||||
|       "= (some value expr)\n some:postings  1.\n" | ||||
|       nulltransactionmodifier { | ||||
|         tmquerytxt = "(some value expr)" | ||||
| @ -797,10 +797,10 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|     ] | ||||
| 
 | ||||
|   ,tests "transactionp" [ | ||||
|    | ||||
| 
 | ||||
|      test "just a date" $ expectParseEq transactionp "2015/1/1\n" nulltransaction{tdate=fromGregorian 2015 1 1} | ||||
|    | ||||
|     ,test "more complex" $ expectParseEq transactionp  | ||||
| 
 | ||||
|     ,test "more complex" $ expectParseEq transactionp | ||||
|       (T.unlines [ | ||||
|         "2012/05/14=2012/05/15 (code) desc  ; tcomment1", | ||||
|         "    ; tcomment2", | ||||
| @ -833,7 +833,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|             } | ||||
|           ] | ||||
|       } | ||||
|    | ||||
| 
 | ||||
|     ,test "parses a well-formed transaction" $ | ||||
|       expect $ isRight $ rjp transactionp $ T.unlines | ||||
|         ["2007/01/28 coopportunity" | ||||
| @ -841,10 +841,10 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|         ,"    assets:checking                          $-47.18" | ||||
|         ,"" | ||||
|         ] | ||||
|    | ||||
| 
 | ||||
|     ,test "does not parse a following comment as part of the description" $ | ||||
|       expectParseEqOn transactionp "2009/1/1 a ;comment\n b 1\n" tdescription "a" | ||||
|    | ||||
| 
 | ||||
|     ,test "transactionp parses a following whitespace line" $ | ||||
|       expect $ isRight $ rjp transactionp $ T.unlines | ||||
|         ["2012/1/1" | ||||
| @ -863,7 +863,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|         ] | ||||
| 
 | ||||
|     ,test "comments everywhere, two postings parsed" $ | ||||
|       expectParseEqOn transactionp  | ||||
|       expectParseEqOn transactionp | ||||
|         (T.unlines | ||||
|           ["2009/1/1 x  ; transaction comment" | ||||
|           ," a  1  ; posting 1 comment" | ||||
| @ -873,13 +873,13 @@ tests_JournalReader = tests "JournalReader" [ | ||||
|           ]) | ||||
|         (length . tpostings) | ||||
|         2 | ||||
|    | ||||
| 
 | ||||
|     ] | ||||
| 
 | ||||
|   -- directives | ||||
| 
 | ||||
|   ,tests "directivep" [ | ||||
|     test "supports !" $ do  | ||||
|     test "supports !" $ do | ||||
|       expectParseE directivep "!account a\n" | ||||
|       expectParseE directivep "!D 1.0\n" | ||||
|     ] | ||||
|  | ||||
| @ -96,7 +96,7 @@ timeclockfilep = do many timeclockitemp | ||||
|       -- As all ledger line types can be distinguished by the first | ||||
|       -- character, excepting transactions versus empty (blank or | ||||
|       -- comment-only) lines, can use choice w/o try | ||||
|       timeclockitemp = choice [  | ||||
|       timeclockitemp = choice [ | ||||
|                             void (lift emptyorcommentlinep) | ||||
|                           , timeclockentryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j}) | ||||
|                           ] <?> "timeclock entry, or default year or historical price directive" | ||||
|  | ||||
| @ -142,16 +142,16 @@ timedotnumericp = do | ||||
|   (q, _, _, _) <- lift $ numberp Nothing | ||||
|   msymbol <- optional $ choice $ map (string . fst) timeUnits | ||||
|   lift (skipMany spacenonewline) | ||||
|   let q' =  | ||||
|   let q' = | ||||
|         case msymbol of | ||||
|           Nothing  -> q | ||||
|           Just sym -> | ||||
|             case lookup sym timeUnits of | ||||
|               Just mult -> q * mult   | ||||
|               Just mult -> q * mult | ||||
|               Nothing   -> q  -- shouldn't happen.. ignore | ||||
|   return q' | ||||
| 
 | ||||
| -- (symbol, equivalent in hours).  | ||||
| -- (symbol, equivalent in hours). | ||||
| timeUnits = | ||||
|   [("s",2.777777777777778e-4) | ||||
|   ,("mo",5040) -- before "m" | ||||
|  | ||||
| @ -11,7 +11,7 @@ module Hledger.Reports.BalanceReport ( | ||||
|   BalanceReportItem, | ||||
|   balanceReport, | ||||
|   flatShowsExclusiveBalance, | ||||
|   sortAccountItemsLike,  | ||||
|   sortAccountItemsLike, | ||||
| 
 | ||||
|   -- * Tests | ||||
|   tests_BalanceReport | ||||
| @ -26,7 +26,7 @@ import Data.Time.Calendar | ||||
| import Hledger.Data | ||||
| import Hledger.Read (mamountp') | ||||
| import Hledger.Query | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Reports.ReportOptions | ||||
| 
 | ||||
| 
 | ||||
| @ -64,8 +64,8 @@ flatShowsExclusiveBalance    = True | ||||
| -- This is like PeriodChangeReport with a single column (but more mature, | ||||
| -- eg this can do hierarchical display). | ||||
| balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport | ||||
| balanceReport ropts@ReportOpts{..} q j@Journal{..} =  | ||||
|   (if invert_ then brNegate  else id) $  | ||||
| balanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|   (if invert_ then brNegate  else id) $ | ||||
|   (sorteditems, total) | ||||
|     where | ||||
|       -- dbg1 = const id -- exclude from debug output | ||||
| @ -117,24 +117,24 @@ balanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|       items = dbg1 "items" $ map (balanceReportItem ropts q) displayaccts | ||||
| 
 | ||||
|       -- Sort report rows (except sorting by amount in tree mode, which was done above). | ||||
|       sorteditems  | ||||
|       sorteditems | ||||
|         | sort_amount_ && tree_ ropts = items | ||||
|         | sort_amount_                = sortFlatBRByAmount items | ||||
|         | otherwise                   = sortBRByAccountDeclaration items | ||||
|         where     | ||||
|           -- Sort the report rows, representing a flat account list, by row total.  | ||||
|         where | ||||
|           -- Sort the report rows, representing a flat account list, by row total. | ||||
|           sortFlatBRByAmount :: [BalanceReportItem] -> [BalanceReportItem] | ||||
|           sortFlatBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fourth4)) | ||||
|             where | ||||
|               maybeflip = if normalbalance_ == Just NormallyNegative then id else flip | ||||
|           -- Sort the report rows by account declaration order then account name.  | ||||
|           -- Sort the report rows by account declaration order then account name. | ||||
|           sortBRByAccountDeclaration :: [BalanceReportItem] -> [BalanceReportItem] | ||||
|           sortBRByAccountDeclaration rows = sortedrows | ||||
|             where  | ||||
|             where | ||||
|               anamesandrows = [(first4 r, r) | r <- rows] | ||||
|               anames = map fst anamesandrows | ||||
|               sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames | ||||
|               sortedrows = sortAccountItemsLike sortedanames anamesandrows  | ||||
|               sortedrows = sortAccountItemsLike sortedanames anamesandrows | ||||
| 
 | ||||
|       -- Calculate the grand total. | ||||
|       total | not (flat_ ropts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0] | ||||
| @ -145,7 +145,7 @@ balanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
| 
 | ||||
| -- | 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. | ||||
| sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b]  | ||||
| sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b] | ||||
| sortAccountItemsLike sortedas items = | ||||
|   concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas | ||||
| 
 | ||||
| @ -181,7 +181,7 @@ balanceReportItem opts q a | ||||
| 
 | ||||
| -- | Flip the sign of all amounts in a BalanceReport. | ||||
| brNegate :: BalanceReport -> BalanceReport | ||||
| brNegate (is, tot) = (map brItemNegate is, -tot)  | ||||
| brNegate (is, tot) = (map brItemNegate is, -tot) | ||||
|   where | ||||
|     brItemNegate (a, a', d, amt) = (a, a', d, -amt) | ||||
| 
 | ||||
| @ -222,10 +222,10 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|         (showMixedAmountDebug etotal) `is` (showMixedAmountDebug atotal) | ||||
|       usd0 = usd 0 | ||||
|     in [ | ||||
|    | ||||
| 
 | ||||
|      test "balanceReport with no args on null journal" $ | ||||
|      (defreportopts, nulljournal) `gives` ([], Mixed [nullamt]) | ||||
|    | ||||
| 
 | ||||
|     ,test "balanceReport with no args on sample journal" $ | ||||
|      (defreportopts, samplejournal) `gives` | ||||
|       ([ | ||||
| @ -242,7 +242,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,("income:salary","salary",1, mamountp' "$-1.00") | ||||
|        ], | ||||
|        Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|     ,test "balanceReport with --depth=N" $ | ||||
|      (defreportopts{depth_=Just 1}, samplejournal) `gives` | ||||
|       ([ | ||||
| @ -250,7 +250,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,("income",      "income",      0, mamountp' "$-2.00") | ||||
|        ], | ||||
|        Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|     ,test "balanceReport with depth:N" $ | ||||
|      (defreportopts{query_="depth:1"}, samplejournal) `gives` | ||||
|       ([ | ||||
| @ -258,7 +258,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,("income",      "income",      0, mamountp' "$-2.00") | ||||
|        ], | ||||
|        Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|     ,tests "balanceReport with a date or secondary date span" [ | ||||
|      (defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives` | ||||
|       ([], | ||||
| @ -278,7 +278,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,("income:salary","income:salary",0, mamountp' "$-1.00") | ||||
|        ], | ||||
|        Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|     ,test "balanceReport with not:desc:" $ | ||||
|      (defreportopts{query_="not:desc:income"}, samplejournal) `gives` | ||||
|       ([ | ||||
| @ -291,7 +291,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,("income:gifts","income:gifts",0, mamountp' "$-1.00") | ||||
|        ], | ||||
|        Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|     ,test "balanceReport with period on a populated period" $ | ||||
|       (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}, samplejournal) `gives` | ||||
|        ( | ||||
| @ -300,13 +300,13 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|         ,("income:salary","income:salary",0, mamountp' "$-1.00") | ||||
|         ], | ||||
|         Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|      ,test "balanceReport with period on an unpopulated period" $ | ||||
|       (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}, samplejournal) `gives` | ||||
|        ([],Mixed [nullamt]) | ||||
|    | ||||
|    | ||||
|    | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   {- | ||||
|       ,test "accounts report with account pattern o" ~: | ||||
|        defreportopts{patterns_=["o"]} `gives` | ||||
| @ -317,7 +317,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                 $-1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with account pattern o and --depth 1" ~: | ||||
|        defreportopts{patterns_=["o"],depth_=Just 1} `gives` | ||||
|        ["                  $1  expenses" | ||||
| @ -325,7 +325,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                 $-1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with account pattern a" ~: | ||||
|        defreportopts{patterns_=["a"]} `gives` | ||||
|        ["                 $-1  assets" | ||||
| @ -336,7 +336,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                 $-1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with account pattern e" ~: | ||||
|        defreportopts{patterns_=["e"]} `gives` | ||||
|        ["                 $-1  assets" | ||||
| @ -352,7 +352,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                   0" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with unmatched parent of two matched subaccounts" ~: | ||||
|        defreportopts{patterns_=["cash","saving"]} `gives` | ||||
|        ["                 $-1  assets" | ||||
| @ -361,14 +361,14 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                 $-1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with multi-part account name" ~: | ||||
|        defreportopts{patterns_=["expenses:food"]} `gives` | ||||
|        ["                  $1  expenses:food" | ||||
|        ,"--------------------" | ||||
|        ,"                  $1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with negative account pattern" ~: | ||||
|        defreportopts{patterns_=["not:assets"]} `gives` | ||||
|        ["                  $2  expenses" | ||||
| @ -381,20 +381,20 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                  $1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report negative account pattern always matches full name" ~: | ||||
|        defreportopts{patterns_=["not:e"]} `gives` | ||||
|        ["--------------------" | ||||
|        ,"                   0" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report negative patterns affect totals" ~: | ||||
|        defreportopts{patterns_=["expenses","not:food"]} `gives` | ||||
|        ["                  $1  expenses:supplies" | ||||
|        ,"--------------------" | ||||
|        ,"                  $1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with -E shows zero-balance accounts" ~: | ||||
|        defreportopts{patterns_=["assets"],empty_=True} `gives` | ||||
|        ["                 $-1  assets" | ||||
| @ -405,7 +405,7 @@ tests_BalanceReport = tests "BalanceReport" [ | ||||
|        ,"--------------------" | ||||
|        ,"                 $-1" | ||||
|        ] | ||||
|    | ||||
| 
 | ||||
|       ,test "accounts report with cost basis" $ | ||||
|          j <- (readJournal def Nothing $ unlines | ||||
|                 ["" | ||||
|  | ||||
| @ -72,14 +72,14 @@ budgetReport ropts' assrt reportspan d j = | ||||
|     -- and that reports with and without --empty make sense when compared side by side | ||||
|     ropts = ropts' { accountlistmode_ = ALTree } | ||||
|     showunbudgeted = empty_ ropts | ||||
|     q = queryFromOpts d ropts  | ||||
|     budgetedaccts =  | ||||
|     q = queryFromOpts d ropts | ||||
|     budgetedaccts = | ||||
|       dbg2 "budgetedacctsinperiod" $ | ||||
|       nub $  | ||||
|       nub $ | ||||
|       concatMap expandAccountName $ | ||||
|       accountNamesFromPostings $  | ||||
|       concatMap tpostings $  | ||||
|       concatMap (flip runPeriodicTransaction reportspan) $  | ||||
|       accountNamesFromPostings $ | ||||
|       concatMap tpostings $ | ||||
|       concatMap (flip runPeriodicTransaction reportspan) $ | ||||
|       jperiodictxns j | ||||
|     actualj = dbg1 "actualj" $ budgetRollUp budgetedaccts showunbudgeted j | ||||
|     budgetj = dbg1 "budgetj" $ budgetJournal assrt ropts reportspan j | ||||
| @ -87,10 +87,10 @@ budgetReport ropts' assrt reportspan d j = | ||||
|     budgetgoalreport@(MultiBalanceReport (_, budgetgoalitems, budgetgoaltotals)) = dbg1 "budgetgoalreport" $ multiBalanceReport (ropts{empty_=True}) q budgetj | ||||
|     budgetgoalreport' | ||||
|       -- If no interval is specified: | ||||
|       -- budgetgoalreport's span might be shorter actualreport's due to periodic txns;  | ||||
|       -- it should be safe to replace it with the latter, so they combine well.  | ||||
|       -- budgetgoalreport's span might be shorter actualreport's due to periodic txns; | ||||
|       -- it should be safe to replace it with the latter, so they combine well. | ||||
|       | interval_ ropts == NoInterval = MultiBalanceReport (actualspans, budgetgoalitems, budgetgoaltotals) | ||||
|       | otherwise = budgetgoalreport  | ||||
|       | otherwise = budgetgoalreport | ||||
|     budgetreport = combineBudgetAndActual budgetgoalreport' actualreport | ||||
|     sortedbudgetreport = sortBudgetReport ropts j budgetreport | ||||
|   in | ||||
| @ -100,13 +100,13 @@ budgetReport ropts' assrt reportspan d j = | ||||
| sortBudgetReport :: ReportOpts -> Journal -> BudgetReport -> BudgetReport | ||||
| sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, sortedrows, trow) | ||||
|   where | ||||
|     sortedrows  | ||||
|     sortedrows | ||||
|       | sort_amount_ ropts && tree_ ropts = sortTreeBURByActualAmount rows | ||||
|       | sort_amount_ ropts                = sortFlatBURByActualAmount rows | ||||
|       | otherwise                         = sortByAccountDeclaration rows | ||||
| 
 | ||||
|     -- Sort a tree-mode budget report's rows by total actual amount at each level. | ||||
|     sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]  | ||||
|     sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow] | ||||
|     sortTreeBURByActualAmount rows = sortedrows | ||||
|       where | ||||
|         anamesandrows = [(first6 r, r) | r <- rows] | ||||
| @ -116,21 +116,21 @@ sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, | ||||
|         accounttreewithbals = mapAccounts setibalance accounttree | ||||
|           where | ||||
|             setibalance a = a{aibalance= | ||||
|               fromMaybe 0 $ -- when there's no actual amount, assume 0; will mess up with negative amounts ? TODO  | ||||
|               fromMaybe (error "sortTreeByAmount 1") $ -- should not happen, but it's ugly; TODO  | ||||
|               fromMaybe 0 $ -- when there's no actual amount, assume 0; will mess up with negative amounts ? TODO | ||||
|               fromMaybe (error "sortTreeByAmount 1") $ -- should not happen, but it's ugly; TODO | ||||
|               lookup (aname a) atotals | ||||
|               } | ||||
|         sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals | ||||
|         sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree | ||||
|         sortedrows = sortAccountItemsLike sortedanames anamesandrows  | ||||
|         sortedrows = sortAccountItemsLike sortedanames anamesandrows | ||||
| 
 | ||||
|     -- Sort a flat-mode budget report's rows by total actual amount. | ||||
|     sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]  | ||||
|     sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow] | ||||
|     sortFlatBURByActualAmount = sortBy (maybeflip $ comparing (fst . fifth6)) | ||||
|       where | ||||
|         maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip | ||||
| 
 | ||||
|     -- Sort the report rows by account declaration order then account name.  | ||||
|     -- Sort the report rows by account declaration order then account name. | ||||
|     -- <unbudgeted> remains at the top. | ||||
|     sortByAccountDeclaration rows = sortedrows | ||||
|       where | ||||
| @ -138,9 +138,9 @@ sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, | ||||
|         anamesandrows = [(first6 r, r) | r <- rows'] | ||||
|         anames = map fst anamesandrows | ||||
|         sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames | ||||
|         sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows  | ||||
|         sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows | ||||
| 
 | ||||
| -- | Use all periodic transactions in the journal to generate  | ||||
| -- | Use all periodic transactions in the journal to generate | ||||
| -- budget transactions in the specified report period. | ||||
| -- Budget transactions are similar to forecast transactions except | ||||
| -- their purpose is to set goal amounts (of change) per account and period. | ||||
| @ -159,11 +159,11 @@ budgetJournal assrt _ropts reportspan j = | ||||
| 
 | ||||
| -- | Adjust a journal's account names for budget reporting, in two ways: | ||||
| -- | ||||
| -- 1. accounts with no budget goal anywhere in their ancestry are moved  | ||||
| -- 1. accounts with no budget goal anywhere in their ancestry are moved | ||||
| --    under the "unbudgeted" top level account. | ||||
| -- | ||||
| -- 2. subaccounts with no budget goal are merged with their closest parent account | ||||
| --    with a budget goal, so that only budgeted accounts are shown.  | ||||
| --    with a budget goal, so that only budgeted accounts are shown. | ||||
| --    This can be disabled by --empty. | ||||
| -- | ||||
| budgetRollUp :: [AccountName] -> Bool -> Journal -> Journal | ||||
| @ -176,7 +176,7 @@ budgetRollUp budgetedaccts showunbudgeted j = j { jtxns = remapTxn <$> jtxns j } | ||||
|           where | ||||
|             remapAccount a | ||||
|               | hasbudget         = a | ||||
|               | hasbudgetedparent = if showunbudgeted then a else budgetedparent  | ||||
|               | hasbudgetedparent = if showunbudgeted then a else budgetedparent | ||||
|               | otherwise         = if showunbudgeted then u <> acctsep <> a else u | ||||
|               where | ||||
|                 hasbudget = a `elem` budgetedaccts | ||||
| @ -270,7 +270,7 @@ budgetReportSpan (PeriodicReport (spans, _, _)) = DateSpan (spanStart $ head spa | ||||
| -- | Render a budget report as plain text suitable for console output. | ||||
| budgetReportAsText :: ReportOpts -> BudgetReport -> String | ||||
| budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) = | ||||
|   title ++ "\n\n" ++  | ||||
|   title ++ "\n\n" ++ | ||||
|   tableAsText ropts showcell (maybetranspose $ budgetReportAsTable ropts budgetr) | ||||
|   where | ||||
|     multiperiod = interval_ /= NoInterval | ||||
| @ -319,7 +319,7 @@ budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) = | ||||
|     percentage :: Change -> BudgetGoal -> Maybe Percentage | ||||
|     percentage actual budget = | ||||
|       case (maybecost $ normaliseMixedAmount actual, maybecost $ normaliseMixedAmount budget) of | ||||
|         (Mixed [a], Mixed [b]) | (acommodity a == acommodity b || isZeroAmount a) && not (isZeroAmount b)  | ||||
|         (Mixed [a], Mixed [b]) | (acommodity a == acommodity b || isZeroAmount a) && not (isZeroAmount b) | ||||
|             -> Just $ 100 * aquantity a / aquantity b | ||||
|         _   -> -- trace (pshow $ (maybecost actual, maybecost budget))  -- debug missing percentage | ||||
|                Nothing | ||||
| @ -337,14 +337,14 @@ budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) = | ||||
| 
 | ||||
| -- | Build a 'Table' from a multi-column balance report. | ||||
| budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount) | ||||
| budgetReportAsTable  | ||||
|   ropts  | ||||
| budgetReportAsTable | ||||
|   ropts | ||||
|   (PeriodicReport | ||||
|     ( periods | ||||
|     , rows | ||||
|     , (_, _, _, coltots, grandtot, grandavg) | ||||
|     )) = | ||||
|     addtotalrow $  | ||||
|     addtotalrow $ | ||||
|     Table | ||||
|       (T.Group NoLine $ map Header accts) | ||||
|       (T.Group NoLine $ map Header colheadings) | ||||
| @ -368,7 +368,7 @@ budgetReportAsTable | ||||
|                                      )) | ||||
| 
 | ||||
| -- XXX here for now | ||||
| -- TODO: does not work for flat-by-default reports with --flat not specified explicitly   | ||||
| -- TODO: does not work for flat-by-default reports with --flat not specified explicitly | ||||
| -- | Drop leading components of accounts names as specified by --drop, but only in --flat mode. | ||||
| maybeAccountNameDrop :: ReportOpts -> AccountName -> AccountName | ||||
| maybeAccountNameDrop opts a | flat_ opts = accountNameDrop (drop_ opts) a | ||||
|  | ||||
| @ -23,7 +23,7 @@ import Data.Time.Calendar (Day, addDays) | ||||
| import Hledger.Data | ||||
| import Hledger.Query | ||||
| import Hledger.Reports.ReportOptions | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| 
 | ||||
| 
 | ||||
| -- | A journal entries report is a list of whole transactions as | ||||
|  | ||||
| @ -30,7 +30,7 @@ import Text.Tabular.AsciiWide | ||||
| 
 | ||||
| import Hledger.Data | ||||
| import Hledger.Query | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Read (mamountp') | ||||
| import Hledger.Reports.ReportOptions | ||||
| import Hledger.Reports.BalanceReport | ||||
| @ -85,13 +85,13 @@ type ClippedAccountName = AccountName | ||||
| -- | Generate a multicolumn balance report for the matched accounts, | ||||
| -- showing the change of balance, accumulated balance, or historical balance | ||||
| -- in each of the specified periods. Does not support tree-mode boring parent eliding. | ||||
| -- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts  | ||||
| -- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts | ||||
| -- (see ReportOpts and CompoundBalanceCommand). | ||||
| -- hledger's most powerful and useful report, used by the balance | ||||
| -- command (in multiperiod mode) and by the bs/cf/is commands. | ||||
| multiBalanceReport :: ReportOpts -> Query -> Journal -> MultiBalanceReport | ||||
| multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|   (if invert_ then mbrNegate else id) $  | ||||
|   (if invert_ then mbrNegate else id) $ | ||||
|   MultiBalanceReport (colspans, sortedrows, totalsrow) | ||||
|     where | ||||
|       dbg1 s = let p = "multiBalanceReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in this function's debug output | ||||
| @ -115,18 +115,18 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|       -- This list can be empty if the journal was empty, | ||||
|       -- or if hledger-ui has added its special date:-tomorrow to the query | ||||
|       -- and all txns are in the future. | ||||
|       intervalspans  = dbg1 "intervalspans"  $ splitSpan interval_ requestedspan'            | ||||
|       intervalspans  = dbg1 "intervalspans"  $ splitSpan interval_ requestedspan' | ||||
|       -- The requested span enlarged to enclose a whole number of intervals. | ||||
|       -- This can be the null span if there were no intervals.  | ||||
|       -- This can be the null span if there were no intervals. | ||||
|       reportspan     = dbg1 "reportspan"     $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) | ||||
|                                                         (maybe Nothing spanEnd   $ lastMay intervalspans) | ||||
|       mreportstart = spanStart reportspan | ||||
|       -- The user's query with no depth limit, and expanded to the report span | ||||
|       -- if there is one (otherwise any date queries are left as-is, which | ||||
|       -- handles the hledger-ui+future txns case above). | ||||
|       reportq   = dbg1 "reportq" $ depthless $  | ||||
|         if reportspan == nulldatespan  | ||||
|         then q  | ||||
|       reportq   = dbg1 "reportq" $ depthless $ | ||||
|         if reportspan == nulldatespan | ||||
|         then q | ||||
|         else And [datelessq, reportspandatesq] | ||||
|           where | ||||
|             reportspandatesq = dbg1 "reportspandatesq" $ dateqcons reportspan | ||||
| @ -157,12 +157,12 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|                   precedingperiod = dateSpanAsPeriod $ spanIntersect (DateSpan Nothing mreportstart) $ periodAsDateSpan period_ | ||||
|               -- q projected back before the report start date. | ||||
|               -- When there's no report start date, in case there are future txns (the hledger-ui case above), | ||||
|               -- we use emptydatespan to make sure they aren't counted as starting balance.   | ||||
|               -- we use emptydatespan to make sure they aren't counted as starting balance. | ||||
|               startbalq = dbg1 "startbalq" $ And [datelessq, dateqcons precedingspan] | ||||
|                 where | ||||
|                   precedingspan = case mreportstart of | ||||
|                                   Just d  -> DateSpan Nothing (Just d) | ||||
|                                   Nothing -> emptydatespan  | ||||
|                                   Nothing -> emptydatespan | ||||
|       -- The matched accounts with a starting balance. All of these should appear | ||||
|       -- in the report even if they have no postings during the report period. | ||||
|       startaccts = dbg1 "startaccts" $ map fst startbals | ||||
| @ -282,7 +282,7 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|                   (error' "multiBalanceReport: expected all spans to have an end date")  -- XXX should not happen | ||||
|                   (addDays (-1))) | ||||
|                 . spanEnd) colspans | ||||
|            | ||||
| 
 | ||||
|       ---------------------------------------------------------------------- | ||||
|       -- 7. Sort the report rows. | ||||
| 
 | ||||
| @ -307,24 +307,24 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
|                   accounttree = accountTree "root" anames | ||||
|                   accounttreewithbals = mapAccounts setibalance accounttree | ||||
|                     where | ||||
|                       -- should not happen, but it's dangerous; TODO  | ||||
|                       -- should not happen, but it's dangerous; TODO | ||||
|                       setibalance a = a{aibalance=fromMaybe (error "sortTreeMBRByAmount 1") $ lookup (aname a) atotals} | ||||
|                   sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive normalbalance_) accounttreewithbals | ||||
|                   sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree | ||||
|                   sortedrows = sortAccountItemsLike sortedanames anamesandrows  | ||||
|                   sortedrows = sortAccountItemsLike sortedanames anamesandrows | ||||
| 
 | ||||
|               -- Sort the report rows, representing a flat account list, by row total.  | ||||
|               -- Sort the report rows, representing a flat account list, by row total. | ||||
|               sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fifth6)) | ||||
|                 where | ||||
|                   maybeflip = if normalbalance_ == Just NormallyNegative then id else flip | ||||
| 
 | ||||
|               -- Sort the report rows by account declaration order then account name.  | ||||
|               -- Sort the report rows by account declaration order then account name. | ||||
|               sortMBRByAccountDeclaration rows = sortedrows | ||||
|                 where  | ||||
|                 where | ||||
|                   anamesandrows = [(first6 r, r) | r <- rows] | ||||
|                   anames = map fst anamesandrows | ||||
|                   sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames | ||||
|                   sortedrows = sortAccountItemsLike sortedanames anamesandrows  | ||||
|                   sortedrows = sortAccountItemsLike sortedanames anamesandrows | ||||
| 
 | ||||
|       ---------------------------------------------------------------------- | ||||
|       -- 8. Build the report totals row. | ||||
| @ -364,9 +364,9 @@ multiBalanceReportSpan :: MultiBalanceReport -> DateSpan | ||||
| multiBalanceReportSpan (MultiBalanceReport ([], _, _))       = DateSpan Nothing Nothing | ||||
| multiBalanceReportSpan (MultiBalanceReport (colspans, _, _)) = DateSpan (spanStart $ head colspans) (spanEnd $ last colspans) | ||||
| 
 | ||||
| -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport,  | ||||
| -- in order to support --historical. Does not support tree-mode boring parent eliding.  | ||||
| -- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts  | ||||
| -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, | ||||
| -- in order to support --historical. Does not support tree-mode boring parent eliding. | ||||
| -- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts | ||||
| -- (see ReportOpts and CompoundBalanceCommand). | ||||
| balanceReportFromMultiBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport | ||||
| balanceReportFromMultiBalanceReport opts q j = (rows', total) | ||||
| @ -408,11 +408,11 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [ | ||||
|       ((\(_, b, _) -> showMixedAmountDebug b) atotal) `is` (showMixedAmountDebug etotal) -- we only check the sum of the totals | ||||
|     usd0 = usd 0 | ||||
|     amount0 = Amount {acommodity="$", aquantity=0, aprice=Nothing, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}, aismultiplier=False} | ||||
|   in  | ||||
|   in | ||||
|    tests "multiBalanceReport" [ | ||||
|       test "null journal"  $ | ||||
|       (defreportopts, nulljournal) `gives` ([], Mixed [nullamt]) | ||||
|    | ||||
| 
 | ||||
|      ,test "with -H on a populated period"  $ | ||||
|       (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives` | ||||
|        ( | ||||
| @ -421,7 +421,7 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [ | ||||
|         ,("income:salary"       ,"salary"   , 2, [mamountp' "$-1.00"], Mixed [nullamt], Mixed [amount0 {aquantity=(-1)}]) | ||||
|         ], | ||||
|         Mixed [nullamt]) | ||||
|    | ||||
| 
 | ||||
|      ,_test "a valid history on an empty period"  $ | ||||
|       (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3), balancetype_=HistoricalBalance}, samplejournal) `gives` | ||||
|        ( | ||||
| @ -430,7 +430,7 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [ | ||||
|         ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}]) | ||||
|         ], | ||||
|         Mixed [usd0]) | ||||
|    | ||||
| 
 | ||||
|      ,_test "a valid history on an empty period (more complex)"  $ | ||||
|       (defreportopts{period_= PeriodBetween (fromGregorian 2009 1 1) (fromGregorian 2009 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives` | ||||
|        ( | ||||
|  | ||||
| @ -32,7 +32,7 @@ import Safe (headMay, lastMay) | ||||
| 
 | ||||
| import Hledger.Data | ||||
| import Hledger.Query | ||||
| import Hledger.Utils  | ||||
| import Hledger.Utils | ||||
| import Hledger.Reports.ReportOptions | ||||
| 
 | ||||
| 
 | ||||
| @ -103,7 +103,7 @@ postingsReport ropts@ReportOpts{..} q j@Journal{..} = | ||||
| 
 | ||||
|       -- Postings, or summary postings with their subperiod's end date, to be displayed. | ||||
|       displayps :: [(Posting, Maybe Day)] | ||||
|         | multiperiod =  | ||||
|         | multiperiod = | ||||
|             let summaryps = summarisePostingsByInterval interval_ whichdate depth showempty reportspan reportps | ||||
|             in [(pvalue p lastday, Just periodend) | (p, periodend) <- summaryps, let lastday = addDays (-1) periodend] | ||||
|         | otherwise = | ||||
| @ -286,13 +286,13 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|      ,(Depth 2, samplejournal) `gives` 13 | ||||
|      ,(And [Depth 1, StatusQ Cleared, Acct "expenses"], samplejournal) `gives` 2 | ||||
|      ,(And [And [Depth 1, StatusQ Cleared], Acct "expenses"], samplejournal) `gives` 2 | ||||
|    | ||||
| 
 | ||||
|      -- with query and/or command-line options | ||||
|      ,(length $ snd $ postingsReport defreportopts Any samplejournal) `is` 13 | ||||
|      ,(length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal) `is` 11 | ||||
|      ,(length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal) `is` 20 | ||||
|      ,(length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal) `is` 5 | ||||
|    | ||||
| 
 | ||||
|      -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0 | ||||
|      -- [(Just (parsedate "2008-01-01","income"),assets:bank:checking             $1,$1) | ||||
|      -- ,(Nothing,income:salary                   $-1,0) | ||||
| @ -304,7 +304,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|      -- ,(Nothing,expenses:supplies                $1,$2) | ||||
|      -- ,(Nothing,assets:cash                     $-2,0) | ||||
|      -- ,(Just (2008-12-31,"pay off"),liabilities:debts                $1,$1) | ||||
|      -- ,(Nothing,assets:bank:checking            $-1,0)     | ||||
|      -- ,(Nothing,assets:bank:checking            $-1,0) | ||||
| 
 | ||||
|     {- | ||||
|         let opts = defreportopts | ||||
| @ -321,7 +321,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|          ,"2008/12/31 pay off              liabilities:debts                $1           $1" | ||||
|          ,"                                assets:bank:checking            $-1            0" | ||||
|          ] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report with cleared option" ~: | ||||
|        do | ||||
|         let opts = defreportopts{cleared_=True} | ||||
| @ -333,7 +333,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|          ,"2008/12/31 pay off              liabilities:debts                $1           $1" | ||||
|          ,"                                assets:bank:checking            $-1            0" | ||||
|          ] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report with uncleared option" ~: | ||||
|        do | ||||
|         let opts = defreportopts{uncleared_=True} | ||||
| @ -346,7 +346,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|          ,"2008/06/02 save                 assets:bank:saving               $1           $1" | ||||
|          ,"                                assets:bank:checking            $-1            0" | ||||
|          ] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report sorts by date" ~: | ||||
|        do | ||||
|         j <- readJournal' $ unlines | ||||
| @ -360,7 +360,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|             ] | ||||
|         let opts = defreportopts | ||||
|         registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/02/02"] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report with account pattern" ~: | ||||
|        do | ||||
|         j <- samplejournal | ||||
| @ -368,7 +368,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|         (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines | ||||
|          ["2008/06/03 eat & shop           assets:cash                     $-2          $-2" | ||||
|          ] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report with account pattern, case insensitive" ~: | ||||
|        do | ||||
|         j <- samplejournal | ||||
| @ -376,7 +376,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|         (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines | ||||
|          ["2008/06/03 eat & shop           assets:cash                     $-2          $-2" | ||||
|          ] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report with display expression" ~: | ||||
|        do | ||||
|         j <- samplejournal | ||||
| @ -388,7 +388,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|         "d=[2008/6/2]"  `gives` ["2008/06/02"] | ||||
|         "d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"] | ||||
|         "d>[2008/6/2]"  `gives` ["2008/06/03","2008/12/31"] | ||||
|      | ||||
| 
 | ||||
|       ,"postings report with period expression" ~: | ||||
|        do | ||||
|         j <- samplejournal | ||||
| @ -416,9 +416,9 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|         registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/10/01"] | ||||
|         let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True} | ||||
|         registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"] | ||||
|      | ||||
| 
 | ||||
|       ] | ||||
|      | ||||
| 
 | ||||
|       , "postings report with depth arg" ~: | ||||
|        do | ||||
|         j <- samplejournal | ||||
| @ -436,7 +436,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|          ,"2008/12/31 pay off              liabilities:debts                $1           $1" | ||||
|          ,"                                assets:bank                     $-1            0" | ||||
|          ] | ||||
|      | ||||
| 
 | ||||
|     -} | ||||
|     ] | ||||
| 
 | ||||
| @ -445,7 +445,7 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|       summarisePostingsByInterval (Quarters 1) PrimaryDate 99999 False (DateSpan Nothing Nothing) [] `is` [] | ||||
|       ] | ||||
|    ] | ||||
|    | ||||
| 
 | ||||
|   -- ,tests_summarisePostingsInDateSpan = [ | ||||
|     --  "summarisePostingsInDateSpan" ~: do | ||||
|     --   let gives (b,e,depth,showempty,ps) = | ||||
| @ -481,5 +481,5 @@ tests_PostingsReport = tests "PostingsReport" [ | ||||
|     --    [ | ||||
|     --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [usd 15]} | ||||
|     --    ] | ||||
|    | ||||
| 
 | ||||
|  ] | ||||
|  | ||||
| @ -81,7 +81,7 @@ instance Default AccountListMode where def = ALDefault | ||||
| -- | Standard options for customising report filtering and output. | ||||
| -- Most of these correspond to standard hledger command-line options | ||||
| -- or query arguments, but not all. Some are used only by certain | ||||
| -- commands, as noted below.  | ||||
| -- commands, as noted below. | ||||
| data ReportOpts = ReportOpts { | ||||
|      today_          :: Maybe Day  -- ^ The current date. A late addition to ReportOpts. | ||||
|                                    -- Optional, but when set it may affect some reports: | ||||
| @ -116,10 +116,10 @@ data ReportOpts = ReportOpts { | ||||
|       -- ^ This can be set when running balance reports on a set of accounts | ||||
|       --   with the same normal balance type (eg all assets, or all incomes). | ||||
|       -- - It helps --sort-amount know how to sort negative numbers | ||||
|       --   (eg in the income section of an income statement)  | ||||
|       -- - It helps compound balance report commands (is, bs etc.) do   | ||||
|       --   sign normalisation, converting normally negative subreports to  | ||||
|       --   normally positive for a more conventional display.    | ||||
|       --   (eg in the income section of an income statement) | ||||
|       -- - It helps compound balance report commands (is, bs etc.) do | ||||
|       --   sign normalisation, converting normally negative subreports to | ||||
|       --   normally positive for a more conventional display. | ||||
|     ,color_          :: Bool | ||||
|     ,forecast_       :: Bool | ||||
|     ,transpose_      :: Bool | ||||
| @ -328,7 +328,7 @@ simplifyStatuses l | ||||
|   | length l' >= numstatuses = [] | ||||
|   | otherwise                = l' | ||||
|   where | ||||
|     l' = nub $ sort l  | ||||
|     l' = nub $ sort l | ||||
|     numstatuses = length [minBound .. maxBound :: Status] | ||||
| 
 | ||||
| -- | Add/remove this status from the status list. Used by hledger-ui. | ||||
| @ -442,7 +442,7 @@ queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts | ||||
| -- Report dates. | ||||
| 
 | ||||
| -- | The effective report span is the start and end dates specified by | ||||
| -- options or queries, or otherwise the earliest and latest transaction or  | ||||
| -- options or queries, or otherwise the earliest and latest transaction or | ||||
| -- posting dates in the journal. If no dates are specified by options/queries | ||||
| -- and the journal is empty, returns the null date span. | ||||
| -- Needs IO to parse smart dates in options/queries. | ||||
| @ -500,7 +500,7 @@ reportPeriodOrJournalStart ropts@ReportOpts{..} j = | ||||
|   reportPeriodStart ropts <|> journalStartDate False j | ||||
| 
 | ||||
| -- Get the last day of the overall report period. | ||||
| -- This the inclusive end date (one day before the  | ||||
| -- This the inclusive end date (one day before the | ||||
| -- more commonly used, exclusive, report end date). | ||||
| -- If no report period is specified, will be Nothing. | ||||
| -- Will also be Nothing if ReportOpts does not have today_ set, | ||||
| @ -528,7 +528,7 @@ tests_ReportOptions = tests "ReportOptions" [ | ||||
|       (queryFromOpts nulldate defreportopts) `is` Any | ||||
|      ,(queryFromOpts nulldate defreportopts{query_="a"}) `is` (Acct "a") | ||||
|      ,(queryFromOpts nulldate defreportopts{query_="desc:'a a'"}) `is` (Desc "a a") | ||||
|      ,(queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01"),query_="date:'to 2013'" })  | ||||
|      ,(queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01"),query_="date:'to 2013'" }) | ||||
|       `is` (Date $ mkdatespan "2012/01/01" "2013/01/01") | ||||
|      ,(queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"}) `is` (Date2 $ mkdatespan "2012/01/01" "2013/01/01") | ||||
|      ,(queryFromOpts nulldate defreportopts{query_="'a a' 'b"}) `is` (Or [Acct "a a", Acct "'b"]) | ||||
|  | ||||
| @ -33,7 +33,7 @@ data PeriodicReport a = | ||||
| type PeriodicReportRow a = | ||||
|   ( AccountName  -- A full account name. | ||||
|   , AccountName  -- Shortened form of the account name to display in tree mode. Usually the leaf name, possibly with parent accounts prefixed. | ||||
|   , Int          -- Indent level for displaying this account name in tree mode. 0, 1, 2...  | ||||
|   , Int          -- Indent level for displaying this account name in tree mode. 0, 1, 2... | ||||
|   , [a]          -- The data value for each subperiod. | ||||
|   , a            -- The total of this row's values. | ||||
|   , a            -- The average of this row's values. | ||||
|  | ||||
| @ -161,14 +161,14 @@ firstJust ms = case dropWhile (==Nothing) ms of | ||||
|     [] -> Nothing | ||||
|     (md:_) -> md | ||||
| 
 | ||||
| -- | Read text from a file,  | ||||
| -- | Read text from a file, | ||||
| -- handling any of the usual line ending conventions, | ||||
| -- using the system locale's text encoding, | ||||
| -- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8.  | ||||
| -- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8. | ||||
| readFilePortably :: FilePath -> IO Text | ||||
| readFilePortably f =  openFile f ReadMode >>= readHandlePortably | ||||
| 
 | ||||
| -- | Like readFilePortably, but read from standard input if the path is "-".  | ||||
| -- | Like readFilePortably, but read from standard input if the path is "-". | ||||
| readFileOrStdinPortably :: String -> IO Text | ||||
| readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably | ||||
|   where | ||||
| @ -236,7 +236,7 @@ embedFileRelative f = makeRelativeToProject f >>= embedStringFile | ||||
| -- hereFileRelative f = makeRelativeToProject f >>= hereFileExp | ||||
| --   where | ||||
| --     QuasiQuoter{quoteExp=hereFileExp} = hereFile | ||||
|      | ||||
| 
 | ||||
| tests_Utils = tests "Utils" [ | ||||
|   tests_Text | ||||
|   ] | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| 
 | ||||
| {-# LANGUAGE OverloadedStrings #-} | ||||
| 
 | ||||
| module Hledger.Utils.Color  | ||||
| module Hledger.Utils.Color | ||||
| ( | ||||
|   color, | ||||
|   bgColor, | ||||
|  | ||||
| @ -94,7 +94,7 @@ traceWith f a = trace (f a) a | ||||
| -- touch and reload this module to see the effect of a new --debug option. | ||||
| -- After command-line processing, it is also available as the @debug_@ | ||||
| -- field of 'Hledger.Cli.CliOptions.CliOpts'. | ||||
| -- {-# OPTIONS_GHC -fno-cse #-}  | ||||
| -- {-# OPTIONS_GHC -fno-cse #-} | ||||
| -- {-# NOINLINE debugLevel #-} | ||||
| debugLevel :: Int | ||||
| debugLevel = case snd $ break (=="--debug") args of | ||||
| @ -251,7 +251,7 @@ dbg9IO = ptraceAtIO 9 | ||||
| plog :: Show a => String -> a -> a | ||||
| plog = plogAt 0 | ||||
| 
 | ||||
| -- | Log a label and a pretty-printed showable value to ./debug.log,  | ||||
| -- | Log a label and a pretty-printed showable value to ./debug.log, | ||||
| -- if the global debug level is at or above the specified level. | ||||
| -- At level 0, always logs. Otherwise, uses unsafePerformIO. | ||||
| -- Tends to fail if called more than once, at least when built with -threaded | ||||
| @ -259,7 +259,7 @@ plog = plogAt 0 | ||||
| plogAt :: Show a => Int -> String -> a -> a | ||||
| plogAt lvl | ||||
|     | lvl > 0 && debugLevel < lvl = flip const | ||||
|     | otherwise = \s a ->  | ||||
|     | otherwise = \s a -> | ||||
|         let p = ppShow a | ||||
|             ls = lines p | ||||
|             nlorspace | length ls > 1 = "\n" | ||||
|  | ||||
| @ -322,9 +322,9 @@ takeWidth w (c:cs) | cw <= w   = c:takeWidth (w-cw) cs | ||||
| -- see also http://unicode.org/reports/tr11/#Description | ||||
| 
 | ||||
| -- | Calculate the render width of a string, considering | ||||
| -- wide characters (counted as double width), ANSI escape codes  | ||||
| -- wide characters (counted as double width), ANSI escape codes | ||||
| -- (not counted), and line breaks (in a multi-line string, the longest | ||||
| -- line determines the width).  | ||||
| -- line determines the width). | ||||
| strWidth :: String -> Int | ||||
| strWidth "" = 0 | ||||
| strWidth s = maximum $ map (foldr (\a b -> charWidth a + b) 0) $ lines s' | ||||
|  | ||||
| @ -24,7 +24,7 @@ module Hledger.Utils.Test ( | ||||
|   ,expectParseEqOn | ||||
|   ,expectParseEqOnE | ||||
|   ,expectParseStateOn | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Control.Exception | ||||
| @ -36,7 +36,7 @@ import Data.Monoid ((<>)) | ||||
| import Data.CallStack | ||||
| import Data.List | ||||
| import qualified Data.Text as T | ||||
| import Safe  | ||||
| import Safe | ||||
| import System.Exit | ||||
| import Text.Megaparsec | ||||
| import Text.Megaparsec.Custom | ||||
| @ -50,38 +50,38 @@ import Hledger.Utils.UTF8IOCompat (error') | ||||
| -- * easytest helpers | ||||
| 
 | ||||
| -- | Name the given test(s). A readability synonym for easytest's "scope". | ||||
| test :: T.Text -> E.Test a -> E.Test a  | ||||
| test :: T.Text -> E.Test a -> E.Test a | ||||
| test = E.scope | ||||
| 
 | ||||
| -- | Skip the given test(s), with the same type signature as "test". | ||||
| -- If called in a monadic sequence of tests, also skips following tests. | ||||
| _test :: T.Text -> E.Test a -> E.Test a  | ||||
| _test _name = (E.skip >>)  | ||||
| _test :: T.Text -> E.Test a -> E.Test a | ||||
| _test _name = (E.skip >>) | ||||
| 
 | ||||
| -- | Name the given test(s). A synonym for "test". | ||||
| it :: T.Text -> E.Test a -> E.Test a  | ||||
| it :: T.Text -> E.Test a -> E.Test a | ||||
| it = test | ||||
| 
 | ||||
| -- | Skip the given test(s), and any following tests in a monadic sequence.  | ||||
| -- | Skip the given test(s), and any following tests in a monadic sequence. | ||||
| -- A synonym for "_test". | ||||
| _it :: T.Text -> E.Test a -> E.Test a  | ||||
| _it :: T.Text -> E.Test a -> E.Test a | ||||
| _it = _test | ||||
| 
 | ||||
| -- | Name and group a list of tests. Combines easytest's "scope" and "tests". | ||||
| tests :: T.Text -> [E.Test ()] -> E.Test ()  | ||||
| tests :: T.Text -> [E.Test ()] -> E.Test () | ||||
| tests name = E.scope name . E.tests | ||||
| 
 | ||||
| -- | Skip the given list of tests, and any following tests in a monadic sequence, | ||||
| -- with the same type signature as "group". | ||||
| _tests :: T.Text -> [E.Test ()] -> E.Test ()  | ||||
| _tests :: T.Text -> [E.Test ()] -> E.Test () | ||||
| _tests _name = (E.skip >>) . E.tests | ||||
| 
 | ||||
| -- | Run some easytest tests, catching easytest's ExitCode exception, | ||||
| -- returning True if there was a problem. | ||||
| -- With arguments, runs only the scope (or single test) named by the first argument | ||||
| -- (exact, case sensitive).  | ||||
| -- (exact, case sensitive). | ||||
| -- If there is a second argument, it should be an integer and will be used | ||||
| -- as the seed for randomness.  | ||||
| -- as the seed for randomness. | ||||
| runEasytests :: [String] -> E.Test () -> IO Bool | ||||
| runEasytests args tests = (do | ||||
|   case args of | ||||
| @ -96,7 +96,7 @@ runEasytests args tests = (do | ||||
|   `catch` (\(_::ExitCode) -> return True) | ||||
| 
 | ||||
| -- | Like easytest's expectEq (asserts the second (actual) value equals the first (expected) value) | ||||
| -- but pretty-prints the values in the failure output.  | ||||
| -- but pretty-prints the values in the failure output. | ||||
| expectEqPP :: (Eq a, Show a, HasCallStack) => a -> a -> E.Test () | ||||
| expectEqPP expected actual = if expected == actual then E.ok else E.crash $ | ||||
|   "\nexpected:\n" <> T.pack (pshow expected) <> "\nbut got:\n" <> T.pack (pshow actual) <> "\n" | ||||
| @ -105,10 +105,10 @@ expectEqPP expected actual = if expected == actual then E.ok else E.crash $ | ||||
| is :: (Eq a, Show a, HasCallStack) => a -> a -> Test () | ||||
| is = flip expectEqPP | ||||
| 
 | ||||
| -- | Test that this stateful parser runnable in IO successfully parses  | ||||
| -- all of the given input text, showing the parse error if it fails.  | ||||
| -- | Test that this stateful parser runnable in IO successfully parses | ||||
| -- all of the given input text, showing the parse error if it fails. | ||||
| -- Suitable for hledger's JournalParser parsers. | ||||
| expectParse :: (Monoid st, Eq a, Show a, HasCallStack) =>  | ||||
| expectParse :: (Monoid st, Eq a, Show a, HasCallStack) => | ||||
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> E.Test () | ||||
| expectParse parser input = do | ||||
|   ep <- E.io (runParserT (evalStateT (parser <* eof) mempty) "" input) | ||||
| @ -135,9 +135,9 @@ expectParseE parser input = do | ||||
|              (const ok) | ||||
|              ep | ||||
| 
 | ||||
| -- | Test that this stateful parser runnable in IO fails to parse  | ||||
| -- the given input text, with a parse error containing the given string.  | ||||
| expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) =>  | ||||
| -- | Test that this stateful parser runnable in IO fails to parse | ||||
| -- the given input text, with a parse error containing the given string. | ||||
| expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) => | ||||
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> String -> E.Test () | ||||
| expectParseError parser input errstr = do | ||||
|   ep <- E.io (runParserT (evalStateT parser mempty) "" input) | ||||
| @ -173,8 +173,8 @@ expectParseErrorE parser input errstr = do | ||||
|         else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n" | ||||
| 
 | ||||
| -- | Like expectParse, but also test the parse result is an expected value, | ||||
| -- pretty-printing both if it fails.  | ||||
| expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) =>  | ||||
| -- pretty-printing both if it fails. | ||||
| expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) => | ||||
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> E.Test () | ||||
| expectParseEq parser input expected = expectParseEqOn parser input id expected | ||||
| 
 | ||||
| @ -186,9 +186,9 @@ expectParseEqE | ||||
|   -> E.Test () | ||||
| expectParseEqE parser input expected = expectParseEqOnE parser input id expected | ||||
| 
 | ||||
| -- | Like expectParseEq, but transform the parse result with the given function  | ||||
| -- | Like expectParseEq, but transform the parse result with the given function | ||||
| -- before comparing it. | ||||
| expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) =>  | ||||
| expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) => | ||||
|   StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> E.Test () | ||||
| expectParseEqOn parser input f expected = do | ||||
|   ep <- E.io $ runParserT (evalStateT (parser <* eof) mempty) "" input | ||||
|  | ||||
| @ -423,11 +423,11 @@ textWidth s = maximum $ map (T.foldr (\a b -> charWidth a + b) 0) $ T.lines s | ||||
| tests_Text = tests "Text" [ | ||||
|    tests "quoteIfSpaced" [ | ||||
|      quoteIfSpaced "a'a" `is` "a'a" | ||||
|     ,quoteIfSpaced "a\"a" `is` "a\"a"               | ||||
|     ,quoteIfSpaced "a a" `is` "\"a a\""                | ||||
|     ,quoteIfSpaced "mimi's cafe" `is` "\"mimi's cafe\""        | ||||
|     ,quoteIfSpaced "\"alex\" cafe" `is` "\"\\\"alex\\\" cafe\""      | ||||
|     ,quoteIfSpaced "le'shan's cafe" `is` "\"le'shan's cafe\""     | ||||
|     ,quoteIfSpaced "\"be'any's\" cafe" `is` "\"\\\"be'any's\\\" cafe\""  | ||||
|     ]  | ||||
|     ,quoteIfSpaced "a\"a" `is` "a\"a" | ||||
|     ,quoteIfSpaced "a a" `is` "\"a a\"" | ||||
|     ,quoteIfSpaced "mimi's cafe" `is` "\"mimi's cafe\"" | ||||
|     ,quoteIfSpaced "\"alex\" cafe" `is` "\"\\\"alex\\\" cafe\"" | ||||
|     ,quoteIfSpaced "le'shan's cafe" `is` "\"le'shan's cafe\"" | ||||
|     ,quoteIfSpaced "\"be'any's\" cafe" `is` "\"\\\"be'any's\\\" cafe\"" | ||||
|     ] | ||||
|   ] | ||||
|  | ||||
| @ -16,7 +16,7 @@ do the right thing, so this file is a no-op and on its way to being removed. | ||||
| Not carefully tested. | ||||
| 
 | ||||
| -} | ||||
| -- TODO obsolete ?  | ||||
| -- TODO obsolete ? | ||||
| 
 | ||||
| module Hledger.Utils.UTF8IOCompat ( | ||||
|   readFile, | ||||
| @ -119,5 +119,5 @@ userError' = userError . toSystemString | ||||
| 
 | ||||
| -- | A SystemString-aware version of error that adds a usage hint. | ||||
| usageError :: String -> a | ||||
| usageError = error' . (++ " (use -h to see usage)")   | ||||
| usageError = error' . (++ " (use -h to see usage)") | ||||
| 
 | ||||
|  | ||||
| @ -230,7 +230,7 @@ customErrorBundlePretty errBundle = | ||||
|     -- (since only one custom error should be used at a time). | ||||
|     findCustomError :: ParseError Text CustomErr -> Maybe CustomErr | ||||
|     findCustomError err = case err of | ||||
|       FancyError _ errSet ->  | ||||
|       FancyError _ errSet -> | ||||
|         finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet | ||||
|       _ -> Nothing | ||||
| 
 | ||||
|  | ||||
| @ -98,7 +98,7 @@ renderHLine' pretty prop is sep h = [ cross pretty, sep ] ++ coreLine ++ [sep, c | ||||
|   coreLine        = concatMap helper $ flattenHeader $ zipHeader 0 is h | ||||
|   helper          = either vsep dashes | ||||
|   dashes (i,_)    = replicate i sep | ||||
|   vsep NoLine     = replicate 2 sep  -- match the double space sep in renderColumns  | ||||
|   vsep NoLine     = replicate 2 sep  -- match the double space sep in renderColumns | ||||
|   vsep SingleLine = sep : cross pretty : [sep] | ||||
|   vsep DoubleLine = sep : cross' ++ [sep] | ||||
|   cross' = case prop of | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| {-  | ||||
| {- | ||||
| Run doctests in Hledger source files under the current directory | ||||
| (./Hledger.hs, ./Hledger/**, ./Text/**) using the doctest runner. | ||||
| 
 | ||||
| @ -7,7 +7,7 @@ Arguments are case-insensitive file path substrings, to limit the files searched | ||||
| --slow reloads ghci between each test (https://github.com/sol/doctest#a-note-on-performance). | ||||
| 
 | ||||
| Eg, in hledger source dir: | ||||
|   | ||||
| 
 | ||||
| $ make ghci-doctest, :main [--verbose] [--slow] [CIFILEPATHSUBSTRINGS] | ||||
| 
 | ||||
| or: | ||||
| @ -40,20 +40,20 @@ main = do | ||||
|     ] | ||||
| 
 | ||||
|   -- filter by patterns (case insensitive infix substring match) | ||||
|   let  | ||||
|   let | ||||
|     fs | null pats = sourcefiles | ||||
|        | otherwise = [f | f <- sourcefiles, let f' = map toLower f, any (`isInfixOf` f') pats'] | ||||
|           where pats' = map (map toLower) pats | ||||
|     fslen = length fs | ||||
|    | ||||
| 
 | ||||
|   if (null fs) | ||||
|   then do | ||||
|     putStrLn $ "No file paths found matching: " ++ unwords pats | ||||
| 
 | ||||
|   else do | ||||
|     putStrLn $  | ||||
|       "Loading and searching for doctests in "  | ||||
|       ++ show fslen  | ||||
|     putStrLn $ | ||||
|       "Loading and searching for doctests in " | ||||
|       ++ show fslen | ||||
|       ++ if fslen > 1 then " files, plus any files they import:" else " file, plus any files it imports:" | ||||
|     when verbose $ putStrLn $ unwords fs | ||||
| 
 | ||||
|  | ||||
| @ -71,7 +71,7 @@ asInit d reset ui@UIState{ | ||||
|         selidx = case (reset, listSelectedElement $ _asList s) of | ||||
|                    (True, _)               -> 0 | ||||
|                    (_, Nothing)            -> 0 | ||||
|                    (_, Just (_,AccountsScreenItem{asItemAccountName=a})) ->  | ||||
|                    (_, Just (_,AccountsScreenItem{asItemAccountName=a})) -> | ||||
|                      headDef 0 $ catMaybes [ | ||||
|                        findIndex (a ==) as | ||||
|                       ,findIndex (a `isAccountNamePrefixOf`) as | ||||
| @ -88,7 +88,7 @@ asInit d reset ui@UIState{ | ||||
|     pfq | presentorfuture_ uopts == PFFuture = Any | ||||
|         | otherwise                          = Date $ DateSpan Nothing (Just $ addDays 1 d) | ||||
|     q = And [queryFromOpts d ropts, pfq] | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|     -- run the report | ||||
|     (items,_total) = report ropts' q j | ||||
| @ -104,14 +104,14 @@ asInit d reset ui@UIState{ | ||||
|     displayitem (fullacct, shortacct, indent, bal) = | ||||
|       AccountsScreenItem{asItemIndentLevel        = indent | ||||
|                         ,asItemAccountName        = fullacct | ||||
|                         ,asItemDisplayAccountName = replaceHiddenAccountsNameWith "All" $ if tree_ ropts' then shortacct else fullacct  | ||||
|                         ,asItemDisplayAccountName = replaceHiddenAccountsNameWith "All" $ if tree_ ropts' then shortacct else fullacct | ||||
|                         ,asItemRenderedAmounts    = map showAmountWithoutPrice amts -- like showMixedAmountOneLineWithoutPrice | ||||
|                         } | ||||
|       where | ||||
|         Mixed amts = normaliseMixedAmountSquashPricesForDisplay $ stripPrices bal | ||||
|         stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=Nothing} | ||||
|     displayitems = map displayitem items | ||||
|     -- blanks added for scrolling control, cf RegisterScreen  | ||||
|     -- blanks added for scrolling control, cf RegisterScreen | ||||
|     blankitems = replicate 100 | ||||
|       AccountsScreenItem{asItemIndentLevel        = 0 | ||||
|                         ,asItemAccountName        = "" | ||||
| @ -201,7 +201,7 @@ asDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} | ||||
|             curidx = case _asList s ^. listSelectedL of | ||||
|                        Nothing -> "-" | ||||
|                        Just i -> show (i + 1) | ||||
|             totidx = show $ V.length nonblanks  | ||||
|             totidx = show $ V.length nonblanks | ||||
|               where | ||||
|                 nonblanks = V.takeWhile (not . T.null . asItemAccountName) $ s ^. asList . listElementsL | ||||
| 
 | ||||
| @ -215,7 +215,7 @@ asDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} | ||||
|               ,("-+", str "depth") | ||||
|               ,("T", renderToggle (tree_ ropts) "flat" "tree") | ||||
|               ,("H", renderToggle (not ishistorical) "end-bals" "changes") | ||||
|               ,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future")  | ||||
|               ,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future") | ||||
|               --,("/", "filter") | ||||
|               --,("DEL", "unfilter") | ||||
|               --,("ESC", "cancel/top") | ||||
| @ -346,14 +346,14 @@ asHandle ui0@UIState{ | ||||
|         VtyEvent (EvKey (KChar 'l') [MCtrl]) -> scrollSelectionToMiddle _asList >> redraw ui | ||||
|         VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui | ||||
| 
 | ||||
|         -- enter register screen for selected account (if there is one),  | ||||
|         -- enter register screen for selected account (if there is one), | ||||
|         -- centering its selected transaction if possible | ||||
|         VtyEvent e | e `elem` moveRightEvents  | ||||
|         VtyEvent e | e `elem` moveRightEvents | ||||
|                    , not $ isBlankElement $ listSelectedElement _asList-> | ||||
|           -- TODO center selection after entering register screen; neither of these works till second time entering; easy strictifications didn't help  | ||||
|           rsCenterAndContinue $   | ||||
|           -- TODO center selection after entering register screen; neither of these works till second time entering; easy strictifications didn't help | ||||
|           rsCenterAndContinue $ | ||||
|           -- flip rsHandle (VtyEvent (EvKey (KChar 'l') [MCtrl])) $ | ||||
|             screenEnter d regscr ui  | ||||
|             screenEnter d regscr ui | ||||
|           where | ||||
|             regscr = rsSetAccount selacct isdepthclipped registerScreen | ||||
|             isdepthclipped = case getDepth ui of | ||||
| @ -363,9 +363,9 @@ asHandle ui0@UIState{ | ||||
|         -- prevent moving down over blank padding items; | ||||
|         -- instead scroll down by one, until maximally scrolled - shows the end has been reached | ||||
|         VtyEvent (EvKey (KDown)     []) | isBlankElement mnextelement -> do | ||||
|           vScrollBy (viewportScroll $ _asList^.listNameL) 1  | ||||
|           vScrollBy (viewportScroll $ _asList^.listNameL) 1 | ||||
|           continue ui | ||||
|           where  | ||||
|           where | ||||
|             mnextelement = listSelectedElement $ listMoveDown _asList | ||||
| 
 | ||||
|         -- if page down or end leads to a blank padding item, stop at last non-blank | ||||
| @ -378,7 +378,7 @@ asHandle ui0@UIState{ | ||||
|             continue ui{aScreen=scr{_asList=list'}} | ||||
|           else | ||||
|             continue ui{aScreen=scr{_asList=list}} | ||||
|            | ||||
| 
 | ||||
|         -- fall through to the list's event handler (handles up/down) | ||||
|         VtyEvent ev -> do | ||||
|           newitems <- handleListEvent (normaliseMovementKeys ev) _asList | ||||
| @ -398,7 +398,7 @@ asHandle _ _ = error "event handler called with wrong screen type, should not ha | ||||
| asSetSelectedAccount a s@AccountsScreen{} = s & asSelectedAccount .~ a | ||||
| asSetSelectedAccount _ s = s | ||||
| 
 | ||||
| isBlankElement mel = ((asItemAccountName . snd) <$> mel) == Just ""  | ||||
| isBlankElement mel = ((asItemAccountName . snd) <$> mel) == Just "" | ||||
| 
 | ||||
| asCenterAndContinue ui = do | ||||
|   scrollSelectionToMiddle $ _asList $ aScreen ui | ||||
|  | ||||
| @ -163,7 +163,7 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{inputopts_=_iopts,reportopts_=rop | ||||
|       } | ||||
| 
 | ||||
|   -- print (length (show ui)) >> exitSuccess  -- show any debug output to this point & quit | ||||
|    | ||||
| 
 | ||||
|   if not (watch_ uopts') | ||||
|   then | ||||
|     void $ defaultMain brickapp ui | ||||
|  | ||||
| @ -112,10 +112,10 @@ rsInit d reset ui@UIState{aopts=uopts@UIOpts{cliopts_=CliOpts{reportopts_=ropts} | ||||
|     -- otherwise, the previously selected transaction if possible; | ||||
|     -- otherwise, the transaction nearest in date to it; | ||||
|     -- or if there's several with the same date, the nearest in journal order; | ||||
|     -- otherwise, the last (latest) transaction.  | ||||
|     -- otherwise, the last (latest) transaction. | ||||
|     newitems' = listMoveTo newselidx newitems | ||||
|       where | ||||
|         newselidx =  | ||||
|         newselidx = | ||||
|           case (reset, listSelectedElement rsList) of | ||||
|             (True, _)    -> endidx | ||||
|             (_, Nothing) -> endidx | ||||
| @ -164,7 +164,7 @@ rsDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} | ||||
|         changewidthproportion = fromIntegral maxchangewidthseen / fromIntegral (maxchangewidthseen + maxbalwidthseen) | ||||
|         maxchangewidth = round $ changewidthproportion * fromIntegral maxamtswidth | ||||
|         maxbalwidth = maxamtswidth - maxchangewidth | ||||
|         changewidth = min maxchangewidth maxchangewidthseen  | ||||
|         changewidth = min maxchangewidth maxchangewidthseen | ||||
|         balwidth = min maxbalwidth maxbalwidthseen | ||||
|         -- assign the remaining space to the description and accounts columns | ||||
|         -- maxdescacctswidth = totalwidth - (whitespacewidth - 4) - changewidth - balwidth | ||||
| @ -177,7 +177,7 @@ rsDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} | ||||
|         -- descwidthproportion = (descwidth' + acctswidth') / descwidth' | ||||
|         -- maxdescwidth = min (maxdescacctswidth - 7) (maxdescacctswidth / descwidthproportion) | ||||
|         -- maxacctswidth = maxdescacctswidth - maxdescwidth | ||||
|         -- descwidth = min maxdescwidth descwidth'  | ||||
|         -- descwidth = min maxdescwidth descwidth' | ||||
|         -- acctswidth = min maxacctswidth acctswidth' | ||||
|         -- allocating equally. | ||||
|         descwidth = maxdescacctswidth `div` 2 | ||||
| @ -232,7 +232,7 @@ rsDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} | ||||
| --              ,("RIGHT", str "transaction") | ||||
|               ,("T", renderToggle (tree_ ropts) "flat(-subs)" "tree(+subs)") -- rsForceInclusive may override, but use tree_ to ensure a visible toggle effect | ||||
|               ,("H", renderToggle (not ishistorical) "historical" "period") | ||||
|               ,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future")  | ||||
|               ,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future") | ||||
| --               ,("a", "add") | ||||
| --               ,("g", "reload") | ||||
| --               ,("q", "quit") | ||||
| @ -271,11 +271,11 @@ rsHandle ui@UIState{ | ||||
|   ,aMode=mode | ||||
|   } ev = do | ||||
|   d <- liftIO getCurrentDay | ||||
|   let  | ||||
|   let | ||||
|     journalspan = journalDateSpan False j | ||||
|     nonblanks = V.takeWhile (not . null . rsItemDate) $ rsList^.listElementsL | ||||
|     lastnonblankidx = max 0 (length nonblanks - 1) | ||||
|    | ||||
| 
 | ||||
|   case mode of | ||||
|     Minibuffer ed -> | ||||
|       case ev of | ||||
| @ -358,9 +358,9 @@ rsHandle ui@UIState{ | ||||
|         -- prevent moving down over blank padding items; | ||||
|         -- instead scroll down by one, until maximally scrolled - shows the end has been reached | ||||
|         VtyEvent e | e `elem` moveDownEvents, isBlankElement mnextelement -> do | ||||
|           vScrollBy (viewportScroll $ rsList^.listNameL) 1  | ||||
|           vScrollBy (viewportScroll $ rsList^.listNameL) 1 | ||||
|           continue ui | ||||
|           where  | ||||
|           where | ||||
|             mnextelement = listSelectedElement $ listMoveDown rsList | ||||
| 
 | ||||
|         -- if page down or end leads to a blank padding item, stop at last non-blank | ||||
| @ -373,7 +373,7 @@ rsHandle ui@UIState{ | ||||
|             continue ui{aScreen=s{rsList=list'}} | ||||
|           else | ||||
|             continue ui{aScreen=s{rsList=list}} | ||||
|            | ||||
| 
 | ||||
|         -- fall through to the list's event handler (handles other [pg]up/down events) | ||||
|         VtyEvent ev -> do | ||||
|           let ev' = normaliseMovementKeys ev | ||||
| @ -386,7 +386,7 @@ rsHandle ui@UIState{ | ||||
| 
 | ||||
| rsHandle _ _ = error "event handler called with wrong screen type, should not happen" | ||||
| 
 | ||||
| isBlankElement mel = ((rsItemDate . snd) <$> mel) == Just ""  | ||||
| isBlankElement mel = ((rsItemDate . snd) <$> mel) == Just "" | ||||
| 
 | ||||
| rsCenterAndContinue ui = do | ||||
|   scrollSelectionToMiddle $ rsList $ aScreen ui | ||||
|  | ||||
| @ -73,7 +73,7 @@ themesList = [ | ||||
|     ,("border" <> "bold"                              , currentAttr & bold) | ||||
|     ,("border" <> "depth"                             , active) | ||||
|     ,("border" <> "filename"                          , currentAttr) | ||||
|     ,("border" <> "key"                               , active)   | ||||
|     ,("border" <> "key"                               , active) | ||||
|     ,("border" <> "minibuffer"                        , white `on` black & bold) | ||||
|     ,("border" <> "query"                             , active) | ||||
|     ,("border" <> "selected"                          , active) | ||||
|  | ||||
| @ -136,7 +136,7 @@ tsHandle ui@UIState{aScreen=s@TransactionScreen{tsTransaction=(i,t) | ||||
|           where | ||||
|             (pos,f) = case tsourcepos t of | ||||
|                         GenericSourcePos f l c    -> (Just (l, Just c),f) | ||||
|                         JournalSourcePos f (l1,_) -> (Just (l1, Nothing),f)  | ||||
|                         JournalSourcePos f (l1,_) -> (Just (l1, Nothing),f) | ||||
|         AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old -> | ||||
|           continue $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui | ||||
|           where | ||||
|  | ||||
| @ -85,7 +85,7 @@ rawOptsToUIOpts rawopts = checkUIOpts <$> do | ||||
|              ,cliopts_ = cliopts | ||||
|              } | ||||
| 
 | ||||
| -- | Should transactions dated later than today be included ?  | ||||
| -- | Should transactions dated later than today be included ? | ||||
| -- Like flat/tree mode, there are three states, and the meaning of default can vary by command. | ||||
| data PresentOrFutureOpt = PFDefault | PFPresent | PFFuture deriving (Eq, Show, Data, Typeable) | ||||
| instance Default PresentOrFutureOpt where def = PFDefault | ||||
| @ -109,7 +109,7 @@ getHledgerUIOpts :: IO UIOpts | ||||
| --getHledgerUIOpts = processArgs uimode >>= return . decodeRawOpts >>= rawOptsToUIOpts | ||||
| getHledgerUIOpts = do | ||||
|   args <- getArgs >>= expandArgsAt | ||||
|   let args' = replaceNumericFlags args  | ||||
|   let args' = replaceNumericFlags args | ||||
|   let cmdargopts = either usageError id $ process uimode args' | ||||
|   rawOptsToUIOpts $ decodeRawOpts cmdargopts  | ||||
|   rawOptsToUIOpts $ decodeRawOpts cmdargopts | ||||
| 
 | ||||
|  | ||||
| @ -35,15 +35,15 @@ toggleCleared :: UIState -> UIState | ||||
| toggleCleared ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = | ||||
|   ui{aopts=uopts{cliopts_=copts{reportopts_=reportOptsToggleStatusSomehow Cleared copts ropts}}} | ||||
| 
 | ||||
| -- TODO testing different status toggle styles  | ||||
| -- TODO testing different status toggle styles | ||||
| 
 | ||||
| -- | Generate zero or more indicators of the status filters currently active,  | ||||
| -- | Generate zero or more indicators of the status filters currently active, | ||||
| -- which will be shown comma-separated as part of the indicators list. | ||||
| uiShowStatus :: CliOpts -> [Status] -> [String] | ||||
| uiShowStatus copts ss = | ||||
|   case style of | ||||
|     -- in style 2, instead of "Y, Z" show "not X"  | ||||
|     Just 2 | length ss == numstatuses-1  | ||||
|     -- in style 2, instead of "Y, Z" show "not X" | ||||
|     Just 2 | length ss == numstatuses-1 | ||||
|       -> map (("not "++). showstatus) $ sort $ complement ss  -- should be just one | ||||
|     _ -> map showstatus $ sort ss | ||||
|   where | ||||
| @ -55,7 +55,7 @@ uiShowStatus copts ss = | ||||
| 
 | ||||
| reportOptsToggleStatusSomehow :: Status -> CliOpts -> ReportOpts -> ReportOpts | ||||
| reportOptsToggleStatusSomehow s copts ropts = | ||||
|   case maybeintopt "status-toggles" $ rawopts_ copts of  | ||||
|   case maybeintopt "status-toggles" $ rawopts_ copts of | ||||
|      Just 2 -> reportOptsToggleStatus2 s ropts | ||||
|      Just 3 -> reportOptsToggleStatus3 s ropts | ||||
| --     Just 4 -> reportOptsToggleStatus4 s ropts | ||||
| @ -78,7 +78,7 @@ reportOptsToggleStatus1 s ropts@ReportOpts{statuses_=ss} | ||||
| reportOptsToggleStatus2 s ropts@ReportOpts{statuses_=ss} | ||||
|   | ss == [s]            = ropts{statuses_=complement [s]} | ||||
|   | ss == complement [s] = ropts{statuses_=[]} | ||||
|   | otherwise            = ropts{statuses_=[s]}  -- XXX assume only three values  | ||||
|   | otherwise            = ropts{statuses_=[s]}  -- XXX assume only three values | ||||
| 
 | ||||
| -- 3 UPC toggles each X | ||||
| reportOptsToggleStatus3 s ropts@ReportOpts{statuses_=ss} | ||||
|  | ||||
| @ -59,7 +59,7 @@ suspendSignal = return () | ||||
| #else | ||||
| import System.Posix.Signals | ||||
| suspendSignal :: IO () | ||||
| suspendSignal = raiseSignal sigSTOP  | ||||
| suspendSignal = raiseSignal sigSTOP | ||||
| #endif | ||||
| 
 | ||||
| -- | On posix platforms, suspend the program using the STOP signal, | ||||
| @ -121,7 +121,7 @@ helpDialog _copts = | ||||
|                 vBox [ | ||||
|                    withAttr ("help" <> "heading") $ str "Filtering" | ||||
|                   ,renderKey ("/   ", "set a filter query") | ||||
|                   ,renderKey ("UPC ", "show unmarked/pending/cleared")  | ||||
|                   ,renderKey ("UPC ", "show unmarked/pending/cleared") | ||||
|                   ,renderKey ("F   ", "show future/present txns") | ||||
|                   ,renderKey ("R   ", "show real/all postings") | ||||
|                   ,renderKey ("Z   ", "show nonzero/all amounts") | ||||
| @ -208,12 +208,12 @@ borderKeysStr' keydescs = | ||||
|     -- sep = str " | " | ||||
|     sep = str " " | ||||
| 
 | ||||
| -- | Render the two states of a toggle, highlighting the active one.  | ||||
| -- | Render the two states of a toggle, highlighting the active one. | ||||
| renderToggle :: Bool -> String -> String -> Widget Name | ||||
| renderToggle isright l r = | ||||
|   let bold = withAttr ("border" <> "selected") in | ||||
|   if isright | ||||
|   then str (l++"/") <+> bold (str r)  | ||||
|   then str (l++"/") <+> bold (str r) | ||||
|   else bold (str l) <+> str ("/"++r) | ||||
| 
 | ||||
| -- temporary shenanigans: | ||||
| @ -310,13 +310,13 @@ withBorderAttr attr = updateAttrMap (applyAttrMappings [("border", attr)]) | ||||
| --scrollToTop :: List Name e -> EventM Name () | ||||
| --scrollToTop list = do | ||||
| --  let vpname = list^.listNameL | ||||
| --  setTop (viewportScroll vpname) 0  | ||||
| --  setTop (viewportScroll vpname) 0 | ||||
| 
 | ||||
| -- | Scroll a list's viewport so that the selected item is centered in the | ||||
| -- middle of the display area. | ||||
| scrollSelectionToMiddle :: List Name e -> EventM Name () | ||||
| scrollSelectionToMiddle list = do | ||||
|   let mselectedrow = list^.listSelectedL  | ||||
|   let mselectedrow = list^.listSelectedL | ||||
|       vpname = list^.listNameL | ||||
|   mvp <- lookupViewport vpname | ||||
|   case (mselectedrow, mvp) of | ||||
| @ -326,7 +326,7 @@ scrollSelectionToMiddle list = do | ||||
|         vpheight     = dbg4 "vpheight" $ vp^.vpSize._2 | ||||
|         itemsperpage = dbg4 "itemsperpage" $ vpheight `div` itemheight | ||||
|         toprow       = dbg4 "toprow" $ max 0 (selectedrow - (itemsperpage `div` 2)) -- assuming ViewportScroll's row offset is measured in list items not screen rows | ||||
|       setTop (viewportScroll vpname) toprow  | ||||
|       setTop (viewportScroll vpname) toprow | ||||
|     _ -> return () | ||||
| 
 | ||||
| --                 arrow keys       vi keys               emacs keys | ||||
|  | ||||
| @ -17,7 +17,7 @@ import Yesod.Default.Config | ||||
| import Hledger.Data (Journal, nulljournal) | ||||
| 
 | ||||
| import Hledger.Web.Handler.AddR | ||||
| import Hledger.Web.Handler.MiscR  | ||||
| import Hledger.Web.Handler.MiscR | ||||
| import Hledger.Web.Handler.EditR | ||||
| import Hledger.Web.Handler.UploadR | ||||
| import Hledger.Web.Handler.JournalR | ||||
|  | ||||
| @ -55,7 +55,7 @@ postAddR = do | ||||
|       |] | ||||
| 
 | ||||
| -- Add a single new transaction, send as JSON via PUT, to the journal. | ||||
| -- The web form handler above should probably use PUT as well.   | ||||
| -- The web form handler above should probably use PUT as well. | ||||
| putAddR :: Handler RepJson | ||||
| putAddR = do | ||||
|   VD{caps, j, opts} <- getViewData | ||||
| @ -66,4 +66,4 @@ putAddR = do | ||||
|     Error err -> sendStatusJSON status400 ("could not parse json: " ++ err ::String) | ||||
|     Success t -> do | ||||
|       void $ liftIO $ journalAddTransaction j (cliopts_ opts) t | ||||
|       sendResponseCreated TransactionsR  | ||||
|       sendResponseCreated TransactionsR | ||||
|  | ||||
| @ -7,11 +7,11 @@ | ||||
| {-# LANGUAGE TemplateHaskell #-} | ||||
| 
 | ||||
| module Hledger.Web.Handler.MiscR | ||||
|   ( getAccountnamesR        | ||||
|   , getTransactionsR        | ||||
|   , getPricesR              | ||||
|   , getCommoditiesR         | ||||
|   , getAccountsR            | ||||
|   ( getAccountnamesR | ||||
|   , getTransactionsR | ||||
|   , getPricesR | ||||
|   , getCommoditiesR | ||||
|   , getAccountsR | ||||
|   , getAccounttransactionsR | ||||
|   , getDownloadR | ||||
|   , getFaviconR | ||||
|  | ||||
| @ -21,7 +21,7 @@ | ||||
| --{-# LANGUAGE TypeFamilies        #-} | ||||
| --{-# LANGUAGE TypeOperators       #-} | ||||
| 
 | ||||
| module Hledger.Web.Json (  | ||||
| module Hledger.Web.Json ( | ||||
|   -- * Instances | ||||
|   -- * Utilities | ||||
|    readJsonFile | ||||
| @ -66,11 +66,11 @@ instance ToJSON Posting where | ||||
|     ,"ptype"             .= toJSON ptype | ||||
|     ,"ptags"             .= toJSON ptags | ||||
|     ,"pbalanceassertion" .= toJSON pbalanceassertion | ||||
|     -- To avoid a cycle, show just the parent transaction's index number  | ||||
|     -- To avoid a cycle, show just the parent transaction's index number | ||||
|     -- in a dummy field. When re-parsed, there will be no parent. | ||||
|     ,"ptransaction_"     .= toJSON (maybe "" (show.tindex) ptransaction) | ||||
|     -- This is probably not wanted in json, we discard it. | ||||
|     ,"poriginal"         .= toJSON (Nothing :: Maybe Posting)  | ||||
|     ,"poriginal"         .= toJSON (Nothing :: Maybe Posting) | ||||
|     ] | ||||
| 
 | ||||
| instance ToJSON Transaction | ||||
| @ -82,7 +82,7 @@ instance ToJSON Account where | ||||
|     ,"aibalance"    .= toJSON (aibalance a) | ||||
|     ,"anumpostings" .= toJSON (anumpostings a) | ||||
|     ,"aboring"      .= toJSON (aboring a) | ||||
|     -- To avoid a cycle, show just the parent account's name  | ||||
|     -- To avoid a cycle, show just the parent account's name | ||||
|     -- in a dummy field. When re-parsed, there will be no parent. | ||||
|     ,"aparent_"     .= toJSON (maybe "" aname $ aparent a) | ||||
|     -- Just the names of subaccounts, as a dummy field, ignored when parsed. | ||||
| @ -110,14 +110,14 @@ instance FromJSON Posting | ||||
| instance FromJSON Transaction | ||||
| instance FromJSON AccountDeclarationInfo | ||||
| -- XXX The ToJSON instance replaces subaccounts with just names. | ||||
| -- Here we should try to make use of those to reconstruct the  | ||||
| -- Here we should try to make use of those to reconstruct the | ||||
| -- parent-child relationships. | ||||
| instance FromJSON Account | ||||
| 
 | ||||
| -- Decimal, various attempts | ||||
| -- | ||||
| -- https://stackoverflow.com/questions/40331851/haskell-data-decimal-as-aeson-type | ||||
| ----instance FromJSON Decimal where parseJSON =  | ||||
| ----instance FromJSON Decimal where parseJSON = | ||||
| ----  A.withScientific "Decimal" (return . right . eitherFromRational . toRational) | ||||
| -- | ||||
| -- https://github.com/bos/aeson/issues/474 | ||||
| @ -156,7 +156,7 @@ instance FromJSON (DecimalRaw Integer) | ||||
| readJsonFile :: FromJSON a => FilePath -> IO a | ||||
| readJsonFile f = do | ||||
|   bs <- BL.readFile f | ||||
|   let v = fromMaybe (error "could not decode bytestring as json value") (decode bs :: Maybe Value)  | ||||
|   let v = fromMaybe (error "could not decode bytestring as json value") (decode bs :: Maybe Value) | ||||
|   case fromJSON v :: FromJSON a => Result a of | ||||
|     Error e   -> error e | ||||
|     Success t -> return t | ||||
|  | ||||
| @ -282,7 +282,7 @@ type CommandDoc = String | ||||
| -- from a help template and flag/argument specifications. | ||||
| -- Reduces boilerplate a little, though the complicated cmdargs | ||||
| -- flag and argument specs are still required. | ||||
| hledgerCommandMode :: CommandDoc -> [Flag RawOpts] -> [(String, [Flag RawOpts])]  | ||||
| hledgerCommandMode :: CommandDoc -> [Flag RawOpts] -> [(String, [Flag RawOpts])] | ||||
|   -> [Flag RawOpts] -> ([Arg RawOpts], Maybe (Arg RawOpts)) -> Mode RawOpts | ||||
| hledgerCommandMode doc unnamedflaggroup namedflaggroups hiddenflaggroup argsdescr = | ||||
|   case parseCommandDoc doc of | ||||
| @ -404,7 +404,7 @@ defaultWidth :: Int | ||||
| defaultWidth = 80 | ||||
| 
 | ||||
| -- | Replace any numeric flags (eg -2) with their long form (--depth 2), | ||||
| -- as I'm guessing cmdargs doesn't support this directly.   | ||||
| -- as I'm guessing cmdargs doesn't support this directly. | ||||
| replaceNumericFlags :: [String] -> [String] | ||||
| replaceNumericFlags = map replace | ||||
|   where | ||||
| @ -452,10 +452,10 @@ checkCliOpts opts = | ||||
|       Right _  -> Right () | ||||
|   -- XXX check registerWidthsFromOpts opts | ||||
| 
 | ||||
| -- | A helper for addon commands: this parses options and arguments from  | ||||
| -- the current command line using the given hledger-style cmdargs mode,  | ||||
| -- and returns a CliOpts. Or, with --help or -h present, it prints  | ||||
| -- long or short help, and exits the program.  | ||||
| -- | A helper for addon commands: this parses options and arguments from | ||||
| -- the current command line using the given hledger-style cmdargs mode, | ||||
| -- and returns a CliOpts. Or, with --help or -h present, it prints | ||||
| -- long or short help, and exits the program. | ||||
| -- When --debug is present, also prints some debug output. | ||||
| -- Note this is not used by the main hledger executable. | ||||
| -- | ||||
| @ -472,7 +472,7 @@ checkCliOpts opts = | ||||
| -- hledger options not displayed. | ||||
| -- | ||||
| -- Tips: | ||||
| -- Empty lines in the pre/postamble are removed by cmdargs;  | ||||
| -- Empty lines in the pre/postamble are removed by cmdargs; | ||||
| -- add a space character to preserve them. | ||||
| -- | ||||
| getHledgerCliOpts :: Mode RawOpts -> IO CliOpts | ||||
| @ -640,7 +640,7 @@ defaultBalanceLineFormat = BottomAligned [ | ||||
| -- or more versions (or two versions that don't look like a | ||||
| -- source/compiled pair), they are all included, with file extensions | ||||
| -- intact. | ||||
| --  | ||||
| -- | ||||
| hledgerAddons :: IO [String] | ||||
| hledgerAddons = do | ||||
|   -- past bug generator | ||||
| @ -658,10 +658,10 @@ dropRedundantSourceVersion [f,g] | ||||
|   | takeExtension g `elem` compiledExts = [g] | ||||
| dropRedundantSourceVersion fs = fs | ||||
| 
 | ||||
| compiledExts = ["",".com",".exe"]  | ||||
| compiledExts = ["",".com",".exe"] | ||||
| 
 | ||||
| 
 | ||||
| -- | Get all sorted unique filenames in the current user's PATH.  | ||||
| -- | Get all sorted unique filenames in the current user's PATH. | ||||
| -- We do not currently filter out non-file objects or files without execute permission. | ||||
| likelyExecutablesInPath :: IO [String] | ||||
| likelyExecutablesInPath = do | ||||
| @ -677,8 +677,8 @@ likelyExecutablesInPath = do | ||||
| 
 | ||||
| -- | Get the sorted unique filenames of all hledger-* executables in | ||||
| -- the current user's PATH. These are files in any of the PATH directories, | ||||
| -- named hledger-*, with either no extension (and no periods in the name)  | ||||
| -- or one of the addonExtensions.  | ||||
| -- named hledger-*, with either no extension (and no periods in the name) | ||||
| -- or one of the addonExtensions. | ||||
| -- We do not currently filter out non-file objects or files without execute permission. | ||||
| hledgerExecutablesInPath :: IO [String] | ||||
| hledgerExecutablesInPath = filter isHledgerExeName <$> likelyExecutablesInPath | ||||
|  | ||||
| @ -66,24 +66,24 @@ accounts CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do | ||||
|       accts                = dbg5 "accts to show" $ -- no need to nub/sort, accountTree will | ||||
|         if | declared     && not used -> matcheddeclaredaccts | ||||
|            | not declared && used     -> matchedusedaccts | ||||
|            | otherwise                -> matcheddeclaredaccts ++ matchedusedaccts  | ||||
|            | otherwise                -> matcheddeclaredaccts ++ matchedusedaccts | ||||
| 
 | ||||
|   -- 2. sort them by declaration order and name, at each level of their tree structure | ||||
|       sortedaccts = sortAccountNamesByDeclaration j tree accts | ||||
| 
 | ||||
|   -- 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 = | ||||
|         dbg1 "clippedaccts" $ | ||||
|         filter (matchesAccount acctq) $  -- clipping can leave accounts that no longer match the query, remove such | ||||
|         nub $                          -- clipping can leave duplicates (adjacent, hopefully) | ||||
|         filter (not . T.null) $        -- depth:0 can leave nulls | ||||
|         map (clipAccountName depth) $  -- clip at depth if specified  | ||||
|         sortedaccts  | ||||
|         map (clipAccountName depth) $  -- clip at depth if specified | ||||
|         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 | ||||
|   mapM_ (T.putStrLn . render) clippedaccts | ||||
|     where | ||||
|       render a  | ||||
|       render a | ||||
|         | tree_ ropts = T.replicate (2 * (accountNameLevel a - 1)) " " <> accountLeafName a | ||||
|         | otherwise   = accountNameDrop (drop_ ropts) a | ||||
| 
 | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| 
 | ||||
| {-| | ||||
| 
 | ||||
| Print a bar chart of posting activity per day, or other report interval.  | ||||
| Print a bar chart of posting activity per day, or other report interval. | ||||
| 
 | ||||
| -} | ||||
| 
 | ||||
|  | ||||
| @ -320,7 +320,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do | ||||
|               "html" -> const $ error' "Sorry, HTML output is not yet implemented for this kind of report."  -- TODO | ||||
|               _      -> budgetReportAsText ropts | ||||
|         writeOutput opts $ render budgetreport | ||||
|            | ||||
| 
 | ||||
|       else | ||||
|         if multiperiod then do  -- multi period balance report | ||||
|           let report = multiBalanceReport ropts (queryFromOpts d ropts) j | ||||
| @ -337,7 +337,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do | ||||
|                                | otherwise   = ropts{accountlistmode_=ALTree} | ||||
|                     in balanceReportFromMultiBalanceReport ropts' (queryFromOpts d ropts) j | ||||
|                           -- for historical balances we must use balanceReportFromMultiBalanceReport (also forces --no-elide) | ||||
|                 | otherwise = balanceReport ropts (queryFromOpts d ropts) j -- simple Ledger-style balance report  | ||||
|                 | otherwise = balanceReport ropts (queryFromOpts d ropts) j -- simple Ledger-style balance report | ||||
|               render = case format of | ||||
|                 "csv"  -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r | ||||
|                 "html" -> \_ _ -> error' "Sorry, HTML output is not yet implemented for this kind of report."  -- TODO | ||||
| @ -458,7 +458,7 @@ renderComponent1 opts (acctname, depth, total) (FormatField ljust min max field) | ||||
| -- and will include the final totals row unless --no-total is set. | ||||
| multiBalanceReportAsCsv :: ReportOpts -> MultiBalanceReport -> CSV | ||||
| multiBalanceReportAsCsv opts@ReportOpts{average_, row_total_} (MultiBalanceReport (colspans, items, (coltotals,tot,avg))) = | ||||
|   maybetranspose $  | ||||
|   maybetranspose $ | ||||
|   ("Account" : map showDateSpan colspans | ||||
|    ++ ["Total"   | row_total_] | ||||
|    ++ ["Average" | average_] | ||||
| @ -481,7 +481,7 @@ multiBalanceReportAsCsv opts@ReportOpts{average_, row_total_} (MultiBalanceRepor | ||||
|   where | ||||
|     maybetranspose | transpose_ opts = transpose | ||||
|                    | otherwise = id | ||||
|      | ||||
| 
 | ||||
| -- | Render a multi-column balance report as HTML. | ||||
| multiBalanceReportAsHtml :: ReportOpts -> MultiBalanceReport -> Html () | ||||
| multiBalanceReportAsHtml ropts mbr = | ||||
| @ -505,7 +505,7 @@ multiBalanceReportHtmlRows ropts mbr = | ||||
|   in | ||||
|     (multiBalanceReportHtmlHeadRow ropts headingsrow | ||||
|     ,map (multiBalanceReportHtmlBodyRow ropts) bodyrows | ||||
|     ,multiBalanceReportHtmlFootRow ropts <$> mtotalsrow -- TODO pad totals row with zeros when there are  | ||||
|     ,multiBalanceReportHtmlFootRow ropts <$> mtotalsrow -- TODO pad totals row with zeros when there are | ||||
|     ) | ||||
| 
 | ||||
| -- | Render one MultiBalanceReport heading row as a HTML table row. | ||||
| @ -548,8 +548,8 @@ multiBalanceReportHtmlBodyRow ropts (label:rest) = | ||||
| multiBalanceReportHtmlFootRow :: ReportOpts -> [String] -> Html () | ||||
| multiBalanceReportHtmlFootRow _ropts [] = mempty | ||||
| -- TODO pad totals row with zeros when subreport is empty | ||||
| --  multiBalanceReportHtmlFootRow ropts $  | ||||
| --     ""  | ||||
| --  multiBalanceReportHtmlFootRow ropts $ | ||||
| --     "" | ||||
| --   : repeat nullmixedamt zeros | ||||
| --  ++ (if row_total_ ropts then [nullmixedamt] else []) | ||||
| --  ++ (if average_ ropts   then [nullmixedamt]   else []) | ||||
| @ -597,7 +597,7 @@ multiBalanceReportAsText ropts@ReportOpts{..} r = | ||||
| balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount | ||||
| balanceReportAsTable opts@ReportOpts{average_, row_total_, balancetype_} (MultiBalanceReport (colspans, items, (coltotals,tot,avg))) = | ||||
|    maybetranspose $ | ||||
|    addtotalrow $  | ||||
|    addtotalrow $ | ||||
|    Table | ||||
|      (T.Group NoLine $ map Header accts) | ||||
|      (T.Group NoLine $ map Header colheadings) | ||||
| @ -625,7 +625,7 @@ balanceReportAsTable opts@ReportOpts{average_, row_total_, balancetype_} (MultiB | ||||
|                                     )) | ||||
|     maybetranspose | transpose_ opts = \(Table rh ch vals) -> Table ch rh (transpose vals) | ||||
|                    | otherwise       = id | ||||
|                     | ||||
| 
 | ||||
| -- | Given a table representing a multi-column balance report (for example, | ||||
| -- made using 'balanceReportAsTable'), render it in a format suitable for | ||||
| -- console output. | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| module Hledger.Cli.Commands.Checkdupes ( | ||||
|   checkdupesmode | ||||
|  ,checkdupes | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Data.Function | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| module Hledger.Cli.Commands.Close ( | ||||
|   closemode | ||||
|  ,close | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Control.Monad (when) | ||||
| @ -29,8 +29,8 @@ closemode = hledgerCommandMode | ||||
| 
 | ||||
| close CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do | ||||
|   today <- getCurrentDay | ||||
|   let  | ||||
|       (opening, closing) =  | ||||
|   let | ||||
|       (opening, closing) = | ||||
|         case (boolopt "opening" rawopts, boolopt "closing" rawopts) of | ||||
|           (False, False) -> (True, True) -- by default show both opening and closing | ||||
|           (o, c) -> (o, c) | ||||
|  | ||||
| @ -34,7 +34,7 @@ files :: CliOpts -> Journal -> IO () | ||||
| files CliOpts{rawopts_=rawopts} j = do | ||||
|   let args = listofstringopt "args" rawopts | ||||
|       regex = headMay args | ||||
|       files = maybe id (filter . regexMatches) regex  | ||||
|               $ map fst  | ||||
|       files = maybe id (filter . regexMatches) regex | ||||
|               $ map fst | ||||
|               $ jfiles j | ||||
|   mapM_ putStrLn files | ||||
|  | ||||
| @ -46,10 +46,10 @@ helpmode = hledgerCommandMode | ||||
|   [] | ||||
|   ([], Just $ argsFlag "[MANUAL]") | ||||
| 
 | ||||
| -- | List or display one of the hledger manuals in various formats.  | ||||
| -- | List or display one of the hledger manuals in various formats. | ||||
| -- You can select a docs viewer with one of the `--info`, `--man`, `--pager`, `--cat` flags. | ||||
| -- Otherwise it will use the first available of: info, man, $PAGER, less, stdout | ||||
| -- (and always stdout if output is non-interactive).  | ||||
| -- (and always stdout if output is non-interactive). | ||||
| help' :: CliOpts -> Journal -> IO () | ||||
| help' opts _ = do | ||||
|   exes <- likelyExecutablesInPath | ||||
| @ -60,18 +60,18 @@ help' opts _ = do | ||||
|     topic = case args of | ||||
|               [pat] -> headMay [t | t <- docTopics, map toLower pat `isInfixOf` t] | ||||
|               _   -> Nothing | ||||
|     [info, man, pager, cat] =  | ||||
|     [info, man, pager, cat] = | ||||
|       [runInfoForTopic, runManForTopic, runPagerForTopic pagerprog, printHelpForTopic] | ||||
|     viewer | ||||
|       | boolopt "info"  $ rawopts_ opts = info | ||||
|       | boolopt "man"   $ rawopts_ opts = man | ||||
|       | boolopt "pager" $ rawopts_ opts = pager | ||||
|       | boolopt "cat"   $ rawopts_ opts = cat | ||||
|       | not interactive                 = cat  | ||||
|       | not interactive                 = cat | ||||
|       | "info"    `elem` exes           = info | ||||
|       | "man"     `elem` exes           = man | ||||
|       | pagerprog `elem` exes           = pager | ||||
|       | otherwise                       = cat  | ||||
|       | otherwise                       = cat | ||||
|   case topic of | ||||
|     Nothing -> putStrLn $ unlines [ | ||||
|        "Please choose a manual by typing \"hledger help MANUAL\" (any substring is ok)." | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| module Hledger.Cli.Commands.Import ( | ||||
|   importmode | ||||
|  ,importcmd | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Control.Monad | ||||
| @ -18,7 +18,7 @@ import Text.Printf | ||||
| 
 | ||||
| importmode = hledgerCommandMode | ||||
|   $(embedFileRelative "Hledger/Cli/Commands/Import.txt") | ||||
|   [flagNone ["dry-run"] (setboolopt "dry-run") "just show the transactions to be imported"]  | ||||
|   [flagNone ["dry-run"] (setboolopt "dry-run") "just show the transactions to be imported"] | ||||
|   [generalflagsgroup1] | ||||
|   hiddenflags | ||||
|   ([], Just $ argsFlag "FILE [...]") | ||||
| @ -33,7 +33,7 @@ importcmd opts@CliOpts{rawopts_=rawopts,inputopts_=iopts} j = do | ||||
|     fs -> do | ||||
|       enewj <- readJournalFiles iopts' fs | ||||
|       case enewj of | ||||
|         Left e     -> error' e  | ||||
|         Left e     -> error' e | ||||
|         Right newj -> | ||||
|           case sortOn tdate $ jtxns newj of | ||||
|             [] -> return () | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| module Hledger.Cli.Commands.Prices ( | ||||
|   pricesmode | ||||
|  ,prices | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Data.Maybe | ||||
| @ -22,7 +22,7 @@ pricesmode = hledgerCommandMode | ||||
|   hiddenflags | ||||
|   ([], Just $ argsFlag "[QUERY]") | ||||
| 
 | ||||
| -- XXX the original hledger-prices script always ignored assertions  | ||||
| -- XXX the original hledger-prices script always ignored assertions | ||||
| prices opts j = do | ||||
|   d <- getCurrentDay | ||||
|   let | ||||
|  | ||||
| @ -59,13 +59,13 @@ printEntries opts@CliOpts{reportopts_=ropts} j = do | ||||
|   writeOutput opts $ render $ entriesReport ropts' q j | ||||
| 
 | ||||
| entriesReportAsText :: CliOpts -> EntriesReport -> String | ||||
| entriesReportAsText opts = concatMap (showTransactionUnelided . gettxn)  | ||||
| entriesReportAsText opts = concatMap (showTransactionUnelided . gettxn) | ||||
|   where | ||||
|     gettxn | useexplicittxn = id                   -- use fully inferred amounts & txn prices  | ||||
|     gettxn | useexplicittxn = id                   -- use fully inferred amounts & txn prices | ||||
|            | otherwise      = originalTransaction  -- use original as-written amounts/txn prices | ||||
|     -- Original vs inferred transactions/postings were causing problems here, disabling -B (#551). | ||||
|     -- Use the explicit one if -B or -x are active. | ||||
|     -- This passes tests; does it also mean -B sometimes shows missing amounts unnecessarily ?   | ||||
|     -- This passes tests; does it also mean -B sometimes shows missing amounts unnecessarily ? | ||||
|     useexplicittxn = boolopt "explicit" (rawopts_ opts) || (valuationTypeIsCost $ reportopts_ opts) | ||||
| 
 | ||||
| -- Replace this transaction's postings with the original postings if any, but keep the | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| module Hledger.Cli.Commands.Printunique ( | ||||
|   printuniquemode | ||||
|  ,printunique | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Data.List | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| module Hledger.Cli.Commands.Registermatch ( | ||||
|   registermatchmode | ||||
|  ,registermatch | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Data.Char (toUpper) | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
| module Hledger.Cli.Commands.Rewrite ( | ||||
|   rewritemode | ||||
|  ,rewrite | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| #if !(MIN_VERSION_base(4,11,0)) | ||||
| @ -36,7 +36,7 @@ rewritemode = hledgerCommandMode | ||||
| -- TODO interpolating match groups in replacement | ||||
| -- TODO allow using this on unbalanced entries, eg to rewrite while editing | ||||
| 
 | ||||
| rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = do  | ||||
| rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = do | ||||
|   -- rewrite matched transactions | ||||
|   let modifiers = transactionModifierFromOpts opts : jtxnmodifiers j | ||||
|   let j' = j{jtxns=modifyTransactions modifiers ts} | ||||
| @ -46,7 +46,7 @@ rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = d | ||||
| -- | Build a 'TransactionModifier' from any query arguments and --add-posting flags | ||||
| -- provided on the command line, or throw a parse error. | ||||
| transactionModifierFromOpts :: CliOpts -> TransactionModifier | ||||
| transactionModifierFromOpts CliOpts{rawopts_=rawopts,reportopts_=ropts} =  | ||||
| transactionModifierFromOpts CliOpts{rawopts_=rawopts,reportopts_=ropts} = | ||||
|   TransactionModifier{tmquerytxt=q, tmpostingrules=ps} | ||||
|   where | ||||
|     q = T.pack $ query_ ropts | ||||
|  | ||||
| @ -6,7 +6,7 @@ The @roi@ command prints internal rate of return and time-weighted rate of retur | ||||
| 
 | ||||
| -} | ||||
| 
 | ||||
| module Hledger.Cli.Commands.Roi (  | ||||
| module Hledger.Cli.Commands.Roi ( | ||||
|   roimode | ||||
|   , roi | ||||
| ) where | ||||
| @ -40,40 +40,40 @@ roimode = hledgerCommandMode | ||||
|   hiddenflags | ||||
|   ([], Just $ argsFlag "[QUERY]") | ||||
| 
 | ||||
| -- One reporting span,  | ||||
| data OneSpan = OneSpan  | ||||
| -- One reporting span, | ||||
| data OneSpan = OneSpan | ||||
|   Day -- start date, inclusive | ||||
|   Day   -- end date, exclusive | ||||
|   Quantity -- value of investment at the beginning of day on spanBegin_ | ||||
|   Quantity  -- value of investment at the end of day on spanEnd_ | ||||
|   [(Day,Quantity)] -- all deposits and withdrawals (but not changes of value) in the DateSpan [spanBegin_,spanEnd_) | ||||
|  deriving (Show) | ||||
|    | ||||
| 
 | ||||
| 
 | ||||
| roi ::  CliOpts -> Journal -> IO () | ||||
| roi CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do | ||||
|   d <- getCurrentDay | ||||
|   let  | ||||
|   let | ||||
|     investmentsQuery = queryFromOpts d $ ropts{query_ = stringopt "investment" rawopts,period_=PeriodAll} | ||||
|     pnlQuery         = queryFromOpts d $ ropts{query_ = stringopt "pnl" rawopts,period_=PeriodAll} | ||||
|     showCashFlow      = boolopt "cashflow" rawopts | ||||
|     prettyTables     = pretty_tables_ ropts | ||||
|      | ||||
| 
 | ||||
|     trans = dbg3 "investments" $ jtxns $ filterJournalTransactions investmentsQuery j | ||||
|    | ||||
|     journalSpan =  | ||||
|         let dates = map transactionDate2 trans in  | ||||
|         DateSpan (Just $ minimum dates) (Just $ addDays 1 $ maximum dates)             | ||||
|          | ||||
| 
 | ||||
|     journalSpan = | ||||
|         let dates = map transactionDate2 trans in | ||||
|         DateSpan (Just $ minimum dates) (Just $ addDays 1 $ maximum dates) | ||||
| 
 | ||||
|     requestedSpan = periodAsDateSpan $ period_ ropts | ||||
|     requestedInterval = interval_ ropts | ||||
|      | ||||
|     wholeSpan = spanDefaultsFrom requestedSpan journalSpan  | ||||
| 
 | ||||
|     wholeSpan = spanDefaultsFrom requestedSpan journalSpan | ||||
| 
 | ||||
|   when (null trans) $ do | ||||
|     putStrLn "No relevant transactions found. Check your investments query" | ||||
|     exitFailure | ||||
|    | ||||
| 
 | ||||
|   let spans = case requestedInterval of | ||||
|         NoInterval -> [wholeSpan] | ||||
|         interval -> | ||||
| @ -82,23 +82,23 @@ roi CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do | ||||
| 
 | ||||
|   tableBody <- forM spans $ \(DateSpan (Just spanBegin) (Just spanEnd)) -> do | ||||
|     -- Spans are [spanBegin,spanEnd), and spanEnd is 1 day after then actual end date we are interested in | ||||
|     let  | ||||
|     let | ||||
|       valueBefore = | ||||
|         total trans (And [ investmentsQuery | ||||
|                          , Date (DateSpan Nothing (Just spanBegin))]) | ||||
|      | ||||
|       valueAfter  =  | ||||
| 
 | ||||
|       valueAfter  = | ||||
|         total trans (And [investmentsQuery | ||||
|                          , Date (DateSpan Nothing (Just spanEnd))]) | ||||
|          | ||||
|       cashFlow =  | ||||
| 
 | ||||
|       cashFlow = | ||||
|         calculateCashFlow trans (And [ Not investmentsQuery | ||||
|                                      , Not pnlQuery | ||||
|                                      , Date (DateSpan (Just spanBegin) (Just spanEnd)) ] ) | ||||
|        | ||||
|       thisSpan = dbg3 "processing span" $  | ||||
| 
 | ||||
|       thisSpan = dbg3 "processing span" $ | ||||
|                  OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow | ||||
|        | ||||
| 
 | ||||
|     irr <- internalRateOfReturn showCashFlow prettyTables thisSpan | ||||
|     twr <- timeWeightedReturn showCashFlow prettyTables investmentsQuery trans thisSpan | ||||
|     let cashFlowAmt = negate $ sum $ map snd cashFlow | ||||
| @ -112,28 +112,28 @@ roi CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do | ||||
|            , printf "%0.2f%%" $ smallIsZero irr | ||||
|            , printf "%0.2f%%" $ smallIsZero twr ] | ||||
| 
 | ||||
|   let table = Table  | ||||
|               (Tbl.Group NoLine (map (Header . show) (take (length tableBody) [1..])))  | ||||
|               (Tbl.Group DoubleLine  | ||||
|   let table = Table | ||||
|               (Tbl.Group NoLine (map (Header . show) (take (length tableBody) [1..]))) | ||||
|               (Tbl.Group DoubleLine | ||||
|                [ Tbl.Group SingleLine [Header "Begin", Header "End"] | ||||
|                , Tbl.Group SingleLine [Header "Value (begin)", Header "Cashflow", Header "Value (end)", Header "PnL"] | ||||
|                , Tbl.Group SingleLine [Header "IRR", Header "TWR"]]) | ||||
|               tableBody | ||||
|    | ||||
| 
 | ||||
|   putStrLn $ Ascii.render prettyTables id id id table | ||||
| 
 | ||||
| timeWeightedReturn showCashFlow prettyTables investmentsQuery trans (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow) = do | ||||
|   let initialUnitPrice = 100 | ||||
|   let initialUnits = valueBefore / initialUnitPrice | ||||
|   let cashflow =  | ||||
|   let cashflow = | ||||
|         -- Aggregate all entries for a single day, assuming that intraday interest is negligible | ||||
|         map (\date_cash -> let (dates, cash) = unzip date_cash in (head dates, sum cash)) | ||||
|         $ groupBy ((==) `on` fst) | ||||
|         $ sortOn fst  | ||||
|         $ map (\(d,a) -> (d, negate a))  | ||||
|         $ sortOn fst | ||||
|         $ map (\(d,a) -> (d, negate a)) | ||||
|         $ filter ((/=0).snd) cashFlow | ||||
|      | ||||
|   let units =  | ||||
| 
 | ||||
|   let units = | ||||
|         tail $ | ||||
|         scanl | ||||
|           (\(_, _, _, unitBalance) (date, amt) -> | ||||
| @ -146,14 +146,14 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans (OneSpan spa | ||||
|               in (valueOnDate, unitsBoughtOrSold, unitPrice, unitBalance + unitsBoughtOrSold)) | ||||
|           (0, 0, 0, initialUnits) | ||||
|           cashflow | ||||
|    | ||||
| 
 | ||||
|   let finalUnitBalance = if null units then initialUnits else let (_,_,_,u) = last units in u | ||||
|       finalUnitPrice = valueAfter / finalUnitBalance | ||||
|       totalTWR = roundTo 2 $ (finalUnitPrice - initialUnitPrice) | ||||
|       years = fromIntegral (diffDays spanEnd spanBegin) / 365 :: Double | ||||
|       annualizedTWR = 100*((1+(realToFrac totalTWR/100))**(1/years)-1) :: Double | ||||
|          | ||||
|   let s d = show $ roundTo 2 d  | ||||
| 
 | ||||
|   let s d = show $ roundTo 2 d | ||||
|   when showCashFlow $ do | ||||
|     printf "\nTWR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd)) | ||||
|     let (dates', amounts') = unzip cashflow | ||||
| @ -165,27 +165,27 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans (OneSpan spa | ||||
|         unitPrices = add initialUnitPrice unitPrices' | ||||
|         unitBalances = add initialUnits unitBalances' | ||||
|         valuesOnDate = add 0 valuesOnDate' | ||||
|          | ||||
|     putStr $ Ascii.render prettyTables id id id  | ||||
|       (Table  | ||||
| 
 | ||||
|     putStr $ Ascii.render prettyTables id id id | ||||
|       (Table | ||||
|        (Tbl.Group NoLine (map (Header . showDate) dates)) | ||||
|        (Tbl.Group DoubleLine [ Tbl.Group SingleLine [Header "Portfolio value", Header "Unit balance"]  | ||||
|        (Tbl.Group DoubleLine [ Tbl.Group SingleLine [Header "Portfolio value", Header "Unit balance"] | ||||
|                          , Tbl.Group SingleLine [Header "Cash", Header "Unit price", Header "Units"] | ||||
|                          , Tbl.Group SingleLine [Header "New Unit Balance"]]) | ||||
|        [ [value, oldBalance, amt, prc, udelta, balance]  | ||||
|        [ [value, oldBalance, amt, prc, udelta, balance] | ||||
|        | value <- map s valuesOnDate | ||||
|        | oldBalance <- map s (0:unitBalances) | ||||
|        | balance <- map s unitBalances | ||||
|        | amt <- map s amounts | ||||
|        | prc <- map s unitPrices | ||||
|        | udelta <- map s unitsBoughtOrSold ]) | ||||
|    | ||||
|     printf "Final unit price: %s/%s=%s U.\nTotal TWR: %s%%.\nPeriod: %.2f years.\nAnnualized TWR: %.2f%%\n\n" (s valueAfter) (s finalUnitBalance) (s finalUnitPrice) (s totalTWR) years annualizedTWR | ||||
|    | ||||
|   return annualizedTWR | ||||
|    | ||||
| 
 | ||||
| internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow) = do  | ||||
|     printf "Final unit price: %s/%s=%s U.\nTotal TWR: %s%%.\nPeriod: %.2f years.\nAnnualized TWR: %.2f%%\n\n" (s valueAfter) (s finalUnitBalance) (s finalUnitPrice) (s totalTWR) years annualizedTWR | ||||
| 
 | ||||
|   return annualizedTWR | ||||
| 
 | ||||
| 
 | ||||
| internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow) = do | ||||
|   let prefix = (spanBegin, negate valueBefore) | ||||
| 
 | ||||
|       postfix = (spanEnd, valueAfter) | ||||
| @ -193,18 +193,18 @@ internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueB | ||||
|       totalCF = filter ((/=0) . snd) $ prefix : (sortOn fst cashFlow) ++ [postfix] | ||||
| 
 | ||||
|   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 spanBegin) (showDate (addDays (-1) spanEnd)) | ||||
|     let (dates, amounts) = unzip totalCF | ||||
|     putStrLn $ Ascii.render prettyTables id id id  | ||||
|       (Table  | ||||
|     putStrLn $ Ascii.render prettyTables id id id | ||||
|       (Table | ||||
|        (Tbl.Group NoLine (map (Header . showDate) dates)) | ||||
|        (Tbl.Group SingleLine [Header "Amount"]) | ||||
|        (map ((:[]) . show) amounts)) | ||||
|                               | ||||
| 
 | ||||
|   -- 0% is always a solution, so require at least something here | ||||
|   case ridders  | ||||
|   case ridders | ||||
| #if MIN_VERSION_math_functions(0,3,0) | ||||
|     (RiddersParam 100 (AbsTol 0.00001))  | ||||
|     (RiddersParam 100 (AbsTol 0.00001)) | ||||
| #else | ||||
|     0.00001 | ||||
| #endif | ||||
| @ -227,9 +227,9 @@ calculateCashFlow trans query = map go trans | ||||
| 
 | ||||
| total :: [Transaction] -> Query -> Quantity | ||||
| total trans query = unMix $ sumPostings $ filter (matchesPosting query) $ concatMap realPostings trans | ||||
|      | ||||
| unMix :: MixedAmount -> Quantity    | ||||
| unMix a =  | ||||
| 
 | ||||
| unMix :: MixedAmount -> Quantity | ||||
| unMix a = | ||||
|   case (normaliseMixedAmount $ costOfMixedAmount a) of | ||||
|     (Mixed [a]) -> aquantity a | ||||
|     _ -> error "MixedAmount failed to normalize" | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| module Hledger.Cli.Commands.Tags ( | ||||
|   tagsmode | ||||
|  ,tags | ||||
| )  | ||||
| ) | ||||
| where | ||||
| 
 | ||||
| import Data.List | ||||
| @ -15,7 +15,7 @@ import Hledger.Cli.CliOptions | ||||
| 
 | ||||
| tagsmode = hledgerCommandMode | ||||
|   $(embedFileRelative "Hledger/Cli/Commands/Tags.txt") | ||||
|   [] -- [flagNone ["strict"] (setboolopt "strict") "makes date comparing strict"] --  | ||||
|   [] -- [flagNone ["strict"] (setboolopt "strict") "makes date comparing strict"] -- | ||||
|   [generalflagsgroup1] | ||||
|   hiddenflags | ||||
|   ([], Just $ argsFlag "[TAGREGEX [QUERY...]]") | ||||
| @ -26,10 +26,10 @@ tags CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do | ||||
|     args      = listofstringopt "args" rawopts | ||||
|     mtagpats  = headMay args | ||||
|     queryargs = drop 1 args | ||||
|     q = queryFromOpts d $ ropts{query_ = unwords queryargs}  | ||||
|     q = queryFromOpts d $ ropts{query_ = unwords queryargs} | ||||
|     txns = filter (q `matchesTransaction`) $ jtxns $ journalSelectingAmountFromOpts ropts j | ||||
|     tags =  | ||||
|       nub $ sort $  | ||||
|       (maybe id (filter . regexMatchesCI) mtagpats) $  | ||||
|     tags = | ||||
|       nub $ sort $ | ||||
|       (maybe id (filter . regexMatchesCI) mtagpats) $ | ||||
|       map (T.unpack . fst) $ concatMap transactionAllTags txns | ||||
|   mapM_ putStrLn tags | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| {-# LANGUAGE OverloadedStrings, RecordWildCards #-} | ||||
| {-| | ||||
| 
 | ||||
| Common helpers for making multi-section balance report commands  | ||||
| Common helpers for making multi-section balance report commands | ||||
| like balancesheet, cashflow, and incomestatement. | ||||
| 
 | ||||
| -} | ||||
| @ -27,16 +27,16 @@ import Hledger.Cli.Commands.Balance | ||||
| import Hledger.Cli.CliOptions | ||||
| import Hledger.Cli.Utils (writeOutput) | ||||
| 
 | ||||
| -- | Description of a compound balance report command,  | ||||
| -- | Description of a compound balance report command, | ||||
| -- from which we generate the command's cmdargs mode and IO action. | ||||
| -- A compound balance report command shows one or more sections/subreports,  | ||||
| -- each with its own title and subtotals row, in a certain order,  | ||||
| -- A compound balance report command shows one or more sections/subreports, | ||||
| -- each with its own title and subtotals row, in a certain order, | ||||
| -- plus a grand totals row if there's more than one section. | ||||
| -- Examples are the balancesheet, cashflow and incomestatement commands. | ||||
| -- | ||||
| -- Compound balance reports do sign normalisation: they show all account balances  | ||||
| -- Compound balance reports do sign normalisation: they show all account balances | ||||
| -- as normally positive, unlike the ordinary BalanceReport and most hledger commands | ||||
| -- which show income/liability/equity balances as normally negative.   | ||||
| -- which show income/liability/equity balances as normally negative. | ||||
| -- Each subreport specifies the normal sign of its amounts, and whether | ||||
| -- it should be added to or subtracted from the grand total. | ||||
| -- | ||||
| @ -44,7 +44,7 @@ data CompoundBalanceCommandSpec = CompoundBalanceCommandSpec { | ||||
|   cbcdoc      :: CommandDoc,          -- ^ the command's name(s) and documentation | ||||
|   cbctitle    :: String,              -- ^ overall report title | ||||
|   cbcqueries  :: [CBCSubreportSpec],  -- ^ subreport details | ||||
|   cbctype     :: BalanceType          -- ^ the "balance" type (change, cumulative, historical)  | ||||
|   cbctype     :: BalanceType          -- ^ the "balance" type (change, cumulative, historical) | ||||
|                                       --   this report shows (overrides command line flags) | ||||
| } | ||||
| 
 | ||||
| @ -62,15 +62,15 @@ data CBCSubreportSpec = CBCSubreportSpec { | ||||
| -- | ||||
| -- * the period (date span) of each column | ||||
| -- | ||||
| -- * one or more named, normal-positive multi balance reports,  | ||||
| -- * one or more named, normal-positive multi balance reports, | ||||
| --   with columns corresponding to the above, and a flag indicating | ||||
| --   whether they increased or decreased the overall totals | ||||
| -- | ||||
| -- * a list of overall totals for each column, and their grand total and average | ||||
| -- | ||||
| -- It is used in compound balance report commands like balancesheet,  | ||||
| -- It is used in compound balance report commands like balancesheet, | ||||
| -- cashflow and incomestatement. | ||||
| type CompoundBalanceReport =  | ||||
| type CompoundBalanceReport = | ||||
|   ( String | ||||
|   , [DateSpan] | ||||
|   , [(String, MultiBalanceReport, Bool)] | ||||
| @ -78,7 +78,7 @@ type CompoundBalanceReport = | ||||
|   ) | ||||
| 
 | ||||
| 
 | ||||
| -- | Generate a cmdargs option-parsing mode from a compound balance command  | ||||
| -- | Generate a cmdargs option-parsing mode from a compound balance command | ||||
| -- specification. | ||||
| compoundBalanceCommandMode :: CompoundBalanceCommandSpec -> Mode RawOpts | ||||
| compoundBalanceCommandMode CompoundBalanceCommandSpec{..} = | ||||
| @ -120,7 +120,7 @@ compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> I | ||||
| compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=ropts@ReportOpts{..}, rawopts_=rawopts} j = do | ||||
|     d <- getCurrentDay | ||||
|     let | ||||
|       -- use the default balance type for this report, unless the user overrides   | ||||
|       -- use the default balance type for this report, unless the user overrides | ||||
|       mBalanceTypeOverride = | ||||
|         case reverse $ filter (`elem` ["change","cumulative","historical"]) $ map fst rawopts of | ||||
|           "historical":_ -> Just HistoricalBalance | ||||
| @ -151,13 +151,13 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=r | ||||
|             Nothing             -> "" | ||||
| 
 | ||||
|       -- Set balance type in the report options. | ||||
|       -- Also, use tree mode (by default, at least?) if --cumulative/--historical  | ||||
|       -- are used in single column mode, since in that situation we will be using  | ||||
|       -- Also, use tree mode (by default, at least?) if --cumulative/--historical | ||||
|       -- are used in single column mode, since in that situation we will be using | ||||
|       -- balanceReportFromMultiBalanceReport which does not support eliding boring parents, | ||||
|       -- and tree mode hides this.. or something.. XXX  | ||||
|       -- and tree mode hides this.. or something.. XXX | ||||
|       ropts' | ||||
|         | not (flat_ ropts) &&  | ||||
|           interval_==NoInterval &&  | ||||
|         | not (flat_ ropts) && | ||||
|           interval_==NoInterval && | ||||
|           balancetype `elem` [CumulativeChange, HistoricalBalance] | ||||
|             = ropts{balancetype_=balancetype, accountlistmode_=ALTree} | ||||
|         | otherwise | ||||
| @ -166,38 +166,38 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=r | ||||
|       format = outputFormatFromOpts opts | ||||
| 
 | ||||
|       -- make a CompoundBalanceReport | ||||
|       subreports =  | ||||
|         map (\CBCSubreportSpec{..} ->  | ||||
|       subreports = | ||||
|         map (\CBCSubreportSpec{..} -> | ||||
|                 (cbcsubreporttitle | ||||
|                 ,mbrNormaliseSign cbcsubreportnormalsign $ -- <- convert normal-negative to normal-positive | ||||
|                   compoundBalanceSubreport ropts' userq j cbcsubreportquery cbcsubreportnormalsign | ||||
|                 ,cbcsubreportincreasestotal | ||||
|                 )) | ||||
|             cbcqueries | ||||
|       subtotalrows =  | ||||
|         [(coltotals, increasesoveralltotal)  | ||||
|       subtotalrows = | ||||
|         [(coltotals, increasesoveralltotal) | ||||
|         | (_, MultiBalanceReport (_,_,(coltotals,_,_)), increasesoveralltotal) <- subreports | ||||
|         ] | ||||
|       -- Sum the subreport totals by column. Handle these cases: | ||||
|       -- - no subreports | ||||
|       -- - empty subreports, having no subtotals (#588) | ||||
|       -- - subreports with a shorter subtotals row than the others   | ||||
|       -- - subreports with a shorter subtotals row than the others | ||||
|       overalltotals = case subtotalrows of | ||||
|         [] -> ([], nullmixedamt, nullmixedamt) | ||||
|         rs -> | ||||
|           let | ||||
|             numcols = maximum $ map (length.fst) rs  -- partial maximum is ok, rs is non-null | ||||
|             paddedsignedsubtotalrows =  | ||||
|             paddedsignedsubtotalrows = | ||||
|               [map (if increasesoveralltotal then id else negate) $  -- maybe flip the signs | ||||
|                take numcols $ as ++ repeat nullmixedamt              -- pad short rows with zeros  | ||||
|                take numcols $ as ++ repeat nullmixedamt              -- pad short rows with zeros | ||||
|               | (as,increasesoveralltotal) <- rs | ||||
|               ] | ||||
|             coltotals = foldl' (zipWith (+)) zeros paddedsignedsubtotalrows  -- sum the columns | ||||
|               where zeros = replicate numcols nullmixedamt | ||||
|             grandtotal = sum coltotals | ||||
|             grandavg | null coltotals = nullmixedamt | ||||
|                      | otherwise      = fromIntegral (length coltotals) `divideMixedAmount` grandtotal  | ||||
|           in  | ||||
|                      | otherwise      = fromIntegral (length coltotals) `divideMixedAmount` grandtotal | ||||
|           in | ||||
|             (coltotals, grandtotal, grandavg) | ||||
|       colspans = | ||||
|         case subreports of | ||||
| @ -230,12 +230,12 @@ compoundBalanceSubreport ropts@ReportOpts{..} userq j subreportqfn subreportnorm | ||||
|     -- if user didn't specify --empty, now remove the all-zero rows, unless they have non-zero subaccounts | ||||
|     -- in this report | ||||
|     r' | empty_    = r | ||||
|        | otherwise = MultiBalanceReport (dates, rows', totals)  | ||||
|        | otherwise = MultiBalanceReport (dates, rows', totals) | ||||
|           where | ||||
|             nonzeroaccounts = | ||||
|               dbg1 "nonzeroaccounts" $ | ||||
|               catMaybes $ map (\(act,_,_,amts,_,_) -> | ||||
|                             if not (all isZeroMixedAmount amts) then Just act else Nothing) rows  | ||||
|                             if not (all isZeroMixedAmount amts) then Just act else Nothing) rows | ||||
|             rows' = filter (not . emptyRow) rows | ||||
|               where | ||||
|                 emptyRow (act,_,_,amts,_,_) = | ||||
| @ -245,34 +245,34 @@ compoundBalanceSubreport ropts@ReportOpts{..} userq j subreportqfn subreportnorm | ||||
| {- Eg: | ||||
| Balance Sheet | ||||
| 
 | ||||
|              ||  2017/12/31    Total  Average  | ||||
|              ||  2017/12/31    Total  Average | ||||
| =============++=============================== | ||||
|  Assets      ||                                | ||||
|  Assets      || | ||||
| -------------++------------------------------- | ||||
|  assets:b    ||           1        1        1  | ||||
|  assets:b    ||           1        1        1 | ||||
| -------------++------------------------------- | ||||
|              ||           1        1        1  | ||||
|              ||           1        1        1 | ||||
| =============++=============================== | ||||
|  Liabilities ||                                | ||||
|  Liabilities || | ||||
| -------------++------------------------------- | ||||
| -------------++------------------------------- | ||||
|              ||                                | ||||
|              || | ||||
| =============++=============================== | ||||
|  Total       ||           1        1        1  | ||||
|  Total       ||           1        1        1 | ||||
| 
 | ||||
| -} | ||||
| compoundBalanceReportAsText :: ReportOpts -> CompoundBalanceReport -> String | ||||
| compoundBalanceReportAsText ropts (title, _colspans, subreports, (coltotals, grandtotal, grandavg)) = | ||||
|   title ++ "\n\n" ++  | ||||
|   title ++ "\n\n" ++ | ||||
|   balanceReportTableAsText ropts bigtable' | ||||
|   where | ||||
|     singlesubreport = length subreports == 1 | ||||
|     bigtable =  | ||||
|     bigtable = | ||||
|       case map (subreportAsTable ropts singlesubreport) subreports of | ||||
|         []   -> T.empty | ||||
|         r:rs -> foldl' concatTables r rs | ||||
|     bigtable' | ||||
|       | no_total_ ropts || singlesubreport =  | ||||
|       | no_total_ ropts || singlesubreport = | ||||
|           bigtable | ||||
|       | otherwise = | ||||
|           bigtable | ||||
| @ -332,11 +332,11 @@ compoundBalanceReportAsCsv ropts (title, colspans, subreports, (coltotals, grand | ||||
|             (if row_total_ ropts then (1+) else id) $ | ||||
|             (if average_ ropts then (1+) else id) $ | ||||
|             maximum $ -- depends on non-null subreports | ||||
|             map (\(MultiBalanceReport (amtcolheadings, _, _)) -> length amtcolheadings) $  | ||||
|             map (\(MultiBalanceReport (amtcolheadings, _, _)) -> length amtcolheadings) $ | ||||
|             map second3 subreports | ||||
|     addtotals | ||||
|       | no_total_ ropts || length subreports == 1 = id | ||||
|       | otherwise = (++  | ||||
|       | otherwise = (++ | ||||
|           ["Net:" : | ||||
|            map showMixedAmountOneLineWithoutPrice ( | ||||
|              coltotals | ||||
| @ -350,7 +350,7 @@ compoundBalanceReportAsHtml :: ReportOpts -> CompoundBalanceReport -> Html () | ||||
| compoundBalanceReportAsHtml ropts cbr = | ||||
|   let | ||||
|     (title, colspans, subreports, (coltotals, grandtotal, grandavg)) = cbr | ||||
|     colspanattr = colspan_ $ TS.pack $ show $  | ||||
|     colspanattr = colspan_ $ TS.pack $ show $ | ||||
|       1 + length colspans + (if row_total_ ropts then 1 else 0) + (if average_ ropts then 1 else 0) | ||||
|     leftattr = style_ "text-align:left" | ||||
|     blankrow = tr_ $ td_ [colspanattr] $ toHtmlRaw (" "::String) | ||||
| @ -366,7 +366,7 @@ compoundBalanceReportAsHtml ropts cbr = | ||||
| 
 | ||||
|     thRow :: [String] -> Html () | ||||
|     thRow = tr_ . mconcat . map (th_ . toHtml) | ||||
|      | ||||
| 
 | ||||
|     -- Make rows for a subreport: its title row, not the headings row, | ||||
|     -- the data rows, any totals row, and a blank row for whitespace. | ||||
|     subreportrows :: (String, MultiBalanceReport, Bool) -> [Html ()] | ||||
|  | ||||
| @ -60,7 +60,7 @@ import Hledger.Reports | ||||
| import Hledger.Utils | ||||
| 
 | ||||
| -- | Parse the user's specified journal file(s) as a Journal, maybe apply some | ||||
| -- transformations according to options, and run a hledger command with it.  | ||||
| -- transformations according to options, and run a hledger command with it. | ||||
| -- Or, throw an error. | ||||
| withJournalDo :: CliOpts -> (Journal -> IO a) -> IO a | ||||
| withJournalDo opts cmd = do | ||||
| @ -149,10 +149,10 @@ journalAddForecast opts@CliOpts{inputopts_=iopts, reportopts_=ropts} j = do | ||||
|       forecasttxns' = (if auto_ iopts then modifyTransactions (jtxnmodifiers j) else id) forecasttxns | ||||
| 
 | ||||
|   return $ | ||||
|     if forecast_ ropts  | ||||
|     if forecast_ ropts | ||||
|       then journalBalanceTransactions' opts j{ jtxns = concat [jtxns j, forecasttxns'] } | ||||
|       else j | ||||
|   where       | ||||
|   where | ||||
|     journalBalanceTransactions' opts j = | ||||
|       let assrt = not . ignore_assertions_ $ inputopts_ opts | ||||
|       in | ||||
| @ -164,7 +164,7 @@ writeOutput :: CliOpts -> String -> IO () | ||||
| writeOutput opts s = do | ||||
|   f <- outputFileFromOpts opts | ||||
|   (if f == "-" then putStr else writeFile f) s | ||||
|    | ||||
| 
 | ||||
| -- -- | Get a journal from the given string and options, or throw an error. | ||||
| -- readJournal :: CliOpts -> String -> IO Journal | ||||
| -- readJournal opts s = readJournal def Nothing s >>= either error' return | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user