Page 1 of 1

Calendar program in C without using any date or time functions. Rate Topic: -----

#1 finetunewithhammer   User is offline

  • D.I.C Head

Reputation: 17
  • View blog
  • Posts: 86
  • Joined: 05-February 21

Posted 12 February 2021 - 09:49 AM

Here is a calendar program I made for submission to this forum.
It even calculates week numbers!
It uses few external functions for formatting, printing and interacting with the user.
The logic that produces the calendar is not dependent on any external function.

The program consists of functions listed below. The last three ones are used to determine the week number. They are not necessary for producing a calendar without week numbers.
I use the ISO-8601 week numbering. This is not the only week numbering scheme in use.

int isLeapYear(int year)
int daysInYear(int year)
int firstWeekdayOfYear(int year)
int daysInMonth(int month, int year)
int firstWeekdayOfMonth(int month, int year)
int firstWeekNumberOfYear(int year)
int firstWeekNumber(int month, int year)
int printMonth(int month, int year)


The bulk of the documentation is in the code comments, close to the pertinent code where it can be read in context and understood better.


/*
This program prints out the Gregorian calendar.
It is based on a calendar I wrote for the casio graphing calculator in 2008. (That did not support week numbering.)


A Calendar should:
1) Give an accurate reading on the progression of the tropical year.
This is achieved with an accuracy of 1 day with the Gregorian calendar.

2) Produce an accurate count of time (in days) elapsed between two dates.
This can be calculated exactly, but is not readily accessible for long time intervals.

The tropical year and the length of a day-night cycle are natural phenomena.
They are not syncronized with eachother.

Important key events in the tropical year are the solstices and equinoxes.
These happen at a certain point in time for the entire planet.
This point in time is obviusly happens at different local time.
From here follows that it also will fall on different calendar days, depending on where on earth you are.


My source for information on the calendar is the book:
"CALENDAR - Humanity's Epic Struggle to Determine a True and Accurate Year" by David Ewing Duncan.
I warmly recommend this book to anyone interested in the historic specifics of the calendar. It is a fun read.

My source for information on week numbering:
en.wikipedia.org/wiki/ISO_week_date


Notes on the program:
Weekday is internally represented as an integer [1..7]
For calculations where division and reminder operations on repeating cycles (such as weeks) are concerned, we need to map this to [0..6]. In both cases the week starts with a monday.
In retrospect, it would have been better to always refer to a weekday as [0..6]

The function that determines the first weekday of a year is independent from the function that determines week numbering.

The function that determines week numbering needs the function that determines the first weekday of a year to operate.


Usage:

calendar 2021 1

calendar 1 2021


*/
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

const char *dayNames[] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
const char *monthNames[] = { "January", "Febuary", "March", "April", "May", "June",
				"July", "August", "September", "October", "November", "December" };


/*
getYear() and getMonth() Processes the command line arguments of this program.
They are not necessary for the functionality of the calendar, their only use is in main()
*/

//Find year in command line arguments.
int getYear(char **args, int argc)
	{
	int ai=0;
	int year=0;	
	int num;

	for( ai=1; ai<argc; ai++ )
		{
		if( isdigit(*args[ai]) )
			num = atoi(args[ai]);
		if( year < num )
			year = num;		  
		}
	if( year < 1 )		//Return Year 1 if year is out of bounds.
		return 1;
	return year;
	}

//Find month in command line arguments.
int getMonth(char **args, int argc)
	{
	int ai=0;
	int month=13;
	int num;

	for( ai=0; ai<argc; ai++ )
		{
		if( isdigit(*args[ai]) )
			num = atoi(args[ai]);
		if( month > num )
			month = num;		  
		}
	if( month > 12 || month < 1 )	//Return January if month is out of bounds.
		return 1;	
	return month;
	}


//Determines if a year is a leap year. 0 is false 1 is true.
int isLeapYear(int year)
	{
	int leap=0;	
	if( year % 4 == 0 )
		leap = 1;
	if( year % 100 == 0 )
		leap = 0;
	if( year % 400 == 0 )
		leap = 1;
	return leap;
	}


//Returns the length of a year in days.
int daysInYear(int year)
	{
	if( isLeapYear(year) )
		return 366;
	return 365;
	}


