dev!: balance: Use DayPartition for multibalance reports.

This allows us to guarantee that the report periods are well-formed and
don't contain errors (e.g. empty spans, spans not contiguous, spans not
a partition).

Note the underlying representation is now for disjoint spans, whereas
previously the end date of a span was equal to the start date of the
next span, and then was adjusted backwards one day when needed.
This commit is contained in:
Stephen Morgan 2025-09-11 12:50:47 +02:00 committed by Simon Michael
parent 288ced572c
commit b9caa4d948
12 changed files with 172 additions and 70 deletions

View File

@ -10,19 +10,20 @@ functionality. This package re-exports all the Hledger.Data.* modules
module Hledger.Data ( module Hledger.Data (
module Hledger.Data.Account, module Hledger.Data.Account,
module Hledger.Data.BalanceData,
module Hledger.Data.PeriodData,
module Hledger.Data.AccountName, module Hledger.Data.AccountName,
module Hledger.Data.Amount, module Hledger.Data.Amount,
module Hledger.Data.BalanceData,
module Hledger.Data.Balancing, module Hledger.Data.Balancing,
module Hledger.Data.Currency, module Hledger.Data.Currency,
module Hledger.Data.Dates, module Hledger.Data.Dates,
module Hledger.Data.DayPartition,
module Hledger.Data.Errors, module Hledger.Data.Errors,
module Hledger.Data.Journal, module Hledger.Data.Journal,
module Hledger.Data.JournalChecks, module Hledger.Data.JournalChecks,
module Hledger.Data.Json, module Hledger.Data.Json,
module Hledger.Data.Ledger, module Hledger.Data.Ledger,
module Hledger.Data.Period, module Hledger.Data.Period,
module Hledger.Data.PeriodData,
module Hledger.Data.PeriodicTransaction, module Hledger.Data.PeriodicTransaction,
module Hledger.Data.Posting, module Hledger.Data.Posting,
module Hledger.Data.RawOptions, module Hledger.Data.RawOptions,
@ -39,18 +40,19 @@ where
import Test.Tasty (testGroup) import Test.Tasty (testGroup)
import Hledger.Data.Account import Hledger.Data.Account
import Hledger.Data.BalanceData import Hledger.Data.BalanceData
import Hledger.Data.PeriodData
import Hledger.Data.AccountName import Hledger.Data.AccountName
import Hledger.Data.Amount import Hledger.Data.Amount
import Hledger.Data.Balancing import Hledger.Data.Balancing
import Hledger.Data.Currency import Hledger.Data.Currency
import Hledger.Data.Dates import Hledger.Data.Dates
import Hledger.Data.DayPartition
import Hledger.Data.Errors import Hledger.Data.Errors
import Hledger.Data.Journal import Hledger.Data.Journal
import Hledger.Data.JournalChecks import Hledger.Data.JournalChecks
import Hledger.Data.Json import Hledger.Data.Json
import Hledger.Data.Ledger import Hledger.Data.Ledger
import Hledger.Data.Period import Hledger.Data.Period
import Hledger.Data.PeriodData
import Hledger.Data.PeriodicTransaction import Hledger.Data.PeriodicTransaction
import Hledger.Data.Posting import Hledger.Data.Posting
import Hledger.Data.RawOptions import Hledger.Data.RawOptions

View File

@ -0,0 +1,131 @@
{-|
A partition of time into contiguous spans, for defining reporting periods.
-}
module Hledger.Data.DayPartition
( DayPartition
, boundariesToDayPartition
, boundariesToMaybeDayPartition
, lookupDayPartition
, unionDayPartitions
, dayPartitionToNonEmpty
, dayPartitionToList
, dayPartitionToPeriodData
, dayPartitionToDateSpans
, maybeDayPartitionToDateSpans
, dateSpansToDayPartition
) where
import qualified Data.IntMap.Strict as IM
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NE
import Data.Time (Day, addDays)
import Hledger.Data.Dates
import Hledger.Data.PeriodData
import Hledger.Data.Types
import Hledger.Utils
-- | A partition of time into contiguous spans, along with a historical period
-- before any of the spans.
--
-- This is a newtype wrapper around 'PeriodData Day', where the start dates are
-- the keys and the end dates are the values. Spans are stored in inclusive format
-- [start, end]. Note that this differs from 'DateSpan' which uses [start, end)
-- format.
--
-- The constructor is not exported so that we can ensure the spans are valid
-- partitions of time.
newtype DayPartition = DayPartition { dayPartitionToPeriodData :: PeriodData Day } deriving (Eq, Ord, Show)
-- Developer's note. All constructors must guarantee that:
-- 1. The value stored in pdperiods has at least one key.
-- 2. The value stored in pdpre equals one day before the smallest key in pdperiods.
-- 3. The value stored in each entry of pdperiods equals one day before the
-- next largest key, except for the value associated to the largest key.
isValidDayPartition :: DayPartition -> Bool
isValidDayPartition (DayPartition pd) = case ds of
[] -> False -- Must be at least one key in pdperiods
xs -> and $ zipWith isContiguous ((nulldate, h) : xs) xs
where
(h, ds) = periodDataToList pd
isContiguous (_, e) (s, _) = addDays 1 e == s
-- | Construct a 'DayPartition' from a non-empty list of boundary days.
boundariesToDayPartition :: NonEmpty Day -> DayPartition
boundariesToDayPartition xs =
DayPartition $ periodDataFromList (addDays (-1) b) $ zip (b:bs) (map (addDays (-1)) bs)
where (b:|bs) = NE.nub $ NE.sort xs
-- | Construct a 'DayPartition' from a list of boundary days, returning
-- 'Nothing' for the empty list.
boundariesToMaybeDayPartition :: [Day] -> Maybe DayPartition
boundariesToMaybeDayPartition = fmap boundariesToDayPartition . NE.nonEmpty
-- | Find the span of a 'DayPartition' which contains a given day.
lookupDayPartition :: Day -> DayPartition -> (Maybe Day, Day)
lookupDayPartition d (DayPartition xs) = lookupPeriodDataOrHistorical d xs
-- | Return the union of two 'DayPartition's if they are consistent, or 'Nothing' otherwise.
unionDayPartitions :: DayPartition -> DayPartition -> Maybe DayPartition
unionDayPartitions (DayPartition (PeriodData h as)) (DayPartition (PeriodData h' as')) =
if equalIntersection as as' && isValidDayPartition union then Just union else Nothing
where
union = DayPartition . PeriodData (min h h') $ as <> as'
equalIntersection x y = and $ IM.intersectionWith (==) x y
-- | Convert 'DayPartition' to a non-empty list of start and end dates for the periods.
--
-- Note that the end date of each period will be one day before the start date
-- of the next period.
dayPartitionToNonEmpty :: DayPartition -> NonEmpty (Day, Day)
dayPartitionToNonEmpty (DayPartition xs) = NE.fromList . snd $ periodDataToList xs -- Constructors guarantee this is non-empty
-- | Convert 'DayPartition' to a list of start and end dates for the periods.
--
-- Note that the end date of each period will be one day before the start date
-- of the next period.
dayPartitionToList :: DayPartition -> [(Day, Day)]
dayPartitionToList = NE.toList . dayPartitionToNonEmpty
-- | Convert 'DayPartition' to a list of 'DateSpan's.
--
-- Note that the end date of each period will be equal to the start date of
-- the next period.
dayPartitionToDateSpans :: DayPartition -> [DateSpan]
dayPartitionToDateSpans = map toDateSpan . dayPartitionToList
where
toDateSpan (s, e) = DateSpan (toEFDay s) (toEFDay $ addDays 1 e)
toEFDay = Just . Exact
-- Convert a periodic report 'Maybe DayPartition' to a list of 'DateSpans',
-- replacing the empty case with an appropriate placeholder.
--
-- Note that the end date of each period will be equal to the start date of
-- the next period.
maybeDayPartitionToDateSpans :: Maybe DayPartition -> [DateSpan]
maybeDayPartitionToDateSpans = maybe [DateSpan Nothing Nothing] dayPartitionToDateSpans
-- | Convert a list of 'DateSpan's to a 'DayPartition', or 'Nothing' if it is not well-formed.
--
-- Warning: This can construct ill-formed 'DayPartitions' and can raise errors.
-- It will be eliminated later.
-- PARTIAL:
dateSpansToDayPartition :: [DateSpan] -> Maybe DayPartition
-- Handle the cases of partitions which would arise from journals with no transactions
dateSpansToDayPartition [] = Nothing
dateSpansToDayPartition [DateSpan Nothing Nothing] = Nothing
dateSpansToDayPartition [DateSpan Nothing (Just _)] = Nothing
dateSpansToDayPartition [DateSpan (Just _) Nothing] = Nothing
-- Handle properly defined reports
dateSpansToDayPartition (x:xs) = Just . DayPartition $
periodDataFromList (addDays (-1) . fst $ boundaries x) (map boundaries (x:xs))
where
boundaries spn = makeJust (spanStart spn, addDays (-1) <$> spanEnd spn)
makeJust (Just a, Just b) = (a, b)
makeJust ab = error' $ "dateSpansToDayPartition: expected all spans to have start and end dates, but one has " ++ show ab

View File

@ -18,10 +18,6 @@ module Hledger.Data.PeriodData
, mergePeriodData , mergePeriodData
, padPeriodData , padPeriodData
, periodDataToDateSpans
, maybePeriodDataToDateSpans
, dateSpansToPeriodData
, tests_PeriodData , tests_PeriodData
) where ) where
@ -38,7 +34,6 @@ import Data.List (foldl')
import Data.Time (Day(..), fromGregorian) import Data.Time (Day(..), fromGregorian)
import Hledger.Data.Amount import Hledger.Data.Amount
import Hledger.Data.Dates
import Hledger.Data.Types import Hledger.Data.Types
import Hledger.Utils import Hledger.Utils
@ -127,31 +122,6 @@ padPeriodData :: a -> PeriodData b -> PeriodData a -> PeriodData a
padPeriodData x pad bal = bal{pdperiods = pdperiods bal <> (x <$ pdperiods pad)} 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 intToDay = ModifiedJulianDay . toInteger
dayToInt = fromInteger . toModifiedJulianDay dayToInt = fromInteger . toModifiedJulianDay

View File

@ -750,13 +750,16 @@ data Account a = Account {
,adata :: PeriodData a -- ^ associated data per report period ,adata :: PeriodData a -- ^ associated data per report period
} deriving (Generic, Functor) } deriving (Generic, Functor)
-- | Data values for zero or more report periods, and for the pre-report period. -- | A general container for storing data values associated to zero or more
-- Report periods are assumed to be contiguous, and represented only by start dates -- report periods, and for the pre-report period. Report periods are assumed to
-- (as keys of an IntMap). XXX how does that work, again ? -- be contiguous, and represented only by start dates.
--
-- Data is stored in an 'IntMap' for efficiency, where Days are stored as as
-- Int representing the underlying modified Julian date.
data PeriodData a = PeriodData { data PeriodData a = PeriodData {
pdpre :: a -- ^ data from the pre-report period (e.g. historical balances) pdpre :: a -- ^ data from the pre-report period (e.g. historical balances)
,pdperiods :: IM.IntMap a -- ^ data for the periods ,pdperiods :: IM.IntMap a -- ^ data for the periods
} deriving (Eq, Functor, Generic) } deriving (Eq, Ord, Functor, Generic)
-- | Data that's useful in "balance" reports: -- | Data that's useful in "balance" reports:
-- subaccount-exclusive and -inclusive amounts, -- subaccount-exclusive and -inclusive amounts,

View File

@ -24,7 +24,6 @@ import Data.Ord (comparing)
import Data.Set qualified as S import Data.Set qualified as S
import Data.Text qualified as T import Data.Text qualified as T
import Data.These (These(..), these) import Data.These (These(..), these)
import Data.Time (Day)
import Safe (minimumDef) import Safe (minimumDef)
import Hledger.Data import Hledger.Data
@ -84,12 +83,13 @@ budgetReport rspec bopts reportspan j = dbg4 "sortedbudgetreport" budgetreport
(_, actualspans) = dbg5 "actualspans" $ reportSpan actualj rspec (_, actualspans) = dbg5 "actualspans" $ reportSpan actualj rspec
(_, budgetspans) = dbg5 "budgetspans" $ reportSpan budgetj rspec (_, budgetspans) = dbg5 "budgetspans" $ reportSpan budgetj rspec
allspans = case interval_ ropts of allspans = dbg5 "allspans" $ case (interval_ ropts, budgetspans) of
-- If no interval is specified: -- If no interval is specified:
-- budgetgoalreport's span might be shorter actualreport's due to periodic txns; -- 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. -- it should be safe to replace it with the latter, so they combine well.
NoInterval -> actualspans (NoInterval, _) -> actualspans
_ -> maybe id (padPeriodData nulldate) budgetspans <$> actualspans (_, Nothing) -> actualspans
(_, Just bspan) -> unionDayPartitions bspan =<< actualspans
actualps = dbg5 "actualps" $ getPostings rspec actualj priceoracle reportspan actualps = dbg5 "actualps" $ getPostings rspec actualj priceoracle reportspan
budgetps = dbg5 "budgetps" $ getPostings rspec budgetj 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 -- | 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 -- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport
-- from the columns. -- from the columns.
generateBudgetReport :: ReportOpts -> Maybe (PeriodData Day) -> Account (These BalanceData BalanceData) -> BudgetReport generateBudgetReport :: ReportOpts -> Maybe DayPartition -> Account (These BalanceData BalanceData) -> BudgetReport
generateBudgetReport = generatePeriodicReport makeBudgetReportRow treeActualBalance flatActualBalance generateBudgetReport = generatePeriodicReport makeBudgetReportRow treeActualBalance flatActualBalance
where where
treeActualBalance = these bdincludingsubs (const nullmixedamt) (const . bdincludingsubs) treeActualBalance = these bdincludingsubs (const nullmixedamt) (const . bdincludingsubs)

View File

@ -47,7 +47,7 @@ import Data.Maybe (fromMaybe, isJust)
import Data.Ord (Down(..)) import Data.Ord (Down(..))
import Data.Semigroup (sconcat) import Data.Semigroup (sconcat)
import Data.These (these) import Data.These (these)
import Data.Time.Calendar (Day(..), addDays, fromGregorian) import Data.Time.Calendar (Day(..), fromGregorian)
import Data.Traversable (mapAccumL) import Data.Traversable (mapAccumL)
import Hledger.Data import Hledger.Data
@ -162,7 +162,7 @@ compoundBalanceReportWith rspec' j priceoracle subreportspecs = cbr
subreportTotal (_, sr, increasestotal) = subreportTotal (_, sr, increasestotal) =
(if increasestotal then id else fmap maNegate) $ prTotals sr (if increasestotal then id else fmap maNegate) $ prTotals sr
cbr = CompoundPeriodicReport "" (maybePeriodDataToDateSpans colspans) subreports overalltotals cbr = CompoundPeriodicReport "" (maybeDayPartitionToDateSpans colspans) subreports overalltotals
-- | Remove any date queries and insert queries from the report span. -- | Remove any date queries and insert queries from the report span.
@ -216,7 +216,7 @@ getPostings rspec@ReportSpec{_rsQuery=query, _rsReportOpts=ropts} j priceoracle
-- | Generate the 'Account' for the requested multi-balance report from a list -- | Generate the 'Account' for the requested multi-balance report from a list
-- of 'Posting's. -- of 'Posting's.
generateMultiBalanceAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe (PeriodData Day) -> [Posting] -> Account BalanceData generateMultiBalanceAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe DayPartition -> [Posting] -> Account BalanceData
generateMultiBalanceAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colspans = generateMultiBalanceAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colspans =
-- Add declared accounts if called with --declared and --empty -- Add declared accounts if called with --declared and --empty
(if (declared_ ropts && empty_ ropts) then addDeclaredAccounts rspec j else id) (if (declared_ ropts && empty_ ropts) then addDeclaredAccounts rspec j else id)
@ -262,7 +262,7 @@ addDeclaredAccounts rspec j acct =
-- | Gather the account balance changes into a regular matrix, then -- | Gather the account balance changes into a regular matrix, then
-- accumulate and value amounts, as specified by the report options. -- accumulate and value amounts, as specified by the report options.
-- Makes sure all report columns have an entry. -- Makes sure all report columns have an entry.
calculateReportAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe (PeriodData Day) -> [Posting] -> Account BalanceData calculateReportAccount :: ReportSpec -> Journal -> PriceOracle -> Maybe DayPartition -> [Posting] -> Account BalanceData
calculateReportAccount _ _ _ Nothing _ = calculateReportAccount _ _ _ Nothing _ =
accountFromBalances "root" $ periodDataFromList mempty [(nulldate, mempty)] accountFromBalances "root" $ periodDataFromList mempty [(nulldate, mempty)]
calculateReportAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle (Just colspans) ps = calculateReportAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle (Just colspans) ps =
@ -292,18 +292,17 @@ calculateReportAccount rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle (Just
avalue = periodDataValuation ropts j priceoracle colspans avalue = periodDataValuation ropts j priceoracle colspans
changesAcct = dbg5With (\x -> "calculateReportAccount changesAcct\n" ++ showAccounts x) . changesAcct = dbg5With (\x -> "calculateReportAccount changesAcct\n" ++ showAccounts x) .
mapPeriodData (padPeriodData mempty colspans) $ mapPeriodData (padPeriodData mempty (dayPartitionToPeriodData colspans)) $
accountFromPostings getIntervalStartDate ps accountFromPostings getIntervalStartDate ps
getIntervalStartDate p = fst <$> lookupPeriodData (getPostingDate p) colspans getIntervalStartDate p = fst $ lookupDayPartition (getPostingDate p) colspans
getPostingDate = postingDateOrDate2 (whichDate (_rsReportOpts rspec)) getPostingDate = postingDateOrDate2 (whichDate (_rsReportOpts rspec))
-- | The valuation function to use for the chosen report options. -- | The valuation function to use for the chosen report options.
-- This can call error in various situations. periodDataValuation :: ReportOpts -> Journal -> PriceOracle -> DayPartition
periodDataValuation :: ReportOpts -> Journal -> PriceOracle -> PeriodData Day
-> PeriodData BalanceData -> PeriodData BalanceData -> PeriodData BalanceData -> PeriodData BalanceData
periodDataValuation ropts j priceoracle periodEnds = periodDataValuation ropts j priceoracle colspans =
opPeriodData valueBalanceData balanceDataPeriodEnds opPeriodData valueBalanceData (dayPartitionToPeriodData colspans)
where where
valueBalanceData :: Day -> BalanceData -> BalanceData valueBalanceData :: Day -> BalanceData -> BalanceData
valueBalanceData d = mapBalanceData (valueMixedAmount d) valueBalanceData d = mapBalanceData (valueMixedAmount d)
@ -311,10 +310,6 @@ periodDataValuation ropts j priceoracle periodEnds =
valueMixedAmount :: Day -> MixedAmount -> MixedAmount valueMixedAmount :: Day -> MixedAmount -> MixedAmount
valueMixedAmount = mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle 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" $ addDays (-1) <$> periodEnds
-- | Mark which nodes of an 'Account' are boring, and so should be omitted from reports. -- | Mark which nodes of an 'Account' are boring, and so should be omitted from reports.
markAccountBoring :: ReportSpec -> Account BalanceData -> Account BalanceData markAccountBoring :: ReportSpec -> Account BalanceData -> Account BalanceData
markAccountBoring ReportSpec{_rsQuery=query,_rsReportOpts=ropts} markAccountBoring ReportSpec{_rsQuery=query,_rsReportOpts=ropts}
@ -367,7 +362,7 @@ markAccountBoring ReportSpec{_rsQuery=query,_rsReportOpts=ropts}
-- | Build a report row. -- | Build a report row.
-- --
-- Calculate the column totals. These are always the sum of column amounts. -- Calculate the column totals. These are always the sum of column amounts.
generateMultiBalanceReport :: ReportOpts -> Maybe (PeriodData Day) -> Account BalanceData -> MultiBalanceReport generateMultiBalanceReport :: ReportOpts -> Maybe DayPartition -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport ropts colspans = generateMultiBalanceReport ropts colspans =
reportPercent ropts . generatePeriodicReport makeMultiBalanceReportRow bdincludingsubs id ropts colspans reportPercent ropts . generatePeriodicReport makeMultiBalanceReportRow bdincludingsubs id ropts colspans
@ -377,9 +372,9 @@ generateMultiBalanceReport ropts colspans =
generatePeriodicReport :: Show c => generatePeriodicReport :: Show c =>
(forall a. ReportOpts -> (BalanceData -> MixedAmount) -> a -> Account b -> PeriodicReportRow a c) (forall a. ReportOpts -> (BalanceData -> MixedAmount) -> a -> Account b -> PeriodicReportRow a c)
-> (b -> MixedAmount) -> (c -> MixedAmount) -> (b -> MixedAmount) -> (c -> MixedAmount)
-> ReportOpts -> Maybe (PeriodData Day) -> Account b -> PeriodicReport DisplayName c -> ReportOpts -> Maybe DayPartition -> Account b -> PeriodicReport DisplayName c
generatePeriodicReport makeRow treeAmt flatAmt ropts colspans acct = generatePeriodicReport makeRow treeAmt flatAmt ropts colspans acct =
PeriodicReport (maybePeriodDataToDateSpans colspans) (buildAndSort acct) totalsrow PeriodicReport (maybeDayPartitionToDateSpans colspans) (buildAndSort acct) totalsrow
where where
-- Build report rows and sort them -- Build report rows and sort them
buildAndSort = dbg5 "generatePeriodicReport buildAndSort" . case accountlistmode_ ropts of buildAndSort = dbg5 "generatePeriodicReport buildAndSort" . case accountlistmode_ ropts of

View File

@ -209,12 +209,12 @@ mkpostingsReportItem showdate showdesc wd mperiod p b =
-- | Convert a list of postings into summary postings, one per interval, -- | Convert a list of postings into summary postings, one per interval,
-- aggregated to the specified depth if any. -- aggregated to the specified depth if any.
-- Each summary posting will have a non-Nothing interval end date. -- Each summary posting will have a non-Nothing interval end date.
summarisePostingsByInterval :: WhichDate -> Maybe Int -> Bool -> Maybe (PeriodData Day) -> [Posting] -> [SummaryPosting] summarisePostingsByInterval :: WhichDate -> Maybe Int -> Bool -> Maybe DayPartition -> [Posting] -> [SummaryPosting]
summarisePostingsByInterval wd mdepth showempty colspans = summarisePostingsByInterval wd mdepth showempty colspans =
concatMap (\(s,ps) -> summarisePostingsInDateSpan s wd mdepth showempty ps) concatMap (\(s,ps) -> summarisePostingsInDateSpan s wd mdepth showempty ps)
-- Group postings into their columns. We try to be efficient, since -- Group postings into their columns. We try to be efficient, since
-- there can possibly be a very large number of intervals (cf #1683) -- there can possibly be a very large number of intervals (cf #1683)
. groupByDateSpan showempty (postingDateOrDate2 wd) (maybePeriodDataToDateSpans colspans) . groupByDateSpan showempty (postingDateOrDate2 wd) (maybeDayPartitionToDateSpans colspans)
-- | Given a date span (representing a report interval) and a list of -- | Given a date span (representing a report interval) and a list of
-- postings within it, aggregate the postings into one summary posting per -- postings within it, aggregate the postings into one summary posting per

View File

@ -677,7 +677,7 @@ journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceo
_ -> spanEnd <=< latestSpanContaining (historical : spans) _ -> spanEnd <=< latestSpanContaining (historical : spans)
historical = DateSpan Nothing $ (fmap Exact . spanStart) =<< headMay spans historical = DateSpan Nothing $ (fmap Exact . spanStart) =<< headMay spans
spans = maybePeriodDataToDateSpans . snd $ reportSpanBothDates j rspec spans = maybeDayPartitionToDateSpans . snd $ reportSpanBothDates j rspec
styles = journalCommodityStyles j styles = journalCommodityStyles j
err = error' "journalApplyValuationFromOpts: expected all spans to have an end date" 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. -- (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. -- 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. -- The report sub-periods caused by a report interval, if any, are also returned.
reportSpan :: Journal -> ReportSpec -> (DateSpan, Maybe (PeriodData Day)) reportSpan :: Journal -> ReportSpec -> (DateSpan, Maybe DayPartition)
reportSpan = reportSpanHelper False reportSpan = reportSpanHelper False
-- Note: In end value reports, the report end date and valuation date are the same. -- 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. -- 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. -- | Like reportSpan, but considers both primary and secondary dates, not just one or the other.
reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, Maybe (PeriodData Day)) reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, Maybe DayPartition)
reportSpanBothDates = reportSpanHelper True reportSpanBothDates = reportSpanHelper True
reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, Maybe (PeriodData Day)) reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, Maybe DayPartition)
reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts, _rsDay=today} = reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts, _rsDay=today} =
(enlargedreportspan, dateSpansToPeriodData $ if not (null intervalspans) then intervalspans else [enlargedreportspan]) (enlargedreportspan, dateSpansToDayPartition $ if not (null intervalspans) then intervalspans else [enlargedreportspan])
where where
-- The date span specified by -b/-e/-p options and query args if any. -- The date span specified by -b/-e/-p options and query args if any.
requestedspan = dbg3 "requestedspan" $ requestedspan = dbg3 "requestedspan" $

