11 our @EXPORT_OK = qw(dispatch generator incr_date can_increment);
23 my $daypat = '(mo|tu|we|th|fr|sa|su)';
24 my $weekpat = '(99|98|97|00|01|02|03|04|05)';
26 my $monthpat = '(01|02|03|04|05|06|07|08|09|10|11|12)';
27 my $seasonpat = '(21|22|23|24)';
29 # Initialize $weeknopat to be '(01|02|03|...|51|52|53)'
31 foreach my $weekno (1..52) {
32 $weeknopat .= sprintf('%02d|', $weekno);
39 # Translate daynames into day of week for DateTime
40 # also used to check if dayname is valid.
42 if (exists $daynames{$pat}) {
44 # figure out day of week for date and compare
45 my $dt = DateTime->new(
50 return ($dt->day_of_week == $daynames{$pat});
51 } elsif (length($pat) == 2) {
53 return $pat == $date[2];
54 } elsif (length($pat) == 4) {
56 my ($mon, $day) = unpack("a2a2", $pat);
58 return (($mon == $date[1]) && ($day == $date[2]));
60 carp "Invalid day pattern '$pat'";
65 # TODO: possible support for extraneous $yp information
66 # ex. $ypdtu but on a bi-weekly (currently assumes weekly)
72 my $dt = DateTime->new(
78 # printf("# subsequent_day: pat='%s' cur='%s'\n", $pat, join('/', @cur));
80 if (exists $daynames{$pat}) {
81 # dd: published on the given weekday
82 my $dow = $dt->day_of_week;
83 my $corr = ($daynames{$pat} - $dow + 7) % 7;
85 if ($dow == $daynames{$pat}) {
86 # the next one is one week hence
89 # the next one is later this week,
90 # or it is next week (ie, on or after next Monday)
91 # $corr will take care of it.
92 $dt->add(days => $corr);
94 @cur = ($dt->year, $dt->month, $dt->day);
95 } elsif (length($pat) == 2) {
96 # DD: published on the give day of every month
97 if ($dt->day >= $pat) {
98 # current date is on or after $pat: next one is next month
99 $dt->set(day => $pat);
100 $dt->add(months => 1);
101 @cur = ($dt->year, $dt->month, $dt->day);
103 # current date is before $pat: set day to pattern
106 } elsif (length($pat) == 4) {
107 # MMDD: published on the given day of the given month
108 my ($mon, $day) = unpack("a2a2", $pat);
110 if (on_or_after($mon, $day, $cur[1], $cur[2])) {
111 # Current date is on or after pattern; next one is next year
114 # Year is now right. Either it's next year (because of on_or_after)
115 # or it's this year, because the current date is NOT on or after
116 # the pattern. Just fix the month and day
120 carp "Invalid day pattern '$pat'";
124 foreach my $i (0..$#cur) {
125 $cur[$i] = '0' . (0 + $cur[$i]) if $cur[$i] < 10;
128 # printf("subsequent_day: returning '%s'\n", join('/', @cur));
133 # Calculate date of 3rd Friday of the month (for example)
134 # 1-5: count from beginning of month
135 # 99-97: count back from end of month
136 sub nth_week_of_month {
142 # printf("# nth_week_of_month(dt, '%s', '%s')\n", $week, $day);
144 if (0 < $week && $week <= 5) {
145 $nth_day = $dt->clone->set(day => 1);
146 } elsif ($week >= 97) {
147 $nth_day = DateTime->last_day_of_month(
155 $dow = $nth_day->day_of_week();
157 # If a particular day was passed in (eg, we want 3rd friday)
158 # then use that day for the calculations, otherwise, just use
159 # the day of the week of the original date (the date $dt).
161 $day = $daynames{$day};
163 $day = $dt->day_of_week;
169 days => ($day - $dow + 7) % 7,
174 $nth_day->subtract(days => ($day - $dow + 7) % 7);
176 # 99: last week of month, 98: second last, etc.
177 for (my $i = 99 - $week; $i > 0; $i--) {
178 $nth_day->subtract(weeks => 1);
182 # There is no nth "day" in the month!
183 return undef if ($dt->month != $nth_day->month);
189 # Internal utility function to match the various different patterns
190 # of month, week, and day
198 # printf("check_date('%s', '%s', '%s')\n", $month, $weekno, $day || '');
203 ($dt->month == $month)
205 ($dt->week_of_month == $weekno)
208 && ($dt->week_of_month ==
209 nth_week_of_month($dt, $weekno, $day)->week_of_month)
216 if ($daynames{$day} != $dt->day_of_week) {
217 # if it's the wrong day of the week, rest doesn't matter
221 if (!defined $month) {
224 ($weekno == 0) # Every week
225 || ($dt->weekday_of_month == $weekno) # this week
228 && ($dt->weekday_of_month ==
229 nth_week_of_month($dt, $weekno, $day)->weekday_of_month)
235 if ($month != $dt->month) {
236 # If it's the wrong month, then we're done
240 # It's the right day of the week
241 # It's the right month
243 if (($weekno == 0) || ($weekno == $dt->weekday_of_month)) {
244 # If this matches, then we're counting from the beginning
245 # of the month and it matches and we're done.
249 # only case left is that the week number is counting from
250 # the end of the month: eg, second last wednesday
253 && (nth_week_of_month($dt, $weekno, $day)->weekday_of_month ==
254 $dt->weekday_of_month)
261 my $dt = DateTime->new(
267 if ($pat =~ m/^$weekpat$daypat$/) {
268 # WWdd: 03we = Third Wednesday
269 return check_date($dt, undef, $1, $2);
270 } elsif ($pat =~ m/^$monthpat$weekpat$daypat$/) {
271 # MMWWdd: 0599tu Last Tuesday in May XXX WRITE ME
272 return check_date($dt, $1, $2, $3);
273 } elsif ($pat =~ m/^$monthpat$weekpat$/) {
274 # MMWW: 1204: Fourth week in December XXX WRITE ME
275 return check_date($dt, $1, $2, undef);
277 carp "invalid week pattern '$pat'";
283 # Use $pat to calcuate the date of the issue following $cur
285 sub subsequent_week {
293 # printf("# subsequent_week('%s', '%s', '%s', '%s')\n", $pat, @cur);
301 if ($pat =~ m/^$weekpat$daypat$/o) {
302 # WWdd: published on given weekday of given week of every month
303 my ($week, $day) = ($1, $2);
305 # printf("# subsequent_week: matched /WWdd/: week='%s', day='%s'\n",
310 $candidate = $dt->clone;
312 if ($dt->day_of_week == $daynames{$day}) {
313 # Current is right day, next one is a week hence
314 $candidate->add(days => 7);
317 days => ($daynames{$day} - $dt->day_of_week + 7) % 7);
320 # 3rd Friday of the month (eg)
321 $candidate = nth_week_of_month($dt, $week, $day);
324 if ($candidate <= $dt) {
325 # If the n'th week of the month happens on before the
326 # current issue, then the next issue is published next
327 # month, otherwise, it's published this month.
328 # This will never happen for the "00: every week" pattern
329 # printf("# subsequent_week: candidate (%s) occurs on or before current date (%s)\n",
330 # join('/', $candidate->year, $candidate->month, $candidate->day),
331 # join('/', $dt->year, $dt->month, $dt->day));
332 $candidate->set(day => 1);
333 $candidate->add(months => 1);
334 $candidate = nth_week_of_month($candidate, $week, $day);
336 } elsif ($pat =~ m/^$monthpat$weekpat$daypat$/) {
337 # MMWWdd: published on given weekday of given week of given month
338 my ($month, $week, $day) = ($1, $2, $3);
340 # printf("# subsequent_week: matched /MMWWdd/: month='%s', week='%s', day='%s'\n",
341 # $month, $week, $day);
343 $candidate = DateTime->new(
348 $candidate = nth_week_of_month($candidate, $week, $day);
349 if ($candidate <= $dt) {
350 # We've missed it for this year, next one that matches
352 $candidate->add(years => 1)->set(day => 1);
353 $candidate = nth_week_of_month($candidate, $week, $day);
355 } elsif ($pat =~ m/^$monthpat$weekpat$/) {
356 # MMWW: published during given week of given month
357 my ($month, $week) = ($1, $2);
359 $candidate = nth_week_of_month(
367 if ($candidate <= $dt) {
368 # Already past the pattern date this year, move to next year
369 $candidate->add(years => 1)->set(day => 1);
370 $candidate = nth_week_of_month($candidate, $week, 'th');
373 carp "invalid week pattern '$pat'";
377 $cur[0] = $candidate->year;
378 $cur[1] = $candidate->month;
379 $cur[2] = $candidate->day;
381 foreach my $i (0..$#cur) {
382 $cur[$i] = '0' . (0 + $cur[$i]) if $cur[$i] < 10;
392 return ($pat eq $date[1]);
395 sub subsequent_month {
401 if ($cur[1] >= $pat) {
402 # Current date is on or after the patter date, so the next
403 # occurence is next year
407 # The year is right, just set the month to the pattern date.
417 return ($pat eq $date[1]);
420 sub subsequent_season {
427 # printf("# subsequent_season: pat='%s', cur='%s'\n", $pat, join('/',@cur));
429 if (($pat < 21) || ($pat > 24)) {
430 carp "Unexpected season '$pat'";
434 if ($caption->winter_starts_year()) {
436 $pat = 20; # fake early winter
439 $cur[1] = 20; # fake early winter
443 if ($cur[1] >= $pat) {
444 # current season is on or past pattern season in this year,
445 # advance to next year
448 # Either we've advanced to the next year or the current season
449 # is before the pattern season in the current year. Either way,
450 # all that remains is to set the season properly
464 sub subsequent_year {
478 # We handle enumeration patterns separately. This just
479 # ensures that when we're processing chronological patterns
480 # we don't match an enumeration pattern.
484 sub subsequent_issue {
490 # Issue generation is handled separately
496 e => \&match_issue, # not really a "chron" code
504 d => \&subsequent_day,
505 e => \&subsequent_issue, # not really a "chron" code
506 w => \&subsequent_week,
507 m => \&subsequent_month,
508 s => \&subsequent_season,
509 y => \&subsequent_year,
513 my $chroncode = shift;
515 return $dispatch{$chroncode};
519 my $chroncode = shift;
521 return $generators{$chroncode};
525 a => {years => 1}, # annual
526 b => {months => 2}, # bimonthly
527 c => {days => 3}, # semiweekly
528 d => {days => 1}, # daily
529 e => {weeks => 2}, # biweekly
530 f => {months => 6}, # semiannual
531 g => {years => 2}, # biennial
532 h => {years => 3}, # triennial
533 i => {days => 2}, # three times / week
534 j => {days => 10}, # three times /month
536 # l => {weeks => 3}, # triweekly (NON-STANDARD)
537 m => {months => 1}, # monthly
538 q => {months => 3}, # quarterly
539 s => {days => 15}, # semimonthly
540 t => {months => 4}, # three times / year
541 w => {weeks => 1}, # weekly
542 # x => completely irregular
548 return exists $increments{$freq};
551 # TODO: add support for weeks as chron level?
554 my $incr = $increments{$freq};
557 if (scalar(@new) == 1) {
558 # only a year is specified. Next date is easy
559 $new[0] += $incr->{years} || 1;
560 } elsif (scalar(@new) == 2) {
561 # Year and month or season
564 $new[1] += ($incr->{months} / 3) || 1;
568 $new[1] -= 4; # 25 - 4 == 21 == Spring after Winter
572 $new[1] += $incr->{months} || 1;
579 } elsif (scalar(@new) == 3) {
580 # Year, Month, Day: now it gets complicated.
582 if ($new[2] =~ /^[0-9]+$/) {
583 # A single number for the day of month, relatively simple
584 my $dt = DateTime->new(
591 $new[1] = $dt->month;
595 warn("Don't know how to cope with @new");
598 foreach my $i (0..$#new) {
599 $new[$i] = '0' . (0 + $new[$i]) if $new[$i] < 10;