diff --git a/README.md b/README.md index bd90ef0247..90e2f722f5 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse + +# 구현 기능 목록 +1. 입력 처리 +- camp.nextstep.edu.missionutils.Console.readLine()을 사용해 문자열 입력을 받는다. +- 입력 문구: "덧셈할 문자열을 입력해 주세요." + +2. 빈 문자열 처리 +- 입력값이 빈 문자열("")일 경우 결과는 0을 반환한다. + +3. 기본 구분자 처리 +- 쉼표(,) 또는 콜론(:)을 구분자로 처리한다. + +4. 커스텀 구분자 처리 +- "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 처리한다. + +5. 예외처리 +- 다음과 같은 경우 `IllegalArgumentException`을 발생시키고 프로그램은 종료된다. + - 음수 입력 (`-1,2,3`) + - 숫자가 아닌 입력 (`a,1,2`) + - 잘못된 구분자 형식 (`//;12;3`, `//\n1,2`) + - 빈 숫자 토큰이 존재하는 경우 (`1,,2`) +- 프로그램 종료 시 `System.exit()`을 호출하지 않는다. + +6. 출력 +- 계산된 결과를 "결과 : " 형식으로 출력한다. \ No newline at end of file diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..e5efffdbbb 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,17 @@ package calculator; +import calculator.controller.CalculatorController; +import calculator.service.StringCalculatorService; +import calculator.view.InputView; +import calculator.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView in = new InputView(); + OutputView out = new OutputView(); + StringCalculatorService svc = new StringCalculatorService(); + CalculatorController controller = new CalculatorController(in, out, svc); + + controller.run(); } } diff --git a/src/main/java/calculator/controller/CalculatorController.java b/src/main/java/calculator/controller/CalculatorController.java new file mode 100644 index 0000000000..9f2236e30e --- /dev/null +++ b/src/main/java/calculator/controller/CalculatorController.java @@ -0,0 +1,25 @@ +package calculator.controller; + +import calculator.service.StringCalculatorService; +import calculator.view.InputView; +import calculator.view.OutputView; + +public class CalculatorController { + private final InputView inputView; + private final OutputView outputView; + private final StringCalculatorService service; + + public CalculatorController(InputView inputView, OutputView outputView, StringCalculatorService service) { + this.inputView = inputView; + this.outputView = outputView; + this.service = service; + } + + public void run() { + outputView.printPrompt(); + String input = inputView.readLine(); + input = input.replace("\\n", "\n"); + int sum = service.add(input); + outputView.printResult(sum); + } +} diff --git a/src/main/java/calculator/service/StringCalculatorService.java b/src/main/java/calculator/service/StringCalculatorService.java new file mode 100644 index 0000000000..8b8e037064 --- /dev/null +++ b/src/main/java/calculator/service/StringCalculatorService.java @@ -0,0 +1,85 @@ +package calculator.service; + +import java.util.regex.Pattern; + +public class StringCalculatorService { + private static final String DEFAULT_DELIMS = "[,:]"; + + public int add(String input) { + if (isEmpty(input)) { + return 0; + } + + Parsed parsed = parse(input); + String[] tokens = tokenize(parsed.numbers, parsed.delimRegex); + validate(tokens); + return sum(tokens); + } + + private boolean isEmpty(String s) { + return s == null || s.isEmpty(); + } + + private Parsed parse(String input) { + if (!input.startsWith("//")) { + return new Parsed(DEFAULT_DELIMS, input); + } + + int nl = input.indexOf('\n'); + if (nl < 0) { + throw new IllegalArgumentException("잘못된 커스텀 구분자 형식입니다."); + } + + String custom = input.substring(2, nl); + if (custom.isEmpty()) { + throw new IllegalArgumentException("구분자가 비어 있습니다."); + } + + String delimRegex = Pattern.quote(custom); + String numbers = input.substring(nl + 1); + return new Parsed(delimRegex, numbers); + } + + private String[] tokenize(String numbers, String delimRegex) { + if (numbers.isEmpty()) { + return new String[0]; + } + if (DEFAULT_DELIMS.equals(delimRegex)) { + return numbers.split(DEFAULT_DELIMS, -1); + } + return numbers.split(delimRegex, -1); + } + + private void validate(String[] tokens) { + for (String t : tokens) { + if (t == null || t.isEmpty()) { + throw new IllegalArgumentException("빈 숫자 토큰이 포함되어 있습니다."); + } + if (!t.chars().allMatch(Character::isDigit)) { + throw new IllegalArgumentException("숫자가 아닌 값이 포함되었습니다."); + } + int v = Integer.parseInt(t); + if (v < 0) { + throw new IllegalArgumentException("음수는 허용되지 않습니다."); + } + } + } + + private int sum(String[] tokens) { + int total = 0; + for (String t : tokens) { + total += Integer.parseInt(t); + } + return total; + } + + private static final class Parsed { + final String delimRegex; + final String numbers; + + Parsed(String d, String n) { + this.delimRegex = d; + this.numbers = n; + } + } +} diff --git a/src/main/java/calculator/view/InputView.java b/src/main/java/calculator/view/InputView.java new file mode 100644 index 0000000000..430825de72 --- /dev/null +++ b/src/main/java/calculator/view/InputView.java @@ -0,0 +1,9 @@ +package calculator.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + public String readLine() { + return Console.readLine(); + } +} diff --git a/src/main/java/calculator/view/OutputView.java b/src/main/java/calculator/view/OutputView.java new file mode 100644 index 0000000000..088303ee02 --- /dev/null +++ b/src/main/java/calculator/view/OutputView.java @@ -0,0 +1,11 @@ +package calculator.view; + +public class OutputView { + public void printPrompt() { + System.out.println("덧셈할 문자열을 입력해 주세요."); + } + + public void printResult(int sum) { + System.out.println("결과 : " + sum); + } +} diff --git a/src/test/java/calculator/ApplicationTest.java b/src/test/java/calculator/ApplicationTest.java index 93771fb011..b06d7dc6b6 100644 --- a/src/test/java/calculator/ApplicationTest.java +++ b/src/test/java/calculator/ApplicationTest.java @@ -8,10 +8,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; class ApplicationTest extends NsTest { + @Test void 커스텀_구분자_사용() { assertSimpleTest(() -> { - run("//;\\n1"); + run("//;\\n1\n"); assertThat(output()).contains("결과 : 1"); }); } @@ -19,13 +20,45 @@ class ApplicationTest extends NsTest { @Test void 예외_테스트() { assertSimpleTest(() -> - assertThatThrownBy(() -> runException("-1,2,3")) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> runException("-1,2,3\n")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 기본_구분자_쉼표와_콜론_테스트() { + assertSimpleTest(() -> { + run("1,2:3\n"); + assertThat(output()).contains("결과 : 6"); + }); + } + + @Test + void 빈_문자열_입력시_0_반환() { + assertSimpleTest(() -> { + run("\n"); + assertThat(output()).contains("결과 : 0"); + }); + } + + @Test + void 잘못된_커스텀_형식_예외() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//;12;3\n")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 비어있는_커스텀_구분자_예외() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//\n1,2\n")) + .isInstanceOf(IllegalArgumentException.class) ); } @Override public void runMain() { - Application.main(new String[]{}); + Application.main(new String[] {}); } }