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
Stringpiece by piece inside a loop - Official category: "Classes" — always FRQ 2 on the AP CSA exam
The Setup
Signneeds a constructor:Sign(String message, int width)—widthis the positive maximum number of characters per line.- The message is split into lines of exactly
widthcharacters 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 needsString getLines()— the message broken into lines, joined with semicolons (;), with no semicolon at the start, end, or doubled up; returnsnullfor an empty message
Building the Sign Class
The Rule, Broken Down
- The class needs to remember the message and the width — there's nothing else given to work with, so these become the two fields.
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), not8 / 3 = 2.getLines()walks the message inwidth-sized chunks, and glues each chunk onto the result with a semicolon between chunks — never before the first one or after the last one.- An empty message is a special case: zero lines, and
getLines()returnsnullrather than an emptyString.
Step-by-Step Approach
- Declare two
privatefields,messageandwidth, and set them in the constructor. - For
numberOfLines(), use the standard "ceiling division" trick for positive integers:(length + width - 1) / width. Check that it also produces0for an empty message without needing a separateif— it does, since(0 + width - 1) / widthtruncates to0wheneverwidthis positive. - For
getLines(), first handle the empty-message case directly: ifmessage.length() == 0, returnnullright away. - Otherwise, loop over the message in steps of
width, usingsubstringto pull out each chunk. The last chunk needs its own ending point, since it might be shorter thanwidth. - Build up the result
Stringas 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— addingwidth - 1before 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 compute8 / 3 = 2, silently losing the 2 leftover characters that need a third line.- That same formula returns
0for an empty message automatically —(0 + width - 1) / widthis always less than 1 for a positivewidth, truncating to0. No separate empty-string check is needed innumberOfLines(). getLines()still needs its own explicit empty-string check, because an empty message legitimately producesnull, not an emptyString— and the loop below it would simply never execute for an empty message anyway (since0 < message.length()is false), so without the early check the method would fall through and return""instead ofnull.if (end > message.length()) { end = message.length(); }— this is what allows the last line to be shorter thanwidth. Every earlier chunk fits perfectly because the loop only continues whilestart < 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
endwithout clamping it tomessage.length(). On the last (possibly short) chunk,start + widthcan run past the end of the message, which would throw aStringIndexOutOfBoundsExceptioninsidesubstring. - Returning
""instead ofnullfor an empty message, or checkingmessage == nullinstead ofmessage.length() == 0— the problem guarantees the message is aString(possibly empty), never an actualnullreference.
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.ceilisn't listed on the AP Quick Reference sheet (onlyabs,pow,sqrt, andrandomfrom theMathclass 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 onmessage.length()is still required here, for the same reason as always: dividing twoints truncates beforeMath.ceilever 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 awkwarddouble-to-intcast around it) without being able to look it up mid-exam. The+ width - 1version 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
Stringin a loop means adding the delimiter before every element except the first, not after every element.
Need help preparing for the AP exam?
FRQs are one of the toughest parts of the AP CS exam. I offer 1-on-1 tutoring to help you work through practice problems, tighten up your responses, and build the confidence to earn full credit on exam day.
Book a tutoring session →