2

Synopsis:

I want to find the week number of a given date with variable starting days and variable anchoring days for the week.

Full version:

I need to calculate the week any given day lands on out of a year, but I need to be able to change what defines a week. Defining a week boils down to two factors:

  • Choosing whether the week goes from Sunday to Saturday or Monday to Sunday.

  • Choosing what day anchors a week.

The start and end of the week is self-explanatory, it simply will decide which Sunday to count in a week, the preceding Sunday or the following Sunday (or if the date we are looking at is a Sunday, decide to which week it belongs). The anchor day will determine year rollover. Assume Wednesday is in yr1 but the next day (Thursday) is in yr2:

  • Case 1: Wednesday defines the week, and therefore the following portion of the week is also part of year yr1, week 52 (sometimes 53 if the year has 53 of the anchor day).
  • Case 2: Thursday defines the week, and therefore the previous portion of the week is also in year yr2, week 01.

I am using a struct tm * to capture the date and time I want to convert, so I have lots of data to work with I simply don't know what manipulations to make in order to calculate this correctly. I know that the function strftime can toss out a week number 00-53 and can even pick which day the week starts on between Sunday and Monday, but there is no way to alter the anchor day of a week so year rollover doesn't work that way.

Any ideas or suggestions are appreciated as this issue won't just go away on its own! Thanks!

Edit: The code to capture the time in a struct tm * is:

time_t time_stamp;
struct tm *time_local;

time(&time_stamp); // fills in the current time in seconds, GMT
time_local = localtime(&time_stamp); // translates seconds GMT to tm in local time zone

The data coming in to these variables is all correct, the right date and time, timezone, etc.

Community
  • 1
  • 1
Lucas Burns
  • 393
  • 1
  • 3
  • 14
  • 1
    "I am using a `struct tm *` to . . . " Please post the [Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) that shows what you have done. This question seems to be "thinking out loud" and I hope that asking it helps you. Moving to the [MCVE](http://stackoverflow.com/help/mcve) is likely to help you more - just by the action. – Weather Vane Jan 11 '18 at 21:25
  • Welcome to Stack Overflow. Please read [What topics can I ask about here?](https://stackoverflow.com/help/on-topic) and [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask) – klutt Jan 11 '18 at 21:27
  • Pick your day of the year of and divide by 7. Then take the remainder (use modulo if you want), and your remainder is the offset from your original day (ie anchor day Monday, remainder 1 is tuesday). If in the next year the first day is something else, then just set that as your base day and repeat. Use an array and a simple for loop and or use modulo. Forget about struct. It is an overcomplication. – Tyler S. Loeper Jan 11 '18 at 21:31
  • Do you really need this flexibility? – MiCo Jan 11 '18 at 21:31
  • 2
    Don't do too much at once. Start with fixed conditions, get that working. Then improve by small steps to your goal. One step at a time. That allows you to rigorously check each step. The alternative is to write a wall of code, and then when it falls over: where do you start to fix it? – Weather Vane Jan 11 '18 at 21:33
  • @MiCo Yes I need the flexibility, its for a shipping program and shipping data in the past here started on Sundays then moved to Mondays, and the anchor day has changed from Monday, to Wednesday, and now is Thursday, but I still need to be able to accurately get info for past dates. – Lucas Burns Jan 11 '18 at 21:37
  • @WeatherVane I would start small if there wasn't a whole library already written I have to integrate this into. It has to work with lots of other pieces which do similar similar tasks with the same data and can't be altered. – Lucas Burns Jan 11 '18 at 21:38
  • OK, otherwise I would go with the week number definition in ISO 8601 which states that the 4. January lies always in the 1. week of the year and that the week starts with Monday. – MiCo Jan 11 '18 at 21:39
  • 1
    Please look at https://stackoverflow.com/questions/274861/how-do-i-calculate-the-week-number-given-a-date and http://www.calendar-week.org/ – MiCo Jan 11 '18 at 21:47
  • When a "week" - what ever that staring day - straddles Dec/Jan, that week, per ISO8601, belongs to the year in which most of its days lie. – chux - Reinstate Monica Jan 11 '18 at 21:49
  • @chux The ISO8601 standards are not something I have to consider here, as this is an already extant internal only application. I need to create this to account for the terrible way everyone around me has been doing things for the last 30+ years, not for the standards. – Lucas Burns Jan 11 '18 at 21:54
  • To find the week of the year for a date, find the 1) a = d_o_year(y,m,d), 2) b = d_o_week(y,1,1) 3) c = first-day-of-the-week-choice. Then `week_number = foo(a,b,c);`. Of course, now `foo(a,b,c)` is another tricky bit. – chux - Reinstate Monica Jan 11 '18 at 21:54
  • 1
    @LucasBurns Yet ISO 8601 does provide guidance on how to deal with resolve the likely squishy definitions of past date rules in which you need to cope - good luck. – chux - Reinstate Monica Jan 11 '18 at 21:56
  • @LucasBurns Having done something similar, in the end you will likely have a function with `bar(y,m,d,select_initial_week_day)` returning 1) week-of-the-year-for-the-date **and** 2) year-for-the-date. – chux - Reinstate Monica Jan 11 '18 at 22:00
  • Take a look at [How do I calculate the week number given a date](https://stackoverflow.com/questions/274861/how-do-i-calculate-the-week-number-given-a-date) (which I see was linked by someone else) — but it is not a complete answer as it works with the ISO 8601 definitions, rather than providing the greater flexibility you mention. I'm not clear whether you want only start-of-week is Monday or Sunday, or whether you wish to accommodate other cultures. _[…continued…]_ – Jonathan Leffler Jan 12 '18 at 19:54
  • _[…continuation…]_ Likewise, I'm not clear what flexibility you wish to provide in which is the first week of the year. The ISO 8601 rule is "first week with 4 days in the current year". So given a week start of Monday, if Thursday is in the new year, it is week 1 (even if Monday is 29th December, because Thursday is 1st January, and Sunday is 4th January, so there are 4 days within the current year. The week 52 vs week 53 rule is interesting; for a Monday start-of-week, if Thursday is the 1st January in any year, or if Wednesday is the 1st January in a leap year, you get 53 weeks in the year. – Jonathan Leffler Jan 12 '18 at 19:58
  • 1
    Clearly, other week numbering rules are possible. The ISO 8601 rule makes sense (4 days is just more than half the week, so the majority of week 1 is in the New Year). But people can decide on other rules. Choosing how much flexibility you want to provide is key. (Note that the `tm_yday` element of `struct tm` will help you in your analysis. It gives you the day of the year for the given time, counting 1st January as 'day 0'. You primarily need to determine which date is 'day 1 of week 1', counting days of the week 1..7. That may not be a date in the current year. The rest is fairly easy.) – Jonathan Leffler Jan 12 '18 at 20:01

1 Answers1

2

Week of year with variable start and anchor days

This issue is fairly direct: find the "week-of-the-year" and its "week-of-the-year year" for a given year-month-day.


Step 1: Find the DayOfTheWeek (0-6). mktime() will take a struct tm and set its tm_yday (0-365) and tm_wday (0-6) members base on the other fields. For mktime(), the week starts on Sunday. Adjust with a value of 0 to 6 for other models of when the day-of-the-week begins - this part is fairly trivial. Insure % 7 is not applied to negative numbers.

Step 2, Adjust the date to week's beginning with .tm_mday -= DayOfTheWeek;

Step 3, Adjust the date to week's mid-week by adding 3. Trick: The mid-week day is always in the same calendar year as the week-of-the-year year.

Step 4: Call mktime() to reset the .tm.tm_year and .tm_yday members. Divide .tm_yday by 7 to get the week-of- the year (and add 1 as the first week is week 1, not 0).

Calling mktime() twice well handles all edge cases as shown below.

#include <stdio.h>
#include <time.h>

// return 1 on failure, 0 on success
int tm_YearWeek(int y, int m, int d, int FirstDOW, int *year, int *week) {
  // Set to noon to avoid DST issues.
  struct tm tm = { .tm_year = y - 1900, .tm_mon = m - 1, .tm_mday = d, .tm_hour = 12};
  // Calculate tm_wday.
  if (mktime(&tm) == -1) {
    return 1;
  }
  // Find day-of-the-week: 0 to 6.
  // Week starts on Monday per ISO 8601
  // 0 <= DayOfTheWeek <= 6, (Monday, Tuesday ... Sunday)
  int DayOfTheWeek = (tm.tm_wday + (7 - 1) - FirstDOW%7) % 7;

  // Offset the month day to the 1st day of the week (Monday).
  // This may make tm.tm_mday <= 0 or > EndOfMonth
  tm.tm_mday -= DayOfTheWeek;
  // Offset the month day to the mid-week (Thursday)
  // tm.tm_mday  <= 0 or > EndOfMonth may be true
  tm.tm_mday += 3;
  // Re-evaluate tm_year and tm_yday  (local time)
  if (mktime(&tm) == -1) {
    return 1;
  }

  *year = tm.tm_year + 1900;
  // Convert yday to week of the year, stating with 1.
  //printf("doy %4d %4d\n", tm.tm_yday, tm.tm_yday/7 + 1);
  *week = tm.tm_yday / 7 + 1;
  return 0;
}

Some test code

#define FirstDOW_Monday 0
#define FirstDOW_Sunday 6
const char *FirstDOW_Ddd[7] =
    { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
void TestHarness_YearWeek(int y, int m, int d, int FirstDOW) {
  int ywd_year, ywd_week;
  int e = tm_YearWeek(y,m,d, FirstDOW, &ywd_year, &ywd_week);
  if (e) {
    fprintf(stderr, "Fail\n");
    exit(EXIT_FAILURE);
  }
  struct tm tm = { .tm_year = y - 1900, .tm_mon = m - 1, .tm_mday = d, .tm_hour = 12};
  mktime(&tm);
  printf("%s %4d-%2d-%2d --> Year/Week %04d-W%02d  (week starts: %s)\n",
      FirstDOW_Ddd[(tm.tm_wday + 6) % 7], y, m, d, ywd_year, ywd_week,
      FirstDOW_Ddd[FirstDOW]);
}

void TestHarness_2012(int year, int FirstDOW) {
  printf("            Jan %d\n", year);
  puts("    S   M   T   W   T   F   S");
  if (year == 2014)
    puts("                1   2   3   4");
  if (year == 2015)
    puts("                    1   2   3");
  for (int i = 28; i <= 31; i++)
    TestHarness_YearWeek(year-1, 12, i, FirstDOW);
  for (int i = 1; i <= 6; i++)
    TestHarness_YearWeek(year, 1, i, FirstDOW);
  puts("");
}

Output

            Jan 2014
    S   M   T   W   T   F   S
                1   2   3   4
Sat 2013-12-28 --> Year/Week 2013-W52  (week starts: Mon)
Sun 2013-12-29 --> Year/Week 2013-W52  (week starts: Mon)
Mon 2013-12-30 --> Year/Week 2014-W01  (week starts: Mon)1st 2014 week start:Dec 30,2013
Tue 2013-12-31 --> Year/Week 2014-W01  (week starts: Mon)
Wed 2014- 1- 1 --> Year/Week 2014-W01  (week starts: Mon)
Thu 2014- 1- 2 --> Year/Week 2014-W01  (week starts: Mon)
Fri 2014- 1- 3 --> Year/Week 2014-W01  (week starts: Mon)
Sat 2014- 1- 4 --> Year/Week 2014-W01  (week starts: Mon)
Sun 2014- 1- 5 --> Year/Week 2014-W01  (week starts: Mon)
Mon 2014- 1- 6 --> Year/Week 2014-W02  (week starts: Mon)

            Jan 2014
    S   M   T   W   T   F   S
                1   2   3   4
Sat 2013-12-28 --> Year/Week 2013-W52  (week starts: Sun)
Sun 2013-12-29 --> Year/Week 2014-W01  (week starts: Sun)1st 2014 week start:Dec 29,2013
Mon 2013-12-30 --> Year/Week 2014-W01  (week starts: Sun)
Tue 2013-12-31 --> Year/Week 2014-W01  (week starts: Sun)
Wed 2014- 1- 1 --> Year/Week 2014-W01  (week starts: Sun)
Thu 2014- 1- 2 --> Year/Week 2014-W01  (week starts: Sun)
Fri 2014- 1- 3 --> Year/Week 2014-W01  (week starts: Sun)
Sat 2014- 1- 4 --> Year/Week 2014-W01  (week starts: Sun)
Sun 2014- 1- 5 --> Year/Week 2014-W02  (week starts: Sun)
Mon 2014- 1- 6 --> Year/Week 2014-W02  (week starts: Sun)

            Jan 2015
    S   M   T   W   T   F   S
                    1   2   3
Sun 2014-12-28 --> Year/Week 2014-W52  (week starts: Mon)
Mon 2014-12-29 --> Year/Week 2015-W01  (week starts: Mon)1st 2015 week start:Dec 29,2014
Tue 2014-12-30 --> Year/Week 2015-W01  (week starts: Mon)
Wed 2014-12-31 --> Year/Week 2015-W01  (week starts: Mon)
Thu 2015- 1- 1 --> Year/Week 2015-W01  (week starts: Mon)
Fri 2015- 1- 2 --> Year/Week 2015-W01  (week starts: Mon)
Sat 2015- 1- 3 --> Year/Week 2015-W01  (week starts: Mon)
Sun 2015- 1- 4 --> Year/Week 2015-W01  (week starts: Mon)
Mon 2015- 1- 5 --> Year/Week 2015-W02  (week starts: Mon)
Tue 2015- 1- 6 --> Year/Week 2015-W02  (week starts: Mon)

            Jan 2015
    S   M   T   W   T   F   S
                    1   2   3
Sun 2014-12-28 --> Year/Week 2014-W53  (week starts: Sun)
Mon 2014-12-29 --> Year/Week 2014-W53  (week starts: Sun)
Tue 2014-12-30 --> Year/Week 2014-W53  (week starts: Sun)
Wed 2014-12-31 --> Year/Week 2014-W53  (week starts: Sun)
Thu 2015- 1- 1 --> Year/Week 2014-W53  (week starts: Sun)
Fri 2015- 1- 2 --> Year/Week 2014-W53  (week starts: Sun)
Sat 2015- 1- 3 --> Year/Week 2014-W53  (week starts: Sun)
Sun 2015- 1- 4 --> Year/Week 2015-W01  (week starts: Sun)1st 2015 week start:Jan 1, 2016
Mon 2015- 1- 5 --> Year/Week 2015-W01  (week starts: Sun)
Tue 2015- 1- 6 --> Year/Week 2015-W01  (week starts: Sun)
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • Using `%.2d` or `%02d` for formatting month or day gives the conventional zero-filled dates like `2015-01-01` which I find (after many years exposure to such dates) less jarring and much easier to read than the variant with blanks instead of zeros (`2015- 1- 1`). – Jonathan Leffler Jan 13 '18 at 01:19
  • @JonathanLeffler Change as you please - its test code. For the purpose of showing year's end/new year's, perhaps jarring is good. The main show is `tm_YearWeek()`. Any thoughts? – chux - Reinstate Monica Jan 13 '18 at 02:15