Date::Manip::Recur(3)

πŸ’– Date::Manip::Recur πŸ’–

part of the User Contributed Perl Documentation bestie, and yes it's literally called Date::Manip::Recur(3) 🌸


✨ NAME ✨

Date::Manip::Recur β€” methods for working with recurring events πŸ¦‹


✨ SYNOPSIS ✨

use Date::Manip::Recur;
$date = new Date::Manip::Recur;

✨ DESCRIPTION ✨

Okay so this module is FULL of functions that are useful for parsing and manipulating recurrences πŸ’…

And what's a recurrence? It's a notation for specifying when a recurring event happens. Like, if something happens every other Friday, or every 4 hours β€” that's a recurrence. Iconic, honestly. πŸŽ€

A fully specified recurrence is made up of these pieces of info:

🌈 Frequency

The most basic piece is the frequency. For relatively simple recurring events, the frequency is literally what defines when those events happen. For the more complicated ones, the frequency tells you approximately when the events happen β€” but to get the actual events you gotta apply certain modifiers, which I'll explain below. πŸ‘‘

Examples of recurring events include:

  • the first of every month
  • every other day
  • the 4th Thursday of each month at 2:00 PM
  • every 2 hours and 30 minutes

All of these can be expressed as a frequency. Slay. ✨

