diff --git a/lib/rdf/datatype.ex b/lib/rdf/datatype.ex index 0e72045..f33ba32 100644 --- a/lib/rdf/datatype.ex +++ b/lib/rdf/datatype.ex @@ -8,6 +8,8 @@ defmodule RDF.Datatype do @callback canonical_lexical(any) :: binary + @callback invalid_lexical(any) :: binary + @callback canonical(RDF.Literal.t) :: RDF.Literal.t @doc """ @@ -85,7 +87,7 @@ defmodule RDF.Datatype do def build_literal_by_value(value, opts) do case convert(value, opts) do nil -> - build_literal(nil, canonical_lexical(value), opts) + build_literal(nil, invalid_lexical(value), opts) converted_value -> build_literal(converted_value, nil, opts) end @@ -128,6 +130,7 @@ defmodule RDF.Datatype do def canonical_lexical(value), do: to_string(value) + def invalid_lexical(value), do: to_string(value) def canonical(%Literal{value: nil} = literal), do: literal def canonical(%Literal{uncanonical_lexical: nil} = literal), do: literal @@ -146,6 +149,7 @@ defmodule RDF.Datatype do build_literal: 3, lexical: 1, canonical_lexical: 1, + invalid_lexical: 1, convert: 2, valid?: 1, ] diff --git a/lib/rdf/datatypes/double.ex b/lib/rdf/datatypes/double.ex index 031cbff..4cc85e5 100644 --- a/lib/rdf/datatypes/double.ex +++ b/lib/rdf/datatypes/double.ex @@ -1,10 +1,82 @@ defmodule RDF.Double do use RDF.Datatype, id: RDF.Datatype.NS.XSD.double + + def build_literal_by_value(value, opts) do + case convert(value, opts) do + float when is_float(float) -> + build_literal(float, decimal_form(float), opts) + nil -> + build_literal(nil, invalid_lexical(value), opts) + special_value when is_atom(special_value) -> + build_literal(special_value, nil, opts) + end + end + + def convert(value, _) when is_float(value), do: value + def convert(value, _) when is_integer(value), do: value / 1 - def convert(value, _) when is_binary(value), do: String.to_float(value) -# def convert(false, _), do: 0.0 -# def convert(true, _), do: 1.0 + + def convert(value, opts) when is_binary(value) do + case Float.parse(value) 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 + convert(to_string(float) <> String.trim_leading(remainder, "."), opts) + else + super(value, opts) + end + :error -> + case String.upcase(value) do + "INF" -> :positive_infinity + "+INF" -> :positive_infinity + "-INF" -> :negative_infinity + "NAN" -> :nan + _ -> super(value, opts) + end + end + end + + def convert(value, _) + when value in ~W[positive_infinity negative_infinity nan]a, + do: value + + def convert(value, opts), do: super(value, opts) + + + def canonical_lexical(value) do + case value do + :nan -> "NaN" + :positive_infinity -> "INF" + :negative_infinity -> "-INF" + float when is_float(float) -> exponential_form(float) + _ -> to_string(value) + + end + end + + def decimal_form(float) when is_float(float) do + to_string(float) + end + + def exponential_form(float) when is_float(float) do + # Can't use simple %f transformation due to special requirements from + # N3 tests in representation + [i, f, e] = + :io_lib.format("~.15e", [float]) + |> List.first + |> to_string + |> String.split(~r/[\.e]/) + f = + case String.replace(f, ~r/0*$/, "", global: false) do # remove any trailing zeroes + "" -> "0" # ...but there must be a digit to the right of the decimal point + f -> f + end + e = String.trim_leading(e, "+") + "#{i}.#{f}E#{e}" + end end diff --git a/lib/rdf/datatypes/integer.ex b/lib/rdf/datatypes/integer.ex index b9b0112..f62e95b 100644 --- a/lib/rdf/datatypes/integer.ex +++ b/lib/rdf/datatypes/integer.ex @@ -7,11 +7,12 @@ defmodule RDF.Integer do def convert(value, opts) when is_binary(value) do case Integer.parse(value) do {integer, ""} -> integer - {integer, _} -> super(value, opts) + {_, _} -> super(value, opts) :error -> super(value, opts) end end def convert(value, opts), do: super(value, opts) + end diff --git a/lib/rdf/datatypes/lang_string.ex b/lib/rdf/datatypes/lang_string.ex index 6f5fbad..4e5a89a 100644 --- a/lib/rdf/datatypes/lang_string.ex +++ b/lib/rdf/datatypes/lang_string.ex @@ -2,13 +2,6 @@ defmodule RDF.LangString do use RDF.Datatype, id: RDF.langString - def convert(value, _), do: to_string(value) - - - def valid?(%Literal{language: nil}), do: false - def valid?(literal), do: super(literal) - - def build_literal(value, lexical, %{language: language} = opts) do %Literal{super(value, lexical, opts) | language: String.downcase(language)} end @@ -17,4 +10,12 @@ defmodule RDF.LangString do raise ArgumentError, "datatype of rdf:langString requires a language" end + + def convert(value, _), do: to_string(value) + + + def valid?(%Literal{language: nil}), do: false + def valid?(literal), do: super(literal) + + end diff --git a/lib/rdf/datatypes/string.ex b/lib/rdf/datatypes/string.ex index 11a17ff..4787f8f 100644 --- a/lib/rdf/datatypes/string.ex +++ b/lib/rdf/datatypes/string.ex @@ -2,11 +2,12 @@ defmodule RDF.String do use RDF.Datatype, id: RDF.Datatype.NS.XSD.string - def convert(value, _), do: to_string(value) - - def build_literal_by_lexical(lexical, opts) do build_literal(lexical, nil, opts) end + + def convert(value, _), do: to_string(value) + + end diff --git a/lib/rdf/literal.ex b/lib/rdf/literal.ex index 0772ab4..1108a0f 100644 --- a/lib/rdf/literal.ex +++ b/lib/rdf/literal.ex @@ -128,7 +128,7 @@ defmodule RDF.Literal do see """ def simple?(%RDF.Literal{datatype: @xsd_string}), do: true - def simple?(foo), do: false + def simple?(_), do: false @doc """ diff --git a/test/unit/datatypes/double_test.exs b/test/unit/datatypes/double_test.exs new file mode 100644 index 0000000..4efe4f2 --- /dev/null +++ b/test/unit/datatypes/double_test.exs @@ -0,0 +1,60 @@ +defmodule RDF.DoubleTest do + use RDF.Datatype.Test.Case, datatype: RDF.Double, id: RDF.NS.XSD.double, + valid: %{ + # input => { value , lexical , canonicalized } + 0 => { 0.0 , "0.0" , "0.0E0" }, + 42 => { 42.0 , "42.0" , "4.2E1" }, + 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" => { :positive_infinity , "+INF" , "INF" }, + "-INF" => { :negative_infinity , nil , "-INF" }, + "NaN" => { :nan , nil , "NaN" }, + }, + invalid: ~w(foo 12.xyz 1.0ez) ++ [true, false, "1.1e1 foo", "foo 1.1e1"] + + + describe "equality" do + test "two literals are equal when they have the same datatype and lexical form" do + [ + {"1.0" , 1.0}, + {"-42.0" , -42.0}, + {"1.0" , 1.0}, + ] + |> Enum.each(fn {l, r} -> + assert Double.new(l) == Double.new(r) + end) + end + + test "two literals with same value but different lexical form are not equal" do + [ + {"1" , 1.0}, + {"01" , 1.0}, + {"1.0E0" , 1.0}, + {"1.0E0" , "1.0"}, + {"+42" , 42.0}, + ] + |> Enum.each(fn {l, r} -> + assert Double.new(l) != Double.new(r) + end) + end + end + +end diff --git a/test/unit/literal_test.exs b/test/unit/literal_test.exs index d05fd24..5f76fb8 100644 --- a/test/unit/literal_test.exs +++ b/test/unit/literal_test.exs @@ -47,6 +47,11 @@ defmodule RDF.LiteralTest do assert Literal.new("42", datatype: XSD.integer) == RDF.Integer.new("42") end + test "double" do + assert Literal.new(3.14, datatype: XSD.double) == RDF.Double.new(3.14) + assert Literal.new("3.14", datatype: XSD.double) == RDF.Double.new("3.14") + end + test "string" do assert Literal.new("foo", datatype: XSD.string) == RDF.String.new("foo") end