fix: port the changes for date_to_unix_ts SQL fun from 4.4

This commit is contained in:
Shawn 2024-03-08 10:36:49 +08:00
parent 1f38813cb9
commit 163d095dca
4 changed files with 157 additions and 67 deletions

View File

@ -1181,7 +1181,7 @@ format_date(TimeUnit, Offset, FormatString, TimeEpoch) ->
date_to_unix_ts(TimeUnit, FormatString, InputString) -> date_to_unix_ts(TimeUnit, FormatString, InputString) ->
Unit = time_unit(TimeUnit), Unit = time_unit(TimeUnit),
emqx_utils_calendar:parse(InputString, Unit, FormatString). emqx_utils_calendar:formatted_datetime_to_system_time(InputString, Unit, FormatString).
date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) -> date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) ->
Unit = time_unit(TimeUnit), Unit = time_unit(TimeUnit),

View File

@ -1143,6 +1143,50 @@ timezone_to_offset_seconds_helper(FunctionName) ->
apply_func(FunctionName, [local]), apply_func(FunctionName, [local]),
ok. ok.
t_date_to_unix_ts(_) ->
TestTab = [
{{"2024-03-01T10:30:38+08:00", second}, [
<<"second">>, <<"+08:00">>, <<"%Y-%m-%d %H-%M-%S">>, <<"2024-03-01 10:30:38">>
]},
{{"2024-03-01T10:30:38.333+08:00", second}, [
<<"second">>, <<"+08:00">>, <<"%Y-%m-%d %H-%M-%S.%3N">>, <<"2024-03-01 10:30:38.333">>
]},
{{"2024-03-01T10:30:38.333+08:00", millisecond}, [
<<"millisecond">>,
<<"+08:00">>,
<<"%Y-%m-%d %H-%M-%S.%3N">>,
<<"2024-03-01 10:30:38.333">>
]},
{{"2024-03-01T10:30:38.333+08:00", microsecond}, [
<<"microsecond">>,
<<"+08:00">>,
<<"%Y-%m-%d %H-%M-%S.%3N">>,
<<"2024-03-01 10:30:38.333">>
]},
{{"2024-03-01T10:30:38.333+08:00", nanosecond}, [
<<"nanosecond">>,
<<"+08:00">>,
<<"%Y-%m-%d %H-%M-%S.%3N">>,
<<"2024-03-01 10:30:38.333">>
]},
{{"2024-03-01T10:30:38.333444+08:00", microsecond}, [
<<"microsecond">>,
<<"+08:00">>,
<<"%Y-%m-%d %H-%M-%S.%6N">>,
<<"2024-03-01 10:30:38.333444">>
]}
],
lists:foreach(
fun({{DateTime3339, Unit}, DateToTsArgs}) ->
?assertEqual(
calendar:rfc3339_to_system_time(DateTime3339, [{unit, Unit}]),
apply_func(date_to_unix_ts, DateToTsArgs),
"Failed on test: " ++ DateTime3339 ++ "/" ++ atom_to_list(Unit)
)
end,
TestTab
).
t_parse_date_errors(_) -> t_parse_date_errors(_) ->
?assertError( ?assertError(
bad_formatter_or_date, bad_formatter_or_date,
@ -1154,6 +1198,37 @@ t_parse_date_errors(_) ->
bad_formatter_or_date, bad_formatter_or_date,
emqx_rule_funcs:date_to_unix_ts(second, <<"%y-%m-%d %H:%M:%S">>, <<"2022-05-26 10:40:12">>) emqx_rule_funcs:date_to_unix_ts(second, <<"%y-%m-%d %H:%M:%S">>, <<"2022-05-26 10:40:12">>)
), ),
%% invalid formats
?assertThrow(
{missing_date_part, month},
emqx_rule_funcs:date_to_unix_ts(
second, <<"%Y-%d %H:%M:%S">>, <<"2022-32 10:40:12">>
)
),
?assertThrow(
{missing_date_part, year},
emqx_rule_funcs:date_to_unix_ts(
second, <<"%H:%M:%S">>, <<"10:40:12">>
)
),
?assertError(
_,
emqx_rule_funcs:date_to_unix_ts(
second, <<"%Y-%m-%d %H:%M:%S">>, <<"2022-05-32 10:40:12">>
)
),
?assertError(
_,
emqx_rule_funcs:date_to_unix_ts(
second, <<"%Y-%m-%d %H:%M:%S">>, <<"2023-02-29 10:40:12">>
)
),
?assertError(
_,
emqx_rule_funcs:date_to_unix_ts(
second, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-02-30 10:40:12">>
)
),
%% Compatibility test %% Compatibility test
%% UTC+0 %% UTC+0
@ -1173,25 +1248,42 @@ t_parse_date_errors(_) ->
emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2022-05-26 10-40-12">>) emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2022-05-26 10-40-12">>)
), ),
%% UTC+0 %% leap year checks
UnixTsLeap0 = 1582986700,
?assertEqual( ?assertEqual(
UnixTsLeap0, %% UTC+0
emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2020-02-29 14:31:40">>) 1709217100,
emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-02-29 14:31:40">>)
), ),
%% UTC+0
UnixTsLeap1 = 1709297071,
?assertEqual( ?assertEqual(
UnixTsLeap1, %% UTC+0
1709297071,
emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-03-01 12:44:31">>) emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-03-01 12:44:31">>)
), ),
%% UTC+0
UnixTsLeap2 = 1709535387,
?assertEqual( ?assertEqual(
UnixTsLeap2, %% UTC+0
emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-03-04 06:56:27">>) 4107588271,
emqx_rule_funcs:date_to_unix_ts(second, <<"%Y-%m-%d %H:%M:%S">>, <<"2100-03-01 12:44:31">>)
),
?assertEqual(
%% UTC+8
1709188300,
emqx_rule_funcs:date_to_unix_ts(
second, <<"+08:00">>, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-02-29 14:31:40">>
)
),
?assertEqual(
%% UTC+8
1709268271,
emqx_rule_funcs:date_to_unix_ts(
second, <<"+08:00">>, <<"%Y-%m-%d %H:%M:%S">>, <<"2024-03-01 12:44:31">>
)
),
?assertEqual(
%% UTC+8
4107559471,
emqx_rule_funcs:date_to_unix_ts(
second, <<"+08:00">>, <<"%Y-%m-%d %H:%M:%S">>, <<"2100-03-01 12:44:31">>
)
), ),
%% None zero zone shift with millisecond level precision %% None zero zone shift with millisecond level precision

