--- /dev/null
+<?php
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package Horde_Date
+ *
+ * @TODO in format():
+ * http://php.net/intldateformatter
+ *
+ * @TODO on timezones:
+ * http://trac.agavi.org/ticket/1008
+ * http://trac.agavi.org/changeset/3659
+ *
+ * @TODO on switching to PHP::DateTime:
+ * The only thing ever stored in the database *IS* Unix timestamps. Doing
+ * anything other than that is unmanageable, yet some frameworks use 'server
+ * based' times in their systems, simply because they do not bother with
+ * daylight saving and only 'serve' one timezone!
+ *
+ * The second you have to manage 'real' time across timezones then daylight
+ * saving becomes essential, BUT only on the display side! Since the browser
+ * only provides a time offset, this is useless and to be honest should simply
+ * be ignored ( until it is upgraded to provide the correct information ;)
+ * ). So we need a 'display' function that takes a simple numeric epoch, and a
+ * separate timezone id into which the epoch is to be 'converted'. My W3C
+ * mapping works simply because ADOdb then converts that to it's own simple
+ * offset abbreviation - in my case GMT or BST. As long as DateTime passes the
+ * full 64 bit number the date range from 100AD is also preserved ( and
+ * further back if 2 digit years are disabled ). If I want to display the
+ * 'real' timezone with this 'time' then I just add it in place of ADOdb's
+ * 'timezone'. I am tempted to simply adjust the ADOdb class to take a
+ * timezone in place of the simple GMT switch it currently uses.
+ *
+ * The return path is just the reverse and simply needs to take the client
+ * display offset off prior to storage of the UTC epoch. SO we use
+ * DateTimeZone to get an offset value for the clients timezone and simply add
+ * or subtract this from a timezone agnostic display on the client end when
+ * entering new times.
+ *
+ *
+ * It's not really feasible to store dates in specific timezone, as most
+ * national/local timezones support DST - and that is a pain to support, as
+ * eg. sorting breaks when some timestamps get repeated. That's why it's
+ * usually better to store datetimes as either UTC datetime or plain unix
+ * timestamp. I usually go with the former - using database datetime type.
+ */
+class Horde_Date
+{
+ const DATE_SUNDAY = 0;
+ const DATE_MONDAY = 1;
+ const DATE_TUESDAY = 2;
+ const DATE_WEDNESDAY = 3;
+ const DATE_THURSDAY = 4;
+ const DATE_FRIDAY = 5;
+ const DATE_SATURDAY = 6;
+
+ const MASK_SUNDAY = 1;
+ const MASK_MONDAY = 2;
+ const MASK_TUESDAY = 4;
+ const MASK_WEDNESDAY = 8;
+ const MASK_THURSDAY = 16;
+ const MASK_FRIDAY = 32;
+ const MASK_SATURDAY = 64;
+ const MASK_WEEKDAYS = 62;
+ const MASK_WEEKEND = 65;
+ const MASK_ALLDAYS = 127;
+
+ const MASK_SECOND = 1;
+ const MASK_MINUTE = 2;
+ const MASK_HOUR = 4;
+ const MASK_DAY = 8;
+ const MASK_MONTH = 16;
+ const MASK_YEAR = 32;
+ const MASK_ALLPARTS = 63;
+
+ const DATE_DEFAULT = 'Y-m-d H:i:s';
+ const DATE_JSON = 'Y-m-d\TH:i:s';
+
+ /**
+ * Year
+ *
+ * @var integer
+ */
+ protected $_year;
+
+ /**
+ * Month
+ *
+ * @var integer
+ */
+ protected $_month;
+
+ /**
+ * Day
+ *
+ * @var integer
+ */
+ protected $_mday;
+
+ /**
+ * Hour
+ *
+ * @var integer
+ */
+ protected $_hour = 0;
+
+ /**
+ * Minute
+ *
+ * @var integer
+ */
+ protected $_min = 0;
+
+ /**
+ * Second
+ *
+ * @var integer
+ */
+ protected $_sec = 0;
+
+ /**
+ * String representation of the date's timezone.
+ *
+ * @var string
+ */
+ protected $_timezone;
+
+ /**
+ * Default format for __toString()
+ *
+ * @var string
+ */
+ protected $_defaultFormat = self::DATE_DEFAULT;
+
+ /**
+ * Default specs that are always supported.
+ * @var string
+ */
+ protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
+
+ /**
+ * Internally supported strftime() specifiers.
+ * @var string
+ */
+ protected static $_supportedSpecs = '';
+
+ /**
+ * Map of required correction masks.
+ *
+ * @see __set()
+ *
+ * @var array
+ */
+ protected static $_corrections = array(
+ 'year' => self::MASK_YEAR,
+ 'month' => self::MASK_MONTH,
+ 'mday' => self::MASK_DAY,
+ 'hour' => self::MASK_HOUR,
+ 'min' => self::MASK_MINUTE,
+ 'sec' => self::MASK_SECOND,
+ );
+
+ var $_formatCache = array();
+
+ /**
+ * Build a new date object. If $date contains date parts, use them to
+ * initialize the object.
+ *
+ * Recognized formats:
+ * - arrays with keys 'year', 'month', 'mday', 'day'
+ * 'hour', 'min', 'minute', 'sec'
+ * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+ * - yyyy-mm-dd hh:mm:ss
+ * - yyyymmddhhmmss
+ * - yyyymmddThhmmssZ
+ * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
+ * 03 Mar 1973)
+ * - unix timestamps
+ */
+ function __construct($date = null, $timezone = null)
+ {
+ if (!self::$_supportedSpecs) {
+ self::$_supportedSpecs = self::$_defaultSpecs;
+ if (function_exists('nl_langinfo')) { self::$_supportedSpecs .= 'bBpxX'; }
+ }
+
+ if (func_num_args() > 2) {
+ $args = func_get_args();
+ // Handle args in order: year month day hour min sec tz
+ $this->_initializeFromArgs(func_get_args());
+ return;
+ }
+
+ $this->_initializeTimezone($timezone);
+
+ if (is_null($date)) {
+ return;
+ }
+
+ if (is_string($date)) {
+ $date = trim($date, '"');
+ }
+
+ if (is_object($date)) {
+ $this->_initializeFromObject($date);
+ } elseif (is_array($date)) {
+ $this->_initializeFromArray($date);
+ } elseif (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(Z?)/', $date, $parts) &&
+ empty($parts[7])) {
+ $this->_year = (int)$parts[1];
+ $this->_month = (int)$parts[2];
+ $this->_mday = (int)$parts[3];
+ $this->_hour = (int)$parts[4];
+ $this->_min = (int)$parts[5];
+ $this->_sec = (int)$parts[6];
+ } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
+ $parts[2] > 0 && $parts[2] <= 12 &&
+ $parts[3] > 0 && $parts[3] <= 31) {
+ $this->_year = (int)$parts[1];
+ $this->_month = (int)$parts[2];
+ $this->_mday = (int)$parts[3];
+ $this->_hour = $this->_min = $this->_sec = 0;
+ } elseif ((string)(int)$date == $date) {
+ // Try as a timestamp.
+ $parts = @getdate($date);
+ if ($parts) {
+ $this->_year = $parts['year'];
+ $this->_month = $parts['mon'];
+ $this->_mday = $parts['mday'];
+ $this->_hour = $parts['hours'];
+ $this->_min = $parts['minutes'];
+ $this->_sec = $parts['seconds'];
+ }
+ } else {
+ try {
+ $date = new DateTime($date);
+ $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ } catch (Exception $e) {}
+ }
+ }
+
+ /**
+ * Returns a simple string representation of the date object
+ *
+ * @return string This object converted to a string.
+ */
+ function __toString()
+ {
+ try {
+ return $this->format($this->_defaultFormat);
+ } catch (Exception $e) {
+ return '';
+ }
+ }
+
+ /**
+ * Returns a DateTime object representing this object.
+ *
+ * @return DateTime
+ */
+ function toDateTime()
+ {
+ $date = new DateTime(null, new DateTimeZone($this->_timezone));
+ $date->setDate($this->_year, $this->_month, $this->_mday);
+ $date->setTime($this->_hour, $this->_min, $this->_sec);
+ return $date;
+ }
+
+ /**
+ * Getter for the date and time properties.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ *
+ * @return integer The property value, or null if not set.
+ */
+ function __get($name)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+
+ return $this->{'_' . $name};
+ }
+
+ /**
+ * Setter for the date and time properties.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ * @param integer $value The property value.
+ */
+ function __set($name, $value)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+
+ if ($name != 'year' && $name != 'month' && $name != 'mday' &&
+ $name != 'hour' && $name != 'min' && $name != 'sec') {
+ throw new InvalidArgumentException('Undefined property ' . $name);
+ }
+
+ $this->{'_' . $name} = $value;
+ $this->_correct(self::$_corrections[$name]);
+ $this->_formatCache = array();
+ }
+
+ /**
+ * Returns whether a date or time property exists.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ *
+ * @return boolen True if the property exists and is set.
+ */
+ function __isset($name)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+ return ($name == 'year' || $name == 'month' || $name == 'mday' ||
+ $name == 'hour' || $name == 'min' || $name == 'sec') &&
+ isset($this->{'_' . $name});
+ }
+
+ /**
+ * Add a number of seconds or units to this date, returning a new Date
+ * object.
+ */
+ public function add($factor)
+ {
+ $d = clone($this);
+ if (is_array($factor)) {
+ foreach ($factor as $property => $value) {
+ $d->$property += $value;
+ }
+ } else {
+ $d->sec += $factor;
+ }
+ return $d;
+ }
+
+ /**
+ * Subtract a number of seconds or units from this date, returning a new
+ * Horde_Date object.
+ */
+ public function sub($factor)
+ {
+ if (is_array($factor)) {
+ foreach ($factor as &$value) {
+ $value *= -1;
+ }
+ } else {
+ $factor *= -1;
+ }
+
+ return $this->add($factor);
+ }
+
+ /**
+ * Converts this object to a different timezone.
+ *
+ * @param string $timezone The new timezone.
+ */
+ public function setTimezone($timezone)
+ {
+ $date = $this->toDateTime();
+ $date->setTimezone(new DateTimeZone($timezone));
+ $this->_timezone = $timezone;
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ $this->_formatCache = array();
+ }
+
+ /**
+ * Set the default date format used in __toString()
+ *
+ * @param string $format
+ */
+ public function setDefaultFormat($format)
+ {
+ $this->_defaultFormat = $format;
+ }
+
+ /**
+ * Returns whether a year is a leap year.
+ *
+ * @static
+ *
+ * @param integer $year The year.
+ *
+ * @return boolan True if the year is a leap year.
+ */
+ function isLeapYear($year)
+ {
+ if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+ return false;
+ }
+
+ return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+ }
+
+ /**
+ * Returns the date of the year that corresponds to the first day of the
+ * given week.
+ *
+ * @param integer $week The week of the year to find the first day of.
+ * @param integer $year The year to calculate for.
+ *
+ * @return Horde_Date The date of the first day of the given week.
+ */
+ function firstDayOfWeek($week, $year)
+ {
+ return new Horde_Date(sprintf('%04dW%02d', $year, $week));
+ }
+
+ /**
+ * Returns the number of days in the specified month.
+ *
+ * @static
+ *
+ * @param integer $month The month
+ * @param integer $year The year.
+ *
+ * @return integer The number of days in the month.
+ */
+ function daysInMonth($month, $year)
+ {
+ static $cache = array();
+ if (!isset($cache[$year][$month])) {
+ $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
+ $cache[$year][$month] = $date->format('t');
+ }
+ return $cache[$year][$month];
+ }
+
+ /**
+ * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
+ *
+ * @return integer The day of the week.
+ */
+ function dayOfWeek()
+ {
+ if ($this->_month > 2) {
+ $month = $this->_month - 2;
+ $year = $this->_year;
+ } else {
+ $month = $this->_month + 10;
+ $year = $this->_year - 1;
+ }
+
+ $day = (floor((13 * $month - 1) / 5) +
+ $this->_mday + ($year % 100) +
+ floor(($year % 100) / 4) +
+ floor(($year / 100) / 4) - 2 *
+ floor($year / 100) + 77);
+
+ return (int)($day - 7 * floor($day / 7));
+ }
+
+ /**
+ * Returns the day number of the year (1 to 365/366).
+ *
+ * @return integer The day of the year.
+ */
+ function dayOfYear()
+ {
+ return $this->format('z') + 1;
+ }
+
+ /**
+ * Returns the week of the month.
+ *
+ * @return integer The week number.
+ */
+ function weekOfMonth()
+ {
+ return ceil($this->_mday / 7);
+ }
+
+ /**
+ * Returns the week of the year, first Monday is first day of first week.
+ *
+ * @return integer The week number.
+ */
+ function weekOfYear()
+ {
+ return $this->format('W');
+ }
+
+ /**
+ * Return the number of weeks in the given year (52 or 53).
+ *
+ * @static
+ *
+ * @param integer $year The year to count the number of weeks in.
+ *
+ * @return integer $numWeeks The number of weeks in $year.
+ */
+ function weeksInYear($year)
+ {
+ // Find the last Thursday of the year.
+ $date = new Horde_Date($year . '-12-31');
+ while ($date->dayOfWeek() != self::DATE_THURSDAY) {
+ --$date->mday;
+ }
+ return $date->weekOfYear();
+ }
+
+ /**
+ * Set the date of this object to the $nth weekday of $weekday.
+ *
+ * @param integer $weekday The day of the week (0 = Sunday, etc).
+ * @param integer $nth The $nth $weekday to set to (defaults to 1).
+ */
+ function setNthWeekday($weekday, $nth = 1)
+ {
+ if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
+ return;
+ }
+
+ $this->_mday = 1;
+ $first = $this->dayOfWeek();
+ if ($weekday < $first) {
+ $this->_mday = 8 + $weekday - $first;
+ } else {
+ $this->_mday = $weekday - $first + 1;
+ }
+ $this->_mday += 7 * $nth - 7;
+ $this->_correct(self::MASK_DAY);
+ }
+
+ /**
+ * Is the date currently represented by this object a valid date?
+ *
+ * @return boolean Validity, counting leap years, etc.
+ */
+ function isValid()
+ {
+ return ($this->_year >= 0 && $this->_year <= 9999);
+ }
+
+ /**
+ * Correct any over- or underflows in any of the date's members.
+ *
+ * @param integer $mask We may not want to correct some overflows.
+ */
+ protected function _correct($mask = self::MASK_ALLPARTS)
+ {
+ if ($mask & self::MASK_SECOND) {
+ if ($this->_sec < 0 || $this->_sec > 59) {
+ $mask |= self::MASK_MINUTE;
+
+ $this->_min += (int)($this->_sec / 60);
+ $this->_sec %= 60;
+ if ($this->_sec < 0) {
+ $this->_min--;
+ $this->_sec += 60;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_MINUTE) {
+ if ($this->_min < 0 || $this->_min > 59) {
+ $mask |= self::MASK_HOUR;
+
+ $this->_hour += (int)($this->_min / 60);
+ $this->_min %= 60;
+ if ($this->_min < 0) {
+ $this->_hour--;
+ $this->_min += 60;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_HOUR) {
+ if ($this->_hour < 0 || $this->_hour > 23) {
+ $mask |= self::MASK_DAY;
+
+ $this->_mday += (int)($this->_hour / 24);
+ $this->_hour %= 24;
+ if ($this->_hour < 0) {
+ $this->_mday--;
+ $this->_hour += 24;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_MONTH) {
+ $this->_year += (int)($this->_month / 12);
+ $this->_month %= 12;
+ if ($this->_month < 1) {
+ $this->_year--;
+ $this->_month += 12;
+ }
+ }
+
+ if ($mask & self::MASK_DAY) {
+ while ($this->_mday > 28 &&
+ $this->_mday > Horde_Date::daysInMonth($this->_month, $this->_year)) {
+ $this->_mday -= Horde_Date::daysInMonth($this->_month, $this->_year);
+ ++$this->_month;
+ $this->_correct(self::MASK_MONTH);
+ }
+ while ($this->_mday < 1) {
+ --$this->_month;
+ $this->_correct(self::MASK_MONTH);
+ $this->_mday += Horde_Date::daysInMonth($this->_month, $this->_year);
+ }
+ }
+ }
+
+ /**
+ * Compare this date to another date object to see which one is
+ * greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are on the same date
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ function compareDate($other)
+ {
+ if (!is_a($other, 'Horde_Date')) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($this->_year != $other->year) {
+ return $this->_year - $other->year;
+ }
+ if ($this->_month != $other->month) {
+ return $this->_month - $other->month;
+ }
+
+ return $this->_mday - $other->mday;
+ }
+
+ /**
+ * Returns whether this date is after the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is after the other.
+ */
+ function after($other)
+ {
+ return $this->compareDate($other) > 0;
+ }
+
+ /**
+ * Returns whether this date is before the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is before the other.
+ */
+ function before($other)
+ {
+ return $this->compareDate($other) < 0;
+ }
+
+ /**
+ * Returns whether this date is the same like the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is the same like the other.
+ */
+ function equals($other)
+ {
+ return $this->compareDate($other) == 0;
+ }
+
+ /**
+ * Compare this to another date object by time, to see which one
+ * is greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are at the same time
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ function compareTime($other)
+ {
+ if (!is_a($other, 'Horde_Date')) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($this->_hour != $other->hour) {
+ return $this->_hour - $other->hour;
+ }
+ if ($this->_min != $other->min) {
+ return $this->_min - $other->min;
+ }
+
+ return $this->_sec - $other->sec;
+ }
+
+ /**
+ * Compare this to another date object, including times, to see
+ * which one is greater (later). Assumes that the dates are in the
+ * same timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are equal
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ function compareDateTime($other)
+ {
+ if (!is_a($other, 'Horde_Date')) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($diff = $this->compareDate($other)) {
+ return $diff;
+ }
+
+ return $this->compareTime($other);
+ }
+
+ /**
+ * Get the time offset for local time zone.
+ *
+ * @param boolean $colon Place a colon between hours and minutes?
+ *
+ * @return string Timezone offset as a string in the format +HH:MM.
+ */
+ function tzOffset($colon = true)
+ {
+ return $colon ? $this->format('P') : $this->format('O');
+ }
+
+ /**
+ * Return the unix timestamp representation of this date.
+ *
+ * @return integer A unix timestamp.
+ */
+ function timestamp()
+ {
+ if ($this->_year >= 1970 && $this->_year < 2038) {
+ return mktime($this->_hour, $this->_min, $this->_sec,
+ $this->_month, $this->_mday, $this->_year);
+ }
+ return $this->format('U');
+ }
+
+ /**
+ * Return the unix timestamp representation of this date, 12:00am.
+ *
+ * @return integer A unix timestamp.
+ */
+ function datestamp()
+ {
+ if ($this->_year >= 1970 && $this->_year < 2038) {
+ return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
+ }
+ $date = new DateTime($this->format('Y-m-d'));
+ return $date->format('U');
+ }
+
+ /**
+ * Format date and time to be passed around as a short url parameter.
+ *
+ * @return string Date and time.
+ */
+ function dateString()
+ {
+ return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
+ }
+
+ /**
+ * Format date and time to the ISO format used by JSON.
+ *
+ * @return string Date and time.
+ */
+ function toJson()
+ {
+ return $this->format(self::DATE_JSON);
+ }
+
+ /**
+ * Format time using the specifiers available in date() or in the DateTime
+ * class' format() method.
+ *
+ * @param string $format
+ *
+ * @return string Formatted time.
+ */
+ function format($format)
+ {
+ if (!isset($this->_formatCache[$format])) {
+ $this->_formatCache[$format] = $this->toDateTime()->format($format);
+ }
+ return $this->_formatCache[$format];
+ }
+
+ /**
+ * Format date and time using strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ function strftime($format)
+ {
+ if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
+ return strftime($format, $this->timestamp());
+ } else {
+ return $this->_strftime($format);
+ }
+ }
+
+ /**
+ * Format date and time using a limited set of the strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ function _strftime($format)
+ {
+ if (preg_match('/%[bBpxX]/', $format)) {
+ require_once 'Horde/NLS.php';
+ }
+
+ return preg_replace(
+ array('/%b/e',
+ '/%B/e',
+ '/%C/e',
+ '/%d/e',
+ '/%D/e',
+ '/%e/e',
+ '/%H/e',
+ '/%I/e',
+ '/%m/e',
+ '/%M/e',
+ '/%n/',
+ '/%p/e',
+ '/%R/e',
+ '/%S/e',
+ '/%t/',
+ '/%T/e',
+ '/%x/e',
+ '/%X/e',
+ '/%y/e',
+ '/%Y/',
+ '/%%/'),
+ array('$this->_strftime(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
+ '$this->_strftime(NLS::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
+ '(int)($this->_year / 100)',
+ 'sprintf(\'%02d\', $this->_mday)',
+ '$this->_strftime(\'%m/%d/%y\')',
+ 'sprintf(\'%2d\', $this->_mday)',
+ 'sprintf(\'%02d\', $this->_hour)',
+ 'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
+ 'sprintf(\'%02d\', $this->_month)',
+ 'sprintf(\'%02d\', $this->_min)',
+ "\n",
+ '$this->_strftime(NLS::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
+ '$this->_strftime(\'%H:%M\')',
+ 'sprintf(\'%02d\', $this->_sec)',
+ "\t",
+ '$this->_strftime(\'%H:%M:%S\')',
+ '$this->_strftime(NLS::getLangInfo(D_FMT))',
+ '$this->_strftime(NLS::getLangInfo(T_FMT))',
+ 'substr(sprintf(\'%04d\', $this->_year), -2)',
+ (int)$this->_year,
+ '%'),
+ $format);
+ }
+
+ /**
+ * Handle args in order: year month day hour min sec tz
+ */
+ protected function _initializeFromArgs($args)
+ {
+ $tz = (isset($args[6])) ? array_pop($args) : null;
+ $this->_initializeTimezone($tz);
+
+ $args = array_slice($args, 0, 6);
+ $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
+ $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
+ $date = array_merge($keys, $date);
+
+ $this->_initializeFromArray($date);
+ }
+
+ protected function _initializeFromArray($date)
+ {
+ if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
+ if ($date['year'] > 70) {
+ $date['year'] += 1900;
+ } else {
+ $date['year'] += 2000;
+ }
+ }
+
+ foreach ($date as $key => $val) {
+ if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+ $this->{'_'. $key} = (int)$val;
+ }
+ }
+
+ // If $date['day'] is present and numeric we may have been passed
+ // a Horde_Form_datetime array.
+ if (isset($date['day']) &&
+ (string)(int)$date['day'] == $date['day']) {
+ $this->_mday = (int)$date['day'];
+ }
+ // 'minute' key also from Horde_Form_datetime
+ if (isset($date['minute']) &&
+ (string)(int)$date['minute'] == $date['minute']) {
+ $this->_min = (int)$date['minute'];
+ }
+ $this->_correct();
+ }
+
+ protected function _initializeFromObject($date)
+ {
+ if ($date instanceof DateTime) {
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ } else {
+ $is_horde_date = $date instanceof Horde_Date;
+ foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
+ if ($is_horde_date || isset($date->$key)) {
+ $this->{'_' . $key} = (int)$date->$key;
+ }
+ }
+ if (!$is_horde_date) {
+ $this->_correct();
+ }
+ }
+ }
+
+ protected function _initializeTimezone($timezone)
+ {
+ if (empty($timezone)) {
+ $timezone = date_default_timezone_get();
+ }
+ $this->_timezone = $timezone;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * This file contains the Horde_Date_Recurrence class and according constants.
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @since Horde 3.2
+ * @category Horde
+ * @package Horde_Date
+ */
+
+/**
+ * The Horde_Date_Recurrence class implements algorithms for calculating
+ * recurrences of events, including several recurrence types, intervals,
+ * exceptions, and conversion from and to vCalendar and iCalendar recurrence
+ * rules.
+ *
+ * All methods expecting dates as parameters accept all values that the
+ * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
+ * object, an ISO time string or a hash.
+ *
+ * @author Jan Schneider <jan@horde.org>
+ * @since Horde 3.2
+ * @category Horde
+ * @package Horde_Date
+ */
+class Horde_Date_Recurrence
+{
+ /** No Recurrence **/
+ const RECUR_NONE = 0;
+
+ /** Recurs daily. */
+ const RECUR_DAILY = 1;
+
+ /** Recurs weekly. */
+ const RECUR_WEEKLY = 2;
+
+ /** Recurs monthly on the same date. */
+ const RECUR_MONTHLY_DATE = 3;
+
+ /** Recurs monthly on the same week day. */
+ const RECUR_MONTHLY_WEEKDAY = 4;
+
+ /** Recurs yearly on the same date. */
+ const RECUR_YEARLY_DATE = 5;
+
+ /** Recurs yearly on the same day of the year. */
+ const RECUR_YEARLY_DAY = 6;
+
+ /** Recurs yearly on the same week day. */
+ const RECUR_YEARLY_WEEKDAY = 7;
+
+ /**
+ * The start time of the event.
+ *
+ * @var Horde_Date
+ */
+ var $start;
+
+ /**
+ * The end date of the recurrence interval.
+ *
+ * @var Horde_Date
+ */
+ var $recurEnd = null;
+
+ /**
+ * The number of recurrences.
+ *
+ * @var integer
+ */
+ var $recurCount = null;
+
+ /**
+ * The type of recurrence this event follows. RECUR_* constant.
+ *
+ * @var integer
+ */
+ var $recurType = self::RECUR_NONE;
+
+ /**
+ * The length of time between recurrences. The time unit depends on the
+ * recurrence type.
+ *
+ * @var integer
+ */
+ var $recurInterval = 1;
+
+ /**
+ * Any additional recurrence data.
+ *
+ * @var integer
+ */
+ var $recurData = null;
+
+ /**
+ * All the exceptions from recurrence for this event.
+ *
+ * @var array
+ */
+ var $exceptions = array();
+
+ /**
+ * All the dates this recurrence has been marked as completed.
+ *
+ * @var array
+ */
+ var $completions = array();
+
+ /**
+ * Constructor.
+ *
+ * @param Horde_Date $start Start of the recurring event.
+ */
+ function __construct($start)
+ {
+ $this->start = is_a($start, 'Horde_Date') ? clone $start : new Horde_Date($start);
+ }
+
+ /**
+ * Checks if this event recurs on a given day of the week.
+ *
+ * @param integer $dayMask A mask consisting of Horde_Date::MASK_*
+ * constants specifying the day(s) to check.
+ *
+ * @return boolean True if this event recurs on the given day(s).
+ */
+ function recurOnDay($dayMask)
+ {
+ return ($this->recurData & $dayMask);
+ }
+
+ /**
+ * Specifies the days this event recurs on.
+ *
+ * @param integer $dayMask A mask consisting of Horde_Date::MASK_*
+ * constants specifying the day(s) to recur on.
+ */
+ function setRecurOnDay($dayMask)
+ {
+ $this->recurData = $dayMask;
+ }
+
+ /**
+ * Returns the days this event recurs on.
+ *
+ * @return integer A mask consisting of Horde_Date::MASK_* constants
+ * specifying the day(s) this event recurs on.
+ */
+ function getRecurOnDays()
+ {
+ return $this->recurData;
+ }
+
+ /**
+ * Returns whether this event has a specific recurrence type.
+ *
+ * @param integer $recurrence RECUR_* constant of the
+ * recurrence type to check for.
+ *
+ * @return boolean True if the event has the specified recurrence type.
+ */
+ function hasRecurType($recurrence)
+ {
+ return ($recurrence == $this->recurType);
+ }
+
+ /**
+ * Sets a recurrence type for this event.
+ *
+ * @param integer $recurrence A RECUR_* constant.
+ */
+ function setRecurType($recurrence)
+ {
+ $this->recurType = $recurrence;
+ }
+
+ /**
+ * Returns recurrence type of this event.
+ *
+ * @return integer A RECUR_* constant.
+ */
+ function getRecurType()
+ {
+ return $this->recurType;
+ }
+
+ /**
+ * Returns a description of this event's recurring type.
+ *
+ * @return string Human readable recurring type.
+ */
+ function getRecurName()
+ {
+ switch ($this->getRecurType()) {
+ case self::RECUR_NONE: return _("No recurrence");
+ case self::RECUR_DAILY: return _("Daily");
+ case self::RECUR_WEEKLY: return _("Weekly");
+ case self::RECUR_MONTHLY_DATE:
+ case self::RECUR_MONTHLY_WEEKDAY: return _("Monthly");
+ case self::RECUR_YEARLY_DATE:
+ case self::RECUR_YEARLY_DAY:
+ case self::RECUR_YEARLY_WEEKDAY: return _("Yearly");
+ }
+ }
+
+ /**
+ * Sets the length of time between recurrences of this event.
+ *
+ * @param integer $interval The time between recurrences.
+ */
+ function setRecurInterval($interval)
+ {
+ if ($interval > 0) {
+ $this->recurInterval = $interval;
+ }
+ }
+
+ /**
+ * Retrieves the length of time between recurrences of this event.
+ *
+ * @return integer The number of seconds between recurrences.
+ */
+ function getRecurInterval()
+ {
+ return $this->recurInterval;
+ }
+
+ /**
+ * Sets the number of recurrences of this event.
+ *
+ * @param integer $count The number of recurrences.
+ */
+ function setRecurCount($count)
+ {
+ if ($count > 0) {
+ $this->recurCount = (int)$count;
+ // Recurrence counts and end dates are mutually exclusive.
+ $this->recurEnd = null;
+ } else {
+ $this->recurCount = null;
+ }
+ }
+
+ /**
+ * Retrieves the number of recurrences of this event.
+ *
+ * @return integer The number recurrences.
+ */
+ function getRecurCount()
+ {
+ return $this->recurCount;
+ }
+
+ /**
+ * Returns whether this event has a recurrence with a fixed count.
+ *
+ * @return boolean True if this recurrence has a fixed count.
+ */
+ function hasRecurCount()
+ {
+ return isset($this->recurCount);
+ }
+
+ /**
+ * Sets the start date of the recurrence interval.
+ *
+ * @param Horde_Date $start The recurrence start.
+ */
+ function setRecurStart($start)
+ {
+ $this->start = clone $start;
+ }
+
+ /**
+ * Retrieves the start date of the recurrence interval.
+ *
+ * @return Horde_Date The recurrence start.
+ */
+ function getRecurStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * Sets the end date of the recurrence interval.
+ *
+ * @param Horde_Date $end The recurrence end.
+ */
+ function setRecurEnd($end)
+ {
+ if (!empty($end)) {
+ // Recurrence counts and end dates are mutually exclusive.
+ $this->recurCount = null;
+ $this->recurEnd = clone $end;
+ } else {
+ $this->recurEnd = $end;
+ }
+ }
+
+ /**
+ * Retrieves the end date of the recurrence interval.
+ *
+ * @return Horde_Date The recurrence end.
+ */
+ function getRecurEnd()
+ {
+ return $this->recurEnd;
+ }
+
+ /**
+ * Returns whether this event has a recurrence end.
+ *
+ * @return boolean True if this recurrence ends.
+ */
+ function hasRecurEnd()
+ {
+ return isset($this->recurEnd) && isset($this->recurEnd->year) &&
+ $this->recurEnd->year != 9999;
+ }
+
+ /**
+ * Finds the next recurrence of this event that's after $afterDate.
+ *
+ * @param Horde_Date $after Return events after this date.
+ *
+ * @return Horde_Date|boolean The date of the next recurrence or false
+ * if the event does not recur after
+ * $afterDate.
+ */
+ function nextRecurrence($after)
+ {
+ if (!is_a($after, 'Horde_Date')) {
+ $after = new Horde_Date($after);
+ }
+
+ if ($this->start->compareDateTime($after) >= 0) {
+ return clone $this->start;
+ }
+
+ if ($this->recurInterval == 0) {
+ return false;
+ }
+
+ switch ($this->getRecurType()) {
+ case self::RECUR_DAILY:
+ $diff = Date_Calc::dateDiff($this->start->mday, $this->start->month, $this->start->year, $after->mday, $after->month, $after->year);
+ $recur = ceil($diff / $this->recurInterval);
+ if ($this->recurCount && $recur >= $this->recurCount) {
+ return false;
+ }
+ $recur *= $this->recurInterval;
+ $next = clone $this->start;
+ list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
+ if ((!$this->hasRecurEnd() ||
+ $next->compareDateTime($this->recurEnd) <= 0) &&
+ $next->compareDateTime($after) >= 0) {
+ return $next;
+ }
+ break;
+
+ case self::RECUR_WEEKLY:
+ if (empty($this->recurData)) {
+ return false;
+ }
+
+ $start_week = Horde_Date::firstDayOfWeek($this->start->format('W'),
+ $this->start->year);
+ $start_week->hour = $this->start->hour;
+ $start_week->min = $this->start->min;
+ $start_week->sec = $this->start->sec;
+
+ // Make sure we are not at the ISO-8601 first week of year while
+ // still in month 12...and adjust the year ahead if we are.
+ $week = $after->format('W');
+ if ($week == 1 && $after->month == 12) {
+ $theYear = $after->year + 1;
+ } else {
+ $theYear = $after->year;
+ }
+ $after_week = Horde_Date::firstDayOfWeek($week, $theYear);
+ $after_week_end = clone $after_week;
+ $after_week_end->mday += 7;
+
+ $diff = Date_Calc::dateDiff($start_week->mday, $start_week->month, $start_week->year,
+ $after_week->mday, $after_week->month, $after_week->year);
+ $recur = $diff + ($diff % ($this->recurInterval * 7));
+ if ($this->recurCount &&
+ ceil($recur / 7) / $this->recurInterval >= $this->recurCount) {
+ return false;
+ }
+ $next = clone $start_week;
+ $next->mday += $recur;
+ while ($next->compareDateTime($after) < 0 &&
+ $next->compareDateTime($after_week_end) < 0) {
+ ++$next->mday;
+ }
+ if (!$this->hasRecurEnd() ||
+ $next->compareDateTime($this->recurEnd) <= 0) {
+ if ($next->compareDateTime($after_week_end) >= 0) {
+ return $this->nextRecurrence($after_week_end);
+ }
+ while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
+ $next->compareDateTime($after_week_end) < 0) {
+ ++$next->mday;
+ }
+ if (!$this->hasRecurEnd() ||
+ $next->compareDateTime($this->recurEnd) <= 0) {
+ if ($next->compareDateTime($after_week_end) >= 0) {
+ return $this->nextRecurrence($after_week_end);
+ } else {
+ return $next;
+ }
+ }
+ }
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $start = clone $this->start;
+ if ($after->compareDateTime($start) < 0) {
+ $after = clone $start;
+ } else {
+ $after = clone $after;
+ }
+
+ // If we're starting past this month's recurrence of the event,
+ // look in the next month on the day the event recurs.
+ if ($after->mday > $start->mday) {
+ ++$after->month;
+ $after->mday = $start->mday;
+ }
+
+ // Adjust $start to be the first match.
+ $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
+ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+ if ($this->recurCount &&
+ ($offset / $this->recurInterval) >= $this->recurCount) {
+ return false;
+ }
+ $start->month += $offset;
+ $count = $offset / $this->recurInterval;
+
+ do {
+ if ($this->recurCount &&
+ $count++ >= $this->recurCount) {
+ return false;
+ }
+
+ // Bail if we've gone past the end of recurrence.
+ if ($this->hasRecurEnd() &&
+ $this->recurEnd->compareDateTime($start) < 0) {
+ return false;
+ }
+ if ($start->isValid()) {
+ return $start;
+ }
+
+ // If the interval is 12, and the date isn't valid, then we
+ // need to see if February 29th is an option. If not, then the
+ // event will _never_ recur, and we need to stop checking to
+ // avoid an infinite loop.
+ if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
+ return false;
+ }
+
+ // Add the recurrence interval.
+ $start->month += $this->recurInterval;
+ } while (true);
+
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ // Start with the start date of the event.
+ $estart = clone $this->start;
+
+ // What day of the week, and week of the month, do we recur on?
+ $nth = ceil($this->start->mday / 7);
+ $weekday = $estart->dayOfWeek();
+
+ // Adjust $estart to be the first candidate.
+ $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
+ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+ // Adjust our working date until it's after $after.
+ $estart->month += $offset - $this->recurInterval;
+
+ $count = $offset / $this->recurInterval;
+ do {
+ if ($this->recurCount &&
+ $count++ >= $this->recurCount) {
+ return false;
+ }
+
+ $estart->month += $this->recurInterval;
+
+ $next = clone $estart;
+ $next->setNthWeekday($weekday, $nth);
+
+ if ($next->compareDateTime($after) < 0) {
+ // We haven't made it past $after yet, try again.
+ continue;
+ }
+ if ($this->hasRecurEnd() &&
+ $next->compareDateTime($this->recurEnd) > 0) {
+ // We've gone past the end of recurrence; we can give up
+ // now.
+ return false;
+ }
+
+ // We have a candidate to return.
+ break;
+ } while (true);
+
+ return $next;
+
+ case self::RECUR_YEARLY_DATE:
+ // Start with the start date of the event.
+ $estart = clone $this->start;
+ $after = clone $after;
+
+ if ($after->month > $estart->month ||
+ ($after->month == $estart->month && $after->mday > $estart->mday)) {
+ ++$after->year;
+ $after->month = $estart->month;
+ $after->mday = $estart->mday;
+ }
+
+ // Seperate case here for February 29th
+ if ($estart->month == 2 && $estart->mday == 29) {
+ while (!Horde_Date::isLeapYear($after->year)) {
+ ++$after->year;
+ }
+ }
+
+ // Adjust $estart to be the first candidate.
+ $offset = $after->year - $estart->year;
+ if ($offset > 0) {
+ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+ $estart->year += $offset;
+ }
+
+ // We've gone past the end of recurrence; give up.
+ if ($this->recurCount &&
+ $offset >= $this->recurCount) {
+ return false;
+ }
+ if ($this->hasRecurEnd() &&
+ $this->recurEnd->compareDateTime($estart) < 0) {
+ return false;
+ }
+
+ return $estart;
+
+ case self::RECUR_YEARLY_DAY:
+ // Check count first.
+ $dayofyear = $this->start->dayOfYear();
+ $count = ($after->year - $this->start->year) / $this->recurInterval + 1;
+ if ($this->recurCount &&
+ ($count > $this->recurCount ||
+ ($count == $this->recurCount &&
+ $after->dayOfYear() > $dayofyear))) {
+ return false;
+ }
+
+ // Start with a rough interval.
+ $estart = clone $this->start;
+ $estart->year += floor($count - 1) * $this->recurInterval;
+
+ // Now add the difference to the required day of year.
+ $estart->mday += $dayofyear - $estart->dayOfYear();
+
+ // Add an interval if the estimation was wrong.
+ if ($estart->compareDate($after) < 0) {
+ $estart->year += $this->recurInterval;
+ $estart->mday += $dayofyear - $estart->dayOfYear();
+ }
+
+ // We've gone past the end of recurrence; give up.
+ if ($this->hasRecurEnd() &&
+ $this->recurEnd->compareDateTime($estart) < 0) {
+ return false;
+ }
+
+ return $estart;
+
+ case self::RECUR_YEARLY_WEEKDAY:
+ // Start with the start date of the event.
+ $estart = clone $this->start;
+
+ // What day of the week, and week of the month, do we recur on?
+ $nth = ceil($this->start->mday / 7);
+ $weekday = $estart->dayOfWeek();
+
+ // Adjust $estart to be the first candidate.
+ $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+ // Adjust our working date until it's after $after.
+ $estart->year += $offset - $this->recurInterval;
+
+ $count = $offset / $this->recurInterval;
+ do {
+ if ($this->recurCount &&
+ $count++ >= $this->recurCount) {
+ return false;
+ }
+
+ $estart->year += $this->recurInterval;
+
+ $next = clone $estart;
+ $next->setNthWeekday($weekday, $nth);
+
+ if ($next->compareDateTime($after) < 0) {
+ // We haven't made it past $after yet, try again.
+ continue;
+ }
+ if ($this->hasRecurEnd() &&
+ $next->compareDateTime($this->recurEnd) > 0) {
+ // We've gone past the end of recurrence; we can give up
+ // now.
+ return false;
+ }
+
+ // We have a candidate to return.
+ break;
+ } while (true);
+
+ return $next;
+ }
+
+ // We didn't find anything, the recurType was bad, or something else
+ // went wrong - return false.
+ return false;
+ }
+
+ /**
+ * Returns whether this event has any date that matches the recurrence
+ * rules and is not an exception.
+ *
+ * @return boolean True if an active recurrence exists.
+ */
+ function hasActiveRecurrence()
+ {
+ if (!$this->hasRecurEnd()) {
+ return true;
+ }
+
+ $next = $this->nextRecurrence(new Horde_Date($this->start));
+ while (is_object($next)) {
+ if (!$this->hasException($next->year, $next->month, $next->mday) &&
+ !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+ return true;
+ }
+
+ $next = $this->nextRecurrence(array('year' => $next->year,
+ 'month' => $next->month,
+ 'mday' => $next->mday + 1,
+ 'hour' => $next->hour,
+ 'min' => $next->min,
+ 'sec' => $next->sec));
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the next active recurrence.
+ *
+ * @param Horde_Date $afterDate Return events after this date.
+ *
+ * @return Horde_Date|boolean The date of the next active
+ * recurrence or false if the event
+ * has no active recurrence after
+ * $afterDate.
+ */
+ function nextActiveRecurrence($afterDate)
+ {
+ $next = $this->nextRecurrence($afterDate);
+ while (is_object($next)) {
+ if (!$this->hasException($next->year, $next->month, $next->mday) &&
+ !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+ return $next;
+ }
+ $next->mday++;
+ $next = $this->nextRecurrence($next);
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds an exception to a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the exception.
+ */
+ function addException($year, $month, $mday)
+ {
+ $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+ }
+
+ /**
+ * Deletes an exception from a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the exception.
+ */
+ function deleteException($year, $month, $mday)
+ {
+ $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
+ if ($key !== false) {
+ unset($this->exceptions[$key]);
+ }
+ }
+
+ /**
+ * Checks if an exception exists for a given reccurence of an event.
+ *
+ * @param integer $year The year of the reucrance.
+ * @param integer $month The month of the reucrance.
+ * @param integer $mday The day of the month of the reucrance.
+ *
+ * @return boolean True if an exception exists for the given date.
+ */
+ function hasException($year, $month, $mday)
+ {
+ return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+ $this->getExceptions());
+ }
+
+ /**
+ * Retrieves all the exceptions for this event.
+ *
+ * @return array Array containing the dates of all the exceptions in
+ * YYYYMMDD form.
+ */
+ function getExceptions()
+ {
+ return $this->exceptions;
+ }
+
+ /**
+ * Adds a completion to a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the completion.
+ */
+ function addCompletion($year, $month, $mday)
+ {
+ $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+ }
+
+ /**
+ * Deletes a completion from a recurring event.
+ *
+ * @param integer $year The year of the execption.
+ * @param integer $month The month of the execption.
+ * @param integer $mday The day of the month of the completion.
+ */
+ function deleteCompletion($year, $month, $mday)
+ {
+ $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
+ if ($key !== false) {
+ unset($this->completions[$key]);
+ }
+ }
+
+ /**
+ * Checks if a completion exists for a given reccurence of an event.
+ *
+ * @param integer $year The year of the reucrance.
+ * @param integer $month The month of the recurrance.
+ * @param integer $mday The day of the month of the recurrance.
+ *
+ * @return boolean True if a completion exists for the given date.
+ */
+ function hasCompletion($year, $month, $mday)
+ {
+ return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+ $this->getCompletions());
+ }
+
+ /**
+ * Retrieves all the completions for this event.
+ *
+ * @return array Array containing the dates of all the completions in
+ * YYYYMMDD form.
+ */
+ function getCompletions()
+ {
+ return $this->completions;
+ }
+
+ /**
+ * Parses a vCalendar 1.0 recurrence rule.
+ *
+ * @link http://www.imc.org/pdi/vcal-10.txt
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param string $rrule A vCalendar 1.0 conform RRULE value.
+ */
+ function fromRRule10($rrule)
+ {
+ if (!$rrule) {
+ return;
+ }
+
+ if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
+ // No recurrence data - event does not recur.
+ $this->setRecurType(self::RECUR_NONE);
+ }
+
+ // Always default the recurInterval to 1.
+ $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
+
+ $remainder = trim($matches[3]);
+
+ switch ($matches[1]) {
+ case 'D':
+ $this->setRecurType(self::RECUR_DAILY);
+ break;
+
+ case 'W':
+ $this->setRecurType(self::RECUR_WEEKLY);
+ if (!empty($remainder)) {
+ $maskdays = array('SU' => Horde_Date::MASK_SUNDAY,
+ 'MO' => Horde_Date::MASK_MONDAY,
+ 'TU' => Horde_Date::MASK_TUESDAY,
+ 'WE' => Horde_Date::MASK_WEDNESDAY,
+ 'TH' => Horde_Date::MASK_THURSDAY,
+ 'FR' => Horde_Date::MASK_FRIDAY,
+ 'SA' => Horde_Date::MASK_SATURDAY);
+ $mask = 0;
+ while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
+ $day = trim($matches[0]);
+ $remainder = substr($remainder, strlen($matches[0]));
+ $mask |= $maskdays[$day];
+ }
+ $this->setRecurOnDay($mask);
+ } else {
+ // Recur on the day of the week of the original recurrence.
+ $maskdays = array(Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+ Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+ Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+ Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+ Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+ Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+ Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
+ $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+ }
+ break;
+
+ case 'MP':
+ $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+ break;
+
+ case 'MD':
+ $this->setRecurType(self::RECUR_MONTHLY_DATE);
+ break;
+
+ case 'YM':
+ $this->setRecurType(self::RECUR_YEARLY_DATE);
+ break;
+
+ case 'YD':
+ $this->setRecurType(self::RECUR_YEARLY_DAY);
+ break;
+ }
+
+ // We don't support modifiers at the moment, strip them.
+ while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
+ $remainder = substr($remainder, 1);
+ }
+ if (!empty($remainder)) {
+ if (strpos($remainder, '#') !== false) {
+ $this->setRecurCount(substr($remainder, 1));
+ } else {
+ list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
+ $this->setRecurEnd(new Horde_Date(array('year' => $year,
+ 'month' => $month,
+ 'mday' => $mday)));
+ }
+ }
+ }
+
+ /**
+ * Creates a vCalendar 1.0 recurrence rule.
+ *
+ * @link http://www.imc.org/pdi/vcal-10.txt
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param Horde_iCalendar $calendar A Horde_iCalendar object instance.
+ *
+ * @return string A vCalendar 1.0 conform RRULE value.
+ */
+ function toRRule10($calendar)
+ {
+ switch ($this->recurType) {
+ case self::RECUR_NONE:
+ return '';
+
+ case self::RECUR_DAILY:
+ $rrule = 'D' . $this->recurInterval;
+ break;
+
+ case self::RECUR_WEEKLY:
+ $rrule = 'W' . $this->recurInterval;
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+ for ($i = 0; $i <= 7 ; ++$i) {
+ if ($this->recurOnDay(pow(2, $i))) {
+ $rrule .= ' ' . $vcaldays[$i];
+ }
+ }
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ $next_week = new Horde_Date($this->start);
+ $next_week->mday += 7;
+
+ if ($this->start->month != $next_week->month) {
+ $p = 5;
+ } else {
+ $p = (int)($this->start->mday / 7);
+ if (($this->start->mday % 7) > 0) {
+ $p++;
+ }
+ }
+
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+ $rrule = 'MP' . $this->recurInterval . ' ' . $p . '+ ' . $vcaldays[$this->start->dayOfWeek()];
+ break;
+
+ case self::RECUR_YEARLY_DATE:
+ $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
+ break;
+
+ case self::RECUR_YEARLY_DAY:
+ $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
+ break;
+
+ default:
+ return '';
+ }
+
+ if ($this->hasRecurEnd()) {
+ $recurEnd = clone $this->recurEnd;
+ $recurEnd->mday++;
+ return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
+ }
+
+ return $rrule . ' #' . (int)$this->getRecurCount();
+ }
+
+ /**
+ * Parses an iCalendar 2.0 recurrence rule.
+ *
+ * @link http://rfc.net/rfc2445.html#s4.3.10
+ * @link http://rfc.net/rfc2445.html#s4.8.5
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param string $rrule An iCalendar 2.0 conform RRULE value.
+ */
+ function fromRRule20($rrule)
+ {
+ // Parse the recurrence rule into keys and values.
+ $rdata = array();
+ $parts = explode(';', $rrule);
+ foreach ($parts as $part) {
+ list($key, $value) = explode('=', $part, 2);
+ $rdata[String::upper($key)] = $value;
+ }
+
+ if (isset($rdata['FREQ'])) {
+ // Always default the recurInterval to 1.
+ $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
+
+ switch (String::upper($rdata['FREQ'])) {
+ case 'DAILY':
+ $this->setRecurType(self::RECUR_DAILY);
+ break;
+
+ case 'WEEKLY':
+ $this->setRecurType(self::RECUR_WEEKLY);
+ if (isset($rdata['BYDAY'])) {
+ $maskdays = array('SU' => Horde_Date::MASK_SUNDAY,
+ 'MO' => Horde_Date::MASK_MONDAY,
+ 'TU' => Horde_Date::MASK_TUESDAY,
+ 'WE' => Horde_Date::MASK_WEDNESDAY,
+ 'TH' => Horde_Date::MASK_THURSDAY,
+ 'FR' => Horde_Date::MASK_FRIDAY,
+ 'SA' => Horde_Date::MASK_SATURDAY);
+ $days = explode(',', $rdata['BYDAY']);
+ $mask = 0;
+ foreach ($days as $day) {
+ $mask |= $maskdays[$day];
+ }
+ $this->setRecurOnDay($mask);
+ } else {
+ // Recur on the day of the week of the original
+ // recurrence.
+ $maskdays = array(
+ Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+ Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+ Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+ Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+ Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+ Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+ Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
+ $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+ }
+ break;
+
+ case 'MONTHLY':
+ if (isset($rdata['BYDAY'])) {
+ $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+ } else {
+ $this->setRecurType(self::RECUR_MONTHLY_DATE);
+ }
+ break;
+
+ case 'YEARLY':
+ if (isset($rdata['BYYEARDAY'])) {
+ $this->setRecurType(self::RECUR_YEARLY_DAY);
+ } elseif (isset($rdata['BYDAY'])) {
+ $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+ } else {
+ $this->setRecurType(self::RECUR_YEARLY_DATE);
+ }
+ break;
+ }
+
+ if (isset($rdata['UNTIL'])) {
+ list($year, $month, $mday) = sscanf($rdata['UNTIL'],
+ '%04d%02d%02d');
+ $this->setRecurEnd(new Horde_Date(array('year' => $year,
+ 'month' => $month,
+ 'mday' => $mday)));
+ }
+ if (isset($rdata['COUNT'])) {
+ $this->setRecurCount($rdata['COUNT']);
+ }
+ } else {
+ // No recurrence data - event does not recur.
+ $this->setRecurType(self::RECUR_NONE);
+ }
+ }
+
+ /**
+ * Creates an iCalendar 2.0 recurrence rule.
+ *
+ * @link http://rfc.net/rfc2445.html#s4.3.10
+ * @link http://rfc.net/rfc2445.html#s4.8.5
+ * @link http://www.shuchow.com/vCalAddendum.html
+ *
+ * @param Horde_iCalendar $calendar A Horde_iCalendar object instance.
+ *
+ * @return string An iCalendar 2.0 conform RRULE value.
+ */
+ function toRRule20($calendar)
+ {
+ switch ($this->recurType) {
+ case self::RECUR_NONE:
+ return '';
+
+ case self::RECUR_DAILY:
+ $rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval;
+ break;
+
+ case self::RECUR_WEEKLY:
+ $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+ for ($i = $flag = 0; $i <= 7 ; ++$i) {
+ if ($this->recurOnDay(pow(2, $i))) {
+ if ($flag) {
+ $rrule .= ',';
+ }
+ $rrule .= $vcaldays[$i];
+ $flag = true;
+ }
+ }
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ $next_week = new Horde_Date($this->start);
+ $next_week->mday += 7;
+ if ($this->start->month != $next_week->month) {
+ $p = 5;
+ } else {
+ $p = (int)($this->start->mday / 7);
+ if (($this->start->mday % 7) > 0) {
+ $p++;
+ }
+ }
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+ $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
+ . ';BYDAY=' . $p . $vcaldays[$this->start->dayOfWeek()];
+ break;
+
+ case self::RECUR_YEARLY_DATE:
+ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
+ break;
+
+ case self::RECUR_YEARLY_DAY:
+ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+ . ';BYYEARDAY=' . $this->start->dayOfYear();
+ break;
+
+ case self::RECUR_YEARLY_WEEKDAY:
+ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+ $weekday = new Horde_Date(array('month' => $this->start->month,
+ 'mday' => 1,
+ 'year' => $this->start->year));
+ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+ . ';BYDAY='
+ . ($this->start->weekOfYear() - $weekday->weekOfYear() + 1)
+ . $vcaldays[$this->start->dayOfWeek()]
+ . ';BYMONTH=' . $this->start->month;
+ break;
+ }
+
+ if ($this->hasRecurEnd()) {
+ $recurEnd = clone $this->recurEnd;
+ $recurEnd->mday++;
+ $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
+ }
+ if ($count = $this->getRecurCount()) {
+ $rrule .= ';COUNT=' . $count;
+ }
+ return $rrule;
+ }
+
+ /**
+ * Parses the recurrence data from a hash.
+ *
+ * @param array $hash The hash to convert.
+ *
+ * @return boolean True if the hash seemed valid, false otherwise.
+ */
+ function fromHash($hash)
+ {
+ if (!isset($hash['interval']) || !isset($hash['interval']) ||
+ !isset($hash['range-type'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurInterval((int) $hash['interval']);
+
+ $parse_day = false;
+ $set_daymask = false;
+ $update_month = false;
+ $update_daynumber = false;
+ $update_weekday = false;
+ $nth_weekday = -1;
+
+ switch ($hash['cycle']) {
+ case 'daily':
+ $this->setRecurType(self::RECUR_DAILY);
+ break;
+
+ case 'weekly':
+ $this->setRecurType(self::RECUR_WEEKLY);
+ $parse_day = true;
+ $set_daymask = true;
+ break;
+
+ case 'monthly':
+ if (!isset($hash['daynumber'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ switch ($hash['type']) {
+ case 'daynumber':
+ $this->setRecurType(self::RECUR_MONTHLY_DATE);
+ $update_daynumber = true;
+ break;
+
+ case 'weekday':
+ $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+ $nth_weekday = (int) $hash['daynumber'];
+ $hash['daynumber'] = 1;
+ $parse_day = true;
+ $update_daynumber = true;
+ $update_weekday = true;
+ break;
+ }
+ break;
+
+ case 'yearly':
+ if (!isset($hash['type'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ switch ($hash['type']) {
+ case 'monthday':
+ $this->setRecurType(self::RECUR_YEARLY_DATE);
+ $update_month = true;
+ $update_daynumber = true;
+ break;
+
+ case 'yearday':
+ if (!isset($hash['month'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurType(self::RECUR_YEARLY_DAY);
+ // Start counting days in January.
+ $hash['month'] = 'january';
+ $update_month = true;
+ $update_daynumber = true;
+ break;
+
+ case 'weekday':
+ if (!isset($hash['daynumber'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+ $nth_weekday = (int) $hash['daynumber'];
+ $hash['daynumber'] = 1;
+ $parse_day = true;
+ $update_month = true;
+ $update_daynumber = true;
+ $update_weekday = true;
+ break;
+ }
+ }
+
+ switch ($hash['range-type']) {
+ case 'number':
+ if (!isset($hash['range'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->setRecurCount((int) $hash['range']);
+ break;
+
+ case 'date':
+ $recur_end = new Horde_Date($hash['range']);
+ $recur_end->hour = 23;
+ $recur_end->min = 59;
+ $recur_end->sec = 59;
+ $this->setRecurEnd($recur_end);
+ break;
+ }
+
+ // Need to parse <day>?
+ $last_found_day = -1;
+ if ($parse_day) {
+ if (!isset($hash['day'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $mask = 0;
+ $bits = array(
+ 'monday' => Horde_Date::MASK_MONDAY,
+ 'tuesday' => Horde_Date::MASK_TUESDAY,
+ 'wednesday' => Horde_Date::MASK_WEDNESDAY,
+ 'thursday' => Horde_Date::MASK_THURSDAY,
+ 'friday' => Horde_Date::MASK_FRIDAY,
+ 'saturday' => Horde_Date::MASK_SATURDAY,
+ 'sunday' => Horde_Date::MASK_SUNDAY,
+ );
+ $days = array(
+ 'monday' => Horde_Date::DATE_MONDAY,
+ 'tuesday' => Horde_Date::DATE_TUESDAY,
+ 'wednesday' => Horde_Date::DATE_WEDNESDAY,
+ 'thursday' => Horde_Date::DATE_THURSDAY,
+ 'friday' => Horde_Date::DATE_FRIDAY,
+ 'saturday' => Horde_Date::DATE_SATURDAY,
+ 'sunday' => Horde_Date::DATE_SUNDAY,
+ );
+
+ foreach ($hash['day'] as $day) {
+ // Validity check.
+ if (empty($day) || !isset($bits[$day])) {
+ continue;
+ }
+
+ $mask |= $bits[$day];
+ $last_found_day = $days[$day];
+ }
+
+ if ($set_daymask) {
+ $this->setRecurOnDay($mask);
+ }
+ }
+
+ if ($update_month || $update_daynumber || $update_weekday) {
+ if ($update_month) {
+ $month2number = array(
+ 'january' => 1,
+ 'february' => 2,
+ 'march' => 3,
+ 'april' => 4,
+ 'may' => 5,
+ 'june' => 6,
+ 'july' => 7,
+ 'august' => 8,
+ 'september' => 9,
+ 'october' => 10,
+ 'november' => 11,
+ 'december' => 12,
+ );
+
+ if (isset($month2number[$hash['month']])) {
+ $this->start->month = $month2number[$hash['month']];
+ }
+ }
+
+ if ($update_daynumber) {
+ if (!isset($hash['daynumber'])) {
+ $this->setRecurType(self::RECUR_NONE);
+ return false;
+ }
+
+ $this->start->mday = $hash['daynumber'];
+ }
+
+ if ($update_weekday) {
+ $this->start->setNthWeekday($last_found_day, $nth_weekday);
+ }
+ }
+
+ // Exceptions.
+ if (isset($hash['exceptions'])) {
+ $this->exceptions = $hash['exceptions'];
+ }
+
+ if (isset($hash['completions'])) {
+ $this->completions = $hash['completions'];
+ }
+
+ return true;
+ }
+
+ /**
+ * Export this object into a hash.
+ *
+ * @return array The recurrence hash.
+ */
+ function toHash()
+ {
+ if ($this->getRecurType() == self::RECUR_NONE) {
+ return array();
+ }
+
+ $day2number = array(
+ 0 => 'sunday',
+ 1 => 'monday',
+ 2 => 'tuesday',
+ 3 => 'wednesday',
+ 4 => 'thursday',
+ 5 => 'friday',
+ 6 => 'saturday'
+ );
+ $month2number = array(
+ 1 => 'january',
+ 2 => 'february',
+ 3 => 'march',
+ 4 => 'april',
+ 5 => 'may',
+ 6 => 'june',
+ 7 => 'july',
+ 8 => 'august',
+ 9 => 'september',
+ 10 => 'october',
+ 11 => 'november',
+ 12 => 'december'
+ );
+
+ $hash = array('interval' => $this->getRecurInterval());
+ $start = $this->getRecurStart();
+
+ switch ($this->getRecurType()) {
+ case self::RECUR_DAILY:
+ $hash['cycle'] = 'daily';
+ break;
+
+ case self::RECUR_WEEKLY:
+ $hash['cycle'] = 'weekly';
+ $bits = array(
+ 'monday' => Horde_Date::MASK_MONDAY,
+ 'tuesday' => Horde_Date::MASK_TUESDAY,
+ 'wednesday' => Horde_Date::MASK_WEDNESDAY,
+ 'thursday' => Horde_Date::MASK_THURSDAY,
+ 'friday' => Horde_Date::MASK_FRIDAY,
+ 'saturday' => Horde_Date::MASK_SATURDAY,
+ 'sunday' => Horde_Date::MASK_SUNDAY,
+ );
+ $days = array();
+ foreach($bits as $name => $bit) {
+ if ($this->recurOnDay($bit)) {
+ $days[] = $name;
+ }
+ }
+ $hash['day'] = $days;
+ break;
+
+ case self::RECUR_MONTHLY_DATE:
+ $hash['cycle'] = 'monthly';
+ $hash['type'] = 'daynumber';
+ $hash['daynumber'] = $start->mday;
+ break;
+
+ case self::RECUR_MONTHLY_WEEKDAY:
+ $hash['cycle'] = 'monthly';
+ $hash['type'] = 'weekday';
+ $hash['daynumber'] = $start->weekOfMonth();
+ $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+ break;
+
+ case self::RECUR_YEARLY_DATE:
+ $hash['cycle'] = 'yearly';
+ $hash['type'] = 'monthday';
+ $hash['daynumber'] = $start->mday;
+ $hash['month'] = $month2number[$start->month];
+ break;
+
+ case self::RECUR_YEARLY_DAY:
+ $hash['cycle'] = 'yearly';
+ $hash['type'] = 'yearday';
+ $hash['daynumber'] = $start->dayOfYear();
+ break;
+
+ case self::RECUR_YEARLY_WEEKDAY:
+ $hash['cycle'] = 'yearly';
+ $hash['type'] = 'weekday';
+ $hash['daynumber'] = $start->weekOfMonth();
+ $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+ $hash['month'] = $month2number[$start->month];
+ }
+
+ if ($this->hasRecurCount()) {
+ $hash['range-type'] = 'number';
+ $hash['range'] = $this->getRecurCount();
+ } elseif ($this->hasRecurEnd()) {
+ $date = $this->getRecurEnd();
+ $hash['range-type'] = 'date';
+ $hash['range'] = $date->datestamp();
+ } else {
+ $hash['range-type'] = 'none';
+ $hash['range'] = '';
+ }
+
+ // Recurrence exceptions
+ $hash['exceptions'] = $this->exceptions;
+ $hash['completions'] = $this->completions;
+
+ return $hash;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ */
+
+/**
+ */
+abstract class Horde_Date_Repeater
+{
+ public $now;
+
+ /**
+ * returns the width (in seconds or months) of this repeatable.
+ */
+ abstract public function width();
+
+ /**
+ * returns the next occurance of this repeatable.
+ */
+ public function next($pointer)
+ {
+ if (is_null($this->now)) {
+ throw new Horde_Date_Repeater_Exception('Start point must be set before calling next()');
+ }
+
+ if (!in_array($pointer, array('future', 'none', 'past'))) {
+ throw new Horde_Date_Repeater_Exception("First argument 'pointer' must be one of 'past', 'future', 'none'");
+ }
+ }
+
+ public function this($pointer)
+ {
+ if (is_null($this->now)) {
+ throw new Horde_Date_Repeater_Exception('Start point must be set before calling this()');
+ }
+
+ if (!in_array($pointer, array('future', 'none', 'past'))) {
+ throw new Horde_Date_Repeater_Exception("First argument 'pointer' must be one of 'past', 'future', 'none'");
+ }
+ }
+
+ public function __toString()
+ {
+ return 'repeater';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Day extends Horde_Date_Repeater
+{
+ // (24 * 60 * 60)
+ const DAY_SECONDS = 86400;
+
+ public $currentDayStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentDayStart) {
+ $this->currentDayStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day));
+ }
+
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $this->currentDayStart->day += $direction;
+
+ $end = clone $this->currentDayStart;
+ $end->day += 1;
+
+ return new Horde_Date_Span($this->currentDayStart, $end);
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ $dayBegin = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour + 1));
+ $dayEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1));
+ break;
+
+ case 'past':
+ $dayBegin = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day));
+ $dayEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour));
+ break;
+
+ case 'none':
+ $dayBegin = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day));
+ $dayEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1));
+ break;
+ }
+
+ return new Horde_Date_Span($dayBegin, $dayEnd);
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add(array('day' => $direction * $amount));
+ }
+
+ public function width()
+ {
+ return self::DAY_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-day';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_DayName extends Horde_Date_Repeater
+{
+ // (24 * 60 * 60)
+ const DAY_SECONDS = 86400;
+
+ public $currentDayStart;
+ public $type;
+
+ public function __construct($type)
+ {
+ $this->type = $type;
+ }
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ $direction = ($pointer == 'future') ? 1 : -1;
+
+ if (!$this->currentDayStart) {
+ $this->currentDayStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + $direction));
+
+ $dayNum = $this->_dayNumber($this->type);
+ while ($this->currentDayStart->dayOfWeek() != $dayNum) {
+ $this->currentDayStart->day += $direction;
+ }
+ } else {
+ $this->currentDayStart->day += $direction * 7;
+ }
+
+ $end = clone $this->currentDayStart;
+ $end->day++;
+ return new Horde_Date_Span($this->currentDayStart, $end);
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::next($pointer);
+
+ if ($pointer == 'none') {
+ $pointer = 'future';
+ }
+ return $this->next($pointer);
+ }
+
+ public function width()
+ {
+ return self::DAY_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-dayname-' . $this->type;
+ }
+
+ protected function _dayNumber($dayName)
+ {
+ $days = array(
+ 'monday' => Horde_Date::DATE_MONDAY,
+ 'tuesday' => Horde_Date::DATE_TUESDAY,
+ 'wednesday' => Horde_Date::DATE_WEDNESDAY,
+ 'thursday' => Horde_Date::DATE_THURSDAY,
+ 'friday' => Horde_Date::DATE_FRIDAY,
+ 'saturday' => Horde_Date::DATE_SATURDAY,
+ 'sunday' => Horde_Date::DATE_SUNDAY,
+ );
+ if (!isset($days[$dayName])) {
+ throw new InvalidArgumentException('Invalid day name "' . $dayName . '"');
+ }
+ return $days[$dayName];
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_DayPortion extends Horde_Date_Repeater
+{
+ /**
+ * 6am-12am (6 * 60 * 60, 12 * 60 * 60)
+ */
+ public static $morning = array(21600, 43200);
+
+ /**
+ * 1pm-5pm (13 * 60 * 60, 17 * 60 * 60)
+ */
+ public static $afternoon = array(46800, 61200);
+
+ /**
+ * 5pm-8pm (17 * 60 * 60, 20 * 60 * 60)
+ */
+ public static $evening = array(61200, 72000);
+
+ /**
+ * 8pm-12pm (20 * 60 * 60, 24 * 60 * 60)
+ */
+ public static $night = array(72000, 86400);
+
+ public $range;
+ public $currentSpan;
+ public $type;
+
+ public function __construct($type)
+ {
+ $this->type = $type;
+
+ if (is_int($type)) {
+ $this->range = array(($type * 3600), (($type + 12) * 3600));
+ } else {
+ $lookup = array(
+ 'am' => array(0, (12 * 3600 - 1)),
+ 'pm' => array((12 * 3600), (24 * 3600 - 1)),
+ 'morning' => self::$morning,
+ 'afternoon' => self::$afternoon,
+ 'evening' => self::$evening,
+ 'night' => self::$night,
+ );
+ if (!isset($lookup[$type])) {
+ throw new InvalidArgumentException("Invalid type '$type' for Repeater_DayPortion");
+ }
+ $this->range = $lookup[$type];
+ }
+ }
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ $fullDay = 60 * 60 * 24;
+
+ if (!$this->currentSpan) {
+ $nowSeconds = $this->now->hour * 3600 + $this->now->min * 60 + $this->now->sec;
+ if ($nowSeconds < $this->range[0]) {
+ switch ($pointer) {
+ case 'future':
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'sec' => $this->range[0]));
+ break;
+
+ case 'past':
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day - 1, 'sec' => $this->range[0]));
+ break;
+ }
+ } elseif ($nowSeconds > $this->range[1]) {
+ switch ($pointer) {
+ case 'future':
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1, 'sec' => $this->range[0]));
+ break;
+
+ case 'past':
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'sec' => $this->range[0]));
+ break;
+ }
+ } else {
+ switch ($pointer) {
+ case 'future':
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1, 'sec' => $this->range[0]));
+ break;
+
+ case 'past':
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day - 1, 'sec' => $this->range[0]));
+ break;
+ }
+ }
+
+ $rangeEnd = $rangeStart->add($this->range[1] - $this->range[0]);
+ $this->currentSpan = new Horde_Date_Span($rangeStart, $rangeEnd);
+ } else {
+ switch ($pointer) {
+ case 'future':
+ $this->currentSpan = $this->currentSpan->add(array('day' => 1));
+ break;
+
+ case 'past':
+ $this->currentSpan = $this->currentSpan->add(array('day' => -1));
+ break;
+ }
+ }
+
+ return $this->currentSpan;
+ }
+
+ public function this($context = 'future')
+ {
+ parent::this($context);
+
+ $rangeStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'sec' => $this->range[0]));
+ $this->currentSpan = new Horde_Date_Span($rangeStart, $rangeStart->add($this->range[1] - $this->range[0]));
+ return $this->currentSpan;
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $this->now = $span->begin;
+ $portionSpan = $this->next($pointer);
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $portionSpan->add(array('day' => $direction * ($amount - 1)));
+ }
+
+ public function width()
+ {
+ if (!$this->range) {
+ throw new Horde_Date_Repeater_Exception('Range has not been set');
+ }
+
+ if ($this->currentSpan) {
+ return $this->currentSpan->width();
+ }
+
+ if (is_int($this->type)) {
+ return (12 * 3600);
+ } else {
+ return $this->range[1] - $this->range[0];
+ }
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-dayportion-' . $this->type;
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Exception extends Exception
+{
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Fortnight extends Horde_Date_Repeater
+{
+ // (14 * 24 * 60 * 60)
+ const FORTNIGHT_SECONDS = 1209600;
+
+ public $currentFortnightStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentFortnightStart) {
+ switch ($pointer) {
+ case 'future':
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $nextSundaySpan = $sundayRepeater->next('future');
+ $this->currentFortnightStart = $nextSundaySpan->begin;
+ break;
+
+ case 'past':
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = clone $this->now;
+ $sundayRepeater->now->day++;
+ $sundayRepeater->next('past');
+ $sundayRepeater->next('past');
+ $lastSundaySpan = $sundayRepeater->next('past');
+ $this->currentFortnightStart = $lastSundaySpan->begin;
+ break;
+ }
+ } else {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $this->currentFortnightStart->add($direction * self::FORTNIGHT_SECONDS);
+ }
+
+ return new Horde_Date_Span($this->currentFortnightStart, $this->currentFortnightStart->add(self::FORTNIGHT_SECONDS));
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ case 'none':
+ $thisFortnightStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour + 1));
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $sundayRepeater->this('future');
+ $thisSundaySpan = $sundayRepeater->this('future');
+ $thisFortnightEnd = $thisSundaySpan->begin;
+ return new Horde_Date_Span($thisFortnightStart, $thisFortnightEnd);
+
+ case 'past':
+ $thisFortnightEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour));
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $lastSundaySpan = $sundayRepeater->next('past');
+ $thisFortnightStart = $lastSundaySpan->begin;
+ return new Horde_Date_Span($thisFortnightStart, $thisFortnightEnd);
+ }
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add($direction * $amount * self::FORTNIGHT_SECONDS);
+ }
+
+ public function width()
+ {
+ return self::FORTNIGHT_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-fortnight';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Hour extends Horde_Date_Repeater
+{
+ public $currentHourStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ $direction = ($pointer == 'future') ? 1 : -1;
+ if (!$this->currentHourStart) {
+ $this->currentHourStart = new Horde_Date(array('month' => $this->now->month, 'year' => $this->now->year, 'day' => $this->now->day, 'hour' => $this->now->hour));
+ }
+ $this->currentHourStart->hour += $direction;
+
+ $end = clone $this->currentHourStart;
+ $end->hour++;
+ return new Horde_Date_Span($this->currentHourStart, $end);
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ $hourStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min + 1));
+ $hourEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour + 1));
+ break;
+
+ case 'past':
+ $hourStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour));
+ $hourEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min));
+ break;
+
+ case 'none':
+ $hourStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour));
+ $hourEnd = $hourStart->add(array('hour' => 1));
+ break;
+ }
+
+ return new Horde_Date_Span($hourStart, $hourEnd);
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add(array('hour' => $direction * $amount));
+ }
+
+ public function width()
+ {
+ return 3600;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-hour';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Minute extends Horde_Date_Repeater
+{
+ public $currentMinuteStart;
+
+ public function next($pointer = 'future')
+ {
+ parent::next($pointer);
+
+ if (!$this->currentMinuteStart) {
+ $this->currentMinuteStart = new Horde_Date(array('month' => $this->now->month, 'year' => $this->now->year, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min));
+ }
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $this->currentMinuteStart->min += $direction;
+
+ $end = clone $this->currentMinuteStart;
+ $end->min++;
+ return new Horde_Date_Span($this->currentMinuteStart, $end);
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ $minuteBegin = clone $this->now;
+ $minuteEnd = new Horde_Date(array('month' => $this->now->month, 'year' => $this->now->year, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min));
+ break;
+
+ case 'past':
+ $minuteBegin = new Horde_Date(array('month' => $this->now->month, 'year' => $this->now->year, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min));
+ $minuteEnd = clone $this->now;
+ break;
+
+ case 'none':
+ $minuteBegin = new Horde_Date(array('month' => $this->now->month, 'year' => $this->now->year, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min));
+ $minuteEnd = new Horde_Date(array('month' => $this->now->month, 'year' => $this->now->year, 'day' => $this->now->day, 'hour' => $this->now->hour, 'min' => $this->now->min + 1));
+ break;
+ }
+
+ return new Horde_Date_Span($minuteBegin, $minuteEnd);
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add(array('min' => $direction * $amount));
+ }
+
+ public function width()
+ {
+ return 60;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-minute';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Month extends Horde_Date_Repeater
+{
+ /**
+ * 30 * 24 * 60 * 60
+ */
+ const MONTH_SECONDS = 2592000;
+
+ public $currentMonthStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentMonthStart) {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => 1));
+ }
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $this->currentMonthStart->month += $direction;
+
+ $end = clone $this->currentMonthStart;
+ $end->month++;
+ return new Horde_Date_Span($this->currentMonthStart, $end);
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ $monthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1));
+ $monthEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month + 1, 'day' => 1));
+ break;
+
+ case 'past':
+ $monthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => 1));
+ $monthEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day));
+ break;
+
+ case 'none':
+ $monthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => 1));
+ $monthEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month + 1, 'day' => 1));
+ break;
+ }
+
+ return new Horde_Date_Span($monthStart, $monthEnd);
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add(array('month' => $amount * $direction));
+ }
+
+ public function width()
+ {
+ return self::MONTH_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-month';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_MonthName extends Horde_Date_Repeater
+{
+ public $currentMonthStart;
+ public $type;
+
+ public function __construct($type)
+ {
+ $this->type = $type;
+ }
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentMonthStart) {
+ $targetMonth = $this->_monthNumber($this->type);
+ switch ($pointer) {
+ case 'future':
+ if ($this->now->month < $targetMonth) {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $targetMonth, 'day' => 1));
+ } else {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year + 1, 'month' => $targetMonth, 'day' => 1));
+ }
+ break;
+
+ case 'none':
+ if ($this->now->month <= $targetMonth) {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $targetMonth, 'day' => 1));
+ } else {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year + 1, 'month' => $targetMonth, 'day' => 1));
+ }
+ break;
+
+ case 'past':
+ if ($this->now->month > $targetMonth) {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year, 'month' => $targetMonth, 'day' => 1));
+ } else {
+ $this->currentMonthStart = new Horde_Date(array('year' => $this->now->year - 1, 'month' => $targetMonth, 'day' => 1));
+ }
+ break;
+ }
+ } else {
+ switch ($pointer) {
+ case 'future':
+ $this->currentMonthStart->year++;
+ break;
+
+ case 'past':
+ $this->currentMonthStart->year--;
+ break;
+ }
+ }
+
+ return new Horde_Date_Span($this->currentMonthStart, $this->currentMonthStart->add(array('month' => 1)));
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'past':
+ return $this->next($pointer);
+
+ case 'future':
+ case 'none':
+ return $this->next('none');
+ }
+ }
+
+ public function width()
+ {
+ return Horde_Date_Repeater_Month::MONTH_SECONDS;
+ }
+
+ public function index()
+ {
+ return $this->_monthNumber($this->type);
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-monthname-' . $this->type;
+ }
+
+ protected function _monthNumber($monthName)
+ {
+ $months = array(
+ 'january' => 1,
+ 'february' => 2,
+ 'march' => 3,
+ 'april' => 4,
+ 'may' => 5,
+ 'june' => 6,
+ 'july' => 7,
+ 'august' => 8,
+ 'september' => 9,
+ 'october' => 10,
+ 'november' => 11,
+ 'december' => 12,
+ );
+ if (!isset($months[$monthName])) {
+ throw new InvalidArgumentException('Invalid month name "' . $monthName . '"');
+ }
+ return $months[$monthName];
+ }
+
+}
\ No newline at end of file
--- /dev/null
+<?php
+class Horde_Date_Repeater_Season extends Horde_Date_Repeater
+{
+ /**
+ * 91 * 24 * 60 * 60
+ */
+ const SEASON_SECONDS = 7862400;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+ throw new Horde_Date_Repeater_Exception('Not implemented');
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+ throw new Horde_Date_Repeater_Exception('Not implemented');
+ }
+
+ public function width()
+ {
+ return self::SEASON_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-season';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_SeasonName extends Horde_Date_Repeater_Season
+{
+ /**
+ * 91 * 24 * 60 * 60
+ */
+ const SEASON_SECONDS = 7862400;
+
+ public $summer = array('jul 21', 'sep 22');
+ public $autumn = array('sep 23', 'dec 21');
+ public $winter = array('dec 22', 'mar 19');
+ public $spring = array('mar 20', 'jul 20');
+ public $type;
+
+ public function __construct($type)
+ {
+ $this->type = $type;
+ }
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+ throw new Horde_Date_Repeater_Exception('Not implemented');
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+ throw new Horde_Date_Repeater_Exception('Not implemented');
+ }
+
+ public function width()
+ {
+ return self::SEASON_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-season-' . $this->type;
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Second extends Horde_Date_Repeater
+{
+ public $secondStart;
+
+ public function next($pointer = 'future')
+ {
+ parent::next($pointer);
+
+ $direction = ($pointer == 'future') ? 1 : -1;
+
+ if (!$this->secondStart) {
+ $this->secondStart = clone $this->now;
+ $this->secondStart->sec += $direction;
+ } else {
+ $this->secondStart += $direction;
+ }
+
+ $end = clone $this->secondStart;
+ $end->sec++;
+ return new Horde_Date_Span($this->secondStart, $end);
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ $end = clone $this->now;
+ $end->sec++;
+ return new Horde_Date_Span($this->now, $end);
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add($direction * $amount);
+ }
+
+ public function width()
+ {
+ return 1;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-second';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Time extends Horde_Date_Repeater
+{
+ public $currentTime;
+ public $type;
+ public $ambiguous;
+
+ public function __construct($time, $options = array())
+ {
+ $t = str_replace(':', '', $time);
+
+ switch (strlen($t)) {
+ case 1:
+ case 2:
+ $hours = (int)$t;
+ $this->ambiguous = true;
+ $this->type = ($hours == 12) ? 0 : $hours * 3600;
+ break;
+
+ case 3:
+ $this->ambiguous = true;
+ $this->type = $t[0] * 3600 + (int)substr($t, 1, 2) * 60;
+ break;
+
+ case 4:
+ $this->ambiguous = (strpos($time, ':') !== false) && ($t[0] != 0) && ((int)substr($t, 0, 2) <= 12);
+ $hours = (int)substr($t, 0, 2);
+ $this->type = ($hours == 12) ?
+ ((int)substr($t, 2, 2) * 60) :
+ ($hours * 60 * 60 + (int)substr($t, 2, 2) * 60);
+ break;
+
+ case 5:
+ $this->ambiguous = true;
+ $this->type = $t[0] * 3600 + (int)substr($t, 1, 2) * 60 + (int)substr($t, 3, 2);
+ break;
+
+ case 6:
+ $this->ambiguous = (strpos($time, ':') !== false) && ($t[0] != 0) && ((int)substr($t, 0, 2) <= 12);
+ $hours = (int)substr($t, 0, 2);
+ $this->type = ($hours == 12) ?
+ ((int)substr($t, 2, 2) * 60 + (int)substr($t, 4, 2)) :
+ ($hours * 60 * 60 + (int)substr($t, 2, 2) * 60 + (int)substr($t, 4, 2));
+ break;
+
+ default:
+ throw new Horde_Date_Repeater_Exception('Time cannot exceed six digits');
+ }
+ }
+
+ /**
+ * Return the next past or future Span for the time that this Repeater represents
+ * pointer - Symbol representing which temporal direction to fetch the next day
+ * must be either :past or :future
+ */
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ $halfDay = 3600 * 12;
+ $fullDay = 3600 * 24;
+
+ $first = false;
+
+ if (!$this->currentTime) {
+ $first = true;
+ $midnight = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day));
+ $yesterdayMidnight = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day - 1));
+ $tomorrowMidnight = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1));
+
+ if ($pointer == 'future') {
+ if ($this->ambiguous) {
+ foreach (array($midnight->add($this->type), $midnight->add($halfDay + $this->type), $tomorrowMidnight->add($this->type)) as $t) {
+ if ($t->compareDateTime($this->now) >= 0) {
+ $this->currentTime = $t;
+ break;
+ }
+ }
+ } else {
+ foreach (array($midnight->add($this->type), $tomorrowMidnight->add($this->type)) as $t) {
+ if ($t->compareDateTime($this->now) >= 0) {
+ $this->currentTime = $t;
+ break;
+ }
+ }
+ }
+ } elseif ($pointer == 'past') {
+ if ($this->ambiguous) {
+ foreach (array($midnight->add($halfDay + $this->type), $midnight->add($this->type), $yesterdayMidnight->add($this->type * 2)) as $t) {
+ if ($t->compareDateTime($this->now) <= 0) {
+ $this->currentTime = $t;
+ break;
+ }
+ }
+ } else {
+ foreach (array($midnight->add($this->type), $yesterdayMidnight->add($this->type)) as $t) {
+ if ($t->compareDateTime($this->now) <= 0) {
+ $this->currentTime = $t;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$this->currentTime) {
+ throw new Horde_Date_Repeater_Exception('Current time cannot be null at this point');
+ }
+ }
+
+ if (!$first) {
+ $increment = $this->ambiguous ? $halfDay : $fullDay;
+ $this->currentTime->sec += ($pointer == 'future') ? $increment : -$increment;
+ }
+
+ return new Horde_Date_Span($this->currentTime, $this->currentTime->add(1));
+ }
+
+ public function this($context = 'future')
+ {
+ parent::this($context);
+
+ if ($context == 'none') { $context = 'future'; }
+ return $this->next($context);
+ }
+
+ public function width()
+ {
+ return 1;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-time-' . $this->type;
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Week extends Horde_Date_Repeater
+{
+ /**
+ * (7 * 24 * 60 * 60)
+ */
+ const WEEK_SECONDS = 604800;
+
+ public $currentWeekStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentWeekStart) {
+ switch ($pointer) {
+ case 'future':
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $nextSundaySpan = $sundayRepeater->next('future');
+ $this->currentWeekStart = $nextSundaySpan->begin;
+ break;
+
+ case 'past':
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = clone $this->now;
+ $sundayRepeater->now->day++;
+ $sundayRepeater->next('past');
+ $lastSundaySpan = $sundayRepeater->next('past');
+ $this->currentWeekStart = $lastSundaySpan->begin;
+ break;
+ }
+ } else {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $this->currentWeekStart->day += $direction * 7;
+ }
+
+ return new Horde_Date_Span($this->currentWeekStart, $this->currentWeekStart->add(array('day' => 7)));
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ $thisWeekStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour + 1));
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $thisSundaySpan = $sundayRepeater->this('future');
+ $thisWeekEnd = $thisSundaySpan->begin;
+ return new Horde_Date_Span($thisWeekStart, $thisWeekEnd);
+
+ case 'past':
+ $thisWeekEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day, 'hour' => $this->now->hour));
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $lastSundaySpan = $sundayRepeater->next('past');
+ $thisWeekStart = $lastSundaySpan->begin;
+ return new Horde_Date_Span($thisWeekStart, $thisWeekEnd);
+
+ case 'none':
+ $sundayRepeater = new Horde_Date_Repeater_DayName('sunday');
+ $sundayRepeater->now = $this->now;
+ $lastSundaySpan = $sundayRepeater->next('past');
+ $thisWeekStart = $lastSundaySpan->begin;
+ $thisWeekEnd = clone $thisWeekStart;
+ $thisWeekEnd->day += 7;
+ return new Horde_Date_Span($thisWeekStart, $thisWeekEnd);
+ }
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add(array('day' => $direction * $amount * 7));
+ }
+
+ public function width()
+ {
+ return self::WEEK_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-week';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Weekend extends Horde_Date_Repeater
+{
+ /**
+ * (2 * 24 * 60 * 60)
+ */
+ const WEEKEND_SECONDS = 172800;
+
+ public $currentWeekStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentWeekStart) {
+ switch ($pointer) {
+ case 'future':
+ $saturdayRepeater = new Horde_Date_Repeater_DayName('saturday');
+ $saturdayRepeater->now = $this->now;
+ $nextSaturdaySpan = $saturdayRepeater->next('future');
+ $this->currentWeekStart = $nextSaturdaySpan->begin;
+ break;
+
+ case 'past':
+ $saturdayRepeater = new Horde_Date_Repeater_DayName('saturday');
+ $saturdayRepeater->now = $this->now;
+ $saturdayRepeater->now->day++;
+ $lastSaturdaySpan = $saturdayRepeater->next('past');
+ $this->currentWeekStart = $lastSaturdaySpan->begin;
+ break;
+ }
+ } else {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $this->currentWeekStart->day += $direction * 7;
+ }
+
+ return new Horde_Date_Span($this->currentWeekStart, $this->currentWeekStart->add(array('day' => 2)));
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ case 'none':
+ $saturdayRepeater = new Horde_Date_Repeater_DayName('saturday');
+ $saturdayRepeater->now = $this->now;
+ $thisSaturdaySpan = $saturdayRepeater->this('future');
+ return new Horde_Date_Span($thisSaturdaySpan->begin, $thisSaturdaySpan->begin->add(array('day' => 2)));
+
+ case 'past':
+ $saturdayRepeater = new Horde_Date_Repeater_DayName('saturday');
+ $saturdayRepeater->now = $this->now;
+ $lastSaturdaySpan = $saturdayRepeater->this('past');
+ return new Horde_Date_Span($lastSaturdaySpan->begin, $lastSaturdaySpan->begin->add(array('day' => 2)));
+ }
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ $weekend = new self();
+ $weekend->now = clone $span->begin;
+ $start = $weekend->next($pointer)->begin;
+ $start->day += ($amount - 1) * $direction * 7;
+ return new Horde_Date_Span($start, $start->add($span->width()));
+ }
+
+ public function width()
+ {
+ return self::WEEKEND_SECONDS;
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-weekend';
+ }
+
+}
--- /dev/null
+<?php
+class Horde_Date_Repeater_Year extends Horde_Date_Repeater
+{
+ public $currentYearStart;
+
+ public function next($pointer)
+ {
+ parent::next($pointer);
+
+ if (!$this->currentYearStart) {
+ $this->currentYearStart = new Horde_Date(array('year' => $this->now->year, 'month' => 1, 'day' => 1));
+ }
+
+ $diff = ($pointer == 'future') ? 1 : -1;
+ $this->currentYearStart->year += $diff;
+
+ return new Horde_Date_Span($this->currentYearStart, $this->currentYearStart->add(array('year' => 1)));
+ }
+
+ public function this($pointer = 'future')
+ {
+ parent::this($pointer);
+
+ switch ($pointer) {
+ case 'future':
+ $thisYearStart = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day + 1));
+ $thisYearEnd = new Horde_Date(array('year' => $this->now->year + 1, 'month' => 1, 'day' => 1));
+ break;
+
+ case 'past':
+ $thisYearStart = new Horde_Date(array('year' => $this->now->year, 'month' => 1, 'day' => 1));
+ $thisYearEnd = new Horde_Date(array('year' => $this->now->year, 'month' => $this->now->month, 'day' => $this->now->day));
+ break;
+
+ case 'none':
+ $thisYearStart = new Horde_Date(array('year' => $this->now->year, 'month' => 1, 'day' => 1));
+ $thisYearEnd = new Horde_Date(array('year' => $this->now->year + 1, 'month' => 1, 'day' => 1));
+ break;
+ }
+
+ return new Horde_Date_Span($thisYearStart, $thisYearEnd);
+ }
+
+ public function offset($span, $amount, $pointer)
+ {
+ $direction = ($pointer == 'future') ? 1 : -1;
+ return $span->add(array('year' => ($amount * $direction)));
+ }
+
+ public function width()
+ {
+ return (365 * 24 * 60 * 60);
+ }
+
+ public function __toString()
+ {
+ return parent::__toString() . '-year';
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * A Span represents a range of time.
+ *
+ * @package Horde_Date
+ */
+
+/**
+ * @package Horde_Date
+ */
+class Horde_Date_Span
+{
+ /**
+ * @var Horde_Date
+ */
+ public $begin;
+
+ /**
+ * @var Horde_Date
+ */
+ public $end;
+
+ /**
+ * Constructor
+ *
+ * @param mixed $begin Horde_Date or other format accepted by the Horde_Date constructor
+ * @param mixed $end Horde_Date or other format accepted by the Horde_Date constructor
+ */
+ public function __construct($begin, $end)
+ {
+ if (! $begin instanceof Horde_Date) { $begin = new Horde_Date($begin); }
+ if (! $end instanceof Horde_Date) { $end = new Horde_Date($end); }
+
+ $this->begin = $begin;
+ $this->end = $end;
+ }
+
+ /**
+ * Return the width of this span in seconds
+ */
+ public function width()
+ {
+ return abs($this->end->timestamp() - $this->begin->timestamp());
+ }
+
+ /**
+ * Is a Horde_Date within this span?
+ *
+ * @param Horde_Date $date
+ */
+ public function includes($date)
+ {
+ return ($this->begin->compareDateTime($date) <= 0) && ($this->end->compareDateTime($date) >= 0);
+ }
+
+ /**
+ * Add a number of seconds to this span, returning the new span
+ */
+ public function add($factor)
+ {
+ return new Horde_Date_Span($this->begin->add($factor), $this->end->add($factor));
+ }
+
+ /**
+ * Subtract a number of seconds from this span, returning the new span.
+ */
+ public function sub($factor)
+ {
+ return new Horde_Date_Span($this->begin->sub($factor), $this->end->sub($factor));
+ }
+
+ public function __toString()
+ {
+ return '(' . $this->begin . '..' . $this->end . ')';
+ }
+
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Date</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Date package</summary>
+ <description>Package for creating and manipulating dates.
+ </description>
+ <lead>
+ <name>Chuck Hagenbuch</name>
+ <user>chuck</user>
+ <email>chuck@horde.org</email>
+ <active>yes</active>
+ </lead>
+ <lead>
+ <name>Jan Schneider</name>
+ <user>jan</user>
+ <email>jan@horde.org</email>
+ <active>yes</active>
+ </lead>
+ <date>2009-05-29</date>
+ <version>
+ <release>0.3.0</release>
+ <api>0.3.0</api>
+ </version>
+ <stability>
+ <release>beta</release>
+ <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Add Horde_Date_Repeater
+* Add Horde_Date_Span</notes>
+ <contents>
+ <dir name="/">
+ <dir name="lib">
+ <dir name="Horde">
+ <dir name="Date">
+ <dir name="Repeater">
+ <file name="Day.php" role="php" />
+ <file name="DayName.php" role="php" />
+ <file name="DayPortion.php" role="php" />
+ <file name="Exception.php" role="php" />
+ <file name="Fortnight.php" role="php" />
+ <file name="Hour.php" role="php" />
+ <file name="Minute.php" role="php" />
+ <file name="Month.php" role="php" />
+ <file name="MonthName.php" role="php" />
+ <file name="Season.php" role="php" />
+ <file name="SeasonName.php" role="php" />
+ <file name="Second.php" role="php" />
+ <file name="Time.php" role="php" />
+ <file name="Week.php" role="php" />
+ <file name="Weekend.php" role="php" />
+ <file name="Year.php" role="php" />
+ </dir> <!-- /lib/Horde/Date/Repeater -->
+ <file name="Recurrence.php" role="php" />
+ <file name="Repeater.php" role="php" />
+ <file name="Span.php" role="php" />
+ </dir> <!-- /lib/Horde/Date -->
+ <file name="Date.php" role="php" />
+ </dir> <!-- /lib/Horde -->
+ </dir> <!-- /lib -->
+ </dir> <!-- / -->
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.2.0</min>
+ </php>
+ <pearinstaller>
+ <min>1.7.0</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease>
+ <filelist>
+ <install name="lib/Horde/Date.php" as="Horde/Date.php" />
+ <install name="lib/Horde/Date/Recurrence.php" as="Horde/Date/Recurrence.php" />
+ <install name="lib/Horde/Date/Repeater.php" as="Horde/Date/Repeater.php" />
+ <install name="lib/Horde/Date/Repeater/Day.php" as="Horde/Date/Repeater/Day.php" />
+ <install name="lib/Horde/Date/Repeater/DayName.php" as="Horde/Date/Repeater/DayName.php" />
+ <install name="lib/Horde/Date/Repeater/DayPortion.php" as="Horde/Date/Repeater/DayPortion.php" />
+ <install name="lib/Horde/Date/Repeater/Exception.php" as="Horde/Date/Repeater/Exception.php" />
+ <install name="lib/Horde/Date/Repeater/Fortnight.php" as="Horde/Date/Repeater/Fortnight.php" />
+ <install name="lib/Horde/Date/Repeater/Hour.php" as="Horde/Date/Repeater/Hour.php" />
+ <install name="lib/Horde/Date/Repeater/Minute.php" as="Horde/Date/Repeater/Minute.php" />
+ <install name="lib/Horde/Date/Repeater/Month.php" as="Horde/Date/Repeater/Month.php" />
+ <install name="lib/Horde/Date/Repeater/MonthName.php" as="Horde/Date/Repeater/MonthName.php" />
+ <install name="lib/Horde/Date/Repeater/Season.php" as="Horde/Date/Repeater/Season.php" />
+ <install name="lib/Horde/Date/Repeater/SeasonName.php" as="Horde/Date/Repeater/SeasonName.php" />
+ <install name="lib/Horde/Date/Repeater/Second.php" as="Horde/Date/Repeater/Second.php" />
+ <install name="lib/Horde/Date/Repeater/Time.php" as="Horde/Date/Repeater/Time.php" />
+ <install name="lib/Horde/Date/Repeater/Week.php" as="Horde/Date/Repeater/Week.php" />
+ <install name="lib/Horde/Date/Repeater/Weekend.php" as="Horde/Date/Repeater/Weekend.php" />
+ <install name="lib/Horde/Date/Repeater/Year.php" as="Horde/Date/Repeater/Year.php" />
+ <install name="lib/Horde/Date/Span.php" as="Horde/Date/Span.php" />
+ </filelist>
+ </phprelease>
+</package>
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Date
+ * @subpackage UnitTests
+ * @copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ * @license http://opensource.org/licenses/bsd-license.php
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+ define('PHPUnit_MAIN_METHOD', 'Horde_Date_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+class Horde_Date_AllTests {
+
+ public static function main()
+ {
+ PHPUnit_TextUI_TestRunner::run(self::suite());
+ }
+
+ public static function suite()
+ {
+ set_include_path(dirname(__FILE__) . '/../../../lib' . PATH_SEPARATOR . get_include_path());
+ if (!spl_autoload_functions()) {
+ spl_autoload_register(create_function('$class', '$filename = str_replace(array(\'::\', \'_\'), \'/\', $class); include "$filename.php";'));
+ }
+
+ $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Date');
+
+ $basedir = dirname(__FILE__);
+ $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+ foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+ if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+ $pathname = $file->getPathname();
+ require $pathname;
+
+ $class = str_replace(DIRECTORY_SEPARATOR, '_',
+ preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+ $suite->addTestSuite('Horde_Date_' . $class);
+ }
+ }
+
+ return $suite;
+ }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Date_AllTests::main') {
+ Horde_Date_AllTests::main();
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_DateTest extends PHPUnit_Framework_TestCase
+{
+ public function testDateCorrection()
+ {
+ $d = new Horde_Date('2008-01-01 00:00:00');
+ $d->month -= 2;
+ $this->assertEquals(2007, $d->year);
+
+ $d = new Horde_Date('2008-01-01 00:00:00');
+ $d->day -= 1;
+ $this->assertEquals(2007, $d->year);
+ $this->assertEquals(12, $d->month);
+
+ $d = new Horde_Date('2008-01-01 00:00:00');
+ $d->day += 370;
+ $this->assertEquals(2009, $d->year);
+ $this->assertEquals(1, $d->month);
+
+ $d = new Horde_Date('2008-01-01 00:00:00');
+ $d->sec += 14400;
+ $this->assertEquals(0, $d->sec);
+ $this->assertEquals(0, $d->min);
+ $this->assertEquals(4, $d->hour);
+ }
+
+ public function testDateMath()
+ {
+ $d = new Horde_Date('2008-01-01 00:00:00');
+
+ $this->assertEquals('2007-12-31 00:00:00', (string)$d->sub(array('day' => 1)));
+ $this->assertEquals('2009-01-01 00:00:00', (string)$d->add(array('year' => 1)));
+ $this->assertEquals('2008-01-01 04:00:00', (string)$d->add(14400));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+require_once 'PHPUnit/Framework.php';
+
+require_once 'Horde/Date/Recurrence.php';
+
+class Horde_Date_RecurrenceTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ */
+ public function testHash()
+ {
+ $r = &new Horde_Date_Recurrence(0);
+ $r->setRecurType(Horde_Date_Recurrence::RECUR_DAILY);
+ $r->addException(1970, 1, 1);
+ $r->addException(1970, 1, 3);
+ $r->addException(1970, 1, 4);
+
+ $r->setRecurEnd(new Horde_Date(86400*3));
+
+ $s = &new Horde_Date_Recurrence(0);
+ $s->fromHash($r->toHash());
+
+ $this->assertTrue($s->hasRecurEnd());
+
+ $next = $s->nextRecurrence(new Horde_Date($s->start));
+ $this->assertEquals(1, $next->mday);
+ $this->assertTrue($s->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $s->nextRecurrence($next);
+ $this->assertFalse($s->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $s->nextRecurrence($next);
+ $this->assertTrue($s->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $s->nextRecurrence($next);
+ $this->assertTrue($s->hasException($next->year, $next->month, $next->mday));
+
+ $this->assertEquals(3, count($s->getExceptions()));
+ $this->assertTrue($s->hasActiveRecurrence());
+ $s->addException(1970, 1, 2);
+ $this->assertFalse($s->hasActiveRecurrence());
+ }
+
+ /**
+ */
+ public function testCompletions()
+ {
+ $r = &new Horde_Date_Recurrence(0);
+ $r->setRecurType(Horde_Date_Recurrence::RECUR_DAILY);
+ $r->addCompletion(1970, 1, 2);
+ $this->assertTrue($r->hasCompletion(1970, 1, 2));
+ $this->assertEquals(1, count($r->getCompletions()));
+ $r->addCompletion(1970, 1, 4);
+ $this->assertEquals(2, count($r->getCompletions()));
+ $r->deleteCompletion(1970, 1, 2);
+ $this->assertEquals(1, count($r->getCompletions()));
+ $this->assertFalse($r->hasCompletion(1970, 1, 2));
+ $r->addCompletion(1970, 1, 2);
+ $r->addException(1970, 1, 1);
+ $r->addException(1970, 1, 3);
+
+ $next = $r->nextRecurrence(new Horde_Date($r->start));
+ $this->assertEquals(1, $next->mday);
+ $this->assertTrue($r->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $r->nextRecurrence($next);
+ $this->assertTrue($r->hasCompletion($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $r->nextRecurrence($next);
+ $this->assertTrue($r->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $r->nextRecurrence($next);
+ $this->assertTrue($r->hasCompletion($next->year, $next->month, $next->mday));
+
+ $r->setRecurEnd(new Horde_Date(86400*3));
+ $this->assertTrue($r->hasRecurEnd());
+
+ $this->assertFalse($r->hasActiveRecurrence());
+
+ $s = &new Horde_Date_Recurrence(0);
+ $s->fromHash($r->toHash());
+
+ $this->assertTrue($s->hasRecurEnd());
+
+ $next = $s->nextRecurrence(new Horde_Date($s->start));
+ $this->assertEquals(1, $next->mday);
+ $this->assertTrue($s->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $s->nextRecurrence($next);
+ $this->assertTrue($s->hasCompletion($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $s->nextRecurrence($next);
+ $this->assertTrue($s->hasException($next->year, $next->month, $next->mday));
+ $next->mday++;
+ $next = $s->nextRecurrence($next);
+ $this->assertTrue($s->hasCompletion($next->year, $next->month, $next->mday));
+
+ $this->assertEquals(2, count($s->getCompletions()));
+ $this->assertEquals(2, count($s->getExceptions()));
+ $this->assertFalse($s->hasActiveRecurrence());
+
+ $this->assertEquals(2, count($s->getCompletions()));
+ $s->deleteCompletion(1970, 1, 2);
+ $this->assertEquals(1, count($s->getCompletions()));
+ $s->deleteCompletion(1970, 1, 4);
+ $this->assertEquals(0, count($s->getCompletions()));
+ }
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_DayNameTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $mondays = new Horde_Date_Repeater_DayName('monday');
+ $mondays->now = $this->now;
+
+ $span = $mondays->next('future');
+ $this->assertEquals('2006-08-21', $span->begin->format('Y-m-d'));
+ $this->assertEquals('2006-08-22', $span->end->format('Y-m-d'));
+
+ $span = $mondays->next('future');
+ $this->assertEquals('2006-08-28', $span->begin->format('Y-m-d'));
+ $this->assertEquals('2006-08-29', $span->end->format('Y-m-d'));
+ }
+
+ public function testNextPast()
+ {
+ $mondays = new Horde_Date_Repeater_DayName('monday');
+ $mondays->now = $this->now;
+
+ $span = $mondays->next('past');
+ $this->assertEquals('2006-08-14', $span->begin->format('Y-m-d'));
+ $this->assertEquals('2006-08-15', $span->end->format('Y-m-d'));
+
+ $span = $mondays->next('past');
+ $this->assertEquals('2006-08-07', $span->begin->format('Y-m-d'));
+ $this->assertEquals('2006-08-08', $span->end->format('Y-m-d'));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_DayTest extends PHPUnit_Framework_TestCase
+{
+ public function testNextFuture()
+ {
+ $repeater = new Horde_Date_Repeater_Day();
+ $repeater->now = new Horde_Date('2009-01-01');
+ $this->assertEquals('(2009-01-02 00:00:00..2009-01-03 00:00:00)', (string)$repeater->next('future'));
+ }
+
+ public function testNextPast()
+ {
+ $repeater = new Horde_Date_Repeater_Day();
+ $repeater->now = new Horde_Date('2009-01-01');
+ $this->assertEquals('(2008-12-31 00:00:00..2009-01-01 00:00:00)', (string)$repeater->next('past'));
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_HourTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $hours = new Horde_Date_Repeater_Hour();
+ $hours->now = $this->now;
+
+ $nextHour = $hours->next('future');
+ $this->assertEquals('2006-08-16 15:00:00', (string)$nextHour->begin);
+ $this->assertEquals('2006-08-16 16:00:00', (string)$nextHour->end);
+
+ $nextNextHour = $hours->next('future');
+ $this->assertEquals('2006-08-16 16:00:00', (string)$nextNextHour->begin);
+ $this->assertEquals('2006-08-16 17:00:00', (string)$nextNextHour->end);
+ }
+
+ public function testNextPast()
+ {
+ $hours = new Horde_Date_Repeater_Hour();
+ $hours->now = $this->now;
+
+ $pastHour = $hours->next('past');
+ $this->assertEquals('2006-08-16 13:00:00', (string)$pastHour->begin);
+ $this->assertEquals('2006-08-16 14:00:00', (string)$pastHour->end);
+
+ $pastPastHour = $hours->next('past');
+ $this->assertEquals('2006-08-16 12:00:00', (string)$pastPastHour->begin);
+ $this->assertEquals('2006-08-16 13:00:00', (string)$pastPastHour->end);
+ }
+
+ public function testThis()
+ {
+ $hours = new Horde_Date_Repeater_Hour();
+ $hours->now = new Horde_Date('2006-08-16 14:30:00');
+
+ $thisHour = $hours->this('future');
+ $this->assertEquals('2006-08-16 14:31:00', (string)$thisHour->begin);
+ $this->assertEquals('2006-08-16 15:00:00', (string)$thisHour->end);
+
+ $thisHour = $hours->this('past');
+ $this->assertEquals('2006-08-16 14:00:00', (string)$thisHour->begin);
+ $this->assertEquals('2006-08-16 14:30:00', (string)$thisHour->end);
+ }
+
+ public function testOffset()
+ {
+ $span = new Horde_Date_Span($this->now, $this->now->add(1));
+ $hours = new Horde_Date_Repeater_Hour();
+
+ $offsetSpan = $hours->offset($span, 3, 'future');
+ $this->assertEquals('2006-08-16 17:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-08-16 17:00:01', (string)$offsetSpan->end);
+
+ $offsetSpan = $hours->offset($span, 24, 'past');
+ $this->assertEquals('2006-08-15 14:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-08-15 14:00:01', (string)$offsetSpan->end);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_MonthNameTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $mays = new Horde_Date_Repeater_MonthName('may');
+ $mays->now = $this->now;
+
+ $nextMay = $mays->next('future');
+ $this->assertEquals('2007-05-01 00:00:00', (string)$nextMay->begin);
+ $this->assertEquals('2007-06-01 00:00:00', (string)$nextMay->end);
+
+ $nextNextMay = $mays->next('future');
+ $this->assertEquals('2008-05-01 00:00:00', (string)$nextNextMay->begin);
+ $this->assertEquals('2008-06-01 00:00:00', (string)$nextNextMay->end);
+
+ $decembers = new Horde_Date_Repeater_MonthName('december');
+ $decembers->now = $this->now;
+
+ $nextDecember = $decembers->next('future');
+ $this->assertEquals('2006-12-01 00:00:00', (string)$nextDecember->begin);
+ $this->assertEquals('2007-01-01 00:00:00', (string)$nextDecember->end);
+ }
+
+ public function testNextPast()
+ {
+ $mays = new Horde_Date_Repeater_MonthName('may');
+ $mays->now = $this->now;
+
+ $this->assertEquals('2006-05-01 00:00:00', (string)$mays->next('past')->begin);
+ $this->assertEquals('2005-05-01 00:00:00', (string)$mays->next('past')->begin);
+ }
+
+ public function testThis()
+ {
+ $octobers = new Horde_Date_Repeater_MonthName('october');
+ $octobers->now = $this->now;
+
+ $thisOctober = $octobers->this('future');
+ $this->assertEquals('2006-10-01 00:00:00', (string)$thisOctober->begin);
+ $this->assertEquals('2006-11-01 00:00:00', (string)$thisOctober->end);
+
+ $aprils = new Horde_Date_Repeater_MonthName('april');
+ $aprils->now = $this->now;
+
+ $thisApril = $aprils->this('past');
+ $this->assertEquals('2006-04-01 00:00:00', (string)$thisApril->begin);
+ $this->assertEquals('2006-05-01 00:00:00', (string)$thisApril->end);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_MonthTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testOffsetFuture()
+ {
+ $span = new Horde_Date_Span($this->now, $this->now->add(60));
+ $repeater = new Horde_Date_Repeater_Month();
+ $offsetSpan = $repeater->offset($span, 1, 'future');
+
+ $this->assertEquals('2006-09-16 14:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-09-16 14:01:00', (string)$offsetSpan->end);
+ }
+
+ public function testOffsetPast()
+ {
+ $span = new Horde_Date_Span($this->now, $this->now->add(60));
+ $repeater = new Horde_Date_Repeater_Month();
+ $offsetSpan = $repeater->offset($span, 1, 'past');
+
+ $this->assertEquals('2006-07-16 14:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-07-16 14:01:00', (string)$offsetSpan->end);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_TimeTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $t = new Horde_Date_Repeater_Time('4:00');
+ $t->now = $this->now;
+
+ $this->assertEquals('2006-08-16 16:00:00', (string)$t->next('future')->begin);
+ $this->assertEquals('2006-08-17 04:00:00', (string)$t->next('future')->begin);
+
+ $t = new Horde_Date_Repeater_Time('13:00');
+ $t->now = $this->now;
+
+ $this->assertEquals('2006-08-17 13:00:00', (string)$t->next('future')->begin);
+ $this->assertEquals('2006-08-18 13:00:00', (string)$t->next('future')->begin);
+
+ $t = new Horde_Date_Repeater_Time('0400');
+ $t->now = $this->now;
+
+ $this->assertEquals('2006-08-17 04:00:00', (string)$t->next('future')->begin);
+ $this->assertEquals('2006-08-18 04:00:00', (string)$t->next('future')->begin);
+ }
+
+ public function testNextPast()
+ {
+ $t = new Horde_Date_Repeater_Time('4:00');
+ $t->now = $this->now;
+ $this->assertEquals('2006-08-16 04:00:00', (string)$t->next('past')->begin);
+ $this->assertEquals('2006-08-15 16:00:00', (string)$t->next('past')->begin);
+
+ $t = new Horde_Date_Repeater_Time('13:00');
+ $t->now = $this->now;
+ $this->assertEquals('2006-08-16 13:00:00', (string)$t->next('past')->begin);
+ $this->assertEquals('2006-08-15 13:00:00', (string)$t->next('past')->begin);
+ }
+
+ public function testType()
+ {
+ $t = new Horde_Date_Repeater_Time('4');
+ $this->assertEquals(14400, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('14');
+ $this->assertEquals(50400, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('4:00');
+ $this->assertEquals(14400, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('4:30');
+ $this->assertEquals(16200, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('1400');
+ $this->assertEquals(50400, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('0400');
+ $this->assertEquals(14400, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('04');
+ $this->assertEquals(14400, $t->type);
+
+ $t = new Horde_Date_Repeater_Time('400');
+ $this->assertEquals(14400, $t->type);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_WeekTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $weeks = new Horde_Date_Repeater_Week();
+ $weeks->now = $this->now;
+
+ $nextWeek = $weeks->next('future');
+ $this->assertEquals('2006-08-20 00:00:00', (string)$nextWeek->begin);
+ $this->assertEquals('2006-08-27 00:00:00', (string)$nextWeek->end);
+
+ $nextNextWeek = $weeks->next('future');
+ $this->assertEquals('2006-08-27 00:00:00', (string)$nextNextWeek->begin);
+ $this->assertEquals('2006-09-03 00:00:00', (string)$nextNextWeek->end);
+ }
+
+ public function testNextPast()
+ {
+ $weeks = new Horde_Date_Repeater_Week();
+ $weeks->now = $this->now;
+
+ $lastWeek = $weeks->next('past');
+ $this->assertEquals('2006-08-06 00:00:00', (string)$lastWeek->begin);
+ $this->assertEquals('2006-08-13 00:00:00', (string)$lastWeek->end);
+
+ $lastLastWeek = $weeks->next('past');
+ $this->assertEquals('2006-07-30 00:00:00', (string)$lastLastWeek->begin);
+ $this->assertEquals('2006-08-06 00:00:00', (string)$lastLastWeek->end);
+ }
+
+ public function testThisFuture()
+ {
+ $weeks = new Horde_Date_Repeater_Week();
+ $weeks->now = $this->now;
+
+ $thisWeek = $weeks->this('future');
+ $this->assertEquals('2006-08-16 15:00:00', (string)$thisWeek->begin);
+ $this->assertEquals('2006-08-20 00:00:00', (string)$thisWeek->end);
+ }
+
+ public function testThisPast()
+ {
+ $weeks = new Horde_Date_Repeater_Week();
+ $weeks->now = $this->now;
+
+ $thisWeek = $weeks->this('past');
+ $this->assertEquals('2006-08-13 00:00:00', (string)$thisWeek->begin);
+ $this->assertEquals('2006-08-16 14:00:00', (string)$thisWeek->end);
+ }
+
+ public function testOffset()
+ {
+ $weeks = new Horde_Date_Repeater_Week();
+ $span = new Horde_Date_Span($this->now, $this->now->add(1));
+
+ $offsetSpan = $weeks->offset($span, 3, 'future');
+ $this->assertEquals('2006-09-06 14:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-09-06 14:00:01', (string)$offsetSpan->end);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_WeekendTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $weekend = new Horde_Date_Repeater_Weekend();
+ $weekend->now = $this->now;
+
+ $nextWeekend = $weekend->next('future');
+ $this->assertEquals('2006-08-19 00:00:00', (string)$nextWeekend->begin);
+ $this->assertEquals('2006-08-21 00:00:00', (string)$nextWeekend->end);
+ }
+
+ public function testNextPast()
+ {
+ $weekend = new Horde_Date_Repeater_Weekend();
+ $weekend->now = $this->now;
+
+ $lastWeekend = $weekend->next('past');
+ $this->assertEquals('2006-08-12 00:00:00', (string)$lastWeekend->begin);
+ $this->assertEquals('2006-08-14 00:00:00', (string)$lastWeekend->end);
+ }
+
+ public function testThisFuture()
+ {
+ $weekend = new Horde_Date_Repeater_Weekend();
+ $weekend->now = $this->now;
+
+ $thisWeekend = $weekend->this('future');
+ $this->assertEquals('2006-08-19 00:00:00', (string)$thisWeekend->begin);
+ $this->assertEquals('2006-08-21 00:00:00', (string)$thisWeekend->end);
+ }
+
+ public function testThisPast()
+ {
+ $weekend = new Horde_Date_Repeater_Weekend();
+ $weekend->now = $this->now;
+
+ $thisWeekend = $weekend->this('past');
+ $this->assertEquals('2006-08-12 00:00:00', (string)$thisWeekend->begin);
+ $this->assertEquals('2006-08-14 00:00:00', (string)$thisWeekend->end);
+ }
+
+ public function testThisNone()
+ {
+ $weekend = new Horde_Date_Repeater_Weekend();
+ $weekend->now = $this->now;
+
+ $thisWeekend = $weekend->this('none');
+ $this->assertEquals('2006-08-19 00:00:00', (string)$thisWeekend->begin);
+ $this->assertEquals('2006-08-21 00:00:00', (string)$thisWeekend->end);
+ }
+
+ public function testOffset()
+ {
+ $weekend = new Horde_Date_Repeater_Weekend();
+ $span = new Horde_Date_Span($this->now, $this->now->add(1));
+
+ $offsetSpan = $weekend->offset($span, 3, 'future');
+ $this->assertEquals('2006-09-02 00:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-09-02 00:00:01', (string)$offsetSpan->end);
+
+ $offsetSpan = $weekend->offset($span, 1, 'past');
+ $this->assertEquals('2006-08-12 00:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-08-12 00:00:01', (string)$offsetSpan->end);
+
+ $offsetSpan = $weekend->offset($span, 0, 'future');
+ $this->assertEquals('2006-08-12 00:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2006-08-12 00:00:01', (string)$offsetSpan->end);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_Repeater_YearTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->now = new Horde_Date('2006-08-16 14:00:00');
+ }
+
+ public function testNextFuture()
+ {
+ $years = new Horde_Date_Repeater_Year();
+ $years->now = $this->now;
+
+ $nextYear = $years->next('future');
+ $this->assertEquals('2007-01-01', $nextYear->begin->format('Y-m-d'));
+ $this->assertEquals('2008-01-01', $nextYear->end->format('Y-m-d'));
+
+ $nextNextYear = $years->next('future');
+ $this->assertEquals('2008-01-01', $nextNextYear->begin->format('Y-m-d'));
+ $this->assertEquals('2009-01-01', $nextNextYear->end->format('Y-m-d'));
+ }
+
+ public function testNextPast()
+ {
+ $years = new Horde_Date_Repeater_Year();
+ $years->now = $this->now;
+
+ $lastYear = $years->next('past');
+ $this->assertEquals('2005-01-01', $lastYear->begin->format('Y-m-d'));
+ $this->assertEquals('2006-01-01', $lastYear->end->format('Y-m-d'));
+
+ $lastLastYear = $years->next('past');
+ $this->assertEquals('2004-01-01', $lastLastYear->begin->format('Y-m-d'));
+ $this->assertEquals('2005-01-01', $lastLastYear->end->format('Y-m-d'));
+ }
+
+ public function testThis()
+ {
+ $years = new Horde_Date_Repeater_Year();
+ $years->now = $this->now;
+
+ $thisYear = $years->this('future');
+ $this->assertEquals('2006-08-17', $thisYear->begin->format('Y-m-d'));
+ $this->assertEquals('2007-01-01', $thisYear->end->format('Y-m-d'));
+
+ $thisYear = $years->this('past');
+ $this->assertEquals('2006-01-01', $thisYear->begin->format('Y-m-d'));
+ $this->assertEquals('2006-08-16', $thisYear->end->format('Y-m-d'));
+ }
+
+ public function testOffset()
+ {
+ $span = new Horde_Date_Span($this->now, $this->now->add(1));
+ $years = new Horde_Date_Repeater_Year();
+
+ $offsetSpan = $years->offset($span, 3, 'future');
+ $this->assertEquals('2009-08-16 14:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('2009-08-16 14:00:01', (string)$offsetSpan->end);
+
+ $offsetSpan = $years->offset($span, 10, 'past');
+ $this->assertEquals('1996-08-16 14:00:00', (string)$offsetSpan->begin);
+ $this->assertEquals('1996-08-16 14:00:01', (string)$offsetSpan->end);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+
+/**
+ * @category Horde
+ * @package Horde_Date
+ * @subpackage UnitTests
+ */
+class Horde_Date_SpanTest extends PHPUnit_Framework_TestCase
+{
+ public function testWidth()
+ {
+ $s = new Horde_Date_Span(new Horde_Date('2006-08-16 00:00:00'), new Horde_Date('2006-08-17 00:00:00'));
+ $this->assertEquals(60 * 60 * 24, $s->width());
+ }
+
+ public function testIncludes()
+ {
+ $s = new Horde_Date_Span(new Horde_Date('2006-08-16 00:00:00'), new Horde_Date('2006-08-17 00:00:00'));
+ $this->assertTrue($s->includes(new Horde_Date('2006-08-16 12:00:00')));
+ $this->assertFalse($s->includes(new Horde_Date('2006-08-15 00:00:00')));
+ $this->assertFalse($s->includes(new Horde_Date('2006-08-18 00:00:00')));
+ }
+
+ public function testSpanMath()
+ {
+ $s = new Horde_Date_Span(new Horde_Date(1), new Horde_Date(2));
+ $this->assertEquals(2, $s->add(1)->begin->timestamp());
+ $this->assertEquals(3, $s->add(1)->end->timestamp());
+ $this->assertEquals(0, $s->sub(1)->begin->timestamp());
+ $this->assertEquals(1, $s->sub(1)->end->timestamp());
+ }
+
+}
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+X-WR-CALNAME:Kronolith Bug
+PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
+X-WR-RELCALID:6478A81B-4979-4F9D-B9DD-E3F8DD360B96
+X-WR-TIMEZONE:US/Eastern
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+BEGIN:VTIMEZONE
+TZID:US/Eastern
+LAST-MODIFIED:20060616T214021Z
+BEGIN:DAYLIGHT
+DTSTART:20060402T070000
+TZOFFSETTO:-0400
+TZOFFSETFROM:+0000
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20061029T020000
+TZOFFSETTO:-0500
+TZOFFSETFROM:-0400
+TZNAME:EST
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060616T180000
+DTEND;TZID=US/Eastern:20060616T190000
+SUMMARY:Test 2
+UID:3496A948-E0AB-49FF-B76F-45B700B86AF2
+SEQUENCE:8
+DTSTAMP:20060616T212821Z
+RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20060618T035959Z
+END:VEVENT
+END:VCALENDAR