Skip to content

[step4] 로또 (수동) #1123

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

Open
wants to merge 14 commits into
base: sarahan774
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,32 @@
- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
- Enum 클래스를 적용해 프로그래밍을 구현한다.
- 일급 컬렉션을 쓴다.


### 4단계 로또 수동 기능구현 목록
- 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다.
- 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다.
- 입력값 받는 순서:
- 구매금액을 입력해주세요
- 수동으로 구매할 로또 수를 입력해주세요
- 수동으로 구매할 번호를 입력해주세요
- 수동으로 3장, 자동으로 11장을 구매했습니다
- 지난주 당첨 번호를 입력해주세요
- 보너스볼을 입력해주세요

### 4단계 로또 수동 프로그래밍 요구사항
- 모든 원시값과 문자열을 포장한다.
- 예외 처리를 통해 에러가 발생하지 않도록 한다.

### 4단계 힌트
모든 원시값과 문자열을 포장한다.
로또 숫자 하나는 Int다. 이 숫자 하나를 추상화한 LottoNumber를 추가해 구현한다.
예외 처리를 통해 에러가 발생하지 않도록 한다.


참고로 코틀린에서는 아래와 같이 예외 처리를 한다. 장기적으로는 아래와 같이 예외 처리하는 걸 연습해 본다.

- 논리적인 오류일 때만 예외를 던진다.
- 논리적인 오류가 아니면 예외를 던지지 말고 null을 반환한다.
- 실패하는 경우가 복잡해서 null로 처리할 수 없으면 sealed class를 반환한다.
- 일반적인 코틀린 코드에서 try-catch를 사용하지 않는다. --> 왜 ?
31 changes: 21 additions & 10 deletions src/main/kotlin/lotto/controller/LottoController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package lotto.controller
import lotto.domain.LottoCalculator
import lotto.domain.LottoFactory
import lotto.domain.LottoResult
import lotto.domain.model.Lotto
import lotto.domain.model.LottoNumber
import lotto.domain.Lotto
import lotto.domain.LottoNumber
import lotto.view.InputView
import lotto.view.ResultView
import java.math.BigDecimal
Expand All @@ -23,31 +23,42 @@ object LottoController {
// 총 구매 로또 개수
ResultView.printTotalPurchaseCount(totalLottoCount)

val myLottoList = createMyLottoList(totalLottoCount)
Copy link

Choose a reason for hiding this comment

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

당첨번호라는 의미를 가진 변수인데, myLotto가 괜찮을기 고민 해보면 좋을 것 같아요 :)

Copy link
Author

Choose a reason for hiding this comment

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

좋습니다!

// 나의 로또 번호 리스트 출력
val myLottoList = LottoFactory(totalLottoCount).createLottoList()
ResultView.printLottoNumbers(myLottoList)

// 결과 출력
val result = getLottoResult(myLottoList)
ResultView.printLottoResult(resultMap = result.resultMap)
ResultView.printProfitRate(
profitRate =
LottoCalculator.calculateProfitRate(
result.getTotalProfit(),
totalPurchaseAmount,
),
LottoCalculator.calculateProfitRate(
result.getTotalProfit(),
totalPurchaseAmount,
),
)
}

private fun getLottoResult(myLottoList: List<Lotto>): LottoResult {
val winningNumbers = InputView.readWinningLotto(InputView.ENTER_LAST_WINNING_NUMBER)
val bonusLottoNumber = InputView.readBonusLotto(InputView.ENTER_BONUS_BALL)
val winningNumbers = InputView.readWinningLotto()
val bonusLottoNumber = InputView.readBonusLotto()
val result =
LottoResult(
winningLotto = LottoFactory.fromList(winningNumbers),
bonusLottoNumber = LottoNumber(bonusLottoNumber),
myLottoList = myLottoList,
myLottos = myLottoList,
)
return result
}

