3.12 An Extended Example (Day Number in a year)

Problem:

Write a program that will calculate the consecutive date number of any day in the year.

Suitability:

This is a tedious calculation by hand. It could be a component of a program that, say, computes interest on periods of time less than a year.

Restatement:

The program is to count January 1 as day number 1, and December 31 as either 365 or 366, depending on whether it is a leap year. The input will be given as three cardinals representing the year, month, and date within the month.

Sample data:

Input:

	1990
	4
	15

Output:

04 15 is day number 105 in 1990.

Library use:

The SWholeIO library will be employed to read and write the cardinal data.

Refinement:

The program DateCalc will:

1. Read data in the form of three cardinals, representing the number of the year, of the month, and of the date in the month, respectively.
2. The consecutive date in the month will be calculated.
3. The result will be printed in the form
mm dd is day number nnn in yyyy.
4. The program will exit.

Second Refinement:

1.1 print a prompt to the screen for the year data
      read the year
1.2 print a prompt to the screen for the month data
      read the month
1.3 print a prompt to the screen for the date data
      read the date
2.0 Set the result to zero
2.1 Add to the result the number of days to the end of the previous month
  If month is 12 add 334
    Else if month is 11 add 304
    Else if month is 10 add 273
    Else if month is 9 add 243
    Else if month is 8 add 212
    Else if month is 7 add 181
    Else if month is 6 add 151
    Else if month is 5 add 120
    Else if month is 4 add 90
    Else if month is 3 add 59
    Else if month is 2 add 31
2.2  Add the date in the month to the result
2.3  Adjust for leap years
    If the month is greater than 2 then
      If the year is divisible by 4 but not by 100 (unless also by 400)
        add one to the result

Data Table:

1. Imports required
	From STextIO: WriteString, WriteLn, ReadChar, SkipLine
	From SWholeIO :ReadCard, WriteCard
2. Variables Required:
	year, month, day, result : Cardinals
	response : Char

User Manual:

DateCalc is a simple utility designed to compute the consecutive number in the year of any date entered. DateCalc runs on any Macintosh computer.

To use DateCalc, either type its name, followed by the <enter> key from the MPW worksheet, or double-click on its icon from the Macintosh desktop.

You will be prompted to enter the year, the month and the date. These should all be whole numbers; that is, use 2 for February, 12 for December, etc.

At this point, the result will be printed on the screen, followed by the message:

	Press a key to continue ====>

After noting the information, press a key and the program will exit.

Errors:

DateCalc will not check to see if you have entered a valid date; the responsibility is the user's to avoid such things as 1990 02 34.

Possible future enhancements:

Add the option of reading the data from a disk file

Add checking for the validity of the date.

Improve the efficiency of the computation by devising a formula.

Code:

(* Name: Daniella Christian
   Student Number: 052001
   CMPT 141 Fall 2008
   Assignment #2
   Calculating date numbers
*)

MODULE DateCalc;

FROM STextIO IMPORT
  WriteString, WriteLn, ReadChar, SkipLine;
FROM SWholeIO IMPORT
  ReadCard, WriteCard;

VAR
  day, month, year, result : CARDINAL;
  usingFile, again : BOOLEAN;
  response : CHAR;

BEGIN
  (* write out header information *)
  WriteString ("Name: Daniella Christian");
  WriteLn;
  WriteString ("Student Number: 052001");
  WriteLn;
  WriteString ("CMPT 141 Fall 2008");
  WriteLn;
  WriteString ("Assignment #2");
  WriteLn;
  WriteString ("Calculating consecutive date numbers");
  WriteLn;
  WriteString ("This program calculates the consecutive ");
  WriteString ("date in the year ");
  WriteLn;
  WriteString ("from user supplied information");
  WriteLn;
  WriteLn;

  REPEAT (* main repeat loop *) 
    WriteString ("Enter the year number here ====>");
    ReadCard (year);
    SkipLine; 
    WriteLn;
    WriteString ("Enter the month number (1 - 12) here ====>");
    ReadCard (month);
    SkipLine;
    WriteLn;
    WriteString ("Enter the day number here ====>");
    ReadCard (day);
    SkipLine;
    WriteLn;

    (* do the calculation *)
    result := 0;  (* initialize the result *)
    
    (* add on right number of days to end of last month *)
    IF month = 12
      THEN
         result := result + 334
      ELSIF month = 11 THEN
        result := result + 304
      ELSIF month = 10 THEN
        result := result +  273
      ELSIF month = 9 THEN
        result := result +  243
      ELSIF month = 8 THEN
        result := result +  212
      ELSIF month = 7 THEN
        result := result +  181
      ELSIF month = 6 THEN
        result := result +  151
      ELSIF month = 5 THEN
        result := result +  120
      ELSIF month = 4 THEN
        result := result +  90
      ELSIF month = 3 THEN
        result := result + 59
      ELSIF month = 2 THEN
        result := result + 31
      END;
    
    (* now add the day in the month *)
    result := result + day;
    
    (* finally, adjust for leap years *)
    IF (month > 2) AND ((year MOD 400 = 0) OR
       ((year MOD 4 = 0) AND (year MOD 100 # 0)) ) 
      THEN
         INC (result)
       END;
    
    (* Output the result in the required form *)
    WriteCard (year, 4);
    WriteCard (month, 3); (* ensure one space between *)
    WriteCard (day, 3);
    WriteString (" is day number ");
    WriteCard (result, 4);
    WriteString (" in that year.");
    WriteLn;
    WriteLn;

    WriteString ( "Do you wish to do another? Y or N ==> ");
    ReadChar (response);
    again := CAP (response) = "Y";
    SkipLine;
    WriteLn;
  UNTIL NOT again;
END DateCalc.

Results:

This module was run with the following result:

Name: Daniella Christian
Student Number: 052001
CMPT 141 Fall 2008
Assignment #2
Calculating consecutive date numbers
This program calculates the consecutive date in the year 
from user supplied information

Enter the year number here ====>1992
Enter the month number (1 - 12) here ====>3
Enter the day number here ====>1
1992  3  1 is day number   61 in that year.

Do you wish to do another? Y or N ==> y
Enter the year number here ====>1993
Enter the month number (1 - 12) here ====>12
Enter the day number here ====>31
1993 12 31 is day number  365 in that year.

Do you wish to do another? Y or N ==> n

Further refinement:

There is an algorithm that allows the calculation to be done in a formula rather than by using the IF ... THEN construction. To see how this works, consider first the "rough estimate" that each month has 30 days, and consider how the 31 day months affect the total number of days to be added for the following month. We ignore February for the moment.

Mon: Jan	Feb	Mar	Apr	May	Jun	Jul	Aug	Sep	Oct	Nov	Dec
No.:	1	2	3	4	5	6	7	8	9	10	11	12
Adj:	0	1	1	2	2	3	3	4	5	5	6	6

Each time there is a 31 day month, one must adjust the total by 1. Now, up to August, the adjustment formula is: month DIV 2, but after that it is: (month +1) DIV 2. This can be expressed in a single adjustment as: (month + month DIV 9) DIV 2.

From this one must subtract 2 if the month is after February, and this quantity can be expressed in a formula as: 2*((month + 9) DIV 12). Try it!

The leap year adjustment changes the "2" in front of this expression to: 2 - (4 - year MOD 4) DIV 4) + (100 - year MOD 100) DIV 100) - (400 - year MOD 400) DIV 400) [The first term of this produces a 1 when the year is divisible by 4, the second removes it when the year is divisible by 100; and the third adds it back in when it is divisible by 400. Thus the two day adjustment for an ordinary February is altered as appropriate to a one day adjustment.]

