2015-06-23 11:31:18 +03:00
< ? php
/**
2015-06-26 22:10:46 +03:00
* Licensed under the MIT license .
2015-06-26 17:02:29 +03:00
*
2015-06-26 22:10:46 +03:00
* For the full copyright and license information , please view the LICENSE file .
2015-06-26 17:02:29 +03:00
*
2015-06-26 22:10:46 +03:00
* @ author Rémi Lanvin < remi @ cloudconnected . fr >
* @ link https :// github . com / rlanvin / php - rrule
2015-06-23 11:31:18 +03:00
*/
namespace RRule ;
/**
2015-06-26 22:10:46 +03:00
* Check that a variable is not empty . 0 and '0' are considered NOT empty
*
2015-06-23 11:31:18 +03:00
* @ return bool
*/
function not_empty ( $var )
{
return ! empty ( $var ) || $var === 0 || $var === '0' ;
}
/**
* closure / goog / math / math . js : modulo
* Copyright 2006 The Closure Library Authors .
*
* The % operator in PHP returns the remainder of a / b , but differs from
* some other languages in that the result will have the same sign as the
* dividend . For example , - 1 % 8 == - 1 , whereas in some other languages
* ( such as Python ) the result would be 7. This function emulates the more
* correct modulo behavior , which is useful for certain applications such as
* calculating an offset index in a circular list .
*
* @ param int $a The dividend .
* @ param int $b The divisor .
*
* @ return int $a % $b where the result is between 0 and $b
* ( either 0 <= x < $b
* or $b < x <= 0 , depending on the sign of $b ) .
*/
function pymod ( $a , $b )
{
$x = $a % $b ;
// If $x and $b differ in sign, add $b to wrap the result to the correct sign.
return ( $x * $b < 0 ) ? $x + $b : $x ;
}
/**
2015-06-26 22:10:46 +03:00
* Check is a year is a leap year .
2015-06-23 11:31:18 +03:00
* @ return bool
*/
function is_leap_year ( $year )
{
if ( $year % 4 !== 0 ) {
return false ;
}
if ( $year % 100 !== 0 ) {
return true ;
}
if ( $year % 400 !== 0 ) {
return false ;
}
return true ;
}
2015-06-29 11:45:39 +03:00
/**
2015-07-01 12:23:39 +03:00
* Implementation of RRULE as defined by RFC 5545 ( iCalendar ) .
2015-06-26 22:10:46 +03:00
* Heavily based on python - dateutil / rrule
*
* Some useful terms to understand the algorithms and variables naming :
*
* yearday = day of the year , from 0 to 365 ( on leap years ) - date ( 'z' )
* weekday = day of the week ( ISO - 8601 ), from 1 ( MO ) to 7 ( SU ) - date ( 'N' )
* monthday = day of the month , from 1 to 31
* wkst = week start , the weekday ( 1 to 7 ) which is the first day of week .
* Default is Monday ( 1 ) . In some countries it ' s Sunday ( 7 ) .
* weekno = number of the week in the year ( ISO - 8601 )
*
* CAREFUL with this bug : https :// bugs . php . net / bug . php ? id = 62476
*
* @ see https :// tools . ietf . org / html / rfc5545
* @ see https :// labix . org / python - dateutil
2015-06-23 11:31:18 +03:00
*/
2015-07-02 17:12:48 +03:00
class RRule implements \Iterator , \ArrayAccess , \Countable
2015-06-23 11:31:18 +03:00
{
2015-06-26 17:02:29 +03:00
const SECONDLY = 7 ;
const MINUTELY = 6 ;
const HOURLY = 5 ;
const DAILY = 4 ;
const WEEKLY = 3 ;
const MONTHLY = 2 ;
const YEARLY = 1 ;
// frequency names
2015-06-27 14:12:57 +03:00
public static $frequencies = array (
2015-06-26 17:02:29 +03:00
'SECONDLY' => self :: SECONDLY ,
'MINUTELY' => self :: MINUTELY ,
'HOURLY' => self :: HOURLY ,
'DAILY' => self :: DAILY ,
'WEEKLY' => self :: WEEKLY ,
'MONTHLY' => self :: MONTHLY ,
'YEARLY' => self :: YEARLY
2015-06-27 14:12:57 +03:00
);
2015-06-23 11:31:18 +03:00
// weekdays numbered from 1 (ISO-8601 or date('N'))
2015-06-27 14:12:57 +03:00
public static $week_days = array (
2015-06-26 17:02:29 +03:00
'MO' => 1 ,
'TU' => 2 ,
'WE' => 3 ,
'TH' => 4 ,
'FR' => 5 ,
'SA' => 6 ,
'SU' => 7
2015-06-27 14:12:57 +03:00
);
2015-06-23 11:31:18 +03:00
// original rule
protected $rule = array (
'DTSTART' => null ,
'FREQ' => null ,
'UNTIL' => null ,
'COUNT' => null ,
'INTERVAL' => 1 ,
'BYSECOND' => null ,
'BYMINUTE' => null ,
'BYHOUR' => null ,
'BYDAY' => null ,
'BYMONTHDAY' => null ,
'BYYEARDAY' => null ,
'BYWEEKNO' => null ,
'BYMONTH' => null ,
'BYSETPOS' => null ,
'WKST' => 'MO'
);
// parsed and validated values
protected $dtstart = null ;
protected $freq = null ;
protected $until = null ;
protected $count = null ;
protected $interval = null ;
protected $bysecond = null ;
protected $byminute = null ;
protected $byhour = null ;
protected $byweekday = null ;
2015-06-26 21:24:52 +03:00
protected $byweekday_nth = null ;
2015-06-23 11:31:18 +03:00
protected $bymonthday = null ;
protected $bymonthday_negative = null ;
protected $byyearday = null ;
protected $byweekno = null ;
protected $bymonth = null ;
protected $bysetpos = null ;
protected $wkst = null ;
2015-06-26 17:02:29 +03:00
protected $timeset = null ;
2015-06-23 11:31:18 +03:00
2015-07-02 17:12:48 +03:00
// cache variables
2015-07-03 12:25:50 +03:00
protected $total = null ;
2015-07-02 17:12:48 +03:00
protected $cache = array ();
2015-06-23 11:31:18 +03:00
// Public interface
/**
2015-06-26 21:24:52 +03:00
* The constructor needs the entire rule at once .
* There is no setter after the class has been instanciated ,
* because in order to validate some BYXXX parts , we need to know
* the value of some other parts ( FREQ or other BXXX parts ) .
2015-06-23 11:31:18 +03:00
*/
2015-07-07 19:16:46 +03:00
public function __construct ( $parts )
2015-06-23 11:31:18 +03:00
{
2015-07-07 19:16:46 +03:00
if ( is_string ( $parts ) ) {
$parts = self :: parseRfcString ( $parts );
}
elseif ( is_array ( $parts ) ) {
$parts = array_change_key_case ( $parts , CASE_UPPER );
}
else {
throw new \InvalidArgumentException ( sprintf (
'The first argument must be a string or an array (%s provided)' ,
gettype ( $parts )
));
}
2015-06-26 17:02:29 +03:00
2015-06-23 11:31:18 +03:00
// validate extra parts
$unsupported = array_diff_key ( $parts , $this -> rule );
if ( ! empty ( $unsupported ) ) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException (
'Unsupported parameter(s): '
. implode ( ',' , array_keys ( $unsupported ))
);
2015-06-23 11:31:18 +03:00
}
$parts = array_merge ( $this -> rule , $parts );
$this -> rule = $parts ; // save original rule
// WKST
$parts [ 'WKST' ] = strtoupper ( $parts [ 'WKST' ]);
if ( ! array_key_exists ( $parts [ 'WKST' ], self :: $week_days ) ) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException (
'The WKST rule part must be one of the following: '
. implode ( ', ' , array_keys ( self :: $week_days ))
);
2015-06-23 11:31:18 +03:00
}
$this -> wkst = self :: $week_days [ $parts [ 'WKST' ]];
// FREQ
2015-07-02 17:12:48 +03:00
if ( is_integer ( $parts [ 'FREQ' ]) ) {
if ( $parts [ 'FREQ' ] > self :: SECONDLY || $parts [ 'FREQ' ] < self :: YEARLY ) {
throw new \InvalidArgumentException (
'The FREQ rule part must be one of the following: '
. implode ( ', ' , array_keys ( self :: $frequencies ))
);
}
$this -> freq = $parts [ 'FREQ' ];
}
else { // string
$parts [ 'FREQ' ] = strtoupper ( $parts [ 'FREQ' ]);
if ( ! array_key_exists ( $parts [ 'FREQ' ], self :: $frequencies ) ) {
throw new \InvalidArgumentException (
'The FREQ rule part must be one of the following: '
. implode ( ', ' , array_keys ( self :: $frequencies ))
);
}
$this -> freq = self :: $frequencies [ $parts [ 'FREQ' ]];
2015-06-23 11:31:18 +03:00
}
// INTERVAL
$parts [ 'INTERVAL' ] = ( int ) $parts [ 'INTERVAL' ];
if ( $parts [ 'INTERVAL' ] < 1 ) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException (
'The INTERVAL rule part must be a positive integer (> 0)'
);
2015-06-23 11:31:18 +03:00
}
$this -> interval = ( int ) $parts [ 'INTERVAL' ];
// DTSTART
if ( not_empty ( $parts [ 'DTSTART' ]) ) {
2015-07-02 17:12:48 +03:00
try {
$this -> dtstart = self :: parseDate ( $parts [ 'DTSTART' ]);
} catch ( \Exception $e ) {
throw new \InvalidArgumentException (
'Failed to parse DTSTART ; it must be a valid date, timestamp or \DateTime object'
);
2015-06-23 11:31:18 +03:00
}
}
else {
2015-06-26 17:02:29 +03:00
$this -> dtstart = new \DateTime ();
2015-06-23 11:31:18 +03:00
}
// UNTIL (optional)
if ( not_empty ( $parts [ 'UNTIL' ]) ) {
2015-07-02 17:12:48 +03:00
try {
$this -> until = self :: parseDate ( $parts [ 'UNTIL' ]);
} catch ( \Exception $e ) {
throw new \InvalidArgumentException (
'Failed to parse UNTIL ; it must be a valid date, timestamp or \DateTime object'
);
2015-06-23 11:31:18 +03:00
}
}
// COUNT (optional)
if ( not_empty ( $parts [ 'COUNT' ]) ) {
$parts [ 'COUNT' ] = ( int ) $parts [ 'COUNT' ];
if ( $parts [ 'COUNT' ] < 1 ) {
throw new \InvalidArgumentException ( 'COUNT must be a positive integer (> 0)' );
}
$this -> count = ( int ) $parts [ 'COUNT' ];
}
2015-07-07 19:16:46 +03:00
if ( $this -> until && $this -> count ) {
throw new \InvalidArgumentException ( 'The UNTIL or COUNT rule parts MUST NOT occur in the same rule' );
}
2015-07-08 12:40:45 +03:00
2015-06-23 11:31:18 +03:00
// infer necessary BYXXX rules from DTSTART, if not provided
if ( ! ( not_empty ( $parts [ 'BYWEEKNO' ]) || not_empty ( $parts [ 'BYYEARDAY' ]) || not_empty ( $parts [ 'BYMONTHDAY' ]) || not_empty ( $parts [ 'BYDAY' ])) ) {
switch ( $this -> freq ) {
2015-06-26 17:02:29 +03:00
case self :: YEARLY :
2015-06-23 11:31:18 +03:00
if ( ! not_empty ( $parts [ 'BYMONTH' ]) ) {
2015-06-27 14:21:07 +03:00
$parts [ 'BYMONTH' ] = array (( int ) $this -> dtstart -> format ( 'm' ));
2015-06-23 11:31:18 +03:00
}
2015-06-27 14:21:07 +03:00
$parts [ 'BYMONTHDAY' ] = array (( int ) $this -> dtstart -> format ( 'j' ));
2015-06-23 11:31:18 +03:00
break ;
2015-06-26 17:02:29 +03:00
case self :: MONTHLY :
2015-06-27 14:21:07 +03:00
$parts [ 'BYMONTHDAY' ] = array (( int ) $this -> dtstart -> format ( 'j' ));
2015-06-23 11:31:18 +03:00
break ;
2015-06-26 17:02:29 +03:00
case self :: WEEKLY :
2015-06-27 14:21:07 +03:00
$parts [ 'BYDAY' ] = array ( array_search ( $this -> dtstart -> format ( 'N' ), self :: $week_days ));
2015-06-23 11:31:18 +03:00
break ;
}
}
// BYDAY (translated to byweekday for convenience)
if ( not_empty ( $parts [ 'BYDAY' ]) ) {
if ( ! is_array ( $parts [ 'BYDAY' ]) ) {
$parts [ 'BYDAY' ] = explode ( ',' , $parts [ 'BYDAY' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byweekday = array ();
$this -> byweekday_nth = array ();
2015-06-23 11:31:18 +03:00
foreach ( $parts [ 'BYDAY' ] as $value ) {
2015-07-02 17:12:48 +03:00
$value = trim ( strtoupper ( $value ));
2015-06-23 11:31:18 +03:00
$valid = preg_match ( '/^([+-]?[0-9]+)?([A-Z]{2})$/' , $value , $matches );
if ( ! $valid || ( not_empty ( $matches [ 1 ]) && ( $matches [ 1 ] == 0 || $matches [ 1 ] > 53 || $matches [ 1 ] < - 53 )) || ! array_key_exists ( $matches [ 2 ], self :: $week_days ) ) {
throw new \InvalidArgumentException ( 'Invalid BYDAY value: ' . $value );
}
2015-06-26 17:02:29 +03:00
2015-06-23 11:31:18 +03:00
if ( $matches [ 1 ] ) {
2015-06-27 14:21:07 +03:00
$this -> byweekday_nth [] = array ( self :: $week_days [ $matches [ 2 ]], ( int ) $matches [ 1 ]);
2015-06-23 11:31:18 +03:00
}
else {
$this -> byweekday [] = self :: $week_days [ $matches [ 2 ]];
}
}
2015-06-26 21:24:52 +03:00
if ( ! empty ( $this -> byweekday_nth ) ) {
2015-06-26 17:02:29 +03:00
if ( ! ( $this -> freq === self :: MONTHLY || $this -> freq === self :: YEARLY ) ) {
throw new \InvalidArgumentException ( 'The BYDAY rule part MUST NOT be specified with a numeric value when the FREQ rule part is not set to MONTHLY or YEARLY.' );
2015-06-23 11:31:18 +03:00
}
2015-06-26 17:02:29 +03:00
if ( $this -> freq === self :: YEARLY && not_empty ( $parts [ 'BYWEEKNO' ]) ) {
throw new \InvalidArgumentException ( 'The BYDAY rule part MUST NOT be specified with a numeric value with the FREQ rule part set to YEARLY when the BYWEEKNO rule part is specified.' );
2015-06-23 11:31:18 +03:00
}
}
}
// The BYMONTHDAY rule part specifies a COMMA-separated list of days
// of the month. Valid values are 1 to 31 or -31 to -1. For
// example, -10 represents the tenth to the last day of the month.
// The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule
// part is set to WEEKLY.
if ( not_empty ( $parts [ 'BYMONTHDAY' ]) ) {
2015-06-26 17:02:29 +03:00
if ( $this -> freq === self :: WEEKLY ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule part is set to WEEKLY.' );
}
if ( ! is_array ( $parts [ 'BYMONTHDAY' ]) ) {
$parts [ 'BYMONTHDAY' ] = explode ( ',' , $parts [ 'BYMONTHDAY' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bymonthday = array ();
$this -> bymonthday_negative = array ();
2015-06-23 11:31:18 +03:00
foreach ( $parts [ 'BYMONTHDAY' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-23 11:31:18 +03:00
if ( ! $value || $value < - 31 || $value > 31 ) {
throw new \InvalidArgumentException ( 'Invalid BYMONTHDAY value: ' . $value . ' (valid values are 1 to 31 or -31 to -1)' );
}
if ( $value < 0 ) {
2015-07-08 12:40:45 +03:00
$this -> bymonthday_negative [] = $value ;
2015-06-23 11:31:18 +03:00
}
else {
2015-07-08 12:40:45 +03:00
$this -> bymonthday [] = $value ;
2015-06-23 11:31:18 +03:00
}
}
}
if ( not_empty ( $parts [ 'BYYEARDAY' ]) ) {
2015-06-26 17:02:29 +03:00
if ( $this -> freq === self :: DAILY || $this -> freq === self :: WEEKLY || $this -> freq === self :: MONTHLY ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'The BYYEARDAY rule part MUST NOT be specified when the FREQ rule part is set to DAILY, WEEKLY, or MONTHLY.' );
}
if ( ! is_array ( $parts [ 'BYYEARDAY' ]) ) {
$parts [ 'BYYEARDAY' ] = explode ( ',' , $parts [ 'BYYEARDAY' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bysetpos = array ();
2015-06-23 11:31:18 +03:00
foreach ( $parts [ 'BYYEARDAY' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-23 11:31:18 +03:00
if ( ! $value || $value < - 366 || $value > 366 ) {
throw new \InvalidArgumentException ( 'Invalid BYSETPOS value: ' . $value . ' (valid values are 1 to 366 or -366 to -1)' );
}
2015-07-08 12:40:45 +03:00
$this -> byyearday [] = $value ;
2015-06-23 11:31:18 +03:00
}
}
// BYWEEKNO
if ( not_empty ( $parts [ 'BYWEEKNO' ]) ) {
2015-06-26 17:02:29 +03:00
if ( $this -> freq !== self :: YEARLY ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'The BYWEEKNO rule part MUST NOT be used when the FREQ rule part is set to anything other than YEARLY.' );
}
if ( ! is_array ( $parts [ 'BYWEEKNO' ]) ) {
$parts [ 'BYWEEKNO' ] = explode ( ',' , $parts [ 'BYWEEKNO' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byweekno = array ();
2015-06-23 11:31:18 +03:00
foreach ( $parts [ 'BYWEEKNO' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-23 11:31:18 +03:00
if ( ! $value || $value < - 53 || $value > 53 ) {
throw new \InvalidArgumentException ( 'Invalid BYWEEKNO value: ' . $value . ' (valid values are 1 to 53 or -53 to -1)' );
}
2015-07-08 12:40:45 +03:00
$this -> byweekno [] = $value ;
2015-06-23 11:31:18 +03:00
}
}
// The BYMONTH rule part specifies a COMMA-separated list of months
// of the year. Valid values are 1 to 12.
if ( not_empty ( $parts [ 'BYMONTH' ]) ) {
if ( ! is_array ( $parts [ 'BYMONTH' ]) ) {
$parts [ 'BYMONTH' ] = explode ( ',' , $parts [ 'BYMONTH' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bymonth = array ();
2015-06-23 11:31:18 +03:00
foreach ( $parts [ 'BYMONTH' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-23 11:31:18 +03:00
if ( $value < 1 || $value > 12 ) {
throw new \InvalidArgumentException ( 'Invalid BYMONTH value: ' . $value );
}
2015-07-08 12:40:45 +03:00
$this -> bymonth [] = $value ;
2015-06-23 11:31:18 +03:00
}
}
if ( not_empty ( $parts [ 'BYSETPOS' ]) ) {
2015-06-29 11:45:39 +03:00
if ( ! ( not_empty ( $parts [ 'BYWEEKNO' ]) || not_empty ( $parts [ 'BYYEARDAY' ])
|| not_empty ( $parts [ 'BYMONTHDAY' ]) || not_empty ( $parts [ 'BYDAY' ])
|| not_empty ( $parts [ 'BYMONTH' ]) || not_empty ( $parts [ 'BYHOUR' ])
|| not_empty ( $parts [ 'BYMINUTE' ]) || not_empty ( $parts [ 'BYSECOND' ])) ) {
throw new \InvalidArgumentException ( 'The BYSETPOS rule part MUST only be used in conjunction with another BYxxx rule part.' );
2015-06-23 11:31:18 +03:00
}
if ( ! is_array ( $parts [ 'BYSETPOS' ]) ) {
$parts [ 'BYSETPOS' ] = explode ( ',' , $parts [ 'BYSETPOS' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bysetpos = array ();
2015-06-23 11:31:18 +03:00
foreach ( $parts [ 'BYSETPOS' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-23 11:31:18 +03:00
if ( ! $value || $value < - 366 || $value > 366 ) {
throw new \InvalidArgumentException ( 'Invalid BYSETPOS value: ' . $value . ' (valid values are 1 to 366 or -366 to -1)' );
}
2015-07-08 12:40:45 +03:00
$this -> bysetpos [] = $value ;
2015-06-23 11:31:18 +03:00
}
}
2015-06-26 17:02:29 +03:00
if ( not_empty ( $parts [ 'BYHOUR' ]) ) {
if ( ! is_array ( $parts [ 'BYHOUR' ]) ) {
$parts [ 'BYHOUR' ] = explode ( ',' , $parts [ 'BYHOUR' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byhour = array ();
2015-06-26 17:02:29 +03:00
foreach ( $parts [ 'BYHOUR' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-26 17:02:29 +03:00
if ( $value < 0 || $value > 23 ) {
throw new \InvalidArgumentException ( 'Invalid BYHOUR value: ' . $value );
}
2015-07-08 12:40:45 +03:00
$this -> byhour [] = $value ;
2015-06-26 17:02:29 +03:00
}
2015-07-01 12:23:39 +03:00
sort ( $this -> byhour );
2015-06-26 17:02:29 +03:00
}
elseif ( $this -> freq < self :: HOURLY ) {
2015-06-27 14:25:37 +03:00
$this -> byhour = array (( int ) $this -> dtstart -> format ( 'G' ));
2015-06-26 17:02:29 +03:00
}
if ( not_empty ( $parts [ 'BYMINUTE' ]) ) {
if ( ! is_array ( $parts [ 'BYMINUTE' ]) ) {
$parts [ 'BYMINUTE' ] = explode ( ',' , $parts [ 'BYMINUTE' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byminute = array ();
2015-06-26 17:02:29 +03:00
foreach ( $parts [ 'BYMINUTE' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-26 17:02:29 +03:00
if ( $value < 0 || $value > 59 ) {
throw new \InvalidArgumentException ( 'Invalid BYMINUTE value: ' . $value );
}
2015-07-08 12:40:45 +03:00
$this -> byminute [] = $value ;
2015-06-26 17:02:29 +03:00
}
2015-07-01 12:23:39 +03:00
sort ( $this -> byminute );
2015-06-26 17:02:29 +03:00
}
elseif ( $this -> freq < self :: MINUTELY ) {
2015-06-27 14:25:37 +03:00
$this -> byminute = array (( int ) $this -> dtstart -> format ( 'i' ));
2015-06-26 17:02:29 +03:00
}
if ( not_empty ( $parts [ 'BYSECOND' ]) ) {
if ( ! is_array ( $parts [ 'BYSECOND' ]) ) {
$parts [ 'BYSECOND' ] = explode ( ',' , $parts [ 'BYSECOND' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bysecond = array ();
2015-06-26 17:02:29 +03:00
foreach ( $parts [ 'BYSECOND' ] as $value ) {
2015-07-08 12:40:45 +03:00
$value = ( int ) $value ;
2015-06-30 18:19:09 +03:00
// yes, "60" is a valid value, in (very rare) cases on leap seconds
// December 31, 2005 23:59:60 UTC is a valid date...
// so is 2012-06-30T23:59:60UTC
2015-06-26 17:02:29 +03:00
if ( $value < 0 || $value > 60 ) {
throw new \InvalidArgumentException ( 'Invalid BYSECOND value: ' . $value );
}
2015-07-08 12:40:45 +03:00
$this -> bysecond [] = $value ;
2015-06-26 17:02:29 +03:00
}
2015-07-01 12:23:39 +03:00
sort ( $this -> bysecond );
2015-06-26 17:02:29 +03:00
}
elseif ( $this -> freq < self :: SECONDLY ) {
2015-06-27 14:25:37 +03:00
$this -> bysecond = array (( int ) $this -> dtstart -> format ( 's' ));
2015-06-26 17:02:29 +03:00
}
if ( $this -> freq < self :: HOURLY ) {
2015-06-30 18:19:09 +03:00
// for frequencies DAILY, WEEKLY, MONTHLY AND YEARLY, we can build
2015-06-26 17:02:29 +03:00
// an array of every time of the day at which there should be an
2015-06-26 21:24:52 +03:00
// occurrence - default, if no BYHOUR/BYMINUTE/BYSECOND are provided
2015-06-30 18:19:09 +03:00
// is only one time, and it's the DTSTART time. This is a cached version
// if you will, since it'll never change at these frequencies
2015-06-26 17:02:29 +03:00
$this -> timeset = array ();
foreach ( $this -> byhour as $hour ) {
foreach ( $this -> byminute as $minute ) {
foreach ( $this -> bysecond as $second ) {
2015-06-27 14:27:02 +03:00
$this -> timeset [] = array ( $hour , $minute , $second );
2015-06-26 17:02:29 +03:00
}
}
}
}
2015-06-23 11:31:18 +03:00
}
2015-07-07 19:16:46 +03:00
public function __toString ()
{
return $this -> rfcString ();
}
/**
* Format a rule according to RFC 5545
* @ return string
*/
public function rfcString ()
{
$str = '' ;
if ( $this -> rule [ 'DTSTART' ] ) {
$str = sprintf (
" DTSTART;TZID=%s:%s \n RRULE: " ,
$this -> dtstart -> getTimezone () -> getName (),
$this -> dtstart -> format ( 'Ymd\THis' )
);
}
2015-07-08 12:40:45 +03:00
$parts = array ();
2015-07-07 19:16:46 +03:00
foreach ( $this -> rule as $key => $value ) {
if ( $key === 'DTSTART' ) {
continue ;
}
if ( $key === 'INTERVAL' && $value == 1 ) {
continue ;
}
if ( $key === 'WKST' && $value === 'MO' ) {
continue ;
}
if ( $key === 'UNTIL' && $value ) {
// for a reason that I do not understand, UNTIL seems to always
// be in UTC (even when DTSTART includes TZID)
$tmp = clone $this -> until ;
$tmp -> setTimezone ( new \DateTimeZone ( 'UTC' ));
$parts [] = 'UNTIL=' . $tmp -> format ( 'Ymd\THis\Z' );
continue ;
}
if ( $value ) {
if ( is_array ( $value ) ) {
$value = implode ( ',' , $value );
}
$parts [] = strtoupper ( str_replace ( ' ' , '' , " $key = $value " ));
}
}
$str .= implode ( ';' , $parts );
return $str ;
}
/**
* Take a RFC 5545 string and returns an array ( to be given to the constructor )
* @ return array
*/
static public function parseRfcString ( $string )
{
$parts = array ();
$string = trim ( $string );
foreach ( explode ( " \n " , $string ) as $line ) {
$line = trim ( $line );
if ( strpos ( $line , ':' ) === false ) {
$property_name = 'RRULE' ;
$property_value = $line ;
}
else {
list ( $property_name , $property_value ) = explode ( ':' , $line );
}
$tmp = explode ( ';' , $property_name );
$property_name = $tmp [ 0 ];
$property_params = array ();
array_splice ( $tmp , 0 , 1 );
foreach ( $tmp as $pair ) {
if ( strpos ( $pair , '=' ) === false ) {
throw new \InvalidArgumentException ( 'Failed to parse RFC string, invlaid property parameters: ' . $pair );
}
list ( $key , $value ) = explode ( '=' , $pair );
$property_params [ $key ] = $value ;
}
switch ( $property_name ) {
case 'DTSTART' :
$tmp = null ;
if ( isset ( $property_params [ 'TZID' ]) ) {
$tmp = new \DateTimeZone ( $property_params [ 'TZID' ]);
}
$parts [ 'DTSTART' ] = new \DateTime ( $property_value , $tmp );
break ;
case 'RRULE' :
foreach ( explode ( ';' , $property_value ) as $pair ) {
list ( $key , $value ) = explode ( '=' , $pair );
if ( $key === 'UNTIL' ) {
$value = new \DateTime ( $value );
}
$parts [ $key ] = $value ;
}
break ;
default :
throw new \InvalidArgumentException ( 'Failed to parse RFC string, unsupported property: ' . $property_name );
}
}
return $parts ;
}
/**
* Clear the cache . Do NOT use while the class is iterating
* @ return $this
*/
2015-07-02 17:12:48 +03:00
public function clearCache ()
{
$this -> total = null ;
$this -> cache = array ();
return $this ;
}
2015-06-26 21:24:52 +03:00
/**
* @ return array
*/
2015-06-23 11:31:18 +03:00
public function getOccurrences ()
{
if ( ! $this -> count && ! $this -> until ) {
2015-06-26 21:24:52 +03:00
throw new \LogicException ( 'Cannot get all occurrences of an infinite recurrence rule.' );
2015-06-23 11:31:18 +03:00
}
2015-07-02 17:12:48 +03:00
// cached version already computed
if ( $this -> total !== null ) {
return $this -> cache ;
}
2015-06-27 14:12:57 +03:00
$res = array ();
2015-06-26 21:24:52 +03:00
foreach ( $this as $occurrence ) {
$res [] = $occurrence ;
2015-06-23 11:31:18 +03:00
}
return $res ;
}
/**
* @ return array
*/
public function getOccurrencesBetween ( $begin , $end )
{
2015-07-03 12:25:50 +03:00
if ( $begin !== null ) {
$begin = self :: parseDate ( $begin );
}
if ( $end !== null ) {
$end = self :: parseDate ( $end );
}
elseif ( ! $this -> count && ! $this -> until ) {
throw new \LogicException ( 'Cannot get all occurrences of an infinite recurrence rule.' );
}
2015-07-02 17:12:48 +03:00
$iterator = $this ;
if ( $this -> total !== null ) {
$iterator = $this -> cache ;
}
2015-06-27 14:12:57 +03:00
$res = array ();
2015-07-02 17:12:48 +03:00
foreach ( $iterator as $occurrence ) {
2015-07-03 12:25:50 +03:00
if ( $begin !== null && $occurrence < $begin ) {
2015-06-23 11:31:18 +03:00
continue ;
}
2015-07-03 12:25:50 +03:00
if ( $end !== null && $occurrence > $end ) {
2015-06-23 11:31:18 +03:00
break ;
}
2015-06-26 21:24:52 +03:00
$res [] = $occurrence ;
2015-06-23 11:31:18 +03:00
}
return $res ;
}
/**
2015-06-26 21:24:52 +03:00
* Alias of occursAt
* Because I think both are correct in English , aren ' t they ?
2015-07-07 19:16:46 +03:00
* @ return bool
2015-06-23 11:31:18 +03:00
*/
public function occursOn ( $date )
{
2015-06-26 21:24:52 +03:00
return $this -> occursAt ( $date );
}
/**
* Return true if $date is an occurrence of the rule .
*
* This method will attempt to determine the result programmatically .
* However depending on the BYXXX rule parts that have been set , it might
* not always be possible . As a last resort , this method will loop
* through all occurrences until $date . This will incurr some performance
* penalty .
*
* @ return bool
*/
public function occursAt ( $date )
{
2015-07-02 17:12:48 +03:00
$date = self :: parseDate ( $date );
if ( in_array ( $date , $this -> cache ) ) {
// in the cache (whether cache is complete or not)
return true ;
}
elseif ( $this -> total !== null ) {
// cache complete and not in cache
return false ;
2015-06-26 21:24:52 +03:00
}
// let's start with the obvious
if ( $date < $this -> dtstart || ( $this -> until && $date > $this -> until ) ) {
return false ;
}
// now the BYXXX rules (expect BYSETPOS)
if ( $this -> byhour && ! in_array ( $date -> format ( 'G' ), $this -> byhour ) ) {
return false ;
}
if ( $this -> byminute && ! in_array (( int ) $date -> format ( 'i' ), $this -> byminute ) ) {
return false ;
}
2015-06-27 13:50:28 +03:00
if ( $this -> bysecond && ! in_array (( int ) $date -> format ( 's' ), $this -> bysecond ) ) {
2015-06-26 21:24:52 +03:00
return false ;
}
// we need some more variables before we continue
list ( $year , $month , $day , $yearday , $weekday ) = explode ( ' ' , $date -> format ( 'Y n j z N' ));
$masks = array ();
2015-07-01 14:13:59 +03:00
$masks [ 'weekday_of_1st_yearday' ] = date_create ( $year . '-01-01 00:00:00' ) -> format ( 'N' );
2015-06-26 21:24:52 +03:00
$masks [ 'yearday_to_weekday' ] = array_slice ( self :: $WEEKDAY_MASK , $masks [ 'weekday_of_1st_yearday' ] - 1 );
if ( is_leap_year ( $year ) ) {
$masks [ 'year_len' ] = 366 ;
$masks [ 'last_day_of_month' ] = self :: $LAST_DAY_OF_MONTH_366 ;
}
else {
$masks [ 'year_len' ] = 365 ;
$masks [ 'last_day_of_month' ] = self :: $LAST_DAY_OF_MONTH ;
}
$month_len = $masks [ 'last_day_of_month' ][ $month ] - $masks [ 'last_day_of_month' ][ $month - 1 ];
if ( $this -> bymonth && ! in_array ( $month , $this -> bymonth ) ) {
return false ;
}
if ( $this -> byweekday && ! in_array ( $weekday , $this -> byweekday ) ) {
return false ;
}
if ( $this -> bymonthday || $this -> bymonthday_negative ) {
2015-06-27 13:50:28 +03:00
$monthday_negative = - 1 * ( $month_len - $day + 1 );
2015-06-26 21:24:52 +03:00
if ( ! in_array ( $day , $this -> bymonthday ) && ! in_array ( $monthday_negative , $this -> bymonthday_negative ) ) {
return false ;
}
}
if ( $this -> byyearday ) {
// caution here, yearday starts from 0 !
$yearday_negative = - 1 * ( $masks [ 'year_len' ] - $yearday );
if ( ! in_array ( $yearday + 1 , $this -> byyearday ) && ! in_array ( $yearday_negative , $this -> byyearday ) ) {
return false ;
}
}
if ( $this -> byweekday_nth ) {
// we need to summon some magic here
$this -> buildNthWeekdayMask ( $year , $month , $day , $masks );
if ( ! isset ( $masks [ 'yearday_is_nth_weekday' ][ $yearday ]) ) {
return false ;
}
}
if ( $this -> byweekno ) {
// more magic
$this -> buildWeeknoMask ( $year , $month , $day , $masks );
if ( ! isset ( $masks [ 'yearday_is_in_weekno' ][ $yearday ]) ) {
return false ;
}
}
// so now we have exhausted all the BYXXX rules (exept bysetpos),
// we still need to consider frequency and interval
list ( $start_year , $start_month , $start_day ) = explode ( '-' , $this -> dtstart -> format ( 'Y-m-d' ));
switch ( $this -> freq ) {
case self :: YEARLY :
if ( ( $year - $start_year ) % $this -> interval !== 0 ) {
return false ;
}
break ;
case self :: MONTHLY :
// we need to count the number of months elapsed
2015-06-29 11:45:39 +03:00
$diff = ( 12 - $start_month ) + 12 * ( $year - $start_year - 1 ) + $month ;
2015-06-26 21:24:52 +03:00
2015-06-29 11:45:39 +03:00
if ( ( $diff % $this -> interval ) !== 0 ) {
2015-06-26 21:24:52 +03:00
return false ;
}
break ;
case self :: WEEKLY :
// count nb of days and divide by 7 to get number of weeks
2015-06-29 11:45:39 +03:00
// we add some days to align dtstart with wkst
$diff = $date -> diff ( $this -> dtstart );
$diff = ( int ) (( $diff -> days + pymod ( $this -> dtstart -> format ( 'N' ) - $this -> wkst , 7 )) / 7 );
if ( $diff % $this -> interval !== 0 ) {
2015-06-26 21:24:52 +03:00
return false ;
}
break ;
case self :: DAILY :
// count nb of days
2015-06-29 11:45:39 +03:00
$diff = $date -> diff ( $this -> dtstart );
if ( $diff -> days % $this -> interval !== 0 ) {
2015-06-26 21:24:52 +03:00
return false ;
}
break ;
2015-06-29 11:45:39 +03:00
// XXX: I'm not sure the 3 formulas below take the DST into account...
2015-06-26 21:24:52 +03:00
case self :: HOURLY :
2015-06-29 11:45:39 +03:00
$diff = $date -> diff ( $this -> dtstart );
$diff = $diff -> h + $diff -> days * 24 ;
if ( $diff % $this -> interval !== 0 ) {
return false ;
}
break ;
2015-06-26 21:24:52 +03:00
case self :: MINUTELY :
2015-06-29 11:45:39 +03:00
$diff = $date -> diff ( $this -> dtstart );
$diff = $diff -> i + $diff -> h * 60 + $diff -> days * 1440 ;
if ( $diff % $this -> interval !== 0 ) {
return false ;
}
break ;
2015-06-26 21:24:52 +03:00
case self :: SECONDLY :
2015-06-29 11:45:39 +03:00
$diff = $date -> diff ( $this -> dtstart );
2015-06-30 18:19:09 +03:00
// XXX does not account for leap second (should it?)
2015-06-29 11:45:39 +03:00
$diff = $diff -> s + $diff -> i * 60 + $diff -> h * 3600 + $diff -> days * 86400 ;
if ( $diff % $this -> interval !== 0 ) {
return false ;
}
break ;
2015-06-26 21:24:52 +03:00
throw new \Exception ( 'Unimplemented frequency' );
}
2015-06-23 11:31:18 +03:00
2015-06-26 21:24:52 +03:00
// now we are left with 2 rules BYSETPOS and COUNT
//
// - I think BYSETPOS *could* be determined without loooping by considering
// the current set, calculating all the occurrences of the current set
// and determining the position of $date in the result set.
// However I'm not convinced it's worth it.
//
// - I don't see any way to determine COUNT programmatically, because occurrences
// might sometimes be dropped (e.g. a 29 Feb on a normal year, or during
// the switch to DST) and not counted in the final set
if ( ! $this -> count && ! $this -> bysetpos ) {
return true ;
}
// so... as a fallback we have to loop
foreach ( $this as $occurrence ) {
if ( $occurrence == $date ) {
return true ; // lucky you!
}
if ( $occurrence > $date ) {
break ;
}
}
// we ended the loop without finding
return false ;
2015-06-23 11:31:18 +03:00
}
// Iterator interface
2015-07-07 19:16:46 +03:00
// Note: if cache is complete, we could probably avoid completely calling iterate()
// and instead iterate directly on the $this->cache array
2015-06-23 11:31:18 +03:00
2015-07-02 17:12:48 +03:00
protected $current = 0 ;
protected $key = 0 ;
2015-06-23 11:31:18 +03:00
public function rewind ()
{
2015-07-02 17:12:48 +03:00
$this -> current = $this -> iterate ( true );
$this -> key = 0 ;
2015-06-23 11:31:18 +03:00
}
public function current ()
{
2015-07-02 17:12:48 +03:00
return $this -> current ;
2015-06-23 11:31:18 +03:00
}
public function key ()
{
2015-07-02 17:12:48 +03:00
return $this -> key ;
2015-06-23 11:31:18 +03:00
}
public function next ()
{
2015-07-02 17:12:48 +03:00
$this -> current = $this -> iterate ();
$this -> key += 1 ;
2015-06-23 11:31:18 +03:00
}
public function valid ()
{
2015-07-02 17:12:48 +03:00
return $this -> current !== null ;
2015-06-23 11:31:18 +03:00
}
// ArrayAccess interface
public function offsetExists ( $offset )
{
2015-07-02 17:12:48 +03:00
return is_numeric ( $offset ) && $offset >= 0 && $offset < count ( $this );
2015-06-23 11:31:18 +03:00
}
public function offsetGet ( $offset )
{
2015-07-02 17:12:48 +03:00
if ( isset ( $this -> cache [ $offset ]) ) {
// found in cache
return $this -> cache [ $offset ];
}
elseif ( $this -> total !== null ) {
// cache complete and not found in cache
return null ;
}
// not in cache and cache not complete, we have to loop to find it
$i = 0 ;
foreach ( $this as $occurrence ) {
if ( $i == $offset ) {
return $occurrence ;
}
$i ++ ;
if ( $i > $offset ) {
break ;
}
}
return null ;
2015-06-23 11:31:18 +03:00
}
public function offsetSet ( $offset , $value )
{
throw new LogicException ( 'Setting a Date in a RRule is not supported' );
}
public function offsetUnset ( $offset )
{
throw new LogicException ( 'Unsetting a Date in a RRule is not supported' );
}
2015-07-02 17:12:48 +03:00
// Countable interface
/**
* Returns the number of recurrences in this set . It will have go
* through the whole recurrence , if this hasn ' t been done before , which
* introduces a performance penality .
* @ return int
*/
public function count ()
{
if ( ! $this -> count && ! $this -> until ) {
throw new \LogicException ( 'Cannot count an infinite recurrence rule.' );
}
if ( $this -> total === null ) {
foreach ( $this as $occurrence ) {}
}
return $this -> total ;
}
2015-06-23 11:31:18 +03:00
// private methods
2015-07-07 19:16:46 +03:00
// where all the magic happens
2015-06-23 11:31:18 +03:00
2015-07-02 17:12:48 +03:00
/**
* Convert any date into a DateTime object .
* @ throws InvalidArgumentException on error
* @ param mixed $date
2016-02-18 16:52:41 +02:00
* @ return DateTime
2015-07-02 17:12:48 +03:00
*/
static public function parseDate ( $date )
{
2015-12-09 14:46:39 +02:00
// DateTimeInterface is only on PHP 5.5+, and includes DateTimeImmutable
if ( ! $date instanceof \DateTime && ! $date instanceof \DateTimeInterface ) {
2015-07-02 17:12:48 +03:00
try {
if ( is_integer ( $date ) ) {
$date = \DateTime :: createFromFormat ( 'U' , $date );
}
else {
$date = new \DateTime ( $date );
}
} catch ( \Exception $e ) {
throw new \InvalidArgumentException (
2015-12-09 14:46:39 +02:00
" Failed to parse the date "
2015-07-02 17:12:48 +03:00
);
}
}
return $date ;
}
2015-06-23 11:31:18 +03:00
/**
* This method returns an array of days of the year ( numbered from 0 to 365 )
* of the current timeframe ( year , month , week , day ) containing the current date
2015-07-07 19:16:46 +03:00
* @ return array
2015-06-23 11:31:18 +03:00
*/
protected function getDaySet ( $year , $month , $day , array $masks )
{
switch ( $this -> freq ) {
2015-06-26 17:02:29 +03:00
case self :: YEARLY :
2015-06-23 11:31:18 +03:00
return range ( 0 , $masks [ 'year_len' ] - 1 );
2015-06-26 17:02:29 +03:00
case self :: MONTHLY :
$start = $masks [ 'last_day_of_month' ][ $month - 1 ];
$stop = $masks [ 'last_day_of_month' ][ $month ];
2015-06-23 11:31:18 +03:00
return range ( $start , $stop - 1 );
2015-06-26 17:02:29 +03:00
case self :: WEEKLY :
2015-06-23 11:31:18 +03:00
// on first iteration, the first week will not be complete
// we don't backtrack to the first day of the week, to avoid
// crossing year boundary in reverse (i.e. if the week started
// during the previous year), because that would generate
// negative indexes (which would not work with the masks)
2015-06-27 14:12:57 +03:00
$set = array ();
2015-07-01 14:13:59 +03:00
$i = ( int ) date_create ( $year . '-' . $month . '-' . $day . ' 00:00:00' ) -> format ( 'z' );
2015-06-23 11:31:18 +03:00
$start = $i ;
for ( $j = 0 ; $j < 7 ; $j ++ ) {
$set [] = $i ;
$i += 1 ;
2015-06-26 17:02:29 +03:00
if ( $masks [ 'yearday_to_weekday' ][ $i ] == $this -> wkst ) {
2015-06-23 11:31:18 +03:00
break ;
}
}
return $set ;
2015-06-26 17:02:29 +03:00
case self :: DAILY :
case self :: HOURLY :
case self :: MINUTELY :
case self :: SECONDLY :
2015-07-01 14:13:59 +03:00
$i = ( int ) date_create ( $year . '-' . $month . '-' . $day . ' 00:00:00' ) -> format ( 'z' );
return array ( $i );
2015-06-23 11:31:18 +03:00
}
}
/**
* Some serious magic is happening here .
2015-06-27 13:50:28 +03:00
* This method will calculate the yeardays corresponding to each Nth weekday
* ( in BYDAY rule part ) .
* For example , in Jan 1998 , in a MONTHLY interval , " 1SU,-1SU " ( first Sunday
* and last Sunday ) would be transformed into [ 3 => true , 24 => true ] because
* the first Sunday of Jan 1998 is yearday 3 ( counting from 0 ) and the
* last Sunday of Jan 1998 is yearday 24 ( counting from 0 ) .
2015-07-07 19:16:46 +03:00
* @ return null ( modifies $mask parameter )
2015-06-23 11:31:18 +03:00
*/
2015-06-26 17:02:29 +03:00
protected function buildNthWeekdayMask ( $year , $month , $day , array & $masks )
2015-06-23 11:31:18 +03:00
{
2015-06-26 21:24:52 +03:00
$masks [ 'yearday_is_nth_weekday' ] = array ();
2015-06-23 11:31:18 +03:00
2015-06-26 21:24:52 +03:00
if ( $this -> byweekday_nth ) {
2015-06-23 11:31:18 +03:00
$ranges = array ();
2015-06-26 17:02:29 +03:00
if ( $this -> freq == self :: YEARLY ) {
2015-06-23 11:31:18 +03:00
if ( $this -> bymonth ) {
foreach ( $this -> bymonth as $bymonth ) {
2015-06-27 14:12:57 +03:00
$ranges [] = array (
$masks [ 'last_day_of_month' ][ $bymonth - 1 ],
$masks [ 'last_day_of_month' ][ $bymonth ] - 1
);
2015-06-23 11:31:18 +03:00
}
}
else {
2015-06-27 14:12:57 +03:00
$ranges = array ( array ( 0 , $masks [ 'year_len' ] - 1 ));
2015-06-23 11:31:18 +03:00
}
}
2015-06-26 17:02:29 +03:00
elseif ( $this -> freq == self :: MONTHLY ) {
2015-06-27 14:12:57 +03:00
$ranges [] = array (
$masks [ 'last_day_of_month' ][ $month - 1 ],
$masks [ 'last_day_of_month' ][ $month ] - 1
);
2015-06-23 11:31:18 +03:00
}
if ( $ranges ) {
2015-06-26 17:02:29 +03:00
// Weekly frequency won't get here, so we may not
// care about cross-year weekly periods.
2015-06-23 11:31:18 +03:00
foreach ( $ranges as $tmp ) {
list ( $first , $last ) = $tmp ;
2015-06-26 21:24:52 +03:00
foreach ( $this -> byweekday_nth as $tmp ) {
2015-06-23 11:31:18 +03:00
list ( $weekday , $nth ) = $tmp ;
if ( $nth < 0 ) {
$i = $last + ( $nth + 1 ) * 7 ;
2015-06-26 17:02:29 +03:00
$i = $i - pymod ( $masks [ 'yearday_to_weekday' ][ $i ] - $weekday , 7 );
2015-06-23 11:31:18 +03:00
}
else {
$i = $first + ( $nth - 1 ) * 7 ;
2015-06-26 17:02:29 +03:00
$i = $i + ( 7 - $masks [ 'yearday_to_weekday' ][ $i ] + $weekday ) % 7 ;
2015-06-23 11:31:18 +03:00
}
2015-06-27 13:50:28 +03:00
2015-06-23 11:31:18 +03:00
if ( $i >= $first && $i <= $last ) {
2015-06-26 21:24:52 +03:00
$masks [ 'yearday_is_nth_weekday' ][ $i ] = true ;
2015-06-23 11:31:18 +03:00
}
}
}
}
}
}
2015-06-26 17:02:29 +03:00
/**
2015-06-29 11:45:39 +03:00
* More serious magic .
* This method calculates the yeardays corresponding to the week number
* ( in the WEEKNO rule part ) .
* Because weeks can cross year boundaries ( that is , week #1 can start the
* previous year , and week 52 / 53 can continue till the next year ), the
* algorithm is quite long .
2015-07-07 19:16:46 +03:00
* @ return null ( modifies $mask )
2015-06-26 17:02:29 +03:00
*/
2015-07-07 19:16:46 +03:00
protected function buildWeeknoMask ( $year , $month , $day , array & $masks )
2015-06-26 17:02:29 +03:00
{
$masks [ 'yearday_is_in_weekno' ] = array ();
// calculate the index of the first wkst day of the year
// 0 means the first day of the year is the wkst day (e.g. wkst is Monday and Jan 1st is a Monday)
// n means there is n days before the first wkst day of the year.
// if n >= 4, this is the first day of the year (even though it started the year before)
$first_wkst = ( 7 - $masks [ 'weekday_of_1st_yearday' ] + $this -> wkst ) % 7 ;
if ( $first_wkst >= 4 ) {
$first_wkst_offset = 0 ;
// Number of days in the year, plus the days we got from last year.
$nb_days = $masks [ 'year_len' ] + $masks [ 'weekday_of_1st_yearday' ] - $this -> wkst ;
// $nb_days = $masks['year_len'] + pymod($masks['weekday_of_1st_yearday'] - $this->wkst,7);
}
else {
$first_wkst_offset = $first_wkst ;
// Number of days in the year, minus the days we left in last year.
$nb_days = $masks [ 'year_len' ] - $first_wkst ;
}
$nb_weeks = ( int ) ( $nb_days / 7 ) + ( int ) (( $nb_days % 7 ) / 4 );
// alright now we now when the first week starts
// and the number of weeks of the year
// so we can generate a map of every yearday that are in the weeks
// specified in byweekno
foreach ( $this -> byweekno as $n ) {
if ( $n < 0 ) {
$n = $n + $nb_weeks + 1 ;
}
if ( $n <= 0 || $n > $nb_weeks ) {
continue ;
}
if ( $n > 1 ) {
$i = $first_wkst_offset + ( $n - 1 ) * 7 ;
if ( $first_wkst_offset != $first_wkst ) {
// if week #1 started the previous year
// realign the start of the week
$i = $i - ( 7 - $first_wkst );
}
}
else {
$i = $first_wkst_offset ;
}
// now add 7 days into the resultset, stopping either at 7 or
// if we reach wkst before (in the case of short first week of year)
for ( $j = 0 ; $j < 7 ; $j ++ ) {
$masks [ 'yearday_is_in_weekno' ][ $i ] = true ;
$i = $i + 1 ;
if ( $masks [ 'yearday_to_weekday' ][ $i ] == $this -> wkst ) {
break ;
}
}
}
// if we asked for week #1, it's possible that the week #1 of next year
// already started this year. Therefore we need to return also the matching
// days of next year.
if ( in_array ( 1 , $this -> byweekno ) ) {
// Check week number 1 of next year as well
// TODO: Check -numweeks for next year.
$i = $first_wkst_offset + $nb_weeks * 7 ;
if ( $first_wkst_offset != $first_wkst ) {
$i = $i - ( 7 - $first_wkst );
}
if ( $i < $masks [ 'year_len' ] ) {
// If week starts in next year, we don't care about it.
for ( $j = 0 ; $j < 7 ; $j ++ ) {
$masks [ 'yearday_is_in_weekno' ][ $i ] = true ;
$i += 1 ;
if ( $masks [ 'yearday_to_weekday' ][ $i ] == $this -> wkst ) {
break ;
}
}
}
}
if ( $first_wkst_offset ) {
// Check last week number of last year as well.
// If first_wkst_offset is 0, either the year started on week start,
// or week number 1 got days from last year, so there are no
// days from last year's last week number in this year.
if ( ! in_array ( - 1 , $this -> byweekno ) ) {
2015-07-01 14:13:59 +03:00
$weekday_of_1st_yearday = date_create (( $year - 1 ) . '-01-01 00:00:00' ) -> format ( 'N' );
2015-06-26 17:02:29 +03:00
$first_wkst_offset_last_year = ( 7 - $weekday_of_1st_yearday + $this -> wkst ) % 7 ;
$last_year_len = 365 + is_leap_year ( $year - 1 );
if ( $first_wkst_offset_last_year >= 4 ) {
$first_wkst_offset_last_year = 0 ;
$nb_weeks_last_year = 52 + ( int ) ((( $last_year_len + ( $weekday_of_1st_yearday - $this -> wkst ) % 7 ) % 7 ) / 4 );
}
else {
$nb_weeks_last_year = 52 + ( int ) ((( $masks [ 'year_len' ] - $first_wkst_offset ) % 7 ) / 4 );
}
}
else {
$nb_weeks_last_year = - 1 ;
}
if ( in_array ( $nb_weeks_last_year , $this -> byweekno ) ) {
for ( $i = 0 ; $i < $first_wkst_offset ; $i ++ ) {
$masks [ 'yearday_is_in_weekno' ][ $i ] = true ;
}
}
}
}
2015-06-29 11:45:39 +03:00
/**
2015-06-30 18:19:09 +03:00
* This builds an array of every time of the day that matches the BYXXX time
* criteria . It will only process $this -> frequency at one time . So :
* - for HOURLY frequencies it builds the minutes and second of the given hour
* - for MINUTELY frequencies it builds the seconds of the given minute
* - for SECONDLY frequencies , it returns an array with one element
*
* This method is called everytime an increment of at least one hour is made .
2015-07-07 19:16:46 +03:00
* @ return array
2015-06-29 11:45:39 +03:00
*/
protected function getTimeSet ( $hour , $minute , $second )
{
switch ( $this -> freq ) {
case self :: HOURLY :
$set = array ();
foreach ( $this -> byminute as $minute ) {
foreach ( $this -> bysecond as $second ) {
// should we use another type?
$set [] = array ( $hour , $minute , $second );
}
}
// sort ?
return $set ;
case self :: MINUTELY :
$set = array ();
foreach ( $this -> bysecond as $second ) {
// should we use another type?
$set [] = array ( $hour , $minute , $second );
}
// sort ?
return $set ;
case self :: SECONDLY :
return array ( array ( $hour , $minute , $second ));
default :
throw new \LogicException ( 'getTimeSet called with an invalid frequency' );
}
}
2015-06-23 11:31:18 +03:00
/**
2015-06-26 22:10:46 +03:00
* This is the main method , where all of the magic happens .
2015-06-23 11:31:18 +03:00
*
* This method is a generator that works for PHP 5.3 / 5.4 ( using static variables )
2015-06-29 11:45:39 +03:00
*
* The main idea is : a brute force made fast by not relying on date () functions
*
* There is one big loop that examines every interval of the given frequency
* ( so every day , every week , every month or every year ), constructs an
* array of all the yeardays of the interval ( for daily frequencies , the array
* only has one element , for weekly 7 , and so on ), and then filters out any
* day that do no match BYXXX elements .
*
* The algorithm does not try to be " smart " in calculating the increment of
* the loop . That is , for a rule like " every day in January for 10 years "
* the algorithm will loop through every day of the year , each year , generating
* some 3650 iterations ( + some to account for the leap years ) .
* This is a bit counter - intuitive , as it is obvious that the loop could skip
* all the days in February till December since they are never going to match .
*
* Fortunately , this approach is still super fast because it doesn ' t rely
* on date () or DateTime functions , and instead does all the date operations
* manually , either arithmetically or using arrays as converters .
*
* Another quirk of this approach is that because the granularity is by day ,
* higher frequencies ( hourly , minutely and secondly ) have to have
2015-06-30 18:19:09 +03:00
* their own special loops within the main loop , making the all thing quite
* convoluted .
2015-06-29 11:45:39 +03:00
* Moreover , at such frequencies , the brute - force approach starts to really
* suck . For example , a rule like
* " Every minute, every Jan 1st between 10:00 and 10:59, for 10 years "
* requires a tremendous amount of useless iterations to jump from Jan 1 st 10 : 59
* at year 1 to Jan 1 st 10.00 at year 2.
*
* In order to make a " smart jump " , we would have to have a way to determine
* the gap between the next occurence arithmetically . I think that would require
* to analyze each " BYXXX " rule part that " Limit " the set ( see the RFC page 43 )
* at the given frequency . For example , a YEARLY frequency doesn ' t need " smart
* jump " at all; MONTHLY and WEEKLY frequencies only need to check BYMONTH;
* DAILY frequency needs to check BYMONTH , BYMONTHDAY and BYDAY , and so on .
* The check probably has to be done in reverse order , e . g . for DAILY frequencies
* attempt to jump to the next weekday ( BYDAY ) or next monthday ( BYMONTHDAY )
* ( I don ' t know yet which one first ), and then if that results in a change of
* month , attempt to jump to the next BYMONTH , and so on .
2015-07-07 19:16:46 +03:00
* @ return \DateTime | null
2015-06-23 11:31:18 +03:00
*/
protected function iterate ( $reset = false )
{
// these are the static variables, i.e. the variables that persists
// at every call of the method (to emulate a generator)
static $year = null , $month = null , $day = null ;
2015-06-26 17:02:29 +03:00
static $hour = null , $minute = null , $second = null ;
2015-06-30 18:19:09 +03:00
static $dayset = null , $masks = null , $timeset = null ;
2015-07-02 17:12:48 +03:00
static $dtstart = null , $total = 0 , $use_cache = true ;
2015-06-23 11:31:18 +03:00
if ( $reset ) {
$year = $month = $day = null ;
2015-06-26 17:02:29 +03:00
$hour = $minute = $second = null ;
2015-06-30 18:19:09 +03:00
$dayset = $masks = $timeset = null ;
2015-07-02 17:12:48 +03:00
$dtstart = null ;
2015-06-23 11:31:18 +03:00
$total = 0 ;
2015-07-02 17:12:48 +03:00
$use_cache = true ;
reset ( $this -> cache );
}
// go through the cache first
if ( $use_cache ) {
while ( ( $occurrence = current ( $this -> cache )) !== false ) {
// echo "Cache hit\n";
$dtstart = $occurrence ;
next ( $this -> cache );
$total += 1 ;
return $occurrence ;
}
reset ( $this -> cache );
// now set use_cache to false to skip the all thing on next iteration
// and start filling the cache instead
$use_cache = false ;
// if the cache as been used up completely and we now there is nothing else
if ( $total === $this -> total ) {
// echo "Cache used up, nothing else to compute\n";
return null ;
}
// echo "Cache used up with occurrences remaining\n";
if ( $dtstart ) {
// so we skip the last occurrence of the cache
if ( $this -> freq === self :: SECONDLY ) {
$dtstart -> modify ( '+' . $this -> interval . 'second' );
}
else {
$dtstart -> modify ( '+1second' );
}
}
2015-06-23 11:31:18 +03:00
}
// stop once $total has reached COUNT
if ( $this -> count && $total >= $this -> count ) {
2015-07-02 17:12:48 +03:00
$this -> total = $total ;
2015-06-23 11:31:18 +03:00
return null ;
}
2015-07-02 17:12:48 +03:00
if ( $dtstart === null ) {
$dtstart = clone $this -> dtstart ;
}
if ( $year === null ) {
2015-06-26 17:02:29 +03:00
if ( $this -> freq === self :: WEEKLY ) {
2015-06-23 11:31:18 +03:00
// we align the start date to the WKST, so we can then
2015-06-26 17:02:29 +03:00
// simply loop by adding +7 days. The Python lib does some
// calculation magic at the end of the loop (when incrementing)
// to realign on first pass.
2015-07-02 17:12:48 +03:00
$tmp = clone $dtstart ;
$tmp -> modify ( '-' . pymod ( $this -> dtstart -> format ( 'N' ) - $this -> wkst , 7 ) . 'days' );
2015-06-29 11:45:39 +03:00
list ( $year , $month , $day , $hour , $minute , $second ) = explode ( ' ' , $tmp -> format ( 'Y n j G i s' ));
2015-06-26 17:02:29 +03:00
unset ( $tmp );
2015-06-23 11:31:18 +03:00
}
else {
2015-07-02 17:12:48 +03:00
list ( $year , $month , $day , $hour , $minute , $second ) = explode ( ' ' , $dtstart -> format ( 'Y n j G i s' ));
2015-06-26 17:02:29 +03:00
}
2015-06-29 11:45:39 +03:00
// remove leading zeros
$minute = ( int ) $minute ;
$second = ( int ) $second ;
2015-06-26 17:02:29 +03:00
}
2015-06-30 18:19:09 +03:00
// we initialize the timeset
2015-06-26 17:02:29 +03:00
if ( $timeset == null ) {
2015-06-29 11:45:39 +03:00
if ( $this -> freq < self :: HOURLY ) {
// daily, weekly, monthly or yearly
// we don't need to calculate a new timeset
2015-06-26 17:02:29 +03:00
$timeset = $this -> timeset ;
}
else {
2015-06-30 18:19:09 +03:00
// initialize empty if it's not going to occurs on the first iteration
2015-06-26 17:02:29 +03:00
if (
( $this -> freq >= self :: HOURLY && $this -> byhour && ! in_array ( $hour , $this -> byhour ))
|| ( $this -> freq >= self :: MINUTELY && $this -> byminute && ! in_array ( $minute , $this -> byminute ))
|| ( $this -> freq >= self :: SECONDLY && $this -> bysecond && ! in_array ( $second , $this -> bysecond ))
) {
$timeset = array ();
}
else {
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
}
2015-06-23 11:31:18 +03:00
}
}
2015-06-29 11:45:39 +03:00
// while (true) {
$max_cycles = self :: $REPEAT_CYCLES [ $this -> freq <= self :: DAILY ? $this -> freq : self :: DAILY ];
for ( $i = 0 ; $i < $max_cycles ; $i ++ ) {
2015-06-23 11:31:18 +03:00
// 1. get an array of all days in the next interval (day, month, week, etc.)
// we filter out from this array all days that do not match the BYXXX conditions
// to speed things up, we use days of the year (day numbers) instead of date
2015-06-30 18:19:09 +03:00
if ( $dayset === null ) {
2015-06-23 11:31:18 +03:00
// rebuild the various masks and converters
// these arrays will allow fast date operations
// without relying on date() methods
2015-06-26 17:02:29 +03:00
if ( empty ( $masks ) || $masks [ 'year' ] != $year || $masks [ 'month' ] != $month ) {
$masks = array ( 'year' => '' , 'month' => '' );
// only if year has changed
if ( $masks [ 'year' ] != $year ) {
$masks [ 'leap_year' ] = is_leap_year ( $year );
$masks [ 'year_len' ] = 365 + ( int ) $masks [ 'leap_year' ];
$masks [ 'next_year_len' ] = 365 + is_leap_year ( $year + 1 );
2015-07-01 14:13:59 +03:00
$masks [ 'weekday_of_1st_yearday' ] = date_create ( $year . " -01-01 00:00:00 " ) -> format ( 'N' );
2015-06-26 17:02:29 +03:00
$masks [ 'yearday_to_weekday' ] = array_slice ( self :: $WEEKDAY_MASK , $masks [ 'weekday_of_1st_yearday' ] - 1 );
if ( $masks [ 'leap_year' ] ) {
$masks [ 'yearday_to_month' ] = self :: $MONTH_MASK_366 ;
$masks [ 'yearday_to_monthday' ] = self :: $MONTHDAY_MASK_366 ;
$masks [ 'yearday_to_monthday_negative' ] = self :: $NEGATIVE_MONTHDAY_MASK_366 ;
$masks [ 'last_day_of_month' ] = self :: $LAST_DAY_OF_MONTH_366 ;
}
else {
$masks [ 'yearday_to_month' ] = self :: $MONTH_MASK ;
$masks [ 'yearday_to_monthday' ] = self :: $MONTHDAY_MASK ;
$masks [ 'yearday_to_monthday_negative' ] = self :: $NEGATIVE_MONTHDAY_MASK ;
$masks [ 'last_day_of_month' ] = self :: $LAST_DAY_OF_MONTH ;
}
if ( $this -> byweekno ) {
$this -> buildWeeknoMask ( $year , $month , $day , $masks );
}
}
// everytime month or year changes
2015-06-26 21:24:52 +03:00
if ( $this -> byweekday_nth ) {
2015-06-26 17:02:29 +03:00
$this -> buildNthWeekdayMask ( $year , $month , $day , $masks );
}
$masks [ 'year' ] = $year ;
$masks [ 'month' ] = $month ;
2015-06-23 11:31:18 +03:00
}
2015-06-26 17:02:29 +03:00
// calculate the current set
2015-06-30 18:19:09 +03:00
$dayset = $this -> getDaySet ( $year , $month , $day , $masks );
2015-07-01 12:23:39 +03:00
2015-06-23 11:31:18 +03:00
$filtered_set = array ();
2015-06-30 18:19:09 +03:00
foreach ( $dayset as $yearday ) {
2015-06-26 17:02:29 +03:00
if ( $this -> bymonth && ! in_array ( $masks [ 'yearday_to_month' ][ $yearday ], $this -> bymonth ) ) {
2015-06-23 11:31:18 +03:00
continue ;
}
2015-06-26 17:02:29 +03:00
if ( $this -> byweekno && ! isset ( $masks [ 'yearday_is_in_weekno' ][ $yearday ]) ) {
2015-06-23 11:31:18 +03:00
continue ;
}
if ( $this -> byyearday ) {
2015-06-26 17:02:29 +03:00
if ( $yearday < $masks [ 'year_len' ] ) {
if ( ! in_array ( $yearday + 1 , $this -> byyearday ) && ! in_array ( - $masks [ 'year_len' ] + $yearday , $this -> byyearday ) ) {
2015-06-23 11:31:18 +03:00
continue ;
}
}
2015-06-26 17:02:29 +03:00
else { // if ( ($yearday >= $masks['year_len']
if ( ! in_array ( $yearday + 1 - $masks [ 'year_len' ], $this -> byyearday ) && ! in_array ( - $masks [ 'next_year_len' ] + $yearday - $mask [ 'year_len' ], $this -> byyearday ) ) {
2015-06-23 11:31:18 +03:00
continue ;
}
}
}
2015-06-26 17:02:29 +03:00
if ( ( $this -> bymonthday || $this -> bymonthday_negative )
&& ! in_array ( $masks [ 'yearday_to_monthday' ][ $yearday ], $this -> bymonthday )
&& ! in_array ( $masks [ 'yearday_to_monthday_negative' ][ $yearday ], $this -> bymonthday_negative ) ) {
continue ;
}
if ( $this -> byweekday && ! in_array ( $masks [ 'yearday_to_weekday' ][ $yearday ], $this -> byweekday ) ) {
continue ;
}
2015-06-26 21:24:52 +03:00
if ( $this -> byweekday_nth && ! isset ( $masks [ 'yearday_is_nth_weekday' ][ $yearday ]) ) {
2015-06-26 17:02:29 +03:00
continue ;
}
$filtered_set [] = $yearday ;
2015-06-23 11:31:18 +03:00
}
2015-06-30 18:19:09 +03:00
$dayset = $filtered_set ;
2015-06-23 11:31:18 +03:00
2015-06-30 18:19:09 +03:00
// if BYSETPOS is set, we need to expand the timeset to filter by pos
// so we make a special loop to return while generating
if ( $this -> bysetpos && $timeset ) {
2015-06-27 14:12:57 +03:00
$filtered_set = array ();
2015-06-23 11:31:18 +03:00
foreach ( $this -> bysetpos as $pos ) {
2015-06-30 18:19:09 +03:00
$n = count ( $timeset );
2015-06-23 11:31:18 +03:00
if ( $pos < 0 ) {
2015-06-30 18:19:09 +03:00
$pos = $n * count ( $dayset ) + $pos ;
2015-06-23 11:31:18 +03:00
}
else {
$pos = $pos - 1 ;
}
2015-07-01 12:23:39 +03:00
2015-06-30 18:19:09 +03:00
$div = ( int ) ( $pos / $n ); // daypos
$mod = $pos % $n ; // timepos
if ( isset ( $dayset [ $div ]) && isset ( $timeset [ $mod ]) ) {
$yearday = $dayset [ $div ];
$time = $timeset [ $mod ];
// used as array key to ensure uniqueness
$tmp = $year . ':' . $yearday . ':' . $time [ 0 ] . ':' . $time [ 1 ] . ':' . $time [ 2 ];
if ( ! isset ( $filtered_set [ $tmp ]) ) {
$occurrence = \DateTime :: createFromFormat (
'Y z' ,
" $year $yearday "
);
$occurrence -> setTime ( $time [ 0 ], $time [ 1 ], $time [ 2 ]);
$filtered_set [ $tmp ] = $occurrence ;
}
2015-06-23 11:31:18 +03:00
}
}
2015-06-30 18:19:09 +03:00
sort ( $filtered_set );
$dayset = $filtered_set ;
2015-06-23 11:31:18 +03:00
}
}
// 2. loop, generate a valid date, and return the result (fake "yield")
// at the same time, we check the end condition and return null if
// we need to stop
2015-06-30 18:19:09 +03:00
if ( $this -> bysetpos && $timeset ) {
while ( ( $occurrence = current ( $dayset )) !== false ) {
2015-06-26 22:10:46 +03:00
2015-06-26 17:02:29 +03:00
// consider end conditions
if ( $this -> until && $occurrence > $this -> until ) {
2015-07-02 17:12:48 +03:00
$this -> total = $total ; // save total for count() cache
2015-06-26 17:02:29 +03:00
return null ;
}
2015-06-23 11:31:18 +03:00
2015-06-30 18:19:09 +03:00
next ( $dayset );
2015-07-02 17:12:48 +03:00
if ( $occurrence >= $dtstart ) { // ignore occurrences before DTSTART
2015-06-26 17:02:29 +03:00
$total += 1 ;
2015-07-02 17:12:48 +03:00
$this -> cache [] = $occurrence ;
2015-06-26 17:02:29 +03:00
return $occurrence ; // yield
}
2015-06-23 11:31:18 +03:00
}
2015-06-30 18:19:09 +03:00
}
else {
// normal loop, without BYSETPOS
while ( ( $yearday = current ( $dayset )) !== false ) {
$occurrence = \DateTime :: createFromFormat ( 'Y z' , " $year $yearday " );
while ( ( $time = current ( $timeset )) !== false ) {
$occurrence -> setTime ( $time [ 0 ], $time [ 1 ], $time [ 2 ]);
// consider end conditions
if ( $this -> until && $occurrence > $this -> until ) {
2015-07-02 17:12:48 +03:00
$this -> total = $total ; // save total for count() cache
2015-06-30 18:19:09 +03:00
return null ;
}
next ( $timeset );
2015-07-02 17:12:48 +03:00
if ( $occurrence >= $dtstart ) { // ignore occurrences before DTSTART
2015-06-30 18:19:09 +03:00
$total += 1 ;
2015-07-02 17:12:48 +03:00
$this -> cache [] = $occurrence ;
2015-06-30 18:19:09 +03:00
return $occurrence ; // yield
}
}
reset ( $timeset );
next ( $dayset );
}
2015-06-23 11:31:18 +03:00
}
// 3. we reset the loop to the next interval
2015-06-29 11:45:39 +03:00
$days_increment = 0 ;
2015-06-23 11:31:18 +03:00
switch ( $this -> freq ) {
2015-06-26 17:02:29 +03:00
case self :: YEARLY :
2015-06-29 11:45:39 +03:00
// we do not care about $month or $day not existing,
// they are not used in yearly frequency
2015-06-23 11:31:18 +03:00
$year = $year + $this -> interval ;
break ;
2015-06-26 17:02:29 +03:00
case self :: MONTHLY :
2015-06-29 11:45:39 +03:00
// we do not care about the day of the month not existing
// it is not used in monthly frequency
2015-06-23 11:31:18 +03:00
$month = $month + $this -> interval ;
if ( $month > 12 ) {
2015-06-29 11:45:39 +03:00
$div = ( int ) ( $month / 12 );
2015-06-23 11:31:18 +03:00
$mod = $month % 12 ;
$month = $mod ;
2015-06-29 11:45:39 +03:00
$year = $year + $div ;
2015-06-23 11:31:18 +03:00
if ( $month == 0 ) {
$month = 12 ;
$year = $year - 1 ;
}
}
break ;
2015-06-26 17:02:29 +03:00
case self :: WEEKLY :
2015-06-29 11:45:39 +03:00
$days_increment = $this -> interval * 7 ;
2015-06-23 11:31:18 +03:00
break ;
2015-06-26 17:02:29 +03:00
case self :: DAILY :
2015-06-29 11:45:39 +03:00
$days_increment = $this -> interval ;
2015-06-23 11:31:18 +03:00
break ;
2015-06-29 11:45:39 +03:00
// For the time frequencies, things are a little bit different.
// We could just add "$this->interval" hours, minutes or seconds
// to the current time, and go through the main loop again,
// but since the frequencies are so high and needs to much iteration
// it's actually a bit faster to have custom loops and only
// call the DateTime method at the very end.
2015-06-26 17:02:29 +03:00
case self :: HOURLY :
2015-06-30 18:19:09 +03:00
if ( empty ( $dayset ) ) {
2015-06-29 11:45:39 +03:00
// an empty set means that this day has been filtered out
// by one of the BYXXX rule. So there is no need to
// examine it any further, we know nothing is going to
// occur anyway.
// so we jump to one iteration right before next day
$hour += (( int ) (( 23 - $hour ) / $this -> interval )) * $this -> interval ;
}
$found = false ;
for ( $j = 0 ; $j < self :: $REPEAT_CYCLES [ self :: HOURLY ]; $j ++ ) {
$hour += $this -> interval ;
$div = ( int ) ( $hour / 24 );
$mod = $hour % 24 ;
if ( $div ) {
$hour = $mod ;
$days_increment += $div ;
}
if ( ! $this -> byhour || in_array ( $hour , $this -> byhour )) {
$found = true ;
break ;
}
}
if ( ! $found ) {
2015-07-02 17:12:48 +03:00
$this -> total = $total ; // save total for count cache
2015-06-29 11:45:39 +03:00
return null ; // stop the iterator
}
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
break ;
2015-06-26 17:02:29 +03:00
case self :: MINUTELY :
2015-06-30 18:19:09 +03:00
if ( empty ( $dayset ) ) {
2015-06-29 11:45:39 +03:00
$minute += (( int ) (( 1439 - ( $hour * 60 + $minute )) / $this -> interval )) * $this -> interval ;
}
$found = false ;
for ( $j = 0 ; $j < self :: $REPEAT_CYCLES [ self :: MINUTELY ]; $j ++ ) {
$minute += $this -> interval ;
$div = ( int ) ( $minute / 60 );
$mod = $minute % 60 ;
if ( $div ) {
$minute = $mod ;
$hour += $div ;
$div = ( int ) ( $hour / 24 );
$mod = $hour % 24 ;
if ( $div ) {
$hour = $mod ;
$days_increment += $div ;
}
}
if ( ( ! $this -> byhour || in_array ( $hour , $this -> byhour )) &&
( ! $this -> byminute || in_array ( $minute , $this -> byminute )) ) {
$found = true ;
break ;
}
}
if ( ! $found ) {
2015-07-02 17:12:48 +03:00
$this -> total = $total ; // save total for count cache
2015-06-29 11:45:39 +03:00
return null ; // stop the iterator
}
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
break ;
2015-06-26 17:02:29 +03:00
case self :: SECONDLY :
2015-06-30 18:19:09 +03:00
if ( empty ( $dayset ) ) {
2015-06-29 11:45:39 +03:00
$second += (( int ) (( 86399 - ( $hour * 3600 + $minute * 60 + $second )) / $this -> interval )) * $this -> interval ;
}
$found = false ;
for ( $j = 0 ; $j < self :: $REPEAT_CYCLES [ self :: SECONDLY ]; $j ++ ) {
$second += $this -> interval ;
$div = ( int ) ( $second / 60 );
$mod = $second % 60 ;
if ( $div ) {
$second = $mod ;
$minute += $div ;
$div = ( int ) ( $minute / 60 );
$mod = $minute % 60 ;
if ( $div ) {
$minute = $mod ;
$hour += $div ;
$div = ( int ) ( $hour / 24 );
$mod = $hour % 24 ;
if ( $div ) {
$hour = $mod ;
$days_increment += $div ;
}
}
}
if ( ( ! $this -> byhour || in_array ( $hour , $this -> byhour ) )
&& ( ! $this -> byminute || in_array ( $minute , $this -> byminute ) )
&& ( ! $this -> bysecond || in_array ( $second , $this -> bysecond ) ) ) {
$found = true ;
break ;
}
}
if ( ! $found ) {
2015-07-02 17:12:48 +03:00
$this -> total = $total ; // save total for count cache
2015-06-29 11:45:39 +03:00
return null ; // stop the iterator
}
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
break ;
}
// here we take a little shortcut from the Python version, by using DateTime
if ( $days_increment ) {
list ( $year , $month , $day ) = explode ( '-' , date_create ( " $year - $month - $day " ) -> modify ( " + $days_increment days " ) -> format ( 'Y-n-j' ));
2015-06-23 11:31:18 +03:00
}
2015-06-30 18:19:09 +03:00
$dayset = null ; // reset the loop
2015-06-23 11:31:18 +03:00
}
2015-07-02 17:12:48 +03:00
$this -> total = $total ; // save total for count cache
2015-06-29 11:45:39 +03:00
return null ; // stop the iterator
2015-06-23 11:31:18 +03:00
}
// constants
// Every mask is 7 days longer to handle cross-year weekly periods.
2015-07-07 19:16:46 +03:00
protected static $MONTH_MASK = array (
2015-06-23 11:31:18 +03:00
1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ,
2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 ,
3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 ,
4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 ,
5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 ,
6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 ,
7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 ,
8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 ,
9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 ,
10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 ,
11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 ,
12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 ,
1 , 1 , 1 , 1 , 1 , 1 , 1
);
2015-07-07 19:16:46 +03:00
protected static $MONTH_MASK_366 = array (
2015-06-23 11:31:18 +03:00
1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ,
2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 ,
3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 ,
4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 ,
5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 ,
6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 ,
7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 ,
8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 ,
9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 , 9 ,
10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 ,
11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 , 11 ,
12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 , 12 ,
1 , 1 , 1 , 1 , 1 , 1 , 1
);
2015-07-07 19:16:46 +03:00
protected static $MONTHDAY_MASK = array (
2015-06-23 11:31:18 +03:00
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7
);
2015-07-07 19:16:46 +03:00
protected static $MONTHDAY_MASK_366 = array (
2015-06-23 11:31:18 +03:00
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 ,
1 , 2 , 3 , 4 , 5 , 6 , 7
);
2015-07-07 19:16:46 +03:00
protected static $NEGATIVE_MONTHDAY_MASK = array (
2015-06-23 11:31:18 +03:00
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25
);
2015-07-07 19:16:46 +03:00
protected static $NEGATIVE_MONTHDAY_MASK_366 = array (
2015-06-23 11:31:18 +03:00
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25 , - 24 , - 23 , - 22 , - 21 , - 20 , - 19 , - 18 , - 17 , - 16 , - 15 , - 14 , - 13 , - 12 , - 11 , - 10 , - 9 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 ,
- 31 , - 30 , - 29 , - 28 , - 27 , - 26 , - 25
);
2015-07-07 19:16:46 +03:00
protected static $WEEKDAY_MASK = array (
2015-06-23 11:31:18 +03:00
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2 , 3 , 4 , 5 , 6 , 7
);
2015-07-07 19:16:46 +03:00
protected static $LAST_DAY_OF_MONTH_366 = array (
2015-06-23 11:31:18 +03:00
0 , 31 , 60 , 91 , 121 , 152 , 182 , 213 , 244 , 274 , 305 , 335 , 366
);
2015-07-07 19:16:46 +03:00
protected static $LAST_DAY_OF_MONTH = array (
2015-06-23 11:31:18 +03:00
0 , 31 , 59 , 90 , 120 , 151 , 181 , 212 , 243 , 273 , 304 , 334 , 365
);
2015-06-29 11:45:39 +03:00
/**
* Maximum number of cycles after which a calendar repeats itself . This
* is used to detect infinite loop : if no occurrence has been found
* after this numbers of cycles , we can abort .
*
* The Gregorian calendar cycle repeat completely every 400 years
* ( 146 , 097 days or 20 , 871 weeks ) .
* A smaller cycle would be 28 years ( 1 , 461 weeks ), but it only works
* if there is no dropped leap year in between .
* 2100 will be a dropped leap year , but I 'm going to assume it' s not
* going to be a problem anytime soon , so at the moment I use the 28 years
* cycle .
*/
2015-07-07 19:16:46 +03:00
protected static $REPEAT_CYCLES = array (
2015-06-29 11:45:39 +03:00
// self::YEARLY => 400,
// self::MONTHLY => 4800,
// self::WEEKLY => 20871,
// self::DAILY => 146097, // that's a lot of cycles, it takes a few seconds to detect infinite loop
self :: YEARLY => 28 ,
self :: MONTHLY => 336 ,
self :: WEEKLY => 1461 ,
self :: DAILY => 10227 ,
self :: HOURLY => 24 ,
self :: MINUTELY => 1440 ,
self :: SECONDLY => 86400 // that's a lot of cycles too
);
2015-07-07 19:16:46 +03:00
// i18n methods (could be moved into a separate class, since it's not always necessary)
2015-07-08 12:40:45 +03:00
/**
* Stores translations once loaded ( so we don ' t have to reload them all the time )
*/
static protected $i18n = array ();
static protected $intl_loaded = null ;
2015-07-07 19:16:46 +03:00
/**
* Select a translation in $array based on the value of $n
* @ return string
*/
static protected function i18nSelect ( $array , $n )
{
if ( ! is_array ( $array ) ) {
return $array ;
}
if ( array_key_exists ( $n , $array ) ) {
return $array [ $n ];
}
elseif ( array_key_exists ( 'else' , $array ) ) {
return $array [ 'else' ];
}
else {
return '' ; // or throw?
}
}
/**
* Create a comma - separated list , with the last item added with an " and "
* Example : Monday , Tuesday and Friday
* @ return string
*/
static protected function i18nList ( array $array , $and = 'and' )
{
if ( count ( $array ) > 1 ) {
$last = array_splice ( $array , - 1 );
return sprintf (
'%s %s %s' ,
implode ( ', ' , $array ),
$and ,
implode ( '' , $last )
);
}
else {
return $array [ 0 ];
}
}
/**
* Load a translation file in memory .
2015-07-08 12:40:45 +03:00
* Will load the basic first ( e . g . " en " ) and then the region - specific if any
* ( e . g . " en_GB " ), merging as necessary .
* So region - specific translation files don ' t need to redefine every strings .
2015-07-07 19:16:46 +03:00
*/
static protected function i18nLoad ( $locale )
{
2015-07-08 12:40:45 +03:00
if ( ! preg_match ( '/^([a-z]{2})(_[A-Z]{2})?$/' , $locale , $matches ) ) {
2015-11-11 14:18:02 -05:00
throw new \InvalidArgumentException ( 'The locale option does not look like a valid locale: ' . $locale );
2015-07-08 12:40:45 +03:00
}
$files = array ();
if ( isset ( $matches [ 2 ]) ) {
$files [] = $matches [ 1 ];
}
$files [] = $locale ;
$result = array ();
foreach ( $files as $file ) {
$path = __DIR__ . " /i18n/ $file .php " ;
if ( isset ( self :: $i18n [ $file ]) ) {
$result = array_merge ( $result , self :: $i18n [ $file ]);
}
elseif ( is_file ( $path ) && is_readable ( $path ) ) {
self :: $i18n [ $file ] = include $path ;
$result = array_merge ( $result , self :: $i18n [ $file ]);
}
else {
self :: $i18n [ $file ] = array ();
}
}
if ( empty ( $result ) ) {
2015-07-08 15:05:44 +03:00
throw new \InvalidArgumentException ( " Failed to load translations for ' $locale ' " );
2015-07-08 12:40:45 +03:00
}
return $result ;
2015-07-07 19:16:46 +03:00
}
/**
* Format a rule in a human readable string
2015-07-08 12:40:45 +03:00
* intl extension is required .
2015-07-07 19:16:46 +03:00
*/
public function humanReadable ( array $opt = array ())
{
2015-07-08 12:40:45 +03:00
if ( self :: $intl_loaded === null ) {
self :: $intl_loaded = extension_loaded ( 'intl' );
}
$default_opt = array (
2015-07-07 19:16:46 +03:00
'locale' => \Locale :: getDefault (),
'date_formatter' => null
2015-07-08 12:40:45 +03:00
);
if ( self :: $intl_loaded ) {
$default_opt [ 'date_format' ] = \IntlDateFormatter :: SHORT ;
if ( $this -> freq >= self :: SECONDLY || not_empty ( $this -> rule [ 'BYSECOND' ]) ) {
$default_opt [ 'time_format' ] = \IntlDateFormatter :: LONG ;
}
elseif ( $this -> freq >= self :: HOURLY || not_empty ( $this -> rule [ 'BYHOUR' ]) || not_empty ( $this -> rule [ 'BYMINUTE' ]) ) {
$default_opt [ 'time_format' ] = \IntlDateFormatter :: SHORT ;
}
else {
$default_opt [ 'time_format' ] = \IntlDateFormatter :: NONE ;
}
}
$opt = array_merge ( $default_opt , $opt );
if ( $opt [ 'date_formatter' ] && ! is_callable ( $opt [ 'date_formatter' ]) ) {
throw new \InvalidArgumentException ( 'The option date_formatter must callable' );
}
2015-07-07 19:16:46 +03:00
if ( ! $opt [ 'date_formatter' ] ) {
2015-07-08 12:40:45 +03:00
if ( self :: $intl_loaded ) {
$formatter = \IntlDateFormatter :: create (
$opt [ 'locale' ],
$opt [ 'date_format' ],
$opt [ 'time_format' ],
$this -> dtstart -> getTimezone () -> getName ()
);
$opt [ 'date_formatter' ] = function ( $date ) use ( $formatter ) {
return $formatter -> format ( $date );
};
}
else {
$opt [ 'date_formatter' ] = function ( $date ) {
return $date -> format ( 'Y-m-d H:i:s' );
};
}
2015-07-07 19:16:46 +03:00
}
$i18n = self :: i18nLoad ( $opt [ 'locale' ]);
$parts = array (
'freq' => '' ,
'byweekday' => '' ,
'bymonth' => '' ,
'byweekno' => '' ,
'byyearday' => '' ,
'bymonthday' => '' ,
'byhour' => '' ,
'byminute' => '' ,
'bysecond' => '' ,
'bysetpos' => ''
);
// Every (INTERVAL) FREQ...
2015-07-08 12:40:45 +03:00
$freq_str = strtolower ( array_search ( $this -> freq , self :: $frequencies ));
2015-07-07 19:16:46 +03:00
$parts [ 'freq' ] = strtr (
2015-07-08 12:40:45 +03:00
self :: i18nSelect ( $i18n [ $freq_str ], $this -> interval ),
2015-07-07 19:16:46 +03:00
array (
'%{interval}' => $this -> interval
)
);
// BYXXX rules
2015-07-08 15:05:44 +03:00
if ( not_empty ( $this -> rule [ 'BYMONTH' ]) ) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> bymonth ;
foreach ( $tmp as & $value ) {
$value = $i18n [ 'months' ][ $value ];
}
2015-07-08 12:40:45 +03:00
$parts [ 'bymonth' ] = strtr ( self :: i18nSelect ( $i18n [ 'bymonth' ], count ( $tmp )), array (
2015-07-07 19:16:46 +03:00
'%{months}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
}
2015-07-08 15:05:44 +03:00
if ( not_empty ( $this -> rule [ 'BYWEEKNO' ]) ) {
2015-07-07 19:16:46 +03:00
// XXX negative week number are not great here
$tmp = $this -> byweekno ;
foreach ( $tmp as & $value ) {
$value = strtr ( $i18n [ 'nth_weekno' ], array (
'%{n}' => $value
));
}
$parts [ 'byweekno' ] = strtr (
self :: i18nSelect ( $i18n [ 'byweekno' ], count ( $this -> byweekno )),
array (
'%{weeks}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
)
);
}
2015-07-08 15:05:44 +03:00
if ( not_empty ( $this -> rule [ 'BYYEARDAY' ]) ) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> byyearday ;
foreach ( $tmp as & $value ) {
$value = strtr ( self :: i18nSelect ( $i18n [ $value > 0 ? 'nth_yearday' : '-nth_yearday' ], $value ), array (
'%{n}' => abs ( $value )
));
}
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'byyearday' ], count ( $tmp )), array (
2015-07-07 19:16:46 +03:00
'%{yeardays}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
// ... of the month
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'x_of_the_y' ], 'yearly' ), array (
2015-07-07 19:16:46 +03:00
'%{x}' => $tmp
));
$parts [ 'byyearday' ] = $tmp ;
}
2015-07-08 15:05:44 +03:00
if ( not_empty ( $this -> rule [ 'BYMONTHDAY' ]) ) {
2015-07-07 19:16:46 +03:00
$parts [ 'bymonthday' ] = array ();
if ( $this -> bymonthday ) {
$tmp = $this -> bymonthday ;
foreach ( $tmp as & $value ) {
$value = strtr ( self :: i18nSelect ( $i18n [ 'nth_monthday' ], $value ), array (
'%{n}' => $value
));
}
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'bymonthday' ], count ( $tmp )), array (
2015-07-07 19:16:46 +03:00
'%{monthdays}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
// ... of the month
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'x_of_the_y' ], 'monthly' ), array (
2015-07-07 19:16:46 +03:00
'%{x}' => $tmp
));
$parts [ 'bymonthday' ][] = $tmp ;
}
if ( $this -> bymonthday_negative ) {
$tmp = $this -> bymonthday_negative ;
foreach ( $tmp as & $value ) {
2015-07-08 12:40:45 +03:00
$value = strtr ( self :: i18nSelect ( $i18n [ '-nth_monthday' ], $value ), array (
2015-07-07 19:16:46 +03:00
'%{n}' => - $value
));
}
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'bymonthday' ], count ( $tmp )), array (
2015-07-07 19:16:46 +03:00
'%{monthdays}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
// ... of the month
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'x_of_the_y' ], 'monthly' ), array (
2015-07-07 19:16:46 +03:00
'%{x}' => $tmp
));
$parts [ 'bymonthday' ][] = $tmp ;
}
2015-07-08 12:40:45 +03:00
$parts [ 'bymonthday' ] = implode ( ' ' . $i18n [ 'and' ], $parts [ 'bymonthday' ]);
2015-07-07 19:16:46 +03:00
}
2015-07-08 15:05:44 +03:00
if ( not_empty ( $this -> rule [ 'BYDAY' ]) ) {
2015-07-07 19:16:46 +03:00
$parts [ 'byweekday' ] = array ();
if ( $this -> byweekday ) {
$tmp = $this -> byweekday ;
foreach ( $tmp as & $value ) {
$value = $i18n [ 'weekdays' ][ $value ];
}
2015-07-08 12:40:45 +03:00
$parts [ 'byweekday' ][] = strtr ( self :: i18nSelect ( $i18n [ 'byweekday' ], count ( $tmp )), array (
2015-07-07 19:16:46 +03:00
'%{weekdays}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
}
if ( $this -> byweekday_nth ) {
$tmp = $this -> byweekday_nth ;
foreach ( $tmp as & $value ) {
list ( $day , $n ) = $value ;
$value = strtr ( self :: i18nSelect ( $i18n [ $n > 0 ? 'nth_weekday' : '-nth_weekday' ], $n ), array (
'%{weekday}' => $i18n [ 'weekdays' ][ $day ],
'%{n}' => abs ( $n )
));
}
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'byweekday' ], count ( $tmp )), array (
2015-07-07 19:16:46 +03:00
'%{weekdays}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
// ... of the year|month
2015-07-08 12:40:45 +03:00
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'x_of_the_y' ], $freq_str ), array (
2015-07-07 19:16:46 +03:00
'%{x}' => $tmp
));
$parts [ 'byweekday' ][] = $tmp ;
}
2015-07-08 12:40:45 +03:00
$parts [ 'byweekday' ] = implode ( ' ' . $i18n [ 'and' ], $parts [ 'byweekday' ]);
2015-07-07 19:16:46 +03:00
}
2015-07-08 12:40:45 +03:00
if ( not_empty ( $this -> rule [ 'BYHOUR' ]) ) {
$tmp = $this -> byhour ;
foreach ( $tmp as & $value ) {
$value = strtr ( $i18n [ 'nth_hour' ], array (
'%{n}' => $value
));
}
$parts [ 'byhour' ] = strtr ( self :: i18nSelect ( $i18n [ 'byhour' ], count ( $tmp )), array (
'%{hours}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
2015-07-07 19:16:46 +03:00
}
2015-07-08 12:40:45 +03:00
if ( not_empty ( $this -> rule [ 'BYMINUTE' ]) ) {
$tmp = $this -> byminute ;
foreach ( $tmp as & $value ) {
$value = strtr ( $i18n [ 'nth_minute' ], array (
'%{n}' => $value
));
}
$parts [ 'byminute' ] = strtr ( self :: i18nSelect ( $i18n [ 'byminute' ], count ( $tmp )), array (
'%{minutes}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
2015-07-07 19:16:46 +03:00
}
2015-07-08 12:40:45 +03:00
if ( not_empty ( $this -> rule [ 'BYSECOND' ]) ) {
$tmp = $this -> bysecond ;
foreach ( $tmp as & $value ) {
$value = strtr ( $i18n [ 'nth_second' ], array (
'%{n}' => $value
));
}
$parts [ 'bysecond' ] = strtr ( self :: i18nSelect ( $i18n [ 'bysecond' ], count ( $tmp )), array (
'%{seconds}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
2015-07-07 19:16:46 +03:00
}
if ( $this -> bysetpos ) {
2015-07-08 12:40:45 +03:00
$tmp = $this -> bysetpos ;
foreach ( $tmp as & $value ) {
$value = strtr ( self :: i18nSelect ( $i18n [ $value > 0 ? 'nth_setpos' : '-nth_setpos' ], $value ), array (
'%{n}' => abs ( $value )
));
}
$tmp = strtr ( self :: i18nSelect ( $i18n [ 'bysetpos' ], count ( $tmp )), array (
'%{setpos}' => self :: i18nList ( $tmp , $i18n [ 'and' ])
));
$parts [ 'bysetpos' ] = $tmp ;
2015-07-07 19:16:46 +03:00
}
// from X
$parts [ 'start' ] = strtr ( $i18n [ 'dtstart' ], array (
2015-07-08 12:40:45 +03:00
'%{date}' => $opt [ 'date_formatter' ]( $this -> dtstart )
2015-07-07 19:16:46 +03:00
));
// to X, or N times, or indefinitely
if ( ! $this -> until && ! $this -> count ) {
$parts [ 'end' ] = $i18n [ 'infinite' ];
}
elseif ( $this -> until ) {
$parts [ 'end' ] = strtr ( $i18n [ 'until' ], array (
2015-07-08 12:40:45 +03:00
'%{date}' => $opt [ 'date_formatter' ]( $this -> until )
2015-07-07 19:16:46 +03:00
));
}
elseif ( $this -> count ) {
$parts [ 'end' ] = strtr (
self :: i18nSelect ( $i18n [ 'count' ], $this -> count ),
array (
'%{count}' => $this -> count
)
);
}
// $str = strtr('%{frequency}%{byday}%{start}%{end}', array(
// '%{frequency}' => $parts['frequency'],
// '%{start}' => $parts['start'],
// '%{end}' => $parts['end'],
// '%{byday}' => $parts['byday'],
// ));
$parts = array_filter ( $parts );
2015-07-08 12:40:45 +03:00
$str = implode ( '' , $parts );
2015-07-07 19:16:46 +03:00
return $str ;
}
2015-11-11 14:18:02 -05:00
}