Examples:
Australia/Adelaide
(+09:30)Asia/Kathmandu
(+05:45)Africa/Monrovia
(+00:44:30) (Before 1979)WET (+0 STD) -> WEST (+1 DST) 2012-04-29
WEST (+1 DST) -> WET (+0 STD) 2012-07-20
WET (+0 STD) -> WEST (+1 DST) 2012-08-20
WEST (+1 DST) -> WET (+0 STD) 2012-09-30
... and Morocco in 2013-present, and Egypt in 2010 and 2014, and Palestine in 2011.
Asia/Shanghai
¶Asia/Urumqi
¶Just a sample of ways people write datetimes:
1994-11-05T08:15:30Z
1994-11-05T14:00:30+05:45
19941105T08:15:30+0000
1994-11-05T081530+00:00
1994-W44-6T08:15:30+00:00
1994-11-05 08:15:30Z
1994-11-05T08:15:30,000Z
1994-11-05T08:15:30.000Z
... and that's just a few ISO 8601/RFC 3339 representations
Don't forget:
2014 Feb 12 10:30pm
2014年12月30日
12:30 PM
13NOV2017
December.0031.30
from dateutil import rrule as rr
# Close of business in New York on weekdays
closing_times = rr.rrule(freq=rr.DAILY, byweekday=(rr.MO, rr.TU, rr.WE, rr.TH, rr.FR),
byhour=17, dtstart=datetime(2017, 3, 9, 17), count=5)
for dt in closing_times:
print(dt.replace(tzinfo=tz.gettz('America/New_York')))
2017-03-09 17:00:00-05:00 2017-03-10 17:00:00-05:00 2017-03-13 17:00:00-04:00 2017-03-14 17:00:00-04:00 2017-03-15 17:00:00-04:00
for dt in closing_times:
print(dt.replace(tzinfo=tz.gettz('America/New_York')).astimezone(tz.UTC))
2017-03-09 22:00:00+00:00 2017-03-10 22:00:00+00:00 2017-03-13 21:00:00+00:00 2017-03-14 21:00:00+00:00 2017-03-15 21:00:00+00:00
# Pins to the end of the month
datetime(2018, 1, 31) + relativedelta(months=1)
datetime.datetime(2018, 2, 28, 0, 0)
datetime(2018, 1, 31) + relativedelta(months=3)
datetime.datetime(2018, 4, 30, 0, 0)
# Get the beginning of the next month
next_month = relativedelta(months=1, day=1)
dts = [datetime(2015, 2, 1), datetime(2015, 2, 28), datetime(2015, 3, 1)]
print_dts([(x, x + next_month) for x in dts])
start_date | +relativedelta ----------------------------------------|---------------------------------------- 2015-02-01 | 2015-03-01 2015-02-28 | 2015-03-01 2015-03-01 | 2015-04-01
tz
: Time zone handlingparser
: Parsing arbitrary datetimesrrule
: Generation of recurrence relationsrelativedelta
: Handling of "calendar" offsetseaster
: Calculation of easterdateutil.tz
¶dateutil also provides a number of classes to conveniently construct and represent time zones.
Way more detail on this in my talk "Timezone Troubles" (see https://ganssle.io/talks)
dateutil.tz.tzutc()
¶The tzutc()
subclass is an alias for the universal coordinated time zone. It has an offset of 0 and does not have DST. As of version 2.7.0, there is a dedicated UTC singleton, dateutil.tz.UTC
dateutil.tz.tzlocal
¶The tzlocal()
object pulls time zone information from what the OS believes is the local time zone.
# Temporarily changes the TZ file on *nix systems.
from helper_functions import TZEnvContext
print_tzinfo(dt.astimezone(tz.tzlocal())); print()
with TZEnvContext('UTC'):
print_tzinfo(dt.astimezone(tz.tzlocal())); print()
with TZEnvContext('PST8PDT'):
print_tzinfo((dt + timedelta(days=180)).astimezone(tzlocal()))
2016-07-17 08:15:00-0400: tzname: EDT; UTC Offset: -4.00h; DST: 1.0h 2016-07-17 12:15:00+0000: tzname: UTC; UTC Offset: 0.00h; DST: 0.0h 2017-01-13 04:15:00-0800: tzname: PST; UTC Offset: -8.00h; DST: 0.0h
dateutil.tz.tzfile
¶The tzfile
specification is a binary format that is in common use among most platforms, and is the format of the compiled IANA (Olson) zoneinfo
database. This database is the most accurate and widely supported source for time zone information, and is shipped with many OSes. A copy of the database is also shipped with dateutil
as a fallback.
If you have an IANA time zone name (e.g. 'America/New_York'
, 'Europe/Belgium'
, 'Asia/Tokyo'
), you should use it if possible.
It is possible to construct a tzfile
directly from either a path to a file or an open file object. This is not the recommended way to do this as a matter of course, but it is supported. It is much preferred to just pass the timezone identifier to gettz()
, which will check the standard paths for you (and fall back to the bundled zoneinfo
data).
NYC = tz.tzfile('/usr/share/zoneinfo/America/New_York')
assert NYC == tz.gettz('America/New_York')
print_tzinfo(dt.astimezone(NYC)) # Eastern Daylight Time
print_tzinfo(datetime(1944, 1, 6, 12, 15, tzinfo=NYC)) # Eastern War Time
print_tzinfo(datetime(1901, 9, 6, 16, 7, tzinfo=NYC)) # Local solar mean
2016-07-17 08:15:00-0400: tzname: EDT; UTC Offset: -4.00h; DST: 1.0h 1944-01-06 12:15:00-0400: tzname: EWT; UTC Offset: -4.00h; DST: 1.0h 1901-09-06 16:07:00-0456: tzname: LMT; UTC Offset: -4.93h; DST: 0.0h
The best way to get a time zone is to pass the relevant timezone string to the gettz()
function, which is intended to be a Python equivalent to the TZ
environment variable.
Passing nothing gets the current local time zone:
gettz()
tzfile('/etc/localtime')
# Retrieve IANA zone:
print(gettz('Pacific/Kiritimati'))
tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
# Directly parse a TZ variable:
print(gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3'))
tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
gettz
will return the same time zone object for the same input string, unless the cache is bypassed
gettz('America/New_York') is gettz('America/New_York')
True
In addition to the performance improvement, this is also because CPython relies on object equality for some of its datetime semantics:
LON = tz.gettz('Europe/London')
# Construct a datetime
x = datetime(2007, 3, 25, 1, 0, tzinfo=tz.gettz('Europe/London'))
ts = x.timestamp() # Get a timestamp representing the same datetime
# Get the same datetime from the timestamp
y = datetime.fromtimestamp(ts, tz.gettz('Europe/London'))
# Get the same datetime from the timestamp with a fresh instance of LON
z = datetime.fromtimestamp(ts, tz.gettz.nocache('Europe/London'))
print(f'x == y: {x == y}')
print(f'x == z: {x == z}')
print(f'y == z: {y == z}')
x == y: False x == z: True y == z: True
For a detailed write-up of this case, see my blog entry: https://blog.ganssle.io/articles/2018/02/a-curious-case-datetimes.html
Ambiguous times are times where the same "wall time" occurs twice, such as during a DST to STD transition. dateutil
provides the tz.datetime_ambiguous()
method to determine if a datetime is ambiguous in a given zone.
dt1 = datetime(2004, 10, 31, 4, 30, tzinfo=tzutc())
for i in range(4):
dt = (dt1 + timedelta(hours=i)).astimezone(NYC)
print('{} | {} | {}'.format(dt, dt.tzname(),
'Ambiguous' if tz.datetime_ambiguous(dt) else 'Unambiguous'))
2004-10-31 00:30:00-04:00 | EDT | Unambiguous 2004-10-31 01:30:00-04:00 | EDT | Ambiguous 2004-10-31 01:30:00-05:00 | EST | Ambiguous 2004-10-31 02:30:00-05:00 | EST | Unambiguous
Imaginary times are wall times that don't exist in a given time zone, such as during an STD to DST transition.
dt1 = datetime(2004, 4, 4, 6, 30, tzinfo=tzutc())
for i in range(3):
dt = (dt1 + timedelta(hours=i)).astimezone(NYC)
print('{} | {} '.format(dt, dt.tzname()))
2004-04-04 01:30:00-05:00 | EST 2004-04-04 03:30:00-04:00 | EDT 2004-04-04 04:30:00-04:00 | EDT
assert not tz.datetime_exists(datetime(2004, 4, 4, 2, 30), tz=NYC)
Using tz.resolve_imaginary
you can automatically shift imaginary datetimes forward
tz.resolve_imaginary(datetime(2004, 4, 4, 2, 30, tzinfo=NYC))
datetime.datetime(2004, 4, 4, 3, 30, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
It has no effect on existing datetimes:
tz.resolve_imaginary(datetime(2004, 8, 12, 12, tzinfo=NYC))
datetime.datetime(2004, 8, 12, 12, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
It automatically detects the amount of the offset:
tz.resolve_imaginary(datetime(1994, 12, 31, 12, tzinfo=tz.gettz('Pacific/Kiritimati')))
datetime.datetime(1995, 1, 1, 12, 0, tzinfo=tzfile('/usr/share/zoneinfo/Pacific/Kiritimati'))
dateutil.parser
¶The parser is used to take datetime strings of a valid but usually unknown format and convert them into datetime
objects.
from dateutil.parser import parse, parser
parser().parse('March 8, 1942 10:13')
datetime.datetime(1942, 3, 8, 10, 13)
parse('1991-02-03')
datetime.datetime(1991, 2, 3, 0, 0)
list(map(parse, ['01-03-04', '11-01-04', '32-04-03']))
[datetime.datetime(2004, 1, 3, 0, 0), datetime.datetime(2004, 11, 1, 0, 0), datetime.datetime(2032, 4, 3, 0, 0)]
parse("Pat Morita's birthday is June 28, 1932", fuzzy=True)
datetime.datetime(1932, 6, 28, 0, 0)
dt_base = '2009-09-14 02:33:44'
# This works for your local time zone or any fixed offset
list(map(parse, (dt_base + x for x in ('EST', 'CST-8', 'UTC-4', '-0400'))))
[datetime.datetime(2009, 9, 14, 2, 33, 44, tzinfo=tzlocal()), datetime.datetime(2009, 9, 14, 2, 33, 44, tzinfo=tzoffset('CST', 28800)), datetime.datetime(2009, 9, 14, 2, 33, 44, tzinfo=tzoffset(None, 14400)), datetime.datetime(2009, 9, 14, 2, 33, 44, tzinfo=tzoffset(None, -14400))]
# You can also specify the time zone context for ambiguous zones
IST = gettz('Asia/Kolkata'); CST = gettz('Asia/Shanghai')
parse("2002-09-14 02:33:44 PM CST", tzinfos={'CST': CST, 'IST': IST})
datetime.datetime(2002, 9, 14, 14, 33, 44, tzinfo=tzfile('/usr/share/zoneinfo/Asia/Shanghai'))
dateutil.parser.isoparse
¶As of version 2.7.0, dateutil
introduced a dedicated ISO-8601 parser.
from dateutil.parser import isoparse
isoparse('2018-04-03')
datetime.datetime(2018, 4, 3, 0, 0)
isoparse('2018-04-03T14:40')
datetime.datetime(2018, 4, 3, 14, 40)
isoparse('2018-04-03T14:40-05:00')
datetime.datetime(2018, 4, 3, 14, 40, tzinfo=tzoffset(None, -18000))
isoparse
advantages¶%timeit parse('2018-04-03T14:40-05:00')
175 µs ± 11.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit isoparse('2018-04-03T14:40-05:00')
26 µs ± 1.92 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
parse('2014.04.03T14:40 GMT+05:00')
datetime.datetime(2014, 4, 3, 14, 40, tzinfo=tzoffset(None, -18000))
try:
isoparse('2014.04.03T14:40 GMT+05:00')
except ValueError as e:
print(f'ValueError: {e}')
ValueError: invalid literal for int() with base 10: b'.04'
When you know the format of the string: Use strptime
Don't use it to validate whether or not something is a datetime
When you know the format of the string to within a few possibilities, it's almost certainly faster and more accurate to guess-and-check.
dateutil.rrule
¶rrule
is an implementation of recurrence rules as laid out in the iCalendar RFC (RFC 5545).
Recurrence rules are rules for generating dates and times at some (potentially complex) interval.
Some examples:
from dateutil.rrule import rrule, rruleset, rrulestr
from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
# All of Pat Morita's birthdays that fell on a Monday
rr = rrule(freq=YEARLY, bymonth=6, bymonthday=28, byweekday=MO,
dtstart=datetime(1932, 6, 28),
until=datetime(2005, 11, 24))
rr.between(datetime(1955, 11, 1),
datetime(1975, 4, 30)) # ...during the Vietnam War
[datetime.datetime(1965, 6, 28, 0, 0), datetime.datetime(1971, 6, 28, 0, 0)]
Fundamental elements of an rrule
are:
dtstart
: The start point of the recurrence (this is similar to a phase)freq
: The units of the fundamental frequency of the recurrence. It takes the values YEARLY
, MONTHLY
, WEEKLY
, DAILY
, HOURLY
, MINUTELY
, SECONDLY
interval
: The fundamental frequency of the recurrence, in units of freq
. If unspecified, this is 1.hourly = rrule(freq=HOURLY, interval=1, dtstart=datetime(2016, 7, 18, 9), count=3)
interval_2 = hourly.replace(interval=2)
dtstart_rr = hourly.replace(dtstart=datetime(2016, 7, 18, 10))
print_rrs([hourly, dtstart_rr, interval_2], ['Hourly', 'dtstart', 'interval=2'])
Hourly | dtstart | interval=2 -------------------------------------------------------------------------------- 2016-07-18 09:00 | 2016-07-18 10:00 | 2016-07-18 09:00 2016-07-18 10:00 | 2016-07-18 11:00 | 2016-07-18 11:00 2016-07-18 11:00 | 2016-07-18 12:00 | 2016-07-18 13:00
byxxx
rules¶byxxx
rules serve to modify the frequency of the recurrence in some way. The supported rules are bymonth
, bymonthday
, byyearday
, byweekno
, byweekday
, byhour
, byminute
and bysecond
, bysetpos
and byeaster
.
byxxx
rules greater than or equal to freq
are constraints and (generally) reduce the frequency of the recurrence: # Base is DAILY, but by restricted to Tuesdays in November
list(rrule(DAILY, bymonth=11, byweekday=(TU, ),
dtstart=datetime(2015, 1, 1, 12), count=5))
[datetime.datetime(2015, 11, 3, 12, 0), datetime.datetime(2015, 11, 10, 12, 0), datetime.datetime(2015, 11, 17, 12, 0), datetime.datetime(2015, 11, 24, 12, 0), datetime.datetime(2016, 11, 1, 12, 0)]
byxxx
rules less than freq
will generally increase the frequency of the recurrence:list(rrule(MONTHLY, bymonthday=(1, 15, 30),
dtstart=datetime(2015, 1, 16, 12, 15), count=4))
[datetime.datetime(2015, 1, 30, 12, 15), datetime.datetime(2015, 2, 1, 12, 15), datetime.datetime(2015, 2, 15, 12, 15), datetime.datetime(2015, 3, 1, 12, 15)]
If otherwise unspecified, recurrences can be generated to infinity (or at least until Python can't represent the date anymore). The two ways to specify a termination point as part of the rule are with the mutually exclusive count
and until
arguments.
count
terminates the rule after a specific number of instances have been generated# The next 2 instances where the 4th of July falls on a Friday
list(rrule(YEARLY, bymonth=7, bymonthday=4, byweekday=FR,
dtstart=datetime(2016, 7, 5), count=2))
[datetime.datetime(2025, 7, 4, 0, 0), datetime.datetime(2031, 7, 4, 0, 0)]
until
terminates the rule on a specific date:# The Friday the 13ths before January 1st, 2018
list(rrule(MONTHLY, bymonthday=13, byweekday=FR,
dtstart=datetime(2016, 7, 17, 12), until=datetime(2018, 1, 1)))
[datetime.datetime(2017, 1, 13, 12, 0), datetime.datetime(2017, 10, 13, 12, 0)]
It is also possible to retrieve specific subsets of the recurrence, e.g. the first recurence after
a given date:
rr = rrule(DAILY, byhour=(9), byweekday=range(0, 5), dtstart=datetime(2016, 7, 1))
rr.after(datetime.now()) # The beginning of the next weekday
datetime.datetime(2018, 5, 30, 9, 0)
You can retrieve the most recent recurrence before a given date:
rr.before(datetime(2017, 3, 14)) # Apparently this is a Saturday
datetime.datetime(2017, 3, 13, 9, 0)
You can also get all the recurrences between two dates:
# byeaster is a non-standard extension in dateutil that calculates a day
# offset from easter. This rule generates all the easters between 1995 and 2000.
rr = rrule(YEARLY, byeaster=0, dtstart=datetime(1990, 1, 1))
rr.between(datetime(1995, 1, 1), datetime(2000, 1, 1))
[datetime.datetime(1995, 4, 16, 0, 0), datetime.datetime(1996, 4, 7, 0, 0), datetime.datetime(1997, 3, 30, 0, 0), datetime.datetime(1998, 4, 12, 0, 0), datetime.datetime(1999, 4, 4, 0, 0)]
rrulestr
¶The iCalendar spec originally refers to a specific string format for specifying recurrence rules.
rrule
s can also be generated from these string using the rrulestr
class:
# DST start and stop transition rules for Pacific Time
dst_start = rrulestr('DTSTART:19671029T020000;\n'
'FREQ=YEARLY;BYDAY=1SU;BYMONTH=4')
dst_end = rrulestr('DTSTART:19671029T020000;\n'
'FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10')
rrset = rruleset()
rrset.rrule(dst_start)
rrset.rrule(dst_end)
rrset.between(datetime(2016, 1, 1), datetime(2018, 1, 1))
[datetime.datetime(2016, 4, 3, 2, 0), datetime.datetime(2016, 10, 30, 2, 0), datetime.datetime(2017, 4, 2, 2, 0), datetime.datetime(2017, 10, 29, 2, 0)]
str(rrule)
¶You can generate RRULE
strings from rrule
objects as well:
# This string should be compatible with other applications using the iCalendar spec
str(rrule(YEARLY, byyearday=180, byhour=(1, 4, 12), dtstart=datetime(2014, 9, 13)))
'DTSTART:20140913T000000\nRRULE:FREQ=YEARLY;BYYEARDAY=180;BYHOUR=1,4,12'
# Note that the BYEASTER directive is the only RFC-incompatible output
str(rrule(YEARLY, byeaster=0, dtstart=datetime(1990, 1, 1), count=14))
'DTSTART:19900101T000000\nRRULE:FREQ=YEARLY;COUNT=14;BYEASTER=0'
Some recurrences cannot be expressed in a single rrule. rruleset
allows you to combine rrule
s and datetime
s to generate an arbitrary recurrence schedule.
rruleset
interface:
rruleset.rrule()
: Add a recurrence rule to the setrruleset.exrule()
: Subtract a recurrence rule from the setrruleset.rdate()
: Add a specific datetime to the setrruleset.exdate()
: Subtract a specific datetime from the setdtstart = datetime(2016, 11, 1, 0, 0) # The base date
WEEKDAYS = (MO, TU, WE, TH, FR); WEEKENDS = (SA, SU)
bus_schedule = rruleset()
# During the week, it comes every hour on the 37 from 6:37AM to 10:37PM...
weekday_schedule = rrule(DAILY, byweekday=WEEKDAYS,
byhour=range(6, 22), byminute=37, dtstart=dtstart)
bus_schedule.rrule(weekday_schedule) # Add an rrule to the rule set
# ..except after 6, when it comes every other hour - so exclude 7:37PM and 9:37PM!
weeknight_schedule = weekday_schedule.replace(byhour=(19, 21))
bus_schedule.exrule(weeknight_schedule)
# During the weekend, it comes every hour on the :07, from 8AM to 7PM
weekend_schedule = rrule(DAILY, byweekday=WEEKENDS,
byhour=range(8, 20), byminute=7, dtstart=dtstart)
bus_schedule.rrule(weekend_schedule)
rdate
and exdate
¶# But on November 8th, 2016, politicians have arranged for busses to undergo
# "service", so the normal bus schedule is canceled that day
exdates = bus_schedule.between(datetime(2016, 11, 8, 0), datetime(2016, 11, 9))
for exdate in exdates:
bus_schedule.exdate(exdate)
# And in its place they've added one bus at 4:32 AM
bus_schedule.rdate(datetime(2016, 11, 8, 4, 37))
# And one at 7:49 PM
bus_schedule.rdate(datetime(2016, 11, 8, 19, 49))
bus_list = bus_schedule.between(datetime(2016, 11, 7), datetime(2016, 11, 14))
o = print_bus_schedule(bus_list)
HTML(o)
2016-11-07 | 2016-11-08 | 2016-11-09 | 2016-11-10 | 2016-11-11 | 2016-11-12 | 2016-11-13 |
---|---|---|---|---|---|---|
Mon | Tue | Wed | Thu | Fri | Sat | Sun |
06:37:00 | 04:37:00 | 06:37:00 | 06:37:00 | 06:37:00 | 08:07:00 | 08:07:00 |
07:37:00 | 19:49:00 | 07:37:00 | 07:37:00 | 07:37:00 | 09:07:00 | 09:07:00 |
08:37:00 | None | 08:37:00 | 08:37:00 | 08:37:00 | 10:07:00 | 10:07:00 |
09:37:00 | None | 09:37:00 | 09:37:00 | 09:37:00 | 11:07:00 | 11:07:00 |
10:37:00 | None | 10:37:00 | 10:37:00 | 10:37:00 | 12:07:00 | 12:07:00 |
11:37:00 | None | 11:37:00 | 11:37:00 | 11:37:00 | 13:07:00 | 13:07:00 |
12:37:00 | None | 12:37:00 | 12:37:00 | 12:37:00 | 14:07:00 | 14:07:00 |
13:37:00 | None | 13:37:00 | 13:37:00 | 13:37:00 | 15:07:00 | 15:07:00 |
14:37:00 | None | 14:37:00 | 14:37:00 | 14:37:00 | 16:07:00 | 16:07:00 |
15:37:00 | None | 15:37:00 | 15:37:00 | 15:37:00 | 17:07:00 | 17:07:00 |
16:37:00 | None | 16:37:00 | 16:37:00 | 16:37:00 | 18:07:00 | 18:07:00 |
17:37:00 | None | 17:37:00 | 17:37:00 | 17:37:00 | 19:07:00 | 19:07:00 |
18:37:00 | None | 18:37:00 | 18:37:00 | 18:37:00 | None | None |
20:37:00 | None | 20:37:00 | 20:37:00 | 20:37:00 | None | None |