diff --git a/CHANGELOG.md b/CHANGELOG.md index 458b58d..e926f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ This project adheres to [Semantic Versioning](http://semver.org/) and - top-level constant functions `RDF.true` and `RDF.false` for the two boolean RDF.Literal values - `RDF.Numeric` with a list of all numeric datatypes +- the logical operators and the Effective Boolean Value (EBV) coercion algorithm + from the XPath and SPARQL specs on `RDF.Boolean` [Compare v0.4.1...HEAD](https://github.com/marcelotto/rdf-ex/compare/v0.4.1...HEAD) diff --git a/lib/rdf/datatypes/boolean.ex b/lib/rdf/datatypes/boolean.ex index 82b6b59..6c7ff0e 100644 --- a/lib/rdf/datatypes/boolean.ex +++ b/lib/rdf/datatypes/boolean.ex @@ -25,6 +25,126 @@ defmodule RDF.Boolean do def convert(value, opts), do: super(value, opts) + @doc """ + Returns `RDF.true` if the effective boolean value of the given argument is `RDF.false`, or `RDF.false` if it is `RDF.true`. + + Otherwise it returns `nil`. + + ## Examples + + iex> RDF.Boolean.fn_not(RDF.true) + RDF.false + iex> RDF.Boolean.fn_not(RDF.false) + RDF.true + + iex> RDF.Boolean.fn_not(true) + RDF.false + iex> RDF.Boolean.fn_not(false) + RDF.true + + iex> RDF.Boolean.fn_not(42) + RDF.false + iex> RDF.Boolean.fn_not("") + RDF.true + + iex> RDF.Boolean.fn_not(nil) + nil + + see + """ + def fn_not(value) do + case ebv(value) do + %RDF.Literal{value: true} -> RDF.Boolean.Value.false + %RDF.Literal{value: false} -> RDF.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.true` and `RDF.false` if the other argument is `RDF.false`. + + ## Examples + + iex> RDF.Boolean.logical_and(RDF.true, RDF.true) + RDF.true + iex> RDF.Boolean.logical_and(RDF.true, RDF.false) + RDF.false + + iex> RDF.Boolean.logical_and(RDF.true, nil) + nil + iex> RDF.Boolean.logical_and(nil, RDF.false) + RDF.false + iex> RDF.Boolean.logical_and(nil, nil) + nil + + see + + """ + def logical_and(left, right) do + case ebv(left) do + %RDF.Literal{value: false} -> + RDF.false + + %RDF.Literal{value: true} -> + case ebv(right) do + %RDF.Literal{value: true} -> RDF.true + %RDF.Literal{value: false} -> RDF.false + nil -> nil + end + + nil -> + if match?(%RDF.Literal{value: false}, ebv(right)) do + RDF.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.false` and `RDF.true` if the other argument is `RDF.true`. + + ## Examples + + iex> RDF.Boolean.logical_or(RDF.true, RDF.false) + RDF.true + iex> RDF.Boolean.logical_or(RDF.false, RDF.false) + RDF.false + + iex> RDF.Boolean.logical_or(RDF.true, nil) + RDF.true + iex> RDF.Boolean.logical_or(nil, RDF.false) + nil + iex> RDF.Boolean.logical_or(nil, nil) + nil + + see + + """ + def logical_or(left, right) do + case ebv(left) do + %RDF.Literal{value: true} -> + RDF.true + + %RDF.Literal{value: false} -> + case ebv(right) do + %RDF.Literal{value: true} -> RDF.true + %RDF.Literal{value: false} -> RDF.false + nil -> nil + end + + nil -> + if match?(%RDF.Literal{value: true}, ebv(right)) do + RDF.true + end + end + end + + @xsd_boolean RDF.Datatype.NS.XSD.boolean @doc """ @@ -39,6 +159,7 @@ defmodule RDF.Boolean do see - - + """ def ebv(value) diff --git a/test/unit/datatypes/boolean_test.exs b/test/unit/datatypes/boolean_test.exs index 72529b0..7142a9c 100644 --- a/test/unit/datatypes/boolean_test.exs +++ b/test/unit/datatypes/boolean_test.exs @@ -134,4 +134,40 @@ defmodule RDF.BooleanTest do end end + test "truth-table of logical_and" do + [ + {RDF.true, RDF.true, RDF.true}, + {RDF.true, RDF.false, RDF.false}, + {RDF.false, RDF.true, RDF.false}, + {RDF.false, RDF.false, RDF.false}, + {RDF.true, nil, nil}, + {nil, RDF.true, nil}, + {RDF.false, nil, RDF.false}, + {nil, RDF.false, RDF.false}, + {nil, nil, nil}, + ] + |> Enum.each(fn {left, right, result} -> + assert RDF.Boolean.logical_and(left, right) == result, + "expected logical_and(#{inspect left}, #{inspect right}) to be #{inspect result}, but got #{inspect RDF.Boolean.logical_and(left, right)}" + end) + end + + test "truth-table of logical_or" do + [ + {RDF.true, RDF.true, RDF.true}, + {RDF.true, RDF.false, RDF.true}, + {RDF.false, RDF.true, RDF.true}, + {RDF.false, RDF.false, RDF.false}, + {RDF.true, nil, RDF.true}, + {nil, RDF.true, RDF.true}, + {RDF.false, nil, nil}, + {nil, RDF.false, nil}, + {nil, nil, nil}, + ] + |> Enum.each(fn {left, right, result} -> + assert RDF.Boolean.logical_or(left, right) == result, + "expected logical_or(#{inspect left}, #{inspect right}) to be #{inspect result}, but got #{inspect RDF.Boolean.logical_and(left, right)}" + end) + end + end