all-in-one-event-calendar/lib/recurrence/rule.php
2019-07-25 14:11:00 +02:00

485 lines
No EOL
18 KiB
PHP

<?php
/**
* Helper for recurrence rules.
*
* @author Time.ly Network Inc.
* @since 2.0
*
* @package AI1EC
* @subpackage AI1EC.Recurrence
*/
class Ai1ec_Recurrence_Rule extends Ai1ec_Base {
/**
* Return given recurrence data as text.
*
* @param string $rrule Recurrence rule
*
* @return string
*/
public function rrule_to_text( $rrule = '' ) {
$txt = '';
$rc = new SG_iCal_Recurrence( new SG_iCal_Line( 'RRULE:' . $rrule ) );
switch( $rc->getFreq() ) {
case 'DAILY':
$this->_get_interval( $txt, 'daily', $rc->getInterval() );
$this->_ending_sentence( $txt, $rc );
break;
case 'WEEKLY':
$this->_get_interval( $txt, 'weekly', $rc->getInterval() );
$this->_get_sentence_by( $txt, 'weekly', $rc );
$this->_ending_sentence( $txt, $rc );
break;
case 'MONTHLY':
$this->_get_interval( $txt, 'monthly', $rc->getInterval() );
$this->_get_sentence_by( $txt, 'monthly', $rc );
$this->_ending_sentence( $txt, $rc );
break;
case 'YEARLY':
$this->_get_interval( $txt, 'yearly', $rc->getInterval() );
$this->_get_sentence_by( $txt, 'yearly', $rc );
$this->_ending_sentence( $txt, $rc );
break;
default:
$processed = explode( '=', $rrule );
if (
isset( $processed[1] ) &&
in_array(
strtoupper( $processed[0] ),
array( 'RDATE', 'EXDATE' )
)
) {
$txt = $this->exdate_to_text( $processed[1] );
} else {
$txt = $rrule;
}
}
return $txt;
}
/**
* Parse a `recurrence rule' into an array that can be used to calculate
* recurrence instances.
*
* @see http://kigkonsult.se/iCalcreator/docs/using.html#EXRULE
*
* @param string $rule
* @return array
*/
public function build_recurrence_rules_array( $rule ) {
$rules = array();
$rule_list = explode( ';', $rule );
foreach ( $rule_list as $single_rule ) {
if ( false === strpos( $single_rule, '=' ) ) {
continue;
}
list( $key, $val ) = explode( '=', $single_rule );
$key = strtoupper( $key );
switch ( $key ) {
case 'BYDAY':
$rules['BYDAY'] = array();
foreach ( explode( ',', $val ) as $day ) {
$rule_map = $this->create_byday_array( $day );
$rules['BYDAY'][] = $rule_map;
if (
preg_match( '/FREQ=(MONTH|YEAR)LY/i', $rule ) &&
1 === count( $rule_map )
) {
// monthly/yearly "last" recurrences need day name
$rules['BYDAY']['DAY'] = substr(
$rule_map['DAY'],
-2
);
}
}
break;
case 'BYMONTHDAY':
case 'BYMONTH':
if ( false === strpos( $val, ',' ) ) {
$rules[$key] = $val;
} else {
$rules[$key] = explode( ',', $val );
}
break;
default:
$rules[$key] = $val;
}
}
return $rules;
}
/**
* _merge_exrule method
*
* Merge RRULE values to EXRULE, to ensure, that it matches the according
* repetition values, it is meant to exclude.
*
* NOTE: one shall ensure, that RRULE values are placed in between EXRULE
* keys, so that wording in UI would remain the same after mangling.
*
* @param string $exrule Value for EXRULE provided by user
* @param string $rrule Value for RRULE provided by user
*
* @return string Modified value to use for EXRULE
*/
public function merge_exrule( $exrule, $rrule ) {
$list_exrule = explode( ';', $exrule );
$list_rrule = explode( ';', $rrule );
$map_exrule = $map_rrule = array();
foreach ( $list_rrule as $entry ) {
if ( empty( $entry ) ) {
continue;
}
list( $key, $value ) = explode( '=', $entry );
$map_rrule[$key] = $value;
}
foreach ( $list_exrule as $entry ) {
if ( empty( $entry ) ) {
continue;
}
list( $key, $value ) = explode( '=', $entry );
$map_exrule[$key] = $value;
}
$resulting_map = array_merge( $map_rrule, $map_exrule );
$result_rule = array();
foreach ( $resulting_map as $key => $value ) {
$result_rule[] = $key . '=' . $value;
}
$result_rule = implode( ';', $result_rule );
return $result_rule;
}
/**
* Return given exception dates as text.
*
* @param array $exception_dates Dates to translate
*
* @return string
*/
public function exdate_to_text( $exception_dates ) {
$dates_to_add = array();
foreach ( explode( ',', $exception_dates ) as $_exdate ) {
$date_format = $this->_registry->get( 'model.option' )
->get( 'date_format', 'l, M j, Y' );
$dates_to_add[] = $this->_registry->get(
'date.time',
vsprintf(
'%04d-%02d-%02d',
sscanf(
$_exdate,
'%04d%02d%02dT%dZ'
)
),
'sys.default'
)
->format_i18n( $date_format );
}
$dates_to_add = str_replace( ' ', '&nbsp;', $dates_to_add );
// append dates to the string and return it;
return implode( '; ', $dates_to_add );
}
/**
* Filter recurrence / exclusion rule or dates. Avoid throwing exception for old, malformed values.
*
* @param string $rule Rule or dates value.
*
* @return string Fixed rule or dates value.
*/
public function filter_rule( $rule ) {
$matches = null;
if (
empty( $rule ) ||
! preg_match('/(T[0-9]+)(ZUNTIL=[0-9Z;T]+)/i', $rule, $matches)
) {
return $rule;
}
return preg_replace('/(T[0-9]+)(ZUNTIL=[0-9Z;T]+)/i', '$1', $rule);
}
/**
* when using BYday you need an array of arrays.
* This function create valid arrays that keep into account the presence
* of a week number beofre the day
*
* @param string $val
* @return array
*/
protected function create_byday_array( $val ) {
$week = substr( $val, 0, 1 );
if ( is_numeric( $week ) ) {
return array( $week, 'DAY' => substr( $val, 1 ) );
}
return array( 'DAY' => $val );
}
/**
* _get_sentence_by function
*
* @internal
*
* @return void
**/
protected function _get_sentence_by( &$txt, $freq, $rc ) {
global $wp_locale;
switch( $freq ) {
case 'weekly':
if( $rc->getByDay() ) {
if( count( $rc->getByDay() ) > 1 ) {
// if there are more than 3 days
// use days's abbr
if( count( $rc->getByDay() ) > 2 ) {
$_days = '';
foreach( $rc->getByDay() as $d ) {
$day = $this->get_weekday_by_id( $d, true );
$_days .= ' ' . $wp_locale->weekday_abbrev[$wp_locale->weekday[$day]] . ',';
}
// remove the last ' and'
$_days = substr( $_days, 0, -1 );
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - weekly tab' ) . $_days;
} else {
$_days = '';
foreach( $rc->getByDay() as $d ) {
$day = $this->get_weekday_by_id( $d, true );
$_days .= ' ' . $wp_locale->weekday[$day] . ' ' . Ai1ec_I18n::__( 'and' );
}
// remove the last ' and'
$_days = substr( $_days, 0, -( strlen( Ai1ec_I18n::__( 'and' ) ) + 1 ) );
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - weekly tab' ) . $_days;
}
} else {
$_days = '';
foreach( $rc->getByDay() as $d ) {
$day = $this->get_weekday_by_id( $d, true );
$_days .= ' ' . $wp_locale->weekday[$day];
}
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - weekly tab' ) . $_days;
}
}
break;
case 'monthly':
if( $rc->getByMonthDay() ) {
// if there are more than 2 days
if( count( $rc->getByMonthDay() ) > 2 ) {
$_days = '';
foreach( $rc->getByMonthDay() as $m_day ) {
$_days .= ' ' . $this->_ordinal( $m_day ) . ',';
}
$_days = substr( $_days, 0, -1 );
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $_days . ' ' . Ai1ec_I18n::__( 'of the month' );
} else if( count( $rc->getByMonthDay() ) > 1 ) {
$_days = '';
foreach( $rc->getByMonthDay() as $m_day ) {
$_days .= ' ' . $this->_ordinal( $m_day ) . ' ' . Ai1ec_I18n::__( 'and' );
}
$_days = substr( $_days, 0, -4 );
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $_days . ' ' . Ai1ec_I18n::__( 'of the month' );
} else {
$_days = '';
foreach( $rc->getByMonthDay() as $m_day ) {
$_days .= ' ' . $this->_ordinal( $m_day );
}
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $_days . ' ' . Ai1ec_I18n::__( 'of the month' );
}
} elseif( $rc->getByDay() ) {
$_days = '';
$dnum = '';
foreach( $rc->getByDay() as $d ) {
if ( ! preg_match( '|^((-?)\d+)([A-Z]{2})$|', $d, $matches ) ) {
continue;
}
$_dnum = $matches[1];
$_day = $matches[3];
if ( '-' === $matches[2] ) {
$dnum = ' ' . Ai1ec_I18n::__( 'last' );
} else {
$dnum = ' ' . $this->_registry->get(
'date.time',
strtotime( $_dnum . '-01-1998 12:00:00' )
)->format_i18n( 'jS' );
}
$day = $this->get_weekday_by_id( $_day, true );
$_days .= ' ' . $wp_locale->weekday[$day];
}
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $dnum . $_days;
}
break;
case 'yearly':
if( $rc->getByMonth() ) {
// if there are more than 2 months
if( count( $rc->getByMonth() ) > 2 ) {
$_months = '';
foreach( $rc->getByMonth() as $_m ) {
$_m = $_m < 10 ? 0 . $_m : $_m;
$_months .= ' ' . $wp_locale->month_abbrev[$wp_locale->month[$_m]] . ',';
}
$_months = substr( $_months, 0, -1 );
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - yearly tab' ) . $_months;
} else if( count( $rc->getByMonth() ) > 1 ) {
$_months = '';
foreach( $rc->getByMonth() as $_m ) {
$_m = $_m < 10 ? 0 . $_m : $_m;
$_months .= ' ' . $wp_locale->month[$_m] . ' ' . Ai1ec_I18n::__( 'and' );
}
$_months = substr( $_months, 0, -4 );
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - yearly tab' ) . $_months;
} else {
$_months = '';
foreach( $rc->getByMonth() as $_m ) {
$_m = $_m < 10 ? 0 . $_m : $_m;
$_months .= ' ' . $wp_locale->month[$_m];
}
$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - yearly tab' ) . $_months;
}
}
break;
}
}
/**
* _ordinal function
*
* @internal
*
* @return void
**/
protected function _ordinal( $cdnl ) {
$locale = explode( '_', get_locale() );
if( isset( $locale[0] ) && $locale[0] != 'en' )
return $cdnl;
$test_c = abs($cdnl) % 10;
$ext = ( ( abs( $cdnl ) % 100 < 21 && abs( $cdnl ) % 100 > 4 ) ? 'th'
: ( ( $test_c < 4 ) ? ( $test_c < 3 ) ? ( $test_c < 2 ) ? ( $test_c < 1 )
? 'th' : 'st' : 'nd' : 'rd' : 'th' ) );
return $cdnl.$ext;
}
/**
* Returns the textual representation of the given recurrence frequency and
* interval, with result stored in $txt.
*
* @internal
*
* @return void
*/
protected function _get_interval( &$txt, $freq, $interval ) {
switch ( $freq ) {
case 'daily':
// check if interval is set
if ( ! $interval || $interval == 1 ) {
$txt = Ai1ec_I18n::__( 'Daily' );
} else {
if ( $interval == 2 ) {
$txt = Ai1ec_I18n::__( 'Every other day' );
} else {
$txt = sprintf(
Ai1ec_I18n::__( 'Every %d days' ),
$interval
);
}
}
break;
case 'weekly':
// check if interval is set
if ( ! $interval || $interval == 1 ) {
$txt = Ai1ec_I18n::__( 'Weekly' );
} else {
if ( $interval == 2 ) {
$txt = Ai1ec_I18n::__( 'Every other week' );
} else {
$txt = sprintf(
Ai1ec_I18n::__( 'Every %d weeks' ),
$interval
);
}
}
break;
case 'monthly':
// check if interval is set
if ( ! $interval || $interval == 1 ) {
$txt = Ai1ec_I18n::__( 'Monthly' );
} else {
if ( $interval == 2 ) {
$txt = Ai1ec_I18n::__( 'Every other month' );
} else {
$txt = sprintf(
Ai1ec_I18n::__( 'Every %d months' ),
$interval
);
}
}
break;
case 'yearly':
// check if interval is set
if ( ! $interval || $interval == 1 ) {
$txt = Ai1ec_I18n::__( 'Yearly' );
} else {
if ( $interval == 2 ) {
$txt = Ai1ec_I18n::__( 'Every other year' );
} else {
$txt = sprintf(
Ai1ec_I18n::__( 'Every %d years' ),
$interval
);
}
}
break;
}
}
/**
* get_weekday_by_id function
*
* Returns weekday name in English
*
* @param int $day_id Day ID
*
* @return string
**/
protected function get_weekday_by_id( $day_id, $by_value = false ) {
return $this->_registry->get( 'view.admin.get-repeat-box' )
->get_weekday_by_id( $day_id, $by_value );
}
/**
* _ending_sentence function
*
* Ends rrule to text sentence
*
* @internal
*
* @return void
**/
protected function _ending_sentence( &$txt, &$rc ) {
if ( $until = $rc->getUntil() ) {
if ( ! is_int( $until ) ) {
$until = strtotime( $until );
}
$txt .= ' ' . sprintf(
Ai1ec_I18n::__( 'until %s' ),
$this->_registry->get(
'date.time',
$until
)->format_i18n(
$this->_registry->get( 'model.option')->get( 'date_format' )
)
);
} else if ( $count = $rc->getCount() ) {
$txt .= ' ' . sprintf(
Ai1ec_I18n::__( 'for %d occurrences' ),
$count
);
} else {
$txt .= ', ' . Ai1ec_I18n::__( 'forever' );
}
}
}