Skip to content

Commit 169fdf7

Browse files
committed
dev!: lib: Refactor splitSpan to return Maybe DayPartition.
This eliminates all error calls from the chain calculating report periods.
1 parent d204675 commit 169fdf7

File tree

6 files changed

+210
-195
lines changed

6 files changed

+210
-195
lines changed

hledger-lib/Hledger/Data.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ import Hledger.Data.Valuation
6666
tests_Data = testGroup "Data" [
6767
tests_Account
6868
,tests_AccountName
69-
,tests_BalanceData
70-
,tests_PeriodData
7169
,tests_Amount
70+
,tests_BalanceData
7271
,tests_Balancing
72+
,tests_DayPartition
7373
-- ,tests_Currency
74-
,tests_Dates
7574
,tests_Journal
7675
,tests_Ledger
76+
,tests_PeriodData
7777
,tests_Posting
7878
,tests_Valuation
7979
,tests_StringFormat

hledger-lib/Hledger/Data/Dates.hs

Lines changed: 16 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ module Hledger.Data.Dates (
7070
daysSpan,
7171
latestSpanContaining,
7272
smartdate,
73-
splitSpan,
74-
spansFromBoundaries,
7573
groupByDateSpan,
7674
fixSmartDate,
7775
fixSmartDateStr,
@@ -80,9 +78,22 @@ module Hledger.Data.Dates (
8078
yearp,
8179
daysInSpan,
8280

83-
tests_Dates
84-
, intervalBoundaryBefore)
85-
where
81+
-- Temp exports
82+
startofyear,
83+
startofquarter,
84+
startofmonth,
85+
startofweek,
86+
nextday,
87+
nextweek,
88+
nextmonthandday,
89+
nextnthdayofmonth,
90+
prevNthWeekdayOfMonth,
91+
nthdayofweekcontaining,
92+
addGregorianMonthsToMonthday,
93+
advanceToNthWeekday,
94+
nextNthWeekdayOfMonth,
95+
isEmptySpan
96+
) where
8697

8798
import Prelude hiding (Applicative(..))
8899
import Control.Applicative (Applicative(..))
@@ -188,76 +199,6 @@ spanYears (DateSpan ma mb) = mapMaybe (fmap (first3 . toGregorian. fromEFDay)) [
188199
spansSpan :: [DateSpan] -> DateSpan
189200
spansSpan spans = DateSpan (spanStartDate =<< headMay spans) (spanEndDate =<< lastMay spans)
190201

191-
-- | Split a DateSpan into consecutive exact spans of the specified Interval.
192-
-- If no interval is specified, the original span is returned.
193-
-- If the original span is the null date span, ie unbounded, the null date span is returned.
194-
-- If the original span is empty, eg if the end date is <= the start date, no spans are returned.
195-
--
196-
-- ==== Date adjustment
197-
-- Some intervals respect the "adjust" flag (years, quarters, months, weeks, every Nth weekday
198-
-- of month seem to be the ones that need it). This will move the start date earlier, if needed,
199-
-- to the previous natural interval boundary (first of year, first of quarter, first of month,
200-
-- monday, previous Nth weekday of month). Related: #1982 #2218
201-
--
202-
-- The end date is always moved later if needed to the next natural interval boundary,
203-
-- so that the last period is the same length as the others.
204-
--
205-
-- ==== Examples
206-
-- >>> let t i y1 m1 d1 y2 m2 d2 = splitSpan True i $ DateSpan (Just $ Flex $ fromGregorian y1 m1 d1) (Just $ Flex $ fromGregorian y2 m2 d2)
207-
-- >>> t NoInterval 2008 01 01 2009 01 01
208-
-- [DateSpan 2008]
209-
-- >>> t (Quarters 1) 2008 01 01 2009 01 01
210-
-- [DateSpan 2008Q1,DateSpan 2008Q2,DateSpan 2008Q3,DateSpan 2008Q4]
211-
-- >>> splitSpan True (Quarters 1) nulldatespan
212-
-- [DateSpan ..]
213-
-- >>> t (Days 1) 2008 01 01 2008 01 01 -- an empty datespan
214-
-- []
215-
-- >>> t (Quarters 1) 2008 01 01 2008 01 01
216-
-- []
217-
-- >>> t (Months 1) 2008 01 01 2008 04 01
218-
-- [DateSpan 2008-01,DateSpan 2008-02,DateSpan 2008-03]
219-
-- >>> t (Months 2) 2008 01 01 2008 04 01
220-
-- [DateSpan 2008-01-01..2008-02-29,DateSpan 2008-03-01..2008-04-30]
221-
-- >>> t (Weeks 1) 2008 01 01 2008 01 15
222-
-- [DateSpan 2008-W01,DateSpan 2008-W02,DateSpan 2008-W03]
223-
-- >>> t (Weeks 2) 2008 01 01 2008 01 15
224-
-- [DateSpan 2007-12-31..2008-01-13,DateSpan 2008-01-14..2008-01-27]
225-
-- >>> t (MonthDay 2) 2008 01 01 2008 04 01
226-
-- [DateSpan 2008-01-02..2008-02-01,DateSpan 2008-02-02..2008-03-01,DateSpan 2008-03-02..2008-04-01]
227-
-- >>> t (NthWeekdayOfMonth 2 4) 2011 01 01 2011 02 15
228-
-- [DateSpan 2010-12-09..2011-01-12,DateSpan 2011-01-13..2011-02-09,DateSpan 2011-02-10..2011-03-09]
229-
-- >>> t (DaysOfWeek [2]) 2011 01 01 2011 01 15
230-
-- [DateSpan 2010-12-28..2011-01-03,DateSpan 2011-01-04..2011-01-10,DateSpan 2011-01-11..2011-01-17]
231-
-- >>> t (MonthAndDay 11 29) 2012 10 01 2013 10 15
232-
-- [DateSpan 2012-11-29..2013-11-28]
233-
--
234-
splitSpan :: Bool -> Interval -> DateSpan -> [DateSpan]
235-
splitSpan _ _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]
236-
splitSpan _ _ ds | isEmptySpan ds = []
237-
splitSpan _ _ ds@(DateSpan (Just s) (Just e)) | s == e = [ds]
238-
splitSpan _ NoInterval ds = [ds]
239-
splitSpan _ (Days n) ds = splitspan id addDays n ds
240-
splitSpan adjust (Weeks n) ds = splitspan (if adjust then startofweek else id) addDays (7*n) ds
241-
splitSpan adjust (Months n) ds = splitspan (if adjust then startofmonth else id) addGregorianMonthsClip n ds
242-
splitSpan adjust (Quarters n) ds = splitspan (if adjust then startofquarter else id) addGregorianMonthsClip (3*n) ds
243-
splitSpan adjust (Years n) ds = splitspan (if adjust then startofyear else id) addGregorianYearsClip n ds
244-
splitSpan adjust (NthWeekdayOfMonth n wd) ds = splitspan (if adjust then prevstart else nextstart) advancemonths 1 ds
245-
where
246-
prevstart = prevNthWeekdayOfMonth n wd
247-
nextstart = nextNthWeekdayOfMonth n wd
248-
advancemonths 0 = id
249-
advancemonths m = advanceToNthWeekday n wd . startofmonth . addGregorianMonthsClip m
250-
splitSpan _ (MonthDay dom) ds = splitspan (nextnthdayofmonth dom) (addGregorianMonthsToMonthday dom) 1 ds
251-
splitSpan _ (MonthAndDay m d) ds = splitspan (nextmonthandday m d) (addGregorianYearsClip) 1 ds
252-
splitSpan _ (DaysOfWeek []) ds = [ds]
253-
splitSpan _ (DaysOfWeek days@(n:_)) ds = spansFromBoundaries e bdrys
254-
where
255-
(s, e) = dateSpanSplitLimits (nthdayofweekcontaining n) nextday ds
256-
-- can't show this when debugging, it'll hang:
257-
bdrys = concatMap (flip map starts . addDays) [0,7..]
258-
-- The first representative of each weekday
259-
starts = map (\d -> addDays (toInteger $ d - n) $ nthdayofweekcontaining n s) days
260-
261202
-- Like addGregorianMonthsClip, add one month to the given date, clipping when needed
262203
-- to fit it within the next month's length. But also, keep a target day of month in mind,
263204
-- and revert to that or as close to it as possible in subsequent longer months.
@@ -267,31 +208,6 @@ addGregorianMonthsToMonthday dom n d =
267208
let (y,m,_) = toGregorian $ addGregorianMonthsClip n d
268209
in fromGregorian y m dom
269210

270-
-- Split the given span into exact spans using the provided helper functions:
271-
--
272-
-- 1. The start function is used to adjust the provided span's start date to get the first sub-span's start date.
273-
--
274-
-- 2. The next function is used to calculate subsequent sub-spans' start dates, possibly with stride increased by a multiplier.
275-
-- It should handle spans of varying length, eg when splitting on "every 31st of month",
276-
-- it adjusts to 28/29/30 in short months but returns to 31 in the long months.
277-
--
278-
splitspan :: (Day -> Day) -> (Integer -> Day -> Day) -> Int -> DateSpan -> [DateSpan]
279-
splitspan start next mult ds = spansFromBoundaries e bdrys
280-
where
281-
(s, e) = dateSpanSplitLimits start (next (toInteger mult)) ds
282-
bdrys = mapM (next . toInteger) [0,mult..] $ start s
283-
284-
-- | Fill in missing start/end dates for calculating 'splitSpan'.
285-
dateSpanSplitLimits :: (Day -> Day) -> (Day -> Day) -> DateSpan -> (Day, Day)
286-
dateSpanSplitLimits start _ (DateSpan (Just s) (Just e)) = (start $ fromEFDay s, fromEFDay e)
287-
dateSpanSplitLimits start next (DateSpan (Just s) Nothing) = (start $ fromEFDay s, next $ start $ fromEFDay s)
288-
dateSpanSplitLimits start next (DateSpan Nothing (Just e)) = (start $ fromEFDay e, next $ start $ fromEFDay e)
289-
dateSpanSplitLimits _ _ (DateSpan Nothing Nothing) = error' "dateSpanSplitLimits: should not be nulldatespan" -- PARTIAL: This case should have been handled in splitSpan
290-
291-
-- | Construct a list of exact 'DateSpan's from a list of boundaries, which fit within a given range.
292-
spansFromBoundaries :: Day -> [Day] -> [DateSpan]
293-
spansFromBoundaries e bdrys = zipWith (DateSpan `on` (Just . Exact)) (takeWhile (< e) bdrys) $ drop 1 bdrys
294-
295211
-- | Count the days in a DateSpan, or if it is open-ended return Nothing.
296212
daysInSpan :: DateSpan -> Maybe Integer
297213
daysInSpan (DateSpan (Just d1) (Just d2)) = Just $ diffDays (fromEFDay d2) (fromEFDay d1)
@@ -669,14 +585,6 @@ thisyear = startofyear
669585
nextyear = startofyear . addGregorianYearsClip 1
670586
startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day
671587

672-
-- Get the natural start for the given interval that falls on or before the given day,
673-
-- when applicable. Works for Weeks, Months, Quarters, Years, eg.
674-
intervalBoundaryBefore :: Interval -> Day -> Day
675-
intervalBoundaryBefore i d =
676-
case splitSpan True i (DateSpan (Just $ Exact d) (Just $ Exact $ addDays 1 d)) of
677-
(DateSpan (Just start) _:_) -> fromEFDay start
678-
_ -> d
679-
680588
-- | Find the next occurrence of the specified month and day of month, on or after the given date.
681589
-- The month should be 1-12 and the day of month should be 1-31, or an error will be raised.
682590
--
@@ -1263,45 +1171,3 @@ emptydatespan = DateSpan (Just $ Exact $ addDays 1 nulldate) (Just $ Exact nulld
12631171

12641172
nulldate :: Day
12651173
nulldate = fromGregorian 0 1 1
1266-
1267-
1268-
-- tests
1269-
1270-
tests_Dates = testGroup "Dates"
1271-
[ testCase "weekday" $ do
1272-
splitSpan False (DaysOfWeek [1..5]) (DateSpan (Just $ Exact $ fromGregorian 2021 07 01) (Just $ Exact $ fromGregorian 2021 07 08))
1273-
@?= [ (DateSpan (Just $ Exact $ fromGregorian 2021 06 28) (Just $ Exact $ fromGregorian 2021 06 29))
1274-
, (DateSpan (Just $ Exact $ fromGregorian 2021 06 29) (Just $ Exact $ fromGregorian 2021 06 30))
1275-
, (DateSpan (Just $ Exact $ fromGregorian 2021 06 30) (Just $ Exact $ fromGregorian 2021 07 01))
1276-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 01) (Just $ Exact $ fromGregorian 2021 07 02))
1277-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 02) (Just $ Exact $ fromGregorian 2021 07 05))
1278-
-- next week
1279-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 05) (Just $ Exact $ fromGregorian 2021 07 06))
1280-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 06) (Just $ Exact $ fromGregorian 2021 07 07))
1281-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 07) (Just $ Exact $ fromGregorian 2021 07 08))
1282-
]
1283-
1284-
splitSpan False (DaysOfWeek [1, 5]) (DateSpan (Just $ Exact $ fromGregorian 2021 07 01) (Just $ Exact $ fromGregorian 2021 07 08))
1285-
@?= [ (DateSpan (Just $ Exact $ fromGregorian 2021 06 28) (Just $ Exact $ fromGregorian 2021 07 02))
1286-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 02) (Just $ Exact $ fromGregorian 2021 07 05))
1287-
-- next week
1288-
, (DateSpan (Just $ Exact $ fromGregorian 2021 07 05) (Just $ Exact $ fromGregorian 2021 07 09))
1289-
]
1290-
1291-
, testCase "match dayOfWeek" $ do
1292-
let dayofweek n = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1
1293-
matchdow ds day = splitSpan False (DaysOfWeek [day]) ds @?= dayofweek day ds
1294-
ys2021 = fromGregorian 2021 01 01
1295-
ye2021 = fromGregorian 2021 12 31
1296-
ys2022 = fromGregorian 2022 01 01
1297-
mapM_ (matchdow (DateSpan (Just $ Exact ys2021) (Just $ Exact ye2021))) [1..7]
1298-
mapM_ (matchdow (DateSpan (Just $ Exact ys2021) (Just $ Exact ys2022))) [1..7]
1299-
mapM_ (matchdow (DateSpan (Just $ Exact ye2021) (Just $ Exact ys2022))) [1..7]
1300-
1301-
mapM_ (matchdow (DateSpan (Just $ Exact ye2021) Nothing)) [1..7]
1302-
mapM_ (matchdow (DateSpan (Just $ Exact ys2022) Nothing)) [1..7]
1303-
1304-
mapM_ (matchdow (DateSpan Nothing (Just $ Exact ye2021))) [1..7]
1305-
mapM_ (matchdow (DateSpan Nothing (Just $ Exact ys2022))) [1..7]
1306-
1307-
]

0 commit comments

Comments
 (0)