diff --git a/README.md b/README.md index bd90ef0247..abcd927fe8 100644 --- a/README.md +++ b/README.md @@ -1 +1,66 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse + +# 1. 기능 요구 사항 +## 입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다. + +### 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다. +>예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6 +앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다. + +>예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다. +사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다. + +#### 입출력 요구 사항 +입력 +>구분자와 양수로 구성된 문자열 + +출력 +>덧셈 결과 +결과 : 6 + +실행 결과 예시 +>덧셈할 문자열을 입력해 주세요. +1,2:3 +결과 : 6 + +# 2. 기능 목록 작성 전, 문제 핵심 파악 +## 2.1. 핵심 알고리즘 파악 : 문자열을 구분자로 분리 +### "String.split()" +> 간단하지만, 매번 정규식 컴파일을 하기 때문에, 대용량 기준으로 성능에 좋지 않음 + +### Pattern.compile(...).split() +> 초기에 정규식을 세팅해놓기는 하지만, 빠르고 정규식 재사용이 가능함. + +### 채택 : Pattern.compile(...).split() + +# 3. 기능 목록 +## 3.1. MVC 구조 분리 +### 소스 코드가 하나로 묶이면 유지보수와 코드 가독성이 떨어짐.이를 방지하고자 코드를 분리함. +> - View : input, output 생성 +> +> - Model : input값 저장 +> +> - Service : 주요 연산 처리 +> +> - Controller : 사용자의 요청을 받아 적절한 Service를 호출하고, 처리 결과를 View에 전달함 +> 흐름 제어(요청 → 처리 → 응답) 역할 + +## 3.2. MVC 구조에서 기능 구현 +### 테스트 코드를 작성하고 모델, 서비스, 컨트롤러와 뷰를 제작 +> - 모델에 임시로 고정된 값을 저장 +> - 서비스 단 제작: +>> - Service를 이용해서 모델에 사용자의 입력값을 처리 +>> - 기본 구분자를 찾아서 사용자의 입력값을 분리 +>> - 사용자 정의 구분자를 찾아서 사용자의 입력값을 분리 +>> - 구분자로 분리되어 저장된 사용자 입력값을 합계 연산하기 +>> - 입력값이 양수만 입력이 가능하도록 테스트 케이스 처리 +>> - 사용자의 입력값에서 기본 구분자와 커스텀 구분자를 함께 사용이 가능하도록 테스트 케이스 처리 +>> - 실질적으로 Model을 사용해서 구분자로 정리된 숫자 목록을 저장 후, 꺼내어 사용 + +> - 컨트롤러 단 제작: +>> - 입력 뷰에서 입력 받은 값을 서비스에 전달 후 출력 뷰에서 출력 + +> - 뷰 단 제작: +>> - 입력 뷰 제작 +>> - 출력 뷰 제작 +>> - 사용자 입력값에 대한 예외 사항 처리 diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..a43d976c42 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,11 @@ package calculator; +import calculator.controller.CalculatorController; + public class Application { + private static final CalculatorController calculatorController = CalculatorController.getInstance(); public static void main(String[] args) { // TODO: 프로그램 구현 + calculatorController.calcFromString(); } } diff --git a/src/main/java/calculator/controller/CalculatorController.java b/src/main/java/calculator/controller/CalculatorController.java new file mode 100644 index 0000000000..e49c9e1cc5 --- /dev/null +++ b/src/main/java/calculator/controller/CalculatorController.java @@ -0,0 +1,48 @@ +package calculator.controller; + +import calculator.service.CalculatorService; +import calculator.view.InputView; +import calculator.view.OutputView; + +public class CalculatorController { + private InputView inputView; + private OutputView outputView; + private CalculatorService calculatorService = CalculatorService.getInstance(); + + // start : singleton + private CalculatorController() { + inputView = new InputView(); + outputView = new OutputView(); + } + + private static final class InnerCalculatorController { + private static final CalculatorController INSTANCE = new CalculatorController(); + } + + public static CalculatorController getInstance() { + return InnerCalculatorController.INSTANCE; + } + // end : singleton + + + /** + * 사용자의 입력을 받아서, 서비스 단에 처리를 위임한 뒤, 연산 결과를 출력 뷰에 전달 + */ + public void calcFromString() { + String userInput = requestInputCalcString(); + + // Service에서 처리 후, 결과를 받아 출력 + Integer calcResult = calculatorService.calcFromString(userInput); + + outputView.printResult(calcResult); + } + + /** + * 사용자에게 프로그램 안내 메시지와 입력값을 입력받아서 리턴 + * @return 사용자의 입력 String + */ + private String requestInputCalcString() { + return inputView.requestInputCalcString(); + } + +} diff --git a/src/main/java/calculator/domain/CalculatorModel.java b/src/main/java/calculator/domain/CalculatorModel.java new file mode 100644 index 0000000000..4a0d5694f2 --- /dev/null +++ b/src/main/java/calculator/domain/CalculatorModel.java @@ -0,0 +1,15 @@ +package calculator.domain; + +import java.util.List; + +public class CalculatorModel { + private final List userInputList; + + public CalculatorModel(List userInputList) { + this.userInputList = userInputList; + } + + public List getUserInputList() { + return userInputList; + } +} diff --git a/src/main/java/calculator/enums/UserInterfaceMsg.java b/src/main/java/calculator/enums/UserInterfaceMsg.java new file mode 100644 index 0000000000..d0b0941b7c --- /dev/null +++ b/src/main/java/calculator/enums/UserInterfaceMsg.java @@ -0,0 +1,24 @@ +package calculator.enums; + +public enum UserInterfaceMsg { + CALCULATOR_INTRO("문자열과 구분자를 입력하면, 문자열에서 양수들을 추출하여 더한 값을 출력합니다.\n" + + "기본 구분자는 쉼표(,)와 콜론(:)을 가질 수 있으며,\n" + + "커스텀 구분자는 문자열 앞부분의 \"//\"와 \"\\n\" 사이에 위치하는 문자를 커스텀 구분자로 사용합니다.\n" + + "음수를 입력하면 예외를 발생시킵니다.\n" + + "덧셈할 문자열을 입력해 주세요"), + CALC_RESULT("결과 : %d") + ; + + private String value; + UserInterfaceMsg(String value) { + this.value = value; + } + + public String getKey() { + return name(); + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/calculator/service/CalculatorService.java b/src/main/java/calculator/service/CalculatorService.java new file mode 100644 index 0000000000..e6306748a0 --- /dev/null +++ b/src/main/java/calculator/service/CalculatorService.java @@ -0,0 +1,101 @@ +package calculator.service; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import calculator.domain.CalculatorModel; + +public class CalculatorService { + private CalculatorModel calculatorModel; + private CalculatorService() { + } + + /** + * 기본 구분자 쉼표(,) 또는 콜론(:)이 사용자 입력 문자열에 있는지 확인하는 메소드 + * @param userInput + * @return Boolean + */ + public static Boolean containsDefaultDelimiters(String userInput) { + return userInput.contains(",") || userInput.contains(":"); + } + + /** + * "//"와 "\n" 사이의 커스텀 구분자를 추출하는 메소드 + * @param userInput + * @return String + */ + public static String extractCustomDelimiter(String userInput) { + String startDelimiter = "//"; + String endDelimiter = "\\n"; + + Integer startIndex = userInput.indexOf(startDelimiter) + startDelimiter.length(); + Integer endIndex = userInput.indexOf(endDelimiter); + if (endIndex == -1) { + // 실제 줄바꿈 문자를 인식하도록 처리 + endDelimiter = "\n"; + endIndex = userInput.indexOf(endDelimiter); + } + + return (startIndex != -1 && endIndex != -1 && startIndex < endIndex) + ? userInput.substring(startIndex, endIndex) : ""; + } + + /** + * 사용자로부터 입력받은 문자열 연산 + * @param userInput + * @return Integer + */ + public Integer calcFromString(String userInput) { + if (userInput.isEmpty()) { + return 0; + } + return calculateSumOfNumbers(userInput); + } + + // 문자열을 적절한 구분자로 분리한 후 숫자의 합을 계산하는 메소드 + public Integer calculateSumOfNumbers(String userInput) { + // \\n을 실제 줄바꿈 문자로 변환 + userInput = userInput.replace("\\n", "\n"); + + String content = userInput.substring(userInput.indexOf("\n") + 1); // 구분자 이후의 문자열 + + // 기본 구분자 + 커스텀 구분자 모두 포함해서 처리 + String delimiter = "[,:" + extractCustomDelimiter(userInput) + "]"; + // Model 단에 사용자의 입력값을 저장 + calculatorModel = new CalculatorModel(splitByDelimiter(content, delimiter)); + + // 숫자로 변환한 후 합계 계산 + return calculatorModel.getUserInputList().stream() + .map(Integer::parseInt) // 문자열을 Integer로 변환 + .reduce(0, Integer::sum); // 합계 + } + + // 구분자를 기준으로 문자열을 분리하는 메소드 + public static List splitByDelimiter(String userInput, String delimiter) { + return Arrays.stream(userInput.split(delimiter)) + .map(String::trim) // 공백 제거 + .peek(CalculatorService::validateNumber) // 숫자 유효성 검사 + .collect(Collectors.toList()); // 리스트로 변환 + } + + // 숫자 유효성 검사: 숫자가 아니거나 음수인 경우 예외 발생 + public static void validateNumber(String validateNumber) { + try { + Integer number = Integer.parseInt(validateNumber); + if (number < 0) { + throw new IllegalArgumentException("양수만 입력 가능합니다." + validateNumber); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("숫자가 아닙니다. " + validateNumber); + } + } + + private static class InnerCalculatorService { + + private static final CalculatorService INSTANCE = new CalculatorService(); + } + public static CalculatorService getInstance(){ + return InnerCalculatorService.INSTANCE; + } +} diff --git a/src/main/java/calculator/view/InputView.java b/src/main/java/calculator/view/InputView.java new file mode 100644 index 0000000000..7478ba9c53 --- /dev/null +++ b/src/main/java/calculator/view/InputView.java @@ -0,0 +1,16 @@ +package calculator.view; + +import static camp.nextstep.edu.missionutils.Console.*; + +import calculator.enums.UserInterfaceMsg; + +public class InputView { + public String requestInputCalcString() { + printMessage(UserInterfaceMsg.CALCULATOR_INTRO.getValue()); + return readLine(); + + } + public void printMessage(String message) { + System.out.println(message); + } +} diff --git a/src/main/java/calculator/view/OutputView.java b/src/main/java/calculator/view/OutputView.java new file mode 100644 index 0000000000..b1b79a96b9 --- /dev/null +++ b/src/main/java/calculator/view/OutputView.java @@ -0,0 +1,11 @@ +package calculator.view; + +import calculator.enums.UserInterfaceMsg; + +public class OutputView { + public void printResult(Integer output) { + System.out.println(String.format(UserInterfaceMsg.CALC_RESULT.getValue(), output)); + + + } +} diff --git a/src/test/java/calculator/ApplicationTest.java b/src/test/java/calculator/ApplicationTest.java index 93771fb011..80a1430668 100644 --- a/src/test/java/calculator/ApplicationTest.java +++ b/src/test/java/calculator/ApplicationTest.java @@ -1,6 +1,8 @@ package calculator; import camp.nextstep.edu.missionutils.test.NsTest; + +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; @@ -24,6 +26,15 @@ class ApplicationTest extends NsTest { ); } + @DisplayName("사용자의 입력값에서 기본 구분자와 커스텀 구분자를 함께 사용이 가능하도록 테스트 케이스 처리") + @Test + void 기본_및_커스텀_구분자_사용() { + assertSimpleTest(() -> { + run("//;\\n1,2;3"); + assertThat(output()).contains("결과 : 6"); + }); + } + @Override public void runMain() { Application.main(new String[]{}); diff --git a/src/test/java/calculator/CalculatorModelTest.java b/src/test/java/calculator/CalculatorModelTest.java new file mode 100644 index 0000000000..6dea9e809d --- /dev/null +++ b/src/test/java/calculator/CalculatorModelTest.java @@ -0,0 +1,31 @@ +package calculator; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import calculator.domain.CalculatorModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CalculatorModelTest { + CalculatorModel calculatorModel; + String inputString; + + @BeforeEach + void setUp() { + inputString = "1,2:3"; + } + + @Test + void saveUserInput() { + // given + List userInputList = new ArrayList<>(Arrays.asList(inputString.split("[,:]"))); + // when + CalculatorModel calculatorModel = new CalculatorModel(userInputList); + // then + assertThat(userInputList).isEqualTo(calculatorModel.getUserInputList()); + } +} diff --git a/src/test/java/calculator/CalculatorServiceTest.java b/src/test/java/calculator/CalculatorServiceTest.java new file mode 100644 index 0000000000..77668ff8da --- /dev/null +++ b/src/test/java/calculator/CalculatorServiceTest.java @@ -0,0 +1,41 @@ +package calculator; + +import static calculator.service.CalculatorService.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import calculator.service.CalculatorService; + +public class CalculatorServiceTest { + CalculatorService calculatorService; + + @BeforeEach + void setUp() { + calculatorService = CalculatorService.getInstance(); + } + + @DisplayName("문자열에 기본 구분자들(쉼표(,)와 콜론(:))을 구분자가 포함되어 있는지 체크") + @Test + void isContainsDefaultDelimiters() { + // give + String userInput = "1,2:3"; + // when + Boolean isContainsDefaultDelimiters = containsDefaultDelimiters((userInput)); + // then + assertThat(isContainsDefaultDelimiters).isEqualTo(true); + } + + @DisplayName("문자열에 커스텀 구분자(\"//\"와 \"\\n\" 사이에 위치하는 문자)가 비교하는 메소드") + @Test + void compareCustomDelimiters() { + // give + String userInput = "//;\n1;2;3"; + // when + String customDelimiters = extractCustomDelimiter((userInput)); + // then + assertThat(customDelimiters).isEqualTo(";"); + } +}