| 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. |