The Dates Examples: Part 1

Storing dates in an array

So far we have covered the idea of a class in Java as a way to group together a collection of static methods. We can then re-use those static methods whenever we need them, we don't even have to recompile the file containing them. This is using a class in a way very much like a "module" in other programming languages.

However, Java is an object-oriented language, and the idea of a class in an object-oriented language is rather different from the idea of a module in a modular language. While Java gives us the ability to use a class as a module, its main use is as a descriptor of objects. To start off on this idea, we shall consider a completely different use of a class from the way we have seen them used up till now.

We saw a number of programs which stored integers in an array, but suppose the data we want to store in an array is not integers, but something more complex. For example, suppose we want to read in a list of dates and store them in an array. Java does in fact have a Date type provided by its library classes, but for the purpose of this exercise we will ignore that and consider building one from scratch. A date could be considered as being made up of three integers: a day, a month and a year. This can be represented directly in Java by a class:

  class Date
  {
   int day,month,year;
  }
Using this representation, the following program behaves very similarly to our Numbers0 program, simply reading a sequence of dates, storing them in an array, and printing them out again:
 1  import java.io.*;
 2 
 3  class Dates0
 4  {
 5  //  Read a number of dates and store them in an array.
 6
 7   public static void main (String[] args) throws IOException
 8   {
 9    Date [] data;
10    Date aDate;
11    int number,count;
12    int d,m,y;
13    BufferedReader in = Text.open(System.in);
14    System.out.print("Type the number of dates that will be entered: ");
15    number=Text.readInt(in);
16    data = new Date[number];
17    for(count=0; count<number; count++)
18       {
19        System.out.println("Enter date "+(count+1)+": ");
20        System.out.print("   Day: ");
21        d=Text.readInt(in);
22        System.out.print(" Month: ");
23        m=Text.readInt(in);
24        System.out.print("  Year: ");
25        y=Text.readInt(in);
26        aDate=new Date();
27        aDate.day=d;
28        aDate.month=m;
29        aDate.year=y;
30        data[count]=aDate;
31       }
32    System.out.println("The dates entered were:");
33    for(count=0;count<number;count++)
34       {
35        aDate=data[count];
36        System.out.println(aDate.day+"/"+aDate.month+"/"+aDate.year);
37       }
38   }
39 }
The class Date defines a new type Date which may be used elsewhere just like any other type in Java. We have already seen object types since arrays are objects, and the type Date behaves similarly. We may declare a variable of type Date as is done on line 10 of Dates0 but declaring it does not allocate any store for it. To allocate store for the date declared on line 10, it is necessary to use new as is done on line 26. Since a Date as defined here is an object, assignment of one Date variable to another works by aliasing. We may have an array of objects of type Date, just as we may have an array of integers, and line 9 shows the declaration of a variable to hold an array of objects of type Date, which is called data.

Just like an array, which may be referred to as a whole or by its parts, so a Date may be referred to as a whole or by its parts. If we have a variable of type Date called d, then the year part is referred to as d.year, the month part as d.month, and the day part as d.day, with each of these treatable as separate int variables. In lines 26-29 above each of these separate parts of the Date variable aDate is assigned a value. Note that when the new object for aDate is created on line 26, the data in the old one would be lost if it were not for data[count] having been made to refer to it on line 30 in the previous time round the loop of lines 17-31. Each Date object has its own day, month and year part, including each Date object in the array data.

If you have already done some programming you may recognise this as similar to a record in a language like Pascal or a struct in C. In general, however, it is not considered good programming practice to use classes like records, and directly access the variables in an object. We will see better ways of manipulating objects later on, so don't take Dates0 as an example of good programming style!

Printing dates

Lines 35-36 in Dates0 print out a date from the array. Strictly it was unnecessary to use a separate variable aDate here, we could have just had the one line:
System.out.println(data[count].day+"/"+data[count].month+"/"+data[count].year);
so we can, for example refer to the day component of the component of data indexed by count by
data[count].day 
It would, however, be nice to have a separate method which printed out a Date value. We could use the following inside class Dates0:
  public static void printDate(Date d)
  {
   System.out.println(d.day+"/"+d.month+"/"+d.year);
  }
then we could replace lines 34-37 by:
   printDate(data[count]);
Note that if we just try printing each date without saying how to print something of type Date by replacing lines 34-37 with:
  System.out.println(data[count]);
