CompSci.rocks
FRQcsapa

AppointmentBook: 2023 FRQ 1

A step-by-step solution to the 2023 AP CSA FRQ 1 (AppointmentBook), covering nested loops that search for a block of consecutive free minutes and reusing that search to book an appointment in Java.

Booking a slot in a teacher's schedule is the real-world scenario behind this AP Computer Science A free-response question — you first search for an open stretch of minutes within a single class period, then reuse that search across several periods to actually reserve an appointment.

What This FRQ Tests

  • AP CSA units: Unit 3 (Boolean Expressions and if Statements), Unit 4 (Iteration), and Unit 5 (Writing Classes/methods)
  • Core skill: using nested loops to scan a fixed range (the minutes in a period) for the first position where a run of consecutive values all satisfy a condition
  • Secondary skill: building a second method on top of a first one you just wrote, instead of duplicating its logic
  • Official category: "Methods and Control Structures" — always FRQ 1 on the AP CSA exam

The Setup

  • Appointments happen during one of 8 class periods (numbered 1–8); each period has 60 minutes, numbered 0–59.
  • Two private helper methods already exist and cannot be modified:
    • boolean isMinuteFree(int period, int minute) — is this single minute open?
    • void reserveBlock(int period, int startMinute, int duration) — marks a whole block of minutes as taken
  • You're asked to write two public methods:
    • int findFreeBlock(int period, int duration) — find the first minute where a duration-minute-long free block starts; return -1 if none exists
    • boolean makeAppointment(int startPeriod, int endPeriod, int duration) — search several periods in order, and reserve the first free block found

Part (a): Writing findFreeBlock(int period, int duration)

The Rule, Broken Down

  1. A "block" means duration consecutive minutes that are all individually free.
  2. Blocks are checked in order of their starting minute (0, then 1, then 2, ...) — the first one found is the answer.
  3. The last minute a block could possibly start on is 60 - duration, since a block can't run past minute 59.
  4. If no starting minute produces a fully-free block, the method returns -1.

Step-by-Step Approach

  1. Loop over every possible starting minute, from 0 up to 60 - duration.
  2. For each candidate starting minute, check every minute inside that candidate's block using isMinuteFree.
  3. If any single minute in the block turns out to be taken, that candidate fails — move on to the next starting minute.
  4. If every minute in the block is free, that starting minute is the answer — return it right away.
  5. If the outer loop finishes without ever returning, no valid block exists — return -1.

The Code

public int findFreeBlock(int period, int duration)
{
    for (int start = 0; start <= 60 - duration; start++)
    {
        boolean blockIsFree = true;

        for (int minute = start; minute < start + duration; minute++)
        {
            if (!isMinuteFree(period, minute))
            {
                blockIsFree = false;
            }
        }

        if (blockIsFree)
        {
            return start;
        }
    }

    return -1;
}

Why Each Piece Matters

  • start <= 60 - duration — this bound stops the search before it ever tests a block that would spill past minute 59. Getting this off by one (< instead of <=, or forgetting to subtract duration at all) either misses a valid last-minute block or lets the inner loop run off the end of the period.
  • The inner loop checks every minute, not just the first one. A block only counts if every minute inside it is free — checking just isMinuteFree(period, start) would wrongly accept a block that starts free but has a taken minute somewhere in the middle.
  • Returning inside the outer loop, the moment a valid block is found, guarantees the earliest qualifying start minute is the one returned — exactly what the problem requires when multiple valid blocks exist.

Tracing the Example

Using the sample data from the question — period 2's minutes:

Minutes Available?
0–9 (10 minutes) No
10–14 (5 minutes) Yes
15–29 (15 minutes) No
30–44 (15 minutes) Yes
45–49 (5 minutes) No
50–59 (10 minutes) Yes
  • findFreeBlock(2, 15): starting minutes 0–29 all fail (either landing on unavailable minutes, or, for starts in 10–14, running into the unavailable 15–29 stretch before 15 minutes are up). Start 30 succeeds — minutes 30–44 are exactly the 15 free minutes needed. Returns 30, matching the question.
  • findFreeBlock(2, 9): the 10–14 stretch is only 5 minutes long, too short for 9. Start 30 is again the first success (30–38 all fall inside the free 30–44 stretch). Returns 30, matching the question — even though a 9-minute block only needs part of the 30–44 range, 30 is still the earliest valid start.
  • findFreeBlock(2, 20): the largest free stretch anywhere in period 2 is 15 minutes (30–44). No starting minute can produce 20 consecutive free minutes. The loop finishes without returning, so it returns -1, matching the question.

