CompSci.rocks
FRQcsapa

Phrase: 2017 FRQ 3

A step-by-step solution to the 2017 AP CSA FRQ 3 (Phrase), covering replacing and locating the nth occurrence of a substring by composing a given helper method with substring extraction in Java.

Locating and replacing one specific occurrence of a substring — not just the first one — drives this AP Computer Science A free-response question, and it leans on a helper method that's already written for you rather than asking you to search from scratch.

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: combining an already-given helper method (findNthOccurrence) with substring to rebuild a String around a target location
  • Secondary skill: turning a "find the last occurrence" search into repeated calls to an "find the nth occurrence" search
  • Official category: "Methods and Control Structures," which on the 2017 exam was FRQ 3 (the fixed FRQ 1–4 category order used in more recent years — Methods and Control Structures, Classes, Array/ArrayList, 2D Array, always in that sequence — wasn't standardized until the 2019–2020 Course and Exam Description redesign; 2017's actual printed order was Digits, MultPractice, Phrase, Successors)

The Setup

  • The given Phrase class has:
    • private String currentPhrase
    • A constructor: Phrase(String p)
    • findNthOccurrence(String str, int n) — already implemented (not shown to you); returns the index of the nth occurrence of str in currentPhrase, or -1 if it doesn't exist; guaranteed not to modify currentPhrase
    • toString() — already implemented, returns currentPhrase
  • You're asked to write two methods:
    • replaceNthOccurrence(String str, int n, String repl) — replaces the nth occurrence of str with repl, if it exists
    • findLastOccurrence(String str) — returns the index of the last occurrence of str, or -1
  • Both parts explicitly state that you must use findNthOccurrence to receive full credit — writing your own search logic instead, even if it produces a correct answer, does not.

Part (a): Writing replaceNthOccurrence

