-
Notifications
You must be signed in to change notification settings - Fork 302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[2단계] 로또 구현 #578
base: cafemug
Are you sure you want to change the base?
[2단계] 로또 구현 #578
Changes from all commits
158ddd6
4f7a05b
30d6ad3
a0d6956
8efdb9d
f1f2af3
0084faf
488f6fb
7678e9c
8fcaaa0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package lotto.common | ||
|
||
sealed class ExceptionCode { | ||
object NotAllowNullOrBlank : IllegalArgumentException("Input에 Null이나 빈 값이 있으면 안됩니다") | ||
object NotMatchNumeric : IllegalArgumentException("Input이 숫자가 아닙니다") | ||
object NotFindSeparator : IllegalArgumentException("Input에 구분자 ,가 없습니다") | ||
object NotWinLotteryCount : IllegalArgumentException("당첨번호가 6자리가 아닙니다") | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package lotto.common | ||
|
||
class InputValidation { | ||
fun amountValidate(input: String?): Int { | ||
require(!input.isNullOrBlank()) { | ||
throw ExceptionCode.NotAllowNullOrBlank | ||
} | ||
|
||
require(input.matches(Regex("\\d+"))) { | ||
throw ExceptionCode.NotMatchNumeric | ||
} | ||
return input.toInt() | ||
} | ||
|
||
fun winLotteryValidation(input: String?): List<Int> { | ||
require(!input.isNullOrBlank()) { | ||
throw ExceptionCode.NotAllowNullOrBlank | ||
} | ||
|
||
require(input.matches(Regex("(.*),(.*)"))) { | ||
throw ExceptionCode.NotFindSeparator | ||
} | ||
|
||
val winLottery = input.split(",").map { it.toInt() } | ||
if (winLottery.size != 6) { | ||
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 당첨 번호도 일반 로또 게임 하나와 동일하게 6개의 숫자, 1-46까지의 숫자라는 동일한 제약 조건을 가집니다. input에서 검증하더라도 이후 로직에 따라 로또 도메인 객체에 7개의 숫자, 46이 넘는 숫자, 음수 등이 들어가 제약 조건이 깨질 수 있습니다. input단에서 검증하지 않고 로또 도메인 객체를 통해 검증해보세요. |
||
throw ExceptionCode.NotWinLotteryCount | ||
} | ||
return winLottery | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package lotto.domain | ||
|
||
import lotto.entity.Lottery | ||
import lotto.entity.WinLotteryResult | ||
|
||
class Draw { | ||
private val lotteryPrice = 1000 | ||
fun calculateBuyNum(amount: Int): Int { | ||
return amount / lotteryPrice | ||
} | ||
|
||
fun drawLotteries(num: Int): List<Lottery> { | ||
return (1..num).map { | ||
Lottery(makeShuffleNumbers()) | ||
} | ||
} | ||
|
||
fun calculateWin(winNumbers: List<Int>, lotteries: List<Lottery>): WinLotteryResult { | ||
val winLotteryResult = WinLotteryResult() | ||
winLotteryResult.apply { | ||
lotteries.forEach { checkWin(it.values.intersect(winNumbers.toSet()).size, this) } | ||
} | ||
return winLotteryResult | ||
} | ||
|
||
private fun checkWin(num: Int, winLotteryResult: WinLotteryResult) { | ||
when (num) { | ||
3 -> winLotteryResult.matchThree.matchNum += 1 | ||
4 -> winLotteryResult.matchFour.matchNum += 1 | ||
5 -> winLotteryResult.matchFive.matchNum += 1 | ||
6 -> winLotteryResult.matchSix.matchNum += 1 | ||
Comment on lines
+27
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로또의 요구사항이 변경될 일은 거의 없겠지만 여기에 당첨 숫자가 늘어났을 때 when 구문에 코드를 작성하는걸 잊으면 버그가 발생할 수 있겠네요. enum 등을 활용해 코드를 개선해보세요.
Comment on lines
+18
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약 enum 등을 활용해 당첨 결과(1등, 2등, 3등 등)을 분류하고 |
||
} | ||
} | ||
|
||
private fun makeShuffleNumbers(): List<Int> { | ||
return (1..45).shuffled().subList(0, 6).sorted() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package lotto.entity | ||
|
||
data class Lottery(val values: List<Int>) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금은 항상 당장 테스트만 봐도 간단한 실수로 제약이 깨질 수 있어보이네요. val lotteries = listOf(Lottery(listOf(1, 2, 3, 11, 12, 13)), Lottery(listOf(1, 2, 3, 4, 5, 6))) input 쪽에서 제약 조건을 검증할수도 있겠지만 이런 도메인 클래스를 통해 예외를 검증해보시면 좋을 것 같아요. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package lotto.entity | ||
|
||
private const val rewardThree = 5000 | ||
private const val rewardFour = 50000 | ||
private const val rewardFive = 1500000 | ||
private const val rewardSix = 2000000000 | ||
|
||
data class WinLottery(var matchNum: Int = 0, val reward: Int, val count: Int = 0) | ||
data class WinLotteryResult( | ||
var matchThree: WinLottery = WinLottery(0, rewardThree), | ||
var matchFour: WinLottery = WinLottery(0, rewardFour), | ||
var matchFive: WinLottery = WinLottery(0, rewardFive), | ||
var matchSix: WinLottery = WinLottery(0, rewardSix) | ||
) | ||
Comment on lines
+3
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. enum 등을 활용해 1등, 2등, 3등 당첨 개수와 금액을 별도로 추출해 관리하면 어떨까요? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package lotto | ||
|
||
import lotto.common.InputValidation | ||
import lotto.domain.Draw | ||
import lotto.view.ResultView | ||
import view.InputView | ||
|
||
fun main() { | ||
val inputView = InputView() | ||
val resultView = ResultView() | ||
val draw = Draw() | ||
val inputValidation = InputValidation() | ||
|
||
// 구입하기 | ||
val amount = inputValidation.amountValidate(inputView.start()) | ||
val num = draw.calculateBuyNum(amount) | ||
resultView.buyLottery(num) | ||
|
||
// 로또 구입 내역 보여주기 | ||
val lotteries = draw.drawLotteries(num) | ||
lotteries.forEach { resultView.printLottery(it) } | ||
|
||
// 지난주 당첨번호 입력 받기 | ||
val winLottery = inputValidation.winLotteryValidation(inputView.winLottery()) | ||
val winLotteryResult = draw.calculateWin(winLottery, lotteries) | ||
resultView.printWinResult(winLotteryResult) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package view | ||
|
||
class InputView { | ||
fun start(): String? { | ||
val startMsg = "구입금액을 입력해주세요" | ||
println(startMsg) | ||
return readln() | ||
} | ||
|
||
fun winLottery(): String? { | ||
var winLotteryMsg = "지난 주 당첨 번호를 입력해주세요." | ||
println(winLotteryMsg) | ||
return readln() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package lotto.view | ||
|
||
import lotto.entity.Lottery | ||
import lotto.entity.WinLottery | ||
import lotto.entity.WinLotteryResult | ||
|
||
class ResultView { | ||
fun buyLottery(num: Int) { | ||
var buyLotteryMsg = "${num}개를 구매했습니다" | ||
println(buyLotteryMsg) | ||
} | ||
|
||
fun printLottery(lottery: Lottery) { | ||
println(lottery.values) | ||
} | ||
|
||
fun printWinResult(winLotteryResult: WinLotteryResult) { | ||
printWin(3, winLotteryResult.matchThree) | ||
printWin(4, winLotteryResult.matchFour) | ||
printWin(5, winLotteryResult.matchFive) | ||
printWin(6, winLotteryResult.matchSix) | ||
} | ||
|
||
private fun printWin(matchNum: Int, winLottery: WinLottery) { | ||
val printWinMsg = "${matchNum}개 일치 (${winLottery.reward}원) - ${winLottery.matchNum}개" | ||
println(printWinMsg) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import stringCaculator.domain.Calculator | ||
import stringCaculator.view.InputView | ||
import stringCaculator.view.OutputView | ||
|
||
fun main() { | ||
val text = InputView().input() | ||
val result = Calculator().sum(text) | ||
OutputView().printConsole(result) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package stringCaculator.domain | ||
|
||
class Calculator { | ||
fun sum(input: String?): Int { | ||
if (input.isNullOrBlank()) return 0 | ||
return Seperator().parse(input).sum() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package stringCaculator.domain | ||
|
||
class Seperator { | ||
|
||
fun parse(text: String): List<Int> { | ||
val result = parseDelimiter(text) | ||
checkMinusInt(result) | ||
return result.map { it.toInt() }.toList() | ||
} | ||
|
||
private fun parseDelimiter(text: String): List<String> { | ||
return if (hasCustomDelimeter(text)) { | ||
customDelimiter(text) | ||
} else { | ||
defaultDelimiter(text) | ||
} | ||
} | ||
|
||
private fun checkMinusInt(input: List<String>) { | ||
require(input.all { it.toInt() >= 0 }) { throw IllegalArgumentException(ExceptionCode.NOT_ALLOWED_MINUS.getMessage()) } | ||
} | ||
|
||
private fun defaultDelimiter(text: String): List<String> { | ||
return text.split(",|:".toRegex()) | ||
} | ||
|
||
private fun customDelimiter(text: String): List<String> { | ||
val result = Regex("//(.)\n(.*)").find(text) | ||
val customDelimiter = result!!.groupValues[1] | ||
return result.groupValues[2].split(customDelimiter) | ||
} | ||
|
||
private fun hasCustomDelimeter(text: String): Boolean { | ||
return Regex("//(.)\n(.*)").find(text) != null | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
enum class ExceptionCode( | ||
private val message: String, | ||
) { | ||
NOT_ALLOWED_MINUS("input에 음수가 있으면 안됩니다"), | ||
; | ||
|
||
fun getMessage(): String { | ||
return this.message | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package stringCaculator.view | ||
|
||
class InputView { | ||
private val guideInputMsg = "값을 입력해주세요" | ||
fun input(): String? { | ||
println(guideInputMsg) | ||
return readln() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package stringCaculator.view | ||
|
||
class OutputView { | ||
fun printConsole(text: Int) { | ||
println(text) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package lotto.common | ||
|
||
import org.assertj.core.api.Assertions | ||
import org.junit.jupiter.api.DisplayName | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.NullAndEmptySource | ||
import org.junit.jupiter.params.provider.ValueSource | ||
|
||
class InputValidationTest { | ||
private val inputValidation = InputValidation() | ||
|
||
@DisplayName("구입금액에 null값이 들어오면 에러를 낸다") | ||
@ParameterizedTest | ||
@NullAndEmptySource | ||
fun amountIsNotAllowedNull(input: String?) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.amountValidate(input) | ||
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("구입금액에 빈값이 들어오면 에러를 낸다") | ||
@ValueSource(strings = ["", " "]) | ||
fun amountIsNotAllowedEmpty(input: String?) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.amountValidate(input) | ||
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("구입금액에 숫자가 아니면 에러를 낸다") | ||
@ValueSource(strings = ["test", "!123", "zz1+"]) | ||
fun amountIsNotMatchNumeric(input: String) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.amountValidate(input) | ||
}.isInstanceOf(ExceptionCode.NotMatchNumeric::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("구입금액에 숫자 string가 들어오면 int로 바꿔준다") | ||
@ValueSource(strings = ["123", "11", "33"]) | ||
fun amountStringToDigit(input: String) { | ||
Assertions.assertThat(inputValidation.amountValidate(input)).isEqualTo(input.toInt()) | ||
} | ||
|
||
@DisplayName("당첨번호에 null값이 들어오면 에러를 낸다") | ||
@ParameterizedTest | ||
@NullAndEmptySource | ||
fun winLotteryIsNotAllowedNull(input: String?) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.winLotteryValidation(input) | ||
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("당첨번호에 빈값이 들어오면 에러를 낸다") | ||
@ValueSource(strings = ["", " "]) | ||
fun winLotteryIsNotAllowedEmpty(input: String?) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.winLotteryValidation(input) | ||
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("당첨번호에 , 구분자가 없으면 에러를 낸다") | ||
@ValueSource(strings = ["1;2;3;4", "123451234", "1234512345"]) | ||
fun winLotteryIsNotFindSeparator(input: String) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.winLotteryValidation(input) | ||
}.isInstanceOf(ExceptionCode.NotFindSeparator::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("당첨번호가 6자리가 아니면 에러를 낸다") | ||
@ValueSource(strings = ["1,2,3,4", "5,6,7,8", "10,11"]) | ||
fun winLotteryIsNotSixNum(input: String) { | ||
Assertions.assertThatThrownBy { | ||
inputValidation.winLotteryValidation(input) | ||
}.isInstanceOf(ExceptionCode.NotWinLotteryCount::class.java) | ||
} | ||
|
||
@ParameterizedTest | ||
@DisplayName("당첨번호가 , 구분자로 split한다") | ||
@ValueSource(strings = ["1,2,3,4,5,6", "5,6,7,8,9,10"]) | ||
fun winLotteryStringToDigit(input: String) { | ||
Assertions.assertThat(inputValidation.winLotteryValidation(input)) | ||
.isEqualTo(input.split(",").map { it.toInt() }) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package lotto.domain | ||
|
||
import lotto.entity.Lottery | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.DisplayName | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.ValueSource | ||
|
||
class DrawTest { | ||
private val draw = Draw() | ||
private val lotteryPrice = 1000 | ||
|
||
@DisplayName(value = "구입금액으로 산 갯수를 계산한다") | ||
@ParameterizedTest | ||
@ValueSource(ints = [1000, 100000, 1000000]) | ||
fun calculateBuyNum(num: Int) { | ||
assertThat(draw.calculateBuyNum(num)).isEqualTo(num / lotteryPrice) | ||
} | ||
|
||
@DisplayName(value = "로또 당첨 갯수를 계산한다") | ||
@Test | ||
fun calculateWin() { | ||
val winNumbers = listOf(1, 2, 3, 4, 5, 6) | ||
val lotteries = listOf(Lottery(listOf(1, 2, 3, 11, 12, 13)), Lottery(listOf(1, 2, 3, 4, 5, 6))) | ||
val winLotteryResult = draw.calculateWin(winNumbers, lotteries) | ||
assertThat(winLotteryResult.matchThree.matchNum).isEqualTo(1) | ||
assertThat(winLotteryResult.matchSix.matchNum).isEqualTo(1) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요건 개인 취향일 수 있는데 저는 가급적이면 커스텀 예외가 아니라 표준 라이브러리에 존재하는 예외(IllegalArgument, IllegalState 등)을 이용해 처리해보아도 좋을 것 같아요. 표준 라이브러리의 예외는 많은 개발자가 알고 있으므로 널리 알려진 요소를 재사용하면 다른 사람들이 API를 더 쉽게 배우고 이해할 수 있는 장점이 있습니다.
예외 메시지의 중복이 문제가 될 수 있는데 중복이 엄청 많은게 아니라면 그냥 그때그때 필요한 도메인에 예외 메시지를 남겨도 된다고 생각해요.