2020-05-05 21:58:44 +00:00
|
|
|
defmodule RDF.XSD.Time do
|
|
|
|
@moduledoc """
|
|
|
|
`RDF.XSD.Datatype` for XSD times.
|
2020-06-01 13:43:38 +00:00
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
- `tz`: this allows to specify a timezone which is not supported by Elixir's `Time` struct; note,
|
|
|
|
that it will also overwrite an eventually already present timezone in an input lexical
|
2020-05-05 21:58:44 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
@type valid_value :: Time.t() | {Time.t(), true}
|
|
|
|
|
|
|
|
use RDF.XSD.Datatype.Primitive,
|
|
|
|
name: "time",
|
2020-05-08 22:54:35 +00:00
|
|
|
id: RDF.Utils.Bootstrapping.xsd_iri("time")
|
2020-05-05 21:58:44 +00:00
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
alias RDF.XSD
|
|
|
|
|
2020-05-05 21:58:44 +00:00
|
|
|
# TODO: Are GMT/UTC actually allowed? Maybe because it is supported by Elixir's Datetime ...
|
|
|
|
@grammar ~r/\A(\d{2}:\d{2}:\d{2}(?:\.\d+)?)((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z/
|
|
|
|
@tz_number_grammar ~r/\A(?:([\+\-])(\d{2}):(\d{2}))\Z/
|
|
|
|
|
2020-05-21 23:01:08 +00:00
|
|
|
def_applicable_facet XSD.Facets.ExplicitTimezone
|
2020-05-21 15:54:38 +00:00
|
|
|
def_applicable_facet XSD.Facets.Pattern
|
|
|
|
|
2020-05-21 23:01:08 +00:00
|
|
|
@doc false
|
|
|
|
def explicit_timezone_conform?(:required, {_, true}, _), do: true
|
|
|
|
def explicit_timezone_conform?(:required, _, _), do: false
|
|
|
|
def explicit_timezone_conform?(:prohibited, {_, true}, _), do: false
|
|
|
|
def explicit_timezone_conform?(:prohibited, _, _), do: true
|
|
|
|
def explicit_timezone_conform?(:optional, _, _), do: true
|
|
|
|
|
2020-05-21 15:54:38 +00:00
|
|
|
@doc false
|
|
|
|
def pattern_conform?(pattern, _value, lexical) do
|
|
|
|
XSD.Facets.Pattern.conform?(pattern, lexical)
|
|
|
|
end
|
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
@impl XSD.Datatype
|
2020-05-05 21:58:44 +00:00
|
|
|
def lexical_mapping(lexical, opts) do
|
|
|
|
case Regex.run(@grammar, lexical) do
|
|
|
|
[_, time] ->
|
|
|
|
do_lexical_mapping(time, opts)
|
|
|
|
|
|
|
|
[_, time, tz] ->
|
|
|
|
do_lexical_mapping(
|
|
|
|
time,
|
|
|
|
opts |> Keyword.put_new(:tz, tz) |> Keyword.put_new(:lexical_present, true)
|
|
|
|
)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
@invalid_value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp do_lexical_mapping(value, opts) do
|
|
|
|
case Time.from_iso8601(value) do
|
|
|
|
{:ok, time} -> elixir_mapping(time, opts)
|
|
|
|
_ -> @invalid_value
|
|
|
|
end
|
|
|
|
|> case do
|
|
|
|
{{_, true} = value, _} -> value
|
|
|
|
value -> value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
@impl XSD.Datatype
|
2020-05-05 21:58:44 +00:00
|
|
|
@spec elixir_mapping(valid_value | any, Keyword.t()) ::
|
2020-05-21 13:42:02 +00:00
|
|
|
value | {value, XSD.Datatype.uncanonical_lexical()}
|
2020-05-05 21:58:44 +00:00
|
|
|
def elixir_mapping(value, opts)
|
|
|
|
|
|
|
|
def elixir_mapping(%Time{} = value, opts) do
|
|
|
|
if tz = Keyword.get(opts, :tz) do
|
|
|
|
case with_offset(value, tz) do
|
|
|
|
@invalid_value ->
|
|
|
|
@invalid_value
|
|
|
|
|
|
|
|
time ->
|
|
|
|
{{time, true}, unless(Keyword.get(opts, :lexical_present), do: Time.to_iso8601(value))}
|
|
|
|
end
|
|
|
|
else
|
|
|
|
value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def elixir_mapping(_, _), do: @invalid_value
|
|
|
|
|
|
|
|
defp with_offset(time, zone) when zone in ~W[Z UTC GMT], do: time
|
|
|
|
|
|
|
|
defp with_offset(time, offset) do
|
|
|
|
case Regex.run(@tz_number_grammar, offset) do
|
|
|
|
[_, "-", hour, minute] ->
|
|
|
|
{hour, minute} = {String.to_integer(hour), String.to_integer(minute)}
|
|
|
|
minute = time.minute + minute
|
|
|
|
{rem(time.hour + hour + div(minute, 60), 24), rem(minute, 60)}
|
|
|
|
|
|
|
|
[_, "+", hour, minute] ->
|
|
|
|
{hour, minute} = {String.to_integer(hour), String.to_integer(minute)}
|
|
|
|
|
|
|
|
if (minute = time.minute - minute) < 0 do
|
|
|
|
{rem(24 + time.hour - hour - 1, 24), minute + 60}
|
|
|
|
else
|
|
|
|
{rem(24 + time.hour - hour - div(minute, 60), 24), rem(minute, 60)}
|
|
|
|
end
|
|
|
|
|
|
|
|
nil ->
|
|
|
|
@invalid_value
|
|
|
|
end
|
|
|
|
|> case do
|
|
|
|
{hour, minute} -> %Time{time | hour: hour, minute: minute}
|
|
|
|
@invalid_value -> @invalid_value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
@impl XSD.Datatype
|
2020-05-05 21:58:44 +00:00
|
|
|
@spec canonical_mapping(valid_value) :: String.t()
|
|
|
|
def canonical_mapping(value)
|
|
|
|
def canonical_mapping(%Time{} = value), do: Time.to_iso8601(value)
|
|
|
|
def canonical_mapping({%Time{} = value, true}), do: canonical_mapping(value) <> "Z"
|
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
@impl XSD.Datatype
|
|
|
|
@spec init_valid_lexical(valid_value, XSD.Datatype.uncanonical_lexical(), Keyword.t()) ::
|
|
|
|
XSD.Datatype.uncanonical_lexical()
|
2020-05-05 21:58:44 +00:00
|
|
|
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
|
|
|
|
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
|
|
|
|
[_, time] -> time
|
|
|
|
[_, time, _] -> time
|
|
|
|
end <> tz
|
|
|
|
else
|
|
|
|
lexical
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
@impl XSD.Datatype
|
2020-05-05 21:58:44 +00:00
|
|
|
@spec init_invalid_lexical(any, Keyword.t()) :: String.t()
|
|
|
|
def init_invalid_lexical(value, opts)
|
|
|
|
|
|
|
|
def init_invalid_lexical({time, tz}, opts) do
|
|
|
|
if tz_opt = Keyword.get(opts, :tz) do
|
|
|
|
to_string(time) <> tz_opt
|
|
|
|
else
|
|
|
|
to_string(time) <> 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)
|
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
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_time()
|
|
|
|
|> new()
|
2020-05-05 21:58:44 +00:00
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
%DateTime{} ->
|
|
|
|
[_date, time_with_zone] =
|
|
|
|
literal
|
|
|
|
|> XSD.DateTime.canonical_lexical_with_zone()
|
|
|
|
|> String.split("T", parts: 2)
|
2020-05-05 21:58:44 +00:00
|
|
|
|
2020-05-21 13:42:02 +00:00
|
|
|
new(time_with_zone)
|
|
|
|
end
|
|
|
|
|
|
|
|
true ->
|
|
|
|
super(literal)
|
2020-05-05 21:58:44 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl RDF.Literal.Datatype
|
2020-05-25 21:10:50 +00:00
|
|
|
def do_equal_value_same_or_derived_datatypes?(left, right)
|
2020-06-29 08:37:42 +00:00
|
|
|
|
|
|
|
def do_equal_value_same_or_derived_datatypes?(%{value: %{}}, %{value: tz_tuple})
|
|
|
|
when is_tuple(tz_tuple),
|
|
|
|
do: nil
|
|
|
|
|
|
|
|
def do_equal_value_same_or_derived_datatypes?(%{value: tz_tuple}, %{value: %{}})
|
|
|
|
when is_tuple(tz_tuple),
|
|
|
|
do: nil
|
|
|
|
|
2020-05-25 21:10:50 +00:00
|
|
|
def do_equal_value_same_or_derived_datatypes?(left, right), do: super(left, right)
|
2020-05-05 21:58:44 +00:00
|
|
|
|
|
|
|
@doc """
|
|
|
|
Extracts the timezone string from a `RDF.XSD.Time` value.
|
|
|
|
"""
|
|
|
|
def tz(time_literal) do
|
|
|
|
if valid?(time_literal) do
|
|
|
|
time_literal
|
|
|
|
|> lexical()
|
2020-05-21 13:42:02 +00:00
|
|
|
|> XSD.Utils.DateTime.tz()
|
2020-05-05 21:58:44 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
Converts a time literal to a canonical string, preserving the zone information.
|
|
|
|
"""
|
|
|
|
@spec canonical_lexical_with_zone(RDF.Literal.t() | t()) :: String.t() | nil
|
|
|
|
def canonical_lexical_with_zone(%RDF.Literal{literal: xsd_time}),
|
2020-06-29 08:37:42 +00:00
|
|
|
do: canonical_lexical_with_zone(xsd_time)
|
|
|
|
|
2020-05-05 21:58:44 +00:00
|
|
|
def canonical_lexical_with_zone(%__MODULE__{} = xsd_time) do
|
|
|
|
case tz(xsd_time) do
|
|
|
|
nil ->
|
|
|
|
nil
|
|
|
|
|
|
|
|
zone when zone in ["Z", "", "+00:00"] ->
|
|
|
|
canonical_lexical(xsd_time)
|
|
|
|
|
|
|
|
zone ->
|
|
|
|
xsd_time
|
|
|
|
|> lexical()
|
|
|
|
|> String.replace_trailing(zone, "")
|
|
|
|
|> Time.from_iso8601!()
|
|
|
|
|> new()
|
|
|
|
|> canonical_lexical()
|
|
|
|
|> Kernel.<>(zone)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|