PHP Date/Time Class

This is a PHP Date/Time class implementation that allows you to use dates beyond or before the UNIX date limitations (UNIX epoch.) It uses the doomsday algorithm to calculate days of the week and there is a class included specifically for that function. Note this is still officially beta. However at this point it looks pretty good.

Please feel free to contact me if you’re planning on trying it out. Use the contact form on the left. If you find any bugs or recommend any advancements, please drop me a line. However, please note the todo list at the start of the class for items that are still coming.

<?

/*
 *
 * Copyright (C) 2005  James Bly
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Version 0.9
 *
 * Todo (by priority):
 *   Validation of input (invalid dates, leap year mistakes, etc)
 *   Definition of missing parameters in date() (not all from PHP are mirrored)
 *   Implement timezone and GMT offset handling functions
 *
 * WARNING: This code is beta and shouldn't be used for critical
 * production purposes. It also might have holes and security
 * issues as only a cursory review has been done prior to its
 * release. Use it at your own risk.
 *
 */


/*
 *
 * Construction:
 *   new DateTime($datestr, $datefmt);
 * 
 *   $datestr - This is the string that identifies the date
 *   $datefmt - This helps identify locality. Is it m-d-y, or d-m-y.
 *              This variable is in the form "mdy" or "dmy" etc. It
 *              will default to mdy, unless the first parameter is
 *              4 digits long, in which case it will go to ymd.
 * 
 *
 */

class DateTime
{


  public $datestr;
  public $datefmt;

  // $n vals = numeric
  public $ndayofweek;
  public $nmonth;
  public $nday;
  public $nyear;

  // $a vals = abbreviated text
  public $adayofweek;
  public $amonth;
  public $aday;
  public $ayear;

  // $l vals = long text
  public $ldayofweek;
  public $lmonth;
  public $lday;
  public $lyear;
  // $t vals = time defaults 24 hour
  public $thour = 00; 
  public $tmin = 00;
  public $tsec = 00;
  public $tprec = 00;
  public $tz;
  public $toffset; // GMT offset

  // Other generals

  private $ldays = array(
    "Monday", "Tuesday", "Wednesday", "Thursday", 
    "Friday", "Saturday", "Sunday");
  private $adays = array(
    "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun");

  private $lmonths = array(
    "January", "February", "March", "April", "May", "June", "July", 
    "August", "September", "October", "November", "December");
  private $amonths = array(
    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", 
    "Aug", "Sep", "Oct", "Nov", "Dec");

  public function show_settings()
  {
    printf("\n---------------------------------------------------------------\n");
    printf("n:  month=%-12s day=%-7s year=%-4s dayofweek=%s\n", $this->nmonth, $this->nday, $this->nyear, $this->ndayofweek);
    printf("l:  month=%-12s day=%-7s year=%-4s dayofweek=%s\n", $this->lmonth, $this->lday, $this->lyear, $this->ldayofweek);
    printf("a:  month=%-12s day=%-7s year=%-4s dayofweek=%s\n", $this->amonth, $this->aday, $this->ayear, $this->adayofweek);
    printf("t:  hour=%-13s min=%-7s sec=%-5s prec=%s\n", $this->thour, $this->tmin, $this->tsec, $this->tprec);
    printf("    tz=%-15s offset=%-6s\n", $this->tz, $this->toffset);
    printf("---------------------------------------------------------------\n");
  }

  public function __construct($datestr, $datefmt = "mdy")
  {
    $this->datestr = $datestr;
    $this->datefmt = $datefmt;
    self::tokens();
    //important post parser functions to build remaining variables
    if(self::goodtogo('date'))
      self::builddayvalues();
    if(self::goodtogo('time'))
      self::buildtimevalues();
  }

  public function date($fmt)
  {
    return preg_replace_callback("/%./", array('self', 'datesub'), $fmt);
  }

