Previous

Contents

Next

Chapter 8:
Program design and debugging

It is to be noted that when any part of this
paper appears dull, there is a design in it.

— Sir Richard Steele, The Tatler


8.1 Stepwise refinement
8.2 Initial design
8.3 Diary package design
8.4 Debugging the main program
8.5 Displaying the appointments
8.6 Adding new appointments
8.7 Deleting appointments
8.8 Loading and saving
8.9 Assessing the program
Exercises

8.1 Stepwise refinement

So far I’ve said very little about how to design a program to solve a particular problem. The examples I’ve used until now have been small enough (tens of lines of code) that their design could effectively be ignored in favour of looking at language details and several of the exercises have involved making changes to the examples rather than writing new programs from scratch. You’ve now covered enough of the language that I can introduce a slightly larger problem here which demands a bit more effort. The solution is probably an order of magnitude larger than the previous examples. So, just what do you do when you’re faced with a specification for a problem and a blank piece of paper to write the solution on?

A good way to start is to try to split your problem up into a number of smaller subproblems and then deal with each of them in turn. This is known as stepwise refinement or top-down design. It is a divide-and-conquer approach which lets you avoid having to deal with a large and complex design as a single monolithic unit. The problem can be broken down into a set of smaller steps, which can then be refined into more detail by applying the same process. The design of the calculator example at the end of chapter 3 used this approach.

Top-down design lets you avoid getting bogged down in details until the last possible moment. It’s not entirely foolproof; to be able to do this effortlessly involves having an appreciation of where you’re trying to get to and a vague idea of what kind of low-level details you’ll end up having to deal with. In particular, it helps to know what packages are available that can provide pieces of the jigsaw puzzle you’re trying to put together; if you can steer your breakdown of the solution in the general direction of being able to use some existing packages, you can save yourself some effort. This is part of the craft of programming which you have to learn through experience; if there was a formula you could apply to generate a ‘correct’ solution someone would have written a program to do it and programmers would find themselves out of a job. In fact, as you’ve already seen, there is no single ‘correct’ solution to a particular problem; different people will tend to find different solutions to the same problem.

Fortunately, there are some general principles that you can use as a guide to get you started. As I mentioned in chapter 3, a good start in most cases is to divide the problem up into an initialisation part, a main processing part and a finalisation part. The initialisation does any initial setting up that may be required (e.g. displaying a window on the screen or opening some files) and the finalisation does any final tidying up (destroying windows, closing files, etc.) before the program ends. The main processing in the middle is where all the hard work is done. This is usually a loop which repetitively processes ‘events’ such as user input, mouse movements, the passage of time or whatever. You may well end up designing the main processing section first and then identifying what initialisation and finalisation it requires.

Beyond this point you have to look at what sort of thing you’re trying to do. Does it involve repeating some action over and over again? If so, you need a loop statement which encloses the required action. Alternatively, does it involve choosing between different actions? If so, you need an if or case statement. Now you’ve got one or more smaller actions inside your loop, if or case statement which you can now break down into smaller pieces using the same approach.

If you want to, you can just sweep some of the details under the carpet by inventing a procedure or function which will (eventually) deal with some aspect of the problem. Your initial implementation of the procedure or function might be a stub which does nothing or which cheats in some way (e.g. by getting the user to supply the value to be returned from a function); you can then test the general outline of the program before coming back to your stub and doing the job properly.

To illustrate all this I’m going to develop a larger example, an electronic diary which can be used to keep track of your appointments. It will need to provide as a minimum the ability to add new appointments, delete existing appointments, display the list of appointments and save the appointments to a file. Also, if there are any saved appointments the program should read them in when it starts up.

The initialisation part of this program will involve reading the diary file if it exists. The main processing will consist of displaying a menu of choices, getting the user’s response and carrying out the specified operation. At the end, there isn’t really anything more to do.


8.2 Initial design

As I said earlier, the initialisation part of this program will involve reading the diary file if it exists. The main processing will consist of displaying a menu of choices, getting the user’s response and carrying out the specified operation. To make life easier, I will create a package called JE.Diaries to hold the procedures and related bits and pieces for this program; I’ll worry about what will go into it as the design progresses. This gives us the following general structure:

    with JE.Diaries; use JE.Diaries;
    procedure Diary is
        -- declarations will go here
    begin
        -- load diary from file (if any)
        -- process user commands (add, delete, list, save, etc.)
    end Diary;

Loading the diary from a file can be delegated to a procedure which I’ll call Load_Diary and define in JE.Diaries:

    with JE.Diaries; use JE.Diaries;
    procedure Diary is
        -- any declarations will go here
    begin
        Load_Diary;
        -- process user commands (add, delete, list, save, etc.)
    end Diary;

The main processing (dealing with commands from the user) is a repetitive task: repeatedly get a command and do it. This means that the main processing will be a loop of some sort:

    with JE.Diaries; use JE.Diaries;
    procedure Diary is
        -- any other declarations will go here
    begin
        Load_Diary;
        loop
            -- process a single user command (add, delete, list, etc.)
        end loop;
    end Diary;

