diff --git a/CHANGES b/CHANGES
--- a/CHANGES
+++ b/CHANGES
@@ -1,9 +1,154 @@
-API-ish changes in hledger-lib.
-For user-visible changes, see hledger's change log.
+API-ish changes in the hledger-lib package.
+(For user-visible changes, see hledger's change log.)
 
-0.27.1 (2016/05/27)
 
-- GHC 8 compatibility
+# 1.0 (2016/10/26)
+
+## timedot format
+
+-   new "timedot" format for retroactive/approximate time logging.
+
+    Timedot is a plain text format for logging dated, categorised
+    quantities (eg time), 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.
+
+## timeclock format
+
+-   renamed "timelog" format to "timeclock", matching the emacs package
+
+-   sessions can no longer span file boundaries (unclocked-out
+
+    sessions will be auto-closed at the end of the file).
+
+-   transaction ids now count up rather than down (#394)
+
+-   timeclock files no longer support default year directives
+
+-   removed old code for appending timeclock transactions to journal transactions.
+
+    A holdover from the days when both were allowed in one file.
+
+## csv format
+
+-   fix empty field assignment parsing, rule parse errors after megaparsec port (#407) (Hans-Peter Deifel)
+
+## journal format
+
+-   journal files can now include timeclock or timedot files (#320)
+
+    (but not yet CSV files).
+
+-   fixed an issue with ordering of same-date transactions included from other files
+
+-   the "commodity" directive and "format" subdirective are now supported, allowing
+
+    full control of commodity style (#295) The commodity directive's
+    format subdirective can now be used to override the inferred
+    style for a commodity, eg to increase or decrease the
+    precision. This is at least a good workaround for #295.
+
+-   Ledger-style "apply account"/"end apply account" directives are now used to set a default parent account.
+
+-   the Ledger-style "account" directive is now accepted (and ignored).
+
+-   bracketed posting dates are more robust (#304)
+
+    Bracketed posting dates were fragile; they worked only if you
+    wrote full 10-character dates. Also some semantics were a bit
+    unclear. Now they should be robust, and have been documented
+    more clearly. This is a legacy undocumented Ledger syntax, but
+    it improves compatibility and might be preferable to the more
+    verbose "date:" tags if you write posting dates often (as I do).
+    Internally, bracketed posting dates are no longer considered to
+    be tags.  Journal comment, tag, and posting date parsers have
+    been reworked, all with doctests.
+
+-   balance assertion failure messages are clearer
+
+-   with --debug=2, more detail about balance assertions is shown.
+
+## misc
+
+-   file parsers have been ported from Parsec to Megaparsec \o/ (#289, #366) (Alexey Shmalko, Moritz Kiefer)
+
+-   most hledger types have been converted from String to Text, reducing memory usage by 30%+ on large files and giving a slight speed increase
+
+-   file parsers have been simplified for easier troubleshooting (#275).
+
+    The journal/timeclock/timedot parsers, instead of constructing
+    opaque journal update functions which are later applied to build
+    the journal, now construct the journal directly by modifying the
+    parser state. This is easier to understand and debug. It also
+    rules out the possibility of journal updates being a space
+    leak. (They weren't, in fact this change increased memory usage
+    slightly, but that has been addressed in other ways).  The
+    ParsedJournal type alias has been added to distinguish
+    "being-parsed" journals and "finalised" journals.
+
+-   file format detection is more robust.
+
+    The Journal, Timelog and Timedot readers' detectors now check
+    each line in the sample data, not just the first one. I think the
+    sample data is only about 30 chars right now, but even so this
+    fixed a format detection issue I was seeing. 
+    Also, we now always try parsing stdin as journal format (not just sometimes).
+
+-   all file formats now produce transaction ids, not just journal (#394)
+
+-   git clone of the hledger repo on windows now works (#345)
+
+-   added missing benchmark file (#342)
+
+-   our stack.yaml files are more compatible across stack versions (#300)
+
+-   use newer file-embed to fix ghci working directory dependence (<https://github.com/snoyberg/file-embed/issues/18>)
+
+-   report more accurate dates in account transaction report when postings have their own dates
+
+    (affects hledger-ui and hledger-web registers).
+    The newly-named "transaction register date" is the date to be
+    displayed for that transaction in a transaction register, for
+    some current account and filter query.  It is either the
+    transaction date from the journal ("transaction general date"),
+    or if postings to the current account and matched by the
+    register's filter query have their own dates, the earliest of
+    those posting dates.
+
+-   simplify account transactions report's running total.
+
+    The account transactions report used for hledger-ui and -web
+    registers now gives either the "period total" or "historical
+    total", depending strictly on the --historical flag. It doesn't
+    try to indicate whether the historical total is the accurate
+    historical balance (which depends on the user's report query).
+
+-   reloading a file now preserves the effect of options, query arguments etc.
+
+-   reloading a journal should now reload all included files as well.
+
+-   the Hledger.Read.\* modules have been reorganised for better reuse.
+
+    Hledger.Read.Utils has been renamed Hledger.Read.Common
+    and holds low-level parsers & utilities; high-level read
+    utilities are now in Hledger.Read.
+
+-   clarify amount display style canonicalisation code and terminology a bit.
+
+    Individual amounts still have styles; from these we derive
+    the standard "commodity styles". In user docs, we might call
+    these "commodity formats" since they can be controlled by the
+    "format" subdirective in journal files.
+
+-   Journal is now a monoid
+
+-   expandPath now throws a proper IO error
+
+-   more unit tests, start using doctest
+
+
 
 
 0.27 (2015/10/30)
diff --git a/Hledger.hs b/Hledger.hs
--- a/Hledger.hs
+++ b/Hledger.hs
@@ -1,19 +1,15 @@
 module Hledger (
-                module Hledger.Data
-               ,module Hledger.Query
-               ,module Hledger.Read
-               ,module Hledger.Reports
-               ,module Hledger.Utils
-               ,tests_Hledger
+  module X
+ ,tests_Hledger
 )
 where
-import Test.HUnit
+import           Test.HUnit
 
-import Hledger.Data
-import Hledger.Query
-import Hledger.Read hiding (samplejournal)
-import Hledger.Reports
-import Hledger.Utils
+import           Hledger.Data    as X
+import           Hledger.Query   as X
+import           Hledger.Read    as X hiding (samplejournal)
+import           Hledger.Reports as X
+import           Hledger.Utils   as X
 
 tests_Hledger = TestList
     [
diff --git a/Hledger/Data.hs b/Hledger/Data.hs
--- a/Hledger/Data.hs
+++ b/Hledger/Data.hs
@@ -15,10 +15,11 @@
                module Hledger.Data.Dates,
                module Hledger.Data.Journal,
                module Hledger.Data.Ledger,
+               module Hledger.Data.Period,
                module Hledger.Data.Posting,
                module Hledger.Data.RawOptions,
                module Hledger.Data.StringFormat,
-               module Hledger.Data.TimeLog,
+               module Hledger.Data.Timeclock,
                module Hledger.Data.Transaction,
                module Hledger.Data.Types,
                tests_Hledger_Data
@@ -33,10 +34,11 @@
 import Hledger.Data.Dates
 import Hledger.Data.Journal
 import Hledger.Data.Ledger
+import Hledger.Data.Period
 import Hledger.Data.Posting
 import Hledger.Data.RawOptions
 import Hledger.Data.StringFormat
-import Hledger.Data.TimeLog
+import Hledger.Data.Timeclock
 import Hledger.Data.Transaction
 import Hledger.Data.Types
 
@@ -47,13 +49,12 @@
     ,tests_Hledger_Data_AccountName
     ,tests_Hledger_Data_Amount
     ,tests_Hledger_Data_Commodity
-    ,tests_Hledger_Data_Dates
     ,tests_Hledger_Data_Journal
     ,tests_Hledger_Data_Ledger
     ,tests_Hledger_Data_Posting
     -- ,tests_Hledger_Data_RawOptions
     -- ,tests_Hledger_Data_StringFormat
-    ,tests_Hledger_Data_TimeLog
+    ,tests_Hledger_Data_Timeclock
     ,tests_Hledger_Data_Transaction
     -- ,tests_Hledger_Data_Types
     ]
diff --git a/Hledger/Data/Account.hs b/Hledger/Data/Account.hs
--- a/Hledger/Data/Account.hs
+++ b/Hledger/Data/Account.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE RecordWildCards, StandaloneDeriving #-}
+{-# LANGUAGE RecordWildCards, StandaloneDeriving, OverloadedStrings #-}
 {-|
 
 
@@ -52,9 +52,12 @@
   , aboring = False
   }
 
--- | Derive 1. an account tree and 2. their balances from a list of postings.
--- (ledger's core feature). The accounts are returned in a list, but
--- retain their tree structure; the first one is the root of the tree.
+-- | Derive 1. an account tree and 2. each account's total exclusive
+-- and inclusive changes from a list of postings.
+-- This is the core of the balance command (and of *ledger).
+-- The accounts are returned as a list in flattened tree order,
+-- and also reference each other as a tree.
+-- (The first account is the root of the tree.)
 accountsFromPostings :: [Posting] -> [Account]
 accountsFromPostings ps =
   let
diff --git a/Hledger/Data/AccountName.hs b/Hledger/Data/AccountName.hs
--- a/Hledger/Data/AccountName.hs
+++ b/Hledger/Data/AccountName.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE NoMonomorphismRestriction#-}
+{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
 {-|
 
 'AccountName's are strings like @assets:cash:petty@, with multiple
@@ -10,7 +10,9 @@
 module Hledger.Data.AccountName
 where
 import Data.List
-import Data.List.Split (splitOn)
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Tree
 import Test.HUnit
 import Text.Printf
@@ -19,24 +21,29 @@
 import Hledger.Utils
 
 
-
--- change to use a different separator for nested accounts
+acctsepchar :: Char
 acctsepchar = ':'
 
-accountNameComponents :: AccountName -> [String]
-accountNameComponents = splitAtElement acctsepchar
+acctsep :: Text
+acctsep = T.pack [acctsepchar]
 
-accountNameFromComponents :: [String] -> AccountName
-accountNameFromComponents = concat . intersperse [acctsepchar]
+-- accountNameComponents :: AccountName -> [String]
+-- accountNameComponents = splitAtElement acctsepchar
 
-accountLeafName :: AccountName -> String
+accountNameComponents :: AccountName -> [Text]
+accountNameComponents = T.splitOn acctsep
+
+accountNameFromComponents :: [Text] -> AccountName
+accountNameFromComponents = T.intercalate acctsep
+
+accountLeafName :: AccountName -> Text
 accountLeafName = last . accountNameComponents
 
 -- | Truncate all account name components but the last to two characters.
-accountSummarisedName :: AccountName -> String
+accountSummarisedName :: AccountName -> Text
 accountSummarisedName a
   --   length cs > 1 = take 2 (head cs) ++ ":" ++ a'
-  | length cs > 1 = intercalate ":" (map (take 2) $ init cs) ++ ":" ++ a'
+  | length cs > 1 = (T.intercalate ":" (map (T.take 2) $ init cs)) <> ":" <> a'
   | otherwise     = a'
     where
       cs = accountNameComponents a
@@ -44,7 +51,7 @@
 
 accountNameLevel :: AccountName -> Int
 accountNameLevel "" = 0
-accountNameLevel a = length (filter (==acctsepchar) a) + 1
+accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1
 
 accountNameDrop :: Int -> AccountName -> AccountName
 accountNameDrop n = accountNameFromComponents . drop n . accountNameComponents
@@ -72,7 +79,7 @@
 
 -- | Is the first account a parent or other ancestor of (and not the same as) the second ?
 isAccountNamePrefixOf :: AccountName -> AccountName -> Bool
-isAccountNamePrefixOf = isPrefixOf . (++ [acctsepchar])
+isAccountNamePrefixOf = T.isPrefixOf . (<> acctsep)
 
 isSubAccountNameOf :: AccountName -> AccountName -> Bool
 s `isSubAccountNameOf` p =
@@ -113,22 +120,22 @@
 elideAccountName :: Int -> AccountName -> AccountName
 elideAccountName width s
   -- XXX special case for transactions register's multi-account pseudo-names
-  | " (split)" `isSuffixOf` s =
+  | " (split)" `T.isSuffixOf` s =
     let
-      names = splitOn ", " $ take (length s - 8) s
+      names = T.splitOn ", " $ T.take (T.length s - 8) s
       widthpername = (max 0 (width - 8 - 2 * (max 1 (length names) - 1))) `div` length names
     in
-     fitString Nothing (Just width) True False $
-     (++" (split)") $
-     intercalate ", " $
+     fitText Nothing (Just width) True False $
+     (<>" (split)") $
+     T.intercalate ", " $
      [accountNameFromComponents $ elideparts widthpername [] $ accountNameComponents s' | s' <- names]
   | otherwise =
-    fitString Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s
+    fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s
       where
-        elideparts :: Int -> [String] -> [String] -> [String]
+        elideparts :: Int -> [Text] -> [Text] -> [Text]
         elideparts width done ss
-          | strWidth (accountNameFromComponents $ done++ss) <= width = done++ss
-          | length ss > 1 = elideparts width (done++[takeWidth 2 $ head ss]) (tail ss)
+          | textWidth (accountNameFromComponents $ done++ss) <= width = done++ss
+          | length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss)
           | otherwise = done++ss
 
 -- | Keep only the first n components of an account name, where n
@@ -143,18 +150,18 @@
 clipOrEllipsifyAccountName n = accountNameFromComponents . take n . accountNameComponents
 
 -- | Convert an account name to a regular expression matching it and its subaccounts.
-accountNameToAccountRegex :: String -> String
+accountNameToAccountRegex :: AccountName -> Regexp
 accountNameToAccountRegex "" = ""
-accountNameToAccountRegex a = printf "^%s(:|$)" a
+accountNameToAccountRegex a = printf "^%s(:|$)" (T.unpack a)
 
 -- | Convert an account name to a regular expression matching it but not its subaccounts.
-accountNameToAccountOnlyRegex :: String -> String
+accountNameToAccountOnlyRegex :: AccountName -> Regexp
 accountNameToAccountOnlyRegex "" = ""
-accountNameToAccountOnlyRegex a = printf "^%s$" a
+accountNameToAccountOnlyRegex a = printf "^%s$"  $ T.unpack a -- XXX pack
 
 -- | Convert an exact account-matching regular expression to a plain account name.
-accountRegexToAccountName :: String -> String
-accountRegexToAccountName = regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1"
+accountRegexToAccountName :: Regexp -> AccountName
+accountRegexToAccountName = T.pack . regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1" -- XXX pack
 
 -- | Does this string look like an exact account-matching regular expression ?
 isAccountRegex  :: String -> Bool
diff --git a/Hledger/Data/Amount.hs b/Hledger/Data/Amount.hs
--- a/Hledger/Data/Amount.hs
+++ b/Hledger/Data/Amount.hs
@@ -1,7 +1,6 @@
-{-# LANGUAGE CPP, StandaloneDeriving, RecordWildCards #-}
 {-|
 A simple 'Amount' is some quantity of money, shares, or anything else.
-It has a (possibly null) 'Commodity' and a numeric quantity:
+It has a (possibly null) 'CommoditySymbol' and a numeric quantity:
 
 @
   $1
@@ -41,6 +40,8 @@
 
 -}
 
+{-# LANGUAGE CPP, StandaloneDeriving, RecordWildCards, OverloadedStrings #-}
+
 module Hledger.Data.Amount (
   -- * Amount
   amount,
@@ -101,15 +102,13 @@
 ) where
 
 import Data.Char (isDigit)
-#ifdef DOUBLE
-roundTo = flip const
-#else
 import Data.Decimal (roundTo)
-#endif
 import Data.Function (on)
 import Data.List
 import Data.Map (findWithDefault)
 import Data.Maybe
+-- import Data.Text (Text)
+import qualified Data.Text as T
 import Test.HUnit
 import Text.Printf
 import qualified Data.Map as M
@@ -180,7 +179,7 @@
 
 -- | Convert an amount to the specified commodity, ignoring and discarding
 -- any assigned prices and assuming an exchange rate of 1.
-amountWithCommodity :: Commodity -> Amount -> Amount
+amountWithCommodity :: CommoditySymbol -> Amount -> Amount
 amountWithCommodity c a = a{acommodity=c, aprice=NoPrice}
 
 -- | Convert an amount to the commodity of its assigned price, if any.  Notes:
@@ -211,16 +210,8 @@
                | otherwise     = (null . filter (`elem` digits) . showAmountWithoutPriceOrCommodity) a
 
 -- | Is this amount "really" zero, regardless of the display precision ?
--- Since we are using floating point, for now just test to some high precision.
 isReallyZeroAmount :: Amount -> Bool
-isReallyZeroAmount Amount{aquantity=q} = iszero q
-  where
-   iszero =
-#ifdef DOUBLE
-    null . filter (`elem` digits) . printf ("%."++show zeroprecision++"f") where zeroprecision = 8
-#else
-    (==0)
-#endif
+isReallyZeroAmount Amount{aquantity=q} = q == 0
 
 -- | Get the string representation of an amount, based on its commodity's
 -- display settings except using the specified precision.
@@ -270,14 +261,14 @@
 showAmountHelper _ Amount{acommodity="AUTO"} = ""
 showAmountHelper showzerocommodity a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) =
     case ascommodityside of
-      L -> printf "%s%s%s%s" c' space quantity' price
-      R -> printf "%s%s%s%s" quantity' space c' price
+      L -> printf "%s%s%s%s" (T.unpack c') space quantity' price
+      R -> printf "%s%s%s%s" quantity' space (T.unpack c') price
     where
       quantity = showamountquantity a
       displayingzero = null $ filter (`elem` digits) $ quantity
       (quantity',c') | displayingzero && not showzerocommodity = ("0","")
                      | otherwise = (quantity, quoteCommoditySymbolIfNeeded c)
-      space = if (not (null c') && ascommodityspaced) then " " else "" :: String
+      space = if (not (T.null c') && ascommodityspaced) then " " else "" :: String
       price = showPrice p
 
 -- | Like showAmount, but show a zero amount's commodity if it has one.
@@ -292,15 +283,9 @@
     where
       -- isint n = fromIntegral (round n) == n
       qstr -- p == maxprecision && isint q = printf "%d" (round q::Integer)
-#ifdef DOUBLE
-        | p == maxprecisionwithpoint = printf "%f" q
-        | p == maxprecision          = chopdotzero $ printf "%f" q
-        | otherwise                  = printf ("%."++show p++"f") q
-#else
         | p == maxprecisionwithpoint = show q
         | p == maxprecision          = chopdotzero $ show q
         | otherwise                  = show $ roundTo (fromIntegral p) q
-#endif
 
 -- | Replace a number string's decimal point with the specified character,
 -- and add the specified digit group separators. The last digit group will
@@ -340,7 +325,7 @@
 
 -- like journalCanonicaliseAmounts
 -- | Canonicalise an amount's display style using the provided commodity style map.
-canonicaliseAmount :: M.Map Commodity AmountStyle -> Amount -> Amount
+canonicaliseAmount :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount
 canonicaliseAmount styles a@Amount{acommodity=c, astyle=s} = a{astyle=s'}
     where
       s' = findWithDefault s c styles
@@ -449,9 +434,9 @@
 sumSimilarAmountsUsingFirstPrice [] = nullamt
 sumSimilarAmountsUsingFirstPrice as = (sum as){aprice=aprice $ head as}
 
--- | Sum same-commodity amounts. If there were different prices, set
--- the price to a special marker indicating "various". Only used as a
--- rendering helper.
+-- -- | Sum same-commodity amounts. If there were different prices, set
+-- -- the price to a special marker indicating "various". Only used as a
+-- -- rendering helper.
 -- sumSimilarAmountsNotingPriceDifference :: [Amount] -> Amount
 -- sumSimilarAmountsNotingPriceDifference [] = nullamt
 -- sumSimilarAmountsNotingPriceDifference as = undefined
@@ -468,7 +453,7 @@
 -- with the specified commodity and the quantity of that commodity
 -- found in the original. NB if Amount's quantity is zero it will be
 -- discarded next time the MixedAmount gets normalised.
-filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount
+filterMixedAmountByCommodity :: CommoditySymbol -> MixedAmount -> MixedAmount
 filterMixedAmountByCommodity c (Mixed as) = Mixed as'
   where
     as' = case filter ((==c) . acommodity) as of
@@ -580,7 +565,7 @@
       stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}
 
 -- | Canonicalise a mixed amount's display styles using the provided commodity style map.
-canonicaliseMixedAmount :: M.Map Commodity AmountStyle -> MixedAmount -> MixedAmount
+canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
 canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as
 
 -------------------------------------------------------------------------------
diff --git a/Hledger/Data/Commodity.hs b/Hledger/Data/Commodity.hs
--- a/Hledger/Data/Commodity.hs
+++ b/Hledger/Data/Commodity.hs
@@ -6,10 +6,16 @@
 are thousands separated by comma, significant decimal places and so on.
 
 -}
+
+{-# LANGUAGE OverloadedStrings #-}
+
 module Hledger.Data.Commodity
 where
 import Data.List
 import Data.Maybe (fromMaybe)
+import Data.Monoid
+-- import Data.Text (Text)
+import qualified Data.Text as T
 import Test.HUnit
 -- import qualified Data.Map as M
 
@@ -18,9 +24,9 @@
 
 
 -- characters that may not be used in a non-quoted commodity symbol
-nonsimplecommoditychars = "0123456789-+.@;\n \"{}=" :: String
+nonsimplecommoditychars = "0123456789-+.@;\n \"{}=" :: [Char]
 
-quoteCommoditySymbolIfNeeded s | any (`elem` nonsimplecommoditychars) s = "\"" ++ s ++ "\""
+quoteCommoditySymbolIfNeeded s | any (`elem` nonsimplecommoditychars) (T.unpack s) = "\"" <> s <> "\""
                                | otherwise = s
 
 commodity = ""
@@ -42,18 +48,18 @@
   ]
 
 -- | Look up one of the sample commodities' symbol by name.
-comm :: String -> Commodity
+comm :: String -> CommoditySymbol
 comm name = snd $ fromMaybe
               (error' "commodity lookup failed")
               (find (\n -> fst n == name) commoditysymbols)
 
 -- | Find the conversion rate between two commodities. Currently returns 1.
-conversionRate :: Commodity -> Commodity -> Double
+conversionRate :: CommoditySymbol -> CommoditySymbol -> Double
 conversionRate _ _ = 1
 
 -- -- | Convert a list of commodities to a map from commodity symbols to
 -- -- unique, display-preference-canonicalised commodities.
--- canonicaliseCommodities :: [Commodity] -> Map.Map String Commodity
+-- canonicaliseCommodities :: [CommoditySymbol] -> Map.Map String CommoditySymbol
 -- canonicaliseCommodities cs =
 --     Map.fromList [(s,firstc{precision=maxp}) | s <- symbols,
 --                   let cs = commoditymap ! s,
diff --git a/Hledger/Data/Dates.hs b/Hledger/Data/Dates.hs
--- a/Hledger/Data/Dates.hs
+++ b/Hledger/Data/Dates.hs
@@ -1,6 +1,8 @@
 {-# LANGUAGE CPP #-}
 {-# LANGUAGE NoMonoLocalBinds #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-|
 
 Date parsing and utilities for hledger.
@@ -19,6 +21,8 @@
 An 'Interval' is ledger's \"reporting interval\" - weekly, monthly,
 quarterly, etc.
 
+'Period' will probably replace DateSpan in due course.
+
 -}
 
 -- XXX fromGregorian silently clips bad dates, use fromGregorianValid instead ?
@@ -38,7 +42,6 @@
   prevday,
   parsePeriodExpr,
   nulldatespan,
-  tests_Hledger_Data_Dates,
   failIfInvalidYear,
   failIfInvalidMonth,
   failIfInvalidDay,
@@ -69,6 +72,8 @@
 import Control.Monad
 import Data.List.Compat
 import Data.Maybe
+import Data.Text (Text)
+import qualified Data.Text as T
 #if MIN_VERSION_time(1,5,0)
 import Data.Time.Format hiding (months)
 #else
@@ -77,82 +82,30 @@
 #endif
 import Data.Time.Calendar
 import Data.Time.Calendar.OrdinalDate
-import Data.Time.Calendar.WeekDate
 import Data.Time.Clock
 import Data.Time.LocalTime
 import Safe (headMay, lastMay, readMay)
-import Test.HUnit
-import Text.Parsec
+import Text.Megaparsec
+import Text.Megaparsec.Text
 import Text.Printf
 
 import Hledger.Data.Types
+import Hledger.Data.Period
 import Hledger.Utils
 
 
 -- Help ppShow parse and line-wrap DateSpans better in debug output.
 instance Show DateSpan where
-    show (DateSpan s1 s2) = "DateSpan \"" ++ show s1 ++ "\" \"" ++ show s2 ++ "\""
+    show s = "DateSpan " ++ showDateSpan s
+    -- show s = "DateSpan \"" ++ showDateSpan s ++ "\"" -- quotes to help pretty-show
 
 showDate :: Day -> String
 showDate = formatTime defaultTimeLocale "%0C%y/%m/%d"
 
--- XXX review for more boundary crossing issues
 -- | Render a datespan as a display string, abbreviating into a
 -- compact form if possible.
-showDateSpan ds@(DateSpan (Just from) (Just to)) =
-  case (toGregorian from, toGregorian to) of
-    -- special cases we can abbreviate:
-    -- a year, YYYY
-    ((fy,1,1), (ty,1,1))   | fy+1==ty           -> formatTime defaultTimeLocale "%0C%y" from
-    -- a half, YYYYhN
-    ((fy,1,1), (ty,7,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yh1" from
-    ((fy,7,1), (ty,1,1))   | fy+1==ty           -> formatTime defaultTimeLocale "%0C%yh2" from
-    -- a quarter, YYYYqN
-    ((fy,1,1), (ty,4,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yq1" from
-    ((fy,4,1), (ty,7,1))   | fy==ty             -> formatTime defaultTimeLocale "%0C%yq2" from
-    ((fy,7,1), (ty,10,1))  | fy==ty             -> formatTime defaultTimeLocale "%0C%yq3" from
-    ((fy,10,1), (ty,1,1))  | fy+1==ty           -> formatTime defaultTimeLocale "%0C%yq4" from
-    -- a month, YYYY/MM
-    ((fy,fm,1), (ty,tm,1)) | fy==ty && fm+1==tm -> formatTime defaultTimeLocale "%0C%y/%m" from
-    ((fy,12,1), (ty,1,1))  | fy+1==ty           -> formatTime defaultTimeLocale "%0C%y/%m" from
-    -- a week (two successive mondays),
-    -- YYYYwN ("week N of year YYYY")
-    -- _ | let ((fy,fw,fd), (ty,tw,td)) = (toWeekDate from, toWeekDate to) in fy==ty && fw+1==tw && fd==1 && td==1
-    --                                             -> formatTime defaultTimeLocale "%0f%gw%V" from
-    -- YYYY/MM/DDwN ("week N, starting on YYYY/MM/DD")
-    _ | let ((fy,fw,fd), (ty,tw,td)) = (toWeekDate from, toWeekDate (addDays (-1) to)) in fy==ty && fw==tw && fd==1 && td==7
-                                                -> formatTime defaultTimeLocale "%0C%y/%m/%dw%V" from
-    -- a day, YYYY/MM/DDd (d suffix is to distinguish from a regular date in register)
-    ((fy,fm,fd), (ty,tm,td)) | fy==ty && fm==tm && fd+1==td -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from
-    -- crossing a year boundary
-    ((fy,fm,fd), (ty,tm,td)) | fy+1==ty && fm==12 && tm==1 && fd==31 && td==1 -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from
-    -- crossing a month boundary XXX wrongly shows LEAPYEAR/2/28-LEAPYEAR/3/1 as LEAPYEAR/2/28
-    ((fy,fm,fd), (ty,tm,td)) | fy==ty && fm+1==tm && fd `elem` fromMaybe [] (lookup fm lastdayofmonth) && td==1 -> formatTime defaultTimeLocale "%0C%y/%m/%dd" from
-    -- otherwise, YYYY/MM/DD-YYYY/MM/DD
-    _                                           -> showDateSpan' ds
-  where lastdayofmonth = [(1,[31])
-                         ,(2,[28,29])
-                         ,(3,[31])
-                         ,(4,[30])
-                         ,(5,[31])
-                         ,(6,[30])
-                         ,(7,[31])
-                         ,(8,[31])
-                         ,(9,[30])
-                         ,(10,[31])
-                         ,(11,[30])
-                         ,(12,[31])
-                         ]
-
-showDateSpan ds = showDateSpan' ds
-
--- | Render a datespan as a display string.
-showDateSpan' (DateSpan from to) =
-  concat
-    [maybe "" showDate from
-    ,"-"
-    ,maybe "" (showDate . prevday) to
-    ]
+showDateSpan :: DateSpan -> String
+showDateSpan = showPeriod . dateSpanAsPeriod
 
 -- | Get the current local date.
 getCurrentDay :: IO Day
@@ -189,6 +142,32 @@
 
 -- | Split a DateSpan into one or more consecutive whole spans of the specified length which enclose it.
 -- If no interval is specified, the original span is returned.
+--
+-- ==== Examples:
+-- >>> let t i d1 d2 = splitSpan i $ mkdatespan d1 d2
+-- >>> t NoInterval "2008/01/01" "2009/01/01"
+-- [DateSpan 2008]
+-- >>> t (Quarters 1) "2008/01/01" "2009/01/01"
+-- [DateSpan 2008q1,DateSpan 2008q2,DateSpan 2008q3,DateSpan 2008q4]
+-- >>> splitSpan (Quarters 1) nulldatespan
+-- [DateSpan -]
+-- >>> t (Days 1) "2008/01/01" "2008/01/01"  -- an empty datespan
+-- [DateSpan 2008/01/01-2007/12/31]
+-- >>> t (Quarters 1) "2008/01/01" "2008/01/01"
+-- [DateSpan 2008/01/01-2007/12/31]
+-- >>> t (Months 1) "2008/01/01" "2008/04/01"
+-- [DateSpan 2008/01,DateSpan 2008/02,DateSpan 2008/03]
+-- >>> t (Months 2) "2008/01/01" "2008/04/01"
+-- [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]
+-- >>> t (Weeks 2) "2008/01/01" "2008/01/15"
+-- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]
+-- >>> t (DayOfMonth 2) "2008/01/01" "2008/04/01"
+-- [DateSpan 2008/01/02-2008/02/01,DateSpan 2008/02/02-2008/03/01,DateSpan 2008/03/02-2008/04/01]
+-- >>> t (DayOfWeek 2) "2011/01/01" "2011/01/15"
+-- [DateSpan 2011/01/04-2011/01/10,DateSpan 2011/01/11-2011/01/17]
+--
 splitSpan :: Interval -> DateSpan -> [DateSpan]
 splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]
 splitSpan NoInterval     s = [s]
@@ -271,10 +250,10 @@
 
 -- | Parse a period expression to an Interval and overall DateSpan using
 -- the provided reference date, or return a parse error.
-parsePeriodExpr :: Day -> String -> Either ParseError (Interval, DateSpan)
+parsePeriodExpr :: Day -> Text -> Either (ParseError Char Dec) (Interval, DateSpan)
 parsePeriodExpr refdate = parsewith (periodexpr refdate <* eof)
 
-maybePeriod :: Day -> String -> Maybe (Interval,DateSpan)
+maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan)
 maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate
 
 -- | Show a DateSpan as a human-readable pseudo-period-expression string.
@@ -327,22 +306,93 @@
 
 -- | Convert a smart date string to an explicit yyyy\/mm\/dd string using
 -- the provided reference date, or raise an error.
-fixSmartDateStr :: Day -> String -> String
+fixSmartDateStr :: Day -> Text -> String
 fixSmartDateStr d s = either
                        (\e->error' $ printf "could not parse date %s %s" (show s) (show e))
                        id
-                       $ fixSmartDateStrEither d s
+                       $ (fixSmartDateStrEither d s :: Either (ParseError Char Dec) String)
 
 -- | A safe version of fixSmartDateStr.
-fixSmartDateStrEither :: Day -> String -> Either ParseError String
+fixSmartDateStrEither :: Day -> Text -> Either (ParseError Char Dec) String
 fixSmartDateStrEither d = either Left (Right . showDate) . fixSmartDateStrEither' d
 
-fixSmartDateStrEither' :: Day -> String -> Either ParseError Day
-fixSmartDateStrEither' d s = case parsewith smartdateonly (lowercase s) of
+fixSmartDateStrEither' :: Day -> Text -> Either (ParseError Char Dec) Day
+fixSmartDateStrEither' d s = case parsewith smartdateonly (T.toLower s) of
                                Right sd -> Right $ fixSmartDate d sd
                                Left e -> Left e
 
 -- | Convert a SmartDate to an absolute date using the provided reference date.
+--
+-- ==== Examples:
+-- >>> let t = fixSmartDateStr (parsedate "2008/11/26")
+-- >>> t "0000-01-01"
+-- "0000/01/01"
+-- >>> t "1999-12-02"
+-- "1999/12/02"
+-- >>> t "1999.12.02"
+-- "1999/12/02"
+-- >>> t "1999/3/2"
+-- "1999/03/02"
+-- >>> t "19990302"
+-- "1999/03/02"
+-- >>> t "2008/2"
+-- "2008/02/01"
+-- >>> t "0020/2"
+-- "0020/02/01"
+-- >>> t "1000"
+-- "1000/01/01"
+-- >>> t "4/2"
+-- "2008/04/02"
+-- >>> t "2"
+-- "2008/11/02"
+-- >>> t "January"
+-- "2008/01/01"
+-- >>> t "feb"
+-- "2008/02/01"
+-- >>> t "today"
+-- "2008/11/26"
+-- >>> t "yesterday"
+-- "2008/11/25"
+-- >>> t "tomorrow"
+-- "2008/11/27"
+-- >>> t "this day"
+-- "2008/11/26"
+-- >>> t "last day"
+-- "2008/11/25"
+-- >>> t "next day"
+-- "2008/11/27"
+-- >>> t "this week"  -- last monday
+-- "2008/11/24"
+-- >>> t "last week"  -- previous monday
+-- "2008/11/17"
+-- >>> t "next week"  -- next monday
+-- "2008/12/01"
+-- >>> t "this month"
+-- "2008/11/01"
+-- >>> t "last month"
+-- "2008/10/01"
+-- >>> t "next month"
+-- "2008/12/01"
+-- >>> t "this quarter"
+-- "2008/10/01"
+-- >>> t "last quarter"
+-- "2008/07/01"
+-- >>> t "next quarter"
+-- "2009/01/01"
+-- >>> t "this year"
+-- "2008/01/01"
+-- >>> t "last year"
+-- "2007/01/01"
+-- >>> t "next year"
+-- "2009/01/01"
+--
+-- t "last wed"
+-- "2008/11/19"
+-- t "next friday"
+-- "2008/11/28"
+-- t "next january"
+-- "2009/01/01"
+--
 fixSmartDate :: Day -> SmartDate -> Day
 fixSmartDate refdate sdate = fix sdate
     where
@@ -448,7 +498,10 @@
 -- parsedatetime s = fromMaybe (error' $ "could not parse timestamp \"" ++ s ++ "\"")
 --                             (parsedatetimeM s)
 
--- | Parse a date string to a time type, or raise an error.
+-- | Parse a YYYY-MM-DD or YYYY/MM/DD date string to a Day, or raise an error. For testing/debugging.
+--
+-- >>> parsedate "2008/02/03"
+-- 2008-02-03
 parsedate :: String -> Day
 parsedate s =  fromMaybe (error' $ "could not parse date \"" ++ s ++ "\"")
                          (parsedateM s)
@@ -471,8 +524,8 @@
 
 -- | Parse a time string to a time type using the provided pattern, or
 -- return the default.
-parsetimewith :: ParseTime t => String -> String -> t -> t
-parsetimewith pat s def = fromMaybe def $ parsetime defaultTimeLocale pat s
+_parsetimewith :: ParseTime t => String -> String -> t -> t
+_parsetimewith pat s def = fromMaybe def $ parsetime defaultTimeLocale pat s
 
 {-|
 Parse a date in any of the formats allowed in ledger's period expressions,
@@ -490,22 +543,23 @@
 Returns a SmartDate, to be converted to a full date later (see fixSmartDate).
 Assumes any text in the parse stream has been lowercased.
 -}
-smartdate :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+smartdate :: Parser SmartDate
 smartdate = do
   -- XXX maybe obscures date errors ? see ledgerdate
   (y,m,d) <- choice' [yyyymmdd, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing]
   return (y,m,d)
 
 -- | Like smartdate, but there must be nothing other than whitespace after the date.
-smartdateonly :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+smartdateonly :: Parser SmartDate
 smartdateonly = do
   d <- smartdate
   many spacenonewline
   eof
   return d
 
+datesepchars :: [Char]
 datesepchars = "/-."
-datesepchar :: Stream [Char] m Char => ParsecT [Char] st m Char
+datesepchar :: TextParser m Char
 datesepchar = oneOf datesepchars
 
 validYear, validMonth, validDay :: String -> Bool
@@ -518,54 +572,54 @@
 failIfInvalidMonth s = unless (validMonth s) $ fail $ "bad month number: " ++ s
 failIfInvalidDay s   = unless (validDay s)   $ fail $ "bad day number: " ++ s
 
-yyyymmdd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+yyyymmdd :: Parser SmartDate
 yyyymmdd = do
-  y <- count 4 digit
-  m <- count 2 digit
+  y <- count 4 digitChar
+  m <- count 2 digitChar
   failIfInvalidMonth m
-  d <- count 2 digit
+  d <- count 2 digitChar
   failIfInvalidDay d
   return (y,m,d)
 
-ymd :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+ymd :: Parser SmartDate
 ymd = do
-  y <- many1 digit
+  y <- some digitChar
   failIfInvalidYear y
   sep <- datesepchar
-  m <- many1 digit
+  m <- some digitChar
   failIfInvalidMonth m
   char sep
-  d <- many1 digit
+  d <- some digitChar
   failIfInvalidDay d
   return $ (y,m,d)
 
-ym :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+ym :: Parser SmartDate
 ym = do
-  y <- many1 digit
+  y <- some digitChar
   failIfInvalidYear y
   datesepchar
-  m <- many1 digit
+  m <- some digitChar
   failIfInvalidMonth m
   return (y,m,"")
 
-y :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+y :: Parser SmartDate
 y = do
-  y <- many1 digit
+  y <- some digitChar
   failIfInvalidYear y
   return (y,"","")
 
-d :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+d :: Parser SmartDate
 d = do
-  d <- many1 digit
+  d <- some digitChar
   failIfInvalidDay d
   return ("","",d)
 
-md :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+md :: Parser SmartDate
 md = do
-  m <- many1 digit
+  m <- some digitChar
   failIfInvalidMonth m
   datesepchar
-  d <- many1 digit
+  d <- some digitChar
   failIfInvalidDay d
   return ("",m,d)
 
@@ -578,24 +632,24 @@
 monthIndex s = maybe 0 (+1) $ lowercase s `elemIndex` months
 monIndex s   = maybe 0 (+1) $ lowercase s `elemIndex` monthabbrevs
 
-month :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+month :: Parser SmartDate
 month = do
   m <- choice $ map (try . string) months
   let i = monthIndex m
   return ("",show i,"")
 
-mon :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+mon :: Parser SmartDate
 mon = do
   m <- choice $ map (try . string) monthabbrevs
   let i = monIndex m
   return ("",show i,"")
 
-today,yesterday,tomorrow :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+today,yesterday,tomorrow :: Parser SmartDate
 today     = string "today"     >> return ("","","today")
 yesterday = string "yesterday" >> return ("","","yesterday")
 tomorrow  = string "tomorrow"  >> return ("","","tomorrow")
 
-lastthisnextthing :: Stream [Char] m Char => ParsecT [Char] st m SmartDate
+lastthisnextthing :: Parser SmartDate
 lastthisnextthing = do
   r <- choice [
         string "last"
@@ -615,7 +669,19 @@
 
   return ("",r,p)
 
-periodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan)
+-- |
+-- >>> let p = parsewith (periodexpr (parsedate "2008/11/26")) :: T.Text -> Either (ParseError Char Dec) (Interval, DateSpan)
+-- >>> p "from aug to oct"
+-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
+-- >>> p "aug to oct"
+-- 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-)
+-- >>> p "every week to 2009"
+-- Right (Weeks 1,DateSpan -2008/12/31)
+periodexpr :: Day -> Parser (Interval, DateSpan)
 periodexpr rdate = choice $ map try [
                     intervalanddateperiodexpr rdate,
                     intervalperiodexpr,
@@ -623,7 +689,7 @@
                     (return (NoInterval,DateSpan Nothing Nothing))
                    ]
 
-intervalanddateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan)
+intervalanddateperiodexpr :: Day -> Parser (Interval, DateSpan)
 intervalanddateperiodexpr rdate = do
   many spacenonewline
   i <- reportinginterval
@@ -631,20 +697,20 @@
   s <- periodexprdatespan rdate
   return (i,s)
 
-intervalperiodexpr :: Stream [Char] m Char => ParsecT [Char] st m (Interval, DateSpan)
+intervalperiodexpr :: Parser (Interval, DateSpan)
 intervalperiodexpr = do
   many spacenonewline
   i <- reportinginterval
   return (i, DateSpan Nothing Nothing)
 
-dateperiodexpr :: Stream [Char] m Char => Day -> ParsecT [Char] st m (Interval, DateSpan)
+dateperiodexpr :: Day -> Parser (Interval, DateSpan)
 dateperiodexpr rdate = do
   many spacenonewline
   s <- periodexprdatespan rdate
   return (NoInterval, s)
 
 -- Parse a reporting interval.
-reportinginterval :: Stream [Char] m Char => ParsecT [Char] st m Interval
+reportinginterval :: Parser Interval
 reportinginterval = choice' [
                        tryinterval "day"     "daily"     Days,
                        tryinterval "week"    "weekly"    Weeks,
@@ -657,7 +723,7 @@
                           return $ Months 2,
                        do string "every"
                           many spacenonewline
-                          n <- fmap read $ many1 digit
+                          n <- fmap read $ some digitChar
                           thsuffix
                           many spacenonewline
                           string "day"
@@ -668,7 +734,7 @@
                           return $ DayOfWeek n,
                        do string "every"
                           many spacenonewline
-                          n <- fmap read $ many1 digit
+                          n <- fmap read $ some digitChar
                           thsuffix
                           many spacenonewline
                           string "day"
@@ -684,7 +750,7 @@
       thsuffix = choice' $ map string ["st","nd","rd","th"]
 
       -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".
-      tryinterval :: Stream [Char] m Char => String -> String -> (Int -> Interval) -> ParsecT [Char] st m Interval
+      tryinterval :: String -> String -> (Int -> Interval) -> Parser Interval
       tryinterval singular compact intcons =
           choice' [
            do string compact
@@ -695,14 +761,14 @@
               return $ intcons 1,
            do string "every"
               many spacenonewline
-              n <- fmap read $ many1 digit
+              n <- fmap read $ some digitChar
               many spacenonewline
               string plural
               return $ intcons n
            ]
           where plural = singular ++ "s"
 
-periodexprdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan
+periodexprdatespan :: Day -> Parser DateSpan
 periodexprdatespan rdate = choice $ map try [
                             doubledatespan rdate,
                             fromdatespan rdate,
@@ -710,7 +776,7 @@
                             justdatespan rdate
                            ]
 
-doubledatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan
+doubledatespan :: Day -> Parser DateSpan
 doubledatespan rdate = do
   optional (string "from" >> many spacenonewline)
   b <- smartdate
@@ -719,7 +785,7 @@
   e <- smartdate
   return $ DateSpan (Just $ fixSmartDate rdate b) (Just $ fixSmartDate rdate e)
 
-fromdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan
+fromdatespan :: Day -> Parser DateSpan
 fromdatespan rdate = do
   b <- choice [
     do
@@ -733,13 +799,13 @@
     ]
   return $ DateSpan (Just $ fixSmartDate rdate b) Nothing
 
-todatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan
+todatespan :: Day -> Parser DateSpan
 todatespan rdate = do
   choice [string "to", string "-"] >> many spacenonewline
   e <- smartdate
   return $ DateSpan Nothing (Just $ fixSmartDate rdate e)
 
-justdatespan :: Stream [Char] m Char => Day -> ParsecT [Char] st m DateSpan
+justdatespan :: Day -> Parser DateSpan
 justdatespan rdate = do
   optional (string "in" >> many spacenonewline)
   d <- smartdate
@@ -754,103 +820,4 @@
 nulldatespan = DateSpan Nothing Nothing
 
 nulldate :: Day
-nulldate = parsedate "0000/01/01"
-
-tests_Hledger_Data_Dates = TestList
- [
-
-   "parsedate" ~: do
-    let date1 = parsedate "2008/11/26"
-    parsedate "2008/02/03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1
-    parsedate "2008-02-03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1
-
-  ,"period expressions" ~: do
-    let todaysdate = parsedate "2008/11/26"
-    let str `gives` result = show (parsewith (periodexpr todaysdate) str) `is` ("Right " ++ result)
-    "from aug to oct"           `gives` "(NoInterval,DateSpan \"Just 2008-08-01\" \"Just 2008-10-01\")"
-    "aug to oct"                `gives` "(NoInterval,DateSpan \"Just 2008-08-01\" \"Just 2008-10-01\")"
-    "every 3 days in aug"       `gives` "(Days 3,DateSpan \"Just 2008-08-01\" \"Just 2008-09-01\")"
-    "daily from aug"            `gives` "(Days 1,DateSpan \"Just 2008-08-01\" \"Nothing\")"
-    "every week to 2009"        `gives` "(Weeks 1,DateSpan \"Nothing\" \"Just 2009-01-01\")"
-
-  ,"splitSpan" ~: do
-    let gives (interval, span) = (splitSpan interval span `is`)
-    (NoInterval,mkdatespan "2008/01/01" "2009/01/01") `gives`
-     [mkdatespan "2008/01/01" "2009/01/01"]
-    (Quarters 1,mkdatespan "2008/01/01" "2009/01/01") `gives`
-     [mkdatespan "2008/01/01" "2008/04/01"
-     ,mkdatespan "2008/04/01" "2008/07/01"
-     ,mkdatespan "2008/07/01" "2008/10/01"
-     ,mkdatespan "2008/10/01" "2009/01/01"
-     ]
-    (Quarters 1,nulldatespan) `gives`
-     [nulldatespan]
-    (Days 1,mkdatespan "2008/01/01" "2008/01/01") `gives`
-     [mkdatespan "2008/01/01" "2008/01/01"]
-    (Quarters 1,mkdatespan "2008/01/01" "2008/01/01") `gives`
-     [mkdatespan "2008/01/01" "2008/01/01"]
-    (Months 1,mkdatespan "2008/01/01" "2008/04/01") `gives`
-     [mkdatespan "2008/01/01" "2008/02/01"
-     ,mkdatespan "2008/02/01" "2008/03/01"
-     ,mkdatespan "2008/03/01" "2008/04/01"
-     ]
-    (Months 2,mkdatespan "2008/01/01" "2008/04/01") `gives`
-     [mkdatespan "2008/01/01" "2008/03/01"
-     ,mkdatespan "2008/03/01" "2008/05/01"
-     ]
-    (Weeks 1,mkdatespan "2008/01/01" "2008/01/15") `gives`
-     [mkdatespan "2007/12/31" "2008/01/07"
-     ,mkdatespan "2008/01/07" "2008/01/14"
-     ,mkdatespan "2008/01/14" "2008/01/21"
-     ]
-    (Weeks 2,mkdatespan "2008/01/01" "2008/01/15") `gives`
-     [mkdatespan "2007/12/31" "2008/01/14"
-     ,mkdatespan "2008/01/14" "2008/01/28"
-     ]
-    (DayOfMonth 2,mkdatespan "2008/01/01" "2008/04/01") `gives`
-     [mkdatespan "2008/01/02" "2008/02/02"
-     ,mkdatespan "2008/02/02" "2008/03/02"
-     ,mkdatespan "2008/03/02" "2008/04/02"
-     ]
-    (DayOfWeek 2,mkdatespan "2011/01/01" "2011/01/15") `gives`
-     [mkdatespan "2011/01/04" "2011/01/11"
-     ,mkdatespan "2011/01/11" "2011/01/18"
-     ]
-
-  ,"fixSmartDateStr" ~: do
-    let gives = is . fixSmartDateStr (parsedate "2008/11/26")
-    "0000-01-01"   `gives` "0000/01/01"
-    "1999-12-02"   `gives` "1999/12/02"
-    "1999.12.02"   `gives` "1999/12/02"
-    "1999/3/2"     `gives` "1999/03/02"
-    "19990302"     `gives` "1999/03/02"
-    "2008/2"       `gives` "2008/02/01"
-    "0020/2"       `gives` "0020/02/01"
-    "1000"         `gives` "1000/01/01"
-    "4/2"          `gives` "2008/04/02"
-    "2"            `gives` "2008/11/02"
-    "January"      `gives` "2008/01/01"
-    "feb"          `gives` "2008/02/01"
-    "today"        `gives` "2008/11/26"
-    "yesterday"    `gives` "2008/11/25"
-    "tomorrow"     `gives` "2008/11/27"
-    "this day"     `gives` "2008/11/26"
-    "last day"     `gives` "2008/11/25"
-    "next day"     `gives` "2008/11/27"
-    "this week"    `gives` "2008/11/24" -- last monday
-    "last week"    `gives` "2008/11/17" -- previous monday
-    "next week"    `gives` "2008/12/01" -- next monday
-    "this month"   `gives` "2008/11/01"
-    "last month"   `gives` "2008/10/01"
-    "next month"   `gives` "2008/12/01"
-    "this quarter" `gives` "2008/10/01"
-    "last quarter" `gives` "2008/07/01"
-    "next quarter" `gives` "2009/01/01"
-    "this year"    `gives` "2008/01/01"
-    "last year"    `gives` "2007/01/01"
-    "next year"    `gives` "2009/01/01"
---     "last wed"     `gives` "2008/11/19"
---     "next friday"  `gives` "2008/11/28"
---     "next january" `gives` "2009/01/01"
-
- ]
+nulldate = fromGregorian 0 1 1
diff --git a/Hledger/Data/Journal.hs b/Hledger/Data/Journal.hs
--- a/Hledger/Data/Journal.hs
+++ b/Hledger/Data/Journal.hs
@@ -1,4 +1,4 @@
--- {-# LANGUAGE CPP #-}
+{-# LANGUAGE StandaloneDeriving, OverloadedStrings #-}
 {-|
 
 A 'Journal' is a set of transactions, plus optional related data.  This is
@@ -12,11 +12,11 @@
   addMarketPrice,
   addModifierTransaction,
   addPeriodicTransaction,
-  addTimeLogEntry,
   addTransaction,
   journalApplyAliases,
   journalBalanceTransactions,
-  journalCanonicaliseAmounts,
+  journalApplyCommodityStyles,
+  commodityStylesFromAmounts,
   journalConvertAmountsToCost,
   journalFinalise,
   -- * Filtering
@@ -24,6 +24,7 @@
   filterJournalPostings,
   filterJournalAmounts,
   filterTransactionAmounts,
+  filterTransactionPostings,
   filterPostingAmount,
   -- * Querying
   journalAccountNames,
@@ -49,10 +50,12 @@
   journalEquityAccountQuery,
   journalCashAccountQuery,
   -- * Misc
-  canonicalStyles,
+  canonicalStyleFrom,
   matchpats,
-  nullctx,
   nulljournal,
+  journalCheckBalanceAssertions,
+  journalNumberAndTieTransactions,
+  journalUntieTransactions,
   -- * Tests
   samplejournal,
   tests_Hledger_Data_Journal,
@@ -62,12 +65,13 @@
 import Data.List
 -- import Data.Map (findWithDefault)
 import Data.Maybe
+import Data.Monoid
 import Data.Ord
-import Safe (headMay)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Safe (headMay, headDef)
 import Data.Time.Calendar
-import Data.Time.LocalTime
 import Data.Tree
-import Safe (headDef)
 import System.Time (ClockTime(TOD))
 import Test.HUnit
 import Text.Printf
@@ -81,10 +85,14 @@
 import Hledger.Data.Dates
 import Hledger.Data.Transaction
 import Hledger.Data.Posting
-import Hledger.Data.TimeLog
 import Hledger.Query
 
 
+-- try to make Journal ppShow-compatible
+-- instance Show ClockTime where
+--   show t = "<ClockTime>"
+-- deriving instance Show Journal
+
 instance Show Journal where
   show j
     | debugLevel < 3 = printf "Journal %s with %d transactions, %d accounts"
@@ -107,7 +115,7 @@
               length (jperiodictxns j))
              (length accounts)
              (show accounts)
-             (show $ jcommoditystyles j)
+             (show $ jinferredcommodities j)
              -- ++ (show $ journalTransactions l)
              where accounts = filter (/= "root") $ flatten $ journalAccountNameTree j
 
@@ -116,37 +124,73 @@
 --                      ,show (jtxns j)
 --                      ,show (jmodifiertxns j)
 --                      ,show (jperiodictxns j)
---                      ,show $ open_timelog_entries j
+--                      ,show $ jparsetimeclockentries j
 --                      ,show $ jmarketprices j
---                      ,show $ final_comment_lines j
---                      ,show $ jContext j
---                      ,show $ map fst $ files j
+--                      ,show $ jfinalcommentlines j
+--                      ,show $ jparsestate j
+--                      ,show $ map fst $ jfiles j
 --                      ]
 
-nulljournal :: Journal
-nulljournal = Journal { jmodifiertxns = []
-                      , jperiodictxns = []
-                      , jtxns = []
-                      , open_timelog_entries = []
-                      , jmarketprices = []
-                      , final_comment_lines = []
-                      , jContext = nullctx
-                      , files = []
-                      , filereadtime = TOD 0 0
-                      , jcommoditystyles = M.fromList []
-                      }
+-- The monoid instance for Journal is useful for two situations.
+-- 
+-- 1. concatenating finalised journals, eg with multiple -f options:
+-- FIRST <> SECOND. The second's list fields are appended to the
+-- first's, map fields are combined, transaction counts are summed,
+-- the parse state of the second is kept.
+-- 
+-- 2. merging a child parsed journal, eg with the include directive:
+-- CHILD <> PARENT. A parsed journal's data is in reverse order, so
+-- this gives what we want.
+--
+instance Monoid Journal where
+  mempty = nulljournal
+  mappend j1 j2 = Journal {
+     jparsedefaultyear          = jparsedefaultyear          j2
+    ,jparsedefaultcommodity     = jparsedefaultcommodity     j2
+    ,jparseparentaccounts       = jparseparentaccounts       j2
+    ,jparsealiases              = jparsealiases              j2
+    -- ,jparsetransactioncount     = jparsetransactioncount     j1 +  jparsetransactioncount     j2
+    ,jparsetimeclockentries = jparsetimeclockentries j1 <> jparsetimeclockentries j2
+    ,jaccounts                  = jaccounts                  j1 <> jaccounts                  j2
+    ,jcommodities               = jcommodities               j1 <> jcommodities               j2
+    ,jinferredcommodities       = jinferredcommodities       j1 <> jinferredcommodities       j2
+    ,jmarketprices              = jmarketprices              j1 <> jmarketprices              j2
+    ,jmodifiertxns              = jmodifiertxns              j1 <> jmodifiertxns              j2
+    ,jperiodictxns              = jperiodictxns              j1 <> jperiodictxns              j2
+    ,jtxns                      = jtxns                      j1 <> jtxns                      j2
+    ,jfinalcommentlines         = jfinalcommentlines         j2
+    ,jfiles                     = jfiles                     j1 <> jfiles                     j2
+    ,jlastreadtime              = max (jlastreadtime j1) (jlastreadtime j2)
+    }
 
-nullctx :: JournalContext
-nullctx = Ctx{ctxYear=Nothing, ctxDefaultCommodityAndStyle=Nothing, ctxAccount=[], ctxAliases=[], ctxTransactionIndex=0}
+nulljournal :: Journal
+nulljournal = Journal {
+   jparsedefaultyear          = Nothing
+  ,jparsedefaultcommodity     = Nothing
+  ,jparseparentaccounts       = []
+  ,jparsealiases              = []
+  -- ,jparsetransactioncount     = 0
+  ,jparsetimeclockentries = []
+  ,jaccounts                  = []
+  ,jcommodities               = M.fromList []
+  ,jinferredcommodities       = M.fromList []
+  ,jmarketprices              = []
+  ,jmodifiertxns              = []
+  ,jperiodictxns              = []
+  ,jtxns                      = []
+  ,jfinalcommentlines         = ""
+  ,jfiles                     = []
+  ,jlastreadtime              = TOD 0 0
+  }
 
 journalFilePath :: Journal -> FilePath
 journalFilePath = fst . mainfile
 
 journalFilePaths :: Journal -> [FilePath]
-journalFilePaths = map fst . files
+journalFilePaths = map fst . jfiles
 
-mainfile :: Journal -> (FilePath, String)
-mainfile = headDef ("", "") . files
+mainfile :: Journal -> (FilePath, Text)
+mainfile = headDef ("", "") . jfiles
 
 addTransaction :: Transaction -> Journal -> Journal
 addTransaction t j = j { jtxns = t : jtxns j }
@@ -160,9 +204,6 @@
 addMarketPrice :: MarketPrice -> Journal -> Journal
 addMarketPrice h j = j { jmarketprices = h : jmarketprices j }
 
-addTimeLogEntry :: TimeLogEntry -> Journal -> Journal
-addTimeLogEntry tle j = j { open_timelog_entries = tle : open_timelog_entries j }
-
 -- | Get the transaction with this index (its 1-based position in the input stream), if any.
 journalTransactionAt :: Journal -> Integer -> Maybe Transaction
 journalTransactionAt Journal{jtxns=ts} i =
@@ -172,13 +213,13 @@
 -- | Get the transaction that appeared immediately after this one in the input stream, if any.
 journalNextTransaction :: Journal -> Transaction -> Maybe Transaction
 journalNextTransaction j t = journalTransactionAt j (tindex t + 1)
-  
+
 -- | Get the transaction that appeared immediately before this one in the input stream, if any.
 journalPrevTransaction :: Journal -> Transaction -> Maybe Transaction
 journalPrevTransaction j t = journalTransactionAt j (tindex t - 1)
-  
+
 -- | Unique transaction descriptions used in this journal.
-journalDescriptions :: Journal -> [String]
+journalDescriptions :: Journal -> [Text]
 journalDescriptions = nub . sort . map tdescription . jtxns
 
 -- | All postings from this journal's transactions, in order.
@@ -258,9 +299,7 @@
 -- | Keep only postings matching the query expression.
 -- This can leave unbalanced transactions.
 filterJournalPostings :: Query -> Journal -> Journal
-filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map filtertransactionpostings ts}
-    where
-      filtertransactionpostings t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps}
+filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionPostings q) ts}
 
 -- | Within each posting's amount, keep only the parts matching the query.
 -- This can leave unbalanced transactions.
@@ -276,6 +315,10 @@
 filterPostingAmount :: Query -> Posting -> Posting
 filterPostingAmount q p@Posting{pamount=Mixed as} = p{pamount=Mixed $ filter (q `matchesAmount`) as}
 
+filterTransactionPostings :: Query -> Transaction -> Transaction
+filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps}
+
+
 {-
 -------------------------------------------------------------------------------
 -- filtering V1
@@ -414,25 +457,40 @@
       dotransaction t@Transaction{tpostings=ps} = t{tpostings=map doposting ps}
       doposting p@Posting{paccount=a} = p{paccount= accountNameApplyAliases aliases a}
 
--- | Do post-parse processing on a journal to make it ready for use: check
--- all transactions balance, canonicalise amount formats, close any open
--- timelog entries, maybe check balance assertions and so on.
-journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Bool -> Journal -> Either String Journal
-journalFinalise tclock tlocal path txt ctx assrt j@Journal{files=fs} = do
-  (journalBalanceTransactions $
-    journalCanonicaliseAmounts $
-    journalCloseTimeLogEntries tlocal $
-    j{ files=(path,txt):fs
-     , filereadtime=tclock
-     , jContext=ctx
-     , jtxns=reverse $ jtxns j -- NOTE: see addTransaction
-     , jmodifiertxns=reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction
-     , jperiodictxns=reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction
-     , jmarketprices=reverse $ jmarketprices j -- NOTE: see addMarketPrice
-     , open_timelog_entries=reverse $ open_timelog_entries j -- NOTE: see addTimeLogEntry
-     })
+-- | Do post-parse processing on a parsed journal to make it ready for
+-- use.  Reverse parsed data to normal order, canonicalise amount
+-- formats, check/ensure that transactions are balanced, and maybe
+-- check balance assertions.
+journalFinalise :: ClockTime -> FilePath -> Text -> Bool -> ParsedJournal -> Either String Journal
+journalFinalise t path txt assrt j@Journal{jfiles=fs} = do
+  (journalNumberAndTieTransactions <$>
+    (journalBalanceTransactions $
+    journalApplyCommodityStyles $
+    j{ jfiles        = (path,txt) : reverse fs
+     , jlastreadtime = t
+     , jtxns         = reverse $ jtxns j -- NOTE: see addTransaction
+     , jmodifiertxns = reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction
+     , jperiodictxns = reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction
+     , jmarketprices = reverse $ jmarketprices j -- NOTE: see addMarketPrice
+     }))
   >>= if assrt then journalCheckBalanceAssertions else return
 
+journalNumberAndTieTransactions = journalTieTransactions . journalNumberTransactions
+
+-- | Number (set the tindex field) this journal's transactions, counting upward from 1.
+journalNumberTransactions :: Journal -> Journal
+journalNumberTransactions j@Journal{jtxns=ts} = j{jtxns=map (\(i,t) -> t{tindex=i}) $ zip [1..] ts}
+
+-- | Tie the knot in all of this journal's transactions, ensuring their postings
+-- refer to them. This should be done last, after any other transaction-modifying operations.
+journalTieTransactions :: Journal -> Journal
+journalTieTransactions j@Journal{jtxns=ts} = j{jtxns=map txnTieKnot ts}
+
+-- | Untie all transaction-posting knots in this journal, so that eg
+-- recursiveSize and GHCI's :sprint can work on it.
+journalUntieTransactions :: Transaction -> Transaction
+journalUntieTransactions t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}
+
 -- | Check any balance assertions in the journal and return an error
 -- message if any of them fail.
 journalCheckBalanceAssertions :: Journal -> Either String Journal
@@ -462,26 +520,47 @@
 checkBalanceAssertion :: ([String],MixedAmount) -> [Posting] -> ([String],MixedAmount)
 checkBalanceAssertion (errs,startbal) ps
   | null ps = (errs,startbal)
-  | isNothing assertion = (errs,startbal)
-  | -- bal' /= assertedbal  -- MixedAmount's Eq instance currently gets confused by different precisions
-    not $ isReallyZeroMixedAmount (bal - assertedbal) = (errs++[err], fullbal)
-  | otherwise = (errs,fullbal)
+  | isNothing $ pbalanceassertion p = (errs,startbal)
+  | iswrong = (errs++[err], finalfullbal)
+  | otherwise = (errs,finalfullbal)
   where
     p = last ps
-    assertion = pbalanceassertion p
-    Just assertedbal = dbg2 "assertedbal" assertion
-    assertedcomm = dbg2 "assertedcomm" $ maybe "" acommodity $ headMay $ amounts assertedbal
-    fullbal = dbg2 "fullbal" $ sum $ [dbg2 "startbal" startbal] ++ map pamount ps
-    singlebal = dbg2 "singlebal" $ filterMixedAmount (\a -> acommodity a == assertedcomm) fullbal
-    bal = singlebal -- check single-commodity balance like Ledger; maybe add == FULLBAL later
-    err = printf "Balance assertion failed for account %s on %s\n%sAfter posting:\n   %s\nexpected balance in commodity \"%s\" is %s, calculated balance was %s."
-                 (paccount p)
-                 (show $ postingDate p)
-                 (maybe "" (("In transaction:\n"++).show) $ ptransaction p)
-                 (show p)
+    Just assertedbal = pbalanceassertion p
+    assertedcomm = maybe "" acommodity $ headMay $ amounts assertedbal
+    finalfullbal = sum $ [startbal] ++ map pamount (dbg2 "ps" ps)
+    finalsinglebal = filterMixedAmount (\a -> acommodity a == assertedcomm) finalfullbal
+    actualbal = finalsinglebal -- just check the single-commodity balance, like Ledger; maybe add ==FULLBAL later
+    iswrong = dbg2 debugmsg $
+      not (isReallyZeroMixedAmount (actualbal - assertedbal))
+      -- bal' /= assertedbal  -- MixedAmount's Eq instance currently gets confused by different precisions
+      where
+        debugmsg = "assertions: on " ++ show (postingDate p) ++ " balance of " ++ show assertedcomm
+                    ++ " in " ++ T.unpack (paccount p) ++ " should be " ++ show assertedbal
+    diff = assertedbal - actualbal
+    diffplus | isNegativeMixedAmount diff == Just False = "+"
+             | otherwise = ""
+    err = printf (unlines [
+                      "balance assertion error%s",
+                      "after posting:",
+                      "%s",
+                      "balance assertion details:",
+                      "date:       %s",
+                      "account:    %s",
+                      "commodity:  %s",
+                      "calculated: %s",
+                      "asserted:   %s (difference: %s)"
+                      ])
+                 (case ptransaction p of
+                    Nothing -> ":" -- shouldn't happen
+                    Just t ->  printf " in \"%s\" (line %d, column %d):\nin transaction:\n%s" f l c (chomp $ show t) :: String
+                      where GenericSourcePos f l c = tsourcepos t)
+                 (showPostingLine p)
+                 (showDate $ postingDate p)
+                 (T.unpack $ paccount p) -- XXX pack
                  assertedcomm
+                 (showMixedAmount finalsinglebal)
                  (showMixedAmount assertedbal)
-                 (showMixedAmount singlebal)
+                 (diffplus ++ showMixedAmount diff)
 
 -- Given a sequence of postings to a single account, split it into
 -- sub-sequences consisting of ordinary postings followed by a single
@@ -496,55 +575,77 @@
 
 -- | Fill in any missing amounts and check that all journal transactions
 -- balance, or return an error message. This is done after parsing all
--- amounts and working out the canonical commodities, since balancing
+-- amounts and applying canonical commodity styles, since balancing
 -- depends on display precision. Reports only the first error encountered.
 journalBalanceTransactions :: Journal -> Either String Journal
-journalBalanceTransactions j@Journal{jtxns=ts, jcommoditystyles=ss} =
-  case sequence $ map balance ts of Right ts' -> Right j{jtxns=map txnTieKnot ts'}
+journalBalanceTransactions j@Journal{jtxns=ts, jinferredcommodities=ss} =
+  case sequence $ map balance ts of Right ts' -> Right j{jtxns=ts'}
                                     Left e    -> Left e
       where balance = balanceTransaction (Just ss)
 
--- | Convert all the journal's posting amounts (and historical price
--- amounts, but currently not transaction price amounts) to their
--- canonical display settings. Ie, all amounts in a given commodity
--- will use (a) the display settings of the first, and (b) the
--- greatest precision, of the posting amounts in that commodity.
-journalCanonicaliseAmounts :: Journal -> Journal
-journalCanonicaliseAmounts j@Journal{jtxns=ts, jmarketprices=mps} = j''
+-- | 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.
+journalApplyCommodityStyles :: Journal -> Journal
+journalApplyCommodityStyles j@Journal{jtxns=ts, jmarketprices=mps} = j''
     where
+      j' = journalInferCommodityStyles j
       j'' = j'{jtxns=map fixtransaction ts, jmarketprices=map fixmarketprice mps}
-      j' = j{jcommoditystyles = canonicalStyles $ dbg8 "journalAmounts" $ journalAmounts j}
       fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps}
       fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a}
       fixmarketprice mp@MarketPrice{mpamount=a} = mp{mpamount=fixamount a}
       fixmixedamount (Mixed as) = Mixed $ map fixamount as
       fixamount a@Amount{acommodity=c} = a{astyle=journalCommodityStyle j' c}
 
--- | Given a list of amounts in parse order, build a map from commodities
--- to canonical display styles for amounts in that commodity.
-canonicalStyles :: [Amount] -> M.Map Commodity AmountStyle
-canonicalStyles amts = M.fromList commstyles
+-- | Get this journal's standard display style for the given
+-- commodity.  That is the style defined by the last corresponding
+-- commodity format directive if any, otherwise the style inferred
+-- from the posting amounts (or in some cases, price amounts) in this
+-- commodity if any, otherwise the default style.
+journalCommodityStyle :: Journal -> CommoditySymbol -> AmountStyle
+journalCommodityStyle j c =
+  headDef amountstyle{asprecision=2} $
+  catMaybes [
+     M.lookup c (jcommodities j) >>= cformat
+    ,M.lookup c $ jinferredcommodities j
+    ]
+
+-- | Infer a display format for each commodity based on the amounts parsed.
+-- "hledger... will use the format of the first posting amount in the
+-- commodity, and the highest precision of all posting amounts in the commodity."
+journalInferCommodityStyles :: Journal -> Journal
+journalInferCommodityStyles j =
+  j{jinferredcommodities =
+        commodityStylesFromAmounts $
+        dbg8 "journalChooseCommmodityStyles using amounts" $ journalAmounts j}
+
+-- | Given a list of amounts in parse order, build a map from their commodity names
+-- to standard commodity display formats.
+commodityStylesFromAmounts :: [Amount] -> M.Map CommoditySymbol AmountStyle
+commodityStylesFromAmounts amts = M.fromList commstyles
   where
     samecomm = \a1 a2 -> acommodity a1 == acommodity a2
     commamts = [(acommodity $ head as, as) | as <- groupBy samecomm $ sortBy (comparing acommodity) amts]
     commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts]
 
--- Given an ordered list of amount styles for a commodity, build a canonical style.
+-- | Given an ordered list of amount styles, choose a canonical style.
+-- That is: the style of the first, and the
+-- maximum precision of all.
 canonicalStyleFrom :: [AmountStyle] -> AmountStyle
 canonicalStyleFrom [] = amountstyle
 canonicalStyleFrom ss@(first:_) =
   first{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
   where
-    -- precision is the maximum of all precisions seen
+    mgrps = maybe Nothing Just $ headMay $ catMaybes $ map asdigitgroups ss
+    -- precision is maximum of all precisions
     prec = maximum $ map asprecision ss
-    -- find the first decimal point and the first digit group style seen,
-    -- or use defaults.
     mdec  = Just $ headDef '.' $ catMaybes $ map asdecimalpoint ss
-    mgrps = maybe Nothing Just $ headMay $ catMaybes $ map asdigitgroups ss
-
--- | Get this journal's canonical amount style for the given commodity, or the null style.
-journalCommodityStyle :: Journal -> Commodity -> AmountStyle
-journalCommodityStyle j c = M.findWithDefault amountstyle c $ jcommoditystyles j
+    -- precision is that of first amount with a decimal point
+    -- (mdec, prec) =
+    --   case filter (isJust . asdecimalpoint) ss of
+    --   (s:_) -> (asdecimalpoint s, asprecision s)
+    --   []    -> (Just '.', 0)
 
 -- -- | Apply this journal's historical price records to unpriced amounts where possible.
 -- journalApplyMarketPrices :: Journal -> Journal
@@ -560,41 +661,36 @@
 
 -- -- | Get the price for a commodity on the specified day from the price database, if known.
 -- -- Does only one lookup step, ie will not look up the price of a price.
--- journalMarketPriceFor :: Journal -> Day -> Commodity -> Maybe MixedAmount
--- journalMarketPriceFor j d Commodity{symbol=s} = do
+-- journalMarketPriceFor :: Journal -> Day -> CommoditySymbol -> Maybe MixedAmount
+-- journalMarketPriceFor j d CommoditySymbol{symbol=s} = do
 --   let ps = reverse $ filter ((<= d).mpdate) $ filter ((s==).hsymbol) $ sortBy (comparing mpdate) $ jmarketprices j
 --   case ps of (MarketPrice{mpamount=a}:_) -> Just a
 --              _ -> Nothing
 
--- | Close any open timelog sessions in this journal using the provided current time.
-journalCloseTimeLogEntries :: LocalTime -> Journal -> Journal
-journalCloseTimeLogEntries now j@Journal{jtxns=ts, open_timelog_entries=es} =
-  j{jtxns = ts ++ (timeLogEntriesToTransactions now es), open_timelog_entries = []}
-
 -- | Convert all this journal's amounts to cost by applying their prices, if any.
 journalConvertAmountsToCost :: Journal -> Journal
 journalConvertAmountsToCost j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts}
     where
-      -- similar to journalCanonicaliseAmounts
+      -- similar to journalApplyCommodityStyles
       fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps}
       fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a}
       fixmixedamount (Mixed as) = Mixed $ map fixamount as
-      fixamount = canonicaliseAmount (jcommoditystyles j) . costOfAmount
+      fixamount = canonicaliseAmount (jinferredcommodities j) . costOfAmount
 
 -- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol.
--- journalCanonicalCommodities :: Journal -> M.Map String Commodity
+-- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol
 -- journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j
 
 -- -- | Get all this journal's amounts' commodities, in the order parsed.
--- journalAmountCommodities :: Journal -> [Commodity]
+-- journalAmountCommodities :: Journal -> [CommoditySymbol]
 -- journalAmountCommodities = map acommodity . concatMap amounts . journalAmounts
 
 -- -- | Get all this journal's amount and price commodities, in the order parsed.
--- journalAmountAndPriceCommodities :: Journal -> [Commodity]
+-- journalAmountAndPriceCommodities :: Journal -> [CommoditySymbol]
 -- journalAmountAndPriceCommodities = concatMap amountCommodities . concatMap amounts . journalAmounts
 
 -- -- | Get this amount's commodity and any commodities referenced in its price.
--- amountCommodities :: Amount -> [Commodity]
+-- amountCommodities :: Amount -> [CommoditySymbol]
 -- amountCommodities Amount{acommodity=c,aprice=p} =
 --     case p of Nothing -> [c]
 --               Just (UnitPrice ma)  -> c:(concatMap amountCommodities $ amounts ma)
diff --git a/Hledger/Data/Ledger.hs b/Hledger/Data/Ledger.hs
--- a/Hledger/Data/Ledger.hs
+++ b/Hledger/Data/Ledger.hs
@@ -10,6 +10,8 @@
 module Hledger.Data.Ledger
 where
 import qualified Data.Map as M
+-- import Data.Text (Text)
+import qualified Data.Text as T
 import Safe (headDef)
 import Test.HUnit
 import Text.Printf
@@ -37,7 +39,7 @@
 
 -- | Filter a journal's transactions with the given query, then derive
 -- a ledger containing the chart of accounts and balances. If the
--- query includes a depth limit, that will affect the this ledger's
+-- query includes a depth limit, that will affect the ledger's
 -- journal but not the ledger's account tree.
 ledgerFromJournal :: Query -> Journal -> Ledger
 ledgerFromJournal q j = nullledger{ljournal=j'', laccounts=as}
@@ -72,7 +74,7 @@
 
 -- | Accounts in ledger whose name matches the pattern, in tree order.
 ledgerAccountsMatching :: [String] -> Ledger -> [Account]
-ledgerAccountsMatching pats = filter (matchpats pats . aname) . laccounts
+ledgerAccountsMatching pats = filter (matchpats pats . T.unpack . aname) . laccounts -- XXX pack
 
 -- | List a ledger's postings, in the order parsed.
 ledgerPostings :: Ledger -> [Posting]
@@ -84,8 +86,8 @@
 ledgerDateSpan = postingsDateSpan . ledgerPostings
 
 -- | All commodities used in this ledger.
-ledgerCommodities :: Ledger -> [Commodity]
-ledgerCommodities = M.keys . jcommoditystyles . ljournal
+ledgerCommodities :: Ledger -> [CommoditySymbol]
+ledgerCommodities = M.keys . jinferredcommodities . ljournal
 
 
 tests_ledgerFromJournal = [
diff --git a/Hledger/Data/Period.hs b/Hledger/Data/Period.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Data/Period.hs
@@ -0,0 +1,262 @@
+{-|
+
+Manipulate the time periods typically used for reports with Period,
+a richer abstraction than DateSpan. See also Types and Dates.
+
+-}
+
+module Hledger.Data.Period
+where
+
+import Data.Time.Calendar
+import Data.Time.Calendar.MonthDay
+import Data.Time.Calendar.OrdinalDate
+import Data.Time.Calendar.WeekDate
+import Data.Time.Format
+import Text.Printf
+
+import Hledger.Data.Types
+
+-- | Convert Periods to DateSpans.
+--
+-- >>> periodAsDateSpan (MonthPeriod 2000 1) == DateSpan (Just $ fromGregorian 2000 1 1) (Just $ fromGregorian 2000 2 1)
+-- True
+periodAsDateSpan :: Period -> DateSpan
+periodAsDateSpan (DayPeriod d) = DateSpan (Just d) (Just $ addDays 1 d)
+periodAsDateSpan (WeekPeriod b) = DateSpan (Just b) (Just $ addDays 7 b)
+periodAsDateSpan (MonthPeriod y m) = DateSpan (Just $ fromGregorian y m 1) (Just $ fromGregorian y' m' 1)
+  where
+    (y',m') | m==12     = (y+1,1)
+            | otherwise = (y,m+1)
+periodAsDateSpan (QuarterPeriod y q) = DateSpan (Just $ fromGregorian y m 1) (Just $ fromGregorian y' m' 1)
+  where
+    (y', q') | q==4      = (y+1,1)
+             | otherwise = (y,q+1)
+    quarterAsMonth q = (q-1) * 3 + 1
+    m  = quarterAsMonth q
+    m' = quarterAsMonth q'
+periodAsDateSpan (YearPeriod y) = DateSpan (Just $ fromGregorian y 1 1) (Just $ fromGregorian (y+1) 1 1)
+periodAsDateSpan (PeriodBetween b e) = DateSpan (Just b) (Just e)
+periodAsDateSpan (PeriodFrom b) = DateSpan (Just b) Nothing
+periodAsDateSpan (PeriodTo e) = DateSpan Nothing (Just e)
+periodAsDateSpan (PeriodAll) = DateSpan Nothing Nothing
+
+-- | Convert DateSpans to Periods.
+--
+-- >>> dateSpanAsPeriod $ DateSpan (Just $ fromGregorian 2000 1 1) (Just $ fromGregorian 2000 2 1)
+-- MonthPeriod 2000 1
+dateSpanAsPeriod :: DateSpan -> Period
+dateSpanAsPeriod (DateSpan (Just b) (Just e)) = simplifyPeriod $ PeriodBetween b e
+dateSpanAsPeriod (DateSpan (Just b) Nothing) = PeriodFrom b
+dateSpanAsPeriod (DateSpan Nothing (Just e)) = PeriodTo e
+dateSpanAsPeriod (DateSpan Nothing Nothing) = PeriodAll
+
+-- | Convert PeriodBetweens to a more abstract period where possible.
+--
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 1 1 1) (fromGregorian 2 1 1)
+-- YearPeriod 1
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 10 1) (fromGregorian 2001 1 1)
+-- QuarterPeriod 2000 4
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 1) (fromGregorian 2000 3 1)
+-- MonthPeriod 2000 2
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2016 7 25) (fromGregorian 2016 8 1)
+-- WeekPeriod 2016-07-25
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 1 1) (fromGregorian 2000 1 2)
+-- DayPeriod 2000-01-01
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 28) (fromGregorian 2000 3 1)
+-- PeriodBetween 2000-02-28 2000-03-01
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 29) (fromGregorian 2000 3 1)
+-- DayPeriod 2000-02-29
+-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 12 31) (fromGregorian 2001 1 1)
+-- DayPeriod 2000-12-31
+--
+simplifyPeriod :: Period -> Period
+simplifyPeriod (PeriodBetween b e) =
+  case (toGregorian b, toGregorian e) of
+    -- a year
+    ((by,1,1), (ey,1,1))   | by+1==ey           -> YearPeriod by
+    -- a half-year
+    -- ((by,1,1), (ey,7,1))   | by==ey             ->
+    -- ((by,7,1), (ey,1,1))   | by+1==ey           ->
+    -- a quarter
+    ((by,1,1), (ey,4,1))   | by==ey             -> QuarterPeriod by 1
+    ((by,4,1), (ey,7,1))   | by==ey             -> QuarterPeriod by 2
+    ((by,7,1), (ey,10,1))  | by==ey             -> QuarterPeriod by 3
+    ((by,10,1), (ey,1,1))  | by+1==ey           -> QuarterPeriod by 4
+    -- a month
+    ((by,bm,1), (ey,em,1)) | by==ey && bm+1==em -> MonthPeriod by bm
+    ((by,12,1), (ey,1,1))  | by+1==ey           -> MonthPeriod by 12
+    -- a week (two successive mondays),
+    -- YYYYwN ("week N of year YYYY")
+    -- _ | let ((by,bw,bd), (ey,ew,ed)) = (toWeekDate from, toWeekDate to) in by==ey && fw+1==tw && bd==1 && ed==1 ->
+    -- a week starting on a monday
+    _ | let ((by,bw,bd), (ey,ew,ed)) = (toWeekDate b, toWeekDate (addDays (-1) e))
+        in by==ey && bw==ew && bd==1 && ed==7   -> WeekPeriod b
+    -- a day
+    ((by,bm,bd), (ey,em,ed)) |
+        (by==ey && bm==em && bd+1==ed) ||
+        (by+1==ey && bm==12 && em==1 && bd==31 && ed==1) || -- crossing a year boundary
+        (by==ey && bm+1==em && isLastDayOfMonth by bm bd && ed==1) -- crossing a month boundary
+         -> DayPeriod b
+    _ -> PeriodBetween b e
+simplifyPeriod p = p
+
+isLastDayOfMonth y m d =
+  case m of
+    1 -> d==31
+    2 | isLeapYear y -> d==29
+      | otherwise    -> d==28
+    3 -> d==31
+    4 -> d==30
+    5 -> d==31
+    6 -> d==30
+    7 -> d==31
+    8 -> d==31
+    9 -> d==30
+    10 -> d==31
+    11 -> d==30
+    12 -> d==31
+    _ -> False
+
+-- | 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
+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 PeriodAll           = "-"
+
+periodStart :: Period -> Maybe Day
+periodStart p = mb
+  where
+    DateSpan mb _ = periodAsDateSpan p
+
+periodEnd :: Period -> Maybe Day
+periodEnd p = me
+  where
+    DateSpan _ me = periodAsDateSpan p
+
+-- | Move a period to the following period of same duration.
+periodNext :: Period -> Period
+periodNext (DayPeriod b) = DayPeriod (addDays 1 b)
+periodNext (WeekPeriod b) = WeekPeriod (addDays 7 b)
+periodNext (MonthPeriod y 12) = MonthPeriod (y+1) 1
+periodNext (MonthPeriod y m) = MonthPeriod y (m+1)
+periodNext (QuarterPeriod y 4) = QuarterPeriod (y+1) 1
+periodNext (QuarterPeriod y q) = QuarterPeriod y (q+1)
+periodNext (YearPeriod y) = YearPeriod (y+1)
+periodNext p = p
+
+-- | Move a period to the preceding period of same duration.
+periodPrevious :: Period -> Period
+periodPrevious (DayPeriod b) = DayPeriod (addDays (-1) b)
+periodPrevious (WeekPeriod b) = WeekPeriod (addDays (-7) b)
+periodPrevious (MonthPeriod y 1) = MonthPeriod (y-1) 12
+periodPrevious (MonthPeriod y m) = MonthPeriod y (m-1)
+periodPrevious (QuarterPeriod y 1) = QuarterPeriod (y-1) 4
+periodPrevious (QuarterPeriod y q) = QuarterPeriod y (q-1)
+periodPrevious (YearPeriod y) = YearPeriod (y-1)
+periodPrevious p = p
+
+-- | Move a period to the following period of same duration, staying within enclosing dates.
+periodNextIn :: DateSpan -> Period -> Period
+periodNextIn (DateSpan _ (Just e)) p =
+  case mb of
+    Just b -> if b < e then p' else p
+    _      -> p
+  where
+    p' = periodNext p
+    mb = periodStart p'
+periodNextIn _ p = periodNext p
+
+-- | Move a period to the preceding period of same duration, staying within enclosing dates.
+periodPreviousIn :: DateSpan -> Period -> Period
+periodPreviousIn (DateSpan (Just b) _) p =
+  case me of
+    Just e -> if e > b then p' else p
+    _      -> p
+  where
+    p' = periodPrevious p
+    me = periodEnd p'
+periodPreviousIn _ p = periodPrevious p
+
+-- | Enlarge a standard period to the next larger enclosing standard period, if there is one.
+-- Eg, a day becomes the enclosing week.
+-- A week becomes whichever month the week's thursday falls into.
+-- A year becomes all (unlimited).
+-- Growing an unlimited period, or a non-standard period (arbitrary dates) has no effect.
+periodGrow :: Period -> Period
+periodGrow (DayPeriod b) = WeekPeriod $ mondayBefore b
+periodGrow (WeekPeriod b) = MonthPeriod y m
+  where (y,m) = yearMonthContainingWeekStarting b
+periodGrow (MonthPeriod y m) = QuarterPeriod y (quarterContainingMonth m)
+periodGrow (QuarterPeriod y _) = YearPeriod y
+periodGrow (YearPeriod _) = PeriodAll
+periodGrow p = p
+
+-- | Shrink a period to the next smaller standard period inside it,
+-- choosing the subperiod which contains today's date if possible,
+-- otherwise the first subperiod. It goes like this:
+-- unbounded periods and nonstandard periods (between two arbitrary dates) ->
+-- current year ->
+-- current quarter if it's in selected year, otherwise first quarter of selected year ->
+-- current month if it's in selected quarter, otherwise first month of selected quarter ->
+-- current week if it's in selected month, otherwise first week of selected month ->
+-- today if it's in selected week, otherwise first day of selected week,
+--  unless that's in previous month, in which case first day of month containing selected week.
+-- Shrinking a day has no effect.
+periodShrink :: Day -> Period -> Period
+periodShrink _     p@(DayPeriod _) = p
+periodShrink today (WeekPeriod b)
+  | today >= b && diffDays today b < 7 = DayPeriod today
+  | m /= weekmonth                     = DayPeriod $ fromGregorian weekyear weekmonth 1
+  | otherwise                          = DayPeriod b
+  where
+    (_,m,_) = toGregorian b
+    (weekyear,weekmonth) = yearMonthContainingWeekStarting b
+periodShrink today (MonthPeriod y m)
+  | (y',m') == (y,m) = WeekPeriod $ mondayBefore today
+  | otherwise        = WeekPeriod $ startOfFirstWeekInMonth y m
+  where (y',m',_) = toGregorian today
+periodShrink today (QuarterPeriod y q)
+  | quarterContainingMonth thismonth == q = MonthPeriod y thismonth
+  | otherwise                             = MonthPeriod y (firstMonthOfQuarter q)
+  where (_,thismonth,_) = toGregorian today
+periodShrink today (YearPeriod y)
+  | y == thisyear = QuarterPeriod y thisquarter
+  | otherwise     = QuarterPeriod y 1
+  where
+    (thisyear,thismonth,_) = toGregorian today
+    thisquarter = quarterContainingMonth thismonth
+periodShrink today _ = YearPeriod y
+  where (y,_,_) = toGregorian today
+
+mondayBefore d = addDays (fromIntegral (1 - wd)) d
+  where
+    (_,_,wd) = toWeekDate d
+
+yearMonthContainingWeekStarting weekstart = (y,m)
+  where
+    thu = addDays 3 weekstart
+    (y,yd) = toOrdinalDate thu
+    (m,_) = dayOfYearToMonthAndDay (isLeapYear y) yd
+
+quarterContainingMonth m = (m-1) `div` 3 + 1
+
+firstMonthOfQuarter q = (q-1)*3 + 1
+
+startOfFirstWeekInMonth y m
+  | monthstartday <= 4 = mon
+  | otherwise          = addDays 7 mon  -- month starts with a fri/sat/sun
+  where
+    monthstart = fromGregorian y m 1
+    mon = mondayBefore monthstart
+    (_,_,monthstartday) = toWeekDate monthstart
+
diff --git a/Hledger/Data/Posting.hs b/Hledger/Data/Posting.hs
--- a/Hledger/Data/Posting.hs
+++ b/Hledger/Data/Posting.hs
@@ -7,6 +7,8 @@
 
 -}
 
+{-# LANGUAGE OverloadedStrings #-}
+
 module Hledger.Data.Posting (
   -- * Posting
   nullposting,
@@ -50,7 +52,10 @@
 import Data.List
 import Data.Maybe
 import Data.MemoUgly (memo)
+import Data.Monoid
 import Data.Ord
+import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Time.Calendar
 import Safe
 import Test.HUnit
@@ -89,7 +94,7 @@
     where
       ledger3ishlayout = False
       acctnamewidth = if ledger3ishlayout then 25 else 22
-      showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . elideAccountName width
+      showaccountname = fitString (Just acctnamewidth) Nothing False False . bracket . T.unpack . elideAccountName width
       (bracket,width) = case t of
                           BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)
                           VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)
@@ -97,8 +102,8 @@
       showamount = padLeftWide 12 . showMixedAmount
 
 
-showComment :: String -> String
-showComment s = if null s then "" else "  ;" ++ s
+showComment :: Text -> String
+showComment t = if T.null t then "" else "  ;" ++ T.unpack t
 
 isReal :: Posting -> Bool
 isReal p = ptype p == RegularPosting
@@ -192,32 +197,32 @@
 
 accountNamePostingType :: AccountName -> PostingType
 accountNamePostingType a
-    | null a = RegularPosting
-    | head a == '[' && last a == ']' = BalancedVirtualPosting
-    | head a == '(' && last a == ')' = VirtualPosting
+    | T.null a = RegularPosting
+    | T.head a == '[' && T.last a == ']' = BalancedVirtualPosting
+    | T.head a == '(' && T.last a == ')' = VirtualPosting
     | otherwise = RegularPosting
 
 accountNameWithoutPostingType :: AccountName -> AccountName
 accountNameWithoutPostingType a = case accountNamePostingType a of
-                                    BalancedVirtualPosting -> init $ tail a
-                                    VirtualPosting -> init $ tail a
+                                    BalancedVirtualPosting -> T.init $ T.tail a
+                                    VirtualPosting -> T.init $ T.tail a
                                     RegularPosting -> a
 
 accountNameWithPostingType :: PostingType -> AccountName -> AccountName
-accountNameWithPostingType BalancedVirtualPosting a = "["++accountNameWithoutPostingType a++"]"
-accountNameWithPostingType VirtualPosting a = "("++accountNameWithoutPostingType a++")"
+accountNameWithPostingType BalancedVirtualPosting a = "["<>accountNameWithoutPostingType a<>"]"
+accountNameWithPostingType VirtualPosting a = "("<>accountNameWithoutPostingType a<>")"
 accountNameWithPostingType RegularPosting a = accountNameWithoutPostingType a
 
 -- | Prefix one account name to another, preserving posting type
 -- indicators like concatAccountNames.
 joinAccountNames :: AccountName -> AccountName -> AccountName
-joinAccountNames a b = concatAccountNames $ filter (not . null) [a,b]
+joinAccountNames a b = concatAccountNames $ filter (not . T.null) [a,b]
 
 -- | Join account names into one. If any of them has () or [] posting type
 -- indicators, these (the first type encountered) will also be applied to
 -- the resulting account name.
 concatAccountNames :: [AccountName] -> AccountName
-concatAccountNames as = accountNameWithPostingType t $ intercalate ":" $ map accountNameWithoutPostingType as
+concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map accountNameWithoutPostingType as
     where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as
 
 -- | Rewrite an account name using all matching aliases from the given list, in sequence.
@@ -241,9 +246,9 @@
 
 aliasReplace :: AccountAlias -> AccountName -> AccountName
 aliasReplace (BasicAlias old new) a
-  | old `isAccountNamePrefixOf` a || old == a = new ++ drop (length old) a
+  | old `isAccountNamePrefixOf` a || old == a = new <> T.drop (T.length old) a
   | otherwise = a
-aliasReplace (RegexAlias re repl) a = regexReplaceCIMemo re repl a
+aliasReplace (RegexAlias re repl) a = T.pack $ regexReplaceCIMemo re repl $ T.unpack a -- XXX
 
 
 tests_Hledger_Data_Posting = TestList [
diff --git a/Hledger/Data/RawOptions.hs b/Hledger/Data/RawOptions.hs
--- a/Hledger/Data/RawOptions.hs
+++ b/Hledger/Data/RawOptions.hs
@@ -23,6 +23,7 @@
 where
 
 import Data.Maybe
+import qualified Data.Text as T
 import Safe
 
 import Hledger.Utils
@@ -32,7 +33,7 @@
 type RawOpts = [(String,String)]
 
 setopt :: String -> String -> RawOpts -> RawOpts
-setopt name val = (++ [(name, quoteIfNeeded val)])
+setopt name val = (++ [(name, quoteIfNeeded $ val)])
 
 setboolopt :: String -> RawOpts -> RawOpts
 setboolopt name = (++ [(name,"")])
@@ -45,7 +46,7 @@
 boolopt = inRawOpts
 
 maybestringopt :: String -> RawOpts -> Maybe String
-maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name . reverse
+maybestringopt name = maybe Nothing (Just . T.unpack . stripquotes . T.pack) . lookup name . reverse
 
 stringopt :: String -> RawOpts -> String
 stringopt name = fromMaybe "" . maybestringopt name
diff --git a/Hledger/Data/StringFormat.hs b/Hledger/Data/StringFormat.hs
--- a/Hledger/Data/StringFormat.hs
+++ b/Hledger/Data/StringFormat.hs
@@ -2,7 +2,7 @@
 -- hledger's report item fields. The formats are used by
 -- report-specific renderers like renderBalanceReportItem.
 
-{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleContexts, TypeFamilies #-}
 
 module Hledger.Data.StringFormat (
           parseStringFormat
@@ -19,7 +19,8 @@
 import Data.Char (isPrint)
 import Data.Maybe
 import Test.HUnit
-import Text.Parsec
+import Text.Megaparsec
+import Text.Megaparsec.String
 
 import Hledger.Utils.String (formatString)
 
@@ -79,15 +80,15 @@
 
 -- | Parse a string format specification, or return a parse error.
 parseStringFormat :: String -> Either String StringFormat
-parseStringFormat input = case (runParser (stringformatp <* eof) () "(unknown)") input of
+parseStringFormat input = case (runParser (stringformatp <* eof) "(unknown)") input of
     Left y -> Left $ show y
     Right x -> Right x
 
 defaultStringFormatStyle = BottomAligned
 
-stringformatp :: Stream [Char] m Char => ParsecT [Char] st m StringFormat
+stringformatp :: Parser StringFormat
 stringformatp = do
-  alignspec <- optionMaybe (try $ char '%' >> oneOf "^_,")
+  alignspec <- optional (try $ char '%' >> oneOf "^_,")
   let constructor =
         case alignspec of
           Just '^' -> TopAligned
@@ -96,24 +97,24 @@
           _        -> defaultStringFormatStyle
   constructor <$> many componentp
 
-componentp :: Stream [Char] m Char => ParsecT [Char] st m StringFormatComponent
+componentp :: Parser StringFormatComponent
 componentp = formatliteralp <|> formatfieldp
 
-formatliteralp :: Stream [Char] m Char => ParsecT [Char] st m StringFormatComponent
+formatliteralp :: Parser StringFormatComponent
 formatliteralp = do
-    s <- many1 c
+    s <- some c
     return $ FormatLiteral s
     where
       isPrintableButNotPercentage x = isPrint x && (not $ x == '%')
       c =     (satisfy isPrintableButNotPercentage <?> "printable character")
           <|> try (string "%%" >> return '%')
 
-formatfieldp :: Stream [Char] m Char => ParsecT [Char] st m StringFormatComponent
+formatfieldp :: Parser StringFormatComponent
 formatfieldp = do
     char '%'
-    leftJustified <- optionMaybe (char '-')
-    minWidth <- optionMaybe (many1 $ digit)
-    maxWidth <- optionMaybe (do char '.'; many1 $ digit) -- TODO: Can this be (char '1') *> (many1 digit)
+    leftJustified <- optional (char '-')
+    minWidth <- optional (some $ digitChar)
+    maxWidth <- optional (do char '.'; some $ digitChar) -- TODO: Can this be (char '1') *> (some digitChar)
     char '('
     f <- fieldp
     char ')'
@@ -123,14 +124,14 @@
         Just text -> Just m where ((m,_):_) = readDec text
         _ -> Nothing
 
-fieldp :: Stream [Char] m Char => ParsecT [Char] st m ReportItemField
+fieldp :: Parser ReportItemField
 fieldp = do
         try (string "account" >> return AccountField)
     <|> try (string "depth_spacer" >> return DepthSpacerField)
     <|> try (string "date" >> return DescriptionField)
     <|> try (string "description" >> return DescriptionField)
     <|> try (string "total" >> return TotalField)
-    <|> try (many1 digit >>= (\s -> return $ FieldNo $ read s))
+    <|> try (some digitChar >>= (\s -> return $ FieldNo $ read s))
 
 ----------------------------------------------------------------------
 
diff --git a/Hledger/Data/TimeLog.hs b/Hledger/Data/TimeLog.hs
deleted file mode 100644
--- a/Hledger/Data/TimeLog.hs
+++ /dev/null
@@ -1,142 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-|
-
-A 'TimeLogEntry' is a clock-in, clock-out, or other directive in a timelog
-file (see timeclock.el or the command-line version). These can be
-converted to 'Transactions' and queried like a ledger.
-
--}
-
-module Hledger.Data.TimeLog
-where
-import Data.Maybe
-import Data.Time.Calendar
-import Data.Time.Clock
-import Data.Time.Format
-import Data.Time.LocalTime
-#if !(MIN_VERSION_time(1,5,0))
-import System.Locale (defaultTimeLocale)
-#endif
-import Test.HUnit
-import Text.Printf
-
-import Hledger.Utils
-import Hledger.Data.Types
-import Hledger.Data.Dates
-import Hledger.Data.Amount
-import Hledger.Data.Posting
-import Hledger.Data.Transaction
-
-instance Show TimeLogEntry where
-    show t = printf "%s %s %s  %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription t)
-
-instance Show TimeLogCode where
-    show SetBalance = "b"
-    show SetRequiredHours = "h"
-    show In = "i"
-    show Out = "o"
-    show FinalOut = "O"
-
-instance Read TimeLogCode where
-    readsPrec _ ('b' : xs) = [(SetBalance, xs)]
-    readsPrec _ ('h' : xs) = [(SetRequiredHours, xs)]
-    readsPrec _ ('i' : xs) = [(In, xs)]
-    readsPrec _ ('o' : xs) = [(Out, xs)]
-    readsPrec _ ('O' : xs) = [(FinalOut, xs)]
-    readsPrec _ _ = []
-
--- | Convert time log entries to journal transactions. When there is no
--- clockout, add one with the provided current time. Sessions crossing
--- midnight are split into days to give accurate per-day totals.
-timeLogEntriesToTransactions :: LocalTime -> [TimeLogEntry] -> [Transaction]
-timeLogEntriesToTransactions _ [] = []
-timeLogEntriesToTransactions now [i]
-    | odate > idate = entryFromTimeLogInOut i o' : timeLogEntriesToTransactions now [i',o]
-    | otherwise = [entryFromTimeLogInOut i o]
-    where
-      o = TimeLogEntry (tlsourcepos i) Out end "" ""
-      end = if itime > now then itime else now
-      (itime,otime) = (tldatetime i,tldatetime o)
-      (idate,odate) = (localDay itime,localDay otime)
-      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}
-      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}
-timeLogEntriesToTransactions now (i:o:rest)
-    | odate > idate = entryFromTimeLogInOut i o' : timeLogEntriesToTransactions now (i':o:rest)
-    | otherwise = entryFromTimeLogInOut i o : timeLogEntriesToTransactions now rest
-    where
-      (itime,otime) = (tldatetime i,tldatetime o)
-      (idate,odate) = (localDay itime,localDay otime)
-      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}
-      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}
-
--- | Convert a timelog clockin and clockout entry to an equivalent journal
--- transaction, representing the time expenditure. Note this entry is  not balanced,
--- since we omit the \"assets:time\" transaction for simpler output.
-entryFromTimeLogInOut :: TimeLogEntry -> TimeLogEntry -> Transaction
-entryFromTimeLogInOut i o
-    | otime >= itime = t
-    | otherwise =
-        error' $ "clock-out time less than clock-in time in:\n" ++ showTransaction t
-    where
-      t = Transaction {
-            tindex       = 0,
-            tsourcepos   = tlsourcepos i,
-            tdate        = idate,
-            tdate2       = Nothing,
-            tstatus      = Cleared,
-            tcode        = "",
-            tdescription = desc,
-            tcomment     = "",
-            ttags        = [],
-            tpostings    = ps,
-            tpreceding_comment_lines=""
-          }
-      itime    = tldatetime i
-      otime    = tldatetime o
-      itod     = localTimeOfDay itime
-      otod     = localTimeOfDay otime
-      idate    = localDay itime
-      desc     | null (tldescription i) = showtime itod ++ "-" ++ showtime otod
-               | otherwise              = tldescription i
-      showtime = take 5 . show
-      hours    = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc
-      acctname = tlaccount i
-      amount   = Mixed [hrs hours]
-      ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]
-
-
-tests_Hledger_Data_TimeLog = TestList [
-
-   "timeLogEntriesToTransactions" ~: do
-     today <- getCurrentDay
-     now' <- getCurrentTime
-     tz <- getCurrentTimeZone
-     let now = utcToLocalTime tz now'
-         nowstr = showtime now
-         yesterday = prevday today
-         clockin = TimeLogEntry nullsourcepos In
-         mktime d = LocalTime d . fromMaybe midnight .
-#if MIN_VERSION_time(1,5,0)
-                    parseTimeM True defaultTimeLocale "%H:%M:%S"
-#else
-                    parseTime defaultTimeLocale "%H:%M:%S"
-#endif
-         showtime = formatTime defaultTimeLocale "%H:%M"
-         assertEntriesGiveStrings name es ss = assertEqual name ss (map tdescription $ timeLogEntriesToTransactions now es)
-
-     assertEntriesGiveStrings "started yesterday, split session at midnight"
-                                  [clockin (mktime yesterday "23:00:00") "" ""]
-                                  ["23:00-23:59","00:00-"++nowstr]
-     assertEntriesGiveStrings "split multi-day sessions at each midnight"
-                                  [clockin (mktime (addDays (-2) today) "23:00:00") "" ""]
-                                  ["23:00-23:59","00:00-23:59","00:00-"++nowstr]
-     assertEntriesGiveStrings "auto-clock-out if needed"
-                                  [clockin (mktime today "00:00:00") "" ""]
-                                  ["00:00-"++nowstr]
-     let future = utcToLocalTime tz $ addUTCTime 100 now'
-         futurestr = showtime future
-     assertEntriesGiveStrings "use the clockin time for auto-clockout if it's in the future"
-                                  [clockin future "" ""]
-                                  [printf "%s-%s" futurestr futurestr]
-
- ]
diff --git a/Hledger/Data/Timeclock.hs b/Hledger/Data/Timeclock.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Data/Timeclock.hs
@@ -0,0 +1,145 @@
+{-|
+
+A 'TimeclockEntry' is a clock-in, clock-out, or other directive in a timeclock
+file (see timeclock.el or the command-line version). These can be
+converted to 'Transactions' and queried like a ledger.
+
+-}
+
+{-# LANGUAGE CPP, OverloadedStrings #-}
+
+module Hledger.Data.Timeclock
+where
+import Data.Maybe
+-- import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Calendar
+import Data.Time.Clock
+import Data.Time.Format
+import Data.Time.LocalTime
+#if !(MIN_VERSION_time(1,5,0))
+import System.Locale (defaultTimeLocale)
+#endif
+import Test.HUnit
+import Text.Printf
+
+import Hledger.Utils
+import Hledger.Data.Types
+import Hledger.Data.Dates
+import Hledger.Data.Amount
+import Hledger.Data.Posting
+import Hledger.Data.Transaction
+
+instance Show TimeclockEntry where
+    show t = printf "%s %s %s  %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription t)
+
+instance Show TimeclockCode where
+    show SetBalance = "b"
+    show SetRequiredHours = "h"
+    show In = "i"
+    show Out = "o"
+    show FinalOut = "O"
+
+instance Read TimeclockCode where
+    readsPrec _ ('b' : xs) = [(SetBalance, xs)]
+    readsPrec _ ('h' : xs) = [(SetRequiredHours, xs)]
+    readsPrec _ ('i' : xs) = [(In, xs)]
+    readsPrec _ ('o' : xs) = [(Out, xs)]
+    readsPrec _ ('O' : xs) = [(FinalOut, xs)]
+    readsPrec _ _ = []
+
+-- | Convert time log entries to journal transactions. When there is no
+-- clockout, add one with the provided current time. Sessions crossing
+-- midnight are split into days to give accurate per-day totals.
+timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]
+timeclockEntriesToTransactions _ [] = []
+timeclockEntriesToTransactions now [i]
+    | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now [i',o]
+    | otherwise = [entryFromTimeclockInOut i o]
+    where
+      o = TimeclockEntry (tlsourcepos i) Out end "" ""
+      end = if itime > now then itime else now
+      (itime,otime) = (tldatetime i,tldatetime o)
+      (idate,odate) = (localDay itime,localDay otime)
+      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)
+    | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now (i':o:rest)
+    | otherwise = entryFromTimeclockInOut i o : timeclockEntriesToTransactions now rest
+    where
+      (itime,otime) = (tldatetime i,tldatetime o)
+      (idate,odate) = (localDay itime,localDay otime)
+      o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}
+      i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}}
+
+-- | Convert a timeclock clockin and clockout entry to an equivalent journal
+-- transaction, representing the time expenditure. Note this entry is  not balanced,
+-- since we omit the \"assets:time\" transaction for simpler output.
+entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction
+entryFromTimeclockInOut i o
+    | otime >= itime = t
+    | otherwise =
+        error' $ "clock-out time less than clock-in time in:\n" ++ showTransaction t
+    where
+      t = Transaction {
+            tindex       = 0,
+            tsourcepos   = tlsourcepos i,
+            tdate        = idate,
+            tdate2       = Nothing,
+            tstatus      = Cleared,
+            tcode        = "",
+            tdescription = desc,
+            tcomment     = "",
+            ttags        = [],
+            tpostings    = ps,
+            tpreceding_comment_lines=""
+          }
+      itime    = tldatetime i
+      otime    = tldatetime o
+      itod     = localTimeOfDay itime
+      otod     = localTimeOfDay otime
+      idate    = localDay itime
+      desc     | T.null (tldescription i) = T.pack $ showtime itod ++ "-" ++ showtime otod
+               | otherwise                = tldescription i
+      showtime = take 5 . show
+      hours    = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc
+      acctname = tlaccount i
+      amount   = Mixed [hrs hours]
+      ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]
+
+
+tests_Hledger_Data_Timeclock = TestList [
+
+   "timeclockEntriesToTransactions" ~: do
+     today <- getCurrentDay
+     now' <- getCurrentTime
+     tz <- getCurrentTimeZone
+     let now = utcToLocalTime tz now'
+         nowstr = showtime now
+         yesterday = prevday today
+         clockin = TimeclockEntry nullsourcepos In
+         mktime d = LocalTime d . fromMaybe midnight .
+#if MIN_VERSION_time(1,5,0)
+                    parseTimeM True defaultTimeLocale "%H:%M:%S"
+#else
+                    parseTime defaultTimeLocale "%H:%M:%S"
+#endif
+         showtime = formatTime defaultTimeLocale "%H:%M"
+         assertEntriesGiveStrings name es ss = assertEqual name ss (map (T.unpack . tdescription) $ timeclockEntriesToTransactions now es)
+
+     assertEntriesGiveStrings "started yesterday, split session at midnight"
+                                  [clockin (mktime yesterday "23:00:00") "" ""]
+                                  ["23:00-23:59","00:00-"++nowstr]
+     assertEntriesGiveStrings "split multi-day sessions at each midnight"
+                                  [clockin (mktime (addDays (-2) today) "23:00:00") "" ""]
+                                  ["23:00-23:59","00:00-23:59","00:00-"++nowstr]
+     assertEntriesGiveStrings "auto-clock-out if needed"
+                                  [clockin (mktime today "00:00:00") "" ""]
+                                  ["00:00-"++nowstr]
+     let future = utcToLocalTime tz $ addUTCTime 100 now'
+         futurestr = showtime future
+     assertEntriesGiveStrings "use the clockin time for auto-clockout if it's in the future"
+                                  [clockin future "" ""]
+                                  [printf "%s-%s" futurestr futurestr]
+
+ ]
diff --git a/Hledger/Data/Transaction.hs b/Hledger/Data/Transaction.hs
--- a/Hledger/Data/Transaction.hs
+++ b/Hledger/Data/Transaction.hs
@@ -7,12 +7,14 @@
 
 -}
 
+{-# LANGUAGE OverloadedStrings #-}
+
 module Hledger.Data.Transaction (
   -- * Transaction
   nullsourcepos,
   nulltransaction,
   txnTieKnot,
-  -- settxn,
+  txnUntieKnot,
   -- * operations
   showAccountName,
   hasRealPostings,
@@ -31,12 +33,15 @@
   showTransaction,
   showTransactionUnelided,
   showTransactionUnelidedOneLineAmounts,
+  showPostingLine,
   -- * misc.
   tests_Hledger_Data_Transaction
 )
 where
 import Data.List
 import Data.Maybe
+import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Time.Calendar
 import Test.HUnit
 import Text.Printf
@@ -51,10 +56,10 @@
 instance Show Transaction where show = showTransactionUnelided
 
 instance Show ModifierTransaction where
-    show t = "= " ++ mtvalueexpr t ++ "\n" ++ unlines (map show (mtpostings t))
+    show t = "= " ++ T.unpack (mtvalueexpr t) ++ "\n" ++ unlines (map show (mtpostings t))
 
 instance Show PeriodicTransaction where
-    show t = "~ " ++ ptperiodicexpr t ++ "\n" ++ unlines (map show (ptpostings t))
+    show t = "~ " ++ T.unpack (ptperiodicexpr t) ++ "\n" ++ unlines (map show (ptpostings t))
 
 nullsourcepos :: GenericSourcePos
 nullsourcepos = GenericSourcePos "" 1 1
@@ -145,16 +150,16 @@
       status | tstatus t == Cleared = " *"
              | tstatus t == Pending = " !"
              | otherwise            = ""
-      code = if length (tcode t) > 0 then printf " (%s)" $ tcode t else ""
-      desc = if null d then "" else " " ++ d where d = tdescription t
+      code = if T.length (tcode t) > 0 then printf " (%s)" $ T.unpack $ tcode t else ""
+      desc = if null d then "" else " " ++ d where d = T.unpack $ tdescription t
       (samelinecomment, newlinecomments) =
         case renderCommentLines (tcomment t) of []   -> ("",[])
                                                 c:cs -> (c,cs)
 
 -- Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.
-renderCommentLines :: String -> [String]
-renderCommentLines s  = case lines s of ("":ls) -> "":map commentprefix ls
-                                        ls      -> map commentprefix ls
+renderCommentLines :: Text -> [String]
+renderCommentLines t  = case lines $ T.unpack t of ("":ls) -> "":map commentprefix ls
+                                                   ls      -> map commentprefix ls
     where
       commentprefix = indent . ("; "++)
 
@@ -187,7 +192,7 @@
         showstatus p ++ fitString (Just acctwidth) Nothing False True (showAccountName Nothing (ptype p) (paccount p))
         where
           showstatus p = if pstatus p == Cleared then "* " else ""
-          acctwidth = maximum $ map (strWidth . paccount) ps
+          acctwidth = maximum $ map (textWidth . paccount) ps
 
     -- currently prices are considered part of the amount string when right-aligning amounts
     amount
@@ -201,6 +206,15 @@
       case renderCommentLines (pcomment p) of []   -> ("",[])
                                               c:cs -> (c,cs)
 
+
+-- used in balance assertion error
+showPostingLine p =
+  indent $
+  if pstatus p == Cleared then "* " else "" ++
+  showAccountName Nothing (ptype p) (paccount p) ++
+  "    " ++
+  showMixedAmountOneLine (pamount p)
+
 tests_postingAsLines = [
    "postingAsLines" ~: do
     let p `gives` ls = assertEqual "" ls (postingAsLines False False [p] p)
@@ -229,13 +243,17 @@
 showAccountName :: Maybe Int -> PostingType -> AccountName -> String
 showAccountName w = fmt
     where
-      fmt RegularPosting = take w'
-      fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse
-      fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse
+      fmt RegularPosting = take w' . T.unpack
+      fmt VirtualPosting = parenthesise . reverse . take (w'-2) . reverse . T.unpack
+      fmt BalancedVirtualPosting = bracket . reverse . take (w'-2) . reverse . T.unpack
       w' = fromMaybe 999999 w
-      parenthesise s = "("++s++")"
-      bracket s = "["++s++"]"
 
+parenthesise :: String -> String
+parenthesise s = "("++s++")"
+
+bracket :: String -> String
+bracket s = "["++s++"]"
+
 hasRealPostings :: Transaction -> Bool
 hasRealPostings = not . null . realPostings
 
@@ -260,7 +278,7 @@
 -- | Is this transaction balanced ? A balanced transaction's real
 -- (non-virtual) postings sum to 0, and any balanced virtual postings
 -- also sum to 0.
-isTransactionBalanced :: Maybe (Map.Map Commodity AmountStyle) -> Transaction -> Bool
+isTransactionBalanced :: Maybe (Map.Map CommoditySymbol AmountStyle) -> Transaction -> Bool
 isTransactionBalanced styles t =
     -- isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum
     isZeroMixedAmount rsum' && isZeroMixedAmount bvsum'
@@ -274,7 +292,7 @@
 -- amount or conversion price(s), or return an error message.
 -- Balancing is affected by commodity display precisions, so those can
 -- (optionally) be provided.
-balanceTransaction :: Maybe (Map.Map Commodity AmountStyle) -> Transaction -> Either String Transaction
+balanceTransaction :: Maybe (Map.Map CommoditySymbol AmountStyle) -> Transaction -> Either String Transaction
 balanceTransaction styles t =
   case inferBalancingAmount t of
     Left err -> Left err
@@ -402,11 +420,16 @@
 -- | Ensure a transaction's postings refer back to it, so that eg
 -- relatedPostings works right.
 txnTieKnot :: Transaction -> Transaction
-txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (settxn t) ps}
+txnTieKnot t@Transaction{tpostings=ps} = t{tpostings=map (postingSetTransaction t) ps}
 
+-- | Ensure a transaction's postings do not refer back to it, so that eg
+-- recursiveSize and GHCI's :sprint work right.
+txnUntieKnot :: Transaction -> Transaction
+txnUntieKnot t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}
+
 -- | Set a posting's parent transaction.
-settxn :: Transaction -> Posting -> Posting
-settxn t p = p{ptransaction=Just t}
+postingSetTransaction :: Transaction -> Posting -> Posting
+postingSetTransaction t p = p{ptransaction=Just t}
 
 tests_Hledger_Data_Transaction = TestList $ concat [
   tests_postingAsLines,
diff --git a/Hledger/Data/Types.hs b/Hledger/Data/Types.hs
--- a/Hledger/Data/Types.hs
+++ b/Hledger/Data/Types.hs
@@ -24,11 +24,12 @@
 import Control.DeepSeq (NFData)
 import Control.Monad.Except (ExceptT)
 import Data.Data
-#ifndef DOUBLE
 import Data.Decimal
+import Data.Default
 import Text.Blaze (ToMarkup(..))
-#endif
 import qualified Data.Map as M
+import Data.Text (Text)
+-- import qualified Data.Text as T
 import Data.Time.Calendar
 import Data.Time.LocalTime
 import System.Time (ClockTime(..))
@@ -44,15 +45,61 @@
 
 instance NFData DateSpan
 
-data Interval = NoInterval
-              | Days Int | Weeks Int | Months Int | Quarters Int | Years Int
-              | DayOfMonth Int | DayOfWeek Int
-              -- WeekOfYear Int | MonthOfYear Int | QuarterOfYear Int
-                deriving (Eq,Show,Ord,Data,Generic,Typeable)
+-- synonyms for various date-related scalars
+type Year = Integer
+type Month = Int     -- 1-12
+type Quarter = Int   -- 1-4
+type YearWeek = Int  -- 1-52
+type MonthWeek = Int -- 1-5
+type YearDay = Int   -- 1-366
+type MonthDay = Int  -- 1-31
+type WeekDay = Int   -- 1-7
 
+-- Typical report periods (spans of time), both finite and open-ended.
+-- A richer abstraction than DateSpan.
+data Period =
+    DayPeriod Day
+  | WeekPeriod Day
+  | MonthPeriod Year Month
+  | QuarterPeriod Year Quarter
+  | YearPeriod Year
+  | PeriodBetween Day Day
+  | PeriodFrom Day
+  | PeriodTo Day
+  | PeriodAll
+  deriving (Eq,Ord,Show,Data,Generic,Typeable)
+
+instance Default Period where def = PeriodAll
+
+---- Typical report period/subperiod durations, from a day to a year.
+--data Duration =
+--    DayLong
+--   WeekLong
+--   MonthLong
+--   QuarterLong
+--   YearLong
+--  deriving (Eq,Ord,Show,Data,Generic,Typeable)
+
+-- Ways in which a period can be divided into subperiods.
+data Interval =
+    NoInterval
+  | Days Int
+  | Weeks Int
+  | Months Int
+  | Quarters Int
+  | Years Int
+  | DayOfMonth Int
+  | DayOfWeek Int
+  -- WeekOfYear Int
+  -- MonthOfYear Int
+  -- QuarterOfYear Int
+  deriving (Eq,Show,Ord,Data,Generic,Typeable)
+
+instance Default Interval where def = NoInterval
+
 instance NFData Interval
 
-type AccountName = String
+type AccountName = Text
 
 data AccountAlias = BasicAlias AccountName AccountName
                   | RegexAlias Regexp Replacement
@@ -64,15 +111,7 @@
 
 instance NFData Side
 
-type Commodity = String
-
--- | The basic numeric type used in amounts. Different implementations
--- can be selected via cabal flag for testing and benchmarking purposes.
-numberRepresentation :: String
-#ifdef DOUBLE
-type Quantity = Double
-numberRepresentation = "Double"
-#else
+-- | The basic numeric type used in amounts.
 type Quantity = Decimal
 deriving instance Data (Quantity)
 -- The following is for hledger-web, and requires blaze-markup.
@@ -80,8 +119,6 @@
 instance ToMarkup (Quantity)
  where
    toMarkup = toMarkup . show
-numberRepresentation = "Decimal"
-#endif
 
 -- | An amount's price (none, per unit, or total) in another commodity.
 -- Note the price should be a positive number, although this is not enforced.
@@ -91,11 +128,11 @@
 
 -- | Display style for an amount.
 data AmountStyle = AmountStyle {
-      ascommodityside :: Side,       -- ^ does the symbol appear on the left or the right ?
-      ascommodityspaced :: Bool,     -- ^ space between symbol and quantity ?
-      asprecision :: Int,            -- ^ number of digits displayed after the decimal point
-      asdecimalpoint :: Maybe Char,  -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"
-      asdigitgroups :: Maybe DigitGroupStyle -- ^ style for displaying digit groups, if any
+      ascommodityside   :: Side,                 -- ^ does the symbol appear on the left or the right ?
+      ascommodityspaced :: Bool,                 -- ^ space between symbol and quantity ?
+      asprecision       :: Int,                  -- ^ number of digits displayed after the decimal point
+      asdecimalpoint    :: Maybe Char,           -- ^ character used as decimal point: period or comma. Nothing means "unspecified, use default"
+      asdigitgroups     :: Maybe DigitGroupStyle -- ^ style for displaying digit groups, if any
 } deriving (Eq,Ord,Read,Show,Typeable,Data,Generic)
 
 instance NFData AmountStyle
@@ -111,11 +148,20 @@
 
 instance NFData DigitGroupStyle
 
+type CommoditySymbol = Text
+
+data Commodity = Commodity {
+  csymbol :: CommoditySymbol,
+  cformat :: Maybe AmountStyle
+  } deriving (Show,Eq,Data,Generic) --,Ord,Typeable,Data,Generic)
+
+instance NFData Commodity
+
 data Amount = Amount {
-      acommodity :: Commodity,
-      aquantity :: Quantity,
-      aprice :: Price,                -- ^ the (fixed) price for this amount, if any
-      astyle :: AmountStyle
+      acommodity :: CommoditySymbol,
+      aquantity  :: Quantity,
+      aprice     :: Price,           -- ^ the (fixed) price for this amount, if any
+      astyle     :: AmountStyle
     } deriving (Eq,Ord,Typeable,Data,Generic)
 
 instance NFData Amount
@@ -129,30 +175,32 @@
 
 instance NFData PostingType
 
-type Tag = (String, String)  -- ^ A tag name and (possibly empty) value.
+type TagName = Text
+type TagValue = Text
+type Tag = (TagName, TagValue)  -- ^ A tag name and (possibly empty) value.
 
 data ClearedStatus = Uncleared | Pending | Cleared
-                   deriving (Eq,Ord,Typeable,Data,Generic)
+  deriving (Eq,Ord,Typeable,Data,Generic)
 
 instance NFData ClearedStatus
 
-instance Show ClearedStatus where -- custom show
-  show Uncleared = ""             -- a bad idea
-  show Pending   = "!"            -- don't do it
+instance Show ClearedStatus where -- custom show.. bad idea.. don't do it..
+  show Uncleared = ""
+  show Pending   = "!"
   show Cleared   = "*"
 
 data Posting = Posting {
-      pdate :: Maybe Day,  -- ^ this posting's date, if different from the transaction's
-      pdate2 :: Maybe Day,  -- ^ this posting's secondary date, if different from the transaction's
-      pstatus :: ClearedStatus,
-      paccount :: AccountName,
-      pamount :: MixedAmount,
-      pcomment :: String, -- ^ this posting's comment lines, as a single non-indented multi-line string
-      ptype :: PostingType,
-      ptags :: [Tag], -- ^ tag names and values, extracted from the comment
-      pbalanceassertion :: Maybe MixedAmount,  -- ^ optional: the expected balance in the account after this posting
-      ptransaction :: Maybe Transaction    -- ^ this posting's parent transaction (co-recursive types).
-                                           -- Tying this knot gets tedious, Maybe makes it easier/optional.
+      pdate             :: Maybe Day,         -- ^ this posting's date, if different from the transaction's
+      pdate2            :: Maybe Day,         -- ^ this posting's secondary date, if different from the transaction's
+      pstatus           :: ClearedStatus,
+      paccount          :: AccountName,
+      pamount           :: MixedAmount,
+      pcomment          :: Text,              -- ^ this posting's comment lines, as a single non-indented multi-line string
+      ptype             :: PostingType,
+      ptags             :: [Tag],             -- ^ tag names and values, extracted from the comment
+      pbalanceassertion :: Maybe MixedAmount, -- ^ optional: the expected balance in the account after this posting
+      ptransaction      :: Maybe Transaction  -- ^ this posting's parent transaction (co-recursive types).
+                                              -- Tying this knot gets tedious, Maybe makes it easier/optional.
     } deriving (Typeable,Data,Generic)
 
 instance NFData Posting
@@ -170,102 +218,99 @@
 instance NFData GenericSourcePos
 
 data Transaction = Transaction {
-      tindex :: Integer, -- ^ this transaction's 1-based position in the input stream, or 0 when not available
-      tsourcepos :: GenericSourcePos,
-      tdate :: Day,
-      tdate2 :: Maybe Day,
-      tstatus :: ClearedStatus,
-      tcode :: String,
-      tdescription :: String,
-      tcomment :: String, -- ^ this transaction's comment lines, as a single non-indented multi-line string
-      ttags :: [Tag], -- ^ tag names and values, extracted from the comment
-      tpostings :: [Posting],            -- ^ this transaction's postings
-      tpreceding_comment_lines :: String -- ^ any comment lines immediately preceding this transaction
+      tindex                   :: Integer,   -- ^ this transaction's 1-based position in the input stream, or 0 when not available
+      tsourcepos               :: GenericSourcePos,
+      tdate                    :: Day,
+      tdate2                   :: Maybe Day,
+      tstatus                  :: ClearedStatus,
+      tcode                    :: Text,
+      tdescription             :: Text,
+      tcomment                 :: Text,      -- ^ this transaction's comment lines, as a single non-indented multi-line string
+      ttags                    :: [Tag],     -- ^ tag names and values, extracted from the comment
+      tpostings                :: [Posting], -- ^ this transaction's postings
+      tpreceding_comment_lines :: Text       -- ^ any comment lines immediately preceding this transaction
     } deriving (Eq,Typeable,Data,Generic)
 
 instance NFData Transaction
 
 data ModifierTransaction = ModifierTransaction {
-      mtvalueexpr :: String,
-      mtpostings :: [Posting]
+      mtvalueexpr :: Text,
+      mtpostings  :: [Posting]
     } deriving (Eq,Typeable,Data,Generic)
 
 instance NFData ModifierTransaction
 
 data PeriodicTransaction = PeriodicTransaction {
-      ptperiodicexpr :: String,
-      ptpostings :: [Posting]
+      ptperiodicexpr :: Text,
+      ptpostings     :: [Posting]
     } deriving (Eq,Typeable,Data,Generic)
 
 instance NFData PeriodicTransaction
 
-data TimeLogCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data,Generic)
+data TimeclockCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data,Generic)
 
-instance NFData TimeLogCode
+instance NFData TimeclockCode
 
-data TimeLogEntry = TimeLogEntry {
-      tlsourcepos :: GenericSourcePos,
-      tlcode :: TimeLogCode,
-      tldatetime :: LocalTime,
-      tlaccount :: String,
-      tldescription :: String
+data TimeclockEntry = TimeclockEntry {
+      tlsourcepos   :: GenericSourcePos,
+      tlcode        :: TimeclockCode,
+      tldatetime    :: LocalTime,
+      tlaccount     :: AccountName,
+      tldescription :: Text
     } deriving (Eq,Ord,Typeable,Data,Generic)
 
-instance NFData TimeLogEntry
+instance NFData TimeclockEntry
 
 data MarketPrice = MarketPrice {
-      mpdate :: Day,
-      mpcommodity :: Commodity,
-      mpamount :: Amount
+      mpdate      :: Day,
+      mpcommodity :: CommoditySymbol,
+      mpamount    :: Amount
     } deriving (Eq,Ord,Typeable,Data,Generic) -- & Show (in Amount.hs)
 
 instance NFData MarketPrice
 
-type Year = Integer
-
--- | A journal "context" is some data which can change in the course of
--- parsing a journal. An example is the default year, which changes when a
--- Y directive is encountered.  At the end of parsing, the final context
--- is saved for later use by eg the add command.
-data JournalContext = Ctx {
-      ctxYear      :: !(Maybe Year)      -- ^ the default year most recently specified with Y
-    , ctxDefaultCommodityAndStyle :: !(Maybe (Commodity,AmountStyle)) -- ^ the default commodity and amount style most recently specified with D
-    , ctxAccount   :: ![AccountName]     -- ^ the current stack of parent accounts/account name components
-                                        --   specified with "account" directive(s). Concatenated, these
-                                        --   are the account prefix prepended to parsed account names.
-    , ctxAliases   :: ![AccountAlias]   -- ^ the current list of account name aliases in effect
-    , ctxTransactionIndex   :: !Integer -- ^ the number of transactions read so far
-    } deriving (Read, Show, Eq, Data, Typeable, Generic)
-
-instance NFData JournalContext
+-- | A Journal, containing transactions and various other things.
+-- The basic data model for hledger.
+--
+-- This is used during parsing (as the type alias ParsedJournal), and
+-- then finalised/validated for use as a Journal. Some extra
+-- parsing-related fields are included for convenience, at least for
+-- now. In a ParsedJournal these are updated as parsing proceeds, in a
+-- Journal they represent the final state at end of parsing (used eg
+-- by the add command).
+--
+data Journal = Journal {
+  -- parsing-related data
+   jparsedefaultyear      :: Maybe Year                            -- ^ the current default year, specified by the most recent Y directive (or current date)
+  ,jparsedefaultcommodity :: Maybe (CommoditySymbol,AmountStyle)   -- ^ the current default commodity and its format, specified by the most recent D directive
+  ,jparseparentaccounts   :: [AccountName]                         -- ^ the current stack of parent account names, specified by apply account directives
+  ,jparsealiases          :: [AccountAlias]                        -- ^ the current account name aliases in effect, specified by alias directives (& options ?)
+  -- ,jparsetransactioncount :: Integer                               -- ^ the current count of transactions parsed so far (only journal format txns, currently)
+  ,jparsetimeclockentries :: [TimeclockEntry]                   -- ^ timeclock sessions which have not been clocked out
+  -- principal data
+  ,jaccounts              :: [AccountName]                         -- ^ accounts that have been declared by account directives
+  ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives
+  ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts
+  ,jmarketprices          :: [MarketPrice]
+  ,jmodifiertxns          :: [ModifierTransaction]
+  ,jperiodictxns          :: [PeriodicTransaction]
+  ,jtxns                  :: [Transaction]
+  ,jfinalcommentlines     :: Text                                   -- ^ any final trailing comments in the (main) journal file
+  ,jfiles                 :: [(FilePath, Text)]                     -- ^ the file path and raw text of the main and
+                                                                    --   any included journal files. The main file is first,
+                                                                    --   followed by any included files in the order encountered.
+  ,jlastreadtime          :: ClockTime                              -- ^ when this journal was last read from its file(s)
+  } deriving (Eq, Typeable, Data, Generic)
 
 deriving instance Data (ClockTime)
 deriving instance Typeable (ClockTime)
 deriving instance Generic (ClockTime)
-
 instance NFData ClockTime
-
-data Journal = Journal {
-      jmodifiertxns :: [ModifierTransaction],
-      jperiodictxns :: [PeriodicTransaction],
-      jtxns :: [Transaction],
-      open_timelog_entries :: [TimeLogEntry],
-      jmarketprices :: [MarketPrice],
-      final_comment_lines :: String,        -- ^ any trailing comments from the journal file
-      jContext :: JournalContext,           -- ^ the context (parse state) at the end of parsing
-      files :: [(FilePath, String)],        -- ^ the file path and raw text of the main and
-                                            -- any included journal files. The main file is
-                                            -- first followed by any included files in the
-                                            -- order encountered.
-      filereadtime :: ClockTime,            -- ^ when this journal was last read from its file(s)
-      jcommoditystyles :: M.Map Commodity AmountStyle  -- ^ how to display amounts in each commodity
-    } deriving (Eq, Typeable, Data, Generic)
-
 instance NFData Journal
 
--- | A JournalUpdate is some transformation of a Journal. It can do I/O or
--- raise an exception.
-type JournalUpdate = ExceptT String IO (Journal -> Journal)
+-- | A journal in the process of being parsed, not yet finalised.
+-- The data is partial, and list fields are in reverse order.
+type ParsedJournal = Journal
 
 -- | The id of a data format understood by hledger, eg @journal@ or @csv@.
 -- The --output-format option selects one of these for output.
@@ -277,9 +322,9 @@
      -- name of the format this reader handles
      rFormat   :: StorageFormat
      -- quickly check if this reader can probably handle the given file path and file content
-    ,rDetector :: FilePath -> String -> Bool
+    ,rDetector :: FilePath -> Text -> Bool
      -- parse the given string, using the given parse rules file if any, returning a journal or error aware of the given file path
-    ,rParser   :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal
+    ,rParser   :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal
     }
 
 instance Show Reader where show r = rFormat r ++ " reader"
@@ -287,22 +332,22 @@
 -- | An account, with name, balances and links to parent/subaccounts
 -- which let you walk up or down the account tree.
 data Account = Account {
-  aname :: AccountName,     -- ^ this account's full name
-  aebalance :: MixedAmount, -- ^ this account's balance, excluding subaccounts
-  asubs :: [Account],       -- ^ sub-accounts
-  anumpostings :: Int,      -- ^ number of postings to this account
-  -- derived from the above:
-  aibalance :: MixedAmount, -- ^ this account's balance, including subaccounts
-  aparent :: Maybe Account, -- ^ parent account
-  aboring :: Bool           -- ^ used in the accounts report to label elidable parents
-  }
+  aname                     :: AccountName,   -- ^ this account's full name
+  aebalance                 :: MixedAmount,   -- ^ this account's balance, excluding subaccounts
+  asubs                     :: [Account],     -- ^ sub-accounts
+  anumpostings              :: Int,           -- ^ number of postings to this account
+  -- derived from the above :
+  aibalance                 :: MixedAmount,   -- ^ this account's balance, including subaccounts
+  aparent                   :: Maybe Account, -- ^ parent account
+  aboring                   :: Bool           -- ^ used in the accounts report to label elidable parents
+  } deriving (Typeable, Data, Generic)
 
 -- | A Ledger has the journal it derives from, and the accounts
 -- derived from that. Accounts are accessible both list-wise and
 -- tree-wise, since each one knows its parent and subs; the first
 -- account is the root of the tree and always exists.
 data Ledger = Ledger {
-  ljournal :: Journal,
+  ljournal  :: Journal,
   laccounts :: [Account]
 }
 
diff --git a/Hledger/Query.hs b/Hledger/Query.hs
--- a/Hledger/Query.hs
+++ b/Hledger/Query.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE DeriveDataTypeable #-}
 {-|
 
 A general query system for matching things (accounts, postings,
@@ -6,6 +5,8 @@
 
 -}
 
+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ViewPatterns #-}
+
 module Hledger.Query (
   -- * Query and QueryOpt
   Query(..),
@@ -23,6 +24,9 @@
   queryIsDateOrDate2,
   queryIsStartDateOnly,
   queryIsSym,
+  queryIsReal,
+  queryIsStatus,
+  queryIsEmpty,
   queryStartDate,
   queryEndDate,
   queryDateSpan,
@@ -45,13 +49,16 @@
 import Data.Either
 import Data.List
 import Data.Maybe
+import Data.Monoid ((<>))
+-- import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Time.Calendar
 import Safe (readDef, headDef)
 import Test.HUnit
--- import Text.ParserCombinators.Parsec
-import Text.Parsec hiding (Empty)
+import Text.Megaparsec
+import Text.Megaparsec.Text
 
-import Hledger.Utils
+import Hledger.Utils hiding (words')
 import Hledger.Data.Types
 import Hledger.Data.AccountName
 import Hledger.Data.Amount (amount, nullamt, usd)
@@ -149,7 +156,7 @@
 -- 1. multiple account patterns are OR'd together
 -- 2. multiple description patterns are OR'd together
 -- 3. then all terms are AND'd together
-parseQuery :: Day -> String -> (Query,[QueryOpt])
+parseQuery :: Day -> T.Text -> (Query,[QueryOpt])
 parseQuery d s = (q, opts)
   where
     terms = words'' prefixes s
@@ -173,21 +180,27 @@
 -- | Quote-and-prefix-aware version of words - don't split on spaces which
 -- are inside quotes, including quotes which may have one of the specified
 -- prefixes in front, and maybe an additional not: prefix in front of that.
-words'' :: [String] -> String -> [String]
+words'' :: [T.Text] -> T.Text -> [T.Text]
 words'' prefixes = fromparse . parsewith maybeprefixedquotedphrases -- XXX
     where
-      maybeprefixedquotedphrases = choice' [prefixedQuotedPattern, singleQuotedPattern, doubleQuotedPattern, pattern] `sepBy` many1 spacenonewline
+      maybeprefixedquotedphrases :: Parser [T.Text]
+      maybeprefixedquotedphrases = choice' [prefixedQuotedPattern, singleQuotedPattern, doubleQuotedPattern, pattern] `sepBy` some spacenonewline
+      prefixedQuotedPattern :: Parser T.Text
       prefixedQuotedPattern = do
-        not' <- fromMaybe "" `fmap` (optionMaybe $ string "not:")
+        not' <- fromMaybe "" `fmap` (optional $ string "not:")
         let allowednexts | null not' = prefixes
                          | otherwise = prefixes ++ [""]
-        next <- choice' $ map string allowednexts
-        let prefix = not' ++ next
+        next <- fmap T.pack $ choice' $ map (string . T.unpack) allowednexts
+        let prefix :: T.Text
+            prefix = T.pack not' <> next
         p <- singleQuotedPattern <|> doubleQuotedPattern
-        return $ prefix ++ stripquotes p
-      singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf "'") >>= return . stripquotes
-      doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf "\"") >>= return . stripquotes
-      pattern = many (noneOf " \n\r")
+        return $ prefix <> stripquotes p
+      singleQuotedPattern :: Parser T.Text
+      singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf ("'" :: [Char])) >>= return . stripquotes . T.pack
+      doubleQuotedPattern :: Parser T.Text
+      doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf ("\"" :: [Char])) >>= return . stripquotes . T.pack
+      pattern :: Parser T.Text
+      pattern = fmap T.pack $ many (noneOf (" \n\r" :: [Char]))
 
 tests_words'' = [
    "words''" ~: do
@@ -204,7 +217,8 @@
 
 -- XXX
 -- keep synced with patterns below, excluding "not"
-prefixes = map (++":") [
+prefixes :: [T.Text]
+prefixes = map (<>":") [
      "inacctonly"
     ,"inacct"
     ,"amt"
@@ -221,6 +235,7 @@
     ,"tag"
     ]
 
+defaultprefix :: T.Text
 defaultprefix = "acct"
 
 -- -- | Parse the query string as a boolean tree of match patterns.
@@ -235,36 +250,37 @@
 
 -- | Parse a single query term as either a query or a query option,
 -- or raise an error if it has invalid syntax.
-parseQueryTerm :: Day -> String -> Either Query QueryOpt
-parseQueryTerm _ ('i':'n':'a':'c':'c':'t':'o':'n':'l':'y':':':s) = Right $ QueryOptInAcctOnly s
-parseQueryTerm _ ('i':'n':'a':'c':'c':'t':':':s) = Right $ QueryOptInAcct s
-parseQueryTerm d ('n':'o':'t':':':s) = case parseQueryTerm d s of
-                                       Left m  -> Left $ Not m
-                                       Right _ -> Left Any -- not:somequeryoption will be ignored
-parseQueryTerm _ ('c':'o':'d':'e':':':s) = Left $ Code s
-parseQueryTerm _ ('d':'e':'s':'c':':':s) = Left $ Desc s
-parseQueryTerm _ ('a':'c':'c':'t':':':s) = Left $ Acct s
-parseQueryTerm d ('d':'a':'t':'e':'2':':':s) =
-        case parsePeriodExpr d s of Left e         -> error' $ "\"date2:"++s++"\" gave a "++showDateParseError e
+parseQueryTerm :: Day -> T.Text -> Either Query QueryOpt
+parseQueryTerm _ (T.stripPrefix "inacctonly:" -> Just s) = Right $ QueryOptInAcctOnly s
+parseQueryTerm _ (T.stripPrefix "inacct:" -> Just s) = Right $ QueryOptInAcct s
+parseQueryTerm d (T.stripPrefix "not:" -> Just s) =
+  case parseQueryTerm d s of
+    Left m -> Left $ Not m
+    Right _ -> Left Any -- not:somequeryoption will be ignored
+parseQueryTerm _ (T.stripPrefix "code:" -> Just s) = Left $ Code $ T.unpack s
+parseQueryTerm _ (T.stripPrefix "desc:" -> Just s) = Left $ Desc $ T.unpack s
+parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left $ Acct $ T.unpack s
+parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =
+        case parsePeriodExpr d s of Left e         -> error' $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e
                                     Right (_,span) -> Left $ Date2 span
-parseQueryTerm d ('d':'a':'t':'e':':':s) =
-        case parsePeriodExpr d s of Left e         -> error' $ "\"date:"++s++"\" gave a "++showDateParseError e
+parseQueryTerm d (T.stripPrefix "date:" -> Just s) =
+        case parsePeriodExpr d s of Left e         -> error' $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e
                                     Right (_,span) -> Left $ Date span
-parseQueryTerm _ ('s':'t':'a':'t':'u':'s':':':s) = 
-        case parseStatus s of Left e   -> error' $ "\"status:"++s++"\" gave a parse error: " ++ e
+parseQueryTerm _ (T.stripPrefix "status:" -> Just s) =
+        case parseStatus s of Left e   -> error' $ "\"status:"++T.unpack s++"\" gave a parse error: " ++ e
                               Right st -> Left $ Status st
-parseQueryTerm _ ('r':'e':'a':'l':':':s) = Left $ Real $ parseBool s || null s
-parseQueryTerm _ ('a':'m':'t':':':s) = Left $ Amt ord q where (ord, q) = parseAmountQueryTerm s
-parseQueryTerm _ ('e':'m':'p':'t':'y':':':s) = Left $ Empty $ parseBool s
-parseQueryTerm _ ('d':'e':'p':'t':'h':':':s)
+parseQueryTerm _ (T.stripPrefix "real:" -> Just s) = Left $ Real $ parseBool s || T.null s
+parseQueryTerm _ (T.stripPrefix "amt:" -> Just s) = Left $ Amt ord q where (ord, q) = parseAmountQueryTerm s
+parseQueryTerm _ (T.stripPrefix "empty:" -> Just s) = Left $ Empty $ parseBool s
+parseQueryTerm _ (T.stripPrefix "depth:" -> Just s)
   | n >= 0    = Left $ Depth n
   | otherwise = error' "depth: should have a positive number"
-  where n = readDef 0 s
+  where n = readDef 0 (T.unpack s)
 
-parseQueryTerm _ ('c':'u':'r':':':s) = Left $ Sym s -- support cur: as an alias
-parseQueryTerm _ ('t':'a':'g':':':s) = Left $ Tag n v where (n,v) = parseTag s
+parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Left $ Sym (T.unpack s) -- support cur: as an alias
+parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = Left $ Tag n v where (n,v) = parseTag s
 parseQueryTerm _ "" = Left $ Any
-parseQueryTerm d s = parseQueryTerm d $ defaultprefix++":"++s
+parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s
 
 tests_parseQueryTerm = [
   "parseQueryTerm" ~: do
@@ -293,35 +309,40 @@
  deriving (Show,Eq,Data,Typeable)
 
 -- can fail
-parseAmountQueryTerm :: String -> (OrdPlus, Quantity)
+parseAmountQueryTerm :: T.Text -> (OrdPlus, Quantity)
 parseAmountQueryTerm s' =
   case s' of
     -- feel free to do this a smarter way
     ""              -> err
-    '<':'+':s       -> (Lt, readDef err s)
-    '<':'=':'+':s   -> (LtEq, readDef err s)
-    '>':'+':s       -> (Gt, readDef err s)
-    '>':'=':'+':s   -> (GtEq, readDef err s)
-    '=':'+':s       -> (Eq, readDef err s)
-    '+':s           -> (Eq, readDef err s)
-    '<':'-':s       -> (Lt, negate $ readDef err s)
-    '<':'=':'-':s   -> (LtEq, negate $ readDef err s)
-    '>':'-':s       -> (Gt, negate $ readDef err s)
-    '>':'=':'-':s   -> (GtEq, negate $ readDef err s)
-    '=':'-':s       -> (Eq, negate $ readDef err s)
-    '-':s           -> (Eq, negate $ readDef err s)
-    '<':'=':s       -> let n = readDef err s in case n of 0 -> (LtEq, 0)
-                                                          _ -> (AbsLtEq, n)
-    '<':s           -> let n = readDef err s in case n of 0 -> (Lt, 0)
-                                                          _ -> (AbsLt, n)
-    '>':'=':s       -> let n = readDef err s in case n of 0 -> (GtEq, 0)
-                                                          _ -> (AbsGtEq, n)
-    '>':s           -> let n = readDef err s in case n of 0 -> (Gt, 0)
-                                                          _ -> (AbsGt, n)
-    '=':s           -> (AbsEq, readDef err s)
-    s               -> (AbsEq, readDef err s)
+    (T.stripPrefix "<+" -> Just s)  -> (Lt, readDef err (T.unpack s))
+    (T.stripPrefix "<=+" -> Just s) -> (LtEq, readDef err (T.unpack s))
+    (T.stripPrefix ">+" -> Just s)  -> (Gt, readDef err (T.unpack s))
+    (T.stripPrefix ">=+" -> Just s) -> (GtEq, readDef err (T.unpack s))
+    (T.stripPrefix "=+" -> Just s)  -> (Eq, readDef err (T.unpack s))
+    (T.stripPrefix "+" -> Just s)   -> (Eq, readDef err (T.unpack s))
+    (T.stripPrefix "<-" -> Just s)  -> (Lt, negate $ readDef err (T.unpack s))
+    (T.stripPrefix "<=-" -> Just s) -> (LtEq, negate $ readDef err (T.unpack s))
+    (T.stripPrefix ">-" -> Just s)  -> (Gt, negate $ readDef err (T.unpack s))
+    (T.stripPrefix ">=-" -> Just s) -> (GtEq, negate $ readDef err (T.unpack s))
+    (T.stripPrefix "=-" -> Just s)  -> (Eq, negate $ readDef err (T.unpack s))
+    (T.stripPrefix "-" -> Just s)   -> (Eq, negate $ readDef err (T.unpack s))
+    (T.stripPrefix "<=" -> Just s)  -> let n = readDef err (T.unpack s) in
+                                         case n of
+                                           0 -> (LtEq, 0)
+                                           _ -> (AbsLtEq, n)
+    (T.stripPrefix "<" -> Just s)   -> let n = readDef err (T.unpack s) in
+                                         case n of 0 -> (Lt, 0)
+                                                   _ -> (AbsLt, n)
+    (T.stripPrefix ">=" -> Just s)  -> let n = readDef err (T.unpack s) in
+                                         case n of 0 -> (GtEq, 0)
+                                                   _ -> (AbsGtEq, n)
+    (T.stripPrefix ">" -> Just s)   -> let n = readDef err (T.unpack s) in
+                                         case n of 0 -> (Gt, 0)
+                                                   _ -> (AbsGt, n)
+    (T.stripPrefix "=" -> Just s)           -> (AbsEq, readDef err (T.unpack s))
+    s               -> (AbsEq, readDef err (T.unpack s))
   where
-    err = error' $ "could not parse as '=', '<', or '>' (optional) followed by a (optionally signed) numeric quantity: " ++ s'
+    err = error' $ "could not parse as '=', '<', or '>' (optional) followed by a (optionally signed) numeric quantity: " ++ T.unpack s'
 
 tests_parseAmountQueryTerm = [
   "parseAmountQueryTerm" ~: do
@@ -335,13 +356,13 @@
     "-0.23" `gives` (Eq,(-0.23))
   ]
 
-parseTag :: String -> (Regexp, Maybe Regexp)
-parseTag s | '=' `elem` s = (n, Just $ tail v)
-           | otherwise    = (s, Nothing)
-           where (n,v) = break (=='=') s
+parseTag :: T.Text -> (Regexp, Maybe Regexp)
+parseTag s | "=" `T.isInfixOf` s = (T.unpack n, Just $ tail $ T.unpack v)
+           | otherwise    = (T.unpack s, Nothing)
+           where (n,v) = T.break (=='=') s
 
 -- | Parse the value part of a "status:" query, or return an error.
-parseStatus :: String -> Either String ClearedStatus
+parseStatus :: T.Text -> Either String ClearedStatus
 parseStatus s | s `elem` ["*","1"] = Right Cleared
               | s `elem` ["!"]     = Right Pending
               | s `elem` ["","0"]  = Right Uncleared
@@ -349,10 +370,10 @@
 
 -- | Parse the boolean value part of a "status:" query. "1" means true,
 -- anything else will be parsed as false without error.
-parseBool :: String -> Bool
+parseBool :: T.Text -> Bool
 parseBool s = s `elem` truestrings
 
-truestrings :: [String]
+truestrings :: [T.Text]
 truestrings = ["1"]
 
 simplifyQuery :: Query -> Query
@@ -395,7 +416,7 @@
 same (a:as) = all (a==) as
 
 -- | Remove query terms (or whole sub-expressions) not matching the given
--- predicate from this query.  XXX Semantics not yet clear.
+-- predicate from this query.  XXX Semantics not completely clear.
 filterQuery :: (Query -> Bool) -> Query -> Query
 filterQuery p = simplifyQuery . filterQuery' p
 
@@ -452,6 +473,18 @@
 queryIsSym (Sym _) = True
 queryIsSym _ = False
 
+queryIsReal :: Query -> Bool
+queryIsReal (Real _) = True
+queryIsReal _ = False
+
+queryIsStatus :: Query -> Bool
+queryIsStatus (Status _) = True
+queryIsStatus _ = False
+
+queryIsEmpty :: Query -> Bool
+queryIsEmpty (Empty _) = True
+queryIsEmpty _ = False
+
 -- | Does this query specify a start date and nothing else (that would
 -- filter postings prior to the date) ?
 -- When the flag is true, look for a starting secondary date instead.
@@ -557,8 +590,8 @@
 -- Just looks at the first query option.
 inAccountQuery :: [QueryOpt] -> Maybe Query
 inAccountQuery [] = Nothing
-inAccountQuery (QueryOptInAcctOnly a:_) = Just $ Acct $ accountNameToAccountOnlyRegex a
-inAccountQuery (QueryOptInAcct a:_) = Just $ Acct $ accountNameToAccountRegex a
+inAccountQuery (QueryOptInAcctOnly a : _) = Just $ Acct $ accountNameToAccountOnlyRegex a
+inAccountQuery (QueryOptInAcct a     : _) = Just $ Acct $ accountNameToAccountRegex a
 
 -- -- | Convert a query to its inverse.
 -- negateQuery :: Query -> Query
@@ -573,7 +606,7 @@
 matchesAccount (Not m) a = not $ matchesAccount m a
 matchesAccount (Or ms) a = any (`matchesAccount` a) ms
 matchesAccount (And ms) a = all (`matchesAccount` a) ms
-matchesAccount (Acct r) a = regexMatchesCI r a
+matchesAccount (Acct r) a = regexMatchesCI r (T.unpack a) -- XXX pack
 matchesAccount (Depth d) a = accountNameLevel a <= d
 matchesAccount (Tag _ _) _ = False
 matchesAccount _ _ = True
@@ -605,7 +638,7 @@
 matchesAmount (And qs) a = all (`matchesAmount` a) qs
 --
 matchesAmount (Amt ord n) a = compareAmount ord n a
-matchesAmount (Sym r) a = regexMatchesCI ("^" ++ r ++ "$") $ acommodity a
+matchesAmount (Sym r) a = regexMatchesCI ("^" ++ r ++ "$") $ T.unpack $ acommodity a
 --
 matchesAmount _ _ = True
 
@@ -632,9 +665,9 @@
 matchesPosting (None) _ = False
 matchesPosting (Or qs) p = any (`matchesPosting` p) qs
 matchesPosting (And qs) p = all (`matchesPosting` p) qs
-matchesPosting (Code r) p = regexMatchesCI r $ maybe "" tcode $ ptransaction p
-matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" tdescription $ ptransaction p
-matchesPosting (Acct r) p = regexMatchesCI r $ paccount p
+matchesPosting (Code r) p = regexMatchesCI r $ maybe "" (T.unpack . tcode) $ ptransaction p
+matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" (T.unpack . tdescription) $ ptransaction p
+matchesPosting (Acct r) p = regexMatchesCI r $ T.unpack $ paccount p -- XXX pack
 matchesPosting (Date span) p = span `spanContainsDate` postingDate p
 matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p
 matchesPosting (Status Uncleared) p = postingStatus p /= Cleared
@@ -647,7 +680,7 @@
 -- matchesPosting (Empty False) Posting{pamount=a} = True
 -- matchesPosting (Empty True) Posting{pamount=a} = isZeroMixedAmount a
 matchesPosting (Empty _) _ = True
-matchesPosting (Sym r) Posting{pamount=Mixed as} = any (regexMatchesCI $ "^" ++ r ++ "$") $ map acommodity as
+matchesPosting (Sym r) Posting{pamount=Mixed as} = any (regexMatchesCI $ "^" ++ r ++ "$") $ map (T.unpack . acommodity) as
 matchesPosting (Tag n v) p = not $ null $ matchedTags n v $ postingAllTags p
 -- matchesPosting _ _ = False
 
@@ -690,8 +723,8 @@
 matchesTransaction (None) _ = False
 matchesTransaction (Or qs) t = any (`matchesTransaction` t) qs
 matchesTransaction (And qs) t = all (`matchesTransaction` t) qs
-matchesTransaction (Code r) t = regexMatchesCI r $ tcode t
-matchesTransaction (Desc r) t = regexMatchesCI r $ tdescription t
+matchesTransaction (Code r) t = regexMatchesCI r $ T.unpack $ tcode t
+matchesTransaction (Desc r) t = regexMatchesCI r $ T.unpack $ tdescription t
 matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t
 matchesTransaction (Date span) t = spanContainsDate span $ tdate t
 matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t
@@ -723,8 +756,8 @@
 matchedTags :: Regexp -> Maybe Regexp -> [Tag] -> [Tag]
 matchedTags namepat valuepat tags = filter (match namepat valuepat) tags
   where
-    match npat Nothing     (n,_) = regexMatchesCI npat n
-    match npat (Just vpat) (n,v) = regexMatchesCI npat n && regexMatchesCI vpat v
+    match npat Nothing     (n,_) = regexMatchesCI npat (T.unpack n) -- XXX
+    match npat (Just vpat) (n,v) = regexMatchesCI npat (T.unpack n) && regexMatchesCI vpat (T.unpack v)
 
 -- tests
 
diff --git a/Hledger/Read.hs b/Hledger/Read.hs
--- a/Hledger/Read.hs
+++ b/Hledger/Read.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE ScopedTypeVariables #-}
 {-|
 
 This is the entry point to hledger's reading system, which can read
@@ -8,7 +7,12 @@
 
 -}
 
-module Hledger.Read (
+{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}
+
+module Hledger.Read
+  (
+       module Hledger.Read.Common,
+       readFormatNames,
        -- * Journal reading API
        defaultJournalPath,
        defaultJournal,
@@ -20,12 +24,12 @@
        ensureJournalFileExists,
        -- * Parsers used elsewhere
        postingp,
-       accountnamep,
-       amountp,
-       amountp',
-       mamountp',
-       numberp,
-       codep,
+       -- accountnamep,
+       -- amountp,
+       -- amountp',
+       -- mamountp',
+       -- numberp,
+       -- codep,
        accountaliasp,
        -- * Tests
        samplejournal,
@@ -36,115 +40,54 @@
 import Control.Monad.Except
 import Data.List
 import Data.Maybe
+import Data.Text (Text)
+import qualified Data.Text as T
 import System.Directory (doesFileExist, getHomeDirectory)
 import System.Environment (getEnv)
 import System.Exit (exitFailure)
 import System.FilePath ((</>))
-import System.IO (IOMode(..), openFile, stdin, stderr, hSetNewlineMode, universalNewlineMode)
+import System.IO (stderr)
 import Test.HUnit
 import Text.Printf
 
 import Hledger.Data.Dates (getCurrentDay)
 import Hledger.Data.Types
-import Hledger.Data.Journal (nullctx)
+import Hledger.Read.Common
 import Hledger.Read.JournalReader as JournalReader
-import Hledger.Read.TimelogReader as TimelogReader
+import Hledger.Read.TimedotReader as TimedotReader
+import Hledger.Read.TimeclockReader as TimeclockReader
 import Hledger.Read.CsvReader as CsvReader
 import Hledger.Utils
 import Prelude hiding (getContents, writeFile)
-import Hledger.Utils.UTF8IOCompat (hGetContents, writeFile)
+import Hledger.Utils.UTF8IOCompat (writeFile)
 
 
-journalEnvVar           = "LEDGER_FILE"
-journalEnvVar2          = "LEDGER"
-journalDefaultFilename  = ".hledger.journal"
-
 -- The available data file readers, each one handling a particular data
 -- format. The first is also used as the default for unknown formats.
 readers :: [Reader]
 readers = [
   JournalReader.reader
- ,TimelogReader.reader
+ ,TimeclockReader.reader
+ ,TimedotReader.reader
  ,CsvReader.reader
  ]
 
--- | All the data formats we can read.
--- formats = map rFormat readers
-
--- | Get the default journal file path specified by the environment.
--- Like ledger, we look first for the LEDGER_FILE environment
--- variable, and if that does not exist, for the legacy LEDGER
--- environment variable. If neither is set, or the value is blank,
--- return the hard-coded default, which is @.hledger.journal@ in the
--- users's home directory (or in the current directory, if we cannot
--- determine a home directory).
-defaultJournalPath :: IO String
-defaultJournalPath = do
-  s <- envJournalPath
-  if null s then defaultJournalPath else return s
-    where
-      envJournalPath =
-        getEnv journalEnvVar
-         `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2
-                                            `C.catch` (\(_::C.IOException) -> return ""))
-      defaultJournalPath = do
-                  home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")
-                  return $ home </> journalDefaultFilename
-
--- | Read the default journal file specified by the environment, or raise an error.
-defaultJournal :: IO Journal
-defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return
-
--- | Read a journal from the given string, trying all known formats, or simply throw an error.
-readJournal' :: String -> IO Journal
-readJournal' s = readJournal Nothing Nothing True Nothing s >>= either error' return
-
-tests_readJournal' = [
-  "readJournal' parses sample journal" ~: do
-     _ <- samplejournal
-     assertBool "" True
- ]
-
-
+readFormatNames :: [StorageFormat]
+readFormatNames = map rFormat readers
 
--- | Read a journal from this string, trying whatever readers seem appropriate:
---
--- - if a format is specified, try that reader only
---
--- - or if one or more readers recognises the file path and data, try those
---
--- - otherwise, try them all.
---
--- A CSV conversion rules file may also be specified for use by the CSV reader.
--- Also there is a flag specifying whether to check or ignore balance assertions in the journal.
-readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> String -> IO (Either String Journal)
-readJournal format rulesfile assrt path s =
-  tryReaders $ readersFor (format, path, s)
-  where
-    -- try each reader in turn, returning the error of the first if all fail
-    tryReaders :: [Reader] -> IO (Either String Journal)
-    tryReaders = firstSuccessOrBestError []
-      where
-        firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal)
-        firstSuccessOrBestError [] []        = return $ Left "no readers found"
-        firstSuccessOrBestError errs (r:rs) = do
-          dbg1IO "trying reader" (rFormat r)
-          result <- (runExceptT . (rParser r) rulesfile assrt path') s
-          dbg1IO "reader result" $ either id show result
-          case result of Right j -> return $ Right j                       -- success!
-                         Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying
-        firstSuccessOrBestError (e:_) []    = return $ Left e              -- none left, return first error
-        path' = fromMaybe "(string)" path
+journalEnvVar           = "LEDGER_FILE"
+journalEnvVar2          = "LEDGER"
+journalDefaultFilename  = ".hledger.journal"
 
 -- | Which readers are worth trying for this (possibly unspecified) format, filepath, and data ?
-readersFor :: (Maybe StorageFormat, Maybe FilePath, String) -> [Reader]
-readersFor (format,path,s) =
-    dbg1 ("possible readers for "++show (format,path,elideRight 30 s)) $
+readersFor :: (Maybe StorageFormat, Maybe FilePath, Text) -> [Reader]
+readersFor (format,path,t) =
+    dbg1 ("possible readers for "++show (format,path,textElideRight 30 t)) $
     case format of
      Just f  -> case readerForStorageFormat f of Just r  -> [r]
                                                  Nothing -> []
      Nothing -> case path of Nothing  -> readers
-                             Just p   -> case readersForPathAndData (p,s) of [] -> readers
+                             Just p   -> case readersForPathAndData (p,t) of [] -> readers
                                                                              rs -> rs
 
 -- | Find the (first) reader which can handle the given format, if any.
@@ -155,36 +98,65 @@
       rs = filter ((s==).rFormat) readers :: [Reader]
 
 -- | Find the readers which think they can handle the given file path and data, if any.
-readersForPathAndData :: (FilePath,String) -> [Reader]
-readersForPathAndData (f,s) = filter (\r -> (rDetector r) f s) readers
+readersForPathAndData :: (FilePath,Text) -> [Reader]
+readersForPathAndData (f,t) = filter (\r -> dbg1 ("try "++rFormat r++" format") $ (rDetector r) f t) readers
 
+-- try each reader in turn, returning the error of the first if all fail
+tryReaders :: [Reader] -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal)
+tryReaders readers mrulesfile assrt path t = firstSuccessOrBestError [] readers
+  where
+    firstSuccessOrBestError :: [String] -> [Reader] -> IO (Either String Journal)
+    firstSuccessOrBestError [] []        = return $ Left "no readers found"
+    firstSuccessOrBestError errs (r:rs) = do
+      dbg1IO "trying reader" (rFormat r)
+      result <- (runExceptT . (rParser r) mrulesfile assrt path') t
+      dbg1IO "reader result" $ either id show result
+      case result of Right j -> return $ Right j                       -- success!
+                     Left e  -> firstSuccessOrBestError (errs++[e]) rs -- keep trying
+    firstSuccessOrBestError (e:_) []    = return $ Left e              -- none left, return first error
+    path' = fromMaybe "(string)" path
+
+-- | Read a journal from this string, trying whatever readers seem appropriate:
+--
+-- - if a format is specified, try that reader only
+--
+-- - or if one or more readers recognises the file path and data, try those
+--
+-- - otherwise, try them all.
+--
+-- A CSV conversion rules file may also be specified for use by the CSV reader.
+-- Also there is a flag specifying whether to check or ignore balance assertions in the journal.
+readJournal :: Maybe StorageFormat -> Maybe FilePath -> Bool -> Maybe FilePath -> Text -> IO (Either String Journal)
+readJournal mformat mrulesfile assrt mpath t = tryReaders (readersFor (mformat, mpath, t)) mrulesfile assrt mpath t
+
+-- | Call readJournalFile on each specified file path, and combine the
+-- resulting journals into one. If there are any errors, the first is
+-- returned, otherwise they are combined per Journal's monoid instance
+-- (concatenated, basically). Parse context (eg directives & aliases)
+-- is not maintained across file boundaries, it resets at the start of
+-- each file (though the rfinal parse state saved in the resulting
+-- journal is the combination of parse states from all files).
+readJournalFiles :: Maybe StorageFormat -> Maybe FilePath -> Bool -> [FilePath] -> IO (Either String Journal)
+readJournalFiles mformat mrulesfile assrt fs = do
+  (either Left (Right . mconcat) . sequence)
+    <$> mapM (readJournalFile mformat mrulesfile assrt) fs
+
 -- | Read a Journal from this file (or stdin if the filename is -) or give
 -- an error message, using the specified data format or trying all known
 -- formats. A CSV conversion rules file may be specified for better
 -- conversion of that format. Also there is a flag specifying whether
 -- to check or ignore balance assertions in the journal.
 readJournalFile :: Maybe StorageFormat -> Maybe FilePath -> Bool -> FilePath -> IO (Either String Journal)
-readJournalFile format rulesfile assrt f = readJournalFiles format rulesfile assrt [f]
-
-readJournalFiles :: Maybe StorageFormat -> Maybe FilePath -> Bool -> [FilePath] -> IO (Either String Journal)
-readJournalFiles format rulesfile assrt f = do
-  contents <- fmap concat $ mapM readFileAnyNewline f
-  readJournal format rulesfile assrt (listToMaybe f) contents
- where
-  readFileAnyNewline f = do
-    requireJournalFileExists f
-    h <- fileHandle f
-    hSetNewlineMode h universalNewlineMode
-    hGetContents h
-  fileHandle "-" = return stdin
-  fileHandle f = openFile f ReadMode
+readJournalFile mformat mrulesfile assrt f = do
+  requireJournalFileExists f
+  readFileOrStdinAnyLineEnding f >>= readJournal mformat mrulesfile assrt (Just f)
 
 -- | If the specified journal file does not exist, give a helpful error and quit.
 requireJournalFileExists :: FilePath -> IO ()
 requireJournalFileExists "-" = return ()
 requireJournalFileExists f = do
   exists <- doesFileExist f
-  when (not exists) $ do
+  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"
@@ -206,9 +178,43 @@
   d <- getCurrentDay
   return $ printf "; journal created %s by hledger\n" (show d)
 
+-- | Get the default journal file path specified by the environment.
+-- Like ledger, we look first for the LEDGER_FILE environment
+-- variable, and if that does not exist, for the legacy LEDGER
+-- environment variable. If neither is set, or the value is blank,
+-- return the hard-coded default, which is @.hledger.journal@ in the
+-- users's home directory (or in the current directory, if we cannot
+-- determine a home directory).
+defaultJournalPath :: IO String
+defaultJournalPath = do
+  s <- envJournalPath
+  if null s then defaultJournalPath else return s
+    where
+      envJournalPath =
+        getEnv journalEnvVar
+         `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2
+                                            `C.catch` (\(_::C.IOException) -> return ""))
+      defaultJournalPath = do
+                  home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")
+                  return $ home </> journalDefaultFilename
+
+-- | Read the default journal file specified by the environment, or raise an error.
+defaultJournal :: IO Journal
+defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing True >>= either error' return
+
+-- | Read a journal from the given text, trying all known formats, or simply throw an error.
+readJournal' :: Text -> IO Journal
+readJournal' t = readJournal Nothing Nothing True Nothing t >>= either error' return
+
+tests_readJournal' = [
+  "readJournal' parses sample journal" ~: do
+     _ <- samplejournal
+     assertBool "" True
+ ]
+
 -- tests
 
-samplejournal = readJournal' $ unlines
+samplejournal = readJournal' $ T.unlines
  ["2008/01/01 income"
  ,"    assets:bank:checking  $1"
  ,"    income:salary"
@@ -240,11 +246,12 @@
   tests_readJournal'
   ++ [
    tests_Hledger_Read_JournalReader,
-   tests_Hledger_Read_TimelogReader,
+   tests_Hledger_Read_TimeclockReader,
+   tests_Hledger_Read_TimedotReader,
    tests_Hledger_Read_CsvReader,
 
    "journal" ~: do
-    r <- runExceptT $ parseWithCtx nullctx JournalReader.journalp ""
+    r <- runExceptT $ parseWithState mempty JournalReader.journalp ""
     assertBool "journalp should parse an empty file" (isRight $ r)
     jE <- readJournal Nothing Nothing True Nothing "" -- don't know how to get it from journal
     either error' (assertBool "journalp parsing an empty file should give an empty journal" . null . jtxns) jE
diff --git a/Hledger/Read/Common.hs b/Hledger/Read/Common.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Read/Common.hs
@@ -0,0 +1,828 @@
+--- * 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.
+
+{-|
+
+Some common parsers and helpers used by several readers.
+Some of these might belong in Hledger.Read.JournalReader or Hledger.Read.
+
+-}
+
+--- * module
+{-# LANGUAGE CPP, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-}
+
+module Hledger.Read.Common
+where
+--- * imports
+import Prelude ()
+import Prelude.Compat hiding (readFile)
+import Control.Monad.Compat
+import Control.Monad.Except (ExceptT(..), runExceptT, throwError) --, catchError)
+import Control.Monad.State.Strict
+import Data.Char (isNumber)
+import Data.Functor.Identity
+import Data.List.Compat
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.List.Split (wordsBy)
+import Data.Maybe
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Calendar
+import Data.Time.LocalTime
+import Safe
+import System.Time (getClockTime)
+import Text.Megaparsec hiding (parse,State)
+import Text.Megaparsec.Text
+
+import Hledger.Data
+import Hledger.Utils
+
+-- $setup
+
+--- * parsing utils
+
+-- | Run a string parser with no state in the identity monad.
+runTextParser, rtp :: TextParser Identity a -> Text -> Either (ParseError Char Dec) a
+runTextParser p t =  runParser p "" t
+rtp = runTextParser
+
+-- | Run a journal parser with a null journal-parsing state.
+runJournalParser, rjp :: Monad m => TextParser m a -> Text -> m (Either (ParseError Char Dec) a)
+runJournalParser p t = runParserT p "" t
+rjp = runJournalParser
+
+-- | Run an error-raising journal parser with a null journal-parsing state.
+runErroringJournalParser, rejp :: ErroringJournalParser a -> Text -> IO (Either String a)
+runErroringJournalParser p t =
+  runExceptT $
+  runJournalParser (evalStateT p mempty)
+                   t >>=
+  either (throwError . parseErrorPretty) return
+rejp = runErroringJournalParser
+
+genericSourcePos :: SourcePos -> GenericSourcePos
+genericSourcePos p = GenericSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p) (fromIntegral . unPos $ sourceColumn p)
+
+-- | Given a parsec ParsedJournal parser, file path and data string,
+-- parse and post-process a ready-to-use Journal, or give an error.
+parseAndFinaliseJournal :: ErroringJournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+parseAndFinaliseJournal parser assrt f txt = do
+  t <- liftIO getClockTime
+  y <- liftIO getCurrentYear
+  ep <- runParserT (evalStateT parser nulljournal {jparsedefaultyear=Just y}) f txt
+  case ep of
+    Right pj -> case journalFinalise t f txt assrt pj of
+                        Right j -> return j
+                        Left e  -> throwError e
+    Left e   -> throwError $ parseErrorPretty e
+
+parseAndFinaliseJournal' :: JournalParser ParsedJournal -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+parseAndFinaliseJournal' parser assrt f txt = do
+  t <- liftIO getClockTime
+  y <- liftIO getCurrentYear
+  let ep = runParser (evalStateT parser nulljournal {jparsedefaultyear=Just y}) f txt
+  case ep of
+    Right pj -> case journalFinalise t f txt assrt pj of
+                        Right j -> return j
+                        Left e  -> throwError e
+    Left e   -> throwError $ parseErrorPretty e
+
+setYear :: Year -> JournalStateParser m ()
+setYear y = modify' (\j -> j{jparsedefaultyear=Just y})
+
+getYear :: JournalStateParser m (Maybe Year)
+getYear = fmap jparsedefaultyear get
+
+setDefaultCommodityAndStyle :: (CommoditySymbol,AmountStyle) -> ErroringJournalParser ()
+setDefaultCommodityAndStyle cs = modify' (\j -> j{jparsedefaultcommodity=Just cs})
+
+getDefaultCommodityAndStyle :: JournalStateParser m (Maybe (CommoditySymbol,AmountStyle))
+getDefaultCommodityAndStyle = jparsedefaultcommodity `fmap` get
+
+pushAccount :: AccountName -> ErroringJournalParser ()
+pushAccount acct = modify' (\j -> j{jaccounts = acct : jaccounts j})
+
+pushParentAccount :: AccountName -> ErroringJournalParser ()
+pushParentAccount acct = modify' (\j -> j{jparseparentaccounts = acct : jparseparentaccounts j})
+
+popParentAccount :: ErroringJournalParser ()
+popParentAccount = do
+  j <- get
+  case jparseparentaccounts j of
+    []       -> unexpected (Tokens ('E' :| "nd of apply account block with no beginning"))
+    (_:rest) -> put j{jparseparentaccounts=rest}
+
+getParentAccount :: ErroringJournalParser AccountName
+getParentAccount = fmap (concatAccountNames . reverse . jparseparentaccounts) get
+
+addAccountAlias :: MonadState Journal m => AccountAlias -> m ()
+addAccountAlias a = modify' (\(j@Journal{..}) -> j{jparsealiases=a:jparsealiases})
+
+getAccountAliases :: MonadState Journal m => m [AccountAlias]
+getAccountAliases = fmap jparsealiases get
+
+clearAccountAliases :: MonadState Journal m => m ()
+clearAccountAliases = modify' (\(j@Journal{..}) -> j{jparsealiases=[]})
+
+-- getTransactionCount :: MonadState Journal m =>  m Integer
+-- getTransactionCount = fmap jparsetransactioncount get
+--
+-- setTransactionCount :: MonadState Journal m => Integer -> m ()
+-- setTransactionCount i = modify' (\j -> j{jparsetransactioncount=i})
+--
+-- -- | Increment the transaction index by one and return the new value.
+-- incrementTransactionCount :: MonadState Journal m => m Integer
+-- incrementTransactionCount = do
+--   modify' (\j -> j{jparsetransactioncount=jparsetransactioncount j + 1})
+--   getTransactionCount
+
+journalAddFile :: (FilePath,Text) -> Journal -> Journal
+journalAddFile f j@Journal{jfiles=fs} = j{jfiles=fs++[f]}
+  -- append, unlike the other fields, even though we do a final reverse,
+  -- to compensate for additional reversal due to including/monoid-concatting
+
+-- -- | Terminate parsing entirely, returning the given error message
+-- -- with the current parse position prepended.
+-- parserError :: String -> ErroringJournalParser a
+-- parserError s = do
+--   pos <- getPosition
+--   parserErrorAt pos s
+
+-- | Terminate parsing entirely, returning the given error message
+-- with the given parse position prepended.
+parserErrorAt :: SourcePos -> String -> ErroringJournalParser a
+parserErrorAt pos s = throwError $ sourcePosPretty pos ++ ":\n" ++ s
+
+--- * parsers
+--- ** transaction bits
+
+statusp :: TextParser m ClearedStatus
+statusp =
+  choice'
+    [ many spacenonewline >> char '*' >> return Cleared
+    , many spacenonewline >> char '!' >> return Pending
+    , return Uncleared
+    ]
+    <?> "cleared status"
+
+codep :: TextParser m String
+codep = try (do { some spacenonewline; char '(' <?> "codep"; anyChar `manyTill` char ')' } ) <|> return ""
+
+descriptionp :: ErroringJournalParser String
+descriptionp = many (noneOf (";\n" :: [Char]))
+
+--- ** dates
+
+-- | Parse a date in YYYY/MM/DD format.
+-- Hyphen (-) 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 :: JournalStateParser m Day
+datep = do
+  -- hacky: try to ensure precise errors for invalid dates
+  -- XXX reported error position is not too good
+  -- pos <- genericSourcePos <$> getPosition
+  datestr <- do
+    c <- digitChar
+    cs <- lift $ many $ choice' [digitChar, datesepchar]
+    return $ c:cs
+  let sepchars = nub $ sort $ filter (`elem` datesepchars) datestr
+  when (length sepchars /= 1) $ fail $ "bad date, different separators used: " ++ datestr
+  let dateparts = wordsBy (`elem` datesepchars) datestr
+  currentyear <- getYear
+  [y,m,d] <- case (dateparts,currentyear) of
+              ([m,d],Just y)  -> return [show y,m,d]
+              ([_,_],Nothing) -> fail $ "partial date "++datestr++" found, but the current year is unknown"
+              ([y,m,d],_)     -> return [y,m,d]
+              _               -> fail $ "bad date: " ++ datestr
+  let maybedate = fromGregorianValid (read y) (read m) (read d)
+  case maybedate of
+    Nothing   -> fail $ "bad date: " ++ datestr
+    Just date -> return date
+  <?> "full or partial date"
+
+-- | Parse a date and time in YYYY/MM/DD HH:MM[:SS][+-ZZZZ] format.
+-- Hyphen (-) 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).
+-- Leading zeroes may be omitted (except in a timezone).
+datetimep :: ErroringJournalParser LocalTime
+datetimep = do
+  day <- datep
+  lift $ some spacenonewline
+  h <- some digitChar
+  let h' = read h
+  guard $ h' >= 0 && h' <= 23
+  char ':'
+  m <- some digitChar
+  let m' = read m
+  guard $ m' >= 0 && m' <= 59
+  s <- optional $ char ':' >> some digitChar
+  let s' = case s of Just sstr -> read sstr
+                     Nothing   -> 0
+  guard $ s' >= 0 && s' <= 59
+  {- tz <- -}
+  optional $ do
+                   plusminus <- oneOf ("-+" :: [Char])
+                   d1 <- digitChar
+                   d2 <- digitChar
+                   d3 <- digitChar
+                   d4 <- digitChar
+                   return $ plusminus:d1:d2:d3:d4:""
+  -- ltz <- liftIO $ getCurrentTimeZone
+  -- let tz' = maybe ltz (fromMaybe ltz . parseTime defaultTimeLocale "%z") tz
+  -- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
+  return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
+
+secondarydatep :: Day -> ErroringJournalParser Day
+secondarydatep primarydate = do
+  char '='
+  -- kludgy way to use primary date for default year
+  let withDefaultYear d p = do
+        y <- getYear
+        let (y',_,_) = toGregorian d in setYear y'
+        r <- p
+        when (isJust y) $ setYear $ fromJust y -- XXX
+        -- mapM setYear <$> y
+        return r
+  withDefaultYear primarydate datep
+
+-- |
+-- >> parsewith twoorthreepartdatestringp "2016/01/2"
+-- Right "2016/01/2"
+-- twoorthreepartdatestringp = do
+--   n1 <- some digitChar
+--   c <- datesepchar
+--   n2 <- some digitChar
+--   mn3 <- optional $ char c >> some digitChar
+--   return $ n1 ++ c:n2 ++ maybe "" (c:) mn3
+
+--- ** account names
+
+-- | Parse an account name, then apply any parent account prefix and/or account aliases currently in effect.
+modifiedaccountnamep :: ErroringJournalParser AccountName
+modifiedaccountnamep = do
+  parent <- getParentAccount
+  aliases <- getAccountAliases
+  a <- lift accountnamep
+  return $
+    accountNameApplyAliases aliases $
+     -- XXX accountNameApplyAliasesMemo ? doesn't seem to make a difference
+    joinAccountNames parent
+    a
+
+-- | Parse an account name. Account names start with a non-space, may
+-- have single spaces inside them, and are terminated by two or more
+-- spaces (or end of input). Also they have one or more components of
+-- at least one character, separated by the account separator char.
+-- (This parser will also consume one following space, if present.)
+accountnamep :: TextParser m AccountName
+accountnamep = do
+    astr <- do
+      c <- nonspace
+      cs <- striptrailingspace <$> many (nonspace <|> singlespace)
+      return $ c:cs
+    let a = T.pack astr
+    when (accountNameFromComponents (accountNameComponents a) /= a)
+         (fail $ "account name seems ill-formed: "++astr)
+    return a
+    where
+      singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})
+      striptrailingspace "" = ""
+      striptrailingspace s  = if last s == ' ' then init s else s
+
+-- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace
+--     <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"
+
+--- ** amounts
+
+-- | Parse whitespace then an amount, with an optional left or right
+-- currency symbol and optional price, or return the special
+-- "missing" marker amount.
+spaceandamountormissingp :: ErroringJournalParser MixedAmount
+spaceandamountormissingp =
+  try (do
+        lift $ some spacenonewline
+        (Mixed . (:[])) `fmap` amountp <|> return missingmixedamt
+      ) <|> return missingmixedamt
+
+#ifdef TESTS
+assertParseEqual' :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion
+assertParseEqual' parse expected = either (assertFailure.show) (`is'` expected) parse
+
+is' :: (Eq a, Show a) => a -> a -> Assertion
+a `is'` e = assertEqual e a
+
+test_spaceandamountormissingp = do
+    assertParseEqual' (parseWithState mempty spaceandamountormissingp " $47.18") (Mixed [usd 47.18])
+    assertParseEqual' (parseWithState mempty spaceandamountormissingp "$47.18") missingmixedamt
+    assertParseEqual' (parseWithState mempty spaceandamountormissingp " ") missingmixedamt
+    assertParseEqual' (parseWithState mempty spaceandamountormissingp "") missingmixedamt
+#endif
+
+-- | Parse a single-commodity amount, with optional symbol on the left or
+-- right, optional unit or total price, and optional (ignored)
+-- ledger-style balance assertion or fixed lot price declaration.
+amountp :: Monad m => JournalStateParser m Amount
+amountp = try leftsymbolamountp <|> try rightsymbolamountp <|> nosymbolamountp
+
+#ifdef TESTS
+test_amountp = do
+    assertParseEqual' (parseWithState mempty amountp "$47.18") (usd 47.18)
+    assertParseEqual' (parseWithState mempty amountp "$1.") (usd 1 `withPrecision` 0)
+  -- ,"amount with unit price" ~: do
+    assertParseEqual'
+     (parseWithState mempty amountp "$10 @ €0.5")
+     (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1))
+  -- ,"amount with total price" ~: do
+    assertParseEqual'
+     (parseWithState mempty amountp "$10 @@ €5")
+     (usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0))
+#endif
+
+-- | Parse an amount from a string, or get an error.
+amountp' :: String -> Amount
+amountp' s =
+  case runParser (evalStateT (amountp <* eof) mempty) "" (T.pack s) of
+    Right amt -> amt
+    Left err  -> error' $ show err -- XXX should throwError
+
+-- | Parse a mixed amount from a string, or get an error.
+mamountp' :: String -> MixedAmount
+mamountp' = Mixed . (:[]) . amountp'
+
+signp :: TextParser m String
+signp = do
+  sign <- optional $ oneOf ("+-" :: [Char])
+  return $ case sign of Just '-' -> "-"
+                        _        -> ""
+
+leftsymbolamountp :: Monad m => JournalStateParser m Amount
+leftsymbolamountp = do
+  sign <- lift signp
+  c <- lift commoditysymbolp
+  sp <- lift $ many spacenonewline
+  (q,prec,mdec,mgrps) <- lift numberp
+  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
+  p <- priceamountp
+  let applysign = if sign=="-" then negate else id
+  return $ applysign $ Amount c q p s
+  <?> "left-symbol amount"
+
+rightsymbolamountp :: Monad m => JournalStateParser m Amount
+rightsymbolamountp = do
+  (q,prec,mdec,mgrps) <- lift numberp
+  sp <- lift $ many spacenonewline
+  c <- lift commoditysymbolp
+  p <- priceamountp
+  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
+  return $ Amount c q p s
+  <?> "right-symbol amount"
+
+nosymbolamountp :: Monad m => JournalStateParser m Amount
+nosymbolamountp = do
+  (q,prec,mdec,mgrps) <- lift numberp
+  p <- priceamountp
+  -- apply the most recently seen default commodity and style to this commodityless amount
+  defcs <- getDefaultCommodityAndStyle
+  let (c,s) = case defcs of
+        Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})
+        Nothing          -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})
+  return $ Amount c q p s
+  <?> "no-symbol amount"
+
+commoditysymbolp :: TextParser m CommoditySymbol
+commoditysymbolp = (quotedcommoditysymbolp <|> simplecommoditysymbolp) <?> "commodity symbol"
+
+quotedcommoditysymbolp :: TextParser m CommoditySymbol
+quotedcommoditysymbolp = do
+  char '"'
+  s <- some $ noneOf (";\n\"" :: [Char])
+  char '"'
+  return $ T.pack s
+
+simplecommoditysymbolp :: TextParser m CommoditySymbol
+simplecommoditysymbolp = T.pack <$> some (noneOf nonsimplecommoditychars)
+
+priceamountp :: Monad m => JournalStateParser m Price
+priceamountp =
+    try (do
+          lift (many spacenonewline)
+          char '@'
+          try (do
+                char '@'
+                lift (many spacenonewline)
+                a <- amountp -- XXX can parse more prices ad infinitum, shouldn't
+                return $ TotalPrice a)
+           <|> (do
+            lift (many spacenonewline)
+            a <- amountp -- XXX can parse more prices ad infinitum, shouldn't
+            return $ UnitPrice a))
+         <|> return NoPrice
+
+partialbalanceassertionp :: ErroringJournalParser (Maybe MixedAmount)
+partialbalanceassertionp =
+    try (do
+          lift (many spacenonewline)
+          char '='
+          lift (many spacenonewline)
+          a <- amountp -- XXX should restrict to a simple amount
+          return $ Just $ Mixed [a])
+         <|> return Nothing
+
+-- balanceassertion :: Monad m => TextParser m (Maybe MixedAmount)
+-- balanceassertion =
+--     try (do
+--           lift (many spacenonewline)
+--           string "=="
+--           lift (many spacenonewline)
+--           a <- amountp -- XXX should restrict to a simple amount
+--           return $ Just $ Mixed [a])
+--          <|> return Nothing
+
+-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
+fixedlotpricep :: ErroringJournalParser (Maybe Amount)
+fixedlotpricep =
+    try (do
+          lift (many spacenonewline)
+          char '{'
+          lift (many spacenonewline)
+          char '='
+          lift (many spacenonewline)
+          a <- amountp -- XXX should restrict to a simple amount
+          lift (many spacenonewline)
+          char '}'
+          return $ Just a)
+         <|> return Nothing
+
+-- | Parse a string representation of a number for its value and display
+-- attributes.
+--
+-- Some international number formats are accepted, eg either period or comma
+-- may be used for the decimal point, and the other of these may be used for
+-- separating digit groups in the integer part. See
+-- http://en.wikipedia.org/wiki/Decimal_separator for more examples.
+--
+-- This returns: the parsed numeric value, the precision (number of digits
+-- seen following the decimal point), the decimal point character used if any,
+-- and the digit group style if any.
+--
+numberp :: TextParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
+numberp = do
+  -- a number is an optional sign followed by a sequence of digits possibly
+  -- interspersed with periods, commas, or both
+  -- ptrace "numberp"
+  sign <- signp
+  parts <- some $ choice' [some digitChar, some $ char ',', some $ char '.']
+  dbg8 "numberp parsed" (sign,parts) `seq` return ()
+
+  -- check the number is well-formed and identify the decimal point and digit
+  -- group separator characters used, if any
+  let (numparts, puncparts) = partition numeric parts
+      (ok, mdecimalpoint, mseparator) =
+          case (numparts, puncparts) of
+            ([],_)     -> (False, Nothing, Nothing)  -- no digits, not ok
+            (_,[])     -> (True, Nothing, Nothing)   -- digits with no punctuation, ok
+            (_,[[d]])  -> (True, Just d, Nothing)    -- just a single punctuation of length 1, assume it's a decimal point
+            (_,[_])    -> (False, Nothing, Nothing)  -- a single punctuation of some other length, not ok
+            (_,_:_:_)  ->                                       -- two or more punctuations
+              let (s:ss, d) = (init puncparts, last puncparts)  -- the leftmost is a separator and the rightmost may be a decimal point
+              in if any ((/=1).length) puncparts               -- adjacent punctuation chars, not ok
+                    || any (s/=) ss                            -- separator chars vary, not ok
+                    || head parts == s                        -- number begins with a separator char, not ok
+                 then (False, Nothing, Nothing)
+                 else if s == d
+                      then (True, Nothing, Just $ head s)       -- just one kind of punctuation - must be separators
+                      else (True, Just $ head d, Just $ head s) -- separator(s) and a decimal point
+  unless ok $ fail $ "number seems ill-formed: "++concat parts
+
+  -- get the digit group sizes and digit group style if any
+  let (intparts',fracparts') = span ((/= mdecimalpoint) . Just . head) parts
+      (intparts, fracpart) = (filter numeric intparts', filter numeric fracparts')
+      groupsizes = reverse $ case map length intparts of
+                               (a:b:cs) | a < b -> b:cs
+                               gs               -> gs
+      mgrps = (`DigitGroups` groupsizes) <$> mseparator
+
+  -- put the parts back together without digit group separators, get the precision and parse the value
+  let int = concat $ "":intparts
+      frac = concat $ "":fracpart
+      precision = length frac
+      int' = if null int then "0" else int
+      frac' = if null frac then "0" else frac
+      quantity = read $ sign++int'++"."++frac' -- this read should never fail
+
+  return $ dbg8 "numberp quantity,precision,mdecimalpoint,mgrps" (quantity,precision,mdecimalpoint,mgrps)
+  <?> "numberp"
+  where
+    numeric = isNumber . headDef '_'
+
+-- test_numberp = do
+--       let s `is` n = assertParseEqual (parseWithState mempty numberp s) n
+--           assertFails = assertBool . isLeft . parseWithState mempty numberp
+--       assertFails ""
+--       "0"          `is` (0, 0, '.', ',', [])
+--       "1"          `is` (1, 0, '.', ',', [])
+--       "1.1"        `is` (1.1, 1, '.', ',', [])
+--       "1,000.1"    `is` (1000.1, 1, '.', ',', [3])
+--       "1.00.000,1" `is` (100000.1, 1, ',', '.', [3,2])
+--       "1,000,000"  `is` (1000000, 0, '.', ',', [3,3])
+--       "1."         `is` (1,   0, '.', ',', [])
+--       "1,"         `is` (1,   0, ',', '.', [])
+--       ".1"         `is` (0.1, 1, '.', ',', [])
+--       ",1"         `is` (0.1, 1, ',', '.', [])
+--       assertFails "1,000.000,1"
+--       assertFails "1.000,000.1"
+--       assertFails "1,000.000.1"
+--       assertFails "1,,1"
+--       assertFails "1..1"
+--       assertFails ".1,"
+--       assertFails ",1."
+
+--- ** comments
+
+multilinecommentp :: ErroringJournalParser ()
+multilinecommentp = do
+  string "comment" >> lift (many spacenonewline) >> newline
+  go
+  where
+    go = try (eof <|> (string "end comment" >> newline >> return ()))
+         <|> (anyLine >> go)
+    anyLine = anyChar `manyTill` newline
+
+emptyorcommentlinep :: ErroringJournalParser ()
+emptyorcommentlinep = do
+  lift (many spacenonewline) >> (commentp <|> (lift (many spacenonewline) >> newline >> return ""))
+  return ()
+
+-- | Parse a possibly multi-line comment following a semicolon.
+followingcommentp :: ErroringJournalParser Text
+followingcommentp =
+  -- ptrace "followingcommentp"
+  do samelinecomment <- lift (many spacenonewline) >> (try semicoloncommentp <|> (newline >> return ""))
+     newlinecomments <- many (try (lift (some spacenonewline) >> semicoloncommentp))
+     return $ T.unlines $ samelinecomment:newlinecomments
+
+-- | Parse a possibly multi-line comment following a semicolon, and
+-- any tags and/or posting dates within it. Posting dates can be
+-- expressed with "date"/"date2" tags and/or bracketed dates.  The
+-- dates are parsed in full here so that errors are reported in the
+-- right position. Missing years can be inferred if a default date is
+-- provided.
+--
+-- >>> rejp (followingcommentandtagsp (Just $ fromGregorian 2000 1 2)) "; a:b, date:3/4, [=5/6]"
+-- Right ("a:b, date:3/4, [=5/6]\n",[("a","b"),("date","3/4")],Just 2000-03-04,Just 2000-05-06)
+--
+-- Year unspecified and no default provided -> unknown year error, at correct position:
+-- >>> rejp (followingcommentandtagsp Nothing) "  ;    xxx   date:3/4\n  ; second line"
+-- Left ...1:22...partial date 3/4 found, but the current year is unknown...
+--
+-- Date tag value contains trailing text - forgot the comma, confused:
+-- the syntaxes ?  We'll accept the leading date anyway
+-- >>> rejp (followingcommentandtagsp (Just $ fromGregorian 2000 1 2)) "; date:3/4=5/6"
+-- Right ("date:3/4=5/6\n",[("date","3/4=5/6")],Just 2000-03-04,Nothing)
+--
+followingcommentandtagsp :: Maybe Day -> ErroringJournalParser (Text, [Tag], Maybe Day, Maybe Day)
+followingcommentandtagsp mdefdate = do
+  -- pdbg 0 "followingcommentandtagsp"
+
+  -- Parse a single or multi-line comment, starting on this line or the next one.
+  -- Save the starting position and preserve all whitespace for the subsequent re-parsing,
+  -- to get good error positions.
+  startpos <- getPosition
+  commentandwhitespace :: String <- do
+    let semicoloncommentp' = (:) <$> char ';' <*> anyChar `manyTill` eolof
+    sp1 <- lift (many spacenonewline)
+    l1  <- try (lift semicoloncommentp') <|> (newline >> return "")
+    ls  <- lift . many $ try ((++) <$> some spacenonewline <*> semicoloncommentp')
+    return $ unlines $ (sp1 ++ l1) : ls
+  let comment = T.pack $ unlines $ map (lstrip . dropWhile (==';') . strip) $ lines commentandwhitespace
+  -- pdbg 0 $ "commentws:"++show commentandwhitespace
+  -- pdbg 0 $ "comment:"++show comment
+
+  -- Reparse the comment for any tags.
+  tags <- case runTextParser (setPosition startpos >> tagsp) $ T.pack commentandwhitespace of
+            Right ts -> return ts
+            Left e   -> throwError $ parseErrorPretty e
+  -- pdbg 0 $ "tags: "++show tags
+
+  -- Reparse the comment for any posting dates. Use the transaction date for defaults, if provided.
+  epdates <- liftIO $ rejp (setPosition startpos >> postingdatesp mdefdate) $ T.pack commentandwhitespace
+  pdates <- case epdates of
+              Right ds -> return ds
+              Left e   -> throwError e
+  -- pdbg 0 $ "pdates: "++show pdates
+  let mdate  = headMay $ map snd $ filter ((=="date").fst)  pdates
+      mdate2 = headMay $ map snd $ filter ((=="date2").fst) pdates
+
+  return (comment, tags, mdate, mdate2)
+
+commentp :: ErroringJournalParser Text
+commentp = commentStartingWithp commentchars
+
+commentchars :: [Char]
+commentchars = "#;*"
+
+semicoloncommentp :: ErroringJournalParser Text
+semicoloncommentp = commentStartingWithp ";"
+
+commentStartingWithp :: [Char] -> ErroringJournalParser Text
+commentStartingWithp cs = do
+  -- ptrace "commentStartingWith"
+  oneOf cs
+  lift (many spacenonewline)
+  l <- anyChar `manyTill` (lift eolof)
+  optional newline
+  return $ T.pack l
+
+--- ** tags
+
+-- | Extract any tags (name:value ended by comma or newline) embedded in a string.
+--
+-- >>> commentTags "a b:, c:c d:d, e"
+-- [("b",""),("c","c d:d")]
+--
+-- >>> commentTags "a [1/1/1] [1/1] [1], [=1/1/1] [=1/1] [=1] [1/1=1/1/1] [1=1/1/1] b:c"
+-- [("b","c")]
+--
+-- --[("date","1/1/1"),("date","1/1"),("date2","1/1/1"),("date2","1/1"),("date","1/1"),("date2","1/1/1"),("date","1"),("date2","1/1/1")]
+--
+-- >>> commentTags "\na b:, \nd:e, f"
+-- [("b",""),("d","e")]
+--
+commentTags :: Text -> [Tag]
+commentTags s =
+  case runTextParser tagsp s of
+    Right r -> r
+    Left _  -> [] -- shouldn't happen
+
+-- | Parse all tags found in a string.
+tagsp :: Parser [Tag]
+tagsp = -- do
+  -- pdbg 0 $ "tagsp"
+  many (try (nontagp >> tagp))
+
+-- | Parse everything up till the first tag.
+--
+-- >>> rtp nontagp "\na b:, \nd:e, f"
+-- Right "\na "
+nontagp :: Parser String
+nontagp = -- do
+  -- pdbg 0 "nontagp"
+  -- anyChar `manyTill` (lookAhead (try (tagorbracketeddatetagsp Nothing >> return ()) <|> eof))
+  anyChar `manyTill` lookAhead (try (void tagp) <|> eof)
+  -- XXX costly ?
+
+-- | Tags begin with a colon-suffixed tag name (a word beginning with
+-- a letter) and are followed by a tag value (any text up to a comma
+-- or newline, whitespace-stripped).
+--
+-- >>> rtp tagp "a:b b , c AuxDate: 4/2"
+-- Right ("a","b b")
+--
+tagp :: Parser Tag
+tagp = do
+  -- pdbg 0 "tagp"
+  n <- tagnamep
+  v <- tagvaluep
+  return (n,v)
+
+-- |
+-- >>> rtp tagnamep "a:"
+-- Right "a"
+tagnamep :: Parser Text
+tagnamep = -- do
+  -- pdbg 0 "tagnamep"
+  T.pack <$> some (noneOf (": \t\n" :: [Char])) <* char ':'
+
+tagvaluep :: TextParser m Text
+tagvaluep = do
+  -- ptrace "tagvalue"
+  v <- anyChar `manyTill` (void (try (char ',')) <|> eolof)
+  return $ T.pack $ strip $ reverse $ dropWhile (==',') $ reverse $ strip v
+
+--- ** posting dates
+
+-- | Parse all posting dates found in a string. Posting dates can be
+-- expressed with date/date2 tags and/or bracketed dates.  The dates
+-- are parsed fully to give useful errors. Missing years can be
+-- inferred only if a default date is provided.
+--
+postingdatesp :: Maybe Day -> ErroringJournalParser [(TagName,Day)]
+postingdatesp mdefdate = do
+  -- pdbg 0 $ "postingdatesp"
+  let p = ((:[]) <$> datetagp mdefdate) <|> bracketeddatetagsp mdefdate
+      nonp =
+         many (notFollowedBy p >> anyChar)
+         -- anyChar `manyTill` (lookAhead (try (p >> return ()) <|> eof))
+  concat <$> many (try (nonp >> p))
+
+--- ** date tags
+
+-- | Date tags are tags with name "date" or "date2". Their value is
+-- parsed as a date, using the provided default date if any for
+-- inferring a missing year if needed. Any error in date parsing is
+-- reported and terminates parsing.
+--
+-- >>> rejp (datetagp Nothing) "date: 2000/1/2 "
+-- Right ("date",2000-01-02)
+--
+-- >>> rejp (datetagp (Just $ fromGregorian 2001 2 3)) "date2:3/4"
+-- Right ("date2",2001-03-04)
+--
+-- >>> rejp (datetagp Nothing) "date:  3/4"
+-- Left ...1:9...partial date 3/4 found, but the current year is unknown...
+--
+datetagp :: Maybe Day -> ErroringJournalParser (TagName,Day)
+datetagp mdefdate = do
+  -- pdbg 0 "datetagp"
+  string "date"
+  n <- T.pack . fromMaybe "" <$> optional (string "2")
+  char ':'
+  startpos <- getPosition
+  v <- lift tagvaluep
+  -- re-parse value as a date.
+  j <- get
+  let ep :: Either (ParseError Char Dec) Day
+      ep = parseWithState'
+             j{jparsedefaultyear=first3.toGregorian <$> mdefdate}
+             -- The value extends to a comma, newline, or end of file.
+             -- It seems like ignoring any extra stuff following a date
+             -- gives better errors here.
+             (do
+                 setPosition startpos
+                 datep) -- <* eof)
+             v
+  case ep
+    of Left e  -> throwError $ parseErrorPretty e
+       Right d -> return ("date"<>n, d)
+
+--- ** bracketed dates
+
+-- tagorbracketeddatetagsp :: Monad m => Maybe Day -> TextParser u m [Tag]
+-- tagorbracketeddatetagsp mdefdate =
+--   bracketeddatetagsp mdefdate <|> ((:[]) <$> tagp)
+
+-- | 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
+-- least one digit and one date separator) is also parsed, and will
+-- throw an appropriate error.
+--
+-- The dates are parsed in full here so that errors are reported in
+-- the right position. A missing year in DATE can be inferred if a
+-- default date is provided. A missing year in DATE2 will be inferred
+-- from DATE.
+--
+-- >>> rejp (bracketeddatetagsp Nothing) "[2016/1/2=3/4]"
+-- Right [("date",2016-01-02),("date2",2016-03-04)]
+--
+-- >>> rejp (bracketeddatetagsp Nothing) "[1]"
+-- Left ...not a bracketed date...
+--
+-- >>> rejp (bracketeddatetagsp Nothing) "[2016/1/32]"
+-- Left ...1:11:...bad date: 2016/1/32...
+--
+-- >>> rejp (bracketeddatetagsp Nothing) "[1/31]"
+-- Left ...1:6:...partial date 1/31 found, but the current year is unknown...
+--
+-- >>> rejp (bracketeddatetagsp Nothing) "[0123456789/-.=/-.=]"
+-- Left ...1:15:...bad date, different separators...
+--
+bracketeddatetagsp :: Maybe Day -> ErroringJournalParser [(TagName, Day)]
+bracketeddatetagsp mdefdate = do
+  -- pdbg 0 "bracketeddatetagsp"
+  char '['
+  startpos <- getPosition
+  let digits = "0123456789"
+  s <- some (oneOf $ '=':digits++datesepchars)
+  char ']'
+  unless (any (`elem` s) digits && any (`elem` datesepchars) s) $
+    fail "not a bracketed date"
+
+  -- looks sufficiently like a bracketed date, now we
+  -- re-parse as dates and throw any errors
+  j <- get
+  let ep :: Either (ParseError Char Dec) (Maybe Day, Maybe Day)
+      ep = parseWithState'
+             j{jparsedefaultyear=first3.toGregorian <$> mdefdate}
+             (do
+               setPosition startpos
+               md1 <- optional datep
+               maybe (return ()) (setYear.first3.toGregorian) md1
+               md2 <- optional $ char '=' >> datep
+               eof
+               return (md1,md2)
+             )
+             (T.pack s)
+  case ep
+    of Left e          -> throwError $ parseErrorPretty e
+       Right (md1,md2) -> return $ catMaybes
+         [("date",) <$> md1, ("date2",) <$> md2]
+
diff --git a/Hledger/Read/CsvReader.hs b/Hledger/Read/CsvReader.hs
--- a/Hledger/Read/CsvReader.hs
+++ b/Hledger/Read/CsvReader.hs
@@ -6,6 +6,11 @@
 -}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
 
 module Hledger.Read.CsvReader (
   -- * Reader
@@ -25,11 +30,15 @@
 import Control.Exception hiding (try)
 import Control.Monad
 import Control.Monad.Except
+import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
 -- import Test.HUnit
 import Data.Char (toLower, isDigit, isSpace)
 import 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 Data.Time.Calendar (Day)
 #if MIN_VERSION_time(1,5,0)
 import Data.Time.Format (parseTimeM, defaultTimeLocale)
@@ -41,17 +50,17 @@
 import System.Directory (doesFileExist)
 import System.FilePath
 import System.IO (stderr)
-import Test.HUnit
+import Test.HUnit hiding (State)
 import Text.CSV (parseCSV, CSV)
-import Text.Parsec hiding (parse)
-import Text.Parsec.Pos
-import Text.Parsec.Error
+import Text.Megaparsec hiding (parse, State)
+import Text.Megaparsec.Text
+import qualified Text.Parsec as Parsec
 import Text.Printf (hPrintf,printf)
 
 import Hledger.Data
 import Hledger.Utils.UTF8IOCompat (getContents)
 import Hledger.Utils
-import Hledger.Read.JournalReader (amountp, statusp, genericSourcePos)
+import Hledger.Read.Common (amountp, statusp, genericSourcePos)
 
 
 reader :: Reader
@@ -61,18 +70,19 @@
 format = "csv"
 
 -- | Does the given file path and data look like it might be CSV ?
-detect :: FilePath -> String -> Bool
-detect f s
+detect :: FilePath -> Text -> Bool
+detect f t
   | f /= "-"  = takeExtension f == '.':format  -- from a file: yes if the extension is .csv
-  | otherwise = length (filter (==',') s) >= 2 -- from stdin: yes if there are two or more commas
+  | otherwise = T.length (T.filter (==',') t) >= 2 -- from stdin: yes if there are two or more commas
 
 -- | Parse and post-process a "Journal" from CSV data, or give an error.
 -- XXX currently ignores the string and reads from the file path
-parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal
-parse rulesfile _ f s = do
-  r <- liftIO $ readJournalFromCsv rulesfile f s
+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+parse rulesfile _ f t = do
+  r <- liftIO $ readJournalFromCsv rulesfile f t
   case r of Left e -> throwError e
-            Right j -> return j
+            Right j -> return $ journalNumberAndTieTransactions j
+-- XXX does not use parseAndFinaliseJournal like the other readers
 
 -- | Read a Journal from the given CSV data (and filename, used for error
 -- messages), or return an error. Proceed as follows:
@@ -85,7 +95,7 @@
 -- 4. parse the rules file
 -- 5. convert the CSV records to a journal using the rules
 -- @
-readJournalFromCsv :: Maybe FilePath -> FilePath -> String -> IO (Either String 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 -> return $ Left $ show (e :: IOException)) $ do
@@ -100,7 +110,7 @@
   rules_ <- liftIO $ runExceptT $ parseRulesFile rulesfile
   let rules = case rules_ of
               Right (t::CsvRules) -> t
-              Left err -> throwerr $ show err
+              Left err -> throwerr err
   dbg2IO "rules" rules
 
   -- apply skip directive
@@ -115,7 +125,7 @@
   records <- (either throwerr id .
               dbg2 "validateCsv" . validateCsv skip .
               dbg2 "parseCsv")
-             `fmap` parseCsv parsecfilename csvdata
+             `fmap` parseCsv parsecfilename (T.unpack csvdata)
   dbg1IO "first 3 csv records" $ take 3 records
 
   -- identify header lines
@@ -124,7 +134,12 @@
 
   -- convert to transactions and return as a journal
   let txns = snd $ mapAccumL
-                     (\pos r -> (pos, transactionFromCsvRecord (incSourceLine pos 1) rules r))
+                     (\pos r -> (pos,
+                                 transactionFromCsvRecord
+                                   (let SourcePos name line col =  pos in
+                                    SourcePos name (unsafePos $ unPos line + 1) col)
+                                   rules
+                                    r))
                      (initialPos parsecfilename) records
 
   -- heuristic: if the records appear to have been in reverse date order,
@@ -134,14 +149,14 @@
             | otherwise = txns
   return $ Right nulljournal{jtxns=sortBy (comparing tdate) txns'}
 
-parseCsv :: FilePath -> String -> IO (Either ParseError CSV)
+parseCsv :: FilePath -> String -> IO (Either Parsec.ParseError CSV)
 parseCsv path csvdata =
   case path of
     "-" -> liftM (parseCSV "(stdin)") getContents
     _   -> return $ parseCSV path csvdata
 
 -- | Return the cleaned up and validated CSV data, or an error.
-validateCsv :: Int -> Either ParseError CSV -> Either String [CsvRecord]
+validateCsv :: Int -> Either Parsec.ParseError CSV -> Either String [CsvRecord]
 validateCsv _ (Left e) = Left $ show e
 validateCsv numhdrlines (Right rs) = validate $ drop numhdrlines $ filternulls rs
   where
@@ -296,6 +311,8 @@
   rconditionalblocks :: [ConditionalBlock]
 } deriving (Show, Eq)
 
+type CsvRulesParser a = StateT CsvRules Parser a
+
 type DirectiveName    = String
 type CsvFieldName     = String
 type CsvFieldIndex    = Int
@@ -339,39 +356,42 @@
 getDirective :: DirectiveName -> CsvRules -> Maybe FieldTemplate
 getDirective directivename = lookup directivename . rdirectives
 
+instance ShowErrorComponent String where
+  showErrorComponent = id
 
 parseRulesFile :: FilePath -> ExceptT String IO CsvRules
 parseRulesFile f = do
   s <- liftIO $ (readFile' f >>= expandIncludes (takeDirectory f))
   let rules = parseCsvRules f s
   case rules of
-    Left e -> ExceptT $ return $ Left $ show e
+    Left e -> ExceptT $ return $ Left $ parseErrorPretty e
     Right r -> do
                r_ <- liftIO $ runExceptT $ validateRules r
                ExceptT $ case r_ of
-                 Left e -> return $ Left $ show $ toParseError e
+                 Left e -> return $ Left $ parseErrorPretty $ toParseError e
                  Right r -> return $ Right r
   where
-    toParseError s = newErrorMessage (Message s) (initialPos "")
+    toParseError :: forall s. Ord s => s -> ParseError Char s
+    toParseError s = (mempty :: ParseError Char s) { errorCustom = S.singleton s}
 
 -- | Pre-parse csv rules to interpolate included files, recursively.
 -- This is a cheap hack to avoid rewriting the existing parser.
-expandIncludes :: FilePath -> String -> IO String
+expandIncludes :: FilePath -> T.Text -> IO T.Text
 expandIncludes basedir content = do
-  let (ls,rest) = break (isPrefixOf "include") $ lines content
+  let (ls,rest) = break (T.isPrefixOf "include") $ T.lines content
   case rest of
-    [] -> return $ unlines ls
-    (('i':'n':'c':'l':'u':'d':'e':f):ls') -> do
-      let f'       = basedir </> dropWhile isSpace f
+    [] -> return $ T.unlines ls
+    ((T.stripPrefix "include" -> Just f):ls') -> do
+      let f'       = basedir </> dropWhile isSpace (T.unpack f)
           basedir' = takeDirectory f'
-      included <- readFile f' >>= expandIncludes basedir'
-      return $ unlines [unlines ls, included, unlines ls']
-    ls' -> return $ unlines $ ls ++ ls'   -- should never get here
+      included <- readFile' f' >>= expandIncludes basedir'
+      return $ T.unlines [T.unlines ls, included, T.unlines ls']
+    ls' -> return $ T.unlines $ ls ++ ls'   -- should never get here
 
-parseCsvRules :: FilePath -> String -> Either ParseError CsvRules
+parseCsvRules :: FilePath -> T.Text -> Either (ParseError Char Dec) CsvRules
 -- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s
 parseCsvRules rulesfile s =
-  runParser rulesp rules rulesfile s
+  runParser (evalStateT rulesp rules) rulesfile s
 
 -- | Return the validated rules, or an error.
 validateRules :: CsvRules -> ExceptT String IO CsvRules
@@ -389,42 +409,42 @@
 
 -- parsers
 
-rulesp :: Stream [Char] m t => ParsecT [Char] CsvRules m CsvRules
+rulesp :: CsvRulesParser CsvRules
 rulesp = do
-  many $ choice'
-    [blankorcommentlinep                                                    <?> "blank or comment line"
-    ,(directivep        >>= modifyState . addDirective)                     <?> "directive"
-    ,(fieldnamelistp    >>= modifyState . setIndexesAndAssignmentsFromList) <?> "field name list"
-    ,(fieldassignmentp  >>= modifyState . addAssignment)                    <?> "field assignment"
-    ,(conditionalblockp >>= modifyState . addConditionalBlock)              <?> "conditional block"
+  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 <- getState
+  r <- get
   return r{rdirectives=reverse $ rdirectives r
           ,rassignments=reverse $ rassignments r
           ,rconditionalblocks=reverse $ rconditionalblocks r
           }
 
-blankorcommentlinep :: Stream [Char] m t => ParsecT [Char] CsvRules m ()
-blankorcommentlinep = pdbg 3 "trying blankorcommentlinep" >> choice' [blanklinep, commentlinep]
+blankorcommentlinep :: CsvRulesParser ()
+blankorcommentlinep = lift (pdbg 3 "trying blankorcommentlinep") >> choiceInState [blanklinep, commentlinep]
 
-blanklinep :: Stream [Char] m t => ParsecT [Char] CsvRules m ()
-blanklinep = many spacenonewline >> newline >> return () <?> "blank line"
+blanklinep :: CsvRulesParser ()
+blanklinep = lift (many spacenonewline) >> newline >> return () <?> "blank line"
 
-commentlinep :: Stream [Char] m t => ParsecT [Char] CsvRules m ()
-commentlinep = many spacenonewline >> commentcharp >> restofline >> return () <?> "comment line"
+commentlinep :: CsvRulesParser ()
+commentlinep = lift (many spacenonewline) >> commentcharp >> lift restofline >> return () <?> "comment line"
 
-commentcharp :: Stream [Char] m t => ParsecT [Char] CsvRules m Char
-commentcharp = oneOf ";#*"
+commentcharp :: CsvRulesParser Char
+commentcharp = oneOf (";#*" :: [Char])
 
-directivep :: Stream [Char] m t => ParsecT [Char] CsvRules m (DirectiveName, String)
-directivep = do
-  pdbg 3 "trying directive"
-  d <- choice' $ map string directives
-  v <- (((char ':' >> many spacenonewline) <|> many1 spacenonewline) >> directivevalp)
-       <|> (optional (char ':') >> many spacenonewline >> eolof >> return "")
+directivep :: CsvRulesParser (DirectiveName, String)
+directivep = (do
+  lift $ pdbg 3 "trying directive"
+  d <- choiceInState $ map string directives
+  v <- (((char ':' >> lift (many spacenonewline)) <|> lift (some spacenonewline)) >> directivevalp)
+       <|> (optional (char ':') >> lift (many spacenonewline) >> lift eolof >> return "")
   return (d,v)
-  <?> "directive"
+  ) <?> "directive"
 
 directives =
   ["date-format"
@@ -436,46 +456,46 @@
    -- ,"base-currency"
   ]
 
-directivevalp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
-directivevalp = anyChar `manyTill` eolof
+directivevalp :: CsvRulesParser String
+directivevalp = anyChar `manyTill` lift eolof
 
-fieldnamelistp :: Stream [Char] m t => ParsecT [Char] CsvRules m [CsvFieldName]
+fieldnamelistp :: CsvRulesParser [CsvFieldName]
 fieldnamelistp = (do
-  pdbg 3 "trying fieldnamelist"
+  lift $ pdbg 3 "trying fieldnamelist"
   string "fields"
   optional $ char ':'
-  many1 spacenonewline
-  let separator = many spacenonewline >> char ',' >> many spacenonewline
-  f <- fromMaybe "" <$> optionMaybe fieldnamep
-  fs <- many1 $ (separator >> fromMaybe "" <$> optionMaybe fieldnamep)
-  restofline
+  lift (some spacenonewline)
+  let separator = lift (many spacenonewline) >> char ',' >> lift (many spacenonewline)
+  f <- fromMaybe "" <$> optional fieldnamep
+  fs <- some $ (separator >> fromMaybe "" <$> optional fieldnamep)
+  lift restofline
   return $ map (map toLower) $ f:fs
   ) <?> "field name list"
 
-fieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
+fieldnamep :: CsvRulesParser String
 fieldnamep = quotedfieldnamep <|> barefieldnamep
 
-quotedfieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
+quotedfieldnamep :: CsvRulesParser String
 quotedfieldnamep = do
   char '"'
-  f <- many1 $ noneOf "\"\n:;#~"
+  f <- some $ noneOf ("\"\n:;#~" :: [Char])
   char '"'
   return f
 
-barefieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
-barefieldnamep = many1 $ noneOf " \t\n,;#~"
+barefieldnamep :: CsvRulesParser String
+barefieldnamep = some $ noneOf (" \t\n,;#~" :: [Char])
 
-fieldassignmentp :: Stream [Char] m t => ParsecT [Char] CsvRules m (JournalFieldName, FieldTemplate)
+fieldassignmentp :: CsvRulesParser (JournalFieldName, FieldTemplate)
 fieldassignmentp = do
-  pdbg 3 "trying fieldassignment"
+  lift $ pdbg 3 "trying fieldassignmentp"
   f <- journalfieldnamep
   assignmentseparatorp
   v <- fieldvalp
   return (f,v)
   <?> "field assignment"
 
-journalfieldnamep :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
-journalfieldnamep = pdbg 2 "trying journalfieldnamep" >> choice' (map string journalfieldnames)
+journalfieldnamep :: CsvRulesParser String
+journalfieldnamep = lift (pdbg 2 "trying journalfieldnamep") >> choiceInState (map string journalfieldnames)
 
 journalfieldnames =
   [-- pseudo fields:
@@ -494,74 +514,74 @@
   ,"comment"
   ]
 
-assignmentseparatorp :: Stream [Char] m t => ParsecT [Char] CsvRules m ()
+assignmentseparatorp :: CsvRulesParser ()
 assignmentseparatorp = do
-  pdbg 3 "trying assignmentseparatorp"
+  lift $ pdbg 3 "trying assignmentseparatorp"
   choice [
-    -- try (many spacenonewline >> oneOf ":="),
-    try (many spacenonewline >> char ':'),
-    space
+    -- try (lift (many spacenonewline) >> oneOf ":="),
+    try (lift (many spacenonewline) >> char ':'),
+    spaceChar
     ]
-  _ <- many spacenonewline
+  _ <- lift (many spacenonewline)
   return ()
 
-fieldvalp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
+fieldvalp :: CsvRulesParser String
 fieldvalp = do
-  pdbg 2 "trying fieldval"
-  anyChar `manyTill` eolof
+  lift $ pdbg 2 "trying fieldvalp"
+  anyChar `manyTill` lift eolof
 
-conditionalblockp :: Stream [Char] m t => ParsecT [Char] CsvRules m ConditionalBlock
+conditionalblockp :: CsvRulesParser ConditionalBlock
 conditionalblockp = do
-  pdbg 3 "trying conditionalblockp"
-  string "if" >> many spacenonewline >> optional newline
-  ms <- many1 recordmatcherp
-  as <- many (many1 spacenonewline >> fieldassignmentp)
+  lift $ pdbg 3 "trying conditionalblockp"
+  string "if" >> lift (many spacenonewline) >> optional newline
+  ms <- some recordmatcherp
+  as <- many (lift (some spacenonewline) >> fieldassignmentp)
   when (null as) $
     fail "start of conditional block found, but no assignment rules afterward\n(assignment rules in a conditional block should be indented)\n"
   return (ms, as)
   <?> "conditional block"
 
-recordmatcherp :: Stream [Char] m t => ParsecT [Char] CsvRules m [[Char]]
+recordmatcherp :: CsvRulesParser [String]
 recordmatcherp = do
-  pdbg 2 "trying recordmatcherp"
+  lift $ pdbg 2 "trying recordmatcherp"
   -- pos <- currentPos
-  _  <- optional (matchoperatorp >> many spacenonewline >> optional newline)
+  _  <- optional (matchoperatorp >> lift (many spacenonewline) >> optional newline)
   ps <- patternsp
   when (null ps) $
     fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"
   return ps
   <?> "record matcher"
 
-matchoperatorp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
-matchoperatorp = choice' $ map string
+matchoperatorp :: CsvRulesParser String
+matchoperatorp = choiceInState $ map string
   ["~"
   -- ,"!~"
   -- ,"="
   -- ,"!="
   ]
 
-patternsp :: Stream [Char] m t => ParsecT [Char] CsvRules m [[Char]]
+patternsp :: CsvRulesParser [String]
 patternsp = do
-  pdbg 3 "trying patternsp"
+  lift $ pdbg 3 "trying patternsp"
   ps <- many regexp
   return ps
 
-regexp :: Stream [Char] m t => ParsecT [Char] CsvRules m [Char]
+regexp :: CsvRulesParser String
 regexp = do
-  pdbg 3 "trying regexp"
+  lift $ pdbg 3 "trying regexp"
   notFollowedBy matchoperatorp
-  c <- nonspace
-  cs <- anyChar `manyTill` eolof
+  c <- lift nonspace
+  cs <- anyChar `manyTill` lift eolof
   return $ strip $ c:cs
 
 -- fieldmatcher = do
 --   pdbg 2 "trying fieldmatcher"
---   f <- fromMaybe "all" `fmap` (optionMaybe $ do
+--   f <- fromMaybe "all" `fmap` (optional $ do
 --          f' <- fieldname
---          many spacenonewline
+--          lift (many spacenonewline)
 --          return f')
 --   char '~'
---   many spacenonewline
+--   lift (many spacenonewline)
 --   ps <- patterns
 --   let r = "(" ++ intercalate "|" ps ++ ")"
 --   return (f,r)
@@ -605,7 +625,9 @@
     status      =
       case mfieldtemplate "status" of
         Nothing  -> Uncleared
-        Just str -> either statuserror id $ runParser (statusp <* eof) nullctx "" $ render str
+        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)"
@@ -617,7 +639,7 @@
     precomment  = maybe "" render $ mfieldtemplate "precomment"
     currency    = maybe (fromMaybe "" mdefaultcurrency) render $ mfieldtemplate "currency"
     amountstr   = (currency++) $ negateIfParenthesised $ getAmountStr rules record
-    amount      = either amounterror (Mixed . (:[])) $ runParser (amountp <* eof) nullctx "" amountstr
+    amount      = either amounterror (Mixed . (:[])) $ runParser (evalStateT (amountp <* eof) mempty) "" $ T.pack amountstr
     amounterror err = error' $ unlines
       ["error: could not parse \""++amountstr++"\" as an amount"
       ,showRecord record
@@ -638,8 +660,8 @@
     defaccount2 = case isNegativeMixedAmount amount2 of
                    Just True -> "income:unknown"
                    _         -> "expenses:unknown"
-    account1    = maybe "" render (mfieldtemplate "account1") `or` defaccount1
-    account2    = maybe "" render (mfieldtemplate "account2") `or` defaccount2
+    account1    = T.pack $ maybe "" render (mfieldtemplate "account1") `or` defaccount1
+    account2    = T.pack $ maybe "" render (mfieldtemplate "account2") `or` defaccount2
 
     -- build the transaction
     t = nulltransaction{
@@ -647,10 +669,10 @@
       tdate                    = date',
       tdate2                   = mdate2',
       tstatus                  = status,
-      tcode                    = code,
-      tdescription             = description,
-      tcomment                 = comment,
-      tpreceding_comment_lines = precomment,
+      tcode                    = T.pack code,
+      tdescription             = T.pack description,
+      tcomment                 = T.pack comment,
+      tpreceding_comment_lines = T.pack precomment,
       tpostings                =
         [posting {paccount=account2, pamount=amount2, ptransaction=Just t}
         ,posting {paccount=account1, pamount=amount1, ptransaction=Just t}
@@ -780,20 +802,23 @@
     assertParseEqual (parseCsvRules "unknown" "") rules
 
   -- ,"convert rules parsing: accountrule" ~: do
-  --    assertParseEqual (parseWithCtx rules accountrule "A\na\n") -- leading blank line required
+  --    assertParseEqual (parseWithState rules accountrule "A\na\n") -- leading blank line required
   --                ([("A",Nothing)], "a")
 
   ,"convert rules parsing: trailing comments" ~: do
-     assertParse (parseWithCtx rules rulesp "skip\n# \n#\n")
+     assertParse (parseWithState' rules rulesp "skip\n# \n#\n")
 
   ,"convert rules parsing: trailing blank lines" ~: do
-     assertParse (parseWithCtx rules rulesp "skip\n\n  \n")
+     assertParse (parseWithState' rules rulesp "skip\n\n  \n")
 
+  ,"convert rules parsing: empty field value" ~: do
+     assertParse (parseWithState' rules rulesp "account1 \nif foo\n  account2 foo\n")
+
   -- not supported
   -- ,"convert rules parsing: no final newline" ~: do
-  --    assertParse (parseWithCtx rules csvrulesfile "A\na")
-  --    assertParse (parseWithCtx rules csvrulesfile "A\na\n# \n#")
-  --    assertParse (parseWithCtx rules csvrulesfile "A\na\n\n  ")
+  --    assertParse (parseWithState rules csvrulesfile "A\na")
+  --    assertParse (parseWithState rules csvrulesfile "A\na\n# \n#")
+  --    assertParse (parseWithState rules csvrulesfile "A\na\n\n  ")
 
                  -- (rules{
                  --   -- dateField=Maybe FieldPosition,
diff --git a/Hledger/Read/JournalReader.hs b/Hledger/Read/JournalReader.hs
--- a/Hledger/Read/JournalReader.hs
+++ b/Hledger/Read/JournalReader.hs
@@ -1,1201 +1,722 @@
--- {-# OPTIONS_GHC -F -pgmF htfpp #-}
-{-# LANGUAGE CPP, RecordWildCards, NoMonoLocalBinds, ScopedTypeVariables #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-|
-
-A reader for hledger's journal file format
-(<http://hledger.org/MANUAL.html#the-journal-file>).  hledger's journal
-format is a compatible subset of c++ ledger's
-(<http://ledger-cli.org/3.0/doc/ledger3.html#Journal-Format>), so this
-reader should handle many ledger files as well. Example:
-
-@
-2012\/3\/24 gift
-    expenses:gifts  $10
-    assets:cash
-@
-
--}
-
-module Hledger.Read.JournalReader (
-  -- * Reader
-  reader,
-  -- * Parsers used elsewhere
-  parseAndFinaliseJournal,
-  genericSourcePos,
-  getParentAccount,
-  journalp,
-  directivep,
-  defaultyeardirectivep,
-  marketpricedirectivep,
-  datetimep,
-  codep,
-  accountnamep,
-  modifiedaccountnamep,
-  postingp,
-  amountp,
-  amountp',
-  mamountp',
-  numberp,
-  statusp,
-  emptyorcommentlinep,
-  followingcommentp,
-  accountaliasp
-  -- * Tests
-  ,tests_Hledger_Read_JournalReader
-#ifdef TESTS
-  -- disabled by default, HTF not available on windows
-  ,htf_thisModulesTests
-  ,htf_Hledger_Read_JournalReader_importedTests
-#endif
-)
-where
-import Prelude ()
-import Prelude.Compat hiding (readFile)
-import qualified Control.Exception as C
-import Control.Monad.Compat
-import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError, catchError)
-import Data.Char (isNumber)
-import Data.List.Compat
-import Data.List.Split (wordsBy)
-import Data.Maybe
-import Data.Time.Calendar
-import Data.Time.LocalTime
-import Safe (headDef, lastDef)
-import Test.HUnit
-#ifdef TESTS
-import Test.Framework
-import Text.Parsec.Error
-#endif
-import Text.Parsec hiding (parse)
-import Text.Printf
-import System.FilePath
-import System.Time (getClockTime)
-
-import Hledger.Data
-import Hledger.Utils
-
-
--- standard reader exports
-
-reader :: Reader
-reader = Reader format detect parse
-
-format :: String
-format = "journal"
-
--- | Does the given file path and data look like it might be hledger's journal format ?
-detect :: FilePath -> String -> Bool
-detect f s
-  | f /= "-"  = takeExtension f `elem` ['.':format, ".j"]  -- from a file: yes if the extension is .journal or .j
-  -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented)
-  | otherwise = regexMatches "^[0-9]+.*\n[ \t]+" s
-
--- | Parse and post-process a "Journal" from hledger's journal file
--- format, or give an error.
-parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal
-parse _ = parseAndFinaliseJournal journalp
-
--- parsing utils
-
-genericSourcePos :: SourcePos -> GenericSourcePos
-genericSourcePos p = GenericSourcePos (sourceName p) (sourceLine p) (sourceColumn p)
-
--- | Flatten a list of JournalUpdate's (journal-transforming
--- monadic actions which can do IO or raise an exception) into a
--- single equivalent action.
-combineJournalUpdates :: [JournalUpdate] -> JournalUpdate
-combineJournalUpdates us = foldl' (flip (.)) id <$> sequence us
--- XXX may be contributing to excessive stack use
-
--- cf http://neilmitchell.blogspot.co.uk/2015/09/detecting-space-leaks.html
--- $ ./devprof +RTS -K576K -xc
--- *** Exception (reporting due to +RTS -xc): (THUNK_STATIC), stack trace: 
---   Hledger.Read.JournalReader.combineJournalUpdates.\,
---   called from Hledger.Read.JournalReader.combineJournalUpdates,
---   called from Hledger.Read.JournalReader.fixedlotprice,
---   called from Hledger.Read.JournalReader.partialbalanceassertion,
---   called from Hledger.Read.JournalReader.getDefaultCommodityAndStyle,
---   called from Hledger.Read.JournalReader.priceamount,
---   called from Hledger.Read.JournalReader.nosymbolamount,
---   called from Hledger.Read.JournalReader.numberp,
---   called from Hledger.Read.JournalReader.rightsymbolamount,
---   called from Hledger.Read.JournalReader.simplecommoditysymbol,
---   called from Hledger.Read.JournalReader.quotedcommoditysymbol,
---   called from Hledger.Read.JournalReader.commoditysymbol,
---   called from Hledger.Read.JournalReader.signp,
---   called from Hledger.Read.JournalReader.leftsymbolamount,
---   called from Hledger.Read.JournalReader.amountp,
---   called from Hledger.Read.JournalReader.spaceandamountormissing,
---   called from Hledger.Read.JournalReader.accountnamep.singlespace,
---   called from Hledger.Utils.Parse.nonspace,
---   called from Hledger.Read.JournalReader.accountnamep,
---   called from Hledger.Read.JournalReader.getAccountAliases,
---   called from Hledger.Read.JournalReader.getParentAccount,
---   called from Hledger.Read.JournalReader.modifiedaccountnamep,
---   called from Hledger.Read.JournalReader.postingp,
---   called from Hledger.Read.JournalReader.postings,
---   called from Hledger.Read.JournalReader.commentStartingWith,
---   called from Hledger.Read.JournalReader.semicoloncomment,
---   called from Hledger.Read.JournalReader.followingcommentp,
---   called from Hledger.Read.JournalReader.descriptionp,
---   called from Hledger.Read.JournalReader.codep,
---   called from Hledger.Read.JournalReader.statusp,
---   called from Hledger.Utils.Parse.spacenonewline,
---   called from Hledger.Read.JournalReader.secondarydatep,
---   called from Hledger.Data.Dates.datesepchar,
---   called from Hledger.Read.JournalReader.datep,
---   called from Hledger.Read.JournalReader.transaction,
---   called from Hledger.Utils.Parse.choice',
---   called from Hledger.Read.JournalReader.directive,
---   called from Hledger.Read.JournalReader.emptyorcommentlinep,
---   called from Hledger.Read.JournalReader.multilinecommentp,
---   called from Hledger.Read.JournalReader.journal.journalItem,
---   called from Hledger.Read.JournalReader.journal,
---   called from Hledger.Read.JournalReader.parseJournalWith,
---   called from Hledger.Read.readJournal.tryReaders.firstSuccessOrBestError,
---   called from Hledger.Read.readJournal.tryReaders,
---   called from Hledger.Read.readJournal,
---   called from Main.main,
---   called from Main.CAF
--- Stack space overflow: current size 33568 bytes.
-
--- | Given a JournalUpdate-generating parsec parser, file path and data string,
--- parse and post-process a Journal so that it's ready to use, or give an error.
-parseAndFinaliseJournal ::
-  (ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate,JournalContext))
-  -> Bool -> FilePath -> String -> ExceptT String IO Journal
-parseAndFinaliseJournal parser assrt f s = do
-  tc <- liftIO getClockTime
-  tl <- liftIO getCurrentLocalTime
-  y <- liftIO getCurrentYear
-  r <- runParserT parser nullctx{ctxYear=Just y} f s
-  case r of
-    Right (updates,ctx) -> do
-                           j <- ap updates (return nulljournal)
-                           case journalFinalise tc tl f s ctx assrt j of
-                             Right j'  -> return j'
-                             Left estr -> throwError estr
-    Left e -> throwError $ show e
-
-setYear :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()
-setYear y = modifyState (\ctx -> ctx{ctxYear=Just y})
-
-getYear :: Stream [Char] m Char => ParsecT s JournalContext m (Maybe Integer)
-getYear = liftM ctxYear getState
-
-setDefaultCommodityAndStyle :: Stream [Char] m Char => (Commodity,AmountStyle) -> ParsecT [Char] JournalContext m ()
-setDefaultCommodityAndStyle cs = modifyState (\ctx -> ctx{ctxDefaultCommodityAndStyle=Just cs})
-
-getDefaultCommodityAndStyle :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe (Commodity,AmountStyle))
-getDefaultCommodityAndStyle = ctxDefaultCommodityAndStyle `fmap` getState
-
-pushParentAccount :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m ()
-pushParentAccount parent = modifyState addParentAccount
-    where addParentAccount ctx0 = ctx0 { ctxAccount = parent : ctxAccount ctx0 }
-
-popParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
-popParentAccount = do ctx0 <- getState
-                      case ctxAccount ctx0 of
-                        [] -> unexpected "End of account block with no beginning"
-                        (_:rest) -> setState $ ctx0 { ctxAccount = rest }
-
-getParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-getParentAccount = liftM (concatAccountNames . reverse . ctxAccount) getState
-
-addAccountAlias :: Stream [Char] m Char => AccountAlias -> ParsecT [Char] JournalContext m ()
-addAccountAlias a = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=a:ctxAliases})
-
-getAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m [AccountAlias]
-getAccountAliases = liftM ctxAliases getState
-
-clearAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
-clearAccountAliases = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=[]})
-
-getIndex :: Stream [Char] m Char => ParsecT s JournalContext m Integer
-getIndex = liftM ctxTransactionIndex getState
-
-setIndex :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()
-setIndex i = modifyState (\ctx -> ctx{ctxTransactionIndex=i})
-
--- parsers
-
--- | Top-level journal parser. Returns a single composite, I/O performing,
--- error-raising "JournalUpdate" (and final "JournalContext") which can be
--- applied to an empty journal to get the final result.
-journalp :: ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate,JournalContext)
-journalp = do
-  journalupdates <- many journalItem
-  eof
-  finalctx <- getState
-  return $ (combineJournalUpdates journalupdates, finalctx)
-    where
-      -- As all journal line types can be distinguished by the first
-      -- character, excepting transactions versus empty (blank or
-      -- comment-only) lines, can use choice w/o try
-      journalItem = choice [ directivep
-                           , liftM (return . addTransaction) transactionp
-                           , liftM (return . addModifierTransaction) modifiertransactionp
-                           , liftM (return . addPeriodicTransaction) periodictransactionp
-                           , liftM (return . addMarketPrice) marketpricedirectivep
-                           , emptyorcommentlinep >> return (return id)
-                           , multilinecommentp >> return (return id)
-                           ] <?> "journal transaction or directive"
-
--- cf http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives
-directivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-directivep = do
-  optional $ char '!'
-  choice' [
-    includedirectivep
-   ,aliasdirectivep
-   ,endaliasesdirectivep
-   ,accountdirectivep
-   ,enddirectivep
-   ,tagdirectivep
-   ,endtagdirectivep
-   ,defaultyeardirectivep
-   ,defaultcommoditydirectivep
-   ,commodityconversiondirectivep
-   ,ignoredpricecommoditydirectivep
-   ]
-  <?> "directive"
-
-includedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-includedirectivep = do
-  string "include"
-  many1 spacenonewline
-  filename <- restofline
-  outerState <- getState
-  outerPos <- getPosition
-  let curdir = takeDirectory (sourceName outerPos)
-  let (u::ExceptT String IO (Journal -> Journal, JournalContext)) = do
-       filepath <- expandPath curdir filename
-       txt <- readFileOrError outerPos filepath
-       let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"
-       r <- runParserT journalp outerState filepath txt
-       case r of
-         Right (ju, ctx) -> do
-                            u <- combineJournalUpdates [ return $ journalAddFile (filepath,txt)
-                                                       , ju
-                                                       ] `catchError` (throwError . (inIncluded ++))
-                            return (u, ctx)
-         Left err -> throwError $ inIncluded ++ show err
-       where readFileOrError pos fp =
-                ExceptT $ liftM Right (readFile' fp) `C.catch`
-                  \e -> return $ Left $ printf "%s reading %s:\n%s" (show pos) fp (show (e::C.IOException))
-  r <- liftIO $ runExceptT u
-  case r of
-    Left err -> return $ throwError err
-    Right (ju, _finalparsectx) -> return $ ExceptT $ return $ Right ju
-
-journalAddFile :: (FilePath,String) -> Journal -> Journal
-journalAddFile f j@Journal{files=fs} = j{files=fs++[f]}
- -- NOTE: first encountered file to left, to avoid a reverse
-
-accountdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-accountdirectivep = do
-  string "account"
-  many1 spacenonewline
-  parent <- accountnamep
-  newline
-  pushParentAccount parent
-  -- return $ return id
-  return $ ExceptT $ return $ Right id
-
-enddirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-enddirectivep = do
-  string "end"
-  popParentAccount
-  -- return (return id)
-  return $ ExceptT $ return $ Right id
-
-aliasdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-aliasdirectivep = do
-  string "alias"
-  many1 spacenonewline
-  alias <- accountaliasp
-  addAccountAlias alias
-  return $ return id
-
-accountaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
-accountaliasp = regexaliasp <|> basicaliasp
-
-basicaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
-basicaliasp = do
-  -- pdbg 0 "basicaliasp"
-  old <- rstrip <$> (many1 $ noneOf "=")
-  char '='
-  many spacenonewline
-  new <- rstrip <$> anyChar `manyTill` eolof  -- don't require a final newline, good for cli options
-  return $ BasicAlias old new
-
-regexaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
-regexaliasp = do
-  -- pdbg 0 "regexaliasp"
-  char '/'
-  re <- many1 $ noneOf "/\n\r" -- paranoid: don't try to read past line end
-  char '/'
-  many spacenonewline
-  char '='
-  many spacenonewline
-  repl <- rstrip <$> anyChar `manyTill` eolof
-  return $ RegexAlias re repl
-
-endaliasesdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-endaliasesdirectivep = do
-  string "end aliases"
-  clearAccountAliases
-  return (return id)
-
-tagdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-tagdirectivep = do
-  string "tag" <?> "tag directive"
-  many1 spacenonewline
-  _ <- many1 nonspace
-  restofline
-  return $ return id
-
-endtagdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-endtagdirectivep = do
-  (string "end tag" <|> string "pop") <?> "end tag or pop directive"
-  restofline
-  return $ return id
-
-defaultyeardirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-defaultyeardirectivep = do
-  char 'Y' <?> "default year"
-  many spacenonewline
-  y <- many1 digit
-  let y' = read y
-  failIfInvalidYear y
-  setYear y'
-  return $ return id
-
-defaultcommoditydirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-defaultcommoditydirectivep = do
-  char 'D' <?> "default commodity"
-  many1 spacenonewline
-  Amount{..} <- amountp
-  setDefaultCommodityAndStyle (acommodity, astyle)
-  restofline
-  return $ return id
-
-marketpricedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) MarketPrice
-marketpricedirectivep = do
-  char 'P' <?> "market price"
-  many spacenonewline
-  date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored
-  many1 spacenonewline
-  symbol <- commoditysymbolp
-  many spacenonewline
-  price <- amountp
-  restofline
-  return $ MarketPrice date symbol price
-
-ignoredpricecommoditydirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-ignoredpricecommoditydirectivep = do
-  char 'N' <?> "ignored-price commodity"
-  many1 spacenonewline
-  commoditysymbolp
-  restofline
-  return $ return id
-
-commodityconversiondirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
-commodityconversiondirectivep = do
-  char 'C' <?> "commodity conversion"
-  many1 spacenonewline
-  amountp
-  many spacenonewline
-  char '='
-  many spacenonewline
-  amountp
-  restofline
-  return $ return id
-
-modifiertransactionp :: ParsecT [Char] JournalContext (ExceptT String IO) ModifierTransaction
-modifiertransactionp = do
-  char '=' <?> "modifier transaction"
-  many spacenonewline
-  valueexpr <- restofline
-  postings <- postingsp
-  return $ ModifierTransaction valueexpr postings
-
-periodictransactionp :: ParsecT [Char] JournalContext (ExceptT String IO) PeriodicTransaction
-periodictransactionp = do
-  char '~' <?> "periodic transaction"
-  many spacenonewline
-  periodexpr <- restofline
-  postings <- postingsp
-  return $ PeriodicTransaction periodexpr postings
-
--- | Parse a (possibly unbalanced) transaction.
-transactionp :: ParsecT [Char] JournalContext (ExceptT String IO) Transaction
-transactionp = do
-  -- ptrace "transactionp"
-  sourcepos <- genericSourcePos <$> getPosition
-  date <- datep <?> "transaction"
-  edate <- optionMaybe (secondarydatep date) <?> "secondary date"
-  lookAhead (spacenonewline <|> newline) <?> "whitespace or newline"
-  status <- statusp <?> "cleared status"
-  code <- codep <?> "transaction code"
-  description <- descriptionp >>= return . strip
-  comment <- try followingcommentp <|> (newline >> return "")
-  let tags = tagsInComment comment
-  postings <- postingsp
-  i' <- (+1) <$> getIndex
-  setIndex i'
-  return $ txnTieKnot $ Transaction i' sourcepos date edate status code description comment tags postings ""
-
-descriptionp = many (noneOf ";\n")
-
-#ifdef TESTS
-test_transactionp = do
-    let s `gives` t = do
-                        let p = parseWithCtx nullctx transactionp s
-                        assertBool $ isRight p
-                        let Right t2 = p
-                            -- same f = assertEqual (f t) (f t2)
-                        assertEqual (tdate t) (tdate t2)
-                        assertEqual (tdate2 t) (tdate2 t2)
-                        assertEqual (tstatus t) (tstatus t2)
-                        assertEqual (tcode t) (tcode t2)
-                        assertEqual (tdescription t) (tdescription t2)
-                        assertEqual (tcomment t) (tcomment t2)
-                        assertEqual (ttags t) (ttags t2)
-                        assertEqual (tpreceding_comment_lines t) (tpreceding_comment_lines t2)
-                        assertEqual (show $ tpostings t) (show $ tpostings t2)
-    -- "0000/01/01\n\n" `gives` nulltransaction
-    unlines [
-      "2012/05/14=2012/05/15 (code) desc  ; tcomment1",
-      "    ; tcomment2",
-      "    ; ttag1: val1",
-      "    * a         $1.00  ; pcomment1",
-      "    ; pcomment2",
-      "    ; ptag1: val1",
-      "    ; ptag2: val2"
-      ]
-     `gives`
-     nulltransaction{
-      tdate=parsedate "2012/05/14",
-      tdate2=Just $ parsedate "2012/05/15",
-      tstatus=Uncleared,
-      tcode="code",
-      tdescription="desc",
-      tcomment=" tcomment1\n tcomment2\n ttag1: val1\n",
-      ttags=[("ttag1","val1")],
-      tpostings=[
-        nullposting{
-          pstatus=Cleared,
-          paccount="a",
-          pamount=Mixed [usd 1],
-          pcomment=" pcomment1\n pcomment2\n ptag1: val1\n  ptag2: val2\n",
-          ptype=RegularPosting,
-          ptags=[("ptag1","val1"),("ptag2","val2")],
-          ptransaction=Nothing
-          }
-        ],
-      tpreceding_comment_lines=""
-      }
-    unlines [
-      "2015/1/1",
-      ]
-     `gives`
-     nulltransaction{
-      tdate=parsedate "2015/01/01",
-      }
-
-    assertRight $ parseWithCtx nullctx transactionp $ unlines
-      ["2007/01/28 coopportunity"
-      ,"    expenses:food:groceries                   $47.18"
-      ,"    assets:checking                          $-47.18"
-      ,""
-      ]
-
-    -- transactionp should not parse just a date
-    assertLeft $ parseWithCtx nullctx transactionp "2009/1/1\n"
-
-    -- transactionp should not parse just a date and description
-    assertLeft $ parseWithCtx nullctx transactionp "2009/1/1 a\n"
-
-    -- transactionp should not parse a following comment as part of the description
-    let p = parseWithCtx nullctx transactionp "2009/1/1 a ;comment\n b 1\n"
-    assertRight p
-    assertEqual "a" (let Right p' = p in tdescription p')
-
-    -- parse transaction with following whitespace line
-    assertRight $ parseWithCtx nullctx transactionp $ unlines
-        ["2012/1/1"
-        ,"  a  1"
-        ,"  b"
-        ," "
-        ]
-
-    let p = parseWithCtx nullctx transactionp $ unlines
-             ["2009/1/1 x  ; transaction comment"
-             ," a  1  ; posting 1 comment"
-             ," ; posting 1 comment 2"
-             ," b"
-             ," ; posting 2 comment"
-             ]
-    assertRight p
-    assertEqual 2 (let Right t = p in length $ tpostings t)
-#endif
-
--- | Parse a date in YYYY/MM/DD format.
--- Hyphen (-) 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 :: Stream [Char] m t => ParsecT [Char] JournalContext m Day
-datep = do
-  -- hacky: try to ensure precise errors for invalid dates
-  -- XXX reported error position is not too good
-  -- pos <- genericSourcePos <$> getPosition
-  datestr <- do
-    c <- digit
-    cs <- many $ choice' [digit, datesepchar]
-    return $ c:cs
-  let sepchars = nub $ sort $ filter (`elem` datesepchars) datestr
-  when (length sepchars /= 1) $ fail $ "bad date, different separators used: " ++ datestr
-  let dateparts = wordsBy (`elem` datesepchars) datestr
-  currentyear <- getYear
-  [y,m,d] <- case (dateparts,currentyear) of
-              ([m,d],Just y)  -> return [show y,m,d]
-              ([_,_],Nothing) -> fail $ "partial date "++datestr++" found, but the current year is unknown"
-              ([y,m,d],_)     -> return [y,m,d]
-              _               -> fail $ "bad date: " ++ datestr
-  let maybedate = fromGregorianValid (read y) (read m) (read d)
-  case maybedate of
-    Nothing   -> fail $ "bad date: " ++ datestr
-    Just date -> return date
-  <?> "full or partial date"
-
--- | Parse a date and time in YYYY/MM/DD HH:MM[:SS][+-ZZZZ] format.
--- Hyphen (-) 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).
--- Leading zeroes may be omitted (except in a timezone).
-datetimep :: Stream [Char] m Char => ParsecT [Char] JournalContext m LocalTime
-datetimep = do
-  day <- datep
-  many1 spacenonewline
-  h <- many1 digit
-  let h' = read h
-  guard $ h' >= 0 && h' <= 23
-  char ':'
-  m <- many1 digit
-  let m' = read m
-  guard $ m' >= 0 && m' <= 59
-  s <- optionMaybe $ char ':' >> many1 digit
-  let s' = case s of Just sstr -> read sstr
-                     Nothing   -> 0
-  guard $ s' >= 0 && s' <= 59
-  {- tz <- -}
-  optionMaybe $ do
-                   plusminus <- oneOf "-+"
-                   d1 <- digit
-                   d2 <- digit
-                   d3 <- digit
-                   d4 <- digit
-                   return $ plusminus:d1:d2:d3:d4:""
-  -- ltz <- liftIO $ getCurrentTimeZone
-  -- let tz' = maybe ltz (fromMaybe ltz . parseTime defaultTimeLocale "%z") tz
-  -- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
-  return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
-
-secondarydatep :: Stream [Char] m Char => Day -> ParsecT [Char] JournalContext m Day
-secondarydatep primarydate = do
-  char '='
-  -- kludgy way to use primary date for default year
-  let withDefaultYear d p = do
-        y <- getYear
-        let (y',_,_) = toGregorian d in setYear y'
-        r <- p
-        when (isJust y) $ setYear $ fromJust y
-        return r
-  edate <- withDefaultYear primarydate datep
-  return edate
-
-statusp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ClearedStatus
-statusp =
-  choice'
-    [ many spacenonewline >> char '*' >> return Cleared
-    , many spacenonewline >> char '!' >> return Pending
-    , return Uncleared
-    ]
-    <?> "cleared status"
-
-codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-codep = try (do { many1 spacenonewline; char '(' <?> "codep"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""
-
--- Parse the following whitespace-beginning lines as postings, posting tags, and/or comments.
-postingsp :: Stream [Char] m Char => ParsecT [Char] JournalContext m [Posting]
-postingsp = many (try postingp) <?> "postings"
-
--- linebeginningwithspaces :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
--- linebeginningwithspaces = do
---   sp <- many1 spacenonewline
---   c <- nonspace
---   cs <- restofline
---   return $ sp ++ (c:cs) ++ "\n"
-
-postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting
-postingp = do
-  many1 spacenonewline
-  status <- statusp
-  many spacenonewline
-  account <- modifiedaccountnamep
-  let (ptype, account') = (accountNamePostingType account, unbracket account)
-  amount <- spaceandamountormissingp
-  massertion <- partialbalanceassertionp
-  _ <- fixedlotpricep
-  many spacenonewline
-  ctx <- getState
-  comment <- try followingcommentp <|> (newline >> return "")
-  let tags = tagsInComment comment
-  -- oh boy
-  date <- case dateValueFromTags tags of
-        Nothing -> return Nothing
-        Just v -> case runParser (datep <* eof) ctx "" v of
-                    Right d -> return $ Just d
-                    Left err -> parserFail $ show err
-  date2 <- case date2ValueFromTags tags of
-        Nothing -> return Nothing
-        Just v -> case runParser (datep <* eof) ctx "" v of
-                    Right d -> return $ Just d
-                    Left err -> parserFail $ show err
-  return posting
-   { pdate=date
-   , pdate2=date2
-   , pstatus=status
-   , paccount=account'
-   , pamount=amount
-   , pcomment=comment
-   , ptype=ptype
-   , ptags=tags
-   , pbalanceassertion=massertion
-   }
-
-#ifdef TESTS
-test_postingp = do
-    let s `gives` ep = do
-                         let parse = parseWithCtx nullctx postingp s
-                         assertBool -- "postingp parser"
-                           $ isRight parse
-                         let Right ap = parse
-                             same f = assertEqual (f ep) (f ap)
-                         same pdate
-                         same pstatus
-                         same paccount
-                         same pamount
-                         same pcomment
-                         same ptype
-                         same ptags
-                         same ptransaction
-    "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n" `gives`
-      posting{paccount="expenses:food:dining", pamount=Mixed [usd 10], pcomment=" a: a a \n b: b b \n", ptags=[("a","a a"), ("b","b b")]}
-
-    " a  1 ; [2012/11/28]\n" `gives`
-      ("a" `post` num 1){pcomment=" [2012/11/28]\n"
-                        ,ptags=[("date","2012/11/28")]
-                        ,pdate=parsedateM "2012/11/28"}
-
-    " a  1 ; a:a, [=2012/11/28]\n" `gives`
-      ("a" `post` num 1){pcomment=" a:a, [=2012/11/28]\n"
-                        ,ptags=[("a","a"), ("date2","2012/11/28")]
-                        ,pdate=Nothing}
-
-    " a  1 ; a:a\n  ; [2012/11/28=2012/11/29],b:b\n" `gives`
-      ("a" `post` num 1){pcomment=" a:a\n [2012/11/28=2012/11/29],b:b\n"
-                        ,ptags=[("a","a"), ("date","2012/11/28"), ("date2","2012/11/29"), ("b","b")]
-                        ,pdate=parsedateM "2012/11/28"}
-
-    assertBool -- "postingp parses a quoted commodity with numbers"
-      (isRight $ parseWithCtx nullctx postingp "  a  1 \"DE123\"\n")
-
-  -- ,"postingp parses balance assertions and fixed lot prices" ~: do
-    assertBool (isRight $ parseWithCtx nullctx postingp "  a  1 \"DE123\" =$1 { =2.2 EUR} \n")
-
-    -- let parse = parseWithCtx nullctx postingp " a\n ;next-line comment\n"
-    -- assertRight parse
-    -- let Right p = parse
-    -- assertEqual "next-line comment\n" (pcomment p)
-    -- assertEqual (Just nullmixedamt) (pbalanceassertion p)
-#endif
-
--- | Parse an account name, then apply any parent account prefix and/or account aliases currently in effect.
-modifiedaccountnamep :: Stream [Char] m Char => ParsecT [Char] JournalContext m AccountName
-modifiedaccountnamep = do
-  parent <- getParentAccount
-  aliases <- getAccountAliases
-  a <- accountnamep
-  return $
-    accountNameApplyAliases aliases $
-     -- XXX accountNameApplyAliasesMemo ? doesn't seem to make a difference
-    joinAccountNames parent
-    a
-
--- | Parse an account name. Account names start with a non-space, may
--- have single spaces inside them, and are terminated by two or more
--- spaces (or end of input). Also they have one or more components of
--- at least one character, separated by the account separator char.
--- (This parser will also consume one following space, if present.)
-accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName
-accountnamep = do
-    a <- do
-      c <- nonspace
-      cs <- striptrailingspace <$> many (nonspace <|> singlespace)
-      return $ c:cs
-    when (accountNameFromComponents (accountNameComponents a) /= a)
-         (fail $ "account name seems ill-formed: "++a)
-    return a
-    where
-      singlespace = try (do {spacenonewline; do {notFollowedBy spacenonewline; return ' '}})
-      striptrailingspace "" = ""
-      striptrailingspace s  = if last s == ' ' then init s else s
-
--- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace
---     <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"
-
--- | Parse whitespace then an amount, with an optional left or right
--- currency symbol and optional price, or return the special
--- "missing" marker amount.
-spaceandamountormissingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m MixedAmount
-spaceandamountormissingp =
-  try (do
-        many1 spacenonewline
-        (Mixed . (:[])) `fmap` amountp <|> return missingmixedamt
-      ) <|> return missingmixedamt
-
-#ifdef TESTS
-assertParseEqual' :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion
-assertParseEqual' parse expected = either (assertFailure.show) (`is'` expected) parse
-
-is' :: (Eq a, Show a) => a -> a -> Assertion
-a `is'` e = assertEqual e a
-
-test_spaceandamountormissingp = do
-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp " $47.18") (Mixed [usd 47.18])
-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp "$47.18") missingmixedamt
-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp " ") missingmixedamt
-    assertParseEqual' (parseWithCtx nullctx spaceandamountormissingp "") missingmixedamt
-#endif
-
--- | Parse a single-commodity amount, with optional symbol on the left or
--- right, optional unit or total price, and optional (ignored)
--- ledger-style balance assertion or fixed lot price declaration.
-amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
-amountp = try leftsymbolamountp <|> try rightsymbolamountp <|> nosymbolamountp
-
-#ifdef TESTS
-test_amountp = do
-    assertParseEqual' (parseWithCtx nullctx amountp "$47.18") (usd 47.18)
-    assertParseEqual' (parseWithCtx nullctx amountp "$1.") (usd 1 `withPrecision` 0)
-  -- ,"amount with unit price" ~: do
-    assertParseEqual'
-     (parseWithCtx nullctx amountp "$10 @ €0.5")
-     (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1))
-  -- ,"amount with total price" ~: do
-    assertParseEqual'
-     (parseWithCtx nullctx amountp "$10 @@ €5")
-     (usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0))
-#endif
-
--- | Parse an amount from a string, or get an error.
-amountp' :: String -> Amount
-amountp' s =
-  case runParser (amountp <* eof) nullctx "" s of
-    Right t -> t
-    Left err -> error' $ show err
-
--- | Parse a mixed amount from a string, or get an error.
-mamountp' :: String -> MixedAmount
-mamountp' = Mixed . (:[]) . amountp'
-
-signp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
-signp = do
-  sign <- optionMaybe $ oneOf "+-"
-  return $ case sign of Just '-' -> "-"
-                        _        -> ""
-
-leftsymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
-leftsymbolamountp = do
-  sign <- signp
-  c <- commoditysymbolp
-  sp <- many spacenonewline
-  (q,prec,mdec,mgrps) <- numberp
-  let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
-  p <- priceamountp
-  let applysign = if sign=="-" then negate else id
-  return $ applysign $ Amount c q p s
-  <?> "left-symbol amount"
-
-rightsymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
-rightsymbolamountp = do
-  (q,prec,mdec,mgrps) <- numberp
-  sp <- many spacenonewline
-  c <- commoditysymbolp
-  p <- priceamountp
-  let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
-  return $ Amount c q p s
-  <?> "right-symbol amount"
-
-nosymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
-nosymbolamountp = do
-  (q,prec,mdec,mgrps) <- numberp
-  p <- priceamountp
-  -- apply the most recently seen default commodity and style to this commodityless amount
-  defcs <- getDefaultCommodityAndStyle
-  let (c,s) = case defcs of
-        Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) prec})
-        Nothing          -> ("", amountstyle{asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps})
-  return $ Amount c q p s
-  <?> "no-symbol amount"
-
-commoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
-commoditysymbolp = (quotedcommoditysymbolp <|> simplecommoditysymbolp) <?> "commodity symbol"
-
-quotedcommoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
-quotedcommoditysymbolp = do
-  char '"'
-  s <- many1 $ noneOf ";\n\""
-  char '"'
-  return s
-
-simplecommoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
-simplecommoditysymbolp = many1 (noneOf nonsimplecommoditychars)
-
-priceamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Price
-priceamountp =
-    try (do
-          many spacenonewline
-          char '@'
-          try (do
-                char '@'
-                many spacenonewline
-                a <- amountp -- XXX can parse more prices ad infinitum, shouldn't
-                return $ TotalPrice a)
-           <|> (do
-            many spacenonewline
-            a <- amountp -- XXX can parse more prices ad infinitum, shouldn't
-            return $ UnitPrice a))
-         <|> return NoPrice
-
-partialbalanceassertionp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Maybe MixedAmount)
-partialbalanceassertionp =
-    try (do
-          many spacenonewline
-          char '='
-          many spacenonewline
-          a <- amountp -- XXX should restrict to a simple amount
-          return $ Just $ Mixed [a])
-         <|> return Nothing
-
--- balanceassertion :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe MixedAmount)
--- balanceassertion =
---     try (do
---           many spacenonewline
---           string "=="
---           many spacenonewline
---           a <- amountp -- XXX should restrict to a simple amount
---           return $ Just $ Mixed [a])
---          <|> return Nothing
-
--- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
-fixedlotpricep :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe Amount)
-fixedlotpricep =
-    try (do
-          many spacenonewline
-          char '{'
-          many spacenonewline
-          char '='
-          many spacenonewline
-          a <- amountp -- XXX should restrict to a simple amount
-          many spacenonewline
-          char '}'
-          return $ Just a)
-         <|> return Nothing
-
--- | Parse a string representation of a number for its value and display
--- attributes.
---
--- Some international number formats are accepted, eg either period or comma
--- may be used for the decimal point, and the other of these may be used for
--- separating digit groups in the integer part. See
--- http://en.wikipedia.org/wiki/Decimal_separator for more examples.
---
--- This returns: the parsed numeric value, the precision (number of digits
--- seen following the decimal point), the decimal point character used if any,
--- and the digit group style if any.
---
-numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
-numberp = do
-  -- a number is an optional sign followed by a sequence of digits possibly
-  -- interspersed with periods, commas, or both
-  -- ptrace "numberp"
-  sign <- signp
-  parts <- many1 $ choice' [many1 digit, many1 $ char ',', many1 $ char '.']
-  dbg8 "numberp parsed" (sign,parts) `seq` return ()
-
-  -- check the number is well-formed and identify the decimal point and digit
-  -- group separator characters used, if any
-  let (numparts, puncparts) = partition numeric parts
-      (ok, mdecimalpoint, mseparator) =
-          case (numparts, puncparts) of
-            ([],_)     -> (False, Nothing, Nothing)  -- no digits, not ok
-            (_,[])     -> (True, Nothing, Nothing)   -- digits with no punctuation, ok
-            (_,[[d]])  -> (True, Just d, Nothing)    -- just a single punctuation of length 1, assume it's a decimal point
-            (_,[_])    -> (False, Nothing, Nothing)  -- a single punctuation of some other length, not ok
-            (_,_:_:_)  ->                                       -- two or more punctuations
-              let (s:ss, d) = (init puncparts, last puncparts)  -- the leftmost is a separator and the rightmost may be a decimal point
-              in if (any ((/=1).length) puncparts               -- adjacent punctuation chars, not ok
-                     || any (s/=) ss                            -- separator chars vary, not ok
-                     || head parts == s)                        -- number begins with a separator char, not ok
-                 then (False, Nothing, Nothing)
-                 else if s == d
-                      then (True, Nothing, Just $ head s)       -- just one kind of punctuation - must be separators
-                      else (True, Just $ head d, Just $ head s) -- separator(s) and a decimal point
-  when (not ok) (fail $ "number seems ill-formed: "++concat parts)
-
-  -- get the digit group sizes and digit group style if any
-  let (intparts',fracparts') = span ((/= mdecimalpoint) . Just . head) parts
-      (intparts, fracpart) = (filter numeric intparts', filter numeric fracparts')
-      groupsizes = reverse $ case map length intparts of
-                               (a:b:cs) | a < b -> b:cs
-                               gs               -> gs
-      mgrps = maybe Nothing (Just . (`DigitGroups` groupsizes)) $ mseparator
-
-  -- put the parts back together without digit group separators, get the precision and parse the value
-  let int = concat $ "":intparts
-      frac = concat $ "":fracpart
-      precision = length frac
-      int' = if null int then "0" else int
-      frac' = if null frac then "0" else frac
-      quantity = read $ sign++int'++"."++frac' -- this read should never fail
-
-  return $ dbg8 "numberp quantity,precision,mdecimalpoint,mgrps" (quantity,precision,mdecimalpoint,mgrps)
-  <?> "numberp"
-  where
-    numeric = isNumber . headDef '_'
-
--- test_numberp = do
---       let s `is` n = assertParseEqual (parseWithCtx nullctx numberp s) n
---           assertFails = assertBool . isLeft . parseWithCtx nullctx numberp
---       assertFails ""
---       "0"          `is` (0, 0, '.', ',', [])
---       "1"          `is` (1, 0, '.', ',', [])
---       "1.1"        `is` (1.1, 1, '.', ',', [])
---       "1,000.1"    `is` (1000.1, 1, '.', ',', [3])
---       "1.00.000,1" `is` (100000.1, 1, ',', '.', [3,2])
---       "1,000,000"  `is` (1000000, 0, '.', ',', [3,3])
---       "1."         `is` (1,   0, '.', ',', [])
---       "1,"         `is` (1,   0, ',', '.', [])
---       ".1"         `is` (0.1, 1, '.', ',', [])
---       ",1"         `is` (0.1, 1, ',', '.', [])
---       assertFails "1,000.000,1"
---       assertFails "1.000,000.1"
---       assertFails "1,000.000.1"
---       assertFails "1,,1"
---       assertFails "1..1"
---       assertFails ".1,"
---       assertFails ",1."
-
--- comment parsers
-
-multilinecommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
-multilinecommentp = do
-  string "comment" >> many spacenonewline >> newline
-  go
-  where
-    go = try (eof <|> (string "end comment" >> newline >> return ()))
-         <|> (anyLine >> go)
-    anyLine = anyChar `manyTill` newline
-
-emptyorcommentlinep :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
-emptyorcommentlinep = do
-  many spacenonewline >> (commentp <|> (many spacenonewline >> newline >> return ""))
-  return ()
-
-followingcommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-followingcommentp =
-  -- ptrace "followingcommentp"
-  do samelinecomment <- many spacenonewline >> (try semicoloncommentp <|> (newline >> return ""))
-     newlinecomments <- many (try (many1 spacenonewline >> semicoloncommentp))
-     return $ unlines $ samelinecomment:newlinecomments
-
-commentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-commentp = commentStartingWithp commentchars
-
-commentchars :: [Char]
-commentchars = "#;*"
-
-semicoloncommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-semicoloncommentp = commentStartingWithp ";"
-
-commentStartingWithp :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m String
-commentStartingWithp cs = do
-  -- ptrace "commentStartingWith"
-  oneOf cs
-  many spacenonewline
-  l <- anyChar `manyTill` eolof
-  optional newline
-  return l
-
-tagsInComment :: String -> [Tag]
-tagsInComment c = concatMap tagsInCommentLine $ lines c'
-  where
-    c' = ledgerDateSyntaxToTags c
-
-tagsInCommentLine :: String -> [Tag]
-tagsInCommentLine = catMaybes . map maybetag . map strip . splitAtElement ','
-  where
-    maybetag s = case runParser (tagp <* eof) nullctx "" s of
-                  Right t -> Just t
-                  Left _ -> Nothing
-
-tagp = do
-  -- ptrace "tag"
-  n <- tagnamep
-  v <- tagvaluep
-  return (n,v)
-
-tagnamep = do
-  -- ptrace "tagname"
-  n <- many1 $ noneOf ": \t"
-  char ':'
-  return n
-
-tagvaluep = do
-  -- ptrace "tagvalue"
-  v <- anyChar `manyTill` ((char ',' >> return ()) <|> eolof)
-  return $ strip $ reverse $ dropWhile (==',') $ reverse $ strip v
-
-ledgerDateSyntaxToTags :: String -> String
-ledgerDateSyntaxToTags = regexReplaceBy "\\[[-.\\/0-9=]+\\]" replace
-  where
-    replace ('[':s) | lastDef ' ' s == ']' = replace' $ init s
-    replace s = s
-
-    replace' s | isdate s = datetag s
-    replace' ('=':s) | isdate s = date2tag s
-    replace' s | last s =='=' && isdate (init s) = datetag (init s)
-    replace' s | length ds == 2 && isdate d1 && isdate d1 = datetag d1 ++ date2tag d2
-      where
-        ds = splitAtElement '=' s
-        d1 = headDef "" ds
-        d2 = lastDef "" ds
-    replace' s = s
-
-    isdate = isJust . parsedateM
-    datetag s = "date:"++s++", "
-    date2tag s = "date2:"++s++", "
-
-#ifdef TESTS
-test_ledgerDateSyntaxToTags = do
-     assertEqual "date2:2012/11/28, " $ ledgerDateSyntaxToTags "[=2012/11/28]"
-#endif
-
-dateValueFromTags, date2ValueFromTags :: [Tag] -> Maybe String
-dateValueFromTags  ts = maybe Nothing (Just . snd) $ find ((=="date") . fst) ts
-date2ValueFromTags ts = maybe Nothing (Just . snd) $ find ((=="date2") . fst) ts
-
-
-tests_Hledger_Read_JournalReader = TestList $ concat [
-    -- test_numberp
- ]
-
-{- old hunit tests
-
-tests_Hledger_Read_JournalReader = TestList $ concat [
-    test_numberp,
-    test_amountp,
-    test_spaceandamountormissingp,
-    test_tagcomment,
-    test_inlinecomment,
-    test_comments,
-    test_ledgerDateSyntaxToTags,
-    test_postingp,
-    test_transactionp,
-    [
-   "modifiertransactionp" ~: do
-     assertParse (parseWithCtx nullctx modifiertransactionp "= (some value expr)\n some:postings  1\n")
-
-  ,"periodictransactionp" ~: do
-     assertParse (parseWithCtx nullctx periodictransactionp "~ (some period expr)\n some:postings  1\n")
-
-  ,"directivep" ~: do
-     assertParse (parseWithCtx nullctx directivep "!include /some/file.x\n")
-     assertParse (parseWithCtx nullctx directivep "account some:account\n")
-     assertParse (parseWithCtx nullctx (directivep >> directivep) "!account a\nend\n")
-
-  ,"comment" ~: do
-     assertParse (parseWithCtx nullctx comment "; some comment \n")
-     assertParse (parseWithCtx nullctx comment " \t; x\n")
-     assertParse (parseWithCtx nullctx comment "#x")
-
-  ,"datep" ~: do
-     assertParse (parseWithCtx nullctx datep "2011/1/1")
-     assertParseFailure (parseWithCtx nullctx datep "1/1")
-     assertParse (parseWithCtx nullctx{ctxYear=Just 2011} datep "1/1")
-
-  ,"datetimep" ~: do
-      let p = do {t <- datetimep; eof; return t}
-          bad = assertParseFailure . parseWithCtx nullctx p
-          good = assertParse . parseWithCtx nullctx p
-      bad "2011/1/1"
-      bad "2011/1/1 24:00:00"
-      bad "2011/1/1 00:60:00"
-      bad "2011/1/1 00:00:60"
-      good "2011/1/1 00:00"
-      good "2011/1/1 23:59:59"
-      good "2011/1/1 3:5:7"
-      -- timezone is parsed but ignored
-      let startofday = LocalTime (fromGregorian 2011 1 1) (TimeOfDay 0 0 (fromIntegral 0))
-      assertParseEqual (parseWithCtx nullctx p "2011/1/1 00:00-0800") startofday
-      assertParseEqual (parseWithCtx nullctx p "2011/1/1 00:00+1234") startofday
-
-  ,"defaultyeardirectivep" ~: do
-     assertParse (parseWithCtx nullctx defaultyeardirectivep "Y 2010\n")
-     assertParse (parseWithCtx nullctx defaultyeardirectivep "Y 10001\n")
-
-  ,"marketpricedirectivep" ~:
-    assertParseEqual (parseWithCtx nullctx marketpricedirectivep "P 2004/05/01 XYZ $55.00\n") (MarketPrice (parsedate "2004/05/01") "XYZ" $ usd 55)
-
-  ,"ignoredpricecommoditydirectivep" ~: do
-     assertParse (parseWithCtx nullctx ignoredpricecommoditydirectivep "N $\n")
-
-  ,"defaultcommoditydirectivep" ~: do
-     assertParse (parseWithCtx nullctx defaultcommoditydirectivep "D $1,000.0\n")
-
-  ,"commodityconversiondirectivep" ~: do
-     assertParse (parseWithCtx nullctx commodityconversiondirectivep "C 1h = $50.00\n")
-
-  ,"tagdirectivep" ~: do
-     assertParse (parseWithCtx nullctx tagdirectivep "tag foo \n")
-
-  ,"endtagdirectivep" ~: do
-     assertParse (parseWithCtx nullctx endtagdirectivep "end tag \n")
-     assertParse (parseWithCtx nullctx endtagdirectivep "pop \n")
-
-  ,"accountnamep" ~: do
-    assertBool "accountnamep parses a normal account name" (isRight $ parsewith accountnamep "a:b:c")
-    assertBool "accountnamep rejects an empty inner component" (isLeft $ parsewith accountnamep "a::c")
-    assertBool "accountnamep rejects an empty leading component" (isLeft $ parsewith accountnamep ":b:c")
-    assertBool "accountnamep rejects an empty trailing component" (isLeft $ parsewith accountnamep "a:b:")
-
-  ,"leftsymbolamountp" ~: do
-    assertParseEqual (parseWithCtx nullctx leftsymbolamountp "$1")  (usd 1 `withPrecision` 0)
-    assertParseEqual (parseWithCtx nullctx leftsymbolamountp "$-1") (usd (-1) `withPrecision` 0)
-    assertParseEqual (parseWithCtx nullctx leftsymbolamountp "-$1") (usd (-1) `withPrecision` 0)
-
-  ,"amount" ~: do
-     let -- | compare a parse result with an expected amount, showing the debug representation for clarity
-         assertAmountParse parseresult amount =
-             (either (const "parse error") showAmountDebug parseresult) ~?= (showAmountDebug amount)
-     assertAmountParse (parseWithCtx nullctx amountp "1 @ $2")
-       (num 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0))
-
- ]]
--}
-
+--- * 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.
+
+{-|
+
+A reader for hledger's journal file format
+(<http://hledger.org/MANUAL.html#the-journal-file>).  hledger's journal
+format is a compatible subset of c++ ledger's
+(<http://ledger-cli.org/3.0/doc/ledger3.html#Journal-Format>), so this
+reader should handle many ledger files as well. Example:
+
+@
+2012\/3\/24 gift
+    expenses:gifts  $10
+    assets:cash
+@
+
+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.
+
+-}
+
+--- * module
+
+{-# LANGUAGE CPP, RecordWildCards, NamedFieldPuns, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections, OverloadedStrings #-}
+
+module Hledger.Read.JournalReader (
+--- * exports
+
+  -- * Reader
+  reader,
+
+  -- * Parsing utils
+  genericSourcePos,
+  parseAndFinaliseJournal,
+  runJournalParser,
+  rjp,
+  runErroringJournalParser,
+  rejp,
+
+  -- * Parsers used elsewhere
+  getParentAccount,
+  journalp,
+  directivep,
+  defaultyeardirectivep,
+  marketpricedirectivep,
+  datetimep,
+  datep,
+  -- codep,
+  -- accountnamep,
+  modifiedaccountnamep,
+  postingp,
+  -- amountp,
+  -- amountp',
+  -- mamountp',
+  -- numberp,
+  statusp,
+  emptyorcommentlinep,
+  followingcommentp,
+  accountaliasp
+
+  -- * Tests
+  ,tests_Hledger_Read_JournalReader
+
+)
+where
+--- * imports
+import Prelude ()
+import Prelude.Compat hiding (readFile)
+import qualified Control.Exception as C
+import Control.Monad
+import Control.Monad.Except (ExceptT(..), runExceptT, throwError)
+import Control.Monad.State.Strict
+import qualified Data.Map.Strict as M
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Calendar
+import Data.Time.LocalTime
+import Safe
+import Test.HUnit
+#ifdef TESTS
+import Test.Framework
+import Text.Megaparsec.Error
+#endif
+import Text.Megaparsec hiding (parse)
+import Text.Printf
+import System.FilePath
+
+import Hledger.Data
+import Hledger.Read.Common
+import Hledger.Read.TimeclockReader (timeclockfilep)
+import Hledger.Read.TimedotReader (timedotfilep)
+import Hledger.Utils
+
+-- $setup
+-- >>> :set -XOverloadedStrings
+
+--- * reader
+
+reader :: Reader
+reader = Reader format detect parse
+
+format :: String
+format = "journal"
+
+-- | Does the given file path and data look like it might be hledger's journal format ?
+detect :: FilePath -> Text -> Bool
+detect f _t
+  | f /= "-"  = takeExtension f `elem` ['.':format, ".j"] -- from a known file name: yes if the extension is this format's name or .j
+  | otherwise = True                                      -- from stdin: yes, always attempt to parse stdin as journal data
+  -- otherwise = regexMatches "(^|\n)[0-9]+.*\n[ \t]+" $ T.unpack t   -- from stdin: yes if we can see something that looks like a journal entry (digits in column 0 with the next line indented)
+
+-- | Parse and post-process a "Journal" from hledger's journal file
+-- format, or give an error.
+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+parse _ = parseAndFinaliseJournal journalp
+
+--- * parsers
+--- ** journal
+
+-- | A journal parser. Accumulates and returns a "ParsedJournal",
+-- which should be finalised/validated before use.
+--
+-- >>> rejp (journalp <* eof) "2015/1/1\n a  0\n"
+-- Right Journal  with 1 transactions, 1 accounts
+--
+journalp :: ErroringJournalParser ParsedJournal
+journalp = do
+  many addJournalItemP
+  eof
+  get
+
+-- | A side-effecting parser; parses any kind of journal item
+-- and updates the parse state accordingly.
+addJournalItemP :: ErroringJournalParser ()
+addJournalItemP =
+  -- all journal line types can be distinguished by the first
+  -- character, can use choice without backtracking
+  choice [
+      directivep
+    , transactionp          >>= modify' . addTransaction
+    , modifiertransactionp  >>= modify' . addModifierTransaction
+    , periodictransactionp  >>= modify' . addPeriodicTransaction
+    , marketpricedirectivep >>= modify' . addMarketPrice
+    , void emptyorcommentlinep
+    , void multilinecommentp
+    ] <?> "transaction or directive"
+
+--- ** directives
+
+-- | Parse any journal directive and update the parse state accordingly.
+-- Cf http://hledger.org/manual.html#directives,
+-- http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives
+directivep :: ErroringJournalParser ()
+directivep = (do
+  optional $ char '!'
+  choiceInState [
+    includedirectivep
+   ,aliasdirectivep
+   ,endaliasesdirectivep
+   ,accountdirectivep
+   ,applyaccountdirectivep
+   ,commoditydirectivep
+   ,endapplyaccountdirectivep
+   ,tagdirectivep
+   ,endtagdirectivep
+   ,defaultyeardirectivep
+   ,defaultcommoditydirectivep
+   ,commodityconversiondirectivep
+   ,ignoredpricecommoditydirectivep
+   ]
+  ) <?> "directive"
+
+includedirectivep :: ErroringJournalParser ()
+includedirectivep = do
+  string "include"
+  lift (some spacenonewline)
+  filename  <- lift restofline
+  parentpos <- getPosition
+  parentj   <- get
+  let childj = newJournalWithParseStateFrom parentj
+  (ej :: Either String ParsedJournal) <-
+    liftIO $ runExceptT $ do
+      let curdir = takeDirectory (sourceName parentpos)
+      filepath <- expandPath curdir filename `orRethrowIOError` (show parentpos ++ " locating " ++ filename)
+      txt      <- readFileAnyLineEnding filepath `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)
+      (ej1::Either (ParseError Char Dec) ParsedJournal) <-
+        runParserT
+           (evalStateT
+              (choiceInState
+                 [journalp
+                 ,timeclockfilep
+                 ,timedotfilep
+                 -- can't include a csv file yet, that reader is special
+                 ])
+              childj)
+           filepath txt
+      either
+        (throwError
+          . ((show parentpos ++ " in included file " ++ show filename ++ ":\n") ++)
+          . show)
+        (return . journalAddFile (filepath, txt))
+        ej1
+  case ej of
+    Left e       -> throwError e
+    Right childj -> modify' (\parentj -> childj <> parentj)
+    -- discard child's parse info, prepend its (reversed) list data, combine other fields
+
+newJournalWithParseStateFrom :: Journal -> Journal
+newJournalWithParseStateFrom j = mempty{
+   jparsedefaultyear      = jparsedefaultyear j
+  ,jparsedefaultcommodity = jparsedefaultcommodity j
+  ,jparseparentaccounts   = jparseparentaccounts j
+  ,jparsealiases          = jparsealiases j
+  -- ,jparsetransactioncount = jparsetransactioncount j
+  ,jparsetimeclockentries = jparsetimeclockentries j
+  }
+
+-- | Lift an IO action into the exception monad, rethrowing any IO
+-- error with the given message prepended.
+orRethrowIOError :: IO a -> String -> ExceptT String IO a
+orRethrowIOError io msg =
+  ExceptT $
+    (Right <$> io)
+    `C.catch` \(e::C.IOException) -> return $ Left $ printf "%s:\n%s" msg (show e)
+
+accountdirectivep :: ErroringJournalParser ()
+accountdirectivep = do
+  string "account"
+  lift (some spacenonewline)
+  acct <- lift accountnamep
+  newline
+  _ <- many indentedlinep
+  modify' (\j -> j{jaccounts = acct : jaccounts j})
+
+indentedlinep = lift (some spacenonewline) >> (rstrip <$> lift restofline)
+
+-- | Parse a one-line or multi-line commodity directive.
+--
+-- >>> Right _ <- rejp commoditydirectivep "commodity $1.00"
+-- >>> Right _ <- rejp commoditydirectivep "commodity $\n  format $1.00"
+-- >>> Right _ <- rejp commoditydirectivep "commodity $\n\n" -- a commodity with no format
+-- >>> Right _ <- rejp commoditydirectivep "commodity $1.00\n  format $1.00" -- both, what happens ?
+commoditydirectivep :: ErroringJournalParser ()
+commoditydirectivep = try commoditydirectiveonelinep <|> commoditydirectivemultilinep
+
+-- | Parse a one-line commodity directive.
+--
+-- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00"
+-- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00 ; blah\n"
+commoditydirectiveonelinep :: ErroringJournalParser ()
+commoditydirectiveonelinep = do
+  string "commodity"
+  lift (some spacenonewline)
+  Amount{acommodity,astyle} <- amountp
+  lift (many spacenonewline)
+  _ <- followingcommentp <|> (lift eolof >> return "")
+  let comm = Commodity{csymbol=acommodity, cformat=Just astyle}
+  modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})
+
+-- | Parse a multi-line commodity directive, containing 0 or more format subdirectives.
+--
+-- >>> Right _ <- rejp commoditydirectivemultilinep "commodity $ ; blah \n  format $1.00 ; blah"
+commoditydirectivemultilinep :: ErroringJournalParser ()
+commoditydirectivemultilinep = do
+  string "commodity"
+  lift (some spacenonewline)
+  sym <- lift commoditysymbolp
+  _ <- followingcommentp <|> (lift eolof >> return "")
+  mformat <- lastMay <$> many (indented $ formatdirectivep sym)
+  let comm = Commodity{csymbol=sym, cformat=mformat}
+  modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j})
+  where
+    indented = (lift (some spacenonewline) >>)
+
+-- | Parse a format (sub)directive, throwing a parse error if its
+-- symbol does not match the one given.
+formatdirectivep :: CommoditySymbol -> ErroringJournalParser AmountStyle
+formatdirectivep expectedsym = do
+  string "format"
+  lift (some spacenonewline)
+  pos <- getPosition
+  Amount{acommodity,astyle} <- amountp
+  _ <- followingcommentp <|> (lift eolof >> return "")
+  if acommodity==expectedsym
+    then return astyle
+    else parserErrorAt pos $
+         printf "commodity directive symbol \"%s\" and format directive symbol \"%s\" should be the same" expectedsym acommodity
+
+applyaccountdirectivep :: ErroringJournalParser ()
+applyaccountdirectivep = do
+  string "apply" >> lift (some spacenonewline) >> string "account"
+  lift (some spacenonewline)
+  parent <- lift accountnamep
+  newline
+  pushParentAccount parent
+
+endapplyaccountdirectivep :: ErroringJournalParser ()
+endapplyaccountdirectivep = do
+  string "end" >> lift (some spacenonewline) >> string "apply" >> lift (some spacenonewline) >> string "account"
+  popParentAccount
+
+aliasdirectivep :: ErroringJournalParser ()
+aliasdirectivep = do
+  string "alias"
+  lift (some spacenonewline)
+  alias <- lift accountaliasp
+  addAccountAlias alias
+
+accountaliasp :: TextParser m AccountAlias
+accountaliasp = regexaliasp <|> basicaliasp
+
+basicaliasp :: TextParser m AccountAlias
+basicaliasp = do
+  -- pdbg 0 "basicaliasp"
+  old <- rstrip <$> (some $ noneOf ("=" :: [Char]))
+  char '='
+  many spacenonewline
+  new <- rstrip <$> anyChar `manyTill` eolof  -- don't require a final newline, good for cli options
+  return $ BasicAlias (T.pack old) (T.pack new)
+
+regexaliasp :: TextParser m AccountAlias
+regexaliasp = do
+  -- pdbg 0 "regexaliasp"
+  char '/'
+  re <- some $ noneOf ("/\n\r" :: [Char]) -- paranoid: don't try to read past line end
+  char '/'
+  many spacenonewline
+  char '='
+  many spacenonewline
+  repl <- rstrip <$> anyChar `manyTill` eolof
+  return $ RegexAlias re repl
+
+endaliasesdirectivep :: ErroringJournalParser ()
+endaliasesdirectivep = do
+  string "end aliases"
+  clearAccountAliases
+
+tagdirectivep :: ErroringJournalParser ()
+tagdirectivep = do
+  string "tag" <?> "tag directive"
+  lift (some spacenonewline)
+  _ <- lift $ some nonspace
+  lift restofline
+  return ()
+
+endtagdirectivep :: ErroringJournalParser ()
+endtagdirectivep = do
+  (string "end tag" <|> string "pop") <?> "end tag or pop directive"
+  lift restofline
+  return ()
+
+defaultyeardirectivep :: ErroringJournalParser ()
+defaultyeardirectivep = do
+  char 'Y' <?> "default year"
+  lift (many spacenonewline)
+  y <- some digitChar
+  let y' = read y
+  failIfInvalidYear y
+  setYear y'
+
+defaultcommoditydirectivep :: ErroringJournalParser ()
+defaultcommoditydirectivep = do
+  char 'D' <?> "default commodity"
+  lift (some spacenonewline)
+  Amount{..} <- amountp
+  lift restofline
+  setDefaultCommodityAndStyle (acommodity, astyle)
+
+marketpricedirectivep :: ErroringJournalParser MarketPrice
+marketpricedirectivep = do
+  char 'P' <?> "market price"
+  lift (many spacenonewline)
+  date <- try (do {LocalTime d _ <- datetimep; return d}) <|> datep -- a time is ignored
+  lift (some spacenonewline)
+  symbol <- lift commoditysymbolp
+  lift (many spacenonewline)
+  price <- amountp
+  lift restofline
+  return $ MarketPrice date symbol price
+
+ignoredpricecommoditydirectivep :: ErroringJournalParser ()
+ignoredpricecommoditydirectivep = do
+  char 'N' <?> "ignored-price commodity"
+  lift (some spacenonewline)
+  lift commoditysymbolp
+  lift restofline
+  return ()
+
+commodityconversiondirectivep :: ErroringJournalParser ()
+commodityconversiondirectivep = do
+  char 'C' <?> "commodity conversion"
+  lift (some spacenonewline)
+  amountp
+  lift (many spacenonewline)
+  char '='
+  lift (many spacenonewline)
+  amountp
+  lift restofline
+  return ()
+
+--- ** transactions
+
+modifiertransactionp :: ErroringJournalParser ModifierTransaction
+modifiertransactionp = do
+  char '=' <?> "modifier transaction"
+  lift (many spacenonewline)
+  valueexpr <- T.pack <$> lift restofline
+  postings <- postingsp Nothing
+  return $ ModifierTransaction valueexpr postings
+
+periodictransactionp :: ErroringJournalParser PeriodicTransaction
+periodictransactionp = do
+  char '~' <?> "periodic transaction"
+  lift (many spacenonewline)
+  periodexpr <- T.pack <$> lift restofline
+  postings <- postingsp Nothing
+  return $ PeriodicTransaction periodexpr postings
+
+-- | Parse a (possibly unbalanced) transaction.
+transactionp :: ErroringJournalParser Transaction
+transactionp = do
+  -- ptrace "transactionp"
+  sourcepos <- genericSourcePos <$> getPosition
+  date <- datep <?> "transaction"
+  edate <- optional (secondarydatep date) <?> "secondary date"
+  lookAhead (lift spacenonewline <|> newline) <?> "whitespace or newline"
+  status <- lift statusp <?> "cleared status"
+  code <- T.pack <$> lift codep <?> "transaction code"
+  description <- T.pack . strip <$> descriptionp
+  comment <- try followingcommentp <|> (newline >> return "")
+  let tags = commentTags comment
+  postings <- postingsp (Just date)
+  return $ txnTieKnot $ Transaction 0 sourcepos date edate status code description comment tags postings ""
+
+#ifdef TESTS
+test_transactionp = do
+    let s `gives` t = do
+                        let p = parseWithState mempty transactionp s
+                        assertBool $ isRight p
+                        let Right t2 = p
+                            -- same f = assertEqual (f t) (f t2)
+                        assertEqual (tdate t) (tdate t2)
+                        assertEqual (tdate2 t) (tdate2 t2)
+                        assertEqual (tstatus t) (tstatus t2)
+                        assertEqual (tcode t) (tcode t2)
+                        assertEqual (tdescription t) (tdescription t2)
+                        assertEqual (tcomment t) (tcomment t2)
+                        assertEqual (ttags t) (ttags t2)
+                        assertEqual (tpreceding_comment_lines t) (tpreceding_comment_lines t2)
+                        assertEqual (show $ tpostings t) (show $ tpostings t2)
+    -- "0000/01/01\n\n" `gives` nulltransaction
+    unlines [
+      "2012/05/14=2012/05/15 (code) desc  ; tcomment1",
+      "    ; tcomment2",
+      "    ; ttag1: val1",
+      "    * a         $1.00  ; pcomment1",
+      "    ; pcomment2",
+      "    ; ptag1: val1",
+      "    ; ptag2: val2"
+      ]
+     `gives`
+     nulltransaction{
+      tdate=parsedate "2012/05/14",
+      tdate2=Just $ parsedate "2012/05/15",
+      tstatus=Uncleared,
+      tcode="code",
+      tdescription="desc",
+      tcomment=" tcomment1\n tcomment2\n ttag1: val1\n",
+      ttags=[("ttag1","val1")],
+      tpostings=[
+        nullposting{
+          pstatus=Cleared,
+          paccount="a",
+          pamount=Mixed [usd 1],
+          pcomment=" pcomment1\n pcomment2\n ptag1: val1\n  ptag2: val2\n",
+          ptype=RegularPosting,
+          ptags=[("ptag1","val1"),("ptag2","val2")],
+          ptransaction=Nothing
+          }
+        ],
+      tpreceding_comment_lines=""
+      }
+    unlines [
+      "2015/1/1",
+      ]
+     `gives`
+     nulltransaction{
+      tdate=parsedate "2015/01/01",
+      }
+
+    assertRight $ parseWithState mempty transactionp $ unlines
+      ["2007/01/28 coopportunity"
+      ,"    expenses:food:groceries                   $47.18"
+      ,"    assets:checking                          $-47.18"
+      ,""
+      ]
+
+    -- transactionp should not parse just a date
+    assertLeft $ parseWithState mempty transactionp "2009/1/1\n"
+
+    -- transactionp should not parse just a date and description
+    assertLeft $ parseWithState mempty transactionp "2009/1/1 a\n"
+
+    -- transactionp should not parse a following comment as part of the description
+    let p = parseWithState mempty transactionp "2009/1/1 a ;comment\n b 1\n"
+    assertRight p
+    assertEqual "a" (let Right p' = p in tdescription p')
+
+    -- parse transaction with following whitespace line
+    assertRight $ parseWithState mempty transactionp $ unlines
+        ["2012/1/1"
+        ,"  a  1"
+        ,"  b"
+        ," "
+        ]
+
+    let p = parseWithState mempty transactionp $ unlines
+             ["2009/1/1 x  ; transaction comment"
+             ," a  1  ; posting 1 comment"
+             ," ; posting 1 comment 2"
+             ," b"
+             ," ; posting 2 comment"
+             ]
+    assertRight p
+    assertEqual 2 (let Right t = p in length $ tpostings t)
+#endif
+
+--- ** postings
+
+-- Parse the following whitespace-beginning lines as postings, posting
+-- tags, and/or comments (inferring year, if needed, from the given date).
+postingsp :: Maybe Day -> ErroringJournalParser [Posting]
+postingsp mdate = many (try $ postingp mdate) <?> "postings"
+
+-- linebeginningwithspaces :: Monad m => JournalParser m String
+-- linebeginningwithspaces = do
+--   sp <- lift (some spacenonewline)
+--   c <- nonspace
+--   cs <- lift restofline
+--   return $ sp ++ (c:cs) ++ "\n"
+
+postingp :: Maybe Day -> ErroringJournalParser Posting
+postingp mtdate = do
+  -- pdbg 0 "postingp"
+  lift (some spacenonewline)
+  status <- lift statusp
+  lift (many spacenonewline)
+  account <- modifiedaccountnamep
+  let (ptype, account') = (accountNamePostingType account, textUnbracket account)
+  amount <- spaceandamountormissingp
+  massertion <- partialbalanceassertionp
+  _ <- fixedlotpricep
+  lift (many spacenonewline)
+  (comment,tags,mdate,mdate2) <-
+    try (followingcommentandtagsp mtdate) <|> (newline >> return ("",[],Nothing,Nothing))
+  return posting
+   { pdate=mdate
+   , pdate2=mdate2
+   , pstatus=status
+   , paccount=account'
+   , pamount=amount
+   , pcomment=comment
+   , ptype=ptype
+   , ptags=tags
+   , pbalanceassertion=massertion
+   }
+
+#ifdef TESTS
+test_postingp = do
+    let s `gives` ep = do
+                         let parse = parseWithState mempty (postingp Nothing) s
+                         assertBool -- "postingp parser"
+                           $ isRight parse
+                         let Right ap = parse
+                             same f = assertEqual (f ep) (f ap)
+                         same pdate
+                         same pstatus
+                         same paccount
+                         same pamount
+                         same pcomment
+                         same ptype
+                         same ptags
+                         same ptransaction
+    "  expenses:food:dining  $10.00   ; a: a a \n   ; b: b b \n" `gives`
+      posting{paccount="expenses:food:dining", pamount=Mixed [usd 10], pcomment=" a: a a \n b: b b \n", ptags=[("a","a a"), ("b","b b")]}
+
+    " a  1 ; [2012/11/28]\n" `gives`
+      ("a" `post` num 1){pcomment=" [2012/11/28]\n"
+                        ,ptags=[("date","2012/11/28")]
+                        ,pdate=parsedateM "2012/11/28"}
+
+    " a  1 ; a:a, [=2012/11/28]\n" `gives`
+      ("a" `post` num 1){pcomment=" a:a, [=2012/11/28]\n"
+                        ,ptags=[("a","a"), ("date2","2012/11/28")]
+                        ,pdate=Nothing}
+
+    " a  1 ; a:a\n  ; [2012/11/28=2012/11/29],b:b\n" `gives`
+      ("a" `post` num 1){pcomment=" a:a\n [2012/11/28=2012/11/29],b:b\n"
+                        ,ptags=[("a","a"), ("date","2012/11/28"), ("date2","2012/11/29"), ("b","b")]
+                        ,pdate=parsedateM "2012/11/28"}
+
+    assertBool -- "postingp parses a quoted commodity with numbers"
+      (isRight $ parseWithState mempty (postingp Nothing) "  a  1 \"DE123\"\n")
+
+  -- ,"postingp parses balance assertions and fixed lot prices" ~: do
+    assertBool (isRight $ parseWithState mempty (postingp Nothing) "  a  1 \"DE123\" =$1 { =2.2 EUR} \n")
+
+    -- let parse = parseWithState mempty postingp " a\n ;next-line comment\n"
+    -- assertRight parse
+    -- let Right p = parse
+    -- assertEqual "next-line comment\n" (pcomment p)
+    -- assertEqual (Just nullmixedamt) (pbalanceassertion p)
+#endif
+
+--- * more tests
+
+tests_Hledger_Read_JournalReader = TestList $ concat [
+    -- test_numberp
+ ]
+
+{- old hunit tests
+
+tests_Hledger_Read_JournalReader = TestList $ concat [
+    test_numberp,
+    test_amountp,
+    test_spaceandamountormissingp,
+    test_tagcomment,
+    test_inlinecomment,
+    test_comments,
+    test_ledgerDateSyntaxToTags,
+    test_postingp,
+    test_transactionp,
+    [
+   "modifiertransactionp" ~: do
+     assertParse (parseWithState mempty modifiertransactionp "= (some value expr)\n some:postings  1\n")
+
+  ,"periodictransactionp" ~: do
+     assertParse (parseWithState mempty periodictransactionp "~ (some period expr)\n some:postings  1\n")
+
+  ,"directivep" ~: do
+     assertParse (parseWithState mempty directivep "!include /some/file.x\n")
+     assertParse (parseWithState mempty directivep "account some:account\n")
+     assertParse (parseWithState mempty (directivep >> directivep) "!account a\nend\n")
+
+  ,"comment" ~: do
+     assertParse (parseWithState mempty comment "; some comment \n")
+     assertParse (parseWithState mempty comment " \t; x\n")
+     assertParse (parseWithState mempty comment "#x")
+
+  ,"datep" ~: do
+     assertParse (parseWithState mempty datep "2011/1/1")
+     assertParseFailure (parseWithState mempty datep "1/1")
+     assertParse (parseWithState mempty{jpsYear=Just 2011} datep "1/1")
+
+  ,"datetimep" ~: do
+      let p = do {t <- datetimep; eof; return t}
+          bad = assertParseFailure . parseWithState mempty p
+          good = assertParse . parseWithState mempty p
+      bad "2011/1/1"
+      bad "2011/1/1 24:00:00"
+      bad "2011/1/1 00:60:00"
+      bad "2011/1/1 00:00:60"
+      good "2011/1/1 00:00"
+      good "2011/1/1 23:59:59"
+      good "2011/1/1 3:5:7"
+      -- timezone is parsed but ignored
+      let startofday = LocalTime (fromGregorian 2011 1 1) (TimeOfDay 0 0 (fromIntegral 0))
+      assertParseEqual (parseWithState mempty p "2011/1/1 00:00-0800") startofday
+      assertParseEqual (parseWithState mempty p "2011/1/1 00:00+1234") startofday
+
+  ,"defaultyeardirectivep" ~: do
+     assertParse (parseWithState mempty defaultyeardirectivep "Y 2010\n")
+     assertParse (parseWithState mempty defaultyeardirectivep "Y 10001\n")
+
+  ,"marketpricedirectivep" ~:
+    assertParseEqual (parseWithState mempty marketpricedirectivep "P 2004/05/01 XYZ $55.00\n") (MarketPrice (parsedate "2004/05/01") "XYZ" $ usd 55)
+
+  ,"ignoredpricecommoditydirectivep" ~: do
+     assertParse (parseWithState mempty ignoredpricecommoditydirectivep "N $\n")
+
+  ,"defaultcommoditydirectivep" ~: do
+     assertParse (parseWithState mempty defaultcommoditydirectivep "D $1,000.0\n")
+
+  ,"commodityconversiondirectivep" ~: do
+     assertParse (parseWithState mempty commodityconversiondirectivep "C 1h = $50.00\n")
+
+  ,"tagdirectivep" ~: do
+     assertParse (parseWithState mempty tagdirectivep "tag foo \n")
+
+  ,"endtagdirectivep" ~: do
+     assertParse (parseWithState mempty endtagdirectivep "end tag \n")
+     assertParse (parseWithState mempty endtagdirectivep "pop \n")
+
+  ,"accountnamep" ~: do
+    assertBool "accountnamep parses a normal account name" (isRight $ parsewith accountnamep "a:b:c")
+    assertBool "accountnamep rejects an empty inner component" (isLeft $ parsewith accountnamep "a::c")
+    assertBool "accountnamep rejects an empty leading component" (isLeft $ parsewith accountnamep ":b:c")
+    assertBool "accountnamep rejects an empty trailing component" (isLeft $ parsewith accountnamep "a:b:")
+
+  ,"leftsymbolamountp" ~: do
+    assertParseEqual (parseWithState mempty leftsymbolamountp "$1")  (usd 1 `withPrecision` 0)
+    assertParseEqual (parseWithState mempty leftsymbolamountp "$-1") (usd (-1) `withPrecision` 0)
+    assertParseEqual (parseWithState mempty leftsymbolamountp "-$1") (usd (-1) `withPrecision` 0)
+
+  ,"amount" ~: do
+     let -- | compare a parse result with an expected amount, showing the debug representation for clarity
+         assertAmountParse parseresult amount =
+             (either (const "parse error") showAmountDebug parseresult) ~?= (showAmountDebug amount)
+     assertAmountParse (parseWithState mempty amountp "1 @ $2")
+       (num 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0))
+
+ ]]
+-}
diff --git a/Hledger/Read/TimeclockReader.hs b/Hledger/Read/TimeclockReader.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Read/TimeclockReader.hs
@@ -0,0 +1,125 @@
+{-|
+
+A reader for the timeclock file format generated by timeclock.el
+(<http://www.emacswiki.org/emacs/TimeClock>). Example:
+
+@
+i 2007\/03\/10 12:26:00 hledger
+o 2007\/03\/10 17:26:02
+@
+
+From timeclock.el 2.6:
+
+@
+A timeclock contains data in the form of a single entry per line.
+Each entry has the form:
+
+  CODE YYYY/MM/DD HH:MM:SS [COMMENT]
+
+CODE is one of: b, h, i, o or O.  COMMENT is optional when the code is
+i, o or O.  The meanings of the codes are:
+
+  b  Set the current time balance, or \"time debt\".  Useful when
+     archiving old log data, when a debt must be carried forward.
+     The COMMENT here is the number of seconds of debt.
+
+  h  Set the required working time for the given day.  This must
+     be the first entry for that day.  The COMMENT in this case is
+     the number of hours in this workday.  Floating point amounts
+     are allowed.
+
+  i  Clock in.  The COMMENT in this case should be the name of the
+     project worked on.
+
+  o  Clock out.  COMMENT is unnecessary, but can be used to provide
+     a description of how the period went, for example.
+
+  O  Final clock out.  Whatever project was being worked on, it is
+     now finished.  Useful for creating summary reports.
+@
+
+-}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hledger.Read.TimeclockReader (
+  -- * Reader
+  reader,
+  -- * Misc other exports
+  timeclockfilep,
+  -- * Tests
+  tests_Hledger_Read_TimeclockReader
+)
+where
+import           Prelude ()
+import           Prelude.Compat
+import           Control.Monad
+import           Control.Monad.Except (ExceptT)
+import           Control.Monad.State.Strict
+import           Data.Maybe (fromMaybe)
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Test.HUnit
+import           Text.Megaparsec hiding (parse)
+import           System.FilePath
+
+import           Hledger.Data
+-- XXX too much reuse ?
+import           Hledger.Read.Common
+import           Hledger.Utils
+
+
+reader :: Reader
+reader = Reader format detect parse
+
+format :: String
+format = "timeclock"
+
+-- | Does the given file path and data look like it might be timeclock.el's timeclock format ?
+detect :: FilePath -> Text -> Bool
+detect f t
+  | f /= "-"  = takeExtension f == '.':format -- from a known file name: yes if the extension is this format's name
+  | otherwise = regexMatches "(^|\n)[io] " $ T.unpack t  -- from stdin: yes if any line starts with "i " or "o "
+
+-- | Parse and post-process a "Journal" from timeclock.el's timeclock
+-- format, saving the provided file path and the current time, or give an
+-- error.
+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+parse _ = parseAndFinaliseJournal timeclockfilep
+
+timeclockfilep :: ErroringJournalParser ParsedJournal
+timeclockfilep = do many timeclockitemp
+                    eof
+                    j@Journal{jparsetimeclockentries=es} <- get
+                    -- Convert timeclock entries in this journal to transactions, closing any unfinished sessions.
+                    -- Doing this here rather than in journalFinalise means timeclock sessions can't span file boundaries,
+                    -- but it simplifies code above.
+                    now <- liftIO getCurrentLocalTime
+                    -- entries have been parsed in reverse order. timeclockEntriesToTransactions
+                    -- expects them to be in normal order, then we must reverse again since
+                    -- journalFinalise expects them in reverse order
+                    let j' = j{jtxns = reverse $ timeclockEntriesToTransactions now $ reverse es, jparsetimeclockentries = []}
+                    return j'
+    where
+      -- As all ledger line types can be distinguished by the first
+      -- character, excepting transactions versus empty (blank or
+      -- comment-only) lines, can use choice w/o try
+      timeclockitemp = choice [ 
+                            void emptyorcommentlinep
+                          , timeclockentryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j})
+                          ] <?> "timeclock entry, or default year or historical price directive"
+
+-- | Parse a timeclock entry.
+timeclockentryp :: ErroringJournalParser TimeclockEntry
+timeclockentryp = do
+  sourcepos <- genericSourcePos <$> lift getPosition
+  code <- oneOf ("bhioO" :: [Char])
+  lift (some spacenonewline)
+  datetime <- datetimep
+  account <- fromMaybe "" <$> optional (lift (some spacenonewline) >> modifiedaccountnamep)
+  description <- T.pack . fromMaybe "" <$> lift (optional (some spacenonewline >> restofline))
+  return $ TimeclockEntry sourcepos (read [code]) datetime account description
+
+tests_Hledger_Read_TimeclockReader = TestList [
+ ]
+
diff --git a/Hledger/Read/TimedotReader.hs b/Hledger/Read/TimedotReader.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Read/TimedotReader.hs
@@ -0,0 +1,159 @@
+{-|
+
+A reader for the "timedot" file format.
+Example:
+
+@
+#DATE
+#ACCT DOTS  # Each dot represents 15m, spaces are ignored
+
+# on 2/1, 1h was spent on FOSS haskell work, 0.25h on research, etc.
+2/1
+fos.haskell   .... ..
+biz.research  .
+inc.client1   .... .... .... .... .... ....
+
+2/2
+biz.research  .
+inc.client1   .... .... ..
+
+@
+
+-}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hledger.Read.TimedotReader (
+  -- * Reader
+  reader,
+  -- * Misc other exports
+  timedotfilep,
+  -- * Tests
+  tests_Hledger_Read_TimedotReader
+)
+where
+import Prelude ()
+import Prelude.Compat
+import Control.Monad
+import Control.Monad.Except (ExceptT)
+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 Test.HUnit
+import Text.Megaparsec hiding (parse)
+import System.FilePath
+
+import Hledger.Data
+import Hledger.Read.Common
+import Hledger.Utils hiding (ptrace)
+
+-- easier to toggle this here sometimes
+-- import qualified Hledger.Utils (ptrace)
+-- ptrace = Hledger.Utils.ptrace
+ptrace = return
+
+reader :: Reader
+reader = Reader format detect parse
+
+format :: String
+format = "timedot"
+
+-- | Does the given file path and data look like it might contain this format ?
+detect :: FilePath -> Text -> Bool
+detect f t
+  | f /= "-"  = takeExtension f == '.':format -- from a file: yes if the extension matches the format name
+  | otherwise = regexMatches "(^|\n)[0-9]" $ T.unpack t  -- from stdin: yes if we can see a possible timedot day entry (digits in column 0)
+
+-- | Parse and post-process a "Journal" from the timedot format, or give an error.
+parse :: Maybe FilePath -> Bool -> FilePath -> Text -> ExceptT String IO Journal
+parse _ = parseAndFinaliseJournal timedotfilep
+
+timedotfilep :: ErroringJournalParser ParsedJournal
+timedotfilep = do many timedotfileitemp
+                  eof
+                  get
+    where
+      timedotfileitemp :: ErroringJournalParser ()
+      timedotfileitemp = do
+        ptrace "timedotfileitemp"
+        choice [
+          void emptyorcommentlinep
+         ,timedotdayp >>= \ts -> modify' (addTransactions ts)
+         ] <?> "timedot day entry, or default year or comment line or blank line"
+
+addTransactions :: [Transaction] -> Journal -> Journal
+addTransactions ts j = foldl' (flip ($)) j (map addTransaction ts)
+
+-- | Parse timedot day entries to zero or more time transactions for that day.
+-- @
+-- 2/1
+-- fos.haskell  .... ..
+-- biz.research .
+-- inc.client1  .... .... .... .... .... ....
+-- @
+timedotdayp :: ErroringJournalParser [Transaction]
+timedotdayp = do
+  ptrace " timedotdayp"
+  d <- datep <* lift eolof
+  es <- catMaybes <$> many (const Nothing <$> try emptyorcommentlinep <|>
+                            Just <$> (notFollowedBy datep >> timedotentryp))
+  return $ map (\t -> t{tdate=d}) es -- <$> many timedotentryp
+
+-- | Parse a single timedot entry to one (dateless) transaction.
+-- @
+-- fos.haskell  .... ..
+-- @
+timedotentryp :: ErroringJournalParser Transaction
+timedotentryp = do
+  ptrace "  timedotentryp"
+  pos <- genericSourcePos <$> getPosition
+  lift (many spacenonewline)
+  a <- modifiedaccountnamep
+  lift (many spacenonewline)
+  hours <-
+    try (followingcommentp >> return 0)
+    <|> (timedotdurationp <*
+         (try followingcommentp <|> (newline >> return "")))
+  let t = nulltransaction{
+        tsourcepos = pos,
+        tstatus    = Cleared,
+        tpostings  = [
+          nullposting{paccount=a
+                     ,pamount=Mixed [setAmountPrecision 2 $ num hours]  -- don't assume hours; do set precision to 2
+                     ,ptype=VirtualPosting
+                     ,ptransaction=Just t
+                     }
+          ]
+        }
+  return t
+
+timedotdurationp :: ErroringJournalParser Quantity
+timedotdurationp = try timedotnumberp <|> timedotdotsp
+
+-- | Parse a duration written as a decimal number of hours (optionally followed by the letter h).
+-- @
+-- 1.5h
+-- @
+timedotnumberp :: ErroringJournalParser Quantity
+timedotnumberp = do
+   (q, _, _, _) <- lift numberp
+   lift (many spacenonewline)
+   optional $ char 'h'
+   lift (many spacenonewline)
+   return q
+
+-- | Parse a quantity written as a line of dots, each representing 0.25.
+-- @
+-- .... ..
+-- @
+timedotdotsp :: ErroringJournalParser Quantity
+timedotdotsp = do
+  dots <- filter (not.isSpace) <$> many (oneOf (". " :: [Char]))
+  return $ (/4) $ fromIntegral $ length dots
+
+tests_Hledger_Read_TimedotReader = TestList [
+ ]
+
diff --git a/Hledger/Read/TimelogReader.hs b/Hledger/Read/TimelogReader.hs
deleted file mode 100644
--- a/Hledger/Read/TimelogReader.hs
+++ /dev/null
@@ -1,116 +0,0 @@
-{-|
-
-A reader for the timelog file format generated by timeclock.el
-(<http://www.emacswiki.org/emacs/TimeClock>). Example:
-
-@
-i 2007\/03\/10 12:26:00 hledger
-o 2007\/03\/10 17:26:02
-@
-
-From timeclock.el 2.6:
-
-@
-A timelog contains data in the form of a single entry per line.
-Each entry has the form:
-
-  CODE YYYY/MM/DD HH:MM:SS [COMMENT]
-
-CODE is one of: b, h, i, o or O.  COMMENT is optional when the code is
-i, o or O.  The meanings of the codes are:
-
-  b  Set the current time balance, or \"time debt\".  Useful when
-     archiving old log data, when a debt must be carried forward.
-     The COMMENT here is the number of seconds of debt.
-
-  h  Set the required working time for the given day.  This must
-     be the first entry for that day.  The COMMENT in this case is
-     the number of hours in this workday.  Floating point amounts
-     are allowed.
-
-  i  Clock in.  The COMMENT in this case should be the name of the
-     project worked on.
-
-  o  Clock out.  COMMENT is unnecessary, but can be used to provide
-     a description of how the period went, for example.
-
-  O  Final clock out.  Whatever project was being worked on, it is
-     now finished.  Useful for creating summary reports.
-@
-
--}
-
-module Hledger.Read.TimelogReader (
-  -- * Reader
-  reader,
-  -- * Tests
-  tests_Hledger_Read_TimelogReader
-)
-where
-import Prelude ()
-import Prelude.Compat
-import Control.Monad (liftM)
-import Control.Monad.Except (ExceptT)
-import Data.List (isPrefixOf, foldl')
-import Data.Maybe (fromMaybe)
-import Test.HUnit
-import Text.Parsec hiding (parse)
-import System.FilePath
-
-import Hledger.Data
--- XXX too much reuse ?
-import Hledger.Read.JournalReader (
-  directivep, marketpricedirectivep, defaultyeardirectivep, emptyorcommentlinep, datetimep,
-  parseAndFinaliseJournal, modifiedaccountnamep, genericSourcePos
-  )
-import Hledger.Utils
-
-
-reader :: Reader
-reader = Reader format detect parse
-
-format :: String
-format = "timelog"
-
--- | Does the given file path and data look like it might be timeclock.el's timelog format ?
-detect :: FilePath -> String -> Bool
-detect f s
-  | f /= "-"  = takeExtension f == '.':format               -- from a file: yes if the extension is .timelog
-  | otherwise = "i " `isPrefixOf` s || "o " `isPrefixOf` s  -- from stdin: yes if it starts with "i " or "o "
-
--- | Parse and post-process a "Journal" from timeclock.el's timelog
--- format, saving the provided file path and the current time, or give an
--- error.
-parse :: Maybe FilePath -> Bool -> FilePath -> String -> ExceptT String IO Journal
-parse _ = parseAndFinaliseJournal timelogfilep
-
-timelogfilep :: ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate, JournalContext)
-timelogfilep = do items <- many timelogitemp
-                  eof
-                  ctx <- getState
-                  return (liftM (foldl' (\acc new x -> new (acc x)) id) $ sequence items, ctx)
-    where
-      -- As all ledger line types can be distinguished by the first
-      -- character, excepting transactions versus empty (blank or
-      -- comment-only) lines, can use choice w/o try
-      timelogitemp = choice [ directivep
-                          , liftM (return . addMarketPrice) marketpricedirectivep
-                          , defaultyeardirectivep
-                          , emptyorcommentlinep >> return (return id)
-                          , liftM (return . addTimeLogEntry)  timelogentryp
-                          ] <?> "timelog entry, or default year or historical price directive"
-
--- | Parse a timelog entry.
-timelogentryp :: ParsecT [Char] JournalContext (ExceptT String IO) TimeLogEntry
-timelogentryp = do
-  sourcepos <- genericSourcePos <$> getPosition
-  code <- oneOf "bhioO"
-  many1 spacenonewline
-  datetime <- datetimep
-  account <- fromMaybe "" <$> optionMaybe (many1 spacenonewline >> modifiedaccountnamep)
-  description <- fromMaybe "" <$> optionMaybe (many1 spacenonewline >> restofline)
-  return $ TimeLogEntry sourcepos (read [code]) datetime account description
-
-tests_Hledger_Read_TimelogReader = TestList [
- ]
-
diff --git a/Hledger/Reports.hs b/Hledger/Reports.hs
--- a/Hledger/Reports.hs
+++ b/Hledger/Reports.hs
@@ -15,7 +15,7 @@
   module Hledger.Reports.TransactionsReports,
   module Hledger.Reports.BalanceReport,
   module Hledger.Reports.MultiBalanceReports,
-  module Hledger.Reports.BalanceHistoryReport,
+--   module Hledger.Reports.BalanceHistoryReport,
 
   -- * Tests
   tests_Hledger_Reports
@@ -30,7 +30,7 @@
 import Hledger.Reports.TransactionsReports
 import Hledger.Reports.BalanceReport
 import Hledger.Reports.MultiBalanceReports
-import Hledger.Reports.BalanceHistoryReport
+-- import Hledger.Reports.BalanceHistoryReport
 
 tests_Hledger_Reports :: Test
 tests_Hledger_Reports = TestList $
diff --git a/Hledger/Reports/BalanceHistoryReport.hs b/Hledger/Reports/BalanceHistoryReport.hs
--- a/Hledger/Reports/BalanceHistoryReport.hs
+++ b/Hledger/Reports/BalanceHistoryReport.hs
@@ -4,6 +4,7 @@
 Account balance history report.
 
 -}
+-- XXX not used
 
 module Hledger.Reports.BalanceHistoryReport (
   accountBalanceHistory
diff --git a/Hledger/Reports/BalanceReport.hs b/Hledger/Reports/BalanceReport.hs
--- a/Hledger/Reports/BalanceReport.hs
+++ b/Hledger/Reports/BalanceReport.hs
@@ -1,14 +1,21 @@
-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, ScopedTypeVariables #-}
 {-|
 
 Balance report, used by the balance command.
 
 -}
 
+
+
+
+
+
+
+
+{-# LANGUAGE FlexibleInstances, ScopedTypeVariables, OverloadedStrings #-}
+
 module Hledger.Reports.BalanceReport (
   BalanceReport,
   BalanceReportItem,
-  RenderableAccountName,
   balanceReport,
   balanceReportValue,
   mixedAmountValue,
@@ -31,24 +38,31 @@
 import Hledger.Reports.ReportOptions
 
 
+
+
+
+
+
+
 -- | A simple single-column balance report. It has:
 --
--- 1. a list of rows, each containing a renderable account name and a corresponding amount
+-- 1. a list of items, one per account, each containing:
 --
--- 2. the final total of the amounts
-type BalanceReport = ([BalanceReportItem], MixedAmount)
-type BalanceReportItem = (RenderableAccountName, MixedAmount)
-
--- | A renderable account name includes some additional hints for rendering accounts in a balance report.
--- It has:
+--   * the full account name
 --
--- * The full account name
+--   * the Ledger-style elided short account name
+--     (the leaf account name, prefixed by any boring parents immediately above);
+--     or with --flat, the full account name again
 --
--- * The ledger-style short elided account name (the leaf name, prefixed by any boring parents immediately above)
+--   * the number of indentation steps for rendering a Ledger-style account tree,
+--     taking into account elided boring parents, --no-elide and --flat
 --
--- * The number of indentation steps to use when rendering a ledger-style account tree
---   (normally the 0-based depth of this account excluding boring parents, or 0 with --flat).
-type RenderableAccountName = (AccountName, AccountName, Int)
+--   * an amount
+--
+-- 2. the total of all amounts
+--
+type BalanceReport = ([BalanceReportItem], MixedAmount)
+type BalanceReportItem = (AccountName, AccountName, Int, MixedAmount)
 
 -- | When true (the default), this makes balance --flat reports and their implementation clearer.
 -- Single/multi-col balance reports currently aren't all correct if this is false.
@@ -61,7 +75,7 @@
 
 -- | Generate a simple balance report, containing the matched accounts and
 -- their balances (change of balance) during the specified period.
--- This is like periodBalanceReport with a single column (but more mature,
+-- This is like PeriodChangeReport with a single column (but more mature,
 -- eg this can do hierarchical display).
 balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
 balanceReport opts q j = (items, total)
@@ -91,10 +105,10 @@
             prunezeros  = if empty_ opts then id else fromMaybe nullacct . pruneAccounts (isZeroMixedAmount . balance)
             markboring  = if no_elide_ opts then id else markBoringParentAccounts
       items = dbg1 "items" $ map (balanceReportItem opts q) accts'
-      total | not (flat_ opts) = dbg1 "total" $ sum [amt | ((_,_,indent),amt) <- items, indent == 0]
+      total | not (flat_ opts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0]
             | otherwise        = dbg1 "total" $
                                  if flatShowsExclusiveBalance
-                                 then sum $ map snd items
+                                 then sum $ map fourth4 items
                                  else sum $ map aebalance $ clipAccountsAndAggregate 1 accts'
 
 -- | In an account tree with zero-balance leaves removed, mark the
@@ -108,13 +122,13 @@
 
 balanceReportItem :: ReportOpts -> Query -> Account -> BalanceReportItem
 balanceReportItem opts q a
-  | flat_ opts = ((name, name,       0),      (if flatShowsExclusiveBalance then aebalance else aibalance) a)
-  | otherwise  = ((name, elidedname, indent), aibalance a)
+  | flat_ opts = (name, name,       0,      (if flatShowsExclusiveBalance then aebalance else aibalance) a)
+  | otherwise  = (name, elidedname, indent, aibalance a)
   where
     name | queryDepth q > 0 = aname a
          | otherwise        = "..."
     elidedname = accountNameFromComponents (adjacentboringparentnames ++ [accountLeafName name])
-    adjacentboringparentnames = reverse $ map (accountLeafName.aname) $ takeWhile aboring $ parents
+    adjacentboringparentnames = reverse $ map (accountLeafName.aname) $ takeWhile aboring parents
     indent = length $ filter (not.aboring) parents
     -- parents exclude the tree's root node
     parents = case parentAccounts a of [] -> []
@@ -123,7 +137,7 @@
 -- -- the above using the newer multi balance report code:
 -- balanceReport' opts q j = (items, total)
 --   where
---     MultiBalanceReport (_,mbrrows,mbrtotals) = periodBalanceReport opts q j
+--     MultiBalanceReport (_,mbrrows,mbrtotals) = PeriodChangeReport opts q j
 --     items = [(a,a',n, headDef 0 bs) | ((a,a',n), bs) <- mbrrows]
 --     total = headDef 0 mbrtotals
 
@@ -134,14 +148,15 @@
 balanceReportValue j d r = r'
   where
     (items,total) = r
-    r' = ([(n, mixedAmountValue j d a) |(n,a) <- items], mixedAmountValue j d total)
+    r' = dbg8 "balanceReportValue"
+         ([(n, n', i, mixedAmountValue j d a) |(n,n',i,a) <- items], mixedAmountValue j d total)
 
 mixedAmountValue :: Journal -> Day -> MixedAmount -> MixedAmount
 mixedAmountValue j d (Mixed as) = Mixed $ map (amountValue j d) as
 
 -- | Find the market value of this amount on the given date, in it's
--- default valuation commodity, based on historical prices. If no
--- default valuation commodity can be found, the amount is left
+-- default valuation commodity, based on recorded market prices.
+-- If no default valuation commodity can be found, the amount is left
 -- unchanged.
 amountValue :: Journal -> Day -> Amount -> Amount
 amountValue j d a =
@@ -154,20 +169,27 @@
 -- | Find the market value, if known, of one unit of this commodity on
 -- the given date, in the commodity in which it has most recently been
 -- market-priced (ie the commodity mentioned in the most recent
--- applicable historical price directive before this date).
-commodityValue :: Journal -> Day -> Commodity -> Maybe Amount
+-- applicable market price directive before this date).
+commodityValue :: Journal -> Day -> CommoditySymbol -> Maybe Amount
 commodityValue j d c
     | null applicableprices = Nothing
     | otherwise             = Just $ mpamount $ last applicableprices
   where
     applicableprices = [p | p <- sort $ jmarketprices j, mpcommodity p == c, mpdate p <= d]
 
+
+
+
+
+
+
+
 tests_balanceReport =
   let
     (opts,journal) `gives` r = do
       let (eitems, etotal) = r
           (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal
-          showw (acct,amt) = (acct, showMixedAmountDebug amt)
+          showw (acct,acct',indent,amt) = (acct, acct', indent, showMixedAmountDebug amt)
       assertEqual "items" (map showw eitems) (map showw aitems)
       assertEqual "total" (showMixedAmountDebug etotal) (showMixedAmountDebug atotal)
     usd0 = usd 0
@@ -179,36 +201,36 @@
   ,"balanceReport with no args on sample journal" ~: do
    (defreportopts, samplejournal) `gives`
     ([
-      (("assets","assets",0), mamountp' "$-1.00")
-     ,(("assets:bank:saving","bank:saving",1), mamountp' "$1.00")
-     ,(("assets:cash","cash",1), mamountp' "$-2.00")
-     ,(("expenses","expenses",0), mamountp' "$2.00")
-     ,(("expenses:food","food",1), mamountp' "$1.00")
-     ,(("expenses:supplies","supplies",1), mamountp' "$1.00")
-     ,(("income","income",0), mamountp' "$-2.00")
-     ,(("income:gifts","gifts",1), mamountp' "$-1.00")
-     ,(("income:salary","salary",1), mamountp' "$-1.00")
-     ,(("liabilities:debts","liabilities:debts",0), mamountp' "$1.00")
+      ("assets","assets",0, mamountp' "$-1.00")
+     ,("assets:bank:saving","bank:saving",1, mamountp' "$1.00")
+     ,("assets:cash","cash",1, mamountp' "$-2.00")
+     ,("expenses","expenses",0, mamountp' "$2.00")
+     ,("expenses:food","food",1, mamountp' "$1.00")
+     ,("expenses:supplies","supplies",1, mamountp' "$1.00")
+     ,("income","income",0, mamountp' "$-2.00")
+     ,("income:gifts","gifts",1, mamountp' "$-1.00")
+     ,("income:salary","salary",1, mamountp' "$-1.00")
+     ,("liabilities:debts","liabilities:debts",0, mamountp' "$1.00")
      ],
      Mixed [usd0])
 
   ,"balanceReport with --depth=N" ~: do
    (defreportopts{depth_=Just 1}, samplejournal) `gives`
     ([
-      (("assets",      "assets",      0), mamountp' "$-1.00")
-     ,(("expenses",    "expenses",    0), mamountp'  "$2.00")
-     ,(("income",      "income",      0), mamountp' "$-2.00")
-     ,(("liabilities", "liabilities", 0), mamountp'  "$1.00")
+      ("assets",      "assets",      0, mamountp' "$-1.00")
+     ,("expenses",    "expenses",    0, mamountp'  "$2.00")
+     ,("income",      "income",      0, mamountp' "$-2.00")
+     ,("liabilities", "liabilities", 0, mamountp'  "$1.00")
      ],
      Mixed [usd0])
 
   ,"balanceReport with depth:N" ~: do
    (defreportopts{query_="depth:1"}, samplejournal) `gives`
     ([
-      (("assets",      "assets",      0), mamountp' "$-1.00")
-     ,(("expenses",    "expenses",    0), mamountp'  "$2.00")
-     ,(("income",      "income",      0), mamountp' "$-2.00")
-     ,(("liabilities", "liabilities", 0), mamountp'  "$1.00")
+      ("assets",      "assets",      0, mamountp' "$-1.00")
+     ,("expenses",    "expenses",    0, mamountp'  "$2.00")
+     ,("income",      "income",      0, mamountp' "$-2.00")
+     ,("liabilities", "liabilities", 0, mamountp'  "$1.00")
      ],
      Mixed [usd0])
 
@@ -218,32 +240,32 @@
      Mixed [nullamt])
    (defreportopts{query_="date2:'in 2009'"}, samplejournal2) `gives`
     ([
-      (("assets:bank:checking","assets:bank:checking",0),mamountp' "$1.00")
-     ,(("income:salary","income:salary",0),mamountp' "$-1.00")
+      ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")
+     ,("income:salary","income:salary",0,mamountp' "$-1.00")
      ],
      Mixed [usd0])
 
   ,"balanceReport with desc:" ~: do
    (defreportopts{query_="desc:income"}, samplejournal) `gives`
     ([
-      (("assets:bank:checking","assets:bank:checking",0),mamountp' "$1.00")
-     ,(("income:salary","income:salary",0), mamountp' "$-1.00")
+      ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")
+     ,("income:salary","income:salary",0, mamountp' "$-1.00")
      ],
      Mixed [usd0])
 
   ,"balanceReport with not:desc:" ~: do
    (defreportopts{query_="not:desc:income"}, samplejournal) `gives`
     ([
-      (("assets","assets",0), mamountp' "$-2.00")
-     ,(("assets:bank","bank",1), Mixed [usd0])
-     ,(("assets:bank:checking","checking",2),mamountp' "$-1.00")
-     ,(("assets:bank:saving","saving",2), mamountp' "$1.00")
-     ,(("assets:cash","cash",1), mamountp' "$-2.00")
-     ,(("expenses","expenses",0), mamountp' "$2.00")
-     ,(("expenses:food","food",1), mamountp' "$1.00")
-     ,(("expenses:supplies","supplies",1), mamountp' "$1.00")
-     ,(("income:gifts","income:gifts",0), mamountp' "$-1.00")
-     ,(("liabilities:debts","liabilities:debts",0), mamountp' "$1.00")
+      ("assets","assets",0, mamountp' "$-2.00")
+     ,("assets:bank","bank",1, Mixed [usd0])
+     ,("assets:bank:checking","checking",2,mamountp' "$-1.00")
+     ,("assets:bank:saving","saving",2, mamountp' "$1.00")
+     ,("assets:cash","cash",1, mamountp' "$-2.00")
+     ,("expenses","expenses",0, mamountp' "$2.00")
+     ,("expenses:food","food",1, mamountp' "$1.00")
+     ,("expenses:supplies","supplies",1, mamountp' "$1.00")
+     ,("income:gifts","income:gifts",0, mamountp' "$-1.00")
+     ,("liabilities:debts","liabilities:debts",0, mamountp' "$1.00")
      ],
      Mixed [usd0])
 
@@ -364,27 +386,28 @@
 -}
  ]
 
-Right samplejournal2 = journalBalanceTransactions $
-         nulljournal
-         {jtxns = [
-           txnTieKnot $ Transaction {
-             tindex=0,
-             tsourcepos=nullsourcepos,
-             tdate=parsedate "2008/01/01",
-             tdate2=Just $ parsedate "2009/01/01",
-             tstatus=Uncleared,
-             tcode="",
-             tdescription="income",
-             tcomment="",
-             ttags=[],
-             tpostings=
-                 [posting {paccount="assets:bank:checking", pamount=Mixed [usd 1]}
-                 ,posting {paccount="income:salary", pamount=missingmixedamt}
-                 ],
-             tpreceding_comment_lines=""
-           }
-          ]
-         }
+Right samplejournal2 =
+  journalBalanceTransactions
+    nulljournal{
+      jtxns = [
+        txnTieKnot Transaction{
+          tindex=0,
+          tsourcepos=nullsourcepos,
+          tdate=parsedate "2008/01/01",
+          tdate2=Just $ parsedate "2009/01/01",
+          tstatus=Uncleared,
+          tcode="",
+          tdescription="income",
+          tcomment="",
+          ttags=[],
+          tpostings=
+            [posting {paccount="assets:bank:checking", pamount=Mixed [usd 1]}
+            ,posting {paccount="income:salary", pamount=missingmixedamt}
+            ],
+          tpreceding_comment_lines=""
+        }
+      ]
+    }
 
 -- tests_isInterestingIndented = [
 --   "isInterestingIndented" ~: do
@@ -395,5 +418,5 @@
 --  ]
 
 tests_Hledger_Reports_BalanceReport :: Test
-tests_Hledger_Reports_BalanceReport = TestList $
-    tests_balanceReport
+tests_Hledger_Reports_BalanceReport = TestList
+  tests_balanceReport
diff --git a/Hledger/Reports/MultiBalanceReports.hs b/Hledger/Reports/MultiBalanceReports.hs
--- a/Hledger/Reports/MultiBalanceReports.hs
+++ b/Hledger/Reports/MultiBalanceReports.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, ScopedTypeVariables #-}
+{-# LANGUAGE FlexibleInstances, ScopedTypeVariables #-}
 {-|
 
 Multi-column balance reports, used by the balance command.
@@ -9,7 +9,8 @@
   MultiBalanceReport(..),
   MultiBalanceReportRow,
   multiBalanceReport,
-  multiBalanceReportValue
+  multiBalanceReportValue,
+  singleBalanceReport
 
   -- -- * Tests
   -- tests_Hledger_Reports_MultiBalanceReport
@@ -32,32 +33,34 @@
 
 -- | A multi balance report is a balance report with one or more columns. It has:
 --
--- 1. a list of each column's date span
+-- 1. a list of each column's period (date span)
 --
--- 2. a list of rows, each containing a renderable account name and the amounts to show in each column
+-- 2. a list of row items, each containing:
 --
--- 3. a list of each column's final total
+--   * the full account name
 --
--- 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.Balance").
-newtype MultiBalanceReport = MultiBalanceReport ([DateSpan]
-                                                ,[MultiBalanceReportRow]
-                                                ,MultiBalanceTotalsRow
-                                                )
-
--- | A row in a multi balance report has
+--   * the leaf account name
 --
--- * An account name, with rendering hints
+--   * the account's depth
 --
--- * A list of amounts to be shown in each of the report's columns.
+--   * the amounts to show in each column
 --
--- * The total of the row amounts.
+--   * the total of the row's amounts
 --
--- * The average of the row amounts.
-type MultiBalanceReportRow = (RenderableAccountName, [MixedAmount], MixedAmount, MixedAmount)
-
-type MultiBalanceTotalsRow = ([MixedAmount], MixedAmount, MixedAmount)
+--   * the average of the row's amounts
+--
+-- 3. the column totals and the overall total and average
+--
+-- 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.Balance").
+newtype MultiBalanceReport =
+  MultiBalanceReport ([DateSpan]
+                     ,[MultiBalanceReportRow]
+                     ,MultiBalanceReportTotals
+                     )
+type MultiBalanceReportRow    = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
+type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount)
 
 instance Show MultiBalanceReport where
     -- use ppShow to break long lists onto multiple lines
@@ -69,6 +72,20 @@
 -- type alias just to remind us which AccountNames might be depth-clipped, below.
 type ClippedAccountName = AccountName
 
+-- | Generates a single column BalanceReport like balanceReport, but uses
+-- multiBalanceReport, so supports --historical. Does not support
+-- boring parent eliding yet.
+singleBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
+singleBalanceReport opts q j = (rows', total)
+  where
+    MultiBalanceReport (_, rows, (totals, _, _)) = multiBalanceReport opts q j
+    rows' = [(a
+             ,if tree_ opts then a' else a   -- BalanceReport expects full account name here with --flat
+             ,if tree_ opts then d-1 else 0  -- BalanceReport uses 0-based account depths
+             , headDef nullmixedamt amts     -- 0 columns is illegal, should not happen, return zeroes if it does
+             ) | (a,a',d, amts, _, _) <- rows]
+    total = headDef nullmixedamt totals
+
 -- | Generate a multicolumn balance report for the matched accounts,
 -- showing the change of balance, accumulated balance, or historical balance
 -- in each of the specified periods.
@@ -84,7 +101,7 @@
       precedingq = dbg1 "precedingq" $ And [datelessq, dateqcons $ DateSpan Nothing (spanStart reportspan)]
       requestedspan  = dbg1 "requestedspan"  $ queryDateSpan (date2_ opts) q                              -- span specified by -b/-e/-p options and query args
       requestedspan' = dbg1 "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan (date2_ opts) j  -- if open-ended, close it using the journal's end dates
-      intervalspans  = dbg1 "intervalspans"  $ splitSpan (intervalFromOpts opts) requestedspan'           -- interval spans enclosing it
+      intervalspans  = dbg1 "intervalspans"  $ splitSpan (interval_ opts) requestedspan'           -- interval spans enclosing it
       reportspan     = dbg1 "reportspan"     $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) -- the requested span enlarged to a whole number of intervals
                                                        (maybe Nothing spanEnd   $ lastMay intervalspans)
       newdatesq = dbg1 "newdateq" $ dateqcons reportspan
@@ -97,15 +114,15 @@
           filterJournalPostings reportq $        -- remove postings not matched by (adjusted) query
           journalSelectingAmountFromOpts opts j
 
-      displayspans = dbg1 "displayspans" $ splitSpan (intervalFromOpts opts) displayspan
+      displayspans = dbg1 "displayspans" $ splitSpan (interval_ opts) displayspan
         where
           displayspan
-            | empty_ opts = dbg1 "displayspan (-E)" $ reportspan                                -- all the requested intervals
+            | empty_ opts = dbg1 "displayspan (-E)" reportspan                                -- all the requested intervals
             | otherwise   = dbg1 "displayspan"      $ requestedspan `spanIntersect` matchedspan -- exclude leading/trailing empty intervals
           matchedspan = dbg1 "matchedspan" $ postingsDateSpan' (whichDateFromOpts opts) ps
 
       psPerSpan :: [[Posting]] =
-          dbg1 "psPerSpan" $
+          dbg1 "psPerSpan"
           [filter (isPostingInDateSpan' (whichDateFromOpts opts) s) ps | s <- displayspans]
 
       postedAcctBalChangesPerSpan :: [[(ClippedAccountName, MixedAmount)]] =
@@ -125,7 +142,7 @@
       postedAccts :: [AccountName] = dbg1 "postedAccts" $ sort $ accountNamesFromPostings ps
 
       -- starting balances and accounts from transactions before the report start date
-      startacctbals = dbg1 "startacctbals" $ map (\((a,_,_),b) -> (a,b)) startbalanceitems
+      startacctbals = dbg1 "startacctbals" $ map (\(a,_,_,b) -> (a,b)) startbalanceitems
           where
             (startbalanceitems,_) = dbg1 "starting balance report" $ balanceReport opts' precedingq j
                                     where
@@ -141,22 +158,22 @@
           if empty_ opts then nub $ sort $ startAccts ++ postedAccts else postedAccts
 
       acctBalChangesPerSpan :: [[(ClippedAccountName, MixedAmount)]] =
-          dbg1 "acctBalChangesPerSpan" $
+          dbg1 "acctBalChangesPerSpan"
           [sortBy (comparing fst) $ unionBy (\(a,_) (a',_) -> a == a') postedacctbals zeroes
            | postedacctbals <- postedAcctBalChangesPerSpan]
           where zeroes = [(a, nullmixedamt) | a <- displayedAccts]
 
       acctBalChanges :: [(ClippedAccountName, [MixedAmount])] =
-          dbg1 "acctBalChanges" $
+          dbg1 "acctBalChanges"
           [(a, map snd abs) | abs@((a,_):_) <- transpose acctBalChangesPerSpan] -- never null, or used when null...
 
       items :: [MultiBalanceReportRow] =
-          dbg1 "items" $
-          [((a, accountLeafName a, accountNameLevel a), displayedBals, rowtot, rowavg)
+          dbg1 "items"
+          [(a, accountLeafName a, accountNameLevel a, displayedBals, rowtot, rowavg)
            | (a,changes) <- acctBalChanges
            , let displayedBals = case balancetype_ opts of
                                   HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor a) changes
-                                  CumulativeBalance -> drop 1 $ scanl (+) nullmixedamt changes
+                                  CumulativeChange -> drop 1 $ scanl (+) nullmixedamt changes
                                   _                 -> changes
            , let rowtot = sum displayedBals
            , let rowavg = averageMixedAmounts displayedBals
@@ -167,13 +184,13 @@
           -- dbg1 "totals" $
           map sum balsbycol
           where
-            balsbycol = transpose [bs | ((a,_,_),bs,_,_) <- items, not (tree_ opts) || a `elem` highestlevelaccts]
+            balsbycol = transpose [bs | (a,_,_,bs,_,_) <- items, not (tree_ opts) || a `elem` highestlevelaccts]
             highestlevelaccts     =
-                dbg1 "highestlevelaccts" $
+                dbg1 "highestlevelaccts"
                 [a | a <- displayedAccts, not $ any (`elem` displayedAccts) $ init $ expandAccountName a]
 
-      totalsrow :: MultiBalanceTotalsRow =
-          dbg1 "totalsrow" $
+      totalsrow :: MultiBalanceReportTotals =
+          dbg1 "totalsrow"
           (totals, sum totals, averageMixedAmounts totals)
 
       dbg1 s = let p = "multiBalanceReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in this function's debug output
@@ -188,7 +205,7 @@
     MultiBalanceReport (spans, rows, (coltotals, rowtotaltotal, rowavgtotal)) = r
     r' = MultiBalanceReport
          (spans,
-          [(n, map convert rowamts, convert rowtotal, convert rowavg) | (n, rowamts, rowtotal, rowavg) <- rows],
+          [(acct, acct', depth, map convert rowamts, convert rowtotal, convert rowavg) | (acct, acct', depth, rowamts, rowtotal, rowavg) <- rows],
           (map convert coltotals, convert rowtotaltotal, convert rowavgtotal))
     convert = mixedAmountValue j d
 
diff --git a/Hledger/Reports/PostingsReport.hs b/Hledger/Reports/PostingsReport.hs
--- a/Hledger/Reports/PostingsReport.hs
+++ b/Hledger/Reports/PostingsReport.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections #-}
+{-# LANGUAGE RecordWildCards, DeriveDataTypeable, FlexibleInstances, TupleSections, OverloadedStrings #-}
 {-|
 
 Postings report, used by the register command.
@@ -19,6 +19,8 @@
 import Data.List
 import Data.Maybe
 import Data.Ord (comparing)
+-- import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Time.Calendar
 import Safe (headMay, lastMay)
 import Test.HUnit
@@ -45,7 +47,10 @@
                                         -- the interval.
                           ,Maybe String -- The posting's transaction's description, if this is the first posting in the transaction.
                           ,Posting      -- The posting, possibly with the account name depth-clipped.
-                          ,MixedAmount  -- The running total after this posting (or with --average, the running average).
+                          ,MixedAmount  -- The running total after this posting, or with --average,
+                                        -- the running average posting amount. With --historical,
+                                        -- postings before the report start date are included in
+                                        -- the running total/average.
                           )
 
 -- | Select postings from the journal and add running balance and other
@@ -53,51 +58,84 @@
 postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport
 postingsReport opts q j = (totallabel, items)
     where
-      -- figure out adjusted queries & spans like multiBalanceReport
-      symq = dbg1 "symq"   $ filterQuery queryIsSym $ dbg1 "requested q" q
+      reportspan = adjustReportDates opts q j
+      whichdate = whichDateFromOpts opts
       depth = queryDepth q
-      depthless = filterQuery (not . queryIsDepth)
-      datelessq = filterQuery (not . queryIsDateOrDate2) q
-      -- XXX date:/date2:/--date2 handling is not robust, combinations of these can confuse it
-      dateq = filterQuery queryIsDateOrDate2 q
-      (dateqcons,pdate) | queryIsDate2 dateq || (queryIsDate dateq && date2_ opts) = (Date2, postingDate2)
-                        | otherwise = (Date, postingDate)
-      requestedspan  = dbg1 "requestedspan"  $ queryDateSpan' q   -- span specified by -b/-e/-p options and query args
-      requestedspan' = dbg1 "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan ({-date2_ opts-} False) j  -- if open-ended, close it using the journal's end dates
-      intervalspans  = dbg1 "intervalspans"  $ splitSpan (intervalFromOpts opts) requestedspan' -- interval spans enclosing it
-      reportstart    = dbg1 "reportstart"    $ maybe Nothing spanStart $ headMay intervalspans
-      reportend      = dbg1 "reportend"      $ maybe Nothing spanEnd   $ lastMay intervalspans
-      reportspan     = dbg1 "reportspan"     $ DateSpan reportstart reportend  -- the requested span enlarged to a whole number of intervals
-      beforestartq   = dbg1 "beforestartq"   $ dateqcons $ DateSpan Nothing reportstart
-      beforeendq     = dbg1 "beforeendq"     $ dateqcons $ DateSpan Nothing reportend
-      reportq        = dbg1 "reportq"        $ depthless $ And [datelessq, beforeendq] -- user's query with no start date, end date on an interval boundary and no depth limit
 
-      pstoend =
-          dbg1 "ps4" $ sortBy (comparing pdate) $                                  -- sort postings by date (or date2)
-          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
-          dbg1 "ps1" $ filter (reportq `matchesPosting`) $                         -- filter postings by the query, including before the report start date, ignoring depth
-                      journalPostings $ journalSelectingAmountFromOpts opts j
-      (precedingps, reportps) = dbg1 "precedingps, reportps" $ span (beforestartq `matchesPosting`) pstoend
+      -- postings to be included in the report, and similarly-matched postings before the report start date
+      (precedingps, reportps) = matchedPostingsBeforeAndDuring opts q j reportspan
 
-      showempty = empty_ opts || average_ opts
-      -- displayexpr = display_ opts  -- XXX
-      interval = intervalFromOpts opts -- XXX
+      -- postings or pseudo postings to be displayed
+      displayps | interval == NoInterval = map (,Nothing) reportps
+                | otherwise              = summarisePostingsByInterval interval whichdate depth showempty reportspan reportps
+        where
+          interval = interval_ opts -- XXX
+          showempty = empty_ opts || average_ opts
 
-      whichdate = whichDateFromOpts opts
-      itemps | interval == NoInterval = map (,Nothing) reportps
-             | otherwise              = summarisePostingsByInterval interval whichdate depth showempty reportspan reportps
-      items = dbg1 "items" $ postingsReportItems itemps (nullposting,Nothing) whichdate depth startbal runningcalc 1
+      -- posting report items ready for display
+      items = dbg1 "postingsReport items" $ postingsReportItems displayps (nullposting,Nothing) whichdate depth startbal runningcalc startnum
         where
-          startbal = if balancetype_ opts == HistoricalBalance then sumPostings precedingps else 0
+          historical = balancetype_ opts == HistoricalBalance
+          precedingsum = sumPostings precedingps
+          precedingavg | null precedingps = 0
+                       | otherwise        = precedingsum `divideMixedAmount` (fromIntegral $ length precedingps)
+          startbal | average_ opts = if historical then precedingavg else 0
+                   | otherwise     = if historical then precedingsum else 0
+          startnum = if historical then length precedingps + 1 else 1
           runningcalc | average_ opts = \i avg amt -> avg + (amt - avg) `divideMixedAmount` (fromIntegral i) -- running average
                       | otherwise     = \_ bal amt -> bal + amt                                              -- running total
 
-      dbg1 s = let p = "postingsReport" in Hledger.Utils.dbg1 (p++" "++s)  -- add prefix in debug output
-      -- dbg1 = const id  -- exclude from debug output
-
 totallabel = "Total"
 
+-- | Adjust report start/end dates to more useful ones based on
+-- journal data and report intervals. Ie:
+-- 1. If the start date is unspecified, use the earliest date in the journal (if any)
+-- 2. If the end date is unspecified, use the latest date in the journal (if any)
+-- 3. If a report interval is specified, enlarge the dates to enclose whole intervals
+adjustReportDates :: ReportOpts -> Query -> Journal -> DateSpan
+adjustReportDates opts q j = reportspan
+  where
+    -- see also multiBalanceReport
+    requestedspan       = dbg1 "requestedspan"       $ queryDateSpan' q                                       -- span specified by -b/-e/-p options and query args
+    journalspan         = dbg1 "journalspan"         $ dates `spanUnion` date2s                               -- earliest and latest dates (or date2s) in the journal
+      where
+        dates  = journalDateSpan False j
+        date2s = journalDateSpan True  j
+    requestedspanclosed = dbg1 "requestedspanclosed" $ requestedspan `spanDefaultsFrom` journalspan           -- if open-ended, close it using the journal's dates (if any)
+    intervalspans       = dbg1 "intervalspans"       $ splitSpan (interval_ opts) requestedspanclosed  -- get the whole intervals enclosing that
+    mreportstart        = dbg1 "reportstart"         $ maybe Nothing spanStart $ headMay intervalspans        -- start of the first interval, or open ended
+    mreportend          = dbg1 "reportend"           $ maybe Nothing spanEnd   $ lastMay intervalspans        -- end of the last interval, or open ended
+    reportspan          = dbg1 "reportspan"          $ DateSpan mreportstart mreportend                       -- the requested span enlarged to whole intervals if possible
+
+-- | Find postings matching a given query, within a given date span,
+-- and also any similarly-matched postings before that date span.
+-- Date restrictions and depth restrictions in the query are ignored.
+-- A helper for the postings report.
+matchedPostingsBeforeAndDuring :: ReportOpts -> Query -> Journal -> DateSpan -> ([Posting],[Posting])
+matchedPostingsBeforeAndDuring opts q j (DateSpan mstart mend) =
+  dbg1 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps
+  where
+    beforestartq = dbg1 "beforestartq" $ dateqtype $ DateSpan Nothing mstart
+    beforeandduringps =
+      dbg1 "ps4" $ sortBy (comparing sortdate) $                               -- sort postings by date or date2
+      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
+      dbg1 "ps1" $ filter (beforeandduringq `matchesPosting`) $                -- filter postings by the query, with no start date or depth limit
+                  journalPostings $ journalSelectingAmountFromOpts opts j
+      where
+        beforeandduringq = dbg1 "beforeandduringq" $ And [depthless $ dateless q, beforeendq]
+          where
+            depthless  = filterQuery (not . queryIsDepth)
+            dateless   = filterQuery (not . queryIsDateOrDate2)
+            beforeendq = dateqtype $ DateSpan Nothing mend
+        sortdate = if date2_ opts then postingDate2 else postingDate
+        symq = dbg1 "symq" $ filterQuery queryIsSym q
+    dateqtype
+      | queryIsDate2 dateq || (queryIsDate dateq && date2_ opts) = Date2
+      | otherwise = Date
+      where
+        dateq = dbg1 "dateq" $ filterQuery queryIsDateOrDate2 $ dbg1 "q" q  -- XXX confused by multiple date:/date2: ?
+
 -- | Generate postings report line items from a list of postings or (with
 -- non-Nothing dates attached) summary postings.
 postingsReportItems :: [(Posting,Maybe Day)] -> (Posting,Maybe Day) -> WhichDate -> Int -> MixedAmount -> (Int -> MixedAmount -> MixedAmount -> MixedAmount) -> Int -> [PostingsReportItem]
@@ -127,7 +165,7 @@
   where
     date = case wd of PrimaryDate   -> postingDate p
                       SecondaryDate -> postingDate2 p
-    desc = maybe "" tdescription $ ptransaction p
+    desc = T.unpack $ maybe "" tdescription $ ptransaction p
 
 -- | Convert a list of postings into summary postings, one per interval,
 -- aggregated to the specified depth if any.
@@ -148,7 +186,7 @@
 -- interval's end date attached with a tuple.
 type SummaryPosting = (Posting, Maybe Day)
 
--- | Given a date span (representing a reporting interval) and a list of
+-- | Given a date span (representing a report interval) and a list of
 -- postings within it, aggregate the postings into one summary posting per
 -- account.
 --
@@ -233,8 +271,8 @@
 
    -- with query and/or command-line options
    assertEqual "" 11 (length $ snd $ postingsReport defreportopts Any samplejournal)
-   assertEqual ""  9 (length $ snd $ postingsReport defreportopts{monthly_=True} Any samplejournal)
-   assertEqual "" 19 (length $ snd $ postingsReport defreportopts{monthly_=True, empty_=True} Any samplejournal)
+   assertEqual ""  9 (length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal)
+   assertEqual "" 19 (length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal)
    assertEqual ""  4 (length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal)
 
    -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
diff --git a/Hledger/Reports/ReportOptions.hs b/Hledger/Reports/ReportOptions.hs
--- a/Hledger/Reports/ReportOptions.hs
+++ b/Hledger/Reports/ReportOptions.hs
@@ -15,9 +15,6 @@
   checkReportOpts,
   flat_,
   tree_,
-  dateSpanFromOpts,
-  intervalFromOpts,
-  clearedValueFromOpts,
   whichDateFromOpts,
   journalSelectingAmountFromOpts,
   queryFromOpts,
@@ -34,9 +31,12 @@
 #if !MIN_VERSION_base(4,8,0)
 import Data.Functor.Compat ((<$>))
 #endif
+import Data.Maybe
+import qualified Data.Text as T
 import Data.Typeable (Typeable)
 import Data.Time.Calendar
-import System.Console.CmdArgs.Default  -- some additional default stuff
+import Data.Default
+import Safe
 import Test.HUnit
 
 import Hledger.Data
@@ -46,13 +46,15 @@
 
 type FormatStr = String
 
--- | Which balance is being shown in a multi-column balance report.
-data BalanceType = PeriodBalance     -- ^ The change of balance in each period.
-                 | CumulativeBalance -- ^ The accumulated balance at each period's end, starting from zero at the report start date.
-                 | HistoricalBalance -- ^ The historical balance at each period's end, starting from the account balances at the report start date.
+-- | Which "balance" is being shown in a balance report.
+data BalanceType = PeriodChange      -- ^ The change of balance in each period.
+                 | CumulativeChange  -- ^ The accumulated change across multiple periods.
+                 | HistoricalBalance -- ^ The historical ending balance, including the effect of
+                                     --   all postings before the report period. Unless altered by,
+                                     --   a query, this is what you would see on a bank statement.
   deriving (Eq,Show,Data,Typeable)
 
-instance Default BalanceType where def = PeriodBalance
+instance Default BalanceType where def = PeriodChange
 
 -- | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ?
 data AccountListMode = ALDefault | ALTree | ALFlat deriving (Eq, Show, Data, Typeable)
@@ -63,12 +65,9 @@
 -- corresponding to hledger's command-line options and query language
 -- arguments. Used in hledger-lib and above.
 data ReportOpts = ReportOpts {
-     begin_          :: Maybe Day
-    ,end_            :: Maybe Day
-    ,period_         :: Maybe (Interval,DateSpan)
-    ,cleared_        :: Bool
-    ,pending_        :: Bool
-    ,uncleared_      :: Bool
+     period_         :: Period
+    ,interval_       :: Interval
+    ,clearedstatus_  :: Maybe ClearedStatus
     ,cost_           :: Bool
     ,depth_          :: Maybe Int
     ,display_        :: Maybe DisplayExp
@@ -76,19 +75,14 @@
     ,empty_          :: Bool
     ,no_elide_       :: Bool
     ,real_           :: Bool
-    ,daily_          :: Bool
-    ,weekly_         :: Bool
-    ,monthly_        :: Bool
-    ,quarterly_      :: Bool
-    ,yearly_         :: Bool
     ,format_         :: Maybe FormatStr
     ,query_          :: String -- all arguments, as a string
-    -- register
+    -- register only
     ,average_        :: Bool
     ,related_        :: Bool
-    -- balance
+    -- balance only
     ,balancetype_    :: BalanceType
-    ,accountlistmode_  :: AccountListMode
+    ,accountlistmode_ :: AccountListMode
     ,drop_           :: Int
     ,row_total_      :: Bool
     ,no_total_       :: Bool
@@ -96,6 +90,7 @@
  } deriving (Show, Data, Typeable)
 
 instance Default ReportOpts where def = defreportopts
+instance Default Bool where def = False
 
 defreportopts :: ReportOpts
 defreportopts = ReportOpts
@@ -119,50 +114,50 @@
     def
     def
     def
-    def
-    def
-    def
-    def
-    def
-    def
-    def
-    def
 
 rawOptsToReportOpts :: RawOpts -> IO ReportOpts
 rawOptsToReportOpts rawopts = checkReportOpts <$> do
   d <- getCurrentDay
+  let rawopts' = checkRawOpts rawopts
   return defreportopts{
-     begin_       = maybesmartdateopt d "begin" rawopts
-    ,end_         = maybesmartdateopt d "end" rawopts
-    ,period_      = maybeperiodopt d rawopts
-    ,cleared_     = boolopt "cleared" rawopts
-    ,pending_     = boolopt "pending" rawopts
-    ,uncleared_   = boolopt "uncleared" rawopts
-    ,cost_        = boolopt "cost" rawopts
-    ,depth_       = maybeintopt "depth" rawopts
-    ,display_     = maybedisplayopt d rawopts
-    ,date2_       = boolopt "date2" rawopts
-    ,empty_       = boolopt "empty" rawopts
-    ,no_elide_    = boolopt "no-elide" rawopts
-    ,real_        = boolopt "real" rawopts
-    ,daily_       = boolopt "daily" rawopts
-    ,weekly_      = boolopt "weekly" rawopts
-    ,monthly_     = boolopt "monthly" rawopts
-    ,quarterly_   = boolopt "quarterly" rawopts
-    ,yearly_      = boolopt "yearly" rawopts
-    ,format_      = maybestringopt "format" rawopts -- XXX move to CliOpts or move validation from Cli.CliOptions to here
-    ,query_       = unwords $ listofstringopt "args" rawopts -- doesn't handle an arg like "" right
-    ,average_     = boolopt "average" rawopts
-    ,related_     = boolopt "related" rawopts
-    ,balancetype_ = balancetypeopt rawopts
-    ,accountlistmode_ = accountlistmodeopt rawopts
-    ,drop_        = intopt "drop" rawopts
-    ,row_total_   = boolopt "row-total" rawopts
-    ,no_total_    = boolopt "no-total" rawopts
-    ,value_       = boolopt "value" rawopts
+     period_      = periodFromRawOpts d rawopts'
+    ,interval_    = intervalFromRawOpts rawopts'
+    ,clearedstatus_ = clearedStatusFromRawOpts rawopts'
+    ,cost_        = boolopt "cost" rawopts'
+    ,depth_       = maybeintopt "depth" rawopts'
+    ,display_     = maybedisplayopt d rawopts'
+    ,date2_       = boolopt "date2" rawopts'
+    ,empty_       = boolopt "empty" rawopts'
+    ,no_elide_    = boolopt "no-elide" rawopts'
+    ,real_        = boolopt "real" rawopts'
+    ,format_      = maybestringopt "format" rawopts' -- XXX move to CliOpts or move validation from Cli.CliOptions to here
+    ,query_       = unwords $ listofstringopt "args" rawopts' -- doesn't handle an arg like "" right
+    ,average_     = boolopt "average" rawopts'
+    ,related_     = boolopt "related" rawopts'
+    ,balancetype_ = balancetypeopt rawopts'
+    ,accountlistmode_ = accountlistmodeopt rawopts'
+    ,drop_        = intopt "drop" rawopts'
+    ,row_total_   = boolopt "row-total" rawopts'
+    ,no_total_    = boolopt "no-total" rawopts'
+    ,value_       = boolopt "value" rawopts'
     }
 
--- | Do extra validation of opts, raising an error if there is trouble.
+-- | Do extra validation of raw option values, raising an error if there's a problem.
+checkRawOpts :: RawOpts -> RawOpts
+checkRawOpts rawopts
+-- our standard behaviour is to accept conflicting options actually,
+-- using the last one - more forgiving for overriding command-line aliases
+--   | countopts ["change","cumulative","historical"] > 1
+--     = optserror "please specify at most one of --change, --cumulative, --historical"
+--   | countopts ["flat","tree"] > 1
+--     = optserror "please specify at most one of --flat, --tree"
+--   | countopts ["daily","weekly","monthly","quarterly","yearly"] > 1
+--     = optserror "please specify at most one of --daily, "
+  | otherwise = rawopts
+--   where
+--     countopts = length . filter (`boolopt` rawopts)
+
+-- | Do extra validation of report options, raising an error if there's a problem.
 checkReportOpts :: ReportOpts -> ReportOpts
 checkReportOpts ropts@ReportOpts{..} =
   either optserror (const ropts) $ do
@@ -178,24 +173,93 @@
     _          -> ALDefault
 
 balancetypeopt :: RawOpts -> BalanceType
-balancetypeopt rawopts
-    | length [o | o <- ["cumulative","historical"], isset o] > 1
-                         = optserror "please specify at most one of --cumulative and --historical"
-    | isset "cumulative" = CumulativeBalance
-    | isset "historical" = HistoricalBalance
-    | otherwise          = PeriodBalance
-    where
-      isset = flip boolopt rawopts
+balancetypeopt rawopts =
+  case reverse $ filter (`elem` ["change","cumulative","historical"]) $ map fst rawopts of
+    ("historical":_) -> HistoricalBalance
+    ("cumulative":_) -> CumulativeChange
+    _                -> PeriodChange
 
-maybesmartdateopt :: Day -> String -> RawOpts -> Maybe Day
-maybesmartdateopt d name rawopts =
-        case maybestringopt name rawopts of
-          Nothing -> Nothing
-          Just s -> either
-                    (\e -> optserror $ "could not parse "++name++" date: "++show e)
-                    Just
-                    $ fixSmartDateStrEither' d s
+-- Get the period specified by the intersection of -b/--begin, -e/--end and/or
+-- -p/--period options, using the given date to interpret relative date expressions.
+periodFromRawOpts :: Day -> RawOpts -> Period
+periodFromRawOpts d rawopts =
+  case (mearliestb, mlateste) of
+    (Nothing, Nothing) -> PeriodAll
+    (Just b, Nothing)  -> PeriodFrom b
+    (Nothing, Just e)  -> PeriodTo e
+    (Just b, Just e)   -> simplifyPeriod $
+                          PeriodBetween b e
+  where
+    mearliestb = case beginDatesFromRawOpts d rawopts of
+                   [] -> Nothing
+                   bs -> Just $ minimum bs
+    mlateste   = case endDatesFromRawOpts d rawopts of
+                   [] -> Nothing
+                   es -> Just $ maximum es
 
+-- Get all begin dates specified by -b/--begin or -p/--period options, in order,
+-- using the given date to interpret relative date expressions.
+beginDatesFromRawOpts :: Day -> RawOpts -> [Day]
+beginDatesFromRawOpts d = catMaybes . map (begindatefromrawopt d)
+  where
+    begindatefromrawopt d (n,v)
+      | n == "begin" =
+          either (\e -> optserror $ "could not parse "++n++" date: "++show e) Just $
+          fixSmartDateStrEither' d (T.pack v)
+      | n == "period" =
+        case
+          either (\e -> optserror $ "could not parse period option: "++show e) id $
+          parsePeriodExpr d (stripquotes $ T.pack v)
+        of
+          (_, DateSpan (Just b) _) -> Just b
+          _                        -> Nothing
+      | otherwise = Nothing
+
+-- Get all end dates specified by -e/--end or -p/--period options, in order,
+-- using the given date to interpret relative date expressions.
+endDatesFromRawOpts :: Day -> RawOpts -> [Day]
+endDatesFromRawOpts d = catMaybes . map (enddatefromrawopt d)
+  where
+    enddatefromrawopt d (n,v)
+      | n == "end" =
+          either (\e -> optserror $ "could not parse "++n++" date: "++show e) Just $
+          fixSmartDateStrEither' d (T.pack v)
+      | n == "period" =
+        case
+          either (\e -> optserror $ "could not parse period option: "++show e) id $
+          parsePeriodExpr d (stripquotes $ T.pack v)
+        of
+          (_, DateSpan _ (Just e)) -> Just e
+          _                        -> Nothing
+      | otherwise = Nothing
+
+-- | Get the report interval, if any, specified by the last of -p/--period,
+-- -D/--daily, -W/--weekly, -M/--monthly etc. options.
+intervalFromRawOpts :: RawOpts -> Interval
+intervalFromRawOpts = lastDef NoInterval . catMaybes . map intervalfromrawopt
+  where
+    intervalfromrawopt (n,v)
+      | n == "period" =
+          either (\e -> optserror $ "could not parse period option: "++show e) (Just . fst) $
+          parsePeriodExpr nulldate (stripquotes $ T.pack v) -- reference date does not affect the interval
+      | n == "daily"     = Just $ Days 1
+      | n == "weekly"    = Just $ Weeks 1
+      | n == "monthly"   = Just $ Months 1
+      | n == "quarterly" = Just $ Quarters 1
+      | n == "yearly"    = Just $ Years 1
+      | otherwise = Nothing
+
+-- | Get the cleared status, if any, specified by the last of -C/--cleared,
+-- --pending, -U/--uncleared options.
+clearedStatusFromRawOpts :: RawOpts -> Maybe ClearedStatus
+clearedStatusFromRawOpts = lastMay . catMaybes . map clearedstatusfromrawopt
+  where
+    clearedstatusfromrawopt (n,_)
+      | n == "cleared"   = Just Cleared
+      | n == "pending"   = Just Pending
+      | n == "uncleared" = Just Uncleared
+      | otherwise        = Nothing
+
 type DisplayExp = String
 
 maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp
@@ -203,17 +267,20 @@
     maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts
     where
       fixbracketeddatestr "" = ""
-      fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"
+      fixbracketeddatestr s = "[" ++ fixSmartDateStr d (T.pack $ init $ tail s) ++ "]"
 
-maybeperiodopt :: Day -> RawOpts -> Maybe (Interval,DateSpan)
-maybeperiodopt d rawopts =
-    case maybestringopt "period" rawopts of
-      Nothing -> Nothing
-      Just s -> either
-                (\e -> optserror $ "could not parse period option: "++show e)
-                Just
-                $ parsePeriodExpr d s
+-- | Select the Transaction date accessor based on --date2.
+transactionDateFn :: ReportOpts -> (Transaction -> Day)
+transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate
 
+-- | Select the Posting date accessor based on --date2.
+postingDateFn :: ReportOpts -> (Posting -> Day)
+postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate
+
+-- | Report which date we will report on based on --date2.
+whichDateFromOpts :: ReportOpts -> WhichDate
+whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate
+
 -- | Legacy-compatible convenience aliases for accountlistmode_.
 tree_ :: ReportOpts -> Bool
 tree_ = (==ALTree) . accountlistmode_
@@ -221,51 +288,9 @@
 flat_ :: ReportOpts -> Bool
 flat_ = (==ALFlat) . accountlistmode_
 
--- | Figure out the date span we should report on, based on any
--- begin/end/period options provided. A period option will cause begin and
--- end options to be ignored.
-dateSpanFromOpts :: Day -> ReportOpts -> DateSpan
-dateSpanFromOpts _ ReportOpts{..} =
-    case period_ of Just (_,span) -> span
-                    Nothing -> DateSpan begin_ end_
-
--- | Figure out the reporting interval, if any, specified by the options.
--- --period overrides --daily overrides --weekly overrides --monthly etc.
-intervalFromOpts :: ReportOpts -> Interval
-intervalFromOpts ReportOpts{..} =
-    case period_ of
-      Just (interval,_) -> interval
-      Nothing -> i
-          where i | daily_ = Days 1
-                  | weekly_ = Weeks 1
-                  | monthly_ = Months 1
-                  | quarterly_ = Quarters 1
-                  | yearly_ = Years 1
-                  | otherwise =  NoInterval
-
--- | Get a maybe boolean representing the last cleared/uncleared option if any.
-clearedValueFromOpts :: ReportOpts -> Maybe ClearedStatus
-clearedValueFromOpts ReportOpts{..} | cleared_   = Just Cleared
-                                    | pending_   = Just Pending
-                                    | uncleared_ = Just Uncleared
-                                    | otherwise  = Nothing
-
 -- depthFromOpts :: ReportOpts -> Int
 -- depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts)
 
--- | Report which date we will report on based on --date2.
-whichDateFromOpts :: ReportOpts -> WhichDate
-whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate
-
--- | Select the Transaction date accessor based on --date2.
-transactionDateFn :: ReportOpts -> (Transaction -> Day)
-transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate
-
--- | Select the Posting date accessor based on --date2.
-postingDateFn :: ReportOpts -> (Posting -> Day)
-postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate
-
-
 -- | Convert this journal's postings' amounts to the cost basis amounts if
 -- specified by options.
 journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal
@@ -275,25 +300,25 @@
 
 -- | Convert report options and arguments to a query.
 queryFromOpts :: Day -> ReportOpts -> Query
-queryFromOpts d opts@ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]
+queryFromOpts d ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]
   where
     flagsq = And $
-              [(if date2_ then Date2 else Date) $ dateSpanFromOpts d opts]
+              [(if date2_ then Date2 else Date) $ periodAsDateSpan period_]
               ++ (if real_ then [Real True] else [])
               ++ (if empty_ then [Empty True] else []) -- ?
-              ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))
+              ++ (maybe [] ((:[]) . Status) clearedstatus_)
               ++ (maybe [] ((:[]) . Depth) depth_)
-    argsq = fst $ parseQuery d query_
+    argsq = fst $ parseQuery d (T.pack query_)
 
 -- | Convert report options to a query, ignoring any non-flag command line arguments.
 queryFromOptsOnly :: Day -> ReportOpts -> Query
-queryFromOptsOnly d opts@ReportOpts{..} = simplifyQuery flagsq
+queryFromOptsOnly _d ReportOpts{..} = simplifyQuery flagsq
   where
     flagsq = And $
-              [(if date2_ then Date2 else Date) $ dateSpanFromOpts d opts]
+              [(if date2_ then Date2 else Date) $ periodAsDateSpan period_]
               ++ (if real_ then [Real True] else [])
               ++ (if empty_ then [Empty True] else []) -- ?
-              ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))
+              ++ (maybe [] ((:[]) . Status) clearedstatus_)
               ++ (maybe [] ((:[]) . Depth) depth_)
 
 tests_queryFromOpts :: [Test]
@@ -303,7 +328,7 @@
   assertEqual "" (Acct "a") (queryFromOpts nulldate defreportopts{query_="a"})
   assertEqual "" (Desc "a a") (queryFromOpts nulldate defreportopts{query_="desc:'a a'"})
   assertEqual "" (Date $ mkdatespan "2012/01/01" "2013/01/01")
-                 (queryFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")
+                 (queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01")
                                                       ,query_="date:'to 2013'"
                                                       })
   assertEqual "" (Date2 $ mkdatespan "2012/01/01" "2013/01/01")
@@ -317,14 +342,14 @@
 queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts
   where
     flagsqopts = []
-    argsqopts = snd $ parseQuery d query_
+    argsqopts = snd $ parseQuery d (T.pack query_)
 
 tests_queryOptsFromOpts :: [Test]
 tests_queryOptsFromOpts = [
  "queryOptsFromOpts" ~: do
   assertEqual "" [] (queryOptsFromOpts nulldate defreportopts)
   assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{query_="a"})
-  assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")
+  assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01")
                                                              ,query_="date:'to 2013'"
                                                              })
  ]
diff --git a/Hledger/Reports/TransactionsReports.hs b/Hledger/Reports/TransactionsReports.hs
--- a/Hledger/Reports/TransactionsReports.hs
+++ b/Hledger/Reports/TransactionsReports.hs
@@ -21,7 +21,8 @@
   triCommodityBalance,
   journalTransactionsReport,
   accountTransactionsReport,
-  transactionsReportByCommodity
+  transactionsReportByCommodity,
+  transactionRegisterDate
 
   -- -- * Tests
   -- tests_Hledger_Reports_TransactionsReports
@@ -30,11 +31,15 @@
 
 import Data.List
 import Data.Ord
+-- import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Calendar
 -- import Test.HUnit
 
 import Hledger.Data
 import Hledger.Query
 import Hledger.Reports.ReportOptions
+import Hledger.Utils.Debug
 
 
 -- | A transactions report includes a list of transactions
@@ -48,11 +53,13 @@
                           ,[TransactionsReportItem] -- line items, one per transaction
                           )
 type TransactionsReportItem = (Transaction -- the original journal transaction, unmodified
-                              ,Transaction -- the transaction as seen from a particular account
+                              ,Transaction -- the transaction as seen from a particular account, with postings maybe filtered
                               ,Bool        -- is this a split, ie more than one other account posting
                               ,String      -- a display string describing the other account(s), if any
-                              ,MixedAmount -- the amount posted to the current account(s) (or total amount posted)
-                              ,MixedAmount -- the running balance for the current account(s) after this transaction
+                              ,MixedAmount -- the amount posted to the current account(s) by the filtered postings (or total amount posted)
+                              ,MixedAmount -- the running total of item amounts, starting from zero;
+                                           -- or with --historical, the running total including items
+                                           -- (matched by the report query) preceding the report period
                               )
 
 triOrigTransaction (torig,_,_,_,_,_) = torig
@@ -80,20 +87,40 @@
 
 -- | An account transactions report represents transactions affecting
 -- a particular account (or possibly several accounts, but we don't
--- use that). It is used by hledger-web's (and hledger-ui's) account
--- register view, where we want to show one row per journal
--- transaction, with:
+-- use that). It is used eg by hledger-ui's and hledger-web's account
+-- register view, where we want to show one row per transaction, in
+-- the context of the current account. Report items consist of:
 --
--- - the total increase/decrease to the current account
+-- - the transaction, unmodified
 --
--- - the names of the other account(s) posted to/from
+-- - the transaction as seen in the context of the current account and query,
+--   which means:
 --
--- - transaction dates, adjusted to the date of the earliest posting to
---   the current account if those postings have their own dates
+--   - the transaction date is set to the "transaction context date",
+--     which can be different from the transaction's general date:
+--     if postings to the current account (and matched by the report query)
+--     have their own dates, it's the earliest of these dates.
 --
--- Currently, reporting intervals are not supported, and report items
--- are most recent first.
+--   - the transaction's postings are filtered, excluding any which are not
+--     matched by the report query
 --
+-- - a text description of the other account(s) posted to/from
+--
+-- - a flag indicating whether there's more than one other account involved
+--
+-- - the total increase/decrease to the current account
+--
+-- - the report transactions' running total after this transaction;
+--   or if historical balance is requested (-H), the historical running total.
+--   The historical running total includes transactions from before the
+--   report start date if one is specified, filtered by the report query.
+--   The historical running total may or may not be the account's historical
+--   running balance, depending on the report query.
+--
+-- Items are sorted by transaction register date (the earliest date the transaction
+-- posts to the current account), most recent first.
+-- Reporting intervals are currently ignored.
+--
 type AccountTransactionsReport =
   (String                          -- label for the balance column, eg "balance" or "total"
   ,[AccountTransactionsReportItem] -- line items, one per transaction
@@ -101,94 +128,98 @@
 
 type AccountTransactionsReportItem =
   (
-   Transaction -- the original journal transaction
-  ,Transaction -- the adjusted account transaction
-  ,Bool        -- is this a split, ie with more than one posting to other account(s)
+   Transaction -- the transaction, unmodified
+  ,Transaction -- the transaction, as seen from the current account
+  ,Bool        -- is this a split (more than one posting to other accounts) ?
   ,String      -- a display string describing the other account(s), if any
   ,MixedAmount -- the amount posted to the current account(s) (or total amount posted)
-  ,MixedAmount -- the running balance for the current account(s) after this transaction
+  ,MixedAmount -- the register's running total or the current account(s)'s historical balance, after this transaction
   )
 
 accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport
-accountTransactionsReport opts j q thisacctquery = (label, items)
- where
-     -- transactions with excluded currencies removed
-     ts1 = jtxns $
-           filterJournalAmounts (filterQuery queryIsSym q) $
-           journalSelectingAmountFromOpts opts j
-     -- affecting this account
-     ts2 = filter (matchesTransaction thisacctquery) ts1
-     -- with dates adjusted for account transactions report
-     ts3 = map (setTransactionDateToPostingDate q thisacctquery) ts2
-     -- and sorted
-     ts = sortBy (comparing tdate) ts3
-
-     -- starting balance: if we are filtering by a start date and nothing else,
-     -- the sum of postings to this account before that date; otherwise zero.
-     (startbal,label) | queryIsNull q                        = (nullmixedamt,        balancelabel)
-                      | queryIsStartDateOnly (date2_ opts) q = (sumPostings priorps, balancelabel)
-                      | otherwise                            = (nullmixedamt,        totallabel)
-                      where
-                        priorps = -- ltrace "priorps" $
-                                  filter (matchesPosting
-                                          (-- ltrace "priormatcher" $
-                                           And [thisacctquery, tostartdatequery]))
-                                         $ transactionsPostings ts
-                        tostartdatequery = Date (DateSpan Nothing startdate)
-                        startdate = queryStartDate (date2_ opts) q
+accountTransactionsReport opts j reportq thisacctq = (label, items)
+  where
+    -- a depth limit does not affect the account transactions report
+    -- seems unnecessary for some reason XXX
+    reportq' = -- filterQuery (not . queryIsDepth)
+               reportq
+    -- get all transactions, with amounts converted to cost basis if -B
+    ts1 = jtxns $ journalSelectingAmountFromOpts opts j
+    -- apply any cur:SYM filters in reportq'
+    symq  = filterQuery queryIsSym reportq'
+    ts2 = (if queryIsNull symq then id else map (filterTransactionAmounts symq)) ts1
+    -- keep just the transactions affecting this account (via possibly realness or status-filtered postings)
+    realq = filterQuery queryIsReal reportq'
+    statusq = filterQuery queryIsStatus reportq'
+    ts3 = filter (matchesTransaction thisacctq . filterTransactionPostings (And [realq, statusq])) ts2
+    -- sort by the transaction's register date, for accurate starting balance
+    ts = sortBy (comparing (transactionRegisterDate reportq' thisacctq)) ts3
 
-     items = reverse $ -- see also registerChartHtml
-             accountTransactionsReportItems q thisacctquery startbal negate ts
+    (startbal,label)
+      | balancetype_ opts == HistoricalBalance = (sumPostings priorps, balancelabel)
+      | otherwise                              = (nullmixedamt,        totallabel)
+      where
+        priorps = dbg1 "priorps" $
+                  filter (matchesPosting
+                          (dbg1 "priorq" $
+                           And [thisacctq, tostartdateq, datelessreportq]))
+                         $ transactionsPostings ts
+        tostartdateq =
+          case mstartdate of
+            Just _  -> Date (DateSpan Nothing mstartdate)
+            Nothing -> None  -- no start date specified, there are no prior postings
+        mstartdate = queryStartDate (date2_ opts) reportq'
+        datelessreportq = filterQuery (not . queryIsDateOrDate2) reportq'
 
--- | Adjust a transaction's date to the earliest date of postings to a
--- particular account, if any, after filtering with a certain query.
-setTransactionDateToPostingDate :: Query -> Query -> Transaction -> Transaction
-setTransactionDateToPostingDate query thisacctquery t = t'
-  where
-    queryps = tpostings $ filterTransactionPostings query t
-    thisacctps = filter (matchesPosting thisacctquery) queryps
-    t' = case thisacctps of
-          [] -> t
-          _  -> t{tdate=d}
-            where
-              d | null ds   = tdate t
-                | otherwise = minimum ds
-              ds = map postingDate thisacctps
-                   -- no opts here, don't even bother with that date/date2 rigmarole
+    items = reverse $ -- see also registerChartHtml
+            accountTransactionsReportItems reportq' thisacctq startbal negate ts
 
-totallabel = "Running Total"
-balancelabel = "Historical Balance"
+totallabel = "Period Total"
+balancelabel = "Historical Total"
 
 -- | Generate transactions report items from a list of transactions,
--- using the provided query and current account queries, starting
--- balance, sign-setting function and balance-summing function. With a
--- "this account" query of None, this can be used the for the
--- journalTransactionsReport also.
+-- using the provided user-specified report query, a query specifying
+-- which account to use as the focus, a starting balance, a sign-setting
+-- function and a balance-summing function. Or with a None current account
+-- query, this can also be used for the journalTransactionsReport.
 accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [TransactionsReportItem]
 accountTransactionsReportItems _ _ _ _ [] = []
-accountTransactionsReportItems query thisacctquery bal signfn (torig:ts) =
-    -- This is used for both accountTransactionsReport and journalTransactionsReport,
-    -- which makes it a bit overcomplicated
+accountTransactionsReportItems reportq thisacctq bal signfn (torig:ts) =
     case i of Just i' -> i':is
               Nothing -> is
+    -- 201403: This is used for both accountTransactionsReport and journalTransactionsReport, which makes it a bit overcomplicated
+    -- 201407: I've lost my grip on this, let's just hope for the best
+    -- 201606: we now calculate change and balance from filtered postings, check this still works well for all callers XXX
     where
-      -- XXX I've lost my grip on this, let's just hope for the best
-      origps = tpostings torig
-      tacct@Transaction{tpostings=queryps} = filterTransactionPostings query torig
-      (thisacctps, otheracctps) = partition (matchesPosting thisacctquery) origps
-      amt = negate $ sum $ map pamount thisacctps
-      numotheraccts = length $ nub $ map paccount otheracctps
-      otheracctstr | thisacctquery == None = summarisePostingAccounts origps
-                   | numotheraccts == 0    = summarisePostingAccounts thisacctps
-                   | otherwise             = summarisePostingAccounts otheracctps
-      (i,bal') = case queryps of
-           [] -> (Nothing,bal)
+      tfiltered@Transaction{tpostings=reportps} = filterTransactionPostings reportq torig
+      tacct = tfiltered{tdate=transactionRegisterDate reportq thisacctq tfiltered}
+      (i,bal') = case reportps of
+           [] -> (Nothing,bal)  -- no matched postings in this transaction, skip it
            _  -> (Just (torig, tacct, numotheraccts > 1, otheracctstr, a, b), b)
                  where
-                  a = signfn amt
+                  (thisacctps, otheracctps) = partition (matchesPosting thisacctq) reportps
+                  numotheraccts = length $ nub $ map paccount otheracctps
+                  otheracctstr | thisacctq == None  = summarisePostingAccounts reportps     -- no current account ? summarise all matched postings
+                               | numotheraccts == 0 = summarisePostingAccounts thisacctps   -- only postings to current account ? summarise those
+                               | otherwise          = summarisePostingAccounts otheracctps  -- summarise matched postings to other account(s)
+                  a = signfn $ negate $ sum $ map pamount thisacctps
                   b = bal + a
-      is = accountTransactionsReportItems query thisacctquery bal' signfn ts
+      is = accountTransactionsReportItems reportq thisacctq bal' signfn ts
 
+-- | What is the transaction's date in the context of a particular account
+-- (specified with a query) and report query, as in an account register ?
+-- It's normally the transaction's general date, but if any posting(s)
+-- matched by the report query and affecting the matched account(s) have
+-- their own earlier dates, it's the earliest of these dates.
+-- Secondary transaction/posting dates are ignored.
+transactionRegisterDate :: Query -> Query -> Transaction -> Day
+transactionRegisterDate reportq thisacctq t
+  | null thisacctps = tdate t
+  | otherwise       = minimum $ map postingDate thisacctps
+  where
+    reportps   = tpostings $ filterTransactionPostings reportq t
+    thisacctps = filter (matchesPosting thisacctq) reportps
+
 -- -- | Generate a short readable summary of some postings, like
 -- -- "from (negatives) to (positives)".
 -- summarisePostings :: [Posting] -> String
@@ -204,20 +235,17 @@
 -- To reduce noise, if there are both real and virtual postings, show only the real ones.
 summarisePostingAccounts :: [Posting] -> String
 summarisePostingAccounts ps =
-  (intercalate ", " . map accountSummarisedName . nub . map paccount) displayps
+  (intercalate ", " . map (T.unpack . accountSummarisedName) . nub . map paccount) displayps -- XXX pack
   where
     realps = filter isReal ps
     displayps | null realps = ps
               | otherwise   = realps
 
-filterTransactionPostings :: Query -> Transaction -> Transaction
-filterTransactionPostings m t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps}
-
 -------------------------------------------------------------------------------
 
 -- | Split a transactions report whose items may involve several commodities,
 -- into one or more single-commodity transactions reports.
-transactionsReportByCommodity :: TransactionsReport -> [(Commodity, TransactionsReport)]
+transactionsReportByCommodity :: TransactionsReport -> [(CommoditySymbol, TransactionsReport)]
 transactionsReportByCommodity tr =
   [(c, filterTransactionsReportByCommodity c tr) | c <- transactionsReportCommodities tr]
   where
@@ -227,7 +255,7 @@
 -- Remove transaction report items and item amount (and running
 -- balance amount) components that don't involve the specified
 -- commodity. Other item fields such as the transaction are left unchanged.
-filterTransactionsReportByCommodity :: Commodity -> TransactionsReport -> TransactionsReport
+filterTransactionsReportByCommodity :: CommoditySymbol -> TransactionsReport -> TransactionsReport
 filterTransactionsReportByCommodity c (label,items) =
   (label, fixTransactionsReportItemBalances $ concat [filterTransactionsReportItemByCommodity c i | i <- items])
   where
diff --git a/Hledger/Utils.hs b/Hledger/Utils.hs
--- a/Hledger/Utils.hs
+++ b/Hledger/Utils.hs
@@ -22,6 +22,7 @@
                           module Hledger.Utils.Parse,
                           module Hledger.Utils.Regex,
                           module Hledger.Utils.String,
+                          module Hledger.Utils.Text,
                           module Hledger.Utils.Test,
                           module Hledger.Utils.Tree,
                           -- Debug.Trace.trace,
@@ -32,13 +33,16 @@
                           )
 where
 import Control.Monad (liftM)
-import Control.Monad.IO.Class (MonadIO, liftIO)
 -- import Data.Char
 -- import Data.List
 -- import Data.Maybe
 -- import Data.PPrint
+import Data.Text (Text)
+import qualified Data.Text.IO as T
 import Data.Time.Clock
 import Data.Time.LocalTime
+-- import Data.Text (Text)
+-- import qualified Data.Text as T
 import System.Directory (getHomeDirectory)
 import System.FilePath((</>), isRelative)
 import System.IO
@@ -49,6 +53,7 @@
 import Hledger.Utils.Parse
 import Hledger.Utils.Regex
 import Hledger.Utils.String
+import Hledger.Utils.Text
 import Hledger.Utils.Test
 import Hledger.Utils.Tree
 -- import Prelude hiding (readFile,writeFile,appendFile,getContents,putStr,putStrLn)
@@ -92,6 +97,8 @@
     split es = let (first,rest) = break (x==) es
                in first : splitAtElement x rest
 
+-- text
+
 -- time
 
 getCurrentLocalTime :: IO LocalTime
@@ -100,6 +107,12 @@
   tz <- getCurrentTimeZone
   return $ utcToLocalTime tz t
 
+getCurrentZonedTime :: IO ZonedTime
+getCurrentZonedTime = do
+  t <- getCurrentTime
+  tz <- getCurrentTimeZone
+  return $ utcToZonedTime tz t
+
 -- misc
 
 isLeft :: Either a b -> Bool
@@ -115,25 +128,44 @@
 
 -- | Convert a possibly relative, possibly tilde-containing file path to an absolute one,
 -- given the current directory. ~username is not supported. Leave "-" unchanged.
-expandPath :: MonadIO m => FilePath -> FilePath -> m FilePath -- general type sig for use in reader parsers
+-- Can raise an error.
+expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers
 expandPath _ "-" = return "-"
 expandPath curdir p = (if isRelative p then (curdir </>) else id) `liftM` expandPath' p
   where
-    expandPath' ('~':'/':p)  = liftIO $ (</> p) `fmap` getHomeDirectory
-    expandPath' ('~':'\\':p) = liftIO $ (</> p) `fmap` getHomeDirectory
-    expandPath' ('~':_)      = error' "~USERNAME in paths is not supported"
+    expandPath' ('~':'/':p)  = (</> p) <$> getHomeDirectory
+    expandPath' ('~':'\\':p) = (</> p) <$> getHomeDirectory
+    expandPath' ('~':_)      = ioError $ userError "~USERNAME in paths is not supported"
     expandPath' p            = return p
 
 firstJust ms = case dropWhile (==Nothing) ms of
     [] -> Nothing
     (md:_) -> md
 
--- | Read a file in universal newline mode, handling whatever newline convention it may contain.
-readFile' :: FilePath -> IO String
+-- | Read a file in universal newline mode, handling any of the usual line ending conventions.
+readFile' :: FilePath -> IO Text
 readFile' name =  do
   h <- openFile name ReadMode
   hSetNewlineMode h universalNewlineMode
-  hGetContents h
+  T.hGetContents h
+
+-- | Read a file in universal newline mode, handling any of the usual line ending conventions.
+readFileAnyLineEnding :: FilePath -> IO Text
+readFileAnyLineEnding path =  do
+  h <- openFile path ReadMode
+  hSetNewlineMode h universalNewlineMode
+  T.hGetContents h
+
+-- | Read the given file, or standard input if the path is "-", using
+-- universal newline mode.
+readFileOrStdinAnyLineEnding :: String -> IO Text
+readFileOrStdinAnyLineEnding f = do
+  h <- fileHandle f
+  hSetNewlineMode h universalNewlineMode
+  T.hGetContents h
+  where
+    fileHandle "-" = return stdin
+    fileHandle f = openFile f ReadMode
 
 -- | Total version of maximum, for integral types, giving 0 for an empty list.
 maximum' :: Integral a => [a] -> a
diff --git a/Hledger/Utils/Debug.hs b/Hledger/Utils/Debug.hs
--- a/Hledger/Utils/Debug.hs
+++ b/Hledger/Utils/Debug.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE CPP, FlexibleContexts #-}
+{-# LANGUAGE CPP, FlexibleContexts, TypeFamilies #-}
 -- | Debugging helpers
 
 -- more:
@@ -16,50 +16,42 @@
 )
 where
 
-import Control.Monad (when)
-import Data.List
-import Debug.Trace
-import Safe (readDef)
-import System.Environment (getArgs)
-import System.Exit
-import System.IO.Unsafe (unsafePerformIO)
-import Text.Parsec
-import Text.Printf
+import           Control.Monad (when)
+import           Control.Monad.IO.Class
+import           Data.List hiding (uncons)
+import qualified Data.Text as T
+import           Debug.Trace
+import           Hledger.Utils.Parse
+import           Safe (readDef)
+import           System.Environment (getArgs)
+import           System.Exit
+import           System.IO.Unsafe (unsafePerformIO)
+import           Text.Megaparsec
+import           Text.Printf
 
 #if __GLASGOW_HASKELL__ >= 704
-import Text.Show.Pretty (ppShow)
+import           Text.Show.Pretty (ppShow)
 #else
 -- the required pretty-show version requires GHC >= 7.4
 ppShow :: Show a => a -> String
 ppShow = show
 #endif
 
-
--- | Trace (print on stdout at runtime) a showable value.
--- (for easily tracing in the middle of a complex expression)
-strace :: Show a => a -> a
-strace a = trace (show a) a
-
--- | Labelled trace - like strace, with a label prepended.
-ltrace :: Show a => String -> a -> a
-ltrace l a = trace (l ++ ": " ++ show a) a
-
--- | Monadic trace - like strace, but works as a standalone line in a monad.
-mtrace :: (Monad m, Show a) => a -> m a
-mtrace a = strace a `seq` return a
+pprint :: Show a => a -> IO ()
+pprint = putStrLn . ppShow
 
--- | Custom trace - like strace, with a custom show function.
+-- | Trace (print to stderr) a showable value using a custom show function.
 traceWith :: (a -> String) -> a -> a
-traceWith f e = trace (f e) e
+traceWith f a = trace (f a) a
 
 -- | Parsec trace - show the current parsec position and next input,
 -- and the provided label if it's non-null.
-ptrace :: Stream [Char] m t => String -> ParsecT [Char] st m ()
+ptrace :: String -> TextParser m ()
 ptrace msg = do
   pos <- getPosition
-  next <- take peeklength `fmap` getInput
+  next <- (T.take peeklength) `fmap` getInput
   let (l,c) = (sourceLine pos, sourceColumn pos)
-      s  = printf "at line %2d col %2d: %s" l c (show next) :: String
+      s  = printf "at line %2d col %2d: %s" (unPos l) (unPos c) (show next) :: String
       s' = printf ("%-"++show (peeklength+30)++"s") s ++ " " ++ msg
   trace s' $ return ()
   where
@@ -70,8 +62,10 @@
 -- @--debug@ command line flag sets it to 1, or @--debug=N@ sets it to
 -- a higher value (note: not @--debug N@ for some reason).  This uses
 -- unsafePerformIO and can be accessed from anywhere and before normal
--- command-line processing. After command-line processing, it is also
--- available as the @debug_@ field of 'Hledger.Cli.CliOptions.CliOpts'.
+-- command-line processing. When running with :main in GHCI, you must
+-- touch and reload this module to see the effect of a new --debug option.
+-- After command-line processing, it is also available as the @debug_@
+-- field of 'Hledger.Cli.CliOptions.CliOpts'.
 -- {-# OPTIONS_GHC -fno-cse #-} 
 -- {-# NOINLINE debugLevel #-}
 debugLevel :: Int
@@ -121,34 +115,34 @@
 
 -- | Convenience aliases for tracePrettyAtIO.
 -- Like dbg, but convenient to insert in an IO monad.
-dbgIO :: Show a => String -> a -> IO ()
+dbgIO :: (MonadIO m, Show a) => String -> a -> m ()
 dbgIO = tracePrettyAtIO 0
 
-dbg1IO :: Show a => String -> a -> IO ()
+dbg1IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg1IO = tracePrettyAtIO 1
 
-dbg2IO :: Show a => String -> a -> IO ()
+dbg2IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg2IO = tracePrettyAtIO 2
 
-dbg3IO :: Show a => String -> a -> IO ()
+dbg3IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg3IO = tracePrettyAtIO 3
 
-dbg4IO :: Show a => String -> a -> IO ()
+dbg4IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg4IO = tracePrettyAtIO 4
 
-dbg5IO :: Show a => String -> a -> IO ()
+dbg5IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg5IO = tracePrettyAtIO 5
 
-dbg6IO :: Show a => String -> a -> IO ()
+dbg6IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg6IO = tracePrettyAtIO 6
 
-dbg7IO :: Show a => String -> a -> IO ()
+dbg7IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg7IO = tracePrettyAtIO 7
 
-dbg8IO :: Show a => String -> a -> IO ()
+dbg8IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg8IO = tracePrettyAtIO 8
 
-dbg9IO :: Show a => String -> a -> IO ()
+dbg9IO :: (MonadIO m, Show a) => String -> a -> m ()
 dbg9IO = tracePrettyAtIO 9
 
 -- | Pretty-print a message and a showable value to the console if the debug level is at or above the specified level.
@@ -156,11 +150,9 @@
 tracePrettyAt :: Show a => Int -> String -> a -> a
 tracePrettyAt lvl = dbgppshow lvl
 
-tracePrettyAtIO :: Show a => Int -> String -> a -> IO ()
-tracePrettyAtIO lvl lbl x = tracePrettyAt lvl lbl x `seq` return ()
-
--- XXX
--- Could not deduce (a ~ ())
+-- tracePrettyAtM :: (Monad m, Show a) => Int -> String -> a -> m a
+-- tracePrettyAtM lvl lbl x = tracePrettyAt lvl lbl x `seq` return x
+-- XXX Could not deduce (a ~ ())
 -- from the context (Show a)
 --   bound by the type signature for
 --              dbgM :: Show a => String -> a -> IO ()
@@ -170,25 +162,25 @@
 --       at hledger/Hledger/Cli/Main.hs:200:13
 -- Expected type: String -> a -> IO ()
 --   Actual type: String -> a -> IO a
---
--- tracePrettyAtM :: (Monad m, Show a) => Int -> String -> a -> m a
--- tracePrettyAtM lvl lbl x = tracePrettyAt lvl lbl x `seq` return x
 
+tracePrettyAtIO :: (MonadIO m, Show a) => Int -> String -> a -> m ()
+tracePrettyAtIO lvl lbl x = liftIO $ tracePrettyAt lvl lbl x `seq` return ()
+
 -- | print this string to the console before evaluating the expression,
--- if the global debug level is non-zero.  Uses unsafePerformIO.
-dbgtrace :: String -> a -> a
-dbgtrace
-    | debugLevel > 0 = trace
-    | otherwise      = flip const
+-- if the global debug level is at or above the specified level.  Uses unsafePerformIO.
+-- dbgtrace :: Int -> String -> a -> a
+-- dbgtrace level
+--     | debugLevel >= level = trace
+--     | otherwise           = flip const
 
 -- | Print a showable value to the console, with a message, if the
 -- debug level is at or above the specified level (uses
 -- unsafePerformIO).
 -- Values are displayed with show, all on one line, which is hard to read.
-dbgshow :: Show a => Int -> String -> a -> a
-dbgshow level
-    | debugLevel >= level = ltrace
-    | otherwise           = flip const
+-- dbgshow :: Show a => Int -> String -> a -> a
+-- dbgshow level
+--     | debugLevel >= level = ltrace
+--     | otherwise           = flip const
 
 -- | Print a showable value to the console, with a message, if the
 -- debug level is at or above the specified level (uses
@@ -218,7 +210,6 @@
 --                               return a
 --     | otherwise           = a
 
-
 -- | Like dbg, then exit the program. Uses unsafePerformIO.
 dbgExit :: Show a => String -> a -> a
 dbgExit msg = const (unsafePerformIO exitFailure) . dbg msg
@@ -227,7 +218,14 @@
 -- input) to the console when the debug level is at or above
 -- this level. Uses unsafePerformIO.
 -- pdbgAt :: GenParser m => Float -> String -> m ()
-pdbg :: Stream [Char] m t => Int -> String -> ParsecT [Char] st m ()
+pdbg :: Int -> String -> TextParser m ()
 pdbg level msg = when (level <= debugLevel) $ ptrace msg
 
-
+-- | Like dbg, but writes the output to "debug.log" in the current directory.
+-- Uses unsafePerformIO. Can fail due to log file contention if called too quickly
+-- ("*** Exception: debug.log: openFile: resource busy (file is locked)").
+dbglog :: Show a => String -> a -> a
+dbglog label a =
+  (unsafePerformIO $
+    appendFile "debug.log" $ label ++ ": " ++ ppShow a ++ "\n")
+  `seq` a
diff --git a/Hledger/Utils/Parse.hs b/Hledger/Utils/Parse.hs
--- a/Hledger/Utils/Parse.hs
+++ b/Hledger/Utils/Parse.hs
@@ -1,45 +1,71 @@
-{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleContexts, TypeFamilies #-}
 module Hledger.Utils.Parse where
 
+import Control.Monad.Except
 import Data.Char
 import Data.List
-import Text.Parsec
+import Data.Text (Text)
+import Text.Megaparsec hiding (State)
+import Data.Functor.Identity (Identity(..))
 import Text.Printf
 
+import Control.Monad.State.Strict (StateT, evalStateT)
+
+import Hledger.Data.Types
 import Hledger.Utils.UTF8IOCompat (error')
 
+-- | A parser of strict text with generic user state, monad and return type.
+type TextParser m a = ParsecT Dec Text m a
+
+type JournalStateParser m a = StateT Journal (ParsecT Dec Text m) a
+
+type JournalParser a = StateT Journal (ParsecT Dec Text Identity) a
+
+-- | A journal parser that runs in IO and can throw an error mid-parse.
+type ErroringJournalParser a = StateT Journal (ParsecT Dec Text (ExceptT String IO)) a
+
 -- | Backtracking choice, use this when alternatives share a prefix.
 -- Consumes no input if all choices fail.
-choice' :: Stream s m t => [ParsecT s u m a] -> ParsecT s u m a
-choice' = choice . map Text.Parsec.try
+choice' :: [TextParser m a] -> TextParser m a
+choice' = choice . map Text.Megaparsec.try
 
-parsewith :: Parsec [Char] () a -> String -> Either ParseError a
-parsewith p = runParser p () ""
+-- | Backtracking choice, use this when alternatives share a prefix.
+-- Consumes no input if all choices fail.
+choiceInState :: [StateT s (ParsecT Dec Text m) a] -> StateT s (ParsecT Dec Text m) a
+choiceInState = choice . map Text.Megaparsec.try
 
-parseWithCtx :: Stream s m t => u -> ParsecT s u m a -> s -> m (Either ParseError a)
-parseWithCtx ctx p = runParserT p ctx ""
+parsewith :: Parsec e Text a -> Text -> Either (ParseError Char e) a
+parsewith p = runParser p ""
 
-fromparse :: Either ParseError a -> a
+parsewithString :: Parsec e String a -> String -> Either (ParseError Char e) a
+parsewithString p = runParser p ""
+
+parseWithState :: Monad m => st -> StateT st (ParsecT Dec Text m) a -> Text -> m (Either (ParseError Char Dec) a)
+parseWithState ctx p s = runParserT (evalStateT p ctx) "" s
+
+parseWithState' :: (Stream s, ErrorComponent e) => st -> StateT st (ParsecT e s Identity) a -> s -> (Either (ParseError (Token s) e) a)
+parseWithState' ctx p s = runParser (evalStateT p ctx) "" s
+
+fromparse :: (Show t, Show e) => Either (ParseError t e) a -> a
 fromparse = either parseerror id
 
-parseerror :: ParseError -> a
+parseerror :: (Show t, Show e) => ParseError t e -> a
 parseerror e = error' $ showParseError e
 
-showParseError :: ParseError -> String
+showParseError :: (Show t, Show e) => ParseError t e -> String
 showParseError e = "parse error at " ++ show e
 
-showDateParseError :: ParseError -> String
+showDateParseError :: (Show t, Show e) => ParseError t e -> String
 showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e)
 
-nonspace :: (Stream [Char] m Char) => ParsecT [Char] st m Char
+nonspace :: TextParser m Char
 nonspace = satisfy (not . isSpace)
 
-spacenonewline :: (Stream [Char] m Char) => ParsecT [Char] st m Char
+spacenonewline :: (Stream s, Char ~ Token s) => ParsecT Dec s m Char
 spacenonewline = satisfy (`elem` " \v\f\t")
 
-restofline :: (Stream [Char] m Char) => ParsecT [Char] st m String
+restofline :: TextParser m String
 restofline = anyChar `manyTill` newline
 
-eolof :: (Stream [Char] m Char) => ParsecT [Char] st m ()
+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
@@ -8,19 +8,13 @@
  stripbrackets,
  unbracket,
  -- quoting
- quoteIfSpaced,
  quoteIfNeeded,
  singleQuoteIfNeeded,
  -- quotechars,
  -- whitespacechars,
- escapeDoubleQuotes,
- escapeSingleQuotes,
  escapeQuotes,
  words',
  unwords',
- stripquotes,
- isSingleQuoted,
- isDoubleQuoted,
  -- * single-line layout
  strip,
  lstrip,
@@ -42,6 +36,7 @@
  cliptopleft,
  fitto,
  -- * wide-character-aware layout
+ charWidth,
  strWidth,
  takeWidth,
  fitString,
@@ -53,7 +48,7 @@
 
 import Data.Char
 import Data.List
-import Text.Parsec
+import Text.Megaparsec
 import Text.Printf (printf)
 
 import Hledger.Utils.Parse
@@ -106,20 +101,11 @@
             | last s == '\n' = s
             | otherwise = s ++ "\n"
 
--- | Wrap a string in double quotes, and \-prefix any embedded single
--- quotes, if it contains whitespace and is not already single- or
--- double-quoted.
-quoteIfSpaced :: String -> String
-quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s
-                | not $ any (`elem` s) whitespacechars = s
-                | otherwise = "'"++escapeSingleQuotes s++"'"
-
 -- | 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 ++ "\""
                 | otherwise = s
-
 -- | Single-quote this string if it contains whitespace or double-quotes.
 -- No good for strings containing single quotes.
 singleQuoteIfNeeded :: String -> String
@@ -133,9 +119,6 @@
 escapeDoubleQuotes :: String -> String
 escapeDoubleQuotes = regexReplace "\"" "\""
 
-escapeSingleQuotes :: String -> String
-escapeSingleQuotes = regexReplace "'" "\'"
-
 escapeQuotes :: String -> String
 escapeQuotes = regexReplace "([\"'])" "\\1"
 
@@ -143,9 +126,9 @@
 -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.
 words' :: String -> [String]
 words' "" = []
-words' s  = map stripquotes $ fromparse $ parsewith p s
+words' s  = map stripquotes $ fromparse $ parsewithString p s
     where
-      p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` many1 spacenonewline
+      p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` some spacenonewline
              -- eof
              return ss
       pattern = many (noneOf whitespacechars)
diff --git a/Hledger/Utils/Test.hs b/Hledger/Utils/Test.hs
--- a/Hledger/Utils/Test.hs
+++ b/Hledger/Utils/Test.hs
@@ -1,7 +1,7 @@
 module Hledger.Utils.Test where
 
 import Test.HUnit
-import Text.Parsec
+import Text.Megaparsec
 
 -- | Get a Test's label, or the empty string.
 testName :: Test -> String
@@ -25,15 +25,16 @@
 a `is` e = assertEqual "" e a
 
 -- | Assert a parse result is successful, printing the parse error on failure.
-assertParse :: (Either ParseError a) -> Assertion
+assertParse :: (Show t, Show e) => (Either (ParseError t e) a) -> Assertion
 assertParse parse = either (assertFailure.show) (const (return ())) parse
 
+
 -- | Assert a parse result is successful, printing the parse error on failure.
-assertParseFailure :: (Either ParseError a) -> Assertion
+assertParseFailure :: (Either (ParseError t e) a) -> Assertion
 assertParseFailure parse = either (const $ return ()) (const $ assertFailure "parse should not have succeeded") parse
 
 -- | Assert a parse result is some expected value, printing the parse error on failure.
-assertParseEqual :: (Show a, Eq a) => (Either ParseError a) -> a -> Assertion
+assertParseEqual :: (Show a, Eq a, Show t, Show e) => (Either (ParseError t e) a) -> a -> Assertion
 assertParseEqual parse expected = either (assertFailure.show) (`is` expected) parse
 
 printParseError :: (Show a) => a -> IO ()
diff --git a/Hledger/Utils/Text.hs b/Hledger/Utils/Text.hs
new file mode 100644
--- /dev/null
+++ b/Hledger/Utils/Text.hs
@@ -0,0 +1,414 @@
+-- | Text formatting helpers, ported from String as needed.
+-- There may be better alternatives out there.
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hledger.Utils.Text
+ --  (
+ -- -- * misc
+ -- lowercase,
+ -- uppercase,
+ -- underline,
+ -- stripbrackets,
+ -- unbracket,
+ -- -- quoting
+ -- quoteIfSpaced,
+ -- quoteIfNeeded,
+ -- singleQuoteIfNeeded,
+ -- -- quotechars,
+ -- -- whitespacechars,
+ -- escapeDoubleQuotes,
+ -- escapeSingleQuotes,
+ -- escapeQuotes,
+ -- words',
+ -- unwords',
+ -- stripquotes,
+ -- isSingleQuoted,
+ -- isDoubleQuoted,
+ -- -- * single-line layout
+ -- strip,
+ -- lstrip,
+ -- rstrip,
+ -- chomp,
+ -- elideLeft,
+ -- elideRight,
+ -- formatString,
+ -- -- * multi-line layout
+ -- concatTopPadded,
+ -- concatBottomPadded,
+ -- concatOneLine,
+ -- vConcatLeftAligned,
+ -- vConcatRightAligned,
+ -- padtop,
+ -- padbottom,
+ -- padleft,
+ -- padright,
+ -- cliptopleft,
+ -- fitto,
+ -- -- * wide-character-aware layout
+ -- strWidth,
+ -- textTakeWidth,
+ -- fitString,
+ -- fitStringMulti,
+ -- padLeftWide,
+ -- padRightWide
+ -- )
+where
+
+-- import Data.Char
+import Data.List
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
+-- import Text.Parsec
+-- import Text.Printf (printf)
+
+-- import Hledger.Utils.Parse
+-- import Hledger.Utils.Regex
+import Hledger.Utils.String (charWidth)
+
+-- lowercase, uppercase :: String -> String
+-- lowercase = map toLower
+-- uppercase = map toUpper
+
+-- | Remove leading and trailing whitespace.
+textstrip :: Text -> Text
+textstrip = textlstrip . textrstrip
+
+-- | Remove leading whitespace.
+textlstrip :: Text -> Text
+textlstrip = T.dropWhile (`elem` (" \t" :: String)) :: Text -> Text -- XXX isSpace ?
+
+-- | Remove trailing whitespace.
+textrstrip = T.reverse . textlstrip . T.reverse
+textrstrip :: Text -> Text
+
+-- -- | Remove trailing newlines/carriage returns.
+-- chomp :: String -> String
+-- chomp = reverse . dropWhile (`elem` "\r\n") . reverse
+
+-- stripbrackets :: String -> String
+-- stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse :: String -> String
+
+-- elideLeft :: Int -> String -> String
+-- elideLeft width s =
+--     if length s > width then ".." ++ reverse (take (width - 2) $ reverse s) else s
+
+textElideRight :: Int -> Text -> Text
+textElideRight width t =
+    if T.length t > width then T.take (width - 2) t <> ".." else t
+
+-- -- | Clip and pad a string to a minimum & maximum width, and/or left/right justify it.
+-- -- Works on multi-line strings too (but will rewrite non-unix line endings).
+-- formatString :: Bool -> Maybe Int -> Maybe Int -> String -> String
+-- formatString leftJustified minwidth maxwidth s = intercalate "\n" $ map (printf fmt) $ lines s
+--     where
+--       justify = if leftJustified then "-" else ""
+--       minwidth' = maybe "" show minwidth
+--       maxwidth' = maybe "" (("."++).show) maxwidth
+--       fmt = "%" ++ justify ++ minwidth' ++ maxwidth' ++ "s"
+
+-- underline :: String -> String
+-- underline s = s' ++ replicate (length s) '-' ++ "\n"
+--     where s'
+--             | last s == '\n' = s
+--             | otherwise = s ++ "\n"
+
+-- | Wrap a string in double quotes, and \-prefix any embedded single
+-- quotes, if it contains whitespace and is not already single- or
+-- double-quoted.
+quoteIfSpaced :: T.Text -> T.Text
+quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s
+                | not $ any (`elem` (T.unpack s)) whitespacechars = s
+                | otherwise = "'"<>escapeSingleQuotes s<>"'"
+
+-- -- | Wrap a string in double quotes, and \-prefix any embedded single
+-- -- quotes, if it contains whitespace and is not already single- or
+-- -- double-quoted.
+-- quoteIfSpaced :: String -> String
+-- quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s
+--                 | not $ any (`elem` s) whitespacechars = s
+--                 | otherwise = "'"++escapeSingleQuotes s++"'"
+
+-- -- | 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
+
+-- -- | Single-quote this string if it contains whitespace or double-quotes.
+-- -- No good for strings containing single quotes.
+-- singleQuoteIfNeeded :: String -> String
+-- singleQuoteIfNeeded s | any (`elem` s) whitespacechars = "'"++s++"'"
+--                       | otherwise = s
+
+quotechars, whitespacechars :: [Char]
+quotechars      = "'\""
+whitespacechars = " \t\n\r"
+
+escapeDoubleQuotes :: T.Text -> T.Text
+escapeDoubleQuotes = T.replace "\"" "\""
+
+escapeSingleQuotes :: T.Text -> T.Text
+escapeSingleQuotes = T.replace "'" "\'"
+
+-- escapeQuotes :: String -> String
+-- escapeQuotes = regexReplace "([\"'])" "\\1"
+
+-- -- | Quote-aware version of words - don't split on spaces which are inside quotes.
+-- -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.
+-- words' :: String -> [String]
+-- words' "" = []
+-- words' s  = map stripquotes $ fromparse $ parsewith p s
+--     where
+--       p = do ss <- (singleQuotedPattern <|> doubleQuotedPattern <|> pattern) `sepBy` many1 spacenonewline
+--              -- eof
+--              return ss
+--       pattern = many (noneOf whitespacechars)
+--       singleQuotedPattern = between (char '\'') (char '\'') (many $ noneOf "'")
+--       doubleQuotedPattern = between (char '"') (char '"') (many $ noneOf "\"")
+
+-- -- | Quote-aware version of unwords - single-quote strings which contain whitespace
+-- unwords' :: [Text] -> Text
+-- unwords' = T.unwords . map quoteIfNeeded
+
+-- | Strip one matching pair of single or double quotes on the ends of a string.
+stripquotes :: Text -> Text
+stripquotes s = if isSingleQuoted s || isDoubleQuoted s then T.init $ T.tail s else s
+
+isSingleQuoted :: Text -> Bool
+isSingleQuoted s =
+  T.length (T.take 2 s) == 2 && T.head s == '\'' && T.last s == '\''
+
+isDoubleQuoted :: Text -> Bool
+isDoubleQuoted s =
+  T.length (T.take 2 s) == 2 && T.head s == '"' && T.last s == '"'
+
+textUnbracket :: Text -> Text
+textUnbracket s
+    | (T.head s == '[' && T.last s == ']') || (T.head s == '(' && T.last s == ')') = T.init $ T.tail s
+    | otherwise = s
+
+-- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded.
+-- Treats wide characters as double width.
+textConcatTopPadded :: [Text] -> Text
+textConcatTopPadded ts = T.intercalate "\n" $ map T.concat $ transpose padded
+    where
+      lss = map T.lines ts :: [[Text]]
+      h = maximum $ map length lss
+      ypad ls = replicate (difforzero h (length ls)) "" ++ ls
+      xpad ls = map (textPadLeftWide w) ls
+        where w | null ls = 0
+                | otherwise = maximum $ map textWidth ls
+      padded = map (xpad . ypad) lss :: [[Text]]
+
+-- -- | Join several multi-line strings as side-by-side rectangular strings of the same height, bottom-padded.
+-- -- Treats wide characters as double width.
+-- concatBottomPadded :: [String] -> String
+-- concatBottomPadded strs = intercalate "\n" $ map concat $ transpose padded
+--     where
+--       lss = map lines strs
+--       h = maximum $ map length lss
+--       ypad ls = ls ++ replicate (difforzero h (length ls)) ""
+--       xpad ls = map (padRightWide w) ls where w | null ls = 0
+--                                                 | otherwise = maximum $ map strWidth ls
+--       padded = map (xpad . ypad) lss
+
+
+-- -- | Join multi-line strings horizontally, after compressing each of
+-- -- them to a single line with a comma and space between each original line.
+-- concatOneLine :: [String] -> String
+-- concatOneLine strs = concat $ map ((intercalate ", ").lines) strs
+
+-- -- | Join strings vertically, left-aligned and right-padded.
+-- vConcatLeftAligned :: [String] -> String
+-- vConcatLeftAligned ss = intercalate "\n" $ map showfixedwidth ss
+--     where
+--       showfixedwidth = printf (printf "%%-%ds" width)
+--       width = maximum $ map length ss
+
+-- -- | Join strings vertically, right-aligned and left-padded.
+-- vConcatRightAligned :: [String] -> String
+-- vConcatRightAligned ss = intercalate "\n" $ map showfixedwidth ss
+--     where
+--       showfixedwidth = printf (printf "%%%ds" width)
+--       width = maximum $ map length ss
+
+-- -- | Convert a multi-line string to a rectangular string top-padded to the specified height.
+-- padtop :: Int -> String -> String
+-- padtop h s = intercalate "\n" xpadded
+--     where
+--       ls = lines s
+--       sh = length ls
+--       sw | null ls = 0
+--          | otherwise = maximum $ map length ls
+--       ypadded = replicate (difforzero h sh) "" ++ ls
+--       xpadded = map (padleft sw) ypadded
+
+-- -- | Convert a multi-line string to a rectangular string bottom-padded to the specified height.
+-- padbottom :: Int -> String -> String
+-- padbottom h s = intercalate "\n" xpadded
+--     where
+--       ls = lines s
+--       sh = length ls
+--       sw | null ls = 0
+--          | otherwise = maximum $ map length ls
+--       ypadded = ls ++ replicate (difforzero h sh) ""
+--       xpadded = map (padleft sw) ypadded
+
+difforzero :: (Num a, Ord a) => a -> a -> a
+difforzero a b = maximum [(a - b), 0]
+
+-- -- | Convert a multi-line string to a rectangular string left-padded to the specified width.
+-- -- Treats wide characters as double width.
+-- padleft :: Int -> String -> String
+-- padleft w "" = concat $ replicate w " "
+-- padleft w s = intercalate "\n" $ map (printf (printf "%%%ds" w)) $ lines s
+
+-- -- | Convert a multi-line string to a rectangular string right-padded to the specified width.
+-- -- Treats wide characters as double width.
+-- padright :: Int -> String -> String
+-- padright w "" = concat $ replicate w " "
+-- padright w s = intercalate "\n" $ map (printf (printf "%%-%ds" w)) $ lines s
+
+-- -- | Clip a multi-line string to the specified width and height from the top left.
+-- cliptopleft :: Int -> Int -> String -> String
+-- cliptopleft w h = intercalate "\n" . take h . map (take w) . lines
+
+-- -- | Clip and pad a multi-line string to fill the specified width and height.
+-- fitto :: Int -> Int -> String -> String
+-- fitto w h s = intercalate "\n" $ take h $ rows ++ repeat blankline
+--     where
+--       rows = map (fit w) $ lines s
+--       fit w = take w . (++ repeat ' ')
+--       blankline = replicate w ' '
+
+-- -- Functions below treat wide (eg CJK) characters as double-width.
+
+-- | General-purpose wide-char-aware single-line text layout function.
+-- It can left- or right-pad a short string to a minimum width.
+-- It can left- or right-clip a long string to a maximum width, optionally inserting an ellipsis (the third argument).
+-- It clips and pads on the right when the fourth argument is true, otherwise on the left.
+-- It treats wide characters as double width.
+fitText :: Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text
+fitText mminwidth mmaxwidth ellipsify rightside s = (clip . pad) s
+  where
+    clip :: Text -> Text
+    clip s =
+      case mmaxwidth of
+        Just w
+          | textWidth s > w ->
+            case rightside of
+              True  -> textTakeWidth (w - T.length ellipsis) s <> ellipsis
+              False -> ellipsis <> T.reverse (textTakeWidth (w - T.length ellipsis) $ T.reverse s)
+          | otherwise -> s
+          where
+            ellipsis = if ellipsify then ".." else ""
+        Nothing -> s
+    pad :: Text -> Text
+    pad s =
+      case mminwidth of
+        Just w
+          | sw < w ->
+            case rightside of
+              True  -> s <> T.replicate (w - sw) " "
+              False -> T.replicate (w - sw) " " <> s
+          | otherwise -> s
+        Nothing -> s
+      where sw = textWidth s
+
+-- -- | A version of fitString that works on multi-line strings,
+-- -- separate for now to avoid breakage.
+-- -- This will rewrite any line endings to unix newlines.
+-- fitStringMulti :: Maybe Int -> Maybe Int -> Bool -> Bool -> String -> String
+-- fitStringMulti mminwidth mmaxwidth ellipsify rightside s =
+--   (intercalate "\n" . map (fitString mminwidth mmaxwidth ellipsify rightside) . lines) s
+
+-- | Left-pad a text to the specified width.
+-- Treats wide characters as double width.
+-- Works on multi-line texts too (but will rewrite non-unix line endings).
+textPadLeftWide :: Int -> Text -> Text
+textPadLeftWide w "" = T.replicate w " "
+textPadLeftWide w s  = T.intercalate "\n" $ map (fitText (Just w) Nothing False False) $ T.lines s
+-- XXX not yet replaceable by
+-- padLeftWide w = fitStringMulti (Just w) Nothing False False
+
+-- | Right-pad a string to the specified width.
+-- Treats wide characters as double width.
+-- Works on multi-line strings too (but will rewrite non-unix line endings).
+textPadRightWide :: Int -> Text -> Text
+textPadRightWide w "" = T.replicate w " "
+textPadRightWide w s  = T.intercalate "\n" $ map (fitText (Just w) Nothing False True) $ T.lines s
+-- XXX not yet replaceable by
+-- padRightWide w = fitStringMulti (Just w) Nothing False True
+
+-- | Double-width-character-aware string truncation. Take as many
+-- characters as possible from a string without exceeding the
+-- specified width. Eg textTakeWidth 3 "りんご" = "り".
+textTakeWidth :: Int -> Text -> Text
+textTakeWidth _ ""     = ""
+textTakeWidth 0 _      = ""
+textTakeWidth w t | not (T.null t),
+                let c = T.head t,
+                let cw = charWidth c,
+                cw <= w
+                = T.cons c $ textTakeWidth (w-cw) (T.tail t)
+              | otherwise = ""
+
+-- -- from Pandoc (copyright John MacFarlane, GPL)
+-- -- see also http://unicode.org/reports/tr11/#Description
+
+-- | Calculate the designated render width of a string, taking into
+-- account wide characters and line breaks (the longest line within a
+-- multi-line string determines the width ).
+textWidth :: Text -> Int
+textWidth "" = 0
+textWidth s = maximum $ map (T.foldr (\a b -> charWidth a + b) 0) $ T.lines s
+
+-- -- | Get the designated render width of a character: 0 for a combining
+-- -- character, 1 for a regular character, 2 for a wide character.
+-- -- (Wide characters are rendered as exactly double width in apps and
+-- -- fonts that support it.) (From Pandoc.)
+-- charWidth :: Char -> Int
+-- charWidth c =
+--   case c of
+--       _ | c <  '\x0300'                    -> 1
+--         | c >= '\x0300' && c <= '\x036F'   -> 0  -- combining
+--         | c >= '\x0370' && c <= '\x10FC'   -> 1
+--         | c >= '\x1100' && c <= '\x115F'   -> 2
+--         | c >= '\x1160' && c <= '\x11A2'   -> 1
+--         | c >= '\x11A3' && c <= '\x11A7'   -> 2
+--         | c >= '\x11A8' && c <= '\x11F9'   -> 1
+--         | c >= '\x11FA' && c <= '\x11FF'   -> 2
+--         | c >= '\x1200' && c <= '\x2328'   -> 1
+--         | c >= '\x2329' && c <= '\x232A'   -> 2
+--         | c >= '\x232B' && c <= '\x2E31'   -> 1
+--         | c >= '\x2E80' && c <= '\x303E'   -> 2
+--         | c == '\x303F'                    -> 1
+--         | c >= '\x3041' && c <= '\x3247'   -> 2
+--         | c >= '\x3248' && c <= '\x324F'   -> 1 -- ambiguous
+--         | c >= '\x3250' && c <= '\x4DBF'   -> 2
+--         | c >= '\x4DC0' && c <= '\x4DFF'   -> 1
+--         | c >= '\x4E00' && c <= '\xA4C6'   -> 2
+--         | c >= '\xA4D0' && c <= '\xA95F'   -> 1
+--         | c >= '\xA960' && c <= '\xA97C'   -> 2
+--         | c >= '\xA980' && c <= '\xABF9'   -> 1
+--         | c >= '\xAC00' && c <= '\xD7FB'   -> 2
+--         | c >= '\xD800' && c <= '\xDFFF'   -> 1
+--         | c >= '\xE000' && c <= '\xF8FF'   -> 1 -- ambiguous
+--         | c >= '\xF900' && c <= '\xFAFF'   -> 2
+--         | c >= '\xFB00' && c <= '\xFDFD'   -> 1
+--         | c >= '\xFE00' && c <= '\xFE0F'   -> 1 -- ambiguous
+--         | c >= '\xFE10' && c <= '\xFE19'   -> 2
+--         | c >= '\xFE20' && c <= '\xFE26'   -> 1
+--         | c >= '\xFE30' && c <= '\xFE6B'   -> 2
+--         | c >= '\xFE70' && c <= '\xFEFF'   -> 1
+--         | c >= '\xFF01' && c <= '\xFF60'   -> 2
+--         | c >= '\xFF61' && c <= '\x16A38'  -> 1
+--         | c >= '\x1B000' && c <= '\x1B001' -> 2
+--         | c >= '\x1D000' && c <= '\x1F1FF' -> 1
+--         | c >= '\x1F200' && c <= '\x1F251' -> 2
+--         | c >= '\x1F300' && c <= '\x1F773' -> 1
+--         | c >= '\x20000' && c <= '\x3FFFD' -> 2
+--         | otherwise                        -> 1
+
diff --git a/Hledger/Utils/Tree.hs b/Hledger/Utils/Tree.hs
--- a/Hledger/Utils/Tree.hs
+++ b/Hledger/Utils/Tree.hs
@@ -4,7 +4,7 @@
 import Data.List (foldl')
 import qualified Data.Map as M
 import Data.Tree
--- import Text.Parsec
+-- import Text.Megaparsec
 -- import Text.Printf
 
 import Hledger.Utils.Regex
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+A reusable library containing hledger's core functionality.
+This is used by most hledger* packages for common data parsing,
+command line option handling, reporting etc.
diff --git a/doc/hledger_csv.5 b/doc/hledger_csv.5
new file mode 100644
--- /dev/null
+++ b/doc/hledger_csv.5
@@ -0,0 +1,239 @@
+
+.TH "hledger_csv" "5" "October 2016" "hledger 1.0" "hledger User Manuals"
+
+
+
+.SH NAME
+.PP
+CSV \- how hledger reads CSV data, and the CSV rules file format
+.SH DESCRIPTION
+.PP
+hledger can read CSV files, converting each CSV record into a journal
+entry (transaction), if you provide some conversion hints in a "rules
+file".
+This file should be named like the CSV file with an additional
+\f[C]\&.rules\f[] suffix (eg: \f[C]mybank.csv.rules\f[]); or, you can
+specify the file with \f[C]\-\-rules\-file\ PATH\f[].
+hledger will create it if necessary, with some default rules which
+you\[aq]ll need to adjust.
+At minimum, the rules file must specify the \f[C]date\f[] and
+\f[C]amount\f[] fields.
+For an example, see How to read CSV files.
+.PP
+To learn about \f[I]exporting\f[] CSV, see CSV output.
+.SH CSV RULES
+.PP
+The following six kinds of rule can appear in the rules file, in any
+order.
+Blank lines and lines beginning with \f[C]#\f[] or \f[C];\f[] are
+ignored.
+.SS skip
+.PP
+\f[C]skip\f[]\f[I]\f[C]N\f[]\f[]
+.PP
+Skip this number of CSV records at the beginning.
+You\[aq]ll need this whenever your CSV data contains header lines.
+Eg:
+.IP
+.nf
+\f[C]
+#\ ignore\ the\ first\ CSV\ line
+skip\ 1
+\f[]
+.fi
+.SS date\-format
+.PP
+\f[C]date\-format\f[]\f[I]\f[C]DATEFMT\f[]\f[]
+.PP
+When your CSV date fields are not formatted like \f[C]YYYY/MM/DD\f[] (or
+\f[C]YYYY\-MM\-DD\f[] or \f[C]YYYY.MM.DD\f[]), you\[aq]ll need to
+specify the format.
+DATEFMT is a strptime\-like date parsing pattern, which must parse the
+date field values completely.
+Examples:
+.IP
+.nf
+\f[C]
+#\ for\ dates\ like\ "6/11/2013":
+date\-format\ %\-d/%\-m/%Y
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+#\ for\ dates\ like\ "11/06/2013":
+date\-format\ %m/%d/%Y
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+#\ for\ dates\ like\ "2013\-Nov\-06":
+date\-format\ %Y\-%h\-%d
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+#\ for\ dates\ like\ "11/6/2013\ 11:32\ PM":
+date\-format\ %\-m/%\-d/%Y\ %l:%M\ %p
+\f[]
+.fi
+.SS field list
+.PP
+\f[C]fields\f[]\f[I]\f[C]FIELDNAME1\f[]\f[],
+\f[I]\f[C]FIELDNAME2\f[]\f[]...
+.PP
+This (a) names the CSV fields, in order (names may not contain
+whitespace; uninteresting names may be left blank), and (b) assigns them
+to journal entry fields if you use any of these standard field names:
+\f[C]date\f[], \f[C]date2\f[], \f[C]status\f[], \f[C]code\f[],
+\f[C]description\f[], \f[C]comment\f[], \f[C]account1\f[],
+\f[C]account2\f[], \f[C]amount\f[], \f[C]amount\-in\f[],
+\f[C]amount\-out\f[], \f[C]currency\f[].
+Eg:
+.IP
+.nf
+\f[C]
+#\ use\ the\ 1st,\ 2nd\ and\ 4th\ CSV\ fields\ as\ the\ entry\[aq]s\ date,\ description\ and\ amount,
+#\ and\ give\ the\ 7th\ and\ 8th\ fields\ meaningful\ names\ for\ later\ reference:
+#
+#\ CSV\ field:
+#\ \ \ \ \ \ 1\ \ \ \ \ 2\ \ \ \ \ \ \ \ \ \ \ \ 3\ 4\ \ \ \ \ \ \ 5\ 6\ 7\ \ \ \ \ \ \ \ \ \ 8
+#\ entry\ field:
+fields\ date,\ description,\ ,\ amount,\ ,\ ,\ somefield,\ anotherfield
+\f[]
+.fi
+.SS field assignment
+.PP
+\f[I]\f[C]ENTRYFIELDNAME\f[]\f[] \f[I]\f[C]FIELDVALUE\f[]\f[]
+.PP
+This sets a journal entry field (one of the standard names above) to the
+given text value, which can include CSV field values interpolated by
+name (\f[C]%CSVFIELDNAME\f[]) or 1\-based position (\f[C]%N\f[]).
+ Eg:
+.IP
+.nf
+\f[C]
+#\ set\ the\ amount\ to\ the\ 4th\ CSV\ field\ with\ "USD\ "\ prepended
+amount\ USD\ %4
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+#\ combine\ three\ fields\ to\ make\ a\ comment\ (containing\ two\ tags)
+comment\ note:\ %somefield\ \-\ %anotherfield,\ date:\ %1
+\f[]
+.fi
+.PP
+Field assignments can be used instead of or in addition to a field list.
+.SS conditional block
+.PP
+\f[C]if\f[] \f[I]\f[C]PATTERN\f[]\f[]
+.PD 0
+.P
+.PD
+\ \ \ \ \f[I]\f[C]FIELDASSIGNMENTS\f[]\f[]...
+.PP
+\f[C]if\f[]
+.PD 0
+.P
+.PD
+\f[I]\f[C]PATTERN\f[]\f[]
+.PD 0
+.P
+.PD
+\f[I]\f[C]PATTERN\f[]\f[]...
+.PD 0
+.P
+.PD
+\ \ \ \ \f[I]\f[C]FIELDASSIGNMENTS\f[]\f[]...
+.PP
+This applies one or more field assignments, only to those CSV records
+matched by one of the PATTERNs.
+The patterns are case\-insensitive regular expressions which match
+anywhere within the whole CSV record (it\[aq]s not yet possible to match
+within a specific field).
+When there are multiple patterns they can be written on separate lines,
+unindented.
+The field assignments are on separate lines indented by at least one
+space.
+Examples:
+.IP
+.nf
+\f[C]
+#\ if\ the\ CSV\ record\ contains\ "groceries",\ set\ account2\ to\ "expenses:groceries"
+if\ groceries
+\ account2\ expenses:groceries
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+#\ if\ the\ CSV\ record\ contains\ any\ of\ these\ patterns,\ set\ account2\ and\ comment\ as\ shown
+if
+monthly\ service\ fee
+atm\ transaction\ fee
+banking\ thru\ software
+\ account2\ expenses:business:banking
+\ comment\ \ XXX\ deductible\ ?\ check\ it
+\f[]
+.fi
+.SS include
+.PP
+\f[C]include\f[]\f[I]\f[C]RULESFILE\f[]\f[]
+.PP
+Include another rules file at this point.
+\f[C]RULESFILE\f[] is either an absolute file path or a path relative to
+the current file\[aq]s directory.
+Eg:
+.IP
+.nf
+\f[C]
+#\ rules\ reused\ with\ several\ CSV\ files
+include\ common.rules
+\f[]
+.fi
+.SH TIPS
+.PP
+Each generated journal entry will have two postings, to
+\f[C]account1\f[] and \f[C]account2\f[] respectively.
+Currently it\[aq]s not possible to generate entries with more than two
+postings.
+.PP
+If the CSV has debit/credit amounts in separate fields, assign to the
+\f[C]amount\-in\f[] and \f[C]amount\-out\f[] pseudo fields instead of
+\f[C]amount\f[].
+.PP
+If the CSV has the currency in a separate field, assign that to the
+\f[C]currency\f[] pseudo field which will be automatically prepended to
+the amount.
+(Or you can do the same thing with a field assignment.)
+.PP
+If an amount value is parenthesised, it will be de\-parenthesised and
+sign\-flipped automatically.
+.PP
+The generated journal entries will be sorted by date.
+The original order of same\-day entries will be preserved, usually.
+
+
+.SH "REPORTING BUGS"
+Report bugs at http://bugs.hledger.org
+(or on the #hledger IRC channel or hledger mail list)
+
+.SH AUTHORS
+Simon Michael <simon@joyful.com> and contributors
+
+.SH COPYRIGHT
+
+Copyright (C) 2007-2016 Simon Michael.
+.br
+Released under GNU GPL v3 or later.
+
+.SH SEE ALSO
+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),
+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),
+ledger(1)
+
+http://hledger.org
diff --git a/doc/hledger_csv.5.info b/doc/hledger_csv.5.info
new file mode 100644
--- /dev/null
+++ b/doc/hledger_csv.5.info
@@ -0,0 +1,231 @@
+This is hledger-lib/doc/hledger_csv.5.info, produced by makeinfo
+version 4.8 from stdin.
+
+
+File: hledger_csv.5.info,  Node: Top,  Up: (dir)
+
+hledger_csv(5) hledger 1.0
+**************************
+
+hledger can read CSV files, converting each CSV record into a journal
+entry (transaction), if you provide some conversion hints in a "rules
+file". This file should be named like the CSV file with an additional
+`.rules' suffix (eg: `mybank.csv.rules'); or, you can specify the file
+with `--rules-file PATH'. hledger will create it if necessary, with
+some default rules which you'll need to adjust. At minimum, the rules
+file must specify the `date' and `amount' fields. For an example, see
+How to read CSV files.
+
+   To learn about _exporting_ CSV, see CSV output.
+
+* Menu:
+
+* CSV RULES::
+* TIPS::
+
+
+File: hledger_csv.5.info,  Node: CSV RULES,  Next: TIPS,  Prev: Top,  Up: Top
+
+1 CSV RULES
+***********
+
+The following six kinds of rule can appear in the rules file, in any
+order. Blank lines and lines beginning with `#' or `;' are ignored.
+
+* Menu:
+
+* skip::
+* date-format::
+* field list::
+* field assignment::
+* conditional block::
+* include::
+
+
+File: hledger_csv.5.info,  Node: skip,  Next: date-format,  Up: CSV RULES
+
+1.1 skip
+========
+
+`skip'_`N'_
+
+   Skip this number of CSV records at the beginning. You'll need this
+whenever your CSV data contains header lines. Eg:
+
+
+# ignore the first CSV line
+skip 1
+
+
+File: hledger_csv.5.info,  Node: date-format,  Next: field list,  Prev: skip,  Up: CSV RULES
+
+1.2 date-format
+===============
+
+`date-format'_`DATEFMT'_
+
+   When your CSV date fields are not formatted like `YYYY/MM/DD' (or
+`YYYY-MM-DD' or `YYYY.MM.DD'), you'll need to specify the format.
+DATEFMT is a strptime-like date parsing pattern, which must parse the
+date field values completely. Examples:
+
+
+# for dates like "6/11/2013":
+date-format %-d/%-m/%Y
+
+
+# for dates like "11/06/2013":
+date-format %m/%d/%Y
+
+
+# for dates like "2013-Nov-06":
+date-format %Y-%h-%d
+
+
+# for dates like "11/6/2013 11:32 PM":
+date-format %-m/%-d/%Y %l:%M %p
+
+
+File: hledger_csv.5.info,  Node: field list,  Next: field assignment,  Prev: date-format,  Up: CSV RULES
+
+1.3 field list
+==============
+
+`fields'_`FIELDNAME1'_, _`FIELDNAME2'_...
+
+   This (a) names the CSV fields, in order (names may not contain
+whitespace; uninteresting names may be left blank), and (b) assigns them
+to journal entry fields if you use any of these standard field names:
+`date', `date2', `status', `code', `description', `comment',
+`account1', `account2', `amount', `amount-in', `amount-out',
+`currency'. Eg:
+
+
+# use the 1st, 2nd and 4th CSV fields as the entry's date, description and amount,
+# and give the 7th and 8th fields meaningful names for later reference:
+#
+# CSV field:
+#      1     2            3 4       5 6 7          8
+# entry field:
+fields date, description, , amount, , , somefield, anotherfield
+
+
+File: hledger_csv.5.info,  Node: field assignment,  Next: conditional block,  Prev: field list,  Up: CSV RULES
+
+1.4 field assignment
+====================
+
+_`ENTRYFIELDNAME'_ _`FIELDVALUE'_
+
+   This sets a journal entry field (one of the standard names above) to
+the given text value, which can include CSV field values interpolated by
+name (`%CSVFIELDNAME') or 1-based position (`%N'). Eg:
+
+
+# set the amount to the 4th CSV field with "USD " prepended
+amount USD %4
+
+
+# combine three fields to make a comment (containing two tags)
+comment note: %somefield - %anotherfield, date: %1
+
+   Field assignments can be used instead of or in addition to a field
+list.
+
+
+File: hledger_csv.5.info,  Node: conditional block,  Next: include,  Prev: field assignment,  Up: CSV RULES
+
+1.5 conditional block
+=====================
+
+`if' _`PATTERN'_
+_`FIELDASSIGNMENTS'_...
+
+   `if'
+_`PATTERN'_
+_`PATTERN'_...
+_`FIELDASSIGNMENTS'_...
+
+   This applies one or more field assignments, only to those CSV records
+matched by one of the PATTERNs. The patterns are case-insensitive
+regular expressions which match anywhere within the whole CSV record
+(it's not yet possible to match within a specific field). When there are
+multiple patterns they can be written on separate lines, unindented. The
+field assignments are on separate lines indented by at least one space.
+Examples:
+
+
+# if the CSV record contains "groceries", set account2 to "expenses:groceries"
+if groceries
+ account2 expenses:groceries
+
+
+# if the CSV record contains any of these patterns, set account2 and comment as shown
+if
+monthly service fee
+atm transaction fee
+banking thru software
+ account2 expenses:business:banking
+ comment  XXX deductible ? check it
+
+
+File: hledger_csv.5.info,  Node: include,  Prev: conditional block,  Up: CSV RULES
+
+1.6 include
+===========
+
+`include'_`RULESFILE'_
+
+   Include another rules file at this point. `RULESFILE' is either an
+absolute file path or a path relative to the current file's directory.
+Eg:
+
+
+# rules reused with several CSV files
+include common.rules
+
+
+File: hledger_csv.5.info,  Node: TIPS,  Prev: CSV RULES,  Up: Top
+
+2 TIPS
+******
+
+Each generated journal entry will have two postings, to `account1' and
+`account2' respectively. Currently it's not possible to generate
+entries with more than two postings.
+
+   If the CSV has debit/credit amounts in separate fields, assign to the
+`amount-in' and `amount-out' pseudo fields instead of `amount'.
+
+   If the CSV has the currency in a separate field, assign that to the
+`currency' pseudo field which will be automatically prepended to the
+amount. (Or you can do the same thing with a field assignment.)
+
+   If an amount value is parenthesised, it will be de-parenthesised and
+sign-flipped automatically.
+
+   The generated journal entries will be sorted by date. The original
+order of same-day entries will be preserved, usually.
+
+
+
+Tag Table:
+Node: Top90
+Node: CSV RULES795
+Ref: #csv-rules901
+Node: skip1144
+Ref: #skip1240
+Node: date-format1411
+Ref: #date-format1540
+Node: field list2049
+Ref: #field-list2188
+Node: field assignment2883
+Ref: #field-assignment3040
+Node: conditional block3545
+Ref: #conditional-block3701
+Node: include4588
+Ref: #include4699
+Node: TIPS4930
+Ref: #tips5014
+
+End Tag Table
diff --git a/doc/hledger_csv.5.txt b/doc/hledger_csv.5.txt
new file mode 100644
--- /dev/null
+++ b/doc/hledger_csv.5.txt
@@ -0,0 +1,169 @@
+
+hledger_csv(5)               hledger User Manuals               hledger_csv(5)
+
+
+
+NAME
+       CSV - how hledger reads CSV data, and the CSV rules file format
+
+DESCRIPTION
+       hledger  can  read CSV files, converting each CSV record into a journal
+       entry (transaction), if you provide some conversion hints in  a  "rules
+       file".   This file should be named like the CSV file with an additional
+       .rules suffix (eg: mybank.csv.rules); or, you can specify the file with
+       --rules-file PATH.   hledger  will  create  it  if necessary, with some
+       default rules which you'll need to adjust.  At minimum, the rules  file
+       must  specify  the  date and amount fields.  For an example, see How to
+       read CSV files.
+
+       To learn about exporting CSV, see CSV output.
+
+CSV RULES
+       The following six kinds of rule can appear in the rules  file,  in  any
+       order.  Blank lines and lines beginning with # or ; are ignored.
+
+   skip
+       skipN
+
+       Skip  this  number  of  CSV records at the beginning.  You'll need this
+       whenever your CSV data contains header lines.  Eg:
+
+              # ignore the first CSV line
+              skip 1
+
+   date-format
+       date-formatDATEFMT
+
+       When your CSV  date  fields  are  not  formatted  like  YYYY/MM/DD  (or
+       YYYY-MM-DD  or YYYY.MM.DD), you'll need to specify the format.  DATEFMT
+       is a strptime-like date parsing pattern,  which  must  parse  the  date
+       field values completely.  Examples:
+
+              # for dates like "6/11/2013":
+              date-format %-d/%-m/%Y
+
+              # for dates like "11/06/2013":
+              date-format %m/%d/%Y
+
+              # for dates like "2013-Nov-06":
+              date-format %Y-%h-%d
+
+              # for dates like "11/6/2013 11:32 PM":
+              date-format %-m/%-d/%Y %l:%M %p
+
+   field list
+       fieldsFIELDNAME1, FIELDNAME2...
+
+       This  (a)  names the CSV fields, in order (names may not contain white-
+       space; uninteresting names may be left blank), and (b) assigns them  to
+       journal  entry  fields  if  you  use any of these standard field names:
+       date, date2, status, code, description,  comment,  account1,  account2,
+       amount, amount-in, amount-out, currency.  Eg:
+
+              # use the 1st, 2nd and 4th CSV fields as the entry's date, description and amount,
+              # and give the 7th and 8th fields meaningful names for later reference:
+              #
+              # CSV field:
+              #      1     2            3 4       5 6 7          8
+              # entry field:
+              fields date, description, , amount, , , somefield, anotherfield
+
+   field assignment
+       ENTRYFIELDNAME FIELDVALUE
+
+       This  sets  a  journal entry field (one of the standard names above) to
+       the given text value, which can include CSV field  values  interpolated
+       by name (%CSVFIELDNAME) or 1-based position (%N).
+        Eg:
+
+              # set the amount to the 4th CSV field with "USD " prepended
+              amount USD %4
+
+              # combine three fields to make a comment (containing two tags)
+              comment note: %somefield - %anotherfield, date: %1
+
+       Field  assignments  can  be  used  instead of or in addition to a field
+       list.
+
+   conditional block
+       if PATTERN
+           FIELDASSIGNMENTS...
+
+       if
+       PATTERN
+       PATTERN...
+           FIELDASSIGNMENTS...
+
+       This applies one or more field assignments, only to those  CSV  records
+       matched by one of the PATTERNs.  The patterns are case-insensitive reg-
+       ular expressions which match anywhere within the whole CSV record (it's
+       not  yet  possible  to  match within a specific field).  When there are
+       multiple patterns they can be written on  separate  lines,  unindented.
+       The  field  assignments  are on separate lines indented by at least one
+       space.  Examples:
+
+              # if the CSV record contains "groceries", set account2 to "expenses:groceries"
+              if groceries
+               account2 expenses:groceries
+
+              # if the CSV record contains any of these patterns, set account2 and comment as shown
+              if
+              monthly service fee
+              atm transaction fee
+              banking thru software
+               account2 expenses:business:banking
+               comment  XXX deductible ? check it
+
+   include
+       includeRULESFILE
+
+       Include another rules file at this point.  RULESFILE is either an abso-
+       lute file path or a path relative to the current file's directory.  Eg:
+
+              # rules reused with several CSV files
+              include common.rules
+
+TIPS
+       Each generated journal entry will have two postings,  to  account1  and
+       account2 respectively.  Currently it's not possible to generate entries
+       with more than two postings.
+
+       If the CSV has debit/credit amounts in separate fields, assign  to  the
+       amount-in and amount-out pseudo fields instead of amount.
+
+       If  the  CSV  has  the currency in a separate field, assign that to the
+       currency pseudo field which will  be  automatically  prepended  to  the
+       amount.  (Or you can do the same thing with a field assignment.)
+
+       If  an  amount  value is parenthesised, it will be de-parenthesised and
+       sign-flipped automatically.
+
+       The generated journal entries will be sorted  by  date.   The  original
+       order of same-day entries will be preserved, usually.
+
+
+
+REPORTING BUGS
+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel
+       or hledger mail list)
+
+
+AUTHORS
+       Simon Michael <simon@joyful.com> and contributors
+
+
+COPYRIGHT
+       Copyright (C) 2007-2016 Simon Michael.
+       Released under GNU GPL v3 or later.
+
+
+SEE ALSO
+       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)
+
+       http://hledger.org
+
+
+
+hledger 1.0                      October 2016                   hledger_csv(5)
diff --git a/doc/hledger_journal.5 b/doc/hledger_journal.5
new file mode 100644
--- /dev/null
+++ b/doc/hledger_journal.5
@@ -0,0 +1,926 @@
+.\"t
+
+.TH "hledger_journal" "5" "October 2016" "hledger 1.0" "hledger User Manuals"
+
+
+
+.SH NAME
+.PP
+Journal \- hledger\[aq]s default file format, representing a General
+Journal
+.SH DESCRIPTION
+.PP
+hledger\[aq]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 \f[C]\&.journal\f[], but that\[aq]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.
+.PP
+hledger\[aq]s journal format is a compatible subset, mostly, of
+ledger\[aq]s journal format, so hledger can work with compatible ledger
+journal files as well.
+It\[aq]s safe, and encouraged, to run both hledger and ledger on the
+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.
+.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/12/31\ *\ pay\ off\ \ \ \ \ \ \ \ \ \ \ \ ;\ <\-\ an\ optional\ *\ or\ !\ after\ the\ date\ means\ "cleared"\ (or\ anything\ you\ want)
+\ \ \ \ liabilities:debts\ \ \ \ \ $1
+\ \ \ \ assets:bank:checking
+\f[]
+.fi
+.SH FILE FORMAT
+.SS Transactions
+.PP
+Transactions are represented by journal entries.
+Each begins with a simple date in column 0, followed by three optional
+fields with spaces between them:
+.IP \[bu] 2
+a status flag, which can be empty or \f[C]!\f[] or \f[C]*\f[] (meaning
+"uncleared", "pending" and "cleared", or whatever you want)
+.IP \[bu] 2
+a transaction code (eg a check number),
+.IP \[bu] 2
+and/or a description
+.PP
+then some number of postings, of some amount to some account.
+Each posting is on its own line, consisting of:
+.IP \[bu] 2
+indentation of one or more spaces (or tabs)
+.IP \[bu] 2
+optionally, a \f[C]!\f[] or \f[C]*\f[] status flag followed by a space
+.IP \[bu] 2
+an account name, optionally containing single spaces
+.IP \[bu] 2
+optionally, two or more spaces or tabs followed by an amount
+.PP
+Usually there are two or more postings, though one or none is also
+possible.
+The posting amounts within a transaction must always balance, ie add up
+to 0.
+Optionally one amount can be left blank, in which case it will be
+inferred.
+.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.
+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: \f[C]2010/01/31\f[], \f[C]1/31\f[],
+\f[C]2010\-01\-31\f[], \f[C]2010.1.31\f[].
+.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.
+.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[] flag is specified
+(\f[C]\-\-aux\-date\f[] or \f[C]\-\-effective\f[] also work).
+.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.
+.IP
+.nf
+\f[C]
+2010/2/23=2/19\ movie\ ticket
+\ \ expenses:cinema\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $10
+\ \ assets:checking
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+$\ hledger\ register\ checking
+2010/02/23\ movie\ ticket\ \ \ \ \ \ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10\ \ \ \ \ \ \ \ \ $\-10
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+$\ hledger\ register\ checking\ \-\-date2
+2010/02/19\ movie\ ticket\ \ \ \ \ \ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10\ \ \ \ \ \ \ \ \ $\-10
+\f[]
+.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[] 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
+transaction, by adding a posting comment containing a tag (see below)
+like \f[C]date:DATE\f[].
+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:
+.IP
+.nf
+\f[C]
+2015/5/30
+\ \ \ \ expenses:food\ \ \ \ \ $10\ \ \ ;\ food\ purchased\ on\ saturday\ 5/30
+\ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ ;\ bank\ cleared\ it\ on\ monday,\ date:6/1
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ t.j\ register\ food
+2015/05/30\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ expenses:food\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $10\ \ \ \ \ \ \ \ \ \ \ $10
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ t.j\ register\ checking
+2015/06/01\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-10\ \ \ \ \ \ \ \ \ \ $\-10
+\f[]
+.fi
+.PP
+DATE should be a simple date; if the year is not specified it will use
+the year of the transaction\[aq]s date.
+You can set the secondary date similarly, with \f[C]date2:DATE2\f[].
+The \f[C]date:\f[] or \f[C]date2:\f[] tags must have a valid simple date
+value if they are present, eg a \f[C]date:\f[] tag with no value is not
+allowed.
+.PP
+Ledger\[aq]s earlier, more compact bracketed date syntax is also
+supported: \f[C][DATE]\f[], \f[C][DATE=DATE2]\f[] or \f[C][=DATE2]\f[].
+hledger will attempt to parse any square\-bracketed sequence of the
+\f[C]0123456789/\-.=\f[] characters in this way.
+With this syntax, DATE infers its year from the transaction and DATE2
+infers its year from DATE.
+.SS Account names
+.PP
+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: \f[C]assets\f[], \f[C]liabilities\f[],
+\f[C]income\f[], \f[C]expenses\f[], and \f[C]equity\f[].
+.PP
+Account names may contain single spaces, eg:
+\f[C]assets:accounts\ receivable\f[].
+Because of this, they must always be followed by \f[B]two or more
+spaces\f[] (or newline).
+.PP
+Account names can be aliased.
+.SS Amounts
+.PP
+After the account name, there is usually an amount.
+Important: between account name and amount, there must be \f[B]two or
+more spaces\f[].
+.PP
+Amounts consist of a number and (usually) a currency symbol or commodity
+name.
+Some examples:
+.PP
+\f[C]2.00001\f[]
+.PD 0
+.P
+.PD
+\f[C]$1\f[]
+.PD 0
+.P
+.PD
+\f[C]4000\ AAPL\f[]
+.PD 0
+.P
+.PD
+\f[C]3\ "green\ apples"\f[]
+.PD 0
+.P
+.PD
+\f[C]\-$1,000,000.00\f[]
+.PD 0
+.P
+.PD
+\f[C]INR\ 9,99,99,999.00\f[]
+.PD 0
+.P
+.PD
+\f[C]EUR\ \-2.000.000,00\f[]
+.PP
+As you can see, the amount format is somewhat flexible:
+.IP \[bu] 2
+amounts are a number (the "quantity") and optionally a currency
+symbol/commodity name (the "commodity").
+.IP \[bu] 2
+the commodity is a symbol, word, or double\-quoted phrase, on the left
+or right, with or without a separating space
+.IP \[bu] 2
+negative amounts with a commodity on the left can have the minus sign
+before or after it
+.IP \[bu] 2
+digit groups (thousands, or any other grouping) can be separated by
+commas (in which case period is used for decimal point) or periods (in
+which case comma is used for decimal point)
+.PP
+You can use any of these variations when recording data, but when
+hledger displays amounts, it will choose a consistent format for each
+commodity.
+(Except for price amounts, which are always formatted as written).
+The display format is chosen as follows:
+.IP \[bu] 2
+if there is a commodity directive specifying the format, that is used
+.IP \[bu] 2
+otherwise the format is inferred from the first posting amount in that
+commodity in the journal, and the precision (number of decimal places)
+will be the maximum from all posting amounts in that commmodity
+.IP \[bu] 2
+or if there are no such amounts in the journal, a default format is used
+(like \f[C]$1000.00\f[]).
+.PP
+Price amounts and amounts in D directives usually don\[aq]t affect
+amount format inference, but in some situations 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, set
+the desired format with a commodity directive.
+.SS Virtual Postings
+.PP
+When you parenthesise the account name in a posting, we call that a
+\f[I]virtual posting\f[], 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[] flag is used,
+or the \f[C]real:1\f[] query.
+.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[] account:
+.IP
+.nf
+\f[C]
+1/1\ special\ unbalanced\ posting\ to\ set\ initial\ balance
+\ \ (assets:checking)\ \ \ $1000
+\f[]
+.fi
+.PP
+When the account name is bracketed, we call it a \f[I]balanced virtual
+posting\f[].
+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[] or
+\f[C]real:1\f[].
+.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
+\f[]
+.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.
+.SS Balance Assertions
+.PP
+hledger supports ledger\-style balance assertions in journal files.
+These look like \f[C]=EXPECTEDBALANCE\f[] following a posting\[aq]s
+amount.
+Eg in this example we assert the expected dollar balance in accounts a
+and b after each posting:
+.IP
+.nf
+\f[C]
+2013/1/1
+\ \ a\ \ \ $1\ \ =$1
+\ \ b\ \ \ \ \ \ \ =$\-1
+
+2013/1/2
+\ \ a\ \ \ $1\ \ =$2
+\ \ b\ \ $\-1\ \ =$\-2
+\f[]
+.fi
+.PP
+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
+\f[C]\-\-ignore\-assertions\f[] flag, which can be useful for
+troubleshooting or for reading Ledger files.
+.SS Assertions and ordering
+.PP
+hledger sorts an account\[aq]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.)
+.PP
+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.
+.PP
+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\[aq]s
+balance on the same day, you\[aq]ll have to put the assertion in the
+right file.
+.SS Assertions and commodities
+.PP
+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.
+We could call this a partial balance assertion.
+This is compatible with Ledger, and makes it possible to make assertions
+about accounts containing multiple commodities.
+.PP
+To assert each commodity\[aq]s balance in such a multi\-commodity
+account, you can add multiple postings (with amount 0 if necessary).
+But note that no matter how many assertions you add, you can\[aq]t be
+sure the account does not contain some unexpected commodity.
+(We\[aq]ll add support for this kind of total balance assertion if
+there\[aq]s demand.)
+.SS Assertions and subaccounts
+.PP
+Balance assertions do not count the balance from subaccounts; they check
+the posted account\[aq]s exclusive balance.
+For example:
+.IP
+.nf
+\f[C]
+1/1
+\ \ checking:fund\ \ \ 1\ =\ 1\ \ ;\ post\ to\ this\ subaccount,\ its\ balance\ is\ now\ 1
+\ \ checking\ \ \ \ \ \ \ \ 1\ =\ 1\ \ ;\ post\ to\ the\ parent\ account,\ its\ exclusive\ balance\ is\ now\ 1
+\ \ equity
+\f[]
+.fi
+.PP
+The balance report\[aq]s flat mode shows these exclusive balances more
+clearly:
+.IP
+.nf
+\f[C]
+$\ hledger\ bal\ checking\ \-\-flat
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 1\ \ checking
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 1\ \ checking:fund
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 2
+\f[]
+.fi
+.SS Assertions and virtual postings
+.PP
+Balance assertions are checked against all postings, both real and
+virtual.
+They are not affected by the \f[C]\-\-real/\-R\f[] flag or
+\f[C]real:\f[] query.
+.SS Prices
+.SS Transaction prices
+.PP
+When recording a transaction, you can also record an amount\[aq]s price
+in another commodity.
+This documents the exchange rate, cost (of a purchase), or selling price
+(of a sale) that was in effect within this particular transaction (or
+more precisely, within the particular posting).
+These transaction prices are fixed, and do not change.
+.PP
+Such priced amounts can be displayed in their transaction price\[aq]s
+commodity, by using the \f[C]\-\-cost/\-B\f[] flag (B for "cost Basis"),
+supported by most hledger commands.
+.PP
+There are three ways to specify a transaction price:
+.IP "1." 3
+Write the unit price (aka exchange rate), as \f[C]\@\ UNITPRICE\f[]
+after the amount:
+.RS 4
+.IP
+.nf
+\f[C]
+2009/1/1
+\ \ assets:foreign\ currency\ \ \ €100\ \@\ $1.35\ \ ;\ one\ hundred\ euros\ at\ $1.35\ each
+\ \ assets:cash
+\f[]
+.fi
+.RE
+.IP "2." 3
+Or write the total price, as \f[C]\@\@\ TOTALPRICE\f[] after the amount:
+.RS 4
+.IP
+.nf
+\f[C]
+2009/1/1
+\ \ assets:foreign\ currency\ \ \ €100\ \@\@\ $135\ \ ;\ one\ hundred\ euros\ at\ $135\ for\ the\ lot
+\ \ assets:cash
+\f[]
+.fi
+.RE
+.IP "3." 3
+Or let hledger infer the price so as to balance the transaction.
+To permit this, you must fully specify all posting amounts, and their
+sum must have a non\-zero amount in exactly two commodities:
+.RS 4
+.IP
+.nf
+\f[C]
+2009/1/1
+\ \ assets:foreign\ currency\ \ \ €100\ \ \ \ \ \ \ \ \ \ ;\ one\ hundred\ euros
+\ \ assets:cash\ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-135\ \ \ \ \ \ \ \ \ \ ;\ exchanged\ for\ $135
+\f[]
+.fi
+.RE
+.PP
+With any of the above examples we get:
+.IP
+.nf
+\f[C]
+$\ hledger\ print\ \-B
+2009/01/01
+\ \ \ \ assets:foreign\ currency\ \ \ \ \ \ \ $135.00
+\ \ \ \ assets:cash\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $\-135.00
+\f[]
+.fi
+.PP
+Example use for transaction prices: recording the effective conversion
+rate of purchases made in a foreign currency.
+.SS Market prices
+.PP
+Market prices are not tied to a particular transaction; they represent
+historical exchange rates between two commodities, usually from some
+public market which publishes such rates.
+.PP
+When market prices are known, the \f[C]\-V/\-\-value\f[] option will use
+them to convert reported amounts to their market value as of the report
+end date.
+This option is currently available only with the balance command.
+.PP
+You record market prices (Ledger calls them historical prices) with a P
+directive, in the journal or perhaps in a separate included file.
+Market price directives have the format:
+.IP
+.nf
+\f[C]
+P\ DATE\ COMMODITYSYMBOL\ UNITPRICE
+\f[]
+.fi
+.PP
+For example, the following directives say that the euro\[aq]s exchange
+rate was 1.35 US dollars during 2009, and $1.40 from 2010 onward (and
+unknown before 2009).
+.IP
+.nf
+\f[C]
+P\ 2009/1/1\ €\ $1.35
+P\ 2010/1/1\ €\ $1.40
+\f[]
+.fi
+.PP
+Example use for market prices: tracking the value of stocks.
+.SS Comments
+.PP
+Lines in the journal beginning with a semicolon (\f[C];\f[]) or hash
+(\f[C]#\f[]) or asterisk (\f[C]*\f[]) are comments, and will be ignored.
+(Asterisk comments make it easy to treat your journal like an org\-mode
+outline in emacs.)
+.PP
+Also, anything between \f[C]comment\f[] and \f[C]end\ comment\f[]
+directives is a (multi\-line) comment.
+If there is no \f[C]end\ comment\f[], the comment extends to the end of
+the file.
+.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.
+.PP
+Some examples:
+.IP
+.nf
+\f[C]
+#\ a\ journal\ comment
+
+;\ also\ a\ journal\ comment
+
+comment
+This\ is\ a\ multiline\ comment,
+which\ continues\ until\ a\ line
+where\ the\ "end\ comment"\ string
+appears\ on\ its\ own.
+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\ journal\ comment\ (because\ not\ indented)
+\f[]
+.fi
+.SS Tags
+.PP
+A \f[I]tag\f[] is a word followed by a full colon inside a transaction
+or posting comment.
+You can write multiple tags, comma separated.
+Eg: \f[C];\ a\ comment\ containing\ sometag:,\ anothertag:\f[].
+You can search for tags with the \f[C]tag:\f[] query.
+.PP
+A tag can also have a value, which is any text between the colon and the
+next comma or newline, excluding leading/trailing whitespace.
+(So hledger tag values can not contain commas or newlines).
+.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 (A, TAG2,
+third\-tag) and the posting has four (A, TAG2, third\-tag,
+posting\-tag):
+.IP
+.nf
+\f[C]
+1/1\ a\ transaction\ \ ;\ A:,\ TAG2:
+\ \ \ \ ;\ third\-tag:\ a\ third\ transaction\ tag,\ this\ time\ with\ a\ value
+\ \ \ \ (a)\ \ $1\ \ ;\ posting\-tag:
+\f[]
+.fi
+.PP
+Tags are like Ledger\[aq]s metadata feature, except hledger\[aq]s tag
+values are simple strings.
+.SS Directives
+.SS Account aliases
+.PP
+You can define aliases which rewrite your account names (after reading
+the journal, before generating reports).
+hledger\[aq]s account aliases can be useful for:
+.IP \[bu] 2
+expanding shorthand account names to their full form, allowing easier
+data entry and a less verbose journal
+.IP \[bu] 2
+adapting old journals to your current chart of accounts
+.IP \[bu] 2
+experimenting with new account organisations, like a new hierarchy or
+combining two accounts into one
+.IP \[bu] 2
+customising reports
+.PP
+See also How to use account aliases.
+.SS Basic aliases
+.PP
+To set an account alias, use the \f[C]alias\f[] 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:
+.IP
+.nf
+\f[C]
+alias\ OLD\ =\ NEW
+\f[]
+.fi
+.PP
+Or, you can use the \f[C]\-\-alias\ \[aq]OLD=NEW\[aq]\f[] option on the
+command line.
+This affects all entries.
+It\[aq]s useful for trying out aliases interactively.
+.PP
+OLD and NEW are full account names.
+hledger will replace any occurrence of the old account name with the new
+one.
+Subaccounts are also affected.
+Eg:
+.IP
+.nf
+\f[C]
+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"
+\f[]
+.fi
+.SS Regex aliases
+.PP
+There is also a more powerful variant that uses a regular expression,
+indicated by the forward slashes.
+(This was the default behaviour in hledger 0.24\-0.25):
+.IP
+.nf
+\f[C]
+alias\ /REGEX/\ =\ REPLACEMENT
+\f[]
+.fi
+.PP
+or \f[C]\-\-alias\ \[aq]/REGEX/=REPLACEMENT\[aq]\f[].
+.PP
+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.
+Note, currently regular expression aliases may cause noticeable
+slow\-downs.
+(And if you use Ledger on your hledger file, they will be ignored.) Eg:
+.IP
+.nf
+\f[C]
+alias\ /^(.+):bank:([^:]+)(.*)/\ =\ \\1:\\2\ \\3
+#\ rewrites\ "assets:bank:wells\ fargo:checking"\ to\ \ "assets:wells\ fargo\ checking"
+\f[]
+.fi
+.SS Multiple aliases
+.PP
+You can define as many aliases as you like using directives or
+command\-line options.
+Aliases are recursive \- each alias sees the result of applying previous
+ones.
+(This is different from Ledger, where aliases are non\-recursive by
+default).
+Aliases are applied in the following order:
+.IP "1." 3
+alias directives, most recently seen first (recent directives take
+precedence over earlier ones; directives not yet seen are ignored)
+.IP "2." 3
+alias options, in the order they appear on the command line
+.SS end aliases
+.PP
+You can clear (forget) all currently defined aliases with the
+\f[C]end\ aliases\f[] directive:
+.IP
+.nf
+\f[C]
+end\ aliases
+\f[]
+.fi
+.SS account directive
+.PP
+The \f[C]account\f[] directive predefines account names, as in Ledger
+and Beancount.
+This may be useful for your own documentation; hledger doesn\[aq]t make
+use of it yet.
+.IP
+.nf
+\f[C]
+;\ account\ ACCT
+;\ \ \ OPTIONAL\ COMMENTS/TAGS...
+
+account\ assets:bank:checking
+\ a\ comment
+\ acct\-no:12345
+
+account\ expenses:food
+
+;\ etc.
+\f[]
+.fi
+.SS apply account directive
+.PP
+You can specify a parent account which will be prepended to all accounts
+within a section of the journal.
+Use the \f[C]apply\ account\f[] and \f[C]end\ apply\ account\f[]
+directives like so:
+.IP
+.nf
+\f[C]
+apply\ account\ home
+
+2010/1/1
+\ \ \ \ food\ \ \ \ $10
+\ \ \ \ cash
+
+end\ apply\ account
+\f[]
+.fi
+.PP
+which is equivalent to:
+.IP
+.nf
+\f[C]
+2010/01/01
+\ \ \ \ home:food\ \ \ \ \ \ \ \ \ \ \ $10
+\ \ \ \ home:cash\ \ \ \ \ \ \ \ \ \ $\-10
+\f[]
+.fi
+.PP
+If \f[C]end\ apply\ account\f[] is omitted, the effect lasts to the end
+of the file.
+Included files are also affected, eg:
+.IP
+.nf
+\f[C]
+apply\ account\ business
+include\ biz.journal
+end\ apply\ account
+apply\ account\ personal
+include\ personal.journal
+\f[]
+.fi
+.PP
+Prior to hledger 1.0, legacy \f[C]account\f[] and \f[C]end\f[] spellings
+were also supported.
+.SS Multi\-line comments
+.PP
+A line containing just \f[C]comment\f[] starts a multi\-line comment,
+and a line containing just \f[C]end\ comment\f[] ends it.
+See comments.
+.SS commodity directive
+.PP
+The \f[C]commodity\f[] directive predefines commodities (currently this
+is just informational), and also it may define the display format for
+amounts in this commodity (overriding the automatically inferred
+format).
+.PP
+It may be written on a single line, like this:
+.IP
+.nf
+\f[C]
+;\ 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
+\f[]
+.fi
+.PP
+or on multiple lines, using the "format" subdirective.
+In this case the commodity symbol appears twice and should be the same
+in both places:
+.IP
+.nf
+\f[C]
+;\ 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\ 9,99,99,999.00
+\f[]
+.fi
+.SS Default commodity
+.PP
+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\[aq]s default commodity directive.) The
+commodity and display format will be applied to all subsequent
+commodity\-less amounts, or until the next D directive.
+.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)
+D\ $1,000.00
+
+1/1
+\ \ a\ \ \ \ \ 5\ \ \ \ #\ <\-\ commodity\-less\ amount,\ becomes\ $1
+\ \ b
+\f[]
+.fi
+.SS Default year
+.PP
+You can set a default year to be used for subsequent dates which
+don\[aq]t specify a year.
+This is a line beginning with \f[C]Y\f[] followed by the year.
+Eg:
+.IP
+.nf
+\f[C]
+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
+\f[]
+.fi
+.SS Including other files
+.PP
+You can pull in the content of additional journal files by writing an
+include directive, like this:
+.IP
+.nf
+\f[C]
+include\ path/to/file.journal
+\f[]
+.fi
+.PP
+If the path does not begin with a slash, it is relative to the current
+file.
+Glob patterns (\f[C]*\f[]) are not currently supported.
+.PP
+The \f[C]include\f[] directive can only be used in journal files.
+It can include journal, timeclock or timedot files, but not CSV files.
+.SH EDITOR SUPPORT
+.PP
+Add\-on modes exist for various text editors, to make working with
+journal files easier.
+They add colour, navigation aids and helpful commands.
+For hledger users who edit the journal file directly (the majority),
+using one of these modes is quite recommended.
+.PP
+These were written with Ledger in mind, but also work with hledger
+files:
+.PP
+.TS
+tab(@);
+lw(16.5n) lw(51.5n).
+T{
+Emacs
+T}@T{
+http://www.ledger\-cli.org/3.0/doc/ledger\-mode.html
+T}
+T{
+Vim
+T}@T{
+https://github.com/ledger/ledger/wiki/Getting\-started
+T}
+T{
+Sublime Text
+T}@T{
+https://github.com/ledger/ledger/wiki/Using\-Sublime\-Text
+T}
+T{
+Textmate
+T}@T{
+https://github.com/ledger/ledger/wiki/Using\-TextMate\-2
+T}
+T{
+Text Wrangler \ 
+T}@T{
+https://github.com/ledger/ledger/wiki/Editing\-Ledger\-files\-with\-TextWrangler
+T}
+.TE
+
+
+.SH "REPORTING BUGS"
+Report bugs at http://bugs.hledger.org
+(or on the #hledger IRC channel or hledger mail list)
+
+.SH AUTHORS
+Simon Michael <simon@joyful.com> and contributors
+
+.SH COPYRIGHT
+
+Copyright (C) 2007-2016 Simon Michael.
+.br
+Released under GNU GPL v3 or later.
+
+.SH SEE ALSO
+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),
+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),
+ledger(1)
+
+http://hledger.org
diff --git a/doc/hledger_journal.5.info b/doc/hledger_journal.5.info
new file mode 100644
--- /dev/null
+++ b/doc/hledger_journal.5.info
@@ -0,0 +1,995 @@
+This is hledger-lib/doc/hledger_journal.5.info, produced by makeinfo
+version 4.8 from stdin.
+
+
+File: hledger_journal.5.info,  Node: Top,  Up: (dir)
+
+hledger_journal(5) hledger 1.0
+******************************
+
+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/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.5.info,  Node: FILE FORMAT,  Next: EDITOR SUPPORT,  Prev: Top,  Up: Top
+
+1 FILE FORMAT
+*************
+
+* Menu:
+
+* Transactions::
+* Dates::
+* Account names::
+* Amounts::
+* Virtual Postings::
+* Balance Assertions::
+* Prices::
+* Comments::
+* Tags::
+* Directives::
+
+
+File: hledger_journal.5.info,  Node: Transactions,  Next: Dates,  Up: FILE FORMAT
+
+1.1 Transactions
+================
+
+Transactions are represented by journal entries. Each begins with a
+simple date in column 0, followed by three optional fields with spaces
+between them:
+
+   * a status flag, which can be empty or `!' or `*' (meaning
+     "uncleared", "pending" and "cleared", or whatever you want)
+
+   * a transaction code (eg a check number),
+
+   * and/or a description
+
+   then some number of postings, of some amount to some account. Each
+posting is on its own line, consisting of:
+
+   * indentation of one or more spaces (or tabs)
+
+   * optionally, a `!' or `*' status flag followed by a space
+
+   * an account name, optionally containing single spaces
+
+   * optionally, two or more spaces or tabs followed by an amount
+
+   Usually there are two or more postings, though one or none is also
+possible. The posting amounts within a transaction must always balance,
+ie add up to 0. Optionally one amount can be left blank, in which case
+it will be inferred.
+
+
+File: hledger_journal.5.info,  Node: Dates,  Next: Account names,  Prev: Transactions,  Up: FILE FORMAT
+
+1.2 Dates
+=========
+
+* Menu:
+
+* Simple dates::
+* Secondary dates::
+* Posting dates::
+
+
+File: hledger_journal.5.info,  Node: Simple dates,  Next: Secondary dates,  Up: Dates
+
+1.2.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.5.info,  Node: Secondary dates,  Next: Posting dates,  Prev: Simple dates,  Up: Dates
+
+1.2.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.5.info,  Node: Posting dates,  Prev: Secondary dates,  Up: Dates
+
+1.2.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.5.info,  Node: Account names,  Next: Amounts,  Prev: Dates,  Up: FILE FORMAT
+
+1.3 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.5.info,  Node: Amounts,  Next: Virtual Postings,  Prev: Account names,  Up: FILE FORMAT
+
+1.4 Amounts
+===========
+
+After the account name, there is usually an amount. Important: between
+account name and amount, there must be *two or more spaces*.
+
+   Amounts consist of a number and (usually) a currency symbol or
+commodity name. Some examples:
+
+   `2.00001'
+`$1'
+`4000 AAPL'
+`3 "green apples"'
+`-$1,000,000.00'
+`INR 9,99,99,999.00'
+`EUR -2.000.000,00'
+
+   As you can see, the amount format is somewhat flexible:
+
+   * amounts are a number (the "quantity") and optionally a currency
+     symbol/commodity name (the "commodity").
+
+   * the commodity is a symbol, word, or double-quoted phrase, on the
+     left or right, with or without a separating space
+
+   * negative amounts with a commodity on the left can have the minus
+     sign before or after it
+
+   * digit groups (thousands, or any other grouping) can be separated by
+     commas (in which case period is used for decimal point) or periods
+     (in which case comma is used for decimal point)
+
+   You can use any of these variations when recording data, but when
+hledger displays amounts, it will choose a consistent format for each
+commodity. (Except for price amounts, which are always formatted as
+written). The display format is chosen as follows:
+
+   * if there is a commodity directive specifying the format, that is
+     used
+
+   * otherwise the format is inferred from the first posting amount in
+     that commodity in the journal, and the precision (number of
+     decimal places) 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 usually don't affect amount
+format inference, but in some situations 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, set the desired
+format with a commodity directive.
+
+
+File: hledger_journal.5.info,  Node: Virtual Postings,  Next: Balance Assertions,  Prev: Amounts,  Up: FILE FORMAT
+
+1.5 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.5.info,  Node: Balance Assertions,  Next: Prices,  Prev: Virtual Postings,  Up: FILE FORMAT
+
+1.6 Balance Assertions
+======================
+
+hledger supports ledger-style balance assertions in journal files. These
+look like `=EXPECTEDBALANCE' following a posting's amount. Eg in this
+example 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
+`--ignore-assertions' flag, which can be useful for troubleshooting or
+for reading Ledger files.
+
+* Menu:
+
+* Assertions and ordering::
+* Assertions and commodities::
+* Assertions and subaccounts::
+* Assertions and virtual postings::
+
+
+File: hledger_journal.5.info,  Node: Assertions and ordering,  Next: Assertions and commodities,  Up: Balance Assertions
+
+1.6.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.
+
+   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.5.info,  Node: Assertions and commodities,  Next: Assertions and subaccounts,  Prev: Assertions and ordering,  Up: Balance Assertions
+
+1.6.2 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. We could call this a partial
+balance assertion. This is compatible with Ledger, and makes it possible
+to make assertions about accounts containing multiple commodities.
+
+   To assert each commodity's balance in such a multi-commodity account,
+you can add multiple postings (with amount 0 if necessary). But note
+that no matter how many assertions you add, you can't be sure the
+account does not contain some unexpected commodity. (We'll add support
+for this kind of total balance assertion if there's demand.)
+
+
+File: hledger_journal.5.info,  Node: Assertions and subaccounts,  Next: Assertions and virtual postings,  Prev: Assertions and commodities,  Up: Balance Assertions
+
+1.6.3 Assertions and subaccounts
+--------------------------------
+
+Balance assertions do not count the balance from subaccounts; they check
+the posted account's exclusive balance. For example:
+
+
+1/1
+  checking:fund   1 = 1  ; post to this subaccount, its balance is now 1
+  checking        1 = 1  ; post to the parent account, its exclusive balance is now 1
+  equity
+
+   The balance report's flat mode shows these exclusive balances more
+clearly:
+
+
+$ hledger bal checking --flat
+                   1  checking
+                   1  checking:fund
+--------------------
+                   2
+
+
+File: hledger_journal.5.info,  Node: Assertions and virtual postings,  Prev: Assertions and subaccounts,  Up: Balance Assertions
+
+1.6.4 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.5.info,  Node: Prices,  Next: Comments,  Prev: Balance Assertions,  Up: FILE FORMAT
+
+1.7 Prices
+==========
+
+* Menu:
+
+* Transaction prices::
+* Market prices::
+
+
+File: hledger_journal.5.info,  Node: Transaction prices,  Next: Market prices,  Up: Prices
+
+1.7.1 Transaction prices
+------------------------
+
+When recording a transaction, you can also record an amount's price in
+another commodity. This documents the exchange rate, cost (of a
+purchase), or selling price (of a sale) that was in effect within this
+particular transaction (or more precisely, within the particular
+posting). These transaction prices are fixed, and do not change.
+
+   Such priced amounts can be displayed in their transaction price's
+commodity, by using the `--cost/-B' flag (B for "cost Basis"),
+supported by most hledger commands.
+
+   There are three ways to specify a transaction price:
+
+  1. Write the unit price (aka exchange rate), as `@ UNITPRICE' after
+     the amount:
+
+
+     2009/1/1
+       assets:foreign currency   €100 @ $1.35  ; one hundred euros at $1.35 each
+       assets:cash
+
+  2. Or write the total price, as `@@ TOTALPRICE' after the amount:
+
+
+     2009/1/1
+       assets:foreign currency   €100 @@ $135  ; one hundred euros at $135 for the lot
+       assets:cash
+
+  3. Or let hledger infer the price so as to balance the transaction. To
+     permit this, you must fully specify all posting amounts, and their
+     sum must have a non-zero amount in exactly two commodities:
+
+
+     2009/1/1
+       assets:foreign currency   €100          ; one hundred euros
+       assets:cash              $-135          ; exchanged for $135
+
+
+   With any of the above examples we get:
+
+
+$ hledger print -B
+2009/01/01
+    assets:foreign currency       $135.00
+    assets:cash                  $-135.00
+
+   Example use for transaction prices: recording the effective
+conversion rate of purchases made in a foreign currency.
+
+
+File: hledger_journal.5.info,  Node: Market prices,  Prev: Transaction prices,  Up: Prices
+
+1.7.2 Market prices
+-------------------
+
+Market prices are not tied to a particular transaction; they represent
+historical exchange rates between two commodities, usually from some
+public market which publishes such rates.
+
+   When market prices are known, the `-V/--value' option will use them
+to convert reported amounts to their market value as of the report end
+date. This option is currently available only with the balance command.
+
+   You record market prices (Ledger calls them historical prices) with
+a P directive, in the journal or perhaps in a separate included file.
+Market price directives have the format:
+
+
+P DATE COMMODITYSYMBOL UNITPRICE
+
+   For example, the following directives say that the euro's exchange
+rate was 1.35 US dollars during 2009, and $1.40 from 2010 onward (and
+unknown before 2009).
+
+
+P 2009/1/1 € $1.35
+P 2010/1/1 € $1.40
+
+   Example use for market prices: tracking the value of stocks.
+
+
+File: hledger_journal.5.info,  Node: Comments,  Next: Tags,  Prev: Prices,  Up: FILE FORMAT
+
+1.8 Comments
+============
+
+Lines in the journal beginning with a semicolon (`;') or hash (`#') or
+asterisk (`*') are comments, and will be ignored.  (Asterisk comments
+make it easy to treat your journal like an org-mode outline in emacs.)
+
+   Also, anything between `comment' and `end comment' directives is a
+(multi-line) comment. If there is no `end comment', the comment extends
+to the end of the file.
+
+   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.
+
+   Some examples:
+
+
+# a journal comment
+
+; also a journal comment
+
+comment
+This is a multiline comment,
+which continues until a line
+where the "end comment" string
+appears on its own.
+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 journal comment (because not indented)
+
+
+File: hledger_journal.5.info,  Node: Tags,  Next: Directives,  Prev: Comments,  Up: FILE FORMAT
+
+1.9 Tags
+========
+
+A _tag_ is a word followed by a full colon inside a transaction or
+posting comment. You can write multiple tags, comma separated. Eg: `; a
+comment containing sometag:, anothertag:'. You can search for tags with
+the `tag:' query.
+
+   A tag can also have a value, which is any text between the colon and
+the next comma or newline, excluding leading/trailing whitespace. (So
+hledger tag values can not contain commas or newlines).
+
+   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 (A, TAG2, third-tag, posting-tag):
+
+
+1/1 a transaction  ; A:, TAG2:
+    ; third-tag: a third transaction tag, this time 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.5.info,  Node: Directives,  Prev: Tags,  Up: FILE FORMAT
+
+1.10 Directives
+===============
+
+* Menu:
+
+* Account aliases::
+* account directive::
+* apply account directive::
+* Multi-line comments::
+* commodity directive::
+* Default commodity::
+* Default year::
+* Including other files::
+
+
+File: hledger_journal.5.info,  Node: Account aliases,  Next: account directive,  Up: Directives
+
+1.10.1 Account aliases
+----------------------
+
+You can define aliases which rewrite your account names (after reading
+the journal, before generating reports). hledger's account aliases 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
+
+   See also How to use account aliases.
+
+* Menu:
+
+* Basic aliases::
+* Regex aliases::
+* Multiple aliases::
+* end aliases::
+
+
+File: hledger_journal.5.info,  Node: Basic aliases,  Next: Regex aliases,  Up: Account aliases
+
+1.10.1.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 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.5.info,  Node: Regex aliases,  Next: Multiple aliases,  Prev: Basic aliases,  Up: Account aliases
+
+1.10.1.2 Regex aliases
+......................
+
+There is also a more powerful variant that uses a regular expression,
+indicated by the forward slashes. (This was the default behaviour in
+hledger 0.24-0.25):
+
+
+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. Note,
+currently regular expression aliases may cause noticeable slow-downs.
+(And if you use Ledger on your hledger file, they will be ignored.) Eg:
+
+
+alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3
+# rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking"
+
+
+File: hledger_journal.5.info,  Node: Multiple aliases,  Next: end aliases,  Prev: Regex aliases,  Up: Account aliases
+
+1.10.1.3 Multiple aliases
+.........................
+
+You can define as many aliases as you like using directives or
+command-line options. Aliases are recursive - each alias sees the result
+of applying previous ones. (This is different from Ledger, where aliases
+are non-recursive by default). Aliases are applied in the following
+order:
+
+  1. alias directives, most recently seen first (recent directives take
+     precedence over earlier ones; directives not yet seen are ignored)
+
+  2. alias options, in the order they appear on the command line
+
+
+File: hledger_journal.5.info,  Node: end aliases,  Prev: Multiple aliases,  Up: Account aliases
+
+1.10.1.4 end aliases
+....................
+
+You can clear (forget) all currently defined aliases with the `end
+aliases' directive:
+
+
+end aliases
+
+
+File: hledger_journal.5.info,  Node: account directive,  Next: apply account directive,  Prev: Account aliases,  Up: Directives
+
+1.10.2 account directive
+------------------------
+
+The `account' directive predefines account names, as in Ledger and
+Beancount. This may be useful for your own documentation; hledger
+doesn't make use of it yet.
+
+
+; account ACCT
+;   OPTIONAL COMMENTS/TAGS...
+
+account assets:bank:checking
+ a comment
+ acct-no:12345
+
+account expenses:food
+
+; etc.
+
+
+File: hledger_journal.5.info,  Node: apply account directive,  Next: Multi-line comments,  Prev: account directive,  Up: Directives
+
+1.10.3 apply account directive
+------------------------------
+
+You can specify a parent account which will be prepended to all accounts
+within a section of the journal. Use the `apply account' and `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.
+
+
+File: hledger_journal.5.info,  Node: Multi-line comments,  Next: commodity directive,  Prev: apply account directive,  Up: Directives
+
+1.10.4 Multi-line comments
+--------------------------
+
+A line containing just `comment' starts a multi-line comment, and a
+line containing just `end comment' ends it. See comments.
+
+
+File: hledger_journal.5.info,  Node: commodity directive,  Next: Default commodity,  Prev: Multi-line comments,  Up: Directives
+
+1.10.5 commodity directive
+--------------------------
+
+The `commodity' directive predefines commodities (currently this is
+just informational), and also it may define the display format for
+amounts in this commodity (overriding the automatically inferred
+format).
+
+   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 9,99,99,999.00
+
+
+File: hledger_journal.5.info,  Node: Default commodity,  Next: Default year,  Prev: commodity directive,  Up: Directives
+
+1.10.6 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
+
+
+File: hledger_journal.5.info,  Node: Default year,  Next: Including other files,  Prev: Default commodity,  Up: Directives
+
+1.10.7 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.5.info,  Node: Including other files,  Prev: Default year,  Up: Directives
+
+1.10.8 Including other files
+----------------------------
+
+You can pull in the content of additional journal 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. Glob patterns (`*') are not currently supported.
+
+   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.5.info,  Node: EDITOR SUPPORT,  Prev: FILE FORMAT,  Up: Top
+
+2 EDITOR SUPPORT
+****************
+
+Add-on modes exist for various text editors, to make working with
+journal files easier. They add colour, navigation aids and helpful
+commands. For hledger users who edit the journal file directly (the
+majority), using one of these modes is quite recommended.
+
+   These were written with Ledger in mind, but also work with hledger
+files:
+
+Emacs             http://www.ledger-cli.org/3.0/doc/ledger-mode.html
+Vim               https://github.com/ledger/ledger/wiki/Getting-started
+Sublime Text      https://github.com/ledger/ledger/wiki/Using-Sublime-Text
+Textmate          https://github.com/ledger/ledger/wiki/Using-TextMate-2
+Text Wrangler     https://github.com/ledger/ledger/wiki/Editing-Ledger-files-with-TextWrangler
+
+
+
+Tag Table:
+Node: Top94
+Node: FILE FORMAT2284
+Ref: #file-format2410
+Node: Transactions2569
+Ref: #transactions2689
+Node: Dates3632
+Ref: #dates3760
+Node: Simple dates3825
+Ref: #simple-dates3953
+Node: Secondary dates4317
+Ref: #secondary-dates4473
+Node: Posting dates6033
+Ref: #posting-dates6164
+Node: Account names7535
+Ref: #account-names7674
+Node: Amounts8159
+Ref: #amounts8297
+Node: Virtual Postings10295
+Ref: #virtual-postings10456
+Node: Balance Assertions11676
+Ref: #balance-assertions11840
+Node: Assertions and ordering12662
+Ref: #assertions-and-ordering12847
+Node: Assertions and commodities13878
+Ref: #assertions-and-commodities14104
+Node: Assertions and subaccounts14796
+Ref: #assertions-and-subaccounts15030
+Node: Assertions and virtual postings15552
+Ref: #assertions-and-virtual-postings15761
+Node: Prices15902
+Ref: #prices16034
+Node: Transaction prices16085
+Ref: #transaction-prices16230
+Node: Market prices17837
+Ref: #market-prices17972
+Node: Comments18860
+Ref: #comments18982
+Node: Tags20094
+Ref: #tags20212
+Node: Directives21135
+Ref: #directives21250
+Node: Account aliases21443
+Ref: #account-aliases21589
+Node: Basic aliases22191
+Ref: #basic-aliases22336
+Node: Regex aliases23024
+Ref: #regex-aliases23194
+Node: Multiple aliases23964
+Ref: #multiple-aliases24138
+Node: end aliases24634
+Ref: #end-aliases24776
+Node: account directive24878
+Ref: #account-directive25060
+Node: apply account directive25356
+Ref: #apply-account-directive25554
+Node: Multi-line comments26214
+Ref: #multi-line-comments26406
+Node: commodity directive26533
+Ref: #commodity-directive26719
+Node: Default commodity27592
+Ref: #default-commodity27767
+Node: Default year28303
+Ref: #default-year28470
+Node: Including other files28893
+Ref: #including-other-files29052
+Node: EDITOR SUPPORT29448
+Ref: #editor-support29568
+
+End Tag Table
diff --git a/doc/hledger_journal.5.txt b/doc/hledger_journal.5.txt
new file mode 100644
--- /dev/null
+++ b/doc/hledger_journal.5.txt
@@ -0,0 +1,700 @@
+
+hledger_journal(5)           hledger User Manuals           hledger_journal(5)
+
+
+
+NAME
+       Journal - hledger's default file format, representing a General Journal
+
+DESCRIPTION
+       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 get-
+       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
+       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/12/31 * pay off            ; <- an optional * or ! after the date means "cleared" (or anything you want)
+                  liabilities:debts     $1
+                  assets:bank:checking
+
+FILE FORMAT
+   Transactions
+       Transactions  are  represented  by journal entries.  Each begins with a
+       simple date in column 0, followed by three optional fields with  spaces
+       between them:
+
+       o a  status  flag,  which  can be empty or ! or * (meaning "uncleared",
+         "pending" and "cleared", or whatever you want)
+
+       o a transaction code (eg a check number),
+
+       o and/or a description
+
+       then some number of postings, of some amount  to  some  account.   Each
+       posting is on its own line, consisting of:
+
+       o indentation of one or more spaces (or tabs)
+
+       o optionally, a ! or * status flag followed by a space
+
+       o an account name, optionally containing single spaces
+
+       o optionally, two or more spaces or tabs followed by an amount
+
+       Usually there are two or more postings, though one or none is also pos-
+       sible.  The posting amounts within a transaction must  always  balance,
+       ie add up to 0.  Optionally one amount can be left blank, in which case
+       it will be inferred.
+
+   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
+       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.
+
+   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  sec-
+       ondary  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  speci-
+       fied (--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 compat-
+       ibility,  but  posting  dates  are  a  more powerful and less confusing
+       alternative.
+
+   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.
+
+   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 receiv-
+       able.  Because of this, they must always be followed  by  two  or  more
+       spaces (or newline).
+
+       Account names can be aliased.
+
+   Amounts
+       After the account name, there is usually an amount.  Important: between
+       account name and amount, there must be two or more spaces.
+
+       Amounts consist of a number and (usually) a currency symbol or  commod-
+       ity name.  Some examples:
+
+       2.00001
+       $1
+       4000 AAPL
+       3 "green apples"
+       -$1,000,000.00
+       INR 9,99,99,999.00
+       EUR -2.000.000,00
+
+       As you can see, the amount format is somewhat flexible:
+
+       o amounts  are a number (the "quantity") and optionally a currency sym-
+         bol/commodity name (the "commodity").
+
+       o the commodity is a symbol, word, or double-quoted phrase, on the left
+         or right, with or without a separating space
+
+       o negative amounts with a commodity on the left can have the minus sign
+         before or after it
+
+       o digit groups (thousands, or any other grouping) can be  separated  by
+         commas  (in  which  case period is used for decimal point) or periods
+         (in which case comma is used for decimal point)
+
+       You can use any of these  variations  when  recording  data,  but  when
+       hledger  displays  amounts, it will choose a consistent format for each
+       commodity.  (Except for price amounts, which are  always  formatted  as
+       written).  The display format is chosen as follows:
+
+       o if there is a commodity directive specifying the format, that is used
+
+       o otherwise the format is inferred from the  first  posting  amount  in
+         that  commodity  in the journal, and the precision (number of decimal
+         places) will be the maximum from all posting amounts in that commmod-
+         ity
+
+       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 usually don't  affect  amount
+       format  inference,  but  in  some situations 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,  set  the  desired
+       format with a commodity directive.
+
+   Virtual Postings
+       When  you  parenthesise  the  account name in a posting, we call that a
+       virtual posting, which means:
+
+       o it is ignored when checking that the transaction is balanced
+
+       o 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 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.
+
+              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.
+
+   Balance Assertions
+       hledger supports ledger-style  balance  assertions  in  journal  files.
+       These  look  like =EXPECTEDBALANCE following a posting's amount.  Eg in
+       this example 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  pro-
+       tect  you  from, eg, inadvertently disrupting reconciled balances while
+       cleaning up old entries.  You can disable  them  temporarily  with  the
+       --ignore-assertions  flag,  which  can be useful for troubleshooting or
+       for reading Ledger files.
+
+   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 dif-
+       ferent from Ledger, which sorts assertions only by parse order.  (Also,
+       Ledger  assertions  do not see the accumulated effect of repeated post-
+       ings to the same account within a transaction.)
+
+       So, hledger balance assertions keep  working  if  you  reorder  differ-
+       ently-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 con-
+       trol over the order of postings and assertions within a day, so you can
+       assert intra-day balances.
+
+       With  included  files, things are a little more complicated.  Including
+       preserves the ordering of postings and assertions.  If you have  multi-
+       ple  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.
+
+   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.  We could call this a par-
+       tial balance assertion.  This is compatible with Ledger, and  makes  it
+       possible to make assertions about accounts containing multiple commodi-
+       ties.
+
+       To assert each commodity's balance in such a  multi-commodity  account,
+       you  can  add multiple postings (with amount 0 if necessary).  But note
+       that no matter how many assertions you  add,  you  can't  be  sure  the
+       account does not contain some unexpected commodity.  (We'll add support
+       for this kind of total balance assertion if there's demand.)
+
+   Assertions and subaccounts
+       Balance assertions do not count  the  balance  from  subaccounts;  they
+       check the posted account's exclusive balance.  For example:
+
+              1/1
+                checking:fund   1 = 1  ; post to this subaccount, its balance is now 1
+                checking        1 = 1  ; post to the parent account, its exclusive balance is now 1
+                equity
+
+       The  balance  report's  flat  mode  shows these exclusive balances more
+       clearly:
+
+              $ hledger bal checking --flat
+                                 1  checking
+                                 1  checking:fund
+              --------------------
+                                 2
+
+   Assertions and virtual postings
+       Balance assertions are checked against all postings, both real and vir-
+       tual.  They are not affected by the --real/-R flag or real: query.
+
+   Prices
+   Transaction prices
+       When  recording a transaction, you can also record an amount's price in
+       another commodity.  This documents the exchange rate, cost (of  a  pur-
+       chase),  or  selling  price  (of a sale) that was in effect within this
+       particular transaction (or more precisely, within the particular  post-
+       ing).  These transaction prices are fixed, and do not change.
+
+       Such  priced amounts can be displayed in their transaction price's com-
+       modity, by using the --cost/-B flag (B for "cost Basis"), supported  by
+       most hledger commands.
+
+       There are three ways to specify a transaction price:
+
+       1. Write  the  unit price (aka exchange rate), as @ UNITPRICE after the
+          amount:
+
+                  2009/1/1
+                    assets:foreign currency   100 @ $1.35  ; one hundred euros at $1.35 each
+                    assets:cash
+
+       2. Or write the total price, as @@ TOTALPRICE after the amount:
+
+                  2009/1/1
+                    assets:foreign currency   100 @@ $135  ; one hundred euros at $135 for the lot
+                    assets:cash
+
+       3. Or let hledger infer the price so as to balance the transaction.  To
+          permit  this,  you must fully specify all posting amounts, and their
+          sum must have a non-zero amount in exactly two commodities:
+
+                  2009/1/1
+                    assets:foreign currency   100          ; one hundred euros
+                    assets:cash              $-135          ; exchanged for $135
+
+       With any of the above examples we get:
+
+              $ hledger print -B
+              2009/01/01
+                  assets:foreign currency       $135.00
+                  assets:cash                  $-135.00
+
+       Example use for transaction prices: recording the effective  conversion
+       rate of purchases made in a foreign currency.
+
+   Market prices
+       Market  prices are not tied to a particular transaction; they represent
+       historical exchange rates between two commodities,  usually  from  some
+       public market which publishes such rates.
+
+       When  market  prices  are known, the -V/--value option will use them to
+       convert reported amounts to their market value as  of  the  report  end
+       date.   This  option  is currently available only with the balance com-
+       mand.
+
+       You record market prices (Ledger calls them historical prices) with a P
+       directive, in the journal or perhaps in a separate included file.  Mar-
+       ket price directives have the format:
+
+              P DATE COMMODITYSYMBOL UNITPRICE
+
+       For example, the following directives say that the euro's exchange rate
+       was  1.35  US  dollars  during  2009,  and  $1.40 from 2010 onward (and
+       unknown before 2009).
+
+              P 2009/1/1  $1.35
+              P 2010/1/1  $1.40
+
+       Example use for market prices: tracking the value of stocks.
+
+   Comments
+       Lines in the journal beginning with a semicolon  (;)  or  hash  (#)  or
+       asterisk  (*)  are  comments,  and will be ignored.  (Asterisk comments
+       make it easy to treat your journal like an org-mode outline in  emacs.)
+
+       Also,   anything  between  comment  and  end comment  directives  is  a
+       (multi-line) comment.  If there is no end comment, the comment  extends
+       to the end of the file.
+
+       You  can  attach  comments  to  a transaction by writing them after the
+       description and/or indented on the following lines  (before  the  post-
+       ings).   Similarly, you can attach comments to an individual posting by
+       writing them after the amount and/or indented on the following lines.
+
+       Some examples:
+
+              # a journal comment
+
+              ; also a journal comment
+
+              comment
+              This is a multiline comment,
+              which continues until a line
+              where the "end comment" string
+              appears on its own.
+              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 journal comment (because not indented)
+
+   Tags
+       A tag is a word followed by a full colon inside a transaction or  post-
+       ing  comment.   You  can  write  multiple  tags,  comma separated.  Eg:
+       ; a comment containing sometag:, anothertag:.  You can search for  tags
+       with the tag: query.
+
+       A  tag  can  also have a value, which is any text between the colon and
+       the next comma or newline, excluding leading/trailing whitespace.   (So
+       hledger tag values can not contain commas or newlines).
+
+       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 (A, TAG2, third-tag, posting-tag):
+
+              1/1 a transaction  ; A:, TAG2:
+                  ; third-tag: a third transaction tag, this time with a value
+                  (a)  $1  ; posting-tag:
+
+       Tags are like Ledger's metadata feature, except  hledger's  tag  values
+       are simple strings.
+
+   Directives
+   Account aliases
+       You  can define aliases which rewrite your account names (after reading
+       the journal, before generating reports).  hledger's account aliases can
+       be useful for:
+
+       o expanding shorthand account names to their full form, allowing easier
+         data entry and a less verbose journal
+
+       o adapting old journals to your current chart of accounts
+
+       o experimenting with new account organisations, like a new hierarchy or
+         combining two accounts into one
+
+       o customising reports
+
+       See also How to use account aliases.
+
+   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 full account names.  hledger will replace any occur-
+       rence 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"
+
+   Regex aliases
+       There  is  also a more powerful variant that uses a regular expression,
+       indicated by the forward slashes.  (This was the default  behaviour  in
+       hledger 0.24-0.25):
+
+              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  REPLACE-
+       MENT.   If REGEX contains parenthesised match groups, these can be ref-
+       erenced by the usual numeric backreferences in REPLACEMENT.  Note, cur-
+       rently  regular  expression  aliases  may  cause noticeable slow-downs.
+       (And if you use Ledger on your hledger file, they will be ignored.) Eg:
+
+              alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3
+              # rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking"
+
+   Multiple aliases
+       You  can  define  as  many aliases as you like using directives or com-
+       mand-line options.  Aliases are recursive - each alias sees the  result
+       of  applying  previous  ones.   (This  is  different from Ledger, where
+       aliases are non-recursive by default).  Aliases are applied in the fol-
+       lowing order:
+
+       1. alias  directives,  most recently seen first (recent directives take
+          precedence over earlier ones; directives not yet seen are ignored)
+
+       2. alias options, in the order they appear on the command line
+
+   end aliases
+       You  can  clear  (forget)  all  currently  defined  aliases  with   the
+       end aliases directive:
+
+              end aliases
+
+   account directive
+       The  account directive predefines account names, as in Ledger and Bean-
+       count.  This may be useful for your own documentation; hledger  doesn't
+       make use of it yet.
+
+              ; account ACCT
+              ;   OPTIONAL COMMENTS/TAGS...
+
+              account assets:bank:checking
+               a comment
+               acct-no:12345
+
+              account expenses:food
+
+              ; etc.
+
+   apply account directive
+       You  can  specify  a  parent  account  which  will  be prepended to all
+       accounts within a section of the journal.  Use  the  apply account  and
+       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  sup-
+       ported.
+
+   Multi-line comments
+       A  line containing just comment starts a multi-line comment, and a line
+       containing just end comment ends it.  See comments.
+
+   commodity directive
+       The commodity directive predefines commodities (currently this is  just
+       informational),  and  also it may define the display format for amounts
+       in this commodity (overriding the automatically inferred format).
+
+       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 9,99,99,999.00
+
+   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
+
+   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
+
+   Including other files
+       You can pull in the content of additional journal 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.  Glob patterns (*) are not currently supported.
+
+       The include directive can only  be  used  in  journal  files.   It  can
+       include journal, timeclock or timedot files, but not CSV files.
+
+EDITOR SUPPORT
+       Add-on modes exist for various text editors, to make working with jour-
+       nal files easier.  They add colour, navigation aids  and  helpful  com-
+       mands.   For  hledger  users  who  edit  the journal file directly (the
+       majority), using one of these modes is quite recommended.
+
+       These were written with Ledger in mind,  but  also  work  with  hledger
+       files:
+
+
+       Emacs              http://www.ledger-cli.org/3.0/doc/ledger-mode.html
+       Vim                https://github.com/ledger/ledger/wiki/Get-
+                          ting-started
+       Sublime Text       https://github.com/ledger/ledger/wiki/Using-Sub-
+                          lime-Text
+       Textmate           https://github.com/ledger/ledger/wiki/Using-Text-
+                          Mate-2
+       Text Wrangler      https://github.com/ledger/ledger/wiki/Edit-
+                          ing-Ledger-files-with-TextWrangler
+
+
+
+REPORTING BUGS
+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel
+       or hledger mail list)
+
+
+AUTHORS
+       Simon Michael <simon@joyful.com> and contributors
+
+
+COPYRIGHT
+       Copyright (C) 2007-2016 Simon Michael.
+       Released under GNU GPL v3 or later.
+
+
+SEE ALSO
+       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)
+
+       http://hledger.org
+
+
+
+hledger 1.0                      October 2016               hledger_journal(5)
diff --git a/doc/hledger_timeclock.5 b/doc/hledger_timeclock.5
new file mode 100644
--- /dev/null
+++ b/doc/hledger_timeclock.5
@@ -0,0 +1,100 @@
+
+.TH "hledger_timeclock" "5" "October 2016" "hledger 1.0" "hledger User Manuals"
+
+
+
+.SH NAME
+.PP
+Timeclock \- the time logging format of timeclock.el, as read by hledger
+.SH DESCRIPTION
+.PP
+hledger can read timeclock files.
+As with Ledger, these are (a subset of) timeclock.el\[aq]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).
+.IP
+.nf
+\f[C]
+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
+o\ 2015/04/01\ 02:00:34
+\f[]
+.fi
+.PP
+hledger treats each clock\-in/clock\-out pair as a transaction posting
+some number of hours to an account.
+Or if the session spans more than one day, it is split into several
+transactions, one for each day.
+For the above time log, \f[C]hledger\ print\f[] generates these journal
+entries:
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ t.timeclock\ print
+2015/03/30\ *\ optional\ description\ after\ two\ spaces
+\ \ \ \ (some:account\ name)\ \ \ \ \ \ \ \ \ 0.33h
+
+2015/03/31\ *\ 22:21\-23:59
+\ \ \ \ (another\ account)\ \ \ \ \ \ \ \ \ 1.64h
+
+2015/04/01\ *\ 00:00\-02:00
+\ \ \ \ (another\ account)\ \ \ \ \ \ \ \ \ 2.01h
+\f[]
+.fi
+.PP
+Here is a sample.timeclock to download and some queries to try:
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ sample.timeclock\ balance\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ current\ time\ balances
+$\ hledger\ \-f\ sample.timeclock\ register\ \-p\ 2009/3\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ #\ sessions\ in\ march\ 2009
+$\ hledger\ \-f\ sample.timeclock\ register\ \-p\ weekly\ \-\-depth\ 1\ \-\-empty\ \ #\ time\ summary\ by\ week
+\f[]
+.fi
+.PP
+To generate time logs, ie to clock in and clock out, you could:
+.IP \[bu] 2
+use emacs and the built\-in timeclock.el, or the extended
+timeclock\-x.el and perhaps the extras in ledgerutils.el
+.IP \[bu] 2
+at the command line, use these bash aliases:
+.RS 2
+.IP
+.nf
+\f[C]
+alias\ ti="echo\ i\ `date\ \[aq]+%Y\-%m\-%d\ %H:%M:%S\[aq]`\ \\$*\ >>$TIMELOG"
+alias\ to="echo\ o\ `date\ \[aq]+%Y\-%m\-%d\ %H:%M:%S\[aq]`\ >>$TIMELOG"
+\f[]
+.fi
+.RE
+.IP \[bu] 2
+or use the old \f[C]ti\f[] and \f[C]to\f[] scripts in the ledger 2.x
+repository.
+These rely on a "timeclock" executable which I think is just the ledger
+2 executable renamed.
+
+
+.SH "REPORTING BUGS"
+Report bugs at http://bugs.hledger.org
+(or on the #hledger IRC channel or hledger mail list)
+
+.SH AUTHORS
+Simon Michael <simon@joyful.com> and contributors
+
+.SH COPYRIGHT
+
+Copyright (C) 2007-2016 Simon Michael.
+.br
+Released under GNU GPL v3 or later.
+
+.SH SEE ALSO
+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),
+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),
+ledger(1)
+
+http://hledger.org
diff --git a/doc/hledger_timeclock.5.info b/doc/hledger_timeclock.5.info
new file mode 100644
--- /dev/null
+++ b/doc/hledger_timeclock.5.info
@@ -0,0 +1,67 @@
+This is hledger-lib/doc/hledger_timeclock.5.info, produced by makeinfo
+version 4.8 from stdin.
+
+
+File: hledger_timeclock.5.info,  Node: Top,  Up: (dir)
+
+hledger_timeclock(5) hledger 1.0
+********************************
+
+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
+o 2015/04/01 02:00:34
+
+   hledger treats each clock-in/clock-out pair as a transaction posting
+some number of hours to an account. Or if the session spans more than
+one day, it is split into several transactions, one for each day. For
+the above time log, `hledger print' generates these journal entries:
+
+
+$ hledger -f t.timeclock print
+2015/03/30 * optional description after two spaces
+    (some:account name)         0.33h
+
+2015/03/31 * 22:21-23:59
+    (another account)         1.64h
+
+2015/04/01 * 00:00-02:00
+    (another account)         2.01h
+
+   Here is a sample.timeclock to download and some queries to try:
+
+
+$ hledger -f sample.timeclock balance                               # current time balances
+$ hledger -f sample.timeclock register -p 2009/3                    # sessions in march 2009
+$ hledger -f sample.timeclock register -p weekly --depth 1 --empty  # time summary by week
+
+   To generate time logs, ie to clock in and clock out, you could:
+
+   * use emacs and the built-in timeclock.el, or the extended
+     timeclock-x.el and perhaps the extras in ledgerutils.el
+
+   * at the command line, use these bash aliases:
+
+
+     alias ti="echo i `date '+%Y-%m-%d %H:%M:%S'` \$* >>$TIMELOG"
+     alias to="echo o `date '+%Y-%m-%d %H:%M:%S'` >>$TIMELOG"
+
+   * or use the old `ti' and `to' scripts in the ledger 2.x repository.
+     These rely on a "timeclock" executable which I think is just the
+     ledger 2 executable renamed.
+
+
+
+
+Tag Table:
+Node: Top96
+
+End Tag Table
diff --git a/doc/hledger_timeclock.5.txt b/doc/hledger_timeclock.5.txt
new file mode 100644
--- /dev/null
+++ b/doc/hledger_timeclock.5.txt
@@ -0,0 +1,82 @@
+
+hledger_timeclock(5)         hledger User Manuals         hledger_timeclock(5)
+
+
+
+NAME
+       Timeclock - the time logging format of timeclock.el, as read by hledger
+
+DESCRIPTION
+       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
+              o 2015/04/01 02:00:34
+
+       hledger  treats  each  clock-in/clock-out pair as a transaction posting
+       some number of hours to an account.  Or if the session spans more  than
+       one  day, it is split into several transactions, one for each day.  For
+       the above time log, hledger print generates these journal entries:
+
+              $ hledger -f t.timeclock print
+              2015/03/30 * optional description after two spaces
+                  (some:account name)         0.33h
+
+              2015/03/31 * 22:21-23:59
+                  (another account)         1.64h
+
+              2015/04/01 * 00:00-02:00
+                  (another account)         2.01h
+
+       Here is a sample.timeclock to download and some queries to try:
+
+              $ hledger -f sample.timeclock balance                               # current time balances
+              $ hledger -f sample.timeclock register -p 2009/3                    # sessions in march 2009
+              $ hledger -f sample.timeclock register -p weekly --depth 1 --empty  # time summary by week
+
+       To generate time logs, ie to clock in and clock out, you could:
+
+       o use emacs and  the  built-in  timeclock.el,  or  the  extended  time-
+         clock-x.el and perhaps the extras in ledgerutils.el
+
+       o at the command line, use these bash aliases:
+
+                alias ti="echo i `date '+%Y-%m-%d %H:%M:%S'` \$* >>$TIMELOG"
+                alias to="echo o `date '+%Y-%m-%d %H:%M:%S'` >>$TIMELOG"
+
+       o or use the old ti and to scripts in the ledger 2.x repository.  These
+         rely on a "timeclock" executable which I think is just the  ledger  2
+         executable renamed.
+
+
+
+REPORTING BUGS
+       Report  bugs at http://bugs.hledger.org (or on the #hledger IRC channel
+       or hledger mail list)
+
+
+AUTHORS
+       Simon Michael <simon@joyful.com> and contributors
+
+
+COPYRIGHT
+       Copyright (C) 2007-2016 Simon Michael.
+       Released under GNU GPL v3 or later.
+
+
+SEE ALSO
+       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)
+
+       http://hledger.org
+
+
+
+hledger 1.0                      October 2016             hledger_timeclock(5)
diff --git a/doc/hledger_timedot.5 b/doc/hledger_timedot.5
new file mode 100644
--- /dev/null
+++ b/doc/hledger_timedot.5
@@ -0,0 +1,144 @@
+
+.TH "hledger_timedot" "5" "October 2016" "hledger 1.0" "hledger User Manuals"
+
+
+
+.SH NAME
+.PP
+Timedot \- hledger\[aq]s human\-friendly time logging format
+.SH DESCRIPTION
+.PP
+Timedot is a plain text format for logging dated, categorised quantities
+(eg time), 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.
+.PP
+Though called "timedot", the format does not specify the commodity being
+logged, so could represent other dated, quantifiable things.
+Eg you could record a single\-entry journal of financial transactions,
+perhaps slightly more conveniently than with hledger_journal(5) format.
+.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.
+There must be at least two spaces between the category and the quantity.
+Quantities can be written in two ways:
+.IP "1." 3
+a series of dots (period characters).
+Each dot represents "a quarter" \- eg, a quarter hour.
+Spaces can be used to group dots into hours, for easier counting.
+.IP "2." 3
+a number (integer or decimal), representing "units" \- eg, hours.
+A good alternative when dots are cumbersome.
+(A number also can record negative quantities.)
+.PP
+Blank lines and lines beginning with #, ; or * are ignored.
+An example:
+.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\ \ \ ....\ ..\ 
+biz:research\ \ .
+
+2016/2/2
+inc:client1\ \ \ ....\ ....
+biz:research\ \ .
+\f[]
+.fi
+.PP
+Or with numbers:
+.IP
+.nf
+\f[C]
+2016/2/3
+inc:client1\ \ \ 4
+fos:hledger\ \ \ 3
+biz:research\ \ 1
+\f[]
+.fi
+.PP
+Reporting:
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ t.timedot\ print\ date:2016/2/2
+2016/02/02\ *
+\ \ \ \ (inc:client1)\ \ \ \ \ \ \ \ \ \ 2.00
+
+2016/02/02\ *
+\ \ \ \ (biz:research)\ \ \ \ \ \ \ \ \ \ 0.25
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ t.timedot\ bal\ \-\-daily\ \-\-tree
+Balance\ changes\ in\ 2016/02/01\-2016/02/03:
+
+\ \ \ \ \ \ \ \ \ \ \ \ ||\ \ 2016/02/01d\ \ 2016/02/02d\ \ 2016/02/03d\ 
+============++========================================
+\ biz\ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 1.00\ 
+\ \ \ research\ ||\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 0.25\ \ \ \ \ \ \ \ \ 1.00\ 
+\ fos\ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 1.50\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ 3.00\ 
+\ \ \ haskell\ \ ||\ \ \ \ \ \ \ \ \ 1.50\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ \ \ \ 0\ 
+\ \ \ hledger\ \ ||\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ \ \ \ 0\ \ \ \ \ \ \ \ \ 3.00\ 
+\ inc\ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 6.00\ \ \ \ \ \ \ \ \ 2.00\ \ \ \ \ \ \ \ \ 4.00\ 
+\ \ \ client1\ \ ||\ \ \ \ \ \ \ \ \ 6.00\ \ \ \ \ \ \ \ \ 2.00\ \ \ \ \ \ \ \ \ 4.00\ 
+\-\-\-\-\-\-\-\-\-\-\-\-++\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
+\ \ \ \ \ \ \ \ \ \ \ \ ||\ \ \ \ \ \ \ \ \ 7.75\ \ \ \ \ \ \ \ \ 2.25\ \ \ \ \ \ \ \ \ 8.00\ 
+\f[]
+.fi
+.PP
+I prefer to use period for separating account components.
+We can make this work with an account alias:
+.IP
+.nf
+\f[C]
+2016/2/4
+fos.hledger.timedot\ \ 4
+fos.ledger\ \ \ \ \ \ \ \ \ \ \ ..
+\f[]
+.fi
+.IP
+.nf
+\f[C]
+$\ hledger\ \-f\ t.timedot\ \-\-alias\ /\\\\./=:\ bal\ date:2016/2/4
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4.50\ \ fos
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4.00\ \ \ \ hledger:timedot
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 0.50\ \ \ \ ledger
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4.50
+\f[]
+.fi
+.PP
+Here is a sample.timedot.
+
+
+.SH "REPORTING BUGS"
+Report bugs at http://bugs.hledger.org
+(or on the #hledger IRC channel or hledger mail list)
+
+.SH AUTHORS
+Simon Michael <simon@joyful.com> and contributors
+
+.SH COPYRIGHT
+
+Copyright (C) 2007-2016 Simon Michael.
+.br
+Released under GNU GPL v3 or later.
+
+.SH SEE ALSO
+hledger(1), hledger\-ui(1), hledger\-web(1), hledger\-api(1),
+hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_timedot(5),
+ledger(1)
+
+http://hledger.org
diff --git a/doc/hledger_timedot.5.info b/doc/hledger_timedot.5.info
new file mode 100644
--- /dev/null
+++ b/doc/hledger_timedot.5.info
@@ -0,0 +1,121 @@
+This is hledger-lib/doc/hledger_timedot.5.info, produced by makeinfo
+version 4.8 from stdin.
+
+
+File: hledger_timedot.5.info,  Node: Top,  Up: (dir)
+
+hledger_timedot(5) hledger 1.0
+******************************
+
+Timedot is a plain text format for logging dated, categorised quantities
+(eg time), 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.
+
+   Though called "timedot", the format does not specify the commodity
+being logged, so could represent other dated, quantifiable things. Eg
+you could record a single-entry journal of financial transactions,
+perhaps slightly more conveniently than with hledger_journal(5) format.
+
+* Menu:
+
+* FILE FORMAT::
+
+
+File: hledger_timedot.5.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 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. There must be at least
+two spaces between the category and the quantity. Quantities can be
+written in two ways:
+
+  1. a series of dots (period characters). Each dot represents "a
+     quarter" - eg, a quarter hour. Spaces can be used to group dots
+     into hours, for easier counting.
+
+  2. a number (integer or decimal), representing "units" - eg, hours. A
+     good alternative when dots are cumbersome. (A number also can
+     record negative quantities.)
+
+
+   Blank lines and lines beginning with #, ; or * are ignored. An
+example:
+
+
+# on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.
+2016/2/1
+inc:client1   .... .... .... .... .... ....
+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
+
+   Reporting:
+
+
+$ hledger -f t.timedot print date:2016/2/2
+2016/02/02 *
+    (inc:client1)          2.00
+
+2016/02/02 *
+    (biz:research)          0.25
+
+
+$ hledger -f t.timedot bal --daily --tree
+Balance changes in 2016/02/01-2016/02/03:
+
+            ||  2016/02/01d  2016/02/02d  2016/02/03d
+============++========================================
+ biz        ||         0.25         0.25         1.00
+   research ||         0.25         0.25         1.00
+ fos        ||         1.50            0         3.00
+   haskell  ||         1.50            0            0
+   hledger  ||            0            0         3.00
+ inc        ||         6.00         2.00         4.00
+   client1  ||         6.00         2.00         4.00
+------------++----------------------------------------
+            ||         7.75         2.25         8.00
+
+   I prefer to use period for separating account components. We can make
+this work with an account alias:
+
+
+2016/2/4
+fos.hledger.timedot  4
+fos.ledger           ..
+
+
+$ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4
+                4.50  fos
+                4.00    hledger:timedot
+                0.50    ledger
+--------------------
+                4.50
+
+   Here is a sample.timedot.
+
+
+
+Tag Table:
+Node: Top94
+Node: FILE FORMAT876
+Ref: #file-format979
+
+End Tag Table
diff --git a/doc/hledger_timedot.5.txt b/doc/hledger_timedot.5.txt
new file mode 100644
--- /dev/null
+++ b/doc/hledger_timedot.5.txt
@@ -0,0 +1,123 @@
+
+hledger_timedot(5)           hledger User Manuals           hledger_timedot(5)
+
+
+
+NAME
+       Timedot - hledger's human-friendly time logging format
+
+DESCRIPTION
+       Timedot  is  a plain text format for logging dated, categorised quanti-
+       ties (eg time), 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.
+
+       Though called "timedot", the format  does  not  specify  the  commodity
+       being  logged, so could represent other dated, quantifiable things.  Eg
+       you could record a single-entry journal of financial transactions, per-
+       haps slightly more conveniently than with hledger_journal(5) format.
+
+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.   There
+       must  be  at  least  two  spaces between the category and the quantity.
+       Quantities can be written in two ways:
+
+       1. a series of dots (period characters).  Each dot represents "a  quar-
+          ter"  -  eg,  a quarter hour.  Spaces can be used to group dots into
+          hours, for easier counting.
+
+       2. a number (integer or decimal), representing "units" - eg, hours.   A
+          good  alternative  when  dots  are  cumbersome.   (A number also can
+          record negative quantities.)
+
+       Blank lines and lines beginning with #, ; or * are ignored.   An  exam-
+       ple:
+
+              # on this day, 6h was spent on client work, 1.5h on haskell FOSS work, etc.
+              2016/2/1
+              inc:client1   .... .... .... .... .... ....
+              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
+
+       Reporting:
+
+              $ hledger -f t.timedot print date:2016/2/2
+              2016/02/02 *
+                  (inc:client1)          2.00
+
+              2016/02/02 *
+                  (biz:research)          0.25
+
+              $ hledger -f t.timedot bal --daily --tree
+              Balance changes in 2016/02/01-2016/02/03:
+
+                          ||  2016/02/01d  2016/02/02d  2016/02/03d
+              ============++========================================
+               biz        ||         0.25         0.25         1.00
+                 research ||         0.25         0.25         1.00
+               fos        ||         1.50            0         3.00
+                 haskell  ||         1.50            0            0
+                 hledger  ||            0            0         3.00
+               inc        ||         6.00         2.00         4.00
+                 client1  ||         6.00         2.00         4.00
+              ------------++----------------------------------------
+                          ||         7.75         2.25         8.00
+
+       I  prefer to use period for separating account components.  We can make
+       this work with an account alias:
+
+              2016/2/4
+              fos.hledger.timedot  4
+              fos.ledger           ..
+
+              $ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4
+                              4.50  fos
+                              4.00    hledger:timedot
+                              0.50    ledger
+              --------------------
+                              4.50
+
+       Here is a sample.timedot.
+
+
+
+REPORTING BUGS
+       Report bugs at http://bugs.hledger.org (or on the #hledger IRC  channel
+       or hledger mail list)
+
+
+AUTHORS
+       Simon Michael <simon@joyful.com> and contributors
+
+
+COPYRIGHT
+       Copyright (C) 2007-2016 Simon Michael.
+       Released under GNU GPL v3 or later.
+
+
+SEE ALSO
+       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)
+
+       http://hledger.org
+
+
+
+hledger 1.0                      October 2016               hledger_timedot(5)
diff --git a/hledger-lib.cabal b/hledger-lib.cabal
--- a/hledger-lib.cabal
+++ b/hledger-lib.cabal
@@ -1,20 +1,20 @@
--- This file has been generated from package.yaml by hpack version 0.5.4.
+-- This file has been generated from package.yaml by hpack version 0.14.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           hledger-lib
-version:        0.27.1
+version:        1.0
 stability:      stable
 category:       Finance
 synopsis:       Core data types, parsers and functionality for the hledger accounting tools
-description:    
-    This is a reusable library containing hledger's core functionality.
-    hledger is a cross-platform program for tracking money, time, or
-    any other commodity, using double-entry accounting and a simple,
-    editable file format. It is inspired by and largely compatible
-    with ledger(1).  hledger provides command-line, curses and web
-    interfaces, and aims to be a reliable, practical tool for daily
-    use.
+description:    This is a reusable library containing hledger's core functionality.
+                .
+                hledger is a cross-platform program for tracking money, time, or
+                any other commodity, using double-entry accounting and a simple,
+                editable file format. It is inspired by and largely compatible
+                with ledger(1).  hledger provides command-line, curses and web
+                interfaces, and aims to be a reliable, practical tool for daily
+                use.
 license:        GPL
 license-file:   LICENSE
 author:         Simon Michael <simon@joyful.com>
@@ -23,65 +23,76 @@
 bug-reports:    http://bugs.hledger.org
 cabal-version:  >= 1.10
 build-type:     Simple
-tested-with:    GHC==7.6.3, GHC==7.8.4, GHC==7.10.3, GHC==8.0.1
+tested-with:    GHC==7.10.3, GHC==8.0
 
 extra-source-files:
-    CHANGES 
+    CHANGES
+    README
 
+data-files:
+    doc/hledger_csv.5
+    doc/hledger_csv.5.info
+    doc/hledger_csv.5.txt
+    doc/hledger_journal.5
+    doc/hledger_journal.5.info
+    doc/hledger_journal.5.txt
+    doc/hledger_timeclock.5
+    doc/hledger_timeclock.5.info
+    doc/hledger_timeclock.5.txt
+    doc/hledger_timedot.5
+    doc/hledger_timedot.5.info
+    doc/hledger_timedot.5.txt
+
 source-repository head
   type: git
   location: https://github.com/simonmichael/hledger
 
-flag double
-  manual: True
+flag oldtime
+  description: If building with time < 1.5, also depend on old-locale. Set automatically by cabal.
+  manual: False
   default: False
-  description:
-    Use old Double number representation (instead of Decimal), for testing/benchmarking.
 
-flag old-locale
-  default: False
-  description: 
-    A compatibility flag, set automatically by cabal.
-    If false then depend on time >= 1.5, 
-    if true then depend on time < 1.5 together with old-locale.
-
 library
-  if flag(double)
-    cpp-options: -DDOUBLE
   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans
   build-depends:
-      base >= 4.3 && < 5
-    , base-compat >= 0.8.1
+      base >=4.3 && <5
+    , base-compat >=0.8.1
     , array
-    , blaze-markup >= 0.5.1
+    , blaze-markup >=0.5.1
     , bytestring
-    , cmdargs >= 0.10 && < 0.11
+    , cmdargs >=0.10 && <0.11
     , containers
     , csv
+    , data-default >=0.5
     , Decimal
     , deepseq
     , directory
     , filepath
+    , megaparsec >=5 && < 5.1
     , mtl
     , mtl-compat
     , old-time
-    , parsec >= 3
+    , pretty-show >=1.6.4
     , regex-tdfa
-    , safe >= 0.2
-    , split >= 0.1 && < 0.3
-    , transformers >= 0.2 && < 0.6
+    , safe >=0.2
+    , split >=0.1 && <0.3
+    , text >=1.2 && <1.3
+    , transformers >=0.2 && <0.6
     , uglymemo
-    , utf8-string >= 0.3.5 && < 1.1
+    , utf8-string >=0.3.5 && <1.1
     , HUnit
-
-  if impl(ghc >= 7.4)
-    build-depends: pretty-show >= 1.6.4
-
-  if flag(old-locale)
-    build-depends: time < 1.5, old-locale
+    , parsec
+    , semigroups
+  if impl(ghc <7.6)
+    build-depends:
+        ghc-prim
+  if flag(oldtime)
+    build-depends:
+        time <1.5
+      , old-locale
   else
-    build-depends: time >= 1.5
-
+    build-depends:
+        time >=1.5
   exposed-modules:
       Hledger
       Hledger.Data
@@ -92,17 +103,20 @@
       Hledger.Data.Dates
       Hledger.Data.Journal
       Hledger.Data.Ledger
+      Hledger.Data.Period
       Hledger.Data.StringFormat
       Hledger.Data.Posting
       Hledger.Data.RawOptions
-      Hledger.Data.TimeLog
+      Hledger.Data.Timeclock
       Hledger.Data.Transaction
       Hledger.Data.Types
       Hledger.Query
       Hledger.Read
+      Hledger.Read.Common
       Hledger.Read.CsvReader
       Hledger.Read.JournalReader
-      Hledger.Read.TimelogReader
+      Hledger.Read.TimedotReader
+      Hledger.Read.TimeclockReader
       Hledger.Reports
       Hledger.Reports.ReportOptions
       Hledger.Reports.BalanceHistoryReport
@@ -117,50 +131,97 @@
       Hledger.Utils.Regex
       Hledger.Utils.String
       Hledger.Utils.Test
+      Hledger.Utils.Text
       Hledger.Utils.Tree
       Hledger.Utils.UTF8IOCompat
+  other-modules:
+      Paths_hledger_lib
   default-language: Haskell2010
 
-test-suite tests
+test-suite doctests
   type: exitcode-stdio-1.0
-  main-is: suite.hs
   hs-source-dirs:
       tests
   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans
+  main-is: doctests.hs
   build-depends:
-      base >= 4.3 && < 5
-    , base-compat >= 0.8.1
+      base >=4.3 && <5
+    , base-compat >=0.8.1
     , array
-    , blaze-markup >= 0.5.1
+    , blaze-markup >=0.5.1
     , bytestring
-    , cmdargs >= 0.10 && < 0.11
+    , cmdargs >=0.10 && <0.11
     , containers
     , csv
+    , data-default >=0.5
     , Decimal
     , deepseq
     , directory
     , filepath
+    , megaparsec >=5 && < 5.1
     , mtl
     , mtl-compat
     , old-time
-    , parsec >= 3
+    , pretty-show >=1.6.4
     , regex-tdfa
-    , safe >= 0.2
-    , split >= 0.1 && < 0.3
-    , transformers >= 0.2 && < 0.6
+    , safe >=0.2
+    , split >=0.1 && <0.3
+    , text >=1.2 && <1.3
+    , transformers >=0.2 && <0.6
     , uglymemo
-    , utf8-string >= 0.3.5 && < 1.1
+    , utf8-string >=0.3.5 && <1.1
     , HUnit
+    , doctest >=0.8
+    , Glob >=0.7
+  if impl(ghc <7.6)
+    build-depends:
+        ghc-prim
+  default-language: Haskell2010
+
+test-suite hunittests
+  type: exitcode-stdio-1.0
+  main-is: hunittests.hs
+  hs-source-dirs:
+      tests
+  ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-type-defaults -fno-warn-orphans
+  build-depends:
+      base >=4.3 && <5
+    , base-compat >=0.8.1
+    , array
+    , blaze-markup >=0.5.1
+    , bytestring
+    , cmdargs >=0.10 && <0.11
+    , containers
+    , csv
+    , data-default >=0.5
+    , Decimal
+    , deepseq
+    , directory
+    , filepath
+    , megaparsec >=5 && < 5.1
+    , mtl
+    , mtl-compat
+    , old-time
+    , pretty-show >=1.6.4
+    , regex-tdfa
+    , safe >=0.2
+    , split >=0.1 && <0.3
+    , text >=1.2 && <1.3
+    , transformers >=0.2 && <0.6
+    , uglymemo
+    , utf8-string >=0.3.5 && <1.1
+    , HUnit
     , hledger-lib
     , test-framework
     , test-framework-hunit
-
-  if impl(ghc >= 7.4)
-    build-depends: pretty-show >= 1.6.4
-
-  if flag(old-locale)
-    build-depends: time < 1.5, old-locale
+  if impl(ghc <7.6)
+    build-depends:
+        ghc-prim
+  if flag(oldtime)
+    build-depends:
+        time <1.5
+      , old-locale
   else
-    build-depends: time >= 1.5
-
+    build-depends:
+        time >=1.5
   default-language: Haskell2010
diff --git a/tests/doctests.hs b/tests/doctests.hs
new file mode 100644
--- /dev/null
+++ b/tests/doctests.hs
@@ -0,0 +1,9 @@
+{-# LANGUAGE PackageImports #-}
+
+import Data.List
+import "Glob" System.FilePath.Glob
+import Test.DocTest
+
+main = do
+  fs <- ("Hledger.hs" :) . filter (not . isInfixOf "/.") <$> glob "Hledger/**/*.hs"
+  doctest fs
diff --git a/tests/hunittests.hs b/tests/hunittests.hs
new file mode 100644
--- /dev/null
+++ b/tests/hunittests.hs
@@ -0,0 +1,6 @@
+import Hledger (tests_Hledger)
+import Test.Framework.Providers.HUnit (hUnitTestToTests)
+import Test.Framework.Runners.Console (defaultMain)
+
+main :: IO ()
+main = defaultMain $ hUnitTestToTests tests_Hledger
diff --git a/tests/suite.hs b/tests/suite.hs
deleted file mode 100644
--- a/tests/suite.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-import Hledger (tests_Hledger)
-import Test.Framework.Providers.HUnit (hUnitTestToTests)
-import Test.Framework.Runners.Console (defaultMain)
-
-main :: IO ()
-main = defaultMain $ hUnitTestToTests tests_Hledger
