diff --git a/README.md b/README.md index 4fc0ae874..9b7b4df07 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ -# kotlin-lotto-precourse +# 프리코스 2주차 미션 - 로또 + +## 기능 요구 사항 + +간단한 로또 발매기를 구현한다. + +- 로또 번호의 숫자 범위는 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]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. + - `Exception`이 아닌`IllegalArgumentException`,`IllegalStateException`등과 같은 명확한 유형을 처리한다. + +## 기능 목록 +- 입력 +- [ ] **로또 구입 금액**을 입력 받는다. +- [ ] **당첨 번호**를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다. +- [ ] **보너스 번호**를 입력 받는다. + + +- 출력 +- [ ] 발행한 로또 **수량** 및 **번호**를 출력한다. 로또 번호는 **오름차순**으로 정렬하여 보여준다. +- [ ] **당첨 내역**을 출력한다. +- [ ] **수익률**을 출력한다. 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, _1,000,000.0%_) + + +- 입력한 값이 유효한지 검증한다.(로또 구입 금액, 당첨 번호, 보너스 번호) +- [ ] 로또 구입 금액이 1000원 미만인 경우 +- [ ] 로또 구입 금액이 1000원으로 나누어 떨어지지 않는 경우 +- [ ] 로또 번호의 숫자 범위를 초과한 경우(1 ~ 45) +- [ ] 로또 번호의 갯수(당첨 번호 6개, 보너스 번호 1개)를 만족하지 않는 경우 +- [ ] 당첨 번호에 중복된 숫자가 포함된 경우 +- [ ] 미입력 +- [ ] 공백이 포함된 경우 +- [ ] 숫자(양수)와 쉼표가 아닌 값이 입력 된 경우 + + +- 구입한 만큼의 로또를 발행한다. + + +- 당첨 내역을 산정한다. + + +- 수익률을 산정한다. \ No newline at end of file diff --git a/src/main/kotlin/lotto/Application.kt b/src/main/kotlin/lotto/Application.kt index 151821c9c..a0b4cd93a 100644 --- a/src/main/kotlin/lotto/Application.kt +++ b/src/main/kotlin/lotto/Application.kt @@ -1,5 +1,5 @@ package lotto fun main() { - // TODO: 프로그램 구현 -} + LottoMachine().run() +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/InputView.kt b/src/main/kotlin/lotto/InputView.kt new file mode 100644 index 000000000..c290382db --- /dev/null +++ b/src/main/kotlin/lotto/InputView.kt @@ -0,0 +1,30 @@ +package lotto + +import camp.nextstep.edu.missionutils.Console + +class InputView { + + fun getPurchaseAmount(): Int { + return tryGetInput("구입금액을 입력해 주세요.") { Validator.validatePurchaseAmount(it) } + } + + fun getLottoNumbers(): List { + return tryGetInput("\n당첨 번호를 입력해 주세요.") { Validator.validateLottoNumbers(it) } + } + + fun getBonusNumber(): Int { + return tryGetInput("\n보너스 번호를 입력해 주세요.") { Validator.validateBonusNumber(it) } + } + + private fun tryGetInput(message: String, validator: (String) -> T): T { + while (true) { + println(message) + val input = Console.readLine() + try { + return validator(input) + } catch (e: IllegalArgumentException) { + println(e.message) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/Lotto.kt b/src/main/kotlin/lotto/Lotto.kt index b97abc385..5d742e840 100644 --- a/src/main/kotlin/lotto/Lotto.kt +++ b/src/main/kotlin/lotto/Lotto.kt @@ -1,9 +1,49 @@ package lotto +import camp.nextstep.edu.missionutils.Randoms + class Lotto(private val numbers: List) { + var firstRank = 0 + var secondRank = 0 + var thirdRank = 0 + var fourthRank = 0 + var fifthRank = 0 + init { require(numbers.size == 6) { "[ERROR] 로또 번호는 6개여야 합니다." } + require(numbers.toSet().size == 6) { "[ERROR] 로또 번호는 중복일 수 없습니다." } + require(numbers.all { it in 1..45 }) { "[ERROR] 로또 번호의 범위는 1 ~ 45입니다." } + } + + + companion object { + fun issueLotto(count: Int): List> { + return (1..count).map { + Randoms.pickUniqueNumbersInRange(1, 45, 6).sorted() + } + } } - // TODO: 추가 기능 구현 + fun getLottoRank(myLotto: List>, bonusNumber: Int) { + myLotto.forEach { lottoNumbers -> + val matchCount = countMatchingNumbers(lottoNumbers, numbers.sorted()) + when { + matchCount == 6 -> firstRank++ + matchCount == 5 && lottoNumbers.contains(bonusNumber) -> secondRank++ + matchCount == 5 -> thirdRank++ + matchCount == 4 -> fourthRank++ + matchCount == 3 -> fifthRank++ + } + } + } + + private fun countMatchingNumbers(list1: List, list2: List): Int { + return list1.intersect(list2.toSet()).size + } + + fun calculateProfitRate(purchaseAmount: Int): Double { + val totalPrizeMoney = + 5000 * fifthRank + 50000 * fourthRank + 1500000 * thirdRank + 30000000 * secondRank + 2000000000 * firstRank + return (totalPrizeMoney / purchaseAmount.toDouble()) * 100 + } } diff --git a/src/main/kotlin/lotto/LottoMachine.kt b/src/main/kotlin/lotto/LottoMachine.kt new file mode 100644 index 000000000..ea9d32657 --- /dev/null +++ b/src/main/kotlin/lotto/LottoMachine.kt @@ -0,0 +1,24 @@ +package lotto + +class LottoMachine { + fun run() { + val inputView = InputView() + val outputView = OutputView() + + val purchaseAmount = inputView.getPurchaseAmount() + val lottoCount = purchaseAmount / 1000 + + val myLotto = Lotto.issueLotto(lottoCount) + outputView.printLottoCount(lottoCount, myLotto) + + val winningNumbers = inputView.getLottoNumbers() + val bonusNumber = inputView.getBonusNumber() + + val lotto = Lotto(winningNumbers) + lotto.getLottoRank(myLotto, bonusNumber) + outputView.printLottoRank(lotto) + + val profitRate = lotto.calculateProfitRate(purchaseAmount) + outputView.printProfitRate(profitRate) + } +} \ No newline at end of file diff --git a/src/main/kotlin/lotto/OutputView.kt b/src/main/kotlin/lotto/OutputView.kt new file mode 100644 index 000000000..39f29ec81 --- /dev/null +++ b/src/main/kotlin/lotto/OutputView.kt @@ -0,0 +1,24 @@ +package lotto + +class OutputView { + fun printLottoCount(count: Int, myLotto: List>) { + println("\n${count}개를 구매했습니다.") + myLotto.forEach { lottoNumbers -> + println(lottoNumbers.joinToString(prefix = "[", postfix = "]", separator = ", ")) + } + } + + fun printLottoRank(lotto: Lotto) { + println("\n당첨 통계") + println("---") + println("3개 일치 (5,000원) - ${lotto.fifthRank}개") + println("4개 일치 (50,000원) - ${lotto.fourthRank}개") + println("5개 일치 (1,500,000원) - ${lotto.thirdRank}개") + println("5개 일치, 보너스 볼 일치 (30,000,000원) - ${lotto.secondRank}개") + println("6개 일치 (2,000,000,000원) - ${lotto.firstRank}개") + } + + fun printProfitRate(profitRate: Double) { + println("총 수익률은 ${"%.1f".format(profitRate)}%입니다.") + } +} \ 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..e8c5c40f0 --- /dev/null +++ b/src/main/kotlin/lotto/Validator.kt @@ -0,0 +1,56 @@ +package lotto + +class Validator { + companion object { + fun validatePurchaseAmount(amount: String): Int { + val purchaseAmount = parseAmount(amount) + checkAmountIsAboveMinimum(purchaseAmount) + checkAmountIsDivisibleByThousand(purchaseAmount) + return purchaseAmount + } + + private fun parseAmount(amount: String): Int { + return amount.toIntOrNull() ?: throw IllegalArgumentException("[ERROR] 구입 금액은 숫자여야 합니다.") + } + + private fun checkAmountIsAboveMinimum(amount: Int) { + require(amount >= 1000) { "[ERROR] 구입 금액은 1000원 이상이어야 합니다." } + } + + private fun checkAmountIsDivisibleByThousand(amount: Int) { + require(amount % 1000 == 0) { "[ERROR] 구입 금액은 1000원 단위로 나누어 떨어져야 합니다." } + } + + fun validateLottoNumbers(input: String): List { + checkInputNotBlank(input) + val numbers = parseLottoNumbers(input) + checkLottoNumbersAreUnique(numbers) + checkLottoNumbersRange(numbers) + return numbers + } + + private fun checkInputNotBlank(input: String) { + require(input.isNotBlank()) { "[ERROR] 로또 번호를 입력해 주세요." } + } + + private fun parseLottoNumbers(input: String): List { + return input.split(",").map { it.trim() } + .map { it.toIntOrNull() ?: throw IllegalArgumentException("[ERROR] 로또 번호는 숫자만 입력할 수 있습니다.") } + } + + private fun checkLottoNumbersAreUnique(numbers: List) { + require(numbers.size == 6) { "[ERROR] 로또 번호는 6개여야 합니다." } + require(numbers.toSet().size == 6) { "[ERROR] 로또 번호는 중복될 수 없습니다." } + } + + private fun checkLottoNumbersRange(numbers: List) { + require(numbers.all { it in 1..45 }) { "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다." } + } + + fun validateBonusNumber(input: String): Int { + checkInputNotBlank(input) + return input.toIntOrNull()?.takeIf { it in 1..45 } + ?: throw IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.") + } + } +} diff --git a/src/test/kotlin/lotto/LottoTest.kt b/src/test/kotlin/lotto/LottoTest.kt index 122fae572..7bd42cd47 100644 --- a/src/test/kotlin/lotto/LottoTest.kt +++ b/src/test/kotlin/lotto/LottoTest.kt @@ -1,7 +1,9 @@ package lotto -import org.junit.jupiter.api.Test +import camp.nextstep.edu.missionutils.test.Assertions.assertRandomUniqueNumbersInRangeTest +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.Test class LottoTest { @Test @@ -11,7 +13,6 @@ class LottoTest { } } - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 @Test fun `로또 번호에 중복된 숫자가 있으면 예외가 발생한다`() { assertThrows { @@ -19,5 +20,31 @@ class LottoTest { } } - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + @Test + fun `로또 번호의 숫자 범위를 초과한 경우(1 ~ 45)`() { + assertThrows { + Lotto(listOf(1, 2, 3, 4, 5, 46)) + Lotto(listOf(0, 2, 3, 4, 5, 6)) + Lotto(listOf(-1, 2, 3, 4, 5, 6)) + } + } + +// @Test +// fun `구입한 만큼의 로또 발행하기`() { +// val lotto = Lotto(listOf(1, 2, 3, 4, 5, 6)) +// assertRandomUniqueNumbersInRangeTest( +// { +// assertThat(lotto.issueLotto(3)).isEqualTo( +// listOf( +// listOf(8, 21, 23, 41, 42, 43), +// listOf(3, 5, 11, 16, 32, 38), +// listOf(7, 11, 16, 35, 36, 44) +// ) +// ) +// }, +// listOf(8, 21, 23, 41, 42, 43), +// listOf(3, 5, 11, 16, 32, 38), +// listOf(7, 11, 16, 35, 36, 44) +// ) +// } }