Merge pull request #7631 from EMQ-YangM/add_time_funcs

feat: add rule-engine functions
This commit is contained in:
Yang Miao 2022-04-18 09:21:30 +08:00 committed by GitHub
commit 168976c357
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 252 additions and 1 deletions

View File

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

View File

@ -190,6 +190,9 @@
, rfc3339_to_unix_ts/2
, now_timestamp/0
, now_timestamp/1
, format_date/3
, format_date/4
, date_to_unix_ts/4
]).
%% Proc Dict Func
@ -880,6 +883,25 @@ time_unit(<<"millisecond">>) -> millisecond;
time_unit(<<"microsecond">>) -> microsecond;
time_unit(<<"nanosecond">>) -> nanosecond.
format_date(TimeUnit, Offset, FormatString) ->
emqx_plugin_libs_rule:bin(
emqx_rule_date:date(time_unit(TimeUnit),
emqx_plugin_libs_rule:str(Offset),
emqx_plugin_libs_rule:str(FormatString))).
format_date(TimeUnit, Offset, FormatString, TimeEpoch) ->
emqx_plugin_libs_rule:bin(
emqx_rule_date:date(time_unit(TimeUnit),
emqx_plugin_libs_rule:str(Offset),
emqx_plugin_libs_rule:str(FormatString),
TimeEpoch)).
date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) ->
emqx_rule_date:parse_date(time_unit(TimeUnit),
emqx_plugin_libs_rule:str(Offset),
emqx_plugin_libs_rule:str(FormatString),
emqx_plugin_libs_rule:str(InputString)).
%% @doc This is for sql funcs that should be handled in the specific modules.
%% Here the emqx_rule_funcs module acts as a proxy, forwarding
%% the function handling to the worker module.

View File

@ -664,6 +664,26 @@ t_rfc3339_to_unix_ts(_) ->
?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit))
end || Unit <- [second,millisecond,microsecond,nanosecond]].
t_format_date_funcs(_) ->
?PROPTEST(prop_format_date_fun).
prop_format_date_fun() ->
Args1 = [<<"second">>, <<"+07:00">>, <<"%m--%d--%y---%H:%M:%S%Z">>],
?FORALL(S, erlang:system_time(second),
S == apply_func(date_to_unix_ts,
Args1 ++ [apply_func(format_date,
Args1 ++ [S])])),
Args2 = [<<"millisecond">>, <<"+04:00">>, <<"--%m--%d--%y---%H:%M:%S%Z">>],
?FORALL(S, erlang:system_time(millisecond),
S == apply_func(date_to_unix_ts,
Args2 ++ [apply_func(format_date,
Args2 ++ [S])])),
Args = [<<"second">>, <<"+08:00">>, <<"%y-%m-%d-%H:%M:%S%Z">>],
?FORALL(S, erlang:system_time(second),
S == apply_func(date_to_unix_ts,
Args ++ [apply_func(format_date,
Args ++ [S])])).
%%------------------------------------------------------------------------------
%% Utility functions
%%------------------------------------------------------------------------------
@ -822,4 +842,3 @@ all() ->
suite() ->
[{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}].