Skip to content

Commit e4f0e8e

Browse files
committed
Add L-shaped room support to paint calculator
Models an L-shaped room as an outer rectangle with a rectangular cut-out removed from one corner. Area = outer.area() minus cutout.area(). The cut-out must be strictly smaller than the outer rectangle on both length and width; equal dimensions are also rejected. PaintCalculator gains a "3) L-shaped" menu option with four sequential prompts (outer length/width, cut-out length/width). The invalid-shape error is updated from "Please enter 1 or 2" to "Please enter 1, 2 or 3".
1 parent 69fc874 commit e4f0e8e

5 files changed

Lines changed: 241 additions & 8 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.delivercraft.paint;
2+
3+
import java.math.BigDecimal;
4+
import java.util.Objects;
5+
6+
public record LShapedRoom(RectangularRoom outer, RectangularRoom cutout) implements RoomDimensions {
7+
8+
public LShapedRoom(RectangularRoom outer, RectangularRoom cutout) {
9+
this.outer = Objects.requireNonNull(outer, "outer must not be null");
10+
this.cutout = Objects.requireNonNull(cutout, "cutout must not be null");
11+
12+
BigDecimal outerLength = outer.length().value();
13+
BigDecimal outerWidth = outer.width().value();
14+
BigDecimal cutoutLength = cutout.length().value();
15+
BigDecimal cutoutWidth = cutout.width().value();
16+
17+
if (cutoutLength.compareTo(outerLength) >= 0 || cutoutWidth.compareTo(outerWidth) >= 0) {
18+
throw new IllegalArgumentException(
19+
"Cut-out must be smaller than the outer rectangle on both length and width.");
20+
}
21+
}
22+
23+
@Override
24+
public BigDecimal area() {
25+
return this.outer.area().subtract(this.cutout.area());
26+
}
27+
}

paint-calculator/src/main/java/dev/delivercraft/paint/PaintCalculator.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@
77

