diff --git a/README.md b/README.md index 31ebdafabb..63888deb49 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # kotlin-lotto +[X]로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다. +[X]로또 1장의 가격은 1000원이다. +[X]로또의 숫자는 6개이다. +[X]로또는 당첨 번호는 중복돼서는 안된다. +[X]로또의 수익률을 마지막에 구해주어야한다. (구매금액) / (당첨금액) +[X]로또의 숫자는 1~45까지이다. +[X]로또의 당첨 횟수를 각각 구해야한다. + 요구사항 [X]쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환 (예: “” => 0, "1,2" => 3, " 1,2,3" => 6, “1,2:3” => 6) diff --git a/src/main/kotlin/lottery/controller/LotteryGame.kt b/src/main/kotlin/lottery/controller/LotteryGame.kt new file mode 100644 index 0000000000..c9a4974136 --- /dev/null +++ b/src/main/kotlin/lottery/controller/LotteryGame.kt @@ -0,0 +1,40 @@ +package lottery.controller + +import lottery.domain.Lotteries +import lottery.domain.Lottery +import lottery.domain.LotteryNumber +import lottery.domain.LotteryPrize +import lottery.domain.LotteryRank +import lottery.domain.WinningLottery +import lottery.view.LotteryGameView + +class LotteryGame { + private lateinit var winningLottery: WinningLottery + private val lotteryRank = LotteryRank() + fun purchaseAutoLotteries(purchasePrice: Int): Lotteries { + return Lotteries.makeAutoLotteries(purchasePrice / Lottery.LOTTERY_PRICE) + } + + fun start() { + LotteryGameView.printPurchaseMoneyView() + val money = readln().toInt() + val purchaseAutoLotteries = purchaseAutoLotteries(money) + LotteryGameView.printPurchaseLotteryView(purchaseAutoLotteries.lotteries.size) + LotteryGameView.printLotteriesNumber(purchaseAutoLotteries) + LotteryGameView.printWinnerLotteryNumber() + val numbers = readln().replace("\\s".toRegex(), "").split(",") + winningLottery = WinningLottery(numbers.map { LotteryNumber.get(it.toInt()) }.toSet()) + + purchaseAutoLotteries.lotteries.forEach { + val correctCount = it.checkCorrectCount(winningLottery.winningNumbers) + lotteryRank.plusRank(LotteryPrize.get(correctCount) ?: LotteryPrize.NONE) + } + LotteryGameView.printLotteryRankView(lotteryRank) + LotteryGameView.printProfitView(lotteryRank.calculateProfit(money)) + } +} + +fun main() { + val game = LotteryGame() + game.start() +} diff --git a/src/main/kotlin/lottery/domain/Lotteries.kt b/src/main/kotlin/lottery/domain/Lotteries.kt new file mode 100644 index 0000000000..7274da2a75 --- /dev/null +++ b/src/main/kotlin/lottery/domain/Lotteries.kt @@ -0,0 +1,15 @@ +package lottery.domain + +class Lotteries { + val lotteries = mutableListOf() + + companion object { + fun makeAutoLotteries(number: Int): Lotteries { + val lotteries = Lotteries() + for (i in 0 until number) { + lotteries.lotteries.add(Lottery.makeAutoLottery()) + } + return lotteries + } + } +} diff --git a/src/main/kotlin/lottery/domain/Lottery.kt b/src/main/kotlin/lottery/domain/Lottery.kt new file mode 100644 index 0000000000..1bb5246ea5 --- /dev/null +++ b/src/main/kotlin/lottery/domain/Lottery.kt @@ -0,0 +1,30 @@ +package lottery.domain + +class Lottery(numbers: Set) { + val lotteryNumbers: Set + + init { + require(!hasDuplicatedLotteryNumbers(numbers)) { "로또 번호에 중복되는 숫자가 있습니다." } + lotteryNumbers = numbers + } + + private fun hasDuplicatedLotteryNumbers(numbers: Set): Boolean { + return numbers.size != LOTTERY_NUMBER_SIZE + } + + fun checkCorrectCount(numbers: Set): Int { + return numbers.count { it in lotteryNumbers } + } + + companion object { + private val BASE_NUMBERS = (LotteryNumber.MIN_LOTTERY_NUMBER..LotteryNumber.MAX_LOTTERY_NUMBER).toSet() + const val LOTTERY_NUMBER_SIZE = 6 + const val LOTTERY_PRICE = 1000 + fun makeAutoLottery(): Lottery { + return Lottery( + BASE_NUMBERS.shuffled().take(LOTTERY_NUMBER_SIZE).sorted().map { LotteryNumber.get(it) } + .toSet() + ) + } + } +} diff --git a/src/main/kotlin/lottery/domain/LotteryNumber.kt b/src/main/kotlin/lottery/domain/LotteryNumber.kt new file mode 100644 index 0000000000..e276766975 --- /dev/null +++ b/src/main/kotlin/lottery/domain/LotteryNumber.kt @@ -0,0 +1,22 @@ +package lottery.domain + +@JvmInline +value class LotteryNumber private constructor( + private val number: Int, +) { + init { + require(number >= MIN_LOTTERY_NUMBER) { "로또 번호는 1이상여야 합니다." } + require(number <= MAX_LOTTERY_NUMBER) { "로또 번호는 45이하이여야 합니다." } + } + + companion object { + fun get(number: Int) = LotteryNumber(number) + + const val MAX_LOTTERY_NUMBER = 45 + const val MIN_LOTTERY_NUMBER = 1 + } + + override fun toString(): String { + return "$number" + } +} diff --git a/src/main/kotlin/lottery/domain/LotteryPrize.kt b/src/main/kotlin/lottery/domain/LotteryPrize.kt new file mode 100644 index 0000000000..475dfff2e7 --- /dev/null +++ b/src/main/kotlin/lottery/domain/LotteryPrize.kt @@ -0,0 +1,15 @@ +package lottery.domain + +enum class LotteryPrize(val correctCount: Int, val rewardMoney: Int) { + NONE(0, 0), + FORTH(3, 5_000), + THIRD(4, 50_000), + SECOND(5, 1_500_000), + FIRST(6, 2_000_000_000), ; + + companion object { + fun get(correctCount: Int): LotteryPrize? { + return LotteryPrize.values().find { it.correctCount == correctCount } + } + } +} diff --git a/src/main/kotlin/lottery/domain/LotteryRank.kt b/src/main/kotlin/lottery/domain/LotteryRank.kt new file mode 100644 index 0000000000..16bef685f9 --- /dev/null +++ b/src/main/kotlin/lottery/domain/LotteryRank.kt @@ -0,0 +1,20 @@ +package lottery.domain + +class LotteryRank { + val lotteriesRank = LotteryPrize.values().associateWith { RANK_DEFAULT_VALUE }.toMutableMap() + + fun plusRank(rank: LotteryPrize) { + lotteriesRank[rank] = lotteriesRank.getOrDefault(rank, 0) + 1 + } + + fun calculateProfit(money: Int): Double { + val total = lotteriesRank.map { (prize, count) -> + prize.rewardMoney * count + }.sumOf { it }.toDouble() + return total / money + } + + companion object { + const val RANK_DEFAULT_VALUE = 0 + } +} diff --git a/src/main/kotlin/lottery/domain/WinningLottery.kt b/src/main/kotlin/lottery/domain/WinningLottery.kt new file mode 100644 index 0000000000..4ecadcb35c --- /dev/null +++ b/src/main/kotlin/lottery/domain/WinningLottery.kt @@ -0,0 +1,14 @@ +package lottery.domain + +class WinningLottery(numbers: Set) { + val winningNumbers: Set + + init { + require(!hasDuplicatedLotteryNumbers(numbers)) { "로또 번호에 중복되는 숫자가 있습니다." } + winningNumbers = numbers + } + + private fun hasDuplicatedLotteryNumbers(numbers: Set): Boolean { + return numbers.size != Lottery.LOTTERY_NUMBER_SIZE + } +} diff --git a/src/main/kotlin/lottery/view/LotteryGameView.kt b/src/main/kotlin/lottery/view/LotteryGameView.kt new file mode 100644 index 0000000000..dd15dff146 --- /dev/null +++ b/src/main/kotlin/lottery/view/LotteryGameView.kt @@ -0,0 +1,39 @@ +package lottery.view + +import lottery.domain.Lotteries +import lottery.domain.LotteryPrize +import lottery.domain.LotteryRank + +object LotteryGameView { + + fun printPurchaseMoneyView() { + println("구입 금액을 입력해 주세요.") + } + + fun printPurchaseLotteryView(number: Int) { + println("${number}개를 구매했습니다.") + } + + fun printLotteriesNumber(lotteries: Lotteries) { + lotteries.lotteries.forEach { + println(it.lotteryNumbers) + } + println() + } + + fun printWinnerLotteryNumber() { + println("지난 주 당첨 번호를 입력해 주세요.") + } + + fun printLotteryRankView(lotteryRank: LotteryRank) { + println("당첨 통계") + println("---------") + lotteryRank.lotteriesRank.filter { it.key != LotteryPrize.NONE }.forEach { (prize, count) -> + println("${prize.correctCount}개 일치 (${prize.rewardMoney}원)- ${count}개") + } + } + + fun printProfitView(profit: Double) { + println("총 수익률은" + String.format("%.2f", profit) + " 입니다.") + } +} diff --git a/src/main/kotlin/stringcalculator/Delimiters.kt b/src/main/kotlin/stringcalculator/Delimiters.kt index 3fc31b9eb5..9e4bc60b01 100644 --- a/src/main/kotlin/stringcalculator/Delimiters.kt +++ b/src/main/kotlin/stringcalculator/Delimiters.kt @@ -1,11 +1,36 @@ package stringcalculator -class Delimiters { - val delimiters = mutableListOf(DEFAULT_DELIMITER_COMMA, DEFAULT_DELIMITER_COLON) +import stringcalculator.Delimiters.Companion.DEFAULT_DELIMITER_COLON +import stringcalculator.Delimiters.Companion.DEFAULT_DELIMITER_COMMA - fun addDelimiter(delimiter: String) { - delimiters.add(delimiter) +object StringParser { + private const val CUSTOM_DELIMITER_INDEX = 1 + private const val WITHOUT_CUSTOM_DELIMITER_EXPRESSION_INDEX = 2 + + fun getDelimitersFromString(text: String): Delimiters { + val matchResult = Regex(Delimiters.CUSTOM_DELIMITER_FIND_REGEX).find(text) + matchResult?.let { + return Delimiters( + listOf( + DEFAULT_DELIMITER_COMMA, + DEFAULT_DELIMITER_COLON, + it.groupValues[CUSTOM_DELIMITER_INDEX] + ) + ) + } + return Delimiters() + } + + fun deleteCustomDelimiters(text: String): String { + val matchResult = Regex(Delimiters.CUSTOM_DELIMITER_FIND_REGEX).find(text) + matchResult?.let { + return it.groupValues[WITHOUT_CUSTOM_DELIMITER_EXPRESSION_INDEX] + } + return text } +} + +class Delimiters(val delimiters: List = listOf(DEFAULT_DELIMITER_COMMA, DEFAULT_DELIMITER_COLON)) { fun getDelimitersRegex(): Regex { return delimiters.joinToString("", "[", "]").toRegex() @@ -14,6 +39,6 @@ class Delimiters { companion object { const val DEFAULT_DELIMITER_COMMA = "," const val DEFAULT_DELIMITER_COLON = ":" - const val CUSTOM_DELIMITER_FIND_REGEX = "//(.)\n(.*)" + const val CUSTOM_DELIMITER_FIND_REGEX = "//(.*)\n(.*)" } } diff --git a/src/main/kotlin/stringcalculator/StringAddCalculator.kt b/src/main/kotlin/stringcalculator/StringAddCalculator.kt index 9cb9042129..277e1a0658 100644 --- a/src/main/kotlin/stringcalculator/StringAddCalculator.kt +++ b/src/main/kotlin/stringcalculator/StringAddCalculator.kt @@ -1,10 +1,13 @@ package stringcalculator class StringAddCalculator { - private val delimiters = Delimiters() + private lateinit var delimiters: Delimiters + fun calculate(text: String?): Int { if (text.isNullOrEmpty()) return 0 - val parseText = StringParser.deleteCustomDelimiters(text, delimiters) + delimiters = StringParser.getDelimitersFromString(text) + val parseText = StringParser.deleteCustomDelimiters(text) + return parseText.split(delimiters.getDelimitersRegex()).sumOf { if (it.toInt() < 0) throw RuntimeException() it.toInt() diff --git a/src/main/kotlin/stringcalculator/StringParser.kt b/src/main/kotlin/stringcalculator/StringParser.kt deleted file mode 100644 index 7e399f9173..0000000000 --- a/src/main/kotlin/stringcalculator/StringParser.kt +++ /dev/null @@ -1,16 +0,0 @@ -package stringcalculator - -class StringParser { - companion object { - private const val CUSTOM_DELIMITER_INDEX = 1 - private const val WITHOUT_CUSTOM_DELIMITER_EXPRESSION_INDEX = 2 - fun deleteCustomDelimiters(text: String, delimiters: Delimiters): String { - val matchResult = Regex(Delimiters.CUSTOM_DELIMITER_FIND_REGEX).find(text) - matchResult?.let { - delimiters.addDelimiter(it.groupValues[CUSTOM_DELIMITER_INDEX]) - return it.groupValues[WITHOUT_CUSTOM_DELIMITER_EXPRESSION_INDEX] - } - return text - } - } -} diff --git a/src/test/kotlin/lottery/controller/LotteryGameTest.kt b/src/test/kotlin/lottery/controller/LotteryGameTest.kt new file mode 100644 index 0000000000..50163b4f3a --- /dev/null +++ b/src/test/kotlin/lottery/controller/LotteryGameTest.kt @@ -0,0 +1,12 @@ +package lottery.controller + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class LotteryGameTest : StringSpec({ + "로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다." { + val lotteryGame = LotteryGame() + val purchaseLotteries = lotteryGame.purchaseAutoLotteries(14000) + purchaseLotteries.lotteries.size shouldBe 14 + } +}) diff --git a/src/test/kotlin/lottery/domain/LotteryNumberTest.kt b/src/test/kotlin/lottery/domain/LotteryNumberTest.kt new file mode 100644 index 0000000000..dbdd2f3fde --- /dev/null +++ b/src/test/kotlin/lottery/domain/LotteryNumberTest.kt @@ -0,0 +1,18 @@ +package lottery.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec + +class LotteryNumberTest : StringSpec({ + "로또 숫자의 크기는 1이상 이여야한다." { + shouldThrow { + LotteryNumber.get(0) + } + } + + "로또 숫자의 크기는 45이하여야 한다." { + shouldThrow { + LotteryNumber.get(46) + } + } +}) diff --git a/src/test/kotlin/lottery/domain/LotteryPrizeTest.kt b/src/test/kotlin/lottery/domain/LotteryPrizeTest.kt new file mode 100644 index 0000000000..b9a4f57f89 --- /dev/null +++ b/src/test/kotlin/lottery/domain/LotteryPrizeTest.kt @@ -0,0 +1,13 @@ +package lottery.domain + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class LotteryPrizeTest : StringSpec({ + "correctCount 갯수에 해당하는 LotteryPrize 를 가져와야한다." { + LotteryPrize.get(correctCount = 3) shouldBe LotteryPrize.FORTH + LotteryPrize.get(correctCount = 4) shouldBe LotteryPrize.THIRD + LotteryPrize.get(correctCount = 5) shouldBe LotteryPrize.SECOND + LotteryPrize.get(correctCount = 6) shouldBe LotteryPrize.FIRST + } +}) diff --git a/src/test/kotlin/lottery/domain/LotteryRankTest.kt b/src/test/kotlin/lottery/domain/LotteryRankTest.kt new file mode 100644 index 0000000000..6cce1cdd0c --- /dev/null +++ b/src/test/kotlin/lottery/domain/LotteryRankTest.kt @@ -0,0 +1,20 @@ +package lottery.domain + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class LotteryRankTest : StringSpec({ + "plusRank 호출시 해당하는 로또 등수를 올려주어야한다." { + val lotteryRank = LotteryRank() + lotteryRank.lotteriesRank[LotteryPrize.THIRD] shouldBe 0 + lotteryRank.plusRank(LotteryPrize.THIRD) + lotteryRank.lotteriesRank[LotteryPrize.THIRD] shouldBe 1 + } + + "로또 구매 금액과 수익금으로 수익률(금액 / 구매금액)을 계산해 주어야한다." { + val lotteryRank = LotteryRank() + lotteryRank.calculateProfit(1000) shouldBe 0 + lotteryRank.plusRank(LotteryPrize.FORTH) + lotteryRank.calculateProfit(1000) shouldBe 5 + } +}) diff --git a/src/test/kotlin/lottery/domain/LotteryTest.kt b/src/test/kotlin/lottery/domain/LotteryTest.kt new file mode 100644 index 0000000000..8c62d4a394 --- /dev/null +++ b/src/test/kotlin/lottery/domain/LotteryTest.kt @@ -0,0 +1,31 @@ +package lottery.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class LotteryTest : StringSpec({ + "자동 로또 생성을 할 경우의 숫자의 개수는 6개이다." { + val lottery = Lottery.makeAutoLottery() + lottery.lotteryNumbers.size shouldBe 6 + } + + "로또는 번호는 중복돼서는 안된다." { + val duplicatedNumbers = listOf(1, 2, 3, 1, 4, 5) + shouldThrow { + Lottery(duplicatedNumbers.map { LotteryNumber.get(it) }.toSet()) + } + } + + "로또 한장의 금액은 1000원 이다." { + Lottery.LOTTERY_PRICE shouldBe 1000 + } + + "로또 두개를 비교했을때 맞는 개수를 정확하게 가져와야한다." { + val numbers = listOf(1, 2, 3, 4, 5, 6) + val winningNumbers = listOf(1, 2, 3, 8, 9, 10) + val lottery = Lottery(numbers.map { LotteryNumber.get(it) }.toSet()) + val winningLottery = WinningLottery(winningNumbers.map { LotteryNumber.get(it) }.toSet()) + lottery.checkCorrectCount(winningLottery.winningNumbers) shouldBe 3 + } +}) diff --git a/src/test/kotlin/lottery/domain/WinningLotteryTest.kt b/src/test/kotlin/lottery/domain/WinningLotteryTest.kt new file mode 100644 index 0000000000..7d1137df29 --- /dev/null +++ b/src/test/kotlin/lottery/domain/WinningLotteryTest.kt @@ -0,0 +1,13 @@ +package lottery.domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec + +class WinningLotteryTest : StringSpec({ + "당첨 번호는 중복되어서는 안된다." { + val duplicatedNumbers = listOf(1, 2, 3, 1, 4, 5) + shouldThrow { + WinningLottery(duplicatedNumbers.map { LotteryNumber.get(it) }.toSet()) + } + } +}) diff --git a/src/test/kotlin/stringcalculator/DelimitersTest.kt b/src/test/kotlin/stringcalculator/DelimitersTest.kt index 2b2ff192b5..c408b90b56 100644 --- a/src/test/kotlin/stringcalculator/DelimitersTest.kt +++ b/src/test/kotlin/stringcalculator/DelimitersTest.kt @@ -1,8 +1,6 @@ package stringcalculator import io.kotest.core.spec.style.StringSpec -import io.kotest.inspectors.forAll -import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe @@ -11,20 +9,8 @@ class DelimitersTest : StringSpec({ val delimiters = Delimiters() delimiters.delimiters shouldContainAll listOf(",", ":") } - "새로운 커스텀한 구분자를 추가할 수 있다." { - val delimiters = Delimiters() - delimiters.addDelimiter("&&") - delimiters.delimiters shouldContain "&&" - } "구분자들을 통해 정규식을 만들어준다." { val delimiters = Delimiters() delimiters.getDelimitersRegex() shouldBe "[,:]".toRegex() } - "커스텀한 구분자를 추가할 경우 정규식에 포함된다." { - listOf("#", "?", "^^").forAll { - val delimiters = Delimiters() - delimiters.addDelimiter(it) - delimiters.getDelimitersRegex() shouldBe "[,:$it]".toRegex() - } - } }) diff --git a/src/test/kotlin/stringcalculator/StringParserTest.kt b/src/test/kotlin/stringcalculator/StringParserTest.kt new file mode 100644 index 0000000000..1f2f5ac7ec --- /dev/null +++ b/src/test/kotlin/stringcalculator/StringParserTest.kt @@ -0,0 +1,11 @@ +package stringcalculator + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContain + +class StringParserTest : StringSpec({ + "//{구분자}\n 사이에 들어온 문자열은 커스텀한 구분자에 추가된다.." { + val delimiters = StringParser.getDelimitersFromString("//^^\n1^^2") + delimiters.delimiters shouldContain "^^" + } +})