Skip to content

Commit

Permalink
[Step2] 문자열 덧셈 계산기 (#1578)
Browse files Browse the repository at this point in the history
* docs: 문자열 덧셈 계산기 기능요구사항 정의

* feat: 컴마(,)구분자로 입력한 경우의 테스트 코드 추가

* refactor(Main): 불필요한 사용자 UI 제거

* refactor: 클래스 이름변경(CalculatorModel > AddCalculatorModel)

* refactor: 메소드(calculate) 추출

* feat: “//”와 “\n” 문자 사이에 커스텀 구분자 관련 테스트 코드 추가

* refactor: 메소드 시그니쳐 변수명 변경

* refactor: polishing inline variable

* refactor: 테스트 코드 간결하게 수정

* style: 불필요한 개행 문자 제거

* refactor:코드 피드백 반영

- requiredNumber > validateString 수정
- 문자 단위가 아닌, 문자열 단위로 체크되도록 수정
- CustomDelimiterMatcher 이름 변경 및 내부 코드 변경
- AddCalculatorModel 내 불필요한 클래스 변수 제거
  • Loading branch information
LenKIM committed May 23, 2021
1 parent 67169c9 commit 2a8e097
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 1 deletion.
52 changes: 51 additions & 1 deletion README.md
Expand Up @@ -6,4 +6,54 @@
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)

---

# [2단계] 문자열 덧셈 계산기를 통한 TDD/리팩토링 실습

## 기능 요구사항

- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환 (예: “” => 0, "1,2" => 3, "1,2,3" => 6, “1,2:3” => 6)
- 앞의 기본 구분자(쉼표, 콜론)외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 “//”와 “\n” 사이에 위치하는 문자를 커스텀 구분자로 사용한다. 예를 들어 “//;\n1;2;3”과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
- 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw한다.

## 프로그래밍 요구사항

- indent(들여쓰기) depth를 2단계에서 1단계로 줄여라.
- depth의 경우 if문을 사용하는 경우 1단계의 depth가 증가한다. if문 안에 while문을 사용한다면 depth가 2단계가 된다.
- 메소드의 크기가 최대 10라인을 넘지 않도록 구현한다.
- method가 한 가지 일만 하도록 최대한 작게 만들어라.
- else를 사용하지 마라.

## 기능 목록 리스트

- [x] 빈 문자열 또는 null 값을 입력할 경우 0을 반환
- [x] 숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환
- [x] 숫자 두개를 컴마(,) 구분자로 입력할 경우 두 숫자의 합을 반환(예: "1,2")
- [x] int의 범위를 넘어가는 문자일 경우
- [x] 구분자를 컴마(,) 뿐만 아니라 콜론(:)을 사용할 수 있다.
- [x] 컴마만 사용된 문자열
- [x] 콜론만 사용된 문자열
- [x] 혼합으로 사용된 문자열
- [x] “//”와 “\n” 문자 사이에 커스텀 구분자를 지정할 수 있다.
- [x] “//”와 “\n” 문자 사이의 문자열을 찾아야 한다.
- [x] 찾은 문자열이 커스텀 구분자로 이어져야 한다.
- [x] 커스텀 구분자는 문자 제한이 없다.
- [x] 음수 또는 문자를 전달할 경우 RuntimeException 예외가 발생해야 한다.
- [x] 커스텀 구분자 외 문자가 음수 또는 문자일 경우 RuntimeException 예외가 발생
- [x] 콤마(,), 콜론(:) 외 문자가 음수 또는 문자일 경우 RuntimeException 예외가 발생



# [3단계] 로또(자동)

// TODO

# [4단계] 로또(2등)

// TODO

# [5단계] 로또(수동)

// TODO
27 changes: 27 additions & 0 deletions src/main/java/calculator/AddCalculatorModel.java
@@ -0,0 +1,27 @@
package calculator;

import static calculator.StringUtils.*;

public final class AddCalculatorModel {
private final static String DEFAULT_DELIMITER_REGEX = ",|:";
private long sum = 0L;

public long execute(String userInput) {
if (isBlank(userInput)) {
return sum;
}

if (CustomDelimiterMatcher.hasDelimiter(userInput)) {
String[] splitTokens = CustomDelimiterMatcher.getSplitTokens(userInput);
return calculate(StringUtils.convertStringToLong(splitTokens));
}
return calculate(StringUtils.convertStringToLong(userInput.split(DEFAULT_DELIMITER_REGEX)));
}

private long calculate(Long[] longs) {
for (Long l : longs) {
this.sum += l;
}
return sum;
}
}
31 changes: 31 additions & 0 deletions src/main/java/calculator/CustomDelimiterMatcher.java
@@ -0,0 +1,31 @@
package calculator;

