User’s Guide#

Zulu’s main type is zulu.Zulu which represents a fixed UTC datetime object.

import zulu

dt = zulu.parse('2016-07-25T19:33:18.137493+00:00')
# <Zulu [2016-07-25T19:33:18.137493+00:00]>

assert isinstance(dt, zulu.Zulu)

from datetime import datetime
assert isinstance(dt, datetime)

It’s a drop-in replacement for native datetime objects (it inherits from datetime.datetime) but deals only with UTC time zones internally.

Basic Data Access#

All the attributes and methods from datetime are available along with a few new ones:

assert dt.year == 2016
assert dt.month == 7
assert dt.day == 25
assert dt.hour == 19
assert dt.minute == 33
assert dt.second == 18
assert dt.microsecond == 137493
assert dt.tzname() == 'UTC'

dt.utcoffset()
# datetime.timedelta(0)

dt.dst()
# datetime.timedelta(0)

dt.isoformat()
# '2016-07-25T19:33:18.137493+00:00'

dt.weekday()
# 0

dt.isoweekday()
# 1

dt.isocalendar()
# (2016, 30, 1)

dt.ctime()
# 'Mon Jul 25 19:33:18 2016'

dt.toordinal()
# 736170

dt.timetuple()
# time.struct_time(tm_year=2016, tm_mon=7, tm_mday=25, tm_hour=19, tm_min=33, tm_sec=18,
#                  tm_wday=0, tm_yday=207, tm_isdst=0)

dt.utctimetuple()
# time.struct_time(tm_year=2016, tm_mon=7, tm_mday=25, tm_hour=19, tm_min=33, tm_sec=18,
#                  tm_wday=0, tm_yday=207, tm_isdst=0)

dt.timestamp()
# 1469475198.137493

dt.date()
# datetime.date(2016, 7, 25)

dt.time()
# datetime.time(19, 33, 18, 137493)

dt.timetz()
# datetime.time(19, 33, 18, 137493, tzinfo=<UTC>)

Along with a few new ones:

dt.naive
# datetime.datetime(2016, 7, 25, 19, 33, 18, 137493)

dt.datetime
# datetime.datetime(2016, 7, 25, 19, 33, 18, 137493, tzinfo=<UTC>)

dt.is_leap_year()
# True

dt.days_in_month()
# 31

tuple(dt)
# (2016, 7, 25, 19, 33, 18, 137493, <UTC>)

Parsing and Formatting#

By default, zulu.parse will look for either an ISO8601 formatted string or a POSIX timestamp while assuming a UTC timezone when no explicit timezone found in the string:

zulu.parse('2016-07-25 15:33:18-0400')
# <Zulu [2016-07-25T19:33:18+00:00]>

zulu.parse('2016-07-25 15:33:18-0400', zulu.ISO8601)
# <Zulu [2016-07-25T19:33:18+00:00]>

zulu.parse('2016-07-25')
# <Zulu [2016-07-25T00:00:00+00:00]>

zulu.parse('2016-07-25 19:33')
# <Zulu [2016-07-25T19:33:00+00:00]>

zulu.parse(1469475198.0)
# <Zulu [2016-07-25T19:33:18+00:00]>

zulu.parse(1469475198.0, zulu.TIMESTAMP)
# <Zulu [2016-07-25T19:33:18+00:00]>

Multiple formats can be supplied and zulu.parse will try them all:

zulu.parse('3/2/1992', 'ISO8601')
# zulu.parser.ParseError: Value "3/2/1992" does not match any format in "ISO8601"
# (Unable to parse date string '3/2/1992')

dt = zulu.parse('3/2/1992', ['ISO8601', 'MM/dd/YYYY'])
# <Zulu [1992-03-02T00:00:00+00:00]>

As shown above, special parse format keywords are supported. See Keyword Parse Formats for details.

Other time zones can be substituted for naive datetimes by setting default_tz:

zulu.parse('2016-07-25', default_tz='US/Eastern')
# <Zulu [2016-07-25T04:00:00+00:00]>

zulu.parse('2016-07-25', default_tz='local')
# <Zulu [2016-07-25T04:00:00+00:00]>

The default timezone is ignored when the input has it set:

