diff --git a/README.md b/README.md index cbae739405..eab8dad43f 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# kotlin-lotto \ No newline at end of file +# kotlin-lotto + +## TODO +- [x] 로또를 생성할 수 있다. + - [x] 중복된 숫자가 포함된 로또를 생성할 수 없다. + - [x] 생성된 로또 숫자들을 오름차순으로 조회할 수 있다. +- [x] 로또 라운드에서 당첨번호를 통해 결과를 도출할 수 있다. + - [x] 해당 라운드에서 발행된 로또중 당첨번호와 일치하는 숫자 개수별로 구분할 수 있다. + - [x] 일치하는 숫자 개수별로 수익률을 알 수 있다. \ No newline at end of file diff --git a/src/main/kotlin/lotto/domain/Lotto.kt b/src/main/kotlin/lotto/domain/Lotto.kt new file mode 100644 index 0000000000..baf30c90f9 --- /dev/null +++ b/src/main/kotlin/lotto/domain/Lotto.kt @@ -0,0 +1,50 @@ +package lotto.domain +class Lotto(lottoNumbers: List) { + val lottoNumbers: List + + init { + require(lottoNumbers.size == LOTTO_SIZE) + hasNoDuplicatedNumbers(lottoNumbers) + + this.lottoNumbers = lottoNumbers.sortedBy { it.number } + } + + fun getSameNumberCount(lotto: Lotto): Int { + return (LOTTO_SIZE * 2) - (lottoNumbers + lotto.lottoNumbers).toSet().size + } + + private fun hasNoDuplicatedNumbers(lottoNumbers: List) { + require(lottoNumbers.toSet().size == LOTTO_SIZE) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Lotto) return false + + if (lottoNumbers != other.lottoNumbers) return false + + return true + } + + override fun hashCode(): Int { + return lottoNumbers.hashCode() + } + + companion object { + const val LOTTO_SIZE = 6 + + fun of(lottoNumbers: List): Lotto = Lotto(lottoNumbers.map { LottoNumber(it) }) + } +} + +data class LottoNumber(val number: Int) { + init { + require(MIN_LOTTO_NUMBER <= number) + require(number <= MAX_LOTTO_NUMBER) + } + + companion object { + const val MIN_LOTTO_NUMBER = 1 + const val MAX_LOTTO_NUMBER = 45 + } +} diff --git a/src/main/kotlin/lotto/domain/LottoGenerator.kt b/src/main/kotlin/lotto/domain/LottoGenerator.kt new file mode 100644 index 0000000000..7fc279bd93 --- /dev/null +++ b/src/main/kotlin/lotto/domain/LottoGenerator.kt @@ -0,0 +1,17 @@ +package lotto.domain + +import lotto.domain.Lotto.Companion.LOTTO_SIZE +import lotto.domain.LottoNumber.Companion.MAX_LOTTO_NUMBER +import lotto.domain.LottoNumber.Companion.MIN_LOTTO_NUMBER + +interface LottoGenerator { + fun generate(): Lotto +} + +class RandomLottoGenerator : LottoGenerator { + override fun generate(): Lotto = LOTTO_NUMBERS.shuffled().subList(0, LOTTO_SIZE).let { Lotto(it) } + + companion object { + private val LOTTO_NUMBERS = (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER).map { LottoNumber(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..4b457676d3 --- /dev/null +++ b/src/main/kotlin/lotto/domain/LottoResult.kt @@ -0,0 +1,29 @@ +package lotto.domain + +import lotto.domain.Money.Companion.toMoney + +data class LottoResult( + val lotto: Lotto, + val winningLotto: Lotto +) { + val sameNumberCount: Int = winningLotto.getSameNumberCount(lotto) + + val reward: LottoReward? = LottoReward.getReward(sameNumberCount) +} + +enum class LottoReward(val prize: Long, val sameNumberCount: Int) { + WINNER_1ST(2000L.millionWon(), 6), + WINNER_2ST(1500L.thousandWon(), 5), + WINNER_3ST(50L.thousandWon(), 4), + WINNER_4ST(5L.thousandWon(), 3); + + fun toMoney(): Money = prize.toMoney() + + companion object { + fun getReward(sameNumberCount: Int): LottoReward? = values().firstOrNull { sameNumberCount == it.sameNumberCount } + } +} + +private fun Long.thousandWon(): Long = this * 1000L + +private fun Long.millionWon(): Long = this.thousandWon().thousandWon() diff --git a/src/main/kotlin/lotto/domain/LottoRound.kt b/src/main/kotlin/lotto/domain/LottoRound.kt new file mode 100644 index 0000000000..bfa18c3f26 --- /dev/null +++ b/src/main/kotlin/lotto/domain/LottoRound.kt @@ -0,0 +1,24 @@ +package lotto.domain +class LottoRound(private val lottoGenerator: LottoGenerator) { + constructor(lottoRoundElements: LottoRoundElements) : this(lottoRoundElements.lottoGenerator) + + private val lottos: MutableList = mutableListOf() + + fun addNewLottos(newLottoSize: Int) { + repeat(newLottoSize) { + lottos.add(newLotto()) + } + } + + fun getLottos(): List = lottos.toList() + + fun lotteryDraw(winningLotto: Lotto): LottoRoundStatistics { + return LottoRoundStatistics(lottos, winningLotto) + } + + private fun newLotto() = lottoGenerator.generate() +} + +data class LottoRoundElements( + val lottoGenerator: LottoGenerator = RandomLottoGenerator() +) diff --git a/src/main/kotlin/lotto/domain/LottoRoundStatistics.kt b/src/main/kotlin/lotto/domain/LottoRoundStatistics.kt new file mode 100644 index 0000000000..f6c707c317 --- /dev/null +++ b/src/main/kotlin/lotto/domain/LottoRoundStatistics.kt @@ -0,0 +1,28 @@ +package lotto.domain + +import lotto.domain.Money.Companion.NO_MONEY +import lotto.domain.Money.Companion.plus + +class LottoRoundStatistics( + lottos: List, + private val winningLotto: Lotto +) { + val lottoResults: List = lottos.map { LottoResult(it, winningLotto) } + + val totalPrize: Money = lottoResults.mapNotNull { it.reward } + .fold(NO_MONEY) { money, lottoReward -> + money + lottoReward.toMoney() + } + + fun getLottoRewardOf(lottoReward: LottoReward): List = lottoResults.filter { it.reward == lottoReward }.toList() +} + +@JvmInline value class Money(val value: Long) { + companion object { + fun Long.toMoney(): Money = Money(this) + + operator fun Money.plus(other: Money): Money = (value + other.value).toMoney() + + val NO_MONEY: Money = 0L.toMoney() + } +} diff --git a/src/main/kotlin/lotto/domain/LottoServiceRound.kt b/src/main/kotlin/lotto/domain/LottoServiceRound.kt new file mode 100644 index 0000000000..2d79b945a9 --- /dev/null +++ b/src/main/kotlin/lotto/domain/LottoServiceRound.kt @@ -0,0 +1,25 @@ +package lotto.domain + +import lotto.domain.Money.Companion.toMoney + +class LottoServiceRound { + private val lottoRound = LottoRound(LottoRoundElements()) + + fun buyLottos(payment: Long): List { + lottoRound.addNewLottos(payment.buyableCount()) + return lottoRound.getLottos() + } + + fun allPayment(): Long = (lottoRound.getLottos().size * LOTTO_BUY_PRIZE.value) + + fun lotteryDraw(numbers: List): LottoRoundStatistics { + val winningLotto = Lotto.of(numbers) + return lottoRound.lotteryDraw(winningLotto) + } + + private fun Long.buyableCount(): Int = (this / LOTTO_BUY_PRIZE.value).toInt() + + companion object { + private val LOTTO_BUY_PRIZE = 1000L.toMoney() + } +} diff --git a/src/main/kotlin/lotto/view/LottoInputView.kt b/src/main/kotlin/lotto/view/LottoInputView.kt new file mode 100644 index 0000000000..5c8ebf479c --- /dev/null +++ b/src/main/kotlin/lotto/view/LottoInputView.kt @@ -0,0 +1,22 @@ +package lotto.view + +import java.lang.IllegalArgumentException + +class LottoInputView { + fun inputLottoBuy(): Int { + println(LOTTO_BUY_COMMENT) + return readLineOrThrows().toInt() + } + + fun inputWinningLotto(): List { + println(WINNING_LOTTO_COMMENT) + return readLineOrThrows().replace(" ", "").split(",").map { it.toInt() } + } + + private fun readLineOrThrows(): String = readLine() ?: throw IllegalArgumentException() + + companion object { + private const val LOTTO_BUY_COMMENT = "구입금액을 입력해 주세요." + private const val WINNING_LOTTO_COMMENT = "지난 주 당첨 번호를 입력해 주세요." + } +} diff --git a/src/main/kotlin/lotto/view/LottoOutputView.kt b/src/main/kotlin/lotto/view/LottoOutputView.kt new file mode 100644 index 0000000000..3ebc3e17b8 --- /dev/null +++ b/src/main/kotlin/lotto/view/LottoOutputView.kt @@ -0,0 +1,49 @@ +package lotto.view + +import lotto.domain.Lotto +import lotto.domain.LottoReward +import lotto.domain.LottoRoundStatistics + +class LottoOutputView { + fun currentLottos(lottos: List) { + val lottoSize = lottos.size + println("${lottoSize}개를 구매했습니다.") + repeat(lottoSize) { index -> + lottos[index].lottoNumbers.map { it.number }.joinToString(prefix = "[", separator = ", ", postfix = "]").also { println(it) } + } + } + + fun result(payment: Long, lottoRoundStatistics: LottoRoundStatistics) { + println(WINNING_C0MMENT) + + with(LottoReward.WINNER_4ST) { + lottoRoundStatistics.getLottoRewardOf(this).run { + println("${sameNumberCount}개 일치 (${prize}원)- $size") + } + } + + with(LottoReward.WINNER_3ST) { + lottoRoundStatistics.getLottoRewardOf(this).run { + println("${sameNumberCount}개 일치 (${prize}원)- $size") + } + } + + with(LottoReward.WINNER_2ST) { + lottoRoundStatistics.getLottoRewardOf(this).run { + println("${sameNumberCount}개 일치 (${prize}원)- $size") + } + } + + with(LottoReward.WINNER_1ST) { + lottoRoundStatistics.getLottoRewardOf(this).run { + println("${sameNumberCount}개 일치 (${prize}원)- $size") + } + } + + println("총 수익률은 0.${(lottoRoundStatistics.totalPrize.value * 100 / payment)}입니다.") + } + + companion object { + private const val WINNING_C0MMENT = "당첨 통계\n---------" + } +} diff --git a/src/main/kotlin/main.kt b/src/main/kotlin/main.kt new file mode 100644 index 0000000000..16022639cf --- /dev/null +++ b/src/main/kotlin/main.kt @@ -0,0 +1,18 @@ +import lotto.domain.LottoServiceRound +import lotto.view.LottoInputView +import lotto.view.LottoOutputView + +fun main() = lotto() + +fun lotto() { + val lottoServiceRound = LottoServiceRound() + val lottoInputView = LottoInputView() + val lottoOutputView = LottoOutputView() + + val payment = lottoInputView.inputLottoBuy() + lottoServiceRound.buyLottos(payment.toLong()).also { lottoOutputView.currentLottos(it) } + + val winningLottoNumbers = lottoInputView.inputWinningLotto() + val lottoRoundStatistics = lottoServiceRound.lotteryDraw(winningLottoNumbers) + lottoOutputView.result(lottoServiceRound.allPayment(), lottoRoundStatistics) +} diff --git a/src/test/kotlin/lotto/domain/LottoNumberTest.kt b/src/test/kotlin/lotto/domain/LottoNumberTest.kt new file mode 100644 index 0000000000..ffff3c9279 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoNumberTest.kt @@ -0,0 +1,30 @@ +package lotto.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.lang.RuntimeException + +class LottoNumberTest { + @Test + fun `로또 숫자의 범위는 1 ~ 45 까지만 허용된다`() { + // given + // when + // then + (1..45).forEach { + assertThat(LottoNumber(it).number).isEqualTo(it) + } + } + + @Test + fun `1 ~ 45 범위 이외에 로또번호는 생성될 수 없다`() { + // given + + // when + + // then + assertThrows { + LottoNumber(0).number + } + } +} diff --git a/src/test/kotlin/lotto/domain/LottoRoundStatisticsTest.kt b/src/test/kotlin/lotto/domain/LottoRoundStatisticsTest.kt new file mode 100644 index 0000000000..17cd3359c5 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoRoundStatisticsTest.kt @@ -0,0 +1,38 @@ +package lotto.domain + +import lotto.domain.Money.Companion.toMoney +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class LottoRoundStatisticsTest { + + @Test + fun `당첨된 로또를 기준으로 추첨 상세결과를 확인할 수 있다`() { + // given + val lotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val winningLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val sut = LottoRoundStatistics(listOf(lotto), winningLotto) + + // when + // then + assertThat(sut.lottoResults.size).isEqualTo(1) + assertThat(sut.lottoResults.first().lotto).isEqualTo(lotto) + assertThat(sut.lottoResults.first().winningLotto).isEqualTo(winningLotto) + assertThat(sut.lottoResults.first().sameNumberCount).isEqualTo(6) + assertThat(sut.lottoResults.first().reward).isEqualTo(LottoReward.WINNER_1ST) + assertThat(sut.totalPrize).isEqualTo(LottoReward.WINNER_1ST.prize.toMoney()) + } + + @Test + fun `LottoReward 에 해당하는 로또를 가져올 수 있다`() { + // given + val lotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val winningLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val sut = LottoRoundStatistics(listOf(lotto), winningLotto) + + // when + // then + assertThat(sut.getLottoRewardOf(LottoReward.WINNER_1ST).size).isEqualTo(1) + assertThat(sut.getLottoRewardOf(LottoReward.WINNER_2ST).size).isEqualTo(0) + } +} diff --git a/src/test/kotlin/lotto/domain/LottoRoundTest.kt b/src/test/kotlin/lotto/domain/LottoRoundTest.kt new file mode 100644 index 0000000000..f5cc3951b0 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoRoundTest.kt @@ -0,0 +1,35 @@ +package lotto.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class LottoRoundTest { + + @Test + fun `새로운 로또를 개수만큼 추가할 수 있다`() { + // given + val sut = LottoRound(LottoRoundElements()) + val newLottoSize = 3 + + // when + sut.addNewLottos(newLottoSize) + + // then + assertThat(sut.getLottos().size).isEqualTo(newLottoSize) + } + + @Test + fun `로또 추첨결과를 얻을 수 있다`() { + // given + val winningLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val sut = LottoRound(LottoRoundElements()) + val newLottoSize = 3 + sut.addNewLottos(3) + + // when + val result = sut.lotteryDraw(winningLotto) + + // then + assertThat(result.lottoResults.size).isEqualTo(newLottoSize) + } +} diff --git a/src/test/kotlin/lotto/domain/LottoTest.kt b/src/test/kotlin/lotto/domain/LottoTest.kt new file mode 100644 index 0000000000..84f07ef932 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoTest.kt @@ -0,0 +1,105 @@ +package lotto.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.lang.RuntimeException + +class LottoTest { + @Test + fun `로또 숫자들로 로또를 생성할 수 있다`() { + // given + val lottoNumbers = (1..6).map { LottoNumber(it) } + + // when + // then + Lotto(lottoNumbers) + } + + @Test + fun `Integer list 를 통해 로또를 생성할 수 있다`() { + // given + val lottoNumbers = (1..6).toList() + + // when + // then + Lotto.of(lottoNumbers) + } + + @Test + fun `6개가 아닌 로또 숫자로 로또를 생성할 수 없다`() { + // given + val lottoNumbers = (1..7).map { LottoNumber(it) } + + // when + // then + assertThrows { + Lotto(lottoNumbers) + } + } + + @Test + fun `중복된 숫자들이 포함된 로또 숫자들로 로또를 생성할 수 없다`() { + // given + val lottoNumbers = (1..3).map { LottoNumber(it) } + (1..3).map { LottoNumber(it) } + + // when + // then + assertThrows { + Lotto(lottoNumbers) + } + } + + @Test + fun `로또 숫자들을 오름차순으로 조회할 수 있다`() { + // given + val reversedLottoNumbers = (1..6).reversed().map { LottoNumber(it) } + + // when + val lotto = Lotto(reversedLottoNumbers) + + // then + lotto.lottoNumbers.asSequence().windowed(2).forEach { + assertThat(it[0].number <= it[1].number).isEqualTo(true) + } + } + + @Test + fun `같은 로또를 비교할 수 있다`() { + // given + val sameLotto1 = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val sameLotto2 = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + + // when + val isSame = sameLotto1 == sameLotto2 + + // then + assertThat(isSame).isEqualTo(true) + } + + @Test + fun `서로 같지 않은 로또를 비교할 수 있다`() { + // given + val oneToSixLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val twoToSevenLotto = (2..7).map { LottoNumber(it) }.toList().let { Lotto(it) } + + // when + val isSame = oneToSixLotto == twoToSevenLotto + + // then + assertThat(isSame).isEqualTo(false) + } + + @Test + fun `다른 로또와 겹치는 숫자 개수를 알 수 있다`() { + // given + val oneToSixLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) } + val twoToSevenLotto = (2..7).map { LottoNumber(it) }.toList().let { Lotto(it) } + + // when + val shouldBeFive = oneToSixLotto.getSameNumberCount(twoToSevenLotto) + + // then + assertThat(shouldBeFive).isEqualTo(5) + } +}