diff --git a/hledger-lib/Hledger/Data.hs b/hledger-lib/Hledger/Data.hs index 839a7175f..4173c4c4d 100644 --- a/hledger-lib/Hledger/Data.hs +++ b/hledger-lib/Hledger/Data.hs @@ -54,6 +54,7 @@ import Hledger.Utils.Test tests_Data = tests "Data" [ tests_AccountName ,tests_Amount + ,tests_Dates ,tests_Journal ,tests_Ledger ,tests_Posting diff --git a/hledger-lib/Hledger/Data/Dates.hs b/hledger-lib/Hledger/Data/Dates.hs index c9c0ca4bd..775c6d887 100644 --- a/hledger-lib/Hledger/Data/Dates.hs +++ b/hledger-lib/Hledger/Data/Dates.hs @@ -73,6 +73,8 @@ module Hledger.Data.Dates ( yearp, daysInSpan, maybePeriod, + + tests_Dates ) where @@ -193,7 +195,7 @@ spansSpan spans = DateSpan (maybe Nothing spanStart $ headMay spans) (maybe Noth -- [DateSpan 2007-12-02..2008-01-01,DateSpan 2008-01-02..2008-02-01,DateSpan 2008-02-02..2008-03-01,DateSpan 2008-03-02..2008-04-01] -- >>> t (WeekdayOfMonth 2 4) 2011 01 01 2011 02 15 -- [DateSpan 2010-12-09..2011-01-12,DateSpan 2011-01-13..2011-02-09,DateSpan 2011-02-10..2011-03-09] --- >>> t (DayOfWeek 2) 2011 01 01 2011 01 15 +-- >>> t (DaysOfWeek [2]) 2011 01 01 2011 01 15 -- [DateSpan 2010-12-28..2011-01-03,DateSpan 2011-01-04..2011-01-10,DateSpan 2011-01-11..2011-01-17] -- >>> t (DayOfYear 11 29) 2011 10 01 2011 10 15 -- [DateSpan 2010-11-29..2011-11-28] @@ -211,7 +213,19 @@ 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 (DayOfWeek n) s = splitspan (nthdayofweekcontaining n) (applyN (n-1) nextday . nextweek) 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)) + where + start = nthdayofweekcontaining n + + wheel = (\x -> zipWith (-) (tail x) x) . concat . zipWith fmap (fmap (+) [0,7..]) . repeat $ days + + split = splitspan' (repeat startofday) (fmap (flip 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 @@ -226,14 +240,15 @@ splitspan start next (DateSpan Nothing (Just e)) = splitspan start next (DateSpa 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' start next span - where - splitspan' start next (DateSpan (Just s) (Just e)) - | s >= e = [] - | otherwise = DateSpan (Just subs) (Just sube) : splitspan' start next (DateSpan (Just sube) (Just e)) - where subs = start s - sube = next subs - splitspan' _ _ _ = error' "won't happen, avoids warnings" -- PARTIAL: + | otherwise = splitspan' (repeat start) (repeat next) span + +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: -- | Count the days in a DateSpan, or if it is open-ended return Nothing. daysInSpan :: DateSpan -> Maybe Integer @@ -824,6 +839,9 @@ weekday = do [] -> Fail.fail $ "weekday: should not happen: attempted to find " <> show wday <> " in " <> show (weekdays ++ weekdayabbrevs) +weekdaysp :: TextParser m [Int] +weekdaysp = fmap head . groupBy (==) . sort <$> sepBy1 weekday (string' ",") + -- | Parse a period expression, specifying a date span and optionally -- a reporting interval. Requires a reference "today" date for -- resolving any relative start/end dates (only; it is not needed for @@ -867,9 +885,9 @@ weekday = do -- >>> p "every 1st monday of month to 2009" -- Right (WeekdayOfMonth 1 1,DateSpan ..2008-12-31) -- >>> p "every tue" --- Right (DayOfWeek 2,DateSpan ..) +-- Right (DaysOfWeek [2],DateSpan ..) -- >>> p "every 2nd day of week" --- Right (DayOfWeek 2,DateSpan ..) +-- Right (DaysOfWeek [2],DateSpan ..) -- >>> p "every 2nd day of month" -- Right (DayOfMonth 2,DateSpan ..) -- >>> p "every 2nd day" @@ -898,7 +916,6 @@ intervalanddateperiodexprp rdate = do reportingintervalp :: TextParser m Interval reportingintervalp = choice' [ tryinterval "day" "daily" Days - , tryinterval "week" "weekly" Weeks , tryinterval "month" "monthly" Months , tryinterval "quarter" "quarterly" Quarters , tryinterval "year" "yearly" Years @@ -906,13 +923,20 @@ reportingintervalp = choice' , Weeks 2 <$ string' "fortnightly" , Months 2 <$ string' "bimonthly" , string' "every" *> skipNonNewlineSpaces *> choice' - [ DayOfWeek <$> (nth <* skipNonNewlineSpaces <* string' "day" <* of_ "week") + [ DaysOfWeek . pure <$> (nth <* skipNonNewlineSpaces <* string' "day" <* of_ "week") , DayOfMonth <$> (nth <* skipNonNewlineSpaces <* string' "day" <* optOf_ "month") , liftA2 WeekdayOfMonth nth $ skipNonNewlineSpaces *> weekday <* optOf_ "month" , uncurry DayOfYear <$> (md <* optOf_ "year") - , DayOfWeek <$> weekday + , DaysOfWeek <$> weekdaysp + , DaysOfWeek [1..5] <$ string' "weekday" + , DaysOfWeek [6..7] <$ string' "weekendday" , d_o_y <* optOf_ "year" ] + -- NB: the ordering is important here since the parse for `every weekday` + -- would match the `tryinterval` first and then error on `d`. Perhaps it + -- would be clearer to factor some of this into the `every` choice or other + -- left-factorings. + , tryinterval "week" "weekly" Weeks ] where of_ period = @@ -1009,3 +1033,45 @@ emptydatespan = DateSpan (Just $ addDays 1 nulldate) (Just nulldate) nulldate :: Day nulldate = fromGregorian 0 1 1 + + +-- tests + +tests_Dates = tests "Dates" + [ test "weekday" $ do + splitSpan (DaysOfWeek [1..5]) (DateSpan (Just $ fromGregorian 2021 07 01) (Just $ fromGregorian 2021 07 08)) + @?= [ (DateSpan (Just $ fromGregorian 2021 06 28) (Just $ fromGregorian 2021 06 29)) + , (DateSpan (Just $ fromGregorian 2021 06 29) (Just $ fromGregorian 2021 06 30)) + , (DateSpan (Just $ fromGregorian 2021 06 30) (Just $ fromGregorian 2021 07 01)) + , (DateSpan (Just $ fromGregorian 2021 07 01) (Just $ fromGregorian 2021 07 02)) + , (DateSpan (Just $ fromGregorian 2021 07 02) (Just $ fromGregorian 2021 07 05)) + -- next week + , (DateSpan (Just $ fromGregorian 2021 07 05) (Just $ fromGregorian 2021 07 06)) + , (DateSpan (Just $ fromGregorian 2021 07 06) (Just $ fromGregorian 2021 07 07)) + , (DateSpan (Just $ fromGregorian 2021 07 07) (Just $ fromGregorian 2021 07 08)) + ] + + splitSpan (DaysOfWeek [1, 5]) (DateSpan (Just $ fromGregorian 2021 07 01) (Just $ fromGregorian 2021 07 08)) + @?= [ (DateSpan (Just $ fromGregorian 2021 06 28) (Just $ fromGregorian 2021 07 02)) + , (DateSpan (Just $ fromGregorian 2021 07 02) (Just $ fromGregorian 2021 07 05)) + -- next week + , (DateSpan (Just $ fromGregorian 2021 07 05) (Just $ fromGregorian 2021 07 09)) + ] + + , test "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 + ys2021 = fromGregorian 2021 01 01 + ye2021 = fromGregorian 2021 12 31 + ys2022 = fromGregorian 2022 01 01 + mapM_ (match (DateSpan (Just ys2021) (Just ye2021))) [1..7] + mapM_ (match (DateSpan (Just ys2021) (Just ys2022))) [1..7] + mapM_ (match (DateSpan (Just ye2021) (Just ys2022))) [1..7] + + mapM_ (match (DateSpan (Just ye2021) Nothing)) [1..7] + mapM_ (match (DateSpan (Just ys2022) Nothing)) [1..7] + + mapM_ (match (DateSpan Nothing (Just ye2021))) [1..7] + mapM_ (match (DateSpan Nothing (Just ys2022))) [1..7] + + ] diff --git a/hledger-lib/Hledger/Data/Types.hs b/hledger-lib/Hledger/Data/Types.hs index 372ccb3cf..eb935937e 100644 --- a/hledger-lib/Hledger/Data/Types.hs +++ b/hledger-lib/Hledger/Data/Types.hs @@ -121,7 +121,7 @@ data Interval = | Years Int | DayOfMonth Int | WeekdayOfMonth Int Int - | DayOfWeek Int + | DaysOfWeek [Int] | DayOfYear Int Int -- Month, Day -- WeekOfYear Int -- MonthOfYear Int diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 69cf1c09d..5114ff3f9 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -710,6 +710,39 @@ Group postings from the start of wednesday to end of the following tuesday (N is $ hledger register checking -p "every 3rd day of week" ``` +### Periods or dates ? + +Report intervals like the above are most often used with `-p|--period`, +to divide reports into multiple subperiods - +each generated date marks a subperiod boundary. +Here, the periods between the dates are what's important. + +But report intervals can also be used +with `--forecast` to generate future transactions, +or with `balance --budget` to generate budget goal-setting transactions. +For these, the dates themselves are what matters. + +### Events on multiple weekdays + +The `every WEEKDAYNAME` form has a special variant with multiple day names, comma-separated. +Eg: `every mon,thu,sat`. +Also, `weekday` and `weekendday` are shorthand for `mon,tue,wed,thu,fri` and `sat,sun` +respectively. + +This form is mainly intended for use with `--forecast`, to generate +[periodic transactions](#periodic-transactions) on arbitrary days of the week. +It may be less useful with `-p`, since it divides each week into subperiods +of unequal length. (Because gaps between periods are not allowed; +if you'd like to change this, see [#1632](https://github.com/simonmichael/hledger/pull/1632).) + +Examples: + +| | | +|------------------------------|----------------------------------------------------------------------------------------| +| `-p "every mon,wed,fri"` | dates will be Mon, Wed, Fri;
periods will be Mon-Tue, Wed-Thu, Fri-Sun | +| `-p "every weekday"` | dates will be Mon, Tue, Wed, Thu, Fri;
periods will be Mon, Tue, Wed, Thu, Fri-Sun | +| `-p "every weekendday"` | dates will be Sat, Sun;
periods will be Sat, Sun-Fri | + # DEPTH With the `--depth NUM` option (short form: `-NUM`), diff --git a/hledger/test/forecast.test b/hledger/test/forecast.test index 7ceba00db..e56071e6e 100644 --- a/hledger/test/forecast.test +++ b/hledger/test/forecast.test @@ -268,3 +268,57 @@ hledger -f - print -x --forecast -e 2021-11 >>>2 >>>=0 + +# 13. Generated forecast for weekday transactions +hledger -f - reg --forecast -b "2021-09-01" -e "2021-09-15" --forecast -w 100 +<<< +2021-08-01 + (a) 0 + +~ every weekday + income:client1 -10 USD + assets:receivables:contractor1 +>>> +2021-09-01 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-02 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-03 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-06 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-07 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-08 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-09 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-10 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-13 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-14 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +>>>2 +>>>=0 + +# 14. Generated forecast for weekend transactions +hledger -f - reg --forecast -b "2021-09-01" -e "2021-09-15" --forecast -w 100 +<<< +2021-08-01 + (a) 0 + +~ every weekendday + income:client1 -10 USD + assets:receivables:contractor1 +>>> +2021-09-04 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-05 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-11 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +2021-09-12 income:client1 -10 USD -10 USD + assets:receivables:contractor1 10 USD 0 +>>>2 +>>>=0