Processing a command involves displaying a menu, getting a command in response to the menu, and then doing it. I’ll use single characters for the command responses, so I’ll need a Character variable to store it in which I’ll call Command. The loop will be terminated when the user selects the Quit command. We can expand the design a bit further now:

    with JE.Diaries; use JE.Diaries;
    procedure Diary is
        Command : Character;
        -- any other declarations will go here
    begin
        Load_Diary;
        loop
            -- display menu
            -- get a command
            -- perform selected command
        end loop;
    end Diary;

Getting a command is trivial; it involves getting a single character using Get from Ada.Text_IO. This means that Ada.Text_IO must be added to the list of packages in the with clause:

    with Ada.Text_IO, JE.Diaries;
    use Ada.Text_IO, JE.Diaries;
    procedure Diary is
        Command : Character;
        -- any other declarations will go here
    begin
        Load_Diary;
        loop
            -- display menu
            Get (Command);

            -- perform selected command
        end loop;
    end Diary;

The menu just needs to list the command choices (A for add, D for delete, L for list, S for save, Q for quit; you can expand or alter the list of commands later if you need to). How you perform the selected command depends on which command it is; it’s a choice of alternative actions, so an if or case statement is needed. A case statement is appropriate here since there are several possible choices which depend on the value of Command:

    with Ada.Text_IO, JE.Diaries;
    use Ada.Text_IO, JE.Diaries;
    procedure Diary is
        Command : Character;
        -- any other declarations will go here
    begin
        Load_Diary;
        loop
            -- display menu
            New_Line (5);
            Put_Line ("Diary menu:");
            Put_Line (" [A]dd appointment");
            Put_Line (" [D]elete appointment");
            Put_Line (" [L]ist appointments");
            Put_Line (" [S]ave appointments");
            Put_Line (" [Q]uit");
            New_Line;
            Put ("Enter your choice: ");

            -- get a command
            Get (Command);

            -- perform selected command
            case Command is
                when 'A' | 'a' =>
                    -- add appointment
                when 'D' | 'd' =>
                    -- delete appointment
                when 'L' | 'l' =>
                    -- list appointments
                when 'S' | 's' =>
                    -- save appointments
                when 'Q' | 'q' =>
                    -- quit
                when others =>
                    -- error: invalid menu choice
            end case;
        end loop;
    end Diary;

Displaying the menu and performing the selected command could easily be implemented as separate procedures, but I’ve left them in the main program so that it’s easier to see the correspondence between the menu and the choices in the case statement, which ensures that if any changes are made to the menu it will be obvious whether the case statement has been changed to reflect this. However, others might prefer to use procedures to minimise code bulk in the main program. The difference in approach isn’t major.

The Quit command is easy to deal with; this just involves exiting from the main loop. The others choice can simply display an error message, and the remaining commands can be handled by procedures in JE.Diaries:

    case Command is
        when 'A' | 'a' =>
            Add_Appointment;
        when 'D' | ‘d' =>
            Delete_Appointment;
        when 'L' | 'l' =>
            List_Appointments;
        when 'S' | 's' =>
            Save_Appointments;
        when 'Q' | 'q' =>
            exit;
        when others =>
            Put_Line ("Invalid choice -- " &
             "please enter A, D, L, S or Q");
    end case;

So far so good. Now let’s construct the specification of JE.Diaries from what we’ve got so far; all it needs is a list of the procedures referred to by the main program:

    package JE.Diaries is
        procedure Load_Diary;
        procedure Add_Appointment;
        procedure Delete_Appointment;
        procedure List_Appointments;
        procedure Save_Appointments;
    end JE.Diaries;

8.3 Diary package design

The procedures in the package all need to manipulate the diary, so we’ll need to define the structure of the diary before we can go any further. Most of the necessary type declarations were developed in chapter 6, so I’ll just take them from there:

    subtype Day_Type is Integer range 1..31;
    type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
    subtype Year_Type is Integer range 1901..2099;
    subtype Hour_Type is Integer range 0..23;
    subtype Minute_Type is Integer range 0..59;

    type Date_Type is
        record
            Day   : Day_Type;
            Month : Month_Type;
            Year  : Year_Type;
        end record;

    type Time_Type is
        record
            Hour   : Hour_Type;
            Minute : Minute_Type;
        end record;
    type Appointment_Type is
        record
            Date    : Date_Type;
            Time    : Time_Type;
            Details : String (1..50);        -- an arbitrarily chosen size
            Length  : Natural := 0;
        end record;

    type Appointment_Array is array (Positive range <>) of Appointment_Type;

The only difference is that Appointment_Type now contains a Length component to record the actual length of the Details component; this will allow a maximum of 50 characters rather than exactly 50 characters. The maximum length is arbitrary; you can change it if you want longer appointment details.

