diff --git a/CHANGES b/CHANGES
--- a/CHANGES
+++ b/CHANGES
@@ -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)
diff --git a/Hledger/Data/Account.hs b/Hledger/Data/Account.hs
--- a/Hledger/Data/Account.hs
+++ b/Hledger/Data/Account.hs
@@ -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
diff --git a/Hledger/Data/AccountName.hs b/Hledger/Data/AccountName.hs
--- a/Hledger/Data/AccountName.hs
+++ b/Hledger/Data/AccountName.hs
@@ -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
  [
diff --git a/Hledger/Data/Amount.hs b/Hledger/Data/Amount.hs
--- a/Hledger/Data/Amount.hs
+++ b/Hledger/Data/Amount.hs
@@ -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"
 
   ]
diff --git a/Hledger/Data/AutoTransaction.hs b/Hledger/Data/AutoTransaction.hs
--- a/Hledger/Data/AutoTransaction.hs
+++ b/Hledger/Data/AutoTransaction.hs
@@ -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
diff --git a/Hledger/Data/Commodity.hs b/Hledger/Data/Commodity.hs
--- a/Hledger/Data/Commodity.hs
+++ b/Hledger/Data/Commodity.hs
@@ -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
diff --git a/Hledger/Data/Dates.hs b/Hledger/Data/Dates.hs
--- a/Hledger/Data/Dates.hs
+++ b/Hledger/Data/Dates.hs
@@ -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
 
diff --git a/Hledger/Data/Journal.hs b/Hledger/Data/Journal.hs
--- a/Hledger/Data/Journal.hs
+++ b/Hledger/Data/Journal.hs
@@ -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.
diff --git a/Hledger/Data/Ledger.hs b/Hledger/Data/Ledger.hs
--- a/Hledger/Data/Ledger.hs
+++ b/Hledger/Data/Ledger.hs
@@ -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.
diff --git a/Hledger/Data/Period.hs b/Hledger/Data/Period.hs
--- a/Hledger/Data/Period.hs
+++ b/Hledger/Data/Period.hs
@@ -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
diff --git a/Hledger/Data/Posting.hs b/Hledger/Data/Posting.hs
--- a/Hledger/Data/Posting.hs
+++ b/Hledger/Data/Posting.hs
@@ -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
diff --git a/Hledger/Data/Types.hs b/Hledger/Data/Types.hs
--- a/Hledger/Data/Types.hs
+++ b/Hledger/Data/Types.hs
@@ -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
diff --git a/Hledger/Query.hs b/Hledger/Query.hs
--- a/Hledger/Query.hs
+++ b/Hledger/Query.hs
@@ -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:")
diff --git a/Hledger/Read.hs b/Hledger/Read.hs
--- a/Hledger/Read.hs
+++ b/Hledger/Read.hs
@@ -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
diff --git a/Hledger/Read/Common.hs b/Hledger/Read/Common.hs
--- a/Hledger/Read/Common.hs
+++ b/Hledger/Read/Common.hs
@@ -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
diff --git a/Hledger/Read/CsvReader.hs b/Hledger/Read/CsvReader.hs
--- a/Hledger/Read/CsvReader.hs
+++ b/Hledger/Read/CsvReader.hs
@@ -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)
diff --git a/Hledger/Read/JournalReader.hs b/Hledger/Read/JournalReader.hs
--- a/Hledger/Read/JournalReader.hs
+++ b/Hledger/Read/JournalReader.hs
@@ -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
diff --git a/Hledger/Read/TimeclockReader.hs b/Hledger/Read/TimeclockReader.hs
--- a/Hledger/Read/TimeclockReader.hs
+++ b/Hledger/Read/TimeclockReader.hs
@@ -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 [
diff --git a/Hledger/Read/TimedotReader.hs b/Hledger/Read/TimedotReader.hs
--- a/Hledger/Read/TimedotReader.hs
+++ b/Hledger/Read/TimedotReader.hs
@@ -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
diff --git a/Hledger/Reports/BalanceReport.hs b/Hledger/Reports/BalanceReport.hs
--- a/Hledger/Reports/BalanceReport.hs
+++ b/Hledger/Reports/BalanceReport.hs
@@ -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
diff --git a/Hledger/Reports/MultiBalanceReports.hs b/Hledger/Reports/MultiBalanceReports.hs
--- a/Hledger/Reports/MultiBalanceReports.hs
+++ b/Hledger/Reports/MultiBalanceReports.hs
@@ -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 =
diff --git a/Hledger/Reports/ReportOptions.hs b/Hledger/Reports/ReportOptions.hs
--- a/Hledger/Reports/ReportOptions.hs
+++ b/Hledger/Reports/ReportOptions.hs
@@ -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.
diff --git a/Hledger/Utils.hs b/Hledger/Utils.hs
--- a/Hledger/Utils.hs
+++ b/Hledger/Utils.hs
@@ -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
diff --git a/Hledger/Utils/String.hs b/Hledger/Utils/String.hs
--- a/Hledger/Utils/String.hs
+++ b/Hledger/Utils/String.hs
@@ -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)
diff --git a/Hledger/Utils/Text.hs b/Hledger/Utils/Text.hs
--- a/Hledger/Utils/Text.hs
+++ b/Hledger/Utils/Text.hs
@@ -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
diff --git a/hledger-lib.cabal b/hledger-lib.cabal
--- a/hledger-lib.cabal
+++ b/hledger-lib.cabal
@@ -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
diff --git a/hledger_csv.5 b/hledger_csv.5
--- a/hledger_csv.5
+++ b/hledger_csv.5
@@ -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"
 
 
 
