diff --git a/CHANGES.md b/CHANGES.md
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,56 @@
 Internal/api/developer-ish changes in the hledger-lib (and hledger) packages.
 For user-visible changes, see the hledger package changelog.
 
+# 1.17 2020-03-01
+
+- Reader-finding utilities have moved from Hledger.Read to
+  Hledger.Read.JournalReader so the include directive can use them.
+
+- Reader changes:
+  - rExperimental flag removed
+  - old rParser renamed to rReadFn
+  - new rParser field provides the actual parser.
+    This seems to require making Reader a higher-kinded type, unfortunately.
+
+- Hledger.Tabular.AsciiWide now renders smoother outer borders in
+  pretty (unicode) mode.
+  Also, a fix for table edges always using single-width intersections
+  and support for double horizontal lines with single vertical lines. (Eric Mertens)
+
+- Hledger.Utils.Parse: restofline can go to eof also
+
+- Hledger.Read cleanup
+
+- Hledger.Read.CsvReader cleanup
+  Exports added: CsvRecord, CsvValue, csvFileFor.
+  Exports removed: expandIncludes, parseAndValidateCsvRules, transactionFromCsvRecord
+
+- more cleanup of amount canonicalisation helpers (#1187)
+  Stop exporting journalAmounts, overJournalAmounts, traverseJournalAmounts.
+  Rename journalAmounts helper to journalStyleInfluencingAmounts.
+
+- export mapMixedAmount
+
+- Don't store leaf name in PeriodReport. (Stephen Morgan)
+  Calculate at the point of consumption instead.
+
+- Generalise PeriodicReport to be polymorphic in the account labels. (Stephen Morgan)
+
+- Use records instead of tuples in PeriodicReport. (Stephen Morgan)
+
+- Use PeriodicReport in place of MultiBalanceReport. (Stephen Morgan)
+
+- Calculate MultiReportBalance columns more efficiently. (Stephen Morgan)
+  Only calculate posting date once for each posting, and calculate their
+  columns instead of checking each DateSpan separately.
+
+- Moved JSON instances from hledger-web to hledger-lib (Hledger.Data.Json),
+  and added ToJSON instances for all (?) remaining data types, up to Ledger.
+
+- Dropped nullassertion's "assertion" alias, fixing a warning.
+  Perhaps we'll stick with the null* naming convention. 
+
+
 # 1.16.2 2020-01-14
 
 - add support for megaparsec 8 (#1175)
@@ -161,7 +211,7 @@
 
 -   showTransaction: fix a case showing multiple missing amounts
     showTransaction could sometimes hide the last posting's amount even if
-    one of the other posting amounts was already implcit, producing invalid
+    one of the other posting amounts was already implicit, producing invalid
     transaction output.
 
 -   plog, plogAt: add missing newline
diff --git a/Hledger/Data.hs b/Hledger/Data.hs
--- a/Hledger/Data.hs
+++ b/Hledger/Data.hs
@@ -15,6 +15,7 @@
                module Hledger.Data.Commodity,
                module Hledger.Data.Dates,
                module Hledger.Data.Journal,
+               module Hledger.Data.Json,
                module Hledger.Data.Ledger,
                module Hledger.Data.Period,
                module Hledger.Data.PeriodicTransaction,
@@ -36,6 +37,7 @@
 import Hledger.Data.Commodity
 import Hledger.Data.Dates
 import Hledger.Data.Journal
+import Hledger.Data.Json
 import Hledger.Data.Ledger
 import Hledger.Data.Period
 import Hledger.Data.PeriodicTransaction
diff --git a/Hledger/Data/AccountName.hs b/Hledger/Data/AccountName.hs
--- a/Hledger/Data/AccountName.hs
+++ b/Hledger/Data/AccountName.hs
@@ -41,6 +41,7 @@
 where
 
 import Data.List
+import Data.List.Extra (nubSort)
 #if !(MIN_VERSION_base(4,11,0))
 import Data.Monoid
 #endif
@@ -110,7 +111,7 @@
 -- ie these plus all their parent accounts up to the root.
 -- Eg: ["a:b:c","d:e"] -> ["a","a:b","a:b:c","d","d:e"]
 expandAccountNames :: [AccountName] -> [AccountName]
-expandAccountNames as = nub $ sort $ concatMap expandAccountName as
+expandAccountNames as = nubSort $ concatMap expandAccountName as
 
 -- | "a:b:c" -> ["a","a:b","a:b:c"]
 expandAccountName :: AccountName -> [AccountName]
diff --git a/Hledger/Data/Amount.hs b/Hledger/Data/Amount.hs
--- a/Hledger/Data/Amount.hs
+++ b/Hledger/Data/Amount.hs
@@ -40,6 +40,10 @@
 
 -}
 
+-- Silence safe 0.3.18's deprecation warnings for (max|min)imum(By)?Def for now
+-- (may hide other deprecation warnings too). https://github.com/ndmitchell/safe/issues/26
+{-# OPTIONS_GHC -Wno-warnings-deprecations #-}
+
 {-# LANGUAGE StandaloneDeriving, RecordWildCards, OverloadedStrings #-}
 
 module Hledger.Data.Amount (
@@ -92,8 +96,10 @@
   amounts,
   filterMixedAmount,
   filterMixedAmountByCommodity,
+  mapMixedAmount,
   normaliseMixedAmountSquashPricesForDisplay,
   normaliseMixedAmount,
+  mixedAmountStripPrices,
   -- ** arithmetic
   costOfMixedAmount,
   mixedAmountToCost,
@@ -291,9 +297,9 @@
 setFullPrecision :: Amount -> Amount
 setFullPrecision a = setAmountPrecision p a
   where
-    p                = max displayprecision normalprecision
+    p                = max displayprecision naturalprecision
     displayprecision = asprecision $ astyle a
-    normalprecision  = fromIntegral $ decimalPlaces $ normalizeDecimal $ aquantity a
+    naturalprecision = fromIntegral $ decimalPlaces $ normalizeDecimal $ aquantity a
 
 -- | Set an amount's display precision to just enough decimal places
 -- to show it exactly (possibly less than the number specified by
diff --git a/Hledger/Data/Dates.hs b/Hledger/Data/Dates.hs
--- a/Hledger/Data/Dates.hs
+++ b/Hledger/Data/Dates.hs
@@ -63,6 +63,8 @@
   spanDefaultsFrom,
   spanUnion,
   spansUnion,
+  daysSpan,
+  latestSpanContaining,
   smartdate,
   splitSpan,
   fixSmartDate,
@@ -79,10 +81,11 @@
 import "base-compat-batteries" Prelude.Compat hiding (fail)
 import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (MonadFail, fail)
 import Control.Applicative.Permutations
-import Control.Monad (unless)
+import Control.Monad (guard, unless)
 import "base-compat-batteries" Data.List.Compat
 import Data.Default
 import Data.Maybe
+import qualified Data.Set as Set
 import Data.Text (Text)
 import qualified Data.Text as T
 #if MIN_VERSION_time(1,5,0)
@@ -95,7 +98,7 @@
 import Data.Time.Calendar.OrdinalDate
 import Data.Time.Clock
 import Data.Time.LocalTime
-import Safe (headMay, lastMay, readMay)
+import Safe (headMay, lastMay, readMay, maximumMay, minimumMay)
 import Text.Megaparsec
 import Text.Megaparsec.Char
 import Text.Megaparsec.Custom
@@ -112,7 +115,7 @@
     -- show s = "DateSpan \"" ++ showDateSpan s ++ "\"" -- quotes to help pretty-show
 
 showDate :: Day -> String
-showDate = formatTime defaultTimeLocale "%0C%y/%m/%d"
+showDate = show
 
 -- | Render a datespan as a display string, abbreviating into a
 -- compact form if possible.
@@ -173,21 +176,21 @@
 -- >>> 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"
--- [DateSpan 2008/01/01-2008/02/29,DateSpan 2008/03/01-2008/04/30]
+-- [DateSpan 2008-01-01-2008-02-29,DateSpan 2008-03-01-2008-04-30]
 -- >>> t (Weeks 1) "2008/01/01" "2008/01/15"
--- [DateSpan 2007/12/31w01,DateSpan 2008/01/07w02,DateSpan 2008/01/14w03]
+-- [DateSpan 2007-12-31w01,DateSpan 2008-01-07w02,DateSpan 2008-01-14w03]
 -- >>> t (Weeks 2) "2008/01/01" "2008/01/15"
--- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]
+-- [DateSpan 2007-12-31-2008-01-13,DateSpan 2008-01-14-2008-01-27]
 -- >>> t (DayOfMonth 2) "2008/01/01" "2008/04/01"
--- [DateSpan 2007/12/02-2008/01/01,DateSpan 2008/01/02-2008/02/01,DateSpan 2008/02/02-2008/03/01,DateSpan 2008/03/02-2008/04/01]
+-- [DateSpan 2007-12-02-2008-01-01,DateSpan 2008-01-02-2008-02-01,DateSpan 2008-02-02-2008-03-01,DateSpan 2008-03-02-2008-04-01]
 -- >>> t (WeekdayOfMonth 2 4) "2011/01/01" "2011/02/15"
--- [DateSpan 2010/12/09-2011/01/12,DateSpan 2011/01/13-2011/02/09,DateSpan 2011/02/10-2011/03/09]
+-- [DateSpan 2010-12-09-2011-01-12,DateSpan 2011-01-13-2011-02-09,DateSpan 2011-02-10-2011-03-09]
 -- >>> t (DayOfWeek 2) "2011/01/01" "2011/01/15"
--- [DateSpan 2010/12/28-2011/01/03,DateSpan 2011/01/04-2011/01/10,DateSpan 2011/01/11-2011/01/17]
+-- [DateSpan 2010-12-28-2011-01-03,DateSpan 2011-01-04-2011-01-10,DateSpan 2011-01-11-2011-01-17]
 -- >>> t (DayOfYear 11 29) "2011/10/01" "2011/10/15"
--- [DateSpan 2010/11/29-2011/11/28]
+-- [DateSpan 2010-11-29-2011-11-28]
 -- >>> t (DayOfYear 11 29) "2011/12/01" "2012/12/15"
--- [DateSpan 2011/11/29-2012/11/28,DateSpan 2012/11/29-2013/11/28]
+-- [DateSpan 2011-11-29-2012-11-28,DateSpan 2012-11-29-2013-11-28]
 --
 splitSpan :: Interval -> DateSpan -> [DateSpan]
 splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]
@@ -231,9 +234,8 @@
 
 -- | 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
+isEmptySpan (DateSpan (Just s) (Just e)) = e <= s
+isEmptySpan _                            = False
 
 -- | Does the span include the given date ?
 spanContainsDate :: DateSpan -> Day -> Bool
@@ -256,7 +258,7 @@
 --
 -- 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
+-- DateSpan 2018-01-03-2018-01-02
 spanIntersect (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e
     where
       b = latest b1 b2
@@ -287,6 +289,36 @@
 earliest Nothing d = d
 earliest (Just d1) (Just d2) = Just $ min d1 d2
 
+-- | Calculate the minimal DateSpan containing all of the given Days (in the
+-- usual exclusive-end-date sense: beginning on the earliest, and ending on
+-- the day after the latest).
+daysSpan :: [Day] -> DateSpan
+daysSpan ds = DateSpan (minimumMay ds) (addDays 1 <$> maximumMay ds)
+
+-- | Select the DateSpan containing a given Day, if any, from a given list of
+-- DateSpans.
+--
+-- If the DateSpans are non-overlapping, this returns the unique containing
+-- DateSpan, if it exists. If the DateSpans are overlapping, it will return the
+-- containing DateSpan with the latest start date, and then latest end date.
+
+-- Note: This will currently return `DateSpan (Just s) (Just e)` before it will
+-- return `DateSpan (Just s) Nothing`. It's unclear which behaviour is desired.
+-- This is irrelevant at the moment as it's never applied to any list with
+-- overlapping DateSpans.
+latestSpanContaining :: [DateSpan] -> Day -> Maybe DateSpan
+latestSpanContaining datespans = go
+  where
+    go day = do
+        span <- Set.lookupLT supSpan spanSet
+        guard $ spanContainsDate span day
+        return span
+      where
+        -- The smallest DateSpan larger than any DateSpan containing day.
+        supSpan = DateSpan (Just $ addDays 1 day) Nothing
+
+    spanSet = Set.fromList $ filter (not . isEmptySpan) datespans
+
 -- | Parse a period expression to an Interval and overall DateSpan using
 -- the provided reference date, or return a parse error.
 parsePeriodExpr
@@ -373,72 +405,72 @@
 -- >>> :set -XOverloadedStrings
 -- >>> let t = fixSmartDateStr (parsedate "2008/11/26")
 -- >>> t "0000-01-01"
--- "0000/01/01"
+-- "0000-01-01"
 -- >>> t "1999-12-02"
--- "1999/12/02"
+-- "1999-12-02"
 -- >>> t "1999.12.02"
--- "1999/12/02"
+-- "1999-12-02"
 -- >>> t "1999/3/2"
--- "1999/03/02"
+-- "1999-03-02"
 -- >>> t "19990302"
--- "1999/03/02"
+-- "1999-03-02"
 -- >>> t "2008/2"
--- "2008/02/01"
+-- "2008-02-01"
 -- >>> t "0020/2"
--- "0020/02/01"
+-- "0020-02-01"
 -- >>> t "1000"
--- "1000/01/01"
+-- "1000-01-01"
 -- >>> t "4/2"
--- "2008/04/02"
+-- "2008-04-02"
 -- >>> t "2"
--- "2008/11/02"
+-- "2008-11-02"
 -- >>> t "January"
--- "2008/01/01"
+-- "2008-01-01"
 -- >>> t "feb"
--- "2008/02/01"
+-- "2008-02-01"
 -- >>> t "today"
--- "2008/11/26"
+-- "2008-11-26"
 -- >>> t "yesterday"
--- "2008/11/25"
+-- "2008-11-25"
 -- >>> t "tomorrow"
--- "2008/11/27"
+-- "2008-11-27"
 -- >>> t "this day"
--- "2008/11/26"
+-- "2008-11-26"
 -- >>> t "last day"
--- "2008/11/25"
+-- "2008-11-25"
 -- >>> t "next day"
--- "2008/11/27"
+-- "2008-11-27"
 -- >>> t "this week"  -- last monday
--- "2008/11/24"
+-- "2008-11-24"
 -- >>> t "last week"  -- previous monday
--- "2008/11/17"
+-- "2008-11-17"
 -- >>> t "next week"  -- next monday
--- "2008/12/01"
+-- "2008-12-01"
 -- >>> t "this month"
--- "2008/11/01"
+-- "2008-11-01"
 -- >>> t "last month"
--- "2008/10/01"
+-- "2008-10-01"
 -- >>> t "next month"
--- "2008/12/01"
+-- "2008-12-01"
 -- >>> t "this quarter"
--- "2008/10/01"
+-- "2008-10-01"
 -- >>> t "last quarter"
--- "2008/07/01"
+-- "2008-07-01"
 -- >>> t "next quarter"
--- "2009/01/01"
+-- "2009-01-01"
 -- >>> t "this year"
--- "2008/01/01"
+-- "2008-01-01"
 -- >>> t "last year"
--- "2007/01/01"
+-- "2007-01-01"
 -- >>> t "next year"
--- "2009/01/01"
+-- "2009-01-01"
 --
 -- t "last wed"
--- "2008/11/19"
+-- "2008-11-19"
 -- t "next friday"
--- "2008/11/28"
+-- "2008-11-28"
 -- t "next january"
--- "2009/01/01"
+-- "2009-01-01"
 --
 fixSmartDate :: Day -> SmartDate -> Day
 fixSmartDate refdate = fix
@@ -636,11 +668,14 @@
 #endif
 
 
--- | Parse a couple of date string formats to a time type.
+-- | Try to parse a couple of date string formats:
+-- `YYYY-MM-DD`, `YYYY/MM/DD` or `YYYY.MM.DD`, with leading zeros required.
+-- For internal use, not quite the same as the journal's "simple dates".
 parsedateM :: String -> Maybe Day
 parsedateM s = firstJust [
      parsetime defaultTimeLocale "%Y/%m/%d" s,
-     parsetime defaultTimeLocale "%Y-%m-%d" s
+     parsetime defaultTimeLocale "%Y-%m-%d" s,
+     parsetime defaultTimeLocale "%Y.%m.%d" s
      ]
 
 
@@ -649,7 +684,7 @@
 -- parsedatetime s = fromMaybe (error' $ "could not parse timestamp \"" ++ s ++ "\"")
 --                             (parsedatetimeM s)
 
--- | Parse a YYYY-MM-DD or YYYY/MM/DD date string to a Day, or raise an error. For testing/debugging.
+-- | Like parsedateM, raising an error on parse failure.
 --
 -- >>> parsedate "2008/02/03"
 -- 2008-02-03
@@ -743,7 +778,7 @@
 datesepchar = satisfy isDateSepChar
 
 isDateSepChar :: Char -> Bool
-isDateSepChar c = c == '/' || c == '-' || c == '.'
+isDateSepChar c = c == '-' || c == '/' || c == '.'
 
 validYear, validMonth, validDay :: String -> Bool
 validYear s = length s >= 4 && isJust (readMay s :: Maybe Year)
@@ -878,35 +913,35 @@
 --
 -- >>> let p = parsePeriodExpr (parsedate "2008/11/26")
 -- >>> p "from Aug to Oct"
--- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
+-- Right (NoInterval,DateSpan 2008-08-01-2008-09-30)
 -- >>> p "aug to oct"
--- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
+-- Right (NoInterval,DateSpan 2008-08-01-2008-09-30)
 -- >>> p "every 3 days in Aug"
 -- Right (Days 3,DateSpan 2008/08)
 -- >>> p "daily from aug"
--- Right (Days 1,DateSpan 2008/08/01-)
+-- Right (Days 1,DateSpan 2008-08-01-)
 -- >>> p "every week to 2009"
--- Right (Weeks 1,DateSpan -2008/12/31)
+-- Right (Weeks 1,DateSpan -2008-12-31)
 -- >>> p "every 2nd day of month"
 -- Right (DayOfMonth 2,DateSpan -)
 -- >>> p "every 2nd day"
 -- Right (DayOfMonth 2,DateSpan -)
 -- >>> p "every 2nd day 2009-"
--- Right (DayOfMonth 2,DateSpan 2009/01/01-)
+-- Right (DayOfMonth 2,DateSpan 2009-01-01-)
 -- >>> p "every 29th Nov"
 -- Right (DayOfYear 11 29,DateSpan -)
 -- >>> p "every 29th nov -2009"
--- Right (DayOfYear 11 29,DateSpan -2008/12/31)
+-- Right (DayOfYear 11 29,DateSpan -2008-12-31)
 -- >>> p "every nov 29th"
 -- Right (DayOfYear 11 29,DateSpan -)
 -- >>> p "every Nov 29th 2009-"
--- Right (DayOfYear 11 29,DateSpan 2009/01/01-)
+-- Right (DayOfYear 11 29,DateSpan 2009-01-01-)
 -- >>> p "every 11/29 from 2009"
--- Right (DayOfYear 11 29,DateSpan 2009/01/01-)
+-- Right (DayOfYear 11 29,DateSpan 2009-01-01-)
 -- >>> p "every 2nd Thursday of month to 2009"
--- Right (WeekdayOfMonth 2 4,DateSpan -2008/12/31)
+-- Right (WeekdayOfMonth 2 4,DateSpan -2008-12-31)
 -- >>> p "every 1st monday of month to 2009"
--- Right (WeekdayOfMonth 1 1,DateSpan -2008/12/31)
+-- Right (WeekdayOfMonth 1 1,DateSpan -2008-12-31)
 -- >>> p "every tue"
 -- Right (DayOfWeek 2,DateSpan -)
 -- >>> p "every 2nd day of week"
@@ -916,9 +951,9 @@
 -- >>> p "every 2nd day"
 -- Right (DayOfMonth 2,DateSpan -)
 -- >>> p "every 2nd day 2009-"
--- Right (DayOfMonth 2,DateSpan 2009/01/01-)
+-- Right (DayOfMonth 2,DateSpan 2009-01-01-)
 -- >>> p "every 2nd day of month 2009-"
--- Right (DayOfMonth 2,DateSpan 2009/01/01-)
+-- Right (DayOfMonth 2,DateSpan 2009-01-01-)
 periodexprp :: Day -> TextParser m (Interval, DateSpan)
 periodexprp rdate = do
   skipMany spacenonewline
@@ -1030,7 +1065,7 @@
 
 -- |
 -- -- >>> parsewith (doubledatespan (parsedate "2018/01/01") <* eof) "20180101-201804"
--- Right DateSpan 2018/01/01-2018/04/01
+-- Right DateSpan 2018-01-01-2018-04-01
 doubledatespanp :: Day -> TextParser m DateSpan
 doubledatespanp rdate = do
   optional (string' "from" >> skipMany spacenonewline)
diff --git a/Hledger/Data/Journal.hs b/Hledger/Data/Journal.hs
--- a/Hledger/Data/Journal.hs
+++ b/Hledger/Data/Journal.hs
@@ -47,9 +47,9 @@
   journalAccountNamesDeclaredOrImplied,
   journalAccountNames,
   -- journalAmountAndPriceCommodities,
-  journalAmounts,
-  overJournalAmounts,
-  traverseJournalAmounts,
+  -- journalAmountStyles,
+  -- overJournalAmounts,
+  -- traverseJournalAmounts,
   -- journalCanonicalCommodities,
   journalDateSpan,
   journalStartDate,
@@ -84,7 +84,6 @@
   tests_Journal,
 )
 where
-import Control.Applicative (Const(..))
 import Control.Monad
 import Control.Monad.Except
 import Control.Monad.Extra
@@ -92,10 +91,9 @@
 import Control.Monad.ST
 import Data.Array.ST
 import Data.Function ((&))
-import Data.Functor.Identity (Identity(..))
 import qualified Data.HashTable.ST.Cuckoo as H
 import Data.List
-import Data.List.Extra (groupSort)
+import Data.List.Extra (groupSort, nubSort)
 import qualified Data.Map as M
 import Data.Maybe
 #if !(MIN_VERSION_base(4,11,0))
@@ -258,7 +256,7 @@
 
 -- | Unique transaction descriptions used in this journal.
 journalDescriptions :: Journal -> [Text]
-journalDescriptions = nub . sort . map tdescription . jtxns
+journalDescriptions = nubSort . map tdescription . jtxns
 
 -- | All postings from this journal's transactions, in order.
 journalPostings :: Journal -> [Posting]
@@ -275,17 +273,17 @@
 
 -- | Sorted unique account names declared by account directives in this journal.
 journalAccountNamesDeclared :: Journal -> [AccountName]
-journalAccountNamesDeclared = nub . sort . map fst . jdeclaredaccounts
+journalAccountNamesDeclared = nubSort . map fst . jdeclaredaccounts
 
 -- | Sorted unique account names declared by account directives or posted to
 -- by transactions in this journal.
 journalAccountNamesDeclaredOrUsed :: Journal -> [AccountName]
-journalAccountNamesDeclaredOrUsed j = nub $ sort $ journalAccountNamesDeclared j ++ journalAccountNamesUsed j
+journalAccountNamesDeclaredOrUsed j = nubSort $ journalAccountNamesDeclared j ++ journalAccountNamesUsed j
 
 -- | Sorted unique account names declared by account directives, or posted to
 -- or implied as parents by transactions in this journal.
 journalAccountNamesDeclaredOrImplied :: Journal -> [AccountName]
-journalAccountNamesDeclaredOrImplied j = nub $ sort $ journalAccountNamesDeclared j ++ journalAccountNamesImplied j
+journalAccountNamesDeclaredOrImplied j = nubSort $ journalAccountNamesDeclared j ++ journalAccountNamesImplied j
 
 -- | Convenience/compatibility alias for journalAccountNamesDeclaredOrImplied.
 journalAccountNames :: Journal -> [AccountName]
@@ -902,11 +900,11 @@
 
 --
 
--- | Choose and apply a consistent display format to the posting
--- amounts in each commodity. Each commodity's format is specified by
--- a commodity format directive, or otherwise inferred from posting
--- amounts as in hledger < 0.28. Can return an error message
--- eg if inconsistent number formats are found.
+-- | Choose and apply a consistent display style to the posting
+-- amounts in each commodity. Each commodity's style is specified by a
+-- commodity (or D) directive, or otherwise inferred from posting
+-- amounts. Can return an error message eg if inconsistent number
+-- formats are found.
 journalApplyCommodityStyles :: Journal -> Either String Journal
 journalApplyCommodityStyles j@Journal{jtxns=ts, jpricedirectives=pds} =
   case journalInferCommodityStyles j of
@@ -923,15 +921,20 @@
         fixbalanceassertion ba = ba{baamount=styleAmount styles $ baamount ba}
         fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a}
 
--- | Get all the amount styles defined in this journal, either declared by
--- a commodity directive or inferred from amounts, as a map from symbol to style.
--- Styles declared by commodity directives take precedence, and these also are
--- guaranteed to know their decimal point character.
+-- | Get the canonical amount styles for this journal, whether
+-- declared by commodity directives, by the last default commodity (D)
+-- directive, or inferred from posting amounts, as a map from symbol
+-- to style. Styles declared by directives take precedence (and
+-- commodity takes precedence over D). Styles from directives are
+-- guaranteed to specify the decimal mark character.
 journalCommodityStyles :: Journal -> M.Map CommoditySymbol AmountStyle
-journalCommodityStyles j = declaredstyles <> inferredstyles
+journalCommodityStyles j =
+  -- XXX could be some redundancy here, cf journalStyleInfluencingAmounts
+  commoditystyles <> defaultcommoditystyle <> inferredstyles
   where
-    declaredstyles = M.mapMaybe cformat $ jcommodities j
-    inferredstyles = jinferredcommodities j
+    commoditystyles       = M.mapMaybe cformat $ jcommodities j
+    defaultcommoditystyle = M.fromList $ catMaybes [jparsedefaultcommodity j]
+    inferredstyles        = jinferredcommodities j
 
 -- | Collect and save inferred amount styles for each commodity based on
 -- the posting amounts in that commodity (excluding price amounts), ie:
@@ -942,15 +945,15 @@
   case
     commodityStylesFromAmounts $
     dbg8 "journalInferCommodityStyles using amounts" $
-    journalAmounts j
+    journalStyleInfluencingAmounts j
   of
     Left e   -> Left e
     Right cs -> Right j{jinferredcommodities = cs}
 
--- | Given a list of parsed amounts, in parse order, build a map from
--- their commodity names to standard commodity display formats. Can
--- return an error message eg if inconsistent number formats are
--- found.
+-- | Given a list of amounts, in parse order (roughly speaking; see journalStyleInfluencingAmounts),
+-- build a map from their commodity names to standard commodity
+-- display formats. Can return an error message eg if inconsistent
+-- number formats are found.
 --
 -- Though, these amounts may have come from multiple files, so we
 -- shouldn't assume they use consistent number formats.
@@ -1037,39 +1040,69 @@
 --               Just (UnitPrice ma)  -> c:(concatMap amountCommodities $ amounts ma)
 --               Just (TotalPrice ma) -> c:(concatMap amountCommodities $ amounts ma)
 
--- | Get an ordered list of the amounts in this journal which will
--- influence amount style canonicalisation. These are:
+-- | Get an ordered list of amounts in this journal which can
+-- influence canonical amount display styles. Those amounts are, in
+-- the following order:
 --
--- * amounts in market price directives (in parse order)
--- * amounts in postings (in parse order)
+-- * amounts in market price (P) directives (in parse order)
+-- * posting amounts in transactions (in parse order)
+-- * the amount in the final default commodity (D) directive
 --
--- Amounts in default commodity directives also influence
--- canonicalisation, but earlier, as amounts are parsed.
--- Amounts in posting prices are not used for canonicalisation.
+-- Transaction price amounts (posting amounts' aprice field) are not included.
 --
-journalAmounts :: Journal -> [Amount]
-journalAmounts = getConst . traverseJournalAmounts (Const . (:[]))
-
--- | Maps over all of the amounts in the journal
-overJournalAmounts :: (Amount -> Amount) -> Journal -> Journal
-overJournalAmounts f = runIdentity . traverseJournalAmounts (Identity . f)
-
--- | Traverses over all of the amounts in the journal, in the order
--- indicated by 'journalAmounts'.
-traverseJournalAmounts
-    :: Applicative f
-    => (Amount -> f Amount)
-    -> Journal -> f Journal
-traverseJournalAmounts f j =
-    recombine <$> (traverse . mpa) f (jpricedirectives j)
-              <*> (traverse . tp . traverse . pamt . maa . traverse) f (jtxns j)
+journalStyleInfluencingAmounts :: Journal -> [Amount]
+journalStyleInfluencingAmounts j = catMaybes $ concat [
+   [mdefaultcommodityamt]
+  ,map (Just . pdamount) $ jpricedirectives j
+  ,map Just $ concatMap amounts $ map pamount $ journalPostings j
+  ]
   where
-    recombine mps txns = j { jpricedirectives = mps, jtxns = txns }
-    -- a bunch of traversals
-    mpa  g pd = (\amt -> pd { pdamount  = amt }) <$> g (pdamount pd)
-    tp   g t  = (\ps  -> t  { tpostings = ps  }) <$> g (tpostings t)
-    pamt g p  = (\amt -> p  { pamount   = amt }) <$> g (pamount p)
-    maa  g (Mixed as) = Mixed <$> g as
+    -- D's amount style isn't actually stored as an amount, make it into one
+    mdefaultcommodityamt =
+      case jparsedefaultcommodity j of
+        Just (symbol,style) -> Just nullamt{acommodity=symbol,astyle=style}
+        Nothing -> Nothing
+
+-- overcomplicated/unused amount traversal stuff
+--
+-- | Get an ordered list of 'AmountStyle's from the amounts in this
+-- journal which influence canonical amount display styles. See
+-- traverseJournalAmounts.
+-- journalAmounts :: Journal -> [Amount]
+-- journalAmounts = getConst . traverseJournalAmounts (Const . (:[]))
+--
+-- | Apply a transformation to the journal amounts traversed by traverseJournalAmounts.
+-- overJournalAmounts :: (Amount -> Amount) -> Journal -> Journal
+-- overJournalAmounts f = runIdentity . traverseJournalAmounts (Identity . f)
+--
+-- | A helper that traverses over most amounts in the journal,
+-- in particular the ones which influence canonical amount display styles,
+-- processing them with the given applicative function.
+--
+-- These include, in the following order:
+--
+-- * the amount in the final default commodity (D) directive
+-- * amounts in market price (P) directives (in parse order)
+-- * posting amounts in transactions (in parse order)
+--
+-- Transaction price amounts, which may be embedded in posting amounts
+-- (the aprice field), are left intact but not traversed/processed.
+--
+-- traverseJournalAmounts :: Applicative f => (Amount -> f Amount) -> Journal -> f Journal
+-- traverseJournalAmounts f j =
+--   recombine <$> (traverse . dcamt) f (jparsedefaultcommodity j)
+--             <*> (traverse . pdamt) f (jpricedirectives j)
+--             <*> (traverse . tps . traverse . pamt . amts . traverse) f (jtxns j)
+--   where
+--     recombine pds txns = j { jpricedirectives = pds, jtxns = txns }
+--     -- a bunch of traversals
+--     dcamt g pd         = (\mdc -> case mdc of Nothing -> Nothing
+--                                               Just ((c,stpd{pdamount =amt}
+--                          ) <$> g (pdamount pd)
+--     pdamt g pd         = (\amt -> pd{pdamount =amt}) <$> g (pdamount pd)
+--     tps   g t          = (\ps  -> t {tpostings=ps }) <$> g (tpostings t)
+--     pamt  g p          = (\amt -> p {pamount  =amt}) <$> g (pamount p)
+--     amts  g (Mixed as) = Mixed <$> g as
 
 -- | The fully specified date span enclosing the dates (primary or secondary)
 -- of all this journal's transactions and postings, or DateSpan Nothing Nothing
diff --git a/Hledger/Data/Json.hs b/Hledger/Data/Json.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Data/Json.hs
@@ -0,0 +1,217 @@
+{-
+JSON instances. Should they be in Types.hs ?
+-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+--{-# LANGUAGE CPP                 #-}
+--{-# LANGUAGE DataKinds           #-}
+--{-# LANGUAGE DeriveAnyClass      #-}
+{-# LANGUAGE DeriveGeneric       #-}
+--{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances   #-}
+--{-# LANGUAGE NamedFieldPuns #-}
+--{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE OverloadedStrings #-}
+--{-# LANGUAGE PolyKinds           #-}
+--{-# LANGUAGE QuasiQuotes         #-}
+--{-# LANGUAGE QuasiQuotes #-}
+--{-# LANGUAGE Rank2Types #-}
+--{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+--{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+--{-# LANGUAGE TemplateHaskell       #-}
+--{-# LANGUAGE TypeFamilies        #-}
+--{-# LANGUAGE TypeOperators       #-}
+
+module Hledger.Data.Json (
+  -- * Instances
+  -- * Utilities
+   readJsonFile
+  ,writeJsonFile
+) where
+
+import           Data.Aeson
+--import           Data.Aeson.TH
+import qualified Data.ByteString.Lazy as BL
+import           Data.Decimal
+import           Data.Maybe
+import           GHC.Generics (Generic)
+import           System.Time (ClockTime)
+
+import           Hledger.Data.Types
+
+-- To JSON
+
+instance ToJSON Status
+instance ToJSON GenericSourcePos
+
+-- https://github.com/simonmichael/hledger/issues/1195
+
+-- The default JSON output for Decimal can contain 255-digit integers
+-- (for repeating decimals caused by implicit transaction prices).
+-- JSON output is intended to be consumed by diverse apps and
+-- programming languages, which can't handle numbers like that.
+-- From #1195:
+--
+-- > - JavaScript uses 64-bit IEEE754 numbers which can only accurately
+-- >   represent integers up to 9007199254740991 (i.e. a maximum of 15 digits).
+-- > - Java’s largest integers are limited to 18 digits.
+-- > - Python 3 integers are unbounded.
+-- > - Python 2 integers are limited to 18 digits like Java.
+-- > - C and C++ number limits depend on platform — most platforms should
+-- >   be able to represent unsigned integers up to 64 bits, i.e. 19 digits.
+--
+-- What is the best compromise for both accuracy and practicality ?
+-- For now, we provide both the maximum precision representation
+-- (decimalPlaces & decimalMantissa), and a floating point representation
+-- with up to 10 decimal places (and an unbounded number of integer digits).
+-- We hope the mere presence of the large number in JSON won't break things,
+-- and that the overall number of significant digits in the floating point
+-- remains manageable in practice. (I'm not sure how to limit the number
+-- of significant digits in a Decimal right now.)
+instance ToJSON Decimal where
+  toJSON d = object
+    ["decimalPlaces"   .= toJSON decimalPlaces
+    ,"decimalMantissa" .= toJSON decimalMantissa
+    ,"floatingPoint"   .= toJSON (fromRational $ toRational d' :: Double)
+    ]
+    where d'@Decimal{..} = roundTo 10 d
+
+instance ToJSON Amount
+instance ToJSON AmountStyle
+instance ToJSON Side
+instance ToJSON DigitGroupStyle
+instance ToJSON MixedAmount
+instance ToJSON BalanceAssertion
+instance ToJSON AmountPrice
+instance ToJSON MarketPrice
+instance ToJSON PostingType
+
+instance ToJSON Posting where
+  toJSON Posting{..} = object
+    ["pdate"             .= pdate
+    ,"pdate2"            .= pdate2
+    ,"pstatus"           .= pstatus
+    ,"paccount"          .= paccount
+    ,"pamount"           .= pamount
+    ,"pcomment"          .= pcomment
+    ,"ptype"             .= ptype
+    ,"ptags"             .= ptags
+    ,"pbalanceassertion" .= pbalanceassertion
+    -- To avoid a cycle, show just the parent transaction's index number
+    -- in a dummy field. When re-parsed, there will be no parent.
+    ,"ptransaction_"     .= maybe "" (show.tindex) ptransaction
+    -- This is probably not wanted in json, we discard it.
+    ,"poriginal"         .= (Nothing :: Maybe Posting)
+    ]
+
+instance ToJSON Transaction
+instance ToJSON TransactionModifier
+instance ToJSON PeriodicTransaction
+instance ToJSON PriceDirective
+instance ToJSON DateSpan
+instance ToJSON Interval
+instance ToJSON AccountAlias
+instance ToJSON AccountType
+instance ToJSONKey AccountType
+instance ToJSON AccountDeclarationInfo
+instance ToJSON Commodity
+instance ToJSON TimeclockCode
+instance ToJSON TimeclockEntry
+instance ToJSON ClockTime
+instance ToJSON Journal
+
+instance ToJSON Account where
+  toJSON a = object
+    ["aname"        .= aname a
+    ,"aebalance"    .= aebalance a
+    ,"aibalance"    .= aibalance a
+    ,"anumpostings" .= anumpostings a
+    ,"aboring"      .= aboring a
+    -- To avoid a cycle, show just the parent account's name
+    -- in a dummy field. When re-parsed, there will be no parent.
+    ,"aparent_"     .= maybe "" aname (aparent a)
+    -- Just the names of subaccounts, as a dummy field, ignored when parsed.
+    ,"asubs_"       .= map aname (asubs a)
+    -- The actual subaccounts (and their subs..), making a (probably highly redundant) tree
+    -- ,"asubs"        .= asubs a
+    -- Omit the actual subaccounts
+    ,"asubs"        .= ([]::[Account])
+    ]
+
+deriving instance Generic (Ledger)
+instance ToJSON Ledger
+
+-- From JSON
+
+instance FromJSON Status
+instance FromJSON GenericSourcePos
+instance FromJSON Amount
+instance FromJSON AmountStyle
+instance FromJSON Side
+instance FromJSON DigitGroupStyle
+instance FromJSON MixedAmount
+instance FromJSON BalanceAssertion
+instance FromJSON AmountPrice
+instance FromJSON MarketPrice
+instance FromJSON PostingType
+instance FromJSON Posting
+instance FromJSON Transaction
+instance FromJSON AccountDeclarationInfo
+-- XXX The ToJSON instance replaces subaccounts with just names.
+-- Here we should try to make use of those to reconstruct the
+-- parent-child relationships.
+instance FromJSON Account
+
+-- Decimal, various attempts
+--
+-- https://stackoverflow.com/questions/40331851/haskell-data-decimal-as-aeson-type
+----instance FromJSON Decimal where parseJSON =
+----  A.withScientific "Decimal" (return . right . eitherFromRational . toRational)
+--
+-- https://github.com/bos/aeson/issues/474
+-- http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson-TH.html
+-- $(deriveFromJSON defaultOptions ''Decimal) -- doesn't work
+-- $(deriveFromJSON defaultOptions ''DecimalRaw)  -- works; requires TH, but gives better parse error messages
+--
+-- https://github.com/PaulJohnson/Haskell-Decimal/issues/6
+--deriving instance Generic Decimal
+--instance FromJSON Decimal
+deriving instance Generic (DecimalRaw a)
+instance FromJSON (DecimalRaw Integer)
+--
+-- @simonmichael, I think the code in your first comment should work if it compiles—though “work” doesn’t mean you can parse a JSON number directly into a `Decimal` using the generic instance, as you’ve discovered.
+--
+--Error messages with these extensions are always rather cryptic, but I’d prefer them to Template Haskell. Typically you’ll want to start by getting a generic `ToJSON` instance working, then use that to figure out what the `FromJSON` instance expects to parse: for a correct instance, `encode` and `decode` should give you an isomorphism between your type and a subset of `Bytestring` (up to the `Maybe` wrapper that `decode` returns).
+--
+--I don’t have time to test it right now, but I think it will also work without `DeriveAnyClass`, just using `DeriveGeneric` and `StandAloneDeriving`. It should also work to use the [`genericParseJSON`](http://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#v:genericParseJSON) function to implement the class explicitly, something like this:
+--
+--{-# LANGUAGE DeriveGeneric #-}
+--{-# LANGUAGE StandAloneDeriving #-}
+--import GHC.Generics
+--import Data.Aeson
+--deriving instance Generic Decimal
+--instance FromJSON Decimal where
+--  parseJSON = genericParseJSON defaultOptions
+--
+--And of course you can avoid `StandAloneDeriving` entirely if you’re willing to wrap `Decimal` in your own `newtype`.
+
+
+-- Utilities
+
+-- | Read a json from a file and decode/parse it as the target type, if we can.
+-- Example: >>> readJsonFile "in.json" :: IO MixedAmount
+readJsonFile :: FromJSON a => FilePath -> IO a
+readJsonFile f = do
+  bs <- BL.readFile f
+  let v = fromMaybe (error "could not decode bytestring as json value") (decode bs :: Maybe Value)
+  case fromJSON v :: FromJSON a => Result a of
+    Error e   -> error e
+    Success t -> return t
+
+-- | Write some to-JSON-convertible haskell value to a json file, if we can.
+-- Example: >>> writeJsonFile "out.json" nullmixedamt
+writeJsonFile :: ToJSON a => FilePath -> a -> IO ()
+writeJsonFile f v = BL.writeFile f (encode v)
diff --git a/Hledger/Data/Ledger.hs b/Hledger/Data/Ledger.hs
--- a/Hledger/Data/Ledger.hs
+++ b/Hledger/Data/Ledger.hs
@@ -34,8 +34,9 @@
 import Hledger.Utils.Test
 import Hledger.Data.Types
 import Hledger.Data.Account
+import Hledger.Data.Dates (daysSpan)
 import Hledger.Data.Journal
-import Hledger.Data.Posting
+import Hledger.Data.Posting (postingDate)
 import Hledger.Query
 
 
@@ -100,7 +101,7 @@
 -- | The (fully specified) date span containing all the ledger's (filtered) transactions,
 -- or DateSpan Nothing Nothing if there are none.
 ledgerDateSpan :: Ledger -> DateSpan
-ledgerDateSpan = postingsDateSpan . ledgerPostings
+ledgerDateSpan = daysSpan . map postingDate . ledgerPostings
 
 -- | All commodities used in this ledger.
 ledgerCommodities :: Ledger -> [CommoditySymbol]
diff --git a/Hledger/Data/Period.hs b/Hledger/Data/Period.hs
--- a/Hledger/Data/Period.hs
+++ b/Hledger/Data/Period.hs
@@ -154,16 +154,16 @@
 -- | Render a period as a compact display string suitable for user output.
 --
 -- >>> showPeriod (WeekPeriod (fromGregorian 2016 7 25))
--- "2016/07/25w30"
-showPeriod (DayPeriod b)       = formatTime defaultTimeLocale "%0C%y/%m/%d" b     -- DATE
-showPeriod (WeekPeriod b)      = formatTime defaultTimeLocale "%0C%y/%m/%dw%V" b  -- STARTDATEwYEARWEEK
+-- "2016-07-25w30"
+showPeriod (DayPeriod b)       = formatTime defaultTimeLocale "%F" b              -- DATE
+showPeriod (WeekPeriod b)      = formatTime defaultTimeLocale "%Fw%V" b           -- STARTDATEwYEARWEEK
 showPeriod (MonthPeriod y m)   = printf "%04d/%02d" y m                           -- YYYY/MM
 showPeriod (QuarterPeriod y q) = printf "%04dq%d" y q                             -- YYYYqN
 showPeriod (YearPeriod y)      = printf "%04d" y                                  -- YYYY
-showPeriod (PeriodBetween b e) = formatTime defaultTimeLocale "%0C%y/%m/%d" b
-                                 ++ formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e) -- STARTDATE-INCLUSIVEENDDATE
-showPeriod (PeriodFrom b)      = formatTime defaultTimeLocale "%0C%y/%m/%d-" b                   -- STARTDATE-
-showPeriod (PeriodTo e)        = formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e)    -- -INCLUSIVEENDDATE
+showPeriod (PeriodBetween b e) = formatTime defaultTimeLocale "%F" b
+                                 ++ formatTime defaultTimeLocale "-%F" (addDays (-1) e) -- STARTDATE-INCLUSIVEENDDATE
+showPeriod (PeriodFrom b)      = formatTime defaultTimeLocale "%F-" b                   -- STARTDATE-
+showPeriod (PeriodTo e)        = formatTime defaultTimeLocale "-%F" (addDays (-1) e)    -- -INCLUSIVEENDDATE
 showPeriod PeriodAll           = "-"
 
 -- | Like showPeriod, but if it's a month period show just
diff --git a/Hledger/Data/PeriodicTransaction.hs b/Hledger/Data/PeriodicTransaction.hs
--- a/Hledger/Data/PeriodicTransaction.hs
+++ b/Hledger/Data/PeriodicTransaction.hs
@@ -86,107 +86,107 @@
 -- - a hidden _generated-transaction: tag which does not appear in the comment. 
 --
 -- >>> _ptgen "monthly from 2017/1 to 2017/4"
--- 2017/01/01
+-- 2017-01-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/02/01
+-- 2017-02-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/03/01
+-- 2017-03-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
 --
 -- >>> _ptgen "monthly from 2017/1 to 2017/5"
--- 2017/01/01
+-- 2017-01-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/02/01
+-- 2017-02-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/03/01
+-- 2017-03-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/04/01
+-- 2017-04-01
 --     ; generated-transaction: ~ monthly from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
 --
 -- >>> _ptgen "every 2nd day of month from 2017/02 to 2017/04"
--- 2017/01/02
+-- 2017-01-02
 --     ; generated-transaction: ~ every 2nd day of month from 2017/02 to 2017/04
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/02/02
+-- 2017-02-02
 --     ; generated-transaction: ~ every 2nd day of month from 2017/02 to 2017/04
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/03/02
+-- 2017-03-02
 --     ; generated-transaction: ~ every 2nd day of month from 2017/02 to 2017/04
 --     a           $1.00
 -- <BLANKLINE>
 --
 -- >>> _ptgen "every 30th day of month from 2017/1 to 2017/5"
--- 2016/12/30
+-- 2016-12-30
 --     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/01/30
+-- 2017-01-30
 --     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/02/28
+-- 2017-02-28
 --     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/03/30
+-- 2017-03-30
 --     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/04/30
+-- 2017-04-30
 --     ; generated-transaction: ~ every 30th day of month from 2017/1 to 2017/5
 --     a           $1.00
 -- <BLANKLINE>
 --
 -- >>> _ptgen "every 2nd Thursday of month from 2017/1 to 2017/4"
--- 2016/12/08
+-- 2016-12-08
 --     ; generated-transaction: ~ every 2nd Thursday of month from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/01/12
+-- 2017-01-12
 --     ; generated-transaction: ~ every 2nd Thursday of month from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/02/09
+-- 2017-02-09
 --     ; generated-transaction: ~ every 2nd Thursday of month from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/03/09
+-- 2017-03-09
 --     ; generated-transaction: ~ every 2nd Thursday of month from 2017/1 to 2017/4
 --     a           $1.00
 -- <BLANKLINE>
 --
 -- >>> _ptgen "every nov 29th from 2017 to 2019"
--- 2016/11/29
+-- 2016-11-29
 --     ; generated-transaction: ~ every nov 29th from 2017 to 2019
 --     a           $1.00
 -- <BLANKLINE>
--- 2017/11/29
+-- 2017-11-29
 --     ; generated-transaction: ~ every nov 29th from 2017 to 2019
 --     a           $1.00
 -- <BLANKLINE>
--- 2018/11/29
+-- 2018-11-29
 --     ; generated-transaction: ~ every nov 29th from 2017 to 2019
 --     a           $1.00
 -- <BLANKLINE>
 --
 -- >>> _ptgen "2017/1"
--- 2017/01/01
+-- 2017-01-01
 --     ; generated-transaction: ~ 2017/1
 --     a           $1.00
 -- <BLANKLINE>
@@ -213,21 +213,21 @@
 -- >>> _ptgenspan "every 3 months from 2019-05" (mkdatespan "2020-01-01" "2020-02-01")
 --  
 -- >>> _ptgenspan "every 3 months from 2019-05" (mkdatespan "2020-02-01" "2020-03-01")
--- 2020/02/01
+-- 2020-02-01
 --     ; generated-transaction: ~ every 3 months from 2019-05
 --     a           $1.00
 -- <BLANKLINE>
 -- >>> _ptgenspan "every 3 days from 2018" (mkdatespan "2018-01-01" "2018-01-05")
--- 2018/01/01
+-- 2018-01-01
 --     ; generated-transaction: ~ every 3 days from 2018
 --     a           $1.00
 -- <BLANKLINE>
--- 2018/01/04
+-- 2018-01-04
 --     ; generated-transaction: ~ every 3 days from 2018
 --     a           $1.00
 -- <BLANKLINE>
 -- >>> _ptgenspan "every 3 days from 2018" (mkdatespan "2018-01-02" "2018-01-05")
--- 2018/01/04
+-- 2018-01-04
 --     ; generated-transaction: ~ every 3 days from 2018
 --     a           $1.00
 -- <BLANKLINE>
diff --git a/Hledger/Data/Posting.hs b/Hledger/Data/Posting.hs
--- a/Hledger/Data/Posting.hs
+++ b/Hledger/Data/Posting.hs
@@ -20,7 +20,6 @@
   vpost',
   nullsourcepos,
   nullassertion,
-  assertion,
   balassert,
   balassertTot,
   balassertParInc,
@@ -43,8 +42,6 @@
   postingDate2,
   isPostingInDateSpan,
   isPostingInDateSpan',
-  postingsDateSpan,
-  postingsDateSpan',
   -- * account name operations
   accountNamesFromPostings,
   accountNamePostingType,
@@ -70,7 +67,9 @@
   tests_Posting
 )
 where
-import Data.List
+
+import Data.Foldable (asum)
+import Data.List.Extra (nubSort)
 import qualified Data.Map as M
 import Data.Maybe
 import Data.MemoUgly (memo)
@@ -128,14 +127,13 @@
 nullsourcepos :: GenericSourcePos
 nullsourcepos = JournalSourcePos "" (1,1)
 
-nullassertion, assertion :: BalanceAssertion
+nullassertion :: BalanceAssertion
 nullassertion = BalanceAssertion
                   {baamount=nullamt
                   ,batotal=False
                   ,bainclusive=False
                   ,baposition=nullsourcepos
                   }
-assertion = nullassertion
 
 -- | Make a partial, exclusive balance assertion.
 balassert :: Amount -> Maybe BalanceAssertion
@@ -192,7 +190,7 @@
 
 -- | Sorted unique account names referenced by these postings.
 accountNamesFromPostings :: [Posting] -> [AccountName]
-accountNamesFromPostings = nub . sort . map paccount
+accountNamesFromPostings = nubSort . map paccount
 
 sumPostings :: [Posting] -> MixedAmount
 sumPostings = sumStrict . map pamount
@@ -206,20 +204,19 @@
 -- otherwise the parent transaction's primary date, or the null date if
 -- there is no parent transaction.
 postingDate :: Posting -> Day
-postingDate p = fromMaybe txndate $ pdate p
-    where
-      txndate = maybe nulldate tdate $ ptransaction p
+postingDate p = fromMaybe nulldate $ asum dates
+    where dates = [ pdate p, tdate <$> ptransaction p ]
 
 -- | Get a posting's secondary (secondary) date, which is the first of:
 -- posting's secondary date, transaction's secondary date, posting's
 -- primary date, transaction's primary date, or the null date if there is
 -- no parent transaction.
 postingDate2 :: Posting -> Day
-postingDate2 p = headDef nulldate $ catMaybes dates
-  where dates = [pdate2 p
-                ,maybe Nothing tdate2 $ ptransaction p
-                ,pdate p
-                ,fmap tdate (ptransaction p)
+postingDate2 p = fromMaybe nulldate $ asum dates
+  where dates = [ pdate2 p
+                , tdate2 =<< ptransaction p
+                , pdate p
+                , tdate <$> ptransaction p
                 ]
 
 -- | Get a posting's status. This is cleared or pending if those are
@@ -248,7 +245,7 @@
 
 -- | Does this posting fall within the given date span ?
 isPostingInDateSpan :: DateSpan -> Posting -> Bool
-isPostingInDateSpan s = spanContainsDate s . postingDate
+isPostingInDateSpan = isPostingInDateSpan' PrimaryDate
 
 -- --date2-sensitive version, separate for now to avoid disturbing multiBalanceReport.
 isPostingInDateSpan' :: WhichDate -> DateSpan -> Posting -> Bool
@@ -258,21 +255,6 @@
 isEmptyPosting :: Posting -> Bool
 isEmptyPosting = isZeroMixedAmount . pamount
 
--- | Get the minimal date span which contains all the postings, or the
--- null date span if there are none.
-postingsDateSpan :: [Posting] -> DateSpan
-postingsDateSpan [] = DateSpan Nothing Nothing
-postingsDateSpan ps = DateSpan (Just $ postingDate $ head ps') (Just $ addDays 1 $ postingDate $ last ps')
-    where ps' = sortOn postingDate ps
-
--- --date2-sensitive version, as above.
-postingsDateSpan' :: WhichDate -> [Posting] -> DateSpan
-postingsDateSpan' _  [] = DateSpan Nothing Nothing
-postingsDateSpan' wd ps = DateSpan (Just $ postingdate $ head ps') (Just $ addDays 1 $ postingdate $ last ps')
-    where
-      ps' = sortOn postingdate ps
-      postingdate = if wd == PrimaryDate then postingDate else postingDate2
-
 -- AccountName stuff that depends on PostingType
 
 accountNamePostingType :: AccountName -> PostingType
@@ -340,6 +322,7 @@
   case v of
     AtCost    Nothing            -> postingToCost styles p
     AtCost    mc                 -> postingValueAtDate priceoracle styles mc periodlast $ postingToCost styles p
+    AtThen    mc                 -> postingValueAtDate priceoracle styles mc (postingDate p) p
     AtEnd     mc                 -> postingValueAtDate priceoracle styles mc periodlast p
     AtNow     mc                 -> postingValueAtDate priceoracle styles mc today p
     AtDefault mc | ismultiperiod -> postingValueAtDate priceoracle styles mc periodlast p
diff --git a/Hledger/Data/Timeclock.hs b/Hledger/Data/Timeclock.hs
--- a/Hledger/Data/Timeclock.hs
+++ b/Hledger/Data/Timeclock.hs
@@ -57,6 +57,7 @@
 timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]
 timeclockEntriesToTransactions _ [] = []
 timeclockEntriesToTransactions now [i]
+    | tlcode i /= In = errorExpectedCodeButGot In i
     | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now [i',o]
     | otherwise = [entryFromTimeclockInOut i o]
     where
@@ -67,6 +68,8 @@
       o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}
       i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}
 timeclockEntriesToTransactions now (i:o:rest)
+    | tlcode i /= In = errorExpectedCodeButGot In i
+    | tlcode o /= Out =errorExpectedCodeButGot Out o
     | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now (i':o:rest)
     | otherwise = entryFromTimeclockInOut i o : timeclockEntriesToTransactions now rest
     where
@@ -75,6 +78,14 @@
       o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}
       i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}
 {- HLINT ignore timeclockEntriesToTransactions -}
+
+errorExpectedCodeButGot expected actual = errorWithSourceLine line $ "expected timeclock code " ++ (show expected) ++ " but got " ++ show (tlcode actual)
+    where
+        line = case tlsourcepos actual of
+                  GenericSourcePos _ l _ -> l
+                  JournalSourcePos _ (l, _) -> l
+
+errorWithSourceLine line msg = error $ "line " ++ show line ++ ": " ++ msg
 
 -- | Convert a timeclock clockin and clockout entry to an equivalent journal
 -- transaction, representing the time expenditure. Note this entry is  not balanced,
diff --git a/Hledger/Data/Transaction.hs b/Hledger/Data/Transaction.hs
--- a/Hledger/Data/Transaction.hs
+++ b/Hledger/Data/Transaction.hs
@@ -127,12 +127,12 @@
     (p, n) = T.span (/= '|') t
 
 {-|
-Render a journal transaction as text in the style of Ledger's print command.
+Render a journal transaction as text similar to the style of Ledger's print command.
 
-Ledger 2.x's standard format looks like this:
+Adapted from Ledger 2.x and 3.x standard format:
 
 @
-yyyy/mm/dd[ *][ CODE] description.........          [  ; comment...............]
+yyyy-mm-dd[ *][ CODE] description.........          [  ; comment...............]
     account name 1.....................  ...$amount1[  ; comment...............]
     account name 2.....................  ..$-amount1[  ; comment...............]
 
@@ -162,6 +162,7 @@
 showTransactionOneLineAmounts = showTransactionHelper True
 
 -- | Deprecated alias for 'showTransactionOneLineAmounts'
+showTransactionUnelidedOneLineAmounts :: Transaction -> String
 showTransactionUnelidedOneLineAmounts = showTransactionOneLineAmounts  -- TODO: drop it
 
 -- | Helper for showTransaction*.
@@ -241,7 +242,7 @@
     ++ newlinecomments
     | postingblock <- postingblocks]
   where
-    postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, "  ", amount, assertion, samelinecomment] | amount <- shownAmounts]
+    postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, "  ", amt, assertion, samelinecomment] | amt <- shownAmounts]
     assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p
     statusandaccount = lineIndent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
         where
@@ -267,6 +268,7 @@
                                               c:cs -> (c,cs)
 
 -- | Render a balance assertion, as the =[=][*] symbol and expected amount.
+showBalanceAssertion :: BalanceAssertion -> [Char]
 showBalanceAssertion BalanceAssertion{..} =
   "=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount
 
@@ -388,9 +390,9 @@
 
   where
     nonzerobalanceerror :: Transaction -> String
-    nonzerobalanceerror t = printf "could not balance this transaction (%s%s%s)" rmsg sep bvmsg
+    nonzerobalanceerror tt = printf "could not balance this transaction (%s%s%s)" rmsg sep bvmsg
         where
-          (rsum, _, bvsum) = transactionPostingBalances t
+          (rsum, _, bvsum) = transactionPostingBalances tt
           rmsg | isReallyZeroMixedAmountCost rsum = ""
                | otherwise = "real postings are off by "
                  ++ showMixedAmount (costOfMixedAmount rsum)
@@ -558,6 +560,7 @@
 
 -- tests
 
+tests_Transaction :: TestTree
 tests_Transaction =
   tests "Transaction" [
 
@@ -635,7 +638,7 @@
            Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` usd 1]}
          
     , tests "showTransaction" [
-          test "null transaction" $ showTransaction nulltransaction @?= "0000/01/01\n\n"
+          test "null transaction" $ showTransaction nulltransaction @?= "0000-01-01\n\n"
         , test "non-null transaction" $ showTransaction
             nulltransaction
               { tdate = parsedate "2012/05/14"
@@ -657,7 +660,7 @@
                   ]
               } @?=
           unlines
-            [ "2012/05/14=2012/05/15 (code) desc  ; tcomment1"
+            [ "2012-05-14=2012-05-15 (code) desc  ; tcomment1"
             , "    ; tcomment2"
             , "    * a         $1.00"
             , "    ; pcomment2"
@@ -683,7 +686,7 @@
                    ]
             in showTransaction t) @?=
           (unlines
-             [ "2007/01/28 coopportunity"
+             [ "2007-01-28 coopportunity"
              , "    expenses:food:groceries          $47.18"
              , "    assets:checking                 $-47.18"
              , ""
@@ -706,7 +709,7 @@
                 , posting {paccount = "assets:checking", pamount = Mixed [usd (-47.19)]}
                 ])) @?=
           (unlines
-             [ "2007/01/28 coopportunity"
+             [ "2007-01-28 coopportunity"
              , "    expenses:food:groceries          $47.18"
              , "    assets:checking                 $-47.19"
              , ""
@@ -726,7 +729,7 @@
                 ""
                 []
                 [posting {paccount = "expenses:food:groceries", pamount = missingmixedamt}])) @?=
-          (unlines ["2007/01/28 coopportunity", "    expenses:food:groceries", ""])
+          (unlines ["2007-01-28 coopportunity", "    expenses:food:groceries", ""])
         , test "show a transaction with a priced commodityless amount" $
           (showTransaction
              (txnTieKnot $
@@ -744,7 +747,7 @@
                 [ posting {paccount = "a", pamount = Mixed [num 1 `at` (usd 2 `withPrecision` 0)]}
                 , posting {paccount = "b", pamount = missingmixedamt}
                 ])) @?=
-          (unlines ["2010/01/01 x", "    a          1 @ $2", "    b", ""])
+          (unlines ["2010-01-01 x", "    a          1 @ $2", "    b", ""])
         ]
     , tests "balanceTransaction" [
          test "detect unbalanced entry, sign error" $
diff --git a/Hledger/Data/TransactionModifier.hs b/Hledger/Data/TransactionModifier.hs
--- a/Hledger/Data/TransactionModifier.hs
+++ b/Hledger/Data/TransactionModifier.hs
@@ -55,16 +55,16 @@
 -- way (ie, 'txnTieKnot' is called).
 --
 -- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
--- 0000/01/01
+-- 0000-01-01
 --     ping           $1.00
 --     pong           $2.00  ; generated-posting: =
 -- <BLANKLINE>
 -- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "miss" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
--- 0000/01/01
+-- 0000-01-01
 --     ping           $1.00
 -- <BLANKLINE>
 -- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "ping" ["pong" `post` amount{aismultiplier=True, aquantity=3}]) nulltransaction{tpostings=["ping" `post` usd 2]}
--- 0000/01/01
+-- 0000-01-01
 --     ping           $2.00
 --     pong           $6.00  ; generated-posting: = ping
 -- <BLANKLINE>
@@ -88,7 +88,9 @@
 -- >>> tmParseQuery (TransactionModifier "date:2016" []) undefined
 -- Date (DateSpan 2016)
 -- >>> tmParseQuery (TransactionModifier "date:today" []) (read "2017-01-01")
--- Date (DateSpan 2017/01/01)
+-- Date (DateSpan 2017-01-01)
+-- >>> tmParseQuery (TransactionModifier "date:today" []) (read "2017-01-01")
+-- Date (DateSpan 2017-01-01)
 tmParseQuery :: TransactionModifier -> (Day -> Query)
 tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt)
 
diff --git a/Hledger/Data/Valuation.hs b/Hledger/Data/Valuation.hs
--- a/Hledger/Data/Valuation.hs
+++ b/Hledger/Data/Valuation.hs
@@ -15,6 +15,7 @@
    ValuationType(..)
   ,PriceOracle
   ,journalPriceOracle
+  ,unsupportedValueThenError
   -- ,amountApplyValuation
   -- ,amountValueAtDate
   ,mixedAmountApplyValuation
@@ -79,12 +80,13 @@
 type PriceOracle = (Day, CommoditySymbol, Maybe CommoditySymbol) -> Maybe (CommoditySymbol, Quantity)
 
 -- | What kind of value conversion should be done on amounts ?
--- UI: --value=cost|end|now|DATE[,COMM]
+-- CLI: --value=cost|then|end|now|DATE[,COMM]
 data ValuationType =
     AtCost     (Maybe CommoditySymbol)  -- ^ convert to cost commodity using transaction prices, then optionally to given commodity using market prices at posting date
-  | AtEnd      (Maybe CommoditySymbol)  -- ^ convert to default valuation commodity or given commodity, using market prices at period end(s)
-  | AtNow      (Maybe CommoditySymbol)  -- ^ convert to default valuation commodity or given commodity, using current market prices
-  | AtDate Day (Maybe CommoditySymbol)  -- ^ convert to default valuation commodity or given commodity, using market prices on some date
+  | AtThen     (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using market prices at each posting's date
+  | AtEnd      (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using market prices at period end(s)
+  | AtNow      (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using current market prices
+  | AtDate Day (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using market prices on some date
   | AtDefault  (Maybe CommoditySymbol)  -- ^ works like AtNow in single period reports, like AtEnd in multiperiod reports
   deriving (Show,Data,Eq) -- Typeable
 
@@ -124,6 +126,9 @@
 -- - the provided "today" date - (--value=now, or -V/X with no report
 --   end date).
 -- 
+-- Note --value=then is not supported by this function, and will cause an error;
+-- use postingApplyValuation for that.
+-- 
 -- This is all a bit complicated. See the reference doc at
 -- https://hledger.org/hledger.html#effect-of-value-on-reports
 -- (hledger_options.m4.md "Effect of --value on reports"), and #1083.
@@ -133,11 +138,17 @@
   case v of
     AtCost    Nothing            -> amountToCost styles a
     AtCost    mc                 -> amountValueAtDate priceoracle styles mc periodlast $ amountToCost styles a
+    AtThen    _mc                -> error' unsupportedValueThenError  -- TODO
+                                 -- amountValueAtDate priceoracle styles mc periodlast a  -- posting date unknown, handle like AtEnd
     AtEnd     mc                 -> amountValueAtDate priceoracle styles mc periodlast a
     AtNow     mc                 -> amountValueAtDate priceoracle styles mc today a
     AtDefault mc | ismultiperiod -> amountValueAtDate priceoracle styles mc periodlast a
     AtDefault mc                 -> amountValueAtDate priceoracle styles mc (fromMaybe today mreportlast) a
     AtDate d  mc                 -> amountValueAtDate priceoracle styles mc d a
+
+-- | Standard error message for a report not supporting --value=then.
+unsupportedValueThenError :: String
+unsupportedValueThenError = "Sorry, --value=then is not yet implemented for this kind of report."
 
 -- | Find the market value of each component amount in the given
 -- commodity, or its default valuation commodity, at the given
diff --git a/Hledger/Query.hs b/Hledger/Query.hs
--- a/Hledger/Query.hs
+++ b/Hledger/Query.hs
@@ -5,6 +5,10 @@
 
 -}
 
+-- Silence safe 0.3.18's deprecation warnings for (max|min)imum(By)?Def for now
+-- (may hide other deprecation warnings too). https://github.com/ndmitchell/safe/issues/26
+{-# OPTIONS_GHC -Wno-warnings-deprecations #-}
+
 {-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ViewPatterns #-}
 {-# LANGUAGE CPP #-}
 
@@ -59,7 +63,7 @@
 #endif
 import qualified Data.Text as T
 import Data.Time.Calendar
-import Safe (readDef, headDef)
+import Safe (readDef, maximumByDef, maximumDef, minimumDef)
 import Text.Megaparsec
 import Text.Megaparsec.Char
 
@@ -167,6 +171,12 @@
 -- 2. multiple description patterns are OR'd together
 -- 3. multiple status patterns are OR'd together
 -- 4. then all terms are AND'd together
+--
+-- >>> parseQuery nulldate "expenses:dining out"
+-- (Or ([Acct "expenses:dining",Acct "out"]),[])
+-- >>> parseQuery nulldate "\"expenses:dining out\""
+-- (Acct "expenses:dining out",[])
+--
 parseQuery :: Day -> T.Text -> (Query,[QueryOpt])
 parseQuery d s = (q, opts)
   where
@@ -475,28 +485,26 @@
 queryDateSpan' (Date2 span) = span
 queryDateSpan' _            = nulldatespan
 
--- | What is the earliest of these dates, where Nothing is latest ?
+-- | What is the earliest of these dates, where Nothing is earliest ?
 earliestMaybeDate :: [Maybe Day] -> Maybe Day
-earliestMaybeDate mds = head $ sortBy compareMaybeDates mds ++ [Nothing]
+earliestMaybeDate = minimumDef Nothing
 
 -- | What is the latest of these dates, where Nothing is earliest ?
 latestMaybeDate :: [Maybe Day] -> Maybe Day
-latestMaybeDate = headDef Nothing . sortBy (flip compareMaybeDates)
+latestMaybeDate = maximumDef Nothing
 
--- | What is the earliest of these dates, ignoring Nothings ?
+-- | What is the earliest of these dates, where Nothing is the latest ?
 earliestMaybeDate' :: [Maybe Day] -> Maybe Day
-earliestMaybeDate' = headDef Nothing . sortBy compareMaybeDates . filter isJust
+earliestMaybeDate' = minimumDef Nothing . filter isJust
 
--- | What is the latest of these dates, ignoring Nothings ?
+-- | What is the latest of these dates, where Nothing is the latest ?
 latestMaybeDate' :: [Maybe Day] -> Maybe Day
-latestMaybeDate' = headDef Nothing . sortBy (flip compareMaybeDates) . filter isJust
-
--- | Compare two maybe dates, Nothing is earliest.
-compareMaybeDates :: Maybe Day -> Maybe Day -> Ordering
-compareMaybeDates Nothing Nothing = EQ
-compareMaybeDates Nothing (Just _) = LT
-compareMaybeDates (Just _) Nothing = GT
-compareMaybeDates (Just a) (Just b) = compare a b
+latestMaybeDate' = maximumByDef Nothing compareNothingMax
+  where
+    compareNothingMax Nothing  Nothing  = EQ
+    compareNothingMax (Just _) Nothing  = LT
+    compareNothingMax Nothing  (Just _) = GT
+    compareNothingMax (Just a) (Just b) = compare a b
 
 -- | The depth limit this query specifies, or a large number if none.
 queryDepth :: Query -> Int
@@ -718,6 +726,22 @@
      parseAmountQueryTerm "<=+0.23"   @?= (LtEq,0.23)
      parseAmountQueryTerm "-0.23"     @?= (Eq,(-0.23))
     -- ,test "number beginning with decimal mark" $ parseAmountQueryTerm "=.23" @?= (AbsEq,0.23)  -- XXX
+
+  ,test "queryStartDate" $ do
+     let small = Just $ fromGregorian 2000 01 01
+         big   = Just $ fromGregorian 2000 01 02
+     queryStartDate False (And [Date $ DateSpan small Nothing, Date $ DateSpan big Nothing])     @?= big
+     queryStartDate False (And [Date $ DateSpan small Nothing, Date $ DateSpan Nothing Nothing]) @?= small
+     queryStartDate False (Or  [Date $ DateSpan small Nothing, Date $ DateSpan big Nothing])     @?= small
+     queryStartDate False (Or  [Date $ DateSpan small Nothing, Date $ DateSpan Nothing Nothing]) @?= Nothing
+
+  ,test "queryEndDate" $ do
+     let small = Just $ fromGregorian 2000 01 01
+         big   = Just $ fromGregorian 2000 01 02
+     queryEndDate False (And [Date $ DateSpan Nothing small, Date $ DateSpan Nothing big])     @?= small
+     queryEndDate False (And [Date $ DateSpan Nothing small, Date $ DateSpan Nothing Nothing]) @?= small
+     queryEndDate False (Or  [Date $ DateSpan Nothing small, Date $ DateSpan Nothing big])     @?= big
+     queryEndDate False (Or  [Date $ DateSpan Nothing small, Date $ DateSpan Nothing Nothing]) @?= Nothing
 
   ,test "matchesAccount" $ do
      assertBool "" $ (Acct "b:c") `matchesAccount` "a:bb:c:d"
diff --git a/Hledger/Read.hs b/Hledger/Read.hs
--- a/Hledger/Read.hs
+++ b/Hledger/Read.hs
@@ -1,14 +1,25 @@
+-- * -*- eval: (orgstruct-mode 1); orgstruct-heading-prefix-regexp:"-- "; -*-
+-- ** doc
+-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
 {-|
 
 This is the entry point to hledger's reading system, which can read
 Journals from various data formats. Use this module if you want to parse
 journal data or read journal files. Generally it should not be necessary
 to import modules below this one.
+
 -}
 
-{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}
+-- ** language
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE PackageImports #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
+-- ** doctest setup
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+-- ** exports
 module Hledger.Read (
 
   -- * Journal files
@@ -19,7 +30,6 @@
   readJournalFile,
   requireJournalFileExists,
   ensureJournalFileExists,
-  splitReaderPrefix,
 
   -- * Journal parsing
   readJournal,
@@ -28,6 +38,8 @@
   -- * Re-exported
   JournalReader.accountaliasp,
   JournalReader.postingp,
+  findReader,
+  splitReaderPrefix,
   module Hledger.Read.Common,
 
   -- * Tests
@@ -35,6 +47,7 @@
 
 ) where
 
+-- ** imports
 import Control.Arrow (right)
 import qualified Control.Exception as C
 import Control.Monad (when)
@@ -58,35 +71,47 @@
 import Hledger.Data.Dates (getCurrentDay, parsedate, showDate)
 import Hledger.Data.Types
 import Hledger.Read.Common
-import Hledger.Read.JournalReader   as JournalReader
--- import qualified Hledger.Read.LedgerReader    as LedgerReader
-import qualified Hledger.Read.TimedotReader   as TimedotReader
-import qualified Hledger.Read.TimeclockReader as TimeclockReader
-import Hledger.Read.CsvReader as CsvReader
+import Hledger.Read.JournalReader as JournalReader
+import Hledger.Read.CsvReader (tests_CsvReader)
+-- import Hledger.Read.TimedotReader (tests_TimedotReader)
+-- import Hledger.Read.TimeclockReader (tests_TimeclockReader)
 import Hledger.Utils
 import Prelude hiding (getContents, writeFile)
 
+-- ** environment
 
 journalEnvVar           = "LEDGER_FILE"
 journalEnvVar2          = "LEDGER"
 journalDefaultFilename  = ".hledger.journal"
 
--- The available journal readers, each one handling a particular data format.
-readers :: [Reader]
-readers = [
-  JournalReader.reader
- ,TimeclockReader.reader
- ,TimedotReader.reader
- ,CsvReader.reader
---  ,LedgerReader.reader
- ]
+-- ** journal reading
 
-readerNames :: [String]
-readerNames = map rFormat readers
+-- | Read a Journal from the given text, assuming journal format; or
+-- throw an error.
+readJournal' :: Text -> IO Journal
+readJournal' t = readJournal def Nothing t >>= either error' return
 
--- | A file path optionally prefixed by a reader name and colon
--- (journal:, csv:, timedot:, etc.).
-type PrefixedFilePath = FilePath
+-- | @readJournal iopts mfile txt@
+--
+-- Read a Journal from some text, or return an error message.
+--
+-- The reader (data format) is chosen based on, in this order:
+--
+-- - a reader name provided in @iopts@
+--
+-- - a reader prefix in the @mfile@ path
+--
+-- - a file extension in @mfile@
+--
+-- If none of these is available, or if the reader name is unrecognised,
+-- we use the journal reader. (We used to try all readers in this case;
+-- since hledger 1.17, we prefer predictability.)
+readJournal :: InputOpts -> Maybe FilePath -> Text -> IO (Either String Journal)
+readJournal iopts mpath txt = do
+  let r :: Reader IO =
+        fromMaybe JournalReader.reader $ findReader (mformat_ iopts) mpath
+  dbg1IO "trying reader" (rFormat r)
+  (runExceptT . (rReadFn r) iopts (fromMaybe "(string)" mpath)) txt
 
 -- | Read the default journal file specified by the environment, or raise an error.
 defaultJournal :: IO Journal
@@ -112,74 +137,9 @@
                   home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")
                   return $ home </> journalDefaultFilename
 
--- | If a filepath is prefixed by one of the reader names and a colon,
--- split that off. Eg "csv:-" -> (Just "csv", "-").
-splitReaderPrefix :: PrefixedFilePath -> (Maybe String, FilePath)
-splitReaderPrefix f =
-  headDef (Nothing, f)
-  [(Just r, drop (length r + 1) f) | r <- readerNames, (r++":") `isPrefixOf` f]
-
--- | If the specified journal file does not exist (and is not "-"),
--- give a helpful error and quit.
-requireJournalFileExists :: FilePath -> IO ()
-requireJournalFileExists "-" = return ()
-requireJournalFileExists f = do
-  exists <- doesFileExist f
-  when (not exists) $ do  -- XXX might not be a journal file
-    hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f
-    hPrintf stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"
-    hPrintf stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n"
-    exitFailure
-
--- | Ensure there is a journal file at the given path, creating an empty one if needed.
--- On Windows, also ensure that the path contains no trailing dots
--- which could cause data loss (see 'isWindowsUnsafeDotPath').
-ensureJournalFileExists :: FilePath -> IO ()
-ensureJournalFileExists f = do
-  when (os/="mingw32" && isWindowsUnsafeDotPath f) $ do
-    hPrintf stderr "Part of file path %s\n ends with a dot, which is unsafe on Windows; please use a different path.\n" (show f)
-    exitFailure
-  exists <- doesFileExist f
-  when (not exists) $ do
-    hPrintf stderr "Creating hledger journal file %s.\n" f
-    -- note Hledger.Utils.UTF8.* do no line ending conversion on windows,
-    -- we currently require unix line endings on all platforms.
-    newJournalContent >>= writeFile f
-
--- | Does any part of this path contain non-. characters and end with a . ?
--- Such paths are not safe to use on Windows (cf #1056).
-isWindowsUnsafeDotPath :: FilePath -> Bool
-isWindowsUnsafeDotPath =
-  not . null .
-  filter (not . all (=='.')) .
-  filter ((=='.').last) .
-  splitDirectories
-
--- | Give the content for a new auto-created journal file.
-newJournalContent :: IO String
-newJournalContent = do
-  d <- getCurrentDay
-  return $ printf "; journal created %s by hledger\n" (show d)
-
--- | Read a Journal from the given text trying all readers in turn, or throw an error.
-readJournal' :: Text -> IO Journal
-readJournal' t = readJournal def Nothing t >>= either error' return
-
--- | @findReader mformat mpath@
---
--- Find the reader named by @mformat@, if provided.
--- Or, if a file path is provided, find the first reader that handles
--- its file extension, if any.
-findReader :: Maybe StorageFormat -> Maybe FilePath -> Maybe Reader
-findReader Nothing Nothing     = Nothing
-findReader (Just fmt) _        = headMay [r | r <- readers, rFormat r == fmt]
-findReader Nothing (Just path) =
-  case prefix of
-    Just fmt -> headMay [r | r <- readers, rFormat r == fmt]
-    Nothing  -> headMay [r | r <- readers, ext `elem` rExtensions r]
-  where
-    (prefix,path') = splitReaderPrefix path
-    ext            = drop 1 $ takeExtension path'
+-- | A file path optionally prefixed by a reader name and colon
+-- (journal:, csv:, timedot:, etc.).
+type PrefixedFilePath = FilePath
 
 -- | Read a Journal from each specified file path and combine them into one.
 -- Or, return the first error message.
@@ -204,7 +164,7 @@
 -- the @mformat_@ specified in the input options, if any;
 -- the file path's READER: prefix, if any;
 -- a recognised file name extension.
--- if none of these identify a known reader, all built-in readers are tried in turn.
+-- if none of these identify a known reader, the journal reader is used.
 --
 -- The input options can also configure balance assertion checking, automated posting
 -- generation, a rules file for converting CSV data, etc.
@@ -215,6 +175,7 @@
     iopts' = iopts{mformat_=firstJust [mfmt, mformat_ iopts]}
   requireJournalFileExists f
   t <- readFileOrStdinPortably f
+    -- <- T.readFile f  -- or without line ending translation, for testing
   ej <- readJournal iopts' (Just f) t
   case ej of
     Left e  -> return $ Left e
@@ -225,6 +186,50 @@
       return $ Right newj
     Right j -> return $ Right j
 
+-- ** utilities
+
+-- | If the specified journal file does not exist (and is not "-"),
+-- give a helpful error and quit.
+requireJournalFileExists :: FilePath -> IO ()
+requireJournalFileExists "-" = return ()
+requireJournalFileExists f = do
+  exists <- doesFileExist f
+  when (not exists) $ do  -- XXX might not be a journal file
+    hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f
+    hPrintf stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"
+    hPrintf stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n"
+    exitFailure
+
+-- | Ensure there is a journal file at the given path, creating an empty one if needed.
+-- On Windows, also ensure that the path contains no trailing dots
+-- which could cause data loss (see 'isWindowsUnsafeDotPath').
+ensureJournalFileExists :: FilePath -> IO ()
+ensureJournalFileExists f = do
+  when (os/="mingw32" && isWindowsUnsafeDotPath f) $ do
+    hPrintf stderr "Part of file path %s\n ends with a dot, which is unsafe on Windows; please use a different path.\n" (show f)
+    exitFailure
+  exists <- doesFileExist f
+  when (not exists) $ do
+    hPrintf stderr "Creating hledger journal file %s.\n" f
+    -- note Hledger.Utils.UTF8.* do no line ending conversion on windows,
+    -- we currently require unix line endings on all platforms.
+    newJournalContent >>= writeFile f
+
+-- | Does any part of this path contain non-. characters and end with a . ?
+-- Such paths are not safe to use on Windows (cf #1056).
+isWindowsUnsafeDotPath :: FilePath -> Bool
+isWindowsUnsafeDotPath =
+  not . null .
+  filter (not . all (=='.')) .
+  filter ((=='.').last) .
+  splitDirectories
+
+-- | Give the content for a new auto-created journal file.
+newJournalContent :: IO String
+newJournalContent = do
+  d <- getCurrentDay
+  return $ printf "; journal created %s by hledger\n" (show d)
+
 -- A "LatestDates" is zero or more copies of the same date,
 -- representing the latest transaction date read from a file,
 -- and how many transactions there were on that date.
@@ -280,84 +285,10 @@
     j'                    = j{jtxns=newsamedatets++laterts}
     ds'                   = latestDates $ map tdate $ samedatets++laterts
 
--- | @readJournal iopts mfile txt@
---
--- Read a Journal from some text, or return an error message.
---
--- The reader (data format) is chosen based on a recognised file name extension in @mfile@ (if provided).
--- If it does not identify a known reader, all built-in readers are tried in turn
--- (returning the first one's error message if none of them succeed).
---
--- Input ioptions (@iopts@) specify CSV conversion rules file to help convert CSV data,
--- enable or disable balance assertion checking and automated posting generation.
---
-readJournal :: InputOpts -> Maybe FilePath -> Text -> IO (Either String Journal)
-readJournal iopts mfile txt =
-  tryReaders iopts mfile specifiedorallreaders txt
-  where
-    specifiedorallreaders = maybe stablereaders (:[]) $ findReader (mformat_ iopts) mfile
-    stablereaders = filter (not.rExperimental) readers
-
--- | @tryReaders iopts readers path t@
---
--- Try to parse the given text to a Journal using each reader in turn,
--- returning the first success, or if all of them fail, the first error message.
---
--- Input ioptions (@iopts@) specify CSV conversion rules file to help convert CSV data,
--- enable or disable balance assertion checking and automated posting generation.
---
-tryReaders :: InputOpts -> Maybe FilePath -> [Reader] -> Text -> IO (Either String Journal)
-tryReaders iopts mpath readers txt = firstSuccessOrFirstError [] readers
-  where
-    -- TODO: #1087 when parsing csv with -f -, if the csv (rules) parser fails, 
-    -- we would rather see that error, not the one from the journal parser
-    firstSuccessOrFirstError :: [String] -> [Reader] -> IO (Either String Journal)
-    firstSuccessOrFirstError [] []        = return $ Left "no readers found"
-    firstSuccessOrFirstError errs (r:rs) = do
-      dbg1IO "trying reader" (rFormat r)
-      result <- (runExceptT . (rParser r) iopts path) txt
-      dbg1IO "reader result" $ either id show result
-      case result of Right j -> return $ Right j                        -- success!
-                     Left e  -> firstSuccessOrFirstError (errs++[e]) rs -- keep trying
-    firstSuccessOrFirstError (e:_) []    = return $ Left e              -- none left, return first error
-    path = fromMaybe "(string)" mpath
-
----
-
-
--- tests
+-- ** tests
 
 tests_Read = tests "Read" [
    tests_Common
   ,tests_CsvReader
   ,tests_JournalReader
   ]
-
---samplejournal = readJournal' $ T.unlines
--- ["2008/01/01 income"
--- ,"    assets:bank:checking  $1"
--- ,"    income:salary"
--- ,""
--- ,"comment"
--- ,"multi line comment here"
--- ,"for testing purposes"
--- ,"end comment"
--- ,""
--- ,"2008/06/01 gift"
--- ,"    assets:bank:checking  $1"
--- ,"    income:gifts"
--- ,""
--- ,"2008/06/02 save"
--- ,"    assets:bank:saving  $1"
--- ,"    assets:bank:checking"
--- ,""
--- ,"2008/06/03 * eat & shop"
--- ,"    expenses:food      $1"
--- ,"    expenses:supplies  $1"
--- ,"    assets:cash"
--- ,""
--- ,"2008/12/31 * pay off"
--- ,"    liabilities:debts  $1"
--- ,"    assets:bank:checking"
--- ]
-
diff --git a/Hledger/Read/Common.hs b/Hledger/Read/Common.hs
--- a/Hledger/Read/Common.hs
+++ b/Hledger/Read/Common.hs
@@ -1,23 +1,37 @@
---- * doc
--- Lines beginning "--- *" are collapsible orgstruct nodes. Emacs users,
--- (add-hook 'haskell-mode-hook
---   (lambda () (set-variable 'orgstruct-heading-prefix-regexp "--- " t))
---   'orgstruct-mode)
--- and press TAB on nodes to expand/collapse.
-
+-- * -*- eval: (orgstruct-mode 1); orgstruct-heading-prefix-regexp:"-- "; -*-
+-- ** doc
+-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
 {-|
 
-Some common parsers and helpers used by several readers.
+File reading/parsing utilities used by multiple readers, and a good
+amount of the parsers for journal format, to avoid import cycles
+when JournalReader imports other readers.
+
 Some of these might belong in Hledger.Read.JournalReader or Hledger.Read.
 
 -}
 
---- * module
-{-# LANGUAGE CPP, BangPatterns, DeriveDataTypeable, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
+-- ** language
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE NoMonoLocalBinds #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE PackageImports #-}
+{-# LANGUAGE Rank2Types #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE TypeFamilies #-}
 
+-- ** doctest setup
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+-- ** exports
 module Hledger.Read.Common (
   Reader (..),
   InputOpts (..),
@@ -99,7 +113,8 @@
   tests_Common,
 )
 where
---- * imports
+
+-- ** imports
 import Prelude ()
 import "base-compat-batteries" Prelude.Compat hiding (fail, readFile)
 import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (fail)
@@ -110,6 +125,7 @@
 import Data.Data
 import Data.Decimal (DecimalRaw (Decimal), Decimal)
 import Data.Default
+import Data.Function ((&))
 import Data.Functor.Identity
 import "base-compat-batteries" Data.List.Compat
 import Data.List.NonEmpty (NonEmpty(..))
@@ -129,12 +145,15 @@
 import Hledger.Data
 import Hledger.Utils
 
--- $setup
--- >>> :set -XOverloadedStrings
+-- ** types
 
+-- main types; a few more below
+
 -- | A hledger journal reader is a triple of storage format name, a
 -- detector of that format, and a parser from that format to Journal.
-data Reader = Reader {
+-- The type variable m appears here so that rParserr can hold a
+-- journal parser, which depends on it.
+data Reader m = Reader {
 
      -- The canonical name of the format handled by this reader
      rFormat   :: StorageFormat
@@ -142,16 +161,17 @@
      -- The file extensions recognised as containing this format
     ,rExtensions :: [String]
 
-     -- A text parser for this format, accepting input options, file
+     -- The entry point for reading this format, accepting input options, file
      -- path for error messages and file contents, producing an exception-raising IO
-     -- action that returns a journal or error message.
-    ,rParser   :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
+     -- action that produces a journal or error message.
+    ,rReadFn   :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
 
-     -- Experimental readers are never tried automatically.
-    ,rExperimental :: Bool
+     -- The actual megaparsec parser called by the above, in case
+     -- another parser (includedirectivep) wants to use it directly.
+    ,rParser :: MonadIO m => ErroringJournalParser m ParsedJournal
     }
 
-instance Show Reader where show r = rFormat r ++ " reader"
+instance Show (Reader m) where show r = rFormat r ++ " reader"
 
 -- $setup
 
@@ -162,7 +182,6 @@
      mformat_           :: Maybe StorageFormat  -- ^ a file/storage format to try, unless overridden
                                                 --   by a filename prefix. Nothing means try all.
     ,mrules_file_       :: Maybe FilePath       -- ^ a conversion rules file to use (when reading CSV)
-    ,separator_         :: Char                 -- ^ the separator to use (when reading CSV)
     ,aliases_           :: [String]             -- ^ account name aliases to apply
     ,anon_              :: Bool                 -- ^ do light anonymisation/obfuscation of the data
     ,ignore_assertions_ :: Bool                 -- ^ don't check balance assertions
@@ -175,14 +194,13 @@
 instance Default InputOpts where def = definputopts
 
 definputopts :: InputOpts
-definputopts = InputOpts def def ',' def def def def True def def
+definputopts = InputOpts def def def def def def True def def
 
 rawOptsToInputOpts :: RawOpts -> InputOpts
 rawOptsToInputOpts rawopts = InputOpts{
    -- files_             = listofstringopt "file" rawopts
    mformat_           = Nothing
   ,mrules_file_       = maybestringopt "rules-file" rawopts
-  ,separator_         = fromMaybe ',' (maybecharopt "separator" rawopts)
   ,aliases_           = listofstringopt "alias" rawopts
   ,anon_              = boolopt "anon" rawopts
   ,ignore_assertions_ = boolopt "ignore-assertions" rawopts
@@ -192,7 +210,7 @@
   ,auto_              = boolopt "auto" rawopts
   }
 
---- * parsing utilities
+-- ** parsing utilities
 
 -- | Run a text parser in the identity monad. See also: parseWithState.
 runTextParser, rtp
@@ -227,9 +245,9 @@
             | (unPos $ sourceColumn p') == 1 = unPos (sourceLine p') - 1
             | otherwise = unPos $ sourceLine p' -- might be at end of file withat last new-line
 
-
--- | Given a megaparsec ParsedJournal parser, input options, file
--- path and file content: parse and finalise a Journal, or give an error.
+-- | Given a parser to ParsedJournal, input options, file path and
+-- content: run the parser on the content, and finalise the result to
+-- get a Journal; or throw an error.
 parseAndFinaliseJournal :: ErroringJournalParser IO ParsedJournal -> InputOpts
                            -> FilePath -> Text -> ExceptT String IO Journal
 parseAndFinaliseJournal parser iopts f txt = do
@@ -275,48 +293,34 @@
 finaliseJournal iopts f txt pj = do
   t <- liftIO getClockTime
   -- Infer and apply canonical styles for each commodity (or fail).
-  -- TODO: since #903's refactoring for hledger 1.12,
+  -- This affects transaction balancing/assertions/assignments, so needs to be done early.
+  -- (TODO: since #903's refactoring for hledger 1.12,
   -- journalApplyCommodityStyles here is seeing the
-  -- transactions before they get reversesd to normal order.
+  -- transactions before they get reversesd to normal order.)
   case journalApplyCommodityStyles pj of
     Left e    -> throwError e
-    Right pj' -> 
-      -- Finalise the parsed journal.
-      let fj =
-            if auto_ iopts && (not . null . jtxnmodifiers) pj
-            then
-              -- When automatic postings are active, we finalise twice:
-              -- once before and once after. However, if we are running it
-              -- twice, we don't check assertions the first time (they might
-              -- be false pending modifiers) and we don't reorder the second
-              -- time. If we are only running once, we reorder and follow
-              -- the options for checking assertions.
-              --
-              -- first pass, doing most of the work
-              (
-               (journalModifyTransactions <$>) $  -- add auto postings after balancing ? #893b fails
-               journalBalanceTransactions False $ -- balance transactions without checking assertions
-               -- journalModifyTransactions <$>   -- add auto postings before balancing ? probably #893a, #928, #938 fail
-               journalReverse $
-               journalSetLastReadTime t $
-               journalAddFile (f, txt) $
-               pj')
-              -- balance transactions a second time, now just checking balance assertions
-              >>= (\j ->
-                 journalBalanceTransactions (not $ ignore_assertions_ iopts) $
-                 j)
-
-            else
-              -- automatic postings are not active
-              journalBalanceTransactions (not $ ignore_assertions_ iopts) $
-              journalReverse $
-              journalSetLastReadTime t $
-              journalAddFile (f, txt) $
-              pj'
-      in
-        case fj of
-          Left e  -> throwError e
-          Right j -> return j
+    Right pj' -> either throwError return $
+      pj'
+      & journalAddFile (f, txt)  -- save the file path and content
+      & journalSetLastReadTime t -- save the last read time
+      & journalReverse           -- convert all lists to parse order
+      & if not (auto_ iopts) || null (jtxnmodifiers pj)
+        then
+          -- Auto postings are not active.
+          -- Balance all transactions and maybe check balance assertions.
+          journalBalanceTransactions (not $ ignore_assertions_ iopts)
+        else \j -> do  -- Either monad
+          -- Auto postings are active.
+          -- Balance all transactions without checking balance assertions,
+          j' <- journalBalanceTransactions False j
+          -- then add the auto postings
+          -- (Note adding auto postings after balancing means #893b fails;
+          -- adding them before balancing probably means #893a, #928, #938 fail.)
+          let j'' = journalModifyTransactions j'
+          -- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?)
+          j''' <- journalApplyCommodityStyles j''
+          -- then check balance assertions.
+          journalBalanceTransactions (not $ ignore_assertions_ iopts) j'''
 
 setYear :: Year -> JournalParser m ()
 setYear y = modify' (\j -> j{jparsedefaultyear=Just y})
@@ -372,7 +376,7 @@
 getAccountAliases = fmap jparsealiases get
 
 clearAccountAliases :: MonadState Journal m => m ()
-clearAccountAliases = modify' (\(j@Journal{..}) -> j{jparsealiases=[]})
+clearAccountAliases = modify' (\j -> j{jparsealiases=[]})
 
 -- getTransactionCount :: MonadState Journal m =>  m Integer
 -- getTransactionCount = fmap jparsetransactioncount get
@@ -391,9 +395,14 @@
   -- append, unlike the other fields, even though we do a final reverse,
   -- to compensate for additional reversal due to including/monoid-concatting
 
---- * parsers
+-- A version of `match` that is strict in the returned text
+match' :: TextParser m a -> TextParser m (Text, a)
+match' p = do
+  (!txt, p) <- match p
+  pure (txt, p)
 
---- ** transaction bits
+-- ** parsers
+-- *** transaction bits
 
 statusp :: TextParser m Status
 statusp =
@@ -416,10 +425,10 @@
 descriptionp = takeWhileP Nothing (not . semicolonOrNewline)
   where semicolonOrNewline c = c == ';' || c == '\n'
 
---- ** dates
+-- *** dates
 
--- | Parse a date in YYYY/MM/DD format.
--- Hyphen (-) and period (.) are also allowed as separators.
+-- | Parse a date in YYYY-MM-DD format.
+-- Slash (/) and period (.) are also allowed as separators.
 -- The year may be omitted if a default year has been set.
 -- Leading zeroes may be omitted.
 datep :: JournalParser m Day
@@ -471,8 +480,8 @@
 
 {-# INLINABLE datep' #-}
 
--- | Parse a date and time in YYYY/MM/DD HH:MM[:SS][+-ZZZZ] format.
--- Hyphen (-) and period (.) are also allowed as date separators.
+-- | Parse a date and time in YYYY-MM-DD HH:MM[:SS][+-ZZZZ] format.
+-- Slash (/) and period (.) are also allowed as date separators.
 -- The year may be omitted if a default year has been set.
 -- Seconds are optional.
 -- The timezone is optional and ignored (the time is always interpreted as a local time).
@@ -534,7 +543,7 @@
 secondarydatep primaryDate = char '=' *> datep' (Just primaryYear)
   where primaryYear = first3 $ toGregorian primaryDate
 
---- ** account names
+-- *** account names
 
 -- | Parse an account name (plus one following space if present),
 -- then apply any parent account prefix and/or account aliases currently in effect,
@@ -565,6 +574,7 @@
 
 -- | Parse any text beginning with a non-whitespace character, until a
 -- double space or the end of input.
+-- TODO including characters which normally start a comment (;#) - exclude those ? 
 singlespacedtextp :: TextParser m T.Text
 singlespacedtextp = singlespacedtextsatisfyingp (const True)
 
@@ -582,7 +592,7 @@
 singlespacep :: TextParser m ()
 singlespacep = void spacenonewline *> notFollowedBy spacenonewline
 
---- ** amounts
+-- *** amounts
 
 -- | Parse whitespace then an amount, with an optional left or right
 -- currency symbol and optional price, or return the special
@@ -985,7 +995,7 @@
     makeGroup = uncurry DigitGrp . foldl' step (0, 0) . T.unpack
     step (!l, !a) c = (l+1, a*10 + fromIntegral (digitToInt c))
 
---- ** comments
+-- *** comments
 
 multilinecommentp :: TextParser m ()
 multilinecommentp = startComment *> anyLine `skipManyTill` endComment
@@ -998,13 +1008,14 @@
 
 {-# INLINABLE multilinecommentp #-}
 
+-- | A blank or comment line in journal format: a line that's empty or
+-- containing only whitespace or whose first non-whitespace character
+-- is semicolon, hash, or star.
 emptyorcommentlinep :: TextParser m ()
 emptyorcommentlinep = do
   skipMany spacenonewline
   skiplinecommentp <|> void newline
   where
-    -- A line (file-level) comment can start with a semicolon, hash, or star
-    -- (allowing org nodes).
     skiplinecommentp :: TextParser m ()
     skiplinecommentp = do
       satisfy $ \c -> c == ';' || c == '#' || c == '*'
@@ -1068,7 +1079,7 @@
 --
 followingcommentp :: TextParser m Text
 followingcommentp =
-  fst <$> followingcommentp' (void $ takeWhileP Nothing (/= '\n'))
+  fst <$> followingcommentp' (void $ takeWhileP Nothing (/= '\n'))  -- XXX support \r\n ?
 {-# INLINABLE followingcommentp #-}
 
 
@@ -1239,9 +1250,6 @@
 
 {-# INLINABLE commenttagsanddatesp #-}
 
-
---- ** bracketed dates
-
 -- | Parse Ledger-style bracketed posting dates ([DATE=DATE2]), as
 -- "date" and/or "date2" tags. Anything that looks like an attempt at
 -- this (a square-bracketed sequence of 0123456789/-.= containing at
@@ -1294,16 +1302,7 @@
 
 {-# INLINABLE bracketeddatetagsp #-}
 
-
---- ** helper parsers
-
--- A version of `match` that is strict in the returned text
-match' :: TextParser m a -> TextParser m (Text, a)
-match' p = do
-  (!txt, p) <- match p
-  pure (txt, p)
-
---- * tests
+-- ** tests
 
 tests_Common = tests "Common" [
 
diff --git a/Hledger/Read/CsvReader.hs b/Hledger/Read/CsvReader.hs
--- a/Hledger/Read/CsvReader.hs
+++ b/Hledger/Read/CsvReader.hs
@@ -1,1012 +1,1237 @@
-{-|
-
-A reader for CSV data, using an extra rules file to help interpret the data.
-
--}
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ViewPatterns #-}
-{-# LANGUAGE TypeSynonymInstances #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE PackageImports #-}
-
-module Hledger.Read.CsvReader (
-  -- * Reader
-  reader,
-  -- * Misc.
-  CsvRecord,
-  CSV, Record, Field,
-  -- rules,
-  rulesFileFor,
-  parseRulesFile,
-  parseAndValidateCsvRules,
-  expandIncludes,
-  transactionFromCsvRecord,
-  printCSV,
-  -- * Tests
-  tests_CsvReader,
-)
-where
-import Prelude ()
-import "base-compat-batteries" Prelude.Compat hiding (fail)
-import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (fail)
-import Control.Exception          (IOException, handle, throw)
-import Control.Monad              (liftM, unless, when)
-import Control.Monad.Except       (ExceptT, throwError)
-import Control.Monad.IO.Class     (liftIO)
-import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
-import Control.Monad.Trans.Class  (lift)
-import Data.Char                  (toLower, isDigit, isSpace, ord)
-import Data.Bifunctor             (first)
-import "base-compat-batteries" Data.List.Compat
-import Data.Maybe
-import Data.Ord
-import qualified Data.Set as S
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
-import qualified Data.Text.IO as T
-import Data.Time.Calendar (Day)
-#if MIN_VERSION_time(1,5,0)
-import Data.Time.Format (parseTimeM, defaultTimeLocale)
-#else
-import Data.Time.Format (parseTime)
-import System.Locale (defaultTimeLocale)
-#endif
-import Safe
-import System.Directory (doesFileExist)
-import System.FilePath
-import qualified Data.Csv as Cassava
-import qualified Data.Csv.Parser.Megaparsec as CassavaMP
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Lazy as BL
-import Data.Foldable
-import Text.Megaparsec hiding (parse)
-import Text.Megaparsec.Char
-import Text.Megaparsec.Custom
-import Text.Printf (printf)
-
-import Hledger.Data
-import Hledger.Utils
-import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos, finaliseJournal)
-
-type CSV = [Record]
-
-type Record = [Field]
-
-type Field = String
-
-reader :: Reader
-reader = Reader
-  {rFormat     = "csv"
-  ,rExtensions = ["csv"]
-  ,rParser     = parse
-  ,rExperimental = False
-  }
-
--- | Parse and post-process a "Journal" from CSV data, or give an error.
--- Does not check balance assertions.
--- XXX currently ignores the provided data, reads it from the file path instead.
-parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
-parse iopts f t = do
-  let rulesfile = mrules_file_ iopts
-  let separator = separator_ iopts
-  r <- liftIO $ readJournalFromCsv separator rulesfile f t
-  case r of Left e   -> throwError e
-            Right pj -> finaliseJournal iopts{ignore_assertions_=True} f t pj'
-              where
-                -- finaliseJournal assumes the journal's items are
-                -- reversed, as produced by JournalReader's parser.
-                -- But here they are already properly ordered. So we'd
-                -- better preemptively reverse them once more. XXX inefficient
-                pj' = journalReverse pj
-
--- | Read a Journal from the given CSV data (and filename, used for error
--- messages), or return an error. Proceed as follows:
--- @
--- 1. parse CSV conversion rules from the specified rules file, or from
---    the default rules file for the specified CSV file, if it exists,
---    or throw a parse error; if it doesn't exist, use built-in default rules
--- 2. parse the CSV data, or throw a parse error
--- 3. convert the CSV records to transactions using the rules
--- 4. if the rules file didn't exist, create it with the default rules and filename
--- 5. return the transactions as a Journal
--- @
-readJournalFromCsv :: Char -> Maybe FilePath -> FilePath -> Text -> IO (Either String Journal)
-readJournalFromCsv _ Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin"
-readJournalFromCsv separator mrulesfile csvfile csvdata =
- handle (\(e::IOException) -> return $ Left $ show e) $ do
-
-  -- make and throw an IO exception.. which we catch and convert to an Either above ?
-  let throwerr = throw . userError
-
-  -- parse the csv rules
-  let rulesfile = fromMaybe (rulesFileFor csvfile) mrulesfile
-  rulesfileexists <- doesFileExist rulesfile
-  rulestext <-
-    if rulesfileexists
-    then do
-      dbg1IO "using conversion rules file" rulesfile
-      readFilePortably rulesfile >>= expandIncludes (takeDirectory rulesfile)
-    else 
-      return $ defaultRulesText rulesfile
-  rules <- either throwerr return $ parseAndValidateCsvRules rulesfile rulestext
-  dbg2IO "rules" rules
-
-  -- parse the skip directive's value, if any
-  let skiplines = case getDirective "skip" rules of
-                    Nothing -> 0
-                    Just "" -> 1
-                    Just s  -> readDef (throwerr $ "could not parse skip value: " ++ show s) s
-
-  -- parse csv
-  -- parsec seems to fail if you pass it "-" here TODO: try again with megaparsec
-  let parsecfilename = if csvfile == "-" then "(stdin)" else csvfile
-  records <- (either throwerr id .
-              dbg2 "validateCsv" . validateCsv rules skiplines .
-              dbg2 "parseCsv")
-             `fmap` parseCsv separator parsecfilename csvdata
-  dbg1IO "first 3 csv records" $ take 3 records
-
-  -- identify header lines
-  -- let (headerlines, datalines) = identifyHeaderLines records
-  --     mfieldnames = lastMay headerlines
-
-  let
-    -- convert CSV records to transactions
-    txns = snd $ mapAccumL
-                   (\pos r ->
-                      let
-                        SourcePos name line col = pos
-                        line' = (mkPos . (+1) . unPos) line
-                        pos' = SourcePos name line' col
-                      in
-                        (pos, transactionFromCsvRecord pos' rules r)
-                   )
-                   (initialPos parsecfilename) records
-
-    -- Ensure transactions are ordered chronologically.
-    -- First, if the CSV records seem to be most-recent-first (because
-    -- there's an explicit "newest-first" directive, or there's more
-    -- than one date and the first date is more recent than the last):
-    -- reverse them to get same-date transactions ordered chronologically.
-    txns' =
-      (if newestfirst || mseemsnewestfirst == Just True then reverse else id) txns
-      where
-        newestfirst = dbg3 "newestfirst" $ isJust $ getDirective "newest-first" rules
-        mseemsnewestfirst = dbg3 "mseemsnewestfirst" $
-          case nub $ map tdate txns of
-            ds | length ds > 1 -> Just $ head ds > last ds
-            _                  -> Nothing
-    -- Second, sort by date.
-    txns'' = sortBy (comparing tdate) txns'
-
-  when (not rulesfileexists) $ do
-    dbg1IO "creating conversion rules file" rulesfile
-    writeFile rulesfile $ T.unpack rulestext
-
-  return $ Right nulljournal{jtxns=txns''}
-
-parseCsv :: Char -> FilePath -> Text -> IO (Either String CSV)
-parseCsv separator filePath csvdata =
-  case filePath of
-    "-" -> liftM (parseCassava separator "(stdin)") T.getContents
-    _   -> return $ parseCassava separator filePath csvdata
-
-parseCassava :: Char -> FilePath -> Text -> Either String CSV
-parseCassava separator path content =
-  either (Left . errorBundlePretty) (Right . parseResultToCsv) <$>
-  CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path $
-  BL.fromStrict $ T.encodeUtf8 content
-
-decodeOptions :: Char -> Cassava.DecodeOptions
-decodeOptions separator = Cassava.defaultDecodeOptions {
-                      Cassava.decDelimiter = fromIntegral (ord separator)
-                    }
-
-parseResultToCsv :: (Foldable t, Functor t) => t (t B.ByteString) -> CSV
-parseResultToCsv = toListList . unpackFields
-    where
-        toListList = toList . fmap toList
-        unpackFields  = (fmap . fmap) (T.unpack . T.decodeUtf8)
-
-printCSV :: CSV -> String
-printCSV records = unlined (printRecord `map` records)
-    where printRecord = concat . intersperse "," . map printField
-          printField f = "\"" ++ concatMap escape f ++ "\""
-          escape '"' = "\"\""
-          escape x = [x]
-          unlined = concat . intersperse "\n"
-
--- | Return the cleaned up and validated CSV data (can be empty), or an error.
-validateCsv :: CsvRules -> Int -> Either String CSV -> Either String [CsvRecord]
-validateCsv _ _           (Left err) = Left err
-validateCsv rules numhdrlines (Right rs) = validate $ applyConditionalSkips $ drop numhdrlines $ filternulls rs
-  where
-    filternulls = filter (/=[""])
-    skipCount r =
-      case (getEffectiveAssignment rules r "end", getEffectiveAssignment rules r "skip") of
-        (Nothing, Nothing) -> Nothing
-        (Just _, _) -> Just maxBound
-        (Nothing, Just "") -> Just 1
-        (Nothing, Just x) -> Just (read x)
-    applyConditionalSkips [] = []
-    applyConditionalSkips (r:rest) =
-      case skipCount r of
-        Nothing -> r:(applyConditionalSkips rest)
-        Just cnt -> applyConditionalSkips (drop (cnt-1) rest)
-    validate [] = Right []
-    validate rs@(_first:_)
-      | isJust lessthan2 = let r = fromJust lessthan2 in
-          Left $ printf "CSV record %s has less than two fields" (show r)
-      | otherwise        = Right rs
-      where
-        lessthan2 = headMay $ filter ((<2).length) rs
-
--- -- | The highest (0-based) field index referenced in the field
--- -- definitions, or -1 if no fields are defined.
--- maxFieldIndex :: CsvRules -> Int
--- maxFieldIndex r = maximumDef (-1) $ catMaybes [
---                    dateField r
---                   ,statusField r
---                   ,codeField r
---                   ,amountField r
---                   ,amountInField r
---                   ,amountOutField r
---                   ,currencyField r
---                   ,accountField r
---                   ,account2Field r
---                   ,date2Field r
---                   ]
-
--- rulesFileFor :: CliOpts -> FilePath -> FilePath
--- rulesFileFor CliOpts{rules_file_=Just f} _ = f
--- rulesFileFor CliOpts{rules_file_=Nothing} csvfile = replaceExtension csvfile ".rules"
-rulesFileFor :: FilePath -> FilePath
-rulesFileFor = (++ ".rules")
-
-csvFileFor :: FilePath -> FilePath
-csvFileFor = reverse . drop 6 . reverse
-
-defaultRulesText :: FilePath -> Text
-defaultRulesText csvfile = T.pack $ unlines
-  ["# hledger csv conversion rules for " ++ csvFileFor (takeFileName csvfile)
-  ,"# cf http://hledger.org/manual#csv-files"
-  ,""
-  ,"account1 assets:bank:checking"
-  ,""
-  ,"fields date, description, amount1"
-  ,""
-  ,"#skip 1"
-  ,"#newest-first"
-  ,""
-  ,"#date-format %-d/%-m/%Y"
-  ,"#date-format %-m/%-d/%Y"
-  ,"#date-format %Y-%h-%d"
-  ,""
-  ,"#currency $"
-  ,""
-  ,"if ITUNES"
-  ," account2 expenses:entertainment"
-  ,""
-  ,"if (TO|FROM) SAVINGS"
-  ," account2 assets:bank:savings\n"
-  ]
-
---------------------------------------------------------------------------------
--- Conversion rules parsing
-
-{-
-Grammar for the CSV conversion rules, more or less:
-
-RULES: RULE*
-
-RULE: ( FIELD-LIST | FIELD-ASSIGNMENT | CONDITIONAL-BLOCK | SKIP | NEWEST-FIRST | DATE-FORMAT | COMMENT | BLANK ) NEWLINE
-
-FIELD-LIST: fields SPACE FIELD-NAME ( SPACE? , SPACE? FIELD-NAME )*
-
-FIELD-NAME: QUOTED-FIELD-NAME | BARE-FIELD-NAME
-
-QUOTED-FIELD-NAME: " (any CHAR except double-quote)+ "
-
-BARE-FIELD-NAME: any CHAR except space, tab, #, ;
-
-FIELD-ASSIGNMENT: JOURNAL-FIELD ASSIGNMENT-SEPARATOR FIELD-VALUE
-
-JOURNAL-FIELD: date | date2 | status | code | description | comment | account1 | account2 | amount | JOURNAL-PSEUDO-FIELD
-
-JOURNAL-PSEUDO-FIELD: amount-in | amount-out | currency
-
-ASSIGNMENT-SEPARATOR: SPACE | ( : SPACE? )
-
-FIELD-VALUE: VALUE (possibly containing CSV-FIELD-REFERENCEs)
-
-CSV-FIELD-REFERENCE: % CSV-FIELD
-
-CSV-FIELD: ( FIELD-NAME | FIELD-NUMBER ) (corresponding to a CSV field)
-
-FIELD-NUMBER: DIGIT+
-
-CONDITIONAL-BLOCK: if ( FIELD-MATCHER NEWLINE )+ INDENTED-BLOCK
-
-FIELD-MATCHER: ( CSV-FIELD-NAME SPACE? )? ( MATCHOP SPACE? )? PATTERNS
-
-MATCHOP: ~
-
-PATTERNS: ( NEWLINE REGEXP )* REGEXP
-
-INDENTED-BLOCK: ( SPACE ( FIELD-ASSIGNMENT | COMMENT ) NEWLINE )+
-
-REGEXP: ( NONSPACE CHAR* ) SPACE?
-
-VALUE: SPACE? ( CHAR* ) SPACE?
-
-COMMENT: SPACE? COMMENT-CHAR VALUE
-
-COMMENT-CHAR: # | ;
-
-NONSPACE: any CHAR not a SPACE-CHAR
-
-BLANK: SPACE?
-
-SPACE: SPACE-CHAR+
-
-SPACE-CHAR: space | tab
-
-CHAR: any character except newline
-
-DIGIT: 0-9
-
--}
-
-{- |
-A set of data definitions and account-matching patterns sufficient to
-convert a particular CSV data file into meaningful journal transactions.
--}
-data CsvRules = CsvRules {
-  rdirectives        :: [(DirectiveName,String)],
-  rcsvfieldindexes   :: [(CsvFieldName, CsvFieldIndex)],
-  rassignments       :: [(JournalFieldName, FieldTemplate)],
-  rconditionalblocks :: [ConditionalBlock]
-} deriving (Show, Eq)
-
-type CsvRulesParser a = StateT CsvRules SimpleTextParser a
-
-type DirectiveName    = String
-type CsvFieldName     = String
-type CsvFieldIndex    = Int
-type JournalFieldName = String
-type FieldTemplate    = String
-type ConditionalBlock = ([RecordMatcher], [(JournalFieldName, FieldTemplate)]) -- block matches if all RecordMatchers match
-type RecordMatcher    = [RegexpPattern] -- match if any regexps match any of the csv fields
--- type FieldMatcher     = (CsvFieldName, [RegexpPattern]) -- match if any regexps match this csv field
-type DateFormat       = String
-type RegexpPattern           = String
-
-defrules = CsvRules {
-  rdirectives=[],
-  rcsvfieldindexes=[],
-  rassignments=[],
-  rconditionalblocks=[]
-}
-
-addDirective :: (DirectiveName, String) -> CsvRules -> CsvRules
-addDirective d r = r{rdirectives=d:rdirectives r}
-
-addAssignment :: (JournalFieldName, FieldTemplate) -> CsvRules -> CsvRules
-addAssignment a r = r{rassignments=a:rassignments r}
-
-setIndexesAndAssignmentsFromList :: [CsvFieldName] -> CsvRules -> CsvRules
-setIndexesAndAssignmentsFromList fs r = addAssignmentsFromList fs . setCsvFieldIndexesFromList fs $ r
-
-setCsvFieldIndexesFromList :: [CsvFieldName] -> CsvRules -> CsvRules
-setCsvFieldIndexesFromList fs r = r{rcsvfieldindexes=zip fs [1..]}
-
-addAssignmentsFromList :: [CsvFieldName] -> CsvRules -> CsvRules
-addAssignmentsFromList fs r = foldl' maybeAddAssignment r journalfieldnames
-  where
-    maybeAddAssignment rules f = (maybe id addAssignmentFromIndex $ elemIndex f fs) rules
-      where
-        addAssignmentFromIndex i = addAssignment (f, "%"++show (i+1))
-
-addConditionalBlock :: ConditionalBlock -> CsvRules -> CsvRules
-addConditionalBlock b r = r{rconditionalblocks=b:rconditionalblocks r}
-
-getDirective :: DirectiveName -> CsvRules -> Maybe FieldTemplate
-getDirective directivename = lookup directivename . rdirectives
-
-instance ShowErrorComponent String where
-  showErrorComponent = id
-
--- Not used by hledger; just for lib users, 
--- | An pure-exception-throwing IO action that parses this file's content
--- as CSV conversion rules, interpolating any included files first,
--- and runs some extra validation checks.
-parseRulesFile :: FilePath -> ExceptT String IO CsvRules
-parseRulesFile f =
-  liftIO (readFilePortably f >>= expandIncludes (takeDirectory f))
-    >>= either throwError return . 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.
--- This is done as a pre-parse step to simplify the CSV rules parser.
-expandIncludes :: FilePath -> Text -> IO Text
-expandIncludes dir content = mapM (expandLine dir) (T.lines content) >>= return . T.unlines
-  where
-    expandLine dir line =
-      case line of
-        (T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< T.readFile f'
-          where
-            f' = dir </> dropWhile isSpace (T.unpack f)
-            dir' = takeDirectory f'
-        _ -> return line
-
--- | An error-throwing IO action that parses this text as CSV conversion rules
--- and runs some extra validation checks. The file path is used in error messages.
-parseAndValidateCsvRules :: FilePath -> T.Text -> Either String CsvRules
-parseAndValidateCsvRules rulesfile s =
-  case parseCsvRules rulesfile s of
-    Left err    -> Left $ customErrorBundlePretty err
-    Right rules -> first makeFancyParseError $ validateRules rules
-  where
-    makeFancyParseError :: String -> String
-    makeFancyParseError s = 
-      parseErrorPretty (FancyError 0 (S.singleton $ ErrorFail s) :: ParseError Text String)
-
--- | Parse this text as CSV conversion rules. The file path is for error messages.
-parseCsvRules :: FilePath -> T.Text -> Either (ParseErrorBundle T.Text CustomErr) CsvRules
--- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s
-parseCsvRules rulesfile s =
-  runParser (evalStateT rulesp defrules) rulesfile s
-
--- | Return the validated rules, or an error.
-validateRules :: CsvRules -> Either String CsvRules
-validateRules rules = do
-  unless (isAssigned "date")   $ Left "Please specify (at top level) the date field. Eg: date %1\n"
-  Right rules
-  where
-    isAssigned f = isJust $ getEffectiveAssignment rules [] f
-
--- parsers
-
-rulesp :: CsvRulesParser CsvRules
-rulesp = do
-  many $ choiceInState
-    [blankorcommentlinep                                                <?> "blank or comment line"
-    ,(directivep        >>= modify' . addDirective)                     <?> "directive"
-    ,(fieldnamelistp    >>= modify' . setIndexesAndAssignmentsFromList) <?> "field name list"
-    ,(fieldassignmentp  >>= modify' . addAssignment)                    <?> "field assignment"
-    ,(conditionalblockp >>= modify' . addConditionalBlock)              <?> "conditional block"
-    ]
-  eof
-  r <- get
-  return r{rdirectives=reverse $ rdirectives r
-          ,rassignments=reverse $ rassignments r
-          ,rconditionalblocks=reverse $ rconditionalblocks r
-          }
-
-blankorcommentlinep :: CsvRulesParser ()
-blankorcommentlinep = lift (dbgparse 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep]
-
-blanklinep :: CsvRulesParser ()
-blanklinep = lift (skipMany spacenonewline) >> newline >> return () <?> "blank line"
-
-commentlinep :: CsvRulesParser ()
-commentlinep = lift (skipMany spacenonewline) >> commentcharp >> lift restofline >> return () <?> "comment line"
-
-commentcharp :: CsvRulesParser Char
-commentcharp = oneOf (";#*" :: [Char])
-
-directivep :: CsvRulesParser (DirectiveName, String)
-directivep = (do
-  lift $ dbgparse 3 "trying directive"
-  d <- fmap T.unpack $ choiceInState $ map (lift . string . T.pack) directives
-  v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp)
-       <|> (optional (char ':') >> lift (skipMany spacenonewline) >> lift eolof >> return "")
-  return (d, v)
-  ) <?> "directive"
-
-directives =
-  ["date-format"
-  -- ,"default-account1"
-  -- ,"default-currency"
-  -- ,"skip-lines" -- old
-  ,"skip"
-  ,"newest-first"
-   -- ,"base-account"
-   -- ,"base-currency"
-  , "balance-type"
-  ]
-
-directivevalp :: CsvRulesParser String
-directivevalp = anySingle `manyTill` lift eolof
-
-fieldnamelistp :: CsvRulesParser [CsvFieldName]
-fieldnamelistp = (do
-  lift $ dbgparse 3 "trying fieldnamelist"
-  string "fields"
-  optional $ char ':'
-  lift (skipSome spacenonewline)
-  let separator = lift (skipMany spacenonewline) >> char ',' >> lift (skipMany spacenonewline)
-  f <- fromMaybe "" <$> optional fieldnamep
-  fs <- some $ (separator >> fromMaybe "" <$> optional fieldnamep)
-  lift restofline
-  return $ map (map toLower) $ f:fs
-  ) <?> "field name list"
-
-fieldnamep :: CsvRulesParser String
-fieldnamep = quotedfieldnamep <|> barefieldnamep
-
-quotedfieldnamep :: CsvRulesParser String
-quotedfieldnamep = do
-  char '"'
-  f <- some $ noneOf ("\"\n:;#~" :: [Char])
-  char '"'
-  return f
-
-barefieldnamep :: CsvRulesParser String
-barefieldnamep = some $ noneOf (" \t\n,;#~" :: [Char])
-
-fieldassignmentp :: CsvRulesParser (JournalFieldName, FieldTemplate)
-fieldassignmentp = do
-  lift $ dbgparse 3 "trying fieldassignmentp"
-  f <- journalfieldnamep
-  v <- choiceInState [ assignmentseparatorp >> fieldvalp
-                     , lift eolof >> return ""
-                     ]
-  return (f,v)
-  <?> "field assignment"
-
-journalfieldnamep :: CsvRulesParser String
-journalfieldnamep = do
-  lift (dbgparse 2 "trying journalfieldnamep")
-  T.unpack <$> choiceInState (map (lift . string . T.pack) journalfieldnames)
-
--- Transaction fields and pseudo fields for CSV conversion.
--- Names must precede any other name they contain, for the parser
--- (amount-in before amount; date2 before date). TODO: fix
-journalfieldnames =
-  concat [[ "account" ++ i
-          ,"amount" ++ i ++ "-in"
-          ,"amount" ++ i ++ "-out"
-          ,"amount" ++ i
-          ,"balance" ++ i
-          ,"comment" ++ i
-          ,"currency" ++ i
-          ] | x <- [1..9], let i = show x]
-  ++
-  ["amount-in"
-  ,"amount-out"
-  ,"amount"
-  ,"balance"
-  ,"code"
-  ,"comment"
-  ,"currency"
-  ,"date2"
-  ,"date"
-  ,"description"
-  ,"status"
-  ,"skip" -- skip and end are not really fields, but we list it here to allow conditional rules that skip records
-  ,"end"
-  ]
-
-assignmentseparatorp :: CsvRulesParser ()
-assignmentseparatorp = do
-  lift $ dbgparse 3 "trying assignmentseparatorp"
-  _ <- choiceInState [ lift (skipMany spacenonewline) >> char ':' >> lift (skipMany spacenonewline)
-                     , lift (skipSome spacenonewline)
-                     ]
-  return ()
-
-fieldvalp :: CsvRulesParser String
-fieldvalp = do
-  lift $ dbgparse 2 "trying fieldvalp"
-  anySingle `manyTill` lift eolof
-
-conditionalblockp :: CsvRulesParser ConditionalBlock
-conditionalblockp = do
-  lift $ dbgparse 3 "trying conditionalblockp"
-  string "if" >> lift (skipMany spacenonewline) >> optional newline
-  ms <- some recordmatcherp
-  as <- many (try $ lift (skipSome spacenonewline) >> fieldassignmentp)
-  when (null as) $
-    Fail.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)
-  <?> "conditional block"
-
-recordmatcherp :: CsvRulesParser [String]
-recordmatcherp = do
-  lift $ dbgparse 2 "trying recordmatcherp"
-  -- pos <- currentPos
-  _  <- optional (matchoperatorp >> lift (skipMany spacenonewline) >> optional newline)
-  ps <- patternsp
-  when (null ps) $
-    Fail.fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"
-  return ps
-  <?> "record matcher"
-
-matchoperatorp :: CsvRulesParser String
-matchoperatorp = fmap T.unpack $ choiceInState $ map string
-  ["~"
-  -- ,"!~"
-  -- ,"="
-  -- ,"!="
-  ]
-
-patternsp :: CsvRulesParser [String]
-patternsp = do
-  lift $ dbgparse 3 "trying patternsp"
-  ps <- many regexp
-  return ps
-
-regexp :: CsvRulesParser String
-regexp = do
-  lift $ dbgparse 3 "trying regexp"
-  notFollowedBy matchoperatorp
-  c <- lift nonspace
-  cs <- anySingle `manyTill` lift eolof
-  return $ strip $ c:cs
-
--- fieldmatcher = do
---   dbgparse 2 "trying fieldmatcher"
---   f <- fromMaybe "all" `fmap` (optional $ do
---          f' <- fieldname
---          lift (skipMany spacenonewline)
---          return f')
---   char '~'
---   lift (skipMany spacenonewline)
---   ps <- patterns
---   let r = "(" ++ intercalate "|" ps ++ ")"
---   return (f,r)
---   <?> "field matcher"
-
---------------------------------------------------------------------------------
--- Converting CSV records to journal transactions
-
-type CsvRecord = [String]
-
-showRules rules record =
-  unlines $ catMaybes [ (("the "++fld++" rule is: ")++) <$> getEffectiveAssignment rules record fld | fld <- journalfieldnames]
-
-transactionFromCsvRecord :: SourcePos -> CsvRules -> CsvRecord -> Transaction
-transactionFromCsvRecord sourcepos rules record = t
-  where
-    mdirective       = (`getDirective` rules)
-    mfieldtemplate   = getEffectiveAssignment rules record
-    render           = renderTemplate rules record
-    mskip            = mdirective "skip"
-    mdefaultcurrency = mdirective "default-currency"
-    mparsedate       = parseDateWithFormatOrDefaultFormats (mdirective "date-format")
-
-    -- render each field using its template and the csv record, and
-    -- in some cases parse the rendered string (eg dates and amounts)
-    mdateformat = mdirective "date-format"
-    date        = render $ fromMaybe "" $ mfieldtemplate "date"
-    date'       = fromMaybe (error' $ dateerror "date" date mdateformat) $ mparsedate date
-    mdate2      = render <$> mfieldtemplate "date2"
-    mdate2'     = maybe Nothing (maybe (error' $ dateerror "date2" (fromMaybe "" mdate2) mdateformat) Just . mparsedate) mdate2
-    dateerror datefield value mdateformat = unlines
-      ["error: could not parse \""++value++"\" as a date using date format "++maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" show mdateformat
-      , showRecord record
-      ,"the "++datefield++" rule is:   "++(fromMaybe "required, but missing" $ mfieldtemplate datefield)
-      ,"the date-format is: "++fromMaybe "unspecified" mdateformat
-      ,"you may need to "
-       ++"change your "++datefield++" rule, "
-       ++maybe "add a" (const "change your") mdateformat++" date-format rule, "
-       ++"or "++maybe "add a" (const "change your") mskip++" skip rule"
-      ,"for m/d/y or d/m/y dates, use date-format %-m/%-d/%Y or date-format %-d/%-m/%Y"
-      ]
-    status      =
-      case mfieldtemplate "status" of
-        Nothing  -> Unmarked
-        Just str -> either statuserror id .
-                    runParser (statusp <* eof) "" .
-                    T.pack $ render str
-          where
-            statuserror err = error' $ unlines
-              ["error: could not parse \""++str++"\" as a cleared status (should be *, ! or empty)"
-              ,"the parse error is:      "++customErrorBundlePretty err
-              ]
-    code        = singleline $ maybe "" render $ mfieldtemplate "code"
-    description = singleline $ maybe "" render $ mfieldtemplate "description"
-    comment     = singleline $ maybe "" render $ mfieldtemplate "comment"
-    precomment  = singleline $ maybe "" render $ mfieldtemplate "precomment"
-
-    s `or` def  = if null s then def else s
-    parsebalance currency n str
-      | all isSpace str  = Nothing
-      | otherwise = Just $ (either (balanceerror n str) id $ runParser (evalStateT (amountp <* eof) mempty) "" $ T.pack $ (currency++) $ simplifySign str, nullsourcepos)
-    balanceerror n str err = error' $ unlines
-      ["error: could not parse \""++str++"\" as balance"++n++" amount"
-      ,showRecord record
-      ,showRules rules record
-      ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency
-      ,"the parse error is:      "++customErrorBundlePretty err
-      ]
-
-    parsePosting' number accountFld amountFld amountInFld amountOutFld balanceFld commentFld =
-      let currency = maybe (fromMaybe "" mdefaultcurrency) render $
-                      (mfieldtemplate ("currency"++number) `or `mfieldtemplate "currency")
-          amount = chooseAmount rules record currency amountFld amountInFld amountOutFld                      
-          account' = ((T.pack . render) <$> (mfieldtemplate accountFld
-                                           `or` mdirective ("default-account" ++ number)))
-          balance = (parsebalance currency number.render) =<< mfieldtemplate balanceFld
-          comment = T.pack $ maybe "" render $ mfieldtemplate commentFld
-          account =
-            case account' of
-              -- If account is explicitly "unassigned", suppress posting
-              -- Otherwise, generate posting with "expenses:unknown" account if we have amount/balance information
-              Just "" -> Nothing
-              Just account -> Just account
-              Nothing ->
-                -- If we have amount or balance assertion (which implies potential amount change),
-                -- but no account name, lets generate "expenses:unknown" account name.
-                case (amount, balance) of
-                  (Just _, _ ) -> Just "expenses:unknown"
-                  (_, Just _)  -> Just "expenses:unknown"
-                  (Nothing, Nothing) -> Nothing
-          in
-        case account of
-          Nothing -> Nothing
-          Just account -> 
-            Just $ (number, posting {paccount=accountNameWithoutPostingType account
-                                    , pamount=fromMaybe missingmixedamt amount
-                                    , ptransaction=Just t
-                                    , pbalanceassertion=toAssertion <$> balance
-                                    , pcomment = comment
-                                    , ptype = accountNamePostingType account})
-
-    parsePosting number =              
-      parsePosting' number
-      ("account"++number)
-      ("amount"++number)
-      ("amount"++number++"-in")
-      ("amount"++number++"-out")
-      ("balance"++number)
-      ("comment" ++ number)
-      
-    withAlias fld alias =
-      case (mfieldtemplate fld, mfieldtemplate alias) of
-        (Just fld, Just alias) -> error' $ unlines
-          [ "error: both \"" ++ fld ++ "\" and \"" ++ alias ++ "\" have values."
-          , showRecord record
-          , showRules rules record
-          ]
-        (Nothing, Just _) -> alias
-        (_, Nothing)      -> fld
-
-    posting1 = parsePosting' "1"
-               ("account1" `withAlias` "account")
-               ("amount1" `withAlias` "amount")
-               ("amount1-in" `withAlias` "amount-in")
-               ("amount1-out" `withAlias` "amount-out")
-               ("balance1" `withAlias` "balance")
-               "comment1" -- comment1 does not have legacy alias
-
-    postings' = catMaybes $ posting1:[ parsePosting i | x<-[2..9], let i = show x]
-
-    improveUnknownAccountName p =
-      if paccount p /="expenses:unknown"
-      then p
-      else case isNegativeMixedAmount (pamount p) of
-        Just True -> p{paccount = "income:unknown"}
-        Just False -> p{paccount = "expenses:unknown"}
-        _ -> p
-        
-    postings =
-      case postings' of
-        -- To be compatible with the behavior of the old code which allowed two postings only, we enforce
-        -- second posting when rules generated just first of them, and posting is of type that should be balanced.
-        -- When we have srictly first and second posting, but second posting does not have amount, we fill it in.
-        [("1",posting1)] ->
-          case ptype posting1 of
-            VirtualPosting -> [posting1]
-            _ ->
-              [posting1,improveUnknownAccountName (posting{paccount="expenses:unknown", pamount=costOfMixedAmount(-(pamount posting1)), ptransaction=Just t})]
-        [("1",posting1),("2",posting2)] ->
-          case (pamount posting1 == missingmixedamt , pamount posting2 == missingmixedamt) of
-            (False, True) -> [posting1, improveUnknownAccountName (posting2{pamount=costOfMixedAmount(-(pamount posting1))})]
-            _  -> [posting1, posting2]
-        _ -> map snd postings'
-        
-    -- build the transaction
-    t = nulltransaction{
-      tsourcepos               = genericSourcePos sourcepos,
-      tdate                    = date',
-      tdate2                   = mdate2',
-      tstatus                  = status,
-      tcode                    = T.pack code,
-      tdescription             = T.pack description,
-      tcomment                 = T.pack comment,
-      tprecedingcomment        = T.pack precomment,
-      tpostings                = postings
-      }
-
-    defaultAssertion =
-      case mdirective "balance-type" of
-        Nothing -> assertion
-        Just "=" -> assertion
-        Just "==" -> assertion {batotal=True}
-        Just "=*" -> assertion {bainclusive=True}
-        Just "==*" -> assertion{batotal=True, bainclusive=True}
-        Just x -> error' $ unlines
-          [ "balance-type \"" ++ x ++"\" is invalid. Use =, ==, =* or ==*." 
-          , showRecord record
-          , showRules rules record
-          ]
-        
-    toAssertion (a, b) = defaultAssertion{
-      baamount   = a,
-      baposition = b
-      }
-
-chooseAmount :: CsvRules -> CsvRecord -> String -> String -> String -> String -> Maybe MixedAmount
-chooseAmount rules record currency amountFld amountInFld amountOutFld =
- let
-   mamount    = getEffectiveAssignment rules record amountFld
-   mamountin  = getEffectiveAssignment rules record amountInFld
-   mamountout = getEffectiveAssignment rules record amountOutFld
-   parse  amt = notZero =<< (parseAmount currency <$> notEmpty =<< (strip . renderTemplate rules record) <$> amt)
- in
-  case (parse mamount, parse mamountin, parse mamountout) of
-    (Nothing, Nothing, Nothing) -> Nothing
-    (Just a,  Nothing, Nothing) -> Just a
-    (Nothing, Just i,  Nothing) -> Just i
-    (Nothing, Nothing, Just o)  -> Just $ negate o
-    (Nothing, Just i,  Just o)  -> error' $    "both "++amountInFld++" and "++amountOutFld++" have a value\n"
-                                            ++ "    "++amountInFld++": "  ++ show i ++ "\n"
-                                            ++ "    "++amountOutFld++": " ++ show o ++ "\n"
-                                            ++ "    record: "     ++ showRecord record
-    _                           -> error' $    "found values for "++amountFld++" and for "++amountInFld++"/"++amountOutFld++"\n"
-                                            ++ "please use either "++amountFld++" or "++amountInFld++"/"++amountOutFld++"\n"
-                                            ++ "    record: " ++ showRecord record
- where
-   notZero amt = if isZeroMixedAmount amt then Nothing else Just amt
-   notEmpty str = if str=="" then Nothing else Just str
-
-   parseAmount currency amountstr =
-     either (amounterror amountstr) (Mixed . (:[]))
-     <$> runParser (evalStateT (amountp <* eof) mempty) ""
-     <$> T.pack
-     <$> (currency++)
-     <$> simplifySign
-     <$> amountstr
-
-   amounterror amountstr err = error' $ unlines
-     ["error: could not parse \""++fromJust amountstr++"\" as an amount"
-     ,showRecord record
-     ,showRules rules record
-     ,"the default-currency is: "++fromMaybe "unspecified" (getDirective "default-currency" rules)
-     ,"the parse error is:      "++customErrorBundlePretty err
-     ,"you may need to "
-      ++"change your amount or currency rules, "
-      ++"or add or change your skip rule"
-     ]
-
-type CsvAmountString = String
-
--- | Canonicalise the sign in a CSV amount string.
--- Such strings can have a minus sign, negating parentheses,
--- or any two of these (which cancels out).
---
--- >>> simplifySign "1"
--- "1"
--- >>> simplifySign "-1"
--- "-1"
--- >>> simplifySign "(1)"
--- "-1"
--- >>> simplifySign "--1"
--- "1"
--- >>> simplifySign "-(1)"
--- "1"
--- >>> simplifySign "(-1)"
--- "1"
--- >>> simplifySign "((1))"
--- "1"
-simplifySign :: CsvAmountString -> CsvAmountString
-simplifySign ('(':s) | lastMay s == Just ')' = simplifySign $ negateStr $ init s
-simplifySign ('-':'(':s) | lastMay s == Just ')' = simplifySign $ init s
-simplifySign ('-':'-':s) = s
-simplifySign s = s
-
-negateStr :: String -> String
-negateStr ('-':s) = s
-negateStr s       = '-':s
-
--- | Show a (approximate) recreation of the original CSV record.
-showRecord :: CsvRecord -> String
-showRecord r = "the CSV record is:       "++intercalate "," (map show r)
-
--- | Given the conversion rules, a CSV record and a journal entry field name, find
--- the template value ultimately assigned to this field, either at top
--- level or in a matching conditional block.  Conditional blocks'
--- patterns are matched against an approximation of the original CSV
--- record: all the field values with commas intercalated.
-getEffectiveAssignment :: CsvRules -> CsvRecord -> JournalFieldName -> Maybe FieldTemplate
-getEffectiveAssignment rules record f = lastMay $ assignmentsFor f
-  where
-    assignmentsFor f = map snd $ filter ((==f).fst) $ toplevelassignments ++ conditionalassignments
-      where
-        toplevelassignments    = rassignments rules
-        conditionalassignments = concatMap snd $ filter blockMatches $ blocksAssigning f
-          where
-            blocksAssigning f = filter (any ((==f).fst) . snd) $ rconditionalblocks rules
-            blockMatches :: ConditionalBlock -> Bool
-            blockMatches (matchers,_) = all matcherMatches matchers
-              where
-                matcherMatches :: RecordMatcher -> Bool
-                -- matcherMatches pats = any patternMatches pats
-                matcherMatches pats = patternMatches $  "(" ++ intercalate "|" pats ++ ")"
-                  where
-                    patternMatches :: RegexpPattern -> Bool
-                    patternMatches pat = regexMatchesCI pat csvline
-                      where
-                        csvline = intercalate "," record
-
--- | Render a field assigment's template, possibly interpolating referenced
--- CSV field values. Outer whitespace is removed from interpolated values.
-renderTemplate ::  CsvRules -> CsvRecord -> FieldTemplate -> String
-renderTemplate rules record t = regexReplaceBy "%[A-z0-9_-]+" replace t
-  where
-    replace ('%':pat) = maybe pat (\i -> strip $ atDef "" record (i-1)) mindex
-      where
-        mindex | all isDigit pat = readMay pat
-               | otherwise       = lookup (map toLower pat) $ rcsvfieldindexes rules
-    replace pat       = pat
-
--- Parse the date string using the specified date-format, or if unspecified try these default formats:
--- YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, MM/DD/YYYY (month and day can be 1 or 2 digits, year must be 4).
-parseDateWithFormatOrDefaultFormats :: Maybe DateFormat -> String -> Maybe Day
-parseDateWithFormatOrDefaultFormats mformat s = firstJust $ map parsewith formats
-  where
-    parsetime =
-#if MIN_VERSION_time(1,5,0)
-     parseTimeM True
-#else
-     parseTime
-#endif
-    parsewith = flip (parsetime defaultTimeLocale) s
-    formats = maybe
-               ["%Y/%-m/%-d"
-               ,"%Y-%-m-%-d"
-               ,"%Y.%-m.%-d"
-               -- ,"%-m/%-d/%Y"
-                -- ,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)
-                -- ,parseTime defaultTimeLocale "%Y-%m-%e" (take 5 s ++ "0" ++ drop 5 s)
-                -- ,parseTime defaultTimeLocale "%m/%e/%Y" ('0':s)
-                -- ,parseTime defaultTimeLocale "%m-%e-%Y" ('0':s)
-               ]
-               (:[])
-                mformat
-
---------------------------------------------------------------------------------
--- tests
-
-tests_CsvReader = tests "CsvReader" [
-   tests "parseCsvRules" [
-     test"empty file" $
-      parseCsvRules "unknown" "" @?= Right defrules
-    ]
-  ,tests "rulesp" [
-     test"trailing comments" $
-      parseWithState' defrules rulesp "skip\n# \n#\n" @?= Right defrules{rdirectives = [("skip","")]}
-
-    ,test"trailing blank lines" $
-      parseWithState' defrules rulesp "skip\n\n  \n" @?= (Right defrules{rdirectives = [("skip","")]})
-
-    ,test"no final newline" $
-      parseWithState' defrules rulesp "skip" @?= (Right defrules{rdirectives=[("skip","")]})
-
-    ,test"assignment with empty value" $
-      parseWithState' defrules rulesp "account1 \nif foo\n  account2 foo\n" @?=
-        (Right defrules{rassignments = [("account1","")], rconditionalblocks = [([["foo"]],[("account2","foo")])]})
-  ]
-  ,tests "conditionalblockp" [
-    test"space after conditional" $ -- #1120
-      parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=
-        (Right ([["a"]],[("account2","b")]))
-  ]
-  ]
+-- * -*- eval: (orgstruct-mode 1); orgstruct-heading-prefix-regexp:"-- "; -*-
+-- ** doc
+-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
+{-|
+
+A reader for CSV data, using an extra rules file to help interpret the data.
+
+-}
+-- Lots of haddocks in this file are for non-exported types.
+-- Here's a command that will render them:
+-- stack haddock hledger-lib --fast --no-haddock-deps --haddock-arguments='--ignore-all-exports' --open
+
+-- ** language
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE ViewPatterns #-}
+
+-- ** doctest setup
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+-- ** exports
+module Hledger.Read.CsvReader (
+  -- * Reader
+  reader,
+  -- * Misc.
+  CSV, CsvRecord, CsvValue,
+  csvFileFor,
+  rulesFileFor,
+  parseRulesFile,
+  printCSV,
+  -- * Tests
+  tests_CsvReader,
+)
+where
+
+-- ** imports
+import Prelude ()
+import "base-compat-batteries" Prelude.Compat hiding (fail)
+import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (fail)
+import Control.Exception          (IOException, handle, throw)
+import Control.Monad              (liftM, unless, when)
+import Control.Monad.Except       (ExceptT, throwError)
+import Control.Monad.IO.Class     (MonadIO, liftIO)
+import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
+import Control.Monad.Trans.Class  (lift)
+import Data.Char                  (toLower, isDigit, isSpace, ord)
+import Data.Bifunctor             (first)
+import "base-compat-batteries" Data.List.Compat
+import Data.Maybe
+import Data.Ord
+import qualified Data.Set as S
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import qualified Data.Text.IO as T
+import Data.Time.Calendar (Day)
+#if MIN_VERSION_time(1,5,0)
+import Data.Time.Format (parseTimeM, defaultTimeLocale)
+#else
+import Data.Time.Format (parseTime)
+import System.Locale (defaultTimeLocale)
+#endif
+import Safe
+import System.Directory (doesFileExist)
+import System.FilePath
+import qualified Data.Csv as Cassava
+import qualified Data.Csv.Parser.Megaparsec as CassavaMP
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as BL
+import Data.Foldable
+import Text.Megaparsec hiding (parse)
+import Text.Megaparsec.Char
+import Text.Megaparsec.Custom
+import Text.Printf (printf)
+
+import Hledger.Data
+import Hledger.Utils
+import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos, finaliseJournal)
+
+-- ** some types
+
+type CSV       = [CsvRecord]
+type CsvRecord = [CsvValue]
+type CsvValue  = String
+
+-- ** reader
+
+reader :: MonadIO m => Reader m
+reader = Reader
+  {rFormat     = "csv"
+  ,rExtensions = ["csv","tsv","ssv"]
+  ,rReadFn     = parse
+  ,rParser    = error' "sorry, CSV files can't be included yet"
+  }
+
+-- | Parse and post-process a "Journal" from CSV data, or give an error.
+-- Does not check balance assertions.
+-- XXX currently ignores the provided data, reads it from the file path instead.
+parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
+parse iopts f t = do
+  let rulesfile = mrules_file_ iopts
+  r <- liftIO $ readJournalFromCsv rulesfile f t
+  case r of Left e   -> throwError e
+            Right pj -> finaliseJournal iopts{ignore_assertions_=True} f t pj'
+              where
+                -- finaliseJournal assumes the journal's items are
+                -- reversed, as produced by JournalReader's parser.
+                -- But here they are already properly ordered. So we'd
+                -- better preemptively reverse them once more. XXX inefficient
+                pj' = journalReverse pj
+
+-- ** reading rules files
+-- *** rules utilities
+
+-- Not used by hledger; just for lib users, 
+-- | An pure-exception-throwing IO action that parses this file's content
+-- as CSV conversion rules, interpolating any included files first,
+-- and runs some extra validation checks.
+parseRulesFile :: FilePath -> ExceptT String IO CsvRules
+parseRulesFile f =
+  liftIO (readFilePortably f >>= expandIncludes (takeDirectory f))
+    >>= either throwError return . parseAndValidateCsvRules f
+
+-- | Given a CSV file path, what would normally be the corresponding rules file ?
+rulesFileFor :: FilePath -> FilePath
+rulesFileFor = (++ ".rules")
+
+-- | Given a CSV rules file path, what would normally be the corresponding CSV file ?
+csvFileFor :: FilePath -> FilePath
+csvFileFor = reverse . drop 6 . reverse
+
+defaultRulesText :: FilePath -> Text
+defaultRulesText csvfile = T.pack $ unlines
+  ["# hledger csv conversion rules for " ++ csvFileFor (takeFileName csvfile)
+  ,"# cf http://hledger.org/manual#csv-files"
+  ,""
+  ,"account1 assets:bank:checking"
+  ,""
+  ,"fields date, description, amount1"
+  ,""
+  ,"#skip 1"
+  ,"#newest-first"
+  ,""
+  ,"#date-format %-d/%-m/%Y"
+  ,"#date-format %-m/%-d/%Y"
+  ,"#date-format %Y-%h-%d"
+  ,""
+  ,"#currency $"
+  ,""
+  ,"if ITUNES"
+  ," account2 expenses:entertainment"
+  ,""
+  ,"if (TO|FROM) SAVINGS"
+  ," account2 assets:bank:savings\n"
+  ]
+
+addDirective :: (DirectiveName, String) -> CsvRules -> CsvRules
+addDirective d r = r{rdirectives=d:rdirectives r}
+
+addAssignment :: (HledgerFieldName, FieldTemplate) -> CsvRules -> CsvRules
+addAssignment a r = r{rassignments=a:rassignments r}
+
+setIndexesAndAssignmentsFromList :: [CsvFieldName] -> CsvRules -> CsvRules
+setIndexesAndAssignmentsFromList fs r = addAssignmentsFromList fs . setCsvFieldIndexesFromList fs $ r
+
+setCsvFieldIndexesFromList :: [CsvFieldName] -> CsvRules -> CsvRules
+setCsvFieldIndexesFromList fs r = r{rcsvfieldindexes=zip fs [1..]}
+
+addAssignmentsFromList :: [CsvFieldName] -> CsvRules -> CsvRules
+addAssignmentsFromList fs r = foldl' maybeAddAssignment r journalfieldnames
+  where
+    maybeAddAssignment rules f = (maybe id addAssignmentFromIndex $ elemIndex f fs) rules
+      where
+        addAssignmentFromIndex i = addAssignment (f, "%"++show (i+1))
+
+addConditionalBlock :: ConditionalBlock -> CsvRules -> CsvRules
+addConditionalBlock b r = r{rconditionalblocks=b:rconditionalblocks r}
+
+getDirective :: DirectiveName -> CsvRules -> Maybe FieldTemplate
+getDirective directivename = lookup directivename . rdirectives
+
+instance ShowErrorComponent String where
+  showErrorComponent = id
+
+-- | 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.
+-- This is done as a pre-parse step to simplify the CSV rules parser.
+expandIncludes :: FilePath -> Text -> IO Text
+expandIncludes dir content = mapM (expandLine dir) (T.lines content) >>= return . T.unlines
+  where
+    expandLine dir line =
+      case line of
+        (T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< T.readFile f'
+          where
+            f' = dir </> dropWhile isSpace (T.unpack f)
+            dir' = takeDirectory f'
+        _ -> return line
+
+-- | An error-throwing IO action that parses this text as CSV conversion rules
+-- and runs some extra validation checks. The file path is used in error messages.
+parseAndValidateCsvRules :: FilePath -> T.Text -> Either String CsvRules
+parseAndValidateCsvRules rulesfile s =
+  case parseCsvRules rulesfile s of
+    Left err    -> Left $ customErrorBundlePretty err
+    Right rules -> first makeFancyParseError $ validateRules rules
+  where
+    makeFancyParseError :: String -> String
+    makeFancyParseError errorString =
+      parseErrorPretty (FancyError 0 (S.singleton $ ErrorFail errorString) :: ParseError Text String)
+
+-- | Parse this text as CSV conversion rules. The file path is for error messages.
+parseCsvRules :: FilePath -> T.Text -> Either (ParseErrorBundle T.Text CustomErr) CsvRules
+-- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s
+parseCsvRules rulesfile s =
+  runParser (evalStateT rulesp defrules) rulesfile s
+
+-- | Return the validated rules, or an error.
+validateRules :: CsvRules -> Either String CsvRules
+validateRules rules = do
+  unless (isAssigned "date")   $ Left "Please specify (at top level) the date field. Eg: date %1\n"
+  Right rules
+  where
+    isAssigned f = isJust $ getEffectiveAssignment rules [] f
+
+-- *** rules types
+
+-- | A set of data definitions and account-matching patterns sufficient to
+-- convert a particular CSV data file into meaningful journal transactions.
+data CsvRules = CsvRules {
+  rdirectives        :: [(DirectiveName,String)],
+    -- ^ top-level rules, as (keyword, value) pairs
+  rcsvfieldindexes   :: [(CsvFieldName, CsvFieldIndex)],
+    -- ^ csv field names and their column number, if declared by a fields list
+  rassignments       :: [(HledgerFieldName, FieldTemplate)],
+    -- ^ top-level assignments to hledger fields, as (field name, value template) pairs
+  rconditionalblocks :: [ConditionalBlock]
+    -- ^ conditional blocks, which containing additional assignments/rules to apply to matched csv records
+} deriving (Show, Eq)
+
+type CsvRulesParser a = StateT CsvRules SimpleTextParser a
+
+-- | The keyword of a CSV rule - "fields", "skip", "if", etc.
+type DirectiveName    = String
+
+-- | CSV field name.
+type CsvFieldName     = String
+
+-- | 1-based CSV column number.
+type CsvFieldIndex    = Int
+
+-- | Percent symbol followed by a CSV field name or column number. Eg: %date, %1.
+type CsvFieldReference = String
+
+-- | One of the standard hledger fields or pseudo-fields that can be assigned to.
+-- Eg date, account1, amount, amount1-in, date-format.
+type HledgerFieldName = String
+
+-- | A text value to be assigned to a hledger field, possibly
+-- containing csv field references to be interpolated.
+type FieldTemplate    = String
+
+-- | A strptime date parsing pattern, as supported by Data.Time.Format.
+type DateFormat       = String
+
+-- | A regular expression.
+type RegexpPattern    = String
+
+-- | A single test for matching a CSV record, in one way or another.
+data Matcher =
+    RecordMatcher RegexpPattern                   -- ^ match if this regexp matches the overall CSV record
+  | FieldMatcher CsvFieldReference RegexpPattern  -- ^ match if this regexp matches the referenced CSV field's value
+  deriving (Show, Eq)
+
+-- | A conditional block: a set of CSV record matchers, and a sequence
+-- of rules which will be enabled only if one or more of the matchers
+-- succeeds.
+--
+-- Three types of rule are allowed inside conditional blocks: field
+-- assignments, skip, end. (A skip or end rule is stored as if it was
+-- a field assignment, and executed in validateCsv. XXX)
+data ConditionalBlock = CB {
+   cbMatchers    :: [Matcher]
+  ,cbAssignments :: [(HledgerFieldName, FieldTemplate)]
+  } deriving (Show, Eq)
+
+defrules = CsvRules {
+  rdirectives=[],
+  rcsvfieldindexes=[],
+  rassignments=[],
+  rconditionalblocks=[]
+}
+
+-- *** rules parsers
+
+{-
+Grammar for the CSV conversion rules, more or less:
+
+RULES: RULE*
+
+RULE: ( FIELD-LIST | FIELD-ASSIGNMENT | CONDITIONAL-BLOCK | SKIP | NEWEST-FIRST | DATE-FORMAT | COMMENT | BLANK ) NEWLINE
+
+FIELD-LIST: fields SPACE FIELD-NAME ( SPACE? , SPACE? FIELD-NAME )*
+
+FIELD-NAME: QUOTED-FIELD-NAME | BARE-FIELD-NAME
+
+QUOTED-FIELD-NAME: " (any CHAR except double-quote)+ "
+
+BARE-FIELD-NAME: any CHAR except space, tab, #, ;
+
+FIELD-ASSIGNMENT: JOURNAL-FIELD ASSIGNMENT-SEPARATOR FIELD-VALUE
+
+JOURNAL-FIELD: date | date2 | status | code | description | comment | account1 | account2 | amount | JOURNAL-PSEUDO-FIELD
+
+JOURNAL-PSEUDO-FIELD: amount-in | amount-out | currency
+
+ASSIGNMENT-SEPARATOR: SPACE | ( : SPACE? )
+
+FIELD-VALUE: VALUE (possibly containing CSV-FIELD-REFERENCEs)
+
+CSV-FIELD-REFERENCE: % CSV-FIELD
+
+CSV-FIELD: ( FIELD-NAME | FIELD-NUMBER ) (corresponding to a CSV field)
+
+FIELD-NUMBER: DIGIT+
+
+CONDITIONAL-BLOCK: if ( FIELD-MATCHER NEWLINE )+ INDENTED-BLOCK
+
+FIELD-MATCHER: ( CSV-FIELD-NAME SPACE? )? ( MATCHOP SPACE? )? PATTERNS
+
+MATCHOP: ~
+
+PATTERNS: ( NEWLINE REGEXP )* REGEXP
+
+INDENTED-BLOCK: ( SPACE ( FIELD-ASSIGNMENT | COMMENT ) NEWLINE )+
+
+REGEXP: ( NONSPACE CHAR* ) SPACE?
+
+VALUE: SPACE? ( CHAR* ) SPACE?
+
+COMMENT: SPACE? COMMENT-CHAR VALUE
+
+COMMENT-CHAR: # | ;
+
+NONSPACE: any CHAR not a SPACE-CHAR
+
+BLANK: SPACE?
+
+SPACE: SPACE-CHAR+
+
+SPACE-CHAR: space | tab
+
+CHAR: any character except newline
+
+DIGIT: 0-9
+
+-}
+
+rulesp :: CsvRulesParser CsvRules
+rulesp = do
+  _ <- many $ choiceInState
+    [blankorcommentlinep                                                <?> "blank or comment line"
+    ,(directivep        >>= modify' . addDirective)                     <?> "directive"
+    ,(fieldnamelistp    >>= modify' . setIndexesAndAssignmentsFromList) <?> "field name list"
+    ,(fieldassignmentp  >>= modify' . addAssignment)                    <?> "field assignment"
+    ,(conditionalblockp >>= modify' . addConditionalBlock)              <?> "conditional block"
+    ]
+  eof
+  r <- get
+  return r{rdirectives=reverse $ rdirectives r
+          ,rassignments=reverse $ rassignments r
+          ,rconditionalblocks=reverse $ rconditionalblocks r
+          }
+
+blankorcommentlinep :: CsvRulesParser ()
+blankorcommentlinep = lift (dbgparse 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep]
+
+blanklinep :: CsvRulesParser ()
+blanklinep = lift (skipMany spacenonewline) >> newline >> return () <?> "blank line"
+
+commentlinep :: CsvRulesParser ()
+commentlinep = lift (skipMany spacenonewline) >> commentcharp >> lift restofline >> return () <?> "comment line"
+
+commentcharp :: CsvRulesParser Char
+commentcharp = oneOf (";#*" :: [Char])
+
+directivep :: CsvRulesParser (DirectiveName, String)
+directivep = (do
+  lift $ dbgparse 3 "trying directive"
+  d <- fmap T.unpack $ choiceInState $ map (lift . string . T.pack) directives
+  v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp)
+       <|> (optional (char ':') >> lift (skipMany spacenonewline) >> lift eolof >> return "")
+  return (d, v)
+  ) <?> "directive"
+
+directives :: [String]
+directives =
+  ["date-format"
+  ,"separator"
+  -- ,"default-account"
+  -- ,"default-currency"
+  ,"skip"
+  ,"newest-first"
+  , "balance-type"
+  ]
+
+directivevalp :: CsvRulesParser String
+directivevalp = anySingle `manyTill` lift eolof
+
+fieldnamelistp :: CsvRulesParser [CsvFieldName]
+fieldnamelistp = (do
+  lift $ dbgparse 3 "trying fieldnamelist"
+  string "fields"
+  optional $ char ':'
+  lift (skipSome spacenonewline)
+  let separator = lift (skipMany spacenonewline) >> char ',' >> lift (skipMany spacenonewline)
+  f <- fromMaybe "" <$> optional fieldnamep
+  fs <- some $ (separator >> fromMaybe "" <$> optional fieldnamep)
+  lift restofline
+  return $ map (map toLower) $ f:fs
+  ) <?> "field name list"
+
+fieldnamep :: CsvRulesParser String
+fieldnamep = quotedfieldnamep <|> barefieldnamep
+
+quotedfieldnamep :: CsvRulesParser String
+quotedfieldnamep = do
+  char '"'
+  f <- some $ noneOf ("\"\n:;#~" :: [Char])
+  char '"'
+  return f
+
+barefieldnamep :: CsvRulesParser String
+barefieldnamep = some $ noneOf (" \t\n,;#~" :: [Char])
+
+fieldassignmentp :: CsvRulesParser (HledgerFieldName, FieldTemplate)
+fieldassignmentp = do
+  lift $ dbgparse 3 "trying fieldassignmentp"
+  f <- journalfieldnamep
+  v <- choiceInState [ assignmentseparatorp >> fieldvalp
+                     , lift eolof >> return ""
+                     ]
+  return (f,v)
+  <?> "field assignment"
+
+journalfieldnamep :: CsvRulesParser String
+journalfieldnamep = do
+  lift (dbgparse 2 "trying journalfieldnamep")
+  T.unpack <$> choiceInState (map (lift . string . T.pack) journalfieldnames)
+
+-- Transaction fields and pseudo fields for CSV conversion.
+-- Names must precede any other name they contain, for the parser
+-- (amount-in before amount; date2 before date). TODO: fix
+journalfieldnames =
+  concat [[ "account" ++ i
+          ,"amount" ++ i ++ "-in"
+          ,"amount" ++ i ++ "-out"
+          ,"amount" ++ i
+          ,"balance" ++ i
+          ,"comment" ++ i
+          ,"currency" ++ i
+          ] | x <- [1..9], let i = show x]
+  ++
+  ["amount-in"
+  ,"amount-out"
+  ,"amount"
+  ,"balance"
+  ,"code"
+  ,"comment"
+  ,"currency"
+  ,"date2"
+  ,"date"
+  ,"description"
+  ,"status"
+  ,"skip" -- skip and end are not really fields, but we list it here to allow conditional rules that skip records
+  ,"end"
+  ]
+
+assignmentseparatorp :: CsvRulesParser ()
+assignmentseparatorp = do
+  lift $ dbgparse 3 "trying assignmentseparatorp"
+  _ <- choiceInState [ lift (skipMany spacenonewline) >> char ':' >> lift (skipMany spacenonewline)
+                     , lift (skipSome spacenonewline)
+                     ]
+  return ()
+
+fieldvalp :: CsvRulesParser String
+fieldvalp = do
+  lift $ dbgparse 2 "trying fieldvalp"
+  anySingle `manyTill` lift eolof
+
+-- A conditional block: one or more matchers, one per line, followed by one or more indented rules.
+conditionalblockp :: CsvRulesParser ConditionalBlock
+conditionalblockp = do
+  lift $ dbgparse 3 "trying conditionalblockp"
+  string "if" >> lift (skipMany spacenonewline) >> optional newline
+  ms <- some matcherp
+  as <- many (try $ lift (skipSome spacenonewline) >> fieldassignmentp)
+  when (null as) $
+    Fail.fail "start of conditional block found, but no assignment rules afterward\n(assignment rules in a conditional block should be indented)\n"
+  return $ CB{cbMatchers=ms, cbAssignments=as}
+  <?> "conditional block"
+
+-- A single matcher, on one line.
+matcherp :: CsvRulesParser Matcher
+matcherp = try fieldmatcherp <|> recordmatcherp
+
+-- A single whole-record matcher.
+-- A pattern on the whole line, not beginning with a csv field reference.
+recordmatcherp :: CsvRulesParser Matcher
+recordmatcherp = do
+  lift $ dbgparse 2 "trying matcherp"
+  -- pos <- currentPos
+  -- _  <- optional (matchoperatorp >> lift (skipMany spacenonewline) >> optional newline)
+  r <- regexp
+  -- when (null ps) $
+  --   Fail.fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"
+  return $ RecordMatcher r
+  <?> "record matcher"
+
+-- | A single matcher for a specific field. A csv field reference
+-- (like %date or %1), and a pattern on the rest of the line,
+-- optionally space-separated. Eg:
+-- %description chez jacques
+fieldmatcherp :: CsvRulesParser Matcher
+fieldmatcherp = do
+  lift $ dbgparse 2 "trying fieldmatcher"
+  -- An optional fieldname (default: "all")
+  -- f <- fromMaybe "all" `fmap` (optional $ do
+  --        f' <- fieldnamep
+  --        lift (skipMany spacenonewline)
+  --        return f')
+  f <- csvfieldreferencep <* lift (skipMany spacenonewline)
+  -- optional operator.. just ~ (case insensitive infix regex) for now
+  -- _op <- fromMaybe "~" <$> optional matchoperatorp
+  lift (skipMany spacenonewline)
+  r <- regexp
+  return $ FieldMatcher f r
+  <?> "field matcher"
+
+csvfieldreferencep :: CsvRulesParser CsvFieldReference
+csvfieldreferencep = do
+  lift $ dbgparse 3 "trying csvfieldreferencep"
+  char '%'
+  f <- fieldnamep
+  return $ '%' : quoteIfNeeded f
+
+-- A single regular expression
+regexp :: CsvRulesParser RegexpPattern
+regexp = do
+  lift $ dbgparse 3 "trying regexp"
+  -- notFollowedBy matchoperatorp
+  c <- lift nonspace
+  cs <- anySingle `manyTill` lift eolof
+  return $ strip $ c:cs
+
+-- -- A match operator, indicating the type of match to perform.
+-- -- Currently just ~ meaning case insensitive infix regex match.
+-- matchoperatorp :: CsvRulesParser String
+-- matchoperatorp = fmap T.unpack $ choiceInState $ map string
+--   ["~"
+--   -- ,"!~"
+--   -- ,"="
+--   -- ,"!="
+--   ]
+
+-- ** reading csv files
+
+-- | Read a Journal from the given CSV data (and filename, used for error
+-- messages), or return an error. Proceed as follows:
+--
+-- 1. parse CSV conversion rules from the specified rules file, or from
+--    the default rules file for the specified CSV file, if it exists,
+--    or throw a parse error; if it doesn't exist, use built-in default rules
+--
+-- 2. parse the CSV data, or throw a parse error
+--
+-- 3. convert the CSV records to transactions using the rules
+--
+-- 4. if the rules file didn't exist, create it with the default rules and filename
+--
+-- 5. return the transactions as a Journal
+-- 
+readJournalFromCsv :: Maybe FilePath -> FilePath -> Text -> IO (Either String Journal)
+readJournalFromCsv Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin"
+readJournalFromCsv mrulesfile csvfile csvdata =
+ handle (\(e::IOException) -> return $ Left $ show e) $ do
+
+  -- make and throw an IO exception.. which we catch and convert to an Either above ?
+  let throwerr = throw . userError
+
+  -- parse the csv rules
+  let rulesfile = fromMaybe (rulesFileFor csvfile) mrulesfile
+  rulesfileexists <- doesFileExist rulesfile
+  rulestext <-
+    if rulesfileexists
+    then do
+      dbg1IO "using conversion rules file" rulesfile
+      readFilePortably rulesfile >>= expandIncludes (takeDirectory rulesfile)
+    else
+      return $ defaultRulesText rulesfile
+  rules <- either throwerr return $ parseAndValidateCsvRules rulesfile rulestext
+  dbg2IO "rules" rules
+
+  -- parse the skip directive's value, if any
+  let skiplines = case getDirective "skip" rules of
+                    Nothing -> 0
+                    Just "" -> 1
+                    Just s  -> readDef (throwerr $ "could not parse skip value: " ++ show s) s
+
+  -- parse csv
+  -- parsec seems to fail if you pass it "-" here TODO: try again with megaparsec
+  let parsecfilename = if csvfile == "-" then "(stdin)" else csvfile
+  let separator = fromMaybe ',' (getDirective "separator" rules >>= parseSeparator)
+  dbg2IO "separator" separator
+  records <- (either throwerr id .
+              dbg2 "validateCsv" . validateCsv rules skiplines .
+              dbg2 "parseCsv")
+             `fmap` parseCsv separator parsecfilename csvdata
+  dbg1IO "first 3 csv records" $ take 3 records
+
+  -- identify header lines
+  -- let (headerlines, datalines) = identifyHeaderLines records
+  --     mfieldnames = lastMay headerlines
+
+  let
+    -- convert CSV records to transactions
+    txns = snd $ mapAccumL
+                   (\pos r ->
+                      let
+                        SourcePos name line col = pos
+                        line' = (mkPos . (+1) . unPos) line
+                        pos' = SourcePos name line' col
+                      in
+                        (pos, transactionFromCsvRecord pos' rules r)
+                   )
+                   (initialPos parsecfilename) records
+
+    -- Ensure transactions are ordered chronologically.
+    -- First, if the CSV records seem to be most-recent-first (because
+    -- there's an explicit "newest-first" directive, or there's more
+    -- than one date and the first date is more recent than the last):
+    -- reverse them to get same-date transactions ordered chronologically.
+    txns' =
+      (if newestfirst || mseemsnewestfirst == Just True then reverse else id) txns
+      where
+        newestfirst = dbg3 "newestfirst" $ isJust $ getDirective "newest-first" rules
+        mseemsnewestfirst = dbg3 "mseemsnewestfirst" $
+          case nub $ map tdate txns of
+            ds | length ds > 1 -> Just $ head ds > last ds
+            _                  -> Nothing
+    -- Second, sort by date.
+    txns'' = sortBy (comparing tdate) txns'
+
+  when (not rulesfileexists) $ do
+    dbg1IO "creating conversion rules file" rulesfile
+    writeFile rulesfile $ T.unpack rulestext
+
+  return $ Right nulljournal{jtxns=txns''}
+
+-- | Parse special separator names TAB and SPACE, or return the first
+-- character. Return Nothing on empty string
+parseSeparator :: String -> Maybe Char
+parseSeparator = specials . map toLower
+  where specials "space" = Just ' '
+        specials "tab"   = Just '\t'
+        specials (x:_)   = Just x
+        specials []      = Nothing
+
+parseCsv :: Char -> FilePath -> Text -> IO (Either String CSV)
+parseCsv separator filePath csvdata =
+  case filePath of
+    "-" -> liftM (parseCassava separator "(stdin)") T.getContents
+    _   -> return $ parseCassava separator filePath csvdata
+
+parseCassava :: Char -> FilePath -> Text -> Either String CSV
+parseCassava separator path content =
+  either (Left . errorBundlePretty) (Right . parseResultToCsv) <$>
+  CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path $
+  BL.fromStrict $ T.encodeUtf8 content
+
+decodeOptions :: Char -> Cassava.DecodeOptions
+decodeOptions separator = Cassava.defaultDecodeOptions {
+                      Cassava.decDelimiter = fromIntegral (ord separator)
+                    }
+
+parseResultToCsv :: (Foldable t, Functor t) => t (t B.ByteString) -> CSV
+parseResultToCsv = toListList . unpackFields
+    where
+        toListList = toList . fmap toList
+        unpackFields  = (fmap . fmap) (T.unpack . T.decodeUtf8)
+
+printCSV :: CSV -> String
+printCSV records = unlined (printRecord `map` records)
+    where printRecord = concat . intersperse "," . map printField
+          printField f = "\"" ++ concatMap escape f ++ "\""
+          escape '"' = "\"\""
+          escape x = [x]
+          unlined = concat . intersperse "\n"
+
+-- | Return the cleaned up and validated CSV data (can be empty), or an error.
+validateCsv :: CsvRules -> Int -> Either String CSV -> Either String [CsvRecord]
+validateCsv _ _           (Left err) = Left err
+validateCsv rules numhdrlines (Right rs) = validate $ applyConditionalSkips $ drop numhdrlines $ filternulls rs
+  where
+    filternulls = filter (/=[""])
+    skipCount r =
+      case (getEffectiveAssignment rules r "end", getEffectiveAssignment rules r "skip") of
+        (Nothing, Nothing) -> Nothing
+        (Just _, _) -> Just maxBound
+        (Nothing, Just "") -> Just 1
+        (Nothing, Just x) -> Just (read x)
+    applyConditionalSkips [] = []
+    applyConditionalSkips (r:rest) =
+      case skipCount r of
+        Nothing -> r:(applyConditionalSkips rest)
+        Just cnt -> applyConditionalSkips (drop (cnt-1) rest)
+    validate [] = Right []
+    validate rs@(_first:_)
+      | isJust lessthan2 = let r = fromJust lessthan2 in
+          Left $ printf "CSV record %s has less than two fields" (show r)
+      | otherwise        = Right rs
+      where
+        lessthan2 = headMay $ filter ((<2).length) rs
+
+-- -- | The highest (0-based) field index referenced in the field
+-- -- definitions, or -1 if no fields are defined.
+-- maxFieldIndex :: CsvRules -> Int
+-- maxFieldIndex r = maximumDef (-1) $ catMaybes [
+--                    dateField r
+--                   ,statusField r
+--                   ,codeField r
+--                   ,amountField r
+--                   ,amountInField r
+--                   ,amountOutField r
+--                   ,currencyField r
+--                   ,accountField r
+--                   ,account2Field r
+--                   ,date2Field r
+--                   ]
+
+-- ** converting csv records to transactions
+
+showRules rules record =
+  unlines $ catMaybes [ (("the "++fld++" rule is: ")++) <$> getEffectiveAssignment rules record fld | fld <- journalfieldnames]
+
+-- | Look up the value (template) of a csv rule by rule keyword.
+csvRule :: CsvRules -> DirectiveName -> Maybe FieldTemplate
+csvRule rules = (`getDirective` rules)
+
+-- | Look up the final value assigned to a csv rule by rule keyword, taking
+-- into account the current record and conditional rules.
+-- Generally rules with keywords ("directives") don't have interpolated
+-- values, but for now it's possible.
+csvRuleValue :: CsvRules -> CsvRecord -> DirectiveName -> Maybe String
+csvRuleValue rules record = fmap (renderTemplate rules record) . csvRule rules
+
+-- | Look up the value template assigned to a hledger field by field
+-- list/field assignment rules, taking into account the current record and
+-- conditional rules.
+hledgerField :: CsvRules -> CsvRecord -> HledgerFieldName -> Maybe FieldTemplate
+hledgerField = getEffectiveAssignment
+
+-- | Look up the final value assigned to a hledger field, with csv field
+-- references interpolated.
+hledgerFieldValue :: CsvRules -> CsvRecord -> HledgerFieldName -> Maybe String
+hledgerFieldValue rules record = fmap (renderTemplate rules record) . hledgerField rules record
+
+s `withDefault` def = if null s then def else s
+
+transactionFromCsvRecord :: SourcePos -> CsvRules -> CsvRecord -> Transaction
+transactionFromCsvRecord sourcepos rules record = t
+  where
+    ----------------------------------------------------------------------
+    -- 1. Define some helpers:
+
+    rule     = csvRule           rules        :: DirectiveName    -> Maybe FieldTemplate
+    -- ruleval  = csvRuleValue      rules record :: DirectiveName    -> Maybe String
+    field    = hledgerField      rules record :: HledgerFieldName -> Maybe FieldTemplate
+    fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe String
+    parsedate' = parseDateWithCustomOrDefaultFormats (rule "date-format")
+    mkdateerror datefield datevalue mdateformat = unlines
+      ["error: could not parse \""++datevalue++"\" as a date using date format "
+        ++maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" show mdateformat
+      ,showRecord record
+      ,"the "++datefield++" rule is:   "++(fromMaybe "required, but missing" $ field datefield)
+      ,"the date-format is: "++fromMaybe "unspecified" mdateformat
+      ,"you may need to "
+        ++"change your "++datefield++" rule, "
+        ++maybe "add a" (const "change your") mdateformat++" date-format rule, "
+        ++"or "++maybe "add a" (const "change your") mskip++" skip rule"
+      ,"for m/d/y or d/m/y dates, use date-format %-m/%-d/%Y or date-format %-d/%-m/%Y"
+      ]
+      where
+        mskip = rule "skip"
+
+    ----------------------------------------------------------------------
+    -- 2. Gather values needed for the transaction itself, by evaluating the
+    -- field assignment rules using the CSV record's data, and parsing a bit
+    -- more where needed (dates, status).
+
+    mdateformat = rule "date-format"
+    date        = fromMaybe "" $ fieldval "date"
+    date'       = fromMaybe (error' $ mkdateerror "date" date mdateformat) $ parsedate' date
+    mdate2      = fieldval "date2"
+    mdate2'     = maybe Nothing (maybe (error' $ mkdateerror "date2" (fromMaybe "" mdate2) mdateformat) Just . parsedate') mdate2
+    status      =
+      case fieldval "status" of
+        Nothing -> Unmarked
+        Just s  -> either statuserror id $ runParser (statusp <* eof) "" $ T.pack s
+          where
+            statuserror err = error' $ unlines
+              ["error: could not parse \""++s++"\" as a cleared status (should be *, ! or empty)"
+              ,"the parse error is:      "++customErrorBundlePretty err
+              ]
+    code        = maybe "" singleline $ fieldval "code"
+    description = maybe "" singleline $ fieldval "description"
+    comment     = maybe "" singleline $ fieldval "comment"
+    precomment  = maybe "" singleline $ fieldval "precomment"
+
+    ----------------------------------------------------------------------
+    -- 3. Generate the postings
+
+    -- Make posting 1 if possible, with special support for old syntax to
+    -- support pre-1.16 rules.
+    posting1 = mkPosting rules record "1"
+               ("account1" `withAlias` "account")
+               ("amount1" `withAlias` "amount")
+               ("amount1-in" `withAlias` "amount-in")
+               ("amount1-out" `withAlias` "amount-out")
+               ("balance1" `withAlias` "balance")
+               "comment1" -- comment1 does not have legacy alias
+               t
+      where
+        withAlias fld alias =
+          case (field fld, field alias) of
+            (Just fld, Just alias) -> error' $ unlines
+              [ "error: both \"" ++ fld ++ "\" and \"" ++ alias ++ "\" have values."
+              , showRecord record
+              , showRules rules record
+              ]
+            (Nothing, Just _) -> alias
+            (_, Nothing)      -> fld
+
+    -- Make other postings where possible, and gather all that were generated.
+    postings = catMaybes $ posting1 : otherpostings
+      where
+        otherpostings = [mkPostingN i | x<-[2..9], let i = show x]
+          where
+            mkPostingN n = mkPosting rules record n
+                           ("account"++n) ("amount"++n) ("amount"++n++"-in")
+                           ("amount"++n++"-out") ("balance"++n) ("comment"++n) t
+  
+    -- Auto-generate a second posting or second posting amount,
+    -- for compatibility with pre-1.16 rules.
+    postings' =
+      case postings of
+        -- when rules generate just one posting, of a kind that needs to be
+        -- balanced, generate the second posting to balance it.
+        [p1@(p1',_)] ->
+          if ptype p1' == VirtualPosting then [p1] else [p1, p2]
+            where
+              p2 = (nullposting{paccount=unknownExpenseAccount
+                               ,pamount=costOfMixedAmount (-pamount p1')
+                               ,ptransaction=Just t}, False)
+        -- when rules generate exactly two postings, and only the second has
+        -- no amount, give it the balancing amount.
+        [p1@(p1',_), p2@(p2',final2)] ->
+          if hasAmount p1' && not (hasAmount p2')
+          then [p1, (p2'{pamount=costOfMixedAmount(-(pamount p1'))}, final2)]
+          else [p1, p2]
+        --
+        ps -> ps
+
+    -- Finally, wherever default "unknown" accounts were used, refine them
+    -- based on the sign of the posting amount if it's now known.
+    postings'' = map maybeImprove postings'
+      where
+        maybeImprove (p,final) = if final then p else improveUnknownAccountName p
+
+    ----------------------------------------------------------------------
+    -- 4. Build the transaction (and name it, so the postings can reference it).
+
+    t = nulltransaction{
+           tsourcepos        = genericSourcePos sourcepos  -- the CSV line number
+          ,tdate             = date'
+          ,tdate2            = mdate2'
+          ,tstatus           = status
+          ,tcode             = T.pack code
+          ,tdescription      = T.pack description
+          ,tcomment          = T.pack comment
+          ,tprecedingcomment = T.pack precomment
+          ,tpostings         = postings''
+          }  
+
+-- | Given CSV rules and a CSV record, generate the corresponding transaction's
+-- Nth posting, if sufficient fields have been assigned for it.
+-- N is provided as a string.
+-- The names of the required fields are provided, allowing more flexibility.
+-- The transaction which will contain this posting is also provided,
+-- so we can build the usual transaction<->posting cyclic reference.
+mkPosting ::
+  CsvRules -> CsvRecord -> String ->
+  HledgerFieldName -> HledgerFieldName -> HledgerFieldName ->
+  HledgerFieldName -> HledgerFieldName -> HledgerFieldName ->
+  Transaction ->
+  Maybe (Posting, Bool)
+mkPosting rules record number accountFld amountFld amountInFld amountOutFld balanceFld commentFld t =
+  -- if we have figured out an account N, make a posting N
+  case maccountAndIsFinal of
+    Nothing            -> Nothing
+    Just (acct, final) ->
+      Just (posting{paccount          = accountNameWithoutPostingType acct
+                   ,pamount           = fromMaybe missingmixedamt mamount
+                   ,ptransaction      = Just t
+                   ,pbalanceassertion = mkBalanceAssertion rules record <$> mbalance
+                   ,pcomment          = comment
+                   ,ptype             = accountNamePostingType acct}
+           ,final)
+  where
+    -- the account name to use for this posting, if any, and whether it is the
+    -- default unknown account, which may be improved later, or an explicitly
+    -- set account, which may not.
+    maccountAndIsFinal :: Maybe (AccountName, Bool) =
+      case maccount of
+        -- accountN is set to the empty string - no posting will be generated
+        Just "" -> Nothing
+        -- accountN is set (possibly to "expenses:unknown"! #1192) - mark it final
+        Just a  -> Just (a, True)
+        -- accountN is unset
+        Nothing ->
+          case (mamount, mbalance) of
+            -- amountN is set, or implied by balanceN - set accountN to
+            -- the default unknown account ("expenses:unknown") and
+            -- allow it to be improved later
+            (Just _, _) -> Just (unknownExpenseAccount, False)
+            (_, Just _) -> Just (unknownExpenseAccount, False)
+            -- amountN is also unset - no posting will be generated
+            (Nothing, Nothing) -> Nothing
+      where
+        maccount = T.pack <$> (fieldval accountFld
+                              -- XXX what's this needed for ? Test & document, or drop.
+                              -- Also, this the only place we interpolate in a keyword rule, I think.
+                               `withDefault` ruleval ("default-account" ++ number))
+    -- XXX what's this needed for ? Test & document, or drop.
+    mdefaultcurrency = rule "default-currency"
+    currency = fromMaybe (fromMaybe "" mdefaultcurrency) $
+               fieldval ("currency"++number) `withDefault` fieldval "currency"
+    mamount = chooseAmount rules record currency amountFld amountInFld amountOutFld
+    mbalance :: Maybe (Amount, GenericSourcePos) =
+      fieldval balanceFld >>= parsebalance currency number
+      where
+        parsebalance currency n str
+          | all isSpace str = Nothing
+          | otherwise = Just
+              (either (balanceerror n str) id $
+                runParser (evalStateT (amountp <* eof) mempty) "" $
+                T.pack $ (currency++) $ simplifySign str
+              ,nullsourcepos)  -- XXX parse position to show when assertion fails,
+                               -- the csv record's line number would be good
+          where
+            balanceerror n str err = error' $ unlines
+              ["error: could not parse \""++str++"\" as balance"++n++" amount"
+              ,showRecord record
+              ,showRules rules record
+              ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency
+              ,"the parse error is:      "++customErrorBundlePretty err
+              ]
+    comment = T.pack $ fromMaybe "" $ fieldval commentFld
+    rule     = csvRule           rules        :: DirectiveName    -> Maybe FieldTemplate
+    ruleval  = csvRuleValue      rules record :: DirectiveName    -> Maybe String
+    -- field    = hledgerField      rules record :: HledgerFieldName -> Maybe FieldTemplate
+    fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe String
+-- | Default account names to use when needed.
+unknownExpenseAccount = "expenses:unknown"
+unknownIncomeAccount  = "income:unknown"
+
+-- | If this posting has the "expenses:unknown" account name,
+-- replace that with "income:unknown" if the amount is negative.
+-- The posting's amount should be explicit.
+improveUnknownAccountName p@Posting{..}
+  | paccount == unknownExpenseAccount
+    && fromMaybe False (isNegativeMixedAmount pamount) = p{paccount=unknownIncomeAccount}
+  | otherwise = p
+
+-- | Make a balance assertion for the given amount, with the given parse
+-- position (to be shown in assertion failures), with the assertion type
+-- possibly set by a balance-type rule.
+-- The CSV rules and current record are also provided, to be shown in case
+-- balance-type's argument is bad (XXX refactor).
+mkBalanceAssertion :: CsvRules -> CsvRecord -> (Amount, GenericSourcePos) -> BalanceAssertion
+mkBalanceAssertion rules record (amt, pos) = assrt{baamount=amt, baposition=pos}
+  where
+    assrt =
+      case getDirective "balance-type" rules of
+        Nothing    -> nullassertion
+        Just "="   -> nullassertion
+        Just "=="  -> nullassertion{batotal=True}
+        Just "=*"  -> nullassertion{bainclusive=True}
+        Just "==*" -> nullassertion{batotal=True, bainclusive=True}
+        Just x     -> error' $ unlines
+          [ "balance-type \"" ++ x ++"\" is invalid. Use =, ==, =* or ==*."
+          , showRecord record
+          , showRules rules record
+          ]
+
+chooseAmount :: CsvRules -> CsvRecord -> String -> String -> String -> String -> Maybe MixedAmount
+chooseAmount rules record currency amountFld amountInFld amountOutFld =
+ let
+   mamount    = getEffectiveAssignment rules record amountFld
+   mamountin  = getEffectiveAssignment rules record amountInFld
+   mamountout = getEffectiveAssignment rules record amountOutFld
+   parse  amt = notZero =<< (parseAmount currency <$> notEmpty =<< (strip . renderTemplate rules record) <$> amt)
+ in
+  case (parse mamount, parse mamountin, parse mamountout) of
+    (Nothing, Nothing, Nothing) -> Nothing
+    (Just a,  Nothing, Nothing) -> Just a
+    (Nothing, Just i,  Nothing) -> Just i
+    (Nothing, Nothing, Just o)  -> Just $ negate o
+    (Nothing, Just i,  Just o)  -> error' $    "both "++amountInFld++" and "++amountOutFld++" have a value\n"
+                                            ++ "    "++amountInFld++": "  ++ show i ++ "\n"
+                                            ++ "    "++amountOutFld++": " ++ show o ++ "\n"
+                                            ++ "    record: "     ++ showRecord record
+    _                           -> error' $    "found values for "++amountFld++" and for "++amountInFld++"/"++amountOutFld++"\n"
+                                            ++ "please use either "++amountFld++" or "++amountInFld++"/"++amountOutFld++"\n"
+                                            ++ "    record: " ++ showRecord record
+ where
+   notZero amt = if isZeroMixedAmount amt then Nothing else Just amt
+   notEmpty str = if str=="" then Nothing else Just str
+
+   parseAmount currency amountstr =
+     either (amounterror amountstr) (Mixed . (:[]))
+     <$> runParser (evalStateT (amountp <* eof) mempty) ""
+     <$> T.pack
+     <$> (currency++)
+     <$> simplifySign
+     <$> amountstr
+
+   amounterror amountstr err = error' $ unlines
+     ["error: could not parse \""++fromJust amountstr++"\" as an amount"
+     ,showRecord record
+     ,showRules rules record
+     ,"the default-currency is: "++fromMaybe "unspecified" (getDirective "default-currency" rules)
+     ,"the parse error is:      "++customErrorBundlePretty err
+     ,"you may need to "
+      ++"change your amount or currency rules, "
+      ++"or add or change your skip rule"
+     ]
+
+type CsvAmountString = String
+
+-- | Canonicalise the sign in a CSV amount string.
+-- Such strings can have a minus sign, negating parentheses,
+-- or any two of these (which cancels out).
+--
+-- >>> simplifySign "1"
+-- "1"
+-- >>> simplifySign "-1"
+-- "-1"
+-- >>> simplifySign "(1)"
+-- "-1"
+-- >>> simplifySign "--1"
+-- "1"
+-- >>> simplifySign "-(1)"
+-- "1"
+-- >>> simplifySign "(-1)"
+-- "1"
+-- >>> simplifySign "((1))"
+-- "1"
+simplifySign :: CsvAmountString -> CsvAmountString
+simplifySign ('(':s) | lastMay s == Just ')' = simplifySign $ negateStr $ init s
+simplifySign ('-':'(':s) | lastMay s == Just ')' = simplifySign $ init s
+simplifySign ('-':'-':s) = s
+simplifySign s = s
+
+negateStr :: String -> String
+negateStr ('-':s) = s
+negateStr s       = '-':s
+
+-- | Show a (approximate) recreation of the original CSV record.
+showRecord :: CsvRecord -> String
+showRecord r = "the CSV record is:       "++intercalate "," (map show r)
+
+-- | Given the conversion rules, a CSV record and a hledger field name, find
+-- the value template ultimately assigned to this field, if any, by a field
+-- assignment at top level or in a conditional block matching this record.
+--
+-- Note conditional blocks' patterns are matched against an approximation of the
+-- CSV record: all the field values, without enclosing quotes, comma-separated.
+--
+getEffectiveAssignment :: CsvRules -> CsvRecord -> HledgerFieldName -> Maybe FieldTemplate
+getEffectiveAssignment rules record f = lastMay $ map snd $ assignments
+  where
+    -- all active assignments to field f, in order
+    assignments = dbg2 "assignments" $ filter ((==f).fst) $ toplevelassignments ++ conditionalassignments
+      where
+        -- all top level field assignments
+        toplevelassignments    = rassignments rules
+        -- all field assignments in conditional blocks assigning to field f and active for the current csv record
+        conditionalassignments = concatMap cbAssignments $ filter isBlockActive $ blocksAssigning f
+          where
+            -- all conditional blocks which can potentially assign field f
+            blocksAssigning f = filter (any ((==f).fst) . cbAssignments) $ rconditionalblocks rules
+            -- does this conditional block match the current csv record ?
+            isBlockActive :: ConditionalBlock -> Bool
+            isBlockActive CB{..} = any matcherMatches cbMatchers
+              where
+                -- does this individual matcher match the current csv record ?
+                matcherMatches :: Matcher -> Bool
+                matcherMatches (RecordMatcher pat) = regexMatchesCI pat wholecsvline
+                  where
+                    -- a synthetic whole CSV record to match against; note, it has
+                    -- no quotes enclosing fields, and is always comma-separated,
+                    -- so may differ from the actual record, and may not be valid CSV.
+                    wholecsvline = dbg3 "wholecsvline" $ intercalate "," record
+                matcherMatches (FieldMatcher csvfieldref pat) = regexMatchesCI pat csvfieldvalue
+                  where
+                    -- the value of the referenced CSV field to match against.
+                    csvfieldvalue = dbg3 "csvfieldvalue" $ replaceCsvFieldReference rules record csvfieldref
+
+-- | Render a field assigment's template, possibly interpolating referenced
+-- CSV field values. Outer whitespace is removed from interpolated values.
+renderTemplate ::  CsvRules -> CsvRecord -> FieldTemplate -> String
+renderTemplate rules record t = regexReplaceBy "%[A-z0-9_-]+" (replaceCsvFieldReference rules record) t
+
+-- | Replace something that looks like a reference to a csv field ("%date" or "%1)
+-- with that field's value. If it doesn't look like a field reference, or if we
+-- can't find such a field, leave it unchanged.
+replaceCsvFieldReference :: CsvRules -> CsvRecord -> CsvFieldReference -> String
+replaceCsvFieldReference rules record s@('%':fieldname) = fromMaybe s $ csvFieldValue rules record fieldname
+replaceCsvFieldReference _ _ s = s
+
+-- | Get the (whitespace-stripped) value of a CSV field, identified by its name or
+-- column number, ("date" or "1"), from the given CSV record, if such a field exists.
+csvFieldValue :: CsvRules -> CsvRecord -> CsvFieldName -> Maybe String
+csvFieldValue rules record fieldname = do
+  fieldindex <- if | all isDigit fieldname -> readMay fieldname
+                   | otherwise             -> lookup (map toLower fieldname) $ rcsvfieldindexes rules
+  fieldvalue <- strip <$> atMay record (fieldindex-1)
+  return fieldvalue
+
+-- | Parse the date string using the specified date-format, or if unspecified
+-- the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, leading
+-- zeroes optional).
+parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> String -> Maybe Day
+parseDateWithCustomOrDefaultFormats mformat s = firstJust $ map parsewith formats
+  where
+    parsetime =
+#if MIN_VERSION_time(1,5,0)
+     parseTimeM True
+#else
+     parseTime
+#endif
+    parsewith = flip (parsetime defaultTimeLocale) s
+    formats = maybe
+               ["%Y/%-m/%-d"
+               ,"%Y-%-m-%-d"
+               ,"%Y.%-m.%-d"
+               -- ,"%-m/%-d/%Y"
+                -- ,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)
+                -- ,parseTime defaultTimeLocale "%Y-%m-%e" (take 5 s ++ "0" ++ drop 5 s)
+                -- ,parseTime defaultTimeLocale "%m/%e/%Y" ('0':s)
+                -- ,parseTime defaultTimeLocale "%m-%e-%Y" ('0':s)
+               ]
+               (:[])
+                mformat
+
+-- ** tests
+
+tests_CsvReader = tests "CsvReader" [
+   tests "parseCsvRules" [
+     test "empty file" $
+      parseCsvRules "unknown" "" @?= Right defrules
+   ]
+  ,tests "rulesp" [
+     test "trailing comments" $
+      parseWithState' defrules rulesp "skip\n# \n#\n" @?= Right defrules{rdirectives = [("skip","")]}
+
+    ,test "trailing blank lines" $
+      parseWithState' defrules rulesp "skip\n\n  \n" @?= (Right defrules{rdirectives = [("skip","")]})
+
+    ,test "no final newline" $
+      parseWithState' defrules rulesp "skip" @?= (Right defrules{rdirectives=[("skip","")]})
+
+    ,test "assignment with empty value" $
+      parseWithState' defrules rulesp "account1 \nif foo\n  account2 foo\n" @?=
+        (Right defrules{rassignments = [("account1","")], rconditionalblocks = [CB{cbMatchers=[RecordMatcher "foo"],cbAssignments=[("account2","foo")]}]})
+   ]
+  ,tests "conditionalblockp" [
+    test "space after conditional" $ -- #1120
+      parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=
+        (Right $ CB{cbMatchers=[RecordMatcher "a"],cbAssignments=[("account2","b")]})
+
+  ,tests "csvfieldreferencep" [
+    test "number" $ parseWithState' defrules csvfieldreferencep "%1" @?= (Right "%1")
+   ,test "name" $ parseWithState' defrules csvfieldreferencep "%date" @?= (Right "%date")
+   ,test "quoted name" $ parseWithState' defrules csvfieldreferencep "%\"csv date\"" @?= (Right "%\"csv date\"")
+   ]
+
+  ,tests "matcherp" [
+
+    test "recordmatcherp" $
+      parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher "A A")
+
+   ,test "fieldmatcherp.starts-with-%" $
+      parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher "description A A")
+
+   ,test "fieldmatcherp" $
+      parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher "%description" "A A")
+
+   -- ,test "fieldmatcherp with operator" $
+   --    parseWithState' defrules matcherp "%description ~ A A\n" @?= (Right $ FieldMatcher "%description" "A A")
+
+   ]
+
+  ,tests "getEffectiveAssignment" [
+    let rules = defrules{rcsvfieldindexes=[("csvdate",1)],rassignments=[("date","%csvdate")]}
+    
+    in test "toplevel" $ getEffectiveAssignment rules ["a","b"] "date" @?= (Just "%csvdate")
+
+   ,let rules = defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher "%csvdate" "a"] [("date","%csvdate")]]}
+    in test "conditional" $ getEffectiveAssignment rules ["a","b"] "date" @?= (Just "%csvdate")
+       
+   ]
+
+  ]
+
+ ]
diff --git a/Hledger/Read/JournalReader.hs b/Hledger/Read/JournalReader.hs
--- a/Hledger/Read/JournalReader.hs
+++ b/Hledger/Read/JournalReader.hs
@@ -1,10 +1,6 @@
---- * doc
--- Lines beginning "--- *" are collapsible orgstruct nodes. Emacs users,
--- (add-hook 'haskell-mode-hook
---   (lambda () (set-variable 'orgstruct-heading-prefix-regexp "--- " t))
---   'orgstruct-mode)
--- and press TAB on nodes to expand/collapse.
-
+-- * -*- eval: (orgstruct-mode 1); orgstruct-heading-prefix-regexp:"-- "; -*-
+-- ** doc
+-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
 {-|
 
 A reader for hledger's journal file format
@@ -21,19 +17,36 @@
 
 Journal format supports the include directive which can read files in
 other formats, so the other file format readers need to be importable
-here.  Some low-level journal syntax parsers which those readers also
-use are therefore defined separately in Hledger.Read.Common, avoiding
-import cycles.
+and invocable here.
 
+Some important parts of journal parsing are therefore kept in
+Hledger.Read.Common, to avoid import cycles.
+
 -}
 
---- * module
+-- ** language
 
-{-# LANGUAGE CPP, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings, PackageImports #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE NoMonoLocalBinds #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
 
+-- ** doctest setup
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+-- ** exports
 module Hledger.Read.JournalReader (
---- * exports
 
+  -- * Reader-finding utils
+  findReader,
+  splitReaderPrefix,
+  
   -- * Reader
   reader,
 
@@ -62,7 +75,8 @@
   ,tests_JournalReader
 )
 where
---- * imports
+
+-- ** imports
 -- import qualified Prelude (fail)
 -- import "base-compat-batteries" Prelude.Compat hiding (fail, readFile)
 import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (fail)
@@ -79,6 +93,7 @@
 import Data.Text (Text)
 import Data.String
 import Data.List
+import Data.Maybe
 import qualified Data.Text as T
 import Data.Time.Calendar
 import Data.Time.LocalTime
@@ -92,21 +107,64 @@
 
 import Hledger.Data
 import Hledger.Read.Common
-import Hledger.Read.TimeclockReader (timeclockfilep)
-import Hledger.Read.TimedotReader (timedotfilep)
 import Hledger.Utils
 
--- $setup
--- >>> :set -XOverloadedStrings
+import qualified Hledger.Read.TimedotReader as TimedotReader (reader)
+import qualified Hledger.Read.TimeclockReader as TimeclockReader (reader)
+import qualified Hledger.Read.CsvReader as CsvReader (reader)
 
---- * reader
+-- ** reader finding utilities
+-- Defined here rather than Hledger.Read so that we can use them in includedirectivep below.
 
-reader :: Reader
+-- The available journal readers, each one handling a particular data format.
+readers' :: MonadIO m => [Reader m]
+readers' = [
+  reader
+ ,TimeclockReader.reader
+ ,TimedotReader.reader
+ ,CsvReader.reader
+--  ,LedgerReader.reader
+ ]
+
+readerNames :: [String]
+readerNames = map rFormat (readers'::[Reader IO])
+
+-- | @findReader mformat mpath@
+--
+-- Find the reader named by @mformat@, if provided.
+-- Or, if a file path is provided, find the first reader that handles
+-- its file extension, if any.
+findReader :: MonadIO m => Maybe StorageFormat -> Maybe FilePath -> Maybe (Reader m)
+findReader Nothing Nothing     = Nothing
+findReader (Just fmt) _        = headMay [r | r <- readers', rFormat r == fmt]
+findReader Nothing (Just path) =
+  case prefix of
+    Just fmt -> headMay [r | r <- readers', rFormat r == fmt]
+    Nothing  -> headMay [r | r <- readers', ext `elem` rExtensions r]
+  where
+    (prefix,path') = splitReaderPrefix path
+    ext            = drop 1 $ takeExtension path'
+
+-- | A file path optionally prefixed by a reader name and colon
+-- (journal:, csv:, timedot:, etc.).
+type PrefixedFilePath = FilePath
+
+-- | If a filepath is prefixed by one of the reader names and a colon,
+-- split that off. Eg "csv:-" -> (Just "csv", "-").
+splitReaderPrefix :: PrefixedFilePath -> (Maybe String, FilePath)
+splitReaderPrefix f =
+  headDef (Nothing, f)
+  [(Just r, drop (length r + 1) f) | r <- readerNames, (r++":") `isPrefixOf` f]
+
+-- ** reader
+
+reader :: MonadIO m => Reader m
 reader = Reader
   {rFormat     = "journal"
   ,rExtensions = ["journal", "j", "hledger", "ledger"]
-  ,rParser     = parse
-  ,rExperimental = False
+  ,rReadFn     = parse
+  ,rParser    = journalp  -- no need to add command line aliases like journalp'
+                           -- when called as a subparser I think
   }
 
 -- | Parse and post-process a "Journal" from hledger's journal file
@@ -124,8 +182,8 @@
 aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp ("--alias "++quoteIfNeeded a) $ T.pack a)
                   . aliases_
 
---- * parsers
---- ** journal
+-- ** parsers
+-- *** journal
 
 -- | A journal parser. Accumulates and returns a "ParsedJournal",
 -- which should be finalised/validated before use.
@@ -155,7 +213,7 @@
     , void (lift multilinecommentp)
     ] <?> "transaction or directive"
 
---- ** directives
+-- *** directives
 
 -- | Parse any journal directive and update the parse state accordingly.
 -- Cf http://hledger.org/manual.html#directives,
@@ -227,11 +285,11 @@
                             `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)
       let initChildj = newJournalWithParseStateFrom filepath parentj
 
-      let parser = choiceInState
-            [ journalp
-            , timeclockfilep
-            , timedotfilep
-            ] -- can't include a csv file yet, that reader is special
+      -- Choose a reader/format based on the file path, or fall back
+      -- on journal. Duplicating readJournal a bit here.
+      let r = fromMaybe reader $ findReader Nothing (Just filepath)
+          parser = rParser r
+      dbg1IO "trying reader" (rFormat r)
       updatedChildj <- journalAddFile (filepath, childInput) <$>
                         parseIncludeFile parser initChildj filepath childInput
 
@@ -525,8 +583,9 @@
   lift restofline
   return ()
 
---- ** transactions
+-- *** transactions
 
+-- | Parse a transaction modifier (auto postings) rule.
 transactionmodifierp :: JournalParser m TransactionModifier
 transactionmodifierp = do
   char '=' <?> "modifier transaction"
@@ -536,7 +595,7 @@
   postings <- postingsp Nothing
   return $ TransactionModifier querytxt postings
 
--- | Parse a periodic transaction
+-- | Parse a periodic transaction rule.
 --
 -- This reuses periodexprp which parses period expressions on the command line.
 -- This is awkward because periodexprp supports relative and partial dates,
@@ -621,7 +680,7 @@
   let sourcepos = journalSourcePos startpos endpos
   return $ txnTieKnot $ Transaction 0 "" sourcepos date edate status code description comment tags postings
 
---- ** postings
+-- *** postings
 
 -- Parse the following whitespace-beginning lines as postings, posting
 -- tags, and/or comments (inferring year, if needed, from the given date).
@@ -664,7 +723,7 @@
    , pbalanceassertion=massertion
    }
 
---- * tests
+-- ** tests
 
 tests_JournalReader = tests "JournalReader" [
 
diff --git a/Hledger/Read/TimeclockReader.hs b/Hledger/Read/TimeclockReader.hs
--- a/Hledger/Read/TimeclockReader.hs
+++ b/Hledger/Read/TimeclockReader.hs
@@ -1,3 +1,6 @@
+-- * -*- eval: (orgstruct-mode 1); orgstruct-heading-prefix-regexp:"-- "; -*-
+-- ** doc
+-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
 {-|
 
 A reader for the timeclock file format generated by timeclock.el
@@ -40,8 +43,15 @@
 
 -}
 
-{-# LANGUAGE OverloadedStrings, PackageImports #-}
+-- ** language
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
 
+-- ** doctest setup
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+-- ** exports
 module Hledger.Read.TimeclockReader (
   -- * Reader
   reader,
@@ -49,6 +59,8 @@
   timeclockfilep,
 )
 where
+
+-- ** imports
 import           Prelude ()
 import "base-compat-batteries" Prelude.Compat
 import           Control.Monad
@@ -64,13 +76,14 @@
 import           Hledger.Read.Common
 import           Hledger.Utils
 
+-- ** reader
 
-reader :: Reader
+reader :: MonadIO m => Reader m
 reader = Reader
   {rFormat     = "timeclock"
   ,rExtensions = ["timeclock"]
-  ,rParser     = parse
-  ,rExperimental = False
+  ,rReadFn     = parse
+  ,rParser    = timeclockfilep
   }
 
 -- | Parse and post-process a "Journal" from timeclock.el's timeclock
@@ -79,6 +92,8 @@
 parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
 parse = parseAndFinaliseJournal' timeclockfilep
 
+-- ** parsers
+
 timeclockfilep :: MonadIO m => JournalParser m ParsedJournal
 timeclockfilep = do many timeclockitemp
                     eof
@@ -99,7 +114,7 @@
       timeclockitemp = choice [
                             void (lift emptyorcommentlinep)
                           , timeclockentryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j})
-                          ] <?> "timeclock entry, or default year or historical price directive"
+                          ] <?> "timeclock entry, comment line, or empty line"
 
 -- | Parse a timeclock entry.
 timeclockentryp :: JournalParser m TimeclockEntry
diff --git a/Hledger/Read/TimedotReader.hs b/Hledger/Read/TimedotReader.hs
--- a/Hledger/Read/TimedotReader.hs
+++ b/Hledger/Read/TimedotReader.hs
@@ -1,3 +1,6 @@
+-- * -*- eval: (orgstruct-mode 1); orgstruct-heading-prefix-regexp:"-- "; -*-
+-- ** doc
+-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
 {-|
 
 A reader for the "timedot" file format.
@@ -23,8 +26,15 @@
 
 -}
 
-{-# LANGUAGE OverloadedStrings, PackageImports #-}
+-- ** language
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
 
+-- ** doctest setup
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+-- ** exports
 module Hledger.Read.TimedotReader (
   -- * Reader
   reader,
@@ -32,6 +42,8 @@
   timedotfilep,
 )
 where
+
+-- ** imports
 import Prelude ()
 import "base-compat-batteries" Prelude.Compat
 import Control.Monad
@@ -39,78 +51,131 @@
 import Control.Monad.State.Strict
 import Data.Char (isSpace)
 import Data.List (foldl')
-import Data.Maybe
 import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time (Day)
 import Text.Megaparsec hiding (parse)
 import Text.Megaparsec.Char
 
 import Hledger.Data
-import Hledger.Read.Common
-import Hledger.Utils hiding (traceParse)
+import Hledger.Read.Common hiding (emptyorcommentlinep)
+import Hledger.Utils
 
--- easier to toggle this here sometimes
--- import qualified Hledger.Utils (parsertrace)
--- parsertrace = Hledger.Utils.parsertrace
-traceParse :: Monad m => a -> m a
-traceParse = return
+-- ** reader
 
-reader :: Reader
+reader :: MonadIO m => Reader m
 reader = Reader
   {rFormat     = "timedot"
   ,rExtensions = ["timedot"]
-  ,rParser     = parse
-  ,rExperimental = False
+  ,rReadFn     = parse
+  ,rParser    = timedotp
   }
 
 -- | Parse and post-process a "Journal" from the timedot format, or give an error.
 parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
-parse = parseAndFinaliseJournal' timedotfilep
+parse = parseAndFinaliseJournal' timedotp
 
-timedotfilep :: JournalParser m ParsedJournal
-timedotfilep = do many timedotfileitemp
-                  eof
-                  get
-    where
-      timedotfileitemp :: JournalParser m ()
-      timedotfileitemp = do
-        traceParse "timedotfileitemp"
-        choice [
-          void $ lift emptyorcommentlinep
-         ,timedotdayp >>= \ts -> modify' (addTransactions ts)
-         ] <?> "timedot day entry, or default year or comment line or blank line"
+-- ** utilities
 
-addTransactions :: [Transaction] -> Journal -> Journal
-addTransactions ts j = foldl' (flip ($)) j (map addTransaction ts)
+traceparse, traceparse' :: String -> TextParser m ()
+traceparse  = const $ return ()
+traceparse' = const $ return ()
+-- for debugging:
+-- traceparse  s = traceParse (s++"?")
+-- traceparse' s = trace s $ return ()
 
+-- ** parsers
+{-
+Rough grammar for timedot format:
+
+timedot:           preamble day*
+preamble:          (emptyline | commentline | orgheading)*
+orgheading:        orgheadingprefix restofline
+day:               dateline entry* (emptyline | commentline)*
+dateline:          orgheadingprefix? date description?
+orgheadingprefix:  star+ space+
+description:       restofline  ; till semicolon?
+entry:          orgheadingprefix? space* singlespaced (doublespace quantity?)?
+doublespace:       space space+
+quantity:          (dot (dot | space)* | number | number unit)
+
+Date lines and item lines can begin with an org heading prefix, which is ignored.
+Org headings before the first date line are ignored, regardless of content.
+-}
+
+timedotfilep = timedotp -- XXX rename export above
+
+timedotp :: JournalParser m ParsedJournal
+timedotp = preamblep >> many dayp >> eof >> get
+
+preamblep :: JournalParser m ()
+preamblep = do
+  lift $ traceparse "preamblep"
+  many $ notFollowedBy datelinep >> (lift $ emptyorcommentlinep "#;*")
+  lift $ traceparse' "preamblep"
+
 -- | Parse timedot day entries to zero or more time transactions for that day.
 -- @
--- 2/1
+-- 2020/2/1 optional day description
 -- fos.haskell  .... ..
 -- biz.research .
 -- inc.client1  .... .... .... .... .... ....
 -- @
-timedotdayp :: JournalParser m [Transaction]
-timedotdayp = do
-  traceParse " timedotdayp"
-  d <- datep <* lift eolof
-  es <- catMaybes <$> many (const Nothing <$> try (lift emptyorcommentlinep) <|>
-                            Just <$> (notFollowedBy datep >> timedotentryp))
-  return $ map (\t -> t{tdate=d}) es -- <$> many timedotentryp
+dayp :: JournalParser m ()
+dayp = label "timedot day entry" $ do
+  lift $ traceparse "dayp"
+  (d,desc) <- datelinep
+  commentlinesp
+  ts <- many $ entryp <* commentlinesp
+  modify' $ addTransactions $ map (\t -> t{tdate=d, tdescription=desc}) ts
+  lift $ traceparse' "dayp"
+  where
+    addTransactions :: [Transaction] -> Journal -> Journal
+    addTransactions ts j = foldl' (flip ($)) j (map addTransaction ts)
 
+datelinep :: JournalParser m (Day,Text)
+datelinep = do
+  lift $ traceparse "datelinep"
+  lift $ optional orgheadingprefixp
+  d <- datep
+  desc <- strip <$> lift restofline
+  lift $ traceparse' "datelinep"
+  return (d, T.pack desc)
+
+-- | Zero or more empty lines or hash/semicolon comment lines
+-- or org headlines which do not start a new day.
+commentlinesp :: JournalParser m ()
+commentlinesp = do
+  lift $ traceparse "commentlinesp"
+  void $ many $ try $ lift $ emptyorcommentlinep "#;"
+
+-- orgnondatelinep :: JournalParser m ()
+-- orgnondatelinep = do
+--   lift $ traceparse "orgnondatelinep"
+--   lift orgheadingprefixp
+--   notFollowedBy datelinep
+--   void $ lift restofline
+--   lift $ traceparse' "orgnondatelinep"
+
+orgheadingprefixp = do
+  -- traceparse "orgheadingprefixp"
+  skipSome (char '*') >> skipSome spacenonewline
+
 -- | Parse a single timedot entry to one (dateless) transaction.
 -- @
 -- fos.haskell  .... ..
 -- @
-timedotentryp :: JournalParser m Transaction
-timedotentryp = do
-  traceParse "  timedotentryp"
+entryp :: JournalParser m Transaction
+entryp = do
+  lift $ traceparse "entryp"
   pos <- genericSourcePos <$> getSourcePos
-  lift (skipMany spacenonewline)
+  notFollowedBy datelinep
+  lift $ optional $ choice [orgheadingprefixp, skipSome spacenonewline]
   a <- modifiedaccountnamep
   lift (skipMany spacenonewline)
   hours <-
     try (lift followingcommentp >> return 0)
-    <|> (timedotdurationp <*
+    <|> (durationp <*
          (try (lift followingcommentp) <|> (newline >> return "")))
   let t = nulltransaction{
         tsourcepos = pos,
@@ -123,10 +188,14 @@
                      }
           ]
         }
+  lift $ traceparse' "entryp"
   return t
 
-timedotdurationp :: JournalParser m Quantity
-timedotdurationp = try timedotnumericp <|> timedotdotsp
+durationp :: JournalParser m Quantity
+durationp = do
+  lift $ traceparse "durationp"
+  try numericquantityp <|> dotquantityp
+    -- <* traceparse' "durationp"
 
 -- | Parse a duration of seconds, minutes, hours, days, weeks, months or years,
 -- written as a decimal number followed by s, m, h, d, w, mo or y, assuming h
@@ -137,8 +206,9 @@
 -- 1.5h
 -- 90m
 -- @
-timedotnumericp :: JournalParser m Quantity
-timedotnumericp = do
+numericquantityp :: JournalParser m Quantity
+numericquantityp = do
+  -- lift $ traceparse "numericquantityp"
   (q, _, _, _) <- lift $ numberp Nothing
   msymbol <- optional $ choice $ map (string . fst) timeUnits
   lift (skipMany spacenonewline)
@@ -166,7 +236,24 @@
 -- @
 -- .... ..
 -- @
-timedotdotsp :: JournalParser m Quantity
-timedotdotsp = do
+dotquantityp :: JournalParser m Quantity
+dotquantityp = do
+  -- lift $ traceparse "dotquantityp"
   dots <- filter (not.isSpace) <$> many (oneOf (". " :: [Char]))
   return $ (/4) $ fromIntegral $ length dots
+
+-- | XXX new comment line parser, move to Hledger.Read.Common.emptyorcommentlinep
+-- Parse empty lines, all-blank lines, and lines beginning with any of the provided
+-- comment-beginning characters.
+emptyorcommentlinep :: [Char] -> TextParser m ()
+emptyorcommentlinep cs =
+  label ("empty line or comment line beginning with "++cs) $ do
+    traceparse "emptyorcommentlinep" -- XXX possible to combine label and traceparse ?
+    skipMany spacenonewline
+    void newline <|> void commentp
+    traceparse' "emptyorcommentlinep"
+    where
+      commentp = do
+        choice (map (some.char) cs)
+        takeWhileP Nothing (/='\n') <* newline
+
diff --git a/Hledger/Reports/BalanceReport.hs b/Hledger/Reports/BalanceReport.hs
--- a/Hledger/Reports/BalanceReport.hs
+++ b/Hledger/Reports/BalanceReport.hs
@@ -66,7 +66,7 @@
 -- This is like PeriodChangeReport with a single column (but more mature,
 -- eg this can do hierarchical display).
 balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
-balanceReport ropts@ReportOpts{..} q j@Journal{..} =
+balanceReport ropts@ReportOpts{..} q j =
   (if invert_ then brNegate  else id) $
   (mappedsorteditems, mappedtotal)
     where
diff --git a/Hledger/Reports/BudgetReport.hs b/Hledger/Reports/BudgetReport.hs
--- a/Hledger/Reports/BudgetReport.hs
+++ b/Hledger/Reports/BudgetReport.hs
@@ -11,6 +11,7 @@
 
 import Data.Decimal
 import Data.List
+import Data.List.Extra (nubSort)
 import Data.Maybe
 #if !(MIN_VERSION_base(4,11,0))
 import Data.Monoid ((<>))
@@ -40,26 +41,14 @@
 import Hledger.Reports.MultiBalanceReport
 
 
--- for reference:
---
---type MultiBalanceReportRow    = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
---type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)
---
---type PeriodicReportRow a =
---  ( AccountName  -- ^ A full account name.
---  , [a]          -- ^ The data value for each subperiod.
---  , a            -- ^ The total of this row's values.
---  , a            -- ^ The average of this row's values.
---  )
-
 type BudgetGoal    = Change
 type BudgetTotal   = Total
 type BudgetAverage = Average
 
 -- | A budget report tracks expected and actual changes per account and subperiod.
 type BudgetCell = (Maybe Change, Maybe BudgetGoal)
-type BudgetReport = PeriodicReport BudgetCell
-type BudgetReportRow = PeriodicReportRow BudgetCell
+type BudgetReport = PeriodicReport AccountName BudgetCell
+type BudgetReportRow = PeriodicReportRow AccountName BudgetCell
 
 -- | Calculate budget goals from all periodic transactions,
 -- actual balance changes from the regular transactions,
@@ -79,17 +68,19 @@
       concatMap expandAccountName $
       accountNamesFromPostings $
       concatMap tpostings $
-      concatMap (flip runPeriodicTransaction reportspan) $
+      concatMap (`runPeriodicTransaction` reportspan) $
       jperiodictxns j
     actualj = dbg1With (("actualj"++).show.jtxns)  $ budgetRollUp budgetedaccts showunbudgeted j
     budgetj = dbg1With (("budgetj"++).show.jtxns)  $ budgetJournal assrt ropts reportspan j
-    actualreport@(MultiBalanceReport (actualspans, _, _)) = dbg1 "actualreport" $ multiBalanceReport ropts  q actualj
-    budgetgoalreport@(MultiBalanceReport (_, budgetgoalitems, budgetgoaltotals)) = dbg1 "budgetgoalreport" $ multiBalanceReport (ropts{empty_=True}) q budgetj
+    actualreport@(PeriodicReport actualspans _ _) =
+        dbg1 "actualreport" $ multiBalanceReport ropts q actualj
+    budgetgoalreport@(PeriodicReport _ budgetgoalitems budgetgoaltotals) =
+        dbg1 "budgetgoalreport" $ multiBalanceReport (ropts{empty_=True}) q budgetj
     budgetgoalreport'
       -- If no interval is specified:
       -- budgetgoalreport's span might be shorter actualreport's due to periodic txns;
       -- it should be safe to replace it with the latter, so they combine well.
-      | interval_ ropts == NoInterval = MultiBalanceReport (actualspans, budgetgoalitems, budgetgoaltotals)
+      | interval_ ropts == NoInterval = PeriodicReport actualspans budgetgoalitems budgetgoaltotals
       | otherwise = budgetgoalreport
     budgetreport = combineBudgetAndActual budgetgoalreport' actualreport
     sortedbudgetreport = sortBudgetReport ropts j budgetreport
@@ -98,7 +89,7 @@
 
 -- | Sort a budget report's rows according to options.
 sortBudgetReport :: ReportOpts -> Journal -> BudgetReport -> BudgetReport
-sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, sortedrows, trow)
+sortBudgetReport ropts j (PeriodicReport ps rows trow) = PeriodicReport ps sortedrows trow
   where
     sortedrows
       | sort_amount_ ropts && tree_ ropts = sortTreeBURByActualAmount rows
@@ -109,9 +100,9 @@
     sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
     sortTreeBURByActualAmount rows = sortedrows
       where
-        anamesandrows = [(first6 r, r) | r <- rows]
+        anamesandrows = [(prrName r, r) | r <- rows]
         anames = map fst anamesandrows
-        atotals = [(a,tot) | (a,_,_,_,(tot,_),_) <- rows]
+        atotals = [(a, tot) | PeriodicReportRow a _ _ (tot,_) _ <- rows]
         accounttree = accountTree "root" anames
         accounttreewithbals = mapAccounts setibalance accounttree
           where
@@ -126,16 +117,16 @@
 
     -- Sort a flat-mode budget report's rows by total actual amount.
     sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
-    sortFlatBURByActualAmount = sortBy (maybeflip $ comparing (fst . fifth6))
-      where
-        maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip
+    sortFlatBURByActualAmount = case normalbalance_ ropts of
+        Just NormallyNegative -> sortOn (fst . prrTotal)
+        _                     -> sortOn (Down . fst . prrTotal)
 
     -- Sort the report rows by account declaration order then account name.
     -- <unbudgeted> remains at the top.
     sortByAccountDeclaration rows = sortedrows
       where
-        (unbudgetedrow,rows') = partition ((=="<unbudgeted>").first6) rows
-        anamesandrows = [(first6 r, r) | r <- rows']
+        (unbudgetedrow,rows') = partition ((=="<unbudgeted>") . prrName) rows
+        anamesandrows = [(prrName r, r) | r <- rows']
         anames = map fst anamesandrows
         sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames
         sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows
@@ -199,85 +190,70 @@
 --
 combineBudgetAndActual :: MultiBalanceReport -> MultiBalanceReport -> BudgetReport
 combineBudgetAndActual
-  (MultiBalanceReport (budgetperiods, budgetrows, (budgettots, budgetgrandtot, budgetgrandavg)))
-  (MultiBalanceReport (actualperiods, actualrows, (actualtots, actualgrandtot, actualgrandavg))) =
-  let
-    periods = nub $ sort $ filter (/= nulldatespan) $ budgetperiods ++ actualperiods
+      (PeriodicReport budgetperiods budgetrows (PeriodicReportRow _ _ budgettots budgetgrandtot budgetgrandavg))
+      (PeriodicReport actualperiods actualrows (PeriodicReportRow _ _ actualtots actualgrandtot actualgrandavg)) =
+    PeriodicReport periods rows totalrow
+  where
+    periods = nubSort . filter (/= nulldatespan) $ budgetperiods ++ actualperiods
 
     -- first, combine any corresponding budget goals with actual changes
     rows1 =
-      [ (acct, treeacct, treeindent, amtandgoals, totamtandgoal, avgamtandgoal)
-      | (acct, treeacct, treeindent, actualamts, actualtot, actualavg) <- actualrows
+      [ PeriodicReportRow acct treeindent amtandgoals totamtandgoal avgamtandgoal
+      | PeriodicReportRow acct treeindent actualamts actualtot actualavg <- actualrows
       , let mbudgetgoals       = Map.lookup acct budgetGoalsByAcct :: Maybe ([BudgetGoal], BudgetTotal, BudgetAverage)
       , let budgetmamts        = maybe (replicate (length periods) Nothing) (map Just . first3) mbudgetgoals :: [Maybe BudgetGoal]
-      , let mbudgettot         = maybe Nothing (Just . second3) mbudgetgoals :: Maybe BudgetTotal
-      , let mbudgetavg         = maybe Nothing (Just . third3)  mbudgetgoals :: Maybe BudgetAverage
+      , let mbudgettot         = second3 <$> mbudgetgoals :: Maybe BudgetTotal
+      , let mbudgetavg         = third3 <$> mbudgetgoals  :: Maybe BudgetAverage
       , let acctBudgetByPeriod = Map.fromList [ (p,budgetamt) | (p, Just budgetamt) <- zip budgetperiods budgetmamts ] :: Map DateSpan BudgetGoal
       , let acctActualByPeriod = Map.fromList [ (p,actualamt) | (p, Just actualamt) <- zip actualperiods (map Just actualamts) ] :: Map DateSpan Change
-      , let amtandgoals        = [ (Map.lookup p acctActualByPeriod, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [(Maybe Change, Maybe BudgetGoal)]
+      , let amtandgoals        = [ (Map.lookup p acctActualByPeriod, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [BudgetCell]
       , let totamtandgoal      = (Just actualtot, mbudgettot)
       , let avgamtandgoal      = (Just actualavg, mbudgetavg)
       ]
       where
         budgetGoalsByAcct :: Map AccountName ([BudgetGoal], BudgetTotal, BudgetAverage) =
-          Map.fromList [ (acct, (amts, tot, avg)) | (acct, _, _, amts, tot, avg) <- budgetrows ]
+          Map.fromList [ (acct, (amts, tot, avg))
+                         | PeriodicReportRow acct _ amts tot avg <- budgetrows ]
 
     -- next, make rows for budget goals with no actual changes
     rows2 =
-      [ (acct, treeacct, treeindent, amtandgoals, totamtandgoal, avgamtandgoal)
-      | (acct, treeacct, treeindent, budgetgoals, budgettot, budgetavg) <- budgetrows
-      , not $ acct `elem` acctsdone
+      [ PeriodicReportRow acct treeindent amtandgoals totamtandgoal avgamtandgoal
+      | PeriodicReportRow acct treeindent budgetgoals budgettot budgetavg <- budgetrows
+      , acct `notElem` map prrName rows1
       , let acctBudgetByPeriod = Map.fromList $ zip budgetperiods budgetgoals :: Map DateSpan BudgetGoal
-      , let amtandgoals        = [ (Nothing, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [(Maybe Change, Maybe BudgetGoal)]
+      , let amtandgoals        = [ (Nothing, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [BudgetCell]
       , let totamtandgoal      = (Nothing, Just budgettot)
       , let avgamtandgoal      = (Nothing, Just budgetavg)
       ]
-      where
-        acctsdone = map first6 rows1
 
     -- combine and re-sort rows
     -- TODO: use MBR code
     -- TODO: respect --sort-amount
     -- TODO: add --sort-budget to sort by budget goal amount
-    rows :: [PeriodicReportRow (Maybe Change, Maybe BudgetGoal)] =
-      sortBy (comparing first6) $ rows1 ++ rows2
+    rows :: [BudgetReportRow] =
+      sortOn prrName $ rows1 ++ rows2
 
     -- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells
-    totalrow =
-      ( ""
-      , ""
-      , 0
-      , [ (Map.lookup p totActualByPeriod, Map.lookup p totBudgetByPeriod) | p <- periods ] :: [(Maybe Total, Maybe BudgetTotal)]
-      , ( Just actualgrandtot, Just budgetgrandtot ) :: (Maybe Total, Maybe BudgetTotal)
-      , ( Just actualgrandavg, Just budgetgrandavg ) :: (Maybe Total, Maybe BudgetTotal)
-      )
+    totalrow = PeriodicReportRow () 0
+        [ (Map.lookup p totActualByPeriod, Map.lookup p totBudgetByPeriod) | p <- periods ]
+        ( Just actualgrandtot, Just budgetgrandtot )
+        ( Just actualgrandavg, Just budgetgrandavg )
       where
         totBudgetByPeriod = Map.fromList $ zip budgetperiods budgettots :: Map DateSpan BudgetTotal
         totActualByPeriod = Map.fromList $ zip actualperiods actualtots :: Map DateSpan Change
 
-  in
-    PeriodicReport
-      ( periods
-      , rows
-      , totalrow
-      )
-
--- | Figure out the overall period of a BudgetReport.
-budgetReportSpan :: BudgetReport -> DateSpan
-budgetReportSpan (PeriodicReport ([], _, _))    = DateSpan Nothing Nothing
-budgetReportSpan (PeriodicReport (spans, _, _)) = DateSpan (spanStart $ head spans) (spanEnd $ last spans)
-
 -- | Render a budget report as plain text suitable for console output.
 budgetReportAsText :: ReportOpts -> BudgetReport -> String
-budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) =
+budgetReportAsText ropts@ReportOpts{..} budgetr =
   title ++ "\n\n" ++
   tableAsText ropts showcell (maybetranspose $ budgetReportAsTable ropts budgetr)
   where
     multiperiod = interval_ /= NoInterval
     title = printf "Budget performance in %s%s:"
-      (showDateSpan $ budgetReportSpan budgetr)
+      (showDateSpan $ periodicReportSpan budgetr)
       (case value_ of
         Just (AtCost _mc)   -> ", valued at cost"
+        Just (AtThen _mc)   -> error' unsupportedValueThenError  -- TODO
         Just (AtEnd _mc)    -> ", valued at period ends"
         Just (AtNow _mc)    -> ", current value"
         -- XXX duplicates the above
@@ -285,16 +261,13 @@
         Just (AtDefault _mc)  -> ", current value"
         Just (AtDate d _mc) -> ", valued at "++showDate d
         Nothing             -> "")
-    actualwidth =
-      maximum' [ maybe 0 (length . showMixedAmountOneLineWithoutPrice) amt
-      | (_, _, _, amtandgoals, _, _) <- rows
-      , (amt, _) <- amtandgoals ]
-    budgetwidth =
-      maximum' [ maybe 0 (length . showMixedAmountOneLineWithoutPrice) goal
-      | (_, _, _, amtandgoals, _, _) <- rows
-      , (_, goal) <- amtandgoals ]
+    actualwidth = maximum' $ map fst amountsAndGoals
+    budgetwidth = maximum' $ map snd amountsAndGoals
+    amountsAndGoals = map (\(a,g) -> (amountLength a, amountLength g))
+                    . concatMap prrAmounts $ prRows budgetr
+      where amountLength = maybe 0 (length . showMixedAmountOneLineWithoutPrice)
     -- XXX lay out actual, percentage and/or goal in the single table cell for now, should probably use separate cells
-    showcell :: (Maybe Change, Maybe BudgetGoal) -> String
+    showcell :: BudgetCell -> String
     showcell (mactual, mbudget) = actualstr ++ " " ++ budgetstr
       where
         percentwidth = 4
@@ -339,11 +312,7 @@
 budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount)
 budgetReportAsTable
   ropts
-  (PeriodicReport
-    ( periods
-    , rows
-    , (_, _, _, coltots, grandtot, grandavg)
-    )) =
+  (PeriodicReport periods rows (PeriodicReportRow _ _ coltots grandtot grandavg)) =
     addtotalrow $
     Table
       (T.Group NoLine $ map Header accts)
@@ -351,21 +320,20 @@
       (map rowvals rows)
   where
     colheadings = map showDateSpanMonthAbbrev periods
-                  ++ (if row_total_ ropts then ["  Total"] else [])
-                  ++ (if average_   ropts then ["Average"] else [])
+                  ++ ["  Total" | row_total_ ropts]
+                  ++ ["Average" | average_ ropts]
     accts = map renderacct rows
-    renderacct (a,a',i,_,_,_)
-      | tree_ ropts = replicate ((i-1)*2) ' ' ++ T.unpack a'
+    renderacct (PeriodicReportRow a i _ _ _)
+      | tree_ ropts = replicate ((i-1)*2) ' ' ++ T.unpack (accountLeafName a)
       | otherwise   = T.unpack $ maybeAccountNameDrop ropts a
-    rowvals (_,_,_,as,rowtot,rowavg) = as
-                                       ++ (if row_total_ ropts then [rowtot] else [])
-                                       ++ (if average_   ropts then [rowavg] else [])
-    addtotalrow | no_total_ ropts = id
-                | otherwise       = (+----+ (row "" $
-                                     coltots
-                                     ++ (if row_total_ ropts && not (null coltots) then [grandtot] else [])
-                                     ++ (if average_   ropts && not (null coltots) then [grandavg] else [])
-                                     ))
+    rowvals (PeriodicReportRow _ _ as rowtot rowavg) =
+        as ++ [rowtot | row_total_ ropts] ++ [rowavg | average_ ropts]
+    addtotalrow
+      | no_total_ ropts = id
+      | otherwise = (+----+ (row "" $
+                       coltots ++ [grandtot | row_total_ ropts && not (null coltots)]
+                               ++ [grandavg | average_ ropts && not (null coltots)]
+                    ))
 
 -- XXX here for now
 -- TODO: does not work for flat-by-default reports with --flat not specified explicitly
diff --git a/Hledger/Reports/MultiBalanceReport.hs b/Hledger/Reports/MultiBalanceReport.hs
--- a/Hledger/Reports/MultiBalanceReport.hs
+++ b/Hledger/Reports/MultiBalanceReport.hs
@@ -6,14 +6,12 @@
 -}
 
 module Hledger.Reports.MultiBalanceReport (
-  MultiBalanceReport(..),
+  MultiBalanceReport,
   MultiBalanceReportRow,
+
   multiBalanceReport,
   multiBalanceReportWith,
   balanceReportFromMultiBalanceReport,
-  mbrNegate,
-  mbrNormaliseSign,
-  multiBalanceReportSpan,
   tableAsText,
 
   -- -- * Tests
@@ -21,9 +19,9 @@
 )
 where
 
-import GHC.Generics (Generic)
-import Control.DeepSeq (NFData)
 import Data.List
+import Data.List.Extra (nubSort)
+import qualified Data.Map as M
 import Data.Maybe
 import Data.Ord
 import Data.Time.Calendar
@@ -36,12 +34,12 @@
 import Hledger.Utils
 import Hledger.Read (mamountp')
 import Hledger.Reports.ReportOptions
+import Hledger.Reports.ReportTypes
 import Hledger.Reports.BalanceReport
 
 
--- | A multi balance report is a balance report with multiple columns,
--- corresponding to consecutive subperiods within the overall report
--- period. It has:
+-- | A multi balance report is a kind of periodic report, where the amounts
+-- correspond to balance changes or ending balances in a given period. It has:
 --
 -- 1. a list of each column's period (date span)
 --
@@ -49,42 +47,19 @@
 --
 --   * the full account name
 --
---   * the leaf account name
---
 --   * the account's depth
 --
---   * A list of amounts, one for each column. The meaning of the
---     amounts depends on the type of multi balance report, of which
---     there are three: periodic, cumulative and historical (see
---     'BalanceType' and "Hledger.Cli.Commands.Balance").
+--   * A list of amounts, one for each column.
 --
---   * the total of the row's amounts for a periodic report,
---     or zero for cumulative/historical reports (since summing
---     end balances generally doesn't make sense).
+--   * the total of the row's amounts for a periodic report
 --
 --   * the average of the row's amounts
 --
 -- 3. the column totals, and the overall grand total (or zero for
 -- cumulative/historical reports) and grand average.
---
-newtype MultiBalanceReport =
-  MultiBalanceReport ([DateSpan]
-                     ,[MultiBalanceReportRow]
-                     ,MultiBalanceReportTotals
-                     )
-  deriving (Generic)
 
-type MultiBalanceReportRow    = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
-type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)
-
-instance NFData MultiBalanceReport
-
-instance Show MultiBalanceReport where
-    -- use pshow (pretty-show's ppShow) to break long lists onto multiple lines
-    -- we add some bogus extra shows here to help it parse the output
-    -- and wrap tuples and lists properly
-    show (MultiBalanceReport (spans, items, totals)) =
-        "MultiBalanceReport (ignore extra quotes):\n" ++ pshow (show spans, map show items, totals)
+type MultiBalanceReport    = PeriodicReport AccountName MixedAmount
+type MultiBalanceReportRow = PeriodicReportRow AccountName MixedAmount
 
 -- type alias just to remind us which AccountNames might be depth-clipped, below.
 type ClippedAccountName = AccountName
@@ -104,9 +79,9 @@
 -- run multiple reports (bs etc.) can generate the price oracle just once
 -- for efficiency, passing it to each report by calling this function directly.
 multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
-multiBalanceReportWith ropts@ReportOpts{..} q j@Journal{..} priceoracle =
-  (if invert_ then mbrNegate else id) $
-  MultiBalanceReport (colspans, mappedsortedrows, mappedtotalsrow)
+multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle =
+  (if invert_ then prNegate else id) $
+  PeriodicReport colspans mappedsortedrows mappedtotalsrow
     where
       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
@@ -150,7 +125,7 @@
           displayspan
             | empty_    = dbg1 "displayspan (-E)" reportspan                              -- all the requested intervals
             | otherwise = dbg1 "displayspan" $ requestedspan `spanIntersect` matchedspan  -- exclude leading/trailing empty intervals
-          matchedspan = dbg1 "matchedspan" $ postingsDateSpan' (whichDateFromOpts ropts) ps
+          matchedspan = dbg1 "matchedspan" . daysSpan $ map snd ps
 
       -- If doing cost valuation, convert amounts to cost.
       j' = journalSelectingAmountFromOpts ropts j
@@ -187,17 +162,26 @@
       -- 3. Gather postings for each column.
 
       -- Postings matching the query within the report period.
-      ps :: [Posting] =
+      ps :: [(Posting, Day)] =
           dbg1 "ps" $
+          map postingWithDate $
           journalPostings $
           filterJournalAmounts symq $      -- remove amount parts excluded by cur:
           filterJournalPostings reportq $  -- remove postings not matched by (adjusted) query
           j'
+        where
+          postingWithDate p = case whichDateFromOpts ropts of
+              PrimaryDate   -> (p, postingDate p)
+              SecondaryDate -> (p, postingDate2 p)
 
       -- Group postings into their columns, with the column end dates.
       colps :: [([Posting], Maybe Day)] =
           dbg1 "colps"
-          [(filter (isPostingInDateSpan' (whichDateFromOpts ropts) s) ps, spanEnd s) | s <- colspans]
+          [ (posts, end) | (DateSpan _ end, posts) <- M.toList colMap ]
+        where
+          colMap = foldr addPosting emptyMap ps
+          addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d
+          emptyMap = M.fromList . zip colspans $ repeat []
 
       ----------------------------------------------------------------------
       -- 4. Calculate account balance changes in each column.
@@ -225,16 +209,16 @@
           (if tree_ ropts then expandAccountNames else id) $
           nub $ map (clipOrEllipsifyAccountName depth) $
           if empty_ || balancetype_ == HistoricalBalance
-          then nub $ sort $ startaccts ++ allpostedaccts
+          then nubSort $ startaccts ++ allpostedaccts
           else allpostedaccts
         where
-          allpostedaccts :: [AccountName] = dbg1 "allpostedaccts" $ sort $ accountNamesFromPostings ps
+          allpostedaccts :: [AccountName] =
+            dbg1 "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps
       -- Each column's balance changes for each account, adding zeroes where needed.
       colallacctchanges :: [[(ClippedAccountName, MixedAmount)]] =
           dbg1 "colallacctchanges"
-          [sortBy (comparing fst) $
-           unionBy (\(a,_) (a',_) -> a == a') postedacctchanges zeroes
-           | postedacctchanges <- colacctchanges]
+          [ sortOn fst $ unionBy (\(a,_) (a',_) -> a == a') postedacctchanges zeroes
+             | postedacctchanges <- colacctchanges ]
           where zeroes = [(a, nullmixedamt) | a <- displayaccts]
       -- Transpose to get each account's balance changes across all columns.
       acctchanges :: [(ClippedAccountName, [MixedAmount])] =
@@ -247,7 +231,7 @@
       -- One row per account, with account name info, row amounts, row total and row average.
       rows :: [MultiBalanceReportRow] =
           dbg1 "rows" $
-          [(a, accountLeafName a, accountNameLevel a, valuedrowbals, rowtot, rowavg)
+          [ PeriodicReportRow a (accountNameLevel a) valuedrowbals rowtot rowavg
            | (a,changes) <- dbg1 "acctchanges" acctchanges
              -- The row amounts to be displayed: per-period changes,
              -- zero-based cumulative totals, or
@@ -297,11 +281,12 @@
             where
               -- Sort the report rows, representing a tree of accounts, by row total at each level.
               -- Similar to sortMBRByAccountDeclaration/sortAccountNamesByDeclaration.
+              sortTreeMBRByAmount :: [MultiBalanceReportRow] -> [MultiBalanceReportRow]
               sortTreeMBRByAmount rows = sortedrows
                 where
-                  anamesandrows = [(first6 r, r) | r <- rows]
+                  anamesandrows = [(prrName r, r) | r <- rows]
                   anames = map fst anamesandrows
-                  atotals = [(a,tot) | (a,_,_,_,tot,_) <- rows]
+                  atotals = [(prrName r, prrTotal r) | r <- rows]
                   accounttree = accountTree "root" anames
                   accounttreewithbals = mapAccounts setibalance accounttree
                     where
@@ -312,14 +297,14 @@
                   sortedrows = sortAccountItemsLike sortedanames anamesandrows
 
               -- Sort the report rows, representing a flat account list, by row total.
-              sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fifth6))
+              sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . prrTotal))
                 where
                   maybeflip = if normalbalance_ == Just NormallyNegative then id else flip
 
               -- Sort the report rows by account declaration order then account name.
               sortMBRByAccountDeclaration rows = sortedrows
                 where
-                  anamesandrows = [(first6 r, r) | r <- rows]
+                  anamesandrows = [(prrName r, r) | r <- rows]
                   anames = map fst anamesandrows
                   sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames
                   sortedrows = sortAccountItemsLike sortedanames anamesandrows
@@ -329,7 +314,8 @@
 
       -- Calculate the column totals. These are always the sum of column amounts.
       highestlevelaccts = [a | a <- displayaccts, not $ any (`elem` displayaccts) $ init $ expandAccountName a]
-      colamts           = transpose [bs | (a,_,_,bs,_,_) <- rows, not (tree_ ropts) || a `elem` highestlevelaccts]
+      colamts = transpose . map prrAmounts $ filter isHighest rows
+        where isHighest row = not (tree_ ropts) || prrName row `elem` highestlevelaccts
       coltotals :: [MixedAmount] =
         dbg1 "coltotals" $ map sum colamts
       -- Calculate the grand total and average. These are always the sum/average
@@ -341,45 +327,29 @@
               ]
         in amts
       -- Totals row.
-      totalsrow :: MultiBalanceReportTotals =
-        dbg1 "totalsrow" (coltotals, grandtotal, grandaverage)
+      totalsrow :: PeriodicReportRow () MixedAmount =
+        dbg1 "totalsrow" $ PeriodicReportRow () 0 coltotals grandtotal grandaverage
 
       ----------------------------------------------------------------------
       -- 9. Map the report rows to percentages if needed
       -- It is not correct to do this before step 6 due to the total and average columns.
       -- This is not done in step 6, since the report totals are calculated in 8.
-      
       -- Perform the divisions to obtain percentages
       mappedsortedrows :: [MultiBalanceReportRow] =
         if not percent_ then sortedrows
         else dbg1 "mappedsortedrows"
-          [(aname, alname, alevel, zipWith perdivide rowvals coltotals, rowtotal `perdivide` grandtotal, rowavg `perdivide` grandaverage)
-           | (aname, alname, alevel, rowvals, rowtotal, rowavg) <- sortedrows
+          [ PeriodicReportRow aname alevel
+              (zipWith perdivide rowvals coltotals)
+              (rowtotal `perdivide` grandtotal)
+              (rowavg `perdivide` grandaverage)
+           | PeriodicReportRow aname alevel rowvals rowtotal rowavg <- sortedrows
           ]
-      mappedtotalsrow :: MultiBalanceReportTotals =
-        if not percent_ then totalsrow
-        else dbg1 "mappedtotalsrow" (
-          map (\t -> perdivide t t) coltotals,
-          perdivide grandtotal grandtotal,
-          perdivide grandaverage grandaverage)
-
--- | 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)
-
--- | Figure out the overall date span of a multicolumn balance report.
-multiBalanceReportSpan :: MultiBalanceReport -> DateSpan
-multiBalanceReportSpan (MultiBalanceReport ([], _, _))       = DateSpan Nothing Nothing
-multiBalanceReportSpan (MultiBalanceReport (colspans, _, _)) = DateSpan (spanStart $ head colspans) (spanEnd $ last colspans)
+      mappedtotalsrow :: PeriodicReportRow () MixedAmount
+        | percent_  = dbg1 "mappedtotalsrow" $ PeriodicReportRow () 0
+             (map (\t -> perdivide t t) coltotals)
+             (perdivide grandtotal grandtotal)
+             (perdivide grandaverage grandaverage)
+        | otherwise = totalsrow
 
 -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport,
 -- in order to support --historical. Does not support tree-mode boring parent eliding.
@@ -388,12 +358,12 @@
 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
+    PeriodicReport _ rows (PeriodicReportRow _ _ totals _ _) = multiBalanceReport opts q j
+    rows' = [( a
+             , if flat_ opts then a else accountLeafName 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]
+             ) | PeriodicReportRow a d amts _ _ <- rows]
     total = headDef nullmixedamt totals
 
 
@@ -421,10 +391,11 @@
     amt0 = Amount {acommodity="$", aquantity=0, aprice=Nothing, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}, aismultiplier=False}
     (opts,journal) `gives` r = do
       let (eitems, etotal) = r
-          (MultiBalanceReport (_, aitems, atotal)) = multiBalanceReport opts (queryFromOpts nulldate opts) journal
-          showw (acct,acct',indent,lAmt,amt,amt') = (acct, acct', indent, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')
+          (PeriodicReport _ aitems atotal) = multiBalanceReport opts (queryFromOpts nulldate opts) journal
+          showw (PeriodicReportRow acct indent lAmt amt amt')
+              = (acct, accountLeafName acct, indent, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')
       (map showw aitems) @?= (map showw eitems)
-      ((\(_, b, _) -> showMixedAmountDebug b) atotal) @?= (showMixedAmountDebug etotal) -- we only check the sum of the totals
+      showMixedAmountDebug (prrTotal atotal) @?= showMixedAmountDebug etotal -- we only check the sum of the totals
   in
    tests "multiBalanceReport" [
       test "null journal"  $
@@ -433,9 +404,8 @@
      ,test "with -H on a populated period"  $
       (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`
        (
-        [
-         ("assets:bank:checking", "checking", 3, [mamountp' "$1.00"] , Mixed [nullamt], Mixed [amt0 {aquantity=1}])
-        ,("income:salary"       ,"salary"   , 2, [mamountp' "$-1.00"], Mixed [nullamt], Mixed [amt0 {aquantity=(-1)}])
+        [ PeriodicReportRow "assets:bank:checking" 3 [mamountp' "$1.00"]  (Mixed [nullamt]) (Mixed [amt0 {aquantity=1}])
+        , PeriodicReportRow "income:salary"        2 [mamountp' "$-1.00"] (Mixed [nullamt]) (Mixed [amt0 {aquantity=(-1)}])
         ],
         Mixed [nullamt])
 
diff --git a/Hledger/Reports/PostingsReport.hs b/Hledger/Reports/PostingsReport.hs
--- a/Hledger/Reports/PostingsReport.hs
+++ b/Hledger/Reports/PostingsReport.hs
@@ -23,8 +23,8 @@
 where
 
 import Data.List
+import Data.List.Extra (nubSort)
 import Data.Maybe
-import Data.Ord (comparing)
 -- import Data.Text (Text)
 import qualified Data.Text as T
 import Data.Time.Calendar
@@ -67,7 +67,7 @@
 -- | Select postings from the journal and add running balance and other
 -- information to make a postings report. Used by eg hledger's register command.
 postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport
-postingsReport ropts@ReportOpts{..} q j@Journal{..} =
+postingsReport ropts@ReportOpts{..} q j =
   (totallabel, items)
     where
       reportspan  = adjustReportDates ropts q j
@@ -166,7 +166,7 @@
   where
     beforestartq = dbg1 "beforestartq" $ dateqtype $ DateSpan Nothing mstart
     beforeandduringps =
-      dbg1 "ps5" $ sortBy (comparing sortdate) $                               -- sort postings by date or date2
+      dbg1 "ps5" $ sortOn sortdate $                                           -- sort postings by date or date2
       dbg1 "ps4" $ (if invert_ opts then map negatePostingAmount else id) $    -- with --invert, invert amounts
       dbg1 "ps3" $ map (filterPostingAmount symq) $                            -- remove amount parts which the query's cur: terms would exclude
       dbg1 "ps2" $ (if related_ opts then concatMap relatedPostings else id) $ -- with -r, replace each with its sibling postings
@@ -254,7 +254,7 @@
       summaryps | depth > 0 = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]
                 | otherwise = [summaryp{paccount="...",pamount=sum $ map pamount ps}]
       summarypes = map (, e') $ (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps
-      anames = sort $ nub $ map paccount ps
+      anames = nubSort $ map paccount ps
       -- aggregate balances by account, like ledgerFromJournal, then do depth-clipping
       accts = accountsFromPostings ps
       balance a = maybe nullmixedamt bal $ lookupAccount a accts
diff --git a/Hledger/Reports/ReportOptions.hs b/Hledger/Reports/ReportOptions.hs
--- a/Hledger/Reports/ReportOptions.hs
+++ b/Hledger/Reports/ReportOptions.hs
@@ -46,7 +46,7 @@
 
 import Control.Applicative ((<|>))
 import Data.Data (Data)
-import Data.List
+import Data.List.Extra (nubSort)
 import Data.Maybe
 import qualified Data.Text as T
 import Data.Typeable (Typeable)
@@ -337,7 +337,7 @@
   | length l' >= numstatuses = []
   | otherwise                = l'
   where
-    l' = nub $ sort l
+    l' = nubSort l
     numstatuses = length [minBound .. maxBound :: Status]
 
 -- | Add/remove this status from the status list. Used by hledger-ui.
@@ -358,13 +358,14 @@
       | n == "value" = Just $ valuation v
       | otherwise    = Nothing
     valuation v
-      | t `elem` ["cost","c"] = AtCost mc
-      | t `elem` ["end" ,"e"] = AtEnd  mc
-      | t `elem` ["now" ,"n"] = AtNow  mc
+      | t `elem` ["cost","c"]  = AtCost mc
+      | t `elem` ["then" ,"t"] = AtThen  mc
+      | t `elem` ["end" ,"e"]  = AtEnd  mc
+      | t `elem` ["now" ,"n"]  = AtNow  mc
       | otherwise =
           case parsedateM t of
             Just d  -> AtDate d mc
-            Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: cost|end|now|c|e|n|YYYY-MM-DD"
+            Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: cost|then|end|now|c|t|e|n|YYYY-MM-DD"
       where
         -- parse --value's value: TYPE[,COMM]
         (t,c') = break (==',') v
@@ -511,7 +512,7 @@
 -- the journal's start date (the earliest posting date). If there's no
 -- report period and nothing in the journal, will be Nothing.
 reportPeriodOrJournalStart :: ReportOpts -> Journal -> Maybe Day
-reportPeriodOrJournalStart ropts@ReportOpts{..} j =
+reportPeriodOrJournalStart ropts j =
   reportPeriodStart ropts <|> journalStartDate False j
 
 -- Get the last day of the overall report period.
@@ -533,7 +534,7 @@
 -- posting date). If there's no report period and nothing in the
 -- journal, will be Nothing.
 reportPeriodOrJournalLastDay :: ReportOpts -> Journal -> Maybe Day
-reportPeriodOrJournalLastDay ropts@ReportOpts{..} j =
+reportPeriodOrJournalLastDay ropts j =
   reportPeriodLastDay ropts <|> journalEndDate False j
 
 -- tests
diff --git a/Hledger/Reports/ReportTypes.hs b/Hledger/Reports/ReportTypes.hs
--- a/Hledger/Reports/ReportTypes.hs
+++ b/Hledger/Reports/ReportTypes.hs
@@ -1,11 +1,27 @@
 {- |
 New common report types, used by the BudgetReport for now, perhaps all reports later.
 -}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
 
 module Hledger.Reports.ReportTypes
-where
+( PeriodicReport(..)
+, PeriodicReportRow(..)
 
+, Percentage
+, Change
+, Balance
+, Total
+, Average
+
+, periodicReportSpan
+, prNegate
+, prNormaliseSign
+) where
+
+import Data.Aeson
 import Data.Decimal
+import GHC.Generics (Generic)
 import Hledger.Data
 
 type Percentage = Decimal
@@ -15,26 +31,68 @@
 type Total   = MixedAmount  -- ^ The sum of 'Change's in a report or a report row. Does not make sense for 'Balance's.
 type Average = MixedAmount  -- ^ The average of 'Change's or 'Balance's in a report or report row.
 
--- | A generic tabular report of some value, where each row corresponds to an account
--- and each column is a date period. The column periods are usually consecutive subperiods
--- formed by splitting the overall report period by some report interval (daily, weekly, etc.)
--- Depending on the value type, this can be a report of balance changes, ending balances,
--- budget performance, etc. Successor to MultiBalanceReport.
-data PeriodicReport a =
+-- | A periodic report is a generic tabular report, where each row corresponds
+-- to some label (usually an account name) and each column to a date period.
+-- The column periods are usually consecutive subperiods formed by splitting
+-- the overall report period by some report interval (daily, weekly, etc.).
+-- It has:
+--
+-- 1. a list of each column's period (date span)
+--
+-- 2. a list of rows, each containing:
+--
+--   * an account label
+--
+--   * the account's depth
+--
+--   * A list of amounts, one for each column. Depending on the value type,
+--     these can represent balance changes, ending balances, budget
+--     performance, etc. (for example, see 'BalanceType' and
+--     "Hledger.Cli.Commands.Balance").
+--
+--   * the total of the row's amounts for a periodic report,
+--     or zero for cumulative/historical reports (since summing
+--     end balances generally doesn't make sense).
+--
+--   * the average of the row's amounts
+--
+-- 3. the column totals, and the overall grand total (or zero for
+-- cumulative/historical reports) and grand average.
+
+data PeriodicReport a b =
   PeriodicReport
-    ( [DateSpan]            -- The subperiods formed by splitting the overall report period by the report interval.
-                            -- For ending-balance reports, only the end date is significant.
-                            -- Usually displayed as report columns.
-    , [PeriodicReportRow a] -- One row per account in the report.
-    , PeriodicReportRow a   -- The grand totals row. The account name in this row is always empty.
-    )
-   deriving (Show)
+  { prDates  :: [DateSpan]               -- The subperiods formed by splitting the overall
+                                         -- report period by the report interval. For
+                                         -- ending-balance reports, only the end date is
+                                         -- significant. Usually displayed as report columns.
+  , prRows   :: [PeriodicReportRow a b]  -- One row per account in the report.
+  , prTotals :: PeriodicReportRow () b   -- The grand totals row.
+  } deriving (Show, Generic, ToJSON)
 
-type PeriodicReportRow a =
-  ( AccountName  -- A full account name.
-  , AccountName  -- Shortened form of the account name to display in tree mode. Usually the leaf name, possibly with parent accounts prefixed.
-  , Int          -- Indent level for displaying this account name in tree mode. 0, 1, 2...
-  , [a]          -- The data value for each subperiod.
-  , a            -- The total of this row's values.
-  , a            -- The average of this row's values.
-  )
+data PeriodicReportRow a b =
+  PeriodicReportRow
+  { prrName    :: a    -- An account name.
+  , prrDepth   :: Int  -- Indent level for displaying this account name in tree mode. 0, 1, 2...
+  , prrAmounts :: [b]  -- The data value for each subperiod.
+  , prrTotal   :: b    -- The total of this row's values.
+  , prrAverage :: b    -- The average of this row's values.
+  } deriving (Show, Generic, ToJSON)
+
+-- | Figure out the overall date span of a PeridicReport
+periodicReportSpan :: PeriodicReport a b -> DateSpan
+periodicReportSpan (PeriodicReport [] _ _)       = DateSpan Nothing Nothing
+periodicReportSpan (PeriodicReport colspans _ _) = DateSpan (spanStart $ head colspans) (spanEnd $ last colspans)
+
+-- | Given a PeriodicReport and its normal balance sign,
+-- if it is known to be normally negative, convert it to normally positive.
+prNormaliseSign :: Num b => NormalSign -> PeriodicReport a b -> PeriodicReport a b
+prNormaliseSign NormallyNegative = prNegate
+prNormaliseSign _ = id
+
+-- | Flip the sign of all amounts in a PeriodicReport.
+prNegate :: Num b => PeriodicReport a b -> PeriodicReport a b
+prNegate (PeriodicReport colspans rows totalsrow) =
+    PeriodicReport colspans (map rowNegate rows) (rowNegate totalsrow)
+  where
+    rowNegate (PeriodicReportRow name indent amts tot avg) =
+        PeriodicReportRow name indent (map negate amts) (-tot) (-avg)
diff --git a/Hledger/Reports/TransactionsReport.hs b/Hledger/Reports/TransactionsReport.hs
--- a/Hledger/Reports/TransactionsReport.hs
+++ b/Hledger/Reports/TransactionsReport.hs
@@ -22,6 +22,7 @@
 where
 
 import Data.List
+import Data.List.Extra (nubSort)
 import Data.Ord
 
 import Hledger.Data
@@ -79,7 +80,7 @@
   [(c, filterTransactionsReportByCommodity c tr) | c <- transactionsReportCommodities tr]
   where
     transactionsReportCommodities (_,items) =
-      nub $ sort $ map acommodity $ concatMap (amounts . triAmount) items
+      nubSort . map acommodity $ concatMap (amounts . triAmount) items
 
 -- Remove transaction report items and item amount (and running
 -- balance amount) components that don't involve the specified
diff --git a/Hledger/Utils.hs b/Hledger/Utils.hs
--- a/Hledger/Utils.hs
+++ b/Hledger/Utils.hs
@@ -183,7 +183,7 @@
     (md:_) -> md
 
 -- | Read text from a file,
--- handling any of the usual line ending conventions,
+-- converting any \r\n line endings to \n,,
 -- 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
diff --git a/Hledger/Utils/Debug.hs b/Hledger/Utils/Debug.hs
--- a/Hledger/Utils/Debug.hs
+++ b/Hledger/Utils/Debug.hs
@@ -204,8 +204,8 @@
 dbgExit :: Show a => String -> a -> a
 dbgExit msg = const (unsafePerformIO exitFailure) . dbg0 msg
 
--- | Like ptraceAt, but convenient to insert in an IO monad (plus
--- convenience aliases).
+-- | Like ptraceAt, but convenient to insert in an IO monad and
+-- enforces monadic sequencing (plus convenience aliases).
 -- XXX These have a bug; they should use
 -- traceIO, not trace, otherwise GHC can occasionally over-optimise
 -- (cf lpaste a few days ago where it killed/blocked a child thread).
diff --git a/Hledger/Utils/Parse.hs b/Hledger/Utils/Parse.hs
--- a/Hledger/Utils/Parse.hs
+++ b/Hledger/Utils/Parse.hs
@@ -120,12 +120,14 @@
 
 isNonNewlineSpace :: Char -> Bool
 isNonNewlineSpace c = c /= '\n' && isSpace c
+-- XXX support \r\n ?
+-- isNonNewlineSpace c = c /= '\n' && c /= '\r' && isSpace c
 
 spacenonewline :: (Stream s, Char ~ Token s) => ParsecT CustomErr s m Char
 spacenonewline = satisfy isNonNewlineSpace
 
 restofline :: TextParser m String
-restofline = anySingle `manyTill` newline
+restofline = anySingle `manyTill` eolof
 
 eolof :: TextParser m ()
 eolof = (newline >> return ()) <|> eof
diff --git a/Hledger/Utils/String.hs b/Hledger/Utils/String.hs
--- a/Hledger/Utils/String.hs
+++ b/Hledger/Utils/String.hs
@@ -77,7 +77,7 @@
 chomp :: String -> String
 chomp = reverse . dropWhile (`elem` "\r\n") . reverse
 
--- | Remove consequtive line breaks, replacing them with single space
+-- | Remove consecutive line breaks, replacing them with single space
 singleline :: String -> String
 singleline = unwords . filter (/="") . (map strip) . lines
 
@@ -111,7 +111,7 @@
 -- | Double-quote this string if it contains whitespace, single quotes
 -- or double-quotes, escaping the quotes as needed.
 quoteIfNeeded :: String -> String
-quoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = "\"" ++ escapeDoubleQuotes s ++ "\""
+quoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars++redirectchars) = "\"" ++ escapeDoubleQuotes s ++ "\""
                 | otherwise = s
 -- | Single-quote this string if it contains whitespace or double-quotes.
 -- No good for strings containing single quotes.
@@ -119,9 +119,10 @@
 singleQuoteIfNeeded s | any (`elem` s) whitespacechars = "'"++s++"'"
                       | otherwise = s
 
-quotechars, whitespacechars :: [Char]
+quotechars, whitespacechars, redirectchars :: [Char]
 quotechars      = "'\""
 whitespacechars = " \t\n\r"
+redirectchars   = "<>"
 
 escapeDoubleQuotes :: String -> String
 escapeDoubleQuotes = regexReplace "\"" "\""
diff --git a/Hledger/Utils/Text.hs b/Hledger/Utils/Text.hs
--- a/Hledger/Utils/Text.hs
+++ b/Hledger/Utils/Text.hs
@@ -14,7 +14,7 @@
   textUnbracket,
  -- -- quoting
   quoteIfSpaced,
- -- quoteIfNeeded,
+  textQuoteIfNeeded,
  -- singleQuoteIfNeeded,
  -- -- quotechars,
  -- -- whitespacechars,
@@ -127,7 +127,7 @@
 quoteIfSpaced :: T.Text -> T.Text
 quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s
                 | not $ any (`elem` (T.unpack s)) whitespacechars = s
-                | otherwise = quoteIfNeeded s
+                | otherwise = textQuoteIfNeeded s
 
 -- -- | Wrap a string in double quotes, and \-prefix any embedded single
 -- -- quotes, if it contains whitespace and is not already single- or
@@ -139,9 +139,9 @@
 
 -- -- | Double-quote this string if it contains whitespace, single quotes
 -- -- or double-quotes, escaping the quotes as needed.
-quoteIfNeeded :: T.Text -> T.Text
-quoteIfNeeded s | any (`elem` T.unpack s) (quotechars++whitespacechars) = "\"" <> escapeDoubleQuotes s <> "\""
-                | otherwise = s
+textQuoteIfNeeded :: T.Text -> T.Text
+textQuoteIfNeeded s | any (`elem` T.unpack s) (quotechars++whitespacechars) = "\"" <> escapeDoubleQuotes s <> "\""
+                    | otherwise = s
 
 -- -- | Single-quote this string if it contains whitespace or double-quotes.
 -- -- No good for strings containing single quotes.
diff --git a/Text/Tabular/AsciiWide.hs b/Text/Tabular/AsciiWide.hs
--- a/Text/Tabular/AsciiWide.hs
+++ b/Text/Tabular/AsciiWide.hs
@@ -16,14 +16,14 @@
        -> Table rh ch a
        -> String
 render pretty fr fc f (Table rh ch cells) =
-  unlines $ [ bar SingleLine   -- +--------------------------------------+
+  unlines $ [ bar VT SingleLine   -- +--------------------------------------+
             , renderColumns pretty sizes ch2
-            , bar DoubleLine   -- +======================================+
+            , bar VM DoubleLine   -- +======================================+
             ] ++
             (renderRs $ fmap renderR $ zipHeader [] cells $ fmap fr rh) ++
-            [ bar SingleLine ] -- +--------------------------------------+
+            [ bar VB SingleLine ] -- +--------------------------------------+
  where
-  bar = concat . renderHLine pretty sizes ch2
+  bar vpos prop = concat (renderHLine vpos pretty sizes ch2 prop)
   -- ch2 and cell2 include the row and column labels
   ch2 = Group DoubleLine [Header "", fmap fc ch]
   cells2 = headerContents ch2
@@ -37,7 +37,7 @@
   sizes   = map (maximum . map strWidth) . transpose $ cells2
   renderRs (Header s)   = [s]
   renderRs (Group p hs) = concat . intersperse sep . map renderRs $ hs
-    where sep = renderHLine pretty sizes ch2 p
+    where sep = renderHLine VM pretty sizes ch2 p
 
 verticalBar :: Bool -> Char
 verticalBar pretty = if pretty then '│' else '|'
@@ -54,12 +54,6 @@
 doubleMidBar :: Bool -> String
 doubleMidBar pretty = if pretty then " ║ " else " || "
 
-horizontalBar :: Bool -> Char
-horizontalBar pretty = if pretty then '─' else '-'
-
-doubleHorizontalBar :: Bool -> Char
-doubleHorizontalBar pretty = if pretty then '═' else '='
-
 -- | We stop rendering on the shortest list!
 renderColumns :: Bool -- ^ pretty
               -> [Int] -- ^ max width for each column
@@ -74,38 +68,104 @@
   hsep SingleLine = midBar pretty
   hsep DoubleLine = doubleMidBar pretty
 
-renderHLine :: Bool -- ^ pretty
+renderHLine :: VPos
+            -> Bool -- ^ pretty
             -> [Int] -- ^ width specifications
             -> Header String
             -> Properties
             -> [String]
-renderHLine _ _ _ NoLine = []
-renderHLine pretty w h SingleLine = [renderHLine' pretty SingleLine w (horizontalBar pretty) h]
-renderHLine pretty w h DoubleLine = [renderHLine' pretty DoubleLine w (doubleHorizontalBar pretty) h]
-
-doubleCross :: Bool -> String
-doubleCross pretty = if pretty then "╬" else "++"
-
-doubleVerticalCross :: Bool -> String
-doubleVerticalCross pretty = if pretty then "╫" else "++"
-
-cross :: Bool -> Char
-cross pretty = if pretty then '┼' else '+'
+renderHLine _ _ _ _ NoLine = []
+renderHLine vpos pretty w h prop = [renderHLine' vpos pretty prop w h]
 
-renderHLine' :: Bool -> Properties -> [Int] -> Char -> Header String -> String
-renderHLine' pretty prop is sep h = [ cross pretty, sep ] ++ coreLine ++ [sep, cross pretty]
+renderHLine' :: VPos -> Bool -> Properties -> [Int] -> Header String -> String
+renderHLine' vpos pretty prop is h = edge HL ++ sep ++ coreLine ++ sep ++ edge HR
  where
+  edge hpos       = boxchar vpos hpos SingleLine prop pretty
   coreLine        = concatMap helper $ flattenHeader $ zipHeader 0 is h
   helper          = either vsep dashes
-  dashes (i,_)    = replicate i sep
-  vsep NoLine     = replicate 2 sep  -- match the double space sep in renderColumns
-  vsep SingleLine = sep : cross pretty : [sep]
-  vsep DoubleLine = sep : cross' ++ [sep]
-  cross' = case prop of
-     DoubleLine -> doubleCross pretty
-     _ -> doubleVerticalCross pretty
+  dashes (i,_)    = concat (replicate i sep)
+  sep             = boxchar vpos HM NoLine prop pretty
+  vsep v          = case v of
+                      NoLine -> sep ++ sep
+                      _      -> sep ++ cross v prop ++ sep
+  cross v h       = boxchar vpos HM v h pretty
 
--- padLeft :: Int -> String -> String
--- padLeft l s = padding ++ s
---  where padding = replicate (l - length s) ' '
+data VPos = VT | VM | VB -- top middle bottom
+data HPos = HL | HM | HR -- left middle right
 
+boxchar :: VPos -> HPos -> Properties -> Properties -> Bool -> String
+boxchar vpos hpos vert horiz = lineart u d l r
+  where
+    u =
+      case vpos of
+        VT -> NoLine
+        _  -> vert
+    d =
+      case vpos of
+        VB -> NoLine
+        _  -> vert
+    l =
+      case hpos of
+        HL -> NoLine
+        _  -> horiz
+    r =
+      case hpos of
+        HR -> NoLine
+        _  -> horiz
+
+pick :: String -> String -> Bool -> String
+pick x _ True  = x
+pick _ x False = x
+
+lineart :: Properties -> Properties -> Properties -> Properties -> Bool -> String
+--      up         down       left      right
+lineart SingleLine SingleLine SingleLine SingleLine = pick "┼" "+"
+lineart SingleLine SingleLine SingleLine NoLine     = pick "┤" "+"
+lineart SingleLine SingleLine NoLine     SingleLine = pick "├" "+"
+lineart SingleLine NoLine     SingleLine SingleLine = pick "┴" "+"
+lineart NoLine     SingleLine SingleLine SingleLine = pick "┬" "+"
+lineart SingleLine NoLine     NoLine     SingleLine = pick "└" "+"
+lineart SingleLine NoLine     SingleLine NoLine     = pick "┘" "+"
+lineart NoLine     SingleLine SingleLine NoLine     = pick "┐" "+"
+lineart NoLine     SingleLine NoLine     SingleLine = pick "┌" "+"
+lineart SingleLine SingleLine NoLine     NoLine     = pick "│" "|"
+lineart NoLine     NoLine     SingleLine SingleLine = pick "─" "-"
+
+lineart DoubleLine DoubleLine DoubleLine DoubleLine = pick "╬" "++"
+lineart DoubleLine DoubleLine DoubleLine NoLine     = pick "╣" "++"
+lineart DoubleLine DoubleLine NoLine     DoubleLine = pick "╠" "++"
+lineart DoubleLine NoLine     DoubleLine DoubleLine = pick "╩" "++"
+lineart NoLine     DoubleLine DoubleLine DoubleLine = pick "╦" "++"
+lineart DoubleLine NoLine     NoLine     DoubleLine = pick "╚" "++"
+lineart DoubleLine NoLine     DoubleLine NoLine     = pick "╝" "++"
+lineart NoLine     DoubleLine DoubleLine NoLine     = pick "╗" "++"
+lineart NoLine     DoubleLine NoLine     DoubleLine = pick "╔" "++"
+lineart DoubleLine DoubleLine NoLine     NoLine     = pick "║" "||"
+lineart NoLine     NoLine     DoubleLine DoubleLine = pick "═" "="
+
+lineart DoubleLine NoLine     NoLine     SingleLine = pick "╙" "++"
+lineart DoubleLine NoLine     SingleLine NoLine     = pick "╜" "++"
+lineart NoLine     DoubleLine SingleLine NoLine     = pick "╖" "++"
+lineart NoLine     DoubleLine NoLine     SingleLine = pick "╓" "++"
+
+lineart SingleLine NoLine     NoLine     DoubleLine = pick "╘" "+"
+lineart SingleLine NoLine     DoubleLine NoLine     = pick "╛" "+"
+lineart NoLine     SingleLine DoubleLine NoLine     = pick "╕" "+"
+lineart NoLine     SingleLine NoLine     DoubleLine = pick "╒" "+"
+
+lineart DoubleLine DoubleLine SingleLine NoLine     = pick "╢" "++"
+lineart DoubleLine DoubleLine NoLine     SingleLine = pick "╟" "++"
+lineart DoubleLine NoLine     SingleLine SingleLine = pick "╨" "++"
+lineart NoLine     DoubleLine SingleLine SingleLine = pick "╥" "++"
+
+lineart SingleLine SingleLine DoubleLine NoLine     = pick "╡" "+"
+lineart SingleLine SingleLine NoLine     DoubleLine = pick "╞" "+"
+lineart SingleLine NoLine     DoubleLine DoubleLine = pick "╧" "+"
+lineart NoLine     SingleLine DoubleLine DoubleLine = pick "╤" "+"
+
+lineart SingleLine SingleLine DoubleLine DoubleLine = pick "╪" "+"
+lineart DoubleLine DoubleLine SingleLine SingleLine = pick "╫" "++"
+
+lineart _          _          _          _          = const ""
+
+-- 
diff --git a/hledger-lib.cabal b/hledger-lib.cabal
--- a/hledger-lib.cabal
+++ b/hledger-lib.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: eff7cc5cd5bf68cfdad1cc47374767d072f03ed6de99dfbd286885788a26dbab
+-- hash: 6b7e54683de41e16e86e5de1b2facbaf3c3ac856bacadda01f61a28157026278
 
 name:           hledger-lib
-version:        1.16.2
+version:        1.17
 synopsis:       Core data types, parsers and functionality for the hledger accounting tools
 description:    This is a reusable library containing hledger's core functionality.
                 .
@@ -25,7 +25,7 @@
 maintainer:     Simon Michael <simon@joyful.com>
 license:        GPL-3
 license-file:   LICENSE
-tested-with:    GHC==8.0.2, GHC==8.2.2, GHC==8.4.3, GHC==8.6.5, GHC==8.8.1
+tested-with:    GHC==8.0.2, GHC==8.2.2, GHC==8.4.3, GHC==8.6.5, GHC==8.8.2, GHC==8.10
 build-type:     Simple
 extra-source-files:
     CHANGES.md
@@ -59,6 +59,7 @@
       Hledger.Data.Commodity
       Hledger.Data.Dates
       Hledger.Data.Journal
+      Hledger.Data.Json
       Hledger.Data.Ledger
       Hledger.Data.Period
       Hledger.Data.PeriodicTransaction
@@ -107,9 +108,10 @@
   build-depends:
       Decimal
     , Glob >=0.9
+    , aeson
     , ansi-terminal >=0.6.2.3
     , array
-    , base >=4.9 && <4.14
+    , base >=4.9 && <4.15
     , base-compat-batteries >=0.10.1 && <0.12
     , blaze-markup >=0.5.1
     , bytestring
@@ -157,9 +159,10 @@
   build-depends:
       Decimal
     , Glob >=0.7
+    , aeson
     , ansi-terminal >=0.6.2.3
     , array
-    , base >=4.9 && <4.14
+    , base >=4.9 && <4.15
     , base-compat-batteries >=0.10.1 && <0.12
     , blaze-markup >=0.5.1
     , bytestring
@@ -211,9 +214,10 @@
   build-depends:
       Decimal
     , Glob >=0.9
+    , aeson
     , ansi-terminal >=0.6.2.3
     , array
-    , base >=4.9 && <4.14
+    , base >=4.9 && <4.15
     , base-compat-batteries >=0.10.1 && <0.12
     , blaze-markup >=0.5.1
     , bytestring
diff --git a/hledger_csv.5 b/hledger_csv.5
--- a/hledger_csv.5
+++ b/hledger_csv.5
@@ -1,6 +1,6 @@
 .\"t
 
-.TH "hledger_csv" "5" "January 2020" "hledger 1.16.2" "hledger User Manuals"
+.TH "hledger_csv" "5" "March 2020" "hledger 1.17" "hledger User Manuals"
 
 
 
@@ -9,9 +9,9 @@
 CSV - how hledger reads CSV data, and the CSV rules file format
 .SH DESCRIPTION
 .PP
-hledger can read CSV (comma-separated value, or character-separated
-value) files as if they were journal files, automatically converting
-each CSV record into a transaction.
+hledger can read CSV (Comma Separated Value/Character Separated Value)
+files as if they were journal files, automatically converting each CSV
+record into a transaction.
 (To learn about \f[I]writing\f[R] CSV, see CSV output.)
 .PP
 We describe each CSV file\[aq]s format with a corresponding \f[I]rules
@@ -52,6 +52,11 @@
 assign a value to one hledger field, with interpolation
 T}
 T{
+\f[B]\f[CB]separator\f[B]\f[R]
+T}@T{
+a custom field separator
+T}
+T{
 \f[B]\f[CB]if\f[B]\f[R]
 T}@T{
 apply some rules to matched CSV records
@@ -78,7 +83,11 @@
 T}
 .TE
 .PP
-There\[aq]s also a Convert CSV files tutorial on hledger.org.
+Note, for best error messages when reading CSV files, use a
+\f[C].csv\f[R], \f[C].tsv\f[R] or \f[C].ssv\f[R] file extension or file
+prefix - see File Extension below.
+.PP
+There\[aq]s an introductory Convert CSV files tutorial on hledger.org.
 .SH EXAMPLES
 .PP
 Here are some sample hledger CSV rules files.
@@ -113,7 +122,7 @@
 .nf
 \f[C]
 $ hledger print -f basic.csv
-2019/11/12 Foo
+2019-11-12 Foo
     expenses:unknown           10.23
     income:unknown            -10.23
 \f[R]
@@ -147,7 +156,7 @@
 # We generate balance assertions by assigning to \[dq]balance\[dq]
 # above, but you may sometimes need to remove these because:
 #
-# - the CSV balance differs from the true balance, 
+# - the CSV balance differs from the true balance,
 #   by up to 0.0000000000005 in my experience
 #
 # - it is sometimes calculated based on non-chronological ordering,
@@ -167,11 +176,11 @@
 .nf
 \f[C]
 $ hledger -f bankofireland-checking.csv print
-2012/12/07 LODGMENT       529898
+2012-12-07 LODGMENT       529898
     assets:bank:boi:checking         EUR10.0 = EUR131.2
     income:unknown                  EUR-10.0
 
-2012/12/07 PAYMENT
+2012-12-07 PAYMENT
     assets:bank:boi:checking         EUR-5.0 = EUR126.0
     expenses:unknown                  EUR5.0
 \f[R]
@@ -227,10 +236,7 @@
 #include categorisation.rules
 
 # add a third posting for fees, but only if they are non-zero.
-# Commas in the data makes counting fields hard, so count from the right instead.
-# (Regex translation: \[dq]a field containing a non-zero dollar amount, 
-# immediately before the 1 right-most fields\[dq])
-if ,\[rs]$[1-9][.0-9]+(,[\[ha],]*){1}$
+if %fees [1-9]
  account3    expenses:fees
  amount3     %fees
 \f[R]
@@ -239,11 +245,11 @@
 .nf
 \f[C]
 $ hledger -f amazon-orders.csv print
-2012/07/29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo.  ; status:Completed
+2012-07-29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo.  ; status:Completed
     assets:amazon
     expenses:misc          $20.00
 
-2012/07/30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc.  ; status:Completed
+2012-07-30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc.  ; status:Completed
     assets:amazon
     expenses:misc          $25.00
     expenses:fees           $1.00
@@ -289,23 +295,21 @@
 if
 In Progress
 Temporary Hold
-Update to 
+Update to
  skip
 
 # add more fields to the description
-description %description_ %itemtitle 
+description %description_ %itemtitle
 
 # save some other fields as tags
 comment  itemid:%itemid, fromemail:%fromemail, toemail:%toemail, time:%time, type:%type, status:%status_
 
 # convert to short currency symbols
-# Note: in conditional block regexps, the line of csv being matched is
-# a synthetic one: the unquoted field values, with commas between them.
-if ,USD,
+if %currency USD
  currency $
-if ,EUR,
+if %currency EUR
  currency E
-if ,GBP,
+if %currency GBP
  currency P
 
 # generate postings
@@ -319,9 +323,8 @@
 # (account2 is set below)
 amount2  -%grossamount
 
-# if there\[aq]s a fee (9th field), add a third posting for the money taken by paypal.
-# TODO: This regexp fails when fields contain a comma (generates a third posting with zero amount)
-if \[ha]([\[ha],]+,){8}[\[ha]0]
+# if there\[aq]s a fee, add a third posting for the money taken by paypal.
+if %feeamount [1-9]
  account3 expenses:banking:paypal
  amount3  -%feeamount
  comment3 business:
@@ -329,11 +332,11 @@
 # choose an account for the second posting
 
 # override the default account names:
-# if amount (8th field) is positive, it\[aq]s income (a debit)
-if \[ha]([\[ha],]+,){7}[0-9]
+# if the amount is positive, it\[aq]s income (a debit)
+if %grossamount \[ha][\[ha]-]
  account2 income:unknown
 # if negative, it\[aq]s an expense (a credit)
-if \[ha]([\[ha],]+,){7}-
+if %grossamount \[ha]-
  account2 expenses:unknown
 
 # apply common rules for setting account2 & other tweaks
@@ -341,9 +344,9 @@
 
 # apply some overrides specific to this csv
 
-# Transfers from/to bank. These are usually marked Pending, 
+# Transfers from/to bank. These are usually marked Pending,
 # which can be disregarded in this case.
-if 
+if
 Bank Account
 Bank Deposit to PP Account
  description %type for %referencetxnid %itemtitle
@@ -386,32 +389,32 @@
 .nf
 \f[C]
 $ hledger -f paypal-custom.csv  print
-2019/10/01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month  ; itemid:, fromemail:simon\[at]joyful.com, toemail:memberships\[at]calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed
+2019-10-01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month  ; itemid:, fromemail:simon\[at]joyful.com, toemail:memberships\[at]calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed
     assets:online:paypal          $-6.99 = $-6.99
     expenses:online:apps           $6.99
 
-2019/10/01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E  ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending
+2019-10-01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E  ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending
     assets:online:paypal               $6.99 = $0.00
     assets:bank:wf:pchecking          $-6.99
 
-2019/10/01 (2722394R5F586712G) Patreon Patreon* Membership  ; itemid:, fromemail:simon\[at]joyful.com, toemail:support\[at]patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed
+2019-10-01 (2722394R5F586712G) Patreon Patreon* Membership  ; itemid:, fromemail:simon\[at]joyful.com, toemail:support\[at]patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed
     assets:online:paypal          $-7.00 = $-7.00
     expenses:dues                  $7.00
 
-2019/10/01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership  ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending
+2019-10-01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership  ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending
     assets:online:paypal               $7.00 = $0.00
     assets:bank:wf:pchecking          $-7.00
 
-2019/10/19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation  ; itemid:, fromemail:simon\[at]joyful.com, toemail:tle\[at]wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed
+2019-10-19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation  ; itemid:, fromemail:simon\[at]joyful.com, toemail:tle\[at]wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed
     assets:online:paypal             $-2.00 = $-2.00
     expenses:dues                     $2.00
     expenses:banking:paypal      ; business:
 
-2019/10/19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M  ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending
+2019-10-19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M  ; itemid:, fromemail:, toemail:simon\[at]joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending
     assets:online:paypal               $2.00 = $0.00
     assets:bank:wf:pchecking          $-2.00
 
-2019/10/22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems  ; itemid:, fromemail:noble\[at]bene.fac.tor, toemail:simon\[at]joyful.com, time:05:07:06, type:Subscription Payment, status:Completed
+2019-10-22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems  ; itemid:, fromemail:noble\[at]bene.fac.tor, toemail:simon\[at]joyful.com, time:05:07:06, type:Subscription Payment, status:Completed
     assets:online:paypal                       $9.41 = $9.41
     revenues:foss donations:darcshub         $-10.00  ; business:
     expenses:banking:paypal                    $0.59  ; business:
@@ -472,6 +475,9 @@
 Currently there must be least two items (there must be at least one
 comma).
 .PP
+Note, always use comma in the fields list, even if your CSV uses another
+separator character.
+.PP
 Here are the standard hledger field/pseudo-field names.
 For more about the transaction parts they refer to, see the manual for
 hledger\[aq]s journal format.
@@ -544,17 +550,42 @@
 Interpolation strips outer whitespace (so a CSV value like
 \f[C]\[dq] 1 \[dq]\f[R] becomes \f[C]1\f[R] when interpolated) (#1051).
 See TIPS below for more about referencing other fields.
+.SS \f[C]separator\f[R]
+.PP
+You can use the \f[C]separator\f[R] directive to read other kinds of
+character-separated data.
+Eg to read SSV (Semicolon Separated Values), use:
+.IP
+.nf
+\f[C]
+separator ;
+\f[R]
+.fi
+.PP
+The separator directive accepts exactly one single byte character as a
+separator.
+To specify whitespace characters, you may use the special words
+\f[C]TAB\f[R] or \f[C]SPACE\f[R].
+Eg to read TSV (Tab Separated Values), use:
+.IP
+.nf
+\f[C]
+separator TAB
+\f[R]
+.fi
+.PP
+See also: File Extension.
 .SS \f[C]if\f[R]
 .IP
 .nf
 \f[C]
-if PATTERN
+if MATCHER
  RULE
 
 if
-PATTERN
-PATTERN
-PATTERN
+MATCHER
+MATCHER
+MATCHER
  RULE
  RULE
 \f[R]
@@ -565,28 +596,40 @@
 They are often used for customising account names based on transaction
 descriptions.
 .PP
-A single pattern can be written on the same line as the \[dq]if\[dq]; or
-multiple patterns can be written on the following lines, non-indented.
-Multiple patterns are OR\[aq]d (any one of them can match).
-Patterns are case-insensitive regular expressions which try to match
-anywhere within the whole CSV record (POSIX extended regular expressions
-with some additions, see
-https://hledger.org/hledger.html#regular-expressions).
-Note the CSV record they see is close to, but not identical to, the one
-in the CSV file; enclosing double quotes will be removed, and the
-separator character is always comma.
+Each MATCHER can be a record matcher, which looks like this:
+.IP
+.nf
+\f[C]
+REGEX
+\f[R]
+.fi
 .PP
-It\[aq]s not yet easy to match within a specific field.
-If the data does not contain commas, you can hack it with a regular
-expression like:
+REGEX is a case-insensitive regular expression which tries to match
+anywhere within the CSV record.
+It is a POSIX extended regular expressions with some additions (see
+Regular expressions in the hledger manual).
+Note: the \[dq]CSV record\[dq] it is matched against is not the original
+record, but a synthetic one, with enclosing double quotes or whitespace
+removed, and always comma-separated.
+(Eg, an SSV record \f[C]2020-01-01; \[dq]Acme, Inc.\[dq]; 1,000\f[R]
+appears to REGEX as \f[C]2020-01-01,Acme, Inc.,1,000\f[R]).
+.PP
+Or, MATCHER can be a field matcher, like this:
 .IP
 .nf
 \f[C]
-# match \[dq]foo\[dq] in the fourth field
-if \[ha]([\[ha],]*,){3}foo
+%CSVFIELD REGEX
 \f[R]
 .fi
 .PP
+which matches just the content of a particular CSV field.
+CSVFIELD is a percent sign followed by the field\[aq]s name or column
+number, like \f[C]%date\f[R] or \f[C]%1\f[R].
+.PP
+A single matcher can be written on the same line as the \[dq]if\[dq]; or
+multiple matchers can be written on the following lines, non-indented.
+Multiple matchers are OR\[aq]d (any one of them can match).
+.PP
 After the patterns there should be one or more rules to apply, all
 indented by at least one space.
 Three kinds of rule are allowed in conditional blocks:
@@ -763,6 +806,23 @@
 \f[R]
 .fi
 .SH TIPS
+.SS Rapid feedback
+.PP
+It\[aq]s a good idea to get rapid feedback while
+creating/troubleshooting CSV rules.
+Here\[aq]s a good way, using entr from http://eradman.com/entrproject :
+.IP
+.nf
+\f[C]
+$ ls foo.csv* | entr bash -c \[aq]echo ----; hledger -f foo.csv print desc:SOMEDESC\[aq]
+\f[R]
+.fi
+.PP
+A desc: query (eg) is used to select just one, or a few, transactions of
+interest.
+\[dq]bash -c\[dq] is used to run multiple commands, so we can echo a
+separator each time the command re-runs, making it easier to read the
+output.
 .SS Valid CSV
 .PP
 hledger accepts CSV conforming to RFC 4180.
@@ -771,19 +831,32 @@
 they must be double quotes (not single quotes)
 .IP \[bu] 2
 spaces outside the quotes are not allowed
-.SS Other separator characters
+.SS File Extension
 .PP
-With the \f[C]--separator \[aq]CHAR\[aq]\f[R] option (experimental),
-hledger will expect the separator to be CHAR instead of a comma.
-Ie it will read other \[dq]Character Separated Values\[dq] formats, such
-as TSV (Tab Separated Values).
-Note: on the command line, use a real tab character in quotes, not Eg:
+CSV (\[dq]Character Separated Values\[dq]) files should be named with
+one of these filename extensions: \f[C].csv\f[R], \f[C].ssv\f[R],
+\f[C].tsv\f[R].
+Or, the file path should be prefixed with one of \f[C]csv:\f[R],
+\f[C]ssv:\f[R], \f[C]tsv:\f[R].
+This helps hledger identify the format and show the right error
+messages.
+For example:
 .IP
 .nf
 \f[C]
-$ hledger -f foo.tsv --separator \[aq]  \[aq] print
+$ hledger -f foo.ssv print
 \f[R]
 .fi
+.PP
+or:
+.IP
+.nf
+\f[C]
+$ cat foo | hledger -f ssv:- foo
+\f[R]
+.fi
+.PP
+More about this: Input files in the hledger manual.
 .SS Reading multiple CSV files
 .PP
 If you use multiple \f[C]-f\f[R] options to read multiple CSV files at
diff --git a/hledger_csv.info b/hledger_csv.info
--- a/hledger_csv.info
+++ b/hledger_csv.info
@@ -3,11 +3,13 @@
 
 File: hledger_csv.info,  Node: Top,  Next: EXAMPLES,  Up: (dir)
 
-hledger_csv(5) hledger 1.16.2
-*****************************
+hledger_csv(5) hledger 1.17
+***************************
 
-hledger can read CSV (comma-separated value, or character-separated
-value) files as if they were journal files, automatically converting
+CSV - how hledger reads CSV data, and the CSV rules file format
+
+   hledger can read CSV (Comma Separated Value/Character Separated
+Value) files as if they were journal files, automatically converting
 each CSV record into a transaction.  (To learn about _writing_ CSV, see
 CSV output.)
 
@@ -30,14 +32,19 @@
 *'fields'*         name CSV fields, assign them to hledger fields
 *field             assign a value to one hledger field, with interpolation
 assignment*
+*'separator'*      a custom field separator
 *'if'*             apply some rules to matched CSV records
 *'end'*            skip the remaining CSV records
 *'date-format'*    describe the format of CSV dates
 *'newest-first'*   disambiguate record order when there's only one date
 *'include'*        inline another CSV rules file
 
-   There's also a Convert CSV files tutorial on hledger.org.
+   Note, for best error messages when reading CSV files, use a '.csv',
+'.tsv' or '.ssv' file extension or file prefix - see File Extension
+below.
 
+   There's an introductory Convert CSV files tutorial on hledger.org.
+
 * Menu:
 
 * EXAMPLES::
@@ -80,7 +87,7 @@
 date-format  %d/%m/%Y
 
 $ hledger print -f basic.csv
-2019/11/12 Foo
+2019-11-12 Foo
     expenses:unknown           10.23
     income:unknown            -10.23
 
@@ -111,7 +118,7 @@
 # We generate balance assertions by assigning to "balance"
 # above, but you may sometimes need to remove these because:
 #
-# - the CSV balance differs from the true balance, 
+# - the CSV balance differs from the true balance,
 #   by up to 0.0000000000005 in my experience
 #
 # - it is sometimes calculated based on non-chronological ordering,
@@ -127,11 +134,11 @@
 account1  assets:bank:boi:checking
 
 $ hledger -f bankofireland-checking.csv print
-2012/12/07 LODGMENT       529898
+2012-12-07 LODGMENT       529898
     assets:bank:boi:checking         EUR10.0 = EUR131.2
     income:unknown                  EUR-10.0
 
-2012/12/07 PAYMENT
+2012-12-07 PAYMENT
     assets:bank:boi:checking         EUR-5.0 = EUR126.0
     expenses:unknown                  EUR5.0
 
@@ -183,19 +190,16 @@
 #include categorisation.rules
 
 # add a third posting for fees, but only if they are non-zero.
-# Commas in the data makes counting fields hard, so count from the right instead.
-# (Regex translation: "a field containing a non-zero dollar amount, 
-# immediately before the 1 right-most fields")
-if ,\$[1-9][.0-9]+(,[^,]*){1}$
+if %fees [1-9]
  account3    expenses:fees
  amount3     %fees
 
 $ hledger -f amazon-orders.csv print
-2012/07/29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo.  ; status:Completed
+2012-07-29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo.  ; status:Completed
     assets:amazon
     expenses:misc          $20.00
 
-2012/07/30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc.  ; status:Completed
+2012-07-30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc.  ; status:Completed
     assets:amazon
     expenses:misc          $25.00
     expenses:fees           $1.00
@@ -238,23 +242,21 @@
 if
 In Progress
 Temporary Hold
-Update to 
+Update to
  skip
 
 # add more fields to the description
-description %description_ %itemtitle 
+description %description_ %itemtitle
 
 # save some other fields as tags
 comment  itemid:%itemid, fromemail:%fromemail, toemail:%toemail, time:%time, type:%type, status:%status_
 
 # convert to short currency symbols
-# Note: in conditional block regexps, the line of csv being matched is
-# a synthetic one: the unquoted field values, with commas between them.
-if ,USD,
+if %currency USD
  currency $
-if ,EUR,
+if %currency EUR
  currency E
-if ,GBP,
+if %currency GBP
  currency P
 
 # generate postings
@@ -268,9 +270,8 @@
 # (account2 is set below)
 amount2  -%grossamount
 
-# if there's a fee (9th field), add a third posting for the money taken by paypal.
-# TODO: This regexp fails when fields contain a comma (generates a third posting with zero amount)
-if ^([^,]+,){8}[^0]
+# if there's a fee, add a third posting for the money taken by paypal.
+if %feeamount [1-9]
  account3 expenses:banking:paypal
  amount3  -%feeamount
  comment3 business:
@@ -278,11 +279,11 @@
 # choose an account for the second posting
 
 # override the default account names:
-# if amount (8th field) is positive, it's income (a debit)
-if ^([^,]+,){7}[0-9]
+# if the amount is positive, it's income (a debit)
+if %grossamount ^[^-]
  account2 income:unknown
 # if negative, it's an expense (a credit)
-if ^([^,]+,){7}-
+if %grossamount ^-
  account2 expenses:unknown
 
 # apply common rules for setting account2 & other tweaks
@@ -290,9 +291,9 @@
 
 # apply some overrides specific to this csv
 
-# Transfers from/to bank. These are usually marked Pending, 
+# Transfers from/to bank. These are usually marked Pending,
 # which can be disregarded in this case.
-if 
+if
 Bank Account
 Bank Deposit to PP Account
  description %type for %referencetxnid %itemtitle
@@ -327,32 +328,32 @@
  description google | music
 
 $ hledger -f paypal-custom.csv  print
-2019/10/01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month  ; itemid:, fromemail:simon@joyful.com, toemail:memberships@calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed
+2019-10-01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month  ; itemid:, fromemail:simon@joyful.com, toemail:memberships@calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed
     assets:online:paypal          $-6.99 = $-6.99
     expenses:online:apps           $6.99
 
-2019/10/01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending
+2019-10-01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending
     assets:online:paypal               $6.99 = $0.00
     assets:bank:wf:pchecking          $-6.99
 
-2019/10/01 (2722394R5F586712G) Patreon Patreon* Membership  ; itemid:, fromemail:simon@joyful.com, toemail:support@patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed
+2019-10-01 (2722394R5F586712G) Patreon Patreon* Membership  ; itemid:, fromemail:simon@joyful.com, toemail:support@patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed
     assets:online:paypal          $-7.00 = $-7.00
     expenses:dues                  $7.00
 
-2019/10/01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership  ; itemid:, fromemail:, toemail:simon@joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending
+2019-10-01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership  ; itemid:, fromemail:, toemail:simon@joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending
     assets:online:paypal               $7.00 = $0.00
     assets:bank:wf:pchecking          $-7.00
 
-2019/10/19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation  ; itemid:, fromemail:simon@joyful.com, toemail:tle@wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed
+2019-10-19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation  ; itemid:, fromemail:simon@joyful.com, toemail:tle@wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed
     assets:online:paypal             $-2.00 = $-2.00
     expenses:dues                     $2.00
     expenses:banking:paypal      ; business:
 
-2019/10/19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending
+2019-10-19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending
     assets:online:paypal               $2.00 = $0.00
     assets:bank:wf:pchecking          $-2.00
 
-2019/10/22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems  ; itemid:, fromemail:noble@bene.fac.tor, toemail:simon@joyful.com, time:05:07:06, type:Subscription Payment, status:Completed
+2019-10-22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems  ; itemid:, fromemail:noble@bene.fac.tor, toemail:simon@joyful.com, time:05:07:06, type:Subscription Payment, status:Completed
     assets:online:paypal                       $9.41 = $9.41
     revenues:foss donations:darcshub         $-10.00  ; business:
     expenses:banking:paypal                    $0.59  ; business:
@@ -371,6 +372,7 @@
 * skip::
 * fields::
 * field assignment::
+* separator::
 * if::
 * end::
 * date-format::
@@ -422,6 +424,9 @@
 can be left unnamed.  Currently there must be least two items (there
 must be at least one comma).
 
+   Note, always use comma in the fields list, even if your CSV uses
+another separator character.
+
    Here are the standard hledger field/pseudo-field names.  For more
 about the transaction parts they refer to, see the manual for hledger's
 journal format.
@@ -476,7 +481,7 @@
    See TIPS below for more about setting amounts and currency.
 
 
-File: hledger_csv.info,  Node: field assignment,  Next: if,  Prev: fields,  Up: CSV RULES
+File: hledger_csv.info,  Node: field assignment,  Next: separator,  Prev: fields,  Up: CSV RULES
 
 2.3 field assignment
 ====================
@@ -501,18 +506,38 @@
 referencing other fields.
 
 
-File: hledger_csv.info,  Node: if,  Next: end,  Prev: field assignment,  Up: CSV RULES
+File: hledger_csv.info,  Node: separator,  Next: if,  Prev: field assignment,  Up: CSV RULES
 
-2.4 'if'
+2.4 'separator'
+===============
+
+You can use the 'separator' directive to read other kinds of
+character-separated data.  Eg to read SSV (Semicolon Separated Values),
+use:
+
+separator ;
+
+   The separator directive accepts exactly one single byte character as
+a separator.  To specify whitespace characters, you may use the special
+words 'TAB' or 'SPACE'.  Eg to read TSV (Tab Separated Values), use:
+
+separator TAB
+
+   See also: File Extension.
+
+
+File: hledger_csv.info,  Node: if,  Next: end,  Prev: separator,  Up: CSV RULES
+
+2.5 'if'
 ========
 
-if PATTERN
+if MATCHER
  RULE
 
 if
-PATTERN
-PATTERN
-PATTERN
+MATCHER
+MATCHER
+MATCHER
  RULE
  RULE
 
@@ -521,22 +546,31 @@
 often used for customising account names based on transaction
 descriptions.
 
-   A single pattern can be written on the same line as the "if"; or
-multiple patterns can be written on the following lines, non-indented.
-Multiple patterns are OR'd (any one of them can match).  Patterns are
-case-insensitive regular expressions which try to match anywhere within
-the whole CSV record (POSIX extended regular expressions with some
-additions, see https://hledger.org/hledger.html#regular-expressions).
-Note the CSV record they see is close to, but not identical to, the one
-in the CSV file; enclosing double quotes will be removed, and the
-separator character is always comma.
+   Each MATCHER can be a record matcher, which looks like this:
 
-   It's not yet easy to match within a specific field.  If the data does
-not contain commas, you can hack it with a regular expression like:
+REGEX
 
-# match "foo" in the fourth field
-if ^([^,]*,){3}foo
+   REGEX is a case-insensitive regular expression which tries to match
+anywhere within the CSV record.  It is a POSIX extended regular
+expressions with some additions (see Regular expressions in the hledger
+manual).  Note: the "CSV record" it is matched against is not the
+original record, but a synthetic one, with enclosing double quotes or
+whitespace removed, and always comma-separated.  (Eg, an SSV record
+'2020-01-01; "Acme, Inc."; 1,000' appears to REGEX as '2020-01-01,Acme,
+Inc.,1,000').
 
+   Or, MATCHER can be a field matcher, like this:
+
+%CSVFIELD REGEX
+
+   which matches just the content of a particular CSV field.  CSVFIELD
+is a percent sign followed by the field's name or column number, like
+'%date' or '%1'.
+
+   A single matcher can be written on the same line as the "if"; or
+multiple matchers can be written on the following lines, non-indented.
+Multiple matchers are OR'd (any one of them can match).
+
    After the patterns there should be one or more rules to apply, all
 indented by at least one space.  Three kinds of rule are allowed in
 conditional blocks:
@@ -562,7 +596,7 @@
 
 File: hledger_csv.info,  Node: end,  Next: date-format,  Prev: if,  Up: CSV RULES
 
-2.5 'end'
+2.6 'end'
 =========
 
 This rule can be used inside if blocks (only), to make hledger stop
@@ -576,7 +610,7 @@
 
 File: hledger_csv.info,  Node: date-format,  Next: newest-first,  Prev: end,  Up: CSV RULES
 
-2.6 'date-format'
+2.7 'date-format'
 =================
 
 date-format DATEFMT
@@ -607,7 +641,7 @@
 
 File: hledger_csv.info,  Node: newest-first,  Next: include,  Prev: date-format,  Up: CSV RULES
 
-2.7 'newest-first'
+2.8 'newest-first'
 ==================
 
 hledger always sorts the generated transactions by date.  Transactions
@@ -629,7 +663,7 @@
 
 File: hledger_csv.info,  Node: include,  Next: balance-type,  Prev: newest-first,  Up: CSV RULES
 
-2.8 'include'
+2.9 'include'
 =============
 
 include RULESFILE
@@ -652,8 +686,8 @@
 
 File: hledger_csv.info,  Node: balance-type,  Prev: include,  Up: CSV RULES
 
-2.9 'balance-type'
-==================
+2.10 'balance-type'
+===================
 
 Balance assertions generated by assigning to balanceN are of the simple
 '=' type by default, which is a single-commodity, subaccount-excluding
@@ -680,8 +714,9 @@
 
 * Menu:
 
+* Rapid feedback::
 * Valid CSV::
-* Other separator characters::
+* File Extension::
 * Reading multiple CSV files::
 * Valid transactions::
 * Deduplicating importing::
@@ -691,9 +726,26 @@
 * How CSV rules are evaluated::
 
 
-File: hledger_csv.info,  Node: Valid CSV,  Next: Other separator characters,  Up: TIPS
+File: hledger_csv.info,  Node: Rapid feedback,  Next: Valid CSV,  Up: TIPS
 
-3.1 Valid CSV
+3.1 Rapid feedback
+==================
+
+It's a good idea to get rapid feedback while creating/troubleshooting
+CSV rules.  Here's a good way, using entr from
+http://eradman.com/entrproject :
+
+$ ls foo.csv* | entr bash -c 'echo ----; hledger -f foo.csv print desc:SOMEDESC'
+
+   A desc: query (eg) is used to select just one, or a few, transactions
+of interest.  "bash -c" is used to run multiple commands, so we can echo
+a separator each time the command re-runs, making it easier to read the
+output.
+
+
+File: hledger_csv.info,  Node: Valid CSV,  Next: File Extension,  Prev: Rapid feedback,  Up: TIPS
+
+3.2 Valid CSV
 =============
 
 hledger accepts CSV conforming to RFC 4180.  When CSV values are
@@ -703,23 +755,29 @@
    * spaces outside the quotes are not allowed
 
 
-File: hledger_csv.info,  Node: Other separator characters,  Next: Reading multiple CSV files,  Prev: Valid CSV,  Up: TIPS
+File: hledger_csv.info,  Node: File Extension,  Next: Reading multiple CSV files,  Prev: Valid CSV,  Up: TIPS
 
-3.2 Other separator characters
-==============================
+3.3 File Extension
+==================
 
-With the '--separator 'CHAR'' option (experimental), hledger will expect
-the separator to be CHAR instead of a comma.  Ie it will read other
-"Character Separated Values" formats, such as TSV (Tab Separated
-Values).  Note: on the command line, use a real tab character in quotes,
-not
+CSV ("Character Separated Values") files should be named with one of
+these filename extensions: '.csv', '.ssv', '.tsv'.  Or, the file path
+should be prefixed with one of 'csv:', 'ssv:', 'tsv:'.  This helps
+hledger identify the format and show the right error messages.  For
+example:
 
-$ hledger -f foo.tsv --separator '  ' print
+$ hledger -f foo.ssv print
 
+   or:
+
+$ cat foo | hledger -f ssv:- foo
+
+   More about this: Input files in the hledger manual.
+
 
-File: hledger_csv.info,  Node: Reading multiple CSV files,  Next: Valid transactions,  Prev: Other separator characters,  Up: TIPS
+File: hledger_csv.info,  Node: Reading multiple CSV files,  Next: Valid transactions,  Prev: File Extension,  Up: TIPS
 
-3.3 Reading multiple CSV files
+3.4 Reading multiple CSV files
 ==============================
 
 If you use multiple '-f' options to read multiple CSV files at once,
@@ -730,7 +788,7 @@
 
 File: hledger_csv.info,  Node: Valid transactions,  Next: Deduplicating importing,  Prev: Reading multiple CSV files,  Up: TIPS
 
-3.4 Valid transactions
+3.5 Valid transactions
 ======================
 
 After reading a CSV file, hledger post-processes and validates the
@@ -749,7 +807,7 @@
 
 File: hledger_csv.info,  Node: Deduplicating importing,  Next: Setting amounts,  Prev: Valid transactions,  Up: TIPS
 
-3.5 Deduplicating, importing
+3.6 Deduplicating, importing
 ============================
 
 When you download a CSV file periodically, eg to get your latest bank
@@ -779,7 +837,7 @@
 
 File: hledger_csv.info,  Node: Setting amounts,  Next: Setting currency/commodity,  Prev: Deduplicating importing,  Up: TIPS
 
-3.6 Setting amounts
+3.7 Setting amounts
 ===================
 
 A posting amount can be set in one of these ways:
@@ -808,7 +866,7 @@
 
 File: hledger_csv.info,  Node: Setting currency/commodity,  Next: Referencing other fields,  Prev: Setting amounts,  Up: TIPS
 
-3.7 Setting currency/commodity
+3.8 Setting currency/commodity
 ==============================
 
 If the currency/commodity symbol is included in the CSV's amount
@@ -835,7 +893,7 @@
 
 File: hledger_csv.info,  Node: Referencing other fields,  Next: How CSV rules are evaluated,  Prev: Setting currency/commodity,  Up: TIPS
 
-3.8 Referencing other fields
+3.9 Referencing other fields
 ============================
 
 In field assignments, you can interpolate only CSV fields, not hledger
@@ -872,8 +930,8 @@
 
 File: hledger_csv.info,  Node: How CSV rules are evaluated,  Prev: Referencing other fields,  Up: TIPS
 
-3.9 How CSV rules are evaluated
-===============================
+3.10 How CSV rules are evaluated
+================================
 
 Here's how to think of CSV rules being evaluated (if you really need
 to).  First,
@@ -913,60 +971,64 @@
 
 Tag Table:
 Node: Top72
-Node: EXAMPLES1833
-Ref: #examples1939
-Node: Basic2147
-Ref: #basic2247
-Node: Bank of Ireland2789
-Ref: #bank-of-ireland2924
-Node: Amazon4387
-Ref: #amazon4505
-Node: Paypal6438
-Ref: #paypal6532
-Node: CSV RULES14415
-Ref: #csv-rules14524
-Node: skip14786
-Ref: #skip14879
-Node: fields15254
-Ref: #fields15376
-Node: Transaction field names16443
-Ref: #transaction-field-names16603
-Node: Posting field names16714
-Ref: #posting-field-names16866
-Node: field assignment18157
-Ref: #field-assignment18293
-Node: if19111
-Ref: #if19220
-Node: end20936
-Ref: #end21042
-Node: date-format21266
-Ref: #date-format21398
-Node: newest-first22147
-Ref: #newest-first22285
-Node: include22968
-Ref: #include23097
-Node: balance-type23541
-Ref: #balance-type23659
-Node: TIPS24359
-Ref: #tips24441
-Node: Valid CSV24690
-Ref: #valid-csv24809
-Node: Other separator characters25001
-Ref: #other-separator-characters25189
-Node: Reading multiple CSV files25518
-Ref: #reading-multiple-csv-files25715
-Node: Valid transactions25956
-Ref: #valid-transactions26134
-Node: Deduplicating importing26762
-Ref: #deduplicating-importing26941
-Node: Setting amounts27974
-Ref: #setting-amounts28143
-Node: Setting currency/commodity29129
-Ref: #setting-currencycommodity29321
-Node: Referencing other fields30124
-Ref: #referencing-other-fields30324
-Node: How CSV rules are evaluated31221
-Ref: #how-csv-rules-are-evaluated31392
+Node: EXAMPLES2093
+Ref: #examples2199
+Node: Basic2407
+Ref: #basic2507
+Node: Bank of Ireland3049
+Ref: #bank-of-ireland3184
+Node: Amazon4646
+Ref: #amazon4764
+Node: Paypal6483
+Ref: #paypal6577
+Node: CSV RULES14221
+Ref: #csv-rules14330
+Node: skip14606
+Ref: #skip14699
+Node: fields15074
+Ref: #fields15196
+Node: Transaction field names16361
+Ref: #transaction-field-names16521
+Node: Posting field names16632
+Ref: #posting-field-names16784
+Node: field assignment18075
+Ref: #field-assignment18218
+Node: separator19036
+Ref: #separator19165
+Node: if19576
+Ref: #if19678
+Node: end21597
+Ref: #end21703
+Node: date-format21927
+Ref: #date-format22059
+Node: newest-first22808
+Ref: #newest-first22946
+Node: include23629
+Ref: #include23758
+Node: balance-type24202
+Ref: #balance-type24322
+Node: TIPS25022
+Ref: #tips25104
+Node: Rapid feedback25360
+Ref: #rapid-feedback25477
+Node: Valid CSV25937
+Ref: #valid-csv26067
+Node: File Extension26259
+Ref: #file-extension26411
+Node: Reading multiple CSV files26821
+Ref: #reading-multiple-csv-files27006
+Node: Valid transactions27247
+Ref: #valid-transactions27425
+Node: Deduplicating importing28053
+Ref: #deduplicating-importing28232
+Node: Setting amounts29265
+Ref: #setting-amounts29434
+Node: Setting currency/commodity30420
+Ref: #setting-currencycommodity30612
+Node: Referencing other fields31415
+Ref: #referencing-other-fields31615
+Node: How CSV rules are evaluated32512
+Ref: #how-csv-rules-are-evaluated32685
 
 End Tag Table
 
diff --git a/hledger_csv.txt b/hledger_csv.txt
--- a/hledger_csv.txt
+++ b/hledger_csv.txt
@@ -7,10 +7,10 @@
        CSV - how hledger reads CSV data, and the CSV rules file format
 
 DESCRIPTION
-       hledger  can  read  CSV  (comma-separated value, or character-separated
-       value) files as if they were journal  files,  automatically  converting
-       each  CSV  record into a transaction.  (To learn about writing CSV, see
-       CSV output.)
+       hledger  can read CSV (Comma Separated Value/Character Separated Value)
+       files as if they were journal files, automatically converting each  CSV
+       record  into  a transaction.  (To learn about writing CSV, see CSV out-
+       put.)
 
        We describe each CSV file's format with a corresponding rules file.  By
        default  this is named like the CSV file with a .rules extension added.
@@ -34,6 +34,7 @@
        field assignment   assign   a  value  to  one
                           hledger field, with inter-
                           polation
+       separator          a custom field separator
        if                 apply    some   rules   to
                           matched CSV records
        end                skip  the  remaining   CSV
@@ -45,16 +46,19 @@
        include            inline  another  CSV rules
                           file
 
-       There's also a Convert CSV files tutorial on hledger.org.
+       Note, for best error messages when reading CSV files, use a .csv,  .tsv
+       or .ssv file extension or file prefix - see File Extension below.
 
+       There's an introductory Convert CSV files tutorial on hledger.org.
+
 EXAMPLES
-       Here are some sample hledger CSV rules files.  See also the  full  col-
+       Here  are  some sample hledger CSV rules files.  See also the full col-
        lection at:
        https://github.com/simonmichael/hledger/tree/master/examples/csv
 
    Basic
-       At  minimum,  the  rules file must identify the date and amount fields,
-       and often it also specifies the date format and how many  header  lines
+       At minimum, the rules file must identify the date  and  amount  fields,
+       and  often  it also specifies the date format and how many header lines
        there are.  Here's a simple CSV file and a rules file for it:
 
               Date, Description, Id, Amount
@@ -66,15 +70,15 @@
               date-format  %d/%m/%Y
 
               $ hledger print -f basic.csv
-              2019/11/12 Foo
+              2019-11-12 Foo
                   expenses:unknown           10.23
                   income:unknown            -10.23
 
        Default account names are chosen, since we didn't set them.
 
    Bank of Ireland
-       Here's  a  CSV with two amount fields (Debit and Credit), and a balance
-       field, which we can use to add balance assertions, which is not  neces-
+       Here's a CSV with two amount fields (Debit and Credit), and  a  balance
+       field,  which we can use to add balance assertions, which is not neces-
        sary but provides extra error checking:
 
               Date,Details,Debit,Credit,Balance
@@ -108,21 +112,21 @@
               account1  assets:bank:boi:checking
 
               $ hledger -f bankofireland-checking.csv print
-              2012/12/07 LODGMENT       529898
+              2012-12-07 LODGMENT       529898
                   assets:bank:boi:checking         EUR10.0 = EUR131.2
                   income:unknown                  EUR-10.0
 
-              2012/12/07 PAYMENT
+              2012-12-07 PAYMENT
                   assets:bank:boi:checking         EUR-5.0 = EUR126.0
                   expenses:unknown                  EUR5.0
 
-       The  balance assertions don't raise an error above, because we're read-
-       ing directly from CSV, but they will be checked if  these  entries  are
+       The balance assertions don't raise an error above, because we're  read-
+       ing  directly  from  CSV, but they will be checked if these entries are
        imported into a journal file.
 
    Amazon
        Here we convert amazon.com order history, and use an if block to gener-
-       ate a third posting if there's a fee.  (In practice you'd probably  get
+       ate  a third posting if there's a fee.  (In practice you'd probably get
        this data from your bank instead, but it's an example.)
 
               "Date","Type","To/From","Name","Status","Amount","Fees","Transaction ID"
@@ -159,25 +163,22 @@
               #include categorisation.rules
 
               # add a third posting for fees, but only if they are non-zero.
-              # Commas in the data makes counting fields hard, so count from the right instead.
-              # (Regex translation: "a field containing a non-zero dollar amount,
-              # immediately before the 1 right-most fields")
-              if ,\$[1-9][.0-9]+(,[^,]*){1}$
+              if %fees [1-9]
                account3    expenses:fees
                amount3     %fees
 
               $ hledger -f amazon-orders.csv print
-              2012/07/29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo.  ; status:Completed
+              2012-07-29 (16000000000000DGLNJPI1P9B8DKPVHL) To Foo.  ; status:Completed
                   assets:amazon
                   expenses:misc          $20.00
 
-              2012/07/30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc.  ; status:Completed
+              2012-07-30 (17LA58JSKRD4HDGLNJPI1P9B8DKPVHL) To Adapteva, Inc.  ; status:Completed
                   assets:amazon
                   expenses:misc          $25.00
                   expenses:fees           $1.00
 
    Paypal
-       Here's  a  real-world rules file for (customised) Paypal CSV, with some
+       Here's a real-world rules file for (customised) Paypal CSV,  with  some
        Paypal-specific rules, and a second rules file included:
 
               "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"
@@ -219,13 +220,11 @@
               comment  itemid:%itemid, fromemail:%fromemail, toemail:%toemail, time:%time, type:%type, status:%status_
 
               # convert to short currency symbols
-              # Note: in conditional block regexps, the line of csv being matched is
-              # a synthetic one: the unquoted field values, with commas between them.
-              if ,USD,
+              if %currency USD
                currency $
-              if ,EUR,
+              if %currency EUR
                currency E
-              if ,GBP,
+              if %currency GBP
                currency P
 
               # generate postings
@@ -239,9 +238,8 @@
               # (account2 is set below)
               amount2  -%grossamount
 
-              # if there's a fee (9th field), add a third posting for the money taken by paypal.
-              # TODO: This regexp fails when fields contain a comma (generates a third posting with zero amount)
-              if ^([^,]+,){8}[^0]
+              # if there's a fee, add a third posting for the money taken by paypal.
+              if %feeamount [1-9]
                account3 expenses:banking:paypal
                amount3  -%feeamount
                comment3 business:
@@ -249,11 +247,11 @@
               # choose an account for the second posting
 
               # override the default account names:
-              # if amount (8th field) is positive, it's income (a debit)
-              if ^([^,]+,){7}[0-9]
+              # if the amount is positive, it's income (a debit)
+              if %grossamount ^[^-]
                account2 income:unknown
               # if negative, it's an expense (a credit)
-              if ^([^,]+,){7}-
+              if %grossamount ^-
                account2 expenses:unknown
 
               # apply common rules for setting account2 & other tweaks
@@ -298,32 +296,32 @@
                description google | music
 
               $ hledger -f paypal-custom.csv  print
-              2019/10/01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month  ; itemid:, fromemail:simon@joyful.com, toemail:memberships@calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed
+              2019-10-01 (60P57143A8206782E) Calm Radio MONTHLY - $1 for the first 2 Months: Me - Order 99309. Item total: $1.00 USD first 2 months, then $6.99 / Month  ; itemid:, fromemail:simon@joyful.com, toemail:memberships@calmradio.com, time:03:46:20, type:Subscription Payment, status:Completed
                   assets:online:paypal          $-6.99 = $-6.99
                   expenses:online:apps           $6.99
 
-              2019/10/01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending
+              2019-10-01 (0TU1544T080463733) Bank Deposit to PP Account for 60P57143A8206782E  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:46:20, type:Bank Deposit to PP Account, status:Pending
                   assets:online:paypal               $6.99 = $0.00
                   assets:bank:wf:pchecking          $-6.99
 
-              2019/10/01 (2722394R5F586712G) Patreon Patreon* Membership  ; itemid:, fromemail:simon@joyful.com, toemail:support@patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed
+              2019-10-01 (2722394R5F586712G) Patreon Patreon* Membership  ; itemid:, fromemail:simon@joyful.com, toemail:support@patreon.com, time:08:57:01, type:PreApproved Payment Bill User Payment, status:Completed
                   assets:online:paypal          $-7.00 = $-7.00
                   expenses:dues                  $7.00
 
-              2019/10/01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership  ; itemid:, fromemail:, toemail:simon@joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending
+              2019-10-01 (71854087RG994194F) Bank Deposit to PP Account for 2722394R5F586712G Patreon* Membership  ; itemid:, fromemail:, toemail:simon@joyful.com, time:08:57:01, type:Bank Deposit to PP Account, status:Pending
                   assets:online:paypal               $7.00 = $0.00
                   assets:bank:wf:pchecking          $-7.00
 
-              2019/10/19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation  ; itemid:, fromemail:simon@joyful.com, toemail:tle@wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed
+              2019-10-19 (K9U43044RY432050M) Wikimedia Foundation, Inc. Monthly donation to the Wikimedia Foundation  ; itemid:, fromemail:simon@joyful.com, toemail:tle@wikimedia.org, time:03:02:12, type:Subscription Payment, status:Completed
                   assets:online:paypal             $-2.00 = $-2.00
                   expenses:dues                     $2.00
                   expenses:banking:paypal      ; business:
 
-              2019/10/19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending
+              2019-10-19 (3XJ107139A851061F) Bank Deposit to PP Account for K9U43044RY432050M  ; itemid:, fromemail:, toemail:simon@joyful.com, time:03:02:12, type:Bank Deposit to PP Account, status:Pending
                   assets:online:paypal               $2.00 = $0.00
                   assets:bank:wf:pchecking          $-2.00
 
-              2019/10/22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems  ; itemid:, fromemail:noble@bene.fac.tor, toemail:simon@joyful.com, time:05:07:06, type:Subscription Payment, status:Completed
+              2019-10-22 (6L8L1662YP1334033) Noble Benefactor Joyful Systems  ; itemid:, fromemail:noble@bene.fac.tor, toemail:simon@joyful.com, time:05:07:06, type:Subscription Payment, status:Completed
                   assets:online:paypal                       $9.41 = $9.41
                   revenues:foss donations:darcshub         $-10.00  ; business:
                   expenses:banking:paypal                    $0.59  ; business:
@@ -335,9 +333,9 @@
    skip
               skip N
 
-       The  word  "skip"  followed by a number (or no number, meaning 1) tells
-       hledger to ignore this many non-empty lines  preceding  the  CSV  data.
-       (Empty/blank  lines  are skipped automatically.) You'll need this when-
+       The word "skip" followed by a number (or no number,  meaning  1)  tells
+       hledger  to  ignore  this  many non-empty lines preceding the CSV data.
+       (Empty/blank lines are skipped automatically.) You'll need  this  when-
        ever your CSV data contains header lines.
 
        It also has a second purpose: it can be used inside if blocks to ignore
@@ -346,26 +344,29 @@
    fields
               fields FIELDNAME1, FIELDNAME2, ...
 
-       A  fields  list  (the  word  "fields" followed by comma-separated field
-       names) is the quick way to assign CSV field values to  hledger  fields.
+       A fields list (the word  "fields"  followed  by  comma-separated  field
+       names)  is  the quick way to assign CSV field values to hledger fields.
        It does two things:
 
-       1. it  names  the  CSV fields.  This is optional, but can be convenient
+       1. it names the CSV fields.  This is optional, but  can  be  convenient
           later for interpolating them.
 
        2. when you use a standard hledger field name, it assigns the CSV value
           to that part of the hledger transaction.
 
-       Here's  an  example  that  says "use the 1st, 2nd and 4th fields as the
-       transaction's date, description and amount; name the  last  two  fields
+       Here's an example that says "use the 1st, 2nd and  4th  fields  as  the
+       transaction's  date,  description  and amount; name the last two fields
        for later reference; and ignore the others":
 
               fields date, description, , amount, , , somefield, anotherfield
 
-       Field  names  may  not contain whitespace.  Fields you don't care about
-       can be left unnamed.  Currently there must be least  two  items  (there
+       Field names may not contain whitespace.  Fields you  don't  care  about
+       can  be  left  unnamed.  Currently there must be least two items (there
        must be at least one comma).
 
+       Note, always use comma in the fields list, even if your  CSV  uses  an-
+       other separator character.
+
        Here are the standard hledger field/pseudo-field names.  For more about
        the transaction parts they refer to, see the manual for hledger's jour-
        nal format.
@@ -424,37 +425,59 @@
        comes 1 when interpolated) (#1051).  See TIPS below for more about ref-
        erencing other fields.
 
+   separator
+       You can use the separator directive to read other kinds  of  character-
+       separated data.  Eg to read SSV (Semicolon Separated Values), use:
+
+              separator ;
+
+       The  separator directive accepts exactly one single byte character as a
+       separator.  To specify whitespace characters, you may use  the  special
+       words TAB or SPACE.  Eg to read TSV (Tab Separated Values), use:
+
+              separator TAB
+
+       See also: File Extension.
+
    if
-              if PATTERN
+              if MATCHER
                RULE
 
               if
-              PATTERN
-              PATTERN
-              PATTERN
+              MATCHER
+              MATCHER
+              MATCHER
                RULE
                RULE
 
-       Conditional blocks ("if blocks") are a block of rules that are  applied
-       only  to CSV records which match certain patterns.  They are often used
+       Conditional  blocks ("if blocks") are a block of rules that are applied
+       only to CSV records which match certain patterns.  They are often  used
        for customising account names based on transaction descriptions.
 
-       A single pattern can be written on the same line as the "if"; or multi-
-       ple patterns can be written on the following lines, non-indented.  Mul-
-       tiple patterns are OR'd (any one of  them  can  match).   Patterns  are
-       case-insensitive regular expressions which try to match anywhere within
-       the whole CSV record (POSIX extended regular expressions with some  ad-
-       ditions,   see   https://hledger.org/hledger.html#regular-expressions).
-       Note the CSV record they see is close to, but not identical to, the one
-       in the CSV file; enclosing double quotes will be removed, and the sepa-
-       rator character is always comma.
+       Each MATCHER can be a record matcher, which looks like this:
 
-       It's not yet easy to match within a specific field.  If the  data  does
-       not contain commas, you can hack it with a regular expression like:
+              REGEX
 
-              # match "foo" in the fourth field
-              if ^([^,]*,){3}foo
+       REGEX  is  a  case-insensitive  regular expression which tries to match
+       anywhere within the CSV record.  It is a POSIX extended regular expres-
+       sions  with some additions (see Regular expressions in the hledger man-
+       ual).  Note: the "CSV record" it is matched against is not the original
+       record, but a synthetic one, with enclosing double quotes or whitespace
+       removed, and always comma-separated.  (Eg, an  SSV  record  2020-01-01;
+       "Acme, Inc."; 1,000 appears to REGEX as 2020-01-01,Acme, Inc.,1,000).
 
+       Or, MATCHER can be a field matcher, like this:
+
+              %CSVFIELD REGEX
+
+       which  matches just the content of a particular CSV field.  CSVFIELD is
+       a percent sign followed by the field's  name  or  column  number,  like
+       %date or %1.
+
+       A single matcher can be written on the same line as the "if"; or multi-
+       ple matchers can be written on the following lines, non-indented.  Mul-
+       tiple matchers are OR'd (any one of them can match).
+
        After  the patterns there should be one or more rules to apply, all in-
        dented by at least one space.  Three kinds of rule are allowed in  con-
        ditional blocks:
@@ -571,23 +594,40 @@
               ==*  multi commodity,  include subaccounts
 
 TIPS
+   Rapid feedback
+       It's  a  good idea to get rapid feedback while creating/troubleshooting
+       CSV rules.  Here's a good way, using entr from http://eradman.com/entr-
+       project :
+
+              $ ls foo.csv* | entr bash -c 'echo ----; hledger -f foo.csv print desc:SOMEDESC'
+
+       A  desc:  query (eg) is used to select just one, or a few, transactions
+       of interest.  "bash -c" is used to run multiple  commands,  so  we  can
+       echo  a  separator  each  time the command re-runs, making it easier to
+       read the output.
+
    Valid CSV
-       hledger  accepts  CSV  conforming to RFC 4180.  When CSV values are en-
+       hledger accepts CSV conforming to RFC 4180.  When CSV  values  are  en-
        closed in quotes, note:
 
        o they must be double quotes (not single quotes)
 
        o spaces outside the quotes are not allowed
 
-   Other separator characters
-       With the --separator 'CHAR' option (experimental), hledger will  expect
-       the  separator  to  be  CHAR instead of a comma.  Ie it will read other
-       "Character Separated Values" formats, such as TSV (Tab  Separated  Val-
-       ues).   Note:  on the command line, use a real tab character in quotes,
-       not Eg:
+   File Extension
+       CSV  ("Character  Separated  Values") files should be named with one of
+       these filename extensions: .csv, .ssv, .tsv.  Or, the file path  should
+       be  prefixed with one of csv:, ssv:, tsv:.  This helps hledger identify
+       the format and show the right error messages.  For example:
 
-              $ hledger -f foo.tsv --separator '  ' print
+              $ hledger -f foo.ssv print
 
+       or:
+
+              $ cat foo | hledger -f ssv:- foo
+
+       More about this: Input files in the hledger manual.
+
    Reading multiple CSV files
        If you use multiple -f options to read  multiple  CSV  files  at  once,
        hledger  will  look for a correspondingly-named rules file for each CSV
@@ -779,4 +819,4 @@
 
 
 
-hledger 1.16.2                   January 2020                   hledger_csv(5)
+hledger 1.17                      March 2020                    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" "January 2020" "hledger 1.16.2" "hledger User Manuals"
+.TH "hledger_journal" "5" "March 2020" "hledger 1.17" "hledger User Manuals"
 
 
 
@@ -26,124 +26,92 @@
 same journal file, eg to validate the results you\[aq]re getting.
 .PP
 You can use hledger without learning any more about this file; just use
-the add or web commands to create and update it.
-Many users, though, also edit the journal file directly with a text
-editor, perhaps assisted by the helper modes for emacs or vim.
+the add or web or import commands to create and update it.
 .PP
-Here\[aq]s an example:
-.IP
-.nf
-\f[C]
-; A sample journal file. This is a comment.
-
-2008/01/01 income             ; <- transaction\[aq]s first line starts in column 0, contains date and description
-    assets:bank:checking  $1  ; <- posting lines start with whitespace, each contains an account name
-    income:salary        $-1  ;    followed by at least two spaces and an amount
-
-2008/06/01 gift
-    assets:bank:checking  $1  ; <- at least two postings in a transaction
-    income:gifts         $-1  ; <- their amounts must balance to 0
-
-2008/06/02 save
-    assets:bank:saving    $1
-    assets:bank:checking      ; <- one amount may be omitted; here $-1 is inferred
-
-2008/06/03 eat & shop         ; <- description can be anything
-    expenses:food         $1
-    expenses:supplies     $1  ; <- this transaction debits two expense accounts
-    assets:cash               ; <- $-2 inferred
-
-2008/10/01 take a loan
-    assets:bank:checking  $1
-    liabilities:debts    $-1
-
-2008/12/31 * pay off          ; <- an optional * or ! after the date means \[dq]cleared\[dq] (or anything you want)
-    liabilities:debts     $1
-    assets:bank:checking
-\f[R]
-.fi
+Many users, though, edit the journal file with a text editor, and track
+changes with a version control system such as git.
+Editor addons such as ledger-mode or hledger-mode for Emacs, vim-ledger
+for Vim, and hledger-vscode for Visual Studio Code, make this easier,
+adding colour, formatting, tab completion, and useful commands.
+See Editor configuration at hledger.org for the full list.
 .SH FILE FORMAT
+.PP
+Here\[aq]s a description of each part of the file format (and
+hledger\[aq]s data model).
+These are mostly in the order you\[aq]ll use them, but in some cases
+related concepts have been grouped together for easy reference, or
+linked before they are introduced, so feel free to skip over anything
+that looks unnecessary right now.
 .SS Transactions
 .PP
-Transactions are movements of some quantity of commodities between named
-accounts.
-Each transaction is represented by a journal entry beginning with a
-simple date in column 0.
-This can be followed by any of the following, separated by spaces:
-.IP \[bu] 2
-(optional) a status character (empty, \f[C]!\f[R], or \f[C]*\f[R])
-.IP \[bu] 2
-(optional) a transaction code (any short number or text, enclosed in
-parentheses)
+Transactions are the main unit of information in a journal file.
+They represent events, typically a movement of some quantity of
+commodities between two or more named accounts.
+.PP
+Each transaction is recorded as a journal entry, beginning with a simple
+date in column 0.
+This can be followed by any of the following optional fields, separated
+by spaces:
 .IP \[bu] 2
-(optional) a transaction description (any remaining text until end of
-line or a semicolon)
+a status character (empty, \f[C]!\f[R], or \f[C]*\f[R])
 .IP \[bu] 2
-(optional) a transaction comment (any remaining text following a
-semicolon until end of line)
-.PP
-Then comes zero or more (but usually at least 2) indented lines
-representing...
-.SS Postings
-.PP
-A posting is an addition of some amount to, or removal of some amount
-from, an account.
-Each posting line begins with at least one space or tab (2 or 4 spaces
-is common), followed by:
+a code (any short number or text, enclosed in parentheses)
 .IP \[bu] 2
-(optional) a status character (empty, \f[C]!\f[R], or \f[C]*\f[R]),
-followed by a space
+a description (any remaining text until end of line or a semicolon)
 .IP \[bu] 2
-(required) an account name (any text, optionally containing \f[B]single
-spaces\f[R], until end of line or a double space)
+a comment (any remaining text following a semicolon until end of line,
+and any following indented lines beginning with a semicolon)
 .IP \[bu] 2
-(optional) \f[B]two or more spaces\f[R] or tabs followed by an amount.
-.PP
-Positive amounts are being added to the account, negative amounts are
-being removed.
-.PP
-The amounts within a transaction must always sum up to zero.
-As a convenience, one amount may be left blank; it will be inferred so
-as to balance the transaction.
+0 or more indented \f[I]posting\f[R] lines, describing what was
+transferred and the accounts involved.
 .PP
-Be sure to note the unusual two-space delimiter between account name and
-amount.
-This makes it easy to write account names containing spaces.
-But if you accidentally leave only one space (or tab) before the amount,
-the amount will be considered part of the account name.
+Here\[aq]s a simple journal file containing one transaction:
+.IP
+.nf
+\f[C]
+2008/01/01 income
+  assets:bank:checking   $1
+  income:salary         $-1
+\f[R]
+.fi
 .SS Dates
 .SS Simple dates
 .PP
-Within a journal file, transaction dates use Y/M/D (or Y-M-D or Y.M.D)
-Leading zeros are optional.
+Dates in the journal file use \f[I]simple dates\f[R] format:
+\f[C]YYYY-MM-DD\f[R] or \f[C]YYYY/MM/DD\f[R] or \f[C]YYYY.MM.DD\f[R],
+with leading zeros optional.
 The year may be omitted, in which case it will be inferred from the
-context - the current transaction, the default year set with a default
+context: the current transaction, the default year set with a default
 year directive, or the current date when the command is run.
-Some examples: \f[C]2010/01/31\f[R], \f[C]1/31\f[R],
-\f[C]2010-01-31\f[R], \f[C]2010.1.31\f[R].
+Some examples: \f[C]2010-01-31\f[R], \f[C]2010/01/31\f[R],
+\f[C]2010.1.31\f[R], \f[C]1/31\f[R].
+.PP
+(The UI also accepts simple dates, as well as the more flexible smart
+dates documented in the hledger manual.)
 .SS Secondary dates
 .PP
 Real-life transactions sometimes involve more than one date - eg the
 date you write a cheque, and the date it clears in your bank.
-When you want to model this, eg for more accurate balances, you can
-specify individual posting dates, which I recommend.
-Or, you can use the secondary dates (aka auxiliary/effective dates)
-feature, supported for compatibility with Ledger.
+When you want to model this, for more accurate daily balances, you can
+specify individual posting dates.
 .PP
-A secondary date can be written after the primary date, separated by an
-equals sign.
-The primary date, on the left, is used by default; the secondary date,
-on the right, is used when the \f[C]--date2\f[R] flag is specified
-(\f[C]--aux-date\f[R] or \f[C]--effective\f[R] also work).
+Or, you can use the older \f[I]secondary date\f[R] feature (Ledger calls
+it auxiliary date or effective date).
+Note: we support this for compatibility, but I usually recommend
+avoiding this feature; posting dates are almost always clearer and
+simpler.
 .PP
+A secondary date is written after the primary date, following an equals
+sign.
+If the year is omitted, the primary date\[aq]s year is assumed.
+When running reports, the primary (left) date is used by default, but
+with the \f[C]--date2\f[R] flag (or \f[C]--aux-date\f[R] or
+\f[C]--effective\f[R]), the secondary (right) date will be used instead.
+.PP
 The meaning of secondary dates is up to you, but it\[aq]s best to follow
 a consistent rule.
-Eg write the bank\[aq]s clearing date as primary, and when needed, the
-date the transaction was initiated as secondary.
-.PP
-Here\[aq]s an example.
-Note that a secondary date will use the year of the primary date if
-unspecified.
+Eg \[dq]primary = the bank\[aq]s clearing date, secondary = date the
+transaction was initiated, if different\[dq], as shown here:
 .IP
 .nf
 \f[C]
@@ -156,22 +124,16 @@
 .nf
 \f[C]
 $ hledger register checking
-2010/02/23 movie ticket         assets:checking                $-10         $-10
+2010-02-23 movie ticket         assets:checking                $-10         $-10
 \f[R]
 .fi
 .IP
 .nf
 \f[C]
 $ hledger register checking --date2
-2010/02/19 movie ticket         assets:checking                $-10         $-10
+2010-02-19 movie ticket         assets:checking                $-10         $-10
 \f[R]
 .fi
-.PP
-Secondary dates require some effort; you must use them consistently in
-your journal entries and remember whether to use or not use the
-\f[C]--date2\f[R] flag for your reports.
-They are included in hledger for Ledger compatibility, but posting dates
-are a more powerful and less confusing alternative.
 .SS Posting dates
 .PP
 You can give individual postings a different date from their parent
@@ -193,14 +155,14 @@
 .nf
 \f[C]
 $ hledger -f t.j register food
-2015/05/30                      expenses:food                  $10           $10
+2015-05-30                      expenses:food                  $10           $10
 \f[R]
 .fi
 .IP
 .nf
 \f[C]
 $ hledger -f t.j register checking
-2015/06/01                      assets:checking               $-10          $-10
+2015-06-01                      assets:checking               $-10          $-10
 \f[R]
 .fi
 .PP
@@ -277,7 +239,7 @@
 .PP
 .TS
 tab(@);
-lw(9.9n) lw(60.1n).
+lw(9.7n) lw(60.3n).
 T{
 status
 T}@T{
@@ -320,6 +282,177 @@
 additional note field on the right (after the first \f[C]|\f[R]).
 This may be worthwhile if you need to do more precise querying and
 pivoting by payee or by note.
+.SS Comments
+.PP
+Lines in the journal beginning with a semicolon (\f[C];\f[R]) or hash
+(\f[C]#\f[R]) or star (\f[C]*\f[R]) are comments, and will be ignored.
+(Star comments cause org-mode nodes to be ignored, allowing emacs users
+to fold and navigate their journals with org-mode or orgstruct-mode.)
+.PP
+You can attach comments to a transaction by writing them after the
+description and/or indented on the following lines (before the
+postings).
+Similarly, you can attach comments to an individual posting by writing
+them after the amount and/or indented on the following lines.
+Transaction and posting comments must begin with a semicolon
+(\f[C];\f[R]).
+.PP
+Some examples:
+.IP
+.nf
+\f[C]
+# a file comment
+; another file comment
+* also a file comment, useful in org/orgstruct mode
+
+comment
+A multiline file comment, which continues
+until a line containing just \[dq]end comment\[dq]
+(or end of file).
+end comment
+
+2012/5/14 something  ; a transaction comment
+    ; the transaction comment, continued
+    posting1  1  ; a comment for posting 1
+    posting2
+    ; a comment for posting 2
+    ; another comment line for posting 2
+; a file comment (because not indented)
+\f[R]
+.fi
+.PP
+You can also comment larger regions of a file using \f[C]comment\f[R]
+and \f[C]end comment\f[R] directives.
+.SS Tags
+.PP
+Tags are a way to add extra labels or labelled data to postings and
+transactions, which you can then search or pivot on.
+.PP
+A simple tag is a word (which may contain hyphens) followed by a full
+colon, written inside a transaction or posting comment line:
+.IP
+.nf
+\f[C]
+2017/1/16 bought groceries  ; sometag:
+\f[R]
+.fi
+.PP
+Tags can have a value, which is the text after the colon, up to the next
+comma or end of line, with leading/trailing whitespace removed:
+.IP
+.nf
+\f[C]
+    expenses:food    $10 ; a-posting-tag: the tag value
+\f[R]
+.fi
+.PP
+Note this means hledger\[aq]s tag values can not contain commas or
+newlines.
+Ending at commas means you can write multiple short tags on one line,
+comma separated:
+.IP
+.nf
+\f[C]
+    assets:checking  ; a comment containing tag1:, tag2: some value ...
+\f[R]
+.fi
+.PP
+Here,
+.IP \[bu] 2
+\[dq]\f[C]a comment containing\f[R]\[dq] is just comment text, not a tag
+.IP \[bu] 2
+\[dq]\f[C]tag1\f[R]\[dq] is a tag with no value
+.IP \[bu] 2
+\[dq]\f[C]tag2\f[R]\[dq] is another tag, whose value is
+\[dq]\f[C]some value ...\f[R]\[dq]
+.PP
+Tags in a transaction comment affect the transaction and all of its
+postings, while tags in a posting comment affect only that posting.
+For example, the following transaction has three tags (\f[C]A\f[R],
+\f[C]TAG2\f[R], \f[C]third-tag\f[R]) and the posting has four (those
+plus \f[C]posting-tag\f[R]):
+.IP
+.nf
+\f[C]
+1/1 a transaction  ; A:, TAG2:
+    ; third-tag: a third transaction tag, <- with a value
+    (a)  $1  ; posting-tag:
+\f[R]
+.fi
+.PP
+Tags are like Ledger\[aq]s metadata feature, except hledger\[aq]s tag
+values are simple strings.
+.SS Postings
+.PP
+A posting is an addition of some amount to, or removal of some amount
+from, an account.
+Each posting line begins with at least one space or tab (2 or 4 spaces
+is common), followed by:
+.IP \[bu] 2
+(optional) a status character (empty, \f[C]!\f[R], or \f[C]*\f[R]),
+followed by a space
+.IP \[bu] 2
+(required) an account name (any text, optionally containing \f[B]single
+spaces\f[R], until end of line or a double space)
+.IP \[bu] 2
+(optional) \f[B]two or more spaces\f[R] or tabs followed by an amount.
+.PP
+Positive amounts are being added to the account, negative amounts are
+being removed.
+.PP
+The amounts within a transaction must always sum up to zero.
+As a convenience, one amount may be left blank; it will be inferred so
+as to balance the transaction.
+.PP
+Be sure to note the unusual two-space delimiter between account name and
+amount.
+This makes it easy to write account names containing spaces.
+But if you accidentally leave only one space (or tab) before the amount,
+the amount will be considered part of the account name.
+.SS Virtual Postings
+.PP
+A posting with a parenthesised account name is called a \f[I]virtual
+posting\f[R] or \f[I]unbalanced posting\f[R], which means it is exempt
+from the usual rule that a transaction\[aq]s postings must balance add
+up to zero.
+.PP
+This is not part of double entry accounting, so you might choose to
+avoid this feature.
+Or you can use it sparingly for certain special cases where it can be
+convenient.
+Eg, you could set opening balances without using a balancing equity
+account:
+.IP
+.nf
+\f[C]
+1/1 opening balances
+  (assets:checking)   $1000
+  (assets:savings)    $2000
+\f[R]
+.fi
+.PP
+A posting with a bracketed account name is called a \f[I]balanced
+virtual posting\f[R].
+The balanced virtual postings in a transaction must add up to zero
+(separately from other postings).
+Eg:
+.IP
+.nf
+\f[C]
+1/1 buy food with cash, update budget envelope subaccounts, & something else
+  assets:cash                    $-10 ; <- these balance
+  expenses:food                    $7 ; <-
+  expenses:food                    $3 ; <-
+  [assets:checking:budget:food]  $-10    ; <- and these balance
+  [assets:checking:available]     $10    ; <-
+  (something:else)                 $5       ; <- not required to balance
+\f[R]
+.fi
+.PP
+Ordinary non-parenthesised, non-bracketed postings are called \f[I]real
+postings\f[R].
+You can exclude virtual postings from reports with the
+\f[C]-R/--real\f[R] flag or \f[C]real:1\f[R] query.
 .SS Account names
 .PP
 Account names typically have several parts separated by a full colon,
@@ -440,15 +573,15 @@
 commodity       1 000 000.9455
 \f[R]
 .fi
-.SS Amount display format
+.SS Amount display style
 .PP
 For each commodity, hledger chooses a consistent format to use when
 displaying amounts.
 (Except price amounts, which are always displayed as written).
-The display format is chosen as follows:
+The display style is chosen as follows:
 .IP \[bu] 2
-If there is a commodity directive for the commodity, that format is used
-(see examples above).
+If there is a commodity directive (or default commodity directive) for
+the commodity, that format is used (see examples above).
 .IP \[bu] 2
 Otherwise the format of the first posting amount in that commodity seen
 in the journal is used.
@@ -458,54 +591,109 @@
 Or if there are no such amounts in the journal, a default format is used
 (like \f[C]$1000.00\f[R]).
 .PP
-Price amounts, and amounts in \f[C]D\f[R] directives don\[aq]t affect
-the amount display format directly, but occasionally they can do so
-indirectly.
-(Eg when D\[aq]s default commodity is applied to a commodity-less
-amount, or when an amountless posting is balanced using a price\[aq]s
-commodity, or when -V is used.) If you find this causing problems, use a
-commodity directive to set the display format.
-.SS Virtual Postings
+Transaction prices don\[aq]t affect the amount display style directly,
+but occasionally they can do so indirectly (eg when an posting\[aq]s
+amount is inferred using a transaction price).
+If you find this causing problems, use a commodity directive to fix the
+display style.
 .PP
-When you parenthesise the account name in a posting, we call that a
-\f[I]virtual posting\f[R], which means:
-.IP \[bu] 2
-it is ignored when checking that the transaction is balanced
-.IP \[bu] 2
-it is excluded from reports when the \f[C]--real/-R\f[R] flag is used,
-or the \f[C]real:1\f[R] query.
+In summary: amounts will be displayed much as they appear in your
+journal, with the max observed number of decimal places.
+If you want to see fewer decimal places in reports, use a commodity
+directive to override that.
+.SS Transaction prices
 .PP
-You could use this, eg, to set an account\[aq]s opening balance without
-needing to use the \f[C]equity:opening balances\f[R] account:
+Within a transaction, you can note an amount\[aq]s price in another
+commodity.
+This can be used to document the cost (in a purchase) or selling price
+(in a sale).
+For example, transaction prices are useful to record purchases of a
+foreign currency.
+Note transaction prices are fixed at the time of the transaction, and do
+not change over time.
+See also market prices, which represent prevailing exchange rates on a
+certain date.
+.PP
+There are several ways to record a transaction price:
+.IP "1." 3
+Write the price per unit, as \f[C]\[at] UNITPRICE\f[R] after the amount:
+.RS 4
 .IP
 .nf
 \f[C]
-1/1 special unbalanced posting to set initial balance
-  (assets:checking)   $1000
+2009/1/1
+  assets:euros     \[Eu]100 \[at] $1.35  ; one hundred euros purchased at $1.35 each
+  assets:dollars                 ; balancing amount is -$135.00
 \f[R]
 .fi
+.RE
+.IP "2." 3
+Write the total price, as \f[C]\[at]\[at] TOTALPRICE\f[R] after the
+amount:
+.RS 4
+.IP
+.nf
+\f[C]
+2009/1/1
+  assets:euros     \[Eu]100 \[at]\[at] $135  ; one hundred euros purchased at $135 for the lot
+  assets:dollars
+\f[R]
+.fi
+.RE
+.IP "3." 3
+Specify amounts for all postings, using exactly two commodities, and let
+hledger infer the price that balances the transaction:
+.RS 4
+.IP
+.nf
+\f[C]
+2009/1/1
+  assets:euros     \[Eu]100          ; one hundred euros purchased
+  assets:dollars  $-135          ; for $135
+\f[R]
+.fi
+.RE
 .PP
-When the account name is bracketed, we call it a \f[I]balanced virtual
-posting\f[R].
-This is like an ordinary virtual posting except the balanced virtual
-postings in a transaction must balance to 0, like the real postings (but
-separately from them).
-Balanced virtual postings are also excluded by \f[C]--real/-R\f[R] or
-\f[C]real:1\f[R].
+(Ledger users: Ledger uses a different syntax for fixed prices,
+\f[C]{=UNITPRICE}\f[R], which hledger currently ignores).
+.PP
+Use the \f[C]-B/--cost\f[R] flag to convert amounts to their transaction
+price\[aq]s commodity, if any.
+(mnemonic: \[dq]B\[dq] is from \[dq]cost Basis\[dq], as in Ledger).
+Eg here is how -B affects the balance report for the example above:
 .IP
 .nf
 \f[C]
-1/1 buy food with cash, and update some budget-tracking subaccounts elsewhere
-  expenses:food                   $10
-  assets:cash                    $-10
-  [assets:checking:available]     $10
-  [assets:checking:budget:food]  $-10
+$ hledger bal -N --flat
+               $-135  assets:dollars
+                \[Eu]100  assets:euros
+$ hledger bal -N --flat -B
+               $-135  assets:dollars
+                $135  assets:euros    # <- the euros\[aq] cost
 \f[R]
 .fi
 .PP
-Virtual postings have some legitimate uses, but those are few.
-You can usually find an equivalent journal entry using real postings,
-which is more correct and provides better error checking.
+Note -B is sensitive to the order of postings when a transaction price
+is inferred: the inferred price will be in the commodity of the last
+amount.
+So if example 3\[aq]s postings are reversed, while the transaction is
+equivalent, -B shows something different:
+.IP
+.nf
+\f[C]
+2009/1/1
+  assets:dollars  $-135              ; 135 dollars sold
+  assets:euros     \[Eu]100              ; for 100 euros
+\f[R]
+.fi
+.IP
+.nf
+\f[C]
+$ hledger bal -N --flat -B
+               \[Eu]-100  assets:dollars  # <- the dollars\[aq] selling price
+                \[Eu]100  assets:euros
+\f[R]
+.fi
 .SS Balance Assertions
 .PP
 hledger supports Ledger-style balance assertions in journal files.
@@ -533,6 +721,7 @@
 You can disable them temporarily with the
 \f[C]-I/--ignore-assertions\f[R] flag, which can be useful for
 troubleshooting or for reading Ledger files.
+(Note: this flag currently does not disable balance assignments, below).
 .SS Assertions and ordering
 .PP
 hledger sorts an account\[aq]s postings and assertions first by date and
@@ -567,9 +756,6 @@
 The asserted balance must be a simple single-commodity amount, and in
 fact the assertion checks only this commodity\[aq]s balance within the
 (possibly multi-commodity) account balance.
-.PD 0
-.P
-.PD
 This is how assertions work in Ledger also.
 We could call this a \[dq]partial\[dq] balance assertion.
 .PP
@@ -675,7 +861,7 @@
 .IP
 .nf
 \f[C]
-; starting a new journal, set asset account balances 
+; starting a new journal, set asset account balances
 2016/1/1 opening balances
   assets:checking            = $409.32
   assets:savings             = $735.24
@@ -717,204 +903,10 @@
 .nf
 \f[C]
 $ hledger print --explicit
-2019/01/01
+2019-01-01
     (a)         $1 \[at] \[Eu]2 = $1 \[at] \[Eu]2
 \f[R]
 .fi
-.SS Transaction prices
-.PP
-Within a transaction, you can note an amount\[aq]s price in another
-commodity.
-This can be used to document the cost (in a purchase) or selling price
-(in a sale).
-For example, transaction prices are useful to record purchases of a
-foreign currency.
-Note transaction prices are fixed at the time of the transaction, and do
-not change over time.
-See also market prices, which represent prevailing exchange rates on a
-certain date.
-.PP
-There are several ways to record a transaction price:
-.IP "1." 3
-Write the price per unit, as \f[C]\[at] UNITPRICE\f[R] after the amount:
-.RS 4
-.IP
-.nf
-\f[C]
-2009/1/1
-  assets:euros     \[Eu]100 \[at] $1.35  ; one hundred euros purchased at $1.35 each
-  assets:dollars                 ; balancing amount is -$135.00
-\f[R]
-.fi
-.RE
-.IP "2." 3
-Write the total price, as \f[C]\[at]\[at] TOTALPRICE\f[R] after the
-amount:
-.RS 4
-.IP
-.nf
-\f[C]
-2009/1/1
-  assets:euros     \[Eu]100 \[at]\[at] $135  ; one hundred euros purchased at $135 for the lot
-  assets:dollars
-\f[R]
-.fi
-.RE
-.IP "3." 3
-Specify amounts for all postings, using exactly two commodities, and let
-hledger infer the price that balances the transaction:
-.RS 4
-.IP
-.nf
-\f[C]
-2009/1/1
-  assets:euros     \[Eu]100          ; one hundred euros purchased
-  assets:dollars  $-135          ; for $135
-\f[R]
-.fi
-.RE
-.PP
-(Ledger users: Ledger uses a different syntax for fixed prices,
-\f[C]{=UNITPRICE}\f[R], which hledger currently ignores).
-.PP
-Use the \f[C]-B/--cost\f[R] flag to convert amounts to their transaction
-price\[aq]s commodity, if any.
-(mnemonic: \[dq]B\[dq] is from \[dq]cost Basis\[dq], as in Ledger).
-Eg here is how -B affects the balance report for the example above:
-.IP
-.nf
-\f[C]
-$ hledger bal -N --flat
-               $-135  assets:dollars
-                \[Eu]100  assets:euros
-$ hledger bal -N --flat -B
-               $-135  assets:dollars
-                $135  assets:euros    # <- the euros\[aq] cost
-\f[R]
-.fi
-.PP
-Note -B is sensitive to the order of postings when a transaction price
-is inferred: the inferred price will be in the commodity of the last
-amount.
-So if example 3\[aq]s postings are reversed, while the transaction is
-equivalent, -B shows something different:
-.IP
-.nf
-\f[C]
-2009/1/1
-  assets:dollars  $-135              ; 135 dollars sold
-  assets:euros     \[Eu]100              ; for 100 euros
-\f[R]
-.fi
-.IP
-.nf
-\f[C]
-$ hledger bal -N --flat -B
-               \[Eu]-100  assets:dollars  # <- the dollars\[aq] selling price
-                \[Eu]100  assets:euros
-\f[R]
-.fi
-.SS Comments
-.PP
-Lines in the journal beginning with a semicolon (\f[C];\f[R]) or hash
-(\f[C]#\f[R]) or star (\f[C]*\f[R]) are comments, and will be ignored.
-(Star comments cause org-mode nodes to be ignored, allowing emacs users
-to fold and navigate their journals with org-mode or orgstruct-mode.)
-.PP
-You can attach comments to a transaction by writing them after the
-description and/or indented on the following lines (before the
-postings).
-Similarly, you can attach comments to an individual posting by writing
-them after the amount and/or indented on the following lines.
-Transaction and posting comments must begin with a semicolon
-(\f[C];\f[R]).
-.PP
-Some examples:
-.IP
-.nf
-\f[C]
-# a file comment
-
-; also a file comment
-
-comment
-This is a multiline file comment,
-which continues until a line
-where the \[dq]end comment\[dq] string
-appears on its own (or end of file).
-end comment
-
-2012/5/14 something  ; a transaction comment
-    ; the transaction comment, continued
-    posting1  1  ; a comment for posting 1
-    posting2
-    ; a comment for posting 2
-    ; another comment line for posting 2
-; a file comment (because not indented)
-\f[R]
-.fi
-.PP
-You can also comment larger regions of a file using \f[C]comment\f[R]
-and \f[C]end comment\f[R] directives.
-.SS Tags
-.PP
-Tags are a way to add extra labels or labelled data to postings and
-transactions, which you can then search or pivot on.
-.PP
-A simple tag is a word (which may contain hyphens) followed by a full
-colon, written inside a transaction or posting comment line:
-.IP
-.nf
-\f[C]
-2017/1/16 bought groceries  ; sometag:
-\f[R]
-.fi
-.PP
-Tags can have a value, which is the text after the colon, up to the next
-comma or end of line, with leading/trailing whitespace removed:
-.IP
-.nf
-\f[C]
-    expenses:food    $10 ; a-posting-tag: the tag value
-\f[R]
-.fi
-.PP
-Note this means hledger\[aq]s tag values can not contain commas or
-newlines.
-Ending at commas means you can write multiple short tags on one line,
-comma separated:
-.IP
-.nf
-\f[C]
-    assets:checking  ; a comment containing tag1:, tag2: some value ...
-\f[R]
-.fi
-.PP
-Here,
-.IP \[bu] 2
-\[dq]\f[C]a comment containing\f[R]\[dq] is just comment text, not a tag
-.IP \[bu] 2
-\[dq]\f[C]tag1\f[R]\[dq] is a tag with no value
-.IP \[bu] 2
-\[dq]\f[C]tag2\f[R]\[dq] is another tag, whose value is
-\[dq]\f[C]some value ...\f[R]\[dq]
-.PP
-Tags in a transaction comment affect the transaction and all of its
-postings, while tags in a posting comment affect only that posting.
-For example, the following transaction has three tags (\f[C]A\f[R],
-\f[C]TAG2\f[R], \f[C]third-tag\f[R]) and the posting has four (those
-plus \f[C]posting-tag\f[R]):
-.IP
-.nf
-\f[C]
-1/1 a transaction  ; A:, TAG2:
-    ; third-tag: a third transaction tag, <- with a value
-    (a)  $1  ; posting-tag:
-\f[R]
-.fi
-.PP
-Tags are like Ledger\[aq]s metadata feature, except hledger\[aq]s tag
-values are simple strings.
 .SS Directives
 .PP
 A directive is a line in the journal beginning with a special keyword,
@@ -1001,12 +993,12 @@
 T}@T{
 T}@T{
 T}@T{
-declare a commodity, number notation & display style for commodityless
-amounts
+declare a commodity to be used for commodityless amounts, and its number
+notation & display style
 T}@T{
-commodity: all commodityless entries in all files; number notation:
-following commodityless entries and entries in that commodity in all
-files; display style: amounts of that commodity in reports
+default commodity: following commodityless entries until end of current
+file; number notation: following entries in that commodity until end of
+current file; display style: amounts of that commodity in reports
 T}
 T{
 \f[C]include\f[R]
@@ -1041,7 +1033,7 @@
 .PP
 .TS
 tab(@);
-lw(8.9n) lw(61.1n).
+lw(6.0n) lw(64.0n).
 T{
 subdirective
 T}@T{
@@ -1138,12 +1130,13 @@
 It declares commodities which may be used in the journal.
 This is currently not enforced, but can serve as documentation.
 .IP "2." 3
-It declares what decimal mark character to expect when parsing input -
-useful to disambiguate international number formats in your data.
+It declares what decimal mark character (period or comma) to expect when
+parsing input - useful to disambiguate international number formats in
+your data.
 (Without this, hledger will parse both \f[C]1,000\f[R] and
 \f[C]1.000\f[R] as 1).
 .IP "3." 3
-It declares the amount display format to use in output - decimal and
+It declares the amount display style to use in output - decimal and
 digit group marks, number of decimal places, symbol placement etc.
 .PP
 You are likely to run into one of the problems solved by commodity
@@ -1188,26 +1181,34 @@
 followed by 0 or more decimal digits.
 .SS Default commodity
 .PP
-The \f[C]D\f[R] directive sets a default commodity (and display format),
-to be used for amounts without a commodity symbol (ie, plain numbers).
-(Note this differs from Ledger\[aq]s default commodity directive.) The
-commodity and display format will be applied to all subsequent
-commodity-less amounts, or until the next \f[C]D\f[R] directive.
+The \f[C]D\f[R] directive sets a default commodity, to be used for
+amounts without a commodity symbol (ie, plain numbers).
+This commodity will be applied to all subsequent commodity-less amounts,
+or until the next \f[C]D\f[R] directive.
+(Note, this is different from Ledger\[aq]s \f[C]D\f[R].)
+.PP
+For compatibility/historical reasons, \f[C]D\f[R] also acts like a
+\f[C]commodity\f[R] directive, setting the commodity\[aq]s display style
+(for output) and decimal mark (for parsing input).
+As with \f[C]commodity\f[R], the amount must always be written with a
+decimal mark (period or comma).
+If both directives are used, \f[C]commodity\f[R]\[aq]s style takes
+precedence.
+.PP
+The syntax is \f[C]D AMOUNT\f[R].
+Eg:
 .IP
 .nf
 \f[C]
 ; commodity-less amounts should be treated as dollars
-; (and displayed with symbol on the left, thousands separators and two decimal places)
+; (and displayed with the dollar sign on the left, thousands separators and two decimal places)
 D $1,000.00
 
 1/1
-  a     5  ; <- commodity-less amount, becomes $1
+  a     5  ; <- commodity-less amount, parsed as $5 and displayed as $5.00
   b
 \f[R]
 .fi
-.PP
-As with the \f[C]commodity\f[R] directive, the amount must always be
-written with a decimal point.
 .SS Market prices
 .PP
 The \f[C]P\f[R] directive declares a market price, which is an exchange
@@ -1274,21 +1275,24 @@
 .fi
 .SS Account comments
 .PP
-Comments, beginning with a semicolon, optionally including tags, can be
-written after the account name, and/or on following lines.
-Eg:
+Comments, beginning with a semicolon, can be added:
+.IP \[bu] 2
+on the same line, \f[B]after two or more spaces\f[R] (because ; is
+allowed in account names)
+.IP \[bu] 2
+on the next lines, indented
+.PP
+An example of both:
 .IP
 .nf
 \f[C]
-account assets:bank:checking  ; a comment
-  ; another comment
-  ; acctno:12345, a tag
+account assets:bank:checking  ; same-line comment, note 2+ spaces before ;
+  ; next-line comment
+  ; another with tag, acctno:12345 (not used yet)
 \f[R]
 .fi
 .PP
-Tip: comments on the same line require hledger 1.12+.
-If you need your journal to be compatible with older hledger versions,
-write comments on the next line instead.
+Same-line comments are not supported by Ledger, or hledger <1.13.
 .SS Account subdirectives
 .PP
 We also allow (and ignore) Ledger-style indented subdirectives, just for
@@ -1336,7 +1340,7 @@
 account liabilities  ; type:Liability
 account equity       ; type:Equity
 account revenues     ; type:Revenue
-account expenses     ; type:Expenses
+account expenses     ; type:Expense
 \f[R]
 .fi
 .SS Account types declared with account type codes
@@ -1365,8 +1369,8 @@
 ; make \[dq]liabilities\[dq] not have the liability type - who knows why
 account liabilities  ; type:E
 
-; we need to ensure some other account has the liability type, 
-; otherwise balancesheet would still show \[dq]liabilities\[dq] under Liabilities 
+; we need to ensure some other account has the liability type,
+; otherwise balancesheet would still show \[dq]liabilities\[dq] under Liabilities
 account -            ; type:L
 \f[R]
 .fi
@@ -1417,11 +1421,14 @@
 would influence the position of \f[C]zoo\f[R] among
 \f[C]other\f[R]\[aq]s subaccounts, but not the position of
 \f[C]other\f[R] among the top-level accounts.
-This means: - you will sometimes declare parent accounts (eg
-\f[C]account other\f[R] above) that you don\[aq]t intend to post to,
-just to customize their display order - sibling accounts stay together
-(you couldn\[aq]t display \f[C]x:y\f[R] in between \f[C]a:b\f[R] and
-\f[C]a:c\f[R]).
+This means:
+.IP \[bu] 2
+you will sometimes declare parent accounts (eg \f[C]account other\f[R]
+above) that you don\[aq]t intend to post to, just to customize their
+display order
+.IP \[bu] 2
+sibling accounts stay together (you couldn\[aq]t display \f[C]x:y\f[R]
+in between \f[C]a:b\f[R] and \f[C]a:c\f[R]).
 .SS Rewriting accounts
 .PP
 You can define account alias rules which rewrite your account names, or
@@ -1442,7 +1449,7 @@
 They do not affect account names being entered via hledger add or
 hledger-web.
 .PP
-See also Cookbook: Rewrite account names.
+See also Rewrite account names.
 .SS Basic aliases
 .PP
 To set an account alias, use the \f[C]alias\f[R] directive in your
@@ -1769,8 +1776,8 @@
 .nf
 \f[C]
 = QUERY
-    ACCT  AMT
-    ACCT  [AMT]
+    ACCOUNT  AMOUNT
+    ACCOUNT  [AMOUNT]
     ...
 \f[R]
 .fi
@@ -1794,6 +1801,17 @@
 The matched posting\[aq]s amount will be multiplied by N, and its
 commodity symbol will be replaced with S.
 .PP
+A query term containing spaces must be enclosed in single or double
+quotes, as on the command line.
+Eg, note the quotes around the second query term below:
+.IP
+.nf
+\f[C]
+= expenses:groceries \[aq]expenses:dining out\[aq]
+    (budget:funds:dining out)                 *-1
+\f[R]
+.fi
+.PP
 These rules have global effect - a rule appearing anywhere in your data
 can potentially affect any transaction, including transactions recorded
 above it or in another file.
@@ -1824,12 +1842,12 @@
 .nf
 \f[C]
 $ hledger print --auto
-2017/12/01
+2017-12-01
     expenses:food              $10
     assets:checking
     (liabilities:charity)      $-1
 
-2017/12/14
+2017-12-14
     expenses:gifts             $20
     assets:checking
     assets:checking:gifts     -$20
@@ -1872,15 +1890,6 @@
 .IP \[bu] 2
 \f[C]_modified:\f[R] - a hidden tag not appearing in the comment; this
 transaction was modified \[dq]just now\[dq].
-.SH EDITOR SUPPORT
-.PP
-Helper modes exist for popular text editors, which make working with
-journal files easier.
-They add colour, formatting, tab completion, and helpful commands, and
-are quite recommended if you edit your journal with a text editor.
-They include ledger-mode or hledger-mode for Emacs, vim-ledger for Vim,
-hledger-vscode for Visual Studio Code, and others.
-See the [[Cookbook]] at hledger.org for the latest information.
 
 
 .SH "REPORTING BUGS"
diff --git a/hledger_journal.info b/hledger_journal.info
--- a/hledger_journal.info
+++ b/hledger_journal.info
@@ -2,1865 +2,1849 @@
 stdin.
 
 
-File: hledger_journal.info,  Node: Top,  Next: FILE FORMAT,  Up: (dir)
-
-hledger_journal(5) hledger 1.16.2
-*********************************
-
-hledger's usual data source is a plain text file containing journal
-entries in hledger journal format.  This file represents a standard
-accounting general journal.  I use file names ending in '.journal', but
-that's not required.  The journal file contains a number of transaction
-entries, each describing a transfer of money (or any commodity) between
-two or more named accounts, in a simple format readable by both hledger
-and humans.
-
-   hledger's journal format is a compatible subset, mostly, of ledger's
-journal format, so hledger can work with compatible ledger journal files
-as well.  It's safe, and encouraged, to run both hledger and ledger on
-the same journal file, eg to validate the results you're getting.
-
-   You can use hledger without learning any more about this file; just
-use the add or web commands to create and update it.  Many users,
-though, also edit the journal file directly with a text editor, perhaps
-assisted by the helper modes for emacs or vim.
-
-   Here's an example:
-
-; A sample journal file. This is a comment.
-
-2008/01/01 income             ; <- transaction's first line starts in column 0, contains date and description
-    assets:bank:checking  $1  ; <- posting lines start with whitespace, each contains an account name
-    income:salary        $-1  ;    followed by at least two spaces and an amount
-
-2008/06/01 gift
-    assets:bank:checking  $1  ; <- at least two postings in a transaction
-    income:gifts         $-1  ; <- their amounts must balance to 0
-
-2008/06/02 save
-    assets:bank:saving    $1
-    assets:bank:checking      ; <- one amount may be omitted; here $-1 is inferred
-
-2008/06/03 eat & shop         ; <- description can be anything
-    expenses:food         $1
-    expenses:supplies     $1  ; <- this transaction debits two expense accounts
-    assets:cash               ; <- $-2 inferred
-
-2008/10/01 take a loan
-    assets:bank:checking  $1
-    liabilities:debts    $-1
-
-2008/12/31 * pay off          ; <- an optional * or ! after the date means "cleared" (or anything you want)
-    liabilities:debts     $1
-    assets:bank:checking
-
-* Menu:
-
-* FILE FORMAT::
-* EDITOR SUPPORT::
-
-
-File: hledger_journal.info,  Node: FILE FORMAT,  Next: EDITOR SUPPORT,  Prev: Top,  Up: Top
-
-1 FILE FORMAT
-*************
-
-* Menu:
-
-* Transactions::
-* Postings::
-* Dates::
-* Status::
-* Description::
-* Account names::
-* Amounts::
-* Virtual Postings::
-* Balance Assertions::
-* Balance Assignments::
-* Transaction prices::
-* Comments::
-* Tags::
-* Directives::
-* Periodic transactions::
-* Auto postings / transaction modifiers::
-
-
-File: hledger_journal.info,  Node: Transactions,  Next: Postings,  Up: FILE FORMAT
-
-1.1 Transactions
-================
-
-Transactions are movements of some quantity of commodities between named
-accounts.  Each transaction is represented by a journal entry beginning
-with a simple date in column 0.  This can be followed by any of the
-following, separated by spaces:
-
-   * (optional) a status character (empty, '!', or '*')
-   * (optional) a transaction code (any short number or text, enclosed
-     in parentheses)
-   * (optional) a transaction description (any remaining text until end
-     of line or a semicolon)
-   * (optional) a transaction comment (any remaining text following a
-     semicolon until end of line)
-
-   Then comes zero or more (but usually at least 2) indented lines
-representing...
-
-
-File: hledger_journal.info,  Node: Postings,  Next: Dates,  Prev: Transactions,  Up: FILE FORMAT
-
-1.2 Postings
-============
-
-A posting is an addition of some amount to, or removal of some amount
-from, an account.  Each posting line begins with at least one space or
-tab (2 or 4 spaces is common), followed by:
-
-   * (optional) a status character (empty, '!', or '*'), followed by a
-     space
-   * (required) an account name (any text, optionally containing *single
-     spaces*, until end of line or a double space)
-   * (optional) *two or more spaces* or tabs followed by an amount.
-
-   Positive amounts are being added to the account, negative amounts are
-being removed.
-
-   The amounts within a transaction must always sum up to zero.  As a
-convenience, one amount may be left blank; it will be inferred so as to
-balance the transaction.
-
-   Be sure to note the unusual two-space delimiter between account name
-and amount.  This makes it easy to write account names containing
-spaces.  But if you accidentally leave only one space (or tab) before
-the amount, the amount will be considered part of the account name.
-
-
-File: hledger_journal.info,  Node: Dates,  Next: Status,  Prev: Postings,  Up: FILE FORMAT
-
-1.3 Dates
-=========
-
-* Menu:
-
-* Simple dates::
-* Secondary dates::
-* Posting dates::
-
-
-File: hledger_journal.info,  Node: Simple dates,  Next: Secondary dates,  Up: Dates
-
-1.3.1 Simple dates
-------------------
-
-Within a journal file, transaction dates use Y/M/D (or Y-M-D or Y.M.D)
-Leading zeros are optional.  The year may be omitted, in which case it
-will be inferred from the context - the current transaction, the default
-year set with a default year directive, or the current date when the
-command is run.  Some examples: '2010/01/31', '1/31', '2010-01-31',
-'2010.1.31'.
-
-
-File: hledger_journal.info,  Node: Secondary dates,  Next: Posting dates,  Prev: Simple dates,  Up: Dates
-
-1.3.2 Secondary dates
----------------------
-
-Real-life transactions sometimes involve more than one date - eg the
-date you write a cheque, and the date it clears in your bank.  When you
-want to model this, eg for more accurate balances, you can specify
-individual posting dates, which I recommend.  Or, you can use the
-secondary dates (aka auxiliary/effective dates) feature, supported for
-compatibility with Ledger.
-
-   A secondary date can be written after the primary date, separated by
-an equals sign.  The primary date, on the left, is used by default; the
-secondary date, on the right, is used when the '--date2' flag is
-specified ('--aux-date' or '--effective' also work).
-
-   The meaning of secondary dates is up to you, but it's best to follow
-a consistent rule.  Eg write the bank's clearing date as primary, and
-when needed, the date the transaction was initiated as secondary.
-
-   Here's an example.  Note that a secondary date will use the year of
-the primary date if unspecified.
-
-2010/2/23=2/19 movie ticket
-  expenses:cinema                   $10
-  assets:checking
-
-$ hledger register checking
-2010/02/23 movie ticket         assets:checking                $-10         $-10
-
-$ hledger register checking --date2
-2010/02/19 movie ticket         assets:checking                $-10         $-10
-
-   Secondary dates require some effort; you must use them consistently
-in your journal entries and remember whether to use or not use the
-'--date2' flag for your reports.  They are included in hledger for
-Ledger compatibility, but posting dates are a more powerful and less
-confusing alternative.
-
-
-File: hledger_journal.info,  Node: Posting dates,  Prev: Secondary dates,  Up: Dates
-
-1.3.3 Posting dates
--------------------
-
-You can give individual postings a different date from their parent
-transaction, by adding a posting comment containing a tag (see below)
-like 'date:DATE'.  This is probably the best way to control posting
-dates precisely.  Eg in this example the expense should appear in May
-reports, and the deduction from checking should be reported on 6/1 for
-easy bank reconciliation:
-
-2015/5/30
-    expenses:food     $10  ; food purchased on saturday 5/30
-    assets:checking        ; bank cleared it on monday, date:6/1
-
-$ hledger -f t.j register food
-2015/05/30                      expenses:food                  $10           $10
-
-$ hledger -f t.j register checking
-2015/06/01                      assets:checking               $-10          $-10
-
-   DATE should be a simple date; if the year is not specified it will
-use the year of the transaction's date.  You can set the secondary date
-similarly, with 'date2:DATE2'.  The 'date:' or 'date2:' tags must have a
-valid simple date value if they are present, eg a 'date:' tag with no
-value is not allowed.
-
-   Ledger's earlier, more compact bracketed date syntax is also
-supported: '[DATE]', '[DATE=DATE2]' or '[=DATE2]'.  hledger will attempt
-to parse any square-bracketed sequence of the '0123456789/-.='
-characters in this way.  With this syntax, DATE infers its year from the
-transaction and DATE2 infers its year from DATE.
-
-
-File: hledger_journal.info,  Node: Status,  Next: Description,  Prev: Dates,  Up: FILE FORMAT
-
-1.4 Status
-==========
-
-Transactions, or individual postings within a transaction, can have a
-status mark, which is a single character before the transaction
-description or posting account name, separated from it by a space,
-indicating one of three statuses:
-
-mark  status
- 
------------------
-      unmarked
-'!'   pending
-'*'   cleared
-
-   When reporting, you can filter by status with the '-U/--unmarked',
-'-P/--pending', and '-C/--cleared' flags; or the 'status:', 'status:!',
-and 'status:*' queries; or the U, P, C keys in hledger-ui.
-
-   Note, in Ledger and in older versions of hledger, the "unmarked"
-state is called "uncleared".  As of hledger 1.3 we have renamed it to
-unmarked for clarity.
-
-   To replicate Ledger and old hledger's behaviour of also matching
-pending, combine -U and -P.
-
-   Status marks are optional, but can be helpful eg for reconciling with
-real-world accounts.  Some editor modes provide highlighting and
-shortcuts for working with status.  Eg in Emacs ledger-mode, you can
-toggle transaction status with C-c C-e, or posting status with C-c C-c.
-
-   What "uncleared", "pending", and "cleared" actually mean is up to
-you.  Here's one suggestion:
-
-status     meaning
---------------------------------------------------------------------------
-uncleared  recorded but not yet reconciled; needs review
-pending    tentatively reconciled (if needed, eg during a big
-           reconciliation)
-cleared    complete, reconciled as far as possible, and considered
-           correct
-
-   With this scheme, you would use '-PC' to see the current balance at
-your bank, '-U' to see things which will probably hit your bank soon
-(like uncashed checks), and no flags to see the most up-to-date state of
-your finances.
-
-
-File: hledger_journal.info,  Node: Description,  Next: Account names,  Prev: Status,  Up: FILE FORMAT
-
-1.5 Description
-===============
-
-A transaction's description is the rest of the line following the date
-and status mark (or until a comment begins).  Sometimes called the
-"narration" in traditional bookkeeping, it can be used for whatever you
-wish, or left blank.  Transaction descriptions can be queried, unlike
-comments.
-
-* Menu:
-
-* Payee and note::
-
-
-File: hledger_journal.info,  Node: Payee and note,  Up: Description
-
-1.5.1 Payee and note
---------------------
-
-You can optionally include a '|' (pipe) character in descriptions to
-subdivide the description into separate fields for payee/payer name on
-the left (up to the first '|') and an additional note field on the right
-(after the first '|').  This may be worthwhile if you need to do more
-precise querying and pivoting by payee or by note.
-
-
-File: hledger_journal.info,  Node: Account names,  Next: Amounts,  Prev: Description,  Up: FILE FORMAT
-
-1.6 Account names
-=================
-
-Account names typically have several parts separated by a full colon,
-from which hledger derives a hierarchical chart of accounts.  They can
-be anything you like, but in finance there are traditionally five
-top-level accounts: 'assets', 'liabilities', 'income', 'expenses', and
-'equity'.
-
-   Account names may contain single spaces, eg: 'assets:accounts
-receivable'.  Because of this, they must always be followed by *two or
-more spaces* (or newline).
-
-   Account names can be aliased.
-
-
-File: hledger_journal.info,  Node: Amounts,  Next: Virtual Postings,  Prev: Account names,  Up: FILE FORMAT
-
-1.7 Amounts
-===========
-
-After the account name, there is usually an amount.  (Important: between
-account name and amount, there must be *two or more spaces*.)
-
-   hledger's amount format is flexible, supporting several international
-formats.  Here are some examples.  Amounts have a number (the
-"quantity"):
-
-1
-
-   ..and usually a currency or commodity name (the "commodity").  This
-is a symbol, word, or phrase, to the left or right of the quantity, with
-or without a separating space:
-
-$1
-4000 AAPL
-
-   If the commodity name contains spaces, numbers, or punctuation, it
-must be enclosed in double quotes:
-
-3 "no. 42 green apples"
-
-   Amounts can be negative.  The minus sign can be written before or
-after a left-side commodity symbol:
-
--$1
-$-1
-
-   Scientific E notation is allowed:
-
-1E-6
-EUR 1E3
-
-   A decimal mark (decimal point) can be written with a period or a
-comma:
-
-1.23
-1,23456780000009
-
-* Menu:
-
-* Digit group marks::
-* Amount display format::
-
-
-File: hledger_journal.info,  Node: Digit group marks,  Next: Amount display format,  Up: Amounts
-
-1.7.1 Digit group marks
------------------------
-
-In the integer part of the quantity (left of the decimal mark), groups
-of digits can optionally be separated by a "digit group mark" - a space,
-comma, or period (different from the decimal mark):
-
-     $1,000,000.00
-  EUR 2.000.000,00
-INR 9,99,99,999.00
-      1 000 000.9455
-
-   Note, a number containing a single group mark and no decimal mark is
-ambiguous.  Are these group marks or decimal marks ?
-
-1,000
-1.000
-
-   hledger will treat them both as decimal marks by default (cf #793).
-If you use digit group marks, to prevent confusion and undetected typos
-we recommend you write commodity directives at the top of the file to
-explicitly declare the decimal mark (and optionally a digit group mark).
-Note, these formats ("amount styles") are specific to each commodity, so
-if your data uses multiple formats, hledger can handle it:
-
-commodity $1,000.00
-commodity EUR 1.000,00
-commodity INR 9,99,99,999.00
-commodity       1 000 000.9455
-
-
-File: hledger_journal.info,  Node: Amount display format,  Prev: Digit group marks,  Up: Amounts
-
-1.7.2 Amount display format
----------------------------
-
-For each commodity, hledger chooses a consistent format to use when
-displaying amounts.  (Except price amounts, which are always displayed
-as written).  The display format is chosen as follows:
-
-   * If there is a commodity directive for the commodity, that format is
-     used (see examples above).
-
-   * Otherwise the format of the first posting amount in that commodity
-     seen in the journal is used.  But the number of decimal places
-     ("precision") will be the maximum from all posting amounts in that
-     commmodity.
-
-   * Or if there are no such amounts in the journal, a default format is
-     used (like '$1000.00').
-
-   Price amounts, and amounts in 'D' directives don't affect the amount
-display format directly, but occasionally they can do so indirectly.
-(Eg when D's default commodity is applied to a commodity-less amount, or
-when an amountless posting is balanced using a price's commodity, or
-when -V is used.)  If you find this causing problems, use a commodity
-directive to set the display format.
-
-
-File: hledger_journal.info,  Node: Virtual Postings,  Next: Balance Assertions,  Prev: Amounts,  Up: FILE FORMAT
-
-1.8 Virtual Postings
-====================
-
-When you parenthesise the account name in a posting, we call that a
-_virtual posting_, which means:
-
-   * it is ignored when checking that the transaction is balanced
-   * it is excluded from reports when the '--real/-R' flag is used, or
-     the 'real:1' query.
-
-   You could use this, eg, to set an account's opening balance without
-needing to use the 'equity:opening balances' account:
-
-1/1 special unbalanced posting to set initial balance
-  (assets:checking)   $1000
-
-   When the account name is bracketed, we call it a _balanced virtual
-posting_.  This is like an ordinary virtual posting except the balanced
-virtual postings in a transaction must balance to 0, like the real
-postings (but separately from them).  Balanced virtual postings are also
-excluded by '--real/-R' or 'real:1'.
-
-1/1 buy food with cash, and update some budget-tracking subaccounts elsewhere
-  expenses:food                   $10
-  assets:cash                    $-10
-  [assets:checking:available]     $10
-  [assets:checking:budget:food]  $-10
-
-   Virtual postings have some legitimate uses, but those are few.  You
-can usually find an equivalent journal entry using real postings, which
-is more correct and provides better error checking.
-
-
-File: hledger_journal.info,  Node: Balance Assertions,  Next: Balance Assignments,  Prev: Virtual Postings,  Up: FILE FORMAT
-
-1.9 Balance Assertions
-======================
-
-hledger supports Ledger-style balance assertions in journal files.
-These look like, for example, '= EXPECTEDBALANCE' following a posting's
-amount.  Eg here we assert the expected dollar balance in accounts a and
-b after each posting:
-
-2013/1/1
-  a   $1  =$1
-  b       =$-1
-
-2013/1/2
-  a   $1  =$2
-  b  $-1  =$-2
-
-   After reading a journal file, hledger will check all balance
-assertions and report an error if any of them fail.  Balance assertions
-can protect you from, eg, inadvertently disrupting reconciled balances
-while cleaning up old entries.  You can disable them temporarily with
-the '-I/--ignore-assertions' flag, which can be useful for
-troubleshooting or for reading Ledger files.
-
-* Menu:
-
-* Assertions and ordering::
-* Assertions and included files::
-* Assertions and multiple -f options::
-* Assertions and commodities::
-* Assertions and prices::
-* Assertions and subaccounts::
-* Assertions and virtual postings::
-* Assertions and precision::
-
-
-File: hledger_journal.info,  Node: Assertions and ordering,  Next: Assertions and included files,  Up: Balance Assertions
-
-1.9.1 Assertions and ordering
------------------------------
-
-hledger sorts an account's postings and assertions first by date and
-then (for postings on the same day) by parse order.  Note this is
-different from Ledger, which sorts assertions only by parse order.
-(Also, Ledger assertions do not see the accumulated effect of repeated
-postings to the same account within a transaction.)
-
-   So, hledger balance assertions keep working if you reorder
-differently-dated transactions within the journal.  But if you reorder
-same-dated transactions or postings, assertions might break and require
-updating.  This order dependence does bring an advantage: precise
-control over the order of postings and assertions within a day, so you
-can assert intra-day balances.
-
-
-File: hledger_journal.info,  Node: Assertions and included files,  Next: Assertions and multiple -f options,  Prev: Assertions and ordering,  Up: Balance Assertions
-
-1.9.2 Assertions and included files
------------------------------------
-
-With included files, things are a little more complicated.  Including
-preserves the ordering of postings and assertions.  If you have multiple
-postings to an account on the same day, split across different files,
-and you also want to assert the account's balance on the same day,
-you'll have to put the assertion in the right file.
-
-
-File: hledger_journal.info,  Node: Assertions and multiple -f options,  Next: Assertions and commodities,  Prev: Assertions and included files,  Up: Balance Assertions
-
-1.9.3 Assertions and multiple -f options
-----------------------------------------
-
-Balance assertions don't work well across files specified with multiple
--f options.  Use include or concatenate the files instead.
-
-
-File: hledger_journal.info,  Node: Assertions and commodities,  Next: Assertions and prices,  Prev: Assertions and multiple -f options,  Up: Balance Assertions
-
-1.9.4 Assertions and commodities
---------------------------------
-
-The asserted balance must be a simple single-commodity amount, and in
-fact the assertion checks only this commodity's balance within the
-(possibly multi-commodity) account balance.
-This is how assertions work in Ledger also.  We could call this a
-"partial" balance assertion.
-
-   To assert the balance of more than one commodity in an account, you
-can write multiple postings, each asserting one commodity's balance.
-
-   You can make a stronger "total" balance assertion by writing a double
-equals sign ('== EXPECTEDBALANCE').  This asserts that there are no
-other unasserted commodities in the account (or, that their balance is
-0).
-
-2013/1/1
-  a   $1
-  a    1€
-  b  $-1
-  c   -1€
-
-2013/1/2  ; These assertions succeed
-  a    0  =  $1
-  a    0  =   1€
-  b    0 == $-1
-  c    0 ==  -1€
-
-2013/1/3  ; This assertion fails as 'a' also contains 1€
-  a    0 ==  $1
-
-   It's not yet possible to make a complete assertion about a balance
-that has multiple commodities.  One workaround is to isolate each
-commodity into its own subaccount:
-
-2013/1/1
-  a:usd   $1
-  a:euro   1€
-  b
-
-2013/1/2
-  a        0 ==  0
-  a:usd    0 == $1
-  a:euro   0 ==  1€
-
-
-File: hledger_journal.info,  Node: Assertions and prices,  Next: Assertions and subaccounts,  Prev: Assertions and commodities,  Up: Balance Assertions
-
-1.9.5 Assertions and prices
----------------------------
-
-Balance assertions ignore transaction prices, and should normally be
-written without one:
-
-2019/1/1
-  (a)     $1 @ €1 = $1
-
-   We do allow prices to be written there, however, and print shows
-them, even though they don't affect whether the assertion passes or
-fails.  This is for backward compatibility (hledger's close command used
-to generate balance assertions with prices), and because balance
-_assignments_ do use them (see below).
-
-
-File: hledger_journal.info,  Node: Assertions and subaccounts,  Next: Assertions and virtual postings,  Prev: Assertions and prices,  Up: Balance Assertions
-
-1.9.6 Assertions and subaccounts
---------------------------------
-
-The balance assertions above ('=' and '==') do not count the balance
-from subaccounts; they check the account's exclusive balance only.  You
-can assert the balance including subaccounts by writing '=*' or '==*',
-eg:
-
-2019/1/1
-  equity:opening balances
-  checking:a       5
-  checking:b       5
-  checking         1  ==* 11
-
-
-File: hledger_journal.info,  Node: Assertions and virtual postings,  Next: Assertions and precision,  Prev: Assertions and subaccounts,  Up: Balance Assertions
-
-1.9.7 Assertions and virtual postings
--------------------------------------
-
-Balance assertions are checked against all postings, both real and
-virtual.  They are not affected by the '--real/-R' flag or 'real:'
-query.
-
-
-File: hledger_journal.info,  Node: Assertions and precision,  Prev: Assertions and virtual postings,  Up: Balance Assertions
-
-1.9.8 Assertions and precision
-------------------------------
-
-Balance assertions compare the exactly calculated amounts, which are not
-always what is shown by reports.  Eg a commodity directive may limit the
-display precision, but this will not affect balance assertions.  Balance
-assertion failure messages show exact amounts.
-
-
-File: hledger_journal.info,  Node: Balance Assignments,  Next: Transaction prices,  Prev: Balance Assertions,  Up: FILE FORMAT
-
-1.10 Balance Assignments
-========================
-
-Ledger-style balance assignments are also supported.  These are like
-balance assertions, but with no posting amount on the left side of the
-equals sign; instead it is calculated automatically so as to satisfy the
-assertion.  This can be a convenience during data entry, eg when setting
-opening balances:
-
-; starting a new journal, set asset account balances 
-2016/1/1 opening balances
-  assets:checking            = $409.32
-  assets:savings             = $735.24
-  assets:cash                 = $42
-  equity:opening balances
-
-   or when adjusting a balance to reality:
-
-; no cash left; update balance, record any untracked spending as a generic expense
-2016/1/15
-  assets:cash    = $0
-  expenses:misc
-
-   The calculated amount depends on the account's balance in the
-commodity at that point (which depends on the previously-dated postings
-of the commodity to that account since the last balance assertion or
-assignment).  Note that using balance assignments makes your journal a
-little less explicit; to know the exact amount posted, you have to run
-hledger or do the calculations yourself, instead of just reading it.
-
-* Menu:
-
-* Balance assignments and prices::
-
-
-File: hledger_journal.info,  Node: Balance assignments and prices,  Up: Balance Assignments
-
-1.10.1 Balance assignments and prices
--------------------------------------
-
-A transaction price in a balance assignment will cause the calculated
-amount to have that price attached:
-
-2019/1/1
-  (a)             = $1 @ €2
-
-$ hledger print --explicit
-2019/01/01
-    (a)         $1 @ €2 = $1 @ €2
-
-
-File: hledger_journal.info,  Node: Transaction prices,  Next: Comments,  Prev: Balance Assignments,  Up: FILE FORMAT
-
-1.11 Transaction prices
-=======================
-
-Within a transaction, you can note an amount's price in another
-commodity.  This can be used to document the cost (in a purchase) or
-selling price (in a sale).  For example, transaction prices are useful
-to record purchases of a foreign currency.  Note transaction prices are
-fixed at the time of the transaction, and do not change over time.  See
-also market prices, which represent prevailing exchange rates on a
-certain date.
-
-   There are several ways to record a transaction price:
-
-  1. Write the price per unit, as '@ UNITPRICE' after the amount:
-
-     2009/1/1
-       assets:euros     €100 @ $1.35  ; one hundred euros purchased at $1.35 each
-       assets:dollars                 ; balancing amount is -$135.00
-
-  2. Write the total price, as '@@ TOTALPRICE' after the amount:
-
-     2009/1/1
-       assets:euros     €100 @@ $135  ; one hundred euros purchased at $135 for the lot
-       assets:dollars
-
-  3. Specify amounts for all postings, using exactly two commodities,
-     and let hledger infer the price that balances the transaction:
-
-     2009/1/1
-       assets:euros     €100          ; one hundred euros purchased
-       assets:dollars  $-135          ; for $135
-
-   (Ledger users: Ledger uses a different syntax for fixed prices,
-'{=UNITPRICE}', which hledger currently ignores).
-
-   Use the '-B/--cost' flag to convert amounts to their transaction
-price's commodity, if any.  (mnemonic: "B" is from "cost Basis", as in
-Ledger).  Eg here is how -B affects the balance report for the example
-above:
-
-$ hledger bal -N --flat
-               $-135  assets:dollars
-                €100  assets:euros
-$ hledger bal -N --flat -B
-               $-135  assets:dollars
-                $135  assets:euros    # <- the euros' cost
-
-   Note -B is sensitive to the order of postings when a transaction
-price is inferred: the inferred price will be in the commodity of the
-last amount.  So if example 3's postings are reversed, while the
-transaction is equivalent, -B shows something different:
-
-2009/1/1
-  assets:dollars  $-135              ; 135 dollars sold
-  assets:euros     €100              ; for 100 euros
-
-$ hledger bal -N --flat -B
-               €-100  assets:dollars  # <- the dollars' selling price
-                €100  assets:euros
-
-
-File: hledger_journal.info,  Node: Comments,  Next: Tags,  Prev: Transaction prices,  Up: FILE FORMAT
-
-1.12 Comments
-=============
-
-Lines in the journal beginning with a semicolon (';') or hash ('#') or
-star ('*') are comments, and will be ignored.  (Star comments cause
-org-mode nodes to be ignored, allowing emacs users to fold and navigate
-their journals with org-mode or orgstruct-mode.)
-
-   You can attach comments to a transaction by writing them after the
-description and/or indented on the following lines (before the
-postings).  Similarly, you can attach comments to an individual posting
-by writing them after the amount and/or indented on the following lines.
-Transaction and posting comments must begin with a semicolon (';').
-
-   Some examples:
-
-# a file comment
-
-; also a file comment
-
-comment
-This is a multiline file comment,
-which continues until a line
-where the "end comment" string
-appears on its own (or end of file).
-end comment
-
-2012/5/14 something  ; a transaction comment
-    ; the transaction comment, continued
-    posting1  1  ; a comment for posting 1
-    posting2
-    ; a comment for posting 2
-    ; another comment line for posting 2
-; a file comment (because not indented)
-
-   You can also comment larger regions of a file using 'comment' and
-'end comment' directives.
-
-
-File: hledger_journal.info,  Node: Tags,  Next: Directives,  Prev: Comments,  Up: FILE FORMAT
-
-1.13 Tags
-=========
-
-Tags are a way to add extra labels or labelled data to postings and
-transactions, which you can then search or pivot on.
-
-   A simple tag is a word (which may contain hyphens) followed by a full
-colon, written inside a transaction or posting comment line:
-
-2017/1/16 bought groceries  ; sometag:
-
-   Tags can have a value, which is the text after the colon, up to the
-next comma or end of line, with leading/trailing whitespace removed:
-
-    expenses:food    $10 ; a-posting-tag: the tag value
-
-   Note this means hledger's tag values can not contain commas or
-newlines.  Ending at commas means you can write multiple short tags on
-one line, comma separated:
-
-    assets:checking  ; a comment containing tag1:, tag2: some value ...
-
-   Here,
-
-   * "'a comment containing'" is just comment text, not a tag
-   * "'tag1'" is a tag with no value
-   * "'tag2'" is another tag, whose value is "'some value ...'"
-
-   Tags in a transaction comment affect the transaction and all of its
-postings, while tags in a posting comment affect only that posting.  For
-example, the following transaction has three tags ('A', 'TAG2',
-'third-tag') and the posting has four (those plus 'posting-tag'):
-
-1/1 a transaction  ; A:, TAG2:
-    ; third-tag: a third transaction tag, <- with a value
-    (a)  $1  ; posting-tag:
-
-   Tags are like Ledger's metadata feature, except hledger's tag values
-are simple strings.
-
-
-File: hledger_journal.info,  Node: Directives,  Next: Periodic transactions,  Prev: Tags,  Up: FILE FORMAT
-
-1.14 Directives
-===============
-
-A directive is a line in the journal beginning with a special keyword,
-that influences how the journal is processed.  hledger's directives are
-based on a subset of Ledger's, but there are many differences (and also
-some differences between hledger versions).
-
-   Directives' behaviour and interactions can get a little bit complex,
-so here is a table summarising the directives and their effects, with
-links to more detailed docs.
-
-directiveend       subdirectivespurpose                  can affect (as of
-         directive                                       2018/06)
------------------------------------------------------------------------------
-'account'          any     document account names,       all entries in
-                   text    declare account types &       all files, before
-                           display order                 or after
-'alias'  'end              rewrite account names         following
-         aliases'                                        inline/included
-                                                         entries until end
-                                                         of current file
-                                                         or end directive
-'apply   'end              prepend a common parent to    following
-account' apply             account names                 inline/included
-         account'                                        entries until end
-                                                         of current file
-                                                         or end directive
-'comment''end              ignore part of journal        following
-         comment'                                        inline/included
-                                                         entries until end
-                                                         of current file
-                                                         or end directive
-'commodity'        'format'declare a commodity and its   number notation:
-                           number notation & display     following entries
-                           style                         in that commodity
-                                                         in all files;
-                                                         display style:
-                                                         amounts of that
-                                                         commodity in
-                                                         reports
-'D'                        declare a commodity, number   commodity: all
-                           notation & display style      commodityless
-                           for commodityless amounts     entries in all
-                                                         files; number
-                                                         notation:
-                                                         following
-                                                         commodityless
-                                                         entries and
-                                                         entries in that
-                                                         commodity in all
-                                                         files; display
-                                                         style: amounts of
-                                                         that commodity in
-                                                         reports
-'include'                  include entries/directives    what the included
-                           from another file             directives affect
-'P'                        declare a market price for    amounts of that
-                           a commodity                   commodity in
-                                                         reports, when -V
-                                                         is used
-'Y'                        declare a year for yearless   following
-                           dates                         inline/included
-                                                         entries until end
-                                                         of current file
-
-   And some definitions:
-
-subdirectiveoptional indented directive line immediately following a
-          parent directive
-number    how to interpret numbers when parsing journal entries (the
-notation  identity of the decimal separator character).  (Currently
-          each commodity can have its own notation, even in the same
-          file.)
-display   how to display amounts of a commodity in reports (symbol side
-style     and spacing, digit groups, decimal separator, decimal places)
-directive which entries and (when there are multiple files) which files
-scope     are affected by a directive
-
-   As you can see, directives vary in which journal entries and files
-they affect, and whether they are focussed on input (parsing) or output
-(reports).  Some directives have multiple effects.
-
-   If you have a journal made up of multiple files, or pass multiple -f
-options on the command line, note that directives which affect input
-typically last only until the end of their defining file.  This provides
-more simplicity and predictability, eg reports are not changed by
-writing file options in a different order.  It can be surprising at
-times though.
-
-* Menu:
-
-* Comment blocks::
-* Including other files::
-* Default year::
-* Declaring commodities::
-* Default commodity::
-* Market prices::
-* Declaring accounts::
-* Rewriting accounts::
-* Default parent account::
-
-
-File: hledger_journal.info,  Node: Comment blocks,  Next: Including other files,  Up: Directives
-
-1.14.1 Comment blocks
----------------------
-
-A line containing just 'comment' starts a commented region of the file,
-and a line containing just 'end comment' (or the end of the current
-file) ends it.  See also comments.
-
-
-File: hledger_journal.info,  Node: Including other files,  Next: Default year,  Prev: Comment blocks,  Up: Directives
-
-1.14.2 Including other files
-----------------------------
-
-You can pull in the content of additional files by writing an include
-directive, like this:
-
-include path/to/file.journal
-
-   If the path does not begin with a slash, it is relative to the
-current file.  The include file path may contain common glob patterns
-(e.g.  '*').
-
-   The 'include' directive can only be used in journal files.  It can
-include journal, timeclock or timedot files, but not CSV files.
-
-
-File: hledger_journal.info,  Node: Default year,  Next: Declaring commodities,  Prev: Including other files,  Up: Directives
-
-1.14.3 Default year
--------------------
-
-You can set a default year to be used for subsequent dates which don't
-specify a year.  This is a line beginning with 'Y' followed by the year.
-Eg:
-
-Y2009  ; set default year to 2009
-
-12/15  ; equivalent to 2009/12/15
-  expenses  1
-  assets
-
-Y2010  ; change default year to 2010
-
-2009/1/30  ; specifies the year, not affected
-  expenses  1
-  assets
-
-1/31   ; equivalent to 2010/1/31
-  expenses  1
-  assets
-
-
-File: hledger_journal.info,  Node: Declaring commodities,  Next: Default commodity,  Prev: Default year,  Up: Directives
-
-1.14.4 Declaring commodities
-----------------------------
-
-The 'commodity' directive has several functions:
-
-  1. It declares commodities which may be used in the journal.  This is
-     currently not enforced, but can serve as documentation.
-
-  2. It declares what decimal mark character to expect when parsing
-     input - useful to disambiguate international number formats in your
-     data.  (Without this, hledger will parse both '1,000' and '1.000'
-     as 1).
-
-  3. It declares the amount display format to use in output - decimal
-     and digit group marks, number of decimal places, symbol placement
-     etc.
-
-   You are likely to run into one of the problems solved by commodity
-directives, sooner or later, so it's a good idea to just always use them
-to declare your commodities.
-
-   A commodity directive is just the word 'commodity' followed by an
-amount.  It may be written on a single line, like this:
-
-; commodity EXAMPLEAMOUNT
-
-; display AAAA amounts with the symbol on the right, space-separated,
-; using period as decimal point, with four decimal places, and
-; separating thousands with comma.
-commodity 1,000.0000 AAAA
-
-   or on multiple lines, using the "format" subdirective.  (In this case
-the commodity symbol appears twice and should be the same in both
-places.):
-
-; commodity SYMBOL
-;   format EXAMPLEAMOUNT
-
-; display indian rupees with currency name on the left,
-; thousands, lakhs and crores comma-separated,
-; period as decimal point, and two decimal places.
-commodity INR
-  format INR 1,00,00,000.00
-
-   The quantity of the amount does not matter; only the format is
-significant.  The number must include a decimal mark: either a period or
-a comma, followed by 0 or more decimal digits.
-
-
-File: hledger_journal.info,  Node: Default commodity,  Next: Market prices,  Prev: Declaring commodities,  Up: Directives
-
-1.14.5 Default commodity
-------------------------
-
-The 'D' directive sets a default commodity (and display format), to be
-used for amounts without a commodity symbol (ie, plain numbers).  (Note
-this differs from Ledger's default commodity directive.)  The commodity
-and display format will be applied to all subsequent commodity-less
-amounts, or until the next 'D' directive.
-
-; commodity-less amounts should be treated as dollars
-; (and displayed with symbol on the left, thousands separators and two decimal places)
-D $1,000.00
-
-1/1
-  a     5  ; <- commodity-less amount, becomes $1
-  b
-
-   As with the 'commodity' directive, the amount must always be written
-with a decimal point.
-
-
-File: hledger_journal.info,  Node: Market prices,  Next: Declaring accounts,  Prev: Default commodity,  Up: Directives
-
-1.14.6 Market prices
---------------------
-
-The 'P' directive declares a market price, which is an exchange rate
-between two commodities on a certain date.  (In Ledger, they are called
-"historical prices".)  These are often obtained from a stock exchange,
-cryptocurrency exchange, or the foreign exchange market.
-
-   Here is the format:
-
-P DATE COMMODITYA COMMODITYBAMOUNT
-
-   * DATE is a simple date
-   * COMMODITYA is the symbol of the commodity being priced
-   * COMMODITYBAMOUNT is an amount (symbol and quantity) in a second
-     commodity, giving the price in commodity B of one unit of commodity
-     A.
-
-   These two market price directives say that one euro was worth 1.35 US
-dollars during 2009, and $1.40 from 2010 onward:
-
-P 2009/1/1 € $1.35
-P 2010/1/1 € $1.40
-
-   The '-V/--value' flag can be used to convert reported amounts to
-another commodity using these prices.
-
-
-File: hledger_journal.info,  Node: Declaring accounts,  Next: Rewriting accounts,  Prev: Market prices,  Up: Directives
-
-1.14.7 Declaring accounts
--------------------------
-
-'account' directives can be used to pre-declare accounts.  Though not
-required, they can provide several benefits:
-
-   * They can document your intended chart of accounts, providing a
-     reference.
-   * They can store extra information about accounts (account numbers,
-     notes, etc.)
-   * They can help hledger know your accounts' types (asset, liability,
-     equity, revenue, expense), useful for reports like balancesheet and
-     incomestatement.
-   * They control account display order in reports, allowing
-     non-alphabetic sorting (eg Revenues to appear above Expenses).
-   * They help with account name completion in the add command,
-     hledger-iadd, hledger-web, ledger-mode etc.
-
-   The simplest form is just the word 'account' followed by a
-hledger-style account name, eg:
-
-account assets:bank:checking
-
-* Menu:
-
-* Account comments::
-* Account subdirectives::
-* Account types::
-* Account display order::
-
-
-File: hledger_journal.info,  Node: Account comments,  Next: Account subdirectives,  Up: Declaring accounts
-
-1.14.7.1 Account comments
-.........................
-
-Comments, beginning with a semicolon, optionally including tags, can be
-written after the account name, and/or on following lines.  Eg:
-
-account assets:bank:checking  ; a comment
-  ; another comment
-  ; acctno:12345, a tag
-
-   Tip: comments on the same line require hledger 1.12+.  If you need
-your journal to be compatible with older hledger versions, write
-comments on the next line instead.
-
-
-File: hledger_journal.info,  Node: Account subdirectives,  Next: Account types,  Prev: Account comments,  Up: Declaring accounts
-
-1.14.7.2 Account subdirectives
-..............................
-
-We also allow (and ignore) Ledger-style indented subdirectives, just for
-compatibility.:
-
-account assets:bank:checking
-  format blah blah  ; <- subdirective, ignored
-
-   Here is the full syntax of account directives:
-
-account ACCTNAME  [ACCTTYPE] [;COMMENT]
-  [;COMMENTS]
-  [LEDGER-STYLE SUBDIRECTIVES, IGNORED]
-
-
-File: hledger_journal.info,  Node: Account types,  Next: Account display order,  Prev: Account subdirectives,  Up: Declaring accounts
-
-1.14.7.3 Account types
-......................
-
-hledger recognises five types (or classes) of account: Asset, Liability,
-Equity, Revenue, Expense.  This is used by a few accounting-aware
-reports such as balancesheet, incomestatement and cashflow.
-Auto-detected account types If you name your top-level accounts with
-some variation of 'assets', 'liabilities'/'debts', 'equity',
-'revenues'/'income', or 'expenses', their types are detected
-automatically.  Account types declared with tags More generally, you can
-declare an account's type with an account directive, by writing a
-'type:' tag in a comment, followed by one of the words 'Asset',
-'Liability', 'Equity', 'Revenue', 'Expense', or one of the letters
-'ALERX' (case insensitive):
-
-account assets       ; type:Asset
-account liabilities  ; type:Liability
-account equity       ; type:Equity
-account revenues     ; type:Revenue
-account expenses     ; type:Expenses
-
-   Account types declared with account type codes Or, you can write one
-of those letters separated from the account name by two or more spaces,
-but this should probably be considered deprecated as of hledger 1.13:
-
-account assets       A
-account liabilities  L
-account equity       E
-account revenues     R
-account expenses     X
-
-   Overriding auto-detected types If you ever override the types of
-those auto-detected english account names mentioned above, you might
-need to help the reports a bit.  Eg:
-
-; make "liabilities" not have the liability type - who knows why
-account liabilities  ; type:E
-
-; we need to ensure some other account has the liability type, 
-; otherwise balancesheet would still show "liabilities" under Liabilities 
-account -            ; type:L
-
-
-File: hledger_journal.info,  Node: Account display order,  Prev: Account types,  Up: Declaring accounts
-
-1.14.7.4 Account display order
-..............................
-
-Account directives also set the order in which accounts are displayed,
-eg in reports, the hledger-ui accounts screen, and the hledger-web
-sidebar.  By default accounts are listed in alphabetical order.  But if
-you have these account directives in the journal:
-
-account assets
-account liabilities
-account equity
-account revenues
-account expenses
-
-   you'll see those accounts displayed in declaration order, not
-alphabetically:
-
-$ hledger accounts -1
-assets
-liabilities
-equity
-revenues
-expenses
-
-   Undeclared accounts, if any, are displayed last, in alphabetical
-order.
-
-   Note that sorting is done at each level of the account tree (within
-each group of sibling accounts under the same parent).  And currently,
-this directive:
-
-account other:zoo
-
-   would influence the position of 'zoo' among 'other''s subaccounts,
-but not the position of 'other' among the top-level accounts.  This
-means: - you will sometimes declare parent accounts (eg 'account other'
-above) that you don't intend to post to, just to customize their display
-order - sibling accounts stay together (you couldn't display 'x:y' in
-between 'a:b' and 'a:c').
-
-
-File: hledger_journal.info,  Node: Rewriting accounts,  Next: Default parent account,  Prev: Declaring accounts,  Up: Directives
-
-1.14.8 Rewriting accounts
--------------------------
-
-You can define account alias rules which rewrite your account names, or
-parts of them, before generating reports.  This can be useful for:
-
-   * expanding shorthand account names to their full form, allowing
-     easier data entry and a less verbose journal
-   * adapting old journals to your current chart of accounts
-   * experimenting with new account organisations, like a new hierarchy
-     or combining two accounts into one
-   * customising reports
-
-   Account aliases also rewrite account names in account directives.
-They do not affect account names being entered via hledger add or
-hledger-web.
-
-   See also Cookbook: Rewrite account names.
-
-* Menu:
-
-* Basic aliases::
-* Regex aliases::
-* Combining aliases::
-* end aliases::
-
-
-File: hledger_journal.info,  Node: Basic aliases,  Next: Regex aliases,  Up: Rewriting accounts
-
-1.14.8.1 Basic aliases
-......................
-
-To set an account alias, use the 'alias' directive in your journal file.
-This affects all subsequent journal entries in the current file or its
-included files.  The spaces around the = are optional:
-
-alias OLD = NEW
-
-   Or, you can use the '--alias 'OLD=NEW'' option on the command line.
-This affects all entries.  It's useful for trying out aliases
-interactively.
-
-   OLD and NEW are case sensitive full account names.  hledger will
-replace any occurrence of the old account name with the new one.
-Subaccounts are also affected.  Eg:
-
-alias checking = assets:bank:wells fargo:checking
-; rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"
-
-
-File: hledger_journal.info,  Node: Regex aliases,  Next: Combining aliases,  Prev: Basic aliases,  Up: Rewriting accounts
-
-1.14.8.2 Regex aliases
-......................
-
-There is also a more powerful variant that uses a regular expression,
-indicated by the forward slashes:
-
-alias /REGEX/ = REPLACEMENT
-
-   or '--alias '/REGEX/=REPLACEMENT''.
-
-   REGEX is a case-insensitive regular expression.  Anywhere it matches
-inside an account name, the matched part will be replaced by
-REPLACEMENT. If REGEX contains parenthesised match groups, these can be
-referenced by the usual numeric backreferences in REPLACEMENT. Eg:
-
-alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3
-; rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking"
-
-   Also note that REPLACEMENT continues to the end of line (or on
-command line, to end of option argument), so it can contain trailing
-whitespace.
-
-
-File: hledger_journal.info,  Node: Combining aliases,  Next: end aliases,  Prev: Regex aliases,  Up: Rewriting accounts
-
-1.14.8.3 Combining aliases
-..........................
-
-You can define as many aliases as you like, using journal directives
-and/or command line options.
-
-   Recursive aliases - where an account name is rewritten by one alias,
-then by another alias, and so on - are allowed.  Each alias sees the
-effect of previously applied aliases.
-
-   In such cases it can be important to understand which aliases will be
-applied and in which order.  For (each account name in) each journal
-entry, we apply:
-
-  1. 'alias' directives preceding the journal entry, most recently
-     parsed first (ie, reading upward from the journal entry, bottom to
-     top)
-  2. '--alias' options, in the order they appeared on the command line
-     (left to right).
-
-   In other words, for (an account name in) a given journal entry:
-
-   * the nearest alias declaration before/above the entry is applied
-     first
-   * the next alias before/above that will be be applied next, and so on
-   * aliases defined after/below the entry do not affect it.
-
-   This gives nearby aliases precedence over distant ones, and helps
-provide semantic stability - aliases will keep working the same way
-independent of which files are being read and in which order.
-
-   In case of trouble, adding '--debug=6' to the command line will show
-which aliases are being applied when.
-
-
-File: hledger_journal.info,  Node: end aliases,  Prev: Combining aliases,  Up: Rewriting accounts
-
-1.14.8.4 'end aliases'
-......................
-
-You can clear (forget) all currently defined aliases with the 'end
-aliases' directive:
-
-end aliases
-
-
-File: hledger_journal.info,  Node: Default parent account,  Prev: Rewriting accounts,  Up: Directives
-
-1.14.9 Default parent account
------------------------------
-
-You can specify a parent account which will be prepended to all accounts
-within a section of the journal.  Use the 'apply account' and 'end apply
-account' directives like so:
-
-apply account home
-
-2010/1/1
-    food    $10
-    cash
-
-end apply account
-
-   which is equivalent to:
-
-2010/01/01
-    home:food           $10
-    home:cash          $-10
-
-   If 'end apply account' is omitted, the effect lasts to the end of the
-file.  Included files are also affected, eg:
-
-apply account business
-include biz.journal
-end apply account
-apply account personal
-include personal.journal
-
-   Prior to hledger 1.0, legacy 'account' and 'end' spellings were also
-supported.
-
-   A default parent account also affects account directives.  It does
-not affect account names being entered via hledger add or hledger-web.
-If account aliases are present, they are applied after the default
-parent account.
-
-
-File: hledger_journal.info,  Node: Periodic transactions,  Next: Auto postings / transaction modifiers,  Prev: Directives,  Up: FILE FORMAT
-
-1.15 Periodic transactions
-==========================
-
-Periodic transaction rules describe transactions that recur.  They allow
-hledger to generate temporary future transactions to help with
-forecasting, so you don't have to write out each one in the journal, and
-it's easy to try out different forecasts.  Secondly, they are also used
-to define the budgets shown in budget reports.
-
-   Periodic transactions can be a little tricky, so before you use them,
-read this whole section - or at least these tips:
-
-  1. Two spaces accidentally added or omitted will cause you trouble -
-     read about this below.
-  2. For troubleshooting, show the generated transactions with 'hledger
-     print --forecast tag:generated' or 'hledger register --forecast
-     tag:generated'.
-  3. Forecasted transactions will begin only after the last
-     non-forecasted transaction's date.
-  4. Forecasted transactions will end 6 months from today, by default.
-     See below for the exact start/end rules.
-  5. period expressions can be tricky.  Their documentation needs
-     improvement, but is worth studying.
-  6. Some period expressions with a repeating interval must begin on a
-     natural boundary of that interval.  Eg in 'weekly from DATE', DATE
-     must be a monday.  '~ weekly from 2019/10/1' (a tuesday) will give
-     an error.
-  7. Other period expressions with an interval are automatically
-     expanded to cover a whole number of that interval.  (This is done
-     to improve reports, but it also affects periodic transactions.
-     Yes, it's a bit inconsistent with the above.)  Eg: '~ every 10th
-     day of month from 2020/01', which is equivalent to '~ every 10th
-     day of month from 2020/01/01', will be adjusted to start on
-     2019/12/10.
-
-* Menu:
-
-* Periodic rule syntax::
-* Two spaces between period expression and description!::
-* Forecasting with periodic transactions::
-* Budgeting with periodic transactions::
-
-
-File: hledger_journal.info,  Node: Periodic rule syntax,  Next: Two spaces between period expression and description!,  Up: Periodic transactions
-
-1.15.1 Periodic rule syntax
----------------------------
-
-A periodic transaction rule looks like a normal journal entry, with the
-date replaced by a tilde ('~') followed by a period expression
-(mnemonic: '~' looks like a recurring sine wave.):
-
-~ monthly
-    expenses:rent          $2000
-    assets:bank:checking
-
-   There is an additional constraint on the period expression: the start
-date must fall on a natural boundary of the interval.  Eg 'monthly from
-2018/1/1' is valid, but 'monthly from 2018/1/15' is not.
-
-   Partial or relative dates (M/D, D, tomorrow, last week) in the period
-expression can work (useful or not).  They will be relative to today's
-date, unless a Y default year directive is in effect, in which case they
-will be relative to Y/1/1.
-
-
-File: hledger_journal.info,  Node: Two spaces between period expression and description!,  Next: Forecasting with periodic transactions,  Prev: Periodic rule syntax,  Up: Periodic transactions
-
-1.15.2 Two spaces between period expression and description!
-------------------------------------------------------------
-
-If the period expression is followed by a transaction description, these
-must be separated by *two or more spaces*.  This helps hledger know
-where the period expression ends, so that descriptions can not
-accidentally alter their meaning, as in this example:
-
-; 2 or more spaces needed here, so the period is not understood as "every 2 months in 2020"
-;               ||
-;               vv
-~ every 2 months  in 2020, we will review
-    assets:bank:checking   $1500
-    income:acme inc
-
-   So,
-
-   * Do write two spaces between your period expression and your
-     transaction description, if any.
-   * Don't accidentally write two spaces in the middle of your period
-     expression.
-
-
-File: hledger_journal.info,  Node: Forecasting with periodic transactions,  Next: Budgeting with periodic transactions,  Prev: Two spaces between period expression and description!,  Up: Periodic transactions
-
-1.15.3 Forecasting with periodic transactions
----------------------------------------------
-
-With the '--forecast' flag, each periodic transaction rule generates
-future transactions recurring at the specified interval.  These are not
-saved in the journal, but appear in all reports.  They will look like
-normal transactions, but with an extra tag:
-
-   * 'generated-transaction:~ PERIODICEXPR' - shows that this was
-     generated by a periodic transaction rule, and the period
-
-   There is also a hidden tag, with an underscore prefix, which does not
-appear in hledger's output:
-
-   * '_generated-transaction:~ PERIODICEXPR'
-
-   This can be used to match transactions generated "just now", rather
-than generated in the past and saved to the journal.
-
-   Forecast transactions start on the first occurrence, and end on the
-last occurrence, of their interval within the forecast period.  The
-forecast period:
-
-   * begins on the later of
-        * the report start date if specified with -b/-p/date:
-        * the day after the latest normal (non-periodic) transaction in
-          the journal, or today if there are no normal transactions.
-
-   * ends on the report end date if specified with -e/-p/date:, or 180
-     days from today.
-
-   where "today" means the current date at report time.  The "later of"
-rule ensures that forecast transactions do not overlap normal
-transactions in time; they will begin only after normal transactions
-end.
-
-   Forecasting can be useful for estimating balances into the future,
-and experimenting with different scenarios.  Note the start date logic
-means that forecasted transactions are automatically replaced by normal
-transactions as you add those.
-
-   Forecasting can also help with data entry: describe most of your
-transactions with periodic rules, and every so often copy the output of
-'print --forecast' to the journal.
-
-   You can generate one-time transactions too: just write a period
-expression specifying a date with no report interval.  (You could also
-write a normal transaction with a future date, but remember this
-disables forecast transactions on previous dates.)
-
-
-File: hledger_journal.info,  Node: Budgeting with periodic transactions,  Prev: Forecasting with periodic transactions,  Up: Periodic transactions
-
-1.15.4 Budgeting with periodic transactions
--------------------------------------------
-
-With the '--budget' flag, currently supported by the balance command,
-each periodic transaction rule declares recurring budget goals for the
-specified accounts.  Eg the first example above declares a goal of
-spending $2000 on rent (and also, a goal of depositing $2000 into
-checking) every month.  Goals and actual performance can then be
-compared in budget reports.
-
-   For more details, see: balance: Budget report and Budgeting and
-Forecasting.
-
-
-File: hledger_journal.info,  Node: Auto postings / transaction modifiers,  Prev: Periodic transactions,  Up: FILE FORMAT
-
-1.16 Auto postings / transaction modifiers
-==========================================
-
-Transaction modifier rules, AKA auto posting rules, describe changes to
-be applied automatically to certain matched transactions.  Currently
-just one kind of change is possible - adding extra postings, which we
-call "automated postings" or just "auto postings".  These rules become
-active when you use the '--auto' flag.
-
-   A transaction modifier rule looks much like a normal transaction
-except the first line is an equals sign followed by a query that matches
-certain postings (mnemonic: '=' suggests matching).  And each "posting"
-is actually a posting-generating rule:
-
-= QUERY
-    ACCT  AMT
-    ACCT  [AMT]
-    ...
-
-   These posting-generating rules look like normal postings, except the
-amount can be:
-
-   * a normal amount with a commodity symbol, eg '$2'.  This will be
-     used as-is.
-   * a number, eg '2'.  The commodity symbol (if any) from the matched
-     posting will be added to this.
-   * a numeric multiplier, eg '*2' (a star followed by a number N). The
-     matched posting's amount (and total price, if any) will be
-     multiplied by N.
-   * a multiplier with a commodity symbol, eg '*$2' (a star, number N,
-     and symbol S). The matched posting's amount will be multiplied by
-     N, and its commodity symbol will be replaced with S.
-
-   These rules have global effect - a rule appearing anywhere in your
-data can potentially affect any transaction, including transactions
-recorded above it or in another file.
-
-   Some examples:
-
-; every time I buy food, schedule a dollar donation
-= expenses:food
-    (liabilities:charity)   $-1
-
-; when I buy a gift, also deduct that amount from a budget envelope subaccount
-= expenses:gifts
-    assets:checking:gifts  *-1
-    assets:checking         *1
-
-2017/12/1
-  expenses:food    $10
-  assets:checking
-
-2017/12/14
-  expenses:gifts   $20
-  assets:checking
-
-$ hledger print --auto
-2017/12/01
-    expenses:food              $10
-    assets:checking
-    (liabilities:charity)      $-1
-
-2017/12/14
-    expenses:gifts             $20
-    assets:checking
-    assets:checking:gifts     -$20
-    assets:checking            $20
-
-* Menu:
-
-* Auto postings and dates::
-* Auto postings and transaction balancing / inferred amounts / balance assertions::
-* Auto posting tags::
-
-
-File: hledger_journal.info,  Node: Auto postings and dates,  Next: Auto postings and transaction balancing / inferred amounts / balance assertions,  Up: Auto postings / transaction modifiers
-
-1.16.1 Auto postings and dates
-------------------------------
-
-A posting date (or secondary date) in the matched posting, or (taking
-precedence) a posting date in the auto posting rule itself, will also be
-used in the generated posting.
-
-
-File: hledger_journal.info,  Node: Auto postings and transaction balancing / inferred amounts / balance assertions,  Next: Auto posting tags,  Prev: Auto postings and dates,  Up: Auto postings / transaction modifiers
-
-1.16.2 Auto postings and transaction balancing / inferred amounts /
--------------------------------------------------------------------
-
-balance assertions Currently, transaction modifiers are applied / auto
-postings are added:
-
-   * after missing amounts are inferred, and transactions are checked
-     for balancedness,
-   * but before balance assertions are checked.
-
-   Note this means that journal entries must be balanced both before and
-after auto postings are added.  This changed in hledger 1.12+; see #893
-for background.
-
-
-File: hledger_journal.info,  Node: Auto posting tags,  Prev: Auto postings and transaction balancing / inferred amounts / balance assertions,  Up: Auto postings / transaction modifiers
-
-1.16.3 Auto posting tags
-------------------------
-
-Postings added by transaction modifiers will have some extra tags:
-
-   * 'generated-posting:= QUERY' - shows this was generated by an auto
-     posting rule, and the query
-   * '_generated-posting:= QUERY' - a hidden tag, which does not appear
-     in hledger's output.  This can be used to match postings generated
-     "just now", rather than generated in the past and saved to the
-     journal.
-
-   Also, any transaction that has been changed by transaction modifier
-rules will have these tags added:
-
-   * 'modified:' - this transaction was modified
-   * '_modified:' - a hidden tag not appearing in the comment; this
-     transaction was modified "just now".
-
-
-File: hledger_journal.info,  Node: EDITOR SUPPORT,  Prev: FILE FORMAT,  Up: Top
-
-2 EDITOR SUPPORT
-****************
-
-Helper modes exist for popular text editors, which make working with
-journal files easier.  They add colour, formatting, tab completion, and
-helpful commands, and are quite recommended if you edit your journal
-with a text editor.  They include ledger-mode or hledger-mode for Emacs,
-vim-ledger for Vim, hledger-vscode for Visual Studio Code, and others.
-See the [[Cookbook]] at hledger.org for the latest information.
-
-
-Tag Table:
-Node: Top76
-Node: FILE FORMAT2356
-Ref: #file-format2480
-Node: Transactions2783
-Ref: #transactions2904
-Node: Postings3588
-Ref: #postings3715
-Node: Dates4710
-Ref: #dates4825
-Node: Simple dates4890
-Ref: #simple-dates5016
-Node: Secondary dates5382
-Ref: #secondary-dates5536
-Node: Posting dates7099
-Ref: #posting-dates7228
-Node: Status8600
-Ref: #status8720
-Node: Description10428
-Ref: #description10566
-Node: Payee and note10886
-Ref: #payee-and-note11000
-Node: Account names11335
-Ref: #account-names11478
-Node: Amounts11965
-Ref: #amounts12101
-Node: Digit group marks13034
-Ref: #digit-group-marks13183
-Node: Amount display format14121
-Ref: #amount-display-format14278
-Node: Virtual Postings15303
-Ref: #virtual-postings15462
-Node: Balance Assertions16682
-Ref: #balance-assertions16857
-Node: Assertions and ordering17816
-Ref: #assertions-and-ordering18002
-Node: Assertions and included files18702
-Ref: #assertions-and-included-files18943
-Node: Assertions and multiple -f options19276
-Ref: #assertions-and-multiple--f-options19530
-Node: Assertions and commodities19662
-Ref: #assertions-and-commodities19892
-Node: Assertions and prices21048
-Ref: #assertions-and-prices21260
-Node: Assertions and subaccounts21700
-Ref: #assertions-and-subaccounts21927
-Node: Assertions and virtual postings22251
-Ref: #assertions-and-virtual-postings22491
-Node: Assertions and precision22633
-Ref: #assertions-and-precision22824
-Node: Balance Assignments23091
-Ref: #balance-assignments23272
-Node: Balance assignments and prices24437
-Ref: #balance-assignments-and-prices24609
-Node: Transaction prices24833
-Ref: #transaction-prices25002
-Node: Comments27268
-Ref: #comments27402
-Node: Tags28572
-Ref: #tags28690
-Node: Directives30083
-Ref: #directives30226
-Node: Comment blocks35834
-Ref: #comment-blocks35979
-Node: Including other files36155
-Ref: #including-other-files36335
-Node: Default year36743
-Ref: #default-year36912
-Node: Declaring commodities37319
-Ref: #declaring-commodities37502
-Node: Default commodity39163
-Ref: #default-commodity39339
-Node: Market prices39973
-Ref: #market-prices40138
-Node: Declaring accounts40979
-Ref: #declaring-accounts41155
-Node: Account comments42080
-Ref: #account-comments42243
-Node: Account subdirectives42638
-Ref: #account-subdirectives42833
-Node: Account types43146
-Ref: #account-types43330
-Node: Account display order44972
-Ref: #account-display-order45142
-Node: Rewriting accounts46271
-Ref: #rewriting-accounts46456
-Node: Basic aliases47192
-Ref: #basic-aliases47338
-Node: Regex aliases48042
-Ref: #regex-aliases48214
-Node: Combining aliases48932
-Ref: #combining-aliases49110
-Node: end aliases50386
-Ref: #end-aliases50534
-Node: Default parent account50635
-Ref: #default-parent-account50801
-Node: Periodic transactions51685
-Ref: #periodic-transactions51883
-Node: Periodic rule syntax53755
-Ref: #periodic-rule-syntax53961
-Node: Two spaces between period expression and description!54665
-Ref: #two-spaces-between-period-expression-and-description54984
-Node: Forecasting with periodic transactions55668
-Ref: #forecasting-with-periodic-transactions55973
-Node: Budgeting with periodic transactions57999
-Ref: #budgeting-with-periodic-transactions58238
-Node: Auto postings / transaction modifiers58687
-Ref: #auto-postings-transaction-modifiers58898
-Node: Auto postings and dates61127
-Ref: #auto-postings-and-dates61384
-Node: Auto postings and transaction balancing / inferred amounts / balance assertions61559
-Ref: #auto-postings-and-transaction-balancing-inferred-amounts-balance-assertions61934
-Node: Auto posting tags62312
-Ref: #auto-posting-tags62551
-Node: EDITOR SUPPORT63216
-Ref: #editor-support63334
+File: hledger_journal.info,  Node: Top,  Up: (dir)
+
+hledger_journal(5) hledger 1.17
+*******************************
+
+Journal - hledger's default file format, representing a General Journal
+
+   hledger's usual data source is a plain text file containing journal
+entries in hledger journal format.  This file represents a standard
+accounting general journal.  I use file names ending in '.journal', but
+that's not required.  The journal file contains a number of transaction
+entries, each describing a transfer of money (or any commodity) between
+two or more named accounts, in a simple format readable by both hledger
+and humans.
+
+   hledger's journal format is a compatible subset, mostly, of ledger's
+journal format, so hledger can work with compatible ledger journal files
+as well.  It's safe, and encouraged, to run both hledger and ledger on
+the same journal file, eg to validate the results you're getting.
+
+   You can use hledger without learning any more about this file; just
+use the add or web or import commands to create and update it.
+
+   Many users, though, edit the journal file with a text editor, and
+track changes with a version control system such as git.  Editor addons
+such as ledger-mode or hledger-mode for Emacs, vim-ledger for Vim, and
+hledger-vscode for Visual Studio Code, make this easier, adding colour,
+formatting, tab completion, and useful commands.  See Editor
+configuration at hledger.org for the full list.
+
+   Here's a description of each part of the file format (and hledger's
+data model).  These are mostly in the order you'll use them, but in some
+cases related concepts have been grouped together for easy reference, or
+linked before they are introduced, so feel free to skip over anything
+that looks unnecessary right now.
+
+* Menu:
+
+* Transactions::
+
+
+File: hledger_journal.info,  Node: Transactions,  Up: Top
+
+1 Transactions
+**************
+
+Transactions are the main unit of information in a journal file.  They
+represent events, typically a movement of some quantity of commodities
+between two or more named accounts.
+
+   Each transaction is recorded as a journal entry, beginning with a
+simple date in column 0.  This can be followed by any of the following
+optional fields, separated by spaces:
+
+   * a status character (empty, '!', or '*')
+   * a code (any short number or text, enclosed in parentheses)
+   * a description (any remaining text until end of line or a semicolon)
+   * a comment (any remaining text following a semicolon until end of
+     line, and any following indented lines beginning with a semicolon)
+   * 0 or more indented _posting_ lines, describing what was transferred
+     and the accounts involved.
+
+   Here's a simple journal file containing one transaction:
+
+2008/01/01 income
+  assets:bank:checking   $1
+  income:salary         $-1
+
+* Menu:
+
+* Dates::
+* Status::
+* Description::
+* Comments::
+* Tags::
+* Postings::
+* Account names::
+* Amounts::
+* Transaction prices::
+* Balance Assertions::
+* Balance Assignments::
+* Directives::
+* Periodic transactions::
+* Auto postings / transaction modifiers::
+
+
+File: hledger_journal.info,  Node: Dates,  Next: Status,  Up: Transactions
+
+1.1 Dates
+=========
+
+* Menu:
+
+* Simple dates::
+* Secondary dates::
+* Posting dates::
+
+
+File: hledger_journal.info,  Node: Simple dates,  Next: Secondary dates,  Up: Dates
+
+1.1.1 Simple dates
+------------------
+
+Dates in the journal file use _simple dates_ format: 'YYYY-MM-DD' or
+'YYYY/MM/DD' or 'YYYY.MM.DD', with leading zeros optional.  The year may
+be omitted, in which case it will be inferred from the context: the
+current transaction, the default year set with a default year directive,
+or the current date when the command is run.  Some examples:
+'2010-01-31', '2010/01/31', '2010.1.31', '1/31'.
+
+   (The UI also accepts simple dates, as well as the more flexible smart
+dates documented in the hledger manual.)
+
+
+File: hledger_journal.info,  Node: Secondary dates,  Next: Posting dates,  Prev: Simple dates,  Up: Dates
+
+1.1.2 Secondary dates
+---------------------
+
+Real-life transactions sometimes involve more than one date - eg the
+date you write a cheque, and the date it clears in your bank.  When you
+want to model this, for more accurate daily balances, you can specify
+individual posting dates.
+
+   Or, you can use the older _secondary date_ feature (Ledger calls it
+auxiliary date or effective date).  Note: we support this for
+compatibility, but I usually recommend avoiding this feature; posting
+dates are almost always clearer and simpler.
+
+   A secondary date is written after the primary date, following an
+equals sign.  If the year is omitted, the primary date's year is
+assumed.  When running reports, the primary (left) date is used by
+default, but with the '--date2' flag (or '--aux-date' or '--effective'),
+the secondary (right) date will be used instead.
+
+   The meaning of secondary dates is up to you, but it's best to follow
+a consistent rule.  Eg "primary = the bank's clearing date, secondary =
+date the transaction was initiated, if different", as shown here:
+
+2010/2/23=2/19 movie ticket
+  expenses:cinema                   $10
+  assets:checking
+
+$ hledger register checking
+2010-02-23 movie ticket         assets:checking                $-10         $-10
+
+$ hledger register checking --date2
+2010-02-19 movie ticket         assets:checking                $-10         $-10
+
+
+File: hledger_journal.info,  Node: Posting dates,  Prev: Secondary dates,  Up: Dates
+
+1.1.3 Posting dates
+-------------------
+
+You can give individual postings a different date from their parent
+transaction, by adding a posting comment containing a tag (see below)
+like 'date:DATE'.  This is probably the best way to control posting
+dates precisely.  Eg in this example the expense should appear in May
+reports, and the deduction from checking should be reported on 6/1 for
+easy bank reconciliation:
+
+2015/5/30
+    expenses:food     $10  ; food purchased on saturday 5/30
+    assets:checking        ; bank cleared it on monday, date:6/1
+
+$ hledger -f t.j register food
+2015-05-30                      expenses:food                  $10           $10
+
+$ hledger -f t.j register checking
+2015-06-01                      assets:checking               $-10          $-10
+
+   DATE should be a simple date; if the year is not specified it will
+use the year of the transaction's date.  You can set the secondary date
+similarly, with 'date2:DATE2'.  The 'date:' or 'date2:' tags must have a
+valid simple date value if they are present, eg a 'date:' tag with no
+value is not allowed.
+
+   Ledger's earlier, more compact bracketed date syntax is also
+supported: '[DATE]', '[DATE=DATE2]' or '[=DATE2]'.  hledger will attempt
+to parse any square-bracketed sequence of the '0123456789/-.='
+characters in this way.  With this syntax, DATE infers its year from the
+transaction and DATE2 infers its year from DATE.
+
+
+File: hledger_journal.info,  Node: Status,  Next: Description,  Prev: Dates,  Up: Transactions
+
+1.2 Status
+==========
+
+Transactions, or individual postings within a transaction, can have a
+status mark, which is a single character before the transaction
+description or posting account name, separated from it by a space,
+indicating one of three statuses:
+
+mark  status
+ 
+-----------------
+      unmarked
+'!'   pending
+'*'   cleared
+
+   When reporting, you can filter by status with the '-U/--unmarked',
+'-P/--pending', and '-C/--cleared' flags; or the 'status:', 'status:!',
+and 'status:*' queries; or the U, P, C keys in hledger-ui.
+
+   Note, in Ledger and in older versions of hledger, the "unmarked"
+state is called "uncleared".  As of hledger 1.3 we have renamed it to
+unmarked for clarity.
+
+   To replicate Ledger and old hledger's behaviour of also matching
+pending, combine -U and -P.
+
+   Status marks are optional, but can be helpful eg for reconciling with
+real-world accounts.  Some editor modes provide highlighting and
+shortcuts for working with status.  Eg in Emacs ledger-mode, you can
+toggle transaction status with C-c C-e, or posting status with C-c C-c.
+
+   What "uncleared", "pending", and "cleared" actually mean is up to
+you.  Here's one suggestion:
+
+status     meaning
+--------------------------------------------------------------------------
+uncleared  recorded but not yet reconciled; needs review
+pending    tentatively reconciled (if needed, eg during a big
+           reconciliation)
+cleared    complete, reconciled as far as possible, and considered
+           correct
+
+   With this scheme, you would use '-PC' to see the current balance at
+your bank, '-U' to see things which will probably hit your bank soon
+(like uncashed checks), and no flags to see the most up-to-date state of
+your finances.
+
+
+File: hledger_journal.info,  Node: Description,  Next: Comments,  Prev: Status,  Up: Transactions
+
+1.3 Description
+===============
+
+A transaction's description is the rest of the line following the date
+and status mark (or until a comment begins).  Sometimes called the
+"narration" in traditional bookkeeping, it can be used for whatever you
+wish, or left blank.  Transaction descriptions can be queried, unlike
+comments.
+
+* Menu:
+
+* Payee and note::
+
+
+File: hledger_journal.info,  Node: Payee and note,  Up: Description
+
+1.3.1 Payee and note
+--------------------
+
+You can optionally include a '|' (pipe) character in descriptions to
+subdivide the description into separate fields for payee/payer name on
+the left (up to the first '|') and an additional note field on the right
+(after the first '|').  This may be worthwhile if you need to do more
+precise querying and pivoting by payee or by note.
+
+
+File: hledger_journal.info,  Node: Comments,  Next: Tags,  Prev: Description,  Up: Transactions
+
+1.4 Comments
+============
+
+Lines in the journal beginning with a semicolon (';') or hash ('#') or
+star ('*') are comments, and will be ignored.  (Star comments cause
+org-mode nodes to be ignored, allowing emacs users to fold and navigate
+their journals with org-mode or orgstruct-mode.)
+
+   You can attach comments to a transaction by writing them after the
+description and/or indented on the following lines (before the
+postings).  Similarly, you can attach comments to an individual posting
+by writing them after the amount and/or indented on the following lines.
+Transaction and posting comments must begin with a semicolon (';').
+
+   Some examples:
+
+# a file comment
+; another file comment
+* also a file comment, useful in org/orgstruct mode
+
+comment
+A multiline file comment, which continues
+until a line containing just "end comment"
+(or end of file).
+end comment
+
+2012/5/14 something  ; a transaction comment
+    ; the transaction comment, continued
+    posting1  1  ; a comment for posting 1
+    posting2
+    ; a comment for posting 2
+    ; another comment line for posting 2
+; a file comment (because not indented)
+
+   You can also comment larger regions of a file using 'comment' and
+'end comment' directives.
+
+
+File: hledger_journal.info,  Node: Tags,  Next: Postings,  Prev: Comments,  Up: Transactions
+
+1.5 Tags
+========
+
+Tags are a way to add extra labels or labelled data to postings and
+transactions, which you can then search or pivot on.
+
+   A simple tag is a word (which may contain hyphens) followed by a full
+colon, written inside a transaction or posting comment line:
+
+2017/1/16 bought groceries  ; sometag:
+
+   Tags can have a value, which is the text after the colon, up to the
+next comma or end of line, with leading/trailing whitespace removed:
+
+    expenses:food    $10 ; a-posting-tag: the tag value
+
+   Note this means hledger's tag values can not contain commas or
+newlines.  Ending at commas means you can write multiple short tags on
+one line, comma separated:
+
+    assets:checking  ; a comment containing tag1:, tag2: some value ...
+
+   Here,
+
+   * "'a comment containing'" is just comment text, not a tag
+   * "'tag1'" is a tag with no value
+   * "'tag2'" is another tag, whose value is "'some value ...'"
+
+   Tags in a transaction comment affect the transaction and all of its
+postings, while tags in a posting comment affect only that posting.  For
+example, the following transaction has three tags ('A', 'TAG2',
+'third-tag') and the posting has four (those plus 'posting-tag'):
+
+1/1 a transaction  ; A:, TAG2:
+    ; third-tag: a third transaction tag, <- with a value
+    (a)  $1  ; posting-tag:
+
+   Tags are like Ledger's metadata feature, except hledger's tag values
+are simple strings.
+
+
+File: hledger_journal.info,  Node: Postings,  Next: Account names,  Prev: Tags,  Up: Transactions
+
+1.6 Postings
+============
+
+A posting is an addition of some amount to, or removal of some amount
+from, an account.  Each posting line begins with at least one space or
+tab (2 or 4 spaces is common), followed by:
+
+   * (optional) a status character (empty, '!', or '*'), followed by a
+     space
+   * (required) an account name (any text, optionally containing *single
+     spaces*, until end of line or a double space)
+   * (optional) *two or more spaces* or tabs followed by an amount.
+
+   Positive amounts are being added to the account, negative amounts are
+being removed.
+
+   The amounts within a transaction must always sum up to zero.  As a
+convenience, one amount may be left blank; it will be inferred so as to
+balance the transaction.
+
+   Be sure to note the unusual two-space delimiter between account name
+and amount.  This makes it easy to write account names containing
+spaces.  But if you accidentally leave only one space (or tab) before
+the amount, the amount will be considered part of the account name.
+
+* Menu:
+
+* Virtual Postings::
+
+
+File: hledger_journal.info,  Node: Virtual Postings,  Up: Postings
+
+1.6.1 Virtual Postings
+----------------------
+
+A posting with a parenthesised account name is called a _virtual
+posting_ or _unbalanced posting_, which means it is exempt from the
+usual rule that a transaction's postings must balance add up to zero.
+
+   This is not part of double entry accounting, so you might choose to
+avoid this feature.  Or you can use it sparingly for certain special
+cases where it can be convenient.  Eg, you could set opening balances
+without using a balancing equity account:
+
+1/1 opening balances
+  (assets:checking)   $1000
+  (assets:savings)    $2000
+
+   A posting with a bracketed account name is called a _balanced virtual
+posting_.  The balanced virtual postings in a transaction must add up to
+zero (separately from other postings).  Eg:
+
+1/1 buy food with cash, update budget envelope subaccounts, & something else
+  assets:cash                    $-10 ; <- these balance
+  expenses:food                    $7 ; <-
+  expenses:food                    $3 ; <-
+  [assets:checking:budget:food]  $-10    ; <- and these balance
+  [assets:checking:available]     $10    ; <-
+  (something:else)                 $5       ; <- not required to balance
+
+   Ordinary non-parenthesised, non-bracketed postings are called _real
+postings_.  You can exclude virtual postings from reports with the
+'-R/--real' flag or 'real:1' query.
+
+
+File: hledger_journal.info,  Node: Account names,  Next: Amounts,  Prev: Postings,  Up: Transactions
+
+1.7 Account names
+=================
+
+Account names typically have several parts separated by a full colon,
+from which hledger derives a hierarchical chart of accounts.  They can
+be anything you like, but in finance there are traditionally five
+top-level accounts: 'assets', 'liabilities', 'income', 'expenses', and
+'equity'.
+
+   Account names may contain single spaces, eg: 'assets:accounts
+receivable'.  Because of this, they must always be followed by *two or
+more spaces* (or newline).
+
+   Account names can be aliased.
+
+
+File: hledger_journal.info,  Node: Amounts,  Next: Transaction prices,  Prev: Account names,  Up: Transactions
+
+1.8 Amounts
+===========
+
+After the account name, there is usually an amount.  (Important: between
+account name and amount, there must be *two or more spaces*.)
+
+   hledger's amount format is flexible, supporting several international
+formats.  Here are some examples.  Amounts have a number (the
+"quantity"):
+
+1
+
+   ..and usually a currency or commodity name (the "commodity").  This
+is a symbol, word, or phrase, to the left or right of the quantity, with
+or without a separating space:
+
+$1
+4000 AAPL
+
+   If the commodity name contains spaces, numbers, or punctuation, it
+must be enclosed in double quotes:
+
+3 "no. 42 green apples"
+
+   Amounts can be negative.  The minus sign can be written before or
+after a left-side commodity symbol:
+
+-$1
+$-1
+
+   Scientific E notation is allowed:
+
+1E-6
+EUR 1E3
+
+   A decimal mark (decimal point) can be written with a period or a
+comma:
+
+1.23
+1,23456780000009
+
+* Menu:
+
+* Digit group marks::
+* Amount display style::
+
+
+File: hledger_journal.info,  Node: Digit group marks,  Next: Amount display style,  Up: Amounts
+
+1.8.1 Digit group marks
+-----------------------
+
+In the integer part of the quantity (left of the decimal mark), groups
+of digits can optionally be separated by a "digit group mark" - a space,
+comma, or period (different from the decimal mark):
+
+     $1,000,000.00
+  EUR 2.000.000,00
+INR 9,99,99,999.00
+      1 000 000.9455
+
+   Note, a number containing a single group mark and no decimal mark is
+ambiguous.  Are these group marks or decimal marks ?
+
+1,000
+1.000
+
+   hledger will treat them both as decimal marks by default (cf #793).
+If you use digit group marks, to prevent confusion and undetected typos
+we recommend you write commodity directives at the top of the file to
+explicitly declare the decimal mark (and optionally a digit group mark).
+Note, these formats ("amount styles") are specific to each commodity, so
+if your data uses multiple formats, hledger can handle it:
+
+commodity $1,000.00
+commodity EUR 1.000,00
+commodity INR 9,99,99,999.00
+commodity       1 000 000.9455
+
+
+File: hledger_journal.info,  Node: Amount display style,  Prev: Digit group marks,  Up: Amounts
+
+1.8.2 Amount display style
+--------------------------
+
+For each commodity, hledger chooses a consistent format to use when
+displaying amounts.  (Except price amounts, which are always displayed
+as written).  The display style is chosen as follows:
+
+   * If there is a commodity directive (or default commodity directive)
+     for the commodity, that format is used (see examples above).
+
+   * Otherwise the format of the first posting amount in that commodity
+     seen in the journal is used.  But the number of decimal places
+     ("precision") will be the maximum from all posting amounts in that
+     commmodity.
+
+   * Or if there are no such amounts in the journal, a default format is
+     used (like '$1000.00').
+
+   Transaction prices don't affect the amount display style directly,
+but occasionally they can do so indirectly (eg when an posting's amount
+is inferred using a transaction price).  If you find this causing
+problems, use a commodity directive to fix the display style.
+
+   In summary: amounts will be displayed much as they appear in your
+journal, with the max observed number of decimal places.  If you want to
+see fewer decimal places in reports, use a commodity directive to
+override that.
+
+
+File: hledger_journal.info,  Node: Transaction prices,  Next: Balance Assertions,  Prev: Amounts,  Up: Transactions
+
+1.9 Transaction prices
+======================
+
+Within a transaction, you can note an amount's price in another
+commodity.  This can be used to document the cost (in a purchase) or
+selling price (in a sale).  For example, transaction prices are useful
+to record purchases of a foreign currency.  Note transaction prices are
+fixed at the time of the transaction, and do not change over time.  See
+also market prices, which represent prevailing exchange rates on a
+certain date.
+
+   There are several ways to record a transaction price:
+
+  1. Write the price per unit, as '@ UNITPRICE' after the amount:
+
+     2009/1/1
+       assets:euros     €100 @ $1.35  ; one hundred euros purchased at $1.35 each
+       assets:dollars                 ; balancing amount is -$135.00
+
+  2. Write the total price, as '@@ TOTALPRICE' after the amount:
+
+     2009/1/1
+       assets:euros     €100 @@ $135  ; one hundred euros purchased at $135 for the lot
+       assets:dollars
+
+  3. Specify amounts for all postings, using exactly two commodities,
+     and let hledger infer the price that balances the transaction:
+
+     2009/1/1
+       assets:euros     €100          ; one hundred euros purchased
+       assets:dollars  $-135          ; for $135
+
+   (Ledger users: Ledger uses a different syntax for fixed prices,
+'{=UNITPRICE}', which hledger currently ignores).
+
+   Use the '-B/--cost' flag to convert amounts to their transaction
+price's commodity, if any.  (mnemonic: "B" is from "cost Basis", as in
+Ledger).  Eg here is how -B affects the balance report for the example
+above:
+
+$ hledger bal -N --flat
+               $-135  assets:dollars
+                €100  assets:euros
+$ hledger bal -N --flat -B
+               $-135  assets:dollars
+                $135  assets:euros    # <- the euros' cost
+
+   Note -B is sensitive to the order of postings when a transaction
+price is inferred: the inferred price will be in the commodity of the
+last amount.  So if example 3's postings are reversed, while the
+transaction is equivalent, -B shows something different:
+
+2009/1/1
+  assets:dollars  $-135              ; 135 dollars sold
+  assets:euros     €100              ; for 100 euros
+
+$ hledger bal -N --flat -B
+               €-100  assets:dollars  # <- the dollars' selling price
+                €100  assets:euros
+
+
+File: hledger_journal.info,  Node: Balance Assertions,  Next: Balance Assignments,  Prev: Transaction prices,  Up: Transactions
+
+1.10 Balance Assertions
+=======================
+
+hledger supports Ledger-style balance assertions in journal files.
+These look like, for example, '= EXPECTEDBALANCE' following a posting's
+amount.  Eg here we assert the expected dollar balance in accounts a and
+b after each posting:
+
+2013/1/1
+  a   $1  =$1
+  b       =$-1
+
+2013/1/2
+  a   $1  =$2
+  b  $-1  =$-2
+
+   After reading a journal file, hledger will check all balance
+assertions and report an error if any of them fail.  Balance assertions
+can protect you from, eg, inadvertently disrupting reconciled balances
+while cleaning up old entries.  You can disable them temporarily with
+the '-I/--ignore-assertions' flag, which can be useful for
+troubleshooting or for reading Ledger files.  (Note: this flag currently
+does not disable balance assignments, below).
+
+* Menu:
+
+* Assertions and ordering::
+* Assertions and included files::
+* Assertions and multiple -f options::
+* Assertions and commodities::
+* Assertions and prices::
+* Assertions and subaccounts::
+* Assertions and virtual postings::
+* Assertions and precision::
+
+
+File: hledger_journal.info,  Node: Assertions and ordering,  Next: Assertions and included files,  Up: Balance Assertions
+
+1.10.1 Assertions and ordering
+------------------------------
+
+hledger sorts an account's postings and assertions first by date and
+then (for postings on the same day) by parse order.  Note this is
+different from Ledger, which sorts assertions only by parse order.
+(Also, Ledger assertions do not see the accumulated effect of repeated
+postings to the same account within a transaction.)
+
+   So, hledger balance assertions keep working if you reorder
+differently-dated transactions within the journal.  But if you reorder
+same-dated transactions or postings, assertions might break and require
+updating.  This order dependence does bring an advantage: precise
+control over the order of postings and assertions within a day, so you
+can assert intra-day balances.
+
+
+File: hledger_journal.info,  Node: Assertions and included files,  Next: Assertions and multiple -f options,  Prev: Assertions and ordering,  Up: Balance Assertions
+
+1.10.2 Assertions and included files
+------------------------------------
+
+With included files, things are a little more complicated.  Including
+preserves the ordering of postings and assertions.  If you have multiple
+postings to an account on the same day, split across different files,
+and you also want to assert the account's balance on the same day,
+you'll have to put the assertion in the right file.
+
+
+File: hledger_journal.info,  Node: Assertions and multiple -f options,  Next: Assertions and commodities,  Prev: Assertions and included files,  Up: Balance Assertions
+
+1.10.3 Assertions and multiple -f options
+-----------------------------------------
+
+Balance assertions don't work well across files specified with multiple
+-f options.  Use include or concatenate the files instead.
+
+
+File: hledger_journal.info,  Node: Assertions and commodities,  Next: Assertions and prices,  Prev: Assertions and multiple -f options,  Up: Balance Assertions
+
+1.10.4 Assertions and commodities
+---------------------------------
+
+The asserted balance must be a simple single-commodity amount, and in
+fact the assertion checks only this commodity's balance within the
+(possibly multi-commodity) account balance.  This is how assertions work
+in Ledger also.  We could call this a "partial" balance assertion.
+
+   To assert the balance of more than one commodity in an account, you
+can write multiple postings, each asserting one commodity's balance.
+
+   You can make a stronger "total" balance assertion by writing a double
+equals sign ('== EXPECTEDBALANCE').  This asserts that there are no
+other unasserted commodities in the account (or, that their balance is
+0).
+
+2013/1/1
+  a   $1
+  a    1€
+  b  $-1
+  c   -1€
+
+2013/1/2  ; These assertions succeed
+  a    0  =  $1
+  a    0  =   1€
+  b    0 == $-1
+  c    0 ==  -1€
+
+2013/1/3  ; This assertion fails as 'a' also contains 1€
+  a    0 ==  $1
+
+   It's not yet possible to make a complete assertion about a balance
+that has multiple commodities.  One workaround is to isolate each
+commodity into its own subaccount:
+
+2013/1/1
+  a:usd   $1
+  a:euro   1€
+  b
+
+2013/1/2
+  a        0 ==  0
+  a:usd    0 == $1
+  a:euro   0 ==  1€
+
+
+File: hledger_journal.info,  Node: Assertions and prices,  Next: Assertions and subaccounts,  Prev: Assertions and commodities,  Up: Balance Assertions
+
+1.10.5 Assertions and prices
+----------------------------
+
+Balance assertions ignore transaction prices, and should normally be
+written without one:
+
+2019/1/1
+  (a)     $1 @ €1 = $1
+
+   We do allow prices to be written there, however, and print shows
+them, even though they don't affect whether the assertion passes or
+fails.  This is for backward compatibility (hledger's close command used
+to generate balance assertions with prices), and because balance
+_assignments_ do use them (see below).
+
+
+File: hledger_journal.info,  Node: Assertions and subaccounts,  Next: Assertions and virtual postings,  Prev: Assertions and prices,  Up: Balance Assertions
+
+1.10.6 Assertions and subaccounts
+---------------------------------
+
+The balance assertions above ('=' and '==') do not count the balance
+from subaccounts; they check the account's exclusive balance only.  You
+can assert the balance including subaccounts by writing '=*' or '==*',
+eg:
+
+2019/1/1
+  equity:opening balances
+  checking:a       5
+  checking:b       5
+  checking         1  ==* 11
+
+
+File: hledger_journal.info,  Node: Assertions and virtual postings,  Next: Assertions and precision,  Prev: Assertions and subaccounts,  Up: Balance Assertions
+
+1.10.7 Assertions and virtual postings
+--------------------------------------
+
+Balance assertions are checked against all postings, both real and
+virtual.  They are not affected by the '--real/-R' flag or 'real:'
+query.
+
+
+File: hledger_journal.info,  Node: Assertions and precision,  Prev: Assertions and virtual postings,  Up: Balance Assertions
+
+1.10.8 Assertions and precision
+-------------------------------
+
+Balance assertions compare the exactly calculated amounts, which are not
+always what is shown by reports.  Eg a commodity directive may limit the
+display precision, but this will not affect balance assertions.  Balance
+assertion failure messages show exact amounts.
+
+
+File: hledger_journal.info,  Node: Balance Assignments,  Next: Directives,  Prev: Balance Assertions,  Up: Transactions
+
+1.11 Balance Assignments
+========================
+
+Ledger-style balance assignments are also supported.  These are like
+balance assertions, but with no posting amount on the left side of the
+equals sign; instead it is calculated automatically so as to satisfy the
+assertion.  This can be a convenience during data entry, eg when setting
+opening balances:
+
+; starting a new journal, set asset account balances
+2016/1/1 opening balances
+  assets:checking            = $409.32
+  assets:savings             = $735.24
+  assets:cash                 = $42
+  equity:opening balances
+
+   or when adjusting a balance to reality:
+
+; no cash left; update balance, record any untracked spending as a generic expense
+2016/1/15
+  assets:cash    = $0
+  expenses:misc
+
+   The calculated amount depends on the account's balance in the
+commodity at that point (which depends on the previously-dated postings
+of the commodity to that account since the last balance assertion or
+assignment).  Note that using balance assignments makes your journal a
+little less explicit; to know the exact amount posted, you have to run
+hledger or do the calculations yourself, instead of just reading it.
+
+* Menu:
+
+* Balance assignments and prices::
+
+
+File: hledger_journal.info,  Node: Balance assignments and prices,  Up: Balance Assignments
+
+1.11.1 Balance assignments and prices
+-------------------------------------
+
+A transaction price in a balance assignment will cause the calculated
+amount to have that price attached:
+
+2019/1/1
+  (a)             = $1 @ €2
+
+$ hledger print --explicit
+2019-01-01
+    (a)         $1 @ €2 = $1 @ €2
+
+
+File: hledger_journal.info,  Node: Directives,  Next: Periodic transactions,  Prev: Balance Assignments,  Up: Transactions
+
+1.12 Directives
+===============
+
+A directive is a line in the journal beginning with a special keyword,
+that influences how the journal is processed.  hledger's directives are
+based on a subset of Ledger's, but there are many differences (and also
+some differences between hledger versions).
+
+   Directives' behaviour and interactions can get a little bit complex,
+so here is a table summarising the directives and their effects, with
+links to more detailed docs.
+
+directiveend       subdirectivespurpose                  can affect (as of
+         directive                                       2018/06)
+-----------------------------------------------------------------------------
+'account'          any     document account names,       all entries in
+                   text    declare account types &       all files, before
+                           display order                 or after
+'alias'  'end              rewrite account names         following
+         aliases'                                        inline/included
+                                                         entries until end
+                                                         of current file
+                                                         or end directive
+'apply   'end              prepend a common parent to    following
+account' apply             account names                 inline/included
+         account'                                        entries until end
+                                                         of current file
+                                                         or end directive
+'comment''end              ignore part of journal        following
+         comment'                                        inline/included
+                                                         entries until end
+                                                         of current file
+                                                         or end directive
+'commodity'        'format'declare a commodity and its   number notation:
+                           number notation & display     following entries
+                           style                         in that commodity
+                                                         in all files;
+                                                         display style:
+                                                         amounts of that
+                                                         commodity in
+                                                         reports
+'D'                        declare a commodity to be     default
+                           used for commodityless        commodity:
+                           amounts, and its number       following
+                           notation & display style      commodityless
+                                                         entries until end
+                                                         of current file;
+                                                         number notation:
+                                                         following entries
+                                                         in that commodity
+                                                         until end of
+                                                         current file;
+                                                         display style:
+                                                         amounts of that
+                                                         commodity in
+                                                         reports
+'include'                  include entries/directives    what the included
+                           from another file             directives affect
+'P'                        declare a market price for    amounts of that
+                           a commodity                   commodity in
+                                                         reports, when -V
+                                                         is used
+'Y'                        declare a year for yearless   following
+                           dates                         inline/included
+                                                         entries until end
+                                                         of current file
+
+   And some definitions:
+
+subdirectiveoptional indented directive line immediately following a parent
+       directive
+number how to interpret numbers when parsing journal entries (the
+notationidentity of the decimal separator character).  (Currently each
+       commodity can have its own notation, even in the same file.)
+displayhow to display amounts of a commodity in reports (symbol side
+style  and spacing, digit groups, decimal separator, decimal places)
+directivewhich entries and (when there are multiple files) which files
+scope  are affected by a directive
+
+   As you can see, directives vary in which journal entries and files
+they affect, and whether they are focussed on input (parsing) or output
+(reports).  Some directives have multiple effects.
+
+   If you have a journal made up of multiple files, or pass multiple -f
+options on the command line, note that directives which affect input
+typically last only until the end of their defining file.  This provides
+more simplicity and predictability, eg reports are not changed by
+writing file options in a different order.  It can be surprising at
+times though.
+
+* Menu:
+
+* Comment blocks::
+* Including other files::
+* Default year::
+* Declaring commodities::
+* Default commodity::
+* Market prices::
+* Declaring accounts::
+* Rewriting accounts::
+* Default parent account::
+
+
+File: hledger_journal.info,  Node: Comment blocks,  Next: Including other files,  Up: Directives
+
+1.12.1 Comment blocks
+---------------------
+
+A line containing just 'comment' starts a commented region of the file,
+and a line containing just 'end comment' (or the end of the current
+file) ends it.  See also comments.
+
+
+File: hledger_journal.info,  Node: Including other files,  Next: Default year,  Prev: Comment blocks,  Up: Directives
+
+1.12.2 Including other files
+----------------------------
+
+You can pull in the content of additional files by writing an include
+directive, like this:
+
+include path/to/file.journal
+
+   If the path does not begin with a slash, it is relative to the
+current file.  The include file path may contain common glob patterns
+(e.g.  '*').
+
+   The 'include' directive can only be used in journal files.  It can
+include journal, timeclock or timedot files, but not CSV files.
+
+
+File: hledger_journal.info,  Node: Default year,  Next: Declaring commodities,  Prev: Including other files,  Up: Directives
+
+1.12.3 Default year
+-------------------
+
+You can set a default year to be used for subsequent dates which don't
+specify a year.  This is a line beginning with 'Y' followed by the year.
+Eg:
+
+Y2009  ; set default year to 2009
+
+12/15  ; equivalent to 2009/12/15
+  expenses  1
+  assets
+
+Y2010  ; change default year to 2010
+
+2009/1/30  ; specifies the year, not affected
+  expenses  1
+  assets
+
+1/31   ; equivalent to 2010/1/31
+  expenses  1
+  assets
+
+
+File: hledger_journal.info,  Node: Declaring commodities,  Next: Default commodity,  Prev: Default year,  Up: Directives
+
+1.12.4 Declaring commodities
+----------------------------
+
+The 'commodity' directive has several functions:
+
+  1. It declares commodities which may be used in the journal.  This is
+     currently not enforced, but can serve as documentation.
+
+  2. It declares what decimal mark character (period or comma) to expect
+     when parsing input - useful to disambiguate international number
+     formats in your data.  (Without this, hledger will parse both
+     '1,000' and '1.000' as 1).
+
+  3. It declares the amount display style to use in output - decimal and
+     digit group marks, number of decimal places, symbol placement etc.
+
+   You are likely to run into one of the problems solved by commodity
+directives, sooner or later, so it's a good idea to just always use them
+to declare your commodities.
+
+   A commodity directive is just the word 'commodity' followed by an
+amount.  It may be written on a single line, like this:
+
+; commodity EXAMPLEAMOUNT
+
+; display AAAA amounts with the symbol on the right, space-separated,
+; using period as decimal point, with four decimal places, and
+; separating thousands with comma.
+commodity 1,000.0000 AAAA
+
+   or on multiple lines, using the "format" subdirective.  (In this case
+the commodity symbol appears twice and should be the same in both
+places.):
+
+; commodity SYMBOL
+;   format EXAMPLEAMOUNT
+
+; display indian rupees with currency name on the left,
+; thousands, lakhs and crores comma-separated,
+; period as decimal point, and two decimal places.
+commodity INR
+  format INR 1,00,00,000.00
+
+   The quantity of the amount does not matter; only the format is
+significant.  The number must include a decimal mark: either a period or
+a comma, followed by 0 or more decimal digits.
+
+
+File: hledger_journal.info,  Node: Default commodity,  Next: Market prices,  Prev: Declaring commodities,  Up: Directives
+
+1.12.5 Default commodity
+------------------------
+
+The 'D' directive sets a default commodity, to be used for amounts
+without a commodity symbol (ie, plain numbers).  This commodity will be
+applied to all subsequent commodity-less amounts, or until the next 'D'
+directive.  (Note, this is different from Ledger's 'D'.)
+
+   For compatibility/historical reasons, 'D' also acts like a
+'commodity' directive, setting the commodity's display style (for
+output) and decimal mark (for parsing input).  As with 'commodity', the
+amount must always be written with a decimal mark (period or comma).  If
+both directives are used, 'commodity''s style takes precedence.
+
+   The syntax is 'D AMOUNT'.  Eg:
+
+; commodity-less amounts should be treated as dollars
+; (and displayed with the dollar sign on the left, thousands separators and two decimal places)
+D $1,000.00
+
+1/1
+  a     5  ; <- commodity-less amount, parsed as $5 and displayed as $5.00
+  b
+
+
+File: hledger_journal.info,  Node: Market prices,  Next: Declaring accounts,  Prev: Default commodity,  Up: Directives
+
+1.12.6 Market prices
+--------------------
+
+The 'P' directive declares a market price, which is an exchange rate
+between two commodities on a certain date.  (In Ledger, they are called
+"historical prices".)  These are often obtained from a stock exchange,
+cryptocurrency exchange, or the foreign exchange market.
+
+   Here is the format:
+
+P DATE COMMODITYA COMMODITYBAMOUNT
+
+   * DATE is a simple date
+   * COMMODITYA is the symbol of the commodity being priced
+   * COMMODITYBAMOUNT is an amount (symbol and quantity) in a second
+     commodity, giving the price in commodity B of one unit of commodity
+     A.
+
+   These two market price directives say that one euro was worth 1.35 US
+dollars during 2009, and $1.40 from 2010 onward:
+
+P 2009/1/1 € $1.35
+P 2010/1/1 € $1.40
+
+   The '-V/--value' flag can be used to convert reported amounts to
+another commodity using these prices.
+
+
+File: hledger_journal.info,  Node: Declaring accounts,  Next: Rewriting accounts,  Prev: Market prices,  Up: Directives
+
+1.12.7 Declaring accounts
+-------------------------
+
+'account' directives can be used to pre-declare accounts.  Though not
+required, they can provide several benefits:
+
+   * They can document your intended chart of accounts, providing a
+     reference.
+   * They can store extra information about accounts (account numbers,
+     notes, etc.)
+   * They can help hledger know your accounts' types (asset, liability,
+     equity, revenue, expense), useful for reports like balancesheet and
+     incomestatement.
+   * They control account display order in reports, allowing
+     non-alphabetic sorting (eg Revenues to appear above Expenses).
+   * They help with account name completion in the add command,
+     hledger-iadd, hledger-web, ledger-mode etc.
+
+   The simplest form is just the word 'account' followed by a
+hledger-style account name, eg:
+
+account assets:bank:checking
+
+* Menu:
+
+* Account comments::
+* Account subdirectives::
+* Account types::
+* Account display order::
+
+
+File: hledger_journal.info,  Node: Account comments,  Next: Account subdirectives,  Up: Declaring accounts
+
+1.12.7.1 Account comments
+.........................
+
+Comments, beginning with a semicolon, can be added:
+
+   * on the same line, *after two or more spaces* (because ; is allowed
+     in account names)
+   * on the next lines, indented
+
+   An example of both:
+
+account assets:bank:checking  ; same-line comment, note 2+ spaces before ;
+  ; next-line comment
+  ; another with tag, acctno:12345 (not used yet)
+
+   Same-line comments are not supported by Ledger, or hledger <1.13.
+
+
+File: hledger_journal.info,  Node: Account subdirectives,  Next: Account types,  Prev: Account comments,  Up: Declaring accounts
+
+1.12.7.2 Account subdirectives
+..............................
+
+We also allow (and ignore) Ledger-style indented subdirectives, just for
+compatibility.:
+
+account assets:bank:checking
+  format blah blah  ; <- subdirective, ignored
+
+   Here is the full syntax of account directives:
+
+account ACCTNAME  [ACCTTYPE] [;COMMENT]
+  [;COMMENTS]
+  [LEDGER-STYLE SUBDIRECTIVES, IGNORED]
+
+
+File: hledger_journal.info,  Node: Account types,  Next: Account display order,  Prev: Account subdirectives,  Up: Declaring accounts
+
+1.12.7.3 Account types
+......................
+
+hledger recognises five types (or classes) of account: Asset, Liability,
+Equity, Revenue, Expense.  This is used by a few accounting-aware
+reports such as balancesheet, incomestatement and cashflow.
+Auto-detected account types If you name your top-level accounts with
+some variation of 'assets', 'liabilities'/'debts', 'equity',
+'revenues'/'income', or 'expenses', their types are detected
+automatically.  Account types declared with tags More generally, you can
+declare an account's type with an account directive, by writing a
+'type:' tag in a comment, followed by one of the words 'Asset',
+'Liability', 'Equity', 'Revenue', 'Expense', or one of the letters
+'ALERX' (case insensitive):
+
+account assets       ; type:Asset
+account liabilities  ; type:Liability
+account equity       ; type:Equity
+account revenues     ; type:Revenue
+account expenses     ; type:Expense
+
+   Account types declared with account type codes Or, you can write one
+of those letters separated from the account name by two or more spaces,
+but this should probably be considered deprecated as of hledger 1.13:
+
+account assets       A
+account liabilities  L
+account equity       E
+account revenues     R
+account expenses     X
+
+   Overriding auto-detected types If you ever override the types of
+those auto-detected english account names mentioned above, you might
+need to help the reports a bit.  Eg:
+
+; make "liabilities" not have the liability type - who knows why
+account liabilities  ; type:E
+
+; we need to ensure some other account has the liability type,
+; otherwise balancesheet would still show "liabilities" under Liabilities
+account -            ; type:L
+
+
+File: hledger_journal.info,  Node: Account display order,  Prev: Account types,  Up: Declaring accounts
+
+1.12.7.4 Account display order
+..............................
+
+Account directives also set the order in which accounts are displayed,
+eg in reports, the hledger-ui accounts screen, and the hledger-web
+sidebar.  By default accounts are listed in alphabetical order.  But if
+you have these account directives in the journal:
+
+account assets
+account liabilities
+account equity
+account revenues
+account expenses
+
+   you'll see those accounts displayed in declaration order, not
+alphabetically:
+
+$ hledger accounts -1
+assets
+liabilities
+equity
+revenues
+expenses
+
+   Undeclared accounts, if any, are displayed last, in alphabetical
+order.
+
+   Note that sorting is done at each level of the account tree (within
+each group of sibling accounts under the same parent).  And currently,
+this directive:
+
+account other:zoo
+
+   would influence the position of 'zoo' among 'other''s subaccounts,
+but not the position of 'other' among the top-level accounts.  This
+means:
+
+   * you will sometimes declare parent accounts (eg 'account other'
+     above) that you don't intend to post to, just to customize their
+     display order
+   * sibling accounts stay together (you couldn't display 'x:y' in
+     between 'a:b' and 'a:c').
+
+
+File: hledger_journal.info,  Node: Rewriting accounts,  Next: Default parent account,  Prev: Declaring accounts,  Up: Directives
+
+1.12.8 Rewriting accounts
+-------------------------
+
+You can define account alias rules which rewrite your account names, or
+parts of them, before generating reports.  This can be useful for:
+
+   * expanding shorthand account names to their full form, allowing
+     easier data entry and a less verbose journal
+   * adapting old journals to your current chart of accounts
+   * experimenting with new account organisations, like a new hierarchy
+     or combining two accounts into one
+   * customising reports
+
+   Account aliases also rewrite account names in account directives.
+They do not affect account names being entered via hledger add or
+hledger-web.
+
+   See also Rewrite account names.
+
+* Menu:
+
+* Basic aliases::
+* Regex aliases::
+* Combining aliases::
+* end aliases::
+
+
+File: hledger_journal.info,  Node: Basic aliases,  Next: Regex aliases,  Up: Rewriting accounts
+
+1.12.8.1 Basic aliases
+......................
+
+To set an account alias, use the 'alias' directive in your journal file.
+This affects all subsequent journal entries in the current file or its
+included files.  The spaces around the = are optional:
+
+alias OLD = NEW
+
+   Or, you can use the '--alias 'OLD=NEW'' option on the command line.
+This affects all entries.  It's useful for trying out aliases
+interactively.
+
+   OLD and NEW are case sensitive full account names.  hledger will
+replace any occurrence of the old account name with the new one.
+Subaccounts are also affected.  Eg:
+
+alias checking = assets:bank:wells fargo:checking
+; rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"
+
+
+File: hledger_journal.info,  Node: Regex aliases,  Next: Combining aliases,  Prev: Basic aliases,  Up: Rewriting accounts
+
+1.12.8.2 Regex aliases
+......................
+
+There is also a more powerful variant that uses a regular expression,
+indicated by the forward slashes:
+
+alias /REGEX/ = REPLACEMENT
+
+   or '--alias '/REGEX/=REPLACEMENT''.
+
+   REGEX is a case-insensitive regular expression.  Anywhere it matches
+inside an account name, the matched part will be replaced by
+REPLACEMENT. If REGEX contains parenthesised match groups, these can be
+referenced by the usual numeric backreferences in REPLACEMENT. Eg:
+
+alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3
+; rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking"
+
+   Also note that REPLACEMENT continues to the end of line (or on
+command line, to end of option argument), so it can contain trailing
+whitespace.
+
+
+File: hledger_journal.info,  Node: Combining aliases,  Next: end aliases,  Prev: Regex aliases,  Up: Rewriting accounts
+
+1.12.8.3 Combining aliases
+..........................
+
+You can define as many aliases as you like, using journal directives
+and/or command line options.
+
+   Recursive aliases - where an account name is rewritten by one alias,
+then by another alias, and so on - are allowed.  Each alias sees the
+effect of previously applied aliases.
+
+   In such cases it can be important to understand which aliases will be
+applied and in which order.  For (each account name in) each journal
+entry, we apply:
+
+  1. 'alias' directives preceding the journal entry, most recently
+     parsed first (ie, reading upward from the journal entry, bottom to
+     top)
+  2. '--alias' options, in the order they appeared on the command line
+     (left to right).
+
+   In other words, for (an account name in) a given journal entry:
+
+   * the nearest alias declaration before/above the entry is applied
+     first
+   * the next alias before/above that will be be applied next, and so on
+   * aliases defined after/below the entry do not affect it.
+
+   This gives nearby aliases precedence over distant ones, and helps
+provide semantic stability - aliases will keep working the same way
+independent of which files are being read and in which order.
+
+   In case of trouble, adding '--debug=6' to the command line will show
+which aliases are being applied when.
+
+
+File: hledger_journal.info,  Node: end aliases,  Prev: Combining aliases,  Up: Rewriting accounts
+
+1.12.8.4 'end aliases'
+......................
+
+You can clear (forget) all currently defined aliases with the 'end
+aliases' directive:
+
+end aliases
+
+
+File: hledger_journal.info,  Node: Default parent account,  Prev: Rewriting accounts,  Up: Directives
+
+1.12.9 Default parent account
+-----------------------------
+
+You can specify a parent account which will be prepended to all accounts
+within a section of the journal.  Use the 'apply account' and 'end apply
+account' directives like so:
+
+apply account home
+
+2010/1/1
+    food    $10
+    cash
+
+end apply account
+
+   which is equivalent to:
+
+2010/01/01
+    home:food           $10
+    home:cash          $-10
+
+   If 'end apply account' is omitted, the effect lasts to the end of the
+file.  Included files are also affected, eg:
+
+apply account business
+include biz.journal
+end apply account
+apply account personal
+include personal.journal
+
+   Prior to hledger 1.0, legacy 'account' and 'end' spellings were also
+supported.
+
+   A default parent account also affects account directives.  It does
+not affect account names being entered via hledger add or hledger-web.
+If account aliases are present, they are applied after the default
+parent account.
+
+
+File: hledger_journal.info,  Node: Periodic transactions,  Next: Auto postings / transaction modifiers,  Prev: Directives,  Up: Transactions
+
+1.13 Periodic transactions
+==========================
+
+Periodic transaction rules describe transactions that recur.  They allow
+hledger to generate temporary future transactions to help with
+forecasting, so you don't have to write out each one in the journal, and
+it's easy to try out different forecasts.  Secondly, they are also used
+to define the budgets shown in budget reports.
+
+   Periodic transactions can be a little tricky, so before you use them,
+read this whole section - or at least these tips:
+
+  1. Two spaces accidentally added or omitted will cause you trouble -
+     read about this below.
+  2. For troubleshooting, show the generated transactions with 'hledger
+     print --forecast tag:generated' or 'hledger register --forecast
+     tag:generated'.
+  3. Forecasted transactions will begin only after the last
+     non-forecasted transaction's date.
+  4. Forecasted transactions will end 6 months from today, by default.
+     See below for the exact start/end rules.
+  5. period expressions can be tricky.  Their documentation needs
+     improvement, but is worth studying.
+  6. Some period expressions with a repeating interval must begin on a
+     natural boundary of that interval.  Eg in 'weekly from DATE', DATE
+     must be a monday.  '~ weekly from 2019/10/1' (a tuesday) will give
+     an error.
+  7. Other period expressions with an interval are automatically
+     expanded to cover a whole number of that interval.  (This is done
+     to improve reports, but it also affects periodic transactions.
+     Yes, it's a bit inconsistent with the above.)  Eg: '~ every 10th
+     day of month from 2020/01', which is equivalent to '~ every 10th
+     day of month from 2020/01/01', will be adjusted to start on
+     2019/12/10.
+
+* Menu:
+
+* Periodic rule syntax::
+* Two spaces between period expression and description!::
+* Forecasting with periodic transactions::
+* Budgeting with periodic transactions::
+
+
+File: hledger_journal.info,  Node: Periodic rule syntax,  Next: Two spaces between period expression and description!,  Up: Periodic transactions
+
+1.13.1 Periodic rule syntax
+---------------------------
+
+A periodic transaction rule looks like a normal journal entry, with the
+date replaced by a tilde ('~') followed by a period expression
+(mnemonic: '~' looks like a recurring sine wave.):
+
+~ monthly
+    expenses:rent          $2000
+    assets:bank:checking
+
+   There is an additional constraint on the period expression: the start
+date must fall on a natural boundary of the interval.  Eg 'monthly from
+2018/1/1' is valid, but 'monthly from 2018/1/15' is not.
+
+   Partial or relative dates (M/D, D, tomorrow, last week) in the period
+expression can work (useful or not).  They will be relative to today's
+date, unless a Y default year directive is in effect, in which case they
+will be relative to Y/1/1.
+
+
+File: hledger_journal.info,  Node: Two spaces between period expression and description!,  Next: Forecasting with periodic transactions,  Prev: Periodic rule syntax,  Up: Periodic transactions
+
+1.13.2 Two spaces between period expression and description!
+------------------------------------------------------------
+
+If the period expression is followed by a transaction description, these
+must be separated by *two or more spaces*.  This helps hledger know
+where the period expression ends, so that descriptions can not
+accidentally alter their meaning, as in this example:
+
+; 2 or more spaces needed here, so the period is not understood as "every 2 months in 2020"
+;               ||
+;               vv
+~ every 2 months  in 2020, we will review
+    assets:bank:checking   $1500
+    income:acme inc
+
+   So,
+
+   * Do write two spaces between your period expression and your
+     transaction description, if any.
+   * Don't accidentally write two spaces in the middle of your period
+     expression.
+
+
+File: hledger_journal.info,  Node: Forecasting with periodic transactions,  Next: Budgeting with periodic transactions,  Prev: Two spaces between period expression and description!,  Up: Periodic transactions
+
+1.13.3 Forecasting with periodic transactions
+---------------------------------------------
+
+With the '--forecast' flag, each periodic transaction rule generates
+future transactions recurring at the specified interval.  These are not
+saved in the journal, but appear in all reports.  They will look like
+normal transactions, but with an extra tag:
+
+   * 'generated-transaction:~ PERIODICEXPR' - shows that this was
+     generated by a periodic transaction rule, and the period
+
+   There is also a hidden tag, with an underscore prefix, which does not
+appear in hledger's output:
+
+   * '_generated-transaction:~ PERIODICEXPR'
+
+   This can be used to match transactions generated "just now", rather
+than generated in the past and saved to the journal.
+
+   Forecast transactions start on the first occurrence, and end on the
+last occurrence, of their interval within the forecast period.  The
+forecast period:
+
+   * begins on the later of
+        * the report start date if specified with -b/-p/date:
+        * the day after the latest normal (non-periodic) transaction in
+          the journal, or today if there are no normal transactions.
+
+   * ends on the report end date if specified with -e/-p/date:, or 180
+     days from today.
+
+   where "today" means the current date at report time.  The "later of"
+rule ensures that forecast transactions do not overlap normal
+transactions in time; they will begin only after normal transactions
+end.
+
+   Forecasting can be useful for estimating balances into the future,
+and experimenting with different scenarios.  Note the start date logic
+means that forecasted transactions are automatically replaced by normal
+transactions as you add those.
+
+   Forecasting can also help with data entry: describe most of your
+transactions with periodic rules, and every so often copy the output of
+'print --forecast' to the journal.
+
+   You can generate one-time transactions too: just write a period
+expression specifying a date with no report interval.  (You could also
+write a normal transaction with a future date, but remember this
+disables forecast transactions on previous dates.)
+
+
+File: hledger_journal.info,  Node: Budgeting with periodic transactions,  Prev: Forecasting with periodic transactions,  Up: Periodic transactions
+
+1.13.4 Budgeting with periodic transactions
+-------------------------------------------
+
+With the '--budget' flag, currently supported by the balance command,
+each periodic transaction rule declares recurring budget goals for the
+specified accounts.  Eg the first example above declares a goal of
+spending $2000 on rent (and also, a goal of depositing $2000 into
+checking) every month.  Goals and actual performance can then be
+compared in budget reports.
+
+   For more details, see: balance: Budget report and Budgeting and
+Forecasting.
+
+
+File: hledger_journal.info,  Node: Auto postings / transaction modifiers,  Prev: Periodic transactions,  Up: Transactions
+
+1.14 Auto postings / transaction modifiers
+==========================================
+
+Transaction modifier rules, AKA auto posting rules, describe changes to
+be applied automatically to certain matched transactions.  Currently
+just one kind of change is possible - adding extra postings, which we
+call "automated postings" or just "auto postings".  These rules become
+active when you use the '--auto' flag.
+
+   A transaction modifier rule looks much like a normal transaction
+except the first line is an equals sign followed by a query that matches
+certain postings (mnemonic: '=' suggests matching).  And each "posting"
+is actually a posting-generating rule:
+
+= QUERY
+    ACCOUNT  AMOUNT
+    ACCOUNT  [AMOUNT]
+    ...
+
+   These posting-generating rules look like normal postings, except the
+amount can be:
+
+   * a normal amount with a commodity symbol, eg '$2'.  This will be
+     used as-is.
+   * a number, eg '2'.  The commodity symbol (if any) from the matched
+     posting will be added to this.
+   * a numeric multiplier, eg '*2' (a star followed by a number N). The
+     matched posting's amount (and total price, if any) will be
+     multiplied by N.
+   * a multiplier with a commodity symbol, eg '*$2' (a star, number N,
+     and symbol S). The matched posting's amount will be multiplied by
+     N, and its commodity symbol will be replaced with S.
+
+   A query term containing spaces must be enclosed in single or double
+quotes, as on the command line.  Eg, note the quotes around the second
+query term below:
+
+= expenses:groceries 'expenses:dining out'
+    (budget:funds:dining out)                 *-1
+
+   These rules have global effect - a rule appearing anywhere in your
+data can potentially affect any transaction, including transactions
+recorded above it or in another file.
+
+   Some examples:
+
+; every time I buy food, schedule a dollar donation
+= expenses:food
+    (liabilities:charity)   $-1
+
+; when I buy a gift, also deduct that amount from a budget envelope subaccount
+= expenses:gifts
+    assets:checking:gifts  *-1
+    assets:checking         *1
+
+2017/12/1
+  expenses:food    $10
+  assets:checking
+
+2017/12/14
+  expenses:gifts   $20
+  assets:checking
+
+$ hledger print --auto
+2017-12-01
+    expenses:food              $10
+    assets:checking
+    (liabilities:charity)      $-1
+
+2017-12-14
+    expenses:gifts             $20
+    assets:checking
+    assets:checking:gifts     -$20
+    assets:checking            $20
+
+* Menu:
+
+* Auto postings and dates::
+* Auto postings and transaction balancing / inferred amounts / balance assertions::
+* Auto posting tags::
+
+
+File: hledger_journal.info,  Node: Auto postings and dates,  Next: Auto postings and transaction balancing / inferred amounts / balance assertions,  Up: Auto postings / transaction modifiers
+
+1.14.1 Auto postings and dates
+------------------------------
+
+A posting date (or secondary date) in the matched posting, or (taking
+precedence) a posting date in the auto posting rule itself, will also be
+used in the generated posting.
+
+
+File: hledger_journal.info,  Node: Auto postings and transaction balancing / inferred amounts / balance assertions,  Next: Auto posting tags,  Prev: Auto postings and dates,  Up: Auto postings / transaction modifiers
+
+1.14.2 Auto postings and transaction balancing / inferred amounts /
+-------------------------------------------------------------------
+
+balance assertions Currently, transaction modifiers are applied / auto
+postings are added:
+
+   * after missing amounts are inferred, and transactions are checked
+     for balancedness,
+   * but before balance assertions are checked.
+
+   Note this means that journal entries must be balanced both before and
+after auto postings are added.  This changed in hledger 1.12+; see #893
+for background.
+
+
+File: hledger_journal.info,  Node: Auto posting tags,  Prev: Auto postings and transaction balancing / inferred amounts / balance assertions,  Up: Auto postings / transaction modifiers
+
+1.14.3 Auto posting tags
+------------------------
+
+Postings added by transaction modifiers will have some extra tags:
+
+   * 'generated-posting:= QUERY' - shows this was generated by an auto
+     posting rule, and the query
+   * '_generated-posting:= QUERY' - a hidden tag, which does not appear
+     in hledger's output.  This can be used to match postings generated
+     "just now", rather than generated in the past and saved to the
+     journal.
+
+   Also, any transaction that has been changed by transaction modifier
+rules will have these tags added:
+
+   * 'modified:' - this transaction was modified
+   * '_modified:' - a hidden tag not appearing in the comment; this
+     transaction was modified "just now".
+
+
+Tag Table:
+Node: Top76
+Node: Transactions1869
+Ref: #transactions1961
+Node: Dates3150
+Ref: #dates3249
+Node: Simple dates3314
+Ref: #simple-dates3440
+Node: Secondary dates3949
+Ref: #secondary-dates4103
+Node: Posting dates5439
+Ref: #posting-dates5568
+Node: Status6940
+Ref: #status7061
+Node: Description8769
+Ref: #description8903
+Node: Payee and note9223
+Ref: #payee-and-note9337
+Node: Comments9672
+Ref: #comments9798
+Node: Tags10992
+Ref: #tags11107
+Node: Postings12500
+Ref: #postings12628
+Node: Virtual Postings13654
+Ref: #virtual-postings13771
+Node: Account names15076
+Ref: #account-names15217
+Node: Amounts15704
+Ref: #amounts15843
+Node: Digit group marks16775
+Ref: #digit-group-marks16923
+Node: Amount display style17861
+Ref: #amount-display-style18015
+Node: Transaction prices19176
+Ref: #transaction-prices19342
+Node: Balance Assertions21608
+Ref: #balance-assertions21788
+Node: Assertions and ordering22821
+Ref: #assertions-and-ordering23009
+Node: Assertions and included files23709
+Ref: #assertions-and-included-files23952
+Node: Assertions and multiple -f options24285
+Ref: #assertions-and-multiple--f-options24541
+Node: Assertions and commodities24673
+Ref: #assertions-and-commodities24905
+Node: Assertions and prices26062
+Ref: #assertions-and-prices26276
+Node: Assertions and subaccounts26716
+Ref: #assertions-and-subaccounts26945
+Node: Assertions and virtual postings27269
+Ref: #assertions-and-virtual-postings27511
+Node: Assertions and precision27653
+Ref: #assertions-and-precision27846
+Node: Balance Assignments28113
+Ref: #balance-assignments28287
+Node: Balance assignments and prices29451
+Ref: #balance-assignments-and-prices29623
+Node: Directives29847
+Ref: #directives30006
+Node: Comment blocks35654
+Ref: #comment-blocks35799
+Node: Including other files35975
+Ref: #including-other-files36155
+Node: Default year36563
+Ref: #default-year36732
+Node: Declaring commodities37139
+Ref: #declaring-commodities37322
+Node: Default commodity38995
+Ref: #default-commodity39171
+Node: Market prices40060
+Ref: #market-prices40225
+Node: Declaring accounts41066
+Ref: #declaring-accounts41242
+Node: Account comments42167
+Ref: #account-comments42330
+Node: Account subdirectives42754
+Ref: #account-subdirectives42949
+Node: Account types43262
+Ref: #account-types43446
+Node: Account display order45085
+Ref: #account-display-order45255
+Node: Rewriting accounts46406
+Ref: #rewriting-accounts46591
+Node: Basic aliases47317
+Ref: #basic-aliases47463
+Node: Regex aliases48167
+Ref: #regex-aliases48339
+Node: Combining aliases49057
+Ref: #combining-aliases49235
+Node: end aliases50511
+Ref: #end-aliases50659
+Node: Default parent account50760
+Ref: #default-parent-account50926
+Node: Periodic transactions51810
+Ref: #periodic-transactions52009
+Node: Periodic rule syntax53881
+Ref: #periodic-rule-syntax54087
+Node: Two spaces between period expression and description!54791
+Ref: #two-spaces-between-period-expression-and-description55110
+Node: Forecasting with periodic transactions55794
+Ref: #forecasting-with-periodic-transactions56099
+Node: Budgeting with periodic transactions58125
+Ref: #budgeting-with-periodic-transactions58364
+Node: Auto postings / transaction modifiers58813
+Ref: #auto-postings-transaction-modifiers59025
+Node: Auto postings and dates61521
+Ref: #auto-postings-and-dates61778
+Node: Auto postings and transaction balancing / inferred amounts / balance assertions61953
+Ref: #auto-postings-and-transaction-balancing-inferred-amounts-balance-assertions62328
+Node: Auto posting tags62706
+Ref: #auto-posting-tags62945
 
 End Tag Table
 
diff --git a/hledger_journal.txt b/hledger_journal.txt
--- a/hledger_journal.txt
+++ b/hledger_journal.txt
@@ -22,135 +22,98 @@
        ting.
 
        You can use hledger without learning any more about this file; just use
-       the add or web commands to create and update it.  Many  users,  though,
-       also  edit  the  journal  file directly with a text editor, perhaps as-
-       sisted by the helper modes for emacs or vim.
-
-       Here's an example:
-
-              ; A sample journal file. This is a comment.
-
-              2008/01/01 income             ; <- transaction's first line starts in column 0, contains date and description
-                  assets:bank:checking  $1  ; <- posting lines start with whitespace, each contains an account name
-                  income:salary        $-1  ;    followed by at least two spaces and an amount
-
-              2008/06/01 gift
-                  assets:bank:checking  $1  ; <- at least two postings in a transaction
-                  income:gifts         $-1  ; <- their amounts must balance to 0
-
-              2008/06/02 save
-                  assets:bank:saving    $1
-                  assets:bank:checking      ; <- one amount may be omitted; here $-1 is inferred
-
-              2008/06/03 eat & shop         ; <- description can be anything
-                  expenses:food         $1
-                  expenses:supplies     $1  ; <- this transaction debits two expense accounts
-                  assets:cash               ; <- $-2 inferred
-
-              2008/10/01 take a loan
-                  assets:bank:checking  $1
-                  liabilities:debts    $-1
+       the add or web or import commands to create and update it.
 
-              2008/12/31 * pay off          ; <- an optional * or ! after the date means "cleared" (or anything you want)
-                  liabilities:debts     $1
-                  assets:bank:checking
+       Many users, though, edit the journal file with a text editor, and track
+       changes  with a version control system such as git.  Editor addons such
+       as ledger-mode or hledger-mode  for  Emacs,  vim-ledger  for  Vim,  and
+       hledger-vscode for Visual Studio Code, make this easier, adding colour,
+       formatting, tab completion, and useful commands.  See Editor configura-
+       tion at hledger.org for the full list.
 
 FILE FORMAT
-   Transactions
-       Transactions are movements of  some  quantity  of  commodities  between
-       named accounts.  Each transaction is represented by a journal entry be-
-       ginning with a simple date in column 0.  This can be followed by any of
-       the following, separated by spaces:
-
-       o (optional) a status character (empty, !, or *)
-
-       o (optional)  a transaction code (any short number or text, enclosed in
-         parentheses)
-
-       o (optional) a transaction description (any remaining text until end of
-         line or a semicolon)
+       Here's  a  description  of  each part of the file format (and hledger's
+       data model).  These are mostly in the order you'll  use  them,  but  in
+       some  cases related concepts have been grouped together for easy refer-
+       ence, or linked before they are introduced, so feel free to  skip  over
+       anything that looks unnecessary right now.
 
-       o (optional)  a  transaction  comment  (any  remaining text following a
-         semicolon until end of line)
+   Transactions
+       Transactions  are the main unit of information in a journal file.  They
+       represent events, typically a movement of some quantity of  commodities
+       between two or more named accounts.
 
-       Then comes zero or more (but usually at least 2) indented lines  repre-
-       senting...
+       Each  transaction is recorded as a journal entry, beginning with a sim-
+       ple date in column 0.  This can be followed by any of the following op-
+       tional fields, separated by spaces:
 
-   Postings
-       A  posting  is an addition of some amount to, or removal of some amount
-       from, an account.  Each posting line begins with at least one space  or
-       tab (2 or 4 spaces is common), followed by:
+       o a status character (empty, !, or *)
 
-       o (optional) a status character (empty, !, or *), followed by a space
+       o a code (any short number or text, enclosed in parentheses)
 
-       o (required)  an  account  name (any text, optionally containing single
-         spaces, until end of line or a double space)
+       o a description (any remaining text until end of line or a semicolon)
 
-       o (optional) two or more spaces or tabs followed by an amount.
+       o a  comment  (any  remaining  text  following a semicolon until end of
+         line, and any following indented lines beginning with a semicolon)
 
-       Positive amounts are being added to the account, negative  amounts  are
-       being removed.
+       o 0 or more indented posting lines, describing what was transferred and
+         the accounts involved.
 
-       The amounts within a transaction must always sum up to zero.  As a con-
-       venience, one amount may be left blank; it will be inferred  so  as  to
-       balance the transaction.
+       Here's a simple journal file containing one transaction:
 
-       Be  sure  to  note the unusual two-space delimiter between account name
-       and amount.  This makes it easy to write account names containing  spa-
-       ces.   But if you accidentally leave only one space (or tab) before the
-       amount, the amount will be considered part of the account name.
+              2008/01/01 income
+                assets:bank:checking   $1
+                income:salary         $-1
 
    Dates
    Simple dates
-       Within a journal file, transaction dates use Y/M/D (or Y-M-D or  Y.M.D)
-       Leading  zeros are optional.  The year may be omitted, in which case it
-       will be inferred from the context - the current  transaction,  the  de-
-       fault  year set with a default year directive, or the current date when
-       the command is  run.   Some  examples:  2010/01/31,  1/31,  2010-01-31,
-       2010.1.31.
+       Dates  in  the  journal  file  use  simple  dates format: YYYY-MM-DD or
+       YYYY/MM/DD or YYYY.MM.DD, with leading zeros optional.  The year may be
+       omitted,  in  which case it will be inferred from the context: the cur-
+       rent transaction, the default year set with a default  year  directive,
+       or   the  current  date  when  the  command  is  run.   Some  examples:
+       2010-01-31, 2010/01/31, 2010.1.31, 1/31.
 
+       (The UI also accepts simple dates, as well as the more  flexible  smart
+       dates documented in the hledger manual.)
+
    Secondary dates
        Real-life  transactions  sometimes  involve more than one date - eg the
        date you write a cheque, and the date it clears in your bank.  When you
-       want  to model this, eg for more accurate balances, you can specify in-
-       dividual posting dates, which I recommend.  Or, you can  use  the  sec-
-       ondary  dates  (aka  auxiliary/effective  dates) feature, supported for
-       compatibility with Ledger.
+       want  to  model this, for more accurate daily balances, you can specify
+       individual posting dates.
 
-       A secondary date can be written after the primary date, separated by an
-       equals  sign.   The  primary date, on the left, is used by default; the
-       secondary date, on the right, is used when the --date2 flag  is  speci-
-       fied (--aux-date or --effective also work).
+       Or, you can use the older secondary date feature (Ledger calls it  aux-
+       iliary  date or effective date).  Note: we support this for compatibil-
+       ity, but I usually recommend avoiding this feature; posting  dates  are
+       almost always clearer and simpler.
 
-       The  meaning of secondary dates is up to you, but it's best to follow a
-       consistent rule.  Eg write the bank's clearing  date  as  primary,  and
-       when needed, the date the transaction was initiated as secondary.
+       A secondary date is written after the primary date, following an equals
+       sign.  If the year is omitted, the  primary  date's  year  is  assumed.
+       When  running  reports, the primary (left) date is used by default, but
+       with the --date2 flag (or --aux-date  or  --effective),  the  secondary
+       (right) date will be used instead.
 
-       Here's an example.  Note that a secondary date will use the year of the
-       primary date if unspecified.
+       The  meaning of secondary dates is up to you, but it's best to follow a
+       consistent rule.  Eg "primary = the bank's clearing date,  secondary  =
+       date the transaction was initiated, if different", as shown here:
 
               2010/2/23=2/19 movie ticket
                 expenses:cinema                   $10
                 assets:checking
 
               $ hledger register checking
-              2010/02/23 movie ticket         assets:checking                $-10         $-10
+              2010-02-23 movie ticket         assets:checking                $-10         $-10
 
               $ hledger register checking --date2
-              2010/02/19 movie ticket         assets:checking                $-10         $-10
-
-       Secondary dates require some effort; you must use them consistently  in
-       your journal entries and remember whether to use or not use the --date2
-       flag for your reports.  They are included in hledger for Ledger compat-
-       ibility,  but  posting dates are a more powerful and less confusing al-
-       ternative.
+              2010-02-19 movie ticket         assets:checking                $-10         $-10
 
    Posting dates
-       You can give individual postings a different  date  from  their  parent
-       transaction,  by  adding a posting comment containing a tag (see below)
+       You  can  give  individual  postings a different date from their parent
+       transaction, by adding a posting comment containing a tag  (see  below)
        like date:DATE.  This is probably the best way to control posting dates
-       precisely.   Eg  in  this  example the expense should appear in May re-
-       ports, and the deduction from checking should be reported  on  6/1  for
+       precisely.  Eg in this example the expense should  appear  in  May  re-
+       ports,  and  the  deduction from checking should be reported on 6/1 for
        easy bank reconciliation:
 
               2015/5/30
@@ -158,27 +121,27 @@
                   assets:checking        ; bank cleared it on monday, date:6/1
 
               $ hledger -f t.j register food
-              2015/05/30                      expenses:food                  $10           $10
+              2015-05-30                      expenses:food                  $10           $10
 
               $ hledger -f t.j register checking
-              2015/06/01                      assets:checking               $-10          $-10
+              2015-06-01                      assets:checking               $-10          $-10
 
-       DATE  should be a simple date; if the year is not specified it will use
-       the year of the transaction's date.  You can  set  the  secondary  date
-       similarly,  with  date2:DATE2.   The  date:  or date2: tags must have a
-       valid simple date value if they are present, eg a  date:  tag  with  no
+       DATE should be a simple date; if the year is not specified it will  use
+       the  year  of  the  transaction's date.  You can set the secondary date
+       similarly, with date2:DATE2.  The date: or  date2:  tags  must  have  a
+       valid  simple  date  value  if they are present, eg a date: tag with no
        value is not allowed.
 
        Ledger's earlier, more compact bracketed date syntax is also supported:
-       [DATE], [DATE=DATE2] or [=DATE2].  hledger will attempt  to  parse  any
+       [DATE],  [DATE=DATE2]  or  [=DATE2].  hledger will attempt to parse any
        square-bracketed sequence of the 0123456789/-.= characters in this way.
-       With this syntax, DATE infers its year from the transaction  and  DATE2
+       With  this  syntax, DATE infers its year from the transaction and DATE2
        infers its year from DATE.
 
    Status
-       Transactions,  or  individual postings within a transaction, can have a
-       status mark, which is a single character  before  the  transaction  de-
-       scription  or posting account name, separated from it by a space, indi-
+       Transactions, or individual postings within a transaction, can  have  a
+       status  mark,  which  is  a single character before the transaction de-
+       scription or posting account name, separated from it by a space,  indi-
        cating one of three statuses:
 
        mark     status
@@ -187,23 +150,23 @@
        !        pending
        *        cleared
 
-       When reporting, you  can  filter  by  status  with  the  -U/--unmarked,
-       -P/--pending,  and  -C/--cleared  flags;  or the status:, status:!, and
+       When  reporting,  you  can  filter  by  status  with the -U/--unmarked,
+       -P/--pending, and -C/--cleared flags; or  the  status:,  status:!,  and
        status:* queries; or the U, P, C keys in hledger-ui.
 
-       Note, in Ledger and in older versions of hledger, the "unmarked"  state
-       is  called  "uncleared".   As  of hledger 1.3 we have renamed it to un-
+       Note,  in Ledger and in older versions of hledger, the "unmarked" state
+       is called "uncleared".  As of hledger 1.3 we have  renamed  it  to  un-
        marked for clarity.
 
-       To replicate Ledger and old hledger's behaviour of also matching  pend-
+       To  replicate Ledger and old hledger's behaviour of also matching pend-
        ing, combine -U and -P.
 
-       Status  marks  are optional, but can be helpful eg for reconciling with
+       Status marks are optional, but can be helpful eg for  reconciling  with
        real-world accounts.  Some editor modes provide highlighting and short-
-       cuts  for working with status.  Eg in Emacs ledger-mode, you can toggle
+       cuts for working with status.  Eg in Emacs ledger-mode, you can  toggle
        transaction status with C-c C-e, or posting status with C-c C-c.
 
-       What "uncleared", "pending", and "cleared" actually mean is up to  you.
+       What  "uncleared", "pending", and "cleared" actually mean is up to you.
        Here's one suggestion:
 
        status       meaning
@@ -214,25 +177,154 @@
        cleared      complete, reconciled as far as possible, and considered cor-
                     rect
 
-       With  this scheme, you would use -PC to see the current balance at your
+       With this scheme, you would use -PC to see the current balance at  your
        bank, -U to see things which will probably hit your bank soon (like un-
-       cashed  checks),  and no flags to see the most up-to-date state of your
+       cashed checks), and no flags to see the most up-to-date state  of  your
        finances.
 
    Description
-       A transaction's description is the rest of the line following the  date
-       and  status  mark  (or  until  a comment begins).  Sometimes called the
+       A  transaction's description is the rest of the line following the date
+       and status mark (or until a  comment  begins).   Sometimes  called  the
        "narration" in traditional bookkeeping, it can be used for whatever you
-       wish,  or  left blank.  Transaction descriptions can be queried, unlike
+       wish, or left blank.  Transaction descriptions can be  queried,  unlike
        comments.
 
    Payee and note
        You can optionally include a | (pipe) character in descriptions to sub-
        divide the description into separate fields for payee/payer name on the
        left (up to the first |) and an additional note field on the right (af-
-       ter  the  first |).  This may be worthwhile if you need to do more pre-
+       ter the first |).  This may be worthwhile if you need to do  more  pre-
        cise querying and pivoting by payee or by note.
 
+   Comments
+       Lines in the journal beginning with a semicolon (;) or hash (#) or star
+       (*) are comments, and will be ignored.  (Star comments  cause  org-mode
+       nodes  to  be  ignored, allowing emacs users to fold and navigate their
+       journals with org-mode or orgstruct-mode.)
+
+       You can attach comments to a transaction by writing them after the  de-
+       scription and/or indented on the following lines (before the postings).
+       Similarly, you can attach comments to an individual posting by  writing
+       them after the amount and/or indented on the following lines.  Transac-
+       tion and posting comments must begin with a semicolon (;).
+
+       Some examples:
+
+              # a file comment
+              ; another file comment
+              * also a file comment, useful in org/orgstruct mode
+
+              comment
+              A multiline file comment, which continues
+              until a line containing just "end comment"
+              (or end of file).
+              end comment
+
+              2012/5/14 something  ; a transaction comment
+                  ; the transaction comment, continued
+                  posting1  1  ; a comment for posting 1
+                  posting2
+                  ; a comment for posting 2
+                  ; another comment line for posting 2
+              ; a file comment (because not indented)
+
+       You can also comment larger regions of a file  using  comment  and  end
+       comment directives.
+
+   Tags
+       Tags  are  a  way  to add extra labels or labelled data to postings and
+       transactions, which you can then search or pivot on.
+
+       A simple tag is a word (which may contain hyphens) followed by  a  full
+       colon, written inside a transaction or posting comment line:
+
+              2017/1/16 bought groceries  ; sometag:
+
+       Tags  can  have  a  value, which is the text after the colon, up to the
+       next comma or end of line, with leading/trailing whitespace removed:
+
+                  expenses:food    $10 ; a-posting-tag: the tag value
+
+       Note this means hledger's tag values can not  contain  commas  or  new-
+       lines.  Ending at commas means you can write multiple short tags on one
+       line, comma separated:
+
+                  assets:checking  ; a comment containing tag1:, tag2: some value ...
+
+       Here,
+
+       o "a comment containing" is just comment text, not a tag
+
+       o "tag1" is a tag with no value
+
+       o "tag2" is another tag, whose value is "some value ..."
+
+       Tags in a transaction comment affect the transaction  and  all  of  its
+       postings,  while  tags  in  a posting comment affect only that posting.
+       For example, the following transaction has three tags (A, TAG2,  third-
+       tag) and the posting has four (those plus posting-tag):
+
+              1/1 a transaction  ; A:, TAG2:
+                  ; third-tag: a third transaction tag, <- with a value
+                  (a)  $1  ; posting-tag:
+
+       Tags  are  like  Ledger's metadata feature, except hledger's tag values
+       are simple strings.
+
+   Postings
+       A posting is an addition of some amount to, or removal of  some  amount
+       from,  an account.  Each posting line begins with at least one space or
+       tab (2 or 4 spaces is common), followed by:
+
+       o (optional) a status character (empty, !, or *), followed by a space
+
+       o (required) an account name (any text,  optionally  containing  single
+         spaces, until end of line or a double space)
+
+       o (optional) two or more spaces or tabs followed by an amount.
+
+       Positive  amounts  are being added to the account, negative amounts are
+       being removed.
+
+       The amounts within a transaction must always sum up to zero.  As a con-
+       venience,  one  amount  may be left blank; it will be inferred so as to
+       balance the transaction.
+
+       Be sure to note the unusual two-space delimiter  between  account  name
+       and  amount.  This makes it easy to write account names containing spa-
+       ces.  But if you accidentally leave only one space (or tab) before  the
+       amount, the amount will be considered part of the account name.
+
+   Virtual Postings
+       A posting with a parenthesised account name is called a virtual posting
+       or unbalanced posting, which means it is exempt  from  the  usual  rule
+       that a transaction's postings must balance add up to zero.
+
+       This  is  not  part  of double entry accounting, so you might choose to
+       avoid this feature.  Or you can use it sparingly  for  certain  special
+       cases  where  it can be convenient.  Eg, you could set opening balances
+       without using a balancing equity account:
+
+              1/1 opening balances
+                (assets:checking)   $1000
+                (assets:savings)    $2000
+
+       A posting with a bracketed account name is called  a  balanced  virtual
+       posting.  The balanced virtual postings in a transaction must add up to
+       zero (separately from other postings).  Eg:
+
+              1/1 buy food with cash, update budget envelope subaccounts, & something else
+                assets:cash                    $-10 ; <- these balance
+                expenses:food                    $7 ; <-
+                expenses:food                    $3 ; <-
+                [assets:checking:budget:food]  $-10    ; <- and these balance
+                [assets:checking:available]     $10    ; <-
+                (something:else)                 $5       ; <- not required to balance
+
+       Ordinary non-parenthesised,  non-bracketed  postings  are  called  real
+       postings.   You  can  exclude  virtual  postings  from reports with the
+       -R/--real flag or real:1 query.
+
    Account names
        Account names typically have several parts separated by a  full  colon,
        from  which hledger derives a hierarchical chart of accounts.  They can
@@ -311,13 +403,13 @@
               commodity INR 9,99,99,999.00
               commodity       1 000 000.9455
 
-   Amount display format
+   Amount display style
        For each commodity, hledger chooses a consistent  format  to  use  when
        displaying  amounts.  (Except price amounts, which are always displayed
-       as written).  The display format is chosen as follows:
+       as written).  The display style is chosen as follows:
 
-       o If there is a commodity directive for the commodity, that  format  is
-         used (see examples above).
+       o If there is a commodity directive (or  default  commodity  directive)
+         for the commodity, that format is used (see examples above).
 
        o Otherwise  the  format  of the first posting amount in that commodity
          seen in the journal is used.  But the number of decimal places ("pre-
@@ -327,48 +419,77 @@
        o Or if there are no such amounts in the journal, a default  format  is
          used (like $1000.00).
 
-       Price amounts, and amounts in D directives don't affect the amount dis-
-       play format directly, but occasionally they can do so indirectly.   (Eg
-       when  D's  default  commodity is applied to a commodity-less amount, or
-       when an amountless posting is balanced using a  price's  commodity,  or
-       when  -V  is  used.) If you find this causing problems, use a commodity
-       directive to set the display format.
+       Transaction  prices don't affect the amount display style directly, but
+       occasionally they can do so indirectly (eg when an posting's amount  is
+       inferred  using  a  transaction price).  If you find this causing prob-
+       lems, use a commodity directive to fix the display style.
 
-   Virtual Postings
-       When you parenthesise the account name in a posting,  we  call  that  a
-       virtual posting, which means:
+       In summary: amounts will be displayed much as they appear in your jour-
+       nal,  with  the  max observed number of decimal places.  If you want to
+       see fewer decimal places in reports, use a commodity directive to over-
+       ride that.
 
-       o it is ignored when checking that the transaction is balanced
+   Transaction prices
+       Within a transaction, you can note an amount's price in another commod-
+       ity.  This can be used to document the cost (in a purchase) or  selling
+       price  (in  a  sale).   For  example,  transaction prices are useful to
+       record purchases of a foreign currency.  Note  transaction  prices  are
+       fixed at the time of the transaction, and do not change over time.  See
+       also market prices, which represent prevailing exchange rates on a cer-
+       tain date.
 
-       o it  is  excluded from reports when the --real/-R flag is used, or the
-         real:1 query.
+       There are several ways to record a transaction price:
 
-       You could use this, eg, to set an  account's  opening  balance  without
-       needing to use the equity:opening balances account:
+       1. Write the price per unit, as @ UNITPRICE after the amount:
 
-              1/1 special unbalanced posting to set initial balance
-                (assets:checking)   $1000
+                  2009/1/1
+                    assets:euros     EUR100 @ $1.35  ; one hundred euros purchased at $1.35 each
+                    assets:dollars                 ; balancing amount is -$135.00
 
-       When the account name is bracketed, we call it a balanced virtual post-
-       ing.  This is like an ordinary virtual posting except the balanced vir-
-       tual  postings  in a transaction must balance to 0, like the real post-
-       ings (but separately from them).  Balanced virtual  postings  are  also
-       excluded by --real/-R or real:1.
+       2. Write the total price, as @@ TOTALPRICE after the amount:
 
-              1/1 buy food with cash, and update some budget-tracking subaccounts elsewhere
-                expenses:food                   $10
-                assets:cash                    $-10
-                [assets:checking:available]     $10
-                [assets:checking:budget:food]  $-10
+                  2009/1/1
+                    assets:euros     EUR100 @@ $135  ; one hundred euros purchased at $135 for the lot
+                    assets:dollars
 
-       Virtual postings have some legitimate uses, but those are few.  You can
-       usually find an equivalent journal entry using real postings, which  is
-       more correct and provides better error checking.
+       3. Specify amounts for all postings, using exactly two commodities, and
+          let hledger infer the price that balances the transaction:
 
+                  2009/1/1
+                    assets:euros     EUR100          ; one hundred euros purchased
+                    assets:dollars  $-135          ; for $135
+
+       (Ledger users: Ledger uses a different syntax for fixed prices, {=UNIT-
+       PRICE}, which hledger currently ignores).
+
+       Use  the -B/--cost flag to convert amounts to their transaction price's
+       commodity, if any.  (mnemonic: "B" is from "cost Basis", as in Ledger).
+       Eg here is how -B affects the balance report for the example above:
+
+              $ hledger bal -N --flat
+                             $-135  assets:dollars
+                              EUR100  assets:euros
+              $ hledger bal -N --flat -B
+                             $-135  assets:dollars
+                              $135  assets:euros    # <- the euros' cost
+
+       Note  -B is sensitive to the order of postings when a transaction price
+       is inferred: the inferred price will be in the commodity  of  the  last
+       amount.  So if example 3's postings are reversed, while the transaction
+       is equivalent, -B shows something different:
+
+              2009/1/1
+                assets:dollars  $-135              ; 135 dollars sold
+                assets:euros     EUR100              ; for 100 euros
+
+              $ hledger bal -N --flat -B
+                             EUR-100  assets:dollars  # <- the dollars' selling price
+                              EUR100  assets:euros
+
    Balance Assertions
-       hledger  supports  Ledger-style  balance  assertions  in journal files.
-       These look like, for example, = EXPECTEDBALANCE following  a  posting's
-       amount.   Eg  here  we assert the expected dollar balance in accounts a
+       hledger supports Ledger-style  balance  assertions  in  journal  files.
+       These  look  like, for example, = EXPECTEDBALANCE following a posting's
+       amount.  Eg here we assert the expected dollar balance  in  accounts  a
        and b after each posting:
 
               2013/1/1
@@ -380,11 +501,12 @@
                 b  $-1  =$-2
 
        After reading a journal file, hledger will check all balance assertions
-       and  report  an error if any of them fail.  Balance assertions can pro-
-       tect you from, eg, inadvertently disrupting reconciled  balances  while
-       cleaning  up  old  entries.   You can disable them temporarily with the
+       and report an error if any of them fail.  Balance assertions  can  pro-
+       tect  you  from, eg, inadvertently disrupting reconciled balances while
+       cleaning up old entries.  You can disable  them  temporarily  with  the
        -I/--ignore-assertions flag, which can be useful for troubleshooting or
-       for reading Ledger files.
+       for reading Ledger files.  (Note: this flag currently does not  disable
+       balance assignments, below).
 
    Assertions and ordering
        hledger  sorts  an  account's postings and assertions first by date and
@@ -414,9 +536,8 @@
    Assertions and commodities
        The  asserted  balance must be a simple single-commodity amount, and in
        fact the assertion checks only  this  commodity's  balance  within  the
-       (possibly multi-commodity) account balance.
-       This is how assertions work in Ledger also.  We could call this a "par-
-       tial" balance assertion.
+       (possibly  multi-commodity)  account  balance.   This is how assertions
+       work in Ledger also.  We could call this a "partial" balance assertion.
 
        To assert the balance of more than one commodity in an account, you can
        write multiple postings, each asserting one commodity's balance.
@@ -524,142 +645,9 @@
                 (a)             = $1 @ EUR2
 
               $ hledger print --explicit
-              2019/01/01
+              2019-01-01
                   (a)         $1 @ EUR2 = $1 @ EUR2
 
-   Transaction prices
-       Within a transaction, you can note an amount's price in another commod-
-       ity.   This can be used to document the cost (in a purchase) or selling
-       price (in a sale).  For  example,  transaction  prices  are  useful  to
-       record  purchases  of  a foreign currency.  Note transaction prices are
-       fixed at the time of the transaction, and do not change over time.  See
-       also market prices, which represent prevailing exchange rates on a cer-
-       tain date.
-
-       There are several ways to record a transaction price:
-
-       1. Write the price per unit, as @ UNITPRICE after the amount:
-
-                  2009/1/1
-                    assets:euros     EUR100 @ $1.35  ; one hundred euros purchased at $1.35 each
-                    assets:dollars                 ; balancing amount is -$135.00
-
-       2. Write the total price, as @@ TOTALPRICE after the amount:
-
-                  2009/1/1
-                    assets:euros     EUR100 @@ $135  ; one hundred euros purchased at $135 for the lot
-                    assets:dollars
-
-       3. Specify amounts for all postings, using exactly two commodities, and
-          let hledger infer the price that balances the transaction:
-
-                  2009/1/1
-                    assets:euros     EUR100          ; one hundred euros purchased
-                    assets:dollars  $-135          ; for $135
-
-       (Ledger users: Ledger uses a different syntax for fixed prices, {=UNIT-
-       PRICE}, which hledger currently ignores).
-
-       Use the -B/--cost flag to convert amounts to their transaction  price's
-       commodity, if any.  (mnemonic: "B" is from "cost Basis", as in Ledger).
-       Eg here is how -B affects the balance report for the example above:
-
-              $ hledger bal -N --flat
-                             $-135  assets:dollars
-                              EUR100  assets:euros
-              $ hledger bal -N --flat -B
-                             $-135  assets:dollars
-                              $135  assets:euros    # <- the euros' cost
-
-       Note -B is sensitive to the order of postings when a transaction  price
-       is  inferred:  the  inferred price will be in the commodity of the last
-       amount.  So if example 3's postings are reversed, while the transaction
-       is equivalent, -B shows something different:
-
-              2009/1/1
-                assets:dollars  $-135              ; 135 dollars sold
-                assets:euros     EUR100              ; for 100 euros
-
-              $ hledger bal -N --flat -B
-                             EUR-100  assets:dollars  # <- the dollars' selling price
-                              EUR100  assets:euros
-
-   Comments
-       Lines in the journal beginning with a semicolon (;) or hash (#) or star
-       (*) are comments, and will be ignored.  (Star comments  cause  org-mode
-       nodes  to  be  ignored, allowing emacs users to fold and navigate their
-       journals with org-mode or orgstruct-mode.)
-
-       You can attach comments to a transaction by writing them after the  de-
-       scription and/or indented on the following lines (before the postings).
-       Similarly, you can attach comments to an individual posting by  writing
-       them after the amount and/or indented on the following lines.  Transac-
-       tion and posting comments must begin with a semicolon (;).
-
-       Some examples:
-
-              # a file comment
-
-              ; also a file comment
-
-              comment
-              This is a multiline file comment,
-              which continues until a line
-              where the "end comment" string
-              appears on its own (or end of file).
-              end comment
-
-              2012/5/14 something  ; a transaction comment
-                  ; the transaction comment, continued
-                  posting1  1  ; a comment for posting 1
-                  posting2
-                  ; a comment for posting 2
-                  ; another comment line for posting 2
-              ; a file comment (because not indented)
-
-       You can also comment larger regions of a file  using  comment  and  end
-       comment directives.
-
-   Tags
-       Tags  are  a  way  to add extra labels or labelled data to postings and
-       transactions, which you can then search or pivot on.
-
-       A simple tag is a word (which may contain hyphens) followed by  a  full
-       colon, written inside a transaction or posting comment line:
-
-              2017/1/16 bought groceries  ; sometag:
-
-       Tags  can  have  a  value, which is the text after the colon, up to the
-       next comma or end of line, with leading/trailing whitespace removed:
-
-                  expenses:food    $10 ; a-posting-tag: the tag value
-
-       Note this means hledger's tag values can not  contain  commas  or  new-
-       lines.  Ending at commas means you can write multiple short tags on one
-       line, comma separated:
-
-                  assets:checking  ; a comment containing tag1:, tag2: some value ...
-
-       Here,
-
-       o "a comment containing" is just comment text, not a tag
-
-       o "tag1" is a tag with no value
-
-       o "tag2" is another tag, whose value is "some value ..."
-
-       Tags in a transaction comment affect the transaction  and  all  of  its
-       postings,  while  tags  in  a posting comment affect only that posting.
-       For example, the following transaction has three tags (A, TAG2,  third-
-       tag) and the posting has four (those plus posting-tag):
-
-              1/1 a transaction  ; A:, TAG2:
-                  ; third-tag: a third transaction tag, <- with a value
-                  (a)  $1  ; posting-tag:
-
-       Tags  are  like  Ledger's metadata feature, except hledger's tag values
-       are simple strings.
-
    Directives
        A directive is a line in the journal beginning with a special  keyword,
        that influences how the journal is processed.  hledger's directives are
@@ -699,44 +687,47 @@
                                                                        play style: amounts
                                                                        of  that  commodity
                                                                        in reports
-       D                                declare  a commodity, number   commodity: all com-
-                                        notation & display style for   modityless  entries
-                                        commodityless amounts          in  all files; num-
-                                                                       ber notation:  fol-
-                                                                       lowing   commodity-
-                                                                       less  entries   and
-                                                                       entries   in   that
-                                                                       commodity  in   all
-                                                                       files;      display
-                                                                       style:  amounts  of
-                                                                       that  commodity  in
-                                                                       reports
+       D                                declare  a  commodity  to be   default  commodity:
+                                        used    for    commodityless   following   commod-
+                                        amounts,  and its number no-   ityless entries un-
+                                        tation & display style         til  end of current
+                                                                       file; number  nota-
+                                                                       tion: following en-
+                                                                       tries in that  com-
+                                                                       modity until end of
+                                                                       current file;  dis-
+                                                                       play style: amounts
+                                                                       of  that  commodity
+                                                                       in reports
        include                          include   entries/directives   what  the  included
                                         from another file              directives affect
        P                                declare a market price for a   amounts   of   that
-                                        commodity                      commodity  in   re-
-                                                                       ports,  when  -V is
+                                        commodity                      commodity   in  re-
+                                                                       ports, when  -V  is
                                                                        used
-       Y                                declare a year for  yearless   following       in-
+       Y                                declare  a year for yearless   following       in-
                                         dates                          line/included   en-
-                                                                       tries  until end of
+                                                                       tries until end  of
                                                                        current file
 
        And some definitions:
 
-       subdirec-   optional indented directive line immediately following a par-
-       tive        ent directive
-       number      how  to  interpret  numbers when parsing journal entries (the
-       notation    identity of the  decimal  separator  character).   (Currently
-                   each  commodity  can  have its own notation, even in the same
-                   file.)
-       display     how to display amounts of a commodity in reports (symbol side
-       style       and spacing, digit groups, decimal separator, decimal places)
+       subdi-   optional  indented directive line immediately following a parent
+       rec-     directive
+       tive
+       number   how to interpret numbers when parsing journal entries (the iden-
+       nota-    tity  of the decimal separator character).  (Currently each com-
+       tion     modity can have its own notation, even in the same file.)
+       dis-     how to display amounts of a commodity in  reports  (symbol  side
+       play     and spacing, digit groups, decimal separator, decimal places)
+       style
 
 
-       directive   which entries and (when there are multiple files) which files
-       scope       are affected by a directive
 
+       direc-   which  entries  and  (when there are multiple files) which files
+       tive     are affected by a directive
+       scope
+
        As you can see, directives vary in which journal entries and files they
        affect, and whether they are focussed on input (parsing) or output (re-
        ports).  Some directives have multiple effects.
@@ -793,15 +784,16 @@
        1. It  declares  commodities which may be used in the journal.  This is
           currently not enforced, but can serve as documentation.
 
-       2. It declares what decimal mark character to expect when parsing input
-          -  useful to disambiguate international number formats in your data.
-          (Without this, hledger will parse both 1,000 and 1.000 as 1).
+       2. It declares what decimal mark character (period or comma) to  expect
+          when  parsing  input  -  useful to disambiguate international number
+          formats in your data.  (Without this, hledger will parse both  1,000
+          and 1.000 as 1).
 
-       3. It declares the amount display format to use in output - decimal and
+       3. It  declares the amount display style to use in output - decimal and
           digit group marks, number of decimal places, symbol placement etc.
 
-       You  are likely to run into one of the problems solved by commodity di-
-       rectives, sooner or later, so it's a good idea to just always use  them
+       You are likely to run into one of the problems solved by commodity  di-
+       rectives,  sooner or later, so it's a good idea to just always use them
        to declare your commodities.
 
        A commodity directive is just the word commodity followed by an amount.
@@ -814,8 +806,8 @@
               ; separating thousands with comma.
               commodity 1,000.0000 AAAA
 
-       or on multiple lines, using the "format" subdirective.  (In  this  case
-       the  commodity  symbol  appears  twice  and  should be the same in both
+       or  on  multiple lines, using the "format" subdirective.  (In this case
+       the commodity symbol appears twice and  should  be  the  same  in  both
        places.):
 
               ; commodity SYMBOL
@@ -828,31 +820,35 @@
                 format INR 1,00,00,000.00
 
        The quantity of the amount does not matter; only the format is signifi-
-       cant.   The  number  must  include a decimal mark: either a period or a
+       cant.  The number must include a decimal mark: either  a  period  or  a
        comma, followed by 0 or more decimal digits.
 
    Default commodity
-       The D directive sets a default commodity (and display  format),  to  be
-       used for amounts without a commodity symbol (ie, plain numbers).  (Note
-       this differs from Ledger's default commodity directive.) The  commodity
-       and  display  format  will  be applied to all subsequent commodity-less
-       amounts, or until the next D directive.
+       The  D directive sets a default commodity, to be used for amounts with-
+       out a commodity symbol (ie, plain numbers).  This commodity will be ap-
+       plied to all subsequent commodity-less amounts, or until the next D di-
+       rective.  (Note, this is different from Ledger's D.)
 
+       For compatibility/historical reasons, D also acts like a commodity  di-
+       rective, setting the commodity's display style (for output) and decimal
+       mark (for parsing input).  As with commodity, the amount must always be
+       written  with a decimal mark (period or comma).  If both directives are
+       used, commodity's style takes precedence.
+
+       The syntax is D AMOUNT.  Eg:
+
               ; commodity-less amounts should be treated as dollars
-              ; (and displayed with symbol on the left, thousands separators and two decimal places)
+              ; (and displayed with the dollar sign on the left, thousands separators and two decimal places)
               D $1,000.00
 
               1/1
-                a     5  ; <- commodity-less amount, becomes $1
+                a     5  ; <- commodity-less amount, parsed as $5 and displayed as $5.00
                 b
 
-       As with the commodity directive, the amount must always be written with
-       a decimal point.
-
    Market prices
-       The  P directive declares a market price, which is an exchange rate be-
-       tween two commodities on a certain date.  (In Ledger, they  are  called
-       "historical  prices".)  These are often obtained from a stock exchange,
+       The P directive declares a market price, which is an exchange rate  be-
+       tween  two  commodities on a certain date.  (In Ledger, they are called
+       "historical prices".) These are often obtained from a  stock  exchange,
        cryptocurrency exchange, or the foreign exchange market.
 
        Here is the format:
@@ -863,16 +859,16 @@
 
        o COMMODITYA is the symbol of the commodity being priced
 
-       o COMMODITYBAMOUNT is an amount (symbol and quantity) in a second  com-
+       o COMMODITYBAMOUNT  is an amount (symbol and quantity) in a second com-
          modity, giving the price in commodity B of one unit of commodity A.
 
-       These  two  market price directives say that one euro was worth 1.35 US
+       These two market price directives say that one euro was worth  1.35  US
        dollars during 2009, and $1.40 from 2010 onward:
 
               P 2009/1/1 EUR $1.35
               P 2010/1/1 EUR $1.40
 
-       The -V/--value flag can be used to convert reported amounts to  another
+       The  -V/--value flag can be used to convert reported amounts to another
        commodity using these prices.
 
    Declaring accounts
@@ -882,38 +878,42 @@
        o They can document your intended chart of accounts, providing a refer-
          ence.
 
-       o They  can  store  extra  information about accounts (account numbers,
+       o They can store extra information  about  accounts  (account  numbers,
          notes, etc.)
 
-       o They can help hledger know your accounts'  types  (asset,  liability,
-         equity,  revenue,  expense), useful for reports like balancesheet and
+       o They  can  help  hledger know your accounts' types (asset, liability,
+         equity, revenue, expense), useful for reports like  balancesheet  and
          incomestatement.
 
-       o They control account display order in  reports,  allowing  non-alpha-
+       o They  control  account  display order in reports, allowing non-alpha-
          betic sorting (eg Revenues to appear above Expenses).
 
-       o They  help  with account name completion in the add command, hledger-
+       o They help with account name completion in the add  command,  hledger-
          iadd, hledger-web, ledger-mode etc.
 
-       The simplest form is just the word account followed by a  hledger-style
+       The  simplest form is just the word account followed by a hledger-style
        account name, eg:
 
               account assets:bank:checking
 
    Account comments
-       Comments, beginning with a semicolon, optionally including tags, can be
-       written after the account name, and/or on following lines.  Eg:
+       Comments, beginning with a semicolon, can be added:
 
-              account assets:bank:checking  ; a comment
-                ; another comment
-                ; acctno:12345, a tag
+       o on the same line, after two or more spaces (because ; is  allowed  in
+         account names)
 
-       Tip: comments on the same line require hledger 1.12+.  If you need your
-       journal to be compatible with older hledger versions, write comments on
-       the next line instead.
+       o on the next lines, indented
 
+       An example of both:
+
+              account assets:bank:checking  ; same-line comment, note 2+ spaces before ;
+                ; next-line comment
+                ; another with tag, acctno:12345 (not used yet)
+
+       Same-line comments are not supported by Ledger, or hledger <1.13.
+
    Account subdirectives
-       We also allow (and ignore) Ledger-style  indented  subdirectives,  just
+       We  also  allow  (and ignore) Ledger-style indented subdirectives, just
        for compatibility.:
 
               account assets:bank:checking
@@ -926,18 +926,18 @@
                 [LEDGER-STYLE SUBDIRECTIVES, IGNORED]
 
    Account types
-       hledger  recognises  five types (or classes) of account: Asset, Liabil-
-       ity, Equity, Revenue, Expense.  This is used by a few  accounting-aware
+       hledger recognises five types (or classes) of account:  Asset,  Liabil-
+       ity,  Equity, Revenue, Expense.  This is used by a few accounting-aware
        reports such as balancesheet, incomestatement and cashflow.
 
    Auto-detected account types
        If you name your top-level accounts with some variation of assets, lia-
-       bilities/debts, equity, revenues/income, or expenses, their  types  are
+       bilities/debts,  equity,  revenues/income, or expenses, their types are
        detected automatically.
 
    Account types declared with tags
-       More  generally,  you can declare an account's type with an account di-
-       rective, by writing a type: tag in a comment, followed by  one  of  the
+       More generally, you can declare an account's type with an  account  di-
+       rective,  by  writing  a type: tag in a comment, followed by one of the
        words Asset, Liability, Equity, Revenue, Expense, or one of the letters
        ALERX (case insensitive):
 
@@ -945,11 +945,11 @@
               account liabilities  ; type:Liability
               account equity       ; type:Equity
               account revenues     ; type:Revenue
-              account expenses     ; type:Expenses
+              account expenses     ; type:Expense
 
    Account types declared with account type codes
-       Or, you can write one of those letters separated from the account  name
-       by  two  or  more spaces, but this should probably be considered depre-
+       Or,  you can write one of those letters separated from the account name
+       by two or more spaces, but this should probably  be  considered  depre-
        cated as of hledger 1.13:
 
               account assets       A
@@ -959,7 +959,7 @@
               account expenses     X
 
    Overriding auto-detected types
-       If you ever override the types of those auto-detected  english  account
+       If  you  ever override the types of those auto-detected english account
        names mentioned above, you might need to help the reports a bit.  Eg:
 
               ; make "liabilities" not have the liability type - who knows why
@@ -970,8 +970,8 @@
               account -            ; type:L
 
    Account display order
-       Account  directives also set the order in which accounts are displayed,
-       eg in reports, the hledger-ui  accounts  screen,  and  the  hledger-web
+       Account directives also set the order in which accounts are  displayed,
+       eg  in  reports,  the  hledger-ui  accounts screen, and the hledger-web
        sidebar.  By default accounts are listed in alphabetical order.  But if
        you have these account directives in the journal:
 
@@ -993,19 +993,22 @@
 
        Undeclared accounts, if any, are displayed last, in alphabetical order.
 
-       Note  that  sorting  is  done at each level of the account tree (within
-       each group of sibling accounts under the same parent).  And  currently,
+       Note that sorting is done at each level of  the  account  tree  (within
+       each  group of sibling accounts under the same parent).  And currently,
        this directive:
 
               account other:zoo
 
-       would  influence the position of zoo among other's subaccounts, but not
-       the position of other among the top-level accounts.  This means: -  you
-       will  sometimes  declare  parent accounts (eg account other above) that
-       you don't intend to post to, just to customize their  display  order  -
-       sibling accounts stay together (you couldn't display x:y in between a:b
-       and a:c).
+       would influence the position of zoo among other's subaccounts, but  not
+       the position of other among the top-level accounts.  This means:
 
+       o you  will  sometimes declare parent accounts (eg account other above)
+         that you don't intend to post to, just to customize their display or-
+         der
+
+       o sibling  accounts  stay together (you couldn't display x:y in between
+         a:b and a:c).
+
    Rewriting accounts
        You can define account alias rules which rewrite your account names, or
        parts of them, before generating reports.  This can be useful for:
@@ -1024,7 +1027,7 @@
        do not affect account names being entered via hledger add  or  hledger-
        web.
 
-       See also Cookbook: Rewrite account names.
+       See also Rewrite account names.
 
    Basic aliases
        To  set an account alias, use the alias directive in your journal file.
@@ -1288,8 +1291,8 @@
        actually a posting-generating rule:
 
               = QUERY
-                  ACCT  AMT
-                  ACCT  [AMT]
+                  ACCOUNT  AMOUNT
+                  ACCOUNT  [AMOUNT]
                   ...
 
        These posting-generating rules look like normal  postings,  except  the
@@ -1309,6 +1312,13 @@
          symbol S).  The matched posting's amount will be multiplied by N, and
          its commodity symbol will be replaced with S.
 
+       A  query  term  containing  spaces must be enclosed in single or double
+       quotes, as on the command line.  Eg, note the quotes around the  second
+       query term below:
+
+              = expenses:groceries 'expenses:dining out'
+                  (budget:funds:dining out)                 *-1
+
        These rules have global effect - a rule appearing anywhere in your data
        can potentially affect any transaction, including transactions recorded
        above it or in another file.
@@ -1333,12 +1343,12 @@
                 assets:checking
 
               $ hledger print --auto
-              2017/12/01
+              2017-12-01
                   expenses:food              $10
                   assets:checking
                   (liabilities:charity)      $-1
 
-              2017/12/14
+              2017-12-14
                   expenses:gifts             $20
                   assets:checking
                   assets:checking:gifts     -$20
@@ -1380,17 +1390,8 @@
        o _modified: - a hidden tag not appearing in the comment; this transac-
          tion was modified "just now".
 
-EDITOR SUPPORT
-       Helper modes exist for popular text editors, which  make  working  with
-       journal files easier.  They add colour, formatting, tab completion, and
-       helpful commands, and are quite recommended if you  edit  your  journal
-       with  a  text  editor.   They  include  ledger-mode or hledger-mode for
-       Emacs, vim-ledger for Vim, hledger-vscode for Visual Studio  Code,  and
-       others.   See  the  [[Cookbook]] at hledger.org for the latest informa-
-       tion.
 
 
-
 REPORTING BUGS
        Report bugs at http://bugs.hledger.org (or on the #hledger IRC  channel
        or hledger mail list)
@@ -1414,4 +1415,4 @@
 
 
 
-hledger 1.16.2                   January 2020               hledger_journal(5)
+hledger 1.17                      March 2020                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" "January 2020" "hledger 1.16.2" "hledger User Manuals"
+.TH "hledger_timeclock" "5" "March 2020" "hledger 1.17" "hledger User Manuals"
 
 
 
@@ -36,13 +36,13 @@
 .nf
 \f[C]
 $ hledger -f t.timeclock print
-2015/03/30 * optional description after two spaces
+2015-03-30 * optional description after two spaces
     (some:account name)         0.33h
 
-2015/03/31 * 22:21-23:59
+2015-03-31 * 22:21-23:59
     (another account)         1.64h
 
-2015/04/01 * 00:00-02:00
+2015-04-01 * 00:00-02:00
     (another account)         2.01h
 \f[R]
 .fi
diff --git a/hledger_timeclock.info b/hledger_timeclock.info
--- a/hledger_timeclock.info
+++ b/hledger_timeclock.info
@@ -4,16 +4,18 @@
 
 File: hledger_timeclock.info,  Node: Top,  Up: (dir)
 
-hledger_timeclock(5) hledger 1.16.2
-***********************************
+hledger_timeclock(5) hledger 1.17
+*********************************
 
-hledger can read timeclock files.  As with Ledger, these are (a subset
-of) timeclock.el's format, containing clock-in and clock-out entries as
-in the example below.  The date is a simple date.  The time format is
-HH:MM[:SS][+-ZZZZ]. Seconds and timezone are optional.  The timezone, if
-present, must be four digits and is ignored (currently the time is
-always interpreted as a local time).
+Timeclock - the time logging format of timeclock.el, as read by hledger
 
+   hledger can read timeclock files.  As with Ledger, these are (a
+subset of) timeclock.el's format, containing clock-in and clock-out
+entries as in the example below.  The date is a simple date.  The time
+format is HH:MM[:SS][+-ZZZZ]. Seconds and timezone are optional.  The
+timezone, if present, must be four digits and is ignored (currently the
+time is always interpreted as a local time).
+
 i 2015/03/30 09:00:00 some:account name  optional description after two spaces
 o 2015/03/30 09:20:00
 i 2015/03/31 22:21:45 another account
@@ -25,13 +27,13 @@
 the above time log, 'hledger print' generates these journal entries:
 
 $ hledger -f t.timeclock print
-2015/03/30 * optional description after two spaces
+2015-03-30 * optional description after two spaces
     (some:account name)         0.33h
 
-2015/03/31 * 22:21-23:59
+2015-03-31 * 22:21-23:59
     (another account)         1.64h
 
-2015/04/01 * 00:00-02:00
+2015-04-01 * 00:00-02:00
     (another account)         2.01h
 
    Here is a sample.timeclock to download and some queries to try:
diff --git a/hledger_timeclock.txt b/hledger_timeclock.txt
--- a/hledger_timeclock.txt
+++ b/hledger_timeclock.txt
@@ -25,13 +25,13 @@
        the above time log, hledger print generates these journal entries:
 
               $ hledger -f t.timeclock print
-              2015/03/30 * optional description after two spaces
+              2015-03-30 * optional description after two spaces
                   (some:account name)         0.33h
 
-              2015/03/31 * 22:21-23:59
+              2015-03-31 * 22:21-23:59
                   (another account)         1.64h
 
-              2015/04/01 * 00:00-02:00
+              2015-04-01 * 00:00-02:00
                   (another account)         2.01h
 
        Here is a sample.timeclock to download and some queries to try:
@@ -78,4 +78,4 @@
 
 
 
-hledger 1.16.2                   January 2020             hledger_timeclock(5)
+hledger 1.17                      March 2020              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" "January 2020" "hledger 1.16.2" "hledger User Manuals"
+.TH "hledger_timedot" "5" "March 2020" "hledger 1.17" "hledger User Manuals"
 
 
 
@@ -20,20 +20,23 @@
 commodityless quantities, so it could be used to represent dated
 quantities other than time.
 In the docs below we\[aq]ll assume it\[aq]s time.
-.SH FILE FORMAT
 .PP
 A timedot file contains a series of day entries.
-A day entry begins with a date, and is followed by category/quantity
-pairs, one per line.
-Dates are hledger-style simple dates (see hledger_journal(5)).
-Categories are hledger-style account names, optionally indented.
-As in a hledger journal, there must be at least two spaces between the
-category (account name) and the quantity.
+A day entry begins with a non-indented hledger-style simple date (Y-M-D,
+Y/M/D, Y.M.D..) Any additional text on the same line is used as a
+transaction description for this day.
 .PP
+This is followed by optionally-indented timelog items for that day, one
+per line.
+Each timelog item is a note, usually a hledger:style:account:name
+representing a time category, followed by two or more spaces, and a
+quantity.
+Each timelog item generates a hledger transaction.
+.PP
 Quantities can be written as:
 .IP \[bu] 2
-a sequence of dots (.) representing quarter hours.
-Spaces may optionally be used for grouping and readability.
+dots: a sequence of dots (.) representing quarter hours.
+Spaces may optionally be used for grouping.
 Eg: ....
 \&..
 .IP \[bu] 2
@@ -48,15 +51,32 @@
 The following equivalencies are assumed, currently: 1m = 60s, 1h = 60m,
 1d = 24h, 1w = 7d, 1mo = 30d, 1y=365d.
 .PP
-Blank lines and lines beginning with #, ; or * are ignored.
-An example:
+There is some flexibility allowing notes and todo lists to be kept right
+in the time log, if needed:
+.IP \[bu] 2
+Blank lines and lines beginning with \f[C]#\f[R] or \f[C];\f[R] are
+ignored.
+.IP \[bu] 2
+Lines not ending with a double-space and quantity are parsed as items
+taking no time, which will not appear in balance reports by default.
+(Add -E to see them.)
+.IP \[bu] 2
+Org mode headlines (lines beginning with one or more \f[C]*\f[R]
+followed by a space) can be used as date lines or timelog items (the
+stars are ignored).
+Also all org headlines before the first date line are ignored.
+This means org users can manage their timelog as an org outline (eg
+using org-mode/orgstruct-mode in Emacs), for organisation, faster
+navigation, controlling visibility etc.
+.PP
+Examples:
 .IP
 .nf
 \f[C]
 # on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.
 2016/2/1
 inc:client1   .... .... .... .... .... ....
-fos:haskell   .... .. 
+fos:haskell   .... ..
 biz:research  .
 
 2016/2/2
@@ -64,8 +84,6 @@
 biz:research  .
 \f[R]
 .fi
-.PP
-Or with numbers:
 .IP
 .nf
 \f[C]
@@ -75,16 +93,45 @@
 biz:research  1
 \f[R]
 .fi
+.IP
+.nf
+\f[C]
+* Time log
+** 2020-01-01
+*** adm:time  .
+*** adm:finance  .
+\f[R]
+.fi
+.IP
+.nf
+\f[C]
+* 2020 Work Diary
+** Q1
+*** 2020-02-29
+**** DONE
+0700 yoga
+**** UNPLANNED
+**** BEGUN
+hom:chores
+ cleaning  ...
+ water plants
+  outdoor - one full watering can
+  indoor - light watering
+**** TODO
+adm:planning: trip
+*** LATER
+\f[R]
+.fi
 .PP
 Reporting:
 .IP
 .nf
 \f[C]
 $ hledger -f t.timedot print date:2016/2/2
-2016/02/02 *
+2016-02-02 *
     (inc:client1)          2.00
 
-2016/02/02 *
+2016-02-02 *
     (biz:research)          0.25
 \f[R]
 .fi
@@ -92,9 +139,9 @@
 .nf
 \f[C]
 $ hledger -f t.timedot bal --daily --tree
-Balance changes in 2016/02/01-2016/02/03:
+Balance changes in 2016-02-01-2016-02-03:
 
-            ||  2016/02/01d  2016/02/02d  2016/02/03d 
+            ||  2016-02-01d  2016-02-02d  2016-02-03d 
 ============++========================================
  biz        ||         0.25         0.25         1.00 
    research ||         0.25         0.25         1.00 
diff --git a/hledger_timedot.info b/hledger_timedot.info
--- a/hledger_timedot.info
+++ b/hledger_timedot.info
@@ -2,14 +2,16 @@
 stdin.
 
 
-File: hledger_timedot.info,  Node: Top,  Next: FILE FORMAT,  Up: (dir)
+File: hledger_timedot.info,  Node: Top,  Up: (dir)
 
-hledger_timedot(5) hledger 1.16.2
-*********************************
+hledger_timedot(5) hledger 1.17
+*******************************
 
-Timedot is a plain text format for logging dated, categorised quantities
-(of time, usually), supported by hledger.  It is convenient for
-approximate and retroactive time logging, eg when the real-time
+Timedot - hledger's human-friendly time logging format
+
+   Timedot is a plain text format for logging dated, categorised
+quantities (of time, usually), supported by hledger.  It is convenient
+for approximate and retroactive time logging, eg when the real-time
 clock-in/out required with a timeclock file is too precise or too
 interruptive.  It can be formatted like a bar chart, making clear at a
 glance where time was spent.
@@ -18,27 +20,21 @@
 commodityless quantities, so it could be used to represent dated
 quantities other than time.  In the docs below we'll assume it's time.
 
-* Menu:
-
-* FILE FORMAT::
-
-
-File: hledger_timedot.info,  Node: FILE FORMAT,  Prev: Top,  Up: Top
-
-1 FILE FORMAT
-*************
+   A timedot file contains a series of day entries.  A day entry begins
+with a non-indented hledger-style simple date (Y-M-D, Y/M/D, Y.M.D..)
+Any additional text on the same line is used as a transaction
+description for this day.
 
-A timedot file contains a series of day entries.  A day entry begins
-with a date, and is followed by category/quantity pairs, one per line.
-Dates are hledger-style simple dates (see hledger_journal(5)).
-Categories are hledger-style account names, optionally indented.  As in
-a hledger journal, there must be at least two spaces between the
-category (account name) and the quantity.
+   This is followed by optionally-indented timelog items for that day,
+one per line.  Each timelog item is a note, usually a
+hledger:style:account:name representing a time category, followed by two
+or more spaces, and a quantity.  Each timelog item generates a hledger
+transaction.
 
    Quantities can be written as:
 
-   * a sequence of dots (.)  representing quarter hours.  Spaces may
-     optionally be used for grouping and readability.  Eg: ....  ..
+   * dots: a sequence of dots (.)  representing quarter hours.  Spaces
+     may optionally be used for grouping.  Eg: ....  ..
 
    * an integral or decimal number, representing hours.  Eg: 1.5
 
@@ -48,39 +44,73 @@
      The following equivalencies are assumed, currently: 1m = 60s, 1h =
      60m, 1d = 24h, 1w = 7d, 1mo = 30d, 1y=365d.
 
-   Blank lines and lines beginning with #, ; or * are ignored.  An
-example:
+   There is some flexibility allowing notes and todo lists to be kept
+right in the time log, if needed:
 
+   * Blank lines and lines beginning with '#' or ';' are ignored.
+
+   * Lines not ending with a double-space and quantity are parsed as
+     items taking no time, which will not appear in balance reports by
+     default.  (Add -E to see them.)
+
+   * Org mode headlines (lines beginning with one or more '*' followed
+     by a space) can be used as date lines or timelog items (the stars
+     are ignored).  Also all org headlines before the first date line
+     are ignored.  This means org users can manage their timelog as an
+     org outline (eg using org-mode/orgstruct-mode in Emacs), for
+     organisation, faster navigation, controlling visibility etc.
+
+   Examples:
+
 # on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.
 2016/2/1
 inc:client1   .... .... .... .... .... ....
-fos:haskell   .... .. 
+fos:haskell   .... ..
 biz:research  .
 
 2016/2/2
 inc:client1   .... ....
 biz:research  .
 
-   Or with numbers:
-
 2016/2/3
 inc:client1   4
 fos:hledger   3
 biz:research  1
 
+* Time log
+** 2020-01-01
+*** adm:time  .
+*** adm:finance  .
+
+* 2020 Work Diary
+** Q1
+*** 2020-02-29
+**** DONE
+0700 yoga
+**** UNPLANNED
+**** BEGUN
+hom:chores
+ cleaning  ...
+ water plants
+  outdoor - one full watering can
+  indoor - light watering
+**** TODO
+adm:planning: trip
+*** LATER
+
    Reporting:
 
 $ hledger -f t.timedot print date:2016/2/2
-2016/02/02 *
+2016-02-02 *
     (inc:client1)          2.00
 
-2016/02/02 *
+2016-02-02 *
     (biz:research)          0.25
 
 $ hledger -f t.timedot bal --daily --tree
-Balance changes in 2016/02/01-2016/02/03:
+Balance changes in 2016-02-01-2016-02-03:
 
-            ||  2016/02/01d  2016/02/02d  2016/02/03d 
+            ||  2016-02-01d  2016-02-02d  2016-02-03d 
 ============++========================================
  biz        ||         0.25         0.25         1.00 
    research ||         0.25         0.25         1.00 
@@ -111,8 +141,6 @@
 
 Tag Table:
 Node: Top76
-Node: FILE FORMAT812
-Ref: #file-format913
 
 End Tag Table
 
diff --git a/hledger_timedot.txt b/hledger_timedot.txt
--- a/hledger_timedot.txt
+++ b/hledger_timedot.txt
@@ -18,18 +18,21 @@
        less  quantities,  so  it  could  be used to represent dated quantities
        other than time.  In the docs below we'll assume it's time.
 
-FILE FORMAT
        A timedot file contains a series of day entries.  A  day  entry  begins
-       with  a date, and is followed by category/quantity pairs, one per line.
-       Dates are hledger-style simple dates (see  hledger_journal(5)).   Cate-
-       gories  are  hledger-style account names, optionally indented.  As in a
-       hledger journal, there must be at least two spaces between the category
-       (account name) and the quantity.
+       with  a  non-indented hledger-style simple date (Y-M-D, Y/M/D, Y.M.D..)
+       Any additional text on the same line is used as a transaction  descrip-
+       tion for this day.
 
+       This is followed by optionally-indented timelog items for that day, one
+       per line.  Each timelog item is a  note,  usually  a  hledger:style:ac-
+       count:name  representing  a time category, followed by two or more spa-
+       ces, and a quantity.  Each timelog item generates  a  hledger  transac-
+       tion.
+
        Quantities can be written as:
 
-       o a  sequence  of  dots (.) representing quarter hours.  Spaces may op-
-         tionally be used for grouping and readability.  Eg: ....  ..
+       o dots:  a sequence of dots (.) representing quarter hours.  Spaces may
+         optionally be used for grouping.  Eg: ....  ..
 
        o an integral or decimal number, representing hours.  Eg: 1.5
 
@@ -39,9 +42,24 @@
          lencies  are  assumed,  currently: 1m = 60s, 1h = 60m, 1d = 24h, 1w =
          7d, 1mo = 30d, 1y=365d.
 
-       Blank lines and lines beginning with #, ; or * are ignored.   An  exam-
-       ple:
+       There is some flexibility allowing notes and  todo  lists  to  be  kept
+       right in the time log, if needed:
 
+       o Blank lines and lines beginning with # or ; are ignored.
+
+       o Lines not ending with a double-space and quantity are parsed as items
+         taking no time, which will not appear in balance reports by  default.
+         (Add -E to see them.)
+
+       o Org  mode headlines (lines beginning with one or more * followed by a
+         space) can be used as date lines or timelog items (the stars are  ig-
+         nored).   Also  all  org headlines before the first date line are ig-
+         nored.  This means org users can manage their timelog as an org  out-
+         line  (eg  using org-mode/orgstruct-mode in Emacs), for organisation,
+         faster navigation, controlling visibility etc.
+
+       Examples:
+
               # on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.
               2016/2/1
               inc:client1   .... .... .... .... .... ....
@@ -52,26 +70,45 @@
               inc:client1   .... ....
               biz:research  .
 
-       Or with numbers:
-
               2016/2/3
               inc:client1   4
               fos:hledger   3
               biz:research  1
 
+              * Time log
+              ** 2020-01-01
+              *** adm:time  .
+              *** adm:finance  .
+
+              * 2020 Work Diary
+              ** Q1
+              *** 2020-02-29
+              **** DONE
+              0700 yoga
+              **** UNPLANNED
+              **** BEGUN
+              hom:chores
+               cleaning  ...
+               water plants
+                outdoor - one full watering can
+                indoor - light watering
+              **** TODO
+              adm:planning: trip
+              *** LATER
+
        Reporting:
 
               $ hledger -f t.timedot print date:2016/2/2
-              2016/02/02 *
+              2016-02-02 *
                   (inc:client1)          2.00
 
-              2016/02/02 *
+              2016-02-02 *
                   (biz:research)          0.25
 
               $ hledger -f t.timedot bal --daily --tree
-              Balance changes in 2016/02/01-2016/02/03:
+              Balance changes in 2016-02-01-2016-02-03:
 
-                          ||  2016/02/01d  2016/02/02d  2016/02/03d
+                          ||  2016-02-01d  2016-02-02d  2016-02-03d
               ============++========================================
                biz        ||         0.25         0.25         1.00
                  research ||         0.25         0.25         1.00
@@ -83,7 +120,7 @@
               ------------++----------------------------------------
                           ||         7.75         2.25         8.00
 
-       I  prefer to use period for separating account components.  We can make
+       I prefer to use period for separating account components.  We can  make
        this work with an account alias:
 
               2016/2/4
@@ -102,7 +139,7 @@
 
 
 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)
 
 
@@ -116,7 +153,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)
 
@@ -124,4 +161,4 @@
 
 
 
-hledger 1.16.2                   January 2020               hledger_timedot(5)
+hledger 1.17                      March 2020                hledger_timedot(5)