zulu.parse('2016-07-25T15:33:18-0700', default_tz='US/Eastern')
# <Zulu [2016-07-25T22:33:18+00:00]>

String parsing/formatting in Zulu supports both strftime/strptime directives and Unicode date patterns.

dt.format('%Y-%m-%d %H:%M:%S%z')
# '2016-07-25 19:33:18+0000'

dt.format('YYYY-MM-dd HH:mm:ssZ')
# '2016-07-25 19:33:18+0000'

dt.format('%Y-%m-%d %H:%M:%S%z', tz='US/Eastern')
# '2016-07-25 15:33:18-0400'

dt.format('%Y-%m-%d %H:%M:%S%z', tz='local')
# '2016-07-25 15:33:18-0400'

zulu.parse('2016-07-25 15:33:18-0400', '%Y-%m-%d %H:%M:%S%z')
# <Zulu [2016-07-25T19:33:18+00:00]>

You can even use zulu.parser.format_datetime with native datetimes:

from zulu.parser import UTC, format_datetime

native = datetime(2016, 7, 25, 19, 33, 18, 137493, tzinfo=UTC)

format_datetime(native, '%Y-%m-%d %H:%M:%S%z')
# '2016-07-25 19:33:18+0000'

format_datetime(native, 'YYYY-MM-dd HH:mm:ssZ')
# '2016-07-25 19:33:18+0000'

dt = Zulu.fromdatetime(native)
format_datetime(dt, 'YYYY-MM-dd HH:mm:ssZ')
# '2016-07-25 19:33:18+0000'

Keyword Parse Formats#

The following keywords can be supplied to zulu.parse in place of a format directive or pattern:

zulu.parse(1469475198, 'timestamp')
# <Zulu [2016-07-25T19:33:18+00:00]>

Keyword

Description

Sample Input

ISO8601

Parse ISO8601 string

  • 2016-07-25 15:33:18-0400

  • 2016-07-25 15:33

  • 2016-07-25

  • 2016-07

timestamp

Parse POSIX timestamp

  • 1469475198

  • 1469475198.314218

Format Tokens#

Zulu supports two different styles of string parsing/formatting tokens:

Either style can be used during parsing:

dt = zulu.parse('07/25/16 15:33:18 -0400', '%m/%d/%y %H:%M:%S %z')
# <Zulu [2016-07-25T19:33:18+00:00]>

dt = zulu.parse('07/25/16 15:33:18 -0400', 'MM/dd/YY HH:mm:ss Z')
# <Zulu [2016-07-25T19:33:18+00:0

and formatting:

dt.format('%m/%d/%y %H:%M:%S %z')
# '07/25/16 19:33:18 +0000'

dt.format('MM/dd/YY HH:mm:ss Z')
'07/25/16 19:33:18 +0000'

Format Directives#

All directives from https://docs.python.org/3.5/library/datetime.html#strftime-and-strptime-behavior are supported.

Date Patterns#

A subset of patterns from http://www.unicode.org/reports/tr35/tr35-19.html#Date_Field_Symbol_Table are supported for parsing while _all_ patterns are supported for formatting:

Attribute

Style

Pattern

Examples

Year

4-digit

YYYY

2000, 2001, 2002 … 2015, 2016

Year

2-digit

YY

00, 01, 02 … 15, 16

Month

full name

MMMM

January, February, March

Month

abbr name

MMM

Jan, Feb, Mar … Nov, Dec

Month

int, padded

MM

01, 02, 03 … 11, 12

Month

int, no padding

M

1, 2, 3 … 11, 12

Day of Month

int, padded

dd

01, 02, 03 … 30, 31

Day of Month

int, no padding

d

1, 2, 3 … 30, 31

Day of Year

int, padded

DDD

001, 002, 003 … 054, 055 … 364, 365

Day of Year

int, padded

DD

01, 02, 03 … 54, 55 … 364, 365

Day of Year

int, no padding

D

1, 2, 3 … 54, 55 … 364, 365

Weekday

full name

EEEE

Sunday, Monday, Tuesday … Friday, Saturday

Weekday

abbr name

EEE

Sun, Mon, Tue … Fri, Sat

Weekday

abbr name

EE

