rdf-ex/lib/rdf/datatypes/date_time.ex
Marcel Otto 535e5b3713 Allow value equality comparisons between XSD dates and datetimes
This is required to make date-2 of the SPARQL 1.0 open-world test pass.
Strangely enough, ordering comparisons are nevertheless disallowed
according to date-3 of the same test suite. An implementation which
would allow order comparison is included but uncommented.
2018-11-04 17:27:45 +01:00

243 lines
6.5 KiB
Elixir

defmodule RDF.DateTime do
@moduledoc """
`RDF.Datatype` for XSD dateTime.
"""
use RDF.Datatype, id: RDF.Datatype.NS.XSD.dateTime
import RDF.Literal.Guards
@xsd_date RDF.Datatype.NS.XSD.date
@impl RDF.Datatype
def convert(value, opts)
# Special case for date and dateTime, for which 0 is not a valid year
def convert(%DateTime{year: 0} = value, opts), do: super(value, opts)
def convert(%DateTime{} = value, _), do: value
# Special case for date and dateTime, for which 0 is not a valid year
def convert(%NaiveDateTime{year: 0} = value, opts), do: super(value, opts)
def convert(%NaiveDateTime{} = value, _), do: value
def convert(value, opts) when is_binary(value) do
case DateTime.from_iso8601(value) do
{:ok, datetime, _} -> convert(datetime, opts)
{:error, :missing_offset} ->
case NaiveDateTime.from_iso8601(value) do
{:ok, datetime} -> convert(datetime, opts)
_ -> super(value, opts)
end
{:error, :invalid_format} ->
if String.ends_with?(value, "-00:00") do
String.replace_trailing(value, "-00:00", "Z")
|> convert(opts)
else
super(value, opts)
end
{:error, :invalid_time} ->
if String.contains?(value, "T24:00:00") do
with [day, tz] <- String.split(value, "T24:00:00", parts: 2),
{:ok, day} <- Date.from_iso8601(day)
do
"#{day |> Date.add(1) |> Date.to_string()}T00:00:00#{tz}"
|> convert(opts)
else
_ -> super(value, opts)
end
else
super(value, opts)
end
_ ->
super(value, opts)
end
end
def convert(value, opts), do: super(value, opts)
@impl RDF.Datatype
def canonical_lexical(value)
def canonical_lexical(%DateTime{} = value) do
DateTime.to_iso8601(value)
end
def canonical_lexical(%NaiveDateTime{} = value) do
NaiveDateTime.to_iso8601(value)
end
@impl RDF.Datatype
def cast(literal)
def cast(%RDF.Literal{datatype: datatype} = literal) do
cond do
not RDF.Literal.valid?(literal) ->
nil
is_xsd_datetime(datatype) ->
literal
is_xsd_date(datatype) ->
case literal.value do
{value, zone} ->
RDF.Date.canonical_lexical(value) <> "T00:00:00" <> zone
value ->
RDF.Date.canonical_lexical(value) <> "T00:00:00"
end
|> new()
is_xsd_string(datatype) ->
literal.value
|> new()
|> validate_cast()
true ->
nil
end
end
def cast(_), do: nil
@doc """
Builds a `RDF.DateTime` literal for current moment in time.
"""
def now() do
new(DateTime.utc_now())
end
@doc """
Extracts the timezone string from a `RDF.DateTime` literal.
"""
def tz(literal)
def tz(%Literal{value: %NaiveDateTime{}}), do: ""
def tz(date_time_literal) do
if valid?(date_time_literal) do
date_time_literal
|> lexical()
|> RDF.DateTimeUtils.tz()
end
end
@doc """
Converts a datetime literal to a canonical string, preserving the zone information.
"""
def canonical_lexical_with_zone(%Literal{datatype: datatype} = literal)
when is_xsd_datetime(datatype) do
case tz(literal) do
nil ->
nil
zone when zone in ["Z", "", "+00:00", "-00:00"] ->
canonical_lexical(literal.value)
zone ->
literal
|> lexical()
|> String.replace_trailing(zone, "Z")
|> DateTime.from_iso8601()
|> elem(1)
|> canonical_lexical()
|> String.replace_trailing("Z", zone)
end
end
@impl RDF.Datatype
def equal_value?(literal1, literal2)
def equal_value?(%Literal{datatype: @id, value: %type{} = value1},
%Literal{datatype: @id, value: %type{} = value2})
do
type.compare(value1, value2) == :eq
end
def equal_value?(%Literal{datatype: @id, value: nil, uncanonical_lexical: lexical1},
%Literal{datatype: @id, value: nil, uncanonical_lexical: lexical2}) do
lexical1 == lexical2
end
def equal_value?(%Literal{datatype: @id} = literal1, %Literal{datatype: @id} = literal2) do
case compare(literal1, literal2) do
:lt -> false
:gt -> false
:eq -> true # This actually can't/shouldn't happen.
_ -> nil
end
end
def equal_value?(%Literal{datatype: @id}, %Literal{datatype: @xsd_date}), do: false
def equal_value?(%Literal{datatype: @xsd_date}, %Literal{datatype: @id}), do: false
def equal_value?(%RDF.Literal{} = left, right) when not is_nil(right) do
unless RDF.Term.term?(right) do
equal_value?(left, RDF.Term.coerce(right))
end
end
def equal_value?(_, _), do: nil
@impl RDF.Datatype
def compare(left, right)
def compare(%Literal{datatype: @id, value: %type{} = value1},
%Literal{datatype: @id, value: %type{} = value2}) do
type.compare(value1, 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 compare(%Literal{datatype: @id} = literal1,
# %Literal{datatype: @xsd_date} = literal2) do
# RDF.Date.compare(literal1, literal2)
# end
#
# def compare(%Literal{datatype: @xsd_date} = literal1,
# %Literal{datatype: @id} = literal2) do
# RDF.Date.compare(literal1, literal2)
# end
def compare(%Literal{datatype: @id, value: %DateTime{}} = literal1,
%Literal{datatype: @id, value: %NaiveDateTime{} = value2}) do
cond do
compare(literal1, new(to_datetime(value2, "+"))) == :lt -> :lt
compare(literal1, new(to_datetime(value2, "-"))) == :gt -> :gt
true -> :indeterminate
end
end
def compare(%Literal{datatype: @id, value: %NaiveDateTime{} = value1},
%Literal{datatype: @id, value: %DateTime{}} = literal2) do
cond do
compare(new(to_datetime(value1, "-")), literal2) == :lt -> :lt
compare(new(to_datetime(value1, "+")), literal2) == :gt -> :gt
true -> :indeterminate
end
end
def compare(_, _), do: nil
defp to_datetime(naive_datetime, offset) do
(NaiveDateTime.to_iso8601(naive_datetime) <> offset <> "14:00")
|> DateTime.from_iso8601()
|> elem(1)
end
end