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.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. diff --git a/src/jsone_encode.erl b/src/jsone_encode.erl index 096b31f..ef5d958 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,17 @@ 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_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 60fb01b..7e97dc3 100644 --- a/test/jsone_encode_tests.erl +++ b/test/jsone_encode_tests.erl @@ -140,6 +140,22 @@ 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..d81608e --- /dev/null +++ b/test/test_time_module.erl @@ -0,0 +1,19 @@ +-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).