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 +30/−3
- Hledger/Data/Account.hs +25/−5
- Hledger/Data/AccountName.hs +9/−5
- Hledger/Data/Amount.hs +45/−39
- Hledger/Data/AutoTransaction.hs +48/−23
- Hledger/Data/Commodity.hs +3/−0
- Hledger/Data/Dates.hs +48/−27
- Hledger/Data/Journal.hs +6/−10
- Hledger/Data/Ledger.hs +1/−1
- Hledger/Data/Period.hs +7/−0
- Hledger/Data/Posting.hs +3/−0
- Hledger/Data/Types.hs +15/−14
- Hledger/Query.hs +4/−1
- Hledger/Read.hs +3/−3
- Hledger/Read/Common.hs +58/−29
- Hledger/Read/CsvReader.hs +17/−17
- Hledger/Read/JournalReader.hs +44/−35
- Hledger/Read/TimeclockReader.hs +3/−3
- Hledger/Read/TimedotReader.hs +3/−3
- Hledger/Reports/BalanceReport.hs +16/−9
- Hledger/Reports/MultiBalanceReports.hs +70/−29
- Hledger/Reports/ReportOptions.hs +30/−18
- Hledger/Utils.hs +19/−21
- Hledger/Utils/String.hs +1/−1
- Hledger/Utils/Text.hs +3/−0
- hledger-lib.cabal +113/−21
- hledger_csv.5 +1/−1
- hledger_csv.info +1/−1
- hledger_csv.txt +1/−1
- hledger_journal.5 +105/−51
- hledger_journal.info +172/−137
- hledger_journal.txt +89/−57
- hledger_timeclock.5 +1/−1
- hledger_timeclock.info +1/−1
- hledger_timeclock.txt +1/−1
- hledger_timedot.5 +1/−1
- hledger_timedot.info +1/−1
- hledger_timedot.txt +1/−1
- tests/easytests.hs +39/−0
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"]+ ]++ ]