determining the nth occurence of a day of the week in any given month

March 12, 2010 01:22 by tomg

Just the other day I ran into a situation on Microsoft Windows Embedded CE 6.0 where I needed a C/C++ method to determine the date for a given day of the week for a specific month and year.  This problem comes up in a number of scenarios such as scheduling, and, as in my particular case, in the calculation of Daylight Savings Time and Standard Time transition dates.

For example, in Microsoft Windows, the dates for the beginning and ending of Daylight Savings Time (DST) are stored in the registry and are retrieved in a TIME_ZONE_INFORMATION structure using the GetTimeZoneInformation API.  The TIME_ZONE_INFORMATION fields StandardDate and DaylightDate are SYSTEMTIME structures containing the date and local time when the transition to Standard Time and Daylight Time occurs, respectively.  However, because DST begins and ends on a different date from year to year, a special interpretation is applied to the SYSTEMTIME fields.  If the SYSTEMTIME in the registry contains a non-zero value for wYear, the SYSTEMTIME is interpreted as an absolute date and time that specifies a one-time transition.  But in the typical case, the wYear field will be zero, and the wDayOfWeek field indicates the weekday for the transition and the wDay field contains a value from 1 to 5 that indicates the occurrence of the day of the week within the month, with 5 meaning the absolute last occurrence during the month even if that day of the week does not occur 5 times.  So, for the current US Eastern Time Zone rules, DaylightDate specifies the 2nd Sunday of the March at 2:00am, so wHour = 2, wMinute = 0, wMonth = 3, wDayOfWeek = 0 (Sunday) and wDay = 2.

In order to determine whether or not some arbitrary date falls within DST or not, it is necessary to determine the DST begin and end dates for a given year.  There are undoubtedly countless ways to do this, but a short and easy method is given below.  This solution makes use of the SystemTimeToFileTime and FileTimeToSystemTime APIs, and a “trick” that utilizes the fact the SystemTimeToFileTime ignores the wDayOfWeek value whereas the FileTimeToSystemTime function populates the wDayOfWeek field with the correct value.  In fact, just calling these two functions back to back is an easy way to determine the day of the week for any given date.

The function BOOL DaylightDateForYear(SYSTEMTIME *pst, WORD year) below takes two parameters: a pointer to a SYSTEMTIME in the typical TIME_ZONE_INFORMATION format where wYear is zero and wMonth, wDayOfWeek and wDay give a specific occurrence of a day of the week in the specified month, and a year for which to calculate the transition date.  If you need a more generalized function for something like a scheduling program, you could easily change the function name and parameters to something like BOOL GetNthDayOfWeek(SYSTEMTIME *pst, WORD day_of_week, WORD occurrence, WORD month, WORD year) and populate the values of local SYSTEMTIME structure st accordingly.  Anyhow, here’s the code, and I hope you’ll find it useful:

 

BOOL DaylightDateForYear(SYSTEMTIME *pst, WORD year)
{
    SYSTEMTIME st = *pst;
    BOOL bValid;

    // Verify input is proper format
    if (st.wYear != 0)
        return FALSE;
    if ((st.wDay  < 1)  || (st.wDay > 5))
        return FALSE;
    if (st.wDayOfWeek > 6)
        return FALSE;

    // Plug in the specified year
    st.wYear = year;

    // Determine day of week of 1st day of month
    st.wDay = 1;
    FILETIME ft;
    SystemTimeToFileTime(&st, &ft);
    FileTimeToSystemTime(&ft, &st);

    int nDayOff;
    
    // Determine offset to first target WeekDay
    nDayOff = (int)pst->wDayOfWeek - (int)st.wDayOfWeek;
    if (nDayOff < 0)
        nDayOff += 7;

    // Offset to wDay occurence of WeekDay
    nDayOff += ((pst->wDay-1) * 7);
    st.wDay = nDayOff + 1;
    bValid = SystemTimeToFileTime(&st, &ft);
    if (bValid)
    {
        FileTimeToSystemTime(&ft, pst);
        return bValid;
    }
    else if (pst->wDay == 5)    // Only allow backup of 1 week if wDay = 5 (last occurrence in month)
    {
        nDayOff -= 7;
        st.wDay = nDayOff + 1;
        bValid = SystemTimeToFileTime(&st, &ft);
        FileTimeToSystemTime(&ft, pst);
        return bValid;
    }
    else
        return FALSE;
}