diff --git a/README.md b/README.md new file mode 100644 index 000000000..46144b02d --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# 로또 미션 + +---- + +### 기능 요구사항 + + - 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야한다. + - 로또 1장의 가격은 1000원 이다. + - 로또 당첨 번호를 받아 일치한 번호 수에 따라 당첨 결과를 보여준다. + - 로또 2등을 위한 보너스볼을 추첨한다. + - 당첨 통계에 2등을 추가한다.(2등 당첨 조건은 당첨 번호 5개 일치 + 보너스 볼 일치다.) + - 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야한다. + - 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야한다. +--- + +### 프로그래밍 요구사항 + +- 자바 코드 컨벤션을 지킨다. +- indent depth를 2를 넘지않도록 구현한다. +- 3항 연산자를 쓰지 않는다. +- else 예약어를 쓰지 않는다. +- 배열 대신 컬렉션을 사용한다. +- 축약 하지 않는다. +- 함수의 길이가 10라인을 넘지 않도록 구현한다. +- 모든 원시값과 문자열을 포장한다. +- 일급 컬렉션을 쓴다. +- Java Enum을 적용한다. + + diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 000000000..ceab9a1ec --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,30 @@ +import domain.*; +import view.InputView; +import view.ResultView; + +import java.util.List; + +public class Main { + public static void main(String[] args) { + final int price = InputView.getPurchaseAmount(); + final int manualCount = InputView.getManualPurchaseAmount(price); + + PurchaseAmount purchaseAmount = new PurchaseAmount(price, manualCount); + PurchaseManage purchaseManage = new PurchaseManage(purchaseAmount); + List manualInputs = InputView.getManualPurchasedLottos(manualCount); + + Lottos lottos = purchaseManage.buyLottos(manualCount, manualInputs); + + ResultView.showNum(lottos); + + String enteredWinningNumber = InputView.getWinningNumber(); + int bonusBall = InputView.getBonusNumber(); + WinningLotto winningLotto = new WinningLotto(enteredWinningNumber, bonusBall); + WinningStatistics winningStatistics = new WinningStatistics(); + + winningStatistics.compareLottos(winningLotto, lottos); + ProfitRate profitRate = new ProfitRate(price, winningStatistics); + ResultView.showStatistics(profitRate, winningStatistics); + } + +} diff --git a/src/main/java/domain/Lotto.java b/src/main/java/domain/Lotto.java new file mode 100644 index 000000000..21a9e6302 --- /dev/null +++ b/src/main/java/domain/Lotto.java @@ -0,0 +1,63 @@ +package domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.HashSet; + +public class Lotto { + private static final List NUMBERS = new ArrayList<>(); + static { + for (int i = 1; i <= 45; i++) { + NUMBERS.add(i); + } + } + + private final List numbers; + + public Lotto() { + List numbers = new ArrayList<>(NUMBERS); + Collections.shuffle(numbers); + this.numbers = new ArrayList<>(numbers.subList(0, 6)); + Collections.sort(this.numbers); + } + + public Lotto(List manualNumbers) { + validateLottoNumber(manualNumbers); + validateDuplication(manualNumbers); + this.numbers = new ArrayList<>(manualNumbers); + Collections.sort(this.numbers); + } + + private void validateLottoNumber(List manualNumbers) { + for (int number : manualNumbers) { + validateNumber(number); + } + if (manualNumbers.size() != 6) { + throw new IllegalArgumentException("로또 번호는 6개여야 합니다."); + } + } + + private void validateNumber(int number) { + if (number < 1 || number > 45) { + throw new IllegalArgumentException("로또 번호는 1부터 45까지여야 합니다."); + } + } + + private void validateDuplication(List manualNumbers) { + Set uniqueNumbers = new HashSet<>(manualNumbers); + if (uniqueNumbers.size() != manualNumbers.size()) { + throw new IllegalArgumentException("중복된 로또 번호가 존재합니다."); + } + } + + public List getLottoNumbers() { + return List.copyOf(numbers); + } + + public int get(int index) { + return numbers.get(index); + } + +} diff --git a/src/main/java/domain/LottoParser.java b/src/main/java/domain/LottoParser.java new file mode 100644 index 000000000..510e37912 --- /dev/null +++ b/src/main/java/domain/LottoParser.java @@ -0,0 +1,16 @@ +package domain; + +import java.util.List; +import java.util.ArrayList; + +public class LottoParser { + + public static List parseInput(String input) { + List numbers = new ArrayList<>(); + for (String value : input.split(",")) { + numbers.add(Integer.parseInt(value.trim())); + } + + return numbers; + } +} diff --git a/src/main/java/domain/Lottos.java b/src/main/java/domain/Lottos.java new file mode 100644 index 000000000..e842d7891 --- /dev/null +++ b/src/main/java/domain/Lottos.java @@ -0,0 +1,36 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; + +public class Lottos { + private List userLottos; + + public Lottos() { + this.userLottos = new ArrayList<>(); + } + public Lottos(List lottos) { + this.userLottos = lottos; + } + + public void makeManualLottos(List manualInputs) { + for (String manualInput : manualInputs) { + Lotto manualLotto = new Lotto(LottoParser.parseInput(manualInput)); + this.userLottos.add(manualLotto); + } + } + + public void makeAutomaticLottos(int automaticLottoCount) { + for (int i = 0; i < automaticLottoCount; i++) { + Lotto automaticLotto = new Lotto(); + this.userLottos.add(automaticLotto); + } + } + + public int size() { + return userLottos.size(); + } + public List getLottos() { + return List.copyOf(userLottos); + } +} diff --git a/src/main/java/domain/ProfitRate.java b/src/main/java/domain/ProfitRate.java new file mode 100644 index 000000000..155f8f192 --- /dev/null +++ b/src/main/java/domain/ProfitRate.java @@ -0,0 +1,28 @@ +package domain; + +import java.util.Map; + +public class ProfitRate { + private final int price; + private final WinningStatistics winningStatistics; + + public ProfitRate(int price, WinningStatistics winningStatistics) { + this.price = price; + this.winningStatistics = winningStatistics; + } + + public long getTotalProfit() { + long totalProfit = 0; + Map statistics = winningStatistics.getWinningStatistics(); + + for (Rank rank : statistics.keySet()) { + int count = statistics.get(rank); + totalProfit += (long) rank.getPrize() * count; + } + return totalProfit; + } + + public double getProfitRate() { + return ((double) getTotalProfit() / price) * 100; + } +} diff --git a/src/main/java/domain/PurchaseAmount.java b/src/main/java/domain/PurchaseAmount.java new file mode 100644 index 000000000..2b0ee8696 --- /dev/null +++ b/src/main/java/domain/PurchaseAmount.java @@ -0,0 +1,34 @@ +package domain; + +public class PurchaseAmount { + public static final int LOTTO_PRICE = 1000; + private final int purchasePrice; + private final int totalCount; + private final int manualCount; + + public PurchaseAmount(int price, int manualCount) { + this.purchasePrice = price; + totalCount = purchasePrice / LOTTO_PRICE; + validateManualCount(manualCount); + this.manualCount = manualCount; + } + + private void validatePurchaseAmount(int purchasePrice) { + if (purchasePrice < 0) { + throw new IllegalArgumentException("구입 금액은 0원 이상이어야 합니다."); + } + if (purchasePrice % LOTTO_PRICE != 0) { + throw new IllegalArgumentException("구입 금액은 1000원 단위이어야 합니다."); + } + } + + private void validateManualCount(int manualCount) { + if (totalCount < manualCount || manualCount < 0) { + throw new IllegalArgumentException("수동 구매 수량은 0개 이상 " + totalCount + "개 이하이어야 합니다."); + } + } + + public int calculateAutomaticCount() { + return totalCount - manualCount; + } +} diff --git a/src/main/java/domain/PurchaseManage.java b/src/main/java/domain/PurchaseManage.java new file mode 100644 index 000000000..5147823d1 --- /dev/null +++ b/src/main/java/domain/PurchaseManage.java @@ -0,0 +1,19 @@ +package domain; + +import java.util.List; + +public class PurchaseManage{ + private final PurchaseAmount purchaseAmount; + + public PurchaseManage(PurchaseAmount purchaseAmount) { + this.purchaseAmount = purchaseAmount; + } + public Lottos buyLottos(int manualCount, List manualInputs) { + int automaticLottoCount = purchaseAmount.calculateAutomaticCount(); + Lottos lottos = new Lottos(); + lottos.makeManualLottos(manualInputs); + lottos.makeAutomaticLottos(automaticLottoCount); + + return lottos; + } +} diff --git a/src/main/java/domain/Rank.java b/src/main/java/domain/Rank.java new file mode 100644 index 000000000..b4fd6e8ec --- /dev/null +++ b/src/main/java/domain/Rank.java @@ -0,0 +1,37 @@ +package domain; + +public enum Rank { + FIRST_PLACE(6, 2000000000, false), + SECOND_PLACE_BONUS(5, 30000000, true), + SECOND_PLACE(5, 1500000, false), + THIRD_PLACE(4, 50000, false), + FOURTH_PLACE(3, 5000, false), + MISS(0, 0, false); + + private final int matchBallNum; + private final int prize; + private final boolean hasBonusBall; + + Rank(int matchBallNum, int prize, boolean hasBonusBall) { + this.matchBallNum = matchBallNum; + this.prize = prize; + this.hasBonusBall = hasBonusBall; + } + + public static Rank getRank(int matchBallNum, boolean hasBonusBall) { + if (matchBallNum == FIRST_PLACE.getMatchBallNum()) { return FIRST_PLACE; } + if (matchBallNum == SECOND_PLACE_BONUS.getMatchBallNum() && hasBonusBall) { return SECOND_PLACE_BONUS; } + if (matchBallNum == SECOND_PLACE.getMatchBallNum() && !hasBonusBall) { return SECOND_PLACE; } + if (matchBallNum == THIRD_PLACE.getMatchBallNum()) { return THIRD_PLACE; } + if (matchBallNum == FOURTH_PLACE.getMatchBallNum()) { return FOURTH_PLACE; } + return MISS; + } + + public int getMatchBallNum() { + return matchBallNum; + } + + public int getPrize() { + return prize; + } +} diff --git a/src/main/java/domain/WinningLotto.java b/src/main/java/domain/WinningLotto.java new file mode 100644 index 000000000..66d0291c8 --- /dev/null +++ b/src/main/java/domain/WinningLotto.java @@ -0,0 +1,53 @@ +package domain; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class WinningLotto { + private final Lotto winningLotto; + private final int bonusBall; + private int count = 0; + private boolean bonusFlag; + + public WinningLotto(String enteredWinningLotto, int bonusBall) { + LottoParser lottoParser = new LottoParser(); + this.winningLotto = new Lotto(lottoParser.parseInput(enteredWinningLotto)); + validateBonusBall(bonusBall); + this.bonusBall = bonusBall; + } + + private void validateBonusBall(int bonusBall) { + if (bonusBall < 1 || bonusBall > 45) { + throw new IllegalArgumentException("보너스 볼은 1부터 45 사이의 숫자여야 합니다."); + } + List copiedWinningNumber = winningLotto.getLottoNumbers(); + Set uniqueBonusball = new HashSet<>(copiedWinningNumber); + if (!uniqueBonusball.add(bonusBall)) throw new IllegalArgumentException("중복된 로또 번호가 존재합니다."); + } + + public int match(Lotto lotto) { + count = 0; + List copiedLotto = lotto.getLottoNumbers(); + for (int i = 0; i < 6; i ++) { + compareNumbers(copiedLotto, i); + } + if (count == 5) { + bonusFlag = hasBonusNumber(copiedLotto); + } + return count; + } + public void compareNumbers(List copiedLotto, int i) { + if (copiedLotto.contains(winningLotto.get(i))) { + count++; + } + } + + public boolean hasBonusNumber(List copiedLotto) { + return copiedLotto.contains(bonusBall); + } + + public boolean getBonusFlag() { + return bonusFlag; + } +} diff --git a/src/main/java/domain/WinningStatistics.java b/src/main/java/domain/WinningStatistics.java new file mode 100644 index 000000000..5f3869d56 --- /dev/null +++ b/src/main/java/domain/WinningStatistics.java @@ -0,0 +1,28 @@ +package domain; + +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; + +public class WinningStatistics { + private final Map winningStatistics = new HashMap<>(); + + public WinningStatistics() { + for (Rank rank : Rank.values()) { + winningStatistics.put(rank, 0); + } + } + + public void compareLottos (WinningLotto winningLotto, Lottos lottos) { + for (Lotto lotto : lottos.getLottos()) { + int count = winningLotto.match(lotto); + Rank rank = Rank.getRank(count, winningLotto.getBonusFlag()); + winningStatistics.put(rank, winningStatistics.get(rank) + 1); + } + } + + + public Map getWinningStatistics() { + return Collections.unmodifiableMap(winningStatistics); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 000000000..bd23d169b --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,60 @@ +package view; + +import domain.PurchaseAmount; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class InputView { + private static Scanner scanner = new Scanner(System.in); + + public static int getPurchaseAmount() { + System.out.println("구입금액을 입력해 주세요."); + String input = scanner.nextLine(); + try { + return Integer.parseInt(input); + } catch(NumberFormatException e) { + System.out.println("숫자만 입력 가능합니다. 다시 입력해주세요."); + return getPurchaseAmount(); + } + } + + public static int getManualPurchaseAmount(int price) { + try { + System.out.println("수동으로 구매할 로또 수를 입력해 주세요."); + int manualPurchaseLottos = Integer.parseInt(scanner.nextLine()); + + new PurchaseAmount(price, manualPurchaseLottos); + return manualPurchaseLottos; + } catch (NumberFormatException e) { + System.out.println("숫자만 입력 가능합니다. 다시 입력해주세요."); + return getManualPurchaseAmount(price); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + return getManualPurchaseAmount(price); + } + } + + public static List getManualPurchasedLottos(int manualCount) { + List inputs = new ArrayList<>(); + for (int i = 0; i < manualCount; i++) { + System.out.println("수동으로 구매할 번호를 입력해 주세요."); + inputs.add(scanner.nextLine()); + } + return inputs; + } + + public static String getWinningNumber() { + System.out.println("지난 주 당첨 번호를 입력해주세요."); + String enteredWinningNumber = scanner.nextLine(); + return enteredWinningNumber; + } + + public static int getBonusNumber() { + System.out.println("보너스 볼을 입력해 주세요."); + int bonusNumber = Integer.parseInt(scanner.nextLine()); + return bonusNumber; + } + +} diff --git a/src/main/java/view/ResultView.java b/src/main/java/view/ResultView.java new file mode 100644 index 000000000..e66c59974 --- /dev/null +++ b/src/main/java/view/ResultView.java @@ -0,0 +1,24 @@ +package view; + +import domain.*; + +public class ResultView { + public static void showNum(Lottos lottos) { + System.out.printf("%d개를 구매했습니다.", lottos.size()); + System.out.println(); + for (Lotto lotto : lottos.getLottos()) { + System.out.println(lotto.getLottoNumbers()); + } + } + + public static void showStatistics(ProfitRate profitRate, WinningStatistics winningStatistics) { + System.out.println("당첨 통계"); + System.out.println("---------"); + System.out.println("3개 일치 (5000원)-" + winningStatistics.getWinningStatistics().get(Rank.FOURTH_PLACE)); + System.out.println("4개 일치 (50000원)-" +winningStatistics.getWinningStatistics().get(Rank.THIRD_PLACE)); + System.out.println("5개 일치 (1500000원)-" + winningStatistics.getWinningStatistics().get(Rank.SECOND_PLACE)); + System.out.println("5개 일치, 보너스 볼 일치(30000000원)-" + winningStatistics.getWinningStatistics().get(Rank.SECOND_PLACE_BONUS)); + System.out.println("6개 일치 (2000000000원)-" + winningStatistics.getWinningStatistics().get(Rank.FIRST_PLACE)); + System.out.println("총 수익률은 " + profitRate.getProfitRate() + "%입니다."); + } +} diff --git a/src/test/java/ExceptionTest.java b/src/test/java/ExceptionTest.java new file mode 100644 index 000000000..ceb27cf12 --- /dev/null +++ b/src/test/java/ExceptionTest.java @@ -0,0 +1,38 @@ +import domain.Lotto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class ExceptionTest { + @Test + @DisplayName("객체 생성 시 숫자가 6개가 아니거나 로또 번호를 초과 혹은 미만이면 예외를 던진다.") + + void validateLottoNumberTest() { + //given + List testLottoNormal = List.of(1, 2, 3, 4, 5, 6); // 정상 로또 + List testLottohasException = List.of(1, 2, 3, 4, 5); // 숫자가 5개인 로또 + List testLottohasException2 = List.of(1, 2, 3, 4, 5, 46); // 로또 숫자를 초과하는 로또 + List testLottohasException3 = List.of(0, 1, 2, 3, 4, 5); // 로또 숫자 미만인 로또 + + //then + assertAll( + () -> assertThatCode(() -> new Lotto(testLottoNormal)).doesNotThrowAnyException(), + () -> assertThatCode(() -> new Lotto(testLottohasException)).isInstanceOf(IllegalArgumentException.class).hasMessage("로또 번호는 6개여야 합니다."), + () -> assertThatCode(() -> new Lotto(testLottohasException2)).isInstanceOf(IllegalArgumentException.class).hasMessage("로또 번호는 1부터 45까지여야 합니다."), + () -> assertThatCode(() -> new Lotto(testLottohasException3)).isInstanceOf(IllegalArgumentException.class).hasMessage("로또 번호는 1부터 45까지여야 합니다.") + ); + } + + @Test + @DisplayName("객체 생성 시 숫자가 중복되면 예외를 던진다.") + + void validateDuplicatonTest() { + List duplicateLotto = List.of(1, 1, 2, 3, 4, 5); + + assertThatCode(() -> new Lotto(duplicateLotto)).isInstanceOf(IllegalArgumentException.class).hasMessage("중복된 로또 번호가 존재합니다."); + } +} diff --git a/src/test/java/ProfitRateTest.java b/src/test/java/ProfitRateTest.java new file mode 100644 index 000000000..8d992dfe1 --- /dev/null +++ b/src/test/java/ProfitRateTest.java @@ -0,0 +1,42 @@ +import domain.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ProfitRateTest { + @Test + @DisplayName("당첨 통계를 기반으로 총 수익률을 정확히 계산한다.") + void calculateProfitRateTest() { + //given + int price = 4000; + String winningInput = "1, 2, 3, 4, 5, 6"; + int testBonusNum = 7; + WinningLotto testWinningLotto = new WinningLotto(winningInput, testBonusNum); + + Lottos testLottos = new Lottos(List.of( + createLotto(1, 2, 3, 4, 5, 6), //1등 + createLotto(1, 2, 3, 4, 5, 7), //보너스 2등 + createLotto(1, 2, 3, 4, 5, 8), //2등 + createLotto(8, 9, 10, 11, 12, 13) //MISS + )); + WinningStatistics testWinningStatistics = new WinningStatistics(); + + //when + testWinningStatistics.compareLottos(testWinningLotto, testLottos); + + ProfitRate profitRate = new ProfitRate(price, testWinningStatistics); + + //then + long expectedTotalProfit = (long) Rank.FIRST_PLACE.getPrize() + Rank.SECOND_PLACE_BONUS.getPrize() + Rank.SECOND_PLACE.getPrize() + Rank.MISS.getPrize(); + double expectedProfitRate = ((double) expectedTotalProfit / price) * 100; + + assertEquals(expectedTotalProfit, profitRate.getTotalProfit()); + assertEquals(expectedProfitRate, profitRate.getProfitRate()); + } + private Lotto createLotto(Integer... numbers) { + return new Lotto(List.of(numbers)); + } +} diff --git a/src/test/java/RankTest.java b/src/test/java/RankTest.java new file mode 100644 index 000000000..e741f83da --- /dev/null +++ b/src/test/java/RankTest.java @@ -0,0 +1,26 @@ +import domain.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + + +class RankTest { + @ParameterizedTest + @DisplayName("일치하는 번호 수와 보너스 볼 유무에 따라 정확한 Rank를 반환한다") + + @CsvSource({ + "6, false, FIRST_PLACE", + "5, true, SECOND_PLACE_BONUS", + "5, false, SECOND_PLACE", + "4, false, THIRD_PLACE", + "3, false, FOURTH_PLACE", + "2, false, MISS", + "0, false, MISS" + }) + void getRankTest(int matchCount, boolean hasBonus, Rank expectedRank) { + Rank rank = Rank.getRank(matchCount, hasBonus); + assertThat(rank).isEqualTo(expectedRank); + } +} \ No newline at end of file diff --git a/src/test/java/WinningStatisticsTest.java b/src/test/java/WinningStatisticsTest.java new file mode 100644 index 000000000..54cbc0aa6 --- /dev/null +++ b/src/test/java/WinningStatisticsTest.java @@ -0,0 +1,45 @@ +import domain.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class WinningStatisticsTest { + @Test + @DisplayName("등수 별 통계를 정확히 집계한다.") + void comparingLottosTest() { + //given + String winningInput = "1, 2, 3, 4, 5, 6"; + int testBonusNum = 7; + WinningLotto testWinningLotto = new WinningLotto(winningInput, testBonusNum); + + Lottos testLottos = new Lottos(List.of( + createLotto(1, 2, 3, 4, 5, 6), //1등 + createLotto(1, 2, 3, 4, 5, 7), //보너스 2등 + createLotto(1, 2, 3, 4, 5, 8), //2등 + createLotto(8, 9, 10, 11, 12, 13) //MISS + )); + WinningStatistics testWinningStatistics = new WinningStatistics(); + + //when + testWinningStatistics.compareLottos(testWinningLotto, testLottos); + + //then + Map result = testWinningStatistics.getWinningStatistics(); + assertEquals(1, result.get(Rank.FIRST_PLACE)); + assertEquals(1, result.get(Rank.SECOND_PLACE_BONUS)); + assertEquals(1, result.get(Rank.SECOND_PLACE)); + assertEquals(1, result.get(Rank.MISS)); + + + + } + private Lotto createLotto(Integer... numbers) { + return new Lotto(List.of(numbers)); + } +}