CompSci.rocks
FRQcsapa

Sign: 2023 FRQ 2

A step-by-step solution to the 2023 AP CSA FRQ 2 (Sign), covering writing a complete Java class from scratch, integer math tricks, and building a semicolon-separated String.

Splitting one long message across the fixed-width lines of an electronic sign is the challenge in this AP Computer Science A free-response question — and unlike most FRQs, you're not filling in methods of a class that's already been started for you; you're writing the entire Sign class yourself, fields included.

What This FRQ Tests

  • AP CSA units: Unit 5 (Writing Classes)
  • Core skill: designing a class's fields and constructor from a plain-English description, with nothing already scaffolded
  • Secondary skill: integer arithmetic tricks (computing a "round up" division without a rounding method) and building up a formatted String piece by piece inside a loop
  • Official category: "Classes" — always FRQ 2 on the AP CSA exam

The Setup

  • Sign needs a constructor: Sign(String message, int width)width is the positive maximum number of characters per line.
  • The message is split into lines of exactly width characters each, without regard to words or punctuation — only the last line may be shorter.
  • Two methods to write:
    • int numberOfLines() — how many lines the message needs
    • String getLines() — the message broken into lines, joined with semicolons (;), with no semicolon at the start, end, or doubled up; returns null for an empty message

Building the Sign Class

The Rule, Broken Down

  1. The class needs to remember the message and the width — there's nothing else given to work with, so these become the two fields.
  2. numberOfLines() is a "round up" division: an 8-character message on a 3-character-wide sign needs 3 lines (2 full lines of 3, plus a partial line of 2), not 8 / 3 = 2.
  3. getLines() walks the message in width-sized chunks, and glues each chunk onto the result with a semicolon between chunks — never before the first one or after the last one.
  4. An empty message is a special case: zero lines, and getLines() returns null rather than an empty String.

Step-by-Step Approach

  1. Declare two private fields, message and width, and set them in the constructor.
  2. For numberOfLines(), use the standard "ceiling division" trick for positive integers: (length + width - 1) / width. Check that it also produces 0 for an empty message without needing a separate if — it does, since (0 + width - 1) / width truncates to 0 whenever width is positive.
  3. For getLines(), first handle the empty-message case directly: if message.length() == 0, return null right away.
  4. Otherwise, loop over the message in steps of width, using substring to pull out each chunk. The last chunk needs its own ending point, since it might be shorter than width.
  5. Build up the result String as you go: append a semicolon before every chunk except the first one, then append the chunk itself.

The Code

public class Sign
{
    private String message;
    private int width;

    public Sign(String message, int width)
    {
        this.message = message;
        this.width = width;
    }

    public int numberOfLines()
    {
        return (message.length() + width - 1) / width;
    }

    public String getLines()
    {
        if (message.length() == 0)
        {
            return null;
        }

        String result = "";

        for (int start = 0; start < message.length(); start += width)
        {
            int end = start + width;

            if (end > message.length())
            {
                end = message.length();
            }

            if (start > 0)
            {
                result = result + ";";
            }

            result = result + message.substring(start, end);
        }

        return result;
    }
}

Why Each Piece Matters

  • (message.length() + width - 1) / width — adding width - 1 before dividing pushes any leftover characters (a partial final line) up into an extra line, because integer division in Java always truncates. Without the + width - 1, an 8-character message on width-3 lines would compute 8 / 3 = 2, silently losing the 2 leftover characters that need a third line.
  • That same formula returns 0 for an empty message automatically(0 + width - 1) / width is always less than 1 for a positive width, truncating to 0. No separate empty-string check is needed in numberOfLines().
  • getLines() still needs its own explicit empty-string check, because an empty message legitimately produces null, not an empty String — and the loop below it would simply never execute for an empty message anyway (since 0 < message.length() is false), so without the early check the method would fall through and return "" instead of null.
  • if (end > message.length()) { end = message.length(); } — this is what allows the last line to be shorter than width. Every earlier chunk fits perfectly because the loop only continues while start < message.length().
  • if (start > 0) before appending the semicolon — this is the difference between "ABC;222;DE" and the wrong ";ABC;222;DE". The very first chunk never gets a semicolon in front of it.

Tracing the Example

Using the question's own execution sequence:

Call Result Why
new Sign("ABC222DE", 3) then numberOfLines() 3 (8 + 2) / 3 = 10 / 3 = 3
getLines() "ABC;222;DE" chunks at start 0 ("ABC"), 3 ("222"), 6 ("DE", since end clamps to 8)
new Sign("ABCD", 10) then numberOfLines() 1 (4 + 9) / 10 = 13 / 10 = 1
getLines() "ABCD" one chunk, no semicolon ever appended
new Sign("", 4) then numberOfLines() 0 (0 + 3) / 4 = 3 / 4 = 0
getLines() null the empty-message check returns immediately
new Sign("AB_CD_EF", 2) then getLines() "AB;_C;D_;EF" chunks "AB", "_C", "D_", "EF", each separated by one semicolon

Every result matches the table in the released question.

Common Mistakes to Avoid

  • Using plain division for numberOfLines() (message.length() / width) — this silently drops any partial final line, undercounting by one whenever the message doesn't divide evenly.
  • Appending a semicolon after every chunk instead of before every chunk-but-the-first. Both approaches can be made to work, but forgetting to strip the trailing semicolon at the end is a common way to fail "no semicolon should appear at the end."
  • Computing end without clamping it to message.length(). On the last (possibly short) chunk, start + width can run past the end of the message, which would throw a StringIndexOutOfBoundsException inside substring.
  • Returning "" instead of null for an empty message, or checking message == null instead of message.length() == 0 — the problem guarantees the message is a String (possibly empty), never an actual null reference.

Notes: A Method Not on the AP CSA Quick Reference Sheet

Math.ceil more directly expresses "round up" than the integer-math trick used above, if your class has covered it:

public int numberOfLines()
{
    return (int) Math.ceil((double) message.length() / width);
}
  • Math.ceil isn't listed on the AP Quick Reference sheet (only abs, pow, sqrt, and random from the Math class are) — but that doesn't make it off-limits. AP CSA graders accept any correct Java, not just methods printed on the sheet.
  • The (double) cast on message.length() is still required here, for the same reason as always: dividing two ints truncates before Math.ceil ever gets a chance to round anything.
  • This version reads more directly as "round up the division," but it depends on remembering Math.ceil's exact behavior (and the awkward double-to-int cast around it) without being able to look it up mid-exam. The + width - 1 version above is what this solution uses because it only relies on plain integer division, which is guaranteed to be there on the reference sheet.

Key Takeaways

  • "Round up" integer division without a rounding method is a reusable trick: (numerator + denominator - 1) / denominator.
  • When a class isn't scaffolded for you, decide the fields first — they should hold exactly what's needed to answer every method call, and nothing more.
  • Building a delimiter-separated String in a loop means adding the delimiter before every element except the first, not after every element.

Related FRQs