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

[로또] 신민준 미션 제출합니다. #95

Open
wants to merge 11 commits into
base: main
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
169 changes: 169 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,170 @@
# kotlin-lotto-precourse

## 구현할 기능

- [X] Lotto Class
- [X] 값을 반환하는 getLottoValue()
- [X] 결과값을 갖는 enum class LottoResult
- [X] LottoController
- [X] 랜덤으로 6개의 숫자를 만들고 이를 Lotto에 저장하는 releaseLotto()
- [X] 값을 입력받고 정상적인 입력인지 확인후 저장
- [X] 입력받은 가격에 따른 로또 구매
- [X] 로또 결과를 확인
- [X] 최종 결과값 반환
- [X] 입력과 결과를 다루는 LottoView

## 구현 과정

### releaseLotto()

Lotto 목록을 저장하는 ArrayList lottos에 getLottoNumber()를 통해 랜덤 로또 번호를 반환하여 만든 Lotto를 add한다.

```
private fun releaseLotto() {
lottos.add(Lotto(getLottoNumber()))
}

private fun getLottoNumber(): List<Int> {
return Randoms.pickUniqueNumbersInRange(1, 45, 6)
}
```

### 로또 구매

```
fun getBuyAmount(): Int {
println(Constant.INPUT_BUY_MESSAGE)
return Console.readLine().toInt()
}

fun showBoughtLotto(lottoAmount: Int, lottos: ArrayList<Lotto>) {
println("\n$lottoAmount" + Constant.BOUGHT_LOTTO_MESSAGE)
println(lottos.joinToString("\n") { "[" + it.getLottoValue().joinToString(", ") + "]" })
println()
}
```
LottoView를 통해 값을 입력받는다.
이때 직접적으로 입출력값을 하드코딩하지 말라했던 2주차 피드백을 참고하여 출력메시지를 Constant 오브젝트에 모두 const val로 별도로 저장해주었다.


```
fun startLotto() {
val lottoView = LottoView()
val buyLottoAmount = divideLottoPrice(lottoView.getBuyAmount())
releaseLotto(buyLottoAmount)
lottoView.showBoughtLotto(buyLottoAmount, lottos)
}

private fun divideLottoPrice(inputBuyAmount: Int): Int {
return inputBuyAmount / Constant.LOTTO_PRICE
}

private fun releaseLotto(buyLottoAmount: Int) {
repeat(buyLottoAmount) {
lottos.add(Lotto(getLottoNumber()))
}
}
```
입력받은 값을 LOTTO_PRICE로 나누고 이를 통해 도출된 구매 갯수에 따라 releaseLotto()를 통해 Lotto를 발매한다.

### 로또 결과 정산
```
private fun calculateLotto(winnerNumber: List<Int>, specialNumber: Int): List<Int> {
val lottoResult = MutableList(5) { 0 }
lottos.forEach {
when (it.getLottoValue().toSet().minus(winnerNumber.toSet()).size) {
3 -> lottoResult[0]++
2 -> lottoResult[1]++
1 -> {
if (it.getLottoValue().contains(specialNumber)) lottoResult[3]++
else lottoResult[2]++
}

0 -> lottoResult[4]++
}
}
return lottoResult
}
```
입력받은 로또 당첨 번호와 발매된 로또 번호의 공통 개수 비교 및 보너스 볼 넘버를 비교하고 그 갯수를 누적한다.

