Add RDF.Decimal datatype
This commit is contained in:
parent
9193dd916f
commit
f5684262e7
9 changed files with 181 additions and 2 deletions
2
.iex.exs
2
.iex.exs
|
@ -20,3 +20,5 @@ alias RDF.{
|
|||
alias RDF.BlankNode, as: BNode
|
||||
|
||||
alias RDF.{NTriples, NQuads, Turtle}
|
||||
|
||||
alias Decimal, as: D
|
||||
|
|
|
@ -12,7 +12,9 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
|
|||
- top-level alias functions for constructors of the basic datatypes
|
||||
- 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
|
||||
- `RDF.Decimal` datatype for `xsd:decimal` literals
|
||||
- `RDF.Numeric` with a list of all numeric datatypes and shared functions for
|
||||
all numeric literals
|
||||
- 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`
|
||||
|
|
|
@ -10,7 +10,6 @@ An implementation of the [RDF](https://www.w3.org/TR/rdf11-primer/) data model i
|
|||
## Features
|
||||
|
||||
- fully compatible with the RDF 1.1 specification
|
||||
- no dependencies
|
||||
- in-memory data structures for RDF descriptions, RDF graphs and RDF datasets
|
||||
- support for RDF vocabularies via Elixir modules for safe, i.e. compile-time checked and concise usage of IRIs
|
||||
- XML schema datatypes for RDF literals (not yet all supported)
|
||||
|
@ -316,6 +315,7 @@ It is also possible to create a typed literal by using a native Elixir non-strin
|
|||
| `Date` | `xsd:date` |
|
||||
| `DateTime` | `xsd:dateTime` |
|
||||
| `NaiveDateTime` | `xsd:dateTime` |
|
||||
| [`Decimal`] | `xsd:decimal` |
|
||||
|
||||
So the former example literal can be created equivalently like this:
|
||||
|
||||
|
@ -759,3 +759,4 @@ see [CONTRIBUTING](CONTRIBUTING.md) for details.
|
|||
[RDF-XML]: https://www.w3.org/TR/rdf-syntax-grammar/
|
||||
[BCP47]: https://tools.ietf.org/html/bcp47
|
||||
[XML schema datatype]: https://www.w3.org/TR/xmlschema11-2/
|
||||
[`Decimal`]: https://github.com/ericmj/decimal
|
||||
|
|
|
@ -135,6 +135,8 @@ defmodule RDF do
|
|||
defdelegate integer(value, opts), to: RDF.Integer, as: :new
|
||||
defdelegate double(value), to: RDF.Double, as: :new
|
||||
defdelegate double(value, opts), to: RDF.Double, as: :new
|
||||
defdelegate decimal(value), to: RDF.Decimal, as: :new
|
||||
defdelegate decimal(value, opts), to: RDF.Decimal, as: :new
|
||||
defdelegate date(value), to: RDF.Date, as: :new
|
||||
defdelegate date(value, opts), to: RDF.Date, as: :new
|
||||
defdelegate time(value), to: RDF.Time, as: :new
|
||||
|
|
|
@ -75,6 +75,7 @@ defmodule RDF.Datatype do
|
|||
XSD.string => RDF.String,
|
||||
XSD.integer => RDF.Integer,
|
||||
XSD.double => RDF.Double,
|
||||
XSD.decimal => RDF.Decimal,
|
||||
XSD.boolean => RDF.Boolean,
|
||||
XSD.date => RDF.Date,
|
||||
XSD.time => RDF.Time,
|
||||
|
|
70
lib/rdf/datatypes/decimal.ex
Normal file
70
lib/rdf/datatypes/decimal.ex
Normal file
|
@ -0,0 +1,70 @@
|
|||
defmodule RDF.Decimal do
|
||||
@moduledoc """
|
||||
`RDF.Datatype` for XSD decimal.
|
||||
"""
|
||||
alias Elixir.Decimal, as: D
|
||||
|
||||
use RDF.Datatype, id: RDF.Datatype.NS.XSD.decimal
|
||||
|
||||
|
||||
def build_literal_by_value(value, opts) when is_integer(value),
|
||||
do: value |> D.new() |> build_literal(to_string(value), opts)
|
||||
|
||||
def build_literal_by_value(value, opts),
|
||||
do: super(value, opts)
|
||||
|
||||
|
||||
def convert(%D{coef: coef} = value, opts) when coef in ~w[qNaN sNaN inf]a,
|
||||
do: super(value, opts)
|
||||
|
||||
def convert(%D{} = decimal, _), do: decimal
|
||||
|
||||
def convert(value, _) when is_float(value), do: D.from_float(value)
|
||||
|
||||
def convert(value, opts) when is_binary(value) do
|
||||
if String.contains?(value, ~w[e E]) do
|
||||
super(value, opts)
|
||||
else
|
||||
case D.parse(value) do
|
||||
{:ok, decimal} -> convert(decimal, opts)
|
||||
:error -> super(value, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert(value, opts), do: super(value, opts)
|
||||
|
||||
|
||||
def canonical_lexical(%D{sign: sign, coef: :qNaN}) do
|
||||
if sign == 1, do: "NaN", else: "-NaN"
|
||||
end
|
||||
|
||||
def canonical_lexical(%D{sign: sign, coef: :sNaN}) do
|
||||
if sign == 1, do: "sNaN", else: "-sNaN"
|
||||
end
|
||||
|
||||
def canonical_lexical(%D{sign: sign, coef: :inf}) do
|
||||
if sign == 1, do: "Infinity", else: "-Infinity"
|
||||
end
|
||||
|
||||
def canonical_lexical(%D{} = decimal) do
|
||||
decimal |> canonical_decimal() |> D.to_string(:normal)
|
||||
end
|
||||
|
||||
defp canonical_decimal(%D{coef: 0} = decimal), do: %{decimal | exp: -1}
|
||||
|
||||
defp canonical_decimal(%D{coef: coef, exp: 0} = decimal),
|
||||
do: %{decimal | coef: coef * 10, exp: -1}
|
||||
|
||||
defp canonical_decimal(%D{coef: coef, exp: exp} = decimal)
|
||||
when exp > 0,
|
||||
do: canonical_decimal(%{decimal | coef: coef * 10, exp: exp - 1})
|
||||
|
||||
defp canonical_decimal(%D{coef: coef} = decimal)
|
||||
when Kernel.rem(coef, 10) != 0,
|
||||
do: decimal
|
||||
|
||||
defp canonical_decimal(%D{coef: coef, exp: exp} = decimal),
|
||||
do: canonical_decimal(%{decimal | coef: Kernel.div(coef, 10), exp: exp + 1})
|
||||
|
||||
end
|
3
mix.exs
3
mix.exs
|
@ -59,11 +59,14 @@ defmodule RDF.Mixfile do
|
|||
|
||||
defp deps do
|
||||
[
|
||||
{:decimal, "~> 1.5"},
|
||||
|
||||
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 0.9", only: [:dev, :test], runtime: false},
|
||||
{:ex_doc, "~> 0.18", only: :dev, runtime: false},
|
||||
{:excoveralls, "~> 0.9", only: :test},
|
||||
{:inch_ex, "~> 0.5", only: [:dev, :test]},
|
||||
|
||||
{:benchee, "~> 0.13", only: :bench},
|
||||
{:erlang_term, "~> 1.7", only: :bench},
|
||||
]
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -3,6 +3,7 @@
|
|||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
|
||||
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, optional: false]}]},
|
||||
"credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, optional: false]}]},
|
||||
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], []},
|
||||
"deep_merge": {:hex, :deep_merge, "0.1.1", "c27866a7524a337b6a039eeb8dd4f17d458fd40fbbcb8c54661b71a22fffe846", [:mix], []},
|
||||
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []},
|
||||
"earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], []},
|
||||
|
|
97
test/unit/datatypes/decimal_test.exs
Normal file
97
test/unit/datatypes/decimal_test.exs
Normal file
|
@ -0,0 +1,97 @@
|
|||
defmodule RDF.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 RDF.Datatype.Test.Case?
|
||||
# alias Elixir.Decimal, as: D
|
||||
use RDF.Datatype.Test.Case, datatype: RDF.Decimal, id: RDF.NS.XSD.decimal,
|
||||
valid: %{
|
||||
# input => { value lexical canonicaized }
|
||||
0 => {Elixir.Decimal.new(0), "0", "0.0"},
|
||||
1 => {Elixir.Decimal.new(1), "1", "1.0"},
|
||||
-1 => {Elixir.Decimal.new(-1), "-1", "-1.0"},
|
||||
1.0 => {Elixir.Decimal.new(1.0), nil, "1.0"},
|
||||
-3.14 => {Elixir.Decimal.new(-3.14), nil, "-3.14"},
|
||||
0.0E2 => {Elixir.Decimal.new(0.0E2), nil, "0.0"},
|
||||
1.2E3 => {Elixir.Decimal.new(1.2E3), nil, "1200.0"},
|
||||
Elixir.Decimal.new(1.0) => {Elixir.Decimal.new(1.0), nil, "1.0"},
|
||||
"1" => {Elixir.Decimal.new(1), "1", "1.0" },
|
||||
"01" => {Elixir.Decimal.new(1), "01", "1.0" },
|
||||
"0123" => {Elixir.Decimal.new(123), "0123", "123.0" },
|
||||
"-1" => {Elixir.Decimal.new(-1), "-1", "-1.0" },
|
||||
"1." => {Elixir.Decimal.new(1), "1.", "1.0" },
|
||||
"1.0" => {Elixir.Decimal.new(1.0), nil, "1.0" },
|
||||
"1.000000000" => {Elixir.Decimal.new("1.000000000"), "1.000000000", "1.0" },
|
||||
"+001.00" => {Elixir.Decimal.new("1.00"), "+001.00", "1.0" },
|
||||
"123.456" => {Elixir.Decimal.new(123.456), nil, "123.456" },
|
||||
"0123.456" => {Elixir.Decimal.new(123.456), "0123.456", "123.456" },
|
||||
"010.020" => {Elixir.Decimal.new("10.020"), "010.020", "10.02" },
|
||||
"2.3" => {Elixir.Decimal.new(2.3), nil, "2.3" },
|
||||
"2.345" => {Elixir.Decimal.new(2.345), nil, "2.345" },
|
||||
"2.234000005" => {Elixir.Decimal.new(2.234000005), nil, "2.234000005" },
|
||||
"1.234567890123456789012345789"
|
||||
=> {Elixir.Decimal.new("1.234567890123456789012345789"),
|
||||
nil, "1.234567890123456789012345789" },
|
||||
".3" => {Elixir.Decimal.new(0.3), ".3", "0.3" },
|
||||
"-.3" => {Elixir.Decimal.new(-0.3), "-.3", "-0.3" },
|
||||
},
|
||||
invalid: ~w(foo 10.1e1 12.xyz 3,5 NaN Inf) ++ [true, false, "1.0 foo", "foo 1.0",
|
||||
Elixir.Decimal.new("NaN"), Elixir.Decimal.new("Inf")]
|
||||
|
||||
|
||||
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 Decimal.new(l) == Decimal.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 Decimal.new(l) != Decimal.new(r)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defmacrop sigil_d(str, _opts) do
|
||||
quote do
|
||||
Elixir.Decimal.new(unquote(str))
|
||||
end
|
||||
end
|
||||
|
||||
test "Decimal.canonical_lexical/1" do
|
||||
assert Decimal.canonical_lexical(~d"0") == "0.0"
|
||||
assert Decimal.canonical_lexical(~d"0.0") == "0.0"
|
||||
assert Decimal.canonical_lexical(~d"0.001") == "0.001"
|
||||
assert Decimal.canonical_lexical(~d"-0") == "-0.0"
|
||||
assert Decimal.canonical_lexical(~d"-1") == "-1.0"
|
||||
assert Decimal.canonical_lexical(~d"-0.00") == "-0.0"
|
||||
assert Decimal.canonical_lexical(~d"1.00") == "1.0"
|
||||
assert Decimal.canonical_lexical(~d"1000") == "1000.0"
|
||||
assert Decimal.canonical_lexical(~d"1000.000000") == "1000.0"
|
||||
assert Decimal.canonical_lexical(~d"12345.000") == "12345.0"
|
||||
assert Decimal.canonical_lexical(~d"42") == "42.0"
|
||||
assert Decimal.canonical_lexical(~d"42.42") == "42.42"
|
||||
assert Decimal.canonical_lexical(~d"0.42") == "0.42"
|
||||
assert Decimal.canonical_lexical(~d"0.0042") == "0.0042"
|
||||
assert Decimal.canonical_lexical(~d"010.020") == "10.02"
|
||||
assert Decimal.canonical_lexical(~d"-1.23") == "-1.23"
|
||||
assert Decimal.canonical_lexical(~d"-0.0123") == "-0.0123"
|
||||
assert Decimal.canonical_lexical(~d"1E+2") == "100.0"
|
||||
assert Decimal.canonical_lexical(~d"-42E+3") == "-42000.0"
|
||||
assert Decimal.canonical_lexical(~d"nan") == "NaN"
|
||||
assert Decimal.canonical_lexical(~d"-nan") == "-NaN"
|
||||
assert Decimal.canonical_lexical(~d"-inf") == "-Infinity"
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in a new issue