Skip to content
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

[Step2] 로또(자동) #705

Open
wants to merge 12 commits into
base: qktlf789456
Choose a base branch
from
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
# kotlin-lotto
# kotlin-lotto

## TODO
- [x] 로또를 생성할 수 있다.
- [x] 중복된 숫자가 포함된 로또를 생성할 수 없다.
- [x] 생성된 로또 숫자들을 오름차순으로 조회할 수 있다.
- [x] 로또 라운드에서 당첨번호를 통해 결과를 도출할 수 있다.
- [x] 해당 라운드에서 발행된 로또중 당첨번호와 일치하는 숫자 개수별로 구분할 수 있다.
- [x] 일치하는 숫자 개수별로 수익률을 알 수 있다.
50 changes: 50 additions & 0 deletions src/main/kotlin/lotto/domain/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package lotto.domain
class Lotto(lottoNumbers: List<LottoNumber>) {
val lottoNumbers: List<LottoNumber>
Comment on lines +2 to +3

Choose a reason for hiding this comment

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

정렬이라면 view의 로직이기에 init에서 제거하고, 생성자를 통해 lottoNumber를 상태로 정의하는건 어떨까요?


init {
require(lottoNumbers.size == LOTTO_SIZE)
hasNoDuplicatedNumbers(lottoNumbers)

Choose a reason for hiding this comment

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

자료구조를 잘 활용하면 이 부분은 제거해도 될거같아요.


this.lottoNumbers = lottoNumbers.sortedBy { it.number }

Choose a reason for hiding this comment

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

이건 화면의 중요한 로직이겠네요~

}

fun getSameNumberCount(lotto: Lotto): Int {
return (LOTTO_SIZE * 2) - (lottoNumbers + lotto.lottoNumbers).toSet().size

Choose a reason for hiding this comment

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

컴퓨팅 파워는 조금 더 들지라도, 조금 더 간단하게 작성할 수 있을까요?

}

private fun hasNoDuplicatedNumbers(lottoNumbers: List<LottoNumber>) {
require(lottoNumbers.toSet().size == LOTTO_SIZE)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Lotto) return false

if (lottoNumbers != other.lottoNumbers) return false

return true
}

override fun hashCode(): Int {
return lottoNumbers.hashCode()
}
Comment on lines +20 to +31

Choose a reason for hiding this comment

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

어떻게 하면 제거할 수 있을까요?


companion object {
const val LOTTO_SIZE = 6

fun of(lottoNumbers: List<Int>): Lotto = Lotto(lottoNumbers.map { LottoNumber(it) })
}
}