88
final class PaintCalculator {
99

10-
private static final String SHAPE_MENU = "Select room shape: 1) Rectangular 2) Round";
10+
private static final String SHAPE_MENU = "Select room shape: 1) Rectangular 2) Round 3) L-shaped";
1111

1212
private static final String LENGTH_PROMPT = "What is the length of the room in feet? ";
1313

1414
private static final String WIDTH_PROMPT = "What is the width of the room in feet? ";
1515

1616
private static final String RADIUS_PROMPT = "What is the radius of the room in feet? ";
1717

18+
private static final String OUTER_LENGTH_PROMPT = "What is the length of the outer rectangle in feet? ";
19+
20+
private static final String OUTER_WIDTH_PROMPT = "What is the width of the outer rectangle in feet? ";
21+
22+
private static final String CUTOUT_LENGTH_PROMPT = "What is the length of the cut-out in feet? ";
23+
24+
private static final String CUTOUT_WIDTH_PROMPT = "What is the width of the cut-out in feet? ";
25+
1826
private final LineReader lineReader;
1927

2028
private final LineWriter lineWriter;
@@ -31,7 +39,8 @@ void calculatePaint() {
3139
RoomDimensions dimensions = switch (shapeChoice != null ? shapeChoice.trim() : null) {
3240
case "1" -> readRectangularDimensions();
3341
case "2" -> readRoundDimensions();
34-
case null, default -> throw new IllegalArgumentException("Please enter 1 or 2");
42+
case "3" -> readLShapedDimensions();
43+
case null, default -> throw new IllegalArgumentException("Please enter 1, 2 or 3");
3544
};
3645

3746
PaintEstimate estimate = PaintEstimator.estimate(dimensions);
@@ -51,6 +60,16 @@ private RoundRoom readRoundDimensions() {
5160
return new RoundRoom(radius);
5261
}
5362

63+
private LShapedRoom readLShapedDimensions() {
64+
RoomDimension outerLength = readDimension(OUTER_LENGTH_PROMPT);
65+
RoomDimension outerWidth = readDimension(OUTER_WIDTH_PROMPT);
66+
RoomDimension cutoutLength = readDimension(CUTOUT_LENGTH_PROMPT);
67+
RoomDimension cutoutWidth = readDimension(CUTOUT_WIDTH_PROMPT);
68+
return new LShapedRoom(
69+
new RectangularRoom(outerLength, outerWidth),
70+
new RectangularRoom(cutoutLength, cutoutWidth));
71+
}
72+
5473
private RoomDimension readDimension(String prompt) {
5574
this.lineWriter.write(prompt);
5675
return RoomDimension.of(this.lineReader.readLine());

paint-calculator/src/main/java/dev/delivercraft/paint/RoomDimensions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.math.BigDecimal;
44

5-
sealed interface RoomDimensions permits RectangularRoom, RoundRoom {
5+
sealed interface RoomDimensions permits RectangularRoom, RoundRoom, LShapedRoom {
66

77
BigDecimal area();
88
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package dev.delivercraft.paint;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.math.BigDecimal;
6+
import java.math.BigInteger;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
10+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
11+
12+
class LShapedRoomTest {
13+
14+
private static final RectangularRoom ANY_OUTER =
15+
new RectangularRoom(RoomDimension.of("20"), RoomDimension.of("15"));
16+
17+
private static final RectangularRoom ANY_CUTOUT =
18+
new RectangularRoom(RoomDimension.of("5"), RoomDimension.of("4"));
19+
20+
private static final String CUTOUT_TOO_BIG_MSG =
21+
"Cut-out must be smaller than the outer rectangle on both length and width.";
22+
23+
@Test
24+
void construction_GivenNullOuter_ShouldThrowNullPointerException() {
25+
assertThatThrownBy(() -> new LShapedRoom(null, ANY_CUTOUT))
26+
.isInstanceOf(NullPointerException.class)
27+
.hasMessage("outer must not be null");
28+
}
29+
30+
@Test
31+
void construction_GivenNullCutout_ShouldThrowNullPointerException() {
32+
assertThatThrownBy(() -> new LShapedRoom(ANY_OUTER, null))
33+
.isInstanceOf(NullPointerException.class)
34+
.hasMessage("cutout must not be null");
35+
}
36+
37+
@Test
38+
void area_GivenOuter20x15AndCutout5x4_ShouldReturn280() {
39+
LShapedRoom room = new LShapedRoom(ANY_OUTER, ANY_CUTOUT);
40+
41+
assertThat(room.area()).isEqualByComparingTo(new BigDecimal("280"));
42+
}
43+
44+
@Test
45+
void area_GivenFractionalDimensions_ShouldComputeExactly() {
46+
LShapedRoom room = new LShapedRoom(
47+
new RectangularRoom(RoomDimension.of("10.5"), RoomDimension.of("10.1")),
48+
new RectangularRoom(RoomDimension.of("2.5"), RoomDimension.of("2")));
49+
50+
// 10.5 * 10.1 = 106.05 ; 2.5 * 2 = 5.0 ; diff = 101.05
51+
assertThat(room.area()).isEqualByComparingTo(new BigDecimal("101.05"));
52+
}
53+
54+
@Test
55+
void construction_GivenCutoutEqualOnLength_ShouldThrow() {
56+
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("20"), RoomDimension.of("4"));
57+
58+
assertThatIllegalArgumentException()
59+
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
60+
.withMessage(CUTOUT_TOO_BIG_MSG);
61+
}
62+
63+
@Test
64+
void construction_GivenCutoutEqualOnWidth_ShouldThrow() {
65+
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("5"), RoomDimension.of("15"));
66+
67+
assertThatIllegalArgumentException()
68+
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
69+
.withMessage(CUTOUT_TOO_BIG_MSG);
70+
}
71+
72+
@Test
73+
void construction_GivenCutoutEqualOnBothAxes_ShouldThrow() {
74+
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("20"), RoomDimension.of("15"));
75+
76+
assertThatIllegalArgumentException()
77+
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
78+
.withMessage(CUTOUT_TOO_BIG_MSG);
79+
}
80+
81+
@Test
82+
void construction_GivenCutoutLargerOnLength_ShouldThrow() {
83+
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("21"), RoomDimension.of("4"));
84+
85+
assertThatIllegalArgumentException()
86+
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
87+
.withMessage(CUTOUT_TOO_BIG_MSG);
88+
}
89+
90+
@Test
91+
void construction_GivenCutoutLargerOnWidth_ShouldThrow() {
92+
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("5"), RoomDimension.of("16"));
93+
94+
assertThatIllegalArgumentException()
95+
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
96+
.withMessage(CUTOUT_TOO_BIG_MSG);
97+
}
98+
99+
@Test
100+
void estimate_GivenLShapedRoom_ShouldRoundGallonsUp() {
101+
// outer 20*15 = 300 ; cutout 5*4 = 20 ; area = 280 ; 280/350 -> ceil 1
102+
PaintEstimate estimate = PaintEstimator.estimate(new LShapedRoom(ANY_OUTER, ANY_CUTOUT));
103+
104+
assertThat(estimate.area()).isEqualTo("280");
105+
assertThat(estimate.gallons()).isEqualTo(BigInteger.ONE);
106+
}
107+
108+
@Test
109+
void estimate_GivenLargeLShapedRoom_ShouldRequireMultipleGallons() {
110+
// outer 50*30 = 1500 ; cutout 10*10 = 100 ; area = 1400 ; 1400/350 = 4 exact
111+
RoomDimensions room = new LShapedRoom(
112+
new RectangularRoom(RoomDimension.of("50"), RoomDimension.of("30")),
113+
new RectangularRoom(RoomDimension.of("10"), RoomDimension.of("10")));
114+
115+
PaintEstimate estimate = PaintEstimator.estimate(room);
116+
117+
assertThat(estimate.area()).isEqualTo("1400");
118+
assertThat(estimate.gallons()).isEqualTo(BigInteger.valueOf(4));
119+
}
120+
}

