/*

  Would this be a good time to comment on the paucity of prolog
  libraries and perhaps the idiocy of writing web-code in prolog.  sigh. 
  
  */

:- [cvs, unit_test].
cvs_id('$Id: time_conversions.pl,v 1.7 2005/10/29 03:14:52 sandro Exp $').

:- ensure_loaded(option).
:- ensure_loaded(library('http/dcg_basics')).

isotime(Time, IsoTime) :-
   convert_time(Time, Year, Month, Day, Hour, Minute, Second, _),
   pad2(Month, Month2),
   pad2(Day, Day2),
   pad2(Hour, Hour2),
   pad2(Minute, Minute2),
   pad2(Second, Second2),
   concat_atom([Year, Month2, Day2], '-', P1),
   concat_atom([Hour2, Minute2, Second2], ':', P2),
   concat_atom([P1, P2], 'T', IsoTime).

w3cdate(Time, Date) :-
   convert_time(Time, Year, Month, Day, _, _, _, _),
   month_name(Month, MonthName),
   concat_atom([Day, ' ', MonthName, ' ', Year], Date).

squished_isotime(Time, IsoTime) :-
   convert_time(Time, Year, Month, Day, Hour, Minute, Second, _),
   pad2(Month, Month2),
   pad2(Day, Day2),
   pad2(Hour, Hour2),
   pad2(Minute, Minute2),
   pad2(Second, Second2),
   concat_atom([Year, Month2, Day2], P1),
   concat_atom([Hour2, Minute2, Second2], P2),
   concat_atom([P1, P2], IsoTime).

isotime_of_day(Time, IsoTimeOfDay) :-
   convert_time(Time, _Year, _Month, _Day, Hour, Minute, Second, Mil),
   pad2(Hour, Hour2),
   pad2(Minute, Minute2),
   sformat(Str, '~`0t~w~2+.~`0t~w~4+', [Second, Mil]),
   string_to_atom(Str, Second2),
   concat_atom([Hour2, Minute2, Second2], ':', IsoTimeOfDay).

% I'm torn.  My printf soul wants to do it like this, but it's quite
% unreadable (unlike printf) and it's actually slower.  So I'm just leaving
% this code here to, um, remind myself how format works, or something.
isotime_using_sformat(Time, IsoTime) :-
   convert_time(Time, Year, Month, Day, Hour, Minute, Second, _),
   sformat(Str, '~w-~|~`0t~w~2+-~|~`0t~w~2+T~|~`0t~w~2+:~|~`0t~w~2+:~|~`0t~w~2+',
	  [Year, Month, Day, Hour, Minute, Second]),
   string_to_atom(Str, IsoTime).

isohour(Time, IsoTime) :-
   convert_time(Time, Year, Month, Day, Hour, _Minute, _Second, _),
   pad2(Month, Month2),
   pad2(Day, Day2),
   pad2(Hour, Hour2),
   concat_atom([Year, Month2, Day2], '-', P1),
   concat_atom([Hour2], ':', P2),
   concat_atom([P1, P2], 'T', IsoTime).

pad2(Num, Str) :-
   Num < 10, !,
   atom_number(S1, Num),
   concat_atom(['0', S1], Str).
pad2(Num, Str) :-
   Num >= 10,
   atom_number(Str, Num).

unit_test( parse_http_datetime('Sun, 06 Nov 1994 08:49:37 GMT',784111777) ).
unit_test( parse_http_datetime('Thu, 11 Mar 2004 20:18:20 GMT',1079036300) ).

parse_http_datetime(Arg, T) :-
	atom_codes(Arg, Codes),
	http_date(Parts, Codes, []),
	to_posix_seconds(Parts, T), !.
	
%
%   Oh my gosh, do I really have to write this myself?!
%
% http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3

unit_test( http_date(X, "Sun, 06 Nov 1994 08:49:37 GMT", ""),
	   X = [day(6), month(11), year(1994),
		hour(8), minute(49), second(37)]
	 ).
unit_test( http_date(X, "Sunday, 06-Nov-94 08:49:37 GMT", ""),
	   X = [day(6), month(11), year(1994),
		hour(8), minute(49), second(37)]
	 ).
unit_test( http_date(X, "Sun Nov  6 08:49:37 1994", ""),
	   X = [day(6), month(11), year(1994),
		hour(8), minute(49), second(37)]
	 ).

	 

http_date(Parts) -->
	( rfc1123_date(Parts)
	; rfc850_date(Parts)
	; asctime_date(Parts)
	).

rfc1123_date(Parts) -->
	wkday,
	", ",
	date1(P1),
	" ",
	time(P2),
	" GMT", { append(P1, P2, Parts) }.

