Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
954e16b
docs: design
arkadiishyndhse Mar 27, 2025
580df84
docs: test cases for TicketModel
arkadiishyndhse Mar 27, 2025
ae08014
test: case 1.1 - failing test
arkadiishyndhse Mar 27, 2025
d4b1f65
test: case 1.1 - passing test
arkadiishyndhse Mar 27, 2025
5ddd2f7
test: case 1.1 - refactor
arkadiishyndhse Mar 27, 2025
e66a204
test: case 1.1 - new test
arkadiishyndhse Mar 27, 2025
a2a7b8e
test: case 1.1 - refactor test
arkadiishyndhse Mar 27, 2025
9dcb672
test: case 1.1 - refactor test
arkadiishyndhse Mar 27, 2025
e107701
test: case 1.2 - failing test
arkadiishyndhse Mar 27, 2025
6ec2604
test: case 1.2 - passing test
arkadiishyndhse Mar 27, 2025
7b41b8c
test: case 1.2 - refactor
arkadiishyndhse Mar 27, 2025
bfaff9b
test: case 1.2 - refactor test
arkadiishyndhse Mar 27, 2025
a13697a
dacs: test cases for PurchasedTickets
arkadiishyndhse Mar 28, 2025
1c4447c
test: case 2.1. passing test
arkadiishyndhse Mar 28, 2025
4266c9f
test: case 2.1. refactor
arkadiishyndhse Mar 28, 2025
55401f4
refactor: implement ticket factory method
arkadiishyndhse Mar 28, 2025
45178dd
refactor: implement buy ticket method
arkadiishyndhse Mar 28, 2025
d5c83a8
test: case 2.1. refactor
arkadiishyndhse Mar 31, 2025
5c8d3a9
test: case 2.2. failing test
arkadiishyndhse Mar 31, 2025
39859df
test: add ticket number generator
arkadiishyndhse Mar 31, 2025
4e8e38b
refactor: generate ticket using generator
arkadiishyndhse Mar 31, 2025
dbef250
test: case 2.2. refactoring
arkadiishyndhse Mar 31, 2025
3596000
test: case 2.2. refactoring
arkadiishyndhse Mar 31, 2025
02a7229
test: case 2.3. passing test
arkadiishyndhse Mar 31, 2025
8751125
test: case 2.4. failing test
arkadiishyndhse Mar 31, 2025
855ee4b
test: case 2.4. passing test
arkadiishyndhse Mar 31, 2025
9bb6693
test: test cases for Lotto Result
arkadiishyndhse Mar 31, 2025
a019f15
test: case 3.1. failing test
arkadiishyndhse Mar 31, 2025
aa601be
test: case 3.1. passing test
arkadiishyndhse Mar 31, 2025
181397d
test: case 3.1. refactor
arkadiishyndhse Mar 31, 2025
e575348
test: case 3.2. failing test
arkadiishyndhse Mar 31, 2025
e28b3da
test: case 3.2. passing test
arkadiishyndhse Mar 31, 2025
87a01f4
test: case 3.2. refactor
arkadiishyndhse Mar 31, 2025
553ac31
test: case 3.3. passing test
arkadiishyndhse Mar 31, 2025
278a134
test: case 3.3. refactor
arkadiishyndhse Mar 31, 2025
0b81b9f
test: case 3.4. implementation
arkadiishyndhse Mar 31, 2025
51a77dc
docs: mark implemented tests
arkadiishyndhse Mar 31, 2025
6b21092
feat: LottoController
arkadiishyndhse Mar 31, 2025
2d8f4bc
feat: InputView
arkadiishyndhse Mar 31, 2025
eb29f3f
feat: ResultView
arkadiishyndhse Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great practice to help you set your standard for the test scenarios. 👍
It's best to include the requirements you listed into your commit messages and test scenarios.

Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
# kotlin-lotto
# 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
7 changes: 7 additions & 0 deletions src/main/kotlin/lotto/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lotto

import lotto.controller.LottoController

fun main() {
LottoController.run()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please activate the configuration to prevent EOF alerts on GitHub. 🙂

Editor -> General -> Ensure every saved file ends with a line break

23 changes: 23 additions & 0 deletions src/main/kotlin/lotto/controller/LottoController.kt
Original file line number Diff line number Diff line change
@@ -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 {
Comment on lines +9 to +11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class with only one companion object could be considered to be declared as object.
(Also for the InputView, ResultView)

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)
}
}
}
30 changes: 30 additions & 0 deletions src/main/kotlin/lotto/model/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto.model

