diff --git a/README.md b/README.md index bd90ef0247..d88e6209f9 100644 --- a/README.md +++ b/README.md @@ -1 +1,76 @@ -# java-calculator-precourse \ No newline at end of file +## 1주차 미션 - 문자열 덧셈 계산기 +### 🔍 진행 방식 +- 미션은 **기능 요구사항** , **프로그래밍 요구사항**, **과제 진행 요구사항** 세가지로 구성되어 있다. +- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다 +- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. +- 매주 진행할 미션은 화요일 오후 3시부터 확인할 수 있으며, 다음 주 월요일까지 구현을 완료하여 제출해야 한다. 제출은 일요일 오후 3시부터 가능하다. + - 정해진 시간을 지키지 않을 경우 미션을 제출하지 않은 것으로 간주한다. + - 종료 일시 이후에는 추가 푸시를 허용하지 않는다 + +### 💡 미션 제출 방식 +- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다. + - GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고해 + 제출한다. +- GitHub에 미션을 제출한 후 [우아한테크코스 지원](https://apply.techcourse.co.kr) 사이트에 접속하여 프리코스 과제를 제출한다. + - 자세한 방법은 [제출 가이드](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 참고 + - **Pull Request만 보내고 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.** + +### 🚨 과제 제출 전 체크 리스트 + +- 기능 구현을 모두 정상적으로 했더라도 **요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리**한다. +- 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공적으로 실행되는지 확인한다. +- **테스트가 실패할 경우 0점으로 처리**되므로, 반드시 확인 후 제출한다. + +### ✏️ 테스트 실행 가이드 +- 터미널에서 `java -version` 을 실행하여 Java 버전이 21인지 확인한다. Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 21로 실행되는지 확인한다. +- 터미널에서 Mac 또는 Linux 사용자의 경우 ` ./gradlew clean test ` 명령을 실행하여 모든 테스트가 아래와 같이 통과하는지 확인한다. +``` +BUILD SUCCESSFUL in 0s +``` + +### 🚀 기능 요구사항 +- 입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다 +- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다. + - (예) "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6 +- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다 + - (예) 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다 +- 사용자가 잘못된 값을 입력한 경우 `IllegalArgumentException` 을 발생시킨 후 애플리케이션은 종료 되어야 한다. + +### 🛠️ 구현 기능 목록 +- [x] 사용자로부터 문자열을 입력받는다 + - 문자열이 구분자와 양수로 구성되어 있는지 확인한다. + - 양수가 아닌 음수가 입력된 경우 -> `NEGATIVE_NUMBER_NOT_ALLOWED` 예외 발생 + - 아무것도 입력하지않은 경우에는 -> 0 을 반환한다. + - 공백만 입력하거나 공백을 문자에 포함한 경우의 -> `NEGATIVE_NUMBER_NOT_ALLOWED`예외 발생 + - 숫자가 아닌 값을 입력할 경우 ->`ENTERED_NON_NUMERIC_VALUE`예외 발생 + - 문자열이 잘못된 값일 경우 예외를 발생 시킨 후 애플리케이션을 종료한다. +- [x] 커스텀 구분자를 지정한다 + - 커스텀 구분자는 문자열 앞부분의 `//` 와 `\n` 사이에 위치하는 문자를 커스텀 구분자로 사용한다. + - (예) `//;\n1;2;3` 과 같이 값을 입력할 경우 커스텀 구분자는 `;` 이며 결과값은 6이 반환되어야 한다. + - 커스텀 구분자는 1글자 이상일 수 있다. + - (예) `//;.\n1;.2;.3 ` 일때 커스텀 구분자는 `;. ` + - 빈 문자열(1글자 이하)인 경우 -> `DELIMITER_MISSING`예외 발생 +- 기본 구분자 `,` `;` +- [x] 문자열을 구분자를 기준으로 분리한다. +- [x] 구분자를 기준으로 분리한 각 숫자를 합친다. +- [x] 각 숫자를 합한 결과를 출력한다. + +**실행 결과 예시** +``` +덧셈할 문자열을 입력해 주세요 +//;\n1;2;3 +결과 : 6 +``` + +### 🎯 프로그래밍 요구 사항 +- JDK 21 버전에서 실행 가능해야 한다. +- 프로그램 실행의 시작점은 Application의 `main()`이다. +- build.gradle 파일은 변경할 수 없으며, 제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다. +- 프로그램 종료 시 System.exit()를 호출하지 않는다. +- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. +- 기본적으로 [Java Style Guide](https://github.com/woowacourse/woowacourse-docs/tree/main/styleguide/java)를 원칙으로 한다. + +### 📚 라이브러리 +- `camp.nextstep.edu.missionutils `에서 제공하는 Console API를 사용하여 구현해야 한다. + - 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 readLine()을 활용한다 diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..bdd2b5b2cb 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,10 @@ package calculator; +import calculator.controller.CalculatorController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + CalculatorController calculatorController = new CalculatorController(); + calculatorController.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..94a7897276 --- /dev/null +++ b/src/main/java/calculator/controller/CalculatorController.java @@ -0,0 +1,24 @@ +package calculator.controller; + +import java.util.List; + +import calculator.domain.Separator; +import calculator.view.Input; +import calculator.view.Output; + +public class CalculatorController { + public void run(){ + Input input = new Input(); + String value = input.readInput(); + + //Separator + Separator separator = new Separator(value); + List numbers = separator.getNumbers(); + + //print + Output output = new Output(); + output.output(numbers); + + + } +} diff --git a/src/main/java/calculator/domain/Delimiter.java b/src/main/java/calculator/domain/Delimiter.java new file mode 100644 index 0000000000..b462c605cf --- /dev/null +++ b/src/main/java/calculator/domain/Delimiter.java @@ -0,0 +1,35 @@ +package calculator.domain; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import calculator.message.ErrorMessage; + +public class Delimiter { + private static final Pattern CUSTOM_PATTERN = Pattern.compile("//(.*?)\\\\?n(.*)", Pattern.DOTALL); + + private final String regex; + + private Delimiter (String regex) { + this.regex = regex; + } + public static Delimiter of(String value) { + Matcher matcher = CUSTOM_PATTERN.matcher(value); + + if (matcher.matches()) { + String custom = matcher.group(1); + if(custom.isEmpty()) { //커스텀 구분자가 빈 문자열이면 예외처리 + throw new IllegalArgumentException(ErrorMessage.DELIMITER_MISSING); + } + return new Delimiter(Pattern.quote(custom)); + } + return new Delimiter("[,\n]"); + } + public String extractNumbers(String value) { + Matcher matcher = CUSTOM_PATTERN.matcher(value); + return matcher.matches() ? matcher.group(2) : value; + } + public String getRegex() { + return regex; + } +} diff --git a/src/main/java/calculator/domain/Separator.java b/src/main/java/calculator/domain/Separator.java new file mode 100644 index 0000000000..630382e608 --- /dev/null +++ b/src/main/java/calculator/domain/Separator.java @@ -0,0 +1,40 @@ +package calculator.domain; + +import java.util.ArrayList; +import java.util.List; + +import calculator.message.ErrorMessage; + +public class Separator { + + private final Delimiter delimiter; + private final List numbers; + + public Separator(String value) { + this.delimiter = Delimiter.of(value);//구분자 객체 생성 + this.numbers = parseNumbers(delimiter.extractNumbers(value)); //숫자 변환 + } + + private List parseNumbers(String nums) { + List result = new ArrayList<>(); + for (String token: nums.split(delimiter.getRegex())){ + if(!token.isEmpty()) { + int num; + try{ + num = Integer.parseInt(token); + }catch (NumberFormatException e){ //숫자가 아닌 경우 예외처리 + throw new IllegalArgumentException(ErrorMessage.ENTERED_NON_NUMERIC_VALUE); + } + //음수인경우 예외처리 + if (num < 0) { + throw new IllegalArgumentException(ErrorMessage.NEGATIVE_NUMBER_NOT_ALLOWED); + } + result.add(num); + } + } + return result; + } + public List getNumbers(){ + return numbers; + } +} diff --git a/src/main/java/calculator/message/ErrorMessage.java b/src/main/java/calculator/message/ErrorMessage.java new file mode 100644 index 0000000000..809f0c0877 --- /dev/null +++ b/src/main/java/calculator/message/ErrorMessage.java @@ -0,0 +1,14 @@ +package calculator.message; + +public abstract class ErrorMessage { + + //입출력 관련 + public final static String TRIM_ERROR = "문자열에 공백이 포함될 수 없습니다"; + public final static String FORMAT_INCORRECT = "입력 형식이 잘못되었습니다"; + //구분자 , 입력값 검증 관련 + public final static String ENTERED_NON_NUMERIC_VALUE = "숫자가 아닌 값이 포함되어 있습니다"; + public final static String NEGATIVE_NUMBER_NOT_ALLOWED = "음수는 입력할 수 없습니다"; + public final static String DELIMITER_MISSING = "커스텀 구분자는 한자리 이상 포함되어 있어야 합니다"; + + +} diff --git a/src/main/java/calculator/message/SuccessMessage.java b/src/main/java/calculator/message/SuccessMessage.java new file mode 100644 index 0000000000..cb3be04c92 --- /dev/null +++ b/src/main/java/calculator/message/SuccessMessage.java @@ -0,0 +1,10 @@ +package calculator.message; + +public abstract class SuccessMessage { + + + //입출력 관련 + public final static String INPUT_MESSAGE = "덧셈할 문자열을 입력해 주세요"; + public final static String OUTPUT_MESSAGE = "결과 :"; + +} diff --git a/src/main/java/calculator/view/Input.java b/src/main/java/calculator/view/Input.java new file mode 100644 index 0000000000..5eb9c4199b --- /dev/null +++ b/src/main/java/calculator/view/Input.java @@ -0,0 +1,23 @@ +package calculator.view; + +import calculator.message.ErrorMessage; +import calculator.message.SuccessMessage; +import camp.nextstep.edu.missionutils.Console; + +public class Input { + public String readInput() { + System.out.println(SuccessMessage.INPUT_MESSAGE); + String input = Console.readLine(); + + //입력값이 null인 경우 0을 반환 + if (input == null || input.isEmpty()) { + return "0"; + } + //공백만 입력하거나 공백을 문자에 포함한 경우의 예외 처리 + if (input.trim().isEmpty() || input.contains(" ") ) { + throw new IllegalArgumentException(ErrorMessage.TRIM_ERROR); + } + + return input; + } +} diff --git a/src/main/java/calculator/view/Output.java b/src/main/java/calculator/view/Output.java new file mode 100644 index 0000000000..1e8c339da7 --- /dev/null +++ b/src/main/java/calculator/view/Output.java @@ -0,0 +1,15 @@ +package calculator.view; + +import java.util.List; + +import calculator.message.SuccessMessage; + +public class Output { + public void output(List numbers) { + int sum = 0; + for (Integer number : numbers) { + sum += number; + } + System.out.println(SuccessMessage.OUTPUT_MESSAGE + " " + sum); } +} + diff --git a/src/test/java/calculator/DelimiterTest.java b/src/test/java/calculator/DelimiterTest.java new file mode 100644 index 0000000000..e2a26610dd --- /dev/null +++ b/src/test/java/calculator/DelimiterTest.java @@ -0,0 +1,54 @@ +package calculator; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.regex.Pattern; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import calculator.domain.Delimiter; +import calculator.message.ErrorMessage; + +class DelimiterTest { + + @DisplayName("기본 구분자 사용") + @Test + void defaultDelimiterTest() { + Delimiter delimiter = Delimiter.of("1,2,3"); + assertEquals("[,\n]", delimiter.getRegex()); + + String numbers = delimiter.extractNumbers("1,2,3"); + assertEquals("1,2,3", numbers); + } + + @DisplayName("커스텀 구분자 적용") + @ParameterizedTest + @ValueSource(strings = {"//;\\n1;2;3", "//*\\n4*5*6"}) + void customDelimiterTest(String input) { + Delimiter delimiter = Delimiter.of(input); + + String numbers = delimiter.extractNumbers(input); + assertNotNull(numbers); + assertTrue(Pattern.matches("\\d+(;|\\*)\\d+(;|\\*)\\d+", numbers)); + } + + @DisplayName("커스텀 구분자가 비어있으면 예외") + @ParameterizedTest + @ValueSource(strings = {"//\\n1,2,3", "//\\n"}) + void emptyCustomDelimiterTest(String input) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> Delimiter.of(input)); + assertEquals(ErrorMessage.DELIMITER_MISSING, exception.getMessage()); + } + + @DisplayName("잘못된 포맷 입력 시 기본 처리") + @Test + void invalidFormatTest() { + Delimiter delimiter = Delimiter.of("1#2#3"); + assertEquals("[,\n]", delimiter.getRegex()); + assertEquals("1#2#3", delimiter.extractNumbers("1#2#3")); + } +} diff --git a/src/test/java/calculator/SeparatorTest.java b/src/test/java/calculator/SeparatorTest.java new file mode 100644 index 0000000000..9176d76ca4 --- /dev/null +++ b/src/test/java/calculator/SeparatorTest.java @@ -0,0 +1,82 @@ +package calculator; + + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import calculator.domain.Separator; +import calculator.message.ErrorMessage; + +class SeparatorTest { + + @DisplayName("기본 구분자 - 쉼표") + @ParameterizedTest + @ValueSource(strings = {"1,2,3", ",1,2,3"}) + void basic_separator_comma(String inputs) { + Separator separator = new Separator(inputs); + List numbers = separator.getNumbers(); + + assertThat(numbers).isEqualTo(List.of(1, 2, 3)); + } + + @DisplayName("기본 구분자 - 개행") + @ParameterizedTest + @ValueSource(strings = {"1\n2\n3","\n1\n2\n3"}) + void basic_separator_newline(String inputs) { + //given + + //when + Separator separator = new Separator(inputs); + List numbers = separator.getNumbers(); + + //then + assertThat(numbers).isEqualTo(List.of(1, 2, 3)); + } + + @DisplayName("기본 구분자 - 세미클론") + @Test + void basic_seperator_semicolon() { + Separator separator = new Separator("//;\\n1;2;3"); + List numbers = separator.getNumbers(); + assertEquals(List.of(1, 2, 3), numbers); + } + + @DisplayName("음수값 예외") + @ParameterizedTest + @ValueSource(strings = {"1,-2,3","-1,1,1","-1,-2,3"}) + void exception_negative_number(String inputs) { + //given + + //when, then + assertThatThrownBy(() -> new Separator(inputs)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.NEGATIVE_NUMBER_NOT_ALLOWED); + + // IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + // () -> new Separator("1,-2,3")); + // assertEquals(ErrorMessage.NEGATIVE_NUMBER_NOT_ALLOWED, exception.getMessage()); + } + + @DisplayName("숫자가 아닌 값이 들어왔을때 예외 처리") + @ParameterizedTest + @ValueSource(strings = {"//;\\na;2;3","a,2:3","@,2:3" }) + void exception_entered_non_numeric_value(String inputs) { + //given + + //when, then + assertThatThrownBy(() -> new Separator(inputs)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.ENTERED_NON_NUMERIC_VALUE); + + // IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + // () -> new Separator("1,a,3")); + // assertEquals(ErrorMessage.ENTERED_NON_NUMERIC_VALUE, exception.getMessage()); + } +}