rfc850_date(Parts) -->
	weekday,
	", ",
	date2(P1),
	" ",
	time(P2),
	" GMT", { append(P1, P2, Parts) }.

asctime_date([year(Year) | Parts]) -->
	wkday,
	" ",
	date3(P1),
	" ",
	time(P2),
	" ",
	digits(4, Year), { append(P1, P2, Parts) }.

date1([day(Day), month(Month), year(Year)]) -->
	digits(2, Day), " ",
	month(Month), " ",
	digits(4, Year).

date2([day(Day), month(Month), year(Year)]) -->
	digits(2, Day), "-",
	month(Month), "-",
	digits(2, Year2),
	{ Year is Year2+1900 }.

date3([day(Day), month(Month)]) -->
	month(Month), " ",
	digits(2, Day), " ".
date3([day(Day), month(Month)]) -->
	month(Month), "  ",
	digits(1, Day), " ".
	

time([hour(Hour), minute(Minute), second(Second)]) -->
	digits(2, Hour), ":",
	digits(2, Minute), ":",
	digits(2, Second).


wkday -->
	"Mon" ; "Tue" ; "Wed"
	; "Thu" ; "Fri" ; "Sat" ; "Sun".
weekday  --> "Monday" ; "Tuesday" ; "Wednesday"
	; "Thursday" ; "Friday" ; "Saturday" ; "Sunday".

month(Month) -->
	( "Jan", { Month = 1}
	; "Feb", { Month = 2}
	; "Mar", { Month = 3}
	; "Apr", { Month = 4}
	; "May", { Month = 5}
	; "Jun", { Month = 6}
	; "Jul", { Month = 7}
	; "Aug", { Month = 8}
	; "Sep", { Month = 9}
	; "Oct", { Month = 10}
	; "Nov", { Month = 11}
	; "Dec", { Month = 12}
	).

% goofy - generalize, maybe?
digits(1, Value) -->
	dcg_basics:digit(D1),
	{ number_codes(Value, [D1]) }.
digits(2, Value) -->
	dcg_basics:digit(D1),
	dcg_basics:digit(D2),
	{ number_codes(Value, [D1, D2]) }.
digits(4, Value) -->
	dcg_basics:digit(D1),
	dcg_basics:digit(D2),
	dcg_basics:digit(D3),
	dcg_basics:digit(D4),
	{ number_codes(Value, [D1, D2,D3,D4]) }.


unit_test(  time([hour(0), minute(0), second(0)], "00:00:00", "") ).
unit_test(  time([hour(23), minute(59), second(59)], "23:59:59", "") ).

% convert_time(+Time, -Year, -Month, -Day, -Hour, -Minute, -Second, -MilliSeconds)

%  date defaults are today
%  time defaults are zero
to_posix_seconds(Parts, Time) :-
	get_time(Now),   % BUG:  should get GMT time for this!
	convert_time(Now, YearNow, MonthNow, DayNow, _HourNow,
		     _MinuteNow, _SecondNow, _MilliSecondNow),
	option(year(Year), Parts, YearNow),
	option(month(Month), Parts, MonthNow),
	option(day(Day), Parts, DayNow),
	option(hour(Hour), Parts, 0),
	option(minute(Minute), Parts, 0),
	option(second(Second), Parts, 0),
	option(millisecond(MilliSecond), Parts, 0),
	posix_leap_year(Leap, Year), 
	day_of_year(Leap, Month, Day, DayOfYear),
	days_since_1970(Year, Days),
	!,    % no time to track down who is leaving the choice point...
	Time is (MilliSecond / 1000 +
		 Second +
		60 * (Minute +
		     60 * (Hour +
			  24 * (Days + DayOfYear - 1) ))).
		
unit_test(  to_posix_seconds([year(1970), month(1), day(1)], 0)  ).
unit_test(  to_posix_seconds([year(1971), month(1), day(1),
			      hour(0), minute(0), second(0)], 31536000) ).
unit_test(  to_posix_seconds([year(1971), month(1), day(1),
			      hour(10), minute(10), second(10)], 31572610) ).
unit_test(  to_posix_seconds([year(1975), month(1), day(1),
			      hour(10), minute(10), second(10)], 157803010) ).
unit_test(  to_posix_seconds([year(2004), month(1), day(1),
			      hour(10), minute(10), second(10)], 1072951810) ).
unit_test(   to_posix_seconds([year(2004), month(3), day(11),
			       hour(19), minute(33), second(54)], 1079033634) ).

posix_leap_year(1, Year) :-
	0 is Year mod 4, !.
posix_leap_year(0, _Year).

