diff --git a/README.md b/README.md new file mode 100644 index 000000000..7afa57c02 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# 로또 (Lotto) 미션 + +## 프로젝트 소개 + +이 프로그램은 주어진 기능 및 프로그래밍 요구사항을 만족하는 콘솔 기반의 로또 게임을 구현하는 것을 목표로 합니다. +사용자는 로또를 수동 또는 자동으로 구매하고, 당첨 번호와 비교하여 당첨금 관련 결과를 확인할 수 있습니다. + +--- + +## 기능 요구사항 + +- 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또 티켓을 발급합니다. +- 사용자가 수동으로 로또 번호를 입력할 수 있어야 합니다. + - 수동으로 구매할 로또 수를 입력받고, 그 수만큼 로또 번호를 입력받습니다. + - 수동 구매 후 남은 금액만큼 자동으로 로또를 발급합니다. +- 지난 주 당첨 번호 6개와 보너스 볼 1개를 입력받습니다. +- 로또 번호와 당첨 번호를 비교하여 당첨 결과를 결정합니다. +- 최종적으로 당첨 통계와 수익률을 계산하여 출력합니다. + - 수익률이 1 미만일 경우 손해임을 명시합니다. + - 수익률이 1 이상일 경우 이득임을 명시합니다. (임의 설정) + +--- + +## 비즈니스 규칙 + +- **로또 구매 규칙** + - 로또 1장의 가격은 1,000원입니다. + - 구매 금액은 1,000원 단위로 입력해야 합니다. +- **로또 번호 규칙** + - 로또 번호는 1부터 45 사이의 숫자입니다. + - 로또 한 장은 중복되지 않는 6개의 숫자로 구성됩니다. + - 수동으로 로또를 구매할 때 6개의 번호를 입력하지 않으면 오류가 발생합니다. +- **당첨 조건 및 상금** + - 1등: 6개 번호 일치 (2,000,000,000원) + - 2등: 5개 번호 일치 + 보너스 볼 일치 (30,000,000원) + - 3등: 5개 번호 일치 (1,500,000원) + - 4등: 4개 번호 일치 (50,000원) + - 5등: 3개 번호 일치 (5,000원) + +--- + +## 프로그래밍 요구사항 + +- **코드 컨벤션**: 자바 코드 컨벤션을 지키면서 프로그래밍합니다. +- **들여쓰기**: `indent`(인덴트, 들여쓰기) depth를 1까지만 허용합니다. (메서드 분리로 해결) +- **`else` 사용 금지**: `else` 예약어, `switch/case`, 3항 연산자를 사용하지 않습니다. (if문에서 값 반환으로 해결) +- **메서드 분리**: + - 함수(또는 메서드)의 길이는 10라인을 넘어가지 않도록 구현합니다. + - 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만듭니다. +- **자료구조**: 배열이 아닌 컬렉션을 사용합니다. +- **축약 금지**: 변수명, 클래스명, 메서드명 등에 축약을 사용하지 않습니다. diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 000000000..eaf8999ca --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,77 @@ +import static view.InputView.lottoScanner; + +import domain.Lotto; +import domain.LottoChecker; +import domain.LottoResult; +import domain.LottoStatistics; +import domain.LottoTicketCount; +import domain.LottoTickets; +import domain.LottoWinningType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import view.InputView; +import view.OutputView; + +public class Application { + + public static void main(String[] args) { + int totalCount = LottoTicketCount.convertLottoPriceToTicketCount(InputView.inputLottoTotalPrice()); + + int manualCount = InputView.inputUserSelectedLottoCount(totalCount); + List userSelectedLottos = inputUserSelectedLottos(manualCount); + + int autoCount = totalCount - manualCount; + + OutputView.printLottoCount(manualCount, autoCount); + + LottoTickets lottoTickets = new LottoTickets(); + lottoTickets.addUserSelectedLottos(userSelectedLottos); + lottoTickets.addAutoLottos(autoCount); + + OutputView.printLottoNumbers(lottoTickets); + + Lotto winningLotto = inputWinningLotto(); + String bonusNumber = InputView.inputBonusBallNumber(); + + LottoChecker lottoChecker = new LottoChecker(winningLotto, lottoTickets, bonusNumber); + ArrayList checkedTickets = lottoChecker.checkAllTickets(); + + LottoStatistics lottoStatistics = new LottoStatistics(); + Map countedMatches = lottoStatistics.countMatches(checkedTickets); + + OutputView.printMatchCount(countedMatches); + + LottoResult lottoResult = new LottoResult(lottoStatistics, (totalCount * LottoTicketCount.PRICE_PER_ONE_LOTTO_TICKET)); + OutputView.printRateOfReturn(lottoResult.calculateProfitRate()); + + InputView.closeScanner(lottoScanner); + } + + private static List inputUserSelectedLottos(int manualCount) { + InputView.printUserSelectedLottoNumbersPrompt(); + List userSelectedLottos = new ArrayList<>(); + for (int i = 0; i < manualCount; i++) { + userSelectedLottos.add(readLottoWithRetry("로또 번호는 숫자로만 구성되어야 합니다.")); + } + return userSelectedLottos; + } + + private static Lotto inputWinningLotto() { + InputView.printWinningLottoNumbersPrompt(); + return readLottoWithRetry("당첨 번호는 숫자로만 구성되어야 합니다."); + } + + private static Lotto readLottoWithRetry(String numberFormatErrorMessage) { + while (true) { + try { + return new Lotto(InputView.readLottoNumbers()); + } catch (NumberFormatException e) { + OutputView.printError(numberFormatErrorMessage); + } catch (IllegalArgumentException e) { + OutputView.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/domain/Lotto.java b/src/main/java/domain/Lotto.java new file mode 100644 index 000000000..ef9a35811 --- /dev/null +++ b/src/main/java/domain/Lotto.java @@ -0,0 +1,46 @@ +package domain; + +import java.util.List; +import java.util.Random; +import java.util.TreeSet; + +public class Lotto { + public static final int LOTTO_NUMBER_LOWER_BOUND = 1; + public static final int LOTTO_NUMBER_BOUND = 45; + public static final int LOTTO_NUMBER_COUNT = 6; + + TreeSet numbers = new TreeSet<>(); + Random random = new Random(); + + public Lotto() { + generateRandomNumbers(); + } + + public Lotto(List userSelectedNumbers) { + if (userSelectedNumbers.size() != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException("로또 번호는" + LOTTO_NUMBER_COUNT + "개여야 합니다."); + } + for (Integer number : userSelectedNumbers) { + if (number < LOTTO_NUMBER_LOWER_BOUND || number > LOTTO_NUMBER_BOUND) { + throw new IllegalArgumentException("로또 번호는 " + LOTTO_NUMBER_LOWER_BOUND + + "부터 " + LOTTO_NUMBER_BOUND + " 사이의 숫자여야 합니다."); + } + } + this.numbers.addAll(userSelectedNumbers); + if (this.numbers.size() != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다."); + } + } + + public TreeSet getNumbers() { + return this.numbers; + } + + private void generateRandomNumbers() { + while (numbers.size() < LOTTO_NUMBER_COUNT) { + numbers.add(random.nextInt(LOTTO_NUMBER_LOWER_BOUND, LOTTO_NUMBER_BOUND + 1)); + } + + } + +} diff --git a/src/main/java/domain/LottoChecker.java b/src/main/java/domain/LottoChecker.java new file mode 100644 index 000000000..0195c9bbc --- /dev/null +++ b/src/main/java/domain/LottoChecker.java @@ -0,0 +1,62 @@ +package domain; + +import java.util.ArrayList; + +public class LottoChecker { + + private final ArrayList winningLottoNumbers; + private final LottoTickets lottoTickets; + + private final int bonusNumber; + + public LottoChecker(Lotto winningLotto, LottoTickets lottoTickets, String bonusNumber) { + this.winningLottoNumbers = new ArrayList<>(winningLotto.getNumbers()); + this.lottoTickets = lottoTickets; + validateBonusNumber(bonusNumber); + this.bonusNumber = Integer.parseInt(bonusNumber); + } + + public ArrayList checkAllTickets() { + ArrayList winningTypes = new ArrayList<>(); + + for (int i = 0; i < lottoTickets.getSize(); i++) { + int matchCount = calculateMatchCountForTicket(i); + boolean matchBonus = hasBonusNumber(i); + + winningTypes.add(LottoWinningType.of(matchCount, matchBonus)); + } + return winningTypes; + } + + public boolean hasBonusNumber(int lottoTicketIndex) { + return lottoTickets.getTicketNumbers(lottoTicketIndex).contains(this.bonusNumber); + } + + private void validateBonusNumber(String bonusNumber) { + try { + int number = Integer.parseInt(bonusNumber); + if (number < Lotto.LOTTO_NUMBER_LOWER_BOUND || number > Lotto.LOTTO_NUMBER_BOUND) { + throw new IllegalArgumentException( + "보너스 볼은" + Lotto.LOTTO_NUMBER_LOWER_BOUND + "과" + Lotto.LOTTO_NUMBER_BOUND + "사이의 숫자여야 합니다."); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("보너스 볼은 숫자여야 합니다."); + } + } + + private int calculateMatchCountForTicket(int lottoTicketIndex) { + int matchCount = 0; + for (int winningNumber : winningLottoNumbers) { + matchCount += getMatchScore(lottoTicketIndex, winningNumber); + } + return matchCount; + } + + private int getMatchScore(int lottoTicketIndex, int winningNumber) { + if (lottoTickets.getTicketNumbers(lottoTicketIndex).contains(winningNumber)) { + return 1; + } + return 0; + } + +} diff --git a/src/main/java/domain/LottoResult.java b/src/main/java/domain/LottoResult.java new file mode 100644 index 000000000..d60ed664a --- /dev/null +++ b/src/main/java/domain/LottoResult.java @@ -0,0 +1,35 @@ +package domain; + +import java.util.Map; + +public class LottoResult { + + private final LottoStatistics lottoStatistics; + private final int purchaseAmount; + + public LottoResult(LottoStatistics lottoStatistics, int purchaseAmount) { + this.lottoStatistics = lottoStatistics; + this.purchaseAmount = purchaseAmount; + } + + public double calculateProfitRate() { + long totalWinningPrize = calculateTotalPrize(); + if (totalWinningPrize == 0) { + return 0.0; + } + return (double) totalWinningPrize / purchaseAmount; + } + + private long calculateTotalPrize() { + long totalPrize = 0; + Map stats = lottoStatistics.getMatchStatistics(); + + for (Map.Entry entry : stats.entrySet()) { + LottoWinningType type = entry.getKey(); + int count = entry.getValue(); + totalPrize += (long) type.calculatePrize(count); + } + return totalPrize; + } + +} diff --git a/src/main/java/domain/LottoStatistics.java b/src/main/java/domain/LottoStatistics.java new file mode 100644 index 000000000..8e9e40e92 --- /dev/null +++ b/src/main/java/domain/LottoStatistics.java @@ -0,0 +1,26 @@ +package domain; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class LottoStatistics { + private Map matchStatistics = new HashMap<>(); + + public LottoStatistics() { + for (LottoWinningType type : LottoWinningType.values()) { + matchStatistics.put(type, 0); + } + } + + public Map countMatches(ArrayList winningTypes) { + for (LottoWinningType type : winningTypes) { + matchStatistics.put(type, matchStatistics.get(type) + 1); + } + return matchStatistics; + } + + public Map getMatchStatistics() { + return matchStatistics; + } +} diff --git a/src/main/java/domain/LottoTicketCount.java b/src/main/java/domain/LottoTicketCount.java new file mode 100644 index 000000000..b0e8cfe9f --- /dev/null +++ b/src/main/java/domain/LottoTicketCount.java @@ -0,0 +1,22 @@ +package domain; + +public class LottoTicketCount { + public static final int PRICE_PER_ONE_LOTTO_TICKET = 1000; + + private LottoTicketCount() { + } + + public static int convertLottoPriceToTicketCount(int totalLottoPrice) { + validatePurchaseAmount(totalLottoPrice); + return totalLottoPrice / PRICE_PER_ONE_LOTTO_TICKET; + } + + private static void validatePurchaseAmount(int price) { + if (price < PRICE_PER_ONE_LOTTO_TICKET) { + throw new IllegalArgumentException("구입 금액은 " + PRICE_PER_ONE_LOTTO_TICKET + "원 이상이어야 합니다."); + } + if (price % PRICE_PER_ONE_LOTTO_TICKET != 0) { + throw new IllegalArgumentException("구입 금액은 " + PRICE_PER_ONE_LOTTO_TICKET + "원 단위로 입력해야 합니다."); + } + } +} diff --git a/src/main/java/domain/LottoTickets.java b/src/main/java/domain/LottoTickets.java new file mode 100644 index 000000000..a76f825da --- /dev/null +++ b/src/main/java/domain/LottoTickets.java @@ -0,0 +1,27 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +public class LottoTickets { + ArrayList lottos = new ArrayList<>(); + + public void addUserSelectedLottos(List userSelectedLottos) { + lottos.addAll(userSelectedLottos); + } + + public void addAutoLottos(int autoCount) { + for (int i = 0; i < autoCount; i++) { + lottos.add(new Lotto()); + } + } + + public TreeSet getTicketNumbers(int ticketIndex){ + return new TreeSet<>(lottos.get(ticketIndex).getNumbers()); + } + + public int getSize() { + return lottos.size(); + } +} diff --git a/src/main/java/domain/LottoWinningType.java b/src/main/java/domain/LottoWinningType.java new file mode 100644 index 000000000..22a366cd0 --- /dev/null +++ b/src/main/java/domain/LottoWinningType.java @@ -0,0 +1,40 @@ +package domain; + +import java.util.function.Function; + +public enum LottoWinningType { + FIRST_PLACE("6개 일치 (2000000000원)- ", tickets -> tickets * 2000000000), + SECOND_PLACE("5개 일치, 보너스 볼 일치(30000000원)- ", tickets -> tickets * 30000000), + THIRD_PLACE("5개 일치 (1500000원)- ", tickets -> tickets * 1500000), + FOURTH_PLACE("4개 일치 (50000원)- ", tickets -> tickets * 50000), + FIFTH_PLACE("3개 일치 (5000원)- ", tickets -> tickets * 5000), + NO_PRIZE("2개 이하 일치 (0원)- ", tickets -> 0d); + + private String winningDescription; + private Function prizeCalculator; + + + LottoWinningType(String winningDescription, Function prizeCalculator) { + this.winningDescription = winningDescription; + this.prizeCalculator = prizeCalculator; + + } + + public double calculatePrize(double winningTicketCount) { + return prizeCalculator.apply(winningTicketCount); + } + + public String getWinningDescription() { + return winningDescription; + } + + public static LottoWinningType of(int matchCount, boolean matchBonus) { + if (matchCount == 6) return FIRST_PLACE; + if (matchCount == 5 && matchBonus) return SECOND_PLACE; + if (matchCount == 5) return THIRD_PLACE; + if (matchCount == 4) return FOURTH_PLACE; + if (matchCount == 3) return FIFTH_PLACE; + return NO_PRIZE; + } + +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 000000000..15048900f --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,119 @@ +package view; + +import domain.Lotto; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +public final class InputView { + public static final int PRICE_PER_ONE_LOTTO_TICKET = 1000; + public static Scanner lottoScanner = new Scanner(System.in); + + private InputView() { + } + + public static int inputLottoTotalPrice() { + System.out.println("구입 금액을 입력해 주세요."); + while (true) { + try { + String input = lottoScanner.nextLine(); + int price = Integer.parseInt(input); + validatePurchaseAmount(price); + return price; + } catch (NumberFormatException e) { + System.out.println("[ERROR] 구입 금액은 숫자로만 입력해야 합니다. 다시 입력해 주세요."); + } catch (IllegalArgumentException e) { + System.out.println("[ERROR] " + e.getMessage() + " 다시 입력해 주세요."); + } + } + } + + public static int inputUserSelectedLottoCount(int totalCount) { + System.out.println("\n수동으로 구매할 로또 수를 입력해 주세요."); + while (true) { + try { + String input = lottoScanner.nextLine(); + int manualCount = Integer.parseInt(input); + validateManualCount(manualCount, totalCount); + return manualCount; + } catch (NumberFormatException e) { + System.out.println("[ERROR] 로또 개수는 숫자로만 입력해야 합니다. 다시 입력해 주세요."); + } catch (IllegalArgumentException e) { + System.out.println("[ERROR] " + e.getMessage() + " 다시 입력해 주세요."); + } + } + } + + public static void printUserSelectedLottoNumbersPrompt() { + System.out.println("\n수동으로 구매할 번호를 입력해 주세요."); + } + + public static List readLottoNumbers() { + String numbersString = lottoScanner.nextLine(); + return parseLottoNumbers(numbersString); + } + + public static void printWinningLottoNumbersPrompt() { + System.out.println("\n지난 주 당첨번호를 입력해 주세요"); + } + + public static String inputBonusBallNumber() { + System.out.println("\n보너스 볼을 입력해 주세요."); + while (true) { + try { + String bonusNumberStr = lottoScanner.nextLine(); + validateBonusBall(bonusNumberStr); + return bonusNumberStr; + } catch (NumberFormatException e) { + System.out.println("[ERROR] 보너스 볼은 숫자로만 입력해야 합니다. 다시 입력해 주세요."); + } catch (IllegalArgumentException e) { + System.out.println("[ERROR] " + e.getMessage() + " 다시 입력해 주세요."); + } + } + } + + public static void closeScanner(Scanner scanner) { + if (scanner != null) { + scanner.close(); + } + } + + private static void validatePurchaseAmount(int price) { + if (price < PRICE_PER_ONE_LOTTO_TICKET) { + throw new IllegalArgumentException("구입 금액은 " + PRICE_PER_ONE_LOTTO_TICKET + "원 이상이어야 합니다."); + } + if (price % PRICE_PER_ONE_LOTTO_TICKET != 0) { + throw new IllegalArgumentException("구입 금액은 " + PRICE_PER_ONE_LOTTO_TICKET + "원 단위로 입력해야 합니다."); + } + } + + private static void validateManualCount(int manualCount, int totalCount) { + if (manualCount < 0) { + throw new IllegalArgumentException("수동 구매 개수는 0 이상이어야 합니다."); + } + if (manualCount > totalCount) { + throw new IllegalArgumentException("수동 구매 개수는 전체 구매 개수(" + totalCount + "개)를 초과할 수 없습니다."); + } + } + + private static List parseLottoNumbers(String numbersString) { + return Arrays.stream(numbersString.split(",\\s*")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + private static void validateBonusBall(String bonusNumberStr) { + if (bonusNumberStr == null || bonusNumberStr.trim().isEmpty()) { + throw new IllegalArgumentException("보너스 볼 번호를 입력해야 합니다."); + } + if (bonusNumberStr.contains(" ") || bonusNumberStr.contains(",")) { + throw new IllegalArgumentException("보너스 볼은 하나의 숫자만 입력해야 합니다."); + } + int bonusNumber = Integer.parseInt(bonusNumberStr); + if (bonusNumber < Lotto.LOTTO_NUMBER_LOWER_BOUND || bonusNumber > Lotto.LOTTO_NUMBER_BOUND) { + throw new IllegalArgumentException("보너스 볼은 " + Lotto.LOTTO_NUMBER_LOWER_BOUND + + "과 " + Lotto.LOTTO_NUMBER_BOUND + " 사이의 숫자여야 합니다."); + } + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 000000000..9db5f493d --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,53 @@ +package view; + +import domain.LottoTickets; +import domain.LottoWinningType; +import java.util.List; +import java.util.Map; + +public final class OutputView { + private OutputView() { + + } + + public static void printError(String message) { + System.out.println("[ERROR] " + message + " 다시 입력해 주세요."); + } + + public static void printLottoCount(int userSelectedCount, int autoCount) { + System.out.printf("\n수동으로 %d장, 자동으로 %d개를 구매했습니다.\n", userSelectedCount, autoCount); + } + + public static void printLottoNumbers(LottoTickets lottoTickets) { + for(int i = 0; i< lottoTickets.getSize(); i++) { + System.out.println(lottoTickets.getTicketNumbers(i)); + } + } + + public static void printMatchCount(Map matchStatistics) { + System.out.println("\n당첨 통계"); + System.out.println("---------"); + + List printOrder = List.of( + LottoWinningType.FIFTH_PLACE, + LottoWinningType.FOURTH_PLACE, + LottoWinningType.THIRD_PLACE, + LottoWinningType.SECOND_PLACE, + LottoWinningType.FIRST_PLACE + ); + + for (LottoWinningType type : printOrder) { + System.out.println(type.getWinningDescription() + matchStatistics.get(type) + "개"); + } + } + + public static void printRateOfReturn(double rateOfReturn) { + if (rateOfReturn < 1) { + System.out.printf("총 수익률은 %.2f입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임)\n", rateOfReturn); + } + if (rateOfReturn >= 1) { + System.out.printf("총 수익률은 %.2f입니다.(기준이 1이기 때문에 결과적으로 이득이라는 의미임)\n", rateOfReturn); + } + } + +} diff --git a/src/test/java/domain/LottoCheckerTest.java b/src/test/java/domain/LottoCheckerTest.java new file mode 100644 index 000000000..6a996ce1d --- /dev/null +++ b/src/test/java/domain/LottoCheckerTest.java @@ -0,0 +1,159 @@ +package domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("LottoChecker 클래스") +class LottoCheckerTest { + + private Lotto winningNumbers; + private String bonusNumber; + + @BeforeEach + void setUp() { + winningNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + bonusNumber = "7"; + } + + private LottoTickets createLottoTickets(List numberStrings) { + LottoTickets lottoTickets = new LottoTickets(); + List lottos = numberStrings.stream() + .map(numbersString -> List.of(numbersString.split(",\\s*")).stream() + .map(Integer::parseInt) + .collect(Collectors.toList())) + .map(Lotto::new) + .collect(Collectors.toList()); + lottoTickets.addUserSelectedLottos(lottos); + return lottoTickets; + } + + @Nested + @DisplayName("생성자 유효성 검사") + class ConstructorValidation { + + @Test + @DisplayName("보너스 볼이 숫자가 아닐 경우 예외가 발생한다.") + void throwExceptionWhenBonusNumberIsNotNumeric() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 4, 5, 6")); + String invalidBonusNumber = "a"; + + assertThatThrownBy(() -> new LottoChecker(winningNumbers, lottoTickets, invalidBonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("보너스 볼은 숫자여야 합니다."); + } + + @Test + @DisplayName("보너스 볼이 범위를 벗어날 경우 예외가 발생한다.") + void throwExceptionWhenBonusNumberIsOutOfRange() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 4, 5, 6")); + String invalidBonusNumber = "46"; + + assertThatThrownBy(() -> new LottoChecker(winningNumbers, lottoTickets, invalidBonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("보너스 볼은" + Lotto.LOTTO_NUMBER_LOWER_BOUND + "과" + Lotto.LOTTO_NUMBER_BOUND + "사이의 숫자여야 합니다."); + } + } + + @Nested + @DisplayName("단일 티켓 당첨 결과 확인") + class SingleTicketResult { + + @Test + @DisplayName("1등 당첨을 확인한다.") + void checkFirstPrize() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 4, 5, 6")); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly(LottoWinningType.FIRST_PLACE); + } + + @Test + @DisplayName("2등 당첨을 확인한다.") + void checkSecondPrize() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 4, 5, 7")); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly(LottoWinningType.SECOND_PLACE); + } + + @Test + @DisplayName("3등 당첨을 확인한다.") + void checkThirdPrize() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 4, 5, 8")); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly(LottoWinningType.THIRD_PLACE); + } + + @Test + @DisplayName("4등 당첨을 확인한다.") + void checkFourthPrize() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 4, 8, 9")); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly(LottoWinningType.FOURTH_PLACE); + } + + @Test + @DisplayName("5등 당첨을 확인한다.") + void checkFifthPrize() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 3, 8, 9, 10")); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly(LottoWinningType.FIFTH_PLACE); + } + + @Test + @DisplayName("꽝을 확인한다.") + void checkMiss() { + LottoTickets lottoTickets = createLottoTickets(List.of("1, 2, 8, 9, 10, 11")); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly(LottoWinningType.NO_PRIZE); + } + } + + @Nested + @DisplayName("여러 티켓 당첨 결과 확인") + class MultipleTicketsResult { + + @Test + @DisplayName("여러 티켓의 당첨 결과를 확인한다.") + void checkMultipleTickets() { + LottoTickets lottoTickets = createLottoTickets(List.of( + "1, 2, 3, 4, 5, 6", + "10, 11, 12, 13, 14, 15", + "1, 2, 3, 8, 9, 10" + )); + LottoChecker lottoChecker = new LottoChecker(winningNumbers, lottoTickets, bonusNumber); + + List results = lottoChecker.checkAllTickets(); + + assertThat(results).containsExactly( + LottoWinningType.FIRST_PLACE, + LottoWinningType.NO_PRIZE, + LottoWinningType.FIFTH_PLACE + ); + } + } +} diff --git a/src/test/java/domain/LottoResultTest.java b/src/test/java/domain/LottoResultTest.java new file mode 100644 index 000000000..18558187f --- /dev/null +++ b/src/test/java/domain/LottoResultTest.java @@ -0,0 +1,59 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class LottoResultTest { + + @DisplayName("수익률을 계산한다.") + @Test + void calculateProfitRate() { + ArrayList winningTypes = new ArrayList<>(List.of(LottoWinningType.FIFTH_PLACE)); + LottoStatistics lottoStatistics = new LottoStatistics(); + lottoStatistics.countMatches(winningTypes); + + int purchaseAmount = 8000; + LottoResult lottoResult = new LottoResult(lottoStatistics, purchaseAmount); + + double profitRate = lottoResult.calculateProfitRate(); + + assertThat(profitRate).isEqualTo(0.625); + } + + @DisplayName("여러 당첨 건에 대한 수익률을 계산한다.") + @Test + void calculateProfitRateWithMultipleWinnings() { + ArrayList winningTypes = new ArrayList<>(List.of( + LottoWinningType.FOURTH_PLACE, + LottoWinningType.FIFTH_PLACE + )); + LottoStatistics lottoStatistics = new LottoStatistics(); + lottoStatistics.countMatches(winningTypes); + + int purchaseAmount = 10000; + LottoResult lottoResult = new LottoResult(lottoStatistics, purchaseAmount); + + double profitRate = lottoResult.calculateProfitRate(); + assertThat(profitRate).isEqualTo(5.5); + } + + @DisplayName("당첨금이 없을 때 수익률은 0이다.") + @Test + void calculateProfitRateWithNoWinnings() { + ArrayList winningTypes = new ArrayList<>(); + LottoStatistics lottoStatistics = new LottoStatistics(); + lottoStatistics.countMatches(winningTypes); + + int purchaseAmount = 1000; + LottoResult lottoResult = new LottoResult(lottoStatistics, purchaseAmount); + + double profitRate = lottoResult.calculateProfitRate(); + + assertThat(profitRate).isEqualTo(0.0); + } +} diff --git a/src/test/java/domain/LottoStatisticsTest.java b/src/test/java/domain/LottoStatisticsTest.java new file mode 100644 index 000000000..9774dfcd5 --- /dev/null +++ b/src/test/java/domain/LottoStatisticsTest.java @@ -0,0 +1,68 @@ +package domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("LottoStatistics 클래스") +class LottoStatisticsTest { + + private LottoStatistics lottoStatistics; + + @BeforeEach + void setUp() { + lottoStatistics = new LottoStatistics(); + } + + @Test + @DisplayName("당첨 결과를 집계한다.") + void shouldCountMatchesCorrectly() { + ArrayList winningTypes = new ArrayList<>(List.of( + LottoWinningType.FIRST_PLACE, + LottoWinningType.FIFTH_PLACE, + LottoWinningType.NO_PRIZE, + LottoWinningType.FIFTH_PLACE, + LottoWinningType.NO_PRIZE, + LottoWinningType.NO_PRIZE + )); + + Map stats = lottoStatistics.countMatches(winningTypes); + + assertThat(stats.get(LottoWinningType.FIRST_PLACE)).isEqualTo(1); + assertThat(stats.get(LottoWinningType.SECOND_PLACE)).isEqualTo(0); + assertThat(stats.get(LottoWinningType.THIRD_PLACE)).isEqualTo(0); + assertThat(stats.get(LottoWinningType.FOURTH_PLACE)).isEqualTo(0); + assertThat(stats.get(LottoWinningType.FIFTH_PLACE)).isEqualTo(2); + assertThat(stats.get(LottoWinningType.NO_PRIZE)).isEqualTo(3); + } + + @Test + @DisplayName("빈 당첨 결과 리스트를 전달하면 모든 횟수는 0으로 유지된다.") + void shouldHandleEmptyWinningList() { + ArrayList winningTypes = new ArrayList<>(); + + Map stats = lottoStatistics.countMatches(winningTypes); + + assertThat(stats.values()).allMatch(count -> count == 0); + } + + @Test + @DisplayName("getMatchStatistics는 현재 통계를 반환한다.") + void getMatchStatisticsShouldReturnCurrentStats() { + ArrayList winningTypes = new ArrayList<>(List.of( + LottoWinningType.FOURTH_PLACE + )); + lottoStatistics.countMatches(winningTypes); + + Map stats = lottoStatistics.getMatchStatistics(); + + assertThat(stats.get(LottoWinningType.FOURTH_PLACE)).isEqualTo(1); + assertThat(stats.get(LottoWinningType.FIFTH_PLACE)).isEqualTo(0); + } +} diff --git a/src/test/java/domain/LottoTest.java b/src/test/java/domain/LottoTest.java new file mode 100644 index 000000000..d939857a5 --- /dev/null +++ b/src/test/java/domain/LottoTest.java @@ -0,0 +1,59 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTest { + + @DisplayName("자동으로 로또를 생성하면 설정된 개수만큼 랜덤로또번호가 생성된다.") + @Test + void createLottoAutomatically() { + Lotto lotto = new Lotto(); + + assertThat(lotto.getNumbers()).hasSize(Lotto.LOTTO_NUMBER_COUNT); + } + + @DisplayName("자동으로 생성된 로또 번호는 1과 45 사이의 값이다.") + @Test + void validateNumberRange() { + Lotto lotto = new Lotto(); + + assertThat(lotto.getNumbers()).allMatch(number -> number >= Lotto.LOTTO_NUMBER_LOWER_BOUND && number <= Lotto.LOTTO_NUMBER_BOUND); + } + + @DisplayName("수동으로 로또를 생성한다.") + @Test + void createLottoManually() { + List userSelectedNumbers = List.of(1, 2, 3, 4, 5, 6); + + Lotto lotto = new Lotto(userSelectedNumbers); + + assertThat(lotto.getNumbers()).hasSize(Lotto.LOTTO_NUMBER_COUNT); + assertThat(lotto.getNumbers()).containsAll(userSelectedNumbers); + } + + @DisplayName("수동으로 로또를 생성할 때 번호가 6개가 아니면 예외가 발생한다.") + @Test + void throwExceptionWhenManualLottoHasInvalidSize() { + List numbers = List.of(1, 2, 3, 4, 5); + + assertThatThrownBy(() -> new Lotto(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("로또 번호는" + Lotto.LOTTO_NUMBER_COUNT + "개여야 합니다."); + } + + @DisplayName("수동으로 로또를 생성할 때 중복된 번호가 있으면 예외가 발생한다.") + @Test + void throwExceptionWhenManualLottoHasDuplicateNumbers() { + List numbers = List.of(1, 2, 3, 4, 5, 5); + + assertThatThrownBy(() -> new Lotto(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("로또 번호는 중복될 수 없습니다."); + } +} diff --git a/src/test/java/domain/LottoTicketCountTest.java b/src/test/java/domain/LottoTicketCountTest.java new file mode 100644 index 000000000..83f9b46f3 --- /dev/null +++ b/src/test/java/domain/LottoTicketCountTest.java @@ -0,0 +1,54 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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; + +@DisplayName("LottoTicketCount 클래스") +class LottoTicketCountTest { + + @Nested + @DisplayName("정상적인 금액 입력 시") + class ValidAmount { + @DisplayName("구매 금액을 로또 티켓 수로 변환한다.") + @ParameterizedTest + @CsvSource({ + "14000, 14", + "1000, 1", + "2000, 2" + }) + void shouldConvertPriceToTicketCountCorrectly(int price, int expectedCount) { + int actualCount = LottoTicketCount.convertLottoPriceToTicketCount(price); + + assertThat(actualCount).isEqualTo(expectedCount); + } + } + + @Nested + @DisplayName("유효하지 않은 금액 입력 시") + class InvalidAmount { + + @DisplayName("1000원 미만일 경우 예외를 발생시킨다.") + @ParameterizedTest + @ValueSource(ints = {0, 100, 999}) + void shouldThrowExceptionForAmountLessThan1000(int price) { + assertThatThrownBy(() -> LottoTicketCount.convertLottoPriceToTicketCount(price)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("구입 금액은 " + LottoTicketCount.PRICE_PER_ONE_LOTTO_TICKET + "원 이상이어야 합니다."); + } + + @DisplayName("1000원 단위가 아닐 경우 예외를 발생시킨다.") + @ParameterizedTest + @ValueSource(ints = {1001, 1500, 2999}) + void shouldThrowExceptionForAmountNotMultipleOf1000(int price) { + assertThatThrownBy(() -> LottoTicketCount.convertLottoPriceToTicketCount(price)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("구입 금액은 " + LottoTicketCount.PRICE_PER_ONE_LOTTO_TICKET + "원 단위로 입력해야 합니다."); + } + } +} diff --git a/src/test/java/domain/LottoTicketsTest.java b/src/test/java/domain/LottoTicketsTest.java new file mode 100644 index 000000000..5db7e6c98 --- /dev/null +++ b/src/test/java/domain/LottoTicketsTest.java @@ -0,0 +1,78 @@ +package domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.TreeSet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("LottoTickets 클래스") +class LottoTicketsTest { + + private LottoTickets lottoTickets; + + @BeforeEach + void setUp() { + lottoTickets = new LottoTickets(); + } + + @Nested + @DisplayName("addUserSelectedLottos 메소드는") + class AddUserSelectedLottos { + + @Test + @DisplayName("이미 생성된 로또 목록을 그대로 추가한다.") + void shouldAddUserSelectedLottos() { + List lottos = List.of( + new Lotto(List.of(1, 2, 3, 4, 5, 6)), + new Lotto(List.of(7, 8, 9, 10, 11, 12)) + ); + + lottoTickets.addUserSelectedLottos(lottos); + + assertThat(lottoTickets.getSize()).isEqualTo(2); + assertThat(lottoTickets.getTicketNumbers(0)).containsExactly(1, 2, 3, 4, 5, 6); + assertThat(lottoTickets.getTicketNumbers(1)).containsExactly(7, 8, 9, 10, 11, 12); + } + } + + @Nested + @DisplayName("addAutoLottos 메소드는") + class AddAutoLottos { + + @Test + @DisplayName("주어진 개수만큼 자동 로또를 생성하고 추가한다.") + void shouldAddAutoLottos() { + int autoCount = 3; + + lottoTickets.addAutoLottos(autoCount); + + assertThat(lottoTickets.getSize()).isEqualTo(3); + + for (int i = 0; i < autoCount; i++) { + assertThat(lottoTickets.getTicketNumbers(i)).hasSize(Lotto.LOTTO_NUMBER_COUNT); + } + } + } + + @Nested + @DisplayName("getTicketNumbers 메소드는") + class GetTicketNumbers { + + @Test + @DisplayName("지정된 인덱스의 로또 번호 Set을 반환한다.") + void shouldReturnCorrectLottoSet() { + lottoTickets.addAutoLottos(1); + lottoTickets.addUserSelectedLottos(List.of(new Lotto(List.of(1, 2, 3, 4, 5, 6)))); + + TreeSet manualLottoSet = lottoTickets.getTicketNumbers(1); + + assertThat(manualLottoSet).containsExactly(1, 2, 3, 4, 5, 6); + } + } +} diff --git a/src/test/java/domain/LottoWinningTypeTest.java b/src/test/java/domain/LottoWinningTypeTest.java new file mode 100644 index 000000000..ea76859c9 --- /dev/null +++ b/src/test/java/domain/LottoWinningTypeTest.java @@ -0,0 +1,59 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("LottoWinningType Enum") +class LottoWinningTypeTest { + + @Nested + @DisplayName("of 메소드는") + class OfTest { + + @DisplayName("일치 개수와 보너스 여부에 따라 정확한 등수를 반환한다.") + @ParameterizedTest + @CsvSource({ + "6, false, FIRST_PLACE", + "5, true, SECOND_PLACE", + "5, false, THIRD_PLACE", + "4, false, FOURTH_PLACE", + "3, false, FIFTH_PLACE", + "2, false, NO_PRIZE", + "1, false, NO_PRIZE", + "0, false, NO_PRIZE" + }) + void returnsCorrectWinningType(int matchCount, boolean matchBonus, LottoWinningType expectedType) { + LottoWinningType actualType = LottoWinningType.of(matchCount, matchBonus); + + assertThat(actualType).isEqualTo(expectedType); + } + } + + @Nested + @DisplayName("calculatePrize 메소드는") + class CalculatePrizeTest { + + @Test + @DisplayName("각 등수별 정확한 상금을 계산한다.") + void calculatesCorrectPrize() { + assertThat(LottoWinningType.FIRST_PLACE.calculatePrize(1)).isEqualTo(2_000_000_000); + assertThat(LottoWinningType.SECOND_PLACE.calculatePrize(1)).isEqualTo(30_000_000); + assertThat(LottoWinningType.THIRD_PLACE.calculatePrize(1)).isEqualTo(1_500_000); + assertThat(LottoWinningType.FOURTH_PLACE.calculatePrize(1)).isEqualTo(50_000); + assertThat(LottoWinningType.FIFTH_PLACE.calculatePrize(1)).isEqualTo(5_000); + assertThat(LottoWinningType.NO_PRIZE.calculatePrize(1)).isEqualTo(0); + } + + @Test + @DisplayName("여러 티켓 당첨 시 총 상금을 계산한다.") + void calculatesCorrectTotalPrizeForMultipleTickets() { + assertThat(LottoWinningType.FIFTH_PLACE.calculatePrize(3)).isEqualTo(15_000); + } + } + +}