230 lines
6.3 KiB
Elixir
230 lines
6.3 KiB
Elixir
defmodule RDF.XSD.Date do
|
|
@moduledoc """
|
|
`RDF.XSD.Datatype` for XSD date.
|
|
|
|
Options:
|
|
|
|
- `tz`: this allows to specify a timezone which is not supported by Elixir's `Date` struct; note,
|
|
that it will also overwrite an eventually already present timezone in an input lexical
|
|
"""
|
|
|
|
@type valid_value :: Date.t() | {Date.t(), String.t()}
|
|
|
|
use RDF.XSD.Datatype.Primitive,
|
|
name: "date",
|
|
id: RDF.Utils.Bootstrapping.xsd_iri("date")
|
|
|
|
alias RDF.XSD
|
|
|
|
def_applicable_facet XSD.Facets.ExplicitTimezone
|
|
def_applicable_facet XSD.Facets.Pattern
|
|
|
|
@doc false
|
|
def explicit_timezone_conform?(:required, {_, _}, _), do: true
|
|
def explicit_timezone_conform?(:required, _, _), do: false
|
|
def explicit_timezone_conform?(:prohibited, {_, _}, _), do: false
|
|
def explicit_timezone_conform?(:prohibited, _, _), do: true
|
|
def explicit_timezone_conform?(:optional, _, _), do: true
|
|
|
|
@doc false
|
|
def pattern_conform?(pattern, _value, lexical) do
|
|
XSD.Facets.Pattern.conform?(pattern, lexical)
|
|
end
|
|
|
|
# TODO: Are GMT/UTC actually allowed? Maybe because it is supported by Elixir's Datetime ...
|
|
@grammar ~r/\A(-?\d{4}-\d{2}-\d{2})((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z/
|
|
@tz_grammar ~r/\A((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)\Z/
|
|
|
|
@impl XSD.Datatype
|
|
def lexical_mapping(lexical, opts) do
|
|
case Regex.run(@grammar, lexical) do
|
|
[_, date] -> do_lexical_mapping(date, opts)
|
|
[_, date, tz] -> do_lexical_mapping(date, Keyword.put_new(opts, :tz, tz))
|
|
_ -> @invalid_value
|
|
end
|
|
end
|
|
|
|
defp do_lexical_mapping(value, opts) do
|
|
case Date.from_iso8601(value) do
|
|
{:ok, date} -> elixir_mapping(date, opts)
|
|
_ -> @invalid_value
|
|
end
|
|
|> case do
|
|
{{_, _} = value, _} -> value
|
|
value -> value
|
|
end
|
|
end
|
|
|
|
@impl XSD.Datatype
|
|
@spec elixir_mapping(Date.t() | any, Keyword.t()) ::
|
|
value | {value, XSD.Datatype.uncanonical_lexical()}
|
|
def elixir_mapping(value, opts)
|
|
|
|
# Special case for date and dateTime, for which 0 is not a valid year
|
|
def elixir_mapping(%Date{year: 0}, _), do: @invalid_value
|
|
|
|
def elixir_mapping(%Date{} = value, opts) do
|
|
if tz = Keyword.get(opts, :tz) do
|
|
if valid_timezone?(tz) do
|
|
{{value, timezone_mapping(tz)}, nil}
|
|
else
|
|
@invalid_value
|
|
end
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
def elixir_mapping(_, _), do: @invalid_value
|
|
|
|
defp valid_timezone?(string), do: Regex.match?(@tz_grammar, string)
|
|
|
|
defp timezone_mapping("+00:00"), do: "Z"
|
|
defp timezone_mapping(tz), do: tz
|
|
|
|
@impl XSD.Datatype
|
|
@spec canonical_mapping(valid_value) :: String.t()
|
|
def canonical_mapping(value)
|
|
def canonical_mapping(%Date{} = value), do: Date.to_iso8601(value)
|
|
def canonical_mapping({%Date{} = value, "+00:00"}), do: canonical_mapping(value) <> "Z"
|
|
def canonical_mapping({%Date{} = value, zone}), do: canonical_mapping(value) <> zone
|
|
|
|
@impl XSD.Datatype
|
|
@spec init_valid_lexical(valid_value, XSD.Datatype.uncanonical_lexical(), Keyword.t()) ::
|
|
XSD.Datatype.uncanonical_lexical()
|
|
def init_valid_lexical(value, lexical, opts)
|
|
|
|
def init_valid_lexical({value, _}, nil, opts) do
|
|
if tz = Keyword.get(opts, :tz) do
|
|
canonical_mapping(value) <> tz
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def init_valid_lexical(_, nil, _), do: nil
|
|
|
|
def init_valid_lexical(_, lexical, opts) do
|
|
if tz = Keyword.get(opts, :tz) do
|
|
# When using the :tz option, we'll have to strip off the original timezone
|
|
case Regex.run(@grammar, lexical) do
|
|
[_, date] -> date
|
|
[_, date, _] -> date
|
|
end <> tz
|
|
else
|
|
lexical
|
|
end
|
|
end
|
|
|
|
@impl XSD.Datatype
|
|
@spec init_invalid_lexical(any, Keyword.t()) :: String.t()
|
|
def init_invalid_lexical(value, opts)
|
|
|
|
def init_invalid_lexical({date, tz}, opts) do
|
|
if tz_opt = Keyword.get(opts, :tz) do
|
|
to_string(date) <> tz_opt
|
|
else
|
|
to_string(date) <> to_string(tz)
|
|
end
|
|
end
|
|
|
|
def init_invalid_lexical(value, _) when is_binary(value), do: value
|
|
|
|
def init_invalid_lexical(value, opts) do
|
|
if tz = Keyword.get(opts, :tz) do
|
|
to_string(value) <> tz
|
|
else
|
|
to_string(value)
|
|
end
|
|
end
|
|
|
|
@impl RDF.Literal.Datatype
|
|
def do_cast(value)
|
|
|
|
def do_cast(%XSD.String{} = xsd_string), do: new(xsd_string.value)
|
|
|
|
def do_cast(literal) do
|
|
cond do
|
|
XSD.DateTime.datatype?(literal) ->
|
|
case literal.value do
|
|
%NaiveDateTime{} = datetime ->
|
|
datetime
|
|
|> NaiveDateTime.to_date()
|
|
|> new()
|
|
|
|
%DateTime{} = datetime ->
|
|
datetime
|
|
|> DateTime.to_date()
|
|
|> new(tz: XSD.DateTime.tz(literal))
|
|
end
|
|
|
|
true ->
|
|
super(literal)
|
|
end
|
|
end
|
|
|
|
@impl RDF.Literal.Datatype
|
|
def do_equal_value_same_or_derived_datatypes?(left, right) do
|
|
XSD.DateTime.equal_value?(
|
|
comparison_normalization(left.value),
|
|
comparison_normalization(right.value)
|
|
)
|
|
end
|
|
|
|
@impl RDF.Literal.Datatype
|
|
def do_equal_value_different_datatypes?(left, right) do
|
|
if XSD.DateTime.datatype?(left) or XSD.DateTime.datatype?(right) do
|
|
false
|
|
else
|
|
super(left, right)
|
|
end
|
|
end
|
|
|
|
@impl RDF.Literal.Datatype
|
|
def do_compare(%{value: value1}, %{value: value2}) do
|
|
XSD.DateTime.compare(
|
|
comparison_normalization(value1),
|
|
comparison_normalization(value2)
|
|
)
|
|
end
|
|
|
|
# It seems quite strange that open-world test date-2 from the SPARQL 1.0 test suite
|
|
# allows for equality comparisons between dates and datetimes, but disallows
|
|
# ordering comparisons in the date-3 test. The following implementation would allow
|
|
# an ordering comparisons between date and datetimes.
|
|
#
|
|
# def do_compare(
|
|
# %__MODULE__{value: date_value},
|
|
# %XSD.DateTime{} = datetime_literal
|
|
# ) do
|
|
# XSD.DateTime.compare(
|
|
# comparison_normalization(date_value).literal,
|
|
# datetime_literal
|
|
# )
|
|
# end
|
|
#
|
|
# def do_compare(
|
|
# %XSD.DateTime{} = datetime_literal,
|
|
# %__MODULE__{value: date_value}
|
|
# ) do
|
|
# XSD.DateTime.do_compare(
|
|
# datetime_literal,
|
|
# comparison_normalization(date_value).literal
|
|
# )
|
|
# end
|
|
|
|
def do_compare(_, _), do: nil
|
|
|
|
defp comparison_normalization({date, tz}) do
|
|
(Date.to_iso8601(date) <> "T00:00:00" <> tz)
|
|
|> XSD.DateTime.new()
|
|
end
|
|
|
|
defp comparison_normalization(%Date{} = date) do
|
|
(Date.to_iso8601(date) <> "T00:00:00")
|
|
|> XSD.DateTime.new()
|
|
end
|
|
|
|
defp comparison_normalization(_), do: nil
|
|
end
|