import static java.util.Objects.*;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class CustomDelimiterMatcher {

private final Matcher matcher;
private final static Pattern CUSTOM_DELIMITER_PATTERN = Pattern.compile("//(.)\n(.*)");

private CustomDelimiterMatcher(Matcher matcher) {
requireNonNull(matcher);
this.matcher = matcher;
}

public static String[] getSplitTokens(String userInput) {
Matcher matcher = new CustomDelimiterMatcher(CUSTOM_DELIMITER_PATTERN.matcher(userInput)).matcher;
if (matcher.find() == false) {
throw new RuntimeException("커스텀 구분자를 찾을 수 없습니다");
}
String delimiter = matcher.group(1);
String group = matcher.group(2);
return group.split(delimiter);
}

public static boolean hasDelimiter(String userInput) {
return CUSTOM_DELIMITER_PATTERN.asPredicate().test(userInput);
}
}
12 changes: 12 additions & 0 deletions src/main/java/calculator/Main.java
@@ -0,0 +1,12 @@
package calculator;

import java.util.Scanner;

public class Main {

public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
AddCalculatorModel model = new AddCalculatorModel();
System.out.println(model.execute(sc.nextLine()));
}
}
14 changes: 14 additions & 0 deletions src/main/java/calculator/NumberUtils.java
@@ -0,0 +1,14 @@
package calculator;

import java.util.regex.Pattern;

public final class NumberUtils {

private static final Pattern numberPattern = Pattern.compile("[0-9]*");

public static void validateString(String string) {
if (numberPattern.matcher(string).matches() == false) {
throw new IllegalArgumentException("유효하지 않은 숫자가 사용되었습니다.");
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/calculator/StringUtils.java
@@ -0,0 +1,21 @@
package calculator;

import static calculator.NumberUtils.*;
import static java.util.Objects.*;

public final class StringUtils {

public static boolean isBlank(String s) {
return isNull(s) || s.equals("");
}

static Long[] convertStringToLong(String[] strings) {
Long[] result = new Long[strings.length];
for (int i = 0; i < strings.length; i++) {
String value = strings[i];
validateString(value);
result[i] = Long.parseLong(value);
}
return result;
}
}
65 changes: 65 additions & 0 deletions src/test/java/calculator/AddCalculatorModelTest.java
@@ -0,0 +1,65 @@
package calculator;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class AddCalculatorModelTest {

private AddCalculatorModel sut;

@BeforeEach
void setUp() {
sut = new AddCalculatorModel();
}

@ParameterizedTest(name = "빈 문자열 또는 Null 테스트")
@NullAndEmptySource
void 빈문자열_또는_Null_값을_입력할경우_0_반환(String userInput) {
assertThat(sut.execute(userInput)).isEqualTo(0);
}

@ParameterizedTest
@ValueSource(strings = {"1", "2", "999999999999"})
void 숫자_하나를_문자열로_입력할_경우_해당_숫자를_반환(String value) {
long expected = Long.parseLong(value);
assertThat(sut.execute(value)).isEqualTo(expected);
}

@ParameterizedTest
@ValueSource(strings = {"-1", "-1,-1", "-1,2:3"})
void 마이너스_또는_유효하지_않는__입력시_예외호출(String value) {
assertThatThrownBy(() -> sut.execute(value))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("유효하지 않은 숫자가 사용되었습니다.");
}

@ParameterizedTest
@CsvSource(value = {"0,1:1", "1,2:3", "999999999999,999999999999:1999999999998"}, delimiterString = ":")
void 숫자_두개를_컴마구분자로_입력할_경우_두숫자의_합을_반환한다(String userInput, Long expected) {
assertThat(sut.execute(userInput)).isEqualTo(expected);
}

@ParameterizedTest(name = "구분자를_컴마_뿐만_아니라_콜론도_가능")
@ValueSource(strings = {"1,2:3", "1:2:3"})
void 구분자를_컴마_뿐만_아니라_콜론도_가능(String value) {
assertThat(sut.execute(value)).isEqualTo(6);
}

@ParameterizedTest(name = "[{index}]({argumentsWithNames})“//”와 “\\n” 문자 사이에 커스텀 구분자를 지정하면 지정된 커스텀 구분자로 구분된다.")
@ValueSource(strings = {"//;\n1;2;3", "//a\n1a2a3", "//!\n1!2!3"})
void _1(String value) {
assertThat(sut.execute(value)).isEqualTo(6);
}

@ParameterizedTest(name = "[{index}] 유효하지 않는 커스텀 구분자({argumentsWithNames})를 지정하면 예외 호출한다.")
@ValueSource(strings = {"//abc\n1abc2abc3", "//;\n1;2:3", "//!\n1!2,3"})
void _2(String value) {
assertThatThrownBy(() -> sut.execute(value)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("유효하지 않은 숫자가 사용되었습니다.");
}
}

0 comments on commit 2a8e097

Please sign in to comment.