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

[로또] 박정민 미션 제출합니다. #161

Open
wants to merge 18 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
85 changes: 85 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 우아한 테크코스 6기 프리코스 미션3 - 로또

## 기능 요구 사항

### 로또 구매
- 구입 금액 입력받기
- 출력 문자열: "구입금액을 입력해 주세요."
- 사용 라이브러리: camp.nextstep.edu.missionutils.Console의 readLine()
- 예외처리 - 숫자가 아닌 입력시
- IllegalArgumentException 발생
- `[ERROR] 구입 금액은 정수여야 합니다.` 출력
- 입력 다시 받기
- 예외처리 - 1000원 미만의 금액 입력시
- IllegalArgumentException 발생
- `[ERROR] 구입 금액은 1000원 이상이어야 합니다.` 출력
- 예외처리 - 1000원으로 나누어 떨어지지 않는 금액 입력시
- IllegalArgumentException 발생
- `[ERROR] 구입 금액은 1000원으로 나누어 떨어져야 합니다.` 출력
- 입력 다시 받기
- 구입 금액 / 1000 만큼 로또를 구매
- 출력 문자열: "${구입금액/1000}개를 구매했습니다."
- 로또 수만큼 6개의 랜덤 숫자 (1~45) 뽑아 출력하기
- 사용 라이브러리: camp.nextstep.edu.missionutils.Randoms의 pickUniqueNumbersInRange()

### 로또 당첨 번호 입력
- 로또 당첨 번호 6개(범위: 1~45) 입력
- 출력 문자열: "당첨 번호를 입력해 주세요."
- 사용 라이브러리: camp.nextstep.edu.missionutils.Console의 readLine()
- 입력 예: "$숫자1,$숫자2,$숫자3"
- 예외처리 - 입력이 6개가 아닐 경우
- IllegalArgumentException 발생
- `[ERROR] 로또 당첨 번호는 6개의 정수여야 합니다.` 출력
- 입력 다시 받기
- 예외처리 - 1~45 범위의 정수가 아닌 입력시
- IllegalArgumentException 발생
- `[ERROR] 로또 당첨 번호는 1~45사이의 정수여야 합니다.` 출력
- 예외처리 - 정수가 아닌 입력시
- IllegalArgumentException 발생
- `[ERROR] 로또 번호의 타입은 정수여야 합니다.` 출력
- 입력 다시 받기
- 예외처리 - 중복되는 정수 입력시
- IllegalArgumentException 발생
- `[ERROR] 로또 당첨 번호는 중복될 수 없습니다.` 출력
- 입력 다시 받기
- 보너스 번호 1개(범위: 1~45) 입력
- 출력 문자열: "보너스 번호를 입력해 주세요."
- 사용 라이브러리: camp.nextstep.edu.missionutils.Console의 readLine()
- 예외처리 - 정수가 아닌 입력시
- IllegalArgumentException 발생
- `[ERROR] 로또 번호의 타입은 정수여야 합니다.` 출력
- 예외처리 - 1~45 범위의 정수가 아닌 입력시
- IllegalArgumentException 발생
- `[ERROR] 로또 번호는 1~45사이의 정수여야 합니다.` 출력
- 입력 다시 받기
- 예외처리 - 로또 당첨 번호와 중복되는 정수 입력시
- IllegalArgumentException 발생
- `[ERROR] 보너스 번호와 로또 당첨 번호는 같을 수 없습니다.` 출력
- 입력 다시 받기

### 당첨 등수 판단과 수익률 계산
- 당첨 기준
- 1등: 6개 번호 모두 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
- 로또별로 당첨 확인, 당첨 금액 합산
- 수익률 = 총 당첨 금액 합 / 로또 구입금액
- 백분율로 계산하며, 소수점 둘째 자리에서 반올림.

### 당첨 내역 출력
- 당첨 내역 출력 시작 문자열 출력하기
- 출력 문자열: "당첨 통계", "---"
- 당첨 기준 출력
- 5등의 당첨 기준부터 차례로 출력. (0개 당첨시에도 출력함.)
- 출력 예: "3개 일치 (5,000원) - 1개"
- 수익률 출력
- 출력 문자열: "총 수익률은 ${수익률}%입니다."

## 프로그래밍 요구 사항
- 함수의 길이가 15라인을 넘어가지 않도록, 한 가지 일만 하도록 구현
- else 지양하기 (조건절에서 return하는 방식으로 구현 or when문 사용)
- Enum 클래스를 적용하기
- 도메인 로직에 단위 테스트 구현. (단, UI 로직 제외)
- 핵심 로직을 구현하는 코드와 UI 담당 로직을 분리 구현
7 changes: 6 additions & 1 deletion src/main/kotlin/lotto/Application.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package lotto

fun main() {
TODO("프로그램 구현")
val myLottoStatus = LottoStatus()
val myTargetLotto = TargetLottoStatus()
myLottoStatus.buyUntilValid()
myTargetLotto.inputTargetLotto()
val myLottoMatcher = LottoMatcher(myTargetLotto, myLottoStatus)
myLottoMatcher.startMatch()
}
17 changes: 17 additions & 0 deletions src/main/kotlin/lotto/Error.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto

object Error {
const val MESSAGE_HEADER = "[ERROR] "
const val LOTTO_NUMBER_SIZE_IS_NOT_SIX = "로또 번호는 6개의 정수여야 합니다."
const val LOTTO_NUMBER_TYPE_IS_NOT_INT = "로또 번호의 타입은 정수여야 합니다."
const val LOTTO_NUMBER_IS_OUT_OF_RANGE = "로또 번호는 1~45사이의 정수여야 합니다."
const val BONUS_NUMBER_CANT_EQUAL_LOTTO_NUMBER = "보너스 번호와 로또 당첨 번호는 같을 수 없습니다."
const val LOTTO_NUMBER_CANT_DUPLICATE = "로또 번호는 중복될 수 없습니다."
const val PRICE_TYPE_IS_NOT_INT = "구입 금액은 정수여야 합니다."
const val PRICE_IS_UNDER_1000 = "구입 금액은 1000원 이상이어야 합니다."
const val PRICE_IS_NOT_PRODUCT_OF_1000 = "구입 금액은 1000원으로 나누어 떨어져야 합니다."

fun printErrorMessage(msg: String) {
println(MESSAGE_HEADER + msg)
}
}
22 changes: 20 additions & 2 deletions src/main/kotlin/lotto/Lotto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@ package lotto

class Lotto(private val numbers: List<Int>) {
init {
require(numbers.size == 6)
require(numbers.size == LottoResource.LOTTO_SIZE) {
Error.printErrorMessage(Error.LOTTO_NUMBER_SIZE_IS_NOT_SIX)
}
require(numbers.distinct().size == LottoResource.LOTTO_SIZE) {
Error.printErrorMessage(Error.LOTTO_NUMBER_CANT_DUPLICATE)
}
}
fun getLottoFormat(): String {
return numbers.toString()
}

fun compareNumbers(target: List<Int>): Int {
val correctNumbers = numbers.filter { it in target }
return correctNumbers.size
}

// TODO: 추가 기능 구현
fun compareBonusNumber(target: Int): Boolean {
if (target in numbers)
return true
return false
}
}

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

import kotlin.math.round

