diff --git a/README.md b/README.md index bd90ef0247..386df9fc42 100644 --- a/README.md +++ b/README.md @@ -1 +1,152 @@ -# java-calculator-precourse \ No newline at end of file +# 기능목록 + +## 도메인 + +입력받은 문자열을 구분자를 이용해 나눈다. + - 구분자는 기본적으로 쉼표(,)와 콜론(:)이 포함되어야 한다. + - 문자열 앞부분의 //와 \n사이에 위치하는 문자를 커스텀 구분자로 추가해야한다. + - 커스텀 구분자는 숫자의 특성상 부호를 표기하는 +, -와 소숫점을 표현하는 .은 잘못된 것으로 처리하여 예외를 던진다. + - 구분자라는 특성상 커스텀 구분자의 속하는 문자열의 길이가 2이상일 경우 각각 하나의 문자를 커스텀 구분자로 사용한다. + - (예시) "//;^$\n1;2^3" 의 경우 ;^$라는 길이 3짜리 구분자를 사용하는 것이 아닌 ; ^ $ 이 셋을 각각 하나의 구분자로 사용한다. + - 기본 구분자 쉼표(,)와 콜론(:) 그리고 추가된 커스텀 문자를 제외한 문자열이 나타난 경우 잘못된 것으로 처리하여 예외를 던진다. + - 커스텀 구분자를 등록한 이후 숫자와 구분자가 있어야할 부분에서 숫자와 구분자가 번갈아 등장하지 않고 연속으로 등장한 경우 예외를 던진다. + - 이는 공백문자열로 나타날 것이므로, 나눠진 문자열의 목록을 숫자로 변환하는 부분에서 예외를 던진다. + +
+ + 숫자의 제한조건은 양수라는 것 외에는 없다. 따라서 확장성을 위하여 문자를 숫자로 변환하는 객체를 따로 관리한다. + - 나눠진 문자열을 숫자로 변환한다. + - 변환한 숫자들을 더하여 문자로 반환한다. + +## 출력 + - 프로그램 실행시 안내메시지를 출력한다. + - 결과를 출력한다. + +## 테스트 + - 기본 구분자를 통해 문자열이 잘 나누어지는지 확인한다. + - 커스텀 구분자가 추가된 경우 문자열이 잘 나누어지는지 확인한다. + - 여러개의 커스텀 구분자가 추가된 경우도 확인한다. + - +, -, .이 커스텀 구분자로 포함될 경우 예외를 던진다. + - 구분자가 연속으로 등장한 경우 예외를 던진다. + +