  private function datesub($values)
  {
    $value = array_shift($values);
    
    switch($value[1])
    {
      // Not implemented yet - 
      //   z - day of the year from 0 - 364
      //   W - week of the year 
      //   t - last day of the month
      //   L - whether it's leap year
      //   B - swatch internet time
      //  all TZ and Full Date/Time settings
      case 'd': $rep = $this->nday; break;
      case 'D': $rep = $this->adays[$this->ndayofweek - 1]; break;
      case 'j': $rep = sprintf("%d", $this->nday); break;
      case 'l': $rep = $this->ldays[$this->ndayofweek-1]; break;
      case 'N': $rep = $this->ndayofweek; break;
      case 'S': $rep = self::ordinal($this->nday); break;
        // This just changes Sunday to 0
      case 'w': $rep = ($this->ndayofweek==7)?0:$this->ndayofweek; break;
      case 'F': $rep = $this->lmonths[$this->nmonth-1]; break;
      case 'm': $rep = $this->nmonth; break;
      case 'M': $rep = $this->amonths[$this->nmonth-1]; break;
      case 'n': $rep = sprintf("%d", $this->nmonth); break;
      case 'Y': $rep = $this->nyear; break;
      case 'y': $rep = self::padnum(substr($this->nyear, -1, 2), 2); break;
       /*** Time **/
      case 'a': $rep = ($this->thour > 11)?'pm':'am'; break;
      case 'A': $rep = ($this->thour > 11)?'PM':'AM'; break;
      case 'g': $rep = ($this->thour > 12)?$this->thour - 12:$this->thour; break;
      case 'G': $rep = sprintf("%d", $this->thour); break;
      case 'h': $rep = self::padnum(($this->thour > 12)?$this->thour - 12:$this->thour, 2); break;
      case 'H': $rep = $this->thour; break;
      case 'i': $rep = $this->tmin; break;
      case 's': $rep = $this->tsec; break;
      case '%': $rep = "%"; break;
      default: self::error(sprintf("Unknown datesub %s!\n", $value)); break;
    }
    $repstr = sprintf("/%s/", $value);
    return preg_replace($repstr, $rep, $value);
  }

  protected function goodtogo($for)
  { 
    if($for == "date" &&
      $this->nmonth &&
      $this->nday &&
      $this->nyear)
        return 1;
    if($for == "time" &&
      $this->thour &&
      $this->tmin)
        return 1;
    return 0; 
  }

  private function builddayvalues()
  {

    require_once 'doomsday.inc'; // The doomsday calculation in a PHP class

    $dd = new DoomsDay($this->nday, $this->nmonth, $this->nyear);
    $this->ndayofweek = $dd->dayofweek;
    if($this->ndayofweek !== null)
    {
      $this->ldayofweek = $this->ldays[$dd->dayofweek-1];
      $this->adayofweek = $this->adays[$dd->dayofweek-1];
    }
    $this->aday = sprintf("%d%s", $this->nday, self::ordinal($this->nday));
    $this->lday = sprintf("%d%s", $this->nday, self::ordinal($this->nday));
  }

  public function ordinal($date)
  {
    $last = substr($date, -1, 1);
    switch($last)
    {
      case 1: return "st";
      case 2: return "nd";
      case 3: return "rd";
      default: return "th";
    }
  }

  private function buildtimevalues() {}

  private function tokens()
  {
    $fulltokens = ' ,'; // Tokens that break major date components
    $fdate = false;
    $ftime = false;

    $tok = strtok($this->datestr, $fulltokens);
    while($tok !== false)
    {
      // Handle short dates, i.e. 10/21/44 or 21/10/1944 or 1994-10-21
      if(preg_match("/^(\d+)[\/-](\d+)[\/-](\d+)$/", $tok, $f))
      {
        // Try to guess if they put the year up front
        if(strlen($f[1]) == 4) $this->datefmt = "ymd";
        self::parse_shortdate($f);
      }

      // Handle times as 14:40, 2:40:23, or 02:40:23.152422
      elseif(preg_match("/^(\d+):(\d+)(?::(\d+)(?:\.(\d+))*)*$/", $tok, $f))
        self::parse_time($f);

      // Handle meridiems
      elseif(strtolower($tok) == "pm" || strtolower($tok) == "am")
        self::parse_meridiem(strtolower($tok));

      // Handle long and short days - trashed in favor of computation
      elseif(in_array($tok, array_merge($this->ldays, $this->adays)))
        $dayholder = $tok; // Trash it for now

      // Handle positive short digit years
      elseif(strlen($tok) == 4 && is_numeric($tok))
      {
        $this->nyear = self::longyear($tok);
        $this->lyear = self::longyear($tok);
        $this->ayear = self::longyear($tok);
      }

      // Handle formal days
      elseif(preg_match("/^(\d+)(st|nd|rd|th)$/", $tok, $d))
      {
        $this->nday = self::padnum($d[1], 2);
        $this->aday = $tok;
      }

      // Handle long month names
      elseif(in_array($tok, $this->lmonths))
        self::parse_val($tok, "F");

      // Handle short month names
      elseif(in_array($tok, $this->amonths))
        self::parse_val($tok, "M");

      // Default to error out
      else
        self::error(sprintf("tokens() can't handle %s in %s\n", $tok, $this->datestr));

      // Get next token
      $tok = strtok($fulltokens);
    }
  }

