Просмотр исходного кода

Merge pull request #14 from altworx/seconds_fractions

Support datetime with fractions of seconds
Takeru Ohta 8 лет назад
Родитель
Сommit
89538d8d3d
3 измененных файлов с 75 добавлено и 30 удалено
  1. 22 19
      README.md
  2. 45 11
      src/jsone_encode.erl
  3. 8 0
      test/jsone_encode_tests.erl

+ 22 - 19
README.md

@@ -171,28 +171,31 @@ Data Mapping (Erlang <=> JSON)
 Erlang                  JSON             Erlang
 Erlang                  JSON             Erlang
 =================================================================================================
 =================================================================================================
 
 
-null                 -> null                   -> null
-true                 -> true                   -> true
-false                -> false                  -> false
-<<"abc">>            -> "abc"                  -> <<"abc">>
-abc                  -> "abc"                  -> <<"abc">> % non-special atom is regarded as a binary
-{{2010,1,1},{0,0,0}} -> "2010-01-01T00:00:00Z" -> <<"2010-01-01T00:00:00Z">> % datetime (see: `jsone:datetime_encode_format/0`)
-123                  -> 123                    -> 123
-123.4                -> 123.4                  -> 123.4
-[1,2,3]              -> [1,2,3]                -> [1,2,3]
-{[]}                 -> {}                     -> {[]}                       % object_format=tuple
-{[{key, <<"val">>}]} -> {"key":"val"}          -> {[{<<"key">>, <<"val">>}]} % object_format=tuple
-[{}]                 -> {}                     -> [{}]                       % object_format=proplist
-[{<<"key">>, val}]   -> {"key":"val"}          -> [{<<"key">>, <<"val">>}]   % object_format=proplist
-#{}                  -> {}                     -> #{}                        % object_format=map
-#{key => val}        -> {"key":"val"}          -> #{<<"key">> => <<"val">>}  % object_format=map
-{json, IOList}       -> Value                  -> ~~~                        % UTF-8 encoded term
-{json_utf8, Chars}   -> Value                  -> ~~~                        % Unicode code points
+null                   -> null                       -> null
+true                   -> true                       -> true
+false                  -> false                      -> false
+<<"abc">>              -> "abc"                      -> <<"abc">>
+abc                    -> "abc"                      -> <<"abc">> % non-special atom is regarded as a binary
+{{2010,1,1},{0,0,0}}   -> "2010-01-01T00:00:00Z"     -> <<"2010-01-01T00:00:00Z">>     % datetime*
+{{2010,1,1},{0,0,0.0}} -> "2010-01-01T00:00:00.000Z" -> <<"2010-01-01T00:00:00.000Z">> % datetime*
+123                    -> 123                        -> 123
+123.4                  -> 123.4                      -> 123.4
+[1,2,3]                -> [1,2,3]                    -> [1,2,3]
+{[]}                   -> {}                         -> {[]}                       % object_format=tuple
+{[{key, <<"val">>}]}   -> {"key":"val"}              -> {[{<<"key">>, <<"val">>}]} % object_format=tuple
+[{}]                   -> {}                         -> [{}]                       % object_format=proplist
+[{<<"key">>, val}]     -> {"key":"val"}              -> [{<<"key">>, <<"val">>}]   % object_format=proplist
+#{}                    -> {}                         -> #{}                        % object_format=map
+#{key => val}          -> {"key":"val"}              -> #{<<"key">> => <<"val">>}  % object_format=map
+{json, IOList}         -> Value                      -> ~~~                        % UTF-8 encoded term**
+{json_utf8, Chars}     -> Value                      -> ~~~                        % Unicode code points**
 ```
 ```
 
 
