diff --git a/docs/README.md b/docs/README.md index e69de29bb2d..2756477ea12 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,67 @@ +# 구현할 기능 목록 +- [X] 구입 금액을 입력받고, 구입 금액에 해당하는 만큼 로또를 발행한다. + - [X] 로또 1장의 가격은 1,000원이다. 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다. + + +- [X] 발행한 로또 수량 및 번호를 출력한다. + - [X] 로또 번호는 오름차순으로 정렬하여 보여준다. + - [X] 로또 번호의 숫자 범위는 1~45까지이다. + - [X] 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다. + + +- [X] 당첨 번호를 입력받는다. + - [X] 1 이상 45 이하, 중복되지 않는 6개의 숫자인지 검증 + + +- [X] 보너스 번호를 입력받는다. + - [X] 1 이상 45 이하 이고, 당첨 번호와 중복되지 않는 숫자 여야 함 + + +- [X] 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 게임을 종료한다. + - [X] 당첨 내역을 출력한다. + - [X] 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다. + - 1등: 6개 번호 일치 / 2,000,000,000원 + - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - 3등: 5개 번호 일치 / 1,500,000원 + - 4등: 4개 번호 일치 / 50,000원 + - 5등: 3개 번호 일치 / 5,000원 + + - [X] 수익률을 출력한다. + - [X] 수익률은 소수점 둘째 자리에서 반올림한다. + - (ex. 100.0%, 51.5%, 1,000,000.0%) + + +- [X] 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. + + +--- +## 전체 흐름 +``` +구입금액을 입력해 주세요. +8000 + +8개를 구매했습니다. +[8, 21, 23, 41, 42, 43] +[3, 5, 11, 16, 32, 38] +[7, 11, 16, 35, 36, 44] +[1, 8, 11, 31, 41, 42] +[13, 14, 16, 38, 42, 45] +[7, 11, 30, 40, 42, 43] +[2, 13, 22, 32, 38, 45] +[1, 3, 5, 14, 22, 45] + +당첨 번호를 입력해 주세요. +1,2,3,4,5,6 + +보너스 번호를 입력해 주세요. +7 + +당첨 통계 +--- +3개 일치 (5,000원) - 1개 +4개 일치 (50,000원) - 0개 +5개 일치 (1,500,000원) - 0개 +5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 +6개 일치 (2,000,000,000원) - 0개 +총 수익률은 62.5%입니다. +``` \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba44..45ef536469e 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,12 @@ package lotto; +import camp.nextstep.edu.missionutils.Console; +import lotto.controller.MainController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + MainController mainController = MainController.create(); + mainController.run(); + Console.close(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 519793d1f73..00000000000 --- a/src/main/java/lotto/Lotto.java +++ /dev/null @@ -1,20 +0,0 @@ -package lotto; - -import java.util.List; - -public class Lotto { - private final List numbers; - - public Lotto(List numbers) { - validate(numbers); - this.numbers = numbers; - } - - private void validate(List numbers) { - if (numbers.size() != 6) { - throw new IllegalArgumentException(); - } - } - - // TODO: 추가 기능 구현 -} diff --git a/src/main/java/lotto/constants/Constants.java b/src/main/java/lotto/constants/Constants.java new file mode 100644 index 00000000000..15aa0e45a2b --- /dev/null +++ b/src/main/java/lotto/constants/Constants.java @@ -0,0 +1,8 @@ +package lotto.constants; + +public class Constants { + public static final int LOTTO_PRICE = 1000; + public static final int MIN_LOTTO_NUMBER = 1; + public static final int MAX_LOTTO_NUMBER = 45; + public static final int LOTTO_NUMBERS_SIZE = 6; +} diff --git a/src/main/java/lotto/controller/MainController.java b/src/main/java/lotto/controller/MainController.java new file mode 100644 index 00000000000..32915da3d1c --- /dev/null +++ b/src/main/java/lotto/controller/MainController.java @@ -0,0 +1,83 @@ +package lotto.controller; + +import lotto.domain.*; +import lotto.dto.LottosDto; +import lotto.dto.ResultDto; +import lotto.service.LottoMaker; +import lotto.utils.Mapper; +import lotto.view.InputView; +import lotto.view.OutputView; + +import java.util.List; +import java.util.function.Supplier; + +public class MainController { + private final InputView inputView; + private final OutputView outputView; + + private MainController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public static MainController create() { + return new MainController(InputView.getInstance(), OutputView.getInstance()); + } + + public void run() { + PurchaseAmount purchaseAmount = createPurchaseAmount(); + Lottos lottos = createLottos(purchaseAmount); + printLottos(lottos); + WinningLotto winningLotto = createWinningLotto(); + printResult(lottos, winningLotto, purchaseAmount); + } + + private PurchaseAmount createPurchaseAmount() { + return readUserInput(() -> { + long input = inputView.readPurchaseAmount(); + return PurchaseAmount.from(input); + }); + } + + private Lottos createLottos(PurchaseAmount purchaseAmount) { + long quantityOfLotto = purchaseAmount.getQuantityOfLotto(); + List lottosMade = LottoMaker.makeLottos(quantityOfLotto); + return Lottos.from(lottosMade); + } + + private void printLottos(Lottos lottos) { + LottosDto lottosDto = Mapper.toLottosDto(lottos); + outputView.printLottos(lottosDto); + } + + private WinningLotto createWinningLotto() { + Lotto winningNumbers = createWinningNumbers(); + return readUserInput(() -> { + int bonusNumber = inputView.readBonusNumber(); + return WinningLotto.of(winningNumbers, bonusNumber); + }); + } + + private Lotto createWinningNumbers() { + return readUserInput(() -> { + List numbers = inputView.readWinningNumbers(); + return new Lotto(numbers); + }); + } + + private void printResult(Lottos lottos, WinningLotto winningLotto, PurchaseAmount purchaseAmount) { + RankResult rankResult = lottos.findRanks(winningLotto); + ResultDto resultDto = Mapper.toTotalRankDto(purchaseAmount, rankResult); + outputView.printResult(resultDto); + } + + private T readUserInput(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 00000000000..ead2e307a8e --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,57 @@ +package lotto.domain; + +import java.util.HashSet; +import java.util.List; + +import static lotto.constants.Constants.*; +import static lotto.exception.ErrorMessage.*; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + this.numbers = numbers; + } + + private void validate(List numbers) { + validateSize(numbers); + validateDuplicates(numbers); + validateRange(numbers); + } + + private static void validateSize(List numbers) { + if (numbers.size() != LOTTO_NUMBERS_SIZE) { + throw new IllegalArgumentException(INVALID_LOTTO_NUMBERS_SIZE.getMessage()); + } + } + + private void validateDuplicates(List numbers) { + HashSet uniqueNumbers = new HashSet<>(numbers); + if (numbers.size() != uniqueNumbers.size()) { + throw new IllegalArgumentException(DUPLICATED_LOTTO_NUMBERS.getMessage()); + } + } + + private void validateRange(List numbers) { + boolean isOutOfRange = numbers.stream() + .anyMatch(number -> number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER); + if (isOutOfRange) { + throw new IllegalArgumentException(INVALID_LOTTO_NUMBER_RANGE.getMessage()); + } + } + + public int getMatchCount(Lotto lotto) { + return (int) numbers.stream() + .filter(lotto.numbers::contains) + .count(); + } + + public boolean contains(int number) { + return numbers.contains(number); + } + + public List getNumbers() { + return List.copyOf(numbers); + } +} diff --git a/src/main/java/lotto/domain/LottoRank.java b/src/main/java/lotto/domain/LottoRank.java new file mode 100644 index 00000000000..a1ff2ab4195 --- /dev/null +++ b/src/main/java/lotto/domain/LottoRank.java @@ -0,0 +1,40 @@ +package lotto.domain; + +import java.util.Arrays; +import java.util.Optional; + +public enum LottoRank { + FIFTH(3, false,5_000), + FOURTH(4, false,50_000), + THIRD(5, false,1_500_000), + SECOND(5, true, 30_000_000), + FIRST(6, false, 2_000_000_000); + + private final int matchCount; + private final boolean requiresBonusMatch; + private final int prize; + + LottoRank(int matchCount, boolean requiresBonusMatch, int prize) { + this.matchCount = matchCount; + this.requiresBonusMatch = requiresBonusMatch; + this.prize = prize; + } + + public static Optional findByMatchResult(int matchCount, boolean bonusMatch) { + return Arrays.stream(LottoRank.values()) + .filter(rank -> rank.matchCount == matchCount && (matchCount != 5 || rank.requiresBonusMatch == bonusMatch)) + .findAny(); + } + + public int getMatchCount() { + return matchCount; + } + + public boolean isRequiresBonusMatch() { + return requiresBonusMatch; + } + + public int getPrize() { + return prize; + } +} diff --git a/src/main/java/lotto/domain/Lottos.java b/src/main/java/lotto/domain/Lottos.java new file mode 100644 index 00000000000..1bb2e07bdf3 --- /dev/null +++ b/src/main/java/lotto/domain/Lottos.java @@ -0,0 +1,34 @@ +package lotto.domain; + +import java.util.List; +import java.util.Optional; + +public class Lottos { + private final List lottos; + + private Lottos(List lottos) { + this.lottos = lottos; + } + + public static Lottos from(List lottos) { + return new Lottos(lottos); + } + + public List getLottos() { + return List.copyOf(lottos); + } + + public RankResult findRanks(WinningLotto winningLotto) { + List lottoRanks = lottos.stream() + .map(lotto -> LottoRank.findByMatchResult( + lotto.getMatchCount(winningLotto.getWinningNumbers()), + lotto.contains(winningLotto.getBonusNumber()))) + .flatMap(Optional::stream) + .toList(); + return RankResult.from(lottoRanks); + } + + public long getQuantity() { + return lottos.size(); + } +} diff --git a/src/main/java/lotto/domain/PurchaseAmount.java b/src/main/java/lotto/domain/PurchaseAmount.java new file mode 100644 index 00000000000..32e16d83a6c --- /dev/null +++ b/src/main/java/lotto/domain/PurchaseAmount.java @@ -0,0 +1,27 @@ +package lotto.domain; + +import lotto.utils.PurchaseAmountValidator; + +import static lotto.constants.Constants.LOTTO_PRICE; + +public class PurchaseAmount { + private final long amount; + + private PurchaseAmount(long amount) { + this.amount = amount; + } + + public static PurchaseAmount from(long amount) { + PurchaseAmountValidator.validatePositive(amount); + PurchaseAmountValidator.validateDividedByUnit(amount); + return new PurchaseAmount(amount); + } + + public long getQuantityOfLotto() { + return amount / LOTTO_PRICE; + } + + public long getAmount() { + return amount; + } +} diff --git a/src/main/java/lotto/domain/RankResult.java b/src/main/java/lotto/domain/RankResult.java new file mode 100644 index 00000000000..d7c0caf996d --- /dev/null +++ b/src/main/java/lotto/domain/RankResult.java @@ -0,0 +1,27 @@ +package lotto.domain; + +import java.util.List; + +public class RankResult { + private final List lottoRanks; + + private RankResult(List lottoRanks) { + this.lottoRanks = lottoRanks; + } + + public static RankResult from(List lottoRanks) { + return new RankResult(lottoRanks); + } + + public long getRankCount(LottoRank rank) { + return lottoRanks.stream() + .filter(lottoRank -> lottoRank == rank) + .count(); + } + + public long getTotalPrize() { + return lottoRanks.stream() + .mapToLong(LottoRank::getPrize) + .sum(); + } +} diff --git a/src/main/java/lotto/domain/WinningLotto.java b/src/main/java/lotto/domain/WinningLotto.java new file mode 100644 index 00000000000..0ba8dc02464 --- /dev/null +++ b/src/main/java/lotto/domain/WinningLotto.java @@ -0,0 +1,26 @@ +package lotto.domain; + +import lotto.utils.WinningLottoValidator; + +public class WinningLotto { + private final Lotto winningNumbers; + private final int bonusNumber; + + private WinningLotto(Lotto winningNumbers, int bonusNumber) { + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + public static WinningLotto of(Lotto winningNumbers, int bonusNumber) { + WinningLottoValidator.validateBonusNumber(winningNumbers, bonusNumber); + return new WinningLotto(winningNumbers, bonusNumber); + } + + public Lotto getWinningNumbers() { + return winningNumbers; + } + + public int getBonusNumber() { + return bonusNumber; + } +} diff --git a/src/main/java/lotto/dto/LottoDto.java b/src/main/java/lotto/dto/LottoDto.java new file mode 100644 index 00000000000..98f9549d60d --- /dev/null +++ b/src/main/java/lotto/dto/LottoDto.java @@ -0,0 +1,6 @@ +package lotto.dto; + +import java.util.List; + +public record LottoDto(List numbers) { +} diff --git a/src/main/java/lotto/dto/LottosDto.java b/src/main/java/lotto/dto/LottosDto.java new file mode 100644 index 00000000000..e197ea58a23 --- /dev/null +++ b/src/main/java/lotto/dto/LottosDto.java @@ -0,0 +1,6 @@ +package lotto.dto; + +import java.util.List; + +public record LottosDto(long quantity, List lottoDtos) { +} diff --git a/src/main/java/lotto/dto/RankDto.java b/src/main/java/lotto/dto/RankDto.java new file mode 100644 index 00000000000..ebd00bd4578 --- /dev/null +++ b/src/main/java/lotto/dto/RankDto.java @@ -0,0 +1,6 @@ +package lotto.dto; + +import lotto.domain.LottoRank; + +public record RankDto(LottoRank rank, long quantity) { +} diff --git a/src/main/java/lotto/dto/ResultDto.java b/src/main/java/lotto/dto/ResultDto.java new file mode 100644 index 00000000000..3de19456de1 --- /dev/null +++ b/src/main/java/lotto/dto/ResultDto.java @@ -0,0 +1,6 @@ +package lotto.dto; + +import java.util.List; + +public record ResultDto(List rankDtos, double profit) { +} diff --git a/src/main/java/lotto/exception/ErrorMessage.java b/src/main/java/lotto/exception/ErrorMessage.java new file mode 100644 index 00000000000..b3e349b7c19 --- /dev/null +++ b/src/main/java/lotto/exception/ErrorMessage.java @@ -0,0 +1,23 @@ +package lotto.exception; + +public enum ErrorMessage { + ERROR_CAPTION("[ERROR] "), + NOT_NUMERIC_INPUT("숫자를 입력해 주세요."), + NOT_POSITIVE_INPUT("0 보다 큰 수를 입력해 주세요."), + INVALID_PURCHASE_AMOUNT("1000 으로 나누어 떨어지는 값을 입력해 주세요."), + INVALID_LOTTO_NUMBERS_SIZE("유효하지 않은 로또 숫자 개수 입니다."), + DUPLICATED_LOTTO_NUMBERS("중복된 로또 숫자가 있습니다."), + INVALID_LOTTO_NUMBER_RANGE("1 이상 45 이하의 숫자만 가능합니다."), + INVALID_WINNING_NUMBERS("유효하지 않은 당첨번호 형식 입니다."), + INVALID_BONUS_NUMBER("보너스 번호는 당첨 번호와 중복되지 않는 숫자로 입력해 주세요."); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return ERROR_CAPTION.message + message; + } +} diff --git a/src/main/java/lotto/service/LottoMaker.java b/src/main/java/lotto/service/LottoMaker.java new file mode 100644 index 00000000000..9d8c2d09bbc --- /dev/null +++ b/src/main/java/lotto/service/LottoMaker.java @@ -0,0 +1,15 @@ +package lotto.service; + +import lotto.domain.Lotto; +import lotto.utils.LottoNumbersGenerator; + +import java.util.List; +import java.util.stream.Stream; + +public class LottoMaker { + public static List makeLottos(long quantity) { + return Stream.generate(() -> new Lotto(LottoNumbersGenerator.generate())) + .limit(quantity) + .toList(); + } +} diff --git a/src/main/java/lotto/utils/LottoNumbersGenerator.java b/src/main/java/lotto/utils/LottoNumbersGenerator.java new file mode 100644 index 00000000000..2d25cc144e7 --- /dev/null +++ b/src/main/java/lotto/utils/LottoNumbersGenerator.java @@ -0,0 +1,21 @@ +package lotto.utils; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static lotto.constants.Constants.LOTTO_NUMBERS_SIZE; +import static lotto.constants.Constants.MIN_LOTTO_NUMBER; +import static lotto.constants.Constants.MAX_LOTTO_NUMBER; +import static lotto.exception.ErrorMessage.NOT_NUMERIC_INPUT; + +public class LottoNumbersGenerator { + public static List generate() { + List numbers = Randoms.pickUniqueNumbersInRange(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER, LOTTO_NUMBERS_SIZE); + List sortedNumbers = new ArrayList<>(numbers); + Collections.sort(sortedNumbers); + return sortedNumbers; + } +} diff --git a/src/main/java/lotto/utils/Mapper.java b/src/main/java/lotto/utils/Mapper.java new file mode 100644 index 00000000000..b8e650003ec --- /dev/null +++ b/src/main/java/lotto/utils/Mapper.java @@ -0,0 +1,32 @@ +package lotto.utils; + +import lotto.domain.LottoRank; +import lotto.domain.Lottos; +import lotto.domain.PurchaseAmount; +import lotto.domain.RankResult; +import lotto.dto.LottoDto; +import lotto.dto.LottosDto; +import lotto.dto.RankDto; +import lotto.dto.ResultDto; + +import java.util.Arrays; +import java.util.List; + +public class Mapper { + public static LottosDto toLottosDto(Lottos lottos) { + List lottoDtos = lottos.getLottos().stream() + .map(lotto -> new LottoDto(lotto.getNumbers())) + .toList(); + return new LottosDto(lottos.getQuantity(), lottoDtos); + } + + public static ResultDto toTotalRankDto(PurchaseAmount purchaseAmount, RankResult rankResult) { + List rankDtos = Arrays.stream(LottoRank.values()) + .map(rank -> new RankDto(rank, rankResult.getRankCount(rank))) + .toList(); + long totalPrize = rankResult.getTotalPrize(); + double profit = (double)totalPrize / purchaseAmount.getAmount() * 100; + profit = Math.round(profit * 100.0) / 100.0; + return new ResultDto(rankDtos, profit); + } +} diff --git a/src/main/java/lotto/utils/PurchaseAmountValidator.java b/src/main/java/lotto/utils/PurchaseAmountValidator.java new file mode 100644 index 00000000000..add86e991cf --- /dev/null +++ b/src/main/java/lotto/utils/PurchaseAmountValidator.java @@ -0,0 +1,26 @@ +package lotto.utils; + +import static lotto.constants.Constants.LOTTO_PRICE; +import static lotto.exception.ErrorMessage.*; + +public class PurchaseAmountValidator { + public static long safeParseLong(String input) { + try { + return Long.parseLong(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(NOT_NUMERIC_INPUT.getMessage()); + } + } + + public static void validatePositive(long value) { + if (value <= 0) { + throw new IllegalArgumentException(NOT_POSITIVE_INPUT.getMessage()); + } + } + + public static void validateDividedByUnit(long value) { + if (value % LOTTO_PRICE != 0) { + throw new IllegalArgumentException(INVALID_PURCHASE_AMOUNT.getMessage()); + } + } +} diff --git a/src/main/java/lotto/utils/WinningLottoValidator.java b/src/main/java/lotto/utils/WinningLottoValidator.java new file mode 100644 index 00000000000..6eb1a8c4890 --- /dev/null +++ b/src/main/java/lotto/utils/WinningLottoValidator.java @@ -0,0 +1,59 @@ +package lotto.utils; + +import lotto.domain.Lotto; +import org.junit.platform.commons.util.StringUtils; + +import java.util.List; + +import static lotto.constants.Constants.MAX_LOTTO_NUMBER; +import static lotto.constants.Constants.MIN_LOTTO_NUMBER; +import static lotto.exception.ErrorMessage.*; +import static lotto.exception.ErrorMessage.INVALID_LOTTO_NUMBER_RANGE; + +public class WinningLottoValidator { + public static List safeSplit(String input, String delimiter) { + validateEmpty(input); + validateStartsOrEndsWithDelimiter(input, delimiter); + List splitInput = List.of(input.split(delimiter)); + return splitInput.stream() + .map(WinningLottoValidator::safeParseInt) + .toList(); + } + + private static void validateEmpty(String input) { + if (StringUtils.isBlank(input)) { + throw new IllegalArgumentException(INVALID_WINNING_NUMBERS.getMessage()); + } + } + + private static void validateStartsOrEndsWithDelimiter(String input, String delimiter) { + if (input.startsWith(delimiter) || input.endsWith(delimiter)) { + throw new IllegalArgumentException(INVALID_WINNING_NUMBERS.getMessage()); + } + } + + public static int safeParseInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(NOT_NUMERIC_INPUT.getMessage()); + } + } + + public static void validateBonusNumber(Lotto winningNumbers, int bonusNumber) { + validateRange(bonusNumber); + validateDuplicate(winningNumbers, bonusNumber); + } + + private static void validateDuplicate(Lotto winningNumbers, int bonusNumber) { + if (winningNumbers.getNumbers().contains(bonusNumber)) { + throw new IllegalArgumentException(INVALID_BONUS_NUMBER.getMessage()); + } + } + + private static void validateRange(int bonusNumber) { + if (bonusNumber < MIN_LOTTO_NUMBER || bonusNumber > MAX_LOTTO_NUMBER) { + throw new IllegalArgumentException(INVALID_LOTTO_NUMBER_RANGE.getMessage()); + } + } +} diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 00000000000..d45b14898de --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,47 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; +import lotto.domain.WinningLotto; +import lotto.utils.PurchaseAmountValidator; +import lotto.utils.WinningLottoValidator; + +import java.util.List; + +public class InputView { + private static final InputView instance = new InputView(); + private static final String ASK_PURCHASE_AMOUNT = "구입금액을 입력해 주세요."; + private static final String ASK_WINNING_NUMBERS = "당첨 번호를 입력해 주세요."; + private static final String WINNING_NUMBERS_DELIMITER = ","; + private static final String ASK_BONUS_NUMBER = "보너스 번호를 입력해 주세요."; + + private InputView() { + } + + public static InputView getInstance() { + return instance; + } + + public long readPurchaseAmount() { + System.out.println(ASK_PURCHASE_AMOUNT); + String input = Console.readLine(); + return PurchaseAmountValidator.safeParseLong(input); + } + + public List readWinningNumbers() { + printLine(); + System.out.println(ASK_WINNING_NUMBERS); + String input = Console.readLine(); + return WinningLottoValidator.safeSplit(input, WINNING_NUMBERS_DELIMITER); + } + + private static void printLine() { + System.out.println(); + } + + public int readBonusNumber() { + printLine(); + System.out.println(ASK_BONUS_NUMBER); + String input = Console.readLine(); + return WinningLottoValidator.safeParseInt(input); + } +} diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 00000000000..b05c70aeb90 --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,78 @@ +package lotto.view; + +import lotto.dto.LottoDto; +import lotto.dto.LottosDto; +import lotto.dto.ResultDto; + +import java.util.stream.Collectors; + +public class OutputView { + private static final OutputView instance = new OutputView(); + private static final String LOTTOS_QUANTITY_FORMAT = "%d개를 구매했습니다."; + private static final String START_LOTTO_FORMAT = "["; + private static final String END_LOTTO_FORMAT = "]"; + private static final String LOTTO_NUMBERS_DELIMITER = ", "; + private static final String RESULT_TITLE = "당첨 통계"; + private static final String RESULT_SIGNATURE = "---"; + private static final String RESULT_FORMAT = "%d개 일치 (%,d원) - %d개"; + private static final String RESULT_FORMAT_BONUS_MATCH = "%d개 일치, 보너스 볼 일치 (%,d원) - %d개"; + private static final int RANK_NEEDS_BONUS = 5; + private static final String PROFIT_FORMAT = "총 수익률은 %,.1f%%입니다."; + + private OutputView() { + } + + public static OutputView getInstance() { + return instance; + } + + public void printError(String errorMessage) { + System.out.println(errorMessage); + } + + public void printLottos(LottosDto lottosDto) { + printLottoQuantity(lottosDto.quantity()); + lottosDto.lottoDtos().forEach(this::printLotto); + } + public void printLottoQuantity(long quantity) { + printLine(); + System.out.printf((LOTTOS_QUANTITY_FORMAT) + "%n", quantity); + } + + private void printLine() { + System.out.println(); + } + + private void printLotto(LottoDto lottoDto) { + System.out.print(START_LOTTO_FORMAT); + String lottoNumbers = lottoDto.numbers().stream() + .map(Object::toString) + .collect(Collectors.joining(LOTTO_NUMBERS_DELIMITER)); + System.out.print(lottoNumbers); + System.out.print(END_LOTTO_FORMAT); + printLine(); + } + + public void printResult(ResultDto resultDto) { + printLine(); + System.out.println(RESULT_TITLE); + System.out.println(RESULT_SIGNATURE); + printRankResult(resultDto); + printProfit(resultDto.profit()); + } + + private static void printRankResult(ResultDto resultDto) { + resultDto.rankDtos().forEach(rankDto -> { + String format = RESULT_FORMAT; + if (rankDto.rank().isRequiresBonusMatch()) { + format = RESULT_FORMAT_BONUS_MATCH; + } + System.out.printf((format) + "%n", + rankDto.rank().getMatchCount(), rankDto.rank().getPrize(), rankDto.quantity()); + }); + } + + private void printProfit(double profit) { + System.out.printf((PROFIT_FORMAT) + "%n", profit); + } +} diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 9f5dfe7eb83..2522fdcdde8 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -1,9 +1,14 @@ package lotto; +import lotto.domain.Lotto; 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.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -18,10 +23,23 @@ void createLottoByOverSize() { @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") @Test void createLottoByDuplicatedNumber() { - // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) .isInstanceOf(IllegalArgumentException.class); } - // 아래에 추가 테스트 작성 가능 + @ParameterizedTest(name = "[{index}] 로또 번호에 1 미만, 45 초과하는 숫자가 있으면 예외가 발생한다.") + @MethodSource("lottoNumberProvider") + void createLottoByInvalidRangeNumber(List numbers) { + assertThatThrownBy(() -> new Lotto(numbers)) + .isInstanceOf(IllegalArgumentException.class); + } + + private static Stream lottoNumberProvider() { + return Stream.of( + Arguments.of(List.of(-1, 1, 2, 3, 4, 5)), + Arguments.of(List.of(0, 1, 2, 3, 4, 5)), + Arguments.of(List.of(1, 2, 3, 4, 5, 47)), + Arguments.of(List.of(-100, 1, 2, 3, 4, 5)) + ); + } } \ No newline at end of file diff --git a/src/test/java/lotto/domain/PurchaseAmountTest.java b/src/test/java/lotto/domain/PurchaseAmountTest.java new file mode 100644 index 00000000000..8821c93a87c --- /dev/null +++ b/src/test/java/lotto/domain/PurchaseAmountTest.java @@ -0,0 +1,24 @@ +package lotto.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.assertj.core.api.Assertions.*; + +class PurchaseAmountTest { + @DisplayName("PurchaseAmount 정상 생성") + @Test + void create() { + PurchaseAmount purchaseAmount = PurchaseAmount.from(1000); + assertThat(purchaseAmount).isNotNull(); + } + + @ParameterizedTest(name = "[{index}] PurchaseAmount 생성 시 {0} 을 입력하면 예외 발생한다.") + @ValueSource(ints = {1, 110, 0, -1000}) + void exception(int element) { + assertThatThrownBy(() -> PurchaseAmount.from(element)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/domain/WinningLottoTest.java b/src/test/java/lotto/domain/WinningLottoTest.java new file mode 100644 index 00000000000..8c5d941e079 --- /dev/null +++ b/src/test/java/lotto/domain/WinningLottoTest.java @@ -0,0 +1,19 @@ +package lotto.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + + +class WinningLottoTest { + @ParameterizedTest + @ValueSource(ints = {47, 0, -1, 6}) + void exception(int element) { + Lotto winningNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + Assertions.assertThatThrownBy(() -> WinningLotto.of(winningNumbers, element)) + .isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file