<?php
// $Id: date_api.module,v 1.44.2.43 2008/08/02 11:45:32 karens Exp $
/**
 * @file
 * This module will make the date API available to other modules.
 * Designed to provide a light but flexible assortment of functions
 * and constants, with more functionality in additional files that
 * are not loaded unless other modules specifically include them.
 */

/**
 * Set up some constants.
 *
 * Includes standard date types, format strings, strict regex strings for ISO
 * and DATETIME formats (seconds are optional).
 *
 * The loose regex will find any variety of ISO date and time, with or
 * without time, with or without dashes and colons separating the elements,
 * and with either a 'T' or a space separating date and time.
 */
define('DATE_ISO',  'date');
define('DATE_UNIX', 'datestamp');
define('DATE_DATETIME', 'datetime');
define('DATE_ARRAY', 'array');
define('DATE_OBJECT', 'object');
define('DATE_ICAL', 'ical');

define('DATE_FORMAT_ISO', "Y-m-d\TH:i:s");
define('DATE_FORMAT_UNIX', "U");
define('DATE_FORMAT_DATETIME', "Y-m-d H:i:s");
define('DATE_FORMAT_ICAL', "Ymd\THis");
define('DATE_FORMAT_DATE', 'Y-m-d');

define('DATE_REGEX_ISO', '/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):?(\d{2})?/');
define('DATE_REGEX_DATETIME', '/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):?(\d{2})?/');
define('DATE_REGEX_LOOSE', '/(\d{4})-?(\d{2})-?(\d{2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})?(\.\d+)?(Z|[\+\-]\d{2}:?\d{2})?)?/');
define('DATE_REGEX_ICAL_DATE', '/(\d{4})(\d{2})(\d{2})/');
define('DATE_REGEX_ICAL_DATETIME', '/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?/');

/**
 * Implementation of hook_menu().
 *
 * @param unknown_type $may_cache
 */
function date_api_menu($may_cache) {
  $items = array();
  if (!$may_cache) {
    drupal_add_css(drupal_get_path('module', 'date_api')  .'/date.css');
  }
  return $items;
}

/**
 * Helper function for getting the format string for a date type.
 */
function date_type_format($type) {
  switch ($type) {
    case DATE_ISO:
      return DATE_FORMAT_ISO;
    case DATE_UNIX:
      return DATE_FORMAT_UNIX;
    case DATE_DATETIME:
      return DATE_FORMAT_DATETIME;
    case DATE_ICAL:
      return DATE_FORMAT_ICAL;
  }
}

/**
 * An untranslated array of month names
 *
 * Needed for css, translation functions, strtotime(), and other places
 * that use the English versions of these words.
 *
 * @return
 *   an array of month names
 */
function date_month_names_untranslated() {
  static $month_names;
  if (empty($month_names)) {
    $month_names = 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');
  }
  return $month_names;
}

/**
 * A translated array of month names
 *
 * @param $required
 *   If not required, will include a blank value at the beginning of the list.
 * @return
 *   an array of month names
 */
function date_month_names($required = FALSE) {
  static $month_names;
  if (empty($month_names)) {
    $month_names = array();
    foreach (date_month_names_untranslated() as $key => $month) {
      $month_names[$key] = t($month);
    }
  }
  $none = array('' => '');
  return !$required ? $none + $month_names : $month_names;
}

/**
 * A translated array of month name abbreviations
 *
 * @param $required
 *   If not required, will include a blank value at the beginning of the list.
 * @return
 *   an array of month abbreviations
 */
function date_month_names_abbr($required = FALSE) {
  static $month_names;
  if (empty($month_names)) {
    $month_names = array();
    foreach (date_month_names_untranslated() as $key => $month) {
      $month_names[$key] = t(drupal_substr($month, 0, 3));
    }
  }
  $none = array('' => '');
  return !$required ? $none + $month_names : $month_names;
}

/**
 * An untranslated array of week days
 *
 * Needed for css, translation functions, strtotime(), and other places
 * that use the English versions of these words.
 *
 * @return
 *   an array of week day names
 */
function date_week_days_untranslated($refresh = TRUE) {
  static $weekdays;
  if ($refresh || empty($weekdays)) {
    $weekdays = array(0 => 'Sunday', 1 => 'Monday', 2 => 'Tuesday',
      3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday',
      6 => 'Saturday');
  }
  return $weekdays;
}

/**
 * A translated array of week days
 *
 * @param $required
 *   If not required, will include a blank value at the beginning of the array.
 * @return
 *   an array of week day names
 */
function date_week_days($required = FALSE, $refresh = TRUE) {
  static $weekdays;
  if ($refresh || empty($weekdays)) {
    $weekdays = array();
    foreach (date_week_days_untranslated($refresh) as $key => $day) {
      $weekdays[$key] = t($day);
    }
  }
  $none = array('' => '');
  return !$required ? $none + $weekdays : $weekdays;
}

/**
 * An translated array of week day abbreviations.
 *
 * @param $required
 *   If not required, will include a blank value at the beginning of the array.
 * @return
 *   an array of week day abbreviations
 */
function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) {
  if ($refresh || empty($weekdays)) {
    $weekdays = array();
    foreach (date_week_days($refresh) as $key => $day) {
      $weekdays[$key] = drupal_substr($day, 0, $length);
    }
  }
  $none = array('' => '');
  return !$required ? $none + $weekdays : $weekdays;
}

/**
 * Order weekdays
 *   Correct weekdays array so first day in array matches the first day of
 *   the week. Use to create things like calendar headers.
 *
 * @param array $weekdays
 * @return array
 */
