diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..bcf5546bb0 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,10 @@ package lotto; +import lotto.lotto.LottoService; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + LottoService lottoService = new LottoService(); + lottoService.run(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 519793d1f7..0000000000 --- a/src/main/java/lotto/Lotto.java +++ /dev/null @@ -1,20 +0,0 @@ -package lotto; - -import java.util.List; - -public class Lotto { - private final List numbers; - - public Lotto(List numbers) { - validate(numbers); - this.numbers = numbers; - } - - private void validate(List numbers) { - if (numbers.size() != 6) { - throw new IllegalArgumentException(); - } - } - - // TODO: 추가 기능 구현 -} diff --git a/src/main/java/lotto/config/LottoConfig.java b/src/main/java/lotto/config/LottoConfig.java new file mode 100644 index 0000000000..7ff9325031 --- /dev/null +++ b/src/main/java/lotto/config/LottoConfig.java @@ -0,0 +1,12 @@ +package lotto.config; + +public class LottoConfig { + + private LottoConfig() { + } + + public final static int LOTTO_NUM_LENGTH = 6; + public final static int LOTTO_MIN_NUM = 1; + public final static int LOTTO_MAX_NUM = 45; + +} diff --git a/src/main/java/lotto/lotto/Lotto.java b/src/main/java/lotto/lotto/Lotto.java new file mode 100644 index 0000000000..b1b6ee1db4 --- /dev/null +++ b/src/main/java/lotto/lotto/Lotto.java @@ -0,0 +1,65 @@ +package lotto.lotto; + +import lotto.config.LottoConfig; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + + try { + numbers.sort(Comparator.naturalOrder()); + } catch (UnsupportedOperationException ignored) {} + + this.numbers = numbers; + } + + private void validate(List numbers) { + if (numbers.size() != LottoConfig.LOTTO_NUM_LENGTH) + throw new IllegalArgumentException("[ERROR] 숫자의 개수가 올바르지 않습니다."); + if (numbers.size() != (new HashSet<>(numbers)).size()) + throw new IllegalArgumentException("[ERROR] 숫자에 중복이 없어야 합니다."); + } + + + public Prize comparePrize(Lotto winningLotto, int bonus) { + List list = new ArrayList<>(); + list.addAll(numbers); + list.addAll(winningLotto.numbers); + int matches = list.size() - (new HashSet<>(list)).size(); + boolean isBonus = numbers.contains(bonus); + + Prize prize = Prize.NONE; + for (Prize p : Prize.values()) { + if ( + p.isPrized() && + p.getMatchNum() <= matches && + p.getGrade() <= prize.getGrade() + ) { + if (p.isBonus() && !isBonus) // 보너스 true인 경우 isBonus 확인한다. + continue; + prize = p; + } + } + return prize; + } + + public boolean contains(int num) { + return numbers.contains(num); + } + + @Override + public String toString() { + return "[" + + numbers.stream().map(String::valueOf).collect(Collectors.joining(", ")) + + "]"; + } + +} diff --git a/src/main/java/lotto/lotto/LottoBundle.java b/src/main/java/lotto/lotto/LottoBundle.java new file mode 100644 index 0000000000..adc973364f --- /dev/null +++ b/src/main/java/lotto/lotto/LottoBundle.java @@ -0,0 +1,99 @@ +package lotto.lotto; + +import lotto.purchase.PurchaseService; +import lotto.util.LottoUtil; +import lotto.util.UserOutput; + +import java.util.ArrayList; +import java.util.List; + +public class LottoBundle { + private List lottos = new ArrayList<>(); + private Lotto winningLotto; + private int bonus; + + + /** + * lottoNum의 수만큼 새로운 로또 생성하기 + */ + public void createLottos(int lottoNum) { + for (int i = 0; i < lottoNum; i++) { + Lotto lotto = new Lotto(LottoUtil.createLotto()); + lottos.add(lotto); + } + } + + /** + * 당첨번호 저장하기 + */ + public void setWinningLotto(List winningNumList) { + Lotto lotto = new Lotto(winningNumList); + this.winningLotto = lotto; + } + + /** + * 보너스번호 저장하기 + */ + public void setBonus(int bonus) { + if (winningLotto.contains(bonus)) { + throw new IllegalArgumentException("[ERROR] 보너스 번호와 당첨 번호에 중복이 있습니다."); + } + this.bonus = bonus; + } + + /** + * 로또 출력하기 + */ + public void printLottos() { + StringBuilder sb = new StringBuilder(); + + for (Lotto lotto : lottos) + sb.append(lotto).append("\n"); + + UserOutput.printLottos(lottos.size(), sb.toString()); + } + + /** + * 당첨 통계 출력하기 + */ + public void printResult() { + int[] prizeStat = getPrizeStat(); + int totalreward = getTotalReward(prizeStat); + double rewardPercent = PurchaseService.getRewardPercent(lottos.size(), totalreward); + + StringBuilder sb = new StringBuilder(); + for (Prize prize : Prize.values()){ + if (!prize.isPrized()) continue; + sb.append(prize.getToString()); + sb.append( + String.format(" (%,d원) - %d개", + prize.getReward(), + prizeStat[prize.ordinal()] + )); + sb.append("\n"); + } + sb.append(String.format("총 수익률은 %.1f%%입니다.", rewardPercent)); + + UserOutput.printResult(sb.toString()); + } + + public int[] getPrizeStat() { + int[] prizeStat = new int[Prize.values().length]; + for (Lotto lotto : lottos) { + Prize prize = lotto.comparePrize(winningLotto, bonus); + if (prize.isPrized()) + prizeStat[prize.ordinal()]++; + } + return prizeStat; + } + + public int getTotalReward(int[] prizeStat) { + int totalReward = 0; + for (Prize prize : Prize.values()){ + if (prizeStat[prize.ordinal()] != 0) + totalReward += prize.getReward() * prizeStat[prize.ordinal()]; + } + return totalReward; + } + +} diff --git a/src/main/java/lotto/lotto/LottoService.java b/src/main/java/lotto/lotto/LottoService.java new file mode 100644 index 0000000000..74c331edd0 --- /dev/null +++ b/src/main/java/lotto/lotto/LottoService.java @@ -0,0 +1,74 @@ +package lotto.lotto; + +import lotto.purchase.PurchaseService; +import lotto.util.UserInput; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class LottoService { + + public void run() { + LottoBundle bundle = new LottoBundle(); + + // 구입금액 입력받기 + String userInput = UserInput.getCostInput(); + int cost = getNum(userInput); + int lottoNum = PurchaseService.getLottoNum(cost); + bundle.createLottos(lottoNum); + + // 로또 출력하기 + bundle.printLottos(); + + // 당첨번호 입력받기 + userInput = UserInput.getLottoInput(); + List winningList = getNumList(userInput); + bundle.setWinningLotto(winningList); + + // 보너스번호 입력받기 + userInput = UserInput.getBonusInput(); + int bonus = getNum(userInput); + bundle.setBonus(bonus); + + // 당첨 통계 출력하기 + bundle.printResult(); + } + + + public int getNum (String input) { + int num; + try { + num = Integer.parseInt(input); + } catch (NumberFormatException ne) { + throw new IllegalArgumentException("[ERROR] 정수를 입력해주세요."); + } + return num; + } + + public List getNumList(String input) { + List strList = new ArrayList<>( + Arrays.asList( + input.split(",") + ) + ); + + // "," 개수를 이용해서 배열의 길이가 올바르게 입력되었는지 확인한다. + int commas = input.length() - input.replace(String.valueOf(","), "").length(); + if (commas+1 != strList.size()) + throw new IllegalArgumentException("[ERROR] 정수와 ,로 이루어진 배열을 입력해주세요."); + + List numList = strList.stream() + .map(s -> { + try { + return Integer.parseInt(s); + } catch (NumberFormatException ne) { + throw new IllegalArgumentException("[ERROR] 정수와 ,로 이루어진 배열을 입력해주세요."); + } + } ).collect(Collectors.toList()); + + return numList; + } + +} diff --git a/src/main/java/lotto/lotto/Prize.java b/src/main/java/lotto/lotto/Prize.java new file mode 100644 index 0000000000..762425e7ee --- /dev/null +++ b/src/main/java/lotto/lotto/Prize.java @@ -0,0 +1,51 @@ +package lotto.lotto; + +public enum Prize { + FIFTH(true, 5, 3, false, 5000, "3개 일치"), + FOURTH(true, 4, 4, false, 50000, "4개 일치"), + THIRD(true, 3, 5, false, 1500000, "5개 일치"), + SECOND(true, 2, 5, true, 30000000, "5개 일치, 보너스 볼 일치"), + FIRST(true, 1, 6, false, 2000000000, "6개 일치"), + NONE(false, 99999, 0, false, 0, ""); + + private final boolean prized; + private final int grade; + private final int matchNum; + private final boolean bonus; + private final int reward; + private final String toString; + + Prize(boolean prized, int grade, int matchNum, boolean bonus, int reward, String toString) { + this.prized = prized; + this.grade = grade; + this.matchNum = matchNum; + this.bonus = bonus; + this.reward = reward; + this.toString = toString; + } + + public boolean isPrized() { + return prized; + } + + public int getGrade() { + return grade; + } + + public int getMatchNum() { + return matchNum; + } + + public boolean isBonus() { + return bonus; + } + + public int getReward() { + return reward; + } + + public String getToString() { + return toString; + } + +} diff --git a/src/main/java/lotto/purchase/PurchaseService.java b/src/main/java/lotto/purchase/PurchaseService.java new file mode 100644 index 0000000000..8aaf074500 --- /dev/null +++ b/src/main/java/lotto/purchase/PurchaseService.java @@ -0,0 +1,30 @@ +package lotto.purchase; + +public class PurchaseService { + + private final static int PRICE_PER_LOTTO = 1000; + + private PurchaseService() { + } + + /** + * 텍스트 입력을 lotto 수로 변환 + */ + public static int getLottoNum(int cost) { + if (cost <=0 ) + throw new IllegalArgumentException("[ERROR] 1 이상의 양수를 입력해주세요"); + if (cost % PRICE_PER_LOTTO != 0) + throw new IllegalArgumentException("[ERROR] 가격의 배수에 해당하는 가격을 입력해주세요"); + + return cost / PRICE_PER_LOTTO; + } + + /** + * 총 로또 수익률을 퍼센트 단위로 반환 + */ + public static double getRewardPercent(int num, int reward){ + return (double) reward / (num * PRICE_PER_LOTTO) * 100; + } + + +} diff --git a/src/main/java/lotto/util/LottoUtil.java b/src/main/java/lotto/util/LottoUtil.java new file mode 100644 index 0000000000..d75e74a372 --- /dev/null +++ b/src/main/java/lotto/util/LottoUtil.java @@ -0,0 +1,21 @@ +package lotto.util; + +import camp.nextstep.edu.missionutils.Randoms; +import lotto.config.LottoConfig; + +import java.util.List; + +public class LottoUtil { + + private LottoUtil() { + } + + public static List createLotto() { + return Randoms.pickUniqueNumbersInRange( + LottoConfig.LOTTO_MIN_NUM, + LottoConfig.LOTTO_MAX_NUM, + LottoConfig.LOTTO_NUM_LENGTH + ); + } + +} diff --git a/src/main/java/lotto/util/UserInput.java b/src/main/java/lotto/util/UserInput.java new file mode 100644 index 0000000000..6874eeccc2 --- /dev/null +++ b/src/main/java/lotto/util/UserInput.java @@ -0,0 +1,25 @@ +package lotto.util; + +import camp.nextstep.edu.missionutils.Console; + +public class UserInput { + + private UserInput() { + } + + public static String getCostInput() { + System.out.println("구입금액을 입력해 주세요."); + return Console.readLine().trim(); + } + + public static String getLottoInput(){ + System.out.println("당첨 번호를 입력해 주세요."); + return Console.readLine().trim(); + } + + public static String getBonusInput(){ + System.out.println("보너스 번호를 입력해 주세요."); + return Console.readLine().trim(); + } + +} diff --git a/src/main/java/lotto/util/UserOutput.java b/src/main/java/lotto/util/UserOutput.java new file mode 100644 index 0000000000..e75a8a749a --- /dev/null +++ b/src/main/java/lotto/util/UserOutput.java @@ -0,0 +1,20 @@ +package lotto.util; + +public class UserOutput { + + private UserOutput() { + } + + public static void printLottos(int num, String lottoStr) { + StringBuilder sb = new StringBuilder(); + sb.append(num).append("개를 구매했습니다.\n").append(lottoStr); + System.out.println(sb); + } + + public static void printResult(String resultStr){ + System.out.println( "당첨 통계\n" + + "---"); + System.out.println(resultStr); + } + +} diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java deleted file mode 100644 index 0f3af0f6c4..0000000000 --- a/src/test/java/lotto/LottoTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package lotto; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class LottoTest { - @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.") - @Test - void createLottoByOverSize() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") - @Test - void createLottoByDuplicatedNumber() { - // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) - .isInstanceOf(IllegalArgumentException.class); - } - - // 아래에 추가 테스트 작성 가능 -} diff --git a/src/test/java/lotto/lotto/LottoServiceTest.java b/src/test/java/lotto/lotto/LottoServiceTest.java new file mode 100644 index 0000000000..9d2c55b756 --- /dev/null +++ b/src/test/java/lotto/lotto/LottoServiceTest.java @@ -0,0 +1,58 @@ +package lotto.lotto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; + +class LottoServiceTest { + + @ParameterizedTest + @DisplayName("getNumList 동작 테스트") + @CsvSource(value = {"1,1","2,2","3,3","4,4"}, delimiter = ',') + void getNum_동작(String input, int expected) { + // given + LottoService lottoService = new LottoService(); + // when + int test = lottoService.getNum(input); + // then + assertThat(test).isEqualTo(expected); + } + + @ParameterizedTest + @DisplayName("getNumList 예와 테스트") + @ValueSource(strings = {"a", "--1", ",,3"}) + void getNum_예와(String input) { + // given + LottoService lottoService = new LottoService(); + // when & then + assertThatThrownBy(()->lottoService.getNum(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + + @ParameterizedTest + @DisplayName("getNumList 동작 테스트") + @ValueSource(strings = {"1", "1,2", "1,200,3000,2,1", "99999,99999,99999,99999,99999,99999"}) + void getNumList_동작(String input) { + // given + LottoService lottoService = new LottoService(); + // when & then + assertThatCode(()->lottoService.getNumList(input)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("getNumList 예외 테스트") + @ValueSource(strings = {"1,,", "d,s,1", "--1,2,3"}) + void getNumList_예외(String input) { + // given + LottoService lottoService = new LottoService(); + // when & then + assertThatThrownBy(()->lottoService.getNumList(input)) + .isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/lotto/lotto/LottoTest.java b/src/test/java/lotto/lotto/LottoTest.java new file mode 100644 index 0000000000..05b4f0ee32 --- /dev/null +++ b/src/test/java/lotto/lotto/LottoTest.java @@ -0,0 +1,73 @@ +package lotto.lotto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTest { + @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.") + @Test + void createLottoByOverSize() { + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") + @Test + void createLottoByDuplicatedNumber() { + // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) + .isInstanceOf(IllegalArgumentException.class); + } + + + List> testList = new ArrayList<>(List.of( + List.of(1, 2, 3, 10, 11, 12), + List.of(1, 2, 3, 4, 11, 12), + List.of(1, 2, 3, 4, 5, 12), + List.of(1, 2, 3, 4, 5, 7), + List.of(1, 2, 3, 4, 5, 6) + )); + + @ParameterizedTest + @DisplayName("comparePrize 동작 테스트") + @CsvSource(value = {"0:FIFTH", "1:FOURTH", "2:THIRD", "3:SECOND", "4:FIRST"}, delimiter = ':') + void comparePrize_동작(int testNum, String prizeName) { + // given + Lotto myLotto = new Lotto(testList.get(testNum)); + Lotto winningLotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + int bonus = 7; + + // when + Prize p = myLotto.comparePrize(winningLotto, bonus); + + // then + assertThat(p).isEqualTo(Prize.valueOf(prizeName)); + } + + @ParameterizedTest + @DisplayName("toString 동작 테스트") + @CsvSource(value = { + "0:[1, 2, 3, 10, 11, 12]", + "1:[1, 2, 3, 4, 11, 12]", + "2:[1, 2, 3, 4, 5, 12]" + }, delimiter = ':') + void toString_동작(int testNum, String expected) { + // given + Lotto myLotto = new Lotto(testList.get(testNum)); + + // when + String test = myLotto.toString(); + + // then + assertThat(test).isEqualTo(expected); + } + +} diff --git a/src/test/java/lotto/purchase/PurchaseServiceTest.java b/src/test/java/lotto/purchase/PurchaseServiceTest.java new file mode 100644 index 0000000000..929cf4f2eb --- /dev/null +++ b/src/test/java/lotto/purchase/PurchaseServiceTest.java @@ -0,0 +1,33 @@ +package lotto.purchase; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PurchaseServiceTest { + + @ParameterizedTest + @DisplayName("getLottoNum 동작 테스트") + @CsvSource(value = {"1000,1", "2000,2", "3000,3", "4000,4"}, delimiter = ',') + void getLottoNum_동작(int cost, int expect) { + // when + int test = PurchaseService.getLottoNum(cost); + // then + assertThat(test).isEqualTo(expect); + } + + @ParameterizedTest + @DisplayName("getLottoNum 예외 테스트") + @ValueSource(ints = {0, 1005, 2050, 3500}) + void getLottoNum_예외(int cost) { + // when & then + assertThatThrownBy(() -> + PurchaseService.getLottoNum(cost) + ).isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file