Skip to content

Commit

Permalink
Step1 : 문자열 덧셈 계산기 (#773)
Browse files Browse the repository at this point in the history
* refactor: step0 리뷰 반영

- Car 생성 예외 이유 표시
- getter/setter convention 반영

* feat(StringCalculator): 문자열 0 또는 null일경우 합산은 0

* feat(StringCalculator): 단순 숫자 입력일경우 합산은 숫자

* feat(StringCalculator): 컴마 구분자로 입력 문자열의 합 계산

* feat(StringCalculator): 콜론 구분자로 입력 문자열의 합 계산

* feat(StringCalculator): 커스텀 구분자 기능 구현

* refactor(StringCalculator): delimiter 공통화

* feat(StringCalculator): 음수 입력 불가 구현

* test(StringCalculator): 합산 테스트 보강

* refactor(StringCalculator): 코드리뷰 반영
- MagicNumber 상수화
- 파라미터 이름 명시
- 역할에 따른 객체 분리
- Pattern 객체 캐싱

* fix(StringCalculator): 구분자 미입력시 숫자 변환 버그 수정

* test(StringCalculator): 숫자 토큰 변환기 테스트 작성

* test(StringCalculator): 식 변환기 테스트 작성

* refactor(StringCalculator): 식 변환기 이름 변경

* test(StringCalculator): 테스트 명칭 변경
  • Loading branch information
cjcjon authored Jan 27, 2025
1 parent 11297c2 commit 325ceb3
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 5 deletions.
10 changes: 5 additions & 5 deletions src/main/java/racingcar/Car.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ public class Car {

public Car(final String name) {
if (name.length() > 5) {
throw new IllegalArgumentException();
throw new IllegalArgumentException("이름은 5글자를 넘을 수 없습니다");
}

this.name = name;
}

public int getPosition() {
return position;
}

public void move(final MovingStrategy movingStrategy) {
if (movingStrategy.movable()) {
position++;
}
}

public int getPosition() {
return position;
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/stringcalculator/ExpressionMatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package stringcalculator

import java.util.regex.Pattern

class ExpressionMatcher {

fun transform(expressionStr: String): Expression {
val matcher = CUSTOM_DELIMITER_PATTERN.matcher(expressionStr)

return if (matcher.find()) {
Expression(
input = matcher.group(2),
delimiters = DEFAULT_DELIMITER + matcher.group(1),
)
} else {
Expression(
input = expressionStr,
delimiters = DEFAULT_DELIMITER,
)
}
}

companion object {
private val DEFAULT_DELIMITER = listOf(",", ":")
private val CUSTOM_DELIMITER_PATTERN = Pattern.compile("//(.)\n(.*)")

data class Expression(
val delimiters: List<String>,
val input: String,
)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/stringcalculator/NumberTokenizer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package stringcalculator

import stringcalculator.ExpressionMatcher.Companion.Expression

class NumberTokenizer {

fun tokenize(expression: Expression): List<Int> {
val numberTokens = splitNumberTokens(expression.input, expression.delimiters)
val numbers = numberTokens.map { it.toInt() }

return numbers
}

private fun splitNumberTokens(input: String, delimiters: List<String>): List<String> {
return if (delimiters.isEmpty()) listOf(input)
else input.split(delimiters.joinToString("|").toRegex())
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/stringcalculator/StringCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package stringcalculator

class StringCalculator {

private val expressionMatcher = ExpressionMatcher()
private val numberTokenizer = NumberTokenizer()

fun calculate(calculatorExpression: String?): Int {
if (calculatorExpression.isNullOrEmpty()) return DEFAULT_VALUE

val expression = expressionMatcher.transform(calculatorExpression)
val numbers = numberTokenizer.tokenize(expression)
validateNegativeValue(numbers)

return numbers.sum()
}

private fun validateNegativeValue(tokens: List<Int>) {
if (tokens.any { it < 0 }) {
throw RuntimeException("음수의 덧셈은 제공하지 않는 기능입니다")
}
}

companion object {
private const val DEFAULT_VALUE = 0
}
}
54 changes: 54 additions & 0 deletions src/test/kotlin/stringcalculator/ExpressionMatcherTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package stringcalculator

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import stringcalculator.ExpressionMatcher.Companion.Expression

internal class ExpressionMatcherTest {

private val sut = ExpressionMatcher()

@DisplayName("커스텀 구분자가 없을경우 기본 구분자로 쉼표(,)와 콜론(:)을 포함시킨다")
@Test
fun useDefaultDelimiterIfNoCustomDelimiter() {
// given
val expressionInput = "1:2,3"

// when
val actual = sut.transform(expressionInput)

// then
assertThat(actual).isEqualTo(Expression(DEFAULT_DELIMITERS, expressionInput))
}

@DisplayName("커스텀 구분자는 //와 \n 문자 사이에 지정해 Expression에 포함할 수 있다")
@Test
fun useCustomDelimiterIfHaveCustomDelimiterInput() {
// given
val expressionInput = "//;\n1:2,3;4"

// when
val actual = sut.transform(expressionInput)

// then
assertThat(actual).isEqualTo(Expression(listOf(*DEFAULT_DELIMITERS.toTypedArray(), ";"), "1:2,3;4"))
}

@DisplayName("커스텀 구분자 이외의 모든 문자열을 입력으로 변환한다")
@Test
fun useAllInputIfNoDelimiter() {
// given
val expressionInput = ",a;2:3,12"

// when
val actual = sut.transform(expressionInput)

// then
assertThat(actual).isEqualTo(Expression(DEFAULT_DELIMITERS, ",a;2:3,12"))
}

companion object {
private val DEFAULT_DELIMITERS = listOf(",", ":")
}
}
66 changes: 66 additions & 0 deletions src/test/kotlin/stringcalculator/NumberTokenizerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package stringcalculator

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
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
import stringcalculator.ExpressionMatcher.Companion.Expression

internal class NumberTokenizerTest {

private val sut = NumberTokenizer()

@DisplayName("숫자로 변환할 수 없을경우 NumberFormatException 예외가 발생한다")
@Test
fun errorIfNoNumberFormat() {
// given
val delimiters = listOf(":")
val input = "1:asdf"
val expr = Expression(delimiters, input)

// when
val actual = kotlin.runCatching { sut.tokenize(expr) }

// then
assertThat(actual.exceptionOrNull()).isInstanceOf(NumberFormatException::class.java)
}

@DisplayName("구분자가 없을경우 입력된 문자열 자체를 숫자로 변환한다")
@Test
fun parseNumberIfNoDelimiter() {
// given
val delimiters = emptyList<String>()
val input = "12345"

// when
val actual = sut.tokenize(Expression(delimiters, input))

// then
assertThat(actual).isEqualTo(listOf(12345))
}

@DisplayName("입력된 문자열을 구분자를 통해 나눈다")
@ParameterizedTest
@MethodSource("provideDelimiterSource")
fun tokenize(delimiters: List<String>, input: String, answer: List<Int>) {
// given
val exp = Expression(delimiters, input)

// when
val actual = sut.tokenize(exp)

// then
assertThat(actual).isEqualTo(answer)
}

companion object {
@JvmStatic
fun provideDelimiterSource() = listOf(
Arguments.of(listOf(":"), "1:2:3", listOf(1, 2, 3)),
Arguments.of(listOf(":", ","), "1:2,3", listOf(1, 2, 3)),
Arguments.of(listOf("=", ",", ":"), "1=2,3:4", listOf(1, 2, 3, 4)),
)
}
}
77 changes: 77 additions & 0 deletions src/test/kotlin/stringcalculator/StringCalculatorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package stringcalculator

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.NullAndEmptySource
import org.junit.jupiter.params.provider.ValueSource

internal class StringCalculatorTest {

private val sut = StringCalculator()

@DisplayName(value = "빈 문자열 또는 null 값을 입력할 경우 0을 반환해야 한다.")
@ParameterizedTest
@NullAndEmptySource
fun emptyOrNull(text: String?) {
assertThat(sut.calculate(text)).isZero
}

@DisplayName(value = "숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환한다.")
@ParameterizedTest
@ValueSource(strings = ["1"])
fun oneNumber(text: String) {
assertThat(sut.calculate(text)).isSameAs(text.toInt())
}

@DisplayName(value = "숫자 두개를 쉼표(,) 구분자로 입력할 경우 두 숫자의 합을 반환한다.")
@ParameterizedTest
@ValueSource(strings = ["1,2"])
fun twoNumbers(text: String?) {
assertThat(sut.calculate(text)).isSameAs(3)
}

@DisplayName(value = "구분자를 쉼표(,) 이외에 콜론(:)을 사용할 수 있다.")
@ParameterizedTest
@ValueSource(strings = ["1,2:3"])
fun colons(text: String?) {
assertThat(sut.calculate(text)).isSameAs(6)
}

@DisplayName(value = "//와 \\n 문자 사이에 커스텀 구분자를 지정할 수 있다.")
@ParameterizedTest
@ValueSource(strings = ["//;\n1;2;3"])
fun customDelimiter(text: String?) {
assertThat(sut.calculate(text)).isSameAs(6)
}

@DisplayName(value = "문자열 계산기에 음수를 전달하는 경우 RuntimeException 예외 처리를 한다.")
@ParameterizedTest
@ValueSource(strings = ["-1", "1:-3", "3,5,7:-29"])
fun negative(text: String) {
assertThatExceptionOfType(RuntimeException::class.java).isThrownBy {
sut.calculate(text)
}
}

@DisplayName(value = "문자열 계산기로 합을 계산한다")
@ParameterizedTest
@MethodSource("provideSumArguments")
fun sum(text: String, result: Int) {
assertThat(sut.calculate(text)).isSameAs(result)
}

companion object {
@JvmStatic
fun provideSumArguments() = listOf(
Arguments.of("0", 0),
Arguments.of("1", 1),
Arguments.of("1,3,5", 9),
Arguments.of("4:15,25", 44),
Arguments.of("//;\n1;2;3,4:5", 15),
)
}
}

0 comments on commit 325ceb3

Please sign in to comment.