Skip to content
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

[step2] 문자열 계산기 #958

Open
wants to merge 9 commits into
base: bperhaps
Choose a base branch
from
16 changes: 15 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.6.21"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
Expand All @@ -12,8 +14,12 @@ repositories {

dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2")
testImplementation("org.assertj", "assertj-core", "3.22.0")

// https://github.com/assertj/assertj/issues/2357
testImplementation("org.assertj", "assertj-core", "3.20.2")

testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3")
implementation(kotlin("stdlib-jdk8"))
}

tasks {
Expand All @@ -30,3 +36,11 @@ tasks {
verbose.set(true)
}
}
val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
jvmTarget = "1.8"
}
val compileTestKotlin: KotlinCompile by tasks
compileTestKotlin.kotlinOptions {
jvmTarget = "1.8"
}
7 changes: 7 additions & 0 deletions src/main/kotlin/step1/Person.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package step1

data class Person(
val name: String,
val age: Int? = null,
var nickname: String? = null
)
35 changes: 35 additions & 0 deletions src/main/kotlin/step2/calculator/Calculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package step2.calculator

import step2.calculator.vo.CalculatorInput
import step2.calculator.vo.Number
import step2.calculator.vo.from

class Calculator(
private val inputs: Inputs
) {
fun calculate(): Number {
var result = inputs.nextNumber;

while(inputs.hasNextNumber && inputs.hasNextOperation) {
val operation = inputs.nextOperation
val operand = inputs.nextNumber

result = operation.calculate(result, operand)
}

return result
}

companion object
}

fun Calculator.Companion.from(input: String): Calculator {
return Calculator(Inputs.from(extractInputs(input)))
}

private fun extractInputs(input: String): List<CalculatorInput> {
return input.split(" ").asSequence()
.filter { it.isNotBlank() }
.map { CalculatorInput.from(it) }
.toList();
}
Comment on lines +23 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
companion object
}
fun Calculator.Companion.from(input: String): Calculator {
return Calculator(Inputs.from(extractInputs(input)))
}
private fun extractInputs(input: String): List<CalculatorInput> {
return input.split(" ").asSequence()
.filter { it.isNotBlank() }
.map { CalculatorInput.from(it) }
.toList();
}
companion object {
fun from(input: String): Calculator {
return Calculator(Inputs.from(extractInputs(input)))
}
private fun extractInputs(input: String): List<CalculatorInput> {
return input.split(" ").asSequence()
.filter { it.isNotBlank() }
.map { CalculatorInput.from(it) }
.toList();
}
}

https://pearlluck.tistory.com/722

companion object는 이렇게 사용하시면 될 것 같습니다 :)

104 changes: 104 additions & 0 deletions src/main/kotlin/step2/calculator/Inputs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package step2.calculator

import step2.calculator.vo.CalculatorInput
import step2.calculator.vo.Number
import step2.calculator.vo.Operation

class Inputs(
private val numbers: List<Number>,
private val operations: List<Operation>,
) {
private var numbersPosition: Int = 0
private var operationsPosition: Int = 0

val hasNextNumber: Boolean
get() {
return numbers.size > numbersPosition
}
Comment on lines +14 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val hasNextNumber: Boolean
get() {
return numbers.size > numbersPosition
}
val hasNextNumber: Boolean
get() = numbers.size > numbersPosition

이렇게 하면 조금 더 깔끔해질 것 같아요!


val nextNumber: Number
get() {
val result = numbers[numbersPosition]
numbersPosition += 1

return result
}

val hasNextOperation: Boolean
get() {
return operations.size > operationsPosition
}

val nextOperation: Operation
get() {
val result = operations[operationsPosition]
operationsPosition += 1

return result
}

companion object
}

@Suppress("UNCHECKED_CAST")
fun Inputs.Companion.from(values: List<CalculatorInput>): Inputs {
values.validateFormat()

val groupBy = values.groupBy { it.getType() }

return Inputs(
groupBy[Number::class.java] as List<Number>,
groupBy[Operation::class.java] as List<Operation>
)
}
Comment on lines +44 to +53
Copy link

@hyunniiii hyunniiii Dec 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 이 코멘트 처럼 companion object 부분을 수정해주세요 :)


private fun List<CalculatorInput>.validateFormat() {
validateNumberPosition()
validateOperationOption()
validateOperandCount()
validateInputLength()
}

private fun List<CalculatorInput>.validateNumberPosition() {
for (i: Int in 0 until this.size step (2)) {
if (this[i] !is Number) {
throw IllegalArgumentException("인풋 포멧 오류")
}
Comment on lines +64 to +66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kotlin 의 require을 사용해보는건 어떨까요?

https://seosh817.tistory.com/155

또한 어떤 에러인지 알 수 있도록 에러 메시지를 조금 더 상세하게 작성해주세요!

}
}

private fun List<CalculatorInput>.validateOperationOption() {
for (i: Int in 1 until this.size step (2)) {
if (this[i] !is Operation) {
throw IllegalArgumentException("인풋 포멧 오류")
}
}
}

private fun List<CalculatorInput>.validateOperandCount() {
val groupBy = this.groupingBy {
it.getType()
}.eachCount()

if (groupBy[Number::class.java] != groupBy[Operation::class.java]?.plus(1)) {
throw IllegalArgumentException("피연산자 오류")
}
}

private fun List<CalculatorInput>.validateInputLength() {
if (this.size < 3) {
throw IllegalArgumentException("인풋 포멧 오류")
}
}

