/
time.ex
152 lines (121 loc) · 3.51 KB
/
time.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
defmodule RDF.Time do
@moduledoc """
`RDF.Datatype` for XSD time.
"""
use RDF.Datatype, id: RDF.Datatype.NS.XSD.time
import RDF.Literal.Guards
@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/
@impl RDF.Datatype
def convert(value, opts)
def convert(%Time{} = value, %{tz: tz} = opts) do
{convert(value, Map.delete(opts, :tz)), tz}
end
def convert(%Time{} = value, _opts), do: value
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
@impl RDF.Datatype
def canonical_lexical(value)
def canonical_lexical(%Time{} = value) do
Time.to_iso8601(value)
end
def canonical_lexical({%Time{} = value, true}) do
canonical_lexical(value) <> "Z"
end
@impl RDF.Datatype
def cast(literal)
def cast(%RDF.Literal{datatype: datatype} = literal) do
cond do
not RDF.Literal.valid?(literal) ->
nil
is_xsd_time(datatype) ->
literal
is_xsd_datetime(datatype) ->
case literal.value do
%NaiveDateTime{} = datetime ->
datetime
|> NaiveDateTime.to_time()
|> new()
%DateTime{} ->
[_date, time_with_zone] =
literal
|> RDF.DateTime.canonical_lexical_with_zone()
|> String.split("T", parts: 2)
new(time_with_zone)
end
is_xsd_string(datatype) ->
literal.value
|> new()
true ->
nil
end
end
def cast(_), do: nil
@doc """
Extracts the timezone string from a `RDF.Time` literal.
"""
def tz(time_literal) do
if valid?(time_literal) do
time_literal
|> lexical()
|> RDF.DateTimeUtils.tz()
end
end
@doc """
Converts a time literal to a canonical string, preserving the zone information.
"""
def canonical_lexical_with_zone(%Literal{datatype: datatype} = literal)
when is_xsd_time(datatype) do
case tz(literal) do
nil ->
nil
zone when zone in ["Z", "", "+00:00"] ->
canonical_lexical(literal.value)
zone ->
literal
|> lexical()
|> String.replace_trailing(zone, "")
|> Time.from_iso8601!()
|> canonical_lexical()
|> Kernel.<>(zone)
end
end
end