From afd18a10bf21d15f92a9199d617748e8664d5332 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sun, 15 Jun 2025 22:30:49 -1000 Subject: [PATCH] fix: show historical balances even if report period is empty [#2403] This adds a safer version of spanDefaultsFrom that won't create spans that end before they start, and updates all reports to use it. The only related change noticed so far is that close now gives an error instead of a malformed entry, when there's no data to close. [#2409] --- hledger-lib/Hledger/Data/Dates.hs | 34 +++++++++++++++++++ .../Hledger/Data/PeriodicTransaction.hs | 2 +- .../Hledger/Reports/MultiBalanceReport.hs | 2 +- hledger-lib/Hledger/Reports/ReportOptions.hs | 5 ++- hledger/Hledger/Cli/Commands/Run.hs | 2 +- hledger/test/balance/balance-multiperiod.test | 20 +++++++++++ hledger/test/close.test | 5 +-- 7 files changed, 62 insertions(+), 8 deletions(-) diff --git a/hledger-lib/Hledger/Data/Dates.hs b/hledger-lib/Hledger/Data/Dates.hs index 6c4a7a959..163003cdc 100644 --- a/hledger-lib/Hledger/Data/Dates.hs +++ b/hledger-lib/Hledger/Data/Dates.hs @@ -63,6 +63,7 @@ module Hledger.Data.Dates ( spanIntersect, spansIntersect, spanDefaultsFrom, + spanValidDefaultsFrom, spanExtend, spanUnion, spansUnion, @@ -348,10 +349,43 @@ spanIntersect (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan (laterDefinite b1 b2) -- | Fill any unspecified dates in the first span with the dates from -- the second one (if specified there). Sort of a one-way spanIntersect. +-- This one can create an invalid span that'll always be empty. +-- +-- >>> :{ +-- DateSpan (Just $ Exact $ fromGregorian 2024 1 1) Nothing +-- `spanDefaultsFrom` +-- DateSpan (Just $ Exact $ fromGregorian 2024 1 1) (Just $ Exact $ fromGregorian 2024 1 2) +-- :} +-- DateSpan 2024-01-01 +-- +-- >>> :{ +-- DateSpan (Just $ Exact $ fromGregorian 2025 1 1) Nothing +-- `spanDefaultsFrom` +-- DateSpan (Just $ Exact $ fromGregorian 2024 1 1) (Just $ Exact $ fromGregorian 2024 1 2) +-- :} +-- DateSpan 2025-01-01..2024-01-01 +-- +spanDefaultsFrom :: DateSpan -> DateSpan -> DateSpan spanDefaultsFrom (DateSpan a1 b1) (DateSpan a2 b2) = DateSpan a b where a = if isJust a1 then a1 else a2 b = if isJust b1 then b1 else b2 +-- | A smarter version of spanDefaultsFrom that avoids creating invalid +-- spans ending before they begin. Kept separate for now to reduce risk. +-- +-- >>> :{ +-- DateSpan (Just $ Exact $ fromGregorian 2025 1 1) Nothing +-- `spanValidDefaultsFrom` +-- DateSpan (Just $ Exact $ fromGregorian 2024 1 1) (Just $ Exact $ fromGregorian 2024 1 2) +-- :} +-- DateSpan 2025-01-01.. +-- +spanValidDefaultsFrom :: DateSpan -> DateSpan -> DateSpan +spanValidDefaultsFrom s1 s2 = + case s1 `spanDefaultsFrom` s2 of + DateSpan b e | b >= e -> s1 + s -> s + -- | Calculate the union of two datespans. -- If either span is open-ended, the union will be too. -- diff --git a/hledger-lib/Hledger/Data/PeriodicTransaction.hs b/hledger-lib/Hledger/Data/PeriodicTransaction.hs index d73e8a7d7..77e165d4f 100644 --- a/hledger-lib/Hledger/Data/PeriodicTransaction.hs +++ b/hledger-lib/Hledger/Data/PeriodicTransaction.hs @@ -215,7 +215,7 @@ runPeriodicTransaction verbosetags PeriodicTransaction{..} requestedspan = alltxnspans = splitSpan adjust ptinterval span' where -- If the PT does not specify start or end dates, we take them from the requestedspan. - span' = ptspan `spanDefaultsFrom` requestedspan + span' = ptspan `spanValidDefaultsFrom` requestedspan -- Unless the PT specified a start date explicitly, we will adjust the start date to the previous interval boundary. adjust = isNothing $ spanStart span' diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index c8b6b7fd9..9573abcbf 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -390,7 +390,7 @@ generatePeriodicReport makeRow treeAmt flatAmt ropts colspans acct = PeriodicReport colspans (buildAndSort acct) totalsrow where -- Build report rows and sort them - buildAndSort = dbg5 "generatePeriodicReport buildAndSort" . case accountlistmode_ ropts of {} + buildAndSort = dbg5 "generatePeriodicReport buildAndSort" . case accountlistmode_ ropts of ALTree | sort_amount_ ropts -> buildRows . sortTreeByAmount ALFlat | sort_amount_ ropts -> sortFlatByAmount . buildRows _ -> buildRows . sortAccountTreeByDeclaration diff --git a/hledger-lib/Hledger/Reports/ReportOptions.hs b/hledger-lib/Hledger/Reports/ReportOptions.hs index 01fb0d3b2..654436b4d 100644 --- a/hledger-lib/Hledger/Reports/ReportOptions.hs +++ b/hledger-lib/Hledger/Reports/ReportOptions.hs @@ -793,9 +793,8 @@ reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} = pricespan = dbg3 "pricespan" . DateSpan Nothing $ case value_ ropts of Just (AtEnd _) -> fmap (Exact . addDays 1) . maximumMay . map pddate $ jpricedirectives j _ -> Nothing - -- If the requested span is open-ended, close it using the journal's start and end dates. - -- This can still be the null (open) span if the journal is empty. - requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanExtend` pricespan) + -- If the requested span has open ends, fill those with the journal's start and/or end dates, if possible. + requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanValidDefaultsFrom` (journalspan `spanExtend` pricespan) -- The list of interval spans enclosing the requested span. -- This list can be empty if the journal was empty, -- or if hledger-ui has added its special date:-tomorrow to the query diff --git a/hledger/Hledger/Cli/Commands/Run.hs b/hledger/Hledger/Cli/Commands/Run.hs index 4fe780a13..405957ed8 100644 --- a/hledger/Hledger/Cli/Commands/Run.hs +++ b/hledger/Hledger/Cli/Commands/Run.hs @@ -256,7 +256,7 @@ withJournalCached defaultJournalOverride cliopts cmd = do -- could use 'forecastPeriod') or to the journal end date (as -- forecast transactions are never generated before journal end -- unless specifically requested). - Just forecastspan -> forecastspan `spanDefaultsFrom` reportspan_ iopts + Just forecastspan -> forecastspan `spanValidDefaultsFrom` reportspan_ iopts -- Read stdin, or if we read it alread, use a cache -- readStdin :: InputOpts -> ExceptT String IO Journal readStdin = do diff --git a/hledger/test/balance/balance-multiperiod.test b/hledger/test/balance/balance-multiperiod.test index 3e087c6b4..f02fe6516 100644 --- a/hledger/test/balance/balance-multiperiod.test +++ b/hledger/test/balance/balance-multiperiod.test @@ -232,3 +232,23 @@ Balance changes in 2014-01-10..2014-03-09: explicit report period || 10 0 ------------------------++------------------------------------------------ || 10 100 + +# ** 19. The historical balances are shown here even though the report period contains no postings. (#2403) +< +2024-01-01 + (a) 1000 + +$ hledger -f - bal -NHY -b 2025 +Ending balances (historical) in 2025: + + || 2025-12-31 +===++============ + a || 1000 + +# ** 20. Without -H, no balance changes are shown here, even with -E. +# Should it show zero for each of the pre-report-period accounts ? +$ hledger -f - bal -NY -b 2025 +Balance changes in 2025: + + || 2025 +==++====== diff --git a/hledger/test/close.test b/hledger/test/close.test index 3e567e281..5641c050e 100644 --- a/hledger/test/close.test +++ b/hledger/test/close.test @@ -235,9 +235,10 @@ $ hledger -f- close >= # ** 16. "override the closing date ... by specifying a report period, where last day of the report period will be the closing date" +# With no data to close in the period, this is currently giving an error. XXX $ hledger -f- close -e 100000-01-01 -> /99999-12-31 closing balances/ ->= +>2 /Error: balanceDataPeriodEnds: expected initial span to have an end date/ +>=1 # ** 17. close (and print) should add trailing decimal marks when needed to posting amounts and costs. <