diff --git a/README.md b/README.md index bd90ef0..076c379 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse + +## Feature Details +- CLI I/O +- 구분자 추출 +- 수식 계산 + +## Code Flow +- 수식 입력 +- (커스텀 구분자가 있다면) 커스텀 구분자 추출 +- 수식 구성 +- 수식 계산 + +## Design Pattern +- Hexagonal Architecture 도입 diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580f..1045911 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,13 @@ package calculator; +import calculator.adapter.in.InputTerminal; +import calculator.adapter.out.OutputTerminal; +import calculator.application.service.CalculatorService; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + CalculatorService calculatorService = new CalculatorService(new OutputTerminal(), new InputTerminal()); + calculatorService.run(); } } diff --git a/src/main/java/calculator/adapter/in/InputTerminal.java b/src/main/java/calculator/adapter/in/InputTerminal.java new file mode 100644 index 0000000..1f6a0a0 --- /dev/null +++ b/src/main/java/calculator/adapter/in/InputTerminal.java @@ -0,0 +1,10 @@ +package calculator.adapter.in; + +import calculator.application.port.inport.GetExpressionUserCase; +import camp.nextstep.edu.missionutils.Console; + +public class InputTerminal implements GetExpressionUserCase { + public String getExpression() { + return Console.readLine(); + } +} diff --git a/src/main/java/calculator/application/port/inport/GetExpressionUserCase.java b/src/main/java/calculator/application/port/inport/GetExpressionUserCase.java new file mode 100644 index 0000000..0414ce6 --- /dev/null +++ b/src/main/java/calculator/application/port/inport/GetExpressionUserCase.java @@ -0,0 +1,5 @@ +package calculator.application.port.inport; + +public interface GetExpressionUserCase { + String getExpression(); +} diff --git a/src/main/java/calculator/application/port/outport/ResultTerminal.java b/src/main/java/calculator/application/port/outport/ResultTerminal.java new file mode 100644 index 0000000..34015cd --- /dev/null +++ b/src/main/java/calculator/application/port/outport/ResultTerminal.java @@ -0,0 +1,9 @@ +package calculator.application.port.outport; + +import calculator.domain.enums.MessageConstant; + +public interface ResultTerminal { + void printMessage(MessageConstant message); + void printResult(int result); + void handleException(RuntimeException e); +} diff --git a/src/main/java/calculator/application/service/CalculatorService.java b/src/main/java/calculator/application/service/CalculatorService.java new file mode 100644 index 0000000..56850dd --- /dev/null +++ b/src/main/java/calculator/application/service/CalculatorService.java @@ -0,0 +1,28 @@ +package calculator.application.service; + +import calculator.application.port.inport.GetExpressionUserCase; +import calculator.application.port.outport.ResultTerminal; +import calculator.domain.Expression; +import calculator.domain.enums.MessageConstant; + +public class CalculatorService { + private final ResultTerminal resultTerminal; + private final GetExpressionUserCase getExpressionUserCase; + + public CalculatorService(ResultTerminal resultTerminal, GetExpressionUserCase getExpressionUserCase) { + this.resultTerminal = resultTerminal; + this.getExpressionUserCase = getExpressionUserCase; + } + + public void run() { + try { + resultTerminal.printMessage(MessageConstant.START_GUIDE_MESSAGE); + String line = getExpressionUserCase.getExpression(); + Expression expression = new Expression(line); + int result = expression.calculate(); + resultTerminal.printResult(result); + } catch (RuntimeException e) { + resultTerminal.handleException(e); + } + } +} diff --git a/src/main/java/calculator/domain/Expression.java b/src/main/java/calculator/domain/Expression.java new file mode 100644 index 0000000..83e0841 --- /dev/null +++ b/src/main/java/calculator/domain/Expression.java @@ -0,0 +1,47 @@ +package calculator.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class Expression { + private final List operands; + private static final String CUSTOM_SEPARATOR_REGEX = "^//(.)\\\\n(.*)"; + private static final String EXPRESSION_REGEX_BASE = "([0-9]+)[[%s]+([0-9]+)]*"; + + public Expression(String expressionLine) { + operands = new ArrayList<>(); + + String separator = "[,:]"; + String expressionBody = expressionLine; + + Matcher matcher = Pattern.compile(CUSTOM_SEPARATOR_REGEX).matcher(expressionBody); + if (matcher.find()) { + separator = String.format("[,:%s]", Pattern.quote(matcher.group(1))); + expressionBody = matcher.group(2); + } + + validateExpression(expressionBody, separator); + parseOperand(expressionBody, separator); + } + + private void validateExpression(String expressionBody, String separator) { + String fullRegex = String.format(EXPRESSION_REGEX_BASE, separator); + if (!expressionBody.matches(fullRegex)) { + throw new IllegalArgumentException("잘못된 수식입니다: " + expressionBody); + } + } + + private void parseOperand(String body, String separatorRegex) { + Stream.of(body.split(separatorRegex)) + .map(Integer::parseInt) + .forEach(operands::add); + } + + + public int calculate() { + return operands.stream().reduce(0, Math::addExact); + } +} diff --git a/src/main/java/calculator/domain/enums/MessageConstant.java b/src/main/java/calculator/domain/enums/MessageConstant.java new file mode 100644 index 0000000..aac192d --- /dev/null +++ b/src/main/java/calculator/domain/enums/MessageConstant.java @@ -0,0 +1,15 @@ +package calculator.domain.enums; + +public enum MessageConstant { + START_GUIDE_MESSAGE("덧셈할 문자열을 입력해 주세요."), + RESULT_MESSAGE("결과 : "); + + private String message; + + MessageConstant(String message) { + this.message = message; + } + public String getMessage() { + return message; + } +} diff --git a/src/test/java/calculator/ApplicationTest.java b/src/test/java/calculator/ApplicationTest.java index 93771fb..af087ec 100644 --- a/src/test/java/calculator/ApplicationTest.java +++ b/src/test/java/calculator/ApplicationTest.java @@ -8,19 +8,100 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; class ApplicationTest extends NsTest { + + @Test + void exampleTest() { + assertSimpleTest(() -> { + run("1,2:3"); + assertThat(output()).contains("결과 : 6"); + }); + } + + @Test + void constantExpressionTest() { + assertSimpleTest(() -> { + run("5"); + assertThat(output()).contains("결과 : 5"); + }); + } + + @Test + void customSeparatorTest() { + assertSimpleTest(() -> { + run("//;\\n3;4:5"); + assertThat(output()).contains("결과 : 12"); + }); + } + + @Test + void intOverflowTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("20000000000")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void longOverflowTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("50000000000000000000")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void severalCustomSeparatorTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//^&\\n3^4&5")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void numberSeparatorTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//3\\n53:43")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void emptyExpressionTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void intAddingOverflowTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("2000000000:2000000000")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void longAddingOverflowTest() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("5000000000000000000:5000000000000000000")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + @Test - void 커스텀_구분자_사용() { + void escapeCharTest() { assertSimpleTest(() -> { - run("//;\\n1"); - assertThat(output()).contains("결과 : 1"); + run("//\\\\n3\\4\\5"); + assertThat(output()).contains("결과 : 12"); }); } @Test - void 예외_테스트() { + void emptyValueTest() { assertSimpleTest(() -> - assertThatThrownBy(() -> runException("-1,2,3")) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> runException("//*\\n3**:,,4")) + .isInstanceOf(IllegalArgumentException.class) ); } diff --git a/src/test/java/calculator/ExpressionTest.java b/src/test/java/calculator/ExpressionTest.java new file mode 100644 index 0000000..d186d87 --- /dev/null +++ b/src/test/java/calculator/ExpressionTest.java @@ -0,0 +1,60 @@ +package calculator; + +import calculator.domain.Expression; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ExpressionTest { + + @Test + void 기본_구분자_쉼표_콜론으로_계산() { + Expression expr = new Expression("1,2:3"); + assertEquals(6, expr.calculate()); + } + + @Test + void 커스텀_구분자_세미콜론으로_계산() { + Expression expr = new Expression("//;\\n1;2;3"); + assertEquals(6, expr.calculate()); + } + + @Test + void 커스텀_구분자_점으로_계산() { + Expression expr = new Expression("//.\\n4.5:6"); + assertEquals(15, expr.calculate()); + } + + @Test + void 커스텀_구분자와_기본_구분자_혼합() { + Expression expr = new Expression("//|\\n7|8,9:10"); + assertEquals(34, expr.calculate()); + } + + @Test + void 유효하지_않은_표현식_예외() { + assertThrows(IllegalArgumentException.class, () -> { + new Expression("1,,2:3"); + }); + } + + @Test + void 커스텀_정의된_구분자가_아닌_문자_포함시_예외() { + assertThrows(IllegalArgumentException.class, () -> { + new Expression("//;\\n1;2-3"); + }); + } + + @Test + void 숫자가_아닌_문자가_포함되면_예외() { + assertThrows(IllegalArgumentException.class, () -> { + new Expression("1,a:3"); + }); + } + + @Test + void 한자리_숫자가_아닌_수() { + Expression expr = new Expression("1,10,100,3000"); + assertEquals(3111, expr.calculate()); + } +}