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; }