Now we’ll need an Appointment_Array together with a variable to keep track of how many appointments there actually are in the diary. This calls for another record type declaration:

    type Diary_Type (Maximum : Positive) is
        record
            Appts : Appointment_Array (1..Maximum);
            Count : Natural := 0;
        end record;

Finally we can declare the diary itself:

    Diary : Diary_Type (10);

I’m only allowing ten entries at the moment so that it’ll be easy to test that the program behaves properly when the diary is full. The size of the diary and the length of the details string can both be changed by changing a single line of the declarations above, recompiling the package body and then relinking the main program. We’ll need to be careful not to assume that those values will always be what they are now, and use the 'Last attribute for the length of the details string and the Maximum discriminant for the number of entries.

The procedures can be implemented as stubs for now. These are temporary versions of the procedures that we can use to complete the package body so that it can be compiled, and the bits that have been written so far can be tested. What I’ll do is provide versions of the procedures that just display a message to say they’ve been called (which will also require a with clause for Ada.Text_IO at the top of the package body):

    procedure Load_Diary is
    begin
        Put_Line ("Load_Diary called");
    end Load_Diary;

    procedure Add_Appointment is
    begin
        Put_Line ("Add_Appointment called");
    end Add_Appointment;

    procedure Delete_Appointment is
    begin
        Put_Line ("Delete_Appointment called");
    end Delete_Appointment;

    procedure List_Appointments is
    begin
        Put_Line ("List_Appointments called");
    end List_Appointments;

    procedure Save_Appointments is
    begin
        Put_Line ("Save_Appointments called");
    end Save_Appointments;

The full version of the package body obtained by putting the above bits and pieces together looks like this:

    with Ada.Text_IO;
    use Ada.Text_IO;
    package body JE.Diaries is

        subtype Day_Type is Integer range 1..31;
        type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);

        subtype Year_Type is Integer range 1901..2099;
        subtype Hour_Type is Integer range 0..23;
        subtype Minute_Type is Integer range 0..59;

        type Date_Type is
            record
                Day   : Day_Type;
                Month : Month_Type;
                Year  : Year_Type;
            end record;
        type Time_Type is
            record
                Hour   : Hour_Type;
                Minute : Minute_Type;
            end record;

        type Appointment_Type is
            record
                Date    : Date_Type;
                Time    : Time_Type;
                Details : String (1..50);        -- an arbitrary size
                Length  : Natural := 0;
            end record;

        type Appointment_Array is array (Positive range <>)
                                                                    of Appointment_Type;

        type Diary_Type (Maximum : Positive) is
            record
                Appts : Appointment_Array (1..Maximum);
                Count : Natural := 0;
            end record;

        Diary : Diary_Type (10);

        procedure Load_Diary is
        begin
            Put_Line ("Load_Diary called");
        end Load_Diary;

        procedure Add_Appointment is
        begin
            Put_Line ("Add_Appointment called");
        end Add_Appointment;

        procedure Delete_Appointment is
        begin
            Put_Line ("Delete_Appointment called");
        end Delete_Appointment;

        procedure List_Appointments is
        begin
            Put_Line ("List_Appointments called");
        end List_Appointments;
        procedure Save_Appointments is
        begin
            Put_Line ("Save_Appointments called");
        end Save_Appointments;

    end JE.Diaries;

Once you’ve checked that the program works so far, you can replace the stubs with working versions of the code for each procedure. One of the advantages of top-down design is that it lets you postpone worrying about the finer details of your programs until the last possible moment; it also lets you do incremental testing, where you test each part of the program before you go any further.


8.4 Debugging the main program

At this point we want to be confident that the main program works. Once we know that it can cope with anything we throw at it we can get down to implementing the rest of the program. There isn’t any point in proceeding until we know that everything so far works; apart from anything else, it’s still only a small program which can be tested, fixed and then recompiled quite quickly.

The first thing to do is to test it with correct input. When the program starts up it should display the message ‘Load_Diary called’ followed by the menu. Try typing A, D, L and S to make sure that the correct procedure is called in each case. Now try a, d, l and s. Is everything all right? This has tested the three main menu commands; the remaining ones to try are Q and q. Type Q and the program should terminate. Now start it up again and try q instead.

So, the program so far works with correct input. Fortunately at this stage the possibilities can be tested exhaustively; in most cases you have to choose a representative set of test data since there are too many possibilities to test them all (unless you know somewhere where you can hire an infinite number of monkeys, that is!). Now what about incorrect input? First of all try typing X. You should get a message saying ‘Invalid choice -- please enter A, D, L, S or Q’. So far so good. Now try typing Add. You should immediately spot that there’s a problem.

You can probably see what the problem is straight away in this case, but to show you how to track down problems of this sort let’s pretend we’ve been hit by an attack of stupidity. If you can’t figure out what’s happening, the first thing to do is to get more information about what’s going wrong. You may have access to a debugger which will let you step through the program line by line, or set breakpoints so that the program halts whenever it gets to a particular line to let you inspect the values of selected variables as the program is running. Then again, you may not.

