I am writing a class for holding dates in c++, and I found the following problem:
I have a number of days N
since a reference date (in
I made a number of failed attempts at solving Gregorian date problems over the years. I developed this code about 15 years ago, and it continues to perform well. Because I wrote versions of this code so long ago, it's in native C, but is easily compiled into C++ programs. Feel free to wrap these in a Date class, if you like.
My code is based on combining all the leap year rules into a 400-year cycle. Under Gregorian leap year rules, every 400-year cycle has exactly 146,097 days.
An optimization I employed is to move January and February to the end of the prior year. This makes the leap day (if present) always fall on the last day of the year. That allows me to build a table (dayOffset) which provides the distance in days from March 1. Because the leap day would fall at the end, this table is accurate for leap- and non-leap-years.
I'll begin with the header file.
#if !defined( TIMECODE_H_ )
#define TIMECODE_H_ 1
#if defined(__cplusplus)
extern "C" {
#endif
int dateCode( int month, int dayOfMonth, int year );
void decodeDate( int *monthPtr, int *dayOfMonthPtr, int *yearPtr, int dateCode );
int dayOfWeek( int dateCode );
int cardinalCode( int nth, int weekday, int month, int year );
enum Weekdays { eMonday, eTuesday, eWednesday, eThursday, eFriday, eSaturday, eSunday };
#if defined(__cplusplus)
}
#endif
#endif
The API consists of four methods: dateCode() calculates the date code for a Gregorian date. decodeDate() calculates the Gregorian month, day and year from a date code. dayOfWeek() returns the day of the week for a date code. cardinalCode() returns the date code for a "cardinal" day of a specific month (for example, the 2nd Wednesday of August 2014).
Here's the implementation:
#include
enum
{
nbrOfDaysPer400Years = 146097,
nbrOfDaysPer100Years = 36524,
nbrOfDaysPer4Years = 1461,
nbrOfDaysPerYear = 365,
unixEpochBeginsOnDay = 135080
};
const int dayOffset[] =
{
0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337, 366
};
/* ------------------------------------------------------------------------------------ */
int mod( int dividend, int divisor, int* quotientPtr )
{
*quotientPtr = (int)floor( (double)dividend / divisor );
return dividend - divisor * *quotientPtr;
}
/* ------------------------------------------------------------------------------------ */
int dateCode( int month, int dayOfMonth, int year )
{
int days;
int temp;
int bYday;
/*
we take the approach of starting the year on March 1 so that leap days fall
at the end. To do this we pretend Jan. - Feb. are part of the previous year.
*/
int bYear = year - 1600;
bYday = dayOffset[ mod( month - 3, 12, &temp ) ] + dayOfMonth - 1;
bYear += temp;
bYear = mod( bYear, 400, &days );
days *= nbrOfDaysPer400Years;
bYear = mod( bYear, 100, &temp );
days += nbrOfDaysPer100Years * temp;
bYear = mod( bYear, 4, &temp );
days += nbrOfDaysPer4Years * temp + nbrOfDaysPerYear * bYear + bYday -
unixEpochBeginsOnDay;
return days;
}
/* ------------------------------------------------------------------------------------ */
int dayOfWeek( int dateCode )
{
int temp;
return mod( dateCode + 3, 7, &temp );
}
/* ------------------------------------------------------------------------------------ */
void decodeDate( int *monthPtr, int *dayOfMonthPtr, int *yearPtr, int dateCode )
{
int diff;
int diff2;
int alpha;
int beta;
int gamma;
int year;
int temp;
/* dateCode has the number of days relative to 1/1/1970, shift this back to 3/1/1600 */
dateCode += unixEpochBeginsOnDay;
dateCode = mod( dateCode, nbrOfDaysPer400Years, &temp );
year = 400 * temp;
dateCode = mod( dateCode, nbrOfDaysPer100Years, &temp );
/* put the leap day at the end of 400-year cycle */
if ( temp == 4 )
{
--temp;
dateCode += nbrOfDaysPer100Years;
}
year += 100 * temp;
dateCode = mod( dateCode, nbrOfDaysPer4Years, &temp );
year += 4 * temp;
dateCode = mod( dateCode, nbrOfDaysPerYear, &temp );
/* put the leap day at the end of 4-year cycle */
if ( temp == 4 )
{
--temp;
dateCode += nbrOfDaysPerYear;
}
year += temp;
/* find the month in the table */
alpha = 0;
beta = 11;
gamma = 0;
for(;;)
{
gamma = ( alpha + beta ) / 2;
diff = dayOffset[ gamma ] - dateCode;
if ( diff < 0 )
{
diff2 = dayOffset[ gamma + 1 ] - dateCode;
if ( diff2 < 0 )
{
alpha = gamma + 1;
}
else if ( diff2 == 0 )
{
++gamma;
break;
}
else
{
break;
}
}
else if ( diff == 0 )
{
break;
}
else
{
beta = gamma;
}
}
if ( gamma >= 10 )
{
++year;
}
*yearPtr = year + 1600;
*monthPtr = ( ( gamma + 2 ) % 12 ) + 1;
*dayOfMonthPtr = dateCode - dayOffset[ gamma ] + 1;
}
/* ------------------------------------------------------------------------------------ */
int cardinalCode( int nth, int weekday, int month, int year )
{
int dow1st;
int dc = dateCode( month, 1, year );
dow1st = dayOfWeek( dc );
if ( weekday < dow1st )
{
weekday += 7;
}
if ( nth < 0 || nth > 4 )
{
nth = 4;
}
dc += weekday - dow1st + 7 * nth;
if ( nth == 4 )
{
/* check that the fifth week is actually in the same month */
int tMonth, tDayOfMonth, tYear;
decodeDate( &tMonth, &tDayOfMonth, &tYear, dc );
if ( tMonth != month )
{
dc -= 7;
}
}
return dc;
}
One issue with efficiency that will be immediately apparent is the mod() function. As you might expect, it provides the quotient and remainder of two integral dividends. C/C++ provides the modulus operator (%) which would seem to be a better choice; however, the standards don't specify how this operation should handle negative dividends. (See here for more information).
There is probably a portable solution which uses efficient integer math; however, I've opted here for one that is slightly less efficient, but guaranteed correct on all platforms.
A date code is simply an offset in days from a base date. I chose 1600-March-01 because it's the start of a 400-year Gregorian cycle that is early enough so that all the dates we are likely to encounter will result in a date code that is a positive integer. However, there's nothing incorrect about date codes before the base date. Since we're using a stable/portable modulo operation, all the math works well for negative date codes.
Some don't like my non-standard base date, so I decided to adopt the standard Unix epoch, which begins 1970-January-01. I defined unixEpochBeginsOnDay to bias the date code to start on the desired date. If you want to use a different base date, you would replace this value with one you prefer.
Calculating a date code is as simple as passing the month, dayOfMonth and year to dateCode():
int dc = dateCode( 2, 21, 2001 ); // returns date code for 2001-Feb-21
I've written dateCode so that it will accept values that are out of range for month and dayOfMonth. You can think of month as one plus the integer number of months after January of the given year. Here's a few tests to demonstrate:
assert(dateCode( 14, 1, 2000 ) == dateCode( 2, 1, 2001 ));
assert(dateCode( 5, 32, 2005 ) == dateCode( 6, 1, 2005 ));
assert(dateCode( 0, 1, 2014 ) == dateCode(12, 1, 2013 ));
Calling dateCode with non-canoncial month and dayOfMonth values, then converting back with decodeDate, is an effective way to canonicalize dates. For example:
int m, d, y;
decodeDate( &m, &d, &y, dateCode( 8, 20 + 90, 2014 ));
printf("90 days after 2014-08-20 is %4d-%02d-%02d\n", y, m, d);
The output should be:
90 days after 2014-08-20 is 2014-11-18
decodeDate() always produces canonical values for month and dayOfMonth.
dayOfWeek() simply returns the modulus 7 of the dateCode, but I had to bias dateCode by 3 since 1970-January-01 was Thursday. If you prefer to start your week on a different day than Monday, then fix the Weekdays enum and change the bias as desired.
cardinalCode() provides an interesting application of these methods. The first parameter provides the week number of the month ("nth"), and the second parameter provides the weekday. So to find the fourth Saturday in August 2007, you would:
int m, d, y;
decodeDate( &m, &d, &y, cardinalCode( 3, eSaturday, 8, 2007 ) );
printf( "%d/%02d/%d\n", m, d, y );
Which produces the answer:
8/25/2007
Note that the nth parameter, 3, in the example above specifies the fourth Saturday. I debated whether this parameter should be zero-based or one-based. For whatever reason, I settled on: 0=first, 1=second, 2=third, etc. Even the shortest months have four occurrences of every weekday. The value 4 has a special meaning. One would expect it to return the fifth occurrence of the requested weekday; however, since the month may or may not have a fifth occurrence, I decided to return the last occurrence of the month.
For example, to display the last Monday of each month next year:
int i, m, d, y;
for (i=1; i <= 12; ++i) {
decodeDate( &m, &d, &y, cardinalCode( 4, eMonday, i, 2015 ) );
printf( "%d/%02d/%d\n", m, d, y );
}
One final example, illustrating one use for cardinalCode(), displaying the number of days until the next general election:
#include
#include /* only needed for time() and localtime() calls */
#include "datecode.h"
void main()
{
int eYear, eday, dc;
int eY, eM, eD;
time_t now;
struct tm bdtm;
time(&now);
if (localtime_r(&now, &bdtm) == NULL) {
printf("Error\n");
return 1;
}
eYear = bdtm.tm_year + 1900;
dc = dateCode(bdtm.tm_mon + 1, bdtm.tm_mday, eYear);
if ((eYear % 2) != 0) {
++eYear;
}
for(;;) {
eday = cardinalCode(0, eTuesday, 11, eYear);
if (eday >= dc) break;
eYear += 2; /* move to the next election! */
}
decodeDate(&eM, &eD, &eY, eday);
printf("Today is %d/%02d/%d\neday is %d/%02d/%d, %d days from today.\n",
bdtm.tm_mon + 1, bdtm.tm_mday, bdtm.tm_year + 1900,
eM, eD, eY, eday - dc);
}