Combining all these gives the result in a single Modula-2 expression:

    result := 30 * (month -1) + day +
            (month + (month DIV 9)) DIV 2 - 
              (2 - (4 - (year MOD 4)) DIV 4 +
			  (100 - (year MOD 100)) DIV 100 -
			  (400 - (year MOD 400)) DIV 400) * 
			      ((month + 9) DIV 12);	  

and this does the entire calculation, eliminating the two IF..THEN constructions in the original code.

Yet another refinement:

The formula used in the second version is rather cumbersome, and may be difficult to understand, especially if the code is examined without the analysis leading up to it (and perhaps even then). Perhaps a compromise ought to be struck between the easy low-technology method with its long IF .. THEN statement, and the high-technology method with its complex formula. Consider a further analysis of the problem. Starting in March, and going through to the following February, the total number of days and the average number of days from March first to the end of the previous month is given by:

Mon:	Mar	Apr	May	Jun	Jul	Aug	Sep	Oct	Nov	Dec	Jan	Feb
No.:	0	31	61	92	122	153	184	214	245	275	306	337
Av:	0	31	30.5	30.7	30.5	30.6	30.7	30.6	30.6	30.6	30.6	30.6

This chart suggests numbering the months starting in March and multiplying this number by 30.6 to get the number of days elapsed to the first of the current month. Using a method similar to that above, the month number is adjusted by adding 12 if it starts out at 1 or 2. This initial adjustment changes the range of months from 1 .. 12 to 3 .. 14.

  month := month + 12 * ( 12 - month) DIV 10

The expression

  (month - 3)

is needed in the final formula to change this range to 0 .. 12, counting from March to February.

The chart below of the revised month numbers gives the result of doing this, first as a raw answer, (*Av) then rounded off to the nearest day (#). The latter are exactly what are desired.

Mon:	Mar	Apr	May	Jun	Jul	Aug	Sep	Oct	Nov	Dec	Jan	Feb
MNo.:	0	1	2	3	4	5	6	7	8	9	10	11
*Av:	0	30.6	61.2	91.8	122.4	153	183.6	214.2	244.8	275.4	306	336.6
#	0	31	61	92	122	153	184	214	245	275	306	337

Rounding a number off to the nearest whole number can be expressed as:

  cardNum := TRUNC (realNum + 0.5)

This gives, for the number of days from last March 1:

  daysSinceMarch := TRUNC (30.6 * (month -3) + 0.5);
      (* with revised month *)

Adding 59 to the result for the first two months, and then removing the excess days (January and February have, in effect been moved to the following year), produces:

  result = (59  + daysSinceMarch + day)

followed by a reduction if greater than 365 (because January and February are not really in the next year, it just made it an easier formula to consider them that way for the moment) and an increase if a leap year.

The code can be revised to have three more variables, adjMonth, daysSinceMarch, and leap and then modularized into a series of steps, instead of rolling everything into a single formula. That is, the algorithm can be expressed on several lines instead of one:

    leap:= (year MOD 400 = 0)
            OR ((year MOD 4 = 0) AND (year MOD 100 # 0));
    adjMonth := month + 12 * (( 12 - month) DIV 10);
    daysSinceMarch := TRUNC (30.6 * FLOAT (adjMonth - 3) + 0.5);
    result := (59 + daysSinceMarch + day);
    IF result > 365 
      THEN
        DEC (result, 365)
      END;
    IF leap AND (month > 2)
      THEN
        INC (result);
      END;

NOTE: DEC is a standard identifier similar to INC.

There are other ways of doing this as well. It would be nice, for instance, to have a procedure for rounding real numbers off to the nearest integer or cardinal. No such procedure is built in to Modula-2, though there are some in the RealMath and LongMath libraries, and in the next chapter, directions will be given for writing one.


Contents