function date_week_days_ordered($weekdays) {
  if (variable_get('date_first_day', 1) > 0) {
    for ($i = 1; $i <= variable_get('date_first_day', 1); $i++) {
      $last = array_shift($weekdays);
      array_push($weekdays, $last);
    }
  }
  return $weekdays;
}

/**
 * An array of years.
 *
 * @param int $min
 *   the minimum year in the array
 * @param int $max
 *   the maximum year in the array
 * @param $required
 *   If not required, will include a blank value at the beginning of the array.
 * @return
 *   an array of years in the selected range
 */
function date_years($min = 0, $max = 0, $required = FALSE) {
  // Have to be sure $min and $max are valid values;
  if (empty($min)) $min = intval(date('Y', time()) - 3);
  if (empty($max)) $max = intval(date('Y', time()) + 3);
  $none = array(0 => '');
  return !$required ? $none + drupal_map_assoc(range($min, $max)) : drupal_map_assoc(range($min, $max));
}

/**
 * An array of days.
 *
 * @param $required
 *   If not required, returned array will include a blank value.
 * @param integer $month (optional)
 * @param integer $year (optional)
 * @return
 *   an array of days for the selected month.
 */
function date_days($required = FALSE, $month = NULL, $year = NULL) {
  // If we have a month and year, find the right last day of the month.
  if (!empty($month) && !empty($year)) {
    $date = date_make_date($year .'-'. $month .'-01 00:00:00', 'UTC');
    $max = date_format('t', $date);
  }
  // If there is no month and year given, default to 31.
  if (empty($max)) $max = 31;
  $none = array(0 => '');
  return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max));
}

/**
 * An array of hours.
 *
 * @param string $format
 * @param $required
 *   If not required, returned array will include a blank value.
 * @return
 *   an array of hours in the selected format.
 */
function date_hours($format = 'H', $required = FALSE) {
  $hours = array();
  if ($format == 'h' || $format == 'g') {
    $min = 1;
    $max = 12;
  }
  else  {
    $min = 0;
    $max = 23;
  }
  for ($i = $min; $i <= $max; $i++) {
    $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i;
  }
  $none = array('' => '');
  return !$required ? $none + $hours : $hours;
}

/**
 * An array of minutes.
 *
 * @param string $format
 * @param $required
 *   If not required, returned array will include a blank value.
 * @return
 *   an array of minutes in the selected format.
 */
function date_minutes($format = 'i', $required = FALSE, $increment = 1) {
  $minutes = array();
  // Have to be sure $increment has a value so we don't loop endlessly;
  if (empty($increment)) $increment = 1;
  for ($i = 0; $i < 60; $i += $increment) {
    $minutes[$i] = $i < 10 && $format == 'i' ? "0$i" : $i;
  }
  $none = array('' => '');
  return !$required ? $none + $minutes : $minutes;
}

/**
 * An array of seconds.
 *
 * @param string $format
 * @param $required
 *   If not required, returned array will include a blank value.
 * @return array an array of seconds in the selected format.
 */
function date_seconds($format = 's', $required = FALSE, $increment = 1) {
  $seconds = array();
  // Have to be sure $increment has a value so we don't loop endlessly;
  if (empty($increment)) $increment = 1;
  for ($i = 0; $i < 60; $i += $increment) {
    $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i;
  }
  $none = array('' => '');
  return !$required ? $none + $seconds : $seconds;
}

/**
 * An array of am and pm options.
 * @param $required
 *   If not required, returned array will include a blank value.
 * @return array an array of am pm options.
 */
function date_ampm($required = FALSE) {
  $none = array('' => '');
  $ampm = array('am' => t('am'), 'pm' => t('pm'));
  return !$required ? $none + $ampm : $ampm;
}

/**
 * An array of short date formats.
 *
 * @return array an array of short date format strings.
 */
function date_short_formats() {
  return array(
    'Y-m-d H:i',    'm/d/Y - H:i',  'd/m/Y - H:i', 'Y/m/d - H:i',
    'd.m.Y - H:i',  'm/d/Y - g:ia', 'd/m/Y - g:ia', 'Y/m/d - g:ia',
    'M j Y - H:i',  'j M Y - H:i',  'Y M j - H:i',
    'M j Y - g:ia', 'j M Y - g:ia', 'Y M j - g:ia');
}

/**
 * An array of medium date formats.
 *
 * @return array an array of medium date format strings.
 */
function date_medium_formats() {
  return array(
    'D, Y-m-d H:i',    'D, m/d/Y - H:i',  'D, d/m/Y - H:i',
    'D, Y/m/d - H:i',  'F j, Y - H:i',    'j F, Y - H:i',
    'Y, F j - H:i',    'D, m/d/Y - g:ia', 'D, d/m/Y - g:ia',
    'D, Y/m/d - g:ia', 'F j, Y - g:ia',   'j F Y - g:ia',
    'Y, F j - g:ia',   'j. F Y - G:i');
}

/**
 * An array of long date formats.
 *
 * @return array an array of long date format strings.
 */
function date_long_formats() {
  return array(
    'l, F j, Y - H:i',  'l, j F, Y - H:i', 'l, Y,  F j - H:i',
    'l, F j, Y - g:ia', 'l, j F Y - g:ia', 'l, Y,  F j - g:ia',
    'l, j. F Y - G:i');
}

/**
 * Array of regex replacement strings for date format elements.
 * Used to allow input in custom formats. Based on work done for
 * the Date module by Yves Chedemois (yched).
 *
 * @return array of date() format letters and their regex equivalents.
 */