paint-calculator/src/test/java/dev/delivercraft/paint/PaintCalculatorTest.java

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,20 @@ class PaintCalculatorTest {
3636

3737
private static final String WIDTH_PROMPT = "What is the width of the room in feet? ";
3838

39-
private static final String SHAPE_MENU = "Select room shape: 1) Rectangular 2) Round";
39+
private static final String SHAPE_MENU = "Select room shape: 1) Rectangular 2) Round 3) L-shaped";
4040

4141
private static final String RADIUS_PROMPT = "What is the radius of the room in feet? ";
4242

43+
private static final String OUTER_LENGTH_PROMPT = "What is the length of the outer rectangle in feet? ";
44+
45+
private static final String OUTER_WIDTH_PROMPT = "What is the width of the outer rectangle in feet? ";
46+
47+
private static final String CUTOUT_LENGTH_PROMPT = "What is the length of the cut-out in feet? ";
48+
49+
private static final String CUTOUT_WIDTH_PROMPT = "What is the width of the cut-out in feet? ";
50+
51+
private static final String L_SHAPED_SHAPE = "3";
52+
4353
private static final String RECTANGULAR_SHAPE = "1";
4454

4555
private static final String TEN_FEET = "10";
@@ -142,7 +152,7 @@ private static Stream<Arguments> invalidInputFlows() {
142152
void calculatePaint_GivenPlainDecimalInput_ShouldAcceptInput(String input) {
143153
LineWriter lineWriter = new CapturingLineWriter();
144154
PaintCalculator calculator = new PaintCalculator(
145-
new StubLineReader(RECTANGULAR_SHAPE, input, "20"), lineWriter);
155+
new StubLineReader(RECTANGULAR_SHAPE, input, VALID_WIDTH), lineWriter);
146156

147157
calculator.calculatePaint();
148158

@@ -162,15 +172,72 @@ void calculatePaint_GivenRoundRoomSelection_ShouldCalculateCorrectArea() {
162172
+ System.lineSeparator());
163173
}
164174

175+
@Test
176+
void calculatePaint_GivenLShapedRoomSelection_ShouldCalculateCorrectArea() {
177+
// outer 20*15 = 300 ; cutout 5*4 = 20 ; area = 280 ; 280/350 -> ceil 1
178+
LineWriter lineWriter = new CapturingLineWriter();
179+
PaintCalculator calculator = new PaintCalculator(
180+
new StubLineReader(L_SHAPED_SHAPE, VALID_WIDTH, "15", "5", "4"), lineWriter);
181+
182+
calculator.calculatePaint();
183+
184+
assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator()
185+
+ OUTER_LENGTH_PROMPT + OUTER_WIDTH_PROMPT
186+
+ CUTOUT_LENGTH_PROMPT + CUTOUT_WIDTH_PROMPT
187+
+ "You will need to purchase 1 gallon of paint to cover 280 square feet."
188+
+ System.lineSeparator());
189+
}
190+
191+
@Test
192+
void calculatePaint_GivenLShapedRoomWithInvalidOuterLength_ShouldFailFastBeforePromptingWidth() {
193+
LineWriter lineWriter = new CapturingLineWriter();
194+
PaintCalculator calculator = new PaintCalculator(
195+
new StubLineReader(L_SHAPED_SHAPE, NON_NUMERIC_INPUT), lineWriter);
196+
197+
assertThatIllegalArgumentException()
198+
.isThrownBy(calculator::calculatePaint);
199+
200+
assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + OUTER_LENGTH_PROMPT);
201+
}
202+
203+
@Test
204+
void calculatePaint_GivenLShapedRoomWithInvalidCutoutWidth_ShouldFailFastAfterAllPriorPrompts() {
205+
LineWriter lineWriter = new CapturingLineWriter();
206+
PaintCalculator calculator = new PaintCalculator(
207+
new StubLineReader(L_SHAPED_SHAPE, VALID_WIDTH, "15", "5", NEGATIVE_INPUT), lineWriter);
208+
209+
assertThatIllegalArgumentException()
210+
.isThrownBy(calculator::calculatePaint);
211+
212+
assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator()
213+
+ OUTER_LENGTH_PROMPT + OUTER_WIDTH_PROMPT
214+
+ CUTOUT_LENGTH_PROMPT + CUTOUT_WIDTH_PROMPT);
215+
}
216+
217+
@Test
218+
void calculatePaint_GivenCutoutNotSmallerThanOuter_ShouldThrowWithDescriptiveMessage() {
219+
LineWriter lineWriter = new CapturingLineWriter();
220+
PaintCalculator calculator = new PaintCalculator(
221+
new StubLineReader(L_SHAPED_SHAPE, VALID_WIDTH, "15", VALID_WIDTH, "4"), lineWriter);
222+
223+
assertThatIllegalArgumentException()
224+
.isThrownBy(calculator::calculatePaint)
225+
.withMessage("Cut-out must be smaller than the outer rectangle on both length and width.");
226+
227+
assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator()
228+
+ OUTER_LENGTH_PROMPT + OUTER_WIDTH_PROMPT
229+
+ CUTOUT_LENGTH_PROMPT + CUTOUT_WIDTH_PROMPT);
230+
}
231+
165232
@ParameterizedTest
166-
@ValueSource(strings = {"3", "0", "abc", "", " "})
233+
@ValueSource(strings = {"4", "0", "abc", "", " "})
167234
void calculatePaint_GivenInvalidShapeInput_ShouldThrowIllegalArgumentException(String invalidInput) {
168235
LineWriter lineWriter = new CapturingLineWriter();
169236
PaintCalculator calculator = new PaintCalculator(new StubLineReader(invalidInput), lineWriter);
170237

171238
assertThatIllegalArgumentException()
172239
.isThrownBy(calculator::calculatePaint)
173-
.withMessage("Please enter 1 or 2");
240+
.withMessage("Please enter 1, 2 or 3");
174241
assertThat(lineWriter.toString()).isEqualTo(SHAPE_MENU + System.lineSeparator());
175242
}
176243

@@ -181,7 +248,7 @@ void calculatePaint_GivenNullShapeInput_ShouldThrowIllegalArgumentException() {
181248

182249
assertThatIllegalArgumentException()
183250
.isThrownBy(calculator::calculatePaint)
184-
.withMessage("Please enter 1 or 2");
251+
.withMessage("Please enter 1, 2 or 3");
185252
assertThat(lineWriter.toString()).isEqualTo(SHAPE_MENU + System.lineSeparator());
186253
}
187254
}

0 commit comments

Comments
 (0)