Thread: Determining the Date of the [nth] [Day] in [Month]

  1. #1
    Just Lurking Dave_Sinkula's Avatar
    Join Date
    Oct 2002
    Posts
    5,005

    Determining the Date of the [nth] [Day] in [Month]

    I'm looking for an algorithm to calculate the date of the last [day] in a given [month]. For example, the last Sunday in March.

    You may have guessed that this relates to Daylight Saving/British Summer Time. We want something user-entered but algorithmically calculated (so you enter the rule and thereafter don't have to change the dates every year), such that Americans can set it up our way, and Europeans can set it up their way. That type of thing.

    I've tried searching the web, and again wandered through some fine documents that reminded me of exactly why it has that I disliked working with date and time. I've been trying search terms such as algorithm calculate "last Sunday of March", but coming up a little empty. Maybe I've just chosen bad search terms.

    But if anyone knows a resource, I thank you for pointing me the right way.
    7. It is easier to write an incorrect program than understand a correct one.
    40. There are two ways to write error-free programs; only the third one works.*

  2. #2
    Registered User
    Join Date
    Sep 2006
    Posts
    8,868
    Hey Dave,

    We don't need no *stinking resource* ( OK, I don't have a link ), but algorithmically speaking there seems to be two approaches (just first blush, haven't written a smidgen of code on this, yet).

    1) Knowing today's date and time (and it requires the system's date and time to be right), you just use this, and the "knuckle rule" of "30 days hath September, April June and November, 31 hath all the rest, except February". (If you run your fingers across the top of the knuckles of one hand, correctly, the high points are months with 31 days, the low points between the knuckles have 30 days or less).

    2) Taking one year as the "cornerstone", say the Last Sunday in March was the 26th in 2005, now we can surmise what it will be in the next year by knowing what any day will move (forward and backward), in a normal year, and in a cycle of years. If you want to see what the blazes I'm talking about, go into a calendar year, select one day, and then keep incrementing the years. You'll see what I mean.

    Both schemes need to have the leap year (every 4th year, + every year divisible by 100, etc), adjustment, factored into it.

    You know more about date and time coding in C than I do, but I do love the challenge of a puzzling algorithm solution. You'll probably find something to use, but if you want some assistance with algorithm development, let me know.

  3. #3
    and the hat of int overfl Salem's Avatar
    Join Date
    Aug 2001
    Location
    The edge of the known universe
    Posts
    39,659
    Do a mktime() for the first day of the following month.
    See what day of the week that is.
    Subtract that many days from the tm_day field.
    Do another mktime() on it to normalise it, and it will be the last Sunday of the previous month.
    If you dance barefoot on the broken glass of undefined behaviour, you've got to expect the occasional cut.
    If at first you don't succeed, try writing your phone number on the exam paper.

  4. #4
    Just Lurking Dave_Sinkula's Avatar
    Join Date
    Oct 2002
    Posts
    5,005
    Quote Originally Posted by Salem View Post
    Do a mktime() for the first day of the following month.
    See what day of the week that is.
    Subtract that many days from the tm_day field.
    Do another mktime() on it to normalise it, and it will be the last Sunday of the previous month.
    So simple only the brightest can make it sound simple! Thank you very much.

    ---

    I should have noted that it's not only the last [day], but I also need to do first, second, etc. as well.

    Some further searching produced this as well::

    I basically think I'm trying to boil them into a grand unified rule.

    But I gotta call it a night.


    ---

    [And the stuff in blue is what I had written after I got back to this question, but before I had seen Salem's reply.]

    BTW, thanks Adak for the reply. I believe I've already got lookup tables for the "knuckles".

    To those who may have followed my story from earlier, I've implemented my time handling such that it matches the PC runtime library for testing, using an algorithmic approach with the new US rules (which are incorrect for years prior to 2007). My epoch goes from Jan 1, 2000 until 128 years later, which gives me a "dead zone" -- invalid dates -- as well as a not-unrolled loop that always has 8 paths. Blah, blah, blah.

    I had next implemented -- or at least started -- a second form for Daylight Saving changeover calculation, given that every year the end user would have to enter the current year's changeover dates. I forget how that turned out. But basically both of these methods will get to play second banana/fiddle/whatever to this new rule system I am to develop.


    ---

    Thanks all, even those who hadn't posted. These efforts are greatly appreciated. But I gotta get some sleep.
    7. It is easier to write an incorrect program than understand a correct one.
    40. There are two ways to write error-free programs; only the third one works.*

  5. #5
    Just Lurking Dave_Sinkula's Avatar
    Join Date
    Oct 2002
    Posts
    5,005

    Update

    After implementing it first very closely to what you described and then turning some attention to how to do the 1st, 2nd, etc. weeks I realized that I could just view the last as the "-1th" of the following month.

    Here is the current state of the code for those interested:
    Code:
    #include <stdio.h>
    #include <time.h>
    
    /**
     * Determine the [nth] [day] of [month] in a given year. For example, "find the
     * last Sunday in March", or "find the Second Tuesday in August".
     * 
     * @param  week   week offset [0=1st,1=2nd,-1=last of previous month]
     * @param  wday   the day of week of interest [0-6, 0=Sunday]
     * @param  month  the month in question [0-11]
     * @param  year   the year in question [4-digit year; i.e. 2007]
     * 
     * @return pointer to a static structure containing the broken-down time
     *         corresponding to the function parameters; null pointer on failure
     * 
     * @credit
     * <A HREF="http://cboard.cprogramming.com/showthread.php?p=645662#post645662">Salem</A>
     * Do a mktime() for the first day of the following month. See what day of the
     * week that is. Subtract that many days from the tm_day field. Do another
     * mktime() on it to normalise it, and it will be the last Sunday of the
     * previous month.
     */
    struct tm *nth_weekday_of_month(int week, int wday, int month, int year)
    {
        struct tm worker = {0}, *result;
        time_t t;
        /*
         * Build the first day of the month.
         */
        worker.tm_year = year - 1900;
        worker.tm_mon  = month;
        worker.tm_mday = 1;
        /*
         * Create the calendar time. Then normalize it back to broken-down form.
         */
        t = mktime(&worker);
        if ( t == (time_t)-1 )
        {
            return 0;
        }
        result = localtime(&t);
        if ( !result )
        {
            return 0;
        }
        /*
         * Go to Nth week, where N is an offset from the first week.
         *   N =  1 is the 2nd week
         *   N =  0 is the 1st week
         *   N = -1 is the last week of the previous month
         */
        if ( wday != result->tm_wday )
        {
            result->tm_mday += wday - result->tm_wday + 7 * (wday < result->tm_wday);
        }
        result->tm_mday += 7 * week;
        /*
         * Re-normalize it.
         */
        t = mktime(result);
        if ( t == (time_t)-1 )
        {
            return 0;
        }
        result = localtime(&t);
        if ( !result )
        {
            return 0;
        }
        return result;
    }
    
    int main(void)
    {
        int wday, year, week;
        puts("Last Sunday of March over a number of years.");
        for ( year = 2000; year < 2012; ++year )
        {
            /* Last Sunday of March is the -1th Sunday of April. */
            struct tm *bst = nth_weekday_of_month(-1, 0/*Sunday*/, 3/*April*/, year);
            if ( bst )
            {
                char *text = asctime(bst);
                text[24] = '\0';
                puts(text);
            }
        }
        puts("Last Sunday, Monday, Tuesday, etc. of March this year.");
        for ( wday = 0; wday < 7; ++wday )
        {
            struct tm *bst = nth_weekday_of_month(-1, wday, 3/*April*/, year);
            if ( bst )
            {
                char *text = asctime(bst);
                text[24] = '\0';
                puts(text);
            }
        }
        puts("Nth Sunday of month (0=first):");
        for ( year = 2000, week = -1; week < 6; ++week )
        {
            printf("week &#37;d\n", week);
            for ( wday = 0; wday < 7; ++wday )
            {
                struct tm *bst = nth_weekday_of_month(week, wday, 0, year);
                if ( bst )
                {
                    char *text = asctime(bst);
                    text[24] = '\0';
                    puts(text);
                }
            }
        }
        return 0;
    }
    So not that much to it once you get to the manipulation and ignore a little of the translation overhead. Thanks again Salem.

    The way this will work in the device is that the DST/BST changeover date will be specified by 3 fields
    • 1st | 2nd | 3rd | 4th | last
    • Sun | Mon | Tue | Wed | Thu | Fri | Sat
      of
    • Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

    I believe then a second related specification of time of change and amount/direction of change, as in
    02:00 by +1:00
    or
    02:00 by -1:00
    Last edited by Dave_Sinkula; 05-23-2007 at 03:35 PM. Reason: Doh! And it's got a bug in it's primary purpose now. :( / Later fixed (I hope).
    7. It is easier to write an incorrect program than understand a correct one.
    40. There are two ways to write error-free programs; only the third one works.*

  6. #6
    Just Lurking Dave_Sinkula's Avatar
    Join Date
    Oct 2002
    Posts
    5,005

    Another Update

    And had I paid closer attention to Salem's wonderfully concise statement, perhaps if I had read up a little more on mktime, or even looked at my own implementation of mktime, I would have realized that the normalization I was doing by adding a follow-up call to localtime is extraneous. mktime has to do the adjustment -- which I had already forgotten. Even though it was where I had an epiphany as to why a non-const pointer to a struct tm is specified.

    I will now bore y'all with more yammering.
    Code:
    #include <stdio.h>
    #include <time.h>
    
    static const struct tm zero = {0};
    static struct tm worker = {0};
    
    struct tm *nth_weekday_of_month(int week, int wday, int month, int year)
    {
       struct tm *result = &worker;
       time_t t;
       *result = zero;
       /*
        * Build the first day of the month.
        */
       worker.tm_year = year;
       worker.tm_mon  = month;
       worker.tm_mday = 1;
       /*
        * Create the calendar time. Then normalize it back to broken-down form.
        */
       t = mktime(result);
       if ( t == (time_t)-1 )
       {
          return 0;
       }
       /*
        * Go to Nth week, where N is an offset from the first week.
        *   N =  1 is the 2nd week
        *   N =  0 is the 1st week
        *   N = -1 is the last week of the previous month
        */
       if ( wday != result->tm_wday )
       {
          result->tm_mday += wday - result->tm_wday + 7 * (wday < result->tm_wday);
       }
       result->tm_mday += 7 * week;
       /*
        * Re-normalize it.
        */
       t = mktime(result);
       if ( t == (time_t)-1 )
       {
          return 0;
       }
       return result;
    }
    
    void test_last_sunday_of_march(void)
    {
       int year;
       puts("Last Sunday of March over a number of years.");
       for ( year = 100; year < 112; ++year )
       {
          /* Last Sunday of March is the -1th Sunday of April. */
          struct tm *bst = nth_weekday_of_month(-1, 0/*Sunday*/, 3/*April*/, year);
          if ( bst )
          {
             char *text = asctime(bst);
             text[24] = '\0';
             puts(text);
          }
       }
    }
    
    void test_last_wday_of_march(void)
    {
       int wday;
       puts("Last Sunday, Monday, Tuesday, etc. of March in 2007.");
       for ( wday = 0; wday < 7; ++wday )
       {
          struct tm *bst = nth_weekday_of_month(-1, wday, 3/*April*/, 107);
          if ( bst )
          {
             char *text = asctime(bst);
             text[24] = '\0';
             puts(text);
          }
       }
    }
    
    void test_nth_week(void)
    {
       int wday, week;
       puts("Nth Sunday of month (0=first):");
       for ( week = -1; week < 6; ++week )
       {
          printf("week %d\n", week);
          for ( wday = 0; wday < 7; ++wday )
          {
             struct tm *bst = nth_weekday_of_month(week, wday, 0, 107);
             if ( bst )
             {
                char *text = asctime(bst);
                text[24] = '\0';
                puts(text);
             }
          }
       }
    }
    
    void holidays(void)
    {
       char text[BUFSIZ];
       int year;
       puts("Variable-Date Holidays:");
       for ( year = 100; year < 112; ++year )
       {
          struct tm *dst;
    
          /* MLK */
          dst = nth_weekday_of_month(2, 1, 0, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Martin Luther King holiday\n", text);
          }
    
          /* Washington's birthday observed */
          dst = nth_weekday_of_month(2, 1, 1, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Washington's birthday observed\n", text);
          }
    
          /* Daylight Saving time begins */
          dst = nth_weekday_of_month(-1, 0, 3, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Daylight Saving time begins\n", text);
          }
    
          /* Armed Forces Day */
          dst = nth_weekday_of_month(2, 6, 4, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Armed Forces Day\n", text);
          }
    
          /* Memorial Day */
          dst = nth_weekday_of_month(-1, 1, 5, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Memorial Day\n", text);
          }
    
          /* Labor Day */
          dst = nth_weekday_of_month(0, 1, 8, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Labor Day\n", text);
          }
    
          /* Columbus Day */
          dst = nth_weekday_of_month(1, 1, 9, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Columbus Day\n", text);
          }
    
          /* Daylight Saving time ends */
          dst = nth_weekday_of_month(-1, 0, 10, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Daylight Saving time ends\n", text);
          }
    
          /* Thanksgiving Day */
          dst = nth_weekday_of_month(3, 4, 10, year);
          if ( dst )
          {
             strftime(text, sizeof text, "%a %b %d %Y", dst);
             printf("%s Thanksgiving Day\n", text);
          }
    
          putchar('\n');
       }
    }
    
    int main(void)
    {
       test_last_sunday_of_march();
       test_last_wday_of_march();
       test_nth_week();
       holidays();
       return 0;
    }
    To test other areas, you may note that I played around with some variable-date US holidays. I haven't really checked them, other than to make sure Labor Day and Memorial Day were on a Monday, or that Thanksgiving was on a Thursday.

    Also of note was another issue that popped up regarding the static struct. I'm not really working on this area of the code so much at the moment. I'm more in the setter for DST start/end dates. But I had been drawn back to this because of a circular issue with my mktime updating the struct tm, but the nth_weekday_of_month calling mktime, which needed to check for DST. I has half expecting it and it didn't take too long to confirm.

    Note for those implementing your own time library: mktime, localtime, and gmtime benefit from internal helper functions.
    7. It is easier to write an incorrect program than understand a correct one.
    40. There are two ways to write error-free programs; only the third one works.*

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Advancing day by day until it matches a second date
    By nhubred in forum C++ Programming
    Replies: 1
    Last Post: 05-30-2009, 08:55 AM
  2. Checking array for string
    By Ayreon in forum C Programming
    Replies: 87
    Last Post: 03-09-2009, 03:25 PM
  3. Date program starts DOS's date
    By jrahhali in forum C++ Programming
    Replies: 1
    Last Post: 11-24-2003, 05:23 PM
  4. CDate Class - handle date manipulation simply
    By LuckY in forum C++ Programming
    Replies: 5
    Last Post: 07-16-2003, 08:35 AM