function date_format_patterns() {
  return array(
    'd' => '\d{2}',    'j' => '\d{1,2}',    'N' => '\d',      'S' => '\w{2}',
    'w' => '\d',       'z' => '\d{1,3}',    'W' => '\d{1,2}', 'm' => '\d{2}',
    'n' => '\d{1,2}',  't' => '\d{2}',      'L' => '\d',      'o' => '\d{4}',
    'Y' => '\d{4}',    'y' => '\d{2}',      'B' => '\d{3}',   'g' => '\d{1,2}',
    'G' => '\d{1,2}',  'h' => '\d{2}',      'H' => '\d{2}',   'i' => '\d{2}',
    's' => '\d{2}',    'e' => '\w*',        'I' => '\d',      'T' => '\w*',
    'U' => '\d*',      'z' => '[+-]?\d*',   'O' => '[+-]?\d{4}',
    //Using S instead of w and 3 as well as 4 to pick up non-ASCII chars like German umlaute
    'D' => '\S{3,4}',    'l' => '\S*', 'M' => '\S{3,4}', 'F' => '\S*',
    'P' => '[+-]?\d{2}\:\d{2}',
    'c' => '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]?\d{2}\:\d{2})',
    'r' => '(\w{3}), (\d{2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):(\d{2})([+-]?\d{4})?',
    );
}

/**
 * Array of granularity options and their labels
 *
 * @return array
 */
function date_granularity_names() {
  return array('year' => t('Year'), 'month' => t('Month'), 'day' => t('Day'),
    'hour' => t('Hour'), 'minute' => t('Minute'), 'second' => t('Second'),
    );
}

/**
 * A translated array of timezone names.
 * Cache the untranslated array, make the translated array a static variable.
 *
 * @param $required
 *   If not required, returned array will include a blank value.
 * @return
 *   an array of timezone names
 */
function date_timezone_names($required = FALSE, $refresh = FALSE) {
  static $zonenames;
  if (empty($zonenames)) {
    $zonenames = array();
    $cached = cache_get('date_timezone_identifiers_list', 'cache');
    if ($refresh || !$data = unserialize($cached->data)) {
      $data = timezone_identifiers_list();
    }
    foreach ($data as $delta => $zone) {
      // Because many time zones exist in PHP only for backward 
      // compatibility reasons and should not be used, the list is 
      // filtered by a regular expression.
      if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) {
        $zonenames[$zone] = t($zone);
      }      
      else {
        unset($data[$delta]);
      }
    }
    if (!empty($data)) {
      cache_set('date_timezone_identifiers_list', 'cache', serialize($data));
    }
  }
  $none = array('' => '');
  return !$required ? $none + $zonenames : $zonenames;
}

/**
 * An array of timezone abbreviations that the system allows.
 * Cache an array of just the abbreviation names because the
 * whole timezone_abbreviations_list is huge so we don't want
 * to get it more than necessary.
 *
 * @return array
 */
function date_timezone_abbr($refresh = FALSE) {
  $cached = cache_get('date_timezone_abbreviations', 'cache');
  $data = unserialize($cached->data);
  if (empty($data) || $refresh) {
    $data = array_keys(timezone_abbreviations_list());
    cache_set('date_timezone_abbreviations', 'cache', serialize($data));
  }
  return $data;
}

/**
 * A version of the t() function for date parts that need translation.
 *
 * Run this over results of functions which do no translation of
 * month and day names, like date() and date_format().
 *
 * @param string $string
 * @return translated value of the string
 */
function date_t($string) {
  static $replace;
  if (empty($replace)) {
    $replace = array();
    // Translate the whole name first, then look for abbreviations.
    foreach (date_month_names_untranslated() as $month) {
      $replace[$month] = t($month);
      $replace[drupal_substr($month, 0, 3)] = t(drupal_substr($month, 0, 3));
    }
    foreach (date_week_days_untranslated() as $day) {
      $replace[$day] = t($day);
      $replace[drupal_substr($day, 0, 3)] = t(drupal_substr($day, 0, 3));
    }
  }
  return strtr($string, $replace);
}

/**
 * An override for interval formatting that adds past and future context
 *
 * @param DateTime $date
 * @param integer $granularity
 * @return formatted string
 */
function date_format_interval($date, $granularity = 2) {
  $interval = time() - date_format($date, 'U');
  if($interval > 0 ) {
    return t('!time ago', array('!time' => format_interval($interval, $granularity)));
  } else {
    return format_interval(abs($interval), $granularity);
  }
}

/**
 * Reworked from Drupal's format_date function to handle pre-1970 and
 * post-2038 dates and accept a date object instead of a timestamp as input.
 *
 * Translates formatted date results, unlike PHP function date_format().
 *
 * @param $oject
 *   A date object, could be created by date_make_date().
 * @param $type
 *   The format to use. Can be "small", "medium" or "large" for the preconfigured
 *   date formats. If "custom" is specified, then $format is required as well.
 * @param $format
 *   A PHP date format string as required by date(). A backslash should be used
 *   before a character to avoid interpreting the character as part of a date
 *   format.
 * @return
 *   A translated date string in the requested format.
 */
function date_format_date($object, $type = 'medium', $format = '') {
  if (empty($object)) {
    return '';
  }
  switch ($type) {
    case 'small':
      $format = variable_get('date_format_short', 'm/d/Y - H:i');
      break;
    case 'large':
      $format = variable_get('date_format_long', 'l, F j, Y - H:i');
      break;
    case 'custom':
      $format = $format;
      break;
    case 'medium':
    default:
      $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
  }
  return date_t(date_format($object, $format));
}

/**
 * A date object for the current time.
 *
 * @param $timezone
 *   Optional method to force time to a specific timezone,
 *   defaults to user timezone, if set, otherwise site timezone.
 * @return object date
 */