diff --git a/hledger_csv.info b/hledger_csv.info
--- a/hledger_csv.info
+++ b/hledger_csv.info
@@ -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
diff --git a/hledger_csv.txt b/hledger_csv.txt
--- a/hledger_csv.txt
+++ b/hledger_csv.txt
@@ -249,4 +249,4 @@
 
 
 
-hledger 1.5                      December 2017                  hledger_csv(5)
+hledger 1.9                       March 2018                    hledger_csv(5)
diff --git a/hledger_journal.5 b/hledger_journal.5
--- a/hledger_journal.5
+++ b/hledger_journal.5
@@ -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
diff --git a/hledger_journal.info b/hledger_journal.info
--- a/hledger_journal.info
+++ b/hledger_journal.info
@@ -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: Top76
-Node: FILE FORMAT2424
-Ref: #file-format2555
-Node: Transactions2778
-Ref: #transactions2899
-Node: Postings3583
-Ref: #postings3710
-Node: Dates4705
-Ref: #dates4820
-Node: Simple dates4885
-Ref: #simple-dates5011
-Node: Secondary dates5377
-Ref: #secondary-dates5531
-Node: Posting dates7094
-Ref: #posting-dates7223
-Node: Status8597
-Ref: #status8717
-Node: Description10425
-Ref: #description10563
-Node: Payee and note10882
-Ref: #payee-and-note10996
-Node: Account names11238
-Ref: #account-names11381
-Node: Amounts11868
-Ref: #amounts12004
-Node: Virtual Postings14684
-Ref: #virtual-postings14843
-Node: Balance Assertions16063
-Ref: #balance-assertions16238
-Node: Assertions and ordering17134
-Ref: #assertions-and-ordering17320
-Node: Assertions and included files18020
-Ref: #assertions-and-included-files18261
-Node: Assertions and multiple -f options18594
-Ref: #assertions-and-multiple--f-options18848
-Node: Assertions and commodities18980
-Ref: #assertions-and-commodities19215
-Node: Assertions and subaccounts19911
-Ref: #assertions-and-subaccounts20143
-Node: Assertions and virtual postings20664
-Ref: #assertions-and-virtual-postings20871
-Node: Balance Assignments21013
-Ref: #balance-assignments21182
-Node: Prices22302
-Ref: #prices22435
-Node: Transaction prices22486
-Ref: #transaction-prices22631
-Node: Market prices24787
-Ref: #market-prices24922
-Node: Comments25882
-Ref: #comments26004
-Node: Tags27246
-Ref: #tags27364
-Node: Directives28766
-Ref: #directives28879
-Node: Account aliases29072
-Ref: #account-aliases29216
-Node: Basic aliases29820
-Ref: #basic-aliases29963
-Node: Regex aliases30653
-Ref: #regex-aliases30821
-Node: Multiple aliases31539
-Ref: #multiple-aliases31711
-Node: end aliases32209
-Ref: #end-aliases32349
-Node: account directive32450
-Ref: #account-directive32630
-Node: apply account directive32926
-Ref: #apply-account-directive33122
-Node: Multi-line comments33781
-Ref: #multi-line-comments33971
-Node: commodity directive34099
-Ref: #commodity-directive34283
-Node: Default commodity35155
-Ref: #default-commodity35328
-Node: Default year35865
-Ref: #default-year36030
-Node: Including other files36453
-Ref: #including-other-files36610
-Node: Periodic transactions37007
-Ref: #periodic-transactions37178
-Node: Automated posting rules37921
-Ref: #automated-posting-rules38099
-Node: EDITOR SUPPORT39208
-Ref: #editor-support39338
+Node: FILE FORMAT2419
+Ref: #file-format2550
+Node: Transactions2773
+Ref: #transactions2894
+Node: Postings3578
+Ref: #postings3705
+Node: Dates4700
+Ref: #dates4815
+Node: Simple dates4880
+Ref: #simple-dates5006
+Node: Secondary dates5372
+Ref: #secondary-dates5526
+Node: Posting dates7089
+Ref: #posting-dates7218
+Node: Status8592
+Ref: #status8712
+Node: Description10420
+Ref: #description10558
+Node: Payee and note10877
+Ref: #payee-and-note10991
+Node: Account names11233
+Ref: #account-names11376
+Node: Amounts11863
+Ref: #amounts11999
+Node: Virtual Postings15014
+Ref: #virtual-postings15173
+Node: Balance Assertions16393
+Ref: #balance-assertions16568
+Node: Assertions and ordering17464
+Ref: #assertions-and-ordering17650
+Node: Assertions and included files18350
+Ref: #assertions-and-included-files18591
+Node: Assertions and multiple -f options18924
+Ref: #assertions-and-multiple--f-options19178
+Node: Assertions and commodities19310
+Ref: #assertions-and-commodities19545
+Node: Assertions and subaccounts20241
+Ref: #assertions-and-subaccounts20473
+Node: Assertions and virtual postings20994
+Ref: #assertions-and-virtual-postings21201
+Node: Balance Assignments21343
+Ref: #balance-assignments21512
+Node: Prices22632
+Ref: #prices22765
+Node: Transaction prices22816
+Ref: #transaction-prices22961
+Node: Market prices25117
+Ref: #market-prices25252
+Node: Comments26212
+Ref: #comments26334
+Node: Tags27576
+Ref: #tags27694
+Node: Directives29096
+Ref: #directives29209
+Node: Account aliases29402
+Ref: #account-aliases29546
+Node: Basic aliases30150
+Ref: #basic-aliases30293
+Node: Regex aliases30983
+Ref: #regex-aliases31151
+Node: Multiple aliases31869
+Ref: #multiple-aliases32041
+Node: end aliases32539
+Ref: #end-aliases32679
+Node: account directive32780
+Ref: #account-directive32960
+Node: apply account directive34307
+Ref: #apply-account-directive34503
+Node: Multi-line comments35162
+Ref: #multi-line-comments35352
+Node: commodity directive35480
+Ref: #commodity-directive35664
+Node: Default commodity36536
+Ref: #default-commodity36709
+Node: Default year37246
+Ref: #default-year37411
+Node: Including other files37834
+Ref: #including-other-files37991
+Node: Periodic transactions38388
+Ref: #periodic-transactions38554
+Node: Automated postings39543
+Ref: #automated-postings39706
+Node: EDITOR SUPPORT40608
+Ref: #editor-support40733
 
 End Tag Table
