src/support/z_datetime.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2026 Marc Worrell
%% @doc Utility functions for datetime handling and representation.
%% @end

%% Copyright 2009-2026 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(z_datetime).
-author("Marc Worrell <marc@worrell.nl").

%% interface functions
-export([
    format/2,
    format/3,

    format_utc/2,
    format_utc/3,

    to_local/2,
    to_utc/2,

    to_datetime/1,
    to_datetime/2,

    timesince/2,
    timesince/3,
    timesince/4,
    timesince/5,

    week_start/0,
    week_start/2,

    days_in_year/1,

    prev_year/1,
    prev_year/2,
    prev_month/1,
    prev_month/2,
    prev_week/1,
    prev_week/2,
    prev_day/1,
    prev_day/2,
    prev_hour/1,
    prev_hour/2,
    prev_minute/1,
    prev_minute/2,
    prev_second/1,
    prev_second/2,

    next_year/1,
    next_year/2,
    next_month/1,
    next_month/2,
    next_week/1,
    next_week/2,
    next_day/1,
    next_day/2,
    next_hour/1,
    next_hour/2,
    next_minute/1,
    next_minute/2,
    next_second/1,
    next_second/2,

    diff/2,

    month_boundaries/1,
    week_boundaries/1,
    week_boundaries/2,

    timestamp/0,
    msec/0,

    timestamp_to_datetime/1,
    datetime_to_timestamp/1,

    undefined_if_invalid_date/1,
    maybe_fix_datetime/1,

    last_day_of_the_month/2,
    is_leap_year/1
]).


-include_lib("zotonic.hrl").


% A date, same as the calendar:datetime() but with a larger year range.
-type datetime() :: calendar:datetime()
                  | {{integer(),1..12,1..31}, {0..23,0..59,0..59}}.

-type date() :: calendar:date()
              | {integer(),1..12,1..31}.

% Data that looks like a datetime and can be coerced into a datetime.
-type fixable_datetime() :: datetime()
                          | date()
                          | {integer(),integer(),integer()}
                          | {{integer(),integer(),integer()}, {integer()|undefined,integer()|undefined,integer()|undefined}}.

-type timestamp() :: integer().

-export_type([ datetime/0, date/0, fixable_datetime/0, timestamp/0 ]).


%% @doc Format the current date according to the format and the timezone settings in the context.
format(Format, Context) ->
    format(calendar:universal_time(), Format, Context).

