Created
August 16, 2012 17:48
-
-
Save jippi/3372050 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| App::uses('CalendarDate', 'Lib'); | |
| /** | |
| * Support class for working with recurring entries | |
| * | |
| */ | |
| class RecurringSupport { | |
| /** | |
| * Protected property for prefixing keys | |
| * | |
| * @var string | |
| */ | |
| protected static $_modelName; | |
| /** | |
| * Set the prefix for prefixKey calls | |
| * | |
| * @param string $model | |
| * @return void | |
| */ | |
| public static function setModelName($model) { | |
| static::$_modelName = $model; | |
| } | |
| /** | |
| * Optionally prefix a key with a modelName | |
| * | |
| * Returns the original key if no prefix is specified | |
| * | |
| * @param string $key | |
| * @return string | |
| */ | |
| public static function prefixKey($key) { | |
| if (empty(static::$_modelName)) { | |
| return $key; | |
| } | |
| return sprintf('%s.%s', static::$_modelName, $key); | |
| } | |
| /** | |
| * Get the next recurring date for an entry or list of entries | |
| * | |
| * This method will only expand forward in time, and only find the next recurring date | |
| * | |
| * The list must be bare, not containing any model alias key | |
| * | |
| * @param array $entries | |
| * @return array | |
| */ | |
| public static function getNextDate($entries, $model = null, $skipBefore = null) { | |
| // Make sure to set the internal prefix key | |
| static::setModelName($model); | |
| // Detect if its a collection or single entry | |
| $singleEntry = !isset($entries[0]); | |
| // Normalize data a bit | |
| if ($singleEntry) { | |
| $entries = array($entries); | |
| } | |
| if (empty($skipBefore)) { | |
| $skipBefore = new CalendarDate(); | |
| } | |
| $result = static::calculateRecurrence($entries, array('limit' => 1, 'skipBefore' => $skipBefore)); | |
| // If we didn't expand any recurring events, return the original data | |
| if (empty($result)) { | |
| $result = $entries; | |
| } | |
| // Single entries should be returned as just one entry too | |
| if ($singleEntry) { | |
| return $result[0]; | |
| } | |
| // Return the full collection | |
| return $result; | |
| } | |
| /** | |
| * Calculate recurrence based on a list of inputs | |
| * | |
| * Settings can be the following: | |
| * - limit: The max number of expansions to do on each entry (Must be integer) - default is 10 expansions | |
| * - skipBefore: Skip all expansions before this date (Must be a CalendarDate object) | |
| * - dateInterval: Number of "time" to skip for each expansion test (Must be a DateInterval object) - default is 1 day | |
| * | |
| * @param array $entries A list of recurring entries that should be expanded | |
| * @param array $settings A list of extra configurations for the calculations | |
| * @return array The expanded list | |
| */ | |
| public static function calculateRecurrence($entries, $settings = array()) { | |
| $defaults = array( | |
| 'limit' => 10, | |
| 'skipBefore' => null, | |
| 'dateInterval' => new DateInterval('P1D'), | |
| ); | |
| $settings = array_merge($defaults, $settings); | |
| $return = array(); | |
| foreach ($entries as $key => $entry) { | |
| // Skip entries that aren't recurrent | |
| if (!Hash::get($entry, static::prefixKey('is_recurrent'))) { | |
| $return[] = $entry; | |
| continue; | |
| } | |
| $start = new CalendarDate(Hash::get($entry, static::prefixKey('start_time'))); | |
| $stop = new CalendarDate(Hash::get($entry, static::prefixKey('end_time'))); | |
| // If we are going to skip longer forward than we can iterate, might as well just skip now | |
| if (isset($settings['skipBefore']) && $settings['skipBefore'] > $stop) { | |
| $return[] = $entry; | |
| continue; | |
| } | |
| $expansions = 0; | |
| for ($date = $start; $date <= $stop; $date->add($settings['dateInterval'])) { | |
| if ($settings['skipBefore'] && $date < $settings['skipBefore']) { | |
| continue; | |
| } | |
| if (!static::ruleIsTrue($entry, $date)) { | |
| continue; | |
| } | |
| $expansions++; | |
| $return[] = static::renderEventForDate($entry, clone $date); | |
| if (is_numeric($settings['limit']) && $expansions >= $settings['limit']) { | |
| break; | |
| } | |
| } | |
| } | |
| return $return; | |
| } | |
| /** | |
| * Renders a recurring entry for the given date. Does not check to make sure the | |
| * event occurs on this date (use RecurrenceRule::ruleIsTrue for that). | |
| * | |
| * @param array $entry Contains data formatted the same as a retrieved entry from a find. | |
| * @param DateTime $date A DateTime object in UTC time. This is the date to render the event for. | |
| * @return array The instance rendered for this recurring event | |
| */ | |
| public static function renderEventForDate($entry, DateTime $date) { | |
| $start_date = new CalendarDate(Hash::get($entry, static::prefixKey('start_time'))); | |
| $end_date = new CalendarDate(Hash::get($entry, static::prefixKey('end_time'))); | |
| $interval = $start_date->diff($end_date); | |
| $interval = new DateInterval("PT{$interval->h}H{$interval->i}M"); | |
| $floating_start_hour = $start_date->format('H'); | |
| $date->setTime($floating_start_hour, $date->format('i'), $date->format('s')); | |
| $entry = Hash::insert($entry, static::prefixKey('start_time'), $date->format()); | |
| $date->add($interval); | |
| $entry = Hash::insert($entry, static::prefixKey('end_time'), $date->format()); | |
| return $entry; | |
| } | |
| /** | |
| * Test if a expansion rule is true | |
| * | |
| * It will check the frequency key and check if should be recurring on the date provided as 2nd argument | |
| * | |
| * @param array $entry | |
| * @param DateTime $date | |
| * @return boolean | |
| */ | |
| public static function ruleIsTrue($entry, DateTime $date) { | |
| $entryStart = new CalendarDate(Hash::get($entry, static::prefixKey('start_time'))); | |
| $frequency = Hash::get($entry, static::prefixKey('frequency')); | |
| if ($frequency == 'weekly') { | |
| $dayOfWeek = strtolower($entryStart->format('l')); | |
| return $dayOfWeek == strtolower($date->format('l')); | |
| } | |
| if ($frequency === 'daily') { | |
| return true; | |
| } | |
| return false; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| App::uses('RecurringSupport', 'Lib'); | |
| class RecurringSupportTest extends CakeTestCase { | |
| public function setUp() { | |
| parent::setUp(); | |
| RecurringSupport::setModelName(null); | |
| } | |
| public function testSetModelName() { | |
| $this->assertSame(RecurringSupport::prefixKey('id'), 'id'); | |
| RecurringSupport::setModelName('Model'); | |
| $this->assertSame(RecurringSupport::prefixKey('id'), 'Model.id'); | |
| RecurringSupport::setModelName(false); | |
| $this->assertSame(RecurringSupport::prefixKey('id'), 'id'); | |
| RecurringSupport::setModelName(null); | |
| $this->assertSame(RecurringSupport::prefixKey('id'), 'id'); | |
| RecurringSupport::setModelName('Hello.World'); | |
| $this->assertSame(RecurringSupport::prefixKey('id'), 'Hello.World.id'); | |
| } | |
| /** | |
| * Test if calculations works without a prefix | |
| * | |
| * @return void | |
| */ | |
| public function testGetNextDateWithoutPrefix() { | |
| $data = array( | |
| 'start_time' => '2012-08-08 19:00:00', | |
| 'end_time' => '2012-12-08 20:00:00', | |
| 'frequency' => 'daily', | |
| 'is_recurrent' => true | |
| ); | |
| // If the date is perfect match, then return the current | |
| $result = RecurringSupport::getNextDate($data, null, new CalendarDate('2012-08-08 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result['end_time']); | |
| // If we are just one second after, show the next start time | |
| $result = RecurringSupport::getNextDate($data, null, new CalendarDate('2012-08-08 19:00:01')); | |
| $this->assertSame('2012-08-09 19:00:00', $result['start_time']); | |
| $this->assertSame('2012-08-09 20:00:00', $result['end_time']); | |
| // If we go way back in time, we should still end up with the first available time | |
| $result = RecurringSupport::getNextDate($data, null, new CalendarDate('2012-01-01 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result['end_time']); | |
| // If we go way forward in time, we just return the data already provided (this should perhaps throw an exception?) | |
| $result = RecurringSupport::getNextDate($data, null, new CalendarDate('2013-01-01 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result['start_time']); | |
| $this->assertSame('2012-12-08 20:00:00', $result['end_time']); | |
| } | |
| /** | |
| * Test if calculations works with a prefix | |
| * | |
| * @return void | |
| */ | |
| public function testGetNextDateWithPrefix() { | |
| $data = array( | |
| 'Model' => array( | |
| 'start_time' => '2012-08-08 19:00:00', | |
| 'end_time' => '2012-12-08 20:00:00', | |
| 'frequency' => 'daily', | |
| 'is_recurrent' => true | |
| ) | |
| ); | |
| // If the date is perfect match, then return the current | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2012-08-08 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result['Model']['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result['Model']['end_time']); | |
| // If we are just one second after, show the next start time | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2012-08-08 19:00:01')); | |
| $this->assertSame('2012-08-09 19:00:00', $result['Model']['start_time']); | |
| $this->assertSame('2012-08-09 20:00:00', $result['Model']['end_time']); | |
| // If we go way back in time, we should still end up with the first available time | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2012-01-01 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result['Model']['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result['Model']['end_time']); | |
| // If we go way forward in time, we just return the data already provided (this should perhaps throw an exception?) | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2013-01-01 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result['Model']['start_time']); | |
| $this->assertSame('2012-12-08 20:00:00', $result['Model']['end_time']); | |
| } | |
| /** | |
| * Test if calculations works with a prefix | |
| * | |
| * @return void | |
| */ | |
| public function testGetNextDateWithPrefixCollection() { | |
| $data = array( | |
| array( | |
| 'Model' => array( | |
| 'start_time' => '2012-08-08 19:00:00', | |
| 'end_time' => '2012-12-08 20:00:00', | |
| 'frequency' => 'daily', | |
| 'is_recurrent' => true, | |
| 'key' => 1 | |
| ) | |
| ), | |
| array( | |
| 'Model' => array( | |
| 'start_time' => '2012-07-01 19:00:00', | |
| 'end_time' => '2012-10-01 20:00:00', | |
| 'frequency' => 'daily', | |
| 'is_recurrent' => true, | |
| 'key' => 2 | |
| ) | |
| ) | |
| ); | |
| // If the date is perfect match, then return the current | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2012-08-08 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result[0]['Model']['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result[0]['Model']['end_time']); | |
| $this->assertSame('2012-08-08 19:00:00', $result[1]['Model']['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result[1]['Model']['end_time']); | |
| // If we are just one second after, show the next start time | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2012-08-08 19:00:01')); | |
| $this->assertSame('2012-08-09 19:00:00', $result[0]['Model']['start_time']); | |
| $this->assertSame('2012-08-09 20:00:00', $result[0]['Model']['end_time']); | |
| $this->assertSame('2012-08-09 19:00:00', $result[1]['Model']['start_time']); | |
| $this->assertSame('2012-08-09 20:00:00', $result[1]['Model']['end_time']); | |
| // If we go way back in time, we should still end up with the first available time | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2012-01-01 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result[0]['Model']['start_time']); | |
| $this->assertSame('2012-08-08 20:00:00', $result[0]['Model']['end_time']); | |
| $this->assertSame('2012-07-01 19:00:00', $result[1]['Model']['start_time']); | |
| $this->assertSame('2012-07-01 20:00:00', $result[1]['Model']['end_time']); | |
| // If we go way forward in time, we just return the data already provided (this should perhaps throw an exception?) | |
| $result = RecurringSupport::getNextDate($data, 'Model', new CalendarDate('2013-01-01 19:00:00')); | |
| $this->assertSame('2012-08-08 19:00:00', $result[0]['Model']['start_time']); | |
| $this->assertSame('2012-12-08 20:00:00', $result[0]['Model']['end_time']); | |
| $this->assertSame('2012-07-01 19:00:00', $result[1]['Model']['start_time']); | |
| $this->assertSame('2012-10-01 20:00:00', $result[1]['Model']['end_time']); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment