You are here

datex_api.class.inc in Datex 7.2

Provides an API to work with dates.

File

datex_api/datex_api.class.inc
View source
<?php

/**
 * @file
 * Provides an API to work with dates.
 */

/**
 * Default behaviour of datex
 */
define('DATEX_USE_INTL', FALSE);

/**
 * Default translator is set to t() for Drupal.
 *
 * Should return name of function responsible for translating names.
 */
function _datex_get_translator() {
  return '_datex_t';
}

/**
 * Drupal translator wrapper.
 *
 * For registering new translator, You should leave this function untouched
 * and change _datex_get_translator().
 */
function _datex_t($string) {
  return t($string, array(), array(
    'context' => 'datex',
  ));
}

/**
 * A calendar class extending DatexObject should implement this interface
 * to function correctly. An example implemention is DatexJalali.
 */
interface DatexCalendarIterface {

  /**
   * Methos responsible for converting Gregorian date to localized date.
   *
   * @return array
   *   Array containing converted date, Array should be indexed from 0 to 7 in
   *   this order: year, month, day, hour, minute, month, day of year (number
   *   of days passed since day 1 of that year), day of week. Day of week MUST
   *   be from 1 to 7.
   */
  public static function convert($timestamp, $offset = 0);

  /**
   * Opposite of DatexCalendarIterface::convert().
   */
  public static function toGregorian($year = 0, $month = 0, $day = 0);

  /**
   * Will calculate whether the localized year is leap year or not.
   */
  public static function isLeap($year);

  /**
   * Returns various information used by DatexObject.
   *
   * For an example look at DatexJalali.
   *
   * Required information are:
   *   intl_args: array of arguments passed to IntlDateFormatter.
   *   First, second, third and fifth arguments of IntlDateFormatter
   *   constructor should be put in array in order.
   *
   *   numbers: Associative array of localized numbers in calendar's language.
   *   from 0 to 9 in array.
   *
   *   names: Name of dates.
   *
   *   month_days: Number of days in each month of year, NOT counting leap
   *   years.
   */
  public function getInfo($name);

  /**
   * To override format charachters DatexObject can't or shouldn't handle.
   *
   * Shoud return an associative array keyed by date format charachter and it's
   * value as formatted date (the date is available from the onject instance).
   */
  function calendarFormat();

  /**
   * Day of week calculated from given year and day of year.
   */
  public static function dayOfWeek($year, $dayOfYear = 0);

  /**
   * Provides an array containing default arguments for INTL formatter.
   */
  function getIntlArgs();

  /**
   * Helper function to make up for missing granularities.
   *
   * While converting dates, If some granularities are missing, It's needed to
   * add some amount to timestamp, To have some final conversion correct.
   * You see, Date module with only year, will store time as 2013-1-1, But it's
   * not obvious user meant 1392 or 1391?
   * Why? half of 2013 is 1392 (from march to december) and half is 1391, (from
   * january to april). But this month is lost after it's converted to 2013-1-1.
   * A fixed amount of time, (equal to choped off) amount of time, needs to be
   * added each time. I hope I could explain it well! I guess not.
   *
   * Special handling is needed for when day is present and month is not and is
   * not supported by this method.
   */
  function fixMissingGranularities(array $granuls);

}

/**
 * Base class for localized DateTime.
 */
abstract class DatexObject extends DateTime {

  /**
   * Number of days in each Gregorian month. Leap year is not counted.
   */
  protected static $daysInGregorianMonth = array(
    31,
    28,
    31,
    30,
    31,
    30,
    31,
    31,
    30,
    31,
    30,
    31,
  );

  /**
   * Default format string.
   */
  protected $formatString;

  /**
   * IntlDateFormatter instance used by object instance.
   *
   * @TODO share with objects.
   */
  protected $intlFormatter;

  /**
   * Whether to use PHP-Intl or not.
   */
  protected $useINTL = DATEX_USE_INTL;

  /**
   * Result of formats handled by extended class.
   */
  protected $calendarFormats = array();

  /**
   * Localized year value.
   */
  protected $year;

  /**
   * Localized month value.
   */
  protected $month;

  /**
   * Localized day value.
   */
  protected $day;

  /**
   * Constructor for DatexObject.
   *
   * @param mixed $datetime
   *   For list of accepted values see objectFromDate().
   * @param mixed $tz
   *   DateTimeZone to use, Can be DateTimeZone object or name string of it.
   * @param string $format
   *   Default format string.
   * @param bool $use_intl
   *   Whether to use PHP-Intl or not.
   */
  public function __construct($datetime = NULL, $tz = NULL, $format = '', $use_intl = DATEX_USE_INTL) {
    parent::__construct();
    $this
      ->setUseINTL($use_intl);
    $this
      ->setDatetime($datetime, $tz);
    $this
      ->setFormat($format);
    return $this;
  }

  /**
   * Magic Function toString.
   */
  public function __toString() {
    return $this
      ->format();
  }

  /**
   * Determines wether PECL package PHP-Intl is available or not.
   */
  public static function hasINTL() {
    return class_exists('IntlDateFormatter');
  }

  /**
   * Set whether to use PHP-Intl or not.
   *
   * Checks and makes sure PHP-Intl is available.
   */
  public function setUseINTL($use = NULL) {
    if (!$this
      ->hasINTL()) {
      $this->useINTL = FALSE;
      return;
    }
    if ($use || $use === NULL && DATEX_USE_INTL) {
      $this
        ->setIntlArgs();
      $this->useINTL = TRUE;
      $this
        ->createINTLFormatter($this
        ->getTimezone());
    }
    else {
      $this->useINTL = FALSE;
    }
    return $this->useINTL;
  }

  /**
   * Makes sure a tz is object, Not string ID of it.
   */
  public static function getTzObject($tz = NULL) {
    return is_string($tz) ? new DateTimeZone($tz) : $tz;
  }

  /**
   * Makes sure $tz is string, Not tz object.
   */
  public static function getTzString($tz) {
    return $tz instanceof DateTimeZone ? $tz
      ->getName() : $tz;
  }

  /**
   * Similar to DateTime::format().
   *
   * If format is not given internal format string set by constructor will be
   * used.
   */
  public function format($format = NULL) {
    if ($format === NULL) {
      $format = $this->formatString;
    }
    return $this->useINTL ? $this
      ->formatINTL($format) : $this
      ->formatPHP($format);
  }

  /**
   * Set date-time of object instance by extracting timestamp from given date.
   *
   * Also calls conversion methods so internal values are set.
   */
  public function setDatetime($datetime, $tz = NULL) {
    $this
      ->checkSetTimezone($tz);
    $date = $this
      ->objectFromDate($datetime);
    $this
      ->setTimestamp($date
      ->getTimestamp());
    return $this;
  }

  /**
   * Same as DateTime::setTimestamp but also resets the object.
   */
  public function setTimestamp($timestamp) {
    parent::setTimestamp($timestamp);
    $this
      ->reset();
  }

  /**
   * Set date-time of object instance by extracting timestamp from given date.
   *
   * Treat datetime as a Gregorian date.
   */
  public function xsetDatetime($datetime, $tz = NULL) {
    $this
      ->checkSetTimezone($tz);
    $date = $this
      ->xobjectFromDate($datetime);
    $this
      ->setTimestamp($date
      ->getTimestamp());
    return $this;
  }

  /**
   * Same as DateTime::setDate().
   */
  public function setDate($year = NULL, $month = NULL, $day = NULL) {
    list($year, $month, $day) = $this
      ->toGregorian($year, $month, $day);
    $this
      ->xsetDate($year, $month, $day);
    return $this;
  }

  /**
   * Set default format string of object.
   */
  public function setFormat($format) {
    $this->formatString = $format;
    return $this;
  }

  /**
   * Returns format string set by setFormat.
   */
  public function getFormat() {
    return $this->formatString;
  }

  /**
   * Sets Time Zone of internal date object.
   *
   * Accepts a DateTimeZone Object or an string representing a timezone.
   */
  public function setTimezone($timezone) {
    $timezone = $this
      ->getTzObject($timezone);
    parent::setTimezone($timezone);
    $this
      ->reset();
    return $this;
  }

  /**
   * Check to see if given timezone is not NULL, Then if it's so, set it.
   */
  protected function checkSetTimezone($tz) {
    if ($tz) {
      $this
        ->setTimezone($tz);
    }
    return $this;
  }

  /**
   * Same as DateTime::format().
   */
  public function xformat($format = NULL) {
    if ($format === NULL) {
      $format = $this->formatString;
    }
    return parent::format($format);
  }

  /**
   * Same as DateTime::setDate().
   */
  public function xsetDate($year, $month, $day) {
    return parent::setDate($year, $month, $day);
    $this
      ->reset();
    return $this;
  }

  /**
   * When new date or time is set on object, This method must be called.
   *
   * Gives a chance to extended classes to recalculate converted date from new
   * date.
   */
  public function reset($reset_datetime = NULL) {

    // If no timestamp, Then just recalculate date and set timezone.
    if ($reset_datetime !== NULL) {
      $this
        ->setTimestamp(time());
    }
    $this->tzOffset = $this
      ->xformat('Z');
    $this
      ->setConvert();
    $this
      ->setIsLeap();
    return $this;
  }

  /**
   * Returns an object containing first day of month.
   */
  public function monthFirstDay() {
    $date = clone $this;
    $date
      ->setDate(NULL, NULL, 1);
    return $date;
  }

  /**
   * Returns an object containing last day of month.
   */
  public function monthLastDay() {
    $date = clone $this;
    $date
      ->setDate(NULL, NULL, $date
      ->format('t'));
    return $date;
  }

  /**
   * Returns this date object granularities, put in an array.
   */
  public function toArray() {
    return array(
      'year' => $this
        ->format('Y'),
      'month' => $this
        ->format('n'),
      'day' => $this
        ->format('j'),
      'hour' => $this
        ->format('H'),
      'minute' => $this
        ->format('i'),
      'second' => $this
        ->format('s'),
      'timezone' => $this
        ->format('e'),
    );
  }

  /**
   * Returns amount of time difference to another date object
   *
   * @TODO properly implement.
   */
  public function difference(DatexObject $date2_in, $measure = 'seconds', $absolute = TRUE) {

    // Create cloned objects or original dates will be impacted by the
    // date_modify() operations done in this code.
    $date1 = clone $this;
    $date2 = clone $date2_in;
    $diff = $date2
      ->format('U') - $date1
      ->format('U');
    if ($diff == 0) {
      return 0;
    }
    elseif ($diff < 0 && $absolute) {

      // Make sure $date1 is the smaller date.
      $temp = $date2;
      $date2 = $date1;
      $date1 = $temp;
      $diff = $date2
        ->format('U') - $date1
        ->format('U');
    }
    $year_diff = intval($date2
      ->format('Y') - $date1
      ->format('Y'));
    switch ($measure) {
      case 'seconds':
        return $diff;
      case 'minutes':
        return $diff / 60;
      case 'hours':
        return $diff / 3600;
      case 'years':
        return $year_diff;
      case 'months':
        $item1 = $date1
          ->format('n');
        $item2 = $date2
          ->format('n');
        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':
        break;
        $item1 = $date1
          ->format('z');
        $item2 = $date2
          ->format('z');
        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':
      default:
        break;
    }
    return NULL;
  }

  /**
   * Same as DatexObject toArray but in Gregorian format.
   */
  public function xtoArray() {
    return array(
      'year' => $this
        ->xformat('Y'),
      'month' => $this
        ->xformat('n'),
      'day' => $this
        ->xformat('j'),
      'hour' => $this
        ->xformat('H'),
      'minute' => $this
        ->xformat('i'),
      'second' => $this
        ->xformat('s'),
      'timezone' => $this
        ->xformat('e'),
    );
  }

  /**
   * Converts date format string (like 'Y-m-d') to it's PHP-Intl equivilant.
   */
  public static function phpToIntl($format) {
    static $format_map = array(
      'd' => 'dd',
      'D' => 'EEE',
      'j' => 'd',
      'l' => 'EEEE',
      'N' => 'e',
      'S' => 'LLLL',
      'w' => '',
      'z' => 'D',
      'W' => 'w',
      'm' => 'MM',
      'M' => 'MMM',
      'F' => 'MMMM',
      'n' => 'M',
      't' => '',
      'L' => '',
      'o' => 'yyyy',
      'y' => 'yy',
      'Y' => 'YYYY',
      'a' => 'a',
      'A' => 'a',
      'B' => '',
      'g' => 'h',
      'G' => 'H',
      'h' => 'hh',
      'H' => 'HH',
      'i' => 'mm',
      's' => 'ss',
      'u' => 'SSSSSS',
      'e' => 'z',
      'I' => '',
      'O' => 'Z',
      'P' => 'ZZZZ',
      'T' => 'v',
      'Z' => '',
      'c' => '',
      'r' => '',
      'U' => '',
      ' ' => ' ',
      '-' => '-',
      '.' => '.',
      '-' => '-',
      ':' => ':',
    );
    $replace_pattern = '/[^ \\:\\-\\/\\.\\\\dDjlNSwzWmMFntLoyYaABgGhHisueIOPTZcrU]/';
    return strtr(preg_replace($replace_pattern, '', $format), $format_map);
  }

  /**
   * Format datetime using PHP-Intl.
   */
  protected function formatINTL($format) {
    $this->intlFormatter
      ->setPattern($this
      ->phpToIntl($format));
    $date = $this->intlFormatter
      ->format($this
      ->getTimestamp());

    // Replace localized number charachters with english equivalant.
    // @TODO check to see if Intl has support of this by itself?
    return self::decor($date, FALSE);
  }

  /**
   * Creates a DateTime object containing date extracted from $date.
   */
  public function xobjectFromDate($date = NULL, $tz = NULL) {
    if (is_object($date)) {
      $date = clone $date;
    }
    else {
      if (is_numeric($date)) {
        $date = '@' . $date;
      }
      elseif (is_array($date)) {
        $now = getdate();
        $year = isset($date['year']) ? intval($date['year']) : $now['year'];
        $month = isset($date['month']) ? intval($date['month']) : $now['mon'];
        $day = isset($date['day']) ? intval($date['day']) : $now['mday'];
        $hour = isset($date['hour']) ? intval($date['hour']) : $now['hours'];
        $minute = isset($date['minute']) ? intval($date['minute']) : $now['minutes'];
        $second = isset($date['second']) ? intval($date['second']) : $now['seconds'];
        $date = '@' . mktime($hour, $minute, $second, $month, $day, $year);
      }
      elseif ($date == NULL) {
        $date = 'now';
      }
      $date = new DateTime($date);
    }

    // For anyone reading this comment: Passing a DateTimeZone to datetime
    // constructor has no effect on it! You MUST use setTimezone to set a
    // tz on the stupid object.
    // Tested on PHP 5.4.15 (built: May 12 2013 13:11:23) Archlinux.
    if (isset($tz)) {
      $tz = $this
        ->getTzObject($tz);
      $date
        ->setTimeZone($tz);
    }
    return $date;
  }

