fix:queries: fix OR-ing open-ended dates, spanUnion; add spanExtend [#2177]

This commit is contained in:
Simon Michael 2024-03-01 18:07:13 -10:00
parent b7d7dda463
commit 3ca208a3b6
4 changed files with 84 additions and 31 deletions

View File

@ -63,6 +63,7 @@ module Hledger.Data.Dates (
spanIntersect, spanIntersect,
spansIntersect, spansIntersect,
spanDefaultsFrom, spanDefaultsFrom,
spanExtend,
spanUnion, spanUnion,
spansUnion, spansUnion,
daysSpan, daysSpan,
@ -314,8 +315,8 @@ groupByDateSpan showempty date colspans =
where where
groupByCols [] _ = [] groupByCols [] _ = []
groupByCols (c:cs) [] = if showempty then (c, []) : groupByCols cs [] else [] groupByCols (c:cs) [] = if showempty then (c, []) : groupByCols cs [] else []
groupByCols (c:cs) ps = (c, map snd matches) : groupByCols cs later groupByCols (c:cs) ps = (c, map snd colps) : groupByCols cs laterps
where (matches, later) = span ((spanEnd c >) . Just . fst) ps where (colps, laterps) = span ((spanEnd c >) . Just . fst) ps
beforeStart = maybe (const False) (>) $ spanStart =<< headMay colspans beforeStart = maybe (const False) (>) $ spanStart =<< headMay colspans
@ -324,40 +325,82 @@ spansIntersect [] = nulldatespan
spansIntersect [d] = d spansIntersect [d] = d
spansIntersect (d:ds) = d `spanIntersect` (spansIntersect ds) spansIntersect (d:ds) = d `spanIntersect` (spansIntersect ds)
-- | Calculate the intersection of two datespans.
--
-- For non-intersecting spans, gives an empty span beginning on the second's start date:
-- >>> DateSpan (Just $ Flex $ fromGregorian 2018 01 01) (Just $ Flex $ fromGregorian 2018 01 03) `spanIntersect` DateSpan (Just $ Flex $ fromGregorian 2018 01 03) (Just $ Flex $ fromGregorian 2018 01 05)
-- DateSpan 2018-01-03..2018-01-02
spanIntersect (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e
where
b = latest b1 b2
e = earliest e1 e2
-- | Fill any unspecified dates in the first span with the dates from
-- the second one. Sort of a one-way spanIntersect.
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
-- | Calculate the union of a number of datespans. -- | Calculate the union of a number of datespans.
spansUnion [] = nulldatespan spansUnion [] = nulldatespan
spansUnion [d] = d spansUnion [d] = d
spansUnion (d:ds) = d `spanUnion` (spansUnion ds) spansUnion (d:ds) = d `spanUnion` (spansUnion ds)
-- | Calculate the intersection of two datespans.
--
-- For non-intersecting spans, gives an empty span beginning on the second's start date:
-- >>> DateSpan (Just $ Flex $ fromGregorian 2018 01 01) (Just $ Flex $ fromGregorian 2018 01 03) `spanIntersect` DateSpan (Just $ Flex $ fromGregorian 2018 01 03) (Just $ Flex $ fromGregorian 2018 01 05)
-- DateSpan 2018-01-03..2018-01-02
spanIntersect (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan (laterDefinite b1 b2) (earlierDefinite e1 e2)
-- | Fill any unspecified dates in the first span with the dates from
-- the second one (if specified there). Sort of a one-way spanIntersect.
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
-- | Calculate the union of two datespans. -- | Calculate the union of two datespans.
spanUnion (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e -- If either span is open-ended, the union will be too.
where --
b = earliest b1 b2 -- >>> ys2024 = fromGregorian 2024 01 01
e = latest e1 e2 -- >>> ys2025 = fromGregorian 2025 01 01
-- >>> to2024 = DateSpan Nothing (Just $ Exact ys2024)
-- >>> in2024 = DateSpan (Just $ Exact ys2024) (Just $ Exact ys2025)
-- >>> spanUnion to2024 in2024
-- DateSpan ..2024-12-31
-- >>> spanUnion in2024 to2024
-- DateSpan ..2024-12-31
spanUnion (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan (earlier b1 b2) (later e1 e2)
latest d Nothing = d -- | Extend the first span to include any definite end dates of the second.
latest Nothing d = d -- Unlike spanUnion, open ends in the second are ignored.
latest (Just d1) (Just d2) = Just $ max d1 d2 -- If the first span was open-ended, it still will be after being extended.
--
-- >>> ys2024 = fromGregorian 2024 01 01
-- >>> ys2025 = fromGregorian 2025 01 01
-- >>> to2024 = DateSpan Nothing (Just $ Exact ys2024)
-- >>> all2024 = DateSpan (Just $ Exact ys2024) (Just $ Exact ys2025)
-- >>> partof2024 = DateSpan (Just $ Exact $ fromGregorian 2024 03 01) (Just $ Exact $ fromGregorian 2024 09 01)
-- >>> spanExtend to2024 all2024
-- DateSpan 2024
-- >>> spanExtend all2024 to2024
-- DateSpan 2024
-- >>> spanExtend partof2024 all2024
-- DateSpan 2024
-- >>> spanExtend all2024 partof2024
-- DateSpan 2024
--
spanExtend (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan (earlierDefinite b1 b2) (laterDefinite e1 e2)
earliest d Nothing = d -- | Pick the earlier of two DateSpan starts, treating Nothing as infinitely early.
earliest Nothing d = d -- An Exact and Flex with the same date are considered equal; the first argument wins.
earliest (Just d1) (Just d2) = Just $ min d1 d2 earlier :: Maybe EFDay -> Maybe EFDay -> Maybe EFDay
earlier = min
-- | Pick the later of two DateSpan starts, treating Nothing as infinitely late.
-- An Exact and Flex with the same date are considered equal; the second argument wins.
later :: Maybe EFDay -> Maybe EFDay -> Maybe EFDay
later _ Nothing = Nothing
later Nothing _ = Nothing
later d1 d2 = max d1 d2
-- | Pick the earlier of two DateSpan ends that is a definite date (if any).
-- An Exact and Flex with the same date are considered equal; the first argument wins.
earlierDefinite :: Maybe EFDay -> Maybe EFDay -> Maybe EFDay
earlierDefinite d1 Nothing = d1
earlierDefinite Nothing d2 = d2
earlierDefinite d1 d2 = min d1 d2
-- | Pick the later of two DateSpan ends that is a definite date (if any).
-- An Exact and Flex with the same date are considered equal; the second argument wins.
laterDefinite :: Maybe EFDay -> Maybe EFDay -> Maybe EFDay
laterDefinite d1 Nothing = d1
laterDefinite Nothing d2 = d2
laterDefinite d1 d2 = max d1 d2
-- | Calculate the minimal DateSpan containing all of the given Days (in the -- | Calculate the minimal DateSpan containing all of the given Days (in the
-- usual exclusive-end-date sense: beginning on the earliest, and ending on -- usual exclusive-end-date sense: beginning on the earliest, and ending on

View File

@ -696,7 +696,7 @@ reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} =
_ -> Nothing _ -> Nothing
-- If the requested span is open-ended, close it using the journal's start and end dates. -- 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. -- This can still be the null (open) span if the journal is empty.
requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanUnion` pricespan) requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanExtend` pricespan)
-- The list of interval spans enclosing the requested span. -- The list of interval spans enclosing the requested span.
-- This list can be empty if the journal was empty, -- This list can be empty if the journal was empty,
-- or if hledger-ui has added its special date:-tomorrow to the query -- or if hledger-ui has added its special date:-tomorrow to the query

View File

@ -5050,8 +5050,6 @@ This allows one to combine queries using `AND`, `OR`, and `NOT`.
`expr:"expenses:food OR (tag:A expenses:drink)"` `expr:"expenses:food OR (tag:A expenses:drink)"`
(But `OR` may not be reliable currently, [#2177](https://github.com/simonmichael/hledger/issues/2177).)
## Queries and command options ## Queries and command options
Some queries can also be expressed as command-line options: Some queries can also be expressed as command-line options:

View File

@ -146,3 +146,15 @@ $ hledger -f - print expr:"not (tag:transactiontag=B)"
expenses:food expenses:food
>= >=
# ** 11. Posting-based reports handle OR'd open-ended date periods properly. (#2177)
<
2023-12-26 2023
(2023) 2023
2024-01-26 2024
(2024) 2024
$ hledger -f- reg -w80 expr:'date:2023 or date:2024'
2023-12-26 2023 (2023) 2023 2023
2024-01-26 2024 (2024) 2024 4047