Using a debugger is the simplest thing to do as it doesn’t involve making any changes to the program to find out what’s going on. In the absence of a debugger, you have to modify the program to display a bit more information. In this case we know that the problem manifests itself in the case statement, so the simplest thing to do is to use Put to display the value of Command just before the case statement:

    Put ("Command = ["); Put (Command); Put_Line ("]");        -- DEBUG
    case Command is
        ...
    end case;

Tinkering with the program like this means you have to be careful to fix all your changes and test everything again when you’ve finished debugging. The comment ‘DEBUG’ at the end of the line is to make it easy to find and remove lines added for debugging purposes once the bug has been fixed. It might also be a good idea to comment out the lines displaying the menu (i.e. make them into comments so that they have no effect but can be reinstated later) in case debugging information scrolls off the top of the screen:

    -- DEBUG        New_Line (5);
    -- DEBUG        Put_Line ("Diary menu:");
    -- DEBUG        Put_Line (" [A]dd appointment");
    -- DEBUG        Put_Line (" [D]elete appointment");
    -- DEBUG        Put_Line (" [L]ist appointments");
    -- DEBUG        Put_Line (" [S]ave appointments");
    -- DEBUG        Put_Line (" [Q]uit");

Again, the comment ‘DEBUG’ shows that the lines are commented out for debugging purposes so we can track down these lines and uncomment them after the bug has been fixed. Removing lines is far more risky; you might introduce extra bugs by doing so, or even remove the source of the bug you’re trying to find! A better alternative would be to send debugging output somewhere else (e.g. into a file, or to a separate screen or window). Anyway, if you make the changes above, run the program and type in ‘ADD’, this is what you should see:

    Load_Diary called

    Enter your choice: ADD
    Command = [A]
    Add_Appointment called

    Enter your choice: Command = [D]
    Delete_Appointment called

    Enter your choice: Command = [D]
    Delete_Appointment called

    Enter your choice:

It should now be fairly obvious what’s going on. Each of the three characters on the line is being read in and treated as a command. A simple solution is to use Skip_Line to get rid of the rest of the line immediately after the call to Get:

    -- get a command
    Get (Command);
    Skip_Line;

Now if you recompile and try again, this is what you should see:

    Load_Diary called

    Enter your choice: ADD
    Command = [A]
    Add_Appointment called

    Enter your choice: XYZZY
    Command = [X]
    Invalid choice -- please enter A, D, L, S or Q

    Enter your choice:

There’s one more test to try. What happens if you type in the end-of-file character (which is usually control-Z or control-D)? You should find that the program halts with an End_Error exception. The solution to this one is obviously to add an exception handler for End_Error. However, after an end-of-file, the program might not be able to read any more input from the keyboard, so there probably isn’t any point in going round the loop again. The simplest thing to do is to exit the program. The end-of-file test is always a good one to remember; it’s surprising how many programs written by novices will just fall over gracelessly if you type the end-of-file character.

Here’s a fixed version of the main program:

    with Ada.Text_IO, JE.Diaries;
    use Ada.Text_IO, JE.Diaries;
    procedure Diary is
        Command : Character;
    begin
        Load_Diary;
        loop
            -- display menu
            ... as before

            -- get a command
            Get (Command);
            Skip_Line;                -- Bug fix added here
            -- perform selected command
            case Command is
                ... as before
            end case;
        end loop;

    exception                        -- Bug fix added here
        when End_Error =>
            null;                    -- do nothing, just end the program
    end Diary;

8.5 Displaying the appointments

The code to display the appointments is fairly simple. Since it will be needed to test the rest of the program, this is where I’ll start. All that it’ll need will be a loop to display each appointment in succession:

    procedure List_Appointments is
    begin
        for I in Diary.Appts'First .. Diary.Count loop
            Put ( Diary.Appts(I) );
        end loop;
    end List_Appointments;

I’m assuming the existence of a Put procedure to display an appointment in an appropriate form on the screen; we’ll need to provide this in the package body. The loop uses the value of the Count component of Diary to control the number of times the loop will be executed, and thus the number of appointments that will be displayed.

The trouble with this is that it’s too simple; when Count is zero, the loop will be executed zero times, with the result that absolutely nothing will be displayed. It would be better to detect this as a special case and deal with it separately:

    procedure List_Appointments is
    begin
        if Diary.Count = 0 then
             Put_Line ("No appointments found.");
        else
            for I in Diary.Appts'First .. Diary.Count loop
                Put ( Diary.Appts(I) );
            end loop;
        end if;
    end List_Appointments;

