Skip to content

Instantly share code, notes, and snippets.

@jippi
Created August 16, 2012 17:48
Show Gist options
  • Select an option

  • Save jippi/3372050 to your computer and use it in GitHub Desktop.

Select an option

Save jippi/3372050 to your computer and use it in GitHub Desktop.
<?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;
}
}
<?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