10

I don't want to use the pytz library as the project I am working on requires paperwork to introduce dependencies. If I can achieve this without a third party library I'll be happier.

I'm having trouble converting a date between CET and UTC when the date is in daylight savings. It's an hour different to what I expect:

>>> print from_cet_to_utc(year=2017, month=7, day=24, hour=10, minute=30)
2017-07-24T09:30Z

2017-07-24T08:30Z  # expected

CET is an hour ahead of UTC and in summertime is 2 hours ahead. So I would expect 10:30 in central Europe in midsummer to actually to 8:30 UTC.

The function is:

from datetime import datetime, tzinfo, timedelta

def from_cet_to_utc(year, month, day, hour, minute):
    cet = datetime(year, month, day, hour, minute, tzinfo=CET())
    utc = cet.astimezone(tz=UTC())
    return '{:%Y-%m-%d:T%H:%MZ}'.format(utc)

I use two timezone info objects:

class CET(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=1)

    def dst(self, dt):
        return timedelta(hours=2)

class UTC(tzinfo):
    def utcoffset(self, dt):
        return timedelta(0)

    def dst(self, dt):
        return timedelta(0)
Peter Wood
  • 23,859
  • 5
  • 60
  • 99
  • 1
    Note: Python 3.9+ has the [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) module in the standard library which has all you need for time zone handling. – FObersteiner Jul 08 '21 at 07:07
  • @MrFuppes thanks for this. I'm only just in the process of migrating from 2.7 to 3.8. Some day... – Peter Wood Jul 08 '21 at 10:39
  • 1
    ok, no worries ;-) just in case, `zoneinfo` is also available via [backports.zoneinfo](https://pypi.org/project/backports.zoneinfo/) for Python <3.9, and there's a [deprecation shim](https://pypi.org/project/pytz-deprecation-shim/) for `pytz`. – FObersteiner Jul 08 '21 at 10:44
  • @MrFuppes that's really very helpful and useful, thank you very much! – Peter Wood Jul 08 '21 at 10:50

2 Answers2

8

Just complementing @Sergey's answer.

To know when you should apply DST or not, you should get the historical data of the date/times when DST starts and ends, and check if it must be applied or not to the specified datetime. You can get those from IANA database or consult in many online sources, such as timeanddate website.

Another detail is that CET is an ambiguous name, because it's used by more than one timezone. Real timezones names use the ones defined by IANA database (always in the format Region/City, like Europe/Paris or Europe/Berlin).


Gaps and Overlaps

I'm going to use Europe/Berlin timezone as an example. In this year (2017), DST in Berlin started in March 26th: at 2 AM, clocks shifted 1 hour forward to 3 AM, and the offset changed from +01:00 to +02:00.

You can also think that clocks jumped directly from 1:59 AM to 3 AM, which means that all the local times between 2 AM and 2:59 AM didn't exist in that day, at that timezone. That's called a gap, and you should check for this situation (one common approach is to adjust 2 AM to the next valid hour - in this case, 3 AM in DST).

In the same timezone, in 2017, DST will end in October 29th: at 3 AM, the clocks will shift back 1 hour to 2 AM, and the offset will change from +02:00 to +01:00.

This means that all the local times between 2 AM and 2:59 AM will exist twice: once in DST (+02:00) and once in non-DST (+01:00). That's called an overlap, and when creating a datetime that falls in this case, you must decide which one you'll choose to create (should it be 2 AM in DST or non-DST offset? you decide!).


PS: Each european country that uses CET nowadays adopted it in a different year, so if you're dealing with old dates you must consider that too.

Another detail is that DST is defined by governments, and there's no guarantee that the rules will be like this forever, and you'll have to update the rules accordingly - another advantage of using pytz, as its updates are generated from IANA releases and published in PyPI (thanks @Matt Johnson for pointing this info).

Are you sure the paperwork is worse than coding these rules by hand? Just check how to read IANA tz files and perhaps you'll change your mind.

  • 2
    Just to answer your point about pytz updates, they're generated from IANA releases. IANA 2017b => pytz 2017.2. Stuart Bishop maintains them, and publishes them to pypi here: https://pypi.python.org/pypi/pytz#downloads – Matt Johnson-Pint Oct 19 '17 at 18:11
  • @MattJohnson I've updated the answer with this info, thanks a lot! –  Oct 19 '17 at 18:22
  • 1
    Both your and Sergey's answers were very helpful. I think you have done the most to convince me that I really should leave it up to the experts. Our solution is to require UTC time as input, then no conversion is required on our part (c: – Peter Wood Oct 20 '17 at 11:00
4

You should read the tzinfo manual carefully, as it is explained there.

First, dst() should usually return a timedelta(0) or timedelta(hours=1), never 2 or more hours, and rarely something in between (e.g. 30 mins). This is only a dst shift relative to the normal TZ offset, not the full TZ offset in the dst mode.

Second, utcoffset() must include dst correction already. The dst() method on its own is also used in some cases described in the manual (e.g. for detection if dst is in effect or not), but it is NOT automatically applied to the TZ offset unless you do so.

Your code should look like this:

from datetime import datetime, tzinfo, timedelta

class CET(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=1) + self.dst(dt)

    def dst(self, dt):
        dston = datetime(year=dt.year, month=3, day=20)
        dstoff = datetime(year=dt.year, month=10, day=20)
        if dston <= dt.replace(tzinfo=None) < dstoff:
            return timedelta(hours=1)
        else:
            return timedelta(0)

class UTC(tzinfo):
    def utcoffset(self, dt):
        return timedelta(0)

    def dst(self, dt):
        return timedelta(0)

def from_cet_to_utc(year, month, day, hour, minute):
    cet = datetime(year, month, day, hour, minute, tzinfo=CET())
    utc = cet.astimezone(tz=UTC())
    return '{:%Y-%m-%d:T%H:%MZ}'.format(utc)


print from_cet_to_utc(year=2017, month=7, day=24, hour=10, minute=30)
# 2017-07-24:T08:30Z

Here, I bravely assume that the DST is in effect 20.03.YYYY - 19.10.YYYY inclusive, where YYYY is the year of the date/datetime object.

Usually this logic of the on & off dates is much more complicated: the shifts happen on the Sunday nights only, sometimes on the last Sunday of the month, and vary between the years, sometimes between the sovereignty of the states (with borderlines also changing over the years), and with the daylight saving laws and timezoning changing every few years (hello, Russia).

Sergey Vasilyev
  • 3,919
  • 3
  • 26
  • 37
  • 3
    Actually, you should also check the time of the day - in central Europe, DST changes usually occur between 2 AM and 3 AM, so checking just the date is not accurate enough. Anyway, I'll upvote as soon as the day changes (I've reached my voting limit for today). –  Oct 19 '17 at 18:01
  • Presently, 2:00 AM in the spring, 3:00 AM in the fall. https://www.timeanddate.com/time/change/france/paris – Matt Johnson-Pint Oct 19 '17 at 18:28
  • 1
    @Hugo You are right. Thanks for complementing my answer. However, even what you described, is only a tip of the iceberg. I intentionally answered the question only regarding the python & `datetime.tzinfo` parts, which seemed to be a problem for the question's author. Though mentioned that the actual tz calculation is a very difficult and large topic. Because it so large, that one can write a book purely on the proper work with the dates & times (not a joke). – Sergey Vasilyev Oct 19 '17 at 19:30
  • 2
    @SergeyVasilyev Indeed, we're just scratching the surface of timezones here. I hope we can convince the OP that it's totally worth to use a proper API for that, instead of reinventing the wheel. –  Oct 20 '17 at 00:26