For the sake of simplicity, I’ll assume that the appointments are stored in ascending order of date and time, so that listing them in sequence produces an ordered list rather than a random jumble of appointments. This will be something to bear in mind when we consider how to add new appointments. I’ve also ignored the situation where there are more appointments than will fit on the screen; if this happens the screen will scroll up so that you’ll only be able to see the last screenful of appointments.

We need to implement Put before we can test this properly. Here’s a possible implementation:

    procedure Put (Item : in Appointment_Type) is
    begin
        Put (Item.Date.Day, Width => 2); Put ("-");
        Put (Item.Date.Month); Put ("-");
        Put (Item.Date.Year, Width => 4); Put (" ");
        Put (Item.Time.Hour, Width => 2); Put (":");
        Put (Item.Time.Minute, Width => 2); Put (" ");
        Put_Line (Item.Details (1..Item.Length));
    end Put;

This in turn requires versions of Put for the individual components of the appointment. These are all subtypes of Integer apart from Month_Type, so we can just instantiate Integer_IO for Integer and Enumeration_IO for Month_Type:

    with Ada.Text_IO;
    use Ada.Text_IO;
    package body JE.Diaries is
        subtype Day_Type is Integer range 1..31;
        type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
        subtype Year_Type is Integer range 1901..2099;
        subtype Hour_Type is Integer range 0..23;
        subtype Minute_Type is Integer range 0..59;

        package Int_IO is new Integer_IO (Integer);
        package Month_IO is new Enumeration_IO (Month_Type);
        use Int_IO, Month_IO;

        ... etc.

    end JE.Diaries;

You can test this before proceeding any further; you should just get a message saying ‘No appointments found’. You’ll need to implement Add_Appointment before you can test any further.


8.6 Adding new appointments

The process of adding an appointment can be broken down into two smaller steps: read in the new appointment, and add it to the diary. You can start with a simple-minded version that doesn’t keep the appointments in order; this will let you test that the Add_Appointment procedure works so far before going any further. All we need to do at this stage is to ask the user to enter the appointment details and then add the appointment to the end of the list. We’ll need an Appointment_Type variable to hold the new appointment; we can write the code to get the appointment details like this:

    procedure Add_Appointment is
        New_Appt : Appointment_Type;
    begin
        Put_Line ("Enter appointment details...");
        Put ("Date: ");
        Get (New_Appt.Date);
        Put ("Time: ");
        Get (New_Appt.Time);
        Put ("Details: ");
        Skip_Line;
        Get_Line (New_Appt.Details, New_Appt.Length);

        -- Add the appointment to the list
    end Add_Appointment;

This assumes that JE.Diaries will provide versions of Get to read in dates and times. Input is always an area where legitimate errors can occur due to typing mistakes, so you should always think about exception handling whenever input is done. You could just run the version of Add_Appointment above and find out which exceptions will occur by trial and error. You’ll find that a constraint error or a data error will be raised if the input is incorrect, so an exception handler will be required to deal with these errors. Here’s what it might look like:

    exception
        when Data_Error | Constraint_Error =>
            Put_Line ("Invalid date or time");
            Skip_Line;

Simple versions of Get for dates and times can just use the versions of Get for Integers and Month_Types which are already available in Int_IO and Month_IO to read in the components of the record, like this:

    procedure Get (Item : out Date_Type) is
    begin
        Get (Item.Day);
        Get (Item.Month);
        Get (Item.Year);
    end Get;

    procedure Get (Item : out Time_Type) is
    begin
        Get (Item.Hour);
        Get (Item.Minute);
    end Get;

This does minimal checking; it might be a good idea to check that the date is valid in the version of Get for dates. You could use the function Valid from chapter 4 or a variant of it, or use Ada.Calendar.Time_Of as described in the previous chapter to do this. If the date is invalid, a sensible response would be to raise a Constraint_Error so that entering the 31st of February would be reported as the same sort of error you would get if you entered the 32nd of January.

Now to add the appointment to the end of the diary. This involves incrementing the number of appointments (Diary.Count) so that it refers to the next free appointment and then storing the new appointment at that point in the array:

    Diary.Count := Diary.Count + 1;
    Diary.Appt (Diary.Count) := New_Appt;

If you try this out, you’ll find that it will also raise a constraint error when the diary is full (i.e. when Diary.Count goes out of range). The code for reading the appointment details also needs to handle constraint errors, so it needs to go in a separate block with its own Constraint_Error handler to allow the code for adding the appointment to have a different Constraint_Error handler. The final version of Add_Appointment will look like this when you put all these bits together:

    procedure Add_Appointment is
        New_Appt : Appointment_Type;
    begin
        begin
            Put_Line ("Enter appointment details...");
            Put ("Date: ");
            Get (New_Appt.Date);
            Put ("Time: ");
            Get (New_Appt.Time);
            Put ("Details: ");
            Skip_Line;
            Get_Line (New_Appt.Details, New_Appt.Length);
        exception
            when Data_Error | Constraint_Error =>
                Put_Line ("Error in input -- appointment not added");
                Skip_Line;
        end;

        Diary.Count               := Diary.Count + 1;
        Diary.Appts (Diary.Count) := New_Appt;

    exception
        when Constraint_Error =>
            Put_Line ("Diary full -- appointment not added");
    end Add_Appointment;