the program will compile but when it is run prints what looks like just gobbledygook - the characters Date@ followed by an assortment of numbers and letters that bear no relationship to what was stored in data[count].

A better way to go about printing objects is to have a print method within the class of the object. That way the same print method can be used whenever objects of that class are used, in fact the print method can be seen as an essential part of the object, the way the object prints itself. So, here is a revised version of Data with a print method called print within it:

  class Date
  {
   int day,month,year;

   public void print()
   {
    System.out.println(day+"/"+month+"/"+year);
   }
  }
With this, lines 34-37 of Dates0 can be replaced by:
  data[count].print();
Note that in this new version of class Date, the method print is not declared as static. This is because it is meant to be attached to individual objects of type Date. If we have an object d of type Date then the command d.print() will cause d to be printed. In object-oriented programming, this is sometimes described as "sending a print() message to object d". In general, a method is only defined as static if there is nothing in it which refers to the components of an individual objects of its class. Static methods are called by attaching them with a dot to their class name, while non-static methods are called by attaching them with a dot to the name of the individual object on which they act. The names day, month and year within the method print are not indicated as attached to any object, which means when print is called they are assumed to be attached to whichever object the call to print was attached to.

The print method has a return type void because it does something (printing) rather than return a value. An alternative would be to have a method which returns the String value that should be printed when the object is printed. This is similar to the way writeDouble in Text works. In general it is more flexible than a straight printing method. Here is a version of class Data with such a method:

 class Date
  {
   int day,month,year;

   public String toString()
   {
    return day+"/"+month+"/"+year;
   }
  }
With this, lines 34-37 of Dates0 could be replaced by:
  System.out.println(data[count].toString());
However, it could also be replaced by just:
  System.out.println(data[count]);
The reason this works correctly rather than printing the goobledygook we saw before when we tried to print Date objects is that any object which has in its class a non-static method called toString(), with no parameters, gets the toString() method used automatically when an attempt is made to print the object.

Constructor methods

Just as the way an object is printed is best left to be given in the class for the object, so the way an object is created and initialised should be expressed inside the class for that object. In general, a class which describes objects should have one or more constructor methods which describe how an object of that class is created and initialised. For the type Date such a constructor method would do the work which was done on lines 26-29 of Dates0. A constructor method has the same name as the class of which it forms part. It may have arguments which give the data that is used to build up an object of that class, but does not have a return type. Here is a version of class Date including a constructor:
 1  class Date
 2  {
 3   private int day,month,year;
 4
 5   public Date(int d,int m,int y)
 6   {
 7    day=d;
 8    month=m;
 9    year=y;
10   }
11
12   public String toString()
13   {
14    return day+"/"+month+"/"+year;
15   }
16  }
Lines 5-10 here are the constructor for Date. Note that it is now unnecessary to refer to the day, month and year components of an object of type Date so these components can be made private as they are on line 3, which prevents other classes from accessing them in any way (either using their values or changing their values).

If a class has no constructor method, as with the previous version of Date what actually happens is that a default constructor is assumed to exist which takes no arguments (hence the Date() in line 26 of Dates0) and sets all the numerical variables to 0. It is possible to write a constructor which has no arguments, so we could add:

   public Date()
   {
    day=1;
    month=1;
    year=1900;
   }
to the class Date above which means you can create a new Date without specifying any arguments, and in this case the date will be the default value of 1st January 1900. Note that it is possible to have more than one constructor in a class. Each constructor must have the same name as the class, but the particular constructor used when an object is created depends on the arguments specified in the call.

Below is a version of the Dates program which uses the constructor given above, and the toString method to print out the dates:

 1  import java.io.*;
 2
 3  class Dates1
 4  {
 5  //  Read a number of dates and store them in an array.
 6
 7   public static void main (String[] args) throws IOException
 8   {
 9    Date [] data;
10    int number,count;
11    int d,m,y;
12    BufferedReader in = Text.open(System.in);
13    System.out.print("Type the number of dates that will be entered: ");
14    number=Text.readInt(in);
15    data = new Date[number];
16    for(count=0; count<number; count++)
17       {
18        System.out.println("Enter date "+(count+1)+": ");
19        System.out.print("   Day: ");
20        d=Text.readInt(in);
21        System.out.print(" Month: "); 
22        m=Text.readInt(in);
23        System.out.print("  Year: ");
24        y=Text.readInt(in);
25        data[count] = new Date(d,m,y);
26       }
27    System.out.println("The dates entered were:");
28    for(count=0;count<number;count++)
29       System.out.println(data[count]);
30   }
31 }

