===========
 aniso8601
===========

----------------------------------
Another ISO 8601 parser for Python
----------------------------------

Features
========
* Pure Python implementation
* Python 3 support
* No extra dependencies
* Logical behavior

 - Parse a time, get a `datetime.time <http://docs.python.org/2/library/datetime.html#datetime.time>`_
 - Parse a date, get a `datetime.date <http://docs.python.org/2/library/datetime.html#datetime.date>`_
 - Parse a datetime, get a `datetime.datetime <http://docs.python.org/2/library/datetime.html#datetime.datetime>`_
 - Parse a duration, get a `datetime.timedelta <http://docs.python.org/2/library/datetime.html#datetime.timedelta>`_
 - Parse an interval, get a tuple of dates or datetimes
 - Parse a repeating interval, get a date or datetime `generator <http://www.python.org/dev/peps/pep-0255/>`_

* UTC offset represented as fixed-offset tzinfo
* No regular expressions

Installation
============

The recommended installation method is to use pip::

  $ pip install aniso8601

Alternatively, you can download the source (git repository hosted at `Bitbucket <https://bitbucket.org/nielsenb/aniso8601>`_) and install directly::

  $ python setup.py install

Use
===

Parsing datetimes
-----------------

To parse a typical ISO 8601 datetime string::

  >>> import aniso8601
  >>> aniso8601.parse_datetime('1977-06-10T12:00:00Z')
  datetime.datetime(1977, 6, 10, 12, 0, tzinfo=<aniso8601.UTCOffset object at 0x7f44fadbbd90>)

Alternative delimiters can be specified, for example, a space::

  >>> aniso8601.parse_datetime('1977-06-10 12:00:00Z', delimiter=' ')
  datetime.datetime(1977, 6, 10, 12, 0, tzinfo=<aniso8601.UTCOffset object at 0x7f44fadbbf50>)

UTC offsets are supported::

  >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00')
  datetime.datetime(1979, 6, 5, 8, 0, tzinfo=<aniso8601.UTCOffset object at 0x7f44fadbbf50>)

If a UTC offset is not specified, the returned datetime will be naive::

  >>> aniso8601.parse_datetime('1983-01-22T08:00:00')
  datetime.datetime(1983, 1, 22, 8, 0)

Parsing dates
-------------

To parse a date represented in an ISO 8601 string::

  >>> import aniso8601
  >>> aniso8601.parse_date('1984-04-23')
  datetime.date(1984, 4, 23)

Basic format is supported as well::

  >>> aniso8601.parse_date('19840423')
  datetime.date(1984, 4, 23)

To parse a date using the ISO 8601 week date format::

  >>> aniso8601.parse_date('1986-W38-1')
  datetime.date(1986, 9, 15)

To parse an ISO 8601 ordinal date::

  >>> aniso8601.parse_date('1988-132')
  datetime.date(1988, 5, 11)

Parsing times
-------------

To parse a time formatted as an ISO 8601 string::

  >>> import aniso8601
  >>> aniso8601.parse_time('11:31:14')
  datetime.time(11, 31, 14)

As with all of the above, basic format is supported::

  >>> aniso8601.parse_time('113114')
  datetime.time(11, 31, 14)

A UTC offset can be specified for times::

  >>> aniso8601.parse_time('17:18:19-02:30')
  datetime.time(17, 18, 19, tzinfo=<aniso8601.UTCOffset object at 0x7f44fad82c50>)
  >>> aniso8601.parse_time('171819Z')
  datetime.time(17, 18, 19, tzinfo=<aniso8601.UTCOffset object at 0x7f44fadbbd90>)

Reduced accuracy is supported::

  >>> aniso8601.parse_time('21:42')
  datetime.time(21, 42)
  >>> aniso8601.parse_time('22')
  datetime.time(22, 0)

A decimal fraction is always allowed on the lowest order element of an ISO 8601 formatted time::

  >>> aniso8601.parse_time('22:33.5')
  datetime.time(22, 33, 30)
  >>> aniso8601.parse_time('23.75')
  datetime.time(23, 45)

Parsing durations
-----------------

To parse a duration formatted as an ISO 8601 string::

  >>> import aniso8601
  >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S')
  datetime.timedelta(428, 17646)