function date_now($timezone = NULL) {
  return date_make_date('now', $timezone);
}

/**
 *  Convert a date of any type or an array of date parts into a valid date
 *  object.

 *  @param $date
 *    A date in any format or the string 'now'.
 *  @param $timezone
 *    Optional, the name of the timezone this date is in, defaults
 *    to the user timezone, if set, otherwise the site timezone.
 *    Accepts either a timezone name or a timezone object as input.
 *  @param $type
 *    The type of date provided, could be
 *    DATE_ARRAY, DATE_UNIX, DATE_DATETIME, DATE_ISO, or DATE_OBJECT.
 */
function date_make_date($date, $timezone = NULL, $type = DATE_DATETIME) {
  // Make sure some value is set for the date and timezone even if the
  // site timezone is not yet set up to avoid fatal installation
  // errors.
  if (empty($timezone)) {
    $timezone = date_default_timezone_name();
  }
  if (!date_is_valid($date, $type) || empty($date)) {
    $date = 'now';
  }
  if (!empty($timezone) && !empty($date)) {
    if (!is_object($timezone)) {
      $timezone = timezone_open($timezone);
    }
    if ($date == 'now') {
      return date_create('now', $timezone);
    }
    elseif ($datetime = date_convert($date, $type, DATE_DATETIME)) {
      if ($type == DATE_UNIX && timezone_name_get($timezone) != 'UTC') {
        $date = date_create($datetime, timezone_open('UTC'));
        date_timezone_set($date, $timezone);
        return $date;
      }
      else {
        return date_create($datetime, $timezone);
      }
    }
  }
  return NULL;
}

/**
 * Return a timezone name to use as a default.
 *
 * @return a timezone name
 *   Identify the default timezone for a user, if available, otherwise the site.
 *   Must return a value even if no timezone info has been set up.
 */
function date_default_timezone_name($check_user = TRUE) {
  global $user;
  if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone_name)) {
    return $user->timezone_name;
  }
  else {
    $default = variable_get('date_default_timezone_name', '');
    return empty($default) ? 'UTC' : $default;
  }
}

/**
 * A timezone object for the default timezone.
 *
 * @return a timezone name
 *   Identify the default timezone for a user, if available, otherwise the site.
 */
function date_default_timezone($check_user = TRUE) {
  $timezone = date_default_timezone_name($check_user);
  return timezone_open(date_default_timezone_name($check_user));
}

/**
 * Identify the number of days in a month for a date.
 *
 * @param mixed $date
 * @return integer
 */
function date_days_in_month($date = NULL, $type = DATE_OBJECT) {
  if (empty($date)) {
    $date = date_now();
    $type = DATE_OBJECT;
  }
  $date = date_convert($date, $type, DATE_OBJECT);
  if (is_object($date)) {
    return date_format($date, 't');
  }
  return NULL;
}

/**
 * Identify the number of days in a year for a date.
 *
 * @param mixed $date
 * @param string $type
 * @return integer
 */
function date_days_in_year($date = NULL, $type = DATE_OBJECT) {
  if (empty($date)) {
    $date = date_now();
    $type = DATE_OBJECT;
  }
  $date = date_convert($date, $type, DATE_OBJECT);
  if (is_object($date)) {
    if (date_format($date, 'L')) {
      return 366;
    }
    else {
      return 365;
    }
  }
  return NULL;
}

/**
 * Identify the number of ISO weeks in a year for a date.
 *
 * December 28 is always in the last ISO week of the year.
 *
 * @param mixed $date
 * @param string $type
 * @return integer
 */
function date_iso_weeks_in_year($date = NULL, $type = DATE_OBJECT) {
  if (empty($date)) {
    $date = date_now();
    $type = DATE_OBJECT;
  }
  $date = date_convert($date, $type, DATE_OBJECT);
  if (is_object($date)) {
    date_date_set($date, date_format($date, 'Y'), 12, 28);
    return date_format($date, 'W');
  }
  return NULL;
}

/**
 * Returns day of week for a given date (0 = Sunday).
 *
 * @param mixed  $date
 *   a date, default is current local day
 * @param string  $type
 *   The type of date, DATE_ISO, DATE_DATETIME, or DATE_UNIX
 * @return
 *    the number of the day in the week
 */
function date_day_of_week($date = NULL, $type = DATE_OBJECT) {
  if (empty($date)) {
    $date = date_now();
    $type = DATE_OBJECT;
  }
  $date = date_convert($date, $type, DATE_OBJECT);
  if (is_object($date)) {
    return date_format($date, 'w');
  }
  return NULL;
}

/**
 * Returns translated name of the day of week for a given date.
 *
 * @param mixed  $date
 *   a date, default is current local day
 * @param string  $type
 *   The type of date, DATE_ISO, DATE_DATETIME, or DATE_UNIX
 * @param string $abbr
 *   Whether to return the abbreviated name for that day
 * @return
 *    the name of the day in the week for that date
 */
function date_day_of_week_name($date = NULL, $abbr = TRUE, $type = DATE_DATETIME) {
  $dow = date_day_of_week($date, $type);
  $days = $abbr ? date_week_days_abbr() : date_week_days();
  return $days[$dow];
}

/**
 * Compute difference between two days using a given measure.
 *
 * @param mixed $date1
 *   the starting date
 * @param mixed $date2
 *   the ending date
 * @param string $measure
 *   'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'
 * @param string $type
 *   the type of dates provided:
 *   DATE_OBJECT, DATE_DATETIME, DATE_ISO, DATE_UNIX, DATE_ARRAY
 */
