-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]). -define(MIDNIGHT, {0,0,0}). -define(NOON, {12,0,0}). -define(TEATIME, {16,0,0}). -define(V, proplists:get_value). %%---------------------------------------------------------------------- %% Types %%---------------------------------------------------------------------- -export_type([timestamp/0, year/0, month/0, day/0, hour/0, minute/0, second/0]). -type option(T) :: undefined | T. %% 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()}). -type timestamp() :: {MegaSecs::integer(), Secs::integer(), MicroSecs::integer() | 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 = "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~9.6.0fZ", IsoStr = io_lib:format(FmtStr, [Y, Mo, D, H, Mn, S]), list_to_binary(IsoStr); format({{Y,Mo,D}, {H,Mn,S}}) -> FmtStr = "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ", IsoStr = io_lib:format(FmtStr, [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, []), {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, []), {Date, {H, M, S + SecondsDecimal}}. -spec gi(string()) ->integer(). %doc get string and return integer part or 0 on error 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 parse_duration(Bin) when is_binary(Bin)-> %TODO extended format 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) -> [{sign, _S}, {years, Y}, {months, M}, {days, D}, {hours, H}, {minutes, MM}, {seconds, SS}] = parse_duration(Duration), D1 = apply_years_offset(Datetime, Y), D2 = apply_months_offset(D1, M), D3 = apply_days_offset(D2, D), apply_offset(D3, H, MM, SS). %% Private functions year([Y1,Y2,Y3,Y4|Rest], Acc) -> acc([Y1,Y2,Y3,Y4], Rest, year, Acc, fun month_or_week/2); year(_, _) -> error(badarg). 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) -> %% ordinal date, no time io:format("Ordinal date, no time!~n"), acc_ordinal_date(D1, D2, D3, [], Acc, fun hour/2); month_or_week([$-,D1,D2,D3,$T|Rest], Acc) -> %% ordinal date with time io:format("Ordinal date, time is ~p!~n", [Rest]), 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); week_day_no_hyphen(_, _) -> 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(_, _) -> error(badarg). month_day_no_hyphen([], _) -> 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(_, _) -> 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), { 0, 0 }; unpack_ordinal_date(_Month, Days, _DaysInMonths) when Days < 0 -> error(badarg), { 0, 0 }; 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(), option(pos_integer()), option(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), OffsetH = ((Week-1)*7 + (Weekday-1))*24, % week/weekday offset in hours {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(Datetime, H, M, S) -> OffsetS = S + (60 * (M + (60 * H))), Gs = round(OffsetS) + calendar:datetime_to_gregorian_seconds(Datetime), calendar:gregorian_seconds_to_datetime(Gs). -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, {{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.