Skip to content
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
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
# kotlin-lotto
# kotlin-lotto

## 기능 목록
### 메인 애플리케이션 진입점
- MyLottoApplication.kt
- view 와 비즈니스 로직을 연결
- 종료 안시키고 무한루프로 입력
- 종료는 ctrl c로

### View
#### InputView
##### 구입 금액 입력
- `구입금액을 입력해 주세요.`
- 입력된 금액을 반환
- 1000원 단위
- 0원 초과
- 검증 실패 -> `다시 입력하세요`

##### 당첨 번호 입력
- `지난 주 당첨 번호를 입력해 주세요.`
- 입력된 6개 당첨번호 set을 받아서 LottoNumbers를 반환
- 콤마로 구분
- 검증 실패 -> `다시 입력하세요`

#### ResultView
##### 구매한 매수 및 번호 출력
- LottoTicket 일급 컬렉션 리스트를 입력으로 받음
- `14개를 구매했습니다.`
- `[8, 21, 23, 41, 42, 43]`

##### 당첨 통계 출력
- LottoResult 를 입력으로 받음
- 일치 개수 별로 당첨된 개수 및 수익률 및 훈수 메세지

### Domain
#### LottoNumber
- 숫자 하나.
- 1~45 사이 숫자

#### LottoWinningNumbers
- 6개 짜리 LottoNumber Set이 있음. (나중에 보너스볼 추가될 예정)
- 검증 메소드
- 중복 불가

#### LottoTicket
- 6개 짜리 LottoNumber Set이 있음.

#### Rank enum
- 등수별로 당첨 금액

#### LottoResult
- 티켓 n개에 대해서 결과를 가지고 있음. + 수익률 계산

#### LottoService
- generateLottoTickets
- 구입 금액 입력 받아서 LottoTicket 리스트 반환하는 method
- shuffled sorted 사용
- matchLottoTicket
- LottoTicket + 당첨 번호 입력 받아서 당첨된 Rank를 반환하는 method
- matchLottoTickets
- LottoTicket 리스트 + 당첨 번호 입력 받아서 LottoResult 반환하는 method
- 위의 단건 매칭 method를 내부에서 사용

## 기능 구현 단위
- domain 객체 뼈대 구현 + service 뼈대 구현
- 뼈대에 대한 테스트 작성
- domain 객체 및 service 로직 작성
- view 로직 구현
- application 구현
47 changes: 47 additions & 0 deletions src/main/kotlin/com/example/mylotto/MyLottoApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.mylotto

import com.example.mylotto.model.LottoNumber
import com.example.mylotto.model.LottoResult
import com.example.mylotto.model.LottoTicket
import com.example.mylotto.model.LottoWinningNumbers
import com.example.mylotto.service.LottoService
import com.example.mylotto.view.InputView
import com.example.mylotto.view.ResultView

fun main() {
while (true) {
doLotto()
}
}

fun doLotto() {
val lottoService = LottoService()
val inputView = InputView()
val resultView = ResultView()

var lottoTickets: List<LottoTicket>

while (true) {
try {
val purchaseAmount = inputView.readPurchaseAmount()
lottoTickets = lottoService.generateLottoTickets(purchaseAmount)
resultView.displayPurchasedTickets(lottoTickets)
break
} catch (e: Exception) {
println("다시 입력해주세요")
}
}

while (true) {
try {
val winningNumbers: LottoWinningNumbers = LottoWinningNumbers.of(inputView.readWinningNumbers().map(::LottoNumber))
val result = LottoResult.of(lottoTickets.map { ticket -> lottoService.matchLottoTicket(ticket, winningNumbers) })
resultView.displayWinningStatistics(result)
break
} catch (e: Exception) {
println("다시 입력해주세요")
}
}

println()
}
9 changes: 9 additions & 0 deletions src/main/kotlin/com/example/mylotto/constant/LottoConstant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.mylotto.constant

