diff --git a/README.md b/README.md index 95d7786fe..8f0fbdb08 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,32 @@ - 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외 - Enum 클래스를 적용해 프로그래밍을 구현한다. - 일급 컬렉션을 쓴다. + + +### 4단계 로또 수동 기능구현 목록 +- 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다. +- 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다. + - 입력값 받는 순서: + - 구매금액을 입력해주세요 + - 수동으로 구매할 로또 수를 입력해주세요 + - 수동으로 구매할 번호를 입력해주세요 + - 수동으로 3장, 자동으로 11장을 구매했습니다 + - 지난주 당첨 번호를 입력해주세요 + - 보너스볼을 입력해주세요 + +### 4단계 로또 수동 프로그래밍 요구사항 +- 모든 원시값과 문자열을 포장한다. +- 예외 처리를 통해 에러가 발생하지 않도록 한다. + +### 4단계 힌트 +모든 원시값과 문자열을 포장한다. +로또 숫자 하나는 Int다. 이 숫자 하나를 추상화한 LottoNumber를 추가해 구현한다. +예외 처리를 통해 에러가 발생하지 않도록 한다. + + +참고로 코틀린에서는 아래와 같이 예외 처리를 한다. 장기적으로는 아래와 같이 예외 처리하는 걸 연습해 본다. + +- 논리적인 오류일 때만 예외를 던진다. +- 논리적인 오류가 아니면 예외를 던지지 말고 null을 반환한다. +- 실패하는 경우가 복잡해서 null로 처리할 수 없으면 sealed class를 반환한다. +- 일반적인 코틀린 코드에서 try-catch를 사용하지 않는다. --> 왜 ? \ No newline at end of file diff --git a/src/main/kotlin/lotto/controller/LottoController.kt b/src/main/kotlin/lotto/controller/LottoController.kt index b88c93eb4..4dd3c1a0c 100644 --- a/src/main/kotlin/lotto/controller/LottoController.kt +++ b/src/main/kotlin/lotto/controller/LottoController.kt @@ -3,8 +3,8 @@ package lotto.controller import lotto.domain.LottoCalculator import lotto.domain.LottoFactory import lotto.domain.LottoResult -import lotto.domain.model.Lotto -import lotto.domain.model.LottoNumber +import lotto.domain.Lotto +import lotto.domain.LottoNumber import lotto.view.InputView import lotto.view.ResultView import java.math.BigDecimal @@ -23,8 +23,8 @@ object LottoController { // 총 구매 로또 개수 ResultView.printTotalPurchaseCount(totalLottoCount) + val myLottoList = createMyLottoList(totalLottoCount) // 나의 로또 번호 리스트 출력 - val myLottoList = LottoFactory(totalLottoCount).createLottoList() ResultView.printLottoNumbers(myLottoList) // 결과 출력 @@ -32,22 +32,33 @@ object LottoController { ResultView.printLottoResult(resultMap = result.resultMap) ResultView.printProfitRate( profitRate = - LottoCalculator.calculateProfitRate( - result.getTotalProfit(), - totalPurchaseAmount, - ), + LottoCalculator.calculateProfitRate( + result.getTotalProfit(), + totalPurchaseAmount, + ), ) } private fun getLottoResult(myLottoList: List): LottoResult { - val winningNumbers = InputView.readWinningLotto(InputView.ENTER_LAST_WINNING_NUMBER) - val bonusLottoNumber = InputView.readBonusLotto(InputView.ENTER_BONUS_BALL) + val winningNumbers = InputView.readWinningLotto() + val bonusLottoNumber = InputView.readBonusLotto() val result = LottoResult( winningLotto = LottoFactory.fromList(winningNumbers), bonusLottoNumber = LottoNumber(bonusLottoNumber), - myLottoList = myLottoList, + myLottos = myLottoList, ) return result } + + private fun createMyLottoList(totalLottoCount: Int): List { + val manualLottoCount = InputView.readManualLottoCount() + val manualLottoNumberList = InputView.readManualLottoList(manualLottoCount) + + val myLottoList = LottoFactory( + autoGeneratedLottoCount = totalLottoCount - manualLottoCount, + manualLottoList = manualLottoNumberList + ).createLottoList() + return myLottoList + } } diff --git a/src/main/kotlin/lotto/domain/model/Lotto.kt b/src/main/kotlin/lotto/domain/Lotto.kt similarity index 94% rename from src/main/kotlin/lotto/domain/model/Lotto.kt rename to src/main/kotlin/lotto/domain/Lotto.kt index e378d1d78..d39b24f5b 100644 --- a/src/main/kotlin/lotto/domain/model/Lotto.kt +++ b/src/main/kotlin/lotto/domain/Lotto.kt @@ -1,4 +1,4 @@ -package lotto.domain.model +package lotto.domain @JvmInline value class Lotto(val value: List) { diff --git a/src/main/kotlin/lotto/domain/LottoFactory.kt b/src/main/kotlin/lotto/domain/LottoFactory.kt index 3e8e269be..cebad20e7 100644 --- a/src/main/kotlin/lotto/domain/LottoFactory.kt +++ b/src/main/kotlin/lotto/domain/LottoFactory.kt @@ -1,16 +1,23 @@ package lotto.domain -import lotto.domain.model.Lotto -import lotto.domain.model.LottoNumber import lotto.util.NumberGenerator import lotto.util.RandomNumberGenerator class LottoFactory( - private val totalLottoCount: Int, + private val autoGeneratedLottoCount: Int, + private val manualLottoList: List>, private val numberGenerator: NumberGenerator = RandomNumberGenerator(), ) { fun createLottoList(): List { - return List(totalLottoCount) { + return buildList { + addAll(manualLottoList.map { fromList(it) }) + addAll(autoGeneratedLottoList()) + } + } + + private fun autoGeneratedLottoList(): List { + check(autoGeneratedLottoCount > 0) { "수동으로 생성하는 로또의 개수는 전체 로또 수를 초과할 수 없습니다." } + return List(autoGeneratedLottoCount) { numberGenerator .getNumbers(Lotto.LOTTO_COUNT) .let { list -> Lotto(list.map { LottoNumber.from(it) }) } diff --git a/src/main/kotlin/lotto/domain/model/LottoNumber.kt b/src/main/kotlin/lotto/domain/LottoNumber.kt similarity index 95% rename from src/main/kotlin/lotto/domain/model/LottoNumber.kt rename to src/main/kotlin/lotto/domain/LottoNumber.kt index 59d2313f0..957a119f6 100644 --- a/src/main/kotlin/lotto/domain/model/LottoNumber.kt +++ b/src/main/kotlin/lotto/domain/LottoNumber.kt @@ -1,4 +1,4 @@ -package lotto.domain.model +package lotto.domain @JvmInline value class LottoNumber(val value: Int) { diff --git a/src/main/kotlin/lotto/domain/LottoResult.kt b/src/main/kotlin/lotto/domain/LottoResult.kt index 006e8e371..054ce8217 100644 --- a/src/main/kotlin/lotto/domain/LottoResult.kt +++ b/src/main/kotlin/lotto/domain/LottoResult.kt @@ -1,20 +1,17 @@ package lotto.domain -import lotto.domain.model.Lotto -import lotto.domain.model.LottoNumber -import lotto.domain.model.Rank import java.math.BigDecimal class LottoResult( private val winningLotto: Lotto, private val bonusLottoNumber: LottoNumber, - myLottoList: List, + myLottos: List, ) { val resultMap: Map init { resultMap = Rank.entries.associateWith { 0 }.toMutableMap() - myLottoList.forEach { lotto -> + myLottos.forEach { lotto -> getRank(lotto)?.let { rank -> var rankCount = resultMap.getOrDefault(rank, 0) resultMap[rank] = ++rankCount diff --git a/src/main/kotlin/lotto/domain/model/Rank.kt b/src/main/kotlin/lotto/domain/Rank.kt similarity index 87% rename from src/main/kotlin/lotto/domain/model/Rank.kt rename to src/main/kotlin/lotto/domain/Rank.kt index 34abc1d88..93549946f 100644 --- a/src/main/kotlin/lotto/domain/model/Rank.kt +++ b/src/main/kotlin/lotto/domain/Rank.kt @@ -1,4 +1,4 @@ -package lotto.domain.model +package lotto.domain import java.math.BigDecimal @@ -14,14 +14,13 @@ enum class Rank(val prizeMoney: BigDecimal, val matchingNumberCount: Int) { fun fromMatchCount( matchCount: Int, isMatchBonus: Boolean, - ): Rank { + ): Rank? { return if (isMatchBonus && matchCount == 5) { SECOND } else if (matchCount == 5) { THIRD } else { entries.find { matchCount == it.matchingNumberCount } - ?: throw IllegalArgumentException("invalid count $matchCount") } } diff --git a/src/main/kotlin/lotto/view/InputView.kt b/src/main/kotlin/lotto/view/InputView.kt index 1320daa41..49c12722f 100644 --- a/src/main/kotlin/lotto/view/InputView.kt +++ b/src/main/kotlin/lotto/view/InputView.kt @@ -4,23 +4,50 @@ import java.math.BigDecimal object InputView { fun readTotalPurchaseAmountAsBigDecimal(): BigDecimal { - return readlnOrNull()?.trim()?.toBigDecimalOrNull() ?: BigDecimal.ZERO + return readlnOrNull()?.trim()?.toBigDecimal() ?: throw IllegalStateException("유효한 원화 형식이 아닙니다") } - fun readWinningLotto(promptMessage: String): List { - println(promptMessage) - val winningLotto = readlnOrNull()?.split(",")?.map { it.trim().toIntOrNull() ?: 0 } + fun readWinningLotto(): List { + println(ENTER_LAST_WINNING_NUMBER) + val winningLotto = readlnOrNull()?.split(",")?.map { it.trim().toInt() } require(winningLotto?.size == LOTTO_SIZE) { "Lotto size must be 6" } return winningLotto ?: throw IllegalStateException("Separate Lotto numbers by comma (,)") } - fun readBonusLotto(promptMessage: String): Int { - println(promptMessage) - val bonusLottoNumber = readlnOrNull()?.trim()?.toIntOrNull() ?: 0 // single number + fun readBonusLotto(): Int { + println(ENTER_BONUS_BALL) + val bonusLottoNumber = readlnOrNull()?.trim()?.toInt() ?: throw IllegalStateException("정수를 입력해 주세요") return bonusLottoNumber } - const val ENTER_LAST_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요." - const val ENTER_BONUS_BALL = "보너스 볼을 입력해 주세요." + fun readManualLottoCount(): Int { + println(ENTER_MANUAL_LOTTO_COUNT) + val manualLottoCount = readlnOrNull()?.trim()?.toInt() ?: throw IllegalStateException("정수를 입력해 주세요") + return manualLottoCount + } + + fun readManualLottoList(count: Int): List> { + val manualLottoNumbers = mutableListOf>() + println(ENTER_MANUAL_LOTTO) + repeat(count) { + manualLottoNumbers.add(readManualLotto()) + } + return manualLottoNumbers + } + + private fun readManualLotto(): List { + val input = + readlnOrNull()?.trim() + ?.split(INPUT_SEPARATOR_COMMA) + ?.map { it.trim().toInt() } + ?: throw IllegalStateException("Separate Lotto numbers by comma (,)") + return input + } + + private const val ENTER_LAST_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요." + private const val ENTER_BONUS_BALL = "보너스 볼을 입력해 주세요." + private const val ENTER_MANUAL_LOTTO_COUNT = "수동으로 구매할 로또 수를 입력해 주세요." + private const val ENTER_MANUAL_LOTTO = "수동으로 구매할 번호를 입력해 주세요. (예: 8, 21, 23, 41, 42, 43)" private const val LOTTO_SIZE = 6 + private const val INPUT_SEPARATOR_COMMA = "," } diff --git a/src/main/kotlin/lotto/view/ResultView.kt b/src/main/kotlin/lotto/view/ResultView.kt index fa4795727..bfc18f4d8 100644 --- a/src/main/kotlin/lotto/view/ResultView.kt +++ b/src/main/kotlin/lotto/view/ResultView.kt @@ -1,7 +1,7 @@ package lotto.view -import lotto.domain.model.Lotto -import lotto.domain.model.Rank +import lotto.domain.Lotto +import lotto.domain.Rank import java.math.BigDecimal object ResultView { diff --git a/src/test/kotlin/lotto/domain/LottoCalculatorTest.kt b/src/test/kotlin/lotto/domain/LottoCalculatorTest.kt index abb8c85c4..e3476c085 100644 --- a/src/test/kotlin/lotto/domain/LottoCalculatorTest.kt +++ b/src/test/kotlin/lotto/domain/LottoCalculatorTest.kt @@ -48,7 +48,7 @@ class LottoCalculatorTest { } @Test - fun `{given} 총 구매 금액이 0원일 경우 {when} calculateTotalProfit() {then} IllegalArgumentException 발생`() { + fun `{given} 총 구매 금액이 0원일 경우 {when} calculateProfitRate() {then} IllegalArgumentException 발생`() { assertThrows { LottoCalculator.calculateProfitRate( totalProfit = BigDecimal(10000), @@ -56,4 +56,13 @@ class LottoCalculatorTest { ) } } + + @Test + fun `{given} 총 수익이 0인 경우 {when} calculateProfitRate() {then} BigDecimal ZERO 반환`() { + val profitRate = LottoCalculator.calculateProfitRate( + totalProfit = BigDecimal.ZERO, + totalPurchaseAmount = BigDecimal(10000), + ) + assertEquals(BigDecimal.ZERO, profitRate) + } } diff --git a/src/test/kotlin/lotto/domain/LottoFactoryTest.kt b/src/test/kotlin/lotto/domain/LottoFactoryTest.kt index d39897f03..12ec177d3 100644 --- a/src/test/kotlin/lotto/domain/LottoFactoryTest.kt +++ b/src/test/kotlin/lotto/domain/LottoFactoryTest.kt @@ -3,8 +3,8 @@ package lotto.domain import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import lotto.domain.model.LottoNumber import lotto.util.NumberGenerator +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows class LottoFactoryTest : StringSpec({ @@ -15,7 +15,8 @@ class LottoFactoryTest : StringSpec({ val fakeNumberGenerator = FakeRandomNumberGenerator(expectedLottoNumbers) val lottoFactory = LottoFactory( - totalLottoCount = totalLottoCount, + autoGeneratedLottoCount = totalLottoCount, + manualLottoList = emptyList(), numberGenerator = fakeNumberGenerator, ) @@ -35,7 +36,8 @@ class LottoFactoryTest : StringSpec({ val fakeNumberGenerator = FakeRandomNumberGenerator(expectedLottoNumbers) val lottoFactory = LottoFactory( - totalLottoCount = 5, + autoGeneratedLottoCount = 5, + manualLottoList = emptyList(), numberGenerator = fakeNumberGenerator, ) @@ -49,7 +51,7 @@ class LottoFactoryTest : StringSpec({ "{given} 정수 리스트, size=6, {when} LottoFactory.fromList() {then} Lotto 생성" { val list = listOf(1, 2, 3, 4, 5, 6) val result = LottoFactory.fromList(list) - // assertTrue(result is Lotto) FIXME : this check is useless ? + assertThat(result.value).containsExactlyElementsOf(list.map { LottoNumber(it) }) } "{given} 정수 리스트, size=10, {when} LottoFactory.fromList() {then} IllegalArgumentException 발생" { diff --git a/src/test/kotlin/lotto/domain/model/LottoNumberTest.kt b/src/test/kotlin/lotto/domain/LottoNumberTest.kt similarity index 94% rename from src/test/kotlin/lotto/domain/model/LottoNumberTest.kt rename to src/test/kotlin/lotto/domain/LottoNumberTest.kt index c4a526bc5..2ce10d9bd 100644 --- a/src/test/kotlin/lotto/domain/model/LottoNumberTest.kt +++ b/src/test/kotlin/lotto/domain/LottoNumberTest.kt @@ -1,4 +1,4 @@ -package lotto.domain.model +package lotto.domain import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/lotto/domain/LottoResultTest.kt b/src/test/kotlin/lotto/domain/LottoResultTest.kt index d3213833f..e04fed6eb 100644 --- a/src/test/kotlin/lotto/domain/LottoResultTest.kt +++ b/src/test/kotlin/lotto/domain/LottoResultTest.kt @@ -1,10 +1,8 @@ package lotto.domain import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainOnly import io.kotest.matchers.shouldBe -import lotto.domain.model.Lotto -import lotto.domain.model.LottoNumber -import lotto.domain.model.Rank class LottoResultTest : StringSpec({ "로또 등수와 결과 카운트 검증" { @@ -22,6 +20,20 @@ class LottoResultTest : StringSpec({ results[Rank.FIFTH] shouldBe 1 } + "일치하는 로또 번호가 없다면 resultMap 의 값들은 0 이다" { + // Given + val lottoResultNoMatch = LottoResult( + winningLotto = Lotto(listOf(1, 2, 3, 4, 5, 6).map { LottoNumber(it) }), + bonusLottoNumber = LottoNumber(7), + myLottos = listOf( + Lotto(listOf(8, 9, 10, 11, 12, 13).map { LottoNumber(it) }) + ) + ) + //when & then + val results = lottoResultNoMatch.resultMap + results.values.shouldContainOnly(0) + } + "getTotalProfit() 결과 검증" { // given val lottoResult = fakeLottoResult() @@ -54,7 +66,7 @@ private fun fakeLottoResult(): LottoResult { LottoResult( winningLotto = winningLotto, bonusLottoNumber = LottoNumber(800), - myLottoList = myLottoList, + myLottos = myLottoList, ) return lottoResult } diff --git a/src/test/kotlin/lotto/domain/LottoTest.kt b/src/test/kotlin/lotto/domain/LottoTest.kt new file mode 100644 index 000000000..4edc1a198 --- /dev/null +++ b/src/test/kotlin/lotto/domain/LottoTest.kt @@ -0,0 +1,66 @@ +package lotto.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +class LottoTest { + @Test + fun `{given} 로또 초기화 {when} LottoNumber 7개 일떄 {then} IllegalArgumentException 발생`() { + assertThrows { + Lotto(listOf(1, 2, 3, 4, 5, 6, 7).map { LottoNumber(it) }) + } + assertDoesNotThrow { + Lotto(listOf(1, 2, 3, 4, 5, 6).map { LottoNumber(it) }) + } + } + + @ParameterizedTest + @MethodSource("provideDataForCountMatchesOf") + fun `{given} Lotto {when} countMatchesOf(Lotto) {then} 일치하는 로또 개수 반환`( + lotto1: List, + lotto2: List, + expectedMatches: Int + ) { + val lotto = Lotto(lotto1.map { LottoNumber(it) }) + val otherLotto = Lotto(lotto2.map { LottoNumber(it) }) + val result = lotto.countMatchesOf(otherLotto) + assertThat(result).isEqualTo(expectedMatches) + } + + @ParameterizedTest + @MethodSource("provideDataForContainsAny") + fun `{given} Lotto {when} containsAny(LottoNumber) {then} 일치하는 로또 번호가 있다면 true 아니라면 false 반환`( + lottoNumbers: List, + numberToCheck: Int, + expectedResult: Boolean + ) { + val lotto = Lotto(lottoNumbers.map { LottoNumber(it) }) + val result = lotto.containsAny(LottoNumber(numberToCheck)) + assertThat(result).isEqualTo(expectedResult) + } + + companion object { + @JvmStatic + fun provideDataForCountMatchesOf(): List { + return listOf( + Arguments.of(listOf(1, 2, 3, 4, 5, 6), listOf(1, 2, 3, 7, 8, 9), 3), + Arguments.of(listOf(1, 2, 3, 4, 5, 6), listOf(10, 11, 12, 13, 14, 15), 0), + Arguments.of(listOf(1, 2, 3, 4, 5, 6), listOf(1, 2, 3, 4, 5, 6), 6) + ) + } + + @JvmStatic + fun provideDataForContainsAny(): List { + return listOf( + Arguments.of(listOf(1, 2, 3, 4, 5, 6), 3, true), + Arguments.of(listOf(1, 2, 3, 4, 5, 6), 7, false), + Arguments.of(listOf(10, 11, 12, 13, 14, 15), 10, true) + ) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/lotto/domain/model/RankTest.kt b/src/test/kotlin/lotto/domain/RankTest.kt similarity index 96% rename from src/test/kotlin/lotto/domain/model/RankTest.kt rename to src/test/kotlin/lotto/domain/RankTest.kt index dd5db941d..8d15ba655 100644 --- a/src/test/kotlin/lotto/domain/model/RankTest.kt +++ b/src/test/kotlin/lotto/domain/RankTest.kt @@ -1,4 +1,4 @@ -package lotto.domain.model +package lotto.domain import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test