Sun, Mon, Tue … Fri, Sat

Weekday

abbr name

E

Sun, Mon, Tue … Fri, Sat

Weekday

abbr name

eee

Sun, Mon, Tue … Fri, Sat

Weekday

int, padded

ee

01, 02, 03 … 06, 07

Weekday

int, no padding

e

1, 2, 3 … 6, 7

Hour

24h, padded

HH

00, 01, 02 … 22, 23

Hour

24h, no padding

H

0, 1, 2 … 22, 23

Hour

12h, padded

hh

00, 01, 02 … 11, 12

Hour

12h, no padding

h

0, 1, 2, … 11, 12

AM / PM

upper case

a

AM, PM

Minute

int, padded

mm

00, 01, 02 … 58, 59

Minute

int, no padding

m

0, 1, 2 … 58, 59

Second

int, padded

ss

00, 01, 02 … 58, 59

Second

int, no padding

s

0, 1, 2 … 58, 59

Microsecond

int, padded

SSSSSS

000000, 000001 … 999998, 999999

Microsecond

int, truncated

SSSSS

00000, 00001 … 99998, 99999

Microsecond

int, truncated

SSSS

0000, 0001 … 9998, 9999

Microsecond

int, truncated

SSS

000, 001 … 998, 999

Microsecond

int, truncated

SS

00, 01 … 98, 99

Microsecond

int, truncated

S

0, 1 … 8, 9

Timezone

w/o separator

Z

-1100, -1000 … +0000 … +1100, +1200

Humanization#

You can humanize the difference between two Zulu objects with Zulu.time_from and Zulu.time_to:

dt
# <Zulu [2016-07-25T19:33:18.137493+00:00]>

dt.time_from(dt.end_of_day())
# '4 hours ago'

dt.time_to(dt.end_of_day())
# 'in 4 hours'

dt.time_from(dt.start_of_day())
# 'in 20 hours'

dt.time_to(dt.start_of_day())
# '20 hours ago'

zulu.now()
# <Zulu [2016-08-12T04:16:17.007335+00:00]>

dt.time_from_now()
# 2 weeks ago

dt.time_to_now()
# in 2 weeks

Time Zone Handling#

Time zones other than UTC are not expressable within a Zulu instance. Other time zones are only ever applied when either converting a Zulu object to a native datetime (via Zulu.astimezone) or during string formatting (via Zulu.format). Zulu understands both tzinfo objects and IANA Timezone Database string names (also known as the Olson database).

local = dt.astimezone()
# same as doing dt.astimezone('local')
# datetime.datetime(2016, 7, 25, 15, 33, 18, 137493,
#                   tzinfo=tzlocal())

pacific = dt.astimezone('US/Pacific')
# datetime.datetime(2016, 7, 25, 12, 33, 18, 137493,
#                   tzinfo=tzfile('/usr/share/zoneinfo/US/Pacific'))

import pytz
mountain = dt.astimezone(pytz.timezone('US/Mountain'))
# datetime.datetime(2016, 7, 25, 13, 33, 18, 137493,
#                   tzinfo=<DstTzInfo 'US/Mountain' MDT-1 day, 18:00:00 DST>)

Shifting, Replacing, and Copying#

Zulu can easily apply timedelta’s using the shift method:

shifted = dt.shift(hours=-5, minutes=10)
# <Zulu [2016-07-25T14:43:18.137493+00:00]>

assert shifted is not dt

And add and subtract with the add and subtract methods:

shifted = dt.subtract(hours=5).add(minutes=10)
# <Zulu [2016-07-25T14:43:18.137493+00:00]>

# First argument to subtract() can be a timedelta or dateutil.relativedelta
shifted = dt.subtract(timedelta(hours=5))
# <Zulu [2016-07-25T14:33:18+00:00]>

# First argument to subtract() can also be another datetime object
dt.subtract(shifted)
# datetime.timedelta(0, 18000)

# First argument to add() can be a timedelta or dateutil.relativedelta
dt.add(timedelta(minutes=10))
# <Zulu [2016-07-25T19:43:18+00:00]>

Or replace datetime attributes:

replaced = dt.replace(hour=14, minute=43)
# <Zulu [2016-07-25T14:43:18.137493+00:00]>

