Browse Source

Merge pull request #14 from altworx/seconds_fractions

Support datetime with fractions of seconds
Takeru Ohta 8 years ago
parent
commit
89538d8d3d
3 changed files with 75 additions and 30 deletions
  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 () ->