diff --git a/README.md b/README.md index 5fa2560b46..b55934ac2b 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ # java-lotto-precourse + +# 기능 구현목록 +- [X] 로또 구입 예산 입력기능 +- [X] 로또 구입 개수 출력기능 +- [X] 구입금액메시지 출력 기능 +- [X] 구입예산에 따른 로또 추출기능 +- [X] 추출된 로또 조합 출력기능 +- [X] 발행한 로또 수량,번호 출력 기능 +- [X] 당첨번호 입력 받는 기능 +- [X] 로또 당첨번호 검증 기능 +- [X] 입력받은 당첨번호 출력 기능 +- [X] 보너스번호 입력 받는 기능 +- [X] 보너스번호 입력메시지 출력기능 +- [X] 보너스 번호 검증기능 +- [X] 당첨내역 계산 기능 +- [X] 당첨내역 출력 기능 +- [X] 수익률 계산 기능 +- [X] 수익률 출력 기능 diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..1be51ae668 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,277 @@ package lotto; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +enum Messages { + PURCHASEABLE_INPUT_MESSAGE("구입금액을 입력해 주세요."), + ANSWER_INPUT_MESSAGE("당첨 번호를 입력해 주세요."), + BONUS_INPUT_MESSAGE("보너스 번호를 입력해 주세요."), + STATISTICS_HEADER("당첨 통계\n---"), + ERROR_MESSAGE("[ERROR] "); + + private final String description; + + Messages(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} + +enum Prize { + FIRST(6, 2_000_000_000, "6개 일치"), + SECOND(5, 30_000_000, "5개 일치, 보너스 볼 일치"), + THIRD(5, 1_500_000, "5개 일치"), + FOURTH(4, 50_000, "4개 일치"), + FIFTH(3, 5_000, "3개 일치"), + NONE(0, 0, "일치하는 번호 없음"); + + private final int matchCount; + private final int prizeMoney; + private final String description; + + Prize(int matchCount, int prizeMoney, String description) { + this.matchCount = matchCount; + this.prizeMoney = prizeMoney; + this.description = description; + } + + public static Prize of(int matchCount, boolean matchBonus) { + if (matchCount == 6) { + return FIRST; + } + if (matchCount == 5 && matchBonus) { + return SECOND; + } + if (matchCount == 5) { + return THIRD; + } + if (matchCount == 4) { + return FOURTH; + } + if (matchCount == 3) { + return FIFTH; + } + return NONE; + } + + public int getPrizeMoney() { + return prizeMoney; + } + + public String getDescription() { + return description; + } +} + +record WinningStatistics( + Map prizeCount, + double profitRate +) {} + public class Application { + private static final int TICKET_PRICE = 1000; + public static void main(String[] args) { // TODO: 프로그램 구현 + try { + // 예산 입력받기 기능 + int totalLottoCnt = getPurchaseableLottoNum(getBudgetInput()); + + // 입력받은 금액 출력 기능 + printTotalLottoNum(totalLottoCnt); + + // 로또 추출기능 + List> extractedLottoNums = extractLottoNumbers(totalLottoCnt); + + // 당첨번호 입력받기 + Lotto lotto = new Lotto(getJackpotInput()); + + // 보너스 번호 입력받기 + int bonusNum = lotto.addBonusNumbers(getBonusNumber()); + + // 당첨 여부 판단 + WinningStatistics statistics = calculateResults(extractedLottoNums, lotto.getNumbers(), bonusNum); + + // 당첨 여부 출력 + printStatistics(statistics); + } catch (IllegalArgumentException e) { + System.out.println(Messages.ERROR_MESSAGE.getDescription() + e.getMessage()); + } + } + + // 예산 입력받기 기능구현 + public static Integer getPurchaseableLottoNum(int budget) { + if (budget < TICKET_PRICE) { + throw new IllegalArgumentException("구입 금액은 1000원 이상이어야 합니다."); + } + return budget / TICKET_PRICE; + } + + // 입력받은 금액 출력 기능 + public static void printTotalLottoNum(int totalLottoCNt){ + System.out.println(totalLottoCNt + "개를 구매했습니다."); + } + + // 구입금액 메시지 출력기능 + public static void printPurchaseableInputMessage(){ + System.out.println(Messages.PURCHASEABLE_INPUT_MESSAGE.getDescription()); + } + + public static Integer getBudgetInput() { + printPurchaseableInputMessage(); + String input = camp.nextstep.edu.missionutils.Console.readLine(); + validateNumericInput(input); + return Integer.parseInt(input); + } + + // 입력값 검증기능 + private static void validateNumericInput(String input) { + if (!input.matches("\\d+")) { + throw new IllegalArgumentException("숫자만 입력 가능합니다."); + } + } + + // 로또 추출기능 + public static List> extractLottoNumbers(int totalLottoCnt) { + List> lottos = new ArrayList<>(); + for (int i = 0; i < totalLottoCnt; i++) { + List numbers = camp.nextstep.edu.missionutils.Randoms.pickUniqueNumbersInRange(1, 45, 6); + printExtractedLottoNumbers(numbers); + lottos.add(numbers); + } + return lottos; + } + + // 추출된 로또번호 출력기능 + public static void printExtractedLottoNumbers(List numbers){ + System.out.println(numbers); } + + // 로또 당첨번호 입력 기능 + public static List getJackpotInput() { + pringJackpotInputMessage(); + String input = camp.nextstep.edu.missionutils.Console.readLine(); + return parseAndValidateNumbers(input); + } + + // 로또 당첨번호 입력메시지 출력 기능 + public static void pringJackpotInputMessage(){ + System.out.println(Messages.ANSWER_INPUT_MESSAGE.getDescription()); + } + + private static List parseAndValidateNumbers(String input) { + try { + List numbers = Arrays.stream(input.split(",")) + .map(String::trim) + .map(Integer::parseInt) + .collect(Collectors.toList()); + validateLottoNumbers(numbers); + return numbers; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("올바른 숫자 형식이 아닙니다."); + } + } + + // 로또 번호 형태 검증기능 + private static void validateLottoNumbers(List numbers) { + if (numbers.size() != 6) { + throw new IllegalArgumentException("로또 번호는 6개여야 합니다."); + } + if (numbers.stream().distinct().count() != 6) { + throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다."); + } + if (numbers.stream().anyMatch(n -> n < 1 || n > 45)) { + throw new IllegalArgumentException("로또 번호는 1부터 45 사이여야 합니다."); + } + } + + // 보너스 번호 입력받기 + public static Integer getBonusNumber() { + printBonumInputMessage(); + String input = camp.nextstep.edu.missionutils.Console.readLine().trim(); + validateNumericInput(input); + int number = Integer.parseInt(input); + if (number < 1 || number > 45) { + throw new IllegalArgumentException("보너스 번호는 1부터 45 사이여야 합니다."); + } + return number; + } + + // 보너스 번호입력 메시지 출력기능 + public static void printBonumInputMessage(){ + System.out.println(Messages.BONUS_INPUT_MESSAGE.getDescription()); + } + + private static WinningStatistics calculateResults( + List> purchasedLottos, + List winningNumbers, + int bonusNumber + ) { + Map prizeCount = initializePrizeCount(); + + for (List lotto : purchasedLottos) { + int matchCount = countMatches(lotto, winningNumbers); + boolean matchBonus = (matchCount == 5) && lotto.contains(bonusNumber); + Prize prize = Prize.of(matchCount, matchBonus); + prizeCount.merge(prize, 1, Integer::sum); + } + + int totalPrize = calculateTotalPrize(prizeCount); + double profitRate = calculateProfitRate(purchasedLottos.size() * TICKET_PRICE, totalPrize); + + return new WinningStatistics(prizeCount, profitRate); + } + + private static Map initializePrizeCount() { + return Arrays.stream(Prize.values()) + .collect(Collectors.toMap( + prize -> prize, + prize -> 0, + (prev, current) -> current, + HashMap::new + )); + } + + private static int countMatches(List lotto, List winningNumbers) { + return (int) lotto.stream() + .filter(winningNumbers::contains) + .count(); + } + + private static int calculateTotalPrize(Map prizeCount) { + return prizeCount.entrySet().stream() + .mapToInt(entry -> entry.getKey().getPrizeMoney() * entry.getValue()) + .sum(); + } + + private static double calculateProfitRate(int totalCost, int totalPrize) { + return ((double) totalPrize / totalCost) * 100; + } + + private static void printStatistics(WinningStatistics statistics) { + System.out.println(Messages.STATISTICS_HEADER.getDescription()); + + Arrays.stream(Prize.values()) + .filter(prize -> prize != Prize.NONE) + .forEach(prize -> printPrizeResult(prize, statistics.prizeCount().get(prize))); + + System.out.printf("총 수익률은 %.1f%%입니다.%n", statistics.profitRate()); + } + + private static void printPrizeResult(Prize prize, int count) { + System.out.printf("%s (%,d원) - %d개%n", + prize.getDescription(), + prize.getPrizeMoney(), + count); + } + } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 88fc5cf12b..42c562ce85 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -1,5 +1,6 @@ package lotto; +import java.util.HashSet; import java.util.List; public class Lotto { @@ -11,10 +12,48 @@ public Lotto(List numbers) { } private void validate(List numbers) { + validateSize(numbers); + validateDuplicate(numbers); + validateRange(numbers); + } + + public List getNumbers() { + return numbers; + } + + private void validateSize(List numbers) { if (numbers.size() != 6) { - throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); + throw new IllegalArgumentException("로또 번호는 6개여야 합니다."); } } - // TODO: 추가 기능 구현 + private void validateDuplicate(List numbers) { + if (new HashSet<>(numbers).size() != 6) { + throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다."); + } + } + + private void validateRange(List numbers) { + if (numbers.stream().anyMatch(num -> num < 1 || num > 45)) { + throw new IllegalArgumentException("로또 번호는 1부터 45 사이여야 합니다."); + } + } + + // 보너스 번호 생성 + public int addBonusNumbers(int bonusNumber) { + validateBonusNumber(bonusNumber); + return bonusNumber; + } + + // 보너스 번호 검증기능 + private void validateBonusNumber(int bonusNumber) { + if (numbers.contains(bonusNumber)) { + throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + if (bonusNumber < 1 || bonusNumber > 45) { + throw new IllegalArgumentException("보너스 번호는 1부터 45 사이여야 합니다."); + } + } + + } diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 309f4e50ae..4f9ef8f949 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -5,7 +5,9 @@ import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; class LottoTest { @Test @@ -22,4 +24,53 @@ class LottoTest { } // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + + @DisplayName("로또 자동 생성 시 요청한 개수만큼 로또를 생성한다.") + @Test + void 자동_생성된_로또_개수를_검증한다() { + int totalCount = 5; + + List> lottos = Application.extractLottoNumbers(totalCount); + + assertThat(lottos).hasSize(totalCount); + } + + @DisplayName("로또 자동 생성 시 각 로또는 1~45 사이의 중복되지 않은 6개의 숫자여야 한다.") + @Test + void 자동_생성된_로또_번호_형태를_검증한다() { + int totalCount = 5; + + List> lottos = Application.extractLottoNumbers(totalCount); + + for (List lotto : lottos) { + assertThat(lotto).hasSize(6); + assertThat(lotto).doesNotHaveDuplicates(); + assertThat(lotto).allMatch(number -> number >= 1 && number <= 45); + } + } + + + @DisplayName("구입 금액을 1000으로 나눈 값만큼 로또를 구매한다.") + @Test + void 구입_금액에_해당하는_수량만큼_로또를_구매한다() { + int count = Application.getPurchaseableLottoNum(8000); + + assertThat(count).isEqualTo(8); + } + + @DisplayName("일치 개수와 보너스 번호에 따라 올바른 등수가 매핑된다.") + @Test + void 당첨_조건에_따라_등수를_반환한다() { + assertAll( + () -> assertThat(Prize.of(6, false)).isEqualTo(Prize.FIRST), + () -> assertThat(Prize.of(5, true)).isEqualTo(Prize.SECOND), + () -> assertThat(Prize.of(5, false)).isEqualTo(Prize.THIRD), + () -> assertThat(Prize.of(4, false)).isEqualTo(Prize.FOURTH), + () -> assertThat(Prize.of(3, false)).isEqualTo(Prize.FIFTH), + () -> assertThat(Prize.of(2, false)).isEqualTo(Prize.NONE), + () -> assertThat(Prize.of(1, false)).isEqualTo(Prize.NONE), + () -> assertThat(Prize.of(0, false)).isEqualTo(Prize.NONE) + ); + } + }