1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-02-20 09:54:16 +01:00

Better string generation & parsing

This commit is contained in:
rlanvin 2015-07-08 12:40:45 +03:00
parent fba57c4ef2
commit 13626f66c5
6 changed files with 540 additions and 187 deletions

View File

@ -28,6 +28,9 @@ foreach ( $rrule as $occurrence ) {
// Tue 01 Sep 2015
// Thu 01 Oct 2015
// Sun 01 Nov 2015
echo $rrule->humanReadable(),"\n";
// monthly on the 1st of the month, starting from 01/06/2015, 6 times
```
Complete doc is available in [the wiki](https://github.com/rlanvin/php-rrule/wiki).
@ -35,6 +38,7 @@ Complete doc is available in [the wiki](https://github.com/rlanvin/php-rrule/wik
## Requirements
- PHP >= 5.3
- [intl extension](http://php.net/manual/en/book.intl.php) is recommended for `humanReadable()` but not strictly required
## Installation
@ -63,6 +67,14 @@ require 'vendor/autoload.php';
You can just download `src/RRule.php` and require it.
## Documentation
Complete doc is available in [the wiki](https://github.com/rlanvin/php-rrule/wiki).
## Contribution
Feel free to contribute! Just create a new issue or a new pull request.
## Note
I started this library because I wasn't happy with the existing implementations
@ -79,9 +91,6 @@ respect of the RFC. This version is a bit strictier and will not accept many
non-compliant combinations of rule parts, that the python version otherwise accepts.
There are also some additional features in this version.
## Documentation
Complete doc is available in [the wiki](https://github.com/rlanvin/php-rrule/wiki).
## License

View File

@ -8,6 +8,9 @@
"require": {
"php": ">=5.3.0"
},
"suggest": {
"ext-intl": "Intl extension is needed for humanReadable()"
},
"autoload": {
"classmap": ["src/"]
}

View File

@ -270,6 +270,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
if ( $this->until && $this->count ) {
throw new \InvalidArgumentException('The UNTIL or COUNT rule parts MUST NOT occur in the same rule');
}
// infer necessary BYXXX rules from DTSTART, if not provided
if ( ! (not_empty($parts['BYWEEKNO']) || not_empty($parts['BYYEARDAY']) || not_empty($parts['BYMONTHDAY']) || not_empty($parts['BYDAY'])) ) {
switch ( $this->freq ) {
@ -337,14 +338,15 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->bymonthday = array();
$this->bymonthday_negative = array();
foreach ( $parts['BYMONTHDAY'] as $value ) {
$value = (int) $value;
if ( ! $value || $value < -31 || $value > 31 ) {
throw new \InvalidArgumentException('Invalid BYMONTHDAY value: '.$value.' (valid values are 1 to 31 or -31 to -1)');
}
if ( $value < 0 ) {
$this->bymonthday_negative[] = (int) $value;
$this->bymonthday_negative[] = $value;
}
else {
$this->bymonthday[] = (int) $value;
$this->bymonthday[] = $value;
}
}
}
@ -360,11 +362,12 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->bysetpos = array();
foreach ( $parts['BYYEARDAY'] as $value ) {
$value = (int) $value;
if ( ! $value || $value < -366 || $value > 366 ) {
throw new \InvalidArgumentException('Invalid BYSETPOS value: '.$value.' (valid values are 1 to 366 or -366 to -1)');
}
$this->byyearday[] = (int) $value;
$this->byyearday[] = $value;
}
}
@ -380,10 +383,11 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->byweekno = array();
foreach ( $parts['BYWEEKNO'] as $value ) {
$value = (int) $value;
if ( ! $value || $value < -53 || $value > 53 ) {
throw new \InvalidArgumentException('Invalid BYWEEKNO value: '.$value.' (valid values are 1 to 53 or -53 to -1)');
}
$this->byweekno[] = (int) $value;
$this->byweekno[] = $value;
}
}
@ -396,10 +400,11 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->bymonth = array();
foreach ( $parts['BYMONTH'] as $value ) {
$value = (int) $value;
if ( $value < 1 || $value > 12 ) {
throw new \InvalidArgumentException('Invalid BYMONTH value: '.$value);
}
$this->bymonth[] = (int) $value;
$this->bymonth[] = $value;
}
}
@ -417,11 +422,12 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->bysetpos = array();
foreach ( $parts['BYSETPOS'] as $value ) {
$value = (int) $value;
if ( ! $value || $value < -366 || $value > 366 ) {
throw new \InvalidArgumentException('Invalid BYSETPOS value: '.$value.' (valid values are 1 to 366 or -366 to -1)');
}
$this->bysetpos[] = (int) $value;
$this->bysetpos[] = $value;
}
}
@ -432,10 +438,11 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->byhour = array();
foreach ( $parts['BYHOUR'] as $value ) {
$value = (int) $value;
if ( $value < 0 || $value > 23 ) {
throw new \InvalidArgumentException('Invalid BYHOUR value: '.$value);
}
$this->byhour[] = (int) $value;
$this->byhour[] = $value;
}
sort($this->byhour);
@ -451,10 +458,11 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->byminute = array();
foreach ( $parts['BYMINUTE'] as $value ) {
$value = (int) $value;
if ( $value < 0 || $value > 59 ) {
throw new \InvalidArgumentException('Invalid BYMINUTE value: '.$value);
}
$this->byminute[] = (int) $value;
$this->byminute[] = $value;
}
sort($this->byminute);
}
@ -469,13 +477,14 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
$this->bysecond = array();
foreach ( $parts['BYSECOND'] as $value ) {
$value = (int) $value;
// 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
if ( $value < 0 || $value > 60 ) {
throw new \InvalidArgumentException('Invalid BYSECOND value: '.$value);
}
$this->bysecond[] = (int) $value;
$this->bysecond[] = $value;
}
sort($this->bysecond);
}
@ -520,7 +529,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
);
}
$parts = [];
$parts = array();
foreach ( $this->rule as $key => $value ) {
if ( $key === 'DTSTART' ) {
continue;
@ -1873,6 +1882,12 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
// i18n methods (could be moved into a separate class, since it's not always necessary)
/**
* Stores translations once loaded (so we don't have to reload them all the time)
*/
static protected $i18n = array();
static protected $intl_loaded = null;
/**
* Select a translation in $array based on the value of $n
* @return string
@ -1917,165 +1932,95 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
/**
* Load a translation file in memory.
* Will load the basic first (e.g. "en") and then the region-specific (e.g. "en_GB")
* 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.
*/
static protected function i18nLoad($locale)
{
return array(
'freq' => array(
self::YEARLY => array(
'1' => 'yearly',
'else' => 'every %{interval} years'
),
self::MONTHLY => array(
'1' => 'monthly',
'else' => 'every %{interval} months'
),
self::WEEKLY => array(
'1' => 'weekly',
'2' => 'every other week',
'else' => 'every %{interval} weeks'
),
self::DAILY => array(
'1' => 'daily',
'2' => 'every other day',
'else' => 'every %{interval} days'
),
self::HOURLY => array(
'1' => 'hourly',
'else' => 'every %{interval} hours'
),
self::MINUTELY => array(
'1' => 'minutely',
'else' => 'every %{interval} minutes'
),
self::SECONDLY => array(
'1' => 'secondly',
'else' => 'every %{interval} seconds'
),
),
'dtstart' => 'starting from %{date}',
'infinite' => 'forever',
'until' => 'until %{date}',
'count' => array(
'1' => 'one time',
'else' => '%{count} times'
),
'and' => 'and',
'bymonth' => array(
'limit' => 'only in %{months}',
'expand' => 'in %{months}',
),
'months' => array(
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
),
'byweekday' => array(
'limit' => 'only on %{weekdays}',
'expand' => 'on %{weekdays}',
),
'weekdays' => array(
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
7 => 'Sunday',
),
'nth_weekday' => array(
'1' => 'the first %{weekday}', // e.g. the first Monday
'2' => 'the second %{weekday}',
'3' => 'the third %{weekday}',
'else' => 'the %{n}th %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'the last %{weekday}',
'-2' => 'the penultimate %{weekday}',
'-3' => 'the antepenultimate %{weekday}',
'else' => 'the %{n}th to the last %{weekday}'
),
'byweekno' => array(
'1' => 'on week %{weeks}',
'else' => 'on weeks number %{weeks}'
),
'nth_weekno' => '%{n}',
'bymonthday' => array(
'limit' => 'only on {%monthdays}',
'expand' => 'on %{monthdays}'
),
'nth_monthday' => array(
'1' => 'the 1st',
'2' => 'the 2nd',
'3' => 'the 3rd',
'21' => 'the 21st',
'22' => 'the 22nd',
'23' => 'the 23rd',
'31' => 'the 31st',
'else' => 'the %{n}th'
),
'-nth_monthday' => array(
'1' => 'the last day',
'2' => 'the penultimate day',
'3' => 'the antepenultimate day',
'21' => 'the 21st to the last day',
'22' => 'the 22nd to the last day',
'23' => 'the 23rd to the last day',
'31' => 'the 31st to the last day',
'else' => 'the %{n}th to the last day'
),
'byyearday' => array(
'limit' => 'only on {%yeardays}',
'expand' => 'on %{yeardays}'
),
'nth_yearday' => array(
'1' => 'the first day',
'2' => 'the second day',
'3' => 'the third day',
'else' => 'the %{n}th day'
),
'-nth_yearday' => array(
'-1' => 'the last day',
'-2' => 'the penultimate day',
'-3' => 'the antepenultimate day',
'else' => 'the %{n}th to the last day'
),
'x_of_the_y' => array(
self::YEARLY => '%{x} of the year',
self::MONTHLY => '%{x} of the month',
)
);
if ( ! preg_match('/^([a-z]{2})(_[A-Z]{2})?$/', $locale, $matches) ) {
throw new \InvalidArgumentException('The locale option does not look like a valid locale: '.$opt['locale']);
}
$files = array();
if ( isset($matches[2]) ) {
$files[] = $matches[1];
}
$files[] = $locale;
$result = array();
foreach ( $files as $file ) {
$path = __DIR__."/i18n/$file.php";
if ( isset(self::$i18n[$file]) ) {
$result = array_merge($result, self::$i18n[$file]);
}
elseif ( is_file($path) && is_readable($path) ) {
self::$i18n[$file] = include $path;
$result = array_merge($result, self::$i18n[$file]);
}
else {
self::$i18n[$file] = array();
}
}
if ( empty($result) ) {
throw new \InvalidArgumentException("Failed to load $locale");
}
return $result;
}
/**
* Format a rule in a human readable string
* intl extension is required.
*/
public function humanReadable(array $opt = array())
{
$opt = array_merge(array(
if ( self::$intl_loaded === null ) {
self::$intl_loaded = extension_loaded('intl');
}
$default_opt = array(
'locale' => \Locale::getDefault(),
'date_format' => \IntlDateFormatter::SHORT,
'time_format' => \IntlDateFormatter::SHORT,
'date_formatter' => null
), $opt);
);
if ( self::$intl_loaded ) {
$default_opt['date_format'] = \IntlDateFormatter::SHORT;
if ( $this->freq >= self::SECONDLY || not_empty($this->rule['BYSECOND']) ) {
$default_opt['time_format'] = \IntlDateFormatter::LONG;
}
elseif ( $this->freq >= self::HOURLY || not_empty($this->rule['BYHOUR']) || not_empty($this->rule['BYMINUTE']) ) {
$default_opt['time_format'] = \IntlDateFormatter::SHORT;
}
else {
$default_opt['time_format'] = \IntlDateFormatter::NONE;
}
}
$opt = array_merge($default_opt, $opt);
if ( $opt['date_formatter'] && ! is_callable($opt['date_formatter']) ) {
throw new \InvalidArgumentException('The option date_formatter must callable');
}
if ( ! $opt['date_formatter'] ) {
$formatter = \IntlDateFormatter::create(
$opt['locale'],
$opt['date_format'],
$opt['time_format'],
date_default_timezone_get() // XXX should be $this->something?
);
if ( self::$intl_loaded ) {
$formatter = \IntlDateFormatter::create(
$opt['locale'],
$opt['date_format'],
$opt['time_format'],
$this->dtstart->getTimezone()->getName()
);
$opt['date_formatter'] = function($date) use ($formatter) {
return $formatter->format($date);
};
}
else {
$opt['date_formatter'] = function($date) {
return $date->format('Y-m-d H:i:s');
};
}
}
$i18n = self::i18nLoad($opt['locale']);
@ -2094,8 +2039,9 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
);
// Every (INTERVAL) FREQ...
$freq_str = strtolower(array_search($this->freq, self::$frequencies));
$parts['freq'] = strtr(
self::i18nSelect($i18n['freq'][$this->freq], $this->interval),
self::i18nSelect($i18n[$freq_str], $this->interval),
array(
'%{interval}' => $this->interval
)
@ -2103,12 +2049,11 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
// BYXXX rules
if ( $this->bymonth ) {
$selector = $this->freq > self::YEARLY ? 'limit' : 'expand';
$tmp = $this->bymonth;
foreach ( $tmp as & $value) {
$value = $i18n['months'][$value];
}
$parts['bymonth'] = strtr($i18n['bymonth'][$selector], array(
$parts['bymonth'] = strtr(self::i18nSelect($i18n['bymonth'], count($tmp)), array(
'%{months}' => self::i18nList($tmp, $i18n['and'])
));
}
@ -2130,25 +2075,23 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
}
if ( $this->byyearday ) {
$selector = $this->freq > self::DAILY ? 'limit' : 'expand';
$tmp = $this->byyearday;
foreach ( $tmp as & $value ) {
$value = strtr(self::i18nSelect($i18n[$value>0?'nth_yearday':'-nth_yearday'],$value), array(
'%{n}' => abs($value)
));
}
$tmp = strtr($i18n['byyearday'][$selector], array(
$tmp = strtr(self::i18nSelect($i18n['byyearday'], count($tmp)), array(
'%{yeardays}' => self::i18nList($tmp, $i18n['and'])
));
// ... of the month
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], self::YEARLY), array(
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], 'yearly'), array(
'%{x}' => $tmp
));
$parts['byyearday'] = $tmp;
}
if ( $this->bymonthday || $this->bymonthday_negative ) {
$selector = $this->freq > self::DAILY ? 'limit' : 'expand';
$parts['bymonthday'] = array();
if ( $this->bymonthday ) {
$tmp = $this->bymonthday;
@ -2157,11 +2100,11 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
'%{n}' => $value
));
}
$tmp = strtr($i18n['bymonthday'][$selector], array(
$tmp = strtr(self::i18nSelect($i18n['bymonthday'], count($tmp)), array(
'%{monthdays}' => self::i18nList($tmp, $i18n['and'])
));
// ... of the month
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], self::MONTHLY), array(
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], 'monthly'), array(
'%{x}' => $tmp
));
$parts['bymonthday'][] = $tmp;
@ -2169,31 +2112,30 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
if ( $this->bymonthday_negative ) {
$tmp = $this->bymonthday_negative;
foreach ( $tmp as & $value ) {
$value = strtr(self::i18nSelect($i18n['-nth_monthday'],-$value), array(
$value = strtr(self::i18nSelect($i18n['-nth_monthday'],$value), array(
'%{n}' => -$value
));
}
$tmp = strtr($i18n['bymonthday'][$selector], array(
$tmp = strtr(self::i18nSelect($i18n['bymonthday'], count($tmp)), array(
'%{monthdays}' => self::i18nList($tmp, $i18n['and'])
));
// ... of the month
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], self::MONTHLY), array(
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], 'monthly'), array(
'%{x}' => $tmp
));
$parts['bymonthday'][] = $tmp;
}
$parts['bymonthday'] = implode(' '.$i18n['and'].' ',$parts['bymonthday']);
$parts['bymonthday'] = implode(' '.$i18n['and'],$parts['bymonthday']);
}
if ( $this->byweekday || $this->byweekday_nth ) {
$parts['byweekday'] = array();
$selector = $this->freq >= self::DAILY ? 'limit' : 'expand';
if ( $this->byweekday ) {
$tmp = $this->byweekday;
foreach ( $tmp as & $value ) {
$value = $i18n['weekdays'][$value];
}
$parts['byweekday'][] = strtr($i18n['byweekday'][$selector], array(
$parts['byweekday'][] = strtr(self::i18nSelect($i18n['byweekday'], count($tmp)), array(
'%{weekdays}' => self::i18nList($tmp, $i18n['and'])
));
}
@ -2206,37 +2148,70 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
'%{n}' => abs($n)
));
}
$tmp = strtr($i18n['byweekday'][$selector], array(
$tmp = strtr(self::i18nSelect($i18n['byweekday'], count($tmp)), array(
'%{weekdays}' => self::i18nList($tmp, $i18n['and'])
));
// ... of the year|month
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], $this->freq), array(
$tmp = strtr(self::i18nSelect($i18n['x_of_the_y'], $freq_str), array(
'%{x}' => $tmp
));
$parts['byweekday'][] = $tmp;
}
$parts['byweekday'] = implode(' '.$i18n['and'].' ',$parts['byweekday']);
$parts['byweekday'] = implode(' '.$i18n['and'],$parts['byweekday']);
}
if ( $this->byhour ) {
if ( not_empty($this->rule['BYHOUR']) ) {
$tmp = $this->byhour;
foreach ( $tmp as &$value) {
$value = strtr($i18n['nth_hour'], array(
'%{n}' => $value
));
}
$parts['byhour'] = strtr(self::i18nSelect($i18n['byhour'],count($tmp)), array(
'%{hours}' => self::i18nList($tmp, $i18n['and'])
));
}
if ( $this->byminute ) {
if ( not_empty($this->rule['BYMINUTE']) ) {
$tmp = $this->byminute;
foreach ( $tmp as &$value) {
$value = strtr($i18n['nth_minute'], array(
'%{n}' => $value
));
}
$parts['byminute'] = strtr(self::i18nSelect($i18n['byminute'],count($tmp)), array(
'%{minutes}' => self::i18nList($tmp, $i18n['and'])
));
}
if ( $this->bysecond ) {
if ( not_empty($this->rule['BYSECOND']) ) {
$tmp = $this->bysecond;
foreach ( $tmp as &$value) {
$value = strtr($i18n['nth_second'], array(
'%{n}' => $value
));
}
$parts['bysecond'] = strtr(self::i18nSelect($i18n['bysecond'],count($tmp)), array(
'%{seconds}' => self::i18nList($tmp, $i18n['and'])
));
}
if ( $this->bysetpos ) {
$tmp = $this->bysetpos;
foreach ( $tmp as & $value ) {
$value = strtr(self::i18nSelect($i18n[$value>0?'nth_setpos':'-nth_setpos'],$value), array(
'%{n}' => abs($value)
));
}
$tmp = strtr(self::i18nSelect($i18n['bysetpos'], count($tmp)), array(
'%{setpos}' => self::i18nList($tmp, $i18n['and'])
));
$parts['bysetpos'] = $tmp;
}
// from X
$parts['start'] = strtr($i18n['dtstart'], array(
'%{date}' => $formatter->format($this->dtstart)
'%{date}' => $opt['date_formatter']($this->dtstart)
));
// to X, or N times, or indefinitely
@ -2245,7 +2220,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
}
elseif ( $this->until ) {
$parts['end'] = strtr($i18n['until'], array(
'%{date}' => $formatter->format($this->until)
'%{date}' => $opt['date_formatter']($this->until)
));
}
elseif ( $this->count ) {
@ -2264,7 +2239,7 @@ class RRule implements \Iterator, \ArrayAccess, \Countable
// '%{byday}' => $parts['byday'],
// ));
$parts = array_filter($parts);
$str = implode(', ',$parts);
$str = implode('',$parts);
return $str;
}

167
src/i18n/en.php Executable file
View File

@ -0,0 +1,167 @@
<?php
/**
* Translation file for English language.
*
* Most strings can be an array, with a value as the key. The system will
* pick the translation corresponding to the key. The key "else" will be picked
* if no matching value is found. This is useful for plurals.
*
* Licensed under the MIT license.
*
* For the full copyright and license information, please view the LICENSE file.
*
* @author Rémi Lanvin <remi@cloudconnected.fr>
* @link https://github.com/rlanvin/php-rrule
*/
return array(
'yearly' => array(
'1' => 'yearly',
'else' => 'every %{interval} years'
),
'monthly' => array(
'1' => 'monthly',
'else' => 'every %{interval} months'
),
'weekly' => array(
'1' => 'weekly',
'2' => 'every other week',
'else' => 'every %{interval} weeks'
),
'daily' => array(
'1' => 'daily',
'2' => 'every other day',
'else' => 'every %{interval} days'
),
'hourly' => array(
'1' => 'hourly',
'else' => 'every %{interval} hours'
),
'minutely' => array(
'1' => 'minutely',
'else' => 'every %{interval} minutes'
),
'secondly' => array(
'1' => 'secondly',
'else' => 'every %{interval} seconds'
),
'dtstart' => ', starting from %{date}',
'infinite' => ', forever',
'until' => ', until %{date}',
'count' => array(
'1' => ', one time',
'else' => ', %{count} times'
),
'and' => 'and',
'x_of_the_y' => array(
'yearly' => '%{x} of the year', // e.g. the first Monday of the year, or the first day of the year
'monthly' => '%{x} of the month',
),
'bymonth' => ' in %{months}',
'months' => array(
1 => 'January',
2 => 'February',
3 => 'March',
4 => 'April',
5 => 'May',
6 => 'June',
7 => 'July',
8 => 'August',
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December',
),
'byweekday' => ' on %{weekdays}',
'weekdays' => array(
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
7 => 'Sunday',
),
'nth_weekday' => array(
'1' => 'the first %{weekday}', // e.g. the first Monday
'2' => 'the second %{weekday}',
'3' => 'the third %{weekday}',
'else' => 'the %{n}th %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'the last %{weekday}', // e.g. the last Monday
'-2' => 'the penultimate %{weekday}',
'-3' => 'the antepenultimate %{weekday}',
'else' => 'the %{n}th to the last %{weekday}'
),
'byweekno' => array(
'1' => ' on week %{weeks}',
'else' => ' on weeks number %{weeks}'
),
'nth_weekno' => '%{n}',
'bymonthday' => ' on %{monthdays}',
'nth_monthday' => array(
'1' => 'the 1st',
'2' => 'the 2nd',
'3' => 'the 3rd',
'21' => 'the 21st',
'22' => 'the 22nd',
'23' => 'the 23rd',
'31' => 'the 31st',
'else' => 'the %{n}th'
),
'-nth_monthday' => array(
'-1' => 'the last day',
'-2' => 'the penultimate day',
'-3' => 'the antepenultimate day',
'-21' => 'the 21st to the last day',
'-22' => 'the 22nd to the last day',
'-23' => 'the 23rd to the last day',
'-31' => 'the 31st to the last day',
'else' => 'the %{n}th to the last day'
),
'byyearday' => array(
'1' => ' on %{yeardays} day',
'else' => ' on %{yeardays} days'
),
'nth_yearday' => array(
'1' => 'the first',
'2' => 'the second',
'3' => 'the third',
'else' => 'the %{n}th'
),
'-nth_yearday' => array(
'-1' => 'the last',
'-2' => 'the penultimate',
'-3' => 'the antepenultimate',
'else' => 'the %{n}th to the last'
),
'byhour' => array(
'1' => ' at %{hours}',
'else' => ' at %{hours}'
),
'nth_hour' => '%{n}h',
'byminute' => array(
'1' => ' at minute %{minutes}',
'else' => ' at minutes %{minutes}'
),
'nth_minute' => '%{n}',
'bysecond' => array(
'1' => ' at second %{seconds}',
'else' => ' at seconds %{seconds}'
),
'nth_second' => '%{n}',
'bysetpos' => ', but only %{setpos} instance of this set',
'nth_setpos' => array(
'1' => 'the first',
'2' => 'the second',
'3' => 'the third',
'else' => 'the %{n}th'
),
'-nth_setpos' => array(
'-1' => 'the last',
'-2' => 'the penultimate',
'-3' => 'the antepenultimate',
'else' => 'the %{n}th to the last'
)
);

155
src/i18n/fr.php Executable file
View File

@ -0,0 +1,155 @@
<?php
/**
* Translation file for French language.
*
* Licensed under the MIT license.
*
* For the full copyright and license information, please view the LICENSE file.
*
* @author Rémi Lanvin <remi@cloudconnected.fr>
* @link https://github.com/rlanvin/php-rrule
*/
return array(
'yearly' => array(
'1' => 'tous les ans',
'2' => 'un an sur deux',
'else' => 'tous les %{interval} ans'
),
'monthly' => array(
'1' => 'tous les mois',
'2' => 'un mois sur deux',
'else' => 'tous les %{interval} mois'
),
'weekly' => array(
'1' => 'toutes les semaines',
'2' => 'une semaine sur deux',
'else' => 'toutes les %{interval} semaines'
),
'daily' => array(
'1' => 'tous les jours',
'2' => 'un jour sur deux',
'else' => 'tous les %{interval} jours'
),
'hourly' => array(
'1' => 'toutes les heures',
'else' => 'toutes les %{interval} heures'
),
'minutely' => array(
'1' => 'toutes les minutes',
'else' => 'toutes les %{interval} minutes'
),
'secondly' => array(
'1' => 'toutes les secondes',
'else' => 'toutes les %{interval} secondes'
),
'dtstart' => ', à partir du %{date}',
'infinite' => ', indéfiniment',
'until' => ', jusqu\'au %{date}',
'count' => array(
'1' => ', une fois',
'else' => ', %{count} fois'
),
'and' => 'et',
'x_of_the_y' => array(
'yearly' => '%{x} de l\'année', // e.g. the first Monday of the year, or the first day of the year
'monthly' => '%{x} du mois',
),
'bymonth' => ' en %{months}',
'months' => array(
1 => 'janvier',
2 => 'février',
3 => 'mars',
4 => 'avril',
5 => 'mai',
6 => 'juin',
7 => 'juillet',
8 => 'août',
9 => 'septembre',
10 => 'octobre',
11 => 'november',
12 => 'décembre',
),
'byweekday' => ' le %{weekdays}',
'weekdays' => array(
1 => 'lundi',
2 => 'mardi',
3 => 'mercredi',
4 => 'jeudi',
5 => 'vendredi',
6 => 'samedi',
7 => 'dimanche',
),
'nth_weekday' => array(
'1' => 'le 1er %{weekday}', // e.g. the first Monday
'else' => 'le %{n}e %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'le dernier %{weekday}', // e.g. the last Monday
'-2' => 'l\'avant-dernier %{weekday}',
'-3' => 'l\'antépénultième %{weekday}',
'else' => 'le %{n}e %{weekday} en partant de la fin'
),
'byweekno' => array(
'1' => ' la semaine %{weeks}',
'else' => ' les semaines %{weeks}'
),
'nth_weekno' => '%{n}',
'bymonthday' => array(
'1' => ' %{monthdays}',
'else' => ' %{monthdays}'
),
'nth_monthday' => array(
'1' => 'le 1er',
'else' => 'le %{n}'
),
'-nth_monthday' => array(
'-1' => 'le dernier jour',
'-2' => 'l\'avant-dernier jour',
'-3' => 'l\'antépénultième jour',
'else' => 'le %{n}e jour en partant de la fin'
),
'byyearday' => array(
'1' => ' le %{yeardays} jour',
'else' => ' les %{yeardays} jours'
),
'nth_yearday' => array(
'1' => '1er',
'else' => '%{n}e'
),
'-nth_yearday' => array(
'-1' => 'dernier',
'-2' => 'avant-dernier',
'-3' => 'antépénultième',
'else' => '%{n}e en partant de la fin'
),
'byhour' => array(
'1' => ' à %{hours}',
'else' => ' à %{hours}'
),
'nth_hour' => '%{n}h',
'byminute' => array(
'1' => ' à %{minutes}',
'else' => ' à %{minutes}'
),
'nth_minute' => '%{n}min',
'bysecond' => array(
'1' => ' à %{seconds}',
'else' => ' à %{seconds}'
),
'nth_second' => '%{n}sec',
'bysetpos' => array(
'1' => ', mais seulement %{setpos} occurrence',
'else' => ', mais seulement %{setpos} occurrence'
),
'nth_setpos' => array(
'1' => 'la 1re',
'else' => 'la %{n}e'
),
'-nth_setpos' => array(
'-1' => 'la dernière',
'-2' => 'l\'avant-dernière',
'-3' => 'l\'antépénultième',
'else' => 'la %{n}e en partant de la fin'
)
);

View File

@ -17,6 +17,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
array(array('FREQ' => 'DAILY', 'COUNT' => -1)),
array(array('FREQ' => 'DAILY', 'UNTIL' => '2015-07-01', 'COUNT' => 1)),
array(array('FREQ' => 'YEARLY', 'BYDAY' => '1MO,X')),
// The BYDAY rule part MUST NOT be specified with a numeric value
// when the FREQ rule part is not set to MONTHLY or YEARLY.
array(array('FREQ' => 'DAILY', 'BYDAY' => array('1MO'))),
@ -28,12 +29,14 @@ class RRuleTest extends PHPUnit_Framework_TestCase
array(array('FREQ' => 'DAILY', 'BYMONTHDAY' => 0)),
array(array('FREQ' => 'DAILY', 'BYMONTHDAY' => 32)),
array(array('FREQ' => 'DAILY', 'BYMONTHDAY' => -32)),
array(array('FREQ' => 'DAILY', 'BYMONTHDAY' => '1,A')),
// The BYMONTHDAY rule part MUST NOT be specified when the FREQ rule
// part is set to WEEKLY.
array(array('FREQ' => 'WEEKLY', 'BYMONTHDAY' => 1)),
array(array('FREQ' => 'YEARLY', 'BYYEARDAY' => 0)),
array(array('FREQ' => 'YEARLY', 'BYYEARDAY' => 367)),
array(array('FREQ' => 'YEARLY', 'BYYEARDAY' => '1,A')),
// The BYYEARDAY rule part MUST NOT be specified when the FREQ
// rule part is set to DAILY, WEEKLY, or MONTHLY.
array(array('FREQ' => 'DAILY', 'BYYEARDAY' => 1)),
@ -43,6 +46,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
// BYSETPOS rule part MUST only be used in conjunction with another
// BYxxx rule part.
array(array('FREQ' => 'DAILY', 'BYSETPOS' => -1)),
array(array('FREQ' => 'DAILY', 'BYDAY' => 'MO', 'BYSETPOS' => '1,A')),
);
}
@ -1574,7 +1578,6 @@ class RRuleTest extends PHPUnit_Framework_TestCase
),
);
}
/**
* @dataProvider notOccurrences
*/
@ -1586,6 +1589,47 @@ class RRuleTest extends PHPUnit_Framework_TestCase
}
}
public function rfcStrings()
{
return array(
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=HOURLY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1;BYHOUR=1'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1;BYHOUR=12;BYMINUTE=15,30'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTHDAY=1,2,5,31,-1,-3,-15'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTHDAY=1,2,5,31,-1,-3,-15;BYSETPOS=-1'),
array('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTHDAY=1,2,5,31,-1,-3,-15;BYSETPOS=-1,1'),
array(' DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;BYWEEKNO=20,30,40;BYDAY=MO'),
array(' DTSTART;TZID=America/New_York:19970512T090000
RRULE:FREQ=YEARLY;BYYEARDAY=1,-1,10,-50;BYDAY=MO')
);
}
/**
* @dataProvider rfcStrings
*/
public function testRfcStrings($str)
{
$rule = new RRule($str);
// test that parsing the string produces the same result
// as generating the string from a rule
$this->assertEquals($rule, new RRule($rule->rfcString()));
}
public function testIsLeapYear()
{
$this->assertFalse(\RRule\is_leap_year(1700));