// -------------------------------------------------------------------------------------
// A sample program in C performing some calculations between two Gregorian dates
// 	difference in days and in (d,m,y) form, weekday & leap years indicators
//	jdn conversions, +/- arithmetic
// -------------------------------------------------------------------------------------
// migf1, Athens 2011 * use at your own risk * free for whatever use, plz give me credit
// -------------------------------------------------------------------------------------
// external alogorithms:
//		http://w...content-available-to-author-only...t.com/KB/datetime/DateDurationCalculation1.aspx
// 		http://w...content-available-to-author-only...c.ch/cal_stud/jdn.htm#comp

#include <stdio.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <stdlib.h>

#define MAX_INBUF	255+1				// for our input buffer
#define INVALID		-1				// just a flag for invalid d,m,y

#define validday(d)	( (d) > 0 && (d) < 32 )		// 1st validity check for days
#define validmonth(m)	( (m) > 0 && (m) < 13 )		// 1st validity check for months
#define validyear(y)	( (y) > 1751 && (y) < 10000 )	// 1st validity check for years

#define isleap(y)	( !((y) % 4) && ( (y) % 100 || !((y) % 400) ) )

#define myabs(x)	( (x) < 0 ? -(x) : (x) )	// absolute value

typedef enum bool { FALSE=0, TRUE } bool;		// our custom boolean type
enum { JAN=1, FEB };					// just two month constants
enum { ID1=1, ID2 };					// to identify 1st and 2nd dates

typedef struct Date {					// our date structure
	long d;						// 1-31
	long m;						// 1-12
	long y;						// 1752-9999
} Date;

// ------------------------------------------------------------------------------------
// Read s from stdin until either len chars have been typed or ENTER has been hit,
// and null-terminate s (if ENTER was there, it is replaced).
// Return the null-terminated s
//
char *s_get(char *s, size_t len)
{
	char *cp;
	for (cp=s; (*cp=getc(stdin)) != '\n' && (cp-s) < len-1; cp++ )
			;				// for-loop with empty body
	*cp = '\0';					// null-terminate last character

	return s;
}

// ------------------------------------------------------------------------------------
// Trim leading & trailing blanks from string s, pad it with '\0's and return it
// (or NULL on error)
//
char *s_trim(char *s)
{
	if ( !s || !*s )				// error, early exit
		return NULL;

	char *cp1;					// for parsing the whole s
	char *cp2;					// for shifting & padding

	// trim leading & shift left remaining
	for (cp1=s; isblank((int)*cp1); cp1++ )	// skip leading blanks, via cp1
		;
	for (cp2=s; *cp1; cp1++, cp2++)			// shift-left remaining chars, via cp2
		*cp2 = *cp1;
	*cp2-- = '\0';					// mark end of left trimmed s

	// replace trailing blanks with '\0's
	while ( cp2 > s && isblank((int)*cp2) )
		*cp2-- = '\0';				// pad with '\0'

	return s;
}

// ------------------------------------------------------------------------------------
// Convert s to lowercase and return it (or NULL on error)
//
char *s_tolower( char *s )
{
	if ( !s  )					// error, early exit
		return NULL;

	char *ret;
	for ( ret=s; (*s=tolower(*s)); s++ )
		;
	return ret;
}

// ------------------------------------------------------------------------------------
// break a string up to maxtokens tokens and store them in *tokens[]
// (uses " " as the delimeter string)
// returns the number of tokens, or 0 on failure
// ------------------------------------------------------------------------------------

int s_tokenize(char *s, char *tokens[], int maxtokens, char *delimiters)
{
	if ( !s || !*s || !tokens || !maxtokens || !delimiters || !*delimiters )
		return 0;

	register int i=0;

	tokens[0] = strtok(s, delimiters);
	if (tokens[0] == NULL)
		return 0;
	for (i=1; i < maxtokens && (tokens[i]=strtok(NULL, delimiters)) != NULL; i++); 

	return i;
}