View File

@ -61,6 +61,7 @@ library
Hledger.Data.Balancing Hledger.Data.Balancing
Hledger.Data.Currency Hledger.Data.Currency
Hledger.Data.Dates Hledger.Data.Dates
Hledger.Data.DayPartition
Hledger.Data.Errors Hledger.Data.Errors
Hledger.Data.Journal Hledger.Data.Journal
Hledger.Data.JournalChecks Hledger.Data.JournalChecks

View File

@ -39,8 +39,8 @@ showHistogram rspec@ReportSpec{_rsQuery=q} j =
_ -> rspec _ -> rspec
spanps = case mspans of spanps = case mspans of
Nothing -> [] Nothing -> []
Just x -> map (\spn -> (spn, filter (postingInRange spn) ps)) . snd $ periodDataToList x Just x -> map (\spn -> (spn, filter (postingInRange spn) ps)) $ dayPartitionToList x
postingInRange (b, e) p = postingDate p >= b && postingDate p < e postingInRange (b, e) p = postingDate p >= b && postingDate p <= e
-- same as Register -- same as Register
-- should count transactions, not postings ? -- should count transactions, not postings ?
-- ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j -- ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j

View File

@ -97,7 +97,7 @@ roi CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsReportOpts=ReportO
let (fullPeriodDateSpan, mspans) = reportSpan filteredj rspec let (fullPeriodDateSpan, mspans) = reportSpan filteredj rspec
let err = error' "Undefined start or end of the period - will be unable to compute the rates of return" 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 spans = maybe err (map (second (addDays 1)) . dayPartitionToList) mspans
fullPeriod = case fullPeriodDateSpan of fullPeriod = case fullPeriodDateSpan of
DateSpan (Just b) (Just e) -> (fromEFDay b, fromEFDay e) DateSpan (Just b) (Just e) -> (fromEFDay b, fromEFDay e)
_ -> err _ -> err

View File

@ -57,7 +57,7 @@ stats opts@CliOpts{rawopts_=rawopts, reportspec_=rspec, progstarttime_} j = do
l = ledgerFromJournal q j l = ledgerFromJournal q j
intervalspans = snd $ reportSpanBothDates j rspec intervalspans = snd $ reportSpanBothDates j rspec
ismultiperiod = length intervalspans > 1 ismultiperiod = length intervalspans > 1
(ls, txncounts) = unzip . map (showLedgerStats verbose l today) $ maybePeriodDataToDateSpans intervalspans (ls, txncounts) = unzip . map (showLedgerStats verbose l today) $ maybeDayPartitionToDateSpans intervalspans
numtxns = sum txncounts numtxns = sum txncounts
txt = (if ismultiperiod then id else TL.init) $ TB.toLazyText $ unlinesB ls txt = (if ismultiperiod then id else TL.init) $ TB.toLazyText $ unlinesB ls
writeOutputLazyText opts txt writeOutputLazyText opts txt