/*
DETERMINING THE FIRST WEEKDAY OF ANY YEAR


How to produce this algorithm:

1) Using the knowledge of how leapyears are determined, we demonstrate that the exact same sequence of year lengths repeat every 400 years.

2) We calculate the length in days of such a 400 year sequence.

3) We calculate that the length of days of such a sequence is divisible by the length of a 7 day week.

4) We note that January 1. 2001 was a monday.

5) We deduce that each 400 year sequence starts with a monday.

6) We demonstrate that it is possible to use the leapyear rules to calculate year lengths for the years from the
start of the sequence to the position of our year of interest in it, and that the daycount offset from the start
of the sequence correlates to the daycount offset of January 1. in our year of interest, such that we can use
this daycount to determine the correct weekday of any year.

Note: January 1. 2001 is not magical. It just happened to be the start of the sequence closest to my time.
We could pick January 1. 2401, which is also a monday.



According to the leapyear rules of the common calendar, a years length in days alternate between the values 365 and 366 according to the following rules:

	A year is;
		365 days
		unless it is divisible by 4, then it is 366 days.
		unless it is divisible by 100, then it is 365 days.
		unless it is divisible by 400, then it is 366 days.

Since all numbers 4 100 and 400 are divisible by eachother, this results in consecutive sequences of 400 years, all being equally long in number of days. (This is important. If these numbers were not divisible, we would have to concider a sequence of years that is the product of the divisors in question.)

This 400 years has the sequence of regular years and leapyears that repeats exactly as is over time. The number 400 is not magical it is just the smallest common multiple of the cycles that determine the leapyear rules. 
	
	How many days are there in such a 400 year sequence?
	years	divider	result	yearCount
	400	1	400	sequence	
	400	4	100	nDivBy4
	400	100	4	nDivBy100
	400	400	1	nDivBy400

Note that each yearCount contains the subsequent ones. nDivBy4 will contain the years in nDivBy100.

	The amount of years with 365 days in the sequence are:
		sequence - nDivBy4 + nDivBy100 - nDivBy400
		400 - 100 + 4 - 1 = 303

	The amount of years with 366 days in the sequence are:
		nDivBy4 - nDivBy100 + nDivBy400
		100 - 4 + 1 = 97

Each of these subsequent 400 year periods, have the same amount of days.
(1) The order of leap years and regular years is the same within each sequence.

(2) The amount of days 146097 is divisible by 7, there is exactly 20871 weeks in any such sequence.
	
Given (1) and (2), follows that the same sequence of Weekdays, (starting at the beginning of each sequence), repeats every 400 years.

January 1. 2001 was a Monday. Every 400 years, that is 2401, 2801 and so on, and with some restrictions every 400 year sequence before that started with a monday.

(In the middle ages it was re-discovered that the calendar was out of sync with the tropical year, and it took approximately half a millennia for everyone to get aboard with the reforms. In the past there will be a time when this algorithm breaks, not because it is yet out of sync with the past tropical year, but because the calendars back then were. If my memory serves they had to remove 10 days from the calendar to syncronize the calendar year back to the tropical year, and expand to the current leap year rules to reduce the error.)



In the algorithm we do not utilize the divisible by 400 rule at all, how is it possible to get an accurate count?
	
We have to consider a 400 year sequence. The years [1..400]
We need the first year of the sequence to be 2001.
We need the sequence to be numbered [0..399]

A simple way to go from a year number to a sequence number is to take the remainder, from divison of the year number by sequence length. (e.g. 2021 % 400 = 21)
	
The result of n % 400 is in the range [0..399]
Why do we have to do anything special to get the specific sequence we want?
	
Here the sequence [0..399] corresponds to years [2000..2399], [2400..2799]..
We want it to correspond to the sequences [2001..2400], [2401..2800]..
A simple way to achive this is to subtract one from the year number before taking the remainder.

We are using a known starting point from where to calculate numbers of elapsed days to the beginning of our year of interest. We know that this starting point will always be the same weekday.
	
We can not map the sequence [0..399] to start at just any year.
We use the year of the sequence to detemine how many leap years there are between the year of interest and a fixed known year. The sequence of the real leap years must be syncronized with the sequnece of leap years for the "year value" within the 'sequence' [0..399].
In essence we want to be able to determine the year length within the sequnece using the normal leap year rules. The alternative would be to have a table of year lengths for any arbitrary 400 year sequence we would have chosen.

	
The year we are looking to determine the first weekday to, can be in the past or in the future, relative to our arbitrary known sequence that starts January 1. 2001, with a monday. We calculate the amount of days from the start of whichever sequence the year of interest happens to be part of and using this day count, we take the remainder:

	daycount % 7
	To determine the weekday in the range [0..6]

	and add 1 to it:
	daycount % 7 + 1
	To change this to a more familiar ordinal in the range [1..7] representing the weekday, [monday..sunday]

It is important to note that the last year of a sequence, one divisible by 400, produces 0 as a result to the remainder and division operations used, ultimately affixing that weekday to the internal value of zero, external ordinal 1 (monday). 

It is equally a happy accident and also somewhat irrelevant the the first day of this sequence happens to be a monday.

As long as the starting point for adding days from the beginning of the sequence, to our year of interest, already contained the value of that day as a value in the range [0..6], the results would still be correct. Having to initialize the counter for a cycle of 0 to 6 to anything but 0 would be confusing, so it's rather great that we do not have to do that.
	
	
How firstWeekdayOfYear() actually works:
We calculate the days from the beginning of the 400 year sequence to the first of january of 'year'.
We take the reminder of this daycount over the length of a week (7) (i.e. daycount % 7) This is the relative count from the weekday of the start of the sequence (which is a monday).
Since that day is internally represented as 0, we do not need to add this offset to anything, our starting point is zero. We add one to the offset to turn it into an ordinal number. (i.e. [0..6] -> [1..7])

The code was metric tonnes simpler to write than this documentation.
*/
int firstWeekdayOfYear(int year)
	{
	int weekdayDateCycle;	
	int j, m, n, q;
	int sequence;
	int nDivBy4;
	int nDivBy100;
	int dayCount;
	
	sequence = (year - 1) % 400;
	nDivBy4 = sequence / 4;
	nDivBy100 = sequence / 100;
	dayCount = (sequence - nDivBy4 + nDivBy100) * 365 + (nDivBy4 - nDivBy100) * 366;
	
	return dayCount % 7 + 1;
	}


