core: RDF.Time datatype
This commit is contained in:
parent
4105a3e59b
commit
354ead9d80
4 changed files with 141 additions and 5 deletions
|
@ -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
75
lib/rdf/datatypes/time.ex
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
63
test/unit/datatypes/time_test.exs
Normal file
63
test/unit/datatypes/time_test.exs
Normal 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
|
Loading…
Reference in a new issue