// ------------------------------------------------------------------------------------
// Convert a Gregorian date to a Julian Day count
// IMPORTANT: accurate ONLY for dates after Oct 15, 1582 (Gregorian Calendar)
// 				Algorithm by Henry F. Fliegel & Thomas C. Van Flandern:
//				http://w...content-available-to-author-only...c.ch/cal_stud/jdn.htm#comp
//
long date_2jdn( Date date )
{
	return
		( 1461 * ( date.y + 4800 + ( date.m - 14 ) / 12 ) ) / 4 +
		( 367 * ( date.m - 2 - 12 * ( ( date.m - 14 ) / 12 ) ) ) / 12 -
		( 3 * ( ( date.y + 4900 + ( date.m - 14 ) / 12 ) / 100 ) ) / 4 +
		date.d - 32075;

}

// ----------------------------------------------------------------------------------
// Convert a Julian Day count to a Gregorian date (d,m,y)
// IMPORTANT: accurate ONLY for dates after Oct 15, 1582 (Gregorian Calendar)
// 				Algorithm by Henry F. Fliegel & Thomas C. Van Flandern:
//				http://w...content-available-to-author-only...c.ch/cal_stud/jdn.htm#comp
//
Date *jdn_2date( Date *date, long jd )
{
        long l = jd + 68569;
        long n = ( 4 * l ) / 146097;
        l = l - ( 146097 * n + 3 ) / 4;
        long i = ( 4000 * ( l + 1 ) ) / 1461001;
        l = l - ( 1461 * i ) / 4 + 31;
        long j = ( 80 * l ) / 2447;
        date->d = l - ( 2447 * j ) / 80;
        l = j / 11;
        date->m = j + 2 - ( 12 * l );
        date->y = 100 * ( n - 49 ) + i + l;

		return date;
}

// ------------------------------------------------------------------------------------
// Convert Gregorian date to weekday (valid from Sep 14, 1752 to Dec 31, 9999)
// Return 0 to 6 (Mon to Sun)
//
int date_2weekday( Date date )
{
	return date_2jdn(date) % 7;		// return jdn % 7
}

// ------------------------------------------------------------------------------------
// Calc the difference between date1 and date2 and RETURN it expressed as # of days.
// IMPORTANT:,
// the difference is also calc'ed as days, months, years and passed into datediff
//		Algorithm by Mohammed Ali Babu
//		http://w...content-available-to-author-only...t.com/KB/datetime/DateDurationCalculation1.aspx
//
long date_diff( Date *datediff, Date date1, Date date2, int mdays[] )
{
	if ( !datediff )
		return -1;

	long int jdn1 = date_2jdn( date1 );		// calc jd of date1
	long int jdn2 = date_2jdn( date2 );		// calc jd of date2
	Date *dp2 = (jdn2 > jdn1) ? &date2 : &date1;	// dp2 points to latest date
	Date *dp1 = (dp2 == &date1) ? &date2 : &date1;	// dp1 points to earliest date

	/*
	 * the following alogorithm is published by Mohammed Ali Babu at:
	 * http://w...content-available-to-author-only...t.com/KB/datetime/DateDurationCalculation1.aspx
	 */

	// first calc the difference of the day part
	int increment = 0; 
	if ( dp1->d > dp2->d )
		increment = mdays[ dp1->m - 1 ]; 

	if (increment == -1)
	{
		if ( isleap( dp1->y ) )
			increment = 29;
		else
			increment = 28;
	}

	if (increment != 0)
	{
		datediff->d = (dp2->d + increment) - dp1->d;
		increment = 1; 
	}
	else
		datediff->d = dp2->d - dp1->d;

	// then calc the difference of the month part
	if ( (dp1->m + increment) > dp2->m )
	{
		datediff->m = (dp2->m + 12) - (dp1->m + increment);
		increment = 1;
	}
	else {
		datediff->m = dp2->m - (dp1->m + increment);
		increment = 0;
	}

	// and last calculate the difference of the year part
	datediff->y = dp2->y - (dp1->y + increment);


	return myabs( jdn2-jdn1 );
}

