diff --git a/plugins/calendar/calendar.js b/plugins/calendar/calendar.js index e4bf8348..1efbcde6 100644 --- a/plugins/calendar/calendar.js +++ b/plugins/calendar/calendar.js @@ -104,7 +104,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { } var buttons = {}; - if (calendar.editable) { + if (calendar.editable && event.editable !== false) { buttons[rcmail.gettext('edit', 'calendar')] = function() { event_edit_dialog('edit', event); }; diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 833255a6..2b80e782 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -28,6 +28,7 @@ class calendar extends rcube_plugin public $task = '?(?!login|logout).*'; public $rc; public $driver; + public $home; // declare public to be used in other classes public $ical; public $ui; @@ -383,7 +384,11 @@ class calendar extends rcube_plugin */ function load_events() { - $events = $this->driver->load_events(get_input_value('start', RCUBE_INPUT_GET), get_input_value('end', RCUBE_INPUT_GET), get_input_value('source', RCUBE_INPUT_GET)); + $events = $this->driver->load_events( + get_input_value('start', RCUBE_INPUT_GET), + get_input_value('end', RCUBE_INPUT_GET), + get_input_value('source', RCUBE_INPUT_GET) + ); echo $this->encode($events); exit; } @@ -511,6 +516,10 @@ class calendar extends rcube_plugin if ($event['recurrence']) $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']); + // TEMPORARY: recurring instances are immutable + if ($event['recurrence_id']) + $event['editable'] = false; + $json[] = array( 'start' => date('c', $event['start']), // ISO 8601 date (added in PHP 5) 'end' => date('c', $event['end']), // ISO 8601 date (added in PHP 5) @@ -579,7 +588,24 @@ class calendar extends rcube_plugin */ private function _recurrence_text($rrule) { - // TODO: implement this + // TODO: finish this + $text = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']); + switch ($rrule['FREQ']) { + case 'DAILY': + $text .= $this->gettext('days'); + break; + case 'WEEKLY': + $text .= $this->gettext('weeks'); + break; + case 'MONTHLY': + $text .= $this->gettext('months'); + break; + case 'YEARY': + $text .= $this->gettext('years'); + break; + } + + return $text; } /** @@ -595,5 +621,27 @@ class calendar extends rcube_plugin return false; } + + /** + * Convert the internal structured data into a vcalendar rrule 2.0 string + */ + public static function to_rrule($recurrence) + { + if (is_string($recurrence)) + return $recurrence; + + $rrule = ''; + foreach ((array)$recurrence as $k => $val) { + $k = strtoupper($k); + switch ($k) { + case 'UNTIL': + $val = gmdate('Ymd\THis', $val); + break; + } + $rrule .= $k . '=' . $val . ';'; + } + + return $rrule; + } } diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 8909fc4a..565bfc8d 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -53,6 +53,9 @@ class database_driver extends calendar_driver $this->cal = $cal; $this->rc = $cal->rc; + // load library classes + require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php'); + // read database config $this->db_events = $this->rc->config->get('db_table_events', $this->db_events); $this->db_calendars = $this->rc->config->get('db_table_calendars', $this->db_calendars); @@ -161,7 +164,11 @@ class database_driver extends calendar_driver $event['alarms'], $event['notifyat'] ); - return $this->rc->db->insert_id($this->sequence_events); + + if ($success = $this->rc->db->insert_id($this->sequence_events)) + $this->_update_recurring($event); + + return $success; } return false; @@ -199,7 +206,11 @@ class database_driver extends calendar_driver $event['notifyat'], $event['id'] ); - return $this->rc->db->affected_rows($query); + + if ($success = $this->rc->db->affected_rows($query)) + $this->_update_recurring($event); + + return $success; } return false; @@ -211,26 +222,22 @@ class database_driver extends calendar_driver private function _save_preprocess($event) { // compose vcalendar-style recurrencue rule from structured data - $rrule = ''; - if (is_array($event['recurrence'])) { - foreach ($event['recurrence'] as $k => $val) { - $k = strtoupper($k); - switch ($k) { - case 'UNTIL': - $val = gmdate('Ymd\THis', $val); - break; - } - $rrule .= $k . '=' . $val . ';'; - } - } - else if (is_string($event['recurrence'])) - $rrule = $event['recurrence']; - + $rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : ''; $event['recurrence'] = rtrim($rrule, ';'); $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]); $event['allday'] = $event['allday'] ? 1 : 0; // compute absolute time to notify the user + $event['notifyat'] = $this->_get_notification($event); + + return $event; + } + + /** + * Compute absolute time to notify the user + */ + private function _get_notification($event) + { if ($event['alarms']) { list($trigger, $action) = explode(':', $event['alarms']); $notify = calendar::parse_alaram_value($trigger); @@ -253,12 +260,63 @@ class database_driver extends calendar_driver } if ($notify_at > time()) - $event['notifyat'] = date('Y-m-d H:i:s', $notify_at); + return date('Y-m-d H:i:s', $notify_at); + } + + return null; + } + + /** + * Insert "fake" entries for recurring occurences of this event + */ + private function _update_recurring($event) + { + if (empty($this->calendars)) + return; + + // clear existing recurrence copies + $this->rc->db->query( + "DELETE FROM " . $this->db_events . " + WHERE recurrence_id=? + AND calendar_id IN (" . $this->calendar_ids . ")", + $event['id'] + ); + + // create new fake entries + if ($event['recurrence']) { + // TODO: replace Horde classes with something that has less than 6'000 lines of code + $recurrence = new Horde_Date_Recurrence($event['start']); + $recurrence->fromRRule20($event['recurrence']); + + $duration = $event['end'] - $event['start']; + $next = new Horde_Date($event['start']); + while ($next = $recurrence->nextActiveRecurrence(array('year' => $next->year, 'month' => $next->month, 'mday' => $next->mday + 1, 'hour' => $next->hour, 'min' => $next->min, 'sec' => $next->sec))) { + $next_ts = $next->timestamp(); + $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_ts, 'end' => $next_ts + $duration)); + $query = $this->rc->db->query(sprintf( + "INSERT INTO " . $this->db_events . " + (calendar_id, recurrence_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, notifyat) + SELECT calendar_id, ?, %s, %s, uid, %s, %s, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, ? + FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")", + $this->rc->db->now(), + $this->rc->db->now(), + $this->rc->db->fromunixtime($next_ts), + $this->rc->db->fromunixtime($next_ts + $duration) + ), + $event['id'], + $notify_at, + $event['id'] + ); + + if (!$this->rc->db->affected_rows($query)) + break; + + // stop adding events for inifinite recurrence after 20 years + if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next->year > date('Y') + 20)) + break; + } } - else - $event['notifyat'] = null; - return $event; } /** @@ -284,7 +342,11 @@ class database_driver extends calendar_driver $event['notifyat'], $event['id'] ); - return $this->rc->db->affected_rows($query); + + if ($success = $this->rc->db->affected_rows($query)) + $this->_update_recurring($event); + + return $success; } return false; @@ -312,7 +374,11 @@ class database_driver extends calendar_driver $event['notifyat'], $event['id'] ); - return $this->rc->db->affected_rows($query); + + if ($success = $this->rc->db->affected_rows($query)) + $this->_update_recurring($event); + + return $success; } return false; @@ -329,8 +395,9 @@ class database_driver extends calendar_driver if (!empty($this->calendars)) { $query = $this->rc->db->query( "DELETE FROM " . $this->db_events . " - WHERE event_id=? + WHERE (event_id=? OR recurrence_id=?) AND calendar_id IN (" . $this->calendar_ids . ")", + $event['id'], $event['id'] ); return $this->rc->db->affected_rows($query); diff --git a/plugins/calendar/drivers/database/sql/mysql.sql b/plugins/calendar/drivers/database/sql/mysql.sql index 5b27a4aa..f0f4ca93 100644 --- a/plugins/calendar/drivers/database/sql/mysql.sql +++ b/plugins/calendar/drivers/database/sql/mysql.sql @@ -44,6 +44,7 @@ CREATE TABLE `events` ( `attendees` text DEFAULT NULL, `notifyat` datetime DEFAULT NULL, PRIMARY KEY(`event_id`), + INDEX `recurrence_idx` (`recurrence_id`), CONSTRAINT `fk_events_calendar_id` FOREIGN KEY (`calendar_id`) REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index ac790ab8..bc8f3fe2 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -84,14 +84,41 @@ class kolab_calendar */ public function list_events($start, $end) { + // use Horde classes to compute recurring instances + require_once 'Horde/Date/Recurrence.php'; + $this->_fetch_events(); $events = array(); foreach ($this->events as $id => $event) { - // TODO: also list recurring events + // list events in requested time window if ($event['start'] <= $end && $event['end'] >= $start) { $events[] = $event; } + + // resolve recurring events (maybe move to _fetch_events() for general use?) + if ($event['recurrence']) { + $recurrence = new Horde_Date_Recurrence($event['start']); + $recurrence->fromRRule20(calendar::to_rrule($event['recurrence'])); + + $duration = $event['end'] - $event['start']; + $next = new Horde_Date($event['start']); + while ($next = $recurrence->nextActiveRecurrence(array('year' => $next->year, 'month' => $next->month, 'mday' => $next->mday + 1, 'hour' => $next->hour, 'min' => $next->min, 'sec' => $next->sec))) { + $rec_start = $next->timestamp(); + $rec_end = $rec_start + $duration; + + // add to output if in range + if ($rec_start <= $end && $rec_end >= $start) { + $rec_event = $event; + $rec_event['recurrence_id'] = $event['id']; + $rec_event['start'] = $rec_start; + $rec_event['end'] = $rec_end; + $events[] = $rec_event; + } + else if ($start_ts > $end) // stop loop if out of range + break; + } + } } return $events; @@ -147,6 +174,54 @@ class kolab_calendar $allday = $start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']); if ($allday) // in Roundcube all-day events only go until 23:59:59 of the last day $rec['end-date']--; + + // convert alarm time into internal format + if ($rec['alarm']) { + $alarm_value = $rec['alarm']; + $alarm_unit = 'M'; + if ($rec['alarm'] % 1440 == 0) { + $alarm_value /= 1440; + $alarm_unit = 'D'; + } + else if ($rec['alarm'] % 60 == 0) { + $alarm_value /= 60; + $alarm_unit = 'H'; + } + $alarm_value *= -1; + } + + // convert recurrence rules into internal pseudo-vcalendar format + if ($recurrence = $rec['recurrence']) { + $rrule = array( + 'FREQ' => strtoupper($recurrence['cycle']), + 'INTERVAL' => intval($recurrence['interval']), + ); + + if ($recurrence['range-type'] == 'number') + $rrule['COUNT'] = intval($recurrence['range']); + else if ($recurrence['range-type'] == 'date') + $rrule['UNTIL'] = strtotime($recurrence['range']); + + if ($recurrence['day']) { + $byday = array(); + $prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : ''; + foreach ($recurrence['day'] as $day) + $byday[] = $prefix . substr(strtoupper($day), 0, 2); + $rrule['BYDAY'] = join(',', $byday); + } + if ($recurrence['daynumber']) { + if ($recurrence['type'] == 'monthday') + $rrule['BYMONTHDAY'] = $recurrence['daynumber']; + else if ($recurrence['type'] == 'yearday') + $rrule['BYYEARDAY'] = $recurrence['daynumber']; + } + if ($rec['month']) { + $monthmap = 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); + $rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]); + } + + // TODO: handle exclusions (not yet supported by the internal format) + } $sensitivity_map = array_flip($this->sensitivity_map); @@ -159,6 +234,8 @@ class kolab_calendar 'start' => $rec['start-date'], 'end' => $rec['end-date'], 'all_day' => $allday, + 'recurrence' => $rrule, + 'alarms' => $alarm_value . $alarm_unit, 'categories' => $rec['categories'], 'free_busy' => $rec['show-time-as'], 'priority' => 1, // normal diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php new file mode 100644 index 00000000..166224b1 --- /dev/null +++ b/plugins/calendar/lib/Horde_Date_Recurrence.php @@ -0,0 +1,6646 @@ +_supportedSpecs .= 'bBpxX'; + } + + if (is_array($date) || is_object($date)) { + 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 (is_array($date) && isset($date['day']) && + is_numeric($date['day'])) { + $this->mday = (int)$date['day']; + } + // 'minute' key also from Horde_Form_datetime + if (is_array($date) && isset($date['minute'])) { + $this->min = $date['minute']; + } + } elseif (!is_null($date)) { + // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'. + if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})Z?/', $date, $parts)) { + $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]; + } else { + // 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']; + } + } + } + } + + /** + * @static + */ + 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 day of the year (1-366) that corresponds to the + * first day of the given week. + * + * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php + * + * @param integer $week The week of the year to find the first day of. + * @param integer $year The year to calculate for. + * + * @return integer The day of the year of the first day of the given week. + */ + function firstDayOfWeek($week, $year) + { + $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1)); + $start = $jan1->dayOfWeek(); + if ($start > HORDE_DATE_THURSDAY) { + $start -= 7; + } + return (($week * 7) - (7 + $start)) + 1; + } + + /** + * @static + */ + function daysInMonth($month, $year) + { + if ($month == 2) { + if (Horde_Date::isLeapYear($year)) { + return 29; + } else { + return 28; + } + } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) { + return 30; + } else { + return 31; + } + } + + /** + * Return the day of the week (0 = Sunday, 6 = Saturday) of this + * object's 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() + { + $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); + $dayOfYear = $this->mday + $monthTotals[$this->month - 1]; + if (Horde_Date::isLeapYear($this->year) && $this->month > 2) { + ++$dayOfYear; + } + + return $dayOfYear; + } + + /** + * Returns the week of the month. + * + * @since Horde 3.2 + * + * @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. + $day = 31; + $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0)); + while ($date->dayOfWeek() != HORDE_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 < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) { + return false; + } + + $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(); + + return true; + } + + function dump($prefix = '') + { + echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "
\n"; + } + + /** + * Is the date currently represented by this object a valid date? + * + * @return boolean Validity, counting leap years, etc. + */ + function isValid() + { + if ($this->year < 0 || $this->year > 9999) { + return false; + } + return checkdate($this->month, $this->mday, $this->year); + } + + /** + * Correct any over- or underflows in any of the date's members. + * + * @param integer $mask We may not want to correct some overflows. + */ + function correct($mask = HORDE_DATE_MASK_ALLPARTS) + { + if ($mask & HORDE_DATE_MASK_SECOND) { + while ($this->sec < 0) { + --$this->min; + $this->sec += 60; + } + while ($this->sec > 59) { + ++$this->min; + $this->sec -= 60; + } + } + + if ($mask & HORDE_DATE_MASK_MINUTE) { + while ($this->min < 0) { + --$this->hour; + $this->min += 60; + } + while ($this->min > 59) { + ++$this->hour; + $this->min -= 60; + } + } + + if ($mask & HORDE_DATE_MASK_HOUR) { + while ($this->hour < 0) { + --$this->mday; + $this->hour += 24; + } + while ($this->hour > 23) { + ++$this->mday; + $this->hour -= 24; + } + } + + if ($mask & HORDE_DATE_MASK_MONTH) { + while ($this->month > 12) { + ++$this->year; + $this->month -= 12; + } + while ($this->month < 1) { + --$this->year; + $this->month += 12; + } + } + + if ($mask & HORDE_DATE_MASK_DAY) { + while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) { + $this->mday -= Horde_Date::daysInMonth($this->month, $this->year); + ++$this->month; + $this->correct(HORDE_DATE_MASK_MONTH); + } + while ($this->mday < 1) { + --$this->month; + $this->correct(HORDE_DATE_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 $date The date to compare to. + * + * @return integer == 0 if the dates are equal + * >= 1 if this date is greater (later) + * <= -1 if the other date is greater (later) + */ + function compareDate($date) + { + if (!is_a($date, 'Horde_Date')) { + $date = new Horde_Date($date); + } + + if ($this->year != $date->year) { + return $this->year - $date->year; + } + if ($this->month != $date->month) { + return $this->month - $date->month; + } + + return $this->mday - $date->mday; + } + + /** + * 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 $date The date to compare to. + * + * @return integer == 0 if the dates are equal + * >= 1 if this date is greater (later) + * <= -1 if the other date is greater (later) + */ + function compareTime($date) + { + if (!is_a($date, 'Horde_Date')) { + $date = new Horde_Date($date); + } + + if ($this->hour != $date->hour) { + return $this->hour - $date->hour; + } + if ($this->min != $date->min) { + return $this->min - $date->min; + } + + return $this->sec - $date->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 $date The date to compare to. + * + * @return integer == 0 if the dates are equal + * >= 1 if this date is greater (later) + * <= -1 if the other date is greater (later) + */ + function compareDateTime($date) + { + if (!is_a($date, 'Horde_Date')) { + $date = new Horde_Date($date); + } + + if ($diff = $this->compareDate($date)) { + return $diff; + } + + return $this->compareTime($date); + } + + /** + * 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) + { + $secs = $this->format('Z'); + + if ($secs < 0) { + $sign = '-'; + $secs = -$secs; + } else { + $sign = '+'; + } + $colon = $colon ? ':' : ''; + $mins = intval(($secs + 30) / 60); + return sprintf('%s%02d%s%02d', + $sign, $mins / 60, $colon, $mins % 60); + } + + /** + * Return the unix timestamp representation of this date. + * + * @return integer A unix timestamp. + */ + function timestamp() + { + if (class_exists('DateTime')) { + return $this->format('U'); + } else { + return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year); + } + } + + /** + * Return the unix timestamp representation of this date, 12:00am. + * + * @return integer A unix timestamp. + */ + function datestamp() + { + if (class_exists('DateTime')) { + $dt = new DateTime(); + $dt->setDate($this->year, $this->month, $this->mday); + $dt->setTime(0, 0, 0); + return $dt->format('U'); + } else { + return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year); + } + } + + /** + * Format time using the specifiers available in date() or in the DateTime + * class' format() method. + * + * @since Horde 3.3 + * + * @param string $format + * + * @return string Formatted time. + */ + function format($format) + { + if (class_exists('DateTime')) { + $dt = new DateTime(); + $dt->setDate($this->year, $this->month, $this->mday); + $dt->setTime($this->hour, $this->min, $this->sec); + return $dt->format($format); + } else { + return date($format, $this->timestamp()); + } + } + + /** + * Format time in ISO-8601 format. Works correctly since Horde 3.2. + * + * @return string Date and time in ISO-8601 format. + */ + function iso8601DateTime() + { + return $this->rfc3339DateTime() . $this->tzOffset(); + } + + /** + * Format time in RFC 2822 format. + * + * @return string Date and time in RFC 2822 format. + */ + function rfc2822DateTime() + { + return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false); + } + + /** + * Format time in RFC 3339 format. + * + * @since Horde 3.1 + * + * @return string Date and time in RFC 3339 format. The seconds part has + * been added with Horde 3.2. + */ + function rfc3339DateTime() + { + return $this->format('Y-m-d\TH:i:s'); + } + + /** + * Format time to standard 'ctime' format. + * + * @return string Date and time. + */ + function cTime() + { + return $this->format('D M j H:i:s Y'); + } + + /** + * Format date and time using strftime() format. + * + * @since Horde 3.1 + * + * @return string strftime() formatted date and time. + */ + function strftime($format) + { + if (preg_match('/%[^' . $this->_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); + } + + /** + * mktime() implementation that supports dates outside of 1970-2038, + * from http://phplens.com/phpeverywhere/adodb_date_library. + * + * @TODO remove in Horde 4 + * + * This does NOT work with pre-1970 daylight saving times. + * + * @static + */ + function _mktime($hr, $min, $sec, $mon = false, $day = false, + $year = false, $is_dst = false, $is_gmt = false) + { + if ($mon === false) { + return $is_gmt + ? @gmmktime($hr, $min, $sec) + : @mktime($hr, $min, $sec); + } + + if ($year > 1901 && $year < 2038 && + ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) { + return $is_gmt + ? @gmmktime($hr, $min, $sec, $mon, $day, $year) + : @mktime($hr, $min, $sec, $mon, $day, $year); + } + + $gmt_different = $is_gmt + ? 0 + : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0)); + + $mon = intval($mon); + $day = intval($day); + $year = intval($year); + + if ($mon > 12) { + $y = floor($mon / 12); + $year += $y; + $mon -= $y * 12; + } elseif ($mon < 1) { + $y = ceil((1 - $mon) / 12); + $year -= $y; + $mon += $y * 12; + } + + $_day_power = 86400; + $_hour_power = 3600; + $_min_power = 60; + + $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + + $_total_date = 0; + if ($year >= 1970) { + for ($a = 1970; $a <= $year; $a++) { + $leaf = Horde_Date::isLeapYear($a); + if ($leaf == true) { + $loop_table = $_month_table_leaf; + $_add_date = 366; + } else { + $loop_table = $_month_table_normal; + $_add_date = 365; + } + if ($a < $year) { + $_total_date += $_add_date; + } else { + for ($b = 1; $b < $mon; $b++) { + $_total_date += $loop_table[$b]; + } + } + } + + return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different; + } + + for ($a = 1969 ; $a >= $year; $a--) { + $leaf = Horde_Date::isLeapYear($a); + if ($leaf == true) { + $loop_table = $_month_table_leaf; + $_add_date = 366; + } else { + $loop_table = $_month_table_normal; + $_add_date = 365; + } + if ($a > $year) { + $_total_date += $_add_date; + } else { + for ($b = 12; $b > $mon; $b--) { + $_total_date += $loop_table[$b]; + } + } + } + + $_total_date += $loop_table[$mon] - $day; + $_day_time = $hr * $_hour_power + $min * $_min_power + $sec; + $_day_time = $_day_power - $_day_time; + $ret = -($_total_date * $_day_power + $_day_time - $gmt_different); + if ($ret < -12220185600) { + // If earlier than 5 Oct 1582 - gregorian correction. + return $ret + 10 * 86400; + } elseif ($ret < -12219321600) { + // If in limbo, reset to 15 Oct 1582. + return -12219321600; + } else { + return $ret; + } + } + +} + + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */ + +// {{{ Header + +/** + * Calculates, manipulates and retrieves dates + * + * It does not rely on 32-bit system time stamps, so it works dates + * before 1970 and after 2038. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 1999-2007 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor, + * C.A. Woodcock + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted under the terms of the BSD License. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @category Date and Time + * @package Date + * @author Monte Ohrt + * @author Pierre-Alain Joye + * @author Daniel Convissor + * @author C.A. Woodcock + * @copyright 1999-2007 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor, C.A. Woodcock + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version CVS: $Id: Calc.php,v 1.57 2008/03/23 18:34:16 c01234 Exp $ + * @link http://pear.php.net/package/Date + * @since File available since Release 1.2 + */ + + +// }}} +// {{{ General constants: + +if (!defined('DATE_CALC_BEGIN_WEEKDAY')) { + /** + * Defines what day starts the week + * + * Monday (1) is the international standard. + * Redefine this to 0 if you want weeks to begin on Sunday. + */ + define('DATE_CALC_BEGIN_WEEKDAY', 1); +} + +if (!defined('DATE_CALC_FORMAT')) { + /** + * The default value for each method's $format parameter + * + * The default is '%Y%m%d'. To override this default, define + * this constant before including Calc.php. + * + * @since Constant available since Release 1.4.4 + */ + define('DATE_CALC_FORMAT', '%Y%m%d'); +} + + +// {{{ Date precision constants (used in 'round()' and 'trunc()'): + +define('DATE_PRECISION_YEAR', -2); +define('DATE_PRECISION_MONTH', -1); +define('DATE_PRECISION_DAY', 0); +define('DATE_PRECISION_HOUR', 1); +define('DATE_PRECISION_10MINUTES', 2); +define('DATE_PRECISION_MINUTE', 3); +define('DATE_PRECISION_10SECONDS', 4); +define('DATE_PRECISION_SECOND', 5); + + +// }}} +// {{{ Class: Date_Calc + +/** + * Calculates, manipulates and retrieves dates + * + * It does not rely on 32-bit system time stamps, so it works dates + * before 1970 and after 2038. + * + * @category Date and Time + * @package Date + * @author Monte Ohrt + * @author Daniel Convissor + * @author C.A. Woodcock + * @copyright 1999-2007 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor, C.A. Woodcock + * @license http://www.opensource.org/licenses/bsd-license.php + * BSD License + * @version Release: 1.5.0a1 + * @link http://pear.php.net/package/Date + * @since Class available since Release 1.2 + */ +class Date_Calc +{ + + // {{{ dateFormat() + + /** + * Formats the date in the given format, much like strfmt() + * + * This function is used to alleviate the problem with 32-bit numbers for + * dates pre 1970 or post 2038, as strfmt() has on most systems. + * Most of the formatting options are compatible. + * + * Formatting options: + *
+     * %a   abbreviated weekday name (Sun, Mon, Tue)
+     * %A   full weekday name (Sunday, Monday, Tuesday)
+     * %b   abbreviated month name (Jan, Feb, Mar)
+     * %B   full month name (January, February, March)
+     * %d   day of month (range 00 to 31)
+     * %e   day of month, single digit (range 0 to 31)
+     * %E   number of days since unspecified epoch (integer)
+     *        (%E is useful for passing a date in a URL as
+     *        an integer value. Then simply use
+     *        daysToDate() to convert back to a date.)
+     * %j   day of year (range 001 to 366)
+     * %m   month as decimal number (range 1 to 12)
+     * %n   newline character (\n)
+     * %t   tab character (\t)
+     * %w   weekday as decimal (0 = Sunday)
+     * %U   week number of current year, first sunday as first week
+     * %y   year as decimal (range 00 to 99)
+     * %Y   year as decimal including century (range 0000 to 9999)
+     * %%   literal '%'
+     * 
+ * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * @param string $format the format string + * + * @return string the date in the desired format + * @access public + * @static + */ + function dateFormat($day, $month, $year, $format) + { + if (!Date_Calc::isValidDate($day, $month, $year)) { + $year = Date_Calc::dateNow('%Y'); + $month = Date_Calc::dateNow('%m'); + $day = Date_Calc::dateNow('%d'); + } + + $output = ''; + + for ($strpos = 0; $strpos < strlen($format); $strpos++) { + $char = substr($format, $strpos, 1); + if ($char == '%') { + $nextchar = substr($format, $strpos + 1, 1); + switch($nextchar) { + case 'a': + $output .= Date_Calc::getWeekdayAbbrname($day, $month, $year); + break; + case 'A': + $output .= Date_Calc::getWeekdayFullname($day, $month, $year); + break; + case 'b': + $output .= Date_Calc::getMonthAbbrname($month); + break; + case 'B': + $output .= Date_Calc::getMonthFullname($month); + break; + case 'd': + $output .= sprintf('%02d', $day); + break; + case 'e': + $output .= $day; + break; + case 'E': + $output .= Date_Calc::dateToDays($day, $month, $year); + break; + case 'j': + $output .= Date_Calc::dayOfYear($day, $month, $year); + break; + case 'm': + $output .= sprintf('%02d', $month); + break; + case 'n': + $output .= "\n"; + break; + case 't': + $output .= "\t"; + break; + case 'w': + $output .= Date_Calc::dayOfWeek($day, $month, $year); + break; + case 'U': + $output .= Date_Calc::weekOfYear($day, $month, $year); + break; + case 'y': + $output .= sprintf('%0' . + ($year < 0 ? '3' : '2') . + 'd', + $year % 100); + break; + case "Y": + $output .= sprintf('%0' . + ($year < 0 ? '5' : '4') . + 'd', + $year); + break; + case '%': + $output .= '%'; + break; + default: + $output .= $char.$nextchar; + } + $strpos++; + } else { + $output .= $char; + } + } + return $output; + } + + + // }}} + // {{{ dateNow() + + /** + * Returns the current local date + * + * NOTE: This function retrieves the local date using strftime(), + * which may or may not be 32-bit safe on your system. + * + * @param string $format the string indicating how to format the output + * + * @return string the current date in the specified format + * @access public + * @static + */ + function dateNow($format = DATE_CALC_FORMAT) + { + return strftime($format, time()); + } + + + // }}} + // {{{ getYear() + + /** + * Returns the current local year in format CCYY + * + * @return string the current year in four digit format + * @access public + * @static + */ + function getYear() + { + return Date_Calc::dateNow('%Y'); + } + + + // }}} + // {{{ getMonth() + + /** + * Returns the current local month in format MM + * + * @return string the current month in two digit format + * @access public + * @static + */ + function getMonth() + { + return Date_Calc::dateNow('%m'); + } + + + // }}} + // {{{ getDay() + + /** + * Returns the current local day in format DD + * + * @return string the current day of the month in two digit format + * @access public + * @static + */ + function getDay() + { + return Date_Calc::dateNow('%d'); + } + + + // }}} + // {{{ defaultCentury() + + /** + * Turns a two digit year into a four digit year + * + * Return value depends on current year; the century chosen + * will be the one which forms the year that is closest + * to the current year. If the two possibilities are + * equidistant to the current year (i.e. 50 years in the past + * and 50 years in the future), then the past year is chosen. + * + * For example, if the current year is 2007: + * 03 - returns 2003 + * 09 - returns 2009 + * 56 - returns 2056 (closer to 2007 than 1956) + * 57 - returns 1957 (1957 and 2007 are equidistant, so previous century + * chosen) + * 58 - returns 1958 + * + * @param int $year the 2 digit year + * + * @return int the 4 digit year + * @access public + * @static + */ + function defaultCentury($year) + { + $hn_century = intval(($hn_currentyear = date("Y")) / 100); + $hn_currentyear = $hn_currentyear % 100; + + if ($year < 0 || $year >= 100) + $year = $year % 100; + + if ($year - $hn_currentyear < -50) + return ($hn_century + 1) * 100 + $year; + else if ($year - $hn_currentyear < 50) + return $hn_century * 100 + $year; + else + return ($hn_century - 1) * 100 + $year; + } + + + // }}} + // {{{ getSecondsInYear() + + /** + * Returns the total number of seconds in the given year + * + * This takes into account leap seconds. + * + * @param int $pn_year the year in four digit format + * + * @return int + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getSecondsInYear($pn_year) + { + $pn_year = intval($pn_year); + + static $ha_leapseconds; + if (!isset($ha_leapseconds)) { + $ha_leapseconds = array(1972 => 2, + 1973 => 1, + 1974 => 1, + 1975 => 1, + 1976 => 1, + 1977 => 1, + 1978 => 1, + 1979 => 1, + 1981 => 1, + 1982 => 1, + 1983 => 1, + 1985 => 1, + 1987 => 1, + 1989 => 1, + 1990 => 1, + 1992 => 1, + 1993 => 1, + 1994 => 1, + 1995 => 1, + 1997 => 1, + 1998 => 1, + 2005 => 1); + } + + $ret = Date_Calc::daysInYear($pn_year) * 86400; + + if (isset($ha_leapseconds[$pn_year])) { + return $ret + $ha_leapseconds[$pn_year]; + } else { + return $ret; + } + } + + + // }}} + // {{{ getSecondsInMonth() + + /** + * Returns the total number of seconds in the given month + * + * This takes into account leap seconds. + * + * @param int $pn_month the month + * @param int $pn_year the year in four digit format + * + * @return int + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getSecondsInMonth($pn_month, $pn_year) + { + $pn_month = intval($pn_month); + $pn_year = intval($pn_year); + + static $ha_leapseconds; + if (!isset($ha_leapseconds)) { + $ha_leapseconds = array(1972 => array(6 => 1, + 12 => 1), + 1973 => array(12 => 1), + 1974 => array(12 => 1), + 1975 => array(12 => 1), + 1976 => array(12 => 1), + 1977 => array(12 => 1), + 1978 => array(12 => 1), + 1979 => array(12 => 1), + 1981 => array(6 => 1), + 1982 => array(6 => 1), + 1983 => array(6 => 1), + 1985 => array(6 => 1), + 1987 => array(12 => 1), + 1989 => array(12 => 1), + 1990 => array(12 => 1), + 1992 => array(6 => 1), + 1993 => array(6 => 1), + 1994 => array(6 => 1), + 1995 => array(12 => 1), + 1997 => array(6 => 1), + 1998 => array(12 => 1), + 2005 => array(12 => 1)); + } + + $ret = Date_Calc::daysInMonth($pn_month, $pn_year) * 86400; + + if (isset($ha_leapseconds[$pn_year][$pn_month])) { + return $ret + $ha_leapseconds[$pn_year][$pn_month]; + } else { + return $ret; + } + } + + + // }}} + // {{{ getSecondsInDay() + + /** + * Returns the total number of seconds in the day of the given date + * + * This takes into account leap seconds. + * + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year in four digit format + * + * @return int + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getSecondsInDay($pn_day, $pn_month, $pn_year) + { + // Note to developers: + // + // The leap seconds listed here are a matter of historical fact, + // that is, it is known on which exact day they occurred. + // However, the implementation of the class as a whole depends + // on the fact that they always occur at the end of the month + // (although it is assumed that they could occur in any month, + // even though practically they only occur in June or December). + // + // Do not define a leap second on a day of the month other than + // the last day without altering the implementation of the + // functions that depend on this one. + // + // It is possible, though, to define an un-leap second (i.e. a skipped + // second (I do not know what they are called), or a number of + // consecutive leap seconds). + + $pn_day = intval($pn_day); + $pn_month = intval($pn_month); + $pn_year = intval($pn_year); + + static $ha_leapseconds; + if (!isset($ha_leapseconds)) { + $ha_leapseconds = array(1972 => array(6 => array(30 => 1), + 12 => array(31 => 1)), + 1973 => array(12 => array(31 => 1)), + 1974 => array(12 => array(31 => 1)), + 1975 => array(12 => array(31 => 1)), + 1976 => array(12 => array(31 => 1)), + 1977 => array(12 => array(31 => 1)), + 1978 => array(12 => array(31 => 1)), + 1979 => array(12 => array(31 => 1)), + 1981 => array(6 => array(30 => 1)), + 1982 => array(6 => array(30 => 1)), + 1983 => array(6 => array(30 => 1)), + 1985 => array(6 => array(30 => 1)), + 1987 => array(12 => array(31 => 1)), + 1989 => array(12 => array(31 => 1)), + 1990 => array(12 => array(31 => 1)), + 1992 => array(6 => array(30 => 1)), + 1993 => array(6 => array(30 => 1)), + 1994 => array(6 => array(30 => 1)), + 1995 => array(12 => array(31 => 1)), + 1997 => array(6 => array(30 => 1)), + 1998 => array(12 => array(31 => 1)), + 2005 => array(12 => array(31 => 1))); + } + + if (isset($ha_leapseconds[$pn_year][$pn_month][$pn_day])) { + return 86400 + $ha_leapseconds[$pn_year][$pn_month][$pn_day]; + } else { + return 86400; + } + } + + + // }}} + // {{{ getSecondsInHour() + + /** + * Returns the total number of seconds in the hour of the given date + * + * This takes into account leap seconds. + * + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year in four digit format + * @param int $pn_hour the hour + * + * @return int + * @access public + * @static + */ + function getSecondsInHour($pn_day, $pn_month, $pn_year, $pn_hour) + { + if ($pn_hour < 23) + return 3600; + else + return Date_Calc::getSecondsInDay($pn_day, $pn_month, $pn_year) - + 82800; + } + + + // }}} + // {{{ getSecondsInMinute() + + /** + * Returns the total number of seconds in the minute of the given hour + * + * This takes into account leap seconds. + * + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year in four digit format + * @param int $pn_hour the hour + * @param int $pn_minute the minute + * + * @return int + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getSecondsInMinute($pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute) + { + if ($pn_hour < 23 || $pn_minute < 59) + return 60; + else + return Date_Calc::getSecondsInDay($pn_day, $pn_month, $pn_year) - + 86340; + } + + + // }}} + // {{{ secondsPastMidnight() + + /** + * Returns the no of seconds since midnight (0-86399) + * + * @param int $pn_hour the hour of the day + * @param int $pn_minute the minute + * @param mixed $pn_second the second as integer or float + * + * @return mixed integer or float from 0-86399 + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function secondsPastMidnight($pn_hour, $pn_minute, $pn_second) + { + return 3600 * $pn_hour + 60 * $pn_minute + $pn_second; + } + + + // }}} + // {{{ secondsPastMidnightToTime() + + /** + * Returns the time as an array (i.e. hour, minute, second) + * + * @param mixed $pn_seconds the no of seconds since midnight (0-86399) + * + * @return mixed array of hour, minute (both as integers), second (as + * integer or float, depending on parameter) + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function secondsPastMidnightToTime($pn_seconds) + { + if ($pn_seconds >= 86400) { + return array(23, 59, $pn_seconds - 86340); + } + + $hn_hour = intval($pn_seconds / 3600); + $hn_minute = intval(($pn_seconds - $hn_hour * 3600) / 60); + $hn_second = is_float($pn_seconds) ? + fmod($pn_seconds, 60) : + $pn_seconds % 60; + + return array($hn_hour, $hn_minute, $hn_second); + } + + + // }}} + // {{{ secondsPastTheHour() + + /** + * Returns the no of seconds since the last hour o'clock (0-3599) + * + * @param int $pn_minute the minute + * @param mixed $pn_second the second as integer or float + * + * @return mixed integer or float from 0-3599 + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function secondsPastTheHour($pn_minute, $pn_second) + { + return 60 * $pn_minute + $pn_second; + } + + + // }}} + // {{{ addHours() + + /** + * Returns the date the specified no of hours from the given date + * + * To subtract hours use a negative value for the '$pn_hours' parameter + * + * @param int $pn_hours hours to add + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year + * @param int $pn_hour the hour + * + * @return array array of year, month, day, hour + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addHours($pn_hours, $pn_day, $pn_month, $pn_year, $pn_hour) + { + if ($pn_hours == 0) + return array((int) $pn_year, + (int) $pn_month, + (int) $pn_day, + (int) $pn_hour); + + $hn_days = intval($pn_hours / 24); + $hn_hour = $pn_hour + $pn_hours % 24; + + if ($hn_hour >= 24) { + ++$hn_days; + $hn_hour -= 24; + } else if ($hn_hour < 0) { + --$hn_days; + $hn_hour += 24; + } + + if ($hn_days == 0) { + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_day = $pn_day; + } else { + list($hn_year, $hn_month, $hn_day) = + explode(" ", + Date_Calc::addDays($hn_days, + $pn_day, + $pn_month, + $pn_year, + "%Y %m %d")); + } + + return array((int) $hn_year, (int) $hn_month, (int) $hn_day, $hn_hour); + } + + + // }}} + // {{{ addMinutes() + + /** + * Returns the date the specified no of minutes from the given date + * + * To subtract minutes use a negative value for the '$pn_minutes' parameter + * + * @param int $pn_minutes minutes to add + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year + * @param int $pn_hour the hour + * @param int $pn_minute the minute + * + * @return array array of year, month, day, hour, minute + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addMinutes($pn_minutes, + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute) + { + if ($pn_minutes == 0) + return array((int) $pn_year, + (int) $pn_month, + (int) $pn_day, + (int) $pn_hour, + (int) $pn_minute); + + $hn_hours = intval($pn_minutes / 60); + $hn_minute = $pn_minute + $pn_minutes % 60; + + if ($hn_minute >= 60) { + ++$hn_hours; + $hn_minute -= 60; + } else if ($hn_minute < 0) { + --$hn_hours; + $hn_minute += 60; + } + + if ($hn_hours == 0) { + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_day = $pn_day; + $hn_hour = $pn_hour; + } else { + list($hn_year, $hn_month, $hn_day, $hn_hour) = + Date_Calc::addHours($hn_hours, + $pn_day, + $pn_month, + $pn_year, + $pn_hour); + } + + return array($hn_year, $hn_month, $hn_day, $hn_hour, $hn_minute); + } + + + // }}} + // {{{ addSeconds() + + /** + * Returns the date the specified no of seconds from the given date + * + * If leap seconds are specified to be counted, the passed time must be UTC. + * To subtract seconds use a negative value for the '$pn_seconds' parameter. + * + * N.B. the return type of the second part of the date is float if + * either '$pn_seconds' or '$pn_second' is a float; otherwise, it + * is integer. + * + * @param mixed $pn_seconds seconds to add as integer or float + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year + * @param int $pn_hour the hour + * @param int $pn_minute the minute + * @param mixed $pn_second the second as integer or float + * @param bool $pb_countleap whether to count leap seconds (defaults to + * DATE_COUNT_LEAP_SECONDS) + * + * @return array array of year, month, day, hour, minute, second + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addSeconds($pn_seconds, + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute, + $pn_second, + $pb_countleap = DATE_COUNT_LEAP_SECONDS) + { + if ($pn_seconds == 0) + return array((int) $pn_year, + (int) $pn_month, + (int) $pn_day, + (int) $pn_hour, + (int) $pn_minute, + $pn_second); + + if ($pb_countleap) { + $hn_seconds = $pn_seconds; + + $hn_day = (int) $pn_day; + $hn_month = (int) $pn_month; + $hn_year = (int) $pn_year; + $hn_hour = (int) $pn_hour; + $hn_minute = (int) $pn_minute; + $hn_second = $pn_second; + + $hn_days = Date_Calc::dateToDays($pn_day, + $pn_month, + $pn_year); + $hn_secondsofmonth = 86400 * ($hn_days - + Date_Calc::firstDayOfMonth($pn_month, + $pn_year)) + + Date_Calc::secondsPastMidnight($pn_hour, + $pn_minute, + $pn_second); + + if ($hn_seconds > 0) { + // Advance to end of month: + // + if ($hn_secondsofmonth != 0 && + $hn_secondsofmonth + $hn_seconds >= + ($hn_secondsinmonth = + Date_Calc::getSecondsInMonth($hn_month, $hn_year))) { + + $hn_seconds -= $hn_secondsinmonth - $hn_secondsofmonth; + $hn_secondsofmonth = 0; + list($hn_year, $hn_month) = + Date_Calc::nextMonth($hn_month, $hn_year); + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + $hn_hour = $hn_minute = $hn_second = 0; + } + + // Advance to end of year: + // + if ($hn_secondsofmonth == 0 && + $hn_month != Date_Calc::getFirstMonthOfYear($hn_year)) { + + while ($hn_year == $pn_year && + $hn_seconds >= ($hn_secondsinmonth = + Date_Calc::getSecondsInMonth($hn_month, + $hn_year))) { + $hn_seconds -= $hn_secondsinmonth; + list($hn_year, $hn_month) = + Date_Calc::nextMonth($hn_month, $hn_year); + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + } + } + + if ($hn_secondsofmonth == 0) { + // Add years: + // + if ($hn_month == Date_Calc::getFirstMonthOfYear($hn_year)) { + while ($hn_seconds >= ($hn_secondsinyear = + Date_Calc::getSecondsInYear($hn_year))) { + $hn_seconds -= $hn_secondsinyear; + $hn_month = Date_Calc::getFirstMonthOfYear(++$hn_year); + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + } + } + + // Add months: + // + while ($hn_seconds >= ($hn_secondsinmonth = + Date_Calc::getSecondsInMonth($hn_month, $hn_year))) { + $hn_seconds -= $hn_secondsinmonth; + list($hn_year, $hn_month) = + Date_Calc::nextMonth($hn_month, $hn_year); + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, $hn_year); + } + } + } else { + // + // (if $hn_seconds < 0) + + // Go back to start of month: + // + if ($hn_secondsofmonth != 0 && + -$hn_seconds >= $hn_secondsofmonth) { + + $hn_seconds += $hn_secondsofmonth; + $hn_secondsofmonth = 0; + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + $hn_hour = $hn_minute = $hn_second = 0; + } + + // Go back to start of year: + // + if ($hn_secondsofmonth == 0) { + while ($hn_month != + Date_Calc::getFirstMonthOfYear($hn_year)) { + + list($hn_year, $hn_prevmonth) = + Date_Calc::prevMonth($hn_month, $hn_year); + + if (-$hn_seconds >= ($hn_secondsinmonth = + Date_Calc::getSecondsInMonth($hn_prevmonth, + $hn_year))) { + $hn_seconds += $hn_secondsinmonth; + $hn_month = $hn_prevmonth; + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + } else { + break; + } + } + } + + if ($hn_secondsofmonth == 0) { + // Subtract years: + // + if ($hn_month == Date_Calc::getFirstMonthOfYear($hn_year)) { + while (-$hn_seconds >= ($hn_secondsinyear = + Date_Calc::getSecondsInYear($hn_year - 1))) { + $hn_seconds += $hn_secondsinyear; + $hn_month = Date_Calc::getFirstMonthOfYear(--$hn_year); + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + } + } + + // Subtract months: + // + list($hn_pmyear, $hn_prevmonth) = + Date_Calc::prevMonth($hn_month, $hn_year); + while (-$hn_seconds >= ($hn_secondsinmonth = + Date_Calc::getSecondsInMonth($hn_prevmonth, + $hn_pmyear))) { + $hn_seconds += $hn_secondsinmonth; + $hn_year = $hn_pmyear; + $hn_month = $hn_prevmonth; + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, + $hn_year); + list($hn_pmyear, $hn_prevmonth) = + Date_Calc::prevMonth($hn_month, $hn_year); + } + } + } + + if ($hn_seconds < 0 && $hn_secondsofmonth == 0) { + list($hn_year, $hn_month) = + Date_Calc::prevMonth($hn_month, $hn_year); + $hn_day = Date_Calc::getFirstDayOfMonth($hn_month, $hn_year); + $hn_seconds += Date_Calc::getSecondsInMonth($hn_month, $hn_year); + } + + $hn_seconds += Date_Calc::secondsPastMidnight($hn_hour, + $hn_minute, + $hn_second); + if ($hn_seconds < 0) { + $hn_daysadd = intval($hn_seconds / 86400) - 1; + } else if ($hn_seconds < 86400) { + $hn_daysadd = 0; + } else { + $hn_daysadd = intval($hn_seconds / 86400) - 1; + } + + if ($hn_daysadd != 0) { + list($hn_year, $hn_month, $hn_day) = + explode(" ", + Date_Calc::addDays($hn_daysadd, + $hn_day, + $hn_month, + $hn_year, + "%Y %m %d")); + $hn_seconds -= $hn_daysadd * 86400; + } + + $hn_secondsinday = Date_Calc::getSecondsInDay($hn_day, + $hn_month, + $hn_year); + if ($hn_seconds >= $hn_secondsinday) { + list($hn_year, $hn_month, $hn_day) = + explode(" ", + Date_Calc::addDays(1, + $hn_day, + $hn_month, + $hn_year, + "%Y %m %d")); + $hn_seconds -= $hn_secondsinday; + } + + list($hn_hour, $hn_minute, $hn_second) = + Date_Calc::secondsPastMidnightToTime($hn_seconds); + + return array((int) $hn_year, + (int) $hn_month, + (int) $hn_day, + $hn_hour, + $hn_minute, + $hn_second); + } else { + // Assume every day has 86400 seconds exactly (ignore leap seconds): + // + $hn_minutes = intval($pn_seconds / 60); + + if (is_float($pn_seconds)) { + $hn_second = $pn_second + fmod($pn_seconds, 60); + } else { + $hn_second = $pn_second + $pn_seconds % 60; + } + + if ($hn_second >= 60) { + ++$hn_minutes; + $hn_second -= 60; + } else if ($hn_second < 0) { + --$hn_minutes; + $hn_second += 60; + } + + if ($hn_minutes == 0) { + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_day = $pn_day; + $hn_hour = $pn_hour; + $hn_minute = $pn_minute; + } else { + list($hn_year, $hn_month, $hn_day, $hn_hour, $hn_minute) = + Date_Calc::addMinutes($hn_minutes, + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute); + } + + return array($hn_year, + $hn_month, + $hn_day, + $hn_hour, + $hn_minute, + $hn_second); + } + } + + + // }}} + // {{{ dateToDays() + + /** + * Converts a date in the proleptic Gregorian calendar to the no of days + * since 24th November, 4714 B.C. + * + * Returns the no of days since Monday, 24th November, 4714 B.C. in the + * proleptic Gregorian calendar (which is 24th November, -4713 using + * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the + * proleptic Julian calendar). This is also the first day of the 'Julian + * Period' proposed by Joseph Scaliger in 1583, and the number of days + * since this date is known as the 'Julian Day'. (It is not directly + * to do with the Julian calendar, although this is where the name + * is derived from.) + * + * The algorithm is valid for all years (positive and negative), and + * also for years preceding 4714 B.C. + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year (using 'Astronomical' year numbering) + * + * @return int the number of days since 24th November, 4714 B.C. + * @access public + * @static + */ + function dateToDays($day, $month, $year) + { + if ($month > 2) { + // March = 0, April = 1, ..., December = 9, + // January = 10, February = 11 + $month -= 3; + } else { + $month += 9; + --$year; + } + + $hb_negativeyear = $year < 0; + $century = intval($year / 100); + $year = $year % 100; + + if ($hb_negativeyear) { + // Subtract 1 because year 0 is a leap year; + // And N.B. that we must treat the leap years as occurring + // one year earlier than they do, because for the purposes + // of calculation, the year starts on 1st March: + // + return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) + + intval((1461 * $year + 1) / 4) + + intval((153 * $month + 2) / 5) + + $day + 1721118; + } else { + return intval(146097 * $century / 4) + + intval(1461 * $year / 4) + + intval((153 * $month + 2) / 5) + + $day + 1721119; + } + } + + + // }}} + // {{{ daysToDate() + + /** + * Converts no of days since 24th November, 4714 B.C. (in the proleptic + * Gregorian calendar, which is year -4713 using 'Astronomical' year + * numbering) to Gregorian calendar date + * + * Returned date belongs to the proleptic Gregorian calendar, using + * 'Astronomical' year numbering. + * + * The algorithm is valid for all years (positive and negative), and + * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'), + * and so the only limitation is platform-dependent (for 32-bit systems + * the maximum year would be something like about 1,465,190 A.D.). + * + * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'. + * + * @param int $days the number of days since 24th November, 4714 B.C. + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function daysToDate($days, $format = DATE_CALC_FORMAT) + { + $days = intval($days); + + $days -= 1721119; + $century = floor((4 * $days - 1) / 146097); + $days = floor(4 * $days - 1 - 146097 * $century); + $day = floor($days / 4); + + $year = floor((4 * $day + 3) / 1461); + $day = floor(4 * $day + 3 - 1461 * $year); + $day = floor(($day + 4) / 4); + + $month = floor((5 * $day - 3) / 153); + $day = floor(5 * $day - 3 - 153 * $month); + $day = floor(($day + 5) / 5); + + $year = $century * 100 + $year; + if ($month < 10) { + $month +=3; + } else { + $month -=9; + ++$year; + } + + return Date_Calc::dateFormat($day, $month, $year, $format); + } + + + // }}} + // {{{ getMonths() + + /** + * Returns array of the month numbers, in order, for the given year + * + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return array array of integer month numbers, in order + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getMonths($pn_year) + { + // N.B. Month numbers can be skipped but not duplicated: + // + return array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + } + + + // }}} + // {{{ getMonthNames() + + /** + * Returns an array of month names + * + * Used to take advantage of the setlocale function to return + * language specific month names. + * + * TODO: cache values to some global array to avoid performance + * hits when called more than once. + * + * @param int $pb_abbreviated whether to return the abbreviated form of the + * months + * + * @return array associative array of integer month numbers, in + * order, to month names + * @access public + * @static + */ + function getMonthNames($pb_abbreviated = false) + { + $ret = array(); + foreach (Date_Calc::getMonths(2001) as $i) { + $ret[$i] = strftime($pb_abbreviated ? '%b' : '%B', + mktime(0, 0, 0, $i, 1, 2001)); + } + return $ret; + } + + + // }}} + // {{{ prevMonth() + + /** + * Returns month and year of previous month + * + * @param int $pn_month the month + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return array array of year, month as integers + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function prevMonth($pn_month, $pn_year) + { + $ha_months = Date_Calc::getMonths($pn_year); + $hn_monthkey = array_search($pn_month, $ha_months); + if (array_key_exists($hn_monthkey - 1, $ha_months)) { + return array((int) $pn_year, $ha_months[$hn_monthkey - 1]); + } else { + $ha_months = Date_Calc::getMonths($pn_year - 1); + return array($pn_year - 1, end($ha_months)); + } + } + + + // }}} + // {{{ nextMonth() + + /** + * Returns month and year of next month + * + * @param int $pn_month the month + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return array array of year, month as integers + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function nextMonth($pn_month, $pn_year) + { + $ha_months = Date_Calc::getMonths($pn_year); + $hn_monthkey = array_search($pn_month, $ha_months); + if (array_key_exists($hn_monthkey + 1, $ha_months)) { + return array((int) $pn_year, $ha_months[$hn_monthkey + 1]); + } else { + $ha_months = Date_Calc::getMonths($pn_year + 1); + return array($pn_year + 1, $ha_months[0]); + } + } + + + // }}} + // {{{ addMonthsToDays() + + /** + * Returns 'Julian Day' of the date the specified no of months + * from the given date + * + * To subtract months use a negative value for the '$pn_months' + * parameter + * + * @param int $pn_months months to add + * @param int $pn_days 'Julian Day', i.e. the no of days since 1st + * January, 4713 B.C. + * + * @return int 'Julian Day', i.e. the no of days since 1st January, + * 4713 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addMonthsToDays($pn_months, $pn_days) + { + if ($pn_months == 0) + return (int) $pn_days; + + list($hn_year, $hn_month, $hn_day) = + explode(" ", Date_Calc::daysToDate($pn_days, "%Y %m %d")); + + $hn_retmonth = $hn_month + $pn_months % 12; + $hn_retyear = $hn_year + intval($pn_months / 12); + if ($hn_retmonth < 1) { + $hn_retmonth += 12; + --$hn_retyear; + } else if ($hn_retmonth > 12) { + $hn_retmonth -= 12; + ++$hn_retyear; + } + + if (Date_Calc::isValidDate($hn_day, $hn_retmonth, $hn_retyear)) + return Date_Calc::dateToDays($hn_day, $hn_retmonth, $hn_retyear); + + // Calculate days since first of month: + // + $hn_dayoffset = $pn_days - + Date_Calc::firstDayOfMonth($hn_month, $hn_year); + + $hn_retmonthfirstday = Date_Calc::firstDayOfMonth($hn_retmonth, + $hn_retyear); + $hn_retmonthlastday = Date_Calc::lastDayOfMonth($hn_retmonth, + $hn_retyear); + + if ($hn_dayoffset > $hn_retmonthlastday - $hn_retmonthfirstday) { + return $hn_retmonthlastday; + } else { + return $hn_retmonthfirstday + $hn_dayoffset; + } + } + + + // }}} + // {{{ addMonths() + + /** + * Returns the date the specified no of months from the given date + * + * To subtract months use a negative value for the '$pn_months' + * parameter + * + * @param int $pn_months months to add + * @param int $pn_day the day of the month, default is current local + * day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is + * current local year + * @param string $ps_format string specifying how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addMonths($pn_months, + $pn_day, + $pn_month, + $pn_year, + $ps_format = DATE_CALC_FORMAT) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + if ($pn_months == 0) + return Date_Calc::dateFormat($pn_day, + $pn_month, + $pn_year, + $ps_format); + + $hn_days = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year); + return Date_Calc::daysToDate(Date_Calc::addMonthsToDays($pn_months, + $hn_days), + $ps_format); + } + + + // }}} + // {{{ addYearsToDays() + + /** + * Returns 'Julian Day' of the date the specified no of years + * from the given date + * + * To subtract years use a negative value for the '$pn_years' + * parameter + * + * @param int $pn_years years to add + * @param int $pn_days 'Julian Day', i.e. the no of days since 1st January, + * 4713 B.C. + * + * @return int 'Julian Day', i.e. the no of days since 1st January, + * 4713 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addYearsToDays($pn_years, $pn_days) + { + if ($pn_years == 0) + return (int) $pn_days; + + list($hn_year, $hn_month, $hn_day) = + explode(" ", Date_Calc::daysToDate($pn_days, "%Y %m %d")); + + $hn_retyear = $hn_year + $pn_years; + if (Date_Calc::isValidDate($hn_day, $hn_month, $hn_retyear)) + return Date_Calc::dateToDays($hn_day, $hn_month, $hn_retyear); + + $ha_months = Date_Calc::getMonths($hn_retyear); + if (in_array($hn_month, $ha_months)) { + $hn_retmonth = $hn_month; + + // Calculate days since first of month: + // + $hn_dayoffset = $pn_days - Date_Calc::firstDayOfMonth($hn_month, + $hn_year); + + $hn_retmonthfirstday = Date_Calc::firstDayOfMonth($hn_retmonth, + $hn_retyear); + $hn_retmonthlastday = Date_Calc::lastDayOfMonth($hn_retmonth, + $hn_retyear); + + if ($hn_dayoffset > $hn_retmonthlastday - $hn_retmonthfirstday) { + return $hn_retmonthlastday; + } else { + return $hn_retmonthfirstday + $hn_dayoffset; + } + } else { + // Calculate days since first of year: + // + $hn_dayoffset = $pn_days - Date_Calc::firstDayOfYear($hn_year); + + $hn_retyearfirstday = Date_Calc::firstDayOfYear($hn_retyear); + $hn_retyearlastday = Date_Calc::lastDayOfYear($hn_retyear); + + if ($hn_dayoffset > $hn_retyearlastday - $hn_retyearfirstday) { + return $hn_retyearlastday; + } else { + return $hn_retyearfirstday + $hn_dayoffset; + } + } + } + + + // }}} + // {{{ addYears() + + /** + * Returns the date the specified no of years from the given date + * + * To subtract years use a negative value for the '$pn_years' + * parameter + * + * @param int $pn_years years to add + * @param int $pn_day the day of the month, default is current local + * day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is + * current local year + * @param string $ps_format string specifying how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addYears($pn_years, + $pn_day, + $pn_month, + $pn_year, + $ps_format = DATE_CALC_FORMAT) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + if ($pn_years == 0) + return Date_Calc::dateFormat($pn_day, + $pn_month, + $pn_year, + $ps_format); + + $hn_days = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year); + return Date_Calc::daysToDate(Date_Calc::addYearsToDays($pn_years, + $hn_days), + $ps_format); + } + + + // }}} + // {{{ addDays() + + /** + * Returns the date the specified no of days from the given date + * + * To subtract days use a negative value for the '$pn_days' parameter + * + * @param int $pn_days days to add + * @param int $pn_day the day of the month, default is current local + * day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is + * current local year + * @param string $ps_format string specifying how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function addDays($pn_days, + $pn_day, + $pn_month, + $pn_year, + $ps_format = DATE_CALC_FORMAT) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + if ($pn_days == 0) + return Date_Calc::dateFormat($pn_day, + $pn_month, + $pn_year, + $ps_format); + + return Date_Calc::daysToDate(Date_Calc::dateToDays($pn_day, + $pn_month, + $pn_year) + + $pn_days, + $ps_format); + } + + + // }}} + // {{{ getFirstDayOfMonth() + + /** + * Returns first day of the specified month of specified year as integer + * + * @param int $pn_month the month + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return int number of first day of month + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getFirstDayOfMonth($pn_month, $pn_year) + { + return 1; + } + + + // }}} + // {{{ getLastDayOfMonth() + + /** + * Returns last day of the specified month of specified year as integer + * + * @param int $pn_month the month + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return int number of last day of month + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getLastDayOfMonth($pn_month, $pn_year) + { + return Date_Calc::daysInMonth($pn_month, $pn_year); + } + + + // }}} + // {{{ firstDayOfMonth() + + /** + * Returns the Julian Day of the first day of the month of the specified + * year (i.e. the no of days since 24th November, 4714 B.C.) + * + * @param int $pn_month the month + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return integer the number of days since 24th November, 4714 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function firstDayOfMonth($pn_month, $pn_year) + { + return Date_Calc::dateToDays(Date_Calc::getFirstDayOfMonth($pn_month, + $pn_year), + $pn_month, + $pn_year); + } + + + // }}} + // {{{ lastDayOfMonth() + + /** + * Returns the Julian Day of the last day of the month of the specified + * year (i.e. the no of days since 24th November, 4714 B.C.) + * + * @param int $pn_month the month + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return integer the number of days since 24th November, 4714 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function lastDayOfMonth($pn_month, $pn_year) + { + list($hn_nmyear, $hn_nextmonth) = Date_Calc::nextMonth($pn_month, + $pn_year); + return Date_Calc::firstDayOfMonth($hn_nextmonth, $hn_nmyear) - 1; + } + + + // }}} + // {{{ getFirstMonthOfYear() + + /** + * Returns first month of specified year as integer + * + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return int number of first month of year + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function getFirstMonthOfYear($pn_year) + { + $ha_months = Date_Calc::getMonths($pn_year); + return $ha_months[0]; + } + + + // }}} + // {{{ firstDayOfYear() + + /** + * Returns the Julian Day of the first day of the year (i.e. the no of + * days since 24th November, 4714 B.C.) + * + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return integer the number of days since 24th November, 4714 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function firstDayOfYear($pn_year) + { + return Date_Calc::firstDayOfMonth(Date_Calc::getFirstMonthOfYear($pn_year), + $pn_year); + } + + + // }}} + // {{{ lastDayOfYear() + + /** + * Returns the Julian Day of the last day of the year (i.e. the no of + * days since 24th November, 4714 B.C.) + * + * @param int $pn_year the year (using 'Astronomical' year numbering) + * + * @return integer the number of days since 24th November, 4714 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function lastDayOfYear($pn_year) + { + return Date_Calc::firstDayOfYear($pn_year + 1) - 1; + } + + + // }}} + // {{{ dateToDaysJulian() + + /** + * Converts a date in the proleptic Julian calendar to the no of days + * since 1st January, 4713 B.C. + * + * Returns the no of days since Monday, 1st January, 4713 B.C. in the + * proleptic Julian calendar (which is 1st January, -4712 using + * 'Astronomical' year numbering, and 24th November, 4713 B.C. in the + * proleptic Gregorian calendar). This is also the first day of the 'Julian + * Period' proposed by Joseph Scaliger in 1583, and the number of days + * since this date is known as the 'Julian Day'. (It is not directly + * to do with the Julian calendar, although this is where the name + * is derived from.) + * + * The algorithm is valid for all years (positive and negative), and + * also for years preceding 4713 B.C. + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year (using 'Astronomical' year numbering) + * + * @return int the number of days since 1st January, 4713 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function dateToDaysJulian($day, $month, $year) + { + if ($month > 2) { + // March = 0, April = 1, ..., December = 9, + // January = 10, February = 11 + $month -= 3; + } else { + $month += 9; + --$year; + } + + $hb_negativeyear = $year < 0; + + if ($hb_negativeyear) { + // Subtract 1 because year 0 is a leap year; + // And N.B. that we must treat the leap years as occurring + // one year earlier than they do, because for the purposes + // of calculation, the year starts on 1st March: + // + return intval((1461 * $year + 1) / 4) + + intval((153 * $month + 2) / 5) + + $day + 1721116; + } else { + return intval(1461 * $year / 4) + + floor((153 * $month + 2) / 5) + + $day + 1721117; + } + } + + + // }}} + // {{{ daysToDateJulian() + + /** + * Converts no of days since 1st January, 4713 B.C. (in the proleptic + * Julian calendar, which is year -4712 using 'Astronomical' year + * numbering) to Julian calendar date + * + * Returned date belongs to the proleptic Julian calendar, using + * 'Astronomical' year numbering. + * + * @param int $days the number of days since 1st January, 4713 B.C. + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function daysToDateJulian($days, $format = DATE_CALC_FORMAT) + { + $days = intval($days); + + $days -= 1721117; + $days = floor(4 * $days - 1); + $day = floor($days / 4); + + $year = floor((4 * $day + 3) / 1461); + $day = floor(4 * $day + 3 - 1461 * $year); + $day = floor(($day + 4) / 4); + + $month = floor((5 * $day - 3) / 153); + $day = floor(5 * $day - 3 - 153 * $month); + $day = floor(($day + 5) / 5); + + if ($month < 10) { + $month +=3; + } else { + $month -=9; + ++$year; + } + + return Date_Calc::dateFormat($day, $month, $year, $format); + } + + + // }}} + // {{{ isoWeekDate() + + /** + * Returns array defining the 'ISO Week Date' as defined in ISO 8601 + * + * Expects a date in the proleptic Gregorian calendar using 'Astronomical' + * year numbering, that is, with a year 0. Algorithm is valid for all + * years (positive and negative). + * + * N.B. the ISO week day no for Sunday is defined as 7, whereas this + * class and its related functions defines Sunday as 0. + * + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year + * + * @return array array of ISO Year, ISO Week No, ISO Day No as + * integers + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function isoWeekDate($pn_day = 0, $pn_month = 0, $pn_year = null) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $hn_jd = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year); + $hn_wd = Date_Calc::daysToDayOfWeek($hn_jd); + if ($hn_wd == 0) + $hn_wd = 7; + + $hn_jd1 = Date_Calc::firstDayOfYear($pn_year); + $hn_day = $hn_jd - $hn_jd1 + 1; + + if ($hn_wd <= $hn_jd - Date_Calc::lastDayOfYear($pn_year) + 3) { + // ISO week is the first week of the next ISO year: + // + $hn_year = $pn_year + 1; + $hn_isoweek = 1; + } else { + switch ($hn_wd1 = Date_Calc::daysToDayOfWeek($hn_jd1)) { + case 1: + case 2: + case 3: + case 4: + // Monday - Thursday: + // + $hn_year = $pn_year; + $hn_isoweek = floor(($hn_day + $hn_wd1 - 2) / 7) + 1; + break; + case 0: + $hn_wd1 = 7; + case 5: + case 6: + // Friday - Sunday: + // + if ($hn_day <= 8 - $hn_wd1) { + // ISO week is the last week of the previous ISO year: + // + list($hn_year, $hn_lastmonth, $hn_lastday) = + explode(" ", + Date_Calc::daysToDate($hn_jd1 - 1, "%Y %m %d")); + list($hn_year, $hn_isoweek, $hn_pisoday) = + Date_Calc::isoWeekDate($hn_lastday, + $hn_lastmonth, + $hn_year); + } else { + $hn_year = $pn_year; + $hn_isoweek = floor(($hn_day + $hn_wd1 - 9) / 7) + 1; + } + + break; + } + } + + return array((int) $hn_year, (int) $hn_isoweek, (int) $hn_wd); + } + + + // }}} + // {{{ gregorianToISO() + + /** + * Converts from Gregorian Year-Month-Day to ISO Year-WeekNumber-WeekDay + * + * Uses ISO 8601 definitions. + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return string the date in ISO Year-WeekNumber-WeekDay format + * @access public + * @static + */ + function gregorianToISO($day, $month, $year) + { + list($yearnumber, $weeknumber, $weekday) = + Date_Calc::isoWeekDate($day, $month, $year); + return sprintf("%04d", $yearnumber) . + '-' . + sprintf("%02d", $weeknumber) . + '-' . + $weekday; + } + + + // }}} + // {{{ weekOfYear4th() + + /** + * Returns week of the year counting week 1 as the week that contains 4th + * January + * + * Week 1 is determined to be the week that includes the 4th January, and + * therefore can be defined as the first week of the year that has at least + * 4 days. The previous week is counted as week 52 or 53 of the previous + * year. Note that this definition depends on which day is the first day of + * the week, and that if this is not passed as the '$pn_firstdayofweek' + * parameter, the default is assumed. + * + * Note also that the last day week of the year is likely to extend into + * the following year, except in the case that the last day of the week + * falls on 31st December. + * + * Also note that this is very similar to the ISO week returned by + * 'isoWeekDate()', the difference being that the ISO week always has + * 7 days, and if the 4th of January is a Friday, for example, + * ISO week 1 would start on Monday, 31st December in the previous year, + * whereas the week defined by this function would start on 1st January, + * but would be only 6 days long. Of course you can also set the day + * of the week, whereas the ISO week starts on a Monday by definition. + * + * Returned week is an integer from 1 to 53. + * + * @param int $pn_day the day of the month, default is current + * local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is + * current local year + * @param int $pn_firstdayofweek optional integer specifying the first day + * of the week + * + * @return array array of year, week no as integers + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function weekOfYear4th($pn_day = 0, + $pn_month = 0, + $pn_year = null, + $pn_firstdayofweek = DATE_CALC_BEGIN_WEEKDAY) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $hn_wd1 = Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfYear($pn_year)); + $hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year); + $hn_week = floor(($hn_day + + (10 + $hn_wd1 - $pn_firstdayofweek) % 7 + + 3) / 7); + + if ($hn_week > 0) { + $hn_year = $pn_year; + } else { + // Week number is the last week of the previous year: + // + list($hn_year, $hn_lastmonth, $hn_lastday) = + explode(" ", + Date_Calc::daysToDate(Date_Calc::lastDayOfYear($pn_year - 1), + "%Y %m %d")); + list($hn_year, $hn_week) = + Date_Calc::weekOfYear4th($hn_lastday, + $hn_lastmonth, + $hn_year, + $pn_firstdayofweek); + } + + return array((int) $hn_year, (int) $hn_week); + } + + + // }}} + // {{{ weekOfYear7th() + + /** + * Returns week of the year counting week 1 as the week that contains 7th + * January + * + * Week 1 is determined to be the week that includes the 7th January, and + * therefore can be defined as the first full week of the year. The + * previous week is counted as week 52 or 53 of the previous year. Note + * that this definition depends on which day is the first day of the week, + * and that if this is not passed as the '$pn_firstdayofweek' parameter, the + * default is assumed. + * + * Note also that the last day week of the year is likely to extend into + * the following year, except in the case that the last day of the week + * falls on 31st December. + * + * Returned week is an integer from 1 to 53. + * + * @param int $pn_day the day of the month, default is current + * local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is + * current local year + * @param int $pn_firstdayofweek optional integer specifying the first day + * of the week + * + * @return array array of year, week no as integers + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function weekOfYear7th($pn_day = 0, + $pn_month = 0, + $pn_year = null, + $pn_firstdayofweek = DATE_CALC_BEGIN_WEEKDAY) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $hn_wd1 = Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfYear($pn_year)); + $hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year); + $hn_week = floor(($hn_day + (6 + $hn_wd1 - $pn_firstdayofweek) % 7) / 7); + + if ($hn_week > 0) { + $hn_year = $pn_year; + } else { + // Week number is the last week of the previous ISO year: + // + list($hn_year, $hn_lastmonth, $hn_lastday) = explode(" ", Date_Calc::daysToDate(Date_Calc::lastDayOfYear($pn_year - 1), "%Y %m %d")); + list($hn_year, $hn_week) = Date_Calc::weekOfYear7th($hn_lastday, $hn_lastmonth, $hn_year, $pn_firstdayofweek); + } + + return array((int) $hn_year, (int) $hn_week); + } + + + // }}} + // {{{ dateSeason() + + /** + * Determines julian date of the given season + * + * Adapted from previous work in Java by James Mark Hamilton. + * + * @param string $season the season to get the date for: VERNALEQUINOX, + * SUMMERSOLSTICE, AUTUMNALEQUINOX, + * or WINTERSOLSTICE + * @param string $year the year in four digit format. Must be between + * -1000 B.C. and 3000 A.D. + * + * @return float the julian date the season starts on + * @access public + * @static + */ + function dateSeason($season, $year = 0) + { + if ($year == '') { + $year = Date_Calc::dateNow('%Y'); + } + if (($year >= -1000) && ($year <= 1000)) { + $y = $year / 1000.0; + switch ($season) { + case 'VERNALEQUINOX': + $juliandate = (((((((-0.00071 * $y) - 0.00111) * $y) + 0.06134) * $y) + 365242.1374) * $y) + 1721139.29189; + break; + case 'SUMMERSOLSTICE': + $juliandate = (((((((0.00025 * $y) + 0.00907) * $y) - 0.05323) * $y) + 365241.72562) * $y) + 1721233.25401; + break; + case 'AUTUMNALEQUINOX': + $juliandate = (((((((0.00074 * $y) - 0.00297) * $y) - 0.11677) * $y) + 365242.49558) * $y) + 1721325.70455; + break; + case 'WINTERSOLSTICE': + default: + $juliandate = (((((((-0.00006 * $y) - 0.00933) * $y) - 0.00769) * $y) + 365242.88257) * $y) + 1721414.39987; + } + } elseif (($year > 1000) && ($year <= 3000)) { + $y = ($year - 2000) / 1000; + switch ($season) { + case 'VERNALEQUINOX': + $juliandate = (((((((-0.00057 * $y) - 0.00411) * $y) + 0.05169) * $y) + 365242.37404) * $y) + 2451623.80984; + break; + case 'SUMMERSOLSTICE': + $juliandate = (((((((-0.0003 * $y) + 0.00888) * $y) + 0.00325) * $y) + 365241.62603) * $y) + 2451716.56767; + break; + case 'AUTUMNALEQUINOX': + $juliandate = (((((((0.00078 * $y) + 0.00337) * $y) - 0.11575) * $y) + 365242.01767) * $y) + 2451810.21715; + break; + case 'WINTERSOLSTICE': + default: + $juliandate = (((((((0.00032 * $y) - 0.00823) * $y) - 0.06223) * $y) + 365242.74049) * $y) + 2451900.05952; + } + } + return $juliandate; + } + + + // }}} + // {{{ dayOfYear() + + /** + * Returns number of days since 31 December of year before given date + * + * @param int $pn_day the day of the month, default is current local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is current + * local year + * + * @return int + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function dayOfYear($pn_day = 0, $pn_month = 0, $pn_year = null) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $hn_jd = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year); + $hn_jd1 = Date_Calc::firstDayOfYear($pn_year); + return $hn_jd - $hn_jd1 + 1; + } + + + // }}} + // {{{ julianDate() + + /** + * Returns number of days since 31 December of year before given date + * + * @param int $pn_day the day of the month, default is current local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is current + * local year + * + * @return int + * @access public + * @static + * @deprecated Method deprecated in Release 1.5.0 + */ + function julianDate($pn_day = 0, $pn_month = 0, $pn_year = null) + { + return Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year); + } + + + // }}} + // {{{ getWeekdayFullname() + + /** + * Returns the full weekday name for the given date + * + * @param int $pn_day the day of the month, default is current local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is current + * local year + * + * @return string the full name of the day of the week + * @access public + * @static + */ + function getWeekdayFullname($pn_day = 0, $pn_month = 0, $pn_year = null) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $weekday_names = Date_Calc::getWeekDays(); + $weekday = Date_Calc::dayOfWeek($pn_day, $pn_month, $pn_year); + return $weekday_names[$weekday]; + } + + + // }}} + // {{{ getWeekdayAbbrname() + + /** + * Returns the abbreviated weekday name for the given date + * + * @param int $pn_day the day of the month, default is current local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is current + * local year + * @param int $length the length of abbreviation + * + * @return string the abbreviated name of the day of the week + * @access public + * @static + * @see Date_Calc::getWeekdayFullname() + */ + function getWeekdayAbbrname($pn_day = 0, + $pn_month = 0, + $pn_year = null, + $length = 3) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $weekday_names = Date_Calc::getWeekDays(true); + $weekday = Date_Calc::dayOfWeek($pn_day, $pn_month, $pn_year); + return $weekday_names[$weekday]; + } + + + // }}} + // {{{ getMonthFullname() + + /** + * Returns the full month name for the given month + * + * @param int $month the month + * + * @return string the full name of the month + * @access public + * @static + */ + function getMonthFullname($month) + { + $month = (int)$month; + if (empty($month)) { + $month = (int)Date_Calc::dateNow('%m'); + } + + $month_names = Date_Calc::getMonthNames(); + return $month_names[$month]; + } + + + // }}} + // {{{ getMonthAbbrname() + + /** + * Returns the abbreviated month name for the given month + * + * @param int $month the month + * @param int $length the length of abbreviation + * + * @return string the abbreviated name of the month + * @access public + * @static + * @see Date_Calc::getMonthFullname + */ + function getMonthAbbrname($month, $length = 3) + { + $month = (int)$month; + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + $month_names = Date_Calc::getMonthNames(true); + return $month_names[$month]; + } + + + // }}} + // {{{ getMonthFromFullname() + + /** + * Returns the numeric month from the month name or an abreviation + * + * Both August and Aug would return 8. + * + * @param string $month the name of the month to examine. + * Case insensitive. + * + * @return int the month's number + * @access public + * @static + */ + function getMonthFromFullName($month) + { + $month = strtolower($month); + $months = Date_Calc::getMonthNames(); + while (list($id, $name) = each($months)) { + if (ereg($month, strtolower($name))) { + return $id; + } + } + return 0; + } + + + // }}} + // {{{ getWeekDays() + + /** + * Returns an array of week day names + * + * Used to take advantage of the setlocale function to return language + * specific week days. + * + * @param int $pb_abbreviated whether to return the abbreviated form of the + * days + * + * @return array an array of week-day names + * @access public + * @static + */ + function getWeekDays($pb_abbreviated = false) + { + for ($i = 0; $i < 7; $i++) { + $weekdays[$i] = strftime($pb_abbreviated ? '%a' : '%A', + mktime(0, 0, 0, 1, $i, 2001)); + } + return $weekdays; + } + + + // }}} + // {{{ daysToDayOfWeek() + + /** + * Returns day of week for specified 'Julian Day' + * + * The algorithm is valid for all years (positive and negative), and + * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'), + * and so the only limitation is platform-dependent (for 32-bit systems + * the maximum year would be something like about 1,465,190 A.D.). + * + * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'. + * + * @param int $pn_days the number of days since 24th November, 4714 B.C. + * + * @return int integer from 0 to 7 where 0 represents Sunday + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function daysToDayOfWeek($pn_days) + { + // On Julian day 0 the day is Monday (PHP day 1): + // + $ret = ($pn_days + 1) % 7; + return $ret < 0 ? $ret + 7 : $ret; + } + + + // }}} + // {{{ dayOfWeek() + + /** + * Returns day of week for given date (0 = Sunday) + * + * The algorithm is valid for all years (positive and negative). + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * + * @return int the number of the day in the week + * @access public + * @static + */ + function dayOfWeek($day = null, $month = null, $year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + // if ($month <= 2) { + // $month += 12; + // --$year; + // } + + // $wd = ($day + + // intval((13 * $month + 3) / 5) + + // $year + + // floor($year / 4) - + // floor($year / 100) + + // floor($year / 400) + + // 1) % 7; + + // return (int) ($wd < 0 ? $wd + 7 : $wd); + + return Date_Calc::daysToDayOfWeek(Date_Calc::dateToDays($day, + $month, + $year)); + } + + + // }}} + // {{{ weekOfYearAbsolute() + + /** + * Returns week of the year counting week 1 as 1st-7th January, + * regardless of what day 1st January falls on + * + * Returned value is an integer from 1 to 53. Week 53 will start on + * 31st December and have only one day, except in a leap year, in + * which it will start a day earlier and contain two days. + * + * @param int $pn_day the day of the month, default is current local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is current + * local year + * + * @return int integer from 1 to 53 + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function weekOfYearAbsolute($pn_day = 0, $pn_month = 0, $pn_year = null) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year); + return intval(($hn_day + 6) / 7); + } + + + // }}} + // {{{ weekOfYear1st() + + /** + * Returns week of the year counting week 1 as the week that contains 1st + * January + * + * Week 1 is determined to be the week that includes the 1st January, even + * if this week extends into the previous year, in which case the week will + * only contain between 1 and 6 days of the current year. Note that this + * definition depends on which day is the first day of the week, and that if + * this is not passed as the '$pn_firstdayofweek' parameter, the default is + * assumed. + * + * Note also that the last day week of the year is also likely to contain + * less than seven days, except in the case that the last day of the week + * falls on 31st December. + * + * Returned value is an integer from 1 to 54. The year will only contain + * 54 weeks in the case of a leap year in which 1st January is the last day + * of the week, and 31st December is the first day of the week. In this + * case, both weeks 1 and 54 will contain one day only. + * + * @param int $pn_day the day of the month, default is current + * local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is + * current local year + * @param int $pn_firstdayofweek optional integer specifying the first day + * of the week + * + * @return int integer from 1 to 54 + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function weekOfYear1st($pn_day = 0, + $pn_month = 0, + $pn_year = null, + $pn_firstdayofweek = DATE_CALC_BEGIN_WEEKDAY) + { + if (is_null($pn_year)) { + $pn_year = Date_Calc::dateNow('%Y'); + } + if (empty($pn_month)) { + $pn_month = Date_Calc::dateNow('%m'); + } + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + + $hn_wd1 = Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfYear($pn_year)); + $hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year); + return floor(($hn_day + (7 + $hn_wd1 - $pn_firstdayofweek) % 7 + 6) / 7); + } + + + // }}} + // {{{ weekOfYear() + + /** + * Returns week of the year, where first Sunday is first day of first week + * + * N.B. this function is equivalent to calling: + * + * Date_Calc::weekOfYear7th($day, $month, $year, 0) + * + * Returned week is an integer from 1 to 53. + * + * @param int $pn_day the day of the month, default is current local day + * @param int $pn_month the month, default is current local month + * @param int $pn_year the year in four digit format, default is current + * local year + * + * @return int integer from 1 to 53 + * @access public + * @static + * @see Date_Calc::weekOfYear7th + * @deprecated Method deprecated in Release 1.5.0 + */ + function weekOfYear($pn_day = 0, $pn_month = 0, $pn_year = null) + { + $ha_week = Date_Calc::weekOfYear7th($pn_day, $pn_month, $pn_year, 0); + return $ha_week[1]; + } + + + // }}} + // {{{ weekOfMonthAbsolute() + + /** + * Returns week of the month counting week 1 as 1st-7th of the month, + * regardless of what day the 1st falls on + * + * Returned value is an integer from 1 to 5. Week 5 will start on + * the 29th of the month and have between 1 and 3 days, except + * in February in a non-leap year, when there will be 4 weeks only. + * + * @param int $pn_day the day of the month, default is current local day + * + * @return int integer from 1 to 5 + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function weekOfMonthAbsolute($pn_day = 0) + { + if (empty($pn_day)) { + $pn_day = Date_Calc::dateNow('%d'); + } + return intval(($pn_day + 6) / 7); + } + + + // }}} + // {{{ weekOfMonth() + + /** + * Alias for 'weekOfMonthAbsolute()' + * + * @param int $pn_day the day of the month, default is current local day + * + * @return int integer from 1 to 5 + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function weekOfMonth($pn_day = 0) + { + return Date_Calc::weekOfMonthAbsolute($pn_day); + } + + + // }}} + // {{{ quarterOfYear() + + /** + * Returns quarter of the year for given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * + * @return int the number of the quarter in the year + * @access public + * @static + */ + function quarterOfYear($day = 0, $month = 0, $year = null) + { + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + return intval(($month - 1) / 3 + 1); + } + + + // }}} + // {{{ daysInMonth() + + /** + * Returns the number of days in the given month + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * + * @return int the number of days the month has + * @access public + * @static + */ + function daysInMonth($month = 0, $year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + return Date_Calc::lastDayOfMonth($month, $year) - + Date_Calc::firstDayOfMonth($month, $year) + + 1; + } + + + // }}} + // {{{ daysInYear() + + /** + * Returns the number of days in the given year + * + * @param int $year the year in four digit format, default is current local + * year + * + * @return int the number of days the year has + * @access public + * @static + */ + function daysInYear($year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + + return Date_Calc::firstDayOfYear($year + 1) - + Date_Calc::firstDayOfYear($year); + } + + + // }}} + // {{{ weeksInMonth() + + /** + * Returns the number of rows on a calendar month + * + * Useful for determining the number of rows when displaying a typical + * month calendar. + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * + * @return int the number of weeks the month has + * @access public + * @static + */ + function weeksInMonth($month = 0, $year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + $FDOM = Date_Calc::firstOfMonthWeekday($month, $year); + if (DATE_CALC_BEGIN_WEEKDAY==1 && $FDOM==0) { + $first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY; + $weeks = 1; + } elseif (DATE_CALC_BEGIN_WEEKDAY==0 && $FDOM == 6) { + $first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY; + $weeks = 1; + } else { + $first_week_days = DATE_CALC_BEGIN_WEEKDAY - $FDOM; + $weeks = 0; + } + $first_week_days %= 7; + return ceil((Date_Calc::daysInMonth($month, $year) + - $first_week_days) / 7) + $weeks; + } + + + // }}} + // {{{ getCalendarWeek() + + /** + * Return an array with days in week + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return array $week[$weekday] + * @access public + * @static + */ + function getCalendarWeek($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $week_array = array(); + + // date for the column of week + + $curr_day = Date_Calc::beginOfWeek($day, $month, $year, '%E'); + + for ($counter = 0; $counter <= 6; $counter++) { + $week_array[$counter] = Date_Calc::daysToDate($curr_day, $format); + $curr_day++; + } + return $week_array; + } + + + // }}} + // {{{ getCalendarMonth() + + /** + * Return a set of arrays to construct a calendar month for the given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return array $month[$row][$col] + * @access public + * @static + */ + function getCalendarMonth($month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + $month_array = array(); + + // date for the first row, first column of calendar month + if (DATE_CALC_BEGIN_WEEKDAY == 1) { + if (Date_Calc::firstOfMonthWeekday($month, $year) == 0) { + $curr_day = Date_Calc::firstDayOfMonth($month, $year) - 6; + } else { + $curr_day = Date_Calc::firstDayOfMonth($month, $year) + - Date_Calc::firstOfMonthWeekday($month, $year) + 1; + } + } else { + $curr_day = (Date_Calc::firstDayOfMonth($month, $year) + - Date_Calc::firstOfMonthWeekday($month, $year)); + } + + // number of days in this month + $daysInMonth = Date_Calc::daysInMonth($month, $year); + + $weeksInMonth = Date_Calc::weeksInMonth($month, $year); + for ($row_counter = 0; $row_counter < $weeksInMonth; $row_counter++) { + for ($column_counter = 0; $column_counter <= 6; $column_counter++) { + $month_array[$row_counter][$column_counter] = + Date_Calc::daysToDate($curr_day, $format); + $curr_day++; + } + } + + return $month_array; + } + + + // }}} + // {{{ getCalendarYear() + + /** + * Return a set of arrays to construct a calendar year for the given date + * + * @param int $year the year in four digit format, default current + * local year + * @param string $format the string indicating how to format the output + * + * @return array $year[$month][$row][$col] + * @access public + * @static + */ + function getCalendarYear($year = null, $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + + $year_array = array(); + + for ($curr_month = 0; $curr_month <= 11; $curr_month++) { + $year_array[$curr_month] = + Date_Calc::getCalendarMonth($curr_month + 1, + $year, $format); + } + + return $year_array; + } + + + // }}} + // {{{ prevDay() + + /** + * Returns date of day before given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function prevDay($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + return Date_Calc::addDays(-1, $day, $month, $year, $format); + } + + + // }}} + // {{{ nextDay() + + /** + * Returns date of day after given date + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function nextDay($day = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + return Date_Calc::addDays(1, $day, $month, $year, $format); + } + + + // }}} + // {{{ prevWeekday() + + /** + * Returns date of the previous weekday, skipping from Monday to Friday + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function prevWeekday($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $days = Date_Calc::dateToDays($day, $month, $year); + if (Date_Calc::dayOfWeek($day, $month, $year) == 1) { + $days -= 3; + } elseif (Date_Calc::dayOfWeek($day, $month, $year) == 0) { + $days -= 2; + } else { + $days -= 1; + } + + return Date_Calc::daysToDate($days, $format); + } + + + // }}} + // {{{ nextWeekday() + + /** + * Returns date of the next weekday of given date, skipping from + * Friday to Monday + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function nextWeekday($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $days = Date_Calc::dateToDays($day, $month, $year); + if (Date_Calc::dayOfWeek($day, $month, $year) == 5) { + $days += 3; + } elseif (Date_Calc::dayOfWeek($day, $month, $year) == 6) { + $days += 2; + } else { + $days += 1; + } + + return Date_Calc::daysToDate($days, $format); + } + + + // }}} + // {{{ daysToPrevDayOfWeek() + + /** + * Returns 'Julian Day' of the previous specific day of the week + * from the given date. + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $days 'Julian Day', i.e. the no of days since 1st + * January, 4713 B.C. + * @param bool $onorbefore if true and days are same, returns current day + * + * @return int 'Julian Day', i.e. the no of days since 1st January, + * 4713 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function daysToPrevDayOfWeek($dow, $days, $onorbefore = false) + { + $curr_weekday = Date_Calc::daysToDayOfWeek($days); + if ($curr_weekday == $dow) { + if ($onorbefore) { + return $days; + } else { + return $days - 7; + } + } else if ($curr_weekday < $dow) { + return $days - 7 + $dow - $curr_weekday; + } else { + return $days - $curr_weekday + $dow; + } + } + + + // }}} + // {{{ prevDayOfWeek() + + /** + * Returns date of the previous specific day of the week + * from the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local + * day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is + * current local year + * @param string $format the string indicating how to format the output + * @param bool $onorbefore if true and days are same, returns current day + * + * @return string the date in the desired format + * @access public + * @static + */ + function prevDayOfWeek($dow, + $day = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT, + $onorbefore = false) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $days = Date_Calc::dateToDays($day, $month, $year); + $days = Date_Calc::daysToPrevDayOfWeek($dow, $days, $onorbefore); + return Date_Calc::daysToDate($days, $format); + } + + + // }}} + // {{{ daysToNextDayOfWeek() + + /** + * Returns 'Julian Day' of the next specific day of the week + * from the given date. + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $days 'Julian Day', i.e. the no of days since 1st + * January, 4713 B.C. + * @param bool $onorafter if true and days are same, returns current day + * + * @return int 'Julian Day', i.e. the no of days since 1st January, + * 4713 B.C. + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function daysToNextDayOfWeek($dow, $days, $onorafter = false) + { + $curr_weekday = Date_Calc::daysToDayOfWeek($days); + if ($curr_weekday == $dow) { + if ($onorafter) { + return $days; + } else { + return $days + 7; + } + } else if ($curr_weekday > $dow) { + return $days + 7 - $curr_weekday + $dow; + } else { + return $days + $dow - $curr_weekday; + } + } + + + // }}} + // {{{ nextDayOfWeek() + + /** + * Returns date of the next specific day of the week + * from the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local + * day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is + * current local year + * @param string $format the string indicating how to format the output + * @param bool $onorafter if true and days are same, returns current day + * + * @return string the date in the desired format + * @access public + * @static + */ + function nextDayOfWeek($dow, + $day = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT, + $onorafter = false) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $days = Date_Calc::dateToDays($day, $month, $year); + $days = Date_Calc::daysToNextDayOfWeek($dow, $days, $onorafter); + return Date_Calc::daysToDate($days, $format); + } + + + // }}} + // {{{ prevDayOfWeekOnOrBefore() + + /** + * Returns date of the previous specific day of the week + * on or before the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function prevDayOfWeekOnOrBefore($dow, + $day = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + return Date_Calc::prevDayOfWeek($dow, + $day, + $month, + $year, + $format, + true); + } + + + // }}} + // {{{ nextDayOfWeekOnOrAfter() + + /** + * Returns date of the next specific day of the week + * on or after the given date + * + * @param int $dow the day of the week (0 = Sunday) + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function nextDayOfWeekOnOrAfter($dow, + $day = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + return Date_Calc::nextDayOfWeek($dow, + $day, + $month, + $year, + $format, + true); + } + + + // }}} + // {{{ beginOfWeek() + + /** + * Find the month day of the beginning of week for given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of prev month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function beginOfWeek($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $hn_days = Date_Calc::dateToDays($day, $month, $year); + $this_weekday = Date_Calc::daysToDayOfWeek($hn_days); + $interval = (7 - DATE_CALC_BEGIN_WEEKDAY + $this_weekday) % 7; + return Date_Calc::daysToDate($hn_days - $interval, $format); + } + + + // }}} + // {{{ endOfWeek() + + /** + * Find the month day of the end of week for given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of following month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function endOfWeek($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + $hn_days = Date_Calc::dateToDays($day, $month, $year); + $this_weekday = Date_Calc::daysToDayOfWeek($hn_days); + $interval = (6 + DATE_CALC_BEGIN_WEEKDAY - $this_weekday) % 7; + return Date_Calc::daysToDate($hn_days + $interval, $format); + } + + + // }}} + // {{{ beginOfPrevWeek() + + /** + * Find the month day of the beginning of week before given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of prev month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function beginOfPrevWeek($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + list($hn_pwyear, $hn_pwmonth, $hn_pwday) = + explode(" ", Date_Calc::daysToDate(Date_Calc::dateToDays($day, + $month, + $year) - 7, + '%Y %m %d')); + return Date_Calc::beginOfWeek($hn_pwday, + $hn_pwmonth, + $hn_pwyear, + $format); + } + + + // }}} + // {{{ beginOfNextWeek() + + /** + * Find the month day of the beginning of week after given date, + * using DATE_CALC_BEGIN_WEEKDAY + * + * Can return weekday of prev month. + * + * @param int $day the day of the month, default is current local day + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function beginOfNextWeek($day = 0, $month = 0, $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + if (empty($day)) { + $day = Date_Calc::dateNow('%d'); + } + + list($hn_pwyear, $hn_pwmonth, $hn_pwday) = + explode(" ", + Date_Calc::daysToDate(Date_Calc::dateToDays($day, + $month, + $year) + 7, + '%Y %m %d')); + return Date_Calc::beginOfWeek($hn_pwday, + $hn_pwmonth, + $hn_pwyear, + $format); + } + + + // }}} + // {{{ beginOfMonth() + + /** + * Return date of first day of month of given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function beginOfMonth($month = 0, $year = null, $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + return Date_Calc::dateFormat(Date_Calc::getFirstDayOfMonth($month, + $year), + $month, + $year, + $format); + } + + + // }}} + // {{{ endOfMonth() + + /** + * Return date of last day of month of given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @since Method available since Release 1.5.0 + * @deprecated Method deprecated in Release 1.5.0 + */ + function endOfMonth($month = 0, $year = null, $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + return Date_Calc::daysToDate(Date_Calc::lastDayOfMonth($month, $year), + $format); + } + + + // }}} + // {{{ beginOfPrevMonth() + + /** + * Returns date of the first day of previous month of given date + * + * @param mixed $dummy irrelevant parameter + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function beginOfPrevMonth($dummy = null, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + list($hn_pmyear, $hn_prevmonth) = Date_Calc::prevMonth($month, $year); + return Date_Calc::dateFormat(Date_Calc::getFirstDayOfMonth($hn_prevmonth, + $hn_pmyear), + $hn_prevmonth, + $hn_pmyear, + $format); + } + + + // }}} + // {{{ endOfPrevMonth() + + /** + * Returns date of the last day of previous month for given date + * + * @param mixed $dummy irrelevant parameter + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @see Date_Calc::endOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function endOfPrevMonth($dummy = null, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + return Date_Calc::daysToDate(Date_Calc::firstDayOfMonth($month, + $year) - 1, + $format); + } + + + // }}} + // {{{ beginOfNextMonth() + + /** + * Returns date of begin of next month of given date + * + * @param mixed $dummy irrelevant parameter + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @see Date_Calc::beginOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function beginOfNextMonth($dummy = null, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + list($hn_nmyear, $hn_nextmonth) = Date_Calc::nextMonth($month, $year); + return Date_Calc::dateFormat(Date_Calc::getFirstDayOfMonth($hn_nextmonth, + $hn_nmyear), + $hn_nextmonth, + $hn_nmyear, + $format); + } + + + // }}} + // {{{ endOfNextMonth() + + /** + * Returns date of the last day of next month of given date + * + * @param mixed $dummy irrelevant parameter + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @see Date_Calc::endOfMonthBySpan() + * @deprecated Method deprecated in Release 1.4.4 + */ + function endOfNextMonth($dummy = null, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + list($hn_nmyear, $hn_nextmonth) = Date_Calc::nextMonth($month, $year); + return Date_Calc::daysToDate(Date_Calc::lastDayOfMonth($hn_nextmonth, + $hn_nmyear), + $format); + } + + + // }}} + // {{{ beginOfMonthBySpan() + + /** + * Returns date of the first day of the month in the number of months + * from the given date + * + * @param int $months the number of months from the date provided. + * Positive numbers go into the future. + * Negative numbers go into the past. + * 0 is the month presented in $month. + * @param string $month the month, default is current local month + * @param string $year the year in four digit format, default is the + * current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @since Method available since Release 1.4.4 + */ + function beginOfMonthBySpan($months = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + return Date_Calc::addMonths($months, + Date_Calc::getFirstDayOfMonth($month, $year), + $month, + $year, + $format); + } + + + // }}} + // {{{ endOfMonthBySpan() + + /** + * Returns date of the last day of the month in the number of months + * from the given date + * + * @param int $months the number of months from the date provided. + * Positive numbers go into the future. + * Negative numbers go into the past. + * 0 is the month presented in $month. + * @param string $month the month, default is current local month + * @param string $year the year in four digit format, default is the + * current local year + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + * @since Method available since Release 1.4.4 + */ + function endOfMonthBySpan($months = 0, + $month = 0, + $year = null, + $format = DATE_CALC_FORMAT) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + + $hn_days = Date_Calc::addMonthsToDays($months + 1, + Date_Calc::firstDayOfMonth($month, $year)) - 1; + return Date_Calc::daysToDate($hn_days, $format); + } + + + // }}} + // {{{ firstOfMonthWeekday() + + /** + * Find the day of the week for the first of the month of given date + * + * @param int $month the month, default is current local month + * @param int $year the year in four digit format, default is current + * local year + * + * @return int number of weekday for the first day, 0=Sunday + * @access public + * @static + */ + function firstOfMonthWeekday($month = 0, $year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if (empty($month)) { + $month = Date_Calc::dateNow('%m'); + } + return Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfMonth($month, + $year)); + } + + + // }}} + // {{{ nWeekdayOfMonth() + + /** + * Calculates the date of the Nth weekday of the month, + * such as the second Saturday of January 2000 + * + * @param int $week the number of the week to get + * (1 = first, etc. Also can be 'last'.) + * @param int $dow the day of the week (0 = Sunday) + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * @param string $format the string indicating how to format the output + * + * @return string the date in the desired format + * @access public + * @static + */ + function nWeekdayOfMonth($week, $dow, $month, $year, + $format = DATE_CALC_FORMAT) + { + if (is_numeric($week)) { + $DOW1day = ($week - 1) * 7 + 1; + $DOW1 = Date_Calc::dayOfWeek($DOW1day, $month, $year); + $wdate = ($week - 1) * 7 + 1 + (7 + $dow - $DOW1) % 7; + if ($wdate > Date_Calc::daysInMonth($month, $year)) { + return -1; + } else { + return Date_Calc::dateFormat($wdate, $month, $year, $format); + } + } elseif ($week == 'last' && $dow < 7) { + $lastday = Date_Calc::daysInMonth($month, $year); + $lastdow = Date_Calc::dayOfWeek($lastday, $month, $year); + $diff = $dow - $lastdow; + if ($diff > 0) { + return Date_Calc::dateFormat($lastday - (7 - $diff), $month, + $year, $format); + } else { + return Date_Calc::dateFormat($lastday + $diff, $month, + $year, $format); + } + } else { + return -1; + } + } + + + // }}} + // {{{ isValidDate() + + /** + * Returns true for valid date, false for invalid date + * + * Uses the proleptic Gregorian calendar, with the year 0 (1 B.C.) + * assumed to be valid and also assumed to be a leap year. + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return bool + * @access public + * @static + */ + function isValidDate($day, $month, $year) + { + if ($day < 1 || $month < 1 || $month > 12) + return false; + if ($month == 2) { + if (Date_Calc::isLeapYearGregorian($year)) { + return $day <= 29; + } else { + return $day <= 28; + } + } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) { + return $day <= 30; + } else { + return $day <= 31; + } + } + + + // }}} + // {{{ isLeapYearGregorian() + + /** + * Returns true for a leap year, else false + * + * Uses the proleptic Gregorian calendar. The year 0 (1 B.C.) is + * assumed in this algorithm to be a leap year. The function is + * valid for all years, positive and negative. + * + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return bool + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function isLeapYearGregorian($year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + return (($year % 4 == 0) && + ($year % 100 != 0)) || + ($year % 400 == 0); + } + + + // }}} + // {{{ isLeapYearJulian() + + /** + * Returns true for a leap year, else false + * + * Uses the proleptic Julian calendar. The year 0 (1 B.C.) is + * assumed in this algorithm to be a leap year. The function is + * valid for all years, positive and negative. + * + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return boolean + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function isLeapYearJulian($year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + return $year % 4 == 0; + } + + + // }}} + // {{{ isLeapYear() + + /** + * Returns true for a leap year, else false + * + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return boolean + * @access public + * @static + */ + function isLeapYear($year = null) + { + if (is_null($year)) { + $year = Date_Calc::dateNow('%Y'); + } + if ($year < 1582) { + // pre Gregorio XIII - 1582 + return Date_Calc::isLeapYearJulian($year); + } else { + // post Gregorio XIII - 1582 + return Date_Calc::isLeapYearGregorian($year); + } + } + + + // }}} + // {{{ isFutureDate() + + /** + * Determines if given date is a future date from now + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return bool + * @access public + * @static + */ + function isFutureDate($day, $month, $year) + { + $this_year = Date_Calc::dateNow('%Y'); + $this_month = Date_Calc::dateNow('%m'); + $this_day = Date_Calc::dateNow('%d'); + + if ($year > $this_year) { + return true; + } elseif ($year == $this_year) { + if ($month > $this_month) { + return true; + } elseif ($month == $this_month) { + if ($day > $this_day) { + return true; + } + } + } + return false; + } + + + // }}} + // {{{ isPastDate() + + /** + * Determines if given date is a past date from now + * + * @param int $day the day of the month + * @param int $month the month + * @param int $year the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return boolean + * @access public + * @static + */ + function isPastDate($day, $month, $year) + { + $this_year = Date_Calc::dateNow('%Y'); + $this_month = Date_Calc::dateNow('%m'); + $this_day = Date_Calc::dateNow('%d'); + + if ($year < $this_year) { + return true; + } elseif ($year == $this_year) { + if ($month < $this_month) { + return true; + } elseif ($month == $this_month) { + if ($day < $this_day) { + return true; + } + } + } + return false; + } + + + // }}} + // {{{ dateDiff() + + /** + * Returns number of days between two given dates + * + * @param int $day1 the day of the month + * @param int $month1 the month + * @param int $year1 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * @param int $day2 the day of the month + * @param int $month2 the month + * @param int $year2 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return int the absolute number of days between the two dates. + * If an error occurs, -1 is returned. + * @access public + * @static + */ + function dateDiff($day1, $month1, $year1, $day2, $month2, $year2) + { + if (!Date_Calc::isValidDate($day1, $month1, $year1)) { + return -1; + } + if (!Date_Calc::isValidDate($day2, $month2, $year2)) { + return -1; + } + return abs(Date_Calc::dateToDays($day1, $month1, $year1) + - Date_Calc::dateToDays($day2, $month2, $year2)); + } + + + // }}} + // {{{ compareDates() + + /** + * Compares two dates + * + * @param int $day1 the day of the month + * @param int $month1 the month + * @param int $year1 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * @param int $day2 the day of the month + * @param int $month2 the month + * @param int $year2 the year. Use the complete year instead of the + * abbreviated version. E.g. use 2005, not 05. + * + * @return int 0 if the dates are equal. 1 if date 1 is later, -1 + * if date 1 is earlier. + * @access public + * @static + */ + function compareDates($day1, $month1, $year1, $day2, $month2, $year2) + { + $ndays1 = Date_Calc::dateToDays($day1, $month1, $year1); + $ndays2 = Date_Calc::dateToDays($day2, $month2, $year2); + if ($ndays1 == $ndays2) { + return 0; + } + return ($ndays1 > $ndays2) ? 1 : -1; + } + + + // }}} + // {{{ round() + + /** + * Rounds the date according to the specified precision + * + * The precision parameter must be one of the following constants: + * + * DATE_PRECISION_YEAR + * DATE_PRECISION_MONTH + * DATE_PRECISION_DAY + * DATE_PRECISION_HOUR + * DATE_PRECISION_10MINUTES + * DATE_PRECISION_MINUTE + * DATE_PRECISION_10SECONDS + * DATE_PRECISION_SECOND + * + * The precision can also be specified as an integral offset from + * one of these constants, where the offset reflects a precision + * of 10 to the power of the offset greater than the constant. + * For example: + * + * DATE_PRECISION_YEAR - 1 rounds the date to the nearest 10 + * years + * DATE_PRECISION_YEAR - 3 rounds the date to the nearest 1000 + * years + * DATE_PRECISION_SECOND + 1 rounds the date to 1 decimal + * point of a second + * DATE_PRECISION_SECOND + 1 rounds the date to 3 decimal + * points of a second + * DATE_PRECISION_SECOND + 1 rounds the date to the nearest 10 + * seconds (thus it is equivalent to + * DATE_PRECISION_10SECONDS) + * + * N.B. This function requires a time in UTC if both the precision is at + * least DATE_PRECISION_SECOND and leap seconds are being counted, otherwise + * any local time is acceptable. + * + * @param int $pn_precision a 'DATE_PRECISION_*' constant + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year + * @param int $pn_hour the hour + * @param int $pn_minute the minute + * @param mixed $pn_second the second as integer or float + * @param bool $pb_countleap whether to count leap seconds (defaults to + * DATE_COUNT_LEAP_SECONDS) + * + * @return array array of year, month, day, hour, minute, second + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function round($pn_precision, + $pn_day, + $pn_month, + $pn_year, + $pn_hour = 0, + $pn_minute = 0, + $pn_second = 0, + $pb_countleap = DATE_COUNT_LEAP_SECONDS) + { + if ($pn_precision <= DATE_PRECISION_YEAR) { + $hn_month = 0; + $hn_day = 0; + $hn_hour = 0; + $hn_minute = 0; + $hn_second = 0; + + if ($pn_precision < DATE_PRECISION_YEAR) { + $hn_year = round($pn_year, $pn_precision - DATE_PRECISION_YEAR); + } else { + // Check part-year: + // + $hn_midyear = (Date_Calc::firstDayOfYear($pn_year + 1) - + Date_Calc::firstDayOfYear($pn_year)) / 2; + if (($hn_days = Date_Calc::dayOfYear($pn_day, + $pn_month, + $pn_year)) <= + $hn_midyear - 1) { + $hn_year = $pn_year; + } else if ($hn_days >= $hn_midyear) { + // Round up: + // + $hn_year = $pn_year + 1; + } else { + // Take time into account: + // + $hn_partday = Date_Calc::secondsPastMidnight($pn_hour, + $pn_minute, + $pn_second) / + 86400; + if ($hn_partday >= $hn_midyear - $hn_days) { + // Round up: + // + $hn_year = $pn_year + 1; + } else { + $hn_year = $pn_year; + } + } + } + } else if ($pn_precision == DATE_PRECISION_MONTH) { + $hn_year = $pn_year; + $hn_day = 0; + $hn_hour = 0; + $hn_minute = 0; + $hn_second = 0; + + $hn_firstofmonth = Date_Calc::firstDayOfMonth($pn_month, $pn_year); + $hn_midmonth = (Date_Calc::lastDayOfMonth($pn_month, $pn_year) + + 1 - + $hn_firstofmonth) / 2; + if (($hn_days = Date_Calc::dateToDays($pn_day, + $pn_month, + $pn_year) - + $hn_firstofmonth) <= $hn_midmonth - 1) { + $hn_month = $pn_month; + } else if ($hn_days >= $hn_midmonth) { + // Round up: + // + list($hn_year, $hn_month) = Date_Calc::nextMonth($pn_month, + $pn_year); + } else { + // Take time into account: + // + $hn_partday = Date_Calc::secondsPastMidnight($pn_hour, + $pn_minute, + $pn_second) / + 86400; + if ($hn_partday >= $hn_midmonth - $hn_days) { + // Round up: + // + list($hn_year, $hn_month) = Date_Calc::nextMonth($pn_month, + $pn_year); + } else { + $hn_month = $pn_month; + } + } + } else if ($pn_precision == DATE_PRECISION_DAY) { + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_hour = 0; + $hn_minute = 0; + $hn_second = 0; + + if (Date_Calc::secondsPastMidnight($pn_hour, + $pn_minute, + $pn_second) >= 43200) { + // Round up: + // + list($hn_year, $hn_month, $hn_day) = + explode(" ", Date_Calc::nextDay($pn_day, + $pn_month, + $pn_year, + "%Y %m %d")); + } else { + $hn_day = $pn_day; + } + } else if ($pn_precision == DATE_PRECISION_HOUR) { + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_day = $pn_day; + $hn_minute = 0; + $hn_second = 0; + + if (Date_Calc::secondsPastTheHour($pn_minute, $pn_second) >= 1800) { + // Round up: + // + list($hn_year, $hn_month, $hn_day, $hn_hour) = + Date_Calc::addHours(1, + $pn_day, + $pn_month, + $pn_year, + $pn_hour); + } else { + $hn_hour = $pn_hour; + } + } else if ($pn_precision <= DATE_PRECISION_MINUTE) { + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_day = $pn_day; + $hn_hour = $pn_hour; + $hn_second = 0; + + if ($pn_precision < DATE_PRECISION_MINUTE) { + $hn_minute = round($pn_minute, + $pn_precision - DATE_PRECISION_MINUTE); + } else { + // Check seconds: + // + if ($pn_second >= 30) { + // Round up: + // + list($hn_year, + $hn_month, + $hn_day, + $hn_hour, + $hn_minute) = + Date_Calc::addMinutes(1, + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute); + } else { + $hn_minute = $pn_minute; + } + } + } else { + // Precision is at least (DATE_PRECISION_SECOND - 1): + // + $hn_year = $pn_year; + $hn_month = $pn_month; + $hn_day = $pn_day; + $hn_hour = $pn_hour; + $hn_minute = $pn_minute; + + $hn_second = round($pn_second, + $pn_precision - DATE_PRECISION_SECOND); + + if (fmod($hn_second, 1) == 0.0) { + $hn_second = (int) $hn_second; + + if ($hn_second != intval($pn_second)) { + list($hn_year, + $hn_month, + $hn_day, + $hn_hour, + $hn_minute, + $hn_second) = + Date_Calc::addSeconds($hn_second - intval($pn_second), + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute, + intval($pn_second), + $pn_precision >= + DATE_PRECISION_SECOND && + $pb_countleap); + // + // (N.B. if rounded to nearest 10 seconds, + // user does not expect seconds to be '60') + } + } + } + + return array((int) $hn_year, + (int) $hn_month, + (int) $hn_day, + (int) $hn_hour, + (int) $hn_minute, + $hn_second); + } + + + // }}} + // {{{ roundSeconds() + + /** + * Rounds seconds up or down to the nearest specified unit + * + * @param int $pn_precision number of digits after the decimal point + * @param int $pn_day the day of the month + * @param int $pn_month the month + * @param int $pn_year the year + * @param int $pn_hour the hour + * @param int $pn_minute the minute + * @param mixed $pn_second the second as integer or float + * @param bool $pb_countleap whether to count leap seconds (defaults to + * DATE_COUNT_LEAP_SECONDS) + * + * @return array array of year, month, day, hour, minute, second + * @access public + * @static + * @since Method available since Release 1.5.0 + */ + function roundSeconds($pn_precision, + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute, + $pn_second, + $pb_countleap = DATE_COUNT_LEAP_SECONDS) + { + return Date_Calc::round(DATE_PRECISION_SECOND + $pn_precision, + $pn_day, + $pn_month, + $pn_year, + $pn_hour, + $pn_minute, + $pn_second); + } + + + // }}} + +} + +// }}} + + +/* + * Local variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * c-hanging-comment-ender-p: nil + * End: + */ + + +/** + * This file contains the Horde_Date_Recurrence class and according constants. + * + * $Horde: framework/Date/Date/Recurrence.php,v 1.7.2.16 2010-10-14 14:18:05 jan Exp $ + * + * 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 + * @package Horde_Date + */ + +/** Horde_Date */ +// require_once 'Horde/Date.php'; + +/** Date_Calc */ +//require_once 'Date/Calc.php'; + +/** No recurrence. */ +define('HORDE_DATE_RECUR_NONE', 0); +/** Recurs daily. */ +define('HORDE_DATE_RECUR_DAILY', 1); +/** Recurs weekly. */ +define('HORDE_DATE_RECUR_WEEKLY', 2); +/** Recurs monthly on the same date. */ +define('HORDE_DATE_RECUR_MONTHLY_DATE', 3); +/** Recurs monthly on the same week day. */ +define('HORDE_DATE_RECUR_MONTHLY_WEEKDAY', 4); +/** Recurs yearly on the same date. */ +define('HORDE_DATE_RECUR_YEARLY_DATE', 5); +/** Recurs yearly on the same day of the year. */ +define('HORDE_DATE_RECUR_YEARLY_DAY', 6); +/** Recurs yearly on the same week day. */ +define('HORDE_DATE_RECUR_YEARLY_WEEKDAY', 7); + +/** + * 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 + * @since Horde 3.2 + * @package Horde_Date + */ +class Horde_Date_Recurrence { + + /** + * 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. HORDE_DATE_RECUR_* constant. + * + * @var integer + */ + var $recurType = HORDE_DATE_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 Horde_Date_Recurrence($start) + { + $this->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 HORDE_DATE_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 HORDE_DATE_RECUR_* constant. + */ + function setRecurType($recurrence) + { + $this->recurType = $recurrence; + } + + /** + * Returns recurrence type of this event. + * + * @return integer A HORDE_DATE_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 HORDE_DATE_RECUR_NONE: return _("No recurrence"); + case HORDE_DATE_RECUR_DAILY: return _("Daily"); + case HORDE_DATE_RECUR_WEEKLY: return _("Weekly"); + case HORDE_DATE_RECUR_MONTHLY_DATE: + case HORDE_DATE_RECUR_MONTHLY_WEEKDAY: return _("Monthly"); + case HORDE_DATE_RECUR_YEARLY_DATE: + case HORDE_DATE_RECUR_YEARLY_DAY: + case HORDE_DATE_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 = new Horde_Date($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 = new Horde_Date($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 $afterDate 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($afterDate) + { + $after = new Horde_Date($afterDate); + $after->correct(); + + if ($this->start->compareDateTime($after) >= 0) { + return new Horde_Date($this->start); + } + + if ($this->recurInterval == 0) { + return false; + } + + switch ($this->getRecurType()) { + case HORDE_DATE_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 = new Horde_Date($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 new Horde_Date($next); + } + break; + + case HORDE_DATE_RECUR_WEEKLY: + if (empty($this->recurData)) { + return false; + } + + list($start_week->mday, $start_week->month, $start_week->year) = explode('/', Date_Calc::beginOfWeek($this->start->mday, $this->start->month, $this->start->year, '%e/%m/%Y')); + $start_week->hour = $this->start->hour; + $start_week->min = $this->start->min; + $start_week->sec = $this->start->sec; + list($after_week->mday, $after_week->month, $after_week->year) = explode('/', Date_Calc::beginOfWeek($after->mday, $after->month, $after->year, '%e/%m/%Y')); + $after_week_end = new Horde_Date($after_week); + $after_week_end->mday += 7; + $after_week_end->correct(); + + $diff = Date_Calc::dateDiff($start_week->mday, $start_week->month, $start_week->year, + $after_week->mday, $after_week->month, $after_week->year); + $interval = $this->recurInterval * 7; + $repeats = floor($diff / $interval); + if ($diff % $interval < 7) { + $recur = $diff; + } else { + /** + * If the after_week is not in the first week interval the + * search needs to skip ahead a complete interval. The way it is + * calculated here means that an event that occurs every second + * week on Monday and Wednesday with the event actually starting + * on Tuesday or Wednesday will only have one incidence in the + * first week. + */ + $recur = $interval * ($repeats + 1); + } + + if ($this->hasRecurCount()) { + $recurrences = 0; + /** + * Correct the number of recurrences by the number of events + * that lay between the start of the start week and the + * recurrence start. + */ + $next = new Horde_Date($start_week); + while ($next->compareDateTime($this->start) < 0) { + if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) { + $recurrences--; + } + ++$next->mday; + $next->correct(); + } + if ($repeats > 0) { + $weekdays = $this->recurData; + $total_recurrences_per_week = 0; + while ($weekdays > 0) { + if ($weekdays % 2) { + $total_recurrences_per_week++; + } + $weekdays = ($weekdays - ($weekdays % 2)) / 2; + } + $recurrences += $total_recurrences_per_week * $repeats; + } + } + + $next = $start_week; + list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y')); + $next = new Horde_Date($next); + while ($next->compareDateTime($after) < 0 && + $next->compareDateTime($after_week_end) < 0) { + if ($this->hasRecurCount() + && $next->compareDateTime($after) < 0 + && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) { + $recurrences++; + } + ++$next->mday; + $next->correct(); + } + if ($this->hasRecurCount() && + $recurrences >= $this->recurCount) { + return false; + } + 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; + $next->correct(); + } + 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 HORDE_DATE_RECUR_MONTHLY_DATE: + $start = new Horde_Date($this->start); + if ($after->compareDateTime($start) < 0) { + $after = $start; + } + + // 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; + $after->correct(); + } + + // 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; + } + + // Don't correct for day overflow; we just skip February 30th, + // for example. + $start->correct(HORDE_DATE_MASK_MONTH); + + // 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 HORDE_DATE_RECUR_MONTHLY_WEEKDAY: + // Start with the start date of the event. + $estart = new Horde_Date($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; + $estart->correct(); + + $next = new Horde_Date($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 HORDE_DATE_RECUR_YEARLY_DATE: + // Start with the start date of the event. + $estart = new Horde_Date($this->start); + + 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 HORDE_DATE_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 = new Horde_Date($this->start); + $estart->year += floor($count - 1) * $this->recurInterval; + + // Now add the difference to the required day of year. + $estart->mday += $dayofyear - $estart->dayOfYear(); + $estart->correct(); + + // Add an interval if the estimation was wrong. + if ($estart->compareDate($after) < 0) { + $estart->year += $this->recurInterval; + $estart->mday += $dayofyear - $estart->dayOfYear(); + $estart->correct(); + } + + // We've gone past the end of recurrence; give up. + if ($this->hasRecurEnd() && + $this->recurEnd->compareDateTime($estart) < 0) { + return false; + } + + return $estart; + + case HORDE_DATE_RECUR_YEARLY_WEEKDAY: + // Start with the start date of the event. + $estart = new Horde_Date($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; + $estart->correct(); + + $next = new Horde_Date($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(HORDE_DATE_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(HORDE_DATE_RECUR_DAILY); + break; + + case 'W': + $this->setRecurType(HORDE_DATE_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_SUNDAY => HORDE_DATE_MASK_SUNDAY, + HORDE_DATE_MONDAY => HORDE_DATE_MASK_MONDAY, + HORDE_DATE_TUESDAY => HORDE_DATE_MASK_TUESDAY, + HORDE_DATE_WEDNESDAY => HORDE_DATE_MASK_WEDNESDAY, + HORDE_DATE_THURSDAY => HORDE_DATE_MASK_THURSDAY, + HORDE_DATE_FRIDAY => HORDE_DATE_MASK_FRIDAY, + HORDE_DATE_SATURDAY => HORDE_DATE_MASK_SATURDAY); + $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]); + } + break; + + case 'MP': + $this->setRecurType(HORDE_DATE_RECUR_MONTHLY_WEEKDAY); + break; + + case 'MD': + $this->setRecurType(HORDE_DATE_RECUR_MONTHLY_DATE); + break; + + case 'YM': + $this->setRecurType(HORDE_DATE_RECUR_YEARLY_DATE); + break; + + case 'YD': + $this->setRecurType(HORDE_DATE_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, '#') === 0) { + $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 HORDE_DATE_RECUR_NONE: + return ''; + + case HORDE_DATE_RECUR_DAILY: + $rrule = 'D' . $this->recurInterval; + break; + + case HORDE_DATE_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 HORDE_DATE_RECUR_MONTHLY_DATE: + $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday); + break; + + case HORDE_DATE_RECUR_MONTHLY_WEEKDAY: + $nth_weekday = (int)($this->start->mday / 7); + if (($this->start->mday % 7) > 0) { + $nth_weekday++; + } + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()]; + break; + + case HORDE_DATE_RECUR_YEARLY_DATE: + $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month); + break; + + case HORDE_DATE_RECUR_YEARLY_DAY: + $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear(); + break; + + default: + return ''; + } + + if ($this->hasRecurEnd()) { + $recurEnd = new Horde_Date($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[strtoupper($key)] = $value; + } + + if (isset($rdata['FREQ'])) { + // Always default the recurInterval to 1. + $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1); + + switch (strtoupper($rdata['FREQ'])) { + case 'DAILY': + $this->setRecurType(HORDE_DATE_RECUR_DAILY); + break; + + case 'WEEKLY': + $this->setRecurType(HORDE_DATE_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_SUNDAY => HORDE_DATE_MASK_SUNDAY, + HORDE_DATE_MONDAY => HORDE_DATE_MASK_MONDAY, + HORDE_DATE_TUESDAY => HORDE_DATE_MASK_TUESDAY, + HORDE_DATE_WEDNESDAY => HORDE_DATE_MASK_WEDNESDAY, + HORDE_DATE_THURSDAY => HORDE_DATE_MASK_THURSDAY, + HORDE_DATE_FRIDAY => HORDE_DATE_MASK_FRIDAY, + HORDE_DATE_SATURDAY => HORDE_DATE_MASK_SATURDAY); + $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]); + } + break; + + case 'MONTHLY': + if (isset($rdata['BYDAY'])) { + $this->setRecurType(HORDE_DATE_RECUR_MONTHLY_WEEKDAY); + } else { + $this->setRecurType(HORDE_DATE_RECUR_MONTHLY_DATE); + } + break; + + case 'YEARLY': + if (isset($rdata['BYYEARDAY'])) { + $this->setRecurType(HORDE_DATE_RECUR_YEARLY_DAY); + } elseif (isset($rdata['BYDAY'])) { + $this->setRecurType(HORDE_DATE_RECUR_YEARLY_WEEKDAY); + } else { + $this->setRecurType(HORDE_DATE_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(HORDE_DATE_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 HORDE_DATE_RECUR_NONE: + return ''; + + case HORDE_DATE_RECUR_DAILY: + $rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval; + break; + + case HORDE_DATE_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 HORDE_DATE_RECUR_MONTHLY_DATE: + $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval; + break; + + case HORDE_DATE_RECUR_MONTHLY_WEEKDAY: + $nth_weekday = (int)($this->start->mday / 7); + if (($this->start->mday % 7) > 0) { + $nth_weekday++; + } + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval + . ';BYDAY=' . $nth_weekday . $vcaldays[$this->start->dayOfWeek()]; + break; + + case HORDE_DATE_RECUR_YEARLY_DATE: + $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval; + break; + + case HORDE_DATE_RECUR_YEARLY_DAY: + $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval + . ';BYYEARDAY=' . $this->start->dayOfYear(); + break; + + case HORDE_DATE_RECUR_YEARLY_WEEKDAY: + $nth_weekday = (int)($this->start->mday / 7); + if (($this->start->mday % 7) > 0) { + $nth_weekday++; + } + $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'); + $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval + . ';BYDAY=' + . $nth_weekday + . $vcaldays[$this->start->dayOfWeek()] + . ';BYMONTH=' . $this->start->month; + break; + } + + if ($this->hasRecurEnd()) { + $recurEnd = new Horde_Date($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(HORDE_DATE_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(HORDE_DATE_RECUR_DAILY); + break; + + case 'weekly': + $this->setRecurType(HORDE_DATE_RECUR_WEEKLY); + $parse_day = true; + $set_daymask = true; + break; + + case 'monthly': + if (!isset($hash['daynumber'])) { + $this->setRecurType(HORDE_DATE_RECUR_NONE); + return false; + } + + switch ($hash['type']) { + case 'daynumber': + $this->setRecurType(HORDE_DATE_RECUR_MONTHLY_DATE); + $update_daynumber = true; + break; + + case 'weekday': + $this->setRecurType(HORDE_DATE_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(HORDE_DATE_RECUR_NONE); + return false; + } + + switch ($hash['type']) { + case 'monthday': + $this->setRecurType(HORDE_DATE_RECUR_YEARLY_DATE); + $update_month = true; + $update_daynumber = true; + break; + + case 'yearday': + if (!isset($hash['month'])) { + $this->setRecurType(HORDE_DATE_RECUR_NONE); + return false; + } + + $this->setRecurType(HORDE_DATE_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(HORDE_DATE_RECUR_NONE); + return false; + } + + $this->setRecurType(HORDE_DATE_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(HORDE_DATE_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 ? + $last_found_day = -1; + if ($parse_day) { + if (!isset($hash['day'])) { + $this->setRecurType(HORDE_DATE_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_MONDAY, + 'tuesday' => HORDE_DATE_TUESDAY, + 'wednesday' => HORDE_DATE_WEDNESDAY, + 'thursday' => HORDE_DATE_THURSDAY, + 'friday' => HORDE_DATE_FRIDAY, + 'saturday' => HORDE_DATE_SATURDAY, + 'sunday' => HORDE_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(HORDE_DATE_RECUR_NONE); + return false; + } + + $this->start->mday = $hash['daynumber']; + } + + if ($update_weekday) { + $this->start->setNthWeekday($last_found_day, $nth_weekday); + } + + $this->start->correct(); + } + + // 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() == HORDE_DATE_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 HORDE_DATE_RECUR_DAILY: + $hash['cycle'] = 'daily'; + break; + + case HORDE_DATE_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 HORDE_DATE_RECUR_MONTHLY_DATE: + $hash['cycle'] = 'monthly'; + $hash['type'] = 'daynumber'; + $hash['daynumber'] = $start->mday; + break; + + case HORDE_DATE_RECUR_MONTHLY_WEEKDAY: + $hash['cycle'] = 'monthly'; + $hash['type'] = 'weekday'; + $hash['daynumber'] = $start->weekOfMonth(); + $hash['day'] = array ($day2number[$start->dayOfWeek()]); + break; + + case HORDE_DATE_RECUR_YEARLY_DATE: + $hash['cycle'] = 'yearly'; + $hash['type'] = 'monthday'; + $hash['daynumber'] = $start->mday; + $hash['month'] = $month2number[$start->month]; + break; + + case HORDE_DATE_RECUR_YEARLY_DAY: + $hash['cycle'] = 'yearly'; + $hash['type'] = 'yearday'; + $hash['daynumber'] = $start->dayOfYear(); + break; + + case HORDE_DATE_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; + } + +} diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index c135954c..54bfe5b0 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -91,6 +91,7 @@ $labels['errorsaving'] = "Failed to save changes"; $labels['operationfailed'] = "The requested operation failed"; // recurrence form +$labels['repeat'] = 'Repeat'; $labels['frequency'] = 'Repeat'; $labels['never'] = 'never'; $labels['daily'] = 'daily';