From ce62e6f0f176152fb6c4ec1caabaa1aa12a04f6a Mon Sep 17 00:00:00 2001 From: moongua404 Date: Mon, 12 May 2025 10:01:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?docs(README.md):=20=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=EB=AF=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd90ef0..b7ab19f 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 도입 \ No newline at end of file From 58aee1563b7fc54fe318196ea0939d0f8ea91422 Mon Sep 17 00:00:00 2001 From: moongua404 Date: Mon, 12 May 2025 11:32:25 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/main/java/calculator/Application.java | 6 +++ .../calculator/adapter/in/InputTerminal.java | 10 ++++ .../port/inport/GetExpressionUserCase.java | 5 ++ .../port/outport/ResultTerminal.java | 9 ++++ .../service/CalculatorService.java | 28 ++++++++++ .../java/calculator/domain/Expression.java | 42 +++++++++++++++ .../domain/enums/MessageConstant.java | 15 ++++++ src/test/java/calculator/ExpressionTest.java | 54 +++++++++++++++++++ 9 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/main/java/calculator/adapter/in/InputTerminal.java create mode 100644 src/main/java/calculator/application/port/inport/GetExpressionUserCase.java create mode 100644 src/main/java/calculator/application/port/outport/ResultTerminal.java create mode 100644 src/main/java/calculator/application/service/CalculatorService.java create mode 100644 src/main/java/calculator/domain/Expression.java create mode 100644 src/main/java/calculator/domain/enums/MessageConstant.java create mode 100644 src/test/java/calculator/ExpressionTest.java diff --git a/README.md b/README.md index b7ab19f..076c379 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,4 @@ - 수식 계산 ## Design Pattern -- Hexagonal Architecture 도입 \ No newline at end of file +- 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..3bdfed1 --- /dev/null +++ b/src/main/java/calculator/domain/Expression.java @@ -0,0 +1,42 @@ +package calculator.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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; + + if (expressionLine.matches(CUSTOM_SEPARATOR_REGEX)) { + Matcher matcher = Pattern.compile(CUSTOM_SEPARATOR_REGEX).matcher(expressionLine); + if (matcher.find()) { + String custom = Pattern.quote(matcher.group(1)); + separator = String.format("[,:%s]", custom); + expressionBody = expressionLine.substring(5); + } + } + + String fullRegex = String.format(EXPRESSION_REGEX_BASE, separator); + if (!expressionBody.matches(fullRegex)) { + throw new IllegalArgumentException("잘못된 수식입니다: " + expressionBody); + } + + String[] tokens = expressionBody.split(separator); + for (String token : tokens) { + operands.add(Integer.parseInt(token)); + } + } + + public int calculate() { + return operands.stream().reduce(0, Integer::sum); + } +} 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/ExpressionTest.java b/src/test/java/calculator/ExpressionTest.java new file mode 100644 index 0000000..023857f --- /dev/null +++ b/src/test/java/calculator/ExpressionTest.java @@ -0,0 +1,54 @@ +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"); + }); + } +} From be3639a5840263f7e5850670c1134e19453e09ac Mon Sep 17 00:00:00 2001 From: moongua404 Date: Mon, 12 May 2025 12:56:57 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(model):=20expression=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 연산 오버플로우 처리 오류 해결 - 파일 구조 개선 (함수 분리) + 테스트 코드 추가 --- .../java/calculator/domain/Expression.java | 31 ++++--- src/test/java/calculator/ApplicationTest.java | 93 +++++++++++++++++-- src/test/java/calculator/ExpressionTest.java | 6 ++ 3 files changed, 111 insertions(+), 19 deletions(-) diff --git a/src/main/java/calculator/domain/Expression.java b/src/main/java/calculator/domain/Expression.java index 3bdfed1..83e0841 100644 --- a/src/main/java/calculator/domain/Expression.java +++ b/src/main/java/calculator/domain/Expression.java @@ -4,10 +4,11 @@ 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 CUSTOM_SEPARATOR_REGEX = "^//(.)\\\\n(.*)"; private static final String EXPRESSION_REGEX_BASE = "([0-9]+)[[%s]+([0-9]+)]*"; public Expression(String expressionLine) { @@ -16,27 +17,31 @@ public Expression(String expressionLine) { String separator = "[,:]"; String expressionBody = expressionLine; - if (expressionLine.matches(CUSTOM_SEPARATOR_REGEX)) { - Matcher matcher = Pattern.compile(CUSTOM_SEPARATOR_REGEX).matcher(expressionLine); - if (matcher.find()) { - String custom = Pattern.quote(matcher.group(1)); - separator = String.format("[,:%s]", custom); - expressionBody = expressionLine.substring(5); - } + 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); } + } - String[] tokens = expressionBody.split(separator); - for (String token : tokens) { - operands.add(Integer.parseInt(token)); - } + 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, Integer::sum); + return operands.stream().reduce(0, Math::addExact); } } 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 index 023857f..d186d87 100644 --- a/src/test/java/calculator/ExpressionTest.java +++ b/src/test/java/calculator/ExpressionTest.java @@ -51,4 +51,10 @@ class ExpressionTest { new Expression("1,a:3"); }); } + + @Test + void 한자리_숫자가_아닌_수() { + Expression expr = new Expression("1,10,100,3000"); + assertEquals(3111, expr.calculate()); + } }