function date_difference($date1_in, $date2_in, $measure = 'seconds', $type = DATE_OBJECT) {
  // Create cloned objects or original dates will be impacted by
  // the date_modify() operations done in this code.
  $date1 = drupal_clone(date_convert($date1_in, $type, DATE_OBJECT));
  $date2 = drupal_clone(date_convert($date2_in, $type, DATE_OBJECT));
  if (is_object($date1) && is_object($date2)) {
    $diff = date_format($date2, 'U') - date_format($date1, 'U');
    if ($diff == 0 ) {
      return 0;
    }
    elseif ($diff < 0) {
      // Make sure $date1 is the smaller date.
      $temp = $date2;
      $date2 = $date1;
      $date1 = $temp;
      $diff = date_format($date2, 'U') - date_format($date1, 'U');
    }
    $year_diff = intval(date_format($date2, 'Y') - date_format($date1, 'Y'));
    switch ($measure) {

      // The easy cases first.
      case 'seconds':
        return $diff;
      case 'minutes':
        return $diff * 60;
      case 'hours':
        return $diff * 24 * 60;
      case 'years':
        return $year_diff;

      case 'months':
        $format = 'n';
        $item1 = date_format($date1, $format);
        $item2 = date_format($date2, $format);
        if ($year_diff == 0) {
          return intval($item2 - $item1);
        }
        else {
          $item_diff = 12 - $item1;
          $item_diff += intval(($year_diff - 1) * 12);
          return $item_diff + $item2;
       }
       break;

      case 'days':
        $format = 'z';
        $item1 = date_format($date1, $format);
        $item2 = date_format($date2, $format);
        if ($year_diff == 0) {
          return intval($item2 - $item1);
        }
        else {
          $item_diff = date_days_in_year($date1) - $item1;
          for ($i = 1; $i < $year_diff; $i++) {
            date_modify($date1, '+1 year');
            $item_diff += date_days_in_year($date1);
          }
          return $item_diff + $item2;
       }
       break;

      case 'weeks':
        $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
        $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
        for ($i = 1; $i <= $year_diff; $i++) {
          date_modify($date1, '+1 year');
          $week_diff += date_iso_weeks_in_year($date1);
        }
        return $week_diff;
    }
  }
  return NULL;
}

/**
 * Start and end dates for a calendar week, adjusted to use the
 * chosen first day of week for this site.
 */
function date_week_range($week, $year) {
  $min_date = date_make_date($year .'-01-01 00:00:00', date_default_timezone_name());
  date_timezone_set($min_date, date_default_timezone());

  // move to the right week
  date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');

  // move backwards to the first day of the week
  $first_day = variable_get('date_first_day', 0);
  $day_wday = date_format($min_date, 'w');
  date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');

  // move forwards to the last day of the week
  $max_date = drupal_clone($min_date);
  date_modify($max_date, '+7 days');

  if (date_format($min_date, 'Y') != $year) {
    $min_date = date_make_date($year .'-01-01 00:00:00', date_default_timezone());
  }
  return array($min_date, $max_date);
}

/**
 * The number of calendar weeks in a year.
 * 
 * PHP week functions return the ISO week, not the calendar week.
 *
 * @param int $year
 * @return int number of calendar weeks in selected year.
 */
function date_weeks_in_year($year) {
  $date = date_make_date(($year + 1) . '-01-01 12:00:00', 'UTC');
  date_modify($date, '-1 day');
  return date_week(date_format($date, 'Y-m-d'));
}

/**
 * The calendar week number for a date.
 * 
 * PHP week functions return the ISO week, not the calendar week.
 *
 * @param string $date, in the format Y-m-d
 * @return int calendar week number.
 */
function date_week($date) {
  $parts = explode('-', $date);

  $date = date_make_date($date . ' 12:00:00', 'UTC');
  $year_date = date_make_date($parts[0] . '-01-01 12:00:00', 'UTC');

  $week = intval(date_format($date, 'W'));
  $year_week = intval(date_format($year_date, 'W'));
  $date_year = intval(date_format($date, 'o'));

  // remove the leap week if it's present
  if ($date_year > intval($parts[0])) {
    $last_date = drupal_clone($date);
    date_modify($last_date, '-7 days');
    $week = date_format($last_date, 'W') + 1;
  } else if ($date_year < intval($parts[0])) {
    $week = 0;
  }
  if ($year_week != 1) $week++;

  // convert to ISO-8601 day number, to match weeks calculated above
  $iso_first_day = 1 + (variable_get('date_first_day', 0) + 6) % 7;

  // if it's before the starting day, it's the previous week
  if (intval(date_format($date, 'N')) < $iso_first_day) $week--;
  // if the year starts before, it's an extra week at the beginning
  if (intval(date_format($year_date, 'N')) < $iso_first_day) $week++;

  return $week;
}

/**
 * Date conversion helper function.
 *
 * A variety of ways to convert dates from one type to another.
 * No timezone conversion is done in this operation!!
 *
 * Example: date_convert('2007-03-15 08:30', DATE_DATETIME, DATE_UNIX);
 *  returns unix value for the supplied date.
 *
 * @param mixed $date
 *   the date to convert
 * @param string $from_type
 *   the type of date to convert from
 * @param string $to_type
 *   the type of date to convert to
 */
