%
% Amzi! inc. Date Time Library
%
% The Date Time Library contains predicates that
% perform logical arithmetic on dates and times.
% The distinction is most noticable when dealing
% with months, which have varying numbers of days.
% See the description of date_add/3 for details.
%
% The library uses three structures: date/3, time/3
% and datetime/6.  Predicates are provided for dealing
% with each.  The structure arguments are:
%
%   date(YEAR,MONTH,DAY)
%   time(HOUR,MINUTES,SECONDS)
%   datetime(YEAR,MONTH,DAY,HOUR,MINUTES,SECONDS)
%
% The various predicates try to do the correct thing
% when overflowing the various quantities, like taking
% into account the number of days in February when adding,
% say, 15 days to Feb 20th.  The result will be a different
% day in March depending on whether the year is a leap
% year or not.
%
% The predicates also recognize the last day of each month
% as a special case.  So if you add a month to a date which
% is the last day of a month, you get the last day of the
% next month.
%
% The structures represent absolute dates and times.
% Relative date/time quantities are represented by structures
% for each unit, with one argument which is the value.  These
% structures are:  years/1, months/1, weeks/1, days/1, hours/1
% mins/1, and secs/1.
%
% Each is defined as a postfix operator as well.  So you can
% ask for example:
%
% ?- date_add(date(2002,1,15), 2 weeks, X).
% X = date(2002, 1, 29)
% yes
%
% The library includes predicates to parse and create date
% and time strings.
%
% To use the library, either consult or load the date_time
% file, or include it in your Prolog project.  Then import
% it.  For example, from the listener:
%
% ?- load(date_time).
% yes
% ?- import(date_time).
% yes
%

:- op(50, xf, days).
:- op(50, xf, months).
:- op(50, xf, weeks).
:- op(50, xf, years).
:- op(50, xf, hours).
:- op(50, xf, mins).
:- op(50, xf, secs).
:- op(700, xfx, <=).

:- module(date_time).
:- export( [
      date_get/2,             % get a date for today, tomorrow, etc.
      date_create/4,          % create a new date structure
      date_extract/2,         % extract date fields from a date structure
      date_age/2,             % compute an age from a birthday
      date_compare/3,         % compare two dates
      date_add/3,             % add years/months/weeks/days to a date
      date_difference/3,      % find the difference between two dates
      date_interval/3,        % find difference in single interval type (year, month, etc.)
      date_string/3,          % convert between date structures and strings
      date_year_day/2,        % calculate the day number for the year
      date_1900_days/2,       % calculate the days since the 0th day of 1900
      is_date_expression/1,   % succeeds if expression is a special date expression
      is_date_interval/1,     % succeeds if expression is a date interval
      is_date/1,              % succeeds if expression is a date
      time_get/2,             % gets the current time
      time_compare/3,         % compares two times
      time_add/3,             % add hours/mins/secs to a time
      time_difference/3,      % find the difference between two times
      time_interval/3,        % find the difference in single interval type(hour, min, sec)
      time_string/2,          % convert between time structures and strings
      datetime_get/2,         % get the current date and time
      datetime_compare/3,     % compare two datetime structures
      datetime_add/3,         % add datetime quantities to a datetime
      datetime_difference/3,  % find the difference between two datetimes
      datetime_string/3,      % convert to/from datetime strings
      datetime_date_time/3,   % convert datetime to/from date and time structures
      datetime_extract/2,     % extract years, months etc. from date time structure
      is_datetime_interval/1, % succeeds if expression is a date or time interval
      is_datetime/1,          % succeeds if expression is a datetime
      week_dayn/2,            % returns number for day of the week, 0 = Monday, 1 = Tuesday, ...
      week_day/2              % returns the day of the week for a date or datetime
      ]).
:- end_module(date_time).

:- body(date_time).


%-----------------------------------------------------------
% date_get(+DATE_TYPE, -DATE)
%
% Given one of the DATE_TYPEs, seen below, returns
% the DATE structure.
%

date_get(today, date(Y,M,D)) :- date(M,D,Y).
date_get(yesterday, DATE) :- date_add(today, days(-1), DATE).
date_get(tomorrow, DATE) :- date_add(today, days(1), DATE).
date_get(last_week, DATE) :- date_add(today, weeks(-1), DATE).
date_get(next_week, DATE) :- date_add(today, weeks(1), DATE).
date_get(last_month, DATE) :- date_add(today, months(-1), DATE).
date_get(next_month, DATE) :- date_add(today, months(1), DATE).
date_get(last_year, DATE) :- date_add(today, years(-1), DATE).
date_get(next_year, DATE) :- date_add(today, years(1), DATE).


%-----------------------------------------------------------
% date_create(+YEAR, +MONTH, +DAY, -DATE)
%
% Creates a new DATE structure from an input
% YEAR, MONTH, and DAY.
%

date_create(Y, M, D, date(Y,M,D)).


%-----------------------------------------------------------
% date_extract(+DATE, -VALUE)
%
% Gets the VALUE of the year, month or day, as
% specified in the TYPE argument, from an input
% DATE structure.
%

