From 863bfbcffda14180cacc83dab54e3165ac105f55 Mon Sep 17 00:00:00 2001 From: ador Date: Sun, 15 Sep 2024 17:51:18 +0200 Subject: [PATCH 1/3] handle variable timezones due daylight saving --- rebar.config | 3 ++- src/jsone_encode.erl | 14 ++++++++++++++ test/jsone_encode_tests.erl | 17 +++++++++++++++++ test/test_time_module.erl | 15 +++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/test_time_module.erl diff --git a/rebar.config b/rebar.config index 8b131af..aee6b07 100644 --- a/rebar.config +++ b/rebar.config @@ -30,7 +30,8 @@ inline, {platform_define, "^R[01][0-9]", 'NO_MAP_TYPE'}, {platform_define, "^(R|17)", 'NO_DIALYZER_SPEC'}, - {d, 'MAP_ITER_ORDERED'}]}]}, + {d, 'MAP_ITER_ORDERED'}, + {d, 'TIME_MODULE', test_time_module}]}]}, {edown, [{edoc_opts, [{doclet, edown_doclet}]}, {deps, [edown]}]}]}. {project_plugins, [covertool, rebar3_efmt]}. diff --git a/src/jsone_encode.erl b/src/jsone_encode.erl index 096b31f..f2e63bf 100644 --- a/src/jsone_encode.erl +++ b/src/jsone_encode.erl @@ -580,6 +580,8 @@ parse_option([{datetime_format, Fmt} | T], Opt) -> parse_option(T, Opt?OPT{datetime_format = {iso8601, 0}}); {iso8601, local} -> parse_option(T, Opt?OPT{datetime_format = {iso8601, local_offset()}}); + {iso8601, local_dst} -> + parse_option(T, Opt?OPT{datetime_format = {iso8601, local_offset_dst()}}); {iso8601, N} when -86400 < N, N < 86400 -> parse_option(T, Opt?OPT{datetime_format = {iso8601, N}}); _ -> @@ -600,3 +602,15 @@ local_offset() -> UTC = {{1970, 1, 2}, {0, 0, 0}}, Local = calendar:universal_time_to_local_time({{1970, 1, 2}, {0, 0, 0}}), calendar:datetime_to_gregorian_seconds(Local) - calendar:datetime_to_gregorian_seconds(UTC). + +-ifndef(TIME_MODULE). + +-define(TIME_MODULE, erlang). + +-endif. + +-spec local_offset_dst () -> jsone:utc_offset_seconds(). +local_offset_dst() -> + LocalDateTime = ?TIME_MODULE:localtime(), + calendar:datetime_to_gregorian_seconds(LocalDateTime) + - calendar:datetime_to_gregorian_seconds(?TIME_MODULE:localtime_to_universaltime(LocalDateTime)). diff --git a/test/jsone_encode_tests.erl b/test/jsone_encode_tests.erl index 60fb01b..b256b12 100644 --- a/test/jsone_encode_tests.erl +++ b/test/jsone_encode_tests.erl @@ -140,6 +140,23 @@ encode_test_() -> ?assertMatch(<<"\"2015-06-25T14:57:25Z\"">>, Json) end end}, + {"datetime: iso8601: local with daylight saving variable zone - summer time (2h offset)", + fun() -> + test_time_module:set_localtime({{2024, 9, 15},{11, 00, 00}}), + test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 9, 15},{9, 00, 00}} end), + + {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), + ?assertMatch(<<"\"2015-06-25T14:57:25+02:00\"">>, Json) + end}, + {"datetime: iso8601: local with daylight saving variable zone - winter time (1h offset)", + fun() -> + test_time_module:set_localtime({{2024, 12, 15},{11, 00, 00}}), + test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 12, 15},{10, 00, 00}} end), + + {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), + ?assertMatch(<<"\"2015-06-25T14:57:25+01:00\"">>, Json) + end + }, {"datetime: iso8601: timezone", fun() -> ?assertEqual({ok, <<"\"2015-06-25T14:57:25Z\"">>}, diff --git a/test/test_time_module.erl b/test/test_time_module.erl new file mode 100644 index 0000000..4143448 --- /dev/null +++ b/test/test_time_module.erl @@ -0,0 +1,15 @@ +-module(test_time_module). +-export([localtime/0, set_localtime/1, localtime_to_universaltime/1, mock_localtime_to_universaltime/1]). + +set_localtime({{_,_,_}, {_,_,_}} = LocalTime) -> + erlang:put('__test_time_module__localtime__', LocalTime). + +localtime() -> + erlang:get('__test_time_module__localtime__'). + +localtime_to_universaltime({{_,_,_}, {_,_,_}} = LocalTime) -> + LocalTimeToUniversalTimeFun = erlang:get('__test_time_module_localtime_to_universaltime__'), + LocalTimeToUniversalTimeFun(LocalTime). + +mock_localtime_to_universaltime(Fun) when is_function(Fun) -> + erlang:put('__test_time_module_localtime_to_universaltime__', Fun). From b871f8f0e0ed4c244d36e4a9b5dc78b2d7e48bf0 Mon Sep 17 00:00:00 2001 From: ador Date: Mon, 16 Sep 2024 07:57:44 +0200 Subject: [PATCH 2/3] fix formatting --- src/jsone_encode.erl | 8 +++++--- test/jsone_decode_tests.erl | 3 ++- test/jsone_encode_tests.erl | 23 +++++++++++------------ test/test_time_module.erl | 8 ++++++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/jsone_encode.erl b/src/jsone_encode.erl index f2e63bf..ef5d958 100644 --- a/src/jsone_encode.erl +++ b/src/jsone_encode.erl @@ -603,14 +603,16 @@ local_offset() -> Local = calendar:universal_time_to_local_time({{1970, 1, 2}, {0, 0, 0}}), calendar:datetime_to_gregorian_seconds(Local) - calendar:datetime_to_gregorian_seconds(UTC). + -ifndef(TIME_MODULE). -define(TIME_MODULE, erlang). -endif. --spec local_offset_dst () -> jsone:utc_offset_seconds(). + +-spec local_offset_dst() -> jsone:utc_offset_seconds(). local_offset_dst() -> LocalDateTime = ?TIME_MODULE:localtime(), - calendar:datetime_to_gregorian_seconds(LocalDateTime) - - calendar:datetime_to_gregorian_seconds(?TIME_MODULE:localtime_to_universaltime(LocalDateTime)). + calendar:datetime_to_gregorian_seconds(LocalDateTime) - + calendar:datetime_to_gregorian_seconds(?TIME_MODULE:localtime_to_universaltime(LocalDateTime)). diff --git a/test/jsone_decode_tests.erl b/test/jsone_decode_tests.erl index cf1bf60..83b8a81 100644 --- a/test/jsone_decode_tests.erl +++ b/test/jsone_decode_tests.erl @@ -43,7 +43,8 @@ decode_test_() -> ?assertEqual({ok, 0, <<"1">>}, jsone_decode:decode(<<"-01">>)) end}, {"integer can't begin with an explicit plus sign", - fun() -> ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"+1">>)) end}, + fun() -> ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"+1">>)) + end}, %% Numbers: Floats {"float: decimal notation", fun() -> ?assertEqual({ok, 1.23, <<"">>}, jsone_decode:decode(<<"1.23">>)) end}, diff --git a/test/jsone_encode_tests.erl b/test/jsone_encode_tests.erl index b256b12..7e97dc3 100644 --- a/test/jsone_encode_tests.erl +++ b/test/jsone_encode_tests.erl @@ -141,22 +141,21 @@ encode_test_() -> end end}, {"datetime: iso8601: local with daylight saving variable zone - summer time (2h offset)", - fun() -> - test_time_module:set_localtime({{2024, 9, 15},{11, 00, 00}}), - test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 9, 15},{9, 00, 00}} end), + fun() -> + test_time_module:set_localtime({{2024, 9, 15}, {11, 00, 00}}), + test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 9, 15}, {9, 00, 00}} end), - {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), - ?assertMatch(<<"\"2015-06-25T14:57:25+02:00\"">>, Json) + {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), + ?assertMatch(<<"\"2015-06-25T14:57:25+02:00\"">>, Json) end}, {"datetime: iso8601: local with daylight saving variable zone - winter time (1h offset)", - fun() -> - test_time_module:set_localtime({{2024, 12, 15},{11, 00, 00}}), - test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 12, 15},{10, 00, 00}} end), + fun() -> + test_time_module:set_localtime({{2024, 12, 15}, {11, 00, 00}}), + test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 12, 15}, {10, 00, 00}} end), - {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), - ?assertMatch(<<"\"2015-06-25T14:57:25+01:00\"">>, Json) - end - }, + {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), + ?assertMatch(<<"\"2015-06-25T14:57:25+01:00\"">>, Json) + end}, {"datetime: iso8601: timezone", fun() -> ?assertEqual({ok, <<"\"2015-06-25T14:57:25Z\"">>}, diff --git a/test/test_time_module.erl b/test/test_time_module.erl index 4143448..d81608e 100644 --- a/test/test_time_module.erl +++ b/test/test_time_module.erl @@ -1,15 +1,19 @@ -module(test_time_module). -export([localtime/0, set_localtime/1, localtime_to_universaltime/1, mock_localtime_to_universaltime/1]). -set_localtime({{_,_,_}, {_,_,_}} = LocalTime) -> + +set_localtime({{_, _, _}, {_, _, _}} = LocalTime) -> erlang:put('__test_time_module__localtime__', LocalTime). + localtime() -> erlang:get('__test_time_module__localtime__'). -localtime_to_universaltime({{_,_,_}, {_,_,_}} = LocalTime) -> + +localtime_to_universaltime({{_, _, _}, {_, _, _}} = LocalTime) -> LocalTimeToUniversalTimeFun = erlang:get('__test_time_module_localtime_to_universaltime__'), LocalTimeToUniversalTimeFun(LocalTime). + mock_localtime_to_universaltime(Fun) when is_function(Fun) -> erlang:put('__test_time_module_localtime_to_universaltime__', Fun). From e0a8de4bf97f43c8d893bbf739a88e9709c17bd1 Mon Sep 17 00:00:00 2001 From: ador Date: Mon, 16 Sep 2024 10:37:33 +0200 Subject: [PATCH 3/3] update doc --- src/jsone.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jsone.erl b/src/jsone.erl index 496a05d..6618b04 100644 --- a/src/jsone.erl +++ b/src/jsone.erl @@ -176,6 +176,8 @@ %% > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, {iso8601, local}}]). %% <<"\"2000-03-10T10:03:58+09:00\"">> %% +%% % Also you can use {iso8601, local_dst} to properly calculate the timezone according to the daylight saving procedure. Consider using it, if the executing computer is located in a country that implements this procedure +%% %% % %% % Explicit TimeZone Offset %% % @@ -184,7 +186,7 @@ %% ''' -type datetime_format() :: iso8601. --type timezone() :: utc | local | utc_offset_seconds(). +-type timezone() :: utc | local | local_dst | utc_offset_seconds(). -type utc_offset_seconds() :: -86399..86399. -type common_option() :: undefined_as_null.