I wrote this a while ago. I can't supply the ancillary components, but those parts should be, overall, fairly explanatory to convert to normal forms. I don't recall offhand if I finished the todo.
Code: Select all
<?php
/**
* @todo convert this to a Super-Julian form, thus allowing far larger range of
* dates. Time of day is stored in microseconds for insane accuracy. This would
* require versions of all date/time related functions available from php...
*/
/**
* TDateTime
* @package canons
* @subpackage time
* @author feyd
* @version $Id $
*/
class TDateTime extends TObject {
/**
* @var MonthFullNames array full names of months
* @access static
*/
static $MonthFullNames = array(
1=>"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December",
);
/**
* @var MonthShortNames array short names of months
* @access static
*/
static $MonthShortNames = array(
1=>"Jan", "Feb", "Mar",
"Apr", "May", "Jun",
"Jul", "Aug", "Sep",
"Oct", "Nov", "Dec",
);
/**
* @var DayFullNames array full names of days
* @access static
*/
static $DayFullNames = array(
"Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday"
);
/**
* @var DayShortNames array short names of days
* @access static
*/
static $DayShortNames = array(
"Sun", "Mon", "Tue",
"Wed", "Thu", "Fri", "Sat"
);
/**
* @var DayTable array lengths of months for standard and leap years
* @access static
*/
static $DayTable = array(
array(1=>31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31),
array(1=>31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31),
);
static $DayToHours = '24'; // hours in a day
static $HourToMinutes = '60'; // minutes in an hour
static $MinuteToSeconds = '60'; // seconds in a minute
static $SecondToMicroseconds = '1000000'; // microseconds in a second
static $J0000 = '1721424.5'; // Julian date of Gregorian epoch: 0000-01-01
static $J0001 = '1721425.5'; // Gregorian epoch
static $J1970 = '2440587.5'; // Julian date at Unix epoch: 1970-01-01
static $JMJD = '2400000.5'; // Epoch of Modified Julian Date system
static $J1900 = '2415020.5'; // Epoch (day 1) of Excel 1900 date system (PC)
static $J1904 = '2416480.5'; // Epoch (day 0) of Excel 1904 date system (Mac)
/**
* TDateTime::__construct
* @return void
*/
function __construct() {
parent::__construct();
$this->AddPropertyEx('Format', vtString, scPublished, false, true);
$this->SetDefault('Format',translate('TIMESTAMP'));
$this->AddPropertyEx('Timezone', vtString, scPublished, false, true);
$this->AddPropertyEx('TimezoneOffset', vtFloat, scPublished, false, true);
//$this->ChangeTimezone('UTC');
}
/**
* TDateTime::Now
* retrieve the current time
* @return string formatted current time
*/
static function Now() {
if(isStaticCall()) {
// use system default, since static version has no internals
return self::Date(translate('TIMESTAMP'));
} else {
return $this->Date($this->Format);
}
}
/**
* TDateTime::mktime
* similar to php's internal mktime function, except the return is a super-size
* julian time mark.
* @return string TDateTime string value
*/
function mktime() {
// do some calculations to set up the data
$data = array();
$names = array('hour','minute','second','month','day','year','is_dst','microsecond');
$nnames = count($names);
$chars = array('H','i','s','m','d','Y');
$nchars = count($chars);
$args = (func_num_args() == 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : func_get_args());
for($i = 0; $i < $nnames; $i++) {
if(isset($args[$i]) and $args[$i] !== null) {
$data[] = $args[$i];
}
elseif(isset($args[$names[$i]]) and $args[$names[$i]] !== null) {
$data[] = $args[$names[$i]];
}
elseif($i < $nchars) {
$data[] = self::idate($chars[$i]);
} elseif($i == $nchars) {
// daylight time
// @todo add support for auto-detection based toggling
$data[] = false;
} elseif( $i == $nnames-1 ) {
// microseconds
$data[] = array_shift(explode(' ',microtime()));
}
}
if(function_exists('bcmul')) {
$p = 0;
$m = $data[3];
$d = $data[4];
$y = $data[5];
$h = $data[0];
$i = $data[1];
$s = $data[2];
$l = $data[6];
$u = $data[count($data)-1];
self::CorrectDateTime($m,$d,$y,$h,$i,$s,$l,$u);
// store off the corrected date
$cm = $m;
$cd = $d;
$cy = $y;
if(bccomp($m,'3',$p) < 1) {
$m = bcadd($m,'12',$p);
$y = bcsub($y,'1',$p);
}
$A = bcdiv($y,'100',$p);
$B = bcdiv($A,'4',$p);
$C = bcadd(bcsub('2',$A,$p),$B,$p);
$E = bcmul('365.25',bcadd($y,'4716',$p),$p);
$F = bcmul('30.6001',bcadd($m,'1',$p),$p);
$jd = bcadd($C,$d,$p);
$jd = bcadd($jd,$E,$p);
$jd = bcadd($jd,$F,$p);
$jd = bcsub($jd,'1524.5',1);
$p = 200;
$t = $u;
// correct the time to UTC, the internal time zone.
$s = bcsub($s,date('Z'),$p);
$t = bcadd($t,$s,$p);
$t = bcdiv($t,self::$MinuteToSeconds,$p);
$t = bcadd($t,$i,$p);
$t = bcdiv($t,self::$HourToMinutes,$p);
$t = bcadd($t,$h,$p);
$t = bcdiv($t,self::$DayToHours,$p);
$ret = bcadd($jd,$t,$p);
return $ret;
//echo "$cm/$cd/$cy\n\n jd = $jd\n+ t = $t\n----------\n$ret\n\n".gregoriantojd($cm,$cd,$cy)."\n\n".jdtogregorian($jd);
} else {
trigger_error('BC Math is required for accurate time calculations.');
}
}
/**
* TDateTime::idate
* retrieve an integer value from the date subsystem
* @return integer the results of the idate call.
*/
static function idate($char, $time = null) {
if(func_num_args() > 1)
return idate($char, intval($time));
else
return idate($char);
}
/**
* TDateTime::isLeap
* test is a given year is a leap year
* @return boolean
*/
static function isLeap($year) {
// if year is divisble by 4 it's a leap year unless it's divisble by 100,
// except when divisible by 400. Divisible by 4000 also isn't.
return ((bcmod($year,'4') == 0 and bcmod($year,'100') != 0) || bcmod($year,'400') == 0 and bcmod($year,'4000') != 0);
}
/**
* TDateTime::CorrectDateTime
* @return void
*/
static function CorrectDateTime(&$m,&$d,&$y,&$h,&$i,&$s,&$l,&$u) {
$p = 200;
$nmonths = count(self::$MonthShortNames);
// echo "$m/$d/$y being corrected.\n";
// echo "starting month correction.\n";
// correct month of the year
while(bccomp('0',$m, 0) >= 0 or bccomp($m,$nmonths, 0) > 0) {
$mdir = bccomp($m,'1', 0);
if($mdir < 0) {
echo "adding $nmonths to $m\n";
$m = bcadd($m,$nmonths, 0);
$y = bcsub($y,'1', 0);
} else {
echo "subtracting $nmonths from $m\n";
$m = bcsub($m,$nmonths, 0);
$y = bcadd($y,'1', 0);
}
}
//echo "starting time correction\n";
$second_minute = self::$MinuteToSeconds;
$second_hour = bcmul(self::$HourToMinutes,$second_minute);
$second_day = bcmul(self::$DayToHours, $second_hour);
// bring all the times into the same timespace
$t = bcadd(bcmul($h,$second_hour,$p),bcadd(bcmul($i,$second_minute,$p),bcadd($s,$u,$p),$p),$p);
$dt = bcdiv( $t, $second_day, 0);
// and now the corrected time, which we need to bring back out to varibles
$t = bcsub($t,$dt,$p);
// hours
$h = bcdiv($t,$second_hour,0);
$t = bcsub($t,bcmul($h,$second_hour,0),0);
// minutes
$i = bcdiv($t,$second_minute,0);
$t = bcsub($t,bcmul($i,$second_minute,0),0);
// seconds
$s = bcdiv($t,'1',0);
$t = bcsub($t,$i,0);
// strip any remaining seconds from the microseconds
$u = bcsub($u,bcdiv($u,'1',0),0);
// microseconds
//$u = bcmul($u,self::$SecondToMicroseconds,$p);
//echo "{$h}::{$i}::".bcadd($s,$u,10);
// add or subtract the correct amount of days based on overflow of the time
// correction
$d = bcadd($d, $dt, 0);
// echo "starting day correction.\n";
// correct the day of the month
while(bccomp('0',$d, 0) >= 0 or bccomp($d,self::$DayTable[self::isLeap($y)][$m],0) > 0) {
$ddir = bccomp($d,'1',0);
if($ddir < 0) {
// go back a month
// echo "subtracting 1 from $m\n";
$m = bcsub($m,'1',0);
// did we hit a wrap around on the month?
if(bccomp($m,'0',0) == 0) {
// echo "$m wraps\n";
$m = $nmonths;
$y = bcsub($y,'1',0);
}
// hopefully move the day into the new month's context
$d = bcadd($d, self::$DayTable[self::isLeap($y)][$m],0);
} else {
// hopefully move the day into the new month's context
$d = bcsub($d, self::$DayTable[self::isLeap($y)][$m],0);
// bump up a month
// echo "adding 1 to $m\n";
$m = bcadd($m,'1',0);
// did we hit a wrap around on the month?
if(bccomp($m,$nmonths,0) > 0) {
echo "$m wraps\n";
$m = '1';
$y = bcadd($y,'1',0);
}
}
}
// echo "Final corrected: {$y}-{$m}-{$d} {$h}:{$i}:{$s}.{$u}\n";
}
/**
* TDateTime::BreakOut
* Break a given TDateTime string into gregorian parts (with daylight)
* @return void
*/
static function BreakOut($Time) {
$p = 200;
$Time = bcadd($Time,'0.5',$p);
$dt = explode('.',$Time);
$jd = $dt[0];
$broken = array();
if(empty($dt[1])) {
$broken['Hour'] = 0;
$broken['Minute'] = 0;
$broken['Second'] = 0;
$broken['Subsecond'] = 0;
} else {
$t = '0.'.$dt[1];
$broken['Hour'] = bcmul($t,self::$DayToHours,0);
$t = bcsub(bcmul($t,self::$DayToHours,$p),$broken['Hour'],$p);
$broken['Minute'] = bcmul($t,self::$HourToMinutes,0);
$t = bcsub(bcmul($t,self::$HourToMinutes,$p),$broken['Minute'],$p);
$broken['Second'] = bcmul($t,self::$MinuteToSeconds,0);
$t = bcsub(bcmul($t,self::$MinuteToSeconds,$p),$broken['Second'],$p);
$t = explode('.',$t);
$broken['Subsecond'] = (isset($t[1]) ? $t[1] : '000');
}
$p = 5;
// make sure $jd is aligned to a julian nychthemeron
$wjd = bcadd(bcdiv(bcsub($jd,'0.5'),'1',0),'0.5',$p);
$depoch = bcsub($wjd,self::$J0001,1);
$quadricent = bcdiv($depoch,'146097',0);
$dqc = bcmod($depoch,'146097');
$cent = bcdiv($dqc,'36524',0);
$dcent = bcmod($dqc,'36524');
$quad = bcdiv($dcent,'1461',0);
$dquad = bcmod($dcent,'1461');
$yindex = bcdiv($dquad,'365',0);
$year = bcadd(bcadd(bcadd(bcmul($quadricent,'400',$p),bcmul($cent,'100',$p),$p),bcmul($quad,'4',$p),$p),$yindex,0);
if (!($cent == '4' or $yindex == '4')) {
$year = bcadd($year,'1',0);
}
$yearday = bcsub($wjd,self::mktime(0,0,0,1,1,$year,false,0),0);
$leapadj = (bccomp($wjd,self::mktime(0,0,0,3,1,$year) < 0) ? 0 : (self::isLeap($year) ? 1 : 2) );
$month = bcdiv(bcadd(bcmul(bcadd($yearday,$leapadj,$p),'12',$p),'373',$p),'367',0);
$day = bcadd(bcsub($wjd,self::mktime(0,0,0,$month,1,$year),$p),1,0);
//$nmonths = count(self::$MonthShortNames);
$broken['Year'] = $year;
$broken['Month'] = $month;
$broken['Day'] = $day;
//$broken['month'] = bcmod(bcadd($month,$nmonths,0),bcadd($nmonths,'1',0));
//$broken['day'] = (bccomp($month,'0') == 0 ? self::$DayTable[self::isLeap($broken['year'])][$broken['month']] : $day);
$broken['YearDay'] = $yearday;
$broken['Time'] = bcsub($Time,'0.5',200);
$broken['JulianDay'] = $wjd;
// debugging
/*
$broken['wjd'] = $wjd;
$broken['depoch'] = $depoch;
$broken['quadricent'] = $quadricent;
$broken['dqc'] = $dqc;
$broken['cent'] = $cent;
$broken['dcent'] = $dcent;
$broken['quad'] = $quad;
$broken['dquad'] = $dquad;
$broken['yindex'] = $yindex;
$broken['yearday'] = $yearday;
$broken['leadadj'] = $leadadj;
*/
//self::CorrectDateTime($broken['month'], $broken['day'], $broken['year'], $broken['hour'], $broken['minute'], $broken['second'], $l, $broken['subsecond']);
return $broken;
}
/**
* TDateTime::JulianWeekday
* @return string
*/
static function JulianWeekday($j) {
return bcmod(bcadd($j,'1.5',0), '7');
}
/**
*
* @return void
*/
static function WeekdayBefore($weekday, $jd) {
return $jd - self::JulianWeekday(bcsub($jd,$weekday,100));
}
/**
* TDateTime::SearchWeekday
* @return mixed
*/
static function SearchWeekday($weekday, $jd, $direction, $offset) {
return self::WeekdayBefore($weekday, bcadd($jd,bcmul($direction,$offset,0),100));
}
/**
* TDateTime::PreviousWeekday
* @return mixed
*/
static function PreviousWeekday($weekday, $jd) {
return self::SearchWeekday($weekday, $jd, -1, 1);
}
/**
* TDateTime::NextWeekday
* @return mixed
*/
static function NextWeekday($weekday, $jd) {
return self::SearchWeekday($weekday, $jd, 1, 7);
}
/**
* TDateTime::NumWeeks
* @return string
*/
static function NumWeeks($weekday, $jd, $nthweek) {
$j = bcmul(7,$nthweek,0);
if (bccomp($nthweek,'0',0) > 0) {
$j = bcadd($j,self::PreviousWeekday($weekday, $jd),0);
} else {
$j = bcadd($j,self::NextWeekday($weekday, $jd),0);
}
return $j;
}
/**
* TDateTime::ISOtoJulian
* @return void
*/
static function ISOToJulian($week, $day, $year) {
return bcadd($day,self::NumWeeks(0, self::mktime(12,0,0,12,28,bcsub($year,1,0),false,0), $week),1);
}
/**
* TDateTime::JulianToISO
* @return array elements: year, week, and day respective to numeric index.
* Conforms to ISO 8601
*/
static function JulianToISO($jd) {
$year = self::BreakOut(bcsub($jd,3,1));
$year = $year['Year'];
if (bccomp($jd,self::ISOToJulian(1,1,bcadd($year,1,0)),1) >= 0) {
$year = bcadd($year,1,0);
}
$week = bcadd(bcdiv(bcsub($jd,self::ISOToJulian(1,1,$year)),7,0),1,0);
$day = self::JulianWeekday($jd);
if ($day == 0) {
$day = 7;
}
return array($year, $week, $day);
}
/**
* TDateTime::date
* @param $aFormat string the formatting string to use. Supports all PHP 5.1.0
* changes.
* @param $aTime string This should be a TDateTime time value, it does _not_
* support unix timestamps
* @return void
*/
static function date($Format=null, $Time=null) {
if($Format === null) {
$Format = self::GetFormat();
}
if($Time === null) {
$Time = self::GetTime();
}
$breakout = self::BreakOut($Time);
$output = '';
for($i = 0, $j = strlen($Format); $i < $j; $i++) {
$pre = '';
$post = '';
$char = $Format{$i};
switch($char) {
case '\\': // next character is escaped, do not process it
$i++;
if($i < $j) {
$output .= $Format{$i};
}
break;
// day related bits
case 'd': // day of month (leading zero)
$pre = (bccomp('10',$breakout['Day'],0) > 0 ? '0' : '');
case 'j': // day of month (no leading zero)
$output .= $pre.$breakout['Day'];
break;
case 'W': // ISO Week
case 'N': // ISO Day
case 'o': // ISO Year
if(!isset($iso)) {
$iso = self::JulianToISO($breakout['JulianDay']);
}
if($char == 'W') {
$output .= $iso[1];
} elseif($char == 'N') {
$output .= $iso[2];
} else {
$output .= $iso[0];
}
break;
case 'D': // short day name
$output .= translate(self::$DayShortNames[self::JulianWeekday($breakout['JulianDay'])]);
break;
case 'l': // full day name
$output .= translate(self::$DayFullNames[self::JulianWeekday($breakout['JulianDay'])]);
break;
case 'w': // weekday number (zero based)
$output .= self::JulianWeekday($breakout['JulianDay']);
break;
case 'z': // year day
$output .= $breakout['YearDay'];
break;
case 'S': // english suffix for a day: st, nd, rd, th
$temp = bcmod($breakout['Day'],'10');
if(bcdiv($breakout['Day'],'10',0) == '1') {
$temp = '0';
}
switch($temp) {
case '1':
$output .= 'st';
break;
case '2':
$output .= 'nd';
break;
case '3':
$output .= 'rd';
break;
default:
$output .= 'th';
break;
}
break;
// month related bits
case 'F': // full name of the month
$output .= translate(self::$MonthFullNames[$breakout['Month']]);
break;
case 'M':
$output .= translate(self::$MonthShortNames[$breakout['Month']]);
break;
case 'm': // month (leading zero)
$pre = (bccomp('10',$breakout['Month']) > 0 ? '0' : '');
case 'n': // month (no leading zero)
$output .= $pre.$breakout['Month'];
break;
// year related bits
case 'L': // leap year
$output .= (self::isLeap($breakout['Year']) ? '1' : '0');
break;
case 'Y':
$output .= $breakout['Year'];
break;
case 'y':
$output .= substr($breakout['Year'],-2);
break;
// time related bits
case 'a': // am pm
case 'A': // AM PM
$pre = (bccomp('11',$breakout['Hour'],0) <= 0 ? 'pm' : 'am');
if($char == 'A') {
$pre = strtoupper($pre);
}
$output .= $pre;
break;
case 'g': // hour (no leading zero) 12 hr form
case 'G': // hour (no leading zero) 24 hr form
case 'h': // hour (leading zero) 12 hr form
case 'H': // hour (leading zero) 24 hr form
$temp = $breakout['Hour'];
if($char == 'g' or $char == 'h') {
if(bccomp('11',$temp,0) <= 0) {
$temp = bcsub($temp,'12',0);
}
$temp = bcadd($temp,'1',0);
}
if($char == 'h' or $char == 'H') {
$pre = (bccomp('10',$temp,0) > 0 ? '0' : '');
}
$output .= $pre.$temp;
break;
case 'i': // minutes (leading zero)
$output .= (bccomp('10',$breakout['Minute'],0) > 0 ? '0' : '').$breakout['Minute'];
break;
case 's': // seconds (leading zero)
$output .= (bccomp('10',$breakout['Second'],0) > 0 ? '0' : '').$breakout['Second'];
break;
case 'B': // Swatch internet time (don't really care about this)
break;
case 'e': // verbal timezone
// @todo add timezone and daylight rules support
$output .= 'Universal Coordinated Time';
break;
default:
$output .= $char;
break;
}
}
return $output;
}
/**
* TDateTime::GetFormat
* @access private
* @return string The "default" format to use
*/
private static function GetFormat() {
if(isStaticCall(1)) {
return translate('TIMESTAMP');
} else {
return $this->Format;
}
}
/**
* TDateTime::GetTime
* @access private
* @return string The "default" current time
*/
private static function GetTime() {
if(isStaticCall(1)) {
return self::mktime();
} else {
return $this->mktime();
}
}
/**
* TDataTime::GetTZ
* @return void
*/
static function GetTZ() {
}
// EOF
}
?>