packages feed

hledger-lib 1.19.1 → 1.20

raw patch · 45 files changed

+2757/−1905 lines, 45 filesdep +pretty-simpledep −fgldep −pretty-showPVP ok

version bump matches the API change (PVP)

Dependencies added: pretty-simple

Dependencies removed: fgl, pretty-show

API changes (from Hackage documentation)

- Hledger.Query: Empty :: Bool -> Query
- Hledger.Query: queryIsEmpty :: Query -> Bool
- Hledger.Read: accountaliasp :: TextParser m AccountAlias
- Hledger.Reports.MultiBalanceReport: tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String
- Hledger.Reports.MultiBalanceReport: type CompoundBalanceReport = CompoundPeriodicReport DisplayName MixedAmount
- Hledger.Reports.ReportOptions: [query_] :: ReportOpts -> String
- Hledger.Reports.ReportOptions: [today_] :: ReportOpts -> Maybe Day
- Hledger.Reports.ReportOptions: checkReportOpts :: ReportOpts -> ReportOpts
- Hledger.Reports.ReportOptions: queryFromOpts :: Day -> ReportOpts -> Query
- Hledger.Reports.ReportOptions: queryFromOptsOnly :: Day -> ReportOpts -> Query
- Hledger.Reports.ReportOptions: queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt]
- Hledger.Reports.ReportOptions: specifiedEndDate :: ReportOpts -> IO (Maybe Day)
- Hledger.Reports.ReportOptions: specifiedStartDate :: ReportOpts -> IO (Maybe Day)
- Hledger.Reports.ReportOptions: specifiedStartEndDates :: ReportOpts -> IO (Maybe Day, Maybe Day)
- Hledger.Reports.ReportOptions: tests_ReportOptions :: TestTree
- Hledger.Reports.ReportOptions: type FormatStr = String
- Hledger.Reports.ReportTypes: [cbcsubreportnormalsign] :: CBCSubreportSpec -> NormalSign
- Hledger.Reports.ReportTypes: prNormaliseSign :: Num b => NormalSign -> PeriodicReport a b -> PeriodicReport a b
- Hledger.Utils: instance Data.Default.Class.Default GHC.Types.Bool
+ Hledger.Data.Amount: amountUnstyled :: Amount -> Amount
+ Hledger.Data.Amount: instance GHC.Show.Show Hledger.Data.Amount.AmountDisplay
+ Hledger.Data.Amount: mixedAmountUnstyled :: MixedAmount -> MixedAmount
+ Hledger.Data.Amount: showMixed :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
+ Hledger.Data.Amount: showMixedOneLine :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
+ Hledger.Data.Amount: showMixedOneLineUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
+ Hledger.Data.Amount: showMixedUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)
+ Hledger.Data.Journal: journalApplyAliases :: [AccountAlias] -> Journal -> Either RegexError Journal
+ Hledger.Data.Journal: journalCommoditiesDeclared :: Journal -> [AccountName]
+ Hledger.Data.Posting: postingApplyAliases :: [AccountAlias] -> Posting -> Either RegexError Posting
+ Hledger.Data.RawOptions: appendopts :: [(String, String)] -> RawOpts -> RawOpts
+ Hledger.Data.RawOptions: overRawOpts :: ([(String, String)] -> [(String, String)]) -> RawOpts -> RawOpts
+ Hledger.Data.StringFormat: defaultBalanceLineFormat :: StringFormat
+ Hledger.Data.StringFormat: instance Data.Default.Class.Default Hledger.Data.StringFormat.StringFormat
+ Hledger.Data.StringFormat: overlineWidth :: StringFormat -> Maybe Int
+ Hledger.Data.Transaction: transactionApplyAliases :: [AccountAlias] -> Transaction -> Either RegexError Transaction
+ Hledger.Data.Types: [jglobalcommoditystyles] :: Journal -> Map CommoditySymbol AmountStyle
+ Hledger.Data.Types: [jparsedecimalmark] :: Journal -> Maybe DecimalMark
+ Hledger.Data.Types: isDecimalMark :: Char -> Bool
+ Hledger.Data.Types: type DecimalMark = Char
+ Hledger.Query: AbsEq :: OrdPlus
+ Hledger.Query: AbsGt :: OrdPlus
+ Hledger.Query: AbsGtEq :: OrdPlus
+ Hledger.Query: AbsLt :: OrdPlus
+ Hledger.Query: AbsLtEq :: OrdPlus
+ Hledger.Query: Eq :: OrdPlus
+ Hledger.Query: Gt :: OrdPlus
+ Hledger.Query: GtEq :: OrdPlus
+ Hledger.Query: Lt :: OrdPlus
+ Hledger.Query: LtEq :: OrdPlus
+ Hledger.Query: data OrdPlus
+ Hledger.Query: instance Data.Default.Class.Default Hledger.Query.Query
+ Hledger.Query: parseQueryList :: Day -> [Text] -> Either String (Query, [QueryOpt])
+ Hledger.Read.Common: [commoditystyles_] :: InputOpts -> Maybe (Map CommoditySymbol AmountStyle)
+ Hledger.Read.Common: [strict_] :: InputOpts -> Bool
+ Hledger.Read.Common: accountaliasp :: TextParser m AccountAlias
+ Hledger.Read.Common: aliasesFromOpts :: InputOpts -> [AccountAlias]
+ Hledger.Reports.BudgetReport: budgetReportAsCsv :: ReportOpts -> BudgetReport -> CSV
+ Hledger.Reports.MultiBalanceReport: calculateColSpans :: ReportOpts -> DateSpan -> [Day] -> [DateSpan]
+ Hledger.Reports.MultiBalanceReport: calculateReportSpan :: ReportSpec -> Journal -> DateSpan
+ Hledger.Reports.MultiBalanceReport: generateMultiBalanceReport :: ReportSpec -> Journal -> (Day -> MixedAmount -> MixedAmount) -> [DateSpan] -> Map DateSpan [Posting] -> HashMap AccountName Account -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReport: getPostings :: ReportSpec -> Journal -> [(Posting, Day)]
+ Hledger.Reports.MultiBalanceReport: getPostingsByColumn :: ReportSpec -> Journal -> DateSpan -> Map DateSpan [Posting]
+ Hledger.Reports.MultiBalanceReport: makeReportQuery :: ReportSpec -> DateSpan -> ReportSpec
+ Hledger.Reports.MultiBalanceReport: startingBalances :: ReportSpec -> Journal -> DateSpan -> HashMap AccountName Account
+ Hledger.Reports.ReportOptions: ReportSpec :: ReportOpts -> Day -> Query -> [QueryOpt] -> ReportSpec
+ Hledger.Reports.ReportOptions: [querystring_] :: ReportOpts -> [Text]
+ Hledger.Reports.ReportOptions: [rsOpts] :: ReportSpec -> ReportOpts
+ Hledger.Reports.ReportOptions: [rsQueryOpts] :: ReportSpec -> [QueryOpt]
+ Hledger.Reports.ReportOptions: [rsQuery] :: ReportSpec -> Query
+ Hledger.Reports.ReportOptions: [rsToday] :: ReportSpec -> Day
+ Hledger.Reports.ReportOptions: data ReportSpec
+ Hledger.Reports.ReportOptions: defreportspec :: ReportSpec
+ Hledger.Reports.ReportOptions: instance Data.Default.Class.Default Hledger.Reports.ReportOptions.ReportSpec
+ Hledger.Reports.ReportOptions: instance GHC.Show.Show Hledger.Reports.ReportOptions.ReportSpec
+ Hledger.Reports.ReportOptions: queryFromFlags :: ReportOpts -> Query
+ Hledger.Reports.ReportOptions: rawOptsToReportSpec :: RawOpts -> IO ReportSpec
+ Hledger.Reports.ReportOptions: reportOptsToSpec :: Day -> ReportOpts -> Either String ReportSpec
+ Hledger.Reports.ReportOptions: updateReportSpecFromOpts :: (ReportOpts -> ReportOpts) -> ReportSpec -> Either String ReportSpec
+ Hledger.Reports.ReportTypes: [cbcsubreportoptions] :: CBCSubreportSpec a -> ReportOpts -> ReportOpts
+ Hledger.Reports.ReportTypes: [cbcsubreporttransform] :: CBCSubreportSpec a -> PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount
+ Hledger.Reports.ReportTypes: instance GHC.Base.Functor (Hledger.Reports.ReportTypes.CompoundPeriodicReport a)
+ Hledger.Utils.String: strWidthAnsi :: String -> Int
+ Text.Tabular.AsciiWide: BottomLeft :: Align
+ Text.Tabular.AsciiWide: BottomRight :: Align
+ Text.Tabular.AsciiWide: Cell :: Align -> [(String, Int)] -> Cell
+ Text.Tabular.AsciiWide: TableOpts :: Bool -> Bool -> Bool -> TableOpts
+ Text.Tabular.AsciiWide: TopLeft :: Align
+ Text.Tabular.AsciiWide: TopRight :: Align
+ Text.Tabular.AsciiWide: [borderSpaces] :: TableOpts -> Bool
+ Text.Tabular.AsciiWide: [prettyTable] :: TableOpts -> Bool
+ Text.Tabular.AsciiWide: [tableBorders] :: TableOpts -> Bool
+ Text.Tabular.AsciiWide: alignCell :: Align -> String -> Cell
+ Text.Tabular.AsciiWide: cellWidth :: Cell -> Int
+ Text.Tabular.AsciiWide: data Align
+ Text.Tabular.AsciiWide: data Cell
+ Text.Tabular.AsciiWide: data TableOpts
+ Text.Tabular.AsciiWide: emptyCell :: Cell
+ Text.Tabular.AsciiWide: instance Data.Default.Class.Default Text.Tabular.AsciiWide.TableOpts
+ Text.Tabular.AsciiWide: instance GHC.Show.Show Text.Tabular.AsciiWide.Align
+ Text.Tabular.AsciiWide: instance GHC.Show.Show Text.Tabular.AsciiWide.Cell
+ Text.Tabular.AsciiWide: instance GHC.Show.Show Text.Tabular.AsciiWide.TableOpts
+ Text.Tabular.AsciiWide: renderRow :: TableOpts -> Header Cell -> String
+ Text.Tabular.AsciiWide: renderTable :: TableOpts -> (rh -> Cell) -> (ch -> Cell) -> (a -> Cell) -> Table rh ch a -> String
- Hledger.Data.Amount: showAmountWithoutPrice :: Bool -> Amount -> String
+ Hledger.Data.Amount: showAmountWithoutPrice :: Amount -> String
- Hledger.Data.Amount: showMixedAmountElided :: Bool -> MixedAmount -> String
+ Hledger.Data.Amount: showMixedAmountElided :: Int -> Bool -> MixedAmount -> String
- Hledger.Data.StringFormat: BottomAligned :: [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: BottomAligned :: Maybe Int -> [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: OneLine :: [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: OneLine :: Maybe Int -> [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: TopAligned :: [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: TopAligned :: Maybe Int -> [StringFormatComponent] -> StringFormat
- Hledger.Data.StringFormat: defaultStringFormatStyle :: [StringFormatComponent] -> StringFormat
+ Hledger.Data.StringFormat: defaultStringFormatStyle :: Maybe Int -> [StringFormatComponent] -> StringFormat
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> ClockTime -> Journal
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> Bool -> String -> Bool -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> Bool -> String -> Bool -> Maybe (Map CommoditySymbol AmountStyle) -> Bool -> InputOpts
- Hledger.Read.Common: journalFinalise :: InputOpts -> FilePath -> Text -> Journal -> ExceptT String IO Journal
+ Hledger.Read.Common: journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal
- Hledger.Read.TimedotReader: timedotfilep :: () => JournalParser m ParsedJournal
+ Hledger.Read.TimedotReader: timedotfilep :: forall (m :: Type -> Type). JournalParser m ParsedJournal
- Hledger.Reports.AccountTransactionsReport: accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport
+ Hledger.Reports.AccountTransactionsReport: accountTransactionsReport :: ReportSpec -> Journal -> Query -> Query -> AccountTransactionsReport
- Hledger.Reports.BalanceReport: balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
+ Hledger.Reports.BalanceReport: balanceReport :: ReportSpec -> Journal -> BalanceReport
- Hledger.Reports.BudgetReport: budgetReport :: ReportOpts -> Bool -> DateSpan -> Day -> Journal -> BudgetReport
+ Hledger.Reports.BudgetReport: budgetReport :: ReportSpec -> Bool -> DateSpan -> Journal -> BudgetReport
- Hledger.Reports.EntriesReport: entriesReport :: ReportOpts -> Query -> Journal -> EntriesReport
+ Hledger.Reports.EntriesReport: entriesReport :: ReportSpec -> Journal -> EntriesReport
- Hledger.Reports.MultiBalanceReport: compoundBalanceReport :: Day -> ReportOpts -> Journal -> [CBCSubreportSpec] -> CompoundBalanceReport
+ Hledger.Reports.MultiBalanceReport: compoundBalanceReport :: ReportSpec -> Journal -> [CBCSubreportSpec a] -> CompoundPeriodicReport a MixedAmount
- Hledger.Reports.MultiBalanceReport: compoundBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> [CBCSubreportSpec] -> CompoundBalanceReport
+ Hledger.Reports.MultiBalanceReport: compoundBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> [CBCSubreportSpec a] -> CompoundPeriodicReport a MixedAmount
- Hledger.Reports.MultiBalanceReport: multiBalanceReport :: Day -> ReportOpts -> Journal -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReport: multiBalanceReport :: ReportSpec -> Journal -> MultiBalanceReport
- Hledger.Reports.MultiBalanceReport: multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReport: multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport
- Hledger.Reports.PostingsReport: postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport
+ Hledger.Reports.PostingsReport: postingsReport :: ReportSpec -> Journal -> PostingsReport
- Hledger.Reports.ReportOptions: ReportOpts :: Maybe Day -> Period -> Interval -> [Status] -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> Maybe FormatStr -> String -> Bool -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Maybe DateSpan -> Bool -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> [Text] -> Bool -> Bool -> Bool -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Maybe DateSpan -> Bool -> ReportOpts
- Hledger.Reports.ReportOptions: [format_] :: ReportOpts -> Maybe FormatStr
+ Hledger.Reports.ReportOptions: [format_] :: ReportOpts -> StringFormat
- Hledger.Reports.ReportOptions: reportEndDate :: Journal -> ReportOpts -> IO (Maybe Day)
+ Hledger.Reports.ReportOptions: reportEndDate :: Journal -> ReportSpec -> Maybe Day
- Hledger.Reports.ReportOptions: reportPeriodLastDay :: ReportOpts -> Maybe Day
+ Hledger.Reports.ReportOptions: reportPeriodLastDay :: ReportSpec -> Maybe Day
- Hledger.Reports.ReportOptions: reportPeriodOrJournalLastDay :: ReportOpts -> Journal -> Maybe Day
+ Hledger.Reports.ReportOptions: reportPeriodOrJournalLastDay :: ReportSpec -> Journal -> Maybe Day
- Hledger.Reports.ReportOptions: reportPeriodOrJournalStart :: ReportOpts -> Journal -> Maybe Day
+ Hledger.Reports.ReportOptions: reportPeriodOrJournalStart :: ReportSpec -> Journal -> Maybe Day
- Hledger.Reports.ReportOptions: reportPeriodStart :: ReportOpts -> Maybe Day
+ Hledger.Reports.ReportOptions: reportPeriodStart :: ReportSpec -> Maybe Day
- Hledger.Reports.ReportOptions: reportSpan :: Journal -> ReportOpts -> IO DateSpan
+ Hledger.Reports.ReportOptions: reportSpan :: Journal -> ReportSpec -> DateSpan
- Hledger.Reports.ReportOptions: reportStartDate :: Journal -> ReportOpts -> IO (Maybe Day)
+ Hledger.Reports.ReportOptions: reportStartDate :: Journal -> ReportSpec -> Maybe Day
- Hledger.Reports.ReportTypes: CBCSubreportSpec :: String -> (Journal -> Query) -> NormalSign -> Bool -> CBCSubreportSpec
+ Hledger.Reports.ReportTypes: CBCSubreportSpec :: String -> (Journal -> Query) -> (ReportOpts -> ReportOpts) -> (PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount) -> Bool -> CBCSubreportSpec a
- Hledger.Reports.ReportTypes: [cbcsubreportincreasestotal] :: CBCSubreportSpec -> Bool
+ Hledger.Reports.ReportTypes: [cbcsubreportincreasestotal] :: CBCSubreportSpec a -> Bool
- Hledger.Reports.ReportTypes: [cbcsubreportquery] :: CBCSubreportSpec -> Journal -> Query
+ Hledger.Reports.ReportTypes: [cbcsubreportquery] :: CBCSubreportSpec a -> Journal -> Query
- Hledger.Reports.ReportTypes: [cbcsubreporttitle] :: CBCSubreportSpec -> String
+ Hledger.Reports.ReportTypes: [cbcsubreporttitle] :: CBCSubreportSpec a -> String
- Hledger.Reports.ReportTypes: data CBCSubreportSpec
+ Hledger.Reports.ReportTypes: data CBCSubreportSpec a
- Hledger.Reports.TransactionsReport: triAmount :: () => (a, b, c, d, e, f) -> e
+ Hledger.Reports.TransactionsReport: triAmount :: (a, b, c, d, e, f) -> e
- Hledger.Reports.TransactionsReport: triBalance :: () => (a, b, c, d, e, f) -> f
+ Hledger.Reports.TransactionsReport: triBalance :: (a, b, c, d, e, f) -> f
- Hledger.Reports.TransactionsReport: triCommodityAmount :: () => CommoditySymbol -> (a, b, c, d, MixedAmount, f) -> MixedAmount
+ Hledger.Reports.TransactionsReport: triCommodityAmount :: CommoditySymbol -> (a, b, c, d, MixedAmount, f) -> MixedAmount
- Hledger.Reports.TransactionsReport: triCommodityBalance :: () => CommoditySymbol -> (a, b, c, d, e, MixedAmount) -> MixedAmount
+ Hledger.Reports.TransactionsReport: triCommodityBalance :: CommoditySymbol -> (a, b, c, d, e, MixedAmount) -> MixedAmount
- Hledger.Reports.TransactionsReport: triDate :: () => (a, Transaction, c, d, e, f) -> Day
+ Hledger.Reports.TransactionsReport: triDate :: (a, Transaction, c, d, e, f) -> Day
- Hledger.Reports.TransactionsReport: triOrigTransaction :: () => (a, b, c, d, e, f) -> a
+ Hledger.Reports.TransactionsReport: triOrigTransaction :: (a, b, c, d, e, f) -> a
- Hledger.Utils: fifth5 :: () => (a, b, c, d, e) -> e
+ Hledger.Utils: fifth5 :: (a, b, c, d, e) -> e
- Hledger.Utils: fifth6 :: () => (a, b, c, d, e, f) -> e
+ Hledger.Utils: fifth6 :: (a, b, c, d, e, f) -> e
- Hledger.Utils: first3 :: () => (a, b, c) -> a
+ Hledger.Utils: first3 :: (a, b, c) -> a
- Hledger.Utils: first4 :: () => (a, b, c, d) -> a
+ Hledger.Utils: first4 :: (a, b, c, d) -> a
- Hledger.Utils: first5 :: () => (a, b, c, d, e) -> a
+ Hledger.Utils: first5 :: (a, b, c, d, e) -> a
- Hledger.Utils: first6 :: () => (a, b, c, d, e, f) -> a
+ Hledger.Utils: first6 :: (a, b, c, d, e, f) -> a
- Hledger.Utils: fourth4 :: () => (a, b, c, d) -> d
+ Hledger.Utils: fourth4 :: (a, b, c, d) -> d
- Hledger.Utils: fourth5 :: () => (a, b, c, d, e) -> d
+ Hledger.Utils: fourth5 :: (a, b, c, d, e) -> d
- Hledger.Utils: fourth6 :: () => (a, b, c, d, e, f) -> d
+ Hledger.Utils: fourth6 :: (a, b, c, d, e, f) -> d
- Hledger.Utils: second3 :: () => (a, b, c) -> b
+ Hledger.Utils: second3 :: (a, b, c) -> b
- Hledger.Utils: second4 :: () => (a, b, c, d) -> b
+ Hledger.Utils: second4 :: (a, b, c, d) -> b
- Hledger.Utils: second5 :: () => (a, b, c, d, e) -> b
+ Hledger.Utils: second5 :: (a, b, c, d, e) -> b
- Hledger.Utils: second6 :: () => (a, b, c, d, e, f) -> b
+ Hledger.Utils: second6 :: (a, b, c, d, e, f) -> b
- Hledger.Utils: sixth6 :: () => (a, b, c, d, e, f) -> f
+ Hledger.Utils: sixth6 :: (a, b, c, d, e, f) -> f
- Hledger.Utils: third3 :: () => (a, b, c) -> c
+ Hledger.Utils: third3 :: (a, b, c) -> c
- Hledger.Utils: third4 :: () => (a, b, c, d) -> c
+ Hledger.Utils: third4 :: (a, b, c, d) -> c
- Hledger.Utils: third5 :: () => (a, b, c, d, e) -> c
+ Hledger.Utils: third5 :: (a, b, c, d, e) -> c
- Hledger.Utils: third6 :: () => (a, b, c, d, e, f) -> c
+ Hledger.Utils: third6 :: (a, b, c, d, e, f) -> c
- Hledger.Utils.String: takeEnd :: () => Int -> [a] -> [a]
+ Hledger.Utils.String: takeEnd :: Int -> [a] -> [a]
- Hledger.Utils.Test: withResource :: () => IO a -> (a -> IO ()) -> (IO a -> TestTree) -> TestTree
+ Hledger.Utils.Test: withResource :: IO a -> (a -> IO ()) -> (IO a -> TestTree) -> TestTree
- Text.Tabular.AsciiWide: doubleMidBar :: Bool -> String
+ Text.Tabular.AsciiWide: doubleMidBar :: Bool -> Bool -> String
- Text.Tabular.AsciiWide: leftBar :: Bool -> String
+ Text.Tabular.AsciiWide: leftBar :: Bool -> Bool -> String
- Text.Tabular.AsciiWide: midBar :: Bool -> String
+ Text.Tabular.AsciiWide: midBar :: Bool -> Bool -> String
- Text.Tabular.AsciiWide: renderColumns :: Bool -> [Int] -> Header String -> String
+ Text.Tabular.AsciiWide: renderColumns :: TableOpts -> [Int] -> Header Cell -> String
- Text.Tabular.AsciiWide: renderHLine :: VPos -> Bool -> [Int] -> Header String -> Properties -> [String]
+ Text.Tabular.AsciiWide: renderHLine :: VPos -> Bool -> Bool -> [Int] -> Header a -> Properties -> [String]
- Text.Tabular.AsciiWide: renderHLine' :: VPos -> Bool -> Properties -> [Int] -> Header String -> String
+ Text.Tabular.AsciiWide: renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> String
- Text.Tabular.AsciiWide: rightBar :: Bool -> String
+ Text.Tabular.AsciiWide: rightBar :: Bool -> Bool -> String

Files

CHANGES.md view
@@ -1,6 +1,130 @@ Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. +# 1.20 2020-12-05++++- added: journalApplyAliases, transactionApplyAliases, postingApplyAliases++- a new more robust price lookup implementation, fgl library dropped (#1402)++- Reverted a stripAnsi change in 1.19.1 that caused a 3x slowdown of amount rendering +  in terminal reports. (#1350)++- Amount and table rendering has been improved, so that stripAnsi is no longer needed.+  This speeds up amount rendering in the terminal, speeding up some reports by 10% or more since 1.19.+  (Stephen Morgan)++- global commodity display styles can now be set in InputOpts or Journal,+  overriding all others (declared or inferred). This is used by the import+  command and probably command-line options in future.++- Journal keeps a new piece of parsing state, a decimal mark character,+  which can optionally be set to force the number format expected by all+  amount parsers.++- Remove Empty Query constructor, which does nothing and has done so for a very long time. (Stephen Morgan)++- In ReportOpts, store query terms term-by-term in a list in querystring_. (Stephen Morgan)+  This helps deal with tricky quoting issues, as we no longer have to make+  sure everything is quoted properly before merging it into a string.++- Implement concat(Top|Bottom)Padded in terms of renderRow, allowing them to be width aware. (Stephen Morgan)++- Expand Tabular.AsciiWide to allow multiline, custom-width,+  vertically/horizontally-aligned cells, and optional table borders.+  (Stephen Morgan)++- Introduce showMixed*Unnormalised, eliminate most direct calls of strWidth. (Stephen Morgan)++- showMixedAmountElided now makes better use of space, showing as many+  Amounts possible as long as they and the elision string fit within+  32 characters. (Stephen Morgan)++- Add Functor instance for CompoundPeriodicReport. (Stephen Morgan)++- Generalise CBCSubreportSpec to allow more subreport control. (Stephen Morgan)++- Export some MultiBalanceReport helper functions. (Stephen Morgan)++- Make Default instances clearer, remove Default instance for Bool. (Stephen Morgan)++- Many ReportOpts-related changes, such as the addition of ReportSpec, aimed+  at preventing runtime errors (from parsing: regexps, dates, format strings;+  from not having today's date set; etc.)+  ReportSpec holds a ReportOpts, the day of the report, and the Query generated from these.++- StringFormat now takes an optional overline width, which is+  currently only used by defaultBalanceLineFormat. (Stephen Morgan)++- quoteIfNeeded should not escape the backslashes in unicode code points. (Stephen Morgan)++- Export OrdPlus and constructors. (Stephen Morgan)++- Debug output now uses pretty-simple instead pretty-show.+  This hopefully gives overall nicer debug output (eg in colour), +  including for values which don't have Read-able Show output.+  This means that we can start removing custom Show instances +  that were a workaround for pretty-show. Eg account names+  in debug output no longer show their colons as underscores.++  Here's some old pretty-show output:++   CsvRules+     { rdirectives = [ ( "skip" , "1" ) ]+     , rcsvfieldindexes = [ ( "date" , 1 ) , ( "amount" , 2 ) ]+     , rassignments = [ ( "amount" , "%2" ) , ( "date" , "%1" ) ]+     , rconditionalblocks = []+     }++  And the new pretty-simple output:++   CsvRules+     { rdirectives=+       [ ( "skip", "1" ) ]+     , rcsvfieldindexes=+       [ ( "date", 1 ), ( "amount", 2 ) ]+     , rassignments=+       [ ( "amount", "%2" ), ( "date", "%1" ) ]+     , rconditionalblocks= []+     }++  We require pretty-simple 4.0.0.0 to get this compact output.+  It's a little less compact than pretty-show, but not too bad.+  Non-compact pretty-simple output would be:++   CsvRules+       { rdirectives=+           [+               ( "skip"+               , "1B"+               )+           ]+       , rcsvfieldindexes=+           [+               ( "date"+               , 1+               )+           ,+               ( "amount"+               , 2+               )+           ]+       , rassignments=+           [+               ( "amount"+               , "%2"+               )+           ,+               ( "date"+               , "%1"+               )+           ]+       , rconditionalblocks=[]+       }++ # 1.19.1 2020-09-07  - Allow megaparsec 9
Hledger/Data/Account.hs view
@@ -14,7 +14,6 @@ import Data.Maybe (fromMaybe) import Data.Ord (Down(..)) import qualified Data.Map as M-import qualified Data.Text as T import Safe (headMay, lookupJustDef) import Text.Printf @@ -28,12 +27,11 @@ -- deriving instance Show Account instance Show Account where     show Account{..} = printf "Account %s (boring:%s, postings:%d, ebalance:%s, ibalance:%s)"-                       (T.map colonToUnderscore aname)  -- hide : so pretty-show doesn't break line+                       aname                        (if aboring then "y" else "n" :: String)                        anumpostings                        (showMixedAmount aebalance)                        (showMixedAmount aibalance)-      where colonToUnderscore x = if x == ':' then '_' else x  instance Eq Account where   (==) a b = aname a == aname b -- quick equality test for speed
Hledger/Data/Amount.hs view
@@ -69,6 +69,7 @@   amountstyle,   styleAmount,   styleAmountExceptPrecision,+  amountUnstyled,   showAmount,   cshowAmount,   showAmountWithZeroCommodity,@@ -108,6 +109,7 @@   mixedAmountTotalPriceToUnitPrice,   -- ** rendering   styleMixedAmount,+  mixedAmountUnstyled,   showMixedAmount,   showMixedAmountOneLine,   showMixedAmountDebug,@@ -116,6 +118,10 @@   showMixedAmountElided,   showMixedAmountWithZeroCommodity,   showMixedAmountWithPrecision,+  showMixed,+  showMixedUnnormalised,+  showMixedOneLine,+  showMixedOneLineUnnormalised,   setMixedAmountPrecision,   canonicaliseMixedAmount,   -- * misc.@@ -127,14 +133,15 @@ import Data.Char (isDigit) import Data.Decimal (decimalPlaces, normalizeDecimal, roundTo) import Data.Function (on)-import Data.List+import Data.List (genericSplitAt, groupBy, intercalate, mapAccumL,+                  partition, sortBy) import qualified Data.Map as M import Data.Map (findWithDefault)-import Data.Maybe+import Data.Maybe (fromMaybe) import qualified Data.Text as T import Data.Word (Word8)-import Safe (lastDef, maximumMay)-import Text.Printf+import Safe (lastDef, lastMay)+import Text.Printf (printf)  import Hledger.Data.Types import Hledger.Data.Commodity@@ -277,11 +284,6 @@ amountIsZero :: Amount -> Bool amountIsZero Amount{aquantity=q} = q == 0 --- | Get the string representation of an amount, based on its commodity's--- display settings except using the specified precision.-showAmountWithPrecision :: AmountPrecision -> Amount -> String-showAmountWithPrecision p = showAmount . setAmountPrecision p- -- | Set an amount's display precision, flipped. withPrecision :: Amount -> AmountPrecision -> Amount withPrecision = flip setAmountPrecision@@ -300,19 +302,6 @@     displayprecision = asprecision $ astyle a     naturalprecision = Precision . decimalPlaces . normalizeDecimal $ aquantity a --- | Get a string representation of an amount for debugging,--- appropriate to the current debug level. 9 shows maximum detail.-showAmountDebug :: Amount -> String-showAmountDebug Amount{acommodity="AUTO"} = "(missing)"-showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice=%s, astyle=%s}" (show acommodity) (show aquantity) (showAmountPriceDebug aprice) (show astyle)---- | Get the string representation of an amount, without any \@ price.--- With a True argument, adds ANSI codes to show negative amounts in red.-showAmountWithoutPrice :: Bool -> Amount -> String-showAmountWithoutPrice c a = showamt a{aprice=Nothing}-  where-    showamt = if c then cshowAmount else showAmount- -- | Set an amount's internal precision, ie rounds the Decimal representing -- the amount's quantity to some number of decimal places. -- Rounding is done with Data.Decimal's default roundTo function:@@ -364,6 +353,10 @@     Just s  -> a{astyle=s{asprecision=origp}}     Nothing -> a +-- | Reset this amount's display style to the default.+amountUnstyled :: Amount -> Amount+amountUnstyled a = a{astyle=amountstyle}+ -- | Get the string representation of an amount, based on its -- commodity's display settings. String representations equivalent to -- zero are converted to just \"0\". The special "missing" amount is@@ -374,10 +367,18 @@ -- | Colour version. For a negative amount, adds ANSI codes to change the colour, -- currently to hard-coded red. cshowAmount :: Amount -> String-cshowAmount a =-  (if isNegativeAmount a then color Dull Red else id) $-  showAmountHelper False a+cshowAmount a = (if isNegativeAmount a then color Dull Red else id) $+                showAmountHelper False a +-- | Get the string representation of an amount, without any \@ price.+showAmountWithoutPrice :: Amount -> String+showAmountWithoutPrice a = showAmount a{aprice=Nothing}++-- | Get the string representation of an amount, based on its commodity's+-- display settings except using the specified precision.+showAmountWithPrecision :: AmountPrecision -> Amount -> String+showAmountWithPrecision p = showAmount . setAmountPrecision p+ showAmountHelper :: Bool -> Amount -> String showAmountHelper _ Amount{acommodity="AUTO"} = "" showAmountHelper showzerocommodity a@Amount{acommodity=c, aprice=mp, astyle=AmountStyle{..}} =@@ -395,6 +396,12 @@ showAmountWithZeroCommodity :: Amount -> String showAmountWithZeroCommodity = showAmountHelper True +-- | Get a string representation of an amount for debugging,+-- appropriate to the current debug level. 9 shows maximum detail.+showAmountDebug :: Amount -> String+showAmountDebug Amount{acommodity="AUTO"} = "(missing)"+showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice=%s, astyle=%s}" (show acommodity) (show aquantity) (showAmountPriceDebug aprice) (show astyle)+ -- | Get the string representation of the number part of of an amount, -- using the display settings from its commodity. showamountquantity :: Amount -> String@@ -608,95 +615,174 @@ styleMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount styleMixedAmount styles (Mixed as) = Mixed $ map (styleAmount styles) as +-- | Reset each individual amount's display style to the default.+mixedAmountUnstyled :: MixedAmount -> MixedAmount+mixedAmountUnstyled = mapMixedAmount amountUnstyled+ -- | Get the string representation of a mixed amount, after -- normalising it to one amount per commodity. Assumes amounts have -- no or similar prices, otherwise this can show misleading prices. showMixedAmount :: MixedAmount -> String-showMixedAmount = showMixedAmountHelper False False---- | Like showMixedAmount, but zero amounts are shown with their--- commodity if they have one.-showMixedAmountWithZeroCommodity :: MixedAmount -> String-showMixedAmountWithZeroCommodity = showMixedAmountHelper True False+showMixedAmount = fst . showMixed showAmount Nothing Nothing False  -- | Get the one-line string representation of a mixed amount. showMixedAmountOneLine :: MixedAmount -> String-showMixedAmountOneLine = showMixedAmountHelper False True--showMixedAmountHelper :: Bool -> Bool -> MixedAmount -> String-showMixedAmountHelper showzerocommodity useoneline m =-  join $ map showamt $ amounts $ normaliseMixedAmountSquashPricesForDisplay m-  where-    join | useoneline = intercalate ", "-         | otherwise  = vConcatRightAligned-    showamt | showzerocommodity = showAmountWithZeroCommodity-            | otherwise         = showAmount---- | Compact labelled trace of a mixed amount, for debugging.-ltraceamount :: String -> MixedAmount -> MixedAmount-ltraceamount s = traceWith (((s ++ ": ") ++).showMixedAmount)+showMixedAmountOneLine = fst . showMixedOneLine showAmountWithoutPrice Nothing Nothing False --- | Set the display precision in the amount's commodities.-setMixedAmountPrecision :: AmountPrecision -> MixedAmount -> MixedAmount-setMixedAmountPrecision p (Mixed as) = Mixed $ map (setAmountPrecision p) as+-- | Like showMixedAmount, but zero amounts are shown with their+-- commodity if they have one.+showMixedAmountWithZeroCommodity :: MixedAmount -> String+showMixedAmountWithZeroCommodity = fst . showMixed showAmountWithZeroCommodity Nothing Nothing False  -- | Get the string representation of a mixed amount, showing each of its -- component amounts with the specified precision, ignoring their -- commoditys' display precision settings. showMixedAmountWithPrecision :: AmountPrecision -> MixedAmount -> String-showMixedAmountWithPrecision p m =-    vConcatRightAligned $ map (showAmountWithPrecision p) $ amounts $ normaliseMixedAmountSquashPricesForDisplay m+showMixedAmountWithPrecision p = fst . showMixed (showAmountWithPrecision p) Nothing Nothing False +-- | Get the string representation of a mixed amount, without showing any transaction prices.+-- With a True argument, adds ANSI codes to show negative amounts in red.+showMixedAmountWithoutPrice :: Bool -> MixedAmount -> String+showMixedAmountWithoutPrice c = fst . showMixed showAmountWithoutPrice Nothing Nothing c++-- | Get the one-line string representation of a mixed amount, but without+-- any \@ prices.+-- With a True argument, adds ANSI codes to show negative amounts in red.+showMixedAmountOneLineWithoutPrice :: Bool -> MixedAmount -> String+showMixedAmountOneLineWithoutPrice c = fst . showMixedOneLine showAmountWithoutPrice Nothing Nothing c++-- | Like showMixedAmountOneLineWithoutPrice, but show at most the given width,+-- with an elision indicator if there are more.+-- With a True argument, adds ANSI codes to show negative amounts in red.+showMixedAmountElided :: Int -> Bool -> MixedAmount -> String+showMixedAmountElided w c = fst . showMixedOneLine showAmountWithoutPrice Nothing (Just w) c+ -- | Get an unambiguous string representation of a mixed amount for debugging. showMixedAmountDebug :: MixedAmount -> String showMixedAmountDebug m | m == missingmixedamt = "(missing)"                        | otherwise       = printf "Mixed [%s]" as     where as = intercalate "\n       " $ map showAmountDebug $ amounts m --- TODO these and related fns are comically complicated:+-- | General function to display a MixedAmount, one Amount on each line.+-- It takes a function to display each Amount, an optional minimum width+-- to pad to, an optional maximum width to display, and a Bool to determine+-- whether to colourise negative numbers. Amounts longer than the maximum+-- width (if given) will be elided. The function also returns the actual+-- width of the output string.+showMixed :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)+showMixed showamt mmin mmax c =+    showMixedUnnormalised showamt mmin mmax c . normaliseMixedAmountSquashPricesForDisplay --- | Get the string representation of a mixed amount, without showing any transaction prices.--- With a True argument, adds ANSI codes to show negative amounts in red.-showMixedAmountWithoutPrice :: Bool -> MixedAmount -> String-showMixedAmountWithoutPrice c m = intercalate "\n" $ map showamt as+-- | Like showMixed, but does not normalise the MixedAmount before displaying.+showMixedUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)+showMixedUnnormalised showamt mmin mmax c (Mixed as) =+    (intercalate "\n" $ map finalise elided, width)   where-    Mixed as = normaliseMixedAmountSquashPricesForDisplay $ mixedAmountStripPrices m-    showamt a =-      (if c && isNegativeAmount a then color Dull Red else id) $-      printf (printf "%%%ds" width) $ showAmountWithoutPrice c a+    width    = maximum $ fromMaybe 0 mmin : map adLength elided+    astrs    = amtDisplayList sepwidth showamt as+    sepwidth = 0  -- "\n" has width 0++    finalise = adString . pad . if c then colourise else id+    pad amt = amt{ adString = applyN (width - adLength amt) (' ':) $ adString amt+                 , adLength = width+                 }++    elided = maybe id elideTo mmax astrs+    elideTo m xs = maybeAppend elisionStr short       where-        width = fromMaybe 0 . maximumMay $ map (length . showAmount) as+        elisionStr = elisionDisplay (Just m) sepwidth (length long) $ lastDef nullAmountDisplay short+        (short, long) = partition ((m>=) . adLength) xs -mixedAmountStripPrices :: MixedAmount -> MixedAmount-mixedAmountStripPrices (Mixed as) = Mixed $ map (\a -> a{aprice=Nothing}) as+-- | General function to display a MixedAmount on a single line. It+-- takes a function to display each Amount, an optional minimum width to+-- pad to, an optional maximum width to display, and a Bool to determine+-- whether to colourise negative numbers. It will display as many Amounts+-- as it can in the maximum width (if given), and further Amounts will be+-- elided. The function also returns the actual width of the output string.+showMixedOneLine :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)+showMixedOneLine showamt mmin mmax c =+    showMixedOneLineUnnormalised showamt mmin mmax c . normaliseMixedAmountSquashPricesForDisplay --- | Get the one-line string representation of a mixed amount, but without--- any \@ prices.--- With a True argument, adds ANSI codes to show negative amounts in red.-showMixedAmountOneLineWithoutPrice :: Bool -> MixedAmount -> String-showMixedAmountOneLineWithoutPrice c m =-  intercalate ", " $ map (showAmountWithoutPrice c) as-    where-      (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m-      stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=Nothing}+-- | Like showMixedOneLine, but does not normalise the MixedAmount before+-- displaying.+showMixedOneLineUnnormalised :: (Amount -> String) -> Maybe Int -> Maybe Int -> Bool -> MixedAmount -> (String, Int)+showMixedOneLineUnnormalised showamt mmin mmax c (Mixed as) =+    (pad . intercalate ", " $ map finalise elided, max width $ fromMaybe 0 mmin)+  where+    width    = maybe 0 adTotal $ lastMay elided+    astrs    = amtDisplayList sepwidth showamt as+    sepwidth = 2  -- ", " has width 2+    n        = length as --- | Like showMixedAmountOneLineWithoutPrice, but show at most two commodities,--- with a elision indicator if there are more.--- With a True argument, adds ANSI codes to show negative amounts in red.-showMixedAmountElided :: Bool -> MixedAmount -> String-showMixedAmountElided c m = intercalate ", " $ take 2 astrs ++ elisionstr+    finalise = adString . if c then colourise else id+    pad = applyN (fromMaybe 0 mmin - width) (' ':)++    elided = maybe id elideTo mmax astrs+    elideTo m = addElide . takeFitting m . withElided+    -- Add the last elision string to the end of the display list+    addElide [] = []+    addElide xs = maybeAppend (snd $ last xs) $ map fst xs+    -- Return the elements of the display list which fit within the maximum width+    -- (including their elision strings)+    takeFitting m = dropWhileRev (\(a,e) -> m < adTotal (fromMaybe a e))+    dropWhileRev p = foldr (\x xs -> if null xs && p x then [] else x:xs) []++    -- Add the elision strings (if any) to each amount+    withElided = zipWith (\num amt -> (amt, elisionDisplay Nothing sepwidth num amt)) [n-1,n-2..0]++data AmountDisplay = AmountDisplay+  { adAmount :: !Amount  -- ^ Amount displayed+  , adString :: !String  -- ^ String representation of the Amount+  , adLength :: !Int     -- ^ Length of the string representation+  , adTotal  :: !Int     -- ^ Cumulative length of MixedAmount this Amount is part of,+                         --   including separators+  } deriving (Show)++nullAmountDisplay :: AmountDisplay+nullAmountDisplay = AmountDisplay nullamt "" 0 0++amtDisplayList :: Int -> (Amount -> String) -> [Amount] -> [AmountDisplay]+amtDisplayList sep showamt = snd . mapAccumL display (-sep)   where-    astrs = map (showAmountWithoutPrice c) as-      where-        (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m-          where-            stripPrices (Mixed as) = Mixed $ map stripprice as-              where-                stripprice a = a{aprice=Nothing}-    elisionstr | n > 2     = [show (n - 2) ++ " more.."]-               | otherwise = []+    display tot amt = (tot', AmountDisplay amt str width tot')       where-        n = length astrs+        str  = showamt amt+        width  = strWidth str+        tot' = tot + width + sep++-- The string "m more", added to the previous running total+elisionDisplay :: Maybe Int -> Int -> Int -> AmountDisplay -> Maybe AmountDisplay+elisionDisplay mmax sep n lastAmt+  | n > 0     = Just $ AmountDisplay 0 str len (adTotal lastAmt + len)+  | otherwise = Nothing+  where+    fullString = show n ++ " more.."+    -- sep from the separator, 7 from " more..", 1 + floor (logBase 10 n) from number+    fullLength = sep + 8 + floor (logBase 10 $ fromIntegral n)++    str | Just m <- mmax, fullLength > m = take (m - 2) fullString ++ ".."+        | otherwise                      = fullString+    len = case mmax of Nothing -> fullLength+                       Just m  -> max 2 $ min m fullLength++maybeAppend :: Maybe a -> [a] -> [a]+maybeAppend Nothing  = id+maybeAppend (Just a) = (++[a])++colourise :: AmountDisplay -> AmountDisplay+colourise amt = amt{adString=markColour $ adString amt}+  where markColour = if isNegativeAmount (adAmount amt) then color Dull Red else id++-- | Compact labelled trace of a mixed amount, for debugging.+ltraceamount :: String -> MixedAmount -> MixedAmount+ltraceamount s = traceWith (((s ++ ": ") ++).showMixedAmount)++-- | Set the display precision in the amount's commodities.+setMixedAmountPrecision :: AmountPrecision -> MixedAmount -> MixedAmount+setMixedAmountPrecision p (Mixed as) = Mixed $ map (setAmountPrecision p) as++mixedAmountStripPrices :: MixedAmount -> MixedAmount+mixedAmountStripPrices (Mixed as) = Mixed $ map (\a -> a{aprice=Nothing}) as  -- | Canonicalise a mixed amount's display styles using the provided commodity style map. canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
Hledger/Data/Dates.hs view
@@ -1,9 +1,10 @@-{-# LANGUAGE NoMonoLocalBinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts    #-}+{-# LANGUAGE NoMonoLocalBinds    #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE PackageImports      #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TupleSections       #-}+{-# LANGUAGE TypeFamilies        #-} {-|  Date parsing and utilities for hledger.@@ -110,7 +111,6 @@ -- Help ppShow parse and line-wrap DateSpans better in debug output. instance Show DateSpan where     show s = "DateSpan " ++ showDateSpan s-    -- show s = "DateSpan \"" ++ showDateSpan s ++ "\"" -- quotes to help pretty-show  showDate :: Day -> String showDate = show@@ -777,10 +777,17 @@   failIfInvalidDate date  ymd :: TextParser m SmartDate-ymd = liftA2 SmartAssumeStart yearp (optional $ try monthday) >>= failIfInvalidDate-  where monthday = do-            sep <- datesepchar-            liftA2 (,) decimal . optional $ char sep *> decimal+ymd = do+    y <- yearp+    fmap (SmartAssumeStart y) . optional . try $ do+        sep <- datesepchar+        m <- decimal+        unless (validMonth m) $ Fail.fail ("Bad month " <> show m)+        fmap (m,) . optional . try $ do+            _ <- char sep+            d <- decimal+            failIfInvalidDate $ SmartAssumeStart y (Just (m, Just d))+            return d  md :: TextParser m (Month, MonthDay) md = do@@ -943,6 +950,14 @@ -- | -- >>> parsewith (doubledatespanp (fromGregorian 2018 01 01) <* eof) "20180101-201804" -- Right DateSpan 2018Q1+-- >>> parsewith (doubledatespanp (fromGregorian 2018 01 01) <* eof) "2017..2018"+-- Right DateSpan 2017+-- >>> parsewith (doubledatespanp (fromGregorian 2018 01 01) <* eof) "2017-2018"+-- Right DateSpan 2017+-- >>> parsewith (doubledatespanp (fromGregorian 2018 01 01) <* eof) "2017-01-2018"+-- Right DateSpan 2017+-- >>> parsewith (doubledatespanp (fromGregorian 2018 01 01) <* eof) "2017-01-01-2018"+-- Right DateSpan 2017 doubledatespanp :: Day -> TextParser m DateSpan doubledatespanp rdate = liftA2 fromToSpan     (optional (string' "from" *> skipNonNewlineSpaces) *> smartdate)
Hledger/Data/Journal.hs view
@@ -52,6 +52,7 @@   -- overJournalAmounts,   -- traverseJournalAmounts,   -- journalCanonicalCommodities,+  journalCommoditiesDeclared,   journalDateSpan,   journalStartDate,   journalEndDate,@@ -79,6 +80,7 @@   journalNumberAndTieTransactions,   journalUntieTransactions,   journalModifyTransactions,+  journalApplyAliases,   -- * Tests   samplejournal,   tests_Journal,@@ -175,6 +177,7 @@   j1 <> j2 = Journal {      jparsedefaultyear          = jparsedefaultyear          j2     ,jparsedefaultcommodity     = jparsedefaultcommodity     j2+    ,jparsedecimalmark          = jparsedecimalmark          j2     ,jparseparentaccounts       = jparseparentaccounts       j2     ,jparsealiases              = jparsealiases              j2     -- ,jparsetransactioncount     = jparsetransactioncount     j1 +  jparsetransactioncount     j2@@ -182,6 +185,7 @@     ,jincludefilestack          = jincludefilestack j2     ,jdeclaredaccounts          = jdeclaredaccounts          j1 <> jdeclaredaccounts          j2     ,jdeclaredaccounttypes      = jdeclaredaccounttypes      j1 <> jdeclaredaccounttypes      j2+    ,jglobalcommoditystyles     = jglobalcommoditystyles     j1 <> jglobalcommoditystyles     j2     ,jcommodities               = jcommodities               j1 <> jcommodities               j2     ,jinferredcommodities       = jinferredcommodities       j1 <> jinferredcommodities       j2     ,jpricedirectives           = jpricedirectives           j1 <> jpricedirectives           j2@@ -201,6 +205,7 @@ nulljournal = Journal {    jparsedefaultyear          = Nothing   ,jparsedefaultcommodity     = Nothing+  ,jparsedecimalmark          = Nothing   ,jparseparentaccounts       = []   ,jparsealiases              = []   -- ,jparsetransactioncount     = 0@@ -208,6 +213,7 @@   ,jincludefilestack          = []   ,jdeclaredaccounts          = []   ,jdeclaredaccounttypes      = M.empty+  ,jglobalcommoditystyles     = M.empty   ,jcommodities               = M.empty   ,jinferredcommodities       = M.empty   ,jpricedirectives           = []@@ -263,6 +269,10 @@ journalPostings :: Journal -> [Posting] journalPostings = concatMap tpostings . jtxns +-- | Sorted unique commodity symbols declared by commodity directives in this journal.+journalCommoditiesDeclared :: Journal -> [AccountName]+journalCommoditiesDeclared = nubSort . M.keys . jcommodities+ -- | Sorted unique account names posted to by this journal's transactions. journalAccountNamesUsed :: Journal -> [AccountName] journalAccountNamesUsed = accountNamesFromPostings . journalPostings@@ -562,7 +572,7 @@     ,jtxns             = reverse $ jtxns j     ,jtxnmodifiers     = reverse $ jtxnmodifiers j     ,jperiodictxns     = reverse $ jperiodictxns j-    ,jpricedirectives     = reverse $ jpricedirectives j+    ,jpricedirectives  = reverse $ jpricedirectives j     }  -- | Set this journal's last read time, ie when its files were last read.@@ -608,7 +618,7 @@ -- WARN tricky algorithm and code ahead.  -- -- Code overview as of 20190219, this could/should be simplified/documented more:---  parseAndFinaliseJournal['] (Cli/Utils.hs), journalAddForecast (Common.hs), budgetJournal (BudgetReport.hs), tests (BalanceReport.hs)+--  parseAndFinaliseJournal['] (Cli/Utils.hs), journalAddForecast (Common.hs), journalAddBudgetGoalTransactions (BudgetReport.hs), tests (BalanceReport.hs) --   journalBalanceTransactions --    runST --     runExceptT@@ -938,10 +948,8 @@ --  -- | Choose and apply a consistent display style to the posting--- amounts in each commodity. Each commodity's style is specified by a--- commodity (or D) directive, or otherwise inferred from posting--- amounts. Can return an error message eg if inconsistent number--- formats are found.+-- amounts in each commodity (see journalCommodityStyles).+-- Can return an error message eg if inconsistent number formats are found. journalApplyCommodityStyles :: Journal -> Either String Journal journalApplyCommodityStyles j@Journal{jtxns=ts, jpricedirectives=pds} =   case journalInferCommodityStyles j of@@ -958,18 +966,20 @@         fixbalanceassertion ba = ba{baamount=styleAmount styles $ baamount ba}         fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a} --- | Get the canonical amount styles for this journal, whether--- declared by commodity directives, by the last default commodity (D)--- directive, or inferred from posting amounts, as a map from symbol--- to style. Styles declared by directives take precedence (and--- commodity takes precedence over D). Styles from directives are--- guaranteed to specify the decimal mark character.+-- | Get the canonical amount styles for this journal, whether (in order of precedence):+-- set globally in InputOpts,+-- declared by commodity directives, +-- declared by a default commodity (D) directive, +-- or inferred from posting amounts, +-- as a map from symbol to style. +-- Styles from directives are assumed to specify the decimal mark. journalCommodityStyles :: Journal -> M.Map CommoditySymbol AmountStyle journalCommodityStyles j =   -- XXX could be some redundancy here, cf journalStyleInfluencingAmounts-  commoditystyles <> defaultcommoditystyle <> inferredstyles+  globalstyles <> declaredstyles <> defaultcommoditystyle <> inferredstyles   where-    commoditystyles       = M.mapMaybe cformat $ jcommodities j+    globalstyles          = jglobalcommoditystyles j+    declaredstyles        = M.mapMaybe cformat $ jcommodities j     defaultcommoditystyle = M.fromList $ catMaybes [jparsedefaultcommodity j]     inferredstyles        = jinferredcommodities j @@ -978,14 +988,12 @@ -- "the format of the first amount, adjusted to the highest precision of all amounts". -- Can return an error message eg if inconsistent number formats are found. journalInferCommodityStyles :: Journal -> Either String Journal-journalInferCommodityStyles j =+journalInferCommodityStyles j =    case-    commodityStylesFromAmounts $-    dbg7 "journalInferCommodityStyles using amounts" $-    journalStyleInfluencingAmounts j+    commodityStylesFromAmounts $ journalStyleInfluencingAmounts j   of     Left e   -> Left e-    Right cs -> Right j{jinferredcommodities = cs}+    Right cs -> Right j{jinferredcommodities = dbg7 "journalInferCommodityStyles" cs}  -- | Given a list of amounts, in parse order (roughly speaking; see journalStyleInfluencingAmounts), -- build a map from their commodity names to standard commodity@@ -1004,10 +1012,14 @@     commamts = groupSort [(acommodity as, as) | as <- amts]     commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts] --- TODO: should probably detect and report inconsistencies here+-- TODO: should probably detect and report inconsistencies here.+-- Though, we don't have the info for a good error message, so maybe elsewhere. -- | Given a list of amount styles (assumed to be from parsed amounts -- in a single commodity), in parse order, choose a canonical style.--- Traditionally it's "the style of the first, with the maximum precision of all".+-- This is:+-- the general style of the first amount, +-- with the first digit group style seen,+-- with the maximum precision of all. -- canonicalStyleFrom :: [AmountStyle] -> AmountStyle canonicalStyleFrom [] = amountstyle@@ -1114,7 +1126,9 @@ -- Transaction price amounts (posting amounts' aprice field) are not included. -- journalStyleInfluencingAmounts :: Journal -> [Amount]-journalStyleInfluencingAmounts j = catMaybes $ concat [+journalStyleInfluencingAmounts j = +  dbg7 "journalStyleInfluencingAmounts" $+  catMaybes $ concat [    [mdefaultcommodityamt]   ,map (Just . pdamount) $ jpricedirectives j   ,map Just $ concatMap amounts $ map pamount $ journalPostings j@@ -1218,6 +1232,16 @@ postingFindTag :: TagName -> Posting -> Maybe (TagName, TagValue) postingFindTag tagname p = find ((tagname==) . fst) $ postingAllTags p +-- | Apply some account aliases to all posting account names in the journal, as described by accountNameApplyAliases.+-- This can fail due to a bad replacement pattern in a regular expression alias.+journalApplyAliases :: [AccountAlias] -> Journal -> Either RegexError Journal+-- short circuit the common case, just in case there's a performance impact from txnTieKnot etc.+journalApplyAliases [] j = Right j+journalApplyAliases aliases j = +  case mapM (transactionApplyAliases aliases) $ jtxns j of+    Right ts -> Right j{jtxns = ts}+    Left err -> Left err+   -- -- | Build a database of market prices in effect on the given date, -- -- from the journal's price directives. -- journalPrices :: Day -> Journal -> Prices
Hledger/Data/Posting.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE NamedFieldPuns #-} {-|  A 'Posting' represents a change (by some 'MixedAmount') of the balance in@@ -37,6 +38,7 @@   transactionAllTags,   relatedPostings,   removePrices,+  postingApplyAliases,   -- * date operations   postingDate,   postingDate2,@@ -168,7 +170,7 @@                           BalancedVirtualPosting -> (\s -> "["++s++"]", acctnamewidth-2)                           VirtualPosting -> (\s -> "("++s++")", acctnamewidth-2)                           _ -> (id,acctnamewidth)-      showamount = padLeftWide 12 . showMixedAmount+      showamount = fst . showMixed showAmount (Just 12) Nothing False   showComment :: Text -> String@@ -287,6 +289,17 @@ concatAccountNames :: [AccountName] -> AccountName concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map accountNameWithoutPostingType as     where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as++-- | Apply some account aliases to the posting's account name, as described by accountNameApplyAliases.+-- This can fail due to a bad replacement pattern in a regular expression alias.+postingApplyAliases :: [AccountAlias] -> Posting -> Either RegexError Posting+postingApplyAliases aliases p@Posting{paccount} =+  case accountNameApplyAliases aliases paccount of+    Right a -> Right p{paccount=a}+    Left e  -> Left err+      where+        err = "problem while applying account aliases:\n" ++ pshow aliases +          ++ "\n to account name: "++T.unpack paccount++"\n "++e  -- | Rewrite an account name using all matching aliases from the given list, in sequence. -- Each alias sees the result of applying the previous aliases.
Hledger/Data/RawOptions.hs view
@@ -11,6 +11,7 @@   RawOpts,   setopt,   setboolopt,+  appendopts,   inRawOpts,   boolopt,   choiceopt,@@ -22,7 +23,8 @@   posintopt,   maybeintopt,   maybeposintopt,-  maybecharopt+  maybecharopt,+  overRawOpts ) where @@ -46,6 +48,9 @@  setboolopt :: String -> RawOpts -> RawOpts setboolopt name = overRawOpts (++ [(name,"")])++appendopts :: [(String,String)] -> RawOpts -> RawOpts+appendopts new = overRawOpts $ \old -> concat [old,new]  -- | Is the named option present ? inRawOpts :: String -> RawOpts -> Bool
Hledger/Data/StringFormat.hs view
@@ -10,27 +10,33 @@         , StringFormat(..)         , StringFormatComponent(..)         , ReportItemField(..)+        , overlineWidth+        , defaultBalanceLineFormat         , tests_StringFormat         ) where  import Prelude () import "base-compat-batteries" Prelude.Compat-import Numeric+import Numeric (readDec) import Data.Char (isPrint)-import Data.Maybe+import Data.Default (Default(..))+import Data.Maybe (isJust) -- import qualified Data.Text as T import Text.Megaparsec-import Text.Megaparsec.Char+import Text.Megaparsec.Char (char, digitChar, string) -import Hledger.Utils.Parse+import Hledger.Utils.Parse (SimpleStringParser) import Hledger.Utils.String (formatString) import Hledger.Utils.Test  -- | A format specification/template to use when rendering a report line item as text. ----- A format is a sequence of components; each is either a literal--- string, or a hledger report item field with specified width and--- justification whose value will be interpolated at render time.+-- A format is an optional width, along with a sequence of components;+-- each is either a literal string, or a hledger report item field with+-- specified width and justification whose value will be interpolated+-- at render time. The optional width determines the length of the+-- overline to draw above the totals row; if it is Nothing, then the+-- maximum width of all lines is used. -- -- A component's value may be a multi-line string (or a -- multi-commodity amount), in which case the final string will be@@ -41,9 +47,9 @@ -- mode, which provides a limited StringFormat renderer. -- data StringFormat =-    OneLine [StringFormatComponent]       -- ^ multi-line values will be rendered on one line, comma-separated-  | TopAligned [StringFormatComponent]    -- ^ values will be top-aligned (and bottom-padded to the same height)-  | BottomAligned [StringFormatComponent] -- ^ values will be bottom-aligned (and top-padded)+    OneLine       (Maybe Int) [StringFormatComponent] -- ^ multi-line values will be rendered on one line, comma-separated+  | TopAligned    (Maybe Int) [StringFormatComponent] -- ^ values will be top-aligned (and bottom-padded to the same height)+  | BottomAligned (Maybe Int) [StringFormatComponent] -- ^ values will be bottom-aligned (and top-padded)   deriving (Show, Eq)  data StringFormatComponent =@@ -73,6 +79,21 @@   | FieldNo Int       -- ^ A report item's nth field. May be unimplemented.     deriving (Show, Eq) +instance Default StringFormat where def = defaultBalanceLineFormat++overlineWidth :: StringFormat -> Maybe Int+overlineWidth (OneLine       w _) = w+overlineWidth (TopAligned    w _) = w+overlineWidth (BottomAligned w _) = w++-- | Default line format for balance report: "%20(total)  %2(depth_spacer)%-(account)"+defaultBalanceLineFormat :: StringFormat+defaultBalanceLineFormat = BottomAligned (Just 20) [+      FormatField False (Just 20) Nothing TotalField+    , FormatLiteral "  "+    , FormatField True (Just 2) Nothing DepthSpacerField+    , FormatField True Nothing Nothing AccountField+    ] ----------------------------------------------------------------------  -- renderStringFormat :: StringFormat -> Map String String -> String@@ -93,10 +114,10 @@   alignspec <- optional (try $ char '%' >> oneOf ("^_,"::String))   let constructor =         case alignspec of-          Just '^' -> TopAligned-          Just '_' -> BottomAligned-          Just ',' -> OneLine-          _        -> defaultStringFormatStyle+          Just '^' -> TopAligned Nothing+          Just '_' -> BottomAligned Nothing+          Just ',' -> OneLine Nothing+          _        -> defaultStringFormatStyle Nothing   constructor <$> many componentp  componentp :: SimpleStringParser StringFormatComponent@@ -157,23 +178,23 @@    ,let s `gives` expected = test s $ parseStringFormat s @?= Right expected    in tests "parseStringFormat" [-      ""                           `gives` (defaultStringFormatStyle [])-    , "D"                          `gives` (defaultStringFormatStyle [FormatLiteral "D"])-    , "%(date)"                    `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing DescriptionField])-    , "%(total)"                   `gives` (defaultStringFormatStyle [FormatField False Nothing Nothing TotalField])+      ""                           `gives` (defaultStringFormatStyle Nothing [])+    , "D"                          `gives` (defaultStringFormatStyle Nothing [FormatLiteral "D"])+    , "%(date)"                    `gives` (defaultStringFormatStyle Nothing [FormatField False Nothing Nothing DescriptionField])+    , "%(total)"                   `gives` (defaultStringFormatStyle Nothing [FormatField False Nothing Nothing TotalField])     -- TODO     -- , "^%(total)"                  `gives` (TopAligned [FormatField False Nothing Nothing TotalField])     -- , "_%(total)"                  `gives` (BottomAligned [FormatField False Nothing Nothing TotalField])     -- , ",%(total)"                  `gives` (OneLine [FormatField False Nothing Nothing TotalField])-    , "Hello %(date)!"             `gives` (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])-    , "%-(date)"                   `gives` (defaultStringFormatStyle [FormatField True Nothing Nothing DescriptionField])-    , "%20(date)"                  `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing DescriptionField])-    , "%.10(date)"                 `gives` (defaultStringFormatStyle [FormatField False Nothing (Just 10) DescriptionField])-    , "%20.10(date)"               `gives` (defaultStringFormatStyle [FormatField False (Just 20) (Just 10) DescriptionField])-    , "%20(account) %.10(total)"   `gives` (defaultStringFormatStyle [FormatField False (Just 20) Nothing AccountField-                                                                     ,FormatLiteral " "-                                                                     ,FormatField False Nothing (Just 10) TotalField-                                                                     ])+    , "Hello %(date)!"             `gives` (defaultStringFormatStyle Nothing [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])+    , "%-(date)"                   `gives` (defaultStringFormatStyle Nothing [FormatField True Nothing Nothing DescriptionField])+    , "%20(date)"                  `gives` (defaultStringFormatStyle Nothing [FormatField False (Just 20) Nothing DescriptionField])+    , "%.10(date)"                 `gives` (defaultStringFormatStyle Nothing [FormatField False Nothing (Just 10) DescriptionField])+    , "%20.10(date)"               `gives` (defaultStringFormatStyle Nothing [FormatField False (Just 20) (Just 10) DescriptionField])+    , "%20(account) %.10(total)"   `gives` (defaultStringFormatStyle Nothing [FormatField False (Just 20) Nothing AccountField+                                                                             ,FormatLiteral " "+                                                                             ,FormatField False Nothing (Just 10) TotalField+                                                                             ])     , test "newline not parsed" $ assertLeft $ parseStringFormat "\n"     ]  ]
Hledger/Data/Transaction.hs view
@@ -33,6 +33,7 @@   transactionTransformPostings,   transactionApplyValuation,   transactionToCost,+  transactionApplyAliases,   -- nonzerobalanceerror,   -- * date operations   transactionDate2,@@ -256,11 +257,11 @@     -- currently prices are considered part of the amount string when right-aligning amounts     shownAmounts       | elideamount    = [""]-      | onelineamounts = [fitString (Just amtwidth) Nothing False False $ showMixedAmountOneLine $ pamount p]+      | onelineamounts = [fst . showMixedOneLineUnnormalised showAmount (Just amtwidth) Nothing False $ pamount p]       | null (amounts $ pamount p) = [""]-      | otherwise      = map (fitStringMulti (Just amtwidth) Nothing False False . showAmount ) . amounts $ pamount p+      | otherwise      = lines . fst . showMixedUnnormalised showAmount (Just amtwidth) Nothing False $ pamount p       where-        amtwidth = maximum $ 12 : map (strWidth . showMixedAmount . pamount) pstoalignwith  -- min. 12 for backwards compatibility+        amtwidth = maximum $ 12 : map (snd . showMixedUnnormalised showAmount Nothing Nothing False . pamount) pstoalignwith  -- min. 12 for backwards compatibility      (samelinecomment, newlinecomments) =       case renderCommentLines (pcomment p) of []   -> ("",[])@@ -590,6 +591,14 @@ -- | Convert this transaction's amounts to cost, and apply the appropriate amount styles. transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction transactionToCost styles t@Transaction{tpostings=ps} = t{tpostings=map (postingToCost styles) ps}++-- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases.+-- This can fail due to a bad replacement pattern in a regular expression alias.+transactionApplyAliases :: [AccountAlias] -> Transaction -> Either RegexError Transaction+transactionApplyAliases aliases t =+  case mapM (postingApplyAliases aliases) $ tpostings t of+    Right ps -> Right $ txnTieKnot $ t{tpostings=ps}+    Left err -> Left err  -- tests 
Hledger/Data/Types.hs view
@@ -89,7 +89,7 @@ type WeekDay = Int   -- 1-7  -- Typical report periods (spans of time), both finite and open-ended.--- A richer abstraction than DateSpan.+-- A higher-level abstraction than DateSpan. data Period =     DayPeriod Day   | WeekPeriod Day@@ -159,6 +159,12 @@  data Side = L | R deriving (Eq,Show,Read,Ord,Generic) +-- | One of the decimal marks we support: either period or comma.+type DecimalMark = Char++isDecimalMark :: Char -> Bool+isDecimalMark c = c == '.' || c == ','+ -- | The basic numeric type used in amounts. type Quantity = Decimal -- The following is for hledger-web, and requires blaze-markup.@@ -440,6 +446,7 @@   -- 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+  ,jparsedecimalmark      :: Maybe DecimalMark                     -- ^ the character to always parse as decimal point, if set by CsvReader's decimal-mark (or a future journal 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)@@ -448,8 +455,9 @@   -- principal data   ,jdeclaredaccounts      :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)   ,jdeclaredaccounttypes  :: M.Map AccountType [AccountName]        -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts)+  ,jglobalcommoditystyles :: M.Map CommoditySymbol AmountStyle      -- ^ per-commodity display styles declared globally, eg by command line option or import command   ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives-  ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts  TODO misnamed, should be eg jusedstyles+  ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts   ,jpricedirectives       :: [PriceDirective]                       -- ^ Declarations of market prices by P directives, in parse order (after journal finalisation)   ,jinferredmarketprices  :: [MarketPrice]                          -- ^ Market prices implied by transactions, in parse order (after journal finalisation)   ,jtxnmodifiers          :: [TransactionModifier]@@ -459,6 +467,8 @@   ,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.+                                                                    --   TODO: FilePath is a sloppy type here, don't assume it's a+                                                                    --   real file; values like "", "-", "(string)" can be seen   ,jlastreadtime          :: ClockTime                              -- ^ when this journal was last read from its file(s)   } deriving (Eq, Generic) 
Hledger/Data/Valuation.hs view
@@ -28,22 +28,23 @@ where  import Control.Applicative ((<|>))-import Data.Decimal (roundTo)+import Data.Foldable (asum) import Data.Function ((&), on)-import Data.Graph.Inductive  (Gr, Node, NodeMap, mkMapGraph, mkNode, lab, out, sp)-import Data.List+import Data.List ( (\\), sortBy ) import Data.List.Extra (nubSortBy) import qualified Data.Map as M-import Data.Maybe+import qualified Data.Set as S+import Data.Maybe ( fromMaybe ) import qualified Data.Text as T import Data.Time.Calendar (Day, fromGregorian) import Data.MemoUgly (memo) import GHC.Generics (Generic)-import Safe (headMay)+import Safe (lastMay)  import Hledger.Utils import Hledger.Data.Types import Hledger.Data.Amount+import Hledger.Data.Dates (nulldate)   ------------------------------------------------------------------------------@@ -60,31 +61,6 @@   | AtDefault  (Maybe CommoditySymbol)  -- ^ works like AtNow in single period reports, like AtEnd in multiperiod reports   deriving (Show,Eq) --- | A snapshot of the known exchange rates between commodity pairs at a given date,--- as a graph allowing fast lookup and path finding, along with some helper data.-data PriceGraph = PriceGraph {-   prGraph   :: Gr CommoditySymbol Quantity-    -- ^ A directed graph of exchange rates between commodity pairs.-    -- Node labels are commodities and edge labels are exchange rates,-    -- which were either:-    -- declared by P directives,-    -- inferred from transaction prices,-    -- inferred by reversing a declared rate,-    -- or inferred by reversing a transaction-inferred rate.-    -- There will be at most one edge between each directed pair of commodities,-    -- eg there can be one USD->EUR and one EUR->USD.-  ,prNodemap :: NodeMap CommoditySymbol-    -- ^ Mapping of graph node ids to commodity symbols.-  ,prDefaultValuationCommodities :: M.Map CommoditySymbol CommoditySymbol-    -- ^ The default valuation commodity for each source commodity.-    --   These are used when a valuation commodity is not specified-    --   (-V). They are the destination commodity of the latest-    --   (declared or inferred, but not reverse) each-    --   source commodity's latest market price (on the date of this-    --   graph).-  }-  deriving (Show,Generic)- -- | A price oracle is a magic memoising function that efficiently -- looks up market prices (exchange rates) from one commodity to -- another (or if unspecified, to a default valuation commodity) on a@@ -172,7 +148,7 @@  -- | Standard error message for a report not supporting --value=then. unsupportedValueThenError :: String-unsupportedValueThenError = "Sorry, --value=then is not yet implemented for this kind of report."+unsupportedValueThenError = "Sorry, --value=then is not yet supported for this kind of report."  -- | Find the market value of each component amount in the given -- commodity, or its default valuation commodity, at the given@@ -227,11 +203,11 @@ priceLookup makepricegraph d from mto =   -- trace ("priceLookup ("++show d++", "++show from++", "++show mto++")") $   let-    -- build a graph of the commodity exchange rates in effect on this day-    -- XXX should hide these fgl details better-    PriceGraph{prGraph=g, prNodemap=m, prDefaultValuationCommodities=defaultdests} =+    PriceGraph{pgEdges=forwardprices+              ,pgEdgesRev=allprices+              ,pgDefaultValuationCommodities=defaultdests+              } =       traceAt 1 ("valuation date: "++show d) $ makepricegraph d-    fromnode = node m from     mto' = mto <|> mdefaultto       where         mdefaultto = dbg1 ("default valuation commodity for "++T.unpack from) $@@ -241,25 +217,15 @@       Nothing            -> Nothing       Just to | to==from -> Nothing       Just to            ->-        -- We have a commodity to convert to. Find the most direct price available.-        case mindirectprice of+        -- We have a commodity to convert to. Find the most direct price available,+        -- according to the rules described in makePriceGraph.+        case +          pricesShortestPath forwardprices from to <|> +          pricesShortestPath allprices     from to +        of           Nothing -> Nothing-          Just q  -> Just (to, q)-        where-          tonode = node m to-          mindirectprice :: Maybe Quantity =-            -- Find the shortest path, if any, between from and to.-            case sp fromnode tonode g :: Maybe [Node] of-              Nothing    -> Nothing-              Just nodes ->-                dbg ("market price for "++intercalate " -> " (map T.unpack comms)) $-                  -- TODO: it would be nice to include price date as part of the label-                  -- in PriceGraph, so we could show the dates of market prices here-                Just $ product $ pathEdgeLabels g nodes  -- convert to a single exchange rate-                where comms = catMaybes $ map (lab g) nodes--          -- log a message and a Maybe Quantity, hiding Just/Nothing and limiting decimal places-          dbg msg = dbg1With (((msg++": ")++) . maybe "" (show . roundTo 8))+          Just [] -> Nothing+          Just ps -> Just (mpto $ last ps, product $ map mprate ps)  tests_priceLookup =   let@@ -278,13 +244,85 @@     priceLookup makepricegraph (fromGregorian 2000 01 01) "B" (Just "A") @?= Just ("A",0.1)     priceLookup makepricegraph (fromGregorian 2000 01 01) "A" (Just "E") @?= Just ("E",500) +------------------------------------------------------------------------------+-- Market price graph++type Edge = MarketPrice+type Path = [Edge]++data PriceGraph = PriceGraph {+   pgDate :: Day+    -- ^ The date on which these prices are in effect.+  ,pgEdges :: [Edge]+    -- ^ "Forward" exchange rates between commodity pairs, either+    --   declared by P directives or inferred from transaction prices,+    --   forming the edges of a directed graph.  +  ,pgEdgesRev :: [Edge]+    -- ^ The same edges, plus any additional edges that can be+    --   inferred by reversing them and inverting the rates.+    --+    --   In both of these there will be at most one edge between each+    --   directed pair of commodities, eg there can be one USD->EUR and one EUR->USD.+  ,pgDefaultValuationCommodities :: M.Map CommoditySymbol CommoditySymbol+    -- ^ The default valuation commodity for each source commodity.+    --   These are used when a valuation commodity is not specified+    --   (-V). They are the destination commodity of each source commodity's+    --   latest (declared or inferred, but not reverse) market price+    --   (on the date of this graph).+  }+  deriving (Show,Generic)++-- | Find the shortest path and corresponding conversion rate, if any, +-- from one commodity to another using the provided market prices which+-- form the edges of a directed graph. There should be at most one edge+-- between each directed pair of commodities, eg there can be one+-- USD->EUR price and one EUR->USD price.+pricesShortestPath :: [Edge] -> CommoditySymbol -> CommoditySymbol -> Maybe Path+pricesShortestPath edges start end =+  dbg1 ("shortest price path for "++T.unpack start++" -> "++T.unpack end) $ +  asum $ map (findPath end edgesremaining) initialpaths+  where+    initialpaths = dbg9 "initial price paths" $ [[p] | p <- edges, mpfrom p == start]+    edgesremaining = dbg9 "initial edges remaining" $ edges \\ concat initialpaths++-- Helper: breadth-first search for a continuation of the given path+-- using zero or more of the given edges, to the specified end commodity.+-- Returns the first & shortest complete path found, or Nothing.+findPath :: CommoditySymbol -> [Edge] -> Path -> Maybe Path+findPath end _ path | mpathend == Just end = Just path  -- path is complete+  where +    mpathend = mpto <$> lastMay path+findPath _ [] _ = Nothing   -- no more edges are available+findPath end edgesremaining path =   -- try continuing with all the remaining edges+  asum [ +      findPath end edgesremaining' path'+    | e <- nextedges+    , let path' = path++[e]+    , let edgesremaining' = filter (/=e) edgesremaining+    ]+  where+    nextedges = [ e | e <- edgesremaining, Just (mpfrom e) == mpathend ]+      where+        mpathend = mpto <$> lastMay path++-- | A snapshot of the known exchange rates between commodity pairs at a given date.+-- This is a home-made version, more tailored to our needs. -- | Build the graph of commodity conversion prices for a given day. -- Converts a list of declared market prices in parse order, and a--- list of transaction-inferred market prices in parse order, to a--- graph of all known exchange rates between commodity pairs in effect--- on that day. Cf hledger.m4.md -> Valuation:+-- list of transaction-inferred market prices in parse order, to: ----- hledger looks for a market price (exchange rate) from commodity A+-- 1. a graph of all known exchange rates declared or inferred from +-- one commodity to another in effect on that day+--+-- 2. a second graph which includes any additional exchange rates+-- that can be inferred by reversing known rates+--+-- 3. a map of each commodity's default valuation commodity, if any.+--+-- These allow price lookup and valuation to be performed as+-- described in hledger.m4.md -> Valuation:+--+-- "hledger looks for a market price (exchange rate) from commodity A -- to commodity B in one or more of these ways, in this order of -- preference: --@@ -296,15 +334,15 @@ -- 2. A *reverse market price*: --    the inverse of a declared or inferred market price from B to A. -- --- 3. A *chained market price*:---    a synthetic price formed by combining the shortest chain of market---    prices (any of the above types) leading from A to B.+-- 3. A *a forward chain of market prices*:+--    a synthetic price formed by combining the shortest chain of+--    "forward" (only 1 above) market prices, leading from A to B. ----- 1 and 2 form the edges of the price graph, and we can query it for--- 3 (which is the reason we use a graph).+-- 4. A *any chain of market prices*:+--    a chain of any market prices, including both forward and+--    reverse prices (1 and 2 above), leading from A to B." ----- We also identify each commodity's default valuation commodity, if--- any. For each commodity A, hledger picks a default valuation+-- and: "For each commodity A, hledger picks a default valuation -- commodity as follows, in this order of preference: -- -- 1. The price commodity from the latest declared market price for A@@ -316,32 +354,31 @@ -- -- 3. If there are no P directives at all (any commodity or date), and --    the `--infer-value` flag is used, then the price commodity from---    the latest transaction price for A on or before valuation date.+--    the latest transaction price for A on or before valuation date." -- makePriceGraph :: [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph makePriceGraph alldeclaredprices allinferredprices d =   dbg9 ("makePriceGraph "++show d) $-  PriceGraph{prGraph=g, prNodemap=m, prDefaultValuationCommodities=defaultdests}+  PriceGraph{+     pgDate = d+    ,pgEdges=forwardprices+    ,pgEdgesRev=allprices+    ,pgDefaultValuationCommodities=defaultdests+    }   where     -- prices in effect on date d, either declared or inferred-    visibledeclaredprices = filter ((<=d).mpdate) alldeclaredprices-    visibleinferredprices = filter ((<=d).mpdate) allinferredprices-    declaredandinferredprices = dbg2 "declaredandinferredprices" $-      effectiveMarketPrices visibledeclaredprices visibleinferredprices+    visibledeclaredprices = dbg2 "visibledeclaredprices" $ filter ((<=d).mpdate) alldeclaredprices+    visibleinferredprices = dbg2 "visibleinferredprices" $ filter ((<=d).mpdate) allinferredprices+    forwardprices = effectiveMarketPrices visibledeclaredprices visibleinferredprices      -- infer any additional reverse prices not already declared or inferred-    reverseprices = dbg2 "reverseprices" $-      map marketPriceReverse declaredandinferredprices \\ declaredandinferredprices--    -- build the graph and associated node map-    (g, m) =-      mkMapGraph-      (dbg9 "price graph labels" $ sort allcomms) -- this must include all nodes mentioned in edges-      (dbg9 "price graph edges" $ [(mpfrom, mpto, mprate) | MarketPrice{..} <- prices])-      :: (Gr CommoditySymbol Quantity, NodeMap CommoditySymbol)+    reverseprices = dbg2 "additional reverse prices" $+      [p | p@MarketPrice{..} <- map marketPriceReverse forwardprices+         , not $ (mpfrom,mpto) `S.member` forwardpairs+      ]       where-        prices   = declaredandinferredprices ++ reverseprices-        allcomms = map mpfrom prices+        forwardpairs = S.fromList [(mpfrom,mpto) | MarketPrice{..} <- forwardprices]+    allprices = forwardprices ++ reverseprices      -- determine a default valuation commodity for each source commodity     -- somewhat but not quite like effectiveMarketPrices@@ -370,6 +407,7 @@     declaredprices' = [(1, i, p) | (i,p) <- zip [1..] declaredprices]     inferredprices' = [(0, i, p) | (i,p) <- zip [1..] inferredprices]   in+    dbg2 "effective forward prices" $     -- combine     declaredprices' ++ inferredprices'     -- sort by decreasing date then decreasing precedence then decreasing parse order@@ -380,35 +418,24 @@     & nubSortBy (compare `on` (\(MarketPrice{..})->(mpfrom,mpto)))  marketPriceReverse :: MarketPrice -> MarketPrice-marketPriceReverse mp@MarketPrice{..} = mp{mpfrom=mpto, mpto=mpfrom, mprate=1/mprate}----------------------------------------------------------------------------------- fgl helpers---- | Look up an existing graph node by its label.--- (If the node does not exist, a new one will be generated, but not--- persisted in the nodemap.)-node :: Ord a => NodeMap a -> a -> Node-node m = fst . fst . mkNode m---- | Convert a valid path within the given graph to the corresponding--- edge labels. When there are multiple edges between two nodes, the--- lowest-sorting label is used.-pathEdgeLabels :: (Show b, Ord b) => Gr a b -> [Node] -> [b]-pathEdgeLabels g = map frommaybe . map (nodesEdgeLabel g) . pathEdges-  where frommaybe = fromMaybe (error' "pathEdgeLabels: expected no Nothings here")  -- PARTIAL:---- | Convert a path to node pairs representing the path's edges.-pathEdges :: [Node] -> [(Node,Node)]-pathEdges p = [(f,t) | f:t:_ <- tails p]+marketPriceReverse mp@MarketPrice{..} = +  mp{mpfrom=mpto, mpto=mpfrom, mprate=if mprate==0 then 0 else 1/mprate}  -- PARTIAL: / --- | Get the label of a graph edge from one node to another.--- When there are multiple such edges, the lowest-sorting label is used.-nodesEdgeLabel :: Ord b => Gr a b -> (Node, Node) -> Maybe b-nodesEdgeLabel g (from,to) = headMay $ sort [l | (_,t,l) <- out g from, t==to]+nullmarketprice :: MarketPrice+nullmarketprice = MarketPrice {+   mpdate=nulldate+  ,mpfrom=""+  ,mpto=""+  ,mprate=0+  }  ------------------------------------------------------------------------------  tests_Valuation = tests "Valuation" [    tests_priceLookup+  ,test "marketPriceReverse" $ do+    marketPriceReverse nullmarketprice{mprate=2} @?= nullmarketprice{mprate=0.5}+    marketPriceReverse nullmarketprice @?= nullmarketprice  -- the reverse of a 0 price is a 0 price++   ]
Hledger/Query.hs view
@@ -5,10 +5,6 @@  -} --- Silence safe 0.3.18's deprecation warnings for (max|min)imum(By)?Def for now--- (may hide other deprecation warnings too). https://github.com/ndmitchell/safe/issues/26-{-# OPTIONS_GHC -Wno-warnings-deprecations #-}- {-# LANGUAGE CPP                #-} {-# LANGUAGE FlexibleContexts   #-} {-# LANGUAGE OverloadedStrings  #-}@@ -18,11 +14,13 @@   -- * Query and QueryOpt   Query(..),   QueryOpt(..),+  OrdPlus(..),   payeeTag,   noteTag,   generatedTransactionTag,   -- * parsing   parseQuery,+  parseQueryList,   simplifyQuery,   filterQuery,   -- * accessors@@ -37,7 +35,6 @@   queryIsSym,   queryIsReal,   queryIsStatus,-  queryIsEmpty,   queryStartDate,   queryEndDate,   queryDateSpan,@@ -62,6 +59,7 @@ where  import Control.Applicative ((<|>), many, optional)+import Data.Default (Default(..)) import Data.Either (partitionEithers) import Data.List (partition) import Data.Maybe (fromMaybe, isJust, mapMaybe)@@ -99,8 +97,6 @@            | Real Bool        -- ^ match if "realness" (involves a real non-virtual account ?) has this value            | Amt OrdPlus Quantity  -- ^ match if the amount's numeric quantity is less than/greater than/equal to/unsignedly equal to some value            | Sym Regexp       -- ^ match if the entire commodity symbol is matched by this regexp-           | Empty Bool       -- ^ if true, show zero-amount postings/accounts which are usually not shown-                              --   more of a query option than a query criteria ?            | Depth Int        -- ^ match if account depth is less than or equal to this value.                               --   Depth is sometimes used like a query (for filtering report data)                               --   and sometimes like a query option (for controlling display)@@ -108,6 +104,8 @@                                         -- matching the regexp if provided, exists     deriving (Eq,Show) +instance Default Query where def = Any+ -- | Construct a payee tag payeeTag :: Maybe String -> Either RegexError Query payeeTag = fmap (Tag (toRegexCI' "payee")) . maybe (pure Nothing) (fmap Just . toRegexCI)@@ -141,10 +139,24 @@ -- showAccountMatcher _ = Nothing  --- | Convert a query expression containing zero or more--- space-separated terms to a query and zero or more query options; or--- return an error message if query parsing fails.+-- | A version of parseQueryList which acts on a single Text of+-- space-separated terms. --+-- The usual shell quoting rules are assumed. When a pattern contains+-- whitespace, it (or the whole term including prefix) should be enclosed+-- in single or double quotes.+--+-- >>> parseQuery nulldate "expenses:dining out"+-- Right (Or [Acct (RegexpCI "expenses:dining"),Acct (RegexpCI "out")],[])+--+-- >>> parseQuery nulldate "\"expenses:dining out\""+-- Right (Acct (RegexpCI "expenses:dining out"),[])+parseQuery :: Day -> T.Text -> Either String (Query,[QueryOpt])+parseQuery d = parseQueryList d . words'' prefixes++-- | Convert a list of query expression containing to a query and zero+-- or more query options; or return an error message if query parsing fails.+-- -- A query term is either: -- -- 1. a search pattern, which matches on one or more fields, eg:@@ -161,10 +173,6 @@ -- --      inacct:FULLACCTNAME ----- The usual shell quoting rules are assumed. When a pattern contains--- whitespace, it (or the whole term including prefix) should be enclosed--- in single or double quotes.--- -- Period expressions may contain relative dates, so a reference date is -- required to fully parse these. --@@ -173,15 +181,8 @@ -- 2. multiple description patterns are OR'd together -- 3. multiple status patterns are OR'd together -- 4. then all terms are AND'd together------ >>> parseQuery nulldate "expenses:dining out"--- Right (Or [Acct (RegexpCI "expenses:dining"),Acct (RegexpCI "out")],[])------ >>> parseQuery nulldate "\"expenses:dining out\""--- Right (Acct (RegexpCI "expenses:dining out"),[])-parseQuery :: Day -> T.Text -> Either String (Query,[QueryOpt])-parseQuery d s = do-  let termstrs = words'' prefixes s+parseQueryList :: Day -> [T.Text] -> Either String (Query, [QueryOpt])+parseQueryList d termstrs = do   eterms <- sequence $ map (parseQueryTerm d) termstrs   let (pats, opts) = partitionEithers eterms       (descpats, pats') = partition queryIsDesc pats@@ -277,7 +278,6 @@                               Right st -> Right $ Left $ StatusQ st parseQueryTerm _ (T.stripPrefix "real:" -> Just s) = Right $ Left $ Real $ parseBool s || T.null s parseQueryTerm _ (T.stripPrefix "amt:" -> Just s) = Right $ Left $ Amt ord q where (ord, q) = either error id $ parseAmountQueryTerm s  -- PARTIAL:-parseQueryTerm _ (T.stripPrefix "empty:" -> Just s) = Right $ Left $ Empty $ parseBool s parseQueryTerm _ (T.stripPrefix "depth:" -> Just s)   | n >= 0    = Right $ Left $ Depth n   | otherwise = Left "depth: should have a positive number"@@ -440,10 +440,6 @@ queryIsStatus (StatusQ _) = 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.@@ -617,11 +613,6 @@ matchesPosting (Real v) p = v == isReal p matchesPosting q@(Depth _) Posting{paccount=a} = q `matchesAccount` a matchesPosting q@(Amt _ _) Posting{pamount=amt} = q `matchesMixedAmount` amt--- matchesPosting q@(Amt _ _) Posting{pamount=amt} = q `matchesMixedAmount` amt--- matchesPosting (Empty v) Posting{pamount=a} = v == mixedAmountLooksZero a--- matchesPosting (Empty False) Posting{pamount=a} = True--- matchesPosting (Empty True) Posting{pamount=a} = mixedAmountLooksZero a-matchesPosting (Empty _) _ = True matchesPosting (Sym r) Posting{pamount=Mixed as} = any (matchesCommodity (Sym r)) $ map acommodity as matchesPosting (Tag n v) p = case (reString n, v) of   ("payee", Just v) -> maybe False (regexMatch v . T.unpack . transactionPayee) $ ptransaction p@@ -643,7 +634,6 @@ matchesTransaction (StatusQ s) t = tstatus t == s matchesTransaction (Real v) t = v == hasRealPostings t matchesTransaction q@(Amt _ _) t = any (q `matchesPosting`) $ tpostings t-matchesTransaction (Empty _) _ = True matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t matchesTransaction q@(Sym _) t = any (q `matchesPosting`) $ tpostings t matchesTransaction (Tag n v) t = case (reString n, v) of
Hledger/Read.hs view
@@ -32,7 +32,6 @@   readJournal',    -- * Re-exported-  JournalReader.accountaliasp,   JournalReader.postingp,   findReader,   splitReaderPrefix,
Hledger/Read/Common.hs view
@@ -75,6 +75,9 @@   modifiedaccountnamep,   accountnamep, +  -- ** account aliases+  accountaliasp,+   -- ** amounts   spaceandamountormissingp,   amountp,@@ -103,9 +106,9 @@   singlespacedtextp,   singlespacedtextsatisfyingp,   singlespacep,-   skipNonNewlineSpaces,   skipNonNewlineSpaces1,+  aliasesFromOpts,    -- * tests   tests_Common,@@ -145,6 +148,7 @@  import Hledger.Data import Hledger.Utils+import Safe (headMay)  --- ** doctest setup -- $setup@@ -194,12 +198,26 @@     ,new_save_          :: Bool                 -- ^ save latest new transactions state for next time     ,pivot_             :: String               -- ^ use the given field's value as the account name     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed+    ,commoditystyles_   :: Maybe (M.Map CommoditySymbol AmountStyle) -- ^ optional commodity display styles affecting all files+    ,strict_            :: Bool                 -- ^ do extra error checking (eg, all posted accounts are declared)  } deriving (Show)  instance Default InputOpts where def = definputopts  definputopts :: InputOpts-definputopts = InputOpts def def def def def def True def def+definputopts = InputOpts+    { mformat_           = Nothing+    , mrules_file_       = Nothing+    , aliases_           = []+    , anon_              = False+    , ignore_assertions_ = False+    , new_               = False+    , new_save_          = True+    , pivot_             = ""+    , auto_              = False+    , commoditystyles_   = Nothing+    , strict_            = False+    }  rawOptsToInputOpts :: RawOpts -> InputOpts rawOptsToInputOpts rawopts = InputOpts{@@ -213,6 +231,8 @@   ,new_save_          = True   ,pivot_             = stringopt "pivot" rawopts   ,auto_              = boolopt "auto" rawopts+  ,commoditystyles_   = Nothing+  ,strict_            = boolopt "strict" rawopts   }  --- ** parsing utilities@@ -266,6 +286,7 @@                   Right pj -> journalFinalise iopts f txt pj  -- | Like parseAndFinaliseJournal but takes a (non-Erroring) JournalParser.+-- Also, applies command-line account aliases before finalising. -- Used for timeclock/timedot. -- TODO: get rid of this, use parseAndFinaliseJournal instead parseAndFinaliseJournal' :: JournalParser IO ParsedJournal -> InputOpts@@ -279,7 +300,11 @@   -- see notes above   case ep of     Left e   -> throwError $ customErrorBundlePretty e-    Right pj -> journalFinalise iopts f txt pj+    Right pj -> +      -- apply any command line account aliases. Can fail with a bad replacement pattern.+      case journalApplyAliases (aliasesFromOpts iopts) pj of+        Left e    -> throwError e+        Right pj' -> journalFinalise iopts f txt pj'  -- | Post-process a Journal that has just been parsed or generated, in this order: --@@ -295,50 +320,105 @@ -- -- - infer transaction-implied market prices from transaction prices ---journalFinalise :: InputOpts -> FilePath -> Text -> Journal -> ExceptT String IO Journal-journalFinalise iopts f txt pj = do+journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal+journalFinalise InputOpts{auto_,ignore_assertions_,commoditystyles_,strict_} f txt pj = do   t <- liftIO getClockTime   d <- liftIO getCurrentDay-  -- Infer and apply canonical styles for each commodity (or fail).-  -- This affects transaction balancing/assertions/assignments, so needs to be done early.-  -- (TODO: since #903's refactoring for hledger 1.12,-  -- journalApplyCommodityStyles here is seeing the-  -- transactions before they get reversesd to normal order.)-  case journalApplyCommodityStyles pj of-    Left e    -> throwError e-    Right pj' -> either throwError return $-      pj'-      & journalAddFile (f, txt)  -- save the file path and content-      & journalSetLastReadTime t -- save the last read time-      & journalReverse           -- convert all lists to parse order-      & (if not (auto_ iopts) || null (jtxnmodifiers pj)-         then-           -- Auto postings are not active.-           -- Balance all transactions and maybe check balance assertions.-           journalBalanceTransactions (not $ ignore_assertions_ iopts)-         else \j -> do  -- Either monad-           -- Auto postings are active.-           -- Balance all transactions without checking balance assertions,-           j' <- journalBalanceTransactions False j-           -- then add the auto postings-           -- (Note adding auto postings after balancing means #893b fails;-           -- adding them before balancing probably means #893a, #928, #938 fail.)-           case journalModifyTransactions d j' of-             Left e -> throwError e-             Right j'' -> do-               -- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?)-               j''' <- journalApplyCommodityStyles j''-               -- then check balance assertions.-               journalBalanceTransactions (not $ ignore_assertions_ iopts) j'''-        )-     & fmap journalInferMarketPricesFromTransactions  -- infer market prices from commodity-exchanging transactions+  let pj' =+        pj{jglobalcommoditystyles=fromMaybe M.empty commoditystyles_}  -- save any global commodity styles+        & journalAddFile (f, txt)  -- save the main file's info+        & journalSetLastReadTime t -- save the last read time+        & journalReverse -- convert all lists to the order they were parsed +  -- If in strict mode, check all postings are to declared accounts+  case if strict_ then journalCheckAccountsDeclared pj' else Right () of+    Left e   -> throwError e+    Right () ->++      -- and using declared commodities+      case if strict_ then journalCheckCommoditiesDeclared pj' else Right () of+        Left e   -> throwError e+        Right () ->++          -- Infer and apply canonical styles for each commodity (or throw an error).+          -- This affects transaction balancing/assertions/assignments, so needs to be done early.+          case journalApplyCommodityStyles pj' of+            Left e     -> throwError e+            Right pj'' -> either throwError return $+              pj''+              & (if not auto_ || null (jtxnmodifiers pj'')+                then+                  -- Auto postings are not active.+                  -- Balance all transactions and maybe check balance assertions.+                  journalBalanceTransactions (not ignore_assertions_)+                else \j -> do  -- Either monad+                  -- Auto postings are active.+                  -- Balance all transactions without checking balance assertions,+                  j' <- journalBalanceTransactions False j+                  -- then add the auto postings+                  -- (Note adding auto postings after balancing means #893b fails;+                  -- adding them before balancing probably means #893a, #928, #938 fail.)+                  case journalModifyTransactions d j' of+                    Left e -> throwError e+                    Right j'' -> do+                      -- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?)+                      j''' <- journalApplyCommodityStyles j''+                      -- then check balance assertions.+                      journalBalanceTransactions (not ignore_assertions_) j'''+                )+            & fmap journalInferMarketPricesFromTransactions  -- infer market prices from commodity-exchanging transactions++-- | Check that all the journal's postings are to accounts declared with+-- account directives, returning an error message otherwise.+journalCheckAccountsDeclared :: Journal -> Either String ()+journalCheckAccountsDeclared j = sequence_ $ map checkacct $ journalPostings j+  where+    checkacct Posting{paccount,ptransaction}+      | paccount `elem` as = Right ()+      | otherwise          = +          Left $ "\nstrict mode: undeclared account \""++T.unpack paccount++"\""+            ++ case ptransaction of+                Just Transaction{tsourcepos} -> "\nin transaction at: "++showGenericSourcePos tsourcepos+                Nothing -> ""+      where+        as = journalAccountNamesDeclared j++-- | Check that all the commodities used in this journal's postings have been declared+-- by commodity directives, returning an error message otherwise.+journalCheckCommoditiesDeclared :: Journal -> Either String ()+journalCheckCommoditiesDeclared j = +  sequence_ $ map checkcommodities $ journalPostings j+  where+    checkcommodities Posting{..} =+      case mfirstundeclaredcomm of+        Nothing -> Right ()+        Just c  -> Left $ +          "\nstrict mode: undeclared commodity \""++T.unpack c++"\""+          ++ case ptransaction of+                Just Transaction{tsourcepos} -> "\nin transaction at: "++showGenericSourcePos tsourcepos+                Nothing -> ""      +      where+        mfirstundeclaredcomm = +          headMay $ filter (not . (`elem` cs)) $ catMaybes $+          (acommodity . baamount <$> pbalanceassertion) :+          (map (Just . acommodity) $ amounts pamount)+        cs = journalCommoditiesDeclared j+ setYear :: Year -> JournalParser m () setYear y = modify' (\j -> j{jparsedefaultyear=Just y})  getYear :: JournalParser m (Maybe Year) getYear = fmap jparsedefaultyear get +-- | Get the decimal mark that has been specified for parsing, if any+-- (eg by the CSV decimal-mark rule, or possibly a future journal directive).+-- Return it as an AmountStyle that amount parsers can use.+getDecimalMarkStyle :: JournalParser m (Maybe AmountStyle)+getDecimalMarkStyle = do+  Journal{jparsedecimalmark} <- get+  let mdecmarkStyle = maybe Nothing (\c -> Just $ amountstyle{asdecimalpoint=Just c}) jparsedecimalmark+  return mdecmarkStyle+ setDefaultCommodityAndStyle :: (CommoditySymbol,AmountStyle) -> JournalParser m () setDefaultCommodityAndStyle cs = modify' (\j -> j{jparsedefaultcommodity=Just cs}) @@ -352,16 +432,14 @@ getDefaultAmountStyle :: JournalParser m (Maybe AmountStyle) getDefaultAmountStyle = fmap snd <$> getDefaultCommodityAndStyle --- | Lookup currency-specific amount style.------ Returns 'AmountStyle' used in commodity directive within current journal--- prior to current position or in its parents files.+-- | Get the 'AmountStyle' declared by the most recently parsed (in the current or parent files,+-- prior to the current position) commodity directive for the given commodity, if any. getAmountStyle :: CommoditySymbol -> JournalParser m (Maybe AmountStyle) getAmountStyle commodity = do-    specificStyle <-  maybe Nothing cformat . M.lookup commodity . jcommodities <$> get-    defaultStyle <- fmap snd <$> getDefaultCommodityAndStyle-    let effectiveStyle = listToMaybe $ catMaybes [specificStyle, defaultStyle]-    return effectiveStyle+  Journal{jcommodities} <- get+  let mspecificStyle = M.lookup commodity jcommodities >>= cformat+  mdefaultStyle <- fmap snd <$> getDefaultCommodityAndStyle+  return $ listToMaybe $ catMaybes [mspecificStyle, mdefaultStyle]  addDeclaredAccountType :: AccountName -> AccountType -> JournalParser m () addDeclaredAccountType acct atype =@@ -466,7 +544,7 @@       let dateStr = show year ++ [sep1] ++ show month ++ [sep2] ++ show day        when (sep1 /= sep2) $ customFailure $ parseErrorAtRegion startOffset endOffset $-        "invalid date (mixing date separators is not allowed): " ++ dateStr+        "invalid date: separators are different, should be the same"        case fromGregorianValid year month day of         Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $@@ -632,9 +710,26 @@ -- or right, followed by, in any order: an optional transaction price, -- an optional ledger-style lot price, and/or an optional ledger-style -- lot date. A lot price and lot date will be ignored.+--+-- To parse the amount's quantity (number) we need to know which character +-- represents a decimal mark. We find it in one of three ways:+--+-- 1. If a decimal mark has been set explicitly in the journal parse state, +--    we use that+--+-- 2. Or if the journal has a commodity declaration for the amount's commodity,+--    we get the decimal mark from  that+--+-- 3. Otherwise we will parse any valid decimal mark appearing in the+--    number, as long as the number appears well formed.+--+-- Note 3 is the default zero-config case; it means we automatically handle+-- files with any supported decimal mark, but it also allows different decimal marks+-- in  different amounts, which is a bit too loose. There's an open issue. amountp :: JournalParser m Amount amountp = label "amount" $ do-  let spaces = lift $ skipNonNewlineSpaces+  let +    spaces = lift $ skipNonNewlineSpaces   amount <- amountwithoutpricep <* spaces   (mprice, _elotprice, _elotdate) <- runPermutation $     (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp <* spaces)@@ -642,9 +737,8 @@          <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces)   pure $ amount { aprice = mprice } --- XXX Just like amountp but don't allow lot prices. Needed for balanceassertionp.-amountpnolotprices :: JournalParser m Amount-amountpnolotprices = label "amount" $ do+amountpnolotpricesp :: JournalParser m Amount+amountpnolotpricesp = label "amount" $ do   let spaces = lift $ skipNonNewlineSpaces   amount <- amountwithoutpricep   spaces@@ -661,7 +755,9 @@   leftsymbolamountp :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount   leftsymbolamountp mult sign = label "amount" $ do     c <- lift commoditysymbolp-    suggestedStyle <- getAmountStyle c+    mdecmarkStyle <- getDecimalMarkStyle+    mcommodityStyle <- getAmountStyle c+    let suggestedStyle = mdecmarkStyle <|> mcommodityStyle     commodityspaced <- lift skipNonNewlineSpaces'     sign2 <- lift $ signp     offBeforeNum <- getOffset@@ -684,14 +780,18 @@     case mSpaceAndCommodity of       -- right symbol amount       Just (commodityspaced, c) -> do-        suggestedStyle <- getAmountStyle c-        (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent+        mdecmarkStyle <- getDecimalMarkStyle+        mcommodityStyle <- getAmountStyle c+        let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle+        (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent         let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}         return $ nullamt{acommodity=c, aquantity=sign q, aismultiplier=mult, astyle=s, aprice=Nothing}       -- no symbol amount       Nothing -> do-        suggestedStyle <- getDefaultAmountStyle-        (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent+        mdecmarkStyle <- getDecimalMarkStyle+        mcommodityStyle <- getDefaultAmountStyle+        let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle+        (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent         -- if a default commodity has been set, apply it and its style to this amount         -- (unless it's a multiplier in an automated posting)         defcs <- getDefaultCommodityAndStyle@@ -708,8 +808,8 @@     -> Either AmbiguousNumber RawNumber     -> Maybe Integer     -> TextParser m (Quantity, AmountPrecision, Maybe Char, Maybe DigitGroupStyle)-  interpretNumber posRegion suggestedStyle ambiguousNum mExp =-    let rawNum = either (disambiguateNumber suggestedStyle) id ambiguousNum+  interpretNumber posRegion msuggestedStyle ambiguousNum mExp =+    let rawNum = either (disambiguateNumber msuggestedStyle) id ambiguousNum     in  case fromRawNumber rawNum mExp of           Left errMsg -> customFailure $                            uncurry parseErrorAtRegion posRegion errMsg@@ -768,7 +868,7 @@   lift skipNonNewlineSpaces   -- this amount can have a price; balance assertions ignore it,   -- but balance assignments will use it-  a <- amountpnolotprices <?> "amount (for a balance assertion or assignment)"+  a <- amountpnolotpricesp <?> "amount (for a balance assertion or assignment)"   return BalanceAssertion     { baamount    = a     , batotal     = istotal@@ -846,7 +946,7 @@   -> Either String             (Quantity, Word8, Maybe Char, Maybe DigitGroupStyle) fromRawNumber (WithSeparators _ _ _) (Just _) =-    Left "invalid number: mixing digit separators with exponents is not allowed"+    Left "invalid number: digit separators and exponents may not be used together" fromRawNumber raw mExp = do     (quantity, precision) <- toQuantity (fromMaybe 0 mExp) (digitGroup raw) (decimalGroup raw)     return (quantity, precision, mDecPt raw, digitGroupStyle raw)@@ -855,7 +955,7 @@     toQuantity e preDecimalGrp postDecimalGrp       | precision < 0   = Right (Decimal 0 (digitGrpNum * 10^(-precision)), 0)       | precision < 256 = Right (Decimal precision8 digitGrpNum, precision8)-      | otherwise = Left "invalid number: numbers with more than 255 decimal digits are not allowed at this time"+      | otherwise = Left "invalid number: numbers with more than 255 decimal places are currently not supported"       where         digitGrpNum = digitGroupNumber $ preDecimalGrp <> postDecimalGrp         precision   = toInteger (digitGroupLength postDecimalGrp) - e@@ -876,13 +976,12 @@       (a:b:cs) | a < b -> b:cs       gs               -> gs - disambiguateNumber :: Maybe AmountStyle -> AmbiguousNumber -> RawNumber-disambiguateNumber suggestedStyle (AmbiguousNumber grp1 sep grp2) =+disambiguateNumber msuggestedStyle (AmbiguousNumber grp1 sep grp2) =   -- If present, use the suggested style to disambiguate;   -- otherwise, assume that the separator is a decimal point where possible.-  if isDecimalPointChar sep &&-     maybe True (sep `isValidDecimalBy`) suggestedStyle+  if isDecimalMark sep &&+     maybe True (sep `isValidDecimalBy`) msuggestedStyle   then NoSeparators grp1 (Just (sep, grp2))   else WithSeparators sep [grp1, grp2] Nothing   where@@ -917,7 +1016,7 @@   rawNumber <- fmap Right leadingDecimalPt <|> leadingDigits    -- Guard against mistyped numbers-  mExtraDecimalSep <- optional $ lookAhead $ satisfy isDecimalPointChar+  mExtraDecimalSep <- optional $ lookAhead $ satisfy isDecimalMark   when (isJust mExtraDecimalSep) $     Fail.fail "invalid number (invalid use of separator)" @@ -933,7 +1032,7 @@    leadingDecimalPt :: TextParser m RawNumber   leadingDecimalPt = do-    decPt <- satisfy isDecimalPointChar+    decPt <- satisfy isDecimalMark     decGrp <- digitgroupp     pure $ NoSeparators mempty (Just (decPt, decGrp)) @@ -954,7 +1053,7 @@    withDecimalPt :: Char -> [DigitGrp] -> TextParser m RawNumber   withDecimalPt digitSep digitGroups = do-    decPt <- satisfy $ \c -> isDecimalPointChar c && c /= digitSep+    decPt <- satisfy $ \c -> isDecimalMark c && c /= digitSep     decDigitGrp <- option mempty digitgroupp      pure $ WithSeparators digitSep digitGroups (Just (decPt, decDigitGrp))@@ -966,21 +1065,17 @@     -> [DigitGrp]     -> Either AmbiguousNumber RawNumber   withoutDecimalPt grp1 sep grp2 grps-    | null grps && isDecimalPointChar sep =+    | null grps && isDecimalMark sep =         Left $ AmbiguousNumber grp1 sep grp2     | otherwise = Right $ WithSeparators sep (grp1:grp2:grps) Nothing    trailingDecimalPt :: DigitGrp -> TextParser m RawNumber   trailingDecimalPt grp1 = do-    decPt <- satisfy isDecimalPointChar+    decPt <- satisfy isDecimalMark     pure $ NoSeparators grp1 (Just (decPt, mempty)) --isDecimalPointChar :: Char -> Bool-isDecimalPointChar c = c == '.' || c == ','- isDigitSeparatorChar :: Char -> Bool-isDigitSeparatorChar c = isDecimalPointChar c || c == ' '+isDigitSeparatorChar c = isDecimalMark c || c == ' '  -- | Some kinds of number literal we might parse. data RawNumber@@ -1333,6 +1428,39 @@     isBracketedDateChar c = isDigit c || isDateSepChar c || c == '='  {-# INLINABLE bracketeddatetagsp #-}++-- | Get the account name aliases from options, if any.+aliasesFromOpts :: InputOpts -> [AccountAlias]+aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp ("--alias "++quoteIfNeeded a) $ T.pack a)+                  . aliases_++accountaliasp :: TextParser m AccountAlias+accountaliasp = regexaliasp <|> basicaliasp++basicaliasp :: TextParser m AccountAlias+basicaliasp = do+  -- dbgparse 0 "basicaliasp"+  old <- rstrip <$> (some $ noneOf ("=" :: [Char]))+  char '='+  skipNonNewlineSpaces+  new <- rstrip <$> anySingle `manyTill` eolof  -- eol in journal, eof in command lines, normally+  return $ BasicAlias (T.pack old) (T.pack new)++regexaliasp :: TextParser m AccountAlias+regexaliasp = do+  -- dbgparse 0 "regexaliasp"+  char '/'+  off1 <- getOffset+  re <- some $ noneOf ("/\n\r" :: [Char]) -- paranoid: don't try to read past line end+  off2 <- getOffset+  char '/'+  skipNonNewlineSpaces+  char '='+  skipNonNewlineSpaces+  repl <- anySingle `manyTill` eolof+  case toRegexCI re of+    Right r -> return $! RegexAlias r repl+    Left e  -> customFailure $! parseErrorAtRegion off1 off2 e  --- ** tests 
Hledger/Read/CsvReader.hs view
@@ -78,7 +78,7 @@  import Hledger.Data import Hledger.Utils-import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos, journalFinalise)+import Hledger.Read.Common (aliasesFromOpts,  Reader(..),InputOpts(..), amountp, statusp, genericSourcePos, journalFinalise )  --- ** doctest setup -- $setup@@ -108,13 +108,16 @@   let rulesfile = mrules_file_ iopts   r <- liftIO $ readJournalFromCsv rulesfile f t   case r of Left e   -> throwError e-            Right pj -> journalFinalise iopts{ignore_assertions_=True} f t pj'-              where-                -- journalFinalise assumes the journal's items are-                -- reversed, as produced by JournalReader's parser.-                -- But here they are already properly ordered. So we'd-                -- better preemptively reverse them once more. XXX inefficient-                pj' = journalReverse pj+            Right pj ->+              -- journalFinalise assumes the journal's items are+              -- reversed, as produced by JournalReader's parser.+              -- But here they are already properly ordered. So we'd+              -- better preemptively reverse them once more. XXX inefficient+              let pj' = journalReverse pj+              -- apply any command line account aliases. Can fail with a bad replacement pattern.+              in case journalApplyAliases (aliasesFromOpts iopts) pj' of+                  Left e -> throwError e+                  Right pj'' -> journalFinalise iopts{ignore_assertions_=True} f t pj''  --- ** reading rules files --- *** rules utilities@@ -263,12 +266,12 @@   r1 == r2 = (rdirectives r1, rcsvfieldindexes r1, rassignments r1) ==              (rdirectives r2, rcsvfieldindexes r2, rassignments r2)  --- It is used for debug output only+-- Custom Show instance used for debug output: omit the rblocksassigning field, which isn't showable. instance Show CsvRules where-  show r = "CsvRules { rdirectives=" ++ show (rdirectives r) ++-           ", rcsvfieldindexes=" ++ show (rcsvfieldindexes r) ++-           ", rassignments=" ++ show (rassignments r) ++-           ", rconditionalblocks="++ show (rconditionalblocks r) +++  show r = "CsvRules { rdirectives = " ++ show (rdirectives r) +++           ", rcsvfieldindexes = "     ++ show (rcsvfieldindexes r) +++           ", rassignments = "         ++ show (rassignments r) +++           ", rconditionalblocks = "   ++ show (rconditionalblocks r) ++            " }"  type CsvRulesParser a = StateT CsvRulesParsed SimpleTextParser a@@ -364,7 +367,7 @@  RULES: RULE* -RULE: ( FIELD-LIST | FIELD-ASSIGNMENT | CONDITIONAL-BLOCK | SKIP | NEWEST-FIRST | DATE-FORMAT | COMMENT | BLANK ) NEWLINE+RULE: ( FIELD-LIST | FIELD-ASSIGNMENT | CONDITIONAL-BLOCK | SKIP | NEWEST-FIRST | DATE-FORMAT | DECIMAL-MARK | COMMENT | BLANK ) NEWLINE  FIELD-LIST: fields SPACE FIELD-NAME ( SPACE? , SPACE? FIELD-NAME )* @@ -462,6 +465,7 @@ directives :: [String] directives =   ["date-format"+  ,"decimal-mark"   ,"separator"   -- ,"default-account"   -- ,"default-currency"@@ -711,7 +715,7 @@     else       return $ defaultRulesText rulesfile   rules <- either throwerr return $ parseAndValidateCsvRules rulesfile rulestext-  dbg6IO "rules" rules+  dbg6IO "csv rules" rules    -- parse the skip directive's value, if any   let skiplines = case getDirective "skip" rules of@@ -744,7 +748,7 @@    let     -- convert CSV records to transactions-    txns = snd $ mapAccumL+    txns = dbg7 "csv txns" $ snd $ mapAccumL                    (\pos r ->                       let                         SourcePos name line col = pos@@ -761,7 +765,9 @@     -- than one date and the first date is more recent than the last):     -- reverse them to get same-date transactions ordered chronologically.     txns' =-      (if newestfirst || mdataseemsnewestfirst == Just True then reverse else id) txns+      (if newestfirst || mdataseemsnewestfirst == Just True +        then dbg7 "reversed csv txns" . reverse else id) +        txns       where         newestfirst = dbg6 "newestfirst" $ isJust $ getDirective "newest-first" rules         mdataseemsnewestfirst = dbg6 "mdataseemsnewestfirst" $@@ -769,7 +775,7 @@             ds | length ds > 1 -> Just $ head ds > last ds             _                  -> Nothing     -- Second, sort by date.-    txns'' = sortBy (comparing tdate) txns'+    txns'' = dbg7 "date-sorted csv txns" $ sortBy (comparing tdate) txns'    when (not rulesfileexists) $ do     dbg1IO "creating conversion rules file" rulesfile@@ -977,7 +983,7 @@ getAmount rules record currency p1IsVirtual n =   -- Warning, many tricky corner cases here.   -- docs: hledger_csv.m4.md #### amount-  -- tests: tests/csv.test ~ 13, 31-34+  -- tests: hledger/test/csv.test ~ 13, 31-34   let     unnumberedfieldnames = ["amount","amount-in","amount-out"] @@ -1024,56 +1030,77 @@              "\t=> value: " ++ showMixedAmount a -- XXX not sure this is showing all the right info            | (f,a) <- fs] -  where-    -- | Given a non-empty amount string to parse, along with a possibly-    -- non-empty currency symbol to prepend, parse as a hledger amount (as-    -- in journal format), or raise an error.-    -- The CSV rules and record are provided for the error message.-    parseAmount :: CsvRules -> CsvRecord -> String -> String -> MixedAmount-    parseAmount rules record currency amountstr =-      either mkerror (Mixed . (:[])) $  -- PARTIAL:-      runParser (evalStateT (amountp <* eof) nulljournal) "" $-      T.pack $ (currency++) $ simplifySign amountstr-      where-        mkerror e = error' $ unlines-          ["error: could not parse \""++amountstr++"\" as an amount"-          ,showRecord record-          ,showRules rules record-          -- ,"the default-currency is: "++fromMaybe "unspecified" (getDirective "default-currency" rules)-          ,"the parse error is:      "++customErrorBundlePretty e-          ,"you may need to "-           ++"change your amount*, balance*, or currency* rules, "-           ++"or add or change your skip rule"-          ]- -- | Figure out the expected balance (assertion or assignment) specified for posting N, -- if any (and its parse position). getBalance :: CsvRules -> CsvRecord -> String -> Int -> Maybe (Amount, GenericSourcePos)-getBalance rules record currency n =-  (fieldval ("balance"++show n)-    -- for posting 1, also recognise the old field name-    <|> if n==1 then fieldval "balance" else Nothing)-  >>= parsebalance currency n . strip+getBalance rules record currency n = do+  v <- (fieldval ("balance"++show n)+        -- for posting 1, also recognise the old field name+        <|> if n==1 then fieldval "balance" else Nothing)+  case v of+    "" -> Nothing+    s  -> Just (+            parseBalanceAmount rules record currency n s+           ,nullsourcepos  -- parse position to show when assertion fails,+           )               -- XXX the csv record's line number would be good+     where-    parsebalance currency n s-      | null s    = Nothing-      | otherwise = Just-          (either (mkerror n s) id $-            runParser (evalStateT (amountp <* eof) nulljournal) "" $-            T.pack $ (currency++) $ simplifySign s-          ,nullsourcepos)  -- XXX parse position to show when assertion fails,-                           -- the csv record's line number would be good-      where-        mkerror n s e = error' $ unlines-          ["error: could not parse \""++s++"\" as balance"++show n++" amount"-          ,showRecord record-          ,showRules rules record-          -- ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency-          ,"the parse error is:      "++customErrorBundlePretty e-          ]-    -- mdefaultcurrency = rule "default-currency"-    fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe String+    fieldval = fmap strip . hledgerFieldValue rules record :: HledgerFieldName -> Maybe String +-- | Given a non-empty amount string (from CSV) to parse, along with a+-- possibly non-empty currency symbol to prepend,+-- parse as a hledger MixedAmount (as in journal format), or raise an error.+-- The whole CSV record is provided for the error message.+parseAmount :: CsvRules -> CsvRecord -> String -> String -> MixedAmount+parseAmount rules record currency s =+  either mkerror (Mixed . (:[])) $  -- PARTIAL:+  runParser (evalStateT (amountp <* eof) journalparsestate) "" $+  T.pack $ (currency++) $ simplifySign s+  where+    journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules}+    mkerror e = error' $ unlines+      ["error: could not parse \""++s++"\" as an amount"+      ,showRecord record+      ,showRules rules record+      -- ,"the default-currency is: "++fromMaybe "unspecified" (getDirective "default-currency" rules)+      ,"the parse error is:      "++customErrorBundlePretty e+      ,"you may need to "+        ++"change your amount*, balance*, or currency* rules, "+        ++"or add or change your skip rule"+      ]++-- XXX unify these ^v++-- | Almost but not quite the same as parseAmount.+-- Given a non-empty amount string (from CSV) to parse, along with a+-- possibly non-empty currency symbol to prepend,+-- parse as a hledger Amount (as in journal format), or raise an error.+-- The CSV record and the field's numeric suffix are provided for the error message.+parseBalanceAmount :: CsvRules -> CsvRecord -> String -> Int -> String -> Amount+parseBalanceAmount rules record currency n s =+  either (mkerror n s) id $+    runParser (evalStateT (amountp <* eof) journalparsestate) "" $+    T.pack $ (currency++) $ simplifySign s+                  -- the csv record's line number would be good+  where+    journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules}+    mkerror n s e = error' $ unlines+      ["error: could not parse \""++s++"\" as balance"++show n++" amount"+      ,showRecord record+      ,showRules rules record+      -- ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency+      ,"the parse error is:      "++customErrorBundlePretty e+      ]++-- Read a valid decimal mark from the decimal-mark rule, if any.+-- If the rule is present with an invalid argument, raise an error.+parseDecimalMark :: CsvRules -> Maybe DecimalMark+parseDecimalMark rules =+  case rules `csvRule` "decimal-mark" of+    Nothing -> Nothing+    Just [c] | isDecimalMark c -> Just c+    Just s -> error' $ "decimal-mark's argument should be \".\" or \",\" (not \""++s++"\")"+ -- | Make a balance assertion for the given amount, with the given parse -- position (to be shown in assertion failures), with the assertion type -- possibly set by a balance-type rule.@@ -1168,7 +1195,7 @@ getEffectiveAssignment rules record f = lastMay $ map snd $ assignments   where     -- all active assignments to field f, in order-    assignments = dbg7 "assignments" $ filter ((==f).fst) $ toplevelassignments ++ conditionalassignments+    assignments = dbg9 "csv assignments" $ filter ((==f).fst) $ toplevelassignments ++ conditionalassignments       where         -- all top level field assignments         toplevelassignments    = rassignments rules
Hledger/Read/JournalReader.hs view
@@ -179,11 +179,6 @@       mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts)       journalp --- | Get the account name aliases from options, if any.-aliasesFromOpts :: InputOpts -> [AccountAlias]-aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp ("--alias "++quoteIfNeeded a) $ T.pack a)-                  . aliases_- --- ** parsers --- *** journal @@ -504,34 +499,6 @@   lift skipNonNewlineSpaces1   alias <- lift accountaliasp   addAccountAlias alias--accountaliasp :: TextParser m AccountAlias-accountaliasp = regexaliasp <|> basicaliasp--basicaliasp :: TextParser m AccountAlias-basicaliasp = do-  -- dbgparse 0 "basicaliasp"-  old <- rstrip <$> (some $ noneOf ("=" :: [Char]))-  char '='-  skipNonNewlineSpaces-  new <- rstrip <$> anySingle `manyTill` eolof  -- eol in journal, eof in command lines, normally-  return $ BasicAlias (T.pack old) (T.pack new)--regexaliasp :: TextParser m AccountAlias-regexaliasp = do-  -- dbgparse 0 "regexaliasp"-  char '/'-  off1 <- getOffset-  re <- some $ noneOf ("/\n\r" :: [Char]) -- paranoid: don't try to read past line end-  off2 <- getOffset-  char '/'-  skipNonNewlineSpaces-  char '='-  skipNonNewlineSpaces-  repl <- anySingle `manyTill` eolof-  case toRegexCI re of-    Right r -> return $! RegexAlias r repl-    Left e  -> customFailure $! parseErrorAtRegion off1 off2 e  endaliasesdirectivep :: JournalParser m () endaliasesdirectivep = do
Hledger/Reports.hs view
@@ -41,5 +41,4 @@   ,tests_EntriesReport   ,tests_MultiBalanceReport   ,tests_PostingsReport-  ,tests_ReportOptions   ]
Hledger/Reports/AccountTransactionsReport.hs view
@@ -82,8 +82,8 @@ totallabel   = "Period Total" balancelabel = "Historical Total" -accountTransactionsReport :: ReportOpts -> Journal -> Query -> Query -> AccountTransactionsReport-accountTransactionsReport ropts j reportq thisacctq = (label, items)+accountTransactionsReport :: ReportSpec -> Journal -> Query -> Query -> AccountTransactionsReport+accountTransactionsReport rspec@ReportSpec{rsOpts=ropts} j reportq thisacctq = (label, items)   where     -- a depth limit should not affect the account transactions report     -- seems unnecessary for some reason XXX@@ -115,12 +115,11 @@     styles = journalCommodityStyles j     periodlast =       fromMaybe (error' "journalApplyValuation: expected a non-empty journal") $ -- XXX shouldn't happen-      reportPeriodOrJournalLastDay ropts j-    mreportlast = reportPeriodLastDay ropts-    today = fromMaybe (error' "journalApplyValuation: could not pick a valuation date, ReportOpts today_ is unset") $ today_ ropts -- XXX shouldn't happen+      reportPeriodOrJournalLastDay rspec j+    mreportlast = reportPeriodLastDay rspec     multiperiod = interval_ ropts /= NoInterval     tval = case value_ ropts of-             Just v  -> \t -> transactionApplyValuation prices styles periodlast mreportlast today multiperiod t v+             Just v  -> \t -> transactionApplyValuation prices styles periodlast mreportlast (rsToday rspec) multiperiod t v              Nothing -> id     ts4 =       ptraceAtWith 5 (("ts4:\n"++).pshowTransactions) $
Hledger/Reports/BalanceReport.hs view
@@ -23,7 +23,7 @@ import Hledger.Read (mamountp') import Hledger.Query import Hledger.Utils-import Hledger.Reports.MultiBalanceReport (multiBalanceReportWith)+import Hledger.Reports.MultiBalanceReport (multiBalanceReport) import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes @@ -61,10 +61,10 @@ -- their balances (change of balance) during the specified period. -- If the normalbalance_ option is set, it adjusts the sorting and sign of -- amounts (see ReportOpts and CompoundBalanceCommand).-balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport-balanceReport ropts q j = (rows, total)+balanceReport :: ReportSpec -> Journal -> BalanceReport+balanceReport rspec j = (rows, total)   where-    report = multiBalanceReportWith ropts q j (journalPriceOracle (infer_value_ ropts) j)+    report = multiBalanceReport rspec j     rows = [( prrFullName row             , prrDisplayName row             , prrDepth row - 1  -- BalanceReport uses 0-based account depths@@ -101,9 +101,10 @@ tests_BalanceReport = tests "BalanceReport" [    let-    (opts,journal) `gives` r = do-      let (eitems, etotal) = r-          (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal+    (rspec,journal) `gives` r = do+      let opts' = rspec{rsQuery=And [queryFromFlags $ rsOpts rspec, rsQuery rspec]}+          (eitems, etotal) = r+          (aitems, atotal) = balanceReport opts' journal           showw (acct,acct',indent,amt) = (acct, acct', indent, showMixedAmountDebug amt)       (map showw aitems) @?= (map showw eitems)       (showMixedAmountDebug atotal) @?= (showMixedAmountDebug etotal)@@ -111,10 +112,10 @@     tests "balanceReport" [       test "no args, null journal" $-     (defreportopts, nulljournal) `gives` ([], 0)+     (defreportspec, nulljournal) `gives` ([], 0)      ,test "no args, sample journal" $-     (defreportopts, samplejournal) `gives`+     (defreportspec, samplejournal) `gives`       ([         ("assets:bank:checking","assets:bank:checking",0, mamountp' "$1.00")        ,("assets:bank:saving","assets:bank:saving",0, mamountp' "$1.00")@@ -127,7 +128,7 @@        Mixed [usd 0])      ,test "with --tree" $-     (defreportopts{accountlistmode_=ALTree}, samplejournal) `gives`+     (defreportspec{rsOpts=defreportopts{accountlistmode_=ALTree}}, samplejournal) `gives`       ([         ("assets","assets",0, mamountp' "$0.00")        ,("assets:bank","bank",1, mamountp' "$2.00")@@ -144,7 +145,7 @@        Mixed [usd 0])      ,test "with --depth=N" $-     (defreportopts{depth_=Just 1}, samplejournal) `gives`+     (defreportspec{rsOpts=defreportopts{depth_=Just 1}}, samplejournal) `gives`       ([        ("expenses",    "expenses",    0, mamountp'  "$2.00")        ,("income",      "income",      0, mamountp' "$-2.00")@@ -152,7 +153,7 @@        Mixed [usd 0])      ,test "with depth:N" $-     (defreportopts{query_="depth:1"}, samplejournal) `gives`+     (defreportspec{rsQuery=Depth 1}, samplejournal) `gives`       ([        ("expenses",    "expenses",    0, mamountp'  "$2.00")        ,("income",      "income",      0, mamountp' "$-2.00")@@ -160,11 +161,11 @@        Mixed [usd 0])      ,test "with date:" $-     (defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives`+     (defreportspec{rsQuery=Date $ DateSpan (Just $ fromGregorian 2009 01 01) (Just $ fromGregorian 2010 01 01)}, samplejournal2) `gives`       ([], 0)      ,test "with date2:" $-     (defreportopts{query_="date2:'in 2009'"}, samplejournal2) `gives`+     (defreportspec{rsQuery=Date2 $ DateSpan (Just $ fromGregorian 2009 01 01) (Just $ fromGregorian 2010 01 01)}, samplejournal2) `gives`       ([         ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")        ,("income:salary","income:salary",0,mamountp' "$-1.00")@@ -172,7 +173,7 @@        Mixed [usd 0])      ,test "with desc:" $-     (defreportopts{query_="desc:income"}, samplejournal) `gives`+     (defreportspec{rsQuery=Desc $ toRegexCI' "income"}, samplejournal) `gives`       ([         ("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")        ,("income:salary","income:salary",0, mamountp' "$-1.00")@@ -180,7 +181,7 @@        Mixed [usd 0])      ,test "with not:desc:" $-     (defreportopts{query_="not:desc:income"}, samplejournal) `gives`+     (defreportspec{rsQuery=Not . Desc $ toRegexCI' "income"}, samplejournal) `gives`       ([         ("assets:bank:saving","assets:bank:saving",0, mamountp' "$1.00")        ,("assets:cash","assets:cash",0, mamountp' "$-2.00")@@ -191,7 +192,7 @@        Mixed [usd 0])      ,test "with period on a populated period" $-      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}, samplejournal) `gives`+      (defreportspec{rsOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}}, samplejournal) `gives`        (         [          ("assets:bank:checking","assets:bank:checking",0, mamountp' "$1.00")@@ -200,7 +201,7 @@         Mixed [usd 0])       ,test "with period on an unpopulated period" $-      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}, samplejournal) `gives`+      (defreportspec{rsOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}}, samplejournal) `gives`        ([], 0)  
Hledger/Reports/BudgetReport.hs view
@@ -1,11 +1,12 @@ {- | -} -{-# LANGUAGE CPP #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE NamedFieldPuns      #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE RecordWildCards     #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections       #-}  module Hledger.Reports.BudgetReport (   BudgetGoal,@@ -17,6 +18,7 @@   budgetReport,   budgetReportAsTable,   budgetReportAsText,+  budgetReportAsCsv,   -- * Helpers   reportPeriodName,   -- * Tests@@ -25,6 +27,7 @@ where  import Data.Decimal+import Data.Default (def) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM import Data.List@@ -33,7 +36,6 @@ #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>)) #endif-import Data.Time.Calendar import Safe --import Data.List --import Data.Maybe@@ -43,11 +45,14 @@ --import qualified Data.Text.Lazy as TL --import System.Console.CmdArgs.Explicit as C --import Lucid as L+ import Text.Printf (printf) import Text.Tabular as T+import Text.Tabular.AsciiWide as T  import Hledger.Data import Hledger.Utils+import Hledger.Read.CsvReader (CSV) import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes import Hledger.Reports.MultiBalanceReport@@ -62,31 +67,33 @@ type BudgetReportRow = PeriodicReportRow DisplayName BudgetCell type BudgetReport    = PeriodicReport    DisplayName BudgetCell --- | Calculate budget goals from all periodic transactions,--- actual balance changes from the regular transactions,--- and compare these to get a 'BudgetReport'.--- Unbudgeted accounts may be hidden or renamed (see budgetRollup).-budgetReport :: ReportOpts -> Bool -> DateSpan -> Day -> Journal -> BudgetReport-budgetReport ropts' assrt reportspan d j = dbg1 "sortedbudgetreport" budgetreport+type BudgetDisplayCell = ((String, Int), Maybe ((String, Int), Maybe (String, Int)))++-- | Calculate per-account, per-period budget (balance change) goals+-- from all periodic transactions, calculate actual balance changes +-- from the regular transactions, and compare these to get a 'BudgetReport'.+-- Unbudgeted accounts may be hidden or renamed (see journalWithBudgetAccountNames).+budgetReport :: ReportSpec -> Bool -> DateSpan -> Journal -> BudgetReport+budgetReport rspec assrt reportspan j = dbg4 "sortedbudgetreport" budgetreport   where     -- Budget report demands ALTree mode to ensure subaccounts and subaccount budgets are properly handled     -- and that reports with and without --empty make sense when compared side by side-    ropts = ropts' { accountlistmode_ = ALTree }+    ropts = (rsOpts rspec){ accountlistmode_ = ALTree }     showunbudgeted = empty_ ropts     budgetedaccts =-      dbg2 "budgetedacctsinperiod" $+      dbg3 "budgetedacctsinperiod" $       nub $       concatMap expandAccountName $       accountNamesFromPostings $       concatMap tpostings $       concatMap (`runPeriodicTransaction` reportspan) $       jperiodictxns j-    actualj = dbg1With (("actualj"++).show.jtxns)  $ budgetRollUp budgetedaccts showunbudgeted j-    budgetj = dbg1With (("budgetj"++).show.jtxns)  $ budgetJournal assrt ropts reportspan j+    actualj = journalWithBudgetAccountNames budgetedaccts showunbudgeted j+    budgetj = journalAddBudgetGoalTransactions assrt ropts reportspan j     actualreport@(PeriodicReport actualspans _ _) =-        dbg1 "actualreport" $ multiBalanceReport d ropts{empty_=True} actualj+        dbg5 "actualreport" $ multiBalanceReport rspec{rsOpts=ropts{empty_=True}} actualj     budgetgoalreport@(PeriodicReport _ budgetgoalitems budgetgoaltotals) =-        dbg1 "budgetgoalreport" $ multiBalanceReport d ropts{empty_=True} budgetj+        dbg5 "budgetgoalreport" $ multiBalanceReport rspec{rsOpts=ropts{empty_=True}} budgetj     budgetgoalreport'       -- If no interval is specified:       -- budgetgoalreport's span might be shorter actualreport's due to periodic txns;@@ -96,16 +103,17 @@     budgetreport = combineBudgetAndActual ropts j budgetgoalreport' actualreport  -- | Use all periodic transactions in the journal to generate--- budget transactions in the specified report period.--- Budget transactions are similar to forecast transactions except--- their purpose is to set goal amounts (of change) per account and period.-budgetJournal :: Bool -> ReportOpts -> DateSpan -> Journal -> Journal-budgetJournal assrt _ropts reportspan j =+-- budget goal transactions in the specified date span.+-- Budget goal transactions are similar to forecast transactions except+-- their purpose and effect is to define balance change goals, per account and period,+-- for BudgetReport.+journalAddBudgetGoalTransactions :: Bool -> ReportOpts -> DateSpan -> Journal -> Journal+journalAddBudgetGoalTransactions assrt _ropts reportspan j =   either error' id $ journalBalanceTransactions assrt j{ jtxns = budgetts }  -- PARTIAL:   where-    budgetspan = dbg2 "budgetspan" $ reportspan+    budgetspan = dbg3 "budget span" $ reportspan     budgetts =-      dbg1 "budgetts" $+      dbg5 "budget goal txns" $       [makeBudgetTxn t       | pt <- jperiodictxns j       , t <- runPeriodicTransaction pt budgetspan@@ -119,10 +127,12 @@ -- -- 2. subaccounts with no budget goal are merged with their closest parent account --    with a budget goal, so that only budgeted accounts are shown.---    This can be disabled by --empty.+--    This can be disabled by -E/--empty. ---budgetRollUp :: [AccountName] -> Bool -> Journal -> Journal-budgetRollUp budgetedaccts showunbudgeted j = j { jtxns = remapTxn <$> jtxns j }+journalWithBudgetAccountNames :: [AccountName] -> Bool -> Journal -> Journal+journalWithBudgetAccountNames budgetedaccts showunbudgeted j = +  dbg5With (("budget account names: "++).pshow.journalAccountNamesUsed) $ +  j { jtxns = remapTxn <$> jtxns j }   where     remapTxn = mapPostings (map remapPosting)       where@@ -210,8 +220,9 @@ -- | Render a budget report as plain text suitable for console output. budgetReportAsText :: ReportOpts -> BudgetReport -> String budgetReportAsText ropts@ReportOpts{..} budgetr =-  title ++ "\n\n" ++-  tableAsText ropts showcell (maybetranspose $ budgetReportAsTable ropts budgetr)+    title ++ "\n\n" +++    renderTable def{tableBorders=False,prettyTable=pretty_tables_}+        (alignCell TopLeft) (alignCell TopRight) (uncurry showcell) displayTableWithWidths   where     multiperiod = interval_ /= NoInterval     title = printf "Budget performance in %s%s:"@@ -226,31 +237,39 @@         Just (AtDefault _mc)  -> ", current value"         Just (AtDate d _mc) -> ", valued at "++showDate d         Nothing             -> "")-    actualwidth = maximum' $ map fst amountsAndGoals-    budgetwidth = maximum' $ map snd amountsAndGoals-    amountsAndGoals =-      map (\(a,g) -> (amountWidth a, amountWidth g)) . concatMap prrAmounts $ prRows budgetr++    displayTableWithWidths :: Table String String ((Int, Int, Int), BudgetDisplayCell)+    displayTableWithWidths = Table rh ch $ map (zipWith (,) widths) displaycells+    Table rh ch displaycells = case budgetReportAsTable ropts budgetr of+        Table rh' ch' vals -> maybetranspose . Table rh' ch' $ map (map displayCell) vals++    displayCell (actual, budget) = (showamt actual', budgetAndPerc <$> budget)       where-        amountWidth = maybe 0 (length . showMixedAmountElided False)+        actual' = fromMaybe 0 actual+        budgetAndPerc b = (showamt b, showper <$> percentage actual' b)+        showamt = showMixedOneLine showAmountWithoutPrice Nothing (Just 32) color_+        showper p = let str = show (roundTo 0 p) in (str, length str)+    cellWidth ((_,wa), Nothing)                    = (wa,  0,  0)+    cellWidth ((_,wa), Just ((_,wb), Nothing))     = (wa, wb,  0)+    cellWidth ((_,wa), Just ((_,wb), Just (_,wp))) = (wa, wb, wp)++    widths = zip3 actualwidths budgetwidths percentwidths+    actualwidths  = map (maximum' . map (first3  . cellWidth)) cols+    budgetwidths  = map (maximum' . map (second3 . cellWidth)) cols+    percentwidths = map (maximum' . map (third3  . cellWidth)) cols+    cols = transpose displaycells+     -- XXX lay out actual, percentage and/or goal in the single table cell for now, should probably use separate cells-    showcell :: BudgetCell -> String-    showcell (mactual, mbudget) = actualstr ++ " " ++ budgetstr+    showcell :: (Int, Int, Int) -> BudgetDisplayCell -> Cell+    showcell (actualwidth, budgetwidth, percentwidth) ((actual,wa), mbudget) =+        Cell TopRight [(replicate (actualwidth - wa) ' ' ++ actual ++ budgetstr, actualwidth + totalbudgetwidth)]       where-        percentwidth = 4-        actual = fromMaybe 0 mactual-        actualstr = printf ("%"++show actualwidth++"s") (showamt actual)+        totalpercentwidth = if percentwidth == 0 then 0 else percentwidth + 5+        totalbudgetwidth  = if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth + 3         budgetstr = case mbudget of-          Nothing     -> replicate (percentwidth + 7 + budgetwidth) ' '-          Just budget ->-            case percentage actual budget of-              Just pct ->-                printf ("[%"++show percentwidth++"s%% of %"++show budgetwidth++"s]")-                       (show $ roundTo 0 pct) (showamt' budget)-              Nothing ->-                printf ("["++replicate (percentwidth+5) ' '++"%"++show budgetwidth++"s]")-                       (showamt' budget)-        showamt = showMixedAmountElided color_-        showamt' = showMixedAmountElided False  -- XXX colored budget amounts disrupts layout+          Nothing                             -> replicate totalbudgetwidth ' '+          Just ((budget, wb), Nothing)        -> " [" ++ replicate totalpercentwidth ' ' ++ replicate (budgetwidth - wb) ' ' ++ budget ++ "]"+          Just ((budget, wb), Just (pct, wp)) -> " [" ++ replicate (percentwidth - wp) ' ' ++ pct ++ "% of " ++ replicate (budgetwidth - wb) ' ' ++ budget ++ "]"      -- | Calculate the percentage of actual change to budget goal to show, if any.     -- If valuing at cost, both amounts are converted to cost before comparing.@@ -321,6 +340,45 @@       where         multiyear = (>1) $ length $ nubSort $ map spanStartYear spans     _ -> maybe "" (showDate . prevday) . spanEnd++-- XXX generalise this with multiBalanceReportAsCsv ?+-- | Render a budget report as CSV. Like multiBalanceReportAsCsv,+-- but includes alternating actual and budget amount columns.+budgetReportAsCsv :: ReportOpts -> BudgetReport -> CSV+budgetReportAsCsv +  ReportOpts{average_, row_total_, no_total_, transpose_}+  (PeriodicReport colspans items (PeriodicReportRow _ abtotals (magrandtot,mbgrandtot) (magrandavg,mbgrandavg)))+  = (if transpose_ then transpose else id) $++  -- heading row+  ("Account" : +   concatMap (\span -> [showDateSpan span, "budget"]) colspans+   ++ concat [["Total"  ,"budget"] | row_total_]+   ++ concat [["Average","budget"] | average_]+  ) :++  -- account rows+  [T.unpack (displayFull a) :+   map showmamt (flattentuples abamts)+   ++ concat [[showmamt mactualrowtot, showmamt mbudgetrowtot] | row_total_]+   ++ concat [[showmamt mactualrowavg, showmamt mbudgetrowavg] | average_]+  | PeriodicReportRow a abamts (mactualrowtot,mbudgetrowtot) (mactualrowavg,mbudgetrowavg) <- items+  ]++  -- totals row+  ++ concat [+    [+    "Total:" :+    map showmamt (flattentuples abtotals)+    ++ concat [[showmamt magrandtot,showmamt mbgrandtot] | row_total_] +    ++ concat [[showmamt magrandavg,showmamt mbgrandavg] | average_]+    ]+  | not no_total_+  ]++  where+    flattentuples abs = concat [[a,b] | (a,b) <- abs]+    showmamt = maybe "" (showMixedAmountOneLineWithoutPrice False)  -- tests 
Hledger/Reports/EntriesReport.hs view
@@ -32,26 +32,25 @@ type EntriesReportItem = Transaction  -- | Select transactions for an entries report.-entriesReport :: ReportOpts -> Query -> Journal -> EntriesReport-entriesReport ropts@ReportOpts{..} q j@Journal{..} =-  sortBy (comparing getdate) $ filter (q `matchesTransaction`) $ map tvalue jtxns+entriesReport :: ReportSpec -> Journal -> EntriesReport+entriesReport rspec@ReportSpec{rsOpts=ropts@ReportOpts{..}} j@Journal{..} =+  sortBy (comparing getdate) $ filter (rsQuery rspec `matchesTransaction`) $ map tvalue jtxns   where     getdate = transactionDateFn ropts     -- We may be converting posting amounts to value, per hledger_options.m4.md "Effect of --value on reports".     tvalue t@Transaction{..} = t{tpostings=map pvalue tpostings}       where         pvalue p = maybe p-          (postingApplyValuation (journalPriceOracle infer_value_ j) (journalCommodityStyles j) periodlast mreportlast today False p)+          (postingApplyValuation (journalPriceOracle infer_value_ j) (journalCommodityStyles j) periodlast mreportlast (rsToday rspec) False p)           value_           where-            periodlast  = fromMaybe today $ reportPeriodOrJournalLastDay ropts j-            mreportlast = reportPeriodLastDay ropts-            today       = fromMaybe (error' "erValue: could not pick a valuation date, ReportOpts today_ is unset") today_  -- PARTIAL: should not happen+            periodlast  = fromMaybe (rsToday rspec) $ reportPeriodOrJournalLastDay rspec j+            mreportlast = reportPeriodLastDay rspec  tests_EntriesReport = tests "EntriesReport" [   tests "entriesReport" [-     test "not acct" $ (length $ entriesReport defreportopts (Not . Acct $ toRegex' "bank") samplejournal) @?= 1-    ,test "date" $ (length $ entriesReport defreportopts (Date $ DateSpan (Just $ fromGregorian 2008 06 01) (Just $ fromGregorian 2008 07 01)) samplejournal) @?= 3+     test "not acct" $ (length $ entriesReport defreportspec{rsQuery=Not . Acct $ toRegex' "bank"} samplejournal) @?= 1+    ,test "date" $ (length $ entriesReport defreportspec{rsQuery=Date $ DateSpan (Just $ fromGregorian 2008 06 01) (Just $ fromGregorian 2008 07 01)} samplejournal) @?= 3   ]  ] 
Hledger/Reports/MultiBalanceReport.hs view
@@ -16,15 +16,21 @@   multiBalanceReport,   multiBalanceReportWith, -  CompoundBalanceReport,   compoundBalanceReport,   compoundBalanceReportWith, -  tableAsText,-   sortRows,   sortRowsLike, +  -- * Helper functions+  calculateReportSpan,+  makeReportQuery,+  getPostingsByColumn,+  getPostings,+  calculateColSpans,+  startingBalances,+  generateMultiBalanceReport,+   -- -- * Tests   tests_MultiBalanceReport )@@ -45,18 +51,23 @@ #endif import Data.Semigroup (sconcat) import Data.Time.Calendar (Day, addDays, fromGregorian)-import Safe (headMay, lastDef, lastMay)-import Text.Tabular as T-import Text.Tabular.AsciiWide (render)+import Safe (headMay, lastDef, lastMay, minimumMay)  import Hledger.Data import Hledger.Query-import Hledger.Utils+import Hledger.Utils hiding (dbg3,dbg4,dbg5)+import qualified Hledger.Utils import Hledger.Read (mamountp') import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes  +-- add a prefix to this function's debug output+dbg3 s = let p = "multiBalanceReport" in Hledger.Utils.dbg3 (p++" "++s)+dbg4 s = let p = "multiBalanceReport" in Hledger.Utils.dbg4 (p++" "++s)+dbg5 s = let p = "multiBalanceReport" in Hledger.Utils.dbg5 (p++" "++s)++ -- | A multi balance report is a kind of periodic report, where the amounts -- correspond to balance changes or ending balances in a given period. It has: --@@ -77,13 +88,11 @@  type MultiBalanceReport    = PeriodicReport    DisplayName MixedAmount type MultiBalanceReportRow = PeriodicReportRow DisplayName MixedAmount-type CompoundBalanceReport = CompoundPeriodicReport DisplayName MixedAmount  -- type alias just to remind us which AccountNames might be depth-clipped, below. type ClippedAccountName = AccountName  - -- | 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. If the normalbalance_ option is set, it@@ -91,79 +100,78 @@ -- CompoundBalanceCommand). hledger's most powerful and useful report, used -- by the balance command (in multiperiod mode) and (via compoundBalanceReport) -- by the bs/cf/is commands.-multiBalanceReport :: Day -> ReportOpts -> Journal -> MultiBalanceReport-multiBalanceReport today ropts j =-    multiBalanceReportWith ropts q j (journalPriceOracle infer j)-  where-    q = queryFromOpts today ropts-    infer = infer_value_ ropts+multiBalanceReport :: ReportSpec -> Journal -> MultiBalanceReport+multiBalanceReport rspec j = multiBalanceReportWith rspec j (journalPriceOracle infer j)+  where infer = infer_value_ $ rsOpts rspec --- | A helper for multiBalanceReport. This one takes an explicit Query--- instead of deriving one from ReportOpts, and an extra argument, a--- PriceOracle to be used for looking up market prices. Commands which+-- | A helper for multiBalanceReport. This one takes an extra argument,+-- a PriceOracle to be used for looking up market prices. Commands which -- run multiple reports (bs etc.) can generate the price oracle just -- once for efficiency, passing it to each report by calling this -- function directly.-multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport-multiBalanceReportWith ropts q j priceoracle = report+multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport+multiBalanceReportWith rspec' j priceoracle = report   where     -- Queries, report/column dates.-    reportspan = dbg "reportspan" $ calculateReportSpan ropts q j-    reportq    = dbg "reportq"    $ makeReportQuery ropts reportspan q+    reportspan = dbg3 "reportspan" $ calculateReportSpan rspec' j+    rspec      = dbg3 "reportopts" $ makeReportQuery rspec' reportspan+    valuation  = makeValuation rspec' j priceoracle  -- Must use rspec' instead of rspec,+                                                     -- so the reportspan isn't used for valuation      -- Group postings into their columns.-    colps    = dbg'' "colps"  $ getPostingsByColumn ropts reportq j reportspan-    colspans = dbg "colspans" $ M.keys colps+    colps    = dbg5 "colps"  $ getPostingsByColumn rspec j reportspan+    colspans = dbg3 "colspans" $ M.keys colps      -- The matched accounts with a starting balance. All of these should appear     -- in the report, even if they have no postings during the report period.-    startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan+    startbals = dbg5 "startbals" $ startingBalances rspec j reportspan      -- Generate and postprocess the report, negating balances and taking percentages if needed-    report = dbg' "report" $-      generateMultiBalanceReport ropts reportq j priceoracle colspans colps startbals+    report = dbg4 "multiBalanceReportWith" $+      generateMultiBalanceReport rspec j valuation colspans colps startbals  -- | Generate a compound balance report from a list of CBCSubreportSpec. This -- shares postings between the subreports.-compoundBalanceReport :: Day -> ReportOpts -> Journal -> [CBCSubreportSpec]-                      -> CompoundBalanceReport-compoundBalanceReport today ropts j =-    compoundBalanceReportWith ropts q j (journalPriceOracle infer j)-  where-    q = queryFromOpts today ropts-    infer = infer_value_ ropts+compoundBalanceReport :: ReportSpec -> Journal -> [CBCSubreportSpec a]+                      -> CompoundPeriodicReport a MixedAmount+compoundBalanceReport rspec j = compoundBalanceReportWith rspec j (journalPriceOracle infer j)+  where infer = infer_value_ $ rsOpts rspec  -- | A helper for compoundBalanceReport, similar to multiBalanceReportWith.-compoundBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle-                          -> [CBCSubreportSpec] -> CompoundBalanceReport-compoundBalanceReportWith ropts q j priceoracle subreportspecs = cbr+compoundBalanceReportWith :: ReportSpec -> Journal -> PriceOracle+                          -> [CBCSubreportSpec a]+                          -> CompoundPeriodicReport a MixedAmount+compoundBalanceReportWith rspec' j priceoracle subreportspecs = cbr   where     -- Queries, report/column dates.-    reportspan = dbg "reportspan" $ calculateReportSpan ropts q j-    reportq    = dbg "reportq"    $ makeReportQuery ropts reportspan q+    reportspan = dbg3 "reportspan" $ calculateReportSpan rspec' j+    rspec      = dbg3 "reportopts" $ makeReportQuery rspec' reportspan+    valuation  = makeValuation rspec' j priceoracle  -- Must use ropts' instead of ropts,+                                                     -- so the reportspan isn't used for valuation      -- Group postings into their columns.-    colps    = dbg'' "colps"  $ getPostingsByColumn ropts{empty_=True} reportq j reportspan-    colspans = dbg "colspans" $ M.keys colps+    colps    = dbg5 "colps"  $ getPostingsByColumn rspec{rsOpts=(rsOpts rspec){empty_=True}} j reportspan+    colspans = dbg3 "colspans" $ M.keys colps      -- The matched accounts with a starting balance. All of these should appear     -- in the report, even if they have no postings during the report period.-    startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan+    startbals = dbg5 "startbals" $ startingBalances rspec j reportspan      subreports = map generateSubreport subreportspecs       where         generateSubreport CBCSubreportSpec{..} =             ( cbcsubreporttitle             -- Postprocess the report, negating balances and taking percentages if needed-            , prNormaliseSign cbcsubreportnormalsign $-                generateMultiBalanceReport ropts' reportq j priceoracle colspans colps' startbals'+            , cbcsubreporttransform $+                generateMultiBalanceReport rspec{rsOpts=ropts} j valuation colspans colps' startbals'             , cbcsubreportincreasestotal             )           where-            ropts'     = ropts{normalbalance_=Just cbcsubreportnormalsign}             -- Filter the column postings according to each subreport-            colps'     = filter (matchesPosting $ cbcsubreportquery j) <$> colps-            startbals' = HM.filterWithKey (\k _ -> matchesAccount (cbcsubreportquery j) k) startbals+            colps'     = filter (matchesPosting q) <$> colps+            startbals' = HM.filterWithKey (\k _ -> matchesAccount q k) startbals+            ropts      = cbcsubreportoptions $ rsOpts rspec+            q          = cbcsubreportquery j      -- Sum the subreport totals by column. Handle these cases:     -- - no subreports@@ -186,21 +194,20 @@ -- TODO: Do we want to check whether to bother calculating these? isHistorical -- and startDate is not nothing, otherwise mempty? This currently gives a -- failure with some totals which are supposed to be 0 being blank.-startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> HashMap AccountName Account-startingBalances ropts q j reportspan = acctchanges+startingBalances :: ReportSpec -> Journal -> DateSpan -> HashMap AccountName Account+startingBalances rspec@ReportSpec{rsQuery=query,rsOpts=ropts} j reportspan =+    acctChangesFromPostings rspec' . map fst $ getPostings rspec' j   where-    acctchanges = acctChangesFromPostings ropts' startbalq . map fst $-        getPostings ropts' startbalq j+    rspec' = rspec{rsQuery=startbalq,rsOpts=ropts'}+    ropts' = case accountlistmode_ ropts of+        ALTree -> ropts{period_=precedingperiod, no_elide_=True}+        ALFlat -> ropts{period_=precedingperiod}      -- q projected back before the report start date.     -- When there's no report start date, in case there are future txns (the hledger-ui case above),     -- we use emptydatespan to make sure they aren't counted as starting balance.-    startbalq = dbg'' "startbalq" $ And [datelessq, precedingspanq]-    datelessq = dbg "datelessq" $ filterQuery (not . queryIsDateOrDate2) q--    ropts' = case accountlistmode_ ropts of-        ALTree -> ropts{no_elide_=True, period_=precedingperiod}-        ALFlat -> ropts{period_=precedingperiod}+    startbalq = dbg3 "startbalq" $ And [datelessq, precedingspanq]+    datelessq = dbg3 "datelessq" $ filterQuery (not . queryIsDateOrDate2) query      precedingperiod = dateSpanAsPeriod . spanIntersect precedingspan .                          periodAsDateSpan $ period_ ropts@@ -210,20 +217,20 @@         a -> a  -- | Calculate the span of the report to be generated.-calculateReportSpan :: ReportOpts -> Query -> Journal -> DateSpan-calculateReportSpan ropts q j = reportspan+calculateReportSpan :: ReportSpec -> Journal -> DateSpan+calculateReportSpan ReportSpec{rsQuery=query,rsOpts=ropts} j = reportspan   where     -- The date span specified by -b/-e/-p options and query args if any.-    requestedspan  = dbg "requestedspan" $ queryDateSpan (date2_ ropts) q+    requestedspan  = dbg3 "requestedspan" $ queryDateSpan (date2_ ropts) query     -- If the requested span is open-ended, close it using the journal's end dates.     -- This can still be the null (open) span if the journal is empty.-    requestedspan' = dbg "requestedspan'" $+    requestedspan' = dbg3 "requestedspan'" $         requestedspan `spanDefaultsFrom` journalDateSpan (date2_ ropts) j     -- The list of interval spans enclosing the requested span.     -- This list can be empty if the journal was empty,     -- or if hledger-ui has added its special date:-tomorrow to the query     -- and all txns are in the future.-    intervalspans  = dbg "intervalspans" $ splitSpan (interval_ ropts) requestedspan'+    intervalspans  = dbg3 "intervalspans" $ splitSpan (interval_ ropts) requestedspan'     -- The requested span enlarged to enclose a whole number of intervals.     -- This can be the null span if there were no intervals.     reportspan = DateSpan (spanStart =<< headMay intervalspans)@@ -233,25 +240,37 @@ -- The user's query expanded to the report span -- if there is one (otherwise any date queries are left as-is, which -- handles the hledger-ui+future txns case above).-makeReportQuery :: ReportOpts -> DateSpan -> Query -> Query-makeReportQuery ropts reportspan q-    | reportspan == nulldatespan = q-    | otherwise = And [dateless q, reportspandatesq]+makeReportQuery :: ReportSpec -> DateSpan -> ReportSpec+makeReportQuery rspec reportspan+    | reportspan == nulldatespan = rspec+    | otherwise = rspec{rsQuery=query}   where-    reportspandatesq = dbg "reportspandatesq" $ dateqcons reportspan-    dateless   = dbg "dateless" . filterQuery (not . queryIsDateOrDate2)-    dateqcons  = if date2_ ropts then Date2 else Date+    query            = simplifyQuery $ And [dateless $ rsQuery rspec, reportspandatesq]+    reportspandatesq = dbg3 "reportspandatesq" $ dateqcons reportspan+    dateless         = dbg3 "dateless" . filterQuery (not . queryIsDateOrDate2)+    dateqcons        = if date2_ (rsOpts rspec) then Date2 else Date +-- | Make a valuation function for valuating MixedAmounts and a given Day+makeValuation :: ReportSpec -> Journal -> PriceOracle -> (Day -> MixedAmount -> MixedAmount)+makeValuation rspec j priceoracle day = case value_ (rsOpts rspec) of+    Nothing -> id+    Just v  -> mixedAmountApplyValuation priceoracle styles day mreportlast (rsToday rspec) multiperiod v+  where+    -- Some things needed if doing valuation.+    styles = journalCommodityStyles j+    mreportlast = reportPeriodLastDay rspec+    multiperiod = interval_ (rsOpts rspec) /= NoInterval+ -- | Group postings, grouped by their column-getPostingsByColumn :: ReportOpts -> Query -> Journal -> DateSpan -> Map DateSpan [Posting]-getPostingsByColumn ropts q j reportspan = columns+getPostingsByColumn :: ReportSpec -> Journal -> DateSpan -> Map DateSpan [Posting]+getPostingsByColumn rspec j reportspan = columns   where     -- Postings matching the query within the report period.-    ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts q j+    ps :: [(Posting, Day)] = dbg5 "getPostingsByColumn" $ getPostings rspec j     days = map snd ps      -- The date spans to be included as report columns.-    colspans = calculateColSpans ropts reportspan days+    colspans = calculateColSpans (rsOpts rspec) reportspan days     addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d     emptyMap = M.fromList . zip colspans $ repeat [] @@ -259,19 +278,19 @@     columns = foldr addPosting emptyMap ps  -- | Gather postings matching the query within the report period.-getPostings :: ReportOpts -> Query -> Journal -> [(Posting, Day)]-getPostings ropts q =+getPostings :: ReportSpec -> Journal -> [(Posting, Day)]+getPostings ReportSpec{rsQuery=query,rsOpts=ropts} =     map (\p -> (p, date p)) .     journalPostings .     filterJournalAmounts symq .    -- remove amount parts excluded by cur:     filterJournalPostings reportq  -- remove postings not matched by (adjusted) query   where-    symq = dbg "symq" . filterQuery queryIsSym $ dbg "requested q" q+    symq = dbg3 "symq" . filterQuery queryIsSym $ dbg3 "requested q" query     -- The user's query with no depth limit, and expanded to the report span     -- if there is one (otherwise any date queries are left as-is, which     -- handles the hledger-ui+future txns case above).-    reportq = dbg "reportq" $ depthless q-    depthless = dbg "depthless" . filterQuery (not . queryIsDepth)+    reportq = dbg3 "reportq" $ depthless query+    depthless = dbg3 "depthless" . filterQuery (not . queryIsDepth)      date = case whichDateFromOpts ropts of         PrimaryDate   -> postingDate@@ -283,25 +302,24 @@     splitSpan (interval_ ropts) displayspan   where     displayspan-      | empty_ ropts = dbg "displayspan (-E)" reportspan                        -- all the requested intervals-      | otherwise = dbg "displayspan" $ reportspan `spanIntersect` matchedspan  -- exclude leading/trailing empty intervals-    matchedspan = dbg "matchedspan" $ daysSpan days+      | empty_ ropts = dbg3 "displayspan (-E)" reportspan                        -- all the requested intervals+      | otherwise = dbg3 "displayspan" $ reportspan `spanIntersect` matchedspan  -- exclude leading/trailing empty intervals+    matchedspan = dbg3 "matchedspan" $ daysSpan days   -- | Gather the account balance changes into a regular matrix -- including the accounts from all columns.-calculateAccountChanges :: ReportOpts -> Query -> [DateSpan]-                        -> Map DateSpan [Posting]+calculateAccountChanges :: ReportSpec -> [DateSpan] -> Map DateSpan [Posting]                         -> HashMap ClippedAccountName (Map DateSpan Account)-calculateAccountChanges ropts q colspans colps-    | queryDepth q == Just 0 = acctchanges <> elided-    | otherwise              = acctchanges+calculateAccountChanges rspec colspans colps+    | queryDepth (rsQuery rspec) == Just 0 = acctchanges <> elided+    | otherwise = acctchanges   where     -- Transpose to get each account's balance changes across all columns.     acctchanges = transposeMap colacctchanges      colacctchanges :: Map DateSpan (HashMap ClippedAccountName Account) =-      dbg'' "colacctchanges" $ fmap (acctChangesFromPostings ropts q) colps+      dbg5 "colacctchanges" $ fmap (acctChangesFromPostings rspec) colps      elided = HM.singleton "..." $ M.fromList [(span, nullacct) | span <- colspans] @@ -309,94 +327,111 @@ -- the accounts that have postings and calculate the change amount for -- each. Accounts and amounts will be depth-clipped appropriately if -- a depth limit is in effect.-acctChangesFromPostings :: ReportOpts -> Query -> [Posting] -> HashMap ClippedAccountName Account-acctChangesFromPostings ropts q ps = HM.fromList [(aname a, a) | a <- as]+acctChangesFromPostings :: ReportSpec -> [Posting] -> HashMap ClippedAccountName Account+acctChangesFromPostings ReportSpec{rsQuery=query,rsOpts=ropts} ps =+    HM.fromList [(aname a, a) | a <- as]   where     as = filterAccounts . drop 1 $ accountsFromPostings ps     filterAccounts = case accountlistmode_ ropts of         ALTree -> filter ((depthq `matchesAccount`) . aname)      -- exclude deeper balances         ALFlat -> clipAccountsAndAggregate (queryDepth depthq) .  -- aggregate deeper balances at the depth limit.                       filter ((0<) . anumpostings)-    depthq = dbg "depthq" $ filterQuery queryIsDepth q+    depthq = dbg3 "depthq" $ filterQuery queryIsDepth query  -- | Accumulate and value amounts, as specified by the report options. -- -- Makes sure all report columns have an entry.-accumValueAmounts :: ReportOpts -> Journal -> PriceOracle -> [DateSpan]+accumValueAmounts :: ReportOpts -> (Day -> MixedAmount -> MixedAmount) -> [DateSpan]                   -> HashMap ClippedAccountName Account                   -> HashMap ClippedAccountName (Map DateSpan Account)                   -> HashMap ClippedAccountName (Map DateSpan Account)-accumValueAmounts ropts j priceoracle colspans startbals acctchanges =  -- PARTIAL:-    HM.mapWithKey processRow $ acctchanges <> (mempty <$ startbals)+accumValueAmounts ropts valuation colspans startbals acctchanges =  -- PARTIAL:+    -- Ensure all columns have entries, including those with starting balances+    HM.mapWithKey rowbals $ ((<>zeros) <$> acctchanges) <> (zeros <$ startbals)   where-    -- Must accumulate before valuing, since valuation can change without any-    -- postings. Make sure every column has an entry.-    processRow name changes = M.mapWithKey valueAcct . rowbals name $ changes <> zeros--    -- The row amounts to be displayed: per-period changes,+    -- The valued row amounts to be displayed: per-period changes,     -- zero-based cumulative totals, or     -- starting-balance-based historical balances.-    rowbals name changes = dbg'' "rowbals" $ case balancetype_ ropts of-        PeriodChange      -> changes-        CumulativeChange  -> snd $ M.mapAccum f nullacct                  changes-        HistoricalBalance -> snd $ M.mapAccum f (startingBalanceFor name) changes-      where f a b = let s = sumAcct a b in (s, s)+    rowbals name changes = dbg5 "rowbals" $ case balancetype_ ropts of+        PeriodChange      -> changeamts+        CumulativeChange  -> cumulative+        HistoricalBalance -> historical+      where+        historical = cumulativeSum startingBalance+        cumulative | fixedValuationDate = cumulativeSum nullacct+                   | otherwise          = fmap (`subtractAcct` valuedStart) historical+        changeamts | fixedValuationDate = M.mapWithKey valueAcct changes+                   | otherwise          = M.fromDistinctAscList . zip dates $+                                            zipWith subtractAcct histamts (valuedStart:histamts)+          where (dates, histamts) = unzip $ M.toAscList historical +        cumulativeSum start = snd $ M.mapAccumWithKey accumValued start changes+          where accumValued startAmt date newAmt = (s, valueAcct date s)+                  where s = sumAcct startAmt newAmt++        -- Whether the market price is measured at the same date for all report+        -- periods, and we can therefore use the simpler calculations for+        -- cumulative and change reports.+        fixedValuationDate = case value_ ropts of+            Just (AtCost (Just _)) -> singleperiod+            Just (AtEnd  _)        -> singleperiod+            Just (AtDefault _)     -> singleperiod+            _                      -> True+          where singleperiod = interval_ ropts == NoInterval++        startingBalance = HM.lookupDefault nullacct name startbals+        valuedStart = valueAcct (DateSpan Nothing historicalDate) startingBalance+     -- Add the values of two accounts. Should be right-biased, since it's used     -- in scanl, so other properties (such as anumpostings) stay in the right place     sumAcct Account{aibalance=i1,aebalance=e1} a@Account{aibalance=i2,aebalance=e2} =         a{aibalance = i1 + i2, aebalance = e1 + e2} +    -- Subtract the values in one account from another. Should be left-biased.+    subtractAcct a@Account{aibalance=i1,aebalance=e1} Account{aibalance=i2,aebalance=e2} =+        a{aibalance = i1 - i2, aebalance = e1 - e2}+     -- We may be converting amounts to value, per hledger_options.m4.md "Effect of --value on reports".     valueAcct (DateSpan _ (Just end)) acct =         acct{aibalance = value (aibalance acct), aebalance = value (aebalance acct)}-      where value = avalue (addDays (-1) end)-    valueAcct _ _ = error' "multiBalanceReport: expected all spans to have an end date"  -- XXX should not happen--    avalue periodlast = maybe id-        (mixedAmountApplyValuation priceoracle styles periodlast mreportlast today multiperiod) $-        value_ ropts-      where-        -- Some things needed if doing valuation.-        styles = journalCommodityStyles j-        mreportlast = reportPeriodLastDay ropts-        today = fromMaybe (error' "multiBalanceReport: could not pick a valuation date, ReportOpts today_ is unset") $ today_ ropts  -- XXX shouldn't happen-        multiperiod = interval_ ropts /= NoInterval+      where value = valuation (addDays (-1) end)+    valueAcct _ _ = error "multiBalanceReport: expected all spans to have an end date"  -- XXX should not happen -    startingBalanceFor a = HM.lookupDefault nullacct a startbals     zeros = M.fromList [(span, nullacct) | span <- colspans]+    historicalDate = minimumMay $ mapMaybe spanStart colspans   -- | Lay out a set of postings grouped by date span into a regular matrix with rows -- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport -- from the columns.-generateMultiBalanceReport :: ReportOpts -> Query -> Journal -> PriceOracle-                           -> [DateSpan]-                           -> Map DateSpan [Posting]-                           -> HashMap AccountName Account+generateMultiBalanceReport :: ReportSpec -> Journal -> (Day -> MixedAmount -> MixedAmount) -> [DateSpan]+                           -> Map DateSpan [Posting] -> HashMap AccountName Account                            -> MultiBalanceReport-generateMultiBalanceReport ropts q j priceoracle colspans colps startbals = report+generateMultiBalanceReport rspec@ReportSpec{rsOpts=ropts} j valuation colspans colps startbals =+    report   where     -- Each account's balance changes across all columns.-    acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q colspans colps+    acctchanges = dbg5 "acctchanges" $ calculateAccountChanges rspec colspans colps      -- Process changes into normal, cumulative, or historical amounts, plus value them-    accumvalued = accumValueAmounts ropts j priceoracle colspans startbals acctchanges+    accumvalued = accumValueAmounts ropts valuation colspans startbals acctchanges      -- All account names that will be displayed, possibly depth-clipped.-    displaynames = dbg'' "displaynames" $ displayedAccounts ropts q accumvalued+    displaynames = dbg5 "displaynames" $ displayedAccounts rspec accumvalued      -- All the rows of the report.-    rows = dbg'' "rows" $ buildReportRows ropts displaynames accumvalued+    rows = dbg5 "rows"+             . (if invert_ ropts then map (fmap negate) else id)  -- Negate amounts if applicable+             $ buildReportRows ropts displaynames accumvalued      -- Calculate column totals-    totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts rows+    totalsrow = dbg5 "totalsrow" $ calculateTotalsRow ropts rows      -- Sorted report rows.-    sortedrows = dbg' "sortedrows" $ sortRows ropts j rows+    sortedrows = dbg5 "sortedrows" $ sortRows ropts j rows -    -- Postprocess the report, negating balances and taking percentages if needed-    report = postprocessReport ropts $ PeriodicReport colspans sortedrows totalsrow+    -- Take percentages if needed+    report = reportPercent ropts $ PeriodicReport colspans sortedrows totalsrow  -- | Build the report rows. --@@ -423,10 +458,9 @@  -- | Calculate accounts which are to be displayed in the report, as well as -- their name and depth-displayedAccounts :: ReportOpts -> Query-                  -> HashMap AccountName (Map DateSpan Account)+displayedAccounts :: ReportSpec -> HashMap AccountName (Map DateSpan Account)                   -> HashMap AccountName DisplayName-displayedAccounts ropts q valuedaccts+displayedAccounts ReportSpec{rsQuery=query,rsOpts=ropts} valuedaccts     | depth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1     | otherwise  = HM.mapWithKey (\a _ -> displayedName a) displayedAccts   where@@ -459,7 +493,7 @@                 | otherwise = aebalance      -- Accounts interesting because they are a fork for interesting subaccounts-    interestingParents = dbg'' "interestingParents" $ case accountlistmode_ ropts of+    interestingParents = dbg5 "interestingParents" $ case accountlistmode_ ropts of         ALTree -> HM.filterWithKey hasEnoughSubs numSubs         ALFlat -> mempty       where@@ -467,7 +501,7 @@         minSubs = if no_elide_ ropts then 1 else 2      isZeroRow balance = all (mixedAmountLooksZero . balance)-    depth = fromMaybe maxBound $ queryDepth q+    depth = fromMaybe maxBound $  queryDepth query     numSubs = subaccountTallies . HM.keys $ HM.filterWithKey isInteresting valuedaccts  -- | Sort the rows by amount or by account declaration order.@@ -518,7 +552,7 @@      colamts = transpose . map prrAmounts $ filter isTopRow rows -    coltotals :: [MixedAmount] = dbg'' "coltotals" $ map sum colamts+    coltotals :: [MixedAmount] = dbg5 "coltotals" $ map sum colamts      -- Calculate the grand total and average. These are always the sum/average     -- of the column totals.@@ -528,21 +562,17 @@         _            -> lastDef 0 coltotals     grandaverage = averageMixedAmounts coltotals --- | Map the report rows to percentages and negate if needed-postprocessReport :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport-postprocessReport ropts = maybePercent . maybeInvert+-- | Map the report rows to percentages if needed+reportPercent :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport+reportPercent ropts report@(PeriodicReport spans rows totalrow)+  | percent_ ropts = PeriodicReport spans (map percentRow rows) (percentRow totalrow)+  | otherwise      = report   where-    maybeInvert  = if invert_  ropts then fmap negate else id-    maybePercent = if percent_ ropts then prPercent   else id--    prPercent (PeriodicReport spans rows totalrow) =-        PeriodicReport spans (map percentRow rows) (percentRow totalrow)-      where-        percentRow (PeriodicReportRow name rowvals rowtotal rowavg) =-            PeriodicReportRow name-                (zipWith perdivide rowvals $ prrAmounts totalrow)-                (perdivide rowtotal $ prrTotal totalrow)-                (perdivide rowavg $ prrAverage totalrow)+    percentRow (PeriodicReportRow name rowvals rowtotal rowavg) =+      PeriodicReportRow name+        (zipWith perdivide rowvals $ prrAmounts totalrow)+        (perdivide rowtotal $ prrTotal totalrow)+        (perdivide rowavg $ prrAverage totalrow)   -- | Transpose a Map of HashMaps to a HashMap of Maps.@@ -583,37 +613,16 @@     return $ mixed [per $ if aquantity b' == 0 then 0 else aquantity a' / abs (aquantity b') * 100]   where errmsg = "Cannot calculate percentages if accounts have different commodities (Hint: Try --cost, -V or similar flags.)" --- Local debug helper--- add a prefix to this function's debug output-dbg   s = let p = "multiBalanceReport" in Hledger.Utils.dbg3 (p++" "++s)-dbg'  s = let p = "multiBalanceReport" in Hledger.Utils.dbg4 (p++" "++s)-dbg'' s = let p = "multiBalanceReport" in Hledger.Utils.dbg5 (p++" "++s)--- dbg = const id  -- exclude this function from debug output---- common rendering helper, XXX here for now-tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String-tableAsText (ReportOpts{pretty_tables_ = pretty}) showcell =-  unlines-  . trimborder-  . lines-  . render pretty id id showcell-  . align-  where-    trimborder = drop 1 . init . map (drop 1 . init)-    align (Table l t d) = Table l' t d-      where-        acctswidth = maximum' $ map strWidth (headerContents l)-        l'         = padRightWide acctswidth <$> l- -- tests  tests_MultiBalanceReport = tests "MultiBalanceReport" [    let     amt0 = Amount {acommodity="$", aquantity=0, aprice=Nothing, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = Precision 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}, aismultiplier=False}-    (opts,journal) `gives` r = do-      let (eitems, etotal) = r-          (PeriodicReport _ aitems atotal) = multiBalanceReport nulldate opts journal+    (rspec,journal) `gives` r = do+      let rspec' = rspec{rsQuery=And [queryFromFlags $ rsOpts rspec, rsQuery rspec]}+          (eitems, etotal) = r+          (PeriodicReport _ aitems atotal) = multiBalanceReport rspec' journal           showw (PeriodicReportRow a lAmt amt amt')               = (displayFull a, displayName a, displayDepth a, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt')       (map showw aitems) @?= (map showw eitems)@@ -621,10 +630,10 @@   in    tests "multiBalanceReport" [       test "null journal"  $-      (defreportopts, nulljournal) `gives` ([], Mixed [nullamt])+      (defreportspec, nulljournal) `gives` ([], Mixed [nullamt])       ,test "with -H on a populated period"  $-      (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`+      (defreportspec{rsOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}}, samplejournal) `gives`        (         [ PeriodicReportRow (flatDisplayName "assets:bank:checking") [mamountp' "$1.00"]  (mamountp' "$1.00")  (Mixed [amt0 {aquantity=1}])         , PeriodicReportRow (flatDisplayName "income:salary")        [mamountp' "$-1.00"] (mamountp' "$-1.00") (Mixed [amt0 {aquantity=(-1)}])
Hledger/Reports/PostingsReport.hs view
@@ -65,20 +65,19 @@  -- | Select postings from the journal and add running balance and other -- information to make a postings report. Used by eg hledger's register command.-postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport-postingsReport ropts@ReportOpts{..} q j =+postingsReport :: ReportSpec -> Journal -> PostingsReport+postingsReport rspec@ReportSpec{rsOpts=ropts@ReportOpts{..}} j =   (totallabel, items)     where-      reportspan  = adjustReportDates ropts q j+      reportspan  = adjustReportDates rspec j       whichdate   = whichDateFromOpts ropts-      mdepth      = queryDepth q+      mdepth      = queryDepth $ rsQuery rspec       styles      = journalCommodityStyles j       priceoracle = journalPriceOracle infer_value_ j       multiperiod = interval_ /= NoInterval-      today       = fromMaybe (error' "postingsReport: could not pick a valuation date, ReportOpts today_ is unset") today_  -- PARTIAL:        -- postings to be included in the report, and similarly-matched postings before the report start date-      (precedingps, reportps) = matchedPostingsBeforeAndDuring ropts q j reportspan+      (precedingps, reportps) = matchedPostingsBeforeAndDuring rspec j reportspan        -- Postings, or summary postings with their subperiod's end date, to be displayed.       displayps :: [(Posting, Maybe Day)]@@ -90,12 +89,12 @@         where           showempty = empty_ || average_           -- We may be converting posting amounts to value, per hledger_options.m4.md "Effect of --value on reports".-          pvalue p periodlast = maybe p (postingApplyValuation priceoracle styles periodlast mreportlast today multiperiod p) value_+          pvalue p periodlast = maybe p (postingApplyValuation priceoracle styles periodlast mreportlast (rsToday rspec) multiperiod p) value_             where-              mreportlast = reportPeriodLastDay ropts+              mreportlast = reportPeriodLastDay rspec           reportorjournallast =             fromMaybe (error' "postingsReport: expected a non-empty journal") $  -- PARTIAL: shouldn't happen-            reportPeriodOrJournalLastDay ropts j+            reportPeriodOrJournalLastDay rspec j        -- Posting report items ready for display.       items =@@ -113,13 +112,13 @@               precedingsum = sumPostings precedingps               precedingavg | null precedingps = 0                            | otherwise        = divideMixedAmount (fromIntegral $ length precedingps) precedingsum-              bvalue = maybe id (mixedAmountApplyValuation priceoracle styles daybeforereportstart Nothing today multiperiod) value_+              bvalue = maybe id (mixedAmountApplyValuation priceoracle styles daybeforereportstart Nothing (rsToday rspec) multiperiod) value_                   -- XXX constrain valuation type to AtDate daybeforereportstart here ?                 where                   daybeforereportstart =                     maybe (error' "postingsReport: expected a non-empty journal")  -- PARTIAL: shouldn't happen                     (addDays (-1))-                    $ reportPeriodOrJournalStart ropts j+                    $ reportPeriodOrJournalStart rspec j            runningcalc = registerRunningCalculationFn ropts           startnum = if historical then length precedingps + 1 else 1@@ -140,17 +139,17 @@ -- 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+adjustReportDates :: ReportSpec -> Journal -> DateSpan+adjustReportDates rspec@ReportSpec{rsOpts=ropts} j = reportspan   where     -- see also multiBalanceReport-    requestedspan       = dbg3 "requestedspan"       $ queryDateSpan' q                                       -- span specified by -b/-e/-p options and query args+    requestedspan       = dbg3 "requestedspan"       $ queryDateSpan' $ rsQuery rspec                         -- span specified by -b/-e/-p options and query args     journalspan         = dbg3 "journalspan"         $ dates `spanUnion` date2s                               -- earliest and latest dates (or date2s) in the journal       where         dates  = journalDateSpan False j         date2s = journalDateSpan True  j     requestedspanclosed = dbg3 "requestedspanclosed" $ requestedspan `spanDefaultsFrom` journalspan           -- if open-ended, close it using the journal's dates (if any)-    intervalspans       = dbg3 "intervalspans"       $ splitSpan (interval_ opts) requestedspanclosed  -- get the whole intervals enclosing that+    intervalspans       = dbg3 "intervalspans"       $ splitSpan (interval_ ropts) requestedspanclosed  -- get the whole intervals enclosing that     mreportstart        = dbg3 "reportstart"         $ maybe Nothing spanStart $ headMay intervalspans        -- start of the first interval, or open ended     mreportend          = dbg3 "reportend"           $ maybe Nothing spanEnd   $ lastMay intervalspans        -- end of the last interval, or open ended     reportspan          = dbg3 "reportspan"          $ DateSpan mreportstart mreportend                       -- the requested span enlarged to whole intervals if possible@@ -159,29 +158,29 @@ -- 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) =+matchedPostingsBeforeAndDuring :: ReportSpec -> Journal -> DateSpan -> ([Posting],[Posting])+matchedPostingsBeforeAndDuring ReportSpec{rsOpts=ropts,rsQuery=q} j (DateSpan mstart mend) =   dbg5 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps   where     beforestartq = dbg3 "beforestartq" $ dateqtype $ DateSpan Nothing mstart     beforeandduringps =-      dbg5 "ps5" $ sortOn sortdate $                                           -- sort postings by date or date2-      dbg5 "ps4" $ (if invert_ opts then map negatePostingAmount else id) $    -- with --invert, invert amounts-      dbg5 "ps3" $ map (filterPostingAmount symq) $                            -- remove amount parts which the query's cur: terms would exclude-      dbg5 "ps2" $ (if related_ opts then concatMap relatedPostings else id) $ -- with -r, replace each with its sibling postings-      dbg5 "ps1" $ filter (beforeandduringq `matchesPosting`) $                -- filter postings by the query, with no start date or depth limit+      dbg5 "ps5" $ sortOn sortdate $                                             -- sort postings by date or date2+      dbg5 "ps4" $ (if invert_ ropts then map negatePostingAmount else id) $     -- with --invert, invert amounts+      dbg5 "ps3" $ map (filterPostingAmount symq) $                              -- remove amount parts which the query's cur: terms would exclude+      dbg5 "ps2" $ (if related_ ropts then concatMap relatedPostings else id) $  -- with -r, replace each with its sibling postings+      dbg5 "ps1" $ filter (beforeandduringq `matchesPosting`) $                  -- filter postings by the query, with no start date or depth limit                   journalPostings $-                  journalSelectingAmountFromOpts opts j    -- maybe convert to cost early, will be seen by amt:. XXX what about converting to value ?+                  journalSelectingAmountFromOpts ropts j    -- maybe convert to cost early, will be seen by amt:. XXX what about converting to value ?       where         beforeandduringq = dbg4 "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+        sortdate = if date2_ ropts then postingDate2 else postingDate         symq = dbg4 "symq" $ filterQuery queryIsSym q     dateqtype-      | queryIsDate2 dateq || (queryIsDate dateq && date2_ opts) = Date2+      | queryIsDate2 dateq || (queryIsDate dateq && date2_ ropts) = Date2       | otherwise = Date       where         dateq = dbg4 "dateq" $ filterQuery queryIsDateOrDate2 $ dbg4 "q" q  -- XXX confused by multiple date:/date2: ?@@ -270,7 +269,7 @@ tests_PostingsReport = tests "PostingsReport" [     test "postingsReport" $ do-    let (query, journal) `gives` n = (length $ snd $ postingsReport defreportopts query journal) @?= n+    let (query, journal) `gives` n = (length $ snd $ postingsReport defreportspec{rsQuery=query} journal) @?= n     -- with the query specified explicitly     (Any, nulljournal) `gives` 0     (Any, samplejournal) `gives` 13@@ -279,10 +278,10 @@     (And [Depth 1, StatusQ Cleared, Acct (toRegex' "expenses")], samplejournal) `gives` 2     (And [And [Depth 1, StatusQ Cleared], Acct (toRegex' "expenses")], samplejournal) `gives` 2     -- with query and/or command-line options-    (length $ snd $ postingsReport defreportopts Any samplejournal) @?= 13-    (length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal) @?= 11-    (length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal) @?= 20-    (length $ snd $ postingsReport defreportopts (Acct (toRegex' "assets:bank:checking")) samplejournal) @?= 5+    (length $ snd $ postingsReport defreportspec samplejournal) @?= 13+    (length $ snd $ postingsReport defreportspec{rsOpts=defreportopts{interval_=Months 1}} samplejournal) @?= 11+    (length $ snd $ postingsReport defreportspec{rsOpts=defreportopts{interval_=Months 1, empty_=True}} samplejournal) @?= 20+    (length $ snd $ postingsReport defreportspec{rsQuery=Acct $ toRegex' "assets:bank:checking"} samplejournal) @?= 5       -- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0      -- [(Just (fromGregorian 2008 01 01,"income"),assets:bank:checking             $1,$1)
Hledger/Reports/ReportOptions.hs view
@@ -10,13 +10,16 @@  module Hledger.Reports.ReportOptions (   ReportOpts(..),+  ReportSpec(..),   BalanceType(..),   AccountListMode(..),   ValuationType(..),-  FormatStr,   defreportopts,   rawOptsToReportOpts,-  checkReportOpts,+  defreportspec,+  reportOptsToSpec,+  updateReportSpecFromOpts,+  rawOptsToReportSpec,   flat_,   tree_,   reportOptsToggleStatus,@@ -25,25 +28,18 @@   journalSelectingAmountFromOpts,   intervalFromRawOpts,   forecastPeriodFromRawOpts,-  queryFromOpts,-  queryFromOptsOnly,-  queryOptsFromOpts,+  queryFromFlags,   transactionDateFn,   postingDateFn,   reportSpan,   reportStartDate,   reportEndDate,-  specifiedStartEndDates,-  specifiedStartDate,-  specifiedEndDate,   reportPeriodStart,   reportPeriodOrJournalStart,   reportPeriodLastDay,   reportPeriodOrJournalLastDay,   valuationTypeIsCost,   valuationTypeIsDefaultValue,--  tests_ReportOptions ) where @@ -51,7 +47,7 @@ import Data.List.Extra (nubSort) import Data.Maybe (fromMaybe, isJust) import qualified Data.Text as T-import Data.Time.Calendar (Day, addDays, fromGregorian)+import Data.Time.Calendar (Day, addDays) import Data.Default (Default(..)) import Safe (lastDef, lastMay) @@ -65,8 +61,6 @@ import Hledger.Utils  -type FormatStr = String- -- | 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.@@ -88,11 +82,7 @@ -- commands, as noted below. data ReportOpts = ReportOpts {      -- for most reports:-     today_          :: Maybe Day  -- ^ The current date. A late addition to ReportOpts.-                                   -- Optional, but when set it may affect some reports:-                                   -- Reports use it when picking a -V valuation date.-                                   -- This is not great, adds indeterminacy.-    ,period_         :: Period+     period_         :: Period     ,interval_       :: Interval     ,statuses_       :: [Status]  -- ^ Zero, one, or two statuses to be matched     ,value_          :: Maybe ValuationType  -- ^ What value should amounts be converted to ?@@ -102,9 +92,8 @@     ,empty_          :: Bool     ,no_elide_       :: Bool     ,real_           :: Bool-    ,format_         :: Maybe FormatStr-    ,query_          :: String -- ^ All query arguments space sepeareted-                               --   and quoted if needed (see 'quoteIfNeeded')+    ,format_         :: StringFormat+    ,querystring_    :: [T.Text]     --     ,average_        :: Bool     -- for posting reports (register)@@ -142,100 +131,134 @@  defreportopts :: ReportOpts defreportopts = ReportOpts-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def-    def+    { period_          = PeriodAll+    , interval_        = NoInterval+    , statuses_        = []+    , value_           = Nothing+    , infer_value_     = False+    , depth_           = Nothing+    , date2_           = False+    , empty_           = False+    , no_elide_        = False+    , real_            = False+    , format_          = def+    , querystring_     = []+    , average_         = False+    , related_         = False+    , txn_dates_       = False+    , balancetype_     = def+    , accountlistmode_ = ALFlat+    , drop_            = 0+    , row_total_       = False+    , no_total_        = False+    , pretty_tables_   = False+    , sort_amount_     = False+    , percent_         = False+    , invert_          = False+    , normalbalance_   = Nothing+    , color_           = False+    , forecast_        = Nothing+    , transpose_       = False+    }  rawOptsToReportOpts :: RawOpts -> IO ReportOpts-rawOptsToReportOpts rawopts = checkReportOpts <$> do-  let rawopts' = checkRawOpts rawopts-  d <- getCurrentDay-  no_color <- isJust <$> lookupEnv "NO_COLOR"-  supports_color <- hSupportsANSIColor stdout-  let colorflag = stringopt "color" rawopts-  return defreportopts{-     today_       = Just d-    ,period_      = periodFromRawOpts d rawopts'-    ,interval_    = intervalFromRawOpts rawopts'-    ,statuses_    = statusesFromRawOpts rawopts'-    ,value_       = valuationTypeFromRawOpts rawopts'-    ,infer_value_ = boolopt "infer-value" rawopts'-    ,depth_       = maybeposintopt "depth" 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 . map quoteIfNeeded $ listofstringopt "args" rawopts' -- doesn't handle an arg like "" right-    ,average_     = boolopt "average" rawopts'-    ,related_     = boolopt "related" rawopts'-    ,txn_dates_   = boolopt "txn-dates" rawopts'-    ,balancetype_ = balancetypeopt rawopts'-    ,accountlistmode_ = accountlistmodeopt rawopts'-    ,drop_        = posintopt "drop" rawopts'-    ,row_total_   = boolopt "row-total" rawopts'-    ,no_total_    = boolopt "no-total" rawopts'-    ,sort_amount_ = boolopt "sort-amount" rawopts'-    ,percent_     = boolopt "percent" rawopts'-    ,invert_      = boolopt "invert" rawopts'-    ,pretty_tables_ = boolopt "pretty-tables" rawopts'-    ,color_       = and [not no_color-                        ,not $ colorflag `elem` ["never","no"]-                        ,colorflag `elem` ["always","yes"] || supports_color-                        ]-    ,forecast_    = forecastPeriodFromRawOpts d rawopts'-    ,transpose_   = boolopt "transpose" rawopts'+rawOptsToReportOpts rawopts = do+    d <- getCurrentDay+    no_color <- isJust <$> lookupEnv "NO_COLOR"+    supports_color <- hSupportsANSIColor stdout++    let colorflag    = stringopt "color" rawopts+        formatstring = maybestringopt "format" rawopts+        querystring  = map T.pack $ listofstringopt "args" rawopts  -- doesn't handle an arg like "" right++    format <- case parseStringFormat <$> formatstring of+        Nothing         -> return defaultBalanceLineFormat+        Just (Right x)  -> return x+        Just (Left err) -> fail $ "could not parse format option: " ++ err++    let reportopts = defreportopts+          {period_      = periodFromRawOpts d rawopts+          ,interval_    = intervalFromRawOpts rawopts+          ,statuses_    = statusesFromRawOpts rawopts+          ,value_       = valuationTypeFromRawOpts rawopts+          ,infer_value_ = boolopt "infer-value" rawopts+          ,depth_       = maybeposintopt "depth" rawopts+          ,date2_       = boolopt "date2" rawopts+          ,empty_       = boolopt "empty" rawopts+          ,no_elide_    = boolopt "no-elide" rawopts+          ,real_        = boolopt "real" rawopts+          ,format_      = format+          ,querystring_ = querystring+          ,average_     = boolopt "average" rawopts+          ,related_     = boolopt "related" rawopts+          ,txn_dates_   = boolopt "txn-dates" rawopts+          ,balancetype_ = balancetypeopt rawopts+          ,accountlistmode_ = accountlistmodeopt rawopts+          ,drop_        = posintopt "drop" rawopts+          ,row_total_   = boolopt "row-total" rawopts+          ,no_total_    = boolopt "no-total" rawopts+          ,sort_amount_ = boolopt "sort-amount" rawopts+          ,percent_     = boolopt "percent" rawopts+          ,invert_      = boolopt "invert" rawopts+          ,pretty_tables_ = boolopt "pretty-tables" rawopts+          ,color_       = and [not no_color+                              ,not $ colorflag `elem` ["never","no"]+                              ,colorflag `elem` ["always","yes"] || supports_color+                              ]+          ,forecast_    = forecastPeriodFromRawOpts d rawopts+          ,transpose_   = boolopt "transpose" rawopts+          }+    return reportopts++-- | The result of successfully parsing a ReportOpts on a particular+-- Day. Any ambiguous dates are completed and Queries are parsed,+-- ensuring that there are no regular expression errors. Values here+-- should be used in preference to re-deriving them from ReportOpts.+-- If you change the query_ in ReportOpts, you should call+-- `reportOptsToSpec` to regenerate the ReportSpec with the new+-- Query.+data ReportSpec = ReportSpec+  { rsOpts      :: ReportOpts  -- ^ The underlying ReportOpts used to generate this ReportSpec+  , rsToday     :: Day         -- ^ The Day this ReportSpec is generated for+  , rsQuery     :: Query       -- ^ The generated Query for the given day+  , rsQueryOpts :: [QueryOpt]  -- ^ A list of QueryOpts for the given day+  } deriving (Show)++instance Default ReportSpec where def = defreportspec++defreportspec :: ReportSpec+defreportspec = ReportSpec+    { rsOpts      = def+    , rsToday     = nulldate+    , rsQuery     = Any+    , rsQueryOpts = []     } --- | 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---     = usageError "please specify at most one of --change, --cumulative, --historical"---   | countopts ["flat","tree"] > 1---     = usageError "please specify at most one of --flat, --tree"---   | countopts ["daily","weekly","monthly","quarterly","yearly"] > 1---     = usageError "please specify at most one of --daily, "-  | otherwise = rawopts---   where---     countopts = length . filter (`boolopt` rawopts)+-- | Generate a ReportSpec from a set of ReportOpts on a given day.+reportOptsToSpec :: Day -> ReportOpts -> Either String ReportSpec+reportOptsToSpec day ropts = do+    (argsquery, queryopts) <- parseQueryList day $ querystring_ ropts+    return ReportSpec+      { rsOpts = ropts+      , rsToday = day+      , rsQuery = simplifyQuery $ And [queryFromFlags ropts, argsquery]+      , rsQueryOpts = queryopts+      } --- | Do extra validation of report options, raising an error if there's a problem.-checkReportOpts :: ReportOpts -> ReportOpts-checkReportOpts ropts@ReportOpts{..} =-  either usageError (const ropts) $ do-    case depth_ of-      Just d | d < 0 -> Left "--depth should have a positive number"-      _              -> Right ()+-- | Regenerate a ReportSpec after updating ReportOpts, or return an error+-- message if there is a problem such as missing or unparseable options data.+-- This helps keep the ReportSpec, its underlying ReportOpts, and the ReportOpts'+-- data fields like querystring_ all in sync.+updateReportSpecFromOpts :: (ReportOpts -> ReportOpts) -> ReportSpec -> Either String ReportSpec+updateReportSpecFromOpts f rspec = reportOptsToSpec (rsToday rspec) . f $ rsOpts rspec +-- | Generate a ReportSpec from RawOpts and the current date.+rawOptsToReportSpec :: RawOpts -> IO ReportSpec+rawOptsToReportSpec rawopts = do+    d <- getCurrentDay+    ropts <- rawOptsToReportOpts rawopts+    either fail return $ reportOptsToSpec d ropts+ accountlistmodeopt :: RawOpts -> AccountListMode accountlistmodeopt =   fromMaybe ALFlat . choiceopt parse where@@ -445,20 +468,11 @@     Just (AtCost _) -> journalToCost     _               -> id --- | Convert report options and arguments to a query.--- If there is a parsing problem, this function calls error.-queryFromOpts :: Day -> ReportOpts -> Query-queryFromOpts d ropts = simplifyQuery . And $ [flagsq, argsq]-  where-    flagsq = queryFromOptsOnly d ropts-    argsq = fst $ either error' id $ parseQuery d (T.pack $ query_ ropts)  -- PARTIAL:- -- | Convert report options to a query, ignoring any non-flag command line arguments.-queryFromOptsOnly :: Day -> ReportOpts -> Query-queryFromOptsOnly _d ReportOpts{..} = simplifyQuery $ And flagsq+queryFromFlags :: ReportOpts -> Query+queryFromFlags ReportOpts{..} = simplifyQuery $ And flagsq   where     flagsq = consIf   Real  real_-           . consIf   Empty empty_            . consJust Depth depth_            $   [ (if date2_ then Date2 else Date) $ periodAsDateSpan period_                , Or $ map StatusQ statuses_@@ -466,108 +480,51 @@     consIf f b = if b then (f True:) else id     consJust f = maybe id ((:) . f) --- | Convert report options and arguments to query options.--- If there is a parsing problem, this function calls error.-queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt]-queryOptsFromOpts d = snd . either error' id . parseQuery d . T.pack . query_  -- PARTIAL:- -- Report dates.  -- | The effective report span is the start and end dates specified by -- options or queries, or otherwise the earliest and latest transaction or -- posting dates in the journal. If no dates are specified by options/queries -- and the journal is empty, returns the null date span.--- Needs IO to parse smart dates in options/queries.-reportSpan :: Journal -> ReportOpts -> IO DateSpan-reportSpan j ropts = do-  (mspecifiedstartdate, mspecifiedenddate) <--    dbg3 "specifieddates" <$> specifiedStartEndDates ropts-  let+reportSpan :: Journal -> ReportSpec -> DateSpan+reportSpan j ReportSpec{rsQuery=query} = dbg3 "reportspan" $ DateSpan mstartdate menddate+  where     DateSpan mjournalstartdate mjournalenddate =       dbg3 "journalspan" $ journalDateSpan False j  -- ignore secondary dates-    mstartdate = mspecifiedstartdate <|> mjournalstartdate-    menddate   = mspecifiedenddate   <|> mjournalenddate-  return $ dbg3 "reportspan" $ DateSpan mstartdate menddate--reportStartDate :: Journal -> ReportOpts -> IO (Maybe Day)-reportStartDate j ropts = spanStart <$> reportSpan j ropts--reportEndDate :: Journal -> ReportOpts -> IO (Maybe Day)-reportEndDate j ropts = spanEnd <$> reportSpan j ropts---- | The specified report start/end dates are the dates specified by options or queries, if any.--- Needs IO to parse smart dates in options/queries.-specifiedStartEndDates :: ReportOpts -> IO (Maybe Day, Maybe Day)-specifiedStartEndDates ropts = do-  today <- getCurrentDay-  let-    q = queryFromOpts today ropts-    mspecifiedstartdate = queryStartDate False q-    mspecifiedenddate   = queryEndDate   False q-  return (mspecifiedstartdate, mspecifiedenddate)+    mstartdate = queryStartDate False query <|> mjournalstartdate+    menddate   = queryEndDate   False query <|> mjournalenddate -specifiedStartDate :: ReportOpts -> IO (Maybe Day)-specifiedStartDate ropts = fst <$> specifiedStartEndDates ropts+reportStartDate :: Journal -> ReportSpec -> Maybe Day+reportStartDate j = spanStart . reportSpan j -specifiedEndDate :: ReportOpts -> IO (Maybe Day)-specifiedEndDate ropts = snd <$> specifiedStartEndDates ropts+reportEndDate :: Journal -> ReportSpec -> Maybe Day+reportEndDate j = spanEnd . reportSpan j  -- Some pure alternatives to the above. XXX review/clean up  -- Get the report's start date. -- If no report period is specified, will be Nothing.--- Will also be Nothing if ReportOpts does not have today_ set,--- since we need that to get the report period robustly--- (unlike reportStartDate, which looks up the date with IO.)-reportPeriodStart :: ReportOpts -> Maybe Day-reportPeriodStart ropts@ReportOpts{..} = do-  t <- today_-  queryStartDate False $ queryFromOpts t ropts+reportPeriodStart :: ReportSpec -> Maybe Day+reportPeriodStart = queryStartDate False . rsQuery  -- Get the report's start date, or if no report period is specified, -- the journal's start date (the earliest posting date). If there's no -- report period and nothing in the journal, will be Nothing.-reportPeriodOrJournalStart :: ReportOpts -> Journal -> Maybe Day-reportPeriodOrJournalStart ropts j =-  reportPeriodStart ropts <|> journalStartDate False j+reportPeriodOrJournalStart :: ReportSpec -> Journal -> Maybe Day+reportPeriodOrJournalStart rspec j =+  reportPeriodStart rspec <|> journalStartDate False j  -- Get the last day of the overall report period. -- This the inclusive end date (one day before the -- more commonly used, exclusive, report end date). -- If no report period is specified, will be Nothing.--- Will also be Nothing if ReportOpts does not have today_ set,--- since we need that to get the report period robustly--- (unlike reportEndDate, which looks up the date with IO.)-reportPeriodLastDay :: ReportOpts -> Maybe Day-reportPeriodLastDay ropts@ReportOpts{..} = do-  t <- today_-  let q = queryFromOpts t ropts-  qend <- queryEndDate False q-  return $ addDays (-1) qend+reportPeriodLastDay :: ReportSpec -> Maybe Day+reportPeriodLastDay = fmap (addDays (-1)) . queryEndDate False . rsQuery  -- Get the last day of the overall report period, or if no report -- period is specified, the last day of the journal (ie the latest -- posting date). If there's no report period and nothing in the -- journal, will be Nothing.-reportPeriodOrJournalLastDay :: ReportOpts -> Journal -> Maybe Day-reportPeriodOrJournalLastDay ropts j =-  reportPeriodLastDay ropts <|> journalEndDate False j---- tests--tests_ReportOptions = tests "ReportOptions" [-   test "queryFromOpts" $ do-       queryFromOpts nulldate defreportopts @?= Any-       queryFromOpts nulldate defreportopts{query_="a"} @?= Acct (toRegexCI' "a")-       queryFromOpts nulldate defreportopts{query_="desc:'a a'"} @?= Desc (toRegexCI' "a a")-       queryFromOpts nulldate defreportopts{period_=PeriodFrom (fromGregorian 2012 01 01),query_="date:'to 2013'" }-         @?= (Date $ DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01))-       queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"} @?= (Date2 $ DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01))-       queryFromOpts nulldate defreportopts{query_="'a a' 'b"} @?= Or [Acct $ toRegexCI' "a a", Acct $ toRegexCI' "'b"]--  ,test "queryOptsFromOpts" $ do-      queryOptsFromOpts nulldate defreportopts @?= []-      queryOptsFromOpts nulldate defreportopts{query_="a"} @?= []-      queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (fromGregorian 2012 01 01)-                                              ,query_="date:'to 2013'"} @?= []- ]+reportPeriodOrJournalLastDay :: ReportSpec -> Journal -> Maybe Day+reportPeriodOrJournalLastDay rspec j =+  reportPeriodLastDay rspec <|> journalEndDate False j
Hledger/Reports/ReportTypes.hs view
@@ -17,7 +17,6 @@ , Average  , periodicReportSpan-, prNormaliseSign , prMapName , prMapMaybeName @@ -40,8 +39,10 @@ import Data.Semigroup (Semigroup(..)) #endif import GHC.Generics (Generic)+ import Hledger.Data import Hledger.Query (Query)+import Hledger.Reports.ReportOptions (ReportOpts)  type Percentage = Decimal @@ -104,17 +105,11 @@       sumPadded as     []     = as       sumPadded []     bs     = bs --- | Figure out the overall date span of a PeridicReport+-- | Figure out the overall date span of a PeriodicReport periodicReportSpan :: PeriodicReport a b -> DateSpan periodicReportSpan (PeriodicReport [] _ _)       = DateSpan Nothing Nothing periodicReportSpan (PeriodicReport colspans _ _) = DateSpan (spanStart $ head colspans) (spanEnd $ last colspans) --- | Given a PeriodicReport and its normal balance sign,--- if it is known to be normally negative, convert it to normally positive.-prNormaliseSign :: Num b => NormalSign -> PeriodicReport a b -> PeriodicReport a b-prNormaliseSign NormallyNegative = fmap negate-prNormaliseSign NormallyPositive = id- -- | Map a function over the row names. prMapName :: (a -> b) -> PeriodicReport a c -> PeriodicReport b c prMapName f report = report{prRows = map (prrMapName f) $ prRows report}@@ -153,15 +148,18 @@   , cbrDates      :: [DateSpan]   , cbrSubreports :: [(String, PeriodicReport a b, Bool)]   , cbrTotals     :: PeriodicReportRow () b-  } deriving (Show, Generic, ToJSON)+  } deriving (Show, Functor, Generic, ToJSON)  -- | Description of one subreport within a compound balance report. -- Part of a "CompoundBalanceCommandSpec", but also used in hledger-lib.-data CBCSubreportSpec = CBCSubreportSpec-  { cbcsubreporttitle          :: String-  , cbcsubreportquery          :: Journal -> Query-  , cbcsubreportnormalsign     :: NormalSign-  , cbcsubreportincreasestotal :: Bool+data CBCSubreportSpec a = CBCSubreportSpec+  { cbcsubreporttitle          :: String                    -- ^ The title to use for the subreport+  , cbcsubreportquery          :: Journal -> Query          -- ^ The Query to use for the subreport+  , cbcsubreportoptions        :: ReportOpts -> ReportOpts  -- ^ A function to transform the ReportOpts used to produce the subreport+  , cbcsubreporttransform      :: PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount  -- ^ A function to transform the result of the subreport+  , cbcsubreportincreasestotal :: Bool                      -- ^ Whether the subreport and overall report total are of the same sign (e.g. Assets are normally+                                                            --   positive in a balance sheet report, as is the overall total. Liabilities are normally of the+                                                            --   opposite sign.)   }  
Hledger/Utils.hs view
@@ -36,7 +36,6 @@  import Control.Monad (liftM, when) -- import Data.Char-import Data.Default import Data.FileEmbed (makeRelativeToProject, embedStringFile) import Data.List -- import Data.Maybe@@ -143,8 +142,6 @@   return $ utcToZonedTime tz t  -- misc--instance Default Bool where def = False  -- | Apply a function the specified number of times, -- which should be > 0 (otherwise does nothing).
Hledger/Utils/Debug.hs view
@@ -89,6 +89,7 @@ import           Control.Monad.IO.Class import           Data.List hiding (uncons) import qualified Data.Text as T+import qualified Data.Text.Lazy as TL import           Debug.Trace import           Hledger.Utils.Parse import           Safe (readDef)@@ -97,17 +98,27 @@ import           System.IO.Unsafe (unsafePerformIO) import           Text.Megaparsec import           Text.Printf-import           Text.Show.Pretty (ppShow, pPrint)+import           Text.Pretty.Simple  -- (defaultOutputOptionsDarkBg, OutputOptions(..), pShowOpt, pPrintOpt) --- | Pretty print. Easier alias for pretty-show's pPrint.+prettyopts = +    defaultOutputOptionsDarkBg+    -- defaultOutputOptionsLightBg+    -- defaultOutputOptionsNoColor+    { outputOptionsIndentAmount=2+    , outputOptionsCompact=True+    }++-- | Pretty print. Generic alias for pretty-simple's pPrint. pprint :: Show a => a -> IO ()-pprint = pPrint+pprint = pPrintOpt CheckColorTty prettyopts --- | Pretty show. Easier alias for pretty-show's ppShow.+-- | Pretty show. Generic alias for pretty-simple's pShow. pshow :: Show a => a -> String-pshow = ppShow+pshow = TL.unpack . pShowOpt prettyopts --- | Pretty trace. Easier alias for traceShowId + ppShow.+-- XXX some of the below can be improved with pretty-simple, https://github.com/cdepillabout/pretty-simple#readme++-- | Pretty trace. Easier alias for traceShowId + pShow. ptrace :: Show a => a -> a ptrace = traceWith pshow @@ -157,7 +168,7 @@ ptraceAt :: Show a => Int -> String -> a -> a ptraceAt level     | level > 0 && debugLevel < level = flip const-    | otherwise = \s a -> let p = ppShow a+    | otherwise = \s a -> let p = pshow a                               ls = lines p                               nlorspace | length ls > 1 = "\n"                                         | otherwise     = " " ++ take (10 - length s) (repeat ' ')@@ -303,7 +314,7 @@ plogAt lvl     | lvl > 0 && debugLevel < lvl = flip const     | otherwise = \s a ->-        let p = ppShow a+        let p = pshow a             ls = lines p             nlorspace | length ls > 1 = "\n"                       | otherwise     = " " ++ take (10 - length s) (repeat ' ')
Hledger/Utils/Regex.hs view
@@ -147,7 +147,9 @@ -- | A replacement pattern. May include numeric backreferences (\N). type Replacement = String --- | An regular expression compilation/processing error message.+-- | An error message arising during a regular expression operation.+-- Eg: trying to compile a malformed regular expression, or+-- trying to apply a malformed replacement pattern. type RegexError = String  -- helpers@@ -162,13 +164,20 @@  -- | A memoising version of regexReplace. Caches the result for each -- search pattern, replacement pattern, target string tuple.+-- This won't generate a regular expression parsing error since that+-- is pre-compiled nowadays, but there can still be a runtime error +-- from the replacement pattern, eg with a backreference referring +-- to a nonexistent match group. regexReplace :: Regexp -> Replacement -> String -> Either RegexError String regexReplace re repl = memo $ regexReplaceUnmemo re repl  -- helpers:  -- Replace this regular expression with this replacement pattern in this--- string, or return an error message.+-- string, or return an error message. (There should be no regexp+-- parsing errors these days since Regexp's compiled form is used,+-- but there can still be a runtime error from the replacement+-- pattern, eg a backreference referring to a nonexistent match group.) regexReplaceUnmemo :: Regexp -> Replacement -> String -> Either RegexError String regexReplaceUnmemo re repl s = foldM (replaceMatch repl) s (reverse $ match (reCompiled re) s :: [MatchText String])   where@@ -190,7 +199,7 @@             -- return the referenced group text, or an error message.             lookupMatchGroup :: MatchText String -> String -> Either RegexError String             lookupMatchGroup grps ('\\':s@(_:_)) | all isDigit s =-              case read s of n | n `elem` indices grps -> Right $ fst (grps ! n)+              case read s of n | n `elem` indices grps -> Right $ fst (grps ! n)  -- PARTIAL: should not fail, all digits                              _                         -> Left $ "no match group exists for backreference \"\\"++s++"\""             lookupMatchGroup _ s = Left $ "lookupMatchGroup called on non-numeric-backreference \""++s++"\", shouldn't happen"     backrefRegex = toRegex' "\\\\[0-9]+"  -- PARTIAL: should not fail
Hledger/Utils/String.hs view
@@ -40,6 +40,7 @@  -- * wide-character-aware layout  charWidth,  strWidth,+ strWidthAnsi,  takeWidth,  fitString,  fitStringMulti,@@ -48,14 +49,18 @@  ) where  -import Data.Char (isDigit, isSpace, toLower, toUpper)-import Data.List (intercalate, transpose)-import Text.Megaparsec (Parsec, (<|>), (<?>), anySingle, between, many, noneOf,-                        oneOf, parseMaybe, sepBy, takeWhileP, try)-import Text.Megaparsec.Char (char, string)+import Data.Char (isSpace, toLower, toUpper)+import Data.Default (def)+import Data.List (intercalate)+import Text.Megaparsec ((<|>), between, many, noneOf, sepBy)+import Text.Megaparsec.Char (char) import Text.Printf (printf)  import Hledger.Utils.Parse+import Hledger.Utils.Regex (toRegex', regexReplace)+import Text.Tabular (Header(..), Properties(..))+import Text.Tabular.AsciiWide (Align(..), Cell(..), TableOpts(..), renderRow)+import Text.WideString (strWidth, charWidth)   -- | Take elements from the end of a list.@@ -119,13 +124,17 @@ -- | 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++redirectchars) = show s+quoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars++redirectchars) = showChar '"' $ escapeQuotes s "\""                 | otherwise = s+  where+    escapeQuotes []       x = x+    escapeQuotes ('"':cs) x = showString "\\\"" $ escapeQuotes cs x+    escapeQuotes (c:cs)   x = showChar c        $ escapeQuotes cs x  -- | 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++"'"+singleQuoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = "'"++s++"'"                       | otherwise = s  quotechars, whitespacechars, redirectchars :: [Char]@@ -168,26 +177,16 @@ -- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded. -- Treats wide characters as double width. concatTopPadded :: [String] -> String-concatTopPadded strs = intercalate "\n" $ map concat $ transpose padded-    where-      lss = map lines strs-      h = maximum $ map length lss-      ypad ls = replicate (difforzero h (length ls)) "" ++ ls-      xpad ls = map (padLeftWide w) ls where w | null ls = 0-                                               | otherwise = maximum $ map strWidth ls-      padded = map (xpad . ypad) lss+concatTopPadded = renderRow def{tableBorders=False, borderSpaces=False}+                . Group NoLine . map (Header . cell)+  where cell = Cell BottomLeft . map (\x -> (x, strWidth x)) . lines  -- | 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+concatBottomPadded = renderRow def{tableBorders=False, borderSpaces=False}+                   . Group NoLine . map (Header . cell)+  where cell = Cell TopLeft . map (\x -> (x, strWidth x)) . lines   -- | Join multi-line strings horizontally, after compressing each of@@ -327,74 +326,20 @@                    | otherwise = ""   where cw = charWidth c --- from Pandoc (copyright John MacFarlane, GPL)--- see also http://unicode.org/reports/tr11/#Description---- | Calculate the render width of a string, considering--- wide characters (counted as double width), ANSI escape codes--- (not counted), and line breaks (in a multi-line string, the longest--- line determines the width).-strWidth :: String -> Int-strWidth = maximum . (0:) . map (foldr (\a b -> charWidth a + b) 0) . lines . stripAnsi+-- | Like strWidth, but also strips ANSI escape sequences before+-- calculating the width.+--+-- This is no longer used in code, as widths are calculated before+-- adding ANSI escape sequences, but is being kept around for now.+strWidthAnsi :: String -> Int+strWidthAnsi = strWidth . stripAnsi  -- | Strip ANSI escape sequences from a string. -- -- >>> stripAnsi "\ESC[31m-1\ESC[m" -- "-1" stripAnsi :: String -> String-stripAnsi s = case parseMaybe (many $ "" <$ try ansi <|> pure <$> anySingle) s of-    Nothing -> error "Bad ansi escape"  -- PARTIAL: should not happen-    Just xs -> concat xs-  where-    -- This parses lots of invalid ANSI escape codes, but that should be fine-    ansi = string "\ESC[" *> digitSemicolons *> suffix <?> "ansi" :: Parsec CustomErr String Char-    digitSemicolons = takeWhileP Nothing (\c -> isDigit c || c == ';')-    suffix = oneOf ['A', 'B', 'C', 'D', 'H', 'J', 'K', 'f', 'm', 's', 'u']---- | 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-+stripAnsi s = either err id $ regexReplace ansire "" s+ where+   err    = error "stripAnsi: invalid replacement pattern"      -- PARTIAL, shouldn't happen+   ansire = toRegex' "\ESC\\[([0-9]+;)*([0-9]+)?[ABCDHJKfmsu]"  -- PARTIAL, should succeed
Hledger/Utils/Text.hs view
@@ -69,8 +69,8 @@  -- import Hledger.Utils.Parse -- import Hledger.Utils.Regex-import Hledger.Utils.String (charWidth) import Hledger.Utils.Test+import Text.WideString (charWidth, textWidth)  -- lowercase, uppercase :: String -> String -- lowercase = map toLower@@ -343,63 +343,6 @@                 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   -- | Read a decimal number from a Text. Assumes the input consists only of digit
Text/Tabular/AsciiWide.hs view
@@ -3,83 +3,161 @@  module Text.Tabular.AsciiWide where +import Data.Maybe (fromMaybe)+import Data.Default (Default(..)) import Data.List (intersperse, transpose)+import Safe (maximumMay) import Text.Tabular-import Hledger.Utils.String+import Text.WideString (strWidth) --- | for simplicity, we assume that each cell is rendered---   on a single line-render :: Bool -- ^ pretty tables-       -> (rh -> String)-       -> (ch -> String)-       -> (a -> String)-       -> Table rh ch a-       -> String-render pretty fr fc f (Table rh ch cells) =-  unlines $ [ bar VT SingleLine   -- +--------------------------------------+-            , renderColumns pretty sizes ch2-            , bar VM DoubleLine   -- +======================================+-            ] ++-            (renderRs $ fmap renderR $ zipHeader [] cells $ fmap fr rh) ++-            [ bar VB SingleLine ] -- +--------------------------------------+++-- | The options to use for rendering a table.+data TableOpts = TableOpts+  { prettyTable  :: Bool  -- ^ Pretty tables+  , tableBorders :: Bool  -- ^ Whether to display the outer borders+  , borderSpaces :: Bool  -- ^ Whether to display spaces around bars+  } deriving (Show)++instance Default TableOpts where+  def = TableOpts { prettyTable  = False+                  , tableBorders = True+                  , borderSpaces = True+                  }++-- | Cell contents along an alignment+data Cell = Cell Align [(String, Int)]+    deriving (Show)++-- | How to align text in a cell+data Align = TopRight | BottomRight | BottomLeft | TopLeft+  deriving (Show)++emptyCell :: Cell+emptyCell = Cell TopRight []++-- | Create a single-line cell from the given contents with its natural width.+alignCell :: Align -> String -> Cell+alignCell a x = Cell a [(x, strWidth x)]++-- | Return the width of a Cell.+cellWidth :: Cell -> Int+cellWidth (Cell _ xs) = fromMaybe 0 . maximumMay $ map snd xs+++-- | Render a table according to common options, for backwards compatibility+render :: Bool -> (rh -> String) -> (ch -> String) -> (a -> String) -> Table rh ch a -> String+render pretty fr fc f = renderTable def{prettyTable=pretty} (cell . fr) (cell . fc) (cell . f)+  where cell = alignCell TopRight++-- | Render a table according to various cell specifications+renderTable :: TableOpts         -- ^ Options controlling Table rendering+            -> (rh -> Cell)  -- ^ Rendering function for row headers+            -> (ch -> Cell)  -- ^ Rendering function for column headers+            -> (a -> Cell)   -- ^ Function determining the string and width of a cell+            -> Table rh ch a+            -> String+renderTable topts@TableOpts{prettyTable=pretty, tableBorders=borders} fr fc f (Table rh ch cells) =+  unlines . addBorders $+    renderColumns topts sizes ch2+    : bar VM DoubleLine   -- +======================================++    : renderRs (fmap renderR $ zipHeader [] cellContents rowHeaders)  where-  bar vpos prop = concat (renderHLine vpos pretty sizes ch2 prop)+  renderR (cs,h) = renderColumns topts sizes $ Group DoubleLine+                     [ Header h+                     , fmap fst $ zipHeader emptyCell cs colHeaders+                     ]++  rowHeaders   = fmap fr rh+  colHeaders   = fmap fc ch+  cellContents = map (map f) cells+   -- ch2 and cell2 include the row and column labels-  ch2 = Group DoubleLine [Header "", fmap fc ch]-  cells2 = headerContents ch2-         : zipWith (\h cs -> h : map f cs) rhStrings cells-  ---  renderR (cs,h) = renderColumns pretty sizes $ Group DoubleLine-                    [ Header h-                    , fmap fst $ zipHeader "" (map f cs) ch]-  rhStrings = map fr $ headerContents rh+  ch2 = Group DoubleLine [Header emptyCell, colHeaders]+  cells2 = headerContents ch2 : zipWith (:) (headerContents rowHeaders) cellContents+   -- maximum width for each column-  sizes   = map (maximum . map strWidth) . transpose $ cells2+  sizes = map (fromMaybe 0 . maximumMay . map cellWidth) $ transpose cells2   renderRs (Header s)   = [s]-  renderRs (Group p hs) = concat . intersperse sep . map renderRs $ hs-    where sep = renderHLine VM pretty sizes ch2 p+  renderRs (Group p hs) = concat . intersperse sep $ map renderRs hs+    where sep = renderHLine VM borders pretty sizes ch2 p +  -- borders and bars+  addBorders xs = if borders then bar VT SingleLine : xs ++ [bar VB SingleLine] else xs+  bar vpos prop = concat $ renderHLine vpos borders pretty sizes ch2 prop++-- | Render a single row according to cell specifications.+renderRow :: TableOpts -> Header Cell -> String+renderRow topts h = renderColumns topts is h+  where is = map (\(Cell _ xs) -> fromMaybe 0 . maximumMay $ map snd xs) $ headerContents h++ verticalBar :: Bool -> Char verticalBar pretty = if pretty then '│' else '|' -leftBar :: Bool -> String-leftBar pretty = verticalBar pretty : " "+leftBar :: Bool -> Bool -> String+leftBar pretty True  = verticalBar pretty : " "+leftBar pretty False = [verticalBar pretty] -rightBar :: Bool -> String-rightBar pretty = " " ++ [verticalBar pretty]+rightBar :: Bool -> Bool -> String+rightBar pretty True  = ' ' : [verticalBar pretty]+rightBar pretty False = [verticalBar pretty] -midBar :: Bool -> String-midBar pretty = " " ++ verticalBar pretty : " "+midBar :: Bool -> Bool -> String+midBar pretty True  = ' ' : verticalBar pretty : " "+midBar pretty False = [verticalBar pretty] -doubleMidBar :: Bool -> String-doubleMidBar pretty = if pretty then " ║ " else " || "+doubleMidBar :: Bool -> Bool -> String+doubleMidBar pretty True  = if pretty then " ║ " else " || "+doubleMidBar pretty False = if pretty then "║" else "||"  -- | We stop rendering on the shortest list!-renderColumns :: Bool -- ^ pretty-              -> [Int] -- ^ max width for each column-              -> Header String+renderColumns :: TableOpts  -- ^ rendering options for the table+              -> [Int]      -- ^ max width for each column+              -> Header Cell               -> String-renderColumns pretty is h = leftBar pretty ++ coreLine ++ rightBar pretty- where-  coreLine = concatMap helper $ flattenHeader $ zipHeader 0 is h-  helper = either hsep (uncurry padLeftWide)-  hsep :: Properties -> String-  hsep NoLine     = "  "-  hsep SingleLine = midBar pretty-  hsep DoubleLine = doubleMidBar pretty+renderColumns TableOpts{prettyTable=pretty, tableBorders=borders, borderSpaces=spaces} is h =+    concat . intersperse "\n"                    -- Put each line on its own line+    . map (addBorders . concat) . transpose      -- Change to a list of lines and add borders+    . map (either hsep padCell) . flattenHeader  -- We now have a matrix of strings+    . zipHeader 0 is $ padRow <$> h  -- Pad cell height and add width marker+  where+    -- Pad each cell to have the appropriate width+    padCell (w, Cell TopLeft     ls) = map (\(x,xw) -> x ++ replicate (w - xw) ' ') ls+    padCell (w, Cell BottomLeft  ls) = map (\(x,xw) -> x ++ replicate (w - xw) ' ') ls+    padCell (w, Cell TopRight    ls) = map (\(x,xw) -> replicate (w - xw) ' ' ++ x) ls+    padCell (w, Cell BottomRight ls) = map (\(x,xw) -> replicate (w - xw) ' ' ++ x) ls +    -- Pad each cell to have the same number of lines+    padRow (Cell TopLeft     ls) = Cell TopLeft     $ ls ++ replicate (nLines - length ls) ("",0)+    padRow (Cell TopRight    ls) = Cell TopRight    $ ls ++ replicate (nLines - length ls) ("",0)+    padRow (Cell BottomLeft  ls) = Cell BottomLeft  $ replicate (nLines - length ls) ("",0) ++ ls+    padRow (Cell BottomRight ls) = Cell BottomRight $ replicate (nLines - length ls) ("",0) ++ ls++    hsep :: Properties -> [String]+    hsep NoLine     = replicate nLines $ if spaces then "  " else ""+    hsep SingleLine = replicate nLines $ midBar pretty spaces+    hsep DoubleLine = replicate nLines $ doubleMidBar pretty spaces++    addBorders xs | borders   = leftBar pretty spaces ++ xs ++ rightBar pretty spaces+                  | spaces    =  ' ' : xs ++ " "+                  | otherwise = xs++    nLines = fromMaybe 0 . maximumMay . map (\(Cell _ ls) -> length ls) $ headerContents h+ renderHLine :: VPos+            -> Bool  -- ^ show outer borders             -> Bool -- ^ pretty             -> [Int] -- ^ width specifications-            -> Header String+            -> Header a             -> Properties             -> [String]-renderHLine _ _ _ _ NoLine = []-renderHLine vpos pretty w h prop = [renderHLine' vpos pretty prop w h]+renderHLine _ _ _ _ _ NoLine = []+renderHLine vpos borders pretty w h prop = [renderHLine' vpos borders pretty prop w h] -renderHLine' :: VPos -> Bool -> Properties -> [Int] -> Header String -> String-renderHLine' vpos pretty prop is h = edge HL ++ sep ++ coreLine ++ sep ++ edge HR+renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> String+renderHLine' vpos borders pretty prop is h = addBorders $ sep ++ coreLine ++ sep  where+  addBorders xs   = if borders then edge HL ++ xs ++ edge HR else xs   edge hpos       = boxchar vpos hpos SingleLine prop pretty   coreLine        = concatMap helper $ flattenHeader $ zipHeader 0 is h   helper          = either vsep dashes
+ Text/WideString.hs view
@@ -0,0 +1,71 @@+-- | Calculate the width of String and Text, being aware of wide characters.++module Text.WideString (+  -- * wide-character-aware layout+  strWidth,+  textWidth,+  charWidth+  ) where++import Data.Text (Text)+import qualified Data.Text as T+++-- | Calculate the render width of a string, considering+-- wide characters (counted as double width)+strWidth :: String -> Int+strWidth = foldr (\a b -> charWidth a + b) 0++-- | Calculate the render width of a string, considering+-- wide characters (counted as double width)+textWidth :: Text -> Int+textWidth = T.foldr (\a b -> charWidth a + b) 0++-- from Pandoc (copyright John MacFarlane, GPL)+-- see also http://unicode.org/reports/tr11/#Description++-- | 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+    | 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
hledger-lib.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 4cff01322093c9a60a07a65c0ed150394360e71104dd3e7b52fc6782c5cf5a57+-- hash: 496cff1a1bc952e36c246f175d8efcb4c6cab6e9adc79c17b2d1e8b71ddaa2e5  name:           hledger-lib-version:        1.19.1+version:        1.20 synopsis:       A reusable library providing the core functionality of hledger description:    A reusable library containing hledger's core functionality.                 This is used by most hledger* packages so that they support the same@@ -103,6 +103,7 @@       Text.Tabular.AsciiWide   other-modules:       Text.Megaparsec.Custom+      Text.WideString       Paths_hledger_lib   hs-source-dirs:       ./.@@ -126,7 +127,6 @@     , data-default >=0.5     , directory     , extra >=1.6.3-    , fgl >=5.5.4.0     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1@@ -135,7 +135,7 @@     , old-time     , parsec >=3     , parser-combinators >=0.4.0-    , pretty-show >=1.6.4+    , pretty-simple >4 && <5     , regex-tdfa     , safe >=0.2     , split >=0.1@@ -179,7 +179,6 @@     , directory     , doctest >=0.16.3     , extra >=1.6.3-    , fgl >=5.5.4.0     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1@@ -188,7 +187,7 @@     , old-time     , parsec >=3     , parser-combinators >=0.4.0-    , pretty-show >=1.6.4+    , pretty-simple >4 && <5     , regex-tdfa     , safe >=0.2     , split >=0.1@@ -233,7 +232,6 @@     , data-default >=0.5     , directory     , extra >=1.6.3-    , fgl >=5.5.4.0     , file-embed >=0.0.10     , filepath     , hashtables >=1.2.3.1@@ -243,7 +241,7 @@     , old-time     , parsec >=3     , parser-combinators >=0.4.0-    , pretty-show >=1.6.4+    , pretty-simple >4 && <5     , regex-tdfa     , safe >=0.2     , split >=0.1
hledger_csv.5 view
@@ -1,6 +1,6 @@ .\"t -.TH "hledger_csv" "5" "September 2020" "hledger 1.18.99" "hledger User Manuals"+.TH "hledger_csv" "5" "November 2020" "hledger 1.20" "hledger User Manuals"   @@ -75,9 +75,14 @@ T{ \f[B]\f[CB]date-format\f[B]\f[R] T}@T{-describe the format of CSV dates+how to parse dates in CSV records T} T{+\f[B]\f[CB]decimal-mark\f[B]\f[R]+T}@T{+the decimal mark used in CSV amounts, if ambiguous+T}+T{ \f[B]\f[CB]newest-first\f[B]\f[R] T}@T{ disambiguate record order when there\[aq]s only one date@@ -866,6 +871,27 @@ .P .PD https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime+.SS \f[C]decimal-mark\f[R]+.IP+.nf+\f[C]+decimal-mark .+\f[R]+.fi+.PP+or:+.IP+.nf+\f[C]+decimal-mark ,+\f[R]+.fi+.PP+hledger automatically accepts either period or comma as a decimal mark+when parsing numbers (cf Amounts).+However if any numbers in the CSV contain digit group marks, such as+thousand-separating commas, you should declare the decimal mark+explicitly with this rule, to avoid misparsed numbers. .SS \f[C]newest-first\f[R] .PP hledger always sorts the generated transactions by date.@@ -1087,31 +1113,80 @@ .SS Setting currency/commodity .PP If the currency/commodity symbol is included in the CSV\[aq]s amount-field(s), you don\[aq]t have to do anything special.+field(s):+.IP+.nf+\f[C]+2020-01-01,foo,$123.00+\f[R]+.fi .PP-If the currency is provided as a separate CSV field, you can either:-.IP \[bu] 2-assign that to \f[C]currency\f[R], which adds it to all posting amounts.-The symbol will prepended to the amount quantity (on the left side).-If you write a trailing space after the symbol, there will be a space-between symbol and amount (an exception to the usual whitespace-stripping).-.IP \[bu] 2-or assign it to \f[C]currencyN\f[R] which adds it to posting N\[aq]s-amount only.-.IP \[bu] 2-or for more control, construct the amount from symbol and quantity using-field assignment, eg:-.RS 2+you don\[aq]t have to do anything special for the commodity symbol, it+will be assigned as part of the amount.+Eg: .IP .nf \f[C]-fields date,description,currency,quantity-# add currency symbol on the right:-amount %quantity %currency+fields date,description,amount \f[R] .fi-.RE+.IP+.nf+\f[C]+2020-01-01 foo+    expenses:unknown         $123.00+    income:unknown          $-123.00+\f[R]+.fi+.PP+If the currency is provided as a separate CSV field:+.IP+.nf+\f[C]+2020-01-01,foo,USD,123.00+\f[R]+.fi+.PP+You can assign that to the \f[C]currency\f[R] pseudo-field, which has+the special effect of prepending itself to every amount in the+transaction (on the left, with no separating space):+.IP+.nf+\f[C]+fields date,description,currency,amount+\f[R]+.fi+.IP+.nf+\f[C]+2020-01-01 foo+    expenses:unknown       USD123.00+    income:unknown        USD-123.00+\f[R]+.fi+.PP+Or, you can use a field assignment to construct the amount yourself,+with more control.+Eg to put the symbol on the right, and separated by a space:+.IP+.nf+\f[C]+fields date,description,cur,amt+amount %amt %cur+\f[R]+.fi+.IP+.nf+\f[C]+2020-01-01 foo+    expenses:unknown        123.00 USD+    income:unknown         -123.00 USD+\f[R]+.fi+.PP+Note we used a temporary field name (\f[C]cur\f[R]) that is not+\f[C]currency\f[R] - that would trigger the prepending effect, which we+don\[aq]t want here. .SS Referencing other fields .PP In field assignments, you can interpolate only CSV fields, not hledger
hledger_csv.info view
@@ -3,8 +3,8 @@  File: hledger_csv.info,  Node: Top,  Next: EXAMPLES,  Up: (dir) -hledger_csv(5) hledger 1.18.99-******************************+hledger_csv(5) hledger 1.20+***************************  CSV - how hledger reads CSV data, and the CSV rules file format @@ -42,7 +42,9 @@ *'if' table*                    apply some rules to CSV records matched                                 by patterns, alternate syntax *'end'*                         skip the remaining CSV records-*'date-format'*                 describe the format of CSV dates+*'date-format'*                 how to parse dates in CSV records+*'decimal-mark'*                the decimal mark used in CSV amounts,+                                if ambiguous *'newest-first'*                disambiguate record order when there's                                 only one date *'include'*                     inline another CSV rules file@@ -387,6 +389,7 @@ * if table:: * end:: * date-format::+* decimal-mark:: * newest-first:: * include:: * balance-type::@@ -787,7 +790,7 @@  end  -File: hledger_csv.info,  Node: date-format,  Next: newest-first,  Prev: end,  Up: CSV RULES+File: hledger_csv.info,  Node: date-format,  Next: decimal-mark,  Prev: end,  Up: CSV RULES  2.8 'date-format' =================@@ -818,11 +821,29 @@ https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime  -File: hledger_csv.info,  Node: newest-first,  Next: include,  Prev: date-format,  Up: CSV RULES+File: hledger_csv.info,  Node: decimal-mark,  Next: newest-first,  Prev: date-format,  Up: CSV RULES -2.9 'newest-first'+2.9 'decimal-mark' ================== +decimal-mark .++   or:++decimal-mark ,++   hledger automatically accepts either period or comma as a decimal+mark when parsing numbers (cf Amounts).  However if any numbers in the+CSV contain digit group marks, such as thousand-separating commas, you+should declare the decimal mark explicitly with this rule, to avoid+misparsed numbers.+++File: hledger_csv.info,  Node: newest-first,  Next: include,  Prev: decimal-mark,  Up: CSV RULES++2.10 'newest-first'+===================+ hledger always sorts the generated transactions by date.  Transactions on the same date should appear in the same order as their CSV records, as hledger can usually auto-detect whether the CSV's normal order is@@ -842,7 +863,7 @@  File: hledger_csv.info,  Node: include,  Next: balance-type,  Prev: newest-first,  Up: CSV RULES -2.10 'include'+2.11 'include' ==============  include RULESFILE@@ -865,7 +886,7 @@  File: hledger_csv.info,  Node: balance-type,  Prev: include,  Up: CSV RULES -2.11 'balance-type'+2.12 'balance-type' ===================  Balance assertions generated by assigning to balanceN are of the simple@@ -1049,26 +1070,47 @@ ==============================  If the currency/commodity symbol is included in the CSV's amount-field(s), you don't have to do anything special.+field(s): -   If the currency is provided as a separate CSV field, you can either:+2020-01-01,foo,$123.00 -   * assign that to 'currency', which adds it to all posting amounts.-     The symbol will prepended to the amount quantity (on the left-     side).  If you write a trailing space after the symbol, there will-     be a space between symbol and amount (an exception to the usual-     whitespace stripping).+   you don't have to do anything special for the commodity symbol, it+will be assigned as part of the amount.  Eg: -   * or assign it to 'currencyN' which adds it to posting N's amount-     only.+fields date,description,amount -   * or for more control, construct the amount from symbol and quantity-     using field assignment, eg:+2020-01-01 foo+    expenses:unknown         $123.00+    income:unknown          $-123.00 -     fields date,description,currency,quantity-     # add currency symbol on the right:-     amount %quantity %currency+   If the currency is provided as a separate CSV field: +2020-01-01,foo,USD,123.00++   You can assign that to the 'currency' pseudo-field, which has the+special effect of prepending itself to every amount in the transaction+(on the left, with no separating space):++fields date,description,currency,amount++2020-01-01 foo+    expenses:unknown       USD123.00+    income:unknown        USD-123.00++   Or, you can use a field assignment to construct the amount yourself,+with more control.  Eg to put the symbol on the right, and separated by+a space:++fields date,description,cur,amt+amount %amt %cur++2020-01-01 foo+    expenses:unknown        123.00 USD+    income:unknown         -123.00 USD++   Note we used a temporary field name ('cur') that is not 'currency' -+that would trigger the prepending effect, which we don't want here.+  File: hledger_csv.info,  Node: Referencing other fields,  Next: How CSV rules are evaluated,  Prev: Setting currency/commodity,  Up: TIPS @@ -1150,84 +1192,86 @@  Tag Table: Node: Top72-Node: EXAMPLES2677-Ref: #examples2783-Node: Basic2991-Ref: #basic3091-Node: Bank of Ireland3633-Ref: #bank-of-ireland3768-Node: Amazon5230-Ref: #amazon5348-Node: Paypal7067-Ref: #paypal7161-Node: CSV RULES14805-Ref: #csv-rules14914-Node: skip15209-Ref: #skip15302-Node: fields15677-Ref: #fields15799-Node: Transaction field names16964-Ref: #transaction-field-names17124-Node: Posting field names17235-Ref: #posting-field-names17387-Node: account17457-Ref: #account17573-Node: amount18110-Ref: #amount18241-Node: currency19348-Ref: #currency19483-Node: balance19689-Ref: #balance19823-Node: comment20140-Ref: #comment20257-Node: field assignment20420-Ref: #field-assignment20563-Node: separator21381-Ref: #separator21516-Node: if block22056-Ref: #if-block22181-Node: Matching the whole record22582-Ref: #matching-the-whole-record22757-Node: Matching individual fields23561-Ref: #matching-individual-fields23765-Node: Combining matchers23989-Ref: #combining-matchers24185-Node: Rules applied on successful match24498-Ref: #rules-applied-on-successful-match24689-Node: if table25343-Ref: #if-table25462-Node: end27200-Ref: #end27312-Node: date-format27536-Ref: #date-format27668-Node: newest-first28417-Ref: #newest-first28555-Node: include29238-Ref: #include29369-Node: balance-type29813-Ref: #balance-type29933-Node: TIPS30633-Ref: #tips30715-Node: Rapid feedback30971-Ref: #rapid-feedback31088-Node: Valid CSV31548-Ref: #valid-csv31678-Node: File Extension31870-Ref: #file-extension32022-Node: Reading multiple CSV files32451-Ref: #reading-multiple-csv-files32636-Node: Valid transactions32877-Ref: #valid-transactions33055-Node: Deduplicating importing33683-Ref: #deduplicating-importing33862-Node: Setting amounts34895-Ref: #setting-amounts35064-Node: Setting currency/commodity36051-Ref: #setting-currencycommodity36243-Node: Referencing other fields37046-Ref: #referencing-other-fields37246-Node: How CSV rules are evaluated38143-Ref: #how-csv-rules-are-evaluated38316+Node: EXAMPLES2787+Ref: #examples2893+Node: Basic3101+Ref: #basic3201+Node: Bank of Ireland3743+Ref: #bank-of-ireland3878+Node: Amazon5340+Ref: #amazon5458+Node: Paypal7177+Ref: #paypal7271+Node: CSV RULES14915+Ref: #csv-rules15024+Node: skip15336+Ref: #skip15429+Node: fields15804+Ref: #fields15926+Node: Transaction field names17091+Ref: #transaction-field-names17251+Node: Posting field names17362+Ref: #posting-field-names17514+Node: account17584+Ref: #account17700+Node: amount18237+Ref: #amount18368+Node: currency19475+Ref: #currency19610+Node: balance19816+Ref: #balance19950+Node: comment20267+Ref: #comment20384+Node: field assignment20547+Ref: #field-assignment20690+Node: separator21508+Ref: #separator21643+Node: if block22183+Ref: #if-block22308+Node: Matching the whole record22709+Ref: #matching-the-whole-record22884+Node: Matching individual fields23688+Ref: #matching-individual-fields23892+Node: Combining matchers24116+Ref: #combining-matchers24312+Node: Rules applied on successful match24625+Ref: #rules-applied-on-successful-match24816+Node: if table25470+Ref: #if-table25589+Node: end27327+Ref: #end27439+Node: date-format27663+Ref: #date-format27795+Node: decimal-mark28544+Ref: #decimal-mark28687+Node: newest-first29026+Ref: #newest-first29167+Node: include29850+Ref: #include29981+Node: balance-type30425+Ref: #balance-type30545+Node: TIPS31245+Ref: #tips31327+Node: Rapid feedback31583+Ref: #rapid-feedback31700+Node: Valid CSV32160+Ref: #valid-csv32290+Node: File Extension32482+Ref: #file-extension32634+Node: Reading multiple CSV files33063+Ref: #reading-multiple-csv-files33248+Node: Valid transactions33489+Ref: #valid-transactions33667+Node: Deduplicating importing34295+Ref: #deduplicating-importing34474+Node: Setting amounts35507+Ref: #setting-amounts35676+Node: Setting currency/commodity36663+Ref: #setting-currencycommodity36855+Node: Referencing other fields38029+Ref: #referencing-other-fields38229+Node: How CSV rules are evaluated39126+Ref: #how-csv-rules-are-evaluated39299  End Tag Table 
hledger_csv.txt view
@@ -39,26 +39,28 @@        if table                         apply  some rules to CSV records matched                                         by patterns, alternate syntax        end                              skip the remaining CSV records-       date-format                      describe the format of CSV dates-       newest-first                     disambiguate record order  when  there's+       date-format                      how to parse dates in CSV records+       decimal-mark                     the decimal mark used in CSV amounts, if+                                        ambiguous+       newest-first                     disambiguate  record  order when there's                                         only one date        include                          inline another CSV rules file        balance-type                     choose which type of balance assignments                                         to use -       Note, for best error messages when reading CSV files, use a .csv,  .tsv+       Note,  for best error messages when reading CSV files, use a .csv, .tsv        or .ssv file extension or file prefix - see File Extension below.         There's an introductory Convert CSV files tutorial on hledger.org.  EXAMPLES-       Here  are  some sample hledger CSV rules files.  See also the full col-+       Here are some sample hledger CSV rules files.  See also the  full  col-        lection at:        https://github.com/simonmichael/hledger/tree/master/examples/csv     Basic-       At minimum, the rules file must identify the date  and  amount  fields,-       and  often  it also specifies the date format and how many header lines+       At  minimum,  the  rules file must identify the date and amount fields,+       and often it also specifies the date format and how many  header  lines        there are.  Here's a simple CSV file and a rules file for it:                Date, Description, Id, Amount@@ -77,8 +79,8 @@        Default account names are chosen, since we didn't set them.     Bank of Ireland-       Here's a CSV with two amount fields (Debit and Credit), and  a  balance-       field,  which we can use to add balance assertions, which is not neces-+       Here's  a  CSV with two amount fields (Debit and Credit), and a balance+       field, which we can use to add balance assertions, which is not  neces-        sary but provides extra error checking:                Date,Details,Debit,Credit,Balance@@ -120,13 +122,13 @@                   assets:bank:boi:checking         EUR-5.0 = EUR126.0                   expenses:unknown                  EUR5.0 -       The balance assertions don't raise an error above, because we're  read--       ing  directly  from  CSV, but they will be checked if these entries are+       The  balance assertions don't raise an error above, because we're read-+       ing directly from CSV, but they will be checked if  these  entries  are        imported into a journal file.     Amazon        Here we convert amazon.com order history, and use an if block to gener--       ate  a third posting if there's a fee.  (In practice you'd probably get+       ate a third posting if there's a fee.  (In practice you'd probably  get        this data from your bank instead, but it's an example.)                "Date","Type","To/From","Name","Status","Amount","Fees","Transaction ID"@@ -178,7 +180,7 @@                   expenses:fees           $1.00     Paypal-       Here's a real-world rules file for (customised) Paypal CSV,  with  some+       Here's  a  real-world rules file for (customised) Paypal CSV, with some        Paypal-specific rules, and a second rules file included:                "Date","Time","TimeZone","Name","Type","Status","Currency","Gross","Fee","Net","From Email Address","To Email Address","Transaction ID","Item Title","Item ID","Reference Txn ID","Receipt ID","Balance","Note"@@ -333,9 +335,9 @@    skip               skip N -       The word "skip" followed by a number (or no number,  meaning  1)  tells-       hledger  to  ignore  this  many non-empty lines preceding the CSV data.-       (Empty/blank lines are skipped automatically.) You'll need  this  when-+       The  word  "skip"  followed by a number (or no number, meaning 1) tells+       hledger to ignore this many non-empty lines  preceding  the  CSV  data.+       (Empty/blank  lines  are skipped automatically.) You'll need this when-        ever your CSV data contains header lines.         It also has a second purpose: it can be used inside if blocks to ignore@@ -344,27 +346,27 @@    fields               fields FIELDNAME1, FIELDNAME2, ... -       A fields list (the word  "fields"  followed  by  comma-separated  field-       names)  is  the quick way to assign CSV field values to hledger fields.+       A  fields  list  (the  word  "fields" followed by comma-separated field+       names) is the quick way to assign CSV field values to  hledger  fields.        It does two things: -       1. it names the CSV fields.  This is optional, but  can  be  convenient+       1. it  names  the  CSV fields.  This is optional, but can be convenient           later for interpolating them.         2. when you use a standard hledger field name, it assigns the CSV value           to that part of the hledger transaction. -       Here's an example that says "use the 1st, 2nd and  4th  fields  as  the-       transaction's  date,  description  and amount; name the last two fields+       Here's  an  example  that  says "use the 1st, 2nd and 4th fields as the+       transaction's date, description and amount; name the  last  two  fields        for later reference; and ignore the others":                fields date, description, , amount, , , somefield, anotherfield -       Field names may not contain whitespace.  Fields you  don't  care  about-       can  be  left  unnamed.  Currently there must be least two items (there+       Field  names  may  not contain whitespace.  Fields you don't care about+       can be left unnamed.  Currently there must be least  two  items  (there        must be at least one comma). -       Note, always use comma in the fields list, even if your  CSV  uses  an-+       Note,  always  use  comma in the fields list, even if your CSV uses an-        other separator character.         Here are the standard hledger field/pseudo-field names.  For more about@@ -377,52 +379,52 @@     Posting field names    account-       accountN, where N is 1 to 99, causes a posting to  be  generated,  with+       accountN,  where  N  is 1 to 99, causes a posting to be generated, with        that account name. -       Most  often  there are two postings, so you'll want to set account1 and-       account2.  Typically account1 is associated with the CSV file,  and  is-       set  once  with  a top-level assignment, while account2 is set based on+       Most often there are two postings, so you'll want to set  account1  and+       account2.   Typically  account1 is associated with the CSV file, and is+       set once with a top-level assignment, while account2 is  set  based  on        each transaction's description, and in conditional blocks. -       If a posting's account name is left unset but its amount  is  set  (see-       below),  a default account name will be chosen (like "expenses:unknown"+       If  a  posting's  account name is left unset but its amount is set (see+       below), a default account name will be chosen (like  "expenses:unknown"        or "income:unknown").     amount-       amountN sets posting N's amount.  If the CSV uses separate  fields  for-       inflows  and  outflows, you can use amountN-in and amountN-out instead.-       By assigning to amount1, amount2, ...  etc.  you can generate  anywhere+       amountN  sets  posting N's amount.  If the CSV uses separate fields for+       inflows and outflows, you can use amountN-in and  amountN-out  instead.+       By  assigning to amount1, amount2, ...  etc.  you can generate anywhere        from 0 to 99 postings. -       There  is  also  an older, unnumbered form of these names, suitable for+       There is also an older, unnumbered form of these  names,  suitable  for        2-posting transactions, which sets both posting 1's and (negated) post--       ing  2's  amount:  amount,  or amount-in and amount-out.  This is still-       supported because it keeps pre-hledger-1.17 csv  rules  files  working,-       and  because  it  can be more succinct, and because it converts posting+       ing 2's amount: amount, or amount-in and  amount-out.   This  is  still+       supported  because  it  keeps pre-hledger-1.17 csv rules files working,+       and because it can be more succinct, and because  it  converts  posting        2's amount to cost if there's a transaction price, which can be useful.         If you have an existing rules file using the unnumbered form, you might-       want  to  use  the numbered form in certain conditional blocks, without-       having to update and retest all the old  rules.   To  facilitate  this,-       posting    1    ignores    amount/amount-in/amount-out    if   any   of+       want to use the numbered form in certain  conditional  blocks,  without+       having  to  update  and  retest all the old rules.  To facilitate this,+       posting   1   ignores    amount/amount-in/amount-out    if    any    of        amount1/amount1-in/amount1-out are assigned, and posting 2 ignores them-       if  any  of  amount2/amount2-in/amount2-out are assigned, avoiding con-+       if any of amount2/amount2-in/amount2-out are  assigned,  avoiding  con-        flicts.     currency        If the CSV has the currency symbol in a separate field (ie, not part of-       the  amount  field), you can use currencyN to prepend it to posting N's+       the amount field), you can use currencyN to prepend it to  posting  N's        amount.  Or, currency with no number affects all postings.     balance-       balanceN sets a balance assertion amount (or if the posting  amount  is+       balanceN  sets  a balance assertion amount (or if the posting amount is        left empty, a balance assignment) on posting N. -       Also,  for  compatibility with hledger <1.17: balance with no number is+       Also, for compatibility with hledger <1.17: balance with no  number  is        equivalent to balance1. -       You can adjust the type of assertion/assignment with  the  balance-type+       You  can  adjust the type of assertion/assignment with the balance-type        rule (see below).     comment@@ -434,11 +436,11 @@    field assignment               HLEDGERFIELDNAME FIELDVALUE -       Instead of or in addition to a fields list, you can use  a  "field  as--       signment"  rule  to set the value of a single hledger field, by writing-       its name (any of the standard hledger field names above) followed by  a-       text  value.  The value may contain interpolated CSV fields, referenced-       by their 1-based position in the CSV record (%N), or by the  name  they+       Instead  of  or  in addition to a fields list, you can use a "field as-+       signment" rule to set the value of a single hledger field,  by  writing+       its  name (any of the standard hledger field names above) followed by a+       text value.  The value may contain interpolated CSV fields,  referenced+       by  their  1-based position in the CSV record (%N), or by the name they        were given in the fields list (%CSVFIELDNAME).  Some examples:                # set the amount to the 4th CSV field, with " USD" appended@@ -447,14 +449,14 @@               # combine three fields to make a comment, containing note: and date: tags               comment note: %somefield - %anotherfield, date: %1 -       Interpolation  strips  outer  whitespace (so a CSV value like " 1 " be-+       Interpolation strips outer whitespace (so a CSV value like "  1  "  be-        comes 1 when interpolated) (#1051).  See TIPS below for more about ref-        erencing other fields.     separator-       You  can  use the separator rule to read other kinds of character-sepa--       rated data.  The argument is any single  separator  character,  or  the-       words  tab or space (case insensitive).  Eg, for comma-separated values+       You can use the separator rule to read other kinds  of  character-sepa-+       rated  data.   The  argument  is any single separator character, or the+       words tab or space (case insensitive).  Eg, for comma-separated  values        (CSV):                separator ,@@ -467,7 +469,7 @@                separator TAB -       If the input file has a .csv, .ssv or .tsv file extension (or  a  csv:,+       If  the  input file has a .csv, .ssv or .tsv file extension (or a csv:,        ssv:, tsv: prefix), the appropriate separator will be inferred automat-        ically, and you won't need this rule. @@ -482,8 +484,8 @@                RULE                RULE -       Conditional blocks ("if blocks") are a block of rules that are  applied-       only  to CSV records which match certain patterns.  They are often used+       Conditional  blocks ("if blocks") are a block of rules that are applied+       only to CSV records which match certain patterns.  They are often  used        for customising account names based on transaction descriptions.     Matching the whole record@@ -491,16 +493,16 @@                REGEX -       REGEX is a case-insensitive regular expression  which  tries  to  match-       anywhere  within  the  CSV record.  It is a POSIX ERE (extended regular-       expression) that also supports GNU word boundaries (\b,  \B,  \<,  \>),+       REGEX  is  a  case-insensitive  regular expression which tries to match+       anywhere within the CSV record.  It is a POSIX  ERE  (extended  regular+       expression)  that  also  supports GNU word boundaries (\b, \B, \<, \>),        and  nothing  else.   If  you  have  trouble,  be  sure  to  check  our        https://hledger.org/hledger.html#regular-expressions doc. -       Important note: the record that is matched is not the original  record,-       but  a synthetic one, with any enclosing double quotes (but not enclos-+       Important  note: the record that is matched is not the original record,+       but a synthetic one, with any enclosing double quotes (but not  enclos-        ing whitespace) removed, and always comma-separated (which means that a-       field  containing  a  comma  will  appear like two fields).  Eg, if the+       field containing a comma will appear like  two  fields).   Eg,  if  the        original record is 2020-01-01; "Acme, Inc.";  1,000, the REGEX will ac-        tually see 2020-01-01,Acme, Inc.,  1,000). @@ -509,14 +511,14 @@                %CSVFIELD REGEX -       which  matches just the content of a particular CSV field.  CSVFIELD is-       a percent sign followed by the field's  name  or  column  number,  like+       which matches just the content of a particular CSV field.  CSVFIELD  is+       a  percent  sign  followed  by  the field's name or column number, like        %date or %1.     Combining matchers        A single matcher can be written on the same line as the "if"; or multi-        ple matchers can be written on the following lines, non-indented.  Mul--       tiple  matchers are OR'd (any one of them can match), unless one begins+       tiple matchers are OR'd (any one of them can match), unless one  begins        with an & symbol, in which case it is AND'ed with the previous matcher.                if@@ -525,8 +527,8 @@                RULE     Rules applied on successful match-       After the patterns there should be one or more rules to apply, all  in--       dented  by at least one space.  Three kinds of rule are allowed in con-+       After  the patterns there should be one or more rules to apply, all in-+       dented by at least one space.  Three kinds of rule are allowed in  con-        ditional blocks:         o field assignments (to set a hledger field)@@ -556,11 +558,11 @@               MATCHER3,VALUE31,VALUE32,...,VALUE3n               <empty line> -       Conditional tables ("if tables") are  a  different  syntax  to  specify-       field  assignments that will be applied only to CSV records which match+       Conditional  tables  ("if  tables")  are  a different syntax to specify+       field assignments that will be applied only to CSV records which  match        certain patterns. -       MATCHER could be either field or record matcher,  as  described  above.+       MATCHER  could  be  either field or record matcher, as described above.        When MATCHER matches, values from that row would be assigned to the CSV        fields named on the if line, in the same order. @@ -584,17 +586,17 @@                 ...                 CSVFIELDNAMEn VALUE3n -       Each line starting with MATCHER should contain enough (possibly  empty)+       Each  line starting with MATCHER should contain enough (possibly empty)        values for all the listed fields. -       Rules  would be checked and applied in the order they are listed in the+       Rules would be checked and applied in the order they are listed in  the        table and, like with if blocks, later rules (in the same or another ta-        ble) or if blocks could override the effect of any rule. -       Instead  of ',' you can use a variety of other non-alphanumeric charac-+       Instead of ',' you can use a variety of other non-alphanumeric  charac-        ters as a separator.  First character after if is taken to be the sepa--       rator  for the rest of the table.  It is the responsibility of the user-       to ensure that separator does not occur inside MATCHERs  and  values  -+       rator for the rest of the table.  It is the responsibility of the  user+       to  ensure  that  separator does not occur inside MATCHERs and values -        there is no way to escape separator.         Example:@@ -605,7 +607,7 @@               2020/01/12.*Plumbing LLC,expenses:house:upkeep,emergency plumbing call-out     end-       This  rule  can  be  used inside if blocks (only), to make hledger stop+       This rule can be used inside if blocks (only),  to  make  hledger  stop        reading this CSV file and move on to the next input file, or to command        execution.  Eg: @@ -616,10 +618,10 @@    date-format               date-format DATEFMT -       This  is  a  helper for the date (and date2) fields.  If your CSV dates-       are not formatted like YYYY-MM-DD,  YYYY/MM/DD  or  YYYY.MM.DD,  you'll-       need  to  add  a  date-format rule describing them with a strptime date-       parsing pattern, which must parse the CSV date value completely.   Some+       This is a helper for the date (and date2) fields.  If  your  CSV  dates+       are  not  formatted  like  YYYY-MM-DD, YYYY/MM/DD or YYYY.MM.DD, you'll+       need to add a date-format rule describing them  with  a  strptime  date+       parsing  pattern, which must parse the CSV date value completely.  Some        examples:                # MM/DD/YY@@ -640,16 +642,29 @@        https://hackage.haskell.org/package/time/docs/Data-Time-For-        mat.html#v:formatTime +   decimal-mark+              decimal-mark .++       or:++              decimal-mark ,++       hledger  automatically accepts either period or comma as a decimal mark+       when parsing numbers (cf Amounts).  However if any numbers in  the  CSV+       contain  digit  group  marks,  such  as thousand-separating commas, you+       should declare the decimal mark explicitly with  this  rule,  to  avoid+       misparsed numbers.+    newest-first-       hledger always sorts the generated transactions by date.   Transactions-       on  the same date should appear in the same order as their CSV records,-       as hledger can usually auto-detect whether the CSV's  normal  order  is+       hledger  always sorts the generated transactions by date.  Transactions+       on the same date should appear in the same order as their CSV  records,+       as  hledger  can  usually auto-detect whether the CSV's normal order is        oldest first or newest first.  But if all of the following are true: -       o the  CSV  might  sometimes  contain just one day of data (all records+       o the CSV might sometimes contain just one day  of  data  (all  records          having the same date) -       o the CSV records are normally in reverse chronological  order  (newest+       o the  CSV  records are normally in reverse chronological order (newest          at the top)         o and you care about preserving the order of same-day transactions@@ -662,9 +677,9 @@    include               include RULESFILE -       This  includes  the  contents  of another CSV rules file at this point.-       RULESFILE is an absolute file path or a path relative  to  the  current-       file's  directory.  This can be useful for sharing common rules between+       This includes the contents of another CSV rules  file  at  this  point.+       RULESFILE  is  an  absolute file path or a path relative to the current+       file's directory.  This can be useful for sharing common rules  between        several rules files, eg:                # someaccount.csv.rules@@ -679,10 +694,10 @@     balance-type        Balance assertions generated by assigning to balanceN are of the simple-       =  type  by  default, which is a single-commodity, subaccount-excluding+       = type by default, which is  a  single-commodity,  subaccount-excluding        assertion.  You may find the subaccount-including variants more useful,-       eg  if  you  have  created some virtual subaccounts of checking to help-       with budgeting.  You can select a different type of assertion with  the+       eg if you have created some virtual subaccounts  of  checking  to  help+       with  budgeting.  You can select a different type of assertion with the        balance-type rule:                # balance assertions will consider all commodities and all subaccounts@@ -697,19 +712,19 @@  TIPS    Rapid feedback-       It's  a  good idea to get rapid feedback while creating/troubleshooting+       It's a good idea to get rapid feedback  while  creating/troubleshooting        CSV rules.  Here's a good way, using entr from http://eradman.com/entr-        project :                $ ls foo.csv* | entr bash -c 'echo ----; hledger -f foo.csv print desc:SOMEDESC' -       A  desc:  query (eg) is used to select just one, or a few, transactions-       of interest.  "bash -c" is used to run multiple  commands,  so  we  can-       echo  a  separator  each  time the command re-runs, making it easier to+       A desc: query (eg) is used to select just one, or a  few,  transactions+       of  interest.   "bash  -c"  is used to run multiple commands, so we can+       echo a separator each time the command re-runs,  making  it  easier  to        read the output.     Valid CSV-       hledger accepts CSV conforming to RFC 4180.  When CSV  values  are  en-+       hledger  accepts  CSV  conforming to RFC 4180.  When CSV values are en-        closed in quotes, note:         o they must be double quotes (not single quotes)@@ -717,9 +732,9 @@        o spaces outside the quotes are not allowed     File Extension-       To  help hledger identify the format and show the right error messages,-       CSV/SSV/TSV files should normally be named with a .csv,  .ssv  or  .tsv-       filename  extension.   Or,  the file path should be prefixed with csv:,+       To help hledger identify the format and show the right error  messages,+       CSV/SSV/TSV  files  should  normally be named with a .csv, .ssv or .tsv+       filename extension.  Or, the file path should be  prefixed  with  csv:,        ssv: or tsv:.  Eg:                $ hledger -f foo.ssv print@@ -728,48 +743,48 @@                $ cat foo | hledger -f ssv:- foo -       You can override the file extension with a separator  rule  if  needed.+       You  can  override  the file extension with a separator rule if needed.        See also: Input files in the hledger manual.     Reading multiple CSV files-       If  you  use  multiple  -f  options to read multiple CSV files at once,-       hledger will look for a correspondingly-named rules file for  each  CSV-       file.   But if you use the --rules-file option, that rules file will be+       If you use multiple -f options to read  multiple  CSV  files  at  once,+       hledger  will  look for a correspondingly-named rules file for each CSV+       file.  But if you use the --rules-file option, that rules file will  be        used for all the CSV files.     Valid transactions        After reading a CSV file, hledger post-processes and validates the gen-        erated journal entries as it would for a journal file - balancing them,-       applying balance assignments, and canonicalising  amount  styles.   Any-       errors  at this stage will be reported in the usual way, displaying the+       applying  balance  assignments,  and canonicalising amount styles.  Any+       errors at this stage will be reported in the usual way, displaying  the        problem entry.         There is one exception: balance assertions, if you have generated them,-       will  not  be checked, since normally these will work only when the CSV-       data is part of the main journal.  If you do need to check balance  as-+       will not be checked, since normally these will work only when  the  CSV+       data  is part of the main journal.  If you do need to check balance as-        sertions generated from CSV right away, pipe into another hledger:                $ hledger -f file.csv print | hledger -f- print     Deduplicating, importing-       When  you  download a CSV file periodically, eg to get your latest bank-       transactions, the new file may overlap with  the  old  one,  containing+       When you download a CSV file periodically, eg to get your  latest  bank+       transactions,  the  new  file  may overlap with the old one, containing        some of the same records.         The import command will (a) detect the new transactions, and (b) append        just those transactions to your main journal.  It is idempotent, so you-       don't  have to remember how many times you ran it or with which version-       of the CSV.  (It keeps state in a hidden .latest.FILE.csv  file.)  This+       don't have to remember how many times you ran it or with which  version+       of  the  CSV.  (It keeps state in a hidden .latest.FILE.csv file.) This        is the easiest way to import CSV data.  Eg:                # download the latest CSV files, then run this command.               # Note, no -f flags needed here.               $ hledger import *.csv [--dry] -       This  method  works  for  most CSV files.  (Where records have a stable+       This method works for most CSV files.  (Where  records  have  a  stable        chronological order, and new records appear only at the new end.) -       A number of other tools and workflows, hledger-specific and  otherwise,+       A  number of other tools and workflows, hledger-specific and otherwise,        exist for converting, deduplicating, classifying and managing CSV data.        See: @@ -780,49 +795,71 @@    Setting amounts        A posting amount can be set in one of these ways: -       o by assigning (with a fields list  or  field  assignment)  to  amountN+       o by  assigning  (with  a  fields  list or field assignment) to amountN          (posting N's amount) or amount (posting 1's amount) -       o by  assigning to amountN-in and amountN-out (or amount-in and amount--         out).  For each CSV record, whichever of these has a  non-zero  value-         will  be  used,  with  appropriate  sign.  If both contain a non-zero+       o by assigning to amountN-in and amountN-out (or amount-in and  amount-+         out).   For  each CSV record, whichever of these has a non-zero value+         will be used, with appropriate sign.   If  both  contain  a  non-zero          value, this may not work. -       o by assigning to balanceN (or balance) instead of the  above,  setting-         the  amount  indirectly via a balance assignment.  If you do this the+       o by  assigning  to balanceN (or balance) instead of the above, setting+         the amount indirectly via a balance assignment.  If you do  this  the          default account name may be wrong, so you should set that explicitly.         There is some special handling for an amount's sign: -       o If an amount value is parenthesised, it will be de-parenthesised  and+       o If  an amount value is parenthesised, it will be de-parenthesised and          sign-flipped. -       o If  an amount value begins with a double minus sign, those cancel out+       o If an amount value begins with a double minus sign, those cancel  out          and are removed.         o If an amount value begins with a plus sign, that will be removed     Setting currency/commodity-       If the currency/commodity  symbol  is  included  in  the  CSV's  amount-       field(s), you don't have to do anything special.+       If  the  currency/commodity  symbol  is  included  in  the CSV's amount+       field(s): -       If the currency is provided as a separate CSV field, you can either:+              2020-01-01,foo,$123.00 -       o assign  that  to currency, which adds it to all posting amounts.  The-         symbol will prepended to the amount quantity (on the left side).   If-         you  write  a  trailing space after the symbol, there will be a space-         between symbol and amount  (an  exception  to  the  usual  whitespace-         stripping).+       you don't have to do anything special for the commodity symbol, it will+       be assigned as part of the amount.  Eg: -       o or assign it to currencyN which adds it to posting N's amount only.+              fields date,description,amount -       o or  for  more  control, construct the amount from symbol and quantity-         using field assignment, eg:+              2020-01-01 foo+                  expenses:unknown         $123.00+                  income:unknown          $-123.00 -                fields date,description,currency,quantity-                # add currency symbol on the right:-                amount %quantity %currency+       If the currency is provided as a separate CSV field: +              2020-01-01,foo,USD,123.00++       You can assign that to the currency pseudo-field, which has the special+       effect of prepending itself to every amount in the transaction (on  the+       left, with no separating space):++              fields date,description,currency,amount++              2020-01-01 foo+                  expenses:unknown       USD123.00+                  income:unknown        USD-123.00++       Or,  you  can  use a field assignment to construct the amount yourself,+       with more control.  Eg to put the symbol on the right, and separated by+       a space:++              fields date,description,cur,amt+              amount %amt %cur++              2020-01-01 foo+                  expenses:unknown        123.00 USD+                  income:unknown         -123.00 USD++       Note  we  used a temporary field name (cur) that is not currency - that+       would trigger the prepending effect, which we don't want here.+    Referencing other fields        In field assignments, you can interpolate only CSV fields, not  hledger        fields.   In  the example below, there's both a CSV field and a hledger@@ -922,4 +959,4 @@   -hledger 1.18.99                 September 2020                  hledger_csv(5)+hledger 1.20                     November 2020                  hledger_csv(5)
hledger_journal.5 view
@@ -1,6 +1,6 @@ .\"t -.TH "hledger_journal" "5" "September 2020" "hledger 1.18.99" "hledger User Manuals"+.TH "hledger_journal" "5" "November 2020" "hledger 1.20" "hledger User Manuals"   @@ -584,40 +584,59 @@ commodity       1 000 000.9455 \f[R] .fi-.SS Amount display style .PP-For each commodity, hledger chooses a consistent format to use when+.SS Commodity display style+.PP+For each commodity, hledger chooses a consistent style to use when displaying amounts. (Except price amounts, which are always displayed as written). The display style is chosen as follows: .IP \[bu] 2 If there is a commodity directive (or default commodity directive) for-the commodity, that format is used (see examples above).+the commodity, its style is used (see examples above). .IP \[bu] 2-Otherwise the format of the first posting amount in that commodity seen-in the journal is used.-But the number of decimal places (\[dq]precision\[dq]) will be the-maximum from all posting amounts in that commodity.+Otherwise the style is inferred from the amounts in that commodity seen+in the journal. .IP \[bu] 2-Or if there are no such amounts in the journal, a default format is used+Or if there are no such amounts in the journal, a default style is used (like \f[C]$1000.00\f[R]). .PP-Transaction prices don\[aq]t affect the amount display style directly,-but occasionally they can do so indirectly (eg when an posting\[aq]s-amount is inferred using a transaction price).+A style is inferred from the journal amounts in a commodity as follows:+.IP \[bu] 2+Use the general style (decimal mark, symbol placement) of the first+amount+.IP \[bu] 2+Use the first-seen digit group style (digit group mark, digit group+sizes), if any+.IP \[bu] 2+Use the maximum number of decimal places of all.+.PP+Transaction price amounts don\[aq]t affect the commodity display style+directly, but occasionally they can do so indirectly (eg when a+posting\[aq]s amount is inferred using a transaction price). If you find this causing problems, use a commodity directive to fix the display style. .PP-In summary: amounts will be displayed much as they appear in your-journal, with the max observed number of decimal places.-If you want to see fewer decimal places in reports, use a commodity-directive to override that.+In summary, each commodity\[aq]s amounts will be normalised to+.IP \[bu] 2+the style declared by a \f[C]commodity\f[R] directive+.IP \[bu] 2+or, the style of the first posting amount in the journal, with the+first-seen digit group style and the maximum-seen number of decimal+places. .PP-hledger uses banker\[aq]s rounding: it rounds to the nearest even+If reports are showing amounts in a way you don\[aq]t like (eg, with too+many decimal places), use a commodity directive to set your preferred+style.+.SS Rounding+.PP+Amounts are stored internally as decimal numbers with up to 255 decimal+places, and displayed with the number of decimal places specified by the+commodity display style.+Note, hledger uses banker\[aq]s rounding: it rounds to the nearest even number, eg 0.5 displayed with zero decimal places is \[dq]0\[dq]).-(Note, prior to hledger 1.17.1 this could vary if hledger happened to be-built with an old version of Decimal (<0.5.1); since 1.17.1 it\[aq]s-guaranteed.)+(Guaranteed since hledger 1.17.1; in older versions this could vary if+hledger was built with Decimal < 0.5.1.) .SS Transaction prices .PP Within a transaction, you can note an amount\[aq]s price in another@@ -950,6 +969,8 @@ Directives\[aq] behaviour and interactions can get a little bit complex, so here is a table summarising the directives and their effects, with links to more detailed docs.+Note part of this table is hidden when viewed in a web browser - scroll+it sideways to see more. .PP .TS tab(@);@@ -984,8 +1005,7 @@ T}@T{ rewrite account names T}@T{-following inline/included entries until end of current file or end-directive+following entries until end of current file or end directive T} T{ \f[C]apply account\f[R]@@ -995,8 +1015,7 @@ T}@T{ prepend a common parent to account names T}@T{-following inline/included entries until end of current file or end-directive+following entries until end of current file or end directive T} T{ \f[C]comment\f[R]@@ -1006,8 +1025,7 @@ T}@T{ ignore part of journal T}@T{-following inline/included entries until end of current file or end-directive+following entries until end of current file or end directive T} T{ \f[C]commodity\f[R]@@ -1017,7 +1035,7 @@ T}@T{ declare a commodity and its number notation & display style T}@T{-number notation: following entries in that commodity in all files;+number notation: following entries in that commodity in all files ; display style: amounts of that commodity in reports T} T{@@ -1057,7 +1075,7 @@ T}@T{ declare a year for yearless dates T}@T{-following inline/included entries until end of current file+following entries until end of current file T} T{ \f[C]=\f[R]@@ -1199,7 +1217,7 @@ (Without this, hledger will parse both \f[C]1,000\f[R] and \f[C]1.000\f[R] as 1). .IP "3." 3-It declares the amount display style to use in output - decimal and+It declares a commodity\[aq]s display style in output - decimal and digit group marks, number of decimal places, symbol placement etc. .PP You are likely to run into one of the problems solved by commodity@@ -1245,7 +1263,14 @@ .PP Note hledger normally uses banker\[aq]s rounding, so 0.5 displayed with zero decimal digits is \[dq]0\[dq].-(More at Amount display style.)+(More at Commodity display style.)+.SS Commodity error checking+.PP+In strict mode, enabled with the \f[C]-s\f[R]/\f[C]--strict\f[R] flag,+hledger will report an error if a commodity symbol is used that has not+been declared by a \f[C]commodity\f[R] directive.+This works similarly to account error checking, see the notes there for+more details. .SS Default commodity .PP The \f[C]D\f[R] directive sets a default commodity, to be used for@@ -1314,15 +1339,13 @@ See Valuation. .SS Declaring accounts .PP-\f[C]account\f[R] directives can be used to pre-declare accounts.-Though not required, they can provide several benefits:+\f[C]account\f[R] directives can be used to declare accounts (ie, the+places that amounts are transferred from and to).+Though not required, these declarations can provide several benefits: .IP \[bu] 2 They can document your intended chart of accounts, providing a reference. .IP \[bu] 2-They can store extra information about accounts (account numbers, notes,-etc.)-.IP \[bu] 2 They can help hledger know your accounts\[aq] types (asset, liability, equity, revenue, expense), useful for reports like balancesheet and incomestatement.@@ -1330,17 +1353,54 @@ They control account display order in reports, allowing non-alphabetic sorting (eg Revenues to appear above Expenses). .IP \[bu] 2+They can store extra information about accounts (account numbers, notes,+etc.)+.IP \[bu] 2 They help with account name completion in the add command, hledger-iadd, hledger-web, ledger-mode etc.+.IP \[bu] 2+In strict mode, they restrict which accounts may be posted to by+transactions, which helps detect typos. .PP The simplest form is just the word \f[C]account\f[R] followed by a-hledger-style account name, eg:+hledger-style account name, eg this account directive declares the+\f[C]assets:bank:checking\f[R] account: .IP .nf \f[C] account assets:bank:checking \f[R] .fi+.SS Account error checking+.PP+By default, accounts come into existence when a transaction references+them by name.+This is convenient, but it means hledger can\[aq]t warn you when you+mis-spell an account name in the journal.+Usually you\[aq]ll find the error later, as an extra account in balance+reports, or an incorrect balance when reconciling.+.PP+In strict mode, enabled with the \f[C]-s\f[R]/\f[C]--strict\f[R] flag,+hledger will report an error if any transaction uses an account name+that has not been declared by an account directive.+Some notes:+.IP \[bu] 2+The declaration is case-sensitive; transactions must use the correct+account name capitalisation.+.IP \[bu] 2+The account directive\[aq]s scope is \[dq]whole file and below\[dq] (see+directives).+This means it affects all of the current file, and any files it+includes, but not parent or sibling files.+The position of account directives within the file does not matter,+though it\[aq]s usual to put them at the top.+.IP \[bu] 2+Accounts can only be declared in \f[C]journal\f[R] files (but will+affect included files in other formats).+.IP \[bu] 2+It\[aq]s currently not possible to declare \[dq]all possible+subaccounts\[dq] with a wildcard; every account posted to must be+declared. .SS Account comments .PP Comments, beginning with a semicolon, can be added:@@ -1941,8 +2001,7 @@ (and also, a goal of depositing $2000 into checking) every month. Goals and actual performance can then be compared in budget reports. .PP-For more details, see: balance: Budget report and Budgeting and-Forecasting.+See also: Budgeting and Forecasting. .PP .SS Auto postings .PP
hledger_journal.info view
@@ -4,8 +4,8 @@  File: hledger_journal.info,  Node: Top,  Up: (dir) -hledger_journal(5) hledger 1.18.99-**********************************+hledger_journal(5) hledger 1.20+*******************************  Journal - hledger's default file format, representing a General Journal @@ -487,10 +487,11 @@ * Menu:  * Digit group marks::-* Amount display style::+* Commodity display style::+* Rounding::  -File: hledger_journal.info,  Node: Digit group marks,  Next: Amount display style,  Up: Amounts+File: hledger_journal.info,  Node: Digit group marks,  Next: Commodity display style,  Up: Amounts  1.8.1 Digit group marks -----------------------@@ -523,42 +524,64 @@ commodity       1 000 000.9455  -File: hledger_journal.info,  Node: Amount display style,  Prev: Digit group marks,  Up: Amounts+File: hledger_journal.info,  Node: Commodity display style,  Next: Rounding,  Prev: Digit group marks,  Up: Amounts -1.8.2 Amount display style---------------------------+1.8.2 Commodity display style+----------------------------- -For each commodity, hledger chooses a consistent format to use when+For each commodity, hledger chooses a consistent style to use when displaying amounts.  (Except price amounts, which are always displayed as written).  The display style is chosen as follows:     * If there is a commodity directive (or default commodity directive)-     for the commodity, that format is used (see examples above).+     for the commodity, its style is used (see examples above). -   * Otherwise the format of the first posting amount in that commodity-     seen in the journal is used.  But the number of decimal places-     ("precision") will be the maximum from all posting amounts in that-     commodity.+   * Otherwise the style is inferred from the amounts in that commodity+     seen in the journal. -   * Or if there are no such amounts in the journal, a default format is+   * Or if there are no such amounts in the journal, a default style is      used (like '$1000.00'). -   Transaction prices don't affect the amount display style directly,-but occasionally they can do so indirectly (eg when an posting's amount-is inferred using a transaction price).  If you find this causing-problems, use a commodity directive to fix the display style.+   A style is inferred from the journal amounts in a commodity as+follows: -   In summary: amounts will be displayed much as they appear in your-journal, with the max observed number of decimal places.  If you want to-see fewer decimal places in reports, use a commodity directive to-override that.+   * Use the general style (decimal mark, symbol placement) of the first+     amount+   * Use the first-seen digit group style (digit group mark, digit group+     sizes), if any+   * Use the maximum number of decimal places of all. -   hledger uses banker's rounding: it rounds to the nearest even number,-eg 0.5 displayed with zero decimal places is "0").  (Note, prior to-hledger 1.17.1 this could vary if hledger happened to be built with an-old version of Decimal (<0.5.1); since 1.17.1 it's guaranteed.)+   Transaction price amounts don't affect the commodity display style+directly, but occasionally they can do so indirectly (eg when a+posting's amount is inferred using a transaction price).  If you find+this causing problems, use a commodity directive to fix the display+style. +   In summary, each commodity's amounts will be normalised to++   * the style declared by a 'commodity' directive+   * or, the style of the first posting amount in the journal, with the+     first-seen digit group style and the maximum-seen number of decimal+     places.++   If reports are showing amounts in a way you don't like (eg, with too+many decimal places), use a commodity directive to set your preferred+style.+ +File: hledger_journal.info,  Node: Rounding,  Prev: Commodity display style,  Up: Amounts++1.8.3 Rounding+--------------++Amounts are stored internally as decimal numbers with up to 255 decimal+places, and displayed with the number of decimal places specified by the+commodity display style.  Note, hledger uses banker's rounding: it+rounds to the nearest even number, eg 0.5 displayed with zero decimal+places is "0").  (Guaranteed since hledger 1.17.1; in older versions+this could vary if hledger was built with Decimal < 0.5.1.)++ File: hledger_journal.info,  Node: Transaction prices,  Next: Lot prices and lot dates,  Prev: Amounts,  Up: Transactions  1.9 Transaction prices@@ -888,7 +911,8 @@     Directives' behaviour and interactions can get a little bit complex, so here is a table summarising the directives and their effects, with-links to more detailed docs.+links to more detailed docs.  Note part of this table is hidden when+viewed in a web browser - scroll it sideways to see more.  directiveend       subdirectivespurpose                  can affect (as of          directive                                       2018/06)@@ -896,25 +920,22 @@ 'account'          any     document account names,       all entries in                    text    declare account types &       all files, before                            display order                 or after-'alias'  'end              rewrite account names         following-         aliases'                                        inline/included-                                                         entries until end-                                                         of current file-                                                         or end directive-'apply   'end              prepend a common parent to    following-account' apply             account names                 inline/included-         account'                                        entries until end-                                                         of current file-                                                         or end directive-'comment''end              ignore part of journal        following-         comment'                                        inline/included-                                                         entries until end-                                                         of current file-                                                         or end directive+'alias'  'end              rewrite account names         following entries+         aliases'                                        until end of+                                                         current file or+                                                         end directive+'apply   'end              prepend a common parent to    following entries+account' apply             account names                 until end of+         account'                                        current file or+                                                         end directive+'comment''end              ignore part of journal        following entries+         comment'                                        until end of+                                                         current file or+                                                         end directive 'commodity'        'format'declare a commodity and its   number notation:                            number notation & display     following entries                            style                         in that commodity-                                                         in all files;+                                                         in all files ;                                                          display style:                                                          amounts of that                                                          commodity in@@ -940,10 +961,9 @@                            a commodity                   commodity in                                                          reports, when -V                                                          is used-'Y'                        declare a year for yearless   following-                           dates                         inline/included-                                                         entries until end-                                                         of current file+'Y'                        declare a year for yearless   following entries+                           dates                         until end of+                                                         current file '='                        declare an auto posting       all entries in                            rule, adding postings to      parent/current/child                            other transactions            files (but not@@ -1082,7 +1102,7 @@      formats in your data.  (Without this, hledger will parse both      '1,000' and '1.000' as 1). -  3. It declares the amount display style to use in output - decimal and+  3. It declares a commodity's display style in output - decimal and      digit group marks, number of decimal places, symbol placement etc.     You are likely to run into one of the problems solved by commodity@@ -1117,9 +1137,24 @@ a comma, followed by 0 or more decimal digits.     Note hledger normally uses banker's rounding, so 0.5 displayed with-zero decimal digits is "0".  (More at Amount display style.)+zero decimal digits is "0".  (More at Commodity display style.) +* Menu:++* Commodity error checking::+ +File: hledger_journal.info,  Node: Commodity error checking,  Up: Declaring commodities++1.13.5.1 Commodity error checking+.................................++In strict mode, enabled with the '-s'/'--strict' flag, hledger will+report an error if a commodity symbol is used that has not been declared+by a 'commodity' directive.  This works similarly to account error+checking, see the notes there for more details.++ File: hledger_journal.info,  Node: Default commodity,  Next: Declaring market prices,  Prev: Declaring commodities,  Up: Directives  1.13.6 Default commodity@@ -1182,37 +1217,70 @@ 1.13.8 Declaring accounts ------------------------- -'account' directives can be used to pre-declare accounts.  Though not-required, they can provide several benefits:+'account' directives can be used to declare accounts (ie, the places+that amounts are transferred from and to).  Though not required, these+declarations can provide several benefits:     * They can document your intended chart of accounts, providing a      reference.-   * They can store extra information about accounts (account numbers,-     notes, etc.)    * They can help hledger know your accounts' types (asset, liability,      equity, revenue, expense), useful for reports like balancesheet and      incomestatement.    * They control account display order in reports, allowing      non-alphabetic sorting (eg Revenues to appear above Expenses).+   * They can store extra information about accounts (account numbers,+     notes, etc.)    * They help with account name completion in the add command,      hledger-iadd, hledger-web, ledger-mode etc.+   * In strict mode, they restrict which accounts may be posted to by+     transactions, which helps detect typos.     The simplest form is just the word 'account' followed by a-hledger-style account name, eg:+hledger-style account name, eg this account directive declares the+'assets:bank:checking' account:  account assets:bank:checking  * Menu: +* Account error checking:: * Account comments:: * Account subdirectives:: * Account types:: * Account display order::  -File: hledger_journal.info,  Node: Account comments,  Next: Account subdirectives,  Up: Declaring accounts+File: hledger_journal.info,  Node: Account error checking,  Next: Account comments,  Up: Declaring accounts -1.13.8.1 Account comments+1.13.8.1 Account error checking+...............................++By default, accounts come into existence when a transaction references+them by name.  This is convenient, but it means hledger can't warn you+when you mis-spell an account name in the journal.  Usually you'll find+the error later, as an extra account in balance reports, or an incorrect+balance when reconciling.++   In strict mode, enabled with the '-s'/'--strict' flag, hledger will+report an error if any transaction uses an account name that has not+been declared by an account directive.  Some notes:++   * The declaration is case-sensitive; transactions must use the+     correct account name capitalisation.+   * The account directive's scope is "whole file and below" (see+     directives).  This means it affects all of the current file, and+     any files it includes, but not parent or sibling files.  The+     position of account directives within the file does not matter,+     though it's usual to put them at the top.+   * Accounts can only be declared in 'journal' files (but will affect+     included files in other formats).+   * It's currently not possible to declare "all possible subaccounts"+     with a wildcard; every account posted to must be declared.+++File: hledger_journal.info,  Node: Account comments,  Next: Account subdirectives,  Prev: Account error checking,  Up: Declaring accounts++1.13.8.2 Account comments .........................  Comments, beginning with a semicolon, can be added:@@ -1232,7 +1300,7 @@  File: hledger_journal.info,  Node: Account subdirectives,  Next: Account types,  Prev: Account comments,  Up: Declaring accounts -1.13.8.2 Account subdirectives+1.13.8.3 Account subdirectives ..............................  We also allow (and ignore) Ledger-style indented subdirectives, just for@@ -1250,7 +1318,7 @@  File: hledger_journal.info,  Node: Account types,  Next: Account display order,  Prev: Account subdirectives,  Up: Declaring accounts -1.13.8.3 Account types+1.13.8.4 Account types ......................  hledger recognises five main types of account, corresponding to the@@ -1329,7 +1397,7 @@  File: hledger_journal.info,  Node: Account display order,  Prev: Account types,  Up: Declaring accounts -1.13.8.4 Account display order+1.13.8.5 Account display order ..............................  Account directives also set the order in which accounts are displayed,@@ -1723,8 +1791,7 @@ checking) every month.  Goals and actual performance can then be compared in budget reports. -   For more details, see: balance: Budget report and Budgeting and-Forecasting.+   See also: Budgeting and Forecasting.   File: hledger_journal.info,  Node: Auto postings,  Prev: Periodic transactions,  Up: Transactions@@ -1865,124 +1932,130 @@  Tag Table: Node: Top76-Node: Transactions1875-Ref: #transactions1967-Node: Dates3251-Ref: #dates3350-Node: Simple dates3415-Ref: #simple-dates3541-Node: Secondary dates4050-Ref: #secondary-dates4204-Node: Posting dates5540-Ref: #posting-dates5669-Node: Status7041-Ref: #status7162-Node: Description8870-Ref: #description9004-Node: Payee and note9324-Ref: #payee-and-note9438-Node: Comments9773-Ref: #comments9899-Node: Tags11093-Ref: #tags11208-Node: Postings12601-Ref: #postings12729-Node: Virtual postings13755-Ref: #virtual-postings13872-Node: Account names15177-Ref: #account-names15318-Node: Amounts15805-Ref: #amounts15944-Node: Digit group marks17052-Ref: #digit-group-marks17200-Node: Amount display style18138-Ref: #amount-display-style18292-Node: Transaction prices19729-Ref: #transaction-prices19901-Node: Lot prices and lot dates22332-Ref: #lot-prices-and-lot-dates22529-Node: Balance assertions23017-Ref: #balance-assertions23203-Node: Assertions and ordering24236-Ref: #assertions-and-ordering24424-Node: Assertions and included files25124-Ref: #assertions-and-included-files25367-Node: Assertions and multiple -f options25700-Ref: #assertions-and-multiple--f-options25956-Node: Assertions and commodities26088-Ref: #assertions-and-commodities26320-Node: Assertions and prices27477-Ref: #assertions-and-prices27691-Node: Assertions and subaccounts28131-Ref: #assertions-and-subaccounts28360-Node: Assertions and virtual postings28684-Ref: #assertions-and-virtual-postings28926-Node: Assertions and precision29068-Ref: #assertions-and-precision29261-Node: Balance assignments29528-Ref: #balance-assignments29702-Node: Balance assignments and prices30866-Ref: #balance-assignments-and-prices31038-Node: Directives31262-Ref: #directives31421-Node: Directives and multiple files37112-Ref: #directives-and-multiple-files37295-Node: Comment blocks37959-Ref: #comment-blocks38142-Node: Including other files38318-Ref: #including-other-files38498-Node: Default year39422-Ref: #default-year39591-Node: Declaring commodities39998-Ref: #declaring-commodities40181-Node: Default commodity41987-Ref: #default-commodity42173-Node: Declaring market prices43062-Ref: #declaring-market-prices43257-Node: Declaring accounts44114-Ref: #declaring-accounts44300-Node: Account comments45225-Ref: #account-comments45388-Node: Account subdirectives45812-Ref: #account-subdirectives46007-Node: Account types46320-Ref: #account-types46504-Node: Account display order49550-Ref: #account-display-order49720-Node: Rewriting accounts50871-Ref: #rewriting-accounts51056-Node: Basic aliases51813-Ref: #basic-aliases51959-Node: Regex aliases52663-Ref: #regex-aliases52835-Node: Combining aliases53554-Ref: #combining-aliases53747-Node: Aliases and multiple files55023-Ref: #aliases-and-multiple-files55232-Node: end aliases55811-Ref: #end-aliases55968-Node: Default parent account56069-Ref: #default-parent-account56237-Node: Periodic transactions57121-Ref: #periodic-transactions57296-Node: Periodic rule syntax59168-Ref: #periodic-rule-syntax59374-Node: Two spaces between period expression and description!60078-Ref: #two-spaces-between-period-expression-and-description60397-Node: Forecasting with periodic transactions61081-Ref: #forecasting-with-periodic-transactions61386-Node: Budgeting with periodic transactions63441-Ref: #budgeting-with-periodic-transactions63680-Node: Auto postings64129-Ref: #auto-postings64269-Node: Auto postings and multiple files66448-Ref: #auto-postings-and-multiple-files66652-Node: Auto postings and dates66861-Ref: #auto-postings-and-dates67135-Node: Auto postings and transaction balancing / inferred amounts / balance assertions67310-Ref: #auto-postings-and-transaction-balancing-inferred-amounts-balance-assertions67661-Node: Auto posting tags68003-Ref: #auto-posting-tags68218+Node: Transactions1869+Ref: #transactions1961+Node: Dates3245+Ref: #dates3344+Node: Simple dates3409+Ref: #simple-dates3535+Node: Secondary dates4044+Ref: #secondary-dates4198+Node: Posting dates5534+Ref: #posting-dates5663+Node: Status7035+Ref: #status7156+Node: Description8864+Ref: #description8998+Node: Payee and note9318+Ref: #payee-and-note9432+Node: Comments9767+Ref: #comments9893+Node: Tags11087+Ref: #tags11202+Node: Postings12595+Ref: #postings12723+Node: Virtual postings13749+Ref: #virtual-postings13866+Node: Account names15171+Ref: #account-names15312+Node: Amounts15799+Ref: #amounts15938+Node: Digit group marks17062+Ref: #digit-group-marks17213+Node: Commodity display style18151+Ref: #commodity-display-style18331+Node: Rounding19874+Ref: #rounding19998+Node: Transaction prices20410+Ref: #transaction-prices20582+Node: Lot prices and lot dates23013+Ref: #lot-prices-and-lot-dates23210+Node: Balance assertions23698+Ref: #balance-assertions23884+Node: Assertions and ordering24917+Ref: #assertions-and-ordering25105+Node: Assertions and included files25805+Ref: #assertions-and-included-files26048+Node: Assertions and multiple -f options26381+Ref: #assertions-and-multiple--f-options26637+Node: Assertions and commodities26769+Ref: #assertions-and-commodities27001+Node: Assertions and prices28158+Ref: #assertions-and-prices28372+Node: Assertions and subaccounts28812+Ref: #assertions-and-subaccounts29041+Node: Assertions and virtual postings29365+Ref: #assertions-and-virtual-postings29607+Node: Assertions and precision29749+Ref: #assertions-and-precision29942+Node: Balance assignments30209+Ref: #balance-assignments30383+Node: Balance assignments and prices31547+Ref: #balance-assignments-and-prices31719+Node: Directives31943+Ref: #directives32102+Node: Directives and multiple files37600+Ref: #directives-and-multiple-files37783+Node: Comment blocks38447+Ref: #comment-blocks38630+Node: Including other files38806+Ref: #including-other-files38986+Node: Default year39910+Ref: #default-year40079+Node: Declaring commodities40486+Ref: #declaring-commodities40669+Node: Commodity error checking42513+Ref: #commodity-error-checking42673+Node: Default commodity42930+Ref: #default-commodity43116+Node: Declaring market prices44005+Ref: #declaring-market-prices44200+Node: Declaring accounts45057+Ref: #declaring-accounts45243+Node: Account error checking46445+Ref: #account-error-checking46621+Node: Account comments47800+Ref: #account-comments47994+Node: Account subdirectives48418+Ref: #account-subdirectives48613+Node: Account types48926+Ref: #account-types49110+Node: Account display order52156+Ref: #account-display-order52326+Node: Rewriting accounts53477+Ref: #rewriting-accounts53662+Node: Basic aliases54419+Ref: #basic-aliases54565+Node: Regex aliases55269+Ref: #regex-aliases55441+Node: Combining aliases56160+Ref: #combining-aliases56353+Node: Aliases and multiple files57629+Ref: #aliases-and-multiple-files57838+Node: end aliases58417+Ref: #end-aliases58574+Node: Default parent account58675+Ref: #default-parent-account58843+Node: Periodic transactions59727+Ref: #periodic-transactions59902+Node: Periodic rule syntax61774+Ref: #periodic-rule-syntax61980+Node: Two spaces between period expression and description!62684+Ref: #two-spaces-between-period-expression-and-description63003+Node: Forecasting with periodic transactions63687+Ref: #forecasting-with-periodic-transactions63992+Node: Budgeting with periodic transactions66047+Ref: #budgeting-with-periodic-transactions66286+Node: Auto postings66695+Ref: #auto-postings66835+Node: Auto postings and multiple files69014+Ref: #auto-postings-and-multiple-files69218+Node: Auto postings and dates69427+Ref: #auto-postings-and-dates69701+Node: Auto postings and transaction balancing / inferred amounts / balance assertions69876+Ref: #auto-postings-and-transaction-balancing-inferred-amounts-balance-assertions70227+Node: Auto posting tags70569+Ref: #auto-posting-tags70784  End Tag Table 
hledger_journal.txt view
@@ -411,42 +411,60 @@               commodity INR 9,99,99,999.00               commodity       1 000 000.9455 -   Amount display style-       For  each  commodity,  hledger  chooses a consistent format to use when-       displaying amounts.  (Except price amounts, which are always  displayed-       as written).  The display style is chosen as follows:+   Commodity display style+       For each commodity, hledger chooses a consistent style to use when dis-+       playing amounts.  (Except price amounts, which are always displayed  as+       written).  The display style is chosen as follows:         o If  there  is  a commodity directive (or default commodity directive)-         for the commodity, that format is used (see examples above).+         for the commodity, its style is used (see examples above). -       o Otherwise the format of the first posting amount  in  that  commodity-         seen in the journal is used.  But the number of decimal places ("pre--         cision") will be the maximum from all posting amounts in that commod--         ity.+       o Otherwise the style is inferred from the amounts  in  that  commodity+         seen in the journal. -       o Or  if  there are no such amounts in the journal, a default format is+       o Or  if  there  are no such amounts in the journal, a default style is          used (like $1000.00). -       Transaction prices don't affect the amount display style directly,  but-       occasionally  they can do so indirectly (eg when an posting's amount is-       inferred using a transaction price).  If you find  this  causing  prob--       lems, use a commodity directive to fix the display style.+       A style is inferred from the journal amounts in a commodity as follows: -       In summary: amounts will be displayed much as they appear in your jour--       nal, with the max observed number of decimal places.  If  you  want  to-       see fewer decimal places in reports, use a commodity directive to over--       ride that.+       o Use the general style (decimal mark, symbol placement) of  the  first+         amount -       hledger uses banker's rounding: it rounds to the nearest  even  number,-       eg  0.5  displayed  with  zero decimal places is "0").  (Note, prior to-       hledger 1.17.1 this could vary if hledger happened to be built with  an-       old version of Decimal (<0.5.1); since 1.17.1 it's guaranteed.)+       o Use  the  first-seen digit group style (digit group mark, digit group+         sizes), if any +       o Use the maximum number of decimal places of all.++       Transaction price amounts don't affect the commodity display style  di-+       rectly, but occasionally they can do so indirectly (eg when a posting's+       amount is inferred using a transaction price).  If you find this  caus-+       ing problems, use a commodity directive to fix the display style.++       In summary, each commodity's amounts will be normalised to++       o the style declared by a commodity directive++       o or,  the  style  of the first posting amount in the journal, with the+         first-seen digit group style and the maximum-seen number  of  decimal+         places.++       If  reports  are  showing amounts in a way you don't like (eg, with too+       many decimal places), use a commodity directive to set  your  preferred+       style.++   Rounding+       Amounts are stored internally as decimal numbers with up to 255 decimal+       places, and displayed with the number of decimal  places  specified  by+       the  commodity display style.  Note, hledger uses banker's rounding: it+       rounds to the nearest even number, eg 0.5 displayed with  zero  decimal+       places  is  "0").   (Guaranteed since hledger 1.17.1; in older versions+       this could vary if hledger was built with Decimal < 0.5.1.)+    Transaction prices        Within a transaction, you can note an amount's price in another commod--       ity.  This can be used to document the cost (in a purchase) or  selling-       price  (in  a  sale).   For  example,  transaction prices are useful to-       record purchases of a foreign currency.  Note  transaction  prices  are+       ity.   This can be used to document the cost (in a purchase) or selling+       price (in a sale).  For  example,  transaction  prices  are  useful  to+       record  purchases  of  a foreign currency.  Note transaction prices are        fixed at the time of the transaction, and do not change over time.  See        also market prices, which represent prevailing exchange rates on a cer-        tain date.@@ -472,14 +490,14 @@                     assets:euros     EUR100          ; one hundred euros purchased                     assets:dollars  $-135          ; for $135 -       4. Like 1, but the @ is parenthesised, i.e.  (@); this is for  compati--          bility  with Ledger journals (Virtual posting costs), and is equiva-+       4. Like  1, but the @ is parenthesised, i.e.  (@); this is for compati-+          bility with Ledger journals (Virtual posting costs), and is  equiva-           lent to 1 in hledger.         5. Like 2, but as in 4 the @@ is parenthesised, i.e.  (@@); in hledger,           this is equivalent to 2. -       Use  the -B/--cost flag to convert amounts to their transaction price's+       Use the -B/--cost flag to convert amounts to their transaction  price's        commodity, if any.  (mnemonic: "B" is from "cost Basis", as in Ledger).        Eg here is how -B affects the balance report for the example above: @@ -490,8 +508,8 @@                              $-135  assets:dollars                               $135  assets:euros    # <- the euros' cost -       Note  -B is sensitive to the order of postings when a transaction price-       is inferred: the inferred price will be in the commodity  of  the  last+       Note -B is sensitive to the order of postings when a transaction  price+       is  inferred:  the  inferred price will be in the commodity of the last        amount.  So if example 3's postings are reversed, while the transaction        is equivalent, -B shows something different: @@ -504,18 +522,18 @@                               EUR100  assets:euros     Lot prices and lot dates-       Ledger allows another kind of price, lot price (four  variants:  {UNIT-+       Ledger  allows  another kind of price, lot price (four variants: {UNIT-        PRICE},   {{TOTALPRICE}},   {=FIXEDUNITPRICE},   {{=FIXEDTOTALPRICE}}),        and/or a lot date ([DATE]) to be specified.  These are normally used to-       select  a  lot when selling investments.  hledger will parse these, for-       compatibility with Ledger journals,  but  currently  ignores  them.   A-       transaction  price,  lot price and/or lot date may appear in any order,+       select a lot when selling investments.  hledger will parse  these,  for+       compatibility  with  Ledger  journals,  but  currently ignores them.  A+       transaction price, lot price and/or lot date may appear in  any  order,        after the posting amount and before the balance assertion if any.     Balance assertions-       hledger supports Ledger-style  balance  assertions  in  journal  files.-       These  look  like, for example, = EXPECTEDBALANCE following a posting's-       amount.  Eg here we assert the expected dollar balance  in  accounts  a+       hledger  supports  Ledger-style  balance  assertions  in journal files.+       These look like, for example, = EXPECTEDBALANCE following  a  posting's+       amount.   Eg  here  we assert the expected dollar balance in accounts a        and b after each posting:                2013/1/1@@ -527,32 +545,32 @@                 b  $-1  =$-2         After reading a journal file, hledger will check all balance assertions-       and report an error if any of them fail.  Balance assertions  can  pro--       tect  you  from, eg, inadvertently disrupting reconciled balances while-       cleaning up old entries.  You can disable  them  temporarily  with  the+       and  report  an error if any of them fail.  Balance assertions can pro-+       tect you from, eg, inadvertently disrupting reconciled  balances  while+       cleaning  up  old  entries.   You can disable them temporarily with the        -I/--ignore-assertions flag, which can be useful for troubleshooting or-       for reading Ledger files.  (Note: this flag currently does not  disable+       for  reading Ledger files.  (Note: this flag currently does not disable        balance assignments, below).     Assertions and ordering-       hledger  sorts  an  account's postings and assertions first by date and-       then (for postings on the same day) by parse order.  Note this is  dif-+       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-+       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 differently--       dated transactions within the journal.  But if you  reorder  same-dated-       transactions  or postings, assertions might break and require updating.+       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.     Assertions and included files-       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+       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 multiple -f options@@ -560,15 +578,15 @@        -f options.  Use include or concatenate the files instead.     Assertions and commodities-       The  asserted  balance must be a simple single-commodity amount, and in-       fact the assertion checks only  this  commodity's  balance  within  the-       (possibly  multi-commodity)  account  balance.   This is how assertions+       The asserted balance must be a simple single-commodity amount,  and  in+       fact  the  assertion  checks  only  this commodity's balance within the+       (possibly multi-commodity) account balance.   This  is  how  assertions        work in Ledger also.  We could call this a "partial" balance assertion.         To assert the balance of more than one commodity in an account, you can        write multiple postings, each asserting one commodity's balance. -       You  can  make a stronger "total" balance assertion by writing a double+       You can make a stronger "total" balance assertion by writing  a  double        equals sign (== EXPECTEDBALANCE).  This asserts that there are no other        unasserted commodities in the account (or, that their balance is 0). @@ -588,7 +606,7 @@                 a    0 ==  $1         It's not yet possible to make a complete assertion about a balance that-       has multiple commodities.  One workaround is to isolate each  commodity+       has  multiple commodities.  One workaround is to isolate each commodity        into its own subaccount:                2013/1/1@@ -602,21 +620,21 @@                 a:euro   0 ==  1EUR     Assertions and prices-       Balance  assertions  ignore  transaction prices, and should normally be+       Balance assertions ignore transaction prices, and  should  normally  be        written without one:                2019/1/1                 (a)     $1 @ EUR1 = $1 -       We do allow prices to be written there, however, and print shows  them,-       even  though  they  don't affect whether the assertion passes or fails.-       This is for backward compatibility (hledger's  close  command  used  to-       generate  balance  assertions with prices), and because balance assign-+       We  do allow prices to be written there, however, and print shows them,+       even though they don't affect whether the assertion  passes  or  fails.+       This  is  for  backward  compatibility (hledger's close command used to+       generate balance assertions with prices), and because  balance  assign-        ments do use them (see below).     Assertions and subaccounts-       The balance assertions above (= and ==) do not count the  balance  from-       subaccounts;  they check the account's exclusive balance only.  You can+       The  balance  assertions above (= and ==) do not count the balance from+       subaccounts; they check the account's exclusive balance only.  You  can        assert the balance including subaccounts by writing =* or ==*, eg:                2019/1/1@@ -630,16 +648,16 @@        tual.  They are not affected by the --real/-R flag or real: query.     Assertions and precision-       Balance  assertions  compare  the exactly calculated amounts, which are-       not always what is shown by reports.   Eg  a  commodity  directive  may-       limit  the  display  precision, but this will not affect balance asser-+       Balance assertions compare the exactly calculated  amounts,  which  are+       not  always  what  is  shown  by reports.  Eg a commodity directive may+       limit the display precision, but this will not  affect  balance  asser-        tions.  Balance assertion failure messages show exact amounts.     Balance assignments-       Ledger-style balance assignments are also supported.   These  are  like-       balance  assertions, but with no posting amount on the left side of the-       equals sign; instead it is calculated automatically so  as  to  satisfy-       the  assertion.   This  can be a convenience during data entry, eg when+       Ledger-style  balance  assignments  are also supported.  These are like+       balance assertions, but with no posting amount on the left side of  the+       equals  sign;  instead  it is calculated automatically so as to satisfy+       the assertion.  This can be a convenience during data  entry,  eg  when        setting opening balances:                ; starting a new journal, set asset account balances@@ -657,14 +675,14 @@                 expenses:misc         The calculated amount depends on the account's balance in the commodity-       at  that  point  (which depends on the previously-dated postings of the-       commodity to that account since the last balance assertion  or  assign-+       at that point (which depends on the previously-dated  postings  of  the+       commodity  to  that account since the last balance assertion or assign-        ment).  Note that using balance assignments makes your journal a little        less explicit; to know the exact amount posted, you have to run hledger        or do the calculations yourself, instead of just reading it.     Balance assignments and prices-       A  transaction  price in a balance assignment will cause the calculated+       A transaction price in a balance assignment will cause  the  calculated        amount to have that price attached:                2019/1/1@@ -675,14 +693,15 @@                   (a)         $1 @ EUR2 = $1 @ EUR2     Directives-       A directive is a line in the journal beginning with a special  keyword,+       A  directive is a line in the journal beginning with a special keyword,        that influences how the journal is processed.  hledger's directives are        based on a subset of Ledger's, but there are many differences (and also        some differences between hledger versions).         Directives' behaviour and interactions can get a little bit complex, so-       here is a table summarising the  directives  and  their  effects,  with-       links to more detailed docs.+       here  is  a  table  summarising  the directives and their effects, with+       links to more detailed docs.  Note part of this table  is  hidden  when+       viewed in a web browser - scroll it sideways to see more.         direc-     end   di-   subdi-    purpose                        can  affect  (as of        tive       rective     rec-                                     2018/06)@@ -691,54 +710,48 @@        account                any       document  account names, de-   all  entries in all                               text      clare account types  &  dis-   files,  before   or                                         play order                     after-       alias      end                   rewrite account names          following       in--                  aliases                                              line/included   en--                                                                       tries until end  of-                                                                       current file or end+       alias      end                   rewrite account names          following   entries+                  aliases                                              until  end  of cur-+                                                                       rent  file  or  end                                                                        directive-       apply      end apply             prepend a common  parent  to   following       in--       account    account               account names                  line/included   en--                                                                       tries  until end of-                                                                       current file or end+       apply      end apply             prepend  a  common parent to   following   entries+       account    account               account names                  until  end  of cur-+                                                                       rent  file  or  end                                                                        directive-       comment    end  com-             ignore part of journal         following       in--                  ment                                                 line/included   en--                                                                       tries until end  of-                                                                       current file or end+       comment    end  com-             ignore part of journal         following   entries+                  ment                                                 until  end  of cur-+                                                                       rent  file  or  end                                                                        directive-       commod-                format    declare  a commodity and its   number    notation:+       commod-                format    declare a commodity and  its   number    notation:        ity                              number  notation  &  display   following   entries                                         style                          in  that  commodity-                                                                       in all files;  dis-+                                                                       in all files ; dis-                                                                        play style: amounts                                                                        of  that  commodity                                                                        in reports-       D                                declare  a  commodity  to be   default  commodity:+++       D                                declare a  commodity  to  be   default  commodity:                                         used    for    commodityless   following   commod--                                        amounts,  and its number no-   ityless entries un--                                        tation & display style         til  end of current-                                                                       file; number  nota-+                                        amounts, and its number  no-   ityless entries un-+                                        tation & display style         til end of  current+                                                                       file;  number nota-                                                                        tion: following en--                                                                       tries in that  com-+                                                                       tries  in that com-                                                                        modity until end of-                                                                       current file;  dis-+                                                                       current  file; dis-                                                                        play style: amounts                                                                        of  that  commodity                                                                        in reports        include                          include   entries/directives   what  the  included                                         from another file              directives affect        P                                declare a market price for a   amounts   of   that-                                        commodity                      commodity   in  re--                                                                       ports, when  -V  is+                                        commodity                      commodity  in   re-+                                                                       ports,  when  -V is                                                                        used-----       Y                                declare  a year for yearless   following       in--                                        dates                          line/included   en--                                                                       tries until end  of-                                                                       current file+       Y                                declare a year for  yearless   following   entries+                                        dates                          until end  of  cur-+                                                                       rent file        =                                declare   an   auto  posting   all entries in par-                                         rule,  adding  postings   to   ent/current/child                                         other transactions             files (but not sib-@@ -842,7 +855,7 @@           formats in your data.  (Without this, hledger will parse both  1,000           and 1.000 as 1). -       3. It  declares the amount display style to use in output - decimal and+       3. It  declares  a  commodity's  display  style in output - decimal and           digit group marks, number of decimal places, symbol placement etc.         You are likely to run into one of the problems solved by commodity  di-@@ -877,18 +890,24 @@        comma, followed by 0 or more decimal digits.         Note  hledger  normally  uses  banker's rounding, so 0.5 displayed with-       zero decimal digits is "0".  (More at Amount display style.)+       zero decimal digits is "0".  (More at Commodity display style.) +   Commodity error checking+       In strict mode, enabled with the -s/--strict flag, hledger will  report+       an  error if a commodity symbol is used that has not been declared by a+       commodity directive.  This works similarly to account  error  checking,+       see the notes there for more details.+    Default commodity-       The D directive sets a default commodity, to be used for amounts  with-+       The  D directive sets a default commodity, to be used for amounts with-        out a commodity symbol (ie, plain numbers).  This commodity will be ap-        plied to all subsequent commodity-less amounts, or until the next D di-        rective.  (Note, this is different from Ledger's D.) -       For  compatibility/historical reasons, D also acts like a commodity di-+       For compatibility/historical reasons, D also acts like a commodity  di-        rective, setting the commodity's display style (for output) and decimal        mark (for parsing input).  As with commodity, the amount must always be-       written with a decimal mark (period or comma).  If both directives  are+       written  with a decimal mark (period or comma).  If both directives are        used, commodity's style takes precedence.         The syntax is D AMOUNT.  Eg:@@ -902,9 +921,9 @@                 b     Declaring market prices-       The  P directive declares a market price, which is an exchange rate be--       tween two commodities on a certain date.  (In Ledger, they  are  called-       "historical  prices".)  These are often obtained from a stock exchange,+       The P directive declares a market price, which is an exchange rate  be-+       tween  two  commodities on a certain date.  (In Ledger, they are called+       "historical prices".) These are often obtained from a  stock  exchange,        cryptocurrency exchange, or the foreign exchange market.         Here is the format:@@ -915,47 +934,78 @@         o COMMODITYA is the symbol of the commodity being priced -       o COMMODITYBAMOUNT is an amount (symbol and quantity) in a second  com-+       o COMMODITYBAMOUNT  is an amount (symbol and quantity) in a second com-          modity, giving the price in commodity B of one unit of commodity A. -       These  two  market price directives say that one euro was worth 1.35 US+       These two market price directives say that one euro was worth  1.35  US        dollars during 2009, and $1.40 from 2010 onward:                P 2009/1/1 EUR $1.35               P 2010/1/1 EUR $1.40 -       The -V, -X and --value flags use these market  prices  to  show  amount+       The  -V,  -X  and  --value flags use these market prices to show amount        values in another commodity.  See Valuation.     Declaring accounts-       account directives can be used to pre-declare accounts.  Though not re--       quired, they can provide several benefits:+       account directives can be used to declare accounts (ie, the places that+       amounts  are transferred from and to).  Though not required, these dec-+       larations can provide several benefits:         o They can document your intended chart of accounts, providing a refer-          ence. -       o They  can  store  extra  information about accounts (account numbers,-         notes, etc.)--       o They can help hledger know your accounts'  types  (asset,  liability,-         equity,  revenue,  expense), useful for reports like balancesheet and+       o They  can  help  hledger know your accounts' types (asset, liability,+         equity, revenue, expense), useful for reports like  balancesheet  and          incomestatement. -       o They control account display order in  reports,  allowing  non-alpha-+       o They  control  account  display order in reports, allowing non-alpha-          betic sorting (eg Revenues to appear above Expenses). +       o They can store extra information  about  accounts  (account  numbers,+         notes, etc.)+        o They  help  with account name completion in the add command, hledger-          iadd, hledger-web, ledger-mode etc. -       The simplest form is just the word account followed by a  hledger-style-       account name, eg:+       o In strict mode, they restrict which accounts  may  be  posted  to  by+         transactions, which helps detect typos. +       The  simplest form is just the word account followed by a hledger-style+       account name, eg this account directive declares the assets:bank:check-+       ing account:+               account assets:bank:checking +   Account error checking+       By  default, accounts come into existence when a transaction references+       them by name.  This is convenient, but it means hledger can't warn  you+       when you mis-spell an account name in the journal.  Usually you'll find+       the error later, as an extra account in balance reports, or  an  incor-+       rect balance when reconciling.++       In  strict mode, enabled with the -s/--strict flag, hledger will report+       an error if any transaction uses an account name that has not been  de-+       clared by an account directive.  Some notes:++       o The  declaration is case-sensitive; transactions must use the correct+         account name capitalisation.++       o The account directive's scope is "whole file and below"  (see  direc-+         tives).  This means it affects all of the current file, and any files+         it includes, but not parent or sibling files.  The  position  of  ac-+         count  directives  within the file does not matter, though it's usual+         to put them at the top.++       o Accounts can only be declared in journal files (but will  affect  in-+         cluded files in other formats).++       o It's  currently  not  possible  to declare "all possible subaccounts"+         with a wildcard; every account posted to must be declared.+    Account comments        Comments, beginning with a semicolon, can be added: -       o on  the  same line, after two or more spaces (because ; is allowed in+       o on the same line, after two or more spaces (because ; is  allowed  in          account names)         o on the next lines, indented@@ -969,7 +1019,7 @@        Same-line comments are not supported by Ledger, or hledger <1.13.     Account subdirectives-       We also allow (and ignore) Ledger-style  indented  subdirectives,  just+       We  also  allow  (and ignore) Ledger-style indented subdirectives, just        for compatibility.:                account assets:bank:checking@@ -988,21 +1038,21 @@        Asset, Liability, Equity, Revenue, Expense.         These account types are important for controlling which accounts appear-       in  the  balancesheet, balancesheetequity, incomestatement reports (and+       in the balancesheet, balancesheetequity, incomestatement  reports  (and        probably for other things in future). -       Additionally, we recognise the Cash type, which is also an  Asset,  and-       which  causes  accounts to appear in the cashflow report.  ("Cash" here-       means liquid assets, eg bank balances but typically not investments  or+       Additionally,  we  recognise the Cash type, which is also an Asset, and+       which causes accounts to appear in the cashflow report.   ("Cash"  here+       means  liquid assets, eg bank balances but typically not investments or        receivables.)     Declaring account types        Generally, to make these reports work you should declare your top-level        accounts and their types, using account directives with type: tags. -       The tag's value should be one of: Asset,  Liability,  Equity,  Revenue,-       Expense,  Cash,  A,  L, E, R, X, C (all case insensitive).  The type is-       inherited by all subaccounts except where they override it.   Here's  a+       The  tag's  value  should be one of: Asset, Liability, Equity, Revenue,+       Expense, Cash, A, L, E, R, X, C (all case insensitive).   The  type  is+       inherited  by  all subaccounts except where they override it.  Here's a        complete example:                account assets       ; type: Asset@@ -1014,8 +1064,8 @@               account expenses     ; type: Expense     Auto-detected account types-       If  you  happen  to use common english top-level account names, you may-       not need to declare account types, as they will be  detected  automati-+       If you happen to use common english top-level account  names,  you  may+       not  need  to declare account types, as they will be detected automati-        cally using the following rules:         If  name  matches  regular   account type is:@@ -1025,10 +1075,11 @@        ^(debts?|lia-                Liability        bilit(y|ies))(:|$)        ^equity(:|$)                 Equity+        ^(income|revenue)s?(:|$)     Revenue        ^expenses?(:|$)              Expense -       If  account type is Asset and name does not contain regu-   account  type+       If account type is Asset and name does not contain  regu-   account  type        lar expression:                                             is:        --------------------------------------------------------------------------        (investment|receivable|:A/R|:fixed)                         Cash@@ -1038,9 +1089,9 @@     Interference from auto-detected account types        If you assign any account type, it's a good idea to assign all of them,-       to  prevent any confusion from mixing declared and auto-detected types.-       Although it's unlikely to happen in real life, here's an example:  with-       the  following  journal, balancesheetequity shows "liabilities" in both+       to prevent any confusion from mixing declared and auto-detected  types.+       Although  it's unlikely to happen in real life, here's an example: with+       the following journal, balancesheetequity shows "liabilities"  in  both        Liabilities and Equity sections.  Declaring another account as type:Li-        ability would fix it: @@ -1052,8 +1103,8 @@                 equity       -2     Old account type syntax-       In  some  hledger  journals  you might instead see this old syntax (the-       letters ALERX, separated from the account name by two or more  spaces);+       In some hledger journals you might instead see  this  old  syntax  (the+       letters  ALERX, separated from the account name by two or more spaces);        this is deprecated and may be removed soon:                account assets       A@@ -1063,8 +1114,8 @@               account expenses     X     Account display order-       Account  directives also set the order in which accounts are displayed,-       eg in reports, the hledger-ui  accounts  screen,  and  the  hledger-web+       Account directives also set the order in which accounts are  displayed,+       eg  in  reports,  the  hledger-ui  accounts screen, and the hledger-web        sidebar.  By default accounts are listed in alphabetical order.  But if        you have these account directives in the journal: @@ -1086,20 +1137,20 @@         Undeclared accounts, if any, are displayed last, in alphabetical order. -       Note  that  sorting  is  done at each level of the account tree (within-       each group of sibling accounts under the same parent).  And  currently,+       Note that sorting is done at each level of  the  account  tree  (within+       each  group of sibling accounts under the same parent).  And currently,        this directive:                account other:zoo -       would  influence the position of zoo among other's subaccounts, but not+       would influence the position of zoo among other's subaccounts, but  not        the position of other among the top-level accounts.  This means: -       o you will sometimes declare parent accounts (eg account  other  above)+       o you  will  sometimes declare parent accounts (eg account other above)          that you don't intend to post to, just to customize their display or-          der -       o sibling accounts stay together (you couldn't display x:y  in  between+       o sibling  accounts  stay together (you couldn't display x:y in between          a:b and a:c).     Rewriting accounts@@ -1117,14 +1168,14 @@        o customising reports         Account aliases also rewrite account names in account directives.  They-       do  not  affect account names being entered via hledger add or hledger-+       do not affect account names being entered via hledger add  or  hledger-        web.         See also Rewrite account names.     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+       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@@ -1132,49 +1183,49 @@        Or, you can use the --alias 'OLD=NEW' option on the command line.  This        affects all entries.  It's useful for trying out aliases interactively. -       OLD  and  NEW  are case sensitive full account names.  hledger will re--       place any occurrence of the old account name with the new one.   Subac-+       OLD and NEW are case sensitive full account names.   hledger  will  re-+       place  any occurrence of the old account name with the new one.  Subac-        counts 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,+       There is also a more powerful variant that uses a  regular  expression,        indicated by the forward slashes:                alias /REGEX/ = REPLACEMENT         or --alias '/REGEX/=REPLACEMENT'. -       REGEX is a case-insensitive regular expression.   Anywhere  it  matches-       inside  an  account name, the matched part will be replaced by REPLACE--       MENT.  If REGEX contains parenthesised match groups, these can be  ref-+       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.  Eg:                alias /^(.+):bank:([^:]+):(.*)/ = \1:\2 \3               ; rewrites "assets:bank:wells fargo:checking" to  "assets:wells fargo checking" -       Also  note that REPLACEMENT continues to the end of line (or on command-       line, to end of option argument), so it  can  contain  trailing  white-+       Also note that REPLACEMENT continues to the end of line (or on  command+       line,  to  end  of  option argument), so it can contain trailing white-        space.     Combining aliases-       You  can  define  as many aliases as you like, using journal directives+       You can define as many aliases as you like,  using  journal  directives        and/or command line options. -       Recursive aliases - where an account name is rewritten  by  one  alias,-       then  by  another  alias, and so on - are allowed.  Each alias sees the+       Recursive  aliases  -  where an account name is rewritten by one alias,+       then by another alias, and so on - are allowed.  Each  alias  sees  the        effect of previously applied aliases. -       In such cases it can be important to understand which aliases  will  be-       applied  and  in  which order.  For (each account name in) each journal+       In  such  cases it can be important to understand which aliases will be+       applied and in which order.  For (each account name  in)  each  journal        entry, we apply: -       1. alias directives preceding the journal entry, most  recently  parsed+       1. alias  directives  preceding the journal entry, most recently parsed           first (ie, reading upward from the journal entry, bottom to top) -       2. --alias  options,  in  the  order  they appeared on the command line+       2. --alias options, in the order they  appeared  on  the  command  line           (left to right).         In other words, for (an account name in) a given journal entry:@@ -1185,20 +1236,20 @@         o aliases defined after/below the entry do not affect it. -       This gives nearby aliases precedence over distant ones, and helps  pro--       vide  semantic stability - aliases will keep working the same way inde-+       This  gives nearby aliases precedence over distant ones, and helps pro-+       vide semantic stability - aliases will keep working the same way  inde-        pendent of which files are being read and in which order. -       In case of trouble, adding --debug=6 to  the  command  line  will  show+       In  case  of  trouble,  adding  --debug=6 to the command line will show        which aliases are being applied when.     Aliases and multiple files-       As  explained at Directives and multiple files, alias directives do not+       As explained at Directives and multiple files, alias directives do  not        affect parent or sibling files.  Eg in this command,                hledger -f a.aliases -f b.journal -       account aliases defined in a.aliases will not  affect  b.journal.   In-+       account  aliases  defined  in a.aliases will not affect b.journal.  In-        cluding the aliases doesn't work either:                include a.aliases@@ -1220,14 +1271,14 @@               include c.journal  ; also affected     end aliases-       You can clear (forget) all  currently  defined  aliases  with  the  end+       You  can  clear  (forget)  all  currently  defined aliases with the end        aliases directive:                end aliases     Default parent account-       You  can  specify  a  parent account which will be prepended to all ac--       counts within a section of the journal.  Use the apply account and  end+       You can specify a parent account which will be  prepended  to  all  ac-+       counts  within a section of the journal.  Use the apply account and end        apply account directives like so:                apply account home@@ -1244,7 +1295,7 @@                   home:food           $10                   home:cash          $-10 -       If  end  apply  account  is omitted, the effect lasts to the end of the+       If end apply account is omitted, the effect lasts to  the  end  of  the        file.  Included files are also affected, eg:                apply account business@@ -1253,50 +1304,50 @@               apply account personal               include personal.journal -       Prior to hledger 1.0, legacy account and end spellings were  also  sup-+       Prior  to  hledger 1.0, legacy account and end spellings were also sup-        ported. -       A  default parent account also affects account directives.  It does not-       affect account names being entered via hledger add or hledger-web.   If-       account  aliases are present, they are applied after the default parent+       A default parent account also affects account directives.  It does  not+       affect  account names being entered via hledger add or hledger-web.  If+       account aliases are present, they are applied after the default  parent        account.     Periodic transactions-       Periodic transaction rules describe transactions that recur.  They  al--       low  hledger  to  generate  temporary  future transactions to help with-       forecasting, so you don't have to write out each one  in  the  journal,-       and  it's easy to try out different forecasts.  Secondly, they are also+       Periodic  transaction rules describe transactions that recur.  They al-+       low hledger to generate temporary  future  transactions  to  help  with+       forecasting,  so  you  don't have to write out each one in the journal,+       and it's easy to try out different forecasts.  Secondly, they are  also        used to define the budgets shown in budget reports. -       Periodic transactions can be a little tricky, so before you  use  them,+       Periodic  transactions  can be a little tricky, so before you use them,        read this whole section - or at least these tips: -       1. Two  spaces  accidentally  added or omitted will cause you trouble -+       1. Two spaces accidentally added or omitted will cause  you  trouble  -           read about this below. -       2. For troubleshooting, show the generated  transactions  with  hledger-          print   --forecast  tag:generated  or  hledger  register  --forecast+       2. For  troubleshooting,  show  the generated transactions with hledger+          print  --forecast  tag:generated  or  hledger  register   --forecast           tag:generated. -       3. Forecasted transactions will begin only  after  the  last  non-fore-+       3. Forecasted  transactions  will  begin  only after the last non-fore-           casted transaction's date. -       4. Forecasted  transactions  will  end 6 months from today, by default.+       4. Forecasted transactions will end 6 months from  today,  by  default.           See below for the exact start/end rules. -       5. period expressions can be tricky.   Their  documentation  needs  im-+       5. period  expressions  can  be  tricky.  Their documentation needs im-           provement, but is worth studying. -       6. Some  period  expressions  with a repeating interval must begin on a-          natural boundary of that interval.  Eg in  weekly  from  DATE,  DATE-          must  be a monday.  ~ weekly from 2019/10/1 (a tuesday) will give an+       6. Some period expressions with a repeating interval must  begin  on  a+          natural  boundary  of  that  interval.  Eg in weekly from DATE, DATE+          must be a monday.  ~ weekly from 2019/10/1 (a tuesday) will give  an           error.         7. Other period expressions with an interval are automatically expanded-          to  cover a whole number of that interval.  (This is done to improve+          to cover a whole number of that interval.  (This is done to  improve           reports, but it also affects periodic transactions.  Yes, it's a bit-          inconsistent  with  the  above.)  Eg: ~ every 10th day of month from-          2020/01, which is equivalent to ~  every  10th  day  of  month  from+          inconsistent with the above.) Eg: ~ every 10th  day  of  month  from+          2020/01,  which  is  equivalent  to  ~  every 10th day of month from           2020/01/01, will be adjusted to start on 2019/12/10.     Periodic rule syntax@@ -1308,17 +1359,17 @@                   expenses:rent          $2000                   assets:bank:checking -       There  is  an additional constraint on the period expression: the start-       date must fall on a natural boundary of the interval.  Eg monthly  from+       There is an additional constraint on the period expression:  the  start+       date  must fall on a natural boundary of the interval.  Eg monthly from        2018/1/1 is valid, but monthly from 2018/1/15 is not. -       Partial  or  relative dates (M/D, D, tomorrow, last week) in the period-       expression can work (useful or not).  They will be relative to  today's-       date,  unless  a  Y  default year directive is in effect, in which case+       Partial or relative dates (M/D, D, tomorrow, last week) in  the  period+       expression  can work (useful or not).  They will be relative to today's+       date, unless a Y default year directive is in  effect,  in  which  case        they will be relative to Y/1/1.     Two spaces between period expression and description!-       If the period expression is  followed  by  a  transaction  description,+       If  the  period  expression  is  followed by a transaction description,        these must be separated by two or more spaces.  This helps hledger know        where the period expression ends, so that descriptions can not acciden-        tally alter their meaning, as in this example:@@ -1332,64 +1383,63 @@         So, -       o Do  write two spaces between your period expression and your transac-+       o Do write two spaces between your period expression and your  transac-          tion description, if any. -       o Don't accidentally write two spaces in the middle of your period  ex-+       o Don't  accidentally write two spaces in the middle of your period ex-          pression.     Forecasting with periodic transactions-       The  --forecast  flag  activates  any periodic transaction rules in the-       journal.  They will generate temporary  recurring  transactions,  which-       are  not  saved  in  the  journal,  but  will appear in all reports (eg+       The --forecast flag activates any periodic  transaction  rules  in  the+       journal.   They  will  generate temporary recurring transactions, which+       are not saved in the journal,  but  will  appear  in  all  reports  (eg        print).  This can be useful for estimating balances into the future, or-       experimenting  with  different scenarios.  Or, it can be used as a data+       experimenting with different scenarios.  Or, it can be used as  a  data        entry aid: describe recurring transactions, and every so often copy the        output of print --forecast into the journal. -       These  transactions  will  have  an extra tag indicating which periodic+       These transactions will have an extra  tag  indicating  which  periodic        rule generated them: generated-transaction:~ PERIODICEXPR.  And a simi--       lar,  hidden  tag  (beginning  with  an underscore) which, because it's-       never displayed by print, can be used to match  transactions  generated+       lar, hidden tag (beginning with  an  underscore)  which,  because  it's+       never  displayed  by print, can be used to match transactions generated        "just now": _generated-transaction:~ PERIODICEXPR. -       Periodic  transactions  are  generated within some forecast period.  By+       Periodic transactions are generated within some  forecast  period.   By        default, this         o begins on the later of           o the report start date if specified with -b/-p/date: -         o the day after the latest normal (non-periodic) transaction  in  the+         o the  day  after the latest normal (non-periodic) transaction in the            journal, or today if there are no normal transactions. -       o ends  on  the  report  end  date  if specified with -e/-p/date:, or 6+       o ends on the report end date  if  specified  with  -e/-p/date:,  or  6          months (180 days) from today. -       This means that periodic transactions will begin only after the  latest-       recorded  transaction.   And a recorded transaction dated in the future-       can prevent generation of periodic transactions.  (You can  avoid  that+       This  means that periodic transactions will begin only after the latest+       recorded transaction.  And a recorded transaction dated in  the  future+       can  prevent  generation of periodic transactions.  (You can avoid that        by writing the future transaction as a one-time periodic rule instead -        put tilde before the date, eg ~ YYYY-MM-DD ...).         Or, you can set your own arbitrary "forecast period", which can overlap-       recorded  transactions,  and need not be in the future, by providing an-       option argument, like --forecast=PERIODEXPR.  Note the equals  sign  is+       recorded transactions, and need not be in the future, by  providing  an+       option  argument,  like --forecast=PERIODEXPR.  Note the equals sign is        required, a space won't work.  PERIODEXPR is a period expression, which-       can specify the start date, end date, or both, like in a  date:  query.-       (See  also  hledger.1  ->  Report  start  &  end date).  Some examples:+       can  specify  the start date, end date, or both, like in a date: query.+       (See also hledger.1 ->  Report  start  &  end  date).   Some  examples:        --forecast=202001-202004, --forecast=jan-, --forecast=2020.     Budgeting with periodic transactions-       With the --budget flag, currently supported  by  the  balance  command,-       each  periodic transaction rule declares recurring budget goals for the-       specified accounts.  Eg the first example  above  declares  a  goal  of-       spending  $2000  on  rent  (and  also,  a goal of depositing $2000 into-       checking) every month.  Goals and actual performance can then  be  com-+       With  the  --budget  flag,  currently supported by the balance command,+       each periodic transaction rule declares recurring budget goals for  the+       specified  accounts.   Eg  the  first  example above declares a goal of+       spending $2000 on rent (and also,  a  goal  of  depositing  $2000  into+       checking)  every  month.  Goals and actual performance can then be com-        pared in budget reports. -       For  more  details, see: balance: Budget report and Budgeting and Fore--       casting.+       See also: Budgeting and Forecasting.     Auto postings        "Automated postings" or "auto postings" are extra  postings  which  get@@ -1526,4 +1576,4 @@   -hledger 1.18.99                 September 2020              hledger_journal(5)+hledger 1.20                     November 2020              hledger_journal(5)
hledger_timeclock.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_timeclock" "5" "September 2020" "hledger 1.18.99" "hledger User Manuals"+.TH "hledger_timeclock" "5" "November 2020" "hledger 1.20" "hledger User Manuals"   
hledger_timeclock.info view
@@ -4,8 +4,8 @@  File: hledger_timeclock.info,  Node: Top,  Up: (dir) -hledger_timeclock(5) hledger 1.18.99-************************************+hledger_timeclock(5) hledger 1.20+*********************************  Timeclock - the time logging format of timeclock.el, as read by hledger 
hledger_timeclock.txt view
@@ -78,4 +78,4 @@   -hledger 1.18.99                 September 2020            hledger_timeclock(5)+hledger 1.20                     November 2020            hledger_timeclock(5)
hledger_timedot.5 view
@@ -1,5 +1,5 @@ -.TH "hledger_timedot" "5" "September 2020" "hledger 1.18.99" "hledger User Manuals"+.TH "hledger_timedot" "5" "November 2020" "hledger 1.20" "hledger User Manuals"   @@ -168,7 +168,7 @@ .IP .nf \f[C]-$ hledger -f t.timedot --alias /\[rs]\[rs]./=: bal date:2016/2/4+$ hledger -f t.timedot --alias /\[rs]\[rs]./=: bal date:2016/2/4 --tree                 4.50  fos                 4.00    hledger:timedot                 0.50    ledger
hledger_timedot.info view
@@ -4,8 +4,8 @@  File: hledger_timedot.info,  Node: Top,  Up: (dir) -hledger_timedot(5) hledger 1.18.99-**********************************+hledger_timedot(5) hledger 1.20+*******************************  Timedot - hledger's human-friendly time logging format @@ -129,7 +129,7 @@ fos.hledger.timedot  4 fos.ledger           .. -$ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4+$ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4 --tree                 4.50  fos                 4.00    hledger:timedot                 0.50    ledger
hledger_timedot.txt view
@@ -127,7 +127,7 @@               fos.hledger.timedot  4               fos.ledger           .. -              $ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4+              $ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4 --tree                               4.50  fos                               4.00    hledger:timedot                               0.50    ledger@@ -161,4 +161,4 @@   -hledger 1.18.99                 September 2020              hledger_timedot(5)+hledger 1.20                     November 2020              hledger_timedot(5)