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 ;
/**
2016-06-30 17:01:20 +03:00
* Check that a variable is not empty .
2015-06-26 22:10:46 +03:00
*
2016-06-30 17:01:20 +03:00
* 0 and '0' are considered NOT empty .
*
* @ param mixed $var Variable to be checked
2015-06-23 11:31:18 +03:00
* @ return bool
*/
function not_empty ( $var )
{
return ! empty ( $var ) || $var === 0 || $var === '0' ;
}
/**
2016-06-30 17:01:20 +03:00
* Python - like modulo .
2015-06-23 11:31:18 +03:00
*
* 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 ) .
2016-06-30 17:01:20 +03:00
*
* @ copyright 2006 The Closure Library Authors .
2015-06-23 11:31:18 +03:00
*/
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 ;
}
/**
2018-10-18 16:31:13 -05:00
* Check if a year is a leap year .
2016-06-30 17:01:20 +03:00
*
* @ param int $year The year to be checked .
2015-06-23 11:31:18 +03:00
* @ return bool
*/
function is_leap_year ( $year )
{
2019-01-14 13:09:43 +00:00
if ( $year % 4 !== 0 ) {
2015-06-23 11:31:18 +03:00
return false ;
}
2019-01-14 13:09:43 +00:00
if ( $year % 100 !== 0 ) {
2015-06-23 11:31:18 +03:00
return true ;
}
2019-01-14 13:09:43 +00:00
if ( $year % 400 !== 0 ) {
2015-06-23 11:31:18 +03:00
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 :
*
2017-01-06 16:03:00 +02:00
* - " 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 .
2016-06-30 17:01:20 +03:00
* Default is Monday ( 1 ) . In some countries it ' s Sunday ( 7 ) .
2017-01-06 16:03:00 +02:00
* - " weekno " = number of the week in the year ( ISO - 8601 )
2015-06-26 22:10:46 +03:00
*
* CAREFUL with this bug : https :// bugs . php . net / bug . php ? id = 62476
*
2016-06-30 17:01:20 +03:00
* @ link https :// tools . ietf . org / html / rfc5545
* @ link https :// labix . org / python - dateutil
2015-06-23 11:31:18 +03:00
*/
2016-03-21 22:43:38 +02:00
class RRule implements RRuleInterface
2015-06-23 11:31:18 +03:00
{
2019-01-13 13:40:21 +00:00
use RRuleTrait ;
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 ;
2016-06-30 17:01:20 +03:00
/**
2017-01-06 16:03:00 +02:00
* Frequency names .
* Used internally for conversion but public if a reference list is needed .
2016-06-30 17:01:20 +03:00
*/
2019-09-01 19:54:12 +01:00
const 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
2016-06-30 17:01:20 +03:00
/**
2017-01-06 16:03:00 +02:00
* Weekdays numbered from 1 ( ISO - 8601 or `date('N')` ) .
* Used internally but public if a reference list is needed .
2016-06-30 17:01:20 +03:00
*/
2019-09-01 19:54:12 +01:00
const WEEKDAYS = 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
2016-06-30 17:01:20 +03:00
/**
* @ var array original rule
*/
2015-06-23 11:31:18 +03:00
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 ();
2016-03-15 23:30:50 +02:00
///////////////////////////////////////////////////////////////////////////////
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 ) .
2016-06-30 17:01:20 +03:00
*
* @ param mixed $parts An assoc array of parts , or a RFC string .
2015-06-23 11:31:18 +03:00
*/
2017-05-06 13:35:15 +01:00
public function __construct ( $parts , $dtstart = null )
2015-06-23 11:31:18 +03:00
{
2019-01-14 13:09:43 +00:00
if ( is_string ( $parts )) {
2017-05-06 13:35:15 +01:00
$parts = RfcParser :: parseRRule ( $parts , $dtstart );
2015-07-07 19:16:46 +03:00
$parts = array_change_key_case ( $parts , CASE_UPPER );
}
else {
2019-01-14 13:09:43 +00:00
if ( $dtstart ) {
2017-05-06 13:35:15 +01:00
throw new \InvalidArgumentException ( '$dtstart argument has no effect if not constructing from a string' );
}
2019-01-14 13:09:43 +00:00
if ( is_array ( $parts )) {
2017-05-06 13:35:15 +01:00
$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-07-07 19:16:46 +03:00
}
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 );
2019-01-14 13:09:43 +00:00
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' ]);
2019-09-01 19:54:12 +01:00
if ( ! array_key_exists ( $parts [ 'WKST' ], self :: WEEKDAYS )) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException (
'The WKST rule part must be one of the following: '
2019-09-01 19:54:12 +01:00
. implode ( ', ' , array_keys ( self :: WEEKDAYS ))
2015-06-26 17:02:29 +03:00
);
2015-06-23 11:31:18 +03:00
}
2019-09-01 19:54:12 +01:00
$this -> wkst = self :: WEEKDAYS [ $parts [ 'WKST' ]];
2015-06-23 11:31:18 +03:00
// FREQ
2019-01-14 13:09:43 +00:00
if ( is_integer ( $parts [ 'FREQ' ])) {
if ( $parts [ 'FREQ' ] > self :: SECONDLY || $parts [ 'FREQ' ] < self :: YEARLY ) {
2015-07-02 17:12:48 +03:00
throw new \InvalidArgumentException (
'The FREQ rule part must be one of the following: '
2019-09-01 19:54:12 +01:00
. implode ( ', ' , array_keys ( self :: FREQUENCIES ))
2015-07-02 17:12:48 +03:00
);
}
$this -> freq = $parts [ 'FREQ' ];
}
else { // string
$parts [ 'FREQ' ] = strtoupper ( $parts [ 'FREQ' ]);
2019-09-01 19:54:12 +01:00
if ( ! array_key_exists ( $parts [ 'FREQ' ], self :: FREQUENCIES )) {
2015-07-02 17:12:48 +03:00
throw new \InvalidArgumentException (
'The FREQ rule part must be one of the following: '
2019-09-01 19:54:12 +01:00
. implode ( ', ' , array_keys ( self :: FREQUENCIES ))
2015-07-02 17:12:48 +03:00
);
}
2019-09-01 19:54:12 +01:00
$this -> freq = self :: FREQUENCIES [ $parts [ 'FREQ' ]];
2015-06-23 11:31:18 +03:00
}
// INTERVAL
2019-01-14 13:09:43 +00:00
if ( filter_var ( $parts [ 'INTERVAL' ], FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => 1 ))) === false ) {
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
2019-01-14 13:09:43 +00:00
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 {
2017-05-07 14:02:05 +01:00
$this -> dtstart = new \DateTime (); // for PHP 7.1+ this contains microseconds which causes many problems
2019-01-14 13:09:43 +00:00
if ( version_compare ( PHP_VERSION , '7.1.0' ) >= 0 ) {
2017-05-07 14:02:05 +01:00
// remove microseconds
$this -> dtstart -> setTime (
$this -> dtstart -> format ( 'H' ),
$this -> dtstart -> format ( 'i' ),
$this -> dtstart -> format ( 's' ),
0
);
}
2015-06-23 11:31:18 +03:00
}
// UNTIL (optional)
2019-01-14 13:09:43 +00:00
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)
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'COUNT' ])) {
if ( filter_var ( $parts [ 'COUNT' ], FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => 1 ))) === false ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'COUNT must be a positive integer (> 0)' );
}
$this -> count = ( int ) $parts [ 'COUNT' ];
}
2019-01-14 13:09:43 +00:00
if ( $this -> until && $this -> count ) {
2015-07-07 19:16:46 +03:00
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
2019-01-14 13:09:43 +00:00
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 :
2019-01-14 13:09:43 +00: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 :
2019-09-01 19:54:12 +01:00
$parts [ 'BYDAY' ] = array ( array_search ( $this -> dtstart -> format ( 'N' ), self :: WEEKDAYS ));
2015-06-23 11:31:18 +03:00
break ;
}
}
// BYDAY (translated to byweekday for convenience)
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYDAY' ])) {
if ( ! is_array ( $parts [ 'BYDAY' ])) {
2015-06-23 11:31:18 +03:00
$parts [ 'BYDAY' ] = explode ( ',' , $parts [ 'BYDAY' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byweekday = array ();
$this -> byweekday_nth = array ();
2019-01-14 13:09:43 +00: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 );
2019-09-01 19:54:12 +01:00
if ( ! $valid || ( not_empty ( $matches [ 1 ]) && ( $matches [ 1 ] == 0 || $matches [ 1 ] > 53 || $matches [ 1 ] < - 53 )) || ! array_key_exists ( $matches [ 2 ], self :: WEEKDAYS )) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'Invalid BYDAY value: ' . $value );
}
2015-06-26 17:02:29 +03:00
2019-01-14 13:09:43 +00:00
if ( $matches [ 1 ]) {
2019-09-01 19:54:12 +01:00
$this -> byweekday_nth [] = array ( self :: WEEKDAYS [ $matches [ 2 ]], ( int ) $matches [ 1 ]);
2015-06-23 11:31:18 +03:00
}
else {
2019-09-01 19:54:12 +01:00
$this -> byweekday [] = self :: WEEKDAYS [ $matches [ 2 ]];
2015-06-23 11:31:18 +03:00
}
}
2019-01-14 13:09:43 +00:00
if ( ! empty ( $this -> byweekday_nth )) {
if ( ! ( $this -> freq === self :: MONTHLY || $this -> freq === self :: YEARLY )) {
2015-06-26 17:02:29 +03:00
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
}
2019-01-14 13:09:43 +00:00
if ( $this -> freq === self :: YEARLY && not_empty ( $parts [ 'BYWEEKNO' ])) {
2015-06-26 17:02:29 +03:00
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.
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYMONTHDAY' ])) {
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.' );
}
2019-01-14 13:09:43 +00:00
if ( ! is_array ( $parts [ 'BYMONTHDAY' ])) {
2015-06-23 11:31:18 +03:00
$parts [ 'BYMONTHDAY' ] = explode ( ',' , $parts [ 'BYMONTHDAY' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bymonthday = array ();
$this -> bymonthday_negative = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYMONTHDAY' ] as $value ) {
if ( ! $value || filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => - 31 , 'max_range' => 31 ))) === false ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'Invalid BYMONTHDAY value: ' . $value . ' (valid values are 1 to 31 or -31 to -1)' );
}
2017-05-09 17:01:08 +01:00
$value = ( int ) $value ;
2019-01-14 13:09:43 +00:00
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
}
}
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYYEARDAY' ])) {
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.' );
}
2019-01-14 13:09:43 +00:00
if ( ! is_array ( $parts [ 'BYYEARDAY' ])) {
2015-06-23 11:31:18 +03:00
$parts [ 'BYYEARDAY' ] = explode ( ',' , $parts [ 'BYYEARDAY' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bysetpos = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYYEARDAY' ] as $value ) {
if ( ! $value || filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => - 366 , 'max_range' => 366 ))) === false ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'Invalid BYSETPOS value: ' . $value . ' (valid values are 1 to 366 or -366 to -1)' );
}
2017-05-09 17:01:08 +01:00
$this -> byyearday [] = ( int ) $value ;
2015-06-23 11:31:18 +03:00
}
}
// BYWEEKNO
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYWEEKNO' ])) {
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.' );
}
2019-01-14 13:09:43 +00:00
if ( ! is_array ( $parts [ 'BYWEEKNO' ])) {
2015-06-23 11:31:18 +03:00
$parts [ 'BYWEEKNO' ] = explode ( ',' , $parts [ 'BYWEEKNO' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byweekno = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYWEEKNO' ] as $value ) {
if ( ! $value || filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => - 53 , 'max_range' => 53 ))) === false ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'Invalid BYWEEKNO value: ' . $value . ' (valid values are 1 to 53 or -53 to -1)' );
}
2017-05-09 17:01:08 +01:00
$this -> byweekno [] = ( int ) $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.
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYMONTH' ])) {
if ( ! is_array ( $parts [ 'BYMONTH' ])) {
2015-06-23 11:31:18 +03:00
$parts [ 'BYMONTH' ] = explode ( ',' , $parts [ 'BYMONTH' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bymonth = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYMONTH' ] as $value ) {
if ( filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => 1 , 'max_range' => 12 ))) === false ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'Invalid BYMONTH value: ' . $value );
}
2017-05-09 17:01:08 +01:00
$this -> bymonth [] = ( int ) $value ;
2015-06-23 11:31:18 +03:00
}
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYSETPOS' ])) {
if ( ! ( not_empty ( $parts [ 'BYWEEKNO' ]) || not_empty ( $parts [ 'BYYEARDAY' ])
2015-06-29 11:45:39 +03:00
|| not_empty ( $parts [ 'BYMONTHDAY' ]) || not_empty ( $parts [ 'BYDAY' ])
|| not_empty ( $parts [ 'BYMONTH' ]) || not_empty ( $parts [ 'BYHOUR' ])
2019-01-14 13:09:43 +00:00
|| not_empty ( $parts [ 'BYMINUTE' ]) || not_empty ( $parts [ 'BYSECOND' ]))) {
2015-06-29 11:45:39 +03:00
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
}
2019-01-14 13:09:43 +00:00
if ( ! is_array ( $parts [ 'BYSETPOS' ])) {
2015-06-23 11:31:18 +03:00
$parts [ 'BYSETPOS' ] = explode ( ',' , $parts [ 'BYSETPOS' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bysetpos = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYSETPOS' ] as $value ) {
if ( ! $value || filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => - 366 , 'max_range' => 366 ))) === false ) {
2015-06-23 11:31:18 +03:00
throw new \InvalidArgumentException ( 'Invalid BYSETPOS value: ' . $value . ' (valid values are 1 to 366 or -366 to -1)' );
}
2017-05-09 17:01:08 +01:00
$this -> bysetpos [] = ( int ) $value ;
2015-06-23 11:31:18 +03:00
}
}
2015-06-26 17:02:29 +03:00
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYHOUR' ])) {
if ( ! is_array ( $parts [ 'BYHOUR' ])) {
2015-06-26 17:02:29 +03:00
$parts [ 'BYHOUR' ] = explode ( ',' , $parts [ 'BYHOUR' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byhour = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYHOUR' ] as $value ) {
if ( filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => 0 , 'max_range' => 23 ))) === false ) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException ( 'Invalid BYHOUR value: ' . $value );
}
2017-05-09 17:01:08 +01:00
$this -> byhour [] = ( int ) $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
}
2019-01-14 13:09:43 +00: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
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYMINUTE' ])) {
if ( ! is_array ( $parts [ 'BYMINUTE' ])) {
2015-06-26 17:02:29 +03:00
$parts [ 'BYMINUTE' ] = explode ( ',' , $parts [ 'BYMINUTE' ]);
}
2015-06-27 14:12:57 +03:00
$this -> byminute = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYMINUTE' ] as $value ) {
if ( filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => 0 , 'max_range' => 59 ))) === false ) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException ( 'Invalid BYMINUTE value: ' . $value );
}
2017-05-09 17:01:08 +01:00
$this -> byminute [] = ( int ) $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
}
2019-01-14 13:09:43 +00: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
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $parts [ 'BYSECOND' ])) {
if ( ! is_array ( $parts [ 'BYSECOND' ])) {
2015-06-26 17:02:29 +03:00
$parts [ 'BYSECOND' ] = explode ( ',' , $parts [ 'BYSECOND' ]);
}
2015-06-27 14:12:57 +03:00
$this -> bysecond = array ();
2019-01-14 13:09:43 +00:00
foreach ( $parts [ 'BYSECOND' ] as $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
2019-01-14 13:09:43 +00:00
if ( filter_var ( $value , FILTER_VALIDATE_INT , array ( 'options' => array ( 'min_range' => 0 , 'max_range' => 60 ))) === false ) {
2015-06-26 17:02:29 +03:00
throw new \InvalidArgumentException ( 'Invalid BYSECOND value: ' . $value );
}
2017-05-09 17:01:08 +01:00
$this -> bysecond [] = ( int ) $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
}
2019-01-14 13:09:43 +00: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
}
2019-01-14 13:09:43 +00: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 ();
2019-01-14 13:09:43 +00:00
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
}
2016-08-31 15:35:01 +03:00
/**
* Return the internal rule array , as it was passed to the constructor .
*
* @ return array
*/
public function getRule ()
{
return $this -> rule ;
}
2016-06-30 17:01:20 +03:00
/**
* Magic string converter .
*
* @ see RRule :: rfcString ()
2016-08-17 17:14:35 +02:00
* @ return string a rfc string
2016-06-30 17:01:20 +03:00
*/
2015-07-07 19:16:46 +03:00
public function __toString ()
{
return $this -> rfcString ();
}
/**
* Format a rule according to RFC 5545
2016-06-30 17:01:20 +03:00
*
* @ param bool $include_timezone Wether to generate a rule with timezone identifier on DTSTART ( and UNTIL ) or not .
2015-07-07 19:16:46 +03:00
* @ return string
*/
2016-06-30 14:03:10 +03:00
public function rfcString ( $include_timezone = true )
2015-07-07 19:16:46 +03:00
{
$str = '' ;
2019-01-14 13:09:43 +00:00
if ( $this -> rule [ 'DTSTART' ]) {
if ( ! $include_timezone ) {
2016-06-30 14:03:10 +03:00
$str = sprintf (
" DTSTART:%s \n RRULE: " ,
$this -> dtstart -> format ( 'Ymd\THis' )
);
}
2016-06-30 13:27:06 +03:00
else {
2016-08-07 14:56:39 +03:00
$dtstart = clone $this -> dtstart ;
$timezone_name = $dtstart -> getTimeZone () -> getName ();
2019-01-14 13:09:43 +00:00
if ( strpos ( $timezone_name , ':' ) !== false ) {
2016-08-07 14:56:39 +03:00
// handle unsupported timezones like "+02:00"
// we convert them to UTC to generate a valid string
// note: there is possibly other weird timezones out there that we should catch
2016-08-07 15:05:26 +03:00
$dtstart -> setTimezone ( new \DateTimeZone ( 'UTC' ));
$timezone_name = 'UTC' ;
2016-08-07 14:56:39 +03:00
}
2019-01-14 13:09:43 +00:00
if ( in_array ( $timezone_name , array ( 'UTC' , 'GMT' , 'Z' ))) {
2016-08-07 14:56:39 +03:00
$str = sprintf (
" DTSTART:%s \n RRULE: " ,
$dtstart -> format ( 'Ymd\THis\Z' )
);
}
else {
$str = sprintf (
" DTSTART;TZID=%s:%s \n RRULE: " ,
$timezone_name ,
$dtstart -> format ( 'Ymd\THis' )
);
}
2016-06-30 13:27:06 +03:00
}
2015-07-07 19:16:46 +03:00
}
2015-07-08 12:40:45 +03:00
$parts = array ();
2019-01-14 13:09:43 +00:00
foreach ( $this -> rule as $key => $value ) {
if ( $key === 'DTSTART' ) {
2015-07-07 19:16:46 +03:00
continue ;
}
2019-01-14 13:09:43 +00:00
if ( $key === 'INTERVAL' && $value == 1 ) {
2015-07-07 19:16:46 +03:00
continue ;
}
2019-01-14 13:09:43 +00:00
if ( $key === 'WKST' && $value === 'MO' ) {
2015-07-07 19:16:46 +03:00
continue ;
}
2019-01-14 13:09:43 +00:00
if ( $key === 'UNTIL' && $value ) {
if ( ! $include_timezone ) {
2016-06-30 14:03:10 +03:00
$tmp = clone $this -> until ;
// put until on the same timezone as DTSTART
$tmp -> setTimeZone ( $this -> dtstart -> getTimezone ());
$parts [] = 'UNTIL=' . $tmp -> format ( 'Ymd\THis' );
}
else {
// according to the RFC, UNTIL must be in UTC
$tmp = clone $this -> until ;
$tmp -> setTimezone ( new \DateTimeZone ( 'UTC' ));
$parts [] = 'UNTIL=' . $tmp -> format ( 'Ymd\THis\Z' );
}
2015-07-07 19:16:46 +03:00
continue ;
}
2019-09-01 19:54:12 +01:00
if ( $key === 'FREQ' && $value && ! array_key_exists ( $value , self :: FREQUENCIES )) {
$frequency_key = array_search ( $value , self :: FREQUENCIES );
2016-08-17 23:14:38 +02:00
if ( $frequency_key !== false ) {
$value = $frequency_key ;
2016-08-17 17:14:35 +02:00
}
}
2019-01-14 13:09:43 +00:00
if ( $value !== NULL ) {
if ( is_array ( $value )) {
2015-07-07 19:16:46 +03:00
$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 )
2016-06-30 17:01:20 +03:00
*
* @ param string $string The rule to be parsed
2015-07-07 19:16:46 +03:00
* @ return array
2016-06-30 17:01:20 +03:00
*
* @ throws \InvalidArgumentException on error
2015-07-07 19:16:46 +03:00
*/
2017-04-11 16:44:50 +01:00
static public function parseRfcString ( $string )
2015-07-07 19:16:46 +03:00
{
2017-04-11 16:44:50 +01:00
trigger_error ( 'parseRfcString() is deprecated - use new RRule(), RRule::createFromRfcString() or \RRule\RfcParser::parseRRule() if necessary' , E_USER_DEPRECATED );
2017-05-15 10:14:37 +01:00
return RfcParser :: parseRRule ( $string );
2017-04-11 16:44:50 +01:00
}
/**
* Take a RFC 5545 string and returns either a RRule or a RSet .
*
2017-05-15 10:14:37 +01:00
* @ param string $string The RFC string
2017-04-11 16:44:50 +01:00
* @ param bool $force_rset Force a RSet to be returned .
* @ return RRule | RSet
*
* @ throws \InvalidArgumentException on error
*/
static public function createFromRfcString ( $string , $force_rset = false )
{
$class = '\RRule\RSet' ;
2019-01-14 13:09:43 +00:00
if ( ! $force_rset ) {
2017-04-11 16:44:50 +01:00
// try to detect if we have a RRULE or a set
2019-09-01 19:50:21 +01:00
$string = strtoupper ( $string );
2017-04-11 16:44:50 +01:00
$nb_rrule = substr_count ( $string , 'RRULE' );
2019-01-14 13:09:43 +00:00
if ( $nb_rrule == 0 ) {
2017-04-11 16:44:50 +01:00
$class = '\RRule\RRule' ;
}
2019-01-14 13:09:43 +00:00
elseif ( $nb_rrule > 1 ) {
2017-04-11 16:44:50 +01:00
$class = '\RRule\RSet' ;
2015-07-07 19:16:46 +03:00
}
else {
2017-04-11 16:44:50 +01:00
$class = '\RRule\RRule' ;
2019-01-14 13:09:43 +00:00
if ( strpos ( $string , 'EXDATE' ) !== false || strpos ( $string , 'RDATE' ) !== false || strpos ( $string , 'EXRULE' ) !== false ) {
2017-04-11 16:44:50 +01:00
$class = '\RRule\RSet' ;
2015-07-07 19:16:46 +03:00
}
}
}
2017-04-11 16:44:50 +01:00
return new $class ( $string );
2015-07-07 19:16:46 +03:00
}
/**
2016-06-30 17:01:20 +03:00
* Clear the cache .
*
* It isn ' t recommended to use this method while iterating .
*
2015-07-07 19:16:46 +03:00
* @ return $this
*/
2015-07-02 17:12:48 +03:00
public function clearCache ()
{
$this -> total = null ;
$this -> cache = array ();
return $this ;
}
2016-03-21 22:43:38 +02:00
///////////////////////////////////////////////////////////////////////////////
// RRule interface
/**
* Return true if the rrule has an end condition , false otherwise
*
* @ return bool
*/
public function isFinite ()
{
return $this -> count || $this -> until ;
}
/**
* Return true if the rrule has no end condition ( infite )
*
* @ return bool
*/
public function isInfinite ()
{
return ! $this -> count && ! $this -> until ;
}
2015-06-26 21:24:52 +03:00
/**
2016-06-30 17:01:20 +03:00
* Return true if $date is an occurrence .
2015-06-26 21:24:52 +03:00
*
* 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 .
*
2016-06-30 17:01:20 +03:00
* @ param mixed $date
2015-06-26 21:24:52 +03:00
* @ return bool
*/
public function occursAt ( $date )
{
2016-03-24 09:22:32 +02:00
$date = self :: parseDate ( $date );
// convert timezone to dtstart timezone for comparison
$date -> setTimezone ( $this -> dtstart -> getTimezone ());
2015-07-02 17:12:48 +03:00
2019-01-14 13:09:43 +00:00
if ( in_array ( $date , $this -> cache )) {
2015-07-02 17:12:48 +03:00
// in the cache (whether cache is complete or not)
return true ;
}
2019-01-14 13:09:43 +00:00
elseif ( $this -> total !== null ) {
2015-07-02 17:12:48 +03:00
// cache complete and not in cache
return false ;
2015-06-26 21:24:52 +03:00
}
// let's start with the obvious
2019-01-14 13:09:43 +00:00
if ( $date < $this -> dtstart || ( $this -> until && $date > $this -> until )) {
2015-06-26 21:24:52 +03:00
return false ;
}
// now the BYXXX rules (expect BYSETPOS)
2019-01-14 13:09:43 +00:00
if ( $this -> byhour && ! in_array ( $date -> format ( 'G' ), $this -> byhour )) {
2015-06-26 21:24:52 +03:00
return false ;
}
2019-01-14 13:09:43 +00:00
if ( $this -> byminute && ! in_array (( int ) $date -> format ( 'i' ), $this -> byminute )) {
2015-06-26 21:24:52 +03:00
return false ;
}
2019-01-14 13:09:43 +00: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' );
2019-09-01 19:54:12 +01:00
$masks [ 'yearday_to_weekday' ] = array_slice ( self :: WEEKDAY_MASK , $masks [ 'weekday_of_1st_yearday' ] - 1 );
2019-01-14 13:09:43 +00:00
if ( is_leap_year ( $year )) {
2015-06-26 21:24:52 +03:00
$masks [ 'year_len' ] = 366 ;
2019-09-01 19:54:12 +01:00
$masks [ 'last_day_of_month' ] = self :: LAST_DAY_OF_MONTH_366 ;
2015-06-26 21:24:52 +03:00
}
else {
$masks [ 'year_len' ] = 365 ;
2019-09-01 19:54:12 +01:00
$masks [ 'last_day_of_month' ] = self :: LAST_DAY_OF_MONTH ;
2015-06-26 21:24:52 +03:00
}
$month_len = $masks [ 'last_day_of_month' ][ $month ] - $masks [ 'last_day_of_month' ][ $month - 1 ];
2019-01-14 13:09:43 +00:00
if ( $this -> bymonth && ! in_array ( $month , $this -> bymonth )) {
2015-06-26 21:24:52 +03:00
return false ;
}
2019-01-14 13:09:43 +00:00
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
2019-01-14 13:09:43 +00:00
if ( ! in_array ( $day , $this -> bymonthday ) && ! in_array ( $monthday_negative , $this -> bymonthday_negative )) {
2015-06-26 21:24:52 +03:00
return false ;
}
}
2019-01-14 13:09:43 +00:00
if ( $this -> byyearday ) {
2015-06-26 21:24:52 +03:00
// caution here, yearday starts from 0 !
$yearday_negative = - 1 * ( $masks [ 'year_len' ] - $yearday );
2019-01-14 13:09:43 +00:00
if ( ! in_array ( $yearday + 1 , $this -> byyearday ) && ! in_array ( $yearday_negative , $this -> byyearday )) {
2015-06-26 21:24:52 +03:00
return false ;
}
}
2019-01-14 13:09:43 +00:00
if ( $this -> byweekday || $this -> byweekday_nth ) {
2015-06-26 21:24:52 +03:00
// we need to summon some magic here
$this -> buildNthWeekdayMask ( $year , $month , $day , $masks );
2016-07-13 11:08:46 +03:00
2019-01-14 13:09:43 +00:00
if ( ! in_array ( $weekday , $this -> byweekday ) && ! isset ( $masks [ 'yearday_is_nth_weekday' ][ $yearday ])) {
2015-06-26 21:24:52 +03:00
return false ;
}
}
2019-01-14 13:09:43 +00:00
if ( $this -> byweekno ) {
2015-06-26 21:24:52 +03:00
// more magic
$this -> buildWeeknoMask ( $year , $month , $day , $masks );
2019-01-14 13:09:43 +00:00
if ( ! isset ( $masks [ 'yearday_is_in_weekno' ][ $yearday ])) {
2015-06-26 21:24:52 +03:00
return false ;
}
}
// so now we have exhausted all the BYXXX rules (exept bysetpos),
// we still need to consider frequency and interval
2019-09-01 19:50:21 +01:00
list ( $start_year , $start_month ) = explode ( '-' , $this -> dtstart -> format ( 'Y-m' ));
2019-01-14 13:09:43 +00:00
switch ( $this -> freq ) {
2015-06-26 21:24:52 +03:00
case self :: YEARLY :
2019-01-14 13:09:43 +00:00
if (( $year - $start_year ) % $this -> interval !== 0 ) {
2015-06-26 21:24:52 +03:00
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
2019-01-14 13:09:43 +00: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 );
2019-01-14 13:09:43 +00:00
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 );
2019-01-14 13:09:43 +00:00
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 ;
2019-01-14 13:09:43 +00:00
if ( $diff % $this -> interval !== 0 ) {
2015-06-29 11:45:39 +03:00
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 ;
2019-01-14 13:09:43 +00:00
if ( $diff % $this -> interval !== 0 ) {
2015-06-29 11:45:39 +03:00
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 ;
2019-01-14 13:09:43 +00:00
if ( $diff % $this -> interval !== 0 ) {
2015-06-29 11:45:39 +03:00
return false ;
}
break ;
2019-10-05 01:21:40 +01:00
default :
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
2019-01-14 13:09:43 +00:00
if ( ! $this -> count && ! $this -> bysetpos ) {
2015-06-26 21:24:52 +03:00
return true ;
}
// so... as a fallback we have to loop
2019-01-14 13:09:43 +00:00
foreach ( $this as $occurrence ) {
if ( $occurrence == $date ) {
2015-06-26 21:24:52 +03:00
return true ; // lucky you!
}
2019-01-14 13:09:43 +00:00
if ( $occurrence > $date ) {
2015-06-26 21:24:52 +03:00
break ;
}
}
// we ended the loop without finding
return false ;
2015-06-23 11:31:18 +03:00
}
2016-03-15 23:30:50 +02:00
///////////////////////////////////////////////////////////////////////////////
2015-06-23 11:31:18 +03:00
// ArrayAccess interface
2016-06-30 17:01:20 +03:00
/**
* @ internal
*/
2015-06-23 11:31:18 +03:00
public function offsetExists ( $offset )
{
2017-01-06 16:16:44 +02:00
return is_numeric ( $offset ) && $offset >= 0 && ! is_float ( $offset ) && $offset < count ( $this );
2015-06-23 11:31:18 +03:00
}
2016-06-30 17:01:20 +03:00
/**
* @ internal
*/
2015-06-23 11:31:18 +03:00
public function offsetGet ( $offset )
{
2019-01-14 13:09:43 +00:00
if ( ! is_numeric ( $offset ) || $offset < 0 || is_float ( $offset )) {
2017-01-06 16:16:44 +02:00
throw new \InvalidArgumentException ( 'Illegal offset type: ' . gettype ( $offset ));
}
2019-01-14 13:09:43 +00:00
if ( isset ( $this -> cache [ $offset ])) {
2015-07-02 17:12:48 +03:00
// found in cache
2016-03-23 15:59:30 +02:00
return clone $this -> cache [ $offset ];
2015-07-02 17:12:48 +03:00
}
2019-01-14 13:09:43 +00:00
elseif ( $this -> total !== null ) {
2015-07-02 17:12:48 +03:00
// 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 ;
2019-01-14 13:09:43 +00:00
foreach ( $this as $occurrence ) {
if ( $i == $offset ) {
2015-07-02 17:12:48 +03:00
return $occurrence ;
}
$i ++ ;
2019-01-14 13:09:43 +00:00
if ( $i > $offset ) {
2015-07-02 17:12:48 +03:00
break ;
}
}
return null ;
2015-06-23 11:31:18 +03:00
}
2016-06-30 17:01:20 +03:00
/**
* @ internal
*/
2015-06-23 11:31:18 +03:00
public function offsetSet ( $offset , $value )
{
2016-02-25 12:35:51 +02:00
throw new \LogicException ( 'Setting a Date in a RRule is not supported' );
2015-06-23 11:31:18 +03:00
}
2016-06-30 17:01:20 +03:00
/**
* @ internal
*/
2015-06-23 11:31:18 +03:00
public function offsetUnset ( $offset )
{
2016-02-25 12:35:51 +02:00
throw new \LogicException ( 'Unsetting a Date in a RRule is not supported' );
2015-06-23 11:31:18 +03:00
}
2016-03-15 23:30:50 +02:00
///////////////////////////////////////////////////////////////////////////////
2015-07-02 17:12:48 +03:00
// Countable interface
/**
2016-03-15 23:30:50 +02:00
* Returns the number of occurrences in this rule . It will have go
2015-07-02 17:12:48 +03:00
* through the whole recurrence , if this hasn ' t been done before , which
* introduces a performance penality .
2016-06-30 17:01:20 +03:00
*
2015-07-02 17:12:48 +03:00
* @ return int
*/
public function count ()
{
2019-01-14 13:09:43 +00:00
if ( $this -> isInfinite ()) {
2015-07-02 17:12:48 +03:00
throw new \LogicException ( 'Cannot count an infinite recurrence rule.' );
}
2019-01-14 13:09:43 +00:00
if ( $this -> total === null ) {
foreach ( $this as $occurrence ) {}
2015-07-02 17:12:48 +03:00
}
return $this -> total ;
}
2016-03-15 23:30:50 +02:00
///////////////////////////////////////////////////////////////////////////////
2016-06-30 17:01:20 +03:00
// Internal methods
2015-07-07 19:16:46 +03:00
// where all the magic happens
2015-06-23 11:31:18 +03:00
/**
2016-06-30 17:01:20 +03:00
* Return an array of days of the year ( numbered from 0 to 365 )
2015-06-23 11:31:18 +03:00
* of the current timeframe ( year , month , week , day ) containing the current date
2016-06-30 17:01:20 +03:00
*
* @ param int $year
* @ param int $month
* @ param int $day
* @ param array $masks
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 )
{
2019-01-14 13:09:43 +00:00
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 ;
2019-01-14 13:09:43 +00:00
for ( $j = 0 ; $j < 7 ; $j ++ ) {
2015-06-23 11:31:18 +03:00
$set [] = $i ;
$i += 1 ;
2019-01-14 13:09:43 +00: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
}
}
/**
2016-06-30 17:01:20 +03:00
* Calculate the yeardays corresponding to each Nth weekday
2015-06-27 13:50:28 +03:00
* ( in BYDAY rule part ) .
2016-06-30 17:01:20 +03:00
*
2015-06-27 13:50:28 +03:00
* 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 ) .
2016-06-30 17:01:20 +03:00
*
2019-09-01 19:50:21 +01:00
* @ param int $year ( not used )
2016-06-30 17:01:20 +03:00
* @ param int $month
2019-09-01 19:50:21 +01:00
* @ param int $day ( not used )
2016-06-30 17:01:20 +03:00
* @ param array $masks
*
2019-09-01 19:50:21 +01:00
* @ return null ( modifies $masks 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
2019-01-14 13:09:43 +00:00
if ( $this -> byweekday_nth ) {
2015-06-23 11:31:18 +03:00
$ranges = array ();
2019-01-14 13:09:43 +00:00
if ( $this -> freq == self :: YEARLY ) {
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
}
}
2019-01-14 13:09:43 +00: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
}
2019-01-14 13:09:43 +00: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.
2019-01-14 13:09:43 +00:00
foreach ( $ranges as $tmp ) {
2015-06-23 11:31:18 +03:00
list ( $first , $last ) = $tmp ;
2019-01-14 13:09:43 +00:00
foreach ( $this -> byweekday_nth as $tmp ) {
2015-06-23 11:31:18 +03:00
list ( $weekday , $nth ) = $tmp ;
2019-01-14 13:09:43 +00:00
if ( $nth < 0 ) {
2015-06-23 11:31:18 +03:00
$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
2019-01-14 13:09:43 +00: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
/**
2016-06-30 17:01:20 +03:00
* Calculate the yeardays corresponding to the week number
2015-06-29 11:45:39 +03:00
* ( in the WEEKNO rule part ) .
2016-06-30 17:01:20 +03:00
*
2015-06-29 11:45:39 +03:00
* 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 .
2016-06-30 17:01:20 +03:00
*
* @ param int $year
2019-09-01 19:50:21 +01:00
* @ param int $month ( not used )
* @ param int $day ( not used )
2016-06-30 17:01:20 +03:00
* @ param array $masks
*
2019-09-01 19:50:21 +01:00
* @ return null ( modifies $masks )
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 ;
2019-01-14 13:09:43 +00:00
if ( $first_wkst >= 4 ) {
2015-06-26 17:02:29 +03:00
$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
2019-01-14 13:09:43 +00:00
foreach ( $this -> byweekno as $n ) {
if ( $n < 0 ) {
2015-06-26 17:02:29 +03:00
$n = $n + $nb_weeks + 1 ;
}
2019-01-14 13:09:43 +00:00
if ( $n <= 0 || $n > $nb_weeks ) {
2015-06-26 17:02:29 +03:00
continue ;
}
2019-01-14 13:09:43 +00:00
if ( $n > 1 ) {
2015-06-26 17:02:29 +03:00
$i = $first_wkst_offset + ( $n - 1 ) * 7 ;
2019-01-14 13:09:43 +00:00
if ( $first_wkst_offset != $first_wkst ) {
2015-06-26 17:02:29 +03:00
// 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)
2019-01-14 13:09:43 +00:00
for ( $j = 0 ; $j < 7 ; $j ++ ) {
2015-06-26 17:02:29 +03:00
$masks [ 'yearday_is_in_weekno' ][ $i ] = true ;
$i = $i + 1 ;
2019-01-14 13:09:43 +00:00
if ( $masks [ 'yearday_to_weekday' ][ $i ] == $this -> wkst ) {
2015-06-26 17:02:29 +03:00
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.
2019-01-14 13:09:43 +00:00
if ( in_array ( 1 , $this -> byweekno )) {
2015-06-26 17:02:29 +03:00
// Check week number 1 of next year as well
// TODO: Check -numweeks for next year.
$i = $first_wkst_offset + $nb_weeks * 7 ;
2019-01-14 13:09:43 +00:00
if ( $first_wkst_offset != $first_wkst ) {
2015-06-26 17:02:29 +03:00
$i = $i - ( 7 - $first_wkst );
}
2019-01-14 13:09:43 +00:00
if ( $i < $masks [ 'year_len' ]) {
2015-06-26 17:02:29 +03:00
// If week starts in next year, we don't care about it.
2019-01-14 13:09:43 +00:00
for ( $j = 0 ; $j < 7 ; $j ++ ) {
2015-06-26 17:02:29 +03:00
$masks [ 'yearday_is_in_weekno' ][ $i ] = true ;
$i += 1 ;
2019-01-14 13:09:43 +00:00
if ( $masks [ 'yearday_to_weekday' ][ $i ] == $this -> wkst ) {
2015-06-26 17:02:29 +03:00
break ;
}
}
}
}
2019-01-14 13:09:43 +00:00
if ( $first_wkst_offset ) {
2015-06-26 17:02:29 +03:00
// 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.
2019-01-14 13:09:43 +00:00
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 );
2019-01-14 13:09:43 +00:00
if ( $first_wkst_offset_last_year >= 4 ) {
2015-06-26 17:02:29 +03:00
$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 ;
}
2019-01-14 13:09:43 +00:00
if ( in_array ( $nb_weeks_last_year , $this -> byweekno )) {
for ( $i = 0 ; $i < $first_wkst_offset ; $i ++ ) {
2015-06-26 17:02:29 +03:00
$masks [ 'yearday_is_in_weekno' ][ $i ] = true ;
}
}
}
}
2015-06-29 11:45:39 +03:00
/**
2016-06-30 17:01:20 +03:00
* Build an array of every time of the day that matches the BYXXX time
* criteria .
*
* It will only process $this -> frequency at one time . So :
2015-06-30 18:19:09 +03:00
* - 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 .
2016-06-30 17:01:20 +03:00
*
* @ param int $hour
* @ param int $minute
* @ param int $second
*
2015-07-07 19:16:46 +03:00
* @ return array
2015-06-29 11:45:39 +03:00
*/
protected function getTimeSet ( $hour , $minute , $second )
{
2019-01-14 13:09:43 +00:00
switch ( $this -> freq ) {
2015-06-29 11:45:39 +03:00
case self :: HOURLY :
$set = array ();
2019-01-14 13:09:43 +00:00
foreach ( $this -> byminute as $minute ) {
foreach ( $this -> bysecond as $second ) {
2015-06-29 11:45:39 +03:00
// should we use another type?
$set [] = array ( $hour , $minute , $second );
}
}
// sort ?
return $set ;
case self :: MINUTELY :
$set = array ();
2019-01-14 13:09:43 +00:00
foreach ( $this -> bysecond as $second ) {
2015-06-29 11:45:39 +03:00
// 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
*
2019-09-01 19:50:21 +01:00
* The main idea is : a brute force loop testing all the dates , made fast by
* not relying on date () functions
2015-06-29 11:45:39 +03:00
*
* 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
2016-03-11 11:03:11 +02:00
* day that do no match BYXXX parts .
2015-06-29 11:45:39 +03:00
*
* 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
2016-03-11 11:03:11 +02:00
* their own special loops within the main loop , making the whole thing quite
2015-06-30 18:19:09 +03:00
* 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 .
2016-03-11 11:03:11 +02:00
*
2015-07-07 19:16:46 +03:00
* @ return \DateTime | null
2015-06-23 11:31:18 +03:00
*/
2019-01-13 10:00:43 +00:00
public function getIterator ()
2015-06-23 11:31:18 +03:00
{
2019-01-13 10:00:43 +00:00
$total = 0 ;
$occurrence = null ;
$dtstart = null ;
$dayset = null ;
2015-07-02 17:12:48 +03:00
// go through the cache first
2019-01-14 13:09:43 +00:00
foreach ( $this -> cache as $occurrence ) {
2019-01-13 10:00:43 +00:00
yield clone $occurrence ; // since DateTime is not immutable, avoid any problem
2015-06-23 11:31:18 +03:00
2019-01-13 10:00:43 +00:00
$total += 1 ;
2015-06-23 11:31:18 +03:00
}
2019-01-13 10:00:43 +00:00
// if the cache as been used up completely and we now there is nothing else,
// we can stop the generator
2019-01-14 13:09:43 +00:00
if ( $total === $this -> total ) {
2019-01-13 10:00:43 +00:00
return ; // end generator
2015-07-02 17:12:48 +03:00
}
2019-01-14 13:09:43 +00:00
if ( $occurrence ) {
2019-01-13 10:00:43 +00:00
$dtstart = clone $occurrence ; // since DateTime is not immutable, clone to avoid any problem
// so we skip the last occurrence of the cache
2019-01-14 13:09:43 +00:00
if ( $this -> freq === self :: SECONDLY ) {
2019-01-13 10:00:43 +00:00
$dtstart -> modify ( '+' . $this -> interval . 'second' );
2015-06-23 11:31:18 +03:00
}
else {
2019-01-13 10:00:43 +00:00
$dtstart -> modify ( '+1second' );
2015-06-26 17:02:29 +03:00
}
}
2019-01-14 13:09:43 +00:00
if ( $dtstart === null ) {
2019-01-13 10:00:43 +00:00
$dtstart = clone $this -> dtstart ;
}
2019-01-14 13:09:43 +00:00
if ( $this -> freq === self :: WEEKLY ) {
2019-01-13 10:00:43 +00:00
// we align the start date to the WKST, so we can then
// 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.
$tmp = clone $dtstart ;
$tmp -> modify ( '-' . pymod ( $dtstart -> format ( 'N' ) - $this -> wkst , 7 ) . 'days' );
list ( $year , $month , $day , $hour , $minute , $second ) = explode ( ' ' , $tmp -> format ( 'Y n j G i s' ));
unset ( $tmp );
}
else {
list ( $year , $month , $day , $hour , $minute , $second ) = explode ( ' ' , $dtstart -> format ( 'Y n j G i s' ));
}
// remove leading zeros
$minute = ( int ) $minute ;
$second = ( int ) $second ;
2015-06-30 18:19:09 +03:00
// we initialize the timeset
2019-01-14 13:09:43 +00:00
if ( $this -> freq < self :: HOURLY ) {
2019-01-13 10:00:43 +00:00
// daily, weekly, monthly or yearly
// we don't need to calculate a new timeset
$timeset = $this -> timeset ;
}
else {
// initialize empty if it's not going to occurs on the first iteration
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 ();
2015-06-26 17:02:29 +03:00
}
else {
2019-01-13 10:00:43 +00:00
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
2015-06-23 11:31:18 +03:00
}
}
2019-09-01 19:54:12 +01:00
$max_cycles = self :: REPEAT_CYCLES [ $this -> freq <= self :: DAILY ? $this -> freq : self :: DAILY ];
2019-01-14 13:09:43 +00:00
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
2019-01-14 13:09:43 +00: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
2019-01-14 13:09:43 +00:00
if ( empty ( $masks ) || $masks [ 'year' ] != $year || $masks [ 'month' ] != $month ) {
2016-03-21 21:55:40 +02:00
$masks = array ( 'year' => '' , 'month' => '' );
2015-06-26 17:02:29 +03:00
// only if year has changed
2019-01-14 13:09:43 +00:00
if ( $masks [ 'year' ] != $year ) {
2016-03-21 21:55:40 +02:00
$masks [ 'leap_year' ] = is_leap_year ( $year );
$masks [ 'year_len' ] = 365 + ( int ) $masks [ 'leap_year' ];
$masks [ 'weekday_of_1st_yearday' ] = date_create ( $year . " -01-01 00:00:00 " ) -> format ( 'N' );
2019-09-01 19:54:12 +01:00
$masks [ 'yearday_to_weekday' ] = array_slice ( self :: WEEKDAY_MASK , $masks [ 'weekday_of_1st_yearday' ] - 1 );
2019-01-14 13:09:43 +00:00
if ( $masks [ 'leap_year' ]) {
2019-09-01 19:54:12 +01:00
$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 ;
2015-06-26 17:02:29 +03:00
}
else {
2019-09-01 19:54:12 +01:00
$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 ;
2015-06-26 17:02:29 +03:00
}
2019-01-14 13:09:43 +00:00
if ( $this -> byweekno ) {
2016-03-21 21:55:40 +02:00
$this -> buildWeeknoMask ( $year , $month , $day , $masks );
2015-06-26 17:02:29 +03:00
}
}
// everytime month or year changes
2019-01-14 13:09:43 +00:00
if ( $this -> byweekday_nth ) {
2016-03-21 21:55:40 +02:00
$this -> buildNthWeekdayMask ( $year , $month , $day , $masks );
2015-06-26 17:02:29 +03:00
}
2016-03-21 21:55:40 +02:00
$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
2016-03-21 21:55:40 +02: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 ();
2019-01-13 10:00:43 +00:00
// filter out the days based on the BYXXX rules
2019-01-14 13:09:43 +00:00
foreach ( $dayset as $yearday ) {
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
2019-01-14 13:09:43 +00:00
if ( $this -> byweekno && ! isset ( $masks [ 'yearday_is_in_weekno' ][ $yearday ])) {
2015-06-23 11:31:18 +03:00
continue ;
}
2019-01-14 13:09:43 +00:00
if ( $this -> byyearday ) {
2019-09-01 19:50:21 +01:00
if ( ! in_array ( $yearday + 1 , $this -> byyearday ) && ! in_array ( - $masks [ 'year_len' ] + $yearday , $this -> byyearday )) {
continue ;
2015-06-23 11:31:18 +03:00
}
}
2019-01-14 13:09:43 +00:00
if (( $this -> bymonthday || $this -> bymonthday_negative )
2016-03-21 21:55:40 +02:00
&& ! in_array ( $masks [ 'yearday_to_monthday' ][ $yearday ], $this -> bymonthday )
2019-01-14 13:09:43 +00:00
&& ! in_array ( $masks [ 'yearday_to_monthday_negative' ][ $yearday ], $this -> bymonthday_negative )) {
2015-06-26 17:02:29 +03:00
continue ;
}
2019-01-14 13:09:43 +00:00
if (( $this -> byweekday || $this -> byweekday_nth )
2016-07-13 11:08:46 +03:00
&& ! in_array ( $masks [ 'yearday_to_weekday' ][ $yearday ], $this -> byweekday )
2019-01-14 13:09:43 +00:00
&& ! isset ( $masks [ 'yearday_is_nth_weekday' ][ $yearday ])) {
2015-06-26 17:02:29 +03:00
continue ;
}
2016-03-21 21:55:40 +02:00
$filtered_set [] = $yearday ;
2015-06-23 11:31:18 +03:00
}
2016-03-21 21:55:40 +02: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
2019-01-13 10:00:43 +00:00
// TODO this is not needed with a generator anymore
// we can yield directly within the loop
2019-01-14 13:09:43 +00:00
if ( $this -> bysetpos && $timeset ) {
2015-06-27 14:12:57 +03:00
$filtered_set = array ();
2019-01-14 13:09:43 +00:00
foreach ( $this -> bysetpos as $pos ) {
2016-03-21 21:55:40 +02:00
$n = count ( $timeset );
2019-01-14 13:09:43 +00:00
if ( $pos < 0 ) {
2016-03-21 21:55:40 +02: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
2019-01-14 13:09:43 +00:00
if ( isset ( $dayset [ $div ]) && isset ( $timeset [ $mod ])) {
2016-03-21 21:55:40 +02:00
$yearday = $dayset [ $div ];
$time = $timeset [ $mod ];
2015-06-30 18:19:09 +03:00
// used as array key to ensure uniqueness
2016-03-21 21:55:40 +02:00
$tmp = $year . ':' . $yearday . ':' . $time [ 0 ] . ':' . $time [ 1 ] . ':' . $time [ 2 ];
2019-01-14 13:09:43 +00:00
if ( ! isset ( $filtered_set [ $tmp ])) {
2015-06-30 18:19:09 +03:00
$occurrence = \DateTime :: createFromFormat (
'Y z' ,
2016-03-21 21:55:40 +02:00
" $year $yearday " ,
2016-03-11 15:42:34 +10:00
$this -> dtstart -> getTimezone ()
2015-06-30 18:19:09 +03:00
);
$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 );
2016-03-21 21:55:40 +02:00
$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
2019-01-14 13:09:43 +00:00
if ( $this -> bysetpos && $timeset ) {
2019-01-13 10:00:43 +00:00
// while ( ($occurrence = current($dayset)) !== false ) {
2019-01-14 13:09:43 +00:00
foreach ( $dayset as $occurrence ) {
2015-06-26 17:02:29 +03:00
// consider end conditions
2019-01-14 13:09:43 +00:00
if ( $this -> until && $occurrence > $this -> until ) {
2016-03-21 21:55:40 +02:00
$this -> total = $total ; // save total for count() cache
2019-01-13 10:00:43 +00:00
return ;
2015-06-26 17:02:29 +03:00
}
2015-06-23 11:31:18 +03:00
2019-01-13 10:00:43 +00:00
// next($dayset);
2019-01-14 13:09:43 +00:00
if ( $occurrence >= $dtstart ) { // ignore occurrences before DTSTART
if ( $this -> count && $total >= $this -> count ) {
2019-01-13 10:00:43 +00:00
$this -> total = $total ;
return ;
}
2016-03-21 21:55:40 +02:00
$total += 1 ;
2019-01-13 10:00:43 +00:00
$this -> cache [] = clone $occurrence ;
yield clone $occurrence ; // yield
2015-06-26 17:02:29 +03:00
}
2015-06-23 11:31:18 +03:00
}
2015-06-30 18:19:09 +03:00
}
else {
// normal loop, without BYSETPOS
2019-01-13 10:00:43 +00:00
// while ( ($yearday = current($dayset)) !== false ) {
2019-01-14 13:09:43 +00:00
foreach ( $dayset as $yearday ) {
2016-03-21 23:56:35 +02:00
$occurrence = \DateTime :: createFromFormat (
'Y z' ,
" $year $yearday " ,
$this -> dtstart -> getTimezone ()
);
2015-06-30 18:19:09 +03:00
2019-01-13 10:00:43 +00:00
// while ( ($time = current($timeset)) !== false ) {
2019-01-14 13:09:43 +00:00
foreach ( $timeset as $time ) {
2015-06-30 18:19:09 +03:00
$occurrence -> setTime ( $time [ 0 ], $time [ 1 ], $time [ 2 ]);
// consider end conditions
2019-01-14 13:09:43 +00:00
if ( $this -> until && $occurrence > $this -> until ) {
2016-03-21 21:55:40 +02:00
$this -> total = $total ; // save total for count() cache
2019-01-13 10:00:43 +00:00
return ;
2015-06-30 18:19:09 +03:00
}
2019-01-13 10:00:43 +00:00
// next($timeset);
2019-01-14 13:09:43 +00:00
if ( $occurrence >= $dtstart ) { // ignore occurrences before DTSTART
if ( $this -> count && $total >= $this -> count ) {
2019-01-13 10:00:43 +00:00
$this -> total = $total ;
return ;
}
2016-03-21 21:55:40 +02:00
$total += 1 ;
2019-01-13 10:00:43 +00:00
$this -> cache [] = clone $occurrence ;
yield clone $occurrence ; // yield
2015-06-30 18:19:09 +03:00
}
}
2019-01-13 10:00:43 +00:00
// reset($timeset);
// next($dayset);
2015-06-30 18:19:09 +03:00
}
2015-06-23 11:31:18 +03:00
}
// 3. we reset the loop to the next interval
2016-03-21 21:55:40 +02:00
$days_increment = 0 ;
2019-01-14 13:09:43 +00:00
switch ( $this -> freq ) {
2015-06-26 17:02:29 +03:00
case self :: YEARLY :
2016-03-21 21:55:40 +02:00
// we do not care about $month or $day not existing,
2015-06-29 11:45:39 +03:00
// they are not used in yearly frequency
2016-03-21 21:55:40 +02:00
$year = $year + $this -> interval ;
2015-06-23 11:31:18 +03:00
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
2016-03-21 21:55:40 +02:00
$month = $month + $this -> interval ;
2019-01-14 13:09:43 +00:00
if ( $month > 12 ) {
2016-03-21 21:55:40 +02:00
$div = ( int ) ( $month / 12 );
$mod = $month % 12 ;
$month = $mod ;
$year = $year + $div ;
2019-01-14 13:09:43 +00:00
if ( $month == 0 ) {
2016-03-21 21:55:40 +02:00
$month = 12 ;
$year = $year - 1 ;
2015-06-23 11:31:18 +03:00
}
}
break ;
2015-06-26 17:02:29 +03:00
case self :: WEEKLY :
2016-03-21 21:55:40 +02: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 :
2016-03-21 21:55:40 +02: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 :
2019-01-14 13:09:43 +00: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
2016-03-21 21:55:40 +02:00
$hour += (( int ) (( 23 - $hour ) / $this -> interval )) * $this -> interval ;
2015-06-29 11:45:39 +03:00
}
$found = false ;
2019-09-01 19:54:12 +01:00
for ( $j = 0 ; $j < self :: REPEAT_CYCLES [ self :: HOURLY ]; $j ++ ) {
2016-03-21 21:55:40 +02:00
$hour += $this -> interval ;
$div = ( int ) ( $hour / 24 );
$mod = $hour % 24 ;
2019-01-14 13:09:43 +00:00
if ( $div ) {
2016-03-21 21:55:40 +02:00
$hour = $mod ;
$days_increment += $div ;
2015-06-29 11:45:39 +03:00
}
2019-01-14 13:09:43 +00:00
if ( ! $this -> byhour || in_array ( $hour , $this -> byhour )) {
2015-06-29 11:45:39 +03:00
$found = true ;
break ;
}
}
2019-01-14 13:09:43 +00:00
if ( ! $found ) {
2016-03-21 21:55:40 +02:00
$this -> total = $total ; // save total for count cache
2019-01-13 10:00:43 +00:00
return ; // stop the iterator
2015-06-29 11:45:39 +03:00
}
2016-03-21 21:55:40 +02:00
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
2015-06-29 11:45:39 +03:00
break ;
2015-06-26 17:02:29 +03:00
case self :: MINUTELY :
2019-01-14 13:09:43 +00:00
if ( empty ( $dayset )) {
2016-03-21 21:55:40 +02:00
$minute += (( int ) (( 1439 - ( $hour * 60 + $minute )) / $this -> interval )) * $this -> interval ;
2015-06-29 11:45:39 +03:00
}
$found = false ;
2019-09-01 19:54:12 +01:00
for ( $j = 0 ; $j < self :: REPEAT_CYCLES [ self :: MINUTELY ]; $j ++ ) {
2016-03-21 21:55:40 +02:00
$minute += $this -> interval ;
$div = ( int ) ( $minute / 60 );
$mod = $minute % 60 ;
2019-01-14 13:09:43 +00:00
if ( $div ) {
2016-03-21 21:55:40 +02:00
$minute = $mod ;
$hour += $div ;
$div = ( int ) ( $hour / 24 );
$mod = $hour % 24 ;
2019-01-14 13:09:43 +00:00
if ( $div ) {
2016-03-21 21:55:40 +02:00
$hour = $mod ;
$days_increment += $div ;
2015-06-29 11:45:39 +03:00
}
}
2019-01-14 13:09:43 +00:00
if (( ! $this -> byhour || in_array ( $hour , $this -> byhour )) &&
( ! $this -> byminute || in_array ( $minute , $this -> byminute ))) {
2015-06-29 11:45:39 +03:00
$found = true ;
break ;
}
}
2019-01-14 13:09:43 +00:00
if ( ! $found ) {
2016-03-21 21:55:40 +02:00
$this -> total = $total ; // save total for count cache
2019-01-13 10:00:43 +00:00
return ; // stop the iterator
2015-06-29 11:45:39 +03:00
}
2016-03-21 21:55:40 +02:00
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
2015-06-29 11:45:39 +03:00
break ;
2015-06-26 17:02:29 +03:00
case self :: SECONDLY :
2019-01-14 13:09:43 +00:00
if ( empty ( $dayset )) {
2016-03-21 21:55:40 +02:00
$second += (( int ) (( 86399 - ( $hour * 3600 + $minute * 60 + $second )) / $this -> interval )) * $this -> interval ;
2015-06-29 11:45:39 +03:00
}
$found = false ;
2019-09-01 19:54:12 +01:00
for ( $j = 0 ; $j < self :: REPEAT_CYCLES [ self :: SECONDLY ]; $j ++ ) {
2016-03-21 21:55:40 +02:00
$second += $this -> interval ;
$div = ( int ) ( $second / 60 );
$mod = $second % 60 ;
2019-01-14 13:09:43 +00:00
if ( $div ) {
2016-03-21 21:55:40 +02:00
$second = $mod ;
$minute += $div ;
$div = ( int ) ( $minute / 60 );
$mod = $minute % 60 ;
2019-01-14 13:09:43 +00:00
if ( $div ) {
2016-03-21 21:55:40 +02:00
$minute = $mod ;
$hour += $div ;
$div = ( int ) ( $hour / 24 );
$mod = $hour % 24 ;
2019-01-14 13:09:43 +00:00
if ( $div ) {
2016-03-21 21:55:40 +02:00
$hour = $mod ;
$days_increment += $div ;
2015-06-29 11:45:39 +03:00
}
}
}
2019-01-14 13:09:43 +00:00
if (( ! $this -> byhour || in_array ( $hour , $this -> byhour ))
&& ( ! $this -> byminute || in_array ( $minute , $this -> byminute ))
&& ( ! $this -> bysecond || in_array ( $second , $this -> bysecond ))) {
2015-06-29 11:45:39 +03:00
$found = true ;
break ;
}
}
2019-01-14 13:09:43 +00:00
if ( ! $found ) {
2016-03-21 21:55:40 +02:00
$this -> total = $total ; // save total for count cache
2019-01-13 10:00:43 +00:00
return ; // stop the iterator
2015-06-29 11:45:39 +03:00
}
2016-03-21 21:55:40 +02:00
$timeset = $this -> getTimeSet ( $hour , $minute , $second );
2015-06-29 11:45:39 +03:00
break ;
}
// here we take a little shortcut from the Python version, by using DateTime
2019-01-14 13:09:43 +00:00
if ( $days_increment ) {
2016-03-21 21:55:40 +02:00
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
}
2016-03-21 21:55:40 +02:00
$dayset = null ; // reset the loop
2015-06-23 11:31:18 +03:00
}
2015-07-02 17:12:48 +03:00
2016-03-21 21:55:40 +02:00
$this -> total = $total ; // save total for count cache
2019-01-13 10:00:43 +00:00
return ; // stop the iterator
2015-06-23 11:31:18 +03:00
}
2016-03-15 23:30:50 +02:00
///////////////////////////////////////////////////////////////////////////////
2015-06-23 11:31:18 +03:00
// constants
// Every mask is 7 days longer to handle cross-year weekly periods.
2019-09-01 19:54:12 +01:00
const MONTH_MASK = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const MONTH_MASK_366 = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const MONTHDAY_MASK = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const MONTHDAY_MASK_366 = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const NEGATIVE_MONTHDAY_MASK = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const NEGATIVE_MONTHDAY_MASK_366 = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const WEEKDAY_MASK = [
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
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const LAST_DAY_OF_MONTH_366 = [
2015-06-23 11:31:18 +03:00
0 , 31 , 60 , 91 , 121 , 152 , 182 , 213 , 244 , 274 , 305 , 335 , 366
2019-09-01 19:54:12 +01:00
];
2015-06-23 11:31:18 +03:00
2019-09-01 19:54:12 +01:00
const LAST_DAY_OF_MONTH = [
2015-06-23 11:31:18 +03:00
0 , 31 , 59 , 90 , 120 , 151 , 181 , 212 , 243 , 273 , 304 , 334 , 365
2019-09-01 19:54:12 +01:00
];
2015-06-29 11:45:39 +03:00
/**
2016-06-30 17:01:20 +03:00
* @ var array
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 .
*/
2019-09-01 19:54:12 +01:00
const REPEAT_CYCLES = [
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
2019-09-01 19:54:12 +01:00
];
2015-07-07 19:16:46 +03:00
2016-03-15 23:30:50 +02:00
///////////////////////////////////////////////////////////////////////////////
2017-02-03 00:35:09 +02:00
// i18n methods
// these could be moved into a separate class maybe, since it's not always necessary
2015-07-07 19:16:46 +03:00
2015-07-08 12:40:45 +03:00
/**
2017-02-03 00:35:09 +02:00
* @ var array Stores translations once loaded ( so we don ' t have to reload them all the time )
2015-07-08 12:40:45 +03:00
*/
static protected $i18n = array ();
2017-02-03 00:35:09 +02:00
/**
* @ var bool if intl extension is loaded
*/
2015-07-08 12:40:45 +03:00
static protected $intl_loaded = null ;
2015-07-07 19:16:46 +03:00
/**
* Select a translation in $array based on the value of $n
2016-06-30 17:01:20 +03:00
*
* Used for selecting plural forms .
*
* @ param mixed $array Array with multiple forms or a string
* @ param string $n
*
2015-07-07 19:16:46 +03:00
* @ return string
*/
static protected function i18nSelect ( $array , $n )
{
2019-01-14 13:09:43 +00:00
if ( ! is_array ( $array )) {
2015-07-07 19:16:46 +03:00
return $array ;
}
2019-01-14 13:09:43 +00:00
if ( array_key_exists ( $n , $array )) {
2015-07-07 19:16:46 +03:00
return $array [ $n ];
}
2019-01-14 13:09:43 +00:00
elseif ( array_key_exists ( 'else' , $array )) {
2015-07-07 19:16:46 +03:00
return $array [ 'else' ];
}
else {
return '' ; // or throw?
}
}
/**
* Create a comma - separated list , with the last item added with an " and "
2016-06-30 17:01:20 +03:00
* Example : Monday , Tuesday and Friday
*
* @ param array $array
* @ param string $and Translation for " and "
*
2015-07-07 19:16:46 +03:00
* @ return string
*/
static protected function i18nList ( array $array , $and = 'and' )
{
2019-01-14 13:09:43 +00:00
if ( count ( $array ) > 1 ) {
2015-07-07 19:16:46 +03:00
$last = array_splice ( $array , - 1 );
return sprintf (
'%s %s %s' ,
implode ( ', ' , $array ),
$and ,
implode ( '' , $last )
);
}
else {
return $array [ 0 ];
}
}
2017-02-03 00:35:09 +02:00
/**
* Test if intl extension is loaded
* @ return bool
*/
2019-01-13 10:47:43 +00:00
static protected function intlLoaded ()
2017-02-03 00:35:09 +02:00
{
2019-01-14 13:09:43 +00:00
if ( self :: $intl_loaded === null ) {
2017-02-03 00:35:09 +02:00
self :: $intl_loaded = extension_loaded ( 'intl' );
}
return self :: $intl_loaded ;
}
/**
* Parse a locale and returns a list of files to load .
2019-01-13 10:47:43 +00:00
* For example " fr_FR " will produce " fr " and " fr_FR "
2017-02-03 00:35:09 +02:00
*
2019-03-17 04:45:34 -05:00
* @ param $locale
* @ param null $use_intl
*
2017-02-03 00:35:09 +02:00
* @ return array
*/
2019-01-13 10:47:43 +00:00
static protected function i18nFilesToLoad ( $locale , $use_intl = null )
2017-02-03 00:35:09 +02:00
{
2019-01-14 13:09:43 +00:00
if ( $use_intl === null ) {
2017-02-03 00:35:09 +02:00
$use_intl = self :: intlLoaded ();
}
$files = array ();
2019-01-14 13:09:43 +00:00
if ( $use_intl ) {
2017-02-03 00:35:09 +02:00
$parsed = \Locale :: parseLocale ( $locale );
$files [] = $parsed [ 'language' ];
2019-01-14 13:09:43 +00:00
if ( isset ( $parsed [ 'region' ])) {
2017-02-03 00:35:09 +02:00
$files [] = $parsed [ 'language' ] . '_' . $parsed [ 'region' ];
}
}
else {
2019-01-14 13:09:43 +00:00
if ( ! preg_match ( '/^([a-z]{2})(?:(?:_|-)[A-Z][a-z]+)?(?:(?:_|-)([A-Za-z]{2}))?(?:(?:_|-)[A-Z]*)?(?:\.[a-zA-Z\-0-9]*)?$/' , $locale , $matches )) {
2017-02-03 00:35:09 +02:00
throw new \InvalidArgumentException ( " The locale option does not look like a valid locale: $locale . For more option install the intl extension. " );
}
$files [] = $matches [ 1 ];
2019-01-14 13:09:43 +00:00
if ( isset ( $matches [ 2 ])) {
2017-02-03 00:35:09 +02:00
$files [] = $matches [ 1 ] . '_' . strtoupper ( $matches [ 2 ]);
}
}
return $files ;
}
2015-07-07 19:16:46 +03:00
/**
* 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 .
2016-04-03 16:41:30 +03:00
*
* @ param string $locale
* @ param string | null $fallback
2019-01-13 10:47:43 +00:00
* @ param bool $use_intl
* @ param string $custom_path
2016-04-03 16:41:30 +03:00
*
* @ return array
* @ throws \InvalidArgumentException
2015-07-07 19:16:46 +03:00
*/
2019-01-13 10:47:43 +00:00
static protected function i18nLoad ( $locale , $fallback = null , $use_intl = null , $custom_path = null )
2015-07-07 19:16:46 +03:00
{
2017-02-03 00:35:09 +02:00
$files = self :: i18nFilesToLoad ( $locale , $use_intl );
2015-07-08 12:40:45 +03:00
2019-01-13 10:47:43 +00:00
$base_path = __DIR__ . '/i18n' ;
2015-07-08 12:40:45 +03:00
$result = array ();
2019-01-14 13:09:43 +00:00
foreach ( $files as $file ) {
2019-01-13 10:47:43 +00:00
// if the file exists in $custom_path, it overrides the default
2019-01-14 13:09:43 +00:00
if ( $custom_path && is_file ( " $custom_path / $file .php " )) {
2019-01-13 10:47:43 +00:00
$path = " $custom_path / $file .php " ;
}
else {
$path = " $base_path / $file .php " ;
}
2019-01-14 13:09:43 +00:00
if ( isset ( self :: $i18n [ $path ])) {
2019-01-13 10:47:43 +00:00
$result = array_merge ( $result , self :: $i18n [ $path ]);
2015-07-08 12:40:45 +03:00
}
2019-01-14 13:09:43 +00:00
elseif ( is_file ( $path ) && is_readable ( $path )) {
2019-01-13 10:47:43 +00:00
self :: $i18n [ $path ] = include $path ;
$result = array_merge ( $result , self :: $i18n [ $path ]);
2015-07-08 12:40:45 +03:00
}
else {
2019-01-13 10:47:43 +00:00
self :: $i18n [ $path ] = array ();
2015-07-08 12:40:45 +03:00
}
}
2019-01-14 13:09:43 +00:00
if ( empty ( $result )) {
2016-04-03 16:41:30 +03:00
if ( ! is_null ( $fallback )) {
2017-02-03 00:35:09 +02:00
return self :: i18nLoad ( $fallback , null , $use_intl );
2016-04-03 16:41:30 +03:00
}
2016-04-04 16:34:57 +03:00
throw new \RuntimeException ( " 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
2019-01-13 10:47:43 +00:00
* `intl` extension is required .
2016-04-03 16:41:30 +03:00
*
2017-04-11 11:20:20 +01:00
* Available options
*
* | Name | Type | Description
* |-------------------|---------|------------
* | `use_intl` | bool | Use the intl extension or not ( autodetect )
* | `locale` | string | The locale to use ( autodetect )
* | `fallback` | string | Fallback locale if main locale is not found ( default en )
* | `date_formatter` | callable | Function used to format the date ( takes date , returns formatted )
* | `explicit_inifite` | bool | Mention " forever " if the rule is infinite ( true )
* | `dtstart` | bool | Mention the start date ( true )
2019-01-13 10:47:43 +00:00
* | `include_start` | bool |
* | `include_until` | bool |
* | `custom_path` | string |
2017-04-11 11:20:20 +01:00
*
2016-04-03 16:41:30 +03:00
* @ param array $opt
*
* @ return string
2015-07-07 19:16:46 +03:00
*/
public function humanReadable ( array $opt = array ())
{
2019-01-14 13:09:43 +00:00
if ( ! isset ( $opt [ 'use_intl' ])) {
2017-02-03 00:35:09 +02:00
$opt [ 'use_intl' ] = self :: intlLoaded ();
2016-04-03 16:41:30 +03:00
}
2015-07-08 12:40:45 +03:00
$default_opt = array (
2017-02-03 00:35:09 +02:00
'use_intl' => self :: intlLoaded (),
'locale' => null ,
2016-04-03 16:41:30 +03:00
'date_formatter' => null ,
'fallback' => 'en' ,
2017-04-11 11:20:20 +01:00
'explicit_infinite' => true ,
2017-10-11 12:29:00 +02:00
'include_start' => true ,
2019-01-13 10:47:43 +00:00
'include_until' => true ,
'custom_path' => null
2015-07-08 12:40:45 +03:00
);
2017-02-03 00:35:09 +02:00
// attempt to detect default locale
2019-01-14 13:09:43 +00:00
if ( $opt [ 'use_intl' ]) {
2017-02-03 00:35:09 +02:00
$default_opt [ 'locale' ] = \Locale :: getDefault ();
} else {
2019-04-29 18:56:23 +02:00
$default_opt [ 'locale' ] = setlocale ( LC_CTYPE , 0 );
2019-01-14 13:09:43 +00:00
if ( $default_opt [ 'locale' ] == 'C' ) {
2017-02-03 00:35:09 +02:00
$default_opt [ 'locale' ] = 'en' ;
}
}
2019-01-14 13:09:43 +00:00
if ( $opt [ 'use_intl' ]) {
2015-07-08 12:40:45 +03:00
$default_opt [ 'date_format' ] = \IntlDateFormatter :: SHORT ;
2019-01-14 13:09:43 +00:00
if ( $this -> freq >= self :: SECONDLY || not_empty ( $this -> rule [ 'BYSECOND' ])) {
2015-07-08 12:40:45 +03:00
$default_opt [ 'time_format' ] = \IntlDateFormatter :: LONG ;
}
2019-01-14 13:09:43 +00:00
elseif ( $this -> freq >= self :: HOURLY || not_empty ( $this -> rule [ 'BYHOUR' ]) || not_empty ( $this -> rule [ 'BYMINUTE' ])) {
2015-07-08 12:40:45 +03:00
$default_opt [ 'time_format' ] = \IntlDateFormatter :: SHORT ;
}
else {
$default_opt [ 'time_format' ] = \IntlDateFormatter :: NONE ;
}
}
$opt = array_merge ( $default_opt , $opt );
2019-01-13 10:47:43 +00:00
$i18n = self :: i18nLoad ( $opt [ 'locale' ], $opt [ 'fallback' ], $opt [ 'use_intl' ], $opt [ 'custom_path' ]);
2017-02-03 00:35:09 +02:00
2019-01-14 13:09:43 +00:00
if ( $opt [ 'date_formatter' ] && ! is_callable ( $opt [ 'date_formatter' ])) {
2015-07-08 12:40:45 +03:00
throw new \InvalidArgumentException ( 'The option date_formatter must callable' );
}
2015-07-07 19:16:46 +03:00
2019-01-14 13:09:43 +00:00
if ( ! $opt [ 'date_formatter' ]) {
if ( $opt [ 'use_intl' ]) {
2017-02-03 00:35:09 +02:00
$timezone = $this -> dtstart -> getTimezone () -> getName ();
2019-01-13 02:29:25 -07:00
2019-01-14 13:09:43 +00:00
if ( $timezone === 'Z' ) {
2017-02-03 00:35:09 +02:00
$timezone = 'GMT' ; // otherwise IntlDateFormatter::create fails because... reasons.
2019-01-14 13:09:43 +00:00
} elseif ( preg_match ( '/[-+]\d{2}/' , $timezone )) {
2019-01-13 02:29:25 -07:00
$timezone = 'GMT' . $timezone ; // otherwise IntlDateFormatter::create fails because... other reasons.
2017-02-03 00:35:09 +02:00
}
2015-07-08 12:40:45 +03:00
$formatter = \IntlDateFormatter :: create (
$opt [ 'locale' ],
$opt [ 'date_format' ],
$opt [ 'time_format' ],
2017-02-03 00:35:09 +02:00
$timezone
2015-07-08 12:40:45 +03:00
);
2019-01-14 13:09:43 +00:00
if ( ! $formatter ) {
2019-01-13 02:29:25 -07:00
throw new \RuntimeException ( 'IntlDateFormatter::create() failed. Error Code: ' . intl_get_error_code () . ' "' . intl_get_error_message () . '" (this should not happen, please open a bug report!)' );
2017-02-03 00:35:09 +02:00
}
2015-07-08 12:40:45 +03:00
$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
}
$parts = array (
'freq' => '' ,
'byweekday' => '' ,
'bymonth' => '' ,
'byweekno' => '' ,
'byyearday' => '' ,
'bymonthday' => '' ,
'byhour' => '' ,
'byminute' => '' ,
'bysecond' => '' ,
'bysetpos' => ''
);
// Every (INTERVAL) FREQ...
2019-09-01 19:54:12 +01: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
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYMONTH' ])) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> bymonth ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-07 19:16:46 +03:00
$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' ])
));
}
2019-01-14 13:09:43 +00: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 ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-07 19:16:46 +03:00
$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' ])
)
);
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYYEARDAY' ])) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> byyearday ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-07 19:16:46 +03:00
$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 ;
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYMONTHDAY' ])) {
2015-07-07 19:16:46 +03:00
$parts [ 'bymonthday' ] = array ();
2019-01-14 13:09:43 +00:00
if ( $this -> bymonthday ) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> bymonthday ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-07 19:16:46 +03:00
$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 ;
}
2019-01-14 13:09:43 +00:00
if ( $this -> bymonthday_negative ) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> bymonthday_negative ;
2019-01-14 13:09:43 +00:00
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
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYDAY' ])) {
2015-07-07 19:16:46 +03:00
$parts [ 'byweekday' ] = array ();
2019-01-14 13:09:43 +00:00
if ( $this -> byweekday ) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> byweekday ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-07 19:16:46 +03:00
$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' ])
));
}
2019-01-14 13:09:43 +00:00
if ( $this -> byweekday_nth ) {
2015-07-07 19:16:46 +03:00
$tmp = $this -> byweekday_nth ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-07 19:16:46 +03:00
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
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYHOUR' ])) {
2015-07-08 12:40:45 +03:00
$tmp = $this -> byhour ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-08 12:40:45 +03:00
$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
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYMINUTE' ])) {
2015-07-08 12:40:45 +03:00
$tmp = $this -> byminute ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-08 12:40:45 +03:00
$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
}
2019-01-14 13:09:43 +00:00
if ( not_empty ( $this -> rule [ 'BYSECOND' ])) {
2015-07-08 12:40:45 +03:00
$tmp = $this -> bysecond ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-08 12:40:45 +03:00
$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
}
2019-01-14 13:09:43 +00:00
if ( $this -> bysetpos ) {
2015-07-08 12:40:45 +03:00
$tmp = $this -> bysetpos ;
2019-01-14 13:09:43 +00:00
foreach ( $tmp as & $value ) {
2015-07-08 12:40:45 +03:00
$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
}
2019-01-14 13:09:43 +00:00
if ( $opt [ 'include_start' ]) {
2017-04-11 11:20:20 +01:00
// from X
$parts [ 'start' ] = strtr ( $i18n [ 'dtstart' ], array (
'%{date}' => $opt [ 'date_formatter' ]( $this -> dtstart )
));
}
2015-07-07 19:16:46 +03:00
// to X, or N times, or indefinitely
2019-01-14 13:09:43 +00:00
if ( $opt [ 'include_until' ]) {
if ( ! $this -> until && ! $this -> count ) {
if ( $opt [ 'explicit_infinite' ]) {
2017-10-11 12:29:00 +02:00
$parts [ 'end' ] = $i18n [ 'infinite' ];
}
}
2019-01-14 13:09:43 +00:00
elseif ( $this -> until ) {
2017-10-11 12:29:00 +02:00
$parts [ 'end' ] = strtr ( $i18n [ 'until' ], array (
'%{date}' => $opt [ 'date_formatter' ]( $this -> until )
));
}
2019-01-14 13:09:43 +00:00
elseif ( $this -> count ) {
2017-10-11 12:29:00 +02:00
$parts [ 'end' ] = strtr (
self :: i18nSelect ( $i18n [ 'count' ], $this -> count ),
array (
'%{count}' => $this -> count
)
);
2017-04-11 11:20:20 +01:00
}
2015-07-07 19:16:46 +03:00
}
$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
}