const val MINIMUM_MATCHES = 3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about moving MINIMUM_MATCHES into the companion object of LottoResult

https://kotlinlang.org/docs/object-declarations.html#companion-objects

Note: class members can access private members of their corresponding companion object. So MINIMUM_MATCHES does not have to be public in this case. 🙂


class LottoResult(private val winningNumbers: List<Int>, private val tickets: List<TicketModel>) {
fun calculateResult(): Map<Int, Int> {
return tickets.groupingBy { ticket ->
ticket.numbers.count { it in winningNumbers }
}
.eachCount()
.filterKeys { it >= MINIMUM_MATCHES }
}
Comment on lines +5 to +12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we work on this Lotto Application with other developers, will they be able to understand what Map<Int, Int> means?


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
)
Comment on lines +14 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary to create Map instance every time the totalPrize() is called!

How about refactoring them using enum classes? ☺️
https://kotlinlang.org/docs/enum-classes.html

Comment on lines +14 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many functions, including totalPrize(), are only referenced in test packages, so they do not need to be opened as public functions.

Private functions separated only for readability reasons can also be considered verifiable by the public. Since the public function uses the private function, it can be naturally included in the test range.
However, if they are needed more than just improving readability, you might want to think about whether it's time to create another object that performs that role to the testable implementation.


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
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/model/PurchasedTickets.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lotto.model

const val TICKET_PRICE = 1000;

class PurchasedTickets(private val tickets: List<TicketModel>) {
fun getTickets(): List<TicketModel> = tickets
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Kotlin, the getter is already included in the property! So we don't need to declare a function that just returns property value 🙂

Suggested change
class PurchasedTickets(private val tickets: List<TicketModel>) {
fun getTickets(): List<TicketModel> = tickets
class PurchasedTickets(val tickets: List<TicketModel>) {

https://kotlinlang.org/docs/properties.html


companion object {
fun buyTickets(amount: Int, generator: TicketNumberGenerator): PurchasedTickets {
require(amount > 0)
val ticketCount = amount / TICKET_PRICE
return PurchasedTickets(List(ticketCount) { TicketModel.generate(generator) })
}
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/lotto/model/RandomTicketNumberGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lotto.model

class RandomTicketNumberGenerator: TicketNumberGenerator {
override fun generateNumbers(): List<Int> {
return (1..45).shuffled().take(6).toList()
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/model/TicketModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.model

const val TICKET_NUMBER_LENGTH = 6

data class TicketModel(val numbers : List<Int>) {
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())
}
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/lotto/model/TicketNumberGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package lotto.model

interface TicketNumberGenerator {
fun generateNumbers(): List<Int>
}
23 changes: 23 additions & 0 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -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<Int> {
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.")
}
}
}
36 changes: 36 additions & 0 deletions src/main/kotlin/lotto/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package lotto.view

import lotto.model.LottoResult
import lotto.model.TicketModel

class ResultView {

companion object {
fun showTickets(tickets: List<TicketModel>) {
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")
}
Comment on lines +19 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this prizeMap is already in LottoResult, how can we improve code duplicates? 🤔


val returnRate = result.returnRate(purchaseAmount)
println("Total return rate is %.2f (A rate below 1 means a loss)".format(returnRate))
}
}

}
55 changes: 55 additions & 0 deletions src/test/kotlin/lotto/model/LottoResultTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
38 changes: 38 additions & 0 deletions src/test/kotlin/lotto/model/PurchasedTicketsTest.kt
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you design your code to be testable 👍

This is also called the Strategy Pattern.
https://refactoring.guru/design-patterns/strategy

Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.


@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<IllegalArgumentException> {
PurchasedTickets.buyTickets(-1, testGenerator)
}
}

class TestTicketNumberGenerator(private val fixedNumbers: List<Int>) : TicketNumberGenerator {
override fun generateNumbers(): List<Int> = fixedNumbers
}
}
38 changes: 38 additions & 0 deletions src/test/kotlin/lotto/model/TicketModelModelTest.kt
Original file line number Diff line number Diff line change
@@ -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<Int>) {
val ticket = TicketModel(numbers)
assertThat(ticket.numbers).isEqualTo(numbers)
}

@ParameterizedTest
@MethodSource("invalidTickets")
fun `should fail if numbers less or more then 6`(numbers: List<Int>) {
assertThrows<IllegalArgumentException> { TicketModel(numbers) }
}

companion object {
@JvmStatic
fun validTickets(): List<List<Int>> = 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<List<Int>> = 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),
)
}
}