-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, "^(?-|\\+)?P" "(?:(?[0-9]+)Y)?" "(?:(?[0-9]+)M)?" "(?:(?[0-9]+)D)?" "(T(?:(?[0-9]+)H)?" "(?:(?[0-9]+)M)?" "(?:(?[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]).