FRQ
› csapa
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.