+ + + + + + + + + + +--- + + + +## 학습 목표 +- Git, GitHub, IDE 등 실제 개발을 위한 환경에 익숙해진다. +- 교육 분야에 맞는 프로그래밍 언어를 사용하여 간단한 문제를 해결한다. + + +# 프리코스 진행 방식 + +## 진행 방식 + +- 미션은 **과제 진행 요구 사항, 기능 요구 사항, 프로그래밍 요구 사항** 세 가지로 구성되어 있다. +- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다. +- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. +- 매주 진행할 미션은 화요일 오후 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#제출-가이드) 를 참고한다. + - 과제를 수행하면서 느낀 점, 배운 점, 많은 시간을 투자한 부분 등 자유롭게 작성한다. + +## 과제 제출 전 체크리스트 + +- 기능을 올바르게 구현했더라도 **요구 사항에 명시된 출력 형식**을 따르지 않으면 0점을 받게 된다. +- 기능 구현을 완료한 후 아래 가이드에 따라 모든 테스트가 성공적으로 실행되는지 확인한다. +- **테스트가 실패하면 점수가 0점**이 되므로 제출하기 전에 반드시 확인한다. + +## 테스트 실행 가이드 +- 터미널에서 `java -version`을 실행하여 Java 버전이 21인지 확인한다. Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 21로 실행되는지 확인한다. +- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고, Windows 사용자의 경우 `gradlew.bat clean test`
+또는 .\gradlew.bat clean test 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다. + +``` +BUILD SUCCESSFUL in 0s +``` + + + +
+ +# 문자열 덧셈 계산기 + +--- + +## 과제 진행 요구 사항 + +--- +- 미션은 [문자열 덧셈 계산기 저장소](https://github.com/woowacourse-precourse/java-calculator-7)를 포크하고 클론하는 것으로 시작한다. +- **기능을 구현하기 전 `README.md`에 구현할 기능 목록을 정리**해 추가한다. + - Git의 커밋 단위는 앞 단계에서 `README.md`에 정리한 기능 목록 단위로 추가한다. + - [AngularJS Git Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)을 참고해 커밋 메시지를 작성한다. +- 자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다. + + + +## 기능 요구 사항 +입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다. +- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다. + - 예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6 +- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다. + - 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다. +- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. + +### 입출력 요구 사항 +#### 입력 +- 구분자와 양수로 구성된 문자열 + +#### 출력 +- 덧셈 결과 +``` +결과 : 6 +``` +#### 실행 결과 예시 +``` +덧셈할 문자열을 입력해 주세요. +1,2:3 +결과 : 6 +``` + + +## 프로그래밍 요구 사항 + +--- + +- JDK 21 버전에서 실행 가능해야 한다. +- 프로그램을 실행하는 시작점은 `Application`의 `main()`이다. +- build.gradle 파일은 변경할 수 없으며, **제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.** +- 프로그램 종료 시 System.exit()를 호출하지 않는다. +- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. + - 기본적으로 Java Style Guide를 원칙으로 한다. + +### 라이브러리 + +- `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..5b3517e5a0 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -2,6 +2,7 @@ public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + Calculator calculator = new Calculator(); + calculator.run(); } } diff --git a/src/main/java/calculator/Calculator.java b/src/main/java/calculator/Calculator.java new file mode 100644 index 0000000000..1dbe896c70 --- /dev/null +++ b/src/main/java/calculator/Calculator.java @@ -0,0 +1,32 @@ +package calculator; + +import calculator.domain.SumCalculator; +import calculator.domain.stringsplitter.CustomStringSplitter; +import calculator.domain.stringsplitter.DefaultStringSplitter; +import calculator.domain.StringSplitterManager; +import camp.nextstep.edu.missionutils.Console; + +import java.util.List; + +public class Calculator { + + private final CalculatorView view = new CalculatorView(); + private final StringSplitterManager splitterManager = new StringSplitterManager( + List.of( + new DefaultStringSplitter(), + new CustomStringSplitter() + ) + ); + + public void run() { + String input = view.readInput(); + + List splitStrings = splitterManager.getSplitStrings(input); + SumCalculator sumCalculator = new SumCalculator(splitStrings); + String result = sumCalculator.calculateSum(); + + view.printOutput(result); + + Console.close(); + } +} diff --git a/src/main/java/calculator/CalculatorView.java b/src/main/java/calculator/CalculatorView.java new file mode 100644 index 0000000000..cbde401d5c --- /dev/null +++ b/src/main/java/calculator/CalculatorView.java @@ -0,0 +1,15 @@ +package calculator; + +import camp.nextstep.edu.missionutils.Console; + +public class CalculatorView { + + public String readInput() { + System.out.println("덧셈할 문자열을 입력해 주세요."); + return Console.readLine(); + } + + public void printOutput(String str) { + System.out.println("결과 : " + str); + } +} diff --git a/src/main/java/calculator/domain/StringSplitterManager.java b/src/main/java/calculator/domain/StringSplitterManager.java new file mode 100644 index 0000000000..838047ac87 --- /dev/null +++ b/src/main/java/calculator/domain/StringSplitterManager.java @@ -0,0 +1,23 @@ +package calculator.domain; + +import calculator.domain.stringsplitter.StringSplitter; + +import java.util.List; + +public class StringSplitterManager { + + private final List stringSplitters; + + public StringSplitterManager(List stringSplitters) { + this.stringSplitters = stringSplitters; + } + + public List getSplitStrings(String str) { + StringSplitter stringSplitter = stringSplitters.stream() + .filter(splitter -> splitter.canSupport(str)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(str)); + + return stringSplitter.splitString(str); + } +} diff --git a/src/main/java/calculator/domain/SumCalculator.java b/src/main/java/calculator/domain/SumCalculator.java new file mode 100644 index 0000000000..a81f8bc34e --- /dev/null +++ b/src/main/java/calculator/domain/SumCalculator.java @@ -0,0 +1,34 @@ +package calculator.domain; + +import java.math.BigDecimal; +import java.util.List; + +public class SumCalculator { + + private final List strings; + + public SumCalculator(List strings) { + this.strings = strings; + } + + public String calculateSum() { + BigDecimal sum = strings.stream() + .map(str -> convertToBigDecimal(str)) + .reduce(new BigDecimal("0"), (o1, o2) -> o1.add(o2)); + + return sum.toString(); + } + + private BigDecimal convertToBigDecimal(String str) { + if (str.equals("0")) { + throw new IllegalArgumentException(strings.toString()); + } + + String positiveNumberPattern = "^[+]?[0-9]+([.][0-9]+)?"; + if (!str.matches(positiveNumberPattern)) { + throw new IllegalArgumentException(strings.toString()); + } + + return new BigDecimal(str); + } +} diff --git a/src/main/java/calculator/domain/stringsplitter/CustomStringSplitter.java b/src/main/java/calculator/domain/stringsplitter/CustomStringSplitter.java new file mode 100644 index 0000000000..efb3718212 --- /dev/null +++ b/src/main/java/calculator/domain/stringsplitter/CustomStringSplitter.java @@ -0,0 +1,93 @@ +package calculator.domain.stringsplitter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CustomStringSplitter implements StringSplitter { + + private static final String CUSTOM_DELIMITER_HEADER = "\\/\\/"; + private static final String CUSTOM_DELIMITER_FOOTER = "[(\n)(\\\\n)]"; + + @Override + public boolean canSupport(String str) { + String patternRegex = generatePatternRegex(); + Pattern pattern = Pattern.compile(patternRegex); + Matcher matcher = pattern.matcher(str); + boolean isFound = matcher.matches(); + + if (isFound) { + return true; + } + return false; + } + + @Override + public List splitString(String str) { + String patternRegex = generatePatternRegex(); + Pattern pattern = Pattern.compile(patternRegex); + Matcher matcher = pattern.matcher(str); + matcher.matches(); + + String customDelimiter = matcher.group(1); + validateCustomDelimiter(customDelimiter, str); + String conjoinedDelimiter = DEFAULT_DELIMITER + customDelimiter; + + String numbersAndDelimiter = matcher.group(2); + numbersAndDelimiter = replaceBackSlash(customDelimiter, numbersAndDelimiter); + List result = splitSubstring(conjoinedDelimiter, numbersAndDelimiter); + validateContinuousDelimiter(result, str); + + return result; + } + + private String replaceBackSlash(String customDelimiter, String numbersAndDelimiter) { + if (customDelimiter.contains("\\")) { + numbersAndDelimiter = numbersAndDelimiter.replace("\\", String.valueOf(DEFAULT_DELIMITER.charAt(0))); + } + return numbersAndDelimiter; + } + + private String generatePatternRegex() { + StringBuilder delimiterBuilder = new StringBuilder(); + + delimiterBuilder.append("^") + .append(CUSTOM_DELIMITER_HEADER) + .append("([^0-9]+)") + .append(CUSTOM_DELIMITER_FOOTER) + .append("(.+)"); + + return delimiterBuilder.toString(); + } + + private void validateCustomDelimiter(String customDelimiter, String str) { + boolean hasInvalidDelimiter = Arrays.stream(customDelimiter.split("")) + .anyMatch(s -> INVALID_DELIMITER.contains(s)); + + if (hasInvalidDelimiter) { + throw new IllegalArgumentException(str); + } + } + + private List splitSubstring(String conjoinedDelimiter, String numbersAndDelimiter) { + StringTokenizer stringTokenizer = new StringTokenizer(numbersAndDelimiter, conjoinedDelimiter); + + List strings = new ArrayList<>(); + while (stringTokenizer.hasMoreTokens()) { + strings.add(stringTokenizer.nextToken()); + } + return strings; + } + + private void validateContinuousDelimiter(List split, String str) { + boolean hasContinuousDelimiter = split.stream() + .anyMatch(s -> s.isEmpty()); + + if (hasContinuousDelimiter) { + throw new IllegalArgumentException(str); + } + } +} diff --git a/src/main/java/calculator/domain/stringsplitter/DefaultStringSplitter.java b/src/main/java/calculator/domain/stringsplitter/DefaultStringSplitter.java new file mode 100644 index 0000000000..2fb6d1cfd8 --- /dev/null +++ b/src/main/java/calculator/domain/stringsplitter/DefaultStringSplitter.java @@ -0,0 +1,27 @@ +package calculator.domain.stringsplitter; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DefaultStringSplitter implements StringSplitter { + + @Override + public boolean canSupport(String str) { + Pattern pattern = Pattern.compile("^[0-9]+([" + DEFAULT_DELIMITER + "][0-9]+)*$"); + Matcher matcher = pattern.matcher(str); + return matcher.find(); + } + + @Override + public List splitString(String str) { + StringTokenizer stringTokenizer = new StringTokenizer(str, DEFAULT_DELIMITER); + List strings = new ArrayList<>(); + while (stringTokenizer.hasMoreTokens()) { + strings.add(stringTokenizer.nextToken()); + } + return strings; + } +} diff --git a/src/main/java/calculator/domain/stringsplitter/StringSplitter.java b/src/main/java/calculator/domain/stringsplitter/StringSplitter.java new file mode 100644 index 0000000000..e765c87605 --- /dev/null +++ b/src/main/java/calculator/domain/stringsplitter/StringSplitter.java @@ -0,0 +1,14 @@ +package calculator.domain.stringsplitter; + +import java.util.List; + +public interface StringSplitter { + + static final String DEFAULT_DELIMITER = ":,"; + static final String INVALID_DELIMITER = "+-."; + + boolean canSupport(String str); + + List splitString(String str); + +} diff --git a/src/test/java/calculator/domain/CustomStringSplitterTest.java b/src/test/java/calculator/domain/CustomStringSplitterTest.java new file mode 100644 index 0000000000..904f50d68d --- /dev/null +++ b/src/test/java/calculator/domain/CustomStringSplitterTest.java @@ -0,0 +1,98 @@ +package calculator.domain; + +import calculator.domain.stringsplitter.CustomStringSplitter; +import calculator.domain.stringsplitter.StringSplitter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +class CustomStringSplitterTest { + + StringSplitter stringSplitter = new CustomStringSplitter(); + + @ParameterizedTest + @CsvSource({ + "'//;\\\n1'", + "'//;\\n1'", + "'//;\n1'", + "'///;*?!@#$\n1'" + }) + void 커스텀_문자열_지원_TRUE(String input) { + Assertions.assertTrue(stringSplitter.canSupport(input)); + } + + @ParameterizedTest + @CsvSource({ + "'//0\n3:4'", + "'3:4//i\n'" + }) + void 커스텀_문자열_지원_FALSE(String input) { + Assertions.assertFalse(stringSplitter.canSupport(input)); + } + + @ParameterizedTest + @CsvSource({ + "'//i\\n2\\23i4', '2', '23', '4'", + "'///i\n2/23i4', '2', '23', '4'" + }) + void 커스텀_문자열_나누기(String input, String expected1, String expected2, String expected3) { + List strings = stringSplitter.splitString(input); + + Assertions.assertTrue(strings.contains(expected1)); + Assertions.assertTrue(strings.contains(expected2)); + Assertions.assertTrue(strings.contains(expected3)); + Assertions.assertTrue(strings.size() == 3); + } + + @ParameterizedTest + @CsvSource({ + "'//*\n0*123,456:7890', '0', '123', '456', '7890'", + "'//**&^%\n0*123&456^7890', '0', '123', '456', '7890'" + }) + void 커스텀_문자열_나누기_기본_구분자_혼합(String input, String expected1, String expected2, String expected3, String expected4) { + List strings = stringSplitter.splitString(input); + + Assertions.assertTrue(strings.contains(expected1)); + Assertions.assertTrue(strings.contains(expected2)); + Assertions.assertTrue(strings.contains(expected3)); + Assertions.assertTrue(strings.contains(expected4)); + Assertions.assertTrue(strings.size() == 4); + } + + @ParameterizedTest + @CsvSource({ + "'//*\n1', '1'", + "'//i\n2345678590', '2345678590'" + }) + void 나누어진_크기가_1인_문자열(String input, String expected1) { + System.out.println(input); + List strings = stringSplitter.splitString(input); + + Assertions.assertTrue(strings.contains(expected1)); + Assertions.assertTrue(strings.size() == 1); + } + + @ParameterizedTest + @CsvSource({ + "'//-\n12:34'", + "'//.\n12:34'", + "'//*-\n12:34'", + "'//+\n1245'" + }) + void 허용되지_않는_커스텀_구분자_canSupport_메소드_통과(String input) { + Assertions.assertTrue(stringSplitter.canSupport(input)); + } + + @ParameterizedTest + @CsvSource({ + "'//-\n12:34'", + "'//.\n12:34'", + "'//*-\n12:34'", + "'//+\n1245'" + }) + void 허용되지_않는_커스텀_구분자_예외_발생(String input) { + Assertions.assertThrows(IllegalArgumentException.class, () -> stringSplitter.splitString(input)); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/domain/DefaultStringSplitterTest.java b/src/test/java/calculator/domain/DefaultStringSplitterTest.java new file mode 100644 index 0000000000..0647f64ecb --- /dev/null +++ b/src/test/java/calculator/domain/DefaultStringSplitterTest.java @@ -0,0 +1,52 @@ +package calculator.domain; + + +import calculator.domain.stringsplitter.DefaultStringSplitter; +import calculator.domain.stringsplitter.StringSplitter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +public class DefaultStringSplitterTest { + + StringSplitter stringSplitter = new DefaultStringSplitter(); + + @ParameterizedTest + @CsvSource({ + "'1,2'", + "'1:2:3'", + "'45,67:89'" + }) + void 기본_문자열_지원_TRUE(String input) { + Assertions.assertTrue(stringSplitter.canSupport(input)); + } + + @ParameterizedTest + @CsvSource({ + "6:3:2, '6', '3', '2'", + "'12,34,56', '12', '34', '56'", + "'100:200,300', '100', '200', '300'", + }) + void 기본_문자열_나누기(String input, String expected1, String expected2, String expected3) { + List strings = stringSplitter.splitString(input); + + Assertions.assertTrue(strings.contains(expected1)); + Assertions.assertTrue(strings.contains(expected2)); + Assertions.assertTrue(strings.contains(expected3)); + Assertions.assertTrue(strings.size() == 3); + } + + @ParameterizedTest + @CsvSource({ + "'1', '1'", + "'234567890', '234567890'" + }) + void 나누어진_크기가_1인_문자열(String input, String expected1) { + List strings = stringSplitter.splitString(input); + + Assertions.assertTrue(strings.contains(expected1)); + Assertions.assertTrue(strings.size() == 1); + } +} diff --git a/src/test/java/calculator/domain/SumCalculatorTest.java b/src/test/java/calculator/domain/SumCalculatorTest.java new file mode 100644 index 0000000000..28a0544411 --- /dev/null +++ b/src/test/java/calculator/domain/SumCalculatorTest.java @@ -0,0 +1,59 @@ +package calculator.domain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class SumCalculatorTest { + + List list = new ArrayList<>(); + SumCalculator sumCalculator = new SumCalculator(list); + + @BeforeEach + void init() { + list.clear(); + } + + @Test + void 계산_테스트() { + list.add("1"); + list.add("3.0123456789"); + + String result = sumCalculator.calculateSum(); + + Assertions.assertTrue(result.equals("4.0123456789")); + } + + @Test + void 계산_테스트_2() { + list.add("0123"); + list.add("0456"); + + String result = sumCalculator.calculateSum(); + + Assertions.assertTrue(result.equals("579")); + } + + @Test + void 계산_테스트_음수() { + list.add("-1"); + list.add("1234567890"); + + Assertions.assertThrows(IllegalArgumentException.class, () -> sumCalculator.calculateSum()); + + } + + @Test + void 계산_테스트_0() { + list.add("1"); + list.add("0"); + + Assertions.assertThrows(IllegalArgumentException.class, () -> sumCalculator.calculateSum()); + + } +} \ No newline at end of file