Skip to content

Commit f26ad6b

Browse files
authored
Step3 (#1115)
* test: cashier 역할 변경에 따른 테스트 코드 수정 * refactor: 기본 생성자를 LottoNumber 생성하도록 수정한다. * refactor: 로또 관련 숫자를 한 클래스에서 관리하도록 변경한다. * refactor: FakeLottoNumberGenerator를 하나의 파일에서 관리하도록 수정한다. * refactor: 정적함수 클래스를 object 클래스에서 관리하도록 수정한다. * refactor: 사용자의 로또와 당첨로또의 숫자를 비교를 Lotto 클래스에서 담당한다. * fix: 로또 생성 시 LottoNumber size를 검증한다. * refactor: 로또 관련 상수를 하나의 파일에서 관리하도록 수정한다. * refactor: LottoNumberListGenerator 반환 타입을 `Set<LottoNumber>`로 수정한다. * refactor: 숫자를 직접 넣어 로또를 생성할 수 있는 코드를 제공한다. * refactor: 로또 생성 시 LottoNumber size를 검증한다. * refactor: 로또 생성시 중복을 허용하지 않는다. * docs: step3 요구사항 정리 * feat: 보너스볼 일치 여부로 2등을 계산한다. * feat: 보너스볼 일치 여부를 판단한다. * test: Lotto.getIntersectSize() 테스트 추가 * feat: 보너스볼을 입력받는다. * refactor: 로또 등수 계산을 WinningLotto 클래스로 분리 * refactor: 로또 등수 계산을 WinningLotto 클래스로 분리 * refactor: 로또 등수 enum으로 변경 * refactor: 로또 등수 enum으로 변경 * feat: 당첨 로또와 수익률을 계산한다. * feat: 로또 결과 출력 * fix: 로또 집계시 1 ~ 5등 출력되도록 수정 * refactor: 매직넘버 수정
1 parent 48c5fef commit f26ad6b

28 files changed

+500
-331
lines changed

docs/step3.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# 🚀 3단계 - 로또(2등)
2+
3+
## 기능 요구사항
4+
5+
- 2등을 위해 추가 번호를 하나 더 추첨한다.
6+
- 당첨 통계에 2등도 추가해야 한다.
7+
8+
- [ ] 보너스볼을 입력받는다. (`InputView.kt`)
9+
- [ ] 보너스볼과 당첨번호를 비교해서 로또 등수를 계산한다. (`Match.kt`)
10+
- [ ] 보너스볼 일치 여부(2등)을 통계에 추가한다. (`Statistics.kt`)

src/main/kotlin/lotto/LottoApplication.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package lotto
22

33
import lotto.domain.Cashier
44
import lotto.domain.Lotto
5+
import lotto.domain.LottoNumber
6+
import lotto.domain.LottoRank
57
import lotto.domain.Statistics
8+
import lotto.domain.WinningLotto
69
import lotto.stretagy.RandomLottoNumberListGenerator
710
import lotto.view.InputView
811
import lotto.view.OutputView
@@ -11,13 +14,19 @@ fun main() {
1114
val randomNumberListGenerator = RandomLottoNumberListGenerator()
1215
val amount = InputView.purchaseAmount()
1316

14-
val userLottos = Cashier.purchaseLotto(amount, randomNumberListGenerator)
15-
OutputView.printPurchaseResult(userLottos)
17+
val cashier = Cashier(amount, randomNumberListGenerator)
18+
val lottos = cashier.purchaseLotto()
1619

17-
val winningNumbers = InputView.winningNumbers()
18-
val winningLotto = Lotto.createLotto(winningNumbers)
20+
OutputView.printPurchaseResult(lottos)
1921

20-
val statisticsList = Statistics.of(userLottos, winningLotto)
21-
val earningRatio = Statistics.calculateEarningRatio(statisticsList, amount)
22-
OutputView.printLottoStatistics(statisticsList, earningRatio)
22+
val winningNumbers = InputView.winningNumbers().map(::LottoNumber).toSet()
23+
val bonusBall = LottoNumber(InputView.bonusBall())
24+
val winningLotto = WinningLotto(Lotto(winningNumbers), bonusBall)
25+
26+
val statistics = Statistics(winningLotto, lottos)
27+
val lottoResult: Map<LottoRank, Int> = statistics.lottoResultGroupByRank()
28+
val earningRatio = statistics.calculateEarningRatio(amount)
29+
val earningResult = statistics.getProfitStatus(earningRatio)
30+
31+
OutputView.printLottoResult(lottoResult, earningRatio, earningResult)
2332
}

src/main/kotlin/lotto/constant/LottoRankConstant.kt

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,4 @@ package lotto.constant
22

33
const val MINIMUM_NUMBER = 1
44
const val MAXIMUM_NUMBER = 45
5-
const val FIRST_RANK = 1
6-
const val SECOND_RANK = 2
7-
const val THIRD_RANK = 3
8-
const val FOURTH_RANK = 4
9-
const val FIFTH_RANK = 5
10-
const val NO_RANK = 0
11-
const val MATCH_COUNT_SIX = 6
12-
const val MATCH_COUNT_FIVE = 5
13-
const val MATCH_COUNT_FOUR = 4
14-
const val MATCH_COUNT_THREE = 3
5+
const val REQUIRED_LOTTO_SIZE = 6

src/main/kotlin/lotto/domain/Cashier.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ package lotto.domain
22

33
import lotto.stretagy.LottoNumberListGenerator
44

5-
class Cashier {
5+
class Cashier(
6+
private val amount: Int,
7+
private val lottoNumberListGenerator: LottoNumberListGenerator,
8+
) {
9+
fun purchaseLotto(): List<Lotto> {
10+
require(amount >= LOTTO_PRICE)
11+
val numberOfLotto = calculateNumberOfLotto(amount)
12+
return List(numberOfLotto) { Lotto(lottoNumberListGenerator.generate()) }
13+
}
14+
615
companion object {
716
private const val LOTTO_PRICE = 1000
817

9-
fun purchaseLotto(
10-
amount: Int,
11-
lottoNumberListGenerator: LottoNumberListGenerator,
12-
): List<Lotto> {
13-
require(amount >= LOTTO_PRICE)
14-
val numberOfLotto = calculateNumberOfLotto(amount)
15-
return List(numberOfLotto) { Lotto.createLotto(lottoNumberListGenerator.generate()) }
16-
}
17-
1818
private fun calculateNumberOfLotto(amount: Int): Int {
1919
return amount / LOTTO_PRICE
2020
}

src/main/kotlin/lotto/domain/Lotto.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
package lotto.domain
22

3-
class Lotto private constructor(val lottoNumbers: Set<LottoNumber>) {
4-
companion object {
5-
fun createLotto(numbers: List<Int>): Lotto {
6-
val lottoNumbers = numbers.map { LottoNumber.from(it) }.toSet()
3+
import lotto.constant.REQUIRED_LOTTO_SIZE
4+
5+
class Lotto(val lottoNumbers: Set<LottoNumber>) {
6+
constructor(vararg numbers: Int) : this(numbers.map(::LottoNumber).toSet())
77

8-
return Lotto(lottoNumbers)
8+
init {
9+
require(lottoNumbers.size == REQUIRED_LOTTO_SIZE) {
10+
ERROR_WRONG_NUMBER_COUNT
911
}
1012
}
13+
14+
fun getIntersectSize(winningLotto: Lotto): Int {
15+
val lottoNumbers: Set<LottoNumber> = lottoNumbers
16+
val winningLottoNumbers: Set<LottoNumber> = winningLotto.lottoNumbers
17+
return lottoNumbers.intersect(winningLottoNumbers).size
18+
}
19+
20+
fun isMatchedBonusBall(bonusBall: LottoNumber): Boolean {
21+
return lottoNumbers.contains(bonusBall)
22+
}
23+
24+
companion object {
25+
private const val ERROR_WRONG_NUMBER_COUNT = "정확히 6개의 숫자를 입력해야 합니다."
26+
}
1127
}
Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
package lotto.domain
22

3+
import lotto.constant.MAXIMUM_NUMBER
4+
import lotto.constant.MINIMUM_NUMBER
5+
36
@JvmInline
47
value class LottoNumber(val number: Int) {
5-
companion object {
6-
private const val MINIMUM_NUMBER = 1
7-
private const val MAXIMUM_NUMBER = 45
8-
private const val CANNOT_CREATE_LOTTO_NUMBER_MESSAGE = "Lotto number must be between %s and %s."
9-
10-
private val NUMBERS: Map<Int, LottoNumber> = (MINIMUM_NUMBER..MAXIMUM_NUMBER).associateWith { LottoNumber(it) }
11-
12-
fun from(number: Int): LottoNumber {
13-
return NUMBERS[number] ?: throw IllegalArgumentException(
14-
CANNOT_CREATE_LOTTO_NUMBER_MESSAGE.format(MINIMUM_NUMBER, MAXIMUM_NUMBER),
15-
)
8+
init {
9+
require(number in MINIMUM_NUMBER..MAXIMUM_NUMBER) {
10+
CANNOT_CREATE_LOTTO_NUMBER_MESSAGE.format(MINIMUM_NUMBER, MAXIMUM_NUMBER)
1611
}
1712
}
1813

19-
override fun toString(): String {
20-
return number.toString()
14+
companion object {
15+
private const val CANNOT_CREATE_LOTTO_NUMBER_MESSAGE = "Lotto number must be between %s and %s."
2116
}
2217
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package lotto.domain
2+
3+
enum class LottoRank(val matchCount: Int, val prize: Long) {
4+
FIRST(6, 2_000_000_000L),
5+
SECOND(5, 30_000_000L),
6+
THIRD(5, 1_500_000L),
7+
FOURTH(4, 50_000L),
8+
FIFTH(3, 5_000L),
9+
NONE(0, 0L),
10+
;
11+
12+
fun calculatePrize(count: Int): Long = prize * count
13+
}

src/main/kotlin/lotto/domain/Match.kt

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,22 @@
11
package lotto.domain
22

3-
import lotto.constant.FIFTH_RANK
4-
import lotto.constant.FIRST_RANK
5-
import lotto.constant.FOURTH_RANK
6-
import lotto.constant.MATCH_COUNT_FIVE
7-
import lotto.constant.MATCH_COUNT_FOUR
8-
import lotto.constant.MATCH_COUNT_SIX
9-
import lotto.constant.MATCH_COUNT_THREE
10-
import lotto.constant.NO_RANK
11-
import lotto.constant.THIRD_RANK
12-
13-
class Match {
14-
companion object {
15-
fun lottoNumber(
16-
userLotto: Lotto,
17-
winningLotto: Lotto,
18-
): Int {
19-
val lottoNumbers: Set<LottoNumber> = userLotto.lottoNumbers
20-
val winningLottoNumbers: Set<LottoNumber> = winningLotto.lottoNumbers
21-
22-
val matchCount = lottoNumbers.intersect(winningLottoNumbers).size
23-
return rank(matchCount)
3+
class Match(private val matchCount: Int, private val isMatchedBonusBall: Boolean) {
4+
fun rank(): LottoRank {
5+
if (matchCount == 6) {
6+
return LottoRank.FIRST
247
}
25-
26-
private fun rank(matchCount: Int): Int {
27-
return when (matchCount) {
28-
MATCH_COUNT_SIX -> FIRST_RANK
29-
MATCH_COUNT_FIVE -> THIRD_RANK
30-
MATCH_COUNT_FOUR -> FOURTH_RANK
31-
MATCH_COUNT_THREE -> FIFTH_RANK
32-
else -> NO_RANK
33-
}
8+
if (matchCount == 5 && isMatchedBonusBall) {
9+
return LottoRank.SECOND
10+
}
11+
if (matchCount == 5) {
12+
return LottoRank.THIRD
13+
}
14+
if (matchCount == 4) {
15+
return LottoRank.FOURTH
16+
}
17+
if (matchCount == 3) {
18+
return LottoRank.FIFTH
3419
}
20+
return LottoRank.NONE
3521
}
3622
}
Lines changed: 35 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,45 @@
11
package lotto.domain
22

3-
import lotto.constant.FIFTH_RANK
4-
import lotto.constant.FIRST_RANK
5-
import lotto.constant.FOURTH_RANK
6-
import lotto.constant.SECOND_RANK
7-
import lotto.constant.THIRD_RANK
8-
9-
data class Statistics(val rank: Int, val matchCount: Int) {
10-
fun earnings(): Long {
11-
return earningPriceByRanking() * matchCount
3+
import kotlin.math.floor
4+
5+
data class Statistics(private val winningLotto: WinningLotto, private val lottos: List<Lotto>) {
6+
fun lottoResultGroupByRank(): Map<LottoRank, Int> {
7+
val initialRanks =
8+
LottoRank.entries
9+
.filter { it.matchCount in MINIMUM_MATCH_COUNT..MAXIMUM_MATCH_COUNT }
10+
.associateWith { 0 }
11+
12+
val actualRanks =
13+
lottos.map { winningLotto.getUserRank(it) }
14+
.filter { it.matchCount in MINIMUM_MATCH_COUNT..MAXIMUM_MATCH_COUNT }
15+
.groupingBy { it }
16+
.eachCount()
17+
18+
return initialRanks + actualRanks
1219
}
1320

14-
private fun earningPriceByRanking(): Long =
15-
when (rank) {
16-
FIRST_RANK -> FIRST_RANK_EARNING
17-
SECOND_RANK -> SECOND_RANK_EARNING
18-
THIRD_RANK -> THIRD_RANK_EARNING
19-
FOURTH_RANK -> FOURTH_RANK_EARNING
20-
FIFTH_RANK -> FIFTH_RANK_EARNING
21-
else -> NO_RANK_EARNING
22-
}
21+
fun calculateEarningRatio(price: Int): Double {
22+
val ratio =
23+
lottoResultGroupByRank()
24+
.map { (rank, count) -> rank.calculatePrize(count) }
25+
.sum().toDouble() / price
2326

24-
companion object {
25-
private const val FIRST_RANK_EARNING = 2_000_000_000L
26-
private const val SECOND_RANK_EARNING = 1_500_000L
27-
private const val THIRD_RANK_EARNING = 50_000L
28-
private const val FOURTH_RANK_EARNING = 5_000L
29-
private const val FIFTH_RANK_EARNING = 0L
30-
private const val NO_RANK_EARNING = 0L
31-
32-
fun of(
33-
userLottos: List<Lotto>,
34-
winningLotto: Lotto,
35-
): List<Statistics> {
36-
val groupByRanking: Map<Int, List<Lotto>> =
37-
(FIFTH_RANK downTo FIRST_RANK).associateWith { emptyList<Lotto>() } +
38-
userLottos.groupBy { Match.lottoNumber(it, winningLotto) }
39-
40-
val statistics: List<Statistics> =
41-
groupByRanking.map { Statistics(it.key, it.value.size) }.sortedByDescending { it.rank }
42-
return getRankedLottos(statistics)
43-
}
27+
return floor(ratio * 100) / 100
28+
}
4429

45-
fun calculateEarningRatio(
46-
statisticsList: List<Statistics>,
47-
amount: Int,
48-
): Double {
49-
return statisticsList.sumOf { it.earnings() }.toDouble() / amount
30+
fun getProfitStatus(earningRatio: Double): String =
31+
when {
32+
earningRatio > EARNING_RATIO_THRESHOLD -> PROFIT_MESSAGE
33+
earningRatio == EARNING_RATIO_THRESHOLD -> BREAK_EVEN_MESSAGE
34+
else -> LOSS_MESSAGE
5035
}
5136

52-
private fun getRankedLottos(statisticsList: List<Statistics>): List<Statistics> {
53-
return statisticsList.filter { it.rank in FIRST_RANK..FIFTH_RANK && it.rank != SECOND_RANK }
54-
}
37+
companion object {
38+
private const val MINIMUM_MATCH_COUNT = 3
39+
private const val MAXIMUM_MATCH_COUNT = 6
40+
private const val EARNING_RATIO_THRESHOLD = 1.0
41+
private const val PROFIT_MESSAGE = "이익"
42+
private const val BREAK_EVEN_MESSAGE = "본전"
43+
private const val LOSS_MESSAGE = "손해"
5544
}
5645
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package lotto.domain
2+
3+
data class WinningLotto(private val winningLotto: Lotto, private val bonusBall: LottoNumber) {
4+
fun getUserRank(userLotto: Lotto): LottoRank {
5+
val matchCount = userLotto.getIntersectSize(this.winningLotto)
6+
val isMatchedBonusBall = userLotto.isMatchedBonusBall(bonusBall)
7+
return Match(matchCount, isMatchedBonusBall).rank()
8+
}
9+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package lotto.stretagy
22

3+
import lotto.domain.LottoNumber
4+
35
interface LottoNumberListGenerator {
4-
fun generate(): List<Int>
6+
fun generate(): Set<LottoNumber>
57
}

src/main/kotlin/lotto/stretagy/RandomLottoNumberListGenerator.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ package lotto.stretagy
22

33
import lotto.constant.MAXIMUM_NUMBER
44
import lotto.constant.MINIMUM_NUMBER
5+
import lotto.constant.REQUIRED_LOTTO_SIZE
6+
import lotto.domain.LottoNumber
57

68
class RandomLottoNumberListGenerator : LottoNumberListGenerator {
7-
override fun generate(): List<Int> {
9+
override fun generate(): Set<LottoNumber> {
810
return (MINIMUM_NUMBER..MAXIMUM_NUMBER).shuffled()
9-
.take(NUMBER_OF_SELECT)
11+
.take(REQUIRED_LOTTO_SIZE)
1012
.sorted()
11-
}
12-
13-
companion object {
14-
private const val NUMBER_OF_SELECT = 6
13+
.map(::LottoNumber)
14+
.toSet()
1515
}
1616
}

src/main/kotlin/lotto/view/InputView.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class InputView {
77
private const val REPLACEMENT_SOURCE = " "
88
private const val REPLACEMENT_TARGET = ""
99
private const val WINNING_NUMBER_INPUT_MESSAGE = "지난 주 당첨 번호를 입력해 주세요."
10+
private const val BONUS_BALL_NUMBER_INPUT_MESSAGE = "보너스 볼을 입력해 주세요."
1011
private const val ERROR_INVALID_NUMBER = "유효한 숫자를 입력해주세요."
1112
private const val ERROR_WRONG_NUMBER_COUNT = "정확히 6개의 숫자를 입력해야 합니다."
1213

@@ -16,12 +17,19 @@ class InputView {
1617
?: throw IllegalArgumentException(ERROR_INVALID_NUMBER)
1718
}
1819

19-
fun winningNumbers(): List<Int> {
20+
fun winningNumbers(): Set<Int> {
2021
println(WINNING_NUMBER_INPUT_MESSAGE)
2122
return readln().replace(REPLACEMENT_SOURCE, REPLACEMENT_TARGET)
2223
.split(WINNING_NUMBER_DELIMITERS)
2324
.also { require(it.size == 6) { ERROR_WRONG_NUMBER_COUNT } }
2425
.map { it.toIntOrNull() ?: throw IllegalArgumentException(ERROR_INVALID_NUMBER) }
26+
.toSet()
27+
}
28+
29+
fun bonusBall(): Int {
30+
println(BONUS_BALL_NUMBER_INPUT_MESSAGE)
31+
return readln().toIntOrNull()
32+
?: throw IllegalArgumentException(ERROR_INVALID_NUMBER)
2533
}
2634
}
2735
}

0 commit comments

Comments
 (0)