% I vaguely remember a clever non-iterative way to do this....
%
% avoid iteration with
%    between(0,1,Leap), between(1,12,Month), day_of_year(Leap, Month, 1, DOY), writeq(day_of_year(Leap, Month, 1, DOY)), write('.'), nl, fail.
%
day_of_year(0, 1, 1, 1).
day_of_year(0, 2, 1, 32).
day_of_year(0, 3, 1, 60).
day_of_year(0, 4, 1, 91).
day_of_year(0, 5, 1, 121).
day_of_year(0, 6, 1, 152).
day_of_year(0, 7, 1, 182).
day_of_year(0, 8, 1, 213).
day_of_year(0, 9, 1, 244).
day_of_year(0, 10, 1, 274).
day_of_year(0, 11, 1, 305).
day_of_year(0, 12, 1, 335).
day_of_year(1, 1, 1, 1).
day_of_year(1, 2, 1, 32).
day_of_year(1, 3, 1, 61).
day_of_year(1, 4, 1, 92).
day_of_year(1, 5, 1, 122).
day_of_year(1, 6, 1, 153).
day_of_year(1, 7, 1, 183).
day_of_year(1, 8, 1, 214).
day_of_year(1, 9, 1, 245).
day_of_year(1, 10, 1, 275).
day_of_year(1, 11, 1, 306).
day_of_year(1, 12, 1, 336).

% day_of_year(+Leap, +Month, +DayOfMonth, -DayOfYear)
day_of_year(_Leap, 1, Day, Day) :- !.
day_of_year(Leap, Month, DayOfMonth, DayOfYear) :-
	PreviousMonth is Month - 1,
	days_in_month(Leap, PreviousMonth, DaysInPreviousMonth),
	day_of_year(Leap, PreviousMonth, 1, Base),
	!,    % ugh, without this cut we get repeated answers
	DayOfYear is Base + DayOfMonth + DaysInPreviousMonth - 1.



unit_test( day_of_year(1, 2, 1, 32) ).
unit_test( day_of_year(0, 12, 31, 365) ).
unit_test( day_of_year(1, 12, 31, 366) ).

% days_since_1970(+Year, -Days).
iter_days_since_1970(1970, 0).
iter_days_since_1970(Year, Days) :-
	Year > 1970,
	Previous is Year - 1,
	posix_leap_year(Leap, Previous),
	day_of_year(Leap, 12, 31, DaysInYear),
	iter_days_since_1970(Previous, PreviousDays), !,
	Days is DaysInYear + PreviousDays.

% between(1970, 2030, Year), iter_days_since_1970(Year, Days), writeq(days_since_1970(Year, Days)), write('.'), nl, fail.
days_since_1970(1970, 0).
days_since_1970(1971, 365).
days_since_1970(1972, 730).
days_since_1970(1973, 1096).
days_since_1970(1974, 1461).
days_since_1970(1975, 1826).
days_since_1970(1976, 2191).
days_since_1970(1977, 2557).
days_since_1970(1978, 2922).
days_since_1970(1979, 3287).
days_since_1970(1980, 3652).
days_since_1970(1981, 4018).
days_since_1970(1982, 4383).
days_since_1970(1983, 4748).
days_since_1970(1984, 5113).
days_since_1970(1985, 5479).
days_since_1970(1986, 5844).
days_since_1970(1987, 6209).
days_since_1970(1988, 6574).
days_since_1970(1989, 6940).
days_since_1970(1990, 7305).
days_since_1970(1991, 7670).
days_since_1970(1992, 8035).
days_since_1970(1993, 8401).
days_since_1970(1994, 8766).
days_since_1970(1995, 9131).
days_since_1970(1996, 9496).
days_since_1970(1997, 9862).
days_since_1970(1998, 10227).
days_since_1970(1999, 10592).
days_since_1970(2000, 10957).
days_since_1970(2001, 11323).
days_since_1970(2002, 11688).
days_since_1970(2003, 12053).
days_since_1970(2004, 12418).
days_since_1970(2005, 12784).
days_since_1970(2006, 13149).
days_since_1970(2007, 13514).
days_since_1970(2008, 13879).
days_since_1970(2009, 14245).
days_since_1970(2010, 14610).
days_since_1970(2011, 14975).
days_since_1970(2012, 15340).
days_since_1970(2013, 15706).
days_since_1970(2014, 16071).
days_since_1970(2015, 16436).
days_since_1970(2016, 16801).
days_since_1970(2017, 17167).
days_since_1970(2018, 17532).
days_since_1970(2019, 17897).
days_since_1970(2020, 18262).
days_since_1970(2021, 18628).
days_since_1970(2022, 18993).
days_since_1970(2023, 19358).
days_since_1970(2024, 19723).
days_since_1970(2025, 20089).
days_since_1970(2026, 20454).
days_since_1970(2027, 20819).
days_since_1970(2028, 21184).
days_since_1970(2029, 21550).
days_since_1970(2030, 21915).



