Packages

An ISO 8601 date formatting and parsing library for Erlang

Current section

Files

Jump to
iso8601 src iso8601.erl
Raw

src/iso8601.erl

-module(iso8601).
-export([
add_time/4,
add_days/2,
add_months/2,
add_years/2,
subtract_time/4,
format/1,
parse/1,
parse_exact/1,
parse_duration/1,
apply_duration/2,
parse_interval/1,
interval_bounds/1,
format_interval/1,
parse_expanded/1,
format_expanded/1
]).
-define(V, proplists:get_value).
%%----------------------------------------------------------------------
%% Types
%%----------------------------------------------------------------------
-export_type([timestamp/0, year/0, month/0, day/0, hour/0, minute/0, second/0]).
-export_type([interval/0, duration/0]).
-export_type([expanded_year/0, expanded_datetime/0]).
%% This group of types is from calendar.erl; we need to use some of them
%% for this lib, but thought it might be nice to provide the rest to users.
-type year() :: non_neg_integer().
-type month() :: 1..12.
-type day() :: 1..31.
-type hour() :: 0..23.
-type minute() :: 0..59.
-type second() :: 0..59.
-type datetime() :: {calendar:date(), {hour(), minute(), second() | float()}}.
-type datetime_plist() :: list({atom(), integer() | string()}).
-type undefined_or(A) :: undefined | A.
-type timestamp() :: {MegaSecs :: integer(), Secs :: integer(), MicroSecs :: integer() | float()}.
-type duration() :: datetime_plist().
-type interval() ::
{interval, start_end, datetime(), datetime()}
| {interval, start_duration, datetime(), duration()}
| {interval, duration_end, duration(), datetime()}
| {interval, duration, duration()}.
-type expanded_year() :: integer().
-type expanded_datetime() ::
{{expanded_year(), month(), day()}, {hour(), minute(), second() | float()}}.
%%----------------------------------------------------------------------
%% API
%%----------------------------------------------------------------------
-spec add_time(calendar:datetime(), integer(), integer(), integer()) ->
calendar:datetime().
%% @doc Add some time to the supplied `calendar:datetime()'.
add_time({_, _, _} = Timestamp, H, M, S) ->
add_time(calendar:now_to_datetime(Timestamp), H, M, S);
add_time(Datetime, H, M, S) ->
apply_offset(Datetime, H, M, S).
-spec add_days(datetime() | timestamp(), integer()) -> datetime().
%% @doc Add some days to the supplied `datetime()'.
add_days({_, _, _} = Timestamp, D) ->
add_days(calendar:now_to_datetime(Timestamp), D);
add_days(Datetime, D) ->
apply_days_offset(Datetime, D).
-spec add_months(datetime() | timestamp(), integer()) -> datetime().
%% @doc Add some months to the supplied `datetime()'.
add_months({_, _, _} = Timestamp, M) ->
add_months(calendar:now_to_datetime(Timestamp), M);
add_months(Datetime, M) ->
apply_months_offset(Datetime, M).
-spec add_years(datetime() | timestamp(), integer()) -> datetime().
%% @doc Add some years to the supplied `datetime()'.
add_years({_, _, _} = Timestamp, Y) ->
add_years(calendar:now_to_datetime(Timestamp), Y);
add_years(Datetime, Y) ->
apply_years_offset(Datetime, Y).
-spec subtract_time(calendar:datetime(), integer(), integer(), integer()) ->
calendar:datetime().
%% @doc Subtract some time from the supplied `calendar:datetime()'.
subtract_time(Datetime, H, M, S) ->
apply_offset(Datetime, -H, -M, -S).
-spec format(datetime() | timestamp()) -> binary().
%% @doc Convert a `util:timestamp()' or a calendar-style `{date(), time()}'
%% tuple to an ISO 8601 formatted string. Note that this function always
%% returns a string with no offset (i.e., ending in "Z").
format({_, _, _} = Timestamp) ->
format(calendar:now_to_datetime(Timestamp));
format({{Y, Mo, D}, {H, Mn, S}}) when is_float(S) ->
FmtStr = "~s-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~9.6.0fZ",
IsoStr = io_lib:format(FmtStr, [format_year(Y), Mo, D, H, Mn, S]),
list_to_binary(IsoStr);
format({{Y, Mo, D}, {H, Mn, S}}) ->
FmtStr = "~s-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
IsoStr = io_lib:format(FmtStr, [format_year(Y), Mo, D, H, Mn, S]),
list_to_binary(IsoStr).
-spec format_expanded(expanded_datetime()) -> binary().
%% @doc Format an expanded datetime (including negative years) as ISO 8601.
format_expanded({{Y, Mo, D}, {H, Mn, S}}) when is_float(S) ->
FmtStr = "~s-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~9.6.0fZ",
IsoStr = io_lib:format(FmtStr, [format_expanded_year(Y), Mo, D, H, Mn, S]),
list_to_binary(IsoStr);
format_expanded({{Y, Mo, D}, {H, Mn, S}}) ->
FmtStr = "~s-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
IsoStr = io_lib:format(FmtStr, [format_expanded_year(Y), Mo, D, H, Mn, S]),
list_to_binary(IsoStr).
-spec parse(iodata()) -> calendar:datetime().
%% @doc Convert an ISO 8601 formatted string to a `{date(), time()}'
parse(Bin) when is_binary(Bin) ->
parse(binary_to_list(Bin));
parse(Str) ->
{{Date, {H, M, S}}, Subsecond} = year(Str, [], positive_only),
{Date, {H, M, S + trunc(Subsecond)}}.
-spec parse_exact(iodata()) -> calendar:datetime().
%% @doc Convert an ISO 8601 formatted string to a `{date(), time()}'
%% tuple with seconds precision to 3 decimal places
parse_exact(Bin) when is_binary(Bin) ->
parse_exact(binary_to_list(Bin));
parse_exact(Str) ->
{{Date, {H, M, S}}, SecondsDecimal} = year(Str, [], positive_only),
{Date, {H, M, S + SecondsDecimal}}.
-spec parse_expanded(iodata()) -> expanded_datetime().
%% @doc Parse an ISO 8601 string with expanded year representation,
%% including negative (astronomical) years. Preserves fractional seconds.
%% Negative years are supported in UTC (`Z') or offset-free form; combining
%% a negative year with a non-zero UTC offset or a week-date raises `badarg'.
parse_expanded(Bin) when is_binary(Bin) ->
parse_expanded(binary_to_list(Bin));
parse_expanded(Str) ->
{{Date, {H, M, S}}, SecondsDecimal} = year(Str, [], allow_neg),
{Date, {H, M, S + SecondsDecimal}}.
-spec gi(string()) -> integer().
%% @doc Get the integer part of a string, or 0 for an empty capture.
gi(DS) ->
{Int, _Rest} = string:to_integer(DS),
case Int of
error -> 0;
_ -> Int
end.
-spec parse_duration(string()) -> datetime_plist().
%% @doc Convert an ISO 8601 Durations string to a
%TODO extended format
parse_duration(Bin) when is_binary(Bin) ->
parse_duration(binary_to_list(Bin));
parse_duration(Str) ->
case
re:run(
Str,
"^(?<sign>-|\\+)?P"
"(?:(?<years>[0-9]+)Y)?"
"(?:(?<months>[0-9]+)M)?"
"(?:(?<days>[0-9]+)D)?"
"(T(?:(?<hours>[0-9]+)H)?"
"(?:(?<minutes>[0-9]+)M)?"
"(?:(?<seconds>[0-9]+(?:\\.[0-9]+)?)S)?)?$",
[{capture, [sign, years, months, days, hours, minutes, seconds], list}]
)
of
{match, [Sign, Years, Months, Days, Hours, Minutes, Seconds]} ->
[
{sign, Sign},
{years, gi(Years)},
{months, gi(Months)},
{days, gi(Days)},
{hours, gi(Hours)},
{minutes, gi(Minutes)},
{seconds, gi(Seconds)}
];
nomatch ->
error(badarg)
end.
-spec apply_duration(datetime(), string()) -> datetime().
%% @doc Return new datetime after apply duration.
apply_duration(Datetime, Duration) ->
apply_duration_plist(Datetime, parse_duration(Duration), forward).
-spec parse_interval(iodata()) -> interval().
%% @doc Parse an ISO 8601 time interval string into its structured form.
parse_interval(Bin) when is_binary(Bin) ->
parse_interval(binary_to_list(Bin));
parse_interval(Str) ->
case split_interval(Str) of
{Left, Right} ->
classify_interval(Left, Right);
single ->
case is_duration_str(Str) of
true -> {interval, duration, parse_duration(Str)};
false -> error(badarg)
end
end.
-spec interval_bounds(interval()) -> {datetime(), datetime()}.
%% @doc Resolve an interval to concrete `{Start, End}' datetimes.
interval_bounds({interval, start_end, S, E}) ->
{S, E};
interval_bounds({interval, start_duration, S, D}) ->
{S, apply_duration_plist(S, D, forward)};
interval_bounds({interval, duration_end, D, E}) ->
{apply_duration_plist(E, D, backward), E};
interval_bounds({interval, duration, _}) ->
error(badarg).
-spec format_interval(interval()) -> binary().
%% @doc Format an interval as an ISO 8601 binary string.
format_interval({interval, start_end, S, E}) ->
iolist_to_binary([format(S), $/, format(E)]);
format_interval({interval, start_duration, S, D}) ->
iolist_to_binary([format(S), $/, format_duration(D)]);
format_interval({interval, duration_end, D, E}) ->
iolist_to_binary([format_duration(D), $/, format(E)]);
format_interval({interval, duration, D}) ->
iolist_to_binary(format_duration(D)).
%% Private functions
year([$- | Rest], Acc, allow_neg) ->
expanded_year(-1, Rest, Acc);
year([$- | _], _, positive_only) ->
error(badarg);
year([$+ | Rest], Acc, _) ->
expanded_year(1, Rest, Acc);
year([Y1, Y2, Y3, Y4 | Rest], Acc, _) ->
acc([Y1, Y2, Y3, Y4], Rest, year, Acc, fun month_or_week/2);
year(_, _, _) ->
error(badarg).
expanded_year(Sign, Str, Acc) ->
F = fun(C) -> C >= $0 andalso C =< $9 end,
{Digits, Rest} = lists:splitwith(F, Str),
case Digits of
[] -> error(badarg);
_ -> month_or_week(Rest, [{year, Sign * list_to_integer(Digits)} | Acc])
end.
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([$-, D1, D2, D3], Acc) ->
acc_ordinal_date(D1, D2, D3, [], Acc, fun hour/2);
month_or_week([$-, D1, D2, D3, $T | Rest], Acc) ->
acc_ordinal_date(D1, D2, D3, [$T | Rest], Acc, fun hour/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(_, _) ->
error(badarg).
week_day([], Acc) ->
datetime(Acc);
week_day([$-, D | Rest], Acc) ->
acc([D], Rest, week_day, Acc, fun hour/2);
week_day(_, _) ->
error(badarg).
week_day_no_hyphen([], Acc) ->
datetime(Acc);
week_day_no_hyphen([D | Rest], Acc) ->
acc([D], Rest, week_day, Acc, fun hour/2).
month_day([], Acc) ->
datetime(Acc);
month_day([$-, D1, D2 | Rest], Acc) ->
acc([D1, D2], Rest, month_day, Acc, fun hour/2);
month_day(_, _) ->
error(badarg).
month_day_no_hyphen([], _) ->
% omission of day disallowed by spec in this case
error(badarg);
month_day_no_hyphen([D1, D2 | Rest], Acc) ->
acc([D1, D2], Rest, month_day, Acc, fun hour/2);
month_day_no_hyphen(_, _) ->
error(badarg).
hour([], Acc) ->
datetime(Acc);
hour([$T, H1, H2, $. | Rest], Acc) ->
acc([H1, H2], Rest, hour, Acc, fun hour_decimal/2);
hour([$T, H1, H2, $, | Rest], Acc) ->
acc([H1, H2], Rest, hour, Acc, fun hour_decimal/2);
hour([$T, H1, H2 | Rest], Acc) ->
acc([H1, H2], Rest, hour, Acc, fun minute/2);
hour(_, _) ->
error(badarg).
hour_decimal(Str, Acc) ->
decimal(Str, Acc, hour_decimal).
minute([], Acc) ->
datetime(Acc);
minute([$:, M1, M2, $. | Rest], Acc) ->
acc([M1, M2], Rest, minute, Acc, fun minute_decimal/2);
minute([$:, M1, M2, $, | Rest], Acc) ->
acc([M1, M2], Rest, minute, Acc, fun minute_decimal/2);
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 minute_decimal/2);
minute([M1, M2, $, | Rest], Acc) ->
acc([M1, M2], Rest, minute, Acc, fun minute_decimal/2);
minute([M1, M2 | Rest], Acc) ->
acc([M1, M2], Rest, minute, Acc, fun second_no_colon/2);
minute(_, _) ->
error(badarg).
minute_decimal(Str, Acc) ->
decimal(Str, Acc, minute_decimal).
second([], Acc) ->
datetime(Acc);
second([$:, S1, S2, $. | Rest], Acc) ->
acc([S1, S2], Rest, second, Acc, fun second_decimal/2);
second([$:, S1, S2, $, | Rest], Acc) ->
acc([S1, S2], Rest, second, Acc, fun second_decimal/2);
second([$:, S1, S2 | Rest], Acc) ->
acc([S1, S2], Rest, second, Acc, fun offset_hour/2);
second(_, _) ->
error(badarg).
second_no_colon([], Acc) ->
datetime(Acc);
second_no_colon([S1, S2, $. | Rest], Acc) ->
acc([S1, S2], Rest, second, Acc, fun second_decimal/2);
second_no_colon([S1, S2, $, | Rest], Acc) ->
acc([S1, S2], Rest, second, Acc, fun second_decimal/2);
second_no_colon([S1, S2 | Rest], Acc) ->
acc([S1, S2], Rest, second, Acc, fun offset_hour/2);
second_no_colon(_, _) ->
error(badarg).
second_decimal(Str, Acc) ->
decimal(Str, Acc, second_decimal).
decimal([], _, _) ->
error(badarg);
decimal(Str, Acc, Key) ->
F = fun
(X) when is_integer(X), X >= $0, X =< $9 ->
true;
(_) ->
false
end,
{Parts, Rest} = lists:splitwith(F, Str),
acc_float([$0, $. | Parts], Rest, Key, Acc, fun offset_hour/2).
offset_hour([], Acc) ->
datetime(Acc);
offset_hour([$Z], Acc) ->
datetime(Acc);
offset_hour([$+, H1, H2 | Rest], Acc) ->
acc([H1, H2], Rest, offset_hour, Acc, fun offset_minute/2);
offset_hour([$-, H1, H2 | Rest], Acc) ->
acc([H1, H2], Rest, offset_hour, [{offset_sign, -1} | Acc], fun offset_minute/2);
offset_hour(_, _) ->
error(badarg).
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(_, _) ->
error(badarg).
acc(IntStr, Rest, Key, Acc, NextF) ->
Acc1 = [{Key, list_to_integer(IntStr)} | Acc],
NextF(Rest, Acc1).
acc_float(FloatStr, Rest, Key, Acc, NextF) ->
Acc1 = [{Key, list_to_float(FloatStr)} | Acc],
NextF(Rest, Acc1).
acc_ordinal_date(D1, D2, D3, Rest, Acc, NextF) ->
Days = list_to_integer([D1, D2, D3]),
Days > 0 orelse error(badarg),
Year = ?V(year, Acc),
Year =/= undefined orelse error(badarg),
DaysInMonths = days_in_months_for_year(Year),
{Month, Day} = unpack_ordinal_date(Days, DaysInMonths),
Acc1 = [{month, Month}, {month_day, Day} | Acc],
NextF(Rest, Acc1).
unpack_ordinal_date(Days, DaysInMonths) -> unpack_ordinal_date(1, Days, DaysInMonths).
unpack_ordinal_date(_Month, _Days, []) ->
error(badarg);
unpack_ordinal_date(Month, Days, [DaysThisMonth | DaysInMonths]) ->
case Days > DaysThisMonth of
true -> unpack_ordinal_date(Month + 1, Days - DaysThisMonth, DaysInMonths);
_ -> {Month, Days}
end.
days_in_months_for_year(Year) ->
case is_leap_year(Year) of
true -> [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
false -> [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
end.
is_leap_year(Year) ->
case Year rem 100 of
0 -> Year rem 400 =:= 0;
_ -> Year rem 4 =:= 0
end.
add_decimal(Datetime, Plist) ->
HDecimal = ?V(hour_decimal, Plist, 0.0),
MDecimal = ?V(minute_decimal, Plist, 0.0),
apply_offset(Datetime, HDecimal, MDecimal, 0.0).
datetime(Plist) ->
{Date, WeekOffsetH} = make_date(Plist),
Time = {?V(hour, Plist, 0), ?V(minute, Plist, 0), ?V(second, Plist, 0)},
Datetime = add_decimal({Date, Time}, Plist),
OffsetSign = ?V(offset_sign, Plist, 1),
OffsetH = -1 * OffsetSign * ?V(offset_hour, Plist, 0),
OffsetM = -1 * OffsetSign * ?V(offset_minute, Plist, 0),
{apply_offset(Datetime, WeekOffsetH + OffsetH, OffsetM, 0), ?V(second_decimal, Plist, 0.0)}.
datetime(_, Plist) ->
datetime(Plist).
-spec make_date(datetime_plist()) ->
{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 error(badarg),
make_date(Year, ?V(month, Plist, 1), ?V(week, Plist), Plist).
-spec make_date(
non_neg_integer(),
undefined_or(pos_integer()),
undefined_or(pos_integer()),
datetime_plist()
) ->
{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) ->
Weekday = ?V(week_day, Plist, 1),
% week/weekday offset in hours
OffsetH = ((Week - 1) * 7 + (Weekday - 1)) * 24,
{date_at_w01_1(Year), OffsetH}.
-spec date_at_w01_1(pos_integer()) -> calendar:date().
%% @doc Calculate the `calendar:date()' at ISO week 1, day 1 in the supplied
%% year.
date_at_w01_1(Year) ->
case calendar:day_of_the_week({Year, 1, 1}) of
1 -> {Year, 1, 1};
2 -> {Year - 1, 12, 31};
3 -> {Year - 1, 12, 30};
4 -> {Year - 1, 12, 29};
5 -> {Year, 1, 4};
6 -> {Year, 1, 3};
7 -> {Year, 1, 2}
end.
-spec apply_offset(calendar:datetime(), number(), number(), number()) ->
calendar:datetime().
%% @doc Add the specified number of hours, minutes and seconds to `Datetime'.
apply_offset({{Y, _, _}, _} = Datetime, H, M, S) ->
OffsetS = S + (60 * (M + (60 * H))),
Rounded = round(OffsetS),
case {Y < 1, Rounded} of
{false, _} ->
Gs = Rounded + calendar:datetime_to_gregorian_seconds(Datetime),
calendar:gregorian_seconds_to_datetime(Gs);
{true, 0} ->
Datetime;
{true, _} ->
error(badarg)
end.
-spec apply_months_offset(datetime(), number()) -> datetime().
%% @doc Add the specified number of months to `Datetime'.
apply_months_offset(Datetime, 0) ->
Datetime;
apply_months_offset(Datetime, AM) ->
{{Y, M, D}, {H, MM, S}} = Datetime,
AY = (Y * 12) + M + AM,
Year = ((AY - 1) div 12),
Month =
case (AY rem 12) of
0 -> 12;
Result -> Result
end,
find_last_valid_date({{Year, Month, D}, {H, MM, S}}).
-spec apply_days_offset(datetime(), number()) -> datetime().
%% @doc Add the specified days to `Datetime'.
apply_days_offset(Datetime, AD) ->
{{Y, M, D}, {H, MM, S}} = Datetime,
DaysTotal = calendar:date_to_gregorian_days({Y, M, D}) + AD,
{calendar:gregorian_days_to_date(DaysTotal), {H, MM, S}}.
-spec apply_years_offset(datetime(), number()) -> datetime().
%% @doc Add the specified years to `Datetime'.
apply_years_offset(Datetime, AY) ->
{{Y, M, D}, {H, MM, S}} = Datetime,
find_last_valid_date({{Y + AY, M, D}, {H, MM, S}}).
-spec find_last_valid_date(datetime()) -> datetime().
%% @doc Decrease days until found valid date'.
find_last_valid_date(Datetime) ->
{{Y, M, D}, {H, MM, S}} = Datetime,
case calendar:valid_date({Y, M, D}) of
true -> Datetime;
false -> find_last_valid_date({{Y, M, D - 1}, {H, MM, S}})
end.
format_year(Y) when Y >= 0, Y =< 9999 ->
io_lib:format("~4.10.0B", [Y]);
format_year(Y) when Y > 9999 ->
[$+ | integer_to_list(Y)].
format_expanded_year(Y) when Y >= 0 ->
[$+ | pad_year(integer_to_list(Y))];
format_expanded_year(Y) ->
[$- | pad_year(integer_to_list(abs(Y)))].
pad_year(S) when length(S) >= 4 -> S;
pad_year(S) -> pad_year([$0 | S]).
%%----------------------------------------------------------------------
%% Interval helpers (private)
%%----------------------------------------------------------------------
apply_duration_plist(Datetime, Plist, Direction) ->
[
{sign, Sign},
{years, Y},
{months, M},
{days, D},
{hours, H},
{minutes, MM},
{seconds, SS}
] = Plist,
Negate = (Sign =:= "-") xor (Direction =:= backward),
F =
case Negate of
true -> fun(X) -> -X end;
false -> fun(X) -> X end
end,
D1 = apply_years_offset(Datetime, F(Y)),
D2 = apply_months_offset(D1, F(M)),
D3 = apply_days_offset(D2, F(D)),
apply_offset(D3, F(H), F(MM), F(SS)).
split_interval(Str) ->
case string:split(Str, "/", all) of
[Left, Right] when Left =/= [], Right =/= [] -> {Left, Right};
[_Single] -> split_double_hyphen(Str);
_ -> error(badarg)
end.
split_double_hyphen(Str) ->
case split_on_double_hyphen(Str) of
{[], _} -> error(badarg);
{_, []} -> error(badarg);
{Left, Right} -> {Left, Right};
none -> single
end.
split_on_double_hyphen(Str) ->
split_on_double_hyphen(Str, []).
split_on_double_hyphen([$-, $- | Right], Acc) ->
{lists:reverse(Acc), Right};
split_on_double_hyphen([C | Rest], Acc) ->
split_on_double_hyphen(Rest, [C | Acc]);
split_on_double_hyphen([], _Acc) ->
none.
is_duration_str(Str) ->
case strip_sign(Str) of
[$P | _] -> true;
_ -> false
end.
strip_sign([$+ | Rest]) -> Rest;
strip_sign([$- | Rest]) -> Rest;
strip_sign(Str) -> Str.
classify_interval(Left, Right) ->
LDur = is_duration_str(Left),
RDur = is_duration_str(Right),
case {LDur, RDur} of
{true, true} -> error(badarg);
{false, true} -> {interval, start_duration, parse_endpoint(Left), parse_duration(Right)};
{true, false} -> {interval, duration_end, parse_duration(Left), parse_endpoint(Right)};
{false, false} -> parse_start_end(Left, Right)
end.
parse_endpoint(Str) ->
case lists:member($., Str) orelse lists:member($,, Str) of
true -> parse_exact(Str);
false -> parse(Str)
end.
parse_start_end(Left, Right) ->
Start = parse_endpoint(Left),
End = resolve_end(Left, Right),
{interval, start_end, Start, End}.
resolve_end(StartStr, EndStr) ->
case is_abbreviated(EndStr) of
false -> parse_endpoint(EndStr);
true -> parse_inherited_end(StartStr, EndStr)
end.
is_abbreviated(Str) ->
case year_prefix(Str) of
true -> false;
false -> true
end.
year_prefix([D1, D2, D3, D4 | _]) when
D1 >= $0,
D1 =< $9,
D2 >= $0,
D2 =< $9,
D3 >= $0,
D3 =< $9,
D4 >= $0,
D4 =< $9
->
true;
year_prefix(_) ->
false.
parse_inherited_end(StartStr, EndStr) ->
case classify_end_fragment(EndStr) of
{time_only, TimePart} ->
DatePart = extract_date_part(StartStr),
parse_endpoint(DatePart ++ "T" ++ TimePart);
{day_and_time, DayStr, TimeStr} ->
YMPart = extract_year_month(StartStr),
parse_endpoint(YMPart ++ "-" ++ DayStr ++ "T" ++ TimeStr);
{month_day, _} ->
YearPart = extract_year(StartStr),
parse_endpoint(YearPart ++ "-" ++ EndStr);
{day_only, _} ->
YMPart = extract_year_month(StartStr),
parse_endpoint(YMPart ++ "-" ++ EndStr);
ambiguous ->
error(badarg)
end.
classify_end_fragment(Str) ->
case lists:member($T, Str) of
true ->
case string:split(Str, "T", all) of
[[], TimePart] ->
{time_only, TimePart};
[DayPart, TimePart] ->
{day_and_time, DayPart, TimePart};
_ ->
error(badarg)
end;
false ->
case lists:member($:, Str) of
true -> {time_only, Str};
false -> classify_date_fragment(Str)
end
end.
classify_date_fragment(Str) ->
case lists:member($-, Str) of
true ->
{month_day, Str};
false ->
case Str of
[_, _] -> {day_only, Str};
_ -> ambiguous
end
end.
extract_date_part(Str) ->
[Date | _] = string:split(Str, "T"),
Date.
extract_year(Str) ->
lists:sublist(Str, 4).
extract_year_month(Str) ->
lists:sublist(Str, 7).
format_duration(Plist) ->
Sign = proplists:get_value(sign, Plist, ""),
Y = proplists:get_value(years, Plist, 0),
Mo = proplists:get_value(months, Plist, 0),
D = proplists:get_value(days, Plist, 0),
H = proplists:get_value(hours, Plist, 0),
Mi = proplists:get_value(minutes, Plist, 0),
S = proplists:get_value(seconds, Plist, 0),
DateParts =
[
integer_to_list(V) ++ U
|| {V, U} <- [{Y, "Y"}, {Mo, "M"}, {D, "D"}], V > 0
],
TimeParts =
[
integer_to_list(V) ++ U
|| {V, U} <- [{H, "H"}, {Mi, "M"}, {S, "S"}], V > 0
],
TimeSec =
case TimeParts of
[] -> "";
_ -> "T" ++ lists:flatten(TimeParts)
end,
lists:flatten([Sign, "P", lists:flatten(DateParts), TimeSec]).