Sorting an array of dates

Suppose we wanted to sort the array of dates we have read in, putting them in order so that the earliest date comes first and the latest date comes last, and so on. We could add a line between lines 26 and lines 27 in Date1 above which calls on some sorting method, similar to that we have already seen to sort an array of integers. This line could be:
DateSorter.sort(data,count);
then the code for DateSorter could be:
 1  class DateSorter
 2  {
 3
 4  public static void sort(Date [] a,int n)
 5  // Sort (in place) the first n dates in array a.
 6  // Uses selection sort
 7  {
 8   int sorted,minPos;
 9   for(sorted=0;sorted<n;sorted++)
10      {
11       minPos=indexEarliest(a,sorted,n);
12       swap(a,sorted,minPos);
13      }
14   }
15
16   private static void swap(Date [] a,int pos1,int pos2)
17   // Swap the dates at index pos1 and pos2 in array a
18   {
19    Date temp;
20    temp=a[pos1];
21    a[pos1]=a[pos2];
22    a[pos2]=temp;
23   }
24
25   private static int indexEarliest(Date [] a,int pos1,int pos2)
26   // Return the index of the earliest date in the portion
27   // of array a starting at index pos1 and up to but not
28   // including index pos2.
29   {
30    int i,pos=pos1;
31    for(i=pos1+1; i<pos2; i++)
32       if(a[i].lessThan(a[pos]))
33          pos=i;
34    return pos;
35   }
36
37  }
This is very similar to the Sorter class given in the "Numbers" examples: part 2 notes. In fact the main thing that has been changed is the type int to the type Date where a type refers to the type of the contents of the array being sorted. The name of the method indexSmallest is changed to indexEarliest, which was not necessary but makes the code a little easier for the human reader to understand. The method swap is made private because it is not intended to be useable outside this class. The other change is on line 32. When integers were being sorted, they could be compared with the test a[i]<a[pos]. However, non-numerical values cannot be compared with the < operator. Instead, it is necessary to write a comparison method for them. Here is the method, which needs to be added to class Date:
 1  public boolean lessThan(Date d)
 2  {
 3   if(year<d.year)
 4      return true;
 5   else if(year==d.year)
 6     if(month<d.month)
 7        return true;
 8     else if(month==d.month)
 9        if(day<d.day)
10           return true;
11   return false;
12  }
So the direct arithmetic test is replaced by a call to a method which returns a boolean value i.e. either true or false. The method is attached to one Date object and takes another as its argument. So inside the method, for example, year refers to the year value of the object to which the call is attached (a[i] for the call on line 32 of DateSorter), while d.year refers to the year value of the object passed as an argument (a[pos] for the call on line 32 of DateSorter). The effect is as if the first Date object were sent a message containing the second Date object inside, and asked to reply true or false whether it was earlier than the date in the message.

An alternative would be to have written lessThan as a static method which takes both Date objects as arguments. In that case it can be considered a function which takes two dates and returns true or false depending on whether the first is earlier than the second. The code for this alternative method is:

 1  public static boolean lessThan(Date d1,Date d2)
 2  {
 3   if(d1.year<d2.year)
 4      return true;
 5   else if(d1.year==d2.year)
 6     if(d1.month<d2.month)
 7        return true;
 8     else if(d1.month==d2.month)
 9        if(d1.day<d2.day)
10           return true;
11   return false;
12  }
and line 32 of DateSorter would be:
     if(Date.lessThan(a[i],a[pos]))
Note the way lessThan works in both cases. It first tests (on line 3) if the year of the first date is less than the year of the second, in which case it is obviously earlier so no further tests need be made and true can be returned. Otherwise it only looks further comparing the month, if the years are equal, and then only looks at the days if the months are equal as well. Line 10 which returns false is only reached if none of the previous lines which return true (4, 7 and 10) were reached, since when they are reached the return causes execution of lessThan to finish and line 11 isn't reached.

