-
Notifications
You must be signed in to change notification settings - Fork 357
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
[STEP-2] 로또(자동) #1054
base: 0703kyj
Are you sure you want to change the base?
[STEP-2] 로또(자동) #1054
Changes from all commits
0eeefef
b565de4
8985d39
31af8e4
2a910cb
9180d34
c767b0f
4743ea7
5e85811
672ed89
5c49d4a
48cfead
3c7a55a
e587585
8c8bc13
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 |
---|---|---|
@@ -0,0 +1,18 @@ | ||
### Lotto | ||
|
||
- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다. | ||
- [x] 로또 1장의 가격은 1000원이다. | ||
- [x] 최소 구매금액은 1000원이다. | ||
- [x] 구매금액의 최소 단위는 1000원이다. | ||
- [x] 사용자는 지불한 금액만큼의 로또를 가지고 있는다. | ||
- [x] 사용자는 지불한 금액을 알고 있다. | ||
- [x] 각 로또마다 6개의 숫자를 가지고 있다. | ||
- [x] 로또의 숫자들은 무작위로 섞여 있다. | ||
- [x] 로또의 숫자는 1~45 까지 이다. | ||
- [x] 당첨 번호를 입력할 수 있다. | ||
- [x] 당첨 번호에 따른 맞춘 개수를 로또가 가지고 있는다. | ||
- [x] 당첨 통계를 낼 수 있다. | ||
- [x] 3~6개 일치 여부를 판단할 수 있다. | ||
- [x] 일치한 개수 별로 당첨 금액이 다르게 책정된다. | ||
- [x] 당첨된 로또의 개수를 계산할 수 있다. | ||
- [x] 총 수익률을 계산할 수 있다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dependencies {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package lotto | ||
|
||
import lotto.domain.CorrectNumbers | ||
import lotto.domain.LottoPurchaseAmount | ||
import lotto.domain.LottoUser | ||
import lotto.view.InputView.inputCorrectNumbers | ||
import lotto.view.InputView.inputPurchaseAmount | ||
import lotto.view.ResultView.printLotteries | ||
import lotto.view.ResultView.printLottoPurchaseAmount | ||
import lotto.view.ResultView.printLottoResult | ||
import lotto.view.ResultView.print수익률 | ||
|
||
fun main() { | ||
val lottoPurchaseAmount = LottoPurchaseAmount(inputPurchaseAmount()) | ||
printLottoPurchaseAmount(lottoPurchaseAmount) | ||
|
||
val lottoUser = LottoUser(lottoPurchaseAmount) | ||
printLotteries(lottoUser.lotteries) | ||
|
||
val correctNumbers = CorrectNumbers(inputCorrectNumbers()) | ||
|
||
lottoUser.checkLotteries(correctNumbers) | ||
printLottoResult(lottoUser.calculateLottoCorrectCount()) | ||
print수익률(lottoUser.수익률) | ||
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. 함수명이나 변수명으로 한글을 활용해주셨네요! 저는 개인적으로 프로젝트 전체적으로 명확히 한글을 사용하는 상황에 대해 팀에서 합의만 된다면 크게 상관없다고 생각합니다 🙂 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package lotto.domain | ||
|
||
data class CorrectNumbers( | ||
val values: Set<Int>, | ||
) { | ||
init { | ||
require(values.size == CORRECT_NUMBER_COUNT) { | ||
"[CorrectNumbers] 당첨 번호의 개수가 6개가 아닙니다. | 당첨번호: '$values'" | ||
Comment on lines
+3
to
+8
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. 다른 개발자들도 |
||
} | ||
} | ||
|
||
companion object { | ||
private const val CORRECT_NUMBER_COUNT = 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. 이 6이라는 숫자는 |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package lotto.domain | ||
|
||
import lotto.domain.enums.LottoCompensationStrategy | ||
|
||
data class Lotto( | ||
private var correctCount: Int? = null, | ||
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. 코틀린의 가장 큰 매력 중 하나는 null-safety 하다는 점인데요, |
||
val rolling: () -> Set<Int> = { rollingRandom() }, | ||
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. 테스트 가능한 구조를 잘 설계하기 위한 노력이 보였습니다! 다만 현재 구조에서는 테스트 가능하게 만드는 여러 가지 구조 아이디어가 있으니 다음 글도 도움이 되었으면 좋겠습니다! |
||
) { | ||
val values: Set<Int> = rolling.invoke() | ||
|
||
val markedCorrectCount | ||
get() = correctCount ?: error("[Lotto] 마킹이 되지 않은 로또입니다.") | ||
|
||
val compensation | ||
get() = LottoCompensationStrategy.getCompensationByCorrectCount(correctCount) | ||
|
||
fun markCorrectCount(correctCount: Int) { | ||
if (this.correctCount != null) { | ||
error("[Lotto] 이미 당첨 개수 마킹이 완료된 로또입니다. | 로또 당첨개수: '$this', 마킹 시도한 당첨개수: $correctCount") | ||
} | ||
this.correctCount = correctCount | ||
} | ||
|
||
companion object { | ||
private const val MIN_LOTTO_NUMBER = 1 | ||
private const val MAX_LOTTO_NUMBER = 45 | ||
private const val LOTTO_NUMBER_COUNT = 6 | ||
|
||
private fun rollingRandom(): Set<Int> { | ||
return (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER) | ||
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.
|
||
.shuffled() | ||
.take(LOTTO_NUMBER_COUNT) | ||
.toSet() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package lotto.domain | ||
|
||
data class LottoPurchaseAmount( | ||
val amount: Int, | ||
) { | ||
Comment on lines
+3
to
+5
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. 이번 기회에 value class에 대해 살펴보고 data class랑 각각 어떤 상황에서 사용하는 것이 적합할지 직접 판단해보셔도 좋을 것 같아요 ! |
||
init { | ||
amount.validateMinimum() | ||
amount.validatePurchaseMinUnit() | ||
} | ||
|
||
fun calculateLottoCount(): Int = amount / MIN_PURCHASE_AMOUNT | ||
|
||
private fun Int.validateMinimum() { | ||
require(this >= MIN_PURCHASE_AMOUNT) { | ||
"[LottoPurchaseAmount] 구매금액은 ${MIN_PURCHASE_AMOUNT}원 이상이어야 합니다. | 입력금액: $this" | ||
} | ||
} | ||
|
||
private fun Int.validatePurchaseMinUnit() { | ||
require(this % MIN_PURCHASE_AMOUNT == 0) { | ||
"[LottoPurchaseAmount] 구매금액은 ${MIN_PURCHASE_AMOUNT}원 단위이어야 합니다. | 입력금액: $this" | ||
} | ||
} | ||
|
||
companion object { | ||
private const val MIN_PURCHASE_AMOUNT = 1000 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package lotto.domain | ||
|
||
import lotto.domain.enums.LottoCompensationStrategy | ||
import java.math.BigDecimal | ||
|
||
class LottoUser( | ||
val lottoPurchaseAmount: LottoPurchaseAmount, | ||
lottoCount: Int = lottoPurchaseAmount.calculateLottoCount(), | ||
lottoGenerateStrategy: (() -> Set<Int>)? = null, | ||
) { | ||
val lotteries: List<Lotto> = generateLotteries(lottoCount, lottoGenerateStrategy) | ||
|
||
val compensation: Long | ||
get() = lotteries.sumOf { it.compensation } | ||
val 수익률: BigDecimal | ||
get() = BigDecimal(compensation) / BigDecimal(lottoPurchaseAmount.amount) | ||
|
||
fun checkLotteries(correctNumbers: CorrectNumbers) { | ||
lotteries.forEach { lotto -> | ||
val lottoNumbers = lotto.values | ||
val correctCount = lottoNumbers.intersect(correctNumbers.values).size | ||
|
||
lotto.markCorrectCount(correctCount) | ||
} | ||
} | ||
|
||
fun calculateLottoCorrectCount(): Map<LottoCompensationStrategy, Int> { | ||
return LottoCompensationStrategy.entries.associateWith { strategy -> | ||
lotteries.count { it.markedCorrectCount == strategy.correctCount } | ||
} | ||
} | ||
|
||
companion object { | ||
private fun generateLotteries(lottoCount: Int, lottoGenerateStrategy: (() -> Set<Int>)?) = List(lottoCount) { | ||
lottoGenerateStrategy?.let { Lotto { it.invoke() } } ?: Lotto() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package lotto.domain.enums | ||
|
||
private const val DEFAULT_COMPENSATION_UNIT = "원" | ||
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. "원"이라는 값은 도메인보다는 뷰 요구사항에 적절하지 않을까요? (자동차 경주 미션 5단계 참고) |
||
|
||
enum class LottoCompensationStrategy( | ||
val correctCount: Int, | ||
val compensation: Long, | ||
val unit: String, | ||
) { | ||
`3개`(3, 5_000, DEFAULT_COMPENSATION_UNIT), | ||
`4개`(4, 50_000, DEFAULT_COMPENSATION_UNIT), | ||
`5개`(5, 1_500_000, DEFAULT_COMPENSATION_UNIT), | ||
`6개`(6, 2_000_000_000, DEFAULT_COMPENSATION_UNIT), | ||
; | ||
|
||
companion object { | ||
private const val DEFAULT_COMPENSATION = 0L | ||
|
||
fun findByCorrectCount(correctCount: Int?): LottoCompensationStrategy? = | ||
entries.find { it.correctCount == correctCount } | ||
|
||
fun getCompensationByCorrectCount(correctCount: Int?): Long = | ||
findByCorrectCount(correctCount)?.compensation | ||
?: DEFAULT_COMPENSATION | ||
Comment on lines
+19
to
+24
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. enum class에게도 메세지를 보내어 외부에서 조작하지 않고, 적절한 값을 받아올 수 있는 구조를 잘 만들어주신것 같아요 👍 |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package lotto.view | ||
|
||
object InputView { | ||
private const val CORRECT_NUMBER_DELIMITER = "," | ||
|
||
fun inputPurchaseAmount() = input("구입금액을 입력해 주세요.") | ||
.toIntOrThrow() | ||
|
||
fun inputCorrectNumbers() = input("지난 주 당첨 번호를 입력해 주세요.") | ||
.toIntsOrThrow() | ||
.toSet() | ||
|
||
private fun input(message: String): String { | ||
println(message) | ||
return readln() | ||
} | ||
|
||
private fun String.toIntOrThrow(): Int = runCatching { | ||
this.toInt() | ||
}.getOrElse { throw IllegalArgumentException("[InputView] 값을 Int로 변환하는데 실패했습니다. | '$this'") } | ||
|
||
private fun String.toIntsOrThrow(): List<Int> { | ||
return this.split(CORRECT_NUMBER_DELIMITER) | ||
.map { it.trim().toIntOrThrow() } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package lotto.view | ||
|
||
import lotto.domain.Lotto | ||
import lotto.domain.LottoPurchaseAmount | ||
import lotto.domain.enums.LottoCompensationStrategy | ||
import java.math.BigDecimal | ||
|
||
object ResultView { | ||
fun printLottoPurchaseAmount(purchaseAmount: LottoPurchaseAmount) { | ||
val totalLottoCount = purchaseAmount.calculateLottoCount() | ||
println("${totalLottoCount}개를 구매했습니다.") | ||
} | ||
|
||
fun printLotteries(lotteries: List<Lotto>) { | ||
lotteries.forEach { | ||
println(it.values) | ||
} | ||
println() | ||
} | ||
|
||
fun printLottoResult(lottoResults: Map<LottoCompensationStrategy, Int>) { | ||
val title = """ | ||
|
||
당첨 통계 | ||
--------- | ||
""".trimIndent() | ||
println(title) | ||
lottoResults.forEach { (strategy, count) -> | ||
println("${strategy.correctCount}개 일치 (${strategy.compensation}${strategy.unit})- ${count}개") | ||
} | ||
} | ||
|
||
fun print수익률(수익률: BigDecimal) { | ||
println("총 수익률은 ${수익률.setScale(2)}입니다.") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package lotto.domain | ||
|
||
import io.kotest.assertions.throwables.shouldThrowExactly | ||
import io.kotest.matchers.string.shouldContain | ||
import org.junit.jupiter.api.Test | ||
|
||
class CorrectNumbersTest { | ||
@Test | ||
fun `당첨번호는 6개가 아닌 경우 예외가 발생한다`() { | ||
val inputs = List(5) { it }.toSet() | ||
|
||
val correctNumbers = shouldThrowExactly<IllegalArgumentException> { | ||
CorrectNumbers(inputs) | ||
} | ||
correctNumbers.message shouldContain "당첨 번호의 개수가 6개가 아닙니다" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package lotto.domain | ||
|
||
import io.kotest.assertions.throwables.shouldNotThrowAny | ||
import io.kotest.assertions.throwables.shouldThrowExactly | ||
import io.kotest.matchers.shouldBe | ||
import io.kotest.matchers.string.shouldContain | ||
import org.junit.jupiter.api.Nested | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.ValueSource | ||
|
||
class LottoPurchaseAmountTest { | ||
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. 얼마의 금액으로 몇 장의 로또를 살 수 있었는지도 검증해보면 어떨까요~? |
||
@Nested | ||
inner class ValidateTest { | ||
@ParameterizedTest | ||
@ValueSource(ints = [1000, 2000, 3000, 4000]) | ||
fun `로또 1장의 최소 구매금액은 1000원이다`(purchaseAmount: Int) { | ||
val lottoPurchaseAmount = shouldNotThrowAny { | ||
LottoPurchaseAmount(purchaseAmount) | ||
} | ||
lottoPurchaseAmount.amount shouldBe purchaseAmount | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = [200, 400, 600, 800, 999]) | ||
fun `로또를 1000원 미만으로 구매하려는 경우 예외가 발생한다`(purchaseAmount: Int) { | ||
val exception = shouldThrowExactly<IllegalArgumentException> { | ||
LottoPurchaseAmount(purchaseAmount) | ||
} | ||
exception.message shouldContain "구매금액은 ${MIN_PURCHASE_AMOUNT}원 이상이어야 합니다" | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = [1000, 2000, 3000, 4000]) | ||
fun `로또 1장의 구매금액의 최소 단위는 1000원이다`(purchaseAmount: Int) { | ||
val lottoPurchaseAmount = shouldNotThrowAny { | ||
LottoPurchaseAmount(purchaseAmount) | ||
} | ||
lottoPurchaseAmount.amount shouldBe purchaseAmount | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = [1001, 2010, 3100, 4111]) | ||
fun `로또 1장의 구매금액이 1000원 단위가 아닌 경우 예외가 발생한다`(purchaseAmount: Int) { | ||
val exception = shouldThrowExactly<IllegalArgumentException> { | ||
LottoPurchaseAmount(purchaseAmount) | ||
} | ||
exception.message shouldContain "구매금액은 ${MIN_PURCHASE_AMOUNT}원 단위이어야 합니다" | ||
} | ||
} | ||
|
||
companion object { | ||
private const val MIN_PURCHASE_AMOUNT = 1000 | ||
} | ||
} |
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.
기능 목록을 잘 만들어주셨네요 👍
이렇게 작성하신 기능 목록이 결국에는 커밋 단위, 테스트 시나리오까지 이어지게 되는데요,
특히 TDD를 하시는 과정에서 객체 설계 및 “어떤 시나리오를 테스트해야 하지“에 대한 본인만의 답을 만들기에도 도움이 될거예요.