Add RDF.Term.equal?/2 and RDF.Term.equal_value?/2

This commit is contained in:
Marcel Otto 2018-06-08 12:26:52 +02:00
parent 389dec6c6b
commit d838424478
11 changed files with 383 additions and 10 deletions

View file

@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
- `RDF.Numeric` with a list of all numeric datatypes
- the logical operators and the Effective Boolean Value (EBV) coercion algorithm
from the XPath and SPARQL specs on `RDF.Boolean`
- `RDF.Term.equal?/2` and `RDF.Term.equal_value?/2`
### Changed

View file

@ -36,6 +36,20 @@ defmodule RDF.BlankNode do
do: id |> to_string |> new
@doc """
Tests for value equality of blank nodes.
Returns `nil` when the given arguments are not comparable as blank nodes.
"""
def equal_value?(left, right)
def equal_value?(%RDF.BlankNode{id: left}, %RDF.BlankNode{id: right}),
do: left == right
def equal_value?(_, _),
do: nil
defimpl String.Chars do
def to_string(%RDF.BlankNode{id: id}), do: "_:#{id}"
end

View file

@ -56,6 +56,16 @@ defmodule RDF.Datatype do
"""
@callback valid?(literal :: RDF.Literal.t) :: boolean
@doc """
Checks if the value of two `RDF.Literal`s of this datatype are equal.
Returns `nil` when the given arguments are not comparable as literals of this datatype.
The default implementation of the `_using__` macro compares the values of the
`canonical/1` forms of the given literals of this datatype.
"""
@callback equal_value?(literal1 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: boolean | nil
@lang_string RDF.iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")
@ -183,6 +193,12 @@ defmodule RDF.Datatype do
def valid?(_), do: false
def equal_value?(%Literal{datatype: @id} = literal1, %Literal{datatype: @id} = literal2) do
canonical(literal1).value == canonical(literal2).value
end
def equal_value?(_, _), do: nil
defoverridable [
build_literal_by_value: 2,
build_literal_by_lexical: 2,
@ -192,6 +208,7 @@ defmodule RDF.Datatype do
invalid_lexical: 1,
convert: 2,
valid?: 1,
equal_value?: 2,
]
end
end

View file

@ -1,9 +1,9 @@
defmodule RDF.Datatype.NS do
@moduledoc false
# Since the capability of RDF.Vocabulary.Namespaces requires the compilation
# of the RDF.NTriples.Decoder and the RDF.NTriples.Decoder depends on RDF.Literals,
# we can't define the XSD namespace in RDF.NS.
@moduledoc !"""
Since the capability of RDF.Vocabulary.Namespaces requires the compilation
of the RDF.NTriples.Decoder and the RDF.NTriples.Decoder depends on RDF.Literals,
we can't define the XSD namespace in RDF.NS.
"""
use RDF.Vocabulary.Namespace

View file

@ -82,4 +82,7 @@ defmodule RDF.Double do
"#{i}.#{f}E#{e}"
end
def equal_value?(left, right), do: RDF.Numeric.equal_value?(left, right)
end

View file

@ -18,5 +18,6 @@ defmodule RDF.Integer do
def convert(value, opts), do: super(value, opts)
def equal_value?(left, right), do: RDF.Numeric.equal_value?(left, right)
end

View file

@ -3,9 +3,10 @@ defmodule RDF.Numeric do
The set of all numeric datatypes.
"""
alias RDF.Literal
alias RDF.Datatype.NS.XSD
@types [
@types MapSet.new [
XSD.integer,
XSD.decimal,
XSD.float,
@ -27,11 +28,38 @@ defmodule RDF.Numeric do
@doc """
The list of all numeric datatypes.
"""
def types(), do: @types
def types(), do: MapSet.to_list(@types)
@doc """
Returns if a given datatype is numeric.
Returns if a given datatype is a numeric datatype.
"""
def type?(type), do: type in @types
def type?(type), do: MapSet.member?(@types, type)
@doc """
Tests for numeric value equality of two numeric literals.
Returns `nil` when the given arguments are not comparable as numeric literals.
see:
- <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
- <https://www.w3.org/TR/xpath-functions/#func-numeric-equal>
"""
def equal_value?(left, right)
def equal_value?(%Literal{datatype: left_datatype} = left,
%Literal{datatype: right_datatype} = right) do
if type?(left_datatype) and type?(right_datatype) do
# We rely here on Elixirs numeric equality comparison.
# TODO: There are probably problematic edge-case, which might require an
# TODO: implementation of XPath type promotion and subtype substitution
# - https://www.w3.org/TR/xpath-functions/#op.numeric
# - https://www.w3.org/TR/xpath20/#id-type-promotion-and-operator-mapping
Literal.canonical(left).value == Literal.canonical(right).value
end
end
def equal_value?(_, _), do: nil
end

View file

@ -186,6 +186,30 @@ defmodule RDF.IRI do
def parse(%URI{} = uri), do: uri
@doc """
Tests for value equality of IRIs.
Returns `nil` when the given arguments are not comparable as IRIs.
see <https://www.w3.org/TR/rdf-concepts/#section-Graph-URIref>
"""
def equal_value?(left, right)
def equal_value?(%RDF.IRI{value: left}, %RDF.IRI{value: right}),
do: left == right
@xsd_any_uri "http://www.w3.org/2001/XMLSchema#anyURI"
def equal_value?(%RDF.Literal{datatype: %RDF.IRI{value: @xsd_any_uri}, value: left}, right),
do: equal_value?(new(left), right)
def equal_value?(left, %RDF.Literal{datatype: %RDF.IRI{value: @xsd_any_uri}, value: right}),
do: equal_value?(left, new(right))
def equal_value?(_, _),
do: nil
defimpl String.Chars do
def to_string(%RDF.IRI{value: value}) do
value

View file

@ -128,7 +128,7 @@ defmodule RDF.Literal do
end
@doc """
Returns the given literal with the canonical lexical representation according to its datatype.
Returns the lexical representation of the given literal according to its datatype.
"""
def lexical(%RDF.Literal{value: value, uncanonical_lexical: nil, datatype: id} = literal) do
case RDF.Datatype.get(id) do
@ -215,6 +215,38 @@ defmodule RDF.Literal do
def typed?(literal), do: not plain?(literal)
@doc """
Checks if two `RDF.Literal`s of this datatype are equal.
Returns `nil` when the given arguments are not comparable as Literals.
see <https://www.w3.org/TR/rdf-concepts/#section-Literal-Equality>
"""
def equal_value?(left, right)
def equal_value?(%RDF.Literal{datatype: id1} = literal1, %RDF.Literal{datatype: id2} = literal2) do
case RDF.Datatype.get(id1) do
nil ->
if id1 == id2 do
literal1.value == literal2.value
end
datatype ->
datatype.equal_value?(literal1, literal2)
end
end
# TODO: Handle AnyURI in its own RDF.Datatype implementation
@xsd_any_uri "http://www.w3.org/2001/XMLSchema#anyURI"
def equal_value?(%RDF.Literal{datatype: %RDF.IRI{value: @xsd_any_uri}} = left, right),
do: RDF.IRI.equal_value?(left, right)
def equal_value?(left, %RDF.Literal{datatype: %RDF.IRI{value: @xsd_any_uri}} = right),
do: RDF.IRI.equal_value?(left, right)
def equal_value?(_, _), do: nil
end
defimpl String.Chars, for: RDF.Literal do

55
lib/rdf/term.ex Normal file
View file

@ -0,0 +1,55 @@
defprotocol RDF.Term do
@moduledoc """
Shared behaviour for all RDF terms.
A `RDF.Term` is anything which can be an element of RDF statements of a RDF graph:
- `RDF.IRI`s
- `RDF.BlankNode`s
- `RDF.Literal`s
see <https://www.w3.org/TR/sparql11-query/#defn_RDFTerm>
"""
@doc """
Tests for term equality.
see <http://www.w3.org/TR/rdf-sparql-query/#func-sameTerm>
"""
@fallback_to_any true
def equal?(term1, term2)
@doc """
Tests for equality of values.
Returns `nil` if the given terms are not comparable.
see <http://www.w3.org/TR/rdf-sparql-query/#func-RDFterm-equal>
and the value equality semantics of the different literal datatypes here:
<https://www.w3.org/TR/sparql11-query/#OperatorMapping>
"""
@fallback_to_any true
def equal_value?(term1, term2)
end
defimpl RDF.Term, for: RDF.IRI do
def equal?(term1, term2), do: term1 == term2
def equal_value?(term1, term2), do: RDF.IRI.equal_value?(term1, term2)
end
defimpl RDF.Term, for: RDF.BlankNode do
def equal?(term1, term2), do: term1 == term2
def equal_value?(term1, term2), do: RDF.BlankNode.equal_value?(term1, term2)
end
defimpl RDF.Term, for: RDF.Literal do
def equal?(term1, term2), do: term1 == term2
def equal_value?(term1, term2), do: RDF.Literal.equal_value?(term1, term2)
end
defimpl RDF.Term, for: Any do
def equal?(term1, term2), do: term1 == term2
def equal_value?(_, _), do: nil
end

198
test/unit/equality_test.exs Normal file
View file

@ -0,0 +1,198 @@
defmodule RDF.EqualityTest do
use RDF.Test.Case
alias RDF.NS.XSD
describe "RDF.IRI" do
@term_equal_iris [
{RDF.iri("http://example.com/"), RDF.iri("http://example.com/")},
]
@term_unequal_iris [
{RDF.iri("http://example.com/foo"), RDF.iri("http://example.com/bar")},
]
@value_equal_iris [
{RDF.iri("http://example.com/"),
RDF.literal("http://example.com/", datatype: XSD.anyURI)},
{RDF.literal("http://example.com/", datatype: XSD.anyURI),
RDF.iri("http://example.com/")},
{RDF.literal("http://example.com/", datatype: XSD.anyURI),
RDF.literal("http://example.com/", datatype: XSD.anyURI)},
]
@value_unequal_iris [
{RDF.iri("http://example.com/foo"),
RDF.literal("http://example.com/bar", datatype: XSD.anyURI)},
]
@incomparable_iris [
{RDF.iri("http://example.com/"), RDF.string("http://example.com/")},
]
test "term equality", do: assert_term_equal @term_equal_iris
test "term inequality", do: assert_term_unequal @term_unequal_iris
test "value equality", do: assert_value_equal @value_equal_iris
test "value inequality", do: assert_value_unequal @value_unequal_iris
test "incomparability", do: assert_incomparable @incomparable_iris
end
describe "RDF.BlankNode" do
@term_equal_bnodes [
{RDF.bnode("foo"), RDF.bnode("foo")},
]
@term_unequal_bnodes [
{RDF.bnode("foo"), RDF.bnode("bar")},
]
@value_equal_bnodes [
]
@value_unequal_bnodes [
]
@incomparable_bnodes [
{RDF.bnode("foo"), RDF.string("foo")},
{RDF.string("foo"), RDF.bnode("foo")},
]
test "term equality", do: assert_term_equal @term_equal_bnodes
test "term inequality", do: assert_term_unequal @term_unequal_bnodes
test "value equality", do: assert_value_equal @value_equal_bnodes
test "value inequality", do: assert_value_unequal @value_unequal_bnodes
test "incomparability", do: assert_incomparable @incomparable_bnodes
end
describe "RDF.Boolean" do
@term_equal_booleans [
{RDF.true, RDF.true},
{RDF.false, RDF.false},
]
@term_unequal_booleans [
{RDF.true, RDF.false},
{RDF.false, RDF.true},
]
@value_equal_booleans [
{RDF.true, RDF.boolean("TRUE")},
{RDF.boolean(1), RDF.true},
]
@value_unequal_booleans [
{RDF.true, RDF.boolean("FALSE")},
{RDF.boolean(0), RDF.true},
]
@incomparable_booleans [
{RDF.true, nil},
{nil, RDF.true},
{RDF.true, RDF.string("FALSE")},
{RDF.integer(0), RDF.true},
]
test "term equality", do: assert_term_equal @term_equal_booleans
test "term inequality", do: assert_term_unequal @term_unequal_booleans
test "value equality", do: assert_value_equal @value_equal_booleans
test "value inequality", do: assert_value_unequal @value_unequal_booleans
test "incomparability", do: assert_incomparable @incomparable_booleans
end
describe "RDF.Numeric" do
@term_equal_numerics [
{RDF.integer(42), RDF.integer(42)},
{RDF.integer("042"), RDF.integer("042")},
]
@term_unequal_numerics [
{RDF.integer(1), RDF.integer(2)},
]
@value_equal_numerics [
{RDF.integer("42"), RDF.integer("042")},
{RDF.double("+0"), RDF.double("-0")},
{RDF.integer("42"), RDF.double("42")},
{RDF.integer(42), RDF.double(42.0)},
]
@value_unequal_numerics [
{RDF.integer("1"), RDF.double("1.1")},
]
@incomparable_numerics [
{RDF.string("42"), RDF.integer(42)},
{RDF.integer("42"), RDF.string("42")},
]
test "term equality", do: assert_term_equal @term_equal_numerics
test "term inequality", do: assert_term_unequal @term_unequal_numerics
test "value equality", do: assert_value_equal @value_equal_numerics
test "value inequality", do: assert_value_unequal @value_unequal_numerics
test "incomparability", do: assert_incomparable @incomparable_numerics
end
describe "RDF.DateTime" do
@term_equal_datetimes [
{RDF.date_time("2002-04-02T12:00:00-01:00"), RDF.date_time("2002-04-02T12:00:00-01:00")},
{RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T12:00:00")},
]
@term_unequal_datetimes [
{RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T17:00:00")},
]
@value_equal_datetimes [
{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("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")},
]
@value_unequal_datetimes [
{RDF.date_time("2005-04-04T24:00:00"), RDF.date_time("2005-04-04T00:00:00")},
]
@incomparable_datetimes [
{RDF.string("2002-04-02T12:00:00-01:00"), RDF.date_time("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")},
]
test "term equality", do: assert_term_equal @term_equal_datetimes
test "term inequality", do: assert_term_unequal @term_unequal_datetimes
test "value equality", do: assert_value_equal @value_equal_datetimes
test "value inequality", do: assert_value_unequal @value_unequal_datetimes
test "incomparability", do: assert_incomparable @incomparable_datetimes
end
defp assert_term_equal(examples) do
Enum.each examples, fn example -> assert_term_equality(example, true) end
Enum.each examples, fn example -> assert_value_equality(example, true) end
end
defp assert_term_unequal(examples) do
Enum.each examples, fn example -> assert_term_equality(example, false) end
Enum.each examples, fn example -> assert_value_equality(example, false) end
end
defp assert_value_equal(examples) do
Enum.each examples, fn example -> assert_value_equality(example, true) end
end
defp assert_value_unequal(examples) do
Enum.each examples, fn example -> assert_value_equality(example, false) end
end
defp assert_incomparable(examples) do
Enum.each examples, fn example -> assert_term_equality(example, false) end
Enum.each examples, fn example -> assert_value_equality(example, nil) end
end
defp assert_term_equality({left, right}, expected) do
result = RDF.Term.equal?(left, right)
assert result == expected, """
expected RDF.Term.equal?(
#{inspect left},
#{inspect right})
to be: #{inspect expected}
but got: #{inspect result}
"""
end
defp assert_value_equality({left, right}, expected) do
result = RDF.Term.equal_value?(left, right)
assert result == expected, """
expected RDF.Term.equal_value?(
#{inspect left},
#{inspect right})
to be: #{inspect expected}
but got: #{inspect result}
"""
end
end