|
@@ -0,0 +1,210 @@
|
|
|
|
|
+%%--------------------------------------------------------------------
|
|
|
|
|
+%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
|
|
|
+%%
|
|
|
|
|
+%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
+%% you may not use this file except in compliance with the License.
|
|
|
|
|
+%% You may obtain a copy of the License at
|
|
|
|
|
+%%
|
|
|
|
|
+%% http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
+%%
|
|
|
|
|
+%% Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
+%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
+%% See the License for the specific language governing permissions and
|
|
|
|
|
+%% limitations under the License.
|
|
|
|
|
+%%--------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+-module(emqx_rule_date).
|
|
|
|
|
+
|
|
|
|
|
+-export([date/3, date/4, parse_date/4]).
|
|
|
|
|
+
|
|
|
|
|
+-export([ is_int_char/1
|
|
|
|
|
+ , is_symbol_char/1
|
|
|
|
|
+ , is_m_char/1
|
|
|
|
|
+ ]).
|
|
|
|
|
+
|
|
|
|
|
+-record(result, {
|
|
|
|
|
+ year = "1970" :: string() %%year()
|
|
|
|
|
+ , month = "1" :: string() %%month()
|
|
|
|
|
+ , day = "1" :: string() %%day()
|
|
|
|
|
+ , hour = "0" :: string() %%hour()
|
|
|
|
|
+ , minute = "0" :: string() %%minute() %% epoch in millisecond precision
|
|
|
|
|
+ , second = "0" :: string() %%second() %% epoch in millisecond precision
|
|
|
|
|
+ , zone = "+00:00" :: string() %%integer() %% zone maybe some value
|
|
|
|
|
+}).
|
|
|
|
|
+
|
|
|
|
|
+%% -type time_unit() :: 'microsecond'
|
|
|
|
|
+%% | 'millisecond'
|
|
|
|
|
+%% | 'nanosecond'
|
|
|
|
|
+%% | 'second'.
|
|
|
|
|
+%% -type offset() :: [byte()] | (Time :: integer()).
|
|
|
|
|
+date(TimeUnit, Offset, FormatString) ->
|
|
|
|
|
+ date(TimeUnit, Offset, FormatString, erlang:system_time(TimeUnit)).
|
|
|
|
|
+
|
|
|
|
|
+date(TimeUnit, Offset, FormatString, TimeEpoch) ->
|
|
|
|
|
+ [Head|Other] = string:split(FormatString, "%", all),
|
|
|
|
|
+ R = create_tag([{st, Head}], Other),
|
|
|
|
|
+ Res = lists:map(fun(Expr) ->
|
|
|
|
|
+ eval_tag(rmap(make_time(TimeUnit, Offset, TimeEpoch)), Expr) end, R),
|
|
|
|
|
+ lists:concat(Res).
|
|
|
|
|
+
|
|
|
|
|
+parse_date(TimeUnit, Offset, FormatString, InputString) ->
|
|
|
|
|
+ [Head|Other] = string:split(FormatString, "%", all),
|
|
|
|
|
+ R = create_tag([{st, Head}], Other),
|
|
|
|
|
+ IsZ = fun(V) -> case V of
|
|
|
|
|
+ {tag, $Z} -> true;
|
|
|
|
|
+ _ -> false
|
|
|
|
|
+ end end,
|
|
|
|
|
+ R1 = lists:filter(IsZ, R),
|
|
|
|
|
+ IfFun = fun(Con, A, B) ->
|
|
|
|
|
+ case Con of
|
|
|
|
|
+ [] -> A;
|
|
|
|
|
+ _ -> B
|
|
|
|
|
+ end end,
|
|
|
|
|
+ Res = parse_input(FormatString, InputString),
|
|
|
|
|
+ Str = Res#result.year ++ "-"
|
|
|
|
|
+ ++ Res#result.month ++ "-"
|
|
|
|
|
+ ++ Res#result.day ++ "T"
|
|
|
|
|
+ ++ Res#result.hour ++ ":"
|
|
|
|
|
+ ++ Res#result.minute ++ ":"
|
|
|
|
|
+ ++ Res#result.second ++
|
|
|
|
|
+ IfFun(R1, Offset, Res#result.zone),
|
|
|
|
|
+ calendar:rfc3339_to_system_time(Str, [{unit, TimeUnit}]).
|
|
|
|
|
+
|
|
|
|
|
+mlist(R)->
|
|
|
|
|
+ [ {$H, R#result.hour} %% %H Shows hour in 24-hour format [15]
|
|
|
|
|
+ , {$M, R#result.minute} %% %M Displays minutes [00-59]
|
|
|
|
|
+ , {$S, R#result.second} %% %S Displays seconds [00-59]
|
|
|
|
|
+ , {$y, R#result.year} %% %y Displays year YYYY [2021]
|
|
|
|
|
+ , {$m, R#result.month} %% %m Displays the number of the month [01-12]
|
|
|
|
|
+ , {$d, R#result.day} %% %d Displays the number of the month [01-12]
|
|
|
|
|
+ , {$Z, R#result.zone} %% %Z Displays Time zone
|
|
|
|
|
+ ].
|
|
|
|
|
+
|
|
|
|
|
+rmap(Result) ->
|
|
|
|
|
+ maps:from_list(mlist(Result)).
|
|
|
|
|
+
|
|
|
|
|
+support_char() -> "HMSymdZ".
|
|
|
|
|
+
|
|
|
|
|
+create_tag(Head, []) ->
|
|
|
|
|
+ Head;
|
|
|
|
|
+create_tag(Head, [Val1|RVal]) ->
|
|
|
|
|
+ case Val1 of
|
|
|
|
|
+ [] -> create_tag(Head ++ [{st, [$%]}], RVal);
|
|
|
|
|
+ [H| Other] ->
|
|
|
|
|
+ case lists:member(H, support_char()) of
|
|
|
|
|
+ true -> create_tag(Head ++ [{tag, H}, {st, Other}], RVal);
|
|
|
|
|
+ false -> create_tag(Head ++ [{st, [$%|Val1]}], RVal)
|
|
|
|
|
+ end
|
|
|
|
|
+ end.
|
|
|
|
|
+
|
|
|
|
|
+eval_tag(_,{st, Str}) ->
|
|
|
|
|
+ Str;
|
|
|
|
|
+eval_tag(Map,{tag, Char}) ->
|
|
|
|
|
+ maps:get(Char, Map, "undefined").
|
|
|
|
|
+
|
|
|
|
|
+%% make_time(TimeUnit, Offset) ->
|
|
|
|
|
+%% make_time(TimeUnit, Offset, erlang:system_time(TimeUnit)).
|
|
|
|
|
+make_time(TimeUnit, Offset, TimeEpoch) ->
|
|
|
|
|
+ Res = calendar:system_time_to_rfc3339(TimeEpoch,
|
|
|
|
|
+ [{unit, TimeUnit}, {offset, Offset}]),
|
|
|
|
|
+ [Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, _T,
|
|
|
|
|
+ H1, H2, $:, Min1, Min2, $:, S1, S2 | TimeStr] = Res,
|
|
|
|
|
+ IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
|
|
|
|
|
+ {FractionStr, UtcOffset} = lists:splitwith(IsFractionChar, TimeStr),
|
|
|
|
|
+ #result{
|
|
|
|
|
+ year = [Y1, Y2, Y3, Y4]
|
|
|
|
|
+ , month = [Mon1, Mon2]
|
|
|
|
|
+ , day = [D1, D2]
|
|
|
|
|
+ , hour = [H1, H2]
|
|
|
|
|
+ , minute = [Min1, Min2]
|
|
|
|
|
+ , second = [S1, S2] ++ FractionStr
|
|
|
|
|
+ , zone = UtcOffset
|
|
|
|
|
+ }.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+is_int_char(C) ->
|
|
|
|
|
+ C >= $0 andalso C =< $9 .
|
|
|
|
|
+is_symbol_char(C) ->
|
|
|
|
|
+ C =:= $- orelse C =:= $+ .
|
|
|
|
|
+is_m_char(C) ->
|
|
|
|
|
+ C =:= $:.
|
|
|
|
|
+
|
|
|
|
|
+parse_char_with_fun(_, []) -> error(null_input);
|
|
|
|
|
+parse_char_with_fun(ValidFun, [C|Other]) ->
|
|
|
|
|
+ Res = case erlang:is_function(ValidFun) of
|
|
|
|
|
+ true -> ValidFun(C);
|
|
|
|
|
+ false -> erlang:apply(emqx_rule_date, ValidFun, [C])
|
|
|
|
|
+ end,
|
|
|
|
|
+ case Res of
|
|
|
|
|
+ true -> {C, Other};
|
|
|
|
|
+ false -> error({unexpected,[C|Other]})
|
|
|
|
|
+ end.
|
|
|
|
|
+parse_string([], Input) -> {[], Input};
|
|
|
|
|
+parse_string([C|Other], Input) ->
|
|
|
|
|
+ {C1, Input1} = parse_char_with_fun(fun(V) -> V =:= C end, Input),
|
|
|
|
|
+ {Res, Input2} = parse_string(Other, Input1),
|
|
|
|
|
+ {[C1|Res], Input2}.
|
|
|
|
|
+
|
|
|
|
|
+parse_times(0, _, Input) -> {[], Input};
|
|
|
|
|
+parse_times(Times, Fun, Input) ->
|
|
|
|
|
+ {C1, Input1} = parse_char_with_fun(Fun, Input),
|
|
|
|
|
+ {Res, Input2} = parse_times((Times - 1), Fun, Input1),
|
|
|
|
|
+ {[C1|Res], Input2}.
|
|
|
|
|
+
|
|
|
|
|
+parse_int_times(Times, Input) ->
|
|
|
|
|
+ parse_times(Times, is_int_char, Input).
|
|
|
|
|
+
|
|
|
|
|
+parse_fraction(Input) ->
|
|
|
|
|
+ IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
|
|
|
|
|
+ lists:splitwith(IsFractionChar, Input).
|
|
|
|
|
+
|
|
|
|
|
+parse_second(Input) ->
|
|
|
|
|
+ {M, Input1} = parse_int_times(2, Input),
|
|
|
|
|
+ {M1, Input2} = parse_fraction(Input1),
|
|
|
|
|
+ {M++M1, Input2}.
|
|
|
|
|
+
|
|
|
|
|
+parse_zone(Input) ->
|
|
|
|
|
+ {S, Input1} = parse_char_with_fun(is_symbol_char, Input),
|
|
|
|
|
+ {M, Input2} = parse_int_times(2, Input1),
|
|
|
|
|
+ {C, Input3} = parse_char_with_fun(is_m_char, Input2),
|
|
|
|
|
+ {V, Input4} = parse_int_times(2, Input3),
|
|
|
|
|
+ {[S|M++[C|V]], Input4}.
|
|
|
|
|
+
|
|
|
|
|
+mlist1()->
|
|
|
|
|
+ maps:from_list(
|
|
|
|
|
+ [ {$H, fun(Input) -> parse_int_times(2, Input) end} %% %H Shows hour in 24-hour format [15]
|
|
|
|
|
+ , {$M, fun(Input) -> parse_int_times(2, Input) end} %% %M Displays minutes [00-59]
|
|
|
|
|
+ , {$S, fun(Input) -> parse_second(Input) end} %% %S Displays seconds [00-59]
|
|
|
|
|
+ , {$y, fun(Input) -> parse_int_times(4, Input) end} %% %y Displays year YYYY [2021]
|
|
|
|
|
+ , {$m, fun(Input) -> parse_int_times(2, Input) end} %% %m Displays the number of the month [01-12]
|
|
|
|
|
+ , {$d, fun(Input) -> parse_int_times(2, Input) end} %% %d Displays the number of the month [01-12]
|
|
|
|
|
+ , {$Z, fun(Input) -> parse_zone(Input) end} %% %Z Displays Time zone
|
|
|
|
|
+ ]).
|
|
|
|
|
+
|
|
|
|
|
+update_result($H, Res, Str) -> Res#result{hour=Str};
|
|
|
|
|
+update_result($M, Res, Str) -> Res#result{minute=Str};
|
|
|
|
|
+update_result($S, Res, Str) -> Res#result{second=Str};
|
|
|
|
|
+update_result($y, Res, Str) -> Res#result{year=Str};
|
|
|
|
|
+update_result($m, Res, Str) -> Res#result{month=Str};
|
|
|
|
|
+update_result($d, Res, Str) -> Res#result{day=Str};
|
|
|
|
|
+update_result($Z, Res, Str) -> Res#result{zone=Str}.
|
|
|
|
|
+
|
|
|
|
|
+parse_tag(Res, {st, St}, InputString) ->
|
|
|
|
|
+ {_A, B} = parse_string(St, InputString),
|
|
|
|
|
+ {Res, B};
|
|
|
|
|
+parse_tag(Res, {tag, St}, InputString) ->
|
|
|
|
|
+ Fun = maps:get(St, mlist1()),
|
|
|
|
|
+ {A, B} = Fun(InputString),
|
|
|
|
|
+ NRes = update_result(St, Res, A),
|
|
|
|
|
+ {NRes, B}.
|
|
|
|
|
+
|
|
|
|
|
+parse_tags(Res, [], _) -> Res;
|
|
|
|
|
+parse_tags(Res, [Tag|Others], InputString) ->
|
|
|
|
|
+ {NRes, B} = parse_tag(Res, Tag, InputString),
|
|
|
|
|
+ parse_tags(NRes, Others, B).
|
|
|
|
|
+
|
|
|
|
|
+parse_input(FormatString, InputString) ->
|
|
|
|
|
+ [Head|Other] = string:split(FormatString, "%", all),
|
|
|
|
|
+ R = create_tag([{st, Head}], Other),
|
|
|
|
|
+ parse_tags(#result{}, R, InputString).
|