packages feed

hledger-lib 1.26.1 → 1.27

raw patch · 41 files changed

+1337/−686 lines, 41 filesdep +breakpointdep +deepseqdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: breakpoint, deepseq

Dependency ranges changed: base

API changes (from Hackage documentation)

- Hledger.Data.Amount: amount :: Amount
- Hledger.Data.Balancing: annotateErrorWithTransaction :: Transaction -> String -> String
- Hledger.Data.Journal: journalAssetAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalBalanceSheetAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalCashAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalEquityAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalExpenseAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalLiabilityAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalProfitAndLossAccountQuery :: Journal -> Query
- Hledger.Data.Journal: journalRevenueAccountQuery :: Journal -> Query
- Hledger.Data.Json: instance Data.Aeson.Types.FromJSON.FromJSON (Data.Decimal.DecimalRaw GHC.Integer.Type.Integer)
- Hledger.Query: prefixes :: [Text]
- Hledger.Utils.Debug: dlog :: Show a => a -> a
+ Hledger.Data: [adisourcepos] :: AccountDeclarationInfo -> SourcePos
+ Hledger.Data.Dates: intervalStartBefore :: Interval -> Day -> Day
+ Hledger.Data.Errors: makeBalanceAssertionErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe Int), Text)
+ Hledger.Data.Errors: makePostingAccountErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe Int), Text)
+ Hledger.Data.Journal: dbgJournalAcctDeclOrder :: String -> Journal -> Journal
+ Hledger.Data.Journal: filterTransactionPostingsExtra :: (AccountName -> Maybe AccountType) -> Query -> Transaction -> Transaction
+ Hledger.Data.Journal: journalAddPricesFromEquity :: Journal -> Either String Journal
+ Hledger.Data.Journal: journalConcat :: Journal -> Journal -> Journal
+ Hledger.Data.Journal: journalRenumberAccountDeclarations :: Journal -> Journal
+ Hledger.Data.JournalChecks: journalCheckRecentAssertions :: Day -> Journal -> Either String ()
+ Hledger.Data.Json: instance Data.Aeson.Types.FromJSON.FromJSON (Data.Decimal.DecimalRaw GHC.Num.Integer.Integer)
+ Hledger.Data.Posting: showBalanceAssertion :: BalanceAssertion -> WideBuilder
+ Hledger.Data.Transaction: annotateErrorWithTransaction :: Transaction -> String -> String
+ Hledger.Data.Transaction: transactionAddPricesFromEquity :: Map AccountName AccountType -> Transaction -> Either String Transaction
+ Hledger.Data.Types: [adisourcepos] :: AccountDeclarationInfo -> SourcePos
+ Hledger.Query: queryprefixes :: [Text]
+ Hledger.Read.Common: [infer_costs_] :: InputOpts -> Bool
+ Hledger.Read.Common: infer_costs :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
+ Hledger.Read.InputOptions: [infer_costs_] :: InputOpts -> Bool
+ Hledger.Read.InputOptions: infer_costs :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
+ Hledger.Utils.Debug: dlog0 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog1 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog2 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog3 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog4 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog5 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog6 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog7 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog8 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlog9 :: Show a => String -> a -> a
+ Hledger.Utils.Debug: dlogAt :: Show a => Int -> String -> a -> a
+ Hledger.Utils.Debug: dlogTrace :: String -> a -> a
+ Hledger.Utils.Debug: dlogTraceAt :: Int -> String -> a -> a
+ Hledger.Utils.Debug: pprint' :: Show a => a -> IO ()
+ Hledger.Utils.Debug: pshow' :: Show a => a -> String
+ Hledger.Utils.Parse: type HledgerParseErrors = ParseErrorBundle Text HledgerParseErrorData
+ Hledger.Utils.String: quoteForCommandLine :: String -> String
- Hledger.Data: AccountDeclarationInfo :: Text -> [Tag] -> Int -> AccountDeclarationInfo
+ Hledger.Data: AccountDeclarationInfo :: Text -> [Tag] -> Int -> SourcePos -> AccountDeclarationInfo
- Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a38kn => Lens' c_a38kn BalancingOpts
+ Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a3v1G => Lens' c_a3v1G BalancingOpts
- Hledger.Data.Balancing: class HasBalancingOpts c_a38kn
+ Hledger.Data.Balancing: class HasBalancingOpts c_a3v1G
- Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a38kn => Lens' c_a38kn (Maybe (Map CommoditySymbol AmountStyle))
+ Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a3v1G => Lens' c_a3v1G (Maybe (Map CommoditySymbol AmountStyle))
- Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a38kn => Lens' c_a38kn Bool
+ Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a3v1G => Lens' c_a3v1G Bool
- Hledger.Data.Balancing: infer_transaction_prices :: HasBalancingOpts c_a38kn => Lens' c_a38kn Bool
+ Hledger.Data.Balancing: infer_transaction_prices :: HasBalancingOpts c_a3v1G => Lens' c_a3v1G Bool
- Hledger.Data.Period: isLastDayOfMonth :: (Eq a1, Eq a2, Num a1, Num a2) => Integer -> a1 -> a2 -> Bool
+ Hledger.Data.Period: isLastDayOfMonth :: (Eq a, Eq a, Num a, Num a) => Year -> a -> a -> Bool
- Hledger.Data.Period: startOfFirstWeekInMonth :: Integer -> Int -> Day
+ Hledger.Data.Period: startOfFirstWeekInMonth :: Year -> MonthOfYear -> Day
- Hledger.Data.Period: yearMonthContainingWeekStarting :: Day -> (Integer, Int)
+ Hledger.Data.Period: yearMonthContainingWeekStarting :: Day -> (Year, MonthOfYear)
- Hledger.Data.Posting: postingToCost :: Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Posting
+ Hledger.Data.Posting: postingToCost :: Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Maybe Posting
- Hledger.Data.Types: AccountDeclarationInfo :: Text -> [Tag] -> Int -> AccountDeclarationInfo
+ Hledger.Data.Types: AccountDeclarationInfo :: Text -> [Tag] -> Int -> SourcePos -> AccountDeclarationInfo
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> Bool -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
- Hledger.Read.Common: aliases :: HasInputOpts c_a3luJ => Lens' c_a3luJ [String]
+ Hledger.Read.Common: aliases :: HasInputOpts c_a3J4P => Lens' c_a3J4P [String]
- Hledger.Read.Common: anon__ :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.Common: anon__ :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.Common: auto__ :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.Common: auto__ :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.Common: balancingopts :: HasInputOpts c_a3luJ => Lens' c_a3luJ BalancingOpts
+ Hledger.Read.Common: balancingopts :: HasInputOpts c_a3J4P => Lens' c_a3J4P BalancingOpts
- Hledger.Read.Common: class HasInputOpts c_a3luJ
+ Hledger.Read.Common: class HasInputOpts c_a3J4P
- Hledger.Read.Common: forecast :: HasInputOpts c_a3luJ => Lens' c_a3luJ (Maybe DateSpan)
+ Hledger.Read.Common: forecast :: HasInputOpts c_a3J4P => Lens' c_a3J4P (Maybe DateSpan)
- Hledger.Read.Common: infer_equity :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.Common: infer_equity :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.Common: inputOpts :: HasInputOpts c_a3luJ => Lens' c_a3luJ InputOpts
+ Hledger.Read.Common: inputOpts :: HasInputOpts c_a3J4P => Lens' c_a3J4P InputOpts
- Hledger.Read.Common: ioDay :: HasInputOpts c_a3luJ => Lens' c_a3luJ Day
+ Hledger.Read.Common: ioDay :: HasInputOpts c_a3J4P => Lens' c_a3J4P Day
- Hledger.Read.Common: mformat :: HasInputOpts c_a3luJ => Lens' c_a3luJ (Maybe StorageFormat)
+ Hledger.Read.Common: mformat :: HasInputOpts c_a3J4P => Lens' c_a3J4P (Maybe StorageFormat)
- Hledger.Read.Common: mrules_file :: HasInputOpts c_a3luJ => Lens' c_a3luJ (Maybe FilePath)
+ Hledger.Read.Common: mrules_file :: HasInputOpts c_a3J4P => Lens' c_a3J4P (Maybe FilePath)
- Hledger.Read.Common: new__ :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.Common: new__ :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.Common: new_save :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.Common: new_save :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.Common: pivot :: HasInputOpts c_a3luJ => Lens' c_a3luJ String
+ Hledger.Read.Common: pivot :: HasInputOpts c_a3J4P => Lens' c_a3J4P String
- Hledger.Read.Common: reportspan :: HasInputOpts c_a3luJ => Lens' c_a3luJ DateSpan
+ Hledger.Read.Common: reportspan :: HasInputOpts c_a3J4P => Lens' c_a3J4P DateSpan
- Hledger.Read.Common: strict :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.Common: strict :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.InputOptions: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
+ Hledger.Read.InputOptions: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> Bool -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
- Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3luJ => Lens' c_a3luJ [String]
+ Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3J4P => Lens' c_a3J4P [String]
- Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3luJ => Lens' c_a3luJ BalancingOpts
+ Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3J4P => Lens' c_a3J4P BalancingOpts
- Hledger.Read.InputOptions: class HasInputOpts c_a3luJ
+ Hledger.Read.InputOptions: class HasInputOpts c_a3J4P
- Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3luJ => Lens' c_a3luJ (Maybe DateSpan)
+ Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3J4P => Lens' c_a3J4P (Maybe DateSpan)
- Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3luJ => Lens' c_a3luJ InputOpts
+ Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3J4P => Lens' c_a3J4P InputOpts
- Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3luJ => Lens' c_a3luJ Day
+ Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3J4P => Lens' c_a3J4P Day
- Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3luJ => Lens' c_a3luJ (Maybe StorageFormat)
+ Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3J4P => Lens' c_a3J4P (Maybe StorageFormat)
- Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3luJ => Lens' c_a3luJ (Maybe FilePath)
+ Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3J4P => Lens' c_a3J4P (Maybe FilePath)
- Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3luJ => Lens' c_a3luJ String
+ Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3J4P => Lens' c_a3J4P String
- Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3luJ => Lens' c_a3luJ DateSpan
+ Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3J4P => Lens' c_a3J4P DateSpan
- Hledger.Read.InputOptions: strict :: HasInputOpts c_a3luJ => Lens' c_a3luJ Bool
+ Hledger.Read.InputOptions: strict :: HasInputOpts c_a3J4P => Lens' c_a3J4P Bool
- Hledger.Read.TimedotReader: timedotfilep :: forall (m :: Type -> Type). JournalParser m ParsedJournal
+ Hledger.Read.TimedotReader: timedotfilep :: forall {m :: Type -> Type}. JournalParser m ParsedJournal
- Hledger.Reports.AccountTransactionsReport: accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [(Day, Transaction)] -> [AccountTransactionsReportItem]
+ Hledger.Reports.AccountTransactionsReport: accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> (AccountName -> Maybe AccountType) -> [(Day, Transaction)] -> [AccountTransactionsReportItem]
- Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT AccountListMode
+ Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG AccountListMode
- Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT BalanceAccumulation
+ Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG BalanceAccumulation
- Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT BalanceCalculation
+ Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG BalanceCalculation
- Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT (Maybe Text)
+ Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG (Maybe Text)
- Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a3sPT
+ Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a3QNG
- Hledger.Reports.ReportOptions: class HasReportSpec c_a3txg
+ Hledger.Reports.ReportOptions: class HasReportSpec c_a3RLv
- Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT (Maybe ConversionOp)
+ Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG (Maybe ConversionOp)
- Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT (Maybe Int)
+ Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG (Maybe Int)
- Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Int
+ Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Int
- Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT StringFormat
+ Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG StringFormat
- Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Interval
+ Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Interval
- Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Layout
+ Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Layout
- Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT (Maybe NormalSign)
+ Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG (Maybe NormalSign)
- Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Period
+ Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Period
- Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT [Text]
+ Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG [Text]
- Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT ReportOpts
+ Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG ReportOpts
- Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a3txg => Lens' c_a3txg ReportSpec
+ Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a3RLv => Lens' c_a3RLv ReportSpec
- Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a3txg => Lens' c_a3txg Day
+ Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a3RLv => Lens' c_a3RLv Day
- Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a3txg => Lens' c_a3txg Query
+ Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a3RLv => Lens' c_a3RLv Query
- Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a3txg => Lens' c_a3txg [QueryOpt]
+ Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a3RLv => Lens' c_a3RLv [QueryOpt]
- Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a3txg => Lens' c_a3txg ReportOpts
+ Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a3RLv => Lens' c_a3RLv ReportOpts
- Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT [Status]
+ Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG [Status]
- Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT Bool
+ Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG Bool
- Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a3sPT => Lens' c_a3sPT (Maybe ValuationType)
+ Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a3QNG => Lens' c_a3QNG (Maybe ValuationType)
- Hledger.Reports.ReportTypes: CBCSubreportSpec :: Text -> (Journal -> Query) -> (ReportOpts -> ReportOpts) -> (PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount) -> Bool -> CBCSubreportSpec a
+ Hledger.Reports.ReportTypes: CBCSubreportSpec :: Text -> Query -> (ReportOpts -> ReportOpts) -> (PeriodicReport DisplayName MixedAmount -> PeriodicReport a MixedAmount) -> Bool -> CBCSubreportSpec a
- Hledger.Reports.ReportTypes: [cbcsubreportquery] :: CBCSubreportSpec a -> Journal -> Query
+ Hledger.Reports.ReportTypes: [cbcsubreportquery] :: CBCSubreportSpec a -> Query

Files

