59

The Python datetime.isocalendar() method returns a tuple (ISO_year, ISO_week_number, ISO_weekday) for the given datetime object. Is there a corresponding inverse function? If not, is there an easy way to compute a date given a year, week number and day of the week?

Joe Doyle
  • 6,363
  • 3
  • 42
  • 45
Tom
  • 42,844
  • 35
  • 95
  • 101

8 Answers8

85

Python 3.8 added the fromisocalendar() method:

>>> datetime.fromisocalendar(2011, 22, 1)
datetime.datetime(2011, 5, 30, 0, 0)

Python 3.6 added the %G, %V and %u directives:

>>> datetime.strptime('2011 22 1', '%G %V %u')
datetime.datetime(2011, 5, 30, 0, 0)

Original answer

I recently had to solve this problem myself, and came up with this solution:

import datetime

def iso_year_start(iso_year):
    "The gregorian calendar date of the first day of the given ISO year"
    fourth_jan = datetime.date(iso_year, 1, 4)
    delta = datetime.timedelta(fourth_jan.isoweekday()-1)
    return fourth_jan - delta 

def iso_to_gregorian(iso_year, iso_week, iso_day):
    "Gregorian calendar date for the given ISO year, week and day"
    year_start = iso_year_start(iso_year)
    return year_start + datetime.timedelta(days=iso_day-1, weeks=iso_week-1)

A few test cases:

>>> iso = datetime.date(2005, 1, 1).isocalendar()
>>> iso
(2004, 53, 6)
>>> iso_to_gregorian(*iso)
datetime.date(2005, 1, 1)

>>> iso = datetime.date(2010, 1, 4).isocalendar()    
>>> iso
(2010, 1, 1)
>>> iso_to_gregorian(*iso)
datetime.date(2010, 1, 4)

>>> iso = datetime.date(2010, 1, 3).isocalendar()
>>> iso
(2009, 53, 7)
>>> iso_to_gregorian(*iso)
datetime.date(2010, 1, 3)
Erik Cederstrand
  • 9,643
  • 8
  • 39
  • 63
Ben James
  • 121,135
  • 26
  • 193
  • 155
  • Can you clarify the significance of the 4th of January? Is there a description of the ISO calendar standard somewhere that's appropriate? – Tom Nov 10 '09 at 05:42
  • 6
    Tom: from the formal ISO definition of week 1 (the week containing the first Thursday of the year) it follows that 4th January is the latest that week 1 can start. – Ben James Nov 10 '09 at 09:32
  • Thanks for this solution. However, generation of `fourth_jan` does not work for me (using Python 2.7), and I had to write: `fourth_jan = datetime.strptime('{0}-01-04'.format(iso_year), '%Y-%m-%d')`. – Joël Mar 06 '14 at 09:55
  • 1
    This is a good answer, but it can be simpler. You don't need to work out the start date of the year. See my answer. – jwg Oct 13 '15 at 11:15
18

As of Python 3.6, you can use the new %G, %u and %V directives. See issue 12006 and the updated documentation:

%G
ISO 8601 year with century representing the year that contains the greater part of the ISO week (%V).

%u
ISO 8601 weekday as a decimal number where 1 is Monday.

%V
ISO 8601 week as a decimal number with Monday as the first day of the week. Week 01 is the week containing Jan 4.

Given a string with year, weeknumber and weekday number, it is easy to parse those out to a date with:

from datetime import datetime

datetime.strptime('2002 01 1', '%G %V %u').date()

or as a function with integer inputs:

from datetime import datetime

def date_from_isoweek(iso_year, iso_weeknumber, iso_weekday):
    return datetime.strptime(
        '{:04d} {:02d} {:d}'.format(iso_year, iso_weeknumber, iso_weekday),
        '%G %V %u').date()
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    If you get an error `AttributeError: type object 'datetime.date' has no attribute 'strptime'`, use `from datetime import datetime` instead. – yl_low Dec 07 '18 at 23:48
  • @yl_low that’s absolutely right, not sure why I made that mistake here or why it took so long for someone to notice! All corrected now. – Martijn Pieters Dec 07 '18 at 23:58
10
import datetime

def iso_to_gregorian(iso_year, iso_week, iso_day):
    "Gregorian calendar date for the given ISO year, week and day"
    fourth_jan = datetime.date(iso_year, 1, 4)
    _, fourth_jan_week, fourth_jan_day = fourth_jan.isocalendar()
    return fourth_jan + datetime.timedelta(days=iso_day-fourth_jan_day, weeks=iso_week-fourth_jan_week)

This was adapted from @BenJames's very good answer. You don't have to know the first day of the year. You just have to know an example of a date which is certainly in the same ISO year, and the ISO calendar week and day of that date.