date_extract(date(Y,_,_), years(Y)).
date_extract(date(_,M,_), months(M)).
date_extract(date(_,_,D), days(D)).


%-----------------------------------------------------------
% date_age(+BIRTHDAY, -AGE)
%
% Computes an AGE in years, given a birthday
% date structure.  But, when it gets close, the
% days might be negative, so need to check for
% that case to prevent premature aging.
%

date_age(BDAY, AGE) :-
   date_get(today, TODAY),
   date_difference(TODAY, BDAY, DIFF),
   member(A years, DIFF),
   member(M months, DIFF),
   member(D days, DIFF),
   (M == 0, D < 0 ->
      AGE is A - 1
      ;
      AGE is A).

%-----------------------------------------------------------
% date_compare(+DATE_1, ?OP, +DATE_2)
%
% Compares the two date structures, unifying
% the result with the comparison operator.  So,
% it can be used to find the relationship or
% test a relationship.
%
% ?- date_compare(date(2002,1,15), X, date(2002,2,24)).
% X = < 
% yes
% ?- date_compare(date(2002,1,15), =<, date(2002,2,24)).
% yes
%

date_compare(D1, =, D2) :- D1 = D2, !.
date_compare(D1, >, D2) :- D1 @> D2, !.
date_compare(D1, <, D2) :- D1 @< D2, !.
date_compare(D1, >=, D2) :- D1 @>= D2, !.
date_compare(D1, =<, D2) :- D1 @=< D2, !.
date_compare(D1, <=, D2) :- D1 @=< D2, !.


%-----------------------------------------------------------
% date_add(+DATE_1, +DATE_QUANTITIES, -DATE_2)
%
% Adds the DATE_QUANTITIES to DATE_1 structure,
% returning DATE_2 structure.  The DATE_QUANTITIES
% are either a single structure or list of structures
% of the form days(D), months(M), or weeks(W).  Each
% is an operator, so can be written as '3 months' for
% example.
%
% The arithmetic is pure date arithmetic.  That is
% it adds calendar months, so Feb 15th plus one
% month yields Mar 15th.  Adding years over leap
% years winds up on same days as well.  Dates are
% correctly fixed for the corner cases, so an intermediate
% result of Feb 30th will become Mar 2nd in a non leap year
% and Mar 1st in leap year.
%
% ?- date_add(date(2002,1,15), [1 months, 2 days], D).
% D = date(2002, 2, 17) 
% yes
%
% ?- date_add(date(2002,1,15), [1 years, 1 months, 15 days], D).
% D = date(2003, 3, 2) 
% yes
%
% The special case of the last day of the month is
% recognized as well, so adding one month to the last
% day of a month gets the last day of the next month.
%
% ?- date_add(date(2002,1,31), 1 months, X).
% X = date(2002, 2, 28) 
% yes
%
% ?- date_add(date(2002,2,28), 1 months, X).
% X = date(2002, 3, 31) 
% yes
%

date_add(DATE, [], DATE) :-
   !.
date_add(D1, -(A1 + A2), DATE) :-
   convert_exp(-(A1+A2), AList),
   date_add(D1, AList, DATE),
   !.
date_add(D1, -(A1 - A2), DATE) :-
   convert_exp(-(A1-A2), AList),
   date_add(D1, AList, DATE),
   !.
date_add(D1, A1 + A2, DATE) :-
   convert_exp(A1+A2, AList),
   date_add(D1, AList, DATE),
   !.
date_add(D1, A1 - A2, DATE) :-
   convert_exp(A1-A2, AList),
   date_add(D1, AList, DATE),
   !.
date_add(D1, [DUNIT|DUNITS], DATE) :-
   date_add(D1, DUNIT, D2),
   !,
   date_add(D2, DUNITS, DATE).
date_add(D1, - [DUNIT|DUNITS], DATE) :-
   !,
   reverse_unit_signs([DUNIT|DUNITS],[RUNIT|RUNITS]),
   date_add(D1, [RUNIT|RUNITS], DATE).
date_add(today, ADD, DATE) :-
   date_get(today, D1),
   !,
   date_add(D1, ADD, DATE).
date_add(D1, -ADD, DATE) :-
   ADD =.. [UNIT, AMOUNT],
   MADD =.. [UNIT, -AMOUNT],
   date_add(D1, MADD, DATE).
date_add(date(Y,M,D), days(D1), date(YY,MM,DD)) :-
   D2 is D + D1,
   date_fix(date(Y,M,D2), date(YY,MM,DD)).
date_add(date(Y,M,D), weeks(D1), date(YY,MM,DD)) :-
   D2 is D + 7 * D1,
   date_fix(date(Y,M,D2), date(YY,MM,DD)).
date_add(date(Y,M,D), months(M1), date(YY,MM,DD)) :-
   M2 is M + M1,
   date_islast(date(Y,M,D), D2),
   date_fix(date(Y,M2,D2), date(YY,MM,DD)).
date_add(date(Y,M,D), years(Y1), date(YY,MM,DD)) :-
   Y2 is Y + Y1,
   date_islast(date(Y,M,D), D2),
   date_fix(date(Y2,M,D2), date(YY,MM,DD)).