// ------------------------------------------------------------------------------------
// Add ndays to basedate and Return the result into date (ndays can be negative)
// (calstart and calend are used for boundary checking)
//
Date *date_plusdays( Date *date, Date basedate, long ndays, Date calstart, Date calend )
{
	long jstart = date_2jdn( calstart );	// julian day of our calendar start date
	long jend = date_2jdn( calend );	// julian day of our calendar end date
	long jd = date_2jdn( basedate );	// julian day of basedate

	if ( jd+ndays > jend )			// fix overflow (calend)
		return jdn_2date( date, jend );

	if ( jd+ndays < jstart )		// fix underflow (calstart)
		return jdn_2date( date, jstart );

	return jdn_2date( date, jd+ndays );
}

// ------------------------------------------------------------------------------------
// Convert string inbuf into a valid signed long int, store it in date->d
// Return FALSE on failure
//
bool date_getoperation( Date *date, char *inbuf )
{
	if ( !inbuf || !*inbuf || (*inbuf != '+' && *inbuf != '-') )
		return FALSE;

	char *tail;					// in strtol() for err checking

	errno = 0;
	date->m = date->y = INVALID;
	date->d = strtol(inbuf, &tail, 10);		// convert inbuf to long
	if ( *tail != '\0' || errno == ERANGE )
	{
		puts("\t*** error: invalid operation");
		date->d = INVALID;
		return FALSE;
	}

	return TRUE;
}

// ------------------------------------------------------------------------------------
// If string inbuf contains any of our recognized mnemonics, date is set accordingly
// and the function returns TRUE (otherwise it returns FALSE).
//
bool date_getmnemonic( Date *date, char *inbuf, const Date calstart, const Date calend )
{
	if ( !inbuf || !*inbuf )		// inbuf is either non-existant or empty
		return FALSE;			// early exit

	time_t today = time( NULL );		// get system time
	struct tm *tmtoday = localtime( &today );// convert it to a tm structure
	long jstart = date_2jdn( calstart );	// julian day of our calendar start date
	long jend = date_2jdn( calend );	// julian day of our calendar end date
	long jtoday;
	Date caltoday;

	caltoday.d = (long) tmtoday->tm_mday;
	caltoday.m = (long) tmtoday->tm_mon + 1;
	caltoday.y = (long) tmtoday->tm_year + 1900;
	jtoday = date_2jdn( caltoday );
	
	if  ( !strcmp( inbuf, "start") ) {
		jdn_2date( date, jstart );
		return TRUE;
	}
	if  ( !strcmp( inbuf, "yesterday") ) {
//		jdn_2date( date, jtoday-1 );	// faster but no boundary checking
		date_plusdays( date, caltoday, -1, calstart, calend);
		return TRUE;
	}
	if  ( !strcmp( inbuf, "today") ) {
		jdn_2date( date, jtoday );
		return TRUE;
	}
	if  ( !strcmp( inbuf, "tomorrow") ) {
//		jdn_2date( date, jtoday+1 );	// faster but no boundary checking
		date_plusdays( date, caltoday, +1, calstart, calend);
		return TRUE;
	}
	if  ( !strcmp( inbuf, "end") ) {
		jdn_2date( date, jend );
		return TRUE;
	}

	return FALSE;
}

