Skip to content

Commit

Permalink
fix:queries: fix OR-ing open-ended dates, spanUnion; add spanExtend [#…
Browse files Browse the repository at this point in the history
  • Loading branch information
simonmichael committed Mar 2, 2024
1 parent b7d7dda commit 3ca208a
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 27 deletions.
91 changes: 67 additions & 24 deletions hledger-lib/Hledger/Data/Dates.hs
Expand Up @@ -63,6 +63,7 @@ module Hledger.Data.Dates (
spanIntersect,
spansIntersect,
spanDefaultsFrom,
spanExtend,
spanUnion,
spansUnion,
daysSpan,
Expand Down Expand Up @@ -314,8 +315,8 @@ groupByDateSpan showempty date colspans =
where
groupByCols [] _ = []
groupByCols (c:cs) [] = if showempty then (c, []) : groupByCols cs [] else []
groupByCols (c:cs) ps = (c, map snd matches) : groupByCols cs later
where (matches, later) = span ((spanEnd c >) . Just . fst) ps
groupByCols (c:cs) ps = (c, map snd colps) : groupByCols cs laterps
where (colps, laterps) = span ((spanEnd c >) . Just . fst) ps

beforeStart = maybe (const False) (>) $ spanStart =<< headMay colspans

Expand All @@ -324,40 +325,82 @@ spansIntersect [] = nulldatespan
spansIntersect [d] = d
spansIntersect (d:ds) = d `spanIntersect` (spansIntersect ds)

-- | Calculate the union of a number of datespans.
spansUnion [] = nulldatespan
spansUnion [d] = d
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 b e
where
b = latest b1 b2
e = earliest e1 e2
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. Sort of a one-way spanIntersect.
-- 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 a number of datespans.
spansUnion [] = nulldatespan
spansUnion [d] = d
spansUnion (d:ds) = d `spanUnion` (spansUnion ds)

-- | Calculate the union of two datespans.
spanUnion (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e
where
b = earliest b1 b2
e = latest e1 e2

latest d Nothing = d
latest Nothing d = d
latest (Just d1) (Just d2) = Just $ max d1 d2

earliest d Nothing = d
earliest Nothing d = d
earliest (Just d1) (Just d2) = Just $ min d1 d2
-- If either span is open-ended, the union will be too.
--
-- >>> ys2024 = fromGregorian 2024 01 01
-- >>> 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)

-- | Extend the first span to include any definite end dates of the second.
-- Unlike spanUnion, open ends in the second are ignored.
-- 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)

-- | Pick the earlier of two DateSpan starts, treating Nothing as infinitely early.
-- An Exact and Flex with the same date are considered equal; the first argument wins.
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
-- usual exclusive-end-date sense: beginning on the earliest, and ending on
Expand Down
2 changes: 1 addition & 1 deletion hledger-lib/Hledger/Reports/ReportOptions.hs
Expand Up @@ -696,7 +696,7 @@ reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} =
_ -> 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 `spanUnion` pricespan)
requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (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
Expand Down
2 changes: 0 additions & 2 deletions hledger/hledger.m4.md
Expand Up @@ -5050,8 +5050,6 @@ This allows one to combine queries using `AND`, `OR`, and `NOT`.

`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

Some queries can also be expressed as command-line options:
Expand Down
12 changes: 12 additions & 0 deletions hledger/test/query-expr.test
Expand Up @@ -146,3 +146,15 @@ $ hledger -f - print expr:"not (tag:transactiontag=B)"
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

0 comments on commit 3ca208a

Please sign in to comment.