diff --git a/README.md b/README.md index bd90ef0247..a897fff0bb 100644 --- a/README.md +++ b/README.md @@ -1 +1,41 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse + +## 기능 요구사항 +입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다. + +쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
+ +예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6
+ +앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다.
+커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
+ +예를 들어 `"//;\n1;2;3"`과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 `6`이 반환되어야 한다.
+ +사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. + +- 입력 : 구분자와 양수로 구성된 문자열
+- 출력 : 덧셈 결과 + +### 라이브러리
+`camp.nextstep.edu.missionutils`에서 제공하는 Console API를 사용하여 구현해야 한다.
+사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다. +## 기능 목록 + +- [x] 문자열 입력받기 + - [x] 입력 클래스 분리 + - [x] 문자열 입력 문구 출력 + - [x] 라이브러리를 이용한 문자열 입력 +- [x] 문자열 쉼표 및 세미콜론, 커스텀 구분자로 나누기 + - [x] 문자열 분석 클래스 분리 + - [x] 구분자 분리 + - [x] 커스텀 구분자 분리 + - [x] 입력값에서 쉼표 및 세미콜론 또는 커스텀 구분자만 사용되었는지 검증 + - [x] 구분 인덱스 enum으로 관리 + - [x] 커스텀 구분자 구문 삭제 로직 구현 + - [x] 구분자를 이용하여 문자열을 숫자만 있는 배열로 추출 +- [x] 숫자 덧셈 + - [x] 계산 클래스 분리 + - [x] 구분자를 비교하여 숫자 덧셈 구현 +- [x] runner 구현 +- [x] 결과 반환 \ No newline at end of file diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..fffe8b4b00 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,12 @@ package calculator; +import calculator.runner.CalculateRunner; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + + CalculateRunner runner = CalculateRunner.getInstance(); + + runner.run(); } } diff --git a/src/main/java/calculator/calculation/Calculation.java b/src/main/java/calculator/calculation/Calculation.java new file mode 100644 index 0000000000..b3eb4e99d1 --- /dev/null +++ b/src/main/java/calculator/calculation/Calculation.java @@ -0,0 +1,20 @@ +package calculator.calculation; + +public class Calculation { + + private static final Calculation INSTANCE = new Calculation(); + + private Calculation() {} + + public static Calculation getInstance() { + return INSTANCE; + } + + public int calculate(String[] numbers) { + int sum = 0; + for(String number : numbers) { + sum += Integer.parseInt(number); + } + return sum; + } +} diff --git a/src/main/java/calculator/global/DefaultDelimiter.java b/src/main/java/calculator/global/DefaultDelimiter.java new file mode 100644 index 0000000000..fcfb1f61ad --- /dev/null +++ b/src/main/java/calculator/global/DefaultDelimiter.java @@ -0,0 +1,17 @@ +package calculator.global; + +public enum DefaultDelimiter { + + COMMA(","), + COLON(":"); + + private final String key; + + DefaultDelimiter(String key) { + this.key = key; + } + + public String getKey() { + return key; + } +} diff --git a/src/main/java/calculator/global/DelimiterSyntaxIndex.java b/src/main/java/calculator/global/DelimiterSyntaxIndex.java new file mode 100644 index 0000000000..e7e1fb436d --- /dev/null +++ b/src/main/java/calculator/global/DelimiterSyntaxIndex.java @@ -0,0 +1,19 @@ +package calculator.global; + +public enum DelimiterSyntaxIndex { + + DELIMITER_INDEX(2), + FIRST_END(3), + SECOND_END(4), + DELIMITER_AFTER(5); + + private final int key; + + DelimiterSyntaxIndex(int key) { + this.key = key; + } + + public int getKey() { + return key; + } +} diff --git a/src/main/java/calculator/input/InputHandler.java b/src/main/java/calculator/input/InputHandler.java new file mode 100644 index 0000000000..ca13ce7b23 --- /dev/null +++ b/src/main/java/calculator/input/InputHandler.java @@ -0,0 +1,23 @@ +package calculator.input; + +import camp.nextstep.edu.missionutils.Console; + +public class InputHandler { + + private static final InputHandler INSTANCE = new InputHandler(); + + private InputHandler() {} + + public static InputHandler getInstance() { + return INSTANCE; + } + + public String getInput() { + System.out.println("문자열을 입력해주세요"); + return Console.readLine(); + } + + public void closeInput() { + Console.close(); + } +} diff --git a/src/main/java/calculator/parser/Delimiter.java b/src/main/java/calculator/parser/Delimiter.java new file mode 100644 index 0000000000..4f036a9560 --- /dev/null +++ b/src/main/java/calculator/parser/Delimiter.java @@ -0,0 +1,46 @@ +package calculator.parser; + +import calculator.global.DefaultDelimiter; +import calculator.global.DelimiterSyntaxIndex; + +public class Delimiter { + + private static final Delimiter INSTANCE = new Delimiter(); + + private Delimiter() {} + + public static Delimiter getInstance() { + return INSTANCE; + } + + public String extractDelimiter(String input) { + if (validateStartsWith(input) && validateEndsWith(input)) { + String rawDelimiter = String.valueOf(input.charAt(DelimiterSyntaxIndex.DELIMITER_INDEX.getKey())); + + return rawDelimiter.matches("[.\\^$|?*+(){}\\[\\]\\\\]") ? "\\" + rawDelimiter : rawDelimiter; + } + return ""; + } + + public String removeDelimiterSyntax(String input) { + if (validateStartsWith(input) && validateEndsWith(input)) { + return input.substring(DelimiterSyntaxIndex.DELIMITER_AFTER.getKey()); + } + return input; + } + + public boolean validateHasDefaultOrCustomDelimiter(String input, String customDelimiter) { + String delimiters = DefaultDelimiter.COLON.getKey() + DefaultDelimiter.COMMA.getKey() + customDelimiter; + String defaultSyntax = removeDelimiterSyntax(input); + return defaultSyntax.matches("[0-9" + delimiters + "]*"); + } + + private boolean validateStartsWith(String input) { + return input.startsWith("//"); + } + + private boolean validateEndsWith(String input) { + return input.charAt(DelimiterSyntaxIndex.FIRST_END.getKey()) == '\\' + && input.charAt(DelimiterSyntaxIndex.SECOND_END.getKey()) == 'n'; + } +} diff --git a/src/main/java/calculator/parser/InputParser.java b/src/main/java/calculator/parser/InputParser.java new file mode 100644 index 0000000000..b37066f999 --- /dev/null +++ b/src/main/java/calculator/parser/InputParser.java @@ -0,0 +1,37 @@ +package calculator.parser; + +import calculator.global.DefaultDelimiter; + +import java.util.Arrays; + +public class InputParser { + + private static final InputParser INSTANCE = new InputParser(); + private final Delimiter delimiter; + + private InputParser() { + this.delimiter = Delimiter.getInstance(); + } + + public static InputParser getInstance() { + return INSTANCE; + } + + public String[] parse(String input) { + String customDelimiter = delimiter.extractDelimiter(input); + if (!delimiter.validateHasDefaultOrCustomDelimiter(input, customDelimiter)) { + throw new IllegalArgumentException("허용되지 않는 입력문입니다."); + } + String defaultSyntax = delimiter.removeDelimiterSyntax(input); + return extractNumbersFromDefaultSyntax(defaultSyntax, customDelimiter); + } + + private String[] extractNumbersFromDefaultSyntax(String defaultSyntax, String customDelimiter) { + String delimiters = DefaultDelimiter.COMMA.getKey() + DefaultDelimiter.COLON.getKey() + customDelimiter; + String[] numbers = defaultSyntax.split("[" + delimiters + "]"); + + return Arrays.stream(numbers) + .filter(num -> !num.isEmpty()) + .toArray(String[]::new); + } +} diff --git a/src/main/java/calculator/runner/CalculateRunner.java b/src/main/java/calculator/runner/CalculateRunner.java new file mode 100644 index 0000000000..81bf741635 --- /dev/null +++ b/src/main/java/calculator/runner/CalculateRunner.java @@ -0,0 +1,34 @@ +package calculator.runner; + +import calculator.calculation.Calculation; +import calculator.input.InputHandler; +import calculator.parser.InputParser; + +public class CalculateRunner { + + private static final CalculateRunner INSTANCE = new CalculateRunner(); + + public static CalculateRunner getInstance() { + return INSTANCE; + } + + private final Calculation calculator; + private final InputParser parser; + private final InputHandler inputHandler; + + private CalculateRunner() { + this.calculator = Calculation.getInstance(); + this.parser = InputParser.getInstance(); + this.inputHandler = InputHandler.getInstance(); + } + + public void run() { + String input = inputHandler.getInput(); + String[] parseResult = parser.parse(input); + int result = calculator.calculate(parseResult); + + System.out.println("Result: " + result); + + inputHandler.closeInput(); + } +} diff --git a/src/test/java/calculator/calculation/CalculationTest.java b/src/test/java/calculator/calculation/CalculationTest.java new file mode 100644 index 0000000000..af4b02d1a1 --- /dev/null +++ b/src/test/java/calculator/calculation/CalculationTest.java @@ -0,0 +1,48 @@ +package calculator.calculation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CalculationTest { + + private final Calculation calculator = Calculation.getInstance(); + + @Test + @DisplayName("정상적인 숫자 배열로 계산") + void testCalculateWithValidNumbers() { + // Given + String[] numbers = {"1", "2", "3"}; + + // When + int result = calculator.calculate(numbers); + + // Then + assertEquals(6, result); + } + + @Test + @DisplayName("빈 배열 입력 시 0 반환") + void testCalculateWithEmptyArray() { + // Given + String[] numbers = {}; + + // When + int result = calculator.calculate(numbers); + + // Then + assertEquals(0, result); + } + + @Test + @DisplayName("숫자가 아닌 값이 포함되었을 때 예외 발생") + void testCalculateWithInvalidNumber() { + // Given + String[] numbers = {"1", "a", "3"}; + + // When & Then + assertThrows(IllegalArgumentException.class, () -> calculator.calculate(numbers)); + } +} diff --git a/src/test/java/calculator/delimiter/DelimiterTest.java b/src/test/java/calculator/delimiter/DelimiterTest.java new file mode 100644 index 0000000000..86d166b75f --- /dev/null +++ b/src/test/java/calculator/delimiter/DelimiterTest.java @@ -0,0 +1,60 @@ +package calculator.delimiter; + +import calculator.parser.Delimiter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DelimiterTest { + + private final Delimiter delimiter = Delimiter.getInstance(); + + @Test + @DisplayName("커스텀 구분자를 올바르게 추출") + void testExtractDelimiterWithCustomSyntax() { + // Given + String input = "//;\\n1;2;3"; + + // When + String result = delimiter.extractDelimiter(input); + + // Then + assertEquals(";", result); + } + + @Test + @DisplayName("구분자 구문 제거 테스트") + void testRemoveDelimiterSyntax() { + // Given + String input = "//;\\n1;2;3"; + + // When + String result = delimiter.removeDelimiterSyntax(input); + + // Then + assertEquals("1;2;3", result); + } + + @Test + @DisplayName("기본 또는 커스텀 구분자 유효성 테스트") + void testValidateHasDefaultOrCustomDelimiter() { + // Given + String input = "//;\\n1;2;3"; + + // When & Then + assertTrue(delimiter.validateHasDefaultOrCustomDelimiter(input, ";")); + } + + @Test + @DisplayName("잘못된 구분자 입력 시 실패") + void testValidateInvalidDelimiter() { + // Given + String input = "//;\\n1,2;3"; + + // When & Then + assertFalse(delimiter.validateHasDefaultOrCustomDelimiter(input, "|")); + } +} diff --git a/src/test/java/calculator/input/InputTest.java b/src/test/java/calculator/input/InputTest.java new file mode 100644 index 0000000000..573e4a6db0 --- /dev/null +++ b/src/test/java/calculator/input/InputTest.java @@ -0,0 +1,31 @@ +package calculator.input; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class InputTest { + + private final InputHandler inputHandler = InputHandler.getInstance(); + + @Test + @DisplayName("사용자 입력을 정상적으로 받는지 테스트") + void testGetInput() { + // Given + String simulatedInput = "1,2,3"; + System.setIn(new ByteArrayInputStream(simulatedInput.getBytes())); + + // When & Then + assertDoesNotThrow(inputHandler::getInput); + } + + @Test + @DisplayName("입력 스트림을 정상적으로 닫는지 테스트") + void testCloseInput() { + // Given & When & Then + assertDoesNotThrow(inputHandler::closeInput); + } +} diff --git a/src/test/java/calculator/parser/ParserTest.java b/src/test/java/calculator/parser/ParserTest.java new file mode 100644 index 0000000000..0d9dc87e22 --- /dev/null +++ b/src/test/java/calculator/parser/ParserTest.java @@ -0,0 +1,48 @@ +package calculator.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ParserTest { + + private final InputParser parser = InputParser.getInstance(); + + @Test + @DisplayName("커스텀 구분자로 입력을 파싱") + void testParseWithCustomDelimiter() { + // Given + String input = "//;\\n1;2;3"; + + // When + String[] result = parser.parse(input); + + // Then + assertArrayEquals(new String[]{"1", "2", "3"}, result); + } + + @Test + @DisplayName("기본 구분자로 입력을 파싱") + void testParseWithDefaultDelimiter() { + // Given + String input = "1,2:3"; + + // When + String[] result = parser.parse(input); + + // Then + assertArrayEquals(new String[]{"1", "2", "3"}, result); + } + + @Test + @DisplayName("잘못된 구분자 구문 입력 시 예외 발생") + void testParseWithInvalidDelimiter() { + // Given + String input = "///;\\n1,2:3"; + + // When & Then + assertThrows(IllegalArgumentException.class, () -> parser.parse(input)); + } +} diff --git a/src/test/java/calculator/runner/RunnerTest.java b/src/test/java/calculator/runner/RunnerTest.java new file mode 100644 index 0000000000..1b459139ea --- /dev/null +++ b/src/test/java/calculator/runner/RunnerTest.java @@ -0,0 +1,23 @@ +package calculator.runner; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class RunnerTest { + + @Test + @DisplayName("CalculateRunner 실행 중 예외 발생 없이 실행") + void testRunnerExecution() { + // Given + CalculateRunner runner = CalculateRunner.getInstance(); + String simulatedInput = "1,2:3"; + System.setIn(new ByteArrayInputStream(simulatedInput.getBytes())); + + // When & Then + assertDoesNotThrow(runner::run); + } +}