Since the code for DateSorter has exactly the same structure as that for Sorter it uses the same selection sort algorithm. In fact it seems a bit of a waste of effort to have to go through the original Sorter class making minor changes in order to sort dates rather than integers. Later on we shall show how it is possible to write a generic sorter, that is one that is not restricted to arrays of a particular base type but instead can be used to sort an array of any type.

Reading from a file

As a final example, let us consider reading dates from a file, as we read numbers from a file in the Average examples: part 2. Here is a program to do that:
 1  import java.io.*;
 2
 3  class Dates2
 4  {
 5  //  Read a number of dates from a file, store them in an array
 6  //  and sort them
 7
 8     static final int MAXDATES=100;
 9
10     public static void main(String[] args) throws IOException
11     {
12      String filename;
13      Date [] data;
14      int count=0;
15      BufferedReader in = Text.open(System.in),inFile;
16      for(;;)
17          try
18             {
19              System.out.print("\nEnter file name to read from: ");
20              filename=Text.readString(in);
21              inFile=Text.open(filename);
22              break;
23             }
24          catch(FileNotFoundException e)
25             {
26              System.out.println("No file of that name found");
27             }
28      data = new Date[MAXDATES];
29      try
30         {
31          for(;;)
32             data[count++] = readDate(inFile);
33         }
34      catch(IOException e)
35         {
36         }
37      catch(ArrayIndexOutOfBoundsException e)
38         {
39          System.out.print("Maximum number of dates ("+MAXDATES);
40          System.out.println(") exceeded");
41         }
42      count--;
43      DateSorter.sort(data,count);
44      System.out.println("The dates read are (after sorting):");
45      for(int i=0;i<count;i++)
46         System.out.println(data[i]);
47     }
48
49     public static Date readDate(BufferedReader reader)
50     throws IOException
51     {
52      int d,m,y;
53      d=Text.readInt(reader);
54      m=Text.readInt(reader);
55      y=Text.readInt(reader);
56      return new Date(d,m,y);
57     }
58
59  }
As before, a try statement inside an infinite loop (lines 16-27) is used in order to have the program keep on prompting for the name of a file until it is given one it can open. Then an infinite loop inside a try statement (lines 29-41) is used to carry on reading dates until the end of the file is reached or the number of dates exceeds the maximum number storable in the previously created (on line 28) array. As the array is created before the number of dates read is known, it has to be of the maximum size considered necessary, and there is a catch clause (lines 37-40) which comes into action if an attempt is made to exceed this limit. The limit is declared as a constant (or static final) on line 8.

A separate method to read dates from a file, called readDate, is given, on lines 49-57. As the numbers are read from a file there are no prompts, but note it expects the numbers to be expressed just as three integers: the day, the month and the year. You cannot put a slash or any other character except a space between then, as the program is not equipped to deal with that. The BufferedReader which was linked with the input file on line 21 is passed as an argument to readDate, and then passed as an argument to readInt from Text to get the integer to be read from the file.

The method readDate will throw an IOException if it encounters one. That is indicated by the throw clause on line 50. What this means is that if an IOException occurs in the call to readDate, which is on line 32, it is not dealt with inside this method, but thrown up so it is dealt with in the try statement of which line 32 is part. This try statement has a clause to deal with IOException, on lines 34-36. In fact it simply reacts by doing nothing. An end-of-file exception is a kind of IOException>, so what this means is that if readDate attempts to read past the end of a file, execution of readDate finishes, wherever it has got to, and execution immediately goes to line 35. As there is nothing there, it goes on to the next statement after the try statement, which is on line 42. Note the use of ++ on line 32 to increase the value in the variable count by one each time round the loop. Line 42 decreases the value in count by one, as the increase before the IOException was called would have been one too many. The effect of this is that the loop on lines 31-32 is correctly exited when the end of the input file is encountered. Note that this works because EOFException is a form of IOExcpeption

The program as given does not contain any tests for whether the integers read form valid dates. This would obviously be a useful modification, ensuring the data read in are valid. We will consider it later. The programs discussed in these notes can be found in the directory:

/import/teaching/BSc/1st/ItP/dates
including a file called dates which could be used in conjunction with the program in Dates2.java.
Matthew Huntbach

These notes were produced as part of the course Introduction to Programming as it was given in the Department of Computer Science at Queen Mary, University of London during the academic years 1998-2001.

Last modified: 14 Oct 1998