5

I'm based in the UK, and grappling with summer time BST and timezones.

Here's my code:

TIME_OFFSET = 1 # 0 for GMT, 1 for BST

def RFC3339_to_localHHMM(input):
                 # Take an XML date (2013-04-08T22:35:00Z)
                 # return e.g. 08/04 23:35
                 return (datetime.datetime.strptime(input, '%Y-%m-%dT%H:%M:%SZ') +
datetime.timedelta(hours=TIME_OFFSET)).strftime('%d/%m %H:%M')

Setting a variable like this feels very wrong, but I can't find any elegant way to achieve the above without hideous amounts of code. Am I missing something, and is there no way to (for example) read the system timezone?

Chris
  • 585
  • 3
  • 12
  • 26

5 Answers5

6

To convert UTC to given timezone:

from datetime import datetime
import pytz

local_tz = pytz.timezone("Europe/London") # time zone name from Olson database

def utc_to_local(utc_dt):
    return utc_dt.replace(tzinfo=pytz.utc).astimezone(local_tz)

rfc3339s = "2013-04-08T22:35:00Z"
utc_dt = datetime.strptime(rfc3339s, '%Y-%m-%dT%H:%M:%SZ')
local_dt = utc_to_local(utc_dt)
print(local_dt.strftime('%d/%m %H:%M')) # -> 08/04 23:35

See also How to convert a python utc datetime to a local datetime using only python standard library?.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
4

You seem to be asking a few separate questions here.

First, if you only care about your own machine's current local timezone, you don't need to know what it is. Just use the local-to-UTC functions. There are a few holes in the API, but even if you can't find the function you need, you can always just get from local to UTC or vice-versa by going through the POSIX timestamp and the fromtimestamp and utcfromtimestamp methods.

If you want to be able to deal with any timezone, see the top of the docs for the difference between aware and naive objects, but basically: an aware object is one that knows its timezone. So, that's what you need. The problem is that, as the docs say:

Note that no concrete tzinfo classes are supplied by the datetime module. Supporting timezones at whatever level of detail is required is up to the application.

The easiest way to support timezones is to install and use the third-party library pytz.

Meanwhile, as strftime() and strptime() Behavior sort-of explains, strptime always returns a naive object. You then have to call replace and/or astimezone (depending on whether the string was a UTC time or a local time) to get an aware object imbued with the right timezone.

But, even with all this, you still need to know what local timezone you're in, which means you still need a constant. In other words:

TIMEZONE = pytz.timezone('Europe/London')
def RFC3339_to_localHHMM(input):
    # Take an XML date (2013-04-08T22:35:00Z)
    # return e.g. 08/04 23:35
    utc_naive = datetime.datetime.strptime(input, '%Y-%m-%dT%H:%M:%SZ')
    utc = utc_naive.replace(pytz.utc)
    bst = utc.astimezone(TIMEZONE)
    return bst.strftime('%d/%m %H:%M')

So, how do you get the OS to give you the local timezone? Well, that's different for different platforms, and Python has nothing built in to help. But there are a few different third-party libraries that do, such as dateutil. For example:

def RFC3339_to_localHHMM(input):
    # Take an XML date (2013-04-08T22:35:00Z)
    # return e.g. 08/04 23:35
    utc = datetime.datetime.strptime(input, '%Y-%m-%dT%H:%M:%SZ')
    bst = utc.astimezone(dateutil.tz.tzlocal())
    return bst.strftime('%d/%m %H:%M')

But now we've come full circle. If all you wanted was the local timezone, you didn't really need the timezone at all (at least for your simple use case). So, this is only necessary if you need to support any timezone, and also want to be able to, e.g., default to your local timezone (without having to write two copies of all of your code for the aware and naive cases).

(Also, if you're going to use dateutil in the first place, you might want to use it for more than just getting the timezone—it can basically replacing everything you're doing with both datetime and pytz.)

