CompSci.rocks
FRQcsapa

SignedText: 2025 FRQ 2

A step-by-step solution to the 2025 AP CSA FRQ 2 (SignedText), covering building a formatted signature string and rewriting text based on where that signature already appears in Java.

Appending a signature to the end of a message — unless it's already there — is the job of the class in this AP Computer Science A free-response question, and getting it right means carefully working through three different cases for where that signature might already sit.

What This FRQ Tests

  • AP CSA units: Unit 5 (Writing Classes)
  • Core skill: designing a class from scratch, including a method that formats a value from two stored fields
  • Secondary skill: checking a fixed-length prefix and suffix of a String to decide between three different outcomes
  • Official category: "Classes" — always FRQ 2 on the AP CSA exam

The Setup

  • No class skeleton is provided — the entire SignedText class is up to you to design.
  • The constructor takes a first name and a last name (the last name's length is always at least 1).
  • getSignature() builds a signature string:
    • If the first name is empty, the signature is just the last name.
    • Otherwise, the signature is the first letter of the first name, a dash, and the last name.
  • addSignature(String text) returns a revised copy of text, which is guaranteed to contain the signature at most once, and only at the very beginning or the very end:
    • If the signature doesn't appear in text at all, append it to the end.
    • If it already appears at the end, return text unchanged.
    • If it appears at the beginning, move it: strip it off the front and append it to the end instead.

Building the SignedText Class

Step-by-Step Approach

  1. Store the first and last names as fields in the constructor.
  2. Write getSignature(): check whether the first name is empty first, since that decides which of the two formats to build.
  3. Write addSignature(text): compute the signature once, then check the end of text first (since "already correctly placed" should short-circuit everything else), then the beginning, and only fall through to "append it" if neither matched.

The Code

public class SignedText
{
    private String firstName;
    private String lastName;

    public SignedText(String first, String last)
    {
        firstName = first;
        lastName = last;
    }

    public String getSignature()
    {
        if (firstName.equals(""))
        {
            return lastName;
        }
        else
        {
            return firstName.substring(0, 1) + "-" + lastName;
        }
    }

    public String addSignature(String text)
    {
        String signature = getSignature();
        int sigLen = signature.length();

        if (text.length() >= sigLen && text.substring(text.length() - sigLen).equals(signature))
        {
            return text;
        }
        else if (text.length() >= sigLen && text.substring(0, sigLen).equals(signature))
        {
            return text.substring(sigLen) + signature;
        }
        else
        {
            return text + signature;
        }
    }
}

Why Each Piece Matters

  • firstName.equals("") — checked first in getSignature(), since it decides between two completely different formats, not a variation on one format.
  • firstName.substring(0, 1) — pulls out just the first character as a String, since charAt isn't part of the method subset this solution sticks to.
  • Checking the end of text before the beginning in addSignature — the precondition guarantees the signature appears in at most one of those two positions, but checking end-first means "already correctly placed" is recognized immediately, without needing to also rule out the beginning case first.
  • text.length() >= sigLen, guarding both substring checks — without it, text.substring(text.length() - sigLen) or text.substring(0, sigLen) could receive a negative or out-of-range index and throw an exception on a text shorter than the signature itself.
  • text.substring(sigLen) + signature — when the signature is found at the beginning, this strips exactly that many characters off the front (everything from index sigLen onward) and reattaches the signature at the end.

Tracing the Example

Using the question's own execution sequence, with st4 = new SignedText("", "FOX") (signature "FOX", length 3):

Call End matches? Start matches? Result
st4.addSignature("Dear") no no "DearFOX"
st4.addSignature("Best wishesFOX") yes "Best wishesFOX" (unchanged)
st4.addSignature("FOXThanks") no yes "Thanks" + "FOX" = "ThanksFOX"

And with st3 = new SignedText("GRACE", "LOPEZ") (signature "G-LOPEZ", length 7):

  • st3.addSignature("G-LOPEZHello"): end check fails (last 7 characters are "ZHello"... actually the last 7 characters of a 12-character string starting at index 5 don't match "G-LOPEZ"), start check succeeds ("G-LOPEZ" is exactly the first 7 characters) → returns "Hello" + "G-LOPEZ" = "HelloG-LOPEZ".

Both results match the question's table exactly, including the case where the signature has to be relocated from front to back.

Common Mistakes to Avoid

  • Checking the beginning before the end. Since the precondition guarantees the signature appears at most once, checking order doesn't change correctness here — but checking the end first keeps the "no change needed" case simple and matches the order the rules are listed in the question.
  • Forgetting the length guard before calling substring. A text shorter than the signature would otherwise crash with a StringIndexOutOfBoundsException instead of correctly falling through to "doesn't contain it, append."
  • Building the first-name check backwards — testing !firstName.equals("") first and putting the "just the last name" case in the else also works, but flips which branch does which; either is fine as long as the empty-first-name case returns just lastName.
  • Using charAt(0) instead of substring(0, 1). Both grab the first character in real Java, but charAt isn't on the Quick Reference sheet — substring keeps the solution within what's guaranteed to be there to look up.

Key Takeaways

  • When a rule branches into "already correct / needs one fix / needs a different fix," check the "already correct" case first — it's usually the simplest to rule in or out.
  • Any time a solution slices a String by a computed length (not a fixed one), guard the slice with a length comparison first, since a too-short input can otherwise crash a substring call.
  • Building a formatted value from two stored fields inside a getter (rather than storing the formatted result itself) keeps a class's state minimal and always up to date if the fields it's built from could ever change.

Related FRQs