diff --git a/lib/rdf.ex b/lib/rdf.ex index b776de9..e3b069d 100644 --- a/lib/rdf.ex +++ b/lib/rdf.ex @@ -47,7 +47,7 @@ defmodule RDF do @standard_prefixes PrefixMap.new( - xsd: IRI.new(XSD.namespace()), + xsd: xsd_iri_base(), rdf: rdf_iri_base(), rdfs: rdfs_iri_base() ) @@ -138,7 +138,7 @@ defmodule RDF do false iex> RDF.resource?(RDF.bnode) true - iex> RDF.resource?(RDF.integer(42)) + iex> RDF.resource?(RDF.XSD.integer(42)) false iex> RDF.resource?(42) false @@ -171,7 +171,7 @@ defmodule RDF do false iex> RDF.term?(RDF.bnode) true - iex> RDF.term?(RDF.integer(42)) + iex> RDF.term?(RDF.XSD.integer(42)) true iex> RDF.term?(42) false @@ -208,6 +208,11 @@ defmodule RDF do defdelegate literal(value), to: Literal, as: :new defdelegate literal(value, opts), to: Literal, as: :new + def literal?(%Literal{}), do: true + def literal?(%RDF.Literal.Generic{}), do: true + def literal?(%datatype{}), do: Literal.Datatype.Registry.datatype?(datatype) + def literal?(_), do: false + defdelegate triple(s, p, o), to: Triple, as: :new defdelegate triple(tuple), to: Triple, as: :new @@ -237,24 +242,11 @@ defmodule RDF do def list(head, %Graph{} = graph), do: RDF.List.new(head, graph) def list(native_list, opts), do: RDF.List.from(native_list, opts) - for datatype <- RDF.Literal.Datatype.Registry.datatypes() -- [RDF.LangString] do - defdelegate unquote(String.to_atom(datatype.name))(value), to: datatype, as: :new - defdelegate unquote(String.to_atom(datatype.name))(value, opts), to: datatype, as: :new - - elixir_name = Macro.underscore(datatype.name) - unless datatype.name == elixir_name do - defdelegate unquote(String.to_atom(elixir_name))(value), to: datatype, as: :new - defdelegate unquote(String.to_atom(elixir_name))(value, opts), to: datatype, as: :new - end - end - - defdelegate langString(value, opts), to: RDF.LangString, as: :new - defdelegate lang_string(value, opts), to: RDF.LangString, as: :new - defdelegate datetime(value), to: RDF.XSD.DateTime, as: :new - defdelegate datetime(value, opts), to: RDF.XSD.DateTime, as: :new - defdelegate prefix_map(prefixes), to: RDF.PrefixMap, as: :new + defdelegate langString(value, opts), to: RDF.LangString, as: :new + defdelegate lang_string(value, opts), to: RDF.LangString, as: :new + for term <- ~w[type subject predicate object first rest value]a do defdelegate unquote(term)(), to: RDF.NS.RDF defdelegate unquote(term)(s, o), to: RDF.NS.RDF @@ -264,10 +256,8 @@ defmodule RDF do defdelegate unquote(term)(s, o1, o2, o3, o4, o5), to: RDF.NS.RDF end - defdelegate unquote(:true)(), to: RDF.XSD.Boolean.Value - defdelegate unquote(:false)(), to: RDF.XSD.Boolean.Value - defdelegate langString(), to: RDF.NS.RDF + defdelegate lang_string(), to: RDF.NS.RDF, as: :langString defdelegate unquote(nil)(), to: RDF.NS.RDF defdelegate __base_iri__(), to: RDF.NS.RDF diff --git a/lib/rdf/dataset.ex b/lib/rdf/dataset.ex index 6ece96b..975fd80 100644 --- a/lib/rdf/dataset.ex +++ b/lib/rdf/dataset.ex @@ -759,7 +759,7 @@ defmodule RDF.Dataset do iex> [ ...> {~I, ~I, ~L"Foo", ~I}, - ...> {~I, ~I, RDF.integer(42), } + ...> {~I, ~I, RDF.XSD.integer(42), } ...> ] ...> |> RDF.Dataset.new() ...> |> RDF.Dataset.values() @@ -774,7 +774,7 @@ defmodule RDF.Dataset do iex> [ ...> {~I, ~I, ~L"Foo", ~I}, - ...> {~I, ~I, RDF.integer(42), } + ...> {~I, ~I, RDF.XSD.integer(42), } ...> ] ...> |> RDF.Dataset.new() ...> |> RDF.Dataset.values(fn diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index 35c8244..c5f25e0 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -786,7 +786,7 @@ defmodule RDF.Graph do iex> [ ...> {~I, ~I, ~L"Foo"}, - ...> {~I, ~I, RDF.integer(42)} + ...> {~I, ~I, RDF.XSD.integer(42)} ...> ] ...> |> RDF.Graph.new() ...> |> RDF.Graph.values() @@ -797,7 +797,7 @@ defmodule RDF.Graph do iex> [ ...> {~I, ~I, ~L"Foo"}, - ...> {~I, ~I, RDF.integer(42)} + ...> {~I, ~I, RDF.XSD.integer(42)} ...> ] ...> |> RDF.Graph.new() ...> |> RDF.Graph.values(fn diff --git a/lib/rdf/literal.ex b/lib/rdf/literal.ex index b2d4e92..e6f481e 100644 --- a/lib/rdf/literal.ex +++ b/lib/rdf/literal.ex @@ -8,8 +8,6 @@ defmodule RDF.Literal do alias RDF.{IRI, LangString} alias RDF.Literal.{Generic, Datatype} - import RDF.Literal.Helper.Macros - @type t :: %__MODULE__{:literal => Datatype.literal()} @rdf_lang_string RDF.Utils.Bootstrapping.rdf_iri("langString") @@ -17,52 +15,23 @@ defmodule RDF.Literal do @doc """ Creates a new `RDF.Literal` of the given value and tries to infer an appropriate XSD datatype. + See `coerce/1` for applied mapping of Elixir types to XSD datatypes. + Note: The `RDF.literal` function is a shortcut to this function. - The following mapping of Elixir types to XSD datatypes is applied: - - | Elixir datatype | XSD datatype | - | :-------------- | :------------- | - | `string` | `xsd:string` | - | `boolean` | `xsd:boolean` | - | `integer` | `xsd:integer` | - | `float` | `xsd:double` | - | `Decimal` | `xsd:decimal` | - | `Time` | `xsd:time` | - | `Date` | `xsd:date` | - | `DateTime` | `xsd:dateTime` | - | `NaiveDateTime` | `xsd:dateTime` | - | `URI` | `xsd:AnyURI` | - ## Examples iex> RDF.Literal.new(42) - %RDF.Literal{literal: %XSD.Integer{value: 42}} + %RDF.Literal{literal: %RDF.XSD.Integer{value: 42}} """ @spec new(t | any) :: t | nil - def new(value) - - def new(%__MODULE__{} = literal), do: literal - def new(value) when is_binary(value), do: RDF.XSD.String.new(value) - def new(value) when is_boolean(value), do: RDF.XSD.Boolean.new(value) - def new(value) when is_integer(value), do: RDF.XSD.Integer.new(value) - def new(value) when is_float(value), do: RDF.XSD.Double.new(value) - def new(%Decimal{} = value), do: RDF.XSD.Decimal.new(value) - def new(%Date{} = value), do: RDF.XSD.Date.new(value) - def new(%Time{} = value), do: RDF.XSD.Time.new(value) - def new(%DateTime{} = value), do: RDF.XSD.DateTime.new(value) - def new(%NaiveDateTime{} = value), do: RDF.XSD.DateTime.new(value) - def new(%URI{} = value), do: RDF.XSD.AnyURI.new(value) - - Enum.each(Datatype.Registry.datatypes(), fn datatype -> - def new(%unquote(datatype.literal_type()){} = literal) do - %__MODULE__{literal: literal} - end - end) - def new(value) do - raise RDF.Literal.InvalidError, "#{inspect value} not convertible to a RDF.Literal" + case coerce(value) do + nil -> + raise RDF.Literal.InvalidError, "#{inspect value} not convertible to a RDF.Literal" + literal -> literal + end end @doc """ @@ -92,6 +61,56 @@ defmodule RDF.Literal do end end + @doc """ + Coerces a new `RDF.Literal` from another value. + + The following mapping of Elixir types to XSD datatypes is applied: + + | Elixir datatype | XSD datatype | + | :-------------- | :------------- | + | `string` | `xsd:string` | + | `boolean` | `xsd:boolean` | + | `integer` | `xsd:integer` | + | `float` | `xsd:double` | + | `Decimal` | `xsd:decimal` | + | `Time` | `xsd:time` | + | `Date` | `xsd:date` | + | `DateTime` | `xsd:dateTime` | + | `NaiveDateTime` | `xsd:dateTime` | + | `URI` | `xsd:AnyURI` | + + When an `RDF.Literal` can not be coerced, `nil` is returned + (as opposed to `new/1` which fails in this case). + + ## Examples + + iex> RDF.Literal.coerce(42) + %RDF.Literal{literal: %RDF.XSD.Integer{value: 42}} + + """ + def coerce(value) + + def coerce(%__MODULE__{} = literal), do: literal + + Enum.each(Datatype.Registry.datatypes(), fn datatype -> + def coerce(%unquote(datatype){} = literal) do + %__MODULE__{literal: literal} + end + end) + + def coerce(value) when is_binary(value), do: RDF.XSD.String.new(value) + def coerce(value) when is_boolean(value), do: RDF.XSD.Boolean.new(value) + def coerce(value) when is_integer(value), do: RDF.XSD.Integer.new(value) + def coerce(value) when is_float(value), do: RDF.XSD.Double.new(value) + def coerce(%Decimal{} = value), do: RDF.XSD.Decimal.new(value) + def coerce(%Date{} = value), do: RDF.XSD.Date.new(value) + def coerce(%Time{} = value), do: RDF.XSD.Time.new(value) + def coerce(%DateTime{} = value), do: RDF.XSD.DateTime.new(value) + def coerce(%NaiveDateTime{} = value), do: RDF.XSD.DateTime.new(value) + def coerce(%URI{} = value), do: RDF.XSD.AnyURI.new(value) + def coerce(_), do: nil + + @doc """ Creates a new `RDF.Literal`, but fails if it's not valid. @@ -101,10 +120,10 @@ defmodule RDF.Literal do ## Examples iex> RDF.Literal.new("foo") - %RDF.Literal{literal: %XSD.String{value: "foo"}} + %RDF.Literal{literal: %RDF.XSD.String{value: "foo"}} iex> RDF.Literal.new!("foo", datatype: RDF.NS.XSD.integer) - ** (RDF.Literal.InvalidError) invalid RDF.Literal: %RDF.Literal{literal: %XSD.Integer{value: nil, lexical: "foo"}, valid: false} + ** (RDF.Literal.InvalidError) invalid RDF.Literal: %RDF.Literal{literal: %RDF.XSD.Integer{value: nil, lexical: "foo"}, valid: false} iex> RDF.Literal.new!("foo", datatype: RDF.langString) ** (RDF.Literal.InvalidError) invalid RDF.Literal: %RDF.Literal{literal: %RDF.LangString{language: nil, value: "foo"}, valid: false} @@ -149,7 +168,7 @@ defmodule RDF.Literal do see """ @spec simple?(t) :: boolean - def simple?(%__MODULE__{literal: %XSD.String{}}), do: true + def simple?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true def simple?(%__MODULE__{} = _), do: false @@ -162,7 +181,7 @@ defmodule RDF.Literal do see """ @spec plain?(t) :: boolean - def plain?(%__MODULE__{literal: %XSD.String{}}), do: true + def plain?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true def plain?(%__MODULE__{literal: %LangString{}}), do: true def plain?(%__MODULE__{} = _), do: false @@ -171,13 +190,13 @@ defmodule RDF.Literal do ############################################################################ - # functions delegating to the RDF.Datatype of a RDF.Literal + # functions delegating to the RDF.Literal.Datatype of a RDF.Literal @spec datatype(t) :: IRI.t() - defdelegate_to_rdf_datatype :datatype + def datatype(%__MODULE__{literal: %datatype{} = literal}), do: datatype.datatype(literal) @spec language(t) :: String.t() | nil - defdelegate_to_rdf_datatype :language + def language(%__MODULE__{literal: %datatype{} = literal}), do: datatype.language(literal) @spec value(t) :: any def value(%__MODULE__{literal: %datatype{} = literal}), do: datatype.value(literal) @@ -186,7 +205,7 @@ defmodule RDF.Literal do def lexical(%__MODULE__{literal: %datatype{} = literal}), do: datatype.lexical(literal) @spec canonical(t) :: t - defdelegate_to_rdf_datatype :canonical + def canonical(%__MODULE__{literal: %datatype{} = literal}), do: datatype.canonical(literal) @spec canonical?(t) :: boolean def canonical?(%__MODULE__{literal: %datatype{} = literal}), do: datatype.canonical?(literal) @@ -195,44 +214,38 @@ defmodule RDF.Literal do def valid?(%__MODULE__{literal: %datatype{} = literal}), do: datatype.valid?(literal) def valid?(_), do: false + @spec equal?(any, any) :: boolean + def equal?(left, right), do: left == right + @spec equal_value?(t, t | any) :: boolean - def equal_value?(%__MODULE__{literal: %datatype{} = left}, right) do - Datatype.Registry.rdf_datatype(datatype).equal_value?(left, right) - end + def equal_value?(%__MODULE__{literal: %datatype{} = left}, right), + do: datatype.equal_value?(left, right) + + def equal_value?(left, right) when not is_nil(left), + do: equal_value?(coerce(left), right) def equal_value?(_, _), do: nil @spec compare(t, t) :: Datatype.comparison_result | :indeterminate | nil def compare(%__MODULE__{literal: %datatype{} = left}, right) do - Datatype.Registry.rdf_datatype(datatype).compare(left, right) + datatype.compare(left, right) end @doc """ Checks if the first of two `RDF.Literal`s is smaller then the other. - - Returns `nil` when the given arguments are not comparable datatypes. """ - @spec less_than?(t, t) :: boolean | nil - def less_than?(literal1, literal2) do - case compare(literal1, literal2) do - :lt -> true - nil -> nil - _ -> false - end + @spec less_than?(t, t) :: boolean + def less_than?(left, right) do + compare(left, right) == :lt end + @doc """ Checks if the first of two `RDF.Literal`s is greater then the other. - - Returns `nil` when the given arguments are not comparable datatypes. """ - @spec greater_than?(t, t) :: boolean | nil - def greater_than?(literal1, literal2) do - case compare(literal1, literal2) do - :gt -> true - nil -> nil - _ -> false - end + @spec greater_than?(t, t) :: boolean + def greater_than?(left, right) do + compare(left, right) == :gt end @@ -247,15 +260,15 @@ defmodule RDF.Literal do def matches?(value, pattern, flags \\ "") def matches?(%__MODULE__{} = literal, pattern, flags), do: matches?(lexical(literal), pattern, flags) - def matches?(value, %__MODULE__{literal: %XSD.String{}} = pattern, flags), + def matches?(value, %__MODULE__{literal: %RDF.XSD.String{}} = pattern, flags), do: matches?(value, lexical(pattern), flags) - def matches?(value, pattern, %__MODULE__{literal: %XSD.String{}} = flags), + def matches?(value, pattern, %__MODULE__{literal: %RDF.XSD.String{}} = flags), do: matches?(value, pattern, lexical(flags)) def matches?(value, pattern, flags) when is_binary(value) and is_binary(pattern) and is_binary(flags), - do: XSD.Utils.Regex.matches?(value, pattern, flags) + do: RDF.XSD.Utils.Regex.matches?(value, pattern, flags) def update(%__MODULE__{literal: %datatype{} = literal}, fun, opts \\ []) do - Datatype.Registry.rdf_datatype(datatype).update(literal, fun, opts) + datatype.update(literal, fun, opts) end defimpl String.Chars do diff --git a/lib/rdf/literal/datatype.ex b/lib/rdf/literal/datatype.ex index d9fdca7..0002289 100644 --- a/lib/rdf/literal/datatype.ex +++ b/lib/rdf/literal/datatype.ex @@ -7,9 +7,6 @@ defmodule RDF.Literal.Datatype do @type comparison_result :: :lt | :gt | :eq - @doc false - @callback literal_type :: module - @doc """ The name of the datatype. """ @@ -18,35 +15,52 @@ defmodule RDF.Literal.Datatype do @doc """ The IRI of the datatype. """ - @callback id :: String.t() | nil + @callback id :: IRI.t() | nil + + @callback new(any) :: Literal.t() + @callback new(any, Keyword.t()) :: Literal.t() + + @callback new!(any) :: Literal.t() + @callback new!(any, Keyword.t()) :: Literal.t() @doc """ - The datatype IRI of the given datatype literal. + Casts a datatype literal or coercible value of one type into a datatype literal of another type. + + If the given literal or value is invalid or can not be converted into this datatype an + implementation should return `nil`. + + This function is called by auto-generated `cast/1` function on the implementations, + which already deals with basic cases and coercion. + """ + @callback do_cast(literal | any) :: Literal.t() | nil + + @doc """ + The datatype IRI of the given `RDF.Literal`. """ @callback datatype(Literal.t | literal) :: IRI.t() @doc """ - The language of the given datatype literal if present. + The language of the given `RDF.Literal` if present. """ @callback language(Literal.t | literal) :: String.t() | nil @doc """ - Returns the value of a datatype literal. + Returns the value of a `RDF.Literal`. """ @callback value(Literal.t | literal) :: any @doc """ - Returns the lexical form of a datatype literal. + Returns the lexical form of a `RDF.Literal`. """ @callback lexical(Literal.t() | literal) :: String.t() @doc """ - Produces the canonical representation of a datatype literal. + Produces the canonical representation of a `RDF.Literal`. """ @callback canonical(Literal.t() | literal) :: Literal.t() @doc """ - Determines if the lexical form of a datatype literal is the canonical form. + Determines if the lexical form of a `RDF.Literal` is the canonical form. Note: For `RDF.Literal.Generic` literals with the canonical form not defined, this always return `true`. @@ -54,27 +68,26 @@ defmodule RDF.Literal.Datatype do @callback canonical?(Literal.t() | literal | any) :: boolean @doc """ - Determines if the lexical form of a datatype literal is a member of its lexical value space. + Determines if the lexical form of a `RDF.Literal` is a member of its lexical value space. """ @callback valid?(Literal.t() | literal | any) :: boolean - @doc """ - Casts a datatype literal or coercible value of one type into a datatype literal of another type. - - If the given literal or value is invalid or can not be converted into this datatype an - implementation should return `nil`. - """ - @callback cast(Literal.t() | literal | any) :: Literal.t() | nil - @doc """ Checks if two datatype literals are equal in terms of the values of their value space. - Non-`RDF.Literal.Datatype` literals are tried to be coerced via `RDF.Term.coerce/1` before comparison. + Should return `nil` when the given arguments are not comparable as literals of this + datatype. This behaviour is particularly important for SPARQL.ex where this + function is used for the `=` operator, where comparisons between incomparable + terms are treated as errors and immediately leads to a rejection of a possible + match. + + This function is called by auto-generated `equal_value?/2` function on the + implementations, which already deals with basic cases and coercion. """ - @callback equal_value?(Literal.t() | literal, Literal.t() | literal | any) :: boolean + @callback do_equal_value?(literal, literal) :: boolean @doc """ - Compares two datatype literals. + Compares two `RDF.Literal`s. Returns `:gt` if value of the first literal is greater than the value of the second in terms of their datatype and `:lt` for vice versa. If the two literals are equal `:eq` is returned. @@ -83,9 +96,156 @@ defmodule RDF.Literal.Datatype do Returns `nil` when the given arguments are not comparable datatypes or if one them is invalid. + + The default implementation of the `_using__` macro of `RDF.XSD.Datatype`s + compares the values of the `canonical/1` forms of the given literals of this datatype. """ @callback compare(Literal.t() | literal, Literal.t() | literal) :: comparison_result | :indeterminate | nil + @doc """ + Updates the value of a `RDF.Literal` without changing everything else. + + ## Example + + iex> RDF.XSD.integer(42) |> RDF.XSD.Integer.update(fn value -> value + 1 end) + RDF.XSD.integer(42) + iex> RDF.literal("foo", language: "de") |> RDF.LangString.update(fn _ -> "bar" end) + RDF.literal("bar", language: "de") + iex> RDF.literal("foo", datatype: "http://example.com/dt") |> RDF.LangString.update(fn _ -> "bar" end) + RDF.literal("bar", datatype: "http://example.com/dt") + """ @callback update(Literal.t() | literal, fun()) :: Literal.t + + @doc """ + Updates the value of a `RDF.Literal` without changing everything else. + + This variant of `c:update/2` allows with the `:as` option to specify what will + be passed to `fun`, eg. with `as: :lexical` the lexical is passed to the function. + + ## Example + + iex> RDF.XSD.integer(42) |> RDF.XSD.Integer.update( + ...> fn value -> value <> "1" end, as: lexical) + RDF.XSD.integer(421) + """ @callback update(Literal.t() | literal, fun(), keyword) :: Literal.t + + defmacro __using__(opts) do + name = Keyword.fetch!(opts, :name) + id = Keyword.fetch!(opts, :id) + + quote do + @behaviour unquote(__MODULE__) + + @name unquote(name) + @impl unquote(__MODULE__) + def name, do: @name + + @id if unquote(id), do: RDF.IRI.new(unquote(id)) + @impl unquote(__MODULE__) + def id, do: @id + + @impl unquote(__MODULE__) + def datatype(%Literal{literal: literal}), do: datatype(literal) + def datatype(%__MODULE__{}), do: @id + + @impl unquote(__MODULE__) + def language(%Literal{literal: literal}), do: language(literal) + def language(%__MODULE__{}), do: nil + + @doc """ + Casts a datatype literal or coercible value of one type into a datatype literal of another type. + + Returns `nil` when the given arguments are not comparable as literals of this + datatype or when the given argument is an invalid literal. + + Implementations define the casting for a given value with the `c:do_cast/1` callback. + """ + def cast(literal_or_value) + def cast(%Literal{literal: literal}), do: cast(literal) + def cast(%__MODULE__{} = datatype_literal), + do: if(valid?(datatype_literal), do: literal(datatype_literal)) + def cast(nil), do: nil + def cast(value) do + case do_cast(value) do + %__MODULE__{} = literal -> if valid?(literal), do: literal(literal) + %Literal{literal: %__MODULE__{}} = literal -> if valid?(literal), do: literal + _ -> nil + end + end + + @impl unquote(__MODULE__) + def do_cast(value) do + value |> Literal.coerce() |> cast() + end + + @doc """ + Checks if two datatype literals are equal in terms of the values of their value space. + + Non-`RDF.Literal`s are tried to be coerced via `RDF.Literal.coerce/1` before comparison. + + Returns `nil` when the given arguments are not comparable as literals of this + datatype. + + Implementations define this equivalence relation via the `c:do_equal_value?/2` callback. + """ + def equal_value?(left, right) + def equal_value?(left, %Literal{literal: right}), do: equal_value?(left, right) + def equal_value?(%Literal{literal: left}, right), do: equal_value?(left, right) + def equal_value?(nil, _), do: nil + def equal_value?(_, nil), do: nil + def equal_value?(left, right) do + cond do + not RDF.literal?(right) -> equal_value?(left, Literal.coerce(right)) + not RDF.literal?(left) -> equal_value?(Literal.coerce(left), right) + true -> do_equal_value?(left, right) + end + end + + # RDF.XSD.Datatypes offers another default implementation, but since it is + # still in a macro implementation defoverridable doesn't work + unless RDF.XSD.Datatype in @behaviour do + @impl unquote(__MODULE__) + def do_equal_value?(left, right) + def do_equal_value?(%__MODULE__{} = left, %__MODULE__{} = right), do: left == right + def do_equal_value?(_, _), do: nil + + defoverridable do_equal_value?: 2 + end + + @impl unquote(__MODULE__) + def update(literal, fun, opts \\ []) + def update(%Literal{literal: literal}, fun, opts), do: update(literal, fun, opts) + def update(%__MODULE__{} = literal, fun, opts) do + case Keyword.get(opts, :as) do + :lexical -> lexical(literal) + nil -> value(literal) + end + |> fun.() + |> new() + end + + @spec less_than?(t, t) :: boolean + def less_than?(literal1, literal2), do: Literal.less_than?(literal1, literal2) + + @spec greater_than?(t, t) :: boolean + def greater_than?(literal1, literal2), do: Literal.greater_than?(literal1, literal2) + + defp literal(datatype_literal), do: %Literal{literal: datatype_literal} + + defoverridable datatype: 1, + language: 1, + cast: 1, + do_cast: 1, + equal_value?: 2, + update: 2, + update: 3 + + defimpl String.Chars do + def to_string(literal) do + literal.__struct__.lexical(literal) + end + end + end + end end diff --git a/lib/rdf/literal/datatype/ns.ex b/lib/rdf/literal/datatype/ns.ex deleted file mode 100644 index 7b91c5c..0000000 --- a/lib/rdf/literal/datatype/ns.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule RDF.Literal.Datatype.NS do - @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. - - TODO: This is no longer the case. Why can't we remove it resp. move it to RDF.NS now? - """ - - use RDF.Vocabulary.Namespace - - @vocabdoc false - defvocab XSD, - base_iri: "http://www.w3.org/2001/XMLSchema#", - terms: ~w[ - string - normalizedString - token - language - Name - NCName - ID - IDREF - IDREFS - ENTITY - ENTITIES - NMTOKEN - NMTOKENS - boolean - float - double - decimal - integer - long - int - short - byte - nonPositiveInteger - negativeInteger - nonNegativeInteger - positiveInteger - unsignedLong - unsignedInt - unsignedShort - unsignedByte - duration - dayTimeDuration - yearMonthDuration - dateTime - time - date - gYearMonth - gYear - gMonthDay - gDay - gMonth - base64Binary - hexBinary - anyURI - QName - NOTATION - ] -end diff --git a/lib/rdf/literal/datatype/registry.ex b/lib/rdf/literal/datatype/registry.ex index 6548e67..bf2af7c 100644 --- a/lib/rdf/literal/datatype/registry.ex +++ b/lib/rdf/literal/datatype/registry.ex @@ -2,15 +2,11 @@ defmodule RDF.Literal.Datatype.Registry do @moduledoc false - alias RDF.Literal - alias RDF.IRI + alias RDF.{Literal, IRI, XSD} - @datatypes [ - RDF.LangString - | Enum.map(XSD.datatypes(), &Literal.XSD.datatype_module_name/1) - ] + @datatypes [RDF.LangString | Enum.to_list(XSD.datatypes())] - @mapping Map.new(@datatypes, fn datatype -> {RDF.IRI.new(datatype.id), datatype} end) + @mapping Map.new(@datatypes, fn datatype -> {IRI.new(datatype.id), datatype} end) @doc """ The mapping of IRIs of datatypes to their `RDF.Literal.Datatype`. @@ -40,13 +36,4 @@ defmodule RDF.Literal.Datatype.Registry do def get(%Literal{} = literal), do: Literal.datatype(literal) def get(id) when is_binary(id), do: id |> IRI.new() |> get() def get(id), do: @mapping[id] - - @doc false - def rdf_datatype(datatype) - Enum.each XSD.datatypes(), fn xsd_datatype -> - def rdf_datatype(unquote(xsd_datatype)) do - unquote(Literal.XSD.datatype_module_name(xsd_datatype)) - end - end - def rdf_datatype(datatype), do: datatype end diff --git a/lib/rdf/literal/datatypes/generic.ex b/lib/rdf/literal/datatypes/generic.ex index d510f8e..3559e29 100644 --- a/lib/rdf/literal/datatypes/generic.ex +++ b/lib/rdf/literal/datatypes/generic.ex @@ -5,7 +5,9 @@ defmodule RDF.Literal.Generic do defstruct [:value, :datatype] - @behaviour RDF.Literal.Datatype + use RDF.Literal.Datatype, + name: "generic", + id: nil alias RDF.Literal.Datatype alias RDF.{Literal, IRI} @@ -16,16 +18,8 @@ defmodule RDF.Literal.Generic do } @impl Datatype - def literal_type, do: __MODULE__ - - @impl Datatype - def name, do: "generic" - - @impl Datatype - def id, do: nil - @spec new(any, String.t | IRI.t | keyword) :: Literal.t - def new(value, datatype_or_opts) + def new(value, datatype_or_opts \\ []) def new(value, datatype) when is_binary(datatype), do: new(value, datatype: datatype) def new(value, %IRI{} = datatype), do: new(value, datatype: datatype) def new(value, opts) do @@ -41,8 +35,9 @@ defmodule RDF.Literal.Generic do defp normalize_datatype(""), do: nil defp normalize_datatype(datatype), do: IRI.new(datatype) + @impl Datatype @spec new!(any, String.t | IRI.t | keyword) :: Literal.t - def new!(value, datatype_or_opts) do + def new!(value, datatype_or_opts \\ []) do literal = new(value, datatype_or_opts) if valid?(literal) do @@ -56,10 +51,6 @@ defmodule RDF.Literal.Generic do def datatype(%Literal{literal: literal}), do: datatype(literal) def datatype(%__MODULE__{} = literal), do: literal.datatype - @impl Datatype - def language(%Literal{literal: literal}), do: language(literal) - def language(%__MODULE__{}), do: nil - @impl Datatype def value(%Literal{literal: literal}), do: value(literal) def value(%__MODULE__{} = literal), do: literal.value @@ -70,7 +61,7 @@ defmodule RDF.Literal.Generic do @impl Datatype def canonical(%Literal{literal: %__MODULE__{}} = literal), do: literal - def canonical(%__MODULE__{} = literal), do: %Literal{literal: literal} + def canonical(%__MODULE__{} = literal), do: literal(literal) @impl Datatype def canonical?(%Literal{literal: literal}), do: canonical?(literal) @@ -81,16 +72,18 @@ defmodule RDF.Literal.Generic do def valid?(%__MODULE__{datatype: %IRI{}}), do: true def valid?(_), do: false - @impl Datatype + @doc """ + Since generic literals don't support casting, always returns `nil`. + """ def cast(_), do: nil @impl Datatype - def equal_value?(left, %Literal{literal: right}), do: equal_value?(left, right) - def equal_value?(%Literal{literal: left}, right), do: equal_value?(left, right) - def equal_value?(%__MODULE__{datatype: datatype} = left, - %__MODULE__{datatype: datatype} = right), - do: left == right - def equal_value?(_, _), do: nil + def do_cast(_), do: nil + + @impl Datatype + def do_equal_value?(%__MODULE__{datatype: datatype} = left, + %__MODULE__{datatype: datatype} = right), do: left == right + def do_equal_value?(_, _), do: nil @impl Datatype def compare(left, %Literal{literal: right}), do: compare(left, right) @@ -118,10 +111,4 @@ defmodule RDF.Literal.Generic do |> fun.() |> new(datatype: literal.datatype) end - - defimpl String.Chars do - def to_string(literal) do - literal.value - end - end end diff --git a/lib/rdf/literal/datatypes/lang_string.ex b/lib/rdf/literal/datatypes/lang_string.ex index 87ca591..6759ca8 100644 --- a/lib/rdf/literal/datatypes/lang_string.ex +++ b/lib/rdf/literal/datatypes/lang_string.ex @@ -5,7 +5,9 @@ defmodule RDF.LangString do defstruct [:value, :language] - @behaviour RDF.Literal.Datatype + use RDF.Literal.Datatype, + name: "langString", + id: RDF.Utils.Bootstrapping.rdf_iri("langString") alias RDF.Literal.Datatype alias RDF.Literal @@ -15,20 +17,9 @@ defmodule RDF.LangString do language: String.t } - @iri RDF.Utils.Bootstrapping.rdf_iri("langString") - @id to_string(@iri) - - @impl Datatype - def literal_type, do: __MODULE__ - - @impl Datatype - def name, do: "langString" - - @impl Datatype - def id, do: @id - + @impl RDF.Literal.Datatype @spec new(any, String.t | atom | keyword) :: Literal.t - def new(value, language_or_opts) + def new(value, language_or_opts \\ []) def new(value, language) when is_binary(language), do: new(value, language: language) def new(value, language) when is_atom(language), do: new(value, language: language) def new(value, opts) do @@ -45,8 +36,9 @@ defmodule RDF.LangString do defp normalize_language(language) when is_atom(language), do: language |> to_string() |> normalize_language() defp normalize_language(language), do: String.downcase(language) + @impl RDF.Literal.Datatype @spec new!(any, String.t | atom | keyword) :: Literal.t - def new!(value, language_or_opts) do + def new!(value, language_or_opts \\ []) do literal = new(value, language_or_opts) if valid?(literal) do @@ -56,10 +48,6 @@ defmodule RDF.LangString do end end - @impl Datatype - def datatype(%Literal{literal: literal}), do: datatype(literal) - def datatype(%__MODULE__{}), do: @iri - @impl Datatype def language(%Literal{literal: literal}), do: language(literal) def language(%__MODULE__{} = literal), do: literal.language @@ -74,7 +62,7 @@ defmodule RDF.LangString do @impl Datatype def canonical(%Literal{literal: %__MODULE__{}} = literal), do: literal - def canonical(%__MODULE__{} = literal), do: %Literal{literal: literal} + def canonical(%__MODULE__{} = literal), do: literal(literal) @impl Datatype def canonical?(%Literal{literal: literal}), do: canonical?(literal) @@ -86,14 +74,7 @@ defmodule RDF.LangString do def valid?(_), do: false @impl Datatype - def cast(%Literal{literal: %__MODULE__{}} = literal), do: if valid?(literal), do: literal - def cast(_), do: nil - - @impl Datatype - def equal_value?(left, %Literal{literal: right}), do: equal_value?(left, right) - def equal_value?(%Literal{literal: left}, right), do: equal_value?(left, right) - def equal_value?(%__MODULE__{} = left, %__MODULE__{} = right), do: left == right - def equal_value?(_, _), do: nil + def do_cast(_), do: nil @impl Datatype def compare(left, %Literal{literal: right}), do: compare(left, right) @@ -157,10 +138,4 @@ defmodule RDF.LangString do end def match_language?(_, _), do: false - - defimpl String.Chars do - def to_string(literal) do - literal.value - end - end end diff --git a/lib/rdf/literal/datatypes/numeric.ex b/lib/rdf/literal/datatypes/numeric.ex deleted file mode 100644 index 3642f35..0000000 --- a/lib/rdf/literal/datatypes/numeric.ex +++ /dev/null @@ -1,191 +0,0 @@ -defmodule RDF.Numeric do - @moduledoc """ - Collection of functions for numeric literals. - """ - - alias RDF.Literal - - import Kernel, except: [abs: 1, floor: 1, ceil: 1, round: 1] - - @datatypes XSD.Numeric.datatypes() - - @doc """ - The set of all numeric datatypes. - """ - def datatypes(), do: @datatypes - - @doc """ - Returns if a given datatype is a numeric datatype. - """ - def datatype?(datatype), do: datatype in @datatypes - - @doc """ - Returns if a given literal has a numeric datatype. - """ - @spec literal?(Literal.t | any) :: boolean - def literal?(%Literal{literal: %datatype{}}), do: datatype?(datatype) - def literal?(_), do: false - - - def zero?(%Literal{literal: literal}), do: zero?(literal) - def zero?(literal), do: XSD.Numeric.zero?(literal) - - def negative_zero?(%Literal{literal: literal}), do: negative_zero?(literal) - def negative_zero?(literal), do: XSD.Numeric.negative_zero?(literal) - - @doc """ - Adds two numeric literals. - - For `xsd:float` or `xsd:double` values, if one of the operands is a zero or a - finite number and the other is INF or -INF, INF or -INF is returned. If both - operands are INF, INF is returned. If both operands are -INF, -INF is returned. - If one of the operands is INF and the other is -INF, NaN is returned. - - If one of the given arguments is not a numeric literal, `nil` is returned. - - see - """ - def add(left, right) - def add(left, %Literal{literal: right}), do: add(left, right) - def add(%Literal{literal: left}, right), do: add(left, right) - def add(left, right) do - if result = XSD.Numeric.add(left, right) do - Literal.new(result) - end - end - - @doc """ - Subtracts two numeric literals. - - For `xsd:float` or `xsd:double` values, if one of the operands is a zero or a - finite number and the other is INF or -INF, an infinity of the appropriate sign - is returned. If both operands are INF or -INF, NaN is returned. If one of the - operands is INF and the other is -INF, an infinity of the appropriate sign is - returned. - - If one of the given arguments is not a numeric literal, `nil` is returned. - - see - """ - def subtract(left, right) - def subtract(left, %Literal{literal: right}), do: subtract(left, right) - def subtract(%Literal{literal: left}, right), do: subtract(left, right) - def subtract(left, right) do - if result = XSD.Numeric.subtract(left, right) do - Literal.new(result) - end - end - - @doc """ - Multiplies two numeric literals. - - For `xsd:float` or `xsd:double` values, if one of the operands is a zero and - the other is an infinity, NaN is returned. If one of the operands is a non-zero - number and the other is an infinity, an infinity with the appropriate sign is - returned. - - If one of the given arguments is not a numeric literal, `nil` is returned. - - see - """ - def multiply(left, right) - def multiply(left, %Literal{literal: right}), do: multiply(left, right) - def multiply(%Literal{literal: left}, right), do: multiply(left, right) - def multiply(left, right) do - if result = XSD.Numeric.multiply(left, right) do - Literal.new(result) - end - end - - @doc """ - Divides two numeric literals. - - For `xsd:float` and `xsd:double` operands, floating point division is performed - as specified in [IEEE 754-2008]. A positive number divided by positive zero - returns INF. A negative number divided by positive zero returns -INF. Division - by negative zero returns -INF and INF, respectively. Positive or negative zero - divided by positive or negative zero returns NaN. Also, INF or -INF divided by - INF or -INF returns NaN. - - If one of the given arguments is not a numeric literal, `nil` is returned. - - `nil` is also returned for `xsd:decimal` and `xsd:integer` operands, if the - divisor is (positive or negative) zero. - - see - """ - def divide(left, right) - def divide(left, %Literal{literal: right}), do: divide(left, right) - def divide(%Literal{literal: left}, right), do: divide(left, right) - def divide(left, right) do - if result = XSD.Numeric.divide(left, right) do - Literal.new(result) - end - end - - @doc """ - Returns the absolute value of a numeric literal. - - If the argument is not a valid numeric literal `nil` is returned. - - see - """ - def abs(numeric) - def abs(%Literal{literal: numeric}), do: abs(numeric) - def abs(numeric) do - if result = XSD.Numeric.abs(numeric) do - Literal.new(result) - end - end - - @doc """ - Rounds a value to a specified number of decimal places, rounding upwards if two such values are equally near. - - The function returns the nearest (that is, numerically closest) value to the - given literal value that is a multiple of ten to the power of minus `precision`. - If two such values are equally near (for example, if the fractional part in the - literal value is exactly .5), the function returns the one that is closest to - positive infinity. - - If the argument is not a valid numeric literal `nil` is returned. - - see - """ - def round(numeric, precision \\ 0) - def round(%Literal{literal: numeric}, precision), do: round(numeric, precision) - def round(numeric, precision) do - if result = XSD.Numeric.round(numeric, precision) do - Literal.new(result) - end - end - - @doc """ - Rounds a numeric literal upwards to a whole number literal. - - If the argument is not a valid numeric literal `nil` is returned. - - see - """ - def ceil(numeric) - def ceil(%Literal{literal: numeric}), do: ceil(numeric) - def ceil(numeric) do - if result = XSD.Numeric.ceil(numeric) do - Literal.new(result) - end - end - - @doc """ - Rounds a numeric literal downwards to a whole number literal. - - If the argument is not a valid numeric literal `nil` is returned. - - see - """ - def floor(numeric) - def floor(%Literal{literal: numeric}), do: floor(numeric) - def floor(numeric) do - if result = XSD.Numeric.floor(numeric) do - Literal.new(result) - end - end -end diff --git a/lib/rdf/literal/datatypes/xsd.ex b/lib/rdf/literal/datatypes/xsd.ex deleted file mode 100644 index 716daca..0000000 --- a/lib/rdf/literal/datatypes/xsd.ex +++ /dev/null @@ -1,157 +0,0 @@ -defmodule RDF.Literal.XSD do - @moduledoc false - - alias RDF.Literal - - @after_compile __MODULE__ - - def __after_compile__(_env, _bytecode) do - Enum.each XSD.datatypes(), &def_xsd_datatype/1 - end - - defp def_xsd_datatype(xsd_datatype) do - xsd_datatype - |> datatype_module_name() - |> Module.create(datatype_module_body(xsd_datatype), Macro.Env.location(__ENV__)) - end - - def datatype_module_name(xsd_datatype) do - Module.concat(RDF, xsd_datatype) - end - - defp datatype_module_body(xsd_datatype) do - [ - quote do - @behaviour RDF.Literal.Datatype - - def new(value, opts \\ []) do - %Literal{literal: unquote(xsd_datatype).new(value, opts)} - end - - def new!(value, opts \\ []) do - %Literal{literal: unquote(xsd_datatype).new!(value, opts)} - end - - @impl RDF.Literal.Datatype - def literal_type, do: unquote(xsd_datatype) - - @impl RDF.Literal.Datatype - defdelegate name, to: unquote(xsd_datatype) - - @impl RDF.Literal.Datatype - defdelegate id, to: unquote(xsd_datatype) - - @iri RDF.IRI.new(unquote(xsd_datatype).id()) - @impl RDF.Literal.Datatype - def datatype(%Literal{literal: literal}), do: datatype(literal) - def datatype(%unquote(xsd_datatype){}), do: @iri - - @impl RDF.Literal.Datatype - def language(%Literal{literal: literal}), do: language(literal) - def language(%unquote(xsd_datatype){}), do: nil - - @impl RDF.Literal.Datatype - def value(%Literal{literal: literal}), do: value(literal) - def value(%unquote(xsd_datatype){} = literal), do: unquote(xsd_datatype).value(literal) - - @impl RDF.Literal.Datatype - def lexical(%Literal{literal: literal}), do: lexical(literal) - def lexical(%unquote(xsd_datatype){} = literal), do: unquote(xsd_datatype).lexical(literal) - - @impl RDF.Literal.Datatype - def canonical(%Literal{literal: %unquote(xsd_datatype){} = typed_literal} = literal), - do: %Literal{literal | literal: unquote(xsd_datatype).canonical(typed_literal)} - def canonical(%unquote(xsd_datatype){} = literal), do: canonical(%Literal{literal: literal}) - - @impl RDF.Literal.Datatype - def canonical?(%Literal{literal: literal}), do: canonical?(literal) - def canonical?(%unquote(xsd_datatype){} = literal), do: unquote(xsd_datatype).canonical?(literal) - - @impl RDF.Literal.Datatype - def valid?(%Literal{literal: literal}), do: valid?(literal) - def valid?(%unquote(xsd_datatype){} = literal), do: unquote(xsd_datatype).valid?(literal) - def valid?(_), do: false - - @impl RDF.Literal.Datatype - def cast(%Literal{literal: %unquote(xsd_datatype){}} = literal) do - if valid?(literal), do: literal - end - def cast(%Literal{literal: literal}) do - if casted_literal = unquote(xsd_datatype).cast(literal) do - %Literal{literal: casted_literal} - end - end - def cast(nil), do: nil - def cast(value), do: value |> Literal.new() |> cast() - - @impl RDF.Literal.Datatype - def equal_value?(left, %Literal{literal: right}), do: equal_value?(left, right) - def equal_value?(%Literal{literal: left}, right), do: equal_value?(left, right) - def equal_value?(%unquote(xsd_datatype){} = left, right) do - unquote(xsd_datatype).equal_value?(left, right) - end - def equal_value?(_, _), do: nil - - @impl RDF.Literal.Datatype - @dialyzer {:nowarn_function, compare: 2} # TODO: Why is this warning raised - def compare(left, %Literal{literal: right}), do: compare(left, right) - def compare(%Literal{literal: left}, right), do: compare(left, right) - def compare(%unquote(xsd_datatype){} = left, right) do - unquote(xsd_datatype).compare(left, right) - end - - @impl RDF.Literal.Datatype - def update(literal, fun, opts \\ []) - def update(%Literal{literal: literal}, fun, opts), do: update(literal, fun, opts) - def update(%unquote(xsd_datatype){} = literal, fun, opts) do - case Keyword.get(opts, :as) do - :lexical -> lexical(literal) - nil -> value(literal) - end - |> fun.() - |> new() - end - end - | datatype_specific_module_body(xsd_datatype)] - end - - defp datatype_specific_module_body(XSD.Boolean) do - [ - quote do - def ebv(%Literal{literal: literal}), do: ebv(literal) - def ebv(literal) do - if ebv = XSD.Boolean.ebv(literal), do: %Literal{literal: ebv} - end - - def effective(value), do: ebv(value) - - def fn_not(%Literal{literal: literal}), do: fn_not(literal) - def fn_not(literal) do - if result = XSD.Boolean.fn_not(literal), do: %Literal{literal: result} - end - - def logical_and(%Literal{literal: left}, right), do: logical_and(left, right) - def logical_and(left, %Literal{literal: right}), do: logical_and(left, right) - def logical_and(left, right) do - if result = XSD.Boolean.logical_and(left, right), do: %Literal{literal: result} - end - - def logical_or(%Literal{literal: left}, right), do: logical_or(left, right) - def logical_or(left, %Literal{literal: right}), do: logical_or(left, right) - def logical_or(left, right) do - if result = XSD.Boolean.logical_or(left, right), do: %Literal{literal: result} - end - end - ] - end - - defp datatype_specific_module_body(XSD.DateTime) do - [ - quote do - def now(), do: XSD.DateTime.now() |> new() - end - ] - end - - defp datatype_specific_module_body(_), do: [] -end diff --git a/lib/rdf/literal/helper_macros.ex b/lib/rdf/literal/helper_macros.ex deleted file mode 100644 index 3903f17..0000000 --- a/lib/rdf/literal/helper_macros.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule RDF.Literal.Helper.Macros do - @moduledoc false - - defmacro defdelegate_to_rdf_datatype(fun_name) do - quote do - def unquote(fun_name)(%__MODULE__{literal: %datatype{}} = literal) do - apply(RDF.Literal.Datatype.Registry.rdf_datatype(datatype), unquote(fun_name), [literal]) - end - end - end -end diff --git a/lib/rdf/ns.ex b/lib/rdf/ns.ex index d1dfd75..4e45211 100644 --- a/lib/rdf/ns.ex +++ b/lib/rdf/ns.ex @@ -13,14 +13,9 @@ defmodule RDF.NS do use RDF.Vocabulary.Namespace - @vocabdoc """ - The XML Schema datatypes vocabulary. - - See - """ - defvocab XSD, - base_iri: "http://www.w3.org/2001/XMLSchema#", - terms: RDF.Literal.Datatype.NS.XSD.__terms__ + # This is needed to ensure that the Turtle compiler is compiled and ready to be used to parse vocabularies. + # Without this we randomly get "unable to detect serialization format" errors depending on the parallel compilation order. + require RDF.Turtle @vocabdoc """ The RDF vocabulary. @@ -62,4 +57,59 @@ defmodule RDF.NS do base_iri: "http://www.w3.org/2004/02/skos/core#", file: "skos.ttl" + @vocabdoc """ + The XML Schema datatypes vocabulary. + + See + """ + defvocab XSD, + base_iri: "http://www.w3.org/2001/XMLSchema#", + terms: ~w[ + string + normalizedString + token + language + Name + NCName + ID + IDREF + IDREFS + ENTITY + ENTITIES + NMTOKEN + NMTOKENS + boolean + float + double + decimal + integer + long + int + short + byte + nonPositiveInteger + negativeInteger + nonNegativeInteger + positiveInteger + unsignedLong + unsignedInt + unsignedShort + unsignedByte + duration + dayTimeDuration + yearMonthDuration + dateTime + time + date + gYearMonth + gYear + gMonthDay + gDay + gMonth + base64Binary + hexBinary + anyURI + QName + NOTATION + ] end diff --git a/lib/rdf/serializations/ntriples_encoder.ex b/lib/rdf/serializations/ntriples_encoder.ex index 021225a..9f5ae06 100644 --- a/lib/rdf/serializations/ntriples_encoder.ex +++ b/lib/rdf/serializations/ntriples_encoder.ex @@ -3,7 +3,7 @@ defmodule RDF.NTriples.Encoder do use RDF.Serialization.Encoder - alias RDF.{BlankNode, Dataset, Graph, IRI, Literal, Statement, Triple, LangString} + alias RDF.{BlankNode, Dataset, Graph, IRI, XSD, Literal, Statement, Triple, LangString} @impl RDF.Serialization.Encoder @callback encode(Graph.t | Dataset.t, keyword | map) :: {:ok, String.t} | {:error, any} diff --git a/lib/rdf/serializations/turtle_encoder.ex b/lib/rdf/serializations/turtle_encoder.ex index 11772d7..47e5c98 100644 --- a/lib/rdf/serializations/turtle_encoder.ex +++ b/lib/rdf/serializations/turtle_encoder.ex @@ -4,7 +4,7 @@ defmodule RDF.Turtle.Encoder do use RDF.Serialization.Encoder alias RDF.Turtle.Encoder.State - alias RDF.{BlankNode, Dataset, Description, Graph, IRI, Literal, LangString} + alias RDF.{BlankNode, Dataset, Description, Graph, IRI, XSD, Literal, LangString} @indentation_char " " @indentation 4 diff --git a/lib/rdf/term.ex b/lib/rdf/term.ex index 22c3e9a..9852bf1 100644 --- a/lib/rdf/term.ex +++ b/lib/rdf/term.ex @@ -28,7 +28,7 @@ defprotocol RDF.Term do false iex> RDF.Term.term?(RDF.bnode) true - iex> RDF.Term.term?(RDF.integer(42)) + iex> RDF.Term.term?(RDF.XSD.integer(42)) true iex> RDF.Term.term?(42) false @@ -68,7 +68,7 @@ defprotocol RDF.Term do iex> RDF.Term.coerce("foo") ~L"foo" iex> RDF.Term.coerce(42) - RDF.integer(42) + RDF.XSD.integer(42) """ def coerce(value) @@ -84,7 +84,7 @@ defprotocol RDF.Term do "http://example.com/" iex> RDF.Term.value(~L"foo") "foo" - iex> RDF.integer(42) |> RDF.Term.value() + iex> RDF.XSD.integer(42) |> RDF.Term.value() 42 """ @@ -119,7 +119,7 @@ defimpl RDF.Term, for: Reference do end defimpl RDF.Term, for: RDF.Literal do - def equal?(term1, term2), do: term1 == term2 + def equal?(term1, term2), do: RDF.Literal.equal?(term1, term2) def equal_value?(term1, term2), do: RDF.Literal.equal_value?(term1, term2) def coerce(term), do: term def value(term), do: RDF.Literal.value(term) || RDF.Literal.lexical(term) @@ -132,8 +132,8 @@ defimpl RDF.Term, for: Atom do def equal_value?(nil, _), do: nil def equal_value?(term1, term2), do: RDF.Term.equal_value?(coerce(term1), term2) - def coerce(true), do: RDF.true - def coerce(false), do: RDF.false + def coerce(true), do: RDF.XSD.true + def coerce(false), do: RDF.XSD.false def coerce(_), do: nil def value(true), do: true diff --git a/lib/rdf/utils/bootstrapping.ex b/lib/rdf/utils/bootstrapping.ex index 79de5d4..6600f1f 100644 --- a/lib/rdf/utils/bootstrapping.ex +++ b/lib/rdf/utils/bootstrapping.ex @@ -3,14 +3,17 @@ defmodule RDF.Utils.Bootstrapping do This module holds functions to circumvent circular dependency problems. """ + @xsd_base_iri "http://www.w3.org/2001/XMLSchema#" @rdf_base_iri "http://www.w3.org/1999/02/22-rdf-syntax-ns#" @rdfs_base_iri "http://www.w3.org/2000/01/rdf-schema#" @owl_base_iri "http://www.w3.org/2002/07/owl#" + def xsd_iri_base(), do: RDF.IRI.new(@xsd_base_iri) def rdf_iri_base(), do: RDF.IRI.new(@rdf_base_iri) def rdfs_iri_base(), do: RDF.IRI.new(@rdfs_base_iri) def owl_iri_base(), do: RDF.IRI.new(@owl_base_iri) + def xsd_iri(term), do: RDF.IRI.new(@xsd_base_iri <> term) def rdf_iri(term), do: RDF.IRI.new(@rdf_base_iri <> term) def rdfs_iri(term), do: RDF.IRI.new(@rdfs_base_iri <> term) def owl_iri(term), do: RDF.IRI.new(@owl_base_iri <> term) diff --git a/lib/rdf/xsd.ex b/lib/rdf/xsd.ex new file mode 100644 index 0000000..62cf8bb --- /dev/null +++ b/lib/rdf/xsd.ex @@ -0,0 +1,83 @@ +defmodule RDF.XSD do + @moduledoc """ + An implementation of the XML Schema (XSD) datatype system for use within `RDF.Literal.Datatype` system. + + see + """ + + alias __MODULE__ + alias RDF.{IRI, Literal} + + @datatypes [ + XSD.Boolean, + XSD.String, + XSD.Date, + XSD.Time, + XSD.DateTime, + XSD.AnyURI + ] + |> MapSet.new() + |> MapSet.union(XSD.Numeric.datatypes()) + + @facets [ + XSD.Facets.MinInclusive, + XSD.Facets.MaxInclusive + ] + + @doc """ + The list of all XSD facets. + """ + @spec facets() :: Enum.t() + def facets(), do: @facets + + @facets_by_name Map.new(@facets, fn facet -> {facet.name(), facet} end) + + def facet(name) when is_atom(name), do: @facets_by_name[to_string(name)] + def facet(name), do: @facets_by_name[name] + + @doc """ + The list of all XSD datatypes. + """ + @spec datatypes() :: Enum.t() + def datatypes(), do: @datatypes + + @datatypes_by_name Map.new(@datatypes, fn datatype -> {datatype.name(), datatype} end) + @datatypes_by_iri Map.new(@datatypes, fn datatype -> {datatype.id(), datatype} end) + + def datatype_by_name(name) when is_atom(name), do: @datatypes_by_name[to_string(name)] + def datatype_by_name(name), do: @datatypes_by_name[name] + def datatype_by_iri(iri) when is_binary(iri), do: @datatypes_by_iri[IRI.new(iri)] + def datatype_by_iri(%IRI{} = iri), do: @datatypes_by_iri[iri] + + @doc """ + Returns if a given datatype is a XSD datatype. + """ + def datatype?(datatype), do: datatype in @datatypes + + @doc """ + Returns if a given argument is a `RDF.XSD.datatype` literal. + """ + def literal?(%Literal{literal: %datatype{}}), do: datatype?(datatype) + def literal?(%datatype{}), do: datatype?(datatype) + def literal?(_), do: false + + @doc false + def valid?(%datatype{} = datatype_literal), do: datatype.valid?(datatype_literal) + + for datatype <- @datatypes do + defdelegate unquote(String.to_atom(datatype.name))(value), to: datatype, as: :new + defdelegate unquote(String.to_atom(datatype.name))(value, opts), to: datatype, as: :new + + elixir_name = Macro.underscore(datatype.name) + unless datatype.name == elixir_name do + defdelegate unquote(String.to_atom(elixir_name))(value), to: datatype, as: :new + defdelegate unquote(String.to_atom(elixir_name))(value, opts), to: datatype, as: :new + end + end + + defdelegate datetime(value), to: XSD.DateTime, as: :new + defdelegate datetime(value, opts), to: XSD.DateTime, as: :new + + defdelegate unquote(true)(), to: XSD.Boolean.Value + defdelegate unquote(false)(), to: XSD.Boolean.Value +end diff --git a/lib/rdf/xsd/datatype.ex b/lib/rdf/xsd/datatype.ex new file mode 100644 index 0000000..e7ef2e0 --- /dev/null +++ b/lib/rdf/xsd/datatype.ex @@ -0,0 +1,310 @@ +defmodule RDF.XSD.Datatype do + @moduledoc """ + The behaviour of all XSD datatypes. + """ + + @type t :: module + + @type uncanonical_lexical :: String.t() | nil + + @type literal :: %{ + :__struct__ => t(), + :value => any(), + :uncanonical_lexical => uncanonical_lexical() + } + + @type comparison_result :: :lt | :gt | :eq + + + @doc """ + Returns if the `RDF.XSD.Datatype` is a primitive datatype. + """ + @callback primitive?() :: boolean + + @doc """ + The base datatype from which a `RDF.XSD.Datatype` is derived. + """ + @callback base :: t() | nil + + @doc """ + The primitive `RDF.XSD.Datatype` from which a `RDF.XSD.Datatype` is derived. + + In case of a primitive `RDF.XSD.Datatype` this function returns this `RDF.XSD.Datatype` itself. + """ + @callback base_primitive :: t() + + @doc """ + Checks if a `RDF.XSD.Datatype` is directly or indirectly derived from another `RDF.XSD.Datatype`. + """ + @callback derived_from?(t()) :: boolean + + @doc """ + Checks if the datatype of a given literal is derived from a `RDF.XSD.Datatype`. + """ + @callback derived?(RDF.XSD.Literal.t()) :: boolean + + @doc """ + The set of applicable facets of a `RDF.XSD.Datatype`. + """ + @callback applicable_facets :: [RDF.XSD.Facet.t()] + + @doc """ + A mapping from the lexical space of a `RDF.XSD.Datatype` into its value space. + """ + @callback lexical_mapping(String.t(), Keyword.t()) :: any + + @doc """ + A mapping from Elixir values into the value space of a `RDF.XSD.Datatype`. + """ + @callback elixir_mapping(any, Keyword.t()) :: any | {any, uncanonical_lexical} + + @doc """ + Returns the standard lexical representation for a value of the value space of a `RDF.XSD.Datatype`. + """ + @callback canonical_mapping(any) :: String.t() + + @doc """ + Produces the lexical representation to be used as for a `RDF.XSD.Datatype` literal. + """ + @callback init_valid_lexical(any, uncanonical_lexical, Keyword.t()) :: uncanonical_lexical + + @doc """ + Produces the lexical representation of an invalid value. + + The default implementation of the `_using__` macro just returns `to_string/1` + representation of the value. + """ + @callback init_invalid_lexical(any, Keyword.t()) :: String.t() + + @doc """ + Matches the lexical form of the given `RDF.XSD.Datatype` literal against a XPath and XQuery regular expression pattern. + + The regular expression language is defined in _XQuery 1.0 and XPath 2.0 Functions and Operators_. + + see + """ + @callback matches?(RDF.XSD.Literal.t(), pattern :: String.t()) :: boolean + + @doc """ + Matches the lexical form of the given `RDF.XSD.Datatype` literal against a XPath and XQuery regular expression pattern with flags. + + The regular expression language is defined in _XQuery 1.0 and XPath 2.0 Functions and Operators_. + + see + """ + @callback matches?(RDF.XSD.Literal.t(), pattern :: String.t(), flags :: String.t()) :: boolean + + + defmacro __using__(opts) do + quote do + defstruct [:value, :uncanonical_lexical] + + @behaviour unquote(__MODULE__) + use RDF.Literal.Datatype, unquote(opts) + + @invalid_value nil + + @type invalid_value :: nil + @type value :: valid_value | invalid_value + + @type t :: %__MODULE__{ + value: value, + uncanonical_lexical: RDF.XSD.Datatype.uncanonical_lexical() + } + + @impl unquote(__MODULE__) + def derived_from?(datatype) + + def derived_from?(__MODULE__), do: true + + def derived_from?(datatype) do + base = base() + not is_nil(base) and base.derived_from?(datatype) + end + + @impl unquote(__MODULE__) + def derived?(literal), do: RDF.XSD.Datatype.derived_from?(literal, __MODULE__) + + # Dialyzer causes a warning on all primitives since the facet_conform?/2 call + # always returns true there, so the other branch is unnecessary. This could + # be fixed by generating a special version for primitives, but it's not worth + # maintaining different versions of this function which must be kept in-sync. + @dialyzer {:nowarn_function, new: 2} + @impl RDF.Literal.Datatype + def new(value, opts \\ []) + + def new(lexical, opts) when is_binary(lexical) do + case lexical_mapping(lexical, opts) do + @invalid_value -> + build_invalid(lexical, opts) + + value -> + if facet_conform?(value, lexical) do + build_valid(value, lexical, opts) + else + build_invalid(lexical, opts) + end + end + end + + def new(value, opts) do + case elixir_mapping(value, opts) do + @invalid_value -> + build_invalid(value, opts) + + value -> + {value, lexical} = + case value do + {value, lexical} -> {value, lexical} + value -> {value, nil} + end + + if facet_conform?(value, lexical) do + build_valid(value, lexical, opts) + else + build_invalid(value, opts) + end + end + end + + @impl RDF.Literal.Datatype + def new!(value, opts \\ []) do + literal = new(value, opts) + + if valid?(literal) do + literal + else + raise ArgumentError, "#{inspect(value)} is not a valid #{inspect(__MODULE__)}" + end + end + + @doc false + @spec build_valid(any, RDF.XSD.Datatype.uncanonical_lexical(), Keyword.t()) :: t() + def build_valid(value, lexical, opts) do + if Keyword.get(opts, :canonicalize) do + literal(%__MODULE__{value: value}) + else + initial_lexical = init_valid_lexical(value, lexical, opts) + + literal(%__MODULE__{ + value: value, + uncanonical_lexical: + if(initial_lexical && initial_lexical != canonical_mapping(value), + do: initial_lexical + ) + }) + end + end + + defp build_invalid(lexical, opts) do + literal(%__MODULE__{uncanonical_lexical: init_invalid_lexical(lexical, opts)}) + end + + def cast(literal_or_value) + def cast(%RDF.Literal{literal: literal}), do: cast(literal) + # Invalid values can not be casted in general + def cast(%{value: @invalid_value}), do: nil + def cast(%__MODULE__{} = datatype_literal), do: literal(datatype_literal) + def cast(nil), do: nil + def cast(value) do + case do_cast(value) do + %__MODULE__{} = literal -> if valid?(literal), do: literal(literal) + %RDF.Literal{literal: %__MODULE__{}} = literal -> if valid?(literal), do: literal + _ -> nil + end + end + + @impl RDF.Literal.Datatype + def value(%RDF.Literal{literal: literal}), do: value(literal) + def value(%__MODULE__{} = literal), do: literal.value + + @impl RDF.Literal.Datatype + def lexical(lexical) + + def lexical(%RDF.Literal{literal: literal}), do: lexical(literal) + + def lexical(%__MODULE__{value: value, uncanonical_lexical: nil}), + do: canonical_mapping(value) + + def lexical(%__MODULE__{uncanonical_lexical: lexical}), do: lexical + + @impl RDF.Literal.Datatype + @spec canonical(t()) :: t() + def canonical(literal) + + def canonical(%RDF.Literal{literal: %__MODULE__{uncanonical_lexical: nil}} = literal), + do: literal + + def canonical(%RDF.Literal{literal: %__MODULE__{value: @invalid_value}} = literal), + do: literal + + def canonical(%RDF.Literal{literal: %__MODULE__{} = literal}), + do: canonical(literal) + + def canonical(%__MODULE__{} = literal), + do: literal(%__MODULE__{literal | uncanonical_lexical: nil}) + + @impl RDF.Literal.Datatype + def canonical?(literal) + def canonical?(%RDF.Literal{literal: literal}), do: canonical?(literal) + def canonical?(%__MODULE__{uncanonical_lexical: nil}), do: true + def canonical?(%__MODULE__{}), do: false + + @impl RDF.Literal.Datatype + def valid?(literal) + def valid?(%RDF.Literal{literal: literal}), do: valid?(literal) + def valid?(%__MODULE__{value: @invalid_value}), do: false + def valid?(%__MODULE__{}), do: true + def valid?(_), do: false + + def canonical_lexical(literal) + def canonical_lexical(%RDF.Literal{literal: literal}), do: canonical_lexical(literal) + def canonical_lexical(%__MODULE__{value: nil}), do: nil + + def canonical_lexical(%__MODULE__{value: value, uncanonical_lexical: nil}), + do: canonical_mapping(value) + + def canonical_lexical(%__MODULE__{} = literal), + do: literal |> canonical() |> lexical() + + def canonical_lexical(_), do: nil + + @doc """ + Matches the string representation of the given value against a XPath and XQuery regular expression pattern. + + The regular expression language is defined in _XQuery 1.0 and XPath 2.0 Functions and Operators_. + + see + """ + @impl RDF.XSD.Datatype + def matches?(literal, pattern, flags \\ "") + def matches?(%RDF.Literal{literal: literal}, pattern, flags), do: matches?(literal, pattern, flags) + def matches?(%__MODULE__{} = literal, pattern, flags) do + literal + |> lexical() + |> RDF.XSD.Utils.Regex.matches?(pattern, flags) + end + + defimpl Inspect do + "Elixir.Inspect." <> datatype_name = to_string(__MODULE__) + @datatype_name datatype_name + + def inspect(literal, _opts) do + "%#{@datatype_name}{value: #{inspect(literal.value)}, lexical: #{ + literal |> literal.__struct__.lexical() |> inspect() + }}" + end + end + end + end + + @spec base_primitive(t()) :: t() + def base_primitive(%RDF.Literal{literal: literal}), do: base_primitive(literal) + def base_primitive(%datatype{}), do: base_primitive(datatype) + def base_primitive(datatype), do: datatype.base_primitive() + + @spec derived_from?(t() | literal() | RDF.Literal.t(), t()) :: boolean + def derived_from?(%RDF.Literal{literal: literal}, super_datatype), do: derived_from?(literal, super_datatype) + def derived_from?(%datatype{}, super_datatype), do: derived_from?(datatype, super_datatype) + def derived_from?(datatype, super_datatype) when is_atom(datatype), do: datatype.derived_from?(super_datatype) +end diff --git a/lib/rdf/xsd/datatype/primitive.ex b/lib/rdf/xsd/datatype/primitive.ex new file mode 100644 index 0000000..822a389 --- /dev/null +++ b/lib/rdf/xsd/datatype/primitive.ex @@ -0,0 +1,105 @@ +defmodule RDF.XSD.Datatype.Primitive do + defmacro def_applicable_facet(facet) do + quote do + @applicable_facets unquote(facet) + use unquote(facet) + end + end + + defmacro __using__(opts) do + quote do + use RDF.XSD.Datatype, unquote(opts) + + import unquote(__MODULE__) + + Module.register_attribute(__MODULE__, :applicable_facets, accumulate: true) + + @impl RDF.XSD.Datatype + def primitive?, do: true + + @impl RDF.XSD.Datatype + def base, do: nil + + @impl RDF.XSD.Datatype + def base_primitive, do: __MODULE__ + + @impl RDF.XSD.Datatype + def init_valid_lexical(value, lexical, opts) + def init_valid_lexical(_value, nil, _opts), do: nil + def init_valid_lexical(_value, lexical, _opts), do: lexical + + @impl RDF.XSD.Datatype + def init_invalid_lexical(value, _opts), do: to_string(value) + + @doc false + # Optimization: facets are generally unconstrained on primitives + def facet_conform?(_, _), do: true + + @impl RDF.XSD.Datatype + def canonical_mapping(value), do: to_string(value) + + @impl RDF.Literal.Datatype + def do_cast(value) do + if RDF.XSD.literal?(value) do + if derived?(value) do + build_valid(value.value, value.uncanonical_lexical, []) + end + else + value |> RDF.Literal.coerce() |> cast() + end + end + + @impl RDF.Literal.Datatype + def do_equal_value?(left, right) + + def do_equal_value?( + %datatype{uncanonical_lexical: lexical1, value: nil}, + %datatype{uncanonical_lexical: lexical2, value: nil} + ) do + lexical1 == lexical2 + end + + def do_equal_value?(%datatype{} = literal1, %datatype{} = literal2) do + literal1 |> datatype.canonical() |> datatype.value() == + literal2 |> datatype.canonical() |> datatype.value() + end + + def do_equal_value?(_, _), do: nil + + @impl RDF.Literal.Datatype + def compare(left, right) + def compare(left, %RDF.Literal{literal: right}), do: compare(left, right) + def compare(%RDF.Literal{literal: left}, right), do: compare(left, right) + + def compare( + %__MODULE__{value: left_value} = left, + %__MODULE__{value: right_value} = right + ) + when not (is_nil(left_value) or is_nil(right_value)) do + case {left |> canonical() |> value(), right |> canonical() |> value()} do + {value1, value2} when value1 < value2 -> :lt + {value1, value2} when value1 > value2 -> :gt + _ -> if equal_value?(left, right), do: :eq + end + end + + def compare(_, _), do: nil + + defoverridable canonical_mapping: 1, + do_cast: 1, + init_valid_lexical: 3, + init_invalid_lexical: 2, + do_equal_value?: 2, + compare: 2 + + @before_compile unquote(__MODULE__) + end + end + + defmacro __before_compile__(_env) do + quote do + @impl RDF.XSD.Datatype + def applicable_facets, do: @applicable_facets + end + end +end diff --git a/lib/rdf/xsd/datatype/restriction.ex b/lib/rdf/xsd/datatype/restriction.ex new file mode 100644 index 0000000..55b1214 --- /dev/null +++ b/lib/rdf/xsd/datatype/restriction.ex @@ -0,0 +1,99 @@ +defmodule RDF.XSD.Datatype.Restriction do + defmacro __using__(opts) do + base = Keyword.fetch!(opts, :base) + + quote do + use RDF.XSD.Datatype, unquote(opts) + + import RDF.XSD.Facet, only: [def_facet_constraint: 2] + + @type valid_value :: unquote(base).valid_value() + + @base unquote(base) + @impl RDF.XSD.Datatype + @spec base :: RDF.XSD.Datatype.t() + def base, do: @base + + @impl RDF.XSD.Datatype + def primitive?, do: false + + @impl RDF.XSD.Datatype + def base_primitive, do: @base.base_primitive() + + @impl RDF.XSD.Datatype + def applicable_facets, do: @base.applicable_facets() + + @impl RDF.XSD.Datatype + def init_valid_lexical(value, lexical, opts), + do: @base.init_valid_lexical(value, lexical, opts) + + @impl RDF.XSD.Datatype + def init_invalid_lexical(value, opts), + do: @base.init_invalid_lexical(value, opts) + + @doc false + def facet_conform?(value, lexical) do + Enum.all?(applicable_facets(), fn facet -> + facet.conform?(__MODULE__, value, lexical) + end) + end + + @impl RDF.XSD.Datatype + def lexical_mapping(lexical, opts), + do: @base.lexical_mapping(lexical, opts) + + @impl RDF.XSD.Datatype + def elixir_mapping(value, opts), + do: @base.elixir_mapping(value, opts) + + @impl RDF.XSD.Datatype + def canonical_mapping(value), + do: @base.canonical_mapping(value) + + @impl RDF.Literal.Datatype + def do_cast(literal_or_value) do + # Note: This direct call of the cast/1 implementation of the base_primitive + # is an optimization to not have go through the whole derivation chain and + # doing potentially a lot of redundant validations, but this relies on + # cast/1 not being overridden on restrictions. + case base_primitive().cast(literal_or_value) do + nil -> + nil + + %RDF.Literal{literal: %{value: value, uncanonical_lexical: lexical}} -> + if facet_conform?(value, lexical) do + build_valid(value, lexical, []) + end + end + end + + # TODO: This makes it impossible to define do_equal_value definitions on derivations, + # but we need to overwrite this to reach for example the XSD.Numeric delegation. + def equal_value?(literal1, literal2), do: @base.equal_value?(literal1, literal2) + + @impl RDF.Literal.Datatype + def do_equal_value?(left, right), do: nil # unused; see comment on equal_value?/2 + + @impl RDF.Literal.Datatype + def compare(left, right), do: @base.compare(left, right) + + defoverridable canonical_mapping: 1, + do_cast: 1, + equal_value?: 2, + compare: 2 + + Module.register_attribute(__MODULE__, :facets, accumulate: true) + + @before_compile unquote(__MODULE__) + end + end + + defmacro __before_compile__(env) do + import RDF.XSD.Facet + + restriction_impl( + Module.get_attribute(env.module, :facets), + Module.get_attribute(env.module, :base).applicable_facets + ) + end +end diff --git a/lib/rdf/xsd/datatypes/any_uri.ex b/lib/rdf/xsd/datatypes/any_uri.ex new file mode 100644 index 0000000..3a64680 --- /dev/null +++ b/lib/rdf/xsd/datatypes/any_uri.ex @@ -0,0 +1,22 @@ +defmodule RDF.XSD.AnyURI do + @moduledoc """ + `RDF.XSD.Datatype` for XSD anyURIs. + + See: + """ + + @type valid_value :: URI.t() + + use RDF.XSD.Datatype.Primitive, + name: "anyURI", + id: RDF.Utils.Bootstrapping.xsd_iri("anyURI") + + @impl RDF.XSD.Datatype + @spec lexical_mapping(String.t(), Keyword.t()) :: valid_value + def lexical_mapping(lexical, _), do: URI.parse(lexical) + + @impl RDF.XSD.Datatype + @spec elixir_mapping(any, Keyword.t()) :: value + def elixir_mapping(%URI{} = uri, _), do: uri + def elixir_mapping(_, _), do: @invalid_value +end diff --git a/lib/rdf/xsd/datatypes/boolean.ex b/lib/rdf/xsd/datatypes/boolean.ex new file mode 100644 index 0000000..6e012f1 --- /dev/null +++ b/lib/rdf/xsd/datatypes/boolean.ex @@ -0,0 +1,232 @@ +defmodule RDF.XSD.Boolean do + @moduledoc """ + `RDF.XSD.Datatype` for XSD booleans. + """ + + @type valid_value :: boolean + @type input_value :: RDF.XSD.Literal.t() | valid_value | number | String.t() | any + + use RDF.XSD.Datatype.Primitive, + name: "boolean", + id: RDF.Utils.Bootstrapping.xsd_iri("boolean") + + @impl RDF.XSD.Datatype + def lexical_mapping(lexical, _) do + with lexical do + cond do + lexical in ~W[true 1] -> true + lexical in ~W[false 0] -> false + true -> @invalid_value + end + end + end + + @impl RDF.XSD.Datatype + @spec elixir_mapping(valid_value | integer | any, Keyword.t()) :: value + def elixir_mapping(value, _) + def elixir_mapping(value, _) when is_boolean(value), do: value + def elixir_mapping(1, _), do: true + def elixir_mapping(0, _), do: false + def elixir_mapping(_, _), do: @invalid_value + + @impl RDF.Literal.Datatype + def do_cast(value) + + def do_cast(%RDF.XSD.String{} = xsd_string) do + xsd_string.value |> new() |> canonical() + end + + def do_cast(%RDF.XSD.Decimal{} = xsd_decimal) do + !Decimal.equal?(xsd_decimal.value, 0) |> new() + end + + def do_cast(literal_or_value) do + if RDF.XSD.Numeric.literal?(literal_or_value) do + new(literal_or_value.value not in [0, 0.0, :nan]) + else + super(literal_or_value) + end + end + + @doc """ + Returns an Effective Boolean Value (EBV). + + The Effective Boolean Value is an algorithm to coerce values to a `RDF.XSD.Boolean`. + + It is specified and used in the SPARQL query language and is based upon XPath's + `fn:boolean`. Other than specified in these specs any value which can not be + converted into a boolean results in `nil`. + + see + - + - + + """ + @spec ebv(input_value) :: t() | nil + def ebv(value) + + def ebv(%RDF.Literal{literal: literal}), do: ebv(literal) + + def ebv(true), do: RDF.XSD.Boolean.Value.true() + def ebv(false), do: RDF.XSD.Boolean.Value.false() + + def ebv(%__MODULE__{value: nil}), do: RDF.XSD.Boolean.Value.false() + def ebv(%__MODULE__{} = value), do: literal(value) + + def ebv(%RDF.XSD.String{} = string) do + if String.length(string.value) == 0, + do: RDF.XSD.Boolean.Value.false(), + else: RDF.XSD.Boolean.Value.true() + end + + def ebv(%datatype{} = literal) do + if RDF.XSD.Numeric.datatype?(datatype) do + if datatype.valid?(literal) and + not (literal.value == 0 or literal.value == :nan), + do: RDF.XSD.Boolean.Value.true(), + else: RDF.XSD.Boolean.Value.false() + end + end + + def ebv(value) when is_binary(value) or is_number(value) do + value |> RDF.Literal.coerce() |> ebv() + end + + def ebv(_), do: nil + + @doc """ + Alias for `ebv/1`. + """ + @spec effective(input_value) :: t() | nil + def effective(value), do: ebv(value) + + @doc """ + Returns `RDF.XSD.true` if the effective boolean value of the given argument is `RDF.XSD.false`, or `RDF.XSD.false` if it is `RDF.XSD.true`. + + Otherwise it returns `nil`. + + ## Examples + + iex> RDF.XSD.Boolean.fn_not(RDF.XSD.true) + RDF.XSD.false + iex> RDF.XSD.Boolean.fn_not(RDF.XSD.false) + RDF.XSD.true + + iex> RDF.XSD.Boolean.fn_not(true) + RDF.XSD.false + iex> RDF.XSD.Boolean.fn_not(false) + RDF.XSD.true + + iex> RDF.XSD.Boolean.fn_not(42) + RDF.XSD.false + iex> RDF.XSD.Boolean.fn_not("") + RDF.XSD.true + + iex> RDF.XSD.Boolean.fn_not(nil) + nil + + see + """ + @spec fn_not(input_value) :: t() | nil + def fn_not(value) + def fn_not(%RDF.Literal{literal: literal}), do: fn_not(literal) + def fn_not(value) do + case ebv(value) do + %RDF.Literal{literal: %__MODULE__{value: true}} -> RDF.XSD.Boolean.Value.false() + %RDF.Literal{literal: %__MODULE__{value: false}} -> RDF.XSD.Boolean.Value.true() + nil -> nil + end + end + + @doc """ + Returns the logical `AND` of the effective boolean value of the given arguments. + + It returns `nil` if only one argument is `nil` and the other argument is + `RDF.XSD.true` and `RDF.XSD.false` if the other argument is `RDF.XSD.false`. + + ## Examples + + iex> RDF.XSD.Boolean.logical_and(RDF.XSD.true, RDF.XSD.true) + RDF.XSD.true + iex> RDF.XSD.Boolean.logical_and(RDF.XSD.true, RDF.XSD.false) + RDF.XSD.false + + iex> RDF.XSD.Boolean.logical_and(RDF.XSD.true, nil) + nil + iex> RDF.XSD.Boolean.logical_and(nil, RDF.XSD.false) + RDF.XSD.false + iex> RDF.XSD.Boolean.logical_and(nil, nil) + nil + + see + + """ + @spec logical_and(input_value, input_value) :: t() | nil + def logical_and(left, right) + def logical_and(%RDF.Literal{literal: left}, right), do: logical_and(left, right) + def logical_and(left, %RDF.Literal{literal: right}), do: logical_and(left, right) + def logical_and(left, right) do + case ebv(left) do + %RDF.Literal{literal: %__MODULE__{value: false}} -> + RDF.XSD.Boolean.Value.false() + + %RDF.Literal{literal: %__MODULE__{value: true}} -> + case ebv(right) do + %RDF.Literal{literal: %__MODULE__{value: true}} -> RDF.XSD.Boolean.Value.true() + %RDF.Literal{literal: %__MODULE__{value: false}} -> RDF.XSD.Boolean.Value.false() + nil -> nil + end + + nil -> + if match?(%RDF.Literal{literal: %__MODULE__{value: false}}, ebv(right)) do + RDF.XSD.Boolean.Value.false() + end + end + end + + @doc """ + Returns the logical `OR` of the effective boolean value of the given arguments. + + It returns `nil` if only one argument is `nil` and the other argument is + `RDF.XSD.false` and `RDF.XSD.true` if the other argument is `RDF.XSD.true`. + + ## Examples + + iex> RDF.XSD.Boolean.logical_or(RDF.XSD.true, RDF.XSD.false) + RDF.XSD.true + iex> RDF.XSD.Boolean.logical_or(RDF.XSD.false, RDF.XSD.false) + RDF.XSD.false + + iex> RDF.XSD.Boolean.logical_or(RDF.XSD.true, nil) + RDF.XSD.true + iex> RDF.XSD.Boolean.logical_or(nil, RDF.XSD.false) + nil + iex> RDF.XSD.Boolean.logical_or(nil, nil) + nil + + see + + """ + @spec logical_or(input_value, input_value) :: t() | nil + def logical_or(left, right) + def logical_or(%RDF.Literal{literal: left}, right), do: logical_or(left, right) + def logical_or(left, %RDF.Literal{literal: right}), do: logical_or(left, right) + def logical_or(left, right) do + case ebv(left) do + %RDF.Literal{literal: %__MODULE__{value: true}} -> + RDF.XSD.Boolean.Value.true() + + %RDF.Literal{literal: %__MODULE__{value: false}} -> + case ebv(right) do + %RDF.Literal{literal: %__MODULE__{value: true}} -> RDF.XSD.Boolean.Value.true() + %RDF.Literal{literal: %__MODULE__{value: false}} -> RDF.XSD.Boolean.Value.false() + nil -> nil + end + + nil -> + if match?(%RDF.Literal{literal: %__MODULE__{value: true}}, ebv(right)) do + RDF.XSD.Boolean.Value.true() + end + end + end +end diff --git a/lib/rdf/literal/datatypes/xsd_boolean_values.ex b/lib/rdf/xsd/datatypes/boolean_values.ex similarity index 70% rename from lib/rdf/literal/datatypes/xsd_boolean_values.ex rename to lib/rdf/xsd/datatypes/boolean_values.ex index 667cade..3997cc1 100644 --- a/lib/rdf/literal/datatypes/xsd_boolean_values.ex +++ b/lib/rdf/xsd/datatypes/boolean_values.ex @@ -2,6 +2,8 @@ defmodule RDF.XSD.Boolean.Value do @moduledoc !""" This module holds the two boolean value literals, so they can be accessed directly without needing to construct them every time. + They can't be defined in the RDF.XSD.Boolean module, because we can not use + the `RDF.XSD.Boolean.new` function without having it compiled first. """ @xsd_true RDF.XSD.Boolean.new(true) diff --git a/lib/rdf/xsd/datatypes/byte.ex b/lib/rdf/xsd/datatypes/byte.ex new file mode 100644 index 0000000..b1e4a8d --- /dev/null +++ b/lib/rdf/xsd/datatypes/byte.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.Byte do + use RDF.XSD.Datatype.Restriction, + name: "byte", + id: RDF.Utils.Bootstrapping.xsd_iri("byte"), + base: RDF.XSD.Short + + def_facet_constraint RDF.XSD.Facets.MinInclusive, -128 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 127 +end diff --git a/lib/rdf/xsd/datatypes/date.ex b/lib/rdf/xsd/datatypes/date.ex new file mode 100644 index 0000000..95b94d1 --- /dev/null +++ b/lib/rdf/xsd/datatypes/date.ex @@ -0,0 +1,231 @@ +defmodule RDF.XSD.Date do + @moduledoc """ + `RDF.XSD.Datatype` for XSD date. + + Options: + + - `tz` ... 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") + + + # 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 RDF.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 RDF.XSD.Datatype + @spec elixir_mapping(Date.t() | any, Keyword.t()) :: + value | {value, RDF.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 RDF.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 RDF.XSD.Datatype + @spec init_valid_lexical(valid_value, RDF.XSD.Datatype.uncanonical_lexical(), Keyword.t()) :: + RDF.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 RDF.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(%RDF.XSD.DateTime{} = xsd_datetime) do + case xsd_datetime.value do + %NaiveDateTime{} = datetime -> + datetime + |> NaiveDateTime.to_date() + |> new() + + %DateTime{} = datetime -> + datetime + |> DateTime.to_date() + |> new(tz: RDF.XSD.DateTime.tz(xsd_datetime)) + end + end + + def do_cast(%RDF.XSD.String{} = xsd_string), do: new(xsd_string.value) + + def do_cast(literal_or_value), do: super(literal_or_value) + + @impl RDF.Literal.Datatype + def do_equal_value?(literal1, literal2) + + def do_equal_value?( + %__MODULE__{value: nil, uncanonical_lexical: lexical1}, + %__MODULE__{value: nil, uncanonical_lexical: lexical2} + ) do + lexical1 == lexical2 + end + + def do_equal_value?(%__MODULE__{value: value1}, %__MODULE__{value: value2}) + when is_nil(value1) or is_nil(value2), + do: false + + def do_equal_value?(%__MODULE__{value: value1}, %__MODULE__{value: value2}) do + RDF.XSD.DateTime.equal_value?( + comparison_normalization(value1), + comparison_normalization(value2) + ) + end + + def do_equal_value?(%__MODULE__{}, %RDF.XSD.DateTime{}), do: false + def do_equal_value?(%RDF.XSD.DateTime{}, %__MODULE__{}), do: false + + def do_equal_value?(_, _), do: nil + + @impl RDF.Literal.Datatype + def compare(left, right) + def compare(left, %RDF.Literal{literal: right}), do: compare(left, right) + def compare(%RDF.Literal{literal: left}, right), do: compare(left, right) + + def compare( + %__MODULE__{value: value1}, + %__MODULE__{value: value2} + ) + when is_nil(value1) or is_nil(value2), + do: nil + + def compare( + %__MODULE__{value: value1}, + %__MODULE__{value: value2} + ) do + RDF.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 compare( + # %__MODULE__{value: date_value}, + # %RDF.XSD.DateTime{} = datetime_literal + # ) do + # RDF.XSD.DateTime.compare( + # comparison_normalization(date_value), + # datetime_literal + # ) + # end + # + # def compare( + # %RDF.XSD.DateTime{} = datetime_literal, + # %__MODULE__{value: date_value} + # ) do + # RDF.XSD.DateTime.compare( + # datetime_literal, + # comparison_normalization(date_value) + # ) + # end + + def compare(_, _), do: nil + + defp comparison_normalization({date, tz}) do + (Date.to_iso8601(date) <> "T00:00:00" <> tz) + |> RDF.XSD.DateTime.new() + end + + defp comparison_normalization(%Date{} = date) do + (Date.to_iso8601(date) <> "T00:00:00") + |> RDF.XSD.DateTime.new() + end + + defp comparison_normalization(_), do: nil +end diff --git a/lib/rdf/xsd/datatypes/date_time.ex b/lib/rdf/xsd/datatypes/date_time.ex new file mode 100644 index 0000000..e49f501 --- /dev/null +++ b/lib/rdf/xsd/datatypes/date_time.ex @@ -0,0 +1,223 @@ +defmodule RDF.XSD.DateTime do + @moduledoc """ + `RDF.XSD.Datatype` for XSD dateTimes. + """ + + @type valid_value :: DateTime.t() | NaiveDateTime.t() + + use RDF.XSD.Datatype.Primitive, + name: "dateTime", + id: RDF.Utils.Bootstrapping.xsd_iri("dateTime") + + @impl RDF.XSD.Datatype + def lexical_mapping(lexical, opts) do + case DateTime.from_iso8601(lexical) do + {:ok, datetime, _} -> + elixir_mapping(datetime, opts) + + {:error, :missing_offset} -> + case NaiveDateTime.from_iso8601(lexical) do + {:ok, datetime} -> elixir_mapping(datetime, opts) + _ -> @invalid_value + end + + {:error, :invalid_format} -> + if String.ends_with?(lexical, "-00:00") do + lexical + |> String.replace_trailing("-00:00", "Z") + |> lexical_mapping(opts) + else + @invalid_value + end + + {:error, :invalid_time} -> + if String.contains?(lexical, "T24:00:00") do + with [day, tz] <- String.split(lexical, "T24:00:00", parts: 2), + {:ok, day} <- Date.from_iso8601(day) do + lexical_mapping("#{day |> Date.add(1) |> Date.to_string()}T00:00:00#{tz}", opts) + else + _ -> @invalid_value + end + else + @invalid_value + end + + _ -> + @invalid_value + end + end + + @impl RDF.XSD.Datatype + @spec elixir_mapping(valid_value | any, Keyword.t()) :: value + def elixir_mapping(value, _) + # Special case for date and dateTime, for which 0 is not a valid year + def elixir_mapping(%DateTime{year: 0}, _), do: @invalid_value + def elixir_mapping(%DateTime{} = value, _), do: value + # Special case for date and dateTime, for which 0 is not a valid year + def elixir_mapping(%NaiveDateTime{year: 0}, _), do: @invalid_value + def elixir_mapping(%NaiveDateTime{} = value, _), do: value + def elixir_mapping(_, _), do: @invalid_value + + @impl RDF.XSD.Datatype + @spec canonical_mapping(valid_value) :: String.t() + def canonical_mapping(value) + def canonical_mapping(%DateTime{} = value), do: DateTime.to_iso8601(value) + def canonical_mapping(%NaiveDateTime{} = value), do: NaiveDateTime.to_iso8601(value) + + @impl RDF.Literal.Datatype + def do_cast(value) + + def do_cast(%RDF.XSD.Date{} = xsd_date) do + case xsd_date.value do + {value, zone} -> + (value |> RDF.XSD.Date.new() |> RDF.XSD.Date.canonical_lexical()) <> "T00:00:00" <> zone + + value -> + (value |> RDF.XSD.Date.new() |> RDF.XSD.Date.canonical_lexical()) <> "T00:00:00" + end + |> new() + end + + def do_cast(%RDF.XSD.String{} = xsd_string), do: new(xsd_string.value) + + def do_cast(literal_or_value), do: super(literal_or_value) + + @doc """ + Builds a `RDF.XSD.DateTime` literal for current moment in time. + """ + @spec now() :: t() + def now() do + new(DateTime.utc_now()) + end + + @doc """ + Extracts the timezone string from a `RDF.XSD.DateTime` value. + """ + @spec tz(RDF.Literal.t() | t()) :: String.t() | nil + def tz(xsd_datetime) + def tz(%RDF.Literal{literal: xsd_datetime}), do: tz(xsd_datetime) + def tz(%__MODULE__{value: %NaiveDateTime{}}), do: "" + def tz(date_time_literal) do + if valid?(date_time_literal) do + date_time_literal + |> lexical() + |> RDF.XSD.Utils.DateTime.tz() + end + end + + @doc """ + Converts a datetime 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_datetime}), + do: canonical_lexical_with_zone(xsd_datetime) + def canonical_lexical_with_zone(%__MODULE__{} = xsd_datetime) do + case tz(xsd_datetime) do + nil -> + nil + + zone when zone in ["Z", "", "+00:00", "-00:00"] -> + canonical_lexical(xsd_datetime) + + zone -> + xsd_datetime + |> lexical() + |> String.replace_trailing(zone, "Z") + |> DateTime.from_iso8601() + |> elem(1) + |> new() + |> canonical_lexical() + |> String.replace_trailing("Z", zone) + end + end + + @impl RDF.Literal.Datatype + def do_equal_value?(literal1, literal2) + def do_equal_value?( + %__MODULE__{value: %type{} = value1}, + %__MODULE__{value: %type{} = value2} + ) do + type.compare(value1, value2) == :eq + end + + def do_equal_value?( + %__MODULE__{value: nil, uncanonical_lexical: lexical1}, + %__MODULE__{value: nil, uncanonical_lexical: lexical2} + ) do + lexical1 == lexical2 + end + + # This is another quirk for the open-world test date-2 from the SPARQL 1.0 test suite: + # comparisons between one date with tz and another one without a tz are incomparable + # when the unequal, but comparable and returning false when equal. + # What's the reasoning behind this madness? + def do_equal_value?(%__MODULE__{} = literal1, %__MODULE__{} = literal2) do + case compare(literal1, literal2) do + :lt -> false + :gt -> false + # This actually can't/shouldn't happen. + :eq -> true + _ -> nil + end + end + + def do_equal_value?(%__MODULE__{}, %RDF.XSD.Date{}), do: false + def do_equal_value?(%RDF.XSD.Date{}, %__MODULE__{}), do: false + + def do_equal_value?(_, _), do: nil + + @impl RDF.Literal.Datatype + def compare(left, right) + def compare(left, %RDF.Literal{literal: right}), do: compare(left, right) + def compare(%RDF.Literal{literal: left}, right), do: compare(left, right) + + def compare( + %__MODULE__{value: %type{} = value1}, + %__MODULE__{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(%__MODULE__{} = literal1, %RDF.XSD.Date{} = literal2) do + # RDF.XSD.Date.compare(literal1, literal2) + # end + # + # def compare(%RDF.XSD.Date{} = literal1, %__MODULE__{} = literal2) do + # RDF.XSD.Date.compare(literal1, literal2) + # end + + def compare( + %__MODULE__{value: %DateTime{}} = left, + %__MODULE__{value: %NaiveDateTime{} = right_value} + ) do + cond do + compare(left, new(to_datetime(right_value, "+"))) == :lt -> :lt + compare(left, new(to_datetime(right_value, "-"))) == :gt -> :gt + true -> :indeterminate + end + end + + def compare( + %__MODULE__{value: %NaiveDateTime{} = left}, + %__MODULE__{value: %DateTime{}} = right_literal + ) do + cond do + compare(new(to_datetime(left, "-")), right_literal) == :lt -> :lt + compare(new(to_datetime(left, "+")), right_literal) == :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 diff --git a/lib/rdf/xsd/datatypes/decimal.ex b/lib/rdf/xsd/datatypes/decimal.ex new file mode 100644 index 0000000..835ec07 --- /dev/null +++ b/lib/rdf/xsd/datatypes/decimal.ex @@ -0,0 +1,152 @@ +defmodule RDF.XSD.Decimal do + @moduledoc """ + `RDF.XSD.Datatype` for XSD decimals. + """ + + @type valid_value :: Decimal.t() + + use RDF.XSD.Datatype.Primitive, + name: "decimal", + id: RDF.Utils.Bootstrapping.xsd_iri("decimal") + + alias Elixir.Decimal, as: D + + @impl RDF.XSD.Datatype + def lexical_mapping(lexical, opts) do + if String.contains?(lexical, ~w[e E]) do + @invalid_value + else + case D.parse(lexical) do + {:ok, decimal} -> elixir_mapping(decimal, opts) + :error -> @invalid_value + end + end + end + + @impl RDF.XSD.Datatype + @spec elixir_mapping(valid_value | integer | float | any, Keyword.t()) :: value + def elixir_mapping(value, _) + + def elixir_mapping(%D{coef: coef}, _) when coef in ~w[qNaN sNaN inf]a, + do: @invalid_value + + def elixir_mapping(%D{} = decimal, _), + do: canonical_decimal(decimal) + + def elixir_mapping(value, opts) when is_integer(value), + do: value |> D.new() |> elixir_mapping(opts) + + def elixir_mapping(value, opts) when is_float(value), + do: value |> D.from_float() |> elixir_mapping(opts) + + def elixir_mapping(_, _), do: @invalid_value + + @doc false + @spec canonical_decimal(valid_value) :: valid_value + def canonical_decimal(decimal) + + def canonical_decimal(%D{coef: 0} = decimal), + do: %{decimal | exp: -1} + + def canonical_decimal(%D{coef: coef, exp: 0} = decimal), + do: %{decimal | coef: coef * 10, exp: -1} + + def canonical_decimal(%D{coef: coef, exp: exp} = decimal) + when exp > 0, + do: canonical_decimal(%{decimal | coef: coef * 10, exp: exp - 1}) + + def canonical_decimal(%D{coef: coef} = decimal) + when Kernel.rem(coef, 10) != 0, + do: decimal + + def canonical_decimal(%D{coef: coef, exp: exp} = decimal), + do: canonical_decimal(%{decimal | coef: Kernel.div(coef, 10), exp: exp + 1}) + + @impl RDF.XSD.Datatype + @spec canonical_mapping(valid_value) :: String.t() + def canonical_mapping(value) + + def canonical_mapping(%D{sign: sign, coef: :qNaN}), + do: if(sign == 1, do: "NaN", else: "-NaN") + + def canonical_mapping(%D{sign: sign, coef: :sNaN}), + do: if(sign == 1, do: "sNaN", else: "-sNaN") + + def canonical_mapping(%D{sign: sign, coef: :inf}), + do: if(sign == 1, do: "Infinity", else: "-Infinity") + + def canonical_mapping(%D{} = decimal), + do: D.to_string(decimal, :normal) + + @impl RDF.Literal.Datatype + def do_cast(value) + + def do_cast(%RDF.XSD.Boolean{value: false}), do: new(0.0) + def do_cast(%RDF.XSD.Boolean{value: true}), do: new(1.0) + + def do_cast(%RDF.XSD.String{} = xsd_string) do + xsd_string.value |> new() |> canonical() + end + + def do_cast(%RDF.XSD.Integer{} = xsd_integer), do: new(xsd_integer.value) + def do_cast(%RDF.XSD.Double{value: value}) when is_float(value), do: new(value) + def do_cast(%RDF.XSD.Float{value: value}) when is_float(value), do: new(value) + + def do_cast(literal_or_value), do: super(literal_or_value) + + def equal_value?(left, right), do: RDF.XSD.Numeric.equal_value?(left, right) + + @impl RDF.Literal.Datatype + def compare(left, right), do: RDF.XSD.Numeric.compare(left, right) + + @doc """ + The number of digits in the XML Schema canonical form of the literal value. + """ + @spec digit_count(RDF.XSD.Literal.t()) :: non_neg_integer | nil + def digit_count(%__MODULE__{} = literal), do: do_digit_count(literal) + + def digit_count(literal) do + cond do + RDF.XSD.Integer.derived?(literal) -> RDF.XSD.Integer.digit_count(literal) + derived?(literal) -> do_digit_count(literal) + true -> nil + end + end + + defp do_digit_count(%datatype{} = literal) do + if datatype.valid?(literal) do + literal + |> datatype.canonical() + |> datatype.lexical() + |> String.replace(".", "") + |> String.replace("-", "") + |> String.length() + end + end + + @doc """ + The number of digits to the right of the decimal point in the XML Schema canonical form of the literal value. + """ + @spec fraction_digit_count(RDF.XSD.Literal.t()) :: non_neg_integer | nil + def fraction_digit_count(%__MODULE__{} = literal), do: do_fraction_digit_count(literal) + + def fraction_digit_count(literal) do + cond do + RDF.XSD.Integer.derived?(literal) -> 0 + derived?(literal) -> do_fraction_digit_count(literal) + true -> nil + end + end + + defp do_fraction_digit_count(%datatype{} = literal) do + if datatype.valid?(literal) do + [_, fraction] = + literal + |> datatype.canonical() + |> datatype.lexical() + |> String.split(".") + + String.length(fraction) + end + end +end diff --git a/lib/rdf/xsd/datatypes/double.ex b/lib/rdf/xsd/datatypes/double.ex new file mode 100644 index 0000000..b7861c7 --- /dev/null +++ b/lib/rdf/xsd/datatypes/double.ex @@ -0,0 +1,125 @@ +defmodule RDF.XSD.Double do + @moduledoc """ + `RDF.XSD.Datatype` for XSD doubles. + """ + + @type special_values :: :positive_infinity | :negative_infinity | :nan + @type valid_value :: float | special_values + + use RDF.XSD.Datatype.Primitive, + name: "double", + id: RDF.Utils.Bootstrapping.xsd_iri("double") + + @special_values ~W[positive_infinity negative_infinity nan]a + + @impl RDF.XSD.Datatype + def lexical_mapping(lexical, opts) do + case Float.parse(lexical) do + {float, ""} -> + float + + {float, remainder} -> + # 1.E-8 is not a valid Elixir float literal and consequently not fully parsed with Float.parse + if Regex.match?(~r/^\.e?[\+\-]?\d+$/i, remainder) do + lexical_mapping(to_string(float) <> String.trim_leading(remainder, "."), opts) + else + @invalid_value + end + + :error -> + case String.upcase(lexical) do + "INF" -> :positive_infinity + "-INF" -> :negative_infinity + "NAN" -> :nan + _ -> @invalid_value + end + end + end + + @impl RDF.XSD.Datatype + @spec elixir_mapping(valid_value | integer | any, Keyword.t()) :: value + def elixir_mapping(value, _) + def elixir_mapping(value, _) when is_float(value), do: value + def elixir_mapping(value, _) when is_integer(value), do: value / 1 + def elixir_mapping(value, _) when value in @special_values, do: value + def elixir_mapping(_, _), do: @invalid_value + + @impl RDF.XSD.Datatype + @spec init_valid_lexical(valid_value, RDF.XSD.Datatype.uncanonical_lexical(), Keyword.t()) :: + RDF.XSD.Datatype.uncanonical_lexical() + def init_valid_lexical(value, lexical, opts) + def init_valid_lexical(value, nil, _) when is_atom(value), do: nil + def init_valid_lexical(value, nil, _), do: decimal_form(value) + def init_valid_lexical(_, lexical, _), do: lexical + + defp decimal_form(float), do: to_string(float) + + @impl RDF.XSD.Datatype + @spec canonical_mapping(valid_value) :: String.t() + def canonical_mapping(value) + + # Produces the exponential form of a float + def canonical_mapping(float) when is_float(float) do + # We can't use simple %f transformation due to special requirements from N3 tests in representation + [i, f, e] = + float + |> float_to_string() + |> String.split(~r/[\.e]/) + + # remove any trailing zeroes + f = + case String.replace(f, ~r/0*$/, "", global: false) do + # ...but there must be a digit to the right of the decimal point + "" -> "0" + f -> f + end + + e = String.trim_leading(e, "+") + + "#{i}.#{f}E#{e}" + end + + def canonical_mapping(:nan), do: "NaN" + def canonical_mapping(:positive_infinity), do: "INF" + def canonical_mapping(:negative_infinity), do: "-INF" + + if List.to_integer(:erlang.system_info(:otp_release)) >= 21 do + defp float_to_string(float) do + :io_lib.format("~.15e", [float]) + |> to_string() + end + else + defp float_to_string(float) do + :io_lib.format("~.15e", [float]) + |> List.first() + |> to_string() + end + end + + @impl RDF.Literal.Datatype + def do_cast(value) + + def do_cast(%RDF.XSD.Boolean{value: false}), do: new(0.0) + def do_cast(%RDF.XSD.Boolean{value: true}), do: new(1.0) + + def do_cast(%RDF.XSD.String{} = xsd_string) do + xsd_string.value |> new() |> canonical() + end + + def do_cast(%RDF.XSD.Integer{} = xsd_integer) do + new(xsd_integer.value) + end + + def do_cast(%RDF.XSD.Decimal{} = xsd_decimal) do + xsd_decimal.value + |> Decimal.to_float() + |> new() + end + + def do_cast(literal_or_value), do: super(literal_or_value) + + def equal_value?(left, right), do: RDF.XSD.Numeric.equal_value?(left, right) + + @impl RDF.Literal.Datatype + def compare(left, right), do: RDF.XSD.Numeric.compare(left, right) +end diff --git a/lib/rdf/xsd/datatypes/float.ex b/lib/rdf/xsd/datatypes/float.ex new file mode 100644 index 0000000..6e3108a --- /dev/null +++ b/lib/rdf/xsd/datatypes/float.ex @@ -0,0 +1,13 @@ +defmodule RDF.XSD.Float do + @moduledoc """ + `RDF.XSD.Datatype` for XSD floats. + + Although the XSD spec defines floats as a primitive we derive it here from `XSD.Double` + with any further constraints, since Erlang doesn't support 32-bit floats. + """ + + use RDF.XSD.Datatype.Restriction, + name: "float", + id: RDF.Utils.Bootstrapping.xsd_iri("float"), + base: RDF.XSD.Double +end diff --git a/lib/rdf/xsd/datatypes/int.ex b/lib/rdf/xsd/datatypes/int.ex new file mode 100644 index 0000000..1844084 --- /dev/null +++ b/lib/rdf/xsd/datatypes/int.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.Int do + use RDF.XSD.Datatype.Restriction, + name: "int", + id: RDF.Utils.Bootstrapping.xsd_iri("int"), + base: RDF.XSD.Long + + def_facet_constraint RDF.XSD.Facets.MinInclusive, -2_147_483_648 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 2_147_483_647 +end diff --git a/lib/rdf/xsd/datatypes/integer.ex b/lib/rdf/xsd/datatypes/integer.ex new file mode 100644 index 0000000..05e8135 --- /dev/null +++ b/lib/rdf/xsd/datatypes/integer.ex @@ -0,0 +1,85 @@ +defmodule RDF.XSD.Integer do + @moduledoc """ + `RDF.XSD.Datatype` for XSD integers. + + Although the XSD spec defines integers as derived from `xsd:decimal` we implement + it here as a primitive datatype for simplicity and performance reasons. + """ + + @type valid_value :: integer + + use RDF.XSD.Datatype.Primitive, + name: "integer", + id: RDF.Utils.Bootstrapping.xsd_iri("integer") + + def_applicable_facet RDF.XSD.Facets.MinInclusive + def_applicable_facet RDF.XSD.Facets.MaxInclusive + + def min_inclusive_conform?(min_inclusive, value, _lexical) do + value >= min_inclusive + end + + def max_inclusive_conform?(max_inclusive, value, _lexical) do + value <= max_inclusive + end + + @impl RDF.XSD.Datatype + def lexical_mapping(lexical, _) do + case Integer.parse(lexical) do + {integer, ""} -> integer + {_, _} -> @invalid_value + :error -> @invalid_value + end + end + + @impl RDF.XSD.Datatype + @spec elixir_mapping(valid_value | any, Keyword.t()) :: value + def elixir_mapping(value, _) + def elixir_mapping(value, _) when is_integer(value), do: value + def elixir_mapping(_, _), do: @invalid_value + + @impl RDF.Literal.Datatype + def do_cast(value) + + def do_cast(%RDF.XSD.Boolean{value: false}), do: new(0) + def do_cast(%RDF.XSD.Boolean{value: true}), do: new(1) + + def do_cast(%RDF.XSD.String{} = xsd_string) do + xsd_string.value |> new() |> canonical() + end + + def do_cast(%RDF.XSD.Decimal{} = xsd_decimal) do + xsd_decimal.value + |> Decimal.round(0, :down) + |> Decimal.to_integer() + |> new() + end + + def do_cast(%datatype{value: value}) + when datatype in [RDF.XSD.Double, RDF.XSD.Float] and is_float(value) do + value + |> trunc() + |> new() + end + + def do_cast(literal_or_value), do: super(literal_or_value) + + def equal_value?(left, right), do: RDF.XSD.Numeric.equal_value?(left, right) + + @impl RDF.Literal.Datatype + def compare(left, right), do: RDF.XSD.Numeric.compare(left, right) + + @doc """ + The number of digits in the XML Schema canonical form of the literal value. + """ + @spec digit_count(RDF.XSD.Literal.t()) :: non_neg_integer | nil + def digit_count(%datatype{} = literal) do + if derived?(literal) and datatype.valid?(literal) do + literal + |> datatype.canonical() + |> datatype.lexical() + |> String.replace("-", "") + |> String.length() + end + end +end diff --git a/lib/rdf/xsd/datatypes/long.ex b/lib/rdf/xsd/datatypes/long.ex new file mode 100644 index 0000000..fc6617b --- /dev/null +++ b/lib/rdf/xsd/datatypes/long.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.Long do + use RDF.XSD.Datatype.Restriction, + name: "long", + id: RDF.Utils.Bootstrapping.xsd_iri("long"), + base: RDF.XSD.Integer + + def_facet_constraint RDF.XSD.Facets.MinInclusive, -9_223_372_036_854_775_808 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 9_223_372_036_854_775_807 +end diff --git a/lib/rdf/xsd/datatypes/negative_integer.ex b/lib/rdf/xsd/datatypes/negative_integer.ex new file mode 100644 index 0000000..4bb0c4f --- /dev/null +++ b/lib/rdf/xsd/datatypes/negative_integer.ex @@ -0,0 +1,8 @@ +defmodule RDF.XSD.NegativeInteger do + use RDF.XSD.Datatype.Restriction, + name: "negativeInteger", + id: RDF.Utils.Bootstrapping.xsd_iri("negativeInteger"), + base: RDF.XSD.NonPositiveInteger + + def_facet_constraint RDF.XSD.Facets.MaxInclusive, -1 +end diff --git a/lib/rdf/xsd/datatypes/non_negative_integer.ex b/lib/rdf/xsd/datatypes/non_negative_integer.ex new file mode 100644 index 0000000..3129cdf --- /dev/null +++ b/lib/rdf/xsd/datatypes/non_negative_integer.ex @@ -0,0 +1,8 @@ +defmodule RDF.XSD.NonNegativeInteger do + use RDF.XSD.Datatype.Restriction, + name: "nonNegativeInteger", + id: RDF.Utils.Bootstrapping.xsd_iri("nonNegativeInteger"), + base: RDF.XSD.Integer + + def_facet_constraint RDF.XSD.Facets.MinInclusive, 0 +end diff --git a/lib/rdf/xsd/datatypes/non_positive_integer.ex b/lib/rdf/xsd/datatypes/non_positive_integer.ex new file mode 100644 index 0000000..5048529 --- /dev/null +++ b/lib/rdf/xsd/datatypes/non_positive_integer.ex @@ -0,0 +1,8 @@ +defmodule RDF.XSD.NonPositiveInteger do + use RDF.XSD.Datatype.Restriction, + name: "nonPositiveInteger", + id: RDF.Utils.Bootstrapping.xsd_iri("nonPositiveInteger"), + base: RDF.XSD.Integer + + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 0 +end diff --git a/lib/rdf/xsd/datatypes/numeric.ex b/lib/rdf/xsd/datatypes/numeric.ex new file mode 100644 index 0000000..827545d --- /dev/null +++ b/lib/rdf/xsd/datatypes/numeric.ex @@ -0,0 +1,613 @@ +defmodule RDF.XSD.Numeric do + @moduledoc """ + Collection of functions for numeric literals. + """ + + alias Elixir.Decimal, as: D + + import Kernel, except: [abs: 1, floor: 1, ceil: 1] + + @datatypes MapSet.new([ + RDF.XSD.Decimal, + RDF.XSD.Integer, + RDF.XSD.Long, + RDF.XSD.Int, + RDF.XSD.Short, + RDF.XSD.Byte, + RDF.XSD.NonNegativeInteger, + RDF.XSD.PositiveInteger, + RDF.XSD.UnsignedLong, + RDF.XSD.UnsignedInt, + RDF.XSD.UnsignedShort, + RDF.XSD.UnsignedByte, + RDF.XSD.NonPositiveInteger, + RDF.XSD.NegativeInteger, + RDF.XSD.Double, + RDF.XSD.Float + ]) + + @type t :: + RDF.XSD.Decimal.t() + | RDF.XSD.Integer.t() + | RDF.XSD.Long.t() + | RDF.XSD.Int.t() + | RDF.XSD.Short.t() + | RDF.XSD.Byte.t() + | RDF.XSD.NonNegativeInteger.t() + | RDF.XSD.PositiveInteger.t() + | RDF.XSD.UnsignedLong.t() + | RDF.XSD.UnsignedInt.t() + | RDF.XSD.UnsignedShort.t() + | RDF.XSD.UnsignedByte.t() + | RDF.XSD.NonPositiveInteger.t() + | RDF.XSD.NegativeInteger.t() + | RDF.XSD.Double.t() + | RDF.XSD.Float.t() + + @doc """ + The set of all numeric datatypes. + """ + @spec datatypes() :: Enum.t + def datatypes(), do: @datatypes + + @doc """ + Returns if a given datatype is a numeric datatype. + """ + @spec datatype?(RDF.XSD.Datatype.t() | any) :: boolean + def datatype?(datatype), do: datatype in @datatypes + + @doc """ + Returns if a given XSD literal has a numeric datatype. + """ + @spec literal?(RDF.Literal.t() | any) :: boolean + def literal?(literal) + def literal?(%RDF.Literal{literal: literal}), do: literal?(literal) + def literal?(%datatype{}), do: datatype?(datatype) + def literal?(_), do: false + + @doc """ + Tests for numeric value equality of two numeric XSD datatyped literals. + + see: + + - + - + """ + @spec equal_value?(t() | any, t() | any) :: boolean + def equal_value?(left, right) + def equal_value?(left, %RDF.Literal{literal: right}), do: equal_value?(left, right) + def equal_value?(%RDF.Literal{literal: left}, right), do: equal_value?(left, right) + def equal_value?(nil, _), do: nil + def equal_value?(_, nil), do: nil + + def equal_value?( + %datatype{value: nil, uncanonical_lexical: lexical1}, + %datatype{value: nil, uncanonical_lexical: lexical2} + ) do + lexical1 == lexical2 + end + + def equal_value?(%left_datatype{value: left}, %right_datatype{value: right}) + when left_datatype == RDF.XSD.Decimal or right_datatype == RDF.XSD.Decimal, + do: not is_nil(left) and not is_nil(right) and equal_decimal_value?(left, right) + + def equal_value?(%left_datatype{value: left}, %right_datatype{value: right}) do + if datatype?(left_datatype) and datatype?(right_datatype) do + left != :nan and right != :nan and left == right + end + end + + def equal_value?(left, right), + do: equal_value?(RDF.Literal.coerce(left), RDF.Literal.coerce(right)) + + defp equal_decimal_value?(%D{} = left, %D{} = right), do: D.equal?(left, right) + + defp equal_decimal_value?(%D{} = left, right), + do: equal_decimal_value?(left, new_decimal(right)) + + defp equal_decimal_value?(left, %D{} = right), + do: equal_decimal_value?(new_decimal(left), right) + + defp equal_decimal_value?(_, _), do: false + + defp new_decimal(value) when is_float(value), do: D.from_float(value) + defp new_decimal(value), do: D.new(value) + + @doc """ + Compares two numeric XSD literals. + + Returns `:gt` if first literal is greater than the second and `:lt` for vice + versa. If the two literals are equal `:eq` is returned. + + Returns `nil` when the given arguments are not comparable datatypes. + + """ + @spec compare(t, t) :: RDF.XSD.Datatype.comparison_result() | nil + def compare(left, right) + def compare(left, %RDF.Literal{literal: right}), do: compare(left, right) + def compare(%RDF.Literal{literal: left}, right), do: compare(left, right) + + def compare( + %RDF.XSD.Decimal{value: left}, + %right_datatype{value: right} + ) do + if datatype?(right_datatype) do + compare_decimal_value(left, right) + end + end + + def compare( + %left_datatype{value: left}, + %RDF.XSD.Decimal{value: right} + ) do + if datatype?(left_datatype) do + compare_decimal_value(left, right) + end + end + + def compare( + %left_datatype{value: left}, + %right_datatype{value: right} + ) + when not (is_nil(left) or is_nil(right)) do + if datatype?(left_datatype) and datatype?(right_datatype) do + cond do + left < right -> :lt + left > right -> :gt + true -> :eq + end + end + end + + def compare(_, _), do: nil + + defp compare_decimal_value(%D{} = left, %D{} = right), do: D.cmp(left, right) + + defp compare_decimal_value(%D{} = left, right), + do: compare_decimal_value(left, new_decimal(right)) + + defp compare_decimal_value(left, %D{} = right), + do: compare_decimal_value(new_decimal(left), right) + + defp compare_decimal_value(_, _), do: nil + + @spec zero?(any) :: boolean + def zero?(%RDF.Literal{literal: literal}), do: zero?(literal) + def zero?(%{value: value}), do: zero_value?(value) + defp zero_value?(zero) when zero == 0, do: true + defp zero_value?(%D{coef: 0}), do: true + defp zero_value?(_), do: false + + @spec negative_zero?(any) :: boolean + def negative_zero?(%RDF.Literal{literal: literal}), do: negative_zero?(literal) + def negative_zero?(%{value: zero, uncanonical_lexical: "-" <> _}) when zero == 0, do: true + def negative_zero?(%{value: %D{sign: -1, coef: 0}}), do: true + def negative_zero?(_), do: false + + @doc """ + Adds two numeric literals. + + For `xsd:float` or `xsd:double` values, if one of the operands is a zero or a + finite number and the other is INF or -INF, INF or -INF is returned. If both + operands are INF, INF is returned. If both operands are -INF, -INF is returned. + If one of the operands is INF and the other is -INF, NaN is returned. + + If one of the given arguments is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def add(arg1, arg2) do + arithmetic_operation(:+, arg1, arg2, fn + :positive_infinity, :negative_infinity, _ -> :nan + :negative_infinity, :positive_infinity, _ -> :nan + :positive_infinity, _, _ -> :positive_infinity + _, :positive_infinity, _ -> :positive_infinity + :negative_infinity, _, _ -> :negative_infinity + _, :negative_infinity, _ -> :negative_infinity + %D{} = arg1, %D{} = arg2, _ -> D.add(arg1, arg2) + arg1, arg2, _ -> arg1 + arg2 + end) + end + + @doc """ + Subtracts two numeric literals. + + For `xsd:float` or `xsd:double` values, if one of the operands is a zero or a + finite number and the other is INF or -INF, an infinity of the appropriate sign + is returned. If both operands are INF or -INF, NaN is returned. If one of the + operands is INF and the other is -INF, an infinity of the appropriate sign is + returned. + + If one of the given arguments is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def subtract(arg1, arg2) do + arithmetic_operation(:-, arg1, arg2, fn + :positive_infinity, :positive_infinity, _ -> :nan + :negative_infinity, :negative_infinity, _ -> :nan + :positive_infinity, :negative_infinity, _ -> :positive_infinity + :negative_infinity, :positive_infinity, _ -> :negative_infinity + :positive_infinity, _, _ -> :positive_infinity + _, :positive_infinity, _ -> :negative_infinity + :negative_infinity, _, _ -> :negative_infinity + _, :negative_infinity, _ -> :positive_infinity + %D{} = arg1, %D{} = arg2, _ -> D.sub(arg1, arg2) + arg1, arg2, _ -> arg1 - arg2 + end) + end + + @doc """ + Multiplies two numeric literals. + + For `xsd:float` or `xsd:double` values, if one of the operands is a zero and + the other is an infinity, NaN is returned. If one of the operands is a non-zero + number and the other is an infinity, an infinity with the appropriate sign is + returned. + + If one of the given arguments is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def multiply(arg1, arg2) do + arithmetic_operation(:*, arg1, arg2, fn + :positive_infinity, :negative_infinity, _ -> :nan + :negative_infinity, :positive_infinity, _ -> :nan + inf, zero, _ when inf in [:positive_infinity, :negative_infinity] and zero == 0 -> :nan + zero, inf, _ when inf in [:positive_infinity, :negative_infinity] and zero == 0 -> :nan + :positive_infinity, number, _ when number < 0 -> :negative_infinity + number, :positive_infinity, _ when number < 0 -> :negative_infinity + :positive_infinity, _, _ -> :positive_infinity + _, :positive_infinity, _ -> :positive_infinity + :negative_infinity, number, _ when number < 0 -> :positive_infinity + number, :negative_infinity, _ when number < 0 -> :positive_infinity + :negative_infinity, _, _ -> :negative_infinity + _, :negative_infinity, _ -> :negative_infinity + %D{} = arg1, %D{} = arg2, _ -> D.mult(arg1, arg2) + arg1, arg2, _ -> arg1 * arg2 + end) + end + + @doc """ + Divides two numeric literals. + + For `xsd:float` and `xsd:double` operands, floating point division is performed + as specified in [IEEE 754-2008]. A positive number divided by positive zero + returns INF. A negative number divided by positive zero returns -INF. Division + by negative zero returns -INF and INF, respectively. Positive or negative zero + divided by positive or negative zero returns NaN. Also, INF or -INF divided by + INF or -INF returns NaN. + + If one of the given arguments is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + `nil` is also returned for `xsd:decimal` and `xsd:integer` operands, if the + divisor is (positive or negative) zero. + + see + + """ + def divide(arg1, arg2) do + negative_zero = negative_zero?(arg2) + + arithmetic_operation(:/, arg1, arg2, fn + inf1, inf2, _ + when inf1 in [:positive_infinity, :negative_infinity] and + inf2 in [:positive_infinity, :negative_infinity] -> + :nan + + %D{} = arg1, %D{coef: coef} = arg2, _ -> + unless coef == 0, do: D.div(arg1, arg2) + + arg1, arg2, result_type -> + if zero_value?(arg2) do + cond do + result_type not in [RDF.XSD.Double, RDF.XSD.Float] -> nil + zero_value?(arg1) -> :nan + negative_zero and arg1 < 0 -> :positive_infinity + negative_zero -> :negative_infinity + arg1 < 0 -> :negative_infinity + true -> :positive_infinity + end + else + arg1 / arg2 + end + end) + end + + @doc """ + Returns the absolute value of a numeric literal. + + If the given argument is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def abs(literal) + + def abs(%RDF.Literal{literal: literal}), do: abs(literal) + + def abs(%RDF.XSD.Decimal{} = literal) do + if RDF.XSD.Decimal.valid?(literal) do + literal.value + |> D.abs() + |> RDF.XSD.Decimal.new() + end + end + + def abs(nil), do: nil + + def abs(value) do + cond do + literal?(value) -> + if RDF.XSD.valid?(value) do + %datatype{} = value + + case value.value do + :nan -> + literal(value) + + :positive_infinity -> + literal(value) + + :negative_infinity -> + datatype.new(:positive_infinity) + + value -> + value + |> Kernel.abs() + |> datatype.new() + end + end + + RDF.XSD.literal?(value) -> + nil + + true -> + value + |> RDF.Literal.coerce() + |> abs() + end + end + + @doc """ + Rounds a value to a specified number of decimal places, rounding upwards if two such values are equally near. + + The function returns the nearest (that is, numerically closest) value to the + given literal value that is a multiple of ten to the power of minus `precision`. + If two such values are equally near (for example, if the fractional part in the + literal value is exactly .5), the function returns the one that is closest to + positive infinity. + + If the given argument is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def round(literal, precision \\ 0) + + def round(%RDF.Literal{literal: literal}, precision), do: round(literal, precision) + + def round(%RDF.XSD.Decimal{} = literal, precision) do + if RDF.XSD.Decimal.valid?(literal) do + literal.value + |> xpath_round(precision) + |> to_string() + |> RDF.XSD.Decimal.new() + end + end + + def round(%datatype{value: value} = datatype_literal, _) + when datatype in [RDF.XSD.Double, RDF.XSD.Float] and + value in ~w[nan positive_infinity negative_infinity]a, + do: literal(datatype_literal) + + def round(%datatype{} = literal, precision) when datatype in [RDF.XSD.Double, RDF.XSD.Float] do + if datatype.valid?(literal) do + literal.value + |> new_decimal() + |> xpath_round(precision) + |> D.to_float() + |> datatype.new() + end + end + + def round(nil, _), do: nil + + def round(value, precision) do + cond do + literal?(value) -> + if RDF.XSD.valid?(value) do + if precision < 0 do + value.value + |> new_decimal() + |> xpath_round(precision) + |> D.to_integer() + |> RDF.XSD.Integer.new() + else + literal(value) + end + end + + RDF.XSD.literal?(value) -> + nil + + true -> + value + |> RDF.Literal.coerce() + |> round(precision) + end + end + + defp xpath_round(%D{sign: -1} = decimal, precision), + do: D.round(decimal, precision, :half_down) + + defp xpath_round(decimal, precision), + do: D.round(decimal, precision) + + @doc """ + Rounds a numeric literal upwards to a whole number literal. + + If the given argument is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def ceil(literal) + + def ceil(%RDF.Literal{literal: literal}), do: ceil(literal) + + def ceil(%RDF.XSD.Decimal{} = literal) do + if RDF.XSD.Decimal.valid?(literal) do + literal.value + |> D.round(0, if(literal.value.sign == -1, do: :down, else: :up)) + |> D.to_string() + |> RDF.XSD.Decimal.new() + end + end + + def ceil(%datatype{value: value} = datatype_literal) + when datatype in [RDF.XSD.Double, RDF.XSD.Float] and + value in ~w[nan positive_infinity negative_infinity]a, + do: literal(datatype_literal) + + def ceil(%datatype{} = literal) when datatype in [RDF.XSD.Double, RDF.XSD.Float] do + if datatype.valid?(literal) do + literal.value + |> Float.ceil() + |> trunc() + |> to_string() + |> datatype.new() + end + end + + def ceil(nil), do: nil + + def ceil(value) do + cond do + literal?(value) -> + if RDF.XSD.valid?(value) do + literal(value) + end + + RDF.XSD.literal?(value) -> + nil + + true -> + value + |> RDF.Literal.coerce() + |> ceil() + end + end + + @doc """ + Rounds a numeric literal downwards to a whole number literal. + + If the given argument is not a numeric literal or a value which + can be coerced into a numeric literal, `nil` is returned. + + see + + """ + def floor(literal) + + def floor(%RDF.Literal{literal: literal}), do: floor(literal) + + def floor(%RDF.XSD.Decimal{} = literal) do + if RDF.XSD.Decimal.valid?(literal) do + literal.value + |> D.round(0, if(literal.value.sign == -1, do: :up, else: :down)) + |> D.to_string() + |> RDF.XSD.Decimal.new() + end + end + + def floor(%datatype{value: value} = datatype_literal) + when datatype in [RDF.XSD.Double, RDF.XSD.Float] and + value in ~w[nan positive_infinity negative_infinity]a, + do: literal(datatype_literal) + + def floor(%datatype{} = literal) when datatype in [RDF.XSD.Double, RDF.XSD.Float] do + if datatype.valid?(literal) do + literal.value + |> Float.floor() + |> trunc() + |> to_string() + |> datatype.new() + end + end + + def floor(nil), do: nil + + def floor(value) do + cond do + literal?(value) -> + if RDF.XSD.valid?(value), do: literal(value) + + RDF.XSD.literal?(value) -> + nil + + true -> + value + |> RDF.Literal.coerce() + |> floor() + end + end + + defp arithmetic_operation(op, %RDF.Literal{literal: literal1}, literal2, fun), do: arithmetic_operation(op, literal1, literal2, fun) + defp arithmetic_operation(op, literal1, %RDF.Literal{literal: literal2}, fun), do: arithmetic_operation(op, literal1, literal2, fun) + defp arithmetic_operation(op, %datatype1{} = literal1, %datatype2{} = literal2, fun) do + if datatype?(datatype1) and datatype?(datatype2) do + result_type = result_type(op, datatype1, datatype2) + {arg1, arg2} = type_conversion(literal1, literal2, result_type) + result = fun.(arg1.value, arg2.value, result_type) + unless is_nil(result), do: result_type.new(result) + end + end + + defp arithmetic_operation(op, left, right, fun) do + cond do + is_nil(left) -> nil + is_nil(right) -> nil + not RDF.XSD.literal?(left) -> arithmetic_operation(op, RDF.Literal.coerce(left), right, fun) + not RDF.XSD.literal?(right) -> arithmetic_operation(op, left, RDF.Literal.coerce(right), fun) + true -> false + end + end + + defp type_conversion(%RDF.XSD.Decimal{} = left_decimal, %{value: right_value}, RDF.XSD.Decimal), + do: {left_decimal, RDF.XSD.Decimal.new(right_value).literal} + + defp type_conversion(%{value: left_value}, %RDF.XSD.Decimal{} = right_decimal, RDF.XSD.Decimal), + do: {RDF.XSD.Decimal.new(left_value).literal, right_decimal} + + defp type_conversion(%RDF.XSD.Decimal{value: left_decimal}, right, datatype) + when datatype in [RDF.XSD.Double, RDF.XSD.Float], + do: {(left_decimal |> D.to_float() |> RDF.XSD.Double.new()).literal, right} + + defp type_conversion(left, %RDF.XSD.Decimal{value: right_decimal}, datatype) + when datatype in [RDF.XSD.Double, RDF.XSD.Float], + do: {left, (right_decimal |> D.to_float() |> RDF.XSD.Double.new()).literal} + + defp type_conversion(left, right, _), do: {left, right} + + defp result_type(_, RDF.XSD.Double, _), do: RDF.XSD.Double + defp result_type(_, _, RDF.XSD.Double), do: RDF.XSD.Double + defp result_type(_, RDF.XSD.Float, _), do: RDF.XSD.Float + defp result_type(_, _, RDF.XSD.Float), do: RDF.XSD.Float + defp result_type(_, RDF.XSD.Decimal, _), do: RDF.XSD.Decimal + defp result_type(_, _, RDF.XSD.Decimal), do: RDF.XSD.Decimal + defp result_type(:/, _, _), do: RDF.XSD.Decimal + defp result_type(_, _, _), do: RDF.XSD.Integer + + defp literal(value), do: %RDF.Literal{literal: value} +end diff --git a/lib/rdf/xsd/datatypes/positive_integer.ex b/lib/rdf/xsd/datatypes/positive_integer.ex new file mode 100644 index 0000000..1b8117f --- /dev/null +++ b/lib/rdf/xsd/datatypes/positive_integer.ex @@ -0,0 +1,8 @@ +defmodule RDF.XSD.PositiveInteger do + use RDF.XSD.Datatype.Restriction, + name: "positiveInteger", + id: RDF.Utils.Bootstrapping.xsd_iri("positiveInteger"), + base: RDF.XSD.NonNegativeInteger + + def_facet_constraint RDF.XSD.Facets.MinInclusive, 1 +end diff --git a/lib/rdf/xsd/datatypes/short.ex b/lib/rdf/xsd/datatypes/short.ex new file mode 100644 index 0000000..96df409 --- /dev/null +++ b/lib/rdf/xsd/datatypes/short.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.Short do + use RDF.XSD.Datatype.Restriction, + name: "short", + id: RDF.Utils.Bootstrapping.xsd_iri("short"), + base: RDF.XSD.Int + + def_facet_constraint RDF.XSD.Facets.MinInclusive, -32768 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 32767 +end diff --git a/lib/rdf/xsd/datatypes/string.ex b/lib/rdf/xsd/datatypes/string.ex new file mode 100644 index 0000000..023ff40 --- /dev/null +++ b/lib/rdf/xsd/datatypes/string.ex @@ -0,0 +1,78 @@ +defmodule RDF.XSD.String do + @moduledoc """ + `RDF.XSD.Datatype` for XSD strings. + """ + + @type valid_value :: String.t() + + use RDF.XSD.Datatype.Primitive, + name: "string", + id: RDF.Utils.Bootstrapping.xsd_iri("string") + + @impl RDF.XSD.Datatype + @spec lexical_mapping(String.t(), Keyword.t()) :: valid_value + def lexical_mapping(lexical, _), do: to_string(lexical) + + @impl RDF.XSD.Datatype + @spec elixir_mapping(any, Keyword.t()) :: value + def elixir_mapping(value, _), do: to_string(value) + + @impl RDF.Literal.Datatype + def do_cast(value) + + def do_cast(%RDF.XSD.Decimal{} = xsd_decimal) do + try do + xsd_decimal.value + |> Decimal.to_integer() + |> RDF.XSD.Integer.new() + |> cast() + rescue + _ -> + default_canonical_cast(xsd_decimal, RDF.XSD.Decimal) + end + end + + def do_cast(%datatype{} = xsd_double) when datatype in [RDF.XSD.Double, RDF.XSD.Float] do + cond do + RDF.XSD.Numeric.negative_zero?(xsd_double) -> + new("-0") + + RDF.XSD.Numeric.zero?(xsd_double) -> + new("0") + + xsd_double.value >= 0.000_001 and xsd_double.value < 1_000_000 -> + xsd_double.value + |> RDF.XSD.Decimal.new() + |> cast() + + true -> + default_canonical_cast(xsd_double, datatype) + end + end + + def do_cast(%RDF.XSD.DateTime{} = xsd_datetime) do + xsd_datetime + |> RDF.XSD.DateTime.canonical_lexical_with_zone() + |> new() + end + + def do_cast(%RDF.XSD.Time{} = xsd_time) do + xsd_time + |> RDF.XSD.Time.canonical_lexical_with_zone() + |> new() + end + + def do_cast(%datatype{} = literal) do + if RDF.XSD.datatype?(datatype) do + default_canonical_cast(literal, datatype) + end + end + + def do_cast(literal_or_value), do: super(literal_or_value) + + defp default_canonical_cast(literal, datatype) do + literal + |> datatype.canonical_lexical() + |> new() + end +end diff --git a/lib/rdf/xsd/datatypes/time.ex b/lib/rdf/xsd/datatypes/time.ex new file mode 100644 index 0000000..78d4ecc --- /dev/null +++ b/lib/rdf/xsd/datatypes/time.ex @@ -0,0 +1,217 @@ +defmodule RDF.XSD.Time do + @moduledoc """ + `RDF.XSD.Datatype` for XSD times. + """ + + @type valid_value :: Time.t() | {Time.t(), true} + + use RDF.XSD.Datatype.Primitive, + name: "time", + id: RDF.Utils.Bootstrapping.xsd_iri("time") + + # 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/ + + @impl RDF.XSD.Datatype + 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 + + @impl RDF.XSD.Datatype + @spec elixir_mapping(valid_value | any, Keyword.t()) :: + value | {value, RDF.XSD.Datatype.uncanonical_lexical()} + 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 + + @impl RDF.XSD.Datatype + @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" + + @impl RDF.XSD.Datatype + @spec init_valid_lexical(valid_value, RDF.XSD.Datatype.uncanonical_lexical(), Keyword.t()) :: + RDF.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 + 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 + + @impl RDF.XSD.Datatype + @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) + + def do_cast(%RDF.XSD.DateTime{} = xsd_datetime) do + case xsd_datetime.value do + %NaiveDateTime{} = datetime -> + datetime + |> NaiveDateTime.to_time() + |> new() + + %DateTime{} -> + [_date, time_with_zone] = + xsd_datetime + |> RDF.XSD.DateTime.canonical_lexical_with_zone() + |> String.split("T", parts: 2) + + new(time_with_zone) + end + end + + def do_cast(%RDF.XSD.String{} = xsd_string), do: new(xsd_string.value) + + def do_cast(literal_or_value), do: super(literal_or_value) + + @impl RDF.Literal.Datatype + def do_equal_value?(literal1, literal2) + + def do_equal_value?(%__MODULE__{value: %_{}}, %__MODULE__{value: tz_tuple}) + when is_tuple(tz_tuple), + do: nil + + def do_equal_value?(%__MODULE__{value: tz_tuple}, %__MODULE__{value: %_{}}) + when is_tuple(tz_tuple), + do: nil + + def do_equal_value?(left, right), do: super(left, right) + + @doc """ + Extracts the timezone string from a `RDF.XSD.Time` value. + """ + def tz(time_literal) do + if valid?(time_literal) do + time_literal + |> lexical() + |> RDF.XSD.Utils.DateTime.tz() + 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}), + do: canonical_lexical_with_zone(xsd_time) + 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 diff --git a/lib/rdf/xsd/datatypes/unsigned_byte.ex b/lib/rdf/xsd/datatypes/unsigned_byte.ex new file mode 100644 index 0000000..9742bd1 --- /dev/null +++ b/lib/rdf/xsd/datatypes/unsigned_byte.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.UnsignedByte do + use RDF.XSD.Datatype.Restriction, + name: "unsignedByte", + id: RDF.Utils.Bootstrapping.xsd_iri("unsignedByte"), + base: RDF.XSD.UnsignedShort + + def_facet_constraint RDF.XSD.Facets.MinInclusive, 0 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 255 +end diff --git a/lib/rdf/xsd/datatypes/unsigned_int.ex b/lib/rdf/xsd/datatypes/unsigned_int.ex new file mode 100644 index 0000000..5864444 --- /dev/null +++ b/lib/rdf/xsd/datatypes/unsigned_int.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.UnsignedInt do + use RDF.XSD.Datatype.Restriction, + name: "unsignedInt", + id: RDF.Utils.Bootstrapping.xsd_iri("unsignedInt"), + base: RDF.XSD.UnsignedLong + + def_facet_constraint RDF.XSD.Facets.MinInclusive, 0 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 4_294_967_295 +end diff --git a/lib/rdf/xsd/datatypes/unsigned_long.ex b/lib/rdf/xsd/datatypes/unsigned_long.ex new file mode 100644 index 0000000..f7d0d8c --- /dev/null +++ b/lib/rdf/xsd/datatypes/unsigned_long.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.UnsignedLong do + use RDF.XSD.Datatype.Restriction, + name: "unsignedLong", + id: RDF.Utils.Bootstrapping.xsd_iri("unsignedLong"), + base: RDF.XSD.NonNegativeInteger + + def_facet_constraint RDF.XSD.Facets.MinInclusive, 0 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 18_446_744_073_709_551_615 +end diff --git a/lib/rdf/xsd/datatypes/unsigned_short.ex b/lib/rdf/xsd/datatypes/unsigned_short.ex new file mode 100644 index 0000000..baa2d72 --- /dev/null +++ b/lib/rdf/xsd/datatypes/unsigned_short.ex @@ -0,0 +1,9 @@ +defmodule RDF.XSD.UnsignedShort do + use RDF.XSD.Datatype.Restriction, + name: "unsignedShort", + id: RDF.Utils.Bootstrapping.xsd_iri("unsignedShort"), + base: RDF.XSD.UnsignedInt + + def_facet_constraint RDF.XSD.Facets.MinInclusive, 0 + def_facet_constraint RDF.XSD.Facets.MaxInclusive, 65535 +end diff --git a/lib/rdf/xsd/facet.ex b/lib/rdf/xsd/facet.ex new file mode 100644 index 0000000..6a1adae --- /dev/null +++ b/lib/rdf/xsd/facet.ex @@ -0,0 +1,113 @@ +defmodule RDF.XSD.Facet do + @type t :: module + + @doc """ + The name of a `RDF.XSD.Facet`. + """ + @callback name :: String.t() + + defmacro __using__(opts) do + name = Keyword.fetch!(opts, :name) + type_ast = Keyword.fetch!(opts, :type) + + quote bind_quoted: [], unquote: true do + @behaviour RDF.XSD.Facet + + @doc """ + Returns the value of this `RDF.XSD.Facet` on specific `RDF.XSD.Datatype`. + """ + @callback unquote(name)() :: unquote(type_ast) | nil + + @doc """ + Validates if a `value` and `lexical` conforms with a concrete `facet_constaint_value` for this `RDF.XSD.Facet`. + + This function must be implemented on a `RDF.XSD.Datatype` using this `RDF.XSD.Facet`. + """ + @callback unquote(conform_fun_name(name))( + facet_constaint_value :: any, + value :: any, + RDF.XSD.Datatype.uncanonical_lexical() + ) :: boolean + + @name unquote(Atom.to_string(name)) + @impl RDF.XSD.Facet + def name, do: @name + + @doc """ + Checks if a `value` and `lexical` conforms with the `c:#{unquote(conform_fun_name(name))}/3` implementation on the `datatype` `RDF.XSD.Datatype`. + """ + @spec conform?(RDF.XSD.Datatype.t(), any, RDF.XSD.Datatype.uncanonical_lexical()) :: boolean + def conform?(datatype, value, lexical) do + constrain_value = apply(datatype, unquote(name), []) + + is_nil(constrain_value) or + apply(datatype, unquote(conform_fun_name(name)), [constrain_value, value, lexical]) + end + + defmacro __using__(_opts) do + import unquote(__MODULE__) + default_facet_impl(__MODULE__, unquote(name)) + end + end + end + + defp conform_fun_name(facet_name), do: :"#{facet_name}_conform?" + + @doc """ + Macro for the definition of concrete constraining `value` for a `RDF.XSD.Facet` on a `RDF.XSD.Datatype`. + """ + defmacro def_facet_constraint(facet, value) do + facet_mod = Macro.expand_once(facet, __CALLER__) + facet_name = String.to_atom(facet_mod.name) + + quote do + unless unquote(facet) in @base.applicable_facets, + do: raise("#{unquote(facet_name)} is not an applicable facet of #{@base}") + + @facets unquote(facet_name) + + @impl unquote(facet) + def unquote(facet_name)(), do: unquote(value) + end + end + + @doc false + def default_facet_impl(facet_mod, facet_name) do + quote do + @behaviour unquote(facet_mod) + + Module.put_attribute(__MODULE__, unquote(facet_mod), nil) + @impl unquote(facet_mod) + def unquote(facet_name)(), do: nil + + defoverridable [{unquote(facet_name), 0}] + end + end + + @doc false + + def restriction_impl(facets, applicable_facets) do + Enum.map(applicable_facets, fn applicable_facet -> + applicable_facet_name = String.to_atom(applicable_facet.name) + + quote do + @behaviour unquote(applicable_facet) + + unless unquote(applicable_facet_name in facets) do + @impl unquote(applicable_facet) + def unquote(applicable_facet_name)(), + do: apply(@base, unquote(applicable_facet_name), []) + end + + @impl unquote(applicable_facet) + def unquote(conform_fun_name(applicable_facet_name))(constrain_value, value, lexical) do + apply(@base, unquote(conform_fun_name(applicable_facet_name)), [ + constrain_value, + value, + lexical + ]) + end + end + end) + end +end diff --git a/lib/rdf/xsd/facets/max_inclusive.ex b/lib/rdf/xsd/facets/max_inclusive.ex new file mode 100644 index 0000000..48f55ac --- /dev/null +++ b/lib/rdf/xsd/facets/max_inclusive.ex @@ -0,0 +1,3 @@ +defmodule RDF.XSD.Facets.MaxInclusive do + use RDF.XSD.Facet, name: :max_inclusive, type: integer +end diff --git a/lib/rdf/xsd/facets/min_inclusive.ex b/lib/rdf/xsd/facets/min_inclusive.ex new file mode 100644 index 0000000..a22b6f3 --- /dev/null +++ b/lib/rdf/xsd/facets/min_inclusive.ex @@ -0,0 +1,3 @@ +defmodule RDF.XSD.Facets.MinInclusive do + use RDF.XSD.Facet, name: :min_inclusive, type: integer +end diff --git a/lib/rdf/xsd/utils/date_time.ex b/lib/rdf/xsd/utils/date_time.ex new file mode 100644 index 0000000..2272230 --- /dev/null +++ b/lib/rdf/xsd/utils/date_time.ex @@ -0,0 +1,18 @@ +defmodule RDF.XSD.Utils.DateTime do + @moduledoc false + + @spec tz(String.t()) :: String.t() + def tz(string) do + case Regex.run(~r/([+-])(\d\d:\d\d)/, string) do + [_, sign, zone] -> + sign <> zone + + _ -> + if String.ends_with?(string, "Z") do + "Z" + else + "" + end + end + end +end diff --git a/lib/rdf/xsd/utils/regex.ex b/lib/rdf/xsd/utils/regex.ex new file mode 100644 index 0000000..ce80ecc --- /dev/null +++ b/lib/rdf/xsd/utils/regex.ex @@ -0,0 +1,86 @@ +defmodule RDF.XSD.Utils.Regex do + @moduledoc !""" + XSD-flavoured regex matching. + + This is not intended to be used directly. + Use `c:RDF.XSD.Datatype.matches?/3` implementations on the datatypes or + `RDF.XSD.Literal.matches?/3` instead. + """ + + @doc """ + Matches the string representation of the given value against a XPath and XQuery regular expression pattern. + + The regular expression language is defined in _XQuery 1.0 and XPath 2.0 Functions and Operators_. + + see + """ + @spec matches?(String.t(), String.t(), String.t()) :: boolean + def matches?(value, pattern, flags \\ "") do + string = to_string(value) + + case xpath_pattern(pattern, flags) do + {:regex, regex} -> + Regex.match?(regex, string) + + {:q, pattern} -> + String.contains?(string, pattern) + + {:qi, pattern} -> + string + |> String.downcase() + |> String.contains?(String.downcase(pattern)) + + {:error, error} -> + raise "Invalid XQuery regex pattern or flags: #{inspect(error)}" + end + end + + @spec xpath_pattern(String.t(), String.t()) :: + {:q | :qi, String.t()} | {:regex, Regex.t()} | {:error, any} + def xpath_pattern(pattern, flags) + + def xpath_pattern(pattern, flags) when is_binary(pattern) and is_binary(flags) do + q_pattern(pattern, flags) || xpath_regex_pattern(pattern, flags) + end + + defp q_pattern(pattern, flags) do + if String.contains?(flags, "q") and String.replace(flags, ~r/[qi]/, "") == "" do + {if(String.contains?(flags, "i"), do: :qi, else: :q), pattern} + end + end + + defp xpath_regex_pattern(pattern, flags) do + with {:ok, regex} <- + pattern + |> convert_utf_escaping() + |> Regex.compile(xpath_regex_flags(flags)) do + {:regex, regex} + end + end + + @spec convert_utf_escaping(String.t()) :: String.t() + def convert_utf_escaping(string) do + require Integer + + xpath_unicode_regex = ~r/(\\*)\\U([0-9]|[A-F]|[a-f]){2}(([0-9]|[A-F]|[a-f]){6})/ + [first | possible_matches] = Regex.split(xpath_unicode_regex, string, include_captures: true) + + [ + first + | Enum.map_every(possible_matches, 2, fn possible_xpath_unicode -> + [_, escapes, _, codepoint, _] = Regex.run(xpath_unicode_regex, possible_xpath_unicode) + + if escapes |> String.length() |> Integer.is_odd() do + "#{escapes}\\u{#{codepoint}}" + else + "\\" <> possible_xpath_unicode + end + end) + ] + |> Enum.join() + end + + defp xpath_regex_flags(flags) do + String.replace(flags, "q", "") <> "u" + end +end diff --git a/mix.exs b/mix.exs index 8223e08..c8f5529 100644 --- a/mix.exs +++ b/mix.exs @@ -67,7 +67,6 @@ defmodule RDF.Mixfile do defp deps do [ - {:xsd, path: "../../../RDF.ex/src/xsd"}, {:decimal, "~> 1.5"}, {:credo, "~> 1.3", only: [:dev, :test], runtime: false}, diff --git a/test/support/rdf_case.ex b/test/support/rdf_case.ex index 739d681..8c79dac 100644 --- a/test/support/rdf_case.ex +++ b/test/support/rdf_case.ex @@ -11,7 +11,7 @@ defmodule RDF.Test.Case do using do quote do - alias RDF.{Dataset, Graph, Description, IRI} + alias RDF.{Dataset, Graph, Description, IRI, XSD} alias unquote(__MODULE__).EX import RDF, only: [iri: 1, literal: 1, bnode: 1] diff --git a/test/support/xsd_datatype_case.ex b/test/support/xsd_datatype_case.ex new file mode 100644 index 0000000..d8dac13 --- /dev/null +++ b/test/support/xsd_datatype_case.ex @@ -0,0 +1,284 @@ +defmodule RDF.XSD.Datatype.Test.Case do + use ExUnit.CaseTemplate + + alias RDF.XSD + + using(opts) do + datatype = Keyword.fetch!(opts, :datatype) + datatype_name = Keyword.fetch!(opts, :name) + + datatype_iri = + Keyword.get(opts, :iri, RDF.NS.XSD.__base_iri__ <> datatype_name) + + valid = Keyword.get(opts, :valid) + invalid = Keyword.get(opts, :invalid) + primitive = Keyword.get(opts, :primitive) + base = unless primitive, do: Keyword.fetch!(opts, :base) + base_primitive = unless primitive, do: Keyword.fetch!(opts, :base_primitive) + applicable_facets = Keyword.get(opts, :applicable_facets, []) + facets = Keyword.get(opts, :facets) + + quote do + alias RDF.XSD + alias RDF.XSD.Datatype + alias unquote(datatype) + import unquote(__MODULE__) + + doctest unquote(datatype) + + @moduletag datatype: unquote(datatype) + + if unquote(valid) do + @valid unquote(valid) + @invalid unquote(invalid) + + test "registration" do + assert unquote(datatype) in XSD.datatypes() + assert XSD.datatype_by_name(unquote(datatype_name)) == unquote(datatype) + assert XSD.datatype_by_iri(unquote(datatype_iri)) == unquote(datatype) + end + + test "primitive/0" do + assert unquote(datatype).primitive?() == unquote(!!primitive) + end + + test "base/0" do + if unquote(primitive) do + assert unquote(datatype).base == nil + else + assert unquote(datatype).base == unquote(base) + end + end + + test "base_primitive/0" do + if unquote(primitive) do + assert unquote(datatype).base_primitive == unquote(datatype) + else + assert unquote(datatype).base_primitive == unquote(base_primitive) + end + end + + test "derived_from?/1" do + assert unquote(datatype).derived_from?(unquote(datatype)) == true + + unless unquote(primitive) do + assert unquote(datatype).derived_from?(unquote(base)) == true + assert unquote(datatype).derived_from?(unquote(base_primitive)) == true + end + end + + test "applicable_facets/0" do + assert MapSet.new(unquote(datatype).applicable_facets()) == + MapSet.new(unquote(applicable_facets)) + end + + if unquote(facets) do + test "facets" do + Enum.each(unquote(facets), fn {facet, value} -> + assert apply(unquote(datatype), facet, []) == value + end) + end + end + + test "name/0" do + assert unquote(datatype).name() == unquote(datatype_name) + end + + test "id/0" do + assert unquote(datatype).id() == RDF.iri(unquote(datatype_iri)) + end + + test "language/1" do + Enum.each(@valid, fn {input, _} -> + assert (unquote(datatype).new(input) |> unquote(datatype).language()) == nil + end) + end + + test "datatype/1" do + Enum.each(@valid, fn {input, _} -> + assert (unquote(datatype).new(input) |> unquote(datatype).datatype()) == RDF.iri(unquote(datatype_iri)) + end) + end + + describe "general new" do + Enum.each(@valid, fn {input, {value, lexical, _}} -> + expected = %RDF.Literal{ + literal: %unquote(datatype){value: value, uncanonical_lexical: lexical} + } + + @tag example: %{input: input, output: expected} + test "valid: #{unquote(datatype)}.new(#{inspect(input)})", %{example: example} do + assert unquote(datatype).new(example.input) == example.output + end + end) + + Enum.each(@invalid, fn value -> + expected = %RDF.Literal{ + literal: %unquote(datatype){ + uncanonical_lexical: unquote(datatype).init_invalid_lexical(value, []) + } + } + + @tag example: %{input: value, output: expected} + test "invalid: #{unquote(datatype)}.new(#{inspect(value)})", + %{example: example} do + assert unquote(datatype).new(example.input) == example.output + end + end) + + test "canonicalize option" do + Enum.each(@valid, fn {input, _} -> + assert unquote(datatype).new(input, canonicalize: true) == + unquote(datatype).new(input) |> unquote(datatype).canonical() + end) + + Enum.each(@invalid, fn input -> + assert unquote(datatype).new(input, canonicalize: true) == + unquote(datatype).new(input) |> unquote(datatype).canonical() + end) + end + end + + describe "general new!" do + test "with valid values, it behaves the same as new" do + Enum.each(@valid, fn {input, _} -> + assert unquote(datatype).new!(input) == unquote(datatype).new(input) + + assert unquote(datatype).new!(input) == + unquote(datatype).new(input) + + assert unquote(datatype).new!(input, canonicalize: true) == + unquote(datatype).new(input, canonicalize: true) + end) + end + + test "with invalid values, it raises an error" do + Enum.each(@invalid, fn value -> + assert_raise ArgumentError, fn -> unquote(datatype).new!(value) end + + assert_raise ArgumentError, fn -> + unquote(datatype).new!(value, canonicalize: true) + end + end) + end + end + + describe "general value" do + Enum.each(@valid, fn {input, {value, _, canonicalized}} -> + @tag example: %{input: input, value: value} + test "of valid #{unquote(datatype)}.new(#{inspect(input)})", + %{example: example} do + assert unquote(datatype).new(example.input) |> unquote(datatype).value() == + example.value + end + end) + + Enum.each(@invalid, fn value -> + @tag example: %{input: value, value: value} + test "of invalid #{unquote(datatype)}.new(#{inspect(value)})", %{example: example} do + assert unquote(datatype).new(example.input) |> unquote(datatype).value() == nil + end + end) + end + + describe "general lexical" do + Enum.each(@valid, fn {input, {_, lexical, canonicalized}} -> + lexical = lexical || canonicalized + @tag example: %{input: input, lexical: lexical} + test "of valid #{unquote(datatype)}.new(#{inspect(input)})", + %{example: example} do + assert unquote(datatype).new(example.input) |> unquote(datatype).lexical() == + example.lexical + end + end) + + Enum.each(@invalid, fn value -> + lexical = unquote(datatype).init_invalid_lexical(value, []) + @tag example: %{input: value, lexical: lexical} + test "of invalid #{unquote(datatype)}.new(#{inspect(value)}) == #{inspect(lexical)}", + %{example: example} do + assert unquote(datatype).new(example.input) |> unquote(datatype).lexical() == + example.lexical + end + end) + end + + describe "general canonicalization" do + Enum.each(@valid, fn {input, {value, _, _}} -> + expected = %RDF.Literal{literal: %unquote(datatype){value: value}} + @tag example: %{input: input, output: expected} + test "#{unquote(datatype)} #{inspect(input)}", %{example: example} do + assert unquote(datatype).new(example.input) |> unquote(datatype).canonical() == + example.output + end + end) + + Enum.each(@valid, fn {input, {_, _, canonicalized}} -> + @tag example: %{input: input, canonicalized: canonicalized} + test "lexical of canonicalized #{unquote(datatype)} #{inspect(input, limit: 4)} is #{ + inspect(canonicalized, limit: 4) + }", + %{example: example} do + assert unquote(datatype).new(example.input) + |> unquote(datatype).canonical() + |> unquote(datatype).lexical() == + example.canonicalized + end + end) + + Enum.each(@valid, fn {input, {_, _, canonicalized}} -> + @tag example: %{input: input, canonicalized: canonicalized} + test "canonical? for #{unquote(datatype)} #{inspect(input)}", %{example: example} do + literal = unquote(datatype).new(example.input) + assert unquote(datatype).canonical?(literal) == ( + unquote(datatype).lexical(literal) ==example.canonicalized + ) + end + end) + + test "does not change the XSD datatype value when it is invalid" do + Enum.each(@invalid, fn value -> + assert unquote(datatype).new(value) |> unquote(datatype).canonical() == + unquote(datatype).new(value) + end) + end + end + + describe "general validation" do + Enum.each(Map.keys(@valid), fn value -> + @tag value: value + test "#{inspect(value)} as a #{unquote(datatype)} is valid", %{value: value} do + assert unquote(datatype).valid?(unquote(datatype).new(value)) + end + end) + + Enum.each(@invalid, fn value -> + @tag value: value + test "#{inspect(value)} as a #{unquote(datatype)} is invalid", %{value: value} do + refute unquote(datatype).valid?(unquote(datatype).new(value)) + end + end) + end + end + + test "XSD.Datatype.matches?/3" do + Enum.each(@valid, fn {input, {_, lexical, canonicalized}} -> + lexical = lexical || canonicalized + assert unquote(datatype).new(input) |> unquote(datatype).matches?(lexical, "q") == true + end) + end + + test "String.Chars protocol implementation" do + Enum.each(@valid, fn {input, _} -> + assert unquote(datatype).new(input) |> to_string() == + unquote(datatype).new(input) |> unquote(datatype).lexical() + end) + end + end + end + + def dt(value) do + {:ok, date, _} = DateTime.from_iso8601(value) + date + end +end diff --git a/test/support/xsd_test_data.ex b/test/support/xsd_test_data.ex new file mode 100644 index 0000000..e2b91cc --- /dev/null +++ b/test/support/xsd_test_data.ex @@ -0,0 +1,187 @@ +defmodule RDF.XSD.TestData do + @zero %{ + 0 => {0, nil, "0"}, + "0" => {0, nil, "0"}, + "+0" => {0, "+0", "0"}, + "-0" => {0, "-0", "0"}, + "000" => {0, "000", "0"} + } + + @basic_valid_positive_integers %{ + # input => { value, lexical, canonicalized } + 1 => {1, nil, "1"}, + "1" => {1, nil, "1"}, + "01" => {1, "01", "1"}, + "0123" => {123, "0123", "123"}, + "+1" => {1, "+1", "1"} + } + |> Map.merge(@zero) + + @basic_valid_negative_integers %{ + -1 => {-1, nil, "-1"}, + "-1" => {-1, nil, "-1"}, + "-01" => {-1, "-01", "-1"}, + "-0123" => {-123, "-0123", "-123"} + } + |> Map.merge(@zero) + + @valid_unsigned_bytes %{ + 255 => {255, nil, "255"}, + "0255" => {255, "0255", "255"} + } + |> Map.merge(@basic_valid_positive_integers) + + @valid_unsigned_shorts %{ + 65535 => {65535, nil, "65535"} + } + |> Map.merge(@valid_unsigned_bytes) + + @valid_unsigned_ints %{ + 4_294_967_295 => {4_294_967_295, nil, "4294967295"} + } + |> Map.merge(@valid_unsigned_shorts) + + @valid_unsigned_longs %{ + 18_446_744_073_709_551_615 => + {18_446_744_073_709_551_615, nil, "18446744073709551615"} + } + |> Map.merge(@valid_unsigned_ints) + + @valid_bytes Map.merge(@basic_valid_positive_integers, @basic_valid_negative_integers) + + @valid_shorts %{ + 32767 => {32767, nil, "32767"}, + -32768 => {-32768, nil, "-32768"} + } + |> Map.merge(@valid_bytes) + + @valid_ints %{ + 2_147_483_647 => {2_147_483_647, nil, "2147483647"}, + -2_147_483_648 => {-2_147_483_648, nil, "-2147483648"} + } + |> Map.merge(@valid_bytes) + + @valid_longs %{ + 9_223_372_036_854_775_807 => + {9_223_372_036_854_775_807, nil, "9223372036854775807"}, + -9_223_372_036_854_775_808 => + {-9_223_372_036_854_775_808, nil, "-9223372036854775808"} + } + |> Map.merge(@valid_bytes) + + @valid_non_negative_integers %{ + 200_000_000_000_000_000_000_000 => + {200_000_000_000_000_000_000_000, nil, + "200000000000000000000000"} + } + |> Map.merge(@valid_unsigned_longs) + + @valid_non_positive_integers %{ + -200_000_000_000_000_000_000_000 => + {-200_000_000_000_000_000_000_000, nil, + "-200000000000000000000000"} + } + |> Map.merge(@basic_valid_negative_integers) + + @valid_positive_integers Map.drop(@valid_non_negative_integers, Map.keys(@zero)) + @valid_negative_integers Map.drop(@valid_non_positive_integers, Map.keys(@zero)) + + @valid_integers @zero + |> Map.merge(@valid_non_negative_integers) + |> Map.merge(@valid_non_positive_integers) + + @basic_invalid_integers [ + "foo", + "10.1", + "12xyz", + true, + false, + 3.14, + "1 2", + "foo 1", + "1 foo" + ] + + @invalid_bytes [128, "128", -129, "-129"] ++ @basic_invalid_integers + @invalid_shorts [32768, "32768", -32769, "-32769"] ++ @basic_invalid_integers + @invalid_ints [2_147_483_648, "2147483648", -21_474_836_489, "-2147483649"] ++ + @basic_invalid_integers + @invalid_longs [ + 9_223_372_036_854_775_808, + "9223372036854775808", + -9_223_372_036_854_775_809, + "-92233720368547758089" + ] ++ @basic_invalid_integers + + @invalid_non_negative_integers [-1, "-1"] ++ @basic_invalid_integers + @invalid_non_positive_integers [1, "1", "+1"] ++ @basic_invalid_integers + @invalid_positive_integers [0, "0"] ++ @invalid_non_negative_integers + @invalid_negative_integers [0, "0"] ++ @invalid_non_positive_integers + + @invalid_unsigned_bytes [256, "256"] ++ @invalid_non_negative_integers + @invalid_unsigned_shorts [65536, "65536"] ++ @invalid_non_negative_integers + @invalid_unsigned_ints [4_294_967_296, "4294967296"] ++ @invalid_non_negative_integers + @invalid_unsigned_longs [18_446_744_073_709_551_616, "18446744073709551616"] ++ + @invalid_non_negative_integers + + @valid_floats %{ + # input => { value, lexical, canonicalized } + 0 => {0.0, "0.0", "0.0E0"}, + 42 => {42.0, "42.0", "4.2E1"}, + 0.0e0 => {0.0, "0.0", "0.0E0"}, + 1.0e0 => {1.0, "1.0", "1.0E0"}, + :positive_infinity => {:positive_infinity, nil, "INF"}, + :negative_infinity => {:negative_infinity, nil, "-INF"}, + :nan => {:nan, nil, "NaN"}, + "1.0E0" => {1.0e0, nil, "1.0E0"}, + "0.0" => {0.0, "0.0", "0.0E0"}, + "1" => {1.0e0, "1", "1.0E0"}, + "01" => {1.0e0, "01", "1.0E0"}, + "0123" => {1.23e2, "0123", "1.23E2"}, + "-1" => {-1.0e0, "-1", "-1.0E0"}, + "+01.000" => {1.0e0, "+01.000", "1.0E0"}, + "1.0" => {1.0e0, "1.0", "1.0E0"}, + "123.456" => {1.23456e2, "123.456", "1.23456E2"}, + "1.0e+1" => {1.0e1, "1.0e+1", "1.0E1"}, + "1.0e-10" => {1.0e-10, "1.0e-10", "1.0E-10"}, + "123.456e4" => {1.23456e6, "123.456e4", "1.23456E6"}, + "1.E-8" => {1.0e-8, "1.E-8", "1.0E-8"}, + "3E1" => {3.0e1, "3E1", "3.0E1"}, + "INF" => {:positive_infinity, nil, "INF"}, + "Inf" => {:positive_infinity, "Inf", "INF"}, + "-INF" => {:negative_infinity, nil, "-INF"}, + "NaN" => {:nan, nil, "NaN"} + } + + @invalid_floats ["foo", "12.xyz", "1.0ez", "+INF", true, false, "1.1e1 foo", "foo 1.1e1"] + + def valid_integers, do: @valid_integers + def valid_non_negative_integers, do: @valid_non_negative_integers + def valid_non_positive_integers, do: @valid_non_positive_integers + def valid_positive_integers, do: @valid_positive_integers + def valid_negative_integers, do: @valid_negative_integers + def valid_bytes, do: @valid_bytes + def valid_shorts, do: @valid_shorts + def valid_ints, do: @valid_ints + def valid_longs, do: @valid_longs + def valid_unsigned_bytes, do: @valid_unsigned_bytes + def valid_unsigned_shorts, do: @valid_unsigned_shorts + def valid_unsigned_ints, do: @valid_unsigned_ints + def valid_unsigned_longs, do: @valid_unsigned_longs + def valid_floats, do: @valid_floats + + def invalid_integers, do: @basic_invalid_integers + def invalid_non_negative_integers, do: @invalid_non_negative_integers + def invalid_non_positive_integers, do: @invalid_non_positive_integers + def invalid_positive_integers, do: @invalid_positive_integers + def invalid_negative_integers, do: @invalid_negative_integers + def invalid_bytes, do: @invalid_bytes + def invalid_shorts, do: @invalid_shorts + def invalid_ints, do: @invalid_ints + def invalid_longs, do: @invalid_longs + def invalid_unsigned_bytes, do: @invalid_unsigned_bytes + def invalid_unsigned_shorts, do: @invalid_unsigned_shorts + def invalid_unsigned_ints, do: @invalid_unsigned_ints + def invalid_unsigned_longs, do: @invalid_unsigned_longs + def invalid_floats, do: @invalid_floats +end diff --git a/test/unit/datatypes/numeric_test.exs b/test/unit/datatypes/numeric_test.exs deleted file mode 100644 index 7a38b47..0000000 --- a/test/unit/datatypes/numeric_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -defmodule RDF.NumericTest do - use RDF.Test.Case - - alias RDF.Numeric - - test "zero?/1" do - assert Numeric.zero?(RDF.integer(0)) == true - assert Numeric.zero?(RDF.string("0")) == false - end - - test "negative_zero?/1" do - assert Numeric.negative_zero?(RDF.double("-0")) == true - assert Numeric.negative_zero?(RDF.integer(0)) == false - end - - test "add/2" do - assert Numeric.add(RDF.integer(1), RDF.integer(2)) == RDF.integer(3) - assert Numeric.add(RDF.float(1), 2) == RDF.float(3.0) - end - - test "subtract/2" do - assert Numeric.subtract(RDF.integer(2), RDF.integer(1)) == RDF.integer(1) - assert Numeric.subtract(RDF.decimal(2), 1) == RDF.decimal(1.0) - end - - test "multiply/2" do - assert Numeric.multiply(RDF.integer(2), RDF.integer(3)) == RDF.integer(6) - assert Numeric.multiply(RDF.double(2), 3) == RDF.double(6.0) - end - - test "divide/2" do - assert Numeric.divide(RDF.integer(4), RDF.integer(2)) == RDF.decimal(2) - assert Numeric.divide(RDF.double(3), 2) == RDF.double(1.5) - end - - test "abs/1" do - assert Numeric.abs(RDF.integer(-2)) == RDF.integer(2) - assert Numeric.abs(RDF.double(-3.14)) == RDF.double(3.14) - end - - test "round/1" do - assert Numeric.round(RDF.integer(2)) == RDF.integer(2) - assert Numeric.round(RDF.double(3.14)) == RDF.double(3.0) - end - - test "round/2" do - assert Numeric.round(RDF.integer(2), 3) == RDF.integer(2) - assert Numeric.round(RDF.double(3.1415), 2) == RDF.double(3.14) - end - - test "ceil/1" do - assert Numeric.ceil(RDF.integer(2)) == RDF.integer(2) - assert Numeric.ceil(RDF.double(3.14)) == RDF.double("4") - end - - test "floor/1" do - assert Numeric.floor(RDF.integer(2)) == RDF.integer(2) - assert Numeric.floor(RDF.double(3.14)) == RDF.double("3") - end -end diff --git a/test/unit/datatypes/xsd_test.exs b/test/unit/datatypes/xsd_test.exs deleted file mode 100644 index 6b6816e..0000000 --- a/test/unit/datatypes/xsd_test.exs +++ /dev/null @@ -1,215 +0,0 @@ -defmodule RDF.Literal.XSDTest do - use ExUnit.Case - - import RDF.TestLiterals - alias RDF.Literal - - @examples [ - {RDF.XSD.Boolean, XSD.Boolean, true}, - {RDF.XSD.Boolean, XSD.Boolean, false}, - {RDF.XSD.String, XSD.String, :plain}, - {RDF.XSD.Date, XSD.Date, :date}, - {RDF.XSD.Time, XSD.Time, :time}, - {RDF.XSD.DateTime, XSD.DateTime, :datetime}, - {RDF.XSD.DateTime, XSD.DateTime, :naive_datetime}, - {RDF.XSD.AnyURI, XSD.AnyURI, :uri}, - {RDF.XSD.Decimal, XSD.Decimal, :decimal}, - {RDF.XSD.Integer, XSD.Integer, :long}, - {RDF.XSD.Long, XSD.Long, :long}, - {RDF.XSD.Int, XSD.Int, :int}, - {RDF.XSD.Short, XSD.Short, :int}, - {RDF.XSD.Byte, XSD.Byte, :int}, - {RDF.XSD.NonNegativeInteger, XSD.NonNegativeInteger, :long}, - {RDF.XSD.PositiveInteger, XSD.PositiveInteger, :long}, - {RDF.XSD.UnsignedLong, XSD.UnsignedLong, :long}, - {RDF.XSD.UnsignedInt, XSD.UnsignedInt, :int}, - {RDF.XSD.UnsignedShort, XSD.UnsignedShort, :int}, - {RDF.XSD.UnsignedByte, XSD.UnsignedByte, :int}, - {RDF.XSD.NonPositiveInteger, XSD.NonPositiveInteger, :neg_int}, - {RDF.XSD.NegativeInteger, XSD.NegativeInteger, :neg_int}, - {RDF.XSD.Double, XSD.Double, :double}, - {RDF.XSD.Float, XSD.Float, :double}, - ] - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.new(#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value} do - assert %Literal{literal: %datatype{}} = literal = rdf_datatype.new(value) - assert datatype == xsd_datatype - assert rdf_datatype.valid?(literal) == true - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype - test "#{rdf_datatype}.name/0 (#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype} do - assert rdf_datatype.name() == xsd_datatype.name() - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype - test "#{rdf_datatype}.id/0 (#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype} do - assert rdf_datatype.id() == xsd_datatype.id() - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.datatype/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value} do - assert rdf_datatype.new(value) |> rdf_datatype.datatype() == RDF.iri(xsd_datatype.id()) - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.language/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, value: value} do - assert rdf_datatype.new(value) |> rdf_datatype.language() == nil - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.value/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.value(literal) == value - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.lexical/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.lexical(literal) == - xsd_datatype.new(value) |> xsd_datatype.lexical() - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.canonical/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.canonical(literal) == - %Literal{literal: xsd_datatype.new(value) |> xsd_datatype.canonical()} - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.canonical?/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.canonical?(literal) == - xsd_datatype.new(value) |> xsd_datatype.canonical?() - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.valid?/1 (#{inspect value})", %{rdf_datatype: rdf_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.valid?(literal) == true - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.equal_value?/2 (#{inspect value})", %{rdf_datatype: rdf_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.equal_value?(literal, literal) == true - assert rdf_datatype.equal_value?(literal, value) == true - end - end) - - Enum.each(@examples, fn {rdf_datatype, xsd_datatype, value_type} -> - value = value_type |> value() |> List.first() - @tag rdf_datatype: rdf_datatype, xsd_datatype: xsd_datatype, value: value - test "#{rdf_datatype}.compare/2 (#{inspect value})", %{rdf_datatype: rdf_datatype, value: value} do - literal = rdf_datatype.new(value) - assert rdf_datatype.compare(literal, literal) == :eq - end - end) - - describe "cast/1" do - test "when given a literal with the same datatype" do - assert RDF.XSD.String.new("foo") |> RDF.XSD.String.cast() == RDF.XSD.String.new("foo") - assert RDF.XSD.Integer.new(42) |> RDF.XSD.Integer.cast() == RDF.XSD.Integer.new(42) - assert RDF.XSD.Byte.new(42) |> RDF.XSD.Byte.cast() == RDF.XSD.Byte.new(42) - end - - test "when given a literal with a datatype which is castable" do - assert RDF.XSD.Integer.new(42) |> RDF.XSD.String.cast() == RDF.XSD.String.new("42") - assert RDF.XSD.String.new("42") |> RDF.XSD.Integer.cast() == RDF.XSD.Integer.new(42) - assert RDF.XSD.Decimal.new(42) |> RDF.XSD.Byte.cast() == RDF.XSD.Byte.new(42) - end - - test "when given a literal with a datatype which is not castable" do - assert RDF.XSD.String.new("foo") |> RDF.XSD.Integer.cast() == nil - assert RDF.XSD.Integer.new(12345) |> RDF.XSD.Byte.cast() == nil - end - - test "when given a coercible value" do - assert "foo" |> RDF.XSD.String.cast() == RDF.XSD.String.new("foo") - assert "42" |> RDF.XSD.Integer.cast() == RDF.XSD.Integer.new(42) - assert 42 |> RDF.XSD.Byte.cast() == RDF.XSD.Byte.new(42) - end - - test "with invalid literals" do - assert RDF.XSD.Integer.new(3.14) |> RDF.XSD.Integer.cast() == nil - assert RDF.XSD.Decimal.new("NAN") |> RDF.XSD.Decimal.cast() == nil - assert RDF.XSD.Double.new(true) |> RDF.XSD.Double.cast() == nil - assert RDF.XSD.Boolean.new("42") |> RDF.XSD.Boolean.cast() == nil - end - - test "with non-coercible value" do - assert_raise RDF.Literal.InvalidError, fn -> RDF.XSD.String.cast(:foo) end - assert_raise RDF.Literal.InvalidError, fn -> assert RDF.XSD.String.cast(make_ref()) end - end - end - - describe "RDF.XSD.Boolean" do - test "ebv/1" do - assert RDF.true |> RDF.XSD.Boolean.ebv() == RDF.true - assert RDF.string("foo") |> RDF.XSD.Boolean.ebv() == RDF.true - assert false |> RDF.XSD.Boolean.ebv() == RDF.false - assert "" |> RDF.XSD.Boolean.ebv() == RDF.false - assert 1 |> RDF.XSD.Boolean.ebv() == RDF.true - assert self() |> RDF.XSD.Boolean.ebv() == nil - end - - test "fn_not/1" do - assert RDF.true |> RDF.XSD.Boolean.fn_not() == RDF.false - assert false |> RDF.XSD.Boolean.fn_not() == RDF.true - end - - test "logical_and/1" do - assert RDF.true |> RDF.XSD.Boolean.logical_and(false) == RDF.false - assert true |> RDF.XSD.Boolean.logical_and(RDF.true) == RDF.true - assert false |> RDF.XSD.Boolean.logical_and(false) == RDF.false - assert 42 |> RDF.XSD.Boolean.logical_and(self()) == nil - end - - test "logical_or/1" do - assert RDF.true |> RDF.XSD.Boolean.logical_or(false) == RDF.true - assert true |> RDF.XSD.Boolean.logical_or(RDF.true) == RDF.true - assert false |> RDF.XSD.Boolean.logical_or(false) == RDF.false - assert self() |> RDF.XSD.Boolean.logical_or(self()) == nil - end - end - - describe "RDF.XSD.DateTime" do - test "now/0" do - assert %Literal{literal: %XSD.DateTime{}} = RDF.XSD.DateTime.now() - end - end -end diff --git a/test/unit/equality_test.exs b/test/unit/equality_test.exs index 54bf75a..e3b1eb9 100644 --- a/test/unit/equality_test.exs +++ b/test/unit/equality_test.exs @@ -1,361 +1,409 @@ defmodule RDF.EqualityTest do use RDF.Test.Case - alias Decimal, as: D + alias RDF.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.anyURI("http://example.com/")}, - - {RDF.anyURI("http://example.com/"), - RDF.iri("http://example.com/")}, - - {RDF.anyURI("http://example.com/"), - RDF.anyURI("http://example.com/")}, + {RDF.iri("http://example.com/"), XSD.anyURI("http://example.com/")}, + {XSD.anyURI("http://example.com/"), XSD.anyURI("http://example.com/")}, ] - @value_unequal_iris [ - {RDF.iri("http://example.com/foo"), - RDF.anyURI("http://example.com/bar")}, + @unequal_iris [ + {RDF.iri("http://example.com/foo"), RDF.iri("http://example.com/bar")}, + {RDF.iri("http://example.com/foo"), XSD.anyURI("http://example.com/bar")}, ] + @equal_iris_by_coercion [] + @unequal_iris_by_coercion [] @incomparable_iris [ - {RDF.iri("http://example.com/"), RDF.string("http://example.com/")}, + {RDF.iri("http://example.com/"), XSD.string("http://example.com/")}, ] - test "term equality", do: assert_term_equal @term_equal_iris - test "term inequality", do: assert_term_unequal @term_unequal_iris - @tag skip: "TODO: finish equality extension of XSD.AnyURI" - test "value equality", do: assert_value_equal @value_equal_iris - @tag skip: "TODO: finish equality extension of XSD.AnyURI" - test "value inequality", do: assert_value_unequal @value_unequal_iris - test "incomparability", do: assert_incomparable @incomparable_iris + test "term equality", do: assert_term_equal(@term_equal_iris) + @tag skip: "TODO: finish value equality of XSD.AnyURI" + test "value equality", do: assert_value_equal(@value_equal_iris) + @tag skip: "TODO: finish value equality of XSD.AnyURI" + test "inequality", do: assert_unequal(@unequal_iris) + test "coerced value equality", do: assert_coerced_equal(@equal_iris_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_iris_by_coercion) + 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 [ + @unequal_bnodes [ + {RDF.bnode("foo"), RDF.bnode("bar")}, ] + @equal_bnodes_by_coercion [] + @unequal_bnodes_by_coercion [] @incomparable_bnodes [ - {RDF.bnode("foo"), RDF.string("foo")}, - {RDF.string("foo"), RDF.bnode("foo")}, + {RDF.bnode("foo"), XSD.string("foo")}, + {XSD.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 + test "term equality", do: assert_term_equal @term_equal_bnodes + test "value equality", do: assert_value_equal @value_equal_bnodes + test "inequality", do: assert_unequal @unequal_bnodes + test "coerced value equality", do: assert_coerced_equal(@equal_bnodes_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_bnodes_by_coercion) + test "incomparability", do: assert_incomparable @incomparable_bnodes end - describe "RDF.String and RDF.LangString" do + describe "XSD.String" do @term_equal_strings [ - {RDF.string("foo"), RDF.string("foo")}, - {RDF.lang_string("foo", language: "de"), RDF.lang_string("foo", language: "de")}, + {XSD.string("foo"), XSD.string("foo")} ] - @term_unequal_strings [ - {RDF.string("foo"), RDF.string("bar")}, - {RDF.lang_string("foo", language: "de"), RDF.lang_string("bar", language: "de")}, + @value_equal_strings [] + @unequal_strings [ + {XSD.string("foo"), XSD.string("bar")} ] - @value_equal_strings [ + @equal_strings_by_coercion [ + {XSD.string("foo"), "foo"} ] - @value_unequal_strings [ - ] - @value_equal_strings_by_coercion [ - {RDF.string("foo"), "foo"}, - ] - @value_unequal_strings_by_coercion [ - {RDF.string("foo"), "bar"}, + @unequal_strings_by_coercion [ + {XSD.string("foo"), "bar"} ] @incomparable_strings [ - {RDF.string("42"), 42}, - {RDF.lang_string("foo", language: "de"), "foo"}, - {RDF.string("foo"), RDF.lang_string("foo", language: "de")}, - {RDF.lang_string("foo", language: "de"), RDF.string("foo")}, - {RDF.string("foo"), RDF.bnode("foo")}, + {XSD.string("42"), 42} ] - test "term equality", do: assert_term_equal @term_equal_strings - test "term inequality", do: assert_term_unequal @term_unequal_strings - test "value equality", do: assert_value_equal @value_equal_strings - test "value inequality", do: assert_value_unequal @value_unequal_strings - test "coerced value equality", do: assert_value_equal @value_equal_strings_by_coercion - test "coerced value inequality", do: assert_value_unequal @value_unequal_strings_by_coercion - test "incomparability", do: assert_incomparable @incomparable_strings + test "term equality", do: assert_term_equal(@term_equal_strings) + test "value equality", do: assert_value_equal(@value_equal_strings) + test "inequality", do: assert_unequal(@unequal_strings) + test "coerced value equality", do: assert_coerced_equal(@equal_strings_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_strings_by_coercion) + test "incomparability", do: assert_incomparable(@incomparable_strings) end - describe "RDF.Boolean" do - @term_equal_booleans [ - {RDF.true, RDF.true}, - {RDF.false, RDF.false}, - # invalid literals - {RDF.boolean("foo"), RDF.boolean("foo")}, + describe "XSD.String and RDF.LangString" do + @term_equal_strings [ + {XSD.string("foo"), XSD.string("foo")}, + {RDF.lang_string("foo", language: "de"), RDF.lang_string("foo", language: "de")}, ] - @term_unequal_booleans [ - {RDF.true, RDF.false}, - {RDF.false, RDF.true}, - # invalid literals - {RDF.boolean("foo"), RDF.boolean("bar")}, + @value_equal_strings [] + @unequal_strings [ + {XSD.string("foo"), XSD.string("bar")}, + {RDF.lang_string("foo", language: "de"), RDF.lang_string("bar", language: "de")}, + ] + @equal_strings_by_coercion [ + {XSD.string("foo"), "foo"} + ] + @unequal_strings_by_coercion [ + {XSD.string("foo"), "bar"} + ] + @incomparable_strings [ + {XSD.string("42"), 42}, + {RDF.lang_string("foo", language: "de"), "foo"}, + {XSD.string("foo"), RDF.lang_string("foo", language: "de")}, + {RDF.lang_string("foo", language: "de"), XSD.string("foo")}, + {XSD.string("foo"), RDF.bnode("foo")}, + ] + + test "term equality", do: assert_term_equal(@term_equal_strings) + test "value equality", do: assert_value_equal(@value_equal_strings) + test "inequality", do: assert_unequal(@unequal_strings) + test "coerced value equality", do: assert_coerced_equal(@equal_strings_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_strings_by_coercion) + test "incomparability", do: assert_incomparable(@incomparable_strings) + end + + describe "XSD.Boolean" do + @term_equal_booleans [ + {XSD.true(), XSD.true()}, + {XSD.false(), XSD.false()} ] @value_equal_booleans [ - {RDF.true, RDF.boolean("1")}, - {RDF.boolean(0), RDF.false}, - # invalid literals - {RDF.boolean("foo"), RDF.boolean("foo")}, + {XSD.true(), XSD.boolean("1")}, + {XSD.false(), XSD.boolean("0")} ] - @value_unequal_booleans [ - {RDF.true, RDF.boolean("false")}, - {RDF.boolean(0), RDF.true}, - # invalid literals - {RDF.boolean("foo"), RDF.boolean("bar")}, + @unequal_booleans [ + {XSD.true(), XSD.false()}, + {XSD.true(), XSD.boolean("false")}, + {XSD.true(), XSD.boolean(0)} ] - @value_equal_booleans_by_coercion [ - {RDF.true, true}, - {RDF.false, false}, + @equal_booleans_by_coercion [ + {XSD.true(), true}, + {XSD.false(), false} ] - @value_unequal_booleans_by_coercion [ - {RDF.true, false}, - {RDF.false, true}, + @unequal_booleans_by_coercion [ + {XSD.true(), false}, + {XSD.false(), true} + ] + @equal_invalid_booleans [ + {XSD.boolean("foo"), XSD.boolean("foo")} + ] + @unequal_invalid_booleans [ + {XSD.boolean("foo"), XSD.boolean("bar")}, + {XSD.true(), XSD.boolean("True")}, + {XSD.false(), XSD.boolean("FALSE")} ] @incomparable_booleans [ - {RDF.false, nil}, - {RDF.true, 42}, - {RDF.true, RDF.string("FALSE")}, - {RDF.true, RDF.integer(0)}, + {XSD.false(), nil}, + {XSD.true(), 42}, + {XSD.true(), XSD.integer(0)}, + {XSD.true(), XSD.non_negative_integer(0)} ] - 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 "coerced value equality", do: assert_value_equal @value_equal_booleans_by_coercion - test "coerced value inequality", do: assert_value_unequal @value_unequal_booleans_by_coercion - test "incomparability", do: assert_incomparable @incomparable_booleans + test "term equality", do: assert_term_equal(@term_equal_booleans) + test "value equality", do: assert_value_equal(@value_equal_booleans) + test "inequality", do: assert_unequal(@unequal_booleans) + test "coerced value equality", do: assert_coerced_equal(@equal_booleans_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_booleans_by_coercion) + test "invalid equality", do: assert_equal_invalid(@equal_invalid_booleans) + test "invalid inequality", do: assert_unequal_invalid(@unequal_invalid_booleans) + test "incomparability", do: assert_incomparable(@incomparable_booleans) end - describe "RDF.Numeric" do + describe "XSD.Numeric" do @term_equal_numerics [ - {RDF.integer(42), RDF.integer(42)}, - {RDF.integer("042"), RDF.integer("042")}, - # invalid literals - {RDF.integer("foo"), RDF.integer("foo")}, - {RDF.decimal("foo"), RDF.decimal("foo")}, - {RDF.double("foo"), RDF.double("foo")}, - ] - @term_unequal_numerics [ - {RDF.integer(1), RDF.integer(2)}, - # invalid literals - {RDF.integer("foo"), RDF.integer("bar")}, - {RDF.decimal("foo"), RDF.decimal("bar")}, - {RDF.double("foo"), RDF.double("bar")}, + {XSD.integer(42), XSD.integer(42)}, + {XSD.integer(42), XSD.integer("42")}, + {XSD.integer("042"), XSD.integer("042")}, + {XSD.double("1.0"), XSD.double(1.0)}, + {XSD.double("-42.0"), XSD.double(-42.0)}, + {XSD.double("1.0"), XSD.double(1.0)}, + {XSD.float("1.0"), XSD.float(1.0)}, + {XSD.decimal("1.0"), XSD.decimal(1.0)}, + {XSD.decimal("-42.0"), XSD.decimal(-42.0)}, + {XSD.decimal("1.0"), XSD.decimal(1.0)} ] @value_equal_numerics [ - {RDF.integer("42"), RDF.integer("042")}, - {RDF.integer("42"), RDF.double("42")}, - {RDF.integer(42), RDF.double(42.0)}, - {RDF.integer("42"), RDF.decimal("42")}, - {RDF.integer(42), RDF.decimal(42.0)}, - {RDF.double(3.14), RDF.decimal(3.14)}, - {RDF.double("+0"), RDF.double("-0")}, - {RDF.decimal("+0"), RDF.decimal("-0")}, - # invalid literals - {RDF.integer("foo"), RDF.integer("foo")}, - {RDF.decimal("foo"), RDF.decimal("foo")}, - {RDF.double("foo"), RDF.double("foo")}, + {XSD.integer("42"), XSD.non_negative_integer("42")}, + {XSD.integer("42"), XSD.positive_integer("42")}, + {XSD.integer("42"), XSD.double("42")}, + {XSD.integer("42"), XSD.decimal("42")}, + {XSD.double(3.14), XSD.float(3.14)}, + {XSD.double(3.14), XSD.decimal(3.14)}, + {XSD.float(3.14), XSD.decimal(3.14)}, + {XSD.integer(42), XSD.integer("042")}, + {XSD.integer("42"), XSD.integer("042")}, + {XSD.integer(42), XSD.integer("+42")}, + {XSD.integer("42"), XSD.integer("+42")}, + {XSD.integer(42), XSD.decimal(42.0)}, + {XSD.integer(42), XSD.double(42.0)}, + {XSD.integer(42), XSD.float(42.0)}, + {XSD.non_negative_integer(42), XSD.decimal(42.0)}, + {XSD.non_negative_integer(42), XSD.double(42.0)}, + {XSD.positive_integer(42), XSD.decimal(42.0)}, + {XSD.positive_integer(42), XSD.double(42.0)}, + {XSD.double("+0"), XSD.double("-0")}, + {XSD.double("1"), XSD.double(1.0)}, + {XSD.double("01"), XSD.double(1.0)}, + {XSD.double("1.0E0"), XSD.double(1.0)}, + {XSD.double("1.0E0"), XSD.double("1.0")}, + {XSD.double("+42"), XSD.double(42.0)}, + {XSD.decimal("+0"), XSD.decimal("-0")}, + {XSD.decimal("1"), XSD.decimal(1.0)}, + {XSD.decimal("01"), XSD.decimal(1.0)}, + {XSD.decimal("+42"), XSD.decimal(42.0)} ] - @value_unequal_numerics [ - {RDF.integer("1"), RDF.double("1.1")}, - {RDF.integer("1"), RDF.decimal("1.1")}, - # invalid literals - {RDF.integer("foo"), RDF.integer("bar")}, - {RDF.decimal("foo"), RDF.decimal("bar")}, - {RDF.double("foo"), RDF.double("bar")}, + @unequal_numerics [ + {XSD.integer(1), XSD.integer(2)}, + {XSD.integer("1"), XSD.double("1.1")}, + {XSD.integer("1"), XSD.decimal("1.1")} ] - @value_equal_numerics_by_coercion [ - {RDF.integer(42), 42}, - {RDF.integer(42), 42.0}, - {RDF.integer(42), D.new(42)}, - {RDF.decimal(42), 42}, - {RDF.decimal(3.14), 3.14}, - {RDF.decimal(3.14), D.from_float(3.14)}, - {RDF.double(42), 42}, - {RDF.double(3.14), 3.14}, - {RDF.double(3.14), D.from_float(3.14)}, + @equal_numerics_by_coercion [ + {XSD.integer(42), 42}, + {XSD.integer(42), 42.0}, + {XSD.integer(42), Elixir.Decimal.new(42)}, + {XSD.decimal(42), 42}, + {XSD.decimal(3.14), 3.14}, + {XSD.decimal(3.14), Elixir.Decimal.from_float(3.14)}, + {XSD.double(42), 42}, + {XSD.double(3.14), 3.14}, + {XSD.double(3.14), Elixir.Decimal.from_float(3.14)}, + {XSD.float(3.14), 3.14} ] - @value_unequal_numerics_by_coercion [ - {RDF.integer(3), 3.14}, - {RDF.integer(3), D.from_float(3.14)}, - {RDF.double(3.14), 3}, - {RDF.decimal(3.14), 3}, + @unequal_numerics_by_coercion [ + {XSD.integer(3), 3.14}, + {XSD.integer(3), Elixir.Decimal.from_float(3.14)}, + {XSD.double(3.14), 3}, + {XSD.float(3.14), 3}, + {XSD.decimal(3.14), 3} + ] + @equal_invalid_numerics [ + {XSD.integer("foo"), XSD.integer("foo")}, + {XSD.decimal("foo"), XSD.decimal("foo")}, + {XSD.double("foo"), XSD.double("foo")}, + {XSD.float("foo"), XSD.float("foo")}, + {XSD.non_negative_integer("foo"), XSD.non_negative_integer("foo")}, + {XSD.positive_integer("foo"), XSD.positive_integer("foo")} + ] + @unequal_invalid_numerics [ + {XSD.integer("foo"), XSD.integer("bar")}, + {XSD.decimal("foo"), XSD.decimal("bar")}, + {XSD.decimal("1.0E0"), XSD.decimal(1.0)}, + {XSD.decimal("1.0E0"), XSD.decimal("1.0")}, + {XSD.double("foo"), XSD.double("bar")}, + {XSD.float("foo"), XSD.float("bar")}, + {XSD.non_negative_integer("foo"), XSD.non_negative_integer("bar")}, + {XSD.positive_integer("foo"), XSD.positive_integer("bar")} ] @incomparable_numerics [ - {RDF.integer("42"), nil}, - {RDF.integer("42"), true}, - {RDF.integer("42"), "42"}, - {RDF.integer("42"), RDF.string("42")}, + {XSD.integer("42"), nil}, + {XSD.integer("42"), true}, + {XSD.integer("42"), "42"}, + {XSD.integer("42"), XSD.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 "coerced value equality", do: assert_value_equal @value_equal_numerics_by_coercion - test "coerced value inequality", do: assert_value_unequal @value_unequal_numerics_by_coercion - test "incomparability", do: assert_incomparable @incomparable_numerics + test "term equality", do: assert_term_equal(@term_equal_numerics) + test "value equality", do: assert_value_equal(@value_equal_numerics) + test "inequality", do: assert_unequal(@unequal_numerics) + test "coerced value equality", do: assert_coerced_equal(@equal_numerics_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_numerics_by_coercion) + test "invalid equality", do: assert_equal_invalid(@equal_invalid_numerics) + test "invalid inequality", do: assert_unequal_invalid(@unequal_invalid_numerics) + test "incomparability", do: assert_incomparable(@incomparable_numerics) + + test "NaN is not equal to itself" do + refute XSD.Double.equal_value?(XSD.double(:nan), XSD.double(:nan)) + end end - describe "RDF.DateTime" do + describe "XSD.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")}, - # invalid literals - {RDF.date_time("foo"), RDF.date_time("foo")}, - ] - @term_unequal_datetimes [ - {RDF.date_time("2002-04-02T12:00:00"), RDF.date_time("2002-04-02T17:00:00")}, - # invalid literals - {RDF.date_time("foo"), RDF.date_time("bar")}, + {XSD.datetime("2002-04-02T12:00:00-01:00"), XSD.datetime("2002-04-02T12:00:00-01:00")}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T12: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")}, - - {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 - {RDF.date_time("foo"), RDF.date_time("foo")}, + {XSD.datetime("2002-04-02T12:00:00-01:00"), XSD.datetime("2002-04-02T17:00:00+04:00")}, + {XSD.datetime("2002-04-02T23:00:00-04:00"), XSD.datetime("2002-04-03T02:00:00-01:00")}, + {XSD.datetime("1999-12-31T24:00:00"), XSD.datetime("2000-01-01T00:00:00")}, + {XSD.datetime("2002-04-02T23:00:00Z"), XSD.datetime("2002-04-02T23:00:00+00:00")}, + {XSD.datetime("2002-04-02T23:00:00Z"), XSD.datetime("2002-04-02T23:00:00-00:00")}, + {XSD.datetime("2010-01-01T00:00:00+00:00"), XSD.datetime("2010-01-01T00:00:00Z")}, + {XSD.datetime("2002-04-02T23:00:00+00:00"), XSD.datetime("2002-04-02T23:00:00-00:00")}, + {XSD.datetime("2010-01-01T00:00:00.0000Z"), XSD.datetime("2010-01-01T00:00:00Z")}, + {XSD.datetime("2005-04-04T24:00:00"), XSD.datetime("2005-04-05T00:00:00")} ] - @value_unequal_datetimes [ - {RDF.date_time("2005-04-04T24:00:00"), RDF.date_time("2005-04-04T00:00:00")}, - # invalid literals - {RDF.date_time("foo"), RDF.date_time("bar")}, + @unequal_datetimes [ + {XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T17:00:00")}, + {XSD.datetime("2005-04-04T24:00:00"), XSD.datetime("2005-04-04T00:00:00")} ] - @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"), ~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)}, + @equal_datetimes_by_coercion [ + {XSD.datetime("2002-04-02T12:00:00-01:00"), + elem(DateTime.from_iso8601("2002-04-02T12:00:00-01:00"), 1)}, + {XSD.datetime("2002-04-02T12:00:00"), ~N"2002-04-02T12:00:00"}, + {XSD.datetime("2002-04-02T23:00:00Z"), + elem(DateTime.from_iso8601("2002-04-02T23:00:00+00:00"), 1)}, + {XSD.datetime("2002-04-02T23:00:00+00:00"), + elem(DateTime.from_iso8601("2002-04-02T23:00:00Z"), 1)}, + {XSD.datetime("2002-04-02T23:00:00-00:00"), + elem(DateTime.from_iso8601("2002-04-02T23:00:00Z"), 1)}, + {XSD.datetime("2002-04-02T23:00:00-00:00"), + elem(DateTime.from_iso8601("2002-04-02T23:00:00+00:00"), 1)} ] - @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)}, + @unequal_datetimes_by_coercion [ + {XSD.datetime("2002-04-02T12:00:00-01:00"), + elem(DateTime.from_iso8601("2002-04-02T12:00:00+00:00"), 1)} + ] + @equal_invalid_datetimes [ + {XSD.datetime("foo"), XSD.datetime("foo")} + ] + @unequal_invalid_datetimes [ + {XSD.datetime("foo"), XSD.datetime("bar")} ] @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")}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T12:00:00Z")}, + {XSD.datetime("2010-01-01T00:00:00Z"), XSD.datetime("2010-01-01T00:00:00")}, + {XSD.string("2002-04-02T12:00:00-01:00"), XSD.datetime("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")}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T23:00:00+00: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 "coerced value equality", do: assert_value_equal @value_equal_datetimes_by_coercion - test "coerced value inequality", do: assert_value_unequal @value_unequal_datetimes_by_coercion - test "incomparability", do: assert_incomparable @incomparable_datetimes + test "term equality", do: assert_term_equal(@term_equal_datetimes) + test "value equality", do: assert_value_equal(@value_equal_datetimes) + test "inequality", do: assert_unequal(@unequal_datetimes) + test "coerced value equality", do: assert_coerced_equal(@equal_datetimes_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_datetimes_by_coercion) + test "invalid equality", do: assert_equal_invalid(@equal_invalid_datetimes) + test "invalid inequality", do: assert_unequal_invalid(@unequal_invalid_datetimes) + test "incomparability", do: assert_incomparable(@incomparable_datetimes) end - describe "RDF.Date" do + describe "XSD.Date" do @term_equal_dates [ - {RDF.date("2002-04-02-01:00"), RDF.date("2002-04-02-01:00")}, - {RDF.date("2002-04-02"), RDF.date("2002-04-02")}, - # invalid literals - {RDF.date("foo"), RDF.date("foo")}, - ] - @term_unequal_dates [ - {RDF.date("2002-04-01"), RDF.date("2002-04-02")}, - # invalid literals - {RDF.date("foo"), RDF.date("bar")}, + {XSD.date("2002-04-02-01:00"), XSD.date("2002-04-02-01:00")}, + {XSD.date("2002-04-02"), XSD.date("2002-04-02")} ] @value_equal_dates [ - {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")}, + {XSD.date("2002-04-02-00:00"), XSD.date("2002-04-02+00:00")}, + {XSD.date("2002-04-02Z"), XSD.date("2002-04-02+00:00")}, + {XSD.date("2002-04-02Z"), XSD.date("2002-04-02-00:00")} ] - @value_unequal_dates [ - {RDF.date("2002-04-03Z"), RDF.date("2002-04-02")}, - {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")}, + @unequal_dates [ + {XSD.date("2002-04-01"), XSD.date("2002-04-02")} ] - @value_equal_dates_by_coercion [ - {RDF.date("2002-04-02"), Date.from_iso8601!("2002-04-02")}, + @equal_dates_by_coercion [ + {XSD.date("2002-04-02"), Date.from_iso8601!("2002-04-02")} ] - @value_unequal_dates_by_coercion [ - {RDF.date("2002-04-02"), Date.from_iso8601!("2002-04-03")}, - {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")}, + @unequal_dates_by_coercion [ + {XSD.date("2002-04-02"), Date.from_iso8601!("2002-04-03")} + ] + @equal_invalid_dates [ + {XSD.date("foo"), XSD.date("foo")} + ] + @unequal_invalid_dates [ + {XSD.date("2002.04.02"), XSD.date("2002-04-02")}, + {XSD.date("foo"), XSD.date("bar")} ] @incomparable_dates [ - {RDF.date("2002-04-02"), RDF.string("2002-04-02")}, + {XSD.date("2002-04-02"), XSD.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")}, + {XSD.date("2002-04-02Z"), XSD.date("2002-04-02")}, + {XSD.date("2002-04-02"), XSD.date("2002-04-02Z")}, + {XSD.date("2010-01-01Z"), XSD.date(~D[2010-01-01])}, + {XSD.date("2010-01-01+00:00"), XSD.date(~D[2010-01-01])}, + {XSD.date("2002-04-02+00:00"), XSD.date("2002-04-02")}, + {XSD.date("2002-04-02-00:00"), XSD.date("2002-04-02")}, + {XSD.date("2002-04-02+01:00"), Date.from_iso8601!("2002-04-02")}, + {XSD.date("2002-04-02Z"), Date.from_iso8601!("2002-04-02")}, + {XSD.date("2002-04-02+00:00"), Date.from_iso8601!("2002-04-02")}, + {XSD.date("2002-04-02-00:00"), Date.from_iso8601!("2002-04-02")} ] - test "term equality", do: assert_term_equal @term_equal_dates - test "term inequality", do: assert_term_unequal @term_unequal_dates - test "value equality", do: assert_value_equal @value_equal_dates - test "value inequality", do: assert_value_unequal @value_unequal_dates - test "coerced value equality", do: assert_value_equal @value_equal_dates_by_coercion - test "coerced value inequality", do: assert_value_unequal @value_unequal_dates_by_coercion - test "incomparability", do: assert_incomparable @incomparable_dates + test "term equality", do: assert_term_equal(@term_equal_dates) + test "value equality", do: assert_value_equal(@value_equal_dates) + test "inequality", do: assert_unequal(@unequal_dates) + test "coerced value equality", do: assert_coerced_equal(@equal_dates_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_dates_by_coercion) + test "invalid equality", do: assert_equal_invalid(@equal_invalid_dates) + test "invalid inequality", do: assert_unequal_invalid(@unequal_invalid_dates) + test "incomparability", do: assert_incomparable(@incomparable_dates) end - describe "equality between RDF.Date and RDF.DateTime" do + describe "equality between XSD.Date and XSD.DateTime" do # 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. # # @value_equal_dates_and_datetimes [ - # {RDF.date("2002-04-02"), RDF.datetime("2002-04-02T00:00:00")}, - # {RDF.datetime("2002-04-02T00:00:00"), RDF.date("2002-04-02")}, - # {RDF.date("2002-04-02Z"), RDF.datetime("2002-04-02T00:00:00Z")}, - # {RDF.datetime("2002-04-02T00:00:00Z"), RDF.date("2002-04-02Z")}, - # {RDF.date("2002-04-02Z"), RDF.datetime("2002-04-02T00:00:00+00:00")}, - # {RDF.datetime("2002-04-02T00:00:00-00:00"), RDF.date("2002-04-02Z")}, + # {XSD.date("2002-04-02"), XSD.datetime("2002-04-02T00:00:00")}, + # {XSD.datetime("2002-04-02T00:00:00"), XSD.date("2002-04-02")}, + # {XSD.date("2002-04-02Z"), XSD.datetime("2002-04-02T00:00:00Z")}, + # {XSD.datetime("2002-04-02T00:00:00Z"), XSD.date("2002-04-02Z")}, + # {XSD.date("2002-04-02Z"), XSD.datetime("2002-04-02T00:00:00+00:00")}, + # {XSD.datetime("2002-04-02T00:00:00-00:00"), XSD.date("2002-04-02Z")}, # ] # @value_unequal_dates_and_datetimes [ - # {RDF.date("2002-04-01"), RDF.datetime("2002-04-02T00:00:00")}, - # {RDF.datetime("2002-04-01T00:00:00"), RDF.date("2002-04-02")}, - # {RDF.date("2002-04-01Z"), RDF.datetime("2002-04-02T00:00:00Z")}, - # {RDF.datetime("2002-04-01T00:00:00Z"), RDF.date("2002-04-02Z")}, - # {RDF.date("2002-04-01Z"), RDF.datetime("2002-04-02T00:00:00+00:00")}, - # {RDF.datetime("2002-04-01T00:00:00-00:00"), RDF.date("2002-04-02Z")}, + # {XSD.date("2002-04-01"), XSD.datetime("2002-04-02T00:00:00")}, + # {XSD.datetime("2002-04-01T00:00:00"), XSD.date("2002-04-02")}, + # {XSD.date("2002-04-01Z"), XSD.datetime("2002-04-02T00:00:00Z")}, + # {XSD.datetime("2002-04-01T00:00:00Z"), XSD.date("2002-04-02Z")}, + # {XSD.date("2002-04-01Z"), XSD.datetime("2002-04-02T00:00:00+00:00")}, + # {XSD.datetime("2002-04-01T00:00:00-00:00"), XSD.date("2002-04-02Z")}, # ] # @incomparable_dates_and_datetimes [ - # {RDF.date("2002-04-02Z"), RDF.datetime("2002-04-02T00:00:00")}, - # {RDF.datetime("2002-04-02T00:00:00Z"), RDF.date("2002-04-02")}, - # {RDF.date("2002-04-02"), RDF.datetime("2002-04-02T00:00:00Z")}, - # {RDF.datetime("2002-04-02T00:00:00"), RDF.date("2002-04-02Z")}, + # {XSD.date("2002-04-02Z"), XSD.datetime("2002-04-02T00:00:00")}, + # {XSD.datetime("2002-04-02T00:00:00Z"), XSD.date("2002-04-02")}, + # {XSD.date("2002-04-02"), XSD.datetime("2002-04-02T00:00:00Z")}, + # {XSD.datetime("2002-04-02T00:00:00"), XSD.date("2002-04-02Z")}, # ] # # test "value equality", do: assert_value_equal @value_equal_dates_and_datetimes @@ -363,49 +411,82 @@ defmodule RDF.EqualityTest do # test "incomparability", do: assert_incomparable @incomparable_dates_and_datetimes @value_unequal_dates_and_datetimes [ - {RDF.datetime("2002-04-02T00:00:00"), RDF.date("2002-04-02")}, - {RDF.datetime("2002-04-02T00:00:00"), RDF.date("2002-04-01")}, + {XSD.datetime("2002-04-02T00:00:00"), XSD.date("2002-04-02")}, + {XSD.datetime("2002-04-02T00:00:00"), XSD.date("2002-04-01")} ] - test "value inequality", do: assert_value_unequal @value_unequal_dates_and_datetimes + test "value inequality", do: assert_unequal(@value_unequal_dates_and_datetimes) end - describe "RDF.Time" do + describe "XSD.Time" do @term_equal_times [ - {RDF.time("12:00:00+01:00"), RDF.time("12:00:00+01:00")}, - {RDF.time("12:00:00"), RDF.time("12:00:00")}, - # invalid literals - {RDF.time("foo"), RDF.time("foo")}, - ] - @term_unequal_times [ - {RDF.time("12:00:00"), RDF.time("13:00:00")}, - # invalid literals - {RDF.time("foo"), RDF.time("bar")}, + {XSD.time("12:00:00+01:00"), XSD.time("12:00:00+01:00")}, + {XSD.time("12:00:00"), XSD.time("12:00:00")} ] @value_equal_times [ + {XSD.time("00:00:00+00:00"), XSD.time("00:00:00Z")} ] - @value_unequal_times [ + @unequal_times [ + {XSD.time("12:00:00"), XSD.time("13:00:00")}, + {XSD.time("00:00:00.0000Z"), XSD.time("00:00:00Z")} ] - @value_equal_times_by_coercion [ - {RDF.time("12:00:00"), Time.from_iso8601!("12:00:00")}, + @equal_times_by_coercion [ + {XSD.time("12:00:00"), Time.from_iso8601!("12:00:00")} ] - @value_unequal_times_by_coercion [ - {RDF.time("12:00:00"), Time.from_iso8601!("13:00:00")}, + @unequal_times_by_coercion [ + {XSD.time("12:00:00"), Time.from_iso8601!("13:00:00")} + ] + @equal_invalid_times [ + {XSD.time("foo"), XSD.time("foo")} + ] + @unequal_invalid_times [ + {XSD.time("foo"), XSD.time("bar")} ] @incomparable_times [ - {RDF.time("12:00:00"), RDF.string("12:00:00")}, + {XSD.time("12:00:00"), XSD.string("12:00:00")}, + {XSD.time("00:00:00"), XSD.time("00:00:00Z")}, + {XSD.time("00:00:00.0000"), XSD.time("00:00:00Z")} ] - test "term equality", do: assert_term_equal @term_equal_times - test "term inequality", do: assert_term_unequal @term_unequal_times - test "value equality", do: assert_value_equal @value_equal_times - test "value inequality", do: assert_value_unequal @value_unequal_times - test "coerced value equality", do: assert_value_equal @value_equal_times_by_coercion - test "coerced value inequality", do: assert_value_unequal @value_unequal_times_by_coercion - test "incomparability", do: assert_incomparable @incomparable_times + test "term equality", do: assert_term_equal(@term_equal_times) + test "value equality", do: assert_value_equal(@value_equal_times) + test "inequality", do: assert_unequal(@unequal_times) + test "coerced value equality", do: assert_coerced_equal(@equal_times_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_times_by_coercion) + test "invalid equality", do: assert_equal_invalid(@equal_invalid_times) + test "invalid inequality", do: assert_unequal_invalid(@unequal_invalid_times) + test "incomparability", do: assert_incomparable(@incomparable_times) end - describe "RDF.Literals with unsupported types" do + describe "XSD.AnyURI" do + @term_equal_uris [ + {XSD.any_uri("http://example.com"), XSD.any_uri("http://example.com")} + ] + @value_equal_uris [] + @unequal_uris [ + {XSD.any_uri("http://example.com"), XSD.any_uri("http://example.com#foo")} + ] + @equal_uris_by_coercion [ + {XSD.any_uri("http://example.com"), URI.parse("http://example.com")} + ] + @unequal_uris_by_coercion [ + {XSD.any_uri("http://example.com"), URI.parse("http://example.com#foo")} + ] + @incomparable_uris [ + {XSD.any_uri("http://example.com"), 42}, + {XSD.any_uri("http://example.com"), "http://example.com"}, + {XSD.any_uri("http://example.com"), XSD.string("http://example.com")} + ] + + test "term equality", do: assert_term_equal(@term_equal_uris) + test "value equality", do: assert_value_equal(@value_equal_uris) + test "inequality", do: assert_unequal(@unequal_uris) + test "coerced value equality", do: assert_coerced_equal(@equal_uris_by_coercion) + test "coerced value inequality", do: assert_coerced_unequal(@unequal_uris_by_coercion) + test "incomparability", do: assert_incomparable(@incomparable_uris) + end + + describe "RDF.Literal.Generics" do @equal_literals [ {RDF.literal("foo", datatype: "http://example.com/datatype"), RDF.literal("foo", datatype: "http://example.com/datatype")}, @@ -419,72 +500,110 @@ defmodule RDF.EqualityTest do RDF.literal("foo", datatype: "http://example.com/datatype2")}, ] - test "term equality", do: assert_term_equal @equal_literals - test "term inequality", do: assert_value_unequal @unequal_literals - test "incomparability", do: assert_incomparable @incomparable_literals + test "equality", do: assert_term_equal @equal_literals + test "inequality", do: assert_unequal @unequal_literals + test "incomparability", do: assert_incomparable @incomparable_literals 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 + Enum.each(examples, fn example -> assert_equality(example, true) end) + 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_value_equal(examples) do - Enum.each examples, fn example -> assert_value_equality(example, true) end + Enum.each(examples, fn example -> assert_equality(example, false) end) + Enum.each(examples, fn example -> assert_term_equality(example, false) end) + 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 + defp assert_unequal(examples) do + Enum.each(examples, fn example -> assert_equality(example, false) end) + 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_coerced_equal(examples) do + Enum.each(examples, fn example -> assert_equality(example, false) end) + Enum.each(examples, fn example -> assert_term_equality(example, false) end) + Enum.each(examples, fn example -> assert_value_equality(example, true) end) + end + + defp assert_coerced_unequal(examples) do + Enum.each(examples, fn example -> assert_equality(example, false) end) + Enum.each(examples, fn example -> assert_term_equality(example, false) end) + Enum.each(examples, fn example -> assert_value_equality(example, false) end) + end + + def assert_equal_invalid(examples) do + Enum.each(examples, fn example -> assert_equality(example, true) end) + Enum.each(examples, fn example -> assert_term_equality(example, true) end) + Enum.each(examples, fn example -> assert_value_equality(example, true) end) + end + + def assert_unequal_invalid(examples) do + Enum.each(examples, fn example -> assert_equality(example, false) end) + 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_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 + Enum.each(examples, fn example -> assert_equality(example, false) end) + 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_equality({left, right}, expected) do + result = left == right + + assert result == expected, """ + expected #{inspect(left)} == #{inspect(right)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} + """ 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} + #{inspect(left)}, + #{inspect(right)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} """ result = RDF.Term.equal?(right, left) + assert result == expected, """ expected RDF.Term.equal?( - #{inspect right}, - #{inspect left}) - to be: #{inspect expected} - but got: #{inspect result} + #{inspect(right)}, + #{inspect(left)}) + 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} + #{inspect(left)}, + #{inspect(right)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} """ result = RDF.Term.equal_value?(right, left) + assert result == expected, """ expected RDF.Term.equal_value?( - #{inspect right}, - #{inspect left}) - to be: #{inspect expected} - but got: #{inspect result} + #{inspect(right)}, + #{inspect(left)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} """ end end diff --git a/test/unit/list_test.exs b/test/unit/list_test.exs index 0ffb6c4..5c9a97a 100644 --- a/test/unit/list_test.exs +++ b/test/unit/list_test.exs @@ -190,7 +190,7 @@ defmodule RDF.ListTest do RDF.List.from([[1]]) assert [nested] = get_in(graph_with_list, [bnode, RDF.first]) assert get_in(graph_with_list, [bnode, RDF.rest]) == [RDF.nil] - assert get_in(graph_with_list, [nested, RDF.first]) == [RDF.integer(1)] + assert get_in(graph_with_list, [nested, RDF.first]) == [XSD.integer(1)] assert get_in(graph_with_list, [nested, RDF.rest]) == [RDF.nil] assert %RDF.List{head: bnode, graph: graph_with_list} = @@ -198,9 +198,9 @@ defmodule RDF.ListTest do assert get_in(graph_with_list, [bnode, RDF.first]) == [~L"foo"] assert [second] = get_in(graph_with_list, [bnode, RDF.rest]) assert [nested] = get_in(graph_with_list, [second, RDF.first]) - assert get_in(graph_with_list, [nested, RDF.first]) == [RDF.integer(1)] + assert get_in(graph_with_list, [nested, RDF.first]) == [XSD.integer(1)] assert [nested_second] = get_in(graph_with_list, [nested, RDF.rest]) - assert get_in(graph_with_list, [nested_second, RDF.first]) == [RDF.integer(2)] + assert get_in(graph_with_list, [nested_second, RDF.first]) == [XSD.integer(2)] assert get_in(graph_with_list, [nested_second, RDF.rest]) == [RDF.nil] assert [third] = get_in(graph_with_list, [second, RDF.rest]) assert get_in(graph_with_list, [third, RDF.first]) == [~L"bar"] @@ -273,17 +273,17 @@ defmodule RDF.ListTest do test "nested list", %{nested: nested} do assert RDF.List.values(nested) == - [~L"foo", [RDF.integer(1), RDF.integer(2)], ~L"bar"] + [~L"foo", [XSD.integer(1), XSD.integer(2)], ~L"bar"] assert RDF.list(["foo", [1, 2]]) |> RDF.List.values == - [~L"foo", [RDF.integer(1), RDF.integer(2)]] + [~L"foo", [XSD.integer(1), XSD.integer(2)]] assert RDF.list([[1, 2], "foo"]) |> RDF.List.values == - [[RDF.integer(1), RDF.integer(2)], ~L"foo"] + [[XSD.integer(1), XSD.integer(2)], ~L"foo"] inner_list = RDF.list([1, 2], head: ~B) assert RDF.list(["foo", ~B], graph: inner_list.graph) - |> RDF.List.values == [~L"foo", [RDF.integer(1), RDF.integer(2)]] + |> RDF.List.values == [~L"foo", [XSD.integer(1), XSD.integer(2)]] end end diff --git a/test/unit/literal/datatype_test.exs b/test/unit/literal/datatype_test.exs new file mode 100644 index 0000000..4e87c2c --- /dev/null +++ b/test/unit/literal/datatype_test.exs @@ -0,0 +1,5 @@ +defmodule RDF.Literal.DatatypeTest do + use RDF.Test.Case + + doctest RDF.Literal.Datatype +end diff --git a/test/unit/literal_test.exs b/test/unit/literal_test.exs index bbb13ad..68e53f9 100644 --- a/test/unit/literal_test.exs +++ b/test/unit/literal_test.exs @@ -4,19 +4,20 @@ defmodule RDF.LiteralTest do import RDF.Sigils import RDF.TestLiterals - alias RDF.{Literal, LangString} + alias RDF.{Literal, XSD, LangString} alias RDF.Literal.{Generic, Datatype} + alias Decimal, as: D doctest RDF.Literal alias RDF.NS @examples %{ - RDF.XSD.String => ["foo"], - RDF.XSD.Integer => [42], - RDF.XSD.Double => [3.14], - RDF.XSD.Decimal => [Decimal.from_float(3.14)], - RDF.XSD.Boolean => [true, false], + XSD.String => ["foo"], + XSD.Integer => [42], + XSD.Double => [3.14], + XSD.Decimal => [Decimal.from_float(3.14)], + XSD.Boolean => [true, false], } describe "new/1" do @@ -32,52 +33,51 @@ defmodule RDF.LiteralTest do test "with typed literals" do Enum.each Datatype.Registry.datatypes() -- [RDF.LangString], fn datatype -> - literal_type = datatype.literal_type() - assert %Literal{literal: typed_literal} = Literal.new(literal_type.new("foo")) - assert typed_literal.__struct__ == literal_type + assert %Literal{literal: typed_literal} = Literal.new(datatype.new("foo")) + assert typed_literal.__struct__ == datatype end end test "when options without datatype given" do - assert Literal.new(true, []) == RDF.XSD.Boolean.new(true) - assert Literal.new(42, []) == RDF.XSD.Integer.new(42) - assert Literal.new!(true, []) == RDF.XSD.Boolean.new!(true) - assert Literal.new!(42, []) == RDF.XSD.Integer.new!(42) + assert Literal.new(true, []) == XSD.Boolean.new(true) + assert Literal.new(42, []) == XSD.Integer.new(42) + assert Literal.new!(true, []) == XSD.Boolean.new!(true) + assert Literal.new!(42, []) == XSD.Integer.new!(42) end end describe "typed construction" do test "boolean" do - assert Literal.new(true, datatype: NS.XSD.boolean) == RDF.XSD.Boolean.new(true) - assert Literal.new(false, datatype: NS.XSD.boolean) == RDF.XSD.Boolean.new(false) - assert Literal.new("true", datatype: NS.XSD.boolean) == RDF.XSD.Boolean.new("true") - assert Literal.new("false", datatype: NS.XSD.boolean) == RDF.XSD.Boolean.new("false") + assert Literal.new(true, datatype: NS.XSD.boolean) == XSD.Boolean.new(true) + assert Literal.new(false, datatype: NS.XSD.boolean) == XSD.Boolean.new(false) + assert Literal.new("true", datatype: NS.XSD.boolean) == XSD.Boolean.new("true") + assert Literal.new("false", datatype: NS.XSD.boolean) == XSD.Boolean.new("false") end test "integer" do - assert Literal.new(42, datatype: NS.XSD.integer) == RDF.XSD.Integer.new(42) - assert Literal.new("42", datatype: NS.XSD.integer) == RDF.XSD.Integer.new("42") + assert Literal.new(42, datatype: NS.XSD.integer) == XSD.Integer.new(42) + assert Literal.new("42", datatype: NS.XSD.integer) == XSD.Integer.new("42") end test "double" do - assert Literal.new(3.14, datatype: NS.XSD.double) == RDF.XSD.Double.new(3.14) - assert Literal.new("3.14", datatype: NS.XSD.double) == RDF.XSD.Double.new("3.14") + assert Literal.new(3.14, datatype: NS.XSD.double) == XSD.Double.new(3.14) + assert Literal.new("3.14", datatype: NS.XSD.double) == XSD.Double.new("3.14") end test "decimal" do - assert Literal.new(3.14, datatype: NS.XSD.decimal) == RDF.XSD.Decimal.new(3.14) - assert Literal.new("3.14", datatype: NS.XSD.decimal) == RDF.XSD.Decimal.new("3.14") + assert Literal.new(3.14, datatype: NS.XSD.decimal) == XSD.Decimal.new(3.14) + assert Literal.new("3.14", datatype: NS.XSD.decimal) == XSD.Decimal.new("3.14") assert Literal.new(Decimal.from_float(3.14), datatype: NS.XSD.decimal) == - RDF.XSD.Decimal.new(Decimal.from_float(3.14)) + XSD.Decimal.new(Decimal.from_float(3.14)) end test "unsignedInt" do - assert Literal.new(42, datatype: NS.XSD.unsignedInt) == RDF.XSD.UnsignedInt.new(42) - assert Literal.new("42", datatype: NS.XSD.unsignedInt) == RDF.XSD.UnsignedInt.new("42") + assert Literal.new(42, datatype: NS.XSD.unsignedInt) == XSD.UnsignedInt.new(42) + assert Literal.new("42", datatype: NS.XSD.unsignedInt) == XSD.UnsignedInt.new("42") end test "string" do - assert Literal.new("foo", datatype: NS.XSD.string) == RDF.XSD.String.new("foo") + assert Literal.new("foo", datatype: NS.XSD.string) == XSD.String.new("foo") end test "unmapped/unknown datatype" do @@ -113,6 +113,66 @@ defmodule RDF.LiteralTest do end end + describe "coerce/1" do + test "with boolean" do + assert Literal.coerce(true) == XSD.true() + assert Literal.coerce(false) == XSD.false() + end + + test "with string" do + assert Literal.coerce("foo") == XSD.string("foo") + end + + test "with integer" do + assert Literal.coerce(42) == XSD.integer(42) + end + + test "with float" do + assert Literal.coerce(3.14) == XSD.double(3.14) + end + + test "with decimal" do + assert D.from_float(3.14) |> Literal.coerce() == XSD.decimal(3.14) + end + + test "with datetime" do + assert DateTime.from_iso8601("2002-04-02T12:00:00+00:00") |> elem(1) |> Literal.coerce() == + DateTime.from_iso8601("2002-04-02T12:00:00+00:00") |> elem(1) |> XSD.datetime() + end + + test "with naive datetime" do + assert ~N"2002-04-02T12:00:00" |> Literal.coerce() == + ~N"2002-04-02T12:00:00" |> XSD.datetime() + end + + test "with date" do + assert ~D"2002-04-02" |> Literal.coerce() == + ~D"2002-04-02" |> XSD.date() + end + + test "with time" do + assert ~T"12:00:00" |> Literal.coerce() == + ~T"12:00:00" |> XSD.time() + end + + test "with URI" do + assert URI.parse("http://example.com") |> Literal.coerce() == + XSD.any_uri("http://example.com") + end + + test "with RDF.Literals" do + assert XSD.integer(42) |> Literal.coerce() == XSD.integer(42) + end + + test "with RDF datatype Literals" do + assert %XSD.Integer{value: 42} |> Literal.coerce() == XSD.integer(42) + end + + test "with inconvertible values" do + assert self() |> Literal.coerce() == nil + end + end + describe "has_datatype?" do Enum.each literals(~W[all_simple all_plain_lang]a), fn literal -> @tag literal: literal @@ -241,16 +301,16 @@ defmodule RDF.LiteralTest do describe "canonical/1" do test "with XSD.Datatype literal" do [ - RDF.XSD.String.new("foo"), - RDF.XSD.Byte.new(42), + XSD.String.new("foo"), + XSD.Byte.new(42), ] |> Enum.each(fn canonical_literal -> assert Literal.canonical(canonical_literal) == canonical_literal end) - assert RDF.XSD.Integer.new("042") |> Literal.canonical() == Literal.new(42) - assert Literal.new(3.14) |> Literal.canonical() == Literal.new(3.14) |> RDF.XSD.Double.canonical() + assert XSD.Integer.new("042") |> Literal.canonical() == Literal.new(42) + assert Literal.new(3.14) |> Literal.canonical() == Literal.new(3.14) |> XSD.Double.canonical() end test "with RDF.LangString literal" do @@ -284,7 +344,7 @@ defmodule RDF.LiteralTest do test "with XSD.Datatype literal" do assert Literal.new("foo") |> Literal.valid?() == true assert Literal.new(42) |> Literal.valid?() == true - assert RDF.XSD.Integer.new("foo") |> Literal.valid?() == false + assert XSD.Integer.new("foo") |> Literal.valid?() == false end test "with RDF.LangString literal" do @@ -300,7 +360,7 @@ defmodule RDF.LiteralTest do describe "equal_value?/2" do test "with XSD.Datatype literal" do assert Literal.equal_value?(Literal.new("foo"), Literal.new("foo")) == true - assert Literal.equal_value?(Literal.new(42), RDF.XSD.Byte.new(42)) == true + assert Literal.equal_value?(Literal.new(42), XSD.Byte.new(42)) == true assert Literal.equal_value?(Literal.new("foo"), "foo") == true assert Literal.equal_value?(Literal.new(42), 42) == true assert Literal.equal_value?(Literal.new(42), 42.0) == true @@ -325,7 +385,7 @@ defmodule RDF.LiteralTest do describe "compare/2" do test "with XSD.Datatype literal" do assert Literal.compare(Literal.new("foo"), Literal.new("bar")) == :gt - assert Literal.compare(Literal.new(42), RDF.XSD.Byte.new(43)) == :lt + assert Literal.compare(Literal.new(42), XSD.Byte.new(43)) == :lt end test "with RDF.LangString literal" do @@ -339,7 +399,7 @@ defmodule RDF.LiteralTest do end end - @poem RDF.XSD.String.new """ + @poem XSD.String.new """ Kaum hat dies der Hahn gesehen, Fängt er auch schon an zu krähen: @@ -365,8 +425,8 @@ defmodule RDF.LiteralTest do {~L"abracadabra"en, ~L"bra", true}, {"abracadabra", "bra", true}, - {RDF.XSD.Integer.new("42"), ~L"4", true}, - {RDF.XSD.Integer.new("42"), ~L"en", false}, + {XSD.Integer.new("42"), ~L"4", true}, + {XSD.Integer.new("42"), ~L"en", false}, ] |> Enum.each(fn {literal, pattern, expected_result} -> result = Literal.matches?(literal, pattern) @@ -404,16 +464,16 @@ defmodule RDF.LiteralTest do describe "update/2" do test "it updates value and lexical form" do - assert RDF.string("foo") + assert XSD.string("foo") |> Literal.update(fn s when is_binary(s) -> s <> "bar" end) == - RDF.string("foobar") - assert RDF.integer(1) |> Literal.update(fn i when is_integer(i) -> i + 1 end) == - RDF.integer(2) - assert RDF.byte(42) |> Literal.update(fn i when is_integer(i) -> i + 1 end) == - RDF.byte(43) - assert RDF.integer(1) + XSD.string("foobar") + assert XSD.integer(1) |> Literal.update(fn i when is_integer(i) -> i + 1 end) == + XSD.integer(2) + assert XSD.byte(42) |> Literal.update(fn i when is_integer(i) -> i + 1 end) == + XSD.byte(43) + assert XSD.integer(1) |> Literal.update(fn i when is_integer(i) -> "0" <> to_string(i) end) == - RDF.integer("01") + XSD.integer("01") end test "it does not change the datatype of generic literals" do @@ -429,9 +489,9 @@ defmodule RDF.LiteralTest do end test "with as: :lexical opt it passes the lexical form" do - assert RDF.integer(1) + assert XSD.integer(1) |> Literal.update(fn i when is_binary(i) -> "0" <> i end, as: :lexical) == - RDF.integer("01") + XSD.integer("01") end end @@ -439,7 +499,7 @@ defmodule RDF.LiteralTest do test "with XSD.Datatype literal" do assert Literal.new("foo") |> to_string() == "foo" assert Literal.new(42) |> to_string() == "42" - assert RDF.XSD.Integer.new("foo") |> to_string() == "foo" + assert XSD.Integer.new("foo") |> to_string() == "foo" end test "with RDF.LangString literal" do diff --git a/test/unit/quad_test.exs b/test/unit/quad_test.exs index 3f789e6..a84d4a0 100644 --- a/test/unit/quad_test.exs +++ b/test/unit/quad_test.exs @@ -7,9 +7,9 @@ defmodule RDF.QuadTest do describe "values/1" do test "with a valid RDF.Quad" do - assert Quad.values({~I, ~I, RDF.integer(42), ~I}) + assert Quad.values({~I, ~I, XSD.integer(42), ~I}) == {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"} - assert Quad.values({~I, ~I, RDF.integer(42), nil}) + assert Quad.values({~I, ~I, XSD.integer(42), nil}) == {"http://example.com/S", "http://example.com/p", 42, nil} end @@ -20,7 +20,7 @@ defmodule RDF.QuadTest do end test "values/2" do - assert {~I, ~I, RDF.integer(42), ~I} + assert {~I, ~I, XSD.integer(42), ~I} |> Quad.values(fn {:subject, subject} -> subject |> to_string() |> String.last() |> String.to_atom() {:predicate, _} -> :p diff --git a/test/unit/rdf_test.exs b/test/unit/rdf_test.exs index 341b43a..2b61a62 100644 --- a/test/unit/rdf_test.exs +++ b/test/unit/rdf_test.exs @@ -4,16 +4,7 @@ defmodule RDFTest do doctest RDF test "Datatype constructor alias functions" do - RDF.Literal.Datatype.Registry.datatypes() -- [RDF.LangString] - |> Enum.each(fn datatype -> - assert apply(RDF, String.to_atom(datatype.name), [1]) == datatype.new(1) - assert apply(RDF, String.to_atom(Macro.underscore(datatype.name)), [1]) == datatype.new(1) - end) - end - - test "true and false aliases" do - assert RDF.true == RDF.XSD.Boolean.new(true) - assert RDF.false == RDF.XSD.Boolean.new(false) + assert RDF.langString("foo", language: "en") == RDF.Literal.new("foo", language: "en") end describe "default_prefixes/0" do diff --git a/test/unit/statement_test.exs b/test/unit/statement_test.exs index 6f87d7c..14c0db7 100644 --- a/test/unit/statement_test.exs +++ b/test/unit/statement_test.exs @@ -7,7 +7,7 @@ defmodule RDF.StatementTest do @iri ~I @bnode ~B @valid_literal ~L"foo" - @invalid_literal RDF.integer("foo") + @invalid_literal XSD.integer("foo") @valid_triples [ {@iri, @iri, @iri}, diff --git a/test/unit/term_test.exs b/test/unit/term_test.exs index 709224f..fa7df1e 100644 --- a/test/unit/term_test.exs +++ b/test/unit/term_test.exs @@ -19,8 +19,8 @@ defmodule RDF.TermTest do end test "with boolean" do - assert RDF.Term.coerce(true) == RDF.true - assert RDF.Term.coerce(false) == RDF.false + assert RDF.Term.coerce(true) == XSD.true + assert RDF.Term.coerce(false) == XSD.false end test "with string" do @@ -28,32 +28,32 @@ defmodule RDF.TermTest do end test "with integer" do - assert RDF.Term.coerce(42) == RDF.integer(42) + assert RDF.Term.coerce(42) == XSD.integer(42) end test "with float" do - assert RDF.Term.coerce(3.14) == RDF.double(3.14) + assert RDF.Term.coerce(3.14) == XSD.double(3.14) end test "with decimal" do - assert D.from_float(3.14) |> RDF.Term.coerce() == RDF.decimal(3.14) + assert D.from_float(3.14) |> RDF.Term.coerce() == XSD.decimal(3.14) end test "with datetime" do assert DateTime.from_iso8601("2002-04-02T12:00:00+00:00") |> elem(1) |> RDF.Term.coerce() == - DateTime.from_iso8601("2002-04-02T12:00:00+00:00") |> elem(1) |> RDF.datetime() + DateTime.from_iso8601("2002-04-02T12:00:00+00:00") |> elem(1) |> XSD.datetime() assert ~N"2002-04-02T12:00:00" |> RDF.Term.coerce() == - ~N"2002-04-02T12:00:00" |> RDF.datetime() + ~N"2002-04-02T12:00:00" |> XSD.datetime() end test "with date" do assert ~D"2002-04-02" |> RDF.Term.coerce() == - ~D"2002-04-02" |> RDF.date() + ~D"2002-04-02" |> XSD.date() end test "with time" do assert ~T"12:00:00" |> RDF.Term.coerce() == - ~T"12:00:00" |> RDF.time() + ~T"12:00:00" |> XSD.time() end test "with reference" do @@ -80,7 +80,7 @@ defmodule RDF.TermTest do end test "with an invalid RDF.Literal" do - assert RDF.integer("foo") |> RDF.Term.value() == "foo" + assert XSD.integer("foo") |> RDF.Term.value() == "foo" end test "with boolean" do diff --git a/test/unit/triple_test.exs b/test/unit/triple_test.exs index ed8d7b3..bda63ca 100644 --- a/test/unit/triple_test.exs +++ b/test/unit/triple_test.exs @@ -7,7 +7,7 @@ defmodule RDF.TripleTest do describe "values/1" do test "with a valid RDF.Triple" do - assert Triple.values({~I, ~I, RDF.integer(42)}) + assert Triple.values({~I, ~I, XSD.integer(42)}) == {"http://example.com/S", "http://example.com/p", 42} end @@ -18,7 +18,7 @@ defmodule RDF.TripleTest do end test "values/2" do - assert {~I, ~I, RDF.integer(42)} + assert {~I, ~I, XSD.integer(42)} |> Triple.values(fn {:object, object} -> object |> RDF.Term.value() |> Kernel.+(1) {_, term} -> term |> to_string() |> String.last() diff --git a/test/unit/turtle_decoder_test.exs b/test/unit/turtle_decoder_test.exs index 23f7f2e..c0641fc 100644 --- a/test/unit/turtle_decoder_test.exs +++ b/test/unit/turtle_decoder_test.exs @@ -5,7 +5,7 @@ defmodule RDF.Turtle.DecoderTest do import RDF.Sigils - alias RDF.{Turtle, Graph, NS} + alias RDF.{Turtle, Graph, NS, XSD} use RDF.Vocabulary.Namespace @@ -240,28 +240,28 @@ defmodule RDF.Turtle.DecoderTest do test "boolean" do assert Turtle.Decoder.decode!(""" true . - """) == Graph.new({EX.Foo, EX.bar, RDF.true}) + """) == Graph.new({EX.Foo, EX.bar, XSD.true}) assert Turtle.Decoder.decode!(""" false . - """) == Graph.new({EX.Foo, EX.bar, RDF.false}) + """) == Graph.new({EX.Foo, EX.bar, XSD.false}) end test "integer" do assert Turtle.Decoder.decode!(""" 42 . - """) == Graph.new({EX.Foo, EX.bar, RDF.integer(42)}) + """) == Graph.new({EX.Foo, EX.bar, XSD.integer(42)}) end test "decimal" do assert Turtle.Decoder.decode!(""" 3.14 . - """) == Graph.new({EX.Foo, EX.bar, RDF.decimal("3.14")}) + """) == Graph.new({EX.Foo, EX.bar, XSD.decimal("3.14")}) end test "double" do assert Turtle.Decoder.decode!(""" 1.2e3 . - """) == Graph.new({EX.Foo, EX.bar, RDF.double("1.2e3")}) + """) == Graph.new({EX.Foo, EX.bar, XSD.double("1.2e3")}) end end diff --git a/test/unit/turtle_encoder_test.exs b/test/unit/turtle_encoder_test.exs index 453e48e..3032001 100644 --- a/test/unit/turtle_encoder_test.exs +++ b/test/unit/turtle_encoder_test.exs @@ -6,7 +6,8 @@ defmodule RDF.Turtle.EncoderTest do doctest Turtle.Encoder alias RDF.Graph - alias RDF.NS.{XSD, RDFS, OWL} + alias RDF.NS + alias RDF.NS.{RDFS, OWL} import RDF.Sigils @@ -47,7 +48,7 @@ defmodule RDF.Turtle.EncoderTest do {EX.S2, EX.p3, EX.O4}, ]), prefixes: %{ ex: EX.__base_iri__, - xsd: XSD.__base_iri__ + xsd: NS.XSD.__base_iri__ }) == """ @prefix ex: <#{to_string(EX.__base_iri__)}> . @@ -66,11 +67,11 @@ defmodule RDF.Turtle.EncoderTest do assert Turtle.Encoder.encode!(Graph.new([ {EX.S1, EX.p1, EX.O1}, {EX.S1, EX.p1, EX.O2}, - {EX.S1, EX.p2, XSD.integer}, + {EX.S1, EX.p2, NS.XSD.integer}, {EX.S2, EX.p3, EX.O4}, ], prefixes: %{ "": EX.__base_iri__, - xsd: XSD.__base_iri__ + xsd: NS.XSD.__base_iri__ })) == """ @prefix : <#{to_string(EX.__base_iri__)}> . @@ -106,11 +107,11 @@ defmodule RDF.Turtle.EncoderTest do end test "when no prefixes are given and no prefixes are in the given graph the default_prefixes are used" do - assert Turtle.Encoder.encode!(Graph.new({EX.S, EX.p, XSD.string})) == + assert Turtle.Encoder.encode!(Graph.new({EX.S, EX.p, NS.XSD.string})) == """ @prefix rdf: <#{to_string(RDF.__base_iri__)}> . @prefix rdfs: <#{to_string(RDFS.__base_iri__)}> . - @prefix xsd: <#{to_string(XSD.__base_iri__)}> . + @prefix xsd: <#{to_string(NS.XSD.__base_iri__)}> . xsd:string . @@ -546,7 +547,7 @@ defmodule RDF.Turtle.EncoderTest do ~r[@prefix xsd: \.], ~r["http://foo/"\^\^xsd:anyURI \.] ], - prefixes: %{xsd: XSD.__base_iri__} + prefixes: %{xsd: NS.XSD.__base_iri__} ) end @@ -560,7 +561,7 @@ defmodule RDF.Turtle.EncoderTest do {"0", "false ."}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.boolean(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.boolean(value)}) |> assert_serialization(matches: [output]) end) end @@ -573,7 +574,7 @@ defmodule RDF.Turtle.EncoderTest do {"FaLsE", ~s{"FaLsE"^^}}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.boolean(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.boolean(value)}) |> assert_serialization(matches: [output]) end) end @@ -591,7 +592,7 @@ defmodule RDF.Turtle.EncoderTest do {"0010", "10 ."}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.integer(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.integer(value)}) |> assert_serialization(matches: [output]) end) end @@ -602,7 +603,7 @@ defmodule RDF.Turtle.EncoderTest do {"true", ~s{"true"^^}}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.integer(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.integer(value)}) |> assert_serialization(matches: [output]) end) end @@ -620,7 +621,7 @@ defmodule RDF.Turtle.EncoderTest do {"010.020", "10.02 ."}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.decimal(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.decimal(value)}) |> assert_serialization(matches: [output]) end) end @@ -631,7 +632,7 @@ defmodule RDF.Turtle.EncoderTest do {"true", ~s{"true"^^}}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.decimal(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.decimal(value)}) |> assert_serialization(matches: [output]) end) end @@ -650,7 +651,7 @@ defmodule RDF.Turtle.EncoderTest do {"-1", "-1.0E0 ."}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.double(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.double(value)}) |> assert_serialization(matches: [output]) end) end @@ -661,7 +662,7 @@ defmodule RDF.Turtle.EncoderTest do {"true", ~s{"true"^^}}, ] |> Enum.each(fn {value, output} -> - Graph.new({EX.S, EX.p, RDF.double(value)}) + Graph.new({EX.S, EX.p, RDF.XSD.double(value)}) |> assert_serialization(matches: [output]) end) end diff --git a/test/unit/xsd/comparison_test.exs b/test/unit/xsd/comparison_test.exs new file mode 100644 index 0000000..7113863 --- /dev/null +++ b/test/unit/xsd/comparison_test.exs @@ -0,0 +1,318 @@ +defmodule RDF.XSD.ComparisonTest do + use ExUnit.Case + + alias RDF.XSD + + describe "XSD.String" do + @ordered_strings [ + {"a", "b"}, + {"0", "1"} + ] + + test "valid comparisons between string literals" do + Enum.each(@ordered_strings, fn {left, right} -> + assert_order({XSD.string(left), XSD.string(right)}) + end) + + assert_equal({XSD.string("foo"), XSD.string("foo")}) + end + end + + describe "XSD.Boolean" do + test "when unequal" do + assert_order({XSD.false(), XSD.true()}) + end + + test "when equal" do + assert_equal({XSD.false(), XSD.false()}) + assert_equal({XSD.true(), XSD.true()}) + end + end + + describe "XSD.Numeric" do + test "when unequal" do + Enum.each( + [ + {XSD.integer(0), XSD.integer(1)}, + {XSD.integer("3"), XSD.integer("007")}, + {XSD.double(1.1), XSD.double(2.2)}, + {XSD.float(1.1), XSD.float(2.2)}, + {XSD.decimal(1.1), XSD.decimal(2.2)}, + {XSD.decimal(1.1), XSD.double(2.2)}, + {XSD.float(1.1), XSD.decimal(2.2)}, + {XSD.double(3.14), XSD.integer(42)}, + {XSD.float(3.14), XSD.integer(42)}, + {XSD.decimal(3.14), XSD.integer(42)}, + {XSD.non_negative_integer(0), XSD.integer(1)}, + {XSD.integer(0), XSD.positive_integer(1)}, + {XSD.non_negative_integer(0), XSD.positive_integer(1)}, + {XSD.positive_integer(1), XSD.non_negative_integer(2)} + ], + &assert_order/1 + ) + end + + test "when equal" do + Enum.each( + [ + {XSD.integer(42), XSD.integer(42)}, + {XSD.integer("42"), XSD.integer("042")}, + {XSD.integer("42"), XSD.double("42")}, + {XSD.integer(42), XSD.double(42.0)}, + {XSD.integer(42), XSD.float(42.0)}, + {XSD.integer("42"), XSD.decimal("42")}, + {XSD.integer(42), XSD.decimal(42.0)}, + {XSD.double(3.14), XSD.decimal(3.14)}, + {XSD.float(3.14), XSD.decimal(3.14)}, + {XSD.double("+0"), XSD.double("-0")}, + {XSD.decimal("+0"), XSD.decimal("-0")}, + {XSD.non_negative_integer(0), XSD.integer(0)}, + {XSD.integer(1), XSD.positive_integer(1)}, + {XSD.non_negative_integer(1), XSD.positive_integer(1)}, + {XSD.positive_integer(1), XSD.non_negative_integer(1)} + ], + &assert_equal/1 + ) + end + end + + describe "XSD.DateTime" do + test "when unequal" do + assert_order({XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T17:00:00")}) + + assert_order( + {XSD.datetime("2002-04-02T12:00:00+01:00"), XSD.datetime("2002-04-02T12:00:00+00:00")} + ) + + assert_order({XSD.datetime("2000-01-15T12:00:00"), XSD.datetime("2000-01-16T12:00:00Z")}) + end + + test "when unequal due to missing time zone" do + assert_order({XSD.datetime("2000-01-15T00:00:00"), XSD.datetime("2000-02-15T00:00:00")}) + end + + test "when equal" do + assert_equal( + {XSD.datetime("2002-04-02T12:00:00-01:00"), XSD.datetime("2002-04-02T12:00:00-01:00")} + ) + + assert_equal({XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T12:00:00")}) + + assert_equal( + {XSD.datetime("2002-04-02T12:00:00-01:00"), XSD.datetime("2002-04-02T17:00:00+04:00")} + ) + + assert_equal( + {XSD.datetime("2002-04-02T23:00:00-04:00"), XSD.datetime("2002-04-03T02:00:00-01:00")} + ) + + assert_equal({XSD.datetime("1999-12-31T24:00:00"), XSD.datetime("2000-01-01T00:00:00")}) + # TODO: Assume that the dynamic context provides an implicit timezone value of -05:00 + # assert_equal {XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T23:00:00+06:00")} + end + + test "when indeterminate" do + assert_indeterminate( + {XSD.datetime("2000-01-01T12:00:00"), XSD.datetime("1999-12-31T23:00:00Z")} + ) + + assert_indeterminate( + {XSD.datetime("2000-01-16T12:00:00"), XSD.datetime("2000-01-16T12:00:00Z")} + ) + + assert_indeterminate( + {XSD.datetime("2000-01-16T00:00:00"), XSD.datetime("2000-01-16T12:00:00Z")} + ) + end + end + + describe "XSD.Date" do + test "when unequal" do + assert_order({XSD.date("2002-04-02"), XSD.date("2002-04-03")}) + assert_order({XSD.date("2002-04-02+01:00"), XSD.date("2002-04-03+00:00")}) + assert_order({XSD.date("2002-04-02"), XSD.date("2002-04-03Z")}) + end + + test "when equal" do + assert_equal({XSD.date("2002-04-02-01:00"), XSD.date("2002-04-02-01:00")}) + assert_equal({XSD.date("2002-04-02"), XSD.date("2002-04-02")}) + assert_equal({XSD.date("2002-04-02-00:00"), XSD.date("2002-04-02+00:00")}) + assert_equal({XSD.date("2002-04-02Z"), XSD.date("2002-04-02+00:00")}) + assert_equal({XSD.date("2002-04-02Z"), XSD.date("2002-04-02-00:00")}) + end + + test "when indeterminate" do + assert_indeterminate({XSD.date("2002-04-02Z"), XSD.date("2002-04-02")}) + assert_indeterminate({XSD.date("2002-04-02+00:00"), XSD.date("2002-04-02")}) + assert_indeterminate({XSD.date("2002-04-02-00:00"), XSD.date("2002-04-02")}) + end + 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. + # + # describe "comparisons XSD.DateTime between XSD.Date and XSD.DateTime" do + # test "when unequal" do + # # without timezone + # assert_order({XSD.datetime("2000-01-14T00:00:00"), XSD.date("2000-02-15")}) + # assert_order({XSD.date("2000-01-15"), XSD.datetime("2000-01-15T00:00:01")}) + # # with timezone + # assert_order({XSD.datetime("2000-01-14T00:00:00"), XSD.date("2000-02-15")}) + # assert_order({XSD.datetime("2000-01-14T00:00:00"), XSD.date("2000-02-15Z")}) + # assert_order({XSD.datetime("2000-01-14T00:00:00"), XSD.date("2000-02-15+01:00")}) + # assert_order({XSD.datetime("2000-01-14T00:00:00Z"), XSD.date("2000-02-15")}) + # assert_order({XSD.datetime("2000-01-14T00:00:00Z"), XSD.date("2000-02-15Z")}) + # assert_order({XSD.datetime("2000-01-14T00:00:00Z"), XSD.date("2000-02-15+01:00")}) + # end + # + # test "when equal" do + # assert_equal({XSD.datetime("2000-01-15T00:00:00"), XSD.date("2000-01-15")}) + # assert_equal({XSD.datetime("2000-01-15T00:00:00Z"), XSD.date("2000-01-15Z")}) + # assert_equal({XSD.datetime("2000-01-15T00:00:00Z"), XSD.date("2000-01-15+00:00")}) + # assert_equal({XSD.datetime("2000-01-15T00:00:00Z"), XSD.date("2000-01-15-00:00")}) + # end + # + # test "when indeterminate" do + # assert_indeterminate({XSD.datetime("2000-01-15T00:00:00"), XSD.date("2000-01-15Z")}) + # assert_indeterminate({XSD.datetime("2000-01-15T00:00:00Z"), XSD.date("2000-01-15")}) + # end + # end + + describe "XSD.Time" do + test "when unequal" do + assert_order({XSD.time("12:00:00+01:00"), XSD.time("13:00:00+01:00")}) + assert_order({XSD.time("12:00:00"), XSD.time("13:00:00")}) + end + + test "when equal" do + assert_equal({XSD.time("12:00:00+01:00"), XSD.time("12:00:00+01:00")}) + assert_equal({XSD.time("12:00:00"), XSD.time("12:00:00")}) + end + + test "when indeterminate" do + assert_indeterminate({XSD.date("2002-04-02Z"), XSD.date("2002-04-02")}) + assert_indeterminate({XSD.date("2002-04-02+00:00"), XSD.date("2002-04-02")}) + assert_indeterminate({XSD.date("2002-04-02-00:00"), XSD.date("2002-04-02")}) + end + end + + describe "incomparable" do + test "when comparing incomparable types" do + Enum.each( + [ + {XSD.string("true"), XSD.true()}, + {XSD.string("42"), XSD.integer(42)}, + {XSD.string("3.14"), XSD.decimal(3.14)}, + {XSD.string("2002-04-02T12:00:00"), XSD.datetime("2002-04-02T12:00:00")}, + {XSD.string("2002-04-02"), XSD.date("2002-04-02")}, + {XSD.string("12:00:00"), XSD.time("12:00:00")}, + {XSD.true(), XSD.integer(42)}, + {XSD.true(), XSD.decimal(3.14)}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.true()}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.integer(42)}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.decimal(3.14)}, + {XSD.date("2002-04-02"), XSD.true()}, + {XSD.date("2002-04-02"), XSD.integer(42)}, + {XSD.date("2002-04-02"), XSD.decimal(3.14)}, + {XSD.time("12:00:00"), XSD.true()}, + {XSD.time("12:00:00"), XSD.integer(42)}, + {XSD.time("12:00:00"), XSD.decimal(3.14)} + ], + &assert_incomparable/1 + ) + end + + test "when comparing invalid literals" do + Enum.each( + [ + {XSD.true(), XSD.boolean(42)}, + {XSD.datetime("2002-04-02T12:00:00"), XSD.datetime("2002.04.02 12:00")}, + {XSD.date("2002-04-02"), XSD.date("2002.04.02")}, + {XSD.time("12:00:00"), XSD.time("12-00-00")} + ], + &assert_incomparable/1 + ) + end + end + + defp assert_order({left, right}) do + assert_compare_result({left, right}, :lt) + assert_compare_result({right, left}, :gt) + + assert_less_than({left, right}, true) + assert_less_than({right, left}, false) + + assert_greater_than({left, right}, false) + assert_greater_than({right, left}, true) + end + + defp assert_equal({left, right}) do + assert_compare_result({left, right}, :eq) + assert_compare_result({right, left}, :eq) + + assert_less_than({left, right}, false) + assert_less_than({right, left}, false) + + assert_greater_than({left, right}, false) + assert_greater_than({right, left}, false) + end + + defp assert_incomparable({left, right}) do + assert_compare_result({left, right}, nil) + assert_compare_result({right, left}, nil) + + 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_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 + result = RDF.Literal.compare(left, right) + + assert result == expected, """ + expected RDF.Literal.compare( + #{inspect(left)}, + #{inspect(right)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} + """ + end + + defp assert_less_than({left, right}, expected) do + result = RDF.Literal.less_than?(left, right) + + assert result == expected, """ + expected RDF.Literal.less_than?( + #{inspect(left)}, + #{inspect(right)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} + """ + end + + defp assert_greater_than({left, right}, expected) do + result = RDF.Literal.greater_than?(left, right) + + assert result == expected, """ + expected RDF.Literal.greater_than?( + #{inspect(left)}, + #{inspect(right)}) + to be: #{inspect(expected)} + but got: #{inspect(result)} + """ + end +end diff --git a/test/unit/xsd/datatype_test.exs b/test/unit/xsd/datatype_test.exs new file mode 100644 index 0000000..7901694 --- /dev/null +++ b/test/unit/xsd/datatype_test.exs @@ -0,0 +1,18 @@ +defmodule RDF.XSD.DatatypeTest do + use ExUnit.Case + + alias RDF.XSD + + test "base_primitive/1" do + assert XSD.integer(42) |> XSD.Datatype.base_primitive() == XSD.Integer + assert XSD.non_negative_integer(42) |> XSD.Datatype.base_primitive() == XSD.Integer + assert XSD.positive_integer(42) |> XSD.Datatype.base_primitive() == XSD.Integer + end + + test "derived_from?/2" do + assert XSD.integer(42) |> XSD.Datatype.derived_from?(XSD.Integer) + assert XSD.non_negative_integer(42) |> XSD.Datatype.derived_from?(XSD.Integer) + assert XSD.positive_integer(42) |> XSD.Datatype.derived_from?(XSD.Integer) + assert XSD.positive_integer(42) |> XSD.Datatype.derived_from?(XSD.NonNegativeInteger) + end +end diff --git a/test/unit/xsd/datatypes/any_uri_test.exs b/test/unit/xsd/datatypes/any_uri_test.exs new file mode 100644 index 0000000..2500138 --- /dev/null +++ b/test/unit/xsd/datatypes/any_uri_test.exs @@ -0,0 +1,14 @@ +defmodule RDF.XSD.AnyURITest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.AnyURI, + name: "anyURI", + primitive: true, + valid: %{ + # input => { value, lexical, canonicalized } + "http://example.com/foo" => + {URI.parse("http://example.com/foo"), nil, "http://example.com/foo"}, + URI.parse("http://example.com/foo") => + {URI.parse("http://example.com/foo"), nil, "http://example.com/foo"} + }, + invalid: [42, 3.14, true, false] +end diff --git a/test/unit/xsd/datatypes/boolean_test.exs b/test/unit/xsd/datatypes/boolean_test.exs new file mode 100644 index 0000000..ce00997 --- /dev/null +++ b/test/unit/xsd/datatypes/boolean_test.exs @@ -0,0 +1,238 @@ +defmodule RDF.XSD.BooleanTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Boolean, + name: "boolean", + primitive: true, + valid: %{ + # input => { value, lexical, canonicalized } + true => {true, nil, "true"}, + false => {false, nil, "false"}, + 0 => {false, nil, "false"}, + 1 => {true, nil, "true"}, + "true" => {true, nil, "true"}, + "false" => {false, nil, "false"}, + "0" => {false, "0", "false"}, + "1" => {true, "1", "true"} + }, + invalid: ["foo", "10", 42, 3.14, "tRuE", "FaLsE", "true false", "true foo"] + + describe "cast/1" do + test "casting a boolean returns the input as it is" do + assert XSD.true() |> XSD.Boolean.cast() == XSD.true() + assert XSD.false() |> XSD.Boolean.cast() == XSD.false() + end + + test "casting a string with a value from the lexical value space of xsd:boolean" do + assert XSD.string("true") |> XSD.Boolean.cast() == XSD.true() + assert XSD.string("1") |> XSD.Boolean.cast() == XSD.true() + + assert XSD.string("false") |> XSD.Boolean.cast() == XSD.false() + assert XSD.string("0") |> XSD.Boolean.cast() == XSD.false() + end + + test "casting a string with a value not in the lexical value space of xsd:boolean" do + assert XSD.string("foo") |> XSD.Boolean.cast() == nil + end + + test "casting an integer" do + assert XSD.integer(0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.integer(1) |> XSD.Boolean.cast() == XSD.true() + assert XSD.integer(42) |> XSD.Boolean.cast() == XSD.true() + end + + test "casting a decimal" do + assert XSD.decimal(0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.decimal(0.0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.decimal("+0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.decimal("-0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.decimal("+0.0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.decimal("-0.0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.decimal(0.0e0) |> XSD.Boolean.cast() == XSD.false() + + assert XSD.decimal(1) |> XSD.Boolean.cast() == XSD.true() + assert XSD.decimal(0.1) |> XSD.Boolean.cast() == XSD.true() + end + + test "casting a double" do + assert XSD.double(0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.double(0.0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.double("+0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.double("-0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.double("+0.0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.double("-0.0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.double("0.0E0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.double("NAN") |> XSD.Boolean.cast() == XSD.false() + + assert XSD.double(1) |> XSD.Boolean.cast() == XSD.true() + assert XSD.double(0.1) |> XSD.Boolean.cast() == XSD.true() + assert XSD.double("-INF") |> XSD.Boolean.cast() == XSD.true() + end + + test "casting a float" do + assert XSD.float(0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.float(0.0) |> XSD.Boolean.cast() == XSD.false() + assert XSD.float("+0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.float("-0.0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.float("0.0E0") |> XSD.Boolean.cast() == XSD.false() + assert XSD.float("NAN") |> XSD.Boolean.cast() == XSD.false() + + assert XSD.float(1) |> XSD.Boolean.cast() == XSD.true() + assert XSD.float(0.1) |> XSD.Boolean.cast() == XSD.true() + assert XSD.float("-INF") |> XSD.Boolean.cast() == XSD.true() + end + + test "with invalid literals" do + assert XSD.boolean("42") |> XSD.Boolean.cast() == nil + assert XSD.integer(3.14) |> XSD.Boolean.cast() == nil + assert XSD.decimal("NAN") |> XSD.Boolean.cast() == nil + assert XSD.double(true) |> XSD.Boolean.cast() == nil + end + + test "with values of unsupported datatypes" do + assert XSD.date("2020-01-01") |> XSD.Boolean.cast() == nil + end + + test "with coercible value" do + assert XSD.Boolean.cast(0) == XSD.false() + assert XSD.Boolean.cast(0.0) == XSD.false() + assert XSD.Boolean.cast(42) == XSD.true() + assert XSD.Boolean.cast(3.14) == XSD.true() + assert XSD.Boolean.cast("true") == XSD.true() + assert XSD.Boolean.cast("false") == XSD.false() + end + + test "with non-coercible value" do + assert XSD.Boolean.cast(:foo) == nil + assert XSD.Boolean.cast(make_ref()) == nil + end + end + + describe "ebv/1" do + import XSD.Boolean, only: [ebv: 1] + + test "if the argument is a xsd:boolean typed literal and it has a valid lexical form, the EBV is the value of that argument" do + [ + XSD.true(), + XSD.false(), + XSD.boolean(1), + XSD.boolean("0") + ] + |> Enum.each(fn literal -> + assert ebv(literal) == literal + end) + end + + test "any literal whose type is xsd:boolean or numeric is false if the lexical form is not valid for that datatype" do + [ + XSD.boolean(42), + XSD.integer(3.14), + XSD.double("Foo") + ] + |> Enum.each(fn literal -> + assert ebv(literal) == XSD.false() + end) + end + + test "if the argument is a xsd:string, the EBV is false if the operand value has zero length" do + assert ebv(XSD.string("")) == XSD.false() + end + + test "if the argument is a xsd:string, the EBV is true if the operand value has length greater zero" do + assert ebv(XSD.string("bar")) == XSD.true() + end + + test "if the argument is a numeric literal with a valid lexical form having the value NaN or being numerically equal to zero, the EBV is false" do + [ + XSD.integer(0), + XSD.integer("0"), + XSD.double("0"), + XSD.double("0.0"), + XSD.double(:nan), + XSD.double("NaN") + ] + |> Enum.each(fn literal -> + assert ebv(literal) == XSD.false() + end) + end + + test "if the argument is a numeric type with a valid lexical form being numerically unequal to zero, the EBV is true" do + assert ebv(XSD.integer(42)) == XSD.true() + assert ebv(XSD.integer("42")) == XSD.true() + assert ebv(XSD.double("3.14")) == XSD.true() + end + + test "Elixirs booleans are treated as XSD.Booleans" do + assert ebv(true) == XSD.true() + assert ebv(false) == XSD.false() + end + + test "Elixirs strings are treated as XSD.strings" do + assert ebv("") == XSD.false() + assert ebv("foo") == XSD.true() + assert ebv("0") == XSD.true() + end + + test "Elixirs numbers are treated as XSD.Numerics" do + assert ebv(0) == XSD.false() + assert ebv(0.0) == XSD.false() + + assert ebv(42) == XSD.true() + assert ebv(3.14) == XSD.true() + end + + test "all other arguments, produce nil" do + [ + XSD.date("2010-01-01"), + XSD.time("00:00:00"), + nil, + self(), + [true], + {true}, + %{foo: :bar} + ] + |> Enum.each(fn value -> + assert ebv(value) == nil + end) + end + end + + test "truth-table of logical_and" do + [ + {XSD.true(), XSD.true(), XSD.true()}, + {XSD.true(), XSD.false(), XSD.false()}, + {XSD.false(), XSD.true(), XSD.false()}, + {XSD.false(), XSD.false(), XSD.false()}, + {XSD.true(), nil, nil}, + {nil, XSD.true(), nil}, + {XSD.false(), nil, XSD.false()}, + {nil, XSD.false(), XSD.false()}, + {nil, nil, nil} + ] + |> Enum.each(fn {left, right, result} -> + assert XSD.Boolean.logical_and(left, right) == result, + "expected logical_and(#{inspect(left)}, #{inspect(right)}) to be #{inspect(result)}, but got #{ + inspect(XSD.Boolean.logical_and(left, right)) + }" + end) + end + + test "truth-table of logical_or" do + [ + {XSD.true(), XSD.true(), XSD.true()}, + {XSD.true(), XSD.false(), XSD.true()}, + {XSD.false(), XSD.true(), XSD.true()}, + {XSD.false(), XSD.false(), XSD.false()}, + {XSD.true(), nil, XSD.true()}, + {nil, XSD.true(), XSD.true()}, + {XSD.false(), nil, nil}, + {nil, XSD.false(), nil}, + {nil, nil, nil} + ] + |> Enum.each(fn {left, right, result} -> + assert XSD.Boolean.logical_or(left, right) == result, + "expected logical_or(#{inspect(left)}, #{inspect(right)}) to be #{inspect(result)}, but got #{ + inspect(XSD.Boolean.logical_and(left, right)) + }" + end) + end +end diff --git a/test/unit/xsd/datatypes/byte_test.exs b/test/unit/xsd/datatypes/byte_test.exs new file mode 100644 index 0000000..5bf09d5 --- /dev/null +++ b/test/unit/xsd/datatypes/byte_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.ByteTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Byte, + name: "byte", + base: RDF.XSD.Short, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: -128, + max_inclusive: 127 + }, + valid: RDF.XSD.TestData.valid_bytes(), + invalid: RDF.XSD.TestData.invalid_bytes() +end diff --git a/test/unit/xsd/datatypes/date_test.exs b/test/unit/xsd/datatypes/date_test.exs new file mode 100644 index 0000000..934f8cf --- /dev/null +++ b/test/unit/xsd/datatypes/date_test.exs @@ -0,0 +1,140 @@ +defmodule RDF.XSD.DateTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Date, + name: "date", + primitive: true, + valid: %{ + # input => { value, lexical, canonicalized } + ~D[2010-01-01] => {~D[2010-01-01], nil, "2010-01-01"}, + "2010-01-01" => {~D[2010-01-01], nil, "2010-01-01"}, + "2010-01-01Z" => {{~D[2010-01-01], "Z"}, nil, "2010-01-01Z"}, + "2010-01-01+00:00" => {{~D[2010-01-01], "Z"}, "2010-01-01+00:00", "2010-01-01Z"}, + "2010-01-01-00:00" => {{~D[2010-01-01], "-00:00"}, nil, "2010-01-01-00:00"}, + "2010-01-01+01:00" => {{~D[2010-01-01], "+01:00"}, nil, "2010-01-01+01:00"}, + "2009-12-31-01:00" => {{~D[2009-12-31], "-01:00"}, nil, "2009-12-31-01:00"}, + "2014-09-01-08:00" => {{~D[2014-09-01], "-08:00"}, nil, "2014-09-01-08:00"}, + + # negative years + "-2010-01-01" => {~D[-2010-01-01], nil, "-2010-01-01"}, + "-2010-01-01Z" => {{~D[-2010-01-01], "Z"}, nil, "-2010-01-01Z"}, + "-2010-01-01+00:00" => {{~D[-2010-01-01], "Z"}, "-2010-01-01+00:00", "-2010-01-01Z"} + }, + invalid: [ + "foo", + "+2010-01-01Z", + "2010-01-01TFOO", + "02010-01-01", + "2010-1-1", + "0000-01-01", + "2011-07", + "2011", + true, + false, + 2010, + 3.14, + # this value representation is just internal and not accepted as + {~D[2010-01-01], "Z"} + ] + + describe "new/2" do + test "with date and tz opt" do + assert XSD.Date.new("2010-01-01", tz: "+01:00") == + %RDF.Literal{literal: %XSD.Date{value: {~D[2010-01-01], "+01:00"}}} + + assert XSD.Date.new(~D[2010-01-01], tz: "+01:00") == + %RDF.Literal{literal: %XSD.Date{value: {~D[2010-01-01], "+01:00"}}} + + assert XSD.Date.new("2010-01-01", tz: "+00:00") == + %RDF.Literal{literal: %XSD.Date{ + value: {~D[2010-01-01], "Z"}, + uncanonical_lexical: "2010-01-01+00:00" + }} + + assert XSD.Date.new(~D[2010-01-01], tz: "+00:00") == + %RDF.Literal{literal: %XSD.Date{ + value: {~D[2010-01-01], "Z"}, + uncanonical_lexical: "2010-01-01+00:00" + }} + end + + test "with date string including a timezone and tz opt" do + assert XSD.Date.new("2010-01-01+00:00", tz: "+01:00") == + %RDF.Literal{literal: %XSD.Date{value: {~D[2010-01-01], "+01:00"}}} + + assert XSD.Date.new("2010-01-01+01:00", tz: "Z") == + %RDF.Literal{literal: %XSD.Date{value: {~D[2010-01-01], "Z"}}} + + assert XSD.Date.new("2010-01-01+01:00", tz: "+00:00") == + %RDF.Literal{literal: %XSD.Date{ + value: {~D[2010-01-01], "Z"}, + uncanonical_lexical: "2010-01-01+00:00" + }} + end + + test "with invalid tz opt" do + assert XSD.Date.new(~D[2020-01-01], tz: "+01:00:42") == + %RDF.Literal{literal: %XSD.Date{uncanonical_lexical: "2020-01-01+01:00:42"}} + + assert XSD.Date.new("2020-01-01-01", tz: "+01:00") == + %RDF.Literal{literal: %XSD.Date{uncanonical_lexical: "2020-01-01-01"}} + + assert XSD.Date.new("2020-01-01", tz: "+01:00:42") == + %RDF.Literal{literal: %XSD.Date{uncanonical_lexical: "2020-01-01"}} + + assert XSD.Date.new("2020-01-01+00:00:", tz: "+01:00:") == + %RDF.Literal{literal: %XSD.Date{uncanonical_lexical: "2020-01-01+00:00:"}} + end + end + + describe "cast/1" do + test "casting a date returns the input as it is" do + assert XSD.date("2010-01-01") |> XSD.Date.cast() == + XSD.date("2010-01-01") + end + + test "casting a string" do + assert XSD.string("2010-01-01") |> XSD.Date.cast() == + XSD.date("2010-01-01") + + assert XSD.string("2010-01-01Z") |> XSD.Date.cast() == + XSD.date("2010-01-01Z") + + assert XSD.string("2010-01-01+01:00") |> XSD.Date.cast() == + XSD.date("2010-01-01+01:00") + end + + test "casting a datetime" do + assert XSD.datetime("2010-01-01T01:00:00") |> XSD.Date.cast() == + XSD.date("2010-01-01") + + assert XSD.datetime("2010-01-01T00:00:00Z") |> XSD.Date.cast() == + XSD.date("2010-01-01Z") + + assert XSD.datetime("2010-01-01T00:00:00+00:00") |> XSD.Date.cast() == + XSD.date("2010-01-01+00:00") + + assert XSD.datetime("2010-01-01T23:00:00+01:00") |> XSD.Date.cast() == + XSD.date("2010-01-01+01:00") + end + + test "with invalid literals" do + assert XSD.date("02010-01-00") |> XSD.Date.cast() == nil + assert XSD.datetime("02010-01-01T00:00:00") |> XSD.Date.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.false() |> XSD.Date.cast() == nil + assert XSD.integer(1) |> XSD.Date.cast() == nil + assert XSD.decimal(3.14) |> XSD.Date.cast() == nil + end + + test "with coercible value" do + assert XSD.Date.cast("2010-01-01") == XSD.date("2010-01-01") + end + + test "with non-coercible value" do + assert XSD.Date.cast(:foo) == nil + assert XSD.Date.cast(make_ref()) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/date_time_test.exs b/test/unit/xsd/datatypes/date_time_test.exs new file mode 100644 index 0000000..af30485 --- /dev/null +++ b/test/unit/xsd/datatypes/date_time_test.exs @@ -0,0 +1,194 @@ +defmodule RDF.XSD.DateTimeTest do + import RDF.XSD.Datatype.Test.Case, only: [dt: 1] + + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.DateTime, + name: "dateTime", + primitive: true, + valid: %{ + # input => { value, lexical, canonicalized } + dt("2010-01-01T00:00:00Z") => {dt("2010-01-01T00:00:00Z"), nil, "2010-01-01T00:00:00Z"}, + ~N[2010-01-01T00:00:00] => {~N[2010-01-01T00:00:00], nil, "2010-01-01T00:00:00"}, + ~N[2010-01-01T00:00:00.00] => {~N[2010-01-01T00:00:00.00], nil, "2010-01-01T00:00:00.00"}, + ~N[2010-01-01T00:00:00.1234] => + {~N[2010-01-01T00:00:00.1234], nil, "2010-01-01T00:00:00.1234"}, + dt("2010-01-01T00:00:00+00:00") => + {dt("2010-01-01T00:00:00Z"), nil, "2010-01-01T00:00:00Z"}, + dt("2010-01-01T01:00:00+01:00") => + {dt("2010-01-01T00:00:00Z"), nil, "2010-01-01T00:00:00Z"}, + dt("2009-12-31T23:00:00-01:00") => + {dt("2010-01-01T00:00:00Z"), nil, "2010-01-01T00:00:00Z"}, + dt("2009-12-31T23:00:00.00-01:00") => + {dt("2010-01-01T00:00:00.00Z"), nil, "2010-01-01T00:00:00.00Z"}, + "2010-01-01T00:00:00Z" => {dt("2010-01-01T00:00:00Z"), nil, "2010-01-01T00:00:00Z"}, + "2010-01-01T00:00:00.0000Z" => + {dt("2010-01-01T00:00:00.0000Z"), nil, "2010-01-01T00:00:00.0000Z"}, + "2010-01-01T00:00:00.123456Z" => + {dt("2010-01-01T00:00:00.123456Z"), nil, "2010-01-01T00:00:00.123456Z"}, + "2010-01-01T00:00:00" => {~N[2010-01-01T00:00:00], nil, "2010-01-01T00:00:00"}, + "2010-01-01T00:00:00+00:00" => + {dt("2010-01-01T00:00:00Z"), "2010-01-01T00:00:00+00:00", "2010-01-01T00:00:00Z"}, + "2010-01-01T00:00:00-00:00" => + {dt("2010-01-01T00:00:00Z"), "2010-01-01T00:00:00-00:00", "2010-01-01T00:00:00Z"}, + "2010-01-01T01:00:00+01:00" => + {dt("2010-01-01T00:00:00Z"), "2010-01-01T01:00:00+01:00", "2010-01-01T00:00:00Z"}, + "2009-12-31T23:00:00.42-01:00" => + {dt("2010-01-01T00:00:00.42Z"), "2009-12-31T23:00:00.42-01:00", "2010-01-01T00:00:00.42Z"}, + "2009-12-31T23:00:00-01:00" => + {dt("2010-01-01T00:00:00Z"), "2009-12-31T23:00:00-01:00", "2010-01-01T00:00:00Z"}, + + # 24:00 is a valid XSD dateTime + "2009-12-31T24:00:00" => + {~N[2010-01-01T00:00:00], "2009-12-31T24:00:00", "2010-01-01T00:00:00"}, + "2009-12-31T24:00:00+00:00" => + {dt("2010-01-01T00:00:00Z"), "2009-12-31T24:00:00+00:00", "2010-01-01T00:00:00Z"}, + "2009-12-31T24:00:00-00:00" => + {dt("2010-01-01T00:00:00Z"), "2009-12-31T24:00:00-00:00", "2010-01-01T00:00:00Z"}, + + # negative years + dt("-2010-01-01T00:00:00Z") => {dt("-2010-01-01T00:00:00Z"), nil, "-2010-01-01T00:00:00Z"}, + "-2010-01-01T00:00:00+00:00" => + {dt("-2010-01-01T00:00:00Z"), "-2010-01-01T00:00:00+00:00", "-2010-01-01T00:00:00Z"} + }, + invalid: [ + "foo", + "+2010-01-01T00:00:00Z", + "2010-01-01T00:00:00FOO", + "02010-01-01T00:00:00", + "2010-01-01", + "2010-1-1T00:00:00", + "0000-01-01T00:00:00", + "2010-07", + "2010_", + true, + false, + 2010, + 3.14, + "2010-01-01T00:00:00Z foo", + "foo 2010-01-01T00:00:00Z" + ] + + describe "cast/1" do + test "casting a datetime returns the input as it is" do + assert XSD.datetime("2010-01-01T12:34:56") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T12:34:56") + end + + test "casting a string with a value from the lexical value space of xsd:dateTime" do + assert XSD.string("2010-01-01T12:34:56") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T12:34:56") + + assert XSD.string("2010-01-01T12:34:56Z") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T12:34:56Z") + + assert XSD.string("2010-01-01T12:34:56+01:00") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T12:34:56+01:00") + end + + test "casting a string with a value not in the lexical value space of xsd:dateTime" do + assert XSD.string("string") |> XSD.DateTime.cast() == nil + assert XSD.string("02010-01-01T00:00:00") |> XSD.DateTime.cast() == nil + end + + test "casting a date" do + assert XSD.date("2010-01-01") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T00:00:00") + + assert XSD.date("2010-01-01Z") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T00:00:00Z") + + assert XSD.date("2010-01-01+00:00") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T00:00:00Z") + + assert XSD.date("2010-01-01+01:00") |> XSD.DateTime.cast() == + XSD.datetime("2010-01-01T00:00:00+01:00") + end + + test "with invalid literals" do + assert XSD.datetime("02010-01-01T00:00:00") |> XSD.DateTime.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.false() |> XSD.DateTime.cast() == nil + assert XSD.integer(1) |> XSD.DateTime.cast() == nil + assert XSD.decimal(3.14) |> XSD.DateTime.cast() == nil + end + + test "with coercible value" do + assert XSD.DateTime.cast("2010-01-01T12:34:56") == XSD.datetime("2010-01-01T12:34:56") + end + + test "with non-coercible value" do + assert XSD.DateTime.cast(:foo) == nil + assert XSD.DateTime.cast(make_ref()) == nil + end + end + + test "now/0" do + assert %RDF.Literal{literal: %XSD.DateTime{}} = XSD.DateTime.now() + end + + describe "tz/1" do + test "with timezone" do + [ + {"2010-01-01T00:00:00-23:00", "-23:00"}, + {"2010-01-01T00:00:00+23:00", "+23:00"}, + {"2010-01-01T00:00:00+00:00", "+00:00"} + ] + |> Enum.each(fn {dt, tz} -> + assert dt |> DateTime.new() |> DateTime.tz() == tz + assert dt |> XSD.DateTime.new() |> DateTime.tz() == tz + end) + end + + test "without any specific timezone" do + [ + "2010-01-01T00:00:00Z", + "2010-01-01T00:00:00.0000Z" + ] + |> Enum.each(fn dt -> + assert dt |> DateTime.new() |> DateTime.tz() == "Z" + end) + end + + test "without any timezone" do + [ + "2010-01-01T00:00:00", + "2010-01-01T00:00:00.0000" + ] + |> Enum.each(fn dt -> + assert dt |> DateTime.new() |> DateTime.tz() == "" + end) + end + + test "with invalid timezone literals" do + [ + DateTime.new("2010-01-01T00:0"), + "2010-01-01T00:00:00.0000" + ] + |> Enum.each(fn dt -> + assert DateTime.tz(dt) == nil + end) + end + end + + test "canonical_lexical_with_zone/1" do + assert XSD.dateTime(~N[2010-01-01T12:34:56]) |> DateTime.canonical_lexical_with_zone() == + "2010-01-01T12:34:56" + + assert XSD.dateTime("2010-01-01T12:34:56") |> DateTime.canonical_lexical_with_zone() == + "2010-01-01T12:34:56" + + assert XSD.dateTime("2010-01-01T00:00:00+00:00") |> DateTime.canonical_lexical_with_zone() == + "2010-01-01T00:00:00Z" + + assert XSD.dateTime("2010-01-01T00:00:00-00:00") |> DateTime.canonical_lexical_with_zone() == + "2010-01-01T00:00:00Z" + + assert XSD.dateTime("2010-01-01T01:00:00+01:00") |> DateTime.canonical_lexical_with_zone() == + "2010-01-01T01:00:00+01:00" + + assert XSD.dateTime("2010-01-01 01:00:00+01:00") |> DateTime.canonical_lexical_with_zone() == + "2010-01-01T01:00:00+01:00" + end +end diff --git a/test/unit/xsd/datatypes/decimal_test.exs b/test/unit/xsd/datatypes/decimal_test.exs new file mode 100644 index 0000000..36c426b --- /dev/null +++ b/test/unit/xsd/datatypes/decimal_test.exs @@ -0,0 +1,184 @@ +defmodule RDF.XSD.DecimalTest do + # TODO: Why can't we use the Decimal alias in the use options? Maybe it's the special ExUnit.CaseTemplate.using/2 macro in XSD.Datatype.Test.Case? + # alias Elixir.Decimal, as: D + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Decimal, + name: "decimal", + primitive: true, + comparable_datatypes: [RDF.XSD.Integer, RDF.XSD.Double], + valid: %{ + # input => {value, lexical, canonicalized} + 0 => {Elixir.Decimal.from_float(0.0), nil, "0.0"}, + 1 => {Elixir.Decimal.from_float(1.0), nil, "1.0"}, + -1 => {Elixir.Decimal.from_float(-1.0), nil, "-1.0"}, + 1.0 => {Elixir.Decimal.from_float(1.0), nil, "1.0"}, + -3.14 => {Elixir.Decimal.from_float(-3.14), nil, "-3.14"}, + 0.0e2 => {Elixir.Decimal.from_float(0.0), nil, "0.0"}, + 1.2e3 => {Elixir.Decimal.new("1200.0"), nil, "1200.0"}, + Elixir.Decimal.from_float(1.0) => {Elixir.Decimal.from_float(1.0), nil, "1.0"}, + Elixir.Decimal.new(1) => {Elixir.Decimal.from_float(1.0), nil, "1.0"}, + Elixir.Decimal.from_float(1.2e3) => {Elixir.Decimal.new("1200.0"), nil, "1200.0"}, + "1" => {Elixir.Decimal.from_float(1.0), "1", "1.0"}, + "01" => {Elixir.Decimal.from_float(1.0), "01", "1.0"}, + "0123" => {Elixir.Decimal.from_float(123.0), "0123", "123.0"}, + "-1" => {Elixir.Decimal.from_float(-1.0), "-1", "-1.0"}, + "1." => {Elixir.Decimal.from_float(1.0), "1.", "1.0"}, + "1.0" => {Elixir.Decimal.from_float(1.0), nil, "1.0"}, + "1.000000000" => {Elixir.Decimal.from_float(1.0), "1.000000000", "1.0"}, + "+001.00" => {Elixir.Decimal.from_float(1.0), "+001.00", "1.0"}, + "123.456" => {Elixir.Decimal.from_float(123.456), nil, "123.456"}, + "0123.456" => {Elixir.Decimal.from_float(123.456), "0123.456", "123.456"}, + "010.020" => {Elixir.Decimal.from_float(10.02), "010.020", "10.02"}, + "2.3" => {Elixir.Decimal.from_float(2.3), nil, "2.3"}, + "2.345" => {Elixir.Decimal.from_float(2.345), nil, "2.345"}, + "2.234000005" => {Elixir.Decimal.from_float(2.234000005), nil, "2.234000005"}, + "1.234567890123456789012345789" => + {Elixir.Decimal.new("1.234567890123456789012345789"), nil, + "1.234567890123456789012345789"}, + ".3" => {Elixir.Decimal.from_float(0.3), ".3", "0.3"}, + "-.3" => {Elixir.Decimal.from_float(-0.3), "-.3", "-0.3"} + }, + invalid: [ + "foo", + "10.1e1", + "1.0E0", + "12.xyz", + "3,5", + "NaN", + "Inf", + true, + false, + "1.0 foo", + "foo 1.0", + Elixir.Decimal.new("NaN"), + Elixir.Decimal.new("Inf") + ] + + describe "cast/1" do + test "casting a decimal returns the input as it is" do + assert XSD.decimal(0) |> XSD.Decimal.cast() == XSD.decimal(0) + assert XSD.decimal("-0.0") |> XSD.Decimal.cast() == XSD.decimal("-0.0") + assert XSD.decimal(1) |> XSD.Decimal.cast() == XSD.decimal(1) + assert XSD.decimal(0.1) |> XSD.Decimal.cast() == XSD.decimal(0.1) + end + + test "casting a boolean" do + assert XSD.true() |> XSD.Decimal.cast() == XSD.decimal(1.0) + assert XSD.false() |> XSD.Decimal.cast() == XSD.decimal(0.0) + end + + test "casting a string with a value from the lexical value space of xsd:decimal" do + assert XSD.string("0") |> XSD.Decimal.cast() == XSD.decimal(0) + assert XSD.string("3.14") |> XSD.Decimal.cast() == XSD.decimal(3.14) + end + + test "casting a string with a value not in the lexical value space of xsd:decimal" do + assert XSD.string("foo") |> XSD.Decimal.cast() == nil + end + + test "casting an integer" do + assert XSD.integer(0) |> XSD.Decimal.cast() == XSD.decimal(0.0) + assert XSD.integer(42) |> XSD.Decimal.cast() == XSD.decimal(42.0) + end + + test "casting a double" do + assert XSD.double(0.0) |> XSD.Decimal.cast() == XSD.decimal(0.0) + assert XSD.double("-0.0") |> XSD.Decimal.cast() == XSD.decimal(0.0) + assert XSD.double(0.1) |> XSD.Decimal.cast() == XSD.decimal(0.1) + assert XSD.double(1) |> XSD.Decimal.cast() == XSD.decimal(1.0) + assert XSD.double(3.14) |> XSD.Decimal.cast() == XSD.decimal(3.14) + assert XSD.double(10.1e1) |> XSD.Decimal.cast() == XSD.decimal(101.0) + + assert XSD.double("NAN") |> XSD.Decimal.cast() == nil + assert XSD.double("+INF") |> XSD.Decimal.cast() == nil + end + + test "casting a float" do + assert XSD.float(0.0) |> XSD.Decimal.cast() == XSD.decimal(0.0) + assert XSD.float("-0.0") |> XSD.Decimal.cast() == XSD.decimal(0.0) + assert XSD.float(0.1) |> XSD.Decimal.cast() == XSD.decimal(0.1) + assert XSD.float(1) |> XSD.Decimal.cast() == XSD.decimal(1.0) + assert XSD.float(3.14) |> XSD.Decimal.cast() == XSD.decimal(3.14) + assert XSD.float(10.1e1) |> XSD.Decimal.cast() == XSD.decimal(101.0) + + assert XSD.float("NAN") |> XSD.Decimal.cast() == nil + assert XSD.float("+INF") |> XSD.Decimal.cast() == nil + end + + test "with invalid literals" do + assert XSD.boolean("42") |> XSD.Decimal.cast() == nil + assert XSD.integer(3.14) |> XSD.Decimal.cast() == nil + assert XSD.decimal("NAN") |> XSD.Decimal.cast() == nil + assert XSD.double(true) |> XSD.Decimal.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.date("2020-01-01") |> XSD.Decimal.cast() == nil + end + + test "with coercible value" do + assert XSD.Decimal.cast("3.14") == XSD.decimal(3.14) + assert XSD.Decimal.cast("42") == XSD.decimal(42.0) + end + + test "with non-coercible value" do + assert XSD.Decimal.cast(:foo) == nil + assert XSD.Decimal.cast(make_ref()) == nil + end + end + + test "digit_count/1" do + assert XSD.Decimal.digit_count(XSD.decimal("1.2345")) == 5 + assert XSD.Decimal.digit_count(XSD.decimal("-1.2345")) == 5 + assert XSD.Decimal.digit_count(XSD.decimal("+1.2345")) == 5 + assert XSD.Decimal.digit_count(XSD.decimal("01.23450")) == 5 + assert XSD.Decimal.digit_count(XSD.decimal("01.23450")) == 5 + assert XSD.Decimal.digit_count(XSD.decimal("NAN")) == nil + + assert XSD.Decimal.digit_count(XSD.integer("2")) == 1 + assert XSD.Decimal.digit_count(XSD.integer("23")) == 2 + assert XSD.Decimal.digit_count(XSD.integer("023")) == 2 + end + + test "fraction_digit_count/1" do + assert XSD.Decimal.fraction_digit_count(XSD.decimal("1.2345")) == 4 + assert XSD.Decimal.fraction_digit_count(XSD.decimal("-1.2345")) == 4 + assert XSD.Decimal.fraction_digit_count(XSD.decimal("+1.2345")) == 4 + assert XSD.Decimal.fraction_digit_count(XSD.decimal("01.23450")) == 4 + assert XSD.Decimal.fraction_digit_count(XSD.decimal("0.023450")) == 5 + assert XSD.Decimal.fraction_digit_count(XSD.decimal("NAN")) == nil + + assert XSD.Decimal.fraction_digit_count(XSD.integer("2")) == 0 + assert XSD.Decimal.fraction_digit_count(XSD.integer("23")) == 0 + assert XSD.Decimal.fraction_digit_count(XSD.integer("023")) == 0 + end + + defmacrop sigil_d(str, _opts) do + quote do + Elixir.Decimal.new(unquote(str)) + end + end + + test "Decimal.canonical_decimal/1" do + assert Decimal.canonical_decimal(~d"0") == ~d"0.0" + assert Decimal.canonical_decimal(~d"0.0") == ~d"0.0" + assert Decimal.canonical_decimal(~d"0.001") == ~d"0.001" + assert Decimal.canonical_decimal(~d"-0") == ~d"-0.0" + assert Decimal.canonical_decimal(~d"-1") == ~d"-1.0" + assert Decimal.canonical_decimal(~d"-0.00") == ~d"-0.0" + assert Decimal.canonical_decimal(~d"1.00") == ~d"1.0" + assert Decimal.canonical_decimal(~d"1000") == ~d"1000.0" + assert Decimal.canonical_decimal(~d"1000.000000") == ~d"1000.0" + assert Decimal.canonical_decimal(~d"12345.000") == ~d"12345.0" + assert Decimal.canonical_decimal(~d"42") == ~d"42.0" + assert Decimal.canonical_decimal(~d"42.42") == ~d"42.42" + assert Decimal.canonical_decimal(~d"0.42") == ~d"0.42" + assert Decimal.canonical_decimal(~d"0.0042") == ~d"0.0042" + assert Decimal.canonical_decimal(~d"010.020") == ~d"10.02" + assert Decimal.canonical_decimal(~d"-1.23") == ~d"-1.23" + assert Decimal.canonical_decimal(~d"-0.0123") == ~d"-0.0123" + assert Decimal.canonical_decimal(~d"1E+2") == ~d"100.0" + assert Decimal.canonical_decimal(~d"1.2E3") == ~d"1200.0" + assert Decimal.canonical_decimal(~d"-42E+3") == ~d"-42000.0" + end +end diff --git a/test/unit/xsd/datatypes/double_test.exs b/test/unit/xsd/datatypes/double_test.exs new file mode 100644 index 0000000..5dc43d2 --- /dev/null +++ b/test/unit/xsd/datatypes/double_test.exs @@ -0,0 +1,69 @@ +defmodule RDF.XSD.DoubleTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Double, + name: "double", + primitive: true, + comparable_datatypes: [RDF.XSD.Integer, RDF.XSD.Decimal], + valid: RDF.XSD.TestData.valid_floats(), + invalid: RDF.XSD.TestData.invalid_floats() + + describe "cast/1" do + test "casting a double returns the input as it is" do + assert XSD.double(3.14) |> XSD.Double.cast() == XSD.double(3.14) + assert XSD.double("NAN") |> XSD.Double.cast() == XSD.double("NAN") + assert XSD.double("-INF") |> XSD.Double.cast() == XSD.double("-INF") + end + + test "casting a boolean" do + assert XSD.true() |> XSD.Double.cast() == XSD.double(1.0) + assert XSD.false() |> XSD.Double.cast() == XSD.double(0.0) + end + + test "casting a string with a value from the lexical value space of xsd:double" do + assert XSD.string("1.0") |> XSD.Double.cast() == XSD.double("1.0E0") + assert XSD.string("3.14") |> XSD.Double.cast() == XSD.double("3.14E0") + assert XSD.string("3.14E0") |> XSD.Double.cast() == XSD.double("3.14E0") + end + + test "casting a string with a value not in the lexical value space of xsd:double" do + assert XSD.string("foo") |> XSD.Double.cast() == nil + end + + test "casting an integer" do + assert XSD.integer(0) |> XSD.Double.cast() == XSD.double(0.0) + assert XSD.integer(42) |> XSD.Double.cast() == XSD.double(42.0) + end + + test "casting a decimal" do + assert XSD.decimal(0) |> XSD.Double.cast() == XSD.double(0) + assert XSD.decimal(1) |> XSD.Double.cast() == XSD.double(1) + assert XSD.decimal(3.14) |> XSD.Double.cast() == XSD.double(3.14) + end + + test "casting a float" do + assert XSD.float(0) |> XSD.Double.cast() == XSD.double(0) + assert XSD.float(1) |> XSD.Double.cast() == XSD.double(1) + assert XSD.float(3.14) |> XSD.Double.cast() == XSD.double(3.14) + end + + test "with invalid literals" do + assert XSD.boolean("42") |> XSD.Double.cast() == nil + assert XSD.integer(3.14) |> XSD.Double.cast() == nil + assert XSD.decimal("NAN") |> XSD.Double.cast() == nil + assert XSD.double(true) |> XSD.Double.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.date("2020-01-01") |> XSD.Double.cast() == nil + end + + test "with coercible value" do + assert XSD.Double.cast("3.14") == XSD.double(3.14) |> XSD.Double.canonical() + end + + test "with non-coercible value" do + assert XSD.Double.cast(:foo) == nil + assert XSD.Double.cast(make_ref()) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/float_test.exs b/test/unit/xsd/datatypes/float_test.exs new file mode 100644 index 0000000..ade04ee --- /dev/null +++ b/test/unit/xsd/datatypes/float_test.exs @@ -0,0 +1,12 @@ +defmodule RDF.XSD.FloatTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Float, + name: "float", + base: RDF.XSD.Double, + base_primitive: RDF.XSD.Double, + comparable_datatypes: [RDF.XSD.Integer, RDF.XSD.Decimal], + applicable_facets: [], + facets: %{}, + valid: RDF.XSD.TestData.valid_floats(), + invalid: RDF.XSD.TestData.invalid_floats() +end diff --git a/test/unit/xsd/datatypes/int_test.exs b/test/unit/xsd/datatypes/int_test.exs new file mode 100644 index 0000000..a460810 --- /dev/null +++ b/test/unit/xsd/datatypes/int_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.IntTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Int, + name: "int", + base: RDF.XSD.Long, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: -2_147_483_648, + max_inclusive: 2_147_483_647 + }, + valid: RDF.XSD.TestData.valid_ints(), + invalid: RDF.XSD.TestData.invalid_ints() +end diff --git a/test/unit/xsd/datatypes/integer_test.exs b/test/unit/xsd/datatypes/integer_test.exs new file mode 100644 index 0000000..16c8f11 --- /dev/null +++ b/test/unit/xsd/datatypes/integer_test.exs @@ -0,0 +1,107 @@ +defmodule RDF.XSD.IntegerTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Integer, + name: "integer", + primitive: true, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: nil, + max_inclusive: nil + }, + valid: RDF.XSD.TestData.valid_integers(), + invalid: RDF.XSD.TestData.invalid_integers() + + describe "cast/1" do + test "casting an integer returns the input as it is" do + assert XSD.integer(0) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.integer(1) |> XSD.Integer.cast() == XSD.integer(1) + end + + test "casting a boolean" do + assert XSD.false() |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.true() |> XSD.Integer.cast() == XSD.integer(1) + end + + test "casting a string with a value from the lexical value space of xsd:integer" do + assert XSD.string("0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.string("042") |> XSD.Integer.cast() == XSD.integer(42) + end + + test "casting a string with a value not in the lexical value space of xsd:integer" do + assert XSD.string("foo") |> XSD.Integer.cast() == nil + assert XSD.string("3.14") |> XSD.Integer.cast() == nil + end + + test "casting an decimal" do + assert XSD.decimal(0) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.decimal(1.0) |> XSD.Integer.cast() == XSD.integer(1) + assert XSD.decimal(3.14) |> XSD.Integer.cast() == XSD.integer(3) + end + + test "casting a double" do + assert XSD.double(0) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double(0.0) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double(0.1) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double("+0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double("+0.0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double("-0.0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double("0.0E0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.double(1) |> XSD.Integer.cast() == XSD.integer(1) + assert XSD.double(3.14) |> XSD.Integer.cast() == XSD.integer(3) + + assert XSD.double("NAN") |> XSD.Integer.cast() == nil + assert XSD.double("+INF") |> XSD.Integer.cast() == nil + end + + test "casting a float" do + assert XSD.float(0) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float(0.0) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float(0.1) |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float("+0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float("+0.0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float("-0.0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float("0.0E0") |> XSD.Integer.cast() == XSD.integer(0) + assert XSD.float(1) |> XSD.Integer.cast() == XSD.integer(1) + assert XSD.float(3.14) |> XSD.Integer.cast() == XSD.integer(3) + + assert XSD.float("NAN") |> XSD.Integer.cast() == nil + assert XSD.float("+INF") |> XSD.Integer.cast() == nil + end + + test "with invalid literals" do + assert XSD.integer(3.14) |> XSD.Integer.cast() == nil + assert XSD.decimal("NAN") |> XSD.Integer.cast() == nil + assert XSD.double(true) |> XSD.Integer.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.date("2020-01-01") |> XSD.Integer.cast() == nil + end + + test "with coercible value" do + assert XSD.Integer.cast("42") == XSD.integer(42) + assert XSD.Integer.cast(3.14) == XSD.integer(3) + assert XSD.Integer.cast(true) == XSD.integer(1) + assert XSD.Integer.cast(false) == XSD.integer(0) + end + + test "with non-coercible value" do + assert XSD.Integer.cast(:foo) == nil + assert XSD.Integer.cast(make_ref()) == nil + end + end + + test "digit_count/1" do + assert XSD.Integer.digit_count(XSD.integer("2")) == 1 + assert XSD.Integer.digit_count(XSD.integer("23")) == 2 + assert XSD.Integer.digit_count(XSD.integer("023")) == 2 + assert XSD.Integer.digit_count(XSD.integer("+023")) == 2 + assert XSD.Integer.digit_count(XSD.integer("-023")) == 2 + assert XSD.Integer.digit_count(XSD.positive_integer("23")) == 2 + assert XSD.Integer.digit_count(XSD.byte("00023")) == 2 + assert XSD.Integer.digit_count(XSD.integer("NaN")) == nil + assert XSD.Integer.digit_count(XSD.positive_integer("-023")) == nil + assert XSD.Integer.digit_count(XSD.byte("12345")) == nil + end +end diff --git a/test/unit/xsd/datatypes/long_test.exs b/test/unit/xsd/datatypes/long_test.exs new file mode 100644 index 0000000..e3c2e46 --- /dev/null +++ b/test/unit/xsd/datatypes/long_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.LongTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Long, + name: "long", + base: RDF.XSD.Integer, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: -9_223_372_036_854_775_808, + max_inclusive: 9_223_372_036_854_775_807 + }, + valid: RDF.XSD.TestData.valid_longs(), + invalid: RDF.XSD.TestData.invalid_longs() +end diff --git a/test/unit/xsd/datatypes/negative_integer_test.exs b/test/unit/xsd/datatypes/negative_integer_test.exs new file mode 100644 index 0000000..a0ebf39 --- /dev/null +++ b/test/unit/xsd/datatypes/negative_integer_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.NegativeIntegerTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.NegativeInteger, + name: "negativeInteger", + base: RDF.XSD.NonPositiveInteger, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: nil, + max_inclusive: -1 + }, + valid: RDF.XSD.TestData.valid_negative_integers(), + invalid: RDF.XSD.TestData.invalid_negative_integers() +end diff --git a/test/unit/xsd/datatypes/non_negative_integer_test.exs b/test/unit/xsd/datatypes/non_negative_integer_test.exs new file mode 100644 index 0000000..7206817 --- /dev/null +++ b/test/unit/xsd/datatypes/non_negative_integer_test.exs @@ -0,0 +1,116 @@ +defmodule RDF.XSD.NonNegativeIntegerTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.NonNegativeInteger, + name: "nonNegativeInteger", + base: RDF.XSD.Integer, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: 0, + max_inclusive: nil + }, + valid: RDF.XSD.TestData.valid_non_negative_integers(), + invalid: RDF.XSD.TestData.invalid_non_negative_integers() + + describe "cast/1" do + test "casting a non_negative_integer returns the input as it is" do + assert XSD.non_negative_integer(0) |> XSD.NonNegativeInteger.cast() == + XSD.non_negative_integer(0) + + assert XSD.non_negative_integer(1) |> XSD.NonNegativeInteger.cast() == + XSD.non_negative_integer(1) + end + + test "casting an integer with a value from the value space of non_negative_integer" do + assert XSD.integer(0) |> XSD.NonNegativeInteger.cast() == + XSD.non_negative_integer(0) + + assert XSD.integer(1) |> XSD.NonNegativeInteger.cast() == + XSD.non_negative_integer(1) + end + + test "casting an integer with a value not from the value space of non_negative_integer" do + assert XSD.integer(-1) |> XSD.NonNegativeInteger.cast() == nil + end + + test "casting a positive_integer" do + assert XSD.positive_integer(1) |> XSD.NonNegativeInteger.cast() == + XSD.non_negative_integer(1) + end + + test "casting a boolean" do + assert XSD.false() |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.true() |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(1) + end + + test "casting a string with a value from the lexical value space of xsd:integer" do + assert XSD.string("0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.string("042") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(42) + end + + test "casting a string with a value not in the lexical value space of xsd:integer" do + assert XSD.string("foo") |> XSD.NonNegativeInteger.cast() == nil + assert XSD.string("3.14") |> XSD.NonNegativeInteger.cast() == nil + end + + test "casting an decimal" do + assert XSD.decimal(0) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.decimal(1.0) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(1) + assert XSD.decimal(3.14) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(3) + end + + test "casting a double" do + assert XSD.double(0) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double(0.0) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double(0.1) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double("+0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double("+0.0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double("-0.0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double("0.0E0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.double(1) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(1) + assert XSD.double(3.14) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(3) + + assert XSD.double("NAN") |> XSD.NonNegativeInteger.cast() == nil + assert XSD.double("+INF") |> XSD.NonNegativeInteger.cast() == nil + end + + test "casting a float" do + assert XSD.float(0) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float(0.0) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float(0.1) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float("+0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float("+0.0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float("-0.0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float("0.0E0") |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(0) + assert XSD.float(1) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(1) + assert XSD.float(3.14) |> XSD.NonNegativeInteger.cast() == XSD.non_negative_integer(3) + + assert XSD.float("NAN") |> XSD.NonNegativeInteger.cast() == nil + assert XSD.float("+INF") |> XSD.NonNegativeInteger.cast() == nil + end + + test "with invalid literals" do + assert XSD.non_negative_integer(3.14) |> XSD.NonNegativeInteger.cast() == nil + assert XSD.positive_integer(0) |> XSD.NonNegativeInteger.cast() == nil + assert XSD.decimal("NAN") |> XSD.NonNegativeInteger.cast() == nil + assert XSD.double(true) |> XSD.NonNegativeInteger.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.date("2020-01-01") |> XSD.NonNegativeInteger.cast() == nil + end + + test "with coercible value" do + assert XSD.NonNegativeInteger.cast("42") == XSD.non_negative_integer(42) + assert XSD.NonNegativeInteger.cast(3.14) == XSD.non_negative_integer(3) + assert XSD.NonNegativeInteger.cast(true) == XSD.non_negative_integer(1) + assert XSD.NonNegativeInteger.cast(false) == XSD.non_negative_integer(0) + end + + test "with non-coercible value" do + assert XSD.NonNegativeInteger.cast(:foo) == nil + assert XSD.NonNegativeInteger.cast(make_ref()) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/non_positive_integer_test.exs b/test/unit/xsd/datatypes/non_positive_integer_test.exs new file mode 100644 index 0000000..a7888e5 --- /dev/null +++ b/test/unit/xsd/datatypes/non_positive_integer_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.NonPositiveIntegerTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.NonPositiveInteger, + name: "nonPositiveInteger", + base: RDF.XSD.Integer, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: nil, + max_inclusive: 0 + }, + valid: RDF.XSD.TestData.valid_non_positive_integers(), + invalid: RDF.XSD.TestData.invalid_non_positive_integers() +end diff --git a/test/unit/xsd/datatypes/numeric_test.exs b/test/unit/xsd/datatypes/numeric_test.exs new file mode 100644 index 0000000..7cbb719 --- /dev/null +++ b/test/unit/xsd/datatypes/numeric_test.exs @@ -0,0 +1,571 @@ +defmodule RDF.XSD.NumericTest do + use ExUnit.Case + + alias RDF.XSD + alias XSD.Numeric + + alias Decimal, as: D + + @positive_infinity XSD.double(:positive_infinity) + @negative_infinity XSD.double(:negative_infinity) + @nan XSD.double(:nan) + + @negative_zeros ~w[ + -0 + -000 + -0.0 + -0.00000 + ] + + test "negative_zero?/1" do + Enum.each(@negative_zeros, fn negative_zero -> + assert Numeric.negative_zero?(XSD.double(negative_zero)) + assert Numeric.negative_zero?(XSD.float(negative_zero)) + assert Numeric.negative_zero?(XSD.decimal(negative_zero)) + end) + + refute Numeric.negative_zero?(XSD.double("-0.00001")) + refute Numeric.negative_zero?(XSD.float("-0.00001")) + refute Numeric.negative_zero?(XSD.decimal("-0.00001")) + end + + test "zero?/1" do + assert Numeric.zero?(XSD.integer(0)) + assert Numeric.zero?(XSD.integer("0")) + + ~w[ + 0 + 000 + 0.0 + 00.00 + ] + |> Enum.each(fn positive_zero -> + assert Numeric.zero?(XSD.double(positive_zero)) + assert Numeric.zero?(XSD.float(positive_zero)) + assert Numeric.zero?(XSD.decimal(positive_zero)) + end) + + Enum.each(@negative_zeros, fn negative_zero -> + assert Numeric.zero?(XSD.double(negative_zero)) + assert Numeric.zero?(XSD.float(negative_zero)) + assert Numeric.zero?(XSD.decimal(negative_zero)) + end) + + refute Numeric.zero?(XSD.double("-0.00001")) + refute Numeric.zero?(XSD.float("-0.00001")) + refute Numeric.zero?(XSD.decimal("-0.00001")) + end + + describe "add/2" do + test "xsd:integer literal + xsd:integer literal" do + assert Numeric.add(XSD.integer(1), XSD.integer(2)) == XSD.integer(3) + end + + test "xsd:decimal literal + xsd:integer literal" do + assert Numeric.add(XSD.decimal(1.1), XSD.integer(2)) == XSD.decimal(3.1) + end + + test "xsd:double literal + xsd:integer literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.add(XSD.double(1.1), XSD.integer(2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(3.1)), 0.000000000000001 + end + + test "xsd:decimal literal + xsd:double literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.add(XSD.decimal(1.1), XSD.double(2.2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(3.3)), 0.000000000000001 + end + + test "xsd:float literal + xsd:integer literal" do + assert result = %RDF.Literal{literal: %XSD.Float{}} = Numeric.add(XSD.float(1.1), XSD.integer(2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.float(3.1)), 0.000000000000001 + end + + test "xsd:decimal literal + xsd:float literal" do + assert result = %RDF.Literal{literal: %XSD.Float{}} = Numeric.add(XSD.decimal(1.1), XSD.float(2.2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.float(3.3)), 0.000000000000001 + end + + test "if one of the operands is a zero or a finite number and the other is INF or -INF, INF or -INF is returned" do + assert Numeric.add(@positive_infinity, XSD.double(0)) == @positive_infinity + assert Numeric.add(@positive_infinity, XSD.double(3.14)) == @positive_infinity + assert Numeric.add(XSD.double(0), @positive_infinity) == @positive_infinity + assert Numeric.add(XSD.double(3.14), @positive_infinity) == @positive_infinity + + assert Numeric.add(@negative_infinity, XSD.double(0)) == @negative_infinity + assert Numeric.add(@negative_infinity, XSD.double(3.14)) == @negative_infinity + assert Numeric.add(XSD.double(0), @negative_infinity) == @negative_infinity + assert Numeric.add(XSD.double(3.14), @negative_infinity) == @negative_infinity + end + + test "if both operands are INF, INF is returned" do + assert Numeric.add(@positive_infinity, @positive_infinity) == + @positive_infinity + end + + test "if both operands are -INF, -INF is returned" do + assert Numeric.add(@negative_infinity, @negative_infinity) == + @negative_infinity + end + + test "if one of the operands is INF and the other is -INF, NaN is returned" do + assert Numeric.add(@positive_infinity, @negative_infinity) == @nan + assert Numeric.add(@negative_infinity, @positive_infinity) == @nan + end + + test "coercion" do + assert Numeric.add(1, 2) == XSD.integer(3) + assert Numeric.add(3.14, 42) == XSD.double(45.14) + assert XSD.decimal(3.14) |> Numeric.add(42) == XSD.decimal(45.14) + assert Numeric.add(42, XSD.decimal(3.14)) == XSD.decimal(45.14) + assert Numeric.add(42, :foo) == nil + assert Numeric.add(:foo, 42) == nil + assert Numeric.add(:foo, :bar) == nil + end + end + + describe "subtract/2" do + test "xsd:integer literal - xsd:integer literal" do + assert Numeric.subtract(XSD.integer(3), XSD.integer(2)) == XSD.integer(1) + end + + test "xsd:decimal literal - xsd:integer literal" do + assert Numeric.subtract(XSD.decimal(3.3), XSD.integer(2)) == XSD.decimal(1.3) + end + + test "xsd:double literal - xsd:integer literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.subtract(XSD.double(3.3), XSD.integer(2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(1.3)), 0.000000000000001 + end + + test "xsd:decimal literal - xsd:double literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.subtract(XSD.decimal(3.3), XSD.double(2.2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(1.1)), 0.000000000000001 + end + + test "if one of the operands is a zero or a finite number and the other is INF or -INF, an infinity of the appropriate sign is returned" do + assert Numeric.subtract(@positive_infinity, XSD.double(0)) == @positive_infinity + assert Numeric.subtract(@positive_infinity, XSD.double(3.14)) == @positive_infinity + assert Numeric.subtract(XSD.double(0), @positive_infinity) == @negative_infinity + assert Numeric.subtract(XSD.double(3.14), @positive_infinity) == @negative_infinity + + assert Numeric.subtract(@negative_infinity, XSD.double(0)) == @negative_infinity + assert Numeric.subtract(@negative_infinity, XSD.double(3.14)) == @negative_infinity + assert Numeric.subtract(XSD.double(0), @negative_infinity) == @positive_infinity + assert Numeric.subtract(XSD.double(3.14), @negative_infinity) == @positive_infinity + end + + test "if both operands are INF or -INF, NaN is returned" do + assert Numeric.subtract(@positive_infinity, @positive_infinity) == XSD.double(:nan) + assert Numeric.subtract(@negative_infinity, @negative_infinity) == XSD.double(:nan) + end + + test "if one of the operands is INF and the other is -INF, an infinity of the appropriate sign is returned" do + assert Numeric.subtract(@positive_infinity, @negative_infinity) == @positive_infinity + assert Numeric.subtract(@negative_infinity, @positive_infinity) == @negative_infinity + end + + test "coercion" do + assert Numeric.subtract(2, 1) == XSD.integer(1) + assert Numeric.subtract(42, 3.14) == XSD.double(38.86) + assert XSD.decimal(3.14) |> Numeric.subtract(42) == XSD.decimal(-38.86) + assert Numeric.subtract(42, XSD.decimal(3.14)) == XSD.decimal(38.86) + end + end + + describe "multiply/2" do + test "xsd:integer literal * xsd:integer literal" do + assert Numeric.multiply(XSD.integer(2), XSD.integer(3)) == XSD.integer(6) + end + + test "xsd:decimal literal * xsd:integer literal" do + assert Numeric.multiply(XSD.decimal(1.5), XSD.integer(3)) == XSD.decimal(4.5) + end + + test "xsd:double literal * xsd:integer literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.multiply(XSD.double(1.5), XSD.integer(3)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(4.5)), 0.000000000000001 + end + + test "xsd:decimal literal * xsd:double literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.multiply(XSD.decimal(0.5), XSD.double(2.5)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(1.25)), 0.000000000000001 + end + + test "if one of the operands is a zero and the other is an infinity, NaN is returned" do + assert Numeric.multiply(@positive_infinity, XSD.double(0.0)) == @nan + assert Numeric.multiply(XSD.integer(0), @positive_infinity) == @nan + assert Numeric.multiply(XSD.decimal(0), @positive_infinity) == @nan + + assert Numeric.multiply(@negative_infinity, XSD.double(0)) == @nan + assert Numeric.multiply(XSD.integer(0), @negative_infinity) == @nan + assert Numeric.multiply(XSD.decimal(0.0), @negative_infinity) == @nan + end + + test "if one of the operands is a non-zero number and the other is an infinity, an infinity with the appropriate sign is returned" do + assert Numeric.multiply(@positive_infinity, XSD.double(3.14)) == @positive_infinity + assert Numeric.multiply(XSD.double(3.14), @positive_infinity) == @positive_infinity + assert Numeric.multiply(@positive_infinity, XSD.double(-3.14)) == @negative_infinity + assert Numeric.multiply(XSD.double(-3.14), @positive_infinity) == @negative_infinity + + assert Numeric.multiply(@negative_infinity, XSD.double(3.14)) == @negative_infinity + assert Numeric.multiply(XSD.double(3.14), @negative_infinity) == @negative_infinity + assert Numeric.multiply(@negative_infinity, XSD.double(-3.14)) == @positive_infinity + assert Numeric.multiply(XSD.double(-3.14), @negative_infinity) == @positive_infinity + end + + # The following assertions are not part of the spec. + + test "if both operands are INF, INF is returned" do + assert Numeric.multiply(@positive_infinity, @positive_infinity) == @positive_infinity + end + + test "if both operands are -INF, -INF is returned" do + assert Numeric.multiply(@negative_infinity, @negative_infinity) == @negative_infinity + end + + test "if one of the operands is INF and the other is -INF, NaN is returned" do + assert Numeric.multiply(@positive_infinity, @negative_infinity) == XSD.double(:nan) + assert Numeric.multiply(@negative_infinity, @positive_infinity) == XSD.double(:nan) + end + + test "coercion" do + assert Numeric.multiply(1, 2) == XSD.integer(2) + assert Numeric.multiply(2, 1.5) == XSD.double(3.0) + assert XSD.decimal(1.5) |> Numeric.multiply(2) == XSD.decimal(3.0) + assert Numeric.multiply(2, XSD.decimal(1.5)) == XSD.decimal(3.0) + end + end + + describe "divide/2" do + test "xsd:integer literal / xsd:integer literal" do + assert Numeric.divide(XSD.integer(4), XSD.integer(2)) == XSD.decimal(2.0) + end + + test "xsd:decimal literal / xsd:integer literal" do + assert Numeric.divide(XSD.decimal(4), XSD.integer(2)) == XSD.decimal(2.0) + end + + test "xsd:double literal / xsd:integer literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.divide(XSD.double(4), XSD.integer(2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(2)), 0.000000000000001 + end + + test "xsd:decimal literal / xsd:double literal" do + assert result = %RDF.Literal{literal: %XSD.Double{}} = Numeric.divide(XSD.decimal(4), XSD.double(2)) + assert_in_delta RDF.Literal.value(result), + RDF.Literal.value(XSD.double(2)), 0.000000000000001 + end + + test "a positive number divided by positive zero returns INF" do + assert Numeric.divide(XSD.double(1.0), XSD.double(0.0)) == @positive_infinity + assert Numeric.divide(XSD.double(1.0), XSD.decimal(0.0)) == @positive_infinity + assert Numeric.divide(XSD.double(1.0), XSD.integer(0)) == @positive_infinity + assert Numeric.divide(XSD.decimal(1.0), XSD.double(0.0)) == @positive_infinity + assert Numeric.divide(XSD.integer(1), XSD.double(0.0)) == @positive_infinity + end + + test "a negative number divided by positive zero returns -INF" do + assert Numeric.divide(XSD.double(-1.0), XSD.double(0.0)) == @negative_infinity + assert Numeric.divide(XSD.double(-1.0), XSD.decimal(0.0)) == @negative_infinity + assert Numeric.divide(XSD.double(-1.0), XSD.integer(0)) == @negative_infinity + assert Numeric.divide(XSD.decimal(-1.0), XSD.double(0.0)) == @negative_infinity + assert Numeric.divide(XSD.integer(-1), XSD.double(0.0)) == @negative_infinity + end + + test "a positive number divided by negative zero returns -INF" do + assert Numeric.divide(XSD.double(1.0), XSD.double("-0.0")) == @negative_infinity + assert Numeric.divide(XSD.double(1.0), XSD.decimal("-0.0")) == @negative_infinity + assert Numeric.divide(XSD.decimal(1.0), XSD.double("-0.0")) == @negative_infinity + assert Numeric.divide(XSD.integer(1), XSD.double("-0.0")) == @negative_infinity + end + + test "a negative number divided by negative zero returns INF" do + assert Numeric.divide(XSD.double(-1.0), XSD.double("-0.0")) == @positive_infinity + assert Numeric.divide(XSD.double(-1.0), XSD.decimal("-0.0")) == @positive_infinity + assert Numeric.divide(XSD.decimal(-1.0), XSD.double("-0.0")) == @positive_infinity + assert Numeric.divide(XSD.integer(-1), XSD.double("-0.0")) == @positive_infinity + end + + test "nil is returned for xs:decimal and xs:integer operands, if the divisor is (positive or negative) zero" do + assert Numeric.divide(XSD.decimal(1.0), XSD.decimal(0.0)) == nil + assert Numeric.divide(XSD.decimal(1.0), XSD.integer(0)) == nil + assert Numeric.divide(XSD.decimal(-1.0), XSD.decimal(0.0)) == nil + assert Numeric.divide(XSD.decimal(-1.0), XSD.integer(0)) == nil + assert Numeric.divide(XSD.integer(1), XSD.integer(0)) == nil + assert Numeric.divide(XSD.integer(1), XSD.decimal(0.0)) == nil + assert Numeric.divide(XSD.integer(-1), XSD.integer(0)) == nil + assert Numeric.divide(XSD.integer(-1), XSD.decimal(0.0)) == nil + end + + test "positive or negative zero divided by positive or negative zero returns NaN" do + assert Numeric.divide(XSD.double("-0.0"), XSD.double(0.0)) == @nan + assert Numeric.divide(XSD.double("-0.0"), XSD.decimal(0.0)) == @nan + assert Numeric.divide(XSD.double("-0.0"), XSD.integer(0)) == @nan + assert Numeric.divide(XSD.decimal("-0.0"), XSD.double(0.0)) == @nan + assert Numeric.divide(XSD.integer("-0"), XSD.double(0.0)) == @nan + + assert Numeric.divide(XSD.double("0.0"), XSD.double(0.0)) == @nan + assert Numeric.divide(XSD.double("0.0"), XSD.decimal(0.0)) == @nan + assert Numeric.divide(XSD.double("0.0"), XSD.integer(0)) == @nan + assert Numeric.divide(XSD.decimal("0.0"), XSD.double(0.0)) == @nan + assert Numeric.divide(XSD.integer("0"), XSD.double(0.0)) == @nan + + assert Numeric.divide(XSD.double(0.0), XSD.double("-0.0")) == @nan + assert Numeric.divide(XSD.decimal(0.0), XSD.double("-0.0")) == @nan + assert Numeric.divide(XSD.integer(0), XSD.double("-0.0")) == @nan + assert Numeric.divide(XSD.double(0.0), XSD.decimal("-0.0")) == @nan + assert Numeric.divide(XSD.double(0.0), XSD.integer("-0")) == @nan + + assert Numeric.divide(XSD.double(0.0), XSD.double("0.0")) == @nan + assert Numeric.divide(XSD.decimal(0.0), XSD.double("0.0")) == @nan + assert Numeric.divide(XSD.integer(0), XSD.double("0.0")) == @nan + assert Numeric.divide(XSD.double(0.0), XSD.decimal("0.0")) == @nan + assert Numeric.divide(XSD.double(0.0), XSD.integer("0")) == @nan + end + + test "INF or -INF divided by INF or -INF returns NaN" do + assert Numeric.divide(@positive_infinity, @positive_infinity) == @nan + assert Numeric.divide(@negative_infinity, @negative_infinity) == @nan + assert Numeric.divide(@positive_infinity, @negative_infinity) == @nan + assert Numeric.divide(@negative_infinity, @positive_infinity) == @nan + end + + # TODO: What happens when using INF/-INF on division with numbers? + + test "coercion" do + assert Numeric.divide(4, 2) == XSD.decimal(2.0) + assert Numeric.divide(4, 2.0) == XSD.double(2.0) + assert XSD.decimal(4) |> Numeric.divide(2) == XSD.decimal(2.0) + assert Numeric.divide(4, XSD.decimal(2.0)) == XSD.decimal(2.0) + assert Numeric.divide("foo", "bar") == nil + assert Numeric.divide(4, "bar") == nil + assert Numeric.divide("foo", 2) == nil + assert Numeric.divide(42, :bar) == nil + assert Numeric.divide(:foo, 42) == nil + assert Numeric.divide(:foo, :bar) == nil + end + end + + describe "abs/1" do + test "with xsd:integer" do + assert XSD.integer(42) |> Numeric.abs() == XSD.integer(42) + assert XSD.integer(-42) |> Numeric.abs() == XSD.integer(42) + end + + test "with xsd:double" do + assert XSD.double(3.14) |> Numeric.abs() == XSD.double(3.14) + assert XSD.double(-3.14) |> Numeric.abs() == XSD.double(3.14) + assert XSD.double("INF") |> Numeric.abs() == XSD.double("INF") + assert XSD.double("-INF") |> Numeric.abs() == XSD.double("INF") + assert XSD.double("NAN") |> Numeric.abs() == XSD.double("NAN") + end + + test "with xsd:decimal" do + assert XSD.decimal(3.14) |> Numeric.abs() == XSD.decimal(3.14) + assert XSD.decimal(-3.14) |> Numeric.abs() == XSD.decimal(3.14) + end + + @tag skip: "TODO: type-promotion" + test "with derived numerics" do + assert XSD.byte(-42) |> Numeric.abs() == XSD.byte(42) + assert XSD.byte("-42") |> Numeric.abs() == XSD.byte(42) + assert XSD.non_positive_integer(-42) |> Numeric.abs() == XSD.integer(42) + end + + test "with invalid numeric literals" do + assert XSD.integer("-3.14") |> Numeric.abs() == nil + assert XSD.double("foo") |> Numeric.abs() == nil + assert XSD.decimal("foo") |> Numeric.abs() == nil + end + + test "coercion" do + assert Numeric.abs(42) == XSD.integer(42) + assert Numeric.abs(-42) == XSD.integer(42) + assert Numeric.abs(-3.14) == XSD.double(3.14) + assert Numeric.abs(D.from_float(-3.14)) == XSD.decimal(3.14) + assert Numeric.abs("foo") == nil + assert Numeric.abs(:foo) == nil + end + end + + describe "round/1" do + test "with xsd:integer" do + assert XSD.integer(42) |> Numeric.round() == XSD.integer(42) + assert XSD.integer(-42) |> Numeric.round() == XSD.integer(-42) + end + + test "with xsd:double" do + assert XSD.double(3.14) |> Numeric.round() == XSD.double(3.0) + assert XSD.double(-3.14) |> Numeric.round() == XSD.double(-3.0) + assert XSD.double(-2.5) |> Numeric.round() == XSD.double(-2.0) + + assert XSD.double("INF") |> Numeric.round() == XSD.double("INF") + assert XSD.double("-INF") |> Numeric.round() == XSD.double("-INF") + assert XSD.double("NAN") |> Numeric.round() == XSD.double("NAN") + end + + test "with xsd:decimal" do + assert XSD.decimal(2.5) |> Numeric.round() == XSD.decimal("3") + assert XSD.decimal(2.4999) |> Numeric.round() == XSD.decimal("2") + assert XSD.decimal(-2.5) |> Numeric.round() == XSD.decimal("-2") + end + + test "with invalid numeric literals" do + assert XSD.integer("-3.14") |> Numeric.round() == nil + assert XSD.double("foo") |> Numeric.round() == nil + assert XSD.decimal("foo") |> Numeric.round() == nil + end + + test "coercion" do + assert Numeric.round(-42) == XSD.integer(-42) + assert Numeric.round(-3.14) == XSD.double(-3.0) + assert Numeric.round(D.from_float(3.14)) == XSD.decimal("3") + assert Numeric.round("foo") == nil + assert Numeric.round(:foo) == nil + end + end + + describe "round/2" do + test "with xsd:integer" do + assert XSD.integer(42) |> Numeric.round(3) == XSD.integer(42) + assert XSD.integer(8452) |> Numeric.round(-2) == XSD.integer(8500) + assert XSD.integer(85) |> Numeric.round(-1) == XSD.integer(90) + assert XSD.integer(-85) |> Numeric.round(-1) == XSD.integer(-80) + end + + test "with xsd:double" do + assert XSD.double(3.14) |> Numeric.round(1) == XSD.double(3.1) + assert XSD.double(3.1415e0) |> Numeric.round(2) == XSD.double(3.14e0) + + assert XSD.double("INF") |> Numeric.round(1) == XSD.double("INF") + assert XSD.double("-INF") |> Numeric.round(2) == XSD.double("-INF") + assert XSD.double("NAN") |> Numeric.round(3) == XSD.double("NAN") + end + + test "with xsd:float" do + assert XSD.float(3.14) |> Numeric.round(1) == XSD.float(3.1) + assert XSD.float(3.1415e0) |> Numeric.round(2) == XSD.float(3.14e0) + + assert XSD.float("INF") |> Numeric.round(1) == XSD.float("INF") + assert XSD.float("-INF") |> Numeric.round(2) == XSD.float("-INF") + assert XSD.float("NAN") |> Numeric.round(3) == XSD.float("NAN") + end + + test "with xsd:decimal" do + assert XSD.decimal(1.125) |> Numeric.round(2) == XSD.decimal("1.13") + assert XSD.decimal(2.4999) |> Numeric.round(2) == XSD.decimal("2.50") + assert XSD.decimal(-2.55) |> Numeric.round(1) == XSD.decimal("-2.5") + end + + test "with invalid numeric literals" do + assert XSD.integer("-3.14") |> Numeric.round(1) == nil + assert XSD.double("foo") |> Numeric.round(2) == nil + assert XSD.decimal("foo") |> Numeric.round(3) == nil + end + + test "coercion" do + assert Numeric.round(-42, 1) == XSD.integer(-42) + assert Numeric.round(-3.14, 1) == XSD.double(-3.1) + assert Numeric.round(D.from_float(3.14), 1) == XSD.decimal("3.1") + assert Numeric.round("foo", 1) == nil + assert Numeric.round(:foo, 1) == nil + end + end + + describe "ceil/1" do + test "with xsd:integer" do + assert XSD.integer(42) |> Numeric.ceil() == XSD.integer(42) + assert XSD.integer(-42) |> Numeric.ceil() == XSD.integer(-42) + end + + test "with xsd:double" do + assert XSD.double(10.5) |> Numeric.ceil() == XSD.double("11") + assert XSD.double(-10.5) |> Numeric.ceil() == XSD.double("-10") + + assert XSD.double("INF") |> Numeric.ceil() == XSD.double("INF") + assert XSD.double("-INF") |> Numeric.ceil() == XSD.double("-INF") + assert XSD.double("NAN") |> Numeric.ceil() == XSD.double("NAN") + end + + test "with xsd:float" do + assert XSD.float(10.5) |> Numeric.ceil() == XSD.float("11") + assert XSD.float(-10.5) |> Numeric.ceil() == XSD.float("-10") + + assert XSD.float("INF") |> Numeric.ceil() == XSD.float("INF") + assert XSD.float("-INF") |> Numeric.ceil() == XSD.float("-INF") + assert XSD.float("NAN") |> Numeric.ceil() == XSD.float("NAN") + end + + test "with xsd:decimal" do + assert XSD.decimal(10.5) |> Numeric.ceil() == XSD.decimal("11") + assert XSD.decimal(-10.5) |> Numeric.ceil() == XSD.decimal("-10") + end + + test "with invalid numeric literals" do + assert XSD.integer("-3.14") |> Numeric.ceil() == nil + assert XSD.double("foo") |> Numeric.ceil() == nil + assert XSD.decimal("foo") |> Numeric.ceil() == nil + end + + test "coercion" do + assert Numeric.ceil(-42) == XSD.integer(-42) + assert Numeric.ceil(-3.14) == XSD.double("-3") + assert Numeric.ceil(D.from_float(3.14)) == XSD.decimal("4") + assert Numeric.ceil("foo") == nil + assert Numeric.ceil(:foo) == nil + end + end + + describe "floor/1" do + test "with xsd:integer" do + assert XSD.integer(42) |> Numeric.floor() == XSD.integer(42) + assert XSD.integer(-42) |> Numeric.floor() == XSD.integer(-42) + end + + test "with xsd:double" do + assert XSD.double(10.5) |> Numeric.floor() == XSD.double("10") + assert XSD.double(-10.5) |> Numeric.floor() == XSD.double("-11") + + assert XSD.double("INF") |> Numeric.floor() == XSD.double("INF") + assert XSD.double("-INF") |> Numeric.floor() == XSD.double("-INF") + assert XSD.double("NAN") |> Numeric.floor() == XSD.double("NAN") + end + + test "with xsd:float" do + assert XSD.float(10.5) |> Numeric.floor() == XSD.float("10") + assert XSD.float(-10.5) |> Numeric.floor() == XSD.float("-11") + + assert XSD.float("INF") |> Numeric.floor() == XSD.float("INF") + assert XSD.float("-INF") |> Numeric.floor() == XSD.float("-INF") + assert XSD.float("NAN") |> Numeric.floor() == XSD.float("NAN") + end + + test "with xsd:decimal" do + assert XSD.decimal(10.5) |> Numeric.floor() == XSD.decimal("10") + assert XSD.decimal(-10.5) |> Numeric.floor() == XSD.decimal("-11") + end + + test "with invalid numeric literals" do + assert XSD.integer("-3.14") |> Numeric.floor() == nil + assert XSD.double("foo") |> Numeric.floor() == nil + assert XSD.decimal("foo") |> Numeric.floor() == nil + end + + test "coercion" do + assert Numeric.floor(-42) == XSD.integer(-42) + assert Numeric.floor(-3.14) == XSD.double("-4") + assert Numeric.floor(D.from_float(3.14)) == XSD.decimal("3") + assert Numeric.floor("foo") == nil + assert Numeric.floor(:foo) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/positive_integer_test.exs b/test/unit/xsd/datatypes/positive_integer_test.exs new file mode 100644 index 0000000..1ca5523 --- /dev/null +++ b/test/unit/xsd/datatypes/positive_integer_test.exs @@ -0,0 +1,126 @@ +defmodule RDF.XSD.PositiveIntegerTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.PositiveInteger, + name: "positiveInteger", + base: RDF.XSD.NonNegativeInteger, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: 1, + max_inclusive: nil + }, + valid: RDF.XSD.TestData.valid_positive_integers(), + invalid: RDF.XSD.TestData.invalid_positive_integers() + + describe "cast/1" do + test "casting a positive_integer returns the input as it is" do + assert XSD.positive_integer(1) |> XSD.PositiveInteger.cast() == + XSD.positive_integer(1) + + assert XSD.positive_integer(1) |> XSD.PositiveInteger.cast() == + XSD.positive_integer(1) + end + + test "casting an integer with a value from the value space of positive_integer" do + assert XSD.integer(1) |> XSD.PositiveInteger.cast() == + XSD.positive_integer(1) + end + + test "casting an integer with a value not from the value space of positive_integer" do + assert XSD.integer(-1) |> XSD.PositiveInteger.cast() == nil + assert XSD.non_negative_integer(0) |> XSD.PositiveInteger.cast() == nil + end + + test "casting a positive_integer" do + assert XSD.positive_integer(1) |> XSD.PositiveInteger.cast() == + XSD.positive_integer(1) + end + + test "casting a boolean" do + assert XSD.true() |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + end + + test "casting a string with a value from the lexical value space of xsd:integer" do + assert XSD.string("1") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.string("042") |> XSD.PositiveInteger.cast() == XSD.positive_integer(42) + assert XSD.string("0") |> XSD.PositiveInteger.cast() == nil + end + + test "casting a string with a value not in the lexical value space of xsd:integer" do + assert XSD.string("foo") |> XSD.PositiveInteger.cast() == nil + assert XSD.string("3.14") |> XSD.PositiveInteger.cast() == nil + end + + test "casting an decimal" do + assert XSD.decimal(1.0) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.decimal(3.14) |> XSD.PositiveInteger.cast() == XSD.positive_integer(3) + assert XSD.decimal(0) |> XSD.PositiveInteger.cast() == nil + end + + test "casting a double" do + assert XSD.double(1) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.double(1.0) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.double(1.1) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.double("+1") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.double("+1.0") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.double("1.0E0") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.double(3.14) |> XSD.PositiveInteger.cast() == XSD.positive_integer(3) + + assert XSD.double("NAN") |> XSD.PositiveInteger.cast() == nil + assert XSD.double("+INF") |> XSD.PositiveInteger.cast() == nil + assert XSD.double(0) |> XSD.PositiveInteger.cast() == nil + assert XSD.double(0.0) |> XSD.PositiveInteger.cast() == nil + assert XSD.double(0.1) |> XSD.PositiveInteger.cast() == nil + assert XSD.double("+0") |> XSD.PositiveInteger.cast() == nil + assert XSD.double("+0.0") |> XSD.PositiveInteger.cast() == nil + assert XSD.double("-0.0") |> XSD.PositiveInteger.cast() == nil + assert XSD.double("0.0E0") |> XSD.PositiveInteger.cast() == nil + assert XSD.double("-1.0") |> XSD.PositiveInteger.cast() == nil + end + + test "casting a float" do + assert XSD.float(1) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.float(1.0) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.float(1.1) |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.float("+1") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.float("+1.0") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.float("1.0E0") |> XSD.PositiveInteger.cast() == XSD.positive_integer(1) + assert XSD.float(3.14) |> XSD.PositiveInteger.cast() == XSD.positive_integer(3) + + assert XSD.float("NAN") |> XSD.PositiveInteger.cast() == nil + assert XSD.float("+INF") |> XSD.PositiveInteger.cast() == nil + assert XSD.float(0) |> XSD.PositiveInteger.cast() == nil + assert XSD.float(0.0) |> XSD.PositiveInteger.cast() == nil + assert XSD.float(0.1) |> XSD.PositiveInteger.cast() == nil + assert XSD.float("+0") |> XSD.PositiveInteger.cast() == nil + assert XSD.float("+0.0") |> XSD.PositiveInteger.cast() == nil + assert XSD.float("-0.0") |> XSD.PositiveInteger.cast() == nil + assert XSD.float("0.0E0") |> XSD.PositiveInteger.cast() == nil + assert XSD.float("-1.0") |> XSD.PositiveInteger.cast() == nil + end + + test "with invalid literals" do + assert XSD.positive_integer(3.14) |> XSD.PositiveInteger.cast() == nil + assert XSD.positive_integer(0) |> XSD.PositiveInteger.cast() == nil + assert XSD.decimal("NAN") |> XSD.PositiveInteger.cast() == nil + assert XSD.double(true) |> XSD.PositiveInteger.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.date("2020-01-01") |> XSD.PositiveInteger.cast() == nil + end + + test "with coercible value" do + assert XSD.PositiveInteger.cast("42") == XSD.positive_integer(42) + assert XSD.PositiveInteger.cast(3.14) == XSD.positive_integer(3) + assert XSD.PositiveInteger.cast(true) == XSD.positive_integer(1) + assert XSD.PositiveInteger.cast(false) == nil + end + + test "with non-coercible value" do + assert XSD.PositiveInteger.cast(:foo) == nil + assert XSD.PositiveInteger.cast(make_ref()) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/short_test.exs b/test/unit/xsd/datatypes/short_test.exs new file mode 100644 index 0000000..065a35a --- /dev/null +++ b/test/unit/xsd/datatypes/short_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.ShortTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Short, + name: "short", + base: RDF.XSD.Int, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: -32768, + max_inclusive: 32767 + }, + valid: RDF.XSD.TestData.valid_shorts(), + invalid: RDF.XSD.TestData.invalid_shorts() +end diff --git a/test/unit/xsd/datatypes/string_test.exs b/test/unit/xsd/datatypes/string_test.exs new file mode 100644 index 0000000..f882aa2 --- /dev/null +++ b/test/unit/xsd/datatypes/string_test.exs @@ -0,0 +1,130 @@ +defmodule RDF.XSD.StringTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.String, + name: "string", + primitive: true, + valid: %{ + # input => { value, lexical, canonicalized } + "foo" => {"foo", nil, "foo"}, + 0 => {"0", nil, "0"}, + 42 => {"42", nil, "42"}, + 3.14 => {"3.14", nil, "3.14"}, + true => {"true", nil, "true"}, + false => {"false", nil, "false"} + }, + invalid: [] + + describe "cast/1" do + test "casting a string returns the input as it is" do + assert XSD.string("foo") |> XSD.String.cast() == XSD.string("foo") + end + + test "casting an integer" do + assert XSD.integer(0) |> XSD.String.cast() == XSD.string("0") + assert XSD.integer(1) |> XSD.String.cast() == XSD.string("1") + end + + test "casting a boolean" do + assert XSD.false() |> XSD.String.cast() == XSD.string("false") + assert XSD.true() |> XSD.String.cast() == XSD.string("true") + end + + test "casting a decimal" do + assert XSD.decimal(0) |> XSD.String.cast() == XSD.string("0") + assert XSD.decimal(1.0) |> XSD.String.cast() == XSD.string("1") + assert XSD.decimal(3.14) |> XSD.String.cast() == XSD.string("3.14") + end + + test "casting a double" do + assert XSD.double(0) |> XSD.String.cast() == XSD.string("0") + assert XSD.double(0.0) |> XSD.String.cast() == XSD.string("0") + assert XSD.double("+0") |> XSD.String.cast() == XSD.string("0") + assert XSD.double("-0") |> XSD.String.cast() == XSD.string("-0") + assert XSD.double(0.1) |> XSD.String.cast() == XSD.string("0.1") + assert XSD.double(3.14) |> XSD.String.cast() == XSD.string("3.14") + assert XSD.double(0.000_001) |> XSD.String.cast() == XSD.string("0.000001") + assert XSD.double(123_456) |> XSD.String.cast() == XSD.string("123456") + assert XSD.double(1_234_567) |> XSD.String.cast() == XSD.string("1.234567E6") + assert XSD.double(0.0000001) |> XSD.String.cast() == XSD.string("1.0E-7") + assert XSD.double(1.0e-10) |> XSD.String.cast() == XSD.string("1.0E-10") + assert XSD.double("1.0e-10") |> XSD.String.cast() == XSD.string("1.0E-10") + assert XSD.double(1.26743223e15) |> XSD.String.cast() == XSD.string("1.26743223E15") + + assert XSD.double(:nan) |> XSD.String.cast() == XSD.string("NaN") + assert XSD.double(:positive_infinity) |> XSD.String.cast() == XSD.string("INF") + assert XSD.double(:negative_infinity) |> XSD.String.cast() == XSD.string("-INF") + end + + test "casting a float" do + assert XSD.float(0) |> XSD.String.cast() == XSD.string("0") + assert XSD.float(0.0) |> XSD.String.cast() == XSD.string("0") + assert XSD.float("+0") |> XSD.String.cast() == XSD.string("0") + assert XSD.float("-0") |> XSD.String.cast() == XSD.string("-0") + assert XSD.float(0.1) |> XSD.String.cast() == XSD.string("0.1") + assert XSD.float(3.14) |> XSD.String.cast() == XSD.string("3.14") + assert XSD.float(0.000_001) |> XSD.String.cast() == XSD.string("0.000001") + assert XSD.float(123_456) |> XSD.String.cast() == XSD.string("123456") + assert XSD.float(1_234_567) |> XSD.String.cast() == XSD.string("1.234567E6") + assert XSD.float(0.0000001) |> XSD.String.cast() == XSD.string("1.0E-7") + assert XSD.float(1.0e-10) |> XSD.String.cast() == XSD.string("1.0E-10") + assert XSD.float("1.0e-10") |> XSD.String.cast() == XSD.string("1.0E-10") + assert XSD.float(1.26743223e15) |> XSD.String.cast() == XSD.string("1.26743223E15") + + assert XSD.float(:nan) |> XSD.String.cast() == XSD.string("NaN") + assert XSD.float(:positive_infinity) |> XSD.String.cast() == XSD.string("INF") + assert XSD.float(:negative_infinity) |> XSD.String.cast() == XSD.string("-INF") + end + + test "casting a datetime" do + assert XSD.datetime(~N[2010-01-01T12:34:56]) |> XSD.String.cast() == + XSD.string("2010-01-01T12:34:56") + + assert XSD.datetime("2010-01-01T00:00:00+00:00") |> XSD.String.cast() == + XSD.string("2010-01-01T00:00:00Z") + + assert XSD.datetime("2010-01-01T01:00:00+01:00") |> XSD.String.cast() == + XSD.string("2010-01-01T01:00:00+01:00") + + assert XSD.datetime("2010-01-01 01:00:00+01:00") |> XSD.String.cast() == + XSD.string("2010-01-01T01:00:00+01:00") + end + + test "casting a date" do + assert XSD.date(~D[2000-01-01]) |> XSD.String.cast() == XSD.string("2000-01-01") + assert XSD.date("2000-01-01") |> XSD.String.cast() == XSD.string("2000-01-01") + assert XSD.date("2000-01-01+00:00") |> XSD.String.cast() == XSD.string("2000-01-01Z") + assert XSD.date("2000-01-01+01:00") |> XSD.String.cast() == XSD.string("2000-01-01+01:00") + assert XSD.date("0001-01-01") |> XSD.String.cast() == XSD.string("0001-01-01") + + unless Version.compare(System.version(), "1.7.2") == :lt do + assert XSD.date("-0001-01-01") |> XSD.String.cast() == XSD.string("-0001-01-01") + end + end + + test "casting a time" do + assert XSD.time(~T[00:00:00]) |> XSD.String.cast() == XSD.string("00:00:00") + assert XSD.time("00:00:00") |> XSD.String.cast() == XSD.string("00:00:00") + assert XSD.time("00:00:00Z") |> XSD.String.cast() == XSD.string("00:00:00Z") + assert XSD.time("00:00:00+00:00") |> XSD.String.cast() == XSD.string("00:00:00Z") + assert XSD.time("00:00:00+01:00") |> XSD.String.cast() == XSD.string("00:00:00+01:00") + end + + test "with invalid literals" do + assert XSD.integer(3.14) |> XSD.String.cast() == nil + assert XSD.decimal("NAN") |> XSD.String.cast() == nil + assert XSD.double(true) |> XSD.String.cast() == nil + end + + test "with coercible value" do + assert XSD.String.cast(42) == XSD.string("42") + assert XSD.String.cast(3.14) == XSD.string("3.14") + assert XSD.String.cast(true) == XSD.string("true") + assert XSD.String.cast(false) == XSD.string("false") + end + + test "with non-coercible value" do + assert XSD.String.cast(:foo) == nil + assert XSD.String.cast(make_ref()) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/time_test.exs b/test/unit/xsd/datatypes/time_test.exs new file mode 100644 index 0000000..f20030e --- /dev/null +++ b/test/unit/xsd/datatypes/time_test.exs @@ -0,0 +1,151 @@ +defmodule RDF.XSD.TimeTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.Time, + name: "time", + primitive: true, + valid: %{ + # input => { value, lexical, canonicalized } + ~T[00:00:00] => {~T[00:00:00], nil, "00:00:00"}, + ~T[00:00:00.123] => {~T[00:00:00.123], nil, "00:00:00.123"}, + "00:00:00" => {~T[00:00:00], nil, "00:00:00"}, + "00:00:00.123" => {~T[00:00:00.123], nil, "00:00:00.123"}, + "00:00:00Z" => {{~T[00:00:00], true}, nil, "00:00:00Z"}, + "00:00:00.1234Z" => {{~T[00:00:00.1234], true}, nil, "00:00:00.1234Z"}, + "00:00:00.0000Z" => {{~T[00:00:00.0000], true}, nil, "00:00:00.0000Z"}, + "00:00:00+00:00" => {{~T[00:00:00], true}, "00:00:00+00:00", "00:00:00Z"}, + "00:00:00-00:00" => {{~T[00:00:00], true}, "00:00:00-00:00", "00:00:00Z"}, + "01:00:00+01:00" => {{~T[00:00:00], true}, "01:00:00+01:00", "00:00:00Z"}, + "23:00:00-01:00" => {{~T[00:00:00], true}, "23:00:00-01:00", "00:00:00Z"}, + "23:00:00.45-01:00" => {{~T[00:00:00.45], true}, "23:00:00.45-01:00", "00:00:00.45Z"} + }, + invalid: [ + "foo", + "+2010-01-01Z", + "2010-01-01TFOO", + "02010-01-01", + "2010-1-1", + "0000-01-01", + "2011-07", + "2011", + true, + false, + 2010, + 3.14, + "00:00:00Z foo", + "foo 00:00:00Z", + # this value representation is just internal and not accepted as + {~T[00:00:00], true}, + {~T[00:00:00], "Z"} + ] + + describe "new/2" do + test "with date and tz opt" do + assert XSD.Time.new("12:00:00", tz: "+01:00") == + %RDF.Literal{literal: %XSD.Time{ + value: {~T[11:00:00], true}, + uncanonical_lexical: "12:00:00+01:00" + }} + + assert XSD.Time.new(~T[12:00:00], tz: "+01:00") == + %RDF.Literal{literal: %XSD.Time{ + value: {~T[11:00:00], true}, + uncanonical_lexical: "12:00:00+01:00" + }} + + assert XSD.Time.new("12:00:00", tz: "+00:00") == + %RDF.Literal{literal: %XSD.Time{ + value: {~T[12:00:00], true}, + uncanonical_lexical: "12:00:00+00:00" + }} + + assert XSD.Time.new(~T[12:00:00], tz: "+00:00") == + %RDF.Literal{literal: %XSD.Time{ + value: {~T[12:00:00], true}, + uncanonical_lexical: "12:00:00+00:00" + }} + end + + test "with date string including a timezone and tz opt" do + assert XSD.Time.new("12:00:00+00:00", tz: "+01:00") == + %RDF.Literal{literal: %XSD.Time{ + value: {~T[11:00:00], true}, + uncanonical_lexical: "12:00:00+01:00" + }} + + assert XSD.Time.new("12:00:00+01:00", tz: "Z") == + %RDF.Literal{literal: %XSD.Time{value: {~T[12:00:00], true}}} + + assert XSD.Time.new("12:00:00+01:00", tz: "+00:00") == + %RDF.Literal{literal: %XSD.Time{ + value: {~T[12:00:00], true}, + uncanonical_lexical: "12:00:00+00:00" + }} + end + + test "with invalid tz opt" do + assert XSD.Time.new(~T[12:00:00], tz: "+01:00:42") == + %RDF.Literal{literal: %XSD.Time{uncanonical_lexical: "12:00:00+01:00:42"}} + + assert XSD.Time.new("12:00:00:foo", tz: "+01:00") == + %RDF.Literal{literal: %XSD.Time{uncanonical_lexical: "12:00:00:foo"}} + + assert XSD.Time.new("12:00:00", tz: "+01:00:42") == + %RDF.Literal{literal: %XSD.Time{uncanonical_lexical: "12:00:00"}} + + assert XSD.Time.new("12:00:00+00:00:", tz: "+01:00:") == + %RDF.Literal{literal: %XSD.Time{uncanonical_lexical: "12:00:00+00:00:"}} + end + end + + describe "cast/1" do + test "casting a time returns the input as it is" do + assert XSD.time("01:00:00") |> XSD.Time.cast() == + XSD.time("01:00:00") + end + + test "casting a string" do + assert XSD.string("01:00:00") |> XSD.Time.cast() == + XSD.time("01:00:00") + + assert XSD.string("01:00:00Z") |> XSD.Time.cast() == + XSD.time("01:00:00Z") + + assert XSD.string("01:00:00+01:00") |> XSD.Time.cast() == + XSD.time("01:00:00+01:00") + end + + test "casting a datetime" do + assert XSD.datetime("2010-01-01T01:00:00") |> XSD.Time.cast() == + XSD.time("01:00:00") + + assert XSD.datetime("2010-01-01T00:00:00Z") |> XSD.Time.cast() == + XSD.time("00:00:00Z") + + assert XSD.datetime("2010-01-01T00:00:00+00:00") |> XSD.Time.cast() == + XSD.time("00:00:00Z") + + assert XSD.datetime("2010-01-01T23:00:00+01:00") |> XSD.Time.cast() == + XSD.time("23:00:00+01:00") + end + + test "with invalid literals" do + assert XSD.time("25:00:00") |> XSD.Time.cast() == nil + assert XSD.datetime("02010-01-01T00:00:00") |> XSD.Time.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert XSD.false() |> XSD.Time.cast() == nil + assert XSD.integer(1) |> XSD.Time.cast() == nil + assert XSD.decimal(3.14) |> XSD.Time.cast() == nil + end + + test "with coercible value" do + assert XSD.Time.cast("01:00:00") == XSD.time("01:00:00") + end + + test "with non-coercible value" do + assert XSD.Time.cast(:foo) == nil + assert XSD.Time.cast(make_ref()) == nil + end + end +end diff --git a/test/unit/xsd/datatypes/unsigned_byte_test.exs b/test/unit/xsd/datatypes/unsigned_byte_test.exs new file mode 100644 index 0000000..84a2eb1 --- /dev/null +++ b/test/unit/xsd/datatypes/unsigned_byte_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.UnsignedByteTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.UnsignedByte, + name: "unsignedByte", + base: RDF.XSD.UnsignedShort, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: 0, + max_inclusive: 255 + }, + valid: RDF.XSD.TestData.valid_unsigned_bytes(), + invalid: RDF.XSD.TestData.invalid_unsigned_bytes() +end diff --git a/test/unit/xsd/datatypes/unsigned_int_test.exs b/test/unit/xsd/datatypes/unsigned_int_test.exs new file mode 100644 index 0000000..5212e87 --- /dev/null +++ b/test/unit/xsd/datatypes/unsigned_int_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.UnsignedIntTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.UnsignedInt, + name: "unsignedInt", + base: RDF.XSD.UnsignedLong, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: 0, + max_inclusive: 4_294_967_295 + }, + valid: RDF.XSD.TestData.valid_unsigned_ints(), + invalid: RDF.XSD.TestData.invalid_unsigned_ints() +end diff --git a/test/unit/xsd/datatypes/unsigned_long_test.exs b/test/unit/xsd/datatypes/unsigned_long_test.exs new file mode 100644 index 0000000..130043d --- /dev/null +++ b/test/unit/xsd/datatypes/unsigned_long_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.UnsignedLongTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.UnsignedLong, + name: "unsignedLong", + base: RDF.XSD.NonNegativeInteger, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: 0, + max_inclusive: 18_446_744_073_709_551_615 + }, + valid: RDF.XSD.TestData.valid_unsigned_longs(), + invalid: RDF.XSD.TestData.invalid_unsigned_longs() +end diff --git a/test/unit/xsd/datatypes/unsigned_short_test.exs b/test/unit/xsd/datatypes/unsigned_short_test.exs new file mode 100644 index 0000000..83c46a9 --- /dev/null +++ b/test/unit/xsd/datatypes/unsigned_short_test.exs @@ -0,0 +1,15 @@ +defmodule RDF.XSD.UnsignedShortTest do + use RDF.XSD.Datatype.Test.Case, + datatype: RDF.XSD.UnsignedShort, + name: "unsignedShort", + base: RDF.XSD.UnsignedInt, + base_primitive: RDF.XSD.Integer, + comparable_datatypes: [RDF.XSD.Decimal, RDF.XSD.Double], + applicable_facets: [RDF.XSD.Facets.MinInclusive, RDF.XSD.Facets.MaxInclusive], + facets: %{ + min_inclusive: 0, + max_inclusive: 65535 + }, + valid: RDF.XSD.TestData.valid_unsigned_shorts(), + invalid: RDF.XSD.TestData.invalid_unsigned_shorts() +end diff --git a/test/unit/xsd/utils/regex_test.exs b/test/unit/xsd/utils/regex_test.exs new file mode 100644 index 0000000..288c341 --- /dev/null +++ b/test/unit/xsd/utils/regex_test.exs @@ -0,0 +1,67 @@ +defmodule RDF.XSD.Utils.RegexTest do + use ExUnit.Case + + alias RDF.XSD.Utils.Regex + + @poem """ + + Kaum hat dies der Hahn gesehen, + Fängt er auch schon an zu krähen: + Kikeriki! Kikikerikih!! + Tak, tak, tak! - da kommen sie. + + """ + + describe "matches?" do + test "without flags" do + [ + {"abracadabra", "bra", true}, + {"abracadabra", "^a.*a$", true}, + {"abracadabra", "^bra", false}, + {@poem, "Kaum.*krähen", false}, + {@poem, "^Kaum.*gesehen,$", false}, + {"foobar", "foo$", false}, + {~S"noe\u0308l", ~S"noe\\u0308l", true}, + {~S"noe\\u0308l", ~S"noe\\\\u0308l", true}, + {~S"\u{01D4B8}", ~S"\\U0001D4B8", true}, + {~S"\\U0001D4B8", ~S"\\\U0001D4B8", true}, + {42, "4", true}, + {42, "en", false} + ] + |> Enum.each(fn {literal, pattern, expected_result} -> + result = Regex.matches?(literal, pattern) + + assert result == expected_result, + "expected XSD.Regex.matches?(#{inspect(literal)}, #{inspect(pattern)}) to return #{ + inspect(expected_result) + }, but got #{result}" + end) + end + + test "with flags" do + [ + {@poem, "Kaum.*krähen", "s", true}, + {@poem, "^Kaum.*gesehen,$", "m", true}, + {@poem, "kiki", "i", true} + ] + |> Enum.each(fn {literal, pattern, flags, result} -> + assert Regex.matches?(literal, pattern, flags) == result + end) + end + + test "with q flag" do + [ + {"abcd", ".*", "q", false}, + {"Mr. B. Obama", "B. OBAMA", "iq", true}, + + # If the q flag is used together with the m, s, or x flag, that flag has no effect. + {"abcd", ".*", "mq", true}, + {"abcd", ".*", "qim", true}, + {"abcd", ".*", "xqm", true} + ] + |> Enum.each(fn {literal, pattern, flags, result} -> + assert Regex.matches?(literal, pattern, flags) == result + end) + end + end +end diff --git a/test/unit/xsd/xsd_test.exs b/test/unit/xsd/xsd_test.exs new file mode 100644 index 0000000..6777eef --- /dev/null +++ b/test/unit/xsd/xsd_test.exs @@ -0,0 +1,17 @@ +defmodule RDF.XSDTest do + use RDF.Test.Case + + doctest RDF.XSD + + test "Datatype constructor alias functions" do + Enum.each(XSD.datatypes(), fn datatype -> + assert apply(XSD, String.to_atom(datatype.name), [1]) == datatype.new(1) + assert apply(XSD, String.to_atom(Macro.underscore(datatype.name)), [1]) == datatype.new(1) + end) + end + + test "true and false aliases" do + assert XSD.true == XSD.Boolean.new(true) + assert XSD.false == XSD.Boolean.new(false) + end +end