// ------------------------------------------------------------------------------------
// Read a Gregorian date as a string form stdin, validate it & return it inside *date
// IMPORTANT:
//
//	The function returns TRUE if the input string is a valid operation (i.e "-200" )
//	instead of a valid date string (i.e. "1/1/2000" ). In that case, date->d
//	is assigned the numeric value of the operation ( -200 for the above example )
//	while date->m and date->y are assined the value -1 ( INVALID ).
//
//	Only the second date is allowed to accept an operation string, so we use
//	the parameter 'id' in order to know whether we're dealing with date1 or
//	date2 in here.
//
//	Since in case of a valid operation in the input, the returned date does NOT
//	hold a valid date, further processing needs to be done in the main() function,
//	to ensure that date2 is properly converted to a valid date, equalling to:
//	date1 + date2.d, before attempting to do anything else with it (e.g. printing
//	it)
//	
//
bool date_askuser( int id, char *prompt, Date *date,
		   int max_inbuf, int mdays[], Date calstart, Date calend  )
{
	if ( !prompt || !date)			// inbuf does not exist or it is empty
		return FALSE;

	char inbuf[ max_inbuf ];			// for reading the user input
	char *stokens[3];				// to read d, m, y as strings
	char *tail;					// in strtol() for err checking
	long jdn;					// for converting date to jdn
	bool stop 	= TRUE;				// for controlling the main loop
	bool operated	= FALSE;			// got a date or an operation?

	do
	{
		stop = TRUE;				// reset boolean flag to TRUE
		operated = FALSE;			// reset boolean flag to FALSE

		// prompt and get user input
		printf( prompt );			// ask for input
		fflush(stdin);				// clear input buffer (stdin)
		s_get(inbuf, max_inbuf);		// read input as string in inbuf
		s_trim( s_tolower(inbuf) );		// trim leading & trailng blanks

		// check if user typed just an ENTER
		if ( !*inbuf )
			continue;			// stop = TRUE

		// check if user typed an operation (+/-) followed by a number
		if ( id == ID1 && (*inbuf =='+' || *inbuf == '-') ) {
			puts("\t*** error: operations only allowed in the 2nd date");
			stop = FALSE;
			continue;
		}
		if ( date_getoperation(date, inbuf)  ) {
			operated = TRUE;
			continue;			// stop = TRUE
		}

		// check if user typed any of our mnemonic strings
		if ( date_getmnemonic( date, inbuf, calstart, calend ) ) {
			stop = TRUE;
			continue;
		}

		// split inbuf in up to 3 strings (tokens) assuming d, m, y
		if ( s_tokenize( inbuf, stokens, 3, " /,.;:\t") != 3 ) {
			puts("\t*** error: invalid date");
			stop = FALSE;
			continue;
		}

		// demand from user to type a day between 1-31
		errno = 0;				// reset golbal var errno
		date->d = strtol(stokens[0], &tail, 10);// convert str day to long
		if ( !validday(date->d) || *tail != '\0' || errno == ERANGE )
		{
			puts("\t*** error: valid days are 1-31");
			date->d = INVALID;
			stop = FALSE;
			continue;
		}

		// demand from user to type a month between 1-12
		errno = 0;				// reset golbal var errno
		date->m = strtol(stokens[1], &tail, 10);// convert str month to long
		if ( *tail != '\0' || errno == ERANGE || !validmonth(date->m) )
		{
			puts("\t*** error: valid months are 1-12");
			date->m = INVALID;
			stop = FALSE;
			continue;
		}

		// demand from user to type a year between 1752-9999
		errno = 0;				// reset golbal errno
		date->y = strtol(stokens[2], &tail, 10);// convert str year to long
		if ( *tail != '\0' || errno == ERANGE || !validyear(date->y) )
		{
			puts("\t*** error: valid years are 1752-9999");
			date->y = INVALID;
			stop = FALSE;
			continue;
		}

		/* now we have a complete date (d,m,y) but this
		 * does not gurantee us that it is a valid one
		 * (e.g. 29/2/2000 is valid, but 29/2/2001 is not)
		 * (also, e.g. 31/4 is invalid, an so on)
		 */

		// ensure day lyes inside the month boundary of the typed date
		if ( date->d > mdays[ date->m - 1 ] )
		{
			if ( date->d == 29 && date->m == FEB && isleap( date->y ) )
				continue;			// stop = TRUE

			date->d = date->m = date->y = INVALID;
			puts("\t*** error: invalid day");
			stop = FALSE;
			continue;
		}

		// ensure date is between 14/9/1752 and 31/12/9999
		jdn = date_2jdn( *date );			// convert date to day
		if ( date_2jdn(calstart) > jdn || jdn > date_2jdn(calend) )
		{
			date->d = date->m = date->y = INVALID;
			puts("\t*** error: valid dates are 14/9/1752 - 31/12/9999");
			stop = FALSE;
			continue;
		}

	} while( !stop || !*inbuf );

	return operated;
}

