From e33de3585ba10c7489dec3de5437831ab560a8b2 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Tue, 4 Jan 2022 15:38:21 +0100 Subject: [PATCH] ref: Clean up splitSpan, to make the logic clearer and more extensible. Previously the helper functions splitspan and splitspan' would calculate each span from the start point of the previous span. This meant we had to be very careful not to lose any relevant information (e.g. what day of the week it was) about the original start date. We now calculate each span from the original start date, so there's no risk of losing information. This simplifies many of the calculations. --- hledger-lib/Hledger/Data/Dates.hs | 81 +++++++++++++++---------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/hledger-lib/Hledger/Data/Dates.hs b/hledger-lib/Hledger/Data/Dates.hs index 88bb8dba1..8b512e6b7 100644 --- a/hledger-lib/Hledger/Data/Dates.hs +++ b/hledger-lib/Hledger/Data/Dates.hs @@ -67,6 +67,7 @@ module Hledger.Data.Dates ( latestSpanContaining, smartdate, splitSpan, + spansFromBoundaries, groupByDateSpan, fixSmartDate, fixSmartDateStr, @@ -207,51 +208,47 @@ spansSpan spans = DateSpan (spanStart =<< headMay spans) (spanEnd =<< lastMay sp -- splitSpan :: Interval -> DateSpan -> [DateSpan] splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing] -splitSpan _ s | isEmptySpan s = [] -splitSpan NoInterval s = [s] -splitSpan (Days n) s = splitspan startofday (applyN n nextday) s -splitSpan (Weeks n) s = splitspan startofweek (applyN n nextweek) s -splitSpan (Months n) s = splitspan startofmonth (applyN n nextmonth) s -splitSpan (Quarters n) s = splitspan startofquarter (applyN n nextquarter) s -splitSpan (Years n) s = splitspan startofyear (applyN n nextyear) s -splitSpan (DayOfMonth n) s = splitspan (nthdayofmonthcontaining n) (nthdayofmonth n . nextmonth) s -splitSpan (WeekdayOfMonth n wd) s = splitspan (nthweekdayofmonthcontaining n wd) (advancetonthweekday n wd . nextmonth) s -splitSpan (DaysOfWeek []) s = [s] -- shouldn't happen in parser but for completeness -splitSpan (DaysOfWeek days@(n:_)) ds - | DateSpan Nothing (Just e) <- ds = split (DateSpan (Just $ start e) (Just $ nextday $ start e)) - | DateSpan (Just s) Nothing <- ds = split (DateSpan (Just $ start s) (Just $ nextday $ start s)) - | DateSpan (Just s) (Just e) <- ds = - if s == e then [ds] else split (DateSpan (Just $ start s) (Just e)) +splitSpan _ ds | isEmptySpan ds = [] +splitSpan _ ds@(DateSpan (Just s) (Just e)) | s == e = [ds] +splitSpan NoInterval ds = [ds] +splitSpan (Days n) ds = splitspan startofday addDays n ds +splitSpan (Weeks n) ds = splitspan startofweek addDays (7*n) ds +splitSpan (Months n) ds = splitspan startofmonth addGregorianMonthsClip n ds +splitSpan (Quarters n) ds = splitspan startofquarter addGregorianMonthsClip (3*n) ds +splitSpan (Years n) ds = splitspan startofyear addGregorianYearsClip n ds +splitSpan (DayOfMonth n) ds = splitspan (nthdayofmonthcontaining n) addGregorianMonthsClip 1 ds +splitSpan (DayOfYear m n) ds = splitspan (nthdayofyearcontaining m n) addGregorianYearsClip 1 ds +splitSpan (WeekdayOfMonth n wd) ds = splitspan (nthweekdayofmonthcontaining n wd) advancemonths 1 ds where - start = nthdayofweekcontaining n - - wheel = (\x -> zipWith (-) (tail x) x) . concat . zipWith fmap (fmap (+) [0,7..]) . repeat $ days - - split = splitspan' (repeat startofday) (fmap (`applyN` nextday) wheel) - -splitSpan (DayOfYear m n) s = splitspan (nthdayofyearcontaining m n) (applyN (n-1) nextday . applyN (m-1) nextmonth . nextyear) s --- splitSpan (WeekOfYear n) s = splitspan startofweek (applyN n nextweek) s --- splitSpan (MonthOfYear n) s = splitspan startofmonth (applyN n nextmonth) s --- splitSpan (QuarterOfYear n) s = splitspan startofquarter (applyN n nextquarter) s + advancemonths 0 = id + advancemonths w = advancetonthweekday n wd . startofmonth . addGregorianMonthsClip w +splitSpan (DaysOfWeek []) ds = [ds] +splitSpan (DaysOfWeek days@(n:_)) ds = spansFromBoundaries e bdrys + where + (s, e) = dateSpanSplitLimits (nthdayofweekcontaining n) nextday ds + bdrys = concatMap (flip map starts . addDays) [0,7..] + -- The first representative of each weekday + starts = map (\d -> addDays (toInteger $ d - n) $ nthdayofweekcontaining n s) days -- Split the given span using the provided helper functions: -- start is applied to the span's start date to get the first sub-span's start date --- next is applied to a sub-span's start date to get the next sub-span's start date -splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan] -splitspan _ _ (DateSpan Nothing Nothing) = [] -splitspan start next (DateSpan Nothing (Just e)) = splitspan start next (DateSpan (Just $ start e) (Just $ next $ start e)) -splitspan start next (DateSpan (Just s) Nothing) = splitspan start next (DateSpan (Just $ start s) (Just $ next $ start s)) -splitspan start next span@(DateSpan (Just s) (Just e)) - | s == e = [span] - | otherwise = splitspan' (repeat start) (repeat next) span +-- addInterval is applied to an integer n (multiplying it by mult) and the span's start date to get the nth sub-span's start date +splitspan :: (Day -> Day) -> (Integer -> Day -> Day) -> Int -> DateSpan -> [DateSpan] +splitspan start addInterval mult ds = spansFromBoundaries e bdrys + where + (s, e) = dateSpanSplitLimits start (addInterval $ toInteger mult) ds + bdrys = mapM (addInterval . toInteger) [0,mult..] $ start s -splitspan' :: [Day -> Day] -> [Day -> Day] -> DateSpan -> [DateSpan] -splitspan' (start:ss) (next:ns) (DateSpan (Just s) (Just e)) - | s >= e = [] - | otherwise = DateSpan (Just subs) (Just sube) : splitspan' ss ns (DateSpan (Just sube) (Just e)) - where subs = start s - sube = next subs -splitspan' _ _ _ = error' "won't happen, avoids warnings" -- PARTIAL: +-- | Fill in missing endpoints for calculating 'splitSpan'. +dateSpanSplitLimits :: (Day -> Day) -> (Day -> Day) -> DateSpan -> (Day, Day) +dateSpanSplitLimits start _ (DateSpan (Just s) (Just e)) = (start s, e) +dateSpanSplitLimits start next (DateSpan (Just s) Nothing) = (start s, next $ start s) +dateSpanSplitLimits start next (DateSpan Nothing (Just e)) = (start e, next $ start e) +dateSpanSplitLimits _ _ (DateSpan Nothing Nothing) = error "dateSpanSplitLimits: Should not be nulldatespan" -- PARTIAL: This case should have been handled in splitSpan + +-- | Construct a list of 'DateSpan's from a list of boundaries, which fit within a given range. +spansFromBoundaries :: Day -> [Day] -> [DateSpan] +spansFromBoundaries e bdrys = zipWith (DateSpan `on` Just) (takeWhile (< e) bdrys) $ drop 1 bdrys -- | Count the days in a DateSpan, or if it is open-ended return Nothing. daysInSpan :: DateSpan -> Maybe Integer @@ -1069,8 +1066,8 @@ tests_Dates = testGroup "Dates" ] , testCase "match dayOfWeek" $ do - let dayofweek n s = splitspan (nthdayofweekcontaining n) (applyN (n-1) nextday . nextweek) s - match ds day = dayofweek day ds == splitSpan (DaysOfWeek [day]) ds @?= True + let dayofweek n s = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1 s + match ds day = splitSpan (DaysOfWeek [day]) ds @?= dayofweek day ds ys2021 = fromGregorian 2021 01 01 ye2021 = fromGregorian 2021 12 31 ys2022 = fromGregorian 2022 01 01