Skip to content
Open
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
# kotlin-lotto
# kotlin-lotto

기능 요구사항
- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- [x] 로또 구입 금액을 입력할 수 있어야 한다.
- [x] 구입 금액이 1000원 단위가 아닌 경우, 안내 문구와 함께 재시도할 수 있도록 한다.
- [x] 로또 1장의 가격은 1000원이며, 구입 금액에 맞는 로또를 여러개 생성한다.
- [x] 지난 주 당첨 번호를 입력할 수 있어야 한다.
- [x] 1 ~ 45 사이의 중복 없는 6개 숫자가 아닐 경우, 안내 문구와 함께 재시도할 수 있도록 한다.
- [x] 숫자는 스페이스바로 구분한다.
- [x] 당첨 통계 결과를 출력한다.
- [x] 당첨 통계 갯수와 금액 정보는 enum을 활용해서 생성한다.
- [x] 당첨 통계 판독기에서 로또 번호와 당첨 번호를 비교해 당첨 통계 / 수익률을 생성한다.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ group = "camp.nextstep.edu"
version = "1.0-SNAPSHOT"

kotlin {
jvmToolchain(21)
jvmToolchain(17)
}

repositories {
Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/lotto/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lotto

private const val MINIMUM_LOTTO_NUMBER = 1
private const val MAXIMUM_LOTTO_NUMBER = 45
private const val LOTTO_NUMBER_COUNT = 6

class Lotto(input: List<Int>) {
val numbers = input

init {
input.forEach {
if (!(MINIMUM_LOTTO_NUMBER..MAXIMUM_LOTTO_NUMBER).contains(it)) {
throw IllegalArgumentException("1부터 45까지의 숫자를 입력하세요")
}
}
}

constructor() : this((MINIMUM_LOTTO_NUMBER..MAXIMUM_LOTTO_NUMBER).toList().shuffled().take(LOTTO_NUMBER_COUNT))
}
38 changes: 38 additions & 0 deletions src/main/kotlin/lotto/LottoGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package lotto

fun main() {
val lottos = buyLotto()
showLottoResult(lottos)
}

fun buyLotto(): List<Lotto> {
println("구입금액을 입력해 주세요.")
val moneyInput = readln()
try {
val money = Money(moneyInput)
val buyLotto = LottoShop().buyLotto(money)
println("${buyLotto.size}개를 구매했습니다.")
buyLotto.forEach {
println(it.numbers)
}
return buyLotto
} catch (e: IllegalArgumentException) {
println(e.message)
buyLotto()
}
return listOf()
}

fun showLottoResult(lottos: List<Lotto>) {
println("지난 주 당첨 번호를 입력해 주세요.")
val winLottoInput = readln()
try {
val winLotto = WinLotto(winLottoInput)
val lottoResult = LottoResult(winLotto, lottos)
lottoResult.process()
lottoResult.printResult()
} catch (e: IllegalArgumentException) {
println(e.message)
showLottoResult(lottos)
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/lotto/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto

import java.util.EnumMap

class LottoResult(val winLotto: WinLotto, val lottos: List<Lotto>) {
val matchMap: EnumMap<Rank, Int> = EnumMap(Rank::class.java)
var rateOfReturn = 0.0

fun process() {
match()
rateOfReturn()
}

private fun rateOfReturn() {
val totalMoney = lottos.size * LottoShop.LOTTO_UNIT_PRICE
val winningMoney = matchMap.entries.sumOf { entry -> entry.key.winningMoney * entry.value }
rateOfReturn = winningMoney.toDouble() / totalMoney.toDouble()
}

private fun match() {
lottos.forEach { lotto ->
val rank = Rank.valueOf(winLotto.matchCount(lotto))
matchMap[rank] = matchMap.getOrDefault(rank, 0) + 1
}
}

fun printResult() {
println(
"""
당첨 통계
${Rank.entries.filter { it != Rank.MISS }
.sorted()
.reversed()
.joinToString("\n") { "${it.countOfMatch}개 일치 (${it.winningMoney}원)- ${matchMap.getOrDefault(it, 0)}개" }}
총 수익률은 %.2f 입니다."
"""
.trimIndent()
.format(rateOfReturn),
)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/lotto/LottoShop.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lotto

class LottoShop {
companion object {
const val LOTTO_UNIT_PRICE = 1000
}

fun buyLotto(money: Money): List<Lotto> {
if (money.price % LOTTO_UNIT_PRICE != 0) {
throw IllegalArgumentException("${LOTTO_UNIT_PRICE}원 단위로 입력안됨")
}

val lottoCount = money.price / LOTTO_UNIT_PRICE

return lottoCount.downTo(1).map {
Lotto()
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/Money.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lotto

class Money {
val price: Int

constructor(price: String?) {
if (price.isNullOrBlank()) {
throw IllegalArgumentException("뭐라도 입력하세요")
}
if (!price.matches("^\\d+$".toRegex()) || price.toInt() <= 0) {
throw IllegalArgumentException("올바른 금액을 입력하세요")
}
this.price = price.toInt()
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/lotto/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lotto

enum class Rank(val countOfMatch: Int, val winningMoney: Int) {
FIRST(6, 2_000_000_000),

// SECOND(5, 30_000_000),
THIRD(5, 1_500_000),
FOURTH(4, 50_000),
FIFTH(3, 5_000),
MISS(0, 0),
;

companion object {
fun valueOf(countOfMatch: Int): Rank {
return Rank.entries.find {
it.countOfMatch == countOfMatch
} ?: MISS
}
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/lotto/WinLotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lotto

class WinLotto {
val winLotto: Lotto

constructor(input: String?) {
if (input.isNullOrBlank()) throw IllegalArgumentException("뭐라도 입력하세요")

val splitted = splitToSix(input)

splitted.forEach { if (!it.matches("^\\d+$".toRegex())) throw IllegalArgumentException("올바른 숫자를 입력하세요") }

val numbers = splitted.map { it.toInt() }
this.winLotto = Lotto(numbers)
}

private fun splitToSix(input: String): List<String> {
val splitted = input.trim().split(" ")
if (splitted.distinct().size != 6) {
throw IllegalArgumentException("6개의 서로 다른 숫자를 입력하세요")
}
return splitted
}

fun matchCount(lotto: Lotto): Int {
return winLotto.numbers.intersect(lotto.numbers.toSet()).size
}
}
46 changes: 46 additions & 0 deletions src/test/kotlin/lotto/LottoResultTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package lotto

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class LottoResultTest : FreeSpec({
"로또 당첨 결과 및 수익률 테스트" - {
val winLotto = WinLotto("1 2 3 4 5 6")

"당첨된 숫자가 하나도 없는 경우 MISS 1개 나오고 수익률은 0 이다." {
val lottos = listOf(Lotto(listOf(7, 8, 9, 10, 11, 12)))
val lottoResult = LottoResult(winLotto, lottos)

// when
lottoResult.process()

// then
lottoResult.matchMap shouldBe mapOf(Pair(Rank.MISS, 1))
lottoResult.rateOfReturn shouldBe 0.0
}

"1등에 당첨된 경우 FIRST 1개 나오고 수익률은 2000000.0 이다" {
val lottos = listOf(Lotto(listOf(1, 2, 3, 4, 5, 6)))
val lottoResult = LottoResult(winLotto, lottos)

// when
lottoResult.process()

// then
lottoResult.matchMap shouldBe mapOf(Pair(Rank.FIRST, 1))
lottoResult.rateOfReturn shouldBe 2000000.0
}

"1등에 당첨되었고, 로또를 2장 산 경우 FIRST 1개, MISS 1개 나오고 수익률은 1000000.0 이다" {
val lottos = listOf(Lotto(listOf(1, 2, 3, 4, 5, 6)), Lotto(listOf(2, 5, 10, 11, 12, 13)))
val lottoResult = LottoResult(winLotto, lottos)

// when
lottoResult.process()

// then
lottoResult.matchMap shouldBe mapOf(Pair(Rank.FIRST, 1), Pair(Rank.MISS, 1))
lottoResult.rateOfReturn shouldBe 1000000.0
}
}
})
30 changes: 30 additions & 0 deletions src/test/kotlin/lotto/LottoShopTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class LottoShopTest : FreeSpec({

"로또 구매 테스트" - {
val lottoShop = LottoShop()

"1000원 단위가 아닐 때 (999원) 예외 발생" {
val exception =
shouldThrow<IllegalArgumentException> {
lottoShop.buyLotto(Money("999"))
}
exception.message shouldBe "1000원 단위로 입력안됨"
}

"1000원 단위일 때 (1000원) 로또 1장 구매됨" {
val lotto = lottoShop.buyLotto(Money("1000"))
lotto.size shouldBe 1
}

"1000원 단위일 때 (2000원) 로또 2장 구매됨" {
val lotto = lottoShop.buyLotto(Money("2000"))
lotto.size shouldBe 2
}
}
})
13 changes: 13 additions & 0 deletions src/test/kotlin/lotto/LottoTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package lotto

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class LottoTest : FreeSpec({

"로또 생성 시 1~45 사이 숫자 6개로 구성된다." {
val lotto = Lotto()
lotto.numbers.size shouldBe 6
lotto.numbers.all { it in 1..45 } shouldBe true
}
})
47 changes: 47 additions & 0 deletions src/test/kotlin/lotto/MoneyTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package lotto

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class MoneyTest : FreeSpec({

"Money 객체의 유효성 검증 테스트" - {
"input이 null" {
val input = null
val exception =
shouldThrow<IllegalArgumentException> { Money(input) }
exception.message shouldBe "뭐라도 입력하세요"
}
"input이 빈 문자열" {
val input = ""
val exception =
shouldThrow<IllegalArgumentException> { Money(input) }
exception.message shouldBe "뭐라도 입력하세요"
}
"input이 스페이스바 문자열" {
val input = " "
val exception =
shouldThrow<IllegalArgumentException> { Money(input) }
exception.message shouldBe "뭐라도 입력하세요"
}
"input에 문자가 포함된 경우" {
val input = "10000원"
val exception =
shouldThrow<IllegalArgumentException> { Money(input) }
exception.message shouldBe "올바른 금액을 입력하세요"
}
"input이 양수가 아닌 경우" {
val input = "-1000"
val exception =
shouldThrow<IllegalArgumentException> { Money(input) }
exception.message shouldBe "올바른 금액을 입력하세요"
}
"input이 0원인 경우" {
val input = "0"
val exception =
shouldThrow<IllegalArgumentException> { Money(input) }
exception.message shouldBe "올바른 금액을 입력하세요"
}
}
})
16 changes: 16 additions & 0 deletions src/test/kotlin/lotto/RankTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lotto

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class RankTest : FreeSpec({

"3개가 일치하면 5등이 나온다." {
val rank = Rank.valueOf(3)
rank shouldBe Rank.FIFTH
}
"2개가 일치하면 미스가 나온다." {
val rank = Rank.valueOf(2)
rank shouldBe Rank.MISS
}
})
29 changes: 29 additions & 0 deletions src/test/kotlin/lotto/WinLottoTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package lotto

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class WinLottoTest : FreeSpec({

"문자가 포함된 경우 예외 발생" {
val exception =
shouldThrow<IllegalArgumentException> { WinLotto("1 2 3 4 5 육") }
exception.message shouldBe "올바른 숫자를 입력하세요"
}
"1보다 작거나 45보다 큰 숫자가 포함된 경우 예외 발생" {
val exception =
shouldThrow<IllegalArgumentException> { WinLotto("0 2 3 4 5 46") }
exception.message shouldBe "1부터 45까지의 숫자를 입력하세요"
}
"5개의 숫자를 입력하는 경우 예외 발생" {
val exception =
shouldThrow<IllegalArgumentException> { WinLotto("2 4 6 8 10") }
exception.message shouldBe "6개의 서로 다른 숫자를 입력하세요"
}
"6개 숫자 간 중복이 있는 경우" {
val exception =
shouldThrow<IllegalArgumentException> { WinLotto("2 4 6 8 10 2") }
exception.message shouldBe "6개의 서로 다른 숫자를 입력하세요"
}
})