function date_convert($date, $from_type, $to_type) {
  if (empty($date) && !$date === 0) return NULL;
  if (empty($from_type) || empty($to_type) || $from_type == $to_type) return $date;
  switch ($from_type) {
    case DATE_ARRAY:
      if (!is_array($date)) return NULL;
      $datetime = date_pad(intval($date['year']), 4) .'-'. date_pad(intval($date['month'])) .
            '-'. date_pad(intval($date['day'])) .' '. date_pad(intval($date['hour'])) .
            ':'. date_pad(intval($date['minute'])) .':'. date_pad(intval($date['second']));
      switch ($to_type) {
        case DATE_ISO:
          return str_replace(' ', 'T', $datetime);
        case DATE_DATETIME:
          return $datetime;
        case DATE_ICAL:
          $replace = array(' ' => 'T', '-' => '', ':' => '');
          return strtr($datetime, $replace);
        case DATE_OBJECT:    
          return date_create($datetime, timezone_open('UTC'));
        case DATE_UNIX:
          $obj = date_create($datetime, timezone_open('UTC'));
          return date_format($obj, 'U');  
      }
      break;
    case DATE_OBJECT:
      if (!is_object($date)) return NULL;
      $obj = $date;
      break;
    case DATE_DATETIME:
    case DATE_ISO:
      if (!preg_match(DATE_REGEX_LOOSE, $date)) return NULL;
      $date = date_fuzzy_datetime($date);
      $obj = date_create($date, timezone_open('UTC'));
      break;
    case DATE_ICAL:
      if (!preg_match(DATE_REGEX_LOOSE, $date)) return NULL;
      preg_match(DATE_REGEX_LOOSE, $date, $regs);
      $datetime = date_pad($regs[1], 4) .'-'. date_pad($regs[2]) .'-'. date_pad($regs[3]) .
        'T'. date_pad($regs[5]) .':'. date_pad($regs[6]) .':'. date_pad($regs[7]);
      $obj = date_create($datetime, timezone_open('UTC'));
    case DATE_UNIX:
      if (!is_numeric($date)) return NULL;
      $obj = date_create("@$date", timezone_open('UTC'));
      break;
  }
  switch ($to_type) {
    case DATE_OBJECT:
      return $obj;
    case DATE_DATETIME:
      return date_format($obj, DATE_FORMAT_DATETIME);
    case DATE_ISO:
      return date_format($obj, DATE_FORMAT_ISO);
    case DATE_ICAL:
      return date_format($obj, DATE_FORMAT_ICAL);
    case DATE_UNIX:
      return date_format($obj, 'U');
    case DATE_ARRAY:
      $date_array = date_array($obj);
      // ISO dates may contain zero values for some date parts,
      // make sure they don't get lost in the conversion.
      if ($from_type == DATE_ISO) {
        $date_array = array_merge($date_array, date_iso_array($date));
      }
      return $date_array;
    default:
      return NULL;
  }
}

/**
 * Create valid datetime value from incomplete ISO dates or arrays.
 */
function date_fuzzy_datetime($date) {
  // A text ISO date, like MMMM-YY-DD HH:MM:SS
  if (!is_array($date)) {
    $date = date_iso_array($date);
  }
  // An date/time value in the format:
  //  array('date' => MMMM-YY-DD, 'time' => HH:MM:SS).
  elseif (array_key_exists('date', $date) || array_key_exists('time', $date)) {
    $date_part = array_key_exists('date', $date) ? $date['date'] : '';
    $time_part = array_key_exists('time', $date) ? $date['time'] : '';
    $date = date_iso_array(trim($date_part .' '. $time_part));
  }
  // Otherwise date must in in format:
  //  array('year' => YYYY, 'month' => MM, 'day' => DD).
  if (empty($date['year'])) {
    $date['year'] = date('Y');
  }
  if (empty($date['month'])) {
    $date['month'] = 1;
  }
  if (empty($date['day'])) {
    $date['day'] = 1;
  }
  $value = date_pad($date['year'], 4) .'-'. date_pad($date['month']) .'-'. 
    date_pad($date['day']) .' '. date_pad($date['hour']) .':'. 
    date_pad($date['minute']) .':'. date_pad($date['second']);
  return $value;
}

/**
 * Create an array of date parts from an ISO date.
 */
function date_iso_array($date) {
  preg_match(DATE_REGEX_LOOSE, $date, $regs);
  return array(
    'year' => isset($regs[1]) ? intval($regs[1]) : '',
    'month' => isset($regs[2]) ? intval($regs[2]) : '',
    'day' => isset($regs[3]) ? intval($regs[3]) : '',
    'hour' => isset($regs[5]) ? intval($regs[5]) : '',
    'minute' => isset($regs[6]) ? intval($regs[6]) : '',
    'second' => isset($regs[7]) ? intval($regs[7]) : '',
    );        
}

/**
 * Create an array of values from a date object. Structured like the
 * results of getdate() but not limited to the 32-bit signed range.
 *
 * @param object $obj
 * @return array
 */
function date_array($obj) {
  $year = intval(date_format($obj, 'Y'));
  $dow = date_format($obj, 'w');
  $days = date_week_days();
  return array(
    'second' => (integer) date_format($obj, 's'),
    'minute' => (integer) date_format($obj, 'i'),
    'hour' => date_format($obj, 'G'),
    'day' => date_format($obj, 'j'),
    'wday' => $dow,
    'month' => date_format($obj, 'n'),
    'year' => date_format($obj, 'Y'),
    'yday' => date_format($obj, 'z'),
    'weekday' => $days[$dow],
    'month_name' => date_format($obj, 'F'),
    0 => date_format($obj, 'U'));
}

/**
 * Extract integer value of any date part from any type of date.
 *
 * Example:
 *   date_part_extract('2007-03-15 00:00', 'month', DATE_DATETIME)
 *   returns: 3
 *
 * @param mixed $date
 *   the date value to analyze.
 * @param string $part
 *   the part of the date to extract, 'year', 'month', 'day', 'hour', 'minute', 'second'
 * @param string $type
 *   the type of date supplied, DATE_ISO, DATE_UNIX, DATE_DATETIME, or DATE_OBJECT;
 * @return integer
 *   the integer value of the requested date part.
 */