  private function parse_time($time)
  {
    if(count($time) < 3 || count($time) > 5)
      self::error("Bad time passed to time()!\n");

    if($time[1] > 24)
      self::error("Bad hour ${time[1]} passed to parse_time()!\n");
    else
      $this->thour = self::padnum($time[1], 2);

    if($time[2] > 59)
      self::error("Bad min ${time[2]} passed to parse_time()!\n");
    else
      $this->tmin = self::padnum($time[2], 2);

    if(count($time) > 3)
      if($time[3] > 60)
        self::error("Bad second ${time[3]} passed to parse_time()!\n");
      else
       $this->tsec = self::padnum($time[3], 2);

    if(count($time) > 4)
      $this->tprec = self::padnum($time[4], 2);

  }

  private function parse_meridiem($m)
  {
    if(!$this->thour || !$this->tmin)
      self::error("parse_meridiem() called with no time set!\n");
    if($this->thour === 0 || $this->thour > 12)
      error("Attempt to set meridiem on 24 hour clock!\n");

    if($m == "pm")
      if($this->thour < 12)
        $this->thour += 12;
      else
        $this->thour = 0; // Handles converting pm thour into 24 hour
  }

  private function parse_shortdate($date)
  {
    if(strlen($this->datefmt) != 3)
      self::error("Invalid datefmt in parse_shortdate()!\n");

    // This little bit of weirdness builds the variables $m, $d, $y dynamically
    for($i = 0; $i < 3; $i++)
      ${$this->datefmt[$i]} = $date[$i+1];
 
    self::parse_val($m, "m");
    self::parse_val($d, "d");
    $this->nyear = self::longyear($y);
    $this->lyear = self::longyear($y);
    $this->ayear = self::longyear($y);
  }

  // Note we don't distinguish like date() between m/n and d/j
  // m and d are treated the same
  private function parse_val($val, $type)
  {
    switch($type)
    {
      case "m":
        $this->nmonth = self::padnum($val, 2);
        $this->lmonth = $this->lmonths[$val - 1];
        $this->amonth = $this->amonths[$val - 1];
        break;

      case "d":
        $this->nday = self::padnum($val, 2);
        break;

      case "F":
        for($i=0; $i<count($this->lmonths); $i++)
          if($val == $this->lmonths[$i]) break;
        $this->nmonth = self::padnum($i+1, 2);
        $this->lmonth = $this->lmonths[$i];
        $this->amonth = $this->amonths[$i];
        break;

      case "M":
        for($i=0; $i<count($this->amonths); $i++)
          if($val == $this->amonths[$i]) $ct = $i;
        $this->nmonth = self::padnum($i+1, 2);
        $this->lmonth = $this->lmonths[$i];
        $this->amonth = $this->amonths[$i];
        break;

      default:
        self::error("Unknown type ${type} passed to parse_val!\n");
        break;
    }
  }
    
  private function longyear($year)
  {
    if($year > 999)
       return $year;
    if($year > 100)
      return sprintf("0%03d", $year);
    // If the year is greater than this year, predate it
    // Otherwise assume it's a future date
    if($year > date("y", time()))
      return sprintf("19%02d", $year);
    else
      return sprintf("20%02d", $year);
  }