diff --git a/hledger_journal.txt b/hledger_journal.txt
--- a/hledger_journal.txt
+++ b/hledger_journal.txt
@@ -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)
diff --git a/hledger_timeclock.5 b/hledger_timeclock.5
--- a/hledger_timeclock.5
+++ b/hledger_timeclock.5
@@ -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"
 
 
 
diff --git a/hledger_timeclock.info b/hledger_timeclock.info
--- a/hledger_timeclock.info
+++ b/hledger_timeclock.info
@@ -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
diff --git a/hledger_timeclock.txt b/hledger_timeclock.txt
--- a/hledger_timeclock.txt
+++ b/hledger_timeclock.txt
@@ -77,4 +77,4 @@
 
 
 
-hledger 1.5                      December 2017            hledger_timeclock(5)
+hledger 1.9                       March 2018              hledger_timeclock(5)
diff --git a/hledger_timedot.5 b/hledger_timedot.5
--- a/hledger_timedot.5
+++ b/hledger_timedot.5
@@ -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"
 
 
 
diff --git a/hledger_timedot.info b/hledger_timedot.info
--- a/hledger_timedot.info
+++ b/hledger_timedot.info
@@ -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
diff --git a/hledger_timedot.txt b/hledger_timedot.txt
--- a/hledger_timedot.txt
+++ b/hledger_timedot.txt
@@ -124,4 +124,4 @@
 
 
 
-hledger 1.5                      December 2017              hledger_timedot(5)
+hledger 1.9                       March 2018                hledger_timedot(5)
diff --git a/tests/easytests.hs b/tests/easytests.hs
new file mode 100644
--- /dev/null
+++ b/tests/easytests.hs
@@ -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"]
+            ]
+
+    ]
