From 56c153198f90b12651ba87e20b36666155544d6b Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Sun, 9 Sep 2018 15:28:35 +0200 Subject: [PATCH] Add implementation of RDF.Datatype.cast/1 on numeric datatypes --- lib/rdf/datatypes/decimal.ex | 32 +++++++++++++++++ lib/rdf/datatypes/double.ex | 37 ++++++++++++++++++++ lib/rdf/datatypes/integer.ex | 40 ++++++++++++++++++++++ test/unit/datatypes/decimal_test.exs | 51 ++++++++++++++++++++++++++++ test/unit/datatypes/double_test.exs | 44 ++++++++++++++++++++++++ test/unit/datatypes/integer_test.exs | 51 ++++++++++++++++++++++++++++ 6 files changed, 255 insertions(+) diff --git a/lib/rdf/datatypes/decimal.ex b/lib/rdf/datatypes/decimal.ex index 894a09d..8b4e3d8 100644 --- a/lib/rdf/datatypes/decimal.ex +++ b/lib/rdf/datatypes/decimal.ex @@ -4,6 +4,8 @@ defmodule RDF.Decimal do """ use RDF.Datatype, id: RDF.Datatype.NS.XSD.decimal + import RDF.Literal.Guards + alias Elixir.Decimal, as: D @@ -64,6 +66,36 @@ defmodule RDF.Decimal do do: canonical_decimal(%{decimal | coef: Kernel.div(coef, 10), exp: exp + 1}) + + def cast(%RDF.Literal{datatype: datatype} = literal) do + cond do + not RDF.Literal.valid?(literal) -> + nil + + is_xsd_decimal(datatype) -> + literal + + literal == RDF.false -> + new(0.0) + + literal == RDF.true -> + new(1.0) + + is_xsd_string(datatype) -> + literal.value + |> new() + |> canonical() + + is_number(literal.value) and (is_xsd_integer(datatype) or + is_xsd_double(datatype) or is_xsd_float(datatype)) -> + new(literal.value) + + true -> + nil + end + end + + def equal_value?(left, right), do: RDF.Numeric.equal_value?(left, right) end diff --git a/lib/rdf/datatypes/double.ex b/lib/rdf/datatypes/double.ex index 6d709f1..4df1f82 100644 --- a/lib/rdf/datatypes/double.ex +++ b/lib/rdf/datatypes/double.ex @@ -5,6 +5,8 @@ defmodule RDF.Double do use RDF.Datatype, id: RDF.Datatype.NS.XSD.double + import RDF.Literal.Guards + def build_literal_by_value(value, opts) do case convert(value, opts) do @@ -91,6 +93,41 @@ defmodule RDF.Double do end end + + + def cast(%RDF.Literal{datatype: datatype} = literal) do + cond do + not RDF.Literal.valid?(literal) -> + nil + + is_xsd_double(datatype) -> + literal + + literal == RDF.false -> + new(0.0) + + literal == RDF.true -> + new(1.0) + + is_xsd_string(datatype) -> + literal.value + |> new() + |> canonical() + + is_xsd_decimal(datatype) -> + literal.value + |> Decimal.to_float() + |> new() + + is_xsd_integer(datatype) or is_xsd_float(datatype) -> + new(literal.value) + + true -> + nil + end + end + + def equal_value?(left, right), do: RDF.Numeric.equal_value?(left, right) end diff --git a/lib/rdf/datatypes/integer.ex b/lib/rdf/datatypes/integer.ex index 7606fa7..196cc68 100644 --- a/lib/rdf/datatypes/integer.ex +++ b/lib/rdf/datatypes/integer.ex @@ -5,6 +5,8 @@ defmodule RDF.Integer do use RDF.Datatype, id: RDF.Datatype.NS.XSD.integer + import RDF.Literal.Guards + def convert(value, _) when is_integer(value), do: value @@ -18,6 +20,44 @@ defmodule RDF.Integer do def convert(value, opts), do: super(value, opts) + + def cast(%RDF.Literal{datatype: datatype} = literal) do + cond do + not RDF.Literal.valid?(literal) -> + nil + + is_xsd_integer(datatype) -> + literal + + literal == RDF.false -> + new(0) + + literal == RDF.true -> + new(1) + + is_xsd_string(datatype) -> + literal.value + |> new() + |> canonical() + + is_xsd_decimal(datatype) -> + literal.value + |> Decimal.round(0, :down) + |> Decimal.to_integer() + |> new() + + is_float(literal.value) and + (is_xsd_double(datatype) or is_xsd_float(datatype)) -> + literal.value + |> trunc() + |> new() + + true -> + nil + end + end + + def equal_value?(left, right), do: RDF.Numeric.equal_value?(left, right) end diff --git a/test/unit/datatypes/decimal_test.exs b/test/unit/datatypes/decimal_test.exs index c390e50..5d50bac 100644 --- a/test/unit/datatypes/decimal_test.exs +++ b/test/unit/datatypes/decimal_test.exs @@ -65,6 +65,57 @@ defmodule RDF.DecimalTest do end + describe "cast/1" do + test "casting a decimal returns the input as it is" do + assert RDF.decimal(0) |> RDF.Decimal.cast() == RDF.decimal(0) + assert RDF.decimal("-0.0") |> RDF.Decimal.cast() == RDF.decimal("-0.0") + assert RDF.decimal(1) |> RDF.Decimal.cast() == RDF.decimal(1) + assert RDF.decimal(0.1) |> RDF.Decimal.cast() == RDF.decimal(0.1) + end + + test "casting a boolean" do + assert RDF.true |> RDF.Decimal.cast() == RDF.decimal(1.0) + assert RDF.false |> RDF.Decimal.cast() == RDF.decimal(0.0) + end + + test "casting a string" do + assert RDF.string("0") |> RDF.Decimal.cast() == RDF.decimal(0) + assert RDF.string("3.14") |> RDF.Decimal.cast() == RDF.decimal(3.14) + end + + test "casting an integer" do + assert RDF.integer(0) |> RDF.Decimal.cast() == RDF.decimal(0.0) + assert RDF.integer(42) |> RDF.Decimal.cast() == RDF.decimal(42.0) + end + + test "casting a double" do + assert RDF.double(0.0) |> RDF.Decimal.cast() == RDF.decimal(0.0) + assert RDF.double("-0.0") |> RDF.Decimal.cast() == RDF.decimal(0.0) + assert RDF.double(0.1) |> RDF.Decimal.cast() == RDF.decimal(0.1) + assert RDF.double(1) |> RDF.Decimal.cast() == RDF.decimal(1.0) + assert RDF.double(3.14) |> RDF.Decimal.cast() == RDF.decimal(3.14) + assert RDF.double(10.1e1) |> RDF.Decimal.cast() == RDF.decimal(101.0) + + assert RDF.double("NAN") |> RDF.Decimal.cast() == nil + assert RDF.double("+INF") |> RDF.Decimal.cast() == nil + end + + @tag skip: "TODO: RDF.Float datatype" + test "casting a float" + + test "with invalid literals" do + assert RDF.boolean("42") |> RDF.Decimal.cast() == nil + assert RDF.integer(3.14) |> RDF.Decimal.cast() == nil + assert RDF.decimal("NAN") |> RDF.Decimal.cast() == nil + assert RDF.double(true) |> RDF.Decimal.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert RDF.DateTime.now() |> RDF.Decimal.cast() == nil + end + end + + defmacrop sigil_d(str, _opts) do quote do Elixir.Decimal.new(unquote(str)) diff --git a/test/unit/datatypes/double_test.exs b/test/unit/datatypes/double_test.exs index a156e40..d490614 100644 --- a/test/unit/datatypes/double_test.exs +++ b/test/unit/datatypes/double_test.exs @@ -58,4 +58,48 @@ defmodule RDF.DoubleTest do end end + + describe "cast/1" do + test "casting a double returns the input as it is" do + assert RDF.double(3.14) |> RDF.Double.cast() == RDF.double(3.14) + assert RDF.double("NAN") |> RDF.Double.cast() == RDF.double("NAN") + assert RDF.double("+INF") |> RDF.Double.cast() == RDF.double("+INF") + end + + test "casting a boolean" do + assert RDF.true |> RDF.Double.cast() == RDF.double(1.0) + assert RDF.false |> RDF.Double.cast() == RDF.double(0.0) + end + + test "casting a string" do + assert RDF.string("1.0") |> RDF.Double.cast() == RDF.double("1.0E0") + assert RDF.string("3.14") |> RDF.Double.cast() == RDF.double("3.14E0") + end + + test "casting an integer" do + assert RDF.integer(0) |> RDF.Double.cast() == RDF.double(0.0) + assert RDF.integer(42) |> RDF.Double.cast() == RDF.double(42.0) + end + + test "casting a decimal" do + assert RDF.decimal(0) |> RDF.Double.cast() == RDF.double(0) + assert RDF.decimal(1) |> RDF.Double.cast() == RDF.double(1) + assert RDF.decimal(3.14) |> RDF.Double.cast() == RDF.double(3.14) + end + + @tag skip: "TODO: RDF.Float datatype" + test "casting a float" + + test "with invalid literals" do + assert RDF.boolean("42") |> RDF.Double.cast() == nil + assert RDF.integer(3.14) |> RDF.Double.cast() == nil + assert RDF.decimal("NAN") |> RDF.Double.cast() == nil + assert RDF.double(true) |> RDF.Double.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert RDF.DateTime.now() |> RDF.Double.cast() == nil + end + end + end diff --git a/test/unit/datatypes/integer_test.exs b/test/unit/datatypes/integer_test.exs index 92cda72..5d71f8d 100644 --- a/test/unit/datatypes/integer_test.exs +++ b/test/unit/datatypes/integer_test.exs @@ -16,6 +16,57 @@ defmodule RDF.IntegerTest do invalid: ~w(foo 10.1 12xyz) ++ [true, false, 3.14, "1 2", "foo 1", "1 foo"] + describe "cast/1" do + test "casting an integer returns the input as it is" do + assert RDF.integer(0) |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.integer(1) |> RDF.Integer.cast() == RDF.integer(1) + end + + test "casting a boolean" do + assert RDF.false |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.true |> RDF.Integer.cast() == RDF.integer(1) + end + + test "casting a string" do + assert RDF.string("0") |> RDF.Integer.cast() == RDF.integer(0) + end + + test "casting an decimal" do + assert RDF.decimal(0) |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.decimal(1.0) |> RDF.Integer.cast() == RDF.integer(1) + assert RDF.decimal(3.14) |> RDF.Integer.cast() == RDF.integer(3) + end + + test "casting a double" do + assert RDF.double(0) |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double(0.0) |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double(0.1) |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double("+0") |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double("+0.0") |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double("-0.0") |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double("0.0E0") |> RDF.Integer.cast() == RDF.integer(0) + assert RDF.double(1) |> RDF.Integer.cast() == RDF.integer(1) + assert RDF.double(3.14) |> RDF.Integer.cast() == RDF.integer(3) + + assert RDF.double("NAN") |> RDF.Integer.cast() == nil + assert RDF.double("+INF") |> RDF.Integer.cast() == nil + end + + @tag skip: "TODO: RDF.Float datatype" + test "casting a float" + + test "with invalid literals" do + assert RDF.integer(3.14) |> RDF.Integer.cast() == nil + assert RDF.decimal("NAN") |> RDF.Integer.cast() == nil + assert RDF.double(true) |> RDF.Integer.cast() == nil + end + + test "with literals of unsupported datatypes" do + assert RDF.DateTime.now() |> RDF.Integer.cast() == nil + end + end + + describe "equality" do test "two literals are equal when they have the same datatype and lexical form" do [