View File

@ -22,7 +22,7 @@
formatter/1, formatter/1,
format/3, format/3,
format/4, format/4,
parse/3, formatted_datetime_to_system_time/3,
offset_second/1 offset_second/1
]). ]).
@ -48,8 +48,9 @@
-define(DAYS_PER_YEAR, 365). -define(DAYS_PER_YEAR, 365).
-define(DAYS_PER_LEAP_YEAR, 366). -define(DAYS_PER_LEAP_YEAR, 366).
-define(DAYS_FROM_0_TO_1970, 719528). -define(DAYS_FROM_0_TO_1970, 719528).
-define(SECONDS_FROM_0_TO_1970, (?DAYS_FROM_0_TO_1970 * ?SECONDS_PER_DAY)). -define(DAYS_FROM_0_TO_10000, 2932897).
-define(SECONDS_FROM_0_TO_1970, ?DAYS_FROM_0_TO_1970 * ?SECONDS_PER_DAY).
-define(SECONDS_FROM_0_TO_10000, (?DAYS_FROM_0_TO_10000 * ?SECONDS_PER_DAY)).
%% the maximum value is the SECONDS_FROM_0_TO_10000 in the calendar.erl, %% the maximum value is the SECONDS_FROM_0_TO_10000 in the calendar.erl,
%% here minus SECONDS_PER_DAY to tolerate timezone time offset, %% here minus SECONDS_PER_DAY to tolerate timezone time offset,
%% so the maximum date can reach 9999-12-31 which is ample. %% so the maximum date can reach 9999-12-31 which is ample.
@ -171,10 +172,10 @@ format(Time, Unit, Offset, FormatterBin) when is_binary(FormatterBin) ->
format(Time, Unit, Offset, Formatter) -> format(Time, Unit, Offset, Formatter) ->
do_format(Time, time_unit(Unit), offset_second(Offset), Formatter). do_format(Time, time_unit(Unit), offset_second(Offset), Formatter).
parse(DateStr, Unit, FormatterBin) when is_binary(FormatterBin) -> formatted_datetime_to_system_time(DateStr, Unit, FormatterBin) when is_binary(FormatterBin) ->
parse(DateStr, Unit, formatter(FormatterBin)); formatted_datetime_to_system_time(DateStr, Unit, formatter(FormatterBin));
parse(DateStr, Unit, Formatter) -> formatted_datetime_to_system_time(DateStr, Unit, Formatter) ->
do_parse(DateStr, Unit, Formatter). do_formatted_datetime_to_system_time(DateStr, Unit, Formatter).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Time unit %% Time unit
@ -467,56 +468,51 @@ padding(Data, _Len) ->
Data. Data.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% internal: parse part %% internal: formatted_datetime_to_system_time part
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
do_parse(DateStr, Unit, Formatter) -> do_formatted_datetime_to_system_time(DateStr, Unit, Formatter) ->
DateInfo = do_parse_date_str(DateStr, Formatter, #{}), DateInfo = do_parse_date_str(DateStr, Formatter, #{}),
{Precise, PrecisionUnit} = precision(DateInfo), PrecisionUnit = precision(DateInfo),
Counter = ToPrecisionUnit = fun(Time, FromUnit) ->
fun erlang:convert_time_unit(Time, FromUnit, PrecisionUnit)
(year, V, Res) -> end,
Res + dy(V) * ?SECONDS_PER_DAY * Precise - (?SECONDS_FROM_0_TO_1970 * Precise); GetRequiredPart = fun(Key) ->
(month, V, Res) -> case maps:get(Key, DateInfo, undefined) of
Dm = dym(maps:get(year, DateInfo, 0), V), undefined -> throw({missing_date_part, Key});
Res + Dm * ?SECONDS_PER_DAY * Precise; Value -> Value
(day, V, Res) -> end
Res + (V * ?SECONDS_PER_DAY * Precise); end,
(hour, V, Res) -> GetOptionalPart = fun(Key) -> maps:get(Key, DateInfo, 0) end,
Res + (V * ?SECONDS_PER_HOUR * Precise); Year = GetRequiredPart(year),
(minute, V, Res) -> Month = GetRequiredPart(month),
Res + (V * ?SECONDS_PER_MINUTE * Precise); Day = GetRequiredPart(day),
(second, V, Res) -> Hour = GetRequiredPart(hour),
Res + V * Precise; Min = GetRequiredPart(minute),
(millisecond, V, Res) -> Sec = GetRequiredPart(second),
case PrecisionUnit of DateTime = {{Year, Month, Day}, {Hour, Min, Sec}},
millisecond -> TotalSecs = datetime_to_system_time(DateTime) - GetOptionalPart(parsed_offset),
Res + V; check(TotalSecs, DateStr, Unit),
microsecond -> TotalTime =
Res + (V * 1000); ToPrecisionUnit(TotalSecs, second) +
nanosecond -> ToPrecisionUnit(GetOptionalPart(millisecond), millisecond) +
Res + (V * 1000000) ToPrecisionUnit(GetOptionalPart(microsecond), microsecond) +
end; ToPrecisionUnit(GetOptionalPart(nanosecond), nanosecond),
(microsecond, V, Res) -> erlang:convert_time_unit(TotalTime, PrecisionUnit, Unit).
case PrecisionUnit of
microsecond ->
Res + V;
nanosecond ->
Res + (V * 1000)
end;
(nanosecond, V, Res) ->
Res + V;
(parsed_offset, V, Res) ->
Res - V * Precise
end,
Count = maps:fold(Counter, 0, DateInfo) - (?SECONDS_PER_DAY * Precise),
erlang:convert_time_unit(Count, PrecisionUnit, Unit).
precision(#{nanosecond := _}) -> {1000_000_000, nanosecond}; check(Secs, _, _) when Secs >= -?SECONDS_FROM_0_TO_1970, Secs < ?SECONDS_FROM_0_TO_10000 ->
precision(#{microsecond := _}) -> {1000_000, microsecond}; ok;
precision(#{millisecond := _}) -> {1000, millisecond}; check(_Secs, DateStr, Unit) ->
precision(#{second := _}) -> {1, second}; throw({bad_format, #{date_string => DateStr, to_unit => Unit}}).
precision(_) -> {1, second}.
datetime_to_system_time(DateTime) ->
calendar:datetime_to_gregorian_seconds(DateTime) - ?SECONDS_FROM_0_TO_1970.
precision(#{nanosecond := _}) -> nanosecond;
precision(#{microsecond := _}) -> microsecond;
precision(#{millisecond := _}) -> millisecond;
precision(#{second := _}) -> second;
precision(_) -> second.
do_parse_date_str(<<>>, _, Result) -> do_parse_date_str(<<>>, _, Result) ->
Result; Result;

View File

@ -0,0 +1,2 @@
Refactor the SQL function: `date_to_unix_ts()` by using `calendar:datetime_to_gregorian_seconds/1`.
This change also added validation for the input date format.