  /**
   * Created datex object from a localized date.
   *
   * The only way to create an object from localized date is using an array,
   * Other type of dates have no difference in localized format such as
   * timestamp. String are not supported yet till parser() is fully
   * implemented.
   */
  public function objectFromDate($date, $tz = NULL) {
    if (is_array($date)) {
      foreach (array(
        'year',
        'month',
        'day',
      ) as $name) {
        if (!isset($date[$name])) {
          $date[$name] = 0;
        }
      }
      list($date['year'], $date['month'], $date['day']) = $this
        ->toGregorian($date['year'], $date['month'], $date['day']);
    }
    return $this
      ->xobjectFromDate($date, $tz);
  }

  /**
   * Create an IntlDateFormatter fot this object.
   */
  protected function createINTLFormatter($tz = NULL) {
    $args = $this->intlArgs;
    if (!$tz) {
      $tz = date_default_timezone_get();
    }
    $this->intlFormatter = new IntlDateFormatter($args[0], $args[1], $args[2], $this
      ->getTzString($tz), $args[3]);
  }

  /**
   * Use php to format date
   */
  protected function formatPHP($format) {
    static $t = FALSE;
    if (!$t) {
      $t = _datex_get_translator();
    }
    $names = $this
      ->getInfo('names');
    $monthDays = $this
      ->getInfo('month_days');
    $this
      ->setConvert();
    $this
      ->setIsLeap();

    // Should contain N, w, W, z, S, M,
    $this
      ->setCalendarFormats();
    $ampm = $this
      ->xformat('a');

    // A series of calls to str replace can not be used since format may
    // contain \ charachter which should not be replaced.
    $formatted = '';
    for ($i = 0; $i < strlen($format); $i++) {
      $f = $format[$i];

      // If extended class wants to override some formats, It should put it
      // in here.
      if (isset($this->calendarFormats[$f])) {
        $formatted .= $this->calendarFormats[$f];
      }
      else {
        switch ($f) {
          case '\\':
            $date .= $format[++$i];
          case 'd':
            $formatted .= sprintf('%02d', $this->day);
            break;
          case 'q':
            $formatted .= $t($names['day_abbr'][$this->dayOfWeek]);
            break;
          case 'D':
            $formatted .= $t($names['day_abbr_short'][$this->dayOfWeek]);
            break;
          case 'j':
            $formatted .= intval($this->day);
            break;
          case 'l':
            $formatted .= $t($names['day'][$this->dayOfWeek]);
            break;
          case 'S':
            $formatted .= $t($names['order'][$this->day - 1]);
            break;
          case 'W':
            $value_w = strval(ceil($this->dayOfYear / 7));
            $formatted = $formatted . $value_w;
            break;
          case 'z':
            $formatted = $this->dayOfYear;
            break;
          case 'M':
          case 'F':
            $formatted .= $t($names['months'][$this->month - 1]);
            break;
          case 'm':
            $formatted .= sprintf('%02d', $this->month);
            break;
          case 'n':
            $formatted .= intval($this->month);
            break;
          case 't':
            $formatted .= $this->isLeap && $this->month == 12 ? 30 : $month_days[$this->month - 1];
            break;
          case 'L':
            $formatted .= $this->isLeap ? 1 : 0;
            break;
          case 'Y':
            $formatted .= $this->year;
            break;
          case 'y':
            $formatted .= substr($this->year, 2, 4);
            break;
          case 'o':
            $formatted .= $this->year;
            break;
          case 'a':
            $formatted .= $t($names['ampm'][$ampm]);
            break;
          case 'A':
            $formatted .= $t($names['ampm'][$ampm]);
            break;
          case 'c':
            $formatted .= "{$this->year} - {$this->month} - {$this->day}T";
            $formatted .= $this
              ->xformat('H:i:sP');
            break;
          case 'r':
            $formatted .= $t($names['day_abbr'][$fw]) . ', ' . $cd . ' ' . $t($names['months'][$cm]) . ' ' . $cy . $this
              ->xformat('H:i:s P');
            break;
          default:

            // Any format charachter not handled by Datex or extended class,
            // Will be handled by DateTime.
            $formatted .= ctype_alpha($format[$i]) ? $this
              ->xformat($format[$i]) : $format[$i];
            break;
        }
      }
    }
    return $formatted;
  }

