PageRenderTime 58ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/repeat-expression.php

https://bitbucket.org/andrewbevitt/every-calendar-1
PHP | 1582 lines | 773 code | 184 blank | 625 comment | 200 complexity | 9f4db7d551940f5527389c80788abb58 MD5 | raw file
Possible License(s): GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Every Calendar Scheduler: Repeating Pattern Expression
  4. *
  5. * Modelled of crontab expressions but with a few extra fun parts
  6. * see the examples below for broad coverage of the syntax that
  7. * is supported. It is worth noting that unlike cron which matches
  8. * on DoM/MoY OR DoW this class requires ALL conditions to be met
  9. * for a repeat instance to be flagged.
  10. *
  11. * Also worth noting Sunday is 1 and Saturday is 7.
  12. *
  13. * The Minutes and Hours components have been removed.
  14. *
  15. * A new component Week since Epoch has been added for weekly schedules,
  16. * this is the number of weeks since the epoch (start) date of the event.
  17. *
  18. * TIMEZONES:
  19. * - All manipulations and comparisons are done on whole calendar days
  20. * - All timezones are assumed to be set in the DateTime objects
  21. * - You MUST ensure timezones are consistent between inputs
  22. */
  23. // Make sure we're included from within the plugin
  24. require( ECP1_DIR . '/includes/check-ecp1-defined.php' );
  25. // Example Expressions: (using // to allow */ in comments)
  26. // DoM MoY DoW WsE
  27. // * * 1 * Every Sunday
  28. // 1 */2 * * First day every 2nd month
  29. // 1 3/2 * * First day every 2nd month when month is March
  30. // -1 * * * Last day of the month
  31. // * * 6 1,-1 First after epoch and last Friday before 1 year repeat
  32. // * * 1 1/3 Every 3rd Sunday (1st in group)
  33. // * * 4 2/4 Every 4th Wednesday (2nd in group)
  34. // * * 1/3 * The 3rd Sunday of every month
  35. // * * 2/-1 * Last Monday every month
  36. // * */3 1/-2 * 2nd last Sunday every 3rd month
  37. // -2 */6 4,5/5 * 2nd last day of every 6 month where it is the 5th Wed|Thur of the month
  38. // Some more complicated expression examples:
  39. // DoM MoY DoW WoY
  40. // * * 2/1,-1 * First and last Monday of every month
  41. // 10-20 * 2 * Mondays between 10th and 20th
  42. // * 2,3,4,5,12 2/-1 * Last Monday of Feb|Mar|Apr|May|Dec
  43. // 5-25 3,4,5,9,10,11 6/1,4 * 1st|4th Friday of the month where day is 5th-25th in Autumn/Spring
  44. // * */3 1/-1--3 * Last, 2nd and 3rd Last Sundays of every 3rd month
  45. // 2-8 2,5,9,10/2,5 3,4/1,2 * 1st|2nd Tue|Wed where is 2nd-8th in Feb|May|Sep|Oct and is 2nd or 5th Month cycle since start
  46. // * 1,2,12 2,3 1,2/5,7 Mon|Tue of 1st|2nd weeks in a 5|7 week rolling cycle since epoch in Summer months
  47. // Define some error types
  48. define( 'PARSE_ERROR_DOM', -1 );
  49. define( 'PARSE_ERROR_MOY', -2 );
  50. define( 'PARSE_ERROR_DOW', -3 );
  51. define( 'PARSE_ERROR_WSE', -4 );
  52. define( 'PATTERN_MISMATCH', -9 );
  53. // Define some constants for days of the week
  54. define( 'ECP1_SUNDAY', 1); define( 'PHP_SUNDAY', 0);
  55. define( 'ECP1_MONDAY', 2); define( 'PHP_MONDAY', 1);
  56. define( 'ECP1_TUESDAY', 3); define( 'PHP_TUESDAY', 2);
  57. define( 'ECP1_WEDNESDAY', 4); define( 'PHP_WEDNESDAY', 3);
  58. define( 'ECP1_THURSDAY', 5); define( 'PHP_THURSDAY', 4);
  59. define( 'ECP1_FRIDAY', 6); define( 'PHP_FRIDAY', 5);
  60. define( 'ECP1_SATURDAY', 7); define( 'PHP_SATURDAY', 6);
  61. /**
  62. * RepeatExpression Class
  63. * This class contains static methods for building instances of an expression
  64. * for commonly repeated types, and also allows an expression to be passed to
  65. * the constructor to build more complicated expressions.
  66. */
  67. class EveryCal_RepeatExpression
  68. {
  69. /**
  70. * Known TYPES of expressions and a map to their build functions
  71. * The build functions will be given the event start and end dates
  72. */
  73. static $TYPES = array(
  74. // Monthly repeats (i.e. once a month on the nominated day)
  75. 'MONTHLY' => array(
  76. 'func' => 'BuildMonthly', // function to call
  77. 'desc' => 'Monthly or every X months',
  78. 'params' => array(
  79. 'every' => array( 'required' => false, 'desc' => 'Every X months', 'default' => 1 )
  80. )
  81. ),
  82. // Weekly repeats (i.e. once a week on the nominated day)
  83. 'WEEKLY' => array(
  84. 'func' => 'BuildWeekly', // function to call
  85. 'desc' => 'Weekly of every X weeks',
  86. 'params' => array(
  87. 'every' => array( 'required' => false, 'desc' => 'Every X weeks', 'default' => 1 )
  88. )
  89. ),
  90. // Last Xday (e.g. sunday) of the month
  91. 'LAST_X_OF_MONTH' => array(
  92. 'func' => 'BuildLastXofMonth', // function to call
  93. 'desc' => 'Last X of month' ,
  94. 'params' => array(
  95. 'day' => array( 'required' => true, 'desc' => 'Day (X) of week',
  96. 'choices' => array( 1=>'Sunday', 2=>'Monday', 3=>'Tuesday', 4=>'Wednesday',
  97. 5=>'Thursday', 6=>'Friday', 7=>'Saturday' )
  98. )
  99. )
  100. ),
  101. // First Xday (e.g. Friday) of the month
  102. 'FIRST_X_OF_MONTH' => array(
  103. 'func' => 'BuildFirstXofMonth',
  104. 'desc' => 'First X of month',
  105. 'params' => array(
  106. 'day' => array( 'required' => true, 'desc' => 'Day (X) of week',
  107. 'choices' => array( 1=>'Sunday', 2=>'Monday', 3=>'Tuesday', 4=>'Wednesday',
  108. 5=>'Thursday', 6=>'Friday', 7=>'Saturday' )
  109. )
  110. )
  111. ),
  112. // Once a year
  113. 'YEARLY' => array(
  114. 'func' => 'BuildYearly', // function to call
  115. 'desc' => 'Yearly',
  116. 'params' => null // no parameters
  117. )
  118. );
  119. /**
  120. * Private string for storing the internal expression
  121. */
  122. private $internal_expression = null;
  123. /**
  124. * Private mapping of the repeat sets and period filter sets.
  125. * A null value means ALL so by default this will repeat every day.
  126. */
  127. private $when_sets = array(
  128. 'DoM' => array( 'repeat' => null ),
  129. 'MoY' => array( 'repeat' => null, 'period' => null ),
  130. 'DoW' => array( 'repeat' => null, 'filter' => null ),
  131. 'WsE' => array( 'repeat' => null, 'cycles' => null ),
  132. );
  133. /**
  134. * Build Function (static)
  135. * Constructs an instance of the repeat expression for the given type
  136. * by calling one of the private static methods with the parameters,
  137. * then calling the constructor with the expression string returned.
  138. *
  139. * @param $type One of the keys from EveryCal_RepeatExpression::$TYPES
  140. * @param $start A DateTime object whose date is the start of repeat cycle
  141. * @param $params Keyed array of parameters to pass to the type build function
  142. * @return An instance of EveryCal_RepeatExpression or null on failure
  143. */
  144. public static function Build( $type, $start, $params=array() )
  145. {
  146. if ( ! array_key_exists( $type, self::$TYPES ) )
  147. return null;
  148. $func = self::$TYPES[$type]['func'];
  149. $expr = self::$func( $start, $params );
  150. if ( null == $expr )
  151. return null; // failed to build an expression string
  152. return new EveryCal_RepeatExpression( $expr );
  153. }
  154. /**
  155. * EveryCal_RepeatExpression Constructor
  156. * Returns a new instance which will match times using the given expression
  157. * string. If the expression string can not be parsed then an Exception will
  158. * be thrown (just a generic PHP Exception).
  159. *
  160. * @param $crontab The string expression to build the instance out of
  161. * @return A new instance of EveryCal_RepeatExpression
  162. */
  163. public function __construct( $crontab )
  164. {
  165. $presult = $this->ParseParts( $crontab );
  166. if ( $presult < 0 )
  167. throw new Exception( sprintf( __( 'Failed (code:%s) to parse the crontab %s' ), $presult, $crontab ) );
  168. // Cache the crontab expression
  169. $this->internal_expression = $crontab;
  170. }
  171. /**
  172. * EveryCal_RepeatExpression String Printing
  173. * Returns a string representation of the repeat showing the source expression
  174. * and the component parts that are built from parsing the expression.
  175. *
  176. * @return String representation of the repeat expression.
  177. */
  178. public function __toString()
  179. {
  180. $str = sprintf( "%s\nDoM: {{DOM}}\nMoY: {{MOY}}\nDoW: {{DOW}}\nWsE: {{WSE}}", $this->internal_expression );
  181. // Day of Month
  182. if ( is_array( $this->when_sets['DoM']['repeat'] ) ) {
  183. $str = str_replace( '{{DOM}}', implode( ',', $this->when_sets['DoM']['repeat'] ), $str );
  184. } else if ( $this->when_sets['DoM']['repeat'] == null ) {
  185. $str = str_replace( '{{DOM}}', '*', $str );
  186. } else {
  187. $str = str_replace( '{{DOM}}', $this->when_sets['DoM']['repeat'], $str );
  188. }
  189. // Day of Week
  190. $dow = '?';
  191. if ( is_array( $this->when_sets['DoW']['repeat'] ) )
  192. $dow = implode( ',', $this->when_sets['DoW']['repeat'] );
  193. else if ( $this->when_sets['DoW']['repeat'] == null ) $dow = '*';
  194. else $dow = $this->when_sets['DoW']['repeat'];
  195. if ( $this->when_sets['DoW']['filter'] !== null )
  196. $dow .= '/' . implode( ',', $this->when_sets['DoW']['filter'] );
  197. $str = str_replace( '{{DOW}}', $dow, $str );
  198. // Month of Year
  199. $moy = '?';
  200. if ( is_array( $this->when_sets['MoY']['repeat'] ) )
  201. $moy = implode( ',', $this->when_sets['MoY']['repeat'] );
  202. else if ( $this->when_sets['MoY']['repeat'] == null ) $moy = '*';
  203. else $moy = $this->when_sets['MoY']['repeat'];
  204. if ( $this->when_sets['MoY']['period'] !== null )
  205. $moy .= '/' . implode( ',', $this->when_sets['MoY']['period'] );
  206. $str = str_replace( '{{MOY}}', $moy, $str );
  207. // Weeks since Epoch
  208. $wse = '?';
  209. if ( is_array( $this->when_sets['WsE']['repeat'] ) )
  210. $wse = implode( ',', $this->when_sets['WsE']['repeat'] );
  211. else if ( $this->when_sets['WsE']['repeat'] == null ) $wse = '*';
  212. else $wse = $this->when_sets['WsE']['repeat'];
  213. if ( $this->when_sets['WsE']['cycles'] !== null )
  214. $wse .= '/' . implode( ',', $this->when_sets['WsE']['cycles'] );
  215. $str = str_replace( '{{WSE}}', $wse, $str );
  216. // Finally return the constructed string
  217. return $str;
  218. }
  219. /**
  220. * ParseParts Function (private)
  221. * Parses the given crontab expression into parts that the matching function
  222. * uses and stores them into $this->parts. If the expression is successfully
  223. * parsed returns true otherwise returns false.
  224. *
  225. * @param $crontab The expression to parse
  226. * @return True of False indicating if parsing was successful
  227. */
  228. private function ParseParts( $crontab )
  229. {
  230. // All valid crontab expressions should have 3 tokens that are
  231. // whitespace separated this function parses each token in turn.
  232. // But first check if there are 3 valid tokens.
  233. $strip_chars = ", \t\n\r\0\x0B";
  234. $regexp_crontab = '/^([0-9,\-\*]+)\s+' . // no / allowed
  235. '([0-9,\/\*]+)\s+' . // no - allowed
  236. '([0-9,\/\-\*]+)\s+' . // - and / allowed
  237. '([0-9,\/\-\*]+)$/'; // - and / allowed
  238. $crontab = trim( $crontab );
  239. $matches = array();
  240. if ( ! preg_match( $regexp_crontab, $crontab, $matches ) )
  241. return PATTERN_MISMATCH;
  242. // FIRST TOKEN: Day of Month
  243. if ( ! $this->ParseDayOfMonth( trim( $matches[1], $strip_chars ) ) )
  244. return PARSE_ERROR_DOM;
  245. // SECOND TOKEN: Month of Year
  246. if ( ! $this->ParseMonthOfYear( trim( $matches[2], $strip_chars ) ) )
  247. return PARSE_ERROR_MOY;
  248. // THIRD TOKEN: Day of Week
  249. if ( ! $this->ParseDayOfWeek( trim( $matches[3], $strip_chars ) ) )
  250. return PARSE_ERROR_DOW;
  251. // FOURTH TOKEN: Weeks since Epoch
  252. if ( ! $this->ParseWeekSinceEpoch( trim( $matches[4], $strip_chars ) ) )
  253. return PARSE_ERROR_WSE;
  254. // All good
  255. return 1;
  256. }
  257. /**
  258. * ParseDayOfMonth Function (private)
  259. * Parses the given substring of a crontab expression as the DoM component.
  260. *
  261. * @param $crontab The crontab DoM expression.
  262. * @return True or False if DoM are successfully parsed.
  263. */
  264. private function ParseDayOfMonth( $crontab )
  265. {
  266. // This expression will be one of three types:
  267. if ( preg_match( '/^\*$/', $crontab ) ) { // all DoM
  268. $this->when_sets['DoM']['repeat'] = null;
  269. } else if ( preg_match( '/^\d+\-\d+$/', $crontab ) ) { // range of days in the month
  270. $parts = explode( '-', $crontab ); // will have exactly two
  271. $this->when_sets['DoM']['repeat'] = array();
  272. for ( $i=$parts[0]; $i<=$parts[1]; $i++ )
  273. $this->when_sets['DoM']['repeat'][] = $i;
  274. } else if ( preg_match( '/^([\-]?\d+[, ]?)+$/', $crontab ) ) { // specific days of the month
  275. $this->when_sets['DoM']['repeat'] = explode( ',', $crontab );
  276. } else { return false; } // unknown DoM expression
  277. // Check the values given make sense
  278. if ( is_array( $this->when_sets['DoM']['repeat'] ) ) {
  279. foreach ( $this->when_sets['DoM']['repeat'] as $v ) {
  280. if ( ! is_numeric( $v ) || $v < -31 || $v > 31 )
  281. return false;
  282. }
  283. }
  284. // As good as can be
  285. return true;
  286. }
  287. /**
  288. * ParseMonthOfYear Function (private)
  289. * Parses the given substring of a crontab expression as the MoY component.
  290. *
  291. * @param $crontab The crontab MoY expression.
  292. * @return True or False if MoY are successfully parsed.
  293. */
  294. private function ParseMonthOfYear( $crontab )
  295. {
  296. // This expression will either have a period set or will not
  297. // for MoY expressions the period set effectively says only
  298. // include every X occurance (where X is in period set) of
  299. // the items in the repeat set.
  300. $slashpos = strpos( $crontab, '/' );
  301. $repeat_string = $slashpos !== false ? substr( $crontab, 0, $slashpos ) : $crontab;
  302. $period_string = $slashpos !== false ? substr( $crontab, $slashpos+1, strlen( $crontab ) - $slashpos - 1 ) : '';
  303. // Repeat string will be either a *, single number or comma separated
  304. if ( preg_match( '/^\*$/', $repeat_string ) ) { // every month by period
  305. $this->when_sets['MoY']['repeat'] = null;
  306. } else if ( preg_match( '/^(\d+[, ]?)+?$/', $repeat_string ) ) { // the listed set of months
  307. $this->when_sets['MoY']['repeat'] = explode( ',', $repeat_string );
  308. } else { return false; } // couldn't parse month component
  309. // Is there a period string to parse?
  310. if ( $slashpos !== false ) {
  311. // Period string will be a single or comma separated set
  312. if ( preg_match( '/^(\d+[, ]?)+$/', $period_string ) ) { // specific intervals of months
  313. $this->when_sets['MoY']['period'] = explode( ',', $period_string );
  314. } else { return false; } // couldn't parse
  315. } else { // $slashpos === false meaning all occurances of the repeating months
  316. $this->when_sets['MoY']['period'] = null;
  317. }
  318. // Validate we have sensible month values
  319. if ( is_array( $this->when_sets['MoY']['repeat'] ) ) {
  320. foreach ( $this->when_sets['MoY']['repeat'] as $v )
  321. if ( ! is_numeric( $v ) || $v < 1 || $v > 12 )
  322. return false; // invalid month
  323. }
  324. // Validate we have sensible period values
  325. if ( is_array( $this->when_sets['MoY']['period'] ) ) {
  326. foreach ( $this->when_sets['MoY']['period'] as $v )
  327. if ( ! is_numeric( $v ) || $v < 1 || $v > 12 )
  328. return false; // invalid month
  329. }
  330. // All good
  331. return true;
  332. }
  333. /**
  334. * ParseDayOfWeek Function (private)
  335. * Parses the given substring of a crontab expression as the DoW component.
  336. *
  337. * @param $crontab The crontab DoW expression.
  338. * @return True or False if DoW are successfully parsed.
  339. */
  340. private function ParseDayOfWeek( $crontab )
  341. {
  342. // This expression is the effectively the same as MoY except
  343. // it can also have negative values for the day and filter
  344. $slashpos = strpos( $crontab, '/' );
  345. $day_string = $slashpos !== false ? substr( $crontab, 0, $slashpos ) : $crontab;
  346. $flr_string = $slashpos !== false ? substr( $crontab, $slashpos+1, strlen( $crontab ) - $slashpos - 1 ) : '';
  347. // The day string will be either a *, single number or comma separated
  348. if ( preg_match( '/^\*$/', $day_string ) ) { // any day of the week by filter
  349. $this->when_sets['DoW']['repeat'] = null;
  350. } else if ( preg_match( '/^(\d+[, ]?)+?$/', $day_string ) ) { // listed day(s)
  351. $this->when_sets['DoW']['repeat'] = explode( ',', $day_string );
  352. } else { return false; } // couldn't parse day component
  353. // If there is a slash then parse the filter
  354. if ( $slashpos !== false ) {
  355. // Filter string will be single number, comma separated or a range
  356. $matches = array();
  357. if ( preg_match( '/^([\-]?\d+)\-([\-]?\d+)$/', $flr_string, $matches ) ) {
  358. $this->when_sets['DoW']['filter'] = array();
  359. $min = $matches[1] < $matches[2] ? $matches[1] : $matches[2];
  360. $max = $matches[1] >= $matches[2] ? $matches[1] : $matches[2];
  361. for ( $i=$min; $i<=$max; $i++ )
  362. $this->when_sets['DoW']['filter'][] = $i;
  363. } else if ( preg_match( '/^([\-]?\d+[, ]?)+$/', $flr_string ) ) { // specific filters
  364. $this->when_sets['DoW']['filter'] = explode( ',', $flr_string );
  365. } else { return false; } // couldn't parse the filter
  366. }
  367. // Do some sanity checking on the days specified
  368. if ( is_array( $this->when_sets['DoW']['repeat'] ) ) {
  369. foreach( $this->when_sets['DoW']['repeat'] as $v )
  370. if ( ! is_numeric( $v ) || abs( $v ) > 7 || abs( $v ) < 1 )
  371. return false;
  372. }
  373. // And also do sanity checking on the filters
  374. if ( is_array( $this->when_sets['DoW']['filter'] ) ) {
  375. foreach( $this->when_sets['DoW']['filter'] as $v )
  376. if ( ! is_numeric( $v ) || $v == 0 || abs( $v ) > 5 ) // can't be 0 and never more than 5 of a day in a month
  377. return false;
  378. }
  379. // All good
  380. return true;
  381. }
  382. /**
  383. * ParseWeekSinceEpoch (private)
  384. * Parses the weeks since epoch component of the cron expression.
  385. *
  386. * @param $crontab The cron expression component for WsE.
  387. * @return True of false if the expression was parsed.
  388. */
  389. private function ParseWeekSinceEpoch( $crontab )
  390. {
  391. // If there is a slash get it's position and divide the string
  392. $slashpos = strpos( $crontab, '/' );
  393. $period_string = $slashpos !== false ? substr( $crontab, 0, $slashpos ) : $crontab;
  394. $cycles_string = $slashpos !== false ? substr( $crontab, $slashpos+1, strlen( $crontab ) - $slashpos - 1 ) : '';
  395. // The period will be either a * or single or comma separated numbers
  396. if ( preg_match( '/^\*$/', $period_string ) ) {
  397. $this->when_sets['WsE']['repeat'] = null;
  398. } else if ( preg_match( '/^([\-]?\d+[, ]?)+$/', $period_string ) ) {
  399. $this->when_sets['WsE']['repeat'] = explode( ',', $period_string );
  400. } else { return false; } // couldn't parse the period of weeks
  401. // If there is a cycle it will be single or comma separated
  402. if ( $slashpos !== false ) {
  403. if ( preg_match( '/^(\d+[, ]?)+$/', $cycles_string ) ) { // no negatives allowed
  404. $this->when_sets['WsE']['cycles'] = explode( ',', $cycles_string );
  405. } else { return false; } // couldn't parse the cycles
  406. }
  407. // Do some error checking on the cycles component
  408. $minCycles = 0;
  409. if ( is_array( $this->when_sets['WsE']['cycles'] ) ) {
  410. foreach( $this->when_sets['WsE']['cycles'] as $v ) {
  411. if ( $v > $minCycles ) $minCycles = $v;
  412. if ( ! is_numeric( $v ) || $v < 1 || $v > 53 )
  413. return false;
  414. }
  415. }
  416. // Do some error checking on the repeat component
  417. if ( is_array( $this->when_sets['WsE']['repeat'] ) ) {
  418. foreach( $this->when_sets['WsE']['repeat'] as $v )
  419. if ( ! is_numeric( $v ) || ( abs( $v ) > $minCycles && $minCycles > 0 ) )
  420. return false;
  421. }
  422. // All good
  423. return true;
  424. }
  425. /**
  426. * GetRepeatsBetween Function
  427. * Returns an array of all the event repeat start dates that this expression
  428. * matches which are between the given start and end parameter DateTimes.
  429. *
  430. * This is one of the most complicated and important parts of the scheduler.
  431. * Any improvements are welcome but please make sure you TEST all changes.
  432. *
  433. * Algorithm:
  434. * 1) Construct EveryCal_RE_DateRange for the start and end
  435. * 2) If specific days of a month are requested disable any that don't match.
  436. * 3) If specific months are requested drop all days not in those months.
  437. * 4) If specific days of the week are requsted disable any that don't match.
  438. * 5) If a monthly cycle exists then apply it.
  439. * 6) If a weekday filter exists then apply it.
  440. * 7) If a weeks since epoch cycle is given then apply it.
  441. * Where only repeats are given and no cycle, the cycle is the whole year.
  442. *
  443. * @param $epoch DateTime object representing the event start date (epoch).
  444. * @param $start DateTime object starting the range to look for matches in.
  445. * @param $end DateTime objects ending the range to look for matches in.
  446. * @return Array of DateTime objects representing the date each valid
  447. * repeat of the event will start. The array can be empty if there
  448. * are no events in the range or will be null if an error occurs.
  449. */
  450. public function GetRepeatsBetween( $epoch, $start, $end )
  451. {
  452. // First make sure the input parameters make sense
  453. if ( ! ( $epoch instanceof DateTime && $start instanceof DateTime && $end instanceof DateTime ) || $start > $end )
  454. return null;
  455. // Repeats can't start before the epoch so copy if trying to
  456. if ( $epoch > $start )
  457. $start = $epoch;
  458. // 1 - Construct EveryCal_RE_DateRange
  459. $ranger = new EveryCal_RE_DateRange( $epoch, $start, $end );
  460. // 2 - Remove non-matching days of the month
  461. if ( is_array( $this->when_sets['DoM']['repeat'] ) )
  462. $ranger->FilterDaysNotIn( $this->when_sets['DoM']['repeat'] );
  463. // 3 - Remove days not in matching months
  464. if ( is_array( $this->when_sets['MoY']['repeat'] ) )
  465. $ranger->FilterMonthsNotIn( $this->when_sets['MoY']['repeat'] );
  466. // 4 - Remove non-matching days of the week
  467. if ( is_array( $this->when_sets['DoW']['repeat'] ) )
  468. $ranger->FilterWeekdaysNotIn( $this->when_sets['DoW']['repeat'] );
  469. // 5 - Apply month cycles
  470. if ( is_array( $this->when_sets['MoY']['period'] ) )
  471. $ranger->ApplyMonthlyCycle( $this->when_sets['MoY']['period'] );
  472. // 6 - Apply weekday filter
  473. if ( is_array( $this->when_sets['DoW']['filter'] ) )
  474. $ranger->ApplyWeekdayFilter( $this->when_sets['DoW']['filter'] );
  475. // 7 - Apply weeks since epoch cycles
  476. if ( is_array( $this->when_sets['WsE']['repeat'] ) )
  477. $ranger->ApplyWeeklyCycle( $this->when_sets['WsE']['repeat'],
  478. $this->when_sets['WsE']['cycles'] );
  479. // DEBUGGING ONLY: MUST BE REMOVED IN PRODUCTION
  480. //return $ranger;
  481. // Finally return the computed array of objects
  482. return $ranger->GetDates();
  483. }
  484. /**
  485. * GetExpression Function
  486. * Returns the crontab expression that represents this instance.
  487. *
  488. * @return The crontab expression string for this instance.
  489. */
  490. public function GetExpression()
  491. {
  492. return $this->internal_expression;
  493. }
  494. /**
  495. * BuildMonthly (private)
  496. * Builds an expression that represents an event repeating X months by month
  497. * on a given day of the month. The most simple examples of this would be
  498. * 1) On the 1st of every month; or
  499. * 2) On the 10th of every 3rd month.
  500. *
  501. * The $params array should contain the following keys:
  502. * every => (optional) The frequency of months 1=every, 2=every 2nd, and so on..
  503. *
  504. * @param $start The start date of the event repeat cycle
  505. * @param $params The keyed parameter array described above
  506. * @return String representation of the monthly repeating event
  507. */
  508. private static function BuildMonthly( $start, $params=array() )
  509. {
  510. // Extract the DoM from the start date
  511. $dom = $start->format( 'j' );
  512. // Every Y months on the same X day is: X */Y * *
  513. $freq = array_key_exists( 'every', $params ) && is_numeric( $params['every'] ) ? $params['every'] : 1;
  514. if ( 1 == $freq )
  515. return sprintf( '%s * * *', $dom );
  516. return sprintf( '%s */%s * *', $dom, $freq );
  517. }
  518. /**
  519. * BuildWeekly (private)
  520. * Builds an expression that represents an event repeating X weeks by week on
  521. * the same day every week. The typical example here is a weekly meeting.
  522. *
  523. * Parameters:
  524. * every - Optional frequency 1=weekly, 2=fortnightly, etc...
  525. *
  526. * @param $start The start date of the event repeat cycle
  527. * @param $params The key parameter array described in parameters above
  528. * @return String representation of the weekly repeating event
  529. */
  530. private static function BuildWeekly( $start, $params=array() )
  531. {
  532. // Extract the DoW from the start date
  533. $dow = $start->format( 'w' ) + 1; // 1=sunday, 6=saturday
  534. // Every Y weeks on the same X day is: * * X 1/Y
  535. // if weekly Y is not needed: * * X *
  536. // NOTE: * * X/1 -> only the first X of month
  537. $freq = array_key_exists( 'every', $params ) && is_numeric( $params['every'] ) ? $params['every'] : 1;
  538. if ( 1 == $freq )
  539. return sprintf( '* * %s *', $dow );
  540. return sprintf( '* * %s 1/%s', $dow, $freq );
  541. }
  542. /**
  543. * BuildLastXofMonth (private)
  544. * Builds an expression that represents an event repeating on the last Xday
  545. * of every month (e.g. the last Sunday of every month).
  546. *
  547. * Parameters:
  548. * day - Required day of the week (1=Sunday, 7=Saturday)
  549. *
  550. * @params $start The start date of the event repeat cycle
  551. * @params $params The key parameter array described in parameters above
  552. * @return String representation of the monthly event
  553. */
  554. private static function BuildLastXofMonth( $start, $params=array() )
  555. {
  556. // This has nothing to do with the first day the event runs
  557. // it is purely based on the day parameter
  558. if ( ! array_key_exists( 'day', $params ) || ! is_numeric( $params['day'] ) || 1 > $params['day'] || 7 < $params['day'] )
  559. return null; // invalid day required parameter
  560. // This is expressed as: * * X/-1 *
  561. // The converse first X of month is: * * X/1 *
  562. // NOTE: These are different to * * X * which is weekly X
  563. return sprintf( '* * %s/-1 *', $params['day'] );
  564. }
  565. /**
  566. * BuildFirstXofMonth (private)
  567. * Builds an expression that represents an event repeating on the first Xday
  568. * of every month (e.g. the first Friday of every month) - the reverse of above.
  569. *
  570. * Parameters:
  571. * day - Required day of the week (1=Sunday, 7=Saturday)
  572. *
  573. * @param $start The start date of the event repeat cycle
  574. * @param $params The key parameter array described in parameters above
  575. * @return String representation of the monthly event
  576. */
  577. private static function BuildFirstXofMonth( $start, $params=array() )
  578. {
  579. // Identical to BuildLastXofMonth
  580. if ( ! array_key_exists( 'day', $params ) || ! is_numeric( $params['day'] ) || 1 > $params['day'] || 7 < $params['day'] )
  581. return null;
  582. return sprintf( '* * %s/1 *', $params['day'] );
  583. }
  584. /**
  585. * BuildYearly (private)
  586. * Builds an expression that represents an event repeating on the same day
  587. * every year (e.g. 26/JAN is Australia Day). Due to long term caching I
  588. * have NOT allowed an X factor here to have every 2nd/3rd/etc.. year.
  589. *
  590. * @param $start The start date of the event repeat cycle
  591. * @param $params Will be an empty array or null because none allowed
  592. * @return String representation of the yearly event
  593. */
  594. private static function BuildYearly( $start, $params=null )
  595. {
  596. // Get the day and month from the start date
  597. $day = $start->format( 'j' );
  598. $mon = $start->format( 'n' );
  599. // Expressed as: X Y *
  600. // Meaning only on X in month Y no matter what DoW it is
  601. return sprintf( '%s %s * *', $day, $mon );
  602. }
  603. }
  604. /* ====================================================================
  605. * The following classes encapsulate days, weeks, months and years that
  606. * exist between two points in time. At it's core the days simply have
  607. * a flag indicating if the day is still active; this flag is disabled
  608. * by the RepeatExpression class above when building repeat dates.
  609. * ==================================================================== */
  610. /**
  611. * A single Day object that can be filtered.
  612. */
  613. class EveryCal_RE_Day
  614. {
  615. private $available = true;
  616. private $date = null;
  617. /**
  618. * Constructor fo the Day object.
  619. *
  620. * @param $d The date of this day.
  621. */
  622. public function __construct( $d )
  623. {
  624. $this->date = clone $d;
  625. $this->available = true;
  626. }
  627. /**
  628. * Returns the available status.
  629. *
  630. * @return True if the day is still available.
  631. */
  632. public function IsEnabled() { return $this->available; }
  633. /**
  634. * Disables the day
  635. */
  636. public function Disable() { $this->available = false; }
  637. /**
  638. * Returns the date of this day.
  639. * @return The date of this day.
  640. */
  641. public function GetDate() { return $this->date; }
  642. }
  643. /**
  644. * Week encapsulates a standard week of day objects for filtering.
  645. * A week may not have all 7 days if it is on the border of a year.
  646. */
  647. class EveryCal_RE_Week
  648. {
  649. private $epoch_offset = null;
  650. private $day_objs = null;
  651. private $day0 = null;
  652. /**
  653. * Constructor for the Week object.
  654. *
  655. * @param $start The first day of this week
  656. */
  657. public function __construct( $start, $offset )
  658. {
  659. $this->day0 = clone $start;
  660. $this->day_objs = array();
  661. $this->epoch_offset = $offset;
  662. }
  663. /**
  664. * Returns the epoch offset (number of weeks since event epoch)
  665. *
  666. * @return Offset from event epoch as number of weeks.
  667. */
  668. public function GetEpochOffset() { return $this->epoch_offset; }
  669. /**
  670. * Add a given day at the given index in the week.
  671. *
  672. * @param $i The index ECP1_SUNDAY - ECP1_SATURDAY to store in
  673. * @param $d Reference to the Day object
  674. */
  675. public function AddDay( $i, &$d ) { $this->day_objs[$i] = $d; }
  676. /**
  677. * Returns an array of the month day numbers for days in this week.
  678. *
  679. * @return Array of day numbers in this week.
  680. */
  681. public function DaysOfMonth()
  682. {
  683. $days = array();
  684. foreach( array_keys( $this->day_objs ) as $day )
  685. $days[] = $this->day_objs[$day]->GetDate()->format( 'j' );
  686. return $days;
  687. }
  688. /**
  689. * Disables any days in the week which are not in the keep parameter.
  690. * This function expects $keep to be all positive values.
  691. *
  692. * @param $keep The array of weekdays to not disable.
  693. */
  694. public function FilterDaysTo( $keep )
  695. {
  696. // We DON'T need to worry about pos/neg values here
  697. foreach( array_keys( $this->day_objs ) as $day ) {
  698. $realday = $this->day_objs[$day]->GetDate()->format( 'w' ) + 1;
  699. if ( ! ( in_array( $realday, $keep ) ) )
  700. $this->day_objs[$day]->Disable();
  701. }
  702. }
  703. /**
  704. * Disables all days in the week.
  705. */
  706. public function FilterAll()
  707. {
  708. foreach( array_keys( $this->day_objs ) as $k ) {
  709. $this->day_objs[$k]->Disable();
  710. }
  711. }
  712. }
  713. /**
  714. * Month encapsulates a standard calendar month of days.
  715. * The month may not have all the days in it's array if the
  716. * month is on the border of a year.
  717. */
  718. class EveryCal_RE_Month
  719. {
  720. private $epoch_offset = null;
  721. private $day_objs = null;
  722. private $day0 = null;
  723. /**
  724. * Constructor for the Month object.
  725. *
  726. * @param $start The first day of this month
  727. */
  728. public function __construct( $start, $offset )
  729. {
  730. $this->day0 = clone $start;
  731. $this->day_objs = array();
  732. $this->epoch_offset = $offset;
  733. }
  734. /**
  735. * Returns the numirical representation of this month 1-12.
  736. *
  737. * @return The numerical representation of this month.
  738. */
  739. public function GetMonth() { return $this->day0->format( 'n' ); }
  740. /**
  741. * Returns the epoch offset for this month.
  742. *
  743. * @return Epoch offset for the month.
  744. */
  745. public function GetEpochOffset() { return $this->epoch_offset; }
  746. /**
  747. * Add a given day at the given index in the month.
  748. *
  749. * @param $i The index 1 - days in month to store in
  750. * @param $d Reference to the Day object
  751. */
  752. public function AddDay( $i, &$d ) { $this->day_objs[$i] = $d; }
  753. /**
  754. * Converts the month into an array / vector of Week objects.
  755. * The weeks align to the boundary of the month either:
  756. * a) The first week always starts on the 1st; or
  757. * b) The last week always ends on the last day.
  758. *
  759. * Pass the reverse parameter as false for start of month alignment,
  760. * which is the default if none given, or true for end of month.
  761. *
  762. * UTC Timezone: This function uses UTC timezones to get the day of
  763. * week for the 1st and last days of the month; it does NOT use the
  764. * timestamp within these DateTime objects for anything comparissons.
  765. * The Week objects in the returned vector will have their day0 in
  766. * UTC timezone too but this is just the first "date" of the week,
  767. * it does not affect the 0th Day object in the week.
  768. *
  769. * @param $reverse Should the weeks aligned to 1st or last day.
  770. * @return Array of week objects holding this months days.
  771. */
  772. public function ToWeeksVector( $reverse=false )
  773. {
  774. $weeks = array();
  775. // Note that day0 is the first date of a real day in month not necessarily 1st of month
  776. // so we may have weeks in the vector that we don't need to copy days into. As noted
  777. // above the 1st of the month is the start of the first week this is NOT aligned to
  778. // weeks in the year.
  779. $wcounter = 0;
  780. $utc = new DateTimeZone( 'UTC' ); // see above
  781. $tplym = $this->day0->format( 'Y-m-' );
  782. $days_in_month = $this->day0->format( 't' );
  783. $first_real_day = $this->day0->format( 'j' );
  784. $first = new DateTime( $tplym . '1', $utc );
  785. $weekday_counter = ECP1_SUNDAY; // NOTE: Not necessarily a SUNDAY just first of week
  786. if ( $reverse ) {
  787. // Move the first day of the first week to force alignment to the
  788. // end of the month; effectively we just shorten the first week
  789. $last = new DateTime( $tplym . $days_in_month, $utc );
  790. $weekday_counter = $first->format( 'w' ) - $last->format( 'w' );
  791. if ( $weekday_counter < ECP1_SUNDAY )
  792. $weekday_counter += ECP1_SATURDAY;
  793. }
  794. // Loop over all the days and add them to weeks as necessary
  795. for ( $i=1; $i<=$days_in_month; $i++ ) {
  796. if ( ! array_key_exists( $wcounter, $weeks ) )
  797. $weeks[$wcounter] = new EveryCal_RE_Week( new DateTime( $tplym . $i, $utc ), -1 );
  798. if ( $i >= $first_real_day && array_key_exists( $i, $this->day_objs ) ) // could be short either end
  799. $weeks[$wcounter]->AddDay( $weekday_counter, $this->day_objs[$i] );
  800. if ( $weekday_counter == ECP1_SATURDAY ) {
  801. $weekday_counter = ECP1_SUNDAY;
  802. $wcounter += 1;
  803. } else {
  804. $weekday_counter += 1;
  805. }
  806. }
  807. // Return the computed vector
  808. return $weeks;
  809. }
  810. /**
  811. * Disables days in this month that are not in the list.
  812. *
  813. * @param $keep Array of days to keep in this month.
  814. */
  815. public function FilterDaysTo( $keep )
  816. {
  817. // How many days are there in this month?
  818. $days = $this->day0->format( 't' );
  819. // Loop over the days in this month
  820. foreach( array_keys( $this->day_objs ) as $day ) {
  821. // If the day or it's negative is not in the array disable it
  822. $realday = $this->day_objs[$day]->GetDate()->format( 'j' );
  823. if ( ! ( in_array( $realday, $keep ) || in_array( $realday - $days - 1, $keep ) ) )
  824. $this->day_objs[$day]->Disable();
  825. }
  826. }
  827. /**
  828. * Disable all days in the month.
  829. */
  830. public function FilterAll()
  831. {
  832. foreach( array_keys( $this->day_objs ) as $day ) {
  833. $this->day_objs[$day]->Disable();
  834. }
  835. }
  836. }
  837. /**
  838. * Year encapsulates a standard calendar year from the epoch yearly
  839. * repeat point forward to the day before that point in the following
  840. * year. THIS IS NOT ALIGNED AS 1JAN - 31DEC DO NOT TREAT IT AS SUCH.
  841. */
  842. class EveryCal_RE_Year
  843. {
  844. private $years_since_epoch = null;
  845. private $month_objs = null;
  846. private $week_objs = null;
  847. private $day_objs = null;
  848. private $day0 = null;
  849. private $day365 = null; // not necessarily 365 maybe 366
  850. /**
  851. * Static method that returns the offset month from epoch for a given start date.
  852. * The epoch is in month 0 (aka offset 0), the next calendar month is offset 1,
  853. * and so on. We exploit some algebra here:
  854. *
  855. * offset = 12*(Y[s])-Y[e]) + M[s] - M[e]
  856. *
  857. * @param $start The date we want the offset calculated for.
  858. * @param $epoch The date we are calculating the offset from.
  859. * @return Number of months the epoch is offset.
  860. */
  861. public static function MonthEpochOffset( $start, $epoch )
  862. {
  863. $ys = $start->format( 'Y' );
  864. $ms = $start->format( 'n' );
  865. $ye = $epoch->format( 'Y' );
  866. $me = $epoch->format( 'n' );
  867. return 12 * ( $ys - $ye ) + $ms - $me;
  868. }
  869. /**
  870. * Static method that returns the offset week from epoch for a given start date.
  871. * As above the epoch is in week 0 (aka offset 0), the next Sunday starts week
  872. * offset 1, and so on. This is a little harder because we effectively want to
  873. * count the number of Sundays between the two dates. LxS means last Sunday.
  874. *
  875. * offset = roundDown( ( TS[s] - TS[LxS[e]] ) / 86400 )
  876. *
  877. * @param $start The date we want the offset calculated for.
  878. * @param $epoch The date we are calculating the offset from.
  879. * @return Number of weeks the epoch is offset.
  880. */
  881. public static function WeekEpochOffset( $start, $epoch )
  882. {
  883. $tss = $start->format( 'U' ); // php 5.2 compatible
  884. $nxs = clone $epoch;
  885. $dow = $nxs->format( 'w' );
  886. if ( $dow > 0 ) // epoch is not a sunday
  887. $nxs->modify( '-' . $dow . ' day' );
  888. $tse = $nxs->format( 'U' );
  889. return (int) floor( ( $tss - $tse ) / 604800 );
  890. }
  891. /**
  892. * Create an instance of the Year object.
  893. *
  894. * @param $count The year index since epoch
  895. * @param $start_date The first day of the year
  896. */
  897. public function __construct( $count, $start_date, $epoch_date )
  898. {
  899. $this->years_since_epoch = $count;
  900. $this->day0 = clone $start_date;
  901. // Calculate the month and week offsets from epoch date
  902. $mepoch_offset = self::MonthEpochOffset( $start_date, $epoch_date );
  903. $wepoch_offset = self::WeekEpochOffset( $start_date, $epoch_date );
  904. // Instantiate the days, weeks and months
  905. $this->day_objs = array();
  906. $this->week_objs = array();
  907. $this->month_objs = array();
  908. // Compute the days for this year, then assign them to weeks
  909. // and months by reference (see AddDay function in classes).
  910. // The week boundaries are sunday to saturday 1 to 7.
  911. // Month boundaries match calendar months.
  912. $mcounter = $wcounter = $dcounter = 0;
  913. $tsdate = clone $start_date; $tfdate = clone $start_date; $tfdate->modify( '+1 year' );
  914. $this->day365 = clone $tfdate; $this->day365->modify( '-1 day' );
  915. // Construct the first week that contains this start point
  916. $wsdate = clone $tsdate; $wsoffset = $wsdate->format( 'w' );
  917. if ( $wsoffset != PHP_SUNDAY )
  918. $wsdate->modify( '-' . $wsoffset . ' day' );
  919. $dwcounter = $wsoffset + 1; // convert to ECP1_XXXX
  920. $tempweek = new EveryCal_RE_Week( $wsdate, $wcounter + $wepoch_offset );
  921. // Construct the first month that contains this start point
  922. $msdate = clone $tsdate; $msoffset = $msdate->format( 'j' );
  923. if ( $msoffset != 1 ) // not 1st of month: 2nd go back 1, 3rd go back 2, etc...
  924. $msdate->modify( '-' . ( $msoffset - 1 ) . ' day' );
  925. $dmcounter = $msoffset; // 1-indexed like calendar
  926. $tempmonth = new EveryCal_RE_Month( $msdate, $mcounter + $mepoch_offset );
  927. //printf( "DEBUG: TSDATE=%s | TFDATE=%s\n", $tsdate->format( 'Y-m-d' ), $tfdate->format( 'Y-m-d' ) );
  928. //printf( "DEBUG: WEEK OFFSET=%s | MONTH OFFSET=%s\n", $wepoch_offset, $mepoch_offset );
  929. // Loop over all the dates in the year and assign to weeks and months
  930. while ( $tsdate < $tfdate ) {
  931. $this->day_objs[$dcounter] = new EveryCal_RE_Day( $tsdate );
  932. $tempweek->AddDay( $dwcounter, $this->day_objs[$dcounter] );
  933. $tempmonth->AddDay( $dmcounter, $this->day_objs[$dcounter] );
  934. $dcounter += 1; $dwcounter += 1; $dmcounter += 1;
  935. $tsdate->modify( '+1 day' );
  936. // If we're on a Sunday (week has looped) add the temp week and make a new one
  937. if ( PHP_SUNDAY == $tsdate->format( 'w' ) ) {
  938. $this->week_objs[$wcounter] = $tempweek;
  939. $wcounter += 1;
  940. $dwcounter = ECP1_SUNDAY; // reset to sunday and start new week
  941. $tempweek = new EveryCal_RE_Week( $tsdate, $wcounter + $wepoch_offset );
  942. }
  943. // If this is the first day of the month (have looped) add the temp month and make a new one
  944. if ( 1 == $tsdate->format( 'j' ) ) {
  945. $this->month_objs[$mcounter] = $tempmonth;
  946. $mcounter += 1;
  947. $dmcounter = 1; // reset to the 1st and start a new month
  948. $tempmonth = new EveryCal_RE_Month( $tsdate, $mcounter + $mepoch_offset );
  949. }
  950. }
  951. // Unless we just added the last week and month we need to add them now
  952. if ( PHP_SATURDAY != $tsdate->format( 'w' ) )
  953. $this->week_objs[$wcounter] = $tempweek;
  954. if ( 1 != $tsdate->format( 'j' ) )
  955. $this->month_objs[$mcounter] = $tempmonth;
  956. }
  957. /**
  958. * Returns an array of Day objects that are still enabled.
  959. *
  960. * @return Array of Day objects that are enabled.
  961. */
  962. public function GetDates()
  963. {
  964. $set = array();
  965. foreach( array_keys( $this->day_objs ) as $k ) {
  966. if ( $this->day_objs[$k]->IsEnabled() )
  967. $set[] = $this->day_objs[$k];
  968. }
  969. return $set;
  970. }
  971. /**
  972. * Returns a string representation of enabled days in the year
  973. *
  974. * @return String representation of enabled days in the year
  975. */
  976. public function __toString()
  977. {
  978. $cyear = $cmonth = 'NONE';
  979. $s = sprintf( "\nYEAR: %s", $this->years_since_epoch );
  980. foreach( $this->GetDates() as $d ) {
  981. if ( $cyear != $d->GetDate()->format( 'Y' ) ) {
  982. $cyear = $d->GetDate()->format( 'Y' );
  983. $cmonth = $d->GetDate()->format( 'M' );
  984. $s .= sprintf( "\n%s:", $cyear );
  985. $s .= sprintf( "\n %s: ", $cmonth );
  986. }
  987. if ( $cmonth != $d->GetDate()->format( 'M' ) ) {
  988. $cmonth = $d->GetDate()->format( 'M' );
  989. $s .= sprintf( "\n %s: ", $cmonth );
  990. }
  991. $s .= sprintf( "%s, ", $d->GetDate()->format( 'j' ) );
  992. }
  993. return $s;
  994. }
  995. /**
  996. * FilterBefore
  997. * Marks all days in this year as disabled if they are before the given date.
  998. *
  999. * @param $d The date to use when marking.
  1000. */
  1001. public function FilterBefore( $d )
  1002. {
  1003. if ( $this->day0 >= $d || count( $this->day_objs ) == 0 )
  1004. return; // no days to mark
  1005. // Counter of days starts at zero for the year and dates are in order
  1006. $counter = 0;
  1007. $max = count( $this->day_objs ) - 1;
  1008. $dtrack = $this->day_objs[$counter]->GetDate();
  1009. while ( $dtrack < $d ) {
  1010. $this->day_objs[$counter]->Disable();
  1011. $counter += 1;
  1012. if ( $counter > $max )
  1013. break;
  1014. $dtrack = $this->day_objs[$counter]->GetDate();
  1015. }
  1016. }
  1017. /**
  1018. * FilterAfter
  1019. * Marks all days in this year as disabled if they are after the given date.
  1020. *
  1021. * @param $d The date to use when marking.
  1022. */
  1023. public function FilterAfter( $d )
  1024. {
  1025. if ( $this->day365 <= $d || count( $this->day_objs ) == 0 )
  1026. return; // no days to mark
  1027. // Counting backwards is easy enough as dates are in order
  1028. $counter = count( $this->day_objs ) - 1;
  1029. $dtrack = $this->day_objs[$counter]->GetDate();
  1030. while ( $dtrack > $d ) {
  1031. $this->day_objs[$counter]->Disable();
  1032. $counter -= 1;
  1033. if ( $counter < 0 )
  1034. break;
  1035. $dtrack = $this->day_objs[$counter]->GetDate();
  1036. }
  1037. }
  1038. /**
  1039. * Filters all the months in the year to only include days given.
  1040. *
  1041. * @param $keep The day numbers to keep.
  1042. */
  1043. public function FilterMonthDaysTo( $keep )
  1044. {
  1045. foreach( array_keys( $this->month_objs ) as $k ) {
  1046. $this->month_objs[$k]->FilterDaysTo( $keep );
  1047. }
  1048. }
  1049. /**
  1050. * Filters weekdays to the given set of days.
  1051. *
  1052. * @param $keep The weekday numbers ECP1_XXX to keep.
  1053. */
  1054. public function FilterWeekdaysTo( $keep )
  1055. {
  1056. foreach( array_keys( $this->week_objs ) as $k ) {
  1057. $this->week_objs[$k]->FilterDaysTo( $keep );
  1058. }
  1059. }
  1060. /**
  1061. * Filter all months and only keep days in months in the given array.
  1062. *
  1063. * @param $keep The month index (1-12) to keep days in.
  1064. */
  1065. public function FilterMonthsTo( $keep )
  1066. {
  1067. foreach( array_keys( $this->month_objs ) as $k ) {
  1068. $mnum = $this->month_objs[$k]->GetMonth();
  1069. if ( ! ( in_array( $mnum, $keep ) ) )
  1070. $this->month_objs[$k]->FilterAll();
  1071. }
  1072. }
  1073. /**
  1074. * Exclude all days in months that are not in any of the given cycles.
  1075. *
  1076. * @param $cycles The array of cycles from epoch to keep.
  1077. */
  1078. public function ApplyMonthlyCycles( $cycles )
  1079. {
  1080. foreach( array_keys( $this->month_objs ) as $k ) {
  1081. $safe = false;
  1082. $moffset = $this->month_objs[$k]->GetEpochOffset();
  1083. foreach( $cycles as $cycle ) {
  1084. if ( $moffset % $cycle == 0 )
  1085. $safe = true;
  1086. }
  1087. // If not marked as safe then remove all from month
  1088. if ( ! $safe )
  1089. $this->month_objs[$k]->FilterAll();
  1090. }
  1091. }
  1092. /**
  1093. * Filters a month in a weekly schedule. Disables all in any week not in filters.
  1094. *
  1095. * @param $filters The set of weeks to keep days in.
  1096. */
  1097. public function ApplyWeekdayFilters( $filters )
  1098. {
  1099. foreach( array_keys( $this->month_objs ) as $k ) {
  1100. $mweeks = $this->month_objs[$k]->ToWeeksVector( false );
  1101. $rweeks = $this->month_objs[$k]->ToWeeksVector( true ); // reversed
  1102. $max = count( $mweeks ); // number of weeks in month same in both
  1103. $keep_days = array();
  1104. for ( $i=0; $i<$max; $i++ ) {
  1105. // Keep days in this week
  1106. if ( in_array( $i+1, $filters ) ) {
  1107. foreach( $mweeks[$i]->DaysOfMonth() as $day )
  1108. $keep_days[] = $day;
  1109. }
  1110. // Keep days in the reverse week
  1111. if ( in_array( $i-$max, $filters ) ) {
  1112. foreach( $rweeks[$i]->DaysOfMonth() as $day )
  1113. $keep_days[] = $day;
  1114. }
  1115. }
  1116. // Apply the day filter
  1117. $this->month_objs[$k]->FilterDaysTo( $keep_days );
  1118. }
  1119. }
  1120. /**
  1121. * Filters the weeks in this year by the repeat and cycle parameters.
  1122. *
  1123. * Example:
  1124. * repeats = 1,2,3
  1125. * cycles = null
  1126. * This would repeat in the 1st, 2nd and 3rd weeks after epoch.
  1127. *
  1128. * Alternatively if
  1129. * repeats = 4, 8
  1130. * cycles = 10
  1131. * This would repeat in the 4th and 8th weeks of a 10 week cycle.
  1132. *
  1133. * @param $repeat Array of weeks to repeat the events in.
  1134. * @param $cycles Array of cycle week lengths from offset (null for year).
  1135. */
  1136. public function ApplyWeeklyCycles( $repeat, $cycles )
  1137. {
  1138. // If there is no cycle set given then just loop over weeks
  1139. // and filter out all days if the week number doesn't match
  1140. if ( $cycles == null || count( $cycles ) == 0 ) {
  1141. // This is an array that store the index of days to keep
  1142. $keepdays_index = array();
  1143. // Calculate the keep days index array
  1144. $numdays = count( $this->day_objs );
  1145. foreach( $repeat as $week ) {
  1146. if ( $week > 0 ) {
  1147. // Multiply out the week to get the range of days to keep
  1148. $start = ( $week - 1 ) * 7;
  1149. $finish = $week * 7;
  1150. if ( $start >= $numdays ) continue;
  1151. if ( $finish > $numdays ) $finish = $numdays;
  1152. for ( $i=$start; $i<$finish; $i++ )
  1153. $keepdays_index[] = $i;
  1154. } else if ( $week < 0 ) {
  1155. // Do a reverse multiplication to get end of year back 7 days
  1156. $finish = $numdays + ( ( $week + 1 ) * 7 ); // week is neg so days - weeks
  1157. $start = $numdays + ( $week * 7 );
  1158. if ( $finish <= 0 ) continue;
  1159. if ( $start < 0 ) $start = 0;
  1160. for ( $i=$start; $i<$finish; $i++ )
  1161. $keepdays_index[] = $i;
  1162. }
  1163. }
  1164. // Loop over the days and disable any not marked
  1165. foreach( array_keys( $this->day_objs ) as $k ) {
  1166. if ( ! in_array( $k, $keepdays_index ) )
  1167. $this->day_objs[$k]->Disable();
  1168. }
  1169. } else if ( is_array( $cycles ) && count( $cycles ) > 0 ) {
  1170. // This is fundamentally different to the above it loops over
  1171. // actual SUN-SAT weeks instead of 7 day blocks. Where the:
  1172. // weeks epoch offset % cycles == repeats - 1
  1173. // the week is considered to contain available days.
  1174. foreach( array_keys( $this->week_objs ) as $k ) {
  1175. $save = false;
  1176. $offset = $this->week_objs[$k]->GetEpochOffset();
  1177. foreach( $cycles as $cycle ) {
  1178. foreach( $repeat as $spot ) {
  1179. if ( $offset % $cycle == $spot - 1 ) {
  1180. $save = true;
  1181. break; // no more iterations required
  1182. }
  1183. }
  1184. if ( $save ) break; // don't iterate once we know the answer
  1185. }
  1186. if ( ! $save )
  1187. $this->week_objs[$k]->FilterAll();
  1188. }
  1189. }
  1190. }
  1191. }
  1192. /**
  1193. * DateRange is the class the RepeatExpression processor works with
  1194. * it encapsulates the Years, Months, Weeks and Days and filters them
  1195. * based on a "age" style year from the event epoch.
  1196. */
  1197. class EveryCal_RE_DateRange
  1198. {
  1199. private $years = null;
  1200. private $from = null;
  1201. private $until = null;
  1202. private $epoch = null;
  1203. public function __construct( $epoch, $start, $finish )
  1204. {
  1205. $this->from = clone $start;
  1206. $this->until = clone $finish;
  1207. $this->epoch = clone $epoch;
  1208. $this->years = array();
  1209. // Calculate the number of years passed from epoch to start
  1210. $counter = 0;
  1211. $eyear = $this->epoch->format( 'Y' );
  1212. $syear = $this->from->format( 'Y' );
  1213. $fyear = $this->until->format( 'Y' );
  1214. // Error Checking
  1215. if ( $syear < $eyear )
  1216. // Can't start calculating before the epoch so throw error
  1217. throw new Exception( sprintf( __( "Can't construct EveryCal_RE_DateRange with start before event epoch" ) ) );
  1218. if ( $fyear < $syear )
  1219. // Can't start calculating after the end point so throw error
  1220. throw new Exception( sprintf( __( "Can't construct EveryCal_RE_DateRange with finish before start" ) ) );
  1221. // Build the year array
  1222. if ( $eyear == $syear ) { // same year still 0th year of life
  1223. // Create a year 0 instance from the epoch date
  1224. $this->years[0] = new EveryCal_RE_Year( 0, $this->epoch, $this->epoch );
  1225. $this->years[0]->FilterBefore( $this->from );
  1226. // Check the finish date, if inside year 0 then dont' worry but if
  1227. // not then we need to build a year 1 and filter out the end
  1228. if ( $eyear == $fyear ) {
  1229. $this->years[0]->FilterAfter( $this->until );
  1230. } else {
  1231. // Check if still within the 1 year boundary
  1232. $tdate = clone $this->epoch;
  1233. $tdate->modify( '+1 year' );
  1234. while ( $tdate < $this->until ) { // not within the boundary
  1235. $counter += 1;
  1236. $this->years[$counter] = new EveryCal_RE_Year( $counter, $tdate, $this->epoch );
  1237. $tdate->modify( '+1 year' );
  1238. }
  1239. // Filter the last year by the end date
  1240. $this->years[$counter]->FilterAfter( $this->until );
  1241. }
  1242. } else { // end same start year as epoch
  1243. // Two options here:
  1244. // 1) still within year 0 and need to filter out days before start; or
  1245. // 2) need to find year that contains start point
  1246. $tsdate = clone $this->epoch;
  1247. $ts2date = clone $this->epoch;
  1248. while ( $ts2date < $this->from ) {
  1249. $ts2date->modify( '+1 year' );
  1250. if ( $ts2date < $this->from ) {
  1251. $tsdate = clone $ts2date;
  1252. $counter += 1;
  1253. }
  1254. }
  1255. // Build the first year at the counter and filter to the start point
  1256. $this->years[$counter] = new EveryCal_RE_Year( $counter, $tsdate, $this->epoch );
  1257. $this->years[$counter]->FilterBefore( $this->from );
  1258. // Now effectively we need to find the finish date and build years to it
  1259. $tsyear = $tsdate->format( 'Y' );
  1260. if ( $tsyear == $fyear ) { // finishes same year so simply filter
  1261. $this->years[$counter]->FilterAfter( $this->until );
  1262. } else {
  1263. // Check if still inside a 1 year boundary and loop if not (as above)
  1264. $tsdate->modify( '+1 year' );
  1265. while ( $tsdate < $this->until ) {
  1266. $counter += 1;
  1267. $this->years[$counter] = new EveryCal_RE_Year( $counter, $tsdate, $this->epoch );
  1268. $tsdate->modify( '+1 year' );
  1269. }
  1270. // Filter the final year by the end date
  1271. $this->years[$counter]->FilterAfter( $this->until );
  1272. }
  1273. }
  1274. }
  1275. /**
  1276. * Returns an string repr…

Large files files are truncated, but you can click here to view the full file