diff --git a/README.md b/README.md index bd90ef0247..bc337504cf 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse + +## 기능 구현 목록 + +1. 덧셈할 문자열 입력 및 검증 + - 예외 사항 + - 잘못된 형식인 경우 + - 잘못된 형식입니다. + - 커스텀 구분자의 기준 + - 기본 구분자, 빈문자열, 숫자, “/“, “\”, “n”을 제외한 모든 문자 허용 + - 1개만 허용 +2. 문자열 파싱 + 1. 커스텀 구분자가 정의되었다면 저장 +3. 숫자 계산 +4. 출력 \ No newline at end of file diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..b1561b903c 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,12 @@ package calculator; +import calculator.controller.CalculatorController; +import calculator.service.CalculatorService; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + CalculatorService calculatorService = new CalculatorService(); + CalculatorController calculatorController = new CalculatorController(calculatorService); + calculatorController.run(); } } diff --git a/src/main/java/calculator/constant/ErrorMessage.java b/src/main/java/calculator/constant/ErrorMessage.java new file mode 100644 index 0000000000..86d0d74cac --- /dev/null +++ b/src/main/java/calculator/constant/ErrorMessage.java @@ -0,0 +1,18 @@ +package calculator.constant; + +public enum ErrorMessage { + + FORMAT_ERROR("잘못된 형식입니다."), + ; + + private static final String ERROR_MESSAGE_PREFIX = "[ERROR] "; + private final String errorMessage; + + ErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorMessage(Object... args) { + return ERROR_MESSAGE_PREFIX + String.format(errorMessage, args); + } +} diff --git a/src/main/java/calculator/controller/CalculatorController.java b/src/main/java/calculator/controller/CalculatorController.java new file mode 100644 index 0000000000..9202dc8282 --- /dev/null +++ b/src/main/java/calculator/controller/CalculatorController.java @@ -0,0 +1,24 @@ +package calculator.controller; + +import calculator.service.CalculatorService; +import calculator.util.InputParser; +import calculator.view.InputView; +import calculator.view.OutputView; + +public class CalculatorController { + + private final CalculatorService calculatorService; + + public CalculatorController(CalculatorService calculatorService) { + this.calculatorService = calculatorService; + } + + public void run() { + String rawInput = InputView.readInput(); + String input = InputParser.parseInput(rawInput); + + int sum = calculatorService.calculate(input); + + OutputView.printSum(sum); + } +} diff --git a/src/main/java/calculator/domain/Delimiter.java b/src/main/java/calculator/domain/Delimiter.java new file mode 100644 index 0000000000..27cfe6d0ab --- /dev/null +++ b/src/main/java/calculator/domain/Delimiter.java @@ -0,0 +1,29 @@ +package calculator.domain; + +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class Delimiter { + + private static final String INIT_DELIMITER = "[,:]"; + + private String delimiter; + + private Delimiter() { + delimiter = INIT_DELIMITER; + } + + public static Delimiter newInstance() { + return new Delimiter(); + } + + public void addDelimiter(String customDelimiter) { + delimiter += "|" + Pattern.quote(customDelimiter); + } + + public List split(String target) { + return Stream.of(target.split(delimiter)) + .toList(); + } +} diff --git a/src/main/java/calculator/domain/Number.java b/src/main/java/calculator/domain/Number.java new file mode 100644 index 0000000000..27ee54a4c9 --- /dev/null +++ b/src/main/java/calculator/domain/Number.java @@ -0,0 +1,28 @@ +package calculator.domain; + +import calculator.constant.ErrorMessage; +import calculator.util.NumberConvertor; + +public class Number { + + private final int number; + + private Number(int number) { + this.number = number; + } + + public static Number from(String s) { + validateNumber(s); + return new Number(NumberConvertor.convertToNumber(s)); + } + + private static void validateNumber(String s) { + if (!s.matches("\\d+")) { + throw new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + } + + public int addNumber(int addNumber) { + return addNumber + number; + } +} diff --git a/src/main/java/calculator/domain/Numbers.java b/src/main/java/calculator/domain/Numbers.java new file mode 100644 index 0000000000..12c40222c1 --- /dev/null +++ b/src/main/java/calculator/domain/Numbers.java @@ -0,0 +1,27 @@ +package calculator.domain; + +import java.util.List; + +public class Numbers { + + private final List numbers; + + private Numbers(List numbers) { + this.numbers = numbers; + } + + public static Numbers from(List strings) { + List numbers = strings.stream() + .map(Number::from) + .toList(); + return new Numbers(numbers); + } + + public int calculateSum() { + int sum = 0; + for (Number number : numbers) { + sum = number.addNumber(sum); + } + return sum; + } +} diff --git a/src/main/java/calculator/domain/Parser.java b/src/main/java/calculator/domain/Parser.java new file mode 100644 index 0000000000..2236cbbbce --- /dev/null +++ b/src/main/java/calculator/domain/Parser.java @@ -0,0 +1,46 @@ +package calculator.domain; + +import java.util.List; + +public class Parser { + + private final Delimiter delimiter; + private final String input; + + private Parser(Delimiter delimiter, String input) { + this.delimiter = delimiter; + this.input = input; + } + + public static Parser from(String input) { + Delimiter delimiter = Delimiter.newInstance(); + return new Parser(delimiter, input); + } + + public List parseTarget() { + String target = input; + + if (isCustom()) { + String customDelimiter = extractCustomDelimiter(input); + delimiter.addDelimiter(customDelimiter); + target = parseInput(input); + } + + return delimiter.split(target); + } + + private boolean isCustom() { + return input.startsWith("/"); + } + + private String extractCustomDelimiter(String input) { + int beginIndex = input.lastIndexOf("/") + 1; + int endIndex = input.indexOf("\\"); + return input.substring(beginIndex, endIndex); + } + + private String parseInput(String input) { + int beginIndex = input.indexOf("n") + 1; + return input.substring(beginIndex); + } +} diff --git a/src/main/java/calculator/service/CalculatorService.java b/src/main/java/calculator/service/CalculatorService.java new file mode 100644 index 0000000000..cf1aa9a4bb --- /dev/null +++ b/src/main/java/calculator/service/CalculatorService.java @@ -0,0 +1,18 @@ +package calculator.service; + +import calculator.domain.Numbers; +import calculator.domain.Parser; +import java.util.List; + +public class CalculatorService { + + public int calculate(String input) { + if (input.isBlank()) { + return 0; + } + Parser parser = Parser.from(input); + List strings = parser.parseTarget(); + Numbers numbers = Numbers.from(strings); + return numbers.calculateSum(); + } +} diff --git a/src/main/java/calculator/util/InputParser.java b/src/main/java/calculator/util/InputParser.java new file mode 100644 index 0000000000..3b03cae10c --- /dev/null +++ b/src/main/java/calculator/util/InputParser.java @@ -0,0 +1,15 @@ +package calculator.util; + +public final class InputParser { + + private InputParser() { + } + + public static String parseInput(String rawInput) { + rawInput = rawInput.strip(); + + Validator.validateInputFormat(rawInput); + + return rawInput; + } +} diff --git a/src/main/java/calculator/util/NumberConvertor.java b/src/main/java/calculator/util/NumberConvertor.java new file mode 100644 index 0000000000..4db39ed0a6 --- /dev/null +++ b/src/main/java/calculator/util/NumberConvertor.java @@ -0,0 +1,14 @@ +package calculator.util; + +import calculator.constant.ErrorMessage; + +public final class NumberConvertor { + + public static Integer convertToNumber(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + } +} diff --git a/src/main/java/calculator/util/Validator.java b/src/main/java/calculator/util/Validator.java new file mode 100644 index 0000000000..06a022b9ac --- /dev/null +++ b/src/main/java/calculator/util/Validator.java @@ -0,0 +1,16 @@ +package calculator.util; + +import calculator.constant.ErrorMessage; + +public final class Validator { + + private static final String FORMAT = "(//[^\\d,:/n\\\\]\\\\n)?[^/n\\\\]*"; + + private Validator() {} + + public static void validateInputFormat(String input) { + if (!input.matches(FORMAT)) { + throw new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + } +} diff --git a/src/main/java/calculator/view/InputView.java b/src/main/java/calculator/view/InputView.java new file mode 100644 index 0000000000..db1fd6880f --- /dev/null +++ b/src/main/java/calculator/view/InputView.java @@ -0,0 +1,11 @@ +package calculator.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + public static String readInput() { + System.out.println("덧셈할 문자열을 입력해주세요."); + 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..99fcc65b7d --- /dev/null +++ b/src/main/java/calculator/view/OutputView.java @@ -0,0 +1,11 @@ +package calculator.view; + +public class OutputView { + + private OutputView() { + } + + public static void printSum(int sum) { + System.out.printf("결과 : %d\n", sum); + } +} diff --git a/src/test/java/calculator/ApplicationTest.java b/src/test/java/calculator/ApplicationTest.java index 93771fb011..ff5cab7e1c 100644 --- a/src/test/java/calculator/ApplicationTest.java +++ b/src/test/java/calculator/ApplicationTest.java @@ -1,12 +1,17 @@ package calculator; -import camp.nextstep.edu.missionutils.test.NsTest; -import org.junit.jupiter.api.Test; - import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import calculator.constant.ErrorMessage; +import camp.nextstep.edu.missionutils.test.NsTest; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + class ApplicationTest extends NsTest { @Test void 커스텀_구분자_사용() { @@ -19,8 +24,49 @@ class ApplicationTest extends NsTest { @Test void 예외_테스트() { assertSimpleTest(() -> - assertThatThrownBy(() -> runException("-1,2,3")) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> runException("-1,2,3")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @ParameterizedTest + @MethodSource("successInputProvider") + void 성공_케이스(String input, int expectedResult) { + assertSimpleTest(() -> { + run(input); + assertThat(output()).contains("결과 : " + expectedResult); + }); + } + + static Stream successInputProvider() { + return Stream.of( + Arguments.of("//;\\n1,2:3;4", 10), + Arguments.of("//;\\n1234", 1234), + Arguments.of("1,2,3", 6), + Arguments.of("1,2:3", 6), + Arguments.of("123", 123) + ); + } + + @ParameterizedTest + @MethodSource("failureInputProvider") + void 실패_케이스(String input) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ErrorMessage.FORMAT_ERROR.getErrorMessage()) + ); + } + + static Stream failureInputProvider() { + return Stream.of( + Arguments.of("//;\\na,b:c;d"), + Arguments.of("//;\\nabcd"), + Arguments.of("a,b,c"), + Arguments.of("//;-\\n1-2:3;4"), + Arguments.of("//;\\\\n1,2:3;4"), + Arguments.of("//;\\n//-\\n1,2-3;4"), + Arguments.of("//;\\n1,2\\3;4") ); } @@ -29,3 +75,4 @@ public void runMain() { Application.main(new String[]{}); } } + diff --git a/src/test/java/calculator/domain/DelimiterTest.java b/src/test/java/calculator/domain/DelimiterTest.java new file mode 100644 index 0000000000..315ea97110 --- /dev/null +++ b/src/test/java/calculator/domain/DelimiterTest.java @@ -0,0 +1,38 @@ +package calculator.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class DelimiterTest { + + @Test + void 기본구분자로_문자열_파싱() { + Delimiter delimiter = Delimiter.newInstance(); + + List result = delimiter.split("1,2:3").stream().toList(); + + assertThat(result).containsExactly("1", "2", "3"); + } + + @Test + void 커스텀구분자로_문자열_파싱() { + Delimiter delimiter = Delimiter.newInstance(); + delimiter.addDelimiter(";"); + + List result = delimiter.split("1;2;3").stream().toList(); + + assertThat(result).containsExactly("1", "2", "3"); + } + + @Test + void 기본구분자_및_커스텀구분자로_문자열_파싱() { + Delimiter delimiter = Delimiter.newInstance(); + delimiter.addDelimiter(";"); + + List result = delimiter.split("1,2:3;4").stream().toList(); + + assertThat(result).containsExactly("1", "2", "3", "4"); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/domain/NumberTest.java b/src/test/java/calculator/domain/NumberTest.java new file mode 100644 index 0000000000..993c8b46f5 --- /dev/null +++ b/src/test/java/calculator/domain/NumberTest.java @@ -0,0 +1,29 @@ +package calculator.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import calculator.constant.ErrorMessage; +import org.junit.jupiter.api.Test; + +class NumberTest { + + @Test + void 숫자_아닌_오류() { + assertThatThrownBy(() -> Number.from("a")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + + @Test + void 숫자_정상_저장() { + Number.from("1"); + } + + @Test + void 더하기() { + Number number = Number.from("1"); + int result = number.addNumber(2); + assertThat(result).isEqualTo(3); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/domain/NumbersTest.java b/src/test/java/calculator/domain/NumbersTest.java new file mode 100644 index 0000000000..9db0e78a5d --- /dev/null +++ b/src/test/java/calculator/domain/NumbersTest.java @@ -0,0 +1,25 @@ +package calculator.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import calculator.constant.ErrorMessage; +import java.util.List; +import org.junit.jupiter.api.Test; + +class NumbersTest { + + @Test + void 숫자가_아닌_문자_오류() { + assertThatThrownBy(() -> Numbers.from(List.of("a", "1", "2"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + + @Test + void 합계_구하기() { + Numbers numbers = Numbers.from(List.of("1", "2", "3", "4", "5")); + int result = numbers.calculateSum(); + assertThat(result).isEqualTo(15); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/domain/ParserTest.java b/src/test/java/calculator/domain/ParserTest.java new file mode 100644 index 0000000000..3083ba3508 --- /dev/null +++ b/src/test/java/calculator/domain/ParserTest.java @@ -0,0 +1,23 @@ +package calculator.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ParserTest { + + @Test + void 기본구분자_문자열_파싱() { + Parser parser = Parser.from("1,2:3"); + List result = parser.parseTarget(); + assertThat(result).containsExactly("1", "2", "3"); + } + + @Test + void 커스텀구분자_문자열_파싱() { + Parser parser = Parser.from("//;\\n1,2:3;4"); + List result = parser.parseTarget(); + assertThat(result).containsExactly("1", "2", "3", "4"); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/service/CalculatorServiceTest.java b/src/test/java/calculator/service/CalculatorServiceTest.java new file mode 100644 index 0000000000..8b1b981557 --- /dev/null +++ b/src/test/java/calculator/service/CalculatorServiceTest.java @@ -0,0 +1,48 @@ +package calculator.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import calculator.constant.ErrorMessage; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class CalculatorServiceTest { + + private final CalculatorService calculatorService = new CalculatorService(); + + @ParameterizedTest + @MethodSource("SuccessInputProvider") + void 결과값_반환_성공(String input, int expectedResult) { + int result = calculatorService.calculate(input); + assertThat(result).isEqualTo(expectedResult); + } + + static Stream SuccessInputProvider() { + return Stream.of( + Arguments.of("//;\\n1,2:3;4", 10), + Arguments.of("//;\\n1234", 1234), + Arguments.of("1,2,3", 6), + Arguments.of("1,2:3", 6), + Arguments.of("123", 123) + ); + } + + @ParameterizedTest + @MethodSource("FailureInputProvider") + void 결과값_반환_실패(String input) { + assertThatThrownBy(() -> calculatorService.calculate(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + + static Stream FailureInputProvider() { + return Stream.of( + Arguments.of("//;\\na,b:c;d"), + Arguments.of("//;\\nabcd"), + Arguments.of("a,b,c") + ); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/util/InputParserTest.java b/src/test/java/calculator/util/InputParserTest.java new file mode 100644 index 0000000000..914a4dc725 --- /dev/null +++ b/src/test/java/calculator/util/InputParserTest.java @@ -0,0 +1,15 @@ +package calculator.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class InputParserTest { + + @Test + void 입력값_파싱() { + String input = InputParser.parseInput(" 1,2,3 "); + assertThat(input).isEqualTo("1,2,3"); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/util/NumberConvertorTest.java b/src/test/java/calculator/util/NumberConvertorTest.java new file mode 100644 index 0000000000..8548216f12 --- /dev/null +++ b/src/test/java/calculator/util/NumberConvertorTest.java @@ -0,0 +1,23 @@ +package calculator.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import calculator.constant.ErrorMessage; +import org.junit.jupiter.api.Test; + +class NumberConvertorTest { + + @Test + void 숫자_변환_성공() { + Integer number = NumberConvertor.convertToNumber("3"); + assertThat(number).isEqualTo(3); + } + + @Test + void 숫자_변환_오류() { + assertThatThrownBy(() -> NumberConvertor.convertToNumber("a")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/util/ValidatorTest.java b/src/test/java/calculator/util/ValidatorTest.java new file mode 100644 index 0000000000..f4dc5b6758 --- /dev/null +++ b/src/test/java/calculator/util/ValidatorTest.java @@ -0,0 +1,48 @@ +package calculator.util; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import calculator.constant.ErrorMessage; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ValidatorTest { + + @ParameterizedTest + @MethodSource("passInputProvider") + void 검증_통과(String input) { + Validator.validateInputFormat(input); + } + + static Stream passInputProvider() { + return Stream.of( + Arguments.of("//;\\n1,2:3;4"), + Arguments.of("//;\\n1234"), + Arguments.of("//;\\na,b:c;d"), + Arguments.of("//;\\nabcd"), + Arguments.of("1,2,3"), + Arguments.of("1,2:3"), + Arguments.of("123"), + Arguments.of("a,b,c") + ); + } + + @ParameterizedTest + @MethodSource("failInputProvider") + void 검증_예외_발생(String input) { + assertThatThrownBy(() -> Validator.validateInputFormat(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + + static Stream failInputProvider() { + return Stream.of( + Arguments.of("//;-\\n1-2:3;4"), + Arguments.of("//;\\\\n1,2:3;4"), + Arguments.of("//;\\n//-\\n1,2-3;4"), + Arguments.of("//;\\n1,2\\3;4") + ); + } +} \ No newline at end of file