Of course there are other options besides these libraries—search PyPI, Google, and/or the ActiveState recipes.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • `.astimezone()` cannot be applied to a naive datetime (returned by `.strptime()`). [`dateutil` might fail for timezones with DST](https://gist.github.com/zed/3838828). I'm not sure that `fromtimestamp`-based solution works on Windows for local timezones that have their utc offset changed e.g., Moscow standard time: [`MSK+0300` in 2010 vs. `MSK+0400`](http://stackoverflow.com/a/13287083/4279) now) i.e., dates from 2010 might be converted incorrectly. – jfs Apr 09 '13 at 20:30
  • @J.F.Sebastian: The reason `astimezone` can't be applied to a naive timezone is that it assumes it's UTC. When it actually _is_ UTC, this works. Although you may be right that it's not _documented_ to work, in which case you can first `replace` with UTC. – abarnert Apr 09 '13 at 20:33
  • 1
    no. It doesn't work. Just try it: `datetime.datetime.strptime("2013-04-08T22:35:00Z", '%Y-%m-%dT%H:%M:%SZ').astimezone(pytz.timezone('Europe/London'))` – jfs Apr 09 '13 at 20:39
  • From a quick test… it seems like it works in some of my Python versions, but not others… Given that, and the fact that the docs say it doesn't work, you're right, and I'll change the code. – abarnert Apr 09 '13 at 21:00
  • As for Windows: The OP wanted to "read the system timezone". If the system timezone is wrong—as it sometimes is for Windows (and, for that matter, for enterprises that use, say, RHEL/CentOS but refuse to install any updates)—reading it will give you an incorrect timezone. Then you have to decide what you want to do. Do you want to render a local time as your OS would render it, as your OS would have rendered it at the time, or as it should be? (Having written email software that deals with this, it's never fun, but it's flat-out impossible until you can get PM/whoever to answer that.) – abarnert Apr 09 '13 at 21:05
0

If you want to convert a UTC input into a local time, regardless of which timezone you're in, try this:

def utctolocal(input):
    if time.localtime()[-1] == 1: st=3600
    else: st=0
    return time.localtime(time.time()-time.mktime(time.gmtime())+time.mktime(time.localtime(time.mktime(time.strptime(input, '%Y-%m-%dT%H:%M:%SZ'))))+st)

Quite long code, but what it does is it simply adds the difference between time.gmtime() and time.localtime() to the time tuple created from the input.

jazzpi
  • 1,399
  • 12
  • 18
  • … which doesn't work if it's summer time now, and you're parsing a date from 6 months ago. – abarnert Apr 09 '13 at 19:40
  • @abarnert Oops, forgot about that :S – jazzpi Apr 09 '13 at 19:49
  • As far as I can tell, all you've done is add `+st` to the end, which just means you get a `NameError` instead of the wrong answer. Unless you're suggesting that you can figure out the DST-crossing value somewhere and set `st` to that? I don't know how that would work, because `st` has to be a different value when parsing a date from 6 months ago vs. parsing a date from today (and that's not even counting crossing years when the law changed, etc.). – abarnert Apr 09 '13 at 20:06
  • @abarnert: Oops, it didn't quite save my changes for some reason :S But for the different st values if the date is in summer or not, the OP requires the input to be in ordinary time ;) – jazzpi Apr 09 '13 at 20:24
  • Besides the fact that there are a few timezones where DST isn't +3600, this figures out whether the current time is in the summer, not whether the parsed time is in the summer. So it's still wrong half the time. – abarnert Apr 09 '13 at 20:31
  • @abarnert Alright, I didn't know DST isn't always +1:00, but if you look at the OP, that sets the time offset to 1h because it is summertime although the input date is 08/04 which is in summertime, so it apparently uses inputs in ordinary time. – jazzpi Apr 09 '13 at 20:37
  • What do you mean by "ordinary time"? The inputs dates appear to be UTC. He needs to convert them to Europe/London. Whether that means offsetting by an hour or not depends on whether or not the _input date_ is in the summer, not the _current date_. – abarnert Apr 09 '13 at 21:02
0

Here's a function I use to do what I think you want. This assumes that the input is really a gmt, or more precisely, a utc datetime object:

def utc_to_local(utc_dt):
    '''Converts a utc datetime obj to local datetime obj.'''
    t = utc_dt.timetuple()
    secs = calendar.timegm(t)
    loc = time.localtime(secs)
    return datetime.datetime.fromtimestamp(time.mktime(loc))

Like you said, this relies on the system time zone, which may give you shaky results, as some of the comments have pointed out. It has worked perfectly for me on Windows, however.

MikeHunter
  • 4,144
  • 1
  • 19
  • 14
0

A simple function to check if a UCT corresponds to BST in London or GMT (for setting TIME_OFFSET above)

import datetime

def is_BST(input_date):
    if input_date.month in range(4,9):
        return True
    if input_date.month in [11,12,1,2]:
        return False
    # Find start and end dates for current year
    current_year = input_date.year

    for day in range(25,32):
        if datetime.datetime(current_year,3,day).weekday()==6:
            BST_start = datetime.datetime(current_year,3,day,1)
        if datetime.datetime(current_year,10,day).weekday()==6:
            BST_end = datetime.datetime(current_year,10,day,1)

    if (input_date > BST_start) and (input_date < BST_end):
        return True

    return False
kosnik
  • 2,342
  • 10
  • 23