Now there are two separate Constraint_Error handlers: one in the inner block to cope with out-of-range input values and one in the outer block to cope with a full diary. When you test this you should discover that when there is an error in the input to Add_Appointment, an appointment is still added. This is because after the inner exception handler, execution continues with the statements after the inner block which will add a non-existent appointment to the diary. This can be fixed by adding a return statement to the inner exception handler:

    exception
        when Data_Error | Constraint_Error =>
            Put_Line ("Invalid date or time");
            Skip_Line;
            return;

At this point you can test List_Appointments more thoroughly. If you start with an empty diary you can add appointments one by one, listing the appointments before and after you do so, and see what happens. Your testing should reveal that there is still a bug in Add_Appointment.


8.7 Deleting appointments

We need to have some way to describe a specific appointment when we come to deletions; the simplest way would be to number the appointments when they are listed and get the user to specify the appointment number when deleting an appointment. To do this, we’ll need to modify List_Appointments to display an appointment number alongside each appointment:

    procedure List_Appointments is
    begin
        if Diary.Count = 0 then
             Put_Line ("No appointments found.");
        else
            for I in Diary.Appts'First .. Diary.Count loop
                Put (I, Width=>3); Put (") ");
                Put ( Diary.Appts(I) );
            end loop;
        end if;
    end List_Appointments;

The steps involved in deleting an appointment will be to read in the appointment number, check that it’s valid and then delete the corresponding appointment from the array. This means that we’ll need to declare a variable to hold the appointment number that the user types in:

    Appt_No : Positive;

Reading in the appointment number involves displaying a prompt and then reading a number into Appt_No:

    Put ("Enter appointment number: ");
    Get (Appt_No);

Of course, we’ll need to check that the appointment number is valid. We’ll need to provide an exception handler to check for input errors (constraint and data errors) as is nearly always the case when performing input:

    exception
        when Constraint_Error | Data_Error =>
            Put_Line ("Invalid appointment number");
            Skip_Line;

Diary.Count gives the number of appointments that the diary currently holds, so Appt_No must not be greater than Diary.Count. An easy way to respond to this is to treat it as a constraint error:

    if Appt_No not in Diary.Appts'First .. Diary.Count then
        raise Constraint_Error;
    end if;

Deleting the appointment involves moving all the appointments after the one identified by Appt_No up one place in the array and decrementing Diary.Count. The appointments can be moved using a slice which selects all the entries from Appt_No+1 to Diary.Count:

    Diary.Appt (Appt_No..Diary.Count-1) := Diary.Appt (Appt_No+1..Diary.Count);
    Diary.Count                         := Diary.Count - 1;

Putting all this together gives us this procedure:

    procedure Delete_Appointment is
        Appt_No : Positive;
    begin
        Put ("Enter appointment number: ");
        Get (Appt_No);
        if Appt_No not in Diary.Appts'First .. Diary.Count then
            raise Constraint_Error;
        end if;
        Diary.Appts(Appt_No..Diary.Count-1) := Diary.Appts(Appt_No+1..Diary.Count);
        Diary.Count                         := Diary.Count - 1;
    exception
        when Constraint_Error | Data_Error =>
            Put_Line ("Invalid appointment number");
            Skip_Line;
    end Delete_Appointment;

Testing this will require typing in both valid and invalid appointment numbers and checking that the correct appointment disappears when a valid appointment number is given. Testing for boundary cases is always important, that is to say those values at the upper and lower limits of the range. If you’ve got appointments numbered 1 to 5, does the procedure work properly for 1 and 5 and does it correctly report an error for 0 and 6? Similarly, test the boundary cases for the number of appointments in the diary. Does it work correctly when the diary is full, or when it’s empty, or when it just has a single appointment in it?


8.8 Loading and saving

The final two procedures are Load_Diary and Save_Appointments. I’ll consider these together since they both deal with the same file holding the diary. I’ll assume that the diary will be stored in a file called ‘Diary’, but I’ll define it as a string constant to make it easy to change later:

    Diary_File_Name : constant String := "Diary";

Saving the appointments can be done just like List_Appointments except that the appointments don’t need to be numbered and they’re written to a file instead of being displayed on the screen. It would also be a good idea to write the number of appointments at the start of the file. A File_Type variable (Diary_File) will be needed to do the file accesses. Here’s an outline:

    procedure Save_Appointments is
        Diary_File : File_Type;
    begin
        -- open the file
        -- write Diary.Count to the file
        for I in Diary.Appts'First .. Diary.Count loop
            -- write the I-th appointment to the file
        end loop;
        -- close the file
    end Save_Appointments;

