Location: PHPKode > projects > Sierra-php PHP Application Framework > sierra/lib/util/SRA_GregorianDate.php
<?php
// {{{ Header
/*
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
 | SIERRA : PHP Application Framework  http://code.google.com/p/sierra-php |
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
 | Copyright 2005 Jason Read                                               |
 |                                                                         |
 | Licensed under the Apache License, Version 2.0 (the "License");         |
 | you may not use this file except in compliance with the License.        |
 | You may obtain a copy of the License at                                 |
 |                                                                         |
 |     http://www.apache.org/licenses/LICENSE-2.0                          |
 |                                                                         |
 | Unless required by applicable law or agreed to in writing, software     |
 | distributed under the License is distributed on an "AS IS" BASIS,       |
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.|
 | See the License for the specific language governing permissions and     |
 | limitations under the License.                                          |
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
 */
// }}}

// {{{ Imports
require_once('util/SRA_TimeZone.php');
// }}}

// {{{ Constants
/**
 * the format string for an IS0 8601 date string 
 * (example: 2004-02-12T15:19:21+00:00)
 * @type string
 */
define('SRA_GREGORIAN_DATE_FORMAT_ISO8601', 'Y-m-dTH:i:sP');

/**
 * the format string for an RFC 2822 date string 
 * (example: Thu, 21 Dec 2000 16:01:07 +0200)
 * @type string
 */
define('SRA_GREGORIAN_DATE_FORMAT_RFC2822', 'D, j M Y H:i:s O');

/**
 * the number of hours to limit relative time usage for in the "format" method 
 * when the format string is "RT"
 * @type int
 */
define('SRA_GREGORIAN_DATE_RELATIVE_TIME_CAP', 6);

/**
 * used by the jump method to identify a jump unit of 1 day
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_DAY', 86400);

/**
 * used by the jump method to identify a jump unit of 1 minute
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_MINUTE', 60);

/**
 * used by the jump method to identify a jump unit of 1 hour
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_HOUR', 3600);

/**
 * used by the jump method to identify a jump unit of 1 second
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_SECOND', 1);

/**
 * used by the jump method to identify a jump unit of 1 week
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_WEEK', 604800);

/**
 * used by the jump method to identify a jump unit of 1 month
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_MONTH', -1);

/**
 * used by the jump method to identify a jump unit of 1 year
 * @type int
 */
define('SRA_GREGORIAN_DATE_UNIT_YEAR', -2);
// }}}

// {{{ SRA_GregorianDate
/**
 * this class is used to manage gregorian based dates and times without the use 
 * of the PHP date and time functions (allowing it to support dates prior to the 
 * Unix Epoch (January 1 1970 00:00:00 GMT)). it may be used to represent both 
 * a date and time or just a date (time is irrelevant). this object can be 
 * stored in a database as an 8 character string (when 'dateOnly' is TRUE) or a
 * 14 character string (when 'dateOnly' is FALSE) - for more info, see the 
 * 'encode' method below
 * @author  Jason Read <hide@address.com>
 * @package sierra.util
 */
class SRA_GregorianDate {
  // public attributes
  /**
   * an alternate default date format for 'toString'. if not specified, the 
   * default app-config (or sierra-config) date/date-only format will be used
   * @type string
   */
  var $toStringFormat;
  
  
  // private attributes
  /**
   * whether or not this object represents just a date (time is irrelevant)
   * @type boolean
   */
  var $_dateOnly = TRUE;
  
  /**
   * the date month day (1-31)
   * @type int
   */
  var $_day;
  
  /**
   * this attribute defines whether or not this date/time occurs during the dst 
   * rollback overlap period (i.e. the second 1:00 hour on 11/2/2008)
   * @type boolean
   */
  var $_dstOverlap;
  
  /**
   * the date hour (i.e. 0-23)
   * @type int
   */
  var $_hour;
  
  /**
   * the date minute (i.e. 0-59)
   * @type int
   */
  var $_minute;
  
  /**
   * the date month (1-12)
   * @type int
   */
  var $_month;
  
  /**
   * the date second (i.e. 0-59)
   * @type int
   */
  var $_second;
  
  /**
   * the time zone used by this object (applicable only when time is relevant - 
   * 'dateOnly' is FALSE)
   * @type SRA_TimeZone
   */
  var $_tz;
  
  /**
   * the date year (i.e. 2001)
   * @type int
   */
  var $_year;
  
  
  // {{{ SRA_GregorianDate
  /**
   * instantiates a new gregorian date object based on the parameters specified. 
   * if NO parameters are specified, the object will be created for the current 
   * system date/time and app (or system) time zone. if ONLY $year, $month and 
   * $day parameters are specified, it will be assumed that the time is not 
   * relevant ('dateOnly' will be set to TRUE)
   * @param mixed $year the year (YYYY or YY - if the latter format YY is used, 
   * 69-99 will be assumed to be 1969-1999 and 00-68 will be assumed to be 
   * 2000-2068), OR the full gregorian encoded date string (as retrieved from 
   * 'encode' - YYYYMMDDHHMMSS (HHMMSS are optional)). when this parameter is an 
   * encoded date string (strlen == 8 or 14), the other parameters will be 
   * ignored OR textual date/time description where this description adheres to 
   * the formats described for calendar date items here: 
   * http://www.gnu.org/software/tar/manual/html_node/tar_111.html#SEC111 
   * and for time of day items here: 
   * http://www.gnu.org/software/tar/manual/html_node/tar_112.html#SEC112
   * and for time zone items here:
   * http://www.gnu.org/software/tar/manual/html_node/tar_113.html#SEC113
   * @param mixed $month the month (1-12) OR the time zone for this new date
   * @param int $day the day (1-31)
   * @param int $hour the hour (0-23)
   * @param int $minute the minute (0-59)
   * @param int $second the second (0-59)
   * @param SRA_TimeZone $tz optional time zone to use for this date object. 
   * applies only when 'dateOnly' is FALSE. if not specified, the current app 
   * (or system time zone will be used)
   * @access public
   */
  function SRA_GregorianDate($year=NULL, $month=NULL, $day=NULL, $hour=NULL, $minute=NULL, $second=NULL, $tz=NULL) {
    $this->_init = TRUE;
    
    // time zone parameter was passed in $month place
    if ($month && SRA_TimeZone::isValid($month)) { $tz = $month; }
    
    // $year is a unix timestamp
    if (is_numeric($year) && (!(strlen($year) == 2 || strlen($year) == 4 || strlen($year) == 14 || strlen($year) == 8) || (!SRA_Util::beginsWith($year, '19') && !SRA_Util::beginsWith($year, '20')))) {
      $timestamp = $year;
      $year = date('Y', $timestamp);
      $month = date('n', $timestamp);
      $day = date('j', $timestamp);
      $hour = date('G', $timestamp);
      $minute = date('i', $timestamp);
      $second = date('s', $timestamp);
    }
    
    // convert encoded date string parameter
    if (isset($year) && is_numeric($year) && (strlen($year) == 14 || strlen($year) == 8)) {
      $month = substr($year, 4, 2);
      $day = substr($year, 6, 2);
      if (strlen($year) == 14) {
        $hour = substr($year, 8, 2);
        $minute = substr($year, 10, 2);
        $second = substr($year, 12, 2);
      }
      $year = substr($year, 0, 4);
    }
    // convert date/time string
    else if (isset($year) && !is_numeric($year)) {
      $dateStr = $year;
      
      // ISO 8601
      if (preg_match('/[0-9]{2}T[0-9]{2}/', $dateStr, $m)) {
        $dateStr = str_replace($m[0], str_replace('T', ' ', $m[0]), $dateStr);
      }
      
      $year = NULL;
      
      // DATE/TIME
      // MM.DD.(YY|YYYY).HH.MM.SS
      if (preg_match('/^([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{2,4})\.([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{1,2})$/', trim($dateStr), $matches)) {
        $month = $matches[1];
        $day = $matches[2];
        $year = $matches[3];
        $hour = $matches[4];
        $minute = $matches[5];
        $second = $matches[6];
        $dateStr = '';
      }
      
      // TIME
      // ISO 8601 (HH:MM:SS or HH:MM or HH)
      if (preg_match('/([0-9]{1,2}):([0-9]{1,2})?:?([0-9]{1,2})? ?([a|A|p|P])?.*/', $dateStr, $matches)) {
        $match = $matches[0];
        $hour = $matches[4] && strtolower($matches[4]) == 'a' && $matches[1] == 12 ? 0 : ($matches[4] && strtolower($matches[4]) == 'p' && $matches[1] < 12 ? $matches[1] + 12 : $matches[1]);
        if ($matches[2]) { $minute = $matches[2]; }
        if ($matches[3]) { $second = $matches[3]; }
        // YEAR
        if (preg_match('/ ([1-2][0-9]{3})/', $dateStr, $matches)) {
          $year = $matches[1];
        }
        // TIMEZONE
        if (preg_match('/:.*([a-z|A-Z|\/]+)?([+|-])([0-9]{2}):?([0-9]{2})? *([a-z|A-Z]{3})?.*/', $dateStr, $matches)) {
          if ($tz || !$matches[1] || !SRA_TimeZone::isValid($tz =& SRA_TimeZone::getTimeZone($matches[1]))) {
            $offset = ($matches[2] . (($matches[3]*60) + (isset($matches[4]) ? $matches[4]*1 : 0))) * 1;
            if (!SRA_TimeZone::isValid($tz)) { $tz =& SRA_Controller::getAppTimeZone(); }
          }
        }
        $dateStr = str_replace($match, '', $dateStr);
      }
      
      // DATE
      // 1972 sep 24
      if (preg_match('/([0-9]{4}) *([a-zA-Z\.]+) *([0-9]{1,2})?.*/', $dateStr, $matches) && ($matches[2] = SRA_GregorianDate::getMonthFromStr($matches[2]))) {
        $day = $matches[3];
        $month = $matches[2];
        $year = $matches[1] ? $matches[1] : (is_numeric($year) ? $year : NULL);
      }
      // 24 sep 1972 or 24-sept-72 or 24sep97
      else if (preg_match('/([0-9]{1,2})-? *([a-zA-Z\.]+)-? *([0-9]{1,4})?.*/', $dateStr, $matches) && ($matches[2] = SRA_GregorianDate::getMonthFromStr($matches[2]))) {
        $day = $matches[1];
        $month = $matches[2];
        $year = $matches[3] ? $matches[3] : (is_numeric($year) ? $year : NULL);
      }
      // sep 24, 1997 or september 24
      else if (preg_match('/([a-zA-Z\.]+)-? *([0-9]{1,2}),?-? *([0-9]{1,4})?.*/', $dateStr, $matches) && ($matches[1] = SRA_GregorianDate::getMonthFromStr($matches[1]))) {
        $month = $matches[1];
        $day = $matches[2];
        $year = $matches[3] ? $matches[3] : (is_numeric($year) ? $year : NULL);
      }
      // ISO 8601 (YYYY-MM-DD or YY-MM-DD)
      else if (preg_match('/([0-9]{1,4})?-?([0-9]{1,2})-([0-9]{1,2})*/', $dateStr, $matches)) {
        $year = $matches[1] ? $matches[1] : (is_numeric($year) ? $year : NULL);
        $month = $matches[2];
        $day = $matches[3];
      }
      // Common U.S. format: MM/DD/YY or MM/DD/YYYY
      else if (preg_match('/([0-9]{1,2})\/([0-9]{1,2})\/?([0-9]{1,4})?.*/', $dateStr, $matches)) {
        $month = $matches[1];
        $day = $matches[2];
        $year = $matches[3] ? $matches[3] : (is_numeric($year) ? $year : NULL);
      }
    }
    
    // prefix 2-character years
    $year = isset($year) && strlen($year) == 2 && is_numeric($year) ? ($year >= 69 && $year <= 99 ? '19' . $year : '20' . $year) : $year; 
    
    $noParams = !isset($year) && !isset($month) && !isset($day) && !isset($hour) && !isset($minute) && !isset($second);
    
    // date/time
    if ($noParams || isset($hour) || isset($minute) || isset($second)) {
      $this->_dateOnly = FALSE;
    }
    
    // set timezone
    if (!$this->_dateOnly) {
      if (SRA_TimeZone::isValid($tz)) {
        $this->_tz =& $tz;
      }
      else {
        $this->_tz =& SRA_Controller::getAppTimeZone();
      }
    }
    
    // create based on current date/time
    if ($noParams) {
      $this->setYear(date('Y'));
      $this->setMonth(date('n'));
      $this->setDay(date('j'));
      $this->setHour(date('G'));
      $this->setMinute(date('i'));
      $this->setSecond(date('s'));
    }
    // create based on parameters
    else {
      $this->setYear($year && is_numeric($year) ? $year : date('Y'));
      $this->setMonth($month && is_numeric($month) ? $month : date('n'));
      $this->setDay($day && is_numeric($day) ? $day : date('j'));
      if (isset($hour) || isset($minute) || isset($second)) {
        $this->setHour($hour && is_numeric($hour) ? $hour : 0);
        $this->setMinute($minute && is_numeric($minute) ? $minute : 0);
        $this->setSecond($second && is_numeric($second) ? $second : 0);
      }
    }
    
    // timezone offset
    if (isset($offset)) {
      // jump (when timezone offset specified, but timezone could not be found)
      if ($jumpMinutes = $tz->getGmtOffset($this) - $offset) {
        $this->jump(SRA_GREGORIAN_DATE_UNIT_MINUTE, $jumpMinutes);
      }
    }
    
    $this->_init = FALSE;
  }
  // }}}
  
  
  
