Skip to content

Commit

Permalink
Support for omission of hyphens and colons, beginnings of week support
Browse files Browse the repository at this point in the history
  • Loading branch information
seansawyer committed Jan 13, 2012
1 parent 926bf27 commit c11b5d9
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.eunit
ebin/*.beam
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ Formats and parses ISO 8601 dates.

## Known deficiencies ##

Does not yet support week-of-year format.
* Does not support expanded year representation.
* Does not support week/weekday format.
* Does not support fractional times.
* Does not support ordinal dates.
* Does not support intervals.

See the [open issues](https://github.com/seansawyer/erlang_iso8601/issues)
for more info.
103 changes: 86 additions & 17 deletions src/iso8601.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@
-define(MIDNIGHT, {0,0,0}).
-define(V, proplists:get_value).

-type datetime() :: tuple(calendar:date(), calendar:time()).
-type timestamp() :: tuple(integer(), integer(), integer()).

-type datetime() :: tuple(Date::calendar:date(),
Time::calendar:time()).
-type datetime_plist() :: list(tuple(atom(), integer())).
-type maybe(A) :: undefined | A.
-type timestamp() :: tuple(MegaSecs::integer(),
Secs::integer(),
MicroSecs::integer()).

%% API

Expand Down Expand Up @@ -41,22 +47,49 @@ parse(Str) ->
%% Private functions

year([Y1,Y2,Y3,Y4|Rest], Acc) ->
acc([Y1,Y2,Y3,Y4], Rest, year, Acc, fun month/2);
acc([Y1,Y2,Y3,Y4], Rest, year, Acc, fun month_or_week/2);
year(_, _) ->
erlang:error(badarg).

month([], Acc) ->
month_or_week([], Acc) ->
datetime(Acc);
month_or_week([$-,$W,W1,W2|Rest], Acc) ->
acc([W1,W2], Rest, week, Acc, fun week_day/2);
month_or_week([$-,M1,M2|Rest], Acc) ->
acc([M1,M2], Rest, month, Acc, fun month_day/2);
month_or_week([$W,W1,W2|Rest], Acc) ->
acc([W1,W2], Rest, week, Acc, fun week_day_no_hyphen/2);
month_or_week([M1,M2|Rest], Acc) ->
acc([M1,M2], Rest, month, Acc, fun month_day_no_hyphen/2);
month_or_week(_, _) ->
erlang:error(badarg).

week_day([], Acc) ->
datetime(Acc);
month([$-,M1,M2|Rest], Acc) ->
acc([M1,M2], Rest, month, Acc, fun day/2);
month(_, _) ->
week_day([$-,D|Rest], Acc) ->
acc([D], Rest, week_day, Acc, fun hour/2);
week_day(_, _) ->
erlang:error(badarg).

day([], Acc) ->
week_day_no_hyphen([], Acc) ->
datetime(Acc);
day([$-,D1,D2|Rest], Acc) ->
acc([D1,D2], Rest, day, Acc, fun hour/2);
day(_, _) ->
week_day_no_hyphen([D|Rest], Acc) ->
acc([D], Rest, week_day, Acc, fun hour/2);
week_day_no_hyphen(_, _) ->
erlang:error(badarg).

month_day([], Acc) ->
datetime(Acc);
month_day([$-,D1,D2|Rest], Acc) ->
acc([D1,D2], Rest, month_day, Acc, fun hour/2);
month_day(_, _) ->
erlang:error(badarg).

month_day_no_hyphen([], _) ->
erlang:error(badarg); % omission of day disallowed by spec in this case
month_day_no_hyphen([D1,D2|Rest], Acc) ->
acc([D1,D2], Rest, month_day, Acc, fun hour/2);
month_day_no_hyphen(_, _) ->
erlang:error(badarg).

hour([], Acc) ->
Expand All @@ -70,6 +103,8 @@ minute([], Acc) ->
datetime(Acc);
minute([$:,M1,M2|Rest], Acc) ->
acc([M1,M2], Rest, minute, Acc, fun second/2);
minute([M1,M2|Rest], Acc) ->
acc([M1,M2], Rest, minute, Acc, fun second_no_colon/2);
minute(_, _) ->
erlang:error(badarg).

Expand All @@ -80,6 +115,13 @@ second([$:,S1,S2|Rest], Acc) ->
second(_, _) ->
erlang:error(badarg).

second_no_colon([], Acc) ->
datetime(Acc);
second_no_colon([S1,S2|Rest], Acc) ->
acc([S1,S2], Rest, second, Acc, fun offset_hour/2);
second_no_colon(_, _) ->
erlang:error(badarg).

offset_hour([], Acc) ->
datetime(Acc);
offset_hour([$Z], Acc) ->
Expand All @@ -93,10 +135,10 @@ offset_hour(_, _) ->

offset_minute([], Acc) ->
datetime(Acc);
offset_minute([M1,M2], Acc) ->
acc([M1,M2], [], offset_minute, Acc, fun datetime/2);
offset_minute([$:,M1,M2], Acc) ->
acc([M1,M2], [], offset_minute, Acc, fun datetime/2);
offset_minute([M1,M2], Acc) ->
acc([M1,M2], [], offset_minute, Acc, fun datetime/2);
offset_minute(_, _) ->
erlang:error(badarg).

Expand All @@ -105,18 +147,45 @@ acc(IntStr, Rest, Key, Acc, NextF) ->
NextF(Rest, Acc1).

datetime(Plist) ->
Year = ?V(year, Plist),
Year =/= undefined orelse erlang:error(badarg),
Date = {Year, ?V(month, Plist, 1), ?V(day, Plist, 1)},
{Date, WeekOffsetH} = make_date(Plist),
Time = {?V(hour, Plist, 0), ?V(minute, Plist, 0), ?V(second, Plist, 0)},
OffsetSign = ?V(offset_sign, Plist, 1),
OffsetH = OffsetSign * ?V(offset_hour, Plist, 0),
OffsetM = OffsetSign * ?V(offset_minute, Plist, 0),
apply_offset({Date, Time}, OffsetH, OffsetM, 0).
apply_offset({Date, Time}, WeekOffsetH+OffsetH, OffsetM, 0).

datetime(_, Plist) ->
datetime(Plist).

-spec make_date (datetime_plist())
-> tuple(Date::calendar:date(), WeekOffsetH::non_neg_integer()).
%% @doc Return a `tuple' containing a date and, if the date is in week format,
%% an offset in hours that can be applied to the date to adjust it to midnight
%% of the day specified. If month format is used, the offset will be zero.
make_date(Plist) ->
Year = ?V(year, Plist),
Year =/= undefined orelse erlang:error(badarg),
make_date(Year, ?V(month, Plist, 1), ?V(week, Plist), Plist).

-spec make_date (non_neg_integer(),
maybe(pos_integer()),
maybe(pos_integer()),
datetime_plist())
-> tuple(calendar:date(), non_neg_integer()).
%% @doc Return a `tuple' containing a date and - if the date is in week format
%% (i.e., `Month' is undefined, `Week' is not) - an offset in hours that can be
%% applied to the date to adjust it to midnight of the day specified. If month
%% format is used (i.e., `Week' is undefined, `Month' is not), the offset will
%% be zero.
make_date(Year, Month, undefined, Plist) ->
Date = {Year, Month, ?V(month_day, Plist, 1)},
{Date, 0};
make_date(Year, _, Week, Plist) ->
Date = {Year, 1, 1},
Weekday = ?V(week_day, Plist, 1),
OffsetH = ((Week-1)*7 + (Weekday-1))*24, % week/weekday offset in hours
{Date, OffsetH}.

apply_offset(Datetime, H, M, S) ->
OffsetS = S + (60 * (M + (60 * H))),
Gs = OffsetS + calendar:datetime_to_gregorian_seconds(Datetime),
Expand Down
47 changes: 47 additions & 0 deletions test/iso8601_tests.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-module(iso8601_tests).
-compile(export_all).

-include_lib("eunit/include/eunit.hrl").

parse_fail_test_() ->
F = fun iso8601:parse/1,
[{"fails to parse YYYYMM",
?_assertError(badarg, F("201212"))}].

parse_year_test_() ->
F = fun iso8601:parse/1,
[{"parses YYYY",
?_assertMatch({{2012,1,1},{0,0,0}}, F("2012"))}].

parse_month_test_() ->
F = fun iso8601:parse/1,
[{"parses YYYY-MM",
?_assertMatch({{2012,12,1},{0,0,0}}, F("2012-12"))}].

parse_month_day_test_() ->
F = fun iso8601:parse/1,
[{"parses YYYYMMDD",
?_assertMatch({{2012,12,12},{0,0,0}}, F("2012-12-12"))},
{"parses YYYY-MM-DD",
?_assertMatch({{2012,12,12},{0,0,0}}, F("20121212"))}].

parse_hour_test_() ->
F = fun iso8601:parse/1,
[{"parses YYYYMMDDTHH",
?_assertMatch({{2012,2,3},{4,0,0}}, F("20120203T04"))},
{"parses YYYY-MM-DDTHH",
?_assertMatch({{2012,2,3},{4,0,0}}, F("2012-02-03T04"))}].

parse_minute_test_() ->
F = fun iso8601:parse/1,
[{"parses YYYYMMDDTHHMM",
?_assertMatch({{2012,2,3},{4,5,0}}, F("20120203T0405"))},
{"parses YYYY-MM-DDTHH:MM",
?_assertMatch({{2012,2,3},{4,5,0}}, F("2012-02-03T04:05"))}].

parse_second_test_() ->
F = fun iso8601:parse/1,
[{"parses YYYYMMDDTHHMMSS",
?_assertMatch({{2012,2,3},{4,5,6}}, F("20120203T040506"))},
{"parses YYYY-MM-DDTHH:MM:SS",
?_assertMatch({{2012,2,3},{4,5,6}}, F("2012-02-03T04:05:06"))}].

0 comments on commit c11b5d9

Please sign in to comment.