The Rule, Broken Down

  1. Find the index of the nth occurrence of str using findNthOccurrence.
  2. If that index is -1 (the occurrence doesn't exist), leave currentPhrase completely unchanged.
  3. Otherwise, rebuild currentPhrase as three pieces glued together: everything before the match, repl, then everything after the matched text.

Step-by-Step Approach

  1. Call findNthOccurrence(str, n) and store the result.
  2. Check whether that result is -1.
  3. If it isn't, build the new phrase from substring(0, index) (everything before the match), repl, and substring(index + str.length()) (everything after the entire matched text).
  4. Assign that combined String back to currentPhrase.

The Code

public void replaceNthOccurrence(String str, int n, String repl)
{
    int index = findNthOccurrence(str, n);

    if (index != -1)
    {
        currentPhrase = currentPhrase.substring(0, index) + repl
                + currentPhrase.substring(index + str.length());
    }
}

Why Each Piece Matters

  • Calling findNthOccurrence(str, n) directly, not currentPhrase.findNthOccurrence(...) (which wouldn't even compile — currentPhrase is a String, and findNthOccurrence is a method on the Phrase object itself, not on the String it stores).
  • substring(index + str.length()), not substring(index + 1) — the "after" piece needs to skip past the entire matched text, which is str.length() characters long, not just one character of it.
  • Wrapping the reassignment in if (index != -1) is exactly what makes "leave the phrase unchanged if the occurrence doesn't exist" work — without it, building a substring starting at index -1 would throw an exception instead.

Tracing the Example

Phrase phrase1 = new Phrase("A cat ate late.") — the substring "at" occurs at indices 3 ("cat"), 6 ("ate"), and 11 ("late"):

Call findNthOccurrence result Result
phrase1.replaceNthOccurrence("at", 1, "rane") index 3 "A c" + "rane" + " ate late.""A crane ate late."
phrase2.replaceNthOccurrence("at", 6, "xx") -1 (only 3 occurrences exist) unchanged: "A cat ate late."
phrase3.replaceNthOccurrence("bat", 2, "xx") -1 ("bat" never occurs) unchanged: "A cat ate late."
phrase4 = new Phrase("aaaa"), .replaceNthOccurrence("aa", 1, "xx") index 0 "" + "xx" + "aa""xxaa"
phrase5 = new Phrase("aaaa"), .replaceNthOccurrence("aa", 2, "bbb") index 1 "a" + "bbb" + "a""abbba"

All five results match the question's expected output exactly.

Common Mistakes to Avoid

  • Reimplementing the search logic instead of calling findNthOccurrence. The problem explicitly requires using it for full credit, even though a hand-written search loop could technically land on the same answer.
  • Using index + 1 instead of index + str.length() for the "after" substring — this only skips one character of the match instead of the whole thing, corrupting any replacement involving a multi-character str.
  • Forgetting the -1 check entirely, which throws a StringIndexOutOfBoundsException the moment the nth occurrence doesn't exist.

Part (b): Writing findLastOccurrence

The Rule, Broken Down

  1. The "last occurrence" is simply whichever occurrence number turns out to be the highest one that still exists.
  2. findNthOccurrence(str, n) already tells you whether occurrence n exists — it returns -1 when it doesn't.
  3. So the plan is: keep checking higher occurrence numbers until the next one stops existing, then return the last one that did.

Step-by-Step Approach

  1. Start a counter, n, at 1 — occurrences are numbered starting at 1, per findNthOccurrence's own precondition (n > 0).
  2. Repeatedly check whether occurrence n + 1 exists, by calling findNthOccurrence(str, n + 1).
  3. If it does exist, advance n to n + 1 and check again.
  4. The moment occurrence n + 1 no longer exists, n itself is the last occurrence that does — return findNthOccurrence(str, n).
  5. If str never occurs at all, this same process still works correctly: even occurrence 1 doesn't exist, so the final call returns -1.

The Code

public int findLastOccurrence(String str)
{
    int n = 1;

    while (findNthOccurrence(str, n + 1) != -1)
    {
        n++;
    }

    return findNthOccurrence(str, n);
}

Why Each Piece Matters

  • Checking n + 1 (the next one) instead of n itself — this is what makes the loop "look ahead" before committing; n only advances once a higher occurrence is confirmed to exist.
  • Both the loop condition and the final return call findNthOccurrence — this reuses the given helper method twice instead of writing brand-new search logic, which is exactly what the problem's instructions require for full credit.
  • Starting n at 1, not 0 — occurrences are 1-indexed, matching findNthOccurrence's own documented precondition.

Tracing the Example

Phrase phrase1 = new Phrase("A cat ate late."):

Call Occurrences of the search string Result
phrase1.findLastOccurrence("at") at indices 3, 6, 11 (occurrences 1, 2, 3) loop advances n to 3 once occurrence 4 fails to exist → returns 11
phrase1.findLastOccurrence("cat") only at index 2 (occurrence 1) occurrence 2 doesn't exist, loop never advances n past 1 → returns 2
phrase1.findLastOccurrence("bat") never occurs even occurrence 1 doesn't exist → returns -1

All three results match the question's table exactly.

Common Mistakes to Avoid

  • Calling currentPhrase.lastIndexOf(str) directly. This is completely valid Java and produces the exact same correct output — but this specific question's own instructions explicitly require routing the answer through findNthOccurrence to receive credit. The official 2017 scoring guidelines confirm a lastIndexOf-only solution earns none of the four points for this part, regardless of whether the returned value is correct. This is different from the usual "not on the Quick Reference sheet, but still valid" situation elsewhere on this site — it's this particular FRQ's own stated requirement, not a general exam rule.
  • Checking n itself instead of n + 1 in the loop condition — this stops one occurrence too early.
  • Forgetting the case where str never occurs at all. The loop needs to terminate correctly and return -1 in that case, not loop forever or throw an exception.

Key Takeaways

  • When a helper method already answers "does this occurrence exist," a "find the last one" problem becomes "keep asking for the next one until it says no."
  • Replacing a located substring is always three pieces glued together — everything before, the replacement, everything after — and the "after" piece must skip exactly str.length() characters, not just one.
  • A problem that hands you a helper method and says "you must use it" is testing your ability to compose methods, not just your ability to reach the right final string — a shortcut that reaches the same answer a different way can still lose all the credit for that part.

Related FRQs