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.

datetime.class
<?

/*
 *
 * 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, -12), 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->thour2); 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->ndayself::ordinal($this->nday));
    
$this->lday sprintf("%d%s"$this->ndayself::ordinal($this->nday));
  }

  public function 
ordinal($date)
  {
    
$last substr($date, -11);
    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($tokarray_merge($this->ldays$this->adays)))
        
$dayholder $tok// Trash it for now

      // Handle positive short digit years
      
elseif(strlen($tok) == && 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) < || 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 === || $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($val2);
        
$this->lmonth $this->lmonths[$val 1];
        
$this->amonth $this->amonths[$val 1];
        break;

      case 
"d":
        
$this->nday self::padnum($val2);
        break;

      case 
"F":
        for(
$i=0$i<count($this->lmonths); $i++)
          if(
$val == $this->lmonths[$i]) break;
        
$this->nmonth self::padnum($i+12);
        
$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+12);
        
$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;
  }
}

?>
doomsday.class
<?

/*
 *
 * 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/== (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 == && !self::leap_year_add ($year)) return 31;
    if (
$month == && self::leap_year_add ($year)) return 25;
    elseif (
$month == 2) return (28 self::leap_year_add ($year));
    elseif (
$month == 3) return 7;
    elseif (
$month/== (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 <= || $month 12 || $day <= || $day self::days_in_month ($month$year))
      {
        
$year = (int)$year/* Too "clean" number.  */
        
printf ("DoomsDay error: Invalid date. %s\n", ($month == && $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;
      }
    }
  }
}