Current section
Files
Jump to
Current section
Files
README.md
# iso8601
[![Build Status][gh-actions-badge]][gh-actions]
[![Coverage][coverage-badge]][project]
[![Tag][tag-badge]][tag]
[![Erlang Version][erl-badge]][erl]
[![Downloads][hex downloads]][hex package]
*An ISO 8601 date formatting and parsing library for Erlang*
[![iso8601 project logo][logo]][logo-large]
<a name="contents"></a>
#### Contents
* [About](#about-)
* [New in 1.4](#new-in-14-)
* [Usage](#usage-)
* [Known Deficiencies](#known-deficiencies-)
* [License](#license-)
<a name="about-"></a>
## About [Back to contents](#contents)
The ``erlang_iso8601`` library was originally created by Sean Sawyer in 2011. In 2016, Sean handed off maintenance of the library to the [erlsci](https://github.com/erlsci) Github org at which point the project (and repo) was renamed to simply ``iso8601``, matching its Erlang app name:
* ``git clone https://github.com/erlsci/iso8601.git``
Thanks to Github's forwarding for project renames and moves, the following still work:
* ``git clone https://github.com/seansawyer/erlang_iso8601.git``
* ``git clone https://github.com/erlsci/erlang_iso8601.git``
<a name="new-in-14-"></a>
## New in 1.4 [Back to contents](#contents)
* **Time interval support.** New `parse_interval/1`, `interval_bounds/1`, and
`format_interval/1` functions handle all four ISO 8601 interval forms
(`start/end`, `start/duration`, `duration/end`, and `duration`-only), accept
both the `/` and `--` separators, support signed durations, and resolve
abbreviated end points by inheriting the missing leading components from the
start (e.g. `2007-11-13/15`). See the interval examples in [Usage](#usage-).
* **Expanded year representation.** `parse/1` and `parse_exact/1` now accept a
leading `+` and more than four digits (e.g. `+10000-01-01`), staying
`calendar`-compatible. Negative (astronomical) years — for paleontological and
geological timescales — are available through the opt-in `parse_expanded/1` and
`format_expanded/1`, which return an `expanded_datetime()` with an `integer()`
year, leaving `parse/1`'s contract unchanged.
* **Parser input and date validation.** `parse/1`, `parse_exact/1`,
`parse_expanded/1`, and `parse_interval/1` accept nested byte iodata. Calendar
month/day fields are checked using proleptic Gregorian rules, including for
year zero and negative years. ISO week dates require a valid weekday and a
week that exists in that ISO week-year; week 53 occurs only when January 1 is
Thursday, or when it is Wednesday in a leap year.
* **Correctness fixes.** `apply_duration/2` now honors a negative sign (a
duration like `-P1Y` is subtracted rather than added); year arithmetic on a
leap day (e.g. adding a year to Feb 29) now clamps to a valid date instead of
producing an impossible one; and stray debug output during ordinal-date
parsing has been removed.
* **Fractional precision** is available via `parse_exact/1`, which preserves
fractional seconds from fractional hours, minutes, or seconds as a float. For
example, `parse_exact("2012-02-03T00.0001")` returns about `0.36` seconds.
`parse/1` keeps its legacy behavior: fractional hours and minutes round to the
nearest whole second, while an explicit fractional-seconds field is truncated.
Float values cannot represent every decimal exactly; formatting rounds to
microseconds and carries a rounded second into the next minute or date.
* **Calendar arithmetic** preserves the fractional part of a datetime's seconds
while applying rounded numeric time shifts and durations. Month and year
changes clamp to the last valid day of the target month; targets before year
zero raise `badarg`. At year zero, `add_time/4` rejects a nonzero rounded
combined shift; component values that cancel or round to zero retain the
existing zero-shift behavior.
<a name="usage-"></a>
## Usage [Back to contents](#contents)
Add it to your `rebar.config` deps:
```erlang
{iso8601, "~> 1.4"}
```
Format a timestamp or calendar datetime tuple. Formatting labels the supplied
tuple as UTC; it does not convert a local timezone. Timestamp conversion drops
microseconds, while float seconds in a datetime are rounded to microseconds:
```erlang
1> iso8601:format({0, 0, 0}).
<<"1970-01-01T00:00:00Z">>
2> iso8601:format({{2012, 2, 16}, {1, 6, 48}}).
<<"2012-02-16T01:06:48Z">>
```
Parse a date string or binary:
```erlang
3> iso8601:parse(<<"2012-02-16T01:06:48Z">>).
{{2012,2,16},{1,6,48}}
4> iso8601:parse("2012-02-16T01:06:48Z").
{{2012,2,16},{1,6,48}}
```
The date, interval and duration parsers accept byte iodata, so strings,
binaries, nested byte fragments and valid improper byte-iodata tails can be
combined directly. Non-byte or otherwise invalid iodata raises `badarg`.
```erlang
4a> iso8601:parse([<<"2012-">>, ["02", <<"-03">>]]).
{{2012,2,3},{0,0,0}}
```
Add 1 hour, 2 minutes and 3 seconds to a datetime tuple:
```erlang
5> Datetime = iso8601:parse(<<"2012-02-16T01:06:48Z">>).
{{2012,2,16},{1,6,48}}
6> iso8601:add_time(Datetime, 1, 2, 3).
{{2012,2,16},{2,8,51}}
```
Subtract 1 hour, 2 minutes and 3 seconds from a datetime tuple:
```erlang
5> Datetime = iso8601:parse(<<"2012-02-16T01:06:48Z">>).
{{2012,2,16},{1,6,48}}
6> iso8601:subtract_time(Datetime, 1, 2, 3).
{{2012,2,16},{0,4,45}}
```
Fractional times:
```erlang
7> iso8601:parse("20120203T040506.50").
{{2012,2,3},{4,5,6}}
8> iso8601:parse_exact("20120203T040506.50").
{{2012,2,3},{4,5,6.50}}
```
Parse durations from nested byte iodata. Duration fields have fixed order and
zero defaults; fractional seconds are accepted then truncated, while
fractional hours/minutes and week units are unsupported:
```erlang
9> iso8601:parse_duration("+P6Y3M1DT1H1M1.1S").
[{sign, "+"}, {years, 6}, {months, 3}, {days, 1}, {hours, 1}, {minutes, 1}, {seconds, 1}]
10> iso8601:parse_duration([<<"PT">>, ["6", <<"M">>]]).
[{sign, []}, {years, 0}, {months, 0}, {days, 0},{hours, 0}, {minutes, 6}, {seconds, 0}]
```
Calendar helpers add integer days, months and years. Month/year changes clamp
to the last valid day of the target month and preserve time/fractional seconds:
```erlang
iso8601:add_days({{2017, 1, 31}, {12, 0, 0.25}}, 1).
{{2017, 2, 1}, {12, 0, 0.25}}
iso8601:add_months({{2017, 1, 31}, {12, 0, 0.25}}, 1).
{{2017, 2, 28}, {12, 0, 0.25}}
iso8601:add_years({{2016, 2, 29}, {12, 0, 0.25}}, 1).
{{2017, 2, 28}, {12, 0, 0.25}}
```
Apply a duration with its sign and fields in years/months/days/time order. Each
calendar step clamps independently; intermediate results below year zero raise
`badarg`:
```erlang
iso8601:apply_duration({{2017, 1, 31}, {12, 0, 0.25}}, <<"P1M">>).
{{2017, 2, 28}, {12, 0, 0.25}}
```
Parse a time interval. `parse_interval/1` preserves which of the four forms was
given, tagging the result so it stays matchable:
```erlang
11> iso8601:parse_interval("2007-03-01T13:00:00Z/2008-05-11T15:30:00Z").
{interval,start_end,{{2007,3,1},{13,0,0}},{{2008,5,11},{15,30,0}}}
12> iso8601:parse_interval("2007-03-01T13:00:00Z/P1Y2M10DT2H30M").
{interval,start_duration,{{2007,3,1},{13,0,0}},
[{sign,[]},{years,1},{months,2},{days,10},
{hours,2},{minutes,30},{seconds,0}]}
13> iso8601:parse_interval("P1Y2M10DT2H30M/2008-05-11T15:30:00Z").
{interval,duration_end,
[{sign,[]},{years,1},{months,2},{days,10},
{hours,2},{minutes,30},{seconds,0}],
{{2008,5,11},{15,30,0}}}
14> iso8601:parse_interval("P1Y2M10DT2H30M").
{interval,duration,
[{sign,[]},{years,1},{months,2},{days,10},
{hours,2},{minutes,30},{seconds,0}]}
```
An abbreviated end inherits the missing leading components from the start, and
the `--` separator is accepted as well as `/`:
```erlang
15> iso8601:parse_interval("2007-11-13/15").
{interval,start_end,{{2007,11,13},{0,0,0}},{{2007,11,15},{0,0,0}}}
16> iso8601:parse_interval("2000--2002").
{interval,start_end,{{2000,1,1},{0,0,0}},{{2002,1,1},{0,0,0}}}
```
An explicit positive expanded year is retained when an abbreviated end inherits
its year or month. For an abbreviated end without its own timezone, the start's
timezone is also inherited after the end's local date and time have been
completed. A complete endpoint remains independent, and an explicit end
timezone takes precedence:
```erlang
17> iso8601:parse_interval("+2007-11-13/15").
{interval,start_end,{{2007,11,13},{0,0,0}},{{2007,11,15},{0,0,0}}}
18> iso8601:interval_bounds(iso8601:parse_interval("2007-12-14T13:30:00+02:00/15:30:00")).
{{{2007,12,14},{11,30,0}},{{2007,12,14},{13,30,0}}}
19> iso8601:interval_bounds(iso8601:parse_interval("2007-12-14T13:30:00+02:00/2007-12-15T15:30:00")).
{{{2007,12,14},{11,30,0}},{{2007,12,15},{15,30,0}}}
```
Resolve an interval to concrete `{Start, End}` datetimes with
`interval_bounds/1` (the `start/duration` and `duration/end` forms are computed
from the duration; a `duration`-only interval has no anchor and raises
`badarg`):
```erlang
20> iso8601:interval_bounds(iso8601:parse_interval("2007-03-01T13:00:00Z/P1Y2M10DT2H30M")).
{{{2007,3,1},{13,0,0}},{{2008,5,11},{15,30,0}}}
```
Format an interval back to a binary with `format_interval/1`:
```erlang
21> iso8601:format_interval(iso8601:parse_interval("2007-03-01T13:00:00Z/P1Y2M10DT2H30M")).
<<"2007-03-01T13:00:00Z/P1Y2M10DT2H30M">>
```
Parse an expanded-representation year. Positive expanded years (an explicit `+`
and any number of digits, e.g. years beyond `9999`) work with `parse/1`, which
stays `calendar`-compatible:
```erlang
22> iso8601:parse("+0002007-01-01").
{{2007,1,1},{0,0,0}}
23> iso8601:parse("+10000-01-01").
{{10000,1,1},{0,0,0}}
```
Negative (astronomical) years — where year `0` is 1 BCE, `-1` is 2 BCE, and so on
— use `parse_expanded/1` / `format_expanded/1`, which return an
`expanded_datetime()` with an `integer()` year. `parse/1` rejects negative years
so its `calendar`-compatible contract is preserved:
```erlang
24> iso8601:parse_expanded("-0001-01-01").
{{-1,1,1},{0,0,0.0}}
25> iso8601:format_expanded({{-44,3,15},{0,0,0}}).
<<"-0044-03-15T00:00:00Z">>
```
`parse_expanded/1` preserves fractional seconds (like `parse_exact/1`), so the
seconds field comes back as a float. Negative years are supported in UTC (`Z`) or
offset-free form; combining a negative year with a non-zero UTC offset or a
week-date raises `badarg`. At year zero or below, nonzero rounded time
adjustments raise `badarg`; an adjustment that rounds to zero remains allowed.
<a name="known-deficiencies-"></a>
## Known Deficiencies [Back to contents](#contents)
* Does not support repeating intervals (`Rn/...`).
* Expanded year representation is supported in extended (hyphenated) format only,
not in basic format (e.g. `+0002007-01-01`, not `+00020070101`).
See the [open issues](https://github.com/erlsci/iso8601/issues)
for more info.
## Donating
A donation account for supporting development on this project has been set up
on Liberapay here:
* [https://liberapay.com/erlsci-iso8601/donate](https://liberapay.com/erlsci-iso8601/donate)
You can learn more about Liberapay on its [Wikipedia entry][libera-wiki] or on the
service's ["About" page][libera-about].
[libera-wiki]: https://en.wikipedia.org/wiki/Liberapay
[libera-about]: https://liberapay.com/about/
<a name="license-"></a>
## License [Back to contents](#contents)
```
The MIT License (MIT)
Copyright © 2011-2014, Sean Sawyer
Copyright © 2012, Tristan Sloughter
Copyright © 2016-2026, Erlang-Aided Enrichment Center
```
[//]: --Named-Links--
[project]: https://github.com/erlsci/iso8601
[gh-actions-badge]: https://github.com/erlsci/iso8601/workflows/ci/badge.svg
[gh-actions]: https://github.com/erlsci/iso8601/actions
[coverage-badge]: https://img.shields.io/badge/coverage-100%25-brightgreen
[tag]: https://github.com/erlsci/iso8601/releases/latest
[tag-badge]: https://img.shields.io/github/tag/erlsci/iso8601.svg
[erl]: http://www.erlang.org/downloads
[erl-badge]: https://img.shields.io/badge/erlang-%E2%89%A520-blue.svg
[logo]: https://raw.githubusercontent.com/erlsci/iso8601/main/priv/images/logo.png
[logo-large]: https://raw.githubusercontent.com/erlsci/iso8601/main/priv/images/logo-large.png
[hex package]: https://hex.pm/packages/iso8601
[hex downloads]: https://img.shields.io/hexpm/dt/iso8601.svg