-
Notifications
You must be signed in to change notification settings - Fork 36
/
time.ex
275 lines (229 loc) · 8.22 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
defmodule Membrane.Time do
@moduledoc """
Module containing functions needed to perform handling of time.
Membrane always internally uses nanosecond as a time unit. This is how all time
units should represented in the code unless there's a good reason to act
differently.
Please note that Erlang VM may internally use different units and that may
differ from platform to platform. Still, unless you need to perform calculations
that do not touch hardware clock, you should use Membrane units for consistency.
"""
@compile {:inline,
native_units: 1, native_unit: 0, nanoseconds: 1, nanosecond: 0, second: 0, seconds: 1}
@type t :: integer
@type non_neg_t :: non_neg_integer
@units [
%{plural: :days, singular: :day, abbrev: "d", duration: 86_400_000_000_000},
%{plural: :hours, singular: :hour, abbrev: "h", duration: 3_600_000_000_000},
%{plural: :minutes, singular: :minute, abbrev: "min", duration: 60_000_000_000},
%{plural: :seconds, singular: :second, abbrev: "s", duration: 1_000_000_000},
%{plural: :milliseconds, singular: :millisecond, abbrev: "ms", duration: 1_000_000},
%{plural: :microseconds, singular: :microsecond, abbrev: "us", duration: 1_000},
%{plural: :nanoseconds, singular: :nanosecond, abbrev: "ns", duration: 1}
]
# Difference between 01.01.1900 (start of NTP epoch) and 01.01.1970 (start of Unix epoch) in seconds
@ntp_unix_epoch_diff 2_208_988_800
@two_to_pow_32 Ratio.pow(2, 32)
@doc """
Checks whether a value is `Membrane.Time.t`.
"""
defguard is_time(value) when is_integer(value)
@doc """
Returns duration as a string with unit. Chosen unit is the biggest possible
that doesn't involve precission loss.
## Examples
iex> import #{inspect(__MODULE__)}
iex> 10 |> milliseconds() |> pretty_duration()
"10 ms"
iex> 60_000_000 |> microseconds() |> pretty_duration()
"1 min"
iex> 2 |> nanoseconds() |> pretty_duration()
"2 ns"
"""
@spec pretty_duration(t) :: String.t()
def pretty_duration(time) when is_time(time) do
{time, unit} = time |> best_unit()
"#{time} #{unit.abbrev}"
end
@doc """
Returns quoted code producing given amount time. Chosen unit is the biggest possible
that doesn't involve precission loss.
## Examples
iex> import #{inspect(__MODULE__)}
iex> 10 |> milliseconds() |> to_code() |> Macro.to_string()
quote do 10 |> #{inspect(__MODULE__)}.milliseconds() end |> Macro.to_string()
iex> 60_000_000 |> microseconds() |> to_code() |> Macro.to_string()
quote do #{inspect(__MODULE__)}.minute() end |> Macro.to_string()
iex> 2 |> nanoseconds() |> to_code() |> Macro.to_string()
quote do 2 |> #{inspect(__MODULE__)}.nanoseconds() end |> Macro.to_string()
"""
@spec to_code(t) :: Macro.t()
def to_code(time) when is_time(time) do
case best_unit(time) do
{1, unit} ->
quote do
unquote(__MODULE__).unquote(unit.singular)()
end
{time, unit} ->
quote do
unquote(time) |> unquote(__MODULE__).unquote(unit.plural)()
end
end
end
@doc """
Returns string representation of result of `to_code/1`.
"""
@spec to_code_str(t) :: Macro.t()
def to_code_str(time) when is_time(time) do
time |> to_code() |> Macro.to_string()
end
@doc """
Returns current time in pretty format (currently iso8601), as string
Uses `os_time/0` under the hood.
"""
@spec pretty_now :: String.t()
def pretty_now do
os_time() |> to_iso8601()
end
@doc """
Returns current monotonic time based on `System.monotonic_time/0`
in `Membrane.Time` units.
"""
@spec monotonic_time() :: t
def monotonic_time do
System.monotonic_time() |> native_units
end
@doc """
Returns current POSIX time of operating system based on `System.os_time/0`
in `Membrane.Time` units.
This time is not monotonic.
"""
@spec os_time() :: t
def os_time() do
System.os_time() |> native_units
end
@doc """
Returns current Erlang VM system time based on `System.system_time/0`
in `Membrane.Time` units.
It is the VM view of the `os_time/0`. They may not match in case of time warps.
It is not monotonic.
"""
@spec vm_time() :: t
def vm_time() do
System.system_time() |> native_units
end
@doc """
Converts iso8601 string to `Membrane.Time` units.
If `value` is invalid, throws match error.
"""
@spec from_iso8601!(String.t()) :: t
def from_iso8601!(value) when is_binary(value) do
{:ok, datetime, _shift} = value |> DateTime.from_iso8601()
datetime |> from_datetime
end
@doc """
Returns time as a iso8601 string.
"""
@spec to_iso8601(t) :: String.t()
def to_iso8601(value) when is_time(value) do
value |> to_datetime |> DateTime.to_iso8601()
end
@doc """
Converts `DateTime` to `Membrane.Time` units.
"""
@spec from_datetime(DateTime.t()) :: t
def from_datetime(%DateTime{} = value) do
value |> DateTime.to_unix(:nanosecond) |> nanoseconds
end
@doc """
Returns time as a `DateTime` struct. TimeZone is set to UTC.
"""
@spec to_datetime(t) :: DateTime.t()
def to_datetime(value) when is_time(value) do
DateTime.from_unix!(value |> nanoseconds, :nanosecond)
end
@doc """
Converts NTP timestamp (time since 0h on 1st Jan 1900) into Unix timestamp
(time since 1st Jan 1970) represented in `Membrane.Time` units.
NTP timestamp uses fixed point representation with the integer part in the first 32 bits
and the fractional part in the last 32 bits.
"""
@spec from_ntp_timestamp(ntp_time :: <<_::64>>) :: t()
def from_ntp_timestamp(<<ntp_seconds::32, ntp_fraction::32>>) do
fractional = (ntp_fraction * second()) |> div(@two_to_pow_32)
unix_seconds = (ntp_seconds - @ntp_unix_epoch_diff) |> seconds()
unix_seconds + fractional
end
@doc """
Converts the timestamp into NTP timestamp. May introduce small rounding errors.
"""
@spec to_ntp_timestamp(timestamp :: t()) :: <<_::64>>
def to_ntp_timestamp(timestamp) do
seconds = timestamp |> div(second())
ntp_seconds = seconds + @ntp_unix_epoch_diff
fractional = rem(timestamp, second())
ntp_fractional = (fractional * @two_to_pow_32) |> div(second())
<<ntp_seconds::32, ntp_fractional::32>>
end
@doc """
Returns one VM native unit in `Membrane.Time` units.
"""
@spec native_unit() :: t
def native_unit() do
native_units(1)
end
@doc """
Returns given amount of VM native units in `Membrane.Time` units.
"""
@spec native_units(integer) :: t
def native_units(number) when is_integer(number) do
number |> System.convert_time_unit(:native, :nanosecond) |> nanoseconds
end
@doc """
Returns time in VM native units. Rounded using Kernel.round/1.
"""
@spec to_native_units(t) :: integer
def to_native_units(value) when is_time(value) do
round(value / native_unit())
end
Enum.map(@units, fn unit ->
@doc """
Returns one #{unit.singular} in `#{inspect(__MODULE__)}` units.
"""
@spec unquote(unit.singular)() :: t
# credo:disable-for-next-line Credo.Check.Readability.Specs
def unquote(unit.singular)() do
unquote(unit.duration)
end
@doc """
Returns given amount of #{unit.plural} in `#{inspect(__MODULE__)}` units.
"""
@spec unquote(unit.plural)(integer) :: t
# credo:disable-for-next-line Credo.Check.Readability.Specs
def unquote(unit.plural)(number) when is_integer(number) do
number * unquote(unit.duration)
end
to_fun_name = :"to_#{unit.plural}"
@doc """
Returns time in #{unit.plural}. Rounded using `Kernel.round/1`.
"""
@spec unquote(to_fun_name)(t) :: integer
# credo:disable-for-next-line Credo.Check.Readability.Specs
def unquote(to_fun_name)(time) when is_time(time) do
round(time / unquote(unit.duration))
end
as_fun_name = :"as_#{unit.plural}"
@doc """
Returns time in #{unit.plural}, represented as a rational number.
"""
@spec unquote(as_fun_name)(t) :: integer | Ratio.t()
# credo:disable-for-next-line Credo.Check.Readability.Specs
def unquote(as_fun_name)(time) when is_time(time) do
Ratio./(time, unquote(unit.duration))
end
end)
defp best_unit(time) do
unit = @units |> Enum.find(&(rem(time, &1.duration) == 0))
{time |> div(unit.duration), unit}
end
end