Skip to content

Commit

Permalink
feat: 로또 구매 구현 (#17)
Browse files Browse the repository at this point in the history
* ci: add ignore file for codecov

* refactor: message 정리

* style: unnecessary trailing comma in test

* feat: 구매할 로또의 개수를 결정하는 LottoCount 구현

* fix: input 검증을 위한 try catch loop 예외 레벨 조정

* style: trailing comma 정의

* style: multiline expression wrapping

* style: new line function signature

* impv: plus, minus 연산 추가

* Add LottoNumbers model and update View and Controller

A feature to validate the number of lotto numbers was created. For this, a LottoNumbers model was added with a from method to transform a list of Integers into LottoNumbers and validate the size. The View is updated to request and display useful lotto-related information. The Controller is also updated to include interactions with the LottoNumbers model. Now, the application is able to manage the user's lotto numbers directly and enforce the correct number of lotto entries.

* style: code formatting

* ci: ignore LottoApp
  • Loading branch information
songkg7 authored Oct 15, 2023
1 parent 99d0c54 commit 3227453
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 69 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ max_line_length = 120
[*.{kt,kts}]
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ val testCoverage by tasks.registering {
dependsOn(
":test",
":jacocoTestReport",
":jacocoTestCoverageVerification"
":jacocoTestCoverageVerification",
)

tasks["jacocoTestReport"].mustRunAfter(tasks["test"])
Expand Down
3 changes: 3 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ignore:
- "**/view/*"
- "**/controller/*"
21 changes: 20 additions & 1 deletion src/main/kotlin/controller/LottoApp.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
package controller

import model.lotto.LottoCount
import model.lotto.LottoNumbers
import model.money.Money
import view.InputView
import view.OutputView
import view.validInputView

class LottoApp(private val inputView: InputView, private val outputView: OutputView) {
fun run() {
validInputView({ inputCapital() }) { outputView.printMessage(it) }
val money = validInputView({ inputCapital() }) { outputView.printMessage(it) }
val lottoCount: LottoCount = validInputView({ inputManualCount(money) }) { outputView.printMessage(it) }
validInputView({ inputManualLottoNumber(lottoCount) }) { outputView.printMessage(it) }
}

private fun inputCapital(): Money {
outputView.requestCapital()
return Money(inputView.requestAmount())
}

private fun inputManualCount(money: Money): LottoCount {
outputView.requestManualCount()
val manualCount = inputView.requestAmount().toInt()
return LottoCount.of(manualCount, money)
}

private fun inputManualLottoNumber(lottoCount: LottoCount): List<LottoNumbers> {
return (1..lottoCount.manualCount)
.map {
outputView.requestManualLottoNumber()
inputView.requestLottoNumber()
}
.map { LottoNumbers.from(it) }
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/model/lotto/LottoCount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package model.lotto

import model.money.Money
import model.money.PRICE
import view.NOT_ENOUGH_MONEY_MESSAGE

class LottoCount(
val manualCount: Int,
val autoCount: Int,
) {
companion object {
fun of(manualCount: Int, money: Money): LottoCount {
val totalCount = money / PRICE
val autoCount = totalCount - manualCount
check(autoCount >= 0) { NOT_ENOUGH_MONEY_MESSAGE }
return LottoCount(manualCount, autoCount)
}
}
}
6 changes: 2 additions & 4 deletions src/main/kotlin/model/lotto/LottoNumber.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package model.lotto

private const val INVALID_LOTTO_NUMBER_MESSAGE = "로또 번호는 1에서 45 사이여야 합니다."

data class LottoNumber(private val value: Int) {
init {
require(value in 1..45) { INVALID_LOTTO_NUMBER_MESSAGE }
}

companion object {
private const val INVALID_LOTTO_NUMBER_MESSAGE = "로또 번호는 1에서 45 사이여야 합니다."
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/model/lotto/LottoNumbers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package model.lotto

import view.INVALID_LOTTO_NUMBER_COUNT_MESSAGE

class LottoNumbers private constructor(private val numbers: List<LottoNumber>) {
companion object {
fun from(numbers: List<Int>): LottoNumbers {
check(numbers.size == 6) { INVALID_LOTTO_NUMBER_COUNT_MESSAGE }
val lottoNumbers = numbers.map { LottoNumber(it) }
return LottoNumbers(lottoNumbers)
}
}
}
13 changes: 5 additions & 8 deletions src/main/kotlin/model/money/Amount.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package model.money

import view.EMPTY_VALUE_ERROR_MESSAGE
import view.INVALID_NUMBER_FORMAT_MESSAGE

class Amount(private val stringAmount: String) {
private val integers = '0'..'9'

init {
require(stringAmount.isNotBlank()) { EMPTY_VALUE_ERROR }
require(stringAmount.isInt()) { INVALID_INPUT_ERROR_PREFIX + stringAmount + INVALID_INPUT_ERROR_SUFFIX }
require(stringAmount.isNotBlank()) { EMPTY_VALUE_ERROR_MESSAGE }
require(stringAmount.isInt()) { INVALID_NUMBER_FORMAT_MESSAGE }
}

fun getBigDecimal() = stringAmount.toBigDecimal()

private fun String.isInt() = this.all { it in integers }

companion object {
private const val EMPTY_VALUE_ERROR = "값이 비어있습니다."
private const val INVALID_INPUT_ERROR_PREFIX = "입력한"
private const val INVALID_INPUT_ERROR_SUFFIX = "값은 숫자여야 합니다."
}
}
25 changes: 22 additions & 3 deletions src/main/kotlin/model/money/Money.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,31 @@ package model.money

import java.math.BigDecimal

class Money(amount: BigDecimal) {
private const val NON_NEGATIVE_NUMBER_ERROR_MESSAGE = "금액은 0원 이상이어야 합니다."
val PRICE = Money(BigDecimal.valueOf(5000))

class Money(private val amount: BigDecimal) {
init {
require(amount >= BigDecimal.ZERO) { NON_NEGATIVE_NUMBER_ERROR }
require(amount >= BigDecimal.ZERO) { NON_NEGATIVE_NUMBER_ERROR_MESSAGE }
}

companion object {
private const val NON_NEGATIVE_NUMBER_ERROR = "금액은 0원 이상이어야 합니다."
val ZERO = Money(BigDecimal.ZERO)
}

operator fun div(other: Money): Int {
return amount.divide(other.amount).toInt()
}

operator fun times(count: Int): Money {
return Money(amount.multiply(BigDecimal.valueOf(count.toLong())))
}

operator fun minus(other: Money): Money {
return Money(amount.subtract(other.amount))
}

operator fun plus(other: Money): Money {
return Money(amount.add(other.amount))
}
}
6 changes: 5 additions & 1 deletion src/main/kotlin/view/InputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fun <T> validInputView(
): T {
return try {
supplier.invoke()
} catch (e: IllegalArgumentException) {
} catch (e: Exception) {
consumer.invoke(e.message!!)
validInputView(supplier, consumer)
}
Expand All @@ -20,4 +20,8 @@ class InputView {
val amount = Amount(readln())
return amount.getBigDecimal()
}

fun requestLottoNumber(): List<Int> {
return readln().split(" ").map { it.toInt() }
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package view

const val EMPTY_VALUE_ERROR_MESSAGE = "값이 비어있습니다."
const val INVALID_NUMBER_FORMAT_MESSAGE = "입력한 값은 숫자여야 합니다."
const val NOT_ENOUGH_MONEY_MESSAGE = "금액이 부족합니다"
const val INVALID_LOTTO_NUMBER_COUNT_MESSAGE = "로또 번호는 6개여야 합니다."

class OutputView {
fun requestCapital() {
println("구입금액을 입력해 주세요.")
Expand All @@ -8,4 +13,12 @@ class OutputView {
fun printMessage(message: String) {
println(message)
}

fun requestManualCount() {
println("수동으로 구매할 로또 수를 입력해 주세요.")
}

fun requestManualLottoNumber() {
println("로또 번호를 입력해 주세요. (공백으로 구분)")
}
}
51 changes: 51 additions & 0 deletions src/test/kotlin/model/lotto/LottoCountTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package model.lotto

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import model.money.Money
import model.money.PRICE
import view.NOT_ENOUGH_MONEY_MESSAGE
import java.math.BigDecimal

class LottoCountTest : FreeSpec(
{
"수동 로또 구매 개수를 입력할 때" - {
"돈이 충분하지 않으면 에러가 발생한다" {
val money = Money.ZERO
val manualCount = 2
val exception =
shouldThrow<IllegalStateException> {
LottoCount.of(manualCount, money)
}
exception.message shouldBe NOT_ENOUGH_MONEY_MESSAGE
}

"돈이 충분하면 수동 로또 개수와 자동 로또 개수를 구매 가능 개수만큼만 반환한다" - {
"나누어 떨어지는 경우 모든 액수를 구매에 사용한다" {
val money = PRICE * 3
val manualCount = 2
val lottoCount = LottoCount.of(manualCount, money)
lottoCount.manualCount shouldBe 2
lottoCount.autoCount shouldBe 1
}

"나누어 떨어지지 않는 경우 구매 가능한 최대 수량만 구매한다" {
val money = Money(BigDecimal.valueOf(12000))
val manualCount = 1
val lottoCount = LottoCount.of(manualCount, money)
lottoCount.manualCount shouldBe 1
lottoCount.autoCount shouldBe 1
}
}

"수동 로또 개수가 0이면 자동 로또 개수만 반환한다" {
val money = PRICE * 3
val manualCount = 0
val lottoCount = LottoCount.of(manualCount, money)
lottoCount.manualCount shouldBe 0
lottoCount.autoCount shouldBe 3
}
}
},
)
36 changes: 19 additions & 17 deletions src/test/kotlin/model/lotto/LottoNumberTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@ import io.kotest.matchers.shouldBe

private const val INVALID_LOTTO_NUMBER_MESSAGE = "로또 번호는 1에서 45 사이여야 합니다."

class LottoNumberTest : FreeSpec({
"LottoNumber 생성" - {
"번호가 1 미만일 때는 예외가 발생해야한다." {
shouldThrowExactly<IllegalArgumentException> {
LottoNumber(0)
}.message shouldBe INVALID_LOTTO_NUMBER_MESSAGE
class LottoNumberTest : FreeSpec(
{
"LottoNumber 생성" - {
"번호가 1 미만일 때는 예외가 발생해야한다." {
shouldThrowExactly<IllegalArgumentException> {
LottoNumber(0)
}.message shouldBe INVALID_LOTTO_NUMBER_MESSAGE
}
"번호가 46 이상일 때는 예외가 발생해야한다." {
shouldThrowExactly<IllegalArgumentException> {
LottoNumber(46)
}.message shouldBe INVALID_LOTTO_NUMBER_MESSAGE
}
"번호가 1 이상 45 이하일 때는 인스턴스가 생성되어야한다." {
LottoNumber(1) shouldBe LottoNumber(1)
LottoNumber(45) shouldBe LottoNumber(45)
}
}
"번호가 46 이상일 때는 예외가 발생해야한다." {
shouldThrowExactly<IllegalArgumentException> {
LottoNumber(46)
}.message shouldBe INVALID_LOTTO_NUMBER_MESSAGE
}
"번호가 1 이상 45 이하일 때는 인스턴스가 생성되어야한다." {
LottoNumber(1) shouldBe LottoNumber(1)
LottoNumber(45) shouldBe LottoNumber(45)
}
}
})
},
)
29 changes: 29 additions & 0 deletions src/test/kotlin/model/lotto/LottoNumbersTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package model.lotto

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import view.INVALID_LOTTO_NUMBER_COUNT_MESSAGE

class LottoNumbersTest : DescribeSpec(
{
describe("로또 번호는 6개의 유일한 숫자로 구성되어야 한다") {
it("6개가 아니면 예외가 발생한다") {
val numbers = listOf(1, 2, 3, 4, 5)
val exception = shouldThrow<IllegalStateException> {
LottoNumbers.from(numbers)
}
exception.message shouldBe INVALID_LOTTO_NUMBER_COUNT_MESSAGE
}

it("6개면 예외가 발생하지 않는다") {
val numbers = listOf(1, 2, 3, 4, 5, 6)
LottoNumbers.from(numbers)
}

xit("중복된 숫자가 있으면 예외가 발생한다") {
TODO("Not yet implemented")
}
}
},
)
46 changes: 24 additions & 22 deletions src/test/kotlin/model/money/AmountTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,31 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import java.math.BigDecimal

class AmountTest : FunSpec({
listOf(
"2400",
"3000",
"4000",
).forEach {
test("금액은 입력받으면 BigDecimal 자료형을 반환한다. 입력받은 금액 : $it") {
Amount(it).getBigDecimal() shouldBe BigDecimal(it)
class AmountTest : FunSpec(
{
listOf(
"2400",
"3000",
"4000",
).forEach {
test("금액은 입력받으면 BigDecimal 자료형을 반환한다. 입력받은 금액 : $it") {
Amount(it).getBigDecimal() shouldBe BigDecimal(it)
}
}
}

listOf(
"invalid amount",
"유효하지 않은 숫자",
"3k34j4",
"k3333",
"3333k",
"",
).forEach {
test("잘못된 문자열을 입력받으면 예외가 발생한다. 입력받은 문자 : $it") {
shouldThrowExactly<IllegalArgumentException> {
Amount(it)
listOf(
"invalid amount",
"유효하지 않은 숫자",
"3k34j4",
"k3333",
"3333k",
"",
).forEach {
test("잘못된 문자열을 입력받으면 예외가 발생한다. 입력받은 문자 : $it") {
shouldThrowExactly<IllegalArgumentException> {
Amount(it)
}
}
}
}
})
},
)
Loading

0 comments on commit 3227453

Please sign in to comment.