Add arithmetic operations on RDF.Numeric

This commit is contained in:
Marcel Otto 2018-06-22 22:03:23 +02:00
parent ca3c4a0104
commit 7ad3c0acc1
3 changed files with 519 additions and 7 deletions

View file

@ -15,7 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
- `RDF.Decimal` datatype for `xsd:decimal` literals and support for decimal
literals in Turtle encoder
- `RDF.Numeric` with a list of all numeric datatypes and shared functions for
all numeric literals
all numeric literals, eg. arithmetic functions
- the logical operators and the Effective Boolean Value (EBV) coercion algorithm
from the XPath and SPARQL specs on `RDF.Boolean`
- `RDF.Term.equal?/2` and `RDF.Term.equal_value?/2`

View file

@ -27,6 +27,10 @@ defmodule RDF.Numeric do
XSD.positiveInteger,
]
@xsd_decimal XSD.decimal
@xsd_double XSD.double
@doc """
The list of all numeric datatypes.
"""
@ -37,7 +41,12 @@ defmodule RDF.Numeric do
"""
def type?(type), do: MapSet.member?(@types, type)
@xsd_decimal XSD.decimal
@doc """
Returns if a given literal has a numeric datatype.
"""
def literal?(%Literal{datatype: datatype}), do: type?(datatype)
def literal?(_), do: false
@doc """
Tests for numeric value equality of two numeric literals.
@ -60,11 +69,6 @@ defmodule RDF.Numeric do
%Literal{datatype: right_datatype, value: right})
do
if type?(left_datatype) and type?(right_datatype) do
# We rely here on Elixirs numeric equality comparison.
# TODO: There are probably problematic edge-case, which might require an
# TODO: implementation of XPath type promotion and subtype substitution
# - https://www.w3.org/TR/xpath-functions/#op.numeric
# - https://www.w3.org/TR/xpath20/#id-type-promotion-and-operator-mapping
left == right
end
end
@ -76,4 +80,198 @@ defmodule RDF.Numeric do
defp equal_decimal_value?(left, %D{} = right), do: equal_decimal_value?(D.new(left), right)
defp equal_decimal_value?(_, _), do: nil
def zero?(%Literal{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
def negative_zero?(%Literal{value: zero, uncanonical_lexical: "-" <> _, datatype: @xsd_double})
when zero == 0, do: true
def negative_zero?(%Literal{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, `nil` is returned.
see <http://www.w3.org/TR/xpath-functions/#func-numeric-add>
"""
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, `nil` is returned.
see <http://www.w3.org/TR/xpath-functions/#func-numeric-subtract>
"""
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, `nil` is returned.
see <http://www.w3.org/TR/xpath-functions/#func-numeric-multiply>
"""
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, `nil` is returned.
'nil` is also returned for `xsd:decimal` and `xsd:integer` operands, if the
divisor is (positive or negative) zero.
see <http://www.w3.org/TR/xpath-functions/#func-numeric-divide>
"""
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
not result_type in [XSD.double] -> nil # TODO: or XSD.float
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
defp arithmetic_operation(op, arg1, arg2, fun) do
if literal?(arg1) && literal?(arg2) do
with result_type = result_type(op, arg1.datatype, arg2.datatype),
{arg1, arg2} = type_conversion(arg1, arg2, result_type),
result = fun.(arg1.value, arg2.value, result_type)
do
unless is_nil(result),
do: Literal.new(result, datatype: result_type)
end
end
end
defp type_conversion(%Literal{datatype: @xsd_decimal} = arg1,
%Literal{value: arg2}, @xsd_decimal),
do: {arg1, RDF.decimal(arg2)}
defp type_conversion(%Literal{value: arg1},
%Literal{datatype: @xsd_decimal} = arg2, @xsd_decimal),
do: {RDF.decimal(arg1), arg2}
defp type_conversion(%Literal{datatype: @xsd_decimal, value: arg1}, arg2, @xsd_double),
do: {arg1 |> D.to_float() |> RDF.double(), arg2}
defp type_conversion(arg1, %Literal{datatype: @xsd_decimal, value: arg2}, @xsd_double),
do: {arg1, arg2 |> D.to_float() |> RDF.double()}
defp type_conversion(arg1, arg2, _), do: {arg1, arg2}
defp result_type(:/, type1, type2) do
types = [type1, type2]
cond do
XSD.double in types -> XSD.double
XSD.float in types -> XSD.float
true -> XSD.decimal
end
end
defp result_type(_, type1, type2) do
types = [type1, type2]
cond do
XSD.double in types -> XSD.double
XSD.float in types -> XSD.float
XSD.decimal in types -> XSD.decimal
true -> XSD.integer
end
end
end

View file

@ -0,0 +1,314 @@
defmodule RDF.NumericTest do
use RDF.Test.Case
alias RDF.Numeric
@positive_infinity RDF.double(:positive_infinity)
@negative_infinity RDF.double(:negative_infinity)
@nan RDF.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?(RDF.double(negative_zero))
assert Numeric.negative_zero?(RDF.decimal(negative_zero))
end
refute Numeric.negative_zero?(RDF.double("-0.00001"))
refute Numeric.negative_zero?(RDF.decimal("-0.00001"))
end
test "zero?/1" do
assert Numeric.zero?(RDF.integer(0))
assert Numeric.zero?(RDF.integer("0"))
~w[
0
000
0.0
00.00
]
|> Enum.each(fn positive_zero ->
assert Numeric.zero?(RDF.double(positive_zero))
assert Numeric.zero?(RDF.decimal(positive_zero))
end)
Enum.each @negative_zeros, fn negative_zero ->
assert Numeric.zero?(RDF.double(negative_zero))
assert Numeric.zero?(RDF.decimal(negative_zero))
end
refute Numeric.zero?(RDF.double("-0.00001"))
refute Numeric.zero?(RDF.decimal("-0.00001"))
end
describe "add/2" do
test "xsd:integer literal + xsd:integer literal" do
assert Numeric.add(RDF.integer(1), RDF.integer(2)) == RDF.integer(3)
end
test "xsd:decimal literal + xsd:integer literal" do
assert Numeric.add(RDF.decimal(1.1), RDF.integer(2)) == RDF.decimal(3.1)
end
test "xsd:double literal + xsd:integer literal" do
result = Numeric.add(RDF.double(1.1), RDF.integer(2))
expected = RDF.double(3.1)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 0.000000000000001
end
test "xsd:decimal literal + xsd:double literal" do
result = Numeric.add(RDF.decimal(1.1), RDF.double(2.2))
expected = RDF.double(3.3)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 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, RDF.double(0)) == @positive_infinity
assert Numeric.add(@positive_infinity, RDF.double(3.14)) == @positive_infinity
assert Numeric.add(RDF.double(0), @positive_infinity) == @positive_infinity
assert Numeric.add(RDF.double(3.14), @positive_infinity) == @positive_infinity
assert Numeric.add(@negative_infinity, RDF.double(0)) == @negative_infinity
assert Numeric.add(@negative_infinity, RDF.double(3.14)) == @negative_infinity
assert Numeric.add(RDF.double(0), @negative_infinity) == @negative_infinity
assert Numeric.add(RDF.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) == RDF.double(:nan)
assert Numeric.add(@negative_infinity, @positive_infinity) == RDF.double(:nan)
end
end
describe "subtract/2" do
test "xsd:integer literal - xsd:integer literal" do
assert Numeric.subtract(RDF.integer(3), RDF.integer(2)) == RDF.integer(1)
end
test "xsd:decimal literal - xsd:integer literal" do
assert Numeric.subtract(RDF.decimal(3.3), RDF.integer(2)) == RDF.decimal(1.3)
end
test "xsd:double literal - xsd:integer literal" do
result = Numeric.subtract(RDF.double(3.3), RDF.integer(2))
expected = RDF.double(1.3)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 0.000000000000001
end
test "xsd:decimal literal - xsd:double literal" do
result = Numeric.subtract(RDF.decimal(3.3), RDF.double(2.2))
expected = RDF.double(1.1)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 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, RDF.double(0)) == @positive_infinity
assert Numeric.subtract(@positive_infinity, RDF.double(3.14)) == @positive_infinity
assert Numeric.subtract(RDF.double(0), @positive_infinity) == @negative_infinity
assert Numeric.subtract(RDF.double(3.14), @positive_infinity) == @negative_infinity
assert Numeric.subtract(@negative_infinity, RDF.double(0)) == @negative_infinity
assert Numeric.subtract(@negative_infinity, RDF.double(3.14)) == @negative_infinity
assert Numeric.subtract(RDF.double(0), @negative_infinity) == @positive_infinity
assert Numeric.subtract(RDF.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) == RDF.double(:nan)
assert Numeric.subtract(@negative_infinity, @negative_infinity) == RDF.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
end
describe "multiply/2" do
test "xsd:integer literal * xsd:integer literal" do
assert Numeric.multiply(RDF.integer(2), RDF.integer(3)) == RDF.integer(6)
end
test "xsd:decimal literal * xsd:integer literal" do
assert Numeric.multiply(RDF.decimal(1.5), RDF.integer(3)) == RDF.decimal(4.5)
end
test "xsd:double literal * xsd:integer literal" do
result = Numeric.multiply(RDF.double(1.5), RDF.integer(3))
expected = RDF.double(4.5)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 0.000000000000001
end
test "xsd:decimal literal * xsd:double literal" do
result = Numeric.multiply(RDF.decimal(0.5), RDF.double(2.5))
expected = RDF.double(1.25)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 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, RDF.double(0.0)) == @nan
assert Numeric.multiply(RDF.integer(0), @positive_infinity) == @nan
assert Numeric.multiply(RDF.decimal(0), @positive_infinity) == @nan
assert Numeric.multiply(@negative_infinity, RDF.double(0)) == @nan
assert Numeric.multiply(RDF.integer(0), @negative_infinity) == @nan
assert Numeric.multiply(RDF.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, RDF.double(3.14)) == @positive_infinity
assert Numeric.multiply(RDF.double(3.14), @positive_infinity) == @positive_infinity
assert Numeric.multiply(@positive_infinity, RDF.double(-3.14)) == @negative_infinity
assert Numeric.multiply(RDF.double(-3.14), @positive_infinity) == @negative_infinity
assert Numeric.multiply(@negative_infinity, RDF.double(3.14)) == @negative_infinity
assert Numeric.multiply(RDF.double(3.14), @negative_infinity) == @negative_infinity
assert Numeric.multiply(@negative_infinity, RDF.double(-3.14)) == @positive_infinity
assert Numeric.multiply(RDF.double(-3.14), @negative_infinity) == @positive_infinity
end
# The following are 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) == RDF.double(:nan)
assert Numeric.multiply(@negative_infinity, @positive_infinity) == RDF.double(:nan)
end
end
describe "divide/2" do
test "xsd:integer literal / xsd:integer literal" do
assert Numeric.divide(RDF.integer(4), RDF.integer(2)) == RDF.decimal(2.0)
end
test "xsd:decimal literal / xsd:integer literal" do
assert Numeric.divide(RDF.decimal(4), RDF.integer(2)) == RDF.decimal(2.0)
end
test "xsd:double literal / xsd:integer literal" do
result = Numeric.divide(RDF.double(4), RDF.integer(2))
expected = RDF.double(2)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 0.000000000000001
end
test "xsd:decimal literal / xsd:double literal" do
result = Numeric.divide(RDF.decimal(4), RDF.double(2))
expected = RDF.double(2)
assert result.datatype == expected.datatype
assert_in_delta result.value, expected.value, 0.000000000000001
end
test "a positive number divided by positive zero returns INF" do
assert Numeric.divide(RDF.double(1.0), RDF.double(0.0)) == @positive_infinity
assert Numeric.divide(RDF.double(1.0), RDF.decimal(0.0)) == @positive_infinity
assert Numeric.divide(RDF.double(1.0), RDF.integer(0)) == @positive_infinity
assert Numeric.divide(RDF.decimal(1.0), RDF.double(0.0)) == @positive_infinity
assert Numeric.divide(RDF.integer(1), RDF.double(0.0)) == @positive_infinity
end
test "a negative number divided by positive zero returns -INF" do
assert Numeric.divide(RDF.double(-1.0), RDF.double(0.0)) == @negative_infinity
assert Numeric.divide(RDF.double(-1.0), RDF.decimal(0.0)) == @negative_infinity
assert Numeric.divide(RDF.double(-1.0), RDF.integer(0)) == @negative_infinity
assert Numeric.divide(RDF.decimal(-1.0), RDF.double(0.0)) == @negative_infinity
assert Numeric.divide(RDF.integer(-1), RDF.double(0.0)) == @negative_infinity
end
test "a positive number divided by negative zero returns -INF" do
assert Numeric.divide(RDF.double(1.0), RDF.double("-0.0")) == @negative_infinity
assert Numeric.divide(RDF.double(1.0), RDF.decimal("-0.0")) == @negative_infinity
assert Numeric.divide(RDF.decimal(1.0), RDF.double("-0.0")) == @negative_infinity
assert Numeric.divide(RDF.integer(1), RDF.double("-0.0")) == @negative_infinity
end
test "a negative number divided by negative zero returns INF" do
assert Numeric.divide(RDF.double(-1.0), RDF.double("-0.0")) == @positive_infinity
assert Numeric.divide(RDF.double(-1.0), RDF.decimal("-0.0")) == @positive_infinity
assert Numeric.divide(RDF.decimal(-1.0), RDF.double("-0.0")) == @positive_infinity
assert Numeric.divide(RDF.integer(-1), RDF.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(RDF.decimal(1.0), RDF.decimal(0.0)) == nil
assert Numeric.divide(RDF.decimal(1.0), RDF.integer(0)) == nil
assert Numeric.divide(RDF.decimal(-1.0), RDF.decimal(0.0)) == nil
assert Numeric.divide(RDF.decimal(-1.0), RDF.integer(0)) == nil
assert Numeric.divide(RDF.integer(1), RDF.integer(0)) == nil
assert Numeric.divide(RDF.integer(1), RDF.decimal(0.0)) == nil
assert Numeric.divide(RDF.integer(-1), RDF.integer(0)) == nil
assert Numeric.divide(RDF.integer(-1), RDF.decimal(0.0)) == nil
end
test "positive or negative zero divided by positive or negative zero returns NaN" do
assert Numeric.divide(RDF.double( "-0.0"), RDF.double(0.0)) == @nan
assert Numeric.divide(RDF.double( "-0.0"), RDF.decimal(0.0)) == @nan
assert Numeric.divide(RDF.double( "-0.0"), RDF.integer(0)) == @nan
assert Numeric.divide(RDF.decimal("-0.0"), RDF.double(0.0)) == @nan
assert Numeric.divide(RDF.integer("-0"), RDF.double(0.0)) == @nan
assert Numeric.divide(RDF.double( "0.0"), RDF.double(0.0)) == @nan
assert Numeric.divide(RDF.double( "0.0"), RDF.decimal(0.0)) == @nan
assert Numeric.divide(RDF.double( "0.0"), RDF.integer(0)) == @nan
assert Numeric.divide(RDF.decimal("0.0"), RDF.double(0.0)) == @nan
assert Numeric.divide(RDF.integer("0"), RDF.double(0.0)) == @nan
assert Numeric.divide(RDF.double(0.0) , RDF.double( "-0.0")) == @nan
assert Numeric.divide(RDF.decimal(0.0), RDF.double( "-0.0")) == @nan
assert Numeric.divide(RDF.integer(0) , RDF.double( "-0.0")) == @nan
assert Numeric.divide(RDF.double(0.0) , RDF.decimal("-0.0")) == @nan
assert Numeric.divide(RDF.double(0.0) , RDF.integer("-0")) == @nan
assert Numeric.divide(RDF.double(0.0) , RDF.double( "0.0")) == @nan
assert Numeric.divide(RDF.decimal(0.0), RDF.double( "0.0")) == @nan
assert Numeric.divide(RDF.integer(0) , RDF.double( "0.0")) == @nan
assert Numeric.divide(RDF.double(0.0) , RDF.decimal("0.0")) == @nan
assert Numeric.divide(RDF.double(0.0) , RDF.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?
end
end