private fun CalculatorInput.getType(): Class<out CalculatorInput> {
if (this is Number) {
return Number::class.java
}

if (this is Operation) {
return Operation::class.java
}

throw IllegalArgumentException("잘못된 타입")
}
Comment on lines +94 to +104

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when을 사용하면 조금 더 간결하게 표현할 수 있을 것 같아요!

34 changes: 34 additions & 0 deletions src/main/kotlin/step2/calculator/vo/CalculatorInput.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package step2.calculator.vo

interface CalculatorInput {
companion object
}

fun CalculatorInput.Companion.from(input: String): CalculatorInput {
return when {
input.isTypeOf(Number::class.java) -> Number(input.toInt())
input.isTypeOf(Operation::class.java) -> Operation.from(input)
else -> throw IllegalArgumentException("알 수 없는 입력값.")
}
}
Comment on lines +7 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 변경할 수 있을 것 같습니다 :)



fun String.isTypeOf(type: Class<out CalculatorInput>): Boolean {
return when {
type.isAssignableFrom(Number::class.java) -> {
runCatching {
Number(this.toInt())
}.isSuccess
}

type.isAssignableFrom(Operation::class.java) -> {
kotlin.runCatching {
Operation.from(this)
}.isSuccess
}

else -> {
false
}
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/step2/calculator/vo/Number.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package step2.calculator.vo

data class Number(
val value: Int
) : CalculatorInput
25 changes: 25 additions & 0 deletions src/main/kotlin/step2/calculator/vo/Operation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package step2.calculator.vo

import java.util.function.BiFunction

enum class Operation(
private val value: String,
private val operation: BiFunction<Number, Number, Number>
) : CalculatorInput {
PLUS("+", { op1, op2 -> Number(op1.value + op2.value) }),
MINUS("-", { op1, op2 -> Number(op1.value - op2.value) }),
DIVIDE("/", { op1, op2 -> Number(op1.value / op2.value) }),
MULTI("*", { op1, op2 -> Number(op1.value * op2.value) });

fun calculate(op1: Number, op2: Number): Number {
return this.operation.apply(op1, op2)
}

companion object {
fun from(input: String): Operation {
return values().asSequence()
.filter { it.value == input }
.first()
}
}
}
56 changes: 56 additions & 0 deletions src/test/kotlin/step1/PersonTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package step1

import io.kotest.matchers.shouldBe
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll

internal class PersonTest {

@Test
fun constructor() {
val person = Person(
name = "손민성",
age = 30,
nickname = "손너잘"
)

person.name shouldBe ("손민성")

person.nickname = "너잘손"
person.nickname shouldBe ("너잘손")
}

@Test
fun `named arguments`() {
val people = listOf(
Person("손민성", 30, "손너잘"),
Person("손민성", 30, nickname = "손너잘"),
Person(name = "손민성", age = 30, nickname = "손너잘")
)

assertThat(people).allSatisfy {
it.name shouldBe "손민성"
it.age shouldBe 30
it.nickname shouldBe "손너잘"
}
}

@Test
fun `default arguments`() {
val person = Person("손민성")

assertAll(
{ person.name shouldBe "손민성" },
{ person.age shouldBe null },
{ person.nickname shouldBe null }
)
}

@Test
fun `data classes`() {
val person1 = Person("손민성", 30, "손너잘")
val person2 = Person("손민성", 30, "손너잘")
assertThat(person1).isEqualTo(person2)
}
}
28 changes: 28 additions & 0 deletions src/test/kotlin/step2/calculator/CalculatorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package step2.calculator

import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import step2.calculator.vo.Number

class CalculatorTest : StringSpec({
"올바른 인풋에 대해서 오류 없이 계산기 생성이 가능하다." {
shouldNotThrowAny { Calculator.from("3 + 2 * 4") }
}

"올바르지 않은 인풋에 대해서 오류 없이 계산기 생성이 불가능하다." {
shouldThrowAny { Calculator.from("3+ 2 * 4") }
}

"계산결과가 출력된다." {
assertSoftly {
Calculator.from("3 + 2 * 4").calculate() shouldBe Number(20)
Calculator.from("3 + 2 * 4 + 1").calculate() shouldBe Number(21)
Calculator.from("3 + 2 * 4 / 5").calculate() shouldBe Number(4)
Calculator.from("3 + 2 * 4 * 5").calculate() shouldBe Number(100)
}

}
})
59 changes: 59 additions & 0 deletions src/test/kotlin/step2/calculator/InputsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package step2.calculator

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.core.spec.style.StringSpec
import step2.calculator.vo.Number
import step2.calculator.vo.Operation

class InputsTest : StringSpec({
"정확한 input 포멧으로 Inputs를 생성한다." {
shouldNotThrowAny {
Inputs.from(
listOf(
Number(1),
Operation.from("+"),
Number(2),
Operation.from("-"),
Number(3)
)
)
}
}

"input 포멧이 부정확 하면 exception. (피연산자)" {
shouldThrowAny {
Inputs.from(
listOf(
Number(1),
Operation.from("+"),
Number(2),
Operation.from("-")
)
)
}
}

"input 포멧이 부정확 하면 exception.(길이)" {
shouldThrowAny {
Inputs.from(
listOf(
Number(1),
Operation.from("+")
)
)
}
}

"input 포멧이 부정확 하면 exception.(순서)" {
shouldThrowAny {
Inputs.from(
listOf(
Number(1),
Operation.from("+"),
Operation.from("-")
)
)
}
}
})
Loading