assert replaced is not dt

Or even make a copy:

copied = dt.copy()
# <Zulu [2016-07-25T19:33:18.137493+00:00]>

assert copied is not dt
assert copied == dt

Note

Since Zulu is meant to be immutable, both shift, replace, and copy return new Zulu instances while leaving the original instance unchanged.

Spans, Ranges, Starts, and Ends#

You can get the span across a time frame:

dt = Zulu(2015, 4, 4, 12, 30, 37, 651839)

dt.span('century')
# (<Zulu [2000-01-01T00:00:00+00:00]>, <Zulu [2099-12-31T23:59:59.999999+00:00]>)

dt.span('decade')
# (<Zulu [2010-01-01T00:00:00+00:00]>, <Zulu [2019-12-31T23:59:59.999999+00:00]>)

dt.span('year')
# (<Zulu [2015-01-01T00:00:00+00:00]>, <Zulu [2015-12-31T23:59:59.999999+00:00]>)

dt.span('month')
# (<Zulu [2015-04-01T00:00:00+00:00]>, <Zulu [2015-04-30T23:59:59.999999+00:00]>)

dt.span('week')
# (<Zulu [2015-03-30T00:00:00+00:00]>, <Zulu [2015-04-05T23:59:59.999999+00:00]>)

dt.span('day')
# (<Zulu [2015-04-04T00:00:00+00:00]>, <Zulu [2015-04-04T23:59:59.999999+00:00]>)

dt.span('hour')
# (<Zulu [2015-04-04T12:00:00+00:00]>, <Zulu [2015-04-04T12:59:59.999999+00:00]>)

dt.span('minute')
# (<Zulu [2015-04-04T12:30:00+00:00]>, <Zulu [2015-04-04T12:30:59.999999+00:00]>)

dt.span('second')
# (<Zulu [2015-04-04T12:30:37+00:00]>, <Zulu [2015-04-04T12:30:37.999999+00:00]>)

dt.span('century', count=3)
# (<Zulu [2000-01-01T00:00:00+00:00]>, <Zulu [2299-12-31T23:59:59.999999+00:00]>)

dt.span('decade', count=3)
# (<Zulu [2010-01-01T00:00:00+00:00]>, <Zulu [2039-12-31T23:59:59.999999+00:00]>)

Or you can get just the start or end of a time frame:

dt.start_of('day')  # OR dt.start_of_day()
# <Zulu [2015-04-04T00:00:00+00:00]>

dt.end_of('day')  # OR dt.end_of_day()
# <Zulu [2015-04-04T23:59:59.999999+00:00]>

dt.end_of('year', count=3)  # OR dt.end_of_year()
# <Zulu [2017-12-31T23:59:59.999999+00:00]>

Note

Supported time frames are century, decade, year, month, week, day, hour, minute, second and are accessible both from start_of(frame)/end_of(frame) and start_of_<frame>()/end_of_<frame>.

You can get a range of time spans:

start = Zulu(2015, 4, 4, 12, 30)
end = Zulu(2015, 4, 4, 16, 30)

for span in zulu.span_range('hour', start, end):
    print(span)
# (<Zulu [2015-04-04T12:00:00+00:00]>, <Zulu [2015-04-04T12:59:59.999999+00:00]>)
# (<Zulu [2015-04-04T13:00:00+00:00]>, <Zulu [2015-04-04T13:59:59.999999+00:00]>)
# (<Zulu [2015-04-04T14:00:00+00:00]>, <Zulu [2015-04-04T14:59:59.999999+00:00]>)
# (<Zulu [2015-04-04T15:00:00+00:00]>, <Zulu [2015-04-04T15:59:59.999999+00:00]>)

Or you can iterate over a range of datetimes:

start = Zulu(2015, 4, 4, 12, 30)
end = Zulu(2015, 4, 4, 16, 30)

for dt in zulu.range('hour', start, end):
    print(dt)
# <Zulu [2015-04-04T12:30:00+00:00]>
# <Zulu [2015-04-04T13:30:00+00:00]>
# <Zulu [2015-04-04T14:30:00+00:00]>

Note

Supported range/span time frames are century, decade, year, month, week, day, hour, minute, second.

