Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.delivercraft.paint;

import java.math.BigDecimal;
import java.util.Objects;

public record LShapedRoom(RectangularRoom outer, RectangularRoom cutout) implements RoomDimensions {

public LShapedRoom(RectangularRoom outer, RectangularRoom cutout) {
this.outer = Objects.requireNonNull(outer, "outer must not be null");
this.cutout = Objects.requireNonNull(cutout, "cutout must not be null");

BigDecimal outerLength = outer.length().value();
BigDecimal outerWidth = outer.width().value();
BigDecimal cutoutLength = cutout.length().value();
BigDecimal cutoutWidth = cutout.width().value();

if (cutoutLength.compareTo(outerLength) >= 0 || cutoutWidth.compareTo(outerWidth) >= 0) {
throw new IllegalArgumentException(
"Cut-out must be smaller than the outer rectangle on both length and width.");
}
}

@Override
public BigDecimal area() {
return this.outer.area().subtract(this.cutout.area());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@

final class PaintCalculator {

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

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

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

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

private static final String OUTER_LENGTH_PROMPT = "What is the length of the outer rectangle in feet? ";

private static final String OUTER_WIDTH_PROMPT = "What is the width of the outer rectangle in feet? ";

private static final String CUTOUT_LENGTH_PROMPT = "What is the length of the cut-out in feet? ";

private static final String CUTOUT_WIDTH_PROMPT = "What is the width of the cut-out in feet? ";

private final LineReader lineReader;

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

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

private LShapedRoom readLShapedDimensions() {
RoomDimension outerLength = readDimension(OUTER_LENGTH_PROMPT);
RoomDimension outerWidth = readDimension(OUTER_WIDTH_PROMPT);
RoomDimension cutoutLength = readDimension(CUTOUT_LENGTH_PROMPT);
RoomDimension cutoutWidth = readDimension(CUTOUT_WIDTH_PROMPT);
return new LShapedRoom(
new RectangularRoom(outerLength, outerWidth),
new RectangularRoom(cutoutLength, cutoutWidth));
}

private RoomDimension readDimension(String prompt) {
this.lineWriter.write(prompt);
return RoomDimension.of(this.lineReader.readLine());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.math.BigDecimal;

sealed interface RoomDimensions permits RectangularRoom, RoundRoom {
sealed interface RoomDimensions permits RectangularRoom, RoundRoom, LShapedRoom {

BigDecimal area();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package dev.delivercraft.paint;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.math.BigInteger;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class LShapedRoomTest {

private static final RectangularRoom ANY_OUTER =
new RectangularRoom(RoomDimension.of("20"), RoomDimension.of("15"));

private static final RectangularRoom ANY_CUTOUT =
new RectangularRoom(RoomDimension.of("5"), RoomDimension.of("4"));

private static final String CUTOUT_TOO_BIG_MSG =
"Cut-out must be smaller than the outer rectangle on both length and width.";

@Test
void construction_GivenNullOuter_ShouldThrowNullPointerException() {
assertThatThrownBy(() -> new LShapedRoom(null, ANY_CUTOUT))
.isInstanceOf(NullPointerException.class)
.hasMessage("outer must not be null");
}

@Test
void construction_GivenNullCutout_ShouldThrowNullPointerException() {
assertThatThrownBy(() -> new LShapedRoom(ANY_OUTER, null))
.isInstanceOf(NullPointerException.class)
.hasMessage("cutout must not be null");
}

@Test
void area_GivenOuter20x15AndCutout5x4_ShouldReturn280() {
LShapedRoom room = new LShapedRoom(ANY_OUTER, ANY_CUTOUT);

assertThat(room.area()).isEqualByComparingTo(new BigDecimal("280"));
}

@Test
void area_GivenFractionalDimensions_ShouldComputeExactly() {
LShapedRoom room = new LShapedRoom(
new RectangularRoom(RoomDimension.of("10.5"), RoomDimension.of("10.1")),
new RectangularRoom(RoomDimension.of("2.5"), RoomDimension.of("2")));

// 10.5 * 10.1 = 106.05 ; 2.5 * 2 = 5.0 ; diff = 101.05
assertThat(room.area()).isEqualByComparingTo(new BigDecimal("101.05"));
}

@Test
void construction_GivenCutoutEqualOnLength_ShouldThrow() {
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("20"), RoomDimension.of("4"));

assertThatIllegalArgumentException()
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
.withMessage(CUTOUT_TOO_BIG_MSG);
}

@Test
void construction_GivenCutoutEqualOnWidth_ShouldThrow() {
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("5"), RoomDimension.of("15"));

assertThatIllegalArgumentException()
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
.withMessage(CUTOUT_TOO_BIG_MSG);
}

@Test
void construction_GivenCutoutEqualOnBothAxes_ShouldThrow() {
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("20"), RoomDimension.of("15"));

assertThatIllegalArgumentException()
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
.withMessage(CUTOUT_TOO_BIG_MSG);
}

@Test
void construction_GivenCutoutLargerOnLength_ShouldThrow() {
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("21"), RoomDimension.of("4"));

assertThatIllegalArgumentException()
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
.withMessage(CUTOUT_TOO_BIG_MSG);
}

@Test
void construction_GivenCutoutLargerOnWidth_ShouldThrow() {
RectangularRoom cutout = new RectangularRoom(RoomDimension.of("5"), RoomDimension.of("16"));

assertThatIllegalArgumentException()
.isThrownBy(() -> new LShapedRoom(ANY_OUTER, cutout))
.withMessage(CUTOUT_TOO_BIG_MSG);
}

@Test
void estimate_GivenLShapedRoom_ShouldRoundGallonsUp() {
// outer 20*15 = 300 ; cutout 5*4 = 20 ; area = 280 ; 280/350 -> ceil 1
PaintEstimate estimate = PaintEstimator.estimate(new LShapedRoom(ANY_OUTER, ANY_CUTOUT));

assertThat(estimate.area()).isEqualTo("280");
assertThat(estimate.gallons()).isEqualTo(BigInteger.ONE);
}

@Test
void estimate_GivenLargeLShapedRoom_ShouldRequireMultipleGallons() {
// outer 50*30 = 1500 ; cutout 10*10 = 100 ; area = 1400 ; 1400/350 = 4 exact
RoomDimensions room = new LShapedRoom(
new RectangularRoom(RoomDimension.of("50"), RoomDimension.of("30")),
new RectangularRoom(RoomDimension.of("10"), RoomDimension.of("10")));

PaintEstimate estimate = PaintEstimator.estimate(room);

assertThat(estimate.area()).isEqualTo("1400");
assertThat(estimate.gallons()).isEqualTo(BigInteger.valueOf(4));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,20 @@ class PaintCalculatorTest {

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

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

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

private static final String OUTER_LENGTH_PROMPT = "What is the length of the outer rectangle in feet? ";

private static final String OUTER_WIDTH_PROMPT = "What is the width of the outer rectangle in feet? ";

private static final String CUTOUT_LENGTH_PROMPT = "What is the length of the cut-out in feet? ";

private static final String CUTOUT_WIDTH_PROMPT = "What is the width of the cut-out in feet? ";

private static final String L_SHAPED_SHAPE = "3";

private static final String RECTANGULAR_SHAPE = "1";

private static final String TEN_FEET = "10";
Expand Down Expand Up @@ -142,7 +152,7 @@ private static Stream<Arguments> invalidInputFlows() {
void calculatePaint_GivenPlainDecimalInput_ShouldAcceptInput(String input) {
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(
new StubLineReader(RECTANGULAR_SHAPE, input, "20"), lineWriter);
new StubLineReader(RECTANGULAR_SHAPE, input, VALID_WIDTH), lineWriter);

calculator.calculatePaint();

Expand All @@ -162,15 +172,72 @@ void calculatePaint_GivenRoundRoomSelection_ShouldCalculateCorrectArea() {
+ System.lineSeparator());
}

@Test
void calculatePaint_GivenLShapedRoomSelection_ShouldCalculateCorrectArea() {
// outer 20*15 = 300 ; cutout 5*4 = 20 ; area = 280 ; 280/350 -> ceil 1
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(
new StubLineReader(L_SHAPED_SHAPE, VALID_WIDTH, "15", "5", "4"), lineWriter);

calculator.calculatePaint();

assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator()
+ OUTER_LENGTH_PROMPT + OUTER_WIDTH_PROMPT
+ CUTOUT_LENGTH_PROMPT + CUTOUT_WIDTH_PROMPT
+ "You will need to purchase 1 gallon of paint to cover 280 square feet."
+ System.lineSeparator());
}

@Test
void calculatePaint_GivenLShapedRoomWithInvalidOuterLength_ShouldFailFastBeforePromptingWidth() {
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(
new StubLineReader(L_SHAPED_SHAPE, NON_NUMERIC_INPUT), lineWriter);

assertThatIllegalArgumentException()
.isThrownBy(calculator::calculatePaint);

assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + OUTER_LENGTH_PROMPT);
}

@Test
void calculatePaint_GivenLShapedRoomWithInvalidCutoutWidth_ShouldFailFastAfterAllPriorPrompts() {
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(
new StubLineReader(L_SHAPED_SHAPE, VALID_WIDTH, "15", "5", NEGATIVE_INPUT), lineWriter);

assertThatIllegalArgumentException()
.isThrownBy(calculator::calculatePaint);

assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator()
+ OUTER_LENGTH_PROMPT + OUTER_WIDTH_PROMPT
+ CUTOUT_LENGTH_PROMPT + CUTOUT_WIDTH_PROMPT);
}

@Test
void calculatePaint_GivenCutoutNotSmallerThanOuter_ShouldThrowWithDescriptiveMessage() {
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(
new StubLineReader(L_SHAPED_SHAPE, VALID_WIDTH, "15", VALID_WIDTH, "4"), lineWriter);

assertThatIllegalArgumentException()
.isThrownBy(calculator::calculatePaint)
.withMessage("Cut-out must be smaller than the outer rectangle on both length and width.");

assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator()
+ OUTER_LENGTH_PROMPT + OUTER_WIDTH_PROMPT
+ CUTOUT_LENGTH_PROMPT + CUTOUT_WIDTH_PROMPT);
}

@ParameterizedTest
@ValueSource(strings = {"3", "0", "abc", "", " "})
@ValueSource(strings = {"4", "0", "abc", "", " "})
void calculatePaint_GivenInvalidShapeInput_ShouldThrowIllegalArgumentException(String invalidInput) {
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(new StubLineReader(invalidInput), lineWriter);

assertThatIllegalArgumentException()
.isThrownBy(calculator::calculatePaint)
.withMessage("Please enter 1 or 2");
.withMessage("Please enter 1, 2 or 3");
assertThat(lineWriter.toString()).isEqualTo(SHAPE_MENU + System.lineSeparator());
}

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

assertThatIllegalArgumentException()
.isThrownBy(calculator::calculatePaint)
.withMessage("Please enter 1 or 2");
.withMessage("Please enter 1, 2 or 3");
assertThat(lineWriter.toString()).isEqualTo(SHAPE_MENU + System.lineSeparator());
}
}