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

[로또] 장은영 미션 제출합니다. #86

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d246213
feat: 구입금액을 입력받는 기능
devfeijoa Nov 3, 2024
a4cfd44
feat: 로또 번호 예외 처리
devfeijoa Nov 4, 2024
9704a1d
feat: 로또 번호 예외 처리
devfeijoa Nov 4, 2024
3b475c9
Style: 예외 경우만 분리
devfeijoa Nov 4, 2024
2dc0c80
feat: 로또구입금액, 당첨 번호, 보너스 번호를 입력받는 기능
devfeijoa Nov 4, 2024
65cfd60
feat: 로또 구입 금액 관련 기능
devfeijoa Nov 4, 2024
aa56782
feat: 로또를 발행하는 기능
devfeijoa Nov 4, 2024
8b430ad
feat: Lotto 클래스 기본 기능 추가
devfeijoa Nov 4, 2024
01665d0
feat: 각 로또 결과에 따른 내용
devfeijoa Nov 4, 2024
fcc079c
feat: 로또 결과에 따른 수익률 계산
devfeijoa Nov 4, 2024
34dd13a
feat: Store 클래스 추상화
devfeijoa Nov 4, 2024
590dff4
feat: 로또 당첨 번호 추첨
devfeijoa Nov 4, 2024
0df59a9
feat: 로또 결과 계산과 순위 확인
devfeijoa Nov 4, 2024
0285235
feat: 로또 로직 실행
devfeijoa Nov 4, 2024
7fc5424
fix: 에러 로직 수정
devfeijoa Nov 4, 2024
e0c22be
fix: 중복된 숫자를 확인하는 로직 수정
devfeijoa Nov 4, 2024
ccceea6
Chore: 기능 명세서 수정
devfeijoa Nov 4, 2024
479afa5
Chore: 기능 명세서 수정
devfeijoa Nov 4, 2024
2dfdc4d
refactor: 기본 생성자를 호출하여 유효성 검증을 위임
devfeijoa Nov 4, 2024
46e71c3
refactor: 매직넘버 상수화
devfeijoa Nov 4, 2024
5b4a7c2
refactor: 초기화 로직, 등수 카운트 증가 및 상금 계산 로직, 수익률 계산 로직을 각각 분리
devfeijoa Nov 4, 2024
ceac4ea
feat: 테스트 코드 작성
devfeijoa Nov 4, 2024
5de360d
feat: 테스트 코드 작성
devfeijoa Nov 4, 2024
a8f8497
feat: 테스트 코드 작성
devfeijoa Nov 4, 2024
45d39a1
chore: 에러 메세지 수정
devfeijoa Nov 4, 2024
a363e6b
chore: 매직넘버 상수화
devfeijoa Nov 4, 2024
715c724
refactor: 코드 포맷에 맞게 수정
devfeijoa Nov 4, 2024
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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
# kotlin-lotto-precourse

## 기능 목록
간단한 로또 발매기를 구현한다.

### 당첨 기준
당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
1등: 6개 번호 일치 / 2,000,000,000원
2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
3등: 5개 번호 일치 / 1,500,000원
4등: 4개 번호 일치 / 50,000원
5등: 3개 번호 일치 / 5,000원


### 입력
- 당첨 번호와 보너스 번호를 입력받음
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑음
- 로또 번호의 숫자 범위는 1~45
- 당첨 번호를 쉼표(,)를 기준으로 구분
- 로또 1장의 가격은 1,000원
- 구입 금액은 1000원 단위로 입력 받으며 1000원으로 나누어 떨어지지 않는 경우 예외처리
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 함
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑음

### 출력
- 발행한 로또 수량 및 번호 출력
- 로또 번호는 오름차순 정렬
- 당첨 내역 출력
- 수익률은 소수점 둘째 자리에서 반올림
- 예외 상황 시 에러 문구를 출력
ex) 단, 에러 문구는 "[ERROR]"로 시작해야 함
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받음
- Exception이 아닌 IllegalArgumentException, IllegalStateException 등과 같은 명확한 유형을 처리
- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료

#### 예외
- 로또 번호가 숫자가 아닌 경우
- 로또 번호가 1~45 사이의 범위를 벗어난 경우
- 로또 리스트 갯수가 맞지 않는 경우
- 로또 번호의 숫자가 중복되는 경우
- 로또 구입 금액이 1000원으로 나누어 떨어지지 않는 경우
7 changes: 7 additions & 0 deletions src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package lotto

import lotto.controller.LottoController
import lotto.view.LottoView

fun main() {
// TODO: 프로그램 구현
val lottoView = LottoView()
val lottoController = LottoController(lottoView)

lottoController.run()
}
9 changes: 0 additions & 9 deletions src/main/kotlin/lotto/Lotto.kt

This file was deleted.

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

import lotto.model.*
import lotto.view.LottoView
import lotto.util.*

