-
Notifications
You must be signed in to change notification settings - Fork 356
Step 2 - Lotto (Auto) #1147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: arkadiishyndhse
Are you sure you want to change the base?
Step 2 - Lotto (Auto) #1147
Changes from all commits
954e16b
580df84
ae08014
d4b1f65
5ddd2f7
e66a204
a2a7b8e
9dcb672
e107701
6ec2604
7b41b8c
bfaff9b
a13697a
1c4447c
4266c9f
55401f4
45178dd
d5c83a8
5c8d3a9
39859df
4e8e38b
dbef250
3596000
02a7229
8751125
855ee4b
9bb6693
a019f15
aa601be
181397d
e575348
e28b3da
87a01f4
553ac31
278a134
0b81b9f
51a77dc
6b21092
2d8f4bc
eb29f3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package lotto | ||
|
||
import lotto.controller.LottoController | ||
|
||
fun main() { | ||
LottoController.run() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
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) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package lotto.model | ||
|
||
const val MINIMUM_MATCHES = 3 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about moving https://kotlinlang.org/docs/object-declarations.html#companion-objects Note: class members can access private members of their corresponding companion object. So |
||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not necessary to create How about refactoring them using enum classes?
Comment on lines
+14
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many functions, including 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. |
||
|
||
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 | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||
|
||||||||
companion object { | ||||||||
fun buyTickets(amount: Int, generator: TicketNumberGenerator): PurchasedTickets { | ||||||||
require(amount > 0) | ||||||||
val ticketCount = amount / TICKET_PRICE | ||||||||
return PurchasedTickets(List(ticketCount) { TicketModel.generate(generator) }) | ||||||||
} | ||||||||
} | ||||||||
} |
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() | ||
} | ||
} |
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()) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package lotto.model | ||
|
||
interface TicketNumberGenerator { | ||
fun generateNumbers(): List<Int> | ||
} |
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.") | ||
} | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this prizeMap is already in |
||
|
||
val returnRate = result.returnRate(purchaseAmount) | ||
println("Total return rate is %.2f (A rate below 1 means a loss)".format(returnRate)) | ||
} | ||
} | ||
|
||
} |
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) | ||
} | ||
} |
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
|
||
@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 | ||
} | ||
} |
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), | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
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.