```
private fun getReturnRate(lottoResult: List<Int>, buyLottoAmount: Int) : Double{
var sum = 0.0
sum += Constant.THREE_REWARD * lottoResult[0]
sum += Constant.FOUR_REWARD * lottoResult[1]
sum += Constant.FIVE_REWARD * lottoResult[2]
sum += Constant.FIVE_SPECIAL_REWARD * lottoResult[3]
sum += Constant.SIX_REWARD * lottoResult[4]
return sum/(buyLottoAmount*Constant.LOTTO_PRICE)
}
```
calculateLotto로 얻어낸 lottoResult를 통해 총 수익률을 구하고 이 값들을 LottoView로 전달해주어
```
fun showLottoResult(lottoResult : List<Int>){
println(Constant.RESULT_MESSAGE)
println(Constant.RESULT_THREE_MATCH.format(lottoResult[0]))
println(Constant.RESULT_FOUR_MATCH.format(lottoResult[1]))
println(Constant.RESULT_FIVE_MATCH.format(lottoResult[2]))
println(Constant.RESULT_FIVE_SPECIAL_MATCH.format(lottoResult[3]))
println(Constant.RESULT_SIX_MATCH.format(lottoResult[4]))
}

fun showReturnRate(returnRate : Double){
println("총 수익률은 ${String.format("%.1f", returnRate)}%입니다.")
}

```

이것을 사용자에게 출력한다.

## enum Class 추가
```
enum class LottoResult(val prize: Int) {
THREE(5000),
FOUR(50000),
FIVE(1500000),
FIVE_SPECIAL(30000000),
SIX(2000000000)
}
```

## 예외처리
각 입력에 대해서는 유효하지 않을 경우 해당 입력을 다시 받아야하므로 각 입력에 대해서
```
fun getSpecialNumber(winnerNumber: List<Int>): Int {
while (true) {
try {
println(Constant.INPUT_SPECIAL_NUMBER_MESSAGE)
val specialNumber = Console.readLine()
if (checkSpecialNumber(specialNumber, winnerNumber)) return specialNumber.toInt()
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

private fun checkSpecialNumber(specialNumber: String, winnerNumber: List<Int>): Boolean {
try {
if (specialNumber.toInt() in 1..45) {
if (!winnerNumber.contains(specialNumber.toInt())) return true
else throw Exception()
} else throw Exception()
} catch (e: Exception) {
throw IllegalArgumentException(Constant.ERROR_SPECIAL_NUMBER_INVALID_MESSAGE)
}
}
```

와 같은 연산을 통해 유효성 검사를 수행한다.