// ------------------------------------------------------------------------------------
void print_help( void )
{
	printf("\n\n%70s\n", "<mig_f1@hotmail.com>\n");
	puts("A sample program performing various calculations between two dates in \nthe time interval: 14/9/1752 to 31/12/9999 (results never exceed it).\n\nEach date input is checked in real time against both syntax & logical\nerrors. It can be in one of the following formats...\n-\td/m/y\t\t: other separators: :,.; and tab\n-\ta signed number\t: e.g. -200 subtracts 200 days from 1st Date\n-\tstart\t\t: equals to 14/9/1752\n-\tyesterday\t: equals to yesterday's date\n-\ttoday\t\t: equals to today's date\n-\ttomorrow\t: equals to tomorrow's date\n-\tend\t\t: equals to 31/12/9999\n\nThe calculated results include...\n-\tdifference expressed in days\n-\tdifference expressed in days, months, years\n-\tweekday indication\n-\tJulian Day Number indication\n-\tleap year indication");

	return;
}

// ------------------------------------------------------------------------------------
void print_results(long int ndays, Date date, char *mnames[], char *dnames[] )
{
	printf("\tDiff:\t%ld day(s)\n", ndays);
	printf(	"\t\t%ld years(s), %02ld months(s) and %02ld days(s)\n",
		date.y, date.m, date.d	);

	return;
}

// ------------------------------------------------------------------------------------
void print_date(Date date, int id, char *mnames[], char *dnames[] )
{
	if ( !validday( date.d ) || !validmonth( date.m ) || !validyear( date.y ) )
		return;

	printf(	"\t%d. %s %02ld %04ld, %-09s (jdn: %ld) %s\n",
		id,
		mnames[ (int)(date.m)-1 ],
		date.d, date.y, dnames[ date_2weekday(date) ],
		date_2jdn( date ),
		isleap( date.y ) ? "\t(leap year)" : ""
	);

	return;
}

// ------------------------------------------------------------------------------------
int main( void )
{
	char *dnames[7] = {				// day names
		"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
	};
	char *mnames[12] = {				// month names
		"JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"
	};
							// month lengths in days
	int mdays[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	Date calstart 	= { .d=14, .m=9,  .y=1752 };	// our calendar starting date
	Date calend	= { .d=31, .m=12, .y=9999 };	// our calendar ending date

	Date date1, date2;				// dates to be read from user
	Date diffdate;					// for holding diff as (d,m,y)
	long ndays;					// difference in days
	bool operated = FALSE;				// input was date or operation?
	char inbuf[MAX_INBUF] = "";			// our own input buffer

	do
	{
		print_help();

		// read date1 and date2
		date_askuser(	ID1, "\nFirst date  (d/m/y): ", &date1,
				MAX_INBUF, mdays, calstart, calend	);
		operated = date_askuser(
				ID2, "Second date (d/m/y): ", &date2,
				MAX_INBUF, mdays, calstart, calend	);

		// if date2 was an operation, store the resulting date into date2
		if ( operated )
			date_plusdays( &date2, date1, date2.d, calstart, calend);

		// calc difference between date1 and date2
		ndays = date_diff( &diffdate, date1, date2, mdays );

		// print dates and results
		putchar('\n');
		puts("\t--------------------------------------------------");
		print_date( date1, ID1, mnames, dnames );
		print_date( date2, ID2, mnames, dnames );
		puts("\t--------------------------------------------------");
		print_results( ndays, diffdate, mnames, dnames );
		puts("\t--------------------------------------------------");

		printf("\ntry again (/n): ");
		fflush(stdin);
	} while ( *s_tolower( s_get(inbuf, MAX_INBUF) ) != 'n' );

	exit(0);
}