%% @doc Format a date according to the format and the timezone settings in the context.
format(Date, Format, #context{} = Context) ->
    z_dateformat:format(to_local(Date, Context), Format, format_opts(Date, z_context:tz(Context), Context)).

%% @doc Format the current date in UTC.
format_utc(Format, Context) ->
    format_utc(calendar:universal_time(), Format, Context).

%% @doc Format the date using the UTC timezone.
format_utc(Date, Format, #context{} = Context) ->
    z_dateformat:format(Date, Format, format_opts(Date, "GMT", Context)).

format_opts(Date, Tz, Context) ->
    [
        {utc, Date},
        {tz, z_convert:to_list(Tz)},
        {tr, {l10n_date, [Context]}}
    ].

%% @doc Convert a time to the local context time using the current timezone.
-spec to_local(DateTime, TimeZone) -> calendar:datetime() | undefined when
    DateTime :: calendar:datetime() | undefined | time_not_exists,
    TimeZone :: string() | binary() | z:context().
to_local(undefined, _Tz) ->
    undefined;
to_local(time_not_exists, _Tz) ->
    undefined;
to_local({_Y, _M, _D} = Date, Tz) ->
    to_local({Date, {0,0,0}}, Tz);
to_local({{9999, _, _}, _} = DT, _Tz) ->
    DT;
to_local({{Y, _, _}, _}, _Tz) when Y > 9999 ->
    ?ST_JUTTEMIS;
to_local(DT, <<"UTC">>) ->
    DT;
to_local(DT, <<"GMT">>) ->
    DT;
to_local(DT, <<>>) ->
    DT;
to_local(DT, #context{} = Context) ->
    to_local(DT, z_context:tz(Context));
to_local(DT, Tz) ->
    try
        case qdate:to_date(z_convert:to_list(Tz), {DT, "GMT"}) of
            {ambiguous, _Standard, Daylight} ->
                Daylight;
            {{_Y, _M, _D}, {_H, _I, _S}} = NewDT ->
                NewDT
        end
    catch
        Type:Reason ->
            ?LOG_WARNING(#{
                text => <<"Error converting date for to_local">>,
                in => zotonic_core,
                tz => Tz,
                date => DT,
                result => Type,
                reason => Reason
            }),
            undefined
    end.

%% @doc Convert a time to the local context time using the current timezone.
-spec to_utc(DateTime, TimeZone) -> calendar:datetime() | undefined when
    DateTime :: calendar:datetime() | undefined | time_not_exists,
    TimeZone :: string() | binary() | z:context().
to_utc(undefined, _Tz) ->
    undefined;
to_utc(time_not_exists, _Tz) ->
    undefined;
to_utc({_Y, _M, _D} = Date, Tz) ->
    to_utc({Date, {0,0,0}}, Tz);
to_utc({{9999, _, _}, _} = DT, _Tz) ->
    DT;
to_utc({{Y, _, _}, _}, _Tz) when Y > 9999 ->
    ?ST_JUTTEMIS;
to_utc({{Y, M, D}, T}, Tz) when Y =< 1 ->
    {{Y1, M1, D1}, T1} = to_utc({{10, M, D}, T}, Tz),
    {{Y1 - 10 + Y, M1, D1}, T1};
to_utc(DT, <<"UTC">>) ->
    DT;
to_utc(DT, <<"GMT">>) ->
    DT;
to_utc(DT, <<>>) ->
    DT;
to_utc(DT, #context{} = Context) ->
    to_utc(DT, z_context:tz(Context));
to_utc(DT, Tz) ->
    try
        case qdate:to_date("GMT", {DT, z_convert:to_list(Tz)}) of
            {ambiguous, _Standard, Daylight} ->
                Daylight;
            {{_Y, _M, _D}, {_H, _I, _S}} = NewDT ->
                NewDT
        end
    catch
        Type:Reason ->
            ?LOG_WARNING(#{
                text => <<"Error converting date for to_utc">>,
                in => zotonic_core,
                tz => Tz,
                date => DT,
                result => Type,
                reason => Reason
            }),
            undefined
    end.


%% @doc Convert an input to a (universal) datetime, using to_date/1 and
%% to_time/1.  When the input is a string, it is expected to be in iso
%% 8601 format, although it can also handle timestamps without time
%% zones. The time component of the datetime is optional.
-spec to_datetime( Input ) -> calendar:datetime() | undefined when
    Input :: undefined
            | binary()
            | string()
            | integer()
            | calendar:datetime()
            | {Y::integer(), M::pos_integer(), D::pos_integer()}.
to_datetime(undefined) ->
    undefined;
to_datetime(N) when is_integer(N) ->
    z_datetime:timestamp_to_datetime(N);
to_datetime(B) when is_binary(B); is_list(B) ->
    case z_utils:only_digits(B) of
        true ->
            to_datetime(z_convert:to_integer(B));
        false ->
            to_dt(B, calendar:universal_time())
    end;
to_datetime(DT) ->
    to_dt(DT, calendar:universal_time()).

to_datetime(DT, Tz) ->
    Now = to_local(calendar:universal_time(), Tz),
    to_utc(to_dt(DT, Now), Tz).

to_dt({{_,_,_},{_,_,_}} = DT, _Now) -> DT;
to_dt({_,_,_} = D, _Now) -> {D, {0,0,0}};
to_dt(<<"now">>, Now) -> Now;
to_dt(<<"today">>, Now) -> Now;
to_dt(<<"tomorrow">>, Now) -> relative_time(1, '+', [<<"day">>], Now);
to_dt(<<"yesterday">>, Now) -> relative_time(1, '+', [<<"day">>], Now);
to_dt(<<"+", Relative/binary>>, Now) -> to_relative_time('+', Relative, Now);
to_dt(<<"-", Relative/binary>>, Now) -> to_relative_time('-', Relative, Now);
to_dt("now", Now) -> Now;
to_dt("today", Now) -> Now;
to_dt("tomorrow", Now) -> relative_time(1, '+', [<<"day">>], Now);
to_dt("yesterday", Now) -> relative_time(1, '+', [<<"day">>], Now);
to_dt("+"++Relative, Now) -> to_relative_time('+', Relative, Now);
to_dt("-"++Relative, Now) -> to_relative_time('-', Relative, Now);
to_dt(DT, _Now) -> z_convert:to_datetime(DT).

to_relative_time(Op, S, Now) when is_list(S) ->
    to_relative_time(Op, z_convert:to_binary(S), Now);
to_relative_time(Op, S, Now) when is_binary(S) ->
    Ts = binary:split(S, <<" ">>, [ global, trim_all ]),
    relative_time(1, Op, Ts, Now).

relative_time(_N, Op, [<<C, _/binary>>=N|Ts], Now) when C >= $0, C =< $9 ->
    relative_time(binary_to_integer(N), Op, Ts, Now);
relative_time(N, '+', [<<"minute", _/binary>>|_], Now) ->    next_minute(Now, N);
relative_time(N, '+', [<<"hour", _/binary>>|_], Now) ->      next_hour(Now, N);
relative_time(N, '+', [<<"day", _/binary>>|_], Now) ->       next_day(Now, N);
relative_time(N, '+', [<<"sunday", _/binary>>|_], Now) ->    next_day(week_start(7, Now), N*7);
relative_time(N, '+', [<<"monday", _/binary>>|_], Now) ->    next_day(week_start(1, Now), N*7);
relative_time(N, '+', [<<"tuesday", _/binary>>|_], Now) ->   next_day(week_start(2, Now), N*7);
relative_time(N, '+', [<<"wednesday", _/binary>>|_], Now) -> next_day(week_start(3, Now), N*7);
relative_time(N, '+', [<<"thursday", _/binary>>|_], Now) ->  next_day(week_start(4, Now), N*7);
relative_time(N, '+', [<<"friday", _/binary>>|_], Now) ->    next_day(week_start(5, Now), N*7);
relative_time(N, '+', [<<"saturday", _/binary>>|_], Now) ->  next_day(week_start(6, Now), N*7);
relative_time(N, '+', [<<"week", _/binary>>|_], Now) ->      next_week(Now, N);
relative_time(N, '+', [<<"month", _/binary>>|_], Now) ->     next_month(Now, N);
relative_time(N, '+', [<<"year", _/binary>>|_], Now) ->      next_year(Now, N);
relative_time(N, '-', [<<"day", _/binary>>|_], Now) ->       prev_day(Now, N);
relative_time(N, '-', [<<"sunday", _/binary>>|_], Now) ->    prev_day(week_start(7, Now), N*7);
relative_time(N, '-', [<<"monday", _/binary>>|_], Now) ->    prev_day(week_start(1, Now), N*7);
relative_time(N, '-', [<<"tuesday", _/binary>>|_], Now) ->   prev_day(week_start(2, Now), N*7);
relative_time(N, '-', [<<"wednesday", _/binary>>|_], Now) -> prev_day(week_start(3, Now), N*7);
relative_time(N, '-', [<<"thursday", _/binary>>|_], Now) ->  prev_day(week_start(4, Now), N*7);
relative_time(N, '-', [<<"friday", _/binary>>|_], Now) ->    prev_day(week_start(5, Now), N*7);
relative_time(N, '-', [<<"week", _/binary>>|_], Now) ->      prev_week(Now, N);
relative_time(N, '-', [<<"month", _/binary>>|_], Now) ->     prev_month(Now, N);
relative_time(N, '-', [<<"year", _/binary>>|_], Now) ->      prev_year(Now, N);
relative_time(_N, _Op, _Unit, _Now) ->         undefined.

%% @doc Show a humanized version of a relative datetime.  Like "4 months, 3 days ago".
-spec timesince(term(), z:context()) -> iodata().
timesince(Date, Context) ->
    timesince(Date, calendar:universal_time(), Context).

%% @doc Show a humanized version of a period between two dates.  Like "4 months, 3 days ago".
-spec timesince(term(), term(), z:context()) -> iodata().
timesince(Date, Base, Context) ->
    timesince(Date, Base, ?__(<<"ago">>, Context), ?__(<<"now">>, Context), ?__(<<"in">>, Context), 2, Context).

timesince(Date, Base, IndicatorStrings, Context) ->
    timesince(Date, Base, IndicatorStrings, 2, Context).

-spec timesince(term(), term(), term(), term(), z:context()) -> iodata().
%% @doc Show a humanized version of a period between two dates.  Like "4 months, 3 days ago".
%% `WhenText' is a string containing a maximum of three tokens. Example "ago, now, in"
timesince(Date, Base, IndicatorStrings, Mode, Context) ->
    %% strip the tokens, so the user can specify the text more flexible.
    case [string:strip(S, both) || S <- string:tokens(z_convert:to_list(IndicatorStrings), ",")] of
    [AgoText, NowText, InText] ->
        timesince(Date, Base, AgoText, NowText, InText, Mode, Context);
    [AgoText, NowText] ->
        timesince(Date, Base, AgoText, NowText, "", Mode, Context);
    [AgoText] ->
        timesince(Date, Base, AgoText, "", "", Mode, Context);
    [] ->
        timesince(Date, Base, "", "", "", Mode, Context)
    end.

%% @doc Show a humanized version of a period between two dates.  Like "4 months, 3 days ago".
-spec timesince(term(), term(), term(), term(), term(), term(), z:context()) -> iodata().
timesince(undefined, _, _AgoText, _NowText, _InText, _Mode, _Context) ->
    "";
timesince(_, undefined, _AgoText, _NowText, _InText, _Mode, _Context) ->
    "";
timesince(Date, Base, _AgoText, NowText, _InText, _Mode, _Context) when Date == Base ->
    NowText;
timesince(Date, Base, _AgoText, _NowText, InText, Mode, Context) when Date > Base ->
    combine({InText, combine(reldate(Base, Date, Mode, Context))}, " ");
timesince(Date, Base, AgoText, _NowText, _InText, Mode, Context) ->
    combine({combine(reldate(Date, Base, Mode, Context)), AgoText}, " ").

combine(Tup) -> combine(Tup, ", ").

combine({"", B},   _Sep) -> B;
combine({A,""},    _Sep) -> A;
combine({<<>>, B}, _Sep) -> B;
combine({A,<<>>},  _Sep) -> A;
combine({A,B}, Sep) -> [A,Sep,B].



%% @doc Return a string describing the relative date difference.
reldate(D1, D2, 1, Context) ->
    {A, _} = reldate(D1,D2, 2, Context),
    {A,[]};
reldate(D1, D2, 2, Context) ->
    case diff(D1,D2) of
        {{0,0,0},{0,0,0}} -> {?__(<<"now">>,Context),[]};
        {{0,0,0},{0,0,S}} when S < 10 -> {?__(<<"moments">>,Context),[]};
        {{0,0,0},{0,0,S}} -> {plural(S, ?__(<<"second">>,Context), ?__(<<"seconds">>,Context)),
                              []};
        {{0,0,0},{0,I,S}} -> {plural(I, ?__(<<"minute">>,Context), ?__(<<"minutes">>,Context)),
                              plural(S, ?__(<<"second">>,Context), ?__(<<"seconds">>,Context))};
        {{0,0,0},{H,I,_}} -> {plural(H, ?__(<<"hour">>,Context),   ?__(<<"hours">>,Context)),
                              plural(I, ?__(<<"minute">>,Context), ?__(<<"minutes">>,Context))};
        {{0,0,D},{H,_,_}} -> {plural(D, ?__(<<"day">>,Context),    ?__(<<"days">>,Context)),
                              plural(H, ?__(<<"hour">>,Context),   ?__(<<"hours">>,Context))};
        {{0,M,D},{_,_,_}} -> {plural(M, ?__(<<"month">>,Context),  ?__(<<"months">>,Context)),
                              plural(D, ?__(<<"day">>,Context),    ?__(<<"days">>,Context))};
        {{Y,M,_},{_,_,_}} -> {plural(Y, ?__(<<"year">>,Context),   ?__(<<"years">>,Context)),
                              plural(M, ?__(<<"month">>,Context),  ?__(<<"months">>,Context))}
    end.


plural(0,_Single,_Plural) ->
    "";
plural(1,Single,_Plural) ->
    [$1, 32, Single];
plural(N,_Single,Plural) ->
    [integer_to_list(N), 32, Plural].


%% @doc Return the date the current week starts (monday)
week_start() ->
    week_start(1, calendar:universal_time()).

week_start(StartDayNr, {D,_}) ->
    Today = {D,{0,0,0}},
    WeekDay = calendar:day_of_the_week(D),
    if
        WeekDay > StartDayNr -> prev_day(Today, WeekDay - StartDayNr);
        WeekDay =:= StartDayNr -> Today;
        WeekDay < StartDayNr -> prev_day(Today, WeekDay - StartDayNr + 7)
    end.

%% @doc Return the date one year earlier.
prev_year({{Y,M,D},T}) ->
    norm_month({{Y-1,M,D}, T});
prev_year({_,_,_} = Date) ->
    prev_year({Date, {0,0,0}}).

prev_year({{Y,M,D}, T}, N) ->
    DT1 = {{Y-N,M,D}, T},
    norm_month(DT1);
prev_year({_,_,_} = Date, N) ->
    prev_year({Date, {0,0,0}}, N).

%% @doc Return the date one month earlier.
prev_month(DT) -> next_month(DT, -1).
prev_month(DT, N) -> next_month(DT, -N).

%% @doc Return the date one week earlier.
prev_week(DT) ->
    prev_week_1(DT, 7).

prev_week_1(DT, 0) -> DT;
prev_week_1(DT, N) ->
    DT1 = prev_day(DT),
    prev_week_1(DT1, N-1).

prev_week(Date, N) ->
    nth(Date, -N, fun next_week/1, fun prev_week/1).

%% @doc Return the date one day earlier.
prev_day({{_,_,1},_} = Date) ->
    {{Y1,M1,_},T1} = prev_month(Date),
    {{Y1,M1,last_day_of_the_month(Y1,M1)}, T1};
prev_day({{Y,M,D},T}) ->
    {{Y,M,D-1}, T};
prev_day({_,_,_} = Date) ->
    prev_day({Date, {0,0,0}}).

prev_day(Date, N) ->
    nth(Date, -N, fun next_day/1, fun prev_day/1).

%% @doc Return the date one hour earlier.
prev_hour({_,{0,_,_}} = Date) ->
    {YMD,{_,I,S}} = prev_day(Date),
    {YMD,{23,I,S}};
prev_hour({YMD,{H,I,S}}) ->
    {YMD, {H-1,I,S}}.

prev_hour(Date, N) ->
    nth(Date, -N, fun next_hour/1, fun prev_hour/1).

%% @doc Return the date one minute earlier.
prev_minute({_,{_,0,_}} = Date) ->
    {YMD,{H,_,S}} = prev_hour(Date),
    {YMD,{H,59,S}};
prev_minute({YMD,{H,I,S}}) ->
    {YMD, {H,I-1,S}}.

prev_minute(Date, N) ->
    nth(Date, -N, fun next_minute/1, fun prev_minute/1).

%% @doc Return the date one second earlier.
prev_second({_,{_,_,0}} = Date) ->
    {YMD,{H,I,_}} = prev_minute(Date),
    {YMD,{H,I,59}};
prev_second({YMD,{H,I,S}}) ->
    {YMD, {H,I,S-1}}.

prev_second(Date, N) ->
    nth(Date, -N, fun next_second/1, fun prev_second/1).

%% @doc Return the date one year later.
next_year({{Y,M,D},T}) ->
    norm_month({{Y+1,M,D}, T});
next_year({_,_,_} = Date) ->
    next_year({Date, {0,0,0}}).

next_year({{Y,M,D}, T}, N) ->
    DT1 = {{Y+N,M,D}, T},
    norm_month(DT1);
next_year({_,_,_} = Date, N) ->
    next_year({Date, {0,0,0}}, N).

%% @doc Return the date one month later. Gives unpredictable results if the
%%      day doesn't exist in the next month. (eg. feb 30 will become feb 28).
next_month(DT) ->
    next_month(DT, 1).

next_month({{Y, M, D}, T}, N) ->
    DT1 = {{Y, M+N, D}, T},
    norm_month(DT1);
next_month({_,_,_} = Date, N) ->
    next_month({Date, {0,0,0}}, N).

%% @doc Move the date so that the month/day number is valid.
norm_month({{Y, M, D}, T}) when M =< 0 ->
    norm_month({{Y-1, M+12, D}, T});
norm_month({{Y, M, D}, T}) when M > 12 ->
    norm_month({{Y+1, M-12, D}, T});
norm_month({{Y, M, D}, T}) ->
    D1 = erlang:min(last_day_of_the_month(Y,M), D),
    {{Y, M, D1}, T}.

%% @doc Return the date one week later.
next_week(DT) ->
    next_week_1(DT, 7).

next_week_1(DT, 0) ->
    DT;
next_week_1(DT, N) ->
    DT1 = next_day(DT),
    next_week_1(DT1, N-1).

next_week(Date, N) ->
    nth(Date, N, fun next_week/1, fun prev_week/1).

%% @doc Return the date one day later.
next_day({{Y,M,D},T} = Date) ->
    case last_day_of_the_month(Y,M) of
        D1 when D1 =< D ->
            {{Y1,M1,_},T1} = next_month(Date),
            {{Y1,M1,1},T1};
        _ ->
            {{Y,M,D+1},T}
    end;
next_day({_,_,_} = Date) ->
    next_day({Date, {0,0,0}}).

next_day(Date, N) ->
    nth(Date, N, fun next_day/1, fun prev_day/1).

%% @doc Return the date one hour later.
next_hour({_,{23,_,_}} = Date) ->
    {YMD,{_,I,S}} = next_day(Date),
    {YMD,{0,I,S}};
next_hour({YMD,{H,I,S}}) ->
    {YMD, {H+1,I,S}}.

next_hour(Date, N) ->
    nth(Date, N, fun next_hour/1, fun prev_hour/1).

%% @doc Return the date one minute later.
next_minute({_,{_,59,_}} = Date) ->
    {YMD,{H,_,S}} = next_hour(Date),
    {YMD,{H,0,S}};
next_minute({YMD,{H,I,S}}) ->
    {YMD, {H,I+1,S}}.

next_minute(Date, N) ->
    nth(Date, N, fun next_minute/1, fun prev_minute/1).

%% @doc Return the date one second later.
next_second({_,{_,_,59}} = Date) ->
    {YMD,{H,I,_}} = next_minute(Date),
    {YMD,{H,I,0}};
next_second({YMD,{H,I,S}}) ->
    {YMD, {H,I,S+1}}.

next_second(Date, N) ->
    nth(Date, N, fun next_second/1, fun prev_second/1).

nth(Date, 0, _Next, _Prev) ->
    Date;
nth(Date, N, Next, Prev) when is_integer(N), N > 0 ->
    nth(Next(Date), N-1, Next, Prev);
nth(Date, N, Next, Prev) when is_integer(N), N < 0 ->
    nth(Prev(Date), N+1, Next, Prev).

%% @doc Return the number of days in a certain year.
days_in_year(Y) ->
    case calendar:is_leap_year(Y) of
        true -> 366;
        false -> 365
    end.

%% @doc Return the absolute difference between two dates.  Does not take daylight saving into account.
diff({Y,M,D}, Date2) when is_integer(Y), is_integer(M), is_integer(D) ->
    diff({{Y,M,D},{0,0,0}}, Date2);
diff(Date1, {Y,M,D}) when is_integer(Y), is_integer(M), is_integer(D) ->
    diff(Date1, {{Y,M,D},{0,0,0}});
diff(Date1, Date2) when Date1 < Date2 ->
    diff(Date2,Date1);
diff({YMD1,{H1,I1,S1}}, {_,{_,_,S2}} = Date2) when S2 > S1 ->
    NextDate2 = next_minute(Date2),
    diff({YMD1,{H1,I1,S1+60}},NextDate2);
diff({YMD1,{H1,I1,S1}}, {_,{_,I2,_}} = Date2) when I2 > I1 ->
    NextDate2 = next_hour(Date2),
    diff({YMD1,{H1,I1+60,S1}},NextDate2);
diff({YMD1,{H1,I1,S1}}, {_,{H2,_,_}} = Date2) when H2 > H1 ->
    NextDate2 = next_day(Date2),
    diff({YMD1,{H1+24,I1,S1}},NextDate2);
diff({{Y1,M1,D1},T1}, {{_Y2,_M2,D2},_} = Date2) when D2 > D1 ->
    NextDate2 = next_day(Date2),
    diff({{Y1,M1,D1+1},T1},NextDate2);
diff({{Y1,M1,D1},T1}, {{_,M2,_},_} = Date2) when M2 > M1 ->
    NextDate2 = next_year(Date2),
    diff({{Y1,M1+12,D1},T1},NextDate2);

diff({{Y1,M1,D1},{H1,I1,S1}}, {{Y2,M2,D2},{H2,I2,S2}}) ->
    {{Y1-Y2, M1-M2, D1-D2}, {H1-H2, I1-I2, S1-S2}}.


%% @doc Return the month-boundaries of a given date
month_boundaries({{Y,M,_}, _}) ->
    Start = {{Y,M,1}, {0,0,0}},
    {End,_} = prev_day(next_month(Start)),
    {Start, {End, {23,59,59}}}.


%% @doc Return the week-boundaries of a given date.
%% WeekStart is optional, and determines on which day a week starts.
week_boundaries(Date) ->
    week_boundaries(Date, 1).

week_boundaries({D,_T}=Date, WeekStart) ->
    DOW = calendar:day_of_the_week(D),
    Start = -weeknorm(DOW - WeekStart),
    {S,_} = day_add(Date, Start),
    {E,_} = day_add(Date, Start + 6),
    { {S, {0,0,0}}, {E, {23,59,59}} }.

weeknorm(D) when D < 0 ->
    weeknorm(D+7);
weeknorm(D) when D > 6 ->
    weeknorm(D-7);
weeknorm(D) ->
    D.

day_add(Date, 0) ->
    Date;
day_add(Date, Num) when Num < 0 ->
    day_add(prev_day(Date), Num + 1);
day_add(Date, Num) when Num > 0 ->
    day_add(next_day(Date), Num - 1).


%% @doc Return the millisec value of the current clock.
-spec msec() -> integer().
msec() ->
    erlang:system_time(millisecond).

% Constant value of calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}})
-define(SECS_1970, 62167219200).

%% @doc Calculate the current UNIX timestamp (seconds since Jan 1, 1970)
-spec timestamp() -> SecondsSince1970
    when SecondsSince1970 :: integer().
timestamp() ->
    calendar:datetime_to_gregorian_seconds(calendar:universal_time())-?SECS_1970.

%% @doc Translate UNIX timestamp to local datetime.
timestamp_to_datetime(Seconds) ->
   calendar:gregorian_seconds_to_datetime(?SECS_1970 + Seconds).

%% @doc Translate a local time date to UNIX timestamp
datetime_to_timestamp(?ST_JUTTEMIS) ->
    undefined;
datetime_to_timestamp(undefined) ->
    undefined;
datetime_to_timestamp(DT) ->
    calendar:datetime_to_gregorian_seconds(DT) - ?SECS_1970.

%% @doc Return 'undefined' if a given date is invalid
undefined_if_invalid_date({{Y,M,D},{H,I,S}} = Date) when
    is_integer(Y), is_integer(M), is_integer(D),
    is_integer(H), is_integer(I), is_integer(S),
    H >= 0, H =< 23, I >= 0, I =< 59, S >= 0, S =< 59,
    M >= 1, M =< 12, D >= 1, Y >= -4713, Y =< 9999
    ->
        MaxDays = case M of
                    1 -> 31;
                    3 -> 31;
                    5 -> 31;
                    7 -> 31;
                    8 -> 31;
                    10 -> 31;
                    12 -> 31;
                    2 ->
                        case Y rem 400 of
                            0 -> 29;
                            _ ->
                                case Y rem 100 of
                                    0 -> 28;
                                    _ ->
                                        case Y rem 4 of
                                            0 -> 29;
                                            _ -> 28
                                        end
                                end
                        end;
                    _ ->
                        30
                  end,
        case D =< MaxDays of
            true -> Date;
            false -> undefined
        end;
undefined_if_invalid_date(_) ->
    undefined.


%% @doc Shift a date if the date falls outside the valid date or time ranges. Return undefined
%% if the date could not be mapped to some valid date.
-spec maybe_fix_datetime(fixable_datetime() | undefined) -> datetime() | undefined.
maybe_fix_datetime({_,_,_} = Date) ->
    maybe_fix_datetime({Date, {0,0,0}});
maybe_fix_datetime({{Y,M,D},{H,I,undefined}}) ->
    maybe_fix_datetime({{Y,M,D},{H,I,0}});
maybe_fix_datetime({{Y,M,D},{H,undefined,S}}) ->
    maybe_fix_datetime({{Y,M,D},{H,0,S}});
maybe_fix_datetime({{Y,M,D},{undefined,I,S}}) ->
    maybe_fix_datetime({{Y,M,D},{0,I,S}});
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when
    not is_integer(Y); not is_integer(M); not is_integer(D),
    not is_integer(H); not is_integer(I); not is_integer(S) ->
    undefined;
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(H), H < 0 ->
    maybe_fix_datetime(prev_day({{Y,M,D},{H+24,I,S}}));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(I), I < 0 ->
    maybe_fix_datetime(prev_hour({{Y,M,D},{H,I+60,S}}));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(S), S < 0 ->
    maybe_fix_datetime(prev_minute({{Y,M,D},{H,I,S+60}}));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(H), H >= 24 ->
    maybe_fix_datetime(next_day({{Y,M,D},{H-24,I,S}}));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(I), I >= 60 ->
    maybe_fix_datetime(next_hour({{Y,M,D},{H,I-60,S}}));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(S), S >= 60 ->
    maybe_fix_datetime(next_minute({{Y,M,D},{H,I,S-60}}));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(M), M =< 0 ->
    maybe_fix_datetime(prev_month({{Y,1,D},{H,I,S}}, 0-M+1));
maybe_fix_datetime({{Y,M,D},{H,I,S}}) when is_integer(D), D =< 0 ->
    maybe_fix_datetime(prev_day({{Y,M,1},{H,I,S}}, 0-D+1));
maybe_fix_datetime({{Y,M,D},{H,I,S}} = Date) when
    is_integer(Y), is_integer(M), is_integer(D),
    is_integer(H), is_integer(I), is_integer(S) ->
    norm_month(Date);
maybe_fix_datetime(_) ->
    undefined.



%% Routines below are adapted from calendar.erl, which is:
%%
%% Copyright Ericsson AB 1996-2011. All Rights Reserved.
%%
%% The adaptation is to make the routines work for BCE.

-type year() :: integer().
-type month() :: 1..12.
-type ldom() :: 28 | 29 | 30 | 31.

%% last_day_of_the_month(Year, Month)
%%
%% Returns the number of days in a month.
%%
-spec last_day_of_the_month(Year, Month) -> LastDay when
      Year :: year(),
      Month :: month(),
      LastDay :: ldom().
last_day_of_the_month(Y, M) when is_integer(Y), is_integer(M) ->
    last_day_of_the_month1(Y, M).

-spec last_day_of_the_month1(year(),month()) -> ldom().
last_day_of_the_month1(_, 4) -> 30;
last_day_of_the_month1(_, 6) -> 30;
last_day_of_the_month1(_, 9) -> 30;
last_day_of_the_month1(_,11) -> 30;
last_day_of_the_month1(Y, 2) ->
   case is_leap_year(Y) of
      true -> 29;
      _    -> 28
   end;
last_day_of_the_month1(_, M) when is_integer(M), M > 0, M < 13 ->
    31.

%% is_leap_year(Year) = true | false
%%
-spec is_leap_year(Year) -> boolean() when
      Year :: year().
is_leap_year(Year) when Year rem 4 =:= 0, Year rem 100 =/= 0 ->
    true;
is_leap_year(Year) when Year rem 400 =:= 0 ->
    true;
is_leap_year(_) ->
    false.