Opening the file can be done using Create as described in the previous chapter. Closing the file just involves using Close. Various exceptions can be raised by Create, so an exception handler will be needed. Use_Error indicates that the file couldn’t be created; perhaps the disk is full or you don’t have write access to it, so this should be reported to the user. Name_Error will be raised if the filename is invalid and Status_Error will be raised if the file is already open. Neither of these should occur unless a total disaster occurs (the constant string "Diary", which is presumably a valid filename, has been corrupted somehow, or you’ve opened the file and forgotten to close it elsewhere in the program), so they shouldn’t be handled. That way if they ever do occur the exception will be reported as a genuine error which terminates the program:

    procedure Save_Appointments is
        Diary_File : File_Type;
    begin
        Create (Diary_File, Name => Diary_File_Name);
        -- write Diary.Count to the file
        for I in Diary.Appts'First .. Diary.Count loop
            -- write the I-th appointment to the file
        end loop;
        Close (Diary_File);
    exception
        when Use_Error =>
            Put_Line ("Couldn't create diary file!");
    end Save_Appointments;

Writing the appointments can be done using appropriate versions of Put for each of the appointment’s components. A space should be output between each component to separate them in the file so that they can be read in by Load_Diary, and each appointment should go on a separate line in the file:

    procedure Save_Appointments is
        Diary_File : File_Type;
    begin
        Create (Diary_File, Name => Diary_File_Name);
        Put (Diary_File, Diary.Count);
        New_Line (Diary_File);

        for I in Diary.Appts'First .. Diary.Count loop
            declare
                Appt : Appointment_Type renames Diary.Appts(I);
            begin
                Put (Diary_File, Appt.Date.Day, Width=>1);
                Put (Diary_File, ' ');
                Put (Diary_File, Appt.Date.Month);
                Put (Diary_File, ' ');
                Put (Diary_File, Appt.Date.Year, Width=>1);
                Put (Diary_File, ' ');
                Put (Diary_File, Appt.Time.Hour, Width=>1);
                Put (Diary_File, ' ');
                Put (Diary_File, Appt.Time.Minute, Width=>1);
                Put (Diary_File, ' ');
                Put (Diary_File, Appt.Details (1..Appt.Length));
                New_Line (Diary_File);
            end;
        end loop;
        Close (Diary_File);
    exception
        when Use_Error =>
            Put_Line ("Couldn't create diary file!");
    end Save_Appointments;

Notice how I’ve used a local block inside the loop so that I can rename the current appointment to avoid having to use long-winded names like Diary.Appts(I).Date.Day.

One of the simplest mistakes to make is to write:

    Put (' ');                    -- display space on screen

instead of:

    Put (Diary_File, ' ');        -- write space to diary file

The effects of this sort of mistake can be easy to overlook if you’re not being sufficiently careful. The program will compile and it will even appear to work. The spaces will be displayed invisibly on the screen instead of in the file, and if it weren’t for the ‘Width=>1’ parameter in the calls to Put you might not notice it due to the number of spaces that will be output in front of each integer. The only way you’d notice it in this case is if you’d worked out exactly what the diary file should look like and discovered that the actual file didn’t meet your expectations. This illustrates just how methodical you have to be if you don’t want to end up with a buggy program.

Load_Diary will need to do the same as Save_Appointments but in reverse; it’ll need to open the file for input, read the number of appointments, and then read in the appointment details one by one and then close the file:

    procedure Load_Diary is
        Diary_File : File_Type;
    begin
        -- open the file
        -- read Diary.Count
        for I in Diary.Appts'First .. Diary.Count loop
            -- read the I-th appointment from the file
        end loop;
        -- close the file
    end Load_Diary;

The file can be opened using Open. The exception handling for Open will need to be slightly different to that for Create; Name_Error indicates that the file doesn’t exist, and one way to handle this is to do nothing, so that the diary will just start off empty. Here’s the next version, with a few more details filled in:

    procedure Load_Diary is
        Diary_File : File_Type;
    begin
        Open (Diary_File, Mode => In_File, Name => Diary_File_Name);
        -- read Diary.Count

        for I in Diary.Appts'First .. Diary.Count loop
            begin
                -- read the I-th appointment from the file
            exception
                when End_Error => exit;
            end;
        end loop;

        Close (Diary_File);

    exception
        when Name_Error =>
            null;
        when Use_Error =>
            Put_Line ("Couldn't open diary file!");
    end Load_Diary;

Reading the appointments from the file can be done using a series of calls to Get:

    procedure Load_Diary is
        Diary_File : File_Type;
    begin
        Open (Diary_File, Mode => In_File, Name => Diary_File_Name);
        Get (Diary_File, Diary.Count);
        Skip_Line (Diary_File);

        for I in Diary.Appts'First .. Diary.Count loop
            declare
                Appt : Appointment_Type renames Diary.Appts(I);
            begin
                Get (Diary_File, Appt.Date.Day);
                Get (Diary_File, Appt.Date.Month);
                Get (Diary_File, Appt.Date.Year);
                Get (Diary_File, Appt.Time.Hour);
                Get (Diary_File, Appt.Time.Minute);
                Get_Line (Diary_File, Appt.Details, Appt.Length);
            end;
        end loop;

        Close (Diary_File);

    exception
        when Name_Error =>
            null;
        when Use_Error =>
            Put_Line ("Couldn't open diary file!");
    end Load_Diary;

