From 8b5be46c252920f304d8b23e4f0c00db27485bf8 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 02:57:37 +0200 Subject: [PATCH 01/44] use PHP 5.2+ DateTimeZone objects if available to get active daylight state Signed-off-by: Tanguy Pruvot --- blocks/SG_iCal_VTimeZone.php | 37 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/blocks/SG_iCal_VTimeZone.php b/blocks/SG_iCal_VTimeZone.php index 506f90a..77af852 100755 --- a/blocks/SG_iCal_VTimeZone.php +++ b/blocks/SG_iCal_VTimeZone.php @@ -65,20 +65,31 @@ class SG_iCal_VTimeZone { * @return string standard|daylight */ private function getActive( $ts ) { - if( isset($this->cache[$ts]) ) { + + if (class_exists('DateTimeZone')) { + + //PHP >= 5.2 + $tz = new DateTimeZone( $this->tzid ); + $date = new DateTime("@$ts", $tz); + return ($date->format('%I') == 1) ? 'daylight' : 'standard'; + + } else { + + if( isset($this->cache[$ts]) ) { + return $this->cache[$ts]; + } + + $daylight_freq = new SG_iCal_Freq($this->daylight['rrule'], strtotime($this->daylight['dtstart'])); + $standard_freq = new SG_iCal_Freq($this->standard['rrule'], strtotime($this->standard['dtstart'])); + $last_standard = $standard_freq->previousOccurrence($ts); + $last_dst = $daylight_freq->previousOccurrence($ts); + if( $last_dst > $last_standard ) { + $this->cache[$ts] = 'daylight'; + } else { + $this->cache[$ts] = 'standard'; + } + return $this->cache[$ts]; } - - $daylight_freq = new SG_iCal_Freq($this->daylight['rrule'], strtotime($this->daylight['dtstart'])); - $standard_freq = new SG_iCal_Freq($this->standard['rrule'], strtotime($this->standard['dtstart'])); - $last_standard = $standard_freq->previousOccurrence($ts); - $last_dst = $daylight_freq->previousOccurrence($ts); - if( $last_dst > $last_standard ) { - $this->cache[$ts] = 'daylight'; - } else { - $this->cache[$ts] = 'standard'; - } - - return $this->cache[$ts]; } } From 760418f6b47966f61bb4192bf195e66ad49abf82 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 03:03:19 +0200 Subject: [PATCH 02/44] add missing requires --- SG_iCal.php | 3 +++ helpers/SG_iCal_Freq.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/SG_iCal.php b/SG_iCal.php index e8ea2b9..6cd22ff 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -34,6 +34,9 @@ class SG_iCal { require_once dirname(__FILE__) . '/helpers/SG_iCal_Factory.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Line.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Query.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Recurrence.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Freq.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Duration.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Parser.php'; // BUILD: Remove line if( $url !== false ) { diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 8558ed3..361d88a 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -137,7 +137,7 @@ class SG_iCal_Freq { * @param int $offset * @return int */ - public function findNext($offset) { + public function findNext($offset) { $echo = false; //make sure the offset is valid From f1ca86fc1c5de2903dc8931c0d1931e9ec0ba8eb Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 03:05:17 +0200 Subject: [PATCH 03/44] fix to build Recurrence --- helpers/SG_iCal_Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/SG_iCal_Parser.php b/helpers/SG_iCal_Parser.php index bf295d9..ca02896 100755 --- a/helpers/SG_iCal_Parser.php +++ b/helpers/SG_iCal_Parser.php @@ -1,4 +1,4 @@ - Date: Fri, 29 Oct 2010 03:07:06 +0200 Subject: [PATCH 04/44] build.cmd (need gnuwin32) --- build.cmd | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 build.cmd diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..32d7609 --- /dev/null +++ b/build.cmd @@ -0,0 +1,4 @@ +#!cmd.exe +cat SG_iCal.php | grep -v "BUILD: Remove line" > sgical.php +DIR /B /S helpers | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> sgical.php +DIR /B /S blocks | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> sgical.php From f28be0bc558f07a66d9fd0203b4a834e8f727372 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 03:26:02 +0200 Subject: [PATCH 05/44] fix warnings for optional regexp blocks --- helpers/SG_iCal_Duration.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers/SG_iCal_Duration.php b/helpers/SG_iCal_Duration.php index 4379218..4d051f2 100755 --- a/helpers/SG_iCal_Duration.php +++ b/helpers/SG_iCal_Duration.php @@ -22,7 +22,7 @@ class SG_iCal_Duration { public function __construct( $duration ) { if( $duration{0} == 'P' || (($duration{0} == '+' || $duration{0} == '-') && $duration{1} == 'P') ) { preg_match('/P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches); - $results = array('weeks'=>(int)$matches[2], + $results = @ array('weeks'=>(int)$matches[2], 'days'=>(int)$matches[4], 'hours'=>(int)$matches[7], 'minutes'=>(int)$matches[9], @@ -33,8 +33,8 @@ class SG_iCal_Duration { $ts += 60 * $results['minutes']; $ts += 60 * 60 * $results['hours']; $ts += 24 * 60 * 60 * $results['days']; - $ts += 7 * 24 * 60 * 60 * $results['weeks']; - + $ts += 7 * 24 * 60 * 60 * $results['weeks']; + $dir = ($duration{0} == '-') ? -1 : 1; $this->dur = $dir * $ts; From 5b5012f71db015cadcc166286ffea15bb5b0bae8 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 03:54:57 +0200 Subject: [PATCH 06/44] fix remaining $evs --- helpers/SG_iCal_Query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/SG_iCal_Query.php b/helpers/SG_iCal_Query.php index 8889150..5ab26ca 100755 --- a/helpers/SG_iCal_Query.php +++ b/helpers/SG_iCal_Query.php @@ -22,12 +22,12 @@ class SG_iCal_Query { if( $ical instanceof SG_iCalReader ) { $ical = $ical->getEvents(); } - if( !is_array($evs) ) { + if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::Between called with invalid input!'); } $rtn = array(); - foreach( $evs AS $e ) { + foreach( $ical AS $e ) { if( ($start <= $e->getStart() && $e->getStart() < $end) || ($start < $e->getEnd() && $e->getEnd() <= $end) ) { $rtn[] = $e; From 3181917e2c119ee9d294d0f7b75fca5cea5d993f Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 04:49:18 +0200 Subject: [PATCH 07/44] variable for build output --- build.cmd | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build.cmd b/build.cmd index 32d7609..dfce940 100644 --- a/build.cmd +++ b/build.cmd @@ -1,4 +1,5 @@ -#!cmd.exe -cat SG_iCal.php | grep -v "BUILD: Remove line" > sgical.php -DIR /B /S helpers | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> sgical.php -DIR /B /S blocks | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> sgical.php +@SET OUTPUT=.\sgical.php + +cat SG_iCal.php | grep -v "BUILD: Remove line" > %OUTPUT% +DIR /B /S helpers | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> %OUTPUT% +DIR /B /S blocks | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> %OUTPUT% From 107781fe43dd8330a5a07b5f8e5fbb8898be9655 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 05:02:17 +0200 Subject: [PATCH 08/44] fix recurrent events Between() query, could break $evt->getEnd() but not in these classes --- blocks/SG_iCal_VEvent.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index d7f68b3..8959fb0 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -18,6 +18,7 @@ class SG_iCal_VEvent { private $uid; private $start; private $end; + private $lastend; private $recurrence; private $summary; private $description; @@ -52,7 +53,10 @@ class SG_iCal_VEvent { $dur = new SG_iCal_Duration( $data['duration']->getData() ); $this->end = $this->start + $dur->getDuration(); unset($data['duration']); - } elseif ( isset($this->recurrence) ) { + } + + //google cal set dtend as end of initial event + if ( isset($this->recurrence) ) { //if there is a recurrence rule $until = $this->recurrence->getUntil(); $count = $this->recurrence->getCount(); @@ -60,7 +64,7 @@ class SG_iCal_VEvent { if ( $this->recurrence->getUntil() or $this->recurrence->getCount() ) { //if until is set, set that as the end date (using getTimeStamp) if ( $until ) { - $this->end = strtotime( $until ); + $this->lastend = strtotime( $until ); } //if count is set, then figure out the last occurrence and set that as the end date } @@ -143,7 +147,7 @@ class SG_iCal_VEvent { * @return int */ public function getEnd() { - return $this->end; + return max($this->end,$this->lastend); } /** From 67bf66c9106e2e98dbdf1edf3c938473f9c97964 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 05:17:55 +0200 Subject: [PATCH 09/44] i hate private $vars :p replaced by protected --- SG_iCal.php | 8 ++++---- blocks/SG_iCal_VCalendar.php | 2 +- blocks/SG_iCal_VEvent.php | 18 +++++++++--------- blocks/SG_iCal_VTimeZone.php | 8 ++++---- helpers/SG_iCal_Duration.php | 2 +- helpers/SG_iCal_Freq.php | 12 ++++++------ helpers/SG_iCal_Line.php | 8 ++++---- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/SG_iCal.php b/SG_iCal.php index 6cd22ff..f00538a 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -1,6 +1,6 @@ 'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday'); - private $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'); - private $simpleMode = true; + protected $weekdays = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday'); + protected $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'); + protected $simpleMode = true; - private $rules = array('freq'=>'yearly', 'interval'=>1); - private $start = 0; - private $freq = ''; + protected $rules = array('freq'=>'yearly', 'interval'=>1); + protected $start = 0; + protected $freq = ''; /** * Constructs a new Freqency-rule diff --git a/helpers/SG_iCal_Line.php b/helpers/SG_iCal_Line.php index 2a734dc..10aaa1b 100755 --- a/helpers/SG_iCal_Line.php +++ b/helpers/SG_iCal_Line.php @@ -15,11 +15,11 @@ * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { - private $ident; - private $data; - private $params = array(); + protected $ident; + protected $data; + protected $params = array(); - private $replacements = array('from'=>array('\\,', '\\n', '\\;', '\\:', '\\"'), 'to'=>array(',', "\n", ';', ':', '"')); + protected $replacements = array('from'=>array('\\,', '\\n', '\\;', '\\:', '\\"'), 'to'=>array(',', "\n", ';', ':', '"')); /** * Constructs a new line. From d7bc4dfbca48dd41d430e1aeea8e0cc0be1e2b10 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 06:17:22 +0200 Subject: [PATCH 10/44] ignore built file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73332e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +/sgical.php \ No newline at end of file From ab7336d9d0ca582fe43135515fb5058b980e7a55 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 06:21:12 +0200 Subject: [PATCH 11/44] - new method getRangeEnd() to get end of recurrence of event, fixed Between() and After() - recurrence object and data array members are now public --- blocks/SG_iCal_VEvent.php | 50 ++++++++++++++++++++++++++++++--------- helpers/SG_iCal_Query.php | 4 ++-- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index 6148432..c2c8841 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -15,15 +15,18 @@ */ class SG_iCal_VEvent { const DEFAULT_CONFIRMED = true; + protected $uid; protected $start; protected $end; + protected $laststart; protected $lastend; - protected $recurrence; protected $summary; protected $description; protected $location; - protected $data; + + public $recurrence; + public $data; /** * Constructs a new SG_iCal_VEvent. Needs the SG_iCalReader @@ -31,7 +34,7 @@ class SG_iCal_VEvent { * @param SG_iCal_Line[] $data * @param SG_iCalReader $ical */ - public function __construct($data, SG_iCal $ical ) { + public function __construct($data, SG_iCal $ical) { $this->uid = $data['uid']->getData(); unset($data['uid']); @@ -41,7 +44,7 @@ class SG_iCal_VEvent { } if( isset($data['dtstart']) ) { - $this->start = $this->getTimestamp( $data['dtstart'], $ical ); + $this->start = $this->getTimestamp($data['dtstart'], $ical); unset($data['dtstart']); } @@ -49,7 +52,6 @@ class SG_iCal_VEvent { $this->end = $this->getTimestamp($data['dtend'], $ical); unset($data['dtend']); } elseif( isset($data['duration']) ) { - require_once dirname(__FILE__).'/../helpers/SG_iCal_Duration.php'; // BUILD: Remove line $dur = new SG_iCal_Duration( $data['duration']->getData() ); $this->end = $this->start + $dur->getDuration(); unset($data['duration']); @@ -64,7 +66,9 @@ class SG_iCal_VEvent { if ( $this->recurrence->getUntil() or $this->recurrence->getCount() ) { //if until is set, set that as the end date (using getTimeStamp) if ( $until ) { - $this->lastend = strtotime( $until ); + //date_default_timezone_set( xx ); + $this->laststart = strtotime($until); + $this->lastend = $this->laststart + $this->getDuration(); } //if count is set, then figure out the last occurrence and set that as the end date } @@ -147,6 +151,14 @@ class SG_iCal_VEvent { * @return int */ public function getEnd() { + return $this->end; + } + + /** + * Returns the timestamp for the end of the last event + * @return int + */ + public function getRangeEnd() { return max($this->end,$this->lastend); } @@ -179,11 +191,27 @@ class SG_iCal_VEvent { * @return int */ private function getTimestamp( SG_iCal_Line $line, SG_iCal $ical ) { - $ts = strtotime($line->getData()); - if( isset($line['tzid']) ) { - $tz = $ical->getTimeZoneInfo($line['tzid']); - $offset = $tz->getOffset($ts); - $ts = strtotime(date('D, d M Y H:i:s', $ts) . ' ' . $offset); + + if (class_exists('DateTimeZone')) { + + if( isset($line['tzid']) ) { + $tz = $ical->getTimeZoneInfo($line['tzid']); + $tz = new DateTimeZone( $tz->getTimeZoneId() ); + $date = new DateTime($line->getData(),$tz); + } else { + $date = new DateTime($line->getData()); + } + $ts = (int) $date->format('U'); + + } else { + + //Warning in PHP 5.2 Strict + $ts = strtotime($line->getData()); + if( isset($line['tzid']) ) { + $tz = $ical->getTimeZoneInfo($line['tzid']); + $offset = $tz->getOffset($ts); + $ts = strtotime(date('D, d M Y H:i:s', $ts) . ' ' . $offset); + } } return $ts; } diff --git a/helpers/SG_iCal_Query.php b/helpers/SG_iCal_Query.php index 5ab26ca..e9e0bac 100755 --- a/helpers/SG_iCal_Query.php +++ b/helpers/SG_iCal_Query.php @@ -29,7 +29,7 @@ class SG_iCal_Query { $rtn = array(); foreach( $ical AS $e ) { if( ($start <= $e->getStart() && $e->getStart() < $end) - || ($start < $e->getEnd() && $e->getEnd() <= $end) ) { + || ($start < $e->getRangeEnd() && $e->getRangeEnd() <= $end) ) { $rtn[] = $e; } } @@ -53,7 +53,7 @@ class SG_iCal_Query { $rtn = array(); foreach( $ical AS $e ) { - if( $start <= $e->getStart() ) { + if($e->getStart() >= $start || $e->getRangeEnd() >= $start) { $rtn[] = $e; } } From 43661c4d859c55a9f6de0e951a844446704439f8 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 06:52:19 +0200 Subject: [PATCH 12/44] comment header : added github url and fixed sample --- SG_iCal.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SG_iCal.php b/SG_iCal.php index f00538a..bf3c1ab 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -1,9 +1,10 @@ getEvents() As $event ) { * // Do stuff with the event $event * } From 9c3d8d9fec55be26a88e0ada688fb9d1bcf2d3cd Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 08:45:35 +0200 Subject: [PATCH 13/44] fix date format modifier --- blocks/SG_iCal_VTimeZone.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/SG_iCal_VTimeZone.php b/blocks/SG_iCal_VTimeZone.php index 71b7336..b60c9bf 100755 --- a/blocks/SG_iCal_VTimeZone.php +++ b/blocks/SG_iCal_VTimeZone.php @@ -71,7 +71,7 @@ class SG_iCal_VTimeZone { //PHP >= 5.2 $tz = new DateTimeZone( $this->tzid ); $date = new DateTime("@$ts", $tz); - return ($date->format('%I') == 1) ? 'daylight' : 'standard'; + return ($date->format('I') == 1) ? 'daylight' : 'standard'; } else { From 8ddf2c8b9ac8f2e0a80accb4bc98e95133462656 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 08:46:03 +0200 Subject: [PATCH 14/44] change order of requires --- SG_iCal.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SG_iCal.php b/SG_iCal.php index bf3c1ab..fa77dc0 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -32,13 +32,13 @@ class SG_iCal { * @param $url string */ public function __construct($url = false) { - require_once dirname(__FILE__) . '/helpers/SG_iCal_Factory.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Line.php'; // BUILD: Remove line - require_once dirname(__FILE__) . '/helpers/SG_iCal_Query.php'; // BUILD: Remove line - require_once dirname(__FILE__) . '/helpers/SG_iCal_Recurrence.php'; // BUILD: Remove line - require_once dirname(__FILE__) . '/helpers/SG_iCal_Freq.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Duration.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Freq.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Recurrence.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Parser.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Query.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Factory.php'; // BUILD: Remove line if( $url !== false ) { SG_iCal_Parser::Parse($url, $this); From 0cd26e11a2b5c01769565a2b2ced5fdade34325f Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 09:09:20 +0200 Subject: [PATCH 15/44] public objects, they are protected themself --- SG_iCal.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SG_iCal.php b/SG_iCal.php index fa77dc0..dce65c1 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -23,9 +23,12 @@ define('SG_ICALREADER_VERSION', '0.5.1-tpruvot'); * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class SG_iCal { - protected $information; - protected $events; - protected $timezones; + + //objects + public $information; //SG_iCal_VCalendar + public $timezones; //SG_iCal_VTimeZone + + protected $events; //SG_iCal_VEvent[] /** * Constructs a new iCalReader. You can supply the url now, or later using setUrl @@ -40,9 +43,7 @@ class SG_iCal { require_once dirname(__FILE__) . '/helpers/SG_iCal_Query.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Factory.php'; // BUILD: Remove line - if( $url !== false ) { - SG_iCal_Parser::Parse($url, $this); - } + $this->setUrl($url); } /** From 911a6b6080a690e807f013bb375fdc2fe459a024 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 09:17:58 +0200 Subject: [PATCH 16/44] check duration regexp result and ignore only common warning --- helpers/SG_iCal_Duration.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/helpers/SG_iCal_Duration.php b/helpers/SG_iCal_Duration.php index ad0a30b..74b57ef 100755 --- a/helpers/SG_iCal_Duration.php +++ b/helpers/SG_iCal_Duration.php @@ -21,12 +21,16 @@ class SG_iCal_Duration { */ public function __construct( $duration ) { if( $duration{0} == 'P' || (($duration{0} == '+' || $duration{0} == '-') && $duration{1} == 'P') ) { - preg_match('/P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches); - $results = @ array('weeks'=>(int)$matches[2], - 'days'=>(int)$matches[4], - 'hours'=>(int)$matches[7], - 'minutes'=>(int)$matches[9], - 'seconds'=>(int)$matches[11]); + + if (preg_match('/P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches) === 1) { + $results = array( + 'weeks'=> (int) $matches[2], + 'days'=> (int) $matches[4], + 'hours'=> (int)@ $matches[7], + 'minutes'=>(int)@ $matches[9], + 'seconds'=>(int)@ $matches[11] + ); //7-9-11 are optional, so ignore warnings if not found with @ + } $ts = 0; $ts += $results['seconds']; From 8719799e9eaa52472671b70f79802cd006089a6c Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 09:46:57 +0200 Subject: [PATCH 17/44] added test.sh for unit tests --- tests/test.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 tests/test.sh diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..5b0538a --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +# aptitude install phpunit + +phpunit AllTests.php From 233c4d9ffcb644f5f598580b7ba827243ce8dadd Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 09:57:09 +0200 Subject: [PATCH 18/44] update test, event getEnd() was modified --- tests/blocks/VEventTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/blocks/VEventTest.php b/tests/blocks/VEventTest.php index 64cee32..1a0f1b1 100755 --- a/tests/blocks/VEventTest.php +++ b/tests/blocks/VEventTest.php @@ -116,6 +116,6 @@ class VEventTest extends PHPUnit_Framework_TestCase { date_default_timezone_set('America/New_York'); $event = new SG_iCal_VEvent($data, $ical); - $this->assertEquals(strtotime('20091030T090000'), $event->getEnd()); + $this->assertEquals(strtotime('20091030T090000'), $event->getProperty('laststart')); } } From 3d4447fffbe1973a41a6b4b66e11ac8b35aebe40 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 09:59:59 +0200 Subject: [PATCH 19/44] fix duration parsing if regexp fails - added some Duration Unit Tests --- helpers/SG_iCal_Duration.php | 32 +++++++++++++++---------------- tests/helpers/AllTests.php | 2 ++ tests/helpers/DurationTest.php | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 17 deletions(-) create mode 100755 tests/helpers/DurationTest.php diff --git a/helpers/SG_iCal_Duration.php b/helpers/SG_iCal_Duration.php index 74b57ef..db10cd2 100755 --- a/helpers/SG_iCal_Duration.php +++ b/helpers/SG_iCal_Duration.php @@ -20,32 +20,30 @@ class SG_iCal_Duration { * @param $duration string */ public function __construct( $duration ) { - if( $duration{0} == 'P' || (($duration{0} == '+' || $duration{0} == '-') && $duration{1} == 'P') ) { + + $ts = 0; + + if (preg_match('/[\\+\\-]{0,1}P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches) === 1) { + $results = array( + 'weeks'=> (int)@ $matches[2], + 'days'=> (int)@ $matches[4], + 'hours'=> (int)@ $matches[7], + 'minutes'=>(int)@ $matches[9], + 'seconds'=>(int)@ $matches[11] + ); - if (preg_match('/P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches) === 1) { - $results = array( - 'weeks'=> (int) $matches[2], - 'days'=> (int) $matches[4], - 'hours'=> (int)@ $matches[7], - 'minutes'=>(int)@ $matches[9], - 'seconds'=>(int)@ $matches[11] - ); //7-9-11 are optional, so ignore warnings if not found with @ - } - - $ts = 0; $ts += $results['seconds']; $ts += 60 * $results['minutes']; $ts += 60 * 60 * $results['hours']; $ts += 24 * 60 * 60 * $results['days']; $ts += 7 * 24 * 60 * 60 * $results['weeks']; - - $dir = ($duration{0} == '-') ? -1 : 1; - - $this->dur = $dir * $ts; } else { // Invalid duration! - $this->dur = 0; } + + $dir = ($duration{0} == '-') ? -1 : 1; + + $this->dur = $dir * $ts; } /** diff --git a/tests/helpers/AllTests.php b/tests/helpers/AllTests.php index c33ed7b..b70eb30 100755 --- a/tests/helpers/AllTests.php +++ b/tests/helpers/AllTests.php @@ -2,6 +2,7 @@ require_once 'PHPUnit/Framework.php'; require_once dirname(__FILE__).'/FreqTest.php'; require_once dirname(__FILE__).'/RecurrenceTest.php'; +require_once dirname(__FILE__).'/DurationTest.php'; class Helpers_AllTests { @@ -9,6 +10,7 @@ class Helpers_AllTests { $suite = new PHPUnit_Framework_TestSuite('Helpers'); $suite->addTestSuite('FreqTest'); $suite->addTestSuite('RecurrenceTest'); + $suite->addTestSuite('DurationTest'); return $suite; } diff --git a/tests/helpers/DurationTest.php b/tests/helpers/DurationTest.php new file mode 100755 index 0000000..5b4a3cf --- /dev/null +++ b/tests/helpers/DurationTest.php @@ -0,0 +1,35 @@ +secsMin = 60; + $this->secsHour = 60 * 60; + $this->secsDay = 24 * 60 * 60; + $this->secsWeek = 7 * 24 * 60 * 60; + } + + public function testDurationDateTime() { + + //A duration of 10 days, 6 hours and 20 seconds + $dur = new SG_iCal_Duration('P10DT6H0M20S'); + $this->assertEquals($this->secsDay*10 + $this->secsHour*6 + 20, $dur->getDuration() ); + } + + public function testDurationWeek() { + + //A duration of 2 weeks + $dur = new SG_iCal_Duration('P2W'); + $this->assertEquals($this->secsWeek * 2, $dur->getDuration() ); + } + + public function testDurationNegative() { + + //A duration of -1 day + $dur = new SG_iCal_Duration('-P1D'); + $this->assertEquals(-1 * $this->secsDay, $dur->getDuration() ); + } + +} From d36357770c515d3383822aecb959f48347908931 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 12:13:16 +0200 Subject: [PATCH 20/44] keep full string to parse Freq --- helpers/SG_iCal_Recurrence.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/helpers/SG_iCal_Recurrence.php b/helpers/SG_iCal_Recurrence.php index 3294c84..fc22898 100755 --- a/helpers/SG_iCal_Recurrence.php +++ b/helpers/SG_iCal_Recurrence.php @@ -14,6 +14,8 @@ */ class SG_iCal_Recurrence { + public $rrule; + protected $freq; protected $until; @@ -59,6 +61,7 @@ class SG_iCal_Recurrence { //split up the properties $recurProperties = explode(';', $line); $recur = array(); + $this->rrule = $line; //loop through the properties in the line and set their associated //member variables From bd10ff1266689b85ce371f7f0ab741d91addf42c Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 12:58:19 +0200 Subject: [PATCH 21/44] add setUntil() method --- helpers/SG_iCal_Recurrence.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/helpers/SG_iCal_Recurrence.php b/helpers/SG_iCal_Recurrence.php index fc22898..bcc05ac 100755 --- a/helpers/SG_iCal_Recurrence.php +++ b/helpers/SG_iCal_Recurrence.php @@ -80,6 +80,18 @@ class SG_iCal_Recurrence { } } + /** + * Set the $until member + * @param mixed timestamp (int) / Valid DateTime format (string) + */ + public function setUntil($ts) { + if ( is_int($ts) ) + $dt = new DateTime('@'.$ts); + else + $dt = new DateTime($ts); + $this->until = $dt->format('Ymd\THisO'); + } + /** * Retrieves the desired member variable and returns it (if it's set) * @param string $member name of the member variable @@ -106,7 +118,6 @@ class SG_iCal_Recurrence { * @return mixed string if the member has been set, false otherwise */ public function getUntil() { - return $this->getMember('until'); } From 5b81d5e906bf227f36d61584de2987d3c991f4ef Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 13:36:34 +0200 Subject: [PATCH 22/44] fix nextOccurrence() loop --- helpers/SG_iCal_Freq.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 561271b..9cfcac1 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -97,7 +97,8 @@ class SG_iCal_Freq { * @return int */ public function nextOccurrence( $offset ) { - return $this->findNext( $this->previousOccurrence( $offset) ); + //return $this->findNext( $this->previousOccurrence( $offset) ); + return $this->findNext($offset); } /** From 93a165fb82ab35b2e4edb6a3dc3d3c7602504096 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 13:37:42 +0200 Subject: [PATCH 23/44] set until on unlimited repeats, to be included in Between() --- blocks/SG_iCal_VEvent.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index c2c8841..f13f9f0 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -63,7 +63,7 @@ class SG_iCal_VEvent { $until = $this->recurrence->getUntil(); $count = $this->recurrence->getCount(); //check if there is either 'until' or 'count' set - if ( $this->recurrence->getUntil() or $this->recurrence->getCount() ) { + if ( $until or $count ) { //if until is set, set that as the end date (using getTimeStamp) if ( $until ) { //date_default_timezone_set( xx ); @@ -71,8 +71,13 @@ class SG_iCal_VEvent { $this->lastend = $this->laststart + $this->getDuration(); } //if count is set, then figure out the last occurrence and set that as the end date + } else { + //forever... limit to 3 years + $this->recurrence->setUntil('+3 years'); + $until = $this->recurrence->getUntil(); + $this->laststart = strtotime($until); + $this->lastend = $this->laststart + $this->getDuration(); } - } $imports = array('summary','description','location'); From 90db32ce7f36950d9419888e58bafd9f0fccec17 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 13:43:13 +0200 Subject: [PATCH 24/44] Fullcalendar demo with a sample google iCal file --- demo/basic.ics | 107 + demo/fullcalendar.css | 615 ++++++ demo/fullcalendar.js | 4765 +++++++++++++++++++++++++++++++++++++++++ demo/index.php | 119 + 4 files changed, 5606 insertions(+) create mode 100644 demo/basic.ics create mode 100644 demo/fullcalendar.css create mode 100644 demo/fullcalendar.js create mode 100644 demo/index.php diff --git a/demo/basic.ics b/demo/basic.ics new file mode 100644 index 0000000..b63f591 --- /dev/null +++ b/demo/basic.ics @@ -0,0 +1,107 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Particuliers - Grand Tour du Bassin +X-WR-TIMEZONE:Europe/Paris +X-WR-CALDESC: +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20100526T143000 +DTEND;TZID=Europe/Paris:20100526T171500 +DTSTAMP:20101028T215738Z +UID:ptb5jqomu2vu0sr2nm24qungag@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Partic + uliers - Grand Tour du Bassin;X-NUM-GUESTS=0:mailto:l2n7ajiud0oaiua4qcdarg1 + krg@group.calendar.google.com +RECURRENCE-ID;TZID=Europe/Paris:20100526T143000 +CREATED:20100428T225030Z +DESCRIPTION: +LAST-MODIFIED:20100527T102538Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20100403T143000 +DTEND;TZID=Europe/Paris:20100403T171500 +RRULE:FREQ=WEEKLY;WKST=MO;UNTIL=20100630T123000Z;BYDAY=SU,WE,SA +DTSTAMP:20101028T215738Z +UID:ptb5jqomu2vu0sr2nm24qungag@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Partic + uliers - Grand Tour du Bassin;X-NUM-GUESTS=0:mailto:l2n7ajiud0oaiua4qcdarg1 + krg@group.calendar.google.com +CREATED:20100428T225030Z +DESCRIPTION: +LAST-MODIFIED:20100518T052328Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20101002T143000 +DTEND;TZID=Europe/Paris:20101002T171500 +RRULE:FREQ=WEEKLY;BYDAY=SU,SA;WKST=MO +DTSTAMP:20101028T215738Z +UID:41v060qrjvpvsv90n6ulljjeg4@google.com +CREATED:20100428T225427Z +DESCRIPTION: +LAST-MODIFIED:20100518T052328Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20100703T143000 +DTEND;TZID=Europe/Paris:20100703T171500 +RRULE:FREQ=DAILY;UNTIL=20100930T123000Z;WKST=MO +DTSTAMP:20101028T215738Z +UID:92tmcm95ktr40ofn3okf0hhr2c@google.com +CREATED:20100428T225334Z +DESCRIPTION: +LAST-MODIFIED:20100518T052327Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART:20100513T123000Z +DTEND:20100513T151500Z +DTSTAMP:20101028T215738Z +UID:254jd6v58v92k7bj69sgu3ulb0@google.com +CREATED:20100428T225221Z +DESCRIPTION: +LAST-MODIFIED:20100518T052327Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/demo/fullcalendar.css b/demo/fullcalendar.css new file mode 100644 index 0000000..a101442 --- /dev/null +++ b/demo/fullcalendar.css @@ -0,0 +1,615 @@ +/* + * FullCalendar v1.4.8-IE9 Stylesheet + * + * Feel free to edit this file to customize the look of FullCalendar. + * When upgrading to newer versions, please upgrade this file as well, + * porting over any customizations afterwards. + * + * Date: Mon Oct 25 02:35:06 2010 +0200 + * + */ + + +/* TODO: make font sizes look the same in all doctypes */ + + +.fc, +.fc .fc-header, +.fc .fc-content { + font-size: 1em; + } + +.fc { + direction: ltr; + text-align: left; + } + +.fc table { + border-collapse: collapse; + border-spacing: 0; + } + +.fc td, .fc th { + padding: 0; + vertical-align: top; + } + + + +/* Header +------------------------------------------------------------------------*/ + +table.fc-header { + width: 100%; + } + +.fc-header-left { + width: 25%; + } + +.fc-header-left table { + float: left; + } + +.fc-header-center { + width: 50%; + text-align: center; + } + +.fc-header-center table { + margin: 0 auto; + } + +.fc-header-right { + width: 25%; + } + +.fc-header-right table { + float: right; + } + +.fc-header-title { + margin-top: 0; + white-space: nowrap; + } + +.fc-header-space { + padding-left: 10px; + } + +/* right-to-left */ + +.fc-rtl .fc-header-title { + direction: rtl; + } + + + +/* Buttons +------------------------------------------------------------------------*/ + +.fc-header .fc-state-default, +.fc-header .ui-state-default { + margin-bottom: 1em; + cursor: pointer; + } + +.fc-header .fc-state-default { + border-width: 1px 0; + padding: 0 1px; + } + +.fc-header .fc-state-default, +.fc-header .fc-state-default a { + border-style: solid; + } + +.fc-header .fc-state-default a { + display: block; + border-width: 0 1px; + margin: 0 -1px; + width: 100%; + text-decoration: none; + } + +.fc-header .fc-state-default span { + display: block; + border-style: solid; + border-width: 1px 0 1px 1px; + padding: 3px 5px; + } + +.fc-header .ui-state-default { + padding: 4px 6px; + } + +.fc-header .fc-state-default span, +.fc-header .ui-state-default span { + white-space: nowrap; + } + +/* for adjacent buttons */ + +.fc-header .fc-no-right { + padding-right: 0; + } + +.fc-header .fc-no-right a { + margin-right: 0; + border-right: 0; + } + +.fc-header .ui-no-right { + border-right: 0; + } + +/* for fake rounded corners */ + +.fc-header .fc-corner-left { + margin-left: 1px; + padding-left: 0; + } + +.fc-header .fc-corner-right { + margin-right: 1px; + padding-right: 0; + } + +/* DEFAULT button COLORS */ + +.fc-header .fc-state-default, +.fc-header .fc-state-default a { + border-color: #777; /* outer border */ + color: #333; + } + +.fc-header .fc-state-default span { + border-color: #fff #fff #d1d1d1; /* inner border */ + background: #e8e8e8; + } + +/* PRESSED button COLORS (down and active) */ + +.fc-header .fc-state-active a { + color: #fff; + } + +.fc-header .fc-state-down span, +.fc-header .fc-state-active span { + background: #888; + border-color: #808080 #808080 #909090; /* inner border */ + } + +/* DISABLED button COLORS */ + +.fc-header .fc-state-disabled a { + color: #999; + } + +.fc-header .fc-state-disabled, +.fc-header .fc-state-disabled a { + border-color: #ccc; /* outer border */ + } + +.fc-header .fc-state-disabled span { + border-color: #fff #fff #f0f0f0; /* inner border */ + background: #f0f0f0; + } + + + +/* Content Area & Global Cell Styles +------------------------------------------------------------------------*/ + +.fc-widget-content { + border: 1px solid #ccc; /* outer border color */ + } + +.fc-content { + clear: both; + } + +.fc-content .fc-state-default { + border-style: solid; + border-color: #ccc; /* inner border color */ + } + +.fc-content .ui-state-highlight, +.fc-content .fc-state-highlight { /* today */ + background: #ffd; + } + +.fc-content .fc-not-today { /* override jq-ui highlight (TODO: ui-widget-content) */ + background: none; + } + +.fc-content .fc-before-today { + background: #fafafa; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #9cf; + opacity: .2; + filter: alpha(opacity=20); /* for IE */ + } + +.fc-view { /* prevents dragging outside of widget */ + width: 100%; + overflow: hidden; + } + + + + + +/* Global Event Styles +------------------------------------------------------------------------*/ + +.fc-event, +.fc-agenda .fc-event-time, +.fc-event a { + border-style: solid; + border-color: #36c; /* default BORDER color (probably the same as background-color) */ + background-color: #36c; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + } + + /* Use the 'className' CalEvent property and the following + * example CSS to change event color on a per-event basis: + * + * .myclass, + * .fc-agenda .myclass .fc-event-time, + * .myclass a { + * background-color: black; + * border-color: black; + * color: red; + * } + */ + +.fc-event { + text-align: left; + } + +.fc-event a { + overflow: hidden; + font-size: .85em; + text-decoration: none; + cursor: pointer; + } + +.fc-event-editable { + cursor: pointer; + } + +.fc-event-time, +.fc-event-title { + padding: 0 1px; + } + +/* for fake rounded corners */ + +.fc-event a { + display: block; + position: relative; + width: 100%; + height: 100%; + } + +/* right-to-left */ + +.fc-rtl .fc-event a { + text-align: right; + } + +/* resizable */ + +.fc .ui-resizable-handle { + display: block; + position: absolute; + z-index: 99999; + border: 0 !important; /* important overrides pre jquery ui 1.7 styles */ + background: url(data:image/gif;base64,AAAA) !important; /* hover fix for IE */ + } + + + +/* Horizontal Events +------------------------------------------------------------------------*/ + +.fc-event-hori { + border-width: 1px 0; + margin-bottom: 1px; + } + +.fc-event-hori a { + border-width: 0; + } + +/* for fake rounded corners */ + +.fc-content .fc-corner-left { + margin-left: 1px; + } + +.fc-content .fc-corner-left a { + margin-left: -1px; + border-left-width: 1px; + } + +.fc-content .fc-corner-right { + margin-right: 1px; + } + +.fc-content .fc-corner-right a { + margin-right: -1px; + border-right-width: 1px; + } + +/* resizable */ + +.fc-event-hori .ui-resizable-e { + top: 0 !important; /* importants override pre jquery ui 1.7 styles */ + right: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: e-resize; + } + +.fc-event-hori .ui-resizable-w { + top: 0 !important; + left: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: w-resize; + } + +.fc-event-hori .ui-resizable-handle { + _padding-bottom: 14px; /* IE6 had 0 height */ + } + + + + +/* Month View, Basic Week View, Basic Day View +------------------------------------------------------------------------*/ + +.fc-grid table { + width: 100%; + } + +.fc .fc-grid th { + border-width: 0 0 0 1px; + text-align: center; + } + +.fc .fc-grid td { + border-width: 1px 0 0 1px; + } + +.fc-grid th.fc-leftmost, +.fc-grid td.fc-leftmost { + border-left: 0; + } + +.fc-grid .fc-day-number { + float: right; + padding: 0 2px; + } + +.fc-grid .fc-other-month .fc-day-number { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ + } + +.fc-grid .fc-day-content { + clear: both; + padding: 2px 2px 0; /* distance between events and day edges */ + } + +/* event styles */ + +.fc-grid .fc-event-time { + font-weight: bold; + } + +/* right-to-left */ + +.fc-rtl .fc-grid { + direction: rtl; + } + +.fc-rtl .fc-grid .fc-day-number { + float: left; + } + +.fc-rtl .fc-grid .fc-event-time { + float: right; + } + +/* week numbers */ + +.fc .fc-grid th.fc-weeknumber { + border-top-width: 1px; +} +/* Agenda Week View, Agenda Day View +------------------------------------------------------------------------*/ + +.fc .fc-agenda th, +.fc .fc-agenda td { + border-width: 1px 0 0 1px; + } + +.fc .fc-agenda .fc-leftmost { + border-left: 0; + } + +.fc-agenda tr.fc-first th, +.fc-agenda tr.fc-first td { + border-top: 0; + } + +.fc-agenda-head tr.fc-last th { + border-bottom-width: 1px; + } + +.fc .fc-agenda-head td, +.fc .fc-agenda-body td { + background: none; + } + +.fc-agenda-head th { + text-align: center; + } + +/* the time axis running down the left side */ + +.fc-agenda .fc-axis { + width: 50px; + padding: 0 4px; + vertical-align: middle; + white-space: nowrap; + text-align: right; + font-weight: normal; + } + +/* all-day event cells at top */ + +.fc-agenda-head tr.fc-all-day th { + height: 35px; + } + +.fc-agenda-head td { + padding-bottom: 10px; + } + +.fc .fc-divider div { + font-size: 1px; /* for IE6/7 */ + height: 2px; + } + +.fc .fc-divider .fc-state-default { + background: #eee; /* color for divider between all-day and time-slot events */ + } + +/* body styles */ + +.fc .fc-agenda-body td div { + height: 20px; /* slot height */ + } + +.fc .fc-agenda-body tr.fc-minor th, +.fc .fc-agenda-body tr.fc-minor td { + border-top-style: dotted; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 0; /* distance between events and day edges */ + } + +/* vertical background columns */ + +.fc .fc-agenda-bg .ui-state-highlight { + background-image: none; /* tall column, don't want repeating background image */ + } + + + +/* Vertical Events +------------------------------------------------------------------------*/ + +.fc-event-vert { + border-width: 0 1px; + } + +.fc-event-vert a { + border-width: 0; + } + +/* for fake rounded corners */ + +.fc-content .fc-corner-top { + margin-top: 1px; + } + +.fc-content .fc-corner-top a { + margin-top: -1px; + border-top-width: 1px; + } + +.fc-content .fc-corner-bottom { + margin-bottom: 1px; + } + +.fc-content .fc-corner-bottom a { + margin-bottom: -1px; + border-bottom-width: 1px; + } + +/* event content */ + +.fc-event-vert span { + display: block; + position: relative; + z-index: 2; + } + +.fc-event-vert span.fc-event-time { + white-space: nowrap; + _white-space: normal; + overflow: hidden; + border: 0; + font-size: 10px; + } + +.fc-event-vert span.fc-event-title { + line-height: 13px; + } + +.fc-event-vert span.fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ + } + +/* resizable */ + +.fc-event-vert .ui-resizable-s { + bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ + width: 100% !important; + height: 8px !important; + line-height: 8px !important; + font-size: 11px !important; + font-family: monospace; + text-align: center; + cursor: s-resize; + } + + + +/* JOMRES */ +.gcal-blackbooking-0, +.fc-agenda .gcal-blackbooking-0 .fc-event-time, +.gcal-blackbooking-0 a { + border-style: solid; + border-color: #400; /* default BORDER color (probably the same as background-color) */ + background-color: #D60; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ +} + +.gcal-blackbooking-1, +.fc-agenda .gcal-blackbooking-1 .fc-event-time, +.gcal-blackbooking-1 a { + border-style: solid; + border-color: #400; /* default BORDER color (probably the same as background-color) */ + background-color: #800; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ +} diff --git a/demo/fullcalendar.js b/demo/fullcalendar.js new file mode 100644 index 0000000..baa6d8f --- /dev/null +++ b/demo/fullcalendar.js @@ -0,0 +1,4765 @@ +/** + * @preserve + * FullCalendar v1.4.9-gcalendar + * http://arshaw.com/fullcalendar/ + * + * with some adaptations for joomla gcalendar component + * http://github.com/tpruvot/fullcalendar + * + * Use fullcalendar.css for basic styling. + * For event drag & drop, requires jQuery UI draggable. + * For event resizing, requires jQuery UI resizable. + * + * Copyright (c) 2010 Adam Shaw + * Dual licensed under the MIT and GPL licenses, located in + * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. + * + * Date: Wed Oct 27 01:12:13 2010 +0200 + * + */ + +(function($, undefined) { + + +var defaults = { + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + + // editing + //editable: false, + //disableDragging: false, + //disableResizing: false, + + allDayDefault: true, + ignoreTimezone: true, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + + // time formats + titleFormat: { + month: 'MMMM yyyy', + week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", + day: 'dddd, MMM d, yyyy' + }, + columnFormat: { + month: 'ddd', + week: 'ddd M/d', + day: 'dddd M/d' + }, + timeFormat: { // for event elements + '': 'h(:mm)t' // default + }, + + // locale + isRTL: false, + firstDay: 0, + monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + buttonText: { + prev: ' ◄ ', + next: ' ► ', + prevYear: ' << ', + nextYear: ' >> ', + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + // jquery-ui theming + theme: false, + buttonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e' + }, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*' + +}; + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonText: { + prev: ' ► ', + next: ' ◄ ', + prevYear: ' >> ', + nextYear: ' << ' + }, + buttonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w' + } +}; + + + +var fc = $.fullCalendar = { version: "1.4.9-gcalendar" }; +var fcViews = fc.views = {}; + + +//IE9 dnd fix (jQuery UI Mouse (<= 1.8.5) doesnt support IE9) +var msie9 = false; +if ($.ui && $.browser.msie && parseInt($.browser.version,10) >= 9) { + msie9 = true; + var mm=$.ui.mouse.prototype._mouseMove; + $.ui.mouse.prototype._mouseMove=function(b){b.button=1;mm.apply(this,[b]);} +} + + +$.fn.fullCalendar = function(options) { + + + // method calling + if (typeof options == 'string') { + var args = Array.prototype.slice.call(arguments, 1); + var res; + this.each(function() { + var calendar = $.data(this, 'fullCalendar'); + if (calendar && $.isFunction(calendar[options])) { + var r = calendar[options].apply(calendar, args); + if (res === undefined) { + res = r; + } + if (options == 'destroy') { + $.removeData(this, 'fullCalendar'); + } + } + }); + if (res !== undefined) { + return res; + } + return this; + } + + + // would like to have this logic in EventManager, but needs to happen before options are extended + var eventSources = options.eventSources || []; + delete options.eventSources; + if (options.events) { + eventSources.push(options.events); + delete options.events; + } + + + options = $.extend(true, {}, + defaults, + (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, + options + ); + + + this.each(function(i, _element) { + var element = $(_element); + var calendar = new Calendar(element, options, eventSources); + element.data('fullCalendar', calendar); // TODO: look into memory leak implications + calendar.render(); + }); + + + return this; + +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + $.extend(true, defaults, d); +} + + + + +function Calendar(element, options, eventSources) { + var t = this; + + + // exports + t.options = options; + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.changeView = changeView; + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.formatDate = function(format, date) { return formatDate(format, date, options) }; + t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; + t.getDate = getDate; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + // imports + EventManager.call(t, options, eventSources); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + + + // locals + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; + var viewInstances = {}; + var elementOuterWidth; + var suggestedViewHeight; + var absoluteViewElement; + var resizeUID = 0; + var ignoreWindowResize = 0; + var date = new Date(); + var events = []; + + + + /* Main Rendering + -----------------------------------------------------------------------------*/ + + + setYMD(date, options.year, options.month, options.date); + + + function render(inc) { + if (!content) { + initialRender(); + }else{ + calcSize(); + markSizesDirty(); + markEventsDirty(); + renderView(inc); + } + } + + + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); + if (options.isRTL) { + element.addClass('fc-rtl'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + content = $("
") + .prependTo(element); + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + changeView(options.defaultView); + $(window).resize(windowResize); + // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize + if (!bodyVisible()) { + lateRender(); + } + } + + + // called when we know the calendar couldn't be rendered when it was initialized, + // but we think it's ready now + function lateRender() { + setTimeout(function() { // IE7 needs this so dimensions are calculated correctly + if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once + renderView(); + } + },0); + } + + + function destroy() { + $(window).unbind('resize', windowResize); + header.destroy(); + content.remove(); + element.removeClass('fc fc-rtl fc-ui-widget'); + } + + + + function elementVisible() { + return _element.offsetWidth !== 0; + } + + + function bodyVisible() { + return $('body')[0].offsetWidth !== 0; + } + + + + /* View Rendering + -----------------------------------------------------------------------------*/ + + + function changeView(newViewName) { + if (!currentView || newViewName != currentView.name) { + ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached + + unselect(); + + var oldView = currentView; + var newViewElement; + + if (oldView) { + (oldView.beforeHide || noop)(); // called before changing min-height. if called after, scroll state is reset (in Opera) + setMinHeight(content, getHeight(content)); + oldView.element.hide(); + }else{ + setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated + } + content.css('overflow', 'hidden'); + + currentView = viewInstances[newViewName]; + if (currentView) { + currentView.element.show(); + }else{ + currentView = viewInstances[newViewName] = new fcViews[newViewName]( + newViewElement = absoluteViewElement = + $("
") + .appendTo(content), + t // the calendar object + ); + } + + if (oldView) { + header.deactivateButton(oldView.name); + } + header.activateButton(newViewName); + + renderView(); // after height has been set, will make absoluteViewElement's position=relative, then set to null + + content.css('overflow', ''); + if (oldView) { + setMinHeight(content, 1); + } + + if (!newViewElement) { + (currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera) + } + + ignoreWindowResize--; + } + } + + + + function renderView(inc) { + if (elementVisible()) { + ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached + + unselect(); + + if (suggestedViewHeight === undefined) { + calcSize(); + } + + var forceEventRender = false; + if (!currentView.start || inc || date < currentView.start || date >= currentView.end) { + // view must render an entire new date range (and refetch/render events) + currentView.render(date, inc || 0); // responsible for clearing events + setSize(true); + forceEventRender = true; + } + else if (currentView.sizeDirty) { + // view must resize (and rerender events) + currentView.clearEvents(); + setSize(); + forceEventRender = true; + } + else if (currentView.eventsDirty) { + currentView.clearEvents(); + forceEventRender = true; + } + currentView.sizeDirty = false; + currentView.eventsDirty = false; + updateEvents(forceEventRender); + + elementOuterWidth = element.outerWidth(); + + header.updateTitle(currentView.title); + var today = new Date(); + if (today >= currentView.start && today < currentView.end) { + header.disableButton('today'); + }else{ + header.enableButton('today'); + } + + ignoreWindowResize--; + currentView.trigger('viewDisplay', _element); + } + } + + + + /* Resizing + -----------------------------------------------------------------------------*/ + + + function updateSize() { + markSizesDirty(); + if (elementVisible()) { + calcSize(); + setSize(); + unselect(); + currentView.renderEvents(events); + currentView.sizeDirty = false; + } + } + + + function markSizesDirty() { + $.each(viewInstances, function(i, inst) { + inst.sizeDirty = true; + }); + } + + + function calcSize() { + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - vsides(content[0]) - (headerElement ? getHeight(headerElement) : 0); + } + else { + suggestedViewHeight = Math.round(getWidth(content) / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize(dateChanged) { // todo: dateChanged? + ignoreWindowResize++; + currentView.setHeight(suggestedViewHeight, dateChanged); + if (absoluteViewElement) { + absoluteViewElement.css('position', 'relative'); + absoluteViewElement = null; + } + currentView.setWidth(getWidth(content), dateChanged); + ignoreWindowResize--; + } + + + function windowResize() { + if (!ignoreWindowResize) { + if (currentView.start) { // view has already been rendered + var uid = ++resizeUID; + setTimeout(function() { // add a delay + if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { + if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { + ignoreWindowResize++; // in case the windowResize callback changes the height + updateSize(); + currentView.trigger('windowResize', _element); + ignoreWindowResize--; + } + } + }, 200); + }else{ + // calendar must have been initialized in a 0x0 iframe that has just been resized + lateRender(); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + + + // fetches events if necessary, rerenders events if necessary (or if forced) + function updateEvents(forceRender) { + if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) { + refetchEvents(); + } + else if (forceRender) { + rerenderEvents(); + } + } + + + function refetchEvents() { + fetchEvents(currentView.visStart, currentView.visEnd); // will call reportEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + rerenderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange(eventID) { + rerenderEvents(eventID); + } + + + // attempts to rerenderEvents + function rerenderEvents(modifiedEventID) { + markEventsDirty(); + if (elementVisible()) { + currentView.clearEvents(); + currentView.renderEvents(events, modifiedEventID); + currentView.eventsDirty = false; + } + } + + + function markEventsDirty() { + $.each(viewInstances, function(i, inst) { + inst.eventsDirty = true; + }); + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end, allDay) { + currentView.select(start, end, allDay===undefined ? true : allDay); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + addYears(date, -1); + renderView(); + } + + + function nextYear() { + addYears(date, 1); + renderView(); + } + + + function today() { + date = new Date(); + renderView(); + } + + + function gotoDate(year, month, dateOfMonth) { + if (year instanceof Date) { + date = cloneDate(year); // provided 1 argument, a Date + }else{ + setYMD(date, year, month, dateOfMonth); + } + renderView(); + } + + + function incrementDate(years, months, days) { + if (years !== undefined) { + addYears(date, years); + } + if (months !== undefined) { + addMonths(date, months); + } + if (days !== undefined) { + addDays(date, days); + } + renderView(); + } + + + function getDate() { + return cloneDate(date); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(); + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + + + + /* External Dragging + ------------------------------------------------------------------------*/ + + var _dragElement; + + if (options.droppable) { + $(document) + .bind('dragstart', function(ev, ui) { + var _e = ev.target; + var e = $(_e); + if (!e.parents('.fc').length) { // not already inside a calendar + var accept = options.dropAccept; + if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { + _dragElement = _e; + currentView.dragStart(_dragElement, ev, ui); + } + } + }) + .bind('dragstop', function(ev, ui) { + if (_dragElement) { + currentView.dragStop(_dragElement, ev, ui); + _dragElement = null; + } + }); + } + + +} + +function Header(calendar, options) { + var t = this; + + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + + + // locals + var element = $([]); + var tm; + + + + function render() { + tm = options.theme ? 'ui' : 'fc'; + var sections = options.header; + if (sections) { + element = $("") + .append($("") + .append($(""); + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + tr.append(""); + } + var prevButton; + $.each(this.split(','), function(j, buttonName) { + if (buttonName == 'title') { + tr.append(""); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + prevButton = null; + }else{ + var buttonClick; + if (calendar[buttonName]) { + buttonClick = calendar[buttonName]; // calendar method + } + else if (fcViews[buttonName]) { + buttonClick = function() { + button.removeClass(tm + '-state-hover'); // forget why + calendar.changeView(buttonName); + }; + } + if (buttonClick) { + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + } + var button; + var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; + var text = smartProperty(options.buttonText, buttonName); + if (icon) { + button = $("
" + + "
"); + } + else if (text) { + button = $(""); + } + if (button) { + button + .click(function() { + if (!button.hasClass(tm + '-state-disabled')) { + buttonClick(); + } + }) + .mousedown(function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); + } + ) + .appendTo($("
") + .append(renderSection(sections.left))) + .append($("") + .append(renderSection(sections.center))) + .append($("") + .append(renderSection(sections.right)))); + return element; + } + } + + + function destroy() { + element.remove(); + } + + + function renderSection(buttonStr) { + if (buttonStr) { + var tr = $("

 

").appendTo(tr)); + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + }else{ + button.addClass(tm + '-corner-left'); + } + disableTextSelection(button.closest('td')); + prevButton = button; + } + } + } + }); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + }); + return $("").append(tr); + } + } + + + function updateTitle(html) { + element.find('h2.fc-header-title') + .html(html); + } + + + function activateButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .removeClass(tm + '-state-disabled'); + } + + +} + +var eventGUID = 1; + +function EventManager(options, sources) { + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.addEventSourceFast = addEventSourceFast; + t.clearEventSources = clearEventSources; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.normalizeEvent = normalizeEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var dynamicEventSource = []; + var cache = []; + + + + /* Fetching + -----------------------------------------------------------------------------*/ + function isFetchNeeded(start, end) { + return !rangeStart || start < rangeStart || end > rangeEnd; + } + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + currentFetchID++; + cache = []; + pendingSourceCnt = sources.length; + for (var i=0; i").appendTo(element); + + s = ""; + if (wkn) { + s += ""; + } + for (i=0; i" + formatDate(d, colFormat) + ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + thead = $(s + "").appendTo(table); + + s = ""; + d = cloneDate(t.visStart); + for (i=0; i"; + if (wkn) { + var monday = cloneDate(d); //skip first day (could be sunday) + addDays(monday, 1); + s += ""; + } + for (j=0; j1 && d.getMonth() != month ? ' fc-other-month' : '') + + (+d == +today ? + ' fc-today '+tm+'-state-highlight' : + (+d < +today ? ' fc-before-today fc-not-today' : + ' fc-not-today')) + "'>" + + (showNumbers ? "
" + d.getDate() + "
" : '') + + "
 
"; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + s += "
"; + } + tbody = $(s + "").appendTo(table); + dayBind(tbody.find('td.fc-new').removeClass('fc-new')); + + daySegmentContainer = $("
").appendTo(element); + + }else{ // NOT first time, reuse as many cells as possible + + clearEvents(); + + var prevRowCnt = tbody.find('tr').length; + if (rowCnt < prevRowCnt) { + tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows + } + else if (rowCnt > prevRowCnt) { // needs to create new rows... + s = ''; + for (i=prevRowCnt; i"; + if (wkn) { + var monday = cloneDate(d); //skip first day (could be sunday) + addDays(monday, 1); + s += "
"; + } + for (j=0; j" + + (showNumbers ? "
" : '') + + "
 
" + + ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + s += "
"; + } + tbody.append(s); + } + dayBind(tbody.find('td.fc-new').removeClass('fc-new')); + + // re-label and re-class existing cells + d = cloneDate(t.visStart); + tbody.find('td').each(function() { + var td = $(this); + if (wkn && d.getDay() == 1) + td.closest('tr').find('th').text(d.getWeek()); + if (rowCnt > 1) { + if (d.getMonth() == month) { + td.removeClass('fc-other-month'); + }else{ + td.addClass('fc-other-month'); + } + } + if (+d == +today) { + td.removeClass('fc-not-today fc-before-today') + .addClass(tm + '-state-highlight fc-today'); + }else if (+d < +today) { + td.addClass('fc-not-today fc-before-today') + .removeClass(tm + '-state-highlight fc-today'); + }else{ + td.addClass('fc-not-today') + .removeClass(tm + '-state-highlight fc-today fc-before-today'); + } + td.find('div.fc-day-number').text(d.getDate()); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + if (rowCnt == 1) { // more changes likely (week or day view) + + // redo column header text and class + d = cloneDate(t.visStart); + thead.find('th').not('.fc-axis').each(function(i, th) { + $(th).text(formatDate(d, colFormat)); + th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + // redo cell day-of-weeks + d = cloneDate(t.visStart); + tbody.find('td').each(function(i, td) { + td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + } + + } + + } + + + function setHeight(height) { + viewHeight = height; + var leftTDs = tbody.find('tr td:first-child'), + tbodyHeight = viewHeight - thead.outerHeight(), + rowHeight1, rowHeight2; + if (wkn) + leftTDs = tbody.find('tr th:first-child'); + if (opt('weekMode') == 'variable') { + rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6)); + }else{ + rowHeight1 = Math.floor(tbodyHeight / rowCnt); + rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1); + } + if (tdHeightBug === undefined) { + // bug in firefox where cell height includes padding + var tr = tbody.find('tr:first'), + td = tr.find('td:first'); + setOuterHeight(td,rowHeight1); + tdHeightBug = rowHeight1 != td.outerHeight(); + } + if (tdHeightBug) { + leftTDs.slice(0, -1).height(rowHeight1); + leftTDs.slice(-1).height(rowHeight2); + }else{ + setOuterHeight(leftTDs.slice(0, -1), rowHeight1); + setOuterHeight(leftTDs.slice(-1), rowHeight2); + } + } + + + function setWidth(width) { + viewWidth = width; + colContentPositions.clear(); + if (wkn) { + colWidth = Math.floor((viewWidth-25) / colCnt); + setOuterWidth(thead.find('th').slice(1, -1), colWidth); + } else { + colWidth = Math.floor(viewWidth / colCnt); + setOuterWidth(thead.find('th').slice(0, -1), colWidth); + } + } + + + /* Day clicking and binding + -----------------------------------------------------------*/ + + + function dayBind(days) { + days.click(dayClick) + .mousedown(daySelectionMousedown); + } + + + function dayClick(ev) { + if (!opt('selectable')) { // SelectionManager will worry about dayClick + var n = parseInt(this.className.match(/fc\-day(\d+)/)[1],10), + date = addDays( + cloneDate(t.visStart), + Math.floor(n/colCnt) * 7 + n % colCnt + ); + // TODO: what about weekends in middle of week? + trigger('dayClick', this, date, true, ev); + } + } + + + + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + var rowStart = cloneDate(t.visStart); + var rowEnd = addDays(cloneDate(rowStart), colCnt); + for (var i=0; i" + + "" + + ""; + for (i=0; i" + formatDate(d, colFormat) + ""; + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + } + s += ""; + if (opt('allDaySlot')) { + s += "" + + "" + + "" + + "" + + ""; + } + s+= "
 
"+monday.getWeek()+"
"+monday.getWeek()+"
" + (opt('weekNumbers') ? formatDate(t.visStart, 'w') : ' ') + " 
" + opt('allDayText') + "" + + "
 
 
"; + head = $(s).appendTo(element); + dayBind(head.find('td')); + + // all-day event container + daySegmentContainer = $("
").appendTo(head); + + // body + d = zeroDate(); + var maxd = addMinutes(cloneDate(d), maxMinute); + addMinutes(d, minMinute); + s = ""; + for (i=0; d < maxd; i++) { + minutes = d.getMinutes(); + s += ""; + addMinutes(d, opt('slotMinutes')); + slotCnt++; + } + s += "
" + + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + + "
 
"; + body = $("
") + .append(bodyContent = $("
") + .append(bodyTable = $(s))) + .appendTo(element); + slotBind(body.find('td')); + + // slot event container + slotSegmentContainer = $("
").appendTo(bodyContent); + + // background stripes + d = cloneDate(d0); + s = "
" + + ""; + for (i=0; i
 
"; + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + } + s += "
"; + bg = $(s).appendTo(element); + + }else{ // skeleton already built, just modify it + + clearEvents(); + + if (opt('weekNumbers')) { + head.find('tr:first th:first').text( formatDate(t.visStart, 'w') ); + } + + // redo column header text and class + head.find('tr:first th').slice(1, -1).each(function(i, th) { + $(th).text(formatDate(d, colFormat)); + th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + }); + + // change classes of background stripes + d = cloneDate(d0); + bg.find('td').each(function(i, td) { + td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + if (+d == +today) { + $(td).removeClass('fc-not-today fc-before-today') + .addClass(tm + '-state-highlight fc-today'); + }else if (+d < +today) { + $(td).addClass('fc-not-today fc-before-today') + .removeClass(tm + '-state-highlight fc-today'); + }else{ + $(td).addClass('fc-not-today') + .removeClass(tm + '-state-highlight fc-today fc-before-today'); + } + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + }); + + } + + } + + + + function setHeight(height, dateChanged) { + + if (height === undefined) { + height = viewHeight; + } + + viewHeight = height; + slotTopCache = {}; + + var bodyHeight = height - getHeight(head); + bodyHeight = Math.min(bodyHeight, getHeight(bodyTable)); // shrink to fit table + setOuterHeight(body, bodyHeight); + if (msie9) setOuterHeight(body,height - getHeight(head) - 1); + + slotHeight = getHeight(body.find('tr:first div')) + 1; + + if (dateChanged) { + resetScroll(); + } + } + + + + function setWidth(width) { + viewWidth = width; + colContentPositions.clear(); + + setOuterWidth(body,width); + body.css('overflow', 'auto').width(width); + + var topTDs = head.find('tr:first th'), + allDayLastTH = head.find('tr.fc-all-day th:last'), + stripeTDs = bg.find('td'), + clientWidth = body[0].clientWidth; + + setOuterWidth(bodyTable,clientWidth); + + clientWidth = body[0].clientWidth; // in ie6, sometimes previous clientWidth was wrongly reported + bodyTable.width(clientWidth); + + // time-axis width + axisWidth = 0; + var axisTHs = head.find('tr:lt(2) th:first').add(body.find('tr:first th')); + axisTHs.width(1); + axisTHs.each(function() { + axisWidth = Math.max(axisWidth, $(this).outerWidth()); + }); + + //if (!msie9) + setOuterWidth(axisTHs,axisWidth); + + // column width, except for last column + colWidth = Math.floor((clientWidth - axisWidth) / colCnt); + setOuterWidth(topTDs.slice(1, -2), colWidth); + setOuterWidth(stripeTDs.slice(0, -1), colWidth); + + //if (msie9) //no border on first day (to recheck with different themes) + // stripeTDs.first().outerWidth(colWidth - 1); + + // column width for last column + if (width != clientWidth) { // has scrollbar + setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1)); + topTDs.slice(-1).show(); + allDayLastTH.show(); + }else{ + body.css('overflow', 'hidden'); + topTDs.slice(-2, -1).width(''); + topTDs.slice(-1).hide(); + allDayLastTH.hide(); + } + + bg.css({ + top: getHeight(head.find('tr')), + left: axisWidth, + width: clientWidth - axisWidth, + height: viewHeight + }); + } + + + function resetScroll() { + var d0 = zeroDate(), + scrollDate = cloneDate(d0); + scrollDate.setHours(opt('firstHour')); + var top = timePosition(d0, scrollDate) + 1, // +1 for the border + scroll = function() { + body.scrollTop(top); + }; + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function beforeHide() { + savedScrollTop = body.scrollTop(); + } + + + function afterShow() { + body.scrollTop(savedScrollTop); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(tds) { + tds.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(tds) { + tds.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // SelectionManager will worry about dayClick + var col = Math.min(colCnt-1, Math.floor((ev.pageX - bg.offset().left) / colWidth)), + date = addDays(cloneDate(t.visStart), col*dis+dit), + rowMatch = this.className.match(/fc-slot(\d+)/); + if (rowMatch) { + var mins = parseInt(rowMatch[1],10) * opt('slotMinutes'), + hours = Math.floor(mins/60); + date.setHours(hours); + date.setMinutes(mins%60 + minMinute); + trigger('dayClick', this, date, false, ev); + }else{ + trigger('dayClick', this, date, true, ev); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + + + function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + var visStart = cloneDate(t.visStart); + var startCol, endCol; + if (rtl) { + startCol = dayDiff(endDate, visStart)*dis+dit+1; + endCol = dayDiff(startDate, visStart)*dis+dit+1; + }else{ + startCol = dayDiff(startDate, visStart); + endCol = dayDiff(endDate, visStart); + } + startCol = Math.max(0, startCol); + endCol = Math.min(colCnt, endCol); + if (startCol < endCol) { + dayBind( + renderCellOverlay(0, startCol, 0, endCol-1) + ); + } + } + + + function renderCellOverlay(col0, row0, col1, row1) { + var rect = coordinateGrid.rect(col0, row0, col1, row1, head); + return renderOverlay(rect, head); + } + + + function renderSlotOverlay(overlayStart, overlayEnd) { + var dayStart = cloneDate(t.visStart); + var dayEnd = addDays(cloneDate(dayStart), 1); + for (var i=0; i= addMinutes(cloneDate(day), maxMinute)) { + return getHeight(bodyContent); + } + var slotMinutes = opt('slotMinutes'), + minutes = time.getHours()*60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); + } + + + function cellDate(cell) { + var d = addDays(cloneDate(t.visStart), cell.col*dis+dit); + var slotIndex = cell.row; + if (opt('allDaySlot')) { + slotIndex--; + } + if (slotIndex >= 0) { + addMinutes(d, minMinute + slotIndex*opt('slotMinutes')); + } + return d; + } + + + function cellIsAllDay(cell) { + return opt('allDaySlot') && !cell.row; + } + + + function allDayBounds() { + return { + left: axisWidth, + right: viewWidth + } + } + + + function allDayTR(index) { + return head.find('tr.fc-all-day'); + } + + + function defaultEventEnd(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; + } + return addMinutes(start, opt('defaultEventMinutes')); + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(startDate, allDay) { + if (allDay) { + return cloneDate(startDate); + } + return addMinutes(cloneDate(startDate), opt('slotMinutes')); + } + + + function renderSelection(startDate, endDate, allDay) { + if (allDay) { + if (opt('allDaySlot')) { + renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); + } + }else{ + renderSlotSelection(startDate, endDate); + } + } + + + function renderSlotSelection(startDate, endDate) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + var col = dayDiff(startDate, t.visStart) * dis + dit; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords + var top = timePosition(startDate, startDate); + var bottom = timePosition(startDate, endDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + rect.zIndex = 8; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(bodyContent); + } + }else{ + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: [], + editable: false + }, + rect, + 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' + )); + if ($.browser.msie) { + selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + bodyContent.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var _mousedownElement = this; + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) { + var d1 = cellDate(origCell); + var d2 = cellDate(cell); + dates = [ + d1, + addMinutes(cloneDate(d1), opt('slotMinutes')), + d2, + addMinutes(cloneDate(d2), opt('slotMinutes')) + ].sort(cmp); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + trigger('dayClick', _mousedownElement, dates[0], false, ev); + // BUG: _mousedownElement will sometimes be the overlay + } + reportSelection(dates[0], dates[3], false, ev); + } + }); + } + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + if (cellIsAllDay(cell)) { + renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + }else{ + var d1 = cellDate(cell); + var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); + renderSlotOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui); + } + } + + +} + +function AgendaEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + t.bindDaySeg = bindDaySeg; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var eventEnd = t.eventEnd; + var reportEvents = t.reportEvents; + var reportEventClear = t.reportEventClear; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var getMaxMinute = t.getMaxMinute; + var getMinMinute = t.getMinMinute; + var timePosition = t.timePosition; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var renderDaySegs = t.renderDaySegs; + var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var getSlotHeight = t.getSlotHeight; + var getBodyContent = t.getBodyContent; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var formatDates = calendar.formatDates; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + reportEvents(events); + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i" + + "" + + "" + + "" + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + "" + + "" + htmlEscape(event.title) + "" + + "" + + ((event.resizable || event.resizable === undefined) && (event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ? + "
=
" + : '') + + "
"; + } + + + function bindDaySeg(event, eventElement, seg) { + eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && opt('editable')) { + draggableDayEvent(event, eventElement, seg.isStart); + if (seg.isEnd) { + resizableDayEvent(event, eventElement, getColWidth()); + } + } + } + + + function bindSlotSeg(event, eventElement, seg) { + eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && opt('editable')) { + var timeElement = eventElement.find('span.fc-event-time'); + draggableSlotEvent(event, eventElement, timeElement); + if (seg.isEnd) { + resizableSlotEvent(event, eventElement, timeElement); + } + } + } + + + + /* Dragging + -----------------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + + function draggableDayEvent(event, eventElement, isStart) { + if (!opt('disableDragging') && eventElement.draggable) { + var origWidth; + var allDay=true; + var dayDelta; + var dis = opt('isRTL') ? -1 : 1; + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var slotHeight = getSlotHeight(); + var minMinute = getMinMinute(); + eventElement.draggable({ + zIndex: 9, + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + origWidth = getWidth(eventElement); + hoverListener.start(function(cell, origCell, rowDelta, colDelta) { + eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta); + clearOverlays(); + if (cell) { + dayDelta = colDelta * dis; + if (!cell.row) { + // on full-days + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + resetElement(); + }else{ + // mouse is over bottom slots + if (isStart && allDay) { + // convert event to temporary slot-event + setOuterWidth(eventElement,colWidth - 10); + setOuterHeight( + eventElement, // don't use entire width + slotHeight * Math.round( + (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) + / opt('slotMinutes') + ) + ); + eventElement.draggable('option', 'grid', [colWidth, 1]); + allDay = false; + } + } + } + }, ev, 'drag'); + }, + stop: function(ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + if (cell && (!allDay || dayDelta)) { + // changed! + eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link + var minuteDelta = 0; + if (!allDay) { + minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight) + * opt('slotMinutes') + + minMinute + - (event.start.getHours() * 60 + event.start.getMinutes()); + } + eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); + }else{ + // hasn't moved or is out of bounds (draggable has already reverted) + resetElement(); + if ($.browser.msie) { + eventElement.css('filter', ''); // clear IE opacity side-effects + } + showEvents(event, eventElement); + } + } + }); + function resetElement() { + if (!allDay) { + setOuterWidth(eventElement,origWidth); + eventElement.height(''); + eventElement.draggable('option', 'grid', null); + allDay = true; + } + } + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + if (!opt('disableDragging') && eventElement.draggable) { + var origPosition; + var allDay=false; + var dayDelta; + var minuteDelta; + var prevMinuteDelta; + var dis = opt('isRTL') ? -1 : 1; + var hoverListener = getHoverListener(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var slotHeight = getSlotHeight(); + eventElement.draggable({ + zIndex: 9, + scroll: false, + grid: [colWidth, slotHeight], + axis: colCnt==1 ? 'y' : false, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + if ($.browser.msie) { + eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + origPosition = eventElement.position(); + minuteDelta = prevMinuteDelta = 0; + hoverListener.start(function(cell, origCell, rowDelta, colDelta) { + eventElement.draggable('option', 'revert', !cell); + clearOverlays(); + if (cell) { + dayDelta = colDelta * dis; + if (opt('allDaySlot') && !cell.row) { + // over full days + if (!allDay) { + // convert to temporary all-day event + allDay = true; + timeElement.hide(); + eventElement.draggable('option', 'grid', null); + } + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + }else{ + // on slots + resetElement(); + } + } + }, ev, 'drag'); + }, + drag: function(ev, ui) { + minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes'); + if (minuteDelta != prevMinuteDelta) { + if (!allDay) { + updateTimeText(minuteDelta); + } + prevMinuteDelta = minuteDelta; + } + }, + stop: function(ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + if (cell && (dayDelta || minuteDelta || allDay)) { + // changed! + eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui); + }else{ + // either no change or out-of-bounds (draggable has already reverted) + resetElement(); + eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position + updateTimeText(0); + if ($.browser.msie) { + eventElement + .css('filter', '') // clear IE opacity side-effects + .find('span.fc-event-bg') + .css('display', ''); // .show() made display=inline + } + showEvents(event, eventElement); + } + } + }); + function updateTimeText(minuteDelta) { + var newStart = addMinutes(cloneDate(event.start), minuteDelta); + var newEnd; + if (event.end) { + newEnd = addMinutes(cloneDate(event.end), minuteDelta); + } + timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); + } + function resetElement() { + // convert back to original slot-event + if (allDay) { + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, slotHeight]); + allDay = false; + } + } + } + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + if (!opt('disableResizing') && eventElement.resizable) { + var slotDelta, prevSlotDelta; + var slotHeight = getSlotHeight(); + eventElement.resizable({ + handles: { + s: 'div.ui-resizable-s' + }, + grid: slotHeight, + start: function(ev, ui) { + slotDelta = prevSlotDelta = 0; + hideEvents(event, eventElement); + if ($.browser.msie && $.browser.version == '6.0') { + eventElement.css('overflow', 'hidden'); + } + eventElement.css('z-index', 9); + trigger('eventResizeStart', this, event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + slotDelta = Math.round((Math.max(slotHeight, getHeight(eventElement)) - ui.originalSize.height) / slotHeight); + if (slotDelta != prevSlotDelta) { + timeElement.text( + formatDates( + event.start, + (!slotDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta), + opt('timeFormat') + ) + ); + prevSlotDelta = slotDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', this, event, ev, ui); + if (slotDelta) { + eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui); + }else{ + eventElement.css('z-index', 8); + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + } + + +} + + +function countForwardSegs(levels) { + var i, j, k, level, segForward, segBack; + for (i=levels.length-1; i>0; i--) { + level = levels[i]; + for (j=0; j" + + "" + + (!event.allDay && seg.isStart ? + "" + + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + + "" + :'') + + "" + htmlEscape(event.title) + "" + + "" + + ((event.resizable || event.resizable === undefined) && (event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ? + "
" + : '') + + "
"; + seg.left = left; + seg.outerWidth = right - left; + } + segmentContainer[0].innerHTML = html; // faster than html() + eventElements = segmentContainer.children(); + + // retrieve elements, run through eventRender callback, bind handlers + for (i=0; i div'); // optimal selector? + setOuterHeight(rowDivs[rowI], maxHeight); + } + + /* + // set row heights, calculate event tops (in relation to row top) + for (i=0, rowI=0; rowI div'); // optimal selector? + if (msie9) setOuterHeight(rowDivs[rowI], top + levelHeight); + rowDivs[rowI].height(top + levelHeight); + } + */ + + // calculate row tops + for (rowI=0; rowI"); + } + if (e[0].parentNode != parent[0]) { + e.appendTo(parent); + } + usedOverlays.push(e.css(rect).show()); + return e; + } + + + function clearOverlays() { + var e; + while (e = usedOverlays.shift()) { + unusedOverlays.push(e.hide().unbind()); + } + } + + +} + +function CoordinateGrid(buildFunc) { + + var t = this; + var rows; + var cols; + + t.build = function() { + rows = []; + cols = []; + buildFunc(rows, cols); + }; + + t.cell = function(x, y) { + var rowCnt = rows.length; + var colCnt = cols.length; + var i, r=-1, c=-1; + for (i=0; i= rows[i][0] && y < rows[i][1]) { + r = i; + break; + } + } + for (i=0; i= cols[i][0] && x < cols[i][1]) { + c = i; + break; + } + } + return (r>=0 && c>=0) ? { row:r, col:c } : null; + }; + + t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive + var origin = originElement.offset(); + return { + top: rows[row0][0] - origin.top, + left: cols[col0][0] - origin.left, + width: cols[col1][1] - cols[col0][0], + height: rows[row1][1] - rows[row0][0] + }; + }; + +} + +function HoverListener(coordinateGrid) { + + + var t = this; + var bindType; + var change; + var firstCell; + var cell; + + + t.start = function(_change, ev, _bindType) { + change = _change; + firstCell = cell = null; + coordinateGrid.build(); + mouse(ev); + bindType = _bindType || 'mousemove'; + $(document).bind(bindType, mouse); + }; + + + function mouse(ev) { + var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); + if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { + if (newCell) { + if (!firstCell) { + firstCell = newCell; + } + change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); + }else{ + change(newCell, firstCell); + } + cell = newCell; + } + } + + + t.stop = function() { + $(document).unbind(bindType, mouse); + return cell; + }; + + +} + +function HorizontalPositionCache(getElement) { + + var t = this, + elements = {}, + lefts = {}, + rights = {}; + + function e(i) { + return elements[i] = elements[i] || getElement(i); + } + + t.left = function(i) { + return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i]; + }; + + t.right = function(i) { + return rights[i] = rights[i] === undefined ? t.left(i) + getWidth(e(i)) : rights[i]; + }; + + t.clear = function() { + elements = {}; + lefts = {}; + rights = {}; + }; + +} + + +fc.addDays = addDays; +fc.cloneDate = cloneDate; +fc.parseDate = parseDate; +fc.parseISO8601 = parseISO8601; +fc.parseTime = parseTime; +fc.formatDate = formatDate; +fc.formatDates = formatDates; + + + +/* Date Math +-----------------------------------------------------------------------------*/ + +var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], + DAY_MS = 86400000, + HOUR_MS = 3600000, + MINUTE_MS = 60000; + + +function addYears(d, n, keepTime) { + d.setFullYear(d.getFullYear() + n); + if (!keepTime) { + clearTime(d); + } + return d; +} + + +function addMonths(d, n, keepTime) { // prevents day overflow/underflow + if (+d) { // prevent infinite looping on invalid dates + var m = d.getMonth() + n, + check = cloneDate(d); + check.setDate(1); + check.setMonth(m); + d.setMonth(m); + if (!keepTime) { + clearTime(d); + } + while (d.getMonth() != check.getMonth()) { + d.setDate(d.getDate() + (d < check ? 1 : -1)); + } + } + return d; +} + + +function addDays(d, n, keepTime) { // deals with daylight savings + if (+d) { + var dd = d.getDate() + n, + check = cloneDate(d); + check.setHours(9); // set to middle of day + check.setDate(dd); + d.setDate(dd); + if (!keepTime) { + clearTime(d); + } + fixDate(d, check); + } + return d; +} + + +function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes + if (+d) { // prevent infinite looping on invalid dates + while (d.getDate() != check.getDate()) { + d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS); + } + } +} + + +function addMinutes(d, n) { + d.setMinutes(d.getMinutes() + n); + return d; +} + + +function clearTime(d) { + d.setHours(0); + d.setMinutes(0); + d.setSeconds(0); + d.setMilliseconds(0); + return d; +} + + +function cloneDate(d, dontKeepTime) { + if (dontKeepTime) { + return clearTime(new Date(+d)); + } + return new Date(+d); +} + + +function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1 + var i=0, d; + do { + d = new Date(1970, i++, 1); + } while (d.getHours()); // != 0 + return d; +} + + +function skipWeekend(date, inc, excl) { + inc = inc || 1; + while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) { + addDays(date, inc); + } + return date; +} + + +function dayDiff(d1, d2) { // d1 - d2 + return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); +} + + +function setYMD(date, y, m, d) { + if (y !== undefined && y != date.getFullYear()) { + date.setDate(1); + date.setMonth(0); + date.setFullYear(y); + } + if (m !== undefined && m != date.getMonth()) { + date.setDate(1); + date.setMonth(m); + } + if (d !== undefined) { + date.setDate(d); + } +} + + + +/* Date Parsing +-----------------------------------------------------------------------------*/ + + +function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true + if (typeof s == 'object') { // already a Date object + return s; + } + if (typeof s == 'number') { // a UNIX timestamp + return new Date(s * 1000); + } + if (typeof s == 'string') { + if (s.match(/^\d+$/)) { // a UNIX timestamp + return new Date(parseInt(s,10) * 1000); + } + if (ignoreTimezone === undefined) { + ignoreTimezone = true; + } + return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null); + } + // TODO: never return invalid dates (like from new Date()), return null instead + return null; +} + + +function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false + // derived from http://delete.me.uk/2005/03/iso8601.html + // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html + var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/); + if (!m) { + return null; + } + var date = new Date(m[1], 0, 1); + if (ignoreTimezone || !m[14]) { + var check = new Date(m[1], 0, 1, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); + } + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + }else{ + date.setUTCFullYear( + m[1], + m[3] ? m[3] - 1 : 0, + m[5] || 1 + ); + date.setUTCHours( + m[7] || 0, + m[8] || 0, + m[10] || 0, + m[12] ? Number("0." + m[12]) * 1000 : 0 + ); + var offset = Number(m[16]) * 60 + Number(m[17]); + offset *= m[15] == '-' ? 1 : -1; + date = new Date(+date + (offset * 60 * 1000)); + } + return date; +} + + +function parseTime(s) { // returns minutes since start of day + if (typeof s == 'number') { // an hour + return s * 60; + } + if (typeof s == 'object') { // a Date object + return s.getHours() * 60 + s.getMinutes(); + } + var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); + if (m) { + var h = parseInt(m[1],10); + if (m[3]) { + h %= 12; + if (m[3].toLowerCase().charAt(0) == 'p') { + h += 12; + } + } + return h * 60 + (m[2] ? parseInt(m[2],10) : 0); + } +} + + + +/* Date Formatting +-----------------------------------------------------------------------------*/ +// TODO: use same function formatDate(date, [date2], format, [options]) + + +function formatDate(date, format, options) { + return formatDates(date, null, format, options); +} + + +function formatDates(date1, date2, format, options) { + options = options || defaults; + var date = date1, + otherDate = date2, + i, len = format.length, c, + i2, formatter, + res = ''; + for (i=0; ii; i2--) { + if (formatter = dateFormatters[format.substring(i, i2)]) { + if (date) { + res += formatter(date, options); + } + i = i2 - 1; + break; + } + } + if (i2 == i) { + if (date) { + res += c; + } + } + } + } + return res; +}; + + +var dateFormatters = { + s : function(d) { return d.getSeconds() }, + ss : function(d) { return zeroPad(d.getSeconds()) }, + m : function(d) { return d.getMinutes() }, + mm : function(d) { return zeroPad(d.getMinutes()) }, + h : function(d) { return d.getHours() % 12 || 12 }, + hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, + H : function(d) { return d.getHours() }, + HH : function(d) { return zeroPad(d.getHours()) }, + d : function(d) { return d.getDate() }, + dd : function(d) { return zeroPad(d.getDate()) }, + ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, + dddd: function(d,o) { return o.dayNames[d.getDay()] }, + M : function(d) { return d.getMonth() + 1 }, + MM : function(d) { return zeroPad(d.getMonth() + 1) }, + MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, + MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, + yy : function(d) { return (d.getFullYear()+'').substring(2) }, + yyyy: function(d) { return d.getFullYear() }, + t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, + tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, + T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, + TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, + u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, + S : function(d) { + var date = d.getDate(); + if (date > 10 && date < 20) { + return 'th'; + } + return ['st', 'nd', 'rd'][date%10-1] || 'th'; + }, + w : function(d) { return d.getWeek(); } +}; + +if (Date.prototype.getWeek === undefined) { + + Date.prototype.getWeek = function() { + //By tanguy.pruvot at gmail.com (2010) + var week, days, jan4, jan4Day, thisDay, msDay = 86400000; + + //first week of year always contains 4th Jan (ISO) + jan4 = new Date(this.getFullYear(),0,4 ,this.getHours()); + days = Math.round( (this - jan4) / msDay); + + //ISO weeks begins on monday, so rotate monday:sunday to 0:6 + jan4Day = (jan4.getDay() + 6) % 7; + + week = Math.floor( (days + jan4Day) / 7) + 1; + + //special cases + thisDay = (this.getDay() + 6) % 7; + if (this.getMonth() == 11 && this.getDate() >= 28) { + + jan4.setFullYear( this.getFullYear() + 1 ); + jan4Day = (jan4.getDay() + 6) % 7; + + if (thisDay < jan4Day) + return 1; + + var prevWeek = new Date( this-(msDay*7) ); + week = prevWeek.getWeek() + 1; + } + + if (week === 0 && thisDay > 3 && this.getMonth() == 0) { + var prevWeek = new Date( this-(msDay*7) ); + week = prevWeek.getWeek() + 1; + } + + return week; + } + +} + + +/* Event Date Math +-----------------------------------------------------------------------------*/ + + +function exclEndDay(event) { + if (event.end) { + return _exclEndDay(event.end, event.allDay); + }else{ + return addDays(cloneDate(event.start), 1); + } +} + + +function _exclEndDay(end, allDay) { + end = cloneDate(end); + return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); +} + + +function segCmp(a, b) { + if (a.event.allDay && !b.event.allDay) return -1; + if (!a.event.allDay && b.event.allDay) return 1; + return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start); +} + + +function segsCollide(seg1, seg2) { + return seg1.end > seg2.start && seg1.start < seg2.end; +} + + + +/* Event Sorting +-----------------------------------------------------------------------------*/ + + +// event rendering utilities +function sliceSegs(events, visEventEnds, start, end) { + var segs = [], + i, len=events.length, event, + eventStart, eventEnd, + segStart, segEnd, + isStart, isEnd; + for (i=0; i start && eventStart < end) { + if (eventStart < start) { + segStart = cloneDate(start); + isStart = false; + }else{ + segStart = eventStart; + isStart = true; + } + if (eventEnd > end) { + segEnd = cloneDate(end); + isEnd = false; + }else{ + segEnd = eventEnd; + isEnd = true; + } + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd, + msLength: segEnd - segStart + }); + } + } + return segs.sort(segCmp); +} + + +// event rendering calculation utilities +function stackSegs(segs) { + var levels = [], + i, len = segs.length, seg, + j, collide, k; + for (i=0; i=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['']; +} + + +function htmlEscape(s) { + return s.replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); +} + + +function cssKey(_element) { + return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, ''); +} + + +function disableTextSelection(element) { + element + .attr('unselectable', 'on') + .css('MozUserSelect', 'none') + .bind('selectstart.ui', function() { return false; }); +} + + +/* +function enableTextSelection(element) { + element + .attr('unselectable', 'off') + .css('MozUserSelect', '') + .unbind('selectstart.ui'); +} +*/ + + + +})(jQuery); \ No newline at end of file diff --git a/demo/index.php b/demo/index.php new file mode 100644 index 0000000..4353f19 --- /dev/null +++ b/demo/index.php @@ -0,0 +1,119 @@ +".print_r($x,true).""; +} + +$ICS = "basic.ics"; + +$ical = new SG_iCalReader($ICS); +$query = new SG_iCal_Query(); + +//$evts = $ical->getEvents(); +$evts = $query->Between($ical,strtotime('20100901'),strtotime('20101131')); + + +$data = array(); +foreach($evts as $id => $ev) { + + $jsEvt = array( + "id" => ($id+1), + "title" => $ev->getProperty('summary'), + "start" => $ev->getStart(), + "end" => $ev->getEnd(), + "allDay" => false + ); + $data[] = $jsEvt; + + if (isset($ev->recurrence)) { + $count = 1; + $start = $ev->getStart(); + $freq = new SG_iCal_Freq($ev->recurrence->rrule, $start); + while (($next = $freq->nextOccurrence($start)) > 0 ) { + if (!$next or $count >= 200) break; + $count++; + $start = $next; + $jsEvt["start"] = $start; + $jsEvt["end"] = $start + $ev->getDuration(); + $data[] = $jsEvt; + } + } + +} + +//dump_t($data); + +$events = "events:".json_encode($data).','; + +?> + + + +Fullcalendar iCal Loader + + + + + + + + +
+ + \ No newline at end of file From e81a439d4fb17281b01c00fb411af91272b7e672 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Fri, 29 Oct 2010 14:04:21 +0200 Subject: [PATCH 25/44] nextOccurrence < start check --- SG_iCal.php | 2 +- helpers/SG_iCal_Freq.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/SG_iCal.php b/SG_iCal.php index dce65c1..2e5bf3c 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -1,6 +1,6 @@ 'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday'); - protected $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'); + protected $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'); //others : 'setpos', 'second' + protected $ruleModifiers = array('wkst'); protected $simpleMode = true; protected $rules = array('freq'=>'yearly', 'interval'=>1); @@ -97,7 +98,7 @@ class SG_iCal_Freq { * @return int */ public function nextOccurrence( $offset ) { - //return $this->findNext( $this->previousOccurrence( $offset) ); + $start = ($offset > $this->start) ? $offset : $this->start; return $this->findNext($offset); } From 7ddf89af7702bfb211c67a7109121379dec40318 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 14:19:08 +0200 Subject: [PATCH 26/44] fix last occurrence of an event (one was missing) --- helpers/SG_iCal_Freq.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index ea12ebe..28dc39e 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -68,7 +68,7 @@ class SG_iCal_Freq { if( isset($this->rules['count']) ) { $n = $start; - for($i=0;$i<$this->rules['count'];$i++) { + for($i=0;$i<$this->rules['count']-1;$i++) { $n = $this->findNext($n); } $this->rules['until'] = $n; @@ -99,7 +99,7 @@ class SG_iCal_Freq { */ public function nextOccurrence( $offset ) { $start = ($offset > $this->start) ? $offset : $this->start; - return $this->findNext($offset); + return $this->findNext($start); } /** @@ -107,13 +107,11 @@ class SG_iCal_Freq { * @return int timestamp */ public function lastOccurrence() { - $temp_timestamp = $this->findNext($this->start); - $timestamp = 0; - while ($temp_timestamp) { - $timestamp = $temp_timestamp; - $temp_timestamp = $this->findNext($temp_timestamp); + $next = $this->findNext($this->start); + while ($next) { + $next = $this->findNext($next); } - return $timestamp; + return $next; } /** @@ -143,7 +141,7 @@ class SG_iCal_Freq { $echo = false; //make sure the offset is valid - if( $offset === false || (isset($this->rules['until']) && $this->rules['until'] <= $offset) ) { + if( $offset === false || (isset($this->rules['until']) && $offset > $this->rules['until']) ) { if($echo) echo 'STOP: ' . date('r', $offset) . "\n"; return false; } @@ -196,7 +194,7 @@ class SG_iCal_Freq { } } - if( $this->start > $offset && $this->start < $t ) { + if( $offset < $this->start && $this->start < $t ) { return $this->start; } else if( $found && ($t != $offset)) { if( $this->validDate( $t ) ) { @@ -398,7 +396,7 @@ class SG_iCal_Freq { } private function validDate( $t ) { - if( isset($this->rules['until']) && $this->rules['until'] <= $t ) { + if( isset($this->rules['until']) && $t > $this->rules['until'] ) { return false; } From 8f7f8062934a2f94adbc5835120fe25200c3a301 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 15:38:02 +0200 Subject: [PATCH 27/44] DateTimeZone class removed, using date_default_timezone_set --- blocks/SG_iCal_VEvent.php | 51 +++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index f13f9f0..00b1f10 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -88,6 +88,10 @@ class SG_iCal_VEvent { } } + if( isset($this->previous_tz) ) { + date_default_timezone_set($this->previous_tz); + } + $this->data = SG_iCal_Line::Remove_Line($data); } @@ -190,34 +194,39 @@ class SG_iCal_VEvent { } } + + + /** + * Set default timezone (temporary) to get timestamps + * @return string + */ + protected function setLineTimeZone(SG_iCal_Line $line) { + if( isset($line['tzid']) ) { + if (!isset($this->previous_tz)) { + $this->previous_tz = @ date_default_timezone_get(); + } + $this->tzid = $line['tzid']; + date_default_timezone_set($this->tzid); + return true; + } + return false; + } + /** * Calculates the timestamp from a DT line. * @param $line SG_iCal_Line * @return int */ - private function getTimestamp( SG_iCal_Line $line, SG_iCal $ical ) { + protected function getTimestamp( SG_iCal_Line $line, SG_iCal $ical ) { - if (class_exists('DateTimeZone')) { - - if( isset($line['tzid']) ) { - $tz = $ical->getTimeZoneInfo($line['tzid']); - $tz = new DateTimeZone( $tz->getTimeZoneId() ); - $date = new DateTime($line->getData(),$tz); - } else { - $date = new DateTime($line->getData()); - } - $ts = (int) $date->format('U'); - - } else { - - //Warning in PHP 5.2 Strict - $ts = strtotime($line->getData()); - if( isset($line['tzid']) ) { - $tz = $ical->getTimeZoneInfo($line['tzid']); - $offset = $tz->getOffset($ts); - $ts = strtotime(date('D, d M Y H:i:s', $ts) . ' ' . $offset); - } + if( isset($line['tzid']) ) { + $this->setLineTimeZone($line); + //$tz = $ical->getTimeZoneInfo($line['tzid']); + //$offset = $tz->getOffset($ts); + //$ts = strtotime(date('D, d M Y H:i:s', $ts) . ' ' . $offset); } + $ts = strtotime($line->getData()); + return $ts; } } From f9e8612eb61a3c80cd576a85f90e8e5fed553aa8 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 16:32:39 +0200 Subject: [PATCH 28/44] add event method : isWholeDay() --- blocks/SG_iCal_VEvent.php | 12 ++++++++++++ demo/index.php | 12 +++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index 00b1f10..166db35 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -179,6 +179,18 @@ class SG_iCal_VEvent { return $this->end - $this->start; } + /** + * Returns true if duration is multiple of 86400 + * @return bool + */ + public function isWholeDay() { + $dur = $this->getDuration(); + if ($dur > 0 && ($dur % 86400) == 0) { + return true; + } + return false; + } + /** * Returns the given property of the event. * @param string $prop diff --git a/demo/index.php b/demo/index.php index 4353f19..8551eb4 100644 --- a/demo/index.php +++ b/demo/index.php @@ -17,14 +17,14 @@ $evts = $query->Between($ical,strtotime('20100901'),strtotime('20101131')); $data = array(); foreach($evts as $id => $ev) { - $jsEvt = array( "id" => ($id+1), "title" => $ev->getProperty('summary'), "start" => $ev->getStart(), - "end" => $ev->getEnd(), - "allDay" => false + "end" => $ev->getEnd()-1, + "allDay" => $ev->isWholeDay() ); + $data[] = $jsEvt; if (isset($ev->recurrence)) { @@ -36,13 +36,15 @@ foreach($evts as $id => $ev) { $count++; $start = $next; $jsEvt["start"] = $start; - $jsEvt["end"] = $start + $ev->getDuration(); + $jsEvt["end"] = $start + $ev->getDuration()-1; + $data[] = $jsEvt; } } } - +//echo(date('Ymd\n',$data[0][start])); +//echo(date('Ymd\n',$data[1][start])); //dump_t($data); $events = "events:".json_encode($data).','; From e03b95f43d3e332cfbd1a21dfcb001ce6d6792b6 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 17:36:40 +0200 Subject: [PATCH 29/44] code cleanup --- helpers/SG_iCal_Freq.php | 122 +++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 28dc39e..9057c8a 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -1,10 +1,10 @@ 'yearly', 'interval'=>1); protected $start = 0; protected $freq = ''; - + /** * Constructs a new Freqency-rule - * @param $rule string + * @param $rule string * @param $start int Unix-timestamp (important!) */ public function __construct( $rule, $start ) { $this->start = $start; - + $rules = array(); foreach( explode(';', $rule) AS $v) { list($k, $v) = explode('=', $v); @@ -50,7 +50,7 @@ class SG_iCal_Freq { $this->rules['until'] = strtotime($this->rules['until']); } $this->freq = strtolower($this->rules['freq']); - + foreach( $this->knownRules AS $rule ) { if( isset($this->rules['by' . $rule]) ) { @@ -59,7 +59,7 @@ class SG_iCal_Freq { } } } - + if(!$this->simpleMode) { if(! (isset($this->rules['byday']) || isset($this->rules['bymonthday']) || isset($this->rules['byyearday']))) { $this->rules['bymonthday'] = date('d', $this->start); @@ -74,7 +74,7 @@ class SG_iCal_Freq { $this->rules['until'] = $n; } } - + /** * Returns the previous (most recent) occurrence of the rule from the * given offset @@ -91,7 +91,7 @@ class SG_iCal_Freq { } return $t1; } - + /** * Returns the next occurrence of this rule after the given offset * @param int $offset @@ -113,36 +113,36 @@ class SG_iCal_Freq { } return $next; } - + /** - * Calculates the next time after the given offset that the rule + * Calculates the next time after the given offset that the rule * will apply. * * The approach to finding the next is as follows: * First we establish a timeframe to find timestamps in. This is * between $offset and the end of the period that $offset is in. - * - * We then loop though all the rules (that is a Prerule in the + * + * We then loop though all the rules (that is a Prerule in the * current freq.), and finds the smallest timestamp inside the * timeframe. * * If we find something, we check if the date is a valid recurrence - * (with validDate). If it is, we return it. Otherwise we try to + * (with validDate). If it is, we return it. Otherwise we try to * find a new date inside the same timeframe (but using the new- * found date as offset) * - * If no new timestamps were found in the period, we try in the + * If no new timestamps were found in the period, we try in the * next period * * @param int $offset * @return int */ public function findNext($offset) { - $echo = false; + $debug = false; //make sure the offset is valid if( $offset === false || (isset($this->rules['until']) && $offset > $this->rules['until']) ) { - if($echo) echo 'STOP: ' . date('r', $offset) . "\n"; + if($debug) echo 'STOP: ' . date('r', $offset) . "\n"; return false; } @@ -150,12 +150,12 @@ class SG_iCal_Freq { //set the timestamp of the offset (ignoring hours and minutes unless we want them to be //part of the calculations. - if($echo) echo 'O: ' . date('r', $offset) . "\n"; + if($debug) echo 'O: ' . date('r', $offset) . "\n"; $hour = (in_array($this->freq, array('hourly','minutely')) && $offset > $this->start) ? date('H', $offset) : date('H', $this->start); $minute = (($this->freq == 'minutely' || isset($this->rules['byminute'])) && $offset > $this->start) ? date('i', $offset) : date('i', $this->start); $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y',$offset)); - if($echo) echo 'START: ' . date('r', $t) . "\n"; - + if($debug) echo 'START: ' . date('r', $t) . "\n"; + if( $this->simpleMode ) { if( $offset < $t ) { return $t; @@ -166,10 +166,10 @@ class SG_iCal_Freq { } return $next; } - + $eop = $this->findEndOfPeriod($offset); - if($echo) echo 'EOP: ' . date('r', $eop) . "\n"; - + if($debug) echo 'EOP: ' . date('r', $eop) . "\n"; + foreach( $this->knownRules AS $rule ) { if( $found && isset($this->rules['by' . $rule]) ) { if( $this->isPrerule($rule, $this->freq) ) { @@ -180,7 +180,7 @@ class SG_iCal_Freq { if( $imm === false ) { break; } - if($echo) echo strtoupper($rule) . ': ' . date('r', $imm) . ' A: ' . ((int) ($imm > $offset && $imm < $eop)) . "\n"; + if($debug) echo strtoupper($rule) . ': ' . date('r', $imm) . ' A: ' . ((int) ($imm > $offset && $imm < $eop)) . "\n"; if( $imm > $offset && $imm < $eop && ($_t == null || $imm < $_t) ) { $_t = $imm; } @@ -198,18 +198,18 @@ class SG_iCal_Freq { return $this->start; } else if( $found && ($t != $offset)) { if( $this->validDate( $t ) ) { - if($echo) echo 'OK' . "\n"; + if($debug) echo 'OK' . "\n"; return $t; } else { - if($echo) echo 'Invalid' . "\n"; + if($debug) echo 'Invalid' . "\n"; return $this->findNext($t); } } else { - if($echo) echo 'Not found' . "\n"; + if($debug) echo 'Not found' . "\n"; return $this->findNext( $this->findStartingPoint( $offset, $this->rules['interval'] ) ); - } + } } - + /** * Finds the starting point for the next rule. It goes $interval * 'freq' forward in time since the given offset @@ -229,14 +229,14 @@ class SG_iCal_Freq { } $sp = strtotime($t, $offset); - + if( $truncate ) { $sp = $this->truncateToPeriod($sp, $this->freq); } - + return $sp; } - + /** * Finds the earliest timestamp posible outside this perioid * @param int $offset @@ -245,11 +245,11 @@ class SG_iCal_Freq { public function findEndOfPeriod($offset) { return $this->findStartingPoint($offset, 1); } - + /** * Resets the timestamp to the beginning of the * period specified by freq - * + * * Yes - the fall-through is on purpose! * * @param int $time @@ -283,7 +283,7 @@ class SG_iCal_Freq { $d = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']); return $d; } - + /** * Applies the BYDAY rule to the given timestamp * @param string $rule @@ -293,24 +293,24 @@ class SG_iCal_Freq { private function ruleByday($rule, $t) { $dir = ($rule{0} == '-') ? -1 : 1; $dir_t = ($dir == 1) ? 'next' : 'last'; - - + + $d = $this->weekdays[substr($rule,-2)]; $s = $dir_t . ' ' . $d . ' ' . date('H:i:s',$t); - + if( $rule == substr($rule, -2) ) { if( date('l', $t) == ucfirst($d) ) { $s = 'today ' . date('H:i:s',$t); } - + $_t = strtotime($s, $t); - + if( $_t == $t && in_array($this->freq, array('monthly', 'yearly')) ) { // Yes. This is not a great idea.. but hey, it works.. for now $s = 'next ' . $d . ' ' . date('H:i:s',$t); $_t = strtotime($s, $_t); } - + return $_t; } else { $_f = $this->freq; @@ -323,10 +323,10 @@ class SG_iCal_Freq { $_t = $this->truncateToPeriod($t, $this->freq); } $this->freq = $_f; - + $c = preg_replace('/[^0-9]/','',$rule); $c = ($c == '') ? 1 : $c; - + $n = $_t; while($c > 0 ) { if( $dir == 1 && $c == 1 && date('l', $t) == ucfirst($d) ) { @@ -335,11 +335,11 @@ class SG_iCal_Freq { $n = strtotime($s, $n); $c--; } - + return $n; } } - + private function ruleBymonth($rule, $t) { $_t = mktime(date('H',$t), date('i',$t), date('s',$t), $rule, date('d', $t), date('Y', $t)); if( $t == $_t && isset($this->rules['byday']) ) { @@ -349,14 +349,14 @@ class SG_iCal_Freq { return $_t; } } - + private function ruleBymonthday($rule, $t) { if( $rule < 0 ) { $rule = date('t', $t) + $rule + 1; } return mktime(date('H',$t), date('i',$t), date('s',$t), date('m', $t), $rule, date('Y', $t)); } - + private function ruleByyearday($rule, $t) { if( $rule < 0 ) { $_t = $this->findEndOfPeriod(); @@ -376,12 +376,12 @@ class SG_iCal_Freq { } else { $_t = $this->truncateToPeriod($t, $this->freq); $d = '+'; - } + } $sub = (date('W', $_t) == 1) ? 2 : 1; $s = $d . abs($rule - $sub) . ' weeks ' . date('H:i:s',$t); $_t = strtotime($s, $_t); - + return $_t; } @@ -389,17 +389,17 @@ class SG_iCal_Freq { $_t = mktime($rule, date('i',$t), date('s',$t), date('m',$t), date('d', $t), date('Y', $t)); return $_t; } - + private function ruleByminute($rule, $t) { $_t = mktime(date('h',$t), $rule, date('s',$t), date('m',$t), date('d', $t), date('Y', $t)); return $_t; } - + private function validDate( $t ) { if( isset($this->rules['until']) && $t > $this->rules['until'] ) { return false; } - + if( isset($this->rules['bymonth']) ) { $months = explode(',', $this->rules['bymonth']); if( !in_array(date('m', $t), $months)) { @@ -438,10 +438,10 @@ class SG_iCal_Freq { return false; } } - + return true; } - + private function isPrerule($rule, $freq) { if( $rule == 'year') return false; @@ -456,11 +456,11 @@ class SG_iCal_Freq { return true; if( $rule == 'day' && in_array($freq, array('yearly', 'monthly', 'weekly'))) return true; - if( $rule == 'hour' && in_array($freq, array('yearly', 'monthly', 'weekly', 'daily'))) + if( $rule == 'hour' && in_array($freq, array('yearly', 'monthly', 'weekly', 'daily'))) return true; - if( $rule == 'minute' ) + if( $rule == 'minute' ) return true; - + return false; } } From dd982d53f96f13bf663628176f6ee5a7c0636929 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 19:40:16 +0200 Subject: [PATCH 30/44] fix bad test and link iCalFreq in event to get last occurrence date --- blocks/SG_iCal_VEvent.php | 19 +++++++++---------- helpers/SG_iCal_Freq.php | 28 +++++++++++++++++++--------- tests/helpers/FreqTest.php | 19 ++++++++++++++++--- tests/timestamp.sh | 6 ++++++ 4 files changed, 50 insertions(+), 22 deletions(-) create mode 100755 tests/timestamp.sh diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index 166db35..02e35ca 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -57,27 +57,26 @@ class SG_iCal_VEvent { unset($data['duration']); } - //google cal set dtend as end of initial event + //google cal set dtend as end of initial event (duration) if ( isset($this->recurrence) ) { //if there is a recurrence rule $until = $this->recurrence->getUntil(); $count = $this->recurrence->getCount(); //check if there is either 'until' or 'count' set - if ( $until or $count ) { - //if until is set, set that as the end date (using getTimeStamp) - if ( $until ) { - //date_default_timezone_set( xx ); - $this->laststart = strtotime($until); - $this->lastend = $this->laststart + $this->getDuration(); - } + if ( $until ) { + //ok.. + } elseif ($count) { //if count is set, then figure out the last occurrence and set that as the end date + $freq = new SG_iCal_Freq($this->recurrence->rrule, $start); + $until = $freq->lastOccurrence($this->start); } else { //forever... limit to 3 years $this->recurrence->setUntil('+3 years'); $until = $this->recurrence->getUntil(); - $this->laststart = strtotime($until); - $this->lastend = $this->laststart + $this->getDuration(); } + //date_default_timezone_set( xx ) needed ?; + $this->laststart = strtotime($until); + $this->lastend = $this->laststart + $this->getDuration(); } $imports = array('summary','description','location'); diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 9057c8a..4ad9c46 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -67,11 +67,11 @@ class SG_iCal_Freq { } if( isset($this->rules['count']) ) { - $n = $start; - for($i=0;$i<$this->rules['count']-1;$i++) { - $n = $this->findNext($n); + $ts = $this->firstOccurrence(); + for($i=1; $i<$this->rules['count']; $i++) { + $ts = $this->findNext($ts); } - $this->rules['until'] = $n; + $this->rules['until'] = $ts; } } @@ -98,8 +98,17 @@ class SG_iCal_Freq { * @return int */ public function nextOccurrence( $offset ) { - $start = ($offset > $this->start) ? $offset : $this->start; - return $this->findNext($start); + if ($offset < $this->start) + return $this->firstOccurrence(); + return $this->findNext($offset); + } + + /** + * Finds the first occurrence of the rule. + * @return int timestamp + */ + public function firstOccurrence() { + return $this->start; } /** @@ -107,11 +116,12 @@ class SG_iCal_Freq { * @return int timestamp */ public function lastOccurrence() { - $next = $this->findNext($this->start); + $ts = $next = $this->findNext($this->start); while ($next) { - $next = $this->findNext($next); + $ts = $next; + $next = $this->findNext($ts); } - return $next; + return $ts; } /** diff --git a/tests/helpers/FreqTest.php b/tests/helpers/FreqTest.php index d390d95..9cea3b9 100755 --- a/tests/helpers/FreqTest.php +++ b/tests/helpers/FreqTest.php @@ -605,13 +605,25 @@ class FreqTest extends PHPUnit_Framework_TestCase { } } - public function testLastOccurrence() { + + //weird : in this test $start is not an occurrence ! + /* + public function testFirstOccurrenceByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2009-10-27T090000'); $freq = new SG_iCal_Freq($rule, $start); - $this->assertEquals(strtotime('2018-01-01T09:00:00'), $freq->lastOccurrence()); + $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->firstOccurrence()); } + */ + public function testLastOccurrenceByYearDay() { + $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; + $start = strtotime('2011-01-01T090000'); + $freq = new SG_iCal_Freq($rule, $start); + $this->assertEquals(strtotime('2019-01-01T09:00:00'), $freq->lastOccurrence()); + } + + // TODO: WKST rule private function assertRule( $rule, $start, $dateset ) { @@ -620,10 +632,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $n = $start - 1; do { $n = $freq->findNext($n); + //echo date('Y-m-d H:i:sO ',$n); $e = (current($dateset) != -1) ? current($dateset) : false; $this->assertEquals($e, $n); } while( next($dateset) !== false ); } } -?> \ No newline at end of file +?> diff --git a/tests/timestamp.sh b/tests/timestamp.sh new file mode 100755 index 0000000..a2f431e --- /dev/null +++ b/tests/timestamp.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +TIMESTAMP=$1 + +php -r "echo date('Y-m-d H:i:s O',$TIMESTAMP); echo \"\n\"; " + From ca0dcc1af094a87f5d3c9dcf2a0cb1b06f725418 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 20:54:42 +0200 Subject: [PATCH 31/44] implement frequency cache, actually only set in some cases : count recurrence or using lastOccurrence() --- blocks/SG_iCal_VEvent.php | 29 +++++++++++++++++++++++------ demo/index.php | 7 ++++--- helpers/SG_iCal_Freq.php | 36 ++++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index 02e35ca..7b4fa69 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -15,7 +15,7 @@ */ class SG_iCal_VEvent { const DEFAULT_CONFIRMED = true; - + protected $uid; protected $start; protected $end; @@ -24,10 +24,12 @@ class SG_iCal_VEvent { protected $summary; protected $description; protected $location; - + public $recurrence; + public $freq; //getFrequency() + public $data; - + /** * Constructs a new SG_iCal_VEvent. Needs the SG_iCalReader * supplied so it can query for timezones. @@ -35,6 +37,7 @@ class SG_iCal_VEvent { * @param SG_iCalReader $ical */ public function __construct($data, SG_iCal $ical) { + $this->uid = $data['uid']->getData(); unset($data['uid']); @@ -42,7 +45,7 @@ class SG_iCal_VEvent { $this->recurrence = new SG_iCal_Recurrence($data['rrule']); unset($data['rrule']); } - + if( isset($data['dtstart']) ) { $this->start = $this->getTimestamp($data['dtstart'], $ical); unset($data['dtstart']); @@ -67,8 +70,8 @@ class SG_iCal_VEvent { //ok.. } elseif ($count) { //if count is set, then figure out the last occurrence and set that as the end date - $freq = new SG_iCal_Freq($this->recurrence->rrule, $start); - $until = $freq->lastOccurrence($this->start); + $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $start); + $until = $this->freq->lastOccurrence($this->start); } else { //forever... limit to 3 years $this->recurrence->setUntil('+3 years'); @@ -94,6 +97,20 @@ class SG_iCal_VEvent { $this->data = SG_iCal_Line::Remove_Line($data); } + + /** + * Returns the Event Occurrences Iterator (if recurrence set) + * @return SG_iCal_Freq + */ + public function getFrequency() { + if (! isset($this->freq)) { + if ( isset($this->recurrence) ) { + $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $this->start); + } + } + return $this->freq; + } + /** * Returns the UID of the event * @return string diff --git a/demo/index.php b/demo/index.php index 8551eb4..f6b4f0c 100644 --- a/demo/index.php +++ b/demo/index.php @@ -11,8 +11,8 @@ $ICS = "basic.ics"; $ical = new SG_iCalReader($ICS); $query = new SG_iCal_Query(); -//$evts = $ical->getEvents(); -$evts = $query->Between($ical,strtotime('20100901'),strtotime('20101131')); +$evts = $ical->getEvents(); +//$evts = $query->Between($ical,strtotime('20100901'),strtotime('20101131')); $data = array(); @@ -30,7 +30,8 @@ foreach($evts as $id => $ev) { if (isset($ev->recurrence)) { $count = 1; $start = $ev->getStart(); - $freq = new SG_iCal_Freq($ev->recurrence->rrule, $start); + //$freq = new SG_iCal_Freq($ev->recurrence->rrule, $start); + $freq = $ev->getFrequency(); while (($next = $freq->nextOccurrence($start)) > 0 ) { if (!$next or $count >= 200) break; $count++; diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 4ad9c46..f02071e 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -31,11 +31,13 @@ class SG_iCal_Freq { protected $rules = array('freq'=>'yearly', 'interval'=>1); protected $start = 0; protected $freq = ''; + + public $cache; /** * Constructs a new Freqency-rule * @param $rule string - * @param $start int Unix-timestamp (important!) + * @param $start int Unix-timestamp (important : Need to be the start of Event) */ public function __construct( $rule, $start ) { $this->start = $start; @@ -65,11 +67,14 @@ class SG_iCal_Freq { $this->rules['bymonthday'] = date('d', $this->start); } } - + + //set until, and cache if( isset($this->rules['count']) ) { $ts = $this->firstOccurrence(); + $this->cache[0] = $ts; for($i=1; $i<$this->rules['count']; $i++) { $ts = $this->findNext($ts); + $this->cache[$i] = $ts; } $this->rules['until'] = $ts; } @@ -82,14 +87,24 @@ class SG_iCal_Freq { * @return int */ public function previousOccurrence( $offset ) { - $t1 = $this->start; - while( ($t2 = $this->findNext($t1)) < $offset) { - if( $t2 == false ){ - break; + if (!empty($this->cache)) { + reset($this->cache); + while( ($t2 = next($this->cache)) < $offset) { + if( $t2 == false ){ + break; + } + $ts = $t2; + } + } else { + $ts = $this->start; + while( ($t2 = $this->findNext($ts)) < $offset) { + if( $t2 == false ){ + break; + } + $ts = $t2; } - $t1 = $t2; } - return $t1; + return $ts; } /** @@ -116,9 +131,14 @@ class SG_iCal_Freq { * @return int timestamp */ public function lastOccurrence() { + if (!empty($this->cache)) { + return end($this->cache); + } + $i=0; $this->cache[0] = $this->start; $ts = $next = $this->findNext($this->start); while ($next) { $ts = $next; + $i++; $this->cache[$i] = $ts; $next = $this->findNext($ts); } return $ts; From 642115b1ccbced54ac4c97dd35f235738b0deed6 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 21:31:49 +0200 Subject: [PATCH 32/44] new getAllOccurrences() method which also set cache, now use cache in findNext() --- helpers/SG_iCal_Freq.php | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index f02071e..b70a292 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -80,6 +80,24 @@ class SG_iCal_Freq { } } + + /** + * Returns all timestamps array(), build the cache if not made before + * @return array + */ + public function getAllOccurrences() { + if (empty($this->cache)) { + //build cache + $i=0; $this->cache[0] = $this->start; + $next = $this->findNext($this->start); + while ($next) { + $i++; $this->cache[$i] = $next; + $next = $this->findNext($next); + } + } + return $this->cache; + } + /** * Returns the previous (most recent) occurrence of the rule from the * given offset @@ -88,12 +106,11 @@ class SG_iCal_Freq { */ public function previousOccurrence( $offset ) { if (!empty($this->cache)) { - reset($this->cache); - while( ($t2 = next($this->cache)) < $offset) { - if( $t2 == false ){ - break; - } - $ts = $t2; + $t2=$this->cache[0]; + foreach($this->cache as $ts) { + if ($ts >= $offset) + return $t2; + $t2 = $ts; } } else { $ts = $this->start; @@ -168,6 +185,14 @@ class SG_iCal_Freq { * @return int */ public function findNext($offset) { + + if (!empty($this->cache)) { + foreach($this->cache as $ts) { + if ($ts > $offset) + return $ts; + } + } + $debug = false; //make sure the offset is valid From 5743c6221e9a18094c8083575ec37164d3c18de3 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 22:44:07 +0200 Subject: [PATCH 33/44] implement EXDATE exclusions --- blocks/SG_iCal_VEvent.php | 19 ++++++++++---- helpers/SG_iCal_Freq.php | 45 ++++++++++++++++++++-------------- helpers/SG_iCal_Parser.php | 9 +++++-- helpers/SG_iCal_Recurrence.php | 1 - 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index 7b4fa69..c5f5eda 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -26,6 +26,7 @@ class SG_iCal_VEvent { protected $location; public $recurrence; + public $excluded; public $freq; //getFrequency() public $data; @@ -37,20 +38,28 @@ class SG_iCal_VEvent { * @param SG_iCalReader $ical */ public function __construct($data, SG_iCal $ical) { - + $this->uid = $data['uid']->getData(); unset($data['uid']); if ( isset($data['rrule']) ) { $this->recurrence = new SG_iCal_Recurrence($data['rrule']); unset($data['rrule']); + + //exclusions + if ( isset($data['exdate']) ) { + foreach ($data['exdate'] as $exdate) { + $this->excluded[] = $this->getTimestamp($exdate, $ical); + } + unset($data['exdate']); + } } if( isset($data['dtstart']) ) { $this->start = $this->getTimestamp($data['dtstart'], $ical); unset($data['dtstart']); } - + if( isset($data['dtend']) ) { $this->end = $this->getTimestamp($data['dtend'], $ical); unset($data['dtend']); @@ -59,7 +68,7 @@ class SG_iCal_VEvent { $this->end = $this->start + $dur->getDuration(); unset($data['duration']); } - + //google cal set dtend as end of initial event (duration) if ( isset($this->recurrence) ) { //if there is a recurrence rule @@ -70,7 +79,7 @@ class SG_iCal_VEvent { //ok.. } elseif ($count) { //if count is set, then figure out the last occurrence and set that as the end date - $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $start); + $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $start, $this->excluded); $until = $this->freq->lastOccurrence($this->start); } else { //forever... limit to 3 years @@ -105,7 +114,7 @@ class SG_iCal_VEvent { public function getFrequency() { if (! isset($this->freq)) { if ( isset($this->recurrence) ) { - $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $this->start); + $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $this->start, $this->excluded); } } return $this->freq; diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index b70a292..3c9d484 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -31,6 +31,7 @@ class SG_iCal_Freq { protected $rules = array('freq'=>'yearly', 'interval'=>1); protected $start = 0; protected $freq = ''; + protected $excluded; public $cache; @@ -39,8 +40,9 @@ class SG_iCal_Freq { * @param $rule string * @param $start int Unix-timestamp (important : Need to be the start of Event) */ - public function __construct( $rule, $start ) { + public function __construct( $rule, $start, $excluded=array()) { $this->start = $start; + $this->excluded = $excluded; $rules = array(); foreach( explode(';', $rule) AS $v) { @@ -53,7 +55,6 @@ class SG_iCal_Freq { } $this->freq = strtolower($this->rules['freq']); - foreach( $this->knownRules AS $rule ) { if( isset($this->rules['by' . $rule]) ) { if( $this->isPrerule($rule, $this->freq) ) { @@ -68,13 +69,11 @@ class SG_iCal_Freq { } } - //set until, and cache + //set until if( isset($this->rules['count']) ) { $ts = $this->firstOccurrence(); - $this->cache[0] = $ts; for($i=1; $i<$this->rules['count']; $i++) { $ts = $this->findNext($ts); - $this->cache[$i] = $ts; } $this->rules['until'] = $ts; } @@ -88,10 +87,12 @@ class SG_iCal_Freq { public function getAllOccurrences() { if (empty($this->cache)) { //build cache - $i=0; $this->cache[0] = $this->start; + unset($this->cache); + $this->cache[] = $this->start; $next = $this->findNext($this->start); while ($next) { - $i++; $this->cache[$i] = $next; + //if (!in_array($next, $this->excluded)) + $this->cache[] = $next; $next = $this->findNext($next); } } @@ -106,7 +107,7 @@ class SG_iCal_Freq { */ public function previousOccurrence( $offset ) { if (!empty($this->cache)) { - $t2=$this->cache[0]; + $t2=$this->start; foreach($this->cache as $ts) { if ($ts >= $offset) return $t2; @@ -151,11 +152,9 @@ class SG_iCal_Freq { if (!empty($this->cache)) { return end($this->cache); } - $i=0; $this->cache[0] = $this->start; $ts = $next = $this->findNext($this->start); while ($next) { $ts = $next; - $i++; $this->cache[$i] = $ts; $next = $this->findNext($ts); } return $ts; @@ -185,7 +184,6 @@ class SG_iCal_Freq { * @return int */ public function findNext($offset) { - if (!empty($this->cache)) { foreach($this->cache as $ts) { if ($ts > $offset) @@ -214,10 +212,11 @@ class SG_iCal_Freq { if( $this->simpleMode ) { if( $offset < $t ) { return $t; - } - $next = $this->findStartingPoint( $t, $this->rules['interval'], false ); - if( !$this->validDate( $next ) ) { - return $this->findNext($next); + } else { + $next = $this->findStartingPoint( $t, $this->rules['interval'], false ); + if( !$this->validDate( $next ) ) { + return $this->findNext($next); + } } return $next; } @@ -250,19 +249,23 @@ class SG_iCal_Freq { } if( $offset < $this->start && $this->start < $t ) { - return $this->start; + $ts = $this->start; } else if( $found && ($t != $offset)) { if( $this->validDate( $t ) ) { if($debug) echo 'OK' . "\n"; - return $t; + $ts = $t; } else { if($debug) echo 'Invalid' . "\n"; - return $this->findNext($t); + $ts = $this->findNext($t); } } else { if($debug) echo 'Not found' . "\n"; - return $this->findNext( $this->findStartingPoint( $offset, $this->rules['interval'] ) ); + $ts = $this->findNext( $this->findStartingPoint( $offset, $this->rules['interval'] ) ); } + if ($ts && in_array($ts, $this->excluded)) + return $this->findNext($ts); + + return $ts; } /** @@ -454,6 +457,10 @@ class SG_iCal_Freq { if( isset($this->rules['until']) && $t > $this->rules['until'] ) { return false; } + + if (in_array($t, $this->excluded)) { + return false; + } if( isset($this->rules['bymonth']) ) { $months = explode(',', $this->rules['bymonth']); diff --git a/helpers/SG_iCal_Parser.php b/helpers/SG_iCal_Parser.php index ca02896..429f7ea 100755 --- a/helpers/SG_iCal_Parser.php +++ b/helpers/SG_iCal_Parser.php @@ -119,8 +119,13 @@ class SG_iCal_Parser { if( array_search($s, $sections) !== false ) { // This section is in the main section if( $section == $s ) { - // It _is_ the main section - $current_data[$s][$line->getIdent()] = $line; + // It _is_ the main section else + if ($line->getIdent() != "exdate") + $current_data[$s][$line->getIdent()] = $line; + else { + //exdate could appears more that once + $current_data[$s][$line->getIdent()][] = $line; + } } else { // Sub section $current_data[$s][$section][$line->getIdent()] = $line; diff --git a/helpers/SG_iCal_Recurrence.php b/helpers/SG_iCal_Recurrence.php index bcc05ac..a80d1db 100755 --- a/helpers/SG_iCal_Recurrence.php +++ b/helpers/SG_iCal_Recurrence.php @@ -42,7 +42,6 @@ class SG_iCal_Recurrence { 'byyearday', 'byyearno', 'bymonth', 'bysetpos' ); - /** * Creates an recurrence object with a passed in line. Parses the line. * @param object $line an SG_iCal_Line object which will be parsed to get the From de4a9e8fe8ff9c479fe3805c43ec08ba6e38ba46 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 22:47:53 +0200 Subject: [PATCH 34/44] v0.7.0 --- SG_iCal.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SG_iCal.php b/SG_iCal.php index 2e5bf3c..298533a 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -1,6 +1,6 @@ Date: Sat, 30 Oct 2010 23:22:16 +0200 Subject: [PATCH 35/44] README v0.7.0 --- README | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..b0dc7c3 --- /dev/null +++ b/README @@ -0,0 +1,17 @@ +About this fork (forked at v0.5.0 from http://github.com/fangel/SG-iCalendar) + +Changelog : + +0.7.0-tpr (30 oct 2010) + + ical EXDATE support (excluded dates in a range) + + $event->isWholeDay() + + getAllOccurrences() for repeated events + + implemented a cache for repeated events + +0.6.0-tpr (29 oct 2010) + + Added demo based on fullcalendar + + Added duration unit tests + + Support of Recurrent events in query Between() + * various fixes on actual (5) issues + +Fixed From 1a63569868ad5fb8d946a1f54acb6b93778798ff Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sat, 30 Oct 2010 23:56:37 +0200 Subject: [PATCH 36/44] fix and reenable cache creation when possible --- helpers/SG_iCal_Freq.php | 47 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 3c9d484..5be7eef 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -31,8 +31,9 @@ class SG_iCal_Freq { protected $rules = array('freq'=>'yearly', 'interval'=>1); protected $start = 0; protected $freq = ''; - protected $excluded; - + + protected $excluded; //EXDATE + public $cache; /** @@ -69,13 +70,16 @@ class SG_iCal_Freq { } } - //set until + //set until, and cache if( isset($this->rules['count']) ) { - $ts = $this->firstOccurrence(); - for($i=1; $i<$this->rules['count']; $i++) { + $ts = $this->start; + $cache[0] = $ts; + for($n=1; $n < $this->rules['count']; $n++) { $ts = $this->findNext($ts); + $cache[$n] = $ts; } $this->rules['until'] = $ts; + $this->cache = $cache; } } @@ -87,14 +91,13 @@ class SG_iCal_Freq { public function getAllOccurrences() { if (empty($this->cache)) { //build cache - unset($this->cache); - $this->cache[] = $this->start; + $n=0; $cache[$n] = $this->start; $next = $this->findNext($this->start); while ($next) { - //if (!in_array($next, $this->excluded)) - $this->cache[] = $next; + $n++; $cache[$n] = $next; $next = $this->findNext($next); } + $this->cache = $cache; } return $this->cache; } @@ -146,18 +149,14 @@ class SG_iCal_Freq { /** * Finds the absolute last occurrence of the rule from the given offset. + * Builds also the cache, if not set before... * @return int timestamp */ public function lastOccurrence() { - if (!empty($this->cache)) { - return end($this->cache); - } - $ts = $next = $this->findNext($this->start); - while ($next) { - $ts = $next; - $next = $this->findNext($ts); - } - return $ts; + //build cache if not done + $this->getAllOccurrences(); + //return last timestamp in cache + return end($this->cache); } /** @@ -211,14 +210,16 @@ class SG_iCal_Freq { if( $this->simpleMode ) { if( $offset < $t ) { - return $t; + $ts = $t; + if ($ts && in_array($ts, $this->excluded)) + $ts = $this->findNext($ts); } else { - $next = $this->findStartingPoint( $t, $this->rules['interval'], false ); - if( !$this->validDate( $next ) ) { - return $this->findNext($next); + $ts = $this->findStartingPoint( $t, $this->rules['interval'], false ); + if( !$this->validDate( $ts ) ) { + $ts = $this->findNext($ts); } } - return $next; + return $ts; } $eop = $this->findEndOfPeriod($offset); From d1561547d89fda59e7f35c22696a62e36a7cf4e7 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 00:23:00 +0200 Subject: [PATCH 37/44] some tests for cache --- tests/helpers/FreqTest.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/helpers/FreqTest.php b/tests/helpers/FreqTest.php index 9cea3b9..7d82505 100755 --- a/tests/helpers/FreqTest.php +++ b/tests/helpers/FreqTest.php @@ -605,16 +605,14 @@ class FreqTest extends PHPUnit_Framework_TestCase { } } - - //weird : in this test $start is not an occurrence ! - /* - public function testFirstOccurrenceByYearDay() { + //weird : in this test $start is not a matched occurrence but... + public function testFirstOccurrencesByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2009-10-27T090000'); $freq = new SG_iCal_Freq($rule, $start); - $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->firstOccurrence()); + $this->assertEquals(strtotime('2009-10-27T09:00:00'), $freq->firstOccurrence()); + $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->nextOccurrence($start)); } - */ public function testLastOccurrenceByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; @@ -623,6 +621,15 @@ class FreqTest extends PHPUnit_Framework_TestCase { $this->assertEquals(strtotime('2019-01-01T09:00:00'), $freq->lastOccurrence()); } + public function testCacheCount() { + $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; + $start = strtotime('2011-01-01T090000'); + $freq = new SG_iCal_Freq($rule, $start); + $this->assertEquals(5, count($freq->getAllOccurrences())); + $this->assertEquals(5, count($freq->getAllOccurrences())); + $this->assertEquals(strtotime('2019-01-01T09:00:00'), $freq->lastOccurrence()); + } + // TODO: WKST rule From ff9c8e202c8d90194073d855168b7f0d343060af Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 15:29:19 +0100 Subject: [PATCH 38/44] updated README --- README | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README b/README index b0dc7c3..4235ffc 100644 --- a/README +++ b/README @@ -1,5 +1,20 @@ +A simple and fast iCal parser. +http://github.com/tpruvot/PHP-iCal + About this fork (forked at v0.5.0 from http://github.com/fangel/SG-iCalendar) +A simple example : + $ical = new SG_iCalReader( "./basic.ics" ); + //or + $ical = new SG_iCalReader( "http://example.com/calendar.ics" ); + foreach( $ical->getEvents() As $event ) { + // Do stuff with the event $event + } + +To check unit tests with phpunit, goto tests/ directory and : + phpunit AllTests + phpunit helpers/FreqTest + Changelog : 0.7.0-tpr (30 oct 2010) @@ -14,4 +29,3 @@ Changelog : + Support of Recurrent events in query Between() * various fixes on actual (5) issues -Fixed From 30d1dfa4a978dfa96c6421dfa4451964b6f3d70a Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 16:23:56 +0100 Subject: [PATCH 39/44] fix excluded first dates --- helpers/SG_iCal_Freq.php | 5 ++++- helpers/SG_iCal_Recurrence.php | 3 ++- tests/helpers/FreqTest.php | 31 +++++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 5be7eef..67e59de 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -144,7 +144,10 @@ class SG_iCal_Freq { * @return int timestamp */ public function firstOccurrence() { - return $this->start; + $t = $this->start; + if (in_array($t, $this->excluded)) + $t = $this->findNext($t); + return $t; } /** diff --git a/helpers/SG_iCal_Recurrence.php b/helpers/SG_iCal_Recurrence.php index a80d1db..29e6ab8 100755 --- a/helpers/SG_iCal_Recurrence.php +++ b/helpers/SG_iCal_Recurrence.php @@ -57,10 +57,11 @@ class SG_iCal_Recurrence { * @param string $line the line to be parsed */ protected function parseLine($line) { + $this->rrule = $line; + //split up the properties $recurProperties = explode(';', $line); $recur = array(); - $this->rrule = $line; //loop through the properties in the line and set their associated //member variables diff --git a/tests/helpers/FreqTest.php b/tests/helpers/FreqTest.php index 7d82505..26ad5f4 100755 --- a/tests/helpers/FreqTest.php +++ b/tests/helpers/FreqTest.php @@ -605,7 +605,15 @@ class FreqTest extends PHPUnit_Framework_TestCase { } } - //weird : in this test $start is not a matched occurrence but... + /* + weird : in this test $start is not a matched occurrence but... + + to do something like that, we need EXDATE : + DTSTART;TZID=US-Eastern:19970902T090000 + EXDATE;TZID=US-Eastern:19970902T090000 + RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 + */ + public function testFirstOccurrencesByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2009-10-27T090000'); @@ -614,6 +622,13 @@ class FreqTest extends PHPUnit_Framework_TestCase { $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->nextOccurrence($start)); } + public function testFirstOccurrencesByYearDayWithoutFirstDate() { + $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; + $start = strtotime('2009-10-27T090000'); + $freq = new SG_iCal_Freq($rule, $start, array($start)); + $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->firstOccurrence()); + } + public function testLastOccurrenceByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2011-01-01T090000'); @@ -626,13 +641,21 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('2011-01-01T090000'); $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(5, count($freq->getAllOccurrences())); - $this->assertEquals(5, count($freq->getAllOccurrences())); $this->assertEquals(strtotime('2019-01-01T09:00:00'), $freq->lastOccurrence()); } - - // TODO: WKST rule + /* TODO: BYSETPOS rule : + The 3rd instance into the month of one of Tuesday, Wednesday or + Thursday, for the next 3 months: + DTSTART;TZID=US-Eastern:19970904T090000 + RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 + */ + + /* TODO: WKST rule + */ + + //check a serie of dates private function assertRule( $rule, $start, $dateset ) { $freq = new SG_iCal_Freq($rule, $start); reset($dateset); From b7c63515040265e1c4376c599d3777763e9f8c31 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 16:59:40 +0100 Subject: [PATCH 40/44] spaces cleanup --- blocks/SG_iCal_VCalendar.php | 10 ++-- blocks/SG_iCal_VEvent.php | 50 +++++++++--------- blocks/SG_iCal_VTimeZone.php | 26 +++++----- helpers/SG_iCal_Freq.php | 10 ++-- helpers/SG_iCal_Line.php | 38 +++++++------- helpers/SG_iCal_Parser.php | 24 ++++----- helpers/SG_iCal_Query.php | 14 +++--- helpers/SG_iCal_Recurrence.php | 7 +-- tests/helpers/FreqTest.php | 92 +++++++++++++++++----------------- 9 files changed, 136 insertions(+), 135 deletions(-) diff --git a/blocks/SG_iCal_VCalendar.php b/blocks/SG_iCal_VCalendar.php index 46c3bbd..1cdafd6 100755 --- a/blocks/SG_iCal_VCalendar.php +++ b/blocks/SG_iCal_VCalendar.php @@ -11,16 +11,16 @@ */ class SG_iCal_VCalendar implements IteratorAggregate { protected $data; - + /** * Creates a new SG_iCal_VCalendar. */ public function __construct($data) { $this->data = $data; } - + /** - * Returns the title of the calendar. If no title is known, NULL + * Returns the title of the calendar. If no title is known, NULL * will be returned * @return string */ @@ -31,7 +31,7 @@ class SG_iCal_VCalendar implements IteratorAggregate { return null; } } - + /** * Returns the description of the calendar. If no description is * known, NULL will be returned. @@ -44,7 +44,7 @@ class SG_iCal_VCalendar implements IteratorAggregate { return null; } } - + /** * @see IteratorAggregate.getIterator() */ diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index c5f5eda..e5ec782 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -1,12 +1,12 @@ recurrence = new SG_iCal_Recurrence($data['rrule']); unset($data['rrule']); - + //exclusions if ( isset($data['exdate']) ) { foreach ($data['exdate'] as $exdate) { @@ -98,15 +98,15 @@ class SG_iCal_VEvent { unset($data[$import]); } } - + if( isset($this->previous_tz) ) { date_default_timezone_set($this->previous_tz); } - + $this->data = SG_iCal_Line::Remove_Line($data); } - - + + /** * Returns the Event Occurrences Iterator (if recurrence set) * @return SG_iCal_Freq @@ -119,7 +119,7 @@ class SG_iCal_VEvent { } return $this->freq; } - + /** * Returns the UID of the event * @return string @@ -127,7 +127,7 @@ class SG_iCal_VEvent { public function getUID() { return $this->uid; } - + /** * Returns the summary (or null if none is given) of the event * @return string @@ -135,7 +135,7 @@ class SG_iCal_VEvent { public function getSummary() { return $this->summary; } - + /** * Returns the description (or null if none is given) of the event * @return string @@ -143,7 +143,7 @@ class SG_iCal_VEvent { public function getDescription() { return $this->description; } - + /** * Returns the location (or null if none is given) of the event * @return string @@ -151,7 +151,7 @@ class SG_iCal_VEvent { public function getLocation() { return $this->location; } - + /** * Returns true if the event is blocking (ie not transparent) * @return bool @@ -159,7 +159,7 @@ class SG_iCal_VEvent { public function isBlocking() { return !(isset($this->data['transp']) && $this->data['transp'] == 'TRANSPARENT'); } - + /** * Returns true if the event is confirmed * @return bool @@ -171,7 +171,7 @@ class SG_iCal_VEvent { return $this->data['status'] == 'CONFIRMED'; } } - + /** * Returns the timestamp for the beginning of the event * @return int @@ -179,7 +179,7 @@ class SG_iCal_VEvent { public function getStart() { return $this->start; } - + /** * Returns the timestamp for the end of the event * @return int @@ -195,7 +195,7 @@ class SG_iCal_VEvent { public function getRangeEnd() { return max($this->end,$this->lastend); } - + /** * Returns the duration of this event in seconds * @return int @@ -203,7 +203,7 @@ class SG_iCal_VEvent { public function getDuration() { return $this->end - $this->start; } - + /** * Returns true if duration is multiple of 86400 * @return bool @@ -215,7 +215,7 @@ class SG_iCal_VEvent { } return false; } - + /** * Returns the given property of the event. * @param string $prop @@ -230,9 +230,9 @@ class SG_iCal_VEvent { return null; } } - - - + + + /** * Set default timezone (temporary) to get timestamps * @return string @@ -248,14 +248,14 @@ class SG_iCal_VEvent { } return false; } - + /** * Calculates the timestamp from a DT line. * @param $line SG_iCal_Line * @return int */ protected function getTimestamp( SG_iCal_Line $line, SG_iCal $ical ) { - + if( isset($line['tzid']) ) { $this->setLineTimeZone($line); //$tz = $ical->getTimeZoneInfo($line['tzid']); diff --git a/blocks/SG_iCal_VTimeZone.php b/blocks/SG_iCal_VTimeZone.php index b60c9bf..6b587ba 100755 --- a/blocks/SG_iCal_VTimeZone.php +++ b/blocks/SG_iCal_VTimeZone.php @@ -13,27 +13,27 @@ class SG_iCal_VTimeZone { protected $daylight; protected $standard; protected $cache = array(); - + /** * Constructs a new SG_iCal_VTimeZone */ public function __construct( $data ) { require_once dirname(__FILE__).'/../helpers/SG_iCal_Freq.php'; // BUILD: Remove line - + $this->tzid = $data['tzid']; $this->daylight = $data['daylight']; $this->standard = $data['standard']; } - + /** - * Returns the timezone-id for this timezone. (Used to + * Returns the timezone-id for this timezone. (Used to * differentiate between different tzs in a calendar) * @return string */ public function getTimeZoneId() { return $this->tzid; } - + /** * Returns the given offset in this timezone for the given * timestamp. (eg +0200) @@ -44,7 +44,7 @@ class SG_iCal_VTimeZone { $act = $this->getActive($ts); return $this->{$act}['tzoffsetto']; } - + /** * Returns the timezone name for the given timestamp (eg CEST) * @param int $ts @@ -54,7 +54,7 @@ class SG_iCal_VTimeZone { $act = $this->getActive($ts); return $this->{$act}['tzname']; } - + /** * Determines which of the daylight or standard is the active * setting. @@ -65,20 +65,20 @@ class SG_iCal_VTimeZone { * @return string standard|daylight */ private function getActive( $ts ) { - + if (class_exists('DateTimeZone')) { - + //PHP >= 5.2 $tz = new DateTimeZone( $this->tzid ); $date = new DateTime("@$ts", $tz); return ($date->format('I') == 1) ? 'daylight' : 'standard'; - + } else { - + if( isset($this->cache[$ts]) ) { return $this->cache[$ts]; } - + $daylight_freq = new SG_iCal_Freq($this->daylight['rrule'], strtotime($this->daylight['dtstart'])); $standard_freq = new SG_iCal_Freq($this->standard['rrule'], strtotime($this->standard['dtstart'])); $last_standard = $standard_freq->previousOccurrence($ts); @@ -88,7 +88,7 @@ class SG_iCal_VTimeZone { } else { $this->cache[$ts] = 'standard'; } - + return $this->cache[$ts]; } } diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 67e59de..69a4801 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -69,7 +69,7 @@ class SG_iCal_Freq { $this->rules['bymonthday'] = date('d', $this->start); } } - + //set until, and cache if( isset($this->rules['count']) ) { $ts = $this->start; @@ -83,7 +83,7 @@ class SG_iCal_Freq { } } - + /** * Returns all timestamps array(), build the cache if not made before * @return array @@ -192,7 +192,7 @@ class SG_iCal_Freq { return $ts; } } - + $debug = false; //make sure the offset is valid @@ -268,7 +268,7 @@ class SG_iCal_Freq { } if ($ts && in_array($ts, $this->excluded)) return $this->findNext($ts); - + return $ts; } @@ -461,7 +461,7 @@ class SG_iCal_Freq { if( isset($this->rules['until']) && $t > $this->rules['until'] ) { return false; } - + if (in_array($t, $this->excluded)) { return false; } diff --git a/helpers/SG_iCal_Line.php b/helpers/SG_iCal_Line.php index 10aaa1b..32c6a92 100755 --- a/helpers/SG_iCal_Line.php +++ b/helpers/SG_iCal_Line.php @@ -4,7 +4,7 @@ * A class for storing a single (complete) line of the iCal file. * Will find the line-type, the arguments and the data of the file and * store them. - * + * * The line-type can be found by querying getIdent(), data via either * getData() or typecasting to a string. * Params can be access via the ArrayAccess. A iterator is also avilable @@ -18,9 +18,9 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { protected $ident; protected $data; protected $params = array(); - + protected $replacements = array('from'=>array('\\,', '\\n', '\\;', '\\:', '\\"'), 'to'=>array(',', "\n", ';', ':', '"')); - + /** * Constructs a new line. */ @@ -28,21 +28,21 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { $split = strpos($line, ':'); $idents = explode(';', substr($line, 0, $split)); $ident = strtolower(array_shift($idents)); - + $data = trim(substr($line, $split+1)); $data = str_replace($this->replacements['from'], $this->replacements['to'], $data); - + $params = array(); foreach( $idents AS $v) { list($k, $v) = explode('=', $v); $params[ strtolower($k) ] = $v; } - + $this->ident = $ident; - $this->params = $params; + $this->params = $params; $this->data = $data; } - + /** * Is this line the begining of a new block? * @return bool @@ -50,7 +50,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function isBegin() { return $this->ident == 'begin'; } - + /** * Is this line the end of a block? * @return bool @@ -58,7 +58,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function isEnd() { return $this->ident == 'end'; } - + /** * Returns the line-type (ident) of the line * @return string @@ -66,7 +66,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function getIdent() { return $this->ident; } - + /** * Returns the content of the line * @return string @@ -74,7 +74,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function getData() { return $this->data; } - + /** * A static helper to get a array of SG_iCal_Line's, and calls * getData() on each of them to lay the data "bare".. @@ -95,14 +95,14 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { } return $rtn; } - + /** * @see ArrayAccess.offsetExists */ public function offsetExists( $param ) { return isset($this->params[ strtolower($param) ]); } - + /** * @see ArrayAccess.offsetGet */ @@ -112,7 +112,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { return $this->params[ $index ]; } } - + /** * Disabled ArrayAccess requirement * @see ArrayAccess.offsetSet @@ -120,7 +120,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function offsetSet( $param, $val ) { return false; } - + /** * Disabled ArrayAccess requirement * @see ArrayAccess.offsetUnset @@ -128,7 +128,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function offsetUnset( $param ) { return false; } - + /** * toString method. * @see getData() @@ -136,14 +136,14 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function __toString() { return $this->getData(); } - + /** * @see Countable.count */ public function count() { return count($this->params); } - + /** * @see IteratorAggregate.getIterator */ diff --git a/helpers/SG_iCal_Parser.php b/helpers/SG_iCal_Parser.php index 429f7ea..81f1a2a 100755 --- a/helpers/SG_iCal_Parser.php +++ b/helpers/SG_iCal_Parser.php @@ -11,7 +11,7 @@ class SG_iCal_Parser { $content = self::UnfoldLines($content); self::_Parse( $content, $ical ); } - + /** * Passes a text string on to be parsed * @param string $content @@ -21,7 +21,7 @@ class SG_iCal_Parser { $content = self::UnfoldLines($content); self::_Parse( $content, $ical ); } - + /** * Fetches a resource and tries to make sure it's UTF8 * encoded @@ -29,7 +29,7 @@ class SG_iCal_Parser { */ protected static function Fetch( $resource ) { $is_utf8 = true; - + if( is_file( $resource ) ) { // The resource is a local file $content = file_get_contents($resource); @@ -59,16 +59,16 @@ class SG_iCal_Parser { $is_utf8 = false; } } - + if( !$is_utf8 ) { $content = utf8_encode($content); } - + return $content; } - + /** - * Takes the string $content, and creates a array of iCal lines. + * Takes the string $content, and creates a array of iCal lines. * This includes unfolding multi-line entries into a single line. * @param $content string */ @@ -121,14 +121,14 @@ class SG_iCal_Parser { if( $section == $s ) { // It _is_ the main section else if ($line->getIdent() != "exdate") - $current_data[$s][$line->getIdent()] = $line; + $current_data[$s][$line->getIdent()] = $line; else { //exdate could appears more that once $current_data[$s][$line->getIdent()][] = $line; } } else { // Sub section - $current_data[$s][$section][$line->getIdent()] = $line; + $current_data[$s][$section][$line->getIdent()] = $line; } break; } @@ -160,10 +160,10 @@ class SG_iCal_Parser { } /** - * This functions does some regexp checking to see if the value is + * This functions does some regexp checking to see if the value is * valid UTF-8. * - * The function is from the book "Building Scalable Web Sites" by + * The function is from the book "Building Scalable Web Sites" by * Cal Henderson. * * @param string $data @@ -186,7 +186,7 @@ class SG_iCal_Parser { $rx .= '|^[\x80-\xBF]'; return ( ! (bool) preg_match('!'.$rx.'!', $data) ); - } + } } diff --git a/helpers/SG_iCal_Query.php b/helpers/SG_iCal_Query.php index e9e0bac..4d3db1e 100755 --- a/helpers/SG_iCal_Query.php +++ b/helpers/SG_iCal_Query.php @@ -25,7 +25,7 @@ class SG_iCal_Query { if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::Between called with invalid input!'); } - + $rtn = array(); foreach( $ical AS $e ) { if( ($start <= $e->getStart() && $e->getStart() < $end) @@ -35,10 +35,10 @@ class SG_iCal_Query { } return $rtn; } - + /** * Returns all events from the calendar after a given timestamp - * + * * @param SG_iCalReader|array $ical The calendar to query * @param int $start * @return SG_iCal_VEvent[] @@ -50,7 +50,7 @@ class SG_iCal_Query { if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::After called with invalid input!'); } - + $rtn = array(); foreach( $ical AS $e ) { if($e->getStart() >= $start || $e->getRangeEnd() >= $start) { @@ -59,10 +59,10 @@ class SG_iCal_Query { } return $rtn; } - + /** * Sorts the events from the calendar after the specified column. - * Column can be all valid entires that getProperty can return. + * Column can be all valid entires that getProperty can return. * So stuff like uid, start, end, summary etc. * @param SG_iCalReader|array $ical The calendar to query * @param string $column @@ -75,7 +75,7 @@ class SG_iCal_Query { if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::Sort called with invalid input!'); } - + $cmp = create_function('$a, $b', 'return strcmp($a->getProperty("' . $column . '"), $b->getProperty("' . $column . '"));'); usort($ical, $cmp); return $ical; diff --git a/helpers/SG_iCal_Recurrence.php b/helpers/SG_iCal_Recurrence.php index 29e6ab8..dd0ebb5 100755 --- a/helpers/SG_iCal_Recurrence.php +++ b/helpers/SG_iCal_Recurrence.php @@ -15,12 +15,12 @@ class SG_iCal_Recurrence { public $rrule; - + protected $freq; protected $until; protected $count; - + protected $interval; protected $bysecond; protected $byminute; @@ -31,6 +31,7 @@ class SG_iCal_Recurrence { protected $byyearno; protected $bymonth; protected $bysetpos; + protected $wkst; /** @@ -58,7 +59,7 @@ class SG_iCal_Recurrence { */ protected function parseLine($line) { $this->rrule = $line; - + //split up the properties $recurProperties = explode(';', $line); $recur = array(); diff --git a/tests/helpers/FreqTest.php b/tests/helpers/FreqTest.php index 26ad5f4..3b0877c 100755 --- a/tests/helpers/FreqTest.php +++ b/tests/helpers/FreqTest.php @@ -23,12 +23,12 @@ class FreqTest extends PHPUnit_Framework_TestCase { 873961200, -1 ); - + $rule = 'FREQ=DAILY;COUNT=10'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testDailyUntil() { $dateset = array( 873183600, @@ -43,15 +43,15 @@ class FreqTest extends PHPUnit_Framework_TestCase { 873961200, 874047600 ); - + $rule = 'FREQ=DAILY;UNTIL=19971224T000000Z'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(882864000, $freq->previousOccurrence(time())); } - + public function testDailyInterval() { $dateset = array( 873183600, @@ -66,7 +66,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testDailyIntervalCount() { $dateset = array( 873183600, @@ -80,7 +80,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testDailyBydayBymonthUntil() { $rules = array( 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA', @@ -106,22 +106,22 @@ class FreqTest extends PHPUnit_Framework_TestCase { 946972800 ) ); - + foreach( $rules As $rule ) { $start = strtotime('19980101T090000'); $this->assertRule( $rule, $start, $datesets[0]); - + $start = strtotime('+1 year', $start); $this->assertRule( $rule, $start, $datesets[1]); - + $start = strtotime('+1 year', $start); $this->assertRule( $rule, $start, $datesets[2]); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(949305600, $freq->previousOccurrence(time())); } } - + public function testWeeklyCount() { $dateset = array( 873183600, @@ -140,7 +140,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testWeeklyUntil() { $dateset = array( 873183600, @@ -158,11 +158,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $rule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(882864000, $freq->previousOccurrence(time()), 'Failed getting correct end date'); } - + public function testWeeklyBydayLimit() { $rules = array( 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH', @@ -186,7 +186,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $this->assertRule( $rule, $start, $dateset); } } - + public function testWeeklyIntervalUntilByday() { $dateset = array( 873183600, @@ -204,11 +204,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $rule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(882777600, $freq->previousOccurrence(time()), 'Failed getting correct end date'); } - + public function testWeeklyIntervalBydayCount() { $dateset = array( 873183600, @@ -225,7 +225,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBydayCount() { $dateset = array( 873442800, @@ -244,7 +244,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970905T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBydayUntil() { $dateset = array( 873442800, @@ -257,7 +257,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970905T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyIntervalBydayCount2() { $dateset = array( 873615600, @@ -291,7 +291,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970922T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBymonthday() { $dateset = array( 875430000, @@ -305,7 +305,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970928T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBymonthdayCount() { $dateset = array( 873183600, @@ -324,7 +324,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBymonthdayCount2() { $dateset = array( 875602800, @@ -343,7 +343,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970930T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyIntervalBymonthdayCount() { $dateset = array( 873874800, @@ -362,7 +362,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970910T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyIntervalByday() { $dateset = array( 873183600, @@ -380,7 +380,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyCountBymonth() { $dateset = array( 865926000, @@ -399,7 +399,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970610T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyIntervalCountBymonth() { $dateset = array( 857980800, @@ -418,7 +418,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970310T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyIntervalCountByyearday() { $dateset = array( 852105600, @@ -437,7 +437,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970101T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyByday() { $dateset = array( 864025200, @@ -448,7 +448,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970519T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyByweeknoByday() { $dateset = array( 863420400, @@ -509,7 +509,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyBydayBymonthday2() { $dateset = array( 874134000, @@ -527,7 +527,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970913T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyIntervalBymonthBydayBymonthday() { $dateset = array( 847180800, @@ -538,9 +538,9 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19961105T090000'); $this->assertRule( $rule, $start, $dateset); } - + // TODO: SETPOS rules - + public function testHourlyIntervalUntil() { $dateset = array( 873183600, @@ -552,7 +552,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMinutelyIntervalCount() { $dateset = array( 873183600, @@ -567,7 +567,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMinutelyIntervalCount2() { $dateset = array( 873183600, @@ -580,11 +580,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMinutelyIntervalByhour() { $rules = array( 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16'/*, - 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40'*/ + 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40'*/ ); // TODO: Fix it so multi byhour and byminute will work $dateset = array( @@ -607,13 +607,13 @@ class FreqTest extends PHPUnit_Framework_TestCase { /* weird : in this test $start is not a matched occurrence but... - + to do something like that, we need EXDATE : DTSTART;TZID=US-Eastern:19970902T090000 EXDATE;TZID=US-Eastern:19970902T090000 RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 */ - + public function testFirstOccurrencesByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2009-10-27T090000'); @@ -621,14 +621,14 @@ class FreqTest extends PHPUnit_Framework_TestCase { $this->assertEquals(strtotime('2009-10-27T09:00:00'), $freq->firstOccurrence()); $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->nextOccurrence($start)); } - + public function testFirstOccurrencesByYearDayWithoutFirstDate() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2009-10-27T090000'); $freq = new SG_iCal_Freq($rule, $start, array($start)); $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->firstOccurrence()); } - + public function testLastOccurrenceByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2011-01-01T090000'); @@ -647,14 +647,14 @@ class FreqTest extends PHPUnit_Framework_TestCase { /* TODO: BYSETPOS rule : The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months: - + DTSTART;TZID=US-Eastern:19970904T090000 RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 */ - + /* TODO: WKST rule */ - + //check a serie of dates private function assertRule( $rule, $start, $dateset ) { $freq = new SG_iCal_Freq($rule, $start); From b04943334a10427892ad78beb73114346ba89b88 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 17:03:05 +0100 Subject: [PATCH 41/44] multiple keys like exdate set in an array --- helpers/SG_iCal_Parser.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helpers/SG_iCal_Parser.php b/helpers/SG_iCal_Parser.php index 81f1a2a..2ddd99a 100755 --- a/helpers/SG_iCal_Parser.php +++ b/helpers/SG_iCal_Parser.php @@ -93,6 +93,7 @@ class SG_iCal_Parser { */ private static function _Parse( $content, SG_iCal $ical ) { $main_sections = array('vevent', 'vjournal', 'vtodo', 'vtimezone', 'vcalendar'); + $array_idents = array('exdate'); $sections = array(); $section = ''; $current_data = array(); @@ -120,11 +121,11 @@ class SG_iCal_Parser { // This section is in the main section if( $section == $s ) { // It _is_ the main section else - if ($line->getIdent() != "exdate") - $current_data[$s][$line->getIdent()] = $line; - else { + if (in_array($line->getIdent(), $array_idents)) //exdate could appears more that once $current_data[$s][$line->getIdent()][] = $line; + else { + $current_data[$s][$line->getIdent()] = $line; } } else { // Sub section From 8079db3ac38a61ea38b4bf1b736a08b8a5dcee82 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 17:19:19 +0100 Subject: [PATCH 42/44] add support for EXDATE arrays : EXDATE;VALUE=DATE:20100909,20100910 --- blocks/SG_iCal_VEvent.php | 20 ++++++++++++-------- helpers/SG_iCal_Line.php | 12 ++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index e5ec782..434655d 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -45,14 +45,6 @@ class SG_iCal_VEvent { if ( isset($data['rrule']) ) { $this->recurrence = new SG_iCal_Recurrence($data['rrule']); unset($data['rrule']); - - //exclusions - if ( isset($data['exdate']) ) { - foreach ($data['exdate'] as $exdate) { - $this->excluded[] = $this->getTimestamp($exdate, $ical); - } - unset($data['exdate']); - } } if( isset($data['dtstart']) ) { @@ -72,6 +64,18 @@ class SG_iCal_VEvent { //google cal set dtend as end of initial event (duration) if ( isset($this->recurrence) ) { //if there is a recurrence rule + + //exclusions + if ( isset($data['exdate']) ) { + foreach ($data['exdate'] as $exdate) { + //$this->excluded[] = $this->getTimestamp($exdate, $ical); + foreach ($exdate->getDataAsArray() as $ts) { + $this->excluded[] = strtotime($ts); + } + } + unset($data['exdate']); + } + $until = $this->recurrence->getUntil(); $count = $this->recurrence->getCount(); //check if there is either 'until' or 'count' set diff --git a/helpers/SG_iCal_Line.php b/helpers/SG_iCal_Line.php index 32c6a92..f386cd7 100755 --- a/helpers/SG_iCal_Line.php +++ b/helpers/SG_iCal_Line.php @@ -75,6 +75,18 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { return $this->data; } + /** + * Returns the content of the line + * @return string + */ + public function getDataAsArray() { + if (strpos($this->data,",") !== false) { + return explode(",",$this->data); + } + else + return array($this->data); + } + /** * A static helper to get a array of SG_iCal_Line's, and calls * getData() on each of them to lay the data "bare".. From 953662254d32714fa40142d98f7c806994a32924 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 17:44:08 +0100 Subject: [PATCH 43/44] updated demo code --- demo/exdate.ics | 43 +++++++++++++++++++++++++++++++++++++++++++ demo/index.php | 16 ++++++++-------- 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 demo/exdate.ics diff --git a/demo/exdate.ics b/demo/exdate.ics new file mode 100644 index 0000000..1408dd3 --- /dev/null +++ b/demo/exdate.ics @@ -0,0 +1,43 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:uba.gestion@gmail.com +X-WR-TIMEZONE:Europe/Paris +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;VALUE=DATE:20100907 +DTEND;VALUE=DATE:20100908 +RRULE:FREQ=DAILY;UNTIL=20100912 +EXDATE;VALUE=DATE:20100911 +EXDATE;VALUE=DATE:20100909,20100910 +DTSTAMP:20101031T155459Z +UID:5oo2ridecth26kcavj8elhtd4s@google.com +CREATED:00001231T000000Z +DESCRIPTION: +LAST-MODIFIED:20101030T193954Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:occur +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/demo/index.php b/demo/index.php index f6b4f0c..10e8794 100644 --- a/demo/index.php +++ b/demo/index.php @@ -5,8 +5,8 @@ require_once('../SG_iCal.php'); function dump_t($x) { echo "
".print_r($x,true)."
"; } - -$ICS = "basic.ics"; +$ICS = "exdate.ics"; +//echo dump_t(file_get_contents($ICS)); $ical = new SG_iCalReader($ICS); $query = new SG_iCal_Query(); @@ -24,16 +24,15 @@ foreach($evts as $id => $ev) { "end" => $ev->getEnd()-1, "allDay" => $ev->isWholeDay() ); - - $data[] = $jsEvt; if (isset($ev->recurrence)) { - $count = 1; + $count = 0; $start = $ev->getStart(); - //$freq = new SG_iCal_Freq($ev->recurrence->rrule, $start); $freq = $ev->getFrequency(); + if ($freq->firstOccurrence() == $start) + $data[] = $jsEvt; while (($next = $freq->nextOccurrence($start)) > 0 ) { - if (!$next or $count >= 200) break; + if (!$next or $count >= 1000) break; $count++; $start = $next; $jsEvt["start"] = $start; @@ -41,7 +40,8 @@ foreach($evts as $id => $ev) { $data[] = $jsEvt; } - } + } else + $data[] = $jsEvt; } //echo(date('Ymd\n',$data[0][start])); From 1ae7b2978ebb6e339024a57d17ec6d305645cbc4 Mon Sep 17 00:00:00 2001 From: Tanguy Pruvot Date: Sun, 31 Oct 2010 20:06:16 +0100 Subject: [PATCH 44/44] RDATE support --- README | 27 +++++++++++++++-- blocks/SG_iCal_VEvent.php | 59 +++++++++++++++++++++++++------------- helpers/SG_iCal_Freq.php | 42 ++++++++++++++++++++------- helpers/SG_iCal_Parser.php | 2 +- 4 files changed, 96 insertions(+), 34 deletions(-) diff --git a/README b/README index 4235ffc..8c14257 100644 --- a/README +++ b/README @@ -1,7 +1,8 @@ A simple and fast iCal parser. +------------------------------------------------------------------------------- http://github.com/tpruvot/PHP-iCal - -About this fork (forked at v0.5.0 from http://github.com/fangel/SG-iCalendar) +fork from http://github.com/fangel/SG-iCalendar +------------------------------------------------------------------------------- A simple example : $ical = new SG_iCalReader( "./basic.ics" ); @@ -15,8 +16,14 @@ To check unit tests with phpunit, goto tests/ directory and : phpunit AllTests phpunit helpers/FreqTest -Changelog : +------------------------------------------------------------------------------- +CHANGELOG : +------------------------------------------------------------------------------- +current (31 oct 2010) + + ical RDATE support (added dates in a range) + + RDATE and EXDATE arrays support + 0.7.0-tpr (30 oct 2010) + ical EXDATE support (excluded dates in a range) + $event->isWholeDay() @@ -29,3 +36,17 @@ Changelog : + Support of Recurrent events in query Between() * various fixes on actual (5) issues +------------------------------------------------------------------------------- +TODO : +------------------------------------------------------------------------------- + +These iCal keywords are not supported for the moment : + - RECURRENCE-ID : to move one event from a recurrence + - EXRULE : to exclude multiple days by a complex rule + +Also, multiple RRULE could be specified for an event, +but that is not the case for most calendar applications + +------------------------------------------------------------------------------- +To get more information about ical format and rules : +see http://www.ietf.org/rfc/rfc2445.txt diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index 434655d..0e6c084 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -17,17 +17,23 @@ class SG_iCal_VEvent { const DEFAULT_CONFIRMED = true; protected $uid; + protected $start; protected $end; - protected $laststart; - protected $lastend; + protected $summary; protected $description; protected $location; - public $recurrence; - public $excluded; - public $freq; //getFrequency() + protected $laststart; + protected $lastend; + + public $recurrence; //RRULE + public $recurex; //EXRULE + public $excluded; //EXDATE(s) + public $added; //RDATE(s) + + public $freq; //getFrequency() SG_iCal_Freq public $data; @@ -47,6 +53,11 @@ class SG_iCal_VEvent { unset($data['rrule']); } + if ( isset($data['exrule']) ) { + $this->recurex = new SG_iCal_Recurrence($data['exrule']); + unset($data['exrule']); + } + if( isset($data['dtstart']) ) { $this->start = $this->getTimestamp($data['dtstart'], $ical); unset($data['dtstart']); @@ -68,13 +79,21 @@ class SG_iCal_VEvent { //exclusions if ( isset($data['exdate']) ) { foreach ($data['exdate'] as $exdate) { - //$this->excluded[] = $this->getTimestamp($exdate, $ical); foreach ($exdate->getDataAsArray() as $ts) { $this->excluded[] = strtotime($ts); } } unset($data['exdate']); } + //additions + if ( isset($data['rdate']) ) { + foreach ($data['rdate'] as $rdate) { + foreach ($rdate->getDataAsArray() as $ts) { + $this->added[] = strtotime($ts); + } + } + unset($data['rdate']); + } $until = $this->recurrence->getUntil(); $count = $this->recurrence->getCount(); @@ -83,7 +102,7 @@ class SG_iCal_VEvent { //ok.. } elseif ($count) { //if count is set, then figure out the last occurrence and set that as the end date - $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $start, $this->excluded); + $this->getFrequency(); $until = $this->freq->lastOccurrence($this->start); } else { //forever... limit to 3 years @@ -118,7 +137,7 @@ class SG_iCal_VEvent { public function getFrequency() { if (! isset($this->freq)) { if ( isset($this->recurrence) ) { - $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $this->start, $this->excluded); + $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $this->start, $this->excluded, $this->added); } } return $this->freq; @@ -176,6 +195,18 @@ class SG_iCal_VEvent { } } + /** + * Returns true if duration is multiple of 86400 + * @return bool + */ + public function isWholeDay() { + $dur = $this->getDuration(); + if ($dur > 0 && ($dur % 86400) == 0) { + return true; + } + return false; + } + /** * Returns the timestamp for the beginning of the event * @return int @@ -208,18 +239,6 @@ class SG_iCal_VEvent { return $this->end - $this->start; } - /** - * Returns true if duration is multiple of 86400 - * @return bool - */ - public function isWholeDay() { - $dur = $this->getDuration(); - if ($dur > 0 && ($dur % 86400) == 0) { - return true; - } - return false; - } - /** * Returns the given property of the event. * @param string $prop diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 69a4801..abb48c0 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -33,17 +33,20 @@ class SG_iCal_Freq { protected $freq = ''; protected $excluded; //EXDATE + protected $added; //RDATE - public $cache; + protected $cache; // getAllOccurrences() /** * Constructs a new Freqency-rule * @param $rule string * @param $start int Unix-timestamp (important : Need to be the start of Event) + * @param $excluded array of int (timestamps), see EXDATE documentation + * @param $added array of int (timestamps), see RDATE documentation */ - public function __construct( $rule, $start, $excluded=array()) { + public function __construct( $rule, $start, $excluded=array(), $added=array()) { $this->start = $start; - $this->excluded = $excluded; + $this->excluded = array(); $rules = array(); foreach( explode(';', $rule) AS $v) { @@ -72,15 +75,31 @@ class SG_iCal_Freq { //set until, and cache if( isset($this->rules['count']) ) { - $ts = $this->start; - $cache[0] = $ts; + + $cache[$ts] = $ts = $this->start; for($n=1; $n < $this->rules['count']; $n++) { $ts = $this->findNext($ts); - $cache[$n] = $ts; + $cache[$ts] = $ts; } $this->rules['until'] = $ts; - $this->cache = $cache; + + //EXDATE + if (!empty($excluded)) { + foreach($excluded as $ts) { + unset($cache[$ts]); + } + } + //RDATE + if (!empty($added)) { + $cache = $cache + $added; + asort($cache); + } + + $this->cache = array_values($cache); } + + $this->excluded = $excluded; + $this->added = $added; } @@ -91,12 +110,15 @@ class SG_iCal_Freq { public function getAllOccurrences() { if (empty($this->cache)) { //build cache - $n=0; $cache[$n] = $this->start; - $next = $this->findNext($this->start); + $next = $this->firstOccurrence(); while ($next) { - $n++; $cache[$n] = $next; + $cache[] = $next; $next = $this->findNext($next); } + if (!empty($this->added)) { + $cache = $cache + $this->added; + asort($cache); + } $this->cache = $cache; } return $this->cache; diff --git a/helpers/SG_iCal_Parser.php b/helpers/SG_iCal_Parser.php index 2ddd99a..5a68576 100755 --- a/helpers/SG_iCal_Parser.php +++ b/helpers/SG_iCal_Parser.php @@ -93,7 +93,7 @@ class SG_iCal_Parser { */ private static function _Parse( $content, SG_iCal $ical ) { $main_sections = array('vevent', 'vjournal', 'vtodo', 'vtimezone', 'vcalendar'); - $array_idents = array('exdate'); + $array_idents = array('exdate','rdate'); $sections = array(); $section = ''; $current_data = array();