-
Notifications
You must be signed in to change notification settings - Fork 227
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
7 changed files
with
279 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(",", ":") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
) | ||
} | ||
} |