convert_exp(Exp, List) :-
   convert_exp(Exp, [], List).

convert_exp(-(I1+I2), SoFar, List) :-
   !, convert_exp(-I1, [-I2|SoFar], List).
convert_exp(-(I1-I2), SoFar, List) :-
   !, convert_exp(-I1, [I2|SoFar], List).
convert_exp(I1+I2, SoFar, List) :-
   !, convert_exp(I1, [I2|SoFar], List).
convert_exp(I1-I2, SoFar, List) :-
   !, convert_exp(I1, [-I2|SoFar], List).
convert_exp(-Int, SoFar, [-Int|SoFar]) :-
   !.
convert_exp(Int, SoFar, [Int|SoFar]) :-
   !.

reverse_unit_signs([], []) :- !.
reverse_unit_signs([- A|As], [A|Bs]) :-
   !, reverse_unit_signs(As, Bs).
reverse_unit_signs([+ A|As], [- A|Bs]) :-
   !, reverse_unit_signs(As, Bs).
reverse_unit_signs([A|As], [- A|Bs]) :-
   !, reverse_unit_signs(As, Bs).
   
%-----------------------------------------------------------
% date_difference(+DATE_1, +DATE_2, -DATE_QUANTITIES).
%
% Subtracts, in pure date mode, DATE_2 date structure
% from DATE_1 date structure, providing a result of
% a list of date quantities.  Note that years are
% rounded, but that the result in the days(D) structure
% might be negative.  This is to allow the correct
% behavior when reapplying the difference by adding it
% to another date.
%
% ?- date_difference(date(2002,3,2), date(2002,1,15), D).
% D = [0 years, 2 months, -13 days] 
% yes
%
% The special case of both dates being end of month
% is recognized as being just a difference of one month.
%
% ?- date_difference(date(2002,2,28), date(2002,1,31), X).
% X = [0 years, 1 months, 0 days] 
% yes
%

date_difference(date(Y1,M1,D1), date(Y2,M2,D2),
      [years(Y), months(M), days(D)]) :-
   (D2 > D1 ->
      (date_islast(date(Y1,M1,D1), last) ->
         M1a is M1,
         D1a is D2
         ;
         M1a is M1 - 1,
         date_month_days(M1a,Y1,Dprev),
         D1a is D1 + Dprev )
      ;
      D1a = D1,
      M1a = M1 ),
   (M2 > M1a ->
      M1b is M1a + 12,
      Y1b is Y1 - 1
      ;
      M1b = M1a,
      Y1b = Y1 ),
   Y is Y1b - Y2,
   M is M1b - M2,
   D is D1a - D2.
    
/* had negative days
date_difference(date(Y1,M1,D1), date(Y2,M2,D2),
      [years(Y), months(M), days(D)]) :-
   Y3 is Y1 - Y2,
   M3 is M1 - M2,
   ( (date_islast(date(Y1,M1,D1),last), date_islast(date(Y2,M2,D2),last)) ->
     D = 0
     ;
     D is D1 - D2 ),
   (M3 < 0 ->
      M4 is M3 + 12,
      Y4 is Y3 - 1
      ;
      Y4 = Y3,
      M4 = M3),
   (Y4 < 0 ->
      Y is Y4 + 1,
      M is M4 - 12
      ;
      Y = Y4,
      M = M4).
*/

%----------------------------------------------------------
% date_1900_days(Date, Days)
%
% express a date as the number of days since the
% 0th day of 1900, which is date(1900,1,0).
%

