0

Feel free to add your favorite module, but so far we've got time, datetime, dateutil, pytz, and locale to deal with, as PEP-431 hasn't been implemented yet, giving us a uniform, concrete way of dealing with timezones and their associated oddities like ambiguous datetimes that occur twice during a daylight savings time switchover, or don't occur at all. At any rate, my question is fairly pedestrian, but I can't seem to find an answer.

The backstory: I'm developing a small plugin for Sublime Text that initially will allow the user to insert a timestamp (in a format they specify, falling back on a default) in the current document. Eventually it will update the timestamp upon saving, but that's not relevant here. Here's my short, self-contained example code:

import sublime_plugin
from datetime import datetime as dt


class TimeStampCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        stamp = dt.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")  # 2013-10-10 22:38:40 UTC
        for r in self.view.sel():
            if r.empty():
                self.view.insert(edit, r.a, stamp)
            else:
                self.view.replace(edit, r, stamp)

From the list of formatting options, I should be able to add a %Z and get the local timezone:

stamp = dt.now().strftime("%Y-%m-%d %H:%M:%S %Z")  # should be 2013-10-10 18:38:40 EDT

or modify it slightly and get a more universal UTC offset:

stamp = dt.now().strftime("%Y-%m-%d %H:%M:%S UTC %z")  # should be 2013-10-10 18:38:40 UTC -0400

Unfortunately, this isn't the case, because the time returned by dt.now() is "naive" - it doesn't have any timezone information attached to it. I could use datetime.tzinfo, but unhelpfully this is only an abstract base class, since timezones are messy and complicated and change a lot. So, to the root of my question: How do I create an "aware" time/datetime/dateutil/whatever object that can take advantage of the full array of strftime() formatting options? I want users to be able to format the timestamp as they please, either according to their own locale (somehow I need to detect this) or according to a supplied one. Preferably I'd like it to work on Windows, Mac, and Linux (the Sublime Text supported platforms). Finally, if there are differences between Python 2.6 and 3.3 (the built-in interpreter's version for ST2 and ST3, respectively) I'd like to know about them.

Please note, I'm not asking you to write all the code for me, I want to learn this myself. I just need some good pointers to get going in the right direction, as I've been spinning my wheels for most of the day. At the absolute minimum, just figuring out how to generate 2013-10-10 18:38:40 UTC -0400 would be a great start, although the other stuff would be really nice too :)

6
  • Have you read the footnote that explains that %Z is deprecated? Besides the limitations of POSIX and Windows, a three-letter timezone is not a unique identifier—AST is Atlantic Standard Time in Canada, Arabia Standard Time in the Middle-East, and Acre Standard Time in Brazil—that is, -4, +3, and -5, respectively. Commented Oct 10, 2013 at 23:25
  • I had not, thanks for pointing that out. Perhaps I'll keep the default as the UTC offset, with the option to print the full timezone name (time.tzname[time.daylight]) if they really want to. Commented Oct 10, 2013 at 23:30
  • 2
    By the way, the right solution to this problem is to conquer the world and outlaw all timezones. People will learn that 16:00 is early morning. Or, if not, you have them shot in the face. If you decide to go this way, I will support your campaign for global overlord. Commented Oct 10, 2013 at 23:48
  • 2
    @abarnert: What would be your plans for Unicode? :-) Commented Oct 14, 2013 at 23:28
  • 1
    @JS.: Well, I personally am not running for global overlord, but I would support anyone with this platform: Outlaw all non-Unicode character sets. All data in cp1252 is automatically treasonous. Complaining about Han unification means your license to speak Japanese at home is revoked, and you must now speak to your kids in Sindarin. If you insist on UTF-16, you can use it, but at your own risk, meaning your first surrogate-pair error gets you transferred from the programming corps to the mining slave pits in Bedfordshire. Commented Oct 14, 2013 at 23:59

1 Answer 1

1

How do I create an "aware" time/datetime/dateutil/whatever object that can take advantage of the full array of strftime() formatting options?

Well, if you have a timezone, this is easy, with the pytz module:

>>> mytz = pytz.timezone('America/Los_Angeles')
>>> now = datetime.datetime.now(tz=mytz)
>>> now
datetime.datetime(2013, 10, 10, 16, 34, 3, 113620, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)

That's an aware timezone. And if I feed it into %Z?

>>> now.strftime('%Y-%m-%d %H:%M:%S %Z')
'2013-10-10 16:34:03 PDT'

Of course this isn't something you can parse back into an unambiguous datetime object, because there is no way to tell whether it means Pacific or Pakistan. So, it may be useful to display to the end user, but not for much else. Even if you decide only the US matters, does 'MST' mean Mountain as in -7 with most-of-the-US DST rules, or as in -7 with Arizona no-DST rules? And most of the popular three-letter codes are even worse than these two.


But, more importantly, how do you get that timezone in the first place?

You can't.

On some POSIX systems, you can read it out of os.environ['TZ']. But usually not—or, if it is available, unless the user has gone out of his way to explicitly set it to a useful timezone identifier, it's going to be an ambiguous abbreviation like "PST".

On almost POSIX systems, you can do this:

>>> time.tzset()
>>> time.tzname
('PST', 'PDT')

But this is guaranteed to be an ambiguous abbreviation. (And it still doesn't work on Windows.)


At the absolute minimum, just figuring out how to generate 2013-10-10 18:38:40 UTC -0400 would be a great start

This is not trivial, but it's at least easy, without any third-party code; it's just tedious.

First, if you can trust tzset (most POSIX platforms):

>>> time.tzset()
>>> sec = -time.timezone

Or, if you can't trust tzset:

>>> t = time.time()
>>> local = datetime.datetime.fromtimestamp(t)
>>> utc = datetime.datetime.utcfromtimestamp(t)
>>> offset = local - utc
>>> sec = offset.total_seconds()

Then you just format it into a string:

>>> hr, sec = divmod(sec, 3600)
>>> min, sec = divmod(sec, 60)
>>> offstr = '{:+03d}{:02d}'.format(hr, min)

Now, if you want to format a naive local time:

>>> datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " " + offstr
'2013-10-10 16:18:36 -0800'

So, what should you do?

Well, obviously, still with UTC times for all internal and persistence uses. Then you only need to deal with local time for direct input and output. And, since you're writing a desktop app, not a web service, just use the various functions that convert naive-local to and from UTC without having to know which timezone "local" actually is.

In your use case, the user's timestamp can be formatted from a naive datetime, plus the name you get out of the time module after tzset if possible, falling back to the offset if not. This will be no worse than building an aware datetime would have been.

Sign up to request clarification or add additional context in comments.

1 Comment

very helpful, and a great start.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.