private fun createMyLottoList(totalLottoCount: Int): List<Lotto> {
val manualLottoCount = InputView.readManualLottoCount()
val manualLottoNumberList = InputView.readManualLottoList(manualLottoCount)

val myLottoList = LottoFactory(
autoGeneratedLottoCount = totalLottoCount - manualLottoCount,
manualLottoList = manualLottoNumberList
).createLottoList()
return myLottoList
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lotto.domain.model
package lotto.domain

@JvmInline
value class Lotto(val value: List<LottoNumber>) {
Copy link

Choose a reason for hiding this comment

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

지금보니까 LottoTest가 없네요!! 테스트를 작성해보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

그러네요 이상하게 커버리지에는 초록색으로 뜨던 ..;?!
작성하도록 하겠습니다.

Expand Down
15 changes: 11 additions & 4 deletions src/main/kotlin/lotto/domain/LottoFactory.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package lotto.domain

import lotto.domain.model.Lotto
import lotto.domain.model.LottoNumber
import lotto.util.NumberGenerator
import lotto.util.RandomNumberGenerator

class LottoFactory(
private val totalLottoCount: Int,
private val autoGeneratedLottoCount: Int,
private val manualLottoList: List<List<Int>>,
private val numberGenerator: NumberGenerator = RandomNumberGenerator(),
) {
fun createLottoList(): List<Lotto> {
return List(totalLottoCount) {
return buildList {
addAll(manualLottoList.map { fromList(it) })
addAll(autoGeneratedLottoList())
}
}

private fun autoGeneratedLottoList(): List<Lotto> {
check(autoGeneratedLottoCount > 0) { "수동으로 생성하는 로또의 개수는 전체 로또 수를 초과할 수 없습니다." }
return List(autoGeneratedLottoCount) {
numberGenerator
.getNumbers(Lotto.LOTTO_COUNT)
.let { list -> Lotto(list.map { LottoNumber.from(it) }) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lotto.domain.model
package lotto.domain

@JvmInline
value class LottoNumber(val value: Int) {
Expand Down
7 changes: 2 additions & 5 deletions src/main/kotlin/lotto/domain/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package lotto.domain

import lotto.domain.model.Lotto
import lotto.domain.model.LottoNumber
import lotto.domain.model.Rank
import java.math.BigDecimal

class LottoResult(
private val winningLotto: Lotto,
private val bonusLottoNumber: LottoNumber,
myLottoList: List<Lotto>,
myLottos: List<Lotto>,
) {
val resultMap: Map<Rank, Int>

init {
resultMap = Rank.entries.associateWith { 0 }.toMutableMap()
myLottoList.forEach { lotto ->
myLottos.forEach { lotto ->
getRank(lotto)?.let { rank ->
var rankCount = resultMap.getOrDefault(rank, 0)
resultMap[rank] = ++rankCount
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lotto.domain.model
package lotto.domain

import java.math.BigDecimal

Expand All @@ -14,14 +14,13 @@ enum class Rank(val prizeMoney: BigDecimal, val matchingNumberCount: Int) {
fun fromMatchCount(
matchCount: Int,
isMatchBonus: Boolean,
): Rank {
): Rank? {
return if (isMatchBonus && matchCount == 5) {
SECOND
} else if (matchCount == 5) {
THIRD
} else {
entries.find { matchCount == it.matchingNumberCount }
?: throw IllegalArgumentException("invalid count $matchCount")
}
}

Expand Down
45 changes: 36 additions & 9 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,50 @@ import java.math.BigDecimal

object InputView {
fun readTotalPurchaseAmountAsBigDecimal(): BigDecimal {
return readlnOrNull()?.trim()?.toBigDecimalOrNull() ?: BigDecimal.ZERO
return readlnOrNull()?.trim()?.toBigDecimal() ?: throw IllegalStateException("유효한 원화 형식이 아닙니다")
}

fun readWinningLotto(promptMessage: String): List<Int> {
println(promptMessage)
val winningLotto = readlnOrNull()?.split(",")?.map { it.trim().toIntOrNull() ?: 0 }
fun readWinningLotto(): List<Int> {
println(ENTER_LAST_WINNING_NUMBER)
val winningLotto = readlnOrNull()?.split(",")?.map { it.trim().toInt() }
require(winningLotto?.size == LOTTO_SIZE) { "Lotto size must be 6" }
return winningLotto ?: throw IllegalStateException("Separate Lotto numbers by comma (,)")
}

fun readBonusLotto(promptMessage: String): Int {
println(promptMessage)
val bonusLottoNumber = readlnOrNull()?.trim()?.toIntOrNull() ?: 0 // single number
fun readBonusLotto(): Int {
println(ENTER_BONUS_BALL)
val bonusLottoNumber = readlnOrNull()?.trim()?.toInt() ?: throw IllegalStateException("정수를 입력해 주세요")
return bonusLottoNumber
}

const val ENTER_LAST_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요."
const val ENTER_BONUS_BALL = "보너스 볼을 입력해 주세요."
fun readManualLottoCount(): Int {
println(ENTER_MANUAL_LOTTO_COUNT)
val manualLottoCount = readlnOrNull()?.trim()?.toInt() ?: throw IllegalStateException("정수를 입력해 주세요")
return manualLottoCount
}

fun readManualLottoList(count: Int): List<List<Int>> {
val manualLottoNumbers = mutableListOf<List<Int>>()
println(ENTER_MANUAL_LOTTO)
repeat(count) {
manualLottoNumbers.add(readManualLotto())
}
return manualLottoNumbers
}

private fun readManualLotto(): List<Int> {
val input =
readlnOrNull()?.trim()
?.split(INPUT_SEPARATOR_COMMA)
?.map { it.trim().toInt() }
?: throw IllegalStateException("Separate Lotto numbers by comma (,)")
return input
}

private const val ENTER_LAST_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요."
private const val ENTER_BONUS_BALL = "보너스 볼을 입력해 주세요."
private const val ENTER_MANUAL_LOTTO_COUNT = "수동으로 구매할 로또 수를 입력해 주세요."
private const val ENTER_MANUAL_LOTTO = "수동으로 구매할 번호를 입력해 주세요. (예: 8, 21, 23, 41, 42, 43)"
private const val LOTTO_SIZE = 6
private const val INPUT_SEPARATOR_COMMA = ","
}
4 changes: 2 additions & 2 deletions src/main/kotlin/lotto/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package lotto.view

import lotto.domain.model.Lotto
import lotto.domain.model.Rank
import lotto.domain.Lotto
import lotto.domain.Rank
import java.math.BigDecimal

object ResultView {
Expand Down
11 changes: 10 additions & 1 deletion src/test/kotlin/lotto/domain/LottoCalculatorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,21 @@ class LottoCalculatorTest {
}

@Test
fun `{given} 총 구매 금액이 0원일 경우 {when} calculateTotalProfit() {then} IllegalArgumentException 발생`() {
fun `{given} 총 구매 금액이 0원일 경우 {when} calculateProfitRate() {then} IllegalArgumentException 발생`() {
assertThrows<IllegalArgumentException> {
LottoCalculator.calculateProfitRate(
totalProfit = BigDecimal(10000),
totalPurchaseAmount = BigDecimal.ZERO,
)
}
}

@Test
fun `{given} 총 수익이 0인 경우 {when} calculateProfitRate() {then} BigDecimal ZERO 반환`() {
val profitRate = LottoCalculator.calculateProfitRate(
totalProfit = BigDecimal.ZERO,
totalPurchaseAmount = BigDecimal(10000),
)
assertEquals(BigDecimal.ZERO, profitRate)
}
}
10 changes: 6 additions & 4 deletions src/test/kotlin/lotto/domain/LottoFactoryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package lotto.domain
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import lotto.domain.model.LottoNumber
import lotto.util.NumberGenerator
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertThrows

class LottoFactoryTest : StringSpec({
Expand All @@ -15,7 +15,8 @@ class LottoFactoryTest : StringSpec({
val fakeNumberGenerator = FakeRandomNumberGenerator(expectedLottoNumbers)
val lottoFactory =
LottoFactory(
totalLottoCount = totalLottoCount,
autoGeneratedLottoCount = totalLottoCount,
manualLottoList = emptyList(),
numberGenerator = fakeNumberGenerator,
)

Expand All @@ -35,7 +36,8 @@ class LottoFactoryTest : StringSpec({
val fakeNumberGenerator = FakeRandomNumberGenerator(expectedLottoNumbers)
val lottoFactory =
LottoFactory(
totalLottoCount = 5,
autoGeneratedLottoCount = 5,
manualLottoList = emptyList(),
numberGenerator = fakeNumberGenerator,
)

Expand All @@ -49,7 +51,7 @@ class LottoFactoryTest : StringSpec({
"{given} 정수 리스트, size=6, {when} LottoFactory.fromList() {then} Lotto 생성" {
val list = listOf(1, 2, 3, 4, 5, 6)
val result = LottoFactory.fromList(list)
// assertTrue(result is Lotto) FIXME : this check is useless ?
assertThat(result.value).containsExactlyElementsOf(list.map { LottoNumber(it) })
}

"{given} 정수 리스트, size=10, {when} LottoFactory.fromList() {then} IllegalArgumentException 발생" {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lotto.domain.model
package lotto.domain

import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
Expand Down
20 changes: 16 additions & 4 deletions src/test/kotlin/lotto/domain/LottoResultTest.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package lotto.domain

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.collections.shouldContainOnly
import io.kotest.matchers.shouldBe
import lotto.domain.model.Lotto
import lotto.domain.model.LottoNumber
import lotto.domain.model.Rank

class LottoResultTest : StringSpec({
"로또 등수와 결과 카운트 검증" {
Expand All @@ -22,6 +20,20 @@ class LottoResultTest : StringSpec({
results[Rank.FIFTH] shouldBe 1
}

"일치하는 로또 번호가 없다면 resultMap 의 값들은 0 이다" {
// Given
val lottoResultNoMatch = LottoResult(
winningLotto = Lotto(listOf(1, 2, 3, 4, 5, 6).map { LottoNumber(it) }),
bonusLottoNumber = LottoNumber(7),
myLottos = listOf(
Lotto(listOf(8, 9, 10, 11, 12, 13).map { LottoNumber(it) })
)
)
//when & then
val results = lottoResultNoMatch.resultMap
results.values.shouldContainOnly(0)
}

"getTotalProfit() 결과 검증" {
// given
val lottoResult = fakeLottoResult()
Expand Down Expand Up @@ -54,7 +66,7 @@ private fun fakeLottoResult(): LottoResult {
LottoResult(
winningLotto = winningLotto,
bonusLottoNumber = LottoNumber(800),
myLottoList = myLottoList,
myLottos = myLottoList,
)
return lottoResult
}
Loading