Skip to content
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

Step2 로또 자동 #992

Open
wants to merge 9 commits into
base: sendkite
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,18 @@
+ [x] 커스텀 구분자를 지정할 수 있도록 구현
+ [x] 커스텀 구분자는 문자열 앞부분의 `//`와 `\n` 사이에 위치
+ [x] 커스텀 구분자는 한 개 지정 가능
+ [] 커스텀 구분자와 기본 구분자를 함께 사용할 수 있음
+ [x] 구분자를 기준으로 분리한 각 숫자의 합을 반환
+ [x] 커스텀 구분자와 기본 구분자를 함께 사용할 수 있음
+ [x] 구분자를 기준으로 분리한 각 숫자의 합을 반환

### 로또 (자동)

[] 로또 자동 생성기 구현
[x] 로또 한장의 가격은 1000원
[x] 로또 1000원 단위로만 구매 가능
[x] 로또는 1부터 45까지의 숫자 중 6개를 랜덤으로 뽑는다.
[x] 로또는 6개의 숫자가 모두 달라야 한다.
[x] 로또 한장은 6개의 숫자를 가진다.
[x] 로또 한장은 숫자를 오름차순으로 정렬한다.
[] 당첨 로또를 입력 받는다.
[] 당첨 로또와 구매한 로또를 비교하여 등수를 출력한다.
[] 당첨 로또와 구매한 로또를 비교하여 수익률을 출력한다.
9 changes: 9 additions & 0 deletions src/main/kotlin/calculator/RegexUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package calculator

fun isNumber(number: String): Boolean {
return Regex("[0-9]+").matches(number)
}

fun findCustomDelimiter(text: String): String {
return Regex("//(.)\n(.*)").find(text)?.groupValues?.get(1) ?: ""
Comment on lines +3 to +8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#981 (comment)
여전히 함수를 부를 때마다 Regex 객체를 생성하고 있어요!
Regex를 상수로 분리해보는 건 어떨까요?

또, 해당 메서드들은 이 역할을 가져야할 객체에게 위치시켜보는 건 어떨까요?

}
29 changes: 4 additions & 25 deletions src/main/kotlin/calculator/StringAddCalculator.kt
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
package calculator

