diff --git a/README.md b/README.md index cbae739405..8fe38ac3e3 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# kotlin-lotto \ No newline at end of file +# kotlin-lottery \ No newline at end of file diff --git a/src/main/kotlin/calculator/StringAddCalculator.kt b/src/main/kotlin/calculator/StringAddCalculator.kt index 6e27952981..28e13a484c 100644 --- a/src/main/kotlin/calculator/StringAddCalculator.kt +++ b/src/main/kotlin/calculator/StringAddCalculator.kt @@ -12,6 +12,6 @@ class StringAddCalculator( } companion object { - const val DEFAULT_RESULT_VALUE = 0 + private const val DEFAULT_RESULT_VALUE = 0 } } diff --git a/src/main/kotlin/lottery/README.md b/src/main/kotlin/lottery/README.md new file mode 100644 index 0000000000..776a3de6e8 --- /dev/null +++ b/src/main/kotlin/lottery/README.md @@ -0,0 +1,19 @@ +# 요구사항 + +## 로또(자동) +* 로또 1장의 가격은 1000원이다. + +### 화면 로직 +- [x] 로또 구입 금액을 입력받을 수 있다. ("구입금액을 입력해 주세요.") +- [x] 로또 구매 결과를 표시할 수 있다. ("14개를 구매했습니다.") +- [x] 지난 주 당첨 번호를 입력받을 수 있다. ("지난 주 당첨 번호를 입력해 주세요.") + +### 비즈니스 로직 +- [x] 구입 금액 입력시 해당하는 로또를 발급할 수 있다. +- [x] 로또는 6개의 숫자를 가진다. +- [x] 로또의 각 숫자는 1 이상 45 이하다. + +- [x] 당첨 결과를 연산할 수 있다. +- [x] 당첨 결과를 표시할 수 있다. -> 당첨 통계 +- [x] 총 수익률을 계산할 수 있다. +- [x] 총 수익률을 표시할 수 있다. "총 수익률은 0.35입니다." diff --git a/src/main/kotlin/lottery/controller/LotteryController.kt b/src/main/kotlin/lottery/controller/LotteryController.kt new file mode 100644 index 0000000000..c3980d8673 --- /dev/null +++ b/src/main/kotlin/lottery/controller/LotteryController.kt @@ -0,0 +1,44 @@ +package lottery.controller + +import lottery.domain.lotto.Lotto +import lottery.domain.winningresult.WinningResult +import lottery.service.CalculatorService +import lottery.service.LottoService +import lottery.service.WinningResultService +import lottery.ui.ViewService + +class LotteryController( + private val viewService: ViewService, + private val lottoService: LottoService, + private val winningResultService: WinningResultService, + private val calculatorService: CalculatorService, +) { + fun run() { + val amount = viewService.getPurchasingAmount() + val lottos = purchaseLottos(amount) + + val result = drawWinners(lottos) + + reportRateOfReturn(amount, result) + } + + private fun purchaseLottos(amount: Long): List { + return lottoService.issue(amount).also { + viewService.showPurchasingResult(it) + } + } + + private fun drawWinners(lottos: List): WinningResult { + val winningNumber = viewService.getWinningNumber() + return winningResultService.draw(lottos, winningNumber) + .also { + viewService.showResultOfWinning(it) + } + } + + private fun reportRateOfReturn(amount: Long, result: WinningResult) { + val rateOfReturn = calculatorService.rateOfReturn(amount, result) + viewService.showRateOfReturn(rateOfReturn) + } + +} diff --git a/src/main/kotlin/lottery/domain/lotto/Lotto.kt b/src/main/kotlin/lottery/domain/lotto/Lotto.kt new file mode 100644 index 0000000000..6e37fe9c36 --- /dev/null +++ b/src/main/kotlin/lottery/domain/lotto/Lotto.kt @@ -0,0 +1,24 @@ +package lottery.domain.lotto + +class Lotto( + var numbers: List = emptyList() +) { + init { + if (numbers.isNotEmpty()) { + check(numbers.size == 6) + } else { + createNumbers() + } + } + + private fun createNumbers() { + numbers = numberRange.shuffled().subList(0, LOTTO_SLOT).sorted() + } + + override fun toString() = "Lotto:$numbers" + + companion object { + private const val LOTTO_SLOT = 6 + private val numberRange = (1..45) + } +} diff --git a/src/main/kotlin/lottery/domain/ranking/NumberOfWins.kt b/src/main/kotlin/lottery/domain/ranking/NumberOfWins.kt new file mode 100644 index 0000000000..e3fc1329a5 --- /dev/null +++ b/src/main/kotlin/lottery/domain/ranking/NumberOfWins.kt @@ -0,0 +1,3 @@ +package lottery.domain.winningresult + +typealias NumberOfWins = Int \ No newline at end of file diff --git a/src/main/kotlin/lottery/domain/ranking/Ranking.kt b/src/main/kotlin/lottery/domain/ranking/Ranking.kt new file mode 100644 index 0000000000..0bdb39faf5 --- /dev/null +++ b/src/main/kotlin/lottery/domain/ranking/Ranking.kt @@ -0,0 +1,10 @@ +package lottery.domain.ranking + +import lottery.domain.winningresult.NumberOfWins + +enum class Ranking(val rank: NumberOfWins, val prize: Int) { + THIRD(3, 5000), + FOURTH(4, 50000), + FIFTH(5, 1500000), + SIXTH(6, 2000000000) +} \ No newline at end of file diff --git a/src/main/kotlin/lottery/domain/ranking/WinningResult.kt b/src/main/kotlin/lottery/domain/ranking/WinningResult.kt new file mode 100644 index 0000000000..cb5e8b5f32 --- /dev/null +++ b/src/main/kotlin/lottery/domain/ranking/WinningResult.kt @@ -0,0 +1,3 @@ +package lottery.domain.winningresult + +typealias WinningResult = Map \ No newline at end of file diff --git a/src/main/kotlin/lottery/main.kt b/src/main/kotlin/lottery/main.kt new file mode 100644 index 0000000000..314c6b2643 --- /dev/null +++ b/src/main/kotlin/lottery/main.kt @@ -0,0 +1,26 @@ +package lottery + +import lottery.controller.LotteryController +import lottery.service.CalculatorService +import lottery.service.ExchangeService +import lottery.service.LottoService +import lottery.service.WinningResultService +import lottery.ui.InputView +import lottery.ui.ResultView +import lottery.ui.ViewService + +fun main() { + val viewService = ViewService(InputView(), ResultView()) + val lottoService = LottoService(ExchangeService()) + val winningResultService = WinningResultService() + val calculatorService = CalculatorService() + + val lotteryController = LotteryController( + viewService, + lottoService, + winningResultService, + calculatorService + ) + + lotteryController.run() +} \ No newline at end of file diff --git a/src/main/kotlin/lottery/service/CalculatorService.kt b/src/main/kotlin/lottery/service/CalculatorService.kt new file mode 100644 index 0000000000..dd47fa440c --- /dev/null +++ b/src/main/kotlin/lottery/service/CalculatorService.kt @@ -0,0 +1,22 @@ +package lottery.service + +import lottery.domain.ranking.Ranking +import lottery.domain.winningresult.WinningResult +import java.math.RoundingMode +import java.text.DecimalFormat + +class CalculatorService { + fun rateOfReturn(amount: Long, result: WinningResult): String { + val earning = Ranking.values().sumOf { + result[it.rank]!!.times(it.prize) + } + + return decimalFormat.format(earning.toDouble().div(amount.toDouble())) + } + + companion object { + private val decimalFormat = DecimalFormat("#.##").also { + it.roundingMode = RoundingMode.DOWN + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lottery/service/ExchangeService.kt b/src/main/kotlin/lottery/service/ExchangeService.kt new file mode 100644 index 0000000000..be7b8e00ff --- /dev/null +++ b/src/main/kotlin/lottery/service/ExchangeService.kt @@ -0,0 +1,14 @@ +package lottery.service + +import kotlin.math.floor + +class ExchangeService { + fun calculateQuantity(amount: Long): Int { + check(amount > LOTTO_PRICE) + return floor(amount.toDouble().div(LOTTO_PRICE)).toInt() + } + + companion object { + const val LOTTO_PRICE = 1000 + } +} \ No newline at end of file diff --git a/src/main/kotlin/lottery/service/LottoService.kt b/src/main/kotlin/lottery/service/LottoService.kt new file mode 100644 index 0000000000..47a5404840 --- /dev/null +++ b/src/main/kotlin/lottery/service/LottoService.kt @@ -0,0 +1,13 @@ +package lottery.service + +import lottery.domain.lotto.Lotto + +class LottoService( + private val exchangeService: ExchangeService +){ + + fun issue(amount: Long): List { + val quantity = exchangeService.calculateQuantity(amount) + return List(quantity) { Lotto() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/lottery/service/WinningResultService.kt b/src/main/kotlin/lottery/service/WinningResultService.kt new file mode 100644 index 0000000000..0127b0728d --- /dev/null +++ b/src/main/kotlin/lottery/service/WinningResultService.kt @@ -0,0 +1,26 @@ +package lottery.service + +import lottery.domain.lotto.Lotto +import lottery.domain.ranking.Ranking +import lottery.domain.winningresult.WinningResult + +class WinningResultService { + fun draw(lottos: List, winningNumbers: List): WinningResult { + val result = prepareResult() + + lottos.forEach { lotto -> + val numberOfWins = lotto.numbers.filter { winningNumbers.contains(it) }.size + result[numberOfWins] = result[numberOfWins]?.plus(1) ?: return@forEach + } + + return result + } + + private fun prepareResult(): MutableMap { + val result = mutableMapOf() + Ranking.values().forEach { result[it.rank] = 0 } + + return result + } + +} \ No newline at end of file diff --git a/src/main/kotlin/lottery/ui/InputView.kt b/src/main/kotlin/lottery/ui/InputView.kt new file mode 100644 index 0000000000..fc46636384 --- /dev/null +++ b/src/main/kotlin/lottery/ui/InputView.kt @@ -0,0 +1,21 @@ +package lottery.ui + +class InputView { + fun getPurchasingAmount(): Long { + println(PURCHASING_AMOUNT_MESSAGE) + return readln().toLong() + } + + fun getWinningNumber(): List { + println(GET_WINNING_NUMBER_MESSAGE) + return readln() + .split(DELIMITER) + .map { it.toInt() } + } + + companion object { + private const val PURCHASING_AMOUNT_MESSAGE = "구입금액을 입력해 주세요." + private const val GET_WINNING_NUMBER_MESSAGE = "지난 주 당첨 번호를 입력해 주세요." + private const val DELIMITER = "," + } +} diff --git a/src/main/kotlin/lottery/ui/ResultView.kt b/src/main/kotlin/lottery/ui/ResultView.kt new file mode 100644 index 0000000000..6655315dc0 --- /dev/null +++ b/src/main/kotlin/lottery/ui/ResultView.kt @@ -0,0 +1,38 @@ +package lottery.ui + +import lottery.domain.lotto.Lotto +import lottery.domain.ranking.Ranking +import lottery.domain.winningresult.WinningResult + +class ResultView { + fun showPurchasingResult(lottos: List) { + println("${lottos.size}개를 구매했습니다.") + showIssuedLottos(lottos) + } + + private fun showIssuedLottos(lottos: List) { + lottos.forEach { + println(it.numbers) + } + println("\n") + } + + fun showResultOfWinning(winningResult: WinningResult) { + val resultByRank = Ranking.values().map { + "${it.rank}개 일치 (${it.prize}원) - ${winningResult[it.rank]}개" + }.joinToString("\n") + + val statistics = buildString { + append("당첨 통계\n") + append("---------\n") + append(resultByRank) + } + + println(statistics) + } + + fun showRateOfReturn(rateOfReturn: String) { + val totalRateOfReturnMessage = "총 수익률은 ${rateOfReturn}입니다." + println(totalRateOfReturnMessage) + } +} diff --git a/src/main/kotlin/lottery/ui/ViewService.kt b/src/main/kotlin/lottery/ui/ViewService.kt new file mode 100644 index 0000000000..a113b03c99 --- /dev/null +++ b/src/main/kotlin/lottery/ui/ViewService.kt @@ -0,0 +1,30 @@ +package lottery.ui + +import lottery.domain.lotto.Lotto +import lottery.domain.winningresult.WinningResult + +class ViewService( + private val inputView: InputView, + private val resultView: ResultView, +) { + + fun getPurchasingAmount(): Long { + return inputView.getPurchasingAmount() + } + + fun getWinningNumber(): List { + return inputView.getWinningNumber() + } + + fun showPurchasingResult(lottos: List) { + return resultView.showPurchasingResult(lottos) + } + + fun showResultOfWinning(result: WinningResult) { + return resultView.showResultOfWinning(result) + } + + fun showRateOfReturn(rateOfReturn: String) { + return resultView.showRateOfReturn(rateOfReturn) + } +} diff --git a/src/test/kotlin/lottery/domain/lotto/LottoSpec.kt b/src/test/kotlin/lottery/domain/lotto/LottoSpec.kt new file mode 100644 index 0000000000..05915b0450 --- /dev/null +++ b/src/test/kotlin/lottery/domain/lotto/LottoSpec.kt @@ -0,0 +1,20 @@ +package lottery.domain.lotto + +import io.kotest.core.spec.style.StringSpec +import io.kotest.inspectors.shouldForAll +import io.kotest.matchers.collections.shouldHaveSize + +class LottoSpec : StringSpec({ + val lotto = Lotto() + + "로또는 6개의 숫자를 가진다" { + lotto.numbers shouldHaveSize 6 + } + + "로또의 각 숫자는 1 이상 45 이하다" { + lotto.numbers.shouldForAll { + it in 1..45 + } + } + +}) diff --git a/src/test/kotlin/lottery/service/CalculatorServiceSpec.kt b/src/test/kotlin/lottery/service/CalculatorServiceSpec.kt new file mode 100644 index 0000000000..7675e93ed0 --- /dev/null +++ b/src/test/kotlin/lottery/service/CalculatorServiceSpec.kt @@ -0,0 +1,27 @@ +package lottery.service + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class CalculatorServiceSpec : BehaviorSpec({ + + given("수익률 계산 서비스는") { + val calculatorService = CalculatorService() + + When("구입 금액과 로또 결과로부터") { + val amount = 14000L + val winningResult = mapOf( + 3 to 1, + 4 to 0, + 5 to 0, + 6 to 0 + ) + + val rateOfReturn = calculatorService.rateOfReturn(amount, winningResult) + + Then("수익률을 계산하여 반환한다") { + rateOfReturn shouldBe "0.35" + } + } + } +}) diff --git a/src/test/kotlin/lottery/service/ExchangeServiceSpec.kt b/src/test/kotlin/lottery/service/ExchangeServiceSpec.kt new file mode 100644 index 0000000000..58f019085b --- /dev/null +++ b/src/test/kotlin/lottery/service/ExchangeServiceSpec.kt @@ -0,0 +1,34 @@ +package lottery.service + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import java.lang.RuntimeException +import kotlin.math.floor + +class ExchangeServiceSpec : BehaviorSpec({ + + Given("교환 서비스는") { + val exchangeService = ExchangeService() + val lottoPrice = ExchangeService.LOTTO_PRICE + + When("로또 구입 금액을 받으면") { + val purchasingAmount = 5000L + val quantity = exchangeService.calculateQuantity(purchasingAmount) + + Then("구입한 로또의 수량을 반환한다") { + quantity shouldBe floor(purchasingAmount.toDouble() / lottoPrice).toInt() + } + } + + When("로또 구입 금액이 로또 금액보다 작으면") { + val purchasingAmount = 900L + + Then("예외를 던진다") { + shouldThrow { + exchangeService.calculateQuantity(purchasingAmount) + } + } + } + } +}) diff --git a/src/test/kotlin/lottery/service/LottoServiceSpec.kt b/src/test/kotlin/lottery/service/LottoServiceSpec.kt new file mode 100644 index 0000000000..059e9b5322 --- /dev/null +++ b/src/test/kotlin/lottery/service/LottoServiceSpec.kt @@ -0,0 +1,23 @@ +package lottery.service + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldHaveSize +import kotlin.math.floor + +class LottoServiceSpec : BehaviorSpec({ + + Given("로또 발행 서비스는") { + val lottoService = LottoService(ExchangeService()) + val lottoPrice = ExchangeService.LOTTO_PRICE + + When("로또 구입 금액을 받으면") { + val purchasingAmount = 10000L + val lottos = lottoService.issue(purchasingAmount) + + Then("구입 금액만큼 로또를 발급한다") { + lottos shouldHaveSize floor(purchasingAmount.toDouble() / lottoPrice).toInt() + } + } + } + +}) diff --git a/src/test/kotlin/lottery/service/WinningResultServiceSpec.kt b/src/test/kotlin/lottery/service/WinningResultServiceSpec.kt new file mode 100644 index 0000000000..150ecc1926 --- /dev/null +++ b/src/test/kotlin/lottery/service/WinningResultServiceSpec.kt @@ -0,0 +1,31 @@ +package lottery.service + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import lottery.domain.lotto.Lotto + +class WinningResultServiceSpec : BehaviorSpec({ + + Given("당첨 결과 서비스는") { + val winningResultService = WinningResultService() + + val lotto1 = Lotto(numbers = listOf(1, 2, 3, 4, 5, 6)) + val lotto2 = Lotto(numbers = listOf(1, 3, 5, 7, 9, 11)) + val lotto3 = Lotto(numbers = listOf(2, 4, 6, 8, 10, 12)) + val lotto4 = Lotto(numbers = listOf(7, 8, 9, 10, 11, 12)) + val lottos = listOf(lotto1, lotto2, lotto3, lotto4) + val winningNumbers = listOf(1, 2, 3, 4, 5, 6) + + When("로또와 당첨번호를 확인하여") { + val result = winningResultService.draw(lottos, winningNumbers) + + println("result = $result") + Then("당첨 결과를 반환한다") { + result[3] shouldBe 2 + result[4] shouldBe 0 + result[5] shouldBe 0 + result[6] shouldBe 1 + } + } + } +})