-`{json, IOList} and {json_utf8, Chars} allows inline already encoded JSON
+\* see [jsone:datetime_encode_format()](doc/jsone.md#type-datetime_encode_format)
+
+\** `{json, IOList}` and `{json_utf8, Chars}` allows inline already encoded JSON
 values. For example, you obtain JSON encoded data from database so you don't
 values. For example, you obtain JSON encoded data from database so you don't
-have to decode it first and encode again. See [json_term()](doc/jsone.md#type-json_term).
+have to decode it first and encode again. See [jsone:json_term()](doc/jsone.md#type-json_term).
 
 
 API
 API
 ---
 ---

+ 45 - 11
src/jsone_encode.erl

@@ -42,8 +42,10 @@
 -define(ERROR(Function, Args), {error, {badarg, [{?MODULE, Function, Args, [{line, ?LINE}]}]}}).
 -define(ERROR(Function, Args), {error, {badarg, [{?MODULE, Function, Args, [{line, ?LINE}]}]}}).
 -define(IS_STR(X), (is_binary(X) orelse is_atom(X))).
 -define(IS_STR(X), (is_binary(X) orelse is_atom(X))).
 -define(IS_UINT(X), (is_integer(X) andalso X >= 0)).
 -define(IS_UINT(X), (is_integer(X) andalso X >= 0)).
+-define(IS_PNUM(X), (is_number(X) andalso X >=0)).
 -define(IS_DATETIME(Y,M,D,H,Mi,S), (?IS_UINT(Y) andalso ?IS_UINT(M) andalso ?IS_UINT(D) andalso
 -define(IS_DATETIME(Y,M,D,H,Mi,S), (?IS_UINT(Y) andalso ?IS_UINT(M) andalso ?IS_UINT(D) andalso
-                                    ?IS_UINT(H) andalso ?IS_UINT(Mi) andalso ?IS_UINT(S))).
+                                    ?IS_UINT(H) andalso ?IS_UINT(Mi) andalso
+                                    ?IS_PNUM(S))).
 
 
 -ifdef('NO_MAP_TYPE').
 -ifdef('NO_MAP_TYPE').
 -define(IS_MAP(X), is_tuple(X)).
 -define(IS_MAP(X), is_tuple(X)).
@@ -146,21 +148,53 @@ string(Str, Nexts, Buf, Opt) ->
 
 
 -spec datetime(calendar:datetime(), [next()], binary(), opt()) -> encode_result().
 -spec datetime(calendar:datetime(), [next()], binary(), opt()) -> encode_result().
 datetime({{Y,M,D}, {H,Mi,S}}, Nexts, Buf, Opt) when ?IS_DATETIME(Y,M,D,H,Mi,S) ->
 datetime({{Y,M,D}, {H,Mi,S}}, Nexts, Buf, Opt) when ?IS_DATETIME(Y,M,D,H,Mi,S) ->
+    {iso8601, Tz} = Opt?OPT.datetime_format,
     Str =
     Str =
-        case Opt?OPT.datetime_format of
-            {iso8601, 0}  -> io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Y, M, D, H, Mi, S]);
-            {iso8601, Tz} ->
-                {Sign, {DiffHour, DiffMinute, _}} =
-                    case Tz > 0 of
-                        true  -> {$+, calendar:seconds_to_time(Tz)};
-                        false -> {$-, calendar:seconds_to_time(-Tz)}
-                    end,
-                io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B~c~2..0B:~2..0B", [Y, M, D, H, Mi, S, Sign, DiffHour, DiffMinute])
-        end,
+    [format_year(Y), $-, format2digit(M), $-, format2digit(D), $T,
+     format2digit(H), $:, format2digit(Mi), $:, format_seconds(S),
+     format_tz(Tz)],
     next(Nexts, <<Buf/binary, $", (list_to_binary(Str))/binary, $">>, Opt);
     next(Nexts, <<Buf/binary, $", (list_to_binary(Str))/binary, $">>, Opt);
 datetime(Datetime, Nexts, Buf, Opt) ->
 datetime(Datetime, Nexts, Buf, Opt) ->
     ?ERROR(datetime, [Datetime, Nexts, Buf, Opt]).
     ?ERROR(datetime, [Datetime, Nexts, Buf, Opt]).
 
 
+-dialyzer({no_improper_lists, [format_year/1]}).
+-spec format_year(non_neg_integer()) -> iodata().
+format_year(Y) when Y > 999 -> integer_to_binary(Y);
+format_year(Y) ->
+    B = integer_to_binary(Y),
+    [lists:duplicate(4-byte_size(B), $0)|B].
+
+-spec format2digit(non_neg_integer()) -> iolist().
+format2digit(0) -> "00";
+format2digit(1) -> "01";
+format2digit(2) -> "02";
+format2digit(3) -> "03";
+format2digit(4) -> "04";
+format2digit(5) -> "05";
+format2digit(6) -> "06";
+format2digit(7) -> "07";
+format2digit(8) -> "08";
+format2digit(9) -> "09";
+format2digit(X) -> integer_to_list(X).
+
+-spec format_seconds(non_neg_integer() | float()) -> iolist().
+format_seconds(S) when is_integer(S) -> format2digit(S);
+format_seconds(S) when is_float(S) -> io_lib:format("~6.3.0f", [S]).
+
+-spec format_tz(integer()) -> byte() | iolist().
+format_tz(0) -> $Z;
+format_tz(Tz) when Tz > 0 -> [$+|format_tz_(Tz)];
+format_tz(Tz) -> [$-|format_tz_(-Tz)].
+
+-define(SECONDS_PER_MINUTE, 60).
+-define(SECONDS_PER_HOUR, 3600).
+-spec format_tz_(integer()) -> iolist().
+format_tz_(S) ->
+    H = S div ?SECONDS_PER_HOUR,
+    S1 = S rem ?SECONDS_PER_HOUR,
+    M = S1 div ?SECONDS_PER_MINUTE,
+    [format2digit(H), $:, format2digit(M)].
+
 -spec object_key(jsone:json_value(), [next()], binary(), opt()) -> encode_result().
 -spec object_key(jsone:json_value(), [next()], binary(), opt()) -> encode_result().
 object_key(Key, Nexts, Buf, Opt) when ?IS_STR(Key) ->
 object_key(Key, Nexts, Buf, Opt) when ?IS_STR(Key) ->
     string(Key, Nexts, Buf, Opt);
     string(Key, Nexts, Buf, Opt);

+ 8 - 0
test/jsone_encode_tests.erl

@@ -148,6 +148,14 @@ encode_test_() ->
        ?_assertEqual({ok, <<"[\"2015-06-25T14:57:25Z\"]">>},
        ?_assertEqual({ok, <<"[\"2015-06-25T14:57:25Z\"]">>},
                       jsone_encode:encode([{{2015,6,25},{14,57,25}}]))},
                       jsone_encode:encode([{{2015,6,25},{14,57,25}}]))},
 
 
+     {"datetime: iso8601: with fractions of seconds",
+      fun () ->
+              ?assertEqual({ok, <<"\"2015-06-25T14:57:25.325Z\"">>},
+                           jsone_encode:encode({{2015,6,25},{14,57,25.3245}})),
+              ?assertEqual({ok, <<"\"2015-06-25T14:57:05.320Z\"">>},
+                           jsone_encode:encode({{2015,6,25},{14,57,5.32}}))
+      end},
+
      %% Arrays
      %% Arrays
      {"simple array",
      {"simple array",
       fun () ->
       fun () ->