function date_part_extract($date, $part, $type = DATE_DATETIME) {
  $formats = array('year' => 'Y', 'month' => 'n', 'day' => 'j',
    'hour' => 'G', 'minute' => 'i', 'second' => 's');
  $positions = array('year' => 0, 'month' => 5, 'day' => 8,
    'hour' => 11, 'minute' => 14, 'second' => 17);
  $ipositions = array('year' => 0, 'month' => 4, 'day' => 6,
    'hour' => 9, 'minute' => 11, 'second' => 13);
  switch ($type) {
    case DATE_ARRAY:
      return (integer) $date[$part];
    case DATE_DATETIME:
    case DATE_ISO:
      return (integer) substr($date, $positions[$part], $part == 'year' ? 4 : 2);
    case DATE_ICAL:
      return (integer) substr($date, $ipositions[$part], $part == 'year' ? 4 : 2);
    case DATE_UNIX:
      $date = date_create("@$date", timezone_open('UTC'));
      return date_format($date, $formats[$part]);
    case DATE_OBJECT:
      return date_format($date, $formats[$part]);
  }
 }

/**
 *  Functions to test the validity of a date in various formats.
 *  Has special case for ISO dates and arrays which can be missing
 *  month and day and still be valid.
 *
 *  @param $type
 *    could be DATE_ARRAY, DATE_UNIX, DATE_DATETIME, DATE_ISO, or DATE_OBJECT
 */
function date_is_valid($date, $type = DATE_DATETIME) {
  if (empty($date)) return FALSE;
  if ($type == DATE_OBJECT && !is_object($date)) return FALSE;
  if (($type == DATE_ISO || $type == DATE_DATETIME) && (!is_string($date) || !preg_match(DATE_REGEX_LOOSE, $date))) return FALSE;
  if ($type == DATE_UNIX and !is_numeric($date)) return FALSE;
  if ($type == DATE_ARRAY and !is_array($date)) return FALSE;
  
  // If checkdate works, no need for further tests.
  // Make sure integer values are sent to checkdate.
  $year = intval(date_part_extract($date, 'year', $type));
  $month = intval(date_part_extract($date, 'month', $type));
  $day = intval(date_part_extract($date, 'day', $type));
  if (!checkdate($month, $day, $year)) {
    // ISO dates and arrays can have empty date parts
    if ($type == DATE_ISO || $type == DATE_ARRAY) {
      if (variable_get('date_max_year', 4000) < $year || variable_get('date_min_year', 1) > $year
        || 12 < $month || 0 > $month || 31 < $day || 0 > $day) {
        return FALSE;
      }
    }
    // Unix and datetime are expected to have at least a year, month, and day.
    // This test is needed to test very old dates in PHP 4, where checkdate won't work.
    // @TODO - this can be removed once everyone is using PHP 5.
    elseif (variable_get('date_max_year', 4000) < $year || variable_get('date_min_year', 1) > $year
      || 12 < $month || 1 > $month || 31 < $day || 1 > $day) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Helper function to left pad date parts with zeros.
 * Provided because this is needed so often with dates.
 *
 * @param int $value
 *   the value to pad
 * @param int $size
 *   total size expected, usually 2 or 4
 * @return string the padded value
 */
function date_pad($value, $size = 2) {
  return sprintf("%0". $size ."d", $value);
}

/**
 *  Function to figure out if any time data is to be collected or displayed.
 *
 *  @param granularity
 *    an array like ('year', 'month', 'day', 'hour', 'minute', 'second');
 */
function date_has_time($granularity) {
  if (!is_array($granularity)) $granularity = array();
  return sizeof(array_intersect($granularity, array('hour', 'minute', 'second'))) > 0 ? TRUE : FALSE;
}

/**
 * Recalculate a date so it only includes elements from a granularity
 * array. Helps prevent errors when unwanted values round up and ensures
 * that unwanted date part values don't get stored in the database.
 *
 * Example:
 *   date_limit_value('2007-05-15 04:45:59', array('year', 'month', 'day'))
 *   returns '2007-05-15 00:00:00'
 *
 * @param $date
 *   a date value
 * @param $granularity
 *   an array of allowed date parts, like ('year', 'month', 'day', 'hour', 'minute', 'second');
 * @param $type
 *   the type of date value provided,
 *   DATE_DATETIME, DATE_ISO, DATE_UNIX, or DATE_ARRAY
 * @return
 *   the date with the unwanted parts reset to zeros (or ones if zeros are
 *   invalid for that date type).
*/
function date_limit_value($date, $granularity, $type = DATE_DATETIME) {
  if (!date_is_valid($date, $type) || !$nongranularity = date_nongranularity($granularity)) {
   return $date;
  }
  else {
    $date = date_convert($date, $type, DATE_ARRAY);
    foreach ($nongranularity as $level) {
      switch ($level) {
        case 'second':
          $date['second'] = 0;
          break;
        case 'minute':
          $date['minute'] = 0;
          break;
        case 'hour':
          $date['hour'] = 0;
          break;
        case 'month':
          $date['month'] = $type != DATE_ISO ? 1 : 0;
          break;
        case 'day':
          $date['day'] = $type != DATE_ISO ? 1 : 0;
          break;
       }
    }
    return date_convert($date, DATE_ARRAY, $type);
  }
}

/**
 * Rewrite a format string so it only inludes elements from a
 * specified granularity array.
 *
 * Example:
 *   date_limit_format('F j, Y - H:i', array('year', 'month', 'day'));
 *   returns 'F j, Y'
 *
 * @param $format
 *   a format string
 * @param $granularity
 *   an array of allowed date parts, all others will be removed
 *   array('year', 'month', 'day', 'hour', 'minute', 'second');
 * @return
 *   a format string with all other elements removed
 */
function date_limit_format($format, $granularity) {
  // Get rid of dash separating date and time if either is missing.
  if (!date_has_time($granularity)
    || sizeof(array_intersect($granularity, array('year', 'month', 'day')) == 0)) {
    $regex[] = '( -)';
  }
  if (!date_has_time($granularity)) {
      $regex[] = '((?<!\\\\)[a|A])';
  }
  // Create regular expressions to remove selected values from string.
  // Use (?<!\\\\) to keep escaped letters from being removed.
  foreach (date_nongranularity($granularity) as $element) {
    switch ($element) {
      case 'year':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
        break;
      case 'day':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[lDdj])';
        break;
      case 'month':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
        break;
      case 'hour':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
        break;
      case 'minute':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
        break;
      case 'second':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
        break;
      case 'timezone':
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[OZPe])';
        break;
    
    }
  }
  // Remove selected values from string.
  $format = trim(preg_replace($regex, array(), $format));
  // Remove orpaned punctuation at the beginning of the string.
  $format = preg_replace('`^([\-/\.,:])`', '', $format);
  // Remove orpaned punctuation at the end of the string.
  $format = preg_replace('([\-/\.,:]$)', '', $format);
  return trim($format);
}

