diff --git a/docs/README.md b/docs/README.md index e69de29..e3e328c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,12 @@ +- 로또 구매 (구매 금액 입력) + PurchasedLotto: 구매 개수, 발행된 로또 + +- 로또 발행 (구매한 로또 출력) + +- 당첨 로또 (당첨 번호 입력) + WinningLotto: 당첨 번호, 보너스 번호 + +- 당첨 통계 출력 + WinningRank: 등수와 당첨금, enum 사용 + +- Exception 처리 \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922..ef67c3b 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,10 @@ package lotto; +import lotto.controller.LottoController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + LottoController lottoController = new LottoController(); + lottoController.run(); } } diff --git a/src/main/java/lotto/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java new file mode 100644 index 0000000..7249356 --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,81 @@ +package lotto.controller; + +import lotto.domain.Lotto; +import lotto.domain.PurchasedLotto; +import lotto.domain.WinningLotto; +import lotto.domain.WinningRank; +import lotto.view.InputView; +import lotto.view.OutputView; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class LottoController { + public void run() { + PurchasedLotto purchasedLotto = new PurchasedLotto(purchaseLotto()); + publishLotto(purchasedLotto); + WinningLotto winningLotto = getLottoNumbers(); + printLottoResult(purchasedLotto, winningLotto); + } + + // 구매 금액 입력 + private int purchaseLotto() { + return InputView.getPurchaseAmount(); + } + + // 구매한 로또 출력 + private void publishLotto(PurchasedLotto purchasedLotto) { + OutputView.printLottoCount(purchasedLotto.getLottoCount()); + OutputView.printPublishedLotto(purchasedLotto.getLottoSet()); + } + + // 당첨 번호와 보너스 번호 입력 + private WinningLotto getLottoNumbers() { + WinningLotto winningLotto = new WinningLotto(InputView.getLottoNumbers()); + winningLotto.setBonusNumber(InputView.getBonusNumber()); + return winningLotto; + } + + // 당첨 통계 출력 + private void printLottoResult(PurchasedLotto purchasedLotto, WinningLotto winningLotto) { + Map winningResult = calculateResult(purchasedLotto.getLottoSet(), winningLotto); + double returnRate = calculateReturnRate(purchasedLotto.getPurchaseAmount(), winningResult); + OutputView.printWinningResult(winningResult, returnRate); + } + + // 당첨 통계 계산 + private Map calculateResult(List lottoSet, WinningLotto winningLotto) { + Map winningResult = new EnumMap<>(WinningRank.class); + Arrays.stream(WinningRank.values()).forEach(winningRank -> winningResult.put(winningRank, 0)); + + for (Lotto lotto : lottoSet) { + int match = compareNumbers(lotto.getNumbers(), winningLotto.getWinningNumber()); + boolean containBonus = isContainBonus(lotto.getNumbers(), winningLotto.getBonusNumber(), match); + WinningRank winningRank = WinningRank.findWinningRank(match, containBonus); + winningResult.replace(winningRank, winningResult.get(winningRank) + 1); + } + + return winningResult; + } + + // 로또 하나의 당첨 결과 계산 + private int compareNumbers(List lottoNumber, List winningLotto) { + return (int) lottoNumber.stream().filter(winningLotto::contains).count(); + } + + // 보너스 번호 포함 여부 확인 + private boolean isContainBonus(List lottoNumber, int bonus, int match) { + if (match != 5) return false; + return lottoNumber.contains(bonus); + } + + // 수익률 계산 + private double calculateReturnRate(int purchaseAmount, Map winningResult) { + long winningAmount = winningResult.entrySet().stream() + .mapToLong(entry -> (long) entry.getKey().getReword() * entry.getValue()) + .sum(); + return (double) winningAmount / purchaseAmount * 100.0f; + } +} \ No newline at end of file diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/domain/Lotto.java similarity index 65% rename from src/main/java/lotto/Lotto.java rename to src/main/java/lotto/domain/Lotto.java index 519793d..b2aef69 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/domain/Lotto.java @@ -1,4 +1,6 @@ -package lotto; +package lotto.domain; + +import lotto.util.Validation; import java.util.List; @@ -7,6 +9,7 @@ public class Lotto { public Lotto(List numbers) { validate(numbers); + Validation.validateDuplicateLottoNumber(numbers); this.numbers = numbers; } @@ -16,5 +19,7 @@ private void validate(List numbers) { } } - // TODO: 추가 기능 구현 + public List getNumbers() { + return numbers; + } } diff --git a/src/main/java/lotto/domain/PurchasedLotto.java b/src/main/java/lotto/domain/PurchasedLotto.java new file mode 100644 index 0000000..dce39ec --- /dev/null +++ b/src/main/java/lotto/domain/PurchasedLotto.java @@ -0,0 +1,47 @@ +package lotto.domain; + +import lotto.util.Validation; +import org.kokodak.Randoms; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// 구매한 로또 +public class PurchasedLotto { + public static final int LOTTO_PRICE = 1000; + + private final int lottoCount; + private final List lottoSet = new ArrayList<>(); + + public PurchasedLotto(int purchasedAmount) { + Validation.validateMoneyRange(purchasedAmount); + this.lottoCount = purchasedAmount / LOTTO_PRICE; + generatedLotto(); + } + + // 구매한 로또 생성 + private void generatedLotto() { + for (int i=0; i numbers = new ArrayList<>( + Randoms.pickUniqueNumbersInRange(1, 45, 6)); + Collections.sort(numbers); + lottoSet.add(new Lotto(numbers)); + } + } + + // 구매한 로또 개수 반환 + public int getLottoCount() { + return lottoCount; + } + + // 구매한 로또 반환 + public List getLottoSet() { + return lottoSet; + } + + // 구매 금액 반환 + public int getPurchaseAmount() { + return lottoCount * LOTTO_PRICE; + } +} diff --git a/src/main/java/lotto/domain/WinningLotto.java b/src/main/java/lotto/domain/WinningLotto.java new file mode 100644 index 0000000..0f4edc3 --- /dev/null +++ b/src/main/java/lotto/domain/WinningLotto.java @@ -0,0 +1,32 @@ +package lotto.domain; + +import lotto.util.Validation; + +import java.util.List; + +// 당첨 번호 +public class WinningLotto { + private final Lotto winningNumber; + private int bonusNumber; + + public WinningLotto(List winningNumber) { + Validation.validateLotto(winningNumber); + this.winningNumber = new Lotto(winningNumber); + } + + // 당첨 번호 반환 + public List getWinningNumber() { + return winningNumber.getNumbers(); + } + + // 보너스 번호 반환 + public int getBonusNumber() { + return bonusNumber; + } + + // 보너스 번호 설정 + public void setBonusNumber(int bonusNumber) { + Validation.validateBonus(winningNumber.getNumbers(), bonusNumber); + this.bonusNumber = bonusNumber; + } +} diff --git a/src/main/java/lotto/domain/WinningRank.java b/src/main/java/lotto/domain/WinningRank.java new file mode 100644 index 0000000..2f14f55 --- /dev/null +++ b/src/main/java/lotto/domain/WinningRank.java @@ -0,0 +1,42 @@ +package lotto.domain; + +import java.util.Arrays; + +// 당첨 등수 +public enum WinningRank { + LAST_RANK(0, 0, false), + FORTH_RANK(3, 5_000, false), + THIRD_RANK(4, 50_000, false), + SECOND_RANK(5, 1_500_000, false), + SECOND_BONUS_RANK(5, 30000000, true), + FIRST_RANK(6, 2_000_000_000, false); + + private final int match; + private final int reword; + private final boolean bonus; + + WinningRank(int match, int reword, boolean bonus) { + this.match = match; + this.reword = reword; + this.bonus = bonus; + } + + // 맞힌 번호 개수 반환 + public int getMatch() { + return match; + } + + // 당첨금 반환 + public int getReword() { + return reword; + } + + // 해당 조건 등수 반환 + public static WinningRank findWinningRank(int match, boolean bonus) { + return Arrays.stream(values()) + .filter(winningRank -> winningRank.match == match) + .filter(winningRank -> winningRank.bonus == bonus) + .findFirst() + .orElse(LAST_RANK); + } +} diff --git a/src/main/java/lotto/util/Validation.java b/src/main/java/lotto/util/Validation.java new file mode 100644 index 0000000..21fbb3e --- /dev/null +++ b/src/main/java/lotto/util/Validation.java @@ -0,0 +1,88 @@ +package lotto.util; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Validation { + private static final String INPUT_LOTTO_PRICE_ERROR = "[ERROR] 구입 금액은 1,000원 단위로 입력해주세요."; + private static final String INPUT_TYPE_ERROR = "[ERROR] 숫자를 입력해주세요."; + private static final String INPUT_NUMBER_RANGE_ERROR = "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."; + private static final String INPUT_LOTTO_SIZE_ERROR = "[ERROR] 당첨 번호는 서로 다른 6개의 수로 입력해주세요."; + private static final String LOTTO_NUMBER_DUPLICATED_ERROR = "[ERROR] 당첨 번호 중 중복되는 번호가 있습니다."; + private static final String INPUT_BONUS_RANGE_ERROR = "[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."; + private static final String LOTTO_BONUS_DUPLICATED_ERROR = "[ERROR] 당첨 번호 중 보너스 번호와 중복되는 번호가 존재합니다."; + + private static final int ZERO = 0; + private static final int LOTTO_PRICE = 1_000; + private static final int LOTTO_MIN_NUMBER = 1; + private static final int LOTTO_MAX_NUMBER = 45; + private static final int LOTTO_NUMBER_SIZE = 6; + + // 입력된 타입 확인 + public static Integer parseInteger(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INPUT_TYPE_ERROR); + } + } + + // 구매 금액은 0이상, 1000원 단위 + public static void validateMoneyRange(int purchasedAmount) { + if (purchasedAmount <= ZERO || purchasedAmount % LOTTO_PRICE != 0) { + throw new IllegalArgumentException(INPUT_LOTTO_PRICE_ERROR); + } + } + + // 로또 번호 적합성 + public static void validateLotto(List lotto) { + validateLottoNumber(lotto); + validateLottoNumberSize(lotto); + validateDuplicateLottoNumber(lotto); + } + + // 로또 번호는 1~45 + private static void validateLottoNumber(List lotto) { + for (Integer l : lotto) { + if (l < LOTTO_MIN_NUMBER || l > LOTTO_MAX_NUMBER) { + throw new IllegalArgumentException(INPUT_NUMBER_RANGE_ERROR); + } + } + } + + // 로또는 6개의 숫자 + private static void validateLottoNumberSize(List lotto) { + if (lotto.size() != LOTTO_NUMBER_SIZE) { + throw new IllegalArgumentException(INPUT_LOTTO_SIZE_ERROR); + } + } + + // 로또 번호는 중복 없음 + public static void validateDuplicateLottoNumber(List lotto) { + Set deduplicate = new HashSet<>(lotto); + if (deduplicate.size() != lotto.size()) { + throw new IllegalArgumentException(LOTTO_NUMBER_DUPLICATED_ERROR); + } + } + + // 보너스 번호 적합성 + public static void validateBonus(List lotto, int bonus) { + validateBonusNumber(bonus); + validateDuplicateLottoBonus(lotto, bonus); + } + + // 보너스 번호는 1~45 + private static void validateBonusNumber(int bonus) { + if (bonus < LOTTO_MIN_NUMBER || bonus > LOTTO_MAX_NUMBER) { + throw new IllegalArgumentException(INPUT_BONUS_RANGE_ERROR); + } + } + + // 보너스 번호는 당첨 번호와 중복 없음 + private static void validateDuplicateLottoBonus(List lotto, int bonus) { + if (lotto.contains(bonus)) { + throw new IllegalArgumentException(LOTTO_BONUS_DUPLICATED_ERROR); + } + } +} diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 0000000..16775b0 --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,35 @@ +package lotto.view; + +import lotto.util.Validation; +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 WINNING_NUMBER_MESSAGE = "\n당첨 번호를 입력해 주세요."; + private static final String BONUS_NUMBER_MESSAGE = "\n보너스 번호를 입력해 주세요."; + private static final String SEPARATOR_VALUE = ","; + + // 구매 금액 입력 + public static int getPurchaseAmount() { + System.out.println(PURCHASE_AMOUNT_MESSAGE); + return Validation.parseInteger(Console.readLine()); + } + + // 당첨 번호 입력 + public static List getLottoNumbers() { + System.out.println(WINNING_NUMBER_MESSAGE); + return Arrays.stream(Console.readLine().split(SEPARATOR_VALUE)) + .map(Validation::parseInteger) + .collect(Collectors.toList()); + } + + // 보너스 번호 입력 + public static int getBonusNumber() { + System.out.println(BONUS_NUMBER_MESSAGE); + return Validation.parseInteger(Console.readLine()); + } +} diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000..fd223c8 --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,52 @@ +package lotto.view; + +import lotto.domain.Lotto; +import lotto.domain.WinningRank; + +import java.text.DecimalFormat; +import java.util.List; +import java.util.Map; + +public class OutputView { + private static final String PURCHASED_COUNT_MESSAGE = "\n%d개를 구매했습니다.\n"; + private static final String WINNING_STATICS_MESSAGE = "\n당첨 통계\n---"; + private static final String WINNING_DETAIL_MESSAGE = "%d개 일치 (%s원) - %d개\n"; + private static final String WINNING_BONUS_DETAIL_MESSAGE = "%d개 일치, 보너스 볼 일치 (%s원) - %d개\n"; + private static final String RETURN_RATE_MESSAGE = "총 수익률은 %.1f%%입니다."; + private static final String COMMA_FORMAT = "###,###"; + + // 구매 개수 출력 + public static void printLottoCount(int lottoCount) { + System.out.printf(PURCHASED_COUNT_MESSAGE, lottoCount); + } + + // 구매한 로또 출력 + public static void printPublishedLotto(List lotto) { + for (Lotto l: lotto) + System.out.println(l.getNumbers().toString()); + } + + // 당첨 통계 출력 + public static void printWinningResult(Map winningResult, double returnRate) { + System.out.println(WINNING_STATICS_MESSAGE); + + winningResult.forEach((rank, count) -> { + String reword = getFormattingResult(rank.getReword()); + + if (rank == WinningRank.LAST_RANK) return; + if (rank == WinningRank.SECOND_BONUS_RANK) { + System.out.printf(WINNING_BONUS_DETAIL_MESSAGE, rank.getMatch(), reword, count); + return; + } + System.out.printf(WINNING_DETAIL_MESSAGE, rank.getMatch(), reword, count); + }); + + System.out.printf(RETURN_RATE_MESSAGE, returnRate); + } + + // 당첨금 형식 설정 + public static String getFormattingResult(int result) { + DecimalFormat df = new DecimalFormat(COMMA_FORMAT); + return df.format(result); + } +} 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;