Skip to content
Closed
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
Expand Up @@ -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;
Expand All @@ -17,17 +25,32 @@ 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();

PaintEstimate estimate = PaintEstimator.estimate(new RoomDimensions(length, width));
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(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 RoomDimension readDimension(String prompt) {
this.lineWriter.write(prompt);
return RoomDimension.of(this.lineReader.readLine());
private RectangularRoom readRectangularDimensions() {
this.lineWriter.write(LENGTH_PROMPT);
RoomDimension length = RoomDimension.of(this.lineReader.readLine());
this.lineWriter.write(WIDTH_PROMPT);
RoomDimension width = RoomDimension.of(this.lineReader.readLine());
return new RectangularRoom(length, width);
}

private RoundRoom readRoundDimensions() {
this.lineWriter.write(RADIUS_PROMPT);
RoomDimension radius = RoomDimension.of(this.lineReader.readLine());
return new RoundRoom(radius);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.delivercraft.paint;

import java.math.BigDecimal;
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,92 +36,128 @@ 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());
}

@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());
}

@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());
}

@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 len, String width, String expOut) {
LineWriter lineWriter = new CapturingLineWriter();
PaintCalculator calculator = new PaintCalculator(new StubLineReader(length, width), lineWriter);
PaintCalculator calculator = new PaintCalculator(new StubLineReader(shape, len, width), lineWriter);

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

assertThat(lineWriter).hasToString(expectedOutput);
assertThat(lineWriter).hasToString(expOut);
}

private static Stream<Arguments> 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();

String output = lineWriter.toString();
assertThat(output).startsWith(SHAPE_MENU + System.lineSeparator() + RADIUS_PROMPT);
assertThat(output).contains("You will need to purchase 1 gallon of paint to cover");
}
Comment on lines +152 to +162
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add test coverage for invalid shape selection.

The new shape-selection logic correctly throws IllegalArgumentException for invalid input, but there's no explicit test verifying this behavior. Consider adding a test case to document expected behavior and prevent regressions.

🧪 Suggested test case
`@ParameterizedTest`
`@ValueSource`(strings = {"3", "0", "abc", "", "  "})
void calculatePaint_GivenInvalidShapeSelection_ShouldThrowException(String invalidShape) {
    LineWriter lineWriter = new CapturingLineWriter();
    PaintCalculator calculator = new PaintCalculator(
            new StubLineReader(invalidShape), lineWriter);

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

`@Test`
void calculatePaint_GivenNullShapeSelection_ShouldThrowException() {
    LineWriter lineWriter = new CapturingLineWriter();
    PaintCalculator calculator = new PaintCalculator(
            new StubLineReader((String) null), lineWriter);

    assertThatIllegalArgumentException()
            .isThrownBy(calculator::calculatePaint)
            .withMessage("Please enter 1 or 2");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@paint-calculator/src/test/java/dev/delivercraft/paint/PaintCalculatorTest.java`
around lines 152 - 162, Add unit tests that assert
PaintCalculator.calculatePaint throws IllegalArgumentException for invalid shape
input: create tests using StubLineReader and CapturingLineWriter to pass values
like "3","0","abc","", "  " (use a `@ParameterizedTest` with `@ValueSource`) and a
separate test for a null input, then
assertThatIllegalArgumentException().isThrownBy(calculator::calculatePaint).withMessage("Please
enter 1 or 2"); also assert the CapturingLineWriter contains only SHAPE_MENU +
System.lineSeparator() before the exception is thrown to verify prompt output.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand All @@ -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"));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package dev.delivercraft.paint;

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

import java.math.BigDecimal;
import java.math.BigInteger;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class RoundRoomTest {

@Nested
class AreaCalculation {

@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));
assertThat(area).isEqualByComparingTo(expected.stripTrailingZeros());
}

@Test
void area_GivenRadius1_ShouldBePi() {
RoundRoom room = new RoundRoom(RoomDimension.of("1"));

BigDecimal area = room.area();

assertThat(area).isEqualByComparingTo(BigDecimal.valueOf(Math.PI));
}

@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));
assertThat(area).isEqualByComparingTo(expected.stripTrailingZeros());
}
}

@Nested
class Validation {

@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);
}
}

@Nested
class PaintEstimation {

@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.1592653589793");
}

@Test
void estimate_GivenRoundRoomRadius15_ShouldCalculateThreeGallons() {
RoomDimensions room = new RoundRoom(RoomDimension.of("15"));

PaintEstimate estimate = PaintEstimator.estimate(room);

assertThat(estimate.gallons()).isEqualByComparingTo(BigInteger.valueOf(3));
}
}
}