date_1900_days(date(Y,M,D), Days) :-
   var(Days),
   !,
   Years is Y - 1900,
   (Y > 2000 ->
      LeapDays is ((Years-1) // 4) - 1
      ;
      LeapDays is (Years-1) // 4 ),
   MM is M - 1,
   date_add_month_days(MM, Y, 0, MonthDays),
   Days is Years * 365 + LeapDays + MonthDays + D.
date_1900_days(Date, Days) :-
   YearEst is 1900 + (Days // 365),
   date_1900_days(date(YearEst,1,1), DaysUsed),
   DaysLeft is Days - DaysUsed,
   date_add(date(YearEst,1,1), DaysLeft days, Date).
   

%----------------------------------------------------------
% date_year_day(Date, YearDay)
%
% for a date, calculate the day number in the year of
% the day
%

date_year_day(date(Y,M,D), YearDay) :-
   MM is M - 1,
   date_add_month_days(MM, Y, 0, MonthDays),
   YearDay is MonthDays + D.

%-----------------------------------------------------------
% date_interval(Date1, Date2, Interval)
%
% The date difference where Interval is in a specific
% unit, such as days or weeks.
%
% ex.  date_interval(D1, D2, M months).
%

date_interval(D1, D2, D days) :-
   !,
   date_1900_days(D1, Days1),
   date_1900_days(D2, Days2),
   D is Days1 - Days2.
date_interval(D1, D2, W weeks) :-
   !,
   date_interval(D1, D2, D days),
   W is D // 7.
date_interval(D1, D2, MM months) :-
   !,
   date_difference(D1, D2, [Y years, M months|_]),
   MM is 12 * Y + M.
date_interval(D1, D2, Y years) :-
   !,
   date_difference(D1, D2, [Y years|_]).

%-----------------------------------------------------------
% Internal predicates used by exported
% date predicates.
%

/* not used
date_expression_ok(_ weeks).
date_expression_ok(_ days).
date_expression_ok(_ months).
date_expression_ok(_ years).
*/

% make a date correct

date_fix(date(Y,M,D), date(YY,MM,DD)) :-
   M < 1,
   !,
   M2 is M + 12,
   Y2 is Y - 1,
   date_fix(date(Y2,M2,D), date(YY,MM,DD)).
date_fix(date(Y,M,D), date(YY,MM,DD)) :-
   M > 12,
   !,
   M2 is M - 12,
   Y2 is Y + 1,
   date_fix(date(Y2,M2,D), date(YY,MM,DD)).
date_fix(date(Y,M,last), date(Y,M,MD)) :-
   !,
   date_month_days(M,Y,MD).
date_fix(date(Y,M,D), date(YY,MM,DD)) :-
   D < 1,
   !,
   M2 is M - 1,
   date_month_days(M2,Y,MD),
   D2 is D + MD,
   date_fix(date(Y,M2,D2), date(YY,MM,DD)).
date_fix(date(Y,M,D), date(YY,MM,DD)) :-
   date_month_days(M,Y,MD),
   D > MD,
   !,
   M2 is M + 1,
   D2 is D - MD,
   date_fix(date(Y,M2,D2), date(YY,MM,DD)).
date_fix(date(Y,M,D), date(Y,M,D)).

% date_islast(+DATE, -DAY)
%
% if the day is the last day of the month,
% mark it as 'last', instead of its number.

date_islast(date(Y,M,MD), last) :-
   date_month_days(M,Y,MD), !.
date_islast(date(Y,M,D), D).

date_month_days(0,_,31).
date_month_days(1,_,31).
date_month_days(2,Y,29) :- date_leap_year(Y), !.
date_month_days(2,_,28).
date_month_days(3,_,31).
date_month_days(4,_,30).
date_month_days(5,_,31).
date_month_days(6,_,30).
date_month_days(7,_,31).
date_month_days(8,_,31).
date_month_days(9,_,30).
date_month_days(10,_,31).
date_month_days(11,_,30).
date_month_days(12,_,31).
date_month_days(13,_,31).

date_leap_year(Y) :-
   ( ( 0 =:= Y mod 100, 0 =:= Y mod 400 ) ;
     ( 0 =\= Y mod 100, 0 =:= Y mod 4 ) ).

% one y2k method

date_year_chk(YYYY, YYYY) :- YYYY > 1000, !.
date_year_chk(Y, YYYY) :-
   Y > 50, !,
   YYYY is Y + 1900.
date_year_chk(Y, YYYY) :-
   YYYY is Y + 2000.

date_add_month_days(0, _, Days, Days) :-
   !.
date_add_month_days(M, Y, Acc, Days) :-
   date_month_days(M, Y, D),
   Acc2 is Acc + D,
   MM is M - 1,
   !, date_add_month_days(MM, Y, Acc2, Days).


%-----------------------------------------------------------
% is_date(+DATE)
%
% Succeeds if DATE is a date
%

is_date(date(_,_,_)).
is_date(today).

%-----------------------------------------------------------
% is_date_interval(+INTERVAL)
%
% Succeeds if INTERVAL is a date interval
%

is_date_interval(INTERVAL) :-
   INTERVAL =.. [UNITS, _],
   member(UNITS, [days, weeks, months, years]).
is_date_interval(I1 + I2) :-
   is_date_interval(I1),
   is_date_interval(I2).
is_date_interval(I1 - I2) :-
   is_date_interval(I1),
   is_date_interval(I2).
is_date_interval(- I2) :-
   is_date_interval(I2).
   

%-----------------------------------------------------------
% is_date_expression(+DATE)
%
% Returns if the expression is a special date one.
%

is_date_expression(date(_,_,_)).
is_date_expression(EXP) :-
   EXP =.. [UNITS, _],
   member(UNITS, [days, weeks, months, years]).


%-----------------------------------------------------------
% time_get(+WHEN, -TIME)
%
% Returns the current time.
%

time_get(now, time(H,M,S)) :- time(H,M,S).


%-----------------------------------------------------------
% time_compare(+TIME_1, ?OP, +TIME_2)
%
% Compares the two times, unifying the
% result with the comparison operator.
%

time_compare(T1, =, T2) :- T1 = T2, !.
time_compare(T1, >, T2) :- T1 @> T2, !.
time_compare(T1, <, T2) :- T1 @< T2, !.
time_compare(T1, >=, T2) :- T1 @>= T2, !.
time_compare(T1, =<, T2) :- T1 @=< T2, !.
time_compare(T1, <=, T2) :- T1 @=< T2, !.


%-----------------------------------------------------------
% time_add(+TIME_1, +TIME_QUANTITIES, -TIME_2)
%
% Adds the TIME_QUANTITIES to TIME_1 and
% returns TIME_2.  Time quantities can be
% hours/1, mins/1 or secs/1.
%

time_add(TIME, [], TIME) :- !.
time_add(T1, [TUNIT|TUNITS], TIME) :-
   time_add(T1, TUNIT, T2),
   !, time_add(T2, TUNITS, TIME).
time_add(now, ADD, TIME) :-
   time_get(now, T1),
   time_add(T1, ADD, TIME).
time_add(T1, -ADD, TIME) :-
   ADD =.. [UNIT, AMOUNT],
   MADD =.. [UNIT, -AMOUNT],
   time_add(T1, MADD, TIME).
time_add(time(H,M,S), secs(S1), time(HH,MM,SS)) :-
   S2 is S + S1,
   time_fix(time(H,M,S2), time(HH,MM,SS)).
time_add(time(H,M,S), mins(M1), time(HH,MM,SS)) :-
   M2 is M + M1,
   time_fix(time(H,M2,S), time(HH,MM,SS)).
time_add(time(H,M,S), hours(H1), time(HH,MM,SS)) :-
   H2 is H + H1,
   time_fix(time(H2,M,S), time(HH,MM,SS)).


%-----------------------------------------------------------
% time_difference(+TIME_1, +TIME_2, -TIME_QUANTITIES
%
% Subtracts two times, returning a list of time
% quantities representing the difference.
%

time_difference(time(H1,M1,S1), time(H2,M2,S2),
      [hours(H), mins(M), secs(S)] ) :-
   H3 is H1 - H2,
   M3 is M1 - M2,
   S3 is S1 - S2,
   (S3 < 0 ->
      M4 is M3 - 1,
      S is S3 + 60
      ;
      M4 = M3,
      S = S3),
   (M4 < 0 ->
      M is M4 + 60,
      H is H3 - 1
      ;
      H = H3,
      M = M4).

time_interval(time(H1,M1,_), time(H2,M2,_), mins(M)) :-
   !, M is 60*(H1-H2) + (M1-M2).
time_interval(time(H1,M1,S1), time(H2,M2,S2), secs(S)) :-
   !, S is 3600*(H1-H2) + 60*(M1-M2) + (S1-S2).
time_interval(time(H1,M1,_), time(H2,M2,_), hours(H)) :-
   !, S is (H1-H2) + (M1-M2)/60.
time_interval(datetime(Y,L,D,H1,M1,_), datetime(Y,L,D,H2,M2,_), mins(M)) :-
   !, M is 60*(H1-H2) + (M1-M2).
time_interval(datetime(Y,L,D,H1,M1,S1), datetime(Y,L,D,H2,M2,S2), secs(S)) :-
   !, S is 3600*(H1-H2) + 60*(M1-M2) + (S1-S2).
time_interval(datetime(Y,L,D,H1,M1,_), datetime(Y,L,D,H2,M2,_), hours(H)) :-
   !, S is (H1-H2) + (M1-M2)/60.
time_interval(datetime(Y1,L1,D1,H1,M1,_), datetime(Y2,L2,D2,H2,M2,_), mins(M)) :-
   !,
   date_interval(date(Y1,L1,D1), date(Y2,L2,D2), days(D)),
   M is 24*60*D + 60*(H1-H2) + (M1-M2).
time_interval(datetime(Y1,L1,D1,H1,M1,S1), datetime(Y2,L2,D2,H2,M2,S2), secs(S)) :-
   !,
   date_interval(date(Y1,L1,D1), date(Y2,L2,D2), days(D)),
   S is 24*60*60*D + 3600*(H1-H2) + 60*(M1-M2) + (S1-S2).
time_interval(datetime(Y1,L1,D1,H1,M1,_), datetime(Y2,L2,D2,H2,M2,_), hours(H)) :-
   !,
   date_interval(date(Y1,L1,D1), date(Y2,L2,D2), days(D)),
   S is 24*D + (H1-H2) + (M1-M2)/60.
   

%-----------------------------------------------------------
% Time internal predicates
%

time_fix(time(H,M,S), time(HH,MM,SS)) :-
   H < 0,
   !,
   H2 is H + 24,
   time_fix(time(H2,M,S), time(HH,MM,SS)).
time_fix(time(H,M,S), time(HH,MM,SS)) :-
   H > 23,
   !,
   H2 is H - 24,
   time_fix(time(H2,M,S), time(HH,MM,SS)).
time_fix(time(H,M,S), time(HH,MM,SS)) :-
   M < 0,
   !,
   M2 is M + 60,
   H2 is H - 1,
   time_fix(time(H2,M2,S), time(HH,MM,SS)).
time_fix(time(H,M,S), time(HH,MM,SS)) :-
   M > 59,
   !,
   M2 is M - 60,
   H2 is H + 1,
   time_fix(time(H2,M2,S), time(HH,MM,SS)).
time_fix(time(H,M,S), time(HH,MM,SS)) :-
   S < 0,
   !,
   S2 is S + 60,
   M2 is M - 1,
   time_fix(time(H,M2,S2), time(HH,MM,SS)).
time_fix(time(H,M,S), time(HH,MM,SS)) :-
   S > 59,
   !,
   S2 is S - 60,
   M2 is M + 1,
   time_fix(time(H,M2,S2), time(HH,MM,SS)).
time_fix(time(H,M,S), time(H,M,S)).


%-----------------------------------------------------------
% is_datetime(+DATETIME)
%
% Succeeds if DATETIME is a datetime
%

is_datetime(datetime(_,_,_,_,_,_)).

%-----------------------------------------------------------
% is_datetime_interval(+INTERVAL)
%
% Succeeds if INTERVAL is a date or time interval
%

is_datetime_interval(INTERVAL) :-
   INTERVAL =.. [UNITS, _],
   member(UNITS, [days, weeks, months, years, hours, mins, secs]).
is_datetime_interval(I1 + I2) :-
   is_datetime_interval(I1),
   is_datetime_interval(I2).
is_datetime_interval(I1 - I2) :-
   is_datetime_interval(I1),
   is_datetime_interval(I2).
is_datetime_interval(- I2) :-
   is_datetime_interval(I2).
   
%--------------------------------------------------------------
% datetime_get(+WHEN, -DATETIME)
%
% Returns the current date and time in a datetime/6
% structure.
%

datetime_get(now, datetime(YEAR,MON,DAY,HOUR,MIN,SEC)) :-
   date_get(today, date(YEAR,MON,DAY)),
   time_get(now, time(HOUR,MIN,SEC)).
datetime_get(today, datetime(YEAR,MON,DAY,0,0,0)) :-
   date_get(today, date(YEAR,MON,DAY)).


%--------------------------------------------------------------
% datetime_compare(+DT_1, ?OP, +DT_2)
%
% Compares the datetime structures, DT_1 and
% DT_2 and unifies with the operator OP.
%

datetime_compare(T1, =, T2) :- T1 = T2, !.
datetime_compare(T1, >, T2) :- T1 @> T2, !.
datetime_compare(T1, <, T2) :- T1 @< T2, !.
datetime_compare(T1, >=, T2) :- T1 @>= T2, !.
datetime_compare(T1, =<, T2) :- T1 @=< T2, !.
datetime_compare(T1, <=, T2) :- T1 @=< T2, !.


%--------------------------------------------------------------
% datetime_add(+DT_1, +DT_QUANTITIES, -DT_2)
%
% Adds the date and time quantities to datetime
% structure DT_1, returning the result in DT_2.
%

datetime_add(DT, [], DT) :- !.
datetime_add(DT1, [DTUNIT|DTUNITS], DT) :-
   datetime_add(DT1, DTUNIT, DT2),
   !, datetime_add(DT2, DTUNITS, DT).
datetime_add(now, ADD, TIME) :-
   datetime_get(now, T1),
   datetime_add(T1, ADD, TIME).
datetime_add(today, ADD, TIME) :-
   datetime_get(today, T1),
   datetime_add(T1, ADD, TIME).
datetime_add(T1, -ADD, TIME) :-
   ADD =.. [UNIT, AMOUNT],
   MADD =.. [UNIT, -AMOUNT],
   datetime_add(T1, MADD, TIME).
datetime_add(datetime(Y,L,D,H,M,S), years(Y1), datetime(YY,LL,DD,HH,MM,SS)) :-
   Y2 is Y + Y1,
   datetime_fix(datetime(Y2,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_add(datetime(Y,L,D,H,M,S), months(L1), datetime(YY,LL,DD,HH,MM,SS)) :-
   L2 is L + L1,
   datetime_fix(datetime(Y,L2,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_add(datetime(Y,L,D,H,M,S), days(D1), datetime(YY,LL,DD,HH,MM,SS)) :-
   D2 is D + D1,
   datetime_fix(datetime(Y,L,D2,H,M,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_add(datetime(Y,L,D,H,M,S), hours(H1), datetime(YY,LL,DD,HH,MM,SS)) :-
   H2 is H + H1,
   datetime_fix(datetime(Y,L,D,H2,M,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_add(datetime(Y,L,D,H,M,S), mins(M1), datetime(YY,LL,DD,HH,MM,SS)) :-
   M2 is M + M1,
   datetime_fix(datetime(Y,L,D,H,M2,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_add(datetime(Y,L,D,H,M,S), secs(S1), datetime(YY,LL,DD,HH,MM,SS)) :-
   S2 is S + S1,
   datetime_fix(datetime(Y,L,D,H,M,S2), datetime(YY,LL,DD,HH,MM,SS)).


%--------------------------------------------------------------
% datetime_difference(+DT_1, +DT_2, -DT_QUANTITIES)
%
% Subtracts two datetime structures, returning the
% datetime quantities.
%

datetime_difference(datetime(Y1,L1,D1,H1,M1,S1), datetime(Y2,L2,D2,H2,M2,S2),
      [years(Y), months(L), days(D), hours(H), mins(M), secs(S)] ) :-
   date_difference(date(Y1,L1,D1), date(Y2,L2,D2), [years(Y), months(L), days(D)]),
   time_difference(time(H1,M1,S1), time(H2,M2,S2), [hours(H), mins(M), secs(S)]).


%--------------------------------------------------------------
% datetime_date_time(?DT, ?DATE, ?TIME)
%
% convert between datetime and date and time structures.
%

datetime_date_time(datetime(YR,MO,DA,HR,MI,SE), date(YR,MO,DA), time(HR,MI,SE)).


%-----------------------------------------------------------
% datetime_extract(+DATETIME, -VALUE)
%
% Gets the VALUE of the year, month or day, as
% specified in the TYPE argument, from an input
% DATE structure.
%

datetime_extract(datetime(Y,_,_,_,_,_), years(Y)).
datetime_extract(datetime(_,M,_,_,_,_), months(M)).
datetime_extract(datetime(_,_,D,_,_,_), days(D)).
datetime_extract(datetime(_,_,_,H,_,_), hours(H)).
datetime_extract(datetime(_,_,_,_,M,_), mins(M)).
datetime_extract(datetime(_,_,_,_,_,S), secs(D)).

%--------------------------------------------------------------
% Internal predicates used in datetime calculations.
%

datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)) :-
   H < 0,
   !,
   H2 is H + 24,
   D2 is D - 1,
   datetime_fix(datetime(Y,L,D2,H2,M,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)) :-
   H > 23,
   !,
   H2 is H - 24,
   D2 is D + 1,
   datetime_fix(datetime(Y,L,D2,H2,M,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)) :-
   M < 0,
   !,
   M2 is M + 60,
   H2 is H - 1,
   datetime_fix(datetime(Y,L,D,H2,M2,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)) :-
   M > 59,
   !,
   M2 is M - 60,
   H2 is H + 1,
   datetime_fix(datetime(Y,L,D,H2,M2,S), datetime(YY,LL,DD,HH,MM,SS)).
datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)) :-
   S < 0,
   !,
   S2 is S + 60,
   M2 is M - 1,
   datetime_fix(datetime(Y,L,D,H,M2,S2), datetime(YY,LL,DD,HH,MM,SS)).
datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,HH,MM,SS)) :-
   S > 59,
   !,
   S2 is S - 60,
   M2 is M + 1,
   datetime_fix(datetime(Y,L,D,H,M2,S2), datetime(YY,LL,DD,HH,MM,SS)).
datetime_fix(datetime(Y,L,D,H,M,S), datetime(YY,LL,DD,H,M,S)) :-
   date_fix(date(Y,L,D), date(YY,LL,DD)).

%--------------------------------------------------
% date_string(?DATE, ?FORMAT, ?STRING)
%
% Convert between a date structure and a string,
% optionally based on a specified format atom.
% See ds_date below for the accepted formats for
% dates.
%
% ?- date_string(D, F, `24 Feb 1946`).
% D = date(1946, 2, 24)
% F = 'd mon y' 
% yes
%
% ?- date_string(D, F, `February 24, 1946`).
% D = date(1946, 2, 24)
% F = 'month d, y' 
% yes
%
% ?- date_string(date(1946,2,24), 'month d, y', X).
% X = `February 24, 1946` 
% yes
%
% ?- date_string(D, 'd/m/y', `24/2/1946`).
% D = date(1946, 2, 24) 
% yes
%
% ?- date_string(date(1946,2,24), F, X).
% F = 'm/d/y'
% X = `2/24/1946` 
% yes
%

date_string(DATE, FORMAT, STRING) :-
   nonvar(STRING), !,
   string_list(STRING, LIST),
   ds_date(DATE, FORMAT, LIST, []),
   !.
date_string(DATE, FORMAT, STRING) :-
   ds_date(DATE, FORMAT, LIST, []),
   !,
   string_list(STRING, LIST).
 
ds_date(date(Y,M,D), 'y/m/d') -->
   ds_year(Y), sp, "/", sp, ds_month(M), sp, "/", sp, ds_day(D), !.
ds_date(date(Y,M,D), 'm/d/y') -->
   ds_month(M), sp, "/", sp, ds_day(D), sp, "/", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'mm/dd/yyyy') -->
   ds_month2(M), sp, "/", sp, ds_day2(D), sp, "/", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'd/m/y') --> 
   ds_day(D), sp, "/", sp, ds_month(M), sp, "/", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'y-m-d') -->
   ds_year(Y), sp, "-", sp, ds_month(M), sp, "-", sp, ds_day(D), !.
ds_date(date(Y,M,D), 'm-d-y') -->
   ds_month(M), sp, "-", sp, ds_day(D), sp, "-", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'd-m-y') --> 
   ds_day(D), sp, "-", sp, ds_month(M), sp, "-", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'd mon y') -->
   ds_day(D), " ", sp, ds_short_month(M), " ", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'month d, y') -->
   ds_long_month(M), " ", sp, ds_day(D), ", ", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'mon d y') -->
   ds_short_month(M), " ", sp, ds_day(D), " ", sp, ds_year(Y), !.
ds_date(date(Y,M,D), 'month d y') -->
   ds_long_month(M), " ", sp, ds_day(D), " ", sp, ds_year(Y), !.

%--------------------------------------------------
% time_string(?TIME, ?STRING)
%
% Convert between time structures and strings of the
% form hh:mm:ss.
%
% ?- time_string(time(2,33,15), X).
% X = `2:33:15` 
% yes
%
% ?- time_string(T, `2:33:22`).
% T = time(2, 33, 22) 
% yes
%

time_string(TIME, STRING) :-
   nonvar(STRING), !,
   string_list(STRING, LIST),
   ds_time(TIME, LIST, []),
   !.
time_string(TIME, STRING) :-
   ds_time(TIME, LIST, []),
   !,
   string_list(STRING, LIST).

%--------------------------------------------------
% datetime_string(?DATE, ?FORMAT, ?STRING)
%
% Convert between a date structure and a string,
% optionally based on a specified format atom.
% See date_string and time_string for details.
%

datetime_string(DT, FORMAT, STRING) :-
   nonvar(STRING), !,
   string_list(STRING, LIST),
   ds_datetime(DT, FORMAT, LIST, []),
   !.
datetime_string(DT, FORMAT, STRING) :-
   ds_datetime(DT, FORMAT, LIST, []),
   !,
   string_list(STRING, LIST).

ds_datetime(datetime(YR,DY,MO,HR,MI,SE), FORMAT) -->
   ds_date(date(YR,DY,MO), FORMAT),
   " ",
   sp,
   ds_time(time(HR,MI,SE)).
   
%--------------------------------------------------
% Supporting predicates for string conversions
%

ds_time(time(H,M,S)) -->
   ds_hour(H), sp, ":", sp, ds_min(M), sp, ":", sp, ds_sec(S).

ds_year(YY) --> { var(YY), ! }, ds_number(Y), { date_year_chk(Y, YY) }.
ds_year(Y) --> { date_year_chk(Y, YY) }, ds_number(YY).
ds_month(M) --> ds_number(M).
ds_day(D) --> ds_number(D).

ds_hour(H) --> ds_number(H).
ds_min(M) --> ds_number(M).
ds_sec(S) --> ds_number(S).

ds_month2(MM) --> ds_number2(MM).
ds_day2(DD) --> ds_number2(DD).

ds_number(N) -->
   { var(N) }, !,
   ds_digits(D),
   { string_list(S, D), string_integer(S, N)}.
ds_number(N) -->
   { string_integer(S, N), string_list(S, D) },
   ds_digits(D).

ds_digits([X|Y]) --> [X], {ds_digit(X)}, ds_digits(Y).
ds_digits([X]) --> [X], {ds_digit(X)}.

ds_number2(N) -->
   { var(N) }, !,
   ds_digits(D),
   { string_list(S, D), string_integer(S, N)}.
ds_number2(N) -->
   { string_integer(S, N), string_list(S, D) },
   ds_digits2(D).

ds_digits2([N]) --> [0'0, N], {ds_digit(N)}.
ds_digits2([A,B]) --> [A,B], {ds_digit(A), ds_digit(B)}.

ds_digit(X) :- number(X), X >= 0'0, X =< 0'9.

sp --> "".
sp --> [W], { number(W), W =< 32 }, sp.

ds_short_month(1) --> "Jan".
ds_short_month(2) --> "Feb".
ds_short_month(3) --> "Mar".
ds_short_month(4) --> "Apr".
ds_short_month(5) --> "May".
ds_short_month(6) --> "Jun".
ds_short_month(7) --> "Jul".
ds_short_month(8) --> "Aug".
ds_short_month(9) --> "Sep".
ds_short_month(10) --> "Oct".
ds_short_month(11) --> "Nov".
ds_short_month(12) --> "Dec".

ds_long_month(1) --> "January".
ds_long_month(2) --> "February".
ds_long_month(3) --> "March".
ds_long_month(4) --> "April".
ds_long_month(5) --> "May".
ds_long_month(6) --> "June".
ds_long_month(7) --> "July".
ds_long_month(8) --> "August".
ds_long_month(9) --> "September".
ds_long_month(10) --> "October".
ds_long_month(11) --> "November".
ds_long_month(12) --> "December".

%---------------------------------------------
% week_day(DT, WD)
%
% Return the day of the week for a date or date time
%

week_day(date(Y,M,D), WD) :-
   date_1900_days(date(Y,M,D), N),
   DN is N mod 7,
   day_name(DN, WD),
   !.
week_day(datetime(Y,M,D,_,_,_), WD) :-
   date_1900_days(date(Y,M,D), N),
   DN is N mod 7,
   day_name(DN, WD),
   !.

week_dayn(date(Y,M,D), DN) :-
   date_1900_days(date(Y,M,D), N),
   DN is N mod 7,
   !.
week_dayn(datetime(Y,M,D,_,_,_), DN) :-
   date_1900_days(date(Y,M,D), N),
   DN is N mod 7,
   !.

day_name(0, 'Monday').
day_name(1, 'Tuesday').
day_name(2, 'Wednesday').
day_name(3, 'Thursday').
day_name(4, 'Friday').
day_name(5, 'Saturday').
day_name(6, 'Sunday').

%--------------------------------------------
% utils
%

member(X, [X|_]).
member(X, [_|Z]) :- member(X, Z).

reverse(A, Z) :- reverse(A, [], Z).

   reverse([], Z, Z).
   reverse([A|X], SoFar, Z) :- reverse(X, [A|SoFar], Z).



:- end_body(date_time).