diff --git a/paint-calculator/src/main/java/dev/delivercraft/paint/PaintCalculator.java b/paint-calculator/src/main/java/dev/delivercraft/paint/PaintCalculator.java index e47e0a7..7f3a85d 100644 --- a/paint-calculator/src/main/java/dev/delivercraft/paint/PaintCalculator.java +++ b/paint-calculator/src/main/java/dev/delivercraft/paint/PaintCalculator.java @@ -7,6 +7,14 @@ final class PaintCalculator { + private static final String SHAPE_MENU = "Select room shape: 1) Rectangular 2) Round"; + + 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 final LineReader lineReader; private final LineWriter lineWriter; @@ -17,13 +25,30 @@ final class PaintCalculator { } void calculatePaint() { - RoomDimension length = readDimension("What is the length of the room in feet? "); - RoomDimension width = readDimension("What is the width of the room in feet? "); + this.lineWriter.writeLine(SHAPE_MENU); + String shapeChoice = this.lineReader.readLine(); + + 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"); + }; - PaintEstimate estimate = PaintEstimator.estimate(new RoomDimensions(length, width)); + PaintEstimate estimate = PaintEstimator.estimate(dimensions); + + this.lineWriter.writeLine("You will need to purchase %s %s of paint to cover %s square feet.".formatted( + estimate.gallons(), estimate.gallonWord(), estimate.area())); + } + + private RectangularRoom readRectangularDimensions() { + RoomDimension length = readDimension(LENGTH_PROMPT); + RoomDimension width = readDimension(WIDTH_PROMPT); + return new RectangularRoom(length, width); + } - this.lineWriter.writeLine("You will need to purchase %s %s of paint to cover %s square feet." - .formatted(estimate.gallons(), estimate.gallonWord(), estimate.area())); + private RoundRoom readRoundDimensions() { + RoomDimension radius = readDimension(RADIUS_PROMPT); + return new RoundRoom(radius); } private RoomDimension readDimension(String prompt) { diff --git a/paint-calculator/src/main/java/dev/delivercraft/paint/RectangularRoom.java b/paint-calculator/src/main/java/dev/delivercraft/paint/RectangularRoom.java new file mode 100644 index 0000000..ee7096d --- /dev/null +++ b/paint-calculator/src/main/java/dev/delivercraft/paint/RectangularRoom.java @@ -0,0 +1,17 @@ +package dev.delivercraft.paint; + +import java.math.BigDecimal; +import java.util.Objects; + +public record RectangularRoom(RoomDimension length, RoomDimension width) implements RoomDimensions { + + public RectangularRoom(RoomDimension length, RoomDimension width) { + this.length = Objects.requireNonNull(length, "length must not be null"); + this.width = Objects.requireNonNull(width, "width must not be null"); + } + + @Override + public BigDecimal area() { + return this.length.value().multiply(this.width.value()); + } +} diff --git a/paint-calculator/src/main/java/dev/delivercraft/paint/RoomDimensions.java b/paint-calculator/src/main/java/dev/delivercraft/paint/RoomDimensions.java index e5cf289..126bb4a 100644 --- a/paint-calculator/src/main/java/dev/delivercraft/paint/RoomDimensions.java +++ b/paint-calculator/src/main/java/dev/delivercraft/paint/RoomDimensions.java @@ -1,16 +1,8 @@ package dev.delivercraft.paint; import java.math.BigDecimal; -import java.util.Objects; -record RoomDimensions(RoomDimension length, RoomDimension width) { +sealed interface RoomDimensions permits RectangularRoom, RoundRoom { - RoomDimensions { - Objects.requireNonNull(length, "length must not be null"); - Objects.requireNonNull(width, "width must not be null"); - } - - BigDecimal area() { - return this.length.value().multiply(this.width.value()); - } + BigDecimal area(); } diff --git a/paint-calculator/src/main/java/dev/delivercraft/paint/RoundRoom.java b/paint-calculator/src/main/java/dev/delivercraft/paint/RoundRoom.java new file mode 100644 index 0000000..b4add61 --- /dev/null +++ b/paint-calculator/src/main/java/dev/delivercraft/paint/RoundRoom.java @@ -0,0 +1,20 @@ +package dev.delivercraft.paint; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; + +public record RoundRoom(RoomDimension radius) implements RoomDimensions { + + public RoundRoom(RoomDimension radius) { + this.radius = Objects.requireNonNull(radius, "radius must not be null"); + } + + @Override + public BigDecimal area() { + return this.radius.value() + .multiply(this.radius.value()) + .multiply(BigDecimal.valueOf(Math.PI)) + .setScale(2, RoundingMode.HALF_UP); + } +} diff --git a/paint-calculator/src/test/java/dev/delivercraft/paint/MainTest.java b/paint-calculator/src/test/java/dev/delivercraft/paint/MainTest.java index 507dc71..de50624 100644 --- a/paint-calculator/src/test/java/dev/delivercraft/paint/MainTest.java +++ b/paint-calculator/src/test/java/dev/delivercraft/paint/MainTest.java @@ -22,7 +22,7 @@ class MainTest { void main_GivenValidInput_ShouldPrintPaintEstimate() throws IOException { InputStream originalInput = System.in; PrintStream originalOutput = System.out; - String inputText = "18%n20%n".formatted(); + String inputText = "1%n18%n20%n".formatted(); ByteArrayInputStream input = new ByteArrayInputStream(inputText.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream output = new ByteArrayOutputStream(); diff --git a/paint-calculator/src/test/java/dev/delivercraft/paint/PaintCalculatorTest.java b/paint-calculator/src/test/java/dev/delivercraft/paint/PaintCalculatorTest.java index 2cfa574..085d404 100644 --- a/paint-calculator/src/test/java/dev/delivercraft/paint/PaintCalculatorTest.java +++ b/paint-calculator/src/test/java/dev/delivercraft/paint/PaintCalculatorTest.java @@ -1,10 +1,5 @@ package dev.delivercraft.paint; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -import java.util.stream.Stream; - import dev.delivercraft.io.CapturingLineWriter; import dev.delivercraft.io.LineWriter; import dev.delivercraft.io.StubLineReader; @@ -14,6 +9,11 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + class PaintCalculatorTest { private static final String VALID_LENGTH = "18"; @@ -36,14 +36,30 @@ 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 RADIUS_PROMPT = "What is the radius of the room in feet? "; + + private static final String RECTANGULAR_SHAPE = "1"; + + private static final String TEN_FEET = "10"; + + private static final String INVALID_LENGTH_OUTPUT = + SHAPE_MENU + System.lineSeparator() + LENGTH_PROMPT; + + private static final String INVALID_WIDTH_OUTPUT = + SHAPE_MENU + System.lineSeparator() + LENGTH_PROMPT + WIDTH_PROMPT; + @Test void calculatePaint_GivenExampleDimensions_ShouldDisplayGallonsAndArea() { LineWriter lineWriter = new CapturingLineWriter(); - PaintCalculator calculator = new PaintCalculator(new StubLineReader(VALID_LENGTH, VALID_WIDTH), lineWriter); + PaintCalculator calculator = new PaintCalculator( + new StubLineReader(RECTANGULAR_SHAPE, VALID_LENGTH, VALID_WIDTH), lineWriter); calculator.calculatePaint(); - assertThat(lineWriter).hasToString(LENGTH_PROMPT + WIDTH_PROMPT + assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + + LENGTH_PROMPT + WIDTH_PROMPT + "You will need to purchase 2 gallons of paint to cover 360 square feet." + System.lineSeparator()); } @@ -51,11 +67,13 @@ void calculatePaint_GivenExampleDimensions_ShouldDisplayGallonsAndArea() { @Test void calculatePaint_GivenSmallCeiling_ShouldUseSingularGallon() { LineWriter lineWriter = new CapturingLineWriter(); - PaintCalculator calculator = new PaintCalculator(new StubLineReader("10", "10"), lineWriter); + PaintCalculator calculator = new PaintCalculator( + new StubLineReader(RECTANGULAR_SHAPE, TEN_FEET, TEN_FEET), lineWriter); calculator.calculatePaint(); - assertThat(lineWriter).hasToString(LENGTH_PROMPT + WIDTH_PROMPT + assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + + LENGTH_PROMPT + WIDTH_PROMPT + "You will need to purchase 1 gallon of paint to cover 100 square feet." + System.lineSeparator()); } @@ -63,11 +81,13 @@ void calculatePaint_GivenSmallCeiling_ShouldUseSingularGallon() { @Test void calculatePaint_GivenFractionalDimensions_ShouldDisplayExactDecimalArea() { LineWriter lineWriter = new CapturingLineWriter(); - PaintCalculator calculator = new PaintCalculator(new StubLineReader("10.5", "10.1"), lineWriter); + PaintCalculator calculator = new PaintCalculator( + new StubLineReader(RECTANGULAR_SHAPE, "10.5", "10.1"), lineWriter); calculator.calculatePaint(); - assertThat(lineWriter).hasToString(LENGTH_PROMPT + WIDTH_PROMPT + assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + + LENGTH_PROMPT + WIDTH_PROMPT + "You will need to purchase 1 gallon of paint to cover 106.05 square feet." + System.lineSeparator()); } @@ -75,20 +95,23 @@ void calculatePaint_GivenFractionalDimensions_ShouldDisplayExactDecimalArea() { @Test void calculatePaint_GivenAreaJustOverOneGallon_ShouldRoundGallonsUp() { LineWriter lineWriter = new CapturingLineWriter(); - PaintCalculator calculator = new PaintCalculator(new StubLineReader("35.1", "10"), lineWriter); + PaintCalculator calculator = new PaintCalculator( + new StubLineReader(RECTANGULAR_SHAPE, "35.1", "10"), lineWriter); calculator.calculatePaint(); - assertThat(lineWriter).hasToString(LENGTH_PROMPT + WIDTH_PROMPT + assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + + LENGTH_PROMPT + WIDTH_PROMPT + "You will need to purchase 2 gallons of paint to cover 351 square feet." + System.lineSeparator()); } @ParameterizedTest @MethodSource("invalidInputFlows") - void calculatePaint_GivenInvalidInput_ShouldFailFast(String length, String width, String expectedOutput) { + void calculatePaint_GivenInvalidInput_ShouldFailFast(String shape, String length, String width, + String expectedOutput) { LineWriter lineWriter = new CapturingLineWriter(); - PaintCalculator calculator = new PaintCalculator(new StubLineReader(length, width), lineWriter); + PaintCalculator calculator = new PaintCalculator(new StubLineReader(shape, length, width), lineWriter); assertThatIllegalArgumentException() .isThrownBy(calculator::calculatePaint); @@ -98,30 +121,67 @@ void calculatePaint_GivenInvalidInput_ShouldFailFast(String length, String width private static Stream invalidInputFlows() { return Stream.of( - Arguments.of(EMPTY_INPUT, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(null, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(NON_NUMERIC_INPUT, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(ZERO_INPUT, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(NEGATIVE_INPUT, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(SCIENTIFIC_INPUT, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(SHORT_DECIMAL, VALID_WIDTH, LENGTH_PROMPT), - Arguments.of(VALID_LENGTH, EMPTY_INPUT, LENGTH_PROMPT + WIDTH_PROMPT), - Arguments.of(VALID_LENGTH, null, LENGTH_PROMPT + WIDTH_PROMPT), - Arguments.of(VALID_LENGTH, NON_NUMERIC_INPUT, LENGTH_PROMPT + WIDTH_PROMPT), - Arguments.of(VALID_LENGTH, ZERO_INPUT, LENGTH_PROMPT + WIDTH_PROMPT), - Arguments.of(VALID_LENGTH, NEGATIVE_INPUT, LENGTH_PROMPT + WIDTH_PROMPT), - Arguments.of(VALID_LENGTH, SCIENTIFIC_INPUT, LENGTH_PROMPT + WIDTH_PROMPT), - Arguments.of(VALID_LENGTH, SHORT_DECIMAL, LENGTH_PROMPT + WIDTH_PROMPT)); + Arguments.of(RECTANGULAR_SHAPE, EMPTY_INPUT, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, null, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, NON_NUMERIC_INPUT, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, ZERO_INPUT, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, NEGATIVE_INPUT, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, SCIENTIFIC_INPUT, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, SHORT_DECIMAL, VALID_WIDTH, INVALID_LENGTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, EMPTY_INPUT, INVALID_WIDTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, null, INVALID_WIDTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, NON_NUMERIC_INPUT, INVALID_WIDTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, ZERO_INPUT, INVALID_WIDTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, NEGATIVE_INPUT, INVALID_WIDTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, SCIENTIFIC_INPUT, INVALID_WIDTH_OUTPUT), + Arguments.of(RECTANGULAR_SHAPE, VALID_LENGTH, SHORT_DECIMAL, INVALID_WIDTH_OUTPUT)); } @ParameterizedTest @ValueSource(strings = {"18", " 18 ", "18.0", "0.5"}) void calculatePaint_GivenPlainDecimalInput_ShouldAcceptInput(String input) { LineWriter lineWriter = new CapturingLineWriter(); - PaintCalculator calculator = new PaintCalculator(new StubLineReader(input, "20"), lineWriter); + PaintCalculator calculator = new PaintCalculator( + new StubLineReader(RECTANGULAR_SHAPE, input, "20"), lineWriter); calculator.calculatePaint(); assertThat(lineWriter.toString()).contains("You will need to purchase"); } + + @Test + void calculatePaint_GivenRoundRoomSelection_ShouldCalculateCorrectArea() { + LineWriter lineWriter = new CapturingLineWriter(); + PaintCalculator calculator = new PaintCalculator(new StubLineReader("2", "10"), lineWriter); + + calculator.calculatePaint(); + + assertThat(lineWriter).hasToString(SHAPE_MENU + System.lineSeparator() + + RADIUS_PROMPT + + "You will need to purchase 1 gallon of paint to cover 314.16 square feet." + + System.lineSeparator()); + } + + @ParameterizedTest + @ValueSource(strings = {"3", "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"); + assertThat(lineWriter.toString()).isEqualTo(SHAPE_MENU + System.lineSeparator()); + } + + @Test + void calculatePaint_GivenNullShapeInput_ShouldThrowIllegalArgumentException() { + LineWriter lineWriter = new CapturingLineWriter(); + PaintCalculator calculator = new PaintCalculator(() -> null, lineWriter); + + assertThatIllegalArgumentException() + .isThrownBy(calculator::calculatePaint) + .withMessage("Please enter 1 or 2"); + assertThat(lineWriter.toString()).isEqualTo(SHAPE_MENU + System.lineSeparator()); + } } diff --git a/paint-calculator/src/test/java/dev/delivercraft/paint/PaintEstimatorTest.java b/paint-calculator/src/test/java/dev/delivercraft/paint/PaintEstimatorTest.java index 56feb6d..65153f0 100644 --- a/paint-calculator/src/test/java/dev/delivercraft/paint/PaintEstimatorTest.java +++ b/paint-calculator/src/test/java/dev/delivercraft/paint/PaintEstimatorTest.java @@ -10,7 +10,7 @@ class PaintEstimatorTest { @Test void estimate_GivenExampleDimensions_ShouldCalculateAreaAndGallons() { - RoomDimensions dimensions = new RoomDimensions( + RoomDimensions dimensions = new RectangularRoom( RoomDimension.of("18"), RoomDimension.of("20")); @@ -22,7 +22,7 @@ void estimate_GivenExampleDimensions_ShouldCalculateAreaAndGallons() { @Test void estimate_GivenAreaJustOverOneGallon_ShouldRoundGallonsUp() { - RoomDimensions dimensions = new RoomDimensions( + RoomDimensions dimensions = new RectangularRoom( RoomDimension.of("35.1"), RoomDimension.of("10")); diff --git a/paint-calculator/src/test/java/dev/delivercraft/paint/RoundRoomTest.java b/paint-calculator/src/test/java/dev/delivercraft/paint/RoundRoomTest.java new file mode 100644 index 0000000..b035fae --- /dev/null +++ b/paint-calculator/src/test/java/dev/delivercraft/paint/RoundRoomTest.java @@ -0,0 +1,84 @@ +package dev.delivercraft.paint; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; + +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 RoundRoomTest { + + @Test + void area_GivenRadius10_ShouldBePiTimesRadiusSquared() { + RoundRoom room = new RoundRoom(RoomDimension.of("10")); + + BigDecimal area = room.area(); + + BigDecimal expected = BigDecimal.valueOf(Math.PI) + .multiply(BigDecimal.valueOf(100)) + .setScale(2, RoundingMode.HALF_UP); + assertThat(area).isEqualByComparingTo(expected); + } + + @Test + void area_GivenRadius1_ShouldBePi() { + RoundRoom room = new RoundRoom(RoomDimension.of("1")); + + BigDecimal area = room.area(); + + BigDecimal expected = BigDecimal.valueOf(Math.PI) + .setScale(2, RoundingMode.HALF_UP); + assertThat(area).isEqualByComparingTo(expected); + } + + @Test + void area_GivenDecimalRadius_ShouldCalculateCorrectly() { + RoundRoom room = new RoundRoom(RoomDimension.of("5.5")); + + BigDecimal area = room.area(); + + BigDecimal expected = BigDecimal.valueOf(Math.PI) + .multiply(BigDecimal.valueOf(5.5)) + .multiply(BigDecimal.valueOf(5.5)) + .setScale(2, RoundingMode.HALF_UP); + assertThat(area).isEqualByComparingTo(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"0", "0.0", "-1", "-5"}) + void construction_GivenZeroOrNegativeRadius_ShouldThrow(String input) { + assertThatIllegalArgumentException() + .isThrownBy(() -> new RoundRoom(RoomDimension.of(input))); + } + + @Test + void construction_GivenNullRadius_ShouldThrowNullPointerException() { + assertThatThrownBy(() -> new RoundRoom(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void estimate_GivenRoundRoomRadius10_ShouldCalculateOneGallon() { + RoomDimensions room = new RoundRoom(RoomDimension.of("10")); + + PaintEstimate estimate = PaintEstimator.estimate(room); + + assertThat(estimate.gallons()).isEqualByComparingTo(BigInteger.ONE); + assertThat(estimate.area()).isEqualTo("314.16"); + } + + @Test + void estimate_GivenRoundRoomRadius15_ShouldCalculateThreeGallons() { + RoomDimensions room = new RoundRoom(RoomDimension.of("15")); + + PaintEstimate estimate = PaintEstimator.estimate(room); + + assertThat(estimate.gallons()).isEqualByComparingTo(BigInteger.valueOf(3)); + } +}