//Determines and returns the length of a month.
int daysInMonth(int month, int year)
	{
	int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if( month == 2 )
		return days[month] + isLeapYear(year);
	else
		return days[month];
	}


/*
Here we count the days until the firts of the month from January 1. this year.
Note that firstWeekdayOfYear() gives us an ordinal weekday number [1..7]
For the calcultions, we need to subtract 1 to get a weekday number in the range [0..6]
Later we add back 1 to return to the ordinal weekday number of [1..7]
*/
int firstWeekdayOfMonth(int month, int year)
	{
	int mi;
	int dayCount=0;
	
	for( mi=1; mi<month; mi++ )
		{
		dayCount += daysInMonth(mi, year);
		}
	return (firstWeekdayOfYear(year) - 1 + dayCount ) % 7 + 1  ;
	}


/*
There are 52 or 53 numbered weeks in a year.
The first days of a year may be part of the previous years week 53 or 52 or the current years week number 1.
(e.g. 2021 starts in week 53)

The first week of the year is week 1 if more days of that week are part of this year than the previous one.

If the majority of that weeks days are part of last year, that week is numbered as part of last years weeks as either 52 or 53. To find the week number for an incomplete last week of the previous year, and subsequently the week number that
the first days of this year belong to:

	a = Find the weekday that this year starts with.
	b = Count number of days before first monday of this year.
	if b < 4 then first week of this year is week 1.
	else:
	c = length of LAST year in days.
	d = Find the weekday that LAST year starts with.
	e = Count number of days in LAST years beginning belonging to first week of that year.

(e.g. If a year starts with a Thursday, the first week will have 4 days that are part of that year.) 
Likewise:
	Weekday|First	|Days first week
	ordinal|weekday	|has in a year.
	1 	Monday		7				
	2	Tuesday		6
	3	Wednesday	5
	4	Thursday	4
	5	Friday		3	Part of previous year.
	6	Saturday	2	Part of previous year.
	7	Sunday		1	Part of previous year.

To calculate the third column from the first one use:
DaysFirstWeekHasInAYear = 8 - WeekdayOrdinal

	f = c - e Number of days since week 1 started, left in this year.

Note that the first week of the year does not have to have all of its 7 days in this year. The maximal length of a year is 366 days for the last week of the year to be week number 53, there has to be at least 4 days of that week in this year. This leaves 362 days for the remaining 52 weeks.
362 / 7 is ~51.71	Now we have 51 full weeks, leaving one partial week
362 % 7 is 5		These are the upto 5, (essentially 5 or 4) days in the beginning of the year belonging to week 1.


In order to calculate the last weeks' week number for LAST year (i.e. this years first week number in cases when the first days of the year belong to a week that has a majority of its days in the LAST year)
we have to:
1) Remove the first partial week from the day count of LAST year.
2) Remove the 51 full weeks from the day count of LAST year.
Now we have already gotten 52 numbered weeks out of LAST years day count.
3) See if the remaining day count for LAST year is enough (4 or greater) to warrant there being a 53. week.
If it is the case that LAST year ends in a 53. week, then this year will begin with that same week.
*/
int firstWeekNumberOfYear(int year)
	{
	int week=0;
	int daysOfFirstWeek;	//That are part of that year. First week can be week 1, 52 or 53.
	int daysLeftInYear;

	//In case THIS year starts with week one.
	//First day of new year is monday, tuesday, wednesday, or thursday. [1..4] from [1..7] 
	if ( firstWeekdayOfMonth(1, year) <= 4 )
		return 1;	//week is 1.

	//How many days of LAST year is part of its first week.	
	daysOfFirstWeek = 8 - firstWeekdayOfMonth(1, year - 1);
	
	//Get total day count of LAST year and remove first weeks days from it
	daysLeftInYear = daysInYear(year-1) - daysOfFirstWeek;

	//If LAST year starts on or before thursday, add a week to the week count.
	//Since this was the first week of last year.
	if( firstWeekdayOfMonth(1, year - 1) <= 4 )
		week += 1;

	//Count whole weeks of LAST year.
	week += daysLeftInYear / 7;
	//Remove these weeks from day count.
	daysLeftInYear = daysLeftInYear % 7;

	//Determine if last days of last year are part of week one of this year,
	//if not add one to week count.
	if( daysLeftInYear >= 4 )
		week++;
	
	return week;	//week is 52 or 53.	
	}