class LottoController(private val lottoView: LottoView) {
fun run() {
val lottoPurchaseAmount = payMoney()
val lottos = purchaseLottos(lottoPurchaseAmount.money)
val lottoResult = processLottoWinningNumbers()
val bonus = processLottoBonusNumber(lottoResult)
val results = checkResults(lottos, lottoResult, bonus)
calculateRanks(results)
}

private fun payMoney(): LottoPurchaseAmount {
return try {
lottoView.printLottoPurchaseRequest()
LottoPurchaseAmount.from(lottoView.inputLottoPurchaseAmount())
} catch (e: IllegalArgumentException) {
lottoView.printError(e.message)
return payMoney()
}
}

private fun purchaseLottos(money: Int): List<Lotto> {
val lottoCount = money / LottoStore.LOTTO_TICKET_PRICE
val lottos = mutableListOf<Lotto>()
lottoView.printLottoCount(lottoCount)
repeat(lottoCount) { lottos.add(Lotto.fromList(LottoStore().sell())) }
lottoView.printLottos(lottos)
return lottos
}

private fun processLottoWinningNumbers(): Lotto {
return try {
lottoView.printWinningNumberRequest()
Lotto.fromInput(lottoView.inputWinningNumber())
} catch (e: IllegalArgumentException) {
lottoView.printError(e.message)
return processLottoWinningNumbers()
}
}

private fun processLottoBonusNumber(lotto: Lotto): Int {
return try {
lottoView.printBonusNumberRequest()
val bonus = lottoView.inputBonusNumber()
.validateInt()
.validateRange(LottoStore.LOTTO_MIN_NUMBER, LottoStore.LOTTO_MAX_NUMBER)

if (lotto.isContain(bonus)) throw IllegalArgumentException("[ERROR] 중복되지 않는 숫자로 입력해 주세요.")
bonus
} catch (e: IllegalArgumentException) {
lottoView.printError(e.message)
return processLottoBonusNumber(lotto)
}
}

private fun checkResults(myLotto: List<Lotto>, answer: Lotto, bonus: Int): List<LottoResult> {
val ranks = mutableListOf<LottoResult>()
for (lotto in myLotto) {
val count = lotto.toList().intersect(answer.toList().toSet()).size
val isBonus = lotto.isContain(bonus)
ranks.add(LottoResult.of(count, isBonus))
}
return ranks
}

private fun calculateRanks(lottoResults: List<LottoResult>) {
lottoView.printLottoRankHeader()
val ranks = LottoRanking.of(lottoResults)

for ((rank, count) in ranks.countByRanking) {
if (rank == LottoResult.MISS) continue
lottoView.printLottoRank(rank, count)
}
lottoView.printRevenue(ranks.totalRevenue)
}
}

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

import lotto.util.*

class Lotto(private val numbers: List<Int>) {
init {
require(numbers.size == 6) { "[ERROR] 로또 번호는 6개여야 합니다." }
}

init {
numbers
.findDuplicates()
.validateRange(LottoStore.LOTTO_MIN_NUMBER, LottoStore.LOTTO_MAX_NUMBER);
}

// TODO: 추가 기능 구현
override fun toString(): String {
return numbers.joinToString(prefix = PREFIX, postfix = POSTFIX, separator = SEPARATOR) { it.toString() }
}

fun toList(): List<Int> {
return numbers
}

fun isContain(number: Int): Boolean {
return numbers.contains(number)
}

companion object {
private const val PREFIX = "["
private const val POSTFIX = "]"
private const val SEPARATOR = ", "

fun fromInput(inputNumbers: String): Lotto {
return fromList(inputNumbers.validateIntList())
}

fun fromList(numbers: List<Int>): Lotto {
return Lotto(numbers)
}
}
}

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

import lotto.util.validateInt
import lotto.util.validatePositive
import lotto.util.validateDivisibleBy

class LottoPurchaseAmount private constructor(val money: Int) {
companion object {
fun from(inputLottoPurchaseAmount: String): LottoPurchaseAmount {
return LottoPurchaseAmount(
inputLottoPurchaseAmount
.validateInt()
.validatePositive()
.validateDivisibleBy(LottoStore.LOTTO_TICKET_PRICE)
)
}
}
}
43 changes: 43 additions & 0 deletions src/main/kotlin/lotto/model/LottoRanking.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package lotto.model

import lotto.model.LottoStore
import lotto.util.round

class LottoRanking(var countByRanking: MutableMap<LottoResult, Int>, var totalRevenue: Double) {
companion object {
fun of(lottoResults: List<LottoResult>): LottoRanking {
val lottoResultBundle = initializeResultBundle()
val prize = calculateTotalPrize(lottoResults, lottoResultBundle)
val totalRevenue = calculateROI(lottoResults.size, prize)
return LottoRanking(lottoResultBundle, totalRevenue)
}

private fun initializeResultBundle(): MutableMap<LottoResult, Int> {
val lottoResultBundle = mutableMapOf<LottoResult, Int>()
LottoResult.entries.forEach { result -> lottoResultBundle[result] = 0 }
return lottoResultBundle
}

private fun calculateTotalPrize(
lottoResults: List<LottoResult>, lottoResultBundle: MutableMap<LottoResult, Int>
): Double {
var prize = 0.0
for (result in lottoResults) {
lottoResultBundle[result] = (lottoResultBundle[result] ?: 0) + 1
prize += result.prize
}
return prize
}

private fun calculateROI(lottoCount: Int, profit: Double): Double {
val initialInvestment = lottoCount * LottoStore.LOTTO_TICKET_PRICE
val roi = (profit / initialInvestment) * PERCENT_FACTOR
return roi.round(PRECISION)
}

private const val PERCENT_FACTOR = 100
private const val PRECISION = 2
}
}


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