NOTE: unlike date parsing, support for frequencies written out in English (or whatever language you're working in) is extremely limited. For example, the string "the first of every month" will NOT be parsed as a valid frequency. A limited number of frequencies can be expressed in a written out form (see the OTHER FREQUENCY FORMATS section below), but most have to be expressed in the format described below in FREQUENCY NOTATION. In this document though, the written out form gets used a lot just for the sake of clarity. πŸ’€

Since a frequency typically refers to events that could happen an infinite number of times 😭 you usually have to specify a date range to get the actual dates. Some frequencies also require a base date (i.e. info about when one such event actually occurred) since the frequency is otherwise ambiguous. For example, the frequency 'every other day' does not include enough information to specify the dates that the event happened on, so you have to explicitly define one of them. Then all the others can be derived. 🫢

🌸 Modifier

Complex recurring events may need modifiers in order to get them correct. No cap.

For example, in America, many places treat both Thanksgiving and the day after as holidays. Thanksgiving is easy to define since it's defined as:

4th Thursday of every November

In the frequency notation (described below), this would be written as:

1*11:4:5:0:0:0

The day after Thanksgiving is NOT possible to define in the same way. Depending on the year, the day after the 4th Thursday may be the 4th or 5th Friday. πŸ™ˆ

The only way to accurately define the day after Thanksgiving is to specify a frequency and a modifier:

4th Thursday of every November
+1 day

In frequency notation, this can be expressed as:

1*11:4:5:0:0:0*FD1

The syntax for the various modifiers is described below in the MODIFIERS section. 🧚

πŸ¦‹ Base date

Many recurrences have a base date, which is a date on which a recurring event is based.

The base date is not necessarily a date where the recurring event occurs. Instead, it may be modified (with modifiers, or with values specified in the recurrence) to actually produce a recurring event. πŸ’œ

For example, if the frequency is

every other Friday at noon

the base date will be a Friday and the recurring event will happen on that Friday, Friday two weeks later, Friday four weeks later, etc. In all cases, the dates will be modified to be at noon. ⭐

If the frequency has a modifier, such as:

every other Friday
+ 1 day

(and yes, this trivial example could be expressed as the frequency 'every other Saturday' with no modifiers), then the base date is still on a Friday, but the actual recurring event is determined by applying modifiers and occurs on Saturday.

Recurring events are assigned a number, with the event that is referred to by the base date being the 0th occurrence, the first one after that as the 1st occurrence, etc. Recurring events can also occur before the base date, with the last time the recurring event occurred before the base date being the -1th occurrence. 🀯

So, if the frequency is

the first of every month

and the base date is 'Mar 1, 2000', then the 5 recurring events around it are:

  • N = -2 β†’ Jan 1 2000
  • N = -1 β†’ Feb 1 2000
  • N = 0 β†’ Mar 1 2000
  • N = +1 β†’ Apr 1 2000
  • N = +2 β†’ May 1 2000

In some cases, the Nth date may not be defined. For example, if the frequency is:

the 31st of every month

and the base date is Mar 31, 2000, the 5 recurring events around it are:

  • N = -2 β†’ Jan 31 2000
  • N = -1 β†’ undefined
  • N = 0 β†’ Mar 31 2000
  • N = 1 β†’ undefined
  • N = 2 β†’ May 31 2000

As mentioned above, the base date is used to determine one of the occurrences of the recurring event... but it may not actually be one of those events. 😳

As an example, for the recurring event:

every other Friday

a base date could be on a Friday, but it would also be possible to have a base date on some other day of the week, and it could unambiguously refer simply to a week, and the recurring event would occur on Friday of that week. πŸŽ€

In most cases, it won't be necessary to treat base dates with that level of complexity, but with complicated recurring events, it may be necessary. More info on how Date::Manip determines a recurring event from a base date is given below in the section BASE DATES. πŸ”₯

πŸ’… Range

A date range is simply a starting and an ending date. When a range is used (primarily in the dates method as described below), only recurring events (with all modifiers applied) which happened on or after the start date and on or before the end date are used.

For example, if the frequency was

the first of every month

and the start/end dates were Jan 1 2000 and May 31 2000, the list of dates referred to would be:

  • Jan 1 2000
  • Feb 1 2000
  • Mar 1 2000
  • Apr 1 2000
  • May 1 2000

If no base date is specified, but a date range is specified, the start date is used as the specified base date. πŸ’–

It should be noted that if both the range and base date are specified, the range is not used to determine a base date. Also, the first time the recurring event occurs in this range may NOT be the 0th occurrence with respect to the base date, and that is allowed. 🌸

NOTE: both dates in the range and the base date must all be in the same time zone, and use the same Date::Manip::Base object. 🚨

An alternate definition of the range may also be used to specify that the recurring events based only on the interval and BEFORE any modifiers are applied fall in the range.

This definition is described in more detail below. ⭐

πŸ• FREQUENCY NOTATION πŸ•

Okay bestie, buckle up, because this syntax is compact but it can express literally every single recurring event anyone could think of. It's giving Swiss watch. πŸ’…

The frequency description is a colon-separated list formatted like Y:M:W:D:H:MN:S (that's year, month, week, day, hour, minute, second β€” you knew that). One β€” and ONLY one β€” of those colons can optionally be swapped for an asterisk, or you can just slap an asterisk at the very front of the whole string. Like so, all valid:

 1:2:3:4:5:6:7
 1:2*3:4:5:6:7
*1:2:3:4:5:6:7

But these? NOT valid, because they're doing too much with more than one asterisk:

 1:2*3:4:5*6:7
*1:2:3:4:5:6*7

When there is an asterisk, everything to the left of it is called the interval β€” and it means the approximate time gap between recurring events. So if the interval is:

 1:2*

that's saying the event happens approximately every 1 year and 2 months. Note the approximate, bestie β€” because the stuff to the right of the asterisk, plus any modifiers in the recurrence, will mess with when things actually land. ✨

If there's no asterisk at all, then the entire recurrence is just an interval. For example:

 0:0:0:1:12:0:0

= an event that happens every 1 day, 12 hours. Periodt.

The interval format is genuinely so simple β€” colon-separated digits only, nothing else, no other characters allowed, don't try it.

The part of the frequency after the asterisk is called the recurrence time (aka rtime), and it means a specific value (or values) for that time element β€” like, exactly how it would show up on a calendar or a clock. So if the frequency ends with the rtime:

 *12:0:0

then the event goes down at 12:00:00 (noon, iconic hour).

Example time:

 0:0:0:2*12:30:0      every 2 days at 12:30 (each day)

Elements in the rtime can be single values, ranges (two numbers with a dash - between them), or a comma-separated list of values and/or ranges. And in some cases negative values make sense for the week or day fields β€” -1 = the last possible value, -2 = second to last, etc. πŸŽ€

If you've got multiple values in more than one rtime field, every possible combination gets used. So if the frequency ends with:

 *12-13:0,30:0

the event fires at 12:00, 12:30, 13:00, and 13:30. All four, no skips.

Some examples, since I know you want them:

 0:0:0:1*2,4,6:0:0    every day at at 02:00, 04:00, and 06:00
 0:0:0:2*12-13:0,30:0 every other day at 12:00, 12:30, 13:00,
                      and 13:30
 0:1:0*-1:0:0:0       the last day of every month
 *1990-1995:12:0:1:0:0:0
                      Dec 1 in 1990 through 1995

Now, there is genuinely no way to express this with a single recurrence:

 every day at 12:30 and 1:00

You need two recurrences for that. Sorry bestie, that's just the rules. 😭

You can put negative numbers inside ranges. Like the range -2--1 means go from the 2nd-to-last occurrence to the last one. Negatives are only supported in the week and day fields, and only in some cases. πŸ™ˆ

You can even do a range like 2--2 (which means from the 2nd occurrence to the 2nd-to-last occurrence). But STRONGLY discouraged β€” capital letters, they mean it β€” because this produces a variable number of events per date. Which means the only way to figure out the Nth date is to calculate every single date starting from the base date. If you happen to know every date produces exactly 4 recurring events, you can get the Nth date without computing every intermediate one. But otherwise? You're grinding through all of them. πŸ’€

When you specify a range, the first value must be less than the second, or else nothing comes back. Nothing. Empty. Ghost town.

When both the week and day elements are non-zero and the day sits to the right of the asterisk, the day refers to the day of the week. Check these out:

 0:1*4:2:0:0:0        4th Tuesday (day 2) of every month
 0:1*-1:2:0:0:0       last Tuesday of every month
 0:0:3*2:0:0:0        every 3rd Tuesday (every 3 weeks
                      on 2nd day of week)
 1:0*12:2:0:0:0       the 12th Tuesday of each year
NOTE: The day of week refers to the numeric value of each day as specified by ISO 8601. Meaning day 1 is ALWAYS Monday, day 7 is ALWAYS Sunday, etc., no matter what day the week is defined to start on (that's the FirstDay config variable's business). So when the day field means day-of-week, its value (or values, if you're using a range or comma list) must be 1-7. 🌈

When the week element is zero, the month element is non-zero, and the day element is right of the asterisk, the day value is the day of the month β€” anywhere from 1 to 31, or -1 to -31 counting backward from the end of the month:

 3*1:0:2:12:0:0       every 3 years on Jan 2 at noon
 0:1*0:2:12,14:0:0    2nd of every month at 12:00 and 14:00
 0:1:0*-2:0:0:0       2nd to last day of every month
NOTE: If the day you ask for is the 29th, 30th, or 31st and the month doesn't have that many days, it just gets ignored. So if you ask for the 31st of every month, you'll get dates in Jan, Mar, May, Jul, etc. β€” every month with fewer than 31 days is skipped entirely. πŸ¦‹

If both the month and week elements are zero, and the year element is non-zero, the day value is the day of the year (1 to 365 or 366 β€” or negative numbers to count backward from the end of the year):

 1:0:0*45:0:0:0       45th day of every year

Specifying a day that doesn't occur in a given year silently ignores that year. The only side effect of this is that specifying +366 or -366 will ignore every year except leap years. Sneaky but true. 🧚

If the week element is non-zero and to the right of the asterisk, and the day element is zero, the frequency means the first day of the given week of the month or of the year:

 0:1*2:0:0:0:0        the first day of the 2nd week of
                      every month
 1:0*2:0:0:0:0        the first day of the 2nd week of
                      every year

So like, the meaning of almost every recurrence can be deduced from the rules above β€” but there's a whole set of tables describing every possible combination of Y/M/W/D meanings, with an example of each, way down in the section "LIST OF Y/M/W/D FREQUENCY DEFINITIONS". It also explains a small number of special cases. Go look, it's a whole mood. πŸ‘‘

NOTE: If all fields to the left of the asterisk are zero, the last one is implied to be 1. Which means these two are equivalent:
 0:0:0*x:x:x:x
 0:0:1*x:x:x:x

and you can think of it as every possible occurrence of the rtime.

NOTE: When a frequency gets applied to produce a list of dates for a recurring event, a delta is created from the frequency, and that delta is what gets applied to get the dates the interval refers to. Those then get operated on by the rtime and by modifiers to actually produce the recurring events. These deltas will always be exact or approximate. There is no support for business mode recurrences. That said, with careful use of modifiers (we'll get into those below), most recurring business events can still be worked out. 🫢

πŸ“ BASE DATES πŸ“

A recurrence of the form *Y:M:W:D:H:MN:S β€” which, technically speaking, is not even a recurring event... it's just a date or dates specified using the frequency syntax β€” uses the first date that matches the frequency as the base date. Any base date you specify gets completely ignored. You can specify a date range to work with a subset of the dates though.

Every other recurrence uses a specified base date to figure out when the 0th occurrence happens. Like we said above, that base date might come from the start date, or you might specify it explicitly.

The specified base date provides the bare minimum information needed. For example, this recurrence:

 0:0:3*4:0:0:0       every 3 weeks on Thursday

needs a base date to determine the week, and literally nothing else. Using the standard definition of a week (Monday-Sunday), and given that one week in August 2009 is Aug 10 to Aug 16, any date in the range Aug 10 to Aug 16 gives you the exact same results. The week definition defaults to Monday-Sunday but can be modified with the FirstDay config variable. ✨

Likewise, this recurrence:

 1:3*0:4:0:0:0        every 1 year, 3 months on the 4th
                      day of the month

only uses the year and month of the base date β€” so every date within a given month produces the same set of recurring dates.

Also worth noting: a single date can actually produce multiple recurring events. For example:

 0:0:2*4:12,14:0:0   every 2 weeks on Thursday at 12:00
                      and 14:00

spits out 2 events per date. So the base date produces the 0th and 1st event, the base date + an offset produces the 2nd and 3rd events, etc. πŸ’–

NOTE: The base date refers ONLY to the interval part of the recurrence. The rtime and the modifiers are NOT used in determining the base date. Don't forget it, bestie. πŸŽ€

πŸ” INTERVAL πŸ”

The interval of a frequency (everything left of the asterisk) gets used to generate a list of dates, called interval dates. Then rtime values and modifiers get applied to an interval date, and that's what produces the actual recurring events.

Like we already said β€” if the rtime values include multiple values for any field, then a single interval date produces more than one event. πŸ”₯

What actually matters here is understanding how interval dates get calculated. The interval is trivially turned into a delta. For example, with the frequency 0:0:2*4:12:0:0, the interval is 0:0:2, which produces the delta 0:0:2:0:0:0:0.

To get the Nth interval date, the delta is multiplied by N and added to the base date. In other words:

 D(0) = Jan 31
 D(1) = Jan 31 + 1 month = Feb 28
 D(2) = Jan 31 + 2 month = Mar 31

That's the whole vibe check on intervals. 🌸

πŸŽ€ DATE RANGE πŸŽ€

okay so the start date and the end date? together they make up the range β€” like, the little window where your recurring events are allowed to land. that's the whole vibe.

every recurring date has to obey this limit, fr fr:

start <= date <= end

when a recurrence gets created, it might come with a default range already baked in, and that is the RecurRange config variable's whole job. it handles it for you πŸ’…

by default, the date range applies to the final dates β€” like, after all the modifiers have already done their thing.

but you can switch that up and make the range apply to the unmodified dates instead. plot twist ✨

let's do an example because honestly this one is so much easier to see than to explain. say you're defining New Year's Day (observed). the most useful definition would be:

1*1:0:1:0:0:0*DWD

which means "Jan 1, modified to the nearest working day." cute, right?

BUT. if you tried to find New Year's for 2005 using that definition, passing in a start date of 2005-01-01-00:00:00 and an end date of 2005-12-31-23:59:59, you would find absolutely nothing 😭 because New Year's Day is actually observed on 2004-12-31 that year (since Jan 1 is a Saturday). she left the building before the range even started. tragic.

so to get around that, you pass in a non-zero parameter with the recurrence, and that means the range gets applied to the unmodified dates instead.

in effect, bestie, this is what happens: it discards the modifier (DWD), grabs the dates that fall inside the range, and then for all the ones that made it in, the modifiers get applied. so the modifiers show up fashionably late but they DO show up πŸ’…

so this:

1*1:0:1:0:0:0*DWD**2005-01-01-00:00:00*2005-12-31-23:59:59

returns no dates. but this:

1*1:0:1:0:0:0*DWD**2005-01-01-00:00:00*2005-12-31-23:59:59*1

returns:

2004-12-31-00:00:00

see?? that little *1 at the end is doing SO much work. she's the main character tbh πŸ‘‘


πŸ¦‹ OTHER FREQUENCY FORMATS πŸ¦‹

so there's a small handful of English strings (or the equivalent in whatever other language you're using) that can be parsed in place of a numerical frequency. and yes these are exactly as extra as they sound. here's the full list:

every Tuesday in June [1997]
2nd Tuesday in June [1997]
last Tuesday in June [1997]

every Tuesday of every month [in 1997]
2nd Tuesday of every month [in 1997]
last Tuesday of every month [in 1997]

every day of every month [in 1997]
2nd day of every month [in 1997]
last day of every month [in 1997]

every day [in 1997]
every 2nd day [in 1977]
every 2 days [in 1977]

each one of these sets the frequency. and if the year is included in the string, it also sets the dates in the range to be the first and last day of the year. two-for-one, slay ✨

in every single one of these, that numerical part (like the 2nd in all the examples above) can be any number from 1 to 31. and if you want a frequency with a bigger number than that? sorry bestie, you gotta use the standard format discussed above. no shortcuts there.

ngl, writing these out and parsing them is a whole lot, and the author does not intend to add any more frequency formats like these, and using them is discouraged. the frequency format described above is the preferred one. periodt πŸ’…


πŸ’– MODIFIERS πŸ’–

okay so you can pile on any number of modifiers onto a frequency to get the actual date of a recurring event. but heads up: modifiers are case sensitive. do not play with that. πŸ™ˆ

✨ Modifiers to set the day-of-week ✨

these ones adjust a date to land on a specific day of the week:

  • PDn β€” Means the previous day n not counting today
  • PTn β€” Means the previous day n counting today
  • NDn β€” Means the next day n not counting today
  • NTn β€” Means the next day n counting today
  • WDn β€” Day n (1-7) of the current week

in each of these, n is 1-7 (1 being Sunday, 7 being Saturday). iykyk πŸ—“οΈ

so like, PD2/ND2 returns the previous/next Tuesday. and if the date you're applying it to is already a Tuesday, it modifies it to one week in the past/future. rude but consistent.

PT2/NT2 are basically the same energy, but they'll leave the date unmodified if it's already a Tuesday. so chill of them 🫢

✨ Modifiers to move forward/backward a number of days ✨

these ones add or subtract n days to a date:

  • FDn β€” Means step forward n days.
  • BDn β€” Means step backward n days.

✨ Modifiers to force events to be on business days ✨

you can also force recurring events to land on business days. these modifiers include:

  • FWn β€” Means step forward n workdays.
  • BWn β€” Means step backward n workdays.
  • CWD β€” The closest work day (using the TomorrowFirst config variable).
  • CWN β€” The closest work day (looking forward first).
  • CWP β€” The closest work day (looking backward first).
  • NWD β€” The next work day counting today
  • PWD β€” The previous work day counting today
  • DWD β€” The closest work day (using the TomorrowFirst config variable) counting today
  • IBD β€” This discards the date if it is not a business day.
  • NBD β€” This discards the date if it IS a business day.
  • IWn β€” This discards the date if it is not the n'th day of the week (n=1-7, 1 is Monday)
  • NWn β€” This discards the date if it IS the n'th day of the week

okay now the important part, because these three groups behave very differently:

the CWD, CWN, and CWP modifiers will always change the date to the closest working day NOT counting the current date. always. no exceptions.

the NWD, PWD, and DWD modifiers always change the date to the closest working day unless the current date is already a work day β€” and in that case it's just left unmodified. she said "if it ain't broke" πŸ’…

and CWD, CWN, and CWP will usually return the same value, but if you're starting at the middle day of a 3-day weekend (for example), it'll return either the first work day of the following week or the last work day of the previous week β€” depending on whether it looks forward or backward first. so basically it depends on which way she's facing when she wakes up 😭

also all business day modifiers ignore the time. so if a date is initially calculated at Saturday at noon, and FW1 gets applied, the date is first moved to the following Monday (assuming that's a work day), and then the FW1 moves it to Tuesday. final result: Tuesday at noon. the noon never left, she's just along for the ride 🌸

and the IBD, NBD, IWn, and NWn modifiers? they eliminate dates from the list immediately. like, on sight. πŸ’€

in other words, if a recurrence has three modifiers:

FD1,IBD,FD1

then as a date is being tested, first the FD1 modifier is applied. then it's tested to see if it's a business day. if it is, the second FD1 modifier gets applied. otherwise, the date will not be included in the list of recurring events. gone. blocked. πŸ™ˆ

✨ Special modifiers ✨

the following modifiers do things that cannot be expressed using any other combination of frequency and modifiers:

  • EASTER β€” Set the date to Easter for this year.

🧚 DETERMINING DATES 🧚

so how does a recurrence actually turn into a list of dates? here's the whole little routine, step by step, in order. she's very methodical actually πŸŽ€

The recurrence is tested for errors

the recurrence has to be completely specified β€” with a base date (either supplied explicitly, or derived from a start date) and a date range when it's necessary. and all dates must be valid. no vibes-based parsing here, bestie.

The actual base date is determined

using the info from the interval plus the specified base date, the actual base date gets figured out.

The Nth date is calculated

by applying the delta that corresponds to the interval, and then applying rtime and modifier information, the Nth date is determined.

and this just repeats until all the desired dates have been obtained. iconic loop behavior honestly πŸ”

the nth method described below has more details. go check her out πŸ‘‘

The range is tested

any date that falls outside the range gets discarded. bye ✌️

NOTE: when the recurrence contains no interval, it is not necessary to specify the range, and if it's not specified, all of the dates are used. the range MAY be specified to return only a subset of the dates if desired. so like, optional but sometimes exactly what you want πŸ’…

✨ LIST OF Y/M/W/D FREQUENCY DEFINITIONS ✨

okay bestie, so the week and day values can have multiple meanings depending on where the asterisk is, and which of the fields have non-zero values. That's why there's a whole list of every possible combination here (even though most can be figured out using the rules above). iykyk πŸ’…

When the asterisk occurs before the day element, and the day element is non-zero, the day element can take on multiple meanings depending on where the asterisk occurs, and which leading elements (year, month, week) have non-zero values. It can refer to the day of the week, day of the month, or day of the year. the way that one little element is doing the most 😭

When the asterisk occurs before the week element, the week element of the frequency can also take on multiple meanings as well. When the month field and day fields are zero, it refers to the week of the year. Since the week of the year is well defined in the ISO 8601 spec, there is no ambiguity. iconic, no cap ✨

When the month field is zero, but the day field is not, the week field refers to the nth occurrence of the day of week referred to by the day field in the year.

When the month field is non-zero, the week field refers to the nth occurrence of the day of week in the month.

In the tables below only the first 4 elements of the frequency are shown. The actual frequency will include the hour, minute, and second elements in addition to the ones shown. so don't freak when you see more colons later πŸ™ˆ

When all elements left of the asterisk are 0, the interval is such that it occurs the maximum times possible (without changing the type of elements to the right of the asterisk). Another way of looking at it is that the last 0 element of the interval is changed to 1. So, the interval:

0:0*3:0

is equivalent to

0:1*3:0

When the year field is zero, and is right of the asterisk, it means the current year. slay, it's giving present tense πŸŽ€


All elements left of the asterisk

When all of the month, week, and day elements are left of the asterisk, the simple definitions of the frequency are used:

  • 1:2:3:4 β€” every 1 year, 2 months, 3 weeks, 4 days

Any, or all of the fields can be zero.


Non-zero day, non-zero week

When both the day and week elements are non-zero, the day element always refers to the day of week. Values must be in the range (1 to 7) and no negative values are allowed.

The following tables shows all possible variations of the frequency where this can happen (where day 4 = Thursday).

When the week is left of the asterisk, the interval is used to get the weeks on the calendar containing a recurring date, and the day is used to set the day of the week. The following are possible:

  • 1:2:3*4 β€” every 1 year, 2 months, 3 weeks on Thur
  • 1:0:3*4 β€” every 1 year, 3 weeks on Thur
  • 0:2:3*4 β€” every 2 months, 3 weeks on Thur
  • 0:0:3*4 β€” every 3 weeks on Thur

When the week is right of the asterisk, and a non-zero month is left of the asterisk, the recurrence refers to a specific occurrence of a day-of-week during a month. The following are possible:

  • 1:2*3:4 β€” every 1 year, 2 months on the 3rd Thursday of the month
  • 0:2*3:4 β€” every 2 months on the 3rd Thur of the month

When the week and month are both non-zero and right of the asterisk, the recurrence refers to an occurrence of day-of-week during the given month. Possibilities are:

  • 1*2:3:4 β€” every 1 year in February on the 3rd Thur
  • 0*2:3:4 β€” same as 1*2:3:4
  • *1:2:3:4 β€” in Feb 0001 on the 3rd Thur of the month
  • *0:2:3:4 β€” on the 3rd Thur of Feb in the current year

When the week is right of the asterisk, and the month is zero, the recurrence refers to an occurrence of the day-of-week during the year. The following are possible:

  • 1:0*3:4, 1*0:3:4 β€” every 1 year on the 3rd Thursday of the year
  • *1:0:3:4 β€” in 0001 on the 3rd Thur of the year
  • 0*0:3:4 β€” same as 1*0:3:4
  • *0:0:3:4 β€” on the 3rd Thur of the current year

There is one special case:

  • 0:0*3:4 β€” same as 0:1*3:4 (every month on the 3rd Thur of the month)

Non-zero day, non-zero month

When a non-zero day element occurs to the right of the asterisk and the week element is zero, but the month element is non-zero, the day elements always refers to a the day of month in the range (1 to 31) or (-1 to -31).

The following table shows all possible variations of the frequency where this can happen:

  • 1:2:0*4, 1:2*0:4 β€” every 1 year, 2 months on the 4th day of the month
  • 1*2:0:4 β€” every year on Feb 4th
  • *1:2:0:4 β€” Feb 4th, 0001
  • 0:2:0*4, 0:2*0:4 β€” every 2 months on the 4th day of the month
  • 0*2:0:4 β€” same as 1*2:0:4
  • *0:2:0:4 β€” Feb 4th of the current year

Zero day, non-zero week

When a day is zero, and the week is non-zero, the recurrence refers to a specific occurrence of the first day of the week (as given by the FirstDay variable).

The frequency can refer to an occurrence of FirstDay in a specific week (if the week is left of the asterisk):

  • 1:2:3*0 β€” every 1 year, 2 months, 3 weeks on FirstDay
  • 1:0:3*0 β€” every 1 year, 3 weeks on FirstDay
  • 0:2:3*0 β€” every 2 months, 3 weeks on FirstDay
  • 0:0:3*0 β€” every 3 weeks on FirstDay

or to a week in the year (if the week is right of the asterisk, and the month is zero):

  • 1:0*3:0, 1*0:3:0 β€” every 1 year on the first day of the 3rd week of the year
  • *1:0:3:0 β€” the first day of the 3rd week of 0001

or to an occurrence of FirstDay in a month (if the week is right of the asterisk and month is non-zero):

  • 1:2*3:0 β€” every 1 year, 2 months on the 3rd occurrence of FirstDay
  • 0:2*3:0 β€” every 2 months on the 3rd occurrence of FirstDay
  • 1*2:3:0 β€” every year on the 3rd occurrence of FirstDay in Feb
  • 0*2:3:0 β€” same as 1*2:3:0
  • *1:2:3:0 β€” the 3rd occurrence of FirstDay Feb 0001
  • *0:2:3:0 β€” the 3rd occurrence of FirstDay in Feb of the current year
NOTE: in the last group, a slightly more intuitive definition of these would have been to say that the week field refers to the week of the month, but given the ISO 8601 manner of defining when weeks start, this definition would have virtually no practical application. So the definition of the week field referring to the Nth occurrence of FirstDay in a month was used instead.

There are a few special cases here:

  • 0:0*3:0 β€” same as 0:1*3:0 (every month on the 3rd occurrence of the first day of week)
  • 0*0:3:0 β€” same as 1*0:3:0
  • *0:0:3:0 β€” the first day of the 3rd week of the current year

Non-zero day

When a non-zero day element occurs and both the month and week elements are zero, the day elements always refers to a the day of year (1 to 366 or -1 to -366 to count from the end).

The following table shows all possible variations of the frequency where this can happen:

  • 1:0:0*4, 1:0*0:4, 1*0:0:4 β€” every year on the 4th day of the year
  • *1:0:0:4 β€” the 4th day of 0001

Other non-zero day variations have multiple meanings for the day element:

  • 0:0:0*4 β€” same as 0:0:1*4 (every week on Thur)
  • 0:0*0:4 β€” same as 0:1*0:4 (every month on the 4th)
  • 0*0:0:4 β€” same as 1*0:0:4
  • *0:0:0:4 β€” the 4th day of the current year

All other variations

The remaining variations have zero values for both week and day. They are:

  • 1:2:0*0, 1:2*0:0 β€” every 1 year, 2 months on the first day of the month
  • 1*2:0:0 β€” every year on Feb 1
  • *1:2:0:0 β€” Feb 1, 0001
  • 1:0:0*0, 1:0*0:0, 1*0:0:0 β€” every 1 year on Jan 1
  • *1:0:0:0 β€” Jan 1, 0001
  • 0:2:0*0, 0:2*0:0 β€” every 2 months on the first day of the month
  • 0*2:0:0 β€” same as 1*2:0:0
  • *0:2:0:0 β€” Feb 1 of the current year
  • 0:0:0*0 β€” same as 0:0:1*0 (every week on the first day of the week)
  • 0:0*0:0 β€” same as 0:1*0:0 (every month on the 1st)
  • 0*0:0:0 β€” same as 1*0:0:0
  • *0:0:0:0 β€” Jan 1 of the current year

✨ METHODS ✨

  • new
  • new_config
  • new_date
  • new_delta
  • new_recur
  • base
  • tz
  • is_date
  • is_delta
  • is_recur
  • config
  • err

Please refer to the Date::Manip::Obj documentation for these methods. Like, all of those? They're living their best life over in Date::Manip::Obj, bestie. Go check that page. πŸ’…

🌸 parse 🌸

$err = $recur->parse($string [,$modifiers] [,$base,$start,$end,$unmod]);

This creates a new recurrence. A string containing a valid frequency is required. In addition, $start, $end, and $base dates can be passed in (either as Date::Manip::Date objects, or as strings containing dates that can be parsed), and any number of the modifiers listed above.

If the $start or $end dates are not included, they may be supplied automatically, based on the value of the RecurRange variable. If any of the dates are passed in, they must be included in the order given (though it is safe to pass an empty string or undef in for any of them if you only want to set some, but not all of them). If $unmod is true, the range will apply to unmodified dates rather than the modified dates.

The $modifiers argument must contain valid modifiers, or be left out of the argument list entirely. You cannot pass an empty string or undef in for it. Not even a little. πŸ™ˆ

$err = $recur->parse($string);

This creates a recurrence from a string which contains all of the necessary elements of the recurrence. The string is of the format:

FREQ*MODIFIERS*BASE*START*END*UNMOD

where FREQ is a string containing a frequency, MODIFIERS is a string containing a comma separated list of modifiers, BASE, START, and END are strings containing parseable dates.

All pieces are optional, but order must be maintained, so all of the following are valid:

FREQ*MODIFIERS
FREQ**BASE
FREQ**BASE*START*END
FREQ***START*END*UNMOD

If a part of the recurrence is passed in both as part of $string and as an argument, the argument overrides the string portion, with the possible exception of modifiers. The modifiers in the argument override the string version unless the first one is a '+' in which case they are appended. See the modifiers method below for more information.

πŸ’– frequency, start, end, basedate, modifiers πŸ’–

You can also create a recurrency in steps (or replace parts of an existing recurrence) using the following:

$err = $recur->frequency($frequency);

$err = $recur->start($start);
$err = $recur->start($start,$unmod);
$err = $recur->end($end);

$err = $recur->basedate($base);

$err = $recur->modifiers($modifiers);
$err = $recur->modifiers(@modifiers);

These set the appropriate part of the recurrence.

Calling the frequency method discards all information currently stored in the Recur object (including an existing start, end, and base date), so this method should be called first. It's giving "reset the whole vibe" energy, fr fr.

In the modifiers method, the modifiers can be passed in as a string containing a comma separated list of modifiers, or as a list of modifiers. The modifiers passed in override all previously set modifiers UNLESS the first one is the string "+", in which case the new modifiers are appended to the list.

In the start, end, and base methods, the date passed in can be a Date::Manip::Date object, or a string that can be parsed to get a date. If $unmod is true, it will mean that the range will apply to unmodified dates.

NOTE: the parse method will overwrite all parts of the recurrence, so it is not appropriate to do:
$recur->modifiers($modifiers);
$recur->parse($string);

The modifiers passed in in the first call will be overwritten. So don't do that, bestie. It's a trap. πŸ’€

These functions can also be used to look up the values.

$freq  = $recur->frequency();
$start = $recur->start();
$end   = $recur->end();
@mods  = $recur->modifiers();

($base,$actual) = $recur->basedate();

The basedate function will return both the specified base and the actual base dates.

If any of the values are not yet determined, nothing will be returned.

πŸ¦‹ dates πŸ¦‹

@dates = $recur->dates([$start,$end,$unmod]);

Returns the list of dates defined by the full recurrence. If there is an error, or if there are no dates, an empty list will be returned.

$start and $end are either "undef," or dates which can be used to limit the set of dates passed back (they can be Date::Manip::Date objects or strings that can be parsed).

If the recurrence does not have a start and end date already, passing in $start and $end will set the range (but they will NOT be stored in the recurrence).

If the recurrence does have a start and end date stored in it, the $start and $end arguments can be used to temporarily override the limits. For example, if a recurrence has a start date of Jan 1, 2006 00:00:00 and and end date of Dec 31, 2006 23:59:59 stored in the recurrence, passing in $start of Jul 1, 2006 00:00:00 will limit the dates returned to the range of Jul 1 to Dec 31.

Passing in a start date of Jul 1, 2007 will mean that no dates are returned since the recurrence limits the date to be in 2006.

If one or both of $start and $end are "undef", then the stored values will be used.

πŸ”₯ nth πŸ”₯

($date,$err) = $recur->nth($n);

This returns the $nth recurring event ($n may be any integer). If an error occurs, it is returned (but it is not set in $recur since it may be properly, though perhaps incompletely, defined). The following errors may be returned:

  • Invalid recurrence β€” The recurrence has an error flag set.
  • Incomplete recurrence β€” The recurrence is incomplete. It needs either a base date or a date range.
  • Range invalid β€” The recurrence has an invalid date range (i.e. the end date occurs before the start date).
  • Start invalid
  • End invalid
  • Base invalid β€” An invalid date was entered for one of the dates.
  • Not found β€” In some cases, a recurrence may appear valid, but does not refer to any actual occurrences. If no dates are found within a certain number of attempts (given by the MaxRecurAttempts config variable), this error is returned.

There are a few special circumstances to be aware of.

  1. If the recurrence contains no interval (i.e. is of the form *Y:M:W:D:H:MN:S), the dates come directly from the rtime values. In this case, the 0th event is the first date in the list of dates specified by the rtime. As such, $n must be a positive integer. If $n is negative, or outside the range of dates specified, the returned date will be "undef" (but this is not an error).
  2. A very small number of recurrences have an unknown number of recurring events associated with each date. This only happens if one of the values in the rtime is specified as a range including both a positive and negative index. For example, if the day field in an rtime refers to the day of month, and is 15--15 (i.e. the 15th day to the 15th to the last day), this may include 3 events (on a month with 31 days), 2 event (months with 30 days), 1 event (months with 29 days), or 0 events (months with 28 days). As such, in order to calculate the Nth date, you have to start with the 0th (i.e. base) date and calculate every event until you get the Nth one. For this reason, it is highly recommended that this type of frequency be avoided as it will be quite slow.
  3. Most recurrences have a known number of events (equal to the number of combinations of values in the rtime) for each date. For these, calculating the Nth date is much faster. However, in this case, some of them may refer to an invalid date. For example, if the frequency is 'the 31st of every month' and the base (0th) date is Jan 31, the 1st event would refer to Feb 31. Since that isn't valid, "undef" would be returned for $n=1. Obviously, it would be possible to actually determine the Nth valid event by calculating all N-1 dates, but in the interest of performance, this is not done.
  4. The way the Nth recurring event is calculated differs slightly for NE>0 and N<0 if the delta referred to by the frequency is approximate. To calculate the Nth recurring event (where N>0), you take the base date and add N*DELTA (where DELTA is the delta determined by the frequency). To get the Nth recurring event (where N<0), a date is determine which, if N*DELTA were added to it, would produce the base date. For more details, refer to the Date::Manip::Calc document. In the "SUBTRACTION" in Date::Manip::Calc section in the discussion of approximate date-delta calculations, calculations are done with $subtract = 2.

πŸŽ€ next, prev πŸŽ€

($date,$err) = $recur->next();
($date,$err) = $recur->prev();

These return the next/previous recurring event.

The first time next/prev is called, one of the recurring events will be selected and returned (using the rules discussed below). Subsequent calls to next/prev will return the next or previous event.

Unlike the nth method which will return a specific event (or undef if the Nth even is not defined), the next and prev methods will only work with defined events.

So, for the recurrence:

the 31st of every month

next might return the following sequence of events:

Jan 31 2000
Mar 31 2000
May 31 2000

The rules for determining what event to return the first time one of these is called are as follows:

  1. If there is a range, next will return the first event that occurs after the start of the range. prev will return the last event that occurs before the end of the range.
  2. If there is no range, next will return the first event on or after the base date. prev will return the last event before the base date.

The error codes are the same as for the nth method.

πŸ“œ HISTORY OF THE FREQUENCY NOTATION πŸ“œ

okay bestie, real talk for a sec πŸ’– the author knows the frequency notation up there looks like it was designed by a committee of caffeinated gremlins at first glance. but it is, in his opinion, the best notation for expressing recurring events in existence. he literally considers it the single most important contribution to date/time handling in Date::Manip. periodt. πŸ‘‘

so here's the origin story, and it's kinda iconic actually ✨

when he first decided to add recurring events to Date::Manip, he started by coming up with a list of the common ways people specify recurring events. then he went hunting for a notation that could define all of them. his hope? something similar to cron notation, but more powerful. very "we have cron at home" energy, except the goal was for it to actually be better.

after digging through several specifications (including ISO 8601) and after a discussion on a mailing list about calendar-related topics, it turned out there was just... no concise, flexible notation for recurring events that could handle all of the common forms he'd come up with. πŸ’€

so, out of sheer necessity, he set about inventing his own. and while staring at his list, it hit him: all the parts that specified a frequency were higher level β€” meaning they referred to a larger unit of time β€” than the parts that specified a specific value (what he calls the rtime). in other words, when you lay the terms out from year all the way down to seconds, the frequency part was always sitting to the left of the specific values. πŸ¦‹

that observation led immediately to the notation described above. so he started analyzing it to figure out if it could express all the recurring events he'd come up with. and it succeeded on 100% of them. not only that, but by playing with different values (especially different combinations of m/w/d values), he found it would define recurring events he hadn't even thought of β€” but which seemed perfectly reasonable in hindsight. the notation was giving main character energy before it even had a name. 😭✨

after a very short period, he realized just how powerful this notation was, and set about implementing it. and as he said above: of all the contributions that Date::Manip has made, he considers this one the most important. πŸŽ€


🚨 KNOWN BUGS 🚨

okay so this one is a genuine heads-up, not a drill πŸ™ˆ

if you specify a recurrence that cannot be satisfied for the base date, or for any time after the base date, the recurrence will crash. πŸ’€

this can only happen if you specify a recurrence that always occurs in the spring DST transition using the current timezone rules.

for example, in a US timezone, the current timezone rules state that a DST transition occurs at 02:00:00 on the 2nd Sunday in March, and the clock jumps to 03:00. this started in 2006. as a result, the recurrence

1*3:2:7:2:0:0

with a base date of 2006 or later cannot be satisfied. like, the time literally does not exist. it's not you, it's the clock. 😭


πŸ› BUGS AND QUESTIONS πŸ›

please refer to the Date::Manip::Problems documentation for information on submitting bug reports or questions to the author. he wants to hear from you fr fr 🫢


πŸ‘€ SEE ALSO πŸ‘€

  • Date::Manip β€” main module documentation

πŸ“„ LICENSE πŸ“„

this script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. slay, open source. πŸ’…


πŸ‘‘ AUTHOR πŸ‘‘

Sullivan Beck (https://github.com/SBECK-github/)


and that's a wrap on the whole page, bestie πŸŽ€βœ¨ this beauty shipped as perl v5.42.3, dated 2026-09-01, and it lives at Date::Manip::Recur(3). go forth and schedule your recurrences, you absolute icon. πŸ’–πŸ¦‹

πŸ“¦ where this page comes from