diff --git a/apps/emqx_rule_engine/src/emqx_rule_date.erl b/apps/emqx_rule_engine/src/emqx_rule_date.erl new file mode 100644 index 000000000..fb9cad4c3 --- /dev/null +++ b/apps/emqx_rule_engine/src/emqx_rule_date.erl @@ -0,0 +1,248 @@ +%%-------------------------------------------------------------------- +%% 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() + year = "1970" :: string(), + %%month() + month = "1" :: string(), + %%day() + day = "1" :: string(), + %%hour() + hour = "0" :: string(), + %%minute() %% epoch in millisecond precision + minute = "0" :: string(), + %%second() %% epoch in millisecond precision + second = "0" :: string(), + %%integer() %% zone maybe some value + zone = "+00:00" :: string() +}). + +%% -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 Shows hour in 24-hour format [15] + [ + {$H, R#result.hour}, + %% %M Displays minutes [00-59] + {$M, R#result.minute}, + %% %S Displays seconds [00-59] + {$S, R#result.second}, + %% %y Displays year YYYY [2021] + {$y, R#result.year}, + %% %m Displays the number of the month [01-12] + {$m, R#result.month}, + %% %d Displays the number of the month [01-12] + {$d, R#result.day}, + %% %Z Displays Time zone + {$Z, R#result.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 Shows hour in 24-hour format [15] + [ + {$H, fun(Input) -> parse_int_times(2, Input) end}, + %% %M Displays minutes [00-59] + {$M, fun(Input) -> parse_int_times(2, Input) end}, + %% %S Displays seconds [00-59] + {$S, fun(Input) -> parse_second(Input) end}, + %% %y Displays year YYYY [2021] + {$y, fun(Input) -> parse_int_times(4, Input) end}, + %% %m Displays the number of the month [01-12] + {$m, fun(Input) -> parse_int_times(2, Input) end}, + %% %d Displays the number of the month [01-12] + {$d, fun(Input) -> parse_int_times(2, Input) end}, + %% %Z Displays Time zone + {$Z, fun(Input) -> parse_zone(Input) end} + ] + ). + +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).