Reduced accuracy is supported::

  >>> aniso8601.parse_duration('P1Y')
  datetime.timedelta(365)

A decimal fraction is allowed on the lowest order element::

  >>> aniso8601.parse_duration('P1YT3.5M')
  datetime.timedelta(365, 210)

The decimal fraction can be specified with a comma instead of a full-stop::

  >>> aniso8601.parse_duration('P1YT3,5M')
  datetime.timedelta(365, 210)

Parsing a duration from a combined date and time is supported as well::

  >>> aniso8601.parse_duration('P0001-01-02T01:30:5')
  datetime.timedelta(397, 5405)

Parsing intervals
-----------------

To parse an interval specified by a start and end::

  >>> import aniso8601
  >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00')
  (datetime.datetime(2007, 3, 1, 13, 0), datetime.datetime(2008, 5, 11, 15, 30))

Intervals specified by a start time and a duration are supported::

  >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M')
  (datetime.datetime(2007, 3, 1, 13, 0, tzinfo=<aniso8601.UTCOffset object at 0x7f698d44d110>), datetime.datetime(2008, 5, 9, 15, 30, tzinfo=<aniso8601.UTCOffset object at 0x7f698d44d110>))

A duration can also be specified by a duration and end time::

  >>> aniso8601.parse_interval('P1M/1981-04-05')
  (datetime.date(1981, 4, 5), datetime.date(1981, 3, 6))

Notice that the result of the above parse is not in order from earliest to latest. If sorted intervals are required, simply use the 'sorted' keyword as shown below::

  >>> sorted(aniso8601.parse_interval('P1M/1981-04-05'))
  [datetime.date(1981, 3, 6), datetime.date(1981, 4, 5)]

Repeating intervals are supported as well, and return a generator::

  >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D')
  <generator object date_generator at 0x7f698cdefc80>
  >>> list(aniso8601.parse_repeating_interval('R3/1981-04-05/P1D'))
  [datetime.date(1981, 4, 5), datetime.date(1981, 4, 6), datetime.date(1981, 4, 7)]

Repeating intervals are allowed to go in the reverse direction::

  >>> list(aniso8601.parse_repeating_interval('R2/PT1H2M/1980-03-05T01:01:00'))
  [datetime.datetime(1980, 3, 5, 1, 1), datetime.datetime(1980, 3, 4, 23, 59)]

Unbounded intervals are also allowed (Python 2)::

  >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')
  >>> result.next()
  datetime.datetime(1980, 3, 5, 1, 1)
  >>> result.next()
  datetime.datetime(1980, 3, 4, 23, 59)

or for Python 3::

  >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')
  >>> next(result)
  datetime.datetime(1980, 3, 5, 1, 1)
  >>> next(result)
  datetime.datetime(1980, 3, 4, 23, 59)

Note that you should never try to convert a generator produced by an unbounded interval to a list::

  >>> list(aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00'))
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "aniso8601/__init__.py", line 707, in date_generator_unbounded
      currentdate += timedelta
  OverflowError: date value out of range

Tests
=====

To run the unit tests, in your source checkout, navigate to the source directory for the Python version being worked on (python2, python3) and type::

   $ python -m unittest discover aniso8601/tests/

Contributing
============

aniso8601 is an open source project hosted on `Bitbucket <https://bitbucket.org/nielsenb/aniso8601>`_.

Any and all bugs are welcome on our `issue tracker <https://bitbucket.org/nielsenb/aniso8601/issues>`_.
Of particular interest are valid ISO 8601 strings that don't parse, or invalid ones that do. At a minimum,
bug reports should include an example of the misbehaving string, as well as the expected result. Of course
patches containing unit tests (or fixed bugs) are welcome!

References
==========

* `ISO 8601:2004(E) <http://dotat.at/tmp/ISO_8601-2004_E.pdf>`_ (Caution, PDF link)
* `Wikipedia article on ISO 8601 <http://en.wikipedia.org/wiki/Iso8601>`_
* `Discussion on alternative ISO 8601 parsers for Python <https://groups.google.com/forum/#!topic/comp.lang.python/Q2w4R89Nq1w>`_