Common Mistakes to Avoid

  • Getting the upper bound wrong — using start < 60 - duration skips a valid final starting minute; forgetting the - duration entirely lets the inner loop check minutes beyond 59.
  • Checking only the candidate's first minute instead of the whole block. A block that starts free but becomes unavailable partway through must still be rejected.
  • Not stopping early once a match is found. It isn't strictly wrong to keep looping, but returning immediately is simpler and directly reflects "the first/earliest block."

Part (b): Writing makeAppointment(int startPeriod, int endPeriod, int duration)

Step-by-Step Approach

  1. Loop through the periods from startPeriod to endPeriod, inclusive.
  2. For each period, call findFreeBlock to search for a valid block.
  3. If a valid block is found (the result isn't -1), call reserveBlock with that period, that starting minute, and duration, then return true immediately.
  4. If the loop finishes without ever finding a block in any period, return false.

The Code

public boolean makeAppointment(int startPeriod, int endPeriod, int duration)
{
    for (int period = startPeriod; period <= endPeriod; period++)
    {
        int start = findFreeBlock(period, duration);

        if (start != -1)
        {
            reserveBlock(period, start, duration);
            return true;
        }
    }

    return false;
}

Why Each Piece Matters

  • Reusing findFreeBlock instead of re-writing the block-search logic is exactly what the problem is testing — the note "assume findFreeBlock works as intended, regardless of what you wrote in part (a)" is a signal to trust and build on top of it.
  • Checking periods in increasing order and returning the moment a block is found guarantees "the lowest-numbered period" is the one used, since the first period that works is kept and nothing later is ever considered.
  • reserveBlock is only ever called with a real, valid starting minute — never with -1. The if (start != -1) check has to come before the call, not after.

Tracing the Example

Using the question's sample data for periods 2, 3, and 4:

Period Minutes Available?
2 0–24 (25 minutes) No
2 25–29 (5 minutes) Yes
2 30–59 (30 minutes) No
3 0–14 (15 minutes) Yes
3 15–40 (26 minutes) No
3 41–59 (19 minutes) Yes
4 0–4 (5 minutes) No
4 5–29 (25 minutes) Yes
4 30–43 (14 minutes) No
4 44–59 (16 minutes) Yes
  • makeAppointment(2, 4, 22): period 2's biggest free stretch is only 5 minutes, period 3's is 19 — both too small for 22. Period 4's 5–29 stretch is 25 minutes long, enough for 22; findFreeBlock(4, 22) returns 5. reserveBlock(4, 5, 22) marks minutes 5–26 unavailable. Returns true — matches the question exactly.
  • makeAppointment(3, 4, 3): period 3 is checked first. Its 0–14 stretch easily fits 3 minutes; findFreeBlock(3, 3) returns 0. reserveBlock(3, 0, 3) marks minutes 0–2 unavailable. Returns true — matches, and period 4 is never even examined.
  • makeAppointment(2, 4, 30): by this point period 2's largest stretch is 5, period 3's is 12 (after the previous call), period 4's is 16 (after the first call) — none reach 30. The loop finishes with nothing reserved, returning false — matches.

Common Mistakes to Avoid

  • Calling reserveBlock unconditionally, even when findFreeBlock returned -1 — this reserves a nonsensical block starting at minute -1.
  • Checking every period before reserving anything, instead of stopping at the first success. This can violate "the lowest-numbered period" if a later period is accidentally preferred, and it's unnecessary extra work.
  • Forgetting to return true immediately after a successful reservation, or accidentally falling through to return false afterward.

Key Takeaways

  • Searching a fixed range for the first position where a multi-step condition holds is a nested-loop pattern: the outer loop tries each position, the inner loop verifies the condition holds across the whole span.
  • Once a helper method is written and trusted, later methods should call it rather than re-implement the same search.
  • Returning immediately on success both simplifies the control flow and naturally guarantees "the first/earliest" result the problem is asking for.

Related FRQs