4

I'm using the Gregorian calendar the and I want to implement IS0 8601 weeks, but I've stumbled onto a issue calculating the date of any week number. For example the ISO dates 2010-W01-1 should return January 4, 2010 and 2009-W01-1 should return December 29, 2008.

// Get the date for a given year, week and weekday(1-7) 
time_t *GetDateFromWeekNumber(int year, int week, int dayOfWeek)
{
    // Algorithm here
}

Edit: I havent found any algorithm that works online, tried a lot but I'm kind of stuck now.

  • I havent found any algorithm that works online. –  Mar 28 '13 at 20:33
  • You might like the [article about Julian dates on Wikipedia](http://en.wikipedia.org/wiki/Julian_date). – pmg Mar 28 '13 at 20:35
  • There's code in the answer to [How do I calculate the week number given a date?](http://stackoverflow.com/questions/274861/how-do-i-calculate-the-week-number-given-a-date/275024#275024). It's not in C, but it is fairly readily comprehensible and would not take much effort to convert. Also, [`strftime()`] (http://pubs.opengroup.org/onlinepubs/9699919799/functions/strftime.html) supports the formatting (`%W`, `%U`, `%V`, etc). – Jonathan Leffler Mar 28 '13 at 20:36
  • @pmg I'm not sure how that would help me as I use the Gregorian calendar. Do you mean I should find an algorithm that does this using Julian dates and then convert the result to Gregorian? –  Mar 28 '13 at 20:37
  • It's only a suggestion. The point is that Julian dates are sequential. All Julian dates multiple of 7 are the same weekday. With that information you can easily calculate all the rest. – pmg Mar 28 '13 at 20:39
  • @JonathanLeffler That's the opposite of what I wanted to do, I want the date from the week number, not the week number from the date. –  Mar 28 '13 at 20:44
  • 1
    This SO answer: http://stackoverflow.com/a/15146434/576911 contains a reference to a gregorian calendar date class and shows code (`week_to_date`) which converts from ISO week-date to gregorian. – Howard Hinnant Mar 28 '13 at 20:45
  • @HowardHinnant Do you know of a way of doing this without using Boost? –  Mar 28 '13 at 21:02
  • Yes. Use the date library presented in http://stackoverflow.com/a/15146434/576911 . This is a small free-standing library consisting of one header and one source. This library is not part of boost, though it carries a boost copyright. If the copyright on that library is unacceptable, let me know what would be acceptable and I'll change it. The intent is that you should be able to do whatever you want with it. – Howard Hinnant Mar 28 '13 at 21:10
  • @HowardHinnant Thanks, I looked at my algorithm this afternoon and figured it out (see my answer) - so no need to change your license. Thank you for your time though. Good luck with your open source library. –  Mar 29 '13 at 14:49

3 Answers3

4

The currently accepted answer gives the wrong answer for week 1 of 2017 (and every week of 2017). The function GetDayAndMonthFromWeekInYear, given an input of 2017 and 1 for year and weekInYear respectively should output 1 in month and 2 in dayInMonth, indicating that 2017-W01 begins on Monday, 2017-01-02, but it instead outputs the Gregorian date 2017-01-01.

This free, open source, C++11/14 library outputs the correct date conversion from ISO week to Gregorian with this syntax:

#include "date/date.h"
#include "date/iso_week.h"
#include <iostream>

int
main()
{
    using namespace iso_week::literals;
    std::cout << date::year_month_day{2017_y/1_w/mon} << '\n';
}

2017-01-02

As the library is open source, one can easily inspect the sources ("iso_week.h" and "date.h") for the algorithms used. The algorithms are also efficient, with no iteration used.

The general approach is to convert the field 2017_y/1_w/mon into a serial count of days since 1970-01-01, using this algorithm:

CONSTCD14
inline
year_weeknum_weekday::operator sys_days() const NOEXCEPT
{
    return sys_days{date::year{int{y_}-1}/date::dec/date::thu[date::last]}
         + (date::mon - date::thu) + weeks{unsigned{wn_}-1} + (wd_ - mon);
}

And then that serial count of days is converted into a year/month/day field type using this algorithm:

CONSTCD14
inline
year_month_day
year_month_day::from_sys_days(const sys_days& dp) NOEXCEPT
{
    static_assert(std::numeric_limits<unsigned>::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits<int>::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    auto const z = dp.time_since_epoch().count() + 719468;
    auto const era = (z >= 0 ? z : z - 146096) / 146097;
    auto const doe = static_cast<unsigned>(z - era * 146097);          // [0, 146096]
    auto const yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;  // [0, 399]
    auto const y = static_cast<sys_days::rep>(yoe) + era * 400;
    auto const doy = doe - (365*yoe + yoe/4 - yoe/100);                // [0, 365]
    auto const mp = (5*doy + 2)/153;                                   // [0, 11]
    auto const d = doy - (153*mp+2)/5 + 1;                             // [1, 31]
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4146) // unary minus operator applied to unsigned type, result still unsigned
#endif
    auto const m = mp + (mp < 10 ? 3 : -9u);                           // [1, 12]
#ifdef _MSVC_VER
#pragma warning(pop)
#endif
    return year_month_day{date::year{y + (m <= 2)}, date::month(m), date::day(d)};
}

The latter algorithm is documented in excruciating detail here.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
1

Maybe you should look at the boost::date_time::gregorian. Using it you can write a function like that:

#include <boost/date_time/gregorian/gregorian.hpp>

// Get the date for a given year, week and weekday(0-6) 
time_t *GetDateFromWeekNumber(int year, int week, int dayOfWeek)
{
    using namespace boost::gregorian;
    date d(year, Jan, 1);
    int curWeekDay = d.day_of_week();
    d += date_duration((week - 1) * 7) + date_duration(dayOfWeek - curWeekDay);
    tm tmp = to_tm(d);
    time_t * ret = new time_t(mktime(&tmp));
    return ret;
}

Unfortunately their format of date is different from yours - they numerate days of week starting from Sunday, i.e. Sunday = 0, Monday = 1, ..., Saturday = 6. If it doesn't satisfy your needs, you can use this slightly changed function:

#include <boost/date_time/gregorian/gregorian.hpp>

// Get the date for a given year, week and weekday(1-7) 
time_t *GetDateFromWeekNumber(int year, int week, int dayOfWeek)
{
    using namespace boost::gregorian;
    date d(year, Jan, 1);
    if(dayOfWeek == 7) {
        dayOfWeek = 0;
        week++;
    }
    int curWeekDay = d.day_of_week();
    d += date_duration((week - 1) * 7) + date_duration(dayOfWeek - curWeekDay);
    tm tmp = to_tm(d);
    time_t * ret = new time_t(mktime(&tmp));
    return ret;
}

EDIT:

After thinking a little I found a way to implement the same function without using boost. Here is the code:

WARNING: the code below is broken, do not use it!

// Get the date for a given year, week and weekday(1-7) 
time_t *GetDateFromWeekNumber(int year, int week, int dayOfWeek)
{
    const time_t SEC_PER_DAY = 60*60*24;
    if(week_day == 7) {
        week_day = 0;
        week++;
    }
    struct tm timeinfo;
    memset(&timeinfo, 0, sizeof(tm));
    timeinfo.tm_year = year - 1900;
    timeinfo.tm_mon = 0;
    timeinfo.tm_mday = 1;
    time_t * ret = new time_t(mktime(&timeinfo));  // set all the other fields
    int cur_week_day = timeinfo.tm_wday;
    *ret += sec_per_day * ((week_day - cur_week_day) + (week - 1) * 7);
    return ret;
}

EDIT2:

Yep, code in EDIT is completely broken because I didn't take enough time to understand how week numbers are assigned.

Community
  • 1
  • 1
Mikhail Melnik
  • 966
  • 9
  • 19
  • Thanks, but we are not using Boost. –  Mar 29 '13 at 14:47
  • 1
    When it comes to your non-Boost example then it returns the _18th of December 2005_ when year is 2005, week is 52 and weekday is 1, when the correct answer should have been _26th of December 2005_ , see http://www.epochconverter.com/date-and-time/weeknumbers-by-year.php?year=2005 . I have fixed my own algorithm now (see my answer) but I thought you would like to know that your code is broken. –  Mar 29 '13 at 15:19
-1

By F#

open System
open System.Globalization

//wday: 1-7, 1:Monday
let DateFromWeekOfYear y w wday =
  let dt = new DateTime(y, 1, 4) //first week include 1/4
  let dow = if dt.DayOfWeek = DayOfWeek.Sunday then 7 else int dt.DayOfWeek //to 1-7
  let dtf = dt.AddDays(float(wday - dow))
  GregorianCalendar().AddWeeks(dtf, w - 1)
BLUEPIXY
  • 39,699
  • 7
  • 33
  • 70