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

🚀 2단계 - 로또(자동) #532

Open
wants to merge 19 commits into
base: shinseongsu
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@
- [x] 쉼표로 문자 구분
- [x] 콜론으로 문자 구분
- [x] 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 “//”와 “\n” 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
- [x] 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw 한다.
- [x] 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw 한다.

# 🚀 2단계 - 로또(자동)

- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- [x] 1~45 숫자중에 6가지를 선택하여 가져온다.
- [x] 로또 1장의 가격은 1000원이다.
- [x] 당첨 로또는 직접 입력받는다.
- [x] 당첨금에 대한 수익률을 출력한다.
- [x] 당첨 통계를 나타낸다.
12 changes: 12 additions & 0 deletions src/main/kotlin/calculator/domain/StringConvert.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package calculator.domain

object StringConvert {

fun toInt(input: String): Int {
val result = input.toInt()
if (result < 0) {
throw RuntimeException("음수는 입력할 수 없습니다.")
}
return result
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/lotto/LottoApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package lotto

import lotto.application.LottoStatisticsService
import lotto.controller.LottoController

fun main() {
val controller = LottoController(LottoStatisticsService())
controller.start()
}
33 changes: 33 additions & 0 deletions src/main/kotlin/lotto/application/LottoStatisticsService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package lotto.application

import lotto.domain.Lotto
import lotto.domain.LottoPrize
import lotto.domain.LottoStatisticsResult
import lotto.domain.LottoStatisticsTotal
import lotto.domain.LottoWinner
import lotto.domain.Reward

class LottoStatisticsService {

fun statistics(luckNumberList: List<Int>, lottoList: List<Lotto>, inputPayment: Int): LottoStatisticsTotal {
val winLottoList = LottoWinner(luckNumberList, lottoList).findWinLottoList()

val winLottoStatisticsResult = winLottoStatistics(winLottoList)
val lottoPrize = LottoPrize(winLottoList.map { it.prizeMoney })
return LottoStatisticsTotal(
totalRate = lottoPrize.totalRate(inputPayment),
winLottoStatisticsResult = winLottoStatisticsResult
)
}

private fun winLottoStatistics(winLottoList: List<Reward>): List<LottoStatisticsResult> {
val hitCountMap = winLottoList.groupBy { winLottoPrize: Reward -> winLottoPrize.hitCount }

return Reward.values().map {
LottoStatisticsResult(
winLottoPrize = it,
winLottoCount = hitCountMap.getOrDefault(it.hitCount, emptyList()).size
)
}
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/lotto/controller/LottoController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.controller

import lotto.application.LottoStatisticsService
import lotto.domain.LottoGenerator
import lotto.domain.LottoShop
import lotto.domain.RandomNumberGenerator
import lotto.view.InputView
import lotto.view.ResultView

class LottoController(
private val lottoStatisticsService: LottoStatisticsService
) {

private val lottoShop = LottoShop(LottoGenerator(RandomNumberGenerator()))
Comment on lines +10 to +14
Copy link
Member

Choose a reason for hiding this comment

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

LottoController 는 전반적으로 로또 프로그램의 흐름을 관제하는 객체로 보이는데요,
로또 통계 서비스 객체만 별개로 생성자 주입을 시켜주신 이유가 있을까요?
제가 로직을 이해하기엔 로또 통계 서비스가 교체대상이 되진 않을 것 같은데, 다른 의도로 주입하신걸까 궁금해서 질문드립니다!


fun start() {
val inputPayment = InputView.inputPayment()
val lottoList = lottoShop.buy(inputPayment)
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

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

inputPayment 가 음수일수도 있겠네요 😄

ResultView.printLotto(lottoList)

val luckNumbers = InputView.inputLuckyNumbers()
val statistics = lottoStatisticsService.statistics(luckNumbers, lottoList, inputPayment)
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

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

입력된 당첨 번호가 1~45 사이 숫자가 아닐수도 있겠네요 😄

ResultView.printLottoStatistics(statistics)
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/lotto/domain/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package lotto.domain

class Lotto(
val numbers: List<Int>
) {

fun countHitNumbers(luckNumberList: List<Int>): Int {
return numbers.count { number -> luckNumberList.contains(number) }
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/lotto/domain/LottoGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package lotto.domain

class LottoGenerator(
private val numberGenerator: NumberGenerator
) {

fun generate(size: Int): List<Lotto> {
return List(size) { Lotto(numberGenerator.generate()) }
}
Comment on lines +7 to +9
Copy link
Member

Choose a reason for hiding this comment

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

정말정말 깔끔하고 좋으네요 😆
(제 취향이므로 반영 안하셔도 됩니다!) 네이밍을 통해 '1~45 사이의 로또 번호를 발급' 한다는 걸 더 티내기 위해 LottoNumberGenerator 와 같이 조금 더 적나라한 네이밍을 쓰는걸 선호합니다 😄

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

import lotto.util.NumberUtil

class LottoPrize(
prizeList: List<Int>
) {
private val totalPrize: Int = prizeList.sum()

fun totalRate(inputPayment: Int): Double {
val totalRate = totalPrize.toDouble() / inputPayment.toDouble()
return NumberUtil.floor(totalRate, EARING_RATE_DECIMAL_PLACE)
}

companion object {
private const val EARING_RATE_DECIMAL_PLACE = 2
}
Comment on lines +12 to +17
Copy link
Member

Choose a reason for hiding this comment

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

소숫점 둘째자리까지 '표현'한다는 것은 다른 녀석에게 더 어울리는 책임이겠죠? 😄

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

class LottoShop(
private val lottoGenerator: LottoGenerator
) {

fun buy(inputPayment: Int): List<Lotto> {
val lottoCount = calculateLottoCount(inputPayment)
return lottoGenerator.generate(lottoCount)
}

private fun calculateLottoCount(payment: Int): Int {
return payment / LOTTO_PRICE
}

companion object {
private const val LOTTO_PRICE = 1000
}
Comment on lines +3 to +18
Copy link
Member

Choose a reason for hiding this comment

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

책임의 분리가 굉장히 훌륭하네요 😮 👍 👍
덕분에 테스트 코드도 깔끔한거 같아요!!!!

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

data class LottoStatisticsResult(
val winLottoPrize: Reward,
val winLottoCount: Int
)
6 changes: 6 additions & 0 deletions src/main/kotlin/lotto/domain/LottoStatisticsTotal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package lotto.domain

data class LottoStatisticsTotal(
val totalRate: Double,
val winLottoStatisticsResult: List<LottoStatisticsResult>
)
16 changes: 16 additions & 0 deletions src/main/kotlin/lotto/domain/LottoWinner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lotto.domain

class LottoWinner(
val luckNumberList: List<Int>,
val lottoList: List<Lotto>
) {

fun findWinLottoList(): List<Reward> {
return lottoList
.map { it.countHitNumbers(luckNumberList) }
.filter { hasPrize(it) }
.map { Reward.from(it) }
}

private fun hasPrize(count: Int) = count >= Reward.MINIMUM_HIT_COUNT
}
5 changes: 5 additions & 0 deletions src/main/kotlin/lotto/domain/NumberGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package lotto.domain

interface NumberGenerator {
fun generate(): List<Int>
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/domain/RandomNumberGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lotto.domain

class RandomNumberGenerator : NumberGenerator {
override fun generate(): List<Int> {
val range = LOTTO_START_NUMBER..LOTTO_END_NUMBER
val shuffled = range.shuffled()
return shuffled.subList(0, LOTTO_NUMBERS_SIZE).sorted()
}
Comment on lines +4 to +8
Copy link
Member

Choose a reason for hiding this comment

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

1~45 사이 6개 난수 발생로직 깔끔하네요 👍 👍
난수가 잘 발생되는지 테스트 코드 작성이 가능할까요?
(혹시나 테스트 코드 작성에 어려움이 느껴지신다면 코멘트 남겨주세요!)


companion object {
const val LOTTO_START_NUMBER = 1
const val LOTTO_END_NUMBER = 45
const val LOTTO_NUMBERS_SIZE = 6
}
Comment on lines +10 to +14
Copy link
Member

Choose a reason for hiding this comment

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

RandomNumberGenerator 내부에서만 사용되는 상수들이라면 private 으로 가려줄 수 있을거 같아요!

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

enum class Reward(
val hitCount: Int,
val prizeMoney: Int
) {

FOURTH(3, 5_000),
THIRD(4, 50_000),
SECOND(5, 1_5000_000),
FIRST(6, 2_000_000_000);

companion object {
val MINIMUM_HIT_COUNT: Int = values().minOf { it.hitCount }

fun from(hitCount: Int): Reward {
return values().first { it.hitCount == hitCount }
}
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/util/NumberUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.util

import kotlin.math.floor
import kotlin.math.pow

object NumberUtil {

fun floor(number: Double, decimalPlace: Int): Double {
if (decimalPlace == -1) {
return number
}

val pow = 10.0.pow(decimalPlace.toDouble())
val floor = floor(number * pow)
return floor / pow
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/lotto/util/StringValidator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lotto.util

object StringValidator {

fun validateNumber(string: String) {
require(string.isNumeric()) { "숫자가 아닙니다." }
}

private fun String.isNumeric(): Boolean {
return this.toCharArray().all { it in '0'..'9' }
}
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.

wow 😮 👍


fun validateNotBlank(string: String) {
require(!string.isBlank()) { "값이 비어 있습니다." }
Copy link
Member

Choose a reason for hiding this comment

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

isNotBlank() 로도 대체할 수 있겠네요! 👍

}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package lotto.view

import lotto.util.StringValidator

object InputView {

const val INPUT_PAYMENT_GUIDE = "# 구입금액을 입력해주세요."
const val INPUT_LUCKY_NUMBERS_GUIDE = "# 지난 주 당첨 번호를 입력해 주세요."
Comment on lines +7 to +8
Copy link
Member

Choose a reason for hiding this comment

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

역시나 내부에서만 사용된다면 private 으로 😉


fun inputPayment(): Int {
println(INPUT_PAYMENT_GUIDE)
val payment = readln()
validatePaymentInput(payment)
return payment.toInt()
}

private fun validatePaymentInput(payment: String) {
StringValidator.validateNotBlank(payment)
StringValidator.validateNumber(payment)
}
Comment on lines +10 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.

InputView 오브젝트는 가독성도 뛰어나고 유지보수가 편리해보이네요 💯 👍


fun inputLuckyNumbers(): List<Int> {
println(INPUT_LUCKY_NUMBERS_GUIDE)
val luckyNumberString = readln()
val luckyNumbers = splitNumbers(luckyNumberString)
validateLuckyNumbersInput(luckyNumbers)
return convert(luckyNumbers)
}

private fun splitNumbers(input: String) = input.split(",").map { it.trim() }

private fun validateLuckyNumbersInput(split: List<String>) {
split.forEach {
StringValidator.validateNotBlank(it)
StringValidator.validateNumber(it)
}
}

private fun convert(split: List<String>): List<Int> {
return split.map { it.toInt() }
}
}
45 changes: 45 additions & 0 deletions src/main/kotlin/lotto/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package lotto.view

import lotto.domain.Lotto
import lotto.domain.LottoStatisticsResult
import lotto.domain.LottoStatisticsTotal

object ResultView {

private const val STATISTICS_GUIDE = "당첨 통계"
private const val SPLIT_LINE = "---------"

fun printLotto(lottoList: List<Lotto>) {
println("${lottoList.size}개를 구매했습니다.")
lottoList.forEach {
println(it.numbers)
}
println()
}

fun printLottoStatistics(statisticsResult: LottoStatisticsTotal) {
println()
println(STATISTICS_GUIDE)
println(SPLIT_LINE)
printWinStatisticsResult(statisticsResult.winLottoStatisticsResult)
printEarningRate(statisticsResult.totalRate)
}

private fun printWinStatisticsResult(winLottoStatisticsResult: List<LottoStatisticsResult>) {
winLottoStatisticsResult.forEach {
val hitCount = it.winLottoPrize.hitCount
val prizeMoney = it.winLottoPrize.prizeMoney
val winLottoCount = it.winLottoCount
println("${hitCount}개 일치 (${prizeMoney}원) - ${winLottoCount}개")
}
Comment on lines +29 to +34
Copy link
Member

Choose a reason for hiding this comment

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

와 👍 👍 👍 👍 👍 👍

}

private fun printEarningRate(earningRate: Double) {
print("총 수익률은 ${earningRate}입니다. ")
if (earningRate > 1) {
println("(기준이 1이기 때문에 이익입니다.)")
return
}
println("(기준이 1이기 때문에 손해입니다.)")
}
}
25 changes: 25 additions & 0 deletions src/test/kotlin/lotto/application/LottoStatisticsServiceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lotto.application

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import lotto.domain.Lotto

class LottoStatisticsServiceTest : FreeSpec({

val luckyNumber = listOf(1, 2, 3, 4, 5, 6)
val lottoList = listOf(
Lotto(listOf(1, 2, 3, 4, 5, 6)),
Lotto(listOf(10, 11, 12, 13, 14, 15))
)

"statistics" - {

"수익률과 당첨 상금을 반환한다." {
val lottoStatisticsService = LottoStatisticsService()

val statistics = lottoStatisticsService.statistics(luckyNumber, lottoList, 2000)

statistics.totalRate shouldBe 1000000.0
}
}
})
Loading