-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: step2 문자열 계산기 구현 * feat: [step2] 패키지 명 변경 * feat: [step2] calculator 리팩토링 및 의미있는 이름으로 클래스명 변경
- Loading branch information
Showing
14 changed files
with
335 additions
and
2 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
src/main/kotlin/step1/Person.kt → src/main/kotlin/person/Person.kt
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 |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package step1 | ||
package person | ||
|
||
data class Person( | ||
val name: 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,4 @@ | ||
package racingcar | ||
|
||
fun main(args: Array<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,24 @@ | ||
package racingcar.application | ||
|
||
import racingcar.service.ExpressionCalculator | ||
import racingcar.service.StringTokenizer | ||
import racingcar.service.Token2ExpressionConvertor | ||
import racingcar.vo.Expression | ||
import racingcar.vo.RawToken | ||
|
||
class CalculateUseCase( | ||
private val stringTokenizer: StringTokenizer, | ||
private val token2ExpressionConvertor: Token2ExpressionConvertor, | ||
private val expressionCalculator: ExpressionCalculator, | ||
) { | ||
fun calculate(stringExpression: String): Double { | ||
require(stringExpression.isNotBlank()) { "빈 표현식은 계산할 수 없습니다." } | ||
|
||
val tokens: List<RawToken> = stringTokenizer.parse(expression = stringExpression) | ||
|
||
val expression: Expression = token2ExpressionConvertor.convert(tokens = tokens) | ||
|
||
require(expressionCalculator.isCalculable(expression)) { "계산할 수 없는 표현식 타입입니다." } | ||
return expressionCalculator.calculate(expression = expression) | ||
} | ||
} |
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,62 @@ | ||
package racingcar.service | ||
|
||
import racingcar.type.ExpressionType | ||
import racingcar.type.OperatorType | ||
import racingcar.vo.Expression | ||
import racingcar.vo.ExpressionToken | ||
import racingcar.vo.NumberToken | ||
import racingcar.vo.OperatorToken | ||
|
||
interface ExpressionCalculator { | ||
fun isCalculable(expression: Expression): Boolean | ||
|
||
fun calculate(expression: Expression): Double | ||
} | ||
|
||
class PostfixExpressionCalculator : ExpressionCalculator { | ||
override fun isCalculable(expression: Expression): Boolean { | ||
return expression.type == ExpressionType.POSTFIX | ||
} | ||
|
||
override fun calculate(expression: Expression): Double { | ||
val stack: ArrayDeque<ExpressionToken> = expression.tokens | ||
.fold(ArrayDeque()) { stack, token -> stack.calculateToken(token) } | ||
|
||
require(stack.size == 1) { "계산이 완료되지 않은 표현식입니다." } | ||
|
||
val result: NumberToken = stack.removeLastOrNull() as? NumberToken | ||
?: throw IllegalArgumentException("계산이 완료되지 않은 표현식입니다.") | ||
|
||
return result.value | ||
} | ||
|
||
private fun ArrayDeque<ExpressionToken>.calculateToken(token: ExpressionToken): ArrayDeque<ExpressionToken> { | ||
when (token) { | ||
is NumberToken -> this.addLast(token) | ||
|
||
is OperatorToken -> { | ||
val number2 = this.removeLastOrNull() | ||
val number1 = this.removeLastOrNull() | ||
this.addLast(NumberToken(evaluate(token, number1, number2))) | ||
} | ||
} | ||
|
||
return this | ||
} | ||
|
||
private fun evaluate(operator: OperatorToken, number1: ExpressionToken?, number2: ExpressionToken?): Double { | ||
if (number1 !is NumberToken || number2 !is NumberToken) { | ||
throw IllegalArgumentException("잘못된 계산 식입니다.") | ||
} | ||
|
||
return when (operator.operatorType) { | ||
OperatorType.ADD -> number1.value + number2.value | ||
OperatorType.SUBTRACT -> number1.value - number2.value | ||
OperatorType.MULTIPLY -> number1.value * number2.value | ||
OperatorType.DIVIDE -> { | ||
require(number2.value != 0.0) { "0으로 나눌 수 없습니다." } | ||
number1.value / number2.value | ||
} | ||
} | ||
} | ||
} |
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,29 @@ | ||
package racingcar.service | ||
|
||
import racingcar.type.ExpressionRegex | ||
import racingcar.vo.RawToken | ||
|
||
/** | ||
* 문자열로 된 표현식을 토큰 단위로 분리한다. | ||
*/ | ||
interface StringTokenizer { | ||
fun parse(expression: String): List<RawToken> | ||
} | ||
|
||
class RegexStringTokenizer : StringTokenizer { | ||
override fun parse(expression: String): List<RawToken> { | ||
val trimmedExpression: String = expression.replace(WHITESPACE_REGEX, "") | ||
|
||
if (trimmedExpression.matches(ExpressionRegex.EXPRESSION.regex) == false) { | ||
throw IllegalArgumentException("유효하지 않은 입력입니다.") | ||
} | ||
|
||
return ExpressionRegex.TOKEN.regex.findAll(input = trimmedExpression) | ||
.map { RawToken(it.value) } | ||
.toList() | ||
} | ||
|
||
companion object { | ||
private val WHITESPACE_REGEX: Regex = Regex("\\s") | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
src/main/kotlin/racingcar/service/Token2ExpressionConvertor.kt
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,61 @@ | ||
package racingcar.service | ||
|
||
import racingcar.type.ExpressionRegex | ||
import racingcar.type.ExpressionType | ||
import racingcar.vo.Expression | ||
import racingcar.vo.ExpressionToken | ||
import racingcar.vo.NumberToken | ||
import racingcar.vo.OperatorToken | ||
import racingcar.vo.RawToken | ||
|
||
/** | ||
* 토큰 리스트를 표현식으로 변환한다. | ||
*/ | ||
interface Token2ExpressionConvertor { | ||
fun convert(tokens: List<RawToken>): Expression | ||
} | ||
|
||
class PostfixToken2ExpressionConvertor : Token2ExpressionConvertor { | ||
override fun convert(tokens: List<RawToken>): Expression { | ||
return tokens.map(::toExpressionToken) | ||
.let { infixTokens: List<ExpressionToken> -> convertToPostfix(infixTokens) } | ||
.let { postfixTokens: List<ExpressionToken> -> | ||
Expression( | ||
type = ExpressionType.POSTFIX, | ||
tokens = postfixTokens, | ||
) | ||
} | ||
} | ||
|
||
private fun convertToPostfix(infixTokens: List<ExpressionToken>): List<ExpressionToken> { | ||
val postfixExpression = mutableListOf<ExpressionToken>() | ||
val operatorStack = ArrayDeque<ExpressionToken>() | ||
|
||
infixTokens.forEach { token -> | ||
when (token) { | ||
is NumberToken -> postfixExpression.add(token) | ||
is OperatorToken -> { | ||
if (operatorStack.isNotEmpty()) postfixExpression.add(operatorStack.removeLast()) | ||
operatorStack.addLast(token) | ||
} | ||
} | ||
} | ||
|
||
postfixExpression.addAll(operatorStack) | ||
return postfixExpression | ||
} | ||
|
||
private fun toExpressionToken(rawToken: RawToken): ExpressionToken { | ||
if (rawToken.value.matches(ExpressionRegex.OPERATOR.regex)) { | ||
return OperatorToken.fromRawTokenOrNull(rawToken) | ||
?: throw IllegalArgumentException("연산자로 변환할 수 없는 토큰입니다 (token=$rawToken)") | ||
} | ||
|
||
if (rawToken.value.matches(ExpressionRegex.NUMBER.regex)) { | ||
return NumberToken.fromRawTokenOrNull(rawToken) | ||
?: throw IllegalArgumentException("숫자로 변환 할 수 없는 토큰입니다 (token=$rawToken)") | ||
} | ||
|
||
throw IllegalArgumentException("유효하지 않은 토큰입니다 (token=$rawToken)") | ||
} | ||
} |
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,8 @@ | ||
package racingcar.type | ||
|
||
enum class ExpressionRegex(val regex: Regex) { | ||
NUMBER(regex = "([1-9][0-9]*)".toRegex()), | ||
OPERATOR(regex = "[+\\-*/]".toRegex()), | ||
TOKEN(regex = "${NUMBER.regex}|${OPERATOR.regex}".toRegex()), | ||
EXPRESSION(regex = "${NUMBER.regex}(${OPERATOR.regex}${NUMBER.regex})*".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,5 @@ | ||
package racingcar.type | ||
|
||
enum class ExpressionType(val description: String) { | ||
POSTFIX("후위표기식"), | ||
} |
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,8 @@ | ||
package racingcar.type | ||
|
||
enum class OperatorType(val symbol: String) { | ||
ADD("+"), | ||
SUBTRACT("-"), | ||
MULTIPLY("*"), | ||
DIVIDE("/"), | ||
} |
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,8 @@ | ||
package racingcar.vo | ||
|
||
import racingcar.type.ExpressionType | ||
|
||
data class Expression( | ||
val type: ExpressionType, | ||
val tokens: List<ExpressionToken>, | ||
) |
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,28 @@ | ||
package racingcar.vo | ||
|
||
import racingcar.type.OperatorType | ||
|
||
sealed interface ExpressionToken | ||
|
||
data class NumberToken(val value: Double) : ExpressionToken { | ||
companion object { | ||
fun fromRawTokenOrNull(raw: RawToken): NumberToken? { | ||
return raw.value.toDoubleOrNull() | ||
?.let(::NumberToken) | ||
} | ||
} | ||
} | ||
|
||
data class OperatorToken(val operatorType: OperatorType) : ExpressionToken { | ||
companion object { | ||
fun fromRawTokenOrNull(raw: RawToken): OperatorToken? { | ||
return when (raw.value) { | ||
"+" -> OperatorToken(OperatorType.ADD) | ||
"-" -> OperatorToken(OperatorType.SUBTRACT) | ||
"*" -> OperatorToken(OperatorType.MULTIPLY) | ||
"/" -> OperatorToken(OperatorType.DIVIDE) | ||
else -> null | ||
} | ||
} | ||
} | ||
} |
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,3 @@ | ||
package racingcar.vo | ||
|
||
data class RawToken(val value: String) |
2 changes: 1 addition & 1 deletion
2
src/test/kotlin/step1/PersonTest.kt → src/test/kotlin/person/PersonTest.kt
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 |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package step1 | ||
package person | ||
|
||
import io.kotest.assertions.assertSoftly | ||
import io.kotest.assertions.withClue | ||
|
93 changes: 93 additions & 0 deletions
93
src/test/kotlin/racingcar/application/CalculateUseCaseTest.kt
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,93 @@ | ||
package racingcar.application | ||
|
||
import io.kotest.assertions.throwables.shouldNotThrowAny | ||
import io.kotest.assertions.throwables.shouldThrow | ||
import io.kotest.core.spec.style.FreeSpec | ||
import io.kotest.matchers.shouldBe | ||
import racingcar.service.PostfixExpressionCalculator | ||
import racingcar.service.PostfixToken2ExpressionConvertor | ||
import racingcar.service.RegexStringTokenizer | ||
|
||
class CalculateUseCaseTest : FreeSpec({ | ||
|
||
val calculateUseCase = CalculateUseCase( | ||
stringTokenizer = RegexStringTokenizer(), | ||
token2ExpressionConvertor = PostfixToken2ExpressionConvertor(), | ||
expressionCalculator = PostfixExpressionCalculator(), | ||
) | ||
|
||
"기본적인 사칙연산을 수행할 수 있어야 한다" - { | ||
"숫자 하나만 입력" { | ||
val result = shouldNotThrowAny { calculateUseCase.calculate("5") } | ||
|
||
result shouldBe 5.0 | ||
} | ||
|
||
"덧셈" { | ||
val result = calculateUseCase.calculate("1+2") | ||
|
||
result shouldBe 3.0 | ||
} | ||
|
||
"뺄셈" { | ||
val result = calculateUseCase.calculate("3-1") | ||
|
||
result shouldBe 2.0 | ||
} | ||
|
||
"곱셈" { | ||
val result = calculateUseCase.calculate("3*3") | ||
|
||
result shouldBe 9.0 | ||
} | ||
|
||
"나눗셈" { | ||
val result = calculateUseCase.calculate("10/8") | ||
|
||
result shouldBe 1.25 | ||
} | ||
} | ||
|
||
"계산 순서는 사칙연산 규칙을 따르지 않고, 입력 값 순서대로 계산한다" - { | ||
"덧셈과 뺼셈" { | ||
val result = calculateUseCase.calculate("1-1+2+4-1+2+602+20+1") | ||
|
||
result shouldBe (1 - 1 + 2 + 4 - 1 + 2 + 602 + 20 + 1) | ||
} | ||
|
||
"덧셈과 곱셈" { | ||
val result = calculateUseCase.calculate("1+3123*36623") | ||
|
||
result shouldBe (1 + 3123) * 36623 | ||
} | ||
|
||
"뺄셈과 나눗셈" { | ||
val result = calculateUseCase.calculate("615121-42323/2") | ||
|
||
result shouldBe (615121 - 42323) / 2 | ||
} | ||
} | ||
|
||
"계산할 수 없는 경우네는 예외를 반환한다" - { | ||
"빈 문자열 입력" { | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("") } | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate(" ") } | ||
} | ||
|
||
"유효하지 않은 문자열 입력" { | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("안녕하세요") } | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("3더하기3") } | ||
} | ||
|
||
"잘못된 수식 입력" { | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("0++12-1") } | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("--32-1") } | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("1+1--1") } | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("+") } | ||
} | ||
|
||
"숫자를 0으로 나눔" { | ||
shouldThrow<IllegalArgumentException> { calculateUseCase.calculate("1+1/0") } | ||
} | ||
} | ||
}) |