diff --git a/src/main/kotlin/lotto/LottoApp.kt b/src/main/kotlin/lotto/LottoApp.kt index fd3a7f5d3e..4eb37d9496 100644 --- a/src/main/kotlin/lotto/LottoApp.kt +++ b/src/main/kotlin/lotto/LottoApp.kt @@ -1,19 +1,21 @@ package lotto import lotto.domain.BuyPrice -import lotto.domain.ProfitRate +import lotto.domain.LottoResult import lotto.domain.WinningLotto import lotto.domain.strategy.LottoAutoGeneratorStrategy import lotto.domain.strategy.LottoFactory +import lotto.ui.InputViews.inputBonusNumber import lotto.ui.InputViews.inputPrice import lotto.ui.InputViews.inputWinningNumber +import lotto.ui.OutputViews.printBonusNumber import lotto.ui.OutputViews.printBoughtLotto import lotto.ui.OutputViews.printBoughtLottos import lotto.ui.OutputViews.printLottoMatchResult import lotto.ui.OutputViews.printProfitRate fun main() { - val inputPrice = inputPrice() ?: 0 + val inputPrice = inputPrice() val buyPrice = BuyPrice(inputPrice) val lottoCount = buyPrice.getLottoCount() @@ -22,13 +24,18 @@ fun main() { val lottoFactory = LottoFactory() val generateStrategy = LottoAutoGeneratorStrategy() - val lotto = lottoFactory.generate(lottoCount, generateStrategy) + val lottos = lottoFactory.generate(lottoCount, generateStrategy) - printBoughtLottos(lotto.value) + printBoughtLottos(lottos.value) - val winningNumbers = WinningLotto(inputWinningNumber()).winningNumbers - val matching = lotto.matchWinningNumbers(winningNumbers) + val winningNumber = inputWinningNumber() + val bonusNumber = inputBonusNumber() + printBonusNumber(bonusNumber) + + val winningNumbers = WinningLotto.of(winningNumber, bonusNumber) + + val matching = lottos.matchWinningNumbers(winningNumbers, bonusNumber) printLottoMatchResult(matching) - printProfitRate(ProfitRate(matching, lottoCount).calculate()) + printProfitRate(LottoResult(matching, inputPrice).calculate()) } diff --git a/src/main/kotlin/lotto/domain/Lotto.kt b/src/main/kotlin/lotto/domain/Lotto.kt index bcad070f34..5b4b248c0b 100644 --- a/src/main/kotlin/lotto/domain/Lotto.kt +++ b/src/main/kotlin/lotto/domain/Lotto.kt @@ -1,3 +1,9 @@ package lotto.domain -class Lotto(val value: List) +@JvmInline +value class Lotto(val value: List) { + + fun findWinningCount(winningLotto: WinningLotto): Int { + return value.count { winningLotto.winningNumbers.contains(it) } + } +} diff --git a/src/main/kotlin/lotto/domain/LottoResult.kt b/src/main/kotlin/lotto/domain/LottoResult.kt new file mode 100644 index 0000000000..9f94eb1753 --- /dev/null +++ b/src/main/kotlin/lotto/domain/LottoResult.kt @@ -0,0 +1,27 @@ +package lotto.domain + +import java.math.RoundingMode +import java.text.DecimalFormat + +class LottoResult( + private val matchResult: Map, + private val inputPrice: Int +) { + + fun calculate(): Double { + val totalWinningMoney = getTotalWinningMoney() + val decimalFormat = DecimalFormat(PROFIT_RATE_PATTERN) + decimalFormat.roundingMode = RoundingMode.DOWN + val profitRate = totalWinningMoney / inputPrice.toDouble() + return decimalFormat.format(profitRate).toDouble() + } + + private fun getTotalWinningMoney(): Int { + return matchResult.entries + .sumOf { it.key.getTotalWinningMoney(it.value) } + } + + companion object { + private const val PROFIT_RATE_PATTERN = "#.##" + } +} diff --git a/src/main/kotlin/lotto/domain/Lottos.kt b/src/main/kotlin/lotto/domain/Lottos.kt index 5b3ecd14ce..c93f54c82f 100644 --- a/src/main/kotlin/lotto/domain/Lottos.kt +++ b/src/main/kotlin/lotto/domain/Lottos.kt @@ -1,14 +1,14 @@ package lotto.domain -class Lottos(val value: List) { +@JvmInline +value class Lottos(val value: List) { - fun matchWinningNumbers(winningNumbers: List): Map { - return value.groupingBy { lottoNumber -> - Ranking.valueOf( - lottoNumber.value.count { - winningNumbers.contains(it) - } - ) + fun matchWinningNumbers(winningNumbers: WinningLotto, bonusNumber: Int): Map { + return value.groupingBy { lotto -> + val findWinningCount = lotto.findWinningCount(winningNumbers) + val isBonus = winningNumbers.isMatchBonus(bonusNumber) + + Ranking.valueOf(findWinningCount, isBonus) }.eachCount() } } diff --git a/src/main/kotlin/lotto/domain/Ranking.kt b/src/main/kotlin/lotto/domain/Ranking.kt index e6665e96c5..67e1bb7128 100644 --- a/src/main/kotlin/lotto/domain/Ranking.kt +++ b/src/main/kotlin/lotto/domain/Ranking.kt @@ -6,9 +6,10 @@ enum class Ranking( ) { FIRST(6, 2_000_000_000), - SECOND(5, 1_500_000), - THIRD(4, 50_000), - FOURTH(3, 5_000), + SECOND(5, 30_000_000), + THIRD(5, 1_500_000), + FOURTH(4, 50_000), + FIFTH(3, 5_000), MISS(0, 0); fun getTotalWinningMoney(matchResultValue: Int): Int { @@ -16,8 +17,13 @@ enum class Ranking( } companion object { - fun valueOf(matchCount: Int): Ranking { - return values().find { it.matchCount == matchCount } ?: MISS + fun valueOf(matchCount: Int, isBonus: Boolean = false): Ranking { + return values().find { + if (matchCount == 5 && !isBonus) { + return THIRD + } + it.matchCount == matchCount + } ?: MISS } } } diff --git a/src/main/kotlin/lotto/domain/WinningLotto.kt b/src/main/kotlin/lotto/domain/WinningLotto.kt index bae735bc31..f8eceda823 100644 --- a/src/main/kotlin/lotto/domain/WinningLotto.kt +++ b/src/main/kotlin/lotto/domain/WinningLotto.kt @@ -1,11 +1,10 @@ package lotto.domain class WinningLotto( - inputNumbers: List + val winningNumbers: List, + private val bonusNumber: Int ) { - val winningNumbers = inputNumbers.map { it.toInt() } - init { if (winningNumbers.isEmpty() || winningNumbers.size != LIMIT_WINNING_NUMBER) { throw IllegalArgumentException("당첨 번호는 6개 입니다.") @@ -13,10 +12,14 @@ class WinningLotto( validateWinningNumber() } + fun isMatchBonus(bonusNumber: Int): Boolean { + return this.bonusNumber == bonusNumber + } + private fun validateWinningNumber() { for (winningNumber in winningNumbers) { - if (winningNumber !in LOTTO_FIRST_NUMBER..LOTTO_LAST_NUMBER) { - throw IllegalArgumentException("당첨 번호는 1 ~ 45 범위의 숫자로만 구성될 수 있습니다.") + require(winningNumber in LOTTO_FIRST_NUMBER..LOTTO_LAST_NUMBER) { + MESSAGE_LOTTO_RANGE } } } @@ -26,5 +29,10 @@ class WinningLotto( private const val LOTTO_FIRST_NUMBER = 1 private const val LOTTO_LAST_NUMBER = 45 + private const val MESSAGE_LOTTO_RANGE = "당첨 번호는 1 ~ 45 범위의 숫자로만 구성될 수 있습니다." + + fun of(winningNumbers: List, bonusNumber: Int): WinningLotto { + return WinningLotto(winningNumbers.map { it.toInt() }, bonusNumber) + } } } diff --git a/src/main/kotlin/lotto/domain/strategy/LottoAutoGeneratorStrategy.kt b/src/main/kotlin/lotto/domain/strategy/LottoAutoGeneratorStrategy.kt index a52436742b..67c9611531 100644 --- a/src/main/kotlin/lotto/domain/strategy/LottoAutoGeneratorStrategy.kt +++ b/src/main/kotlin/lotto/domain/strategy/LottoAutoGeneratorStrategy.kt @@ -1,7 +1,7 @@ package lotto.domain.strategy -import lotto.domain.Lottos import lotto.domain.Lotto +import lotto.domain.Lottos class LottoAutoGeneratorStrategy : LottoGeneratorStrategy { @@ -16,7 +16,6 @@ class LottoAutoGeneratorStrategy : LottoGeneratorStrategy { .shuffled() .subList(LOTTO_FIRST_INDEX, LOTTO_LAST_INDEX) .sorted() - .toList() return Lotto(lottoNumbers) } diff --git a/src/main/kotlin/lotto/ui/InputViews.kt b/src/main/kotlin/lotto/ui/InputViews.kt index 3e14d7440c..35be7a9b91 100644 --- a/src/main/kotlin/lotto/ui/InputViews.kt +++ b/src/main/kotlin/lotto/ui/InputViews.kt @@ -4,8 +4,10 @@ object InputViews { private const val MESSAGE_INPUT_PRICE = "구입금액을 입력해 주세요." private const val MESSAGE_INPUT_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요." + private const val MESSAGE_INPUT_BONUS_NUMBER = "보너스 볼을 입력해주세요." private const val DELIMITERS = ", " private const val DEFAULT_PRICE = 0 + private const val DEFAULT_BONUS_NUMBER = 0 fun inputPrice(): Int { println(MESSAGE_INPUT_PRICE) @@ -16,4 +18,9 @@ object InputViews { println(MESSAGE_INPUT_WINNING_NUMBER) return readLine()?.split(DELIMITERS) ?: emptyList() } + + fun inputBonusNumber(): Int { + println(MESSAGE_INPUT_BONUS_NUMBER) + return readLine()?.toIntOrNull() ?: DEFAULT_BONUS_NUMBER + } } diff --git a/src/main/kotlin/lotto/ui/OutputViews.kt b/src/main/kotlin/lotto/ui/OutputViews.kt index b1709f11a7..73c20ee934 100644 --- a/src/main/kotlin/lotto/ui/OutputViews.kt +++ b/src/main/kotlin/lotto/ui/OutputViews.kt @@ -20,7 +20,11 @@ object OutputViews { } fun printBoughtLotto(lottoCount: Int) { - println("${lottoCount}${MESSAGE_BOUGHT_LOTTO}") + println("${lottoCount}$MESSAGE_BOUGHT_LOTTO") + } + + fun printBonusNumber(bonusNumber: Int) { + println(bonusNumber) } fun printLottoMatchResult(matchResult: Map) { @@ -28,7 +32,7 @@ object OutputViews { println("---------") println("${FOURTH.matchCount}개 일치 (${FOURTH.winningMoney}원) - ${matchResult[FOURTH] ?: 0}개") println("${THIRD.matchCount}개 일치 (${THIRD.winningMoney}원) - ${matchResult[THIRD] ?: 0}개") - println("${SECOND.matchCount}개 일치 (${SECOND.winningMoney}원) - ${matchResult[SECOND] ?: 0}개") + println("${SECOND.matchCount}개 일치, 보너스 볼 일치 (${SECOND.winningMoney}원) - ${matchResult[SECOND] ?: 0}개") println("${FIRST.matchCount}개 일치 (${FIRST.winningMoney}원) - ${matchResult[FIRST] ?: 0}개") } diff --git a/src/test/kotlin/lotto/domain/LottoResultTest.kt b/src/test/kotlin/lotto/domain/LottoResultTest.kt new file mode 100644 index 0000000000..746855552a --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoResultTest.kt @@ -0,0 +1,27 @@ +package lotto.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class LottoResultTest { + + @Test + fun `구매금액 대비 당첨금액의 수익률을 계산한다`() { + // given + val matchResult = mapOf( + Ranking.FIRST to 0, + Ranking.SECOND to 0, + Ranking.THIRD to 0, + Ranking.FOURTH to 1 + ) + + val price = 14000 + + // when + val actual = LottoResult(matchResult, price).calculate() + + // then + val expected = 0.35 + assertThat(actual).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/lotto/domain/LottosTest.kt b/src/test/kotlin/lotto/domain/LottosTest.kt new file mode 100644 index 0000000000..6989af8b16 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottosTest.kt @@ -0,0 +1,41 @@ +package lotto.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class LottosTest { + + @Test + fun `구매한 로또번호의 랭킹을 확인한다`() { + // given + val lottos = Lottos( + listOf(Lotto(listOf(1, 2, 3, 4, 5, 6))) + ) + val bonusNumber = 7 + val winningLotto = WinningLotto(listOf(1, 2, 3, 4, 5, 6), bonusNumber) + + // when + val actual = lottos.matchWinningNumbers(winningLotto, bonusNumber) + + // then + val expected = mapOf(Ranking.FIRST to 1) + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `구매한 로또번호가 5개 맞고 보너스번호가 맞을 경우 2등이 된다`() { + // given + val lottos = Lottos( + listOf(Lotto(listOf(1, 2, 3, 4, 5, 7))) + ) + val bonusNumber = 7 + val winningLotto = WinningLotto(listOf(1, 2, 3, 4, 5, 6), bonusNumber) + + // when + val actual = lottos.matchWinningNumbers(winningLotto, bonusNumber) + + // then + val expected = mapOf(Ranking.SECOND to 1) + assertThat(actual).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/lotto/domain/WinningLottosTest.kt b/src/test/kotlin/lotto/domain/WinningLottosTest.kt index 728e646c5a..6b4477d222 100644 --- a/src/test/kotlin/lotto/domain/WinningLottosTest.kt +++ b/src/test/kotlin/lotto/domain/WinningLottosTest.kt @@ -7,12 +7,13 @@ import org.junit.jupiter.api.assertThrows internal class WinningLottosTest { @Test - fun `당첨번호를 6개 입력한다`() { + fun `당첨번호를 6개와 보너스 번호를 입력한다`() { // given val inputWinningNumber = listOf("1", "2", "3", "4", "5", "6") + val bonusNumber = 7 // when - val actual = WinningLotto(inputWinningNumber).winningNumbers.size + val actual = WinningLotto.of(inputWinningNumber, bonusNumber).winningNumbers.size // then val expected = 6 @@ -22,21 +23,47 @@ internal class WinningLottosTest { @Test fun `당첨번호가 입력되지 않으면 예외가 발생한다`() { assertThrows { - WinningLotto(emptyList()) + WinningLotto(emptyList(), 7) } } @Test fun `당첨번호가 6개가 아니면 예외가 발생한다`() { assertThrows { - WinningLotto(listOf("1", "2")) + WinningLotto.of(listOf("1", "2"), 7) } } @Test fun `당첨번호가 1에서 45숫자가 아니면 예외가 발생한다`() { assertThrows { - WinningLotto(listOf("1", "2", "3", "4", "5", "46")) + WinningLotto.of(listOf("1", "2", "3", "4", "5", "46"), 7) } } + + @Test + fun `입력한 로또 번호에 보너스 번호가 포함될 경우 true`() { + // given + val inputWinningNumber = listOf("1", "2", "3", "4", "5", "7") + val bonusNumber = 7 + + // when + val actual = WinningLotto.of(inputWinningNumber, bonusNumber).isMatchBonus(bonusNumber) + + // then + assertThat(actual).isTrue + } + + @Test + fun `입력한 로또 번호에 보너스 번호가 포함되지 않은 경우 false`() { + // given + val inputWinningNumber = listOf("1", "2", "3", "4", "5", "6") + val bonusNumber = 7 + + // when + val actual = WinningLotto.of(inputWinningNumber, bonusNumber).isMatchBonus(8) + + // then + assertThat(actual).isFalse + } }