From aad61e465d1bddc0447503d39d3a8d30d753d360 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Mon, 8 Sep 2025 19:55:44 +0200 Subject: [PATCH] fix: balance: Correctly handle empty journals (#2452) Eliminate several partial functions. --- hledger-lib/Hledger/Data/PeriodData.hs | 75 +++++++++++++++---- hledger-lib/Hledger/Data/Posting.hs | 13 +--- hledger-lib/Hledger/Reports/BudgetReport.hs | 6 +- .../Hledger/Reports/MultiBalanceReport.hs | 46 ++++-------- hledger-lib/Hledger/Reports/PostingsReport.hs | 6 +- hledger-lib/Hledger/Reports/ReportOptions.hs | 10 +-- hledger/Hledger/Cli/Commands/Activity.hs | 10 ++- hledger/Hledger/Cli/Commands/Roi.hs | 20 +++-- hledger/Hledger/Cli/Commands/Stats.hs | 2 +- hledger/test/balance/balance.test | 20 +++++ hledger/test/close.test | 7 +- 11 files changed, 131 insertions(+), 84 deletions(-) diff --git a/hledger-lib/Hledger/Data/PeriodData.hs b/hledger-lib/Hledger/Data/PeriodData.hs index 78032a2b9..6de7767e0 100644 --- a/hledger-lib/Hledger/Data/PeriodData.hs +++ b/hledger-lib/Hledger/Data/PeriodData.hs @@ -9,13 +9,19 @@ Report periods are assumed to be contiguous, and represented only by start dates -} module Hledger.Data.PeriodData ( periodDataFromList +, periodDataToList , lookupPeriodData +, lookupPeriodDataOrHistorical , insertPeriodData , opPeriodData , mergePeriodData , padPeriodData +, periodDataToDateSpans +, maybePeriodDataToDateSpans +, dateSpansToPeriodData + , tests_PeriodData ) where @@ -24,18 +30,17 @@ import Data.Foldable1 (Foldable1(..)) #else import Control.Applicative (liftA2) #endif +import Data.Bifunctor (first) import qualified Data.IntMap.Strict as IM -import qualified Data.IntSet as IS #if !MIN_VERSION_base(4,20,0) import Data.List (foldl') #endif import Data.Time (Day(..), fromGregorian) -import Test.Tasty (testGroup) -import Test.Tasty.HUnit ((@?=), testCase) - import Hledger.Data.Amount +import Hledger.Data.Dates import Hledger.Data.Types +import Hledger.Utils instance Show a => Show (PeriodData a) where @@ -44,7 +49,7 @@ instance Show a => Show (PeriodData a) where showString "PeriodData" . showString "{ pdpre = " . shows h . showString ", pdperiods = " - . showString "fromList " . shows (map (\(day, x) -> (ModifiedJulianDay $ toInteger day, x)) $ IM.toList ds) + . showString "fromList " . shows (map (\(day, x) -> (intToDay day, x)) $ IM.toList ds) . showChar '}' instance Foldable PeriodData where @@ -73,18 +78,32 @@ instance Monoid a => Monoid (PeriodData a) where -- | Construct an 'PeriodData' from a list. periodDataFromList :: a -> [(Day, a)] -> PeriodData a -periodDataFromList h = PeriodData h . IM.fromList . map (\(d, a) -> (fromInteger $ toModifiedJulianDay d, a)) +periodDataFromList h = PeriodData h . IM.fromList . map (\(d, a) -> (dayToInt d, a)) --- | Get account balance information to the period containing a given 'Day'. -lookupPeriodData :: Day -> PeriodData a -> a -lookupPeriodData d (PeriodData h as) = - maybe h snd $ IM.lookupLE (fromInteger $ toModifiedJulianDay d) as +-- | Convert 'PeriodData' to a list of pairs. +periodDataToList :: PeriodData a -> (a, [(Day, a)]) +periodDataToList (PeriodData h as) = (h, map (\(s, e) -> (intToDay s, e)) $ IM.toList as) + + +-- | Get account balance information for the period containing a given 'Day', +-- along with the start of the period, or 'Nothing' if this day lies in the +-- historical period. +lookupPeriodData :: Day -> PeriodData a -> Maybe (Day, a) +lookupPeriodData d (PeriodData _ as) = first intToDay <$> IM.lookupLE (dayToInt d) as + +-- | Get account balance information for the period containing a given 'Day' +-- or the historical data if this day lies in the historical period, along with +-- the start of the period or 'Nothing' if it lies in the historical period. +lookupPeriodDataOrHistorical :: Day -> PeriodData a -> (Maybe Day, a) +lookupPeriodDataOrHistorical d pd@(PeriodData h _) = case lookupPeriodData d pd of + Nothing -> (Nothing, h) + Just (a, b) -> (Just a, b) -- | Add account balance information to the appropriate location in 'PeriodData'. insertPeriodData :: Semigroup a => Maybe Day -> a -> PeriodData a -> PeriodData a insertPeriodData mday b balances = case mday of Nothing -> balances{pdpre = pdpre balances <> b} - Just day -> balances{pdperiods = IM.insertWith (<>) (fromInteger $ toModifiedJulianDay day) b $ pdperiods balances} + Just day -> balances{pdperiods = IM.insertWith (<>) (dayToInt day) b $ pdperiods balances} -- | Merges two 'PeriodData', using the given operation to combine their balance information. -- @@ -103,11 +122,39 @@ mergePeriodData only1 only2 f = \(PeriodData h1 as1) (PeriodData h2 as2) -> where merge = IM.mergeWithKey (\_ x y -> Just $ f x y) (fmap only1) (fmap only2) --- | Pad out the datemap of an 'PeriodData' so that every key from a set is present. -padPeriodData :: Monoid a => IS.IntSet -> PeriodData a -> PeriodData a -padPeriodData keys bal = bal{pdperiods = pdperiods bal <> IM.fromSet (const mempty) keys} +-- | Pad out the datemap of a 'PeriodData' so that every key from another 'PeriodData' is present. +padPeriodData :: a -> PeriodData b -> PeriodData a -> PeriodData a +padPeriodData x pad bal = bal{pdperiods = pdperiods bal <> (x <$ pdperiods pad)} +-- | Convert 'PeriodData Day' to a list of 'DateSpan's. +periodDataToDateSpans :: PeriodData Day -> [DateSpan] +periodDataToDateSpans = map (\(s, e) -> DateSpan (toEFDay s) (toEFDay e)) . snd . periodDataToList + where toEFDay = Just . Exact + +-- Convert a periodic report 'Maybe (PeriodData Day)' to a list of 'DateSpans', +-- replacing the empty case with an appropriate placeholder. +maybePeriodDataToDateSpans :: Maybe (PeriodData Day) -> [DateSpan] +maybePeriodDataToDateSpans = maybe [DateSpan Nothing Nothing] periodDataToDateSpans + +-- | Convert a list of 'DateSpan's to a 'PeriodData Day', or 'Nothing' if it is not well-formed. +-- PARTIAL: +dateSpansToPeriodData :: [DateSpan] -> Maybe (PeriodData Day) +-- Handle the cases of partitions which would arise from journals with no transactions +dateSpansToPeriodData [] = Nothing +dateSpansToPeriodData [DateSpan Nothing Nothing] = Nothing +dateSpansToPeriodData [DateSpan Nothing (Just _)] = Nothing +dateSpansToPeriodData [DateSpan (Just _) Nothing] = Nothing +-- Handle properly defined reports +dateSpansToPeriodData (x:xs) = Just $ periodDataFromList (fst $ boundaries x) (map boundaries (x:xs)) + where + boundaries spn = makeJust (spanStart spn, spanEnd spn) + makeJust (Just a, Just b) = (a, b) + makeJust ab = error' $ "dateSpansToPeriodData: expected all spans to have start and end dates, but one has " ++ show ab + +intToDay = ModifiedJulianDay . toInteger +dayToInt = fromInteger . toModifiedJulianDay + -- tests tests_PeriodData = diff --git a/hledger-lib/Hledger/Data/Posting.hs b/hledger-lib/Hledger/Data/Posting.hs index f64363d91..db3630963 100644 --- a/hledger-lib/Hledger/Data/Posting.hs +++ b/hledger-lib/Hledger/Data/Posting.hs @@ -46,8 +46,6 @@ module Hledger.Data.Posting ( postingDate, postingDate2, postingDateOrDate2, - isPostingInDateSpan, - isPostingInDateSpan', -- * account name operations accountNamesFromPostings, -- * comment/tag operations @@ -107,7 +105,7 @@ import Hledger.Utils import Hledger.Data.Types import Hledger.Data.Amount import Hledger.Data.AccountName -import Hledger.Data.Dates (nulldate, spanContainsDate) +import Hledger.Data.Dates (nulldate) import Hledger.Data.Valuation @@ -444,15 +442,6 @@ relatedPostings :: Posting -> [Posting] relatedPostings p@Posting{ptransaction=Just t} = filter (/= p) $ tpostings t relatedPostings _ = [] --- | Does this posting fall within the given date span ? -isPostingInDateSpan :: DateSpan -> Posting -> Bool -isPostingInDateSpan = isPostingInDateSpan' PrimaryDate - --- --date2-sensitive version, separate for now to avoid disturbing multiBalanceReport. -isPostingInDateSpan' :: WhichDate -> DateSpan -> Posting -> Bool -isPostingInDateSpan' PrimaryDate s = spanContainsDate s . postingDate -isPostingInDateSpan' SecondaryDate s = spanContainsDate s . postingDate2 - isEmptyPosting :: Posting -> Bool isEmptyPosting = mixedAmountLooksZero . pamount diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index 47c2e043f..d6d2a4c41 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -19,12 +19,12 @@ import Control.Monad ((>=>)) import Data.Bifunctor (bimap) import Data.Foldable (toList) import Data.List (find, maximumBy, intercalate) -import Data.List.Extra (nubSort) import Data.Maybe (catMaybes, fromMaybe, isJust) import Data.Ord (comparing) import qualified Data.Set as S import qualified Data.Text as T import Data.These (These(..), these) +import Data.Time (Day) import Safe (minimumDef) import Hledger.Data @@ -89,7 +89,7 @@ budgetReport rspec bopts reportspan j = dbg4 "sortedbudgetreport" budgetreport -- budgetgoalreport's span might be shorter actualreport's due to periodic txns; -- it should be safe to replace it with the latter, so they combine well. NoInterval -> actualspans - _ -> nubSort . filter (/= nulldatespan) $ actualspans ++ budgetspans + _ -> maybe id (padPeriodData nulldate) budgetspans <$> actualspans actualps = dbg5 "actualps" $ getPostings rspec actualj priceoracle reportspan budgetps = dbg5 "budgetps" $ getPostings rspec budgetj priceoracle reportspan @@ -107,7 +107,7 @@ budgetReport rspec bopts reportspan j = dbg4 "sortedbudgetreport" budgetreport -- | Lay out a set of postings grouped by date span into a regular matrix with rows -- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport -- from the columns. -generateBudgetReport :: ReportOpts -> [DateSpan] -> Account (These BalanceData BalanceData) -> BudgetReport +generateBudgetReport :: ReportOpts -> Maybe (PeriodData Day) -> Account (These BalanceData BalanceData) -> BudgetReport generateBudgetReport = generatePeriodicReport makeBudgetReportRow treeActualBalance flatActualBalance where treeActualBalance = these bdincludingsubs (const nullmixedamt) (const . bdincludingsubs) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 6f05cd061..24118f484 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -43,8 +43,7 @@ import Data.List (sortOn) import Data.List.NonEmpty (NonEmpty((:|))) import qualified Data.HashSet as HS import qualified Data.IntMap.Strict as IM -import qualified Data.IntSet as IS -import Data.Maybe (fromMaybe, isJust, mapMaybe) +import Data.Maybe (fromMaybe, isJust) import Data.Ord (Down(..)) import Data.Semigroup (sconcat) import Data.These (these) @@ -163,7 +162,7 @@ compoundBalanceReportWith rspec' j priceoracle subreportspecs = cbr subreportTotal (_, sr, increasestotal) = (if increasestotal then id else fmap maNegate) $ prTotals sr - cbr = CompoundPeriodicReport "" colspans subreports overalltotals + cbr = CompoundPeriodicReport "" (maybePeriodDataToDateSpans colspans) subreports overalltotals -- | Remove any date queries and insert queries from the report span. @@ -217,7 +216,7 @@ getPostings rspec@ReportSpec{_rsQuery=query, _rsReportOpts=ropts} j priceoracle -- | Generate the 'Account' for the requested multi-balance report from a list -- of 'Posting's. -generateMultiBalanceAccount :: ReportSpec -> Journal -> PriceOracle -> [DateSpan] -> [Posting] -> Account BalanceData +generateMultiBalanceAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe (PeriodData Day) -> [Posting] -> Account BalanceData generateMultiBalanceAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colspans = -- Add declared accounts if called with --declared and --empty (if (declared_ ropts && empty_ ropts) then addDeclaredAccounts rspec j else id) @@ -263,8 +262,10 @@ addDeclaredAccounts rspec j acct = -- | Gather the account balance changes into a regular matrix, then -- accumulate and value amounts, as specified by the report options. -- Makes sure all report columns have an entry. -calculateReportAccount :: ReportSpec -> Journal -> PriceOracle -> [DateSpan] -> [Posting] -> Account BalanceData -calculateReportAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colspans ps = -- PARTIAL: +calculateReportAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe (PeriodData Day) -> [Posting] -> Account BalanceData +calculateReportAccount _ _ _ Nothing _ = + accountFromBalances "root" $ periodDataFromList mempty [(nulldate, mempty)] +calculateReportAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle (Just colspans) ps = mapPeriodData rowbals changesAcct where -- The valued row amounts to be displayed: per-period changes, @@ -291,23 +292,17 @@ calculateReportAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colsp avalue = periodDataValuation ropts j priceoracle colspans changesAcct = dbg5With (\x -> "calculateReportAccount changesAcct\n" ++ showAccounts x) . - mapPeriodData (padPeriodData intervalStarts) $ + mapPeriodData (padPeriodData mempty colspans) $ accountFromPostings getIntervalStartDate ps - getIntervalStartDate p = intToDay <$> IS.lookupLE (dayToInt $ getPostingDate p) intervalStarts + getIntervalStartDate p = fst <$> lookupPeriodData (getPostingDate p) colspans getPostingDate = postingDateOrDate2 (whichDate (_rsReportOpts rspec)) - intervalStarts = IS.fromList . map dayToInt $ case mapMaybe spanStart colspans of - [] -> [nulldate] -- Deal with the case of the empty journal - xs -> xs - dayToInt = fromInteger . toModifiedJulianDay - intToDay = ModifiedJulianDay . toInteger - -- | The valuation function to use for the chosen report options. -- This can call error in various situations. -periodDataValuation :: ReportOpts -> Journal -> PriceOracle -> [DateSpan] +periodDataValuation :: ReportOpts -> Journal -> PriceOracle -> PeriodData Day -> PeriodData BalanceData -> PeriodData BalanceData -periodDataValuation ropts j priceoracle colspans = +periodDataValuation ropts j priceoracle periodEnds = opPeriodData valueBalanceData balanceDataPeriodEnds where valueBalanceData :: Day -> BalanceData -> BalanceData @@ -316,18 +311,9 @@ periodDataValuation ropts j priceoracle colspans = valueMixedAmount :: Day -> MixedAmount -> MixedAmount valueMixedAmount = mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle + -- The end date of a period is one before the beginning of the next period balanceDataPeriodEnds :: PeriodData Day - balanceDataPeriodEnds = dbg5 "balanceDataPeriodEnds" $ case colspans of -- FIXME: Change colspans to nonempty list - [DateSpan Nothing Nothing] -> periodDataFromList nulldate [(nulldate, nulldate)] -- Empty journal - h:ds -> periodDataFromList (makeJustFst $ boundaries h) $ map (makeJust . boundaries) (h:ds) - [] -> error' "balanceDataPeriodEnds: Shouldn't have empty colspans" -- PARTIAL: Shouldn't occur - where - boundaries spn = (spanStart spn, spanEnd spn) - - makeJust (Just x, Just y) = (x, addDays (-1) y) - makeJust _ = error' "balanceDataPeriodEnds: expected all non-initial spans to have start and end dates" - makeJustFst (Just x, _) = addDays (-1) x - makeJustFst _ = error' "balanceDataPeriodEnds: expected initial span to have an end date" + balanceDataPeriodEnds = dbg5 "balanceDataPeriodEnds" $ addDays (-1) <$> periodEnds -- | Mark which nodes of an 'Account' are boring, and so should be omitted from reports. markAccountBoring :: ReportSpec -> Account BalanceData -> Account BalanceData @@ -381,7 +367,7 @@ markAccountBoring ReportSpec{_rsQuery=query,_rsReportOpts=ropts} -- | Build a report row. -- -- Calculate the column totals. These are always the sum of column amounts. -generateMultiBalanceReport :: ReportOpts -> [DateSpan] -> Account BalanceData -> MultiBalanceReport +generateMultiBalanceReport :: ReportOpts -> Maybe (PeriodData Day) -> Account BalanceData -> MultiBalanceReport generateMultiBalanceReport ropts colspans = reportPercent ropts . generatePeriodicReport makeMultiBalanceReportRow bdincludingsubs id ropts colspans @@ -391,9 +377,9 @@ generateMultiBalanceReport ropts colspans = generatePeriodicReport :: Show c => (forall a. ReportOpts -> (BalanceData -> MixedAmount) -> a -> Account b -> PeriodicReportRow a c) -> (b -> MixedAmount) -> (c -> MixedAmount) - -> ReportOpts -> [DateSpan] -> Account b -> PeriodicReport DisplayName c + -> ReportOpts -> Maybe (PeriodData Day) -> Account b -> PeriodicReport DisplayName c generatePeriodicReport makeRow treeAmt flatAmt ropts colspans acct = - PeriodicReport colspans (buildAndSort acct) totalsrow + PeriodicReport (maybePeriodDataToDateSpans colspans) (buildAndSort acct) totalsrow where -- Build report rows and sort them buildAndSort = dbg5 "generatePeriodicReport buildAndSort" . case accountlistmode_ ropts of diff --git a/hledger-lib/Hledger/Reports/PostingsReport.hs b/hledger-lib/Hledger/Reports/PostingsReport.hs index c0d6ae411..d43de0598 100644 --- a/hledger-lib/Hledger/Reports/PostingsReport.hs +++ b/hledger-lib/Hledger/Reports/PostingsReport.hs @@ -209,12 +209,12 @@ mkpostingsReportItem showdate showdesc wd mperiod p b = -- | Convert a list of postings into summary postings, one per interval, -- aggregated to the specified depth if any. -- Each summary posting will have a non-Nothing interval end date. -summarisePostingsByInterval :: WhichDate -> Maybe Int -> Bool -> [DateSpan] -> [Posting] -> [SummaryPosting] +summarisePostingsByInterval :: WhichDate -> Maybe Int -> Bool -> Maybe (PeriodData Day) -> [Posting] -> [SummaryPosting] summarisePostingsByInterval wd mdepth showempty colspans = concatMap (\(s,ps) -> summarisePostingsInDateSpan s wd mdepth showempty ps) -- Group postings into their columns. We try to be efficient, since -- there can possibly be a very large number of intervals (cf #1683) - . groupByDateSpan showempty (postingDateOrDate2 wd) colspans + . groupByDateSpan showempty (postingDateOrDate2 wd) (maybePeriodDataToDateSpans colspans) -- | Given a date span (representing a report interval) and a list of -- postings within it, aggregate the postings into one summary posting per @@ -416,7 +416,7 @@ tests_PostingsReport = testGroup "PostingsReport" [ -} ,testCase "summarisePostingsByInterval" $ - summarisePostingsByInterval PrimaryDate Nothing False [DateSpan Nothing Nothing] [] @?= [] + summarisePostingsByInterval PrimaryDate Nothing False Nothing [] @?= [] -- ,tests_summarisePostingsInDateSpan = [ -- "summarisePostingsInDateSpan" ~: do diff --git a/hledger-lib/Hledger/Reports/ReportOptions.hs b/hledger-lib/Hledger/Reports/ReportOptions.hs index 010dbd28a..695b1a349 100644 --- a/hledger-lib/Hledger/Reports/ReportOptions.hs +++ b/hledger-lib/Hledger/Reports/ReportOptions.hs @@ -677,7 +677,7 @@ journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceo _ -> spanEnd <=< latestSpanContaining (historical : spans) historical = DateSpan Nothing $ (fmap Exact . spanStart) =<< headMay spans - spans = snd $ reportSpanBothDates j rspec + spans = maybePeriodDataToDateSpans . snd $ reportSpanBothDates j rspec styles = journalCommodityStyles j err = error' "journalApplyValuationFromOpts: expected all spans to have an end date" @@ -778,18 +778,18 @@ sortKeysDescription = "date, desc, account, amount, absamount" -- 'description' -- (or non-future market price date, when doing an end value report) is used. -- If none of these things are present, the null date span is returned. -- The report sub-periods caused by a report interval, if any, are also returned. -reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan]) +reportSpan :: Journal -> ReportSpec -> (DateSpan, Maybe (PeriodData Day)) reportSpan = reportSpanHelper False -- Note: In end value reports, the report end date and valuation date are the same. -- If valuation date ever needs to be different, journalApplyValuationFromOptsWith is the place. -- | Like reportSpan, but considers both primary and secondary dates, not just one or the other. -reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [DateSpan]) +reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, Maybe (PeriodData Day)) reportSpanBothDates = reportSpanHelper True -reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, [DateSpan]) +reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, Maybe (PeriodData Day)) reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts, _rsDay=today} = - (enlargedreportspan, if not (null intervalspans) then intervalspans else [enlargedreportspan]) + (enlargedreportspan, dateSpansToPeriodData $ if not (null intervalspans) then intervalspans else [enlargedreportspan]) where -- The date span specified by -b/-e/-p options and query args if any. requestedspan = dbg3 "requestedspan" $ diff --git a/hledger/Hledger/Cli/Commands/Activity.hs b/hledger/Hledger/Cli/Commands/Activity.hs index 4c595974e..fb64be373 100644 --- a/hledger/Hledger/Cli/Commands/Activity.hs +++ b/hledger/Hledger/Cli/Commands/Activity.hs @@ -34,16 +34,18 @@ showHistogram :: ReportSpec -> Journal -> String showHistogram rspec@ReportSpec{_rsQuery=q} j = concatMap (printDayWith countBar) spanps where - spans = filter (DateSpan Nothing Nothing /=) . snd . reportSpan j $ case rspec ^. interval of + mspans = snd . reportSpan j $ case rspec ^. interval of NoInterval -> set interval (Days 1) rspec _ -> rspec - spanps = [(s, filter (isPostingInDateSpan s) ps) | s <- spans] + spanps = case mspans of + Nothing -> [] + Just x -> map (\spn -> (spn, filter (postingInRange spn) ps)) . snd $ periodDataToList x + postingInRange (b, e) p = postingDate p >= b && postingDate p < e -- same as Register -- should count transactions, not postings ? -- ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j ps = sortOn postingDate $ filter (q `matchesPosting`) $ journalPostings j -printDayWith f (DateSpan (Just b) _, ps) = printf "%s %s\n" (show $ fromEFDay b) (f ps) -printDayWith _ _ = error' "Expected start date for DateSpan" -- PARTIAL: +printDayWith f ((b, _), ps) = printf "%s %s\n" (show b) (f ps) countBar ps = replicate (length ps) barchar diff --git a/hledger/Hledger/Cli/Commands/Roi.hs b/hledger/Hledger/Cli/Commands/Roi.hs index 43f00b413..0e84bbd5a 100644 --- a/hledger/Hledger/Cli/Commands/Roi.hs +++ b/hledger/Hledger/Cli/Commands/Roi.hs @@ -94,26 +94,30 @@ roi CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsReportOpts=ReportO when (null trans) $ error' "No relevant transactions found. Check your investments query" - let (fullPeriod, spans) = reportSpan filteredj rspec + let (fullPeriodDateSpan, mspans) = reportSpan filteredj rspec - let processSpan (DateSpan Nothing _) = error' "Undefined start of the period - will be unable to compute the rates of return" - processSpan (DateSpan _ Nothing) = error' "Undefined end of the period - will be unable to compute the rates of return" - processSpan spn@(DateSpan (Just begin) (Just end)) = do + let err = error' "Undefined start or end of the period - will be unable to compute the rates of return" + spans = maybe err (snd . periodDataToList) mspans + fullPeriod = case fullPeriodDateSpan of + DateSpan (Just b) (Just e) -> (fromEFDay b, fromEFDay e) + _ -> err + + let processSpan (b, e) = do -- Spans are [begin,end), and end is 1 day after the actual end date we are interested in let - b = fromEFDay begin - e = fromEFDay end + spn = DateSpan (Just $ Exact b) (Just $ Exact e) + cashFlowApplyCostValue = map (\(d,amt) -> (d,mixedAmountValue e d amt)) valueBefore = dbg3 "valueBefore" $ mixedAmountValue e b $ total trans (And [ investmentsQuery - , Date (DateSpan Nothing (Just begin))]) + , Date (DateSpan Nothing (Just $ Exact b))]) valueAfter = dbg3 "valueAfter" $ mixedAmountValue e e $ total trans (And [investmentsQuery - , Date (DateSpan Nothing (Just end))]) + , Date (DateSpan Nothing (Just $ Exact e))]) cashFlow = dbg3 "cashFlow" $ cashFlowApplyCostValue $ diff --git a/hledger/Hledger/Cli/Commands/Stats.hs b/hledger/Hledger/Cli/Commands/Stats.hs index 2d6249fee..ea39ba30a 100644 --- a/hledger/Hledger/Cli/Commands/Stats.hs +++ b/hledger/Hledger/Cli/Commands/Stats.hs @@ -57,7 +57,7 @@ stats opts@CliOpts{rawopts_=rawopts, reportspec_=rspec, progstarttime_} j = do l = ledgerFromJournal q j intervalspans = snd $ reportSpanBothDates j rspec ismultiperiod = length intervalspans > 1 - (ls, txncounts) = unzip $ map (showLedgerStats verbose l today) intervalspans + (ls, txncounts) = unzip . map (showLedgerStats verbose l today) $ maybePeriodDataToDateSpans intervalspans numtxns = sum txncounts txt = (if ismultiperiod then id else TL.init) $ TB.toLazyText $ unlinesB ls writeOutputLazyText opts txt diff --git a/hledger/test/balance/balance.test b/hledger/test/balance/balance.test index 1e997cbe9..398e89a35 100644 --- a/hledger/test/balance/balance.test +++ b/hledger/test/balance/balance.test @@ -239,3 +239,23 @@ $ hledger -f sample.journal balance --flat --empty -------------------- 0 +# ** 19. Shows zero if period starts after all transactions +$ hledger -f sample.journal balance -b 3000-01-01 +-------------------- + 0 + +# ** 20. Shows zero if period ends before all transactions +$ hledger -f sample.journal balance -e 1000-01-01 +-------------------- + 0 + +# ** 20. Shows zero if period starts and ends before all transactions +$ hledger -f sample.journal balance -b 1000-01-01 -e 1000-01-02 +-------------------- + 0 + +# ** 19. Shows zero on empty journal +< +$ hledger -f- balance +-------------------- + 0 diff --git a/hledger/test/close.test b/hledger/test/close.test index 5641c050e..d1514df4f 100644 --- a/hledger/test/close.test +++ b/hledger/test/close.test @@ -235,10 +235,9 @@ $ 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 ->2 /Error: balanceDataPeriodEnds: expected initial span to have an end date/ ->=1 +$ hledger -f- close -e 10000 +> /9999-12-31 closing balances/ +>= # ** 17. close (and print) should add trailing decimal marks when needed to posting amounts and costs. <