diff --git a/README.md b/README.md index 9594b6c872..873eb4ab02 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,24 @@ # java-lotto -로또 미션 저장소 +## 비즈니스 요구 사항 -# [미션 리드미](https://github.com/talmood/private-mission-README/tree/main/%EB%AF%B8%EC%85%98%203%20-%20%EB%A1%9C%EB%98%90) +- 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다. +- 로또 1장의 가격은 1000원이다. +- 로또 구입 금액은 0보다 커야 한다. +- 로또는 1개이상 구매해야한다. +- 지난 주 당첨 번호는 "1, 2, 3, 4, 5, 6" 같은 형식이어야 한다. +- 보너스 번호는 당첨 번호에 포함되면 안된다. + +## 구현 기능 목록 + +- [x] 구입 금액 사용자 입력을 받는다. +- [x] 수동으로 구매할 로또 수를 입력한다. +- [x] 수동으로 구매할 번호를 입력한다. +- [x] 구입 금액에 맞춰 로또를 구입 갯수를 계산한다. +- [x] 구입한 로또의 갯수를 출력한다. +- [x] 구입한 로또의 목록을 생성한다. +- [x] 구입한 로또의 목록을 출력한다. +- [x] 지난 주 당첨 번호를 입력받는다. +- [x] 보너스 볼을 입력 받는다. +- [x] 당첨 결과를 판단한다. +- [x] 당첨 결과를 출력한다. \ No newline at end of file diff --git a/src/main/java/LottoApplication.java b/src/main/java/LottoApplication.java new file mode 100644 index 0000000000..d272a92208 --- /dev/null +++ b/src/main/java/LottoApplication.java @@ -0,0 +1,8 @@ +import controller.LottoSimulator; + +public class LottoApplication { + public static void main(String[] args) { + new LottoSimulator().run(); + } +} + diff --git a/src/main/java/constant/LottoConstants.java b/src/main/java/constant/LottoConstants.java new file mode 100644 index 0000000000..a9d5d7ad08 --- /dev/null +++ b/src/main/java/constant/LottoConstants.java @@ -0,0 +1,9 @@ +package constant; + +public abstract class LottoConstants { + + public static final int MIN_LOTTO_NUMBER_INCLUSIVE = 1; + public static final int MAX_LOTTO_NUMBER_INCLUSIVE = 45; + public static final int LOTTO_PRICE = 1000; + public static final int LOTTO_NUMBERS_SIZE = 6; +} diff --git a/src/main/java/controller/LottoSimulator.java b/src/main/java/controller/LottoSimulator.java new file mode 100644 index 0000000000..6a04ba2e91 --- /dev/null +++ b/src/main/java/controller/LottoSimulator.java @@ -0,0 +1,46 @@ +package controller; + +import domain.*; +import view.input.ConsoleInputView; +import view.input.InputView; +import view.input.dto.*; +import view.output.ConsoleOutputView; +import view.output.OutputView; +import view.output.dto.LottoWinningStatisticsOutput; +import view.output.dto.LottosOutput; +import view.output.dto.PurchaseCountOutput; + +public class LottoSimulator { + + public void run() { + InputView inputView = new ConsoleInputView(); + OutputView outputView = new ConsoleOutputView(); + + PurchaseInput purchaseInput = inputView.inputPurchaseAmount(); + PurchaseAmount purchaseAmount = purchaseInput.toPurchaseAmount(); + + ManualPurchaseCountInput manualPurchaseCountInput = inputView.inputManualPurchaseCount(); + PurchaseCount manualPurchaseCount = manualPurchaseCountInput.toManualPurchaseCount(); + ManualLottoNumbersInput manualLottoNumbersInput = inputView.inputManualLottoNumbers(manualPurchaseCount.fetchPurchaseCount()); + PurchaseCountCalculator purchaseCountCalculator = new PurchaseCountCalculator(purchaseAmount); + PurchaseCount autoPurchaseCount = purchaseCountCalculator.calculateAutoPurchaseCount(manualPurchaseCount); + outputView.viewPurchaseCount(PurchaseCountOutput.of(manualPurchaseCount, autoPurchaseCount)); + + LottosGenerator lottosGenerator = new LottosGenerator(autoPurchaseCount); + Lottos manualLottos = manualLottoNumbersInput.toLottos(); + Lottos autoLottos = lottosGenerator.generate(); + Lottos lottos = manualLottos.addLottos(autoLottos); + outputView.viewLottos(LottosOutput.from(lottos)); + + WinningNumbersInput winningNumbersInput = inputView.inputWinningNumbers(); + WinningNumbers winningNumbers = winningNumbersInput.toWinningNumbers(); + + BonusNumberInput bonusNumberInput = inputView.inputBonusNumber(); + BonusNumber bonusNumber = bonusNumberInput.toBonusNumber(); + + LottosWinningCalculator lottosWinningCalculator = new LottosWinningCalculator(lottos, winningNumbers, bonusNumber); + LottoWinnings lottoWinnings = lottosWinningCalculator.calculate(); + LottoWinningStatisticsOutput lottoWinningStatisticsOutput = LottoWinningStatisticsOutput.from(lottoWinnings, purchaseAmount); + outputView.viewWinningStatistics(lottoWinningStatisticsOutput); + } +} diff --git a/src/main/java/domain/AutoPurchaseCount.java b/src/main/java/domain/AutoPurchaseCount.java new file mode 100644 index 0000000000..44ab32adbc --- /dev/null +++ b/src/main/java/domain/AutoPurchaseCount.java @@ -0,0 +1,11 @@ +package domain; + +public class AutoPurchaseCount extends PurchaseCount { + private AutoPurchaseCount(int purchaseCount) { + super(purchaseCount); + } + + public static AutoPurchaseCount create(int purchaseCount) { + return new AutoPurchaseCount(purchaseCount); + } +} diff --git a/src/main/java/domain/BonusNumber.java b/src/main/java/domain/BonusNumber.java new file mode 100644 index 0000000000..5f52e5d8a9 --- /dev/null +++ b/src/main/java/domain/BonusNumber.java @@ -0,0 +1,24 @@ +package domain; + +public class BonusNumber { + + private final int bonusNumber; + + private BonusNumber(int bonusNumber) { + this.bonusNumber = bonusNumber; + } + + public static BonusNumber create(int bonusNumber) { + return new BonusNumber(bonusNumber); + } + + public int fetchBonusNumber() { + return this.bonusNumber; + } + + public boolean isSameNumber(int number) { + return this.bonusNumber == number; + } + + +} diff --git a/src/main/java/domain/Lotto.java b/src/main/java/domain/Lotto.java new file mode 100644 index 0000000000..0666f29269 --- /dev/null +++ b/src/main/java/domain/Lotto.java @@ -0,0 +1,86 @@ +package domain; + +import exception.DomainValidationException; + +import java.util.HashSet; +import java.util.List; + +import static constant.LottoConstants.*; +import static exception.code.ErrorCode.*; + +public class Lotto { + + private final List lottoNumbers; + + private Lotto(List lottoNumbers) { + this.validateLottoNumbersSize(lottoNumbers); + this.validateLottoNumbersUnique(lottoNumbers); + this.validateLottoNumbersInRange(lottoNumbers); + this.lottoNumbers = lottoNumbers; + } + + public static Lotto create(List lottoNumbers) { + return new Lotto(lottoNumbers); + } + + private void validateLottoNumbersInRange(List lottoNumbers) { + if (!isAllLottoNumberRange(lottoNumbers)) { + throw new DomainValidationException( + INVALID_LOTTO_NUMBER_RANGE, + String.format("로또 번호의 범위는 %d부터 %d까지 입니다.", + MIN_LOTTO_NUMBER_INCLUSIVE, + MAX_LOTTO_NUMBER_INCLUSIVE + ) + ); + } + } + + private void validateLottoNumbersSize(List lottoNumbers) { + if (lottoNumbers.size() != LOTTO_NUMBERS_SIZE) { + throw new DomainValidationException( + INVALID_LOTTO_NUMBERS_SIZE, + String.format("로또 번호의 갯수는 %d개여야 합니다.", LOTTO_NUMBERS_SIZE) + ); + } + } + + private void validateLottoNumbersUnique(List lottoNumbers) { + if (!isUniqueLottoNumbers(lottoNumbers)) { + throw new DomainValidationException(NOT_UNIQUE_LOTTO_NUMBERS, "로또 번호는 모두 달라야 합니다."); + } + } + + private boolean isAllLottoNumberRange(List lottoNumbers) { + return lottoNumbers.stream() + .allMatch(this::isLottoNumberRange); + } + + private boolean isLottoNumberRange(Integer number) { + return number >= MIN_LOTTO_NUMBER_INCLUSIVE && number <= MAX_LOTTO_NUMBER_INCLUSIVE; + } + + private boolean isUniqueLottoNumbers(List lottoNumbers) { + return new HashSet<>(lottoNumbers).size() == lottoNumbers.size(); + } + + public List fetchLottoNumberList() { + return List.copyOf(this.lottoNumbers); + } + + private long countMatchWinningNumbers(WinningNumbers winningNumbers) { + return winningNumbers.countMatchNumbers(this.lottoNumbers); + } + + private long countMatchBonusNumber(BonusNumber bonusNumber) { + return this.lottoNumbers.stream() + .filter(bonusNumber::isSameNumber) + .count(); + } + + public LottoWinning findLottoWinning(WinningNumbers winningNumbers, BonusNumber bonusNumber) { + long matchedWinningNumberCount = this.countMatchWinningNumbers(winningNumbers); + long matchedBonusNumberCount = this.countMatchBonusNumber(bonusNumber); + + return LottoWinning.findLottoWinning(matchedWinningNumberCount, matchedBonusNumberCount); + } +} diff --git a/src/main/java/domain/LottoWinning.java b/src/main/java/domain/LottoWinning.java new file mode 100644 index 0000000000..503c6bdac3 --- /dev/null +++ b/src/main/java/domain/LottoWinning.java @@ -0,0 +1,59 @@ +package domain; + +import java.util.Arrays; + +public enum LottoWinning { + FIRST_PLACE(2000000000, 6, 0), + SECOND_PLACE(30000000, 5, 1), + THIRD_PLACE(1500000, 5, 0), + FOURTH_PLACE(50000, 4, 0), + FIFTH_PLACE(5000, 3, 0), + ELSE_PLACE(0, 0, 0); + + private final long winningAmount; + + private final long matchWinningNumberCount; + + private final long matchBonusNumberCount; + + LottoWinning(long winningAmount, long matchWinningNumberCount, long matchBonusNumberCount) { + this.winningAmount = winningAmount; + this.matchWinningNumberCount = matchWinningNumberCount; + this.matchBonusNumberCount = matchBonusNumberCount; + } + + public static LottoWinning findLottoWinning(long matchWinningNumberCount, long matchBonusNumberCount) { + return Arrays.stream(LottoWinning.values()) + .filter(lottoWinning -> lottoWinning.isMatchLottoWinning(matchWinningNumberCount, matchBonusNumberCount)) + .findAny() + .orElseGet(() -> ELSE_PLACE); + } + + private boolean isMatchLottoWinning(long matchWinningNumberCount, long matchBonusNumberCount) { + return this.isSameMatchWinningCount(matchWinningNumberCount) && this.isSameMatchBonusNumberCount(matchBonusNumberCount); + } + + private boolean isSameMatchWinningCount(long matchWinningNumberCount) { + return this.matchWinningNumberCount == matchWinningNumberCount; + } + + private boolean isSameMatchBonusNumberCount(long matchBonusNumberCount) { + return this.matchBonusNumberCount == matchBonusNumberCount; + } + + public long fetchWinningAmount() { + return this.winningAmount; + } + + public long fetchMatchWinningNumberCount() { + return this.matchWinningNumberCount; + } + + public boolean isMatchBonusNumber() { + return this.matchBonusNumberCount > 0; + } + + public boolean isEqual(LottoWinning lottoWinning) { + return this == lottoWinning; + } +} diff --git a/src/main/java/domain/LottoWinnings.java b/src/main/java/domain/LottoWinnings.java new file mode 100644 index 0000000000..0c3272818b --- /dev/null +++ b/src/main/java/domain/LottoWinnings.java @@ -0,0 +1,39 @@ +package domain; + +import exception.DomainValidationException; +import exception.code.ErrorCode; +import util.CollectionUtils; + +import java.util.List; + +public class LottoWinnings { + + private final List lottoWinnings; + + private LottoWinnings(List lottoWinnings) { + this.validateNotEmpty(lottoWinnings); + this.lottoWinnings = lottoWinnings; + } + + public static LottoWinnings create(List lottoWinnings) { + return new LottoWinnings(lottoWinnings); + } + + private void validateNotEmpty(List lottoWinnings) { + if (CollectionUtils.isEmpty(lottoWinnings)) { + throw new DomainValidationException(ErrorCode.COLLECTION_MUST_NOT_BE_EMPTY, "당첨 정보는 null이거나 empty이면 안됩니다."); + } + } + + public long sumAllWinningAmount() { + return this.lottoWinnings.stream() + .mapToLong(LottoWinning::fetchWinningAmount) + .sum(); + } + + public long countMatchLottoWinning(LottoWinning lottoWinning) { + return this.lottoWinnings.stream() + .filter(lottoWinning::isEqual) + .count(); + } +} diff --git a/src/main/java/domain/Lottos.java b/src/main/java/domain/Lottos.java new file mode 100644 index 0000000000..2a0e778868 --- /dev/null +++ b/src/main/java/domain/Lottos.java @@ -0,0 +1,35 @@ +package domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Lottos { + + private final List lottos; + + private Lottos(List lottos) { + this.lottos = lottos; + } + + public static Lottos create(List lottos) { + return new Lottos(lottos); + } + + public List toList() { + return List.copyOf(this.lottos); + } + + public List findLottoWinnings(WinningNumbers winningNumbers, BonusNumber bonusNumber) { + return this.lottos.stream() + .map(lotto -> lotto.findLottoWinning(winningNumbers, bonusNumber)) + .collect(Collectors.toList()); + } + + public Lottos addLottos(Lottos lottos) { + List copyLottos = new ArrayList<>(List.copyOf(this.lottos)); + copyLottos.addAll(lottos.toList()); + + return new Lottos(copyLottos); + } +} diff --git a/src/main/java/domain/LottosGenerator.java b/src/main/java/domain/LottosGenerator.java new file mode 100644 index 0000000000..329b2196a4 --- /dev/null +++ b/src/main/java/domain/LottosGenerator.java @@ -0,0 +1,38 @@ +package domain; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static constant.LottoConstants.*; + +public class LottosGenerator { + + private static final List totalLottoNumbers = + IntStream.range(MIN_LOTTO_NUMBER_INCLUSIVE, MAX_LOTTO_NUMBER_INCLUSIVE) + .boxed() + .collect(Collectors.toList()); + + private final PurchaseCount purchaseCount; + + public LottosGenerator(PurchaseCount purchaseCount) { + this.purchaseCount = purchaseCount; + } + + public Lottos generate() { + return Lottos.create( + IntStream.range(0, this.purchaseCount.fetchPurchaseCount()) + .mapToObj(count -> this.generateLotto()) + .collect(Collectors.toList()) + ); + } + + private Lotto generateLotto() { + Collections.shuffle(totalLottoNumbers); + List lottoNumbers = totalLottoNumbers.subList(0, LOTTO_NUMBERS_SIZE); + Collections.sort(lottoNumbers); + + return Lotto.create(List.copyOf(totalLottoNumbers.subList(0, LOTTO_NUMBERS_SIZE))); + } +} diff --git a/src/main/java/domain/LottosWinningCalculator.java b/src/main/java/domain/LottosWinningCalculator.java new file mode 100644 index 0000000000..e719f37a6c --- /dev/null +++ b/src/main/java/domain/LottosWinningCalculator.java @@ -0,0 +1,33 @@ +package domain; + +import exception.DomainValidationException; + +import java.util.List; + +import static exception.code.ErrorCode.WINNING_NUMBERS_CONTAIN_BONUS_NUMBER; + +public class LottosWinningCalculator { + + private final Lottos lottos; + private final WinningNumbers winningNumbers; + private final BonusNumber bonusNumber; + + public LottosWinningCalculator(Lottos lottos, WinningNumbers winningNumbers, BonusNumber bonusNumber) { + this.validateBonusNumberNotContainWinningNumber(winningNumbers, bonusNumber); + this.lottos = lottos; + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + private void validateBonusNumberNotContainWinningNumber(WinningNumbers winningNumbers, BonusNumber bonusNumber) { + if (winningNumbers.isContainNumber(bonusNumber.fetchBonusNumber())) { + throw new DomainValidationException(WINNING_NUMBERS_CONTAIN_BONUS_NUMBER, "보너스 번호가 당첨 번호에 포함되어있으면 안됩니다."); + } + } + + public LottoWinnings calculate() { + List lottoWinnings = this.lottos.findLottoWinnings(this.winningNumbers, this.bonusNumber); + + return LottoWinnings.create(lottoWinnings); + } +} diff --git a/src/main/java/domain/ManualPurchaseCount.java b/src/main/java/domain/ManualPurchaseCount.java new file mode 100644 index 0000000000..ad3eef2063 --- /dev/null +++ b/src/main/java/domain/ManualPurchaseCount.java @@ -0,0 +1,12 @@ +package domain; + +public class ManualPurchaseCount extends PurchaseCount { + + private ManualPurchaseCount(int purchaseCount) { + super(purchaseCount); + } + + public static ManualPurchaseCount create(int purchaseCount) { + return new ManualPurchaseCount(purchaseCount); + } +} diff --git a/src/main/java/domain/PurchaseAmount.java b/src/main/java/domain/PurchaseAmount.java new file mode 100644 index 0000000000..ce0b571e3a --- /dev/null +++ b/src/main/java/domain/PurchaseAmount.java @@ -0,0 +1,33 @@ +package domain; + +import exception.DomainValidationException; + +import static exception.code.ErrorCode.INVALID_PURCHASE_AMOUNT; + +public class PurchaseAmount { + + private final int purchaseAmount; + + private PurchaseAmount(int purchaseAmount) { + this.validateAmount(purchaseAmount); + this.purchaseAmount = purchaseAmount; + } + + public static PurchaseAmount create(int purchaseAmount) { + return new PurchaseAmount(purchaseAmount); + } + + public int fetchPurchaseAmount() { + return this.purchaseAmount; + } + + private void validateAmount(int purchaseAmount) { + if (purchaseAmount <= 0) { + throw new DomainValidationException(INVALID_PURCHASE_AMOUNT, "구입 금액은 0보다 커야합니다."); + } + } + + public boolean isLowerThan(int purchaseAmount) { + return this.purchaseAmount < purchaseAmount; + } +} diff --git a/src/main/java/domain/PurchaseCount.java b/src/main/java/domain/PurchaseCount.java new file mode 100644 index 0000000000..6ef8b03b61 --- /dev/null +++ b/src/main/java/domain/PurchaseCount.java @@ -0,0 +1,25 @@ +package domain; + +import exception.DomainValidationException; + +import static exception.code.ErrorCode.INVALID_PURCHASE_COUNT; + +public abstract class PurchaseCount { + + private final int purchaseCount; + + protected PurchaseCount(int purchaseCount) { + this.validatePurchaseCount(purchaseCount); + this.purchaseCount = purchaseCount; + } + + private void validatePurchaseCount(int purchaseCount) { + if (purchaseCount < 1) { + throw new DomainValidationException(INVALID_PURCHASE_COUNT, "구입 갯수는 1개이상이어야 합니다."); + } + } + + public int fetchPurchaseCount() { + return this.purchaseCount; + } +} diff --git a/src/main/java/domain/PurchaseCountCalculator.java b/src/main/java/domain/PurchaseCountCalculator.java new file mode 100644 index 0000000000..5c03b4c1a4 --- /dev/null +++ b/src/main/java/domain/PurchaseCountCalculator.java @@ -0,0 +1,29 @@ +package domain; + +import exception.DomainValidationException; +import exception.code.ErrorCode; + +import static constant.LottoConstants.LOTTO_PRICE; + +public class PurchaseCountCalculator { + + private final PurchaseAmount purchaseAmount; + + public PurchaseCountCalculator(PurchaseAmount purchaseAmount) { + this.purchaseAmount = purchaseAmount; + } + + public AutoPurchaseCount calculateAutoPurchaseCount(PurchaseCount manualPurchaseCount) { + this.validateEnablePurchaseAmount(manualPurchaseCount); + + return AutoPurchaseCount.create( + this.purchaseAmount.fetchPurchaseAmount() / LOTTO_PRICE - manualPurchaseCount.fetchPurchaseCount() + ); + } + + private void validateEnablePurchaseAmount(PurchaseCount purchaseCount) { + if (this.purchaseAmount.isLowerThan(LOTTO_PRICE * purchaseCount.fetchPurchaseCount())) { + throw new DomainValidationException(ErrorCode.NOT_ENOUGH_PURCHASE_AMOUNT, "구입 금액이 부족합니다."); + } + } +} diff --git a/src/main/java/domain/WinningNumber.java b/src/main/java/domain/WinningNumber.java new file mode 100644 index 0000000000..0f258df5fc --- /dev/null +++ b/src/main/java/domain/WinningNumber.java @@ -0,0 +1,33 @@ +package domain; + +import java.util.Objects; + +public class WinningNumber { + + private final int winningNumber; + + private WinningNumber(int winningNumber) { + this.winningNumber = winningNumber; + } + + public static WinningNumber create(int winningNumber) { + return new WinningNumber(winningNumber); + } + + public boolean isSameNumber(int number) { + return this.winningNumber == number; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WinningNumber that = (WinningNumber) o; + return winningNumber == that.winningNumber; + } + + @Override + public int hashCode() { + return Objects.hash(winningNumber); + } +} diff --git a/src/main/java/domain/WinningNumbers.java b/src/main/java/domain/WinningNumbers.java new file mode 100644 index 0000000000..c9c48317e1 --- /dev/null +++ b/src/main/java/domain/WinningNumbers.java @@ -0,0 +1,55 @@ +package domain; + +import exception.DomainValidationException; +import util.CollectionUtils; + +import java.util.HashSet; +import java.util.List; + +import static exception.code.ErrorCode.COLLECTION_MUST_NOT_BE_EMPTY; +import static exception.code.ErrorCode.NOT_UNIQUE_WINNING_NUMBERS; + +public class WinningNumbers { + + private final List winningNumbers; + + private WinningNumbers(List winningNumbers) { + this.validateNotEmpty(winningNumbers); + this.validateAllNumbersUnique(winningNumbers); + this.winningNumbers = winningNumbers; + } + + public static WinningNumbers create(List winningNumbers) { + return new WinningNumbers(winningNumbers); + } + + private void validateNotEmpty(List winningNumbers) { + if (CollectionUtils.isEmpty(winningNumbers)) { + throw new DomainValidationException(COLLECTION_MUST_NOT_BE_EMPTY, "당첨 번호는 null이거나 empty이면 안됩니다."); + } + } + + private void validateAllNumbersUnique(List winningNumbers) { + if (!isUniqueWinningNumbers(winningNumbers)) { + throw new DomainValidationException(NOT_UNIQUE_WINNING_NUMBERS, "당첨 번호는 모두 다른 값이여야 합니다."); + } + } + + private boolean isUniqueWinningNumbers(List winningNumbers) { + return new HashSet<>(winningNumbers).size() == winningNumbers.size(); + } + + public long countMatchNumbers(List numbers) { + return numbers.stream() + .filter(this::isContainNumber) + .count(); + } + + public boolean isContainNumber(int number) { + long count = this.winningNumbers.stream() + .filter(winningNumber -> winningNumber.isSameNumber(number)) + .count(); + + return count > 0; + } +} diff --git a/src/main/java/exception/DomainValidationException.java b/src/main/java/exception/DomainValidationException.java new file mode 100644 index 0000000000..8677a15745 --- /dev/null +++ b/src/main/java/exception/DomainValidationException.java @@ -0,0 +1,17 @@ +package exception; + +import exception.code.ErrorCode; + +public class DomainValidationException extends IllegalArgumentException { + + private final ErrorCode errorCode; + + public DomainValidationException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public ErrorCode fetchErrorCode() { + return this.errorCode; + } +} diff --git a/src/main/java/exception/InvalidInputException.java b/src/main/java/exception/InvalidInputException.java new file mode 100644 index 0000000000..8056a08d87 --- /dev/null +++ b/src/main/java/exception/InvalidInputException.java @@ -0,0 +1,13 @@ +package exception; + +import exception.code.ErrorCode; + +public class InvalidInputException extends IllegalArgumentException { + + private final ErrorCode errorCode; + + public InvalidInputException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/exception/InvalidOutputException.java b/src/main/java/exception/InvalidOutputException.java new file mode 100644 index 0000000000..d9d9b76f1a --- /dev/null +++ b/src/main/java/exception/InvalidOutputException.java @@ -0,0 +1,13 @@ +package exception; + +import exception.code.ErrorCode; + +public class InvalidOutputException extends IllegalArgumentException { + + private final ErrorCode errorCode; + + public InvalidOutputException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/exception/code/ErrorCode.java b/src/main/java/exception/code/ErrorCode.java new file mode 100644 index 0000000000..0dd82ab3fe --- /dev/null +++ b/src/main/java/exception/code/ErrorCode.java @@ -0,0 +1,19 @@ +package exception.code; + +public enum ErrorCode { + + INVALID_NUMBER_INPUT, + INVALID_RANGE_NUMBERS_INPUT, + INVALID_NUMBERS_INPUT, + INVALID_LOTTO_NUMBERS_SIZE, + INVALID_PURCHASE_COUNT, + INVALID_LOTTO_NUMBER_RANGE, + INVALID_BONUS_NUMBER_RANGE, + INVALID_PURCHASE_AMOUNT, + COLLECTION_MUST_NOT_BE_EMPTY, + NOT_UNIQUE_WINNING_NUMBERS, + NOT_UNIQUE_LOTTO_NUMBERS, + NOT_ENOUGH_PURCHASE_AMOUNT, + WINNING_NUMBERS_CONTAIN_BONUS_NUMBER, + NOT_MATCHED_LOTTO_WINNING_OUTPUT +} diff --git a/src/main/java/util/CollectionUtils.java b/src/main/java/util/CollectionUtils.java new file mode 100644 index 0000000000..48e5691e23 --- /dev/null +++ b/src/main/java/util/CollectionUtils.java @@ -0,0 +1,11 @@ +package util; + +import java.util.Collection; +import java.util.Objects; + +public abstract class CollectionUtils { + + public static boolean isEmpty(Collection collection) { + return Objects.isNull(collection) || collection.isEmpty(); + } +} diff --git a/src/main/java/util/Console.java b/src/main/java/util/Console.java new file mode 100644 index 0000000000..27dd27ac1b --- /dev/null +++ b/src/main/java/util/Console.java @@ -0,0 +1,18 @@ +package util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public abstract class Console { + + private static final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); + + public static String readLine() { + try { + return bufferedReader.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/util/PatternMatchUtils.java b/src/main/java/util/PatternMatchUtils.java new file mode 100644 index 0000000000..c912fae322 --- /dev/null +++ b/src/main/java/util/PatternMatchUtils.java @@ -0,0 +1,10 @@ +package util; + +import java.util.Objects; + +public abstract class PatternMatchUtils { + + public static boolean matches(String regex, String str) { + return Objects.nonNull(str) && str.matches(regex); + } +} diff --git a/src/main/java/util/StringUtils.java b/src/main/java/util/StringUtils.java new file mode 100644 index 0000000000..4a6003034a --- /dev/null +++ b/src/main/java/util/StringUtils.java @@ -0,0 +1,21 @@ +package util; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public abstract class StringUtils { + + private static final String NUMERIC_PATTERN = "[+-]?\\d*(\\.\\d+)?"; + + public static boolean isNumeric(String str) { + return Objects.nonNull(str) && str.matches(NUMERIC_PATTERN); + } + + public static List splitStrToNumbers(String delimiter, String input) { + return Arrays.stream(input.split(delimiter)) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/view/input/ConsoleInputView.java b/src/main/java/view/input/ConsoleInputView.java new file mode 100644 index 0000000000..f3da6bb4e9 --- /dev/null +++ b/src/main/java/view/input/ConsoleInputView.java @@ -0,0 +1,95 @@ +package view.input; + +import exception.InvalidInputException; +import util.Console; +import util.PatternMatchUtils; +import util.StringUtils; +import view.input.dto.*; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static exception.code.ErrorCode.INVALID_NUMBERS_INPUT; +import static exception.code.ErrorCode.INVALID_NUMBER_INPUT; + +public class ConsoleInputView implements InputView { + + private static final String PURCHASE_AMOUNT_NAVIGATION = "구입금액을 입력해 주세요."; + private static final String MANUAL_PURCHASE_COUNT_NAVIGATION = "수동으로 구매할 로또 수를 입력해 주세요."; + private static final String MANUAL_LOTTO_NUMBERS_NAVIGATION = "수동으로 구매할 번호를 입력해 주세요."; + private static final String WINNING_NUMBERS_NAVIGATION = "지난 주 당첨 번호를 입력해 주세요."; + private static final String BONUS_NUMBER_NAVIGATION = "보너스 볼을 입력해 주세요."; + private static final String NUMBERS_INPUT_REGEX = "^\\d{1,2}(,\\s\\d{1,2}){5}$"; + + @Override + public PurchaseInput inputPurchaseAmount() { + System.out.println(PURCHASE_AMOUNT_NAVIGATION); + String input = Console.readLine(); + this.validateNumber(input); + System.out.println(); + + return new PurchaseInput(Integer.parseInt(input)); + } + + @Override + public ManualPurchaseCountInput inputManualPurchaseCount() { + System.out.println(MANUAL_PURCHASE_COUNT_NAVIGATION); + String input = Console.readLine(); + this.validateNumber(input); + System.out.println(); + + return new ManualPurchaseCountInput(Integer.parseInt(input)); + } + + @Override + public ManualLottoNumbersInput inputManualLottoNumbers(int manualPurchaseCount) { + System.out.println(MANUAL_LOTTO_NUMBERS_NAVIGATION); + + ManualLottoNumbersInput manualLottoNumbersInput = new ManualLottoNumbersInput( + IntStream.range(0, manualPurchaseCount) + .mapToObj(count -> { + String input = Console.readLine(); + this.validateNumbers(input); + + return input; + }) + .map(input -> StringUtils.splitStrToNumbers(", ", input)) + .map(ManualLottoNumberInput::new) + .collect(Collectors.toList()) + ); + System.out.println(); + + return manualLottoNumbersInput; + } + + @Override + public WinningNumbersInput inputWinningNumbers() { + System.out.println(WINNING_NUMBERS_NAVIGATION); + String input = Console.readLine(); + this.validateNumbers(input); + + return new WinningNumbersInput(StringUtils.splitStrToNumbers(", ", input)); + } + + @Override + public BonusNumberInput inputBonusNumber() { + System.out.println(BONUS_NUMBER_NAVIGATION); + String input = Console.readLine(); + System.out.println(); + this.validateNumber(input); + + return new BonusNumberInput(Integer.parseInt(input)); + } + + private void validateNumber(String input) { + if (!StringUtils.isNumeric(input)) { + throw new InvalidInputException(INVALID_NUMBER_INPUT, "숫자인 입력값만 허용됩니다."); + } + } + + private void validateNumbers(String input) { + if (!PatternMatchUtils.matches(NUMBERS_INPUT_REGEX, input)) { + throw new InvalidInputException(INVALID_NUMBERS_INPUT, "번호 입력 형식이 올바르지 않습니다."); + } + } +} diff --git a/src/main/java/view/input/InputView.java b/src/main/java/view/input/InputView.java new file mode 100644 index 0000000000..109fc07c62 --- /dev/null +++ b/src/main/java/view/input/InputView.java @@ -0,0 +1,16 @@ +package view.input; + +import view.input.dto.*; + +public interface InputView { + + PurchaseInput inputPurchaseAmount(); + + ManualPurchaseCountInput inputManualPurchaseCount(); + + ManualLottoNumbersInput inputManualLottoNumbers(int manualPurchaseCount); + + WinningNumbersInput inputWinningNumbers(); + + BonusNumberInput inputBonusNumber(); +} diff --git a/src/main/java/view/input/dto/BonusNumberInput.java b/src/main/java/view/input/dto/BonusNumberInput.java new file mode 100644 index 0000000000..26eb3baa4b --- /dev/null +++ b/src/main/java/view/input/dto/BonusNumberInput.java @@ -0,0 +1,38 @@ +package view.input.dto; + +import domain.BonusNumber; +import exception.InvalidInputException; + +import static constant.LottoConstants.MAX_LOTTO_NUMBER_INCLUSIVE; +import static constant.LottoConstants.MIN_LOTTO_NUMBER_INCLUSIVE; +import static exception.code.ErrorCode.INVALID_BONUS_NUMBER_RANGE; + +public class BonusNumberInput { + + private final int bonusNumber; + + public BonusNumberInput(int bonusNumber) { + this.validateBonusNumberInRange(bonusNumber); + this.bonusNumber = bonusNumber; + } + + public BonusNumber toBonusNumber() { + return BonusNumber.create(this.bonusNumber); + } + + private void validateBonusNumberInRange(int bonusNumber) { + if (!isNumberInRange(bonusNumber)) { + throw new InvalidInputException( + INVALID_BONUS_NUMBER_RANGE, + String.format("보너스 번호의 범위는 %d이상 %d이하 여야 합니다.", + MIN_LOTTO_NUMBER_INCLUSIVE, + MAX_LOTTO_NUMBER_INCLUSIVE + ) + ); + } + } + + private boolean isNumberInRange(int number) { + return number >= MIN_LOTTO_NUMBER_INCLUSIVE && number <= MAX_LOTTO_NUMBER_INCLUSIVE; + } +} diff --git a/src/main/java/view/input/dto/ManualLottoNumberInput.java b/src/main/java/view/input/dto/ManualLottoNumberInput.java new file mode 100644 index 0000000000..3d22d18fb8 --- /dev/null +++ b/src/main/java/view/input/dto/ManualLottoNumberInput.java @@ -0,0 +1,19 @@ +package view.input.dto; + +import domain.Lotto; + +import java.util.List; + +public class ManualLottoNumberInput { + + private final List lottoNumbers; + + public ManualLottoNumberInput(List lottoNumbers) { + this.lottoNumbers = lottoNumbers; + } + + public Lotto toLotto() { + return Lotto.create(lottoNumbers); + } +} + diff --git a/src/main/java/view/input/dto/ManualLottoNumbersInput.java b/src/main/java/view/input/dto/ManualLottoNumbersInput.java new file mode 100644 index 0000000000..f5f7262d77 --- /dev/null +++ b/src/main/java/view/input/dto/ManualLottoNumbersInput.java @@ -0,0 +1,24 @@ +package view.input.dto; + +import domain.Lottos; + +import java.util.List; +import java.util.stream.Collectors; + +public class ManualLottoNumbersInput { + + private final List lottos; + + public ManualLottoNumbersInput(List lottos) { + this.lottos = lottos; + } + + public Lottos toLottos() { + return Lottos.create( + this.lottos.stream() + .map(ManualLottoNumberInput::toLotto) + .collect(Collectors.toList()) + ); + } + +} diff --git a/src/main/java/view/input/dto/ManualPurchaseCountInput.java b/src/main/java/view/input/dto/ManualPurchaseCountInput.java new file mode 100644 index 0000000000..d8eb7bc7fb --- /dev/null +++ b/src/main/java/view/input/dto/ManualPurchaseCountInput.java @@ -0,0 +1,16 @@ +package view.input.dto; + +import domain.ManualPurchaseCount; + +public class ManualPurchaseCountInput { + + private final int manualPurchaseCount; + + public ManualPurchaseCountInput(int manualPurchaseCount) { + this.manualPurchaseCount = manualPurchaseCount; + } + + public ManualPurchaseCount toManualPurchaseCount() { + return ManualPurchaseCount.create(this.manualPurchaseCount); + } +} diff --git a/src/main/java/view/input/dto/PurchaseInput.java b/src/main/java/view/input/dto/PurchaseInput.java new file mode 100644 index 0000000000..ab0066d47e --- /dev/null +++ b/src/main/java/view/input/dto/PurchaseInput.java @@ -0,0 +1,26 @@ +package view.input.dto; + +import domain.PurchaseAmount; +import exception.DomainValidationException; + +import static exception.code.ErrorCode.INVALID_PURCHASE_AMOUNT; + +public class PurchaseInput { + + private final int purchaseAmount; + + public PurchaseInput(int purchaseAmount) { + this.validateAmount(purchaseAmount); + this.purchaseAmount = purchaseAmount; + } + + public PurchaseAmount toPurchaseAmount() { + return PurchaseAmount.create(purchaseAmount); + } + + private void validateAmount(int purchaseAmount) { + if (purchaseAmount <= 0) { + throw new DomainValidationException(INVALID_PURCHASE_AMOUNT, "구입 금액은 0보다 커야합니다."); + } + } +} diff --git a/src/main/java/view/input/dto/WinningNumbersInput.java b/src/main/java/view/input/dto/WinningNumbersInput.java new file mode 100644 index 0000000000..db6be60f67 --- /dev/null +++ b/src/main/java/view/input/dto/WinningNumbersInput.java @@ -0,0 +1,61 @@ +package view.input.dto; + +import domain.WinningNumber; +import domain.WinningNumbers; +import exception.InvalidInputException; +import util.CollectionUtils; + +import java.util.List; +import java.util.stream.Collectors; + +import static constant.LottoConstants.MAX_LOTTO_NUMBER_INCLUSIVE; +import static constant.LottoConstants.MIN_LOTTO_NUMBER_INCLUSIVE; +import static exception.code.ErrorCode.COLLECTION_MUST_NOT_BE_EMPTY; +import static exception.code.ErrorCode.INVALID_RANGE_NUMBERS_INPUT; + +public class WinningNumbersInput { + + private final List winningNumbers; + + public WinningNumbersInput(List winningNumbers) { + this.validateWinningNumbersNotEmpty(winningNumbers); + this.validateWinningNumbersInRange(winningNumbers); + this.winningNumbers = winningNumbers; + } + + public WinningNumbers toWinningNumbers() { + return WinningNumbers.create( + this.winningNumbers.stream() + .map(WinningNumber::create) + .collect(Collectors.toList()) + ); + } + + private void validateWinningNumbersInRange(List winningNumbers) { + if (!this.isWinningNumbersInRange(winningNumbers)) { + throw new InvalidInputException( + INVALID_RANGE_NUMBERS_INPUT, + String.format("당첨 번호의 범위는 %d이상 %d이하 이어야 합니다.", + MIN_LOTTO_NUMBER_INCLUSIVE, + MAX_LOTTO_NUMBER_INCLUSIVE + ) + ); + } + } + + private boolean isWinningNumbersInRange(List winningNumbers) { + return !CollectionUtils.isEmpty(winningNumbers) && + winningNumbers.stream() + .allMatch(this::isNumberInRange); + } + + private boolean isNumberInRange(int number) { + return number >= MIN_LOTTO_NUMBER_INCLUSIVE && number <= MAX_LOTTO_NUMBER_INCLUSIVE; + } + + private void validateWinningNumbersNotEmpty(List winningNumbers) { + if (CollectionUtils.isEmpty(winningNumbers)) { + throw new InvalidInputException(COLLECTION_MUST_NOT_BE_EMPTY, "당첨 번호는 null이거나 empty이면 안됩니다."); + } + } +} diff --git a/src/main/java/view/output/ConsoleOutputView.java b/src/main/java/view/output/ConsoleOutputView.java new file mode 100644 index 0000000000..2bf230cf67 --- /dev/null +++ b/src/main/java/view/output/ConsoleOutputView.java @@ -0,0 +1,81 @@ +package view.output; + +import view.output.dto.LottoWinningStatisticsOutput; +import view.output.dto.LottoWinningsOutput; +import view.output.dto.LottosOutput; +import view.output.dto.PurchaseCountOutput; + +public class ConsoleOutputView implements OutputView { + + private static final String PURCHASE_COUNT_NAVIGATION = "수동으로 %d장, 자동으로 %d개를 구매했습니다."; + private static final String WINNING_STATISTICS_NAVIGATION = "당첨 통계"; + private static final String WINNING_SEPARATOR = "---------"; + private static final String MATCH_WINNING_NUMBER_NAVIGATION = "%d개 일치"; + private static final String MATCH_BONUS_NUMBER_NAVIGATION = ", 보너스 볼 일치"; + private static final String WINNING_AMOUNT_NAVIGATION = " (%d원)"; + private static final String MATCH_COUNT_NAVIGATION = " - %d개"; + private static final String WINNING_REVENUE_RATE_NAVIGATION = "총 수익률은 %.2f입니다."; + private static final String LOTTO_LOSS_NAVIGATION = "(기준이 1이기 때문에 결과적으로 손해라는 의미임)"; + private static final String LOTTO_GAIN_NAVIGATION = "(기준이 1이기 때문에 결과적으로 이득이라는 의미임)"; + private static final String LOTTO_BREAK_EVEN_NAVIGATION = "(기준이 1이기 때문에 결과적으로 본전이라는 의미임)"; + + @Override + public void viewPurchaseCount(PurchaseCountOutput purchaseCountOutput) { + System.out.printf( + PURCHASE_COUNT_NAVIGATION + "%n", + purchaseCountOutput.fetchManualPurchaseCount(), + purchaseCountOutput.fetchAutoPurchaseCount() + ); + } + + @Override + public void viewLottos(LottosOutput lottosOutput) { + System.out.println(lottosOutput.fetchLottosNumbersStr() + "\n"); + } + + @Override + public void viewWinningStatistics(LottoWinningStatisticsOutput lottoWinningStatisticsOutput) { + System.out.println(WINNING_STATISTICS_NAVIGATION); + System.out.println(WINNING_SEPARATOR); + LottoWinningsOutput lottoWinningsOutput = lottoWinningStatisticsOutput.fetchWinningsOutput(); + double revenueRate = lottoWinningStatisticsOutput.fetchRevenueRate(); + lottoWinningsOutput.toList().forEach( + lottoWinningOutput -> + System.out.printf( + makeWinningStatisticsNavigation(lottoWinningOutput.isMatchBonusNumber()), + lottoWinningOutput.fetchMatchWinningNumberCount(), + lottoWinningOutput.fetchWinningAmount(), + lottoWinningOutput.fetchMatchCount() + ) + ); + System.out.printf(makeRevenueNavigation(revenueRate), Math.floor(revenueRate * 100) / 100.0); + } + + private String makeWinningStatisticsNavigation(boolean matchBonusNumber) { + String bonusNavigation = ""; + + if (matchBonusNumber) { + bonusNavigation = MATCH_BONUS_NUMBER_NAVIGATION; + } + + return MATCH_WINNING_NUMBER_NAVIGATION + bonusNavigation + WINNING_AMOUNT_NAVIGATION + MATCH_COUNT_NAVIGATION + "\n"; + } + + private String makeRevenueNavigation(double revenueRate) { + String gainNavigation = ""; + + if (revenueRate > 1.0) { + gainNavigation = LOTTO_GAIN_NAVIGATION; + } + + if (revenueRate == 1.0) { + gainNavigation = LOTTO_BREAK_EVEN_NAVIGATION; + } + + if (revenueRate < 1.0) { + gainNavigation = LOTTO_LOSS_NAVIGATION; + } + + return WINNING_REVENUE_RATE_NAVIGATION + gainNavigation; + } +} diff --git a/src/main/java/view/output/OutputView.java b/src/main/java/view/output/OutputView.java new file mode 100644 index 0000000000..9608278c61 --- /dev/null +++ b/src/main/java/view/output/OutputView.java @@ -0,0 +1,14 @@ +package view.output; + +import view.output.dto.LottoWinningStatisticsOutput; +import view.output.dto.LottosOutput; +import view.output.dto.PurchaseCountOutput; + +public interface OutputView { + + void viewPurchaseCount(PurchaseCountOutput purchaseCountOutput); + + void viewLottos(LottosOutput lottosOutput); + + void viewWinningStatistics(LottoWinningStatisticsOutput lottoWinningStatisticsOutput); +} diff --git a/src/main/java/view/output/dto/LottoOutput.java b/src/main/java/view/output/dto/LottoOutput.java new file mode 100644 index 0000000000..938a6cdb25 --- /dev/null +++ b/src/main/java/view/output/dto/LottoOutput.java @@ -0,0 +1,27 @@ +package view.output.dto; + +import domain.Lotto; + +import java.util.List; +import java.util.stream.Collectors; + +public class LottoOutput { + + private final List lottoNumbers; + + public LottoOutput(List lottoNumbers) { + this.lottoNumbers = lottoNumbers; + } + + public String fetchLottoNumbersToStr() { + String lottoNumbersStr = this.lottoNumbers.stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + + return "[" + lottoNumbersStr + "]"; + } + + public static LottoOutput from(Lotto lotto) { + return new LottoOutput(lotto.fetchLottoNumberList()); + } +} diff --git a/src/main/java/view/output/dto/LottoRevenueRateCalculator.java b/src/main/java/view/output/dto/LottoRevenueRateCalculator.java new file mode 100644 index 0000000000..6878b01798 --- /dev/null +++ b/src/main/java/view/output/dto/LottoRevenueRateCalculator.java @@ -0,0 +1,21 @@ +package view.output.dto; + +import domain.LottoWinnings; +import domain.PurchaseAmount; + +public class LottoRevenueRateCalculator { + + private final LottoWinnings lottoWinnings; + private final PurchaseAmount purchaseAmount; + + public LottoRevenueRateCalculator(LottoWinnings lottoWinnings, PurchaseAmount purchaseAmount) { + this.lottoWinnings = lottoWinnings; + this.purchaseAmount = purchaseAmount; + } + + public double calculate() { + long totalWinningAmount = this.lottoWinnings.sumAllWinningAmount(); + + return (double) totalWinningAmount / purchaseAmount.fetchPurchaseAmount(); + } +} diff --git a/src/main/java/view/output/dto/LottoWinningOutput.java b/src/main/java/view/output/dto/LottoWinningOutput.java new file mode 100644 index 0000000000..a5429649a3 --- /dev/null +++ b/src/main/java/view/output/dto/LottoWinningOutput.java @@ -0,0 +1,46 @@ +package view.output.dto; + +import domain.LottoWinning; + +public class LottoWinningOutput { + + private final long matchWinningNumberCount; + + private final long winningAmount; + + private final boolean isMatchBonusNumber; + + private final long matchCount; + + public LottoWinningOutput(long matchWinningNumberCount, long winningAmount, boolean isMatchBonusNumber, long matchCount) { + this.matchWinningNumberCount = matchWinningNumberCount; + this.winningAmount = winningAmount; + this.isMatchBonusNumber = isMatchBonusNumber; + this.matchCount = matchCount; + } + + public static LottoWinningOutput of(LottoWinning lottoWinning, long matchCount) { + return new LottoWinningOutput( + lottoWinning.fetchMatchWinningNumberCount(), + lottoWinning.fetchWinningAmount(), + lottoWinning.isMatchBonusNumber(), + matchCount + ); + } + + public long fetchMatchWinningNumberCount() { + return matchWinningNumberCount; + } + + public long fetchWinningAmount() { + return winningAmount; + } + + public boolean isMatchBonusNumber() { + return isMatchBonusNumber; + } + + public long fetchMatchCount() { + return matchCount; + } +} diff --git a/src/main/java/view/output/dto/LottoWinningStatisticsOutput.java b/src/main/java/view/output/dto/LottoWinningStatisticsOutput.java new file mode 100644 index 0000000000..11b0606e5b --- /dev/null +++ b/src/main/java/view/output/dto/LottoWinningStatisticsOutput.java @@ -0,0 +1,30 @@ +package view.output.dto; + +import domain.LottoWinnings; +import domain.PurchaseAmount; + +public class LottoWinningStatisticsOutput { + + private final LottoWinningsOutput lottoWinningOutputs; + private final double revenueRate; + + public LottoWinningStatisticsOutput(LottoWinningsOutput lottoWinningOutputs, double revenueRate) { + this.lottoWinningOutputs = lottoWinningOutputs; + this.revenueRate = revenueRate; + } + + public static LottoWinningStatisticsOutput from(LottoWinnings lottoWinnings, PurchaseAmount purchaseAmount) { + return new LottoWinningStatisticsOutput( + LottoWinningsOutput.from(lottoWinnings), + new LottoRevenueRateCalculator(lottoWinnings, purchaseAmount).calculate() + ); + } + + public LottoWinningsOutput fetchWinningsOutput() { + return this.lottoWinningOutputs; + } + + public double fetchRevenueRate() { + return this.revenueRate; + } +} diff --git a/src/main/java/view/output/dto/LottoWinningsOutput.java b/src/main/java/view/output/dto/LottoWinningsOutput.java new file mode 100644 index 0000000000..a878d6d153 --- /dev/null +++ b/src/main/java/view/output/dto/LottoWinningsOutput.java @@ -0,0 +1,34 @@ +package view.output.dto; + +import domain.LottoWinning; +import domain.LottoWinnings; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class LottoWinningsOutput { + + private final List lottoWinningOutputs; + + public LottoWinningsOutput(List lottoWinningOutputs) { + this.lottoWinningOutputs = lottoWinningOutputs; + } + + public static LottoWinningsOutput from(LottoWinnings lottoWinnings) { + return new LottoWinningsOutput( + Arrays.stream(LottoWinning.values()) + .filter(lottoWinning -> !lottoWinning.isEqual(LottoWinning.ELSE_PLACE)) + .map(lottoWinning -> + LottoWinningOutput.of(lottoWinning, lottoWinnings.countMatchLottoWinning(lottoWinning)) + ) + .sorted(Comparator.comparing(LottoWinningOutput::fetchWinningAmount)) + .collect(Collectors.toList()) + ); + } + + public List toList() { + return List.copyOf(this.lottoWinningOutputs); + } +} diff --git a/src/main/java/view/output/dto/LottosOutput.java b/src/main/java/view/output/dto/LottosOutput.java new file mode 100644 index 0000000000..51f5ed7f73 --- /dev/null +++ b/src/main/java/view/output/dto/LottosOutput.java @@ -0,0 +1,29 @@ +package view.output.dto; + +import domain.Lottos; + +import java.util.List; +import java.util.stream.Collectors; + +public class LottosOutput { + + private final List lottoOutputs; + + public LottosOutput(List lottoOutputs) { + this.lottoOutputs = lottoOutputs; + } + + public static LottosOutput from(Lottos lottos) { + return new LottosOutput( + lottos.toList().stream() + .map(LottoOutput::from) + .collect(Collectors.toList()) + ); + } + + public String fetchLottosNumbersStr() { + return this.lottoOutputs.stream() + .map(LottoOutput::fetchLottoNumbersToStr) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/view/output/dto/PurchaseCountOutput.java b/src/main/java/view/output/dto/PurchaseCountOutput.java new file mode 100644 index 0000000000..53c3952f91 --- /dev/null +++ b/src/main/java/view/output/dto/PurchaseCountOutput.java @@ -0,0 +1,26 @@ +package view.output.dto; + +import domain.PurchaseCount; + +public class PurchaseCountOutput { + + private final int manualPurchaseCount; + private final int autoPurchaseCount; + + private PurchaseCountOutput(int manualPurchaseCount, int autoPurchaseCount) { + this.manualPurchaseCount = manualPurchaseCount; + this.autoPurchaseCount = autoPurchaseCount; + } + + public static PurchaseCountOutput of(PurchaseCount manualPurchaseCount, PurchaseCount autoPurchaseCount) { + return new PurchaseCountOutput(manualPurchaseCount.fetchPurchaseCount(), autoPurchaseCount.fetchPurchaseCount()); + } + + public int fetchManualPurchaseCount() { + return this.manualPurchaseCount; + } + + public int fetchAutoPurchaseCount() { + return this.autoPurchaseCount; + } +} diff --git a/src/test/java/domain/LottoTest.java b/src/test/java/domain/LottoTest.java new file mode 100644 index 0000000000..2c7aeeadd8 --- /dev/null +++ b/src/test/java/domain/LottoTest.java @@ -0,0 +1,91 @@ +package domain; + +import exception.DomainValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static exception.code.ErrorCode.*; + +class LottoTest { + + + static Stream invalidLottoRange() { + return Stream.of( + Arguments.arguments(List.of(0, 1, 2, 3, 4, 5)), + Arguments.arguments(List.of(1, 2, 3, 4, 5, 46)), + Arguments.arguments(List.of(-1, -2, -3, -4, -5, -6)) + ); + } + + static Stream invalidLottoNumbersSize() { + return Stream.of( + Arguments.arguments(List.of(1, 2, 3, 4)), + Arguments.arguments(List.of(1, 2, 3, 4, 5)), + Arguments.arguments(List.of(1, 2, 3, 4, 5, 6, 7)), + Arguments.arguments(Collections.emptyList()) + ); + } + + static Stream lottoNumbersAndWinning() { + return Stream.of( + Arguments.arguments(List.of(1, 2, 3, 4, 5, 6), 15, LottoWinning.FIRST_PLACE), + Arguments.arguments(List.of(1, 2, 3, 4, 5, 7), 6, LottoWinning.SECOND_PLACE), + Arguments.arguments(List.of(1, 2, 3, 4, 5, 8), 15, LottoWinning.THIRD_PLACE), + Arguments.arguments(List.of(1, 2, 3, 4, 8, 9), 15, LottoWinning.FOURTH_PLACE), + Arguments.arguments(List.of(1, 2, 3, 8, 9, 10), 15, LottoWinning.FIFTH_PLACE) + ); + } + + @ParameterizedTest(name = "로또 번호 : {0}") + @MethodSource("invalidLottoRange") + @DisplayName("로또 번호는 범위 안에 있어야 한다.") + void lottoInRange(List lottoNumbers) { + DomainValidationException domainValidationException = + Assertions.assertThrows(DomainValidationException.class, () -> Lotto.create(lottoNumbers)); + + Assertions.assertSame(domainValidationException.fetchErrorCode(), INVALID_LOTTO_NUMBER_RANGE); + } + + @ParameterizedTest(name = "로또 번호 : {0}") + @MethodSource("invalidLottoNumbersSize") + @DisplayName("로또의 갯수는 정해져있어야 한다.") + void lottoNumbersFixed(List lottoNumbers) { + DomainValidationException domainValidationException = + Assertions.assertThrows(DomainValidationException.class, () -> Lotto.create(lottoNumbers)); + + Assertions.assertSame(domainValidationException.fetchErrorCode(), INVALID_LOTTO_NUMBERS_SIZE); + } + + @Test + @DisplayName("로또는 모두 다른 값이여야 한다.") + void lottoNumbersAllUnique() { + List lottoNumbers = List.of(1, 1, 2, 3, 4, 5); + DomainValidationException domainValidationException = + Assertions.assertThrows(DomainValidationException.class, () -> Lotto.create(lottoNumbers)); + + Assertions.assertSame(domainValidationException.fetchErrorCode(), NOT_UNIQUE_LOTTO_NUMBERS); + } + + @ParameterizedTest(name = "당첨번호 : {0}, 보너스 번호 : {1}, 당첨 : {2}") + @MethodSource("lottoNumbersAndWinning") + @DisplayName("로또 번호에 따라 당첨 정보를 찾는다.") + void findLottoWinning(List numbers, int bonus, LottoWinning lottoWinning) { + WinningNumbers winningNumbers = WinningNumbers.create(numbers.stream() + .map(WinningNumber::create) + .collect(Collectors.toList()) + ); + BonusNumber bonusNumber = BonusNumber.create(bonus); + Lotto lotto = Lotto.create(List.of(1, 2, 3, 4, 5, 6)); + + Assertions.assertSame(lotto.findLottoWinning(winningNumbers, bonusNumber), lottoWinning); + } +} \ No newline at end of file diff --git a/src/test/java/domain/PurchaseCountCalculatorTest.java b/src/test/java/domain/PurchaseCountCalculatorTest.java new file mode 100644 index 0000000000..cf7df0dc21 --- /dev/null +++ b/src/test/java/domain/PurchaseCountCalculatorTest.java @@ -0,0 +1,45 @@ +package domain; + +import constant.LottoConstants; +import exception.DomainValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static exception.code.ErrorCode.NOT_ENOUGH_PURCHASE_AMOUNT; + +class PurchaseCountCalculatorTest { + + @Test + @DisplayName("자동 구입 갯수는 수동 구입 갯수를 제외한 나머지를 구매한다.") + void autoPurchaseCountIsAllMinusManualPurchaseCount() { + int purchaseAmountValue = 14000; + int manualCountValue = 10; + + PurchaseAmount purchaseAmount = PurchaseAmount.create(purchaseAmountValue); + PurchaseCount manualPurchaseCount = ManualPurchaseCount.create(manualCountValue); + PurchaseCountCalculator purchaseCountCalculator = new PurchaseCountCalculator(purchaseAmount); + AutoPurchaseCount autoPurchaseCount = purchaseCountCalculator.calculateAutoPurchaseCount(manualPurchaseCount); + + Assertions.assertEquals( + autoPurchaseCount.fetchPurchaseCount(), + (purchaseAmountValue - (LottoConstants.LOTTO_PRICE * manualCountValue)) / LottoConstants.LOTTO_PRICE + ); + } + + @Test + @DisplayName("수동 구매 금액이 구매 금액보다 큰 경우 예외를 발생시킨다.") + void manualPurchaseGreaterThanPurchaseAmount() { + PurchaseAmount purchaseAmount = PurchaseAmount.create(10); + PurchaseCount manualPurchaseCount = ManualPurchaseCount.create(20); + PurchaseCountCalculator purchaseCountCalculator = new PurchaseCountCalculator(purchaseAmount); + + DomainValidationException domainValidationException = Assertions.assertThrows( + DomainValidationException.class, + () -> purchaseCountCalculator.calculateAutoPurchaseCount(manualPurchaseCount) + ); + Assertions.assertSame(domainValidationException.fetchErrorCode(), NOT_ENOUGH_PURCHASE_AMOUNT); + } + + +} \ No newline at end of file