  /**
   * Call calendarFormat() on extended classes but hide storage details.
   *
   * Get extended classes opportunity to format date formats which Datex should
   * not handle.
   */
  protected function setCalendarFormats() {
    $this->calendarFormats = $this
      ->calendarFormat();
  }

  /**
   * Transliterate English numbers to localized numbers and vice versa.
   */
  public function decor($str, $decorate = FALSE) {
    static $en = array(
      0,
      1,
      2,
      3,
      4,
      5,
      6,
      7,
      8,
      9,
    );
    $localized = $this
      ->getInfo('numbers');
    return $decorate ? str_replace($en, $localized, $str) : str_replace($localized, $en, $str);
  }

  /**
   * Call extended class convert and set converted date on object instance,
   */
  protected function setConvert() {
    list($this->year, $this->month, $this->day, $this->hour, $this->minute, $this->second, $this->dayOfYear, $this->dayOfWeek) = $this
      ->convert($this
      ->xformat('U'), $this->tzOffset);
    return $this;
  }

  /**
   * Call extended class isLeap and set isLeap on object instance,
   */
  protected function setIsLeap() {
    $this->isLeap = $this
      ->isLeap($this->year);
    return $this;
  }

  /**
   * A bad method which set's day directly.
   *
   * A must have for when dealing with dates which have been stored as
   * timestamp according to a format without month but with year and
   * day.
   */
  public function setDayBad($day) {
    $this->day = $day;
    return $this;
  }

  /**
   * Generate arguments passed to INTL formatter.
   */
  public function setIntlArgs($a1 = NULL, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
    $default_intl_args = $this
      ->getIntlArgs();
    $this->intlArgs = array();
    foreach (func_get_args() as $index => $arg) {
      if ($arg === NULL) {
        $this->intlArgs[$index] = $default_intl_args[$index];
      }
      else {
        $this->intlArgs[$index] = $arg;
      }
    }
    $this->intlArgs = $default_intl_args;
  }

  /**
   * Names of date granularities.
   */
  public static function granularityNames() {
    return array(
      'year',
      'month',
      'day',
      'hour',
      'minute',
      'second',
    );
  }

  /**
   * Helper method to make up for time difference if some granularites are
   * missing.
   */
  public function fixByGranularities(array $granularities) {
    $this
      ->setTimestamp($this
      ->getTimestamp() + $this
      ->fixMissingGranularities($granularities));
  }

}

/**
 * Factory for datex object.
 */
function datex_factory($locale, $dt = NULL, $tz = NULL, $fmt = NULL, $intl = NULL) {
  switch (strtolower($locale)) {
    case 'fa':
    case 'farsi':
    case 'parsi':
    case 'persian':
    case 'jalali':
    case 'shamsi':
    case 'iran':
    case 'iranian':
      $locale = 'Jalali';
      break;
    default:
      throw new Exception('Datex: Unknown calendar.');
  }
  $datex = 'Datex' . $locale;
  return new $datex($dt, $tz, $fmt, $intl);
}

/**
 * Similar to php date, localized version.
 */
function datex_format($locale, $dt = NULL, $fmt = NULL, $tz = NULL, $intl = NULL) {
  return datex_factory($locale, $dt, $tz, $fmt, $intl)
    ->format();
}

Functions

Namesort descending Description
datex_factory Factory for datex object.
datex_format Similar to php date, localized version.
_datex_get_translator Default translator is set to t() for Drupal.
_datex_t Drupal translator wrapper.

Constants

Namesort descending Description
DATEX_USE_INTL Default behaviour of datex

Classes

Namesort descending Description
DatexObject Base class for localized DateTime.

Interfaces

Namesort descending Description
DatexCalendarIterface A calendar class extending DatexObject should implement this interface to function correctly. An example implemention is DatexJalali.