diff --git a/README.md b/README.md new file mode 100644 index 000000000..474631395 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# 로또 - 클린 코드 + +## 미션 소개 + +로또 구매 금액과 수동 구매 번호를 입력받아 로또를 발급하고, +지난주 당첨 번호와 보너스 번호를 기준으로 당첨 결과와 수익률을 계산하는 프로그램 + +--- + +## 요구사항 + +- 로또 한 장의 가격은 1,000원이다. +- 로또 번호는 1부터 45 사이의 숫자이다. +- 하나의 로또는 서로 다른 6개의 번호로 구성된다. +- 구입 금액에 따라 구매 가능한 로또 개수를 계산한다. +- 사용자가 원하는 개수만큼 로또 번호를 수동으로 입력할 수 있다. +- 수동으로 구매하고 남은 개수는 자동으로 생성한다. +- 자동 로또 번호는 중복되지 않는 6개의 숫자로 생성한다. +- 지난주 당첨 번호 6개와 보너스 번호 1개를 입력받는다. +- 당첨 번호와 보너스 번호는 중복될 수 없다. +- 당첨 번호의 일치 개수와 보너스 번호 일치 여부에 따라 등수를 결정한다. +- 당첨 결과를 등수별로 집계한다. +- 총 당첨 금액을 기준으로 수익률을 계산한다. +- Java Enum을 활용하여 로또 등수와 상금을 관리한다. +- 메서드의 길이는 최대한 10라인을 넘지 않도록 구현한다. +- 들여쓰기 depth는 1을 넘지 않도록 구현한다. + +### 추가 구현 기준 +- 최소 구입 금액은 로또 1장 가격과 동일한 1,000원으로 정한다. +- 1,000원 미만의 금액을 입력하면 예외가 발생한다. + +--- + +## 기능 목록 + +### 로또 구입 + +- [x] 구입 금액 입력 +- [x] 최소 구입 금액 검증 +- [x] 구입 금액에 따른 총 구매 가능 개수 계산 +- [x] 수동 구매 개수 입력 +- [x] 수동 구매 개수가 음수인지 검증 +- [x] 수동 구매 개수가 전체 구매 가능 개수를 초과하는지 검증 +- [x] 수동 구매 개수를 제외한 자동 구매 개수 계산 + +### 로또 생성 + +- [x] 수동 로또 번호 입력 +- [x] 수동 로또 번호 개수 검증 +- [x] 수동 로또 번호 중복 검증 +- [x] 자동 로또 번호 생성 +- [x] 로또 번호 범위 검증 +- [x] 로또 번호 정렬 +- [x] 수동 로또와 자동 로또를 하나의 컬렉션으로 관리 + +### 당첨 번호 + +- [x] 지난주 당첨 번호 입력 +- [x] 당첨 번호 개수 검증 +- [x] 당첨 번호 중복 검증 +- [x] 보너스 번호 입력 +- [x] 보너스 번호와 당첨 번호의 중복 검증 + +### 당첨 결과 + +- [x] 로또와 당첨 번호의 일치 개수 계산 +- [x] 보너스 번호 일치 여부 확인 +- [x] 일치 개수와 보너스 번호에 따른 당첨 등수 결정 +- [x] 등수별 당첨 개수 집계 +- [x] 총 당첨 금액 계산 +- [x] 수익률 계산 +- [x] 당첨 통계 출력 + +--- + +## 당첨 기준 + +| 등수 | 당첨 기준 | 상금 | +| --- | --- | ---: | +| 1등 | 6개 번호 일치 | 2,000,000,000원 | +| 2등 | 5개 번호 + 보너스 번호 일치 | 30,000,000원 | +| 3등 | 5개 번호 일치 | 1,500,000원 | +| 4등 | 4개 번호 일치 | 50,000원 | +| 5등 | 3개 번호 일치 | 5,000원 | + +--- + +## 주요 객체 + +| 객체 | 역할 | +| --- | --- | +| `PurchaseAmount` | 구입 금액을 관리하고 구매 가능한 로또 개수를 계산한다. | +| `LottoNumber` | 로또 번호 하나를 관리하고 번호의 범위를 검증한다. | +| `Lotto` | 6개의 로또 번호를 관리하고 자동 또는 수동으로 로또를 생성한다. | +| `Lottos` | 여러 개의 로또를 관리한다. | +| `WinningNumbers` | 당첨 번호를 관리하고 로또와 일치하는 번호 개수를 계산한다. | +| `LottoRank` | 당첨 등수와 상금을 관리한다. | +| `LottoStatistics` | 등수별 당첨 결과와 수익률을 계산한다. | +| `InputView` | 사용자 입력을 담당한다. | +| `OutputView` | 로또 및 당첨 결과 출력을 담당한다. | + +--- + +## 테스트 + +### PurchaseAmountTest + +- 구입 금액에 따른 로또 구매 개수 계산 +- 최소 구입 금액 검증 +- 수동 구매 개수가 음수인 경우 검증 +- 수동 구매 개수가 구매 가능 개수를 초과하는 경우 검증 +- 수동 구매 개수를 제외한 자동 구매 개수 계산 +- 모든 로또를 수동으로 구매한 경우 자동 구매 개수가 0인지 확인 + +### LottoNumberTest + +- 로또 번호의 최소/최대 범위 확인 +- 로또 번호가 유효 범위를 벗어난 경우 예외 확인 + +### LottoTest + +- 로또가 6개의 번호로 생성되는지 확인 +- 로또 번호 중복 여부 확인 +- 직접 지정한 번호가 6개가 아닌 경우 예외 확인 + +### LottosTest + +- 수동 로또와 자동 로또를 합한 개수만큼 로또가 생성되는지 확인 +- 수동 구매가 없는 경우 자동 로또만 생성되는지 확인 + +### WinningNumbersTest + +- 당첨 번호가 6개인지 확인 +- 당첨 번호 중복 검증 +- 보너스 번호와 당첨 번호 중복 검증 + +### LottoRankTest + +- 일치 개수와 보너스 번호 일치 여부에 따라 올바른 당첨 등수를 반환하는지 확인 +- 당첨 기준에 미달하면 당첨되지 않는지 확인 + +### LottoStatisticsTest + +- 실제 로또의 당첨 결과를 등수별로 올바르게 집계하는지 확인 + + diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 000000000..b4a7b8e8c --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,53 @@ +import domain.Lotto; +import domain.LottoNumber; +import domain.LottoStatistics; +import domain.Lottos; +import domain.PurchaseAmount; +import domain.WinningLotto; + +import view.InputView; +import view.OutputView; + +import java.util.List; + +public class Application { + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + + PurchaseAmount purchaseAmount = new PurchaseAmount(inputView.readPurchaseAmount()); + + Lottos lottos = purchaseLottos(inputView, outputView, purchaseAmount); + + WinningLotto winningLotto = createWinningLotto(inputView); + + printStatistics(outputView, lottos, winningLotto, purchaseAmount); + } + + private static Lottos purchaseLottos(InputView inputView, OutputView outputView, PurchaseAmount purchaseAmount) { + int manualLottoCount = inputView.readManualLottoCount(); + int autoLottoCount = purchaseAmount.calculateAutoLottoCount(manualLottoCount); + + List> manualLottoNumbers = inputView.readManualLottoNumbers(manualLottoCount); + + Lottos lottos = new Lottos(manualLottoNumbers, autoLottoCount); + + outputView.printPurchasedCount(manualLottoCount, autoLottoCount); + outputView.printLottos(lottos); + + return lottos; + } + + private static WinningLotto createWinningLotto(InputView inputView) { + Lotto winningNumbers = new Lotto(inputView.readWinningNumbers()); + LottoNumber bonusNumber = new LottoNumber(inputView.readBonusNumber()); + + return new WinningLotto(winningNumbers, bonusNumber); + } + + private static void printStatistics(OutputView outputView, Lottos lottos, + WinningLotto winningLotto, PurchaseAmount purchaseAmount) { + LottoStatistics lottoStatistics = new LottoStatistics(lottos, winningLotto); + outputView.printLottoStatistics(purchaseAmount, lottoStatistics); + } +} diff --git a/src/main/java/domain/Lotto.java b/src/main/java/domain/Lotto.java new file mode 100644 index 000000000..1dc807b85 --- /dev/null +++ b/src/main/java/domain/Lotto.java @@ -0,0 +1,66 @@ +package domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +public class Lotto { + private static final int NUMBER_COUNT = 6; + + private final List numbers = new ArrayList<>(); + + public Lotto(List numberValues) { + validateNumberCount(numberValues); + validateDuplicateNumbers(numberValues); + + convertToLottoNumbers(numberValues); + + Collections.sort(numbers); + } + + public List getNumbers() { + return List.copyOf(numbers); + } + + private void convertToLottoNumbers(List numberValues) { + for (int number : numberValues) { + LottoNumber lottoNumber = new LottoNumber(number); + numbers.add(lottoNumber); + } + } + + public int countMatchingNumbers(Lotto other) { + int count = 0; + + for (LottoNumber lottoNumber : other.numbers) { + count += countMatch(lottoNumber); + } + + return count; + } + + private int countMatch(LottoNumber lottoNumber) { + if (containsNumber(lottoNumber)) { + return 1; + } + + return 0; + } + + public boolean containsNumber(LottoNumber lottoNumber) { + return numbers.contains(lottoNumber); + } + + private void validateNumberCount(List numberValues) { + if (numberValues.size() != NUMBER_COUNT) { + throw new IllegalArgumentException("로또 번호는 " + NUMBER_COUNT + "개여야 합니다."); + } + } + + private void validateDuplicateNumbers(List numberValues) { + if (new HashSet<>(numberValues).size() != numberValues.size()) { + throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다."); + } + } +} diff --git a/src/main/java/domain/LottoGenerator.java b/src/main/java/domain/LottoGenerator.java new file mode 100644 index 000000000..862e8cd65 --- /dev/null +++ b/src/main/java/domain/LottoGenerator.java @@ -0,0 +1,40 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class LottoGenerator { + private static final int NUMBER_COUNT = 6; + private static final int RANDOM_NUMBER_BOUND = 45; + + private final Random random = new Random(); + + public Lotto generate() { + return new Lotto(generateNumbers()); + } + + private List generateNumbers() { + List numbers = new ArrayList<>(); + + for (int i = 0; i < NUMBER_COUNT; i++) { + numbers.add(generateRandomNumber(numbers)); + } + + return numbers; + } + + private int generateRandomNumber(List numbers) { + int number; + + do { + number = random.nextInt(RANDOM_NUMBER_BOUND) + 1; + } while (containsNumber(numbers, number)); + + return number; + } + + private boolean containsNumber(List numbers, int number) { + return numbers.contains(number); + } +} diff --git a/src/main/java/domain/LottoNumber.java b/src/main/java/domain/LottoNumber.java new file mode 100644 index 000000000..e08a596d1 --- /dev/null +++ b/src/main/java/domain/LottoNumber.java @@ -0,0 +1,51 @@ +package domain; + +import java.util.Objects; + +public class LottoNumber implements Comparable{ + private static final int MIN_LOTTO_NUMBER = 1; + private static final int MAX_LOTTO_NUMBER = 45; + + private final int number; + + public LottoNumber(int number) { + validateNumberRange(number); + + this.number = number; + } + + public int getNumber() { + return number; + } + + @Override + public int compareTo(LottoNumber lottoNumber) { + return Integer.compare(this.number, lottoNumber.number); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof LottoNumber)) { + return false; + } + + LottoNumber lottoNumber = (LottoNumber) object; + + return this.number == lottoNumber.number; + } + + @Override + public int hashCode() { + return Objects.hash(number); + } + + private void validateNumberRange(int number) { + if (number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER) { + throw new IllegalArgumentException("로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } +} diff --git a/src/main/java/domain/LottoRank.java b/src/main/java/domain/LottoRank.java new file mode 100644 index 000000000..c5674925a --- /dev/null +++ b/src/main/java/domain/LottoRank.java @@ -0,0 +1,44 @@ +package domain; + +import java.util.Arrays; +import java.util.List; + +public enum LottoRank { + MISS(0,0), + FIFTH(3, 5000), + FOURTH(4,50000), + THIRD(5, 1500000), + SECOND(5, 30000000), + FIRST(6, 2000000000); + + private final int matchCount; + private final long prize; + + LottoRank(int matchCount, long prize) { + this.matchCount = matchCount; + this.prize = prize; + } + + public static LottoRank from(int matchCount, boolean bonusMatched) { + if (matchCount == 6) return FIRST; + if (matchCount == 5 && bonusMatched) return SECOND; + if (matchCount == 5) return THIRD; + if (matchCount == 4) return FOURTH; + if (matchCount == 3) return FIFTH; + return MISS; + } + + public int getMatchCount() { + return matchCount; + } + + public long getPrize() { + return prize; + } + + public static List winningRanks() { + return Arrays.stream(values()) + .filter(rank -> rank != MISS) + .toList(); + } +} diff --git a/src/main/java/domain/LottoStatistics.java b/src/main/java/domain/LottoStatistics.java new file mode 100644 index 000000000..63430a8e8 --- /dev/null +++ b/src/main/java/domain/LottoStatistics.java @@ -0,0 +1,54 @@ +package domain; + +import java.util.EnumMap; +import java.util.Map; + +public class LottoStatistics { + private final Map winningCounts = new EnumMap<>(LottoRank.class); + + public LottoStatistics(Lottos lottos, WinningLotto winningLotto) { + initializeWinningCounts(); + + calculateWinningCounts(lottos, winningLotto); + } + + public int getWinningCount(LottoRank rank) { + return winningCounts.get(rank); + } + + public double calculateProfitRate(PurchaseAmount purchaseAmount) { + return (double)calculateTotalPrize() / purchaseAmount.getAmount(); + } + + private long calculateTotalPrize() { + long total = 0; + + for (LottoRank rank : LottoRank.values()) { + total += getWinningCount(rank) * rank.getPrize(); + } + + return total; + } + + private void initializeWinningCounts() { + for (LottoRank rank : LottoRank.values()) { + winningCounts.put(rank, 0); + } + } + + private void calculateWinningCounts(Lottos lottos, WinningLotto winningLotto) { + for (Lotto lotto : lottos.getLottos()) { + processLottoResult(lotto, winningLotto); + } + } + + private void processLottoResult(Lotto lotto, WinningLotto winningLotto) { + LottoRank rank = winningLotto.determineRank(lotto); + + increaseWinningCount(rank); + } + + private void increaseWinningCount(LottoRank rank) { + winningCounts.put(rank, winningCounts.get(rank) + 1); + } +} diff --git a/src/main/java/domain/Lottos.java b/src/main/java/domain/Lottos.java new file mode 100644 index 000000000..1bcf7afd8 --- /dev/null +++ b/src/main/java/domain/Lottos.java @@ -0,0 +1,23 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; + +public class Lottos { + private final List lottos = new ArrayList<>(); + private final LottoGenerator lottoGenerator = new LottoGenerator(); + + public Lottos(List> manualLottoNumbers, int autoLottoCount) { + for (List manualLottoNumber : manualLottoNumbers) { + lottos.add(new Lotto(manualLottoNumber)); + } + + for (int i = 0; i < autoLottoCount; i++) { + lottos.add(lottoGenerator.generate()); + } + } + + public List getLottos() { + return List.copyOf(lottos); + } +} diff --git a/src/main/java/domain/PurchaseAmount.java b/src/main/java/domain/PurchaseAmount.java new file mode 100644 index 000000000..8e2e0e676 --- /dev/null +++ b/src/main/java/domain/PurchaseAmount.java @@ -0,0 +1,51 @@ +package domain; + +public class PurchaseAmount { + private static final int LOTTO_PRICE = 1000; + private static final int MIN_MANUAL_LOTTO_COUNT = 0; + + private final int amount; + + public PurchaseAmount(int amount) { + validateMinimumPurchaseAmount(amount); + validatePurchaseAmountUnit(amount); + + this.amount = amount; + } + + public int getAmount() { + return amount; + } + + public int calculateLottoCount() { + return amount / LOTTO_PRICE; + } + + public int calculateAutoLottoCount(int manualLottoCount) { + validateManualLottoCount(manualLottoCount); + + return calculateLottoCount() - manualLottoCount; + } + + private void validateManualLottoCount(int manualLottoCount) { + if (manualLottoCount > calculateLottoCount()) { + throw new IllegalArgumentException("수동 구매 개수는 전체 구매 가능 개수를 초과할 수 없습니다."); + } + + if (manualLottoCount < MIN_MANUAL_LOTTO_COUNT) { + throw new IllegalArgumentException("수동 구매 개수는 " + MIN_MANUAL_LOTTO_COUNT + "이상이어야 합니다."); + } + } + + private void validateMinimumPurchaseAmount(int amount) { + if (amount < LOTTO_PRICE) { + throw new IllegalArgumentException("최소 구입 금액은 " + LOTTO_PRICE + "원 입니다."); + } + } + + private void validatePurchaseAmountUnit(int amount) { + if (amount % LOTTO_PRICE != 0) { + throw new IllegalArgumentException("구입 금액은 1,000원 단위여야 합니다."); + } + } +} diff --git a/src/main/java/domain/WinningLotto.java b/src/main/java/domain/WinningLotto.java new file mode 100644 index 000000000..cafe33af8 --- /dev/null +++ b/src/main/java/domain/WinningLotto.java @@ -0,0 +1,26 @@ +package domain; + +public class WinningLotto { + private final Lotto winningNumbers; + private final LottoNumber bonusNumber; + + public WinningLotto(Lotto winningNumbers, LottoNumber bonusNumber) { + validateBonusNumber(winningNumbers, bonusNumber); + + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + public LottoRank determineRank(Lotto lotto) { + int matchCount = winningNumbers.countMatchingNumbers(lotto); + boolean bonusMatched = lotto.containsNumber(bonusNumber); + + return LottoRank.from(matchCount, bonusMatched); + } + + private void validateBonusNumber(Lotto winningNumbers, LottoNumber bonusNumber) { + if (winningNumbers.containsNumber(bonusNumber)) { + throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 000000000..0824dd034 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,74 @@ +package view; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class InputView { + private final Scanner scanner = new Scanner(System.in); + + public int readPurchaseAmount() { + System.out.println("구입금액을 입력해 주세요."); + String inputPurchaseAmount = scanner.nextLine(); + int purchaseAmount = Integer.parseInt(inputPurchaseAmount); + + System.out.println(); + + return purchaseAmount; + } + + public int readManualLottoCount() { + System.out.println("수동으로 구매할 로또 수를 입력해 주세요."); + String inputManualLottoCount = scanner.nextLine(); + int manualLottoCount = Integer.parseInt(inputManualLottoCount); + + System.out.println(); + + return manualLottoCount; + } + + public List> readManualLottoNumbers(int manualLottoCount) { + List> manualLottoNumbers = new ArrayList<>(); + + System.out.println("수동으로 구매할 번호를 입력해 주세요."); + + for (int i = 0; i < manualLottoCount; i++) { + String inputNumbers = scanner.nextLine(); + manualLottoNumbers.add(parseNumbers(inputNumbers)); + } + + System.out.println(); + + return manualLottoNumbers; + } + + public List readWinningNumbers() { + System.out.println("지난 주 당첨 번호를 입력해 주세요."); + String winningNumbers = scanner.nextLine(); + System.out.println(); + + return parseNumbers(winningNumbers); + } + + public int readBonusNumber() { + System.out.println("보너스 볼을 입력해 주세요."); + String inputBonusNumber = scanner.nextLine(); + int bonusNumber = Integer.parseInt(inputBonusNumber); + + System.out.println(); + + return bonusNumber; + } + + private List parseNumbers(String input) { + List numbers = new ArrayList<>(); + + String[] numberStrings = input.split(","); + + for (String numberString : numberStrings) { + numbers.add(Integer.parseInt(numberString.trim())); + } + + return numbers; + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 000000000..0e5338ffb --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,69 @@ +package view; + +import domain.Lotto; +import domain.LottoNumber; +import domain.LottoRank; +import domain.LottoStatistics; +import domain.Lottos; +import domain.PurchaseAmount; + +import java.util.ArrayList; +import java.util.List; + +public class OutputView { + public void printPurchasedCount(int manualLottoCount, int autoLottoCount) { + System.out.println("수동으로 " + manualLottoCount + "장, 자동으로 " + autoLottoCount + "개를 구매했습니다."); + } + + public void printLottos(Lottos lottos) { + for (Lotto lotto : lottos.getLottos()) { + printLotto(lotto); + } + System.out.println(); + } + + private void printLotto(Lotto lotto) { + List numberValues = new ArrayList<>(); + + for (LottoNumber lottoNumber : lotto.getNumbers()) { + numberValues.add(lottoNumber.getNumber()); + } + + System.out.println(numberValues); + } + + public void printLottoStatistics(PurchaseAmount purchaseAmount, LottoStatistics lottoStatistics) { + printStatisticsHeader(); + + printWinningCounts(lottoStatistics); + + printProfitRate(lottoStatistics, purchaseAmount); + } + + private void printStatisticsHeader() { + System.out.println("당첨 통계"); + System.out.println("---------"); + } + + private void printWinningCounts(LottoStatistics lottoStatistics) { + for (LottoRank rank : LottoRank.winningRanks()) { + printWinningResult(lottoStatistics, rank); + } + } + + private void printWinningResult(LottoStatistics lottoStatistics, LottoRank rank) { + if (rank == LottoRank.SECOND) { + System.out.println(rank.getMatchCount() + "개 일치, 보너스 볼 일치(" + + rank.getPrize() + "원) - " + + lottoStatistics.getWinningCount(rank) + "개"); + return; + } + System.out.println(rank.getMatchCount() + "개 일치 (" + + rank.getPrize() + "원)- " + + lottoStatistics.getWinningCount(rank) + "개"); + } + + private void printProfitRate(LottoStatistics lottoStatistics, PurchaseAmount purchaseAmount) { + System.out.println("총 수익률은 " + lottoStatistics.calculateProfitRate(purchaseAmount) + "입니다."); + } +} diff --git a/src/test/java/domain/LottoGeneratorTest.java b/src/test/java/domain/LottoGeneratorTest.java new file mode 100644 index 000000000..61207aa4c --- /dev/null +++ b/src/test/java/domain/LottoGeneratorTest.java @@ -0,0 +1,26 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LottoGeneratorTest { + @Test + @DisplayName("자동 생성된 로또 번호는 중복되지 않는다") + void lottoNumbersShouldNotContainDuplicates() { + LottoGenerator lottoGenerator = new LottoGenerator(); + Lotto lotto = lottoGenerator.generate(); + + assertThat(lotto.getNumbers()).doesNotHaveDuplicates(); + } + + @Test + @DisplayName("자동 생성된 로또는 6개의 번호를 가진다") + void lottoShouldContainsSixNumbers() { + LottoGenerator lottoGenerator = new LottoGenerator(); + Lotto lotto = lottoGenerator.generate(); + + assertThat(lotto.getNumbers()).hasSize(6); + } +} diff --git a/src/test/java/domain/LottoNumberTest.java b/src/test/java/domain/LottoNumberTest.java new file mode 100644 index 000000000..aa77e06cb --- /dev/null +++ b/src/test/java/domain/LottoNumberTest.java @@ -0,0 +1,29 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class LottoNumberTest { + @ParameterizedTest + @ValueSource(ints = {1, 45}) + @DisplayName("로또 번호가 1 이상 45 이하이면 생성할 수 있다") + void createLottoNumberWithinValidRange(int value) { + LottoNumber lottoNumber = new LottoNumber(value); + + assertEquals(value, lottoNumber.getNumber()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 46}) + @DisplayName("로또 번호가 유효한 범위를 벗어나면 예외가 발생한다") + void throwsExceptionWhenLottoNumberIsOutOfRange(int value) { + assertThrows( + IllegalArgumentException.class, + () -> new LottoNumber(value) + ); + } +} diff --git a/src/test/java/domain/LottoRankTest.java b/src/test/java/domain/LottoRankTest.java new file mode 100644 index 000000000..2d3344f2f --- /dev/null +++ b/src/test/java/domain/LottoRankTest.java @@ -0,0 +1,56 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LottoRankTest { + @Test + @DisplayName("6개 번호가 일치하면 1등이다") + void returnsFirstWhenSixNumbersMatch() { + LottoRank rank = LottoRank.from(6, false); + + assertEquals(LottoRank.FIRST, rank); + } + + @Test + @DisplayName("5개 번호와 보너스 번호가 일치하면 2등이다") + void returnsSecondWhenFiveNumbersAndBonusMatch() { + LottoRank rank = LottoRank.from(5, true); + + assertEquals(LottoRank.SECOND, rank); + } + + @Test + @DisplayName("5개 번호가 일치하고 보너스 번호가 일치하지 않으면 3등이다") + void returnsThirdWhenFiveNumbersMatchWithoutBonus() { + LottoRank rank = LottoRank.from(5, false); + + assertEquals(LottoRank.THIRD, rank); + } + + @Test + @DisplayName("4개 번호가 일치하면 4등이다") + void returnsFourthWhenFourNumbersMatch() { + LottoRank rank = LottoRank.from(4, false); + + assertEquals(LottoRank.FOURTH, rank); + } + + @Test + @DisplayName("3개 번호가 일치하면 5등이다") + void returnsFifthWhenThreeNumbersMatch() { + LottoRank rank = LottoRank.from(3, false); + + assertEquals(LottoRank.FIFTH, rank); + } + + @Test + @DisplayName("일치하는 번호가 3개 미만이면 당첨되지 않는다") + void returnsNullWhenMatchCountIsLessThanThree() { + LottoRank rank = LottoRank.from(2, false); + + assertEquals(LottoRank.MISS, rank); + } +} diff --git a/src/test/java/domain/LottoStatisticsTest.java b/src/test/java/domain/LottoStatisticsTest.java new file mode 100644 index 000000000..8033b9ed8 --- /dev/null +++ b/src/test/java/domain/LottoStatisticsTest.java @@ -0,0 +1,60 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class LottoStatisticsTest { + @Test + @DisplayName("로또 당첨 결과를 등수별로 집계한다") + void calculatesWinningCountForEachRank() { + List winningNumberValues = List.of(1, 2, 3, 4, 5, 6); + Lotto winningNumbers = new Lotto(winningNumberValues); + LottoNumber bonusNumber = new LottoNumber(7); + WinningLotto winningLotto = new WinningLotto(winningNumbers, bonusNumber); + + List> lottoNumbers = List.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 3, 4, 5, 7), + List.of(1, 2, 3, 4, 5, 8), + List.of(1, 2, 3, 4, 8, 9), + List.of(1, 2, 3, 8, 9, 10) + ); + + Lottos lottos = new Lottos(lottoNumbers, 0); + LottoStatistics lottoStatistics = new LottoStatistics(lottos, winningLotto); + + assertEquals(1, lottoStatistics.getWinningCount(LottoRank.FIRST)); + assertEquals(1, lottoStatistics.getWinningCount(LottoRank.SECOND)); + assertEquals(1, lottoStatistics.getWinningCount(LottoRank.THIRD)); + assertEquals(1, lottoStatistics.getWinningCount(LottoRank.FOURTH)); + assertEquals(1, lottoStatistics.getWinningCount(LottoRank.FIFTH)); + } + + @Test + @DisplayName("총 당첨 금액과 구입 금액을 기준으로 수익률을 계산한다") + void calculatesProfitRateBasedOnTotalPrize() { + Lottos lottos = new Lottos(createLottoNumbersForProfitRate(), 0); + Lotto winningNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + LottoNumber bonusNumber = new LottoNumber(7); + WinningLotto winningLotto = new WinningLotto(winningNumbers, bonusNumber); + PurchaseAmount purchaseAmount = new PurchaseAmount(5000); + + LottoStatistics lottoStatistics = new LottoStatistics(lottos, winningLotto); + + assertEquals(1.0, lottoStatistics.calculateProfitRate(purchaseAmount), 0.0001); + } + + private List> createLottoNumbersForProfitRate() { + return List.of( + List.of(1, 2, 3, 8, 9, 10), + List.of(1, 2, 8, 9, 10, 11), + List.of(1, 8, 9, 10, 11, 12), + List.of(8, 9, 10, 11, 12, 13), + List.of(14, 15, 16, 17, 18, 19) + ); + } +} diff --git a/src/test/java/domain/LottoTest.java b/src/test/java/domain/LottoTest.java new file mode 100644 index 000000000..53a9bc42c --- /dev/null +++ b/src/test/java/domain/LottoTest.java @@ -0,0 +1,50 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +public class LottoTest { + @Test + @DisplayName("로또 번호가 중복되면 예외가 발생한다") + void throwsExceptionWhenLottoNumbersContainDuplicates() { + List numbers = List.of(1, 2, 3, 4, 5, 5); + + assertThrows( + IllegalArgumentException.class, + () -> new Lotto(numbers) + ); + } + + @Test + @DisplayName("로또 번호가 정확히 6개면 생성된다") + void createsLottoWithSixNumbers() { + List numbers = List.of(1, 2, 3, 4, 5, 6); + + assertDoesNotThrow(() -> new Lotto(numbers)); + } + + @ParameterizedTest + @MethodSource("invalidLottoNumbers") + @DisplayName("로또 번호가 6개가 아니면 예외가 발생한다") + void throwsExceptionWhenLottoNumberCountIsNotSix(List numbers) { + assertThrows( + IllegalArgumentException.class, + () -> new Lotto(numbers) + ); + } + + static Stream> invalidLottoNumbers() { + return Stream.of( + List.of(1, 2, 3, 4, 5), + List.of(1, 2, 3, 4, 5, 6, 7) + ); + } +} diff --git a/src/test/java/domain/LottosTest.java b/src/test/java/domain/LottosTest.java new file mode 100644 index 000000000..78483886f --- /dev/null +++ b/src/test/java/domain/LottosTest.java @@ -0,0 +1,55 @@ +package domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class LottosTest { + @Test + @DisplayName("수동 로또를 포함하여 구매 개수만큼 로또를 생성한다") + void createsManualAndAutoLottos() { + PurchaseAmount purchaseAmount = new PurchaseAmount(12000); + + int manualLottoCount = 3; + int autoLottoCount = purchaseAmount.calculateAutoLottoCount(manualLottoCount); + List> manualLottoNumbers = List.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(7, 8, 9, 10, 11, 12), + List.of(13, 14, 15, 16, 17, 18) + ); + + Lottos lottos = new Lottos(manualLottoNumbers, autoLottoCount); + + assertEquals(12, lottos.getLottos().size()); + assertContainsManualLottos(lottos, manualLottoNumbers); + } + + private void assertContainsManualLottos(Lottos lottos, List> manualLottoNumbers) { + List> expectedManualLottos = manualLottoNumbers.stream() + .map(Lotto::new) + .map(Lotto::getNumbers) + .toList(); + + assertThat(lottos.getLottos()) + .extracting(Lotto::getNumbers) + .containsAll(expectedManualLottos); + } + + @Test + @DisplayName("수동 로또가 없으면 자동 구매 개수만큼 로또를 생성한다") + void createsOnlyAutoLottosWhenThereAreNoManualLottos() { + PurchaseAmount purchaseAmount = new PurchaseAmount(14000); + + int manualLottoCount = 0; + int autoLottoCount = purchaseAmount.calculateAutoLottoCount(manualLottoCount); + List> manualLottoNumbers = List.of(); + + Lottos lottos = new Lottos(manualLottoNumbers, autoLottoCount); + + assertEquals(14, lottos.getLottos().size()); + } +} diff --git a/src/test/java/domain/PurchaseAmountTest.java b/src/test/java/domain/PurchaseAmountTest.java new file mode 100644 index 000000000..697d6b95d --- /dev/null +++ b/src/test/java/domain/PurchaseAmountTest.java @@ -0,0 +1,88 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PurchaseAmountTest { + @Test + @DisplayName("구입 금액에 따라 구매 가능한 로또 개수를 계산한다") + void calculatesLottoCountBasedOnPurchaseAmount() { + PurchaseAmount purchaseAmount = new PurchaseAmount(5000); + + assertEquals(5, purchaseAmount.calculateLottoCount()); + } + + @Test + @DisplayName("최소 구입 금액으로 구매 가능한 로또 개수는 1개이다") + void calculatesOneLottoForMinimumPurchaseAmount() { + PurchaseAmount purchaseAmount = new PurchaseAmount(1000); + + assertEquals(1, purchaseAmount.calculateLottoCount()); + } + + @ParameterizedTest + @ValueSource(ints = {999, 0, -5000}) + @DisplayName("구입 금액이 최소 구입 금액보다 작으면 예외가 발생한다") + void throwsExceptionWhenPurchaseAmountIsLessThanLottoPrice(int amount) { + assertThrows( + IllegalArgumentException.class, + () -> new PurchaseAmount(amount) + ); + } + + @Test + @DisplayName("구입 금액이 로또 가격 단위가 아니면 예외가 발생한다") + void throwsExceptionWhenPurchaseAMountIsNotMultipleOfLottoPrice() { + assertThrows( + IllegalArgumentException.class, + () -> new PurchaseAmount(1500) + ); + } + + @Test + @DisplayName("수동 구매 개수가 음수이면 예외가 발생한다") + void throwsExceptionWhenManualLottoCountIsNegative() { + PurchaseAmount purchaseAmount = new PurchaseAmount(14000); + int manualLottoCount = -2; + + assertThrows( + IllegalArgumentException.class, + () -> purchaseAmount.calculateAutoLottoCount(manualLottoCount) + ); + } + + @Test + @DisplayName("수동 구매 개수가 전체 구매 가능 개수를 초과하면 예외가 발생한다") + void throwsExceptionWhenManualLottoCountExceedsTotalLottoCount() { + PurchaseAmount purchaseAmount = new PurchaseAmount(14000); + int manualLottoCount = 15; + + assertThrows( + IllegalArgumentException.class, + () -> purchaseAmount.calculateAutoLottoCount(manualLottoCount) + ); + } + + @Test + @DisplayName("수동 구매 개수를 제외한 자동 구매 개수를 계산한다") + void calculatesAutoLottoCountWithValidManualLottoCount() { + PurchaseAmount purchaseAmount = new PurchaseAmount(14000); + int manualLottoCount = 3; + + assertEquals(11, purchaseAmount.calculateAutoLottoCount(manualLottoCount)); + } + + @Test + @DisplayName("구매 가능한 로또를 모두 수동으로 구매하면 자동 구매 개수는 0이다") + void calculateZeroAutoLottoCountWhenAllLottosAreManual() { + PurchaseAmount purchaseAmount = new PurchaseAmount(14000); + int manualLottoCount = 14; + + assertEquals(0, purchaseAmount.calculateAutoLottoCount(manualLottoCount)); + } +} diff --git a/src/test/java/domain/WinningLottoTest.java b/src/test/java/domain/WinningLottoTest.java new file mode 100644 index 000000000..9abf1c0e7 --- /dev/null +++ b/src/test/java/domain/WinningLottoTest.java @@ -0,0 +1,23 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class WinningLottoTest { + @Test + @DisplayName("보너스 번호가 당첨 번호와 중복되면 예외가 발생한다") + void throwsExceptionWhenBonusNumberIsDuplicatedWithWinningNumbers() { + List numbers = List.of(1, 2, 3, 4, 5, 6); + LottoNumber bonusNumber = new LottoNumber(3); + Lotto winningNumbers = new Lotto(numbers); + + assertThrows( + IllegalArgumentException.class, + () -> new WinningLotto(winningNumbers, bonusNumber) + ); + } +}