Thanks!
Rewriting calendar display classes
Moderator: General Moderators
Rewriting calendar display classes
A few months back, I wrote a small set of classes to render an events calendar I wrote. I'd like to rewrite these classes from the ground up using test-driven development. Right now, the system uses unix timestamps and the date() and getdate() functions. I'd like for the classes to have the widest range of dates possible and provide the best flexibility and most ease of use. Now, with all of that in mind, what would you guys suggest I use? Still timestamps? Something else?
Thanks!
Thanks!
- feyd
- Neighborhood Spidermoddy
- Posts: 31559
- Joined: Mon Mar 29, 2004 3:24 pm
- Location: Bothell, Washington, USA
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.
some tests
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
}
?>Code: Select all
<?php
import('time.TDateTime');
$test = new TDateTime();
/*
$y = 2005;
$m = 0;
$d = 0;
$h = idate('H');
$i = idate('i');
$s = idate('s');
$l = 0;
$u = 0;
TDateTime::CorrectDateTime($m,$d,$y,$h,$i,$s,$l,$u);
*/
$breakout = $test->BreakOut($test->mktime());
var_export($breakout);
echo "\n\n".TDateTime::$DayFullNames[TDateTime::JulianWeekday($breakout['JulianDay'])]."\n\n";
echo $test->date('\\d=d \\j=j \\W=W \\N=N \\o=o \\D=D \\l=l \\w=w \\z=z \\S=S \\F=F \\M=M \\m=m \\n=n \\L=L \\Y=Y \\y=y \\a=a \\A=A \\g=g \\G=G \\H=H \\h=h \\i=i \\s=s \\B=B \\e=e',$breakout['Time'])."\n\n";
$years = array('1600','1648','1700','1800','1900','1996','2000','4000');
$results = array();
foreach($years as $year)
$results[$year] = TDateTime::isLeap($year);
echo "\nLeap year calculations:\n";
var_export($results);
?>
Last edited by feyd on Wed Dec 05, 2007 2:27 pm, edited 1 time in total.
- Kieran Huggins
- DevNet Master
- Posts: 3635
- Joined: Wed Dec 06, 2006 4:14 pm
- Location: Toronto, Canada
- Contact:
feyd, I've barely read your code so I might be missing something, but is there a reason as to why you do this?
Code: Select all
$chars = array('H','i','s','m','d','Y');
$nchars = count($chars); - Maugrim_The_Reaper
- DevNet Master
- Posts: 2704
- Joined: Tue Nov 02, 2004 5:43 am
- Location: Ireland
Timestamps offer the most flexibility, but they also lose their context off the local system which is why UTC or similar make a good common format (hence it's support in iCalendar). Something along the lines of Feyd class is also very useful - just be careful of the date parameter types which vary between PHP and the relevant ISO specification (Zend_Date, another good set of classes to peek at, allows for both). Date classes inevitably end up being a parcel of reuseable code so it's worth a little extra work than usual to get right.
Small comment on feyd's class - it would be nice for a solution to support the faster alternatives to BCMath - GMP particularly is quite faster at precision calculations. It's pretty rare to see idate() in practice - kudos to the PHP wizard of Devnetwork
.
Since it's test driven, a good place to start is taking your requirements, attempting to identify relevant classes, and designing an interface you think would work. From there you can take a base unit and start with the infamous failing test to implement a piece of that functionality
. Assuming a Date class is a good place to start (seems reasonable) you now have a few suggestion to grab some inspiration from.
Small comment on feyd's class - it would be nice for a solution to support the faster alternatives to BCMath - GMP particularly is quite faster at precision calculations. It's pretty rare to see idate() in practice - kudos to the PHP wizard of Devnetwork
Since it's test driven, a good place to start is taking your requirements, attempting to identify relevant classes, and designing an interface you think would work. From there you can take a base unit and start with the infamous failing test to implement a piece of that functionality
- feyd
- Neighborhood Spidermoddy
- Posts: 31559
- Joined: Mon Mar 29, 2004 3:24 pm
- Location: Bothell, Washington, USA
Oren, I have no idea right now.. It's been so long since I've even looked at that code, let alone used it. (The code dates back to 2005) Looking at the code, they are the default values for the call to mktime(). They are used when no input is passed or is missing to the method.
Maugrim, the reason why I'm not using GMP is because it processes the time into the equation too since it's using Julian date storage.
Maugrim, the reason why I'm not using GMP is because it processes the time into the equation too since it's using Julian date storage.