class StringAddCalculator {
object StringAddCalculator {
fun add(text: String): Int {
if (text.isBlank()) return 0
if (text.matches(Regex("[0-9]+"))) {
if (isNumber(text)) {
return text.toInt()
}

val customDelimiterRegex = Regex("//(.)\n(.*)").find(text)
val customDelimiter = customDelimiterRegex?.groupValues?.get(1) ?: ""
val tokens = parseTokens(text, customDelimiter)

return tokens.sum()
}

private fun parseTokens(text: String, customDelimiter: String): List<Int> {
require(text.contains("-").not()) { "음수를 입력할 수 없습니다." }
require(text.contains(Regex("[,:${customDelimiter}]"))) { "숫자와 지정된 구분자만 입력할 수 있습니다." }

if (customDelimiter.isNotBlank()) {
val replacedText = text.replace("//$customDelimiter\n", "")
return replacedText.split(Regex("[,:${customDelimiter}\n]")).map {
require(it.matches(Regex("[0-9]+"))) { "숫자와 지정된 구분자만 입력할 수 있습니다." }
it.toInt()
}
}

return text.split(Regex("[,:\n]")).map {
require(it.matches(Regex("[0-9]+"))) { "숫자와 지정된 구분자만 입력할 수 있습니다." }
it.toInt()
}
val inputNumbers = StringParser().parseString(text)
return inputNumbers.sum()
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/calculator/StringParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package calculator

class StringParser {
fun parseString(text: String): List<Int> {

val customDelimiter = findCustomDelimiter(text)

require(text.contains("-").not()) { "음수를 입력할 수 없습니다." }
require(text.contains(Regex("[,:${customDelimiter}]"))) { "숫자와 지정된 구분자만 입력할 수 있습니다." }

if (customDelimiter.isNotBlank()) {
val replacedText = text.replace("//$customDelimiter\n", "")
return replacedText.split(Regex("[,:${customDelimiter}\n]")).map {
require(isNumber(it)) { "숫자와 지정된 구분자만 입력할 수 있습니다." }
it.toInt()
}
}

return text.split(Regex("[,:\n]")).map {
require(isNumber(it)) { "숫자와 지정된 구분자만 입력할 수 있습니다." }
it.toInt()
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/Lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lotto

class Lotto(
val numbers: List<Int> = generateLottoNumbers(),
Comment on lines +3 to +4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Int에는 1~45 라는 로또 번호를 제한하기에는 너무 범용적인 자료형이라 생각해요.
로또 번호를 나타내는 도메인 객체를 만들어보는 건 어떨까요?

var matchCount: Int = 0
) {

companion object {
const val PRICE: Int = 1000
}

fun match(target: Lotto) {
matchCount = numbers.intersect(target.numbers).count()
}
Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target 과 비교한 후, 이 로또는 matchCount를 그대로 갖고있을 것 같아요.
이후 이 값은 어떤 로또와 비교했었는지 추적하기 어려워질 것으로 우려돼요.
matchCount에 사이드이펙트를 발생시키는 것 보다, match 한 결과를 반환하게 만드는 것은 어떨까요?

}
11 changes: 11 additions & 0 deletions src/main/kotlin/lotto/LottoBuyer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package lotto

class LottoBuyer(
private val money: Int
) {

fun buyLottoFrom(lottoStore: LottoStore): List<Lotto> {
require(money >= Lotto.PRICE) { "돈이 부족합니다." }
return lottoStore.sell(money)
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/lotto/LottoNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package lotto

fun generateLottoNumbers(): List<Int> {
return (1..45).shuffled().take(6).sorted()
}
13 changes: 13 additions & 0 deletions src/main/kotlin/lotto/LottoStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package lotto

class LottoStore {

fun sell(money: Int): List<Lotto> {
Comment on lines +3 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LottoStore입장에서는 sell 하는 것이 맞는데, 이 메서드를 사용하는 사람 입장에서는 굉장히 어색할 것으로 예상돼요.
returnNumber 같은 말 보다는, getNumber를 쓰듯이요!

val count = money / Lotto.PRICE
val lottos = mutableListOf<Lotto>()
for (i in 1..count) {
lottos.add(Lotto())
}
return lottos
Comment on lines +7 to +11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 형태는 코틀린의 확장함수를 활용해보셔도 좋겠어요 :)

(1..count).map { }

혹은, List의 fake constructor를 활용해보셔도 좋겟네요 :)

List(count) { ... }

}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/lotto/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package lotto

fun main(args: Array<String>) {
println("구매금액을 입력해주세요.")
var inputMoney = readLine()!!.toInt()

val lottoBuyer = LottoBuyer(inputMoney)
val lottos = lottoBuyer.buyLottoFrom(LottoStore())

println(String.format("%s개를 구매했습니다.", lottos.size))
lottos.forEach { lotto ->
println(lotto.numbers)
}

println("지난 주 당첨 번호를 입력해주세요.")
val lastWeekWinningNumbers = readLine()!!.split(", ").map { it.toInt() }

lottos.forEach { lotto ->
lotto.match(Lotto(lastWeekWinningNumbers))
}

println("당첨 통계")
println("---------")
println("3개 일치 (5000원) - ${lottos.count { it.matchCount == 3 }}개")
println("4개 일치 (50000원) - ${lottos.count { it.matchCount == 4 }}개")
println("5개 일치 (1500000원) - ${lottos.count { it.matchCount == 5 }}개")
println("6개 일치 (2000000000원) - ${lottos.count { it.matchCount == 6 }}개")
println("총 수익률은 ${lottos.size * Lotto.PRICE / inputMoney}입니다.")
Comment on lines +24 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로또 등수에 대해 도메인 객체로 표현해보는 건 어떨까요?

}
20 changes: 6 additions & 14 deletions src/test/kotlin/calculator/StringAddCalculatorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package calculator

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
Expand All @@ -11,52 +10,45 @@ import org.junit.jupiter.params.provider.ValueSource

class StringAddCalculatorTest {

private lateinit var calculator: StringAddCalculator

@BeforeEach
fun setUp() {
calculator = StringAddCalculator();
}

@ParameterizedTest
@EmptySource
fun `빈 문자열 입력할 경우 0을 반환해야 한다`(text: String) {
val result = calculator.add(text)
val result = StringAddCalculator.add(text)
assertThat(result).isEqualTo(0)
}

@ParameterizedTest
@CsvSource(value = ["1,1", "2,2", "3,3"])
fun `숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환한다`(text: String, expected: Int) {
val result = calculator.add(text)
val result = StringAddCalculator.add(text)
assertThat(result).isEqualTo(expected)
}

@ParameterizedTest
@ValueSource(strings = ["안녕", "$", "1,2[3"])
fun `잘못된 문자열을 입력할 경우 RuntimeException`(text: String) {
assertThatExceptionOfType(RuntimeException::class.java)
.isThrownBy { calculator.add(text) }
.isThrownBy { StringAddCalculator.add(text) }
.withMessageMatching("숫자와 지정된 구분자만 입력할 수 있습니다.")
}

@Test
fun `음수를 입력하면 RuntimeException`() {
assertThatExceptionOfType(RuntimeException::class.java)
.isThrownBy { calculator.add("-1") }
.isThrownBy { StringAddCalculator.add("-1") }
.withMessageMatching("음수를 입력할 수 없습니다.")
}

@ParameterizedTest
@ValueSource(strings = ["1,2:3"])
fun `쉼표 또는 콜론 구분자로 숫자를 입력할 경우 정상적으로 합을 반환한다`(text: String) {
val result = calculator.add(text)
val result = StringAddCalculator.add(text)
assertThat(result).isEqualTo(6)
}

@ParameterizedTest
@ValueSource(strings = ["//;\n1;2;3", "//<\n1<2<3", "//<\n1<2,3"])
fun `custom 구분자로 입력값을 구분`(text: String) {
assertThat(calculator.add(text)).isSameAs(6);
assertThat(StringAddCalculator.add(text)).isSameAs(6);
}
}
26 changes: 26 additions & 0 deletions src/test/kotlin/lotto/LottoStoreTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package lotto

import io.kotest.matchers.collections.haveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class LottoStoreTest {

@ParameterizedTest
@CsvSource(value = ["1000,1", "2000,2", "3000,3"])
fun `로또를 여라장 구매한다`(money: Int, expected: Int) {
val lotto = LottoStore().sell(money)
lotto should haveSize(expected)
Comment on lines +13 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

몇 장을 샀는지 검증하는 것은 테스트에 큰 의미를 가지지 못한다고 생각해요.
개발자가 제어 가능한 형태로, 어떤 티켓이 반환되었는지까지 테스트할 수 있게 만들어보는 건 어떨까요?

}

@Test
fun `로또를 구매할 돈이 부족하면 예외가 발생한다`() {
val lottoStore = LottoStore()
val money = 900
val lotto = lottoStore.sell(money)
lotto shouldBe emptyList()
}
}
69 changes: 69 additions & 0 deletions src/test/kotlin/lotto/LottoTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package lotto

import io.kotest.matchers.collections.shouldBeOneOf
import io.kotest.matchers.ints.shouldBeInRange
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class LottoTest {

@Test
fun `로또는 6자리 숫자를 가진다`() {
val lotto = Lotto()
lotto.numbers.size shouldBe 6
}

@Test
fun `로또는 1부터 45까지의 숫자를 가진다`() {
val lotto = Lotto()
lotto.numbers.forEach {
it.shouldBeInRange(1..45)
}
}

@Test
fun `로또는 중복되지 않는 숫자를 가진다`() {
val lotto = Lotto()
lotto.numbers.toSet().size shouldBe 6
}

@Test
fun `로또는 숫자는 오름차순 정렬`() {
val lotto = Lotto()
lotto.numbers shouldBe lotto.numbers.sorted()
}

@Test
fun `로또 숫자는 중복되지 않는다`() {
val lotto = Lotto()
lotto.numbers.forEach {
it shouldBeOneOf lotto.numbers
}
}

@Test
fun `test lotto matching`() {

val targetLottoNumbers = listOf(1, 2, 3, 4, 5, 6)
val targetLotto = Lotto(targetLottoNumbers)

val countAndLotto = listOf(
Pair(1, listOf(1, 7, 8, 9, 10, 11)),
Pair(2, listOf(1, 2, 8, 9, 10, 11)),
Pair(3, listOf(1, 2, 3, 9, 10, 11)),
Pair(4, listOf(1, 2, 3, 4, 10, 11)),
Pair(5, listOf(1, 2, 3, 4, 5, 11)),
Pair(6, listOf(1, 2, 3, 4, 5, 6))
)


for ((expectedMatchCount, lottoNumbers) in countAndLotto) {

val lotto = Lotto(lottoNumbers)
targetLotto.match(lotto)

val actualMatchCount = targetLotto.matchCount
actualMatchCount shouldBe expectedMatchCount
}
}
Comment on lines +44 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 종류의 테스트는 @ParameterizedTest를 활용해보는 건 어떨까요?

}