diff --git a/README.md b/README.md index cbae739405..52235f1783 100644 --- a/README.md +++ b/README.md @@ -1 +1,41 @@ -# kotlin-lotto \ No newline at end of file +# kotlin-lotto + +## Process + +1. Input purchase +2. Calculate purchased amount of tickets +3. Generate purchased tickets +4. Input winning numbers +5. Calculate matches +6. Calculate return rate +7. Display result + +## MVC + +### Model +1. Ticket +2. Purchased Tickets +3. Lotto Result +### View +1. Input View +2. Result View +### Controller +1. Lotto Controller + +## TDD + +### 1. Ticket +- [x] 1.1. ticket should contain 6 numbers +- [x] 1.2. should fail if numbers less or more then 6 + +### 2. Purchased Ticket +- [X] 2.1. should calculate correct number of tickets based on purchased amount +- [x] 2.2. should generate tickets with correct numbers using generator +- [x] 2.3. should return empty list if purchased amount less that ticket price +- [x] 2.4. should fail if purchased amount is negative + +### 3. Lotto Result +- [x] 3.1. should calculate matches +- [x] 3.1. should calculate total prize correctly +- [x] 3.2. should calculate return rate correctly +- [x] 3.4. should return empty map if matches less than 3 diff --git a/src/main/kotlin/lotto/Main.kt b/src/main/kotlin/lotto/Main.kt new file mode 100644 index 0000000000..403b6a06e6 --- /dev/null +++ b/src/main/kotlin/lotto/Main.kt @@ -0,0 +1,7 @@ +package lotto + +import lotto.controller.LottoController + +fun main() { + LottoController.run() +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/controller/LottoController.kt b/src/main/kotlin/lotto/controller/LottoController.kt new file mode 100644 index 0000000000..1f84445e75 --- /dev/null +++ b/src/main/kotlin/lotto/controller/LottoController.kt @@ -0,0 +1,23 @@ +package lotto.controller + +import lotto.model.LottoResult +import lotto.model.PurchasedTickets +import lotto.model.RandomTicketNumberGenerator +import lotto.view.InputView +import lotto.view.ResultView + +class LottoController { + + companion object { + private val generator = RandomTicketNumberGenerator() + fun run() { + val purchasedAmount = InputView.readPurchaseAmount() + val purchasedTickets = PurchasedTickets.buyTickets(purchasedAmount, generator) + ResultView.showTickets(purchasedTickets.getTickets()) + + val winningNumbers = InputView.readWinningNumbers() + val result = LottoResult(winningNumbers, purchasedTickets.getTickets()) + ResultView.showStatistics(result, purchasedAmount) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/model/LottoResult.kt b/src/main/kotlin/lotto/model/LottoResult.kt new file mode 100644 index 0000000000..4943145c5d --- /dev/null +++ b/src/main/kotlin/lotto/model/LottoResult.kt @@ -0,0 +1,30 @@ +package lotto.model + +const val MINIMUM_MATCHES = 3 + +class LottoResult(private val winningNumbers: List, private val tickets: List) { + fun calculateResult(): Map { + return tickets.groupingBy { ticket -> + ticket.numbers.count { it in winningNumbers } + } + .eachCount() + .filterKeys { it >= MINIMUM_MATCHES } + } + + fun totalPrize(): Int{ + val prizeMap = mapOf( + 3 to 5_000, + 4 to 50_000, + 5 to 1_500_000, + 6 to 2_000_000_000 + ) + + return calculateResult().entries.sumOf { (matchCount, count) -> + prizeMap[matchCount]?.times(count) ?: 0 + } + } + + fun returnRate(purchaseAmount: Int): Double { + return if (purchaseAmount == 0) 0.0 else totalPrize().toDouble() / purchaseAmount + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/model/PurchasedTickets.kt b/src/main/kotlin/lotto/model/PurchasedTickets.kt new file mode 100644 index 0000000000..df26055c8c --- /dev/null +++ b/src/main/kotlin/lotto/model/PurchasedTickets.kt @@ -0,0 +1,15 @@ +package lotto.model + +const val TICKET_PRICE = 1000; + +class PurchasedTickets(private val tickets: List) { + fun getTickets(): List = tickets + + companion object { + fun buyTickets(amount: Int, generator: TicketNumberGenerator): PurchasedTickets { + require(amount > 0) + val ticketCount = amount / TICKET_PRICE + return PurchasedTickets(List(ticketCount) { TicketModel.generate(generator) }) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/model/RandomTicketNumberGenerator.kt b/src/main/kotlin/lotto/model/RandomTicketNumberGenerator.kt new file mode 100644 index 0000000000..059ee56780 --- /dev/null +++ b/src/main/kotlin/lotto/model/RandomTicketNumberGenerator.kt @@ -0,0 +1,7 @@ +package lotto.model + +class RandomTicketNumberGenerator: TicketNumberGenerator { + override fun generateNumbers(): List { + return (1..45).shuffled().take(6).toList() + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/model/TicketModel.kt b/src/main/kotlin/lotto/model/TicketModel.kt new file mode 100644 index 0000000000..4f73f315f8 --- /dev/null +++ b/src/main/kotlin/lotto/model/TicketModel.kt @@ -0,0 +1,17 @@ +package lotto.model + +const val TICKET_NUMBER_LENGTH = 6 + +data class TicketModel(val numbers : List) { + init { + require(numbers.size == TICKET_NUMBER_LENGTH) { + "Ticket should contain $TICKET_NUMBER_LENGTH numbers" + } + } + + companion object { + fun generate(generator: TicketNumberGenerator): TicketModel { + return TicketModel(generator.generateNumbers()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/model/TicketNumberGenerator.kt b/src/main/kotlin/lotto/model/TicketNumberGenerator.kt new file mode 100644 index 0000000000..820c072227 --- /dev/null +++ b/src/main/kotlin/lotto/model/TicketNumberGenerator.kt @@ -0,0 +1,5 @@ +package lotto.model + +interface TicketNumberGenerator { + fun generateNumbers(): List +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/view/InputView.kt b/src/main/kotlin/lotto/view/InputView.kt new file mode 100644 index 0000000000..cd45e16927 --- /dev/null +++ b/src/main/kotlin/lotto/view/InputView.kt @@ -0,0 +1,23 @@ +package lotto.view + +class InputView { + + companion object { + fun readPurchaseAmount(): Int { + println("Please enter the purchase amount.") + return readlnOrNull()?.toIntOrNull() + ?.takeIf { it >= 1000 } + ?: throw IllegalArgumentException("Invalid amount. Must be at least 1,000 KRW.") + } + + fun readWinningNumbers(): List { + println("Please enter last week’s winning numbers.") + return readlnOrNull() + ?.split(",") + ?.map { it.trim().toInt() } + ?.toList() + ?.takeIf { it.size == 6 } + ?: throw IllegalArgumentException("Invalid input. Must enter exactly 6 unique numbers.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/view/ResultView.kt b/src/main/kotlin/lotto/view/ResultView.kt new file mode 100644 index 0000000000..3b5bc99b46 --- /dev/null +++ b/src/main/kotlin/lotto/view/ResultView.kt @@ -0,0 +1,36 @@ +package lotto.view + +import lotto.model.LottoResult +import lotto.model.TicketModel + +class ResultView { + + companion object { + fun showTickets(tickets: List) { + println("You have purchased ${tickets.size} tickets.") + tickets.forEach { println(it) } + } + + fun showStatistics(result: LottoResult, purchaseAmount: Int) { + println("Winning Statistics") + println("------------------") + + val stats = result.calculateResult() + val prizeMap = mapOf( + 3 to 5_000L, + 4 to 50_000L, + 5 to 1_500_000L, + 6 to 2_000_000_000L + ) + + prizeMap.forEach { (matchCount, prize) -> + val count = stats.getOrDefault(matchCount, 0) + println("$matchCount Matches ($prize KRW) - $count tickets") + } + + val returnRate = result.returnRate(purchaseAmount) + println("Total return rate is %.2f (A rate below 1 means a loss)".format(returnRate)) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/lotto/model/LottoResultTest.kt b/src/test/kotlin/lotto/model/LottoResultTest.kt new file mode 100644 index 0000000000..017f88f0ee --- /dev/null +++ b/src/test/kotlin/lotto/model/LottoResultTest.kt @@ -0,0 +1,55 @@ +package lotto.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class LottoResultTest { + @Test + fun `should calculate matches`() { + val winningNumbers = listOf(1, 2, 3, 4, 5, 6) + val tickets = listOf( + TicketModel(listOf(1, 2, 3, 4, 5, 7)) + ) + val lottoResult = LottoResult(winningNumbers, tickets) + val result = lottoResult.calculateResult() + assertThat(result[5]).isEqualTo(1) + } + + @Test + fun `should return empty map if matches less than 3`() { + val winningNumbers = listOf(1, 2, 3, 4, 5, 6) + val tickets = listOf( + TicketModel(listOf(1, 2, 7, 8, 9, 10)) + ) + val lottoResult = LottoResult(winningNumbers, tickets) + val result = lottoResult.calculateResult() + assertThat(result).isEmpty() + } + + @Test + fun `should calculate total prize correctly`() { + val winningNumbers = listOf(1, 2, 3, 4, 5, 6) + val tickets = listOf( + TicketModel(listOf(1, 2, 3, 4, 5, 6)), + TicketModel(listOf(1, 2, 3, 4, 5, 7)), + TicketModel(listOf(1, 2, 3, 4, 7, 8)), + + ) + val lottoResult = LottoResult(winningNumbers, tickets) + assertThat(lottoResult.totalPrize()).isEqualTo(2_001_550_000) + } + + @Test + fun `should calculate return rate correctly`() { + val winningNumbers = listOf(1, 2, 3, 4, 5, 6) + val tickets = listOf( + TicketModel(listOf(1, 2, 3, 4, 5, 6)), + TicketModel(listOf(1, 2, 3, 4, 5, 7)), + TicketModel(listOf(1, 2, 3, 4, 7, 8)), + + ) + val lottoResult = LottoResult(winningNumbers, tickets) + val purchasedAmount = 3000 + assertThat(lottoResult.returnRate(purchasedAmount)).isEqualTo(667_183.3333333334) + } +} \ No newline at end of file diff --git a/src/test/kotlin/lotto/model/PurchasedTicketsTest.kt b/src/test/kotlin/lotto/model/PurchasedTicketsTest.kt new file mode 100644 index 0000000000..93efa1fa1a --- /dev/null +++ b/src/test/kotlin/lotto/model/PurchasedTicketsTest.kt @@ -0,0 +1,38 @@ +package lotto.model + +import org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.assertThrows + +class PurchasedTicketsTest { + private val testGenerator = TestTicketNumberGenerator(listOf(1, 2, 3, 4, 5, 6)) + + @Test + fun `should calculate correct number of tickets based on purchased amount`() { + val purchasedTickets = PurchasedTickets.buyTickets(5000, testGenerator) + assertThat(purchasedTickets.getTickets().size).isEqualTo(5) + } + + @Test + fun `should generate tickets with correct numbers using generator`() { + val purchasedTickets = PurchasedTickets.buyTickets(1000, testGenerator) + assertThat(purchasedTickets.getTickets().first()).isEqualTo(TicketModel(listOf(1, 2, 3, 4, 5, 6))) + } + + @Test + fun `should return empty list if purchased amount less that ticket price`() { + val purchasedTickets = PurchasedTickets.buyTickets(999, testGenerator) + assertThat(purchasedTickets.getTickets().size).isEqualTo(0) + } + + @Test + fun `should fail if purchased amount is negative`() { + assertThrows { + PurchasedTickets.buyTickets(-1, testGenerator) + } + } + + class TestTicketNumberGenerator(private val fixedNumbers: List) : TicketNumberGenerator { + override fun generateNumbers(): List = fixedNumbers + } +} \ No newline at end of file diff --git a/src/test/kotlin/lotto/model/TicketModelModelTest.kt b/src/test/kotlin/lotto/model/TicketModelModelTest.kt new file mode 100644 index 0000000000..874dae8da4 --- /dev/null +++ b/src/test/kotlin/lotto/model/TicketModelModelTest.kt @@ -0,0 +1,38 @@ +package lotto.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +class TicketModelModelTest { + + @ParameterizedTest + @MethodSource("validTickets") + fun `ticket should contain 6 numbers`(numbers: List) { + val ticket = TicketModel(numbers) + assertThat(ticket.numbers).isEqualTo(numbers) + } + + @ParameterizedTest + @MethodSource("invalidTickets") + fun `should fail if numbers less or more then 6`(numbers: List) { + assertThrows { TicketModel(numbers) } + } + + companion object { + @JvmStatic + fun validTickets(): List> = listOf( + listOf(1, 2, 3, 4, 5, 6), + listOf(6, 5, 4, 3, 2, 1), + listOf(8, 9, 10, 11, 12, 13) + ) + @JvmStatic + fun invalidTickets(): List> = listOf( + listOf(1, 2, 3), + listOf(3, 2, 1), + listOf(1, 2, 3, 4, 5, 6, 7), + listOf(7, 6, 5, 4, 3, 2, 1), + ) + } +} \ No newline at end of file