PHP Cookbook/Dates and Times

From WikiContent

< PHP Cookbook(Difference between revisions)
Jump to: navigation, search
(Initial conversion from Docbook)
Current revision (13:36, 7 March 2008) (edit) (undo)
(Initial conversion from Docbook)
 
(One intermediate revision not shown.)

Current revision

PHP Cookbook


Contents

Introduction

Displaying and manipulating dates and times seems simple at first but gets more difficult depending on how diverse and complicated your users are. Do your users span more than one time zone? Probably so, unless you are building an intranet or a site with a very specific geographical audience. Is your audience frightened away by timestamps that look like "2002-07-20 14:56:34 EDT" or do they need to be calmed with familiar representations like "Saturday July 20, 2000 (2:56 P.M.)." Calculating the number of hours between today at 10 A.M. and today at 7 P.M. is pretty easy. How about between today at 3 A.M. and noon on the first day of next month? Finding the difference between dates is discussed in Recipe 3.6 and Recipe 3.7.

These calculations and manipulations are made even more hectic by daylight saving (or summer) time (DST). Thanks to DST, there are times that don't exist (in most of the United States, 2 A.M. to 3 A.M. on the first Sunday in April) and times that exist twice (in most of the United States, 1 A.M. to 2 A.M. on the last Sunday in October). Some of your users may live in places that observe DST, some may not. Recipe 3.12 and Recipe 3.13 provide ways to work with time zones and DST.

Programmatic time handling is made much easier by two conventions. First, treat time internally as Coordinated Universal Time (abbreviated UTC and also known as GMT, Greenwich Mean Time), the patriarch of the time-zone family with no DST or summer time observance. This is the time zone at 0 degrees longitude, and all other time zones are expressed as offsets (either positive or negative) from it. Second, treat time not as an array of different values for month, day, year, minute, second, etc., but as seconds elapsed since the Unix epoch: midnight on January 1, 1970 (UTC, of course). This makes calculating intervals much easier, and PHP has plenty of functions to help you move easily between epoch timestamps and human-readable time representations.

The function mktime( ) produces epoch timestamps from a given set of time parts, while date( ) , given an epoch timestamp, returns a formatted time string. You can use these functions, for example, to find on what day of the week New Year's Day 1986 occurred:

$stamp = mktime(0,0,0,1,1,1986);
print date('l',$stamp);
Wednesday
         

This use of mktime( ) returns the epoch timestamp at midnight on January 1, 1986. The l format character to date( ) tells it to return the full name of the day of the week that corresponds to the given epoch timestamp. Recipe 3.5 details the many format characters available to date( ).

In this book, the phrase epoch timestamp refers to a count of seconds since the Unix epoch. Time parts (or date parts or time and date parts) means an array or group of time and date components such as day, month, year, hour, minute, and second. Formatted time string (or formatted date string, etc.) means a string that contains some particular grouping of time and date parts, for example "2002-03-12," "Wednesday, 11:23 A.M.," or "February 25."

If you used epoch timestamps as your internal time representation, you avoided any Y2K issues, because the difference between 946702799 (1999-12-31 23:59:59 UTC) and 946702800 (2000-01-01 00:00:00 UTC) is treated just like the difference between any other two timestamps. You may, however, run into a Y2038 problem. January 19, 2038 at 3:14:07 A.M. (UTC) is 2147483647 seconds after midnight January 1, 1970. What's special about 2147483647? It's 231 - 1, which is the largest integer expressible when 32 bits represent a signed integer. (The 32nd bit is used for the sign.)