class LottoConstant {
companion object {
const val LOTTO_NUMBER_MAX = 45
const val LOTTO_NUMBER_SIZE = 6
const val LOTTO_TICKET_PRICE = 1000
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/com/example/mylotto/enum/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.mylotto.enum

enum class Rank(
val countOfMatch: Int,
val winningMoney: Int,
) {
FIRST(6, 2_000_000_000),
THIRD(5, 1_500_000),
FOURTH(4, 50_000),
FIFTH(3, 5_000),
MISS(0, 0),
;

companion object {
fun valueOf(countOfMatch: Int): Rank =
when (countOfMatch) {
6 -> FIRST
5 -> THIRD
4 -> FOURTH
3 -> FIFTH
else -> MISS
}
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/com/example/mylotto/model/LottoNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.mylotto.model

import com.example.mylotto.constant.LottoConstant

data class LottoNumber(
val number: Int,
) {
init {
require(number in 1..LottoConstant.LOTTO_NUMBER_MAX) { "Number must be between 1 and 45." }
Copy link

Choose a reason for hiding this comment

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

require 를 쓰셨군요. 나도 써야지..

}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/com/example/mylotto/model/LottoResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.mylotto.model

import com.example.mylotto.constant.LottoConstant
import com.example.mylotto.enum.Rank

class LottoResult private constructor(
val rankCountMap: Map<Rank, Int>,
val profitRate: Double,
) {
companion object {
fun of(ranks: List<Rank>): LottoResult {
val rankCountMap = ranks.groupingBy { it }.eachCount()
val totalWinnings = rankCountMap.entries.sumOf { it.key.winningMoney * it.value }
val profitRate = totalWinnings.toDouble() / ranks.size / LottoConstant.LOTTO_TICKET_PRICE
return LottoResult(
rankCountMap = rankCountMap,
profitRate = profitRate,
)
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/com/example/mylotto/model/LottoTicket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.mylotto.model

import com.example.mylotto.constant.LottoConstant

class LottoTicket(
val numbers: Set<LottoNumber>,
) {
constructor() : this(
(1..LottoConstant.LOTTO_NUMBER_MAX)
.shuffled()
.take(LottoConstant.LOTTO_NUMBER_SIZE)
.sorted()
.map { LottoNumber(it) }
.toSet(),
)
}
16 changes: 16 additions & 0 deletions src/main/kotlin/com/example/mylotto/model/LottoWinningNumbers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.mylotto.model

import com.example.mylotto.constant.LottoConstant

class LottoWinningNumbers private constructor(
val numbers: Set<LottoNumber>,
) {
companion object {
fun of(numberList: List<LottoNumber>): LottoWinningNumbers {
require(numberList.size == LottoConstant.LOTTO_NUMBER_SIZE) { "There must be exactly 6 winning numbers." }
val set = numberList.toSet()
require(set.size == LottoConstant.LOTTO_NUMBER_SIZE) { "There must be no duplicates." }
return LottoWinningNumbers(set)
}
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/com/example/mylotto/service/LottoService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.mylotto.service

import com.example.mylotto.constant.LottoConstant
import com.example.mylotto.enum.Rank
import com.example.mylotto.model.LottoTicket
import com.example.mylotto.model.LottoWinningNumbers

class LottoService {
fun generateLottoTickets(purchaseAmount: Long): List<LottoTicket> {
require(purchaseAmount > 0 && purchaseAmount % LottoConstant.LOTTO_TICKET_PRICE == 0L) {
"Purchase amount must be a positive multiple of ${LottoConstant.LOTTO_TICKET_PRICE}."
}
val ticketCount = (purchaseAmount / LottoConstant.LOTTO_TICKET_PRICE).toInt()
return List(ticketCount) {
LottoTicket()
}
}

fun matchLottoTicket(
lottoTicket: LottoTicket,
winningNumbers: LottoWinningNumbers,
): Rank {
val matchedCount = lottoTicket.numbers.intersect(winningNumbers.numbers).size
return Rank.valueOf(matchedCount)
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/com/example/mylotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.mylotto.view

class InputView {
fun readPurchaseAmount(): Long {
Copy link

Choose a reason for hiding this comment

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

구입금액을 천원 으로 입력하면 어떻게 되나요?

println("구입금액을 입력해 주세요.")
val amount = readlnOrNull()?.toLongOrNull()
require(amount != null)
return amount
}

fun readWinningNumbers(): List<Int> {
println("지난 주 당첨 번호를 입력해 주세요.")
val numbers =
readlnOrNull()
?.split(",")
?.mapNotNull { it.trim().toIntOrNull() }
require(numbers != null)
return numbers
}
}
38 changes: 38 additions & 0 deletions src/main/kotlin/com/example/mylotto/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.mylotto.view

import com.example.mylotto.enum.Rank
import com.example.mylotto.model.LottoResult
import com.example.mylotto.model.LottoTicket

class ResultView {
fun displayPurchasedTickets(tickets: List<LottoTicket>) {
println("${tickets.size}개를 구매했습니다.")
tickets.forEach { ticket ->
println(ticket.numbers.joinToString(prefix = "[", postfix = "]") { it.number.toString() })
}

println()
}

fun displayWinningStatistics(result: LottoResult) {
println()
println("당첨 통계")
println("---------")

Rank.entries
.filter { it != Rank.MISS }
.sortedBy { it.winningMoney }
.forEach { rank ->
val count = result.rankCountMap.getOrDefault(rank, 0)
println("${rank.countOfMatch}개 일치 (${rank.winningMoney}원)- ${count}개")
}

print("총 수익률은 ${"%.2f".format(result.profitRate)}입니다.")

if (result.profitRate < 1.0) {
println("(기준이 1이기 때문에 결과적으로 손해라는 의미임)")
} else {
println("(이득)")
}
}
}
20 changes: 20 additions & 0 deletions src/test/kotlin/com/example/mylotto/model/LottoNumberTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.mylotto.model

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec

class LottoNumberTest :
FunSpec({
test("success") {
(1..45).forEach {
shouldNotThrowAny { LottoNumber(it) }
}
}

test("should throw an exception for numbers outside 1 to 45 range") {
listOf(-1, 0, 46, 100).forEach {
shouldThrow<IllegalArgumentException> { LottoNumber(it) }
}
}
})
24 changes: 24 additions & 0 deletions src/test/kotlin/com/example/mylotto/model/LottoResultTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.mylotto.model

import com.example.mylotto.enum.Rank
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe

class LottoResultTest :
BehaviorSpec({
given("a list of ranks") {
val ranks = listOf(Rank.FIRST, Rank.THIRD, Rank.THIRD, Rank.THIRD)

`when`("a LottoResult is created") {
val lottoResult = LottoResult.of(ranks)

then("the rank count map should correctly group counts") {
val rankCounts = lottoResult.rankCountMap

rankCounts[Rank.FIRST].shouldBe(1)
rankCounts[Rank.THIRD].shouldBe(3)
rankCounts[Rank.FOURTH].shouldBe(null)
}
}
}
})
11 changes: 11 additions & 0 deletions src/test/kotlin/com/example/mylotto/model/LottoTicketTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.mylotto.model
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class LottoTicketTest :
FunSpec({
test("success") {
val newTicket = LottoTicket()
newTicket.numbers.size.shouldBe(6)
}
})
Loading