core: RDF.Time datatype

This commit is contained in:
Marcel Otto 2017-05-01 16:19:03 +02:00
parent 4105a3e59b
commit 354ead9d80
4 changed files with 141 additions and 5 deletions

View file

@ -40,9 +40,8 @@ defmodule RDF.Datatype do
XSD.integer => RDF.Integer,
XSD.double => RDF.Double,
XSD.boolean => RDF.Boolean,
# TODO:
# XSD.date => RDF.Date,
# XSD.time => RDF.Time,
XSD.date => RDF.Date,
XSD.time => RDF.Time,
XSD.dateTime => RDF.DateTime,
}

75
lib/rdf/datatypes/time.ex Normal file
View file

@ -0,0 +1,75 @@
defmodule RDF.Time do
use RDF.Datatype, id: RDF.Datatype.NS.XSD.time
@grammar ~r/\A(\d{2}:\d{2}:\d{2}(?:\.\d+)?)((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z/
@tz_grammar ~r/\A(?:([\+\-])(\d{2}):(\d{2}))\Z/
def convert(%Time{} = value, %{tz: tz} = opts) do
{convert(value, Map.delete(opts, :tz)), tz}
end
def convert(%Time{} = value, _opts) do
value |> strip_microseconds
end
def convert(value, opts) when is_binary(value) do
case Regex.run(@grammar, value) do
[_, time] ->
time
|> do_convert
|> convert(opts)
[_, time, zone] ->
time
|> do_convert
|> with_offset(zone)
|> convert(Map.put(opts, :tz, true))
_ ->
super(value, opts)
end
end
def convert(value, opts), do: super(value, opts)
defp do_convert(value) do
case Time.from_iso8601(value) do
{:ok, time} -> time
_ -> nil
end
end
defp with_offset(time, zone) when zone in ~W[Z UTC GMT], do: time
defp with_offset(time, offset) do
{hour, minute} =
case Regex.run(@tz_grammar, offset) do
[_, "-", hour, minute] ->
{hour, minute} = {String.to_integer(hour), String.to_integer(minute)}
minute = time.minute + minute
{rem(time.hour + hour + div(minute, 60), 24), rem(minute, 60)}
[_, "+", hour, minute] ->
{hour, minute} = {String.to_integer(hour), String.to_integer(minute)}
if (minute = time.minute - minute) < 0 do
{rem(24 + time.hour - hour - 1, 24), minute + 60}
else
{time.hour - hour - div(minute, 60), rem(minute, 60)}
end
end
%Time{time | hour: hour, minute: minute}
end
# microseconds are not part of the xsd:dateTime value space
defp strip_microseconds(%{microsecond: ms} = date_time) when ms != {0, 0},
do: %{date_time | microsecond: {0, 0}}
defp strip_microseconds(date_time),
do: date_time
def canonical_lexical(%Time{} = value) do
Time.to_iso8601(value)
end
def canonical_lexical({%Time{} = value, true}) do
canonical_lexical(value) <> "Z"
end
end

View file

@ -48,9 +48,8 @@ defmodule RDF.Literal do
# TODO:
def new(%Date{} = date), do: %RDF.Literal{value: date, datatype: XSD.date}
def new(%Time{} = time), do: %RDF.Literal{value: time, datatype: XSD.time}
# def new(%Date{} = value), do: RDF.Date.new(value)
# def new(%Time{} = value), do: RDF.Time.new(value)
def new(%Time{} = value), do: RDF.Time.new(value)
def new(%DateTime{} = value), do: RDF.DateTime.new(value)
def new(%NaiveDateTime{} = value), do: RDF.DateTime.new(value)

View file

@ -0,0 +1,63 @@
defmodule RDF.TimeTest do
use RDF.Datatype.Test.Case, datatype: RDF.Time, id: RDF.NS.XSD.time,
valid: %{
# input => { value , lexical , canonicalized }
~T[00:00:00] => { ~T[00:00:00] , nil , "00:00:00" },
"00:00:00" => { ~T[00:00:00] , nil , "00:00:00" },
"00:00:00Z" => { {~T[00:00:00], true } , nil , "00:00:00Z" },
"00:00:00.0000Z" => { {~T[00:00:00], true } , "00:00:00.0000Z" , "00:00:00Z" },
"00:00:00+00:00" => { {~T[00:00:00], true } , "00:00:00+00:00" , "00:00:00Z" },
"01:00:00+01:00" => { {~T[00:00:00], true } , "01:00:00+01:00" , "00:00:00Z" },
"23:00:00-01:00" => { {~T[00:00:00], true } , "23:00:00-01:00" , "00:00:00Z" },
},
invalid: ~w(
foo
+2010-01-01Z
2010-01-01TFOO
02010-01-01
2010-1-1
0000-01-01
2011-07
2011
) ++ [true, false, 2010, 3.14, "00:00:00Z foo", "foo 00:00:00Z"]
test "conversion with time zones" do
[
{ "01:00:00+01:00", ~T[00:00:00] },
{ "01:00:00-01:00", ~T[02:00:00] },
{ "01:00:00-00:01", ~T[01:01:00] },
{ "01:00:00+00:01", ~T[00:59:00] },
{ "00:00:00+01:30", ~T[22:30:00] },
{ "23:00:00-02:30", ~T[01:30:00] },
]
|> Enum.each(fn {input, output} ->
assert RDF.Time.convert(input, %{}) == {output, true}
end)
end
describe "equality" do
test "two literals are equal when they have the same datatype and lexical form" do
[
{ ~T[00:00:00] , "00:00:00" },
]
|> Enum.each(fn {l, r} ->
assert Time.new(l) == Time.new(r)
end)
end
test "two literals with same value but different lexical form are not equal" do
[
{ ~T[00:00:00] , "00:00:00Z" },
{ "00:00:00" , "00:00:00Z" },
{ "00:00:00.0000" , "00:00:00Z" },
{ "00:00:00.0000Z" , "00:00:00Z" },
{ "00:00:00+00:00" , "00:00:00Z" },
]
|> Enum.each(fn {l, r} ->
assert Time.new(l) != Time.new(r)
end)
end
end
end