data class LottoNumber(val number: Int) {
init {
require(MIN_LOTTO_NUMBER <= number)
require(number <= MAX_LOTTO_NUMBER)
Comment on lines +42 to +43

Choose a reason for hiding this comment

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

number in (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER)는 어떨까요?~

}

companion object {
const val MIN_LOTTO_NUMBER = 1
const val MAX_LOTTO_NUMBER = 45
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/domain/LottoGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.domain

import lotto.domain.Lotto.Companion.LOTTO_SIZE
import lotto.domain.LottoNumber.Companion.MAX_LOTTO_NUMBER
import lotto.domain.LottoNumber.Companion.MIN_LOTTO_NUMBER

interface LottoGenerator {
fun generate(): Lotto
}

class RandomLottoGenerator : LottoGenerator {
override fun generate(): Lotto = LOTTO_NUMBERS.shuffled().subList(0, LOTTO_SIZE).let { Lotto(it) }

companion object {
private val LOTTO_NUMBERS = (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER).map { LottoNumber(it) }
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/lotto/domain/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package lotto.domain

import lotto.domain.Money.Companion.toMoney

data class LottoResult(
val lotto: Lotto,
val winningLotto: Lotto
) {
val sameNumberCount: Int = winningLotto.getSameNumberCount(lotto)

val reward: LottoReward? = LottoReward.getReward(sameNumberCount)
Comment on lines +9 to +11

Choose a reason for hiding this comment

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

이것도 순서를 위아래 바꾸면 좋겟네요~!

}

enum class LottoReward(val prize: Long, val sameNumberCount: Int) {
WINNER_1ST(2000L.millionWon(), 6),
WINNER_2ST(1500L.thousandWon(), 5),
WINNER_3ST(50L.thousandWon(), 4),
WINNER_4ST(5L.thousandWon(), 3);

fun toMoney(): Money = prize.toMoney()

companion object {
fun getReward(sameNumberCount: Int): LottoReward? = values().firstOrNull { sameNumberCount == it.sameNumberCount }
}
}

private fun Long.thousandWon(): Long = this * 1000L

private fun Long.millionWon(): Long = this.thousandWon().thousandWon()
Comment on lines +27 to +29

Choose a reason for hiding this comment

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

to~로 하는것도 좋을거 같아요~

24 changes: 24 additions & 0 deletions src/main/kotlin/lotto/domain/LottoRound.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package lotto.domain
class LottoRound(private val lottoGenerator: LottoGenerator) {
constructor(lottoRoundElements: LottoRoundElements) : this(lottoRoundElements.lottoGenerator)

private val lottos: MutableList<Lotto> = mutableListOf()

fun addNewLottos(newLottoSize: Int) {
repeat(newLottoSize) {
lottos.add(newLotto())
}
}
Comment on lines +5 to +11

Choose a reason for hiding this comment

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

mutable로 정의하면 코드가 취약해져요. 어떻게 하면 제거할 수 있을까요?
팩터리 메서드를 활용하면 괜찮을거 같아요.


fun getLottos(): List<Lotto> = lottos.toList()

fun lotteryDraw(winningLotto: Lotto): LottoRoundStatistics {
return LottoRoundStatistics(lottos, winningLotto)
}

private fun newLotto() = lottoGenerator.generate()

Choose a reason for hiding this comment

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

좋네요~ 이런 명확한 함수는 좋습니다~

}

data class LottoRoundElements(
val lottoGenerator: LottoGenerator = RandomLottoGenerator()
)
Comment on lines +22 to +24

Choose a reason for hiding this comment

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

해당 클래스는 무엇일까요?

28 changes: 28 additions & 0 deletions src/main/kotlin/lotto/domain/LottoRoundStatistics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lotto.domain

import lotto.domain.Money.Companion.NO_MONEY
import lotto.domain.Money.Companion.plus

class LottoRoundStatistics(
lottos: List<Lotto>,
private val winningLotto: Lotto
) {
val lottoResults: List<LottoResult> = lottos.map { LottoResult(it, winningLotto) }

val totalPrize: Money = lottoResults.mapNotNull { it.reward }
.fold(NO_MONEY) { money, lottoReward ->
money + lottoReward.toMoney()
}

fun getLottoRewardOf(lottoReward: LottoReward): List<LottoResult> = lottoResults.filter { it.reward == lottoReward }.toList()
}

@JvmInline value class Money(val value: Long) {
companion object {
fun Long.toMoney(): Money = Money(this)

operator fun Money.plus(other: Money): Money = (value + other.value).toMoney()

val NO_MONEY: Money = 0L.toMoney()
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/lotto/domain/LottoServiceRound.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.domain

import lotto.domain.Money.Companion.toMoney

class LottoServiceRound {
private val lottoRound = LottoRound(LottoRoundElements())

fun buyLottos(payment: Long): List<Lotto> {
lottoRound.addNewLottos(payment.buyableCount())
return lottoRound.getLottos()
}

fun allPayment(): Long = (lottoRound.getLottos().size * LOTTO_BUY_PRIZE.value)

fun lotteryDraw(numbers: List<Int>): LottoRoundStatistics {

Choose a reason for hiding this comment

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

함수명은 동사로 해볼까요?

val winningLotto = Lotto.of(numbers)
return lottoRound.lotteryDraw(winningLotto)
}

private fun Long.buyableCount(): Int = (this / LOTTO_BUY_PRIZE.value).toInt()

companion object {
private val LOTTO_BUY_PRIZE = 1000L.toMoney()
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/lotto/view/LottoInputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package lotto.view

import java.lang.IllegalArgumentException

class LottoInputView {
fun inputLottoBuy(): Int {
println(LOTTO_BUY_COMMENT)
return readLineOrThrows().toInt()
}

fun inputWinningLotto(): List<Int> {
println(WINNING_LOTTO_COMMENT)
return readLineOrThrows().replace(" ", "").split(",").map { it.toInt() }
}

private fun readLineOrThrows(): String = readLine() ?: throw IllegalArgumentException()

Choose a reason for hiding this comment

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

readln()을 쓰면 만족하실거 같아요!


companion object {
private const val LOTTO_BUY_COMMENT = "구입금액을 입력해 주세요."
private const val WINNING_LOTTO_COMMENT = "지난 주 당첨 번호를 입력해 주세요."
}
}
49 changes: 49 additions & 0 deletions src/main/kotlin/lotto/view/LottoOutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lotto.view

import lotto.domain.Lotto
import lotto.domain.LottoReward
import lotto.domain.LottoRoundStatistics

class LottoOutputView {
fun currentLottos(lottos: List<Lotto>) {
val lottoSize = lottos.size
println("${lottoSize}개를 구매했습니다.")
repeat(lottoSize) { index ->
lottos[index].lottoNumbers.map { it.number }.joinToString(prefix = "[", separator = ", ", postfix = "]").also { println(it) }
}
}

fun result(payment: Long, lottoRoundStatistics: LottoRoundStatistics) {
println(WINNING_C0MMENT)

with(LottoReward.WINNER_4ST) {

Choose a reason for hiding this comment

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

lottoRoundStatistics에게 등수와 개수를 알려달라고 한번에 요청하는건 어떨까요? Map리턴 타입으로요~

lottoRoundStatistics.getLottoRewardOf(this).run {

Choose a reason for hiding this comment

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

가독성을 위해 개인적으로 with, run, let, also는 해당 객체의 상태를 변경하고, 추가로 해당 객체에 작업을 이어서 작업할 때 사용하는 편입니다.

println("${sameNumberCount}개 일치 (${prize}원)- $size")

Choose a reason for hiding this comment

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

sameNumberCount, prize , sizeLottoReward의 값인지 getLottoRewardOf의 값인지 알기 어려울 수 있을거 같아요!

}
}

with(LottoReward.WINNER_3ST) {
lottoRoundStatistics.getLottoRewardOf(this).run {
println("${sameNumberCount}개 일치 (${prize}원)- $size")
}
}

with(LottoReward.WINNER_2ST) {
lottoRoundStatistics.getLottoRewardOf(this).run {
println("${sameNumberCount}개 일치 (${prize}원)- $size")
}
}

with(LottoReward.WINNER_1ST) {
lottoRoundStatistics.getLottoRewardOf(this).run {
println("${sameNumberCount}개 일치 (${prize}원)- $size")
}
}

println("총 수익률은 0.${(lottoRoundStatistics.totalPrize.value * 100 / payment)}입니다.")
}

companion object {
private const val WINNING_C0MMENT = "당첨 통계\n---------"
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import lotto.domain.LottoServiceRound
import lotto.view.LottoInputView
import lotto.view.LottoOutputView

fun main() = lotto()

Choose a reason for hiding this comment

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

메인을 바로 사용하는것은 어떨까요?


fun lotto() {
val lottoServiceRound = LottoServiceRound()
val lottoInputView = LottoInputView()
val lottoOutputView = LottoOutputView()

val payment = lottoInputView.inputLottoBuy()
lottoServiceRound.buyLottos(payment.toLong()).also { lottoOutputView.currentLottos(it) }

val winningLottoNumbers = lottoInputView.inputWinningLotto()
val lottoRoundStatistics = lottoServiceRound.lotteryDraw(winningLottoNumbers)
lottoOutputView.result(lottoServiceRound.allPayment(), lottoRoundStatistics)
}
30 changes: 30 additions & 0 deletions src/test/kotlin/lotto/domain/LottoNumberTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.lang.RuntimeException

class LottoNumberTest {
@Test
fun `로또 숫자의 범위는 1 ~ 45 까지만 허용된다`() {
// given
// when
// then
(1..45).forEach {
assertThat(LottoNumber(it).number).isEqualTo(it)
}
Comment on lines +14 to +16

Choose a reason for hiding this comment

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

테스트코드 내부에서 반복을 사용하면 복잡해질 수 있어요.
@ParametereizedTest등을 사용해볼까요

}

@Test
fun `1 ~ 45 범위 이외에 로또번호는 생성될 수 없다`() {

Choose a reason for hiding this comment

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

46에 대한 검즈은 없네요!

// given

// when

// then
assertThrows<RuntimeException> {

Choose a reason for hiding this comment

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

정확한 Exception을 명시해볼까요?

LottoNumber(0).number

Choose a reason for hiding this comment

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

.number가 있으면 해당 코드 때문에 발생하는걸로 인지할 수 있을거 같아요.

}
}
}
38 changes: 38 additions & 0 deletions src/test/kotlin/lotto/domain/LottoRoundStatisticsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package lotto.domain

import lotto.domain.Money.Companion.toMoney
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class LottoRoundStatisticsTest {

@Test
fun `당첨된 로또를 기준으로 추첨 상세결과를 확인할 수 있다`() {
// given
val lotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) }
val winningLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) }
val sut = LottoRoundStatistics(listOf(lotto), winningLotto)

// when
// then
assertThat(sut.lottoResults.size).isEqualTo(1)
assertThat(sut.lottoResults.first().lotto).isEqualTo(lotto)
assertThat(sut.lottoResults.first().winningLotto).isEqualTo(winningLotto)
assertThat(sut.lottoResults.first().sameNumberCount).isEqualTo(6)
assertThat(sut.lottoResults.first().reward).isEqualTo(LottoReward.WINNER_1ST)
assertThat(sut.totalPrize).isEqualTo(LottoReward.WINNER_1ST.prize.toMoney())
Comment on lines +18 to +23

Choose a reason for hiding this comment

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

여기서 생성자를 테스트 하는 코드도 조금은 보이네요! 테스트를 조금 더 분리해볼까요?

}

@Test
fun `LottoReward 에 해당하는 로또를 가져올 수 있다`() {
// given
val lotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) }
val winningLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) }
val sut = LottoRoundStatistics(listOf(lotto), winningLotto)

// when
// then
assertThat(sut.getLottoRewardOf(LottoReward.WINNER_1ST).size).isEqualTo(1)
assertThat(sut.getLottoRewardOf(LottoReward.WINNER_2ST).size).isEqualTo(0)
}
}
35 changes: 35 additions & 0 deletions src/test/kotlin/lotto/domain/LottoRoundTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package lotto.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class LottoRoundTest {

@Test
fun `새로운 로또를 개수만큼 추가할 수 있다`() {
// given
val sut = LottoRound(LottoRoundElements())
val newLottoSize = 3

// when
sut.addNewLottos(newLottoSize)

// then
assertThat(sut.getLottos().size).isEqualTo(newLottoSize)
}

@Test
fun `로또 추첨결과를 얻을 수 있다`() {
// given
val winningLotto = (1..6).map { LottoNumber(it) }.toList().let { Lotto(it) }
val sut = LottoRound(LottoRoundElements())
val newLottoSize = 3
sut.addNewLottos(3)

// when
val result = sut.lotteryDraw(winningLotto)

// then
assertThat(result.lottoResults.size).isEqualTo(newLottoSize)
}
}
Loading