% days_in_month(?Leap, ?Month, ?Days).
days_in_month(0, 1, 31).		
days_in_month(0, 2, 28).		
days_in_month(0, 3, 31).		
days_in_month(0, 4, 30).		
days_in_month(0, 5, 31).		
days_in_month(0, 6, 30).		
days_in_month(0, 7, 31).		
days_in_month(0, 8, 31).		
days_in_month(0, 9, 30).		
days_in_month(0, 10, 31).		
days_in_month(0, 11, 30).		
days_in_month(0, 12, 31).		
days_in_month(1, 1, 31).		
days_in_month(1, 2, 29).		
days_in_month(1, 3, 31).		
days_in_month(1, 4, 30).		
days_in_month(1, 5, 31).		
days_in_month(1, 6, 30).		
days_in_month(1, 7, 31).		
days_in_month(1, 8, 31).		
days_in_month(1, 9, 30).		
days_in_month(1, 10, 31).		
days_in_month(1, 11, 30).		
days_in_month(1, 12, 31).		

/*

       POSIX.1 defines seconds since the Epoch as a value to be interpreted as
       the number of seconds between a specified time and the Epoch, according
       to a formula for conversion from UTC equivalent to conversion on the na
       basis  that  leap  seconds are ignored and all years divisible by 4 are
       leap years.  This value is not the same as the actual number of seconds
       between  the  time  and  the Epoch, because of leap seconds and because
       clocks are not required to be synchronised  to  a  standard  reference.
       The  intention  is  that  the interpretation of seconds since the Epoch
       values be consistent; see POSIX.1 Annex B 2.2.2 for further  rationale.
 */

%%  ago(+Time, -Ago)
%%  relative_ago(+RelTime, -Ago)
%%
%%  Convert a unix-time number into a string which reflects how
%%  long ago the time was

ago(Time, Ago) :-
	get_time(Now),
	RelTime is round(Now - Time),
	relative_ago(RelTime, Ago).
reltime(Time, Message) :-
	get_time(Now),
	RelTime is round(Now - Time),
	(   RelTime < 0
	->  R is -RelTime,
            relative_ago(R, Ago),
	    atom_concat('in ', Ago, Message)
	;   relative_ago(RelTime, Ago),
	    atom_concat(Ago, ' ago', Message)
	).
	
relative_ago(Seconds, 'future') :- Seconds < 0.
relative_ago(Seconds, Ago) :-
	time_units(Divisor, Name, Max),
	Value is integer(Seconds / Divisor),
	Value < Max,
	atom_number(Atom, Value),
	(   Value = 1
	->  PluralEnding = ''
	;   PluralEnding = 's'
	),
	concat_atom([Atom, ' ', Name, PluralEnding], Ago),
	!.

time_units(1, second, 60).
time_units(60, minute, 60).
time_units(3600, hour, 24).
time_units(86400, day, 28).
time_units(7*86400, week, 52).
time_units(365*86400, year, 1000).

unit_test(relative_ago(-2, 'future')).
unit_test((  relative_ago(1, A), A = '1 second'  )).
unit_test((  relative_ago(2, A), A = '2 seconds'  )).
unit_test((  relative_ago(59, A), A = '59 seconds'  )).
unit_test((  relative_ago(59.9, A), A = '1 minute'  )).
unit_test((  relative_ago(59.4, A), A = '59 seconds'  )).
unit_test((  relative_ago(59.6, A), A = '1 minute'  )).
unit_test((  relative_ago(3600, A), A = '1 hour'  )).
unit_test((  relative_ago(3500, A), A = '58 minutes'  )).
unit_test((  relative_ago(86401, A), A = '1 day'  )).
unit_test((  relative_ago(864000, A), A = '10 days'  )).
unit_test((  relative_ago(8640000, A), A = '14 weeks'  )).
unit_test((  relative_ago(86400000, A), A = '3 years'  )).
unit_test((  relative_ago(864000000, A), A = '27 years'  )).

month_name(1, 'January').
month_name(2, 'February').
month_name(3, 'March').
month_name(4, 'April').
month_name(5, 'May').
month_name(6, 'June').
month_name(7, 'July').
month_name(8, 'August').
month_name(9, 'September').
month_name(10, 'October').
month_name(11, 'November').
month_name(12, 'December').
