├── LICENSE ├── README.md ├── composer.json ├── src └── Recurr │ ├── DateExclusion.php │ ├── DateInclusion.php │ ├── DateInfo.php │ ├── DateUtil.php │ ├── DaySet.php │ ├── Exception.php │ ├── Exception │ ├── InvalidArgument.php │ ├── InvalidRRule.php │ └── InvalidWeekday.php │ ├── Frequency.php │ ├── Recurrence.php │ ├── RecurrenceCollection.php │ ├── Rule.php │ ├── Time.php │ ├── Transformer │ ├── ArrayTransformer.php │ ├── ArrayTransformerConfig.php │ ├── Constraint.php │ ├── Constraint │ │ ├── AfterConstraint.php │ │ ├── BeforeConstraint.php │ │ └── BetweenConstraint.php │ ├── ConstraintInterface.php │ ├── TextTransformer.php │ ├── Translator.php │ └── TranslatorInterface.php │ └── Weekday.php ├── test.ps1 ├── test.sh └── translations ├── da.php ├── de.php ├── el.php ├── en.php ├── es.php ├── eu.php ├── fr.php ├── it.php ├── nl.php ├── no.php ├── pt-br.php ├── sv.php └── tr.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Shaun Simmons 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | ---------------------------------------------- 22 | 23 | Recurr is heavily based on rrule.js: 24 | 25 | Copyright 2010, Jakub Roztocil and Lars Schöning 26 | 27 | Redistribution and use in source and binary forms, with or without 28 | modification, are permitted provided that the following conditions are met: 29 | 30 | 1. Redistributions of source code must retain the above copyright notice, 31 | this list of conditions and the following disclaimer. 32 | 33 | 2. Redistributions in binary form must reproduce the above copyright 34 | notice, this list of conditions and the following disclaimer in the 35 | documentation and/or other materials provided with the distribution. 36 | 37 | 3. Neither the name of The author nor the names of its contributors may 38 | be used to endorse or promote products derived from this software 39 | without specific prior written permission. 40 | 41 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND 42 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 43 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS BE LIABLE FOR 45 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 46 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 47 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 48 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 49 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 50 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recurr 2 | 3 | [![tests](https://github.com/simshaun/recurr/workflows/tests/badge.svg)](https://github.com/simshaun/recurr/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/simshaun/recurr/v/stable.svg)](https://packagist.org/packages/simshaun/recurr) 5 | [![Total Downloads](https://poser.pugx.org/simshaun/recurr/downloads.svg)](https://packagist.org/packages/simshaun/recurr) 6 | [![Latest Unstable Version](https://poser.pugx.org/simshaun/recurr/v/unstable.svg)](https://packagist.org/packages/simshaun/recurr) 7 | [![License](https://poser.pugx.org/simshaun/recurr/license.svg)](https://packagist.org/packages/simshaun/recurr) 8 | 9 | Recurr is a PHP library for working with recurrence rules ([RRULE](https://tools.ietf.org/html/rfc5545)) and converting them in to DateTime objects. 10 | 11 | Recurr was developed as a precursor for a calendar with recurring events, and is heavily inspired by [rrule.js](https://github.com/jkbr/rrule). 12 | 13 | Installing Recurr 14 | ------------ 15 | 16 | The recommended way to install Recurr is through [Composer](http://getcomposer.org). 17 | 18 | ```bash 19 | composer require simshaun/recurr 20 | ``` 21 | 22 | Using Recurr 23 | ----------- 24 | 25 | ### Creating RRULE rule objects ### 26 | 27 | You can create a new Rule object by passing the ([RRULE](https://tools.ietf.org/html/rfc5545)) string or an array with the rule parts, the start date, end date (optional) and timezone. 28 | 29 | ```php 30 | $timezone = 'America/New_York'; 31 | $startDate = new \DateTime('2013-06-12 20:00:00', new \DateTimeZone($timezone)); 32 | $endDate = new \DateTime('2013-06-14 20:00:00', new \DateTimeZone($timezone)); // Optional 33 | $rule = new \Recurr\Rule('FREQ=MONTHLY;COUNT=5', $startDate, $endDate, $timezone); 34 | ``` 35 | 36 | You can also use chained methods to build your rule programmatically and get the resulting RRULE. 37 | 38 | ```php 39 | $rule = (new \Recurr\Rule) 40 | ->setStartDate($startDate) 41 | ->setTimezone($timezone) 42 | ->setFreq('DAILY') 43 | ->setByDay(['MO', 'TU']) 44 | ->setUntil(new \DateTime('2017-12-31')) 45 | ; 46 | 47 | echo $rule->getString(); //FREQ=DAILY;UNTIL=20171231T000000;BYDAY=MO,TU 48 | ``` 49 | 50 | ### RRULE to DateTime objects ### 51 | 52 | ```php 53 | $transformer = new \Recurr\Transformer\ArrayTransformer(); 54 | 55 | print_r($transformer->transform($rule)); 56 | ``` 57 | 58 | 1. `$transformer->transform(...)` returns a `RecurrenceCollection` of `Recurrence` objects. 59 | 2. Each `Recurrence` has `getStart()` and `getEnd()` methods that return a `\DateTime` object. 60 | 3. If the transformed `Rule` lacks an end date, `getEnd()` will return a `\DateTime` object equal to that of `getStart()`. 61 | 62 | > Note: The transformer has a "virtual" limit (default 732) on the number of objects it generates. 63 | > This prevents the script from crashing on an infinitely recurring rule. 64 | > You can change the virtual limit with an `ArrayTransformerConfig` object that you pass to `ArrayTransformer`. 65 | 66 | ### Transformation Constraints ### 67 | 68 | Constraints are used by the ArrayTransformer to allow or prevent certain dates from being added to a `RecurrenceCollection`. Recurr provides the following constraints: 69 | 70 | - `AfterConstraint(\DateTime $after, $inc = false)` 71 | - `BeforeConstraint(\DateTime $before, $inc = false)` 72 | - `BetweenConstraint(\DateTime $after, \DateTime $before, $inc = false)` 73 | 74 | `$inc` defines what happens if `$after` or `$before` are themselves recurrences. If `$inc = true`, they will be included in the collection. For example, 75 | 76 | ```php 77 | $startDate = new \DateTime('2014-06-17 04:00:00'); 78 | $rule = new \Recurr\Rule('FREQ=MONTHLY;COUNT=5', $startDate); 79 | $transformer = new \Recurr\Transformer\ArrayTransformer(); 80 | 81 | $constraint = new \Recurr\Transformer\Constraint\BeforeConstraint(new \DateTime('2014-08-01 00:00:00')); 82 | print_r($transformer->transform($rule, $constraint)); 83 | ``` 84 | 85 | > Note: If building your own constraint, it is important to know that dates which do not meet the constraint's requirements do **not** count toward the transformer's virtual limit. If you manually set your constraint's `$stopsTransformer` property to `false`, the transformer *might* crash via an infinite loop. See the `BetweenConstraint` for an example on how to prevent that. 86 | 87 | ### Post-Transformation `RecurrenceCollection` Filters ### 88 | 89 | `RecurrenceCollection` provides the following chainable helper methods to filter out recurrences: 90 | 91 | - `startsBetween(\DateTime $after, \DateTime $before, $inc = false)` 92 | - `startsBefore(\DateTime $before, $inc = false)` 93 | - `startsAfter(\DateTime $after, $inc = false)` 94 | - `endsBetween(\DateTime $after, \DateTime $before, $inc = false)` 95 | - `endsBefore(\DateTime $before, $inc = false)` 96 | - `endsAfter(\DateTime $after, $inc = false)` 97 | 98 | `$inc` defines what happens if `$after` or `$before` are themselves recurrences. If `$inc = true`, they will be included in the filtered collection. For example, 99 | 100 | pseudo... 101 | 2014-06-01 startsBetween(2014-06-01, 2014-06-20) // false 102 | 2014-06-01 startsBetween(2014-06-01, 2014-06-20, true) // true 103 | 104 | > Note: `RecurrenceCollection` extends the Doctrine project's [ArrayCollection](https://github.com/doctrine/collections/blob/master/lib/Doctrine/Common/Collections/ArrayCollection.php) class. 105 | 106 | RRULE to Text 107 | -------------------------- 108 | 109 | Recurr supports transforming some recurrence rules into human readable text. 110 | This feature is still in beta and only supports yearly, monthly, weekly, and daily frequencies. 111 | 112 | ```php 113 | $rule = new Rule('FREQ=YEARLY;INTERVAL=2;COUNT=3;', new \DateTime()); 114 | 115 | $textTransformer = new TextTransformer(); 116 | echo $textTransformer->transform($rule); 117 | ``` 118 | 119 | If you need more than English you can pass in a translator with one of the 120 | supported locales *(see translations folder)*. 121 | 122 | ```php 123 | $rule = new Rule('FREQ=YEARLY;INTERVAL=2;COUNT=3;', new \DateTime()); 124 | 125 | $textTransformer = new TextTransformer( 126 | new \Recurr\Transformer\Translator('de') 127 | ); 128 | echo $textTransformer->transform($rule); 129 | ``` 130 | 131 | Warnings 132 | --------------- 133 | 134 | - **Monthly recurring rules ** 135 | By default, if your start date is on the 29th, 30th, or 31st, Recurr will skip following months that don't have at least that many days. 136 | *(e.g. Jan 31 + 1 month = March)* 137 | 138 | This behavior is configurable: 139 | 140 | ```php 141 | $timezone = 'America/New_York'; 142 | $startDate = new \DateTime('2013-01-31 20:00:00', new \DateTimeZone($timezone)); 143 | $rule = new \Recurr\Rule('FREQ=MONTHLY;COUNT=5', $startDate, null, $timezone); 144 | $transformer = new \Recurr\Transformer\ArrayTransformer(); 145 | 146 | $transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig(); 147 | $transformerConfig->enableLastDayOfMonthFix(); 148 | $transformer->setConfig($transformerConfig); 149 | 150 | print_r($transformer->transform($rule)); 151 | // 2013-01-31, 2013-02-28, 2013-03-31, 2013-04-30, 2013-05-31 152 | ``` 153 | 154 | 155 | Contribute 156 | ---------- 157 | 158 | Feel free to comment or make pull requests. Please include tests with PRs. 159 | 160 | 161 | License 162 | ------- 163 | 164 | Recurr is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. 165 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simshaun/recurr", 3 | "description": "PHP library for working with recurrence rules", 4 | "keywords": ["rrule", "recurrence", "recurring", "events", "dates"], 5 | "homepage": "https://github.com/simshaun/recurr", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Shaun Simmons", 11 | "email": "shaun@shaun.pub", 12 | "homepage": "https://shaun.pub" 13 | } 14 | ], 15 | "require": { 16 | "php": "^7.2||^8.0", 17 | "doctrine/collections": "~1.6||^2.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^8.5.16", 21 | "symfony/yaml": "^5.3" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Recurr\\": "src/Recurr/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Recurr\\Test\\": "tests/Recurr/Test" 31 | } 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "0.x-dev" 36 | } 37 | }, 38 | "scripts": { 39 | "test": "./vendor/bin/phpunit --color=always" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Recurr/DateExclusion.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class DateExclusion 25 | { 26 | /** @var \DateTimeInterface */ 27 | public $date; 28 | 29 | /** @var bool Day of year */ 30 | public $hasTime; 31 | 32 | /** @var bool */ 33 | public $isUtcExplicit; 34 | 35 | /** 36 | * Constructor 37 | * 38 | * @param \DateTimeInterface $date 39 | * @param bool $hasTime 40 | * @param bool $isUtcExplicit 41 | */ 42 | public function __construct(\DateTimeInterface $date, $hasTime = true, $isUtcExplicit = false) 43 | { 44 | $this->date = $date; 45 | $this->hasTime = $hasTime; 46 | $this->isUtcExplicit = $isUtcExplicit; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Recurr/DateInclusion.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class DateInclusion 25 | { 26 | /** @var \DateTimeInterface */ 27 | public $date; 28 | 29 | /** @var bool Day of year */ 30 | public $hasTime; 31 | 32 | /** @var bool */ 33 | public $isUtcExplicit; 34 | 35 | /** 36 | * Constructor 37 | * 38 | * @param \DateTimeInterface $date 39 | * @param bool $hasTime 40 | * @param bool $isUtcExplicit 41 | */ 42 | public function __construct(\DateTimeInterface $date, $hasTime = true, $isUtcExplicit = false) 43 | { 44 | $this->date = $date; 45 | $this->hasTime = $hasTime; 46 | $this->isUtcExplicit = $isUtcExplicit; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Recurr/DateInfo.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class DateInfo 24 | { 25 | /** @var \DateTime */ 26 | public $dt; 27 | 28 | /** 29 | * @var int Number of days in the month. 30 | */ 31 | public $monthLength; 32 | 33 | /** 34 | * @var int Number of days in the year (365 normally, 366 on leap years) 35 | */ 36 | public $yearLength; 37 | 38 | /** 39 | * @var int Number of days in the next year (365 normally, 366 on leap years) 40 | */ 41 | public $nextYearLength; 42 | 43 | /** 44 | * @var array Day of year of last day of each month. 45 | */ 46 | public $mRanges; 47 | 48 | /** @var int Day of week */ 49 | public $dayOfWeek; 50 | 51 | /** @var int Day of week of the year's first day */ 52 | public $dayOfWeekYearDay1; 53 | 54 | /** 55 | * @var array Month number for each day of the year. 56 | */ 57 | public $mMask; 58 | 59 | /** 60 | * @var array Month-daynumber for each day of the year. 61 | */ 62 | public $mDayMask; 63 | 64 | /** 65 | * @var array Month-daynumber for each day of the year (in reverse). 66 | */ 67 | public $mDayMaskNeg; 68 | 69 | /** 70 | * @var array Day of week (0-6) for each day of the year, 0 being Monday 71 | */ 72 | public $wDayMask; 73 | } 74 | -------------------------------------------------------------------------------- /src/Recurr/DateUtil.php: -------------------------------------------------------------------------------- 1 | 15 | * Copyright (c) 2012 - Tomi Pieviläinen 16 | */ 17 | 18 | namespace Recurr; 19 | 20 | /** 21 | * Class DateUtil is responsible for providing utilities applicable to Rules. 22 | * 23 | * @package Recurr 24 | * @author Shaun Simmons 25 | */ 26 | class DateUtil 27 | { 28 | public static $leapBug = null; 29 | 30 | public static $monthEndDoY366 = array( 31 | 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 32 | ); 33 | 34 | public static $monthEndDoY365 = array( 35 | 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 36 | ); 37 | 38 | public static $wDayMask = array( 39 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 40 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 41 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 42 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 43 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 44 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 45 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 46 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 47 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 48 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 49 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 50 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 51 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 52 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 53 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 54 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 55 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 56 | 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 57 | 0, 1, 2, 3, 4, 5, 6, 58 | ); 59 | 60 | /** 61 | * Get an object containing info for a particular date 62 | * 63 | * @param \DateTimeInterface $dt 64 | * 65 | * @return DateInfo 66 | */ 67 | public static function getDateInfo(\DateTimeInterface $dt) 68 | { 69 | $i = new DateInfo(); 70 | $i->dt = $dt; 71 | $i->dayOfWeek = self::getDayOfWeek($dt); 72 | $i->monthLength = $dt->format('t'); 73 | $i->yearLength = self::getYearLength($dt); 74 | 75 | $i->mMask = self::getMonthMask($dt); 76 | $i->mDayMask = self::getMonthDaysMask($dt); 77 | $i->mDayMaskNeg = self::getMonthDaysMask($dt, true); 78 | 79 | if ($i->yearLength == 365) { 80 | $i->mRanges = self::$monthEndDoY365; 81 | } else { 82 | $i->mRanges = self::$monthEndDoY366; 83 | } 84 | 85 | $tmpDt = clone $dt; 86 | $tmpDt = $tmpDt->setDate($dt->format('Y') + 1, 1, 1); 87 | $i->nextYearLength = self::getYearLength($tmpDt); 88 | 89 | $tmpDt = clone $dt; 90 | $tmpDt = $tmpDt->setDate($dt->format('Y'), 1, 1); 91 | $i->dayOfWeekYearDay1 = self::getDayOfWeek($tmpDt); 92 | 93 | $i->wDayMask = array_slice( 94 | self::$wDayMask, 95 | $i->dayOfWeekYearDay1 96 | ); 97 | 98 | return $i; 99 | } 100 | 101 | /** 102 | * Get an array of DOY (Day of Year) for each day in a particular week. 103 | * 104 | * @param \DateTimeInterface $dt 105 | * @param \DateTimeInterface $start 106 | * @param null|Rule $rule 107 | * @param null|DateInfo $dtInfo 108 | * 109 | * @return DaySet 110 | */ 111 | public static function getDaySetOfWeek( 112 | \DateTimeInterface $dt, 113 | \DateTimeInterface $start, 114 | ?Rule $rule = null, 115 | ?DateInfo $dtInfo = null 116 | ) 117 | { 118 | $start = clone $dt; 119 | $start = $start->setDate($start->format('Y'), 1, 1); 120 | 121 | $diff = $dt->diff($start); 122 | $start = $diff->days; 123 | 124 | $set = array(); 125 | for ($i = $start, $k = 0; $k < 7; $k++) { 126 | $set[] = $i; 127 | ++$i; 128 | 129 | if (null !== $dtInfo && null !== $rule && $dtInfo->wDayMask[$i] == $rule->getWeekStartAsNum()) { 130 | break; 131 | } 132 | } 133 | 134 | $obj = new DaySet($set, $start, $i); 135 | 136 | return $obj; 137 | } 138 | 139 | /** 140 | * @param Rule $rule 141 | * @param \DateTimeInterface $dt 142 | * @param DateInfo $dtInfo 143 | * @param \DateTimeInterface $start 144 | * 145 | * @return DaySet 146 | */ 147 | public static function getDaySet(Rule $rule, \DateTimeInterface $dt, DateInfo $dtInfo, $start) 148 | { 149 | switch ($rule->getFreq()) { 150 | case Frequency::SECONDLY: 151 | return self::getDaySetOfDay($dt, $start, $rule, $dtInfo); 152 | break; 153 | case Frequency::MINUTELY: 154 | return self::getDaySetOfDay($dt, $start, $rule, $dtInfo); 155 | break; 156 | case Frequency::HOURLY: 157 | return self::getDaySetOfDay($dt, $start, $rule, $dtInfo); 158 | break; 159 | case Frequency::DAILY: 160 | return self::getDaySetOfDay($dt, $start, $rule, $dtInfo); 161 | break; 162 | case Frequency::WEEKLY: 163 | return self::getDaySetOfWeek($dt, $start, $rule, $dtInfo); 164 | case Frequency::MONTHLY: 165 | return self::getDaySetOfMonth($dt, $start, $rule, $dtInfo); 166 | case Frequency::YEARLY: 167 | return self::getDaySetOfYear($dt, $start, $rule, $dtInfo); 168 | } 169 | 170 | throw new \RuntimeException('Invalid freq.'); 171 | } 172 | 173 | /** 174 | * Get an array of DOY (Day of Year) for each day in a particular year. 175 | * 176 | * @param \DateTimeInterface $dt The datetime 177 | * 178 | * @return DaySet 179 | */ 180 | public static function getDaySetOfYear(\DateTimeInterface $dt) 181 | { 182 | $yearLen = self::getYearLength($dt); 183 | $set = range(0, $yearLen - 1); 184 | 185 | return new DaySet($set, 0, $yearLen); 186 | } 187 | 188 | /** 189 | * Get an array of DOY (Day of Year) for each day in a particular month. 190 | * 191 | * @param \DateTimeInterface $dt The datetime 192 | * 193 | * @return DaySet 194 | */ 195 | public static function getDaySetOfMonth(\DateTimeInterface $dt) 196 | { 197 | $dateInfo = self::getDateInfo($dt); 198 | $monthNum = $dt->format('n'); 199 | 200 | $start = $dateInfo->mRanges[$monthNum - 1]; 201 | $end = $dateInfo->mRanges[$monthNum]; 202 | 203 | $days = range(0, $dt->format('t') - 1); 204 | $set = range($start, $end - 1); 205 | $set = array_combine($days, $set); 206 | $obj = new DaySet($set, $start, $end - 1); 207 | 208 | return $obj; 209 | } 210 | 211 | /** 212 | * Get an array of DOY (Day of Year) for each day in a particular month. 213 | * 214 | * @param \DateTimeInterface $dt The datetime 215 | * 216 | * @return DaySet 217 | */ 218 | public static function getDaySetOfDay(\DateTimeInterface $dt) 219 | { 220 | $dayOfYear = $dt->format('z'); 221 | 222 | if (self::isLeapYearDate($dt) && self::hasLeapYearBug() && $dt->format('nj') > 229) { 223 | $dayOfYear -= 1; 224 | } 225 | 226 | $start = $dayOfYear; 227 | $end = $dayOfYear; 228 | 229 | $set = range($start, $end); 230 | $obj = new DaySet($set, $start, $end + 1); 231 | 232 | return $obj; 233 | } 234 | 235 | /** 236 | * @param Rule $rule 237 | * @param \DateTimeInterface $dt 238 | * 239 | * @return array 240 | */ 241 | public static function getTimeSetOfHour(Rule $rule, \DateTimeInterface $dt) 242 | { 243 | $set = array(); 244 | 245 | $hour = $dt->format('G'); 246 | $byMinute = $rule->getByMinute(); 247 | $bySecond = $rule->getBySecond(); 248 | 249 | if (empty($byMinute)) { 250 | $byMinute = array($dt->format('i')); 251 | } 252 | 253 | if (empty($bySecond)) { 254 | $bySecond = array($dt->format('s')); 255 | } 256 | 257 | foreach ($byMinute as $minute) { 258 | foreach ($bySecond as $second) { 259 | $set[] = new Time($hour, $minute, $second); 260 | } 261 | } 262 | 263 | return $set; 264 | } 265 | 266 | /** 267 | * @param Rule $rule 268 | * @param \DateTimeInterface $dt 269 | * 270 | * @return array 271 | */ 272 | public static function getTimeSetOfMinute(Rule $rule, \DateTimeInterface $dt) 273 | { 274 | $set = array(); 275 | 276 | $hour = $dt->format('G'); 277 | $minute = $dt->format('i'); 278 | $bySecond = $rule->getBySecond(); 279 | 280 | if (empty($bySecond)) { 281 | $bySecond = array($dt->format('s')); 282 | } 283 | 284 | foreach ($bySecond as $second) { 285 | $set[] = new Time($hour, $minute, $second); 286 | } 287 | 288 | return $set; 289 | } 290 | 291 | /** 292 | * @param \DateTimeInterface $dt 293 | * 294 | * @return array 295 | */ 296 | public static function getTimeSetOfSecond(\DateTimeInterface $dt) 297 | { 298 | return array(new Time($dt->format('G'), $dt->format('i'), $dt->format('s'))); 299 | } 300 | 301 | /** 302 | * @param Rule $rule 303 | * @param \DateTimeInterface $dt 304 | * 305 | * @return array 306 | */ 307 | public static function getTimeSet(Rule $rule, \DateTimeInterface $dt) 308 | { 309 | $set = array(); 310 | 311 | if (null === $rule || $rule->getFreq() >= Frequency::HOURLY) { 312 | return $set; 313 | } 314 | 315 | $byHour = $rule->getByHour(); 316 | $byMinute = $rule->getByMinute(); 317 | $bySecond = $rule->getBySecond(); 318 | 319 | if (empty($byHour)) { 320 | $byHour = array($dt->format('G')); 321 | } 322 | 323 | if (empty($byMinute)) { 324 | $byMinute = array($dt->format('i')); 325 | } 326 | 327 | if (empty($bySecond)) { 328 | $bySecond = array($dt->format('s')); 329 | } 330 | 331 | foreach ($byHour as $hour) { 332 | foreach ($byMinute as $minute) { 333 | foreach ($bySecond as $second) { 334 | $set[] = new Time($hour, $minute, $second); 335 | } 336 | } 337 | } 338 | 339 | return $set; 340 | } 341 | 342 | /** 343 | * Get a reference array with the day number for each day of each month. 344 | * 345 | * @param \DateTimeInterface $dt The datetime 346 | * @param bool $negative 347 | * 348 | * @return array 349 | */ 350 | public static function getMonthDaysMask(\DateTimeInterface $dt, $negative = false) 351 | { 352 | if ($negative) { 353 | $m29 = range(-29, -1); 354 | $m30 = range(-30, -1); 355 | $m31 = range(-31, -1); 356 | } else { 357 | $m29 = range(1, 29); 358 | $m30 = range(1, 30); 359 | $m31 = range(1, 31); 360 | } 361 | 362 | $mask = array_merge( 363 | $m31, // Jan (31) 364 | $m29, // Feb (28) 365 | $m31, // Mar (31) 366 | $m30, // Apr (30) 367 | $m31, // May (31) 368 | $m30, // Jun (30) 369 | $m31, // Jul (31) 370 | $m31, // Aug (31) 371 | $m30, // Sep (30) 372 | $m31, // Oct (31) 373 | $m30, // Nov (30) 374 | $m31, // Dec (31) 375 | array_slice( 376 | $m31, 377 | 0, 378 | 7 379 | ) 380 | ); 381 | 382 | if (self::isLeapYearDate($dt)) { 383 | return $mask; 384 | } else { 385 | if ($negative) { 386 | $mask = array_merge(array_slice($mask, 0, 31), array_slice($mask, 32)); 387 | } else { 388 | $mask = array_merge(array_slice($mask, 0, 59), array_slice($mask, 60)); 389 | } 390 | 391 | return $mask; 392 | } 393 | } 394 | 395 | public static function getMonthMask(\DateTimeInterface $dt) 396 | { 397 | if (self::isLeapYearDate($dt)) { 398 | return array_merge( 399 | array_fill(0, 31, 1), // Jan (31) 400 | array_fill(0, 29, 2), // Feb (29) 401 | array_fill(0, 31, 3), // Mar (31) 402 | array_fill(0, 30, 4), // Apr (30) 403 | array_fill(0, 31, 5), // May (31) 404 | array_fill(0, 30, 6), // Jun (30) 405 | array_fill(0, 31, 7), // Jul (31) 406 | array_fill(0, 31, 8), // Aug (31) 407 | array_fill(0, 30, 9), // Sep (30) 408 | array_fill(0, 31, 10), // Oct (31) 409 | array_fill(0, 30, 11), // Nov (30) 410 | array_fill(0, 31, 12), // Dec (31) 411 | array_fill(0, 7, 1) 412 | ); 413 | } else { 414 | return array_merge( 415 | array_fill(0, 31, 1), // Jan (31) 416 | array_fill(0, 28, 2), // Feb (28) 417 | array_fill(0, 31, 3), // Mar (31) 418 | array_fill(0, 30, 4), // Apr (30) 419 | array_fill(0, 31, 5), // May (31) 420 | array_fill(0, 30, 6), // Jun (30) 421 | array_fill(0, 31, 7), // Jul (31) 422 | array_fill(0, 31, 8), // Aug (31) 423 | array_fill(0, 30, 9), // Sep (30) 424 | array_fill(0, 31, 10), // Oct (31) 425 | array_fill(0, 30, 11), // Nov (30) 426 | array_fill(0, 31, 12), // Dec (31) 427 | array_fill(0, 7, 1) 428 | ); 429 | } 430 | } 431 | 432 | public static function getDateTimeByDayOfYear($dayOfYear, $year, \DateTimeZone $timezone) 433 | { 434 | $dtTmp = new \DateTime('now', $timezone); 435 | $dtTmp = $dtTmp->setDate($year, 1, 1); 436 | $dtTmp = $dtTmp->modify("+$dayOfYear day"); 437 | 438 | return $dtTmp; 439 | } 440 | 441 | public static function hasLeapYearBug() 442 | { 443 | $leapBugTest = \DateTime::createFromFormat('Y-m-d', '2016-03-21'); 444 | return $leapBugTest->format('z') != '80'; 445 | } 446 | 447 | /** 448 | * closure/goog/math/math.js:modulo 449 | * Copyright 2006 The Closure Library Authors. 450 | * 451 | * The % operator in PHP returns the remainder of a / b, but differs from 452 | * some other languages in that the result will have the same sign as the 453 | * dividend. For example, -1 % 8 == -1, whereas in some other languages 454 | * (such as Python) the result would be 7. This function emulates the more 455 | * correct modulo behavior, which is useful for certain applications such as 456 | * calculating an offset index in a circular list. 457 | * 458 | * @param int $a The dividend. 459 | * @param int $b The divisor. 460 | * 461 | * @return int $a % $b where the result is between 0 and $b 462 | * (either 0 <= x < $b 463 | * or $b < x <= 0, depending on the sign of $b). 464 | */ 465 | public static function pymod($a, $b) 466 | { 467 | $x = $a % $b; 468 | 469 | // If $x and $b differ in sign, add $b to wrap the result to the correct sign. 470 | return ($x * $b < 0) ? $x + $b : $x; 471 | } 472 | 473 | /** 474 | * Alias method to determine if a date falls within a leap year. 475 | * 476 | * @param \DateTimeInterface $dt 477 | * 478 | * @return bool 479 | */ 480 | public static function isLeapYearDate(\DateTimeInterface $dt) 481 | { 482 | return $dt->format('L') ? true : false; 483 | } 484 | 485 | /** 486 | * Alias method to determine if a year is a leap year. 487 | * 488 | * @param int $year 489 | * 490 | * @return bool 491 | */ 492 | public static function isLeapYear($year) 493 | { 494 | $isDivisBy4 = $year % 4 == 0 ? true : false; 495 | $isDivisBy100 = $year % 100 == 0? true : false; 496 | $isDivisBy400 = $year % 400 == 0 ? true : false; 497 | 498 | // http://en.wikipedia.org/wiki/February_29 499 | if ($isDivisBy100 && !$isDivisBy400) { 500 | return false; 501 | } 502 | 503 | return $isDivisBy4; 504 | } 505 | 506 | /** 507 | * Method to determine the day of the week from MO-SU. 508 | * 509 | * MO = Monday 510 | * TU = Tuesday 511 | * WE = Wednesday 512 | * TH = Thursday 513 | * FR = Friday 514 | * SA = Saturday 515 | * SU = Sunday 516 | * 517 | * @param \DateTimeInterface $dt 518 | * 519 | * @return string 520 | */ 521 | public static function getDayOfWeekAsText(\DateTimeInterface $dt) 522 | { 523 | $dayOfWeek = $dt->format('w') - 1; 524 | 525 | if ($dayOfWeek < 0) { 526 | $dayOfWeek = 6; 527 | } 528 | 529 | $map = array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'); 530 | 531 | return $map[$dayOfWeek]; 532 | } 533 | 534 | /** 535 | * Alias method to determine the day of the week from 0-6. 536 | * 537 | * 0 = Monday 538 | * 1 = Tuesday 539 | * 2 = Wednesday 540 | * 3 = Thursday 541 | * 4 = Friday 542 | * 5 = Saturday 543 | * 6 = Sunday 544 | * 545 | * @param \DateTimeInterface $dt 546 | * 547 | * @return int 548 | */ 549 | public static function getDayOfWeek(\DateTimeInterface $dt) 550 | { 551 | $dayOfWeek = $dt->format('w') - 1; 552 | 553 | if ($dayOfWeek < 0) { 554 | $dayOfWeek = 6; 555 | } 556 | 557 | return $dayOfWeek; 558 | } 559 | 560 | /** 561 | * Get the number of days in a year. 562 | * 563 | * @param \DateTimeInterface $dt 564 | * 565 | * @return int 566 | */ 567 | public static function getYearLength(\DateTimeInterface $dt) 568 | { 569 | return self::isLeapYearDate($dt) ? 366 : 365; 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /src/Recurr/DaySet.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class DaySet 23 | { 24 | /** @var array */ 25 | public $set; 26 | 27 | /** @var int Day of year */ 28 | public $start; 29 | 30 | /** @var int Day of year */ 31 | public $end; 32 | 33 | /** 34 | * Constructor 35 | * 36 | * @param array $set Set of days 37 | * @param int $start Day of year of start day 38 | * @param int $end Day of year of end day 39 | */ 40 | public function __construct($set, $start, $end) 41 | { 42 | $this->set = $set; 43 | $this->start = $start; 44 | $this->end = $end; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Recurr/Exception.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class InvalidArgument extends Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Recurr/Exception/InvalidRRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class InvalidRRule extends Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Recurr/Exception/InvalidWeekday.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class InvalidWeekday extends Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Recurr/Frequency.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Recurrence 20 | { 21 | /** @var \DateTimeInterface */ 22 | protected $start; 23 | 24 | /** @var \DateTimeInterface */ 25 | protected $end; 26 | 27 | /** @var int */ 28 | protected $index; 29 | 30 | public function __construct(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null, $index = 0) 31 | { 32 | if ($start instanceof \DateTimeInterface) { 33 | $this->setStart($start); 34 | } 35 | 36 | if ($end instanceof \DateTimeInterface) { 37 | $this->setEnd($end); 38 | } 39 | 40 | $this->index = $index; 41 | } 42 | 43 | /** 44 | * @return \DateTimeInterface 45 | */ 46 | public function getStart() 47 | { 48 | return $this->start; 49 | } 50 | 51 | /** 52 | * @param \DateTime $start 53 | */ 54 | public function setStart($start) 55 | { 56 | $this->start = $start; 57 | } 58 | 59 | /** 60 | * @return \DateTime 61 | */ 62 | public function getEnd() 63 | { 64 | return $this->end; 65 | } 66 | 67 | /** 68 | * @param \DateTime $end 69 | */ 70 | public function setEnd($end) 71 | { 72 | $this->end = $end; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getIndex() 79 | { 80 | return $this->index; 81 | } 82 | 83 | /** 84 | * @param int $index 85 | */ 86 | public function setIndex($index) 87 | { 88 | $this->index = $index; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Recurr/RecurrenceCollection.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RecurrenceCollection extends BaseCollection 19 | { 20 | /** 21 | * @param \DateTimeInterface $after 22 | * @param \DateTimeInterface $before 23 | * @param bool $inc Include $after or $before if they happen to be a recurrence. 24 | * 25 | * @return RecurrenceCollection 26 | */ 27 | public function startsBetween(\DateTimeInterface $after, \DateTimeInterface $before, $inc = false) 28 | { 29 | return $this->filter( 30 | function ($recurrence) use ($after, $before, $inc) { 31 | /** @var $recurrence Recurrence */ 32 | $start = $recurrence->getStart(); 33 | 34 | if ($inc) { 35 | return $start >= $after && $start <= $before; 36 | } 37 | 38 | return $start > $after && $start < $before; 39 | } 40 | ); 41 | } 42 | 43 | /** 44 | * @param \DateTimeInterface $before 45 | * @param bool $inc Include $before if it is a recurrence. 46 | * 47 | * @return RecurrenceCollection 48 | */ 49 | public function startsBefore(\DateTimeInterface $before, $inc = false) 50 | { 51 | return $this->filter( 52 | function ($recurrence) use ($before, $inc) { 53 | /** @var $recurrence Recurrence */ 54 | $start = $recurrence->getStart(); 55 | 56 | if ($inc) { 57 | return $start <= $before; 58 | } 59 | 60 | return $start < $before; 61 | } 62 | ); 63 | } 64 | 65 | /** 66 | * @param \DateTimeInterface $after 67 | * @param bool $inc Include $after if it a recurrence. 68 | * 69 | * @return RecurrenceCollection 70 | */ 71 | public function startsAfter(\DateTimeInterface $after, $inc = false) 72 | { 73 | return $this->filter( 74 | function ($recurrence) use ($after, $inc) { 75 | /** @var $recurrence Recurrence */ 76 | $start = $recurrence->getStart(); 77 | 78 | if ($inc) { 79 | return $start >= $after; 80 | } 81 | 82 | return $start > $after; 83 | } 84 | ); 85 | } 86 | 87 | /** 88 | * @param \DateTimeInterface $after 89 | * @param \DateTimeInterface $before 90 | * @param bool $inc Include $after or $before if they happen to be a recurrence. 91 | * 92 | * @return RecurrenceCollection 93 | */ 94 | public function endsBetween(\DateTimeInterface $after, \DateTimeInterface $before, $inc = false) 95 | { 96 | return $this->filter( 97 | function ($recurrence) use ($after, $before, $inc) { 98 | /** @var $recurrence Recurrence */ 99 | $end = $recurrence->getEnd(); 100 | 101 | if ($inc) { 102 | return $end >= $after && $end <= $before; 103 | } 104 | 105 | return $end > $after && $end < $before; 106 | } 107 | ); 108 | } 109 | 110 | /** 111 | * @param \DateTimeInterface $before 112 | * @param bool $inc Include $before if it is a recurrence. 113 | * 114 | * @return RecurrenceCollection 115 | */ 116 | public function endsBefore(\DateTimeInterface $before, $inc = false) 117 | { 118 | return $this->filter( 119 | function ($recurrence) use ($before, $inc) { 120 | /** @var $recurrence Recurrence */ 121 | $end = $recurrence->getEnd(); 122 | 123 | if ($inc) { 124 | return $end <= $before; 125 | } 126 | 127 | return $end < $before; 128 | } 129 | ); 130 | } 131 | 132 | /** 133 | * @param \DateTimeInterface $after 134 | * @param bool $inc Include $after if it a recurrence. 135 | * 136 | * @return RecurrenceCollection 137 | */ 138 | public function endsAfter(\DateTimeInterface $after, $inc = false) 139 | { 140 | return $this->filter( 141 | function ($recurrence) use ($after, $inc) { 142 | /** @var $recurrence Recurrence */ 143 | $end = $recurrence->getEnd(); 144 | 145 | if ($inc) { 146 | return $end >= $after; 147 | } 148 | 149 | return $end > $after; 150 | } 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Recurr/Rule.php: -------------------------------------------------------------------------------- 1 | 15 | * Copyright (c) 2012 - Tomi Pieviläinen 16 | */ 17 | 18 | namespace Recurr; 19 | 20 | use Recurr\Exception\InvalidArgument; 21 | use Recurr\Exception\InvalidRRule; 22 | use Recurr\Exception\InvalidWeekday; 23 | 24 | /** 25 | * This class is responsible for providing a programmatic way of building, 26 | * parsing, and handling RRULEs. 27 | * 28 | * http://www.ietf.org/rfc/rfc2445.txt 29 | * 30 | * Information, not contained in the built/parsed RRULE, necessary to determine 31 | * the various recurrence instance start time and dates are derived from the 32 | * DTSTART property (default: \DateTime()). 33 | * 34 | * For example, "FREQ=YEARLY;BYMONTH=1" doesn't specify a specific day within 35 | * the month or a time. This information would be the same as what is specified 36 | * for DTSTART. 37 | * 38 | * 39 | * BYxxx rule parts modify the recurrence in some manner. BYxxx rule parts for 40 | * a period of time which is the same or greater than the frequency generally 41 | * reduce or limit the number of occurrences of the recurrence generated. 42 | * 43 | * For example, "FREQ=DAILY;BYMONTH=1" reduces the number of recurrence 44 | * instances from all days (if BYMONTH tag is not present) to all days in 45 | * January. 46 | * 47 | * BYxxx rule parts for a period of time less than the frequency generally 48 | * increase or expand the number of occurrences of the recurrence. 49 | * 50 | * For example, "FREQ=YEARLY;BYMONTH=1,2" increases the number of days within 51 | * the yearly recurrence set from 1 (if BYMONTH tag is not present) to 2. 52 | * 53 | * If multiple BYxxx rule parts are specified, then after evaluating the 54 | * specified FREQ and INTERVAL rule parts, the BYxxx rule parts are applied to 55 | * the current set of evaluated occurrences in the following order: 56 | * 57 | * BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, 58 | * BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are evaluated. 59 | * 60 | * Here is an example of evaluating multiple BYxxx rule parts. 61 | * 62 | * FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30 63 | * 64 | * First, the "INTERVAL=2" would be applied to "FREQ=YEARLY" to arrive at 65 | * "every other year". 66 | * Then, "BYMONTH=1" would be applied to arrive at "every January, every 67 | * other year". 68 | * Then, "BYDAY=SU" would be applied to arrive at "every Sunday in January, 69 | * every other year". 70 | * Then, "BYHOUR=8,9" would be applied to arrive at "every Sunday in January 71 | * at 8 AM and 9 AM, every other year". 72 | * Then, "BYMINUTE=30" would be applied to arrive at "every Sunday in January 73 | * at 8:30 AM and 9:30 AM, every other year". 74 | * Then, lacking information from RRULE, the second is derived from DTSTART, to 75 | * end up in "every Sunday in January at 8:30:00 AM and 9:30:00 AM, every 76 | * other year". Similarly, if the BYMINUTE, BYHOUR, BYDAY, BYMONTHDAY or 77 | * BYMONTH rule part were missing, the appropriate minute, hour, day or month 78 | * would have been retrieved from the "DTSTART" property. 79 | * 80 | * Example: The following is a rule which specifies 10 meetings which occur 81 | * every other day: 82 | * 83 | * FREQ=DAILY;COUNT=10;INTERVAL=2 84 | * 85 | * @package Recurr 86 | * @author Shaun Simmons 87 | */ 88 | class Rule 89 | { 90 | const TZ_FIXED = 'fixed'; 91 | const TZ_FLOAT = 'floating'; 92 | 93 | public static $freqs = array( 94 | 'YEARLY' => 0, 95 | 'MONTHLY' => 1, 96 | 'WEEKLY' => 2, 97 | 'DAILY' => 3, 98 | 'HOURLY' => 4, 99 | 'MINUTELY' => 5, 100 | 'SECONDLY' => 6, 101 | ); 102 | 103 | /** @var string */ 104 | protected $timezone; 105 | 106 | /** @var \DateTimeInterface|null */ 107 | protected $startDate; 108 | 109 | /** @var \DateTimeInterface|null */ 110 | protected $endDate; 111 | 112 | /** @var bool */ 113 | protected $isStartDateFromDtstart = false; 114 | 115 | /** @var string */ 116 | protected $freq; 117 | 118 | /** @var int */ 119 | protected $interval = 1; 120 | 121 | /** @var bool */ 122 | protected $isExplicitInterval = false; 123 | 124 | /** @var \DateTimeInterface|null */ 125 | protected $until; 126 | 127 | /** @var int|null */ 128 | protected $count; 129 | 130 | /** @var array */ 131 | protected $bySecond; 132 | 133 | /** @var array */ 134 | protected $byMinute; 135 | 136 | /** @var array */ 137 | protected $byHour; 138 | 139 | /** @var array */ 140 | protected $byDay; 141 | 142 | /** @var array */ 143 | protected $byMonthDay; 144 | 145 | /** @var array */ 146 | protected $byYearDay; 147 | 148 | /** @var array */ 149 | protected $byWeekNumber; 150 | 151 | /** @var array */ 152 | protected $byMonth; 153 | 154 | /** @var string */ 155 | protected $weekStart = 'MO'; 156 | protected $weekStartDefined = false; 157 | 158 | /** @var array */ 159 | protected $days = array( 160 | 'MO' => 0, 161 | 'TU' => 1, 162 | 'WE' => 2, 163 | 'TH' => 3, 164 | 'FR' => 4, 165 | 'SA' => 5, 166 | 'SU' => 6 167 | ); 168 | 169 | /** @var int[] */ 170 | protected $bySetPosition; 171 | 172 | /** @var array */ 173 | protected $rDates = array(); 174 | 175 | /** @var array */ 176 | protected $exDates = array(); 177 | 178 | /** 179 | * Construct a new Rule. 180 | * 181 | * @param string $rrule RRULE string 182 | * @param string|\DateTimeInterface|null $startDate 183 | * @param string|\DateTimeInterface|null $endDate 184 | * @param string $timezone 185 | * 186 | * @throws InvalidRRule 187 | */ 188 | public function __construct($rrule = null, $startDate = null, $endDate = null, $timezone = null) 189 | { 190 | if (empty($timezone)) { 191 | if ($startDate instanceof \DateTimeInterface) { 192 | $timezone = $startDate->getTimezone()->getName(); 193 | } else { 194 | $timezone = date_default_timezone_get(); 195 | } 196 | } 197 | $this->setTimezone($timezone); 198 | 199 | if ($startDate !== null && !$startDate instanceof \DateTimeInterface) { 200 | $startDate = new \DateTime($startDate, new \DateTimeZone($timezone)); 201 | } 202 | 203 | $this->setStartDate($startDate); 204 | 205 | if ($endDate !== null && !$endDate instanceof \DateTimeInterface) { 206 | $endDate = new \DateTime($endDate, new \DateTimeZone($timezone)); 207 | } 208 | 209 | $this->setEndDate($endDate); 210 | 211 | if (is_array($rrule)) { 212 | $this->loadFromArray($rrule); 213 | } else if (!empty($rrule)) { 214 | $this->loadFromString($rrule); 215 | } 216 | } 217 | 218 | /** 219 | * Create a Rule object based on a RRULE string. 220 | * 221 | * @param string $rrule RRULE string 222 | * @param string|\DateTimeInterface $startDate 223 | * @param \DateTimeInterface|null $endDate 224 | * @param string $timezone 225 | * 226 | * @return Rule 227 | * @throws InvalidRRule 228 | */ 229 | public static function createFromString($rrule, $startDate = null, $endDate = null, $timezone = null) 230 | { 231 | $rule = new static($rrule, $startDate, $endDate, $timezone); 232 | 233 | return $rule; 234 | } 235 | 236 | /** 237 | * Create a Rule object based on a RRULE array. 238 | * 239 | * @param array $rrule RRULE array 240 | * @param string|\DateTimeInterface $startDate 241 | * @param \DateTimeInterface|null $endDate 242 | * @param string $timezone 243 | * 244 | * @return Rule 245 | * @throws InvalidRRule 246 | */ 247 | public static function createFromArray($rrule, $startDate = null, $endDate = null, $timezone = null) 248 | { 249 | $rule = new static($rrule, $startDate, $endDate, $timezone); 250 | 251 | return $rule; 252 | } 253 | 254 | /** 255 | * Populate the object based on a RRULE string. 256 | * 257 | * @param string $rrule RRULE string 258 | * 259 | * @return Rule 260 | * @throws InvalidRRule 261 | */ 262 | public function loadFromString($rrule) 263 | { 264 | $rrule = strtoupper($rrule); 265 | $rrule = trim($rrule, ';'); 266 | $rrule = trim($rrule, "\n"); 267 | $rows = explode("\n", $rrule); 268 | 269 | $parts = array(); 270 | 271 | foreach ($rows as $rruleForRow) { 272 | $parts = array_merge($parts, $this->parseString($rruleForRow)); 273 | } 274 | 275 | return $this->loadFromArray($parts); 276 | } 277 | 278 | /** 279 | * Parse string for parts 280 | * 281 | * @param string $rrule 282 | * 283 | * @return array 284 | * 285 | * @throws InvalidRRule 286 | */ 287 | public function parseString($rrule) 288 | { 289 | if (strpos($rrule, 'DTSTART:') === 0) { 290 | $pieces = explode(':', $rrule); 291 | 292 | if (count($pieces) !== 2) { 293 | throw new InvalidRRule('DSTART is not valid'); 294 | } 295 | 296 | return array('DTSTART' => $pieces[1]); 297 | } 298 | 299 | if (strpos($rrule, 'RRULE:') === 0) { 300 | $rrule = str_replace('RRULE:', '', $rrule); 301 | } 302 | 303 | $pieces = explode(';', $rrule); 304 | $parts = array(); 305 | 306 | if (!count($pieces)) { 307 | throw new InvalidRRule('RRULE is empty'); 308 | } 309 | 310 | // Split each piece of the RRULE in to KEY=>VAL 311 | foreach ($pieces as $piece) { 312 | if (false === strpos($piece, '=')) { 313 | continue; 314 | } 315 | 316 | list($key, $val) = explode('=', $piece); 317 | $parts[$key] = $val; 318 | } 319 | 320 | return $parts; 321 | } 322 | 323 | /** 324 | * Populate the object based on a RRULE array. 325 | * 326 | * @param array 327 | * 328 | * @return Rule 329 | * @throws InvalidRRule 330 | */ 331 | public function loadFromArray($parts) 332 | { 333 | // FREQ is required 334 | if (!isset($parts['FREQ'])) { 335 | throw new InvalidRRule('FREQ is required'); 336 | } else { 337 | if (!in_array($parts['FREQ'], array_keys(self::$freqs))) { 338 | throw new InvalidRRule('FREQ is invalid'); 339 | } 340 | 341 | $this->setFreq(self::$freqs[$parts['FREQ']]); 342 | } 343 | 344 | // DTSTART 345 | if (isset($parts['DTSTART'])) { 346 | $this->isStartDateFromDtstart = true; 347 | $date = new \DateTime($parts['DTSTART']); 348 | $date = $date->setTimezone(new \DateTimeZone($this->getTimezone())); 349 | $this->setStartDate($date); 350 | } 351 | 352 | // DTEND 353 | if (isset($parts['DTEND'])) { 354 | $date = new \DateTime($parts['DTEND']); 355 | $date = $date->setTimezone(new \DateTimeZone($this->getTimezone())); 356 | $this->setEndDate($date); 357 | } 358 | 359 | // UNTIL or COUNT 360 | if (isset($parts['UNTIL']) && isset($parts['COUNT'])) { 361 | throw new InvalidRRule('UNTIL and COUNT must not exist together in the same RRULE'); 362 | } elseif (isset($parts['UNTIL'])) { 363 | $date = new \DateTime($parts['UNTIL']); 364 | $date = $date->setTimezone(new \DateTimeZone($this->getTimezone())); 365 | $this->setUntil($date); 366 | } elseif (isset($parts['COUNT'])) { 367 | $this->setCount($parts['COUNT']); 368 | } 369 | 370 | // INTERVAL 371 | if (isset($parts['INTERVAL'])) { 372 | $this->setInterval($parts['INTERVAL']); 373 | } 374 | 375 | // BYSECOND 376 | if (isset($parts['BYSECOND'])) { 377 | $this->setBySecond(explode(',', $parts['BYSECOND'])); 378 | } 379 | 380 | // BYMINUTE 381 | if (isset($parts['BYMINUTE'])) { 382 | $this->setByMinute(explode(',', $parts['BYMINUTE'])); 383 | } 384 | 385 | // BYHOUR 386 | if (isset($parts['BYHOUR'])) { 387 | $this->setByHour(explode(',', $parts['BYHOUR'])); 388 | } 389 | 390 | // BYDAY 391 | if (isset($parts['BYDAY'])) { 392 | $this->setByDay(explode(',', $parts['BYDAY'])); 393 | } 394 | 395 | // BYMONTHDAY 396 | if (isset($parts['BYMONTHDAY'])) { 397 | $this->setByMonthDay(explode(',', $parts['BYMONTHDAY'])); 398 | } 399 | 400 | // BYYEARDAY 401 | if (isset($parts['BYYEARDAY'])) { 402 | $this->setByYearDay(explode(',', $parts['BYYEARDAY'])); 403 | } 404 | 405 | // BYWEEKNO 406 | if (isset($parts['BYWEEKNO'])) { 407 | $this->setByWeekNumber(explode(',', $parts['BYWEEKNO'])); 408 | } 409 | 410 | // BYMONTH 411 | if (isset($parts['BYMONTH'])) { 412 | $this->setByMonth(explode(',', $parts['BYMONTH'])); 413 | } 414 | 415 | // BYSETPOS 416 | if (isset($parts['BYSETPOS'])) { 417 | $this->setBySetPosition(explode(',', $parts['BYSETPOS'])); 418 | } 419 | 420 | // WKST 421 | if (isset($parts['WKST'])) { 422 | $this->setWeekStart($parts['WKST']); 423 | } 424 | 425 | // RDATE 426 | if (isset($parts['RDATE'])) { 427 | $this->setRDates(explode(',', $parts['RDATE'])); 428 | } 429 | 430 | // EXDATE 431 | if (isset($parts['EXDATE'])) { 432 | $this->setExDates(explode(',', $parts['EXDATE'])); 433 | } 434 | 435 | return $this; 436 | } 437 | 438 | /** 439 | * Get the RRULE as a string 440 | * 441 | * @param string $timezoneType 442 | * 443 | * @return string 444 | */ 445 | public function getString($timezoneType=self::TZ_FLOAT) 446 | { 447 | $format = 'Ymd\THis'; 448 | 449 | $parts = array(); 450 | 451 | // FREQ 452 | $parts[] = 'FREQ='.$this->getFreqAsText(); 453 | 454 | // UNTIL or COUNT 455 | $until = $this->getUntil(); 456 | $count = $this->getCount(); 457 | if (!empty($until)) { 458 | if ($timezoneType === self::TZ_FIXED) { 459 | $u = clone $until; 460 | $u = $u->setTimezone(new \DateTimeZone('UTC')); 461 | $parts[] = 'UNTIL='.$u->format($format.'\Z'); 462 | } else { 463 | $parts[] = 'UNTIL='.$until->format($format); 464 | } 465 | } elseif (!empty($count)) { 466 | $parts[] = 'COUNT='.$count; 467 | } 468 | 469 | // DTSTART 470 | if ($this->isStartDateFromDtstart) { 471 | if ($timezoneType === self::TZ_FIXED) { 472 | $d = $this->getStartDate(); 473 | $tzid = $d->getTimezone()->getName(); 474 | $date = $d->format($format); 475 | $parts[] = "DTSTART;TZID=$tzid:$date"; 476 | } else { 477 | $parts[] = 'DTSTART='.$this->getStartDate()->format($format); 478 | } 479 | } 480 | 481 | // DTEND 482 | if ($this->endDate instanceof \DateTime) { 483 | if ($timezoneType === self::TZ_FIXED) { 484 | $d = $this->getEndDate(); 485 | $tzid = $d->getTimezone()->getName(); 486 | $date = $d->format($format); 487 | 488 | $parts[] = "DTEND;TZID=$tzid:$date"; 489 | } else { 490 | $parts[] = 'DTEND='.$this->getEndDate()->format($format); 491 | } 492 | } 493 | 494 | // INTERVAL 495 | $interval = $this->getInterval(); 496 | if ($this->isExplicitInterval && !empty($interval)) { 497 | $parts[] = 'INTERVAL='.$interval; 498 | } 499 | 500 | // BYSECOND 501 | $bySecond = $this->getBySecond(); 502 | if (!empty($bySecond)) { 503 | $parts[] = 'BYSECOND='.implode(',', $bySecond); 504 | } 505 | 506 | // BYMINUTE 507 | $byMinute = $this->getByMinute(); 508 | if (!empty($byMinute)) { 509 | $parts[] = 'BYMINUTE='.implode(',', $byMinute); 510 | } 511 | 512 | // BYHOUR 513 | $byHour = $this->getByHour(); 514 | if (!empty($byHour)) { 515 | $parts[] = 'BYHOUR='.implode(',', $byHour); 516 | } 517 | 518 | // BYDAY 519 | $byDay = $this->getByDay(); 520 | if (!empty($byDay)) { 521 | $parts[] = 'BYDAY='.implode(',', $byDay); 522 | } 523 | 524 | // BYMONTHDAY 525 | $byMonthDay = $this->getByMonthDay(); 526 | if (!empty($byMonthDay)) { 527 | $parts[] = 'BYMONTHDAY='.implode(',', $byMonthDay); 528 | } 529 | 530 | // BYYEARDAY 531 | $byYearDay = $this->getByYearDay(); 532 | if (!empty($byYearDay)) { 533 | $parts[] = 'BYYEARDAY='.implode(',', $byYearDay); 534 | } 535 | 536 | // BYWEEKNO 537 | $byWeekNumber = $this->getByWeekNumber(); 538 | if (!empty($byWeekNumber)) { 539 | $parts[] = 'BYWEEKNO='.implode(',', $byWeekNumber); 540 | } 541 | 542 | // BYMONTH 543 | $byMonth = $this->getByMonth(); 544 | if (!empty($byMonth)) { 545 | $parts[] = 'BYMONTH='.implode(',', $byMonth); 546 | } 547 | 548 | // BYSETPOS 549 | $bySetPosition = $this->getBySetPosition(); 550 | if (!empty($bySetPosition)) { 551 | $parts[] = 'BYSETPOS='.implode(',', $bySetPosition); 552 | } 553 | 554 | // WKST 555 | $weekStart = $this->getWeekStart(); 556 | if ($this->weekStartDefined && !empty($weekStart)) { 557 | $parts[] = 'WKST='.$weekStart; 558 | } 559 | 560 | // RDATE 561 | $rDates = $this->getRDates(); 562 | if (!empty($rDates)) { 563 | foreach ($rDates as $key => $inclusion) { 564 | $format = 'Ymd'; 565 | if ($inclusion->hasTime) { 566 | $format .= '\THis'; 567 | if ($inclusion->isUtcExplicit) { 568 | $format .= '\Z'; 569 | } 570 | } 571 | $rDates[$key] = $inclusion->date->format($format); 572 | } 573 | $parts[] = 'RDATE='.implode(',', $rDates); 574 | } 575 | 576 | // EXDATE 577 | $exDates = $this->getExDates(); 578 | if (!empty($exDates)) { 579 | foreach ($exDates as $key => $exclusion) { 580 | $format = 'Ymd'; 581 | if ($exclusion->hasTime) { 582 | $format .= '\THis'; 583 | if ($exclusion->isUtcExplicit) { 584 | $format .= '\Z'; 585 | } 586 | } 587 | $exDates[$key] = $exclusion->date->format($format); 588 | } 589 | $parts[] = 'EXDATE='.implode(',', $exDates); 590 | } 591 | 592 | return implode(';', $parts); 593 | } 594 | 595 | /** 596 | * @param string $timezone 597 | * 598 | * @see http://www.php.net/manual/en/timezones.php 599 | * @return $this 600 | */ 601 | public function setTimezone($timezone) 602 | { 603 | $this->timezone = $timezone; 604 | 605 | return $this; 606 | } 607 | 608 | /** 609 | * Get timezone to use for \DateTimeInterface objects that are UTC. 610 | * 611 | * @return null|string 612 | */ 613 | public function getTimezone() 614 | { 615 | return $this->timezone; 616 | } 617 | 618 | /** 619 | * This date specifies the first instance in the recurrence set. 620 | * 621 | * @param \DateTimeInterface|null $startDate Date of the first instance in the recurrence 622 | * @param bool|null $includeInString If true, include as DTSTART when calling getString() 623 | * 624 | * @return $this 625 | */ 626 | public function setStartDate($startDate, $includeInString = null) 627 | { 628 | $this->startDate = $startDate; 629 | 630 | if ($includeInString !== null) { 631 | $this->isStartDateFromDtstart = (bool) $includeInString; 632 | } 633 | 634 | return $this; 635 | } 636 | 637 | /** 638 | * @return \DateTimeInterface 639 | */ 640 | public function getStartDate() 641 | { 642 | return $this->startDate; 643 | } 644 | 645 | /** 646 | * This date specifies the last possible instance in the recurrence set. 647 | * 648 | * @param \DateTimeInterface|null $endDate Date of the last possible instance in the recurrence 649 | * 650 | * @return $this 651 | */ 652 | public function setEndDate($endDate) 653 | { 654 | $this->endDate = $endDate; 655 | 656 | return $this; 657 | } 658 | 659 | /** 660 | * @return \DateTimeInterface|null 661 | */ 662 | public function getEndDate() 663 | { 664 | return $this->endDate; 665 | } 666 | 667 | /** 668 | * Identifies the type of recurrence rule. 669 | * 670 | * May be one of: 671 | * - Frequency::SECONDLY to specify repeating events based on an 672 | * interval of a second or more. 673 | * - Frequency::MINUTELY to specify repeating events based on an 674 | * interval of a minute or more. 675 | * - Frequency::HOURLY to specify repeating events based on an 676 | * interval of an hour or more. 677 | * - Frequency::DAILY to specify repeating events based on an 678 | * interval of a day or more. 679 | * - Frequency::WEEKLY to specify repeating events based on an 680 | * interval of a week or more. 681 | * - Frequency::MONTHLY to specify repeating events based on an 682 | * interval of a month or more. 683 | * - Frequency::YEAR to specify repeating events based on an 684 | * interval of a year or more. 685 | * 686 | * @param string|int $freq Frequency of recurrence. 687 | * 688 | * @return $this 689 | * @throws Exception\InvalidArgument 690 | */ 691 | public function setFreq($freq) 692 | { 693 | if (is_string($freq)) { 694 | if (!array_key_exists($freq, self::$freqs)) { 695 | throw new InvalidArgument('Frequency must comply with RFC 2445.'); 696 | } else { 697 | $freq = self::$freqs[$freq]; 698 | } 699 | } 700 | 701 | if (is_int($freq) && ($freq < 0 || $freq > 6)) { 702 | throw new InvalidArgument('Frequency integer must be between 0 and 6 Use the class constants.'); 703 | } 704 | 705 | $this->freq = $freq; 706 | 707 | return $this; 708 | } 709 | 710 | /** 711 | * Get the type of recurrence rule (as integer). 712 | * 713 | * @return int 714 | */ 715 | public function getFreq() 716 | { 717 | return $this->freq; 718 | } 719 | 720 | /** 721 | * Get the type of recurrence rule (as text). 722 | * 723 | * @return string 724 | */ 725 | public function getFreqAsText() 726 | { 727 | return array_search($this->getFreq(), self::$freqs); 728 | } 729 | 730 | /** 731 | * The interval represents how often the recurrence rule repeats. 732 | * 733 | * The default value is "1", meaning every second for a SECONDLY rule, 734 | * or every minute for a MINUTELY rule, every hour for an HOURLY rule, 735 | * every day for a DAILY rule, every week for a WEEKLY rule, every month 736 | * for a MONTHLY rule and every year for a YEARLY rule. 737 | * 738 | * @param int $interval Positive integer that represents how often the 739 | * recurrence rule repeats. 740 | * 741 | * @return $this 742 | * @throws Exception\InvalidArgument 743 | */ 744 | public function setInterval($interval) 745 | { 746 | $interval = (int) $interval; 747 | 748 | if ($interval < 1) { 749 | throw new InvalidArgument('Interval must be a positive integer'); 750 | } 751 | 752 | $this->interval = $interval; 753 | $this->isExplicitInterval = true; 754 | 755 | return $this; 756 | } 757 | 758 | /** 759 | * Get the interval that represents how often the recurrence rule repeats. 760 | * 761 | * @return int 762 | */ 763 | public function getInterval() 764 | { 765 | return $this->interval; 766 | } 767 | 768 | /** 769 | * Define a \DateTimeInterface value which bounds the recurrence rule in an 770 | * inclusive manner. If the value specified is synchronized with the 771 | * specified recurrence, this DateTime becomes the last instance of the 772 | * recurrence. If not present, and a COUNT is also not present, the RRULE 773 | * is considered to repeat forever. 774 | * 775 | * Either UNTIL or COUNT may be specified, but UNTIL and COUNT MUST NOT 776 | * both be specified. 777 | * 778 | * @param \DateTimeInterface $until The upper bound of the recurrence. 779 | * 780 | * @return $this 781 | */ 782 | public function setUntil(\DateTimeInterface $until) 783 | { 784 | $this->until = $until; 785 | $this->count = null; 786 | 787 | return $this; 788 | } 789 | 790 | /** 791 | * Get the \DateTimeInterface that the recurrence lasts until. 792 | * 793 | * @return \DateTimeInterface|null 794 | */ 795 | public function getUntil() 796 | { 797 | $date = $this->until; 798 | 799 | if ($date instanceof \DateTime 800 | && $date->getTimezone()->getName() == 'UTC' 801 | && $this->getTimezone() != 'UTC' 802 | ) { 803 | $timestamp = $date->getTimestamp(); 804 | $date = $date->setTimezone(new \DateTimeZone($this->getTimezone())); 805 | $date = $date->setTimestamp($timestamp); 806 | } 807 | 808 | return $date; 809 | } 810 | 811 | /** 812 | * The count defines the number of occurrences at which to range-bound the 813 | * recurrence. The DTSTART counts as the first occurrence. 814 | * 815 | * Either COUNT or UNTIL may be specified, but COUNT and UNTIL MUST NOT 816 | * both be specified. 817 | * 818 | * @param int $count Number of occurrences 819 | * 820 | * @return $this 821 | */ 822 | public function setCount($count) 823 | { 824 | $this->count = (int) $count; 825 | $this->until = null; 826 | 827 | return $this; 828 | } 829 | 830 | /** 831 | * Get the number of occurrences at which the recurrence is range-bound. 832 | * 833 | * @return int|null 834 | */ 835 | public function getCount() 836 | { 837 | return $this->count; 838 | } 839 | 840 | /** 841 | * This rule specifies an array of seconds within a minute. 842 | * 843 | * Valid values are 0 to 59. 844 | * 845 | * @param array $bySecond Array of seconds within a minute 846 | * 847 | * @return $this 848 | */ 849 | public function setBySecond(array $bySecond) 850 | { 851 | $this->bySecond = $bySecond; 852 | 853 | return $this; 854 | } 855 | 856 | /** 857 | * Get an array of seconds within a minute. 858 | * 859 | * @return array 860 | */ 861 | public function getBySecond() 862 | { 863 | return $this->bySecond; 864 | } 865 | 866 | /** 867 | * This rule specifies an array of minutes within an hour. 868 | * 869 | * Valid values are 0 to 59. 870 | * 871 | * @param array $byMinute Array of minutes within an hour 872 | * 873 | * @return $this 874 | */ 875 | public function setByMinute(array $byMinute) 876 | { 877 | $this->byMinute = $byMinute; 878 | 879 | return $this; 880 | } 881 | 882 | /** 883 | * Get an array of minutes within an hour. 884 | * 885 | * @return array 886 | */ 887 | public function getByMinute() 888 | { 889 | return $this->byMinute; 890 | } 891 | 892 | /** 893 | * This rule specifies an array of hours of the day. 894 | * 895 | * Valid values are 0 to 23. 896 | * 897 | * @param array $byHour Array of hours of the day 898 | * 899 | * @return $this 900 | */ 901 | public function setByHour(array $byHour) 902 | { 903 | $this->byHour = $byHour; 904 | 905 | return $this; 906 | } 907 | 908 | /** 909 | * Get an array of hours of the day. 910 | * 911 | * @return array 912 | */ 913 | public function getByHour() 914 | { 915 | return $this->byHour; 916 | } 917 | 918 | /** 919 | * This rule specifies an array of days of the week; 920 | * 921 | * MO indicates Monday; TU indicates Tuesday; WE indicates Wednesday; 922 | * TH indicates Thursday; FR indicates Friday; SA indicates Saturday; 923 | * SU indicates Sunday. 924 | * 925 | * Each BYDAY value can also be preceded by a positive (+n) or negative 926 | * (-n) integer. If present, this indicates the nth occurrence of the 927 | * specific day within the MONTHLY or YEARLY RRULE. For example, within 928 | * a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday 929 | * within the month, whereas -1MO represents the last Monday of the 930 | * month. If an integer modifier is not present, it means all days of 931 | * this type within the specified frequency. For example, within a 932 | * MONTHLY rule, MO represents all Mondays within the month. 933 | * 934 | * ------------------------------------------- 935 | * DO NOT MIX DAYS AND DAYS WITH MODIFIERS. 936 | * This is not supported. 937 | * ------------------------------------------- 938 | * 939 | * @param array $byDay Array of days of the week 940 | * 941 | * @return $this 942 | * @throws InvalidRRule 943 | */ 944 | public function setByDay(array $byDay) 945 | { 946 | if ($this->getFreq() > static::$freqs['MONTHLY'] && preg_match('/\d/', implode(',', $byDay))) { 947 | throw new InvalidRRule('BYDAY only supports MONTHLY and YEARLY frequencies'); 948 | } 949 | if (count($byDay) === 0 || $byDay === array('')) { 950 | throw new InvalidRRule('BYDAY must be set to at least one day'); 951 | } 952 | 953 | $this->byDay = $byDay; 954 | 955 | return $this; 956 | } 957 | 958 | /** 959 | * Get an array of days of the week (SU, MO, TU, ..) 960 | * 961 | * @return array 962 | */ 963 | public function getByDay() 964 | { 965 | return $this->byDay; 966 | } 967 | 968 | /** 969 | * Get an array of Weekdays 970 | * 971 | * @return array of Weekdays 972 | * @throws InvalidWeekday 973 | */ 974 | public function getByDayTransformedToWeekdays() 975 | { 976 | $byDay = $this->getByDay(); 977 | 978 | if (null === $byDay || !count($byDay)) { 979 | return array(); 980 | } 981 | 982 | foreach ($byDay as $idx => $day) { 983 | if (strlen($day) === 2) { 984 | $byDay[$idx] = new Weekday($day, null); 985 | } else { 986 | preg_match('/^([+-]?[0-9]+)([A-Z]{2})$/', $day, $dayParts); 987 | $byDay[$idx] = new Weekday($dayParts[2], $dayParts[1]); 988 | } 989 | } 990 | 991 | return $byDay; 992 | } 993 | 994 | /** 995 | * This rule specifies an array of days of the month. 996 | * Valid values are 1 to 31 or -31 to -1. 997 | * 998 | * For example, -10 represents the tenth to the last day of the month. 999 | * 1000 | * @param array $byMonthDay Array of days of the month from -31 to 31 1001 | * 1002 | * @return $this 1003 | */ 1004 | public function setByMonthDay(array $byMonthDay) 1005 | { 1006 | $this->byMonthDay = $byMonthDay; 1007 | 1008 | return $this; 1009 | } 1010 | 1011 | /** 1012 | * Get an array of days of the month. 1013 | * 1014 | * @return array 1015 | */ 1016 | public function getByMonthDay() 1017 | { 1018 | return $this->byMonthDay; 1019 | } 1020 | 1021 | /** 1022 | * This rule specifies an array of days of the year. 1023 | * Valid values are 1 to 366 or -366 to -1. 1024 | * 1025 | * For example, -1 represents the last day of the year (December 31st) and 1026 | * -306 represents the 306th to the last day of the year (March 1st). 1027 | * 1028 | * @param array $byYearDay Array of days of the year from -1 to 306 1029 | * 1030 | * @return $this 1031 | */ 1032 | public function setByYearDay(array $byYearDay) 1033 | { 1034 | $this->byYearDay = $byYearDay; 1035 | 1036 | return $this; 1037 | } 1038 | 1039 | /** 1040 | * Get an array of days of the year. 1041 | * 1042 | * @return array 1043 | */ 1044 | public function getByYearDay() 1045 | { 1046 | return $this->byYearDay; 1047 | } 1048 | 1049 | /** 1050 | * This rule specifies an array of ordinals specifying weeks of the year. 1051 | * Valid values are 1 to 53 or -53 to -1. 1052 | * 1053 | * This corresponds to weeks according to week numbering as defined in 1054 | * [ISO 8601]. A week is defined as a seven day period, starting on the day 1055 | * of the week defined to be the week start (see setWeekStart). Week number 1056 | * one of the calendar year is the first week which contains at least four 1057 | * days in that calendar year. This rule is only valid for YEARLY rules. 1058 | * 1059 | * For example, 3 represents the third week of the year. 1060 | * 1061 | * Note: Assuming a Monday week start, week 53 can only occur when 1062 | * Thursday is January 1 or if it is a leap year and Wednesday is January 1. 1063 | * 1064 | * @param array $byWeekNumber Array of ordinals specifying weeks of the year. 1065 | * 1066 | * @return $this 1067 | */ 1068 | public function setByWeekNumber(array $byWeekNumber) 1069 | { 1070 | $this->byWeekNumber = $byWeekNumber; 1071 | 1072 | return $this; 1073 | } 1074 | 1075 | /** 1076 | * Get an array of ordinals specifying weeks of the year. 1077 | * 1078 | * @return array 1079 | */ 1080 | public function getByWeekNumber() 1081 | { 1082 | return $this->byWeekNumber; 1083 | } 1084 | 1085 | /** 1086 | * This rule specifies an array of months of the year. 1087 | * 1088 | * Valid values are 1 to 12. 1089 | * 1090 | * @param array $byMonth Array of months of the year from 1 to 12 1091 | * 1092 | * @return $this 1093 | */ 1094 | public function setByMonth(array $byMonth) 1095 | { 1096 | $this->byMonth = $byMonth; 1097 | 1098 | return $this; 1099 | } 1100 | 1101 | /** 1102 | * Get an array of months of the year. 1103 | * 1104 | * @return array 1105 | */ 1106 | public function getByMonth() 1107 | { 1108 | return $this->byMonth; 1109 | } 1110 | 1111 | public function hasByMonth() 1112 | { 1113 | $val = $this->getByMonth(); 1114 | 1115 | return ! empty($val); 1116 | } 1117 | 1118 | /** 1119 | * This rule specifies the day on which the workweek starts. 1120 | * 1121 | * Valid values are MO, TU, WE, TH, FR, SA and SU. 1122 | * 1123 | * This is significant when a WEEKLY RRULE has an interval greater than 1, 1124 | * and a BYDAY rule part is specified. 1125 | * 1126 | * This is also significant when in a YEARLY RRULE when a BYWEEKNO rule 1127 | * is specified. The default value is MO. 1128 | * 1129 | * @param string $weekStart The day on which the workweek starts. 1130 | * 1131 | * @return $this 1132 | * @throws Exception\InvalidArgument 1133 | */ 1134 | public function setWeekStart($weekStart) 1135 | { 1136 | $weekStart = strtoupper($weekStart); 1137 | 1138 | if (!in_array($weekStart, array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'))) { 1139 | throw new InvalidArgument('Week Start must be one of MO, TU, WE, TH, FR, SA, SU'); 1140 | } 1141 | 1142 | $this->weekStart = $weekStart; 1143 | $this->weekStartDefined = true; 1144 | 1145 | return $this; 1146 | } 1147 | 1148 | /** 1149 | * Get the day on which the workweek starts. 1150 | * 1151 | * @return string 1152 | */ 1153 | public function getWeekStart() 1154 | { 1155 | return $this->weekStart; 1156 | } 1157 | 1158 | /** 1159 | * Get the day on which the workweek starts, as an integer from 0-6, 1160 | * 0 being Monday and 6 being Sunday. 1161 | * 1162 | * @return int 1163 | */ 1164 | public function getWeekStartAsNum() 1165 | { 1166 | $weekStart = $this->getWeekStart(); 1167 | 1168 | return $this->days[$weekStart]; 1169 | } 1170 | 1171 | /** 1172 | * This rule specifies an array of values which corresponds to the nth 1173 | * occurrence within the set of events specified by the rule. Valid values 1174 | * are 1 to 366 or -366 to -1. It MUST only be used in conjunction with 1175 | * another BYxxx rule part. 1176 | * 1177 | * For example "the last work day of the month" could be represented as: 1178 | * RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 1179 | * 1180 | * Each BYSETPOS value can include a positive or negative integer. 1181 | * If present, this indicates the nth occurrence of the specific occurrence 1182 | * within the set of events specified by the rule. 1183 | * 1184 | * @param array $bySetPosition Array of values which corresponds to the nth 1185 | * occurrence within the set of events specified by the rule. 1186 | * 1187 | * @return $this 1188 | */ 1189 | public function setBySetPosition($bySetPosition) 1190 | { 1191 | $this->bySetPosition = $bySetPosition; 1192 | 1193 | return $this; 1194 | } 1195 | 1196 | /** 1197 | * Get the array of values which corresponds to the nth occurrence within 1198 | * the set of events specified by the rule. 1199 | * 1200 | * @return array 1201 | */ 1202 | public function getBySetPosition() 1203 | { 1204 | return $this->bySetPosition; 1205 | } 1206 | 1207 | /** 1208 | * This rule specifies an array of dates that will be 1209 | * included in a recurrence set. 1210 | * 1211 | * @param string[]|DateInclusion[] $rDates Array of dates that will be 1212 | * included in the recurrence set. 1213 | * 1214 | * @return $this 1215 | */ 1216 | public function setRDates(array $rDates) 1217 | { 1218 | $timezone = new \DateTimeZone($this->getTimezone()); 1219 | 1220 | foreach ($rDates as $key => $val) { 1221 | if ($val instanceof DateInclusion) { 1222 | $val->date = $this->convertZtoUtc($val->date); 1223 | } else { 1224 | $date = new \DateTime($val, $timezone); 1225 | $rDates[$key] = new DateInclusion( 1226 | $this->convertZtoUtc($date), 1227 | strpos($val, 'T') !== false, 1228 | strpos($val, 'Z') !== false 1229 | ); 1230 | } 1231 | } 1232 | 1233 | $this->rDates = $rDates; 1234 | 1235 | return $this; 1236 | } 1237 | 1238 | /** 1239 | * Get the array of dates that will be included in a recurrence set. 1240 | * 1241 | * @return DateInclusion[] 1242 | */ 1243 | public function getRDates() 1244 | { 1245 | return $this->rDates; 1246 | } 1247 | 1248 | /** 1249 | * This rule specifies an array of exception dates that will not be 1250 | * included in a recurrence set. 1251 | * 1252 | * @param string[]|DateExclusion[] $exDates Array of dates that will not be 1253 | * included in the recurrence set. 1254 | * 1255 | * @return $this 1256 | */ 1257 | public function setExDates(array $exDates) 1258 | { 1259 | $timezone = new \DateTimeZone($this->getTimezone()); 1260 | 1261 | foreach ($exDates as $key => $val) { 1262 | if ($val instanceof DateExclusion) { 1263 | $val->date = $this->convertZtoUtc($val->date); 1264 | } else { 1265 | $date = new \DateTime($val, $timezone); 1266 | $exDates[$key] = new DateExclusion( 1267 | $this->convertZtoUtc($date), 1268 | strpos($val, 'T') !== false, 1269 | strpos($val, 'Z') !== false 1270 | ); 1271 | } 1272 | } 1273 | 1274 | $this->exDates = $exDates; 1275 | 1276 | return $this; 1277 | } 1278 | 1279 | /** 1280 | * DateTime::setTimezone fails if the timezone does not have an ID. 1281 | * "Z" is the same as "UTC", but "Z" does not have an ID. 1282 | * 1283 | * This is necessary for exclusion dates to be handled properly. 1284 | * 1285 | * @param \DateTimeInterface $date 1286 | * 1287 | * @return \DateTimeInterface 1288 | */ 1289 | private function convertZtoUtc(\DateTimeInterface $date) 1290 | { 1291 | if ($date->getTimezone()->getName() !== 'Z') { 1292 | return $date; 1293 | } 1294 | 1295 | return $date->setTimezone(new \DateTimeZone('UTC')); 1296 | } 1297 | 1298 | /** 1299 | * Get the array of dates that will not be included in a recurrence set. 1300 | * 1301 | * @return DateExclusion[] 1302 | */ 1303 | public function getExDates() 1304 | { 1305 | return $this->exDates; 1306 | } 1307 | 1308 | /** 1309 | * @return bool 1310 | */ 1311 | public function repeatsIndefinitely() 1312 | { 1313 | return !$this->getCount() && !$this->getUntil() && !$this->getEndDate(); 1314 | } 1315 | } 1316 | -------------------------------------------------------------------------------- /src/Recurr/Time.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Time 23 | { 24 | /** @var int */ 25 | public $hour; 26 | 27 | /** @var int */ 28 | public $minute; 29 | 30 | /** @var int */ 31 | public $second; 32 | 33 | public function __construct($hour, $minute, $second) 34 | { 35 | $this->hour = $hour; 36 | $this->minute = $minute; 37 | $this->second = $second; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/ArrayTransformer.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | class ArrayTransformer 38 | { 39 | /** @var ArrayTransformerConfig */ 40 | protected $config; 41 | 42 | /** 43 | * Some versions of PHP are affected by a bug where 44 | * \DateTimeInterface::createFromFormat('z Y', ...) does not account for leap years. 45 | * 46 | * @var bool 47 | */ 48 | protected $leapBug = false; 49 | 50 | /** 51 | * Construct a new ArrayTransformer 52 | * 53 | * @param ArrayTransformerConfig $config 54 | */ 55 | public function __construct(?ArrayTransformerConfig $config = null) 56 | { 57 | if (!$config instanceof ArrayTransformerConfig) { 58 | $config = new ArrayTransformerConfig(); 59 | } 60 | 61 | $this->config = $config; 62 | 63 | $this->leapBug = DateUtil::hasLeapYearBug(); 64 | } 65 | 66 | /** 67 | * @param ArrayTransformerConfig $config 68 | */ 69 | public function setConfig($config) 70 | { 71 | $this->config = $config; 72 | } 73 | 74 | /** 75 | * Transform a Rule in to an array of \DateTimeInterface objects 76 | * 77 | * @param Rule $rule the Rule object 78 | * @param ConstraintInterface|null $constraint Potential recurrences must pass the constraint, else 79 | * they will not be included in the returned collection. 80 | * @param bool $countConstraintFailures Whether recurrences that fail the constraint's test 81 | * should count towards a rule's COUNT limit. 82 | * 83 | * @return RecurrenceCollection|Recurrence[] 84 | * @throws InvalidWeekday 85 | */ 86 | public function transform(Rule $rule, ?ConstraintInterface $constraint = null, $countConstraintFailures = true) 87 | { 88 | $start = $rule->getStartDate(); 89 | $end = $rule->getEndDate(); 90 | $until = $rule->getUntil(); 91 | 92 | if (null === $start) { 93 | $start = new \DateTime( 94 | 'now', $until instanceof \DateTimeInterface ? $until->getTimezone() : null 95 | ); 96 | } 97 | 98 | if (null === $end) { 99 | $end = $start; 100 | } 101 | 102 | $durationInterval = $start->diff($end); 103 | 104 | $startDay = $start->format('j'); 105 | $startMonthLength = $start->format('t'); 106 | $fixLastDayOfMonth = false; 107 | 108 | $dt = clone $start; 109 | 110 | $maxCount = $rule->getCount(); 111 | $vLimit = $this->config->getVirtualLimit(); 112 | 113 | $freq = $rule->getFreq(); 114 | $weekStart = $rule->getWeekStartAsNum(); 115 | $bySecond = $rule->getBySecond(); 116 | $byMinute = $rule->getByMinute(); 117 | $byHour = $rule->getByHour(); 118 | $byMonth = $rule->getByMonth(); 119 | $byWeekNum = $rule->getByWeekNumber(); 120 | $byYearDay = $rule->getByYearDay(); 121 | $byMonthDay = $rule->getByMonthDay(); 122 | $byMonthDayNeg = array(); 123 | $byWeekDay = $rule->getByDayTransformedToWeekdays(); 124 | $byWeekDayRel = array(); 125 | $bySetPos = $rule->getBySetPosition(); 126 | 127 | $implicitByMonthDay = false; 128 | if (!(!empty($byWeekNum) || !empty($byYearDay) || !empty($byMonthDay) || !empty($byWeekDay))) { 129 | switch ($freq) { 130 | case Frequency::YEARLY: 131 | if (empty($byMonth)) { 132 | $byMonth = array($start->format('n')); 133 | } 134 | 135 | if ($startDay > 28) { 136 | $fixLastDayOfMonth = true; 137 | } 138 | 139 | $implicitByMonthDay = true; 140 | $byMonthDay = array($startDay); 141 | break; 142 | case Frequency::MONTHLY: 143 | if ($startDay > 28) { 144 | $fixLastDayOfMonth = true; 145 | } 146 | 147 | $implicitByMonthDay = true; 148 | $byMonthDay = array($startDay); 149 | break; 150 | case Frequency::WEEKLY: 151 | $byWeekDay = array( 152 | new Weekday( 153 | DateUtil::getDayOfWeek($start), null 154 | ) 155 | ); 156 | break; 157 | } 158 | } 159 | 160 | if (!$this->config->isLastDayOfMonthFixEnabled()) { 161 | $fixLastDayOfMonth = false; 162 | } 163 | 164 | if (is_array($byMonthDay) && count($byMonthDay)) { 165 | foreach ($byMonthDay as $idx => $day) { 166 | if ($day < 0) { 167 | unset($byMonthDay[$idx]); 168 | $byMonthDayNeg[] = $day; 169 | } 170 | } 171 | } 172 | 173 | if (!empty($byWeekDay)) { 174 | foreach ($byWeekDay as $idx => $day) { 175 | /** @var $day Weekday */ 176 | 177 | if (!empty($day->num)) { 178 | $byWeekDayRel[] = $day; 179 | unset($byWeekDay[$idx]); 180 | } else { 181 | $byWeekDay[$idx] = $day->weekday; 182 | } 183 | } 184 | } 185 | 186 | if (empty($byYearDay)) { 187 | $byYearDay = null; 188 | } 189 | 190 | if (empty($byMonthDay)) { 191 | $byMonthDay = null; 192 | } 193 | 194 | if (empty($byMonthDayNeg)) { 195 | $byMonthDayNeg = null; 196 | } 197 | 198 | if (empty($byWeekDay)) { 199 | $byWeekDay = null; 200 | } 201 | 202 | if (!count($byWeekDayRel)) { 203 | $byWeekDayRel = null; 204 | } 205 | 206 | $year = $dt->format('Y'); 207 | $month = $dt->format('n'); 208 | $hour = $dt->format('G'); 209 | $minute = $dt->format('i'); 210 | $second = $dt->format('s'); 211 | 212 | $dates = array(); 213 | $total = 1; 214 | $count = $maxCount; 215 | $continue = true; 216 | $iterations = 0; 217 | while ($continue) { 218 | $dtInfo = DateUtil::getDateInfo($dt); 219 | 220 | $tmp = DateUtil::getDaySet($rule, $dt, $dtInfo, $start); 221 | $daySet = $tmp->set; 222 | $daySetStart = $tmp->start; 223 | $daySetEnd = $tmp->end; 224 | $wNoMask = array(); 225 | $wDayMaskRel = array(); 226 | $timeSet = DateUtil::getTimeSet($rule, $dt); 227 | 228 | if ($freq >= Frequency::HOURLY) { 229 | if ( 230 | ($freq >= Frequency::HOURLY && !empty($byHour) && !in_array($hour, $byHour)) 231 | || ($freq >= Frequency::MINUTELY && !empty($byMinute) && !in_array($minute, $byMinute)) 232 | || ($freq >= Frequency::SECONDLY && !empty($bySecond) && !in_array($second, $bySecond)) 233 | ) { 234 | $timeSet = array(); 235 | } else { 236 | switch ($freq) { 237 | case Frequency::HOURLY: 238 | $timeSet = DateUtil::getTimeSetOfHour($rule, $dt); 239 | break; 240 | case Frequency::MINUTELY: 241 | $timeSet = DateUtil::getTimeSetOfMinute($rule, $dt); 242 | break; 243 | case Frequency::SECONDLY: 244 | $timeSet = DateUtil::getTimeSetOfSecond($dt); 245 | break; 246 | } 247 | } 248 | } 249 | 250 | // Handle byWeekNum 251 | if (!empty($byWeekNum)) { 252 | $no1WeekStart = $firstWeekStart = DateUtil::pymod(7 - $dtInfo->dayOfWeekYearDay1 + $weekStart, 7); 253 | 254 | if ($no1WeekStart >= 4) { 255 | $no1WeekStart = 0; 256 | 257 | $wYearLength = $dtInfo->yearLength + DateUtil::pymod( 258 | $dtInfo->dayOfWeekYearDay1 - $weekStart, 259 | 7 260 | ); 261 | } else { 262 | $wYearLength = $dtInfo->yearLength - $no1WeekStart; 263 | } 264 | 265 | $div = floor($wYearLength / 7); 266 | $mod = DateUtil::pymod($wYearLength, 7); 267 | $numWeeks = floor($div + ($mod / 4)); 268 | 269 | foreach ($byWeekNum as $weekNum) { 270 | if ($weekNum < 0) { 271 | $weekNum += $numWeeks + 1; 272 | } 273 | 274 | if (!(0 < $weekNum && $weekNum <= $numWeeks)) { 275 | continue; 276 | } 277 | 278 | if ($weekNum > 1) { 279 | $offset = $no1WeekStart + ($weekNum - 1) * 7; 280 | if ($no1WeekStart != $firstWeekStart) { 281 | $offset -= 7 - $firstWeekStart; 282 | } 283 | } else { 284 | $offset = $no1WeekStart; 285 | } 286 | 287 | for ($i = 0; $i < 7; $i++) { 288 | $wNoMask[] = $offset; 289 | $offset++; 290 | if ($dtInfo->wDayMask[$offset] == $weekStart) { 291 | break; 292 | } 293 | } 294 | } 295 | 296 | // Check week number 1 of next year as well 297 | if (in_array(1, $byWeekNum)) { 298 | $offset = $no1WeekStart + $numWeeks * 7; 299 | 300 | if ($no1WeekStart != $firstWeekStart) { 301 | $offset -= 7 - $firstWeekStart; 302 | } 303 | 304 | // If week starts in next year, we don't care about it. 305 | if ($offset < $dtInfo->yearLength) { 306 | for ($k = 0; $k < 7; $k++) { 307 | $wNoMask[] = $offset; 308 | $offset += 1; 309 | if ($dtInfo->wDayMask[$offset] == $weekStart) { 310 | break; 311 | } 312 | } 313 | } 314 | } 315 | 316 | if ($no1WeekStart) { 317 | // Check last week number of last year as well. 318 | // If $no1WeekStart is 0, either the year started on week start, 319 | // or week number 1 got days from last year, so there are no 320 | // days from last year's last week number in this year. 321 | if (!in_array(-1, $byWeekNum)) { 322 | $dtTmp = new \DateTime(); 323 | $dtTmp = $dtTmp->setDate($year - 1, 1, 1); 324 | $lastYearWeekDay = DateUtil::getDayOfWeek($dtTmp); 325 | $lastYearNo1WeekStart = DateUtil::pymod(7 - $lastYearWeekDay + $weekStart, 7); 326 | $lastYearLength = DateUtil::getYearLength($dtTmp); 327 | if ($lastYearNo1WeekStart >= 4) { 328 | $lastYearNo1WeekStart = 0; 329 | $lastYearNumWeeks = floor( 330 | 52 + DateUtil::pymod( 331 | $lastYearLength + DateUtil::pymod( 332 | $lastYearWeekDay - $weekStart, 333 | 7 334 | ), 335 | 7 336 | ) / 4 337 | ); 338 | } else { 339 | $lastYearNumWeeks = floor( 340 | 52 + DateUtil::pymod( 341 | $dtInfo->yearLength - $no1WeekStart, 342 | 7 343 | ) / 4 344 | ); 345 | } 346 | } else { 347 | $lastYearNumWeeks = -1; 348 | } 349 | 350 | if (in_array($lastYearNumWeeks, $byWeekNum)) { 351 | for ($i = 0; $i < $no1WeekStart; $i++) { 352 | $wNoMask[] = $i; 353 | } 354 | } 355 | } 356 | } 357 | 358 | // Handle relative weekdays (e.g. 3rd Friday of month) 359 | if (!empty($byWeekDayRel)) { 360 | $ranges = array(); 361 | 362 | if (Frequency::YEARLY == $freq) { 363 | if (!empty($byMonth)) { 364 | foreach ($byMonth as $mo) { 365 | $ranges[] = array_slice($dtInfo->mRanges, $mo - 1, 2); 366 | } 367 | } else { 368 | $ranges[] = array(0, $dtInfo->yearLength); 369 | } 370 | } elseif (Frequency::MONTHLY == $freq) { 371 | $ranges[] = array_slice($dtInfo->mRanges, $month - 1, 2); 372 | } 373 | 374 | if (!empty($ranges)) { 375 | foreach ($ranges as $range) { 376 | $rangeStart = $range[0]; 377 | $rangeEnd = $range[1]; 378 | --$rangeEnd; 379 | 380 | reset($byWeekDayRel); 381 | foreach ($byWeekDayRel as $weekday) { 382 | /** @var Weekday $weekday */ 383 | 384 | if ($weekday->num < 0) { 385 | $i = $rangeEnd + ($weekday->num + 1) * 7; 386 | $i -= DateUtil::pymod( 387 | $dtInfo->wDayMask[$i] - $weekday->weekday, 388 | 7 389 | ); 390 | } else { 391 | $i = $rangeStart + ($weekday->num - 1) * 7; 392 | $i += DateUtil::pymod( 393 | 7 - $dtInfo->wDayMask[$i] + $weekday->weekday, 394 | 7 395 | ); 396 | } 397 | 398 | if ($rangeStart <= $i && $i <= $rangeEnd) { 399 | $wDayMaskRel[] = $i; 400 | } 401 | } 402 | } 403 | } 404 | } 405 | 406 | $numMatched = 0; 407 | foreach ($daySet as $i => $dayOfYear) { 408 | $dayOfMonth = $dtInfo->mDayMask[$dayOfYear]; 409 | 410 | $ifByMonth = $byMonth !== null && !in_array($dtInfo->mMask[$dayOfYear], $byMonth); 411 | 412 | $ifByWeekNum = $byWeekNum !== null && !in_array($i, $wNoMask); 413 | 414 | $ifByYearDay = $byYearDay !== null && ( 415 | ( 416 | $i < $dtInfo->yearLength && 417 | !in_array($i + 1, $byYearDay) && 418 | !in_array(-$dtInfo->yearLength + $i, $byYearDay) 419 | ) || 420 | ( 421 | $i >= $dtInfo->yearLength && 422 | !in_array($i + 1 - $dtInfo->yearLength, $byYearDay) && 423 | !in_array(-$dtInfo->nextYearLength + $i - $dtInfo->yearLength, $byYearDay) 424 | ) 425 | ); 426 | 427 | $ifByMonthDay = $byMonthDay !== null && !in_array($dtInfo->mDayMask[$dayOfYear], $byMonthDay); 428 | 429 | // Handle "last day of next month" problem. 430 | if ($fixLastDayOfMonth 431 | && $ifByMonthDay 432 | && $implicitByMonthDay 433 | && $startMonthLength > $dtInfo->monthLength 434 | && $dayOfMonth == $dtInfo->monthLength 435 | && $dayOfMonth < $startMonthLength 436 | && !$numMatched 437 | ) { 438 | $ifByMonthDay = false; 439 | } 440 | 441 | $ifByMonthDayNeg = $byMonthDayNeg !== null 442 | && !in_array($dtInfo->mDayMaskNeg[$dayOfYear], $byMonthDayNeg); 443 | 444 | $ifByDay = $byWeekDay !== null && count($byWeekDay) 445 | && !in_array($dtInfo->wDayMask[$dayOfYear], $byWeekDay); 446 | 447 | $ifWDayMaskRel = $byWeekDayRel !== null && !in_array($dayOfYear, $wDayMaskRel); 448 | 449 | if ($byMonthDay !== null && $byMonthDayNeg !== null) { 450 | if ($ifByMonthDay && $ifByMonthDayNeg) { 451 | unset($daySet[$i]); 452 | } 453 | } elseif ($ifByMonth || $ifByWeekNum || $ifByYearDay || $ifByMonthDay || $ifByMonthDayNeg || $ifByDay || $ifWDayMaskRel) { 454 | unset($daySet[$i]); 455 | } else { 456 | ++$numMatched; 457 | } 458 | } 459 | 460 | if (!empty($bySetPos) && !empty($daySet)) { 461 | $datesAdj = array(); 462 | $tmpDaySet = array_combine($daySet, $daySet); 463 | 464 | foreach ($bySetPos as $setPos) { 465 | if ($setPos < 0) { 466 | $dayPos = floor($setPos / count($timeSet)); 467 | $timePos = DateUtil::pymod($setPos, count($timeSet)); 468 | } else { 469 | $dayPos = floor(($setPos - 1) / count($timeSet)); 470 | $timePos = DateUtil::pymod(($setPos - 1), count($timeSet)); 471 | } 472 | 473 | $tmp = array(); 474 | for ($k = $daySetStart; $k <= $daySetEnd; $k++) { 475 | if (!array_key_exists($k, $tmpDaySet)) { 476 | continue; 477 | } 478 | 479 | $tmp[] = $tmpDaySet[$k]; 480 | } 481 | 482 | if ($dayPos < 0) { 483 | $nextInSet = array_slice($tmp, $dayPos, 1); 484 | if (count($nextInSet) === 0) { 485 | continue; 486 | } 487 | $nextInSet = $nextInSet[0]; 488 | } else { 489 | $nextInSet = isset($tmp[$dayPos]) ? $tmp[$dayPos] : null; 490 | } 491 | 492 | if (null !== $nextInSet) { 493 | /** @var Time $time */ 494 | $time = $timeSet[$timePos]; 495 | 496 | $dtTmp = DateUtil::getDateTimeByDayOfYear($nextInSet, $dt->format('Y'), $start->getTimezone()); 497 | 498 | $dtTmp = $dtTmp->setTime( 499 | $time->hour, 500 | $time->minute, 501 | $time->second 502 | ); 503 | 504 | $datesAdj[] = $dtTmp; 505 | } 506 | } 507 | 508 | foreach ($datesAdj as $dtTmp) { 509 | if (null !== $until && $dtTmp > $until) { 510 | $continue = false; 511 | break; 512 | } 513 | 514 | if ($dtTmp < $start) { 515 | continue; 516 | } 517 | 518 | if ($constraint instanceof ConstraintInterface && !$constraint->test($dtTmp)) { 519 | if (!$countConstraintFailures) { 520 | if ($constraint->stopsTransformer()) { 521 | $continue = false; 522 | break; 523 | } else { 524 | continue; 525 | } 526 | } 527 | } else { 528 | $dates[$total] = $dtTmp; 529 | } 530 | 531 | if (null !== $count) { 532 | --$count; 533 | if ($count <= 0) { 534 | $continue = false; 535 | break; 536 | } 537 | } 538 | 539 | ++$total; 540 | if ($total > $vLimit) { 541 | $continue = false; 542 | break; 543 | } 544 | } 545 | } else { 546 | foreach ($daySet as $dayOfYear) { 547 | $dtTmp = DateUtil::getDateTimeByDayOfYear($dayOfYear, $dt->format('Y'), $start->getTimezone()); 548 | 549 | foreach ($timeSet as $time) { 550 | /** @var Time $time */ 551 | $dtTmp = $dtTmp->setTime( 552 | $time->hour, 553 | $time->minute, 554 | $time->second 555 | ); 556 | 557 | if (null !== $until && $dtTmp > $until) { 558 | $continue = false; 559 | break; 560 | } 561 | 562 | if ($dtTmp < $start) { 563 | continue; 564 | } 565 | 566 | if ($constraint instanceof ConstraintInterface && !$constraint->test($dtTmp)) { 567 | if (!$countConstraintFailures) { 568 | if ($constraint->stopsTransformer()) { 569 | $continue = false; 570 | break; 571 | } else { 572 | continue; 573 | } 574 | } 575 | } else { 576 | $dates[$total] = clone $dtTmp; 577 | } 578 | 579 | if (null !== $count) { 580 | --$count; 581 | if ($count <= 0) { 582 | $continue = false; 583 | break; 584 | } 585 | } 586 | 587 | ++$total; 588 | if ($total > $vLimit) { 589 | $continue = false; 590 | break; 591 | } 592 | } 593 | 594 | if (!$continue) { 595 | break; 596 | } 597 | } 598 | 599 | if ($total > $vLimit) { 600 | $continue = false; 601 | break; 602 | } 603 | } 604 | 605 | switch ($freq) { 606 | case Frequency::YEARLY: 607 | $year += $rule->getInterval(); 608 | $month = $dt->format('n'); 609 | $dt = $dt->setDate($year, $month, 1); 610 | 611 | // Stop an infinite loop w/ a sane limit 612 | ++$iterations; 613 | if ($iterations > 300 && !count($dates)) { 614 | break 2; 615 | } 616 | break; 617 | case Frequency::MONTHLY: 618 | $month += $rule->getInterval(); 619 | if ($month > 12) { 620 | $delta = floor($month / 12); 621 | $mod = DateUtil::pymod($month, 12); 622 | $month = $mod; 623 | $year += $delta; 624 | if ($month == 0) { 625 | $month = 12; 626 | --$year; 627 | } 628 | } 629 | $dt = $dt->setDate($year, $month, 1); 630 | break; 631 | case Frequency::WEEKLY: 632 | if ($weekStart > $dtInfo->dayOfWeek) { 633 | $delta = ($dtInfo->dayOfWeek + 1 + (6 - $weekStart)) * -1 + $rule->getInterval() * 7; 634 | } else { 635 | $delta = ($dtInfo->dayOfWeek - $weekStart) * -1 + $rule->getInterval() * 7; 636 | } 637 | 638 | $dt = $dt->modify("+$delta day"); 639 | $year = $dt->format('Y'); 640 | $month = $dt->format('n'); 641 | break; 642 | case Frequency::DAILY: 643 | $dt = $dt->modify('+'.$rule->getInterval().' day'); 644 | $year = $dt->format('Y'); 645 | $month = $dt->format('n'); 646 | break; 647 | case Frequency::HOURLY: 648 | $dt = $dt->modify('+'.$rule->getInterval().' hours'); 649 | $year = $dt->format('Y'); 650 | $month = $dt->format('n'); 651 | $hour = $dt->format('G'); 652 | break; 653 | case Frequency::MINUTELY: 654 | $dt = $dt->modify('+'.$rule->getInterval().' minutes'); 655 | $year = $dt->format('Y'); 656 | $month = $dt->format('n'); 657 | $hour = $dt->format('G'); 658 | $minute = $dt->format('i'); 659 | break; 660 | case Frequency::SECONDLY: 661 | $dt = $dt->modify('+'.$rule->getInterval().' seconds'); 662 | $year = $dt->format('Y'); 663 | $month = $dt->format('n'); 664 | $hour = $dt->format('G'); 665 | $minute = $dt->format('i'); 666 | $second = $dt->format('s'); 667 | break; 668 | } 669 | } 670 | 671 | /** @var Recurrence[] $recurrences */ 672 | $recurrences = array(); 673 | foreach ($dates as $key => $start) { 674 | /** @var \DateTimeInterface $end */ 675 | $end = clone $start; 676 | 677 | $recurrences[] = new Recurrence($start, $end->add($durationInterval), $key); 678 | } 679 | 680 | $recurrences = $this->handleInclusions($rule->getRDates(), $recurrences); 681 | $recurrences = $this->handleExclusions($rule->getExDates(), $recurrences); 682 | 683 | return new RecurrenceCollection($recurrences); 684 | } 685 | 686 | /** 687 | * @param DateExclusion[] $exclusions 688 | * @param Recurrence[] $recurrences 689 | * 690 | * @return Recurrence[] 691 | */ 692 | protected function handleExclusions(array $exclusions, array $recurrences) 693 | { 694 | foreach ($exclusions as $exclusion) { 695 | $exclusionDate = $exclusion->date->format('Ymd'); 696 | $exclusionTime = $exclusion->date->format('Ymd\THis'); 697 | $exclusionTimezone = $exclusion->date->getTimezone(); 698 | 699 | foreach ($recurrences as $key => $recurrence) { 700 | $recurrenceDate = $recurrence->getStart(); 701 | 702 | if ($recurrenceDate->getTimezone()->getName() !== $exclusionTimezone->getName()) { 703 | $recurrenceDate = clone $recurrenceDate; 704 | $recurrenceDate = $recurrenceDate->setTimezone($exclusionTimezone); 705 | } 706 | 707 | if (!$exclusion->hasTime && $recurrenceDate->format('Ymd') == $exclusionDate) { 708 | unset($recurrences[$key]); 709 | continue; 710 | } 711 | 712 | if ($exclusion->hasTime && $recurrenceDate->format('Ymd\THis') == $exclusionTime) { 713 | unset($recurrences[$key]); 714 | } 715 | } 716 | } 717 | 718 | return array_values($recurrences); 719 | } 720 | 721 | /** 722 | * @param DateInclusion[] $inclusions 723 | * @param Recurrence[] $recurrences 724 | * 725 | * @return Recurrence[] 726 | */ 727 | protected function handleInclusions(array $inclusions, array $recurrences) 728 | { 729 | foreach ($inclusions as $inclusion) { 730 | $recurrence = new Recurrence(clone $inclusion->date, clone $inclusion->date); 731 | $recurrences[] = $recurrence; 732 | } 733 | 734 | return array_values($recurrences); 735 | } 736 | } 737 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/ArrayTransformerConfig.php: -------------------------------------------------------------------------------- 1 | virtualLimit = (int) $virtualLimit; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Get the virtual limit imposed upon infinitely recurring events. 35 | * 36 | * @return int 37 | */ 38 | public function getVirtualLimit() 39 | { 40 | return $this->virtualLimit; 41 | } 42 | 43 | /** 44 | * By default, January 30 + 1 month results in March 30 because February doesn't have 30 days. 45 | * 46 | * Enabling this fix tells Recurr that +1 month means "last day of next month". 47 | */ 48 | public function enableLastDayOfMonthFix() 49 | { 50 | $this->lastDayOfMonthFix = true; 51 | } 52 | 53 | public function disableLastDayOfMonthFix() 54 | { 55 | $this->lastDayOfMonthFix = false; 56 | } 57 | 58 | /** 59 | * @return boolean 60 | */ 61 | public function isLastDayOfMonthFixEnabled() 62 | { 63 | return $this->lastDayOfMonthFix; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/Constraint.php: -------------------------------------------------------------------------------- 1 | stopsTransformer; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Recurr/Transformer/Constraint/AfterConstraint.php: -------------------------------------------------------------------------------- 1 | after = $after; 32 | $this->inc = $inc; 33 | } 34 | 35 | /** 36 | * Passes if $date is after $after 37 | * 38 | * {@inheritdoc} 39 | */ 40 | public function test(\DateTimeInterface $date) 41 | { 42 | if ($this->inc) { 43 | return $date >= $this->after; 44 | } 45 | 46 | return $date > $this->after; 47 | } 48 | 49 | /** 50 | * @return \DateTimeInterface 51 | */ 52 | public function getAfter() 53 | { 54 | return $this->after; 55 | } 56 | 57 | /** 58 | * @return bool 59 | */ 60 | public function isInc() 61 | { 62 | return $this->inc; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/Constraint/BeforeConstraint.php: -------------------------------------------------------------------------------- 1 | before = $before; 32 | $this->inc = $inc; 33 | } 34 | 35 | /** 36 | * Passes if $date is before $before 37 | * 38 | * {@inheritdoc} 39 | */ 40 | public function test(\DateTimeInterface $date) 41 | { 42 | if ($this->inc) { 43 | return $date <= $this->before; 44 | } 45 | 46 | return $date < $this->before; 47 | } 48 | 49 | /** 50 | * @return \DateTimeInterface 51 | */ 52 | public function getBefore() 53 | { 54 | return $this->before; 55 | } 56 | 57 | /** 58 | * @return bool 59 | */ 60 | public function isInc() 61 | { 62 | return $this->inc; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/Constraint/BetweenConstraint.php: -------------------------------------------------------------------------------- 1 | after = $after; 36 | $this->before = $before; 37 | $this->inc = $inc; 38 | } 39 | 40 | /** 41 | * Passes if $date is between $after and $before 42 | * 43 | * {@inheritdoc} 44 | */ 45 | public function test(\DateTimeInterface $date) 46 | { 47 | if ($date > $this->before) { 48 | $this->stopsTransformer = true; 49 | } 50 | 51 | if ($this->inc) { 52 | return $date >= $this->after && $date <= $this->before; 53 | } 54 | 55 | return $date > $this->after && $date < $this->before; 56 | } 57 | 58 | /** 59 | * @return \DateTimeInterface 60 | */ 61 | public function getBefore() 62 | { 63 | return $this->before; 64 | } 65 | 66 | /** 67 | * @return \DateTimeInterface 68 | */ 69 | public function getAfter() 70 | { 71 | return $this->after; 72 | } 73 | 74 | /** 75 | * @return bool 76 | */ 77 | public function isInc() 78 | { 79 | return $this->inc; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/ConstraintInterface.php: -------------------------------------------------------------------------------- 1 | translator = $translator ?: new Translator('en'); 15 | } 16 | 17 | public function transform(Rule $rule) 18 | { 19 | $this->fragments = array(); 20 | 21 | switch ($rule->getFreq()) { 22 | case 0: 23 | $this->addYearly($rule); 24 | break; 25 | case 1: 26 | $this->addMonthly($rule); 27 | break; 28 | case 2: 29 | $this->addWeekly($rule); 30 | break; 31 | case 3: 32 | $this->addDaily($rule); 33 | break; 34 | case 4: 35 | $this->addHourly($rule); 36 | break; 37 | case 5: 38 | case 6: 39 | return $this->translator->trans('Unable to fully convert this rrule to text.'); 40 | } 41 | 42 | $until = $rule->getUntil(); 43 | $count = $rule->getCount(); 44 | if ($until instanceof \DateTimeInterface) { 45 | $dateFormatted = $this->translator->trans('day_date', array('date' => $until->format('U'))); 46 | $this->addFragment($this->translator->trans('until %date%', array('date' => $dateFormatted))); 47 | } else if (!empty($count)) { 48 | if ($this->isPlural($count)) { 49 | $this->addFragment($this->translator->trans('for %count% times', array('count' => $count))); 50 | } else { 51 | $this->addFragment($this->translator->trans('for one time')); 52 | } 53 | } 54 | 55 | if (!$this->isFullyConvertible($rule)) { 56 | $this->addFragment($this->translator->trans('(~ approximate)')); 57 | } 58 | 59 | return implode(' ', $this->fragments); 60 | } 61 | 62 | protected function isFullyConvertible(Rule $rule) 63 | { 64 | if ($rule->getFreq() >= 5) { 65 | return false; 66 | } 67 | 68 | $until = $rule->getUntil(); 69 | $count = $rule->getCount(); 70 | if (!empty($until) && !empty($count)) { 71 | return false; 72 | } 73 | 74 | $bySecond = $rule->getBySecond(); 75 | $byMinute = $rule->getByMinute(); 76 | $byHour = $rule->getByHour(); 77 | 78 | if (!empty($bySecond) || !empty($byMinute) || !empty($byHour)) { 79 | return false; 80 | } 81 | 82 | $byWeekNum = $rule->getByWeekNumber(); 83 | $byYearDay = $rule->getByYearDay(); 84 | if ($rule->getFreq() != 0 && (!empty($byWeekNum) || !empty($byYearDay))) { 85 | return false; 86 | } 87 | 88 | return true; 89 | } 90 | 91 | protected function addYearly(Rule $rule) 92 | { 93 | $interval = $rule->getInterval(); 94 | $byMonth = $rule->getByMonth(); 95 | $byMonthDay = $rule->getByMonthDay(); 96 | $byDay = $rule->getByDay(); 97 | $byYearDay = $rule->getByYearDay(); 98 | $byWeekNum = $rule->getByWeekNumber(); 99 | 100 | if (!empty($byMonth) && count($byMonth) > 1 && $interval == 1) { 101 | $this->addFragment($this->translator->trans('every_month_list')); 102 | } else { 103 | $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% years' : 'every year', array('count' => $interval))); 104 | } 105 | 106 | $hasNoOrOneByMonth = is_null($byMonth) || count($byMonth) <= 1; 107 | if ($hasNoOrOneByMonth && empty($byMonthDay) && empty($byDay) && empty($byYearDay) && empty($byWeekNum)) { 108 | $this->addFragment($this->translator->trans('on')); 109 | $monthNum = (is_array($byMonth) && count($byMonth)) ? $byMonth[0] : $rule->getStartDate()->format('n'); 110 | $this->addFragment( 111 | $this->translator->trans('day_month', array('month' => $monthNum, 'day' => $rule->getStartDate()->format('d'))) 112 | ); 113 | } elseif (!empty($byMonth)) { 114 | if ($interval != 1) { 115 | $this->addFragment($this->translator->trans('in_month')); 116 | } 117 | 118 | $this->addByMonth($rule); 119 | } 120 | 121 | if (!empty($byMonthDay)) { 122 | $this->addByMonthDay($rule); 123 | $this->addFragment($this->translator->trans('of_the_month')); 124 | } else if (!empty($byDay)) { 125 | $this->addByDay($rule); 126 | } 127 | 128 | if (!empty($byYearDay)) { 129 | $this->addFragment($this->translator->trans('on the')); 130 | $this->addFragment($this->getByYearDayAsText($byYearDay)); 131 | $this->addFragment($this->translator->trans('day')); 132 | } 133 | 134 | if (!empty($byWeekNum)) { 135 | $this->addFragment($this->translator->trans('in_week')); 136 | $this->addFragment($this->translator->trans($this->isPlural(count($byWeekNum)) ? 'weeks' : 'week')); 137 | $this->addFragment($this->getByWeekNumberAsText($byWeekNum)); 138 | } 139 | 140 | if (empty($byMonthDay) && empty($byYearDay) && empty($byDay) && !empty($byWeekNum)) { 141 | $this->addDayOfWeek($rule); 142 | } 143 | } 144 | 145 | protected function addMonthly(Rule $rule) 146 | { 147 | $interval = $rule->getInterval(); 148 | $byMonth = $rule->getByMonth(); 149 | 150 | if (!empty($byMonth) && $interval == 1) { 151 | $this->addFragment($this->translator->trans('every_month_list')); 152 | } else { 153 | $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% months' : 'every month', array('count' => $interval))); 154 | } 155 | 156 | if (!empty($byMonth)) { 157 | if ($interval != 1) { 158 | $this->addFragment($this->translator->trans('in_month')); 159 | } 160 | 161 | $this->addByMonth($rule); 162 | } 163 | 164 | $byMonthDay = $rule->getByMonthDay(); 165 | $byDay = $rule->getByDay(); 166 | if (!empty($byMonthDay)) { 167 | $this->addByMonthDay($rule); 168 | } else if (!empty($byDay)) { 169 | $this->addByDay($rule); 170 | } 171 | } 172 | 173 | protected function addWeekly(Rule $rule) 174 | { 175 | $interval = $rule->getInterval(); 176 | $byMonth = $rule->getByMonth(); 177 | $byMonthDay = $rule->getByMonthDay(); 178 | $byDay = $rule->getByDay(); 179 | 180 | $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% weeks' : 'every week', array('count' => $interval))); 181 | 182 | if (empty($byMonthDay) && empty($byDay)) { 183 | $this->addDayOfWeek($rule); 184 | } 185 | 186 | if (!empty($byMonth)) { 187 | $this->addFragment($this->translator->trans('in_month')); 188 | $this->addByMonth($rule); 189 | } 190 | 191 | if (!empty($byMonthDay)) { 192 | $this->addByMonthDay($rule); 193 | $this->addFragment($this->translator->trans('of_the_month')); 194 | } else if (!empty($byDay)) { 195 | $this->addByDay($rule); 196 | } 197 | } 198 | 199 | protected function addDaily(Rule $rule) 200 | { 201 | $interval = $rule->getInterval(); 202 | $byMonth = $rule->getByMonth(); 203 | 204 | $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% days' : 'every day', array('count' => $interval))); 205 | 206 | if (!empty($byMonth)) { 207 | $this->addFragment($this->translator->trans('in_month')); 208 | $this->addByMonth($rule); 209 | } 210 | 211 | $byMonthDay = $rule->getByMonthDay(); 212 | $byDay = $rule->getByDay(); 213 | if (!empty($byMonthDay)) { 214 | $this->addByMonthDay($rule); 215 | $this->addFragment($this->translator->trans('of_the_month')); 216 | } else if (!empty($byDay)) { 217 | $this->addByDay($rule); 218 | } 219 | } 220 | 221 | protected function addHourly(Rule $rule) 222 | { 223 | $interval = $rule->getInterval(); 224 | $byMonth = $rule->getByMonth(); 225 | 226 | $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% hours' : 'every hour', array('count' => $interval))); 227 | 228 | if (!empty($byMonth)) { 229 | $this->addFragment($this->translator->trans('in_month')); 230 | $this->addByMonth($rule); 231 | } 232 | 233 | $byMonthDay = $rule->getByMonthDay(); 234 | $byDay = $rule->getByDay(); 235 | if (!empty($byMonthDay)) { 236 | $this->addByMonthDay($rule); 237 | $this->addFragment($this->translator->trans('of_the_month')); 238 | } else if (!empty($byDay)) { 239 | $this->addByDay($rule); 240 | } 241 | } 242 | 243 | protected function addByMonth(Rule $rule) 244 | { 245 | $byMonth = $rule->getByMonth(); 246 | 247 | if (empty($byMonth)) { 248 | return; 249 | } 250 | 251 | $this->addFragment($this->getByMonthAsText($byMonth)); 252 | } 253 | 254 | protected function addByMonthDay(Rule $rule) 255 | { 256 | $byMonthDay = $rule->getByMonthDay(); 257 | $byDay = $rule->getByDay(); 258 | 259 | if (!empty($byDay)) { 260 | $this->addFragment($this->translator->trans('on')); 261 | $this->addFragment($this->getByDayAsText($byDay, 'or')); 262 | $this->addFragment($this->translator->trans('the_for_monthday')); 263 | $this->addFragment($this->getByMonthDayAsText($byMonthDay, 'or')); 264 | } else { 265 | $this->addFragment($this->translator->trans('on the')); 266 | $this->addFragment($this->getByMonthDayAsText($byMonthDay, 'and')); 267 | } 268 | } 269 | 270 | protected function addByDay(Rule $rule) 271 | { 272 | $byDay = $rule->getByDay(); 273 | 274 | $this->addFragment($this->translator->trans('on')); 275 | $this->addFragment($this->getByDayAsText($byDay)); 276 | } 277 | 278 | protected function addDayOfWeek(Rule $rule) 279 | { 280 | $this->addFragment($this->translator->trans('on')); 281 | $dayNames = $this->translator->trans('day_names'); 282 | $this->addFragment($dayNames[$rule->getStartDate()->format('w')]); 283 | } 284 | 285 | public function getByMonthAsText($byMonth) 286 | { 287 | if (empty($byMonth)) { 288 | return ''; 289 | } 290 | 291 | if (count($byMonth) > 1) { 292 | sort($byMonth); 293 | } 294 | 295 | $monthNames = $this->translator->trans('month_names'); 296 | $byMonth = array_map( 297 | function ($monthInt) use ($monthNames) { 298 | return $monthNames[$monthInt - 1]; 299 | }, 300 | $byMonth 301 | ); 302 | 303 | return $this->getListStringFromArray($byMonth); 304 | } 305 | 306 | public function getByDayAsText($byDay, $listSeparator = 'and') 307 | { 308 | if (empty($byDay)) { 309 | return ''; 310 | } 311 | 312 | $map = array( 313 | 'SU' => null, 314 | 'MO' => null, 315 | 'TU' => null, 316 | 'WE' => null, 317 | 'TH' => null, 318 | 'FR' => null, 319 | 'SA' => null 320 | ); 321 | 322 | $dayNames = $this->translator->trans('day_names'); 323 | $timestamp = mktime(1, 1, 1, 1, 12, 2014); // A Sunday 324 | foreach (array_keys($map) as $short) { 325 | $long = $dayNames[date('w', $timestamp)]; 326 | $map[$short] = $long; 327 | $timestamp += 86400; 328 | } 329 | 330 | $numOrdinals = 0; 331 | foreach ($byDay as $key => $short) { 332 | $day = strtoupper($short); 333 | $string = ''; 334 | 335 | if (preg_match('/([+-]?)([0-9]*)([A-Z]+)/', $short, $parts)) { 336 | $symbol = $parts[1]; 337 | $nth = $parts[2]; 338 | $day = $parts[3]; 339 | 340 | if (!empty($nth)) { 341 | ++$numOrdinals; 342 | $string .= $this->getOrdinalNumber($symbol == '-' ? -$nth : $nth); 343 | } 344 | } 345 | 346 | if (!isset($map[$day])) { 347 | throw new \RuntimeException("byDay $short could not be transformed"); 348 | } 349 | 350 | if (!empty($string)) { 351 | $string .= ' '; 352 | } 353 | 354 | $byDay[$key] = ltrim($string.$map[$day]); 355 | } 356 | 357 | $output = $numOrdinals ? $this->translator->trans('the_for_weekday') . ' ' : ''; 358 | if ($output == ' ') { 359 | $output = ''; 360 | } 361 | $output .= $this->getListStringFromArray($byDay, $listSeparator); 362 | 363 | return $output; 364 | } 365 | 366 | public function getByMonthDayAsText($byMonthDay, $listSeparator = 'and') 367 | { 368 | if (empty($byMonthDay)) { 369 | return ''; 370 | } 371 | 372 | // sort negative indices in reverse order so we get e.g. 1st, 2nd, 4th, 3rd last, last day 373 | usort($byMonthDay, function ($a, $b) { 374 | if (($a < 0 && $b < 0) || ($a >= 0 && $b >= 0)) { 375 | return $a - $b; 376 | } 377 | 378 | return $b - $a; 379 | }); 380 | 381 | // generate ordinal numbers and insert a "on the" for clarity in the middle if we have both 382 | // positive and negative ordinals. This is to avoid confusing situations like: 383 | // 384 | // monthly on the 1st and 2nd to the last day 385 | // 386 | // which gets clarified to: 387 | // 388 | // monthly on the 1st day and on the 2nd to the last day 389 | $hadPositives = false; 390 | $hadNegatives = false; 391 | foreach ($byMonthDay as $index => $day) { 392 | $prefix = ''; 393 | if ($day >= 0) { 394 | $hadPositives = true; 395 | } 396 | if ($day < 0) { 397 | if ($hadPositives && !$hadNegatives && $listSeparator === 'and') { 398 | $prefix = $this->translator->trans('on the') . ' '; 399 | } 400 | $hadNegatives = true; 401 | } 402 | $byMonthDay[$index] = $prefix . $this->getOrdinalNumber($day, end($byMonthDay) < 0, true); 403 | } 404 | 405 | return $this->getListStringFromArray($byMonthDay, $listSeparator); 406 | } 407 | 408 | public function getByYearDayAsText($byYearDay) 409 | { 410 | if (empty($byYearDay)) { 411 | return ''; 412 | } 413 | 414 | // sort negative indices in reverse order so we get e.g. 1st, 2nd, 4th, 3rd last, last day 415 | usort($byYearDay, function ($a, $b) { 416 | if (($a < 0 && $b < 0) || ($a >= 0 && $b >= 0)) { 417 | return $a - $b; 418 | } 419 | 420 | return $b - $a; 421 | }); 422 | 423 | $byYearDay = array_map( 424 | array($this, 'getOrdinalNumber'), 425 | $byYearDay, 426 | array_fill(0, count($byYearDay), end($byYearDay) < 0) 427 | ); 428 | 429 | return $this->getListStringFromArray($byYearDay); 430 | } 431 | 432 | public function getByWeekNumberAsText($byWeekNum) 433 | { 434 | if (empty($byWeekNum)) { 435 | return ''; 436 | } 437 | 438 | if (count($byWeekNum) > 1) { 439 | sort($byWeekNum); 440 | } 441 | 442 | return $this->getListStringFromArray($byWeekNum); 443 | } 444 | 445 | protected function addFragment($fragment) 446 | { 447 | if ($fragment && $fragment !== ' ') { 448 | $this->fragments[] = $fragment; 449 | } 450 | } 451 | 452 | public function resetFragments() 453 | { 454 | $this->fragments = array(); 455 | } 456 | 457 | protected function isPlural($number) 458 | { 459 | return $number % 100 != 1; 460 | } 461 | 462 | 463 | protected function getOrdinalNumber($number, $hasNegatives = false, $dayInMonth = false) 464 | { 465 | if (!preg_match('{^-?\d+$}D', $number)) { 466 | throw new \RuntimeException('$number must be a whole number'); 467 | } 468 | 469 | return $this->translator->trans('ordinal_number', array('number' => $number, 'has_negatives' => $hasNegatives, 'day_in_month' => $dayInMonth)); 470 | } 471 | 472 | protected function getListStringFromArray($values, $separator = 'and') 473 | { 474 | $separator = $this->translator->trans($separator); 475 | 476 | if (!is_array($values)) { 477 | throw new \RuntimeException('$values must be an array.'); 478 | } 479 | 480 | $numValues = count($values); 481 | 482 | if (!$numValues) { 483 | return ''; 484 | } 485 | 486 | if ($numValues == 1) { 487 | reset($values); 488 | 489 | return current($values); 490 | } 491 | 492 | if ($numValues == 2) { 493 | return implode(" $separator ", $values); 494 | } 495 | 496 | $lastValue = array_pop($values); 497 | $output = implode(', ', $values); 498 | $output .= " $separator ".$lastValue; 499 | 500 | return $output; 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/Translator.php: -------------------------------------------------------------------------------- 1 | loadLocale($fallbackLocale); 12 | if ($locale !== $fallbackLocale) { 13 | $this->loadLocale($locale); 14 | } 15 | } 16 | 17 | public function loadLocale($locale, $path = null) 18 | { 19 | if (!$path) { 20 | $path = __DIR__ . '/../../../translations/' . $locale . '.php'; 21 | } 22 | if (!file_exists($path)) { 23 | throw new \InvalidArgumentException('Locale '.$locale.' could not be found in '.$path); 24 | } 25 | 26 | $this->data = array_merge($this->data, include $path); 27 | } 28 | 29 | public function trans($string, array $params = array()) 30 | { 31 | $res = $this->data[$string]; 32 | if (is_object($res) && is_callable($res)) { 33 | $res = $res($string, $params); 34 | } 35 | 36 | foreach ($params as $key => $val) { 37 | $res = str_replace('%' . $key . '%', $val, $res); 38 | } 39 | 40 | return $res; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Recurr/Transformer/TranslatorInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Weekday 25 | { 26 | /** 27 | * Weekday number. 28 | * 29 | * 0 = Sunday 30 | * 1 = Monday 31 | * 2 = Tuesday 32 | * 3 = Wednesday 33 | * 4 = Thursday 34 | * 5 = Friday 35 | * 6 = Saturday 36 | * 37 | * @var string 38 | */ 39 | public $weekday; 40 | 41 | /** @var int nth occurrence of the weekday */ 42 | public $num; 43 | 44 | protected $days = array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'); 45 | 46 | /** 47 | * @param int|string $weekday 0-6 or MO..SU 48 | * @param null|int $num 49 | * 50 | * @throws InvalidWeekday 51 | */ 52 | public function __construct($weekday, $num) 53 | { 54 | if (is_numeric($weekday) && $weekday > 6 || $weekday < 0) { 55 | throw new InvalidWeekday('Day is not a valid weekday (0-6)'); 56 | } elseif (!is_numeric($weekday) && !in_array($weekday, $this->days)) { 57 | throw new InvalidWeekday('Day is not a valid weekday (SU, MO, ...)'); 58 | } 59 | 60 | if (!is_numeric($weekday)) { 61 | $weekday = array_search($weekday, $this->days); 62 | } 63 | 64 | $this->weekday = $weekday; 65 | $this->num = $num; 66 | } 67 | 68 | public function __toString() 69 | { 70 | return $this->num . $this->days[$this->weekday]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test.ps1: -------------------------------------------------------------------------------- 1 | #$env:XDEBUG_MODE = "debug" 2 | #$env:XDEBUG_SESSION = 1 3 | 4 | # If no arguments, run all tests. 5 | if (-not $args) 6 | { 7 | ./vendor/bin/phpunit --stop-on-failure -c phpunit.xml.dist 8 | } 9 | else 10 | { 11 | ./vendor/bin/phpunit --stop-on-failure -c phpunit.xml.dist --filter $args[0] 12 | } 13 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #export XDEBUG_MODE=debug XDEBUG_SESSION=1 4 | 5 | # If no arguments, run all tests. 6 | if [ -z "$1" ] 7 | then 8 | ./vendor/bin/phpunit --stop-on-failure -c phpunit.xml.dist 9 | else 10 | ./vendor/bin/phpunit --stop-on-failure -c phpunit.xml.dist --filter "$1" 11 | fi 12 | -------------------------------------------------------------------------------- /translations/da.php: -------------------------------------------------------------------------------- 1 | 'Kunne ikke konvertere denne regel til tekst.', 30 | 'for %count% times' => '%count% gange', 31 | 'for one time' => 'en gang', 32 | '(~ approximate)' => '(~ cirka)', 33 | 'until %date%' => 't.o.m. %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. July 4, 2014 35 | return date('j', $params['date']) . '. '. $months[date('n', $params['date']) - 1].date(', Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $params['day'].'. '.$months[$params['month'] - 1]; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'og', 43 | 'or' => 'eller', 44 | 'in_month' => 'i', // e.g. weekly in January, May and August 45 | 'in_week' => 'i', // e.g. yearly in week 3 46 | 'on' => 'hver', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'den', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'den', // e.g. monthly on the 4th Monday 49 | 'on the' => 'på den', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'i måneden', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'hvert %count% år', 52 | 'every year' => 'årligt', 53 | 'every_month_list' => 'hver', // e.g. every January, May and August 54 | 'every %count% months' => 'hver %count% måned', 55 | 'every month' => 'månedsvis', 56 | 'every %count% weeks' => 'hver %count% uge', 57 | 'every week' => 'ugenligt', 58 | 'every %count% days' => 'hver %count% dag', 59 | 'every day' => 'dagligt', 60 | 'last' => 'sidste', // e.g. 2nd last Friday 61 | 'days' => 'dage', 62 | 'day' => 'dag', 63 | 'weeks' => 'uger', 64 | 'week' => 'uge', 65 | // formats a number with a prefix e.g. every year on the 1st and 200th day 66 | // negative numbers should be handled as in '5th to the last' or 'last' 67 | // 68 | // if has_negatives is true in the params, it is good form to add 'day' after 69 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 70 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 71 | 'ordinal_number' => function ($str, $params) { 72 | $number = $params['number']; 73 | 74 | $ends = array(':e', ':a', ':a', ':e', ':e', ':e', ':e', ':e', ':e', ':e'); 75 | $suffix = ''; 76 | 77 | $isNegative = $number < 0; 78 | 79 | if ($number == -1) { 80 | $abbreviation = 'last'; 81 | } else { 82 | if ($isNegative) { 83 | $number = abs($number); 84 | $suffix = ' to the last'; 85 | } 86 | 87 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 88 | $abbreviation = $number.'.'; 89 | } else { 90 | $abbreviation = $number.$ends[$number % 10]; 91 | } 92 | } 93 | 94 | if (!empty($params['has_negatives'])) { 95 | $suffix .= ' dag'; 96 | } 97 | 98 | return $abbreviation . $suffix; 99 | }, 100 | ); -------------------------------------------------------------------------------- /translations/de.php: -------------------------------------------------------------------------------- 1 | 'RRule kann nicht vollständig zu Text konvertiert werden.', 30 | 'for %count% times' => '%count% Mal', 31 | 'for one time' => 'einmal', 32 | '(~ approximate)' => '(~ ungefähr)', 33 | 'until %date%' => 'bis %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. 4. Juli, 2014 35 | return date('j. ', $params['date']) . $months[date('n', $params['date']) - 1] . date(', Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $params['day'].'. '.$months[$params['month'] - 1]; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'und', 43 | 'or' => 'oder', 44 | 'in_month' => 'im', // e.g. weekly in January, May and August 45 | 'in_week' => 'in', // e.g. yearly in week 3 46 | 'on' => 'am', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'dem', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => '', // e.g. monthly on the 4th Monday 49 | 'on the' => 'am', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'des Monats', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'alle %count% Jahre', 52 | 'every year' => 'jährlich', 53 | 'every_month_list' => 'jeden', // e.g. every January, May and August 54 | 'every %count% months' => 'alle %count% Monate', 55 | 'every month' => 'monatlich', 56 | 'every %count% weeks' => 'alle %count% Wochen', 57 | 'every week' => 'wöchentlich', 58 | 'every %count% days' => 'alle %count% Tage', 59 | 'every day' => 'täglich', 60 | 'every %count% hours' => 'alle %count% Stunden', 61 | 'every hour' => 'stündlich', 62 | 'last' => 'letzte', // e.g. 2nd last Friday 63 | 'days' => 'Tage', 64 | 'day' => 'Tag', 65 | 'weeks' => 'Wochen', 66 | 'week' => 'Woche', 67 | 'hours' => 'Stunden', 68 | 'hour' => 'stündlich', 69 | // formats a number with a prefix e.g. every year on the 1st and 200th day 70 | // negative numbers should be handled as in '5th to the last' or 'last' 71 | // 72 | // if has_negatives is true in the params, it is good form to add 'day' after 73 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 74 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 75 | 'ordinal_number' => function ($str, $params) { 76 | $number = $params['number']; 77 | 78 | $suffix = ''; 79 | $isNegative = $number < 0; 80 | 81 | if ($number == -1) { 82 | $abbreviation = 'letzten'; 83 | } elseif ($number == -2) { 84 | $abbreviation = 'vorletzten'; 85 | } elseif ($number == -3) { 86 | $abbreviation = 'drittletzten'; 87 | } elseif ($number == -4) { 88 | $abbreviation = 'viertletzten'; 89 | } elseif ($number == -5) { 90 | $abbreviation = 'fünftletzten'; 91 | } elseif ($number == -6) { 92 | $abbreviation = 'sechstletzten'; 93 | } elseif ($number == -7) { 94 | $abbreviation = 'siebtletzten'; 95 | } elseif ($number == -8) { 96 | $abbreviation = 'achtletzten'; 97 | } elseif ($number == -9) { 98 | $abbreviation = 'neuntletzten'; 99 | } elseif ($number == -10) { 100 | $abbreviation = 'zehntletzten'; 101 | } elseif ($number == -11) { 102 | $abbreviation = 'elftletzten'; 103 | } elseif ($isNegative) { 104 | $number = abs($number); 105 | $abbreviation = $number . 't letzten'; 106 | } else { 107 | $abbreviation = $number . '.'; 108 | } 109 | 110 | if (!empty($params['has_negatives']) && $isNegative) { 111 | $suffix .= ' Tag'; 112 | } 113 | 114 | return $abbreviation . $suffix; 115 | }, 116 | ); 117 | -------------------------------------------------------------------------------- /translations/el.php: -------------------------------------------------------------------------------- 1 | 'Αδυναμία πλήρους μετατροπής αυτού του κανόνα rrule σε κείμενο.', 44 | 'for %count% times' => 'για %count% φορές', 45 | 'for one time' => 'για μία φορά', 46 | '(~ approximate)' => '(~ κατά προσέγγιση)', 47 | 'until %date%' => 'μέχρι %date%', // e.g. every year until July 4, 2014 48 | 'day_date' => function ($str, $params) use ($days, $months_genitive) { // outputs a day date, e.g. 4 Ιουλίου 2014 49 | return date('j', $params['date']) . ' ' . $months_genitive[date('n', $params['date']) - 1] . ' '. date('Y', $params['date']); 50 | }, 51 | 'day_month' => function ($str, $params) use ($days, $months_genitive) { // outputs a day month, e.g. 4 Ιουλίου 52 | return $params['day'] . ' ' . $months_genitive[$params['month'] - 1]; 53 | }, 54 | 'day_names' => $days, 55 | 'month_names' => $months, 56 | 'and' => 'και', 57 | 'or' => 'ή', 58 | 'in_month' => 'τον', // e.g. weekly in January, May and August 59 | 'in_week' => 'την', // e.g. yearly in week 3 60 | 'on' => 'την', // e.g. every day on Tuesday, Wednesday and Friday 61 | 'the_for_monthday' => 'την', // e.g. monthly on Tuesday the 1st 62 | 'the_for_weekday' => 'την', // e.g. monthly on the 4th Monday 63 | 'on the' => 'την', // e.g. every year on the 1st and 200th day 64 | 'of_the_month' => 'του μήνα', // e.g. every year on the 2nd or 3rd of the month 65 | 'every %count% years' => 'κάθε %count% χρόνια', 66 | 'every year' => 'ετήσια', 67 | 'every_month_list' => 'κάθε', // e.g. every January, May and August 68 | 'every %count% months' => 'κάθε %count% μήνες', 69 | 'every month' => 'μηνιαία', 70 | 'every %count% weeks' => 'κάθε %count% εβδομάδες', 71 | 'every week' => 'εβδομαδιαία', 72 | 'every %count% days' => 'κάθε %count% ημέρες', 73 | 'every day' => 'καθημερινά', 74 | 'last' => 'τελευταία', // e.g. 2nd last Friday 75 | 'days' => 'ημέρες', 76 | 'day' => 'ημέρα', 77 | 'weeks' => 'εβδομάδες', 78 | 'week' => 'εβδομάδα', 79 | // formats a number with a prefix e.g. every year on the 1st and 200th day 80 | // negative numbers should be handled as in '5th to the last' or 'last' 81 | // 82 | // if has_negatives is true in the params, it is good form to add 'day' after 83 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 84 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 85 | 'ordinal_number' => function ($str, $params) { 86 | $number = $params['number']; 87 | 88 | $ends = 'η'; 89 | $suffix = ''; 90 | 91 | $isNegative = $number < 0; 92 | 93 | if ($number == -1) { 94 | $abbreviation = 'τελευταία'; 95 | } else { 96 | if ($isNegative) { 97 | $number = abs($number); 98 | $suffix = ' μέχρι την τελευταία'; 99 | } 100 | 101 | $abbreviation = $number . $ends; 102 | } 103 | 104 | if (!empty($params['has_negatives'])) { 105 | $suffix .= ' ημέρα'; 106 | } 107 | 108 | return $abbreviation . $suffix; 109 | }, 110 | ); 111 | -------------------------------------------------------------------------------- /translations/en.php: -------------------------------------------------------------------------------- 1 | 'Unable to fully convert this rrule to text.', 30 | 'for %count% times' => 'for %count% times', 31 | 'for one time' => 'once', 32 | '(~ approximate)' => '(~ approximate)', 33 | 'until %date%' => 'until %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. July 4, 2014 35 | return $months[date('n', $params['date']) - 1] . ' '. date('j, Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $months[$params['month'] - 1] . ' '. $params['day']; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'and', 43 | 'or' => 'or', 44 | 'in_month' => 'in', // e.g. weekly in January, May and August 45 | 'in_week' => 'in', // e.g. yearly in week 3 46 | 'on' => 'on', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'the', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'the', // e.g. monthly on the 4th Monday 49 | 'on the' => 'on the', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'of the month', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'every %count% years', 52 | 'every year' => 'yearly', 53 | 'every_month_list' => 'every', // e.g. every January, May and August 54 | 'every %count% months' => 'every %count% months', 55 | 'every month' => 'monthly', 56 | 'every %count% weeks' => 'every %count% weeks', 57 | 'every week' => 'weekly', 58 | 'every %count% days' => 'every %count% days', 59 | 'every day' => 'daily', 60 | 'every %count% hours' => 'every %count% hours', 61 | 'every hour' => 'hourly', 62 | 'last' => 'last', // e.g. 2nd last Friday 63 | 'days' => 'days', 64 | 'day' => 'day', 65 | 'weeks' => 'weeks', 66 | 'week' => 'week', 67 | 'hours' => 'hours', 68 | 'hour' => 'hour', 69 | // formats a number with a prefix e.g. every year on the 1st and 200th day 70 | // negative numbers should be handled as in '5th to the last' or 'last' 71 | // 72 | // if has_negatives is true in the params, it is good form to add 'day' after 73 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 74 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 75 | 'ordinal_number' => function ($str, $params) { 76 | $number = $params['number']; 77 | 78 | $ends = array('th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'); 79 | $suffix = ''; 80 | 81 | $isNegative = $number < 0; 82 | 83 | if ($number == -1) { 84 | $abbreviation = 'last'; 85 | } else { 86 | if ($isNegative) { 87 | $number = abs($number); 88 | $suffix = ' to the last'; 89 | } 90 | 91 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 92 | $abbreviation = $number.'th'; 93 | } else { 94 | $abbreviation = $number.$ends[$number % 10]; 95 | } 96 | } 97 | 98 | if (!empty($params['has_negatives'])) { 99 | $suffix .= ' day'; 100 | } 101 | 102 | return $abbreviation . $suffix; 103 | }, 104 | ); 105 | -------------------------------------------------------------------------------- /translations/es.php: -------------------------------------------------------------------------------- 1 | 'No se puede convertir completamente este RRULE al texto.', 30 | 'for %count% times' => 'para %count% veces', 31 | 'for one time' => 'por una vez', 32 | '(~ approximate)' => '(~ aproximado)', 33 | 'until %date%' => 'hasta %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. July 4, 2014 35 | return $months[date('n', $params['date']) - 1] . ' '. date('j, Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $months[$params['month'] - 1] . ' '. $params['day']; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'y', 43 | 'or' => 'o', 44 | 'in_month' => 'en', // e.g. weekly in January, May and August 45 | 'in_week' => 'en', // e.g. yearly in week 3 46 | 'on' => 'en', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'el', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'en el', // e.g. monthly on the 4th Monday 49 | 'on the' => 'en el', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'del mes', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'cada %count% años', 52 | 'every year' => 'anual', 53 | 'every_month_list' => 'cada', // e.g. every January, May and August 54 | 'every %count% months' => 'cada %count% meses', 55 | 'every month' => 'mensual', 56 | 'every %count% weeks' => 'cada %count% semanas', 57 | 'every week' => 'cada semana', 58 | 'every %count% days' => 'cada %count% días', 59 | 'every day' => 'diariamente', 60 | 'last' => 'pasado', // e.g. 2nd last Friday 61 | 'days' => 'día', 62 | 'day' => 'el día', 63 | 'weeks' => 'semanas', 64 | 'week' => 'semana', 65 | // formats a number with a prefix e.g. every year on the 1st and 200th day 66 | // negative numbers should be handled as in '5th to the last' or 'last' 67 | // 68 | // if has_negatives is true in the params, it is good form to add 'day' after 69 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 70 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 71 | 'ordinal_number' => function ($str, $params) { 72 | $number = $params['number']; 73 | 74 | $ends = array('a', 'a', 'nd', 'a', 'a', 'a', 'a', 'a', 'a', 'a'); 75 | $suffix = ''; 76 | 77 | $isNegative = $number < 0; 78 | 79 | if ($number == -1) { 80 | $abbreviation = 'último'; 81 | } else { 82 | if ($isNegative) { 83 | $number = abs($number); 84 | $suffix = ' a la última'; 85 | } 86 | 87 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 88 | $abbreviation = $number.'a'; 89 | } else { 90 | $abbreviation = $number.$ends[$number % 10]; 91 | } 92 | } 93 | 94 | if (!empty($params['has_negatives'])) { 95 | $suffix .= ' día'; 96 | } 97 | 98 | return $abbreviation . $suffix; 99 | }, 100 | ); 101 | -------------------------------------------------------------------------------- /translations/eu.php: -------------------------------------------------------------------------------- 1 | 'Ezin izan da rrule testura osoki bihurtu.', 5 | 'for %count% times' => '%count% aldiz', 6 | 'for %count% time' => '%count% aldia', 7 | '(~ approximate)' => '(~ inguru)', 8 | 'until %date%' => '%date% arte', // e.g. every year until July 4, 2014 9 | 'day_date' => defined('PHP_WINDOWS_VERSION_BUILD') ? '%B %#d, %Y' : '%B %e, %Y', 10 | 'and' => 'eta', 11 | 'or' => 'edo', 12 | 'in' => 'hilabete hauetan:', // e.g. every week in January, May and August 13 | 'on' => 'egun hauetan:', // e.g. every day on Tuesday, Wednesday and Friday 14 | 'the' => '', 15 | 'on the' => '', // e.g. every year on the 1st and 200th day 16 | 'every %count% years' => '%count% urtero', 17 | 'every year' => 'urtero', 18 | 'every_month_list' => 'hilabete hauetan', // e.g. every January, May and August 19 | 'every %count% months' => '%count% hilabetero', 20 | 'every month' => 'hilabetero', 21 | 'every %count% weeks' => '%count% astero', 22 | 'every week' => 'astero', 23 | 'every %count% days' => '%count% egunero', 24 | 'every day' => 'egunero', 25 | 'last' => 'azken', // e.g. 2nd last Friday 26 | 'days' => 'egun', 27 | 'day' => 'egun', 28 | 'weeks' => 'aste', 29 | 'week' => 'aste', 30 | 'ordinal_number' => function ($str, $params) { // formats a number with a prefix e.g. every year on the 1st and 200th day 31 | $number = $params['number']; 32 | 33 | $ends = array('garren', 'go', 'garren', 'garren', 'garren', 'garren', 'garren', 'garren', 'garren', 'garren'); 34 | 35 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 36 | $abbreviation = $number.'garren'; 37 | } else { 38 | $abbreviation = $number.$ends[$number % 10]; 39 | } 40 | 41 | return $abbreviation; 42 | }, 43 | ); 44 | -------------------------------------------------------------------------------- /translations/fr.php: -------------------------------------------------------------------------------- 1 | 'Cette règle de récurrence n\'a pas pu être convertie en texte.', 30 | 'for %count% times' => '%count% fois', 31 | 'for one time' => 'une fois', 32 | '(~ approximate)' => '(~ approximation)', 33 | 'until %date%' => 'jusqu\'au %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. 4 juillet, 2014 35 | return date('j ', $params['date']) . $months[date('n', $params['date']) - 1] . date(', Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $params['day'].' '.$months[$params['month'] - 1]; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'et', 43 | 'or' => 'ou', 44 | 'in_month' => 'en', // e.g. weekly in January, May and August 45 | 'in_week' => 'en', // e.g. yearly in week 3 46 | 'on' => 'le', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'le', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => '', // e.g. monthly on the 4th Monday 49 | 'on the' => 'le', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'du mois', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'tous les %count% ans', 52 | 'every year' => 'chaque année', 53 | 'every_month_list' => 'chaque', // e.g. every January, May and August 54 | 'every %count% months' => 'tous les %count% mois', 55 | 'every month' => 'chaque mois', 56 | 'every %count% weeks' => 'toutes les %count% semaines', 57 | 'every week' => 'chaque semaine', 58 | 'every %count% days' => 'tous les %count% jours', 59 | 'every day' => 'chaque jour', 60 | 'every %count% hours' => 'toutes les %count% heures', 61 | 'every hour' => 'chaque heure', 62 | 'last' => 'dernier', // e.g. 2nd last Friday 63 | 'days' => 'jours', 64 | 'day' => 'jour', 65 | 'weeks' => 'semaines', 66 | 'week' => 'semaine', 67 | 'hours' => 'heures', 68 | 'hour' => 'heure', 69 | // formats a number with a prefix e.g. every year on the 1st and 200th day 70 | // negative numbers should be handled as in '5th to the last' or 'last' 71 | // 72 | // if has_negatives is true in the params, it is good form to add 'day' after 73 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 74 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 75 | 'ordinal_number' => function ($str, $params) { 76 | $number = $params['number']; 77 | 78 | $suffix = ''; 79 | $isNegative = $number < 0; 80 | 81 | if ($number == -1) { 82 | $abbreviation = 'dernier'; 83 | } elseif ($number == -2) { 84 | $abbreviation = 'avant dernier'; 85 | } elseif ($isNegative) { 86 | $number = abs($number); 87 | $abbreviation = $number . 'ème au dernier'; 88 | } elseif ($number == 1 && (!$params['day_in_month'])) { 89 | $abbreviation = $number . 'er'; 90 | } else if (!$params['day_in_month']) { 91 | $abbreviation = $number . 'ème'; 92 | } 93 | else { 94 | $abbreviation = $number; 95 | } 96 | 97 | if (!empty($params['has_negatives'])) { 98 | $suffix .= ' jour'; 99 | } 100 | 101 | return $abbreviation . $suffix; 102 | }, 103 | ); 104 | -------------------------------------------------------------------------------- /translations/it.php: -------------------------------------------------------------------------------- 1 | 'Non è possibile convertire questo rrule in testo.', 30 | 'for %count% times' => 'per %count% volte', 31 | 'for one time' => 'per una volta', 32 | '(~ approximate)' => '(~ approssimato)', 33 | 'until %date%' => 'fino al %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. 4 luglio, 2014 35 | return date('j ', $params['date']) . $months[date('n', $params['date']) - 1] . date(', Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $params['day'].' '.$months[$params['month'] - 1]; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'e', 43 | 'or' => 'o', 44 | 'in_month' => 'in', // e.g. weekly in January, May and August 45 | 'in_week' => 'in', // e.g. yearly in week 3 46 | 'on' => 'il', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'il', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'il', // e.g. monthly on the 4th Monday 49 | 'on the' => 'il', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'del mese', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'ogni %count% anni', 52 | 'every year' => 'ogni anno', 53 | 'every_month_list' => 'ogni', // e.g. every January, May and August 54 | 'every %count% months' => 'ogni %count% mesi', 55 | 'every month' => 'ogni mese', 56 | 'every %count% weeks' => 'ogni %count% settimane', 57 | 'every week' => 'ogni settimana', 58 | 'every %count% days' => 'ogni %count% giorni', 59 | 'every day' => 'ogni giorno', 60 | 'every %count% hours' => 'ogni %count% ore', 61 | 'every hour' => 'ogni ora', 62 | 'last' => 'scorso', // e.g. 2nd last Friday 63 | 'days' => 'giorni', 64 | 'day' => 'giorno', 65 | 'weeks' => 'settimane', 66 | 'week' => 'settimana', 67 | 'hours' => 'ore', 68 | 'hour' => 'ora', 69 | // formats a number with a prefix e.g. every year on the 1st and 200th day 70 | // negative numbers should be handled as in '5th to the last' or 'last' 71 | // 72 | // if has_negatives is true in the params, it is good form to add 'day' after 73 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 74 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 75 | 'ordinal_number' => function ($str, $params) { 76 | $number = $params['number']; 77 | 78 | $suffix = ''; 79 | $isNegative = $number < 0; 80 | 81 | if ($number == -1) { 82 | $abbreviation = 'ultimo'; 83 | } elseif ($number == -2) { 84 | $abbreviation = 'penultimo'; 85 | } elseif ($number == -3) { 86 | $abbreviation = 'terzultimo'; 87 | } elseif ($number == -4) { 88 | $abbreviation = 'quarto ultimo'; 89 | } elseif ($number == -5) { 90 | $abbreviation = 'quinta ultimo'; 91 | } elseif ($number == -6) { 92 | $abbreviation = 'sesto ultimo'; 93 | } elseif ($number == -7) { 94 | $abbreviation = 'settimo ultimo'; 95 | } elseif ($number == -8) { 96 | $abbreviation = 'otto ultimo'; 97 | } elseif ($number == -9) { 98 | $abbreviation = 'nono ultimo'; 99 | } elseif ($number == -10) { 100 | $abbreviation = 'decimo ultimo'; 101 | } elseif ($number == -11) { 102 | $abbreviation = 'undici ultimo'; 103 | } elseif ($isNegative) { 104 | $number = abs($number); 105 | $abbreviation = $number . ' ultimo'; 106 | } else { 107 | $abbreviation = $number; 108 | } 109 | 110 | if (!empty($params['has_negatives'])) { 111 | $suffix .= ' giorno'; 112 | } 113 | 114 | return $abbreviation . $suffix; 115 | }, 116 | ); 117 | -------------------------------------------------------------------------------- /translations/nl.php: -------------------------------------------------------------------------------- 1 | 'Unable to fully convert this rrule to text.', 30 | 'for %count% times' => 'voor %count% keer', 31 | 'for one time' => 'eenmalig', 32 | '(~ approximate)' => '(~ ongeveer)', 33 | 'until %date%' => 'tot en met %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. July 4, 2014 35 | return date('j', $params['date']).' '.$months[date('n', $params['date']) - 1] . ' '. date('Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $params['day'].' '.$months[$params['month'] - 1]; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'en', 43 | 'or' => 'of', 44 | 'in_month' => 'op', // e.g. weekly in January, May and August 45 | 'in_week' => 'op', // e.g. yearly in week 3 46 | 'on' => 'op', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'de', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'de', // e.g. monthly on the 4th Monday 49 | 'on the' => 'op de', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'van de maand', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'elke %count% jaar', 52 | 'every year' => 'jaarlijks', 53 | 'every_month_list' => 'elke', // e.g. every January, May and August 54 | 'every %count% months' => 'elke %count% maanden', 55 | 'every month' => 'maandelijks', 56 | 'every %count% weeks' => 'elke %count% weken', 57 | 'every week' => 'wekelijks', 58 | 'every %count% days' => 'elke %count% dagen', 59 | 'every day' => 'dagelijks', 60 | 'last' => 'laatste', // e.g. 2nd last Friday 61 | 'days' => 'dagen', 62 | 'day' => 'dag', 63 | 'weeks' => 'weken', 64 | 'week' => 'week', 65 | // formats a number with a prefix e.g. every year on the 1st and 200th day 66 | // negative numbers should be handled as in '5th to the last' or 'last' 67 | // 68 | // if has_negatives is true in the params, it is good form to add 'day' after 69 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 70 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 71 | 'ordinal_number' => function ($str, $params) { 72 | $number = $params['number']; 73 | 74 | $ends = array('ste', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de'); 75 | $suffix = ''; 76 | 77 | $isNegative = $number < 0; 78 | 79 | if ($number == -1) { 80 | $abbreviation = 'laatste'; 81 | } else { 82 | if ($isNegative) { 83 | $number = abs($number); 84 | $suffix = ' na laatste'; 85 | } 86 | 87 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 88 | $abbreviation = $number.'ste'; 89 | } else { 90 | $abbreviation = $number.$ends[$number % 10]; 91 | } 92 | } 93 | 94 | if (!empty($params['has_negatives'])) { 95 | $suffix .= ' dag'; 96 | } 97 | 98 | return $abbreviation . $suffix; 99 | }, 100 | ); 101 | -------------------------------------------------------------------------------- /translations/no.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simshaun/recurr/7b136768d64f257065e38a804ee6d2f9af6ba6d1/translations/no.php -------------------------------------------------------------------------------- /translations/pt-br.php: -------------------------------------------------------------------------------- 1 | 'Não foi possível converter esta regra para texto.', 30 | 'for %count% times' => 'por %count% vezes', 31 | 'for one time' => 'uma vez', 32 | '(~ approximate)' => '(~ approximado)', 33 | 'until %date%' => 'até %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. July 4, 2014 35 | return date('j', $params['date']) . ' de ' . $months[date('n', $params['date']) - 1] . ' de ' . date('Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $params['day'].' de '.$months[$params['month'] - 1]; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'e', 43 | 'or' => 'ou', 44 | 'in_month' => 'em', // e.g. weekly in January, May and August 45 | 'in_week' => 'na', // e.g. yearly in week 3 46 | 'on' => 'à', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'o', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'o', // e.g. monthly on the 4th Monday 49 | 'on the' => 'no', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'do mês', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'a cada %count% anos', 52 | 'every year' => 'anualmente', 53 | 'every_month_list' => 'sempre em', // e.g. every January, May and August 54 | 'every %count% months' => 'a cada %count% meses', 55 | 'every month' => 'mensalmente', 56 | 'every %count% weeks' => 'a cada %count% semanas', 57 | 'every week' => 'semanalmente', 58 | 'every %count% days' => 'a cada %count% dias', 59 | 'every day' => 'diariamente', 60 | 'last' => 'último', // e.g. 2nd last Friday 61 | 'days' => 'dias', 62 | 'day' => 'dia', 63 | 'weeks' => 'semanas', 64 | 'week' => 'semana', 65 | // formats a number with a prefix e.g. every year on the 1st and 200th day 66 | // negative numbers should be handled as in '5th to the last' or 'last' 67 | // 68 | // if has_negatives is true in the params, it is good form to add 'day' after 69 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 70 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 71 | 'ordinal_number' => function ($str, $params) { 72 | $number = $params['number']; 73 | 74 | $abbreviation = $number.'°'; 75 | $isNegative = $number < 0; 76 | if ($isNegative) { 77 | $abbreviation = $abbreviation.' último'; 78 | } 79 | 80 | $suffix = ''; 81 | if (!empty($params['has_negatives'])) { 82 | $suffix .= ' dia'; 83 | } 84 | 85 | return $abbreviation . $suffix; 86 | }, 87 | ); 88 | -------------------------------------------------------------------------------- /translations/sv.php: -------------------------------------------------------------------------------- 1 | 'Kunde inte konvertera denna rrule till text.', 30 | 'for %count% times' => '%count% gånger', 31 | 'for one time' => 'en gång', 32 | '(~ approximate)' => '(~ ungefärlig)', 33 | 'until %date%' => 't.o.m. %date%', // e.g. every year until July 4, 2014 34 | 'day_date' => function ($str, $params) use ($days, $months) { // outputs a day date, e.g. July 4, 2014 35 | return $months[date('n', $params['date']) - 1] . ' '. date('j, Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $months[$params['month'] - 1].' '.$params['day']; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 'och', 43 | 'or' => 'eller', 44 | 'in_month' => 'i', // e.g. weekly in January, May and August 45 | 'in_week' => 'i', // e.g. yearly in week 3 46 | 'on' => 'på', // e.g. every day on Tuesday, Wednesday and Friday 47 | 'the_for_monthday' => 'den', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'den', // e.g. monthly on the 4th Monday 49 | 'on the' => 'på den', // e.g. every year on the 1st and 200th day 50 | 'of_the_month' => 'i månaden', // e.g. every year on the 2nd or 3rd of the month 51 | 'every %count% years' => 'varje %count% år', 52 | 'every year' => 'årligen', 53 | 'every_month_list' => 'varje', // e.g. every January, May and August 54 | 'every %count% months' => 'varje %count% månad', 55 | 'every month' => 'månadsvis', 56 | 'every %count% weeks' => 'varje %count% vecka', 57 | 'every week' => 'veckovis', 58 | 'every %count% days' => 'varje %count% dag', 59 | 'every day' => 'dagligen', 60 | 'last' => 'sista', // e.g. 2nd last Friday 61 | 'days' => 'dagar', 62 | 'day' => 'dag', 63 | 'weeks' => 'veckor', 64 | 'week' => 'vecka', 65 | // formats a number with a prefix e.g. every year on the 1st and 200th day 66 | // negative numbers should be handled as in '5th to the last' or 'last' 67 | // 68 | // if has_negatives is true in the params, it is good form to add 'day' after 69 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 70 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 71 | 'ordinal_number' => function ($str, $params) { 72 | $number = $params['number']; 73 | 74 | $ends = array(':e', ':a', ':a', ':e', ':e', ':e', ':e', ':e', ':e', ':e'); 75 | $suffix = ''; 76 | 77 | $isNegative = $number < 0; 78 | 79 | if ($number == -1) { 80 | $abbreviation = 'last'; 81 | } else { 82 | if ($isNegative) { 83 | $number = abs($number); 84 | $suffix = ' to the last'; 85 | } 86 | 87 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 88 | $abbreviation = $number.'th'; 89 | } else { 90 | $abbreviation = $number.$ends[$number % 10]; 91 | } 92 | } 93 | 94 | if (!empty($params['has_negatives'])) { 95 | $suffix .= ' dag'; 96 | } 97 | 98 | return $abbreviation . $suffix; 99 | }, 100 | ); 101 | -------------------------------------------------------------------------------- /translations/tr.php: -------------------------------------------------------------------------------- 1 | 'Bu rrule tam metne dönüştürülemiyor.', 30 | 'for %count% times' => '%count% kez', 31 | 'for one time' => 'bir kere', 32 | '(~ approximate)' => '(~ yaklaşık)', 33 | 'until %date%' => 'kadar %date%', // e.g. 4 Temmuz 2014 e kadar her yıl 34 | 'day_date' => function ($str, $params) use ($days, $months) { // tarih çıktıları, e.g. Temmuz 4, 2014 35 | return $months[date('n', $params['date']) - 1] . ' '. date('j, Y', $params['date']); 36 | }, 37 | 'day_month' => function ($str, $params) use ($days, $months) { // outputs a day month, e.g. July 4 38 | return $months[$params['month'] - 1].' '.$params['day']; 39 | }, 40 | 'day_names' => $days, 41 | 'month_names' => $months, 42 | 'and' => 've', 43 | 'or' => 'veya', 44 | 'in_month' => 'içinde', // e.g. Ocak, Mayıs ve Ağustos'ta haftalık 45 | 'in_week' => 'içinde', // e.g. yıllık haftada 3 46 | 'on' => 'on', // e.g. her Salı, Çarşamba ve Cuma günü 47 | 'the_for_monthday' => 'the', // e.g. monthly on Tuesday the 1st 48 | 'the_for_weekday' => 'the', // e.g. monthly on the 4th Monday 49 | 'on the' => 'üzerinde', // e.g. her yıl 1. ve 200. günde 50 | 'of_the_month' => 'ayın', // e.g. her yıl 2. ve 3. ayın 51 | 'every %count% years' => 'every %count% years', 52 | 'every year' => 'yıllık', 53 | 'every_month_list' => 'her', // e.g. her Ocak, Mayıs ve Ağustos 54 | 'every %count% months' => 'her %count% ay', 55 | 'every month' => 'aylık', 56 | 'every %count% weeks' => 'her %count% hafta', 57 | 'every week' => 'haftalık', 58 | 'every %count% days' => 'her %count% gün', 59 | 'every day' => 'günlük', 60 | 'last' => 'son', // e.g. 2nd last Friday 61 | 'days' => 'günler', 62 | 'day' => 'gün', 63 | 'weeks' => 'haftalar', 64 | 'week' => 'hafta', 65 | // formats a number with a prefix e.g. every year on the 1st and 200th day 66 | // negative numbers should be handled as in '5th to the last' or 'last' 67 | // 68 | // if has_negatives is true in the params, it is good form to add 'day' after 69 | // each number, as in: 'every month on the 5th day or 2nd to the last day' or 70 | // it may be confusing like 'every month on the 5th or 2nd to the last day' 71 | 'ordinal_number' => function ($str, $params) { 72 | $number = $params['number']; 73 | 74 | $ends = array('th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'); 75 | $suffix = ''; 76 | 77 | $isNegative = $number < 0; 78 | 79 | if ($number == -1) { 80 | $abbreviation = 'son'; 81 | } else { 82 | if ($isNegative) { 83 | $number = abs($number); 84 | $suffix = ' sonuna kadar'; 85 | } 86 | 87 | if (($number % 100) >= 11 && ($number % 100) <= 13) { 88 | $abbreviation = $number.'th'; 89 | } else { 90 | $abbreviation = $number.$ends[$number % 10]; 91 | } 92 | } 93 | 94 | if (!empty($params['has_negatives'])) { 95 | $suffix .= ' gün'; 96 | } 97 | 98 | return $abbreviation . $suffix; 99 | }, 100 | ); 101 | --------------------------------------------------------------------------------