From d052e63ba5acd5d24046a0c10d365aaeb5c45d61 Mon Sep 17 00:00:00 2001 From: Kirilll Zaborsky Date: Mon, 26 Jan 2015 23:24:41 +0300 Subject: [PATCH 1/7] Proper zero padding for microseconds --- src/ec_date.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ec_date.erl b/src/ec_date.erl index c21c2a8..e970250 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -530,7 +530,7 @@ format([$i|T], {_,{_,M,_,_}}=Dt, Acc) -> format([$s|T], {_,{_,_,S,_}}=Dt, Acc) -> format(T, Dt, [pad2(S)|Acc]); format([$f|T], {_,{_,_,_,Ms}}=Dt, Acc) -> - format(T, Dt, [itol(Ms)|Acc]); + format(T, Dt, [pad6(Ms)|Acc]); %% Whole Dates format([$c|T], {{Y,M,D},{H,Min,S}}=Dt, Acc) -> @@ -687,6 +687,10 @@ pad2(X) when is_integer(X) -> pad2(X) when is_float(X) -> io_lib:format("~2.10.0B",[trunc(X)]). +-spec pad6(integer()) -> list(). +pad6(X) when is_integer(X) -> + io_lib:format("~6.10.0B",[X]). + ltoi(X) -> list_to_integer(X). @@ -933,7 +937,7 @@ iso_test_() -> ms_test_() -> Now=os:timestamp(), [ - ?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.1234")), + ?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.001234")), ?_assertEqual(format("H:m:s.f \\m \\i\\s \\m\\o\\n\\t\\h",?DATEMS), "17:03:17.123456 m is month"), ?_assertEqual(format("Y-m-d\\TH:i:s.f",?DATEMS), @@ -944,6 +948,8 @@ ms_test_() -> "2001-03-10T05:16:17.123456"), ?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T15:16:17.123456")), "2001-03-10T15:16:17.123456"), + ?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T15:16:17.000123")), + "2001-03-10T15:16:17.000123"), ?_assertEqual(Now, nparse(format("Y-m-d\\TH:i:s.f", Now))) ]. From 8dd9f826dbd6d0e8680ebda039af5d73d04669dc Mon Sep 17 00:00:00 2001 From: Kirilll Zaborsky Date: Mon, 26 Jan 2015 23:25:13 +0300 Subject: [PATCH 2/7] pad2 spec fix --- src/ec_date.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ec_date.erl b/src/ec_date.erl index e970250..22cfa7d 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -680,7 +680,7 @@ iso_week_one(Y) -> itol(X) -> integer_to_list(X). --spec pad2(integer()) -> list(). +-spec pad2(integer() | float()) -> list(). %% @doc int padded with 0 to make sure its 2 chars pad2(X) when is_integer(X) -> io_lib:format("~2.10.0B",[X]); From ab321b16e64da4973ed61ace680a68d9d60d43ad Mon Sep 17 00:00:00 2001 From: Kirilll Zaborsky Date: Mon, 26 Jan 2015 23:25:44 +0300 Subject: [PATCH 3/7] Testcase showing broken microseconds parsing --- src/ec_date.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ec_date.erl b/src/ec_date.erl index 22cfa7d..24dc8de 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -938,6 +938,7 @@ ms_test_() -> Now=os:timestamp(), [ ?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.001234")), + ?_assertEqual({{2012,12,12}, {12,12,12,123000}}, parse("2012-12-12T12:12:12.123")), ?_assertEqual(format("H:m:s.f \\m \\i\\s \\m\\o\\n\\t\\h",?DATEMS), "17:03:17.123456 m is month"), ?_assertEqual(format("Y-m-d\\TH:i:s.f",?DATEMS), From ed107c94b47deff5eb03db53083bb53e865bbff8 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 19 Aug 2016 11:03:44 +0200 Subject: [PATCH 4/7] Fix microsecond() range --- src/ec_date.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ec_date.erl b/src/ec_date.erl index 24dc8de..0424294 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -54,11 +54,11 @@ -type hour() :: 0..23. -type minute() :: 0..59. -type second() :: 0..59. --type microsecond() :: 0..1000000. +-type microsecond() :: 0..999999. -type daynum() :: 1..7. -type date() :: {year(),month(),day()}. --type time() :: {hour(),minute(),second()} |{hour(),minute(),second(), microsecond()}. +-type time() :: {hour(),minute(),second()} | {hour(),minute(),second(),microsecond()}. -type datetime() :: {date(),time()}. -type now() :: {integer(),integer(),integer()}. From a298a7b045e777fd3b81ca59246ac93b86ad6fda Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 19 Aug 2016 11:08:05 +0200 Subject: [PATCH 5/7] Fix support for ISO 8601 fractions of a second This is limited to milli- and microseconds interpreted as 3 or 6 places after decimal comma. All of the following, while valid according to the standard, won't be accepted: - 2001-03-10T17:16:17.1Z - 2001-03-10T17:16:17.12Z - 2001-03-10T17:16:17.1234Z - 2001-03-10T17:16:17.12345Z - 2001-03-10T17:16:17.1234567Z --- src/ec_date.erl | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/ec_date.erl b/src/ec_date.erl index 0424294..aebb429 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -149,10 +149,16 @@ nparse(Date) -> %% LOCAL FUNCTIONS %% +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $Z ], _Now, _Opts) + when ?is_world_sep(X) + andalso (Micros >= 0 andalso Micros < 1000000) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Micros}}; + parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $Z ], _Now, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) andalso Year > 31 -> - {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, { 0}}; + {{Year, Month, Day}, {hour(Hour, []), Min, Sec}}; parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $+, Off | _Rest ], _Now, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) @@ -306,6 +312,15 @@ parse(_Tokens, _Now, _Opts) -> tokenise([], Acc) -> lists:reverse(Acc); +%% ISO 8601 fractions of a second: only 3 or 6 places after comma, +%% i.e. milli- or microseconds +tokenise([$., N1, N2, N3, N4, N5, N6 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> + tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]), $. | Acc]); +tokenise([$., N1, N2, N3 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3) -> + tokenise(Rest, [ ltoi([N1, N2, N3, $0, $0, $0]), $. | Acc]); + tokenise([N1, N2, N3, N4, N5, N6 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]) | Acc]); @@ -405,7 +420,6 @@ tokenise("ST"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("OF"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("T"++Rest, Acc) -> tokenise(Rest, Acc); % 2012-12-12T12:12:12 ISO formatting. tokenise([$Z | Rest], Acc) -> tokenise(Rest, [$Z | Acc]); % 2012-12-12T12:12:12Zulu -tokenise([$. | Rest], Acc) -> tokenise(Rest, [$. | Acc]); % 2012-12-12T12:12:12.xxxx ISO formatting. tokenise([$+, H1,H2,M1,M2| Rest], Acc) when ?is_tz_offset(H1,H2,M1,M2) -> tokenise(Rest, Acc); % Tue Nov 11 15:03:18 +0000 2014 Twitter format tokenise([$+| Rest], Acc) -> tokenise(Rest, [$+ | Acc]); % 2012-12-12T12:12:12.xxxx+ ISO formatting. @@ -972,8 +986,32 @@ zulu_test_() -> format_iso8601_test_() -> [ - ?_assertEqual("2001-03-10T17:16:17Z", format_iso8601(?DATE)), - ?_assertEqual("2001-03-10T17:16:17.123456Z", format_iso8601(?DATEMS)) + ?_assertEqual("2001-03-10T17:16:17Z", + format_iso8601({{2001,3,10},{17,16,17}})), + ?_assertEqual("2001-03-10T17:16:17.000000Z", + format_iso8601({{2001,3,10},{17,16,17,0}})), + ?_assertEqual("2001-03-10T17:16:17.123456Z", + format_iso8601({{2001,3,10},{17,16,17,123456}})), + ?_assertEqual("2001-03-10T17:16:17.000456Z", + format_iso8601({{2001,3,10},{17,16,17,456}})), + ?_assertEqual("2001-03-10T17:16:17.123000Z", + format_iso8601({{2001,3,10},{17,16,17,123000}})) + ]. + +parse_iso8601_test_() -> + [ + ?_assertEqual({{2001,3,10},{17,16,17}}, + parse("2001-03-10T17:16:17Z")), + ?_assertEqual({{2001,3,10},{17,16,17,0}}, + parse("2001-03-10T17:16:17.000Z")), + ?_assertEqual({{2001,3,10},{17,16,17,0}}, + parse("2001-03-10T17:16:17.000000Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123456}}, + parse("2001-03-10T17:16:17.123456Z")), + ?_assertEqual({{2001,3,10},{17,16,17,456}}, + parse("2001-03-10T17:16:17.000456Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123000}}, + parse("2001-03-10T17:16:17.123000Z")) ]. -endif. From 5d729253d37717b260ef3d6b28a07f4e058005bc Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 19 Aug 2016 14:09:40 +0200 Subject: [PATCH 6/7] Add one more parsing test (just 3 places after the comma) --- src/ec_date.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ec_date.erl b/src/ec_date.erl index aebb429..d371874 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -1010,6 +1010,8 @@ parse_iso8601_test_() -> parse("2001-03-10T17:16:17.123456Z")), ?_assertEqual({{2001,3,10},{17,16,17,456}}, parse("2001-03-10T17:16:17.000456Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123000}}, + parse("2001-03-10T17:16:17.123Z")), ?_assertEqual({{2001,3,10},{17,16,17,123000}}, parse("2001-03-10T17:16:17.123000Z")) ]. From a91c96eb92700244dc7ffd324e97278aaae1c454 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Mon, 22 Aug 2016 10:28:34 +0200 Subject: [PATCH 7/7] Support ISO 8601 fractions of a seconds up to 6 places after the comma --- src/ec_date.erl | 50 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/ec_date.erl b/src/ec_date.erl index d371874..edb091a 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -312,14 +312,22 @@ parse(_Tokens, _Now, _Opts) -> tokenise([], Acc) -> lists:reverse(Acc); -%% ISO 8601 fractions of a second: only 3 or 6 places after comma, -%% i.e. milli- or microseconds +%% ISO 8601 fractions of a second tokenise([$., N1, N2, N3, N4, N5, N6 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]), $. | Acc]); -tokenise([$., N1, N2, N3 | Rest], Acc) - when ?is_num(N1), ?is_num(N2), ?is_num(N3) -> - tokenise(Rest, [ ltoi([N1, N2, N3, $0, $0, $0]), $. | Acc]); +tokenise([$., N1, N2, N3, N4, N5 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5) -> + tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5]) * 10, $. | Acc]); +tokenise([$., N1, N2, N3, N4 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4) -> + tokenise(Rest, [ ltoi([N1, N2, N3, N4]) * 100, $. | Acc]); +tokenise([$., N1, N2, N3 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3) -> + tokenise(Rest, [ ltoi([N1, N2, N3]) * 1000, $. | Acc]); +tokenise([$., N1, N2 | Rest], Acc) when ?is_num(N1), ?is_num(N2) -> + tokenise(Rest, [ ltoi([N1, N2]) * 10000, $. | Acc]); +tokenise([$., N1 | Rest], Acc) when ?is_num(N1) -> + tokenise(Rest, [ ltoi([N1]) * 100000, $. | Acc]); tokenise([N1, N2, N3, N4, N5, N6 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> @@ -990,12 +998,28 @@ format_iso8601_test_() -> format_iso8601({{2001,3,10},{17,16,17}})), ?_assertEqual("2001-03-10T17:16:17.000000Z", format_iso8601({{2001,3,10},{17,16,17,0}})), + ?_assertEqual("2001-03-10T17:16:17.100000Z", + format_iso8601({{2001,3,10},{17,16,17,100000}})), + ?_assertEqual("2001-03-10T17:16:17.120000Z", + format_iso8601({{2001,3,10},{17,16,17,120000}})), + ?_assertEqual("2001-03-10T17:16:17.123000Z", + format_iso8601({{2001,3,10},{17,16,17,123000}})), + ?_assertEqual("2001-03-10T17:16:17.123400Z", + format_iso8601({{2001,3,10},{17,16,17,123400}})), + ?_assertEqual("2001-03-10T17:16:17.123450Z", + format_iso8601({{2001,3,10},{17,16,17,123450}})), ?_assertEqual("2001-03-10T17:16:17.123456Z", format_iso8601({{2001,3,10},{17,16,17,123456}})), + ?_assertEqual("2001-03-10T17:16:17.023456Z", + format_iso8601({{2001,3,10},{17,16,17,23456}})), + ?_assertEqual("2001-03-10T17:16:17.003456Z", + format_iso8601({{2001,3,10},{17,16,17,3456}})), ?_assertEqual("2001-03-10T17:16:17.000456Z", format_iso8601({{2001,3,10},{17,16,17,456}})), - ?_assertEqual("2001-03-10T17:16:17.123000Z", - format_iso8601({{2001,3,10},{17,16,17,123000}})) + ?_assertEqual("2001-03-10T17:16:17.000056Z", + format_iso8601({{2001,3,10},{17,16,17,56}})), + ?_assertEqual("2001-03-10T17:16:17.000006Z", + format_iso8601({{2001,3,10},{17,16,17,6}})) ]. parse_iso8601_test_() -> @@ -1006,12 +1030,20 @@ parse_iso8601_test_() -> parse("2001-03-10T17:16:17.000Z")), ?_assertEqual({{2001,3,10},{17,16,17,0}}, parse("2001-03-10T17:16:17.000000Z")), + ?_assertEqual({{2001,3,10},{17,16,17,100000}}, + parse("2001-03-10T17:16:17.1Z")), + ?_assertEqual({{2001,3,10},{17,16,17,120000}}, + parse("2001-03-10T17:16:17.12Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123000}}, + parse("2001-03-10T17:16:17.123Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123400}}, + parse("2001-03-10T17:16:17.1234Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123450}}, + parse("2001-03-10T17:16:17.12345Z")), ?_assertEqual({{2001,3,10},{17,16,17,123456}}, parse("2001-03-10T17:16:17.123456Z")), ?_assertEqual({{2001,3,10},{17,16,17,456}}, parse("2001-03-10T17:16:17.000456Z")), - ?_assertEqual({{2001,3,10},{17,16,17,123000}}, - parse("2001-03-10T17:16:17.123Z")), ?_assertEqual({{2001,3,10},{17,16,17,123000}}, parse("2001-03-10T17:16:17.123000Z")) ].