You can test this by starting the program up, adding some appointments, saving them and then quitting. You should then have a file called Diary which you can look at with a text editor to verify that it’s correct. Make a copy of the file, and then start the program again. If you list the appointments you should see exactly what you had before. If you save the appointments again and quit the program, the Diary file and the copy of it that you saved earlier should be identical.

Unfortunately you’ll find that they aren’t identical; you should see two spaces between the time and the details in the latest version of the file but only one space in the saved copy. Of course, what’s happened is that Load_Diary has read the separating space as the first character of Details, so it will need modifying to read and ignore the space:

    procedure Load_Diary is
        Diary_File : File_Type;
    begin
        Open (Diary_File, Mode => In_File, Name => Diary_File_Name);
        Get (Diary_File, Diary.Count);
        Skip_Line (Diary_File);

        for I in Diary.Appts'First .. Diary.Count loop
            declare
                Appt  : Appointment_Type renames Diary.Appts(I);
                Space : Character;                      -- bug fix
            begin
                Get (Diary_File, Appt.Date.Day);
                Get (Diary_File, Appt.Date.Month);
                Get (Diary_File, Appt.Date.Year);
                Get (Diary_File, Appt.Time.Hour);
                Get (Diary_File, Appt.Time.Minute);
                Get (Diary_File, Space);                -- bug fix
                Get_Line (Diary_File, Appt.Details, Appt.Length);
            end;
        end loop;

        Close (Diary_File);

    exception
        when Name_Error =>
            null;
        when Use_Error =>
            Put_Line ("Couldn't open diary file!");
    end Load_Diary;

8.9 Assessing the program

So, at the end of all this you’ve got a working appointments diary. It’s not quite finished yet; there are still some details to attend to. Add_Appointment needs quite a bit more work, even if you’ve managed to fix all the bugs in the current version; appointments aren’t added in order yet, and as a result it’s possible to make double bookings. Dates aren’t validated either. Some more testing will be needed (e.g. what will happen if you’ve already got a file called Diary which doesn’t hold appointments produced by this program the first time you run it?).

One of the advantages of top-down design is that it allows programs to be developed on a piecemeal basis, a bit at a time. That way you don’t end up biting off more than you can chew. You can use stubs or incomplete versions to get yourself to a point where you can start testing; after your tests have been passed satisfactorily, you can incrementally implement a bit more, do a bit more testing, and so on until you’re finished (phew!). Testing a program thoroughly is an essential part of development; you’ve got to try and think of everything that can go wrong and what to do about it. You can help track down bugs by adding extra code to display debugging information or commenting out code as necessary, or by using a debugger. If all else fails, I find that just explaining the problem to anyone who’s prepared to listen usually helps. If they understand Ada, they might spot something you’ve overlooked; if they don’t, you might find that you spot the problem as you’re trying to explain what’s happening (or at least come up with a theory as to what the problem is). Well, it works for me anyway!

Exhaustive testing is usually out of the question, so you have to come up with a representative set of test data and a plan of how you’re going to carry out your testing. Plan your development steps in advance and do them in an order which will help you to test things. Make sure that valid data is handled correctly; look particularly closely at boundary conditions since these are usually where bugs can creep in. Make sure invalid data is detected and dealt with properly. Make sure you know what you expect the program to do, and check that it does exactly what you expect. Get used to looking at your code with a critical eye, and try to think of things that can go wrong as you’re writing it (such as the user typing an end-of-file character unexpectedly). And when you think you’ve finished and there’s no more to be done, spend a bit more time playing around with the program doing a mixture of sensible and silly things, because there might still be some combination of circumstances you’ve overlooked.


Exercises

8.1 It would be a good idea to ask the user whether or not to save any changes made to the diary before quitting. This is only necessary if any appointments have been added or deleted since the last time the diary was saved. Make the necessary modifications to the program.

8.2 Change the way that appointments are listed so that they are displayed a screenful at a time (or just under a screenful to allow room for prompts etc.) on the system that you’re using. Provide the ability to go either forwards or backwards a screen or half a screen at a time.

8.3 Add a ‘Find’ command to display all appointments containing a specified string in the Details component. Ignore distinctions between upper and lower case when searching for the string.

8.4 Write a program which will read the diary file and display all appointments for the next three working days (where Saturday and Sunday do not count as working days), so that if you ran this program on a Thursday it would show you all appointments from that day until the following Monday inclusive, since the three working days involved are Thursday, Friday and Monday.



Previous

Contents

Next

This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright © John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $