packages feed

hledger-lib 1.5.1 → 1.9

raw patch · 39 files changed

+1038/−571 lines, 39 filesdep +easytestdep ~base

Dependencies added: easytest

Dependency ranges changed: base

Files

CHANGES view
@@ -1,9 +1,36 @@-API-ish changes in the hledger-lib package. See also hledger.+API-ish changes in the hledger-lib package. +Most user-visible changes are noted in the hledger changelog, instead.  -# 1.5.1 (2018/3/22)+# 1.9 (2018/3/31) -* support GHC 8.4 (add missing Semigroup instance for Journal)+* support ghc 8.4, latest deps++* when the system text encoding is UTF-8, ignore any UTF-8 BOM prefix+found when reading files.++* CompoundBalanceReport amounts are now normally positive.+The bs/bse/cf/is commands now show normal income, liability and equity+balances as positive.  Negative numbers now indicate a contra-balance+(eg an overdrawn checking account), a net loss, a negative net worth,+etc.  This makes these reports more like conventional financial+statements, and easier to read and share with others. (experimental)++* splitSpan now returns no spans for an empty datespan++* don't count periodic/modifier txns in Journal debug output++* lib/ui/web/api: move embedded manual files to extra-source-files++* Use skipMany/skipSome for parsing spacenonewline (Moritz Kiefer)+This avoids allocating the list of space characters only to then+discard it.++* rename, clarify purpose of balanceReportFromMultiBalanceReport++* fix some hlint warnings++* add some easytest tests   # 1.5 (2017/12/31)
Hledger/Data/Account.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, StandaloneDeriving, OverloadedStrings #-}+{-# LANGUAGE RecordWildCards, OverloadedStrings #-} {-|  @@ -47,6 +47,7 @@  nullacct = Account   { aname = ""+  , acode = Nothing   , aparent = Nothing   , asubs = []   , anumpostings = 0@@ -90,6 +91,10 @@       where         a' = a{aparent=parent, asubs=map (tie (Just a')) asubs} +-- | Look up an account's numeric code, if any, from the Journal and set it.+accountSetCodeFrom :: Journal -> Account -> Account+accountSetCodeFrom j a = a{acode=fromMaybe Nothing $ lookup (aname a) (jaccounts j)}+ -- | Get this account's parent accounts, from the nearest up to the root. parentAccounts :: Account -> [Account] parentAccounts Account{aparent=Nothing} = []@@ -188,16 +193,31 @@ -- | Sort each level of an account tree by inclusive amount, -- so that the accounts with largest normal balances are listed first.   -- The provided normal balance sign determines whether normal balances--- are negative or positive.-sortAccountTreeByAmount :: NormalBalance -> Account -> Account+-- are negative or positive, affecting the sort order. Ie,+-- if balances are normally negative, then the most negative balances+-- sort first, and vice versa.+sortAccountTreeByAmount :: NormalSign -> Account -> Account sortAccountTreeByAmount normalsign a   | null $ asubs a = a   | otherwise      = a{asubs=-                        sortBy (maybeflip $ comparing aibalance) $ +                        sortBy (maybeflip $ comparing aibalance) $                         map (sortAccountTreeByAmount normalsign) $ asubs a}   where-    maybeflip | normalsign==NormalNegative = id+    maybeflip | normalsign==NormallyNegative = id               | otherwise                  = flip++-- | Sort each level of an account tree first by the account code+-- if any, with the empty account code sorting last, and then by+-- the account name. +sortAccountTreeByAccountCodeAndName :: Account -> Account+sortAccountTreeByAccountCodeAndName a+  | null $ asubs a = a+  | otherwise      = a{asubs=+      sortBy (comparing accountCodeAndNameForSort) $ map sortAccountTreeByAccountCodeAndName $ asubs a}++accountCodeAndNameForSort a = (acode', aname a)+  where+    acode' = fromMaybe maxBound (acode a)  -- | Search an account list by name. lookupAccount :: AccountName -> [Account] -> Maybe Account
Hledger/Data/AccountName.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} {-|  'AccountName's are strings like @assets:cash:petty@, with multiple@@ -10,7 +12,9 @@ module Hledger.Data.AccountName where import Data.List+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Text (Text) import qualified Data.Text as T import Data.Tree@@ -43,7 +47,7 @@ accountSummarisedName :: AccountName -> Text accountSummarisedName a   --   length cs > 1 = take 2 (head cs) ++ ":" ++ a'-  | length cs > 1 = (T.intercalate ":" (map (T.take 2) $ init cs)) <> ":" <> a'+  | length cs > 1 = T.intercalate ":" (map (T.take 2) $ init cs) <> ":" <> a'   | otherwise     = a'     where       cs = accountNameComponents a@@ -125,11 +129,11 @@   | " (split)" `T.isSuffixOf` s =     let       names = T.splitOn ", " $ T.take (T.length s - 8) s-      widthpername = (max 0 (width - 8 - 2 * (max 1 (length names) - 1))) `div` length names+      widthpername = max 0 (width - 8 - 2 * (max 1 (length names) - 1)) `div` length names     in      fitText Nothing (Just width) True False $      (<>" (split)") $-     T.intercalate ", " $+     T.intercalate ", "      [accountNameFromComponents $ elideparts widthpername [] $ accountNameComponents s' | s' <- names]   | otherwise =     fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s@@ -174,7 +178,7 @@  -- | Does this string look like an exact account-matching regular expression ? isAccountRegex  :: String -> Bool-isAccountRegex s = take 1 s == "^" && (take 5 $ reverse s) == ")$|:("+isAccountRegex s = take 1 s == "^" && take 5 (reverse s) == ")$|:("  tests_Hledger_Data_AccountName = TestList  [
Hledger/Data/Amount.hs view
@@ -119,6 +119,7 @@ import Data.Ord (comparing) -- import Data.Text (Text) import qualified Data.Text as T+import Safe (maximumDef) import Test.HUnit import Text.Printf import qualified Data.Map as M@@ -148,7 +149,7 @@     abs a@Amount{aquantity=q}    = a{aquantity=abs q}     signum a@Amount{aquantity=q} = a{aquantity=signum q}     fromInteger i                = nullamt{aquantity=fromInteger i}-    negate a@Amount{aquantity=q} = a{aquantity=(-q)}+    negate a@Amount{aquantity=q} = a{aquantity= -q}     (+)                          = similarAmountsOp (+)     (-)                          = similarAmountsOp (-)     (*)                          = similarAmountsOp (*)@@ -216,8 +217,8 @@  -- | Does this amount appear to be zero when displayed with its given precision ? isZeroAmount :: Amount -> Bool-isZeroAmount a --  a==missingamt = False-               | otherwise     = (null . filter (`elem` digits) . showAmountWithoutPriceOrCommodity) a+isZeroAmount --  a==missingamt = False+  = not . any (`elem` digits) . showAmountWithoutPriceOrCommodity  -- | Is this amount "really" zero, regardless of the display precision ? isReallyZeroAmount :: Amount -> Bool@@ -280,16 +281,16 @@  showAmountHelper :: Bool -> Amount -> String showAmountHelper _ Amount{acommodity="AUTO"} = ""-showAmountHelper showzerocommodity a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) =+showAmountHelper showzerocommodity a@Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}} =     case ascommodityside of       L -> printf "%s%s%s%s" (T.unpack c') space quantity' price       R -> printf "%s%s%s%s" quantity' space (T.unpack c') price     where       quantity = showamountquantity a-      displayingzero = null $ filter (`elem` digits) $ quantity+      displayingzero = not (any (`elem` digits) quantity)       (quantity',c') | displayingzero && not showzerocommodity = ("0","")                      | otherwise = (quantity, quoteCommoditySymbolIfNeeded c)-      space = if (not (T.null c') && ascommodityspaced) then " " else "" :: String+      space = if not (T.null c') && ascommodityspaced then " " else "" :: String       price = showPrice p  -- | Like showAmount, but show a zero amount's commodity if it has one.@@ -300,7 +301,7 @@ -- using the display settings from its commodity. showamountquantity :: Amount -> String showamountquantity Amount{aquantity=q, astyle=AmountStyle{asprecision=p, asdecimalpoint=mdec, asdigitgroups=mgrps}} =-    punctuatenumber (fromMaybe '.' mdec) mgrps $ qstr+    punctuatenumber (fromMaybe '.' mdec) mgrps qstr     where       -- isint n = fromIntegral (round n) == n       qstr -- p == maxprecision && isint q = printf "%d" (round q::Integer)@@ -439,7 +440,7 @@     (zeros, nonzeros) = partition isReallyZeroAmount $                         map sumSimilarAmountsUsingFirstPrice $                         groupBy groupfn $-                        sortBy sortfn $+                        sortBy sortfn                         as     sortfn  | squashprices = compare `on` acommodity             | otherwise    = compare `on` \a -> (acommodity a, aprice a)@@ -520,7 +521,7 @@  -- | Divide a mixed amount's quantities by a constant. divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount-divideMixedAmount (Mixed as) d = Mixed $ map (flip divideAmount d) as+divideMixedAmount (Mixed as) d = Mixed $ map (`divideAmount` d) as  -- | Calculate the average of some mixed amounts. averageMixedAmounts :: [MixedAmount] -> MixedAmount@@ -599,38 +600,43 @@                        | otherwise       = printf "Mixed [%s]" as     where as = intercalate "\n       " $ map showAmountDebug $ amounts m --- | Get the string representation of a mixed amount, but without--- any \@ prices.+-- TODO these and related fns are comically complicated:++-- | Get the string representation of a mixed amount, without showing any transaction prices. showMixedAmountWithoutPrice :: MixedAmount -> String-showMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showfixedwidth as-    where-      (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m-      stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}-      width = maximum $ map (length . showAmount) as-      showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice+showMixedAmountWithoutPrice m = intercalate "\n" $ map showamt as+  where+    Mixed as = normaliseMixedAmountSquashPricesForDisplay $ mixedAmountStripPrices m+    showamt = printf (printf "%%%ds" width) . showAmountWithoutPrice+      where+        width = maximumDef 0 $ map (length . showAmount) as --- | Colour version.+-- | Colour version of showMixedAmountWithoutPrice. Any individual Amount+-- which is negative is wrapped in ANSI codes to make it display in red. cshowMixedAmountWithoutPrice :: MixedAmount -> String-cshowMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showamt as-    where-      (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m-      stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}-      width = maximum $ map (length . showAmount) as-      showamt a = -        (if isNegativeAmount a then color Dull Red else id) $-        printf (printf "%%%ds" width) $ showAmountWithoutPrice a+cshowMixedAmountWithoutPrice m = intercalate "\n" $ map showamt as+  where+    Mixed as = normaliseMixedAmountSquashPricesForDisplay $ mixedAmountStripPrices m+    showamt a =+      (if isNegativeAmount a then color Dull Red else id) $+      printf (printf "%%%ds" width) $ showAmountWithoutPrice a+      where+        width = maximumDef 0 $ map (length . showAmount) as +mixedAmountStripPrices :: MixedAmount -> MixedAmount+mixedAmountStripPrices (Mixed as) = Mixed $ map (\a -> a{aprice=NoPrice}) as+ -- | Get the one-line string representation of a mixed amount, but without -- any \@ prices. showMixedAmountOneLineWithoutPrice :: MixedAmount -> String-showMixedAmountOneLineWithoutPrice m = concat $ intersperse ", " $ map showAmountWithoutPrice as+showMixedAmountOneLineWithoutPrice m = intercalate ", " $ map showAmountWithoutPrice as     where       (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m       stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}  -- | Colour version. cshowMixedAmountOneLineWithoutPrice :: MixedAmount -> String-cshowMixedAmountOneLineWithoutPrice m = concat $ intersperse ", " $ map cshowAmountWithoutPrice as+cshowMixedAmountOneLineWithoutPrice m = intercalate ", " $ map cshowAmountWithoutPrice as     where       (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m       stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}@@ -659,14 +665,14 @@     costOfAmount (eur (-1)){aprice=TotalPrice $ usd 2} `is` usd (-2)    ,"isZeroAmount" ~: do-    assertBool "" $ isZeroAmount $ amount+    assertBool "" $ isZeroAmount amount     assertBool "" $ isZeroAmount $ usd 0    ,"negating amounts" ~: do     let a = usd 1-    negate a `is` a{aquantity=(-1)}+    negate a `is` a{aquantity= -1}     let b = (usd 1){aprice=UnitPrice $ eur 2}-    negate b `is` b{aquantity=(-1)}+    negate b `is` b{aquantity= -1}    ,"adding amounts without prices" ~: do     let a1 = usd 1.23@@ -680,8 +686,8 @@     -- highest precision is preserved     let ap1 = usd 1 `withPrecision` 1         ap3 = usd 1 `withPrecision` 3-    (asprecision $ astyle $ sum [ap1,ap3]) `is` 3-    (asprecision $ astyle $ sum [ap3,ap1]) `is` 3+    asprecision (astyle $ sum [ap1,ap3]) `is` 3+    asprecision (astyle $ sum [ap3,ap1]) `is` 3     -- adding different commodities assumes conversion rate 1     assertBool "" $ isZeroAmount (a1 - eur 1.23) @@ -691,7 +697,7 @@   -- MixedAmount    ,"adding mixed amounts to zero, the commodity and amount style are preserved" ~: do-    (sum $ map (Mixed . (:[]))+    sum (map (Mixed . (:[]))              [usd 1.25              ,usd (-1) `withPrecision` 3              ,usd (-0.25)@@ -699,13 +705,13 @@       `is` Mixed [usd 0 `withPrecision` 3]    ,"adding mixed amounts with total prices" ~: do-    (sum $ map (Mixed . (:[]))+    sum (map (Mixed . (:[]))      [usd 1 @@ eur 1      ,usd (-2) @@ eur 1      ])-      `is` (Mixed [usd 1 @@ eur 1-                  ,usd (-2) @@ eur 1-                  ])+      `is` Mixed [usd 1 @@ eur 1+                 ,usd (-2) @@ eur 1+                 ]    ,"showMixedAmount" ~: do     showMixedAmount (Mixed [usd 1]) `is` "$1.00"@@ -717,6 +723,6 @@   ,"showMixedAmountWithoutPrice" ~: do     let a = usd 1 `at` eur 2     showMixedAmountWithoutPrice (Mixed [a]) `is` "$1.00"-    showMixedAmountWithoutPrice (Mixed [a, (-a)]) `is` "0"+    showMixedAmountWithoutPrice (Mixed [a, -a]) `is` "0"    ]
Hledger/Data/AutoTransaction.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE OverloadedStrings, ViewPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE CPP #-} {-|  This module provides utilities for applying automated transactions like@@ -14,11 +16,14 @@     -- * Accessors     , mtvaluequery     , jdatespan+    , periodTransactionInterval     ) where  import Data.Maybe+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>))+#endif import Data.Time.Calendar import qualified Data.Text as T import Hledger.Data.Types@@ -28,6 +33,7 @@ import Hledger.Utils.Parse import Hledger.Utils.UTF8IOCompat (error') import Hledger.Query+-- import Hledger.Utils.Debug  -- $setup -- >>> :set -XOverloadedStrings@@ -233,26 +239,45 @@ -- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" as 2017-01-02 is not a first day of the quarter         -- >>> gen "yearly from 2017/1/14" -- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" as 2017-01-14 is not a first day of the year        +--+-- >>> let reportperiod="daily from 2018/01/03" in runPeriodicTransaction (PeriodicTransaction reportperiod [post "a" (usd 1)]) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03"))+-- [] runPeriodicTransaction :: PeriodicTransaction -> (DateSpan -> [Transaction])-runPeriodicTransaction pt = generate where-    base = nulltransaction { tpostings = ptpostings pt }-    periodExpr = ptperiodicexpr pt-    errCurrent = error' $ "Current date cannot be referenced in " ++ show (T.unpack periodExpr)-    (interval, effectspan) =-        case parsePeriodExpr errCurrent periodExpr of-            Left e -> error' $ "Failed to parse " ++ show (T.unpack periodExpr) ++ ": " ++ showDateParseError e-            Right x -> checkProperStartDate x-    generate jspan = [base {tdate=date} | span <- interval `splitSpan` spanIntersect effectspan jspan, let Just date = spanStart span]-    checkProperStartDate (i,s) = -      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"-        _                    -> (i,s) -        where-          checkStart d x =-            let firstDate = fixSmartDate d ("","this",x) -            in   -             if d == firstDate then (i,s)-             else error' $ "Unable to generate transactions according to "++(show periodExpr)++" as "++(show d)++" is not a first day of the "++x+runPeriodicTransaction pt =+  \requestedspan ->+    let fillspan = ptspan `spanIntersect` requestedspan+    in  [ t{tdate=d} | (DateSpan (Just d) _) <- ptinterval `splitSpan` fillspan ]+  where+    t = nulltransaction { tpostings = ptpostings pt }+    periodexpr = ptperiodicexpr pt+    currentdateerr = error' $ "Current date cannot be referenced in " ++ show (T.unpack periodexpr)+    (ptinterval, ptspan) =+        case parsePeriodExpr currentdateerr periodexpr of+            Left e  -> error' $ "Failed to parse " ++ show (T.unpack periodexpr) ++ ": " ++ showDateParseError e+            Right x -> checkPeriodTransactionStartDate periodexpr x++checkPeriodTransactionStartDate :: T.Text -> (Interval, DateSpan) -> (Interval, DateSpan)+checkPeriodTransactionStartDate periodexpr (i,s) = +  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"+    _                    -> (i,s) +    where+      checkStart d x =+        let firstDate = fixSmartDate d ("","this",x) +        in   +         if d == firstDate then (i,s)+         else error' $ "Unable to generate transactions according to "++(show periodexpr)++" as "++(show d)++" is not a first day of the "++x++-- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?+periodTransactionInterval :: PeriodicTransaction -> Maybe Interval+periodTransactionInterval pt =+  let+    expr = ptperiodicexpr pt+    err  = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)+  in+    case parsePeriodExpr err expr of+      Left _      -> Nothing+      Right (i,_) -> Just i
Hledger/Data/Commodity.hs view
@@ -8,12 +8,15 @@ -}  {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}  module Hledger.Data.Commodity where import Data.List import Data.Maybe (fromMaybe)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import qualified Data.Text as T import Test.HUnit -- import qualified Data.Map as M
Hledger/Data/Dates.hs view
@@ -40,6 +40,7 @@   parsedate,   showDate,   showDateSpan,+  showDateSpanMonthAbbrev,   elapsedSeconds,   prevday,   parsePeriodExpr,@@ -110,6 +111,11 @@ showDateSpan :: DateSpan -> String showDateSpan = showPeriod . dateSpanAsPeriod +-- | Like showDateSpan, but show month spans as just the abbreviated month name+-- in the current locale.+showDateSpanMonthAbbrev :: DateSpan -> String+showDateSpanMonthAbbrev = showPeriodMonthAbbrev . dateSpanAsPeriod+ -- | Get the current local date. getCurrentDay :: IO Day getCurrentDay = do@@ -143,8 +149,12 @@ spansSpan :: [DateSpan] -> DateSpan spansSpan spans = DateSpan (maybe Nothing spanStart $ headMay spans) (maybe Nothing spanEnd $ lastMay spans) --- | Split a DateSpan into one or more consecutive whole spans of the specified length which enclose it.+-- | Split a DateSpan into consecutive whole spans of the specified interval+-- which fully encompass the original span (and a little more when necessary). -- 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@@ -155,9 +165,9 @@ -- >>> splitSpan (Quarters 1) nulldatespan -- [DateSpan -] -- >>> t (Days 1) "2008/01/01" "2008/01/01"  -- an empty datespan--- [DateSpan 2008/01/01-2007/12/31]+-- [] -- >>> t (Quarters 1) "2008/01/01" "2008/01/01"--- [DateSpan 2008/01/01-2007/12/31]+-- [] -- >>> t (Months 1) "2008/01/01" "2008/04/01" -- [DateSpan 2008/01,DateSpan 2008/02,DateSpan 2008/03] -- >>> t (Months 2) "2008/01/01" "2008/04/01"@@ -179,6 +189,7 @@ -- splitSpan :: Interval -> DateSpan -> [DateSpan] splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]+splitSpan _ s | isEmptySpan s = [] splitSpan NoInterval     s = [s] splitSpan (Days n)       s = splitspan startofday     (applyN n nextday)     s splitSpan (Weeks n)      s = splitspan startofweek    (applyN n nextweek)    s@@ -216,6 +227,12 @@ daysInSpan (DateSpan (Just d1) (Just d2)) = Just $ diffDays d2 d1 daysInSpan _ = Nothing +-- | Is this an empty span, ie closed with the end date on or before the start date ?+isEmptySpan :: DateSpan -> Bool+isEmptySpan s = case daysInSpan s of+                  Just n  -> n < 1+                  Nothing -> False+ -- | Does the span include the given date ? spanContainsDate :: DateSpan -> Day -> Bool spanContainsDate (DateSpan Nothing Nothing)   _ = True@@ -234,6 +251,10 @@ spansIntersect (d:ds) = d `spanIntersect` (spansIntersect ds)  -- | Calculate the intersection of two datespans.+--+-- For non-intersecting spans, gives an empty span beginning on the second's start date:+-- >>> mkdatespan "2018-01-01" "2018-01-03" `spanIntersect` mkdatespan "2018-01-03" "2018-01-05"+-- DateSpan 2018/01/03-2018/01/02 spanIntersect (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e     where       b = latest b1 b2@@ -654,7 +675,7 @@ smartdateonly :: SimpleTextParser SmartDate smartdateonly = do   d <- smartdate-  many spacenonewline+  skipMany spacenonewline   eof   return d @@ -763,7 +784,7 @@        ,"this"        ,"next"       ]-  many spacenonewline  -- make the space optional for easier scripting+  skipMany spacenonewline  -- make the space optional for easier scripting   p <- choice $ map mptext [         "day"        ,"week"@@ -821,7 +842,7 @@ -- >>> p "every 2nd day of month 2009-" -- Right (DayOfMonth 2,DateSpan 2009/01/01-) periodexpr :: Day -> SimpleTextParser (Interval, DateSpan)-periodexpr rdate = surroundedBy (many spacenonewline) . choice $ map try [+periodexpr rdate = surroundedBy (skipMany spacenonewline) . choice $ map try [                     intervalanddateperiodexpr rdate,                     (,) NoInterval <$> periodexprdatespan rdate                    ]@@ -830,7 +851,7 @@ intervalanddateperiodexpr rdate = do   i <- reportinginterval   s <- option def . try $ do-      many spacenonewline+      skipMany spacenonewline       periodexprdatespan rdate   return (i,s) @@ -847,46 +868,46 @@                        do string "bimonthly"                           return $ Months 2,                        do string "every"-                          many spacenonewline+                          skipMany spacenonewline                           n <- nth-                          many spacenonewline+                          skipMany spacenonewline                           string "day"                           of_ "week"                           return $ DayOfWeek n,                        do string "every"-                          many spacenonewline+                          skipMany spacenonewline                           n <- weekday                           return $ DayOfWeek n,                        do string "every"-                          many spacenonewline+                          skipMany spacenonewline                           n <- nth-                          many spacenonewline+                          skipMany spacenonewline                           string "day"                           optOf_ "month"                           return $ DayOfMonth n,                        do string "every"                           let mnth = choice' [month, mon] >>= \(_,m,_) -> return (read m)-                          d_o_y <- makePermParser $ DayOfYear <$$> try (many spacenonewline *> mnth) <||> try (many spacenonewline *> nth)+                          d_o_y <- makePermParser $ DayOfYear <$$> try (skipMany spacenonewline *> mnth) <||> try (skipMany spacenonewline *> nth)                           optOf_ "year"                           return d_o_y,                        do string "every"-                          many spacenonewline+                          skipMany spacenonewline                           ("",m,d) <- md                           optOf_ "year"                           return $ DayOfYear (read m) (read d),                        do string "every"-                          many spacenonewline+                          skipMany spacenonewline                           n <- nth-                          many spacenonewline+                          skipMany spacenonewline                           wd <- weekday                           optOf_ "month"                           return $ WeekdayOfMonth n wd                     ]     where       of_ period = do-        many spacenonewline+        skipMany spacenonewline         string "of"-        many spacenonewline+        skipMany spacenonewline         string period                optOf_ period = optional $ try $ of_ period@@ -902,13 +923,13 @@           do mptext compact'              return $ intcons 1,           do mptext "every"-             many spacenonewline+             skipMany spacenonewline              mptext singular'              return $ intcons 1,           do mptext "every"-             many spacenonewline+             skipMany spacenonewline              n <- fmap read $ some digitChar-             many spacenonewline+             skipMany spacenonewline              mptext plural'              return $ intcons n           ]@@ -927,10 +948,10 @@  doubledatespan :: Day -> SimpleTextParser DateSpan doubledatespan rdate = do-  optional (string "from" >> many spacenonewline)+  optional (string "from" >> skipMany spacenonewline)   b <- smartdate-  many spacenonewline-  optional (choice [string "to", string "-"] >> many spacenonewline)+  skipMany spacenonewline+  optional (choice [string "to", string "-"] >> skipMany spacenonewline)   e <- smartdate   return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e) @@ -938,7 +959,7 @@ fromdatespan rdate = do   b <- choice [     do-      string "from" >> many spacenonewline+      string "from" >> skipMany spacenonewline       smartdate     ,     do@@ -950,13 +971,13 @@  todatespan :: Day -> SimpleTextParser DateSpan todatespan rdate = do-  choice [string "to", string "-"] >> many spacenonewline+  choice [string "to", string "-"] >> skipMany spacenonewline   e <- smartdate   return $ DateSpan Nothing (Just $ fixSmartDate rdate e)  justdatespan :: Day -> SimpleTextParser DateSpan justdatespan rdate = do-  optional (string "in" >> many spacenonewline)+  optional (string "in" >> skipMany spacenonewline)   d <- smartdate   return $ spanFromSmartDate rdate d 
Hledger/Data/Journal.hs view
@@ -84,7 +84,9 @@ import Data.List.Extra (groupSort) -- import Data.Map (findWithDefault) import Data.Maybe+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Ord import qualified Data.Semigroup as Sem import Data.Text (Text)@@ -117,22 +119,16 @@   show j     | debugLevel < 3 = printf "Journal %s with %d transactions, %d accounts"              (journalFilePath j)-             (length (jtxns j) +-              length (jmodifiertxns j) +-              length (jperiodictxns j))+             (length $ jtxns j)              (length accounts)     | debugLevel < 6 = printf "Journal %s with %d transactions, %d accounts: %s"              (journalFilePath j)-             (length (jtxns j) +-              length (jmodifiertxns j) +-              length (jperiodictxns j))+             (length $ jtxns j)              (length accounts)              (show accounts)     | otherwise = printf "Journal %s with %d transactions, %d accounts: %s, commodity styles: %s"              (journalFilePath j)-             (length (jtxns j) +-              length (jmodifiertxns j) +-              length (jperiodictxns j))+             (length $ jtxns j)              (length accounts)              (show accounts)              (show $ jinferredcommodities j)@@ -263,7 +259,7 @@  -- | Sorted unique account names declared by account directives in this journal. journalAccountNamesDeclared :: Journal -> [AccountName]-journalAccountNamesDeclared = nub . sort . jaccounts+journalAccountNamesDeclared = nub . sort . map fst . jaccounts  -- | Sorted unique account names declared by account directives or posted to -- by transactions in this journal.
Hledger/Data/Ledger.hs view
@@ -47,7 +47,7 @@     (q',depthq)  = (filterQuery (not . queryIsDepth) q, filterQuery queryIsDepth q)     j'  = filterJournalAmounts (filterQuery queryIsSym q) $ -- remove amount parts which the query's sym: terms would exclude           filterJournalPostings q' j-    as  = accountsFromPostings $ journalPostings j'+    as  = map (accountSetCodeFrom j) $ accountsFromPostings $ journalPostings j'     j'' = filterJournalPostings depthq j'  -- | List a ledger's account names.
Hledger/Data/Period.hs view
@@ -144,6 +144,13 @@ 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 +-- the 3 letter month name abbreviation for the current locale.+showPeriodMonthAbbrev (MonthPeriod _ m)                           -- Jan+  | m > 0 && m <= length monthnames = snd $ monthnames !! (m-1)+  where monthnames = months defaultTimeLocale+showPeriodMonthAbbrev p = showPeriod p+ periodStart :: Period -> Maybe Day periodStart p = mb   where
Hledger/Data/Posting.hs view
@@ -8,6 +8,7 @@ -}  {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}  module Hledger.Data.Posting (   -- * Posting@@ -59,7 +60,9 @@ import Data.List import Data.Maybe import Data.MemoUgly (memo)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Ord import Data.Text (Text) import qualified Data.Text as T
Hledger/Data/Types.hs view
@@ -104,6 +104,7 @@ instance NFData Interval  type AccountName = Text+type AccountCode = Int  data AccountAlias = BasicAlias AccountName AccountName                   | RegexAlias Regexp Replacement@@ -117,10 +118,10 @@  -- | The basic numeric type used in amounts. type Quantity = Decimal-deriving instance Data (Quantity)+deriving instance Data Quantity -- The following is for hledger-web, and requires blaze-markup. -- Doing it here avoids needing a matching flag on the hledger-web package.-instance ToMarkup (Quantity)+instance ToMarkup Quantity  where    toMarkup = toMarkup . show @@ -228,6 +229,7 @@  instance NFData GenericSourcePos +{-# ANN Transaction "HLint: ignore" #-} data Transaction = Transaction {       tindex                   :: Integer,   -- ^ this transaction's 1-based position in the input stream, or 0 when not available       tsourcepos               :: GenericSourcePos,@@ -299,7 +301,7 @@   -- ,jparsetransactioncount :: Integer                               -- ^ the current count of transactions parsed so far (only journal format txns, currently)   ,jparsetimeclockentries :: [TimeclockEntry]                   -- ^ timeclock sessions which have not been clocked out   -- principal data-  ,jaccounts              :: [AccountName]                         -- ^ accounts that have been declared by account directives+  ,jaccounts              :: [(AccountName, Maybe AccountCode)]     -- ^ accounts that have been declared by account directives   ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives   ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts   ,jmarketprices          :: [MarketPrice]@@ -313,9 +315,9 @@   ,jlastreadtime          :: ClockTime                              -- ^ when this journal was last read from its file(s)   } deriving (Eq, Typeable, Data, Generic) -deriving instance Data (ClockTime)-deriving instance Typeable (ClockTime)-deriving instance Generic (ClockTime)+deriving instance Data ClockTime+deriving instance Typeable ClockTime+deriving instance Generic ClockTime instance NFData ClockTime instance NFData Journal @@ -353,6 +355,7 @@ -- which let you walk up or down the account tree. data Account = Account {   aname                     :: AccountName,   -- ^ this account's full name+  acode                     :: Maybe AccountCode,   -- ^ this account's numeric code, if any (not always set)    aebalance                 :: MixedAmount,   -- ^ this account's balance, excluding subaccounts   asubs                     :: [Account],     -- ^ sub-accounts   anumpostings              :: Int,           -- ^ number of postings to this account@@ -362,14 +365,12 @@   aboring                   :: Bool           -- ^ used in the accounts report to label elidable parents   } deriving (Typeable, Data, Generic) --- | Whether an account's balance is normally a positive number (in accounting terms,--- normally a debit balance), as for asset and expense accounts, or a negative number--- (in accounting terms, normally a credit balance), as for liability, equity and --- income accounts. Cf https://en.wikipedia.org/wiki/Normal_balance .-data NormalBalance = -    NormalPositive -- ^ normally debit - assets, expenses...-  | NormalNegative -- ^ normally credit - liabilities, equity, income...-  deriving (Show, Data, Eq) +-- | 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)   -- | A Ledger has the journal it derives from, and the accounts -- derived from that. Accounts are accessible both list-wise and
Hledger/Query.hs view
@@ -6,6 +6,7 @@ -}  {-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ViewPatterns #-}+{-# LANGUAGE CPP #-}  module Hledger.Query (   -- * Query and QueryOpt@@ -49,7 +50,9 @@ import Data.Either import Data.List import Data.Maybe+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>))+#endif -- import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar@@ -185,7 +188,7 @@ words'' prefixes = fromparse . parsewith maybeprefixedquotedphrases -- XXX     where       maybeprefixedquotedphrases :: SimpleTextParser [T.Text]-      maybeprefixedquotedphrases = choice' [prefixedQuotedPattern, singleQuotedPattern, doubleQuotedPattern, pattern] `sepBy` some spacenonewline+      maybeprefixedquotedphrases = choice' [prefixedQuotedPattern, singleQuotedPattern, doubleQuotedPattern, pattern] `sepBy` skipSome spacenonewline       prefixedQuotedPattern :: SimpleTextParser T.Text       prefixedQuotedPattern = do         not' <- fromMaybe "" `fmap` (optional $ mptext "not:")
Hledger/Read.hs view
@@ -156,7 +156,7 @@     (mprefixformat, f) = splitReaderPrefix prefixedfile     mfmt = mformat <|> mprefixformat   requireJournalFileExists f-  readFileOrStdinAnyLineEnding f >>= readJournal mfmt mrulesfile assrt (Just f)+  readFileOrStdinPortably f >>= readJournal mfmt mrulesfile assrt (Just f)  -- | If a filepath is prefixed by one of the reader names and a colon, -- split that off. Eg "csv:-" -> (Just "csv", "-").@@ -276,7 +276,7 @@     (mfmt, f) = splitReaderPrefix prefixedfile     iopts' = iopts{mformat_=firstJust [mfmt, mformat_ iopts]}   requireJournalFileExists f-  t <- readFileOrStdinAnyLineEnding f+  t <- readFileOrStdinPortably f   ej <- readJournalWithOpts iopts' (Just f) t   case ej of     Left e  -> return $ Left e@@ -324,7 +324,7 @@     (dir, fname) = splitFileName f  readFileStrictly :: FilePath -> IO Text-readFileStrictly f = readFile' f >>= \t -> C.evaluate (T.length t) >> return t+readFileStrictly f = readFilePortably f >>= \t -> C.evaluate (T.length t) >> return t  -- | Given zero or more latest dates (all the same, representing the -- latest previously seen transaction date, and how many transactions
Hledger/Read/Common.hs view
@@ -13,7 +13,7 @@ -}  --- * module-{-# LANGUAGE CPP, DeriveDataTypeable, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-}+{-# LANGUAGE CPP, BangPatterns, DeriveDataTypeable, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-} {-# LANGUAGE LambdaCase #-}  module Hledger.Read.Common@@ -21,6 +21,7 @@ --- * imports import Prelude () import Prelude.Compat hiding (readFile)+import Control.Arrow ((***)) import Control.Monad.Compat import Control.Monad.Except (ExceptT(..), runExceptT, throwError) --, catchError) import Control.Monad.State.Strict@@ -33,7 +34,9 @@ import Data.List.Split (wordsBy) import Data.Maybe import qualified Data.Map as M+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar@@ -87,6 +90,7 @@ runTextParser p t =  runParser p "" t rtp = runTextParser +-- XXX odd, why doesn't this take a JournalParser ? -- | Run a journal parser with a null journal-parsing state. runJournalParser, rjp :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char MPErr) a) runJournalParser p t = runParserT p "" t@@ -167,7 +171,7 @@     return effectiveStyle  pushAccount :: AccountName -> JournalParser m ()-pushAccount acct = modify' (\j -> j{jaccounts = acct : jaccounts j})+pushAccount acct = modify' (\j -> j{jaccounts = (acct, Nothing) : jaccounts j})  pushParentAccount :: AccountName -> JournalParser m () pushParentAccount acct = modify' (\j -> j{jparseparentaccounts = acct : jparseparentaccounts j})@@ -226,14 +230,14 @@ statusp :: TextParser m Status statusp =   choice'-    [ many spacenonewline >> char '*' >> return Cleared-    , many spacenonewline >> char '!' >> return Pending+    [ skipMany spacenonewline >> char '*' >> return Cleared+    , skipMany spacenonewline >> char '!' >> return Pending     , return Unmarked     ]     <?> "cleared status"  codep :: TextParser m String-codep = try (do { some spacenonewline; char '(' <?> "codep"; anyChar `manyTill` char ')' } ) <|> return ""+codep = try (do { skipSome spacenonewline; char '(' <?> "codep"; anyChar `manyTill` char ')' } ) <|> return ""  descriptionp :: JournalParser m String descriptionp = many (noneOf (";\n" :: [Char]))@@ -277,7 +281,7 @@ datetimep :: JournalParser m LocalTime datetimep = do   day <- datep-  lift $ some spacenonewline+  lift $ skipSome spacenonewline   h <- some digitChar   let h' = read h   guard $ h' >= 0 && h' <= 23@@ -370,7 +374,7 @@ spaceandamountormissingp :: Monad m => JournalParser m MixedAmount spaceandamountormissingp =   try (do-        lift $ some spacenonewline+        lift $ skipSome spacenonewline         (Mixed . (:[])) `fmap` amountp <|> return missingmixedamt       ) <|> return missingmixedamt @@ -431,15 +435,27 @@   return $ case multiplier of Just '*' -> True                               _        -> False +-- | This is like skipMany but it returns True if at least one element+-- was skipped. This is helpful if you’re just using many to check if+-- the resulting list is empty or not.+skipMany' :: MonadPlus m => m a -> m Bool+skipMany' p = go False+  where+    go !isNull = do+      more <- option False (True <$ p)+      if more+        then go True+        else pure isNull+ leftsymbolamountp :: Monad m => JournalParser m Amount leftsymbolamountp = do   sign <- lift signp   m <- lift multiplierp   c <- lift commoditysymbolp   suggestedStyle <- getAmountStyle c-  sp <- lift $ many spacenonewline+  commodityspaced <- lift $ skipMany' spacenonewline   (q,prec,mdec,mgrps) <- lift $ numberp suggestedStyle-  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}+  let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}   p <- priceamountp   let applysign = if sign=="-" then negate else id   return $ applysign $ Amount c q p s m@@ -450,12 +466,14 @@   m <- lift multiplierp   sign <- lift signp   rawnum <- lift $ rawnumberp-  sp <- lift $ many spacenonewline+  expMod <- lift . option id $ try exponentp+  commodityspaced <- lift $ skipMany' spacenonewline   c <- lift commoditysymbolp   suggestedStyle <- getAmountStyle c-  let (q,prec,mdec,mgrps) = fromRawNumber suggestedStyle (sign == "-") rawnum+  let (q0,prec0,mdec,mgrps) = fromRawNumber suggestedStyle (sign == "-") rawnum+      (q, prec) = expMod (q0, prec0)   p <- priceamountp-  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}+  let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}   return $ Amount c q p s m   <?> "right-symbol amount" @@ -489,15 +507,15 @@ priceamountp :: Monad m => JournalParser m Price priceamountp =     try (do-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           char '@'           try (do                 char '@'-                lift (many spacenonewline)+                lift (skipMany spacenonewline)                 a <- amountp -- XXX can parse more prices ad infinitum, shouldn't                 return $ TotalPrice a)            <|> (do-            lift (many spacenonewline)+            lift (skipMany spacenonewline)             a <- amountp -- XXX can parse more prices ad infinitum, shouldn't             return $ UnitPrice a))          <|> return NoPrice@@ -505,10 +523,10 @@ partialbalanceassertionp :: Monad m => JournalParser m BalanceAssertion partialbalanceassertionp =     try (do-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           sourcepos <- genericSourcePos <$> lift getPosition           char '='-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           a <- amountp -- XXX should restrict to a simple amount           return $ Just (a, sourcepos))          <|> return Nothing@@ -516,9 +534,9 @@ -- balanceassertion :: Monad m => TextParser m (Maybe MixedAmount) -- balanceassertion = --     try (do---           lift (many spacenonewline)+--           lift (skipMany spacenonewline) --           string "=="---           lift (many spacenonewline)+--           lift (skipMany spacenonewline) --           a <- amountp -- XXX should restrict to a simple amount --           return $ Just $ Mixed [a]) --          <|> return Nothing@@ -527,13 +545,13 @@ fixedlotpricep :: Monad m => JournalParser m (Maybe Amount) fixedlotpricep =     try (do-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           char '{'-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           char '='-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           a <- amountp -- XXX should restrict to a simple amount-          lift (many spacenonewline)+          lift (skipMany spacenonewline)           char '}'           return $ Just a)          <|> return Nothing@@ -558,9 +576,20 @@     sign <- signp     raw <- rawnumberp     dbg8 "numberp parsed" raw `seq` return ()-    return $ dbg8 "numberp quantity,precision,mdecimalpoint,mgrps" (fromRawNumber suggestedStyle (sign == "-") raw)+    let num@(q, prec, decSep, groups) = dbg8 "numberp quantity,precision,mdecimalpoint,mgrps" (fromRawNumber suggestedStyle (sign == "-") raw)+    option num . try $ do+        when (isJust groups) $ fail "groups and exponent are not mixable"+        (q', prec') <- exponentp <*> pure (q, prec)+        return (q', prec', decSep, groups)     <?> "numberp" +exponentp :: TextParser m ((Quantity, Int) -> (Quantity, Int))+exponentp = do+    char' 'e'+    exp <- liftM read $ (++) <$> signp <*> some digitChar+    return $ (* 10^^exp) *** (0 `max`) . (+ (-exp))+    <?> "exponentp"+ fromRawNumber :: Maybe AmountStyle -> Bool -> (Maybe Char, [String], Maybe (Char, String)) -> (Quantity, Int, Maybe Char, Maybe DigitGroupStyle) fromRawNumber suggestedStyle negated raw = (quantity, precision, mdecimalpoint, mgrps) where     -- unpack with a hint if useful@@ -650,7 +679,7 @@  multilinecommentp :: JournalParser m () multilinecommentp = do-  string "comment" >> lift (many spacenonewline) >> newline+  string "comment" >> lift (skipMany spacenonewline) >> newline   go   where     go = try (eof <|> (string "end comment" >> newline >> return ()))@@ -659,15 +688,15 @@  emptyorcommentlinep :: JournalParser m () emptyorcommentlinep = do-  lift (many spacenonewline) >> (linecommentp <|> (lift (many spacenonewline) >> newline >> return ""))+  lift (skipMany spacenonewline) >> (linecommentp <|> (lift (skipMany spacenonewline) >> newline >> return ""))   return ()  -- | Parse a possibly multi-line comment following a semicolon. followingcommentp :: JournalParser m Text followingcommentp =   -- ptrace "followingcommentp"-  do samelinecomment <- lift (many spacenonewline) >> (try commentp <|> (newline >> return ""))-     newlinecomments <- many (try (lift (some spacenonewline) >> commentp))+  do samelinecomment <- lift (skipMany spacenonewline) >> (try commentp <|> (newline >> return ""))+     newlinecomments <- many (try (lift (skipSome spacenonewline) >> commentp))      return $ T.unlines $ samelinecomment:newlinecomments  -- | Parse a possibly multi-line comment following a semicolon, and@@ -739,7 +768,7 @@ commentStartingWithp cs = do   -- ptrace "commentStartingWith"   oneOf cs-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   l <- anyChar `manyTill` (lift eolof)   optional newline   return $ T.pack l
Hledger/Read/CsvReader.hs view
@@ -104,7 +104,7 @@     if rulesfileexists     then do       dbg1IO "using conversion rules file" rulesfile-      liftIO $ (readFile' rulesfile >>= expandIncludes (takeDirectory rulesfile))+      liftIO $ (readFilePortably rulesfile >>= expandIncludes (takeDirectory rulesfile))     else return $ defaultRulesText rulesfile   rules <- liftIO (runExceptT $ parseAndValidateCsvRules rulesfile rulestext) >>= either throwerr return    dbg2IO "rules" rules@@ -169,13 +169,13 @@     "-" -> liftM (parseCSV "(stdin)") getContents     _   -> return $ parseCSV path csvdata --- | Return the cleaned up and validated CSV data, or an error.+-- | Return the cleaned up and validated CSV data (can be empty), or an error. validateCsv :: Int -> Either Parsec.ParseError CSV -> Either String [CsvRecord] validateCsv _ (Left e) = Left $ show e validateCsv numhdrlines (Right rs) = validate $ drop numhdrlines $ filternulls rs   where     filternulls = filter (/=[""])-    validate [] = Left "no CSV records found"+    validate [] = Right []     validate rs@(first:_)       | isJust lessthan2 = let r = fromJust lessthan2 in Left $ printf "CSV record %s has less than two fields" (show r)       | isJust different = let r = fromJust different in Left $ printf "the first CSV record %s has %d fields but %s has %d" (show first) length1 (show r) (length r)@@ -365,7 +365,7 @@ -- and runs some extra validation checks. parseRulesFile :: FilePath -> ExceptT String IO CsvRules parseRulesFile f = -  liftIO (readFile' f >>= expandIncludes (takeDirectory f)) >>= parseAndValidateCsvRules f+  liftIO (readFilePortably f >>= expandIncludes (takeDirectory f)) >>= parseAndValidateCsvRules f  -- | Inline all files referenced by include directives in this hledger CSV rules text, recursively. -- Included file paths may be relative to the directory of the provided file path.@@ -435,10 +435,10 @@ blankorcommentlinep = lift (pdbg 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep]  blanklinep :: CsvRulesParser ()-blanklinep = lift (many spacenonewline) >> newline >> return () <?> "blank line"+blanklinep = lift (skipMany spacenonewline) >> newline >> return () <?> "blank line"  commentlinep :: CsvRulesParser ()-commentlinep = lift (many spacenonewline) >> commentcharp >> lift restofline >> return () <?> "comment line"+commentlinep = lift (skipMany spacenonewline) >> commentcharp >> lift restofline >> return () <?> "comment line"  commentcharp :: CsvRulesParser Char commentcharp = oneOf (";#*" :: [Char])@@ -448,7 +448,7 @@   lift $ pdbg 3 "trying directive"   d <- fmap T.unpack $ choiceInState $ map (lift . mptext . T.pack) directives   v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp)-       <|> (optional (char ':') >> lift (many spacenonewline) >> lift eolof >> return "")+       <|> (optional (char ':') >> lift (skipMany spacenonewline) >> lift eolof >> return "")   return (d, v)   ) <?> "directive" @@ -471,8 +471,8 @@   lift $ pdbg 3 "trying fieldnamelist"   string "fields"   optional $ char ':'-  lift (some spacenonewline)-  let separator = lift (many spacenonewline) >> char ',' >> lift (many spacenonewline)+  lift (skipSome spacenonewline)+  let separator = lift (skipMany spacenonewline) >> char ',' >> lift (skipMany spacenonewline)   f <- fromMaybe "" <$> optional fieldnamep   fs <- some $ (separator >> fromMaybe "" <$> optional fieldnamep)   lift restofline@@ -529,11 +529,11 @@ assignmentseparatorp = do   lift $ pdbg 3 "trying assignmentseparatorp"   choice [-    -- try (lift (many spacenonewline) >> oneOf ":="),-    try (lift (many spacenonewline) >> char ':'),+    -- try (lift (skipMany spacenonewline) >> oneOf ":="),+    try (lift (skipMany spacenonewline) >> char ':'),     spaceChar     ]-  _ <- lift (many spacenonewline)+  _ <- lift (skipMany spacenonewline)   return ()  fieldvalp :: CsvRulesParser String@@ -544,9 +544,9 @@ conditionalblockp :: CsvRulesParser ConditionalBlock conditionalblockp = do   lift $ pdbg 3 "trying conditionalblockp"-  string "if" >> lift (many spacenonewline) >> optional newline+  string "if" >> lift (skipMany spacenonewline) >> optional newline   ms <- some recordmatcherp-  as <- many (lift (some spacenonewline) >> fieldassignmentp)+  as <- many (lift (skipSome spacenonewline) >> fieldassignmentp)   when (null as) $     fail "start of conditional block found, but no assignment rules afterward\n(assignment rules in a conditional block should be indented)\n"   return (ms, as)@@ -556,7 +556,7 @@ recordmatcherp = do   lift $ pdbg 2 "trying recordmatcherp"   -- pos <- currentPos-  _  <- optional (matchoperatorp >> lift (many spacenonewline) >> optional newline)+  _  <- optional (matchoperatorp >> lift (skipMany spacenonewline) >> optional newline)   ps <- patternsp   when (null ps) $     fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"@@ -589,10 +589,10 @@ --   pdbg 2 "trying fieldmatcher" --   f <- fromMaybe "all" `fmap` (optional $ do --          f' <- fieldname---          lift (many spacenonewline)+--          lift (skipMany spacenonewline) --          return f') --   char '~'---   lift (many spacenonewline)+--   lift (skipMany spacenonewline) --   ps <- patterns --   let r = "(" ++ intercalate "|" ps ++ ")" --   return (f,r)
Hledger/Read/JournalReader.hs view
@@ -79,7 +79,9 @@ import Control.Monad.Except (ExceptT(..), runExceptT, throwError) import Control.Monad.State.Strict import qualified Data.Map.Strict as M+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Text (Text) import Data.String import Data.List@@ -179,7 +181,7 @@ includedirectivep :: MonadIO m => ErroringJournalParser m () includedirectivep = do   string "include"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   filename  <- lift restofline   parentpos <- getPosition   parentj   <- get@@ -188,7 +190,7 @@     liftIO $ runExceptT $ do       let curdir = takeDirectory (sourceName parentpos)       filepath <- expandPath curdir filename `orRethrowIOError` (show parentpos ++ " locating " ++ filename)-      txt      <- readFileAnyLineEnding filepath `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)+      txt      <- readFilePortably filepath `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)       (ej1::Either (ParseError Char MPErr) ParsedJournal) <-         runParserT            (evalStateT@@ -233,15 +235,22 @@ accountdirectivep :: JournalParser m () accountdirectivep = do   string "account"-  lift (some spacenonewline)-  acct <- lift accountnamep+  lift (skipSome spacenonewline)+  acct <- lift accountnamep  -- eats single spaces+  macode' :: Maybe String <- (optional $ lift $ skipSome spacenonewline >> some digitChar)+  let macode :: Maybe AccountCode = read <$> macode'   newline-  many indentedlinep-  modify' (\j -> j{jaccounts = acct : jaccounts j})-+  _tags <- many $ do+    startpos <- getPosition+    l <- indentedlinep+    case runTextParser (setPosition startpos >> tagsp) $ T.pack l of+      Right ts -> return ts+      Left _e   -> return [] -- TODO throwError $ parseErrorPretty e+    +  modify' (\j -> j{jaccounts = (acct, macode) : jaccounts j})  indentedlinep :: JournalParser m String-indentedlinep = lift (some spacenonewline) >> (rstrip <$> lift restofline)+indentedlinep = lift (skipSome spacenonewline) >> (rstrip <$> lift restofline)  -- | Parse a one-line or multi-line commodity directive. --@@ -259,9 +268,9 @@ commoditydirectiveonelinep :: Monad m => JournalParser m () commoditydirectiveonelinep = do   string "commodity"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   Amount{acommodity,astyle} <- amountp-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   _ <- followingcommentp <|> (lift eolof >> return "")   let comm = Commodity{csymbol=acommodity, cformat=Just astyle}   modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})@@ -272,21 +281,21 @@ commoditydirectivemultilinep :: Monad m => ErroringJournalParser m () commoditydirectivemultilinep = do   string "commodity"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   sym <- lift commoditysymbolp   _ <- followingcommentp <|> (lift eolof >> return "")   mformat <- lastMay <$> many (indented $ formatdirectivep sym)   let comm = Commodity{csymbol=sym, cformat=mformat}   modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j})   where-    indented = (lift (some spacenonewline) >>)+    indented = (lift (skipSome spacenonewline) >>)  -- | Parse a format (sub)directive, throwing a parse error if its -- symbol does not match the one given. formatdirectivep :: Monad m => CommoditySymbol -> ErroringJournalParser m AmountStyle formatdirectivep expectedsym = do   string "format"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   pos <- getPosition   Amount{acommodity,astyle} <- amountp   _ <- followingcommentp <|> (lift eolof >> return "")@@ -299,7 +308,7 @@ keywordp = (() <$) . string . fromString  spacesp :: JournalParser m ()-spacesp = () <$ lift (some spacenonewline)+spacesp = () <$ lift (skipSome spacenonewline)  -- | Backtracking parser similar to string, but allows varying amount of space between words keywordsp :: String -> JournalParser m ()@@ -308,7 +317,7 @@ applyaccountdirectivep :: JournalParser m () applyaccountdirectivep = do   keywordsp "apply account" <?> "apply account directive"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   parent <- lift accountnamep   newline   pushParentAccount parent@@ -321,7 +330,7 @@ aliasdirectivep :: JournalParser m () aliasdirectivep = do   string "alias"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   alias <- lift accountaliasp   addAccountAlias alias @@ -333,7 +342,7 @@   -- pdbg 0 "basicaliasp"   old <- rstrip <$> (some $ noneOf ("=" :: [Char]))   char '='-  many spacenonewline+  skipMany spacenonewline   new <- rstrip <$> anyChar `manyTill` eolof  -- eol in journal, eof in command lines, normally   return $ BasicAlias (T.pack old) (T.pack new) @@ -343,9 +352,9 @@   char '/'   re <- some $ noneOf ("/\n\r" :: [Char]) -- paranoid: don't try to read past line end   char '/'-  many spacenonewline+  skipMany spacenonewline   char '='-  many spacenonewline+  skipMany spacenonewline   repl <- anyChar `manyTill` eolof   return $ RegexAlias re repl @@ -357,7 +366,7 @@ tagdirectivep :: JournalParser m () tagdirectivep = do   string "tag" <?> "tag directive"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   _ <- lift $ some nonspace   lift restofline   return ()@@ -371,7 +380,7 @@ defaultyeardirectivep :: JournalParser m () defaultyeardirectivep = do   char 'Y' <?> "default year"-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   y <- some digitChar   let y' = read y   failIfInvalidYear y@@ -380,7 +389,7 @@ defaultcommoditydirectivep :: Monad m => JournalParser m () defaultcommoditydirectivep = do   char 'D' <?> "default commodity"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   Amount{..} <- amountp   lift restofline   setDefaultCommodityAndStyle (acommodity, astyle)@@ -388,11 +397,11 @@ marketpricedirectivep :: Monad m => JournalParser m MarketPrice marketpricedirectivep = do   char 'P' <?> "market price"-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   symbol <- lift commoditysymbolp-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   price <- amountp   lift restofline   return $ MarketPrice date symbol price@@ -400,7 +409,7 @@ ignoredpricecommoditydirectivep :: JournalParser m () ignoredpricecommoditydirectivep = do   char 'N' <?> "ignored-price commodity"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   lift commoditysymbolp   lift restofline   return ()@@ -408,11 +417,11 @@ commodityconversiondirectivep :: Monad m => JournalParser m () commodityconversiondirectivep = do   char 'C' <?> "commodity conversion"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   amountp-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   char '='-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   amountp   lift restofline   return ()@@ -422,7 +431,7 @@ modifiertransactionp :: MonadIO m => ErroringJournalParser m ModifierTransaction modifiertransactionp = do   char '=' <?> "modifier transaction"-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   valueexpr <- T.pack <$> lift restofline   postings <- postingsp Nothing   return $ ModifierTransaction valueexpr postings@@ -430,7 +439,7 @@ periodictransactionp :: MonadIO m => ErroringJournalParser m PeriodicTransaction periodictransactionp = do   char '~' <?> "periodic transaction"-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   periodexpr <- T.pack <$> lift restofline   postings <- postingsp Nothing   return $ PeriodicTransaction periodexpr postings@@ -555,7 +564,7 @@  -- linebeginningwithspaces :: Monad m => JournalParser m String -- linebeginningwithspaces = do---   sp <- lift (some spacenonewline)+--   sp <- lift (skipSome spacenonewline) --   c <- nonspace --   cs <- lift restofline --   return $ sp ++ (c:cs) ++ "\n"@@ -563,15 +572,15 @@ postingp :: MonadIO m => Maybe Day -> ErroringJournalParser m Posting postingp mtdate = do   -- pdbg 0 "postingp"-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   status <- lift statusp-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   account <- modifiedaccountnamep   let (ptype, account') = (accountNamePostingType account, textUnbracket account)   amount <- spaceandamountormissingp   massertion <- partialbalanceassertionp   _ <- fixedlotpricep-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   (comment,tags,mdate,mdate2) <-     try (followingcommentandtagsp mtdate) <|> (newline >> return ("",[],Nothing,Nothing))   return posting
Hledger/Read/TimeclockReader.hs view
@@ -109,10 +109,10 @@ timeclockentryp = do   sourcepos <- genericSourcePos <$> lift getPosition   code <- oneOf ("bhioO" :: [Char])-  lift (some spacenonewline)+  lift (skipSome spacenonewline)   datetime <- datetimep-  account <- fromMaybe "" <$> optional (lift (some spacenonewline) >> modifiedaccountnamep)-  description <- T.pack . fromMaybe "" <$> lift (optional (some spacenonewline >> restofline))+  account <- fromMaybe "" <$> optional (lift (skipSome spacenonewline) >> modifiedaccountnamep)+  description <- T.pack . fromMaybe "" <$> lift (optional (skipSome spacenonewline >> restofline))   return $ TimeclockEntry sourcepos (read [code]) datetime account description  tests_Hledger_Read_TimeclockReader = TestList [
Hledger/Read/TimedotReader.hs view
@@ -107,9 +107,9 @@ timedotentryp = do   ptrace "  timedotentryp"   pos <- genericSourcePos <$> getPosition-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   a <- modifiedaccountnamep-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   hours <-     try (followingcommentp >> return 0)     <|> (timedotdurationp <*@@ -143,7 +143,7 @@ timedotnumericp = do   (q, _, _, _) <- lift $ numberp Nothing   msymbol <- optional $ choice $ map (string . fst) timeUnits-  lift (many spacenonewline)+  lift (skipMany spacenonewline)   let q' =          case msymbol of           Nothing  -> q
Hledger/Reports/BalanceReport.hs view
@@ -43,7 +43,7 @@   --- | A simple single-column balance report. It has:+-- | A simple balance report. It has: -- -- 1. a list of items, one per account, each containing: --@@ -77,7 +77,9 @@ -- This is like PeriodChangeReport with a single column (but more mature, -- eg this can do hierarchical display). balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport-balanceReport opts q j = (items, total)+balanceReport opts q j = +  (if invert_ opts then brNegate else id) $ +  (items, total)     where       -- dbg1 = const id -- exclude from debug output       dbg1 s = let p = "balanceReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in debug output@@ -88,7 +90,7 @@                          dbg1 "accts" $                          take 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts           | flat_ opts = dbg1 "accts" $-                         maybesortflat $+                         sortflat $                          filterzeros $                          filterempty $                          drop 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts@@ -97,7 +99,7 @@                          drop 1 $ flattenAccounts $                          markboring $                          prunezeros $-                         maybesorttree $+                         sorttree $                          clipAccounts (queryDepth q) accts           where             balance     = if flat_ opts then aebalance else aibalance@@ -105,12 +107,12 @@             filterempty = filter (\a -> anumpostings a > 0 || not (isZeroMixedAmount (balance a)))             prunezeros  = if empty_ opts then id else fromMaybe nullacct . pruneAccounts (isZeroMixedAmount . balance)             markboring  = if no_elide_ opts then id else markBoringParentAccounts-            maybesortflat | sort_amount_ opts = sortBy (maybeflip $ comparing balance)-                          | otherwise = id+            sortflat | sort_amount_ opts = sortBy (maybeflip $ comparing balance)+                     | otherwise         = sortBy (comparing accountCodeAndNameForSort)               where-                maybeflip = if normalbalance_ opts == Just NormalNegative then id else flip-            maybesorttree | sort_amount_ opts = sortAccountTreeByAmount (fromMaybe NormalPositive $ normalbalance_ opts)-                          | otherwise = id+                maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip+            sorttree | sort_amount_ opts = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts)+                     | otherwise         = sortAccountTreeByAccountCodeAndName       items = dbg1 "items" $ map (balanceReportItem opts q) accts'       total | not (flat_ opts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0]             | otherwise        = dbg1 "total" $@@ -148,6 +150,11 @@ --     items = [(a,a',n, headDef 0 bs) | ((a,a',n), bs) <- mbrrows] --     total = headDef 0 mbrtotals +-- | Flip the sign of all amounts in a BalanceReport.+brNegate :: BalanceReport -> BalanceReport+brNegate (is, tot) = (map brItemNegate is, -tot) +  where+    brItemNegate (a, a', d, amt) = (a, a', d, -amt)  tests_balanceReport =   let
Hledger/Reports/MultiBalanceReports.hs view
@@ -9,7 +9,9 @@   MultiBalanceReport(..),   MultiBalanceReportRow,   multiBalanceReport,-  singleBalanceReport,+  balanceReportFromMultiBalanceReport,+  mbrNegate,+  mbrNormaliseSign,    -- -- * Tests   tests_Hledger_Reports_MultiBalanceReport@@ -72,25 +74,15 @@ -- type alias just to remind us which AccountNames might be depth-clipped, below. type ClippedAccountName = AccountName --- | Generates a single column BalanceReport like balanceReport, but uses--- multiBalanceReport, so supports --historical. --- TODO Does not support boring parent eliding or --flat yet.-singleBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport-singleBalanceReport opts q j = (rows', total)-  where-    MultiBalanceReport (_, rows, (totals, _, _)) = multiBalanceReport opts q j-    rows' = [(a-             ,if flat_ opts then a else a'   -- BalanceReport expects full account name here with --flat-             ,if tree_ opts then d-1 else 0  -- BalanceReport uses 0-based account depths-             , headDef nullmixedamt amts     -- 0 columns is illegal, should not happen, return zeroes if it does-             ) | (a,a',d, amts, _, _) <- rows]-    total = headDef nullmixedamt totals- -- | 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.+-- 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 +-- (see ReportOpts and CompoundBalanceCommand). multiBalanceReport :: ReportOpts -> Query -> Journal -> MultiBalanceReport-multiBalanceReport opts q j = MultiBalanceReport (displayspans, sorteditems, totalsrow)+multiBalanceReport opts q j =+  (if invert_ opts then mbrNegate else id) $ +  MultiBalanceReport (displayspans, sorteditems, totalsrow)     where       symq       = dbg1 "symq"   $ filterQuery queryIsSym $ dbg1 "requested q" q       depthq     = dbg1 "depthq" $ filterQuery queryIsDepth q@@ -182,17 +174,18 @@        sorteditems :: [MultiBalanceReportRow] =         dbg1 "sorteditems" $-        maybesort items+        sortitems items         where-          maybesort-            | not $ sort_amount_ opts         = id-            | accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAmount-            | otherwise                       = sortFlatMultiBalanceReportRowsByAmount+          sortitems+            | sort_amount_ opts && accountlistmode_ opts == ALTree       = sortTreeMultiBalanceReportRowsByAmount+            | sort_amount_ opts                                          = sortFlatMultiBalanceReportRowsByAmount+            | not (sort_amount_ opts) && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAccountCodeAndName+            | otherwise                                                  = sortFlatMultiBalanceReportRowsByAccountCodeAndName             where               -- Sort the report rows, representing a flat account list, by row total.                sortFlatMultiBalanceReportRowsByAmount = sortBy (maybeflip $ comparing fifth6)                 where-                  maybeflip = if normalbalance_ opts == Just NormalNegative then id else flip+                  maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip                -- Sort the report rows, representing a tree of accounts, by row total at each level.               -- To do this we recreate an Account tree with the row totals as balances, @@ -209,14 +202,34 @@                     where                       -- this error should not happen, but it's ugly TODO                        setibalance a = a{aibalance=fromMaybe (error "sortTreeMultiBalanceReportRowsByAmount 1") $ lookup (aname a) atotals}-                  sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormalPositive $ normalbalance_ opts) accounttreewithbals+                  sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals                   sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree-                  sortedrows = [ -                    -- this error should not happen, but it's ugly TODO -                    fromMaybe (error "sortTreeMultiBalanceReportRowsByAmount 2") $ lookup (aname a) anamesandrows-                    | a <- sortedaccounts -                    ]+                  -- dropped the root account, also ignore any parent accounts not in rows+                  sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts  +              -- Sort the report rows by account code if any, with the empty account code coming last, then account name. +              sortFlatMultiBalanceReportRowsByAccountCodeAndName = sortBy (comparing acodeandname)+                where+                  acodeandname r = (acode', aname)+                    where+                      aname = first6 r+                      macode = fromMaybe Nothing $ lookup aname $ jaccounts j+                      acode' = fromMaybe maxBound macode ++              -- Sort the report rows, representing a tree of accounts, by account code and then account name at each level.+              -- Convert a tree of account names, look up the account codes, sort and flatten the tree, reorder the rows.+              sortTreeMultiBalanceReportRowsByAccountCodeAndName rows = sortedrows+                where+                  anamesandrows = [(first6 r, r) | r <- rows]+                  anames = map fst anamesandrows+                  nametree = treeFromPaths $ map expandAccountName anames+                  accounttree = nameTreeToAccount "root" nametree+                  accounttreewithcodes = mapAccounts (accountSetCodeFrom j) accounttree+                  sortedaccounttree = sortAccountTreeByAccountCodeAndName accounttreewithcodes+                  sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree+                  -- dropped the root account, also ignore any parent accounts not in rows+                  sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts +       totals :: [MixedAmount] =           -- dbg1 "totals" $           map sum balsbycol@@ -232,6 +245,34 @@        dbg1 s = let p = "multiBalanceReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in this function's debug output       -- dbg1 = const id  -- exclude this function from debug output++-- | Given a MultiBalanceReport and its normal balance sign,+-- if it is known to be normally negative, convert it to normally positive.+mbrNormaliseSign :: NormalSign -> MultiBalanceReport -> MultiBalanceReport+mbrNormaliseSign NormallyNegative = mbrNegate+mbrNormaliseSign _ = id++-- | Flip the sign of all amounts in a MultiBalanceReport.+mbrNegate (MultiBalanceReport (colspans, rows, totalsrow)) =+  MultiBalanceReport (colspans, map mbrRowNegate rows, mbrTotalsRowNegate totalsrow)+  where+    mbrRowNegate (acct,shortacct,indent,amts,tot,avg) = (acct,shortacct,indent,map negate amts,-tot,-avg)+    mbrTotalsRowNegate (amts,tot,avg) = (map negate amts,-tot,-avg)++-- | 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)+  where+    MultiBalanceReport (_, rows, (totals, _, _)) = multiBalanceReport opts q j+    rows' = [(a+             ,if flat_ opts then a else a'   -- BalanceReport expects full account name here with --flat+             ,if tree_ opts then d-1 else 0  -- BalanceReport uses 0-based account depths+             , headDef nullmixedamt amts     -- 0 columns is illegal, should not happen, return zeroes if it does+             ) | (a,a',d, amts, _, _) <- rows]+    total = headDef nullmixedamt totals   tests_multiBalanceReport =
Hledger/Reports/ReportOptions.hs view
@@ -19,12 +19,13 @@   simplifyStatuses,   whichDateFromOpts,   journalSelectingAmountFromOpts,+  intervalFromRawOpts,   queryFromOpts,   queryFromOptsOnly,   queryOptsFromOpts,   transactionDateFn,   postingDateFn,-  reportStartEndDates,+  reportSpan,   reportStartDate,   reportEndDate,   specifiedStartEndDates,@@ -35,6 +36,7 @@ ) where +import Control.Applicative ((<|>)) import Data.Data (Data) #if !MIN_VERSION_base(4,8,0) import Data.Functor.Compat ((<$>))@@ -102,10 +104,15 @@     ,value_          :: Bool     ,pretty_tables_  :: Bool     ,sort_amount_    :: Bool-    ,normalbalance_  :: Maybe NormalBalance-      -- ^ when running a balance report on accounts of the same normal balance type,-      -- eg in the income section of an income statement, this helps --sort-amount know-      -- how to sort negative numbers.+    ,invert_         :: Bool  -- ^ if true, flip all amount signs in reports+    ,normalbalance_  :: Maybe NormalSign+      -- ^ 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.        ,color_          :: Bool     ,forecast_       :: Bool     ,auto_           :: Bool@@ -141,6 +148,7 @@     def     def     def+    def  rawOptsToReportOpts :: RawOpts -> IO ReportOpts rawOptsToReportOpts rawopts = checkReportOpts <$> do@@ -169,6 +177,7 @@     ,no_total_    = boolopt "no-total" rawopts'     ,value_       = boolopt "value" rawopts'     ,sort_amount_ = boolopt "sort-amount" rawopts'+    ,invert_      = boolopt "invert" rawopts'     ,pretty_tables_ = boolopt "pretty-tables" rawopts'     ,color_       = color     ,forecast_    = boolopt "forecast" rawopts'@@ -402,24 +411,27 @@                                                              })  ] --- | The effective report start/end dates are the dates specified by options or queries,--- otherwise the earliest/latest transaction or posting date in the journal,--- otherwise (for an empty journal) nothing.+-- | The effective report span is the start and end dates specified by+-- 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.-reportStartEndDates :: Journal -> ReportOpts -> IO (Maybe (Day,Day))-reportStartEndDates j ropts = do-  (mspecifiedstartdate, mspecifiedenddate) <- specifiedStartEndDates ropts-  return $-    case journalDateSpan False j of  -- don't bother with secondary dates-      DateSpan (Just journalstartdate) (Just journalenddate) ->-        Just (fromMaybe journalstartdate mspecifiedstartdate, fromMaybe journalenddate mspecifiedenddate)-      _ -> Nothing+reportSpan :: Journal -> ReportOpts -> IO DateSpan+reportSpan j ropts = do+  (mspecifiedstartdate, mspecifiedenddate) <-+    dbg2 "specifieddates" <$> specifiedStartEndDates ropts+  let+    DateSpan mjournalstartdate mjournalenddate =+      dbg2 "journalspan" $ journalDateSpan False j  -- don't bother with secondary dates+    mstartdate = mspecifiedstartdate <|> mjournalstartdate+    menddate   = mspecifiedenddate   <|> mjournalenddate+  return $ dbg1 "reportspan" $ DateSpan mstartdate menddate  reportStartDate :: Journal -> ReportOpts -> IO (Maybe Day)-reportStartDate j ropts = (fst <$>) <$> reportStartEndDates j ropts+reportStartDate j ropts = spanStart <$> reportSpan j ropts  reportEndDate :: Journal -> ReportOpts -> IO (Maybe Day)-reportEndDate j ropts = (snd <$>) <$> reportStartEndDates j ropts+reportEndDate j ropts = spanEnd <$> reportSpan j ropts  -- | The specified report start/end dates are the dates specified by options or queries, if any. -- Needs IO to parse smart dates in options/queries.
Hledger/Utils.hs view
@@ -33,7 +33,7 @@                           -- the rest need to be done in each module I think                           ) where-import Control.Monad (liftM)+import Control.Monad (liftM, when) -- import Data.Char import Data.Default import Data.List@@ -150,30 +150,28 @@     [] -> Nothing     (md:_) -> md --- | Read a file in universal newline mode, handling any of the usual line ending conventions.-readFile' :: FilePath -> IO Text-readFile' name =  do-  h <- openFile name ReadMode-  hSetNewlineMode h universalNewlineMode-  T.hGetContents h+-- | 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. +readFilePortably :: FilePath -> IO Text+readFilePortably f =  openFile f ReadMode >>= readHandlePortably --- | Read a file in universal newline mode, handling any of the usual line ending conventions.-readFileAnyLineEnding :: FilePath -> IO Text-readFileAnyLineEnding path =  do-  h <- openFile path ReadMode-  hSetNewlineMode h universalNewlineMode-  T.hGetContents h+-- | Like readFilePortably, but read from standard input if the path is "-". +readFileOrStdinPortably :: String -> IO Text+readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably+  where+    openFileOrStdin :: String -> IOMode -> IO Handle+    openFileOrStdin "-" _ = return stdin+    openFileOrStdin f m   = openFile f m --- | Read the given file, or standard input if the path is "-", using--- universal newline mode.-readFileOrStdinAnyLineEnding :: String -> IO Text-readFileOrStdinAnyLineEnding f = do-  h <- fileHandle f+readHandlePortably :: Handle -> IO Text+readHandlePortably h = do   hSetNewlineMode h universalNewlineMode+  menc <- hGetEncoding h+  when (fmap show menc == Just "UTF-8") $  -- XXX no Eq instance, rely on Show+    hSetEncoding h utf8_bom   T.hGetContents h-  where-    fileHandle "-" = return stdin-    fileHandle f = openFile f ReadMode  -- | Total version of maximum, for integral types, giving 0 for an empty list. maximum' :: Integral a => [a] -> a
Hledger/Utils/String.hs view
@@ -129,7 +129,7 @@ words' "" = [] words' s  = map stripquotes $ fromparse $ parsewithString p s     where-      p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` some spacenonewline+      p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` skipSome spacenonewline              -- eof              return ss       pattern = many (noneOf whitespacechars)
Hledger/Utils/Text.hs view
@@ -2,6 +2,7 @@ -- There may be better alternatives out there.  {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}  module Hledger.Utils.Text  --  (@@ -57,7 +58,9 @@  -- import Data.Char import Data.List+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Text (Text) import qualified Data.Text as T -- import Text.Parsec
hledger-lib.cabal view
@@ -1,11 +1,11 @@--- This file has been generated from package.yaml by hpack version 0.21.2.+-- This file has been generated from package.yaml by hpack version 0.20.0. -- -- see: https://github.com/sol/hpack ----- hash: 65b38111258a2bd9065067e58c4d087cfca39eb38e7bb02c035d9265a9dd59fc+-- hash: 151fb38a89818af88d66f068ef87e0af6f8497b1ef11ab825558b8147952c88d  name:           hledger-lib-version:        1.5.1+version:        1.9 synopsis:       Core data types, parsers and functionality for the hledger accounting tools description:    This is a reusable library containing hledger's core functionality.                 .@@ -29,9 +29,6 @@  extra-source-files:     CHANGES-    README--data-files:     hledger_csv.5     hledger_csv.info     hledger_csv.txt@@ -44,12 +41,51 @@     hledger_timedot.5     hledger_timedot.info     hledger_timedot.txt+    README  source-repository head   type: git   location: https://github.com/simonmichael/hledger  library+  hs-source-dirs:+      ./.+  ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans+  build-depends:+      Decimal+    , HUnit+    , ansi-terminal >=0.6.2.3+    , array+    , base >=4.8 && <4.12+    , base-compat >=0.8.1+    , blaze-markup >=0.5.1+    , bytestring+    , cmdargs >=0.10+    , containers+    , csv+    , data-default >=0.5+    , deepseq+    , directory+    , extra+    , filepath+    , hashtables >=1.2+    , megaparsec >=5.0+    , mtl+    , mtl-compat+    , old-time+    , parsec >=3+    , pretty-show >=1.6.4+    , regex-tdfa+    , safe >=0.2+    , split >=0.1+    , text >=1.2+    , time >=1.5+    , transformers >=0.2+    , uglymemo+    , utf8-string >=0.3.5+  if (!impl(ghc >= 8.0))+    build-depends:+        semigroups ==0.18.*   exposed-modules:       Hledger       Hledger.Data@@ -97,15 +133,22 @@       Text.Megaparsec.Compat   other-modules:       Paths_hledger_lib+  default-language: Haskell2010++test-suite doctests+  type: exitcode-stdio-1.0+  main-is: doctests.hs   hs-source-dirs:       ./.+      tests   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans   build-depends:       Decimal+    , Glob >=0.7     , HUnit     , ansi-terminal >=0.6.2.3     , array-    , base >=4.8 && <5+    , base >=4.8 && <4.12     , base-compat >=0.8.1     , blaze-markup >=0.5.1     , bytestring@@ -115,6 +158,7 @@     , data-default >=0.5     , deepseq     , directory+    , doctest >=0.8     , extra     , filepath     , hashtables >=1.2@@ -135,11 +179,8 @@   if (!impl(ghc >= 8.0))     build-depends:         semigroups ==0.18.*-  default-language: Haskell2010--test-suite doctests-  type: exitcode-stdio-1.0-  main-is: doctests.hs+  if impl(ghc >= 8.4) && os(darwin)+    buildable: False   other-modules:       Hledger       Hledger.Data@@ -186,17 +227,21 @@       Hledger.Utils.UTF8IOCompat       Text.Megaparsec.Compat       Paths_hledger_lib+  default-language: Haskell2010++test-suite easytests+  type: exitcode-stdio-1.0+  main-is: easytests.hs   hs-source-dirs:       ./.       tests   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans   build-depends:       Decimal-    , Glob >=0.7     , HUnit     , ansi-terminal >=0.6.2.3     , array-    , base >=4.8 && <5+    , base >=4.8 && <4.12     , base-compat >=0.8.1     , blaze-markup >=0.5.1     , bytestring@@ -206,10 +251,11 @@     , data-default >=0.5     , deepseq     , directory-    , doctest >=0.8+    , easytest     , extra     , filepath     , hashtables >=1.2+    , hledger-lib     , megaparsec >=5.0     , mtl     , mtl-compat@@ -227,11 +273,6 @@   if (!impl(ghc >= 8.0))     build-depends:         semigroups ==0.18.*-  default-language: Haskell2010--test-suite hunittests-  type: exitcode-stdio-1.0-  main-is: hunittests.hs   other-modules:       Hledger       Hledger.Data@@ -278,6 +319,11 @@       Hledger.Utils.UTF8IOCompat       Text.Megaparsec.Compat       Paths_hledger_lib+  default-language: Haskell2010++test-suite hunittests+  type: exitcode-stdio-1.0+  main-is: hunittests.hs   hs-source-dirs:       ./.       tests@@ -287,7 +333,7 @@     , HUnit     , ansi-terminal >=0.6.2.3     , array-    , base >=4.8 && <5+    , base >=4.8 && <4.12     , base-compat >=0.8.1     , blaze-markup >=0.5.1     , bytestring@@ -320,4 +366,50 @@   if (!impl(ghc >= 8.0))     build-depends:         semigroups ==0.18.*+  other-modules:+      Hledger+      Hledger.Data+      Hledger.Data.Account+      Hledger.Data.AccountName+      Hledger.Data.Amount+      Hledger.Data.AutoTransaction+      Hledger.Data.Commodity+      Hledger.Data.Dates+      Hledger.Data.Journal+      Hledger.Data.Ledger+      Hledger.Data.MarketPrice+      Hledger.Data.Period+      Hledger.Data.Posting+      Hledger.Data.RawOptions+      Hledger.Data.StringFormat+      Hledger.Data.Timeclock+      Hledger.Data.Transaction+      Hledger.Data.Types+      Hledger.Query+      Hledger.Read+      Hledger.Read.Common+      Hledger.Read.CsvReader+      Hledger.Read.JournalReader+      Hledger.Read.TimeclockReader+      Hledger.Read.TimedotReader+      Hledger.Reports+      Hledger.Reports.BalanceHistoryReport+      Hledger.Reports.BalanceReport+      Hledger.Reports.EntriesReport+      Hledger.Reports.MultiBalanceReports+      Hledger.Reports.PostingsReport+      Hledger.Reports.ReportOptions+      Hledger.Reports.TransactionsReports+      Hledger.Utils+      Hledger.Utils.Color+      Hledger.Utils.Debug+      Hledger.Utils.Parse+      Hledger.Utils.Regex+      Hledger.Utils.String+      Hledger.Utils.Test+      Hledger.Utils.Text+      Hledger.Utils.Tree+      Hledger.Utils.UTF8IOCompat+      Text.Megaparsec.Compat+      Paths_hledger_lib   default-language: Haskell2010
hledger_csv.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_csv" "5" "December 2017" "hledger 1.5" "hledger User Manuals"+.TH "hledger_csv" "5" "March 2018" "hledger 1.9" "hledger User Manuals"   
hledger_csv.info view
@@ -3,7 +3,7 @@  File: hledger_csv.info,  Node: Top,  Next: CSV RULES,  Up: (dir) -hledger_csv(5) hledger 1.5+hledger_csv(5) hledger 1.9 **************************  hledger can read CSV (comma-separated value) files as if they were
hledger_csv.txt view
@@ -249,4 +249,4 @@   -hledger 1.5                      December 2017                  hledger_csv(5)+hledger 1.9                       March 2018                    hledger_csv(5)
hledger_journal.5 view
@@ -1,6 +1,6 @@ .\"t -.TH "hledger_journal" "5" "December 2017" "hledger 1.5" "hledger User Manuals"+.TH "hledger_journal" "5" "March 2018" "hledger 1.9" "hledger User Manuals"   @@ -369,6 +369,14 @@ .P .PD \f[C]1\ 999\ 999.9455\f[]+.PD 0+.P+.PD+\f[C]EUR\ 1E3\f[]+.PD 0+.P+.PD+\f[C]1000E\-6s\f[] .PP As you can see, the amount format is somewhat flexible: .IP \[bu] 2@@ -389,6 +397,13 @@ .IP \[bu] 2 decimal part can be separated by comma or period and should be different from digit groups separator+.IP \[bu] 2+scientific E\-notation is allowed.+Be careful not to use a digit group separator character in scientific+notation, as it's not supported and it might get mistaken for a decimal+point.+(Declaring the digit group separator character explicitly with a+commodity directive will prevent this.) .PP You can use any of these variations when recording data. However, there is some ambiguous way of representing numbers like@@ -943,23 +958,55 @@ .fi .SS account directive .PP-The \f[C]account\f[] directive predefines account names, as in Ledger-and Beancount.-This may be useful for your own documentation; hledger doesn't make use-of it yet.+The \f[C]account\f[] directive predeclares account names.+The simplest form is \f[C]account\ ACCTNAME\f[], eg: .IP .nf \f[C]-;\ account\ ACCT-;\ \ \ OPTIONAL\ COMMENTS/TAGS...- account\ assets:bank:checking-\ a\ comment-\ acct\-no:12345--account\ expenses:food+\f[]+.fi+.PP+Currently this mainly helps with account name autocompletion in eg+hledger add, hledger\-iadd, hledger\-web, and ledger\-mode.+.PD 0+.P+.PD+In future it will also help detect misspelled accounts.+.PP+Account names can be followed by a numeric account code:+.IP+.nf+\f[C]+account\ assets\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 1000+account\ assets:bank:checking\ \ \ \ 1110+account\ liabilities\ \ \ \ \ \ \ \ \ \ \ \ \ 2000+account\ revenues\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4000+account\ expenses\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 6000+\f[]+.fi+.PP+This affects account display order in reports: accounts with codes are+listed before accounts without codes, in increasing code order.+(Otherwise, accounts are listed alphabetically.) Account codes should be+all numeric digits, unique, and separated from the account name by at+least two spaces (since account names may contain single spaces).+By convention, often the first digit indicates the type of account, as+in this numbering scheme and the example above.+In future, we might use this to recognize account types.+.PP+An account directive can also have indented subdirectives following it,+which are currently ignored.+Here is the full syntax:+.IP+.nf+\f[C]+;\ account\ ACCTNAME\ \ [OPTIONALCODE]+;\ \ \ [OPTIONALSUBDIRECTIVES] -;\ etc.+account\ assets:bank:checking\ \ \ 1110+\ \ a\ comment+\ \ some\-tag:12345 \f[] .fi .SS apply account directive@@ -1112,8 +1159,11 @@ It can include journal, timeclock or timedot files, but not CSV files. .SH Periodic transactions .PP-A periodic transaction starts with a tilde `~' in place of a date-followed by a period expression:+Periodic transactions are a kind of rule with a dual purpose: they can+specify recurring future transactions (with \f[C]\-\-forecast\f[]), or+budget goals (with \f[C]\-\-budget\f[]).+They look a bit like a transaction, except the first line is a tilde+(\f[C]~\f[]) followed by a period expression: .IP .nf \f[C]@@ -1123,23 +1173,25 @@ \f[] .fi .PP-Periodic transactions are used for forecasting and budgeting only, they-have no effect unless the \f[C]\-\-forecast\f[] or \f[C]\-\-budget\f[]-flag is used. With \f[C]\-\-forecast\f[], each periodic transaction rule generates-recurring forecast transactions at the specified interval, beginning the-day after the last recorded journal transaction and ending 6 months from-today, or at the specified report end date.+recurring \[lq]forecast\[rq] transactions at the specified interval,+beginning the day after the latest recorded journal transaction (or+today, if there are no transactions) and ending 6 months from today (or+at the report end date, if specified).+.PP With \f[C]balance\ \-\-budget\f[], each periodic transaction declares-recurring budget goals for one or more accounts.-.PD 0-.P-.PD-For more details, see: balance > Budgeting, Budgeting and Forecasting.-.SH Automated posting rules+recurring budget goals for the specified accounts.+Eg the example above declares the goal of receiving $400 from+\f[C]income:acme\ inc\f[] (and also, depositing $400 into+\f[C]assets:bank:checking\f[]) every week. .PP-Automated posting rule starts with an equal sign `=' in place of a date,-followed by a query:+For more details, see: balance: Budgeting and Budgeting and Forecasting.+.SH Automated postings+.PP+Automated postings are postings added automatically by rule to certain+transactions (with \f[C]\-\-auto\f[]).+An automated posting rule looks like a transaction where the first line+is an equal sign (\f[C]=\f[]) followed by a query: .IP .nf \f[C]@@ -1149,36 +1201,33 @@ \f[] .fi .PP-When \f[C]\-\-auto\f[] option is specified on the command line,-automated posting rule will add its postings to all transactions that-match the query.-.PP-If amount in the automated posting rule includes commodity name, new-posting will be made in the given commodity, otherwise commodity of the-matched transaction will be used.-.PP-When amount in the automated posting rule begins with the '*', amount-will be treated as a multiplier that is applied to the amount of the-first posting in the matched transaction.+The posting amounts can be of the form \f[C]*N\f[], which means \[lq]the+amount of the matched transaction's first posting, multiplied by N\[rq].+They can also be ordinary fixed amounts.+Fixed amounts with no commodity symbol will be given the same commodity+as the matched transaction's first posting. .PP-In example above, every transaction in \f[C]expenses:gifts\f[] account-will have two additional postings added to it: amount of the original-gift will be debited from \f[C]budget:gifts\f[] and credited into-\f[C]assets:budget\f[]:+This example adds a corresponding (unbalanced) budget posting to every+transaction involving the \f[C]expenses:gifts\f[] account: .IP .nf \f[C]-;\ Original\ transaction+=\ expenses:gifts+\ \ \ \ (budget:gifts)\ \ *\-1+ 2017\-12\-14 \ \ expenses:gifts\ \ $20 \ \ assets--;\ With\ automated\ postings\ applied+\f[]+.fi+.IP+.nf+\f[C]+$\ hledger\ print\ \-\-auto 2017/12/14 \ \ \ \ expenses:gifts\ \ \ \ \ \ \ \ \ \ \ \ \ $20 \ \ \ \ assets-\ \ \ \ budget:gifts\ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-20-\ \ \ \ assets:budget\ \ \ \ \ \ \ \ \ \ \ \ \ \ $20+\ \ \ \ (budget:gifts)\ \ \ \ \ \ \ \ \ \ \ \ $\-20 \f[] .fi .SH EDITOR SUPPORT@@ -1194,8 +1243,13 @@ .PP .TS tab(@);-lw(16.5n) lw(53.5n).+lw(12.2n) lw(57.8n). T{+Editor+T}@T{+T}+_+T{ Emacs T}@T{ http://www.ledger\-cli.org/3.0/doc/ledger\-mode.html@@ -1208,7 +1262,7 @@ T{ Sublime Text T}@T{-https://github.com/ledger/ledger/wiki/Using\-Sublime\-Text+https://github.com/ledger/ledger/wiki/Editing\-Ledger\-files\-with\-Sublime\-Text\-or\-RubyMine T} T{ Textmate
hledger_journal.info view
@@ -4,7 +4,7 @@  File: hledger_journal.info,  Node: Top,  Next: FILE FORMAT,  Up: (dir) -hledger_journal(5) hledger 1.5+hledger_journal(5) hledger 1.9 ******************************  hledger's usual data source is a plain text file containing journal@@ -58,7 +58,7 @@  * FILE FORMAT:: * Periodic transactions::-* Automated posting rules::+* Automated postings:: * EDITOR SUPPORT::  @@ -349,6 +349,8 @@ 'INR 9,99,99,999.00' 'EUR -2.000.000,00' '1 999 999.9455'+'EUR 1E3'+'1000E-6s'     As you can see, the amount format is somewhat flexible: @@ -365,6 +367,11 @@      all groups    * decimal part can be separated by comma or period and should be      different from digit groups separator+   * scientific E-notation is allowed.  Be careful not to use a digit+     group separator character in scientific notation, as it's not+     supported and it might get mistaken for a decimal point.+     (Declaring the digit group separator character explicitly with a+     commodity directive will prevent this.)     You can use any of these variations when recording data.  However, there is some ambiguous way of representing numbers like '$1.000' and@@ -903,21 +910,42 @@ 1.14.2 account directive ------------------------ -The 'account' directive predefines account names, as in Ledger and-Beancount.  This may be useful for your own documentation; hledger-doesn't make use of it yet.--; account ACCT-;   OPTIONAL COMMENTS/TAGS...+The 'account' directive predeclares account names.  The simplest form is+'account ACCTNAME', eg:  account assets:bank:checking- a comment- acct-no:12345 -account expenses:food+   Currently this mainly helps with account name autocompletion in eg+hledger add, hledger-iadd, hledger-web, and ledger-mode.+In future it will also help detect misspelled accounts. -; etc.+   Account names can be followed by a numeric account code: +account assets                  1000+account assets:bank:checking    1110+account liabilities             2000+account revenues                4000+account expenses                6000++   This affects account display order in reports: accounts with codes+are listed before accounts without codes, in increasing code order.+(Otherwise, accounts are listed alphabetically.)  Account codes should+be all numeric digits, unique, and separated from the account name by at+least two spaces (since account names may contain single spaces).  By+convention, often the first digit indicates the type of account, as in+this numbering scheme and the example above.  In future, we might use+this to recognize account types.++   An account directive can also have indented subdirectives following+it, which are currently ignored.  Here is the full syntax:++; account ACCTNAME  [OPTIONALCODE]+;   [OPTIONALSUBDIRECTIVES]++account assets:bank:checking   1110+  a comment+  some-tag:12345+  File: hledger_journal.info,  Node: apply account directive,  Next: Multi-line comments,  Prev: account directive,  Up: Directives @@ -1059,71 +1087,73 @@ include journal, timeclock or timedot files, but not CSV files.  -File: hledger_journal.info,  Node: Periodic transactions,  Next: Automated posting rules,  Prev: FILE FORMAT,  Up: Top+File: hledger_journal.info,  Node: Periodic transactions,  Next: Automated postings,  Prev: FILE FORMAT,  Up: Top  2 Periodic transactions *********************** -A periodic transaction starts with a tilde '~' in place of a date-followed by a period expression:+Periodic transactions are a kind of rule with a dual purpose: they can+specify recurring future transactions (with '--forecast'), or budget+goals (with '--budget').  They look a bit like a transaction, except the+first line is a tilde ('~') followed by a period expression:  ~ weekly   assets:bank:checking   $400 ; paycheck   income:acme inc -   Periodic transactions are used for forecasting and budgeting only,-they have no effect unless the '--forecast' or '--budget' flag is used.-With '--forecast', each periodic transaction rule generates recurring-forecast transactions at the specified interval, beginning the day after-the last recorded journal transaction and ending 6 months from today, or-at the specified report end date.  With 'balance --budget', each-periodic transaction declares recurring budget goals for one or more-accounts.-For more details, see: balance > Budgeting, Budgeting and Forecasting.+   With '--forecast', each periodic transaction rule generates recurring+"forecast" transactions at the specified interval, beginning the day+after the latest recorded journal transaction (or today, if there are no+transactions) and ending 6 months from today (or at the report end date,+if specified). +   With 'balance --budget', each periodic transaction declares recurring+budget goals for the specified accounts.  Eg the example above declares+the goal of receiving $400 from 'income:acme inc' (and also, depositing+$400 into 'assets:bank:checking') every week.++   For more details, see: balance: Budgeting and Budgeting and+Forecasting.+ -File: hledger_journal.info,  Node: Automated posting rules,  Next: EDITOR SUPPORT,  Prev: Periodic transactions,  Up: Top+File: hledger_journal.info,  Node: Automated postings,  Next: EDITOR SUPPORT,  Prev: Periodic transactions,  Up: Top -3 Automated posting rules-*************************+3 Automated postings+******************** -Automated posting rule starts with an equal sign '=' in place of a date,-followed by a query:+Automated postings are postings added automatically by rule to certain+transactions (with '--auto').  An automated posting rule looks like a+transaction where the first line is an equal sign ('=') followed by a+query:  = expenses:gifts     budget:gifts  *-1     assets:budget  *1 -   When '--auto' option is specified on the command line, automated-posting rule will add its postings to all transactions that match the-query.--   If amount in the automated posting rule includes commodity name, new-posting will be made in the given commodity, otherwise commodity of the-matched transaction will be used.+   The posting amounts can be of the form '*N', which means "the amount+of the matched transaction's first posting, multiplied by N". They can+also be ordinary fixed amounts.  Fixed amounts with no commodity symbol+will be given the same commodity as the matched transaction's first+posting. -   When amount in the automated posting rule begins with the '*', amount-will be treated as a multiplier that is applied to the amount of the-first posting in the matched transaction.+   This example adds a corresponding (unbalanced) budget posting to+every transaction involving the 'expenses:gifts' account: -   In example above, every transaction in 'expenses:gifts' account will-have two additional postings added to it: amount of the original gift-will be debited from 'budget:gifts' and credited into 'assets:budget':+= expenses:gifts+    (budget:gifts)  *-1 -; Original transaction 2017-12-14   expenses:gifts  $20   assets -; With automated postings applied+$ hledger print --auto 2017/12/14     expenses:gifts             $20     assets-    budget:gifts              $-20-    assets:budget              $20+    (budget:gifts)            $-20  -File: hledger_journal.info,  Node: EDITOR SUPPORT,  Prev: Automated posting rules,  Up: Top+File: hledger_journal.info,  Node: EDITOR SUPPORT,  Prev: Automated postings,  Up: Top  4 EDITOR SUPPORT ****************@@ -1136,100 +1166,105 @@    These were written with Ledger in mind, but also work with hledger files: -Emacs             http://www.ledger-cli.org/3.0/doc/ledger-mode.html-Vim               https://github.com/ledger/ledger/wiki/Getting-started-Sublime Text      https://github.com/ledger/ledger/wiki/Using-Sublime-Text-Textmate          https://github.com/ledger/ledger/wiki/Using-TextMate-2-Text Wrangler     https://github.com/ledger/ledger/wiki/Editing-Ledger-files-with-TextWrangler-Visual Studio     https://marketplace.visualstudio.com/items?itemName=mark-hansen.hledger-vscode+Editor+--------------------------------------------------------------------------+Emacs        http://www.ledger-cli.org/3.0/doc/ledger-mode.html+Vim          https://github.com/ledger/ledger/wiki/Getting-started+Sublime      https://github.com/ledger/ledger/wiki/Editing-Ledger-files-with-Sublime-Text-or-RubyMine+Text+Textmate     https://github.com/ledger/ledger/wiki/Using-TextMate-2+Text         https://github.com/ledger/ledger/wiki/Editing-Ledger-files-with-TextWrangler+Wrangler  +Visual       https://marketplace.visualstudio.com/items?itemName=mark-hansen.hledger-vscode+Studio Code   Tag Table: Node: Top76-Node: FILE FORMAT2424-Ref: #file-format2555-Node: Transactions2778-Ref: #transactions2899-Node: Postings3583-Ref: #postings3710-Node: Dates4705-Ref: #dates4820-Node: Simple dates4885-Ref: #simple-dates5011-Node: Secondary dates5377-Ref: #secondary-dates5531-Node: Posting dates7094-Ref: #posting-dates7223-Node: Status8597-Ref: #status8717-Node: Description10425-Ref: #description10563-Node: Payee and note10882-Ref: #payee-and-note10996-Node: Account names11238-Ref: #account-names11381-Node: Amounts11868-Ref: #amounts12004-Node: Virtual Postings14684-Ref: #virtual-postings14843-Node: Balance Assertions16063-Ref: #balance-assertions16238-Node: Assertions and ordering17134-Ref: #assertions-and-ordering17320-Node: Assertions and included files18020-Ref: #assertions-and-included-files18261-Node: Assertions and multiple -f options18594-Ref: #assertions-and-multiple--f-options18848-Node: Assertions and commodities18980-Ref: #assertions-and-commodities19215-Node: Assertions and subaccounts19911-Ref: #assertions-and-subaccounts20143-Node: Assertions and virtual postings20664-Ref: #assertions-and-virtual-postings20871-Node: Balance Assignments21013-Ref: #balance-assignments21182-Node: Prices22302-Ref: #prices22435-Node: Transaction prices22486-Ref: #transaction-prices22631-Node: Market prices24787-Ref: #market-prices24922-Node: Comments25882-Ref: #comments26004-Node: Tags27246-Ref: #tags27364-Node: Directives28766-Ref: #directives28879-Node: Account aliases29072-Ref: #account-aliases29216-Node: Basic aliases29820-Ref: #basic-aliases29963-Node: Regex aliases30653-Ref: #regex-aliases30821-Node: Multiple aliases31539-Ref: #multiple-aliases31711-Node: end aliases32209-Ref: #end-aliases32349-Node: account directive32450-Ref: #account-directive32630-Node: apply account directive32926-Ref: #apply-account-directive33122-Node: Multi-line comments33781-Ref: #multi-line-comments33971-Node: commodity directive34099-Ref: #commodity-directive34283-Node: Default commodity35155-Ref: #default-commodity35328-Node: Default year35865-Ref: #default-year36030-Node: Including other files36453-Ref: #including-other-files36610-Node: Periodic transactions37007-Ref: #periodic-transactions37178-Node: Automated posting rules37921-Ref: #automated-posting-rules38099-Node: EDITOR SUPPORT39208-Ref: #editor-support39338+Node: FILE FORMAT2419+Ref: #file-format2550+Node: Transactions2773+Ref: #transactions2894+Node: Postings3578+Ref: #postings3705+Node: Dates4700+Ref: #dates4815+Node: Simple dates4880+Ref: #simple-dates5006+Node: Secondary dates5372+Ref: #secondary-dates5526+Node: Posting dates7089+Ref: #posting-dates7218+Node: Status8592+Ref: #status8712+Node: Description10420+Ref: #description10558+Node: Payee and note10877+Ref: #payee-and-note10991+Node: Account names11233+Ref: #account-names11376+Node: Amounts11863+Ref: #amounts11999+Node: Virtual Postings15014+Ref: #virtual-postings15173+Node: Balance Assertions16393+Ref: #balance-assertions16568+Node: Assertions and ordering17464+Ref: #assertions-and-ordering17650+Node: Assertions and included files18350+Ref: #assertions-and-included-files18591+Node: Assertions and multiple -f options18924+Ref: #assertions-and-multiple--f-options19178+Node: Assertions and commodities19310+Ref: #assertions-and-commodities19545+Node: Assertions and subaccounts20241+Ref: #assertions-and-subaccounts20473+Node: Assertions and virtual postings20994+Ref: #assertions-and-virtual-postings21201+Node: Balance Assignments21343+Ref: #balance-assignments21512+Node: Prices22632+Ref: #prices22765+Node: Transaction prices22816+Ref: #transaction-prices22961+Node: Market prices25117+Ref: #market-prices25252+Node: Comments26212+Ref: #comments26334+Node: Tags27576+Ref: #tags27694+Node: Directives29096+Ref: #directives29209+Node: Account aliases29402+Ref: #account-aliases29546+Node: Basic aliases30150+Ref: #basic-aliases30293+Node: Regex aliases30983+Ref: #regex-aliases31151+Node: Multiple aliases31869+Ref: #multiple-aliases32041+Node: end aliases32539+Ref: #end-aliases32679+Node: account directive32780+Ref: #account-directive32960+Node: apply account directive34307+Ref: #apply-account-directive34503+Node: Multi-line comments35162+Ref: #multi-line-comments35352+Node: commodity directive35480+Ref: #commodity-directive35664+Node: Default commodity36536+Ref: #default-commodity36709+Node: Default year37246+Ref: #default-year37411+Node: Including other files37834+Ref: #including-other-files37991+Node: Periodic transactions38388+Ref: #periodic-transactions38554+Node: Automated postings39543+Ref: #automated-postings39706+Node: EDITOR SUPPORT40608+Ref: #editor-support40733  End Tag Table
hledger_journal.txt view
@@ -261,6 +261,8 @@        INR 9,99,99,999.00        EUR -2.000.000,00        1 999 999.9455+       EUR 1E3+       1000E-6s         As you can see, the amount format is somewhat flexible: @@ -282,6 +284,12 @@        o decimal  part  can be separated by comma or period and should be dif-          ferent from digit groups separator +       o scientific E-notation is allowed.  Be careful  not  to  use  a  digit+         group  separator  character  in scientific notation, as it's not sup-+         ported and it might get mistaken for a decimal point.  (Declaring the+         digit group separator character explicitly with a commodity directive+         will prevent this.)+        You can use any of these  variations  when  recording  data.   However,        there  is  some  ambiguous  way of representing numbers like $1.000 and        $1,000 both may mean either one thousand or  one  dollar.   By  default@@ -690,21 +698,42 @@               end aliases     account directive-       The account directive predefines account names, as in Ledger and  Bean--       count.   This may be useful for your own documentation; hledger doesn't-       make use of it yet.--              ; account ACCT-              ;   OPTIONAL COMMENTS/TAGS...+       The account directive predeclares account names.  The simplest form  is+       account ACCTNAME, eg:                account assets:bank:checking-               a comment-               acct-no:12345 -              account expenses:food+       Currently  this  mainly  helps  with  account name autocompletion in eg+       hledger add, hledger-iadd, hledger-web, and ledger-mode.+       In future it will also help detect misspelled accounts. -              ; etc.+       Account names can be followed by a numeric account code: +              account assets                  1000+              account assets:bank:checking    1110+              account liabilities             2000+              account revenues                4000+              account expenses                6000++       This affects account display order in reports: accounts with codes  are+       listed  before accounts without codes, in increasing code order.  (Oth-+       erwise, accounts are listed alphabetically.) Account  codes  should  be+       all  numeric  digits, unique, and separated from the account name by at+       least two spaces (since account names may contain single  spaces).   By+       convention,  often the first digit indicates the type of account, as in+       this numbering scheme and the example above.  In future, we  might  use+       this to recognize account types.++       An account directive can also have indented subdirectives following it,+       which are currently ignored.  Here is the full syntax:++              ; account ACCTNAME  [OPTIONALCODE]+              ;   [OPTIONALSUBDIRECTIVES]++              account assets:bank:checking   1110+                a comment+                some-tag:12345+    apply account directive        You can specify a  parent  account  which  will  be  prepended  to  all        accounts  within  a  section of the journal.  Use the apply account and@@ -816,84 +845,87 @@        include journal, timeclock or timedot files, but not CSV files.  Periodic transactions-       A periodic transaction starts with a tilde `~' in place of a date  fol--       lowed by a period expression:+       Periodic transactions are a kind of rule with a dual purpose: they  can+       specify  recurring  future  transactions  (with  --forecast), or budget+       goals (with --budget).  They look a bit like a transaction, except  the+       first line is a tilde (~) followed by a period expression:                ~ weekly                 assets:bank:checking   $400 ; paycheck                 income:acme inc -       Periodic transactions are used for forecasting and budgeting only, they-       have no effect unless the --forecast or --budget flag  is  used.   With-       --forecast, each periodic transaction rule generates recurring forecast-       transactions at the specified interval, beginning  the  day  after  the-       last recorded journal transaction and ending 6 months from today, or at-       the specified report end date.  With  balance --budget,  each  periodic-       transaction declares recurring budget goals for one or more accounts.-       For  more details, see: balance > Budgeting, Budgeting and Forecasting.+       With  --forecast,  each  periodic  transaction rule generates recurring+       "forecast" transactions at the specified interval,  beginning  the  day+       after  the  latest recorded journal transaction (or today, if there are+       no transactions) and ending 6 months from today (or at the  report  end+       date, if specified). -Automated posting rules-       Automated posting rule starts with an equal sign  `='  in  place  of  a-       date, followed by a query:+       With  balance --budget,  each  periodic  transaction declares recurring+       budget goals for the specified accounts.  Eg the example above declares+       the  goal  of receiving $400 from income:acme inc (and also, depositing+       $400 into assets:bank:checking) every week. +       For more details, see: balance: Budgeting and Budgeting  and  Forecast-+       ing.++Automated postings+       Automated  postings are postings added automatically by rule to certain+       transactions (with --auto).  An automated posting  rule  looks  like  a+       transaction  where  the  first  line is an equal sign (=) followed by a+       query:+               = expenses:gifts                   budget:gifts  *-1                   assets:budget  *1 -       When  --auto option is specified on the command line, automated posting-       rule will add its postings to all transactions that match the query.--       If amount in the automated posting rule includes  commodity  name,  new-       posting will be made in the given commodity, otherwise commodity of the-       matched transaction will be used.+       The posting amounts can be of the form *N, which means "the  amount  of+       the  matched  transaction's  first posting, multiplied by N".  They can+       also be ordinary fixed amounts.  Fixed amounts with no commodity symbol+       will  be  given  the  same commodity as the matched transaction's first+       posting. -       When amount in the automated posting rule begins with the  '*',  amount-       will  be  treated  as a multiplier that is applied to the amount of the-       first posting in the matched transaction.+       This example adds a corresponding (unbalanced) budget posting to  every+       transaction involving the expenses:gifts account: -       In example above, every transaction in expenses:gifts account will have-       two  additional  postings added to it: amount of the original gift will-       be debited from budget:gifts and credited into assets:budget:+              = expenses:gifts+                  (budget:gifts)  *-1 -              ; Original transaction               2017-12-14                 expenses:gifts  $20                 assets -              ; With automated postings applied+              $ hledger print --auto               2017/12/14                   expenses:gifts             $20                   assets-                  budget:gifts              $-20-                  assets:budget              $20+                  (budget:gifts)            $-20  EDITOR SUPPORT        Add-on modes exist for various text editors, to make working with jour--       nal  files  easier.   They add colour, navigation aids and helpful com--       mands.  For hledger users who  edit  the  journal  file  directly  (the+       nal files easier.  They add colour, navigation aids  and  helpful  com-+       mands.   For  hledger  users  who  edit  the journal file directly (the        majority), using one of these modes is quite recommended. -       These  were  written  with  Ledger  in mind, but also work with hledger+       These were written with Ledger in mind,  but  also  work  with  hledger        files:  -       Emacs              http://www.ledger-cli.org/3.0/doc/ledger-mode.html-       Vim                https://github.com/ledger/ledger/wiki/Getting-started---       Sublime Text       https://github.com/ledger/ledger/wiki/Using-Sub--                          lime-Text-       Textmate           https://github.com/ledger/ledger/wiki/Using-Text--                          Mate-2-       Text Wrangler      https://github.com/ledger/ledger/wiki/Edit--                          ing-Ledger-files-with-TextWrangler-       Visual    Studio   https://marketplace.visualstudio.com/items?item--       Code               Name=mark-hansen.hledger-vscode+       Editor+       --------------------------------------------------------------------------+       Emacs          http://www.ledger-cli.org/3.0/doc/ledger-mode.html+       Vim            https://github.com/ledger/ledger/wiki/Getting-started+       Sublime Text   https://github.com/ledger/ledger/wiki/Edit-+                      ing-Ledger-files-with-Sublime-Text-or-RubyMine+       Textmate       https://github.com/ledger/ledger/wiki/Using-TextMate-2+       Text   Wran-   https://github.com/ledger/ledger/wiki/Edit-+       gler           ing-Ledger-files-with-TextWrangler+       Visual  Stu-   https://marketplace.visualstudio.com/items?item-+       dio Code       Name=mark-hansen.hledger-vscode    REPORTING BUGS-       Report bugs at http://bugs.hledger.org (or on the #hledger IRC  channel+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel        or hledger mail list)  @@ -907,7 +939,7 @@   SEE ALSO-       hledger(1),      hledger-ui(1),     hledger-web(1),     hledger-api(1),+       hledger(1),     hledger-ui(1),     hledger-web(1),      hledger-api(1),        hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_time-        dot(5), ledger(1) @@ -915,4 +947,4 @@   -hledger 1.5                      December 2017              hledger_journal(5)+hledger 1.9                       March 2018                hledger_journal(5)
hledger_timeclock.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_timeclock" "5" "December 2017" "hledger 1.5" "hledger User Manuals"+.TH "hledger_timeclock" "5" "March 2018" "hledger 1.9" "hledger User Manuals"   
hledger_timeclock.info view
@@ -4,7 +4,7 @@  File: hledger_timeclock.info,  Node: Top,  Up: (dir) -hledger_timeclock(5) hledger 1.5+hledger_timeclock(5) hledger 1.9 ********************************  hledger can read timeclock files.  As with Ledger, these are (a subset
hledger_timeclock.txt view
@@ -77,4 +77,4 @@   -hledger 1.5                      December 2017            hledger_timeclock(5)+hledger 1.9                       March 2018              hledger_timeclock(5)
hledger_timedot.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_timedot" "5" "December 2017" "hledger 1.5" "hledger User Manuals"+.TH "hledger_timedot" "5" "March 2018" "hledger 1.9" "hledger User Manuals"   
hledger_timedot.info view
@@ -4,7 +4,7 @@  File: hledger_timedot.info,  Node: Top,  Next: FILE FORMAT,  Up: (dir) -hledger_timedot(5) hledger 1.5+hledger_timedot(5) hledger 1.9 ******************************  Timedot is a plain text format for logging dated, categorised quantities
hledger_timedot.txt view
@@ -124,4 +124,4 @@   -hledger 1.5                      December 2017              hledger_timedot(5)+hledger 1.9                       March 2018                hledger_timedot(5)
+ tests/easytests.hs view
@@ -0,0 +1,39 @@+#!/usr/bin/env stack exec -- ghcid -Tmain+-- Run tests using project's resolver, whenever ghcid is happy.+--+-- Experimental tests using easytest, an alternative to hunit (eg).+-- https://hackage.haskell.org/package/easytest++{-# LANGUAGE OverloadedStrings #-}++import EasyTest+import Hledger++main :: IO ()+main = do+  run+  -- rerun "journal.standard account types.queries.assets"+  -- rerunOnly 2686786430487349354 "journal.standard account types.queries.assets"+    $ tests [++      scope "journal.standard account types.queries" $+        let+          j = samplejournal+          journalAccountNamesMatching :: Query -> Journal -> [AccountName]+          journalAccountNamesMatching q = filter (q `matchesAccount`) . journalAccountNames+          namesfrom qfunc = journalAccountNamesMatching (qfunc j) j+        in+          tests+            [ scope "assets" $+              expectEq (namesfrom journalAssetAccountQuery)     ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]+            , scope "liabilities" $+              expectEq (namesfrom journalLiabilityAccountQuery) ["liabilities","liabilities:debts"]+            , scope "equity" $+              expectEq (namesfrom journalEquityAccountQuery)    []+            , scope "income" $+              expectEq (namesfrom journalIncomeAccountQuery)    ["income","income:gifts","income:salary"]+            , scope "expenses" $+              expectEq (namesfrom journalExpenseAccountQuery)   ["expenses","expenses:food","expenses:supplies"]+            ]++    ]