  private function padnum($num, $size)
  {
    $fmt = sprintf("%%0%dd", $size);
    return sprintf($fmt, $num);
  }

  public function error($str)
  {
    print $str;
    exit;
  }
}

?>

 

<?

/*
 *
 * Based on a PHP implementation by Andreas T&oslash;r&aring; Hagli
 * http://www.stud.ntnu.no/~andrhag/software/doomsday.php
 * Now converted into a handy dandy PHP class. (With permission.)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Constructor:
 *    $val = new DoomsDay(day, month, year);
 *    $val->dayofweek;
 *
 * Can't get much simpler than that.
 * 
 */

class DoomsDay 
{

  public $dayofweek = null;
  // Null just avoids Monday being a "0" day
  // Even though we all hate Mondays
  public $long_weekdays = array(null,
                           "Monday",
                           "Tuesday",
                           "Wednesday",
                           "Thursday",
                           "Friday",
                           "Saturday",
                           "Sunday");

  public $short_weekdays = array(null,
                           "Mon",
                           "Tue",
                           "Wed",
                           "Thu",
                           "Fri",
                           "Sat",
                           "Sun");

  private function leap_year_add ($year)
  {
    if ($year/4 == (int)($year/4) && 
        ($year/100 != (int)($year/100) || 
        $year/400 == (int)($year/400)))
          return 1; 
    else return 0; 
  }

  private function doomsday_in_month ($month, $year)
  {
    if ($month == 1 && !self::leap_year_add ($year)) return 31;
    if ($month == 1 && self::leap_year_add ($year)) return 25;
    elseif ($month == 2) return (28 + self::leap_year_add ($year));
    elseif ($month == 3) return 7;
    elseif ($month/2 == (int)($month/2)) return $month;
    elseif ($month > 8) return ($month - 4);
    else return ($month + 4);
  }

  private function days_in_month ($month, $year)
  {
    switch ($month)
    {
      case 12: return 31;
      case 2: return 28+self::leap_year_add ($year);
      default: return 30;
    }
  }

  public function __construct($day, $month, $year)
  {
  
    if ($day != NULL || $month != NULL || $year != NULL)
    {
      if ($day == NULL || $month == NULL || $year == NULL)
      {
        print "DoomsDay error: Invalid date.\n";
        exit;
      }
      elseif (abs ($year) > 10000)
      {
        print "DoomsDay error: Year must be before 10,000\n";
        exit;
      }
      elseif ($month <= 0 || $month > 12 || $day <= 0 || $day > self::days_in_month ($month, $year))
      {
        $year = (int)$year; /* Too "clean" number.  */
        printf ("DoomsDay error: Invalid date. %s\n", ($month == 2 && $day == 29) ? " $year is not a leap year." : "");
      }
      else
      {
        /* Finding doomsday difference.  */
        $doomdiff = ($day - self::doomsday_in_month($month, $year))%7;
        if ($doomdiff >= 4) $doomdiff -= 7;
        elseif ($doomdiff <= -4) $doomdiff += 7;
    
        /* Finding centuryday.  */
        $similarcentury = $year - $year%100;
        while ($similarcentury < 2000) $similarcentury += 400;
        while ($similarcentury > 2000) $similarcentury -= 400;
        if ($similarcentury == 2000) $centuryday = 2;
        elseif ($similarcentury == 1900) $centuryday = 3;
        elseif ($similarcentury == 1800) $centuryday = 5;
        elseif ($similarcentury == 1700) $centuryday = 7;
      
        /* Finding dozens.  */
        $dozens = 0;
        while($year - $year%100 + ($dozens+1)*12 <= $year) $dozens++;
      
        /* Finding remainder.  */
        $remainder = $year%100 - ($dozens)*12;
      
        /* Finding 4s of remainder.  */
        $fourremainder = (int)($remainder/4);
      
        /* And now, the weekday.  */
        $weekday = ($doomdiff + $centuryday + $dozens +
        $remainder + $fourremainder)%7;
        if ($weekday <= 0) $weekday += 7;
        $this->dayofweek = $weekday;
      }
    }
  }
}

 

Leave a Reply

Your email address will not be published. Required fields are marked *