int firstWeekNumber(int month, int year)
	{
	int daysUptoMonth;
	int mi=1;		//month iterator
	int fwn;		//first week number
	
	//The year does not always start with a full week. Will be one of [ 1, 52, 53 ]
	fwn = firstWeekNumberOfYear(year);

	if( month == 1 )
		return fwn;
	
	/*
	Here daysUptoMonth is counted from zero.
	After calculating the division we add one to indicate an ordinal week.
	There is no zero week.
	*/
	daysUptoMonth = 0;	
	
	//Add the day counts of this years months before 'month' together.
	while (mi < month)
		{
		daysUptoMonth += daysInMonth(mi, year);
		mi++;
		}

	//Year did not start with week 1.
	if( fwn != 1 )
		{
		//Remove days of this year before first week from daycount upto 'month'.
		daysUptoMonth -= ( 8 - firstWeekdayOfYear(year) ) ;
		}
	//first week is week number one.
	else
		{
		//Add days of first week that are part of the previous year to daycount upto 'month'.
		//We count the missing days from this years first week as this is the same amount.
		daysUptoMonth += firstWeekdayOfYear(year) - 1 ;
		}

	//daysUptoMonth / 7 produces a week numbering starting at zero.
	//Add one to this to get an ordinal number starting at one.
	return daysUptoMonth / 7 + 1;
	}



//Print the calendar of 'month' in 'year'
int printMonth(int month, int year)
	{
	int col;	// Printout grid column.
	int row;	// Printout grid row.
	int day;	// [1..7] Monday..Sunday
	int dayCount;	// Last day to print out in the grid.
	int di;		// Day iterator, for printing out day names.
	int si;		// Space iterator, for centering the header in printout.
	int wi;		// Week iterator.
	int headerLen;	// Width of the printout header.


	//The printout will be 40 charactes wide.
	//Calculate the headers length.
	headerLen = strlen( monthNames[month-1] ) + log10(year) + 1;

	//Center the header within the printout.
	for( si=0; si<( 40 - headerLen  ) / 2; si++)
		printf(" ");

	//Print the header
	printf("%s  %d\n", monthNames[month-1], year );


	//Print names of the weekdays.
	printf("     ");
	for(di=1; di<=7; di++)
		{
		//dayNames[di-1] are 3 characters wide.
		printf(" %s ", dayNames[di-1] );
		}
	printf("\n");


	//Print number grid of dates and week numbers.
	//'day' is the current date number to print, start at one.
	day=1;
	//Find out how many days are in this month.
	dayCount=daysInMonth(month, year);
	//Find out the initial week number of this month.
	wi = firstWeekNumber(month, year);

	//'row' and 'col' are the rows and columns of the printout grid.
	for( row=0; ;row++ )
		{
		/*
		If we are in the last week of the year, we can not assume that this week is
		part of this year and not the next. firstWeekNumber() can not offer this
		functionality. We could make a function that returns the week number for any date
		and call that for each new week.
		*/	
		//'(dayCount - day + 1)' are the days yet to be printed of this month
		//Since we are at the beginning of a new week, these are the days left in this
		//year for this week. If there are less than 4 days left, the last week is week 1.
		//Only do this for the end of december.
		if( (dayCount - day + 1) < 4 && month == 12 )
			wi = 1;

		//Print week number.
		printf("[%2.d] ", wi);
	
		//Get next week number 1 or 2 in case of first week of january.
		//Increment the week number in any other case.
		if( row == 0 && wi != 1 && month == 1 )
			wi = 1;
		else
			wi++;

		for( col=0; col<7 ;col++ )
			{
			//Print whitespace for days before the first of the month in the calendar grid.
			//firstWeekdayOfMonth() returns [1..7] col is [0..6] that is why we subtract 1.
			if( row == 0 && col < firstWeekdayOfMonth(month, year) - 1 )
				{
				printf("     ");
				}
			//Print the day number, then increment it.
			else
				{
				printf("  %2.d ", day);
				day++;
				}

			//We are done printing the calendar grid.
			if( day > dayCount )
				{
				//Print a newline after last row, before exiting.
				printf("\n");
				return 0;
				}
			}
		//Print a newline after each row.
		printf("\n");	
		}
	}