## 테스트 추가
```
@Test
fun `로또 번호가 1에서 45 사이의 숫자가 아니라면 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
Lotto(listOf(1, 2, 3, 4, 5, 46))
}
}
```
로또 번혹가 1~45 사이인지 확인하는 테스트를 추가해주었다.
3 changes: 2 additions & 1 deletion src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package lotto

fun main() {
// TODO: 프로그램 구현
val lottoController = LottoController()
lottoController.startLotto()
}
19 changes: 19 additions & 0 deletions src/main/kotlin/lotto/Constant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lotto

object Constant {
const val INPUT_BUY_MESSAGE = "구입금액을 입력해 주세요."
const val LOTTO_PRICE = 1000

Choose a reason for hiding this comment

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

로또 가격까지 상수화 하셨군요! 전 이 부분은 생각 못했네요👍

const val BOUGHT_LOTTO_MESSAGE = "개를 구매했습니다."
const val INPUT_WINNER_NUMBER = "당첨 번호를 입력해 주세요."
const val INPUT_SPECIAL_NUMBER_MESSAGE = "\n보너스 번호를 입력해 주세요."
const val RESULT_MESSAGE = "당첨 통계\n---"
const val RESULT_THREE_MATCH = "3개 일치 (5,000원) - %d개"
const val RESULT_FOUR_MATCH = "4개 일치 (50,000원) - %d개"
const val RESULT_FIVE_MATCH = "5개 일치 (1,500,000원) - %d개"
const val RESULT_FIVE_SPECIAL_MATCH = "5개 일치, 보너스 볼 일치 (30,000,000원) - %d개"
const val RESULT_SIX_MATCH = "6개 일치 (2,000,000,000원) - %d개"

const val ERROR_SPECIAL_NUMBER_INVALID_MESSAGE = "[ERROR] 보너스 번호는 당첨 번호에 속하지 않은 1부터 45 사이의 정수여야 합니다."
const val ERROR_BUY_AMOUNT_INVALID_MESSAGE = "[ERROR] 구매금액은 1000의 배수인 정수여야 합니다."
const val ERROR_WINNER_NUMBER_INVALID_MESSAGE = "[ERROR] 당첨 번호는 서로 다른 6개의 1부터 45사이의 정수여야 합니다."
}
6 changes: 5 additions & 1 deletion src/main/kotlin/lotto/Lotto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package lotto
class Lotto(private val numbers: List<Int>) {
init {
require(numbers.size == 6) { "[ERROR] 로또 번호는 6개여야 합니다." }
require(numbers.all { it in 1..45 }) { "[ERROR] 로또 번호는 1에서 45 사이여야 합니다." }
require(numbers.toSet().size == numbers.size) { "[ERROR] 로또 번호는 중복될 수 없습니다."}
}

// TODO: 추가 기능 구현
fun getLottoValue(): List<Int> {
return numbers
}
}
61 changes: 61 additions & 0 deletions src/main/kotlin/lotto/LottoController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package lotto

import camp.nextstep.edu.missionutils.Randoms

class LottoController {

val lottos = ArrayList<Lotto>()

fun startLotto() {
val lottoView = LottoView()
val buyLottoAmount = divideLottoPrice(lottoView.getBuyAmount())
releaseLotto(buyLottoAmount)
lottoView.showBoughtLotto(buyLottoAmount, lottos)
val winnerNumber = lottoView.getWinnerNumber()
val specialNumber = lottoView.getSpecialNumber(winnerNumber)
val lottoResult = calculateLotto(winnerNumber, specialNumber)
lottoView.showLottoResult(lottoResult)
lottoView.showReturnRate(getReturnRate(lottoResult, buyLottoAmount))
}

private fun getReturnRate(lottoResult: List<Int>, buyLottoAmount: Int): Double {
var sum = 0.0
sum += LottoResult.THREE.prize * lottoResult[0]
sum += LottoResult.FOUR.prize * lottoResult[1]
sum += LottoResult.FIVE.prize * lottoResult[2]
sum += LottoResult.FIVE_SPECIAL.prize * lottoResult[3]
sum += LottoResult.SIX.prize * lottoResult[4]
return sum / (buyLottoAmount * Constant.LOTTO_PRICE) * 100
}

private fun calculateLotto(winnerNumber: List<Int>, specialNumber: Int): List<Int> {
val lottoResult = MutableList(5) { 0 }
lottos.forEach {
when (it.getLottoValue().toSet().minus(winnerNumber.toSet()).size) {
3 -> lottoResult[0]++
2 -> lottoResult[1]++
1 -> {
if (it.getLottoValue().contains(specialNumber)) lottoResult[3]++
else lottoResult[2]++
}

0 -> lottoResult[4]++
}
}
return lottoResult
}

private fun divideLottoPrice(inputBuyAmount: Int): Int {
return inputBuyAmount / Constant.LOTTO_PRICE
}

private fun releaseLotto(buyLottoAmount: Int) {
repeat(buyLottoAmount) {
lottos.add(Lotto(getLottoNumber()))
}
}

private fun getLottoNumber(): List<Int> {
return Randoms.pickUniqueNumbersInRange(1, 45, 6).sorted()
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/lotto/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package lotto

enum class LottoResult(val prize: Int) {
THREE(5000),
FOUR(50000),
FIVE(1500000),
FIVE_SPECIAL(30000000),
SIX(2000000000)
}
92 changes: 92 additions & 0 deletions src/main/kotlin/lotto/LottoView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package lotto

import camp.nextstep.edu.missionutils.Console

class LottoView {

fun getBuyAmount(): Int {
println(Constant.INPUT_BUY_MESSAGE)
while (true) {
try {
val buyAmount = Console.readLine()
if (checkBuyAmount(buyAmount)) return buyAmount.toInt()
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

private fun checkBuyAmount(buyAmount: String): Boolean {

Choose a reason for hiding this comment

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

뷰 안에서 너무 많은 역할을 담당할 수 있으니, 이렇게 체킹하는 함수는 따로 validator 같은 파일로 분리하는 게 관리하기 좋을 것 같아요!🙂🙂

Copy link
Author

Choose a reason for hiding this comment

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

아하! 확실히 분리하는 게 용도도 명확해보여서 더 좋은 것 같습니다! 감사합니다!

try {
if (buyAmount.toInt() % 1000 == 0) return true
else throw Exception()
} catch (e: Exception) {
throw IllegalArgumentException(Constant.ERROR_BUY_AMOUNT_INVALID_MESSAGE)
}
}

fun showBoughtLotto(lottoAmount: Int, lottos: ArrayList<Lotto>) {
println("\n$lottoAmount" + Constant.BOUGHT_LOTTO_MESSAGE)
println(lottos.joinToString("\n") { "[" + it.getLottoValue().joinToString(", ") + "]" })
println()
}

fun getWinnerNumber(): List<Int> {
while (true) {
try {
println(Constant.INPUT_WINNER_NUMBER)
val winnerNumber = Console.readLine()
if (checkWinnerNumber(winnerNumber)) return winnerNumber.split(",").map { it.toInt() }
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

private fun checkWinnerNumber(winnerNumber: String): Boolean {
try {
if (winnerNumber.split(",").map { if (it.toInt() in 1..45) it.toInt() else throw Exception() }
.toSet().size == 6
) return true
else throw Exception()
} catch (e: Exception) {
throw IllegalArgumentException(Constant.ERROR_WINNER_NUMBER_INVALID_MESSAGE)
}
}

fun getSpecialNumber(winnerNumber: List<Int>): Int {
while (true) {
try {
println(Constant.INPUT_SPECIAL_NUMBER_MESSAGE)
val specialNumber = Console.readLine()
if (checkSpecialNumber(specialNumber, winnerNumber)) return specialNumber.toInt()
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
}

private fun checkSpecialNumber(specialNumber: String, winnerNumber: List<Int>): Boolean {
try {
if (specialNumber.toInt() in 1..45) {
if (!winnerNumber.contains(specialNumber.toInt())) return true
else throw Exception()
} else throw Exception()
} catch (e: Exception) {
throw IllegalArgumentException(Constant.ERROR_SPECIAL_NUMBER_INVALID_MESSAGE)
}
}

fun showLottoResult(lottoResult: List<Int>) {
println(Constant.RESULT_MESSAGE)
println(Constant.RESULT_THREE_MATCH.format(lottoResult[0]))
println(Constant.RESULT_FOUR_MATCH.format(lottoResult[1]))
println(Constant.RESULT_FIVE_MATCH.format(lottoResult[2]))
println(Constant.RESULT_FIVE_SPECIAL_MATCH.format(lottoResult[3]))
println(Constant.RESULT_SIX_MATCH.format(lottoResult[4]))
}

fun showReturnRate(returnRate: Double) {
println("총 수익률은 ${String.format("%.1f", returnRate)}%입니다.")
}
}
8 changes: 8 additions & 0 deletions src/test/kotlin/lotto/LottoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,13 @@ class LottoTest {
}
}

@Test
fun `로또 번호가 1에서 45 사이의 숫자가 아니라면 예외가 발생한다`() {
assertThrows<IllegalArgumentException> {
Lotto(listOf(1, 2, 3, 4, 5, 46))
}
}


// TODO: 추가 기능 구현에 따른 테스트 코드 작성

Choose a reason for hiding this comment

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

컨트롤러의 함수나 유효성 검사 기능을 담당하는 코드에 대한 추가 테스트 코드가 있으면 좋을 거 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

제가 테스트 코드 작성이 조금 미숙해서... 이번 미션에서 한번 추가해보겠습니다!

}