Time Deltas#

In addition to having a drop-in replacement for datetime, zulu also has a drop-in replacement for timedelta:

delta = zulu.parse_delta('1w 3d 2h 32m')
# <Delta [10 days, 2:32:00]>

assert isinstance(delta, zulu.Delta)

from datetime import timedelta
assert isinstance(delta, timedelta)

zulu.parse_delta('2:04:13:02.266')
# <Delta [2 days, 4:13:02.266000]>

zulu.parse_delta('2 days, 5 hours, 34 minutes, 56 seconds')
# <Delta [2 days, 5:34:56]>

Other formats that zulu.parse_delta can parse are:

  • 32m

  • 2h32m

  • 3d2h32m

  • 1w3d2h32m

  • 1w 3d 2h 32m

  • 1 w 3 d 2 h 32 m

  • 4:13

  • 4:13:02

  • 4:13:02.266

  • 2:04:13:02.266

  • 2 days,  4:13:02 (uptime format)

  • 2 days,  4:13:02.266

  • 5hr34m56s

  • 5 hours, 34 minutes, 56 seconds

  • 5 hrs, 34 mins, 56 secs

  • 2 days, 5 hours, 34 minutes, 56 seconds

  • 1.2 m

  • 1.2 min

  • 1.2 mins

  • 1.2 minute

  • 1.2 minutes

  • 172 hours

  • 172 hr

  • 172 h

  • 172 hrs

  • 172 hour

  • 1.24 days

  • 5 d

  • 5 day

  • 5 days

  • 5.6 wk

  • 5.6 week

  • 5.6 weeks

Similar to Zulu.time_to/from, Delta objects can be humanized with the Delta.format method:

delta = zulu.parse_delta('2h 32m')
# <Delta [2:32:00]>

delta.format()
# '3 hours'

delta.format(add_direction=True)
# 'in 3 hours'

zulu.parse_delta('-2h 32m').format(add_direction=True)
# '3 hours ago'

delta.format(granularity='day')
# '1 day'

delta.format(locale='de')
# '3 Stunden'

delta.format(locale='fr', add_direction=True)
# 'dans 3 heures'

delta.format(threshold=0)
# '0 years'

delta.format(threshold=0.1)
# '0 days'

delta.format(threshold=0.2)
# '3 hours'

delta.format(threshold=5)
# '152 minutes'

delta.format(threshold=155)
# '9120 seconds'

delta.format(threshold=155, granularity='minute')
# '152 minutes'

delta.format(style='long')
# '3 hours'

delta.format(style='short')
# '3 hr'

delta.format(style='narrow')
# '3h'

Utilities#

zulu.to_seconds#

Easily convert time units from microseconds to weeks into seconds:

zulu.to_seconds(seconds=5, minutes=2, hours=3, days=2, weeks=1)
# 788525

zulu.to_seconds(milliseconds=25300, seconds=5, minutes=2)
# 150.3

zulu.Timer#

A timer object that can be used for keeping track of elapsed time or as a coutdown timer.

As a timer:

timer = zulu.Timer()
timer.start()

# is the same as...
timer = zulu.Timer().start()

timer.started()
# True

timer.stopped()
# False

timer.elapsed()
# 0.0003867149353027344

timer.stop()

timer.elapsed()
# 0.0009307861328125

Can be used as a context manager to track the duration of blocks of code:

timer = zulu.Timer()

with timer:
    time.sleep(1)

# is the same as ...
# with zulu.Timer() as timer:
#     ...

timer.elapsed()
# 1.001131534576416

# can be used multiple times to accumulate durations
with timer:
    time.sleep(2)

timer.elapsed()
# 3.0032811164855957

# reset the timer
timer.reset()

timer.started()
# False

timer.elapsed()
# 0

And as a countdown timer:

# timer that runs out after 15 seconds
timer = zulu.Timer(timeout=15)
timer.start()

timer.done()
# False

timer.remaining()
# 14.999720811843872

time.sleep(5)

timer.remaining()
# 9.99406123161316

time.sleep(10)

timer.done()
# True

timer.remaining()
# -0.01725912094116211

# restart the timer by calling start() again
timer.start()

timer.done()
# False