int main(int argc, char *argv[])
{
int year, month;
int direction=0;

//Sanity check command line parameters.
if( argc != 3 )
	{
	printf("Give a year and month number as command line variables.\n");
	return 1;
	}

//Deduce what the user meant.
year = getYear(argv, argc);
month = getMonth(argv, argc);

printf("To show next month type 2 and press enter.\n");
printf("To show the previous month type 8 and press enter.\n");
printf("To show the next year type 6 and press enter.\n");
printf("To show the previous year type 4 and press enter.\n");
printf("You can also give several print commands at a time:\n");
printf("e.g. 2222 + [enter]\n");
printf("press q + [enter] to exit the program.\n\n");

do
	{
	printMonth(month, year);
	printf("\n");
	
	scanf(" %c", &direction);
	fflush(stdin);
	
	switch (direction)
		{
		case '6':
			year++;
			break;
		case '4':
			year--;
			break;
		case '8':
			month--;
			break;
		case '2':
			month++;
			break;
		};
	
	if( month == 0 )
		{
		month = 12;
		year--;
		}
	if( month == 13 )
		{
		month = 1;
		year++;
		}

	} while (direction != 'q'); 	


return 0;
}



Is This A Good Question/Topic? 0
  • +

Replies To: Calendar program in C without using any date or time functions.

#2 modi123_1   User is online

  • Suitor #2
  • member icon



Reputation: 16172
  • View blog
  • Posts: 64,419
  • Joined: 12-June 08

Posted 24 February 2021 - 09:55 AM

Not a super fan of a few things, but nothing breaking.

Well done!

First - A lot of confusing variable names that are later explained in a comment. Minimize that confusion and just call the variable what it is in the comment!

373    int mi=1;       //month iterator


Second - the sprinkling of uninitialized variables before use.

ex:
068    int num;


Third, a number of unused variables floating around.

ex:
240    int weekdayDateCycle;  
241    int j, m, n, q;

Was This Post Helpful? 1
  • +
  • -

#3 finetunewithhammer   User is offline

  • D.I.C Head

Reputation: 17
  • View blog
  • Posts: 86
  • Joined: 05-February 21

Posted 24 February 2021 - 11:53 AM

Thanks for your feedback.
I think that naming things, be it variables, functions, classes or files, is one of the hardest things to do well. My rationale for using short variable names, in particular for array indices is that sentences tend to be shorter, and I can see more code on one glance. Worse yet, I do not do this consistently. I agree it is not pretty.

I'm torn about the issue of uninitialized variables. 'int num' is given the return value of atoi() before it is used, would it really be clearer if it had an initial value that is never used? I would argue that any variable that is uninitialized when declared, carries the additional information of needing to be set to some value before it is used. I'm not saying that I'm right about it, it is just my point of view.

Yeah, those unused variables are remnants of me trying to overcome my shortcomings in the ability to name things descriptively, by going over the code and naming many variables and functions anew and forgetting to remove variables I used or planned to use in previous versions of the function in question. I forgot to remove those lines.

I have since modified the code to be a library with these (public? external? meant_to_be_used?) functions:
struct calendarPage *setCalendarPage(int month, int year);
int freeCalendarPage(struct calendarPage *cal);
int printCalendarPage(struct calendarPage *cal);


The struct calendarPage is:
struct calendarPage
	{
	int year;
	int month;
	int days;
	int weeks;        //Amount of weeks this month spans (how many rows in the grid).
	int **dateGrid;
	int *weekGrid;
	};

I wish I had waited a few days and posted that version instead. Should I add it here, or would that just be beating a dead horse?
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1