diff --git a/docs/README.md b/docs/README.md index e69de29..db2de15 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,18 @@ +- [ ] 구입 금액 입력 + - [ ] 1000원 단위인지 검사하는 기능 +- [ ] 당첨 번호 입력 + - [ ] 1 ~ 45 사이인지 검사하는 기능 + - [ ] 중복 여부 검사하는 기능 + - [ ] 쉼표로 구분하는 기능 +- [ ] 보너스 번호 입력 + - [ ] 1 ~ 45 사이인지 검사하는 기능 + - [ ] 당첨 번호와의 중복 여부 검사하는 기능 +- [ ] 발행한 로또 출력 + - [ ] 1 ~ 45 사이의 중복되지 않는 숫자 6개를 뽑는 기능 + - [ ] 오름차순으로 만드는 기능 +- [ ] 당첨 내역 출력 + - [ ] 당첨 번호와 발행한 로또를 비교하는 기능 + - [ ] 당첨된를 등수 저장하는 기능 +- [ ] 수익률 출력 + - [ ] 수익금 계산하는 기능 + - [ ] 소수점 둘째 자리에서 반올림하는 기능 \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922..a0862b0 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,11 @@ package lotto; +import lotto.controller.LottoController; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + LottoController lottoController = new LottoController(); + lottoController.lottoStart(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 519793d..0000000 --- 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/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java new file mode 100644 index 0000000..93bdddf --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,36 @@ +package lotto.controller; + +import lotto.domain.*; +import lotto.view.InputView; +import lotto.view.OutputView; + +import java.util.Map; + +public class LottoController { + + public void lottoStart() { + try { + int purchaseAmount = InputView.getPurchaseAmount(); + LottoGenerator lottoGenerator = new LottoGenerator(purchaseAmount); + Lottos lottos = new Lottos(lottoGenerator.generateLottos()); + printLottosInformation(lottoGenerator, lottos); + WinningLotto winningLotto = new WinningLotto(InputView.getLottoNumber(), InputView.getLottoBonusNumber()); + Map winningDetails = WinningStatistics.getWinningDetails(lottos, winningLotto); + printWinningInformation(winningDetails, purchaseAmount); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + + private void printLottosInformation(LottoGenerator lottoGenerator, Lottos lottos) { + OutputView.printHowManyLottoUserPurchased(lottoGenerator.getLottoQuantity()); + OutputView.printLottos(lottos); + } + + private void printWinningInformation(Map winningDetails, int purchaseAmount) { + OutputView.printWinningStatistics(); + OutputView.printWinningDetails(winningDetails); + long winningAmount = WinningStatistics.getWinningAmount(winningDetails); + OutputView.printLottoYield(WinningStatistics.getLottoYield(winningAmount, purchaseAmount)); + } +} diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000..f1f9a25 --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,34 @@ +package lotto.domain; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + this.numbers = numbers; + } + // TODO: 추가 기능 구현 + + private void validate(List numbers) { + if (!isSizeSix(numbers) || isDuplicate(numbers)) { + throw new IllegalArgumentException(); + } + } + + private boolean isSizeSix(List numbers) { + return numbers.size() == 6; + } + + private boolean isDuplicate(List numbers) { + Set duplicateChecker = new HashSet<>(numbers); + return duplicateChecker.size() != 6; + } + + public List getNumbers() { + return numbers; + } +} diff --git a/src/main/java/lotto/domain/LottoGenerator.java b/src/main/java/lotto/domain/LottoGenerator.java new file mode 100644 index 0000000..fb842d0 --- /dev/null +++ b/src/main/java/lotto/domain/LottoGenerator.java @@ -0,0 +1,57 @@ +package lotto.domain; + +import org.kokodak.Randoms; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class LottoGenerator { + private static final int LOTTO_PRICE = 1_000; + private static final int LOTTO_NUMBER_LOWER_LIMIT = 1; + private static final int LOTTO_NUMBER_UPPER_LIMIT = 45; + private static final int LOTTO_NUMBER_QUANTITY = 6; + private static final int ZERO = 0; + private static final String MONEY_SHOULD_BE_DIVIDED_BY_ONE_THOUSAND = "[ERROR] 구입 금액은 1,000원 단위로만 받을 수 있습니다."; + // static 변수로 생성 + private final List lottos = new ArrayList<>(); + private final int lottoQuantity; + + public LottoGenerator(int money) { + validateMoney(money); + lottoQuantity = money / LOTTO_PRICE; + } + + private void validateMoney(int money) { + if (money % LOTTO_PRICE != ZERO) { + throw new IllegalArgumentException(MONEY_SHOULD_BE_DIVIDED_BY_ONE_THOUSAND); + } + } + + private boolean isZeroOrNegativeNumber(int money) { + return money <= ZERO; + } + + private boolean isDividedByOneThousand(int money) { + return money % LOTTO_PRICE == ZERO; + } + + public List generateLottos() { // 로또들을 담을 객체 반환 + for (int i = 0; i < lottoQuantity; i++) { + Lotto lotto = generateLotto(); + lottos.add(lotto); + } + return lottos; + } + + private Lotto generateLotto() { + List randomNumbers = Randoms.pickUniqueNumbersInRange(LOTTO_NUMBER_LOWER_LIMIT, LOTTO_NUMBER_UPPER_LIMIT, LOTTO_NUMBER_QUANTITY); + // 범위 내에서 로또 번호 생성 + randomNumbers.sort(Comparator.naturalOrder()); // 오름차순 정렬 + return new Lotto(randomNumbers); // 생성한 로또 반환 + } + + public int getLottoQuantity() { + return lottoQuantity; + } +} diff --git a/src/main/java/lotto/domain/Lottos.java b/src/main/java/lotto/domain/Lottos.java new file mode 100644 index 0000000..7d8920e --- /dev/null +++ b/src/main/java/lotto/domain/Lottos.java @@ -0,0 +1,14 @@ +package lotto.domain; + +import java.util.List; + +public class Lottos { + private List lottos; + + public Lottos(List lottos) { + this.lottos = lottos; + } + public List getLottos() { + return lottos; + } +} diff --git a/src/main/java/lotto/domain/WinningLotto.java b/src/main/java/lotto/domain/WinningLotto.java new file mode 100644 index 0000000..3ab2ea1 --- /dev/null +++ b/src/main/java/lotto/domain/WinningLotto.java @@ -0,0 +1,73 @@ +package lotto.domain; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class WinningLotto { + private static final String WINNING_NUMBERS_ARE_BETWEEN_ONE_AND_FORTY_FIVE = "[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다."; + private static final String WINNING_NUMBERS_MUST_BE_SIX_DIFFERENT_NUMBERS = "[ERROR] 당첨 번호는 서로 다른 6개의 수여야 합니다."; + private static final String BONUS_NUMBER_IS_BETWEEN_ONE_AND_FORTY_FIVE = "[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."; + private static final String WINNING_NUMBERS_CONTAIN_BONUS_NUMBER = "[ERROR] 당첨 번호와 보너스 번호가 중복됩니다."; + private static final int LOTTO_NUMBER_LOWER_LIMIT = 1; + private static final int LOTTO_NUMBER_UPPER_LIMIT = 45; + private static final int LOTTO_NUMBERS_SIZE = 6; + + private final List winningNumbers; + private final int bonusNumber; + + public WinningLotto(List winningNumbers, int bonusNumber) { + validateWinningNumbers(winningNumbers); + validateBonusNumber(bonusNumber); + validateDuplicate(winningNumbers, bonusNumber); + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + private void validateWinningNumbers(List winningNumbers) { + if (!isSixDifferentNumbers(winningNumbers)) { + throw new IllegalArgumentException(WINNING_NUMBERS_MUST_BE_SIX_DIFFERENT_NUMBERS); + } + if (!isBetweenOneAndFortyFive(winningNumbers)) { + throw new IllegalArgumentException(WINNING_NUMBERS_ARE_BETWEEN_ONE_AND_FORTY_FIVE); + } + } + + private boolean isBetweenOneAndFortyFive(List winningNumbers) { + for (int winningNumber : winningNumbers) { + if (winningNumber < LOTTO_NUMBER_LOWER_LIMIT || winningNumber > LOTTO_NUMBER_UPPER_LIMIT) { + return false; + } + } + return true; + } + + private boolean isSixDifferentNumbers(List winningNumbers) { + Set duplicateChecker = new HashSet<>(winningNumbers); + return duplicateChecker.size() == LOTTO_NUMBERS_SIZE; + } + + private void validateBonusNumber(int bonusNumber) { + if (!isBetweenOneAndFortyFive(bonusNumber)) { + throw new IllegalArgumentException(BONUS_NUMBER_IS_BETWEEN_ONE_AND_FORTY_FIVE); + } + } + + private boolean isBetweenOneAndFortyFive(int bonusNumber) { + return bonusNumber >= LOTTO_NUMBER_LOWER_LIMIT && bonusNumber <= LOTTO_NUMBER_UPPER_LIMIT; + } + + private void validateDuplicate(List winningNumbers, int bonusNumber) { + if (winningNumbers.contains(bonusNumber)) { + throw new IllegalArgumentException(WINNING_NUMBERS_CONTAIN_BONUS_NUMBER); + } + } + + public List getWinningNumbers() { + return winningNumbers; + } + + public int getBonusNumber() { + return bonusNumber; + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/WinningRank.java b/src/main/java/lotto/domain/WinningRank.java new file mode 100644 index 0000000..2e49034 --- /dev/null +++ b/src/main/java/lotto/domain/WinningRank.java @@ -0,0 +1,40 @@ +package lotto.domain; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; + +public enum WinningRank { + LAST_PLACE(0, false, 0), + FIFTH_PLACE(3, false, 5_000), + FOURTH_PLACE(4, false, 50_000), + THIRD_PLACE(5, false, 1_500_000), + SECOND_PLACE(5, true, 30_000_000), + FIRST_PLACE(6, false, 2_000_000_000); + + private final int matchingCount; + private final boolean containsBonusNumber; + private final int winningPrice; + + WinningRank(int matchingCount, boolean containsBonusNumber, int winningPrice) { + this.matchingCount = matchingCount; + this.containsBonusNumber = containsBonusNumber; + this.winningPrice = winningPrice; + } + + public static WinningRank findWinningRank(int matchingCount, boolean containsBonusNumber) { + return Arrays.stream(values()) + .filter(winningRank -> winningRank.matchingCount == matchingCount) + .filter(winningRank -> winningRank.containsBonusNumber == containsBonusNumber) + .findFirst() + .orElse(WinningRank.LAST_PLACE); + } + + public int getMatchingCount() { + return matchingCount; + } + + public int getWinningPrice() { + return winningPrice; + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/WinningStatistics.java b/src/main/java/lotto/domain/WinningStatistics.java new file mode 100644 index 0000000..746e651 --- /dev/null +++ b/src/main/java/lotto/domain/WinningStatistics.java @@ -0,0 +1,58 @@ +package lotto.domain; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class WinningStatistics { + private static final int AT_LEAST_THIRD_PLACE = 5; + private static final int PERCENTAGE = 100; + private static final int INITIAL_VALUE = 0; + + public static Map getWinningDetails(Lottos lottos, WinningLotto winningLotto) { + Map winningDetails = generateWinningDetails(); + for (Lotto lotto : lottos.getLottos()) { + int matchingCount = compareNumbersWithWinningNumbers(lotto, winningLotto); + boolean containsBonusNumber = compareNumbersWithBonusNumber(lotto, winningLotto, matchingCount); + WinningRank winningRank = WinningRank.findWinningRank(matchingCount, containsBonusNumber); + winningDetails.replace(winningRank, winningDetails.get(winningRank) + 1); + } + return winningDetails; + } + + public static Map generateWinningDetails() { + Map winningDetails = new EnumMap<>(WinningRank.class); + Arrays.stream(WinningRank.values()).forEach(winningRank -> winningDetails.put(winningRank, INITIAL_VALUE)); + return winningDetails; + } + + private static int compareNumbersWithWinningNumbers(Lotto lotto, WinningLotto winningLotto) { + List numbers = lotto.getNumbers(); + List winningNumbers = winningLotto.getWinningNumbers(); + return (int) numbers.stream() + .filter(winningNumbers::contains) + .count(); + } + + private static boolean compareNumbersWithBonusNumber(Lotto lotto, WinningLotto winningLotto, int matchingCount) { + if (matchingCount != AT_LEAST_THIRD_PLACE) { + return false; + } + List numbers = lotto.getNumbers(); + int bonusNumber = winningLotto.getBonusNumber(); + return numbers.contains(bonusNumber); + } + + public static long getWinningAmount(Map winningDetails) { + return winningDetails.entrySet().stream() + .mapToLong(entry -> (long) entry.getKey().getWinningPrice() * entry.getValue()) + .sum(); + } + + public static double getLottoYield(long winningAmount, int money) { + double lottoYield = PERCENTAGE + (double) (winningAmount - money) / money * PERCENTAGE; + return Math.round(lottoYield * 10) / 10.0; + } + +} diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 0000000..329e2fa --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,44 @@ +package lotto.view; + +import org.kokodak.Console; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class InputView { + private static final String PURCHASE_AMOUNT_MESSAGE = "구입금액을 입력해 주세요."; + private static final String LOTTO_NUMBER_MESSAGE = "당첨 번호를 입력해 주세요."; + private static final String LOTTO_BONUS_NUMBER_MESSAGE = "보너스 번호를 입력해 주세요."; + private static final String NOT_NUMBER = "[ERROR] 숫자가 아닌 값이 입력됐습니다."; + private static final String SEPARATOR_VALUE = ","; + + public static int getPurchaseAmount() { + System.out.println(PURCHASE_AMOUNT_MESSAGE); + try { + return Integer.parseInt(Console.readLine()); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(NOT_NUMBER); + } + } + + public static List getLottoNumber() { + System.out.println(LOTTO_NUMBER_MESSAGE); + try { + return Arrays.stream(Console.readLine().split(SEPARATOR_VALUE)) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(NOT_NUMBER); + } + } + + public static int getLottoBonusNumber() { + System.out.println(LOTTO_BONUS_NUMBER_MESSAGE); + try { + return Integer.parseInt(Console.readLine()); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(NOT_NUMBER); + } + } +} \ No newline at end of file diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000..1651d1e --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,65 @@ +package lotto.view; + +import lotto.domain.Lotto; +import lotto.domain.Lottos; +import lotto.domain.WinningRank; + +import java.text.DecimalFormat; +import java.util.Map; + +public class OutputView { + + private static final String HOW_MANY_LOTTO_USER_PURCHASED_MESSAGE = "%d개를 구매했습니다.\n"; + private static final String WINNING_STATISTICS_MESSAGE = "당첨 통계\n---"; + private static final String WINNING_DETAILS_MESSAGE = "%d개 일치 (%s원) - %d개\n"; + private static final String WINNING_DETAILS_WITH_BONUS_MESSAGE = "%d개 일치, 보너스 볼 일치 (%s원) - %d개\n"; + private static final String LOTTO_YIELD_MESSAGE = "총 수익률은 %.1f%%입니다.\n"; + private static final String SEPARATOR_PATTERN = "###,###"; + + public static void printHowManyLottoUserPurchased(int lottoQuantity) { + System.out.printf(HOW_MANY_LOTTO_USER_PURCHASED_MESSAGE, lottoQuantity); + } + + public static void printLottos(Lottos lottos) { + lottos.getLottos().stream() + .forEach(lotto -> System.out.println(lotto.getNumbers().toString())); + } + + public static void printWinningStatistics() { + System.out.println(WINNING_STATISTICS_MESSAGE); + } + + public static void printWinningDetails(Map winningDetails) { + winningDetails.entrySet().stream() + .filter(entry -> entry.getKey() != WinningRank.LAST_PLACE) + .forEach(entry -> { + if (entry.getKey() == WinningRank.SECOND_PLACE) { + printWinningDetailsWithBonus(entry); + } + printWinningDetailsWithoutBonus(entry); + }); + } + + private static void printWinningDetailsWithBonus(Map.Entry entry) { + System.out.printf(WINNING_DETAILS_WITH_BONUS_MESSAGE, + entry.getKey().getMatchingCount(), + getFormattingPrice(entry.getKey().getWinningPrice()), + entry.getValue()); + } + + private static void printWinningDetailsWithoutBonus(Map.Entry entry) { + System.out.printf(WINNING_DETAILS_MESSAGE, + entry.getKey().getMatchingCount(), + getFormattingPrice(entry.getKey().getWinningPrice()), + entry.getValue()); + } + + private static String getFormattingPrice(int winningPrice) { + DecimalFormat df = new DecimalFormat(SEPARATOR_PATTERN); + return df.format(winningPrice); + } + + public static void printLottoYield(double lottoYield) { + System.out.printf(LOTTO_YIELD_MESSAGE, lottoYield); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 14ed50f..8f9a149 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; + +import lotto.domain.Lotto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;