Proper handling of comparisons between date literals

This commit is contained in:
Marcel Otto 2018-11-04 15:54:49 +01:00
parent 10dc249531
commit b7291c7d93
7 changed files with 228 additions and 55 deletions

View file

@ -22,7 +22,8 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
### Fixed ### Fixed
- `RDF.Date.equal_value?/2` did not handle timezones correctly - `RDF.DateTime.equal_value?/2` and `RDF.Date.equal_value?/2` did not handle
timezones correctly
- `-00:00` is a valid timezone offset on `RDF.DateTime` - `-00:00` is a valid timezone offset on `RDF.DateTime`

View file

@ -83,6 +83,8 @@ defmodule RDF.Datatype do
Returns `:gt` if first literal is greater than the second in terms of their datatype Returns `:gt` if first literal is greater than the second in terms of their datatype
and `:lt` for vice versa. If the two literals are equal `:eq` is returned. and `:lt` for vice versa. If the two literals are equal `:eq` is returned.
For datatypes with only partial ordering `:indeterminate` is returned when the
order of the given literals is not defined.
Returns `nil` when the given arguments are not comparable datatypes or if one Returns `nil` when the given arguments are not comparable datatypes or if one
them is invalid. them is invalid.
@ -90,7 +92,7 @@ defmodule RDF.Datatype do
The default implementation of the `_using__` macro compares the values of the The default implementation of the `_using__` macro compares the values of the
`canonical/1` forms of the given literals of this datatype. `canonical/1` forms of the given literals of this datatype.
""" """
@callback compare(literal1 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: :lt | :gt | :eq | nil @callback compare(literal1 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: :lt | :gt | :eq | :indeterminate | nil
@lang_string RDF.iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString") @lang_string RDF.iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")

View file

@ -99,21 +99,56 @@ defmodule RDF.Date do
@impl RDF.Datatype @impl RDF.Datatype
def equal_value?(%Literal{datatype: @id, value: value1} = left, def equal_value?(literal1, literal2)
%Literal{datatype: @id, value: value2} = right)
when is_nil(value1) or is_nil(value2),
do: left.uncanonical_lexical == right.uncanonical_lexical
def equal_value?(%Literal{datatype: @id} = left, %Literal{datatype: @id} = right), def equal_value?(%Literal{datatype: @id, value: nil, uncanonical_lexical: lexical1},
do: equal_normalization(left).value == equal_normalization(right).value %Literal{datatype: @id, value: nil, uncanonical_lexical: lexical2}) do
lexical1 == lexical2
end
def equal_value?(%Literal{datatype: @id, value: value1},
%Literal{datatype: @id, value: value2})
when is_nil(value1) or is_nil(value2), do: false
def equal_value?(%Literal{datatype: @id, value: value1},
%Literal{datatype: @id, value: value2}) do
RDF.DateTime.equal_value?(
comparison_normalization(value1),
comparison_normalization(value2)
)
end
def equal_value?(_, _), do: nil def equal_value?(_, _), do: nil
defp equal_normalization(%{value: {value, "-00:00"}}),
do: new(value, %{tz: "Z"}) @impl RDF.Datatype
defp equal_normalization(%{value: value}) when not is_tuple(value), def compare(left, right)
do: new(value, %{tz: "Z"})
defp equal_normalization(literal), def compare(%Literal{datatype: @id, value: value1},
do: literal %Literal{datatype: @id, value: value2})
when is_nil(value1) or is_nil(value2), do: nil
def compare(%Literal{datatype: @id, value: value1},
%Literal{datatype: @id, value: value2}) do
RDF.DateTime.compare(
comparison_normalization(value1),
comparison_normalization(value2)
)
end
def compare(_, _), do: nil
defp comparison_normalization({date, tz}) do
(Date.to_iso8601(date) <> "T00:00:00" <> tz)
|> RDF.DateTime.new()
end
defp comparison_normalization(%Date{} = date) do
(Date.to_iso8601(date) <> "T00:00:00")
|> RDF.DateTime.new()
end
defp comparison_normalization(_), do: nil
end end

View file

@ -152,4 +152,72 @@ defmodule RDF.DateTime do
end end
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?(%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
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 end

View file

@ -293,6 +293,8 @@ defmodule RDF.Literal do
Returns `:gt` if first literal is greater than the second in terms of their datatype Returns `:gt` if first literal is greater than the second in terms of their datatype
and `:lt` for vice versa. If the two literals are equal `:eq` is returned. and `:lt` for vice versa. If the two literals are equal `:eq` is returned.
For datatypes with only partial ordering `:indeterminate` is returned when the
order of the given literals is not defined.
Returns `nil` when the given arguments are not comparable datatypes. Returns `nil` when the given arguments are not comparable datatypes.

View file

@ -231,8 +231,11 @@ defmodule RDF.EqualityTest do
{RDF.date_time("2002-04-02T12:00:00-01:00"), RDF.date_time("2002-04-02T17:00:00+04:00")}, {RDF.date_time("2002-04-02T12:00:00-01:00"), RDF.date_time("2002-04-02T17:00:00+04:00")},
{RDF.date_time("2002-04-02T23:00:00-04:00"), RDF.date_time("2002-04-03T02:00:00-01:00")}, {RDF.date_time("2002-04-02T23:00:00-04:00"), RDF.date_time("2002-04-03T02:00:00-01:00")},
{RDF.date_time("1999-12-31T24:00:00"), RDF.date_time("2000-01-01T00:00:00")}, {RDF.date_time("1999-12-31T24:00:00"), RDF.date_time("2000-01-01T00:00:00")},
# TODO: Assume that the dynamic context provides an implicit timezone value of -05:00
# {RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T23:00:00+06:00")}, {RDF.date_time("2002-04-02T23:00:00Z"), RDF.date_time("2002-04-02T23:00:00+00:00")},
{RDF.date_time("2002-04-02T23:00:00Z"), RDF.date_time("2002-04-02T23:00:00-00:00")},
{RDF.date_time("2002-04-02T23:00:00+00:00"), RDF.date_time("2002-04-02T23:00:00-00:00")},
# invalid literals # invalid literals
{RDF.date_time("foo"), RDF.date_time("foo")}, {RDF.date_time("foo"), RDF.date_time("foo")},
] ]
@ -244,12 +247,19 @@ defmodule RDF.EqualityTest do
@value_equal_datetimes_by_coercion [ @value_equal_datetimes_by_coercion [
{RDF.date_time("2002-04-02T12:00:00-01:00"), elem(DateTime.from_iso8601("2002-04-02T12:00:00-01:00"), 1)}, {RDF.date_time("2002-04-02T12:00:00-01:00"), elem(DateTime.from_iso8601("2002-04-02T12:00:00-01:00"), 1)},
{RDF.date_time("2002-04-02T12:00:00"), ~N"2002-04-02T12:00:00"}, {RDF.date_time("2002-04-02T12:00:00"), ~N"2002-04-02T12:00:00"},
{RDF.date_time("2002-04-02T23:00:00Z"), elem(DateTime.from_iso8601("2002-04-02T23:00:00+00:00"), 1)},
{RDF.date_time("2002-04-02T23:00:00+00:00"), elem(DateTime.from_iso8601("2002-04-02T23:00:00Z"), 1)},
{RDF.date_time("2002-04-02T23:00:00-00:00"), elem(DateTime.from_iso8601("2002-04-02T23:00:00Z"), 1)},
{RDF.date_time("2002-04-02T23:00:00-00:00"), elem(DateTime.from_iso8601("2002-04-02T23:00:00+00:00"), 1)},
] ]
@value_unequal_datetimes_by_coercion [ @value_unequal_datetimes_by_coercion [
{RDF.date_time("2002-04-02T12:00:00-01:00"), elem(DateTime.from_iso8601("2002-04-02T12:00:00+00:00"), 1)}, {RDF.date_time("2002-04-02T12:00:00-01:00"), elem(DateTime.from_iso8601("2002-04-02T12:00:00+00:00"), 1)},
] ]
@incomparable_datetimes [ @incomparable_datetimes [
{RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T12:00:00Z")},
{RDF.string("2002-04-02T12:00:00-01:00"), RDF.date_time("2002-04-02T12:00:00-01:00")}, {RDF.string("2002-04-02T12:00:00-01:00"), RDF.date_time("2002-04-02T12:00:00-01:00")},
# These are incomparable because of indeterminacy due to missing timezone
{RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T23:00:00+00:00")},
] ]
test "term equality", do: assert_term_equal @term_equal_datetimes test "term equality", do: assert_term_equal @term_equal_datetimes
@ -277,24 +287,36 @@ defmodule RDF.EqualityTest do
{RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02+00:00")}, {RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02+00:00")},
{RDF.date("2002-04-02Z"), RDF.date("2002-04-02+00:00")}, {RDF.date("2002-04-02Z"), RDF.date("2002-04-02+00:00")},
{RDF.date("2002-04-02Z"), RDF.date("2002-04-02-00:00")}, {RDF.date("2002-04-02Z"), RDF.date("2002-04-02-00:00")},
{RDF.date("2002-04-02Z"), RDF.date("2002-04-02")},
{RDF.date("2002-04-02+00:00"), RDF.date("2002-04-02")},
{RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02")},
] ]
@value_unequal_dates [ @value_unequal_dates [
{RDF.date("2002-04-03Z"), RDF.date("2002-04-02")},
{RDF.date("2002-04-03"), RDF.date("2002-04-02Z")},
{RDF.date("2002-04-03+00:00"), RDF.date("2002-04-02")},
{RDF.date("2002-04-03-00:00"), RDF.date("2002-04-02")},
# invalid literals
{RDF.date("2002.04.02"), RDF.date("2002-04-02")},
] ]
@value_equal_dates_by_coercion [ @value_equal_dates_by_coercion [
{RDF.date("2002-04-02"), Date.from_iso8601!("2002-04-02")}, {RDF.date("2002-04-02"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-02Z"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-02+00:00"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-02-00:00"), Date.from_iso8601!("2002-04-02")},
] ]
@value_unequal_dates_by_coercion [ @value_unequal_dates_by_coercion [
{RDF.date("2002-04-02"), Date.from_iso8601!("2002-04-03")}, {RDF.date("2002-04-02"), Date.from_iso8601!("2002-04-03")},
{RDF.date("2002-04-02+01:00"), Date.from_iso8601!("2002-04-02")}, {RDF.date("2002-04-03+01:00"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-03Z"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-03+00:00"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-03-00:00"), Date.from_iso8601!("2002-04-02")},
] ]
@incomparable_dates [ @incomparable_dates [
{RDF.date("2002-04-02"), RDF.string("2002-04-02")}, {RDF.date("2002-04-02"), RDF.string("2002-04-02")},
# These are incomparable because of indeterminacy due to missing timezone
{RDF.date("2002-04-02Z"), RDF.date("2002-04-02")},
{RDF.date("2002-04-02"), RDF.date("2002-04-02Z")},
{RDF.date("2002-04-02+00:00"), RDF.date("2002-04-02")},
{RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02")},
{RDF.date("2002-04-02+01:00"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-02Z"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-02+00:00"), Date.from_iso8601!("2002-04-02")},
{RDF.date("2002-04-02-00:00"), Date.from_iso8601!("2002-04-02")},
] ]
test "term equality", do: assert_term_equal @term_equal_dates test "term equality", do: assert_term_equal @term_equal_dates

View file

@ -85,6 +85,11 @@ defmodule RDF.LiteralComparisonTest do
test "when unequal" do test "when unequal" do
assert_order {RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T17:00:00")} assert_order {RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T17:00:00")}
assert_order {RDF.date_time("2002-04-02T12:00:00+01:00"), RDF.date_time("2002-04-02T12:00:00+00:00")} assert_order {RDF.date_time("2002-04-02T12:00:00+01:00"), RDF.date_time("2002-04-02T12:00:00+00:00")}
assert_order {RDF.date_time("2000-01-15T12:00:00"), RDF.date_time("2000-01-16T12:00:00Z")}
end
test "when unequal due to missing time zone" do
assert_order {RDF.date_time("2000-01-15T00:00:00"), RDF.date_time("2000-02-15T00:00:00")}
end end
test "when equal" do test "when equal" do
@ -96,24 +101,34 @@ defmodule RDF.LiteralComparisonTest do
# TODO: Assume that the dynamic context provides an implicit timezone value of -05:00 # TODO: Assume that the dynamic context provides an implicit timezone value of -05:00
# assert_equal {RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T23:00:00+06:00")} # assert_equal {RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T23:00:00+06:00")}
end end
test "when indeterminate" do
assert_indeterminate {RDF.date_time("2000-01-01T12:00:00"), RDF.date_time("1999-12-31T23:00:00Z")}
assert_indeterminate {RDF.date_time("2000-01-16T12:00:00"), RDF.date_time("2000-01-16T12:00:00Z")}
assert_indeterminate {RDF.date_time("2000-01-16T00:00:00"), RDF.date_time("2000-01-16T12:00:00Z")}
end
end end
describe "RDF.Date comparisons" do describe "RDF.Date comparisons" do
test "when unequal" do test "when unequal" do
assert_order {RDF.date("2002-04-02"), RDF.date("2002-04-03")} assert_order {RDF.date("2002-04-02"), RDF.date("2002-04-03")}
assert_order {RDF.date("2002-04-02+01:00"), RDF.date("2002-04-03+00:00")} assert_order {RDF.date("2002-04-02+01:00"), RDF.date("2002-04-03+00:00")}
assert_order {RDF.date("2002-04-02"), RDF.date("2002-04-03Z")}
end end
test "when equal" do test "when equal" do
assert_equal {RDF.date("2002-04-02-01:00"), RDF.date("2002-04-02-01:00")} assert_equal {RDF.date("2002-04-02-01:00"), RDF.date("2002-04-02-01:00")}
assert_equal {RDF.date("2002-04-02"), RDF.date("2002-04-02")} assert_equal {RDF.date("2002-04-02"), RDF.date("2002-04-02")}
# TODO: # TODO:
# assert_equal {RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02+00:00")} assert_equal {RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02+00:00")}
# assert_equal {RDF.date("2002-04-02Z"), RDF.date("2002-04-02+00:00")} assert_equal {RDF.date("2002-04-02Z"), RDF.date("2002-04-02+00:00")}
# assert_equal {RDF.date("2002-04-02Z"), RDF.date("2002-04-02-00:00")} assert_equal {RDF.date("2002-04-02Z"), RDF.date("2002-04-02-00:00")}
# assert_equal {RDF.date("2002-04-02Z"), RDF.date("2002-04-02")} end
# assert_equal {RDF.date("2002-04-02+00:00"), RDF.date("2002-04-02")}
# assert_equal {RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02")} test "when indeterminate" do
assert_indeterminate {RDF.date("2002-04-02Z"), RDF.date("2002-04-02")}
assert_indeterminate {RDF.date("2002-04-02+00:00"), RDF.date("2002-04-02")}
assert_indeterminate {RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02")}
end end
end end
@ -127,6 +142,12 @@ defmodule RDF.LiteralComparisonTest do
assert_equal {RDF.time("12:00:00+01:00"), RDF.time("12:00:00+01:00")} assert_equal {RDF.time("12:00:00+01:00"), RDF.time("12:00:00+01:00")}
assert_equal {RDF.time("12:00:00"), RDF.time("12:00:00")} assert_equal {RDF.time("12:00:00"), RDF.time("12:00:00")}
end end
test "when indeterminate" do
assert_indeterminate {RDF.date("2002-04-02Z"), RDF.date("2002-04-02")}
assert_indeterminate {RDF.date("2002-04-02+00:00"), RDF.date("2002-04-02")}
assert_indeterminate {RDF.date("2002-04-02-00:00"), RDF.date("2002-04-02")}
end
end end
describe "comparisons on RDF.Literals with unsupported types" do describe "comparisons on RDF.Literals with unsupported types" do
@ -141,7 +162,8 @@ defmodule RDF.LiteralComparisonTest do
end end
end end
test "incomparable" do describe "incomparable " do
test "when comparing incomparable types" do
Enum.each [ Enum.each [
{RDF.string("http://example.com/"), RDF.iri("http://example.com/")}, {RDF.string("http://example.com/"), RDF.iri("http://example.com/")},
{RDF.string("foo"), RDF.bnode("foo")}, {RDF.string("foo"), RDF.bnode("foo")},
@ -166,6 +188,16 @@ defmodule RDF.LiteralComparisonTest do
], &assert_incomparable/1 ], &assert_incomparable/1
end end
test "when comparing invalid literals" do
Enum.each [
{RDF.true, RDF.boolean(42)},
{RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002.04.02 12:00")},
{RDF.date("2002-04-02"), RDF.date("2002.04.02")},
{RDF.time("12:00:00"), RDF.time("12-00-00")},
], &assert_incomparable/1
end
end
defp assert_order({left, right}) do defp assert_order({left, right}) do
assert_compare_result({left, right}, :lt) assert_compare_result({left, right}, :lt)
@ -200,6 +232,17 @@ defmodule RDF.LiteralComparisonTest do
assert_less_than({right, left}, nil) assert_less_than({right, left}, nil)
end end
defp assert_indeterminate({left, right}) do
assert_compare_result({left, right}, :indeterminate)
assert_compare_result({right, left}, :indeterminate)
assert_greater_than({left, right}, false)
assert_greater_than({right, left}, false)
assert_less_than({left, right}, false)
assert_less_than({right, left}, false)
end
defp assert_compare_result({left, right}, expected) do defp assert_compare_result({left, right}, expected) do
result = RDF.Literal.compare(left, right) result = RDF.Literal.compare(left, right)
assert result == expected, """ assert result == expected, """