Commit | Line | Data |
---|---|---|
299cfa16 SK |
1 | -module(hope_time). |
2 | ||
3 | -export_type( | |
4 | [ t/0 | |
5 | ]). | |
6 | ||
7 | -export( | |
8 | [ now/0 | |
9 | , of_timestamp/1 | |
10 | , to_unix_time/1 | |
11 | , of_iso8601/1 | |
12 | ||
13 | % Floatable | |
14 | , of_float/1 | |
15 | , to_float/1 | |
16 | ||
17 | % TODO: Stringable | |
18 | ]). | |
19 | ||
20 | ||
21 | -define(T, #?MODULE). | |
22 | ||
23 | ||
24 | -record(?MODULE, | |
25 | { unix_time :: float() | |
26 | }). | |
27 | ||
28 | -opaque t() :: | |
29 | ?T{}. | |
30 | ||
31 | ||
32 | -spec now() -> | |
33 | t(). | |
34 | now() -> | |
35 | Timestamp = os:timestamp(), | |
36 | of_timestamp(Timestamp). | |
37 | ||
38 | -spec of_timestamp(erlang:timestamp()) -> | |
39 | t(). | |
40 | of_timestamp({MegasecondsInt, SecondsInt, MicrosecondsInt}) -> | |
41 | Million = 1000000.0, | |
42 | Megaseconds = float(MegasecondsInt), | |
43 | Seconds = float(SecondsInt), | |
44 | Microseconds = float(MicrosecondsInt), | |
45 | UnixTime = (Megaseconds * Million) + Seconds + (Microseconds / Million), | |
46 | ?T{unix_time = UnixTime}. | |
47 | ||
48 | -spec to_unix_time(t()) -> | |
49 | float(). | |
50 | to_unix_time(?T{unix_time=UnixTime}) -> | |
51 | UnixTime. | |
52 | ||
53 | -spec of_float(float()) -> | |
54 | t(). | |
55 | of_float(Float) when is_float(Float) -> | |
56 | ?T{unix_time = Float}. | |
57 | ||
58 | -spec to_float(t()) -> | |
59 | float(). | |
60 | to_float(?T{unix_time=Float}) -> | |
61 | Float. | |
62 | ||
63 | -spec of_iso8601(binary()) -> | |
64 | hope_result:t(t(), {unrecognized_as_iso8601, binary()}). | |
65 | of_iso8601(<<Bin/binary>>) -> | |
66 | % We use regexp rather than just simple binary pattern match, because we | |
67 | % also want to validate character ranges, i.e., that components are | |
68 | % integers. | |
69 | ValidPatterns = | |
70 | [ {zoneless, <<"\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d">>} | |
71 | ], | |
72 | ValidPatternMatchers = | |
73 | [{Tag, make_regexp_bool(RegExp)} || {Tag, RegExp} <- ValidPatterns], | |
74 | case hope_list:first_match(ValidPatternMatchers, Bin) | |
75 | of none -> {error, {unrecognized_as_iso8601, Bin}} | |
76 | ; {some, zoneless} -> {ok, of_iso8601_zoneless(Bin)} | |
77 | end. | |
78 | ||
79 | -spec of_iso8601_zoneless(binary()) -> | |
80 | t(). | |
81 | of_iso8601_zoneless(<<Bin/binary>>) -> | |
82 | << YearBin:4/binary, "-", MonthBin:2/binary, "-", DayBin:2/binary | |
83 | , "T" | |
84 | , HourBin:2/binary, ":", MinBin:2/binary , ":", SecBin:2/binary | |
85 | >> = Bin, | |
86 | Year = binary_to_integer(YearBin), | |
87 | Month = binary_to_integer(MonthBin), | |
88 | Day = binary_to_integer(DayBin), | |
89 | Hour = binary_to_integer(HourBin), | |
90 | Min = binary_to_integer(MinBin), | |
91 | Sec = binary_to_integer(SecBin), | |
92 | DateTime = {{Year, Month, Day}, {Hour, Min, Sec}}, | |
93 | SecondsGregorian = calendar:datetime_to_gregorian_seconds(DateTime), | |
94 | SecondsFromZeroToUnixEpoch = 62167219200, | |
95 | SecondsUnixEpochInt = SecondsGregorian - SecondsFromZeroToUnixEpoch, | |
96 | SecondsUnixEpoch = float(SecondsUnixEpochInt), | |
97 | of_float(SecondsUnixEpoch). | |
98 | ||
99 | -spec make_regexp_bool(binary()) -> | |
100 | fun((binary()) -> boolean()). | |
101 | make_regexp_bool(<<RegExp/binary>>) -> | |
102 | fun (<<String/binary>>) -> | |
103 | case re:run(String, RegExp) | |
104 | of nomatch -> false | |
105 | ; {match, _} -> true | |
106 | end | |
107 | end. |