/**
 * Convert a format to an ordered array of granularity parts.
 *
 * Example:
 *   date_format_order('m/d/Y H:i')
 *   returns
 *     array(
 *       0 => 'month',
 *       1 => 'day',
 *       2 => 'year',
 *       3 => 'hour',
 *       4 => 'minute',
 *     );
 *
 * @param string $format
 * @return array of ordered granularity elements in this format string
 */
function date_format_order($format) {
  $max = strlen($format);
  $order = array();
  for ($i = 0; $i <= $max; $i++) {
    $c = $format[$i];
    switch ($c) {
      case 'd':
      case 'j':
        $order[] = 'day';
        break;
      case 'F':
      case 'M':
      case 'm':
      case 'n':
        $order[] = 'month';
        break;
      case 'Y':
      case 'y':
        $order[] = 'year';
        break;
      case 'g':
      case 'G':
      case 'h':
      case 'H':
        $order[] = 'hour';
        break;
      case 'i':
        $order[] = 'minute';
        break;
      case 's':
        $order[] = 'second';
        break;
    }
  }
  return $order;
}

/**
 * An difference array of granularity elements that are NOT in the
 * granularity array. Used by functions that strip unwanted
 * granularity elements out of formats and values.
 *
 * @param $granularity
 *   an array like ('year', 'month', 'day', 'hour', 'minute', 'second');
 */
function date_nongranularity($granularity) {
  return array_diff(array('year', 'month', 'day', 'hour', 'minute', 'second', 'timezone'), (array) $granularity);
}

/**
 * Implementation of hook_simpletest().
 */
function date_api_simpletest() {
  $dir = drupal_get_path('module', 'date_api') .'/tests';
  $tests = file_scan_directory($dir, '\.test$');
  return array_keys($tests);
}

/**
 * Implementation of hook_elements().
 */
function date_api_elements() {
  include_once(drupal_get_path('module', 'date_api') .'/date_api_elements.inc');
  return _date_api_elements();
}


/**
 * Wrapper around date handler setting for timezone.
 */
function date_api_set_db_timezone($offset = '+00:00') {
  include_once(drupal_get_path('module', 'date_api') .'/date_api_sql.inc');
  $handler = new date_sql_handler();
  return $handler->set_db_timezone($offset);
}

/**
 * Backport of drupal_json().
 */
function date_api_json($var = NULL) {
  // We are returning javascript, so tell the browser.
  drupal_set_header('Content-Type: text/javascript; charset=utf-8');
  if (isset($var)) {
    echo drupal_to_js($var);
  }
}

/**
 * Avoid problem with bad replacement values for %%d, fixed in Views
 * in January, but not yet available in an offical release.
 * 
 * TODO Once the Views fix gets in an official release, this can
 * be removed.
 */
function date_api_views_query_substitutions($view) {
  return array('***SQLD***' => '%%d', '***SQLS***' => '%%s');  
}

/**
 * Store personalized format options for each user.
 *
 * Store to avoid work of re-creating it over and over,
 * personalize for each user to preserve translation.
 *
 * @return array
 */
function date_format_options() {
  global $user;
  $cached = cache_get('date_format_options:'. $user->uid);
  $options = unserialize($cached->data);
  if (empty($options)) {
    $formats = array_merge(date_short_formats(), date_medium_formats(), date_long_formats());
    $options = array();
    $now = date_example_date();
    if (!empty($now)) {
      foreach ($formats as $format) {
        // Create an option that shows date only without time, along with the
        // default string which has both date and time.
        $no_time = date_limit_format($format, array('month', 'day', 'year'));
        $zones = array('', 'O', 'P', 'e');
        foreach ($zones as $zone) {
          $time_format = !empty($zone) ? $format .' '. $zone : $format;
          $options[$no_time] = date_format_date($now, 'custom', $no_time);
          $options[$time_format] = date_format_date($now, 'custom', $time_format);
        }
      }
      asort($options);
    }
    if (!empty($options)) {
      cache_set('date_format_options:'. $user->uid, 'cache', serialize($options), CACHE_TEMPORARY);
    }
  }
  return $options;
}