Packages

Extended Date Time Format (EDTF) / ISO 8601-2 parsing and English language rendering

Current section

Files

Jump to
edtf lib edtf.ex
Raw

lib/edtf.ex

defmodule EDTF do
@moduledoc """
Parse, validate, and humanize EDTF date strings
"""
alias EDTF.{Aggregate, Date, Interval, Level, Validate}
@doc """
Parse an EDTF date string
Well-formed strings that don't denote a real calendar date (e.g.
`"1999-02-30"`) return `{:error, :invalid_date}`. Pass `validate: false` to
skip calendar validation and accept the assembled struct anyway — useful when
cataloging an item whose stamped date is known to be impossible.
## Options
* `:validate` — when `false`, skip calendar validation. Defaults to `true`.
Example:
```elixir
iex> parse("1999-06-10")
{:ok, %EDTF.Date{level: 0, type: :date, values: [1999, 5, 10]}}
iex> parse("bad date!")
{:error, :invalid_format}
iex> parse("1999-02-30")
{:error, :invalid_date}
iex> parse("1999-02-30", validate: false)
{:ok, %EDTF.Date{level: 0, type: :date, values: [1999, 1, 30]}}
```
"""
def parse(edtf, opts \\ []) do
validate? = Keyword.get(opts, :validate, true)
case EDTF.Parser.parse(edtf) do
{:ok, [result], _, _, _, _} ->
assembled = assemble(result) |> Level.add_level()
cond do
not validate? -> {:ok, assembled}
Validate.valid?(assembled) -> {:ok, assembled}
true -> {:error, :invalid_date}
end
{:error, _, _, _, _, _} ->
{:error, :invalid_format}
end
end
defp assemble({:date, _} = result), do: Date.assemble(result)
defp assemble({:year, _} = result), do: Date.assemble(result)
defp assemble({:decade, _} = result), do: Date.assemble(result)
defp assemble({:century, _} = result), do: Date.assemble(result)
defp assemble({:interval, _} = result), do: Interval.assemble(result)
defp assemble({:set, _} = result), do: Aggregate.assemble(result)
defp assemble({:list, _} = result), do: Aggregate.assemble(result)
@doc """
Validate an EDTF date string
Example:
```elixir
iex> validate("1999-06-10")
{:ok, "1999-06-10"}
iex> validate("bad date!")
{:error, :invalid_format}
iex> validate("1999-02-30")
{:error, :invalid_date}
```
"""
def validate(edtf) do
case parse(edtf) do
{:ok, _} -> {:ok, edtf}
error -> error
end
end
@doc """
Humanize an EDTF date string
Accepts the same options as `parse/2`. Passing `validate: false` lets you
render a well-formed but calendar-invalid date (e.g. an impossible stamped
date) instead of returning `{:error, :invalid_date}`.
## Options
* `:validate` — when `false`, skip calendar validation. Defaults to `true`.
Example:
```elixir
iex> humanize("1999-06-10")
"June 10, 1999"
iex> humanize("bad date!")
{:error, :invalid_format}
iex> humanize("1999-02-30")
{:error, :invalid_date}
iex> humanize("1999-02-30", validate: false)
"February 30, 1999"
```
"""
def humanize(edtf, opts \\ []) do
case edtf |> parse(opts) |> EDTF.Humanize.humanize() do
:original -> edtf
other -> other
end
end
@doc """
Convert an EDTF date string (or a previously parsed `EDTF.Date`,
`EDTF.Interval`, or `EDTF.Aggregate` struct) into a `{start_date, end_date}`
tuple of `Date.t()` values.
Explicitly open inputs (`/..`, `../`, aggregate `..`-continuations) produce
`:unbounded` on that side. Inputs with an unknown bound (`1985/`, `/1985`)
produce `:unknown`. Qualifiers (`~`, `?`, `%`) are ignored; the range uses
the nominal date. Unspecified-digit suffixes (e.g. `19XX`) expand to their
full span. See `EDTF.DateRange` for the full semantics.
Returns `{:error, :invalid_format}` when the string is malformed,
`{:error, :invalid_date}` when it is well-formed but not a real calendar date
(e.g. `1999-02-30`), `{:error, :out_of_range}` when `Date.new/3` itself rejects
a value, and `{:error, :unsupported}` for shapes the converter declines (e.g.
fully-unknown year `XXXX` or non-suffix unspecified digits like `X9X2`).
Example:
```elixir
iex> to_date_range("1999-06-10")
{:ok, {~D[1999-06-10], ~D[1999-06-10]}}
iex> to_date_range("1985/..")
{:ok, {~D[1985-01-01], :unbounded}}
iex> to_date_range("19XX")
{:ok, {~D[1900-01-01], ~D[1999-12-31]}}
iex> to_date_range("[1667, 1668, 1670..1672]")
{:ok, {~D[1667-01-01], ~D[1672-12-31]}}
iex> to_date_range("bad date!")
{:error, :invalid_format}
```
"""
def to_date_range(edtf) when is_binary(edtf) do
edtf |> parse() |> EDTF.DateRange.to_date_range()
end
def to_date_range(edtf), do: EDTF.DateRange.to_date_range(edtf)
end