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 - 로또(자동) #3462

Open
wants to merge 7 commits into
base: kwonyoungbin
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@
* [X] 나눗셈의 경우 결과 값을 정수로 떨어지는 값으로 한정한다.
* [X] 문자열 계산기는 사칙연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다. 즉, 수학에서는 곱셈, 나눗셈이 덧셈, 뺄셈 보다 먼저 계산해야 하지만 이를 무시한다.
* 예를 들어 2 + 3 * 4 / 2와 같은 문자열을 입력할 경우 2 + 3 * 4 / 2 실행 결과인 10을 출력해야 한다.

---
## Step2 - 로또(자동) 기능 요구사항
* [X] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
* [X] 로또 1장의 가격은 1000원이다.

- 기능 목록
* [X] 구입 금액을 입력받는다.
* [X] 구입 금액을 바탕으로 구매 수량을 판단한다.
* [X] 중복이 없는 6개의 숫자(1~45)로 구성된 로또를 구매 수량만큼 발행한다.
* [X] 지난 주 당첨 번호를 입력받는다.
* [X] 입력받은 당첨 번호를 바탕으로 당첨 통계를 구한다.
* [X] 구매 금액 대비 수익률을 계산한다.
Comment on lines +24 to +30
Copy link
Member

Choose a reason for hiding this comment

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

객체별로 기능을 정리해 보셔도 좋을 것 같아요

23 changes: 23 additions & 0 deletions src/main/java/lotto/LottoApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package lotto;

import lotto.domain.MyLotto;
import lotto.domain.PrizeResult;
import lotto.view.InputView;
import lotto.view.ResultView;

import java.util.List;

public class LottoApplication {
public static void main(String[] args){
int money = InputView.inputPurchaseAmount();

MyLotto myLotto = new MyLotto(money);
ResultView.printMyLottoList(myLotto);

List<Integer> winningNumbers = InputView.inputWinningNumbers();
PrizeResult prizeResult = myLotto.getResult(winningNumbers);

ResultView.printLottoResult(prizeResult);
ResultView.printEarnRate(prizeResult, money);
}
}
36 changes: 36 additions & 0 deletions src/main/java/lotto/domain/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package lotto.domain;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Lotto {
private final List<Integer> numList = IntStream.range(1, 45).boxed().collect(Collectors.toList());

private final List<Integer> lottoNumberList;

public Lotto(){
shuffle();
this.lottoNumberList = numList.subList(0, 6);
sort();
}
Comment on lines +13 to +17
Copy link
Member

Choose a reason for hiding this comment

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

생성자에 로직은 제거하고 필드에 할당할 인자를 받으면 어떨까요?
로직이 필요하다면 정적 팩토리 메서드를 활용해 보시길 추천드립니다


private void shuffle(){
Collections.shuffle(numList);
}

private void sort(){
Collections.sort(this.lottoNumberList);
}

public List<Integer> getLottoNumberList() {
return this.lottoNumberList;
}

public long getMatchCount(List<Integer> winningNumbers){
return lottoNumberList.stream()
.filter(val -> winningNumbers.contains(val))
.count();
}
}
35 changes: 35 additions & 0 deletions src/main/java/lotto/domain/MatchInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package lotto.domain;

import java.util.Arrays;
import java.util.List;

public enum MatchInfo {
MATCH_SIX(6, 2000000000)
, MATCH_FIVE(5, 1500000)
, MATCH_FOUR(4, 50000)
, MATCH_THREE(3, 5000)
, OTHERS(0, 0);

private final long count;
private final int reward;

MatchInfo(int count, int reward){
this.count = count;
this.reward = reward;
}

public long getCount(){
return this.count;
}

public int getReward(){
return this.reward;
}

public static MatchInfo checkMatch(long count){
return Arrays.stream(MatchInfo.values())
.filter(matchInfo -> matchInfo.count == count)
.findFirst()
.orElse(OTHERS);
}
}
32 changes: 32 additions & 0 deletions src/main/java/lotto/domain/MyLotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package lotto.domain;

import java.util.ArrayList;
import java.util.List;

