-
Notifications
You must be signed in to change notification settings - Fork 227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Step1 : 문자열 덧셈 계산기 #773
Step1 : 문자열 덧셈 계산기 #773
Changes from all commits
ac4e46a
cbcdece
82e1172
8476877
7d9370e
d9466c2
e9fd1c8
fe95959
52a21e5
4732683
a6451e6
56b236d
33d515f
2444875
a47d4da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
val matcher = CUSTOM_DELIMITER_PATTERN.matcher(expressionStr) | ||
|
||
return if (matcher.find()) { | ||
Expression( | ||
input = matcher.group(2), | ||
delimiters = DEFAULT_DELIMITER + matcher.group(1), | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
} 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, | ||
) | ||
Comment on lines
+27
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ExpressionMatcher.kt 파일에서 바깥에 같이 정의하거나 stringcalculator 패키지에 정의해도 되는데, 특정 객체를 통해서 주도적으로 생성되는 data class 같은 경우에는 해당 class의 companion 내부에서 정의하는게 명시적이라고 생각해서 companion object 내부에 정의했습니다. |
||
} | ||
} | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package stringcalculator | ||
|
||
import stringcalculator.ExpressionMatcher.Companion.Expression | ||
|
||
class NumberTokenizer { | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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()) | ||
} | ||
} |
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() | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
} | ||
} |
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(",", ":") | ||
} | ||
} |
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)), | ||
) | ||
} | ||
} |
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 | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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") | ||
cjcjon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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), | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 정답이 없는 이야기라 단순히 저의 주관적인 생각입니다.
객체지향적으로 설계하고 SOLID 원칙을 지키며 코딩하는 방식은 유지보수 관점에서 중요하다고 생각하는데, 말씀하신 것 처럼
이런 부분이 과하다고 생각할 수 있다고 생각해요. 저 또한 개인적으로 YANGI 원칙를 선호하기 때문에 저 또한 정말 필요성을 느끼면 진행하는 것을 좋아합니다만 하지만 그렇다고해서 이런 설계를 전혀 고려하지 않는 것은 지양해야한다고 생각해요.
지금은 단순하기 때문에 이런 객체지향적인 설계와 SRP 원칙을 준수하는 등의 행위가 덜 중요하게 느껴질 수 있다고 생각하지만 조금만 더 로직이 복잡해지기 시작하면 이런 설계의 필요성을 금방 느끼게 될거라 생각해요. 🤔
�특히 지금과 같이 문자열 덧셈 계산기의 경우 저희는 이 미션에서만 잠시 다루고 이후로는 유지보수하지 않을 코드이기 때문에 이런 객체지향적인 설계와 SRP를 지키며 코딩하는 것이 조금 과하다고 생각될 수 있지만 말씀하신 것처럼 지금은 학습의 관점에서 하는 것이 조금 더 강한 것은 맞습니다. 😃