Study Planner

The Course
•
5 min read

Today you will connect the pieces you have practised separately. The planner is a console application with three source files. It accepts sessions, stores valid ones, marks the first accepted session complete and reports planned and completed minutes. It deliberately has no database, files, GUI or web service. Data lasts for one run; learning the model is the goal.

Read the contract first

Each session has a trimmed, nonblank topic and whole-number minutes from 1 to 240. Duplicate topics are allowed: the list position identifies a session. A plan accepts at most 100 sessions, keeping its largest total at 24,000 minutes. Invalid additions leave the plan unchanged. Completion of a valid index returns true; invalid indexes return false. Completing the same session twice counts its minutes only once. An empty plan reports zero totals.

Three files, three responsibilities

Main.java reads two lines per record: topic, then duration text. It parses the number, calls the model and explains rejected pairs. Session.java validates and stores one record. Planner.java owns an ArrayList<Session>, completes by index and calculates reports. A final list field means the planner keeps that list reference; it does not mean the list’s contents cannot grow.

Follow one accepted record through these files before following an invalid one. Planner constructs the Session before adding it, so a throwing constructor cannot leave a partially accepted element in the list. The report calculates totals from current records rather than incrementing a second stored completion total that could drift out of sync. A rule that must stay true after every allowed operation is called an invariant. Here, completed minutes must never exceed planned minutes, and repeating completion must not increase the total.

Input and error policy

The supplied input has Java/25, Broken/nope and Reading/10. Main skips the malformed pair and continues. A final topic with no duration produces a missing-minutes message. NumberFormatException is caught before IllegalArgumentException because it is a more specific kind of that exception; the reverse catch order would make the specific catch unreachable. IllegalStateException reports a full plan and stops adding. These are separate failures with intentional behavior.

Run the complete planner Java
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner input = new Scanner(System.in); Planner planner = new Planner(); // Two lines per session: a topic, then whole-number minutes. while (input.hasNextLine()) { String topic = input.nextLine(); if (!input.hasNextLine()) { System.out.println("Missing minutes for: " + topic); break; } String minutesText = input.nextLine(); try { int minutes = Integer.parseInt(minutesText.trim()); planner.add(topic, minutes); } catch (NumberFormatException error) { System.out.println("Skipped: minutes must be a whole number"); } catch (IllegalArgumentException error) { System.out.println("Skipped: " + error.getMessage()); } catch (IllegalStateException error) { System.out.println(error.getMessage()); break; } } // This demo completes the first accepted session, if there is one. planner.complete(0); System.out.println(planner.report()); } } import java.util.ArrayList; public class Planner { private final ArrayList<Session> sessions = new ArrayList<>(); public void add(String topic, int minutes) { if (sessions.size() >= 100) { throw new IllegalStateException("Plan is full"); } // Construct and validate before changing the list. Session session = new Session(topic, minutes); sessions.add(session); } public int size() { return sessions.size(); } public boolean complete(int index) { if (index < 0 || index >= sessions.size()) { return false; } sessions.get(index).complete(); return true; } public int plannedMinutes() { int total = 0; for (Session session : sessions) { total += session.minutes(); } return total; } public int completedMinutes() { int total = 0; for (Session session : sessions) { if (session.isDone()) { total += session.minutes(); } } return total; } public String report() { int planned = plannedMinutes(); int completed = completedMinutes(); return size() + " sessions, " + completed + "/" + planned + " min complete"; } } public class Session { private final String topic; private final int minutes; private boolean done; public Session(String topic, int minutes) { if (topic == null || topic.trim().isEmpty()) { throw new IllegalArgumentException("Topic is required"); } if (minutes < 1 || minutes > 240) { throw new IllegalArgumentException("Minutes must be 1 to 240"); } this.topic = topic.trim(); this.minutes = minutes; } public String topic() { return topic; } public int minutes() { return minutes; } public boolean isDone() { return done; } public void complete() { // Completing a session twice still counts it only once. done = true; } }
Check your prediction
Skipped: minutes must be a whole number
2 sessions, 25/35 min complete

Open Session.java and read its guards, then trace planner.add in Planner.java. The failed Broken pair never becomes a Session. Main completes index 0 only after reading input, so the first accepted record contributes 25 completed minutes.

The reporting arithmetic is still ordinary code Java
public class Main {
    public static void main(String[] args) {
        int planned = 25 + 10;
        int completed = 25;

        System.out.println(completed + "/" + planned + " min complete");
        System.out.println(planned - completed);
    }
}
Check your prediction
25/35 min complete
10

Separating a project into files has not changed the arithmetic. Keep the data-flow questions small: which values are included, which state determines inclusion, and where is the report built?

Your turn

Exercise

Trace repeated completion

Before editing the full project, reproduce its counting rule with arrays: durations {25,10}, done flags {true,false}. Sum only done sessions and print 25. Then set done[0]=true again, recalculate from the records and print 25 again. Explain why there is no extra increment.

Your workspace Java
public class Main {
    public static void main(String[] args) {
        int[] minutes = {25, 10};
        boolean[] done = {true, false};
        // Calculate completion twice without counting one record twice.
    }
}
Hint 1: the idea

The result is derived from current records, not from the number of times an action was requested. Assume the two arrays have matching lengths.

Hint 2: a first step

Initialize a fresh total for each calculation and include each completed position once. Assigning true twice does not create another record.

Show one solution
public class Main {
    public static void main(String[] args) {
        int[] minutes = {25, 10};
        boolean[] done = {true, false};

        System.out.println(completed(minutes, done));
        done[0] = true;
        System.out.println(completed(minutes, done));
    }

    static int completed(int[] minutes, boolean[] done) {
        int total = 0;

        for (int index = 0; index < minutes.length; index++) {
            if (done[index]) {
                total += minutes[index];
            }
        }

        return total;
    }
}
25
25

Change it and explain

In the project editor, try empty input; a blank topic with a valid duration; minutes 0, 1, 240 and 241; and a final unmatched topic. Predict both rejection messages and the final report before running. A skipped record must not secretly increase planner.size or plannedMinutes.

Next, open the test entry instead of relying on manual output inspection. Do not add features while still uncertain which class owns the existing rules. Draw the call sequence Main → Planner → Session and explain why input parsing belongs in Main while domain validation still belongs in Session.

Was this article helpful?

Your response is saved on this device.