public class MyLotto {
private static int LOTTO_PRICE = 1000;
Copy link
Member

Choose a reason for hiding this comment

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

공유하는 자원을 변경하면 어떤 이슈가 발생할까요?
static을 사용할 경우 final 도 함께 사용하시길 권장드립니다

private List<Lotto> lottoList = new ArrayList<>();

public MyLotto(int money){
for(int i=0; i<money/LOTTO_PRICE; i++){
lottoList.add(new Lotto());
}
}
Comment on lines +10 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.

모든 클래스의 생성자에 로직이 있네요
객체의 필드를 주입받지 않고 내부에서 생성하는 이유가 궁금합니다


public List<Lotto> getLottoList() {
return lottoList;
}

public PrizeResult getResult(List<Integer> winningNumbers){
PrizeResult prizeResult = new PrizeResult();
for(Lotto lotto : lottoList){
prizeResult.addPrizeResult(matchResult(winningNumbers, lotto));
}

return prizeResult;
}

private MatchInfo matchResult(List<Integer> winningNumbers, Lotto lotto){
return MatchInfo.checkMatch(lotto.getMatchCount(winningNumbers));
}
Comment on lines +20 to +31
Copy link
Member

Choose a reason for hiding this comment

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

단위 테스트가 누락되었네요

}
30 changes: 30 additions & 0 deletions src/main/java/lotto/domain/PrizeResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto.domain;

import java.util.HashMap;
import java.util.Map;

public class PrizeResult {
private final Map<MatchInfo, Integer> prizeResultInfoList = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

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

HashMap 대신 EnumMap 을 활용해 보셔도 좋을 것 같아요


public PrizeResult(){
for(MatchInfo matchInfo : MatchInfo.values()){
prizeResultInfoList.put(matchInfo, 0);
}
}
Comment on lines +9 to +13
Copy link
Member

Choose a reason for hiding this comment

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

컨벤션이 지켜지지 않은 코드가 많은 것 같아요
세이브 또는 커밋 전에 ide 의 reformat code 기능을 활용해 보시면 좋을 것 같아요

Suggested change
public PrizeResult(){
for(MatchInfo matchInfo : MatchInfo.values()){
prizeResultInfoList.put(matchInfo, 0);
}
}
public PrizeResult() {
for (MatchInfo matchInfo : MatchInfo.values()) {
prizeResultInfoList.put(matchInfo, 0);
}
}


public void addPrizeResult(MatchInfo matchInfo){
prizeResultInfoList.put(matchInfo, prizeResultInfoList.get(matchInfo)+1);
}

public Map<MatchInfo, Integer> getPrizeResultInfoList(){
return prizeResultInfoList;
}

public double getEarnRate(int inputMoney){
int income = prizeResultInfoList.entrySet()
.stream()
.mapToInt(matchInfoIntegerEntry -> matchInfoIntegerEntry.getKey().getReward() * matchInfoIntegerEntry.getValue())
Copy link
Member

Choose a reason for hiding this comment

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

스트림 내부에서의 변수 명도 의미있게 작성해 보시면 좋을 것 같아요
Map 자료 구조를 사용한다고 해도 IntegerEntry 표현은 없어도 괜찮지 않을까요?

.sum();
return (double) income/inputMoney;
}
}
31 changes: 31 additions & 0 deletions src/main/java/lotto/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package lotto.view;

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public class InputView {
private static final String INPUT_MESSAGE_OF_PURCHASE_AMOUNT = "구입금액을 입력해 주세요.";
private static final String INPUT_MESSAGE_OF_WINNING_NUMBER = "지난 주 당첨 번호를 입력해 주세요.";
private static final String SEPARATOR = ", ";
private static final Scanner scanner = new Scanner(System.in);

public static int inputPurchaseAmount(){
System.out.println(INPUT_MESSAGE_OF_PURCHASE_AMOUNT);
return Integer.parseInt(scanner.nextLine());
}

public static List<Integer> inputWinningNumbers(){
System.out.println(INPUT_MESSAGE_OF_WINNING_NUMBER);
String numbers = scanner.nextLine();
return getWinningNumbers(numbers);
}

private static List<Integer> getWinningNumbers(String numbers){
return Arrays.asList(numbers.split(SEPARATOR))
.stream().mapToInt(Integer::parseInt)
.boxed()
.collect(Collectors.toList());
}
}
49 changes: 49 additions & 0 deletions src/main/java/lotto/view/ResultView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lotto.view;

