Java Arena
Step 3 of 5

Commands, and input that doesn't fit

The command programs of module 25 trusted their users. Real users mistype a command, ask for something that isn't there, or type five where a number should be. A program shouldn't crash then, and it shouldn't quietly do something wrong either: it should say what's wrong and wait for the next command. Three kinds of bad input come up in almost every command program:

Bad input Example The answer
An unknown command lend Unknown command: lend
A name that isn't there log, then Cleo, who isn't a volunteer No volunteer called Cleo
A number that isn't valid five, or 40 hours in one shift Hours must be a whole number from 1 to 12

A food bank's volunteers log the hours they help out. The plan is step 1's: Volunteer for the data, FoodBank for the logic, and UserInterface for the commands, as in module 25. The volunteers are added in main, and the commands are:

  • log, then a name and a number of hours: adds the hours to that volunteer. A shift is 1 to 12 hours.
  • total: prints the hours of all the volunteers added up.
  • quit: ends the program.
Main.java
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        FoodBank bank = new FoodBank();
        bank.add(new Volunteer("Ada"));
        bank.add(new Volunteer("Ben"));
        UserInterface ui = new UserInterface(scanner, bank);
        ui.start();
    }
}
Volunteer.java
public class Volunteer {
    private String name;
    private int hours;

    public Volunteer(String name) {
        this.name = name;
        this.hours = 0;
    }

    public String getName() {
        return this.name;
    }

    public int getHours() {
        return this.hours;
    }

    public void addHours(int hours) {
        this.hours += hours;
    }

    public String toString() {
        return this.name + ": " + this.hours + " hours";
    }
}
FoodBank.java
import java.util.ArrayList;

public class FoodBank {
    private ArrayList<Volunteer> volunteers;

    public FoodBank() {
        this.volunteers = new ArrayList<>();
    }

    public void add(Volunteer volunteer) {
        this.volunteers.add(volunteer);
    }

    public Volunteer find(String name) {
        for (Volunteer volunteer : this.volunteers) {
            if (volunteer.getName().equals(name)) {
                return volunteer;
            }
        }
        return null;
    }

    public int totalHours() {
        int total = 0;
        for (Volunteer volunteer : this.volunteers) {
            total += volunteer.getHours();
        }
        return total;
    }
}
UserInterface.java
import java.util.Scanner;

public class UserInterface {
    private Scanner scanner;
    private FoodBank bank;

    public UserInterface(Scanner scanner, FoodBank bank) {
        this.scanner = scanner;
        this.bank = bank;
    }

    public void start() {
        while (true) {
            System.out.print("> ");
            String command = this.scanner.nextLine();
            if (command.equals("quit")) {
                System.out.println("Thank you!");
                break;
            }
            if (command.equals("log")) {
                String name = this.scanner.nextLine();
                int hours = this.toNumber(this.scanner.nextLine());
                Volunteer volunteer = this.bank.find(name);
                if (volunteer == null) {
                    System.out.println("No volunteer called " + name);
                } else if (hours < 1 || hours > 12) {
                    System.out.println("Hours must be a whole number from 1 to 12");
                } else {
                    volunteer.addHours(hours);
                    System.out.println(volunteer);
                }
            } else if (command.equals("total")) {
                System.out.println("Total: " + this.bank.totalHours() + " hours");
            } else {
                System.out.println("Unknown command: " + command);
            }
        }
    }

    private int toNumber(String text) {
        try {
            return Integer.valueOf(text);
        } catch (Exception e) {
            return -1;
        }
    }
}
Input
log
Ada
4
log
Cleo
3
log
Ben
five
log
Ben
5
lend
total
quit
Output
> Ada: 4 hours
> No volunteer called Cleo
> Hours must be a whole number from 1 to 12
> Ben: 5 hours
> Unknown command: lend
> Total: 9 hours
> Thank you!

Each kind of bad input has its own place:

  • An unknown command falls through every if and else if to the last else. Without that else, a typo gets no answer at all.
  • A name that isn't there: the logic class says so with its return value. find returns null, and the user interface turns that into a message. FoodBank still doesn't print anything.
  • A number that isn't valid: turning typed text into a number is the user interface's job, so it checks the number too, before the logic gets it.

Read the whole command first

log takes three lines: the command, the name and the hours. The user interface reads all of them before it checks anything. Suppose it checked the name first and skipped the rest of the command when the name was wrong. The hours, 3, would still be waiting in the input, and the next round would read them as a command: Unknown command: 3. Every command reads the same number of lines, whatever is wrong with them.

Is it a number?

Integer.valueOf("five") throws a NumberFormatException, and without a catch the program would crash. Module 20's try and catch turn the crash into an answer. The helper method toNumber returns the number, or -1 when the text isn't a whole number:

private int toNumber(String text) {
    try {
        return Integer.valueOf(text);
    } catch (Exception e) {
        return -1;
    }
}
  • When Integer.valueOf succeeds, return hands the number back right away. When it throws, Java jumps to the catch block, and that returns -1.
  • -1 works as a signal because no valid number of hours is negative. The range check after it catches -1 together with 0, 40 and every other number outside 1 to 12, so one message covers them all.
  • The method is private (module 18): it's a helper for the user interface itself, and no other class needs it.

Your task

Challenge 1 of 3

Neighbors share tools through a tool library. Tool.java, ToolLibrary.java and Main.java are ready: the library has a drill, a ladder and a saw, find returns the tool with the given name or null, and isAvailable() tells whether a tool is on the shelf. In UserInterface.java, return already checks its tool. borrow reads two lines, the tool and the person, but it has no checks yet, and unknown commands get no answer. Add:

  • in borrow, when there's no tool by that name: No tool called and the name,
  • in borrow, when the tool is lent out: The , the name and is lent out,
  • otherwise lend it, as now,
  • an answer to unknown commands: Unknown command: and the command.

For the input shown, the program prints:

> Ana borrowed the ladder
> The ladder is lent out
> No tool called hammer
> The ladder is back on the shelf
> Ben borrowed the ladder
> Unknown command: fix
> drill: on the shelf
ladder: lent to Ben
saw: on the shelf
> Bye

Challenge 2

A café's cake counter sells cakes by the slice. Cake.java, CakeCounter.java and Main.java are ready: the counter has a carrot cake of 8 slices and a lemon tart of 4, find returns the cake with the given name or null, and a cake has getName(), getSlices() and sell(int slices). In UserInterface.java, write the sell command. It reads two lines: the cake's name, and how many slices. Read both lines first, and then check, in this order:

  1. No cake by that name: print No cake called and the name.
  2. The slices aren't a whole number of at least 1 (such as two, 0 or -3): print Slices must be a whole number, 1 or more.
  3. The cake has fewer slices left: print Not enough , the name, : and the slices left, like Not enough lemon tart: 4 left.
  4. Otherwise, sell them and print Sold , the slices, of, the name, , and the slices left, like Sold 2 of carrot cake, 6 left.

Turn the text into a number with try and catch, as in the lesson, so that text that isn't a whole number counts as not valid. For the input shown, the program prints:

> Sold 2 of carrot cake, 6 left
> No cake called banana bread
> Slices must be a whole number, 1 or more
> Slices must be a whole number, 1 or more
> Not enough lemon tart: 4 left
> Sold 4 of lemon tart, 0 left
> carrot cake: 6 slices
lemon tart: 0 slices
> Closed

Challenge 3

A music school books its practice rooms with commands. book should read a room's name and then a number of hours. What does this program print? Follow each line of input: which part of the program reads it?