class LottoMatcher(val target: TargetLottoStatus, val ownLotto: LottoStatus) {
var profitSum = 0
var winCount = mutableListOf<Int>(0,0,0,0,0,0)
fun startMatch() {
var correctCount = 0
var isBonusCorrect = false
ownLotto.myLottos.forEach {
correctCount = it.compareNumbers(target.lottoNumbers)
if (correctCount == LottoResource.NUMBER_OF_BRANCH_2ND_AND_3RD)
isBonusCorrect = it.compareBonusNumber(target.bonusNumber)
val rank = checkRank(correctCount, isBonusCorrect)
takeRankPrize(rank)
}
printStatistics()
}
fun checkRank(correctCount: Int, bonusCount: Boolean): Rank {
val currentRank: Rank = when (correctCount) {
Rank.FIRST.correctCount -> Rank.FIRST
LottoResource.NUMBER_OF_BRANCH_2ND_AND_3RD -> when (bonusCount) {
true -> Rank.SECOND
false -> Rank.THIRD
}
Rank.FOURTH.correctCount -> Rank.FOURTH
Rank.FIFTH.correctCount -> Rank.FIFTH
else -> Rank.NO_PRIZE
}
return currentRank
}
fun checkRank(place: Int): Rank {
val rank: Rank = when (place) {
Rank.FIRST.rank -> Rank.FIRST
Rank.SECOND.rank -> Rank.SECOND
Rank.THIRD.rank -> Rank.THIRD
Rank.FOURTH.rank -> Rank.FOURTH
Rank.FIFTH.rank -> Rank.FIFTH
else -> Rank.NO_PRIZE
}
return rank
}
fun takeRankPrize(rank: Rank) {
profitSum += rank.winnerPrize
winCount[rank.rank]++
}
fun printStatistics() {
println(LottoResource.PRINT_STATISTICS_MESSAGE)
for (place in LottoResource.MAX_RANK_PLACE downTo LottoResource.MIN_RANK_PLACE) {
printPerPlacePrize(place)
}
printProfitRate()
}
fun printProfitRate() {
val originPrice = ownLotto.countOfLotto * LottoResource.LOTTO_PRICE
val profitRate = round(((profitSum.toDouble() / originPrice.toDouble() * 100) * 100)) / 100
println("총 수익률은 ${profitRate}%입니다.")
}
fun printPerPlacePrize(rank: Int) {
val currentRank = checkRank(rank)
println(currentRank.message + "${winCount[rank]}개")
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/lotto/LottoResource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lotto

object LottoResource {
const val MAX_LOTTO_NUMBER = 45
const val MIN_LOTTO_NUMBER = 1
const val LOTTO_SIZE = 6
const val LOTTO_PRICE = 1000
const val LOTTO_NUMBER_INPUT_MESSAGE = "당첨 번호를 입력해 주세요."
const val BONUS_NUMBER_INPUT_MESSAGE = "보너스 번호를 입력해 주세요."
const val PRICE_INPUT_MESSAGE = "구입 금액을 입력해 주세요."
const val COUNT_OF_LOTTO_OUTPUT_MESSAGE = "개를 구매했습니다."
const val PRINT_STATISTICS_MESSAGE = "당첨 통계\n---"
const val MAX_RANK_PLACE = 5
const val MIN_RANK_PLACE = 1
const val NUMBER_OF_BRANCH_2ND_AND_3RD = 5
}
58 changes: 58 additions & 0 deletions src/main/kotlin/lotto/LottoStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package lotto

import camp.nextstep.edu.missionutils.Console
import camp.nextstep.edu.missionutils.Randoms

class LottoStatus {
var countOfLotto: Int = 0
var myLottos = mutableListOf<Lotto>()
fun buyUntilValid() {
var isValidPrice = false
while (!isValidPrice) {
isValidPrice = true
try {
inputPrice()
} catch (e: IllegalArgumentException) {
isValidPrice = false
}
}
pickLotto()
}
fun pickLotto() {
for (i in 0 until countOfLotto) {
val currentLottoNumbers = pickRandomNumbersForLotto()
val currentLotto = Lotto(currentLottoNumbers)
myLottos.add(currentLotto)
println(currentLotto.getLottoFormat())
}
println()
}
private fun pickRandomNumbersForLotto(): List<Int> {
return Randoms.pickUniqueNumbersInRange(
LottoResource.MIN_LOTTO_NUMBER,
LottoResource.MAX_LOTTO_NUMBER,
LottoResource.LOTTO_SIZE
)
}
private fun inputPrice() {
println(LottoResource.PRICE_INPUT_MESSAGE)
val inputPrice = Console.readLine()
countOfLotto = validatePrice(inputPrice) / LottoResource.LOTTO_PRICE
println()
println("$countOfLotto" + LottoResource.COUNT_OF_LOTTO_OUTPUT_MESSAGE)
}

fun validatePrice(inputPrice: String): Int {
require(inputPrice.toIntOrNull() != null) {
Error.printErrorMessage(Error.PRICE_TYPE_IS_NOT_INT)
}
val realPrice = inputPrice.toInt()
require(realPrice >= LottoResource.LOTTO_PRICE) {
Error.printErrorMessage(Error.PRICE_IS_UNDER_1000)
}
require(realPrice % LottoResource.LOTTO_PRICE == 0) {
Error.printErrorMessage(Error.PRICE_IS_NOT_PRODUCT_OF_1000)
}
return realPrice
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/lotto/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package lotto

enum class Rank(val winnerPrize: Int, val rank: Int, val message: String, val correctCount: Int) {
FIRST(2000000000, 1, "6개 일치 (2,000,000,000원) - ", 6),
SECOND(30000000, 2, "5개 일치, 보너스 볼 일치 (30,000,000원) - ", 5),
THIRD(1500000, 3, "5개 일치 (1,500,000원) - ", 5),
FOURTH(50000, 4, "4개 일치 (50,000원) - ", 4),
FIFTH(5000, 5, "3개 일치 (5,000원) - ", 3),
NO_PRIZE(0, 0, "", 0)
}
71 changes: 71 additions & 0 deletions src/main/kotlin/lotto/TargetLottoStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package lotto

import camp.nextstep.edu.missionutils.Console

class TargetLottoStatus {
var lottoNumbers = mutableListOf<Int>()
var bonusNumber: Int = 0
fun inputTargetLotto() {
repeatUntilValidLottoNumber()
repeatUntilValidBonusNumber()
}
fun repeatUntilValidLottoNumber() {
var isValidLotto = false
while (!isValidLotto) {
isValidLotto = true
try {
inputLottoNumbers()
} catch (e: IllegalArgumentException) {
isValidLotto = false
}
}
}
fun repeatUntilValidBonusNumber() {
var isValidBonus = false
while (!isValidBonus) {
isValidBonus = true
try {
inputBonusNumber()
} catch (e: IllegalArgumentException) {
isValidBonus = false
}
}
}
fun inputBonusNumber() {
println(LottoResource.BONUS_NUMBER_INPUT_MESSAGE)
val inputNumber = Console.readLine()
println()
bonusNumber = validateNumber(inputNumber)
validateBonusNumberIsDuplicateWithLottoNumbers()
}
fun inputLottoNumbers() {
lottoNumbers.clear()
println(LottoResource.LOTTO_NUMBER_INPUT_MESSAGE)
val inputNumbers = Console.readLine()
println()
putValidNumberIntoLottoNumbers(inputNumbers)
Lotto(lottoNumbers)
}
fun putValidNumberIntoLottoNumbers(inputNumbers: String) {
val numbers = inputNumbers.split(",")
numbers.forEach {
val number = validateNumber(it)
lottoNumbers.add(number)
}
}
fun validateNumber(number: String): Int {
require(number.toIntOrNull() != null) {
Error.printErrorMessage(Error.LOTTO_NUMBER_TYPE_IS_NOT_INT)
}
val currentNumber = number.toInt()
require(currentNumber in LottoResource.MIN_LOTTO_NUMBER..LottoResource.MAX_LOTTO_NUMBER) {
Error.printErrorMessage(Error.LOTTO_NUMBER_IS_OUT_OF_RANGE)
}
return currentNumber
}
fun validateBonusNumberIsDuplicateWithLottoNumbers() {
require(!lottoNumbers.contains(bonusNumber)) {
Error.printErrorMessage(Error.BONUS_NUMBER_CANT_EQUAL_LOTTO_NUMBER)
}
}
}
Loading