The 4th of Jan is simply one example, because, as Ben pointed out, the 4th of Jan always belongs to the same ISO year and Gregorian year, and is the first day of the year to do so.

Since weeks are all the same length, you can simply subtract the days and weeks between the ISO of the date you want, and the ISO of the date which you know in both forms, and add on that number of days and weeks. (It doesn't matter whether these numbers are positive or negative, so you could choose some other 'fixed day' such as Dec 28th.)

Edit

I corrected this because, as was helpfully pointed by @JoSo, the first day of the Gregorian year which also belongs to the ISO year is Jan 4th not Jan 5th. As the explanation says, it doesn't matter which date is chosen as a reference point, but choosing the Jan 4th makes this choice less 'magic'.

Jo So
  • 25,005
  • 6
  • 42
  • 59
jwg
  • 5,547
  • 3
  • 43
  • 57
  • 1
    Could you please tell me, if it does really work flawless in each situation? – Geeocode Oct 30 '15 at 11:33
  • 3
    @GeorgeSolymosi I have used the hypothesis Python testing library to search for any valid `datetime` objects for which this function isn't the inverse of `isocalendar`. It hasn't found any/ See https://github.com/jwg4/qual/blob/master/qual/tests/test_iso.py This isn't a guarantee of course - I would be interested in hearing about any bugs. If you work through the logic of the code, you might or might not be convinced that it does the right thing. – jwg Oct 31 '15 at 10:19
  • this is more complicated than necessary. Look at Ben's answer or or at Wikipedia. January 4 will do as well and furthermore always lies in the first ISO week of its ISO year. – Jo So Jul 09 '16 at 16:08
  • @JoSo How is this more complicated than Ben's answer? – jwg Jul 11 '16 at 07:22
  • doesn't lie in the first ISO week of its year. You can use any date between Jan 4 and Dec 28, but only Jan 4 always lies in the first week. The choice of Jan 5 is odd if you consider Wikipedia, and you have to calculate with the ISO weeek of Jan 5, which you don't if you take Jan 4. (See my clean-up answer). – Jo So Jul 11 '16 at 12:48
  • You are right about Jan 4 v. Jan 5. As seem from my answer, for some reason I mistakenly thought Jan 5 was the first day of the Gregorian year guaranteed to belong to the same ISO year. As for your answer, I would say that they are similar in complexity. You make a call to `isoweekday` instead of `isocalendar`. But I would say that yours is less clear - it isn't immediately obvious whether start is a date, a number of days, a weekday, or what, much less what the first timedelta calculation is supposed to be calculating. – jwg Jul 11 '16 at 12:56
4

For the next people coming here, a shorter, single-def, version of Ben's good solution:

def iso_to_gregorian(iso_year, iso_week, iso_day):
    jan4 = datetime.date(iso_year, 1, 4)
    start = jan4 - datetime.timedelta(days=jan4.isoweekday()-1)
    return start + datetime.timedelta(weeks=iso_week-1, days=iso_day-1)
Jo So
  • 25,005
  • 6
  • 42
  • 59
3

Starting in Python 3.6, datetime.strptime() will support the %G, %V and %u directives, so you can simply do datetime.strptime('2015 1 2', '%G %V %u').date(). See: https://hg.python.org/cpython/rev/acdebfbfbdcf

Erik Cederstrand
  • 9,643
  • 8
  • 39
  • 63
0

Note that %W is the week # (0-53) which is NOT THE SAME as the ISO week (1-53). There will be edge cases where %W will not work.

0

I came up with a solution similar to the one posted by Ben James, but using a single function:

import datetime
def getDateFromWeek(year,week,day):
    """Method to retrieve the date from the specified week, year and weekday"""

    year_start = datetime.date(year,1,1)
    ys_weekday =  year_start.weekday()
    delta = (week*7)+(day-ys_weekday)
    if ys_weekday<4:
        delta -= 7

    return  year_start  + datetime.timedelta(days=delta)

I tested it out with boundary values such as the last week of 2020 and first week of 2021, and it worked out pretty well.

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
-1

EDIT: ignore this, the edge cases are a pain. Go with Ben's solution.

Ok, on closer inspection I noticed that strptime has %W and %w parameters, so the following works:

def fromisocalendar(y,w,d):
   return datetime.strptime( "%04dW%02d-%d"%(y,w-1,d), "%YW%W-%w")

A couple of gotchas: The ISO week number starts at 1, while %W starts at 0. The ISO week day starts at 1 (Monday), which is the same as %w, so Sunday would probably have to be 0, not 7...

Tom
  • 42,844
  • 35
  • 95
  • 101