1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-03-14 06:29:16 +01:00

Compare commits

...

34 Commits

Author SHA1 Message Date
Rémi Lanvin
343c015118 Update Changelog for 2.5.2 2025-02-21 09:21:51 +01:00
Ancelin Bouchet
fd213620c2 fix: Fix november typo in french translation file 2025-02-21 09:16:23 +01:00
Rémi Lanvin
c68668b195 Fix incorrect calculation from partially filled cache
When resuming calculation from a partially filled cache,
if the time is the last second of the day, this causes the generator
to consider the wrong frame of reference and therefore generate
the wrong occurrences.
Ref #160
2025-02-21 09:15:24 +01:00
Viktor Szépe
bef9568de9 Fix typos 2024-06-27 09:13:29 +02:00
Rémi Lanvin
cb5c6f44f2 Update Changelog for 2.5.1 2024-06-23 09:45:45 +02:00
Rémi Lanvin
a2dd785693 Fix insufficient type detection for FREQ and WKST
In some cases non-string types would end up passed to strtoupper
which causes a deprecation warning from PHP 8.3.
There might be other places where stricter type check is needed
Ref #149
2024-06-23 09:24:58 +02:00
Rémi Lanvin
4b19d8ef60 Fix utf8 C locale detection wihout intl 2024-06-23 09:01:48 +02:00
Rémi Lanvin
747bc473d9 Fix failing tests with ICU 72.1 because of NNBSP 2024-06-23 08:18:06 +02:00
Rémi Lanvin
0f48c3f93b Add PHP 8.2 to Github workflow 2024-06-13 09:36:07 +02:00
Rémi Lanvin
75b76c85c9 Release 2.5.0 2024-06-08 08:10:35 +02:00
Rémi Lanvin
9fd062ae06 Update changelog 2024-06-08 07:39:51 +02:00
nils&paul
44d5b4c3f0 Update nl.php
In Dutch, weekdays and monts are written in lowercase (https://www.babbel.com/en/magazine/a-guide-to-how-to-write-the-date-in-dutch).
2024-06-08 07:36:03 +02:00
kluvi
963a466e48 Czech localization 2024-06-08 07:35:21 +02:00
Taichi Kurihara
8bbb753c5a Added Japanese translation 2024-06-08 07:34:42 +02:00
Erik
949addfb2d add test 2024-06-08 07:32:49 +02:00
Erik
7af14b7dac Better handle TZ with Exchange / M365 generated iCal files 2024-06-08 07:32:49 +02:00
Gonzalo Guevara
9cd53d7bcc Improve clarity in daily recurrence translation 2024-06-08 07:30:56 +02:00
Sebastian Hultstrand
2cad19b61d Corrects the spelling of monday and the grammar of enumeration partials. 2023-08-19 09:23:18 +02:00
Jeff Bierschbach
bef5c05e43
Add human readable time of day option 2023-06-14 19:35:17 +02:00
rlanvin
7ddef3d49b Update Changelog for 2.4.1 2023-06-07 15:15:59 +02:00
Jeff Bierschbach
ccbc749f65 Humanreadable gets monthly wrong 2023-06-07 15:06:42 +02:00
Alexandru Busuioc
38ea18eb55 correctly parse DateTimeImmutable 2023-05-01 20:52:19 +02:00
Craig Heydenburg
f7bcad3538
Fix namespace on return type
phpstan interprets the namespace RRule and so turn the return type into RRule\DateTimeInterface which of course doesn't exist.
2023-02-22 09:36:35 +01:00
rlanvin
2acd9950e8 Update Changelog for 2.4.0 2023-01-06 11:19:10 +01:00
Joel Stein
fa13bf3f6b
Fix #120 Reset time to midnight to fix timezone offsets. 2023-01-06 11:01:00 +01:00
Edu
15646f89da Update es.php
Fix Spanish typos
2022-09-30 19:23:15 +02:00
L. Fliss
8ad92b376d Improve German translation (fixes #112)
Fixed grammar problems for example in "Monatlich am ersten Montag und letzten Dienstag des Monats, ab dem 06.09.22, für immer"
2022-09-24 09:49:38 +02:00
rlanvin
bfb494a64d Revert breaking change in test class mistakenly merged 2022-08-21 11:08:21 +02:00
Janusz Paszyński
dbeabe3e2f Polish translation added 2022-08-04 09:01:04 +02:00
Janusz Paszyński
601e2cccfb deprecation messages supress without braking backward compatibility 2022-08-04 09:00:03 +02:00
Felippe Roberto Bayestorff Duarte
32bab843e4 Create pt.php
Added portuguese translation
2022-08-04 08:57:17 +02:00
Cédric Anne
d29a068795 Exclude files from dist package 2022-08-04 08:48:42 +02:00
rlanvin
2221e8cdac Fix #103 createFromRfcString forced uppercase 2022-05-03 15:11:22 +02:00
rlanvin
5ef9eedb5d Fix #104 remove microseconds from date input 2022-04-22 10:45:04 +02:00
26 changed files with 1077 additions and 128 deletions

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
/.github/ export-ignore
/tests/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/phpunit.xml.dist export-ignore

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
name: PHP ${{ matrix.php }}
steps:
- name: Checkout

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
vendor
test.php
composer.lock
.idea
.idea
.phpunit.result.cache

View File

@ -1,5 +1,70 @@
# Changelog
## [Unreleased]
## [2.5.2] - 2025-02-21
### Fixed
- Fix "november" typo in french translation [#155](https://github.com/rlanvin/php-rrule/issues/155)
- Fix incorrect calculation from partially filled cache [#160](https://github.com/rlanvin/php-rrule/issues/160)
## [2.5.1] - 2024-06-23
### Fixed
- Fix insufficient type detection for FREQ and WKST leading to deprecation warning in tests with PHP 8.3 [#149](https://github.com/rlanvin/php-rrule/pull/149)
- Fix failing tests with ICU 72.1 because of NNBSP
- Fix C.UTF8 locale support when intl isn't installed
## [2.5.0] - 2024-06-08
### Fixed
- Swedish: Corrects the spelling of monday and the grammar of enumeration partials. [#134](https://github.com/rlanvin/php-rrule/pull/134)
- Spanish: Improve clarity in daily and weekly recurrence translation [#147](https://github.com/rlanvin/php-rrule/pull/147)
- Dutch: weekdays and months are written in lowercase [#136](https://github.com/rlanvin/php-rrule/pull/136)
- Better handle TZ with Exchange / M365 generated iCal files [#143](https://github.com/rlanvin/php-rrule/pull/143)
### Added
- Human readable time of day option [#124](https://github.com/rlanvin/php-rrule/pull/124)
- Japanese translation [#139](https://github.com/rlanvin/php-rrule/pull/139)
- Czech translation [#137](https://github.com/rlanvin/php-rrule/pull/137)
## [2.4.1] - 2023-06-07
### Fixed
- Correctly parse `DateTimeImmutable` [#132](https://github.com/rlanvin/php-rrule/pull/132)
- Fix namespace on return type [#130](https://github.com/rlanvin/php-rrule/pull/130)
- Humanreadable gets monthly wrong [#129](https://github.com/rlanvin/php-rrule/pull/129)
## [2.4.0] - 2023-01-06
### Fixed
- Exclude files from dist packages [#110](https://github.com/rlanvin/php-rrule/pull/110)
- Improve German translation [#112](https://github.com/rlanvin/php-rrule/issues/112)
- Daylight Saving Time issue with PHP 8.1 [#120](https://github.com/rlanvin/php-rrule/issues/120)
### Added
- Added Portuguese translation [#108](https://github.com/rlanvin/php-rrule/pull/108)
- Added Polish translation [#106](https://github.com/rlanvin/php-rrule/pull/106)
## [2.3.2] - 2022-05-03
### Fixed
- Fix timezone (and the entire rule) changed to uppercase if rule was created using `createdFromRfcString` [#103](https://github.com/rlanvin/php-rrule/issues/103)
## [2.3.1] - 2022-04-22
### Fixed
- Fix microseconds not always removed from dtstart, causing date comparison issues with specific date input [#104](https://github.com/rlanvin/php-rrule/issues/104)
## [2.3.0] - 2021-10-25
### Added
@ -56,7 +121,7 @@
## [2.0.0-rc1] - 2019-01-13
- Rewrite the core algorithm to use a native PHP generator, drop compability with PHP < 5.6 [#43](https://github.com/rlanvin/php-rrule/issues/43)
- Rewrite the core algorithm to use a native PHP generator, drop compatibility with PHP < 5.6 [#43](https://github.com/rlanvin/php-rrule/issues/43)
### Added
@ -170,7 +235,7 @@
### Fixed
- `RRule::parseRfcString()` is strictier and will not accept invalid `DTSTART` and `UNTIL` formats (use the array syntax in the constructor with `DateTime` objects if you need to create rules with complex combinations of timezones). [#13](https://github.com/rlanvin/php-rrule/issues/13)
- `RRule::parseRfcString()` is stricter and will not accept invalid `DTSTART` and `UNTIL` formats (use the array syntax in the constructor with `DateTime` objects if you need to create rules with complex combinations of timezones). [#13](https://github.com/rlanvin/php-rrule/issues/13)
## [1.2.0] - 2016-04-09
@ -215,7 +280,15 @@
- First release, everything before that was unversioned (`dev-master` was used).
[Unreleased]: https://github.com/rlanvin/php-rrule/compare/v2.2.2...HEAD
[Unreleased]: https://github.com/rlanvin/php-rrule/compare/v2.5.2...HEAD
[2.5.2]: https://github.com/rlanvin/php-rrule/compare/v2.5.1...v2.5.2
[2.5.1]: https://github.com/rlanvin/php-rrule/compare/v2.5.0...v2.5.1
[2.5.0]: https://github.com/rlanvin/php-rrule/compare/v2.4.1...v2.5.0
[2.4.1]: https://github.com/rlanvin/php-rrule/compare/v2.4.0...v2.4.1
[2.4.0]: https://github.com/rlanvin/php-rrule/compare/v2.3.2...v2.4.0
[2.3.2]: https://github.com/rlanvin/php-rrule/compare/v2.3.1...v2.3.2
[2.3.1]: https://github.com/rlanvin/php-rrule/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/rlanvin/php-rrule/compare/v2.2.2...v2.3.0
[2.2.2]: https://github.com/rlanvin/php-rrule/compare/v2.2.1...v2.2.2
[2.2.1]: https://github.com/rlanvin/php-rrule/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/rlanvin/php-rrule/compare/v2.1.0...v2.2.0

View File

@ -76,7 +76,7 @@ you `lno1wkst`). I tried to comment and explain as much of the algorithm as poss
in this PHP port, so feel free to check the code if you're interested.
The lib differs from the python version in various aspects, notably in the
respect of the RFC. This version is a bit strictier and will not accept many
respect of the RFC. This version is a bit stricter 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.

View File

@ -179,7 +179,7 @@ class RRule implements RRuleInterface
/**
* The constructor needs the entire rule at once.
* There is no setter after the class has been instanciated,
* There is no setter after the class has been instantiated,
* because in order to validate some BYXXX parts, we need to know
* the value of some other parts (FREQ or other BXXX parts).
*
@ -219,34 +219,38 @@ class RRule implements RRuleInterface
$this->rule = $parts; // save original rule
// WKST
$parts['WKST'] = strtoupper($parts['WKST']);
if (! array_key_exists($parts['WKST'], self::WEEKDAYS)) {
if (is_string($parts['WKST'])) {
$parts['WKST'] = strtoupper($parts['WKST']);
if (array_key_exists($parts['WKST'], self::WEEKDAYS)) {
$this->wkst = self::WEEKDAYS[$parts['WKST']];
}
}
if (!$this->wkst) {
throw new \InvalidArgumentException(
'The WKST rule part must be one of the following: '
.implode(', ',array_keys(self::WEEKDAYS))
);
}
$this->wkst = self::WEEKDAYS[$parts['WKST']];
// FREQ
if (is_integer($parts['FREQ'])) {
if ($parts['FREQ'] > self::SECONDLY || $parts['FREQ'] < self::YEARLY) {
throw new \InvalidArgumentException(
'The FREQ rule part must be one of the following: '
.implode(', ',array_keys(self::FREQUENCIES))
);
if ($parts['FREQ'] <= self::SECONDLY && $parts['FREQ'] >= self::YEARLY) {
$this->freq = $parts['FREQ'];
}
$this->freq = $parts['FREQ'];
}
else { // string
elseif (is_string($parts['FREQ'])) {
$parts['FREQ'] = strtoupper($parts['FREQ']);
if (! array_key_exists($parts['FREQ'], self::FREQUENCIES)) {
throw new \InvalidArgumentException(
'The FREQ rule part must be one of the following: '
.implode(', ',array_keys(self::FREQUENCIES))
);
if (array_key_exists($parts['FREQ'], self::FREQUENCIES)) {
$this->freq = self::FREQUENCIES[$parts['FREQ']];
}
$this->freq = self::FREQUENCIES[$parts['FREQ']];
}
if (!$this->freq) {
throw new \InvalidArgumentException(
'The FREQ rule part must be one of the following: '
.implode(', ',array_keys(self::FREQUENCIES))
);
}
// INTERVAL
@ -558,7 +562,7 @@ class RRule implements RRuleInterface
/**
* Format a rule according to RFC 5545
*
* @param bool $include_timezone Wether to generate a rule with timezone identifier on DTSTART (and UNTIL) or not.
* @param bool $include_timezone Whether to generate a rule with timezone identifier on DTSTART (and UNTIL) or not.
* @return string
*/
public function rfcString($include_timezone = true)
@ -670,8 +674,8 @@ class RRule implements RRuleInterface
if (! $force_rset) {
// try to detect if we have a RRULE or a set
$string = strtoupper($string);
$nb_rrule = substr_count($string, 'RRULE');
$upper_string = strtoupper($string);
$nb_rrule = substr_count($upper_string, 'RRULE');
if ($nb_rrule == 0) {
$class = '\RRule\RRule';
}
@ -680,7 +684,7 @@ class RRule implements RRuleInterface
}
else {
$class = '\RRule\RRule';
if (strpos($string, 'EXDATE') !== false || strpos($string, 'RDATE') !== false || strpos($string, 'EXRULE') !== false) {
if (strpos($upper_string, 'EXDATE') !== false || strpos($upper_string, 'RDATE') !== false || strpos($upper_string, 'EXRULE') !== false) {
$class = '\RRule\RSet';
}
}
@ -717,7 +721,7 @@ class RRule implements RRuleInterface
}
/**
* Return true if the rrule has no end condition (infite)
* Return true if the rrule has no end condition (infinite)
*
* @return bool
*/
@ -822,7 +826,7 @@ class RRule implements RRuleInterface
}
}
// so now we have exhausted all the BYXXX rules (exept bysetpos),
// so now we have exhausted all the BYXXX rules (except bysetpos),
// we still need to consider frequency and interval
list($start_year, $start_month) = explode('-',$this->dtstart->format('Y-m'));
switch ($this->freq) {
@ -916,6 +920,7 @@ class RRule implements RRuleInterface
/**
* @internal
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
@ -925,6 +930,7 @@ class RRule implements RRuleInterface
/**
* @internal
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
@ -958,6 +964,7 @@ class RRule implements RRuleInterface
/**
* @internal
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
@ -967,6 +974,7 @@ class RRule implements RRuleInterface
/**
* @internal
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
@ -980,7 +988,7 @@ class RRule implements RRuleInterface
/**
* Returns the number of occurrences in this rule. It will have go
* through the whole recurrence, if this hasn't been done before, which
* introduces a performance penality.
* introduces a performance penalty.
*
* @return int
*/
@ -1354,16 +1362,8 @@ class RRule implements RRuleInterface
if ($occurrence) {
$dtstart = clone $occurrence; // since DateTime is not immutable, clone to avoid any problem
// so we skip the last occurrence of the cache
if ($this->freq === self::SECONDLY) {
$dtstart = $dtstart->modify('+'.$this->interval.'second');
}
else {
$dtstart = $dtstart->modify('+1second');
}
}
if ($dtstart === null) {
elseif ($dtstart === null) {
$dtstart = clone $this->dtstart;
}
@ -1391,7 +1391,7 @@ class RRule implements RRuleInterface
$timeset = $this->timeset;
}
else {
// initialize empty if it's not going to occurs on the first iteration
// initialize empty if it's not going to occur 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))
@ -1404,8 +1404,21 @@ class RRule implements RRuleInterface
}
}
// if we restarted the calculation from cache, we know that dtstart has already been yielded
// so we can skip ahead to the next second to avoid the same date to be yielded again
// we need to do that after the correct frame as been set (see https://github.com/rlanvin/php-rrule/issues/160)
if ($occurrence) {
if ($this->freq === self::SECONDLY) {
$dtstart = $dtstart->modify('+'.$this->interval.'second');
}
else {
$dtstart = $dtstart->modify('+1second');
}
}
$max_cycles = self::MAX_CYCLES[$this->freq <= self::DAILY ? $this->freq : self::DAILY];
for ($i = 0; $i < $max_cycles; $i++) {
// 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
@ -1506,8 +1519,8 @@ class RRule implements RRuleInterface
$tmp = $year.':'.$yearday.':'.$time[0].':'.$time[1].':'.$time[2];
if (! isset($filtered_set[$tmp])) {
$occurrence = \DateTime::createFromFormat(
'Y z',
"$year $yearday",
'Y z H:i:s',
"$year $yearday 00:00:00",
$this->dtstart->getTimezone()
);
$occurrence->setTime($time[0], $time[1], $time[2]);
@ -1538,6 +1551,7 @@ class RRule implements RRuleInterface
$this->total = $total;
return;
}
$total += 1;
$this->cache[] = clone $occurrence;
yield clone $occurrence; // yield
@ -1549,8 +1563,8 @@ class RRule implements RRuleInterface
// normal loop, without BYSETPOS
foreach ($dayset as $yearday) {
$occurrence = \DateTime::createFromFormat(
'Y z',
"$year $yearday",
'Y z H:i:s',
"$year $yearday 00:00:00",
$this->dtstart->getTimezone()
);
@ -1563,12 +1577,12 @@ class RRule implements RRuleInterface
return;
}
// next($timeset);
if ($occurrence >= $dtstart) { // ignore occurrences before DTSTART
if ($this->count && $total >= $this->count) {
$this->total = $total;
return;
}
$total += 1;
$this->cache[] = clone $occurrence;
yield clone $occurrence; // yield
@ -2065,9 +2079,10 @@ class RRule implements RRuleInterface
* | `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)
* | `explicit_infinite`| bool | Mention "forever" if the rule is infinite (true)
* | `dtstart` | bool | Mention the start date (true)
* | `include_start` | bool |
* | `start_time_only` | bool | Mention the time of day only, without the date
* | `include_until` | bool |
* | `custom_path` | string |
*
@ -2088,6 +2103,7 @@ class RRule implements RRuleInterface
'fallback' => 'en',
'explicit_infinite' => true,
'include_start' => true,
'start_time_only' => false,
'include_until' => true,
'custom_path' => null
);
@ -2097,13 +2113,13 @@ class RRule implements RRuleInterface
$default_opt['locale'] = \Locale::getDefault();
} else {
$default_opt['locale'] = setlocale(LC_CTYPE, 0);
if ($default_opt['locale'] == 'C') {
if (!$default_opt['locale'] || $default_opt['locale'][0] == 'C') {
$default_opt['locale'] = 'en';
}
}
if ($opt['use_intl']) {
$default_opt['date_format'] = \IntlDateFormatter::SHORT;
$default_opt['date_format'] = isset($opt['start_time_only']) && $opt['start_time_only'] ? \IntlDateFormatter::NONE : \IntlDateFormatter::SHORT;
if ($this->freq >= self::SECONDLY || not_empty($this->rule['BYSECOND'])) {
$default_opt['time_format'] = \IntlDateFormatter::LONG;
}
@ -2111,7 +2127,7 @@ class RRule implements RRuleInterface
$default_opt['time_format'] = \IntlDateFormatter::SHORT;
}
else {
$default_opt['time_format'] = \IntlDateFormatter::NONE;
$default_opt['time_format'] = isset($opt['start_time_only']) && $opt['start_time_only'] ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE;
}
}
@ -2146,8 +2162,9 @@ class RRule implements RRuleInterface
};
}
else {
$opt['date_formatter'] = function($date) {
return $date->format('Y-m-d H:i:s');
$opt['date_formatter'] = function ($date) use ($opt) {
$format = $opt['start_time_only'] ? 'H:i:s' : 'Y-m-d H:i:s';
return $date->format($format);
};
}
}
@ -2183,6 +2200,11 @@ class RRule implements RRuleInterface
$parts['bymonth'] = strtr(self::i18nSelect($i18n['bymonth'], count($tmp)), array(
'%{months}' => self::i18nList($tmp, $i18n['and'])
));
if ($freq_str == 'yearly') {
// if a yearly frequency is being displayed by month, then switch "of the year" text to be monthly
$freq_str = 'monthly';
}
}
if (not_empty($this->rule['BYWEEKNO'])) {
@ -2354,7 +2376,12 @@ class RRule implements RRuleInterface
if ($opt['include_start']) {
// from X
$parts['start'] = strtr($i18n['dtstart'], array(
if ($opt['start_time_only']) {
$value = $this->freq >= self::HOURLY ? 'startingtimeofday' : 'timeofday';
} else {
$value = 'dtstart';
}
$parts['start'] = strtr($i18n[$value], array(
'%{date}' => $opt['date_formatter']($this->dtstart)
));
}

View File

@ -25,7 +25,7 @@ interface RRuleInterface extends \ArrayAccess, \Countable, \IteratorAggregate
public function getOccurrences($limit = null);
/**
* Return all the ocurrences after a date, before a date, or between two dates.
* Return all the occurrences after a date, before a date, or between two dates.
*
* @param mixed $begin Can be null to return all occurrences before $end
* @param mixed $end Can be null to return all occurrences after $begin
@ -49,7 +49,7 @@ interface RRuleInterface extends \ArrayAccess, \Countable, \IteratorAggregate
*
* @param mixed $date
* @param int $index The index (starts at 1)
* @return DateTimeInterface|null
* @return \DateTimeInterface|null
*/
public function getNthOccurrenceAfter($date, $index);
@ -68,7 +68,7 @@ interface RRuleInterface extends \ArrayAccess, \Countable, \IteratorAggregate
*
* @param mixed $date
* @param int $index The index (starts at 1)
* @return DateTimeInterface|null
* @return \DateTimeInterface|null
*/
public function getNthOccurrenceBefore($date, $index);
@ -77,7 +77,7 @@ interface RRuleInterface extends \ArrayAccess, \Countable, \IteratorAggregate
*
* @param mixed $date
* @param int $index 0 returns the date, positive integer returns index in the future, negative in the past
* @return DateTimeInterface|null
* @return \DateTimeInterface|null
*/
public function getNthOccurrenceFrom($date, $index);
@ -97,9 +97,9 @@ interface RRuleInterface extends \ArrayAccess, \Countable, \IteratorAggregate
public function isFinite();
/**
* Return true if the rrule has no end condition (infite)
* Return true if the rrule has no end condition (infinite)
*
* @return bool
*/
public function isInfinite();
}
}

View File

@ -52,7 +52,7 @@ trait RRuleTrait
}
/**
* Return all the ocurrences after a date, before a date, or between two dates.
* Return all the occurrences after a date, before a date, or between two dates.
*
* @param mixed $begin Can be null to return all occurrences before $end
* @param mixed $end Can be null to return all occurrences after $begin
@ -204,6 +204,22 @@ trait RRuleTrait
else {
$date = clone $date; // avoid reference problems
}
// ensure there is no microseconds in the DateTime object even if
// the input contained microseconds, to avoid date comparison issues
// (see #104)
if (version_compare(PHP_VERSION, '7.1.0') < 0) {
$date = new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone());
}
else {
$date = $date->setTime(
$date->format('H'),
$date->format('i'),
$date->format('s'),
0
);
}
return $date;
}
}
}

View File

@ -202,7 +202,7 @@ class RSet implements RRuleInterface
}
/**
* Add a RDATE (renamed Date for simplicy, since we don't support full RDATE syntax at the moment)
* Add a RDATE (renamed Date for simplicity, since we don't support full RDATE syntax at the moment)
*
* @param mixed $date a valid date representation or a \DateTime object
* @return $this
@ -382,7 +382,7 @@ class RSet implements RRuleInterface
}
/**
* Return true if the rrule has no end condition (infite)
* Return true if the rrule has no end condition (infinite)
*
* @return bool
*/
@ -488,6 +488,7 @@ class RSet implements RRuleInterface
/**
* @internal
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
@ -497,6 +498,7 @@ class RSet implements RRuleInterface
/**
* @internal
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
@ -530,6 +532,7 @@ class RSet implements RRuleInterface
/**
* @internal
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
@ -539,6 +542,7 @@ class RSet implements RRuleInterface
/**
* @internal
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
@ -552,7 +556,7 @@ class RSet implements RRuleInterface
/**
* Returns the number of recurrences in this set. It will have go
* through the whole recurrence, if this hasn't been done before, which
* introduces a performance penality.
* introduces a performance penalty.
* @return int
*/
#[\ReturnTypeWillChange]

View File

@ -64,7 +64,7 @@ class RfcParser
/**
* Parse both DTSTART and RRULE (and EXRULE).
*
* It's impossible to accuratly parse a RRULE in isolation (without the DTSTART)
* It's impossible to accurately parse a RRULE in isolation (without the DTSTART)
* as some tests depends on DTSTART (notably the date format for UNTIL).
*
* @param string $string The RFC-like string
@ -193,7 +193,7 @@ class RfcParser
}
// this is an invalid rule, however we'll support it since the JS lib is broken
// see https://github.com/rlanvin/php-rrule/issues/25
trigger_error("This string is not compliant with the RFC (DTSTART cannot be part of RRULE). It is accepted as is for compability reasons only.", E_USER_NOTICE);
trigger_error("This string is not compliant with the RFC (DTSTART cannot be part of RRULE). It is accepted as is for compatibility reasons only.", E_USER_NOTICE);
}
$parts[$key] = $value;
}
@ -221,7 +221,7 @@ class RfcParser
foreach ($property['params'] as $name => $value) {
switch (strtoupper($name)) {
case 'TZID':
$tz = new \DateTimeZone($value);
$tz = self::parseTimeZone($value);
break;
case 'VALUE':
switch ($value) {
@ -284,7 +284,7 @@ class RfcParser
// Ignore optional words
break;
case 'TZID':
$tz = new \DateTimeZone($value);
$tz = self::parseTimeZone($value);
break;
default:
throw new \InvalidArgumentException("Unknown property parameter: $name");
@ -325,4 +325,4 @@ class RfcParser
return new \DateTimeZone($tzid);
}
}
}

174
src/i18n/cz.php Normal file
View File

@ -0,0 +1,174 @@
<?php
/**
* Translation file for Czech 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 Jakub Kluvánek <jakub@kluvanek.dev>
* @link https://github.com/rlanvin/php-rrule
*/
return array(
'yearly' => array(
'1' => 'ročně',
'2' => 'každé %{interval} roky',
'3' => 'každé %{interval} roky',
'4' => 'každé %{interval} roky',
'else' => 'každých %{interval} let'
),
'monthly' => array(
'1' => 'měsíčně',
'2' => 'každé %{interval} měsíce',
'3' => 'každé %{interval} měsíce',
'4' => 'každé %{interval} měsíce',
'else' => 'každých %{interval} měsíců'
),
'weekly' => array(
'1' => 'týdně',
'2' => 'každé %{interval} týdny',
'3' => 'každé %{interval} týdny',
'4' => 'každé %{interval} týdny',
'else' => 'každých %{interval} týdnů'
),
'daily' => array(
'1' => 'denně',
'2' => 'každé %{interval} dny',
'3' => 'každé %{interval} dny',
'4' => 'každé %{interval} dny',
'5' => 'každé %{interval} dny',
'else' => 'každých %{interval} dnů'
),
'hourly' => array(
'1' => 'každou hodinu',
'2' => 'každé %{interval} hodiny',
'3' => 'každé %{interval} hodiny',
'4' => 'každé %{interval} hodiny',
'else' => 'každých %{interval} hodin'
),
'minutely' => array(
'1' => 'každou minutu',
'2' => 'každé %{interval} minuty',
'3' => 'každé %{interval} minuty',
'4' => 'každé %{interval} minuty',
'else' => 'každých %{interval} minut'
),
'secondly' => array(
'1' => 'každou sekundu',
'2' => 'každé %{interval} sekundy',
'3' => 'každé %{interval} sekundy',
'4' => 'každé %{interval} sekundy',
'else' => 'každých %{interval} sekund'
),
'dtstart' => ', počínaje %{date}',
'timeofday' => ' v %{date}',
'startingtimeofday' => ' začínající v %{date}',
'infinite' => ', navždy',
'until' => ', do %{date}',
'count' => array(
'1' => ', jednou',
'else' => ', %{count}x'
),
'and' => 'a ',
'x_of_the_y' => array(
'yearly' => '%{x} roku', // e.g. the first Monday of the year, or the first day of the year
'monthly' => '%{x} měsíce'
),
'bymonth' => ' v %{months}',
'months' => array(
1 => 'leden',
2 => 'únor',
3 => 'březen',
4 => 'duben',
5 => 'květen',
6 => 'červen',
7 => 'červenec',
8 => 'srpen',
9 => 'září',
10 => 'říjen',
11 => 'listopad',
12 => 'prosinec'
),
'byweekday' => ' v %{weekdays}',
'weekdays' => array(
1 => 'pondělí',
2 => 'úterý',
3 => 'středa',
4 => 'čtvrtek',
5 => 'pátek',
6 => 'sobota',
7 => 'neděle'
),
'nth_weekday' => array(
'1' => 'první %{weekday}', // e.g. the first Monday
'2' => 'druhé %{weekday}',
'3' => 'třetí %{weekday}',
'else' => '%{n}. %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'poslední %{weekday}', // e.g. the last Monday
'-2' => 'předposlední %{weekday}',
'else' => '%{n}. od konce %{weekday}'
),
'byweekno' => array(
'1' => ' v týdnu %{weeks}',
'else' => ' v týdnech č.%{weeks}'
),
'nth_weekno' => '%{n}',
'bymonthday' => ' ve dnech %{monthdays}',
'nth_monthday' => array(
'else' => '%{n}.'
),
'-nth_monthday' => array(
'-1' => 'poslední',
'-2' => 'předposlední',
'else' => '%{n}. od konce'
),
'byyearday' => array(
'1' => ' ve dnu %{yeardays}',
'else' => ' ve dnech %{yeardays}'
),
'nth_yearday' => array(
'1' => 'první',
'2' => 'druhý',
'3' => 'třetí',
'else' => '%{n}.'
),
'-nth_yearday' => array(
'-1' => 'poslední',
'-2' => 'předposlední',
'else' => '%{n}. od konce'
),
'byhour' => array(
'1' => ' v hodině %{hours}',
'else' => ' v hodiny %{hours}'
),
'nth_hour' => '%{n}h',
'byminute' => array(
'1' => ' v minutu %{minutes}',
'else' => ' v minuty %{minutes}'
),
'nth_minute' => '%{n}',
'bysecond' => array(
'1' => ' v sekundě %{seconds}',
'else' => ' v sekundy %{seconds}'
),
'nth_second' => '%{n}',
'bysetpos' => ', ale pouze %{setpos} instance sady',
'nth_setpos' => array(
'1' => 'první',
'2' => 'druhá',
'3' => 'třetí',
'else' => ' %{n}.'
),
'-nth_setpos' => array(
'-1' => 'poslední',
'-2' => 'předposlední',
'else' => '%{n}. od konce'
)
);

View File

@ -44,6 +44,8 @@ return array(
'else' => 'Alle %{interval} Sekunden'
),
'dtstart' => ', ab dem %{date}',
'timeofday' => ' um %{date}',
'startingtimeofday' => ' ab %{date}',
'infinite' => ', für immer',
'until' => ', bis zum %{date}',
'count' => array(
@ -70,26 +72,26 @@ return array(
11 => 'November',
12 => 'Dezember',
),
'byweekday' => ' %{weekdays}',
'byweekday' => ' am %{weekdays}',
'weekdays' => array(
1 => 'Montags',
2 => 'Dienstags',
3 => 'Mittwochs',
4 => 'Donnerstags',
5 => 'Freitags',
6 => 'Samstags',
7 => 'Sonntags',
1 => 'Montag',
2 => 'Dienstag',
3 => 'Mittwoch',
4 => 'Donnerstag',
5 => 'Freitag',
6 => 'Samstag',
7 => 'Sonntag',
),
'nth_weekday' => array(
'1' => 'der erste %{weekday}', // e.g. the first Monday
'2' => 'der zweite %{weekday}',
'3' => 'der dritte %{weekday}',
'else' => 'der %{n}. %{weekday}'
'1' => 'ersten %{weekday}', // e.g. the first Monday
'2' => 'zweiten %{weekday}',
'3' => 'dritten %{weekday}',
'else' => '%{n}. %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'der letzte %{weekday}', // e.g. the last Monday
'-2' => 'der vorletzte %{weekday}',
'else' => 'der %{n}. letzte %{weekday}'
'-1' => 'letzten %{weekday}', // e.g. the last Monday
'-2' => 'vorletzten %{weekday}',
'else' => ' %{n}. letzten %{weekday}'
),
'byweekno' => array(
'1' => ' in Kalenderwoche %{weeks}',
@ -116,9 +118,9 @@ return array(
'else' => '%{n}.'
),
'-nth_yearday' => array(
'-1' => 'der letzte',
'-2' => 'der vorletzte',
'else' => 'der %{n}. letzte'
'-1' => 'letzten',
'-2' => 'vorletzten',
'else' => '%{n}. letzten'
),
'byhour' => array(
'1' => ' zur %{hours} Stunde',

View File

@ -46,6 +46,8 @@ return array(
'else' => 'every %{interval} seconds'
),
'dtstart' => ', starting from %{date}',
'timeofday' => ' at %{date}',
'startingtimeofday' => ' starting at %{date}',
'infinite' => ', forever',
'until' => ', until %{date}',
'count' => array(

View File

@ -18,12 +18,12 @@ return array(
),
'weekly' => array(
'1' => 'semanal',
'2' => 'cualquier otra semana',
'2' => 'semana por medio',
'else' => 'cada %{interval} semanas' // cada 8 semanas
),
'daily' => array(
'1' => 'diario',
'2' => 'cualquier otro día',
'2' => 'día por medio',
'else' => 'cada %{interval} días' // cada 8 días
),
'hourly' => array(
@ -39,6 +39,8 @@ return array(
'else' => 'cada %{interval} segundos'// cada 8 segundos
),
'dtstart' => ', empezando desde %{date}',
'timeofday' => ' a las %{date}',
'startingtimeofday' => ' empezando desde %{date}',
'infinite' => ', por siempre',
'until' => ', hasta %{date}',
'count' => array(
@ -69,10 +71,10 @@ return array(
'weekdays' => array(
1 => 'Lunes',
2 => 'Martes',
3 => 'Miercoles',
3 => 'Miércoles',
4 => 'Jueves',
5 => 'Viernes',
6 => 'Sabado',
6 => 'Sábado',
7 => 'Domingo',
),
'nth_weekday' => array(

View File

@ -42,6 +42,8 @@ return array(
'else' => 'هر %{interval} ثانیه'
),
'dtstart' => ', از %{date}',
'timeofday' => ' از %{date}',
'startingtimeofday' => ' از %{date}',
'infinite' => ', همیشه',
'until' => ', تا %{date}',
'count' => array(

View File

@ -46,6 +46,8 @@ return array(
'else' => 'joka %{interval} sekunti'
),
'dtstart' => ', alkaen %{date}',
'timeofday' => ' klo %{date}',
'startingtimeofday' => ' alkaen %{date}',
'infinite' => ', jatkuvasti',
'until' => ', %{date} asti',
'count' => array(

View File

@ -44,6 +44,8 @@ return array(
'else' => 'toutes les %{interval} secondes'
),
'dtstart' => ', à partir du %{date}',
'timeofday' => ' à %{date}',
'startingtimeofday' => ' à partir du %{date}',
'infinite' => ', indéfiniment',
'until' => ', jusqu\'au %{date}',
'count' => array(
@ -67,7 +69,7 @@ return array(
8 => 'août',
9 => 'septembre',
10 => 'octobre',
11 => 'november',
11 => 'novembre',
12 => 'décembre',
),
'byweekday' => ' le %{weekdays}',

View File

@ -41,6 +41,8 @@ return array(
'else' => 'כל %{interval} שניות'
),
'dtstart' => ', החל מ%{date}',
'timeofday' => ' החל מ%{date}',
'startingtimeofday' => ' החל מ%{date}',
'infinite' => ', לעד',
'until' => ', עד %{date}',
'count' => array(

View File

@ -38,6 +38,8 @@ return array(
'else' => 'ogni %{interval} secondi'
),
'dtstart' => ', a partire dal %{date}',
'timeofday' => ' alle %{date}',
'startingtimeofday' => ' a partire dal %{date}',
'infinite' => ', per sempre',
'until' => ', fino al %{date}',
'count' => array(

143
src/i18n/ja.php Normal file
View File

@ -0,0 +1,143 @@
<?php
/**
* Translation file for Japanese 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 Taichi Kurihara <taichi.kurihara416@gmail.com>
* @link https://github.com/Kuri-Tai/php-rrule
*/
return array(
'yearly' => array(
'1' => '毎年',
'else' => '%{interval} 年ごと',
),
'monthly' => array(
'1' => '毎月',
'else' => '%{interval} か月ごと',
),
'weekly' => array(
'1' => '毎週',
'2' => '隔週',
'else' => '%{interval} 週間ごと',
),
'daily' => array(
'1' => '毎日',
'2' => '隔日',
'else' => '%{interval} 日ごと',
),
'hourly' => array(
'1' => '毎時',
'else' => '%{interval} 時間ごと',
),
'minutely' => array(
'1' => '毎分',
'else' => '%{interval} 分ごと',
),
'secondly' => array(
'1' => '毎秒',
'else' => '%{interval} 秒ごと',
),
'dtstart' => ', %{date} から',
'infinite' => ', 期日なし',
'until' => ', %{date} まで',
'count' => array(
'1' => ', 1 回',
'else' => ', %{count} 回',
),
'and' => 'かつ ',
'x_of_the_y' => array(
'yearly' => 'その年の %{x}', // e.g. その年の 最初の 月曜日, もしくは その年の 最初の 日
'monthly' => 'その月の %{x}',
),
'bymonth' => ' %{months}',
'months' => array(
1 => '1月',
2 => '2月',
3 => '3月',
4 => '4月',
5 => '5月',
6 => '6月',
7 => '7月',
8 => '8月',
9 => '9月',
10 => '10月',
11 => '11月',
12 => '12月',
),
'byweekday' => ' %{weekdays}',
'weekdays' => array(
1 => '月曜日',
2 => '火曜日',
3 => '水曜日',
4 => '木曜日',
5 => '金曜日',
6 => '土曜日',
7 => '日曜日',
),
'nth_weekday' => array(
'1' => '最初の %{weekday}', // e.g. 最初の 月曜日
'else' => '%{n}番目の %{weekday}',
),
'-nth_weekday' => array(
'-1' => '最後の %{weekday}', // e.g. 最後の 月曜日
'else' => '最後から%{n}番目の %{weekday}',
),
'byweekno' => array(
'1' => ' 第%{weeks}週目',
'else' => ' 第%{weeks}週目',
),
'nth_weekno' => '%{n}',
'bymonthday' => ' %{monthdays}',
'nth_monthday' => array(
'1' => '1番目の',
'else' => '%{n}番目の',
),
'-nth_monthday' => array(
'-1' => '最後の',
'else' => '最後から%{n}番目の',
),
'byyearday' => array(
'1' => ' %{yeardays}',
'else' => ' %{yeardays}',
),
'nth_yearday' => array(
'1' => '1番目の',
'else' => '%{n}番目の',
),
'-nth_yearday' => array(
'-1' => '最後の',
'else' => '最後から%{n}番目の',
),
'byhour' => array(
'1' => ' %{hours}',
'else' => ' %{hours}',
),
'nth_hour' => '%{n}時',
'byminute' => array(
'1' => ' %{minutes}',
'else' => ' %{minutes}',
),
'nth_minute' => '%{n}分',
'bysecond' => array(
'1' => ' %{seconds}',
'else' => ' %{seconds}',
),
'nth_second' => '%{n}秒',
'bysetpos' => ', ただし、そのうちの %{setpos} 該当するもののみ',
'nth_setpos' => array(
'1' => '最初の',
'else' => '%{n}番目の',
),
'-nth_setpos' => array(
'-1' => '最後の',
'else' => '最後から%{n}番目の',
),
);

View File

@ -46,6 +46,8 @@ return array(
'else' => 'elke %{interval} seconden'
),
'dtstart' => ', wordt gestart vanaf %{date}',
'timeofday' => ' om %{date}',
'startingtimeofday' => ' wordt gestart vanaf %{date}',
'infinite' => ', oneindig',
'until' => ', tot en met %{date}',
'count' => array(
@ -59,28 +61,28 @@ return array(
),
'bymonth' => ' in %{months}',
'months' => array(
1 => 'Januari',
2 => 'Februari',
3 => 'Maart',
4 => 'April',
5 => 'Mei',
6 => 'Juni',
7 => 'Juli',
8 => 'Augustus',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'December',
1 => 'januari',
2 => 'februari',
3 => 'maart',
4 => 'april',
5 => 'mei',
6 => 'juni',
7 => 'juli',
8 => 'augustus',
9 => 'september',
10 => 'oktober',
11 => 'november',
12 => 'december',
),
'byweekday' => ' op %{weekdays}',
'weekdays' => array(
1 => 'Maandag',
2 => 'Dinsdag',
3 => 'Woensdag',
4 => 'Donderdag',
5 => 'Vrijdag',
6 => 'Zaterdag',
7 => 'Zondag',
1 => 'maandag',
2 => 'dinsdag',
3 => 'woensdag',
4 => 'donderdag',
5 => 'vrijdag',
6 => 'zaterdag',
7 => 'zondag',
),
'nth_weekday' => array(
'1' => 'de eerste %{weekday}', // e.g. the first Monday

171
src/i18n/pl.php Normal file
View File

@ -0,0 +1,171 @@
<?php
/**
* Translation file for Polish 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>
* @author Janusz Paszyński <j.paszynski@itchaos.pl>
* @link https://github.com/rlanvin/php-rrule
*/
return array(
'yearly' => array(
'1' => 'co roku',
'2' => 'co drugi rok',
'3' => 'co trzeci rok',
'else' => 'co %{interval} lat'
),
'monthly' => array(
'1' => 'co miesiąc',
'else' => 'co %{interval} miesięcy'
),
'weekly' => array(
'1' => 'co tydzień',
'2' => 'co drugi tydzień',
'3' => 'co trzeci tydzień',
'else' => 'co %{interval} tygodni'
),
'daily' => array(
'1' => 'codziennie',
'2' => 'co drugi dzień',
'3' => 'co trzeci dzień',
'else' => 'co %{interval} dni'
),
'hourly' => array(
'1' => 'co godzinę',
'2' => 'co drugą godzinę',
'3' => 'co trzecią godzinę',
'else' => 'co %{interval} godzin'
),
'minutely' => array(
'1' => 'co minutę',
'2' => 'co dwie minuty',
'3' => 'co trzy minuty',
'else' => 'co %{interval} minut'
),
'secondly' => array(
'1' => 'co sekundę',
'2' => 'co dwie sekundy',
'3' => 'co trzy sekundy',
'4' => 'co cztery sekundy',
'else' => 'co %{interval} sekund'
),
'dtstart' => ', zaczynając od %{date}',
'timeofday' => ' o %{date}',
'startingtimeofday' => ' zaczynając od %{date}',
'infinite' => ', zawsze',
'until' => ', do daty %{date}',
'count' => array(
'1' => ', jeden raz',
'else' => ', %{count} razy'
),
'and' => 'i ',
'x_of_the_y' => array(
'yearly' => '%{x} roku', // e.g. the first Monday of the year, or the first day of the year
'monthly' => '%{x} miesiąca',
'weekly' => '%{x} tygodnia',
),
'bymonth' => ' w %{months}',
'months' => array(
1 => 'Styczeń',
2 => 'Luty',
3 => 'Marzec',
4 => 'Kwiecień',
5 => 'Maj',
6 => 'Czerwiec',
7 => 'Lipiec',
8 => 'Sierpień',
9 => 'Wrzesień',
10 => 'Październik',
11 => 'Listopad',
12 => 'Grudzień',
),
'byweekday' => ' w %{weekdays}',
'weekdays' => array(
1 => 'Poniedziałek',
2 => 'Wtorek',
3 => 'Środa',
4 => 'Czwartek',
5 => 'Piątek',
6 => 'Sobota',
7 => 'Niedziela',
),
'nth_weekday' => array(
'1' => 'w pierwszy %{weekday}', // e.g. the first Monday
'2' => 'w drugi %{weekday}',
'3' => 'w trzeci %{weekday}',
'else' => 'w %{n} %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'w ostatni %{weekday}', // e.g. the last Monday
'-2' => 'w przedostatni %{weekday}',
'else' => 'w %{n} od końca %{weekday}'
),
'byweekno' => array(
'1' => ' w tygodniu %{weeks}',
'else' => ' w tygodniach numer %{weeks}'
),
'nth_weekno' => '%{n}',
'bymonthday' => ' w %{monthdays}',
'nth_monthday' => array(
'1' => 'pierwszy',
'2' => 'drugi',
'3' => 'trzeci',
'else' => '%{n}.'
),
'-nth_monthday' => array(
'-1' => 'ostatni',
'-2' => 'przedostatni',
'-3' => 'drugi od końca',
'else' => '%{n}. od końca'
),
'byyearday' => array(
'1' => ' pierwszego %{yeardays} dnia',
'else' => ' w dniu %{yeardays}'
),
'nth_yearday' => array(
'1' => 'pierwszy',
'2' => 'drugi',
'3' => 'trzeci',
'else' => '%{n}.'
),
'-nth_yearday' => array(
'-1' => 'ostatni',
'-2' => 'przedostatni',
'else' => '%{n}. od końca'
),
'byhour' => array(
'1' => ' o %{hours}',
'else' => ' o %{hours}'
),
'nth_hour' => '%{n}h',
'byminute' => array(
'1' => ' w minucie %{minutes}',
'else' => ' w minucie %{minutes}'
),
'nth_minute' => '%{n}',
'bysecond' => array(
'1' => ' w sekundzie %{seconds}',
'else' => ' w sekundzie %{seconds}'
),
'nth_second' => '%{n}',
'bysetpos' => ', tylko %{setpos} wystąpienie w zbiorze',
'nth_setpos' => array(
'1' => 'pierwszy',
'2' => 'drugi',
'3' => 'trzeci',
'else' => 'the %{n}th'
),
'-nth_setpos' => array(
'-1' => 'ostatni',
'-2' => 'przedostatni',
'else' => '%{n}. od końca'
)
);

162
src/i18n/pt.php Normal file
View File

@ -0,0 +1,162 @@
<?php
/**
* Translation file for Portuguese language.
*
* @author Felippe Roberto Bayestorff Duarte <felippeduarte@gmail.com>
* @link https://github.com/rlanvin/php-rrule
*/
return array(
'yearly' => array(
'1' => 'anual',
'else' => 'cada %{interval} anos' // cada 8 anos
),
'monthly' => array(
'1' => 'mensal',
'else' => 'cada %{interval} meses' //cada 8 meses
),
'weekly' => array(
'1' => 'semanal',
'2' => 'qualquer outra semana',
'else' => 'cada %{interval} semanas' // cada 8 semanas
),
'daily' => array(
'1' => 'diário',
'2' => 'qualquer outro dia',
'else' => 'cada %{interval} dias' // cada 8 dias
),
'hourly' => array(
'1' => 'cada hora',
'else' => 'cada %{interval} horas'// cada 8 horas
),
'minutely' => array(
'1' => 'cada minuto',
'else' => 'cada %{interval} minutos'// cada 8 minutos
),
'secondly' => array(
'1' => 'cada segundo',
'else' => 'cada %{interval} segundos'// cada 8 segundos
),
'dtstart' => ', começando de %{date}',
'timeofday' => ' às %{date}',
'startingtimeofday' => ' começando de %{date}',
'infinite' => ', para sempre',
'until' => ', até %{date}',
'count' => array(
'1' => ', uma vez',
'else' => ', %{count} vezes'
),
'and' => 'e ',
'x_of_the_y' => array(
'yearly' => '%{x} do ano', // e.g. the first Monday of the year, or the first day of the year
'monthly' => '%{x} do mês',
),
'bymonth' => ' em %{months}',
'months' => array(
1 => 'Janeiro',
2 => 'Fevereiro',
3 => 'Março',
4 => 'Abril',
5 => 'Maio',
6 => 'Junho',
7 => 'Julho',
8 => 'Agosto',
9 => 'Setembro',
10 => 'Outubro',
11 => 'Novembro',
12 => 'Dezembro',
),
'byweekday' => ' em %{weekdays}',
'weekdays' => array(
1 => 'Segunda-feira',
2 => 'Terça-feira',
3 => 'Quarta-feira',
4 => 'Quinta-feira',
5 => 'Sexta-feira',
6 => 'Sábado',
7 => 'Domingo',
),
'nth_weekday' => array(
'1' => 'o(a) primero(a) %{weekday}', // e.g. the first Monday
'2' => 'o(a) segundo(a) %{weekday}',
'3' => 'o(a) terceiro(a) %{weekday}',
'else' => 'o %{n}º %{weekday}'
),
'-nth_weekday' => array(
'-1' => 'o(a) último(a) %{weekday}', // e.g. the last Monday
'-2' => 'o(a) penúltimo(a) %{weekday}',
'-3' => 'o(a) antepeúltimo(a) %{weekday}',
'else' => 'o %{n}º até o último %{weekday}'
),
'byweekno' => array(
'1' => ' na semana %{weeks}',
'else' => ' nas semanas # %{weeks}'
),
'nth_weekno' => '%{n}',
'bymonthday' => ' no %{monthdays}',
'nth_monthday' => array(
'1' => 'o 1º',
'2' => 'o 2º',
'3' => 'o 3º',
'21' => 'o 21º',
'22' => 'o 22º',
'23' => 'o 23º',
'31' => 'o 31º',
'else' => 'o %{n}º'
),
'-nth_monthday' => array(
'-1' => 'o último dia',
'-2' => 'o penúltimo dia',
'-3' => 'o antepenúltimo dia',
'-21' => 'o 21º até o último dia',
'-22' => 'o 22º até o último dia',
'-23' => 'o 23º até o último dia',
'-31' => 'o 31º até o último dia',
'else' => 'o %{n}º até o último dia'
),
'byyearday' => array(
'1' => ' no %{yeardays} dia',
'else' => ' nos %{yeardays} dias'
),
'nth_yearday' => array(
'1' => 'o primero',
'2' => 'o segundo',
'3' => 'o tercero',
'else' => 'o %{n}º'
),
'-nth_yearday' => array(
'-1' => 'o último',
'-2' => 'o penúltimo',
'-3' => 'o antepenúltimo',
'else' => 'o %{n}º até o último'
),
'byhour' => array(
'1' => ' a %{hours}',
'else' => ' a %{hours}'
),
'nth_hour' => '%{n}h',
'byminute' => array(
'1' => ' ao minuto %{minutes}',
'else' => ' aos minutos %{minutes}'
),
'nth_minute' => '%{n}',
'bysecond' => array(
'1' => ' ao segundo %{seconds}',
'else' => ' aos segundos %{seconds}'
),
'nth_second' => '%{n}',
'bysetpos' => ', mas somente %{setpos} ocorrência deste conjunto',
'nth_setpos' => array(
'1' => 'o primero',
'2' => 'o segundo',
'3' => 'o terceiro',
'else' => 'o %{n}º'
),
'-nth_setpos' => array(
'-1' => 'o último',
'-2' => 'o penúltimo',
'-3' => 'o antepenúltimo',
'else' => 'o %{n}º até o último'
)
);

View File

@ -51,6 +51,8 @@ return array(
'else' => 'var %{interval}:e sekund'
),
'dtstart' => ', börjar %{date}',
'timeofday' => ' kl %{date}',
'startingtimeofday' => ' börjar %{date}',
'infinite' => ', tills vidare',
'until' => ', t.om %{date}',
'count' => array(
@ -79,7 +81,7 @@ return array(
),
'byweekday' => ' på %{weekdays}',
'weekdays' => array(
1 => 'Månday',
1 => 'Måndag',
2 => 'Tisdag',
3 => 'Onsdag',
4 => 'Torsdag',
@ -146,14 +148,14 @@ return array(
'nth_second' => '%{n}',
'bysetpos' => ', men bara %{setpos} tillfället i serien',
'nth_setpos' => array(
'1' => 'det första',
'2' => 'det andra',
'3' => 'det tredje',
'else' => 'det %{n}:e'
'1' => 'den första',
'2' => 'den andra',
'3' => 'den tredje',
'else' => 'den %{n}:e'
),
'-nth_setpos' => array(
'-1' => 'det sista',
'-2' => 'det näst sista',
'else' => 'det %{n}:e sista'
'-1' => 'den sista',
'-2' => 'den näst sista',
'else' => 'den %{n}:e sista'
)
);

168
tests/RRuleTest.php Executable file → Normal file
View File

@ -20,8 +20,12 @@ class RRuleTest extends TestCase
array(array()),
array(array('FOOBAR' => 'DAILY')),
array(array('FREQ' => 'foobar')),
'invalid string freq' => [['FREQ' => 'foobar']],
'Invalid integer frequency' => [['FREQ' => 42]],
'Array freq' => [['FREQ' => array()]],
'null freq' => [['FREQ' => null]],
'object freq' => [['FREQ' => new Stdclass()]],
array(array('FREQ' => 'DAILY', 'INTERVAL' => -1)),
array(array('FREQ' => 'DAILY', 'INTERVAL' => 1.5)),
array(array('FREQ' => 'DAILY', 'UNTIL' => 'foobar')),
@ -87,6 +91,9 @@ class RRuleTest extends TestCase
array(array('FREQ' => 'MONTHLY', 'BYSECOND' => 61)),
'Invalid WKST' => [['FREQ' => 'DAILY', 'WKST' => 'XX']],
'Null WKST' => [['FREQ' => 'DAILY', 'WKST' => null]],
'Array WKST' => [['FREQ' => 'DAILY', 'WKST' => array()]],
'Object WKST' => [['FREQ' => 'DAILY', 'WKST' => new stdClass()]],
'Invalid DTSTART (invalid date)' => [['FREQ' => 'DAILY', 'DTSTART' => new stdClass()]]
);
@ -241,15 +248,15 @@ class RRuleTest extends TestCase
$this->assertEquals($occurrences, $rule->getOccurrences());
$this->assertEquals($occurrences, $rule->getOccurrences(), 'Cached version');
foreach ($occurrences as $date) {
$this->assertTrue($rule->occursAt($date), $date->format('r').'in cached version');
$this->assertTrue($rule->occursAt($date), $date->format('r').' in cached version');
}
$rule->clearCache();
foreach ($occurrences as $date) {
$this->assertTrue($rule->occursAt($date), $date->format('r').'in uncached version');
$this->assertTrue($rule->occursAt($date), $date->format('r').' in uncached version');
}
$rule->clearCache();
for ($i = 0; $i < count($occurrences); $i++) {
$this->assertEquals($rule[$i], $occurrences[$i], 'array access uncached');
$this->assertEquals($rule[$i], $occurrences[$i], ' array access uncached');
}
}
@ -1815,6 +1822,43 @@ class RRuleTest extends TestCase
$this->assertEquals($count, $rrule->count());
}
/**
* Tests timezone transition in Daylight Savings Time switch.
*/
public function testDST()
{
$rrule = new RRule([
'FREQ' => 'WEEKLY',
'DTSTART' => new \DateTime('2022-10-30T01:00', new \DateTimeZone('America/Chicago')),
'COUNT' => 2,
]);
$this->assertSame('2022-11-06T01:00:00-05:00 CDT 1667714400', $rrule[1]->format('c T U'));
}
public function testResumeFromPartiallyFilledCache()
{
// https://github.com/rlanvin/php-rrule/issues/160
$rrule = new \RRule\RRule([
'DTSTART' => new DateTime('2023-03-31 23:59:59.000000'),
'FREQ' => 'MONTHLY',
'INTERVAL' => '12',
'WKST' => 'MO',
'COUNT' => 3
]);
// Break on first loop during first iterator use.
foreach ($rrule as $occurrence) {
break;
}
// Print first 3 occurrences (cache used).
$this->assertEquals([
date_create('2023-03-31 23:59:59'),
date_create('2024-03-31 23:59:59'),
date_create('2025-03-31 23:59:59')
],$rrule->getOccurrences());
}
///////////////////////////////////////////////////////////////////////////////
// GetOccurrences
@ -2520,6 +2564,13 @@ class RRuleTest extends TestCase
$this->assertInstanceOf('\RRule\RSet', $object);
}
public function testCreateFromRfcStringDoesntChangeCase()
{
$str = "DTSTART;TZID=Europe/Paris:20200929T000000\nRRULE:FREQ=DAILY;BYSECOND=0;BYMINUTE=0;BYHOUR=9";
$rule = RRule::createFromRfcString($str);
$this->assertEquals($str, $rule->rfcString());
}
///////////////////////////////////////////////////////////////////////////////
// Timezone
@ -2827,6 +2878,24 @@ class RRuleTest extends TestCase
], $occurrences, 'DateTimeImmutable produces valid results');
}
/**
* Test bug #104
* @see https://github.com/rlanvin/php-rrule/issues/104
*/
public function testMicrosecondsAreRemovedFromInput()
{
$dtstart = '2022-04-22 12:00:00.5';
$rule = new RRule([
'dtstart' => $dtstart,
'freq' => 'daily',
'interval' => 1,
'count' => 1
]);
$this->assertTrue($rule->occursAt('2022-04-22 12:00:00'));
$this->assertTrue($rule->occursAt('2022-04-22 12:00:00.5'));
$this->assertEquals(date_create('2022-04-22 12:00:00'), $rule[0]);
}
///////////////////////////////////////////////////////////////////////////////
// Array access and countable interfaces
@ -2949,7 +3018,7 @@ class RRuleTest extends TestCase
array('en_US.utf-8', array('en','en_US'), array('en','en_US')),
array('en_US_POSIX', array('en','en_US'), array('en','en_US')),
array('sv_SE', array('sv','sv_SE'), array('sv','sv_SE')),
// case insentitive
// case insensitive
array('en_sg', array('en','en_SG'), array('en','en_SG')),
array('sv_se', array('sv','sv_SE'), array('sv','sv_SE')),
// with a dash
@ -3075,7 +3144,7 @@ class RRuleTest extends TestCase
/**
* Tests that the RRule::i18nLoad() fails as expected on invalid $fallback settings
*/
public function testI18nLoadFallbackFailsWitoutIntl()
public function testI18nLoadFallbackFailsWithoutIntl()
{
$this->expectException(\InvalidArgumentException::class);
$reflector = new ReflectionClass('RRule\RRule');
@ -3166,6 +3235,36 @@ class RRuleTest extends TestCase
"daily",
"daily"
),
array(
"DTSTART:20170202T161500Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=2",
array('locale' => "en", 'start_time_only' => true, 'explicit_infinite' => false),
"monthly on the 2nd of the month at 4:15PM",
"monthly on the 2nd of the month at 16:15:00"
),
array(
"DTSTART:20170202T063000Z\nRRULE:FREQ=HOURLY;INTERVAL=7",
array('locale' => "en", 'start_time_only' => true, 'explicit_infinite' => false),
"every 7 hours starting at 6:30AM",
"every 7 hours starting at 06:30:00"
),
array(
"RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=+2TU",
array('locale' => "en", 'include_start' => false, 'explicit_infinite' => false),
"yearly on the second Tuesday of the month in March",
"yearly on the second Tuesday of the month in March"
),
array(
"RRULE:FREQ=YEARLY;BYMONTH=3,6,9;BYDAY=+2TU",
array('locale' => "en", 'include_start' => false, 'explicit_infinite' => false),
"yearly on the second Tuesday of the month in March, June and September",
"yearly on the second Tuesday of the month in March, June and September"
),
array(
"RRULE:FREQ=YEARLY;BYDAY=+6WE",
array('locale' => "en", 'include_start' => false, 'explicit_infinite' => false),
"yearly on the 6th Wednesday of the year",
"yearly on the 6th Wednesday of the year"
),
// with custom_path
'custom_path' => array(
"DTSTART:20170202T000000Z\nRRULE:FREQ=YEARLY;UNTIL=20170205T000000Z",
@ -3191,13 +3290,64 @@ class RRuleTest extends TestCase
/**
* @dataProvider humanReadableStrings
*/
public function testHumanReadable($rrule, $options, $withIntl, $withoutIntl, $dtstart = null)
public function testHumanReadableWithoutIntl($rrule, $options, $withIntl, $withoutIntl, $dtstart = null)
{
if ($dtstart) {
$dtstart = new DateTime($dtstart);
}
$rrule = new RRule($rrule, $dtstart);
$expected = extension_loaded('intl') ? $withIntl : $withoutIntl;
$this->assertEquals($expected, $rrule->humanReadable($options));
$options['use_intl'] = false;
$this->assertEquals($withoutIntl, $rrule->humanReadable($options));
}
/**
* @dataProvider humanReadableStrings
*/
public function testHumanReadableWithIntl($rrule, $options, $withIntl, $withoutIntl, $dtstart = null)
{
if (!extension_loaded('intl')) {
$this->markTestSkipped('intl not loaded');
}
if ($dtstart) {
$dtstart = new DateTime($dtstart);
}
$rrule = new RRule($rrule, $dtstart);
// Narrow No-Break Space (NNBSP) was added in ICU72.1 before the meridian
// as a workaround we replace unicode 0x202f char with a regular space for backwards compatibility
if (version_compare(INTL_ICU_VERSION, '72.1') < 0) {
// if you don't see the difference, use an editor that displays unicode
$withIntl = str_replace('', ' ', $withIntl);
}
$this->assertEquals($withIntl, $rrule->humanReadable($options));
}
/**
* @var \DateTimeInterface|string
*
* @dataProvider dataForTestParseDate
*/
public function testParseDate($dateTime, \DateTimeInterface $expectedDateTime)
{
$parsed = RRule::parseDate($dateTime);
$this->assertEquals($expectedDateTime->format('U.u'), $parsed->format('U.u'));
}
/**
* @return list<list<\DateTimeInterface|string>>
*/
public function dataForTestParseDate()
{
$dateTimeImmutableMicroseconds = new \DateTimeImmutable('2023-04-27 12:13:14.567');
$dateTimeImmutableNoMicroseconds = new \DateTimeImmutable('2023-04-27 12:13:14');
return [
['2023-04-27 12:13:14', new DateTime('2023-04-27 12:13:14')],
['2023-04-27 12:13:14.123', new DateTime('2023-04-27 12:13:14')],
[new DateTime('2023-04-27 12:13:14'), new DateTime('2023-04-27 12:13:14')],
[$dateTimeImmutableMicroseconds, $dateTimeImmutableNoMicroseconds],
];
}
}

View File

@ -79,6 +79,9 @@ class RfcParserTest extends TestCase
array('EXDATE;TZID=America/New_York:19970714T083000',
array(date_create('19970714T083000',new \DateTimeZone('America/New_York')))
),
array('EXDATE;TZID=W. Europe Standard Time:20230915T222222',
array(date_create('20230915T222222',new \DateTimeZone('Europe/Berlin')))
),
);
}
@ -90,4 +93,4 @@ class RfcParserTest extends TestCase
$dates = RfcParser::parseExDate($string);
$this->assertEquals($dates, $expected);
}
}
}