  // {{{ compare
  /**
   * compares 2 SRA_GregorianDate instances. returns 0 if they represent the 
   * exact same date/time, -1 if $date is before this date, and +1 if $date is 
   * after this date. time zone is also considered in the evaluation. if either 
   * date is a 'dateOnly', the time will be ignored in the comparison for both
   * @param SRA_GregorianDate $date the date to compare with
   * @param boolean $dateOnly if both this and $date are non-dateOnly dates, 
   * this parameter can be used to compare the dates only (ignore times)
   * @access public
   * @return int
   */
  function compare(&$date, $dateOnly=FALSE) {
    if (is_scalar($date)) { $date = new SRA_GregorianDate($date); }
    
    return SRA_GregorianDate::isValid($date) ? $this->toInt() == $date->toInt() ? ($dateOnly || $this->isDateOnly() || $date->isDateOnly() || $this->toIntTime() == $date->toIntTime() ? 0 : ($this->toIntTime() < $date->toIntTime() ? -1 : 1)) : ($this->toInt() < $date->toInt() ? -1 : 1) : FALSE;
  }
  // }}}
  
  // {{{ copy
  /**
   * returns a new instance of SRA_GregorianDate with the same date as this 
   * instance
   * @access public
   * @return SRA_GregorianDate
   */
  function &copy() {
    return new SRA_GregorianDate($this->_year, $this->_month, $this->_day, $this->_hour, $this->_minute, $this->_second, $this->_tz);
  }
  // }}}
  
  // {{{ cron
  /**
   * used to determine whether or not a cron formatted schedule string is valid 
   * for the current time (or for the SRA_GregorianDate instance when invoked as 
   * an instance method). if $schedule is not a valid formatted cron schedule 
   * string, this method will return NULL. otherwise, TRUE or FALSE will be 
   * returned (use the === operator to evaluate the results)
   * @param string $schedule the cron-formatted schedule string. this string 
   * is a space separated list of time identifiers in the following order:
   *   1: the schedule minute of the hour (between 0 and 59)
   *   2: the schedule hour (0 and 23 where 0 is midnight)
   *   3: the schedule day of month
   *   4: the schedule month (1-12)
   *   5: the schedule day of week (0-6 where 0=sunday)
   * '*' in any of these identifiers means that the schedule should occur in 
   * all instances of that type of identifier. additionally, multiple comma 
   * separated values may be specified for each identifier. for more 
   * information, there are a variety of resources available on the web 
   * regarding "cron scheduling"
   * @access public
   * @return boolean
   */
  function cron($schedule) {
    $do = FALSE;
    $dt = SRA_GregorianDate::isValid($this) && !$this->isDateOnly() ? $this : new SRA_GregorianDate();
    $schedule = explode(' ', $schedule);
    if (SRA_GregorianDate::isValid($dt) && $schedule && count($schedule) == 5) {
      $scheduleParts['minute'] = explode(',', $schedule[0]);
      $scheduleParts['hour'] = explode(',', $schedule[1]);
      $scheduleParts['dom'] = explode(',', $schedule[2]);
      $scheduleParts['month'] = explode(',', $schedule[3]);
      $scheduleParts['dow'] = explode(',', $schedule[4]);
      $keys = array_keys($scheduleParts['minute']);
      foreach($keys as $key) {
        if ($scheduleParts['minute'][$key] != '*' && (!is_numeric($scheduleParts['minute'][$key]) || ($scheduleParts['minute'][$key] < 0 || $scheduleParts['minute'][$key] > 59))) {
          $do = NULL;
        }
        else if ($scheduleParts['minute'][$key] == '*') {
          $scheduleParts['minute'] = '*';
          break;
        }
      }
      $keys = array_keys($scheduleParts['hour']);
      foreach($keys as $key) {
        if ($scheduleParts['hour'][$key] != '*' && (!is_numeric($scheduleParts['hour'][$key]) || ($scheduleParts['hour'][$key] < 0 || $scheduleParts['hour'][$key] > 23))) {
          $do = NULL;
        }
        else if ($scheduleParts['hour'][$key] == '*') {
          $scheduleParts['hour'] = '*';
          break;
        }
      }
      $keys = array_keys($scheduleParts['dom']);
      foreach($keys as $key) {
        if ($scheduleParts['dom'][$key] != '*' && (!is_numeric($scheduleParts['dom'][$key]) || ($scheduleParts['dom'][$key] < 1 || $scheduleParts['dom'][$key] > 31))) {
          $do = NULL;
        }
        else if ($scheduleParts['dom'][$key] == '*') {
          $scheduleParts['dom'] = '*';
          break;
        }
      }
      $keys = array_keys($scheduleParts['month']);
      foreach($keys as $key) {
        if ($scheduleParts['month'][$key] != '*' && (!is_numeric($scheduleParts['month'][$key]) || ($scheduleParts['month'][$key] < 1 || $scheduleParts['month'][$key] > 12))) {
          $do = NULL;
        }
        else if ($scheduleParts['month'][$key] == '*') {
          $scheduleParts['month'] = '*';
          break;
        }
      }
      $keys = array_keys($scheduleParts['dow']);
      foreach($keys as $key) {
        if ($scheduleParts['dow'][$key] != '*' && (!is_numeric($scheduleParts['dow'][$key]) || ($scheduleParts['dow'][$key] < 0 || $scheduleParts['dow'][$key] > 6))) {
          $do = NULL;
        }
        else if ($scheduleParts['dow'][$key] == '*') {
          $scheduleParts['dow'] = '*';
          break;
        }
      }
      $do = $do !== NULL ? ($scheduleParts['dow'] == '*' || in_array($dt->getDayOfWeek(), $scheduleParts['dow'])) && ($scheduleParts['month'] == '*' || in_array($dt->getMonth(), $scheduleParts['month']) || in_array($dt->getMonth(TRUE), $scheduleParts['month'])) && ($scheduleParts['dom'] == '*' || in_array($dt->getDay(), $scheduleParts['dom']) || in_array($dt->getDay(TRUE), $scheduleParts['dom'])) && ($scheduleParts['hour'] == '*' || in_array($dt->getHour(), $scheduleParts['hour']) || in_array($dt->getHour(TRUE), $scheduleParts['hour'])) && ($scheduleParts['minute'] == '*' || in_array($dt->getMinute(), $scheduleParts['minute']) || in_array($dt->getMinute(TRUE), $scheduleParts['minute'])) : $do;
    }
    return $do;
  }
  // }}}
  
  // {{{ encode
  /**
   * returns this gregorian date object as an encoded date string in the format 
   * YYYYMMDDHHMMSS (HHMMSS are only applicable when 'dateOnly' is FALSE). thus
   * the return value will be 8 characters in length when dateOnly is TRUE, 14
   * characters in length otherwise
   * @access public
   * @return string
   */
  function encode() {
    return $this->format($this->isDateOnly() ? 'Ymd' : 'YmdHis');
  }
  // }}}
  
  // {{{ equals
  /**
   * returns TRUE if this data is equal to $date. in order to be equal, the 
   * 'format' string must be equal and the timezone must be equal (for 
   * non-date only dates only)
   * @param mixed $date the date to compare with
   * @access public
   * @return boolean
   */
  function equals(& $date) {
    return SRA_GregorianDate::isValid($date) && $this->encode() == $date->encode() && ($this->isDateOnly() || (($tz =& $this->getTimeZone()) && $tz->equals($date->getTimeZone())));
  }
  // }}}
  