CHANGES.md view
@@ -9,6 +9,55 @@ Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. +# 1.27 2022-09-01++Breaking changes++- Support for GHC 8.6 and 8.8 has been dropped.+  hledger now requires GHC 8.10 or newer.++- Hledger.Data.Amount: `amount` has been dropped; use `nullamt` instead.++- journal*AccountQuery functions have been dropped; use a type: query instead.+  cbcsubreportquery no longer takes Journal as an argument.+  (#1921)++Misc. changes++- Hledger.Utils.Debug now re-exports Debug.Breakpoint from the+  breakpoint library, so that breakpoint's helpers can be used easily+  during development.++- Hledger.Utils.Debug:+  dlog has been replaced by more reliable functions for debug-logging+  to a file (useful for debugging TUI apps like hledger-ui):++  dlogTrace+  dlogTraceAt+  dlogAt+  dlog0+  dlog1+  dlog2+  dlog3+  dlog4+  dlog5+  dlog6+  dlog7+  dlog8+  dlog9++- Hledger.Utils.Debug: pprint' and pshow' have been added,+  forcing monochrome output.++- Hledger.Utils.String: add quoteForCommandLine++- Hledger.Data.Errors: export makeBalanceAssertionErrorExcerpt++- Hledger.Utils.Parse: export HledgerParseErrors++- Debug logging from journalFilePath and the include directive will+  now show "(unknown)" instead of an empty string.+ # 1.26.1 2022-07-11  - require safe 0.3.19+ to avoid deprecation warning
Hledger/Data/Account.hs view
@@ -104,10 +104,10 @@ accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m }   where     T m = treeFromPaths $ map expandAccountName as :: FastTree AccountName-    accountTree' a (T m) =+    accountTree' a (T m') =       nullacct{         aname=a-       ,asubs=map (uncurry accountTree') $ M.assocs m+       ,asubs=map (uncurry accountTree') $ M.assocs m'        }  -- | An efficient-to-build tree suggested by Cale Gibbard, probably@@ -223,7 +223,7 @@ -- tree's structure remains intact and can still be used. It's a tree/list! flattenAccounts :: Account -> [Account] flattenAccounts a = squish a []-  where squish a as = a : Prelude.foldr squish as (asubs a)+  where squish a' as = a' : Prelude.foldr squish as (asubs a')  -- | Filter an account tree (to a list). filterAccounts :: (Account -> Bool) -> Account -> [Account]
Hledger/Data/AccountName.hs view
@@ -177,12 +177,12 @@ -- Or, return any error arising from a bad regular expression in the aliases. accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName accountNameApplyAliases aliases a =-  let (aname,atype) = (accountNameWithoutPostingType a, accountNamePostingType a)+  let (name,typ) = (accountNameWithoutPostingType a, accountNamePostingType a)   in foldM      (\acct alias -> dbg6 "result" $ aliasReplace (dbg6 "alias" alias) (dbg6 "account" acct))-     aname+     name      aliases-     >>= Right . accountNameWithPostingType atype+     >>= Right . accountNameWithPostingType typ  -- | Memoising version of accountNameApplyAliases, maybe overkill. accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName@@ -238,7 +238,7 @@ parentAccountNames a = parentAccountNames' $ parentAccountName a     where       parentAccountNames' "" = []-      parentAccountNames' a = a : parentAccountNames' (parentAccountName a)+      parentAccountNames' a2 = a2 : parentAccountNames' (parentAccountName a2)  -- | Is the first account a parent or other ancestor of (and not the same as) the second ? isAccountNamePrefixOf :: AccountName -> AccountName -> Bool@@ -296,9 +296,9 @@     fitText Nothing (Just width) True False $ accountNameFromComponents $ elideparts width [] $ accountNameComponents s       where         elideparts :: Int -> [Text] -> [Text] -> [Text]-        elideparts width done ss-          | realLength (accountNameFromComponents $ done++ss) <= width = done++ss-          | length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss)+        elideparts w done ss+          | realLength (accountNameFromComponents $ done++ss) <= w = done++ss+          | length ss > 1 = elideparts w (done++[textTakeWidth 2 $ head ss]) (tail ss)           | otherwise = done++ss  -- | Keep only the first n components of an account name, where n
Hledger/Data/Amount.hs view
@@ -49,7 +49,6 @@   isNonsimpleCommodityChar,   quoteCommoditySymbolIfNeeded,   -- * Amount-  amount,   nullamt,   missingamt,   num,@@ -256,22 +255,22 @@     (*)                          = similarAmountsOp (*)  -- | The empty simple amount.-amount, nullamt :: Amount-amount = Amount{acommodity="", aquantity=0, aprice=Nothing, astyle=amountstyle}-nullamt = amount+nullamt :: Amount+nullamt = Amount{acommodity="", aquantity=0, aprice=Nothing, astyle=amountstyle}  -- | A temporary value for parsed transactions which had no amount specified. missingamt :: Amount-missingamt = amount{acommodity="AUTO"}+missingamt = nullamt{acommodity="AUTO"}  -- Handy amount constructors for tests. -- usd/eur/gbp round their argument to a whole number of pennies/cents.-num n = amount{acommodity="",  aquantity=n}-hrs n = amount{acommodity="h", aquantity=n,           astyle=amountstyle{asprecision=Precision 2, ascommodityside=R}}-usd n = amount{acommodity="$", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}-eur n = amount{acommodity="€", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}-gbp n = amount{acommodity="£", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}-per n = amount{acommodity="%", aquantity=n,           astyle=amountstyle{asprecision=Precision 1, ascommodityside=R, ascommodityspaced=True}}+-- XXX these are a bit clashy+num n = nullamt{acommodity="",  aquantity=n}+hrs n = nullamt{acommodity="h", aquantity=n,           astyle=amountstyle{asprecision=Precision 2, ascommodityside=R}}+usd n = nullamt{acommodity="$", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}+eur n = nullamt{acommodity="€", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}+gbp n = nullamt{acommodity="£", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}+per n = nullamt{acommodity="%", aquantity=n,           astyle=amountstyle{asprecision=Precision 1, ascommodityside=R, ascommodityspaced=True}} amt `at` priceamt = amt{aprice=Just $ UnitPrice priceamt} amt @@ priceamt = amt{aprice=Just $ TotalPrice priceamt} @@ -286,7 +285,7 @@ similarAmountsOp op Amount{acommodity=_,  aquantity=q1, astyle=AmountStyle{asprecision=p1}}                     Amount{acommodity=c2, aquantity=q2, astyle=s2@AmountStyle{asprecision=p2}} =    -- trace ("a1:"++showAmountDebug a1) $ trace ("a2:"++showAmountDebug a2) $ traceWith (("= :"++).showAmountDebug)-   amount{acommodity=c2, aquantity=q1 `op` q2, astyle=s2{asprecision=max p1 p2}}+   nullamt{acommodity=c2, aquantity=q1 `op` q2, astyle=s2{asprecision=max p1 p2}}   --  c1==c2 || q1==0 || q2==0 =   --  otherwise = error "tried to do simple arithmetic with amounts in different commodities" @@ -314,8 +313,8 @@ transformAmount :: (Quantity -> Quantity) -> Amount -> Amount transformAmount f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p}   where-    f' (TotalPrice a@Amount{aquantity=pq}) = TotalPrice a{aquantity = f pq}-    f' p = p+    f' (TotalPrice a1@Amount{aquantity=pq}) = TotalPrice a1{aquantity = f pq}+    f' p' = p'  -- | Divide an amount's quantity (and its total price, if it has one) by a constant. divideAmount :: Quantity -> Amount -> Amount@@ -522,15 +521,15 @@ applyDigitGroupStyle :: Maybe DigitGroupStyle -> Int -> T.Text -> WideBuilder applyDigitGroupStyle Nothing                       l s = WideBuilder (TB.fromText s) l applyDigitGroupStyle (Just (DigitGroups _ []))     l s = WideBuilder (TB.fromText s) l-applyDigitGroupStyle (Just (DigitGroups c (g:gs))) l s = addseps (g:|gs) (toInteger l) s+applyDigitGroupStyle (Just (DigitGroups c (g0:gs0))) l0 s0 = addseps (g0:|gs0) (toInteger l0) s0   where-    addseps (g:|gs) l s-        | l' > 0    = addseps gs' l' rest <> WideBuilder (TB.singleton c <> TB.fromText part) (fromIntegral g + 1)-        | otherwise = WideBuilder (TB.fromText s) (fromInteger l)+    addseps (g1:|gs1) l1 s1+        | l2 > 0    = addseps gs2 l2 rest <> WideBuilder (TB.singleton c <> TB.fromText part) (fromIntegral g1 + 1)+        | otherwise = WideBuilder (TB.fromText s1) (fromInteger l1)       where-        (rest, part) = T.splitAt (fromInteger l') s-        gs' = fromMaybe (g:|[]) $ nonEmpty gs-        l' = l - toInteger g+        (rest, part) = T.splitAt (fromInteger l2) s1+        gs2 = fromMaybe (g1:|[]) $ nonEmpty gs1+        l2 = l1 - toInteger g1  -- like journalCanonicaliseAmounts -- | Canonicalise an amount's display style using the provided commodity style map.@@ -702,11 +701,11 @@ unifyMixedAmount :: MixedAmount -> Maybe Amount unifyMixedAmount = foldM combine 0 . amounts   where-    combine amount result-      | amountIsZero amount                    = Just result-      | amountIsZero result                    = Just amount-      | acommodity amount == acommodity result = Just $ amount + result-      | otherwise                              = Nothing+    combine amt result+      | amountIsZero amt                    = Just result+      | amountIsZero result                 = Just amt+      | acommodity amt == acommodity result = Just $ amt + result+      | otherwise                           = Nothing  -- | Sum same-commodity amounts in a lossy way, applying the first -- price to the result and discarding any other prices. Only used as a@@ -839,10 +838,10 @@ showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder showMixedAmountB opts ma     | displayOneLine opts = showMixedAmountOneLineB opts ma-    | otherwise           = WideBuilder (wbBuilder . mconcat $ intersperse sep lines) width+    | otherwise           = WideBuilder (wbBuilder . mconcat $ intersperse sep ls) width   where-    lines = showMixedAmountLinesB opts ma-    width = headDef 0 $ map wbWidth lines+    ls = showMixedAmountLinesB opts ma+    width = headDef 0 $ map wbWidth ls     sep = WideBuilder (TB.singleton '\n') 0  -- | Helper for showMixedAmountB to show a list of Amounts on multiple lines. This returns@@ -900,7 +899,7 @@     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 (wbWidth sep) num amt)) [n-1,n-2..0]+    withElided = zipWith (\n2 amt -> (amt, elisionDisplay Nothing (wbWidth sep) n2 amt)) [n-1,n-2..0]  orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount] orderedAmounts dopts = maybe id (mapM pad) (displayOrder dopts) . amounts@@ -981,7 +980,7 @@        amountCost (eur (-1)){aprice=Just $ TotalPrice $ usd (-2)} @?= usd (-2)      ,testCase "amountLooksZero" $ do-       assertBool "" $ amountLooksZero amount+       assertBool "" $ amountLooksZero nullamt        assertBool "" $ amountLooksZero $ usd 0      ,testCase "negating amounts" $ do
Hledger/Data/Balancing.hs view
@@ -18,7 +18,6 @@ , isTransactionBalanced , balanceTransaction , balanceTransactionHelper-, annotateErrorWithTransaction   -- * journal balancing , journalBalanceTransactions , journalCheckBalanceAssertions@@ -36,7 +35,7 @@ import Data.Function ((&)) import qualified Data.HashTable.Class as H (toList) import qualified Data.HashTable.ST.Cuckoo as H-import Data.List (intercalate, partition, sortOn)+import Data.List (partition, sortOn) import Data.List.Extra (nubSort) import Data.Maybe (fromJust, fromMaybe, isJust, isNothing, mapMaybe) import qualified Data.Set as S@@ -50,10 +49,10 @@ import Hledger.Data.Types import Hledger.Data.AccountName (isAccountNamePrefixOf) import Hledger.Data.Amount-import Hledger.Data.Dates (showDate) import Hledger.Data.Journal import Hledger.Data.Posting import Hledger.Data.Transaction+import Hledger.Data.Errors   data BalancingOpts = BalancingOpts@@ -98,16 +97,18 @@      -- check for mixed signs, detecting nonzeros at display precision     canonicalise = maybe id canonicaliseMixedAmount commodity_styles_+    postingBalancingAmount p+      | "_price-matched" `elem` map fst (ptags p) = mixedAmountStripPrices $ pamount p+      | otherwise                                 = mixedAmountCost $ pamount p     signsOk ps =-      case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of+      case filter (not.mixedAmountLooksZero) $ map (canonicalise.postingBalancingAmount) ps of         nonzeros | length nonzeros >= 2                    -> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1         _          -> True     (rsignsok, bvsignsok)       = (signsOk rps, signsOk bvps)      -- check for zero sum, at display precision-    (rsum, bvsum)               = (sumPostings rps, sumPostings bvps)-    (rsumcost, bvsumcost)       = (mixedAmountCost rsum, mixedAmountCost bvsum)+    (rsumcost, bvsumcost)       = (foldMap postingBalancingAmount rps, foldMap postingBalancingAmount bvps)     (rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost)     (rsumok, bvsumok)           = (mixedAmountLooksZero rsumdisplay, mixedAmountLooksZero bvsumdisplay) @@ -116,12 +117,12 @@       where         rmsg           | rsumok        = ""-          | not rsignsok  = "real postings all have the same sign"-          | otherwise     = "real postings' sum should be 0 but is: " ++ showMixedAmount rsumcost+          | not rsignsok  = "The real postings all have the same sign. Consider negating some of them."+          | otherwise     = "The real postings' sum should be 0 but is: " ++ showMixedAmountOneLine rsumcost         bvmsg           | bvsumok       = ""-          | not bvsignsok = "balanced virtual postings all have the same sign"-          | otherwise     = "balanced virtual postings' sum should be 0 but is: " ++ showMixedAmount bvsumcost+          | not bvsignsok = "The balanced virtual postings all have the same sign. Consider negating some of them."+          | otherwise     = "The balanced virtual postings' sum should be 0 but is: " ++ showMixedAmountOneLine bvsumcost  -- | Legacy form of transactionCheckBalanced. isTransactionBalanced :: BalancingOpts -> Transaction -> Bool@@ -158,20 +159,42 @@     if infer_transaction_prices_ bopts then inferBalancingPrices t else t   case transactionCheckBalanced bopts t' of     []   -> Right (txnTieKnot t', inferredamtsandaccts)-    errs -> Left $ transactionBalanceError t' errs+    errs -> Left $ transactionBalanceError t' errs'+      where+        ismulticommodity = (length $ transactionCommodities t') > 1+        errs' =+          [ "Automatic commodity conversion is not enabled."+          | ismulticommodity && not (infer_transaction_prices_ bopts)+          ] +++          errs +++          if ismulticommodity+          then+          [ "Consider adjusting this entry's amounts, adding missing postings,"+          , "or recording conversion price(s) with @, @@ or equity postings." +          ]+          else+          [ "Consider adjusting this entry's amounts, or adding missing postings."+          ] +transactionCommodities :: Transaction -> S.Set CommoditySymbol+transactionCommodities t = mconcat $ map (maCommodities . pamount) $ tpostings t+ -- | Generate a transaction balancing error message, given the transaction -- and one or more suberror messages. transactionBalanceError :: Transaction -> [String] -> String-transactionBalanceError t errs =-  annotateErrorWithTransaction t $-  intercalate "\n" $ "could not balance this transaction:" : errs--annotateErrorWithTransaction :: Transaction -> String -> String-annotateErrorWithTransaction t s =-  unlines [ sourcePosPairPretty $ tsourcepos t, s-          , T.unpack . T.stripEnd $ showTransaction t-          ]+transactionBalanceError t errs = printf "%s:\n%s\n\nThis %stransaction is unbalanced.\n%s"+  (sourcePosPairPretty $ tsourcepos t)+  (textChomp ex)+  (if ismulticommodity then "multi-commodity " else "" :: String)+  (chomp $ unlines errs)+  where+    ismulticommodity = (length $ transactionCommodities t) > 1+    (_f,_l,_mcols,ex) = makeTransactionErrorExcerpt t finderrcols+      where+        finderrcols _ = Nothing+        -- finderrcols t = Just (1, Just w)+        --   where+        --     w = maximumDef 1 $ map T.length $ T.lines $ showTransaction t  -- | Infer up to one missing amount for this transactions's real postings, and -- likewise for its balanced virtual postings, if needed; or return an error@@ -188,12 +211,12 @@ inferBalancingAmount styles t@Transaction{tpostings=ps}   | length amountlessrealps > 1       = Left $ transactionBalanceError t-        ["can't have more than one real posting with no amount"-        ,"(remember to put two or more spaces between account and amount)"]+        ["There can't be more than one real posting with no amount."+        ,"(Remember to put two or more spaces between account and amount.)"]   | length amountlessbvps > 1       = Left $ transactionBalanceError t-        ["can't have more than one balanced virtual posting with no amount"-        ,"(remember to put two or more spaces between account and amount)"]+        ["There can't be more than one balanced virtual posting with no amount."+        ,"(Remember to put two or more spaces between account and amount.)"]   | otherwise       = let psandinferredamts = map inferamount ps             inferredacctsandamts = [(paccount p, amt) | (p, Just amt) <- psandinferredamts]@@ -292,11 +315,11 @@     -- For each posting, if the posting type matches, there is only a single amount in the posting,     -- and the commodity of the amount matches the amount we're converting from,     -- then set its price based on the ratio between fromamount and toamount.-    inferprice (fromamount, toamount) posting-        | [a] <- amounts (pamount posting), ptype posting == pt, acommodity a == acommodity fromamount-            = posting{ pamount   = mixedAmount a{aprice=Just conversionprice}-                     , poriginal = Just $ originalPosting posting }-        | otherwise = posting+    inferprice (fromamount, toamount) p+        | [a] <- amounts (pamount p), ptype p == pt, acommodity a == acommodity fromamount+            = p{ pamount   = mixedAmount a{aprice=Just conversionprice}+                     , poriginal = Just $ originalPosting p }+        | otherwise = p       where         -- If only one Amount in the posting list matches fromamount we can use TotalPrice.         -- Otherwise divide the conversion equally among the Amounts by using a unit price.@@ -550,6 +573,7 @@ checkBalanceAssertionOneCommodityB :: Posting -> Amount -> MixedAmount -> Balancing s () checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt actualbal = do   let isinclusive = maybe False bainclusive $ pbalanceassertion p+  let istotal     = maybe False batotal     $ pbalanceassertion p   actualbal' <-     if isinclusive     then@@ -571,38 +595,40 @@       aquantity         -- traceWith (("actual:"++).showAmountDebug)         actualbalincomm--    errmsg = printf (unlines-                  [ "balance assertion: %s",-                    "\nassertion details:",-                    "date:       %s",-                    "account:    %s%s",-                    "commodity:  %s",-                    -- "display precision:  %d",-                    "calculated: %s", -- (at display precision: %s)",-                    "asserted:   %s", -- (at display precision: %s)",-                    "difference: %s"-                  ])-      (case ptransaction p of-         Nothing -> "?" -- shouldn't happen-         Just t ->  printf "%s\ntransaction:\n%s"-                      (sourcePosPretty pos)-                      (textChomp $ showTransaction t)-                      :: String-                      where-                        pos = baposition $ fromJust $ pbalanceassertion p-      )-      (showDate $ postingDate p)-      (T.unpack $ paccount p) -- XXX pack-      (if isinclusive then " (and subs)" else "" :: String)-      assertedcomm+    errmsg = chomp $ printf (unlines+      [ "%s:",+        "%s\n",+        "This balance assertion failed.",+        -- "date:       %s",+        "In account:    %s",+        "and commodity: %s",+        -- "display precision:  %d",+        "this balance was asserted:     %s", -- (at display precision: %s)",+        "but the calculated balance is: %s", -- (at display precision: %s)",+        "a difference of:               %s",+        "",+        "Consider viewing this account's calculated balances to troubleshoot. Eg:",+        "",+        "hledger reg '%s'%s -I  # -f FILE"+      ])+      (sourcePosPretty pos)+      (textChomp ex)+      -- (showDate $ postingDate p)+      (if isinclusive then printf "%-30s  (including subaccounts)" acct else acct)+      (if istotal     then printf "%-30s  (no other commodities allowed)" (T.unpack assertedcomm) else (T.unpack assertedcomm))       -- (asprecision $ astyle actualbalincommodity)  -- should be the standard display precision I think-      (show $ aquantity actualbalincomm)-      -- (showAmount actualbalincommodity)       (show $ aquantity assertedamt)       -- (showAmount assertedamt)+      (show $ aquantity actualbalincomm)+      -- (showAmount actualbalincommodity)       (show $ aquantity assertedamt - aquantity actualbalincomm)-+      (acct ++ if isinclusive then "" else "$")+      (if istotal then "" else (" cur:" ++ quoteForCommandLine (T.unpack assertedcomm)))+      where+        acct = T.unpack $ paccount p+        ass = fromJust $ pbalanceassertion p  -- PARTIAL: fromJust won't fail, there is a balance assertion+        pos = baposition ass+        (_,_,_,ex) = makeBalanceAssertionErrorExcerpt p   unless pass $ throwError errmsg  -- | Throw an error if this posting is trying to do an illegal balance assignment.@@ -619,7 +645,7 @@ checkBalanceAssignmentPostingDateB p =   when (hasBalanceAssignment p && isJust (pdate p)) $     throwError $ chomp $ unlines [-       "can't use balance assignment with custom posting date"+       "Balance assignments and custom posting dates may not be combined."       ,""       ,chomp1 $ T.unpack $ maybe (T.unlines $ showPostingLines p) showTransaction $ ptransaction p       ,"Balance assignments may not be used on postings with a custom posting date"@@ -635,7 +661,7 @@   unassignable <- R.asks bsUnassignable   when (hasBalanceAssignment p && paccount p `S.member` unassignable) $     throwError $ chomp $ unlines [-       "can't use balance assignment with auto postings"+       "Balance assignments and auto postings may not be combined."       ,""       ,chomp1 $ T.unpack $ maybe (T.unlines $ showPostingLines p) (showTransaction) $ ptransaction p       ,"Balance assignments may not be used on accounts affected by auto posting rules"
Hledger/Data/Dates.hs view
@@ -2,7 +2,6 @@ {-# LANGUAGE NoMonoLocalBinds    #-} {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections       #-} {-# LANGUAGE TypeFamilies        #-} {-| @@ -76,7 +75,7 @@   daysInSpan,    tests_Dates-)+, intervalStartBefore) where  import qualified Control.Monad.Fail as Fail (MonadFail, fail)@@ -105,7 +104,7 @@ import Text.Megaparsec import Text.Megaparsec.Char (char, char', digitChar, string, string') import Text.Megaparsec.Char.Lexer (decimal, signed)-import Text.Megaparsec.Custom (customErrorBundlePretty, HledgerParseErrors)+import Text.Megaparsec.Custom (customErrorBundlePretty) import Text.Printf (printf)  import Hledger.Data.Types@@ -348,9 +347,9 @@ latestSpanContaining datespans = go   where     go day = do-        span <- Set.lookupLT supSpan spanSet-        guard $ spanContainsDate span day-        return span+        spn <- Set.lookupLT supSpan spanSet+        guard $ spanContainsDate spn day+        return spn       where         -- The smallest DateSpan larger than any DateSpan containing day.         supSpan = DateSpan (Just $ addDays 1 day) Nothing@@ -387,18 +386,19 @@ spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e)     where       (ry,rm,_) = toGregorian refdate-      (b,e) = span sdate-      span :: SmartDate -> (Day,Day)-      span (SmartCompleteDate day)       = (day, nextday day)-      span (SmartAssumeStart y Nothing)  = (startofyear day, nextyear day) where day = fromGregorian y 1 1-      span (SmartAssumeStart y (Just m)) = (startofmonth day, nextmonth day) where day = fromGregorian y m 1-      span (SmartFromReference m d)      = (day, nextday day) where day = fromGregorian ry (fromMaybe rm m) d-      span (SmartMonth m)                = (startofmonth day, nextmonth day) where day = fromGregorian ry m 1-      span (SmartRelative n Day)         = (addDays n refdate, addDays (n+1) refdate)-      span (SmartRelative n Week)        = (addDays (7*n) d, addDays (7*n+7) d) where d = thisweek refdate-      span (SmartRelative n Month)       = (addGregorianMonthsClip n d, addGregorianMonthsClip (n+1) d) where d = thismonth refdate-      span (SmartRelative n Quarter)     = (addGregorianMonthsClip (3*n) d, addGregorianMonthsClip (3*n+3) d) where d = thisquarter refdate-      span (SmartRelative n Year)        = (addGregorianYearsClip n d, addGregorianYearsClip (n+1) d) where d = thisyear refdate+      (b,e) = span' sdate+        where+          span' :: SmartDate -> (Day,Day)+          span' (SmartCompleteDate day)       = (day, nextday day)+          span' (SmartAssumeStart y Nothing)  = (startofyear day, nextyear day) where day = fromGregorian y 1 1+          span' (SmartAssumeStart y (Just m)) = (startofmonth day, nextmonth day) where day = fromGregorian y m 1+          span' (SmartFromReference m d)      = (day, nextday day) where day = fromGregorian ry (fromMaybe rm m) d+          span' (SmartMonth m)                = (startofmonth day, nextmonth day) where day = fromGregorian ry m 1+          span' (SmartRelative n Day)         = (addDays n refdate, addDays (n+1) refdate)+          span' (SmartRelative n Week)        = (addDays (7*n) d, addDays (7*n+7) d) where d = thisweek refdate+          span' (SmartRelative n Month)       = (addGregorianMonthsClip n d, addGregorianMonthsClip (n+1) d) where d = thismonth refdate+          span' (SmartRelative n Quarter)     = (addGregorianMonthsClip (3*n) d, addGregorianMonthsClip (3*n+3) d) where d = thisquarter refdate+          span' (SmartRelative n Year)        = (addGregorianYearsClip n d, addGregorianYearsClip (n+1) d) where d = thisyear refdate  -- showDay :: Day -> String -- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day@@ -541,13 +541,20 @@ startofquarter day = fromGregorian y (firstmonthofquarter m) 1     where       (y,m,_) = toGregorian day-      firstmonthofquarter m = ((m-1) `div` 3) * 3 + 1+      firstmonthofquarter m2 = ((m2-1) `div` 3) * 3 + 1  thisyear = startofyear prevyear = startofyear . addGregorianYearsClip (-1) nextyear = startofyear . addGregorianYearsClip 1 startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day +-- Get the natural start for the given interval that falls on or before the given day.+intervalStartBefore :: Interval -> Day -> Day+intervalStartBefore int d =+  case splitSpan int (DateSpan (Just d) (Just $ addDays 1 d)) of+    (DateSpan (Just start) _:_) -> start+    _ -> d+ -- | For given date d find year-long interval that starts on given -- MM/DD of year and covers it. -- The given MM and DD should be basically valid (1-12 & 1-31),@@ -570,14 +577,14 @@ -- >>> nthdayofyearcontaining 1 1 wed22nd -- 2017-01-01 nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day-nthdayofyearcontaining m md date+nthdayofyearcontaining m mdy date   -- PARTIAL:   | not (validMonth m)  = error' $ "nthdayofyearcontaining: invalid month "++show m-  | not (validDay   md) = error' $ "nthdayofyearcontaining: invalid day "  ++show md+  | not (validDay   mdy) = error' $ "nthdayofyearcontaining: invalid day "  ++show mdy   | mmddOfSameYear <= date = mmddOfSameYear   | otherwise = mmddOfPrevYear-  where mmddOfSameYear = addDays (toInteger md-1) $ applyN (m-1) nextmonth s-        mmddOfPrevYear = addDays (toInteger md-1) $ applyN (m-1) nextmonth $ prevyear s+  where mmddOfSameYear = addDays (toInteger mdy-1) $ applyN (m-1) nextmonth s+        mmddOfPrevYear = addDays (toInteger mdy-1) $ applyN (m-1) nextmonth $ prevyear s         s = startofyear date  -- | For given date d find month-long interval that starts on nth day of month@@ -599,13 +606,13 @@ -- >>> nthdayofmonthcontaining 30 wed22nd -- 2017-10-30 nthdayofmonthcontaining :: MonthDay -> Day -> Day-nthdayofmonthcontaining md date+nthdayofmonthcontaining mdy date   -- PARTIAL:-  | not (validDay md) = error' $ "nthdayofmonthcontaining: invalid day "  ++show md+  | not (validDay mdy) = error' $ "nthdayofmonthcontaining: invalid day "  ++show mdy   | nthOfSameMonth <= date = nthOfSameMonth   | otherwise = nthOfPrevMonth-  where nthOfSameMonth = nthdayofmonth md s-        nthOfPrevMonth = nthdayofmonth md $ prevmonth s+  where nthOfSameMonth = nthdayofmonth mdy s+        nthOfPrevMonth = nthdayofmonth mdy $ prevmonth s         s = startofmonth date  -- | For given date d find week-long interval that starts on nth day of week@@ -800,8 +807,8 @@ yyyymmdd = do   y <- read <$> count 4 digitChar   m <- read <$> count 2 digitChar-  md <- optional $ read <$> count 2 digitChar-  case md of+  mdy <- optional $ read <$> count 2 digitChar+  case mdy of       Nothing -> failIfInvalidDate $ SmartAssumeStart y (Just m)       Just d  -> maybe (Fail.fail $ showBadDate y m d) (return . SmartCompleteDate) $                    fromGregorianValid y m d@@ -1073,19 +1080,19 @@             ]    , testCase "match dayOfWeek" $ do-      let dayofweek n s = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1 s-          match ds day = splitSpan (DaysOfWeek [day]) ds @?= dayofweek day ds+      let dayofweek n = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1+          matchdow ds day = splitSpan (DaysOfWeek [day]) ds @?= dayofweek day ds           ys2021 = fromGregorian 2021 01 01           ye2021 = fromGregorian 2021 12 31           ys2022 = fromGregorian 2022 01 01-      mapM_ (match (DateSpan (Just ys2021) (Just ye2021))) [1..7]-      mapM_ (match (DateSpan (Just ys2021) (Just ys2022))) [1..7]-      mapM_ (match (DateSpan (Just ye2021) (Just ys2022))) [1..7]+      mapM_ (matchdow (DateSpan (Just ys2021) (Just ye2021))) [1..7]+      mapM_ (matchdow (DateSpan (Just ys2021) (Just ys2022))) [1..7]+      mapM_ (matchdow (DateSpan (Just ye2021) (Just ys2022))) [1..7] -      mapM_ (match (DateSpan (Just ye2021) Nothing)) [1..7]-      mapM_ (match (DateSpan (Just ys2022) Nothing)) [1..7]+      mapM_ (matchdow (DateSpan (Just ye2021) Nothing)) [1..7]+      mapM_ (matchdow (DateSpan (Just ys2022) Nothing)) [1..7] -      mapM_ (match (DateSpan Nothing (Just ye2021))) [1..7]-      mapM_ (match (DateSpan Nothing (Just ys2022))) [1..7]+      mapM_ (matchdow (DateSpan Nothing (Just ye2021))) [1..7]+      mapM_ (matchdow (DateSpan Nothing (Just ys2022))) [1..7]    ]
Hledger/Data/Errors.hs view
@@ -7,6 +7,8 @@ module Hledger.Data.Errors (   makeTransactionErrorExcerpt,   makePostingErrorExcerpt,+  makePostingAccountErrorExcerpt,+  makeBalanceAssertionErrorExcerpt,   transactionFindPostingIndex, ) where@@ -19,6 +21,9 @@ import Hledger.Data.Transaction (showTransaction) import Hledger.Data.Types import Hledger.Utils+import Data.Maybe+import Safe (headMay)+import Hledger.Data.Posting (isVirtual)  -- | Given a problem transaction and a function calculating the best -- column(s) for marking the error region:@@ -26,6 +31,7 @@ -- on the transaction line, and a column(s) marker. -- Returns the file path, line number, column(s) if known, -- and the rendered excerpt, or as much of these as is possible.+-- A limitation: columns will be accurate for the rendered error message but not for the original journal data. makeTransactionErrorExcerpt :: Transaction -> (Transaction -> Maybe (Int, Maybe Int)) -> (FilePath, Int, Maybe (Int, Maybe Int), Text) makeTransactionErrorExcerpt t findtxnerrorcolumns = (f, tl, merrcols, ex)   -- XXX findtxnerrorcolumns is awkward, I don't think this is the final form@@ -58,6 +64,7 @@ -- on the problem posting's line, and a column indicator. -- Returns the file path, line number, column(s) if known, -- and the rendered excerpt, or as much of these as is possible.+-- A limitation: columns will be accurate for the rendered error message but not for the original journal data. makePostingErrorExcerpt :: Posting -> (Posting -> Transaction -> Text -> Maybe (Int, Maybe Int)) -> (FilePath, Int, Maybe (Int, Maybe Int), Text) makePostingErrorExcerpt p findpostingerrorcolumns =   case ptransaction p of@@ -97,4 +104,41 @@ transactionFindPostingIndex :: (Posting -> Bool) -> Transaction -> Maybe Int transactionFindPostingIndex ppredicate =    fmap fst . find (ppredicate.snd) . zip [1..] . tpostings++-- | From the given posting, make an error excerpt showing the transaction with+-- this posting's account part highlighted.+makePostingAccountErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe Int), Text)+makePostingAccountErrorExcerpt p = makePostingErrorExcerpt p finderrcols+  where+    -- Calculate columns suitable for highlighting the synthetic excerpt.+    finderrcols p' _ _ = Just (col, Just col2)+      where+        col = 5 + if isVirtual p' then 1 else 0+        col2 = col + T.length (paccount p') - 1++-- | From the given posting, make an error excerpt showing the transaction with+-- the balance assertion highlighted.+makeBalanceAssertionErrorExcerpt :: Posting -> (FilePath, Int, Maybe (Int, Maybe Int), Text)+makeBalanceAssertionErrorExcerpt p = makePostingErrorExcerpt p finderrcols+  where+    finderrcols p' t trendered = Just (col, Just col2)+      where+        -- Analyse the rendering to find the columns to highlight.+        tlines = dbg5 "tlines" $ max 1 $ length $ T.lines $ tcomment t  -- transaction comment can generate extra lines+        (col, col2) =+          let def = (5, maximum (map T.length $ T.lines trendered))  -- fallback: underline whole posting. Shouldn't happen.+          in+            case transactionFindPostingIndex (==p') t of+              Nothing  -> def+              Just idx -> fromMaybe def $ do+                let+                  beforeps = take (idx-1) $ tpostings t+                  beforepslines = dbg5 "beforepslines" $ sum $ map (max 1 . length . T.lines . pcomment) beforeps   -- posting comment can generate extra lines (assume only one commodity shown)+                assertionline <- dbg5 "assertionline" $ headMay $ drop (tlines + beforepslines) $ T.lines trendered+                let+                  col2' = T.length assertionline+                  l = dropWhile (/= '=') $ reverse $ T.unpack assertionline+                  l' = dropWhile (`elem` ['=','*']) l+                  col' = length l' + 1+                return (col', col2') 
Hledger/Data/Journal.hs view
@@ -26,8 +26,10 @@   journalCommodityStyles,   journalToCost,   journalAddInferredEquityPostings,+  journalAddPricesFromEquity,   journalReverse,   journalSetLastReadTime,+  journalRenumberAccountDeclarations,   journalPivot,   -- * Filtering   filterJournalTransactions,@@ -36,6 +38,7 @@   filterJournalAmounts,   filterTransactionAmounts,   filterTransactionPostings,+  filterTransactionPostingsExtra,   filterTransactionRelatedPostings,   filterPostingAmount,   -- * Mapping@@ -83,24 +86,17 @@   journalAddAccountTypes,   journalPostingsAddAccountTags,   -- journalPrices,-  -- * Standard account types-  journalBalanceSheetAccountQuery,-  journalProfitAndLossAccountQuery,-  journalRevenueAccountQuery,-  journalExpenseAccountQuery,-  journalAssetAccountQuery,-  journalLiabilityAccountQuery,-  journalEquityAccountQuery,-  journalCashAccountQuery,   journalConversionAccount,   -- * Misc   canonicalStyleFrom,   nulljournal,+  journalConcat,   journalNumberTransactions,   journalNumberAndTieTransactions,   journalUntieTransactions,   journalModifyTransactions,   journalApplyAliases,+  dbgJournalAcctDeclOrder,   -- * Tests   samplejournal,   samplejournalMaybeExplicit,@@ -115,7 +111,7 @@ import Data.Char (toUpper, isDigit) import Data.Default (Default(..)) import Data.Foldable (toList)-import Data.List ((\\), find, foldl', sortBy, union)+import Data.List ((\\), find, foldl', sortBy, union, intercalate) import Data.List.Extra (nubSort) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList)@@ -139,6 +135,7 @@ import Hledger.Data.TransactionModifier import Hledger.Data.Valuation import Hledger.Query+import System.FilePath (takeFileName)   -- | A parser of text that runs in some monad, keeping a Journal as state.@@ -186,9 +183,7 @@ -- The semigroup instance for Journal is useful for two situations. -- -- 1. concatenating finalised journals, eg with multiple -f options:--- FIRST <> SECOND. The second's list fields are appended to the--- first's, map fields are combined, transaction counts are summed,--- the parse state of the second is kept.+-- FIRST <> SECOND. -- -- 2. merging a child parsed journal, eg with the include directive: -- CHILD <> PARENT. A parsed journal's data is in reverse order, so@@ -196,8 +191,22 @@ -- -- Note that (<>) is right-biased, so nulljournal is only a left identity. -- In particular, this prevents Journal from being a monoid.-instance Semigroup Journal where-  j1 <> j2 = Journal {+instance Semigroup Journal where j1 <> j2 = j1 `journalConcat` j2++-- | Merge two journals into one.+-- Transaction counts are summed, map fields are combined,+-- the second's list fields are appended to the first's,+-- the second's parse state is kept.+journalConcat :: Journal -> Journal -> Journal+journalConcat j1 j2 =+  let+    f1 = takeFileName $ journalFilePath j1+    f2 = maybe "(unknown)" takeFileName $ headMay $ jincludefilestack j2  -- XXX more accurate than journalFilePath for some reason+  in+    dbgJournalAcctDeclOrder ("journalConcat: " <> f1 <> " <> " <> f2 <> ", acct decls renumbered: ") $+    journalRenumberAccountDeclarations $+    dbgJournalAcctDeclOrder ("journalConcat: " <> f1 <> " <> " <> f2 <> ", acct decls           : ") $+    Journal {      jparsedefaultyear          = jparsedefaultyear          j2     ,jparsedefaultcommodity     = jparsedefaultcommodity     j2     ,jparsedecimalmark          = jparsedecimalmark          j2@@ -224,6 +233,34 @@     ,jlastreadtime              = max (jlastreadtime j1) (jlastreadtime j2)     } +-- | Renumber all the account declarations. This is useful to call when+-- finalising or concatenating Journals, to give account declarations+-- a total order across files.+journalRenumberAccountDeclarations :: Journal -> Journal+journalRenumberAccountDeclarations j = j{jdeclaredaccounts=jdas'}+  where+    jdas' = [(a, adi{adideclarationorder=n}) | (n, (a,adi)) <- zip [1..] $ jdeclaredaccounts j]+    -- the per-file declaration order saved during parsing is discarded,+    -- it seems unneeded except perhaps for debugging++-- | Debug log the ordering of a journal's account declarations+-- (at debug level 5+).+dbgJournalAcctDeclOrder :: String -> Journal -> Journal+dbgJournalAcctDeclOrder prefix+  | debugLevel >= 5 = traceWith ((prefix++) . showAcctDeclsSummary . jdeclaredaccounts)+  | otherwise       = id+  where+    showAcctDeclsSummary :: [(AccountName,AccountDeclarationInfo)] -> String+    showAcctDeclsSummary adis+      | length adis < (2*n+2) = "[" <> showadis adis <> "]"+      | otherwise =+          "[" <> showadis (take n adis) <> " ... " <> showadis (takelast n adis) <> "]"+      where+        n = 3+        showadis = intercalate ", " . map showadi+        showadi (a,adi) = "("<>show (adideclarationorder adi)<>","<>T.unpack a<>")"+        takelast n' = reverse . take n' . reverse+ instance Default Journal where   def = nulljournal @@ -262,7 +299,7 @@ journalFilePaths = map fst . jfiles  mainfile :: Journal -> (FilePath, Text)-mainfile = headDef ("", "") . jfiles+mainfile = headDef ("(unknown)", "") . jfiles  addTransaction :: Transaction -> Journal -> Journal addTransaction t j = j { jtxns = t : jtxns j }@@ -368,7 +405,7 @@ -- | Which tags are in effect for this account, including tags inherited from parent accounts ? journalInheritedAccountTags :: Journal -> AccountName -> [Tag] journalInheritedAccountTags j a =-  foldl' (\ts a -> ts `union` journalAccountTags j a) [] as+  foldl' (\ts a' -> ts `union` journalAccountTags j a') [] as   where     as = a : parentAccountNames a -- PERF: cache in journal ?@@ -428,111 +465,6 @@ letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest) letterPairs _ = [] --- Older account type code---- queries for standard account types---- | Get a query for accounts of the specified types in this journal. --- Account types include:--- Asset, Liability, Equity, Revenue, Expense, Cash, Conversion.--- For each type, if no accounts were declared with this type, the query --- will instead match accounts with names matched by the case-insensitive --- regular expression provided as a fallback.--- The query will match all accounts which were declared as one of--- these types (by account directives with the type: tag), plus all their --- subaccounts which have not been declared as some other type.------ This is older code pre-dating 2022's expansion of account types.-journalAccountTypeQuery :: [AccountType] -> Regexp -> Journal -> Query-journalAccountTypeQuery atypes fallbackregex Journal{jdeclaredaccounttypes} =-  let-    declaredacctsoftype :: [AccountName] =-      concat $ mapMaybe (`M.lookup` jdeclaredaccounttypes) atypes-  in case declaredacctsoftype of-    [] -> Acct fallbackregex-    as -> And $ Or acctnameRegexes : if null differentlyTypedRegexes then [] else [ Not $ Or differentlyTypedRegexes ]-      where-        -- XXX Query isn't able to match account type since that requires extra info from the journal.-        -- So we do a hacky search by name instead.-        acctnameRegexes = map (Acct . accountNameToAccountRegex) as-        differentlyTypedRegexes = map (Acct . accountNameToAccountRegex) differentlytypedsubs--        differentlytypedsubs = concat-          [subs | (t,bs) <- M.toList jdeclaredaccounttypes-              , t `notElem` atypes-              , let subs = [b | b <- bs, any (`isAccountNamePrefixOf` b) as]-          ]---- | A query for accounts in this journal which have been--- declared as Asset (or Cash, a subtype of Asset) by account directives, --- or otherwise for accounts with names matched by the case-insensitive --- regular expression @^assets?(:|$)@.-journalAssetAccountQuery :: Journal -> Query-journalAssetAccountQuery j =-  Or [-     journalAccountTypeQuery [Asset] assetAccountRegex j-    ,journalCashAccountOnlyQuery j-  ]---- | A query for Cash (liquid asset) accounts in this journal, ie accounts--- declared as Cash by account directives, or otherwise accounts whose--- names match the case-insensitive regular expression--- @(^assets:(.+:)?(cash|bank)(:|$)@.-journalCashAccountQuery :: Journal -> Query-journalCashAccountQuery = journalAccountTypeQuery [Cash] cashAccountRegex---- | A query for accounts in this journal specifically declared as Cash by --- account directives, or otherwise the None query.-journalCashAccountOnlyQuery :: Journal -> Query-journalCashAccountOnlyQuery j-  -- Cash accounts are declared; get a query for them (the fallback regex won't be used)-  | Cash `M.member` jdeclaredaccounttypes j = journalAccountTypeQuery [Cash] notused j-  | otherwise = None-  where notused = error' "journalCashAccountOnlyQuery: this should not have happened!"  -- PARTIAL:---- | A query for accounts in this journal which have been--- declared as Liability by account directives, or otherwise for--- accounts with names matched by the case-insensitive regular expression--- @^(debts?|liabilit(y|ies))(:|$)@.-journalLiabilityAccountQuery :: Journal -> Query-journalLiabilityAccountQuery = journalAccountTypeQuery [Liability] liabilityAccountRegex---- | A query for accounts in this journal which have been--- declared as Equity by account directives, or otherwise for--- accounts with names matched by the case-insensitive regular expression--- @^equity(:|$)@.-journalEquityAccountQuery :: Journal -> Query-journalEquityAccountQuery = journalAccountTypeQuery [Equity] equityAccountRegex---- | A query for accounts in this journal which have been--- declared as Revenue by account directives, or otherwise for--- accounts with names matched by the case-insensitive regular expression--- @^(income|revenue)s?(:|$)@.-journalRevenueAccountQuery :: Journal -> Query-journalRevenueAccountQuery = journalAccountTypeQuery [Revenue] revenueAccountRegex---- | A query for accounts in this journal which have been--- declared as Expense by account directives, or otherwise for--- accounts with names matched by the case-insensitive regular expression--- @^expenses?(:|$)@.-journalExpenseAccountQuery  :: Journal -> Query-journalExpenseAccountQuery = journalAccountTypeQuery [Expense] expenseAccountRegex---- | A query for Asset, Liability & Equity accounts in this journal.--- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Balance_Sheet_Accounts>.-journalBalanceSheetAccountQuery :: Journal -> Query-journalBalanceSheetAccountQuery j = Or [journalAssetAccountQuery j-                                       ,journalLiabilityAccountQuery j-                                       ,journalEquityAccountQuery j-                                       ]---- | A query for Profit & Loss accounts in this journal.--- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Profit_.26_Loss_accounts>.-journalProfitAndLossAccountQuery  :: Journal -> Query-journalProfitAndLossAccountQuery j = Or [journalRevenueAccountQuery j-                                        ,journalExpenseAccountQuery j-                                        ]- -- | The 'AccountName' to use for automatically generated conversion postings. journalConversionAccount :: Journal -> AccountName journalConversionAccount =@@ -949,6 +881,12 @@   where     equityAcct = journalConversionAccount j +-- | Add inferred transaction prices from equity postings.+journalAddPricesFromEquity :: Journal -> Either String Journal+journalAddPricesFromEquity j = do+    ts <- mapM (transactionAddPricesFromEquity $ jaccounttypes j) $ jtxns j+    return j{jtxns=ts}+ -- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. -- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol -- journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j@@ -1287,24 +1225,4 @@               ]       }     @?= (DateSpan (Just $ fromGregorian 2014 1 10) (Just $ fromGregorian 2014 10 11))--  ,testGroup "standard account type queries" $-    let-      j = samplejournal-      journalAccountNamesMatching :: Query -> Journal -> [AccountName]-      journalAccountNamesMatching q = filter (q `matchesAccount`) . journalAccountNames-      namesfrom qfunc = journalAccountNamesMatching (qfunc j) j-    in [testCase "assets"      $ assertEqual "" ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]-         (namesfrom journalAssetAccountQuery)-       ,testCase "cash"        $ assertEqual "" ["assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]-         (namesfrom journalCashAccountQuery)-       ,testCase "liabilities" $ assertEqual "" ["liabilities","liabilities:debts"]-         (namesfrom journalLiabilityAccountQuery)-       ,testCase "equity"      $ assertEqual "" []-         (namesfrom journalEquityAccountQuery)-       ,testCase "income"      $ assertEqual "" ["income","income:gifts","income:salary"]-         (namesfrom journalRevenueAccountQuery)-       ,testCase "expenses"    $ assertEqual "" ["expenses","expenses:food","expenses:supplies"]-         (namesfrom journalExpenseAccountQuery)-       ]   ]
Hledger/Data/JournalChecks.hs view
@@ -6,32 +6,36 @@  {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RecordWildCards #-}  module Hledger.Data.JournalChecks (   journalCheckAccounts,   journalCheckCommodities,   journalCheckPayees,+  journalCheckRecentAssertions,   module Hledger.Data.JournalChecks.Ordereddates,   module Hledger.Data.JournalChecks.Uniqueleafnames, ) where  import Data.Char (isSpace)-import Data.List (find) import Data.Maybe import qualified Data.Map.Strict as M import qualified Data.Text as T-import Safe (atMay)+import Safe (atMay, lastMay) import Text.Printf (printf)  import Hledger.Data.Errors import Hledger.Data.Journal import Hledger.Data.JournalChecks.Ordereddates import Hledger.Data.JournalChecks.Uniqueleafnames-import Hledger.Data.Posting (isVirtual)+import Hledger.Data.Posting (isVirtual, postingDate, postingStatus) import Hledger.Data.Types import Hledger.Data.Amount (amountIsZero, amountsRaw, missingamt) import Hledger.Data.Transaction (transactionPayee, showTransactionLineFirstPart)+import Data.Time (Day, diffDays)+import Data.List.Extra+import Hledger.Utils (chomp, textChomp, sourcePosPretty)  -- | Check that all the journal's postings are to accounts  with -- account directives, returning an error message otherwise.@@ -40,16 +44,18 @@   where     checkacct p@Posting{paccount=a}       | a `elem` journalAccountNamesDeclared j = Right ()-      | otherwise = Left $ -        printf "%s:%d:%d-%d:\n%sundeclared account \"%s\"\n" f l col col2 ex a+      | otherwise = Left $ printf (unlines [+           "%s:%d:"+          ,"%s"+          ,"Strict account checking is enabled, and"+          ,"account %s has not been declared."+          ,"Consider adding an account directive. Examples:"+          ,""+          ,"account %s"+          ,"account %s    ; type:A  ; (L,E,R,X,C,V)"+          ]) f l ex (show a) a a         where-          (f,l,mcols,ex) = makePostingErrorExcerpt p finderrcols-          col  = maybe 0 fst mcols-          col2 = maybe 0 (fromMaybe 0 . snd) mcols-          finderrcols p _ _ = Just (col, Just col2)-            where-              col = 5 + if isVirtual p then 1 else 0-              col2 = col + T.length a - 1+          (f,l,_mcols,ex) = makePostingAccountErrorExcerpt p  -- | Check that all the commodities used in this journal's postings have been declared -- by commodity directives, returning an error message otherwise.@@ -60,11 +66,18 @@       case findundeclaredcomm p of         Nothing -> Right ()         Just (comm, _) ->-          Left $ printf "%s:%d:%d-%d:\n%sundeclared commodity \"%s\"\n" f l col col2 ex comm+          Left $ printf (unlines [+           "%s:%d:"+          ,"%s"+          ,"Strict commodity checking is enabled, and"+          ,"commodity %s has not been declared."+          ,"Consider adding a commodity directive. Examples:"+          ,""+          ,"commodity %s1000.00"+          ,"commodity 1.000,00 %s"+          ]) f l ex (show comm) comm comm           where-            (f,l,mcols,ex) = makePostingErrorExcerpt p finderrcols-            col  = maybe 0 fst mcols-            col2 = maybe 0 (fromMaybe 0 . snd) mcols+            (f,l,_mcols,ex) = makePostingErrorExcerpt p finderrcols       where         -- Find the first undeclared commodity symbol in this posting's amount         -- or balance assertion amount, if any. The boolean will be true if@@ -83,6 +96,10 @@             assertioncomms = [acommodity a | Just a <- [baamount <$> pbalanceassertion]]             findundeclared = find (`M.notMember` jcommodities j) +        -- Calculate columns suitable for highlighting the excerpt.+        -- We won't show these in the main error line as they aren't+        -- accurate for the actual data.+         -- Find the best position for an error column marker when this posting         -- is rendered by showTransaction.         -- Reliably locating a problem commodity symbol in showTransaction output@@ -99,15 +116,15 @@         --     assets      "C $" -1 @ $ 2         --                 ^^^^^^^^^^^^^^         -- XXX refine this region when it's easy-        finderrcols p t txntxt =-          case transactionFindPostingIndex (==p) t of+        finderrcols p' t txntxt =+          case transactionFindPostingIndex (==p') t of             Nothing     -> Nothing             Just pindex -> Just (amtstart, Just amtend)               where                 tcommentlines = max 0 (length (T.lines $ tcomment t) - 1)                 errrelline = 1 + tcommentlines + pindex   -- XXX doesn't count posting coment lines                 errline = fromMaybe "" (T.lines txntxt `atMay` (errrelline-1))-                acctend = 4 + T.length (paccount p) + if isVirtual p then 2 else 0+                acctend = 4 + T.length (paccount p') + if isVirtual p' then 2 else 0                 amtstart = acctend + (T.length $ T.takeWhile isSpace $ T.drop acctend errline) + 1                 amtend = amtstart + (T.length $ T.stripEnd $ T.takeWhile (/=';') $ T.drop amtstart errline) @@ -119,13 +136,126 @@     checkpayee t       | payee `elem` journalPayeesDeclared j = Right ()       | otherwise = Left $-        printf "%s:%d:%d-%d:\n%sundeclared payee \"%s\"\n" f l col col2 ex payee+        printf (unlines [+           "%s:%d:"+          ,"%s"+          ,"Strict payee checking is enabled, and"+          ,"payee %s has not been declared."+          ,"Consider adding a payee directive. Examples:"+          ,""+          ,"payee %s"+          ]) f l ex (show payee) payee       where         payee = transactionPayee t-        (f,l,mcols,ex) = makeTransactionErrorExcerpt t finderrcols-        col  = maybe 0 fst mcols-        col2 = maybe 0 (fromMaybe 0 . snd) mcols-        finderrcols t = Just (col, Just col2)+        (f,l,_mcols,ex) = makeTransactionErrorExcerpt t finderrcols+        -- Calculate columns suitable for highlighting the excerpt.+        -- We won't show these in the main error line as they aren't+        -- accurate for the actual data.+        finderrcols t' = Just (col, Just col2)           where-            col = T.length (showTransactionLineFirstPart t) + 2-            col2 = col + T.length (transactionPayee t) - 1+            col  = T.length (showTransactionLineFirstPart t') + 2+            col2 = col + T.length (transactionPayee t') - 1++----------++-- | Information useful for checking the age and lag of an account's latest balance assertion.+data BalanceAssertionInfo = BAI {+    baiAccount                :: AccountName -- ^ the account+  , baiLatestAssertionPosting :: Posting     -- ^ the account's latest posting with a balance assertion+  , baiLatestAssertionDate    :: Day         -- ^ the posting date+  , baiLatestAssertionStatus  :: Status      -- ^ the posting status+  , baiLatestPostingDate      :: Day         -- ^ the date of this account's latest posting with or without a balance assertion+}++-- | Given a list of postings to the same account,+-- if any of them contain a balance assertion,+-- calculate the last asserted and posted dates.+balanceAssertionInfo :: [Posting] -> Maybe BalanceAssertionInfo+balanceAssertionInfo ps =+  case (mlatestp, mlatestassertp) of+    (Just latestp, Just latestassertp) -> Just $+      BAI{baiAccount                 = paccount latestassertp+          ,baiLatestAssertionDate    = postingDate latestassertp+          ,baiLatestAssertionPosting = latestassertp+          ,baiLatestAssertionStatus  = postingStatus latestassertp+          ,baiLatestPostingDate      = postingDate latestp+          }+    _ -> Nothing+  where+    ps' = sortOn postingDate ps+    mlatestp = lastMay ps'+    mlatestassertp = lastMay [p | p@Posting{pbalanceassertion=Just _} <- ps']++-- | The number of days allowed between an account's latest balance assertion +-- and latest posting.+maxlag = 7++-- | The number of days between this balance assertion and the latest posting in its account.+baiLag BAI{..} = diffDays baiLatestPostingDate baiLatestAssertionDate++-- -- | The earliest balance assertion date which would satisfy the recentassertions check.+-- baiLagOkDate :: BalanceAssertionInfo -> Day+-- baiLagOkDate BAI{..} = addDays (-7) baiLatestPostingDate++-- | Check that this latest assertion is close enough to the account's latest posting.+checkRecentAssertion :: BalanceAssertionInfo -> Either (BalanceAssertionInfo, String) ()+checkRecentAssertion bai@BAI{..}+  | lag > maxlag =+    Left (bai, printf (chomp $ unlines [+       "the last balance assertion (%s) was %d days before"+      ,"the latest posting (%s)."+      ])+      (show baiLatestAssertionDate) lag (show baiLatestPostingDate)+      )+  | otherwise = Right ()+  where +    lag = baiLag bai++-- | Check that all the journal's accounts with balance assertions have+-- an assertion no more than 7 days before their latest posting.+-- Today's date is provided for error messages.+journalCheckRecentAssertions :: Day -> Journal -> Either String ()+journalCheckRecentAssertions today j =+  let+    acctps = groupOn paccount $ sortOn paccount $ journalPostings j+    acctassertioninfos = mapMaybe balanceAssertionInfo acctps+  in+    case mapM_ checkRecentAssertion acctassertioninfos of+      Right () -> Right ()+      Left (BAI{..}, msg) -> Left errmsg+        where+          errmsg = chomp $ printf +            (unlines [+              "%s:",+              "%s\n",+              "The recentassertions check is enabled, so accounts with balance assertions must",+              "have a balance assertion no more than %d days before their latest posting date.",+              "In account %s,",+              "%s",+              "",+              "%s"+              ])+            (maybe "(no position)"  -- shouldn't happen+              (sourcePosPretty . baposition) $ pbalanceassertion baiLatestAssertionPosting)+            (textChomp excerpt)+            maxlag+            baiAccount+            msg+            recommendation+            where+              (_,_,_,excerpt) = makeBalanceAssertionErrorExcerpt baiLatestAssertionPosting+              recommendation = unlines [+                "Consider adding a more recent balance assertion for this account. Eg:",+                "",+                printf "%s *\n    %s    $0 = $0  ; <- adjust" (show today) baiAccount+                ]++-- -- | Print the last balance assertion date & status of all accounts with balance assertions.+-- printAccountLastAssertions :: Day -> [BalanceAssertionInfo] -> IO ()+-- printAccountLastAssertions today acctassertioninfos = do+--   forM_ acctassertioninfos $ \BAI{..} -> do+--     putStr $ printf "%-30s  %s %s, %d days ago\n"+--       baiAccount+--       (if baiLatestClearedAssertionStatus==Unmarked then " " else show baiLatestClearedAssertionStatus)+--       (show baiLatestClearedAssertionDate)+--       (diffDays today baiLatestClearedAssertionDate)
Hledger/Data/JournalChecks/Ordereddates.hs view
@@ -6,35 +6,37 @@ import Control.Monad (forM) import Data.List (groupBy) import Text.Printf (printf)-import Data.Maybe (fromMaybe)+import qualified Data.Text as T (pack, unlines)  import Hledger.Data.Errors (makeTransactionErrorExcerpt) import Hledger.Data.Transaction (transactionFile, transactionDateOrDate2) import Hledger.Data.Types+import Hledger.Utils (textChomp)  journalCheckOrdereddates :: WhichDate -> Journal -> Either String () journalCheckOrdereddates whichdate j = do-  let +  let     -- we check date ordering within each file, not across files     -- note, relying on txns always being sorted by file here     txnsbyfile = groupBy (\t1 t2 -> transactionFile t1 == transactionFile t2) $ jtxns j     getdate = transactionDateOrDate2 whichdate-    compare a b = getdate a <= getdate b-  either Left (const $ Right ()) $ -   forM txnsbyfile $ \ts ->-    case checkTransactions compare ts of+    compare' a b = getdate a <= getdate b+  (const $ Right ()) =<< (forM txnsbyfile $ \ts ->+    case checkTransactions compare' ts of       FoldAcc{fa_previous=Nothing} -> Right ()       FoldAcc{fa_error=Nothing}    -> Right ()       FoldAcc{fa_error=Just t, fa_previous=Just tprev} -> Left $ printf-        "%s:%d:%d-%d:\n%stransaction date%s is out of order with previous transaction date %s" -        f l col col2 ex datenum tprevdate+        ("%s:%d:\n%s\nOrdered dates checking is enabled, and this transaction's\n"+          ++ "date%s (%s) is out of order with the previous transaction.\n"+          ++ "Consider moving this entry into date order, or adjusting its date.")+        f l ex datenum (show $ getdate t)         where-          (f,l,mcols,ex) = makeTransactionErrorExcerpt t finderrcols-          col  = maybe 0 fst mcols-          col2 = maybe 0 (fromMaybe 0 . snd) mcols+          (_,_,_,ex1) = makeTransactionErrorExcerpt tprev (const Nothing)+          (f,l,_,ex2) = makeTransactionErrorExcerpt t finderrcols+          -- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them+          ex = T.unlines [textChomp ex1, T.pack " ", textChomp ex2]           finderrcols _t = Just (1, Just 10)-          datenum   = if whichdate==SecondaryDate then "2" else ""-          tprevdate = show $ getdate tprev+          datenum   = if whichdate==SecondaryDate then "2" else "")  data FoldAcc a b = FoldAcc  { fa_error    :: Maybe a@@ -43,11 +45,11 @@  checkTransactions :: (Transaction -> Transaction -> Bool)   -> [Transaction] -> FoldAcc Transaction Transaction-checkTransactions compare = foldWhile f FoldAcc{fa_error=Nothing, fa_previous=Nothing}+checkTransactions compare' = foldWhile f FoldAcc{fa_error=Nothing, fa_previous=Nothing}   where     f current acc@FoldAcc{fa_previous=Nothing} = acc{fa_previous=Just current}     f current acc@FoldAcc{fa_previous=Just previous} =-      if compare previous current+      if compare' previous current       then acc{fa_previous=Just current}       else acc{fa_error=Just current} @@ -55,5 +57,5 @@ foldWhile _ acc [] = acc foldWhile fold acc (a:as) =   case fold a acc of-   acc@FoldAcc{fa_error=Just _} -> acc-   acc -> foldWhile fold acc as+   acc'@FoldAcc{fa_error=Just _} -> acc'+   acc' -> foldWhile fold acc' as
Hledger/Data/JournalChecks/Uniqueleafnames.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE NamedFieldPuns    #-} {-# LANGUAGE OverloadedStrings #-}  module Hledger.Data.JournalChecks.Uniqueleafnames (@@ -11,13 +10,13 @@ import Data.Text (Text) import qualified Data.Text as T import Text.Printf (printf)-import Data.Maybe (fromMaybe)  import Hledger.Data.AccountName (accountLeafName) import Hledger.Data.Errors (makePostingErrorExcerpt) import Hledger.Data.Journal (journalPostings, journalAccountNamesUsed) import Hledger.Data.Posting (isVirtual) import Hledger.Data.Types+import Hledger.Utils (chomp, textChomp)  -- | Check that all the journal's postings are to accounts with a unique leaf name. -- Otherwise, return an error message for the first offending posting.@@ -26,10 +25,34 @@   -- find all duplicate leafnames, and the full account names they appear in   case finddupes $ journalLeafAndFullAccountNames j of     [] -> Right ()-    dupes ->-      -- report the first posting that references one of them (and its position), for now-      mapM_ (checkposting dupes) $ journalPostings j+    -- pick the first duplicated leafname and show the transactions of+    -- the first two postings using it, highlighting the second as the error.+    (leaf,fulls):_ ->+      case filter ((`elem` fulls).paccount) $ journalPostings j of+        ps@(p:p2:_) -> Left $ chomp $ printf+          ("%s:%d:\n%s\nChecking for unique account leaf names is enabled, and\n"+          ++"account leaf name %s is not unique.\n"+          ++"It appears in these account names, which are used in %d places:\n%s"+          ++"\nConsider changing these account names so their last parts are different."+          )+          f l ex (show leaf) (length ps) accts+          where+            -- t = fromMaybe nulltransaction ptransaction  -- XXX sloppy+            (_,_,_,ex1) = makePostingErrorExcerpt p (\_ _ _ -> Nothing)+            (f,l,_,ex2) = makePostingErrorExcerpt p2 finderrcols+            -- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them+            ex = T.unlines [textChomp ex1, T.pack " ...", textChomp ex2]+            finderrcols p' _ _ = Just (col, Just col2)+              where+                a = paccount p'+                alen = T.length a+                llen = T.length $ accountLeafName a+                col = 5 + (if isVirtual p' then 1 else 0) + alen - llen+                col2 = col + llen - 1+            accts = T.unlines fulls +        _ -> Right ()  -- shouldn't happen+ finddupes :: (Ord leaf, Eq full) => [(leaf, full)] -> [(leaf, [full])] finddupes leafandfullnames = zip dupLeafs dupAccountNames   where dupLeafs = map (fst . head) d@@ -42,24 +65,3 @@ journalLeafAndFullAccountNames :: Journal -> [(Text, AccountName)] journalLeafAndFullAccountNames = map leafAndAccountName . journalAccountNamesUsed   where leafAndAccountName a = (accountLeafName a, a)--checkposting :: [(Text,[AccountName])] -> Posting -> Either String ()-checkposting leafandfullnames p@Posting{paccount=a} =-  case [lf | lf@(_,fs) <- leafandfullnames, a `elem` fs] of-    []             -> Right ()-    (leaf,fulls):_ -> Left $ printf-      "%s:%d:%d-%d:\n%saccount leaf name \"%s\" is not unique\nit is used in account names: %s" -      f l col col2 ex leaf accts-      where-        -- t = fromMaybe nulltransaction ptransaction  -- XXX sloppy-        col  = maybe 0 fst mcols-        col2 = maybe 0 (fromMaybe 0 . snd) mcols-        (f,l,mcols,ex) = makePostingErrorExcerpt p finderrcols-          where-            finderrcols p _ _ = Just (col, Just col2)-              where-                alen = T.length $ paccount p-                llen = T.length $ accountLeafName a-                col = 5 + (if isVirtual p then 1 else 0) + alen - llen-                col2 = col + llen - 1-        accts = T.intercalate ", " $ map (("\""<>).(<>"\"")) fulls
Hledger/Data/Period.hs view
@@ -59,7 +59,7 @@   where     (y', q') | q==4      = (y+1,1)              | otherwise = (y,q+1)-    quarterAsMonth q = (q-1) * 3 + 1+    quarterAsMonth q2 = (q2-1) * 3 + 1     m  = quarterAsMonth q     m' = quarterAsMonth q' periodAsDateSpan (YearPeriod y) = DateSpan (Just $ fromGregorian y 1 1) (Just $ fromGregorian (y+1) 1 1)
Hledger/Data/PeriodicTransaction.hs view
@@ -41,7 +41,7 @@           nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }           nulldatespan -_ptgenspan str span = do+_ptgenspan str spn = do   let     t = T.pack str     (i,s) = parsePeriodExpr' nulldate t@@ -51,7 +51,7 @@       mapM_ (T.putStr . showTransaction) $         runPeriodicTransaction           nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }-          span+          spn  --deriving instance Show PeriodicTransaction -- for better pretty-printing:@@ -73,7 +73,9 @@  --nullperiodictransaction is defined in Types.hs --- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan'+-- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan'.+-- This should be a closed span with both start and end dates specified;+-- an open ended span will generate no transactions. -- -- Note that new transactions require 'txnTieKnot' post-processing. -- The new transactions will have three tags added: 
Hledger/Data/Posting.hs view
@@ -61,6 +61,7 @@   postingsAsLines,   showAccountName,   renderCommentLines,+  showBalanceAssertion,   -- * misc.   postingTransformAmount,   postingApplyValuation,@@ -85,7 +86,7 @@ import Safe (maximumBound) import Text.DocLayout (realLength) -import Text.Tabular.AsciiWide+import Text.Tabular.AsciiWide hiding (render)  import Hledger.Utils import Hledger.Data.Types@@ -395,7 +396,7 @@     Right a -> Right p{paccount=a}     Left e  -> Left err       where-        err = "problem while applying account aliases:\n" ++ pshow aliases +        err = "problem while applying account aliases:\n" ++ pshow aliases           ++ "\n to account name: "++T.unpack paccount++"\n "++e  -- | Choose and apply a consistent display style to the posting@@ -419,19 +420,26 @@  -- | Maybe convert this 'Posting's amount to cost, and apply apply appropriate -- amount styles.-postingToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Posting-postingToCost _      NoConversionOp p = p-postingToCost styles ToCost         p = postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p+postingToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Maybe Posting+postingToCost _      NoConversionOp p = Just p+postingToCost styles ToCost         p+    -- If this is a conversion posting with a matched transaction price posting, ignore it+    | "_conversion-matched" `elem` map fst (ptags p) && noCost = Nothing+    | otherwise = Just $ postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p+  where+    noCost = (not . any (isJust . aprice) . amountsRaw) $ pamount p  -- | Generate inferred equity postings from a 'Posting' using transaction prices.+-- Make sure not to generate equity postings when there are already matched+-- conversion postings. postingAddInferredEquityPostings :: Text -> Posting -> [Posting]-postingAddInferredEquityPostings equityAcct p = taggedPosting : concatMap conversionPostings priceAmounts+postingAddInferredEquityPostings equityAcct p+    | "_price-matched" `elem` map fst (ptags p) = [p]+    | otherwise = taggedPosting : concatMap conversionPostings priceAmounts   where     taggedPosting       | null priceAmounts = p-      | otherwise         = p{ pcomment = pcomment p `commentAddTag` priceTag-                             , ptags = priceTag : ptags p-                             }+      | otherwise         = p{ ptags = ("_price-matched","") : ptags p }     conversionPostings amt = case aprice amt of         Nothing -> []         Just _  -> [ cp{ paccount = accountPrefix <> amtCommodity@@ -446,7 +454,7 @@         amtCommodity  = commodity amt         costCommodity = commodity cost         cp = p{ pcomment = pcomment p `commentAddTag` ("generated-posting","")-              , ptags = [("generated-posting", ""), ("_generated-posting", "")]+              , ptags = [("_conversion-matched", ""), ("generated-posting", ""), ("_generated-posting", "")]               , pbalanceassertion = Nothing               , poriginal = Nothing               }@@ -454,7 +462,6 @@         -- Take the commodity of an amount and collapse consecutive spaces to a single space         commodity = T.unwords . filter (not . T.null) . T.words . acommodity -    priceTag = ("cost", T.strip . wbToText $ foldMap showAmountPrice priceAmounts)     priceAmounts = filter (isJust . aprice) . amountsRaw $ pamount p  -- | Make a market price equivalent to this posting's amount's unit@@ -490,7 +497,7 @@ -- A space is inserted following the colon, before the value. commentAddTagNextLine :: Text -> Tag -> Text commentAddTagNextLine cmt (t,v) =-  cmt <> (if "\n" `T.isSuffixOf` cmt then "" else "\n") <> t <> ": " <> v +  cmt <> (if "\n" `T.isSuffixOf` cmt then "" else "\n") <> t <> ": " <> v   -- tests
Hledger/Data/StringFormat.hs view
@@ -4,7 +4,6 @@  {-# LANGUAGE FlexibleContexts  #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PackageImports    #-} {-# LANGUAGE TypeFamilies      #-}  module Hledger.Data.StringFormat (@@ -154,8 +153,8 @@ formatStringTester fs value expected = actual @?= expected   where     actual = case fs of-      FormatLiteral l                   -> formatText False Nothing Nothing l-      FormatField leftJustify min max _ -> formatText leftJustify min max value+      FormatLiteral l                 -> formatText False Nothing Nothing l+      FormatField leftJustify mn mx _ -> formatText leftJustify mn mx value  tests_StringFormat = testGroup "StringFormat" [ 
Hledger/Data/Timeclock.hs view
@@ -28,7 +28,6 @@ import Hledger.Data.Dates import Hledger.Data.Amount import Hledger.Data.Posting-import Hledger.Data.Transaction  instance Show TimeclockEntry where     show t = printf "%s %s %s  %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription t)@@ -65,10 +64,10 @@       o' = o{tldatetime=itime{localDay=idate, localTimeOfDay=TimeOfDay 23 59 59}}       i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}} timeclockEntriesToTransactions now (i:o:rest)-    | tlcode i /= In = errorExpectedCodeButGot In i-    | tlcode o /= Out =errorExpectedCodeButGot Out o-    | odate > idate = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now (i':o:rest)-    | otherwise = entryFromTimeclockInOut i o : timeclockEntriesToTransactions now rest+    | tlcode i /= In  = errorExpectedCodeButGot In i+    | tlcode o /= Out = errorExpectedCodeButGot Out o+    | odate > idate   = entryFromTimeclockInOut i o' : timeclockEntriesToTransactions now (i':o:rest)+    | otherwise       = entryFromTimeclockInOut i o : timeclockEntriesToTransactions now rest     where       (itime,otime) = (tldatetime i,tldatetime o)       (idate,odate) = (localDay itime,localDay otime)@@ -76,10 +75,19 @@       i' = i{tldatetime=itime{localDay=addDays 1 idate, localTimeOfDay=midnight}} {- HLINT ignore timeclockEntriesToTransactions -} -errorExpectedCodeButGot expected actual = errorWithSourceLine line $ "expected timeclock code " ++ (show expected) ++ " but got " ++ show (tlcode actual)-    where line = unPos . sourceLine $ tlsourcepos actual--errorWithSourceLine line msg = error $ "line " ++ show line ++ ": " ++ msg+errorExpectedCodeButGot :: TimeclockCode -> TimeclockEntry -> a+errorExpectedCodeButGot expected actual = error' $ printf+  ("%s:\n%s\n%s\n\nExpected a timeclock %s entry but got %s.\n"+  ++"Only one session may be clocked in at a time.\n"+  ++"Please alternate i and o, beginning with i.")+  (sourcePosPretty $ tlsourcepos actual)+  (l ++ " | " ++ show actual)+  (replicate (length l) ' ' ++ " |" ++ replicate c ' ' ++ "^")+  (show expected)+  (show $ tlcode actual)+  where+    l = show $ unPos $ sourceLine $ tlsourcepos actual+    c = unPos $ sourceColumn $ tlsourcepos actual  -- | Convert a timeclock clockin and clockout entry to an equivalent journal -- transaction, representing the time expenditure. Note this entry is  not balanced,@@ -87,9 +95,23 @@ entryFromTimeclockInOut :: TimeclockEntry -> TimeclockEntry -> Transaction entryFromTimeclockInOut i o     | otime >= itime = t-    | otherwise = error' . T.unpack $-        "clock-out time less than clock-in time in:\n" <> showTransaction t  -- PARTIAL:+    | otherwise =+      -- Clockout time earlier than clockin is an error.+      -- (Clockin earlier than preceding clockin/clockout is allowed.)+      error' $ printf+        ("%s:\n%s\nThis clockout time (%s) is earlier than the previous clockin.\n"+        ++"Please adjust it to be later than %s.")+        (sourcePosPretty $ tlsourcepos o)+        (unlines [+          replicate (length l) ' '++ " | " ++ show i,+          l ++ " | " ++ show o,+          (replicate (length l) ' ' ++ " |" ++ replicate c ' ' ++ replicate 19 '^')+          ])+        (show $ tldatetime o)+        (show $ tldatetime i)     where+      l = show $ unPos $ sourceLine $ tlsourcepos o+      c = (unPos $ sourceColumn $ tlsourcepos o) + 2       t = Transaction {             tindex       = 0,             tsourcepos   = (tlsourcepos i, tlsourcepos i),@@ -117,8 +139,8 @@       -- since otherwise it will often have large recurring decimal parts which (since 1.21)       -- print would display all 255 digits of. timeclock amounts have one second resolution,       -- so two decimal places is precise enough (#1527).-      amount   = mixedAmount $ setAmountInternalPrecision 2 $ hrs hours-      ps       = [posting{paccount=acctname, pamount=amount, ptype=VirtualPosting, ptransaction=Just t}]+      amt   = mixedAmount $ setAmountInternalPrecision 2 $ hrs hours+      ps       = [posting{paccount=acctname, pamount=amt, ptype=VirtualPosting, ptransaction=Just t}]   -- tests
Hledger/Data/Transaction.hs view
@@ -7,6 +7,7 @@  -} +{-# LANGUAGE MultiWayIf        #-} {-# LANGUAGE NamedFieldPuns    #-} {-# LANGUAGE OverloadedStrings #-} @@ -27,6 +28,7 @@ , transactionApplyValuation , transactionToCost , transactionAddInferredEquityPostings+, transactionAddPricesFromEquity , transactionApplyAliases , transactionMapPostings , transactionMapPostingAmounts@@ -43,17 +45,23 @@ , showTransactionOneLineAmounts , showTransactionLineFirstPart , transactionFile+  -- * transaction errors+, annotateErrorWithTransaction   -- * tests , tests_Transaction ) where -import Data.Maybe (fromMaybe)+import Control.Monad.Trans.State (StateT(..), evalStateT)+import Data.Bifunctor (first)+import Data.Foldable (foldrM)+import Data.Maybe (fromMaybe, isJust, mapMaybe)+import Data.Semigroup (Endo(..)) import Data.Text (Text)+import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB import Data.Time.Calendar (Day, fromGregorian)-import qualified Data.Map as M  import Hledger.Utils import Hledger.Data.Types@@ -210,13 +218,128 @@ -- | Maybe convert this 'Transaction's amounts to cost and apply the -- appropriate amount styles. transactionToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Transaction -> Transaction-transactionToCost styles cost = transactionMapPostings (postingToCost styles cost)+transactionToCost styles cost t = t{tpostings = mapMaybe (postingToCost styles cost) $ tpostings t}  -- | Add inferred equity postings to a 'Transaction' using transaction prices. transactionAddInferredEquityPostings :: AccountName -> Transaction -> Transaction transactionAddInferredEquityPostings equityAcct t =     t{tpostings=concatMap (postingAddInferredEquityPostings equityAcct) $ tpostings t} +-- | Add inferred transaction prices from equity postings. For every adjacent+-- pair of conversion postings, it will first search the postings with+-- transaction prices to see if any match. If so, it will tag it as matched.+-- If no postings with transaction prices match, it will then search the+-- postings without transaction prices, and will match the first such posting+-- which matches one of the conversion amounts. If it finds a match, it will+-- add a transaction price and then tag it.+type IdxPosting = (Int, Posting)+transactionAddPricesFromEquity :: M.Map AccountName AccountType -> Transaction -> Either String Transaction+transactionAddPricesFromEquity acctTypes t = first (annotateErrorWithTransaction t . T.unpack) $ do+    (conversionPairs, stateps) <- partitionPs npostings+    f <- transformIndexedPostingsF addPricesToPostings conversionPairs stateps+    return t{tpostings = map (snd . f) npostings}+  where+    -- Include indices for postings+    npostings = zip [0..] $ tpostings t+    transformIndexedPostingsF f = evalStateT . fmap (appEndo . foldMap Endo) . traverse f++    -- Sort postings into pairs of conversion postings, transaction price postings, and other postings+    partitionPs = fmap fst . foldrM select (([], ([], [])), Nothing)+    select np@(_, p) ((cs, others@(ps, os)), Nothing)+      | isConversion p = Right ((cs, others),      Just np)+      | hasPrice p     = Right ((cs, (np:ps, os)), Nothing)+      | otherwise      = Right ((cs, (ps, np:os)), Nothing)+    select np@(_, p) ((cs, others), Just lst)+      | isConversion p = Right (((lst, np):cs, others), Nothing)+      | otherwise      = Left "Conversion postings must occur in adjacent pairs"++    -- Given a pair of indexed conversion postings, and a state consisting of lists of+    -- priced and unpriced non-conversion postings, create a function which adds transaction+    -- prices to the posting which matches the conversion postings if necessary, and tags+    -- the conversion and matched postings. Then update the state by removing the matched+    -- postings. If there are no matching postings or too much ambiguity, return an error+    -- string annotated with the conversion postings.+    addPricesToPostings :: (IdxPosting, IdxPosting)+                        -> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)+    addPricesToPostings ((n1, cp1), (n2, cp2)) = StateT $ \(priceps, otherps) -> do+        -- Get the two conversion posting amounts, if possible+        ca1 <- postingAmountNoPrice cp1+        ca2 <- postingAmountNoPrice cp2+        let -- The function to add transaction prices and tag postings in the indexed list of postings+            transformPostingF np pricep (n,p) =+              (n, if | n == np            -> pricep `postingAddTags` [("_price-matched","")]+                     | n == n1 || n == n2 -> p      `postingAddTags` [("_conversion-matched","")]+                     | otherwise          -> p)+            -- All priced postings which match the conversion posting pair+            matchingPricePs = mapMaybe (mapM $ pricedPostingIfMatchesBothAmounts ca1 ca2) priceps+            -- All other postings which match at least one of the conversion posting pair+            matchingOtherPs = mapMaybe (mapM $ addPriceIfMatchesOneAmount ca1 ca2) otherps++        -- Annotate any errors with the conversion posting pair+        first (annotateWithPostings [cp1, cp2]) $+            if -- If a single transaction price posting matches the conversion postings,+               -- delete it from the list of priced postings in the state, delete the+               -- first matching unpriced posting from the list of non-priced postings+               -- in the state, and return the transformation function with the new state.+               | [(np, (pricep, _))] <- matchingPricePs+               , Just newpriceps <- deleteIdx np priceps+                   -> Right (transformPostingF np pricep, (newpriceps, otherps))+               -- If no transaction price postings match the conversion postings, but some+               -- of the unpriced postings match, check that the first such posting has a+               -- different amount from all the others, and if so add a transaction price to+               -- it, then delete it from the list of non-priced postings in the state, and+               -- return the transformation function with the new state.+               | [] <- matchingPricePs+               , (np, (pricep, amt)):nps <- matchingOtherPs+               , not $ any (amountMatches amt . snd . snd) nps+               , Just newotherps <- deleteIdx np otherps+                   -> Right (transformPostingF np pricep, (priceps, newotherps))+               -- Otherwise it's too ambiguous to make a guess, so return an error.+               | otherwise -> Left "There is not a unique posting which matches the conversion posting pair:"++    -- If a posting with transaction price matches both the conversion amounts, return it along+    -- with the matching amount which must be present in another non-conversion posting.+    pricedPostingIfMatchesBothAmounts :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)+    pricedPostingIfMatchesBothAmounts a1 a2 p = do+        a@Amount{aprice=Just _} <- postingSingleAmount p+        if | amountMatches (-a1) a && amountMatches a2 (amountCost a) -> Just (p, -a2)+           | amountMatches (-a2) a && amountMatches a1 (amountCost a) -> Just (p, -a1)+           | otherwise -> Nothing++    -- Add a transaction price to a posting if it matches (negative) one of the+    -- supplied conversion amounts, adding the other amount as the price+    addPriceIfMatchesOneAmount :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)+    addPriceIfMatchesOneAmount a1 a2 p = do+        a <- postingSingleAmount p+        let newp price = p{pamount = mixedAmount a{aprice = Just $ TotalPrice price}}+        if | amountMatches (-a1) a -> Just (newp a2, a2)+           | amountMatches (-a2) a -> Just (newp a1, a1)+           | otherwise             -> Nothing++    hasPrice p = isJust $ aprice =<< postingSingleAmount p+    postingAmountNoPrice p = case postingSingleAmount p of+        Just a@Amount{aprice=Nothing} -> Right a+        _ -> Left $ annotateWithPostings [p] "The posting must only have a single amount with no transaction price"+    postingSingleAmount p = case amountsRaw (pamount p) of+        [a] -> Just a+        _   -> Nothing++    amountMatches a b = acommodity a == acommodity b && aquantity a == aquantity b+    isConversion p = M.lookup (paccount p) acctTypes == Just Conversion++    -- Delete a posting from the indexed list of postings based on either its+    -- index or its posting amount.+    -- Note: traversing the whole list to delete a single match is generally not efficient,+    -- but given that a transaction probably doesn't have more than four postings, it should+    -- still be more efficient than using a Map or another data structure. Even monster+    -- transactions with up to 10 postings, which are generally not a good+    -- idea, are still too small for there to be an advantage.+    deleteIdx n = deleteUniqueMatch ((n==) . fst)+    deleteUniqueMatch p (x:xs) | p x       = if any p xs then Nothing else Just xs+                               | otherwise = (x:) <$> deleteUniqueMatch p xs+    deleteUniqueMatch _ []                 = Nothing+    annotateWithPostings xs str = T.unlines $ str : postingsAsLines False xs+ -- | 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@@ -236,6 +359,13 @@ -- | The file path from which this transaction was parsed. transactionFile :: Transaction -> FilePath transactionFile Transaction{tsourcepos} = sourceName $ fst tsourcepos++-- Add transaction information to an error message.+annotateErrorWithTransaction :: Transaction -> String -> String+annotateErrorWithTransaction t s =+  unlines [ sourcePosPairPretty $ tsourcepos t, s+          , T.unpack . T.stripEnd $ showTransaction t+          ]  -- tests 
Hledger/Data/TransactionModifier.hs view
@@ -76,7 +76,7 @@ -- 0000-01-01 --     ping           $1.00 -- <BLANKLINE>--- >>> test $ TransactionModifier "ping" [("pong" `tmpost` amount{aquantity=3}){tmprIsMultiplier=True}]+-- >>> test $ TransactionModifier "ping" [("pong" `tmpost` nullamt{aquantity=3}){tmprIsMultiplier=True}] -- 0000-01-01 --     ping           $1.00 --     pong           $3.00  ; generated-posting: = ping
Hledger/Data/Types.hs view
@@ -49,7 +49,7 @@ import Data.Time.LocalTime (LocalTime) import Data.Word (Word8) import Text.Blaze (ToMarkup(..))-import Text.Megaparsec (SourcePos)+import Text.Megaparsec (SourcePos(SourcePos), mkPos)  import Hledger.Utils.Regex @@ -585,12 +585,14 @@   ,aditags             :: [Tag]  -- ^ tags extracted from the account comment, if any   ,adideclarationorder :: Int    -- ^ the order in which this account was declared,                                  --   relative to other account declarations, during parsing (1..)+  ,adisourcepos        :: SourcePos  -- ^ source file and position } deriving (Eq,Show,Generic)  nullaccountdeclarationinfo = AccountDeclarationInfo {    adicomment          = ""   ,aditags             = []   ,adideclarationorder = 0+  ,adisourcepos        = SourcePos "" (mkPos 1) (mkPos 1) }  -- | An account, with its balances, parent/subaccount relationships, etc.
Hledger/Data/Valuation.hs view
@@ -193,7 +193,7 @@                                       -- Make default display style use precision 2 instead of 0 ?                                       -- Leave as is for now; mentioned in manual.       styleAmount styles-      amount{acommodity=comm, aquantity=rate * aquantity a}+      nullamt{acommodity=comm, aquantity=rate * aquantity a}  -- | Calculate the gain of each component amount, that is the difference -- between the valued amount and the value of the cost basis (see
Hledger/Query.hs view
@@ -67,7 +67,7 @@   matchesTags,   matchesPriceDirective,   words'',-  prefixes,+  queryprefixes,   -- * tests   tests_Query )@@ -167,7 +167,7 @@ -- >>> 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+parseQuery d = parseQueryList d . words'' queryprefixes  -- | 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.@@ -234,8 +234,8 @@  -- XXX -- keep synced with patterns below, excluding "not"-prefixes :: [T.Text]-prefixes = map (<>":") [+queryprefixes :: [T.Text]+queryprefixes = map (<>":") [      "inacctonly"     ,"inacct"     ,"amt"@@ -285,10 +285,10 @@ parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left . Acct <$> toRegexCI s parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =         case parsePeriodExpr d s of Left e         -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e-                                    Right (_,span) -> Right $ Left $ Date2 span+                                    Right (_,spn) -> Right $ Left $ Date2 spn parseQueryTerm d (T.stripPrefix "date:" -> Just s) =         case parsePeriodExpr d s of Left e         -> Left $ "\"date:"++T.unpack s++"\" gave a "++showDateParseError e-                                    Right (_,span) -> Right $ Left $ Date span+                                    Right (_,spn) -> Right $ Left $ Date spn parseQueryTerm _ (T.stripPrefix "status:" -> Just s) =         case parseStatus s of Left e   -> Left $ "\"status:"++T.unpack s++"\" gave a parse error: " ++ e                               Right st -> Right $ Left $ StatusQ st@@ -412,9 +412,9 @@ -- * modifying  simplifyQuery :: Query -> Query-simplifyQuery q =-  let q' = simplify q-  in if q' == q then q else simplifyQuery q'+simplifyQuery q0 =+  let q1 = simplify q0+  in if q1 == q0 then q0 else simplifyQuery q1   where     simplify (And []) = Any     simplify (And [q]) = simplify q@@ -455,7 +455,7 @@ -- (Since 1.24.1, might be merged into filterQuery in future.) -- XXX Semantics not completely clear. filterQueryOrNotQuery :: (Query -> Bool) -> Query -> Query-filterQueryOrNotQuery p = simplifyQuery . filterQueryOrNotQuery' p+filterQueryOrNotQuery p0 = simplifyQuery . filterQueryOrNotQuery' p0   where     filterQueryOrNotQuery' :: (Query -> Bool) -> Query -> Query     filterQueryOrNotQuery' p (And qs)      = And $ map (filterQueryOrNotQuery p) qs@@ -584,7 +584,7 @@ queryEndDate True (Date2 (DateSpan _ (Just d))) = Just d queryEndDate _ _ = Nothing -queryTermDateSpan (Date span) = Just span+queryTermDateSpan (Date spn) = Just spn queryTermDateSpan _ = Nothing  -- | What date span (or with a true argument, what secondary date span) does this query specify ?@@ -594,8 +594,8 @@ queryDateSpan :: Bool -> Query -> DateSpan queryDateSpan secondary (Or qs)  = spansUnion     $ map (queryDateSpan secondary) qs queryDateSpan secondary (And qs) = spansIntersect $ map (queryDateSpan secondary) qs-queryDateSpan _     (Date span)  = span-queryDateSpan True (Date2 span)  = span+queryDateSpan _     (Date spn)  = spn+queryDateSpan True (Date2 spn)  = spn queryDateSpan _ _                = nulldatespan  -- | What date span does this query specify, treating primary and secondary dates as equivalent ?@@ -605,8 +605,8 @@ queryDateSpan' :: Query -> DateSpan queryDateSpan' (Or qs)      = spansUnion     $ map queryDateSpan' qs queryDateSpan' (And qs)     = spansIntersect $ map queryDateSpan' qs-queryDateSpan' (Date span)  = span-queryDateSpan' (Date2 span) = span+queryDateSpan' (Date spn)  = spn+queryDateSpan' (Date2 spn) = spn queryDateSpan' _            = nulldatespan  -- | What is the earliest of these dates, where Nothing is earliest ?@@ -732,16 +732,16 @@ matchesPosting (Code r) p = maybe False (regexMatchText r . tcode) $ ptransaction p matchesPosting (Desc r) p = maybe False (regexMatchText r . tdescription) $ ptransaction p matchesPosting (Acct r) p = matches p || maybe False matches (poriginal p) where matches = regexMatchText r . paccount-matchesPosting (Date span) p = span `spanContainsDate` postingDate p-matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p+matchesPosting (Date spn) p = spn `spanContainsDate` postingDate p+matchesPosting (Date2 spn) p = spn `spanContainsDate` postingDate2 p matchesPosting (StatusQ s) p = postingStatus p == s matchesPosting (Real v) p = v == isReal p matchesPosting q@(Depth _) Posting{paccount=a} = q `matchesAccount` a matchesPosting q@(Amt _ _) Posting{pamount=as} = q `matchesMixedAmount` as matchesPosting (Sym r) Posting{pamount=as} = any (matchesCommodity (Sym r) . acommodity) $ amountsRaw as matchesPosting (Tag n v) p = case (reString n, v) of-  ("payee", Just v) -> maybe False (regexMatchText v . transactionPayee) $ ptransaction p-  ("note", Just v) -> maybe False (regexMatchText v . transactionNote) $ ptransaction p+  ("payee", Just v') -> maybe False (regexMatchText v' . transactionPayee) $ ptransaction p+  ("note", Just v') -> maybe False (regexMatchText v' . transactionNote) $ ptransaction p   (_, mv) -> matchesTags n mv $ postingAllTags p matchesPosting (Type _) _ = False @@ -765,17 +765,17 @@ matchesTransaction (Code r) t = regexMatchText r $ tcode t matchesTransaction (Desc r) t = regexMatchText r $ tdescription t matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t-matchesTransaction (Date span) t = spanContainsDate span $ tdate t-matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t+matchesTransaction (Date spn) t = spanContainsDate spn $ tdate t+matchesTransaction (Date2 spn) t = spanContainsDate spn $ transactionDate2 t matchesTransaction (StatusQ s) t = tstatus t == s matchesTransaction (Real v) t = v == hasRealPostings t matchesTransaction q@(Amt _ _) t = any (q `matchesPosting`) $ tpostings t 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-  ("payee", Just v) -> regexMatchText v $ transactionPayee t-  ("note", Just v) -> regexMatchText v $ transactionNote t-  (_, v) -> matchesTags n v $ transactionAllTags t+  ("payee", Just v') -> regexMatchText v' $ transactionPayee t+  ("note", Just v') -> regexMatchText v' $ transactionNote t+  (_, v') -> matchesTags n v' $ transactionAllTags t matchesTransaction (Type _) _ = False  -- | Like matchesTransaction, but if the journal's account types are provided,@@ -821,7 +821,7 @@ matchesPriceDirective (And qs) p    = all (`matchesPriceDirective` p) qs matchesPriceDirective q@(Amt _ _) p = matchesAmount q (pdamount p) matchesPriceDirective q@(Sym _) p   = matchesCommodity q (pdcommodity p)-matchesPriceDirective (Date span) p = spanContainsDate span (pddate p)+matchesPriceDirective (Date spn) p = spanContainsDate spn (pddate p) matchesPriceDirective _ _           = True  @@ -854,8 +854,8 @@       (words'' [] "not:'a b'")             @?= ["not:a b"]       (words'' [] "'not:a b'")             @?= ["not:a b"]       (words'' ["desc:"] "not:desc:'a b'") @?= ["not:desc:a b"]-      (words'' prefixes "\"acct:expenses:autres d\233penses\"") @?= ["acct:expenses:autres d\233penses"]-      (words'' prefixes "\"")              @?= ["\""]+      (words'' queryprefixes "\"acct:expenses:autres d\233penses\"") @?= ["acct:expenses:autres d\233penses"]+      (words'' queryprefixes "\"")              @?= ["\""]    ,testCase "filterQuery" $ do      filterQuery queryIsDepth Any       @?= Any
Hledger/Read.hs view
@@ -69,7 +69,7 @@ import System.Directory (doesFileExist, getHomeDirectory) import System.Environment (getEnv) import System.Exit (exitFailure)-import System.FilePath ((<.>), (</>), splitDirectories, splitFileName)+import System.FilePath ((<.>), (</>), splitDirectories, splitFileName, takeFileName) import System.Info (os) import System.IO (hPutStr, stderr) @@ -108,15 +108,15 @@ defaultJournalPath :: IO String defaultJournalPath = do   s <- envJournalPath-  if null s then defaultJournalPath else return s+  if null s then defpath else return s     where       envJournalPath =         getEnv journalEnvVar          `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2                                             `C.catch` (\(_::C.IOException) -> return ""))-      defaultJournalPath = do-                  home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")-                  return $ home </> journalDefaultFilename+      defpath = do+        home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "")+        return $ home </> journalDefaultFilename  -- | A file path optionally prefixed by a reader name and colon -- (journal:, csv:, timedot:, etc.).@@ -139,9 +139,8 @@ -- since hledger 1.17, we prefer predictability.) readJournal :: InputOpts -> Maybe FilePath -> Text -> ExceptT String IO Journal readJournal iopts mpath txt = do-  let r :: Reader IO =-        fromMaybe JournalReader.reader $ findReader (mformat_ iopts) mpath-  dbg6IO "trying reader" (rFormat r)+  let r :: Reader IO = fromMaybe JournalReader.reader $ findReader (mformat_ iopts) mpath+  dbg6IO "readJournal: trying reader" (rFormat r)   rReadFn r iopts (fromMaybe "(string)" mpath) txt  -- | Read a Journal from this file, or from stdin if the file path is -,@@ -161,7 +160,9 @@     (mfmt, f) = splitReaderPrefix prefixedfile     iopts' = iopts{mformat_=asum [mfmt, mformat_ iopts]}   liftIO $ requireJournalFileExists f-  t <- liftIO $ readFileOrStdinPortably f+  t <-+    traceAt 6 ("readJournalFile: "++takeFileName f) $+    liftIO $ readFileOrStdinPortably f     -- <- T.readFile f  -- or without line ending translation, for testing   j <- readJournal iopts' (Just f) t   if new_ iopts
Hledger/Read/Common.hs view
@@ -141,13 +141,14 @@ import Text.Megaparsec.Char (char, char', digitChar, newline, string) import Text.Megaparsec.Char.Lexer (decimal) import Text.Megaparsec.Custom-  (FinalParseError, attachSource, customErrorBundlePretty, finalErrorBundlePretty, parseErrorAt, parseErrorAtRegion, HledgerParseErrors)+  (FinalParseError, attachSource, customErrorBundlePretty, finalErrorBundlePretty, parseErrorAt, parseErrorAtRegion)  import Hledger.Data import Hledger.Query (Query(..), filterQuery, parseQueryTerm, queryEndDate, queryStartDate, queryIsDate, simplifyQuery) import Hledger.Reports.ReportOptions (ReportOpts(..), queryFromFlags, rawOptsToReportOpts) import Hledger.Utils import Hledger.Read.InputOptions+import System.FilePath (takeFileName)  --- ** doctest setup -- $setup@@ -194,10 +195,10 @@         argsquery = lefts . rights . map (parseQueryTerm day) $ querystring_ ropts         datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery -        commodity_styles = either err id $ commodityStyleFromRawOpts rawopts+        styles = either err id $ commodityStyleFromRawOpts rawopts           where err e = error' $ "could not parse commodity-style: '" ++ e ++ "'"  -- PARTIAL: -    in InputOpts{+    in definputopts{        -- files_             = listofstringopt "file" rawopts        mformat_           = Nothing       ,mrules_file_       = maybestringopt "rules-file" rawopts@@ -210,10 +211,11 @@       ,reportspan_        = DateSpan (queryStartDate False datequery) (queryEndDate False datequery)       ,auto_              = boolopt "auto" rawopts       ,infer_equity_      = boolopt "infer-equity" rawopts && conversionop_ ropts /= Just ToCost+      ,infer_costs_       = boolopt "infer-costs" rawopts       ,balancingopts_     = defbalancingopts{                                  ignore_assertions_ = boolopt "ignore-assertions" rawopts                                , infer_transaction_prices_ = not noinferprice-                               , commodity_styles_  = Just commodity_styles+                               , commodity_styles_  = Just styles                                }       ,strict_            = boolopt "strict" rawopts       ,_ioDay             = day@@ -305,7 +307,7 @@ -- - check all commodities have been declared if in strict mode -- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal-journalFinalise iopts@InputOpts{auto_,infer_equity_,balancingopts_,strict_,_ioDay} f txt pj = do+journalFinalise iopts@InputOpts{..} f txt pj = do   t <- liftIO getPOSIXTime   liftEither $ do     j <- pj{jglobalcommoditystyles=fromMaybe mempty $ commodity_styles_ balancingopts_}@@ -319,9 +321,14 @@       >>= (if auto_ && not (null $ jtxnmodifiers pj)               then journalAddAutoPostings _ioDay balancingopts_  -- Add auto postings if enabled, and account tags if needed               else pure)-      >>= journalBalanceTransactions balancingopts_      -- Balance all transactions and maybe check balance assertions.-      <&> (if infer_equity_ then journalAddInferredEquityPostings else id)  -- Add inferred equity postings, after balancing transactions and generating auto postings+      >>= (if infer_costs_  then journalAddPricesFromEquity else pure)      -- Add inferred transaction prices from equity postings, if present+      >>= journalBalanceTransactions balancingopts_                         -- Balance all transactions and maybe check balance assertions.+      <&> (if infer_equity_ then journalAddInferredEquityPostings else id)  -- Add inferred equity postings, after balancing and generating auto postings       <&> journalInferMarketPricesFromTransactions       -- infer market prices from commodity-exchanging transactions+      <&> traceAt 6 ("journalFinalise: " <> takeFileName f)  -- debug logging+      <&> dbgJournalAcctDeclOrder ("journalFinalise: " <> takeFileName f <> "   acct decls           : ")+      <&> journalRenumberAccountDeclarations+      <&> dbgJournalAcctDeclOrder ("journalFinalise: " <> takeFileName f <> "   acct decls renumbered: ")     when strict_ $ do       journalCheckAccounts j                     -- If in strict mode, check all postings are to declared accounts       journalCheckCommodities j                  -- and using declared commodities@@ -439,8 +446,8 @@ -- A version of `match` that is strict in the returned text match' :: TextParser m a -> TextParser m (Text, a) match' p = do-  (!txt, p) <- match p-  pure (txt, p)+  (!txt, p') <- match p+  pure (txt, p')  --- ** parsers --- *** transaction bits@@ -498,26 +505,28 @@       let dateStr = show year ++ [sep1] ++ show month ++ [sep2] ++ show day        when (sep1 /= sep2) $ customFailure $ parseErrorAtRegion startOffset endOffset $-        "invalid date: separators are different, should be the same"+        "This date is malformed because the separators are different.\n"+        ++"Please use consistent separators."        case fromGregorianValid year month day of         Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $-                     "well-formed but invalid date: " ++ dateStr+                     "This date is invalid, please correct it: " ++ dateStr         Just date -> pure $! date      partialDate :: Int -> Maybe Year -> Month -> Char -> MonthDay -> TextParser m Day-    partialDate startOffset mYear month sep day = do+    partialDate startOffset myr month sep day = do       endOffset <- getOffset-      case mYear of+      case myr of         Just year ->           case fromGregorianValid year month day of             Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $-                        "well-formed but invalid date: " ++ dateStr+                        "This date is invalid, please correct it: " ++ dateStr             Just date -> pure $! date           where dateStr = show year ++ [sep] ++ show month ++ [sep] ++ show day          Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $-          "partial date "++dateStr++" found, but the current year is unknown"+          "The partial date "++dateStr++" can not be parsed because the current year is unknown.\n"+          ++"Consider making it a full date, or add a default year directive.\n"           where dateStr = show month ++ [sep] ++ show day  {-# INLINABLE datep' #-}@@ -602,12 +611,12 @@ modifiedaccountnamep :: JournalParser m AccountName modifiedaccountnamep = do   parent  <- getParentAccount-  aliases <- getAccountAliases+  als     <- getAccountAliases   -- off1    <- getOffset   a       <- lift accountnamep   -- off2    <- getOffset   -- XXX or accountNameApplyAliasesMemo ? doesn't seem to make a difference (retest that function)-  case accountNameApplyAliases aliases $ joinAccountNames parent a of+  case accountNameApplyAliases als $ joinAccountNames parent a of     Right a' -> return $! a'     -- should not happen, regexaliasp will have displayed a better error already:     -- (XXX why does customFailure cause error to be displayed there, but not here ?)@@ -651,12 +660,12 @@ -- | Parse non-empty, single-spaced text starting and ending with non-whitespace, -- where all characters satisfy the given predicate. singlespacedtextsatisfying1p :: (Char -> Bool) -> TextParser m T.Text-singlespacedtextsatisfying1p pred = do+singlespacedtextsatisfying1p f = do   firstPart <- partp   otherParts <- many $ try $ singlespacep *> partp   pure $! T.unwords $ firstPart : otherParts   where-    partp = takeWhile1P Nothing (\c -> pred c && not (isSpace c))+    partp = takeWhile1P Nothing (\c -> f c && not (isSpace c))  -- | Parse one non-newline whitespace character that is not followed by another one. singlespacep :: TextParser m ()@@ -699,20 +708,20 @@ amountpwithmultiplier :: Bool -> JournalParser m Amount amountpwithmultiplier mult = label "amount" $ do   let spaces = lift $ skipNonNewlineSpaces-  amount <- amountwithoutpricep mult <* spaces+  amt <- amountwithoutpricep mult <* spaces   (mprice, _elotprice, _elotdate) <- runPermutation $-    (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amount <* spaces)+    (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp amt <* spaces)          <*> toPermutationWithDefault Nothing (Just <$> lotpricep <* spaces)          <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces)-  pure $ amount { aprice = mprice }+  pure $ amt { aprice = mprice }  amountpnolotpricesp :: JournalParser m Amount amountpnolotpricesp = label "amount" $ do   let spaces = lift $ skipNonNewlineSpaces-  amount <- amountwithoutpricep False+  amt <- amountwithoutpricep False   spaces-  mprice <- optional $ priceamountp amount <* spaces-  pure $ amount { aprice = mprice }+  mprice <- optional $ priceamountp amt <* spaces+  pure $ amt { aprice = mprice }  amountwithoutpricep :: Bool -> JournalParser m Amount amountwithoutpricep mult = do@@ -1085,8 +1094,8 @@  -- | A custom show instance, showing digit groups as the parser saw them. instance Show DigitGrp where-  show (DigitGrp len num) = "\"" ++ padding ++ numStr ++ "\""-    where numStr = show num+  show (DigitGrp len n) = "\"" ++ padding ++ numStr ++ "\""+    where numStr = show n           padding = genericReplicate (toInteger len - toInteger (length numStr)) '0'  instance Sem.Semigroup DigitGrp where@@ -1147,18 +1156,20 @@ isSameLineCommentStart ';' = True isSameLineCommentStart _   = False --- A parser combinator for parsing (possibly multiline) comments--- following journal items.+-- A parser for (possibly multiline) comments following a journal item. ----- Several journal items may be followed by comments, which begin with--- semicolons and extend to the end of the line. Such comments may span--- multiple lines, but comment lines below the journal item must be--- preceded by leading whitespace.+-- Comments following a journal item begin with a semicolon and extend to+-- the end of the line. They may span multiple lines; any comment lines +-- not on the same line as the journal item must be indented (preceded by+-- leading whitespace). ----- This parser combinator accepts a parser that consumes all input up--- until the next newline. This parser should extract the "content" from--- comments. The resulting parser returns this content plus the raw text--- of the comment itself.+-- Like Ledger, we sometimes allow data to be embedded in comments. Eg,+-- comments on the account directive and on transactions can contain tags,+-- and comments on postings can contain tags and/or bracketed posting dates.+-- To handle these variations, this parser takes as parameter a subparser,+-- which should consume all input up until the next newline, and which can+-- optionally extract some kind of data from it.+-- followingcommentp' returns this data along with the full text of the comment. -- -- See followingcommentp for tests. --@@ -1389,10 +1400,10 @@ -- Left ...not a bracketed date... -- -- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[2016/1/32]"--- Left ...1:2:...well-formed but invalid date: 2016/1/32...+-- Left ...1:2:...This date is invalid... -- -- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[1/31]"--- Left ...1:2:...partial date 1/31 found, but the current year is unknown...+-- Left ...1:2:...The partial date 1/31 can not be parsed... -- -- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[0123456789/-.=/-.=]" -- Left ...1:13:...expecting month or day...@@ -1469,24 +1480,24 @@    ,testCase "unit price"             $ assertParseEq amountp "$10 @ €0.5"       -- not precise enough:       -- (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1)) -- `withStyle` asdecimalpoint=Just '.'-      amount{+      nullamt{          acommodity="$"         ,aquantity=10 -- need to test internal precision with roundTo ? I think not         ,astyle=amountstyle{asprecision=Precision 0, asdecimalpoint=Nothing}         ,aprice=Just $ UnitPrice $-          amount{+          nullamt{              acommodity="€"             ,aquantity=0.5             ,astyle=amountstyle{asprecision=Precision 1, asdecimalpoint=Just '.'}             }         }    ,testCase "total price"            $ assertParseEq amountp "$10 @@ €5"-      amount{+      nullamt{          acommodity="$"         ,aquantity=10         ,astyle=amountstyle{asprecision=Precision 0, asdecimalpoint=Nothing}         ,aprice=Just $ TotalPrice $-          amount{+          nullamt{              acommodity="€"             ,aquantity=5             ,astyle=amountstyle{asprecision=Precision 0, asdecimalpoint=Nothing}
Hledger/Read/CsvReader.hs view
@@ -13,9 +13,7 @@ --- ** language {-# LANGUAGE FlexibleContexts     #-} {-# LANGUAGE FlexibleInstances    #-}-{-# LANGUAGE MultiWayIf           #-} {-# LANGUAGE OverloadedStrings    #-}-{-# LANGUAGE PackageImports       #-} {-# LANGUAGE RecordWildCards      #-} {-# LANGUAGE ScopedTypeVariables  #-} {-# LANGUAGE TypeFamilies         #-}@@ -38,7 +36,7 @@  --- ** imports import Control.Applicative        (liftA2)-import Control.Monad              (unless, when)+import Control.Monad              (unless, when, void) import Control.Monad.Except       (ExceptT(..), liftEither, throwError) import qualified Control.Monad.Fail as Fail import Control.Monad.IO.Class     (MonadIO, liftIO)@@ -104,13 +102,13 @@ parse iopts f t = do   let rulesfile = mrules_file_ iopts   readJournalFromCsv rulesfile f t-  -- 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-  <&> journalReverse   -- apply any command line account aliases. Can fail with a bad replacement pattern.   >>= liftEither . journalApplyAliases (aliasesFromOpts iopts)+      -- 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+      . journalReverse   >>= journalFinalise iopts{balancingopts_=(balancingopts_ iopts){ignore_assertions_=True}} f t  --- ** reading rules files@@ -193,14 +191,14 @@ -- Included file paths may be relative to the directory of the provided file path. -- This is done as a pre-parse step to simplify the CSV rules parser. expandIncludes :: FilePath -> Text -> IO Text-expandIncludes dir content = mapM (expandLine dir) (T.lines content) >>= return . T.unlines+expandIncludes dir0 content = mapM (expandLine dir0) (T.lines content) <&> T.unlines   where-    expandLine dir line =+    expandLine dir1 line =       case line of-        (T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< T.readFile f'+        (T.stripPrefix "include " -> Just f) -> expandIncludes dir2 =<< T.readFile f'           where-            f' = dir </> T.unpack (T.dropWhile isSpace f)-            dir' = takeDirectory f'+            f' = dir1 </> T.unpack (T.dropWhile isSpace f)+            dir2 = takeDirectory f'         _ -> return line  -- | An error-throwing IO action that parses this text as CSV conversion rules@@ -257,7 +255,7 @@  instance Eq CsvRules where   r1 == r2 = (rdirectives r1, rcsvfieldindexes r1, rassignments r1) ==-             (rdirectives r2, rcsvfieldindexes r2, rassignments r2) +             (rdirectives r2, rcsvfieldindexes r2, rassignments r2)  -- Custom Show instance used for debug output: omit the rblocksassigning field, which isn't showable. instance Show CsvRules where@@ -582,7 +580,7 @@   newline   body <- flip manyTill (lift eolof) $ do     off <- getOffset-    m <- matcherp' (char sep >> return ())+    m <- matcherp' $ void $ char sep     vs <- T.split (==sep) . T.pack <$> lift restofline     if (length vs /= length fields)       then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d" (length fields) (length vs)) :: String)@@ -745,8 +743,8 @@       -- 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 dbg7 "reversed csv txns" . reverse else id) +        (if newestfirst || mdataseemsnewestfirst == Just True+          then dbg7 "reversed csv txns" . reverse else id)           txns         where           newestfirst = dbg6 "newestfirst" $ isJust $ getDirective "newest-first" rules@@ -757,7 +755,7 @@       -- Second, sort by date.       txns'' = dbg7 "date-sorted csv txns" $ sortBy (comparing tdate) txns' -    liftIO $ when (not rulesfileexists) $ do+    liftIO $ unless rulesfileexists $ do       dbg1IO "creating conversion rules file" rulesfile       T.writeFile rulesfile rulestext @@ -804,7 +802,7 @@ validateCsv rules numhdrlines = validate . applyConditionalSkips . drop numhdrlines . filternulls   where     filternulls = filter (/=[""])-    skipCount r =+    skipnum r =       case (getEffectiveAssignment rules r "end", getEffectiveAssignment rules r "skip") of         (Nothing, Nothing) -> Nothing         (Just _, _) -> Just maxBound@@ -812,7 +810,7 @@         (Nothing, Just x) -> Just (read $ T.unpack x)     applyConditionalSkips [] = []     applyConditionalSkips (r:rest) =-      case skipCount r of+      case skipnum r of         Nothing -> r:(applyConditionalSkips rest)         Just cnt -> applyConditionalSkips (drop (cnt-1) rest)     validate [] = Right []@@ -869,15 +867,15 @@     field    = hledgerField      rules record :: HledgerFieldName -> Maybe FieldTemplate     fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text     parsedate = parseDateWithCustomOrDefaultFormats (rule "date-format")-    mkdateerror datefield datevalue mdateformat = T.unpack $ T.unlines+    mkdateerror datefield datevalue mdateformat' = T.unpack $ T.unlines       ["error: could not parse \""<>datevalue<>"\" as a date using date format "-        <>maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" (T.pack . show) mdateformat+        <>maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" (T.pack . show) mdateformat'       ,showRecord record       ,"the "<>datefield<>" rule is:   "<>(fromMaybe "required, but missing" $ field datefield)-      ,"the date-format is: "<>fromMaybe "unspecified" mdateformat+      ,"the date-format is: "<>fromMaybe "unspecified" mdateformat'       ,"you may need to "         <>"change your "<>datefield<>" rule, "-        <>maybe "add a" (const "change your") mdateformat<>" date-format rule, "+        <>maybe "add a" (const "change your") mdateformat'<>" date-format rule, "         <>"or "<>maybe "add a" (const "change your") mskip<>" skip rule"       ,"for m/d/y or d/m/y dates, use date-format %-m/%-d/%Y or date-format %-d/%-m/%Y"       ]@@ -894,7 +892,7 @@     -- PARTIAL:     date'       = fromMaybe (error' $ mkdateerror "date" date mdateformat) $ parsedate date     mdate2      = fieldval "date2"-    mdate2'     = maybe Nothing (maybe (error' $ mkdateerror "date2" (fromMaybe "" mdate2) mdateformat) Just . parsedate) mdate2+    mdate2'     = (maybe (error' $ mkdateerror "date2" (fromMaybe "" mdate2) mdateformat) Just . parsedate) =<< mdate2     status      =       case fieldval "status" of         Nothing -> Unmarked@@ -904,12 +902,12 @@               ["error: could not parse \""<>s<>"\" as a cleared status (should be *, ! or empty)"               ,"the parse error is:      "<>T.pack (customErrorBundlePretty err)               ]-    code        = maybe "" singleline $ fieldval "code"-    description = maybe "" singleline $ fieldval "description"+    code        = maybe "" singleline' $ fieldval "code"+    description = maybe "" singleline' $ fieldval "description"     comment     = maybe "" unescapeNewlines $ fieldval "comment"     precomment  = maybe "" unescapeNewlines $ fieldval "precomment" -    singleline = T.unwords . filter (not . T.null) . map T.strip . T.lines+    singleline' = T.unwords . filter (not . T.null) . map T.strip . T.lines     unescapeNewlines = T.intercalate "\n" . T.splitOn "\\n"      ----------------------------------------------------------------------@@ -918,7 +916,7 @@      p1IsVirtual = (accountNamePostingType <$> fieldval "account1") == Just VirtualPosting     ps = [p | n <- [1..maxpostings]-         ,let comment  = maybe "" unescapeNewlines $ fieldval ("comment"<> T.pack (show n))+         ,let cmt  = maybe "" unescapeNewlines $ fieldval ("comment"<> T.pack (show n))          ,let currency = fromMaybe "" (fieldval ("currency"<> T.pack (show n)) <|> fieldval "currency")          ,let mamount  = getAmount rules record currency p1IsVirtual n          ,let mbalance = getBalance rules record currency n@@ -930,7 +928,7 @@                              ,pamount           = fromMaybe missingmixedamt mamount                              ,ptransaction      = Just t                              ,pbalanceassertion = mkBalanceAssertion rules record <$> mbalance-                             ,pcomment          = comment+                             ,pcomment          = cmt                              ,ptype             = accountNamePostingType acct                              }          ]@@ -967,7 +965,7 @@     unnumberedfieldnames = ["amount","amount-in","amount-out"]      -- amount field names which can affect this posting-    fieldnames = map (("amount"<> T.pack(show n))<>) ["","-in","-out"]+    fieldnames = map (("amount"<> T.pack (show n))<>) ["","-in","-out"]                  -- For posting 1, also recognise the old amount/amount-in/amount-out names.                  -- For posting 2, the same but only if posting 1 needs balancing.                  ++ if n==1 || n==2 && not p1IsVirtual then unnumberedfieldnames else []@@ -999,17 +997,53 @@   in case discardExcessZeros $ discardUnnumbered assignments of       []      -> Nothing       [(f,a)] -> Just $ negateIfOut f a-      fs      -> error' . T.unpack . T.unlines $  -- PARTIAL:-        ["multiple non-zero amounts assigned,"-        ,"please ensure just one. (https://hledger.org/csv.html#amount)"-        ,"  " <> showRecord record-        ,"  for posting: " <> T.pack (show n)+      fs      -> error' . T.unpack . textChomp . T.unlines $  -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+          -- PARTIAL:+        ["in CSV rules:"+        ,"While processing " <> showRecord record+        ,"while calculating amount for posting " <> T.pack (show n)         ] ++-        ["  assignment: " <> f <> " " <>+        ["rule \"" <> f <> " " <>           fromMaybe "" (hledgerField rules record f) <>-          "\t=> value: " <> wbToText (showMixedAmountB noColour a) -- XXX not sure this is showing all the right info-        | (f,a) <- fs]-+          "\" assigned value \"" <> wbToText (showMixedAmountB noColour a) <> "\"" -- XXX not sure this is showing all the right info+          | (f,a) <- fs+        ] +++        [""+        ,"Multiple non-zero amounts were assigned for an amount field."+        ,"Please ensure just one non-zero amount is assigned, perhaps with an if rule."+        ,"See also: https://hledger.org/hledger.html#setting-amounts"+        ,"(hledger manual -> CSV format -> Tips -> Setting amounts)"+        ] -- | Figure out the expected balance (assertion or assignment) specified for posting N, -- if any (and its parse position). getBalance :: CsvRules -> CsvRecord -> Text -> Int -> Maybe (Amount, SourcePos)@@ -1033,6 +1067,37 @@ parseAmount :: CsvRules -> CsvRecord -> Text -> Text -> MixedAmount parseAmount rules record currency s =     either mkerror mixedAmount $  -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:+      -- PARTIAL:     runParser (evalStateT (amountp <* eof) journalparsestate) "" $     currency <> simplifySign s   where@@ -1063,8 +1128,8 @@                   -- the csv record's line number would be good   where     journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules}-    mkerror n s e = error' . T.unpack $ T.unlines-      ["error: could not parse \"" <> s <> "\" as balance"<> T.pack (show n) <> " amount"+    mkerror n' s' e = error' . T.unpack $ T.unlines+      ["error: could not parse \"" <> s' <> "\" as balance"<> T.pack (show n') <> " amount"       ,showRecord record       ,showRules rules record       -- ,"the default-currency is: "++fromMaybe "unspecified" mdefaultcurrency@@ -1183,7 +1248,7 @@  -- | Show a (approximate) recreation of the original CSV record. showRecord :: CsvRecord -> Text-showRecord r = "record values: "<>T.intercalate "," (map (wrap "\"" "\"") r)+showRecord r = "CSV record: "<>T.intercalate "," (map (wrap "\"" "\"") r)  -- | Given the conversion rules, a CSV record and a hledger field name, find -- the value template ultimately assigned to this field, if any, by a field@@ -1247,17 +1312,19 @@ -- column number, ("date" or "1"), from the given CSV record, if such a field exists. csvFieldValue :: CsvRules -> CsvRecord -> CsvFieldName -> Maybe Text csvFieldValue rules record fieldname = do-  fieldindex <- if | T.all isDigit fieldname -> readMay $ T.unpack fieldname-                   | otherwise               -> lookup (T.toLower fieldname) $ rcsvfieldindexes rules+  fieldindex <-+    if T.all isDigit fieldname +    then readMay $ T.unpack fieldname+    else lookup (T.toLower fieldname) $ rcsvfieldindexes rules   T.strip <$> atMay record (fieldindex-1)  -- | Parse the date string using the specified date-format, or if unspecified -- the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, leading -- zeroes optional). parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> Text -> Maybe Day-parseDateWithCustomOrDefaultFormats mformat s = asum $ map parsewith formats+parseDateWithCustomOrDefaultFormats mformat s = asum $ map parsewith' formats   where-    parsewith = flip (parseTimeM True defaultTimeLocale) (T.unpack s)+    parsewith' = flip (parseTimeM True defaultTimeLocale) (T.unpack s)     formats = map T.unpack $ maybe                ["%Y/%-m/%-d"                ,"%Y-%-m-%-d"@@ -1294,6 +1361,37 @@    ]   ,testGroup "conditionalblockp" [     testCase "space after conditional" $ -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120+       -- #1120       parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=         (Right $ CB{cbMatchers=[RecordMatcher None $ toRegexCI' "a"],cbAssignments=[("account2","b")]}) 
Hledger/Read/InputOptions.hs view
@@ -37,6 +37,7 @@     ,reportspan_        :: DateSpan             -- ^ a dirty hack keeping the query dates in InputOpts. This rightfully lives in ReportSpec, but is duplicated here.     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed     ,infer_equity_      :: Bool                 -- ^ generate automatic equity postings from transaction prices+    ,infer_costs_       :: Bool                 -- ^ infer transaction prices from equity conversion postings     ,balancingopts_     :: BalancingOpts        -- ^ options for balancing transactions     ,strict_            :: Bool                 -- ^ do extra error checking (eg, all posted accounts are declared, no prices are inferred)     ,_ioDay             :: Day                  -- ^ today's date, for use with forecast transactions  XXX this duplicates _rsDay, and should eventually be removed when it's not needed anymore.@@ -55,6 +56,7 @@     , reportspan_        = nulldatespan     , auto_              = False     , infer_equity_      = False+    , infer_costs_       = False     , balancingopts_     = defbalancingopts     , strict_            = False     , _ioDay             = nulldate
Hledger/Read/JournalReader.hs view
@@ -185,7 +185,7 @@ -- | Parse and post-process a "Journal" from hledger's journal file -- format, or give an error. parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal-parse iopts = parseAndFinaliseJournal journalp' iopts+parse iopts f = parseAndFinaliseJournal journalp' iopts f   where     journalp' = do       -- reverse parsed aliases to ensure that they are applied in order given on commandline@@ -199,7 +199,7 @@ -- which should be finalised/validated before use. -- -- >>> rejp (journalp <* eof) "2015/1/1\n a  0\n"--- Right (Right Journal  with 1 transactions, 1 accounts)+-- Right (Right Journal (unknown) with 1 transactions, 1 accounts) -- journalp :: MonadIO m => ErroringJournalParser m ParsedJournal journalp = do@@ -261,8 +261,8 @@   prefixedglob <- T.unpack <$> takeWhileP Nothing (/= '\n') -- don't consume newline yet   parentoff <- getOffset   parentpos <- getSourcePos-  let (mprefix,glob) = splitReaderPrefix prefixedglob-  paths <- getFilePaths parentoff parentpos glob+  let (mprefix,glb) = splitReaderPrefix prefixedglob+  paths <- getFilePaths parentoff parentpos glb   let prefixedpaths = case mprefix of         Nothing  -> paths         Just fmt -> map ((fmt++":")++) paths@@ -298,21 +298,41 @@       when (filepath `elem` parentfilestack) $         Fail.fail ("Cyclic include: " ++ filepath) -      childInput <- lift $ readFilePortably filepath-                            `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)+      childInput <-+        traceAt 6 ("parseChild: "++takeFileName filepath) $+        lift $ readFilePortably filepath+          `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)       let initChildj = newJournalWithParseStateFrom filepath parentj -      -- Choose a reader/format based on the file path, or fall back-      -- on journal. Duplicating readJournal a bit here.+      -- Choose a reader/parser based on the file path prefix or file extension,+      -- defaulting to JournalReader. Duplicating readJournal a bit here.       let r = fromMaybe reader $ findReader Nothing (Just prefixedpath)           parser = rParser r-      dbg6IO "trying reader" (rFormat r)+      dbg6IO "parseChild: trying reader" (rFormat r)++      -- Parse the file (of whichever format) to a Journal, with file path and source text attached.       updatedChildj <- journalAddFile (filepath, childInput) <$>                         parseIncludeFile parser initChildj filepath childInput -      -- discard child's parse info,  combine other fields-      put $ updatedChildj <> parentj+      -- Merge this child journal into the parent journal+      -- (with debug logging for troubleshooting account display order).+      -- The parent journal is the second argument to journalConcat; this means+      -- its parse state is kept, and its lists are appended to child's (which+      -- ultimately produces the right list order, because parent's and child's+      -- lists are in reverse order at this stage. Cf #1909).+      let+        parentj' =+          dbgJournalAcctDeclOrder ("parseChild: child " <> childfilename <> " acct decls: ") updatedChildj+          `journalConcat`+          dbgJournalAcctDeclOrder ("parseChild: parent " <> parentfilename <> " acct decls: ") parentj +          where+            childfilename = takeFileName filepath+            parentfilename = maybe "(unknown)" takeFileName $ headMay $ jincludefilestack parentj  -- XXX more accurate than journalFilePath for some reason++      -- Update the parse state.+      put parentj'+     newJournalWithParseStateFrom :: FilePath -> Journal -> Journal     newJournalWithParseStateFrom filepath j = nulljournal{       jparsedefaultyear      = jparsedefaultyear j@@ -340,6 +360,7 @@ accountdirectivep :: JournalParser m () accountdirectivep = do   off <- getOffset -- XXX figure out a more precise position later+  pos <- getSourcePos    string "account"   lift skipNonNewlineSpaces1@@ -359,7 +380,7 @@     metype = parseAccountTypeCode <$> lookup accountTypeTagName tags    -- update the journal-  addAccountDeclaration (acct, cmt, tags)+  addAccountDeclaration (acct, cmt, tags, pos)   unless (null tags) $ addDeclaredAccountTags acct tags   case metype of     Nothing         -> return ()@@ -392,15 +413,16 @@             T.intercalate ", " ["A","L","E","R","X","C","V","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"]  -- Add an account declaration to the journal, auto-numbering it.-addAccountDeclaration :: (AccountName,Text,[Tag]) -> JournalParser m ()-addAccountDeclaration (a,cmt,tags) =+addAccountDeclaration :: (AccountName,Text,[Tag],SourcePos) -> JournalParser m ()+addAccountDeclaration (a,cmt,tags,pos) = do   modify' (\j ->              let                decls = jdeclaredaccounts j                d     = (a, nullaccountdeclarationinfo{                               adicomment          = cmt                              ,aditags             = tags-                             ,adideclarationorder = length decls + 1+                             ,adideclarationorder = length decls + 1  -- gets renumbered when Journals are finalised or merged+                             ,adisourcepos        = pos                              })              in                j{jdeclaredaccounts = d:decls})@@ -438,8 +460,8 @@     string "commodity"     lift skipNonNewlineSpaces1     off <- getOffset-    amount <- amountp-    pure $ (off, amount)+    amt <- amountp+    pure $ (off, amt)   lift skipNonNewlineSpaces   _ <- lift followingcommentp   let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg6 "style from commodity directive" astyle}@@ -467,8 +489,8 @@   lift skipNonNewlineSpaces1   sym <- lift commoditysymbolp   _ <- lift followingcommentp-  mformat <- lastMay <$> many (indented $ formatdirectivep sym)-  let comm = Commodity{csymbol=sym, cformat=mformat}+  mfmt <- lastMay <$> many (indented $ formatdirectivep sym)+  let comm = Commodity{csymbol=sym, cformat=mfmt}   modify' (\j -> j{jcommodities=M.insert sym comm $ jcommodities j})   where     indented = (lift skipNonNewlineSpaces1 >>)@@ -491,10 +513,10 @@          printf "commodity directive symbol \"%s\" and format directive symbol \"%s\" should be the same" expectedsym acommodity  keywordp :: String -> JournalParser m ()-keywordp = (() <$) . string . fromString+keywordp = void . string . fromString  spacesp :: JournalParser m ()-spacesp = () <$ lift skipNonNewlineSpaces1+spacesp = void $ lift skipNonNewlineSpaces1  -- | Backtracking parser similar to string, but allows varying amount of space between words keywordsp :: String -> JournalParser m ()@@ -652,7 +674,7 @@   -- first parsing with 'singlespacedtextp', then "re-parsing" with   -- 'periodexprp' saves 'periodexprp' from having to respect the single-   -- and double-space parsing rules-  (interval, span) <- lift $ reparseExcerpt periodExcerpt $ do+  (interval, spn) <- lift $ reparseExcerpt periodExcerpt $ do     pexp <- periodexprp refdate     (<|>) eof $ do       offset1 <- getOffset@@ -665,7 +687,7 @@     pure pexp    -- In periodic transactions, the period expression has an additional constraint:-  case checkPeriodicTransactionStartDate interval span periodtxt of+  case checkPeriodicTransactionStartDate interval spn periodtxt of     Just e -> customFailure $ parseErrorAt off e     Nothing -> pure () @@ -679,7 +701,7 @@   return $ nullperiodictransaction{      ptperiodexpr=periodtxt     ,ptinterval=interval-    ,ptspan=span+    ,ptspan=spn     ,ptstatus=status     ,ptcode=code     ,ptdescription=description@@ -745,7 +767,7 @@     let (ptype, account') = (accountNamePostingType account, textUnbracket account)     lift skipNonNewlineSpaces     mult <- if isPostingRule then multiplierp else pure False-    amount <- optional $ amountpwithmultiplier mult+    amt <- optional $ amountpwithmultiplier mult     lift skipNonNewlineSpaces     massertion <- optional balanceassertionp     lift skipNonNewlineSpaces@@ -755,7 +777,7 @@             , pdate2=mdate2             , pstatus=status             , paccount=account'-            , pamount=maybe missingmixedamt mixedAmount amount+            , pamount=maybe missingmixedamt mixedAmount amt             , pcomment=comment             , ptype=ptype             , ptags=tags@@ -794,8 +816,8 @@     ]   ,testCase "datetimep" $ do      let-       good = assertParse datetimep-       bad  = (\t -> assertParseError datetimep t "")+       good  = assertParse datetimep+       bad t = assertParseError datetimep t ""      good "2011/1/1 00:00"      good "2011/1/1 23:59:59"      bad "2011/1/1"@@ -1010,6 +1032,7 @@         [("a:b", AccountDeclarationInfo{adicomment          = "type:asset\n"                                        ,aditags             = [("type","asset")]                                        ,adideclarationorder = 1+                                       ,adisourcepos        = fst nullsourcepos                                        })         ]       ]
Hledger/Read/TimedotReader.hs view
@@ -28,7 +28,6 @@  --- ** language {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PackageImports #-}  --- ** exports module Hledger.Read.TimedotReader (@@ -173,7 +172,7 @@   lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1]   a <- modifiedaccountnamep   lift skipNonNewlineSpaces-  hrs <-+  hours <-     try (lift followingcommentp >> return 0)     <|> (lift durationp <*          (try (lift followingcommentp) <|> (newline >> return "")))@@ -187,7 +186,7 @@           tstatus    = Cleared,           tpostings  = [             nullposting{paccount=a-                      ,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hrs, astyle=s}+                      ,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hours, astyle=s}                       ,ptype=VirtualPosting                       ,ptransaction=Just t                       }
Hledger/Reports/AccountTransactionsReport.hs view
@@ -153,7 +153,7 @@         datelessreportq = filterQuery (not . queryIsDateOrDate2) reportq      items =-        accountTransactionsReportItems reportq thisacctq startbal maNegate+        accountTransactionsReportItems reportq thisacctq startbal maNegate (journalAccountType j)       -- sort by the transaction's register date, then index, for accurate starting balance       . ptraceAtWith 5 (("ts4:\n"++).pshowTransactions.map snd)       . sortBy (comparing (Down . fst) <> comparing (Down . tindex . snd))@@ -171,19 +171,21 @@ -- in the report. This is not necessarily the transaction date - see -- transactionRegisterDate. accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount)-                               -> [(Day, Transaction)] -> [AccountTransactionsReportItem]-accountTransactionsReportItems reportq thisacctq bal signfn =-    catMaybes . snd . mapAccumR (accountTransactionsReportItem reportq thisacctq signfn) bal+                               -> (AccountName -> Maybe AccountType) -> [(Day, Transaction)]+                               -> [AccountTransactionsReportItem]+accountTransactionsReportItems reportq thisacctq bal signfn accttypefn =+    catMaybes . snd . mapAccumR (accountTransactionsReportItem reportq thisacctq signfn accttypefn) bal -accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount) -> MixedAmount-                              -> (Day, Transaction) -> (MixedAmount, Maybe AccountTransactionsReportItem)-accountTransactionsReportItem reportq thisacctq signfn bal (d, torig)+accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount)+                              -> (AccountName -> Maybe AccountType) -> MixedAmount -> (Day, Transaction)+                              -> (MixedAmount, Maybe AccountTransactionsReportItem)+accountTransactionsReportItem reportq thisacctq signfn accttypefn bal (d, torig)     -- 201407: I've lost my grip on this, let's just hope for the best     -- 201606: we now calculate change and balance from filtered postings, check this still works well for all callers XXX     | null reportps = (bal, Nothing)  -- no matched postings in this transaction, skip it     | otherwise     = (b, Just (torig, tacct{tdate=d}, numotheraccts > 1, otheracctstr, a, b))     where-      tacct@Transaction{tpostings=reportps} = filterTransactionPostings reportq torig  -- TODO needs to consider --date2, #1731+      tacct@Transaction{tpostings=reportps} = filterTransactionPostingsExtra accttypefn reportq torig  -- TODO needs to consider --date2, #1731       (thisacctps, otheracctps) = partition (matchesPosting thisacctq) reportps       numotheraccts = length $ nub $ map paccount otheracctps       otheracctstr | thisacctq == None  = summarisePostingAccounts reportps     -- no current account ? summarise all matched postings@@ -243,8 +245,8 @@ -- balance amount) components that don't involve the specified -- commodity. Other item fields such as the transaction are left unchanged. filterAccountTransactionsReportByCommodity :: CommoditySymbol -> AccountTransactionsReport -> AccountTransactionsReport-filterAccountTransactionsReportByCommodity c =-    fixTransactionsReportItemBalances . concatMap (filterTransactionsReportItemByCommodity c)+filterAccountTransactionsReportByCommodity comm =+    fixTransactionsReportItemBalances . concatMap (filterTransactionsReportItemByCommodity comm)   where     filterTransactionsReportItemByCommodity c (t,t2,s,o,a,bal)       | c `elem` cs = [item']@@ -259,9 +261,9 @@     fixTransactionsReportItemBalances items = reverse $ i:(go startbal is)       where         i:is = reverse items-        startbal = filterMixedAmountByCommodity c $ triBalance i+        startbal = filterMixedAmountByCommodity comm $ triBalance i         go _ [] = []-        go bal ((t,t2,s,o,amt,_):is) = (t,t2,s,o,amt,bal'):go bal' is+        go bal ((t,t2,s,o,amt,_):is') = (t,t2,s,o,amt,bal'):go bal' is'           where bal' = bal `maPlus` amt  -- tests
Hledger/Reports/BudgetReport.hs view
@@ -26,9 +26,9 @@ import Data.Function (on) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM-import Data.List (find, partition, transpose, foldl')+import Data.List (find, partition, transpose, foldl', maximumBy) import Data.List.Extra (nubSort)-import Data.Maybe (fromMaybe, catMaybes)+import Data.Maybe (fromMaybe, catMaybes, isJust) import Data.Map (Map) import qualified Data.Map as Map import qualified Data.Set as S@@ -36,6 +36,7 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB+import Safe (minimumDef) --import System.Console.CmdArgs.Explicit as C --import Lucid as L import qualified Text.Tabular.AsciiWide as Tab@@ -45,6 +46,8 @@ import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes import Hledger.Reports.MultiBalanceReport+import Data.Ord (comparing)+import Control.Monad ((>=>))   type BudgetGoal    = Change@@ -96,27 +99,63 @@       | otherwise = budgetgoalreport     budgetreport = combineBudgetAndActual ropts j budgetgoalreport' actualreport --- | Use all periodic transactions in the journal to generate--- 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.+-- | Use all (or all matched by --budget's argument) periodic transactions in the journal +-- to generate budget goal transactions in the specified date span (and before, to support+-- --historical. The precise start date is the natural start date of the largest interval+-- of the active periodic transaction rules that is on or before the earlier of journal start date,+-- report start date.)+-- 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 :: BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal journalAddBudgetGoalTransactions bopts ropts reportspan j =-  either error' id $ journalBalanceTransactions bopts j{ jtxns = budgetts }  -- PARTIAL:+  either error' id $  -- PARTIAL:+    (journalApplyCommodityStyles >=> journalBalanceTransactions bopts) j{ jtxns = budgetts }   where-    budgetspan = dbg3 "budget span" $ reportspan-    pat = fromMaybe "" $ dbg3 "budget pattern" $ T.toLower <$> budgetpat_ ropts+    budgetspan = dbg3 "budget span" $ DateSpan mbudgetgoalsstartdate (spanEnd reportspan)+      where+        mbudgetgoalsstartdate =+          -- We want to also generate budget goal txns before the report start date, in case -H is used.+          -- What should the actual starting date for goal txns be ? This gets tricky. +          -- Consider a journal with a "~ monthly" periodic transaction rule, where the first transaction is on 1/5.+          -- Users will certainly expect a budget goal for january, but "~ monthly" generates transactions+          -- on the first of month, and starting from 1/5 would exclude 1/1.+          -- Secondly, consider a rule like "~ every february 2nd from 2020/01"; we should not start that+          -- before 2020-02-02.+          -- Hopefully the following algorithm produces intuitive behaviour in general:+          -- from the earlier of the journal start date and the report start date,+          -- move backward to the nearest natural start date of the largest period seen among the+          -- active periodic transactions, unless that is disallowed by a start date in the periodic rule.+          -- (Do we need to pay attention to an end date in the rule ? Don't think so.)+          -- (So with "~ monthly", the journal start date 1/5 is adjusted to 1/1.)+          case minimumDef Nothing $ filter isJust [journalStartDate False j, spanStart reportspan] of+            Nothing -> Nothing+            Just d  -> Just d'+              where+                -- the interval and any date span of the periodic transaction with longest period+                (intervl, spn) =+                  case budgetpts of+                    []  -> (Days 1, nulldatespan)+                    pts -> (ptinterval pt, ptspan pt)+                      where pt = maximumBy (comparing ptinterval) pts  -- PARTIAL: maximumBy won't fail+                -- the natural start of this interval on or before the journal/report start+                intervalstart = intervalStartBefore intervl d+                -- the natural interval start before the journal/report start,+                -- or the rule-specified start if later,+                -- but no later than the journal/report start.+                d' = min d $ maybe intervalstart (max intervalstart) $ spanStart spn+     -- select periodic transactions matching a pattern     -- (the argument of the (final) --budget option).     -- XXX two limitations/wishes, requiring more extensive type changes:     -- - give an error if pat is non-null and matches no periodic txns     -- - allow a regexp or a full hledger query, not just a substring+    pat = fromMaybe "" $ dbg3 "budget pattern" $ T.toLower <$> budgetpat_ ropts+    budgetpts = [pt | pt <- jperiodictxns j, pat `T.isInfixOf` T.toLower (ptdescription pt)]     budgetts =       dbg5 "budget goal txns" $       [makeBudgetTxn t-      | pt <- jperiodictxns j-      , pat `T.isInfixOf` T.toLower (ptdescription pt)+      | pt <- budgetpts       , t <- runPeriodicTransaction pt budgetspan       ]     makeBudgetTxn t = txnTieKnot $ t { tdescription = T.pack "Budget transaction" }@@ -269,11 +308,11 @@       | transpose_ = \(Tab.Table rh ch vals) -> Tab.Table ch rh (transpose vals)       | otherwise  = id -    (accts, rows, totalrows) = (accts, prependcs itemscs (padcells texts), prependcs trcs (padtr trtexts))+    (accts, rows, totalrows) = (accts', prependcs itemscs (padcells texts), prependcs trcs (padtr trtexts))       where         shownitems :: [[(AccountName, WideBuilder, BudgetDisplayRow)]]         shownitems = (fmap (\i -> fmap (\(cs, cvals) -> (renderacct i, cs, cvals)) . showrow $ rowToBudgetCells i) items)-        (accts, itemscs, texts) = unzip3 $ concat shownitems+        (accts', itemscs, texts) = unzip3 $ concat shownitems          showntr    :: [[(WideBuilder, BudgetDisplayRow)]]         showntr    = [showrow $ rowToBudgetCells tr]@@ -342,10 +381,8 @@       where         actual' = fromMaybe nullmixedamt actual -        budgetAndPerc b = uncurry zip-          ( showmixed b-          , fmap (wbFromText . T.pack . show . roundTo 0) <$> percbudget actual' b-          )+        budgetAndPerc b = +          zip (showmixed b) (fmap (wbFromText . T.pack . show . roundTo 0) <$> percbudget actual' b)          full           | Just b <- mbudget = Just <$> budgetAndPerc b@@ -358,9 +395,9 @@             (TB.fromText . flip T.replicate " " $ actualwidth - w) <> b          (totalpercentwidth, totalbudgetwidth) =-          let totalpercentwidth = if percentwidth == 0 then 0 else percentwidth + 5-           in ( totalpercentwidth-              , if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth + 3+          let totalpercentwidth' = if percentwidth == 0 then 0 else percentwidth + 5+           in ( totalpercentwidth'+              , if budgetwidth == 0 then 0 else budgetwidth + totalpercentwidth' + 3               )          -- | Display a padded budget string@@ -408,37 +445,43 @@   = (if transpose_ then transpose else id) $    -- heading row+  ++  -- heading row   ("Account" :   ["Commodity" | layout_ == LayoutBare ]-   ++ concatMap (\span -> [showDateSpan span, "budget"]) colspans+   ++ concatMap (\spn -> [showDateSpan spn, "budget"]) colspans    ++ concat [["Total"  ,"budget"] | row_total_]    ++ concat [["Average","budget"] | average_]   ) :    -- account rows+  ++  -- account rows   concatMap (rowAsTexts prrFullName) items    -- totals row   ++ concat [ rowAsTexts (const "Total:") tr | not no_total_ ]    where-    flattentuples abs = concat [[a,b] | (a,b) <- abs]+    flattentuples tups = concat [[a,b] | (a,b) <- tups]     showNorm = maybe "" (wbToText . showMixedAmountB oneLine)      rowAsTexts :: (PeriodicReportRow a BudgetCell -> Text)                -> PeriodicReportRow a BudgetCell                -> [[Text]]     rowAsTexts render row@(PeriodicReportRow _ as (rowtot,budgettot) (rowavg, budgetavg))-      | layout_ /= LayoutBare = [render row : fmap showNorm all]+      | layout_ /= LayoutBare = [render row : fmap showNorm vals]       | otherwise =             joinNames . zipWith (:) cs  -- add symbols and names           . transpose                   -- each row becomes a list of Text quantities           . fmap (fmap wbToText . showMixedAmountLinesB oneLine{displayOrder=Just cs, displayMinWidth=Nothing}                  .fromMaybe nullmixedamt)-          $ all+          $ vals       where-        cs = S.toList . foldl' S.union mempty . fmap maCommodities $ catMaybes all-        all = flattentuples as+        cs = S.toList . foldl' S.union mempty . fmap maCommodities $ catMaybes vals+        vals = flattentuples as             ++ concat [[rowtot, budgettot] | row_total_]             ++ concat [[rowavg, budgetavg] | average_] 
Hledger/Reports/MultiBalanceReport.hs view
@@ -165,15 +165,14 @@             , cbcsubreportincreasestotal             )           where+            ropts = cbcsubreportoptions $ _rsReportOpts rspec             -- Add a restriction to this subreport to the report query.             -- XXX in non-thorough way, consider updateReportSpec ?-            q = cbcsubreportquery j-            ropts = cbcsubreportoptions $ _rsReportOpts rspec-            rspecsub = rspec{_rsReportOpts=ropts, _rsQuery=And [q, _rsQuery rspec]}+            rspecsub = rspec{_rsReportOpts=ropts, _rsQuery=And [cbcsubreportquery, _rsQuery rspec]}             -- Starting balances and column postings specific to this subreport.             startbals' = startingBalances rspecsub j priceoracle $-              filter (matchesPostingExtra (journalAccountType j) q) startps-            colps' = map (second $ filter (matchesPostingExtra (journalAccountType j) q)) colps+              filter (matchesPostingExtra (journalAccountType j) cbcsubreportquery) startps+            colps' = map (second $ filter (matchesPostingExtra (journalAccountType j) cbcsubreportquery)) colps      -- Sum the subreport totals by column. Handle these cases:     -- - no subreports@@ -315,20 +314,22 @@     -- The valued row amounts to be displayed: per-period changes,     -- zero-based cumulative totals, or     -- starting-balance-based historical balances.-    rowbals name changes = dbg5 "rowbals" $ case balanceaccum_ ropts of-        PerPeriod  -> changeamts+    rowbals name unvaluedChanges = dbg5 "rowbals" $ case balanceaccum_ ropts of+        PerPeriod  -> changes         Cumulative -> cumulative         Historical -> historical       where-        -- changes to report on: usually just the changes itself, but use the-        -- differences in the historical amount for ValueChangeReports.-        changeamts = case balancecalc_ ropts of-            CalcChange      -> M.mapWithKey avalue changes-            CalcBudget      -> M.mapWithKey avalue changes+        -- changes to report on: usually just the valued changes themselves, but use the+        -- differences in the valued historical amount for CalcValueChange and CalcGain.+        changes = case balancecalc_ ropts of+            CalcChange      -> M.mapWithKey avalue unvaluedChanges+            CalcBudget      -> M.mapWithKey avalue unvaluedChanges             CalcValueChange -> periodChanges valuedStart historical             CalcGain        -> periodChanges valuedStart historical-        cumulative = cumulativeSum avalue nullacct changeamts-        historical = cumulativeSum avalue startingBalance changes+        -- the historical balance is the valued cumulative sum of all unvalued changes+        historical = M.mapWithKey avalue $ cumulativeSum startingBalance unvaluedChanges+        -- since this is a cumulative sum of valued amounts, it should not be valued again+        cumulative = cumulativeSum nullacct changes         startingBalance = HM.lookupDefault nullacct name startbals         valuedStart = avalue (DateSpan Nothing historicalDate) startingBalance @@ -342,7 +343,7 @@     avalue = acctApplyBoth . mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle     acctApplyBoth f a = a{aibalance = f $ aibalance a, aebalance = f $ aebalance a}     historicalDate = minimumMay $ mapMaybe spanStart colspans-    zeros = M.fromList [(span, nullacct) | span <- colspans]+    zeros = M.fromList [(spn, nullacct) | spn <- colspans]     colspans = map fst colps  @@ -405,11 +406,11 @@                   -> HashMap AccountName (Map DateSpan Account)                   -> HashMap AccountName DisplayName displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts valuedaccts-    | depth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1+    | qdepth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1     | otherwise  = HM.mapWithKey (\a _ -> displayedName a) displayedAccts   where     -- Accounts which are to be displayed-    displayedAccts = (if depth == 0 then id else HM.filterWithKey keep) valuedaccts+    displayedAccts = (if qdepth == 0 then id else HM.filterWithKey keep) valuedaccts       where         keep name amts = isInteresting name amts || name `HM.member` interestingParents @@ -428,7 +429,7 @@      -- Accounts interesting for their own sake     isInteresting name amts =-        d <= depth                                 -- Throw out anything too deep+        d <= qdepth                                  -- Throw out anything too deep         && ( name `Set.member` unelidableaccts     -- Unelidable accounts should be kept unless too deep            ||(empty_ ropts && keepWhenEmpty amts)  -- Keep empty accounts when called with --empty            || not (isZeroRow balance amts)         -- Keep everything with a non-zero balance in the row@@ -439,8 +440,8 @@             ALFlat -> const True          -- Keep all empty accounts in flat mode             ALTree -> all (null . asubs)  -- Keep only empty leaves in tree mode         balance = maybeStripPrices . case accountlistmode_ ropts of-            ALTree | d == depth -> aibalance-            _                   -> aebalance+            ALTree | d == qdepth -> aibalance+            _                    -> aebalance           where maybeStripPrices = if conversionop_ ropts == Just NoConversionOp then id else mixedAmountStripPrices      -- Accounts interesting because they are a fork for interesting subaccounts@@ -452,7 +453,7 @@         minSubs = if no_elide_ ropts then 1 else 2      isZeroRow balance = all (mixedAmountLooksZero . balance)-    depth = fromMaybe maxBound $  queryDepth query+    qdepth = fromMaybe maxBound $ queryDepth query     numSubs = subaccountTallies . HM.keys $ HM.filterWithKey isInteresting valuedaccts  -- | Sort the rows by amount or by account declaration order.@@ -533,10 +534,10 @@              -> HashMap AccountName (Map DateSpan a) transposeMap = foldr (uncurry addSpan) mempty   where-    addSpan span acctmap seen = HM.foldrWithKey (addAcctSpan span) seen acctmap+    addSpan spn acctmap seen = HM.foldrWithKey (addAcctSpan spn) seen acctmap -    addAcctSpan span acct a = HM.alter f acct-      where f = Just . M.insert span a . fromMaybe mempty+    addAcctSpan spn acct a = HM.alter f acct+      where f = Just . M.insert spn a . fromMaybe mempty  -- | A sorting helper: sort a list of things (eg report rows) keyed by account name -- to match the provided ordering of those same account names.@@ -580,11 +581,9 @@     M.fromDistinctAscList . zip dates $ zipWith subtractAcct amts (start:amts)   where (dates, amts) = unzip $ M.toAscList amtmap --- | Calculate a cumulative sum from a list of period changes and a valuation--- function.-cumulativeSum :: (DateSpan -> Account -> Account) -> Account -> Map DateSpan Account -> Map DateSpan Account-cumulativeSum value start = snd . M.mapAccumWithKey accumValued start-  where accumValued startAmt date newAmt = let s = sumAcct startAmt newAmt in (s, value date s)+-- | Calculate a cumulative sum from a list of period changes.+cumulativeSum :: Account -> Map DateSpan Account -> Map DateSpan Account+cumulativeSum start = snd . M.mapAccum (\a b -> let s = sumAcct a b in (s, s)) start  -- | Given a table representing a multi-column balance report (for example, -- made using 'balanceReportAsTable'), render it in a format suitable for
Hledger/Reports/PostingsReport.hs view
@@ -73,8 +73,8 @@        -- Postings, or summary postings with their subperiod's end date, to be displayed.       displayps :: [(Posting, Maybe Period)]-        | multiperiod = [(p, Just period) | (p, period) <- summariseps reportps]-        | otherwise   = [(p, Nothing) | p <- reportps]+        | multiperiod = [(p', Just period') | (p', period') <- summariseps reportps]+        | otherwise   = [(p', Nothing) | p' <- reportps]         where           summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans           showempty = empty_ || average_@@ -189,9 +189,9 @@ -- with 0 amount. -- summarisePostingsInDateSpan :: DateSpan -> WhichDate -> Maybe Int -> Bool -> [Posting] -> [SummaryPosting]-summarisePostingsInDateSpan span@(DateSpan b e) wd mdepth showempty ps+summarisePostingsInDateSpan spn@(DateSpan b e) wd mdepth showempty ps   | null ps && (isNothing b || isNothing e) = []-  | null ps && showempty = [(summaryp, dateSpanAsPeriod span)]+  | null ps && showempty = [(summaryp, dateSpanAsPeriod spn)]   | otherwise = summarypes   where     postingdate = if wd == PrimaryDate then postingDate else postingDate2@@ -200,14 +200,14 @@     clippedanames = nub $ map (clipAccountName mdepth) anames     summaryps | mdepth == Just 0 = [summaryp{paccount="...",pamount=sumPostings ps}]               | otherwise        = [summaryp{paccount=a,pamount=balance a} | a <- clippedanames]-    summarypes = map (, dateSpanAsPeriod span) $ (if showempty then id else filter (not . mixedAmountLooksZero . pamount)) summaryps+    summarypes = map (, dateSpanAsPeriod spn) $ (if showempty then id else filter (not . mixedAmountLooksZero . pamount)) summaryps     anames = nubSort $ map paccount ps     -- aggregate balances by account, like ledgerFromJournal, then do depth-clipping     accts = accountsFromPostings ps     balance a = maybe nullmixedamt bal $ lookupAccount a accts       where         bal = if isclipped a then aibalance else aebalance-        isclipped a = maybe False (accountNameLevel a >=) mdepth+        isclipped a' = maybe False (accountNameLevel a' >=) mdepth  negatePostingAmount :: Posting -> Posting negatePostingAmount = postingTransformAmount negate
Hledger/Reports/ReportOptions.hs view
@@ -292,8 +292,8 @@  -- | Set the default ConversionOp. setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec-setDefaultConversionOp def rspec@ReportSpec{_rsReportOpts=ropts} =-    rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just def}}+setDefaultConversionOp defop rspec@ReportSpec{_rsReportOpts=ropts} =+    rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just defop}}  accountlistmodeopt :: RawOpts -> AccountListMode accountlistmodeopt =@@ -360,7 +360,7 @@         (s,n) = break (==',') $ map toLower opt         w = case drop 1 n of               "" -> Nothing-              c | Just w <- readMay c -> Just w+              c | Just w' <- readMay c -> Just w'               _ -> usageError "width in --layout=wide,WIDTH must be an integer"          err = usageError "--layout's argument should be \"wide[,WIDTH]\", \"tall\", \"bare\", or \"tidy\""@@ -390,14 +390,14 @@ beginDatesFromRawOpts :: Day -> RawOpts -> [Day] beginDatesFromRawOpts d = collectopts (begindatefromrawopt d)   where-    begindatefromrawopt d (n,v)+    begindatefromrawopt d' (n,v)       | n == "begin" =           either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $-          fixSmartDateStrEither' d (T.pack v)+          fixSmartDateStrEither' d' (T.pack v)       | n == "period" =         case           either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $-          parsePeriodExpr d (stripquotes $ T.pack v)+          parsePeriodExpr d' (stripquotes $ T.pack v)         of           (_, DateSpan (Just b) _) -> Just b           _                        -> Nothing@@ -408,14 +408,14 @@ endDatesFromRawOpts :: Day -> RawOpts -> [Day] endDatesFromRawOpts d = collectopts (enddatefromrawopt d)   where-    enddatefromrawopt d (n,v)+    enddatefromrawopt d' (n,v)       | n == "end" =           either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $-          fixSmartDateStrEither' d (T.pack v)+          fixSmartDateStrEither' d' (T.pack v)       | n == "period" =         case           either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $-          parsePeriodExpr d (stripquotes $ T.pack v)+          parsePeriodExpr d' (stripquotes $ T.pack v)         of           (_, DateSpan _ (Just e)) -> Just e           _                        -> Nothing@@ -589,12 +589,12 @@       CalcGain -> journalMapPostings (\p -> postingTransformAmount (gain p) p) j       _        -> journalMapPostings (\p -> postingTransformAmount (valuation p) p) $ costing j   where-    valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts)-    gain      p = maybe id (mixedAmountApplyGain      priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts)+    valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)+    gain      p = maybe id (mixedAmountApplyGain      priceoracle styles (postingperiodend p) (_rsDay rspec) (postingDate p)) (value_ ropts)     costing     = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts)      -- Find the end of the period containing this posting-    periodEnd  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts)+    postingperiodend  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts)     mPeriodEnd = case interval_ ropts of         NoInterval -> const . spanEnd . fst $ reportSpan j rspec         _          -> spanEnd <=< latestSpanContaining (historical : spans)@@ -611,11 +611,11 @@     case valuationAfterSum ropts of         Just mc -> case balancecalc_ ropts of             CalcGain -> gain mc-            _        -> \span -> valuation mc span . costing+            _        -> \spn -> valuation mc spn . costing         Nothing      -> const id   where-    valuation mc span = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)-    gain mc span = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)+    valuation mc spn = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)+    gain mc spn = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd spn)     costing = case fromMaybe NoConversionOp $ conversionop_ ropts of         NoConversionOp -> id         ToCost         -> styleMixedAmount styles . mixedAmountCost@@ -797,7 +797,7 @@ -- >>> _rsQuery <$> setEither querystring ["assets"] defreportspec -- Right (Acct (RegexpCI "assets")) -- >>> _rsQuery <$> setEither querystring ["(assets"] defreportspec--- Left "this regular expression could not be compiled: (assets"+-- Left "This regular expression is malformed... -- >>> _rsQuery $ set querystring ["assets"] defreportspec -- Acct (RegexpCI "assets") -- >>> _rsQuery $ set querystring ["(assets"] defreportspec@@ -808,6 +808,8 @@     reportOpts :: ReportableLens' a ReportOpts     reportOpts = reportOptsNoUpdate     {-# INLINE reportOpts #-}++    -- XXX these names are a bit clashy      period :: ReportableLens' a Period     period = reportOpts.periodNoUpdate
Hledger/Reports/ReportTypes.hs view
@@ -166,7 +166,7 @@ -- Part of a "CompoundBalanceCommandSpec", but also used in hledger-lib. data CBCSubreportSpec a = CBCSubreportSpec   { cbcsubreporttitle          :: Text                      -- ^ The title to use for the subreport-  , cbcsubreportquery          :: Journal -> Query          -- ^ The Query to use for the subreport+  , cbcsubreportquery          :: 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
Hledger/Utils.hs view
@@ -183,7 +183,7 @@   where     openFileOrStdin :: String -> IOMode -> IO Handle     openFileOrStdin "-" _ = return stdin-    openFileOrStdin f m   = openFile f m+    openFileOrStdin f' m   = openFile f' m  readHandlePortably :: Handle -> IO Text readHandlePortably h = do@@ -225,9 +225,9 @@   return (h [])   where     go h [] = return h-    go h (m:ms) = do+    go h (m:ms') = do       x <- m-      go (h . (x :)) ms+      go (h . (x :)) ms'  -- | Like mapM but uses sequence'. {-# INLINABLE mapM' #-}@@ -339,7 +339,7 @@     -- HasReportOpts class with some special behaviour. We therefore give the     -- basic lenses a special NoUpdate name to avoid conflicts.     className "ReportOpts" = Just (mkName "HasReportOptsNoUpdate", mkName "reportOptsNoUpdate")-    className (x:xs)       = Just (mkName ("Has" ++ x:xs), mkName (toLower x : xs))+    className (x':xs)       = Just (mkName ("Has" ++ x':xs), mkName (toLower x' : xs))     className []           = Nothing      -- Fields of ReportOpts which need to update the Query when they are updated.
Hledger/Utils/Debug.hs view
@@ -41,7 +41,11 @@ module Hledger.Utils.Debug (   -- * Pretty printing    pprint+  ,pprint'   ,pshow+  ,pshow'+  ,useColorOnStdout+  ,useColorOnStderr   -- * Tracing   ,traceWith   -- * Pretty tracing@@ -90,17 +94,33 @@   -- ** Trace the state of hledger parsers   ,traceParse   ,dbgparse+  -- ** Debug-logging to a file+  ,dlogTrace+  ,dlogTraceAt+  ,dlogAt+  ,dlog0+  ,dlog1+  ,dlog2+  ,dlog3+  ,dlog4+  ,dlog5+  ,dlog6+  ,dlog7+  ,dlog8+  ,dlog9+  -- ** Re-exports+  ,module Debug.Breakpoint   ,module Debug.Trace-  ,useColorOnStdout-  ,useColorOnStderr-  ,dlog)+  ) where +import           Control.DeepSeq (force) import           Control.Monad (when) 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.Breakpoint import           Debug.Trace import           Hledger.Utils.Parse import           Safe (readDef)@@ -113,25 +133,38 @@ import Data.Maybe (isJust) import System.Console.ANSI (hSupportsANSIColor) import System.IO (stdout, Handle, stderr)+import Control.Exception (evaluate) +-- | pretty-simple options with colour enabled if allowed. prettyopts = -  baseopts+  (if useColorOnStderr then defaultOutputOptionsDarkBg else defaultOutputOptionsNoColor)     { outputOptionsIndentAmount=2     , outputOptionsCompact=True     }-  where-    baseopts-      | useColorOnStderr = defaultOutputOptionsDarkBg -- defaultOutputOptionsLightBg-      | otherwise        = defaultOutputOptionsNoColor +-- | pretty-simple options with colour disabled.+prettyopts' =+  defaultOutputOptionsNoColor+    { outputOptionsIndentAmount=2+    , outputOptionsCompact=True+    }+ -- | Pretty print. Generic alias for pretty-simple's pPrint. pprint :: Show a => a -> IO () pprint = pPrintOpt CheckColorTty prettyopts +-- | Monochrome version of pprint.+pprint' :: Show a => a -> IO ()+pprint' = pPrintOpt CheckColorTty prettyopts'+ -- | Pretty show. Generic alias for pretty-simple's pShow. pshow :: Show a => a -> String pshow = TL.unpack . pShowOpt prettyopts +-- | Monochrome version of pshow.+pshow' :: Show a => a -> String+pshow' = TL.unpack . pShowOpt prettyopts'+ -- 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.@@ -284,10 +317,9 @@ ptraceAt :: Show a => Int -> String -> a -> a ptraceAt level     | level > 0 && debugLevel < level = const id-    | otherwise = \s a -> let p = pshow a-                              ls = lines p+    | otherwise = \s a -> let ls = lines $ pshow a                               nlorspace | length ls > 1 = "\n"-                                        | otherwise     = replicate (11 - length s) ' '+                                        | otherwise     = replicate (max 1 $ 11 - length s) ' '                               ls' | length ls > 1 = map (' ':) ls                                   | otherwise     = ls                           in trace (s++":"++nlorspace++intercalate "\n" ls') a@@ -305,10 +337,6 @@                         -- in trace (s++":"++nlorspace++intercalate "\n" ls') a                         in trace p a --- | Log a pretty-printed showable value to "./debug.log". Uses unsafePerformIO.-dlog :: Show a => a -> a-dlog x = unsafePerformIO $ appendFile "debug.log" (pshow x ++ "\n") >> return x- -- "dbg" would clash with megaparsec. -- | Pretty-print a label and the showable value to the console, then return it. dbg0 :: Show a => String -> a -> a@@ -344,6 +372,7 @@ dbg9 = ptraceAt 9  -- | Like dbg0, but also exit the program. Uses unsafePerformIO.+-- {-# NOINLINE dbgExit #-} dbgExit :: Show a => String -> a -> a dbgExit msg = const (unsafePerformIO exitFailure) . dbg0 msg @@ -419,6 +448,70 @@  dbg9IO :: (MonadIO m, Show a) => String -> a -> m () dbg9IO = ptraceAtIO 9++-- | Log a string to ./debug.log before returning the second argument.+-- Uses unsafePerformIO.+-- {-# NOINLINE dlogTrace #-}+dlogTrace :: String -> a -> a+dlogTrace s x = unsafePerformIO $ do+  evaluate (force s)  -- to complete any previous logging before we attempt more+  appendFile "debug.log" (s ++ "\n")+  return x++-- | Log a string to ./debug.log before returning the second argument,+-- if the global debug level is at or above the specified level.+-- At level 0, always logs. Otherwise, uses unsafePerformIO.+dlogTraceAt :: Int -> String -> a -> a+dlogTraceAt level s+  | level > 0 && debugLevel < level = id+  | otherwise = dlogTrace s++-- | Log a label and pretty-printed showable value to "./debug.log",+-- if the global debug level is at or above the specified level.+-- At level 0, always prints. Otherwise, uses unsafePerformIO.+dlogAt :: Show a => Int -> String -> a -> a+dlogAt level+  | level > 0 && debugLevel < level = const id+  | otherwise = \lbl a ->+    let +      ls = lines $ pshow' a+      nlorspace | length ls > 1 = "\n"+                | otherwise     = replicate (max 1 $ 11 - length lbl) ' '+      ls' | length ls > 1 = map (' ':) ls+          | otherwise     = ls+    in dlogTrace (lbl++":"++nlorspace++intercalate "\n" ls') a++-- | Pretty-print a label and the showable value to ./debug.log if at or above+-- a certain debug level, then return it.+dlog0 :: Show a => String -> a -> a+dlog0 = dlogAt 0++dlog1 :: Show a => String -> a -> a+dlog1 = dlogAt 1++dlog2 :: Show a => String -> a -> a+dlog2 = dlogAt 2++dlog3 :: Show a => String -> a -> a+dlog3 = dlogAt 3++dlog4 :: Show a => String -> a -> a+dlog4 = dlogAt 4++dlog5 :: Show a => String -> a -> a+dlog5 = dlogAt 5++dlog6 :: Show a => String -> a -> a+dlog6 = dlogAt 6++dlog7 :: Show a => String -> a -> a+dlog7 = dlogAt 7++dlog8 :: Show a => String -> a -> a+dlog8 = dlogAt 8++dlog9 :: Show a => String -> a -> a+dlog9 = dlogAt 9  -- | Print the provided label (if non-null) and current parser state -- (position and next input) to the console. See also megaparsec's dbg.
Hledger/Utils/Parse.hs view
@@ -38,6 +38,7 @@   skipNonNewlineSpaces',    -- * re-exports+  HledgerParseErrors,   HledgerParseErrorData ) where
Hledger/Utils/Regex.hs view
@@ -99,11 +99,11 @@                              RegexpCI _ _ -> showString "RegexpCI "  instance Read Regexp where-  readsPrec d r =  readParen (d > app_prec) (\r -> [(toRegexCI' m,t) |-                                                    ("RegexCI",s) <- lex r,+  readsPrec d r =  readParen (d > app_prec) (\r' -> [(toRegexCI' m,t) |+                                                    ("RegexCI",s) <- lex r',                                                     (m,t) <- readsPrec (app_prec+1) s]) r-                ++ readParen (d > app_prec) (\r -> [(toRegex' m, t) |-                                                    ("Regex",s) <- lex r,+                ++ readParen (d > app_prec) (\r' -> [(toRegex' m, t) |+                                                    ("Regex",s) <- lex r',                                                     (m,t) <- readsPrec (app_prec+1) s]) r     where app_prec = 10 @@ -134,7 +134,7 @@ -- | Make a nice error message for a regexp error. mkRegexErr :: Text -> Maybe a -> Either RegexError a mkRegexErr s = maybe (Left errmsg) Right-  where errmsg = T.unpack $ "this regular expression could not be compiled: " <> s+  where errmsg = T.unpack $ "This regular expression is malformed, please correct it:\n" <> s  -- Convert a Regexp string to a compiled Regex, throw an error toRegex' :: Text -> Regexp@@ -186,7 +186,7 @@ -- 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])+regexReplaceUnmemo re repl str = foldM (replaceMatch repl) str (reverse $ match (reCompiled re) str :: [MatchText String])   where     -- Replace one match within the string with the replacement text     -- appropriate for this match. Or return an error message.@@ -195,22 +195,22 @@       case elems matchgroups of          [] -> Right s         ((_,(off,len)):_) ->   -- groups should have 0-based indexes, and there should always be at least one, since this is a match-          erepl >>= \repl -> Right $ pre ++ repl ++ post+          erpl >>= \rpl -> Right $ pre ++ rpl ++ post           where             (pre, post') = splitAt off s             post = drop len post'             -- The replacement text: the replacement pattern with all             -- numeric backreferences replaced by the appropriate groups             -- from this match. Or an error message.-            erepl = regexReplaceAllByM backrefRegex (lookupMatchGroup matchgroups) replpat+            erpl = regexReplaceAllByM backrefRegex (lookupMatchGroup matchgroups) replpat               where                 -- Given some match groups and a numeric backreference,                 -- 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)  -- 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"+                lookupMatchGroup grps ('\\':s2@(_:_)) | all isDigit s2 =+                  case read s2 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 _ s2 = Left $ "lookupMatchGroup called on non-numeric-backreference \""++s2++"\", shouldn't happen"     backrefRegex = toRegex' "\\\\[0-9]+"  -- PARTIAL: should not fail  -- regexReplace' :: Regexp -> Replacement -> String -> String@@ -249,8 +249,8 @@         go :: (Int,String,String->String) -> (Int,Int) ->  (Int,String,String->String)         go (pos,todo,prepend) (off,len) =           let (prematch, matchandrest) = splitAt (off - pos) todo-              (matched, rest) = splitAt len matchandrest-          in (off + len, rest, prepend . (prematch++) . (transform matched ++))+              (matched, rest2) = splitAt len matchandrest+          in (off + len, rest2, prepend . (prematch++) . (transform matched ++))  -- Replace all occurrences of a regexp in a string, transforming each match -- with the given monadic function. Eg if the monad is Either, a Left result
Hledger/Utils/String.hs view
@@ -10,6 +10,7 @@  -- quoting  quoteIfNeeded,  singleQuoteIfNeeded,+ quoteForCommandLine,  -- quotechars,  -- whitespacechars,  words',@@ -118,15 +119,44 @@     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.+-- Does not work for strings containing single quotes. singleQuoteIfNeeded :: String -> String-singleQuoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = "'"++s++"'"+singleQuoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = singleQuote s                       | otherwise = s -quotechars, whitespacechars, redirectchars :: [Char]+-- | Prepend and append single quotes to a string.+singleQuote :: String -> String+singleQuote s = "'"++s++"'"++-- | Try to single- and backslash-quote a string as needed to make it usable+-- as an argument on a (sh/bash) shell command line. At least, well enough +-- to handle common currency symbols, like $. Probably broken in many ways.+--+-- >>> quoteForCommandLine "a"+-- "a"+-- >>> quoteForCommandLine "\""+-- "'\"'"+-- >>> quoteForCommandLine "$"+-- "'\\$'"+--+quoteForCommandLine :: String -> String+quoteForCommandLine s+  | any (`elem` s) (quotechars++whitespacechars++shellchars) = singleQuote $ quoteShellChars s+  | otherwise = s++-- | Try to backslash-quote common shell-significant characters in this string.+-- Doesn't handle single quotes, & probably others.+quoteShellChars :: String -> String+quoteShellChars = concatMap escapeShellChar+  where+    escapeShellChar c | c `elem` shellchars = ['\\',c]+    escapeShellChar c = [c]++quotechars, whitespacechars, redirectchars, shellchars :: [Char] quotechars      = "'\"" whitespacechars = " \t\n\r" redirectchars   = "<>"+shellchars      = "<>(){}[]$7?#!~`"  -- | Quote-aware version of words - don't split on spaces which are inside quotes. -- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.
Text/Megaparsec/Custom.hs view
@@ -120,8 +120,10 @@   -> HledgerParseErrorData parseErrorAtRegion startOffset endOffset msg =   if startOffset < endOffset-    then ErrorFailAt startOffset endOffset msg-    else ErrorFailAt startOffset (startOffset+1) msg+    then ErrorFailAt startOffset endOffset msg'+    else ErrorFailAt startOffset (startOffset+1) msg'+  where+    msg' = "\n" ++ msg   --- * Re-parsing@@ -369,9 +371,9 @@    -- A parse error thrown directly with the 'FinalError' constructor   -- requires both source and filepath.-  FinalError parseError ->+  FinalError err ->     let bundle = ParseErrorBundle-          { bundleErrors = parseError NE.:| []+          { bundleErrors = err NE.:| []           , bundlePosState = initialPosState filePath sourceText }     in  FinalParseErrorBundle'           { finalErrorBundle = bundle
Text/Tabular/AsciiWide.hs view
@@ -211,11 +211,11 @@ renderHLine vpos borders pretty w h prop = [renderHLine' vpos borders pretty prop w h]  renderHLine' :: VPos -> Bool -> Bool -> Properties -> [Int] -> Header a -> Builder-renderHLine' vpos borders pretty prop is h = addBorders $ sep <> coreLine <> sep+renderHLine' vpos borders pretty prop is hdr = 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        = foldMap helper $ flattenHeader $ zipHeader 0 is h+  coreLine        = foldMap helper $ flattenHeader $ zipHeader 0 is hdr   helper          = either vsep dashes   dashes (i,_)    = stimesMonoid i sep   sep             = boxchar vpos HM NoLine prop pretty
hledger-lib.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        1.26.1+version:        1.27 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@@ -27,7 +27,7 @@ license-file:   LICENSE build-type:     Simple tested-with:-    GHC==8.6.5, GHC==8.8.4, GHC==8.10.4, GHC==9.0.1+    GHC==8.10.7, GHC==9.0.2, GHC==9.2.4 extra-source-files:     CHANGES.md     README.md@@ -95,7 +95,7 @@       Paths_hledger_lib   hs-source-dirs:       ./-  ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-name-shadowing -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind+  ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind   build-depends:       Decimal >=0.5.1     , Glob >=0.9@@ -103,8 +103,9 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.11 && <4.17+    , base >=4.14 && <4.17     , blaze-markup >=0.5.1+    , breakpoint     , bytestring     , call-stack     , cassava@@ -112,6 +113,7 @@     , cmdargs >=0.10     , containers >=0.5.9     , data-default >=0.5+    , deepseq     , directory     , doclayout >=0.3 && <0.5     , extra >=1.6.3@@ -145,7 +147,7 @@   hs-source-dirs:       ./       test-  ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-name-shadowing -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind+  ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind   build-depends:       Decimal >=0.5.1     , Glob >=0.7@@ -153,8 +155,9 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.11 && <4.17+    , base >=4.14 && <4.17     , blaze-markup >=0.5.1+    , breakpoint     , bytestring     , call-stack     , cassava@@ -162,6 +165,7 @@     , cmdargs >=0.10     , containers >=0.5.9     , data-default >=0.5+    , deepseq     , directory     , doclayout >=0.3 && <0.5     , doctest >=0.18.1@@ -188,7 +192,7 @@     , uglymemo     , unordered-containers >=0.2     , utf8-string >=0.3.5-  if impl(ghc < 9.2)+  if impl(ghc >= 9.0) && impl(ghc < 9.2)     buildable: False   default-language: Haskell2010 @@ -198,7 +202,7 @@   hs-source-dirs:       ./       test-  ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-name-shadowing -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind+  ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind   build-depends:       Decimal >=0.5.1     , Glob >=0.9@@ -206,8 +210,9 @@     , aeson-pretty     , ansi-terminal >=0.9     , array-    , base >=4.11 && <4.17+    , base >=4.14 && <4.17     , blaze-markup >=0.5.1+    , breakpoint     , bytestring     , call-stack     , cassava@@ -215,6 +220,7 @@     , cmdargs >=0.10     , containers >=0.5.9     , data-default >=0.5+    , deepseq     , directory     , doclayout >=0.3 && <0.5     , extra >=1.6.3