Calculate Gregorian date from week number in C/C++

algorithm, c, c++

Solution

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.

Problem

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.

Original source

Related problems