diff --git a/src/main/kotlin/lotto/Main.kt b/src/main/kotlin/lotto/Main.kt new file mode 100644 index 0000000000..925233fe97 --- /dev/null +++ b/src/main/kotlin/lotto/Main.kt @@ -0,0 +1,18 @@ +package lotto + +import lotto.domain.Lottery +import lotto.domain.LotteryMachine +import lotto.view.LotteryMachineInputView +import lotto.view.LotteryMachineOutputView + +fun main() { + + val payAmount = LotteryMachineInputView.inputPayAmount() + val lotteries = LotteryMachine.buyLotteries(payAmount) + LotteryMachineOutputView.printLotteries(lotteries) + + val lastWinningLottery = Lottery(LotteryMachineInputView.inputLastWinningNumbers()) + + val result = LotteryMachine.getMatchCount(lotteries, lastWinningLottery) + LotteryMachineOutputView.printResult(result, LotteryMachine.calculateReturnRate(payAmount, result)) +} diff --git a/src/main/kotlin/lotto/README.md b/src/main/kotlin/lotto/README.md new file mode 100644 index 0000000000..18c80d812c --- /dev/null +++ b/src/main/kotlin/lotto/README.md @@ -0,0 +1,10 @@ +# Step2. 로또 기능 요구사항 + * 구입금액을 입력받는다 + * 구입 금액에 해당하는 로또를 발급한다 (로또 1장의 가격은 1000원이다) + * 발급한 로또를 출력한다 + * 지난 주 당첨번호를 입력받는다 + * 발급한 로또의 당첨 통계를 구한다 + * 발급한 로또의 총 수익률을 구한다 + * 당첨 통계와 수익률을 출력한다 + + diff --git a/src/main/kotlin/lotto/domain/LotteryMachine.kt b/src/main/kotlin/lotto/domain/LotteryMachine.kt new file mode 100644 index 0000000000..ef88e3df2e --- /dev/null +++ b/src/main/kotlin/lotto/domain/LotteryMachine.kt @@ -0,0 +1,40 @@ +package lotto.domain + +import java.math.BigDecimal +import java.math.RoundingMode + +object LotteryMachine { + private const val PRICE = 1_000 + + private val LOTTO_RETURN_MAP = mapOf( + 3 to 5_000, + 4 to 50_000, + 5 to 1_500_000, + 6 to 2_000_000_000 + ) + + fun buyLotteries(payAmount: Int): Lotteries { + val howMany = (payAmount / PRICE) + val lotteryList = List(howMany) { Lottery(randomLottoNumbers()) } + return Lotteries(lotteryList) + } + + private fun randomLottoNumbers() = LottoNumber.allNumbers().shuffled().subList(0, Lottery.COUNT) + + fun getMatchCount(lotteries: Lotteries, lastWinningLottery: Lottery): LotteryMatchCount { + val matchCount = lotteries.lotteries + .groupingBy { it.countSameLottoNumbers(lastWinningLottery) } + .eachCount() + + return LotteryMatchCount(matchCount) + } + + fun calculateReturnRate(payAmount: Int, lotteryResult: LotteryMatchCount): BigDecimal { + val totalPrize = LOTTO_RETURN_MAP.entries.map { + it.value * (lotteryResult.matchCount[it.key] ?: 0) + }.sum() + + return BigDecimal.valueOf(totalPrize / payAmount.toDouble()) + .setScale(2, RoundingMode.FLOOR) + } +} diff --git a/src/main/kotlin/lotto/domain/LotteryMatchCount.kt b/src/main/kotlin/lotto/domain/LotteryMatchCount.kt new file mode 100644 index 0000000000..1ff528ba0e --- /dev/null +++ b/src/main/kotlin/lotto/domain/LotteryMatchCount.kt @@ -0,0 +1,3 @@ +package lotto.domain + +data class LotteryMatchCount(val matchCount: Map) diff --git a/src/main/kotlin/lotto/domain/Model.kt b/src/main/kotlin/lotto/domain/Model.kt new file mode 100644 index 0000000000..d011e78534 --- /dev/null +++ b/src/main/kotlin/lotto/domain/Model.kt @@ -0,0 +1,72 @@ +package lotto.domain + +class Lotteries(val lotteries: List) { + constructor(vararg lotteries: Lottery) : this(lotteries.toList()) + + fun count(): Int { + return lotteries.size + } +} + +class Lottery(numbers: List) { + + val numbers: List + + constructor(vararg inputNumbers: Int) : this(inputNumbers.toList()) + + init { + require(numbers.size == COUNT) + this.numbers = numbers.map { LottoNumber.of(it) }.sorted() + } + + fun countSameLottoNumbers(other: Lottery): Int { + return this.numbers.count { other.numbers.contains(it) } + } + companion object { + const val COUNT = 6 + } +} + +class LottoNumber private constructor(val value: Int) : Comparable { + init { + require(value in (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER)) + } + + override fun toString(): String { + return "LottoNumber(number=$value)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LottoNumber + + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + return value + } + + override fun compareTo(other: LottoNumber): Int { + return this.value.compareTo(other.value) + } + + companion object { + private const val MIN_LOTTO_NUMBER = 1 + private const val MAX_LOTTO_NUMBER = 45 + private val NUMBERS = List(MAX_LOTTO_NUMBER) { LottoNumber(it + MIN_LOTTO_NUMBER) } + fun allNumbers(): List { + return NUMBERS.map { it.value } + } + + fun of(number: Int): LottoNumber { + require(number - MIN_LOTTO_NUMBER in (NUMBERS.indices)) + + return NUMBERS[number - MIN_LOTTO_NUMBER] + } + } +} diff --git a/src/main/kotlin/lotto/view/LotteryMachineInputView.kt b/src/main/kotlin/lotto/view/LotteryMachineInputView.kt new file mode 100644 index 0000000000..facab6d227 --- /dev/null +++ b/src/main/kotlin/lotto/view/LotteryMachineInputView.kt @@ -0,0 +1,17 @@ +package lotto.view + +object LotteryMachineInputView { + + fun inputPayAmount(): Int { + println("구입금액을 입력해 주세요.") + return readLine()?.toIntOrNull() ?: throw IllegalArgumentException() + } + + fun inputLastWinningNumbers(): List { + println("지난 주 당첨 번호를 입력해 주세요.") + + return readLine().orEmpty() + .split(",") + .map { it.toIntOrNull() ?: throw IllegalArgumentException() } + } +} diff --git a/src/main/kotlin/lotto/view/LotteryMachineOutputView.kt b/src/main/kotlin/lotto/view/LotteryMachineOutputView.kt new file mode 100644 index 0000000000..cb29170990 --- /dev/null +++ b/src/main/kotlin/lotto/view/LotteryMachineOutputView.kt @@ -0,0 +1,29 @@ +package lotto.view + +import lotto.domain.Lotteries +import lotto.domain.LotteryMatchCount +import java.math.BigDecimal + +object LotteryMachineOutputView { + fun printLotteries(lotteries: Lotteries) { + println("${lotteries.count()}개를 구매했습니다.") + + for (it in lotteries.lotteries) { + println("[${it.numbers.map { it.value }.toList().joinToString(", ")}]") + } + } + + fun printResult(result: LotteryMatchCount, returnRate: BigDecimal) { + println( + """ + |당첨 통계 + |--------- + |3개 일치 (5000원)- ${result.matchCount[3] ?: 0}개 + |4개 일치 (50000원)- ${result.matchCount[4] ?: 0}개 + |5개 일치 (1500000원)- ${result.matchCount[5] ?: 0}개 + |6개 일치 (2000000000원)- ${result.matchCount[6] ?: 0}개 + |총 수익률은 ${returnRate}입니다. + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/lotto/domain/LotteryMachineTest.kt b/src/test/kotlin/lotto/domain/LotteryMachineTest.kt new file mode 100644 index 0000000000..e1fdb04c1a --- /dev/null +++ b/src/test/kotlin/lotto/domain/LotteryMachineTest.kt @@ -0,0 +1,72 @@ +package lotto.domain + +import io.kotest.matchers.comparables.shouldBeEqualComparingTo +import io.kotest.matchers.maps.shouldNotHaveKey +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.math.BigDecimal +import java.util.stream.Stream + +internal class LotteryMachineTest { + + @DisplayName("구입 금액에 해당하는 로또를 발급한다") + @Test + fun buyLottery() { + listOf( + 1000 to 1, + 1500 to 1, + 3900 to 3, + 10000 to 10 + ).map { (payAmount, howManyBought) -> + { + val result = LotteryMachine.buyLotteries(payAmount) + + result.count() shouldBe howManyBought + } + } + } + + @Test + fun getMatchResult() { + + val result = LotteryMachine.getMatchCount( + Lotteries( + Lottery(1, 2, 13, 14, 15, 16), + Lottery(11, 12, 13, 4, 5, 6), + Lottery(1, 2, 3, 14, 15, 16) + ), + Lottery(1, 2, 3, 4, 5, 6) + ) + assertAll( + { result.matchCount[2] shouldBe 1 }, + { result.matchCount[3] shouldBe 2 }, + { listOf(0, 1, 4, 5).forEach { result.matchCount shouldNotHaveKey it } } + ) + } + + @DisplayName("수익률을 구한다 (소숫점 2자리수에서 반올림한다)") + @ParameterizedTest + @MethodSource("calculateReturnRateProvider") + fun calculateReturnRate(payAmount: Int, countSameLottoNumber: Map, expected: BigDecimal) { + + LotteryMachine.calculateReturnRate( + payAmount, + LotteryMatchCount(countSameLottoNumber) + ) shouldBeEqualComparingTo expected + } + + companion object { + @JvmStatic + fun calculateReturnRateProvider(): Stream = Stream.of( + Arguments.of(14000, mapOf(3 to 1, 2 to 1), BigDecimal.valueOf(0.35)), + Arguments.of(10000, mapOf(1 to 3), BigDecimal.ZERO), + Arguments.of(30000, mapOf(4 to 1, 3 to 1, 2 to 1), BigDecimal.valueOf(1.83)), + Arguments.of(20000, mapOf(5 to 1, 4 to 1), BigDecimal.valueOf(77.5)) + ) + } +} diff --git a/src/test/kotlin/lotto/domain/LotteryTest.kt b/src/test/kotlin/lotto/domain/LotteryTest.kt new file mode 100644 index 0000000000..f2309a5bd1 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LotteryTest.kt @@ -0,0 +1,34 @@ +package lotto.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +internal class LotteryTest { + + @DisplayName("6개의 로또 번호를 포함해야한다") + @Test + fun create() { + listOf( + intArrayOf(1, 2), + intArrayOf(1, 2, 3, 4, 5), + intArrayOf(1, 2, 3, 4, 5, 6, 7) + ).map { numbers -> + shouldThrow { Lottery(*numbers) } + } + } + + @DisplayName("일치하는 로또 번호 갯수를 구한다") + @Test + fun countSameLottoNumbers() { + val sut = Lottery(1, 2, 3, 4, 5, 6) + listOf( + Lottery(1, 12, 13, 14, 15, 16) to 1, + Lottery(1, 2, 13, 14, 15, 16) to 2, + Lottery(1, 2, 3, 4, 5, 6) to 6 + ).map { (other, expected) -> + sut.countSameLottoNumbers(other) shouldBe expected + } + } +} diff --git a/src/test/kotlin/lotto/domain/LottoNumberTest.kt b/src/test/kotlin/lotto/domain/LottoNumberTest.kt new file mode 100644 index 0000000000..8604b13fb7 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoNumberTest.kt @@ -0,0 +1,59 @@ +package lotto.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.comparables.shouldBeEqualComparingTo +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +internal class LottoNumberTest { + + @DisplayName("특정 로또 번호로 생성할 수 있다") + @ParameterizedTest + @ValueSource(ints = [1, 2, 3, 4, 43, 44, 45]) + fun number(number: Int) { + LottoNumber.of(number).value shouldBe number + } + + @DisplayName("로또 번호는 1-45 사이의 값이여야 한다") + @ParameterizedTest + @ValueSource(ints = [0, -1, 46, 50, 100]) + fun getInstanceFailIfNotLottoNumber(number: Int) { + shouldThrow { LottoNumber.of(number) } + } + + @DisplayName("값 객체이다") + @ParameterizedTest + @ValueSource(ints = [1, 2, 3, 4, 43, 44, 45]) + fun getInstance(number: Int) { + val sut = LottoNumber.of(number) + val other = LottoNumber.of(number) + + assertAll( + { sut shouldBe other }, + { sut shouldBeSameInstanceAs other }, + { sut shouldBeEqualComparingTo other } + ) + } + + @DisplayName("로또 번호 값으로 비교할 수 있다") + @Test + fun compare() { + listOf( + 1 to 1, + 2 to 0, + 3 to -1, + ).map { (compareTo, compareResult) -> + { + val sut = LottoNumber.of(2) + val other = LottoNumber.of(compareTo) + + sut.compareTo(other) shouldBe compareResult + } + } + } +}