diff --git a/.gitignore b/.gitignore index b6c378809..7e0720ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/** !**/src/test/** +./.gitmessage.txt ### macOS ### .DS_Store diff --git a/.gitmessage.txt b/.gitmessage.txt new file mode 100644 index 000000000..9621beba2 --- /dev/null +++ b/.gitmessage.txt @@ -0,0 +1,24 @@ +###### <제목> ###### +# <타입>: <제목> 형식으로 아래 공백 라인에 작성 +# 제목은 최대 50글자, 제목 끝에 마침표 금지, '무엇'을 했는지 명확하게 작성 + +# 아래 공백 지우지 않기(제목, 본문의 분리) + +###### <본문> ###### +# 본문(추가 설명)을 아랫줄에 작성 + +###### <꼬릿말> ##### +# 꼬릿말(footer)을 아랫줄에 작성 (현재 커밋과 관련된 이슈 번호 추가 등) + +###### <타입> ###### +# FEAT : 기능 추가 및 수정 +# FIX : 버그 수정 +# DOCS : 문서 수정 +# ADD : 파일, 디렉터리 생성 +# REMOVE : 파일, 디렉터 삭제 +# TEST : 테스트 코드 추가 +# REFACTOR : 코드 리팩토링 +# STYLE : 코드 의미에 영향을 주지 않는 변경사항(코드 포맷팅 등) +# CHORE : 빌드 부분 혹은 패키지 매니저 수정사항(gitignore 등) +# CICD : CI/CD 관련 설정 +################### \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e69de29bb..ce1bbb5df 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,80 @@ +3주차 미션 - 로또 +================== +------------------ + +## 구현 기능 목록 +### 로또 게임의 규칙 +로또 게임 규칙은 아래와 같다. +> - 로또 번호의 숫자 범위는 1~45까지이다. +> - 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다. +> - 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다. +> - 당첨은 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원 +> ``` + +추가 규칙들은 아래와 같다. +> * 로또 1장당 가격은 1,000원이며, 구입 금액을 입력하면 그에 해당하는 만큼 로또를 발행해야 한다. +> * 당첨 번호와 보너스 번호를 입력받는다. +> * 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다. +> * 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시키고, +> "[ERROR]"로 시작하는 에러 메시지를 출력한 후 그 부분부터 입력을 다시 받는다. +> + `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다. + +위 규칙을 바탕으로 필요한 기능을 구현한다. + +### 사용자 입력 +사용자의 입력은 `Console.readLine()`을 활용한다. +* 사용자로부터 로또 구입 금액을 입력 받는다. + + 로또는 장당 1,000원이며, 구입 금액은 1,000원 단위로 입력 받는다. + + 1,000원으로 나누어 떨어지지 않는 경우 예외 처리 후 다시 입력 받는다. +* 사용자로부터 당첨 번호를 입력 받는다. + + 번호는 쉼표(,)를 기준으로 구분한다. + + 로또 번호의 개수는 6개이다. + + 로또 번호는 1과 45사이의 정수이며, 중복되지 않는다. + + 잘못된 입력을 받으면 예외 처리 후 다시 입력 받는다. +* 사용자로부터 보너스 번호를 입력받는다. + + 1과 45사이의 정수이며, 마찬가지로 중복되지 않는다. + + 잘못된 입력을 받으면 예외 처리 후 다시 입력 받는다. +### 로또 번호 생성 +로또를 발행하여 출력한다. +* 사용자가 구매한 수량만큼 로또를 발행한다(로또 번호 생성). + + 로또 번호를 무작위로 생성하는 기능은 `Randoms.pickUniqueNumbersInRange()`을 활용한다. +* 발행한 수량과 로또 번호를 출력한다. + + 로또 번호 출력 시 오름차순으로 정렬하여 출력한다. +### 당첨 내역 출력 +* 사용자가 입력한 당첨 번호와 발행한 로또 번호를 비교한다. + + 일치한 개수와 상금, 일치한 로또의 개수를 출력한다. +* 번호를 비교하여 일치한 개수에 따라 등수가 매겨진다. +* 등수에 따른 상금을 총합한다. +### 수익률 계산 +* 수익률을 계산하여 출력한다. + + 구매한 금액에 대한 당첨금의 비율을 계산하여 출력한다. + + 출력 시 소수점 둘째 자리에서 반올림한다. +### 예외 처리 +사용자 입력에 대한 예외를 체크하고 처리한다. +* 로또 구입 금액의 예외를 처리한다. + + 양의 정수만을 입력받는다. + - 숫자 이외의 다른 문자가 입력되면 예외 처리한다. + + 1,000원으로 나누어 떨어지지 않으면 예외 처리한다. +* 당첨 번호 입력에 대한 예외를 처리한다. + + 쉼표와 숫자 이외에 다른 문자가 있는지 확인한다. + + 쉼표로 구분된 숫자들이 중복이 있는지 검사한다. + + 쉼표로 구분된 숫자가 1에서 45사이의 숫자인지 확인한다. +* 보너스 번호 입력에 대한 예외를 처리한다. + + 정수 이외의 다른 문자는 아닌지 확인한다. + + 숫자가 1과 45 사이의 정수인지 확인한다. + + 당첨 번호와 중복인지 아닌지 확인한다. +* 예외 상황 시 "[ERROR]"로 시작하는 에러 문구를 출력해야 한다. + + 각 예외 상황에 맞는 에러 문구를 출력한다. + + 예외 처리 후 입력을 다시 받는다. + +추가적으로 아래 사항들을 따른다. +### Lotto 클래스 활용 +* 제공된 Lotto 클래스를 활용하여 구현한다. +* 테스트코드인 LottoTest를 참고하여 구현한다. + + 로또 번호에 대한 검증이 Lotto 클래스 안에서 일어나야 한다. \ No newline at end of file diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index d7168761c..94149a17b 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,5 +1,27 @@ package lotto +import camp.nextstep.edu.missionutils.Console + fun main() { - TODO("프로그램 구현") + val user = User() + user.inputPurchaseMoney() + user.purchaseLottoTickets() + + var lotto: Lotto + var validation: Boolean = false + var winning = listOf() + while (!validation) { + try { + winning = user.inputLottoNumbers() + lotto = Lotto(winning) + validation = true + } catch (e: IllegalArgumentException){ + println("[ERROR] ${e.message}") + } + } + user.inputBonusNumber() + val winningNumbers = lotto.getWinningNumbers() + + Statistics.howManyWins(lotto.getWinningNumbers(), user) + } diff --git a/src/main/kotlin/lotto/Lotto.kt b/src/main/kotlin/lotto/Lotto.kt index 5ca00b4e4..00a39bb8e 100644 --- a/src/main/kotlin/lotto/Lotto.kt +++ b/src/main/kotlin/lotto/Lotto.kt @@ -2,8 +2,35 @@ package lotto class Lotto(private val numbers: List) { init { - require(numbers.size == 6) + // 로또 번호의 개수는 6개여야 한다. + require(numbers.size == 6) { "로또 번호는 6개여야 합니다." } + // 로또 번호는 1부터 45 사이의 정수여야 한다. + require(inCorrectRange(numbers)) { "로또 번호는 1부터 45 사이의 숫자여야 합니다." } + // 로또 번호는 중복이 없어야 한다. + require(hasDuplicateNumbers(numbers)) { "로또 번호는 중복되지 않아야 합니다." } } - // TODO: 추가 기능 구현 + private fun inCorrectRange(lottoNumbers: List): Boolean { + for (number in lottoNumbers) { + if (number !in 1..45) { + return false + } + } + return true + } + + private fun hasDuplicateNumbers(lottoNumbers: List): Boolean { + val validator = mutableListOf() + for (lotto in lottoNumbers) { + if (validator.contains(lotto.toInt())) { + return false + } + validator.add(lotto.toInt()) + } + return true + } + + fun getWinningNumbers(): List{ + return numbers + } } diff --git a/src/main/kotlin/lotto/Statistics.kt b/src/main/kotlin/lotto/Statistics.kt new file mode 100644 index 000000000..36cb320b5 --- /dev/null +++ b/src/main/kotlin/lotto/Statistics.kt @@ -0,0 +1,49 @@ +package lotto + +class Statistics { + companion object { + fun howManyWins(winNumbers: List, user: User) { + for (lotto in user.lottoTickets) { + compareWinNumbers(winNumbers, lotto, user.bonusNumber) + } + val returnRate = rateOfReturn(user.purchaseMoney) + printResult(returnRate) + } + + private fun compareWinNumbers(winNumbers: List, lotto: List, bonus: Int) { + var counts: Int = 0 + for (winNumber in winNumbers) { + if (lotto.contains(winNumber)) { + counts++ + } + } + if (counts == 3) WinEnum.FIFTH.counts++ + else if (counts == 4) WinEnum.FOURTH.counts++ + else if (counts == 5){ + if (lotto.contains(bonus)) WinEnum.SECOND.counts++ + else WinEnum.THIRD.counts++ + } + else if (counts == 6) WinEnum.FIRST.counts + } + + private fun rateOfReturn(purchaseMoney: Int): Double { + val benefit = WinEnum.FIFTH.win * WinEnum.FIFTH.counts + +WinEnum.FOURTH.win * WinEnum.FOURTH.counts + +WinEnum.THIRD.win * WinEnum.THIRD.counts + +WinEnum.SECOND.win * WinEnum.SECOND.counts + +WinEnum.FIRST.win * WinEnum.FIRST.counts + return (benefit / purchaseMoney) * 100.0 + } + + private fun printResult(returnRate: Double) { + val roundOff = String.format("%.2f", returnRate) + println("\n당첨 통계\n---") + println(WinEnum.FIFTH.printer) + println(WinEnum.FOURTH.printer) + println(WinEnum.THIRD.printer) + println(WinEnum.SECOND.printer) + println(WinEnum.FIRST.printer) + println("총 수익률은 ${roundOff}%입니다.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/User.kt b/src/main/kotlin/lotto/User.kt new file mode 100644 index 000000000..6d2972681 --- /dev/null +++ b/src/main/kotlin/lotto/User.kt @@ -0,0 +1,64 @@ +package lotto + +import camp.nextstep.edu.missionutils.Console +import camp.nextstep.edu.missionutils.Randoms + +class User { + var purchaseMoney: Int = 0 + val lottoTickets = mutableListOf>() + var bonusNumber: Int = 0 + val validator = Validator() + + fun inputPurchaseMoney() { + var validation: Boolean = false + var money: String = "" + while (!validation) { + println("구입금액을 입력해 주세요.") + money = Console.readLine() + validation = validator.validatePurchaseMoney(money) + } + purchaseMoney = money.toInt() + } + + fun purchaseLottoTickets() { + val ticketCounts: Int = purchaseMoney / 1000 + println("\n${ticketCounts}개를 구매했습니다.") + for (count in 0 until ticketCounts) { + val numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6) + numbers.sort() + lottoTickets.add(numbers) + println(numbers) + } + } + + fun inputLottoNumbers(): List { + var validation: Boolean = false + var lottoNumbers: String = "" + while (!validation) { + println("\n당첨 번호를 입력해 주세요.") + lottoNumbers = Console.readLine() + validation = validator.couldConvertIntList(lottoNumbers) + } + return convertIntList(lottoNumbers) + } + + private fun convertIntList(winning: String): List { + var winningNumbers = mutableListOf() + for (number in winning.split(",")) { + winningNumbers.add(number.toInt()) + } + winningNumbers.sort() + return winningNumbers + } + + fun inputBonusNumber() { + var validation: Boolean = false + var bonus: String = "" + while (!validation) { + println("\n보너스 번호를 입력해 주세요.") + bonus = Console.readLine() + validation = validator.validateBonusNumber(bonus) + } + bonusNumber = bonus.toInt() + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/Validator.kt b/src/main/kotlin/lotto/Validator.kt new file mode 100644 index 000000000..ceda4e12c --- /dev/null +++ b/src/main/kotlin/lotto/Validator.kt @@ -0,0 +1,66 @@ +package lotto + +class Validator { + fun validatePurchaseMoney(purchaseMoney: String): Boolean { + return try { + isNumberOverZero(purchaseMoney) && isDividedUpThousand(purchaseMoney.toInt()) + } catch (e: IllegalArgumentException) { + println("[ERROR] ${e.message}") + false + } + } + + private fun isNumberOverZero(input: String): Boolean { + val number = input.toIntOrNull() + return if (number != null && number > 0) { + true + } else { + throw IllegalArgumentException("0보다 큰 정수를 입력해주세요.") + } + } + + private fun isDividedUpThousand(money: Int): Boolean { + return if (money % 1000 == 0) { + true + } else { + throw IllegalArgumentException("구입 금액이 1000원으로 나누어 떨어지지 않습니다.") + } + } + + fun couldConvertIntList(winning: String): Boolean { + return try { + containsComma(winning) + val winningNumbers: List = winning.split(",") + for (number in winningNumbers) { + isNumberOverZero(number) + } + true + } catch (e: IllegalArgumentException) { + println("[ERROR] ${e.message}") + false + } + } + + private fun containsComma(lotto: String) { + if (!lotto.contains(',')) { + throw IllegalArgumentException("로또 번호는 쉼표(,)로 구분지어주세요.") + } + } + + fun validateBonusNumber(bonus: String): Boolean { + return try { + isNumberOverZero(bonus) + inCorrectRange(bonus.toInt()) + true + } catch (e: IllegalArgumentException) { + println("[ERROR] ${e.message}") + false + } + } + + private fun inCorrectRange(number: Int) { + if (number !in 1..45){ + throw IllegalArgumentException("로또 번호는 1부터 45 사이의 숫자여야 합니다.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/WinEnum.kt b/src/main/kotlin/lotto/WinEnum.kt new file mode 100644 index 000000000..ea44ee500 --- /dev/null +++ b/src/main/kotlin/lotto/WinEnum.kt @@ -0,0 +1,9 @@ +package lotto + +enum class WinEnum(val win: Int, val printer: String, var counts: Int) { + FIFTH(5000, "3개 일치 (5,000원) - ${FIFTH.counts}", 0), + FOURTH(50000, "4개 일치 (50,000원) - ${FOURTH.counts}", 0), + THIRD(1500000, "5개 일치 (1,500,000원) - ${THIRD.counts}", 0), + SECOND(30000000, "5개 일치, 보너스 볼 일치 (30,000,000원) - ${SECOND.counts}", 0), + FIRST(2000000000, "6개 일치 (2,000,000,000원) - ${FIRST.counts}", 0) +} \ No newline at end of file