enum class LottoResult(val prize: Int, private val message: String) {
MISS(0, "꽝"),
THREE_MATCH(5000, "3개 일치 (5,000원)"),
FOUR_MATCH(50000, "4개 일치 (50,000원)"),
FIVE_MATCH(1500000, "5개 일치 (1,500,000원)"),
FIVE_MATCH_WITH_BONUS(30000000, "5개 일치, 보너스 볼 일치 (30,000,000원)"),
SIX_MATCH(200000000, "6개 일치 (2,000,000,000원)");

fun getMessage(count: Int): String {
return "$message - ${count}개"
}

companion object {
fun of(countOfMatch: Int, isBonusMatch: Boolean): LottoResult {
return when {
countOfMatch == 6 -> SIX_MATCH
countOfMatch == 5 && isBonusMatch -> FIVE_MATCH_WITH_BONUS
countOfMatch == 5 -> FIVE_MATCH
countOfMatch == 4 -> FOUR_MATCH
countOfMatch == 3 -> THREE_MATCH
else -> MISS
}
}
}
}

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

import camp.nextstep.edu.missionutils.Randoms

class LottoStore : Store {
override fun sell(): List<Int> {
return Randoms.pickUniqueNumbersInRange(LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_NUMBER_COUNT)
}

companion object {
const val LOTTO_NUMBER_COUNT = 6
const val LOTTO_MIN_NUMBER = 1
const val LOTTO_MAX_NUMBER = 45
const val LOTTO_TICKET_PRICE = 1000
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/lotto/model/Store.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package lotto.model

interface Store {
fun sell(): List<Int>
}
74 changes: 74 additions & 0 deletions src/main/kotlin/lotto/util/Validation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package lotto.util

import kotlin.math.pow
import kotlin.math.roundToInt

enum class LottoConstants(val value: Int) {
LOTTO_NUMBER_COUNT(6),
MIN_LOTTO_NUMBER(1),
MAX_LOTTO_NUMBER(45),
DIVISOR(1000),
PRECISION(2)
}

object ErrorMessages {
const val INVALID_NUMBER = "[ERROR] 숫자를 입력하세요."
const val NON_POSITIVE = "[ERROR] 양수 숫자를 입력하세요."
const val INVALID_DIVISOR = "[ERROR] %d원 단위로 입력해 주세요."
const val INVALID_INPUT = "[ERROR] 숫자만 입력해 주세요."
const val DUPLICATE_NUMBERS = "[ERROR] 중복되지 않는 숫자로 입력해 주세요."
const val INVALID_COUNT = "[ERROR] 갯수에 맞게 입력해 주세요."
const val OUT_OF_RANGE = "[ERROR] %d~%d 내의 숫자를 입력해 주세요."
}

fun String.validateInt(): Int {
return this.toIntOrNull() ?: throw IllegalArgumentException(ErrorMessages.INVALID_NUMBER)
}

fun Int.validatePositive(): Int {
require(this > 0) { ErrorMessages.NON_POSITIVE }
return this
}

fun Int.validateDivisibleBy(divisor: Int = LottoConstants.DIVISOR.value): Int {
require(this % divisor == 0) { ErrorMessages.INVALID_DIVISOR.format(divisor) }
return this
}

fun String.validateIntList(): List<Int> {
return this.split(',').map {
it.toIntOrNull() ?: throw IllegalArgumentException(ErrorMessages.INVALID_INPUT)
}
}

fun List<Int>.findDuplicates(): List<Int> {
val duplicates = this.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
require(duplicates.isEmpty()) { ErrorMessages.DUPLICATE_NUMBERS }
return this
}

fun List<Int>.validateCount(count: Int = LottoConstants.LOTTO_NUMBER_COUNT.value): List<Int> {
require(this.size == count) { ErrorMessages.INVALID_COUNT }
return this
}

fun Int.validateRange(
start: Int = LottoConstants.MIN_LOTTO_NUMBER.value,
end: Int = LottoConstants.MAX_LOTTO_NUMBER.value
): Int {
require(this in start..end) { ErrorMessages.OUT_OF_RANGE.format(start, end) }
return this
}

fun List<Int>.validateRange(
start: Int = LottoConstants.MIN_LOTTO_NUMBER.value,
end: Int = LottoConstants.MAX_LOTTO_NUMBER.value
): List<Int> {
require(this.all { it in start..end }) { ErrorMessages.OUT_OF_RANGE.format(start, end) }
return this
}

fun Double.round(decimalPlaces: Int = LottoConstants.PRECISION.value): Double {
val factor = 10.0.pow(decimalPlaces)
return (this * factor).roundToInt() / factor
}
Loading