{-# LANGUAGE RecordWildCards #-} ----------------------------------------------------------------------------- -- | -- Module : Data.Time.Patterns -- Copyright : (C) 2013 Jann Mueller -- License : BSD3 (see the file LICENSE) -- Maintainer : j.mueller.11@ucl.ac.uk -- Stability : experimental -- Patterns for re-occurring events. Use the @DatePattern@ type to build up -- a pattern, and the functions @elementOf@, @instancesFrom@ and -- @intervalsFrom@ to evaluate it. -- Simple example: -- -- > import Control.Lens -- > import Data.Thyme.Calendar -- > import Data.Time.Patterns -- > import qualified Prelude as P -- > Module Main where -- > -- > main = do -- > -- get the 6th of April for the next ten years -- > let april6 = (take 1 $ skip 5 day) `inEach` april -- > let today = (YearMonthDay 2013 12 01)^.from gregorian -- > print $ P.take 10 $ instancesFrom today april6 -- -- @DatePattern@s can be combined using @union@, @intersect@ with their -- obvious meanings and @inEach@ which repeats one pattern inside another one. -- For example, -- -- > ((take 1 day) 'inEach' august) `intersect` sunday -- -- will give the 1st of August in years when it falls on a Sunday. ---------------------------------------------------------------------------- module Data.Time.Patterns( -- * Date Patterns DatePattern, day, mondayWeek, sundayWeek, month, year, -- ** Months january, february, march, april, may, june, july, august, september, october, november, december, -- ** Days monday, tuesday, wednesday, thursday, friday, saturday, sunday, -- * Operations on date patterns never, every, shiftBy, inEach, take, skip, except, intersect, union, -- * Queries elementOf, instancesFrom, intervalsFrom ) where import Numeric.Interval import Control.Lens hiding (elementOf, elements, contains) import Data.Thyme.Calendar (Day, Days, Months, YearMonthDay(..), gregorian, modifiedJulianDay, _ymdYear, _ymdMonth, _ymdDay) import Data.Thyme.Calendar.WeekDate (_mwDay, _swDay) import qualified Data.Thyme.Calendar.WeekDate as W import Data.Time.Patterns.Internal hiding (elementOf, every, never, take, skip, except, intersect, occurrencesFrom, union) import qualified Data.Time.Patterns.Internal as I import Prelude hiding (cycle, elem, filter, take) import qualified Prelude as P -- | A DatePattern describes a sequence of intervals of type Data.Thyme.Day. type DatePattern = IntervalSequence Day -- | An event that occurs every month. month :: DatePattern month = IntervalSequence $ \t -> let m = firstOfMonth t in let m' = addMonths 1 m in Just (I m m', month) where -- | Every January. january :: DatePattern january = monthOfYear 1 -- | Every February. february :: DatePattern february = monthOfYear 2 -- | Every March. march :: DatePattern march = monthOfYear 3 -- | Every April. april :: DatePattern april = monthOfYear 4 -- | Every May. may :: DatePattern may = monthOfYear 5 -- | Every June. june :: DatePattern june = monthOfYear 6 -- | Every July. july :: DatePattern july = monthOfYear 7 -- | Every August. august :: DatePattern august = monthOfYear 8 -- | Every September. september :: DatePattern september = monthOfYear 9 -- | Every October. october :: DatePattern october = monthOfYear 10 -- | Every November. november :: DatePattern november = monthOfYear 11 -- | Every December. december :: DatePattern december = monthOfYear 12 -- | An event that occurs every day. day :: DatePattern day = IntervalSequence{..} where nextInterval t = Just (I t (succ t), day) -- | Every Monday. monday :: DatePattern monday = filter (isDayOfWeek 1) day -- | Every Tuesday. tuesday :: DatePattern tuesday = filter (isDayOfWeek 2) day -- | Every Wednesday. wednesday :: DatePattern wednesday = filter (isDayOfWeek 3) day -- | Every Thursday. thursday :: DatePattern thursday = filter (isDayOfWeek 4) day -- | Every Friday. friday :: DatePattern friday = filter (isDayOfWeek 5) day -- | Every Saturday. saturday :: DatePattern saturday = filter (isDayOfWeek 6) day -- | Every Sunday. sunday :: DatePattern sunday = filter (isDayOfWeek 7) day -- | Weeks, starting on Monday mondayWeek :: DatePattern mondayWeek = IntervalSequence $ \d -> let m = lastMonday d in Just (I m $ addDays 7 m, mondayWeek) -- | Weeks, starting on Sunday. sundayWeek :: DatePattern sundayWeek = IntervalSequence $ \d -> let m = lastSunday d in Just (I m $ addDays 7 m, sundayWeek) -- | Years, starting from Jan. 1 year :: DatePattern year = IntervalSequence $ \d -> let m = jan1 d in Just (I m $ addYears 1 m, year) -- | The first pattern repeated for each interval of the -- second pattern. E.g.: -- -- > (take 3 $ every 4 monday) `inEach` year -- -- will give the fourth, eighth and twelveth Monday in each year inEach :: DatePattern -> DatePattern -> DatePattern inEach i o = IntervalSequence (inEach' o i i) -- | like inEach, except that the ``inner`` DatePattern is replaced by a sequence of DatePatterns -- so that for every new outer interval, the next element from the sequence will be used. inEach' :: DatePattern -> DatePattern -> DatePattern -> Day -> Maybe (Interval Day, DatePattern) inEach' outer inner orig d = do (o1, outer') <- nextInterval outer d let inner' = stopAt' (sup o1) inner case (firstOccurrenceIn (max d $ inf o1) o1 inner') of Nothing -> inEach' outer' orig orig $ sup o1 Just (i1,inner'') -> return (i1, IntervalSequence $ inEach' outer inner'' orig) -- | Shift all the results by a number of day shiftBy :: Days -> DatePattern -> DatePattern shiftBy n sq = mapS (addDays n) sq -- | Add a number of day to a day addDays :: Days -> Day -> Day addDays n d = (d^.modifiedJulianDay + n)^.from modifiedJulianDay -- | Take every nth occurrence every :: Int -> DatePattern -> DatePattern every = I.every -- | Stop after n occurrences take :: Int -> DatePattern -> DatePattern take = I.take -- | Skip the first n occurrences skip :: Int -> DatePattern -> DatePattern skip = I.skip -- | Skip over all occurrences of a day. -- If the pattern describes a period longer -- than a day, the entire period will be -- skipped. except :: Day -> DatePattern -> DatePattern except = I.except -- | Check if a date is covered by a DatePattern elementOf :: Day -> DatePattern -> Bool elementOf = I.elementOf -- | Get occurrences of an event starting with a given day instancesFrom :: Day -> DatePattern -> [Day] instancesFrom = I.elementsFrom -- | An event that never occurs never :: DatePattern never = I.never -- | Return only occurrences that are present in both patterns -- -- > let myBirthday = (take 1 day) `inEach` august -- > let s = intersect myBirthday sunday -- -- Will return August 1 in years when it falls on a Sunday intersect :: DatePattern -> DatePattern -> DatePattern intersect = I.intersect -- | Occurrences of both patterns. -- -- > union april june -- -- Will return the months April and June in each year -- -- > let fifteenth = (take 1 $ skip 14 day) `inEach` month -- > let third = (take 1 $ skip 2 day) `inEach` month -- > union fifteenth third -- -- Will return the 3rd and the 15th of each month union :: DatePattern -> DatePattern -> DatePattern union = I.union -- | Get the date intervals described by the pattern, starting -- from the specified date. -- -- The intervals range from the first -- day included by the pattern to the first day after it, so -- a single day @d@ would be described as @(d ... succ d)@ and -- the interval for a month will go from the 1st of the month -- to the 1st of the next month. intervalsFrom :: Day -> DatePattern -> [Interval Day] intervalsFrom = I.occurrencesFrom -- | Check if a day interval covers exactly a given weekday -- with Monday = 1, Tuesday = 2, etc. isDayOfWeek :: Int -> Interval Day -> Bool isDayOfWeek d i = case (elements i) of [dt] -> dt^. W.mondayWeek . _mwDay == d _ -> False -- | Get the last Monday before or on the date lastMonday :: Day -> Day lastMonday d = case (d^.W.mondayWeek._mwDay) of 1 -> d _ -> lastMonday $ pred d -- | Get the last Monday before or on the date lastSunday :: Day -> Day lastSunday d = case (d^.W.sundayWeek._swDay) of 1 -> d _ -> lastSunday $ pred d -- | Get the beginning of a year jan1 :: Day -> Day jan1 d = let d' = d^.gregorian in (YearMonthDay (d'^._ymdYear) 1 1)^.from gregorian addYears :: Int -> Day -> Day addYears n d = let d' = d^.gregorian in (YearMonthDay (d'^._ymdYear + n) (d'^._ymdMonth) (d'^._ymdDay))^.from gregorian addMonths :: Months -> Day -> Day addMonths m d = let d' = d^.gregorian in let (years,months) = (d'^._ymdMonth + m) `divMod` 12 in (YearMonthDay (d'^._ymdYear + years) months (d'^._ymdDay))^.from gregorian firstOfMonth :: Day -> Day firstOfMonth d = let d' = d^.gregorian in (YearMonthDay (d'^._ymdYear) (d'^._ymdMonth) 1)^.from gregorian get1stOfMonth :: Int -> Day -> Day get1stOfMonth i d = let d' = d^.gregorian in let y = abs $ (i - d'^._ymdMonth) `div` 12 in (YearMonthDay (d'^._ymdYear + y) i 1)^.from gregorian getMonth :: Int -> Day -> Interval Day getMonth i d = (d' ... addMonths 1 d') where d' = get1stOfMonth i d monthOfYear :: Int -> DatePattern monthOfYear i = IntervalSequence $ \d -> Just (getMonth i d, monthOfYear i)