PageRenderTime 57ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/examples/calendar/remote/php/lib/recur.php

https://github.com/mattsmith321/Extensible
PHP | 726 lines | 564 code | 111 blank | 51 comment | 63 complexity | aeb8c290b2142077a596fd8cda4780f1 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. * Name: When
  4. * Author: Thomas Planer <tplaner@gmail.com>
  5. * Location: http://github.com/tplaner/When
  6. * Created: September 2010
  7. * Description: Determines the next date of recursion given an iCalendar "rrule" like pattern.
  8. * Requirements: PHP 5.3+ - makes extensive use of the Date and Time library (http://us2.php.net/manual/en/book.datetime.php)
  9. * Last update: April 12, 2011
  10. */
  11. class When
  12. {
  13. public $frequency;
  14. public $start_date;
  15. public $try_date;
  16. public $end_date;
  17. public $gobymonth;
  18. public $bymonth;
  19. public $gobyweekno;
  20. public $byweekno;
  21. public $gobyyearday;
  22. public $byyearday;
  23. public $gobymonthday;
  24. public $bymonthday;
  25. public $gobyday;
  26. public $byday;
  27. public $gobysetpos;
  28. public $bysetpos;
  29. public $suggestions;
  30. public $count;
  31. public $counter;
  32. public $goenddate;
  33. public $interval;
  34. public $wkst;
  35. public $valid_week_days;
  36. public $valid_frequency;
  37. /**
  38. * __construct
  39. */
  40. public function __construct()
  41. {
  42. $this->frequency = null;
  43. $this->gobymonth = false;
  44. $this->bymonth = range(1,12);
  45. $this->gobymonthday = false;
  46. $this->bymonthday = range(1,31);
  47. $this->gobyday = false;
  48. // setup the valid week days (0 = sunday)
  49. $this->byday = range(0,6);
  50. $this->gobyyearday = false;
  51. $this->byyearday = range(0,366);
  52. $this->gobysetpos = false;
  53. $this->bysetpos = range(1,366);
  54. $this->gobyweekno = false;
  55. // setup the range for valid weeks
  56. $this->byweekno = range(0,54);
  57. $this->suggestions = array();
  58. // this will be set if a count() is specified
  59. $this->count = 0;
  60. // how many *valid* results we returned
  61. $this->counter = 0;
  62. // max date we'll return
  63. $this->end_date = new DateTime('9999-12-31');
  64. // the interval to increase the pattern by
  65. $this->interval = 1;
  66. // what day does the week start on? (0 = sunday)
  67. $this->wkst = 0;
  68. $this->valid_week_days = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
  69. $this->valid_frequency = array('SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY');
  70. }
  71. /**
  72. * @param DateTime|string $start_date of the recursion - also is the first return value.
  73. * @param string $frequency of the recrusion, valid frequencies: secondly, minutely, hourly, daily, weekly, monthly, yearly
  74. */
  75. public function recur($start_date, $frequency = "daily")
  76. {
  77. try
  78. {
  79. if(is_object($start_date))
  80. {
  81. $this->start_date = clone $start_date;
  82. }
  83. else
  84. {
  85. // timestamps within the RFC have a 'Z' at the end of them, remove this.
  86. $start_date = trim($start_date, 'Z');
  87. $this->start_date = new DateTime($start_date);
  88. }
  89. $this->try_date = clone $this->start_date;
  90. }
  91. catch(Exception $e)
  92. {
  93. throw new InvalidArgumentException('Invalid start date DateTime: ' . $e);
  94. }
  95. $this->freq($frequency);
  96. return $this;
  97. }
  98. public function freq($frequency)
  99. {
  100. if(in_array(strtoupper($frequency), $this->valid_frequency))
  101. {
  102. $this->frequency = strtoupper($frequency);
  103. }
  104. else
  105. {
  106. throw new InvalidArgumentException('Invalid frequency type.');
  107. }
  108. return $this;
  109. }
  110. // accepts an rrule directly
  111. public function rrule($rrule)
  112. {
  113. // strip off a trailing semi-colon
  114. $rrule = trim($rrule, ";");
  115. $parts = explode(";", $rrule);
  116. foreach($parts as $part)
  117. {
  118. list($rule, $param) = explode("=", $part);
  119. $rule = strtoupper($rule);
  120. $param = strtoupper($param);
  121. switch($rule)
  122. {
  123. case "FREQ":
  124. $this->frequency = $param;
  125. break;
  126. case "UNTIL":
  127. $this->until($param);
  128. break;
  129. case "COUNT":
  130. $this->count($param);
  131. break;
  132. case "INTERVAL":
  133. $this->interval($param);
  134. break;
  135. case "BYDAY":
  136. $params = explode(",", $param);
  137. $this->byday($params);
  138. break;
  139. case "BYMONTHDAY":
  140. $params = explode(",", $param);
  141. $this->bymonthday($params);
  142. break;
  143. case "BYYEARDAY":
  144. $params = explode(",", $param);
  145. $this->byyearday($params);
  146. break;
  147. case "BYWEEKNO":
  148. $params = explode(",", $param);
  149. $this->byweekno($params);
  150. break;
  151. case "BYMONTH":
  152. $params = explode(",", $param);
  153. $this->bymonth($params);
  154. break;
  155. case "BYSETPOS":
  156. $params = explode(",", $param);
  157. $this->bysetpos($params);
  158. break;
  159. case "WKST":
  160. $this->wkst($param);
  161. break;
  162. }
  163. }
  164. return $this;
  165. }
  166. //max number of items to return based on the pattern
  167. public function count($count)
  168. {
  169. $this->count = (int)$count;
  170. return $this;
  171. }
  172. // how often the recurrence rule repeats
  173. public function interval($interval)
  174. {
  175. $this->interval = (int)$interval;
  176. return $this;
  177. }
  178. // starting day of the week
  179. public function wkst($day)
  180. {
  181. switch($day)
  182. {
  183. case 'SU':
  184. $this->wkst = 0;
  185. break;
  186. case 'MO':
  187. $this->wkst = 1;
  188. break;
  189. case 'TU':
  190. $this->wkst = 2;
  191. break;
  192. case 'WE':
  193. $this->wkst = 3;
  194. break;
  195. case 'TH':
  196. $this->wkst = 4;
  197. break;
  198. case 'FR':
  199. $this->wkst = 5;
  200. break;
  201. case 'SA':
  202. $this->wkst = 6;
  203. break;
  204. }
  205. return $this;
  206. }
  207. // max date
  208. public function until($end_date)
  209. {
  210. try
  211. {
  212. if(is_object($end_date))
  213. {
  214. $this->end_date = clone $end_date;
  215. }
  216. else
  217. {
  218. // timestamps within the RFC have a 'Z' at the end of them, remove this.
  219. $end_date = trim($end_date, 'Z');
  220. $this->end_date = new DateTime($end_date);
  221. }
  222. }
  223. catch(Exception $e)
  224. {
  225. throw new InvalidArgumentException('Invalid end date DateTime: ' . $e);
  226. }
  227. return $this;
  228. }
  229. public function bymonth($months)
  230. {
  231. if(is_array($months))
  232. {
  233. $this->gobymonth = true;
  234. $this->bymonth = $months;
  235. }
  236. return $this;
  237. }
  238. public function bymonthday($days)
  239. {
  240. if(is_array($days))
  241. {
  242. $this->gobymonthday = true;
  243. $this->bymonthday = $days;
  244. }
  245. return $this;
  246. }
  247. public function byweekno($weeks)
  248. {
  249. $this->gobyweekno = true;
  250. if(is_array($weeks))
  251. {
  252. $this->byweekno = $weeks;
  253. }
  254. return $this;
  255. }
  256. public function bysetpos($days)
  257. {
  258. $this->gobysetpos = true;
  259. if(is_array($days))
  260. {
  261. $this->bysetpos = $days;
  262. }
  263. return $this;
  264. }
  265. public function byday($days)
  266. {
  267. $this->gobyday = true;
  268. if(is_array($days))
  269. {
  270. $this->byday = array();
  271. foreach($days as $day)
  272. {
  273. $len = strlen($day);
  274. $as = '+';
  275. // 0 mean no occurence is set
  276. $occ = 0;
  277. if($len == 3)
  278. {
  279. $occ = substr($day, 0, 1);
  280. }
  281. if($len == 4)
  282. {
  283. $as = substr($day, 0, 1);
  284. $occ = substr($day, 1, 1);
  285. }
  286. if($as == '-')
  287. {
  288. $occ = '-' . $occ;
  289. }
  290. else
  291. {
  292. $occ = '+' . $occ;
  293. }
  294. $day = substr($day, -2, 2);
  295. switch($day)
  296. {
  297. case 'SU':
  298. $this->byday[] = $occ . 'SU';
  299. break;
  300. case 'MO':
  301. $this->byday[] = $occ . 'MO';
  302. break;
  303. case 'TU':
  304. $this->byday[] = $occ . 'TU';
  305. break;
  306. case 'WE':
  307. $this->byday[] = $occ . 'WE';
  308. break;
  309. case 'TH':
  310. $this->byday[] = $occ . 'TH';
  311. break;
  312. case 'FR':
  313. $this->byday[] = $occ . 'FR';
  314. break;
  315. case 'SA':
  316. $this->byday[] = $occ . 'SA';
  317. break;
  318. }
  319. }
  320. }
  321. return $this;
  322. }
  323. public function byyearday($days)
  324. {
  325. $this->gobyyearday = true;
  326. if(is_array($days))
  327. {
  328. $this->byyearday = $days;
  329. }
  330. return $this;
  331. }
  332. // this creates a basic list of dates to "try"
  333. protected function create_suggestions()
  334. {
  335. switch($this->frequency)
  336. {
  337. case "YEARLY":
  338. $interval = 'year';
  339. break;
  340. case "MONTHLY":
  341. $interval = 'month';
  342. break;
  343. case "WEEKLY":
  344. $interval = 'week';
  345. break;
  346. case "DAILY":
  347. $interval = 'day';
  348. break;
  349. case "HOURLY":
  350. $interval = 'hour';
  351. break;
  352. case "MINUTELY":
  353. $interval = 'minute';
  354. break;
  355. case "SECONDLY":
  356. $interval = 'second';
  357. break;
  358. }
  359. $month_day = $this->try_date->format('j');
  360. $month = $this->try_date->format('n');
  361. $year = $this->try_date->format('Y');
  362. $timestamp = $this->try_date->format('H:i:s');
  363. if($this->gobysetpos)
  364. {
  365. if($this->try_date == $this->start_date)
  366. {
  367. $this->suggestions[] = clone $this->try_date;
  368. }
  369. else
  370. {
  371. if($this->gobyday)
  372. {
  373. foreach($this->bysetpos as $_pos)
  374. {
  375. $tmp_array = array();
  376. $_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
  377. foreach($_mdays as $_mday)
  378. {
  379. $date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
  380. $occur = ceil($_mday / 7);
  381. $day_of_week = $date_time->format('l');
  382. $dow_abr = strtoupper(substr($day_of_week, 0, 2));
  383. // set the day of the month + (positive)
  384. $occur = '+' . $occur . $dow_abr;
  385. $occur_zero = '+0' . $dow_abr;
  386. // set the day of the month - (negative)
  387. $total_days = $date_time->format('t') - $date_time->format('j');
  388. $occur_neg = '-' . ceil(($total_days + 1)/7) . $dow_abr;
  389. $day_from_end_of_month = $date_time->format('t') + 1 - $_mday;
  390. if(in_array($occur, $this->byday) || in_array($occur_zero, $this->byday) || in_array($occur_neg, $this->byday))
  391. {
  392. $tmp_array[] = clone $date_time;
  393. }
  394. }
  395. if($_pos > 0)
  396. {
  397. $this->suggestions[] = clone $tmp_array[$_pos - 1];
  398. }
  399. else
  400. {
  401. $this->suggestions[] = clone $tmp_array[count($tmp_array) + $_pos];
  402. }
  403. }
  404. }
  405. }
  406. }
  407. elseif($this->gobyyearday)
  408. {
  409. foreach($this->byyearday as $_day)
  410. {
  411. if($_day >= 0)
  412. {
  413. $_day--;
  414. $_time = strtotime('+' . $_day . ' days', mktime(0, 0, 0, 1, 1, $year));
  415. $this->suggestions[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
  416. }
  417. else
  418. {
  419. $year_day_neg = 365 + $_day;
  420. $leap_year = $this->try_date->format('L');
  421. if($leap_year == 1)
  422. {
  423. $year_day_neg = 366 + $_day;
  424. }
  425. $_time = strtotime('+' . $year_day_neg . ' days', mktime(0, 0, 0, 1, 1, $year));
  426. $this->suggestions[] = new Datetime(date('Y-m-d', $_time) . ' ' . $timestamp);
  427. }
  428. }
  429. }
  430. // special case because for years you need to loop through the months too
  431. elseif($this->gobyday && $interval == "year")
  432. {
  433. foreach($this->bymonth as $_month)
  434. {
  435. // this creates an array of days of the month
  436. $_mdays = range(1, date('t',mktime(0,0,0,$_month,1,$year)));
  437. foreach($_mdays as $_mday)
  438. {
  439. $date_time = new DateTime($year . '-' . $_month . '-' . $_mday . ' ' . $timestamp);
  440. // get the week of the month (1, 2, 3, 4, 5, etc)
  441. $week = $date_time->format('W');
  442. if($date_time >= $this->start_date && in_array($week, $this->byweekno))
  443. {
  444. $this->suggestions[] = clone $date_time;
  445. }
  446. }
  447. }
  448. }
  449. elseif($interval == "day")
  450. {
  451. $this->suggestions[] = clone $this->try_date;
  452. }
  453. elseif($interval == "week")
  454. {
  455. $this->suggestions[] = clone $this->try_date;
  456. if($this->gobyday)
  457. {
  458. $week_day = $this->try_date->format('w');
  459. $days_in_month = $this->try_date->format('t');
  460. $overflow_count = 1;
  461. $_day = $month_day;
  462. $run = true;
  463. while($run)
  464. {
  465. $_day++;
  466. if($_day <= $days_in_month)
  467. {
  468. $tmp_date = new DateTime($year . '-' . $month . '-' . $_day . ' ' . $timestamp);
  469. }
  470. else
  471. {
  472. //$tmp_month = $month+1;
  473. $tmp_date = new DateTime($year . '-' . $month . '-' . $overflow_count . ' ' . $timestamp);
  474. $tmp_date->modify('+1 month');
  475. $overflow_count++;
  476. }
  477. $week_day = $tmp_date->format('w');
  478. if($this->try_date == $this->start_date)
  479. {
  480. if($week_day == $this->wkst)
  481. {
  482. $this->try_date = clone $tmp_date;
  483. $this->try_date->modify('-7 days');
  484. $run = false;
  485. }
  486. }
  487. if($week_day != $this->wkst)
  488. {
  489. $this->suggestions[] = clone $tmp_date;
  490. }
  491. else
  492. {
  493. $run = false;
  494. }
  495. }
  496. }
  497. }
  498. elseif($this->gobyday || $interval == "month")
  499. {
  500. $_mdays = range(1, date('t',mktime(0,0,0,$month,1,$year)));
  501. foreach($_mdays as $_mday)
  502. {
  503. $date_time = new DateTime($year . '-' . $month . '-' . $_mday . ' ' . $timestamp);
  504. // get the week of the month (1, 2, 3, 4, 5, etc)
  505. $week = $date_time->format('W');
  506. if($date_time >= $this->start_date && in_array($week, $this->byweekno))
  507. {
  508. $this->suggestions[] = clone $date_time;
  509. }
  510. }
  511. }
  512. elseif($this->gobymonth)
  513. {
  514. foreach($this->bymonth as $_month)
  515. {
  516. $date_time = new DateTime($year . '-' . $_month . '-' . $month_day . ' ' . $timestamp);
  517. if($date_time >= $this->start_date)
  518. {
  519. $this->suggestions[] = clone $date_time;
  520. }
  521. }
  522. }
  523. else
  524. {
  525. $this->suggestions[] = clone $this->try_date;
  526. }
  527. if($interval == "month")
  528. {
  529. $this->try_date->modify('last day of ' . $this->interval . ' ' . $interval);
  530. }
  531. else
  532. {
  533. $this->try_date->modify($this->interval . ' ' . $interval);
  534. }
  535. }
  536. protected function valid_date($date)
  537. {
  538. $year = $date->format('Y');
  539. $month = $date->format('n');
  540. $day = $date->format('j');
  541. $year_day = $date->format('z') + 1;
  542. $year_day_neg = -366 + $year_day;
  543. $leap_year = $date->format('L');
  544. if($leap_year == 1)
  545. {
  546. $year_day_neg = -367 + $year_day;
  547. }
  548. // this is the nth occurence of the date
  549. $occur = ceil($day / 7);
  550. $week = $date->format('W');
  551. $day_of_week = $date->format('l');
  552. $dow_abr = strtoupper(substr($day_of_week, 0, 2));
  553. // set the day of the month + (positive)
  554. $occur = '+' . $occur . $dow_abr;
  555. $occur_zero = '+0' . $dow_abr;
  556. // set the day of the month - (negative)
  557. $total_days = $date->format('t') - $date->format('j');
  558. $occur_neg = '-' . ceil(($total_days + 1)/7) . $dow_abr;
  559. $day_from_end_of_month = $date->format('t') + 1 - $day;
  560. if(in_array($month, $this->bymonth) &&
  561. (in_array($occur, $this->byday) || in_array($occur_zero, $this->byday) || in_array($occur_neg, $this->byday)) &&
  562. in_array($week, $this->byweekno) &&
  563. (in_array($day, $this->bymonthday) || in_array(-$day_from_end_of_month, $this->bymonthday)) &&
  564. (in_array($year_day, $this->byyearday) || in_array($year_day_neg, $this->byyearday)))
  565. {
  566. return true;
  567. }
  568. else
  569. {
  570. return false;
  571. }
  572. }
  573. // return the next valid DateTime object which matches the pattern and follows the rules
  574. public function next()
  575. {
  576. // check the counter is set
  577. if($this->count !== 0)
  578. {
  579. if($this->counter >= $this->count)
  580. {
  581. return false;
  582. }
  583. }
  584. // create initial set of suggested dates
  585. if(count($this->suggestions) === 0)
  586. {
  587. $this->create_suggestions();
  588. }
  589. // loop through the suggested dates
  590. while(count($this->suggestions) > 0)
  591. {
  592. // get the first one on the array
  593. $try_date = array_shift($this->suggestions);
  594. // make sure the date doesn't exceed the max date
  595. if($try_date > $this->end_date)
  596. {
  597. return false;
  598. }
  599. // make sure it falls within the allowed days
  600. if($this->valid_date($try_date) === true)
  601. {
  602. $this->counter++;
  603. return $try_date;
  604. }
  605. else
  606. {
  607. // we might be out of suggested days, so load some more
  608. if(count($this->suggestions) === 0)
  609. {
  610. $this->create_suggestions();
  611. }
  612. }
  613. }
  614. }
  615. }