The solution? At some point before January 19, 2038, make sure you trade up to hardware that uses, say, a 64-bit quantity for time storage. This buys you about another 292 billion years. (Just 39 bits would be enough to last you until about 10680, well after the impact of the Y10K bug has leveled the Earth's cold fusion factories and faster-than-light travel stations.) 2038 might seem far off right now, but so did 2000 to COBOL programmers in the 1950s and 1960s. Don't repeat their mistake!

Finding the Current Date and Time

Problem

You want to know what the time or date is.

Solution

Use strftime( ) or date( ) for a formatted time string:

print strftime('%c');
print date('r');
Mon Aug 12 18:23:45 2002
               Mon, 12 Aug 2002 18:23:45 -0400
            

Use getdate( ) or localtime( ) if you want time parts:

$now_1 = getdate( );
$now_2 = localtime( );
print "$now_1[hours]:$now_1[minutes]:$now_1[seconds]";
print "$now_2[2]:$now_2[1]:$now_2[0]";
18:23:45
               18:23:45
            

Discussion

The functions strftime( ) and date( ) can produce a variety of formatted time and date strings. They are discussed in more detail in Recipe 3.5. Both localtime( ) and getdate( ), on the other hand, return arrays whose elements are the different pieces of the specified date and time.

The associative array getdate( ) returns has the key/value pairs listed in Table 3-1.

Table 3-1. Return array from getdate( )

Key Value
seconds Seconds
minutes Minutes
hours Hours
mday Day of the month
wday Day of the week, numeric (Sunday is 0, Saturday is 6)
mon Month, numeric
year Year, numeric
yday Day of the year, numeric (e.g., 299)
weekday Day of the week, textual, full (e.g., "Friday")
month Month, textual, full (e.g., "January")


For example, here's how to use getdate( ) to print out the month, day, and year:

$a = getdate( );
printf('%s %d, %d',$a['month'],$a['mday'],$a['year']);
August 7, 2002
            

Pass getdate( ) an epoch timestamp as an argument to make the returned array the appropriate values for local time at that timestamp. For example, the month, day, and year at epoch timestamp 163727100 is:

$a = getdate(163727100);
printf('%s %d, %d',$a['month'],$a['mday'],$a['year']);
March 10, 1975
            

The function localtime( ) returns an array of time and date parts. It also takes an epoch timestamp as an optional first argument, as well as a boolean as an optional second argument. If that second argument is true, localtime( ) returns an associative array instead of a numerically indexed array. The keys of that array are the same as the members of the tm_struct structure that the C function localtime( ) returns, as shown in Table 3-2.

Table 3-2. Return array from localtime( )

Numeric position Key Value
0 tm_sec Second
1 tm_min Minutes
2 tm_hour Hour
3 tm_mday Day of the month
4 tm_mon Month of the year (January is 0)
5 tm_year Years since 1900
6 tm_wday Day of the week
7 tm_yday Day of the year
8 tm_isdst Is daylight saving time in effect?


For example, here's how to use localtime( ) to print out today's date in month/day/year format:

$a = localtime( );
$a[4] += 1;
$a[5] += 1900;
print "$a[4]/$a[3]/$a[5]";
8/7/2002
            

The month is incremented by 1 before printing since localtime( ) starts counting months with 0 for January, but we want to display 1 if the current month is January. Similarly, the year is incremented by 1900 because localtime( ) starts counting years with 0 for 1900.

Like getdate( ), localtime( ) accepts an epoch timestamp as an optional first argument to produce time parts for that timestamp:

$a = localtime(163727100);
$a[4] += 1;
$a[5] += 1900;
print "$a[4]/$a[3]/$a[5]";
3/10/1975
            

See Also

Documentation on strftime( ) at http://www.php.net/strftime, date( ) at http://www.php.net/date, getdate( ) at http://www.php.net/getdate, and localtime( ) at http://www.php.net/localtime.

Converting Time and Date Parts to an Epoch Timestamp

Problem

You want to know what epoch timestamp corresponds to a set of time and date parts.

Solution

Use mktime( ) if your time and date parts are in the local time zone:

// 7:45:03 PM on March 10, 1975, local time
$then = mktime(19,45,3,3,10,1975);

Use gmmktime( ) if your time and date parts are in GMT:

// 7:45:03 PM on March 10, 1975, in GMT
$then = gmmktime(19,45,3,3,10,1975);

Pass no arguments to get the current date and time in the local or UTC time zone:

$now = mktime();
$now_utc = gmmktime();

Discussion

The functions mktime( ) and gmmktime( ) each take a date and time's parts (hour, minute, second, month, day, year, DST flag) and return the appropriate Unix epoch timestamp. The components are treated as local time by mktime( ), while gmmktime( ) treats them as a date and time in UTC. For both functions, a seventh argument, the DST flag (1 if DST is being observed, 0 if not), is optional. These functions return sensible results only for times within the epoch. Most systems store epoch timestamps in a 32-bit signed integer, so "within the epoch" means between 8:45:51 P.M. December 13, 1901 UTC and 3:14:07 A.M. January 19, 2038 UTC.

In the following example, $stamp_now is the epoch timestamp when mktime( ) is called and $stamp_future is the epoch timestamp for 3:25 P.M. on June 4, 2012:

$stamp_now = mktime( );
$stamp_future = mktime(15,25,0,6,4,2012);

print $stamp_now;
print $stamp_future;
1028782421
               1338837900
            

Both epoch timestamps can be fed back to strftime( ) to produce formatted time strings:

print strftime('%c',$stamp_now);
print strftime('%c',$stamp_future);
Thu Aug  8 00:53:41 2002
               Mon Jun 4 15:25:00 2012
            

Because the previous calls to mktime( ) were made on a computer set to EDT (which is four hours behind GMT), using gmmktime( ) instead produces epoch timestamps that are 14400 seconds (four hours) smaller:

$stamp_now = gmmktime( );
$stamp_future = gmmktime(15,25,0,6,4,2012);

print $stamp_now;
print $stamp_future;
1028768021
               1338823500
            

Feeding these gmmktime( )-generated epoch timestamps back to strftime( ) produces formatting time strings that are also four hours earlier:

print strftime('%c',$stamp_now);
print strftime('%c',$stamp_future);
Wed Aug  7 20:53:41 2002
               Mon Jun 4 11:25:00 2012
            

See Also

Recipe 3.4 for how to convert an epoch timestamp back to time and date parts; documentation on mktime( ) at http://www.php.net/mktime and gmmktime( ) at http://www.php.net/gmmktime.

Converting an Epoch Timestamp to Time and Date Parts

Problem

You want the set of time and date parts that corresponds to an epoch timestamp.

Solution

Pass an epoch timestamp to getdate( ) :

$time_parts = getdate(163727100);

Discussion

The time parts returned by getdate( ) are detailed in Table 3-1. These time parts are in local time. If you want time parts in another time zone corresponding to a particular epoch timestamp, see Recipe 3.12.

See Also

Recipe 3.3 for how to convert time and date parts back to epoch timestamps; Recipe 3.12 for how to deal with time zones; documentation on getdate( ) at http://www.php.net/getdate.

Printing a Date or Time in a Specified Format

Problem

You need to print out a date or time formatted in a particular way.

Solution

Use date( ) or strftime( ):

print strftime('%c');
print date('m/d/Y');
Tue Jul 30 11:31:08 2002
               07/30/2002
            

Discussion

Both date( ) and strftime( ) are flexible functions that can produce a formatted time string with a variety of components. The formatting characters for these functions are listed in Table 3-3. The Windows column indicates whether the formatting character is supported by strftime( ) on Windows systems.

Table 3-3. strftime( ) and date( ) format characters

Type strftime( ) date( ) Description Range Windows
Hour %H H Hour, numeric, 24-hour clock 00-23 Yes
Hour %I h Hour, numeric, 12-hour clock 01-12 Yes
Hour %k Hour, numeric, 24-hour clock, leading zero as space 0-23 No
Hour %l Hour, numeric, 12-hour clock, leading zero as space 1-12 No
Hour %p A AM or PM designation for current locale Yes
Hour %P a am/pm designation for current locale No
Hour G Hour, numeric, 24-hour clock, leading zero trimmed 0-23 No
Hour g Hour, numeric, 12-hour clock, leading zero trimmed 0-1 No
Minute %M I Minute, numeric 00-59 Yes
Second %S s Second, numeric 00-61[1] Yes
Day %d d Day of the month, numeric 01-31 Yes
Day %e Day of the month, numeric, leading zero as space 1-31 No
Day %j z Day of the year, numeric 001-366 for strftime( ); 0-365 for date( ) Yes
Day %u Day of the week, numeric (Monday is 1) 1-7 No
Day %w w Day of the week, numeric (Sunday is 0) 0-6 Yes
Day j Day of the month, numeric, leading zero trimmed 1-31 No
Day S English ordinal suffix for day of the month, textual "st," "th," "nd," "rd" No
Week %a D Abbreviated weekday name, text for current locale Yes
Week %A l Full weekday name, text for current locale Yes
Week %U Week number in the year; numeric; first Sunday is the first day of the first week 00-53 Yes
Week %V W ISO 8601:1988 week number in the year; numeric; week 1 is the first week that has at least 4 days in the current year; Monday is the first day of the week 01-53 No
Week %W Week number in the year; numeric; first Monday is the first day of the first week 00-53 Yes
Month %B F Full month name, text for current locale Yes
Month %b M Abbreviated month name, text for current locale Yes
Month %h Same as %b No
Month %m m Month, numeric 01-12 Yes
Month n Month, numeric, leading zero trimmed 1-12 No
Month t Month length in days, numeric 28, 29, 30, 31 No
Year %C Century, numeric 00-99 No
Year %g Like %G, but without the century 00-99 No
Year %G ISO 8601 year with century; numeric; the four-digit year corresponding to the ISO week number; same as %y except if the ISO week number belongs to the previous or next year, that year is used instead No
Year %y y Year without century, numeric 00-99 Yes
Year %Y Y Year, numeric, including century Yes
Year L Leap year flag (yes is 1) 0, 1 No
Timezone %z O Hour offset from GMT, +/-HHMM (e.g., -0400, +0230) -1200-+1200 Yes, but acts like %Z
Timezone %Z T Time zone, name, or abbreviation; textual Yes
Timezone I Daylight saving time flag (yes is 1) 0, 1 No
Timezone Z Seconds offset from GMT; west of GMT is negative, east of GMT is positive -43200-43200 No
Compound %c Standard date and time format for current locale Yes
Compound %D Same as %m/%d/%y No
Compound %F Same as %Y-%m-%d No
Compound %r Time in AM or PM notation for current locale No
Compound %R Time in 24-hour notation for current locale No
Compound %T Time in 24-hour notation (same as %H:%M:%S) No
Compound %x Standard date format for current locale(without time) Yes
Compound %X Standard time format for current locale(without date) Yes
Compound r RFC 822 formatted date (e.g., "Thu, 22 Aug 2002 16:01:07 +0200") No
Other %s U Seconds since the epoch No
Other B Swatch Internet time No
Formatting %% Literal % character Yes
Formatting %n Newline character No
Formatting %t Tab character No
[1]The range for seconds extends to 61 to account for leap seconds.


The first argument to each function is a format string, and the second argument is an epoch timestamp. If you leave out the second argument, both functions default to the current date and time. While date( ) and strftime( ) operate over local time, they each have UTC-centric counterparts (gmdate( ) and gmstrftime( )).

The formatting characters for date( ) are PHP-specific, but strftime( ) uses the C-library strftime( ) function. This may make strftime( ) more understandable to someone coming to PHP from another language, but it also makes its behavior slightly different on various platforms. Windows doesn't support as many strftime( ) formatting commands as most Unix-based systems. Also, strftime( ) expects its formatting characters to each be preceded by a % (think printf( )), so it's easier to produce strings with lots of interpolated time and date values in them.

For example, at 12:49 P.M. on July 15, 2002, the code to print out:

It's after 12 pm on July 15, 2002

with strftime( ) looks like:

print strftime("It's after %I %P on %B %d, %Y");

With date( ) it looks like:

print "It's after ".date('h a').' on '.date('F d, Y');

Non-date-related characters in a format string are fine for strftime( ), because it looks for the % character to decide where to interpolate the appropriate time information. However, date( ) doesn't have such a delimiter, so about the only extras you can tuck into the formatting string are spaces and punctuation. If you pass strftime( )'s formatting string to date( ):

print date("It's after %I %P on %B%d, %Y");

you'd almost certainly not want what you'd get:

131'44 pmf31eMon, 15 Jul 2002 12:49:44 -0400 %1 %P o7 %742%15, %2002

To generate time parts with date( ) that are easy to interpolate, group all time and date parts from date( ) into one string, separating the different components with a delimiter that date( ) won't translate into anything and that isn't itself part of one of your substrings. Then, using explode( ) with that delimiter character, put each piece of the return value from date( ) in an array, which is easily interpolated in your output string:

$ar = explode(':',date("h a:F d, Y"));
print "It's after $ar[0] on $ar[1]";

See Also

Documentation on date( ) at http://www.php.net/date and strftime( ) at http://www.php.net/strftime; on Unix-based systems, man strftime for your system-specific strftime( ) options; on Windows, see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/_crt_strftime.2c_.wcsftime.asp for strftime( ) details.

Finding the Difference of Two Dates

Problem

You want to find the elapsed time between two dates. For example, you want to tell a user how long it's been since she last logged onto your site.

Solution

Convert both dates to epoch timestamps and subtract one from the other. Use this code to separate the difference into weeks, days, hours, minutes, and seconds:

// 7:32:56 pm on May 10, 1965
$epoch_1 = mktime(19,32,56,5,10,1965);
// 4:29:11 am on November 20, 1962
$epoch_2 = mktime(4,29,11,11,20,1962);

$diff_seconds  = $epoch_1 - $epoch_2;
$diff_weeks    = floor($diff_seconds/604800);
$diff_seconds -= $diff_weeks   * 604800;
$diff_days     = floor($diff_seconds/86400);
$diff_seconds -= $diff_days    * 86400;
$diff_hours    = floor($diff_seconds/3600);
$diff_seconds -= $diff_hours   * 3600;
$diff_minutes  = floor($diff_seconds/60);
$diff_seconds -= $diff_minutes * 60;

print "The two dates have $diff_weeks weeks, $diff_days days, ";
print "$diff_hours hours, $diff_minutes minutes, and $diff_seconds ";
print "seconds elapsed between them.";
The two dates have 128 weeks, 6 days, 14 hours, 3 minutes, 
               and 45 seconds elapsed between them.
            

Note that the difference isn't divided into larger chunks than weeks (i.e., months or years) because those chunks have variable length and wouldn't give an accurate count of the time difference calculated.

Discussion

There are a few strange things going on here that you should be aware of. First of all, 1962 and 1965 precede the beginning of the epoch. Fortunately, mktime( ) fails gracefully here and produces negative epoch timestamps for each. This is okay because the absolute time value of either of these questionable timestamps isn't necessary, just the difference between the two. As long as epoch timestamps for the dates fall within the range of a signed integer, their difference is calculated correctly.

Next, a wall clock (or calendar) reflects a slightly different amount of time change between these two dates, because they are on different sides of a DST switch. The result subtracting epoch timestamps gives is the correct amount of elapsed time, but the perceived human time change is an hour off. For example, on the Sunday morning in April when DST is activated, what's the difference between 1:30 A.M. and 4:30 A.M.? It seems like three hours, but the epoch timestamps for these two times are only 7,200 seconds apart — two hours. When a local clock springs forward an hour (or falls back an hour in October), the steady march of epoch timestamps takes no notice. Truly, only two hours have passed, although our clock manipulations make it seem like three.

If you want to measure actual elapsed time (and you usually do), this method is fine. If you're more concerned with the difference in what a clock says at two points in time, use Julian days to compute the interval, as discussed in Recipe 3.7.

To tell a user the elapsed time since her last login, you need to find the difference between the login time and her last login time:

$epoch_1 = time();
$r = mysql_query("SELECT UNIX_TIMESTAMP(last_login) AS login 
                 FROM user WHERE id = $id") or die();
$ob = mysql_fetch_object($r);
$epoch_2 = $ob->login;

$diff_seconds  = $epoch_1 - $epoch_2;
$diff_weeks    = floor($diff_seconds/604800);
$diff_seconds -= $diff_weeks   * 604800;
$diff_days     = floor($diff_seconds/86400);
$diff_seconds -= $diff_days    * 86400;
$diff_hours    = floor($diff_seconds/3600);
$diff_seconds -= $diff_hours   * 3600;
$diff_minutes  = floor($diff_seconds/60);
$diff_seconds -= $diff_minutes * 60;

print "You last logged in $diff_weeks weeks, $diff_days days, ";
print "$diff_hours hours, $diff_minutes minutes, and $diff_seconds ago.";

See Also

Recipe 3.7 to find the difference between two dates with Julian days; Recipe 3.11 for adding and subtracting from a date; documentation on MySQL's UNIX_TIMESTAMP( ) function at http://www.mysql.com/doc/D/a/Date_and_time_functions.html.

Finding the Difference of Two Dates with Julian Days

Problem

You want to find the difference of two dates measured by what a clock would say, not the actual elapsed time.

Solution

Use gregoriantojd( ) to get the Julian day for a set of date parts, then subtract one Julian day from the other to find the date difference. Then convert the time parts to seconds and subtract one from the other to find the time difference. If the time difference is less than 0, decrease the date difference by one and adjust the time difference to apply to the previous day. Here's the code:

$diff_date = gregoriantojd($date_1_mo, $date_1_dy, $date_1_yr) -
             gregoriantojd($date_2_mo, $date_2_dy, $date_2_yr);
$diff_time = $date_1_hr * 3600 + $date_1_mn * 60 + $date_1_sc -
             $date_2_hr * 3600 - $date_2_mn * 60 - $date_2_sc;
if ($diff_time < 0) {
   $diff_date--; 
   $diff_time = 86400 - $diff_time;
}

Discussion

Finding differences with Julian days lets you operate outside the range of epoch seconds and also accounts for DST differences.

If you have the components of your two days in arrays:

// 7:32:56 pm on May 10, 1965
list($date_1_yr, $date_1_mo, $date_1_dy, $date_1_hr, $date_1_mn, $date_1_sc)=
    array(1965, 5, 10, 19, 32, 56);
// 4:29:11 am on November 20, 1962
list($date_2_yr, $date_2_mo, $date_2_dy, $date_2_hr, $date_2_mn, $date_2_sc)=
    array(1962, 11, 20, 4, 29, 11);

$diff_date = gregoriantojd($date_1_mo, $date_1_dy, $date_1_yr) -
             gregoriantojd($date_2_mo, $date_2_dy, $date_2_yr);
$diff_time = $date_1_hr * 3600 + $date_1_mn * 60 + $date_1_sc -
             $date_2_hr * 3600 - $date_2_mn * 60 - $date_2_sc;
if ($diff_time < 0) {
   $diff_date--; 
   $diff_time = 86400 - $diff_time;
}
$diff_weeks = floor($diff_date/7); $diff_date -= $diff_weeks * 7;
$diff_hours = floor($diff_time/3600); $diff_time -= $diff_hours * 3600;
$diff_minutes = floor($diff_time/60); $diff_time -= $diff_minutes * 60;

print "The two dates have $diff_weeks weeks, $diff_date days, ";
print "$diff_hours hours, $diff_minutes minutes, and $diff_time ";
print "seconds between them.";
The two dates have 128 weeks, 6 days, 15 hours, 3 minutes,
               and 45 seconds between them.
            

This method produces a time difference based on clock time, which is why the result shows an hour more of difference than in Recipe 3.6. May 10 is during DST, and November 11 is during standard time.

The function gregoriantojd( ) is part of PHP's calendar extension, and so is available only if that extension is loaded.

See Also

Recipe 3.6 to find the difference between two dates in elapsed time; Recipe 3.11 for adding and subtracting from a date; documentation on gregoriantojd( ) at http://www.php.net/gregoriantojd; an overview of the Julian Day system is at http://tycho.usno.navy.mil/mjd.html.

Finding the Day in a Week, Month, Year, or the Week Number in a Year

Problem

You want to know the day or week of the year, the day of the week, or the day of the month. For example, you want to print a special message every Monday, or on the first of every month.

Solution

Use the appropriate arguments to date( ) or strftime( ):

print strftime("Today is day %d of the month and %j of the year.");
print 'Today is day '.date('d').' of the month and '.date('z').' of the year.';

Discussion

The two functions date( ) and strftime( ) don't behave identically. Days of the year start with 0 for date( ), but with 1 for strftime( ). Table 3-4 contains all the day and week number format characters date( ) and strftime( ) understand.

Table 3-4. Day and week number format characters

Type strftime( ) date( ) Description Range
Day %d d Day of the month, numeric 01-31
Day %j z Day of the year, numeric 001-366 for strftime( ); 0-365 for date( )
Day %u Day of the week, numeric (Monday is 1) 1-7
Day %w w Day of the week, numeric (Sunday is 0) 0-6
Day %W ISO 8601 day of the week, numeric (Monday is the first day of the week) 0-6
Week %U Week number in the year; numeric; first Sunday is the first day of the first week 00-53
Week %V
W
ISO 8601:1988 week number in the year; numeric; week 1 is the first week that has at least four days in the current year; Monday is the first day of the week 01-53


To print out something only on Mondays, use the w formatting character to date( ) or the %w string with strftime( ):

if (1 == date('w')) {
    print "Welcome to the beginning of your work week.";
}

if (1 == strftime('%w')) {
    print "Only 4 more days until the weekend!";
}

There are different ways to calculate week numbers and days in a week, so be careful to choose the appropriate one. The ISO standard (ISO 8601), says that weeks begin on Mondays and that the days in the week are numbered 1 (Monday) through 7 (Sunday). Week 1 in a year is the first week in a year with a Thursday in that year. This means the first week in a year is the first week with a majority of its days in that year. These week numbers range from 01 to 53.

Other week number standards range from 00 to 53, with days in a year's week 53 potentially overlapping with days in the following year's week 00.

As long as you're consistent within your programs, you shouldn't run into any trouble, but be careful when interfacing with other PHP programs or your database. For example, MySQL's DAYOFWEEK( ) function treats Sunday as the first day of the week, but numbers the days 1 to 7, which is the ODBC standard. Its WEEKDAY( ) function, however, treats Monday as the first day of the week and numbers the days from 0 to 6. Its WEEK( ) function lets you choose whether weeks should start on Sunday or Monday, but it's incompatible with the ISO standard.

See Also

Documentation on date( ) at http://www.php.net/date and strftime( ) at http://www.php.net/strftime; MySQL's DAYOFWEEK( ), WEEKDAY( ), and WEEK( ) functions are documented at http://www.mysql.com/doc/D/a/Date_and_time_functions.html.

Validating a Date

Problem

You want to check if a date is valid. For example, you want to make sure a user hasn't provided a birthdate such as February 30, 1962.

Solution

Use checkdate( ):

$valid = checkdate($month,$day,$year);

Discussion

The function checkdate( ) returns true if $month is between 1 and 12, $year is between 1 and 32767, and $day is between 1 and the correct maximum number of days for $month and $year. Leap years are correctly handled by checkdate( ), and dates are rendered using the Gregorian calendar.

Because checkdate( ) has such a broad range of valid years, you should do additional validation on user input if, for example, you're expecting a valid birthdate. The Guinness Book of World Records says the oldest person ever reached 122. To check that a birthdate indicates a user between 18 and 122 years old, use the pc_checkbirthdate( ) function shown in Example 3-1.

Example 3-1. pc_checkbirthdate( )

function pc_checkbirthdate($month,$day,$year) {
    $min_age = 18;
    $max_age = 122;

    if (! checkdate($month,$day,$year)) {
        return false;
    }

    list($this_year,$this_month,$this_day) = explode(',',date('Y,m,d'));

    $min_year = $this_year - $max_age;
    $max_year = $this_year - $min_age;

    print "$min_year,$max_year,$month,$day,$year\n";
    
    if (($year > $min_year) && ($year < $max_year)) {
        return true;
    } elseif (($year == $max_year) && 
              (($month < $this_month) ||
               (($month == $this_month) && ($day <= $this_day)))) {
        return true;
    } elseif (($year == $min_year) &&
              (($month > $this_month) ||
               (($month == $this_month && ($day > $this_day))))) {
        return true;
    } else {
        return false;
    }
}

Here is some sample usage:

// check December 3, 1974
if (pc_checkbirthdate(12,3,1974)) {
    print "You may use this web site.";
} else {
    print "You are too young to proceed.";
    exit( );
}

This function first uses checkdate( ) to make sure that $month, $day, and $year represent a valid date. Various comparisons then make sure that the supplied date is in the range set by $min_age and $max_age.

If $year is noninclusively between $min_year and $max_year, the date is definitely within the range, and the function returns true. If not, some additional checks are required. If $year equals $max_year (e.g., in 2002, $year is 1984), $month must be before the current month. If $month equals the current month, $day must be before or equal to the current day. If $year equals $min_year (e.g., in 2002, $year is 1880), $month must be after the current month. If $month equals the current month, $day must be after the current day. If none of these conditions are met, the supplied date is outside the appropriate range, and the function returns false.

The function returns true if the supplied date is exactly $min_age years before the current date, but false if the supplied date is exactly $max_age years after the current date. That is, it would let you through on your 18th birthday, but not on your 123rd.

See Also

Documentation on checkdate( ) at http://www.php.net/checkdate; information about The Guinness Book's oldest person is at http://www.guinnessworldrecords.com (navigate to "The Human Body," "Age and Youth," and then "Oldest Woman Ever").

Parsing Dates and Times from Strings

Problem

You need to get a date or time in a string into a format you can use in calculations. For example, you want to convert date expressions such as "last Thursday" into an epoch timestamp.

Solution

The simplest way to parse a date or time string is with strtotime( ) , which turns a variety of human-readable date and time strings into epoch timestamps:

$a = strtotime('march 10'); // defaults to the current year

Discussion

The grammar strtotime( ) uses is both complicated and comprehensive so the best way to get comfortable with it is to try out lots of different time expressions. If you're curious about its nuts and bolts, check out ext/standard/parsedate.y in the PHP source distribution.

The function strtotime( ) understands words about the current time:

$a = strtotime('now');
print strftime('%c',$a);
$a = strtotime('today');
print strftime('%c',$a);
Mon Aug 12 20:35:10 2002
               Mon Aug 12 20:35:10 2002
            

It understands different ways to identify a time and date:

$a = strtotime('5/12/1994');
print strftime('%c',$a);
$a = strtotime('12 may 1994');
print strftime('%c',$a);
Thu May 12 00:00:00 1994
               Thu May 12 00:00:00 1994
            

It understands relative times and dates:

$a = strtotime('last thursday');   // On August 12, 2002
print strftime('%c',$a);
$a = strtotime('2001-07-12 2pm + 1 month');
print strftime('%c',$a);
Thu Aug  8 00:00:00 2002
               Mon Aug 12 14:00:00 2002
            

It understands time zones. When the following is run from a computer in EDT, it prints out the same time:

$a = strtotime('2002-07-12 2pm edt + 1 month');
print strftime('%c',$a);
Mon Aug 12 14:00:00 2002
            

However, when the following is run from a computer in EDT, it prints out the time in EDT when it is 2 P.M. in MDT (two hours before EDT):

$a = strtotime('2002-07-12 2pm mdt + 1 month');
print strftime('%c',$a);
Mon Aug 12 16:00:00 2002
            

If the date and time you want to parse out of a string are in a format you know in advance, instead of calling strtotime( ), you can build a regular expression that grabs the different date and time parts you need. For example, here's how to parse "YYYY-MM-DD HH:MM:SS" dates, such as a MySQL DATETIME field:

$date = '1974-12-03 05:12:56';
preg_match('/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/',$date,$date_parts);

This puts the year, month, day, hour, minute, and second into $date_parts[1] through $date_parts[6]. (preg_match( ) puts the entire matched expression into $date_parts[0].)

You can use regular expressions to pull the date and time out of a larger string that might also contain other information (from user input, or a file you're reading), but if you're sure about the position of the date in the string you're parsing, you can use substr( ) to make it even faster:

$date_parts[0] = substr($date,0,4);
$date_parts[1] = substr($date,5,2);
$date_parts[2] = substr($date,8,2);
$date_parts[3] = substr($date,11,2);
$date_parts[4] = substr($date,14,2);
$date_parts[5] = substr($date,17,2);

You can also use split( );

$ar = split('[- :]',$date);
print_r($ar);
Array
               (
                   [0] => 1974
                   [1] => 12
                   [2] => 03
                   [3] => 05
                   [4] => 12
                   [5] => 56
               )
            

Be careful: PHP converts between numbers and strings without any prompting, but numbers beginning with a 0 are considered to be in octal (base 8). So, 03 and 05 are 3 and 5; but, 08 and 09 are not 8 and 9.

preg_match( ) and strtotime( ) are equally efficient in parsing a date format such as "YYYY-MM-DD HH:MM:SS", but ereg( ) is about four times slower than either. If you need the individual parts of the date string, preg_match( ) is more convenient, but strtotime( ) is obviously much more flexible.

See Also

Documentation on strtotime( ) at http://www.php.net/strtotime; the grammar for strtotime( ) is available at http://cvs.php.net/cvs.php/php4/ext/standard/parsedate.y.

Adding to or Subtracting from a Date

Problem

You need to add or subtract an interval from a date.

Solution

Depending on how your date and interval are represented, use strtotime( ) or some simple arithmetic.

If you have your date and interval in appropriate formats, the easiest thing to do is use strtotime( ) :

$birthday = 'March 10, 1975';
$whoopee_made = strtotime("$birthday - 9 months ago");

If your date in an epoch timestamp and you can express your interval in seconds, subtract the interval from the timestamp:

$birthday = 163727100;
$gestation = 36 * 7 * 86400; // 36 weeks
$whoopee_made = $birthday - $gestation;

Discussion

Using strtotime( ) is good for intervals that are of varying lengths, like months. If you can't use strtotime( ), you can convert your date to an epoch timestamp and add or subtract the appropriate interval in seconds. This is mostly useful for intervals of a fixed time, such as days or weeks:

$now = time( );
$next_week = $now + 7 * 86400;

Using this method, however, you can run into problems if the endpoints of your interval are on different sides of a DST switch. In this case, one of your fixed length days isn't 86,400 seconds long; it's either 82,800 or 90,000 seconds long, depending on the season. If you use UTC exclusively in your application, you don't have to worry about this. But if you have to use local time, you can count days without worrying about this hiccup with Julian days. You can convert between epoch timestamps and Julian days with unixtojd( ) and jdtounix( ):

$now = time( );
$today = unixtojd($now);
$next_week = jdtounix($today + 7);
// don't forget to add back hours, minutes, and seconds
$next_week += 3600 * date('H',$now) + 60 * date('i',$now) + date('s',$now);

The functions unixtojd( ) and jdtounix( ) are part of PHP's calendar extension, so they are only available if that extension is loaded.

See Also

Recipe 3.6 for finding the difference between two dates in elapsed time; Recipe 3.7 for finding the difference between two dates in Julian days; documentation on strtotime( ) at http://www.php.net/strtotime, unixtojd( ) at http://www.php.net/unixtojd, and jdtounix( ) at http://www.php.net/jdtounix.

Calculating Time with Time Zones

Problem

You need to calculate times in different time zones. For example, you want to give users information adjusted to their local time, not the local time of your server.

Solution

For simple calculations, you can explicitly add or subtract the offsets between two time zones:

// If local time is EST
$time_parts = localtime();
// California (PST) is three hours earlier
$california_time_parts = localtime(time() - 3 * 3600);

On Unix-based systems, if you don't know the offsets between time zones, just set the TZ environment variable to your target time zone:

putenv('TZ=PST8PDT');
$california_time_parts = localtime();

Discussion

Before we sink too deeply into the ins and outs of time zones, we want to pass along the disclaimer that the U.S. Naval Observatory offers at http://tycho.usno.navy.mil/tzones.html. Namely, official worldwide time-zone information is somewhat fragile "because nations are sovereign powers that can and do change their timekeeping systems as they see fit." So, remembering that we are at the mercy of the vagaries of international relations, here are some ways to cope with Earth's many time zones.

For a relatively simple treatment of offsets between time zones, use an array in your program that has the various time zones' offsets from UTC. Once you determine what time zone your user is in, just add that offset to the appropriate UTC time and the functions that print out UTC time (e.g., gmdate( ), gmstrftime( )) can print out the correct adjusted time.

// Find the current time 
$now = time();

// California is 8 hours behind UTC
$now += $pc_timezones['PST'];

// Use gmdate() or gmstrftime() to print California-appropriate time
print gmstrftime('%c',$now);

The previous code uses this $pc_timezones array, which contains offsets from UTC:

// From Perl's Time::Timezone
$pc_timezones = array(
  'GMT'  =>   0,           // Greenwich Mean
  'UTC'  =>   0,           // Universal (Coordinated)
  'WET'  =>   0,           // Western European
  'WAT'  =>  -1*3600,      // West Africa
  'AT'   =>  -2*3600,      // Azores
  'NFT'  =>  -3*3600-1800, // Newfoundland
  'AST'  =>  -4*3600,      // Atlantic Standard
  'EST'  =>  -5*3600,      // Eastern Standard
  'CST'  =>  -6*3600,      // Central Standard
  'MST'  =>  -7*3600,      // Mountain Standard
  'PST'  =>  -8*3600,      // Pacific Standard
  'YST'  =>  -9*3600,      // Yukon Standard
  'HST'  => -10*3600,      // Hawaii Standard
  'CAT'  => -10*3600,      // Central Alaska
  'AHST' => -10*3600,      // Alaska-Hawaii Standard
  'NT'   => -11*3600,      // Nome
  'IDLW' => -12*3600,      // International Date Line West
  'CET'  =>  +1*3600,      // Central European
  'MET'  =>  +1*3600,      // Middle European
  'MEWT' =>  +1*3600,      // Middle European Winter
  'SWT'  =>  +1*3600,      // Swedish Winter
  'FWT'  =>  +1*3600,      // French Winter
  'EET'  =>  +2*3600,      // Eastern Europe, USSR Zone 1
  'BT'   =>  +3*3600,      // Baghdad, USSR Zone 2
  'IT'   =>  +3*3600+1800, // Iran
  'ZP4'  =>  +4*3600,      // USSR Zone 3
  'ZP5'  =>  +5*3600,      // USSR Zone 4
  'IST'  =>  +5*3600+1800, // Indian Standard
  'ZP6'  =>  +6*3600,      // USSR Zone 5
  'SST'  =>  +7*3600,      // South Sumatra, USSR Zone 6
  'WAST' =>  +7*3600,      // West Australian Standard
  'JT'   =>  +7*3600+1800, // Java 
  'CCT'  =>  +8*3600,      // China Coast, USSR Zone 7
  'JST'  =>  +9*3600,      // Japan Standard, USSR Zone 8
  'CAST' =>  +9*3600+1800, // Central Australian Standard
  'EAST' => +10*3600,      // Eastern Australian Standard
  'GST'  => +10*3600,      // Guam Standard, USSR Zone 9
  'NZT'  => +12*3600,      // New Zealand
  'NZST' => +12*3600,      // New Zealand Standard
  'IDLE' => +12*3600       // International Date Line East
);

On Unix systems, you can use the zoneinfo library to do the conversions. This makes your code more compact and also transparently handles DST, as discussed in Recipe 3.13.

To take advantage of zoneinfo in PHP, do all your internal date math with epoch timestamps. Generate them from time parts with the pc_mktime( ) function shown in Example 3-2.

Example 3-2. pc_mktime( )

function pc_mktime($tz,$hr,$min,$sec,$mon,$day,$yr) {
    putenv("TZ=$tz");
    $a = mktime($hr,$min,$sec,$mon,$day,$yr);
    putenv('TZ=EST5EDT');   // change EST5EDT to your server's time zone!
    return $a;
}

Calling putenv( ) before mktime( ) fools the system functions mktime( ) uses into thinking they're in a different time zone. After the call to mktime( ), the correct time zone has to be restored. On the East Coast of the United States, that's EST5EDT. Change this to the appropriate value for your computer's location (see Table 3-5).

Time parts are turned into epoch timestamps by pc_mktime( ). Its counterpart, to turn epoch timestamps into formatted time strings and time parts, is pc_strftime( ), shown in Example 3-3.

Example 3-3. pc_strftime( )

function pc_strftime($tz,$format,$timestamp) {
    putenv("TZ=$tz");
    $a = strftime($format,$timestamp);
    putenv('TZ=EST5EDT');   // change EST5EDT to your server's time zone!
    return $a;
}

This example uses the same system-function-fooling pc_mktime( ) does to get the right results from strftime( ).

The great thing about these functions is that you don't have to worry about the offsets from UTC of different time zones, whether DST is in effect, or any other irregularities of time-zone differences. You just set the appropriate zone and let your system's libraries do the rest.

Note that the value of the $tz variable in both these functions should not be a time-zone name but a zoneinfo zone. zoneinfo zones are more specific than time zones, because they correspond to particular places. Table 3-5 contains mappings for appropriate zoneinfo zones for some UTC offsets. The last column indicates whether the zone observes DST.

Table 3-5. zoneinfo zones

UTC offset (hours) UTC offset (seconds) zoneinfo zone DST?
-12 -43200 Etc/GMT+12 No
-11 -39600 Pacific/Midway No
-10 -36000 US/Aleutian Yes
-10 -36000 Pacific/Honolulu No
-9 -32400 America/Anchorage Yes
-9 -32400 Etc/GMT+9 No
-8 -28800 PST8PDT Yes
-8 -28800 America/Dawson_Creek No
-7 -25200 MST7MDT Yes
-7 -25200 MST No
-6 -21600 CST6CDT Yes
-6 -21600 Canada/Saskatchewan No
-5 -18000 EST5EDT Yes
-5 -18000 EST No
-4 -14400 America/Halifax Yes
-4 -14400 America/Puerto_Rico No
-3.5 -12600 America/St_Johns Yes
-3 -10800 America/Buenos_Aires No
0 0 Europe/London Yes
0 0 GMT No
1 3600 CET Yes
1 3600 GMT-1 No
2 7200 EET No
2 7200 GMT-2 No
3 10800 Asia/Baghdad Yes
3 10800 GMT-3 No
3.5 12600 Asia/Tehran Yes
4 14400 Asia/Dubai No
4 14400 Asia/Baku Yes
4.5 16200 Asia/Kabul No
5 18000 Asia/Tashkent No
5.5 19800 Asia/Calcutta No
5.75 20700 Asia/Katmandu No
6 21600 Asia/Novosibirsk Yes
6 21600 Etc/GMT-6 No
6.5 23400 Asia/Rangoon No
7 25200 Asia/Jakarta No
8 28800 Hongkong No
9 32400 Japan No
9.5 34200 Australia/Darwin No
10 36000 Australia/Sydney Yes
10 36000 Pacific/Guam No
12 43200 Etc/GMT-13 No
12 43200 Pacific/Auckland Yes


Countries around the world don't begin and end DST observance on the same days or at the same times. To calculate time appropriately for an international DST-observing location, pick a zoneinfo zone that matches your desired location as specifically as possible.

See Also

Recipe 3.13 for dealing with DST; documentation on putenv( ) at http://www.php.net/putenv, localtime( ) at http://www.php.net/localtime, gmdate( ) at http://www.php.net/gmdate, and gmstrftime( ) at http://www.php.net/gmstrftime; zoneinfo zone names and longitude and latitude coordinates for hundreds of places around the world are available at ftp://elsie.nci.nih.gov/pub/tzdata2002c.tar.gz; many links to historical and technical information about time zones can be found at http://www.twinsun.com/tz/tz-link.htm.

Accounting for Daylight Saving Time

Problem

You need to make sure your time calculations properly consider DST.

Solution

The zoneinfo library calculates the effects of DST properly. If you are using a Unix-based system, take advantage of zoneinfo with putenv( ):

putenv('TZ=MST7MDT');
print strftime('%c');

If you can't use zoneinfo, you can modify hardcoded time-zone offsets based on whether the local time zone is currently observing DST. Use localtime( ) to determine the current DST observance status:

// Find the current UTC time 
$now = time();

// California is 8 hours behind UTC
$now -= 8 * 3600;

// Is it DST? 
$ar = localtime($now,true);
if ($ar['tm_isdst']) { $now += 3600; }

// Use gmdate() or gmstrftime() to print California-appropriate time
print gmstrftime('%c',$now);

Discussion

Altering an epoch timestamp by the amount of a time zone's offset from UTC and then using gmdate( ) or gmstrftime( ) to print out time zone-appropriate functions is flexible — it works from any time zone — but the DST calculations are slightly inaccurate. For the brief intervals when the server's DST status is different from the target time zone's, the results are incorrect. For example, at 3:30 A.M. EDT on the first Sunday in April (after the switch to DST), it's still before the switch (11:30 P.M.) in the Pacific time zone. A server in Eastern time using this method calculates California time to be seven hours behind UTC, whereas it's actually eight hours. At 6:00 A.M. EDT (3:00 A.M. PDT), both Pacific and Eastern time are observing DST, and the calculation is correct again (putting California at seven hours behind UTC).

See Also

Recipe 3.12 for dealing with time zones; documentation on putenv( ) at http://www.php.net/putenv, localtime( ) at http://www.php.net/localtime, gmdate( ) at http://www.php.net/gmdate, and gmstrftime( ) at http://www.php.net/gmstrftime; a detailed presentation on DST is at http://webexhibits.org/daylightsaving/.

Generating a High-Precision Time

Problem

You need to measure time with finer than one-second resolution, for example to generate a unique ID.

Solution

Use microtime( ) :

list($microseconds,$seconds) = explode(' ',microtime());

Discussion

The function microtime( ) returns a string that contains the microseconds part of elapsed time since the epoch, a space, and seconds since the epoch. For example, a return value of 0.41644100 1026683258 means that 1026683258.41644100 seconds have elapsed since the epoch. A string is returned instead of a double because the double doesn't have enough capacity to hold the entire value with microsecond precision.

Time including microseconds is useful for generating unique IDs. When combined with the current process ID, it guarantees a unique ID, as long as a process doesn't generate more than one ID per microsecond:

list($microseconds,$seconds) = explode(' ',microtime());
$id = $seconds.$microseconds.getmypid();

However, this method is not as foolproof on multithreaded systems, where there is a nonzero (but very tiny) chance that two threads of the same process could call microtime( ) simultaneously.

See Also

Documentation on microtime( ) at http://www.php.net/microtime.

Generating Time Ranges

Problem

You need to know all the days in a week or a month. For example, you want to print out a list of appointments for a week.

Solution

Identify your start date using time( ) and strftime( ). If your interval has a fixed length, you can loop through that many days. If not, you need to test each subsequent day for membership in your desired range.

For example, a week has seven days, so you can use a fixed loop to generate all the days in the current week:

// generate a time range for this week
$now = time();

// If it's before 3 AM, increment $now, so we don't get caught by DST
// when moving back to the beginning of the week
if (3 < strftime('%H', $now)) { $now += 7200; }

// What day of the week is today?
$today = strftime('%w', $now);

// How many days ago was the start of the week?
$start_day = $now - (86400 * $today);

// Print out each day of the week
for ($i = 0; $i < 7; $i++) {
  print strftime('%c',$start_day + 86400 * $i);
}

Discussion

A particular month or year could have a variable number of days, so you need to compute the end of the time range based on the specifics of that month or year. To loop through every day in a month, find the epoch timestamps for the first day of the month and the first day of the next month. The loop variable, $day is incremented a day at a time (86400 seconds) until it's no longer less than the epoch timestamp at the beginning of the next month:

// Generate a time range for this month
$now = time();

// If it's before 3 AM, increment $now, so we don't get caught by DST
// when moving back to the beginning of the month
if (3 < strftime('%H', $now)) { $now += 7200; }

// What month is this?
$this_month = strftime('%m',$now);

// Epoch timestamp for midnight on the first day of this month
$day = mktime(0,0,0,$this_month,1);
// Epoch timestamp for midnight on the first day of next month
$month_end = mktime(0,0,0,$this_month+1,1);

while ($day < $month_end) {
  print strftime('%c',$day); 
  $day += 86400;
}

See Also

Documentation on time( ) at http://www.php.net/time and strftime( ) at http://www.php.net/strftime.

Using Non-Gregorian Calendars

Problem

You want to use a non-Gregorian calendar, such as a Julian, Jewish, or French Republican calendar.

Solution

PHP's calendar extension provides conversion functions for working with the Julian calendar, as well as the French Republican and Jewish calendars. To use these functions, the calendar extension must be loaded.

These functions use the Julian day count (which is different than the Julian calendar) as their intermediate format to move information between them.

The two functions jdtogregorian( ) and gregoriantojd( ) convert between Julian days and the familiar Gregorian calendar:

$jd = gregoriantojd(3,9,1876);      // March 9, 1876; $jd = 2406323

$gregorian = jdtogregorian($jd);    // $gregorian = 3/9/1876

The valid range for the Gregorian calendar is 4714 BCE to 9999 CE.

Discussion

To convert between Julian days and the Julian calendar, use jdtojulian( ) and juliantojd( ):

// February 29, 1900 (not a Gregorian leap year)
$jd = juliantojd(2,29,1900);      // $jd = 2415092
$julian = jdtojulian($jd);        // $julian = 2/29/1900
$gregorian = jdtogregorian($jd);  // $gregorian = 3/13/1900

The valid range for the Julian calendar is 4713 BCE to 9999 CE, but since it was created in 46 BCE, you run the risk of annoying Julian calendar purists if you use it for dates before that.

To convert between Julian days and the French Republican calendar, use jdtofrench( ) and frenchtojd( ):

$jd = frenchtojd(8,13,11);       // 13 floréal XI; $jd = 2379714
$french = jdtofrench($jd);       // $french = 8/13/11
$gregorian = jdtofregorian($jd); // $gregorian = 5/3/1803; sale of Louisiana to U.S.

The valid range for the French Republican calendar is September 1792 to September 1806, which is small, but since the calendar was only in use from October 1793 to January 1806, it's comprehensive enough.

To convert between Julian days and the Jewish calendar, use jdtojewish( ) and jewishtojd( ):

$jd = JewishToJD(6,14,5761);      // Adar 14, 5761; $jd = 2451978
$jewish = JDToJewish($jd);        // $jewish = 6/14/5761
$gregorian = JDToGregorian($jd);  // $gregorian = 3/9/2001

The valid range for the Jewish calendar starts with 3761 BCE (year 1 on the Jewish calendar).

See Also

Documentation for the calendar functions at http://www.php.net/calendar; the history of the Gregorian calendar is explained at http://scienceworld.wolfram.com/astronomy/GregorianCalendar.html.

Program: Calendar

The pc_calendar() function shown in Example 3-4 prints out a month's calendar, similar to the Unix cal program. Here's how you can use the function:

// print the calendar for the current month
list($month,$year) = explode(',',date('m,Y'));
pc_calendar($month,$year);

The pc_calendar( ) function prints out a table with a month's calendar in it. It provides links to the previous and next month and highlights the current day.

Example 3-4. pc_calendar( )

<?php
function pc_calendar($month,$year,$opts = '') {
    // set default options //
    if (! is_array($opts)) { $opts = array(); }
    if (! isset($opts['today_color'])) { $opts['today_color'] = '#FFFF00'; }
    if (! isset($opts['month_link'])) {
        $opts['month_link'] = 
            '<a href="'.$_SERVER['PHP_SELF'].'?month=%d&year=%d">%s</a>';
    }
    
    list($this_month,$this_year,$this_day) = split(',',strftime('%m,%Y,%d'));
    $day_highlight = (($this_month == $month) && ($this_year == $year));
    
    list($prev_month,$prev_year) = 
        split(',',strftime('%m,%Y',mktime(0,0,0,$month-1,1,$year)));
    $prev_month_link = sprintf($opts['month_link'],$prev_month,$prev_year,'&lt;');
    
    list($next_month,$next_year) = 
        split(',',strftime('%m,%Y',mktime(0,0,0,$month+1,1,$year)));
    $next_month_link = sprintf($opts['month_link'],$next_month,$next_year,'&gt;');
    
?>
<table border="0" cellspacing="0" cellpadding="2" align="center">
        <tr>
                <td align="left">
                        <?php print $prev_month_link ?>
                </td>
                <td colspan="5" align="center">
                <?php print strftime('%B %Y',mktime(0,0,0,$month,1,$year)); ?>
                </td>
                <td align="right">
                        <?php print $next_month_link ?>
                </td>
        </tr>
<?php
    $totaldays = date('t',mktime(0,0,0,$month,1,$year));
 
    // print out days of the week
    print '<tr>';
    $weekdays = array('Su','Mo','Tu','We','Th','Fr','Sa');
    while (list($k,$v) = each($weekdays)) {
        print '<td align="center">'.$v.'</td>';
    }
    print '</tr><tr>';
    // align the first day of the month with the right week day
    $day_offset = date("w",mktime(0, 0, 0, $month, 1, $year));
    if ($day_offset > 0) { 
        for ($i = 0; $i < $day_offset; $i++) { print '<td>&nbsp;</td>'; }
    }
    $yesterday = time() - 86400; 

    // print out the days
    for ($day = 1; $day <= $totaldays; $day++) {
        $day_secs = mktime(0,0,0,$month,$day,$year);
        if ($day_secs >= $yesterday) {  
            if ($day_highlight && ($day == $this_day)) {
                print sprintf('<td align="center" bgcolor="%s">%d</td>',
                              $opts['today_color'],$day);
            } else {
                print sprintf('<td align="center">%d</td>',$day);
            }
        } else {
            print sprintf('<td align="center">%d</td>',$day);
        }
        $day_offset++;

        // start a new row each week // 
        if ($day_offset == 7) {
            $day_offset = 0;
            print "</tr>\n";
            if ($day < $totaldays) { print '<tr>'; }
        }
    }
    // fill in the last week with blanks //
    if ($day_offset > 0) { $day_offset = 7 - $day_offset; }
    if ($day_offset > 0) { 
        for ($i = 0; $i < $day_offset; $i++) { print '<td>&nbsp;</td>'; }
    }
    print '</tr></table>';
}
?>

The pc_calendar( ) function begins by checking options passed to it in $opts. The color that the current day is highlighted with can be passed as an RGB value in $opts['today_color']. This defaults to #FFFF00, bright yellow. Also, you can pass a printf( )-style format string in $opts['month_link'] to change how the links to the previous and next months are printed.

Next, the function sets $day_highlight to true if the month and year for the calendar match the current month and year. The links to the previous month and next month are put into $prev_month_link and $next_month_link using the format string in $opts['month_link'].

pc_calendar( ) then prints out the top of the HTML table that contains the calendar and a table row of weekday abbreviations. Using the day of the week returned from strftime('%w'), blank table cells are printed so the first day of the month is aligned with the appropriate day of the week. For example, if the first day of the month is a Tuesday, two blank cells have to be printed to occupy the slots under Sunday and Monday in the first row of the table.

After this preliminary information has been printed, pc_calendar( ) loops through all the days in the month. It prints a plain table cell for most days, but a table cell with a different background color for the current day. When $day_offset reaches 7, a week has completed, and a new table row needs to start.

Once a table cell has been printed for each day in the month, blank cells are added to fill out the last row of the table. For example, if the last day of the month is a Thursday, two cells are added to occupy the slots under Friday and Saturday. Last, the table is closed, and the calendar is complete.

Notes

  1. The range for seconds extends to 61 to account for leap seconds.
Personal tools