| [ Index ] |
PHP Cross Reference of Drupal 6 (gatewave) |
[Summary view] [Print] [Text view]
1 <?php 2 // $Id: date_repeat_calc.inc,v 1.8.4.25 2011/01/01 00:52:57 karens Exp $ 3 /** 4 * @file 5 * Code to compute the dates that match an iCal RRULE. 6 * 7 * Moved to a separate file since it is not used on most pages 8 * so the code is not parsed unless needed. 9 * 10 * Extensive simpletests have been created to test the RRULE calculation 11 * results against official examples from RFC 2445. 12 * 13 * These calculations are expensive and results should be stored or cached 14 * so the calculation code is not called more often than necessary. 15 * 16 * Currently implemented: 17 * INTERVAL, UNTIL, COUNT, EXDATE, RDATE, BYDAY, BYMONTHDAY, BYMONTH, 18 * YEARLY, MONTHLY, WEEKLY, DAILY 19 * 20 * Currently not implemented: 21 * 22 * BYYEARDAY, MINUTELY, HOURLY, SECONDLY, BYMINUTE, BYHOUR, BYSECOND 23 * These could be implemented in the future. 24 * 25 * BYSETPOS 26 * Seldom used anywhere, so no reason to complicated the code. 27 */ 28 29 /** 30 * Private implementation of date_repeat_calc(). 31 * 32 * Compute dates that match the requested rule, within a specified date range. 33 */ 34 function _date_repeat_calc($rrule, $start, $end, $exceptions, $timezone, $additions) { 35 require_once('./'. drupal_get_path('module', 'date_api') .'/date_api_ical.inc'); 36 37 if (empty($timezone)) { 38 $timezone = date_default_timezone_name(); 39 } 40 41 // Make sure the 'EXCEPTIONS' string isn't appended to the rule. 42 $parts = explode("\n", $rrule); 43 if (count($parts)) { 44 $rrule = $parts[0]; 45 } 46 // Get the parsed array of rule values. 47 $rrule = date_ical_parse_rrule('RRULE:', $rrule); 48 49 // Create a date object for the start and end dates. 50 $start_date = date_make_date($start, $timezone); 51 $end_date = date_make_date($end, $timezone); 52 53 // If the rule has an UNTIL, see if that is earlier than the end date. 54 if (!empty($rrule['UNTIL'])) { 55 $until_date = date_ical_date($rrule['UNTIL']); 56 date_timezone_set($until_date, timezone_open($timezone)); 57 if (date_format($until_date, 'U') < date_format($end_date, 'U')) { 58 $end_date = $until_date; 59 } 60 } 61 62 // Get an integer value for the interval, if none given, '1' is implied. 63 $interval = max(1, isset($rrule['INTERVAL']) ? $rrule['INTERVAL'] : 1); 64 $count = isset($rrule['COUNT']) ? $rrule['COUNT'] : NULL; 65 66 if (empty($rrule['FREQ'])) { 67 $rrule['FREQ'] = 'DAILY'; 68 } 69 70 // Make sure DAILY frequency isn't used in places it won't work; 71 if (!empty($rrule['BYMONTHDAY']) && !in_array($rrule['FREQ'], array('MONTHLY', 'YEARLY'))) { 72 $rrule['FREQ'] = 'MONTHLY'; 73 } 74 else if (!empty($rrule['BYDAY']) && !in_array($rrule['FREQ'], array('MONTHLY', 'WEEKLY', 'YEARLY'))) { 75 $rrule['FREQ'] = 'WEEKLY'; 76 } 77 78 // Find the time period to jump forward between dates. 79 switch ($rrule['FREQ']) { 80 case 'DAILY': 81 $jump = $interval .' days'; 82 break; 83 case 'WEEKLY': 84 $jump = $interval .' weeks'; 85 break; 86 case 'MONTHLY': 87 $jump = $interval .' months'; 88 break; 89 case 'YEARLY': 90 $jump = $interval .' years'; 91 break; 92 } 93 94 $rrule = date_repeat_adjust_rrule($rrule, $start_date); 95 96 // The start date always goes into the results, whether or not it meets 97 // the rules. RFC 2445 includes examples where the start date DOES NOT 98 // meet the rules, but the expected results always include the start date. 99 $days = array(date_format($start_date, DATE_FORMAT_DATETIME)); 100 101 // BYMONTHDAY will look for specific days of the month in one or more months. 102 // This process is only valid when frequency is monthly or yearly. 103 104 if (!empty($rrule['BYMONTHDAY'])) { 105 $finished = FALSE; 106 $current_day = drupal_clone($start_date); 107 $direction_days = array(); 108 // Deconstruct the day in case it has a negative modifier. 109 foreach ($rrule['BYMONTHDAY'] as $day) { 110 preg_match("@(-)?([0-9]{1,2})@", $day, $regs); 111 if (!empty($regs[2])) { 112 // Convert parameters into full day name, count, and direction. 113 $direction_days[$day] = array( 114 'direction' => !empty($regs[1]) ? $regs[1] : '+', 115 'direction_count' => $regs[2], 116 ); 117 } 118 } 119 while (!$finished) { 120 $period_finished = FALSE; 121 while (!$period_finished) { 122 foreach ($rrule['BYMONTHDAY'] as $monthday) { 123 $day = $direction_days[$monthday]; 124 $current_day = date_repeat_set_month_day($current_day, NULL, $day['direction_count'], $day['direction'], $timezone); 125 date_repeat_add_dates($days, $current_day, $start_date, $end_date, $exceptions, $rrule); 126 if ($finished = date_repeat_is_finished($current_day, $days, $count, $end_date)) { 127 $period_finished = TRUE; 128 } 129 } 130 // If it's monthly, keep looping through months, one INTERVAL at a time. 131 if ($rrule['FREQ'] == 'MONTHLY') { 132 if ($finished = date_repeat_is_finished($current_day, $days, $count, $end_date)) { 133 $period_finished = TRUE; 134 } 135 // Back up to first of month and jump. 136 $current_day = date_repeat_set_month_day($current_day, NULL, 1, '+', $timezone); 137 date_modify($current_day, '+'. $jump); 138 } 139 // If it's yearly, break out of the loop at the 140 // end of every year, and jump one INTERVAL in years. 141 else { 142 if (date_format($current_day, 'n') == 12) { 143 $period_finished = TRUE; 144 } 145 else { 146 // Back up to first of month and jump. 147 $current_day = date_repeat_set_month_day($current_day, NULL, 1, '+', $timezone); 148 date_modify($current_day, '+1 month'); 149 } 150 } 151 } 152 if ($rrule['FREQ'] == 'YEARLY') { 153 // Back up to first of year and jump. 154 $current_day = date_repeat_set_year_day($current_day, NULL, 1, '+', $timezone); 155 date_modify($current_day, '+'. $jump); 156 } 157 $finished = date_repeat_is_finished($current_day, $days, $count, $end_date); 158 } 159 } 160 161 // This is the simple fallback case, not looking for any BYDAY, 162 // just repeating the start date. Because of imputed BYDAY above, this 163 // will only test TRUE for a DAILY or less frequency (like HOURLY). 164 165 elseif (empty($rrule['BYDAY'])) { 166 // $current_day will keep track of where we are in the calculation. 167 $current_day = drupal_clone($start_date); 168 $finished = FALSE; 169 $months = !empty($rrule['BYMONTH']) ? $rrule['BYMONTH'] : array(); 170 while (!$finished) { 171 date_repeat_add_dates($days, $current_day, $start_date, $end_date, $exceptions, $rrule); 172 $finished = date_repeat_is_finished($current_day, $days, $count, $end_date); 173 date_modify($current_day, '+'. $jump); 174 } 175 } 176 177 else { 178 179 // More complex searches for day names and criteria like '-1SU' or '2TU,2TH', 180 // require that we interate through the whole time period checking each BYDAY. 181 182 // Create helper array to pull day names out of iCal day strings. 183 $day_names = date_repeat_dow_day_options(FALSE); 184 $days_of_week = array_keys($day_names); 185 186 // Parse out information about the BYDAYs and separate them 187 // depending on whether they have directional parameters like -1SU or 2TH. 188 $month_days = array(); 189 $week_days = array(); 190 191 // Find the right first day of the week to use, iCal rules say Monday 192 // should be used if none is specified. 193 $week_start_rule = !empty($rrule['WKST']) ? trim($rrule['WKST']) : 'MO'; 194 $week_start_day = $day_names[$week_start_rule]; 195 196 // Make sure the week days array is sorted into week order, 197 // we use the $ordered_keys to get the right values into the key 198 // and force the array to that order. Needed later when we 199 // iterate through each week looking for days so we don't 200 // jump to the next week when we hit a day out of order. 201 $ordered = date_repeat_days_ordered($week_start_rule); 202 $ordered_keys = array_flip($ordered); 203 204 foreach ($rrule['BYDAY'] as $day) { 205 preg_match("@(-)?([0-9]+)?([SU|MO|TU|WE|TH|FR|SA]{2})@", trim($day), $regs); 206 if (!empty($regs[2])) { 207 // Convert parameters into full day name, count, and direction. 208 $direction_days[] = array( 209 'day' => $day_names[$regs[3]], 210 'direction' => !empty($regs[1]) ? $regs[1] : '+', 211 'direction_count' => $regs[2], 212 ); 213 } 214 else { 215 $week_days[$ordered_keys[$regs[3]]] = $day_names[$regs[3]]; 216 } 217 } 218 ksort($week_days); 219 220 // BYDAYs with parameters like -1SU (last Sun) or 2TH (second Thur) 221 // need to be processed one month or year at a time. 222 223 if (!empty($direction_days) && in_array($rrule['FREQ'], array('MONTHLY', 'YEARLY'))) { 224 $finished = FALSE; 225 $current_day = drupal_clone($start_date); 226 while (!$finished) { 227 foreach ($direction_days as $day) { 228 // Find the BYDAY date in the current month. 229 if ($rrule['FREQ'] == 'MONTHLY') { 230 $current_day = date_repeat_set_month_day($current_day, $day['day'], $day['direction_count'], $day['direction'], $timezone); 231 } 232 else { 233 $current_day = date_repeat_set_year_day($current_day, $day['day'], $day['direction_count'], $day['direction'], $timezone); 234 } 235 date_repeat_add_dates($days, $current_day, $start_date, $end_date, $exceptions, $rrule); 236 } 237 $finished = date_repeat_is_finished($current_day, $days, $count, $end_date); 238 // Reset to beginning of period before jumping to next period. 239 // Needed especially when working with values like 'last Saturday' 240 // to be sure we don't skip months like February. 241 $year = date_format($current_day, 'Y'); 242 $month = date_format($current_day, 'n'); 243 if ($rrule['FREQ'] == 'MONTHLY') { 244 date_date_set($current_day, $year, $month, 1); 245 } 246 else { 247 date_date_set($current_day, $year, 1, 1); 248 } 249 // Jump to the next period. 250 date_modify($current_day, '+'. $jump); 251 } 252 } 253 254 // For BYDAYs without parameters,like TU,TH (every Tues and Thur), 255 // we look for every one of those days during the frequency period. 256 // Iterate through periods of a WEEK, MONTH, or YEAR, checking for 257 // the days of the week that match our criteria for each week in the 258 // period, then jumping ahead to the next week, month, or year, 259 // an INTERVAL at a time. 260 261 if (!empty($week_days) && in_array($rrule['FREQ'], array('MONTHLY', 'WEEKLY', 'YEARLY'))) { 262 $finished = FALSE; 263 $current_day = drupal_clone($start_date); 264 $format = $rrule['FREQ'] == 'YEARLY' ? 'Y' : 'n'; 265 $current_period = date_format($current_day, $format); 266 // Back up to the beginning of the week in case we are somewhere in the 267 // middle of the possible week days, needed so we don't prematurely 268 // jump to the next week. The date_repeat_add_dates() function will 269 // keep dates outside the range from getting added. 270 if (date_format($current_day, 'l') != $day_names[$day]) { 271 date_modify($current_day, '-1 '. $week_start_day); 272 } 273 while (!$finished) { 274 $period_finished = FALSE; 275 while (!$period_finished) { 276 $moved = FALSE; 277 foreach ($week_days as $delta => $day) { 278 // Find the next occurence of each day in this week, only add it 279 // if we are still in the current month or year. The date_repeat_add_dates 280 // function is insufficient to test whether to include this date 281 // if we are using a rule like 'every other month', so we must 282 // explicitly test it here. 283 284 // If we're already on the right day, don't jump or we 285 // will prematurely move into the next week. 286 if (date_format($current_day, 'l') != $day) { 287 date_modify($current_day, '+1 '. $day); 288 $moved = TRUE; 289 } 290 if ($rrule['FREQ'] == 'WEEKLY' || date_format($current_day, $format) == $current_period) { 291 date_repeat_add_dates($days, $current_day, $start_date, $end_date, $exceptions, $rrule); 292 } 293 } 294 $finished = date_repeat_is_finished($current_day, $days, $count, $end_date); 295 296 // Make sure we don't get stuck in endless loop if the current 297 // day never got changed above. 298 if (!$moved) { 299 date_modify($current_day, '+1 day'); 300 } 301 302 // If this is a WEEKLY frequency, stop after each week, 303 // otherwise, stop when we've moved outside the current period. 304 // Jump to the end of the week, then test the period. 305 if ($finished || $rrule['FREQ'] == 'WEEKLY') { 306 $period_finished = TRUE; 307 } 308 elseif ($rrule['FREQ'] != 'WEEKLY' && date_format($current_day, $format) != $current_period) { 309 $period_finished = TRUE; 310 } 311 } 312 313 if ($finished) { 314 continue; 315 } 316 317 // We'll be at the end of a week, month, or year when 318 // we get to this point in the code. 319 320 // Go back to the beginning of this period before we jump, to 321 // ensure we jump to the first day of the next period. 322 switch ($rrule['FREQ']) { 323 case 'WEEKLY': 324 date_modify($current_day, '+1 '. $week_start_day); 325 date_modify($current_day, '-1 week'); 326 break; 327 case 'MONTHLY': 328 date_modify($current_day, '-'. (date_format($current_day, 'j') - 1) .' days'); 329 date_modify($current_day, '-1 month'); 330 break; 331 case 'YEARLY': 332 date_modify($current_day, '-'. date_format($current_day, 'z') .' days'); 333 date_modify($current_day, '-1 year'); 334 break; 335 } 336 // Jump ahead to the next period to be evaluated. 337 date_modify($current_day, '+'. $jump); 338 $current_period = date_format($current_day, $format); 339 $finished = date_repeat_is_finished($current_day, $days, $count, $end_date); 340 } 341 } 342 } 343 344 // add additional dates 345 foreach($additions as $addition) { 346 $days[] = date_format($addition, DATE_FORMAT_DATETIME); 347 } 348 349 sort($days); 350 return $days; 351 } 352 353 /** 354 * See if the RRULE needs some imputed values added to it. 355 */ 356 function date_repeat_adjust_rrule($rrule, $start_date) { 357 // If this is not a valid value, do nothing; 358 if (empty($rrule) || empty($rrule['FREQ'])) { 359 return array(); 360 } 361 362 // RFC 2445 says if no day or monthday is specified when creating repeats for 363 // weeks, months, or years, impute the value from the start date. 364 365 if (empty($rrule['BYDAY']) && $rrule['FREQ'] == 'WEEKLY') { 366 $rrule['BYDAY'] = array(date_repeat_dow2day(date_format($start_date, 'w'))); 367 } 368 elseif (empty($rrule['BYDAY']) && empty($rrule['BYMONTHDAY']) && $rrule['FREQ'] == 'MONTHLY') { 369 $rrule['BYMONTHDAY'] = array(date_format($start_date, 'j')); 370 } 371 elseif (empty($rrule['BYDAY']) && empty($rrule['BYMONTHDAY']) && empty($rrule['BYYEARDAY']) && $rrule['FREQ'] == 'YEARLY') { 372 $rrule['BYMONTHDAY'] = array(date_format($start_date, 'j')); 373 if (empty($rrule['BYMONTH'])) { 374 $rrule['BYMONTH'] = array(date_format($start_date, 'n')); 375 } 376 } 377 // If we are processing rules for period other than YEARLY or MONTHLY 378 // and have BYDAYS like 2SU or -1SA, simplify them to SU or SA since the 379 // position rules make no sense in other periods and just add complexity. 380 381 elseif (!empty($rrule['BYDAY']) && !in_array($rrule['FREQ'], array('MONTHLY', 'YEARLY'))) { 382 foreach ($rrule['BYDAY'] as $delta => $BYDAY) { 383 $rrule['BYDAY'][$delta] = substr($BYDAY, -2); 384 } 385 } 386 387 return $rrule; 388 } 389 390 /** 391 * Helper function to add found date to the $dates array. 392 * 393 * Check that the date to be added is between the start and end date 394 * and that it is not in the $exceptions, nor already in the $days array, 395 * and that it meets other criteria in the RRULE. 396 */ 397 function date_repeat_add_dates(&$days, $current_day, $start_date, $end_date, $exceptions, $rrule) { 398 if (isset($rrule['COUNT']) && sizeof($days) >= $rrule['COUNT']) { 399 return FALSE; 400 } 401 $formatted = date_format($current_day, DATE_FORMAT_DATETIME); 402 if ($formatted > date_format($end_date, DATE_FORMAT_DATETIME)) { 403 return FALSE; 404 } 405 if ($formatted < date_format($start_date, DATE_FORMAT_DATETIME)) { 406 return FALSE; 407 } 408 if (in_array(date_format($current_day, 'Y-m-d'), $exceptions)) { 409 return FALSE; 410 } 411 if (!empty($rrule['BYDAY'])) { 412 $BYDAYS = $rrule['BYDAY']; 413 foreach ($BYDAYS as $delta => $BYDAY) { 414 $BYDAYS[$delta] = substr($BYDAY, -2); 415 } 416 if (!in_array(date_repeat_dow2day(date_format($current_day, 'w')), $BYDAYS)) { 417 return FALSE; 418 }} 419 if (!empty($rrule['BYYEAR']) && !in_array(date_format($current_day, 'Y'), $rrule['BYYEAR'])) { 420 return FALSE; 421 } 422 if (!empty($rrule['BYMONTH']) && !in_array(date_format($current_day, 'n'), $rrule['BYMONTH'])) { 423 return FALSE; 424 } 425 if (!empty($rrule['BYMONTHDAY'])) { 426 // Test month days, but only if there are no negative numbers. 427 $test = TRUE; 428 $BYMONTHDAYS = array(); 429 foreach ($rrule['BYMONTHDAY'] as $day) { 430 if ($day > 0) { 431 $BYMONTHDAYS[] = $day; 432 } 433 else { 434 $test = FALSE; 435 break; 436 } 437 } 438 if ($test && !empty($BYMONTHDAYS) && !in_array(date_format($current_day, 'j'), $BYMONTHDAYS)) { 439 return FALSE; 440 } 441 } 442 // Don't add a day if it is already saved so we don't throw the count off. 443 if (in_array($formatted, $days)) { 444 return TRUE; 445 } 446 else { 447 $days[] = $formatted; 448 } 449 } 450 451 /** 452 * Stop when $current_day is greater than $end_date or $count is reached. 453 */ 454 function date_repeat_is_finished($current_day, $days, $count, $end_date) { 455 if (($count && sizeof($days) >= $count) 456 || date_format($current_day, 'U') > date_format($end_date, 'U')) { 457 return TRUE; 458 } 459 else { 460 return FALSE; 461 } 462 } 463 464 /** 465 * Set a date object to a specific day of the month. 466 * 467 * Example, 468 * date_set_month_day($date, 'Sunday', 2, '-') 469 * will reset $date to the second to last Sunday in the month. 470 * If $day is empty, will set to the number of days from the 471 * beginning or end of the month. 472 */ 473 function date_repeat_set_month_day($date_in, $day, $count = 1, $direction = '+', $timezone = 'UTC') { 474 if (is_object($date_in)) { 475 $current_month = date_format($date_in, 'n'); 476 477 // Reset to the start of the month. 478 // We should be able to do this with date_date_set(), but 479 // for some reason the date occasionally gets confused if run 480 // through this function multiple times. It seems to work 481 // reliably if we create a new object each time. 482 $datetime = date_format($date_in, DATE_FORMAT_DATETIME); 483 $datetime = substr_replace($datetime, '01', 8, 2); 484 $date = date_make_date($datetime, $timezone); 485 if ($direction == '-') { 486 // For negative search, start from the end of the month. 487 date_modify($date, '+1 month'); 488 } 489 else { 490 // For positive search, back up one day to get outside the 491 // current month, so we can catch the first of the month. 492 date_modify($date, '-1 day'); 493 } 494 495 if (empty($day)) { 496 date_modify($date, $direction . $count .' days'); 497 } 498 else { 499 // Use the English text for order, like First Sunday 500 // instead of +1 Sunday to overcome PHP5 bug, (see #369020). 501 $order = date_order(); 502 $step = $count <= 5 ? $order[$direction . $count] : $count; 503 date_modify($date, $step . ' ' . $day); 504 } 505 506 // If that takes us outside the current month, don't go there. 507 if (date_format($date, 'n') == $current_month) { 508 return $date; 509 } 510 } 511 return $date_in; 512 } 513 514 /** 515 * Set a date object to a specific day of the year. 516 * 517 * Example, 518 * date_set_year_day($date, 'Sunday', 2, '-') 519 * will reset $date to the second to last Sunday in the year. 520 * If $day is empty, will set to the number of days from the 521 * beginning or end of the year. 522 */ 523 function date_repeat_set_year_day($date_in, $day, $count = 1, $direction = '+', $timezone = 'UTC') { 524 if (is_object($date_in)) { 525 $current_year = date_format($date_in, 'Y'); 526 527 // Reset to the start of the month. 528 // See note above. 529 $datetime = date_format($date_in, DATE_FORMAT_DATETIME); 530 $datetime = substr_replace($datetime, '01-01', 5, 5); 531 $date = date_make_date($datetime, $timezone); 532 if ($direction == '-') { 533 // For negative search, start from the end of the year. 534 date_modify($date, '+1 year'); 535 } 536 else { 537 // For positive search, back up one day to get outside the 538 // current year, so we can catch the first of the year. 539 date_modify($date, '-1 day'); 540 } 541 542 if (empty($day)) { 543 date_modify($date, $direction . $count .' days'); 544 } 545 else { 546 // Use the English text for order, like First Sunday 547 // instead of +1 Sunday to overcome PHP5 bug, (see #369020). 548 $order = date_order(); 549 $step = $count <= 5 ? $order[$direction . $count] : $count; 550 date_modify($date, $step . ' ' . $day); 551 } 552 553 // If that takes us outside the current year, don't go there. 554 if (date_format($date, 'Y') == $current_year) { 555 return $date; 556 } 557 } 558 return $date_in; 559 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Thu Mar 24 11:18:33 2011 | Cross-referenced by PHPXref 0.7 |