  // {{{ format
  /**
   * used to convert this gregorian date object to a formatted string based on 
   * the $format string provided. if this method is called statically, a new 
   * the formatted string will be for the current time
   * @param string $format the format string to use. if not specified, 
   * SRA_Controller::getAppDateOnlyFormat() will be used (see documentation in 
   * sierra/etc/app-config*.dtd for the "date-only-format" attribute)
   * 
   * The following characters are allowed in the $format parameter string (break 
   * any of these characters by preceding it with \):
   * 
   *  DAY FORMAT CHARACTERS
   *    d  day of the month, 2 digits with leading zeros: 01-31
   *    D  a textual representation of a day, three letters: Mon through Sun
   *    j  day of the month without leading zeros: 1-31
   *    l  a full textual representation of the day of the week: Sunday - Saturday
   *    L  lowercase full textual representation of the day of the week: sunday - saturday
   *    N  ISO-8601 numeric representation of the day of the week: 1 (for Monday) through 7 (for Sunday)
   *    S  english ordinal suffix for the day of the month: st, nd, rd or th. works well with j
   *    w  numeric representation of the day of the week: 0 (for Sunday) through 6 (for Saturday)
   *    z  the day of the year (starting from 0): 0 through 365
   *
   *  WEEK FORMAT CHARACTERS
   *    W  ISO-8601 week number of year, weeks starting on monday: example: 42 (the 42nd week in the year)
   *
   *  MONTH FORMAT CHARACTERS
   *    F  a full textual representation of a month, such as January or March: January through December
   *    m  numeric representation of a month, with leading zeros: 01 through 12
   *    M  a short textual representation of a month, three letters: Jan through Dec
   *    n  numeric representation of a month, without leading zeros: 1 through 12
   *    t  number of days in the given month: 28 through 31
   *
   *  YEAR FORMAT CHARACTERS
   *    L  whether it's a leap year: 1 if it is a leap year, 0 otherwise
   *    o  ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead: examples: 1999 or 2003
   *    Y  a full numeric representation of a year, 4 digits: Examples: 1999 or 2003
   *    y  a two digit representation of a year: Examples: 99 or 03
   *
   * The format characters below are ONLY applicable for non-dateOnly dates. if 
   * used otherwise, they will be replaced with empty string
   *  TIME FORMAT CHARACTERS
   *    a  lowercase Ante meridiem and Post meridiem: am or pm
   *    A  uppercase Ante meridiem and Post meridiem: AM or PM
   *    g  12-hour format of an hour without leading zeros: 1 through 12
   *    G  24-hour format of an hour without leading zeros: 0 through 23
   *    h  12-hour format of an hour with leading zeros: 01 through 12
   *    H  24-hour format of an hour with leading zeros: 00 through 23
   *    i  minutes with leading zeros: 00 to 59
   *    s  seconds with leading zeros: 00 to 59
   * 
   *  TIMEZONE FORMAT CHARACTERS
   *    e  timezone identifier: i.e. America/Boise, GMT, etc.
   *    I  whether or not the date is in daylight saving time: 1 if daylight saving time, 0 otherwise
   *    O  difference to Greenwich time (GMT) in hours: example: +0200
   *    P  difference to Greenwich time (GMT) with colon between hours and minutes: example: +02:00
   *    T  timezone abbreviation: examples: EST, MDT
   *    Z  timezone offset in seconds. the offset for timezones west of GMT is always negative, and for those east of GMT is always positive: -43200 through 50400
   *
   *  FULL DATE/TIME
   *    c  ISO 8601 date: 2004-02-12T15:19:21+00:00 (basically a shortcut to format string SRA_GREGORIAN_DATE_FORMAT_ISO8601)
   *    r  RFC 2822 formatted date: example: Thu, 21 Dec 2000 16:01:07 +0200 (basically a shortcut to format string SRA_GREGORIAN_DATE_FORMAT_RFC2822)
   *
   *  RELATIVE DATE STRINGS
   *    when a relative token is used and a relative value is not possible, the 
   *    application date-format or date-only-format wil be used
   *    R  relative date string such as "Today", "Tommorow", "Yesterday", 
   *       "3 days ago" (up to 6 days max), "1 week ago", "In 2 weeks", 
   *       "1 month ago", "In 2 months", "In 1 year", "1 year ago", etc. if no 
   *       valid relative strings are possible, this token will be replaced with 
   *       the date string using the app or sys default format. when a relative 
   *       date string is used, any other date tokens will be ignored
   *    R1 relative date string - days only
   *    RT relative time string - same as R, but includes relative times such as 
   *       "About 5 minutes ago", "About 2 hours ago", "In about 3 hours", 
   *       "30 seconds ago", etc. relative time strings are used for up to +/- 6
   *       SRA_GREGORIAN_DATE_RELATIVE_TIME_CAP hours. when a relative time 
   *       string is used, any other time tokens will be ignored. if not, a 
   *       relative date string will be used
   * @param boolean $ignoreDateTokens whether or not to ignore date tokens in 
   * $format
   * @param boolean $ignoreTimeTokens whether or not to ignore time tokens in 
   * $format
   * @param boolean $ignoreRelativeTokens whether or not to ignore relative 
   * date/time tokens in $format
   * @access public
   * @return string
   */
  function format($format=NULL, $ignoreDateTokens=FALSE, $ignoreTimeTokens=FALSE, $ignoreRelativeTokens=FALSE) {
    if ($this && SRA_GregorianDate::isValid($this)) {
      $dt =& $this;
    }
    else {
      $dt = new SRA_GregorianDate();
    }
    
    $formatted = '';
    $dow = $dt->getDayOfWeek();
    $format = $format ? $format : ($dt->isDateOnly() ? SRA_Controller::getAppDateOnlyFormat() : SRA_Controller::getAppDateFormat());
    $resources =& SRA_Controller::getSysResources();
    for($i=0; $i<strlen($format); $i++) {
      $str = substr($format, $i, 1);
      $str1 = substr($format, $i+1, 1);
      
      if ($str == 'R' && ($str1 == '1' || $str1 == 'T')) {
        $str .= $str1;
        $i++;
      }
      
      // break character
      if ($str == '\\') {
        $i++;
        $str = substr($format, $i, 1);
      }
      
      // day
      else if ($str === 'd') { $str = $ignoreDateTokens ? '' : $dt->getDay(TRUE); }
      else if ($str === 'D') { $str = $ignoreDateTokens ? '' : $resources->getString('date.day.abbr.' . $dow); }
      else if ($str === 'j') { $str = $ignoreDateTokens ? '' : $dt->getDay(); }
      else if ($str === 'l') { $str = $ignoreDateTokens ? '' : $resources->getString('date.day.' . $dow); }
      else if ($str === 'L') { $str = $ignoreDateTokens ? '' : strtolower($resources->getString('date.day.' . $dow)); }
      else if ($str === 'N') { $str = $ignoreDateTokens ? '' : $dt->getISO8601DayOfWeek(); }
      else if ($str === 'S') { $str = $ignoreDateTokens ? '' : SRA_Util::getEnglishOrdinalSuffix($dt->getDay()); }
      else if ($str === 'w') { $str = $ignoreDateTokens ? '' : $dow; }
      else if ($str === 'z') { $str = $ignoreDateTokens ? '' : $dt->getDayOfYear(); }
      // week
      else if ($str === 'W') { $str = $ignoreDateTokens ? '' : $dt->getISO8601WeekOfYear(); }
      // month
      else if ($str === 'F') { $str = $ignoreDateTokens ? '' : $resources->getString('date.month.' . $dt->getMonth()); }
      else if ($str === 'm') { $str = $ignoreDateTokens ? '' : $dt->getMonth(TRUE); }
      else if ($str === 'M') { $str = $ignoreDateTokens ? '' : $resources->getString('date.month.abbr.' . $dt->getMonth()); }
      else if ($str === 'n') { $str = $ignoreDateTokens ? '' : $dt->getMonth(); }
      else if ($str === 't') { $str = $ignoreDateTokens ? '' : $dt->getNumDaysInMonth(); }
      // year
      else if ($str === 'L') { $str = $ignoreDateTokens ? '' : $dt->isLeapYear() ? '1' : '0'; }
      else if ($str === 'o') { $str = $ignoreDateTokens ? '' : $dt->getISO8601Year(); }
      else if ($str === 'Y') { $str = $ignoreDateTokens ? '' : $dt->getYear(); }
      else if ($str === 'y') { $str = $ignoreDateTokens ? '' : substr($dt->getYear(), 2, 2); }
      // time
      else if ($str === 'a') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : strtolower($resources->getString($dt->getHour()>11 ? 'date.meridiem.pm' : 'date.meridiem.am')); }
      else if ($str === 'A') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $resources->getString($dt->getHour()>11 ? 'date.meridiem.pm' : 'date.meridiem.am'); }
      else if ($str === 'g') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->getHour12(); }
      else if ($str === 'G') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->getHour(); }
      else if ($str === 'h') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->getHour12(TRUE); }
      else if ($str === 'H') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->getHour(TRUE); }
      else if ($str === 'i') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->getMinute(TRUE); }
      else if ($str === 's') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->getSecond(TRUE); }
      // timezone
      else if ($str === 'e') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->_tz->getId(); }
      else if ($str === 'I') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->isInDaylightSavings() ? '1' : '0'; }
      else if ($str === 'O') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->_tz->getGmtOffsetString($dt); }
      else if ($str === 'P') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->_tz->getGmtOffsetStringISO8601($dt); }
      else if ($str === 'T' && $format != SRA_GREGORIAN_DATE_FORMAT_ISO8601) { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->_tz->getAbbr($dt); }
      else if ($str === 'Z') { $str = $ignoreTimeTokens ? '' : $dt->isDateOnly() ? '' : $dt->_tz->getGmtOffset($dt)*60*60; }
      // full date/time
      else if ($str === 'c') { 
        $str = $ignoreDateTokens ? '' : $dt->format(SRA_GREGORIAN_DATE_FORMAT_ISO8601, $includeTime);
        $str = $dt->_tz ? str_replace($dt->_tz->getAbbr($dt), 'T', $str) : str_replace('::', '', $str);
      }
      else if ($str === 'r') { $str = $ignoreDateTokens ? '' : $dt->format(SRA_GREGORIAN_DATE_FORMAT_RFC2822, $includeTime); }
      // relative date strings
      else if ($str === 'R' || $str === 'R1' || $str === 'RT') {
        if ($ignoreRelativeTokens) {
          $str = '';
        }
        else {
          $baseStr = $str;
          $now = new SRA_GregorianDate();
          $isPast = $dt->compare($now) < 0;
          $ignoreDateTokens = TRUE;
          $relativeTime = FALSE;
          
          if ($str === 'RT' && ($nymdh = $now->format('YmdH')) && ($ymdh = $dt->format('YmdH')) && (($isPast && ($ymdh + SRA_GREGORIAN_DATE_RELATIVE_TIME_CAP) >= $nymdh) || (!$isPast && ($ymdh - SRA_GREGORIAN_DATE_RELATIVE_TIME_CAP) <= $nymdh))) {
            $numSeconds = abs($now->getUnixTimeStamp() - $dt->getUnixTimeStamp());
            $numMinutes = round($numSeconds/60);
            $numHours = round($numMinutes/60);
            if ($numSeconds > 55 && $numSeconds < 60) $numSeconds = 60;
            if ($numMinutes > 55 && $numMinutes < 60) $numMinutes = 60;
            
            $str = $resources->getString('date.relative.' . (!$numSeconds && !$numMinutes && !$numHours ? 'now' : ($numSeconds < 60 ? 'second' : ($numMinutes < 60 ? 'minute' : 'hour')) . ($isPast ? 'Ago' : '') . (($numSeconds < 60 && $numSeconds > 1) || ($numMinutes < 60 && $numMinutes > 1) || $numHours > 1 ? 's' : '')), array('num' => $numSeconds < 60 ? $numSeconds : ($numMinutes < 60 ? $numMinutes : $numHours)));
            $relativeTime = TRUE;
          }
          else if (($numYears = abs($now->getYear() - $dt->getYear())) && $str != 'R1' && $now->format('md') == $dt->format('md')) { $str = $resources->getString('date.relative.year' . ($isPast ? 'Ago' : '') . ($numYears == 1 ? '' : 's'), array('num' => $numYears)); }
          else if (($numMonths = $dt->getMonthsDelta($now)) && $str != 'R1' && $now->format('d') == $dt->format('d')) { $str = $resources->getString('date.relative.month' . ($isPast ? 'Ago' : '') . ($numMonths == 1 ? '' : 's'), array('num' => $numMonths)); }
          else if (($sameYear = $now->format('Y') == $dt->format('Y')) && ((($numDays = $dt->getDayDelta($now))) == 1)) { $str = $resources->getString('date.relative.' . ($isPast ? 'yesterday' : 'tomorrow')); }
          else if ($sameYear && !$numDays) { $str = $resources->getString('date.relative.today'); }
          else if ($sameYear && $numDays && (($numDays/7) <= 5) && $str != 'R1' && !($numDays % 7)) { $str = $resources->getString('date.relative.week' . ($isPast ? 'Ago' : '') . ($numDays == 7 ? '' : 's'), array('num' => ($numDays/7))); }
          else if ($sameYear && $numDays < 7) { $str = $resources->getString('date.relative.day' . ($isPast ? 'Ago' : '') . 's', array('num' => ($numDays))); }
          else {
            $ignoreDateTokens = FALSE;
            $str = $dt->format(str_replace($str, '', $str === 'RT' ? SRA_Controller::getAppDateFormat() : SRA_Controller::getAppDateOnlyFormat())); 
          }
          $ignoreTimeTokens = $baseStr == 'RT' && $ignoreDateTokens;
          if ($format == 'RT' && !$relativeTime && $ignoreDateTokens) {
            $str .= ' ' . $dt->format(NULL, TRUE, FALSE, TRUE);
          }
        }
      }
      $formatted .= $str;
    }
    return trim($formatted);
  }
  // }}}
  
  // {{{ fromRelativeStr
  /**
   * creates a new relative SRA_GregorianDate object based on input in the 
   * format "YYYY-MM-DD HH:MM:SS" (time is optional). where any of the date 
   * values (YYYY, MM, DD, HH, MM, or SS) may be replaced with a relative 
   * modifier in the format "+n" where n is the increase from the current 
   * timestamp. For example, to specify the 1st of the following month, the 
   * $expr would be: "+0-+1-01" - where +0 signifies the current year, and +1 
   * signifies the following month. if the current month was december (12), the 
   * following month will be January and the year will be incremented 
   * automatically. Another example: to specify exactly one week from the 
   * current time, $expr would be "+0-+0-+7" - where the first +0 signifies the 
   * current year, the second +0 signifies the current month, and +7 signifies 7 
   * days from the current date. month and year rollovers resulting in the 1 
   * week jump will be automatically applied (for example, if the action was 
   * created on 12/28). negative increments can be applied by enclosing the 
   * increment value "n" in parenthesis. for example, to specify 1 month minus 1 
   * week from the current date, $expr would be: "+0-+1-+(7)". returns NULL on 
   * error
   * @param string $expr the relative date expression to use
   * @param SRA_GregorianDate $start the start date. if not specified, the 
   * current date will be used
   * @access	public static
   * @return	SRA_GregorianDate
   */
  function fromRelativeStr($expr, $start=NULL) {
    $pieces = explode(' ', $expr);
    $dateParts = explode('-', $pieces[0]);
    if (isset($pieces[1])) { $timeParts = explode(':', $pieces[1]); }
    if (count($dateParts) != 3) {
      return NULL;
    }
    $date = SRA_GregorianDate::isValid($start) ? $start : new SRA_GregorianDate();
    
    // first set explicit values
    if (!SRA_Util::beginsWith($dateParts[0], '+')) { $date->setYear($dateParts[0]); }
    if (!SRA_Util::beginsWith($dateParts[1], '+')) { $date->setMonth($dateParts[1]); }
    if (!SRA_Util::beginsWith($dateParts[2], '+')) { $date->setDay($dateParts[2]); }
    $date->setHour($timeParts && isset($timeParts[0]) && !SRA_Util::beginsWith($timeParts[0], '+') ? $timeParts[0] : (SRA_Util::beginsWith($timeParts[0], '+') ? $date->getHour() : 0));
    $date->setMinute($timeParts && isset($timeParts[1]) && !SRA_Util::beginsWith($timeParts[1], '+') ? $timeParts[1] : (SRA_Util::beginsWith($timeParts[1], '+') ? $date->getMinute() : 0));
    $date->setSecond($timeParts && isset($timeParts[2]) && !SRA_Util::beginsWith($timeParts[2], '+') ? $timeParts[2] : (SRA_Util::beginsWith($timeParts[2], '+') ? $date->getSecond() : 0));
    
    // now set relative values
    if (SRA_Util::beginsWith($dateParts[0], '+')) { $date->jump(SRA_GREGORIAN_DATE_UNIT_YEAR, SRA_Util::getRelativeNumericVal($dateParts[0])); }
    if (SRA_Util::beginsWith($dateParts[1], '+')) { $date->jump(SRA_GREGORIAN_DATE_UNIT_MONTH, SRA_Util::getRelativeNumericVal($dateParts[1])); }
    if (SRA_Util::beginsWith($dateParts[2], '+')) { $date->jump(SRA_GREGORIAN_DATE_UNIT_DAY, SRA_Util::getRelativeNumericVal($dateParts[2])); }
    if ($timeParts && isset($timeParts[0]) && SRA_Util::beginsWith($timeParts[0], '+')) { $date->jump(SRA_GREGORIAN_DATE_UNIT_HOUR, SRA_Util::getRelativeNumericVal($timeParts[0])); }
    if ($timeParts && isset($timeParts[1]) && SRA_Util::beginsWith($timeParts[1], '+')) { $date->jump(SRA_GREGORIAN_DATE_UNIT_MINUTE, SRA_Util::getRelativeNumericVal($timeParts[1])); }
    if ($timeParts && isset($timeParts[2]) && SRA_Util::beginsWith($timeParts[2], '+')) { $date->jump(SRA_GREGORIAN_DATE_UNIT_SECOND, SRA_Util::getRelativeNumericVal($timeParts[2])); }
    
    return $date;
  }
  // }}}
  
  // {{{ get
  /**
   * wrapper method to the corresonding get method for the property specified
   * @param string $property the property to get: 
   * (day|hour|meridiem|minute|month|second|year)
   * @access public
   * @return string
   */
   function get($property) {
     return $this->format($property=='day' ? 'j' : ($property=='hour' ? 'H' : ($property=='meridiem' ? 'a' : ($property=='minute' ? 'i' : ($property=='month' ? 'n' : ($property=='second' ? 's' : ($property=='year' ? 'Y' : NULL)))))));
   }
   // }}}
  
  // {{{ getDay
  /**
   * returns the date month day 1-31 or 01-31 when $pad is TRUE
   * @param boolean $pad whether or not to pad the day with a leading 0 when it 
   * is < 10
   * @access public
   * @return mixed
   */
  function getDay($pad=FALSE) {
    return $pad ? sprintf('%02d', $this->_day) : $this->_day;
  }
  // }}}
  
  // {{{ getDayDelta
  /**
   * compares this date with $compare and returns the # of days that separate 
   * them
   * @param SRA_GregorianDate $compare the date to compare with. if not 
   * specified the current date will be used
   * @access	public
   * @return	int
   */
  function getDayDelta(&$compare) {
    $compare = $compare ? $compare : new SRA_GregorianDate();
    if (SRA_GregorianDate::isValid($compare)) {
      $copy =& $compare->copy();
      $past = $this->compare($copy) < 0;
      $delta = 0;
      $ymd = $this->format('Ymd');
      while($ymd != ($cymd = $copy->format('Ymd'))) {
        $delta++;
        $copy->jump(SRA_GREGORIAN_DATE_UNIT_DAY, $past ? -1 : 1);
      }
      return $delta;
    }
  }
  // }}}
  
  // {{{ getDayOfWeek
  /**
   * returns the numeric representation of the day of week for this date 
   * (0=sun and 6=sat) OR for the $year/$month/$day specified (when invoked 
   * statically)
   * @param int $year the year. the current date year used if not specified
   * @param int $month the month. the current date month used if not specified
   * @param int $day the day. the current date day used if not specified
   * @access public OR public static
   * @return int
   */
  function getDayOfWeek($year=NULL, $month=NULL, $day=NULL) {
    $year = $year ? $year : $this->_year;
    $month = $month ? $month : $this->_month;
    $day = $day ? $day : $this->_day;
    $mcodes = array(0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5);
    $mlcodes = array(6, 2);
    $y1 = substr($year, 0, 2) * 1;
    $y2 = substr($year, 2, 2) * 1;
    
    $c = (3 - ($y1 % 4)) * 2;
    $y = $y2 + floor($y2/4);
    $m = $month < 3 && SRA_GregorianDate::isLeapYear($year) ? $mlcodes[$month-1] : $mcodes[$month-1];
    return ($c+$y+$m+$day) % 7;
  }
  // }}}
  
  // {{{ getDayOfYear
  /**
   * returns the numeric representation of the day of year for the this date 
   * (0-364 or 365 for leap years) OR for the $year/$month/$day specified (when 
   * invoked statically)
   * @param int $year the year. the current date year used if not specified
   * @param int $month the month. the current date month used if not specified
   * @param int $day the day. the current date day used if not specified
   * @access public OR public static
   * @return int
   */
  function getDayOfYear($year=NULL, $month=NULL, $day=NULL) {
    $year = $year ? $year : $this->_year;
    $month = $month ? $month : $this->_month;
    $day = $day ? $day : $this->_day;
    $num = 0;
    for($i=1; $i<$month; $i++) { $num += SRA_GregorianDate::getNumDaysInMonth($year, $i); }
    return $num + $day - 1;
  }
  // }}}
  
  // {{{ getEaster
  /**
   * returns a date object representing when easter occurs for the year of this 
   * date object, or for $year is invoked statically
   * @param int $year the year. the current date year used if not specified
   * @access public OR public static
   * @return SRA_GregorianDate
   */
  function getEaster($year=NULL) {
    $year = $year ? $year : $this->getYear();
		$g = $year % 19;
		$c = (int)($year / 100);
		$h = (int)($c - ($c / 4) - ((8*$c+13) / 25) + 19*$g + 15) % 30;
		$i = (int)$h - (int)($h / 28)*(1 - (int)($h / 28)*(int)(29 / ($h + 1))*((int)(21 - $g) / 11));
		$j = ($year + (int)($year/4) + $i + 2 - $c + (int)($c/4)) % 7;
		$l = $i - $j;
		$m = 3 + (int)(($l + 40) / 44);
		$d = $l + 28 - 31 * ((int)($m / 4));
		return new SRA_GregorianDate($year, $m, $d);
  }
  // }}}
  
  // {{{ getISO8601DayOfWeek
  /**
   * returns the ISO 8601 numeric representation of the day of week for this 
   * date (1=Mon and 7=Sun) OR for the $year/$month/$day specified (when invoked 
   * statically). this is slightly different to the behavior of 'getDayOfWeek' 
   * in that weeks start on Monday and are numbered 1-7 instead of 0-6
   * @param int $year the year. the current date year used if not specified
   * @param int $month the month. the current date month used if not specified
   * @param int $day the day. the current date day used if not specified
   * @access public OR public static
   * @return int
   */
  function getISO8601DayOfWeek($year=NULL, $month=NULL, $day=NULL) {
    $dow = SRA_GregorianDate::getDayOfWeek($year ? $year : $this->_year, $month ? $month : $this->_month, $day ? $day : $this->_day);
    return $dow == 0 ? 7 : $dow;
  }
  // }}}
  
  // {{{ getISO8601EndDate
  /**
   * returns the ISO 8601 year end date for the this date OR for the $year 
   * specified (when invoked statically). the ISO 8601 end date is the day prior 
   * to the following year's ISO 8601 start date (see below)
   * @param int $year the year. the current date year used if not specified
   * @access public OR public static
   * @return SRA_GregorianDate
   */
  function &getISO8601EndDate($year=NULL) {
    $year = $year ? $year : $this->_year;
    $end = SRA_GregorianDate::getISO8601StartDate($year + 1);
    $end->jump(SRA_GREGORIAN_DATE_UNIT_DAY, -1);
    return $end;
  }
  // }}}
  
  // {{{ getISO8601NumWeeks
  /**
   * returns the number of ISO 8601 weeks in this date's year, or for the $year 
   * specified (when invoked statically). this is the # of Thursdays occuring 
   * in that year starting from the ISO 8601 start date (see below)
   * @param int $year the year. the current date year used if not specified
   * @access public OR public static
   * @return int
   */
  function getISO8601NumWeeks($year=NULL) {
    $start =& SRA_GregorianDate::getISO8601StartDate($year);
    $end =& SRA_GregorianDate::getISO8601EndDate($year);
    $weeks = 0;
    while($start->compare($end) < 0) { 
      $start->jump(SRA_GREGORIAN_DATE_UNIT_WEEK);
      $weeks++; 
    }
    return $weeks;
  }
  // }}}
  
  // {{{ getISO8601StartDate
  /**
   * returns the ISO 8601 year start date for the this date OR for the $year 
   * specified (when invoked statically). the ISO 8601 start date is the Monday 
   * of the week with the year's first Thursday in it
   * @param int $year the year. the current date year used if not specified
   * @access public OR public static
   * @return SRA_GregorianDate
   */
  function &getISO8601StartDate($year=NULL) {
    $year = $year ? $year : $this->_year;
    $start = new SRA_GregorianDate($year, 1, 1);
    // find the first thursday
    while($start->getDayOfWeek() != 4) { $start->jump(SRA_GREGORIAN_DATE_UNIT_DAY); }
    // backtrack to the prior monday
    while($start->getDayOfWeek() != 1) { $start->jump(SRA_GREGORIAN_DATE_UNIT_DAY, -1); }
    return $start;
  }
  // }}}
  
  // {{{ getISO8601WeekOfYear
  /**
   * returns the ISO-8601 week number for this gregorian date (1-53) OR for the 
   * $year/$month/$day specified (when invoked statically). the week number can 
   * be described as counting Thursdays (week 12 contains the 12th Thursday of 
   * the year). thus the first day of week 1 CAN occur in the prior year and the 
   * last day of the last week CAN occur in the next year. ISO-8601 considers 
   * Monday to be the first day of the week. for more information, see 
   * http://en.wikipedia.org/wiki/ISO_8601
   * @param int $year the year. the current date year used if not specified
   * @param int $month the month. the current date month used if not specified
   * @param int $day the day. the current date day used if not specified
   * @access public OR public static
   * @return int
   */
  function getISO8601WeekOfYear($year=NULL, $month=NULL, $day=NULL) {
    $year = $year ? $year : $this->_year;
    $month = $month ? $month : $this->_month;
    $day = $day ? $day : $this->_day;
    $date = new SRA_GregorianDate($year, $month, $day);
    $start =& SRA_GregorianDate::getISO8601StartDate($year);
    $end =& SRA_GregorianDate::getISO8601EndDate($year);
    
    // week is in last week of prior year
    if ($date->compare($start) < 0) {
      return SRA_GregorianDate::getISO8601NumWeeks($year-1);
    }
    // week is in first week of following year
    else if ($date->compare($end) > 0) {
      return 1;
    }
    // count weeks
    else {
      $week = 0;
      do {
        $week++;
        $start->jump(SRA_GREGORIAN_DATE_UNIT_WEEK);
      } while($date->compare($start) > 1);
    }
  }
  // }}}
  
  // {{{ getISO8601Year
  /**
   * returns the ISO 8601 year for the this date OR for the $year/$month/$day 
   * specified (when invoked statically). the ISO 8601 year is the year in which 
   * the week this date resides in occurred. for more information see the api 
   * documentation for 'getISO8601WeekOfYear'
   * @param int $year the year. the current date year used if not specified
   * @param int $month the month. the current date month used if not specified
   * @param int $day the day. the current date day used if not specified
   * @access public OR public static
   * @return int
   */
  function getISO8601Year($year=NULL, $month=NULL, $day=NULL) {
    $year = $year ? $year : $this->_year;
    $month = $month ? $month : $this->_month;
    $day = $day ? $day : $this->_day;
    $date = new SRA_GregorianDate($year, $month, $day);
    $start =& SRA_GregorianDate::getISO8601StartDate($year);
    $end =& SRA_GregorianDate::getISO8601EndDate($year);
    return $date->compare($start) < 0 ? $year - 1 : ($date->compare($end) > 0 ? $year + 1 : $year);
  }
  // }}}
  
  // {{{ getHour
  /**
   * returns the date hour 0-23 or 00-23 when $pad is TRUE. returns NULL if 
   * 'dateOnly' is TRUE
   * @param boolean $pad whether or not to pad the hour with a leading 0 when 
   * it is < 10
   * @access public
   * @return mixed
   */
  function getHour($pad=FALSE) {
    return $this->isDateOnly() ? NULL : ($pad ? sprintf('%02d', $this->_hour) : $this->_hour);
  }
  // }}}
  
  // {{{ getHour12
  /**
   * returns the date hour (as a 12-hour representation) 1-12 or 01-12 when $pad 
   * is TRUE. returns NULL if 'dateOnly' is TRUE
   * @param boolean $pad whether or not to pad the hour with a leading 0 when 
   * it is < 10
   * @access public
   * @return mixed
   */
  function getHour12($pad=FALSE) {
    $hour12 = $this->_hour==0 ? 12 : ($this->_hour>12 ? $this->_hour-12 : $this->_hour);
    return $this->isDateOnly() ? NULL : ($pad ? sprintf('%02d', $hour12) : $hour12);
  }
  // }}}
  
  // {{{ getMinute
  /**
   * returns the date minute 0-59 or 00-59 when $pad is TRUE
   * @param boolean $pad whether or not to pad the minute with a leading 0 when 
   * it is < 10. returns NULL if 'dateOnly' is TRUE
   * @access public
   * @return mixed
   */
  function getMinute($pad=FALSE) {
    return $this->isDateOnly() ? NULL : ($pad ? sprintf('%02d', $this->_minute) : $this->_minute);
  }
  // }}}
  
  // {{{ getMonth
  /**
   * returns the date month day 1-12 or 01-12 when $pad is TRUE
   * @param boolean $pad whether or not to pad the month with a leading 0 when 
   * it is < 10
   * @access public
   * @return mixed
   */
  function getMonth($pad=FALSE) {
    return $pad ? sprintf('%02d', $this->_month) : $this->_month;
  }
  // }}}
  
  // {{{ getMonthFromStr
  /**
   * attempts to parse the month string $str and return the corresponding month 
   * number (1-12) for that month. $str can be either a full month name, or an 
   * abbreviation (with or without trainling period). the search is also not 
   * case-sensitive. returns NULL if not successful
   * @param string $str the month string to parse. example: September, sept, jun
   * sept.
   * @access public static
   * @return int
   */
  function getMonthFromStr($str) {
    if ($str) {
      if (SRA_Util::endsWith($str, '.')) { $str = substr($str, 0, -1); }
      
      $resources =& SRA_Controller::getSysResources();
      for($i=1; $i<=12; $i++) {
        if (SRA_Util::beginsWith($resources->getString('date.month.' . $i), $str, FALSE)) {
          return $i;
        }
      }
    }
    return NULL;
  }
  // }}}
  
  // {{{ getMonthsDelta
  /**
   * compares this date with $compare and returns the # of months that separate 
   * them
   * @param SRA_GregorianDate $compare the date to compare with. if not 
   * specified, the current date will be used
   * @access	public
   * @return	int
   */
  function getMonthsDelta(&$compare) {
    $compared = $compared ? $compared : new SRA_GregorianDate();
    if (SRA_GregorianDate::isValid($compare)) {
      $copy =& $compare->copy();
      $past = $this->compare($copy) < 0;
      $delta = 0;
      while($this->format('Ym') != $copy->format('Ym')) {
        $delta++;
        $copy->jump(SRA_GREGORIAN_DATE_UNIT_MONTH, $past ? -1 : 1);
      }
      return $delta;
    }
  }
  // }}}
  
  // {{{ getNumDaysInMonth
  /**
   * returns the # of days in the month/year for this date object OR for the 
   * $month/$year specified (when invoked statically)
   * @param int $year the year. the current date year used if not specified
   * @param int $month the month. the current date month used if not specified
   * @access public OR public static
   * @return int
   */
  function getNumDaysInMonth($year=NULL, $month=NULL) {
    $year = $year ? $year : $this->_year;
    $month = $month ? $month : $this->_month;
    $days = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    return $month == 2 && SRA_GregorianDate::isLeapYear($year) ? 29 : $days[$month-1];
  }
  // }}}
  
  // {{{ getPropertyRange
  /**
   * returns an array of possible values for the property specified. the $range 
   * parameter may have the following structure:
   *         "N1 N3 N5"   : only options N1, N3 and N5 will be available
   *         "N1-N3"      : only options >= N1 and <= N3 will be available
   *				 "N1-N3 N5-N8": combination of the above
   *				 "-N1"        : the current property value to N1
   *				 "N1-"        : N1 to the current property value
   *				 "-"          : only the current property value
   *				 "-+3"        : the current property value or the current value +1, +2 and +3
   *				 "-!3"        : the current property value or the current value -1, -2, and -3
   *				 "N1-+3"      : N1 or N1+1, or N1+2, or N1+3
   *				 "*"          : all possible values for that property. for the year 
   *				                property, this range will be 1901-2038
   *         "%N"         : only values from the resulting set that are evenly divisible by N
   *         "(^|v)"      : sort method: ^ = ascending order, v = descending order. the default 
   *                        behavior is to apply the natural sort resulting from the parameters 
   *                        specified above
   *         "#"          : sets the current property values as the first in the property array 
   *                        (followed by the sort method specified)
   *
   * @param string $property the property to return the range for: (day|hour|hour12|meridiem|minute|month|second|year)
   * @param string $range the range to return. defaults to '*'
   * @param string $formatString optional format string defining the display value 
   *               format for each range value. if $formatString 
   *               contains the substring "{$rangeVal}", then that value will be 
   *               substituted with the actual range value and the display value 
   *               will be the value returned by the app resource bundle using 
   *               the resulting string as the key. if $formatString is not 
   *               specified, the following default formatStrings will be assumed:
   *                day:      'j'
   *                hour[12]: 'H'
   *                meridiem: 'A'
   *                minute:   'i'
   *                month:    'n'
   *                second:   's'
   *                year:     'Y'
   *                
   * @access public static
   * @return associative array (key = range value (with leading zeros), value = display value)
   */
  function getPropertyRange($property, $range='*', $formatString=FALSE) {
    $rb =& SRA_Controller::getAppResources();
    $workingDate = new SRA_GregorianDate();
    $workingDate->jumpToStartOfYear();
    $propertyRange = array();
    $divisors = array();
    $sort = '^';
    $firstCurrent = FALSE;
    if ($property == 'day') {
      $keyString = 'd';
      $formatString = $formatString ? $formatString : 'j';
      $propertyRange = SRA_Util::getArray(31);
    }
    if ($property == 'hour') {
      $keyString = 'G';
      $formatString = $formatString ? $formatString : 'H';
      $propertyRange = SRA_Util::getArray(24,0);
    }
    if ($property == 'hour12') {
      $keyString = 'g';
      $formatString = $formatString ? $formatString : 'H';
      $propertyRange = SRA_Util::getArray(12);
      $property = 'hour';
    }
    if ($property == 'meridiem') {
      $keyString = 'G';
      $formatString = $formatString ? $formatString : 'A';
      $propertyRange = array(1, 20);
      $setMethod = 'setHour';
    }
    if ($property == 'minute') {
      $keyString = 'i';
      $formatString = $formatString ? $formatString : 'i';
      $propertyRange = SRA_Util::getArray(60,0);
    }
    if ($property == 'month') {
      $keyString = 'n';
      $formatString = $formatString ? $formatString : 'n';
      $propertyRange = SRA_Util::getArray(12);
    }
    if ($property == 'second') {
      $keyString = 's';
      $formatString = $formatString ? $formatString : 's';
      $propertyRange = SRA_Util::getArray(60,0);
    }
    if ($property == 'year') {
      $keyString = 'Y';
      $formatString = $formatString ? $formatString : 'Y';
      $propertyRange = SRA_Util::getArray(138,1901);
    }
    if (!$setMethod) {
      $setMethod = 'set' . strtoupper(substr($property, 0, 1)) . substr($property, 1);
    }
    if (!$getMethod) {
      $getMethod = 'get' . strtoupper(substr($property, 0, 1)) . substr($property, 1);
    }
    if ($range && $range != '*') {
      $baseRange = $propertyRange;
      $propertyRange = array();
      $pieces = explode(' ', $range);
      foreach ($pieces as $piece) {
        $piece = trim($piece);
        if (!$piece) {
          continue;
        }
        if (strstr($piece, '-')) {
          $rangePieces = explode('-', $piece);
          $start = isset($rangePieces[0]) && strlen($rangePieces[0]) ? $rangePieces[0] : $this->${getMethod}();
          $end = isset($rangePieces[1]) && strlen($rangePieces[1]) ? $rangePieces[1] : $this->${getMethod}();
          if (strstr($start, '+')) {
            $start = str_replace('+', '', $start);
            $start = $this->${getMethod}() + $start;
          }
          if (strstr($end, '+')) {
            $end = str_replace('+', '', $end);
            $end = $this->${getMethod}() + $end;
          }
          if (strstr($start, '!')) {
            $start = str_replace('!', '', $start);
            $start = $this->${getMethod}() - $start;
          }
          if (strstr($end, '!')) {
            $end = str_replace('!', '', $end);
            $end = $this->${getMethod}() - $end;
          }
          if ($property == 'day') {
            $start = $start >= 1 ? $start : 1;
          }
          if ($property == 'hour') {
            $start = $start >= 0 ? $start : 0;
          }
          if ($property == 'hour12' || $property == 'month') {
            $start = $start >= 0 ? $start : 12;
          }
          if ($property == 'minute' || $property == 'second') {
            $start = $start >= 0 ? $start : 0;
          }
          if ($property == 'year') {
            $start = $start >= 1901 ? $start : 1901;
          }
          for($i=$start; $i<=$end; $i++) {
            $val = $i;
            if ($property == 'day') {
              $val = $val <= 31 ? $val : $val % 31;
            }
            if ($property == 'hour') {
              $val = $val <= 23 ? $val : $val % 24;
            }
            if ($property == 'hour12' || $property == 'month') {
              $val = $val <= 12 ? $val : $val % 12;
            }
            if ($property == 'minute' || $property == 'second') {
              $val = $val <= 59 ? $val : $val % 60;
            }
            if ($property == 'year') {
              $val = $val <= 2038 ? $val : $val % 2038;
            }
            $propertyRange[$val] = TRUE;
          }
          $propertyRange = array_keys($propertyRange);
        }
        else if (substr($piece, 0, 1) == '%') {
          $divisors[] = substr($piece, 1);
        }
        else if ($piece == '*') {
          $propertyRange = $baseRange;
        }
        else if ($piece == '^' || $piece == 'v') {
          $sort = $piece;
        }
        else if ($piece == '#') {
          $firstCurrent = TRUE;
        }
        else {
          $propertyRange[] = $piece;
        }
      }
    }
    $myRange = array();
    foreach($propertyRange as $val) {
      $workingDate->${setMethod}($val);
      $key = (int) $workingDate->format($keyString);
      $cont = TRUE;
      foreach ($divisors as $div) {
        if ($key % $div) {
          $cont = FALSE;
        }
      }
      if ($cont) {
        $myRange[$property == 'meridiem' ? ($key == 1 ? 'am' : 'pm') : $key] = strstr($formatString, '{$rangeVal}') ? $rb->getString(str_replace('{$rangeVal}', $val, $formatString)) : $workingDate->format($formatString);
      }
    }
    if ($sort == '^') {
      ksort($myRange);
    }
    else if ($sort == 'v') {
      krsort($myRange);
    }
    if ($firstCurrent) {
      $current = new SRA_GregorianDate();
      $curKey = (int) $current->format($keyString);
      $tmp = array();
      if (isset($myRange[$curKey])) {
        $tmp[$curKey] = $myRange[$curKey];
        foreach($myRange as $key => $val) {
          if ($key != $curKey) {
            $tmp[$key] = $val;
          }
        }
        $myRange = $tmp;
      }
    }
    
    return $myRange;
  }
  // }}}
  
  // {{{ getSecond
  /**
   * returns the date second 0-59 or 00-59 when $pad is TRUE. returns NULL if 
   * 'dateOnly' is TRUE
   * @param boolean $pad whether or not to pad the second with a leading 0 when 
   * it is < 10
   * @access public
   * @return mixed
   */
  function getSecond($pad=FALSE) {
    return $this->isDateOnly() ? NULL : ($pad ? sprintf('%02d', $this->_second) : $this->_second);
  }
  // }}}
  
  // {{{ getTimeZone
  /**
   * returns the time zone for this date instance (if applicable)
   * @access public
   * @return SRA_TimeZone
   */
  function &getTimeZone() {
    return $this->_tz;
  }
  // }}}
  
  // {{{ getUnixTimeStamp
  /**
   * returns the current time measured in the number of seconds since the Unix 
   * Epoch (January 1 1970 00:00:00 GMT). Warning: do not use this method for 
   * dates that occurred prior to the unix epoch
   * @access public
   * @return int
   */
  function getUnixTimeStamp() {
    $tz = $this->isDateOnly() ? SRA_Controller::getAppTimeZone() : $this->_tz;
    $oldTz = SRA_TimeZone::getTzEnv();
    SRA_TimeZone::setTzEnvVar($tz);
    $timestamp = mktime($this->_hour, $this->_minute, $this->_second, $this->_month, $this->_day, $this->_year);
    SRA_TimeZone::setTzEnvVar($oldTz);
    return $timestamp;
  }
  // }}}
  
  // {{{ getYear
  /**
   * returns the date year
   * @access public
   * @return int
   */
  function getYear() {
    return $this->_year;
  }
  // }}}
  
  // {{{ isDateOnly
  /**
   * returns TRUE if this gregorian date object represents ONLY a date (time is 
   * irrelevant)
   * @access public
   * @return boolean
   */
  function isDateOnly() {
    return $this->_dateOnly;
  }
  // }}}
  
  // {{{ isDstOverlap
  /**
   * returns TRUE if this date/time is during the dst rollback overlap period 
   * (i.e. the 2nd 1:00 hour on 11/2/2008)
   * @access public
   * @return boolean
   */
  function isDstOverlap() {
    if (!$this->isDateOnly() && $this->_dstOverlap && SRA_GregorianDate::isValid($end = $this->_tz->getDaylightSavingsEnd($this)) && $this->format('YmdH') == $end->format('YmdH')) {
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ isFuture
  /**
   * returns TRUE if this date is in the future (after the current date)
   * @access	public
   * @return	boolean
   */
  function isFuture() {
    $today = new SRA_GregorianDate();
    return $today->compare($this) < 0;
  }
  // }}}
  
  // {{{ isInDaylightSavings
  /**
   * returns TRUE if this date object uses a timezone that applies daylight 
   * savings and the current date is within the daylight savings time period. 
   * applies only to non-dateOnly objects
   * @access public
   * @return boolean
   */
  function isInDaylightSavings() {
    return $this->_tz->isDaylightSavings($this);
  }
  // }}}
  
  // {{{ isLeapYear
  /**
   * returns TRUE if this date object year is a leap year OR if $year is a leap 
   * year (when invoked statically), FALSE otherwise
   * @param int $year the year. the current date year used if not specified
   * @access public OR public static
   * @return boolean
   */
  function isLeapYear($year=NULL) {
    $year = $year ? $year : $this->_year;
    return is_numeric($year) && ($year%4) == 0 && (($year%100) != 0 || ($year%400) == 0);
  }
  // }}}
  
  // {{{ isPast
  /**
   * returns TRUE if this date is in the past (prior to the current date)
   * @access	public
   * @return	boolean
   */
  function isPast() {
    $today = new SRA_GregorianDate();
    return $today->compare($this) > 0;
  }
  // }}}
  
  // {{{ isToday
  /**
   * returns TRUE if this date is today
   * @access	public
   * @return	boolean
   */
  function isToday() {
    $today = new SRA_GregorianDate(NULL, $this->_tz);
    return $today->format('Ymd') == $this->format('Ymd');
  }
  // }}}
  
  // {{{ isTommorow
  /**
   * returns TRUE if this date is today
   * @access	public
   * @return	boolean
   */
  function isTomorrow() {
    $today = new SRA_GregorianDate(NULL, $this->_tz);
    return ($today->format('Ymd') + 1) == $this->format('Ymd');
  }
  // }}}
  
  // {{{ isYesterday
  /**
   * returns TRUE if this date is today
   * @access	public
   * @return	boolean
   */
  function isYesterday() {
    $today = new SRA_GregorianDate(NULL, $this->_tz);
    return ($today->format('Ymd') - 1) == $this->format('Ymd');
  }
  // }}}
  
  // {{{ isTodayOrFuture
  /**
   * returns TRUE if this date is today or a future date
   * @access	public
   * @return	boolean
   */
  function isTodayOrFuture() {
    return $this->isToday() || $this->isFuture();
  }
  // }}}
  
  // {{{ isWeekday
  /**
   * returns TRUE if this date is a weekday (M-F)
   * @access	public
   * @return	boolean
   */
  function isWeekday() {
    $d = $this->format('w');
    return $d > 0 && $d < 6;
  }
  // }}}
  
  // {{{ isValid
  /**
   * static method that returns true if $object is a SRA_GregorianDate instance
   * @param object $object the object to evaluate
   * @access public
   * @return boolean
   */
  function isValid( &$object ) {
    return (is_object($object) && (!isset($object->err) || !SRA_Error::isError($object->err)) && strtolower(get_class($object)) == 'sra_gregoriandate');
  }
  // }}}
  
  // {{{ jump
  /**
   * used to move this date from one date to another based on the $quantity and 
   * $unit specified. for example: advance date forward by 1 week, or back by 
   * 1 month. appropriate adjustments to all of this date's data attributes will 
   * be made (example: if jumping 1 month forward starting in December, the year 
   * will automatically be advanced). IF the $unit is 
   * SRA_GREGORIAN_DATE_UNIT_MONTH, and the current day is greater than the # of 
   * days in the new month, the current day will be changed to the last day of 
   * that month. For example, if jumping 1 month forward from 1/31, the new date 
   * would be 2/28 (2/29 for leap years). returns TRUE on success
   * @param int $unit the unit of measure. this value MUST correspond with one 
   * of the SRA_GREGORIAN_DATE_UNIT_* constants
   * @param int $quantity the quantity of $unit to jump. if not specified, 1 
   * will be assumed
   * @access public
   * @return boolean
   */
  function jump($unit, $quantity=1) {
    if ($unit == SRA_GREGORIAN_DATE_UNIT_DAY || $unit == SRA_GREGORIAN_DATE_UNIT_MINUTE || 
        $unit == SRA_GREGORIAN_DATE_UNIT_HOUR || $unit == SRA_GREGORIAN_DATE_UNIT_SECOND || 
        $unit == SRA_GREGORIAN_DATE_UNIT_WEEK || $unit == SRA_GREGORIAN_DATE_UNIT_MONTH || 
        $unit == SRA_GREGORIAN_DATE_UNIT_YEAR) {
      switch ($unit) {
        case SRA_GREGORIAN_DATE_UNIT_DAY:
          for($i=0; $i<abs($quantity); $i++) {
            $this->_day += $quantity > 0 ? 1 : -1;
            if ($this->_day == 0) {
              $this->_day = 32;
              $this->jump(SRA_GREGORIAN_DATE_UNIT_MONTH, -1);
            }
            else if ($this->_day > $this->getNumDaysInMonth()) {
              $this->_day = 1;
              $this->jump(SRA_GREGORIAN_DATE_UNIT_MONTH);
            }
          }
          break;
        case SRA_GREGORIAN_DATE_UNIT_MINUTE: 
          $this->_minute += $quantity;
          $hours = 0;
          while($this->_minute < 0 || $this->_minute > 59) {
            $hours += $this->_minute < 0 ? -1 : 1;
            $this->_minute += $this->_minute < 0 ? 60 : -60;
          }
          if ($hours !== 0) { $this->jump(SRA_GREGORIAN_DATE_UNIT_HOUR, $hours); }
          break;
        case SRA_GREGORIAN_DATE_UNIT_HOUR: 
          $this->_hour += $quantity;
          $days = 0;
          while($this->_hour < 0 || $this->_hour > 23) {
            $days += $this->_hour < 0 ? -1 : 1;
            $this->_hour += $this->_hour < 0 ? 24 : -24;
          }
          if ($days !== 0) { $this->jump(SRA_GREGORIAN_DATE_UNIT_DAY, $days); }
          break;
        case SRA_GREGORIAN_DATE_UNIT_SECOND: 
          $this->_second += $quantity;
          $minutes = 0;
          while($this->_second < 0 || $this->_second > 59) {
            $minutes += $this->_second < 0 ? -1 : 1;
            $this->_second += $this->_second < 0 ? 60 : -60;
          }
          if ($minutes !== 0) { $this->jump(SRA_GREGORIAN_DATE_UNIT_MINUTE, $minutes); }
          break;
        case SRA_GREGORIAN_DATE_UNIT_WEEK:
          $this->jump(SRA_GREGORIAN_DATE_UNIT_DAY, 7 * $quantity);
          break;
        case SRA_GREGORIAN_DATE_UNIT_MONTH: 
          $this->_month += $quantity;
          $years = 0;
          while ($this->_month < 1 || $this->_month > 12) {
            $years += $this->_month < 1 ? -1 : 1;
            $this->_month += $this->_month < 1 ? 12 : -12;
          }
          if ($years !== 0) { $this->jump(SRA_GREGORIAN_DATE_UNIT_YEAR, $years); }
          
          // if current day is now > number of days in the new month, change the 
          // day to the last day of the month
          if ($this->_day > $this->getNumDaysInMonth()) { $this->_day = $this->getNumDaysInMonth(); }
          break;
        case SRA_GREGORIAN_DATE_UNIT_YEAR: 
          $this->setYear($this->_year + $quantity);
          break;
      }
      $this->validateDst();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ jumpTo
  /**
   * used to jump to the 'nth' 'day' of a month. example: the first sunday of 
   * march
   * @param int $dow the day of week to jump to (0=sunday, 6=saturday)
   * @param int $week the week jump to. default is 1. example: 1=1st $dow of 
   * $month - ($dow=0, $week=1, $month=3 would be the first Sunday of March). 
   * $week MUST be between 1 and 5 and the jump will NEVER go into the next 
   * month. so to jump to the last week of $month, simply set $week=5
   * @param int $month an optional month. if not specified, the current month 
   * will be used
   * @access public
   * @return boolean
   */
  function jumpTo($dow, $week=1, $month=NULL) {
    if (is_numeric($dow) && $dow >= 0 && $dow <= 6 && is_numeric($week) && $week >= 0 && $week <= 5 && (!isset($month) || (is_numeric($month) && $month >= 1 && $month <=12))) {
      $month = $month ? $month : $this->_month;
      $this->setMonth($month);
      $this->setDay(1);
      while($this->getDayOfWeek() != $dow) { $this->jump(SRA_GREGORIAN_DATE_UNIT_DAY); }
      for($i=1; $i<$week; $i++) { $this->jump(SRA_GREGORIAN_DATE_UNIT_WEEK); }
      // backtrack if month was past
      while($this->getMonth() != $month) { $this->jump(SRA_GREGORIAN_DATE_UNIT_WEEK, -1); }
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ jumpToEndOfDay
  /**
   * used to jump to end of the current day
   * @access public
   * @return boolean
   */
  function jumpToEndOfDay() {
    if (!$this->isDateOnly()) {
      $this->setHour(23);
      $this->jumpToEndOfHour();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ jumpToStartOfDay
  /**
   * used to jump to start of the current day
   * @access public
   * @return boolean
   */
  function jumpToStartOfDay() {
    if (!$this->isDateOnly()) {
      $this->setHour(0);
      $this->jumpToStartOfHour();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ jumpToEndOfHour
  /**
   * used to jump to end of the current hour
   * @access public
   * @return boolean
   */
  function jumpToEndOfHour() {
    if (!$this->isDateOnly()) {
      $this->setMinute(59);
      $this->setSecond(59);
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ jumpToStartOfHour
  /**
   * used to jump to start of the current hour
   * @access public
   * @return boolean
   */
  function jumpToStartOfHour() {
    if (!$this->isDateOnly()) {
      $this->setMinute(0);
      $this->setSecond(0);
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ jumpToEndOfMonth
  /**
   * used to jump to end of the current month
   * @access public
   * @return boolean
   */
  function jumpToEndOfMonth() {
    $this->setDay($this->getNumDaysInMonth());
    $this->jumpToEndOfDay();
    return TRUE;
  }
  // }}}
  
  // {{{ jumpToStartOfMonth
  /**
   * used to jump to start of the current month
   * @access public
   * @return boolean
   */
  function jumpToStartOfMonth() {
    $this->setDay(1);
    $this->jumpToStartOfDay();
    return TRUE;
  }
  // }}}
  
  // {{{ jumpToEndOfYear
  /**
   * used to jump to end of the current year
   * @access public
   * @return boolean
   */
  function jumpToEndOfYear() {
    $this->setMonth(12);
    $this->jumpToEndOfMonth();
    return TRUE;
  }
  // }}}
  
  // {{{ jumpToStartOfYear
  /**
   * used to jump to start of the current year
   * @access public
   * @return boolean
   */
  function jumpToStartOfYear() {
    $this->setMonth(1);
    $this->jumpToStartOfMonth();
    return TRUE;
  }
  // }}}
  
  // {{{ jumpToEndOfWeek
  /**
   * used to jump to end of the current week (Saturday)
   * @access public
   * @return boolean
   */
  function jumpToEndOfWeek() {
    $this->setDayOfWeek(6);
    $this->jumpToEndOfDay();
    return TRUE;
  }
  // }}}
  
  // {{{ jumpToStartOfWeek
  /**
   * used to jump to start of the current week (Sunday)
   * @access public
   * @return boolean
   */
  function jumpToStartOfWeek() {
    $this->setDayOfWeek(0);
    $this->jumpToStartOfDay();
    return TRUE;
  }
  // }}}
  
  // {{{ newInstanceFromNtp
  /**
   * uses an NTP server to instantiate a new SRA_GregorianDate object. returns 
   * NULL if $ntpServer could not be queried
   * @param string $ntpServer the ntp server to use
   * @param int $timeout the # of seconds to wait to establish a connection with
   * $ntpServer. default is 3 seconds
   * @param int $port the port to use to connect to $ntpServer (defaults to the 
   * standard ntp port 13)
   * @access public
   * @return SRA_GregorianDate
   */
  function &newInstanceFromNtp($ntpServer, $timeout = 3, $port = 123) {
    if ($fp = @fsockopen('udp://' . $ntpServer, $port, $errno, $errstr, $timeout)) {
      @socket_set_timeout($fp, $timeout);
      fputs($fp, "\n");
      $reply  = fread($fp,1);
      $bytes = stream_get_meta_data($fp);
      $reply .= fread($fp,$bytes['unread_bytes']);
      fclose($fp);
      return $reply;
    }
    return NULL;
    $ntpDate = NULL;
    $start = time();
    if ($fp = @fsockopen($ntpServer, $port, $errno, $errstr, $timeout)) {
      $timeStr = fread($fp, 50);
      if (preg_match('/([0-9]{2}.*[0-9]{2}:[0-9]{2}:[0-9]{2}).*([a-zA-Z]{3})/', $timeStr, $m)) {
        include_once('util/l10n/SRA_Country.php');
        $country = new SRA_Country();
        $tz = NULL;
        foreach(array_keys($timezones =& $country->getTimeZones()) as $key) {
          if ($timezones[$key]->_abbr == trim($m[2]) || $timezones[$key]->_abbrDst == trim($m[2]) || $timezones[$key]->getId() == trim($m[2])) {
            $tz =& $timezones[$key];
            break;
          }
        }
        $ntpDate = new SRA_GregorianDate($m[1], $tz);
        $ntpDate->setTimeZone(SRA_Controller::getAppTimeZone());
      }
      fclose($fp);
    }
    return $ntpDate;
  }
  // }}}
  
  // {{{ parseString
  /**
   * this method can be used to parse a string replacing inbedded date time tags 
   * as necessary. the tags are replaced according to the SRA_GregorianDate 
   * object properties. the possible tags are those listed in the php date 
   * function api. tags must be imbedded as follows:
   *
   * {DTR_"format string"(+/-quantity)(SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)}
   *
   * multiple imbedded tags may exist.
   *
   * so, for example, to add next year to a string you would need to have the 
   * following tag imbedded in that string: {DTR_"Y"(+1)(YEAR)}
   * @param string $string the string to parse
   * @access public
   * @return string
   */
  function parseString($string) {
    $full=false;
    $dateTime=$this->copy();
    if (preg_match("'\{ *DTR_(\"|\')(.*?)(\"|\') *\((\+|\-)(.*?)\) *\((SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)\) *\}'si", $string, $matches)) {
      $full=true;
      $increment=0;
      $replace="";
      if (count($matches)==7) {
        $increment=$matches[5];
        if ($matches[4]=="-") { $increment*=-1; }
        switch (strtoupper($matches[6])) {
          case 'SECOND':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_SECOND, $increment);
            break;
          case 'MINUTE':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_MINUTE, $increment);
            break;
          case 'HOUR':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_HOUR, $increment);
            break;
          case 'DAY':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_DAY, $increment);
            break;
          case 'WEEK':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_WEEK, $increment);
            break;
          case 'MONTH':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_MONTH, $increment);
            break;
          case 'YEAR':
            $dateTime->jump(SRA_GREGORIAN_DATE_UNIT_YEAR, $increment);
            break;
        }
      }
    }
    if (count($matches)==7 || preg_match("'\{ *DTR_(\"|\')(.*?)(\"|\') *\}'si", $string, $matches)) {
      if (count($matches)>2) {
        $replace=$dateTime->format($matches[2]);
      }
      if ($full) {
        $string=preg_replace("'\{ *DTR_(\"|\')(.*?)(\"|\') *\((\+|\-)(.*?)\) *\((SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)\) *\}'si", $replace, $string, 1);
      }
      else {
        $string=preg_replace("'\{ *DTR_(\"|\')(.*?)(\"|\') *\}'si", $replace, $string, 1);
      }
      return $this->parseString($string);
    }
    else {
      return $string;
    }
  }
  // }}}
  
  // {{{ setAppTimeZone
  /**
   * this method converts the date's time zone to the currently selected app 
   * time zone (if an app is currently selected)
   * @param boolean $convert whether or not to adjust this date's time by the 
   * difference in offsets between the current time zone and the new time zone. 
   * default is TRUE
   * @access public
   * @return boolean
   */
  function &setAppTimeZone($convert=TRUE) {
    return $this->setTimeZone(SRA_Controller::getAppTimeZone(), $convert);
  }
  // }}}
  
  // {{{ setDateOnly
  /**
   * sets whether or not this gregorian date object represents ONLY a date. 
   * returns TRUE if the value was changed. hour, minute and second will default 
   * to 0 and the current app (or system) time zone will be set when a date is 
   * changed from dateOnly=TRUE to dateOnly=FALSE. these values will be set to 
   * NULL when the opposite change is made
   * @param boolean $dateOnly the value to set
   * @access public
   * @return boolean
   */
  function setDateOnly($dateOnly) {
    if ($dateOnly != $this->_dateOnly) {
      if (!$dateOnly) {
        $this->_hour = 0;
        $this->_minute = 0;
        $this->_second = 0;
        $this->_tz =& SRA_Controller::getAppTimeZone();
      }
      else {
        $this->_hour = NULL;
        $this->_minute = NULL;
        $this->_second = NULL;
        $this->_tz = NULL;
      }
      $this->_dateOnly = $dateOnly;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setDay
  /**
   * sets the date month day. returns TRUE if the value was changed. Note: IF 
   * $day exceeds the # of days in the current month, the day will be changed 
   * to the max # of days in that month instead. example: if the month was 
   * February and $day is 31, the day that is actually set would be 28 (or 29 
   * for a leap year)
   * @param int $day the day to set (1-31)
   * @access public
   * @return boolean
   */
  function setDay($day) {
    if ($day != $this->_day && $day >= 1 && $day <= 32) {
      $day = $day > $this->getNumDaysInMonth() ? $this->getNumDaysInMonth() : $day;
      $this->_day = $day*1;
      $this->validateDst();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setDayOfWeek
  /**
   * sets the day of the current week. returns TRUE on success
   * @param int $day the day of week to set (0=Sun, 6=Sat)
   * @access public
   * @return boolean
   */
  function setDayOfWeek($dow) {
    if ($dow >= 0 && $dow <= 6) {
      $cdow = $this->getDayOfWeek();
      while($dow != $this->getDayOfWeek()) {
        $this->jump(SRA_GREGORIAN_DATE_UNIT_DAY, $cdow > $dow ? -1 : 1);
      }
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setDstOverlap
  /**
   * sets the _dstOverlap attribute
   * @param boolean $dstOverlap the overlap value to set
   * @access public
   * @return boolean
   */
  function setDstOverlap($dstOverlap) {
    $this->_dstOverlap = $dstOverlap;
    return TRUE;
  }
  // }}}
  
  // {{{ setHour
  /**
   * sets the date hour. returns TRUE if the value was changed. applies only 
   * to non-dateOnly dates
   * @param int $hour the hour to set (0-23)
   * @access public
   * @return boolean
   */
  function setHour($hour) {
    if (!$this->isDateOnly() && $hour != $this->_hour && $hour >= 0 && $hour <= 23) {
      $this->_hour = $hour*1;
      $this->validateDst();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setMinute
  /**
   * sets the date minute. returns TRUE if the value was changed. applies only 
   * to non-dateOnly dates
   * @param int $minute the minute to set (0-59)
   * @access public
   * @return boolean
   */
  function setMinute($minute) {
    if (!$this->isDateOnly() && $minute != $this->_minute && $minute >= 0 && $minute <= 59) {
      $this->_minute = $minute*1;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setMonth
  /**
   * sets the date month. returns TRUE if the value was changed. if the current 
   * day exceeds the max # of days in the new month, the current day will be 
   * changed to the last day of the current month
   * @param int $month the month to set (1-12)
   * @access public
   * @return boolean
   */
  function setMonth($month) {
    if ($month != $this->_month && $month >= 1 && $month <= 12) {
      $this->_month = $month*1;
      if ($this->_day > $this->getNumDaysInMonth()) { $this->_day = $this->getNumDaysInMonth(); }
      $this->validateDst();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setSecond
  /**
   * sets the date second. returns TRUE if the value was changed. applies only 
   * to non-dateOnly dates
   * @param int $second the second to set (0-59)
   * @access public
   * @return boolean
   */
  function setSecond($second) {
    if (!$this->isDateOnly() && $second != $this->_second && $second >= 0 && $second <= 59) {
      $this->_second = $second*1;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setTimeZone
  /**
   * sets the date time zone. returns TRUE if the value was changed and set 
   * successfully. applies only to non-dateOnly dates
   * @param SRA_TimeZone $tz the new time zone to use
   * @param boolean $convert whether or not to adjust this date's time by the 
   * difference in offsets between the current time zone and the new time zone. 
   * default is TRUE
   * @access public
   * @return boolean
   */
  function setTimeZone(&$tz, $convert=TRUE) {
    if (!$this->isDateOnly() && SRA_TimeZone::isValid($tz) && !$tz->equals($this->_tz)) {
      if ($convert && SRA_TimeZone::isValid($this->_tz)) {
        $this->jump(SRA_GREGORIAN_DATE_UNIT_MINUTE, $tz->getGmtOffset($this) - $this->_tz->getGmtOffset($this));
      }
      $this->_tz =& $tz;
      $this->validateDst();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setUnixTimeStamp
  /**
   * sets the values for this timestamp using a unix $timestamp
   * @param int $timestamp the timestamp to set
   * @access public
   * @return boolean
   */
  function setUnixTimeStamp($timestamp) {
    if ($timestamp) {
      $this->setYear(date('Y', $timestamp));
      $this->setMonth(date('n', $timestamp));
      $this->setDay(date('j', $timestamp));
      if (!$this->isDateOnly()) {
        $tz =& $this->getTimeZone();
        $oldTz = SRA_TimeZone::getTzEnv();
        SRA_TimeZone::setTzEnvVar($tz);
        $this->setHour(date('G', $timestamp));
        $this->setMinute(date('i', $timestamp));
        $this->setSecond(date('s', $timestamp));
        SRA_TimeZone::setTzEnvVar($oldTz);
      }
      $this->validateDst();
      return TRUE;
    }
    return FALSE;
  }
  // }}}
  
  // {{{ setYear
  /**
   * sets the date year. returns TRUE if the value was changed. if the current 
   * day exceeds the max # of days in the new month, the current day will be 
   * changed to the last day of the current month (applies only when changing 
   * year from a leap year to a non leap year when date is 2/29 in the original 
   * year)
   * @param int $year the year to set (YY or YYYY). year must be greater than 
   * 1752 when the gregorian calendar was official adopted. if $year is only 2 
   * characters, they will be prefixed with the current century (i.e. 06 will be 
   * prefixed will '20')
   * @access public
   * @return boolean
   */
  function setYear($year) {
    if (strlen($year) == 2) { $year = substr(date('Y'), 0, 2) * 1; }
    if ($year != $this->_year && $year >= 1752 && $year <= 9999) {
      $this->_year = $year*1;
      if ($this->_day > $this->getNumDaysInMonth()) { $this->_day = $this->getNumDaysInMonth(); }
      $this->validateDst();
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ setYmd
  /**
   * shortcut method to set all (or at least 1), year, month, and/or day. 
   * returns TRUE if any value was set
   * @param int $year the year to set
   * @param int $month the month to set
   * @param int $day the day to set
   * @access public
   * @return boolean
   */
  function setYmd($year=NULL, $month=NULL, $day=NULL) {
    if ($year) { $ret1 = $this->setYear($year); }
    if ($month) { $ret2 = $this->setMonth($month); }
    if ($day) { $ret3 = $this->setDay($day); }
    $this->validateDst();
    return $ret1 || $ret2 || $ret3;
  }
  // }}}
  
  // {{{ toInt
  /**
   * converts the date portion of this date instance to an integer value that 
   * can be used to compare it with other dates. please note: unlike unix 
   * timestamps, the values in this integer do not correspond with seconds, 
   * rather it is a numeric representation of the year/month/day values in the 
   * following format: YYYYMMDD. this method utilizes the GMT timezone
   * @access public
   * @return int
   */
  function toInt() {
    $tz =& $this->_tz;
    $this->setTimeZone(SRA_TimeZone::getTimeZone(SRA_TIME_ZONE_GMT));
    $val = ($this->getYear() . $this->getMonth(TRUE) . $this->getDay(TRUE)) * 1;
    $this->setTimeZone($tz);
    return $val;
  }
  // }}}
  
  // {{{ toIntDateTime
  /**
   * converts this date and time for this date to an integer value that 
   * can be used to compare it with other dates. please note: unlike unix 
   * timestamps, the values in this integer do not correspond with seconds, 
   * rather it is a numeric representation of the 
   * year/month/day/hour/minute/second values in the following format: 
   * YYYYMMDDHHIISS. this method utilizes the GMT timezone
   * @access public
   * @return int
   */
  function toIntDateTime() {
    if ($this->isDateOnly()) {
      $this->setDateOnly(FALSE);
      $resetDateOnly = TRUE;
    }
    $tz =& $this->_tz;
    $this->setTimeZone(SRA_TimeZone::getTimeZone(SRA_TIME_ZONE_GMT));
    $val = ($this->getYear() . $this->getMonth(TRUE) . $this->getDay(TRUE) . $this->getHour(TRUE) . $this->getMinute(TRUE) . $this->getSecond(TRUE)) * 1;
    $this->setTimeZone($tz);
    if ($resetDateOnly) { $this->setDateOnly(TRUE); }
    return $val;
  }
  // }}}
  
  // {{{ toIntTime
  /**
   * converts the time portion of this date instance to an integer value that 
   * can be used to compare it with other dates. please note: unlike unix 
   * timestamps, the values in this integer do not correspond with seconds, 
   * rather it is a numeric representation of the hour/minute/second values in 
   * the following format: HHMMSS. 0 is returns for dateOnly type dates. this 
   * method utilizes the GMT timezone
   * @access public
   * @return int
   */
  function toIntTime() {
    $tz =& $this->_tz;
    $this->setTimeZone(SRA_TimeZone::getTimeZone(SRA_TIME_ZONE_GMT));
    $val = !$this->isDateOnly() ? ($this->getHour(TRUE) . $this->getMinute(TRUE) . $this->getSecond(TRUE)) * 1 : 0;
    $this->setTimeZone($tz);
    return $val;
  }
  // }}}
  
  // {{{ toString
  /**
   * returns this date formatted using the app-config date/time format or 
   * $this->toStringFormat if member attribute has been set
   * @param boolean $dateOnly optional parameter allowing the output to be 
   * either the $dateOnly based format (when 1 or TRUE), or the full timestamp 
   * format (when 0 or FALSE). if not specified (NULL), the format used will be 
   * based on the _dateOnly flag for this instance
   * @access public
   * @return string
   */
  function toString($dateOnly=NULL) {
    return $this->format($dateOnly === TRUE || $dateOnly === 1 ? SRA_Controller::getAppDateOnlyFormat() : ($dateOnly === FALSE || $dateOnly === 0 ? SRA_Controller::getAppDateFormat() : $this->toStringFormat));
  }
  // }}}
  
  // {{{ validateDst
  /**
   * this method advances this date/time object by 1 hour if it used a dst 
   * timezone and the time it represents resides in one of the non-existent 
   * dst times (i.e. 3/9/2008 2:00 AM will become 3/9/2008 3:00 AM for the 
   * America/Denver timezone). returns TRUE if the time was changed, false 
   * otherwise
   * @access public
   * @return boolean
   */
  function validateDst() {
    if (!$this->_init && !$this->_dateOnly && ($end = $this->_tz->getDaylightSavingsStart($this->getYear())) && $this->format('YmdH') == ($end->format('YmdH') - 1)) {
      $this->_hour++;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }
  // }}}
  
  // {{{ validateNtpServer
  /**
   * validates an ntp server using ntpq -p
   * @param string $server the server to validate
   * @access public
   * @return boolean
   */
  function validateNtpServer($server) {
    return ($ntpq = SRA_File::findInPath('ntpq', array('/sbin', '/usr/sbin'))) && preg_match('/jitter/', shell_exec("$ntpq -p -c \"timeout 1000\" $server")) ? TRUE : FALSE;
  }
  // }}}
  
}
// }}}
?>
Return current item: Sierra-php PHP Application Framework