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

Compare commits

...

19 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
25 changed files with 559 additions and 95 deletions

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

@ -2,6 +2,36 @@
## [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
@ -20,7 +50,7 @@
### Added
- Added Portugese translation [#108](https://github.com/rlanvin/php-rrule/pull/108)
- 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
@ -91,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
@ -205,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
@ -250,7 +280,10 @@
- First release, everything before that was unversioned (`dev-master` was used).
[Unreleased]: https://github.com/rlanvin/php-rrule/compare/v2.4.1...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

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)
@ -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) {
@ -984,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
*/
@ -1358,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;
}
@ -1408,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
@ -1542,6 +1551,7 @@ class RRule implements RRuleInterface
$this->total = $total;
return;
}
$total += 1;
$this->cache[] = clone $occurrence;
yield clone $occurrence; // yield
@ -1572,6 +1582,7 @@ class RRule implements RRuleInterface
$this->total = $total;
return;
}
$total += 1;
$this->cache[] = clone $occurrence;
yield clone $occurrence; // yield
@ -2068,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 |
*
@ -2091,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
);
@ -2100,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;
}
@ -2114,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;
}
}
@ -2149,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);
};
}
}
@ -2362,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
@ -97,7 +97,7 @@ 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
*/

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

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
*/
@ -556,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(

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(

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

View File

@ -58,6 +58,8 @@ return array(
'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(

View File

@ -39,6 +39,8 @@ return array(
'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(

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'
)
);

85
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');
}
}
@ -1828,6 +1835,30 @@ class RRuleTest extends TestCase
$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
@ -2987,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
@ -3113,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');
@ -3204,6 +3235,18 @@ 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),
@ -3247,14 +3290,38 @@ 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));
}
/**

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);
}
}
}