import lotto.domain.Lotto;
import lotto.domain.MatchInfo;
import lotto.domain.MyLotto;
import lotto.domain.PrizeResult;

import java.util.Comparator;
import java.util.List;

public class ResultView {
private static final String OUTPUT_MESSAGE_OF_PURCHASE_AMOUNT = "개를 구매했습니다.";
private static final String OUTPUT_MESSAGE_OF_RESULT_START = "당첨 통계\n---------";
private static final String OUTPUT_FORMAT_OF_MATCHING = "%d개 일치 (%d원)- %d개\n";
private static final String OUTPUT_FORMAT_OF_RATE = "총 수익률은 %.2f입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임)";

public static void printMyLottoList(MyLotto myLotto){
List<Lotto> lottoList = myLotto.getLottoList();
printAmount(lottoList.size());
for(Lotto lotto : lottoList){
System.out.println(lotto.getLottoNumberList());
}
System.out.println();
}

private static void printAmount(int amount){
StringBuilder builder = new StringBuilder();
builder.append(amount);
builder.append(OUTPUT_MESSAGE_OF_PURCHASE_AMOUNT);
System.out.println(builder);
}

public static void printLottoResult(PrizeResult prizeResult){
System.out.println(OUTPUT_MESSAGE_OF_RESULT_START);
prizeResult.getPrizeResultInfoList().entrySet()
.stream()
.filter(matchInfoIntegerEntry -> !matchInfoIntegerEntry.getKey().equals(MatchInfo.OTHERS))
.sorted(Comparator.comparing(matchInfoIntegerEntry -> matchInfoIntegerEntry.getKey().getCount()))
.forEach(matchInfoIntegerEntry -> printMatchResult(matchInfoIntegerEntry.getKey(), matchInfoIntegerEntry.getValue()));
}

private static void printMatchResult(MatchInfo matchInfo, Integer matchCount){
System.out.printf(OUTPUT_FORMAT_OF_MATCHING, matchInfo.getCount(), matchInfo.getReward(), matchCount);
}

public static void printEarnRate(PrizeResult prizeResult, int inputMoney){
System.out.printf(OUTPUT_FORMAT_OF_RATE, prizeResult.getEarnRate(inputMoney));
}
}
26 changes: 26 additions & 0 deletions src/test/java/calculator/CalculatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package calculator;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CalculatorTest {
@DisplayName("빈 문자 혹은 공백 입력 테스트")
@ParameterizedTest
@NullAndEmptySource
void emptyOrNullInputTest(String text){
assertThatThrownBy(() -> Calculator.calculate(text))
.isInstanceOf(IllegalArgumentException.class);
}

@DisplayName("문자열 사칙연산 테스트")
@Test
void formulaTest(){
String formula = "2 + 3 * 4 / 2";
assertThat(Calculator.calculate(formula)).isEqualTo(10);
}
}
23 changes: 23 additions & 0 deletions src/test/java/calculator/OperatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package calculator;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class OperatorTest {
@DisplayName("단순 사칙연산 테스트")
@Test
void calculateTest(){
assertThat(Operator.PLUS.calculate(4, 2)).isEqualTo(6);
assertThat(Operator.MINUS.calculate(4, 2)).isEqualTo(2);
assertThat(Operator.MULTIPLE.calculate(4, 2)).isEqualTo(8);
assertThat(Operator.DIVIDE.calculate(4, 2)).isEqualTo(2);
}

@DisplayName("나눗셈 결과 정수 여부 테스트")
@Test
void divideResultIntegerTest(){
assertThat(Operator.DIVIDE.calculate(5, 2)).isEqualTo(2);
}
}
79 changes: 79 additions & 0 deletions src/test/java/lotto/LottoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package lotto;

import lotto.domain.Lotto;
import lotto.domain.MatchInfo;
import lotto.domain.MyLotto;
import lotto.domain.PrizeResult;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.*;

import static org.assertj.core.api.Assertions.assertThat;

public class LottoTest {
private static final int LOTTO_PRICE = 1000;
private static final int LOTTO_NUMBER_COUNT = 6;

@DisplayName("로또 1장 가격 1000원 및 금액에 해당하는 로또 발급 여부 테스트")
@ParameterizedTest()
@ValueSource(ints = {14000, 8000, 999, 2000})
void lottoCountTest(int money){
MyLotto myLotto = new MyLotto(money);
assertThat(myLotto.getLottoList().size()).isEqualTo(money/LOTTO_PRICE);
}
Comment on lines +21 to +27
Copy link
Member

Choose a reason for hiding this comment

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

테스트는 클래스별로 만들어 주세요 (MyLottoTest, MatchInfoTest)

테스트를 검증할 때 로직을 사용하지 마시고 명시된 값을 활용하면 어떨까요?
단위 테스트는 프로덕션 코드를 검증하는 것인데 검증부의 로직때문에 실패하면 신뢰할 수 있는 테스트로 보기 어려울 것 같아요
14000원이면 14장, 8000원이면 8장 처럼 누가봐도 명확한 값들로 테스트해 보시면 좋을 것 같아요


@DisplayName("로또 중복 번호 존재 유무 테스트")
@RepeatedTest(value = 10)
void lottoNumberDuplicateTest(){
Lotto lotto = new Lotto();
assertThat(new HashSet<>(lotto.getLottoNumberList()).size()).isEqualTo(LOTTO_NUMBER_COUNT);
}
Comment on lines +29 to +34
Copy link
Member

Choose a reason for hiding this comment

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

테스트 코드 작성자 외에 lotto 객체가 중복되지 않는 6개 숫자를 가진다는 사실을 어떻게 알 수 있을까요?
생성자 내부에 로직으로 숨겨져 있어서 lotto 객체 생성시 중복되지 않는 6개의 번호를 가진다는 사실을 알 수가 없습니다

lotto를 생성할 때 5개, 7개 숫자를 주입한다거나, 중복된 숫자가 포하된 6개 숫자를 주입하면 객체 생성 시 예외가 발생함을 검증하면 어떨까요?


@DisplayName("로또 번호 오름차순 정렬 유무 테스트")
@RepeatedTest(value = 10)
void lottoNumberSortingTest(){
Lotto lotto = new Lotto();
List<Integer> lottoNumberList = lotto.getLottoNumberList();
assertThat(lottoNumberList).allSatisfy(val ->{
int idx = lottoNumberList.indexOf(val);
assertThat(Collections.max(lottoNumberList.subList(0, idx+1))).isEqualTo(val);
});
}

@DisplayName("로또 당첨 통계 테스트")
@ParameterizedTest()
@ValueSource(ints = {0, 1, 2, 3, 4, 5, 6})
void lottoMatchTest(int matchCount){
Lotto lotto = new Lotto();
List<Integer> winningNumbers = Arrays.asList(0, 0, 0, 0, 0, 0);
for(int i=0; i<matchCount; i++){
winningNumbers.set(i, lotto.getLottoNumberList().get(i));
}
assertThat(MatchInfo.checkMatch(lotto.getMatchCount(winningNumbers)))
.isEqualTo(MatchInfo.checkMatch(matchCount));
}

@DisplayName("로또 수익률 테스트")
@ParameterizedTest()
@ValueSource(ints = {14000, 8000, 999, 2000})
void lottoEarnRateTest(int inputMoney){
List<MatchInfo> matchInfoList = new ArrayList<>();
PrizeResult prizeResult = new PrizeResult();
for(int i=0; i<inputMoney/1000; i++){
int rand = (int) (Math.random()*7);
MatchInfo matchInfo = MatchInfo.checkMatch(rand);
matchInfoList.add(matchInfo);
prizeResult.addPrizeResult(matchInfo);
}

int reward = matchInfoList.stream()
.mapToInt(MatchInfo::getReward)
.sum();

assertThat(prizeResult.getEarnRate(inputMoney)).isEqualTo((double) (reward/inputMoney));
}
Comment on lines +63 to +78
Copy link
Member

Choose a reason for hiding this comment

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

테스트가 실패하고 있어요
커밋 전에 모든 테스트가 성공하는지 확인해 보시면 좋을 것 같아요
image

image

}