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
 =================================================================================================
 
-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
-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
 ---

+ 45 - 11
src/jsone_encode.erl

@@ -42,8 +42,10 @@
 -define(ERROR(Function, Args), {error, {badarg, [{?MODULE, Function, Args, [{line, ?LINE}]}]}}).
 -define(IS_STR(X), (is_binary(X) orelse is_atom(X))).
 -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
-                                    ?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').
 -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().
 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 =
-        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);
 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().
 object_key(Key, Nexts, Buf, Opt) when ?IS_STR(Key) ->
     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\"]">>},
                       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
      {"simple array",
       fun () ->