diff --git a/README.md b/README.md index 32b16cd0..1fe627a9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - `Application` : 로또 실행을 담당하는 진입점 클래스 -## model +## domain - `Price` : 구매 가격 VO @@ -13,21 +13,21 @@ - `LottoNumber` : 로또 번호 VO - `Lotto` : 로또 객체 - `Lottos` : 여러 개의 Lotto 객체를 관리하는 일급 컬렉션 -- `LottoResult` : 로또 결과 VO +- `LottoResult` : 로또 당첨 결과 객체 +- `LottoCount` : 로또 구매 개수 VO #### constant -- `LottoBoundary` : 로또 경계값 상수 -- `LottoMatch` : 로또 당첨 결과 상수 +- `WinningRank` : 당첨 순위 Enum -## view +## io - `InputView` : 입력을 담당하는 클래스 - `OutputView` : 출력을 담당하는 클래스 -## controller +## game -- `LottoController` : 로또 전체 흐름을 제어하는 클래스 +- `LottoGame` : 로또 게임을 담당하는 클래스
@@ -44,23 +44,23 @@ ```java 구입금액을 입력해 주세요. -14000 - -14개를 구매했습니다. -[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] -[23,25,33,36,39,41] -[1,3,5,14,22,45] -[5,9,38,41,43,44] -[2,8,9,18,19,21] -[13,14,18,21,23,35] -[17,21,29,37,42,45] -[3,8,27,30,35,44] + 14000 + + 14개를 구매했습니다. + [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] + [23,25,33,36,39,41] + [1,3,5,14,22,45] + [5,9,38,41,43,44] + [2,8,9,18,19,21] + [13,14,18,21,23,35] + [17,21,29,37,42,45] + [3,8,27,30,35,44] ``` ### 새로운 프로그래밍 요구사항 @@ -90,34 +90,34 @@ ```java 구입금액을 입력해 주세요. -14000 - -14개를 구매했습니다. -[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] -[23,25,33,36,39,41] -[1,3,5,14,22,45] -[5,9,38,41,43,44] -[2,8,9,18,19,21] -[13,14,18,21,23,35] -[17,21,29,37,42,45] -[3,8,27,30,35,44] - -지난 주 당첨 번호를 입력해 주세요. -1,2,3,4,5,6 - -당첨 통계 ---------- -3개 일치(5000원)-1개 -4개 일치(50000원)-0개 -5개 일치(1500000원)-0개 -6개 일치(2000000000원)-0개 -총 수익률은0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임) + 14000 + + 14개를 구매했습니다. + [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] + [23,25,33,36,39,41] + [1,3,5,14,22,45] + [5,9,38,41,43,44] + [2,8,9,18,19,21] + [13,14,18,21,23,35] + [17,21,29,37,42,45] + [3,8,27,30,35,44] + + 지난 주 당첨 번호를 입력해 주세요. + 1,2,3,4,5,6 + + 당첨 통계 + --------- + 3개 일치(5000원)-1개 + 4개 일치(50000원)-0개 + 5개 일치(1500000원)-0개 + 6개 일치(2000000000원)-0개 + 총 수익률은0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임) ``` @@ -130,47 +130,106 @@ - 2등을 위한 보너스볼을 추첨한다. - 당첨 통계에 2등을 추가한다. - - 2등 당첨 조건은 당첨 번호 5개 일치 + 보너스 볼 일치다. + - 2등 당첨 조건은 당첨 번호 5개 일치 + 보너스 볼 일치다. -실행 결과 + +- 실행 결과 위 요구사항에 따라 14000원 어치 로또를 구매하였을 경우 프로그램을 실행한 결과는 다음과 같다. ```java 구입금액을 입력해 주세요. -14000 - -14개를 구매했습니다. -[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] -[23,25,33,36,39,41] -[1,3,5,14,22,45] -[5,9,38,41,43,44] -[2,8,9,18,19,21] -[13,14,18,21,23,35] -[17,21,29,37,42,45] -[3,8,27,30,35,44] - -지난 주 당첨 번호를 입력해 주세요. -1,2,3,4,5,6 - -보너스 볼을 입력해 주세요. -7 - -당첨 통계 ---------- -3개 일치(5000원)-1개 -4개 일치(50000원)-0개 -5개 일치(1500000원)-0개 -5개 일치,보너스 볼 일치(30000000원)-0개 -6개 일치(2000000000원)-0개 -총 수익률은0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임) + 14000 + + 14개를 구매했습니다. + [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] + [23,25,33,36,39,41] + [1,3,5,14,22,45] + [5,9,38,41,43,44] + [2,8,9,18,19,21] + [13,14,18,21,23,35] + [17,21,29,37,42,45] + [3,8,27,30,35,44] + + 지난 주 당첨 번호를 입력해 주세요. + 1,2,3,4,5,6 + + 보너스 볼을 입력해 주세요. + 7 + + 당첨 통계 + --------- + 3개 일치(5000원)-1개 + 4개 일치(50000원)-0개 + 5개 일치(1500000원)-0개 + 5개 일치,보너스 볼 일치(30000000원)-0개 + 6개 일치(2000000000원)-0개 + 총 수익률은0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임) ``` ### 새로운 프로그래밍 요구사항 + Java Enum을 적용한다. + +### 4단계 - 로또 수동 구매 + +- 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다. +- 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다. + +실행 결과 + +- 위 요구사항에 따라 14000원 어치 중 수동 3개, 자동 11개를 구매한 경우 프로그램을 실행한 결과는 다음과 같다. + +```java +구입금액을 입력해 주세요. + 14000 + + 수동으로 구매할 로또 수를 입력해 주세요. + 3 + + 수동으로 구매할 번호를 입력해 주세요. + 8,21,23,41,42,43 + 3,5,11,16,32,38 + 7,11,16,35,36,44 + + 수동으로 3장,자동으로 11개를 구매했습니다. + [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] + [23,25,33,36,39,41] + [1,3,5,14,22,45] + [5,9,38,41,43,44] + [2,8,9,18,19,21] + [13,14,18,21,23,35] + [17,21,29,37,42,45] + [3,8,27,30,35,44] + + 지난 주 당첨 번호를 입력해 주세요. + 1,2,3,4,5,6 + + 보너스 볼을 입력해 주세요. + 7 + + 당첨 통계 + --------- + 3개 일치(5000원)-1개 + 4개 일치(50000원)-0개 + 5개 일치(1500000원)-0개 + 5개 일치,보너스 볼 일치(30000000원)-0개 + 6개 일치(2000000000원)-0개 + 총 수익률은0.35입니다.(기준이 1이기 때문에 결과적으로 손해라는 의미임) +``` + +### 5단계 - 리팩터링 + +- 기존 프로그래밍 요구사항을 다시 한번 확인하고, 학습 테스트를 통해 학습한 내용을 반영한다. diff --git a/src/main/java/org/duckstudy/Application.java b/src/main/java/org/duckstudy/Application.java index 811618ef..6fcc749d 100644 --- a/src/main/java/org/duckstudy/Application.java +++ b/src/main/java/org/duckstudy/Application.java @@ -2,17 +2,17 @@ import java.io.BufferedReader; import java.io.InputStreamReader; -import org.duckstudy.controller.LottoController; -import org.duckstudy.view.InputView; -import org.duckstudy.view.OutputView; +import org.duckstudy.game.LottoGame; +import org.duckstudy.io.InputView; +import org.duckstudy.io.OutputView; public class Application { public static void main(String[] args) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); OutputView outputView = new OutputView(); - InputView inputView = new InputView(bufferedReader, outputView); + InputView inputView = new InputView(bufferedReader); - LottoController lottoController = new LottoController(outputView, inputView); - lottoController.run(); + LottoGame lottoGame = new LottoGame(outputView, inputView); + lottoGame.run(); } } diff --git a/src/main/java/org/duckstudy/controller/LottoController.java b/src/main/java/org/duckstudy/controller/LottoController.java deleted file mode 100644 index 6d570936..00000000 --- a/src/main/java/org/duckstudy/controller/LottoController.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.duckstudy.controller; - -import org.duckstudy.model.Price; -import org.duckstudy.model.lotto.Lotto; -import org.duckstudy.model.lotto.LottoNumber; -import org.duckstudy.model.lotto.LottoResult; -import org.duckstudy.model.lotto.Lottos; -import org.duckstudy.view.InputView; -import org.duckstudy.view.OutputView; - -public class LottoController { - - private final OutputView outputView; - private final InputView inputView; - - public LottoController(OutputView outputView, InputView inputView) { - this.outputView = outputView; - this.inputView = inputView; - } - - public void run() { - Price price = createPrice(); - Lottos lottos = Lottos.generateLottosByPrice(price); - outputView.printLottos(lottos); - - Lotto winningLotto = createWinningLotto(); - LottoNumber bonusNumber = createBonusNumber(winningLotto); - - getWinningResult(price, lottos, winningLotto, bonusNumber); - } - - private Price createPrice() { - try { - Price price = new Price(inputView.inputPrice()); - price.validateInputPrice(); - return price; - } catch (IllegalArgumentException e) { - outputView.printException(e); - return createPrice(); - } - } - - private Lotto createWinningLotto() { - try { - return Lotto.from(inputView.inputWinningLotto()); - } catch (IllegalArgumentException e) { - outputView.printException(e); - return createWinningLotto(); - } - } - - private LottoNumber createBonusNumber(Lotto winningLotto) { - LottoNumber bonusNumber = LottoNumber.valueOf(inputView.inputBonusNumber()); - if (winningLotto.containsNumber(bonusNumber)) { - outputView.printExceptionForBonusNumber(); - return createBonusNumber(winningLotto); - } - outputView.printExceptionForBonusNumber(); - return bonusNumber; - } - - private void getWinningResult(Price price, Lottos lottos, Lotto winningLotto, LottoNumber bonusNumber) { - LottoResult result = createLottoResult(lottos, winningLotto, bonusNumber); - - calculateProfitRate(price, result); - } - - private LottoResult createLottoResult(Lottos lottos, Lotto winningLotto, LottoNumber bonusNumber) { - LottoResult result = lottos.accumulateLottoResult(winningLotto, bonusNumber); - outputView.printLottoResult(result); - return result; - } - - private void calculateProfitRate(Price price, LottoResult result) { - double profitRate = price.calculateProfitRate(result); - outputView.printTotalProfit(profitRate); - } -} diff --git a/src/main/java/org/duckstudy/model/Price.java b/src/main/java/org/duckstudy/domain/Price.java similarity index 64% rename from src/main/java/org/duckstudy/model/Price.java rename to src/main/java/org/duckstudy/domain/Price.java index e1184485..f06c63d5 100644 --- a/src/main/java/org/duckstudy/model/Price.java +++ b/src/main/java/org/duckstudy/domain/Price.java @@ -1,8 +1,9 @@ -package org.duckstudy.model; +package org.duckstudy.domain; import java.util.Objects; -import org.duckstudy.model.lotto.LottoResult; -import org.duckstudy.model.lotto.constant.WinningRank; +import org.duckstudy.domain.lotto.LottoCount; +import org.duckstudy.domain.lotto.LottoResult; +import org.duckstudy.domain.lotto.constant.WinningRank; public class Price { @@ -13,16 +14,16 @@ public class Price { private final int value; - public Price(int price) { + public Price(final int price) { validatePrice(price); this.value = price; } - public static Price initialize() { + public static Price zero() { return new Price(INCLUSIVE_MIN_PRICE); } - private void validatePrice(int price) { + private void validatePrice(final int price) { if (price < INCLUSIVE_MIN_PRICE) { throw new IllegalArgumentException(String.format("가격은 %d원 이상이어야 합니다.", INCLUSIVE_MIN_PRICE)); } @@ -34,41 +35,36 @@ public void validateInputPrice() { } } - public Price addPrice(int value) { + public Price addPrice(final int value) { return new Price(this.value + value); } - public Price multiplyTimes(int times) { - return new Price(value * times); - } - - public double divideByPrice(Price divisor) { + public double divideByPrice(final Price divisor) { checkIfZero(divisor.getValue()); return (double) value / divisor.getValue(); } - private void checkIfZero(int divisor) { + private void checkIfZero(final int divisor) { if (divisor == ZERO) { throw new IllegalArgumentException(String.format("%d으로 나눌 수 없습니다.", ZERO)); } } - public int calculateLottoCount() { + public LottoCount calculateLottoCount() { checkIfZero(LOTTO_PRICE); - return value / LOTTO_PRICE; + return new LottoCount(value / LOTTO_PRICE); } - public double calculateProfitRate(LottoResult result) { - Price profit = Price.initialize(); + public double calculateProfitRate(final LottoResult result) { + Price profit = Price.zero(); for (WinningRank winningRank : WinningRank.values()) { - profit = profit.accumulateProfit(winningRank, result.getMatchingCount(winningRank.getKey())); + profit = profit.accumulateProfit(winningRank.getPrice(), result.getMatchingCount(winningRank)); } return profit.divideByPrice(this) * PERCENT_BASE; } - private Price accumulateProfit(WinningRank winningRank, int count) { - return this.addPrice(winningRank.getPrice()) - .multiplyTimes(count); + private Price accumulateProfit(final int price, final int count) { + return this.addPrice(price * count); } public int getValue() { diff --git a/src/main/java/org/duckstudy/model/lotto/Lotto.java b/src/main/java/org/duckstudy/domain/lotto/Lotto.java similarity index 53% rename from src/main/java/org/duckstudy/model/lotto/Lotto.java rename to src/main/java/org/duckstudy/domain/lotto/Lotto.java index aeeef811..a8e43748 100644 --- a/src/main/java/org/duckstudy/model/lotto/Lotto.java +++ b/src/main/java/org/duckstudy/domain/lotto/Lotto.java @@ -1,40 +1,42 @@ -package org.duckstudy.model.lotto; +package org.duckstudy.domain.lotto; import static java.util.Collections.shuffle; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toList; -import static org.duckstudy.model.lotto.constant.LottoNumberRange.END_INCLUSIVE_NUMBER; -import static org.duckstudy.model.lotto.constant.LottoNumberRange.START_INCLUSIVE_NUMBER; +import static java.util.stream.Collectors.toSet; +import static org.duckstudy.domain.lotto.LottoNumber.END_INCLUSIVE_NUMBER; +import static org.duckstudy.domain.lotto.LottoNumber.START_INCLUSIVE_NUMBER; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collector; import java.util.stream.IntStream; import java.util.stream.Stream; public class Lotto { - private static final List NUMBERS; + private static final Set NUMBERS; private static final int LOTTO_SIZE = 6; static { - NUMBERS = IntStream.range(START_INCLUSIVE_NUMBER.getValue(), END_INCLUSIVE_NUMBER.getValue() + 1) + NUMBERS = IntStream.range(START_INCLUSIVE_NUMBER, END_INCLUSIVE_NUMBER + 1) .boxed() - .toList(); + .collect(toSet()); } - private final List lotto; + private final Set lotto; - public Lotto(List lotto) { + public Lotto(final Set lotto) { validateLottoSize(lotto); - validateDuplicate(lotto); - this.lotto = Collections.unmodifiableList(lotto); + this.lotto = new HashSet<>(lotto); } - public static Lotto from(List values) { + public static Lotto from(final Set values) { return new Lotto(values.stream() .map(LottoNumber::valueOf) - .collect(toList())); + .collect(toSet())); } public static Lotto createRandomLotto() { @@ -43,7 +45,7 @@ public static Lotto createRandomLotto() { .limit(LOTTO_SIZE) .sorted() .map(LottoNumber::valueOf) - .collect(toList())); + .collect(toSet())); } private static Collector> getCollector() { @@ -54,30 +56,24 @@ public static Lotto createRandomLotto() { }); } - public int countMatchingNumber(Lotto compareLotto) { + public int countMatchingNumber(final Lotto compareLotto) { return lotto.stream() .filter(lottoNumber -> compareLotto.getLotto().contains(lottoNumber)) .toList() .size(); } - public boolean containsNumber(LottoNumber lottoNumber) { + public boolean containsNumber(final LottoNumber lottoNumber) { return lotto.contains(lottoNumber); } - private void validateLottoSize(List lotto) { + private void validateLottoSize(final Set lotto) { if (lotto.size() != LOTTO_SIZE) { - throw new IllegalArgumentException(String.format("로또 번호는 %d개여야 합니다.", LOTTO_SIZE)); + throw new IllegalArgumentException(String.format("로또 번호는 중복되지 않은 %d개여야 합니다.\n", LOTTO_SIZE)); } } - private void validateDuplicate(List lotto) { - if (lotto.stream().distinct().count() != LOTTO_SIZE) { - throw new IllegalArgumentException("로또 번호는 중복되지 않아야 합니다."); - } - } - - public List getLotto() { - return lotto; + public Set getLotto() { + return Collections.unmodifiableSet(lotto); } } diff --git a/src/main/java/org/duckstudy/domain/lotto/LottoCount.java b/src/main/java/org/duckstudy/domain/lotto/LottoCount.java new file mode 100644 index 00000000..7bf975eb --- /dev/null +++ b/src/main/java/org/duckstudy/domain/lotto/LottoCount.java @@ -0,0 +1,50 @@ +package org.duckstudy.domain.lotto; + +import java.util.Objects; + +public class LottoCount { + + private final int count; + + public LottoCount(final int count) { + validateLottoCount(count); + this.count = count; + } + + private void validateLottoCount(final int count) { + if (count < 0) { + throw new IllegalArgumentException("로또 개수는 0개 이상이어야 합니다."); + } + } + + public void validateManualLottoCount(final LottoCount totalLottoCount) { + if (count > totalLottoCount.getCount()) { + throw new IllegalArgumentException("수동으로 구매할 로또 수가 전체 로또 수를 초과합니다.\n"); + } + } + + public LottoCount subtract(final LottoCount lottoCount) { + return new LottoCount(count - lottoCount.count); + } + + public int getCount() { + return count; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LottoCount that = (LottoCount) o; + return count == that.count; + } + + @Override + public int hashCode() { + return Objects.hash(count); + } +} diff --git a/src/main/java/org/duckstudy/model/lotto/LottoNumber.java b/src/main/java/org/duckstudy/domain/lotto/LottoNumber.java similarity index 55% rename from src/main/java/org/duckstudy/model/lotto/LottoNumber.java rename to src/main/java/org/duckstudy/domain/lotto/LottoNumber.java index 62a18119..d51dac4f 100644 --- a/src/main/java/org/duckstudy/model/lotto/LottoNumber.java +++ b/src/main/java/org/duckstudy/domain/lotto/LottoNumber.java @@ -1,7 +1,4 @@ -package org.duckstudy.model.lotto; - -import static org.duckstudy.model.lotto.constant.LottoNumberRange.END_INCLUSIVE_NUMBER; -import static org.duckstudy.model.lotto.constant.LottoNumberRange.START_INCLUSIVE_NUMBER; +package org.duckstudy.domain.lotto; import java.util.List; import java.util.Objects; @@ -9,31 +6,33 @@ public class LottoNumber { + public static final int START_INCLUSIVE_NUMBER = 1; + public static final int END_INCLUSIVE_NUMBER = 45; private static final List cache; static { - cache = IntStream.range(0, END_INCLUSIVE_NUMBER.getValue()) - .mapToObj(i -> new LottoNumber(START_INCLUSIVE_NUMBER.getValue() + i)) + cache = IntStream.range(0, END_INCLUSIVE_NUMBER) + .mapToObj(i -> new LottoNumber(START_INCLUSIVE_NUMBER + i)) .toList(); } private final int value; - private LottoNumber(int number) { + private LottoNumber(final int number) { this.value = number; } - public static LottoNumber valueOf(int number) { + public static LottoNumber valueOf(final int number) { validateNumber(number); - return cache.get(number - START_INCLUSIVE_NUMBER.getValue()); + return cache.get(number - START_INCLUSIVE_NUMBER); } - private static void validateNumber(int number) { - if (number < START_INCLUSIVE_NUMBER.getValue() || number > END_INCLUSIVE_NUMBER.getValue()) { + private static void validateNumber(final int number) { + if (number < START_INCLUSIVE_NUMBER || number > END_INCLUSIVE_NUMBER) { throw new IllegalArgumentException( - String.format("로또 번호는 %d 이상 %d 이하의 숫자여야 합니다.", START_INCLUSIVE_NUMBER.getValue(), - END_INCLUSIVE_NUMBER.getValue())); + String.format("로또 번호는 %d 이상 %d 이하의 숫자여야 합니다.", START_INCLUSIVE_NUMBER, + END_INCLUSIVE_NUMBER)); } } diff --git a/src/main/java/org/duckstudy/domain/lotto/LottoResult.java b/src/main/java/org/duckstudy/domain/lotto/LottoResult.java new file mode 100644 index 00000000..a44d7c3e --- /dev/null +++ b/src/main/java/org/duckstudy/domain/lotto/LottoResult.java @@ -0,0 +1,48 @@ +package org.duckstudy.domain.lotto; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.duckstudy.domain.lotto.constant.WinningRank; + +public class LottoResult { + + public static final int DEFAULT_FREQUENCY = 1; + public static final int DEFAULT_VALUE = 0; + + private final Map result; + + public LottoResult(final Map result) { + this.result = new HashMap<>(result); + } + + public static LottoResult createLottoResult(final Lotto lotto, final Lotto winningLotto, + final LottoNumber bonusNumber) { + int matchingCount = lotto.countMatchingNumber(winningLotto); + boolean matchBonus = lotto.containsNumber(bonusNumber); + + WinningRank winningRank = WinningRank.findByMatchCountAndBonus(matchingCount, matchBonus); + + return new LottoResult(Map.of(winningRank, DEFAULT_FREQUENCY)); + } + + public LottoResult merge(final LottoResult other) { + return new LottoResult(Stream.of(this.result, other.result) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + Integer::sum + ))); + } + + public int getMatchingCount(final WinningRank winningRank) { + return result.getOrDefault(winningRank, DEFAULT_VALUE); + } + + public Map getResult() { + return Collections.unmodifiableMap(result); + } +} diff --git a/src/main/java/org/duckstudy/model/lotto/Lottos.java b/src/main/java/org/duckstudy/domain/lotto/Lottos.java similarity index 55% rename from src/main/java/org/duckstudy/model/lotto/Lottos.java rename to src/main/java/org/duckstudy/domain/lotto/Lottos.java index 9c4690e3..9a40e2b9 100644 --- a/src/main/java/org/duckstudy/model/lotto/Lottos.java +++ b/src/main/java/org/duckstudy/domain/lotto/Lottos.java @@ -1,41 +1,41 @@ -package org.duckstudy.model.lotto; +package org.duckstudy.domain.lotto; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toList; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Stream; -import org.duckstudy.model.Price; public class Lottos { private final List lottos; - public Lottos(List lottos) { - this.lottos = Collections.unmodifiableList(lottos); + public Lottos(final List lottos) { + this.lottos = new ArrayList<>(lottos); } - public static Lottos generateLottosByPrice(Price price) { - int lottoCount = price.calculateLottoCount(); - + public static Lottos generateLottos(final int lottoCount) { return Stream.generate(Lotto::createRandomLotto) .limit(lottoCount) .collect(collectingAndThen(toList(), Lottos::new)); } - public LottoResult accumulateLottoResult(Lotto winningLotto, LottoNumber bonusNumber) { + public LottoResult accumulateLottoResult(final Lotto winningLotto, final LottoNumber bonusNumber) { return lottos.stream() .map(lotto -> LottoResult.createLottoResult(lotto, winningLotto, bonusNumber)) .reduce(new LottoResult(Map.of()), LottoResult::merge); } - public List getLottos() { - return lottos; + public Lottos merge(final Lottos other) { + return new Lottos(Stream.of(this.lottos, other.lottos) + .flatMap(List::stream) + .collect(toList())); } - public int getSize() { - return lottos.size(); + public List getLottos() { + return Collections.unmodifiableList(lottos); } } diff --git a/src/main/java/org/duckstudy/domain/lotto/constant/WinningRank.java b/src/main/java/org/duckstudy/domain/lotto/constant/WinningRank.java new file mode 100644 index 00000000..0abbac9c --- /dev/null +++ b/src/main/java/org/duckstudy/domain/lotto/constant/WinningRank.java @@ -0,0 +1,45 @@ +package org.duckstudy.domain.lotto.constant; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiPredicate; + +public enum WinningRank { + + NONE(List.of(0, 1, 2), 0, (matchCount, matchBonus) -> matchCount < 3), + FIFTH(List.of(3), 5_000, (matchCount, matchBonus) -> matchCount == 3), + FOURTH(List.of(4), 50_000, (matchCount, matchBonus) -> matchCount == 4), + THIRD(List.of(5), 1_500_000, (matchCount, matchBonus) -> matchCount == 5 && !matchBonus), + SECOND(List.of(5), 30_000_000, (matchCount, matchBonus) -> matchCount == 5 && matchBonus), + FIRST(List.of(6), 2_000_000_000, (matchCount, matchBonus) -> matchCount == 6); + + private final List matchCount; + private final int price; + private final BiPredicate isMatchPredicate; + + WinningRank(final List matchCount, final int price, + final BiPredicate isMatchPredicate) { + this.matchCount = matchCount; + this.price = price; + this.isMatchPredicate = isMatchPredicate; + } + + public static WinningRank findByMatchCountAndBonus(final int matchCount, final boolean matchBonus) { + return Arrays.stream(values()) + .filter(winningLank -> winningLank.isMatch(matchCount, matchBonus)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("적절한 당첨 등수를 찾을 수 없습니다.")); + } + + private boolean isMatch(final int matchCount, final boolean matchBonus) { + return isMatchPredicate.test(matchCount, matchBonus); + } + + public List getMatchCount() { + return matchCount; + } + + public int getPrice() { + return price; + } +} diff --git a/src/main/java/org/duckstudy/game/LottoGame.java b/src/main/java/org/duckstudy/game/LottoGame.java new file mode 100644 index 00000000..b06cd180 --- /dev/null +++ b/src/main/java/org/duckstudy/game/LottoGame.java @@ -0,0 +1,123 @@ +package org.duckstudy.game; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.duckstudy.domain.Price; +import org.duckstudy.domain.lotto.Lotto; +import org.duckstudy.domain.lotto.LottoCount; +import org.duckstudy.domain.lotto.LottoNumber; +import org.duckstudy.domain.lotto.LottoResult; +import org.duckstudy.domain.lotto.Lottos; +import org.duckstudy.io.InputView; +import org.duckstudy.io.OutputView; + +public class LottoGame { + + private final OutputView outputView; + private final InputView inputView; + + public LottoGame(final OutputView outputView, final InputView inputView) { + this.outputView = outputView; + this.inputView = inputView; + } + + public void run() { + Price price = createPrice(); + Lottos totalLottos = createTotalLottos(price); + + Lotto winningLotto = createWinningLotto(); + LottoNumber bonusNumber = createBonusNumber(winningLotto); + + printWinningResult(price, totalLottos, winningLotto, bonusNumber); + } + + private Lottos createTotalLottos(final Price price) { + LottoCount totalLottoCount = price.calculateLottoCount(); + LottoCount manualLottoCount = createManualLottoCount(totalLottoCount); + Lottos manualLottos = createManualLottos(manualLottoCount.getCount()); + + LottoCount autoLottoCount = totalLottoCount.subtract(manualLottoCount); + Lottos autoLottos = Lottos.generateLottos(autoLottoCount.getCount()); + Lottos totalLottos = manualLottos.merge(autoLottos); + + outputView.printLottos(manualLottoCount.getCount(), autoLottoCount.getCount(), totalLottos); + return totalLottos; + } + + private Price createPrice() { + outputView.printInputPrice(); + try { + Price price = new Price(inputView.inputPrice()); + price.validateInputPrice(); + return price; + } catch (IllegalArgumentException e) { + outputView.printException(e); + return createPrice(); + } + } + + private Lottos createManualLottos(final int manualLottoCount) { + outputView.printInputManualLotto(); + + return new Lottos(IntStream.range(0, manualLottoCount) + .mapToObj(i -> createManualLotto()) + .collect(Collectors.toList())); + } + + private LottoCount createManualLottoCount(final LottoCount lottoCount) { + outputView.printInputManualLottoCount(); + try { + LottoCount manualLottoCount = new LottoCount(inputView.inputManualLottoCount()); + manualLottoCount.validateManualLottoCount(lottoCount); + return manualLottoCount; + } catch (IllegalArgumentException e) { + outputView.printException(e); + return createManualLottoCount(lottoCount); + } + } + + private Lotto createManualLotto() { + try { + return Lotto.from(inputView.inputManualLotto()); + } catch (IllegalArgumentException e) { + outputView.printException(e); + return createManualLotto(); + } + } + + private Lotto createWinningLotto() { + outputView.printInputWinningLotto(); + try { + return Lotto.from(inputView.inputWinningLotto()); + } catch (IllegalArgumentException e) { + outputView.printException(e); + return createWinningLotto(); + } + } + + private LottoNumber createBonusNumber(final Lotto winningLotto) { + outputView.printInputBonusNumber(); + try { + LottoNumber bonusNumber = LottoNumber.valueOf(inputView.inputBonusNumber()); + validateBonusNumber(winningLotto, bonusNumber); + return bonusNumber; + } catch (IllegalArgumentException e) { + outputView.printException(e); + return createBonusNumber(winningLotto); + } + } + + private void validateBonusNumber(final Lotto winningLotto, final LottoNumber bonusNumber) { + if (winningLotto.containsNumber(bonusNumber)) { + throw new IllegalArgumentException("당첨 번호와 중복되는 보너스 볼은 입력할 수 없습니다."); + } + } + + private void printWinningResult(Price price, Lottos totalLottos, Lotto winningLotto, LottoNumber bonusNumber) { + LottoResult lottoResult = totalLottos.accumulateLottoResult(winningLotto, bonusNumber); + outputView.printLottoResult(lottoResult); + + double profitRate = price.calculateProfitRate(lottoResult); + outputView.printTotalProfit(profitRate); + } +} diff --git a/src/main/java/org/duckstudy/view/InputView.java b/src/main/java/org/duckstudy/io/InputView.java similarity index 57% rename from src/main/java/org/duckstudy/view/InputView.java rename to src/main/java/org/duckstudy/io/InputView.java index faf356c7..fec048bd 100644 --- a/src/main/java/org/duckstudy/view/InputView.java +++ b/src/main/java/org/duckstudy/io/InputView.java @@ -1,22 +1,21 @@ -package org.duckstudy.view; +package org.duckstudy.io; + +import static java.util.stream.Collectors.toSet; import java.io.BufferedReader; import java.io.IOException; import java.util.Arrays; -import java.util.List; +import java.util.Set; public class InputView { private final BufferedReader bufferedReader; - private final OutputView outputView; - public InputView(BufferedReader bufferedReader, OutputView outputView) { + public InputView(BufferedReader bufferedReader) { this.bufferedReader = bufferedReader; - this.outputView = outputView; } public int inputPrice() { - outputView.printInputPrice(); try { return Integer.parseInt(bufferedReader.readLine()); } catch (NumberFormatException | IOException e) { @@ -24,9 +23,7 @@ public int inputPrice() { } } - public List inputWinningLotto() { - outputView.printInputWinningLotto(); - + public Set inputWinningLotto() { try { return inputLottoNumber(); } catch (NumberFormatException | IOException e) { @@ -34,20 +31,34 @@ public List inputWinningLotto() { } } - private List inputLottoNumber() throws IOException { + private Set inputLottoNumber() throws IOException { return Arrays.stream(bufferedReader.readLine().split(",")) .map(String::trim) .map(Integer::parseInt) - .toList(); + .collect(toSet()); } public int inputBonusNumber() { - outputView.printInputBonusNumber(); + try { + return Integer.parseInt(bufferedReader.readLine()); + } catch (NumberFormatException | IOException e) { + throw new NumberFormatException("숫자만 입력 가능합니다."); + } + } + public int inputManualLottoCount() { try { return Integer.parseInt(bufferedReader.readLine()); } catch (NumberFormatException | IOException e) { throw new NumberFormatException("숫자만 입력 가능합니다."); } } + + public Set inputManualLotto() { + try { + return inputLottoNumber(); + } catch (NumberFormatException | IOException e) { + throw new NumberFormatException("숫자만 입력 가능합니다.\n"); + } + } } diff --git a/src/main/java/org/duckstudy/io/OutputView.java b/src/main/java/org/duckstudy/io/OutputView.java new file mode 100644 index 00000000..7beca5ab --- /dev/null +++ b/src/main/java/org/duckstudy/io/OutputView.java @@ -0,0 +1,92 @@ +package org.duckstudy.io; + +import static org.duckstudy.domain.lotto.constant.WinningRank.NONE; +import static org.duckstudy.domain.lotto.constant.WinningRank.SECOND; + +import java.util.List; +import java.util.stream.Collectors; +import org.duckstudy.domain.lotto.Lotto; +import org.duckstudy.domain.lotto.LottoNumber; +import org.duckstudy.domain.lotto.LottoResult; +import org.duckstudy.domain.lotto.Lottos; +import org.duckstudy.domain.lotto.constant.WinningRank; + +public class OutputView { + + public void printInputPrice() { + System.out.println("구입금액을 입력해 주세요."); + } + + public void printException(final Exception e) { + System.out.println(e.getMessage()); + } + + public void printLottos(final int manualLottoCount, final int autoLottoCount, final Lottos lottos) { + System.out.printf("\n수동으로 %d개, 자동으로 %d개를 구매했습니다.\n", manualLottoCount, autoLottoCount); + lottos.getLottos() + .forEach(this::printLotto); + } + + private void printLotto(final Lotto lotto) { + System.out.println(lotto.getLotto() + .stream() + .map(LottoNumber::getValue) + .sorted() + .map(String::valueOf) + .collect(Collectors.joining(", ", "[", "]"))); + } + + public void printInputWinningLotto() { + System.out.println("\n지난 주 당첨 번호를 입력해 주세요."); + } + + public void printInputBonusNumber() { + System.out.println("\n보너스 볼을 입력해 주세요."); + } + + public void printLottoResult(final LottoResult result) { + System.out.println("\n당첨 통계"); + System.out.println("---------"); + iterateLottoResult(result); + } + + private void iterateLottoResult(final LottoResult result) { + for (WinningRank winningRank : WinningRank.values()) { + printMatchingResult(result, winningRank); + } + System.out.println(); + } + + private void printMatchingResult(final LottoResult result, final WinningRank winningRank) { + if (winningRank == NONE) { + return; + } + + List matchCounts = winningRank.getMatchCount(); + int price = winningRank.getPrice(); + + String matchPriceMessage = getMatchPrice(winningRank, matchCounts.get(0), price); + int matchingCount = result.getMatchingCount(winningRank); + + System.out.println(matchPriceMessage + matchingCount + "개"); + } + + private String getMatchPrice(final WinningRank winningRank, final int cnt, final int price) { + if (winningRank == SECOND) { + return String.format("%d개 일치, 보너스 볼 일치(%d원)- ", cnt, price); + } + return String.format("%d개 일치 (%d원)- ", cnt, price); + } + + public void printTotalProfit(final double totalProfitRate) { + System.out.printf("총 수익률은 %.2f입니다.\n", totalProfitRate); + } + + public void printInputManualLottoCount() { + System.out.println("\n수동으로 구매할 로또 수를 입력해 주세요."); + } + + public void printInputManualLotto() { + System.out.println("\n수동으로 구매할 번호를 입력해 주세요."); + } +} diff --git a/src/main/java/org/duckstudy/model/lotto/LottoResult.java b/src/main/java/org/duckstudy/model/lotto/LottoResult.java deleted file mode 100644 index ec43bcc3..00000000 --- a/src/main/java/org/duckstudy/model/lotto/LottoResult.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.duckstudy.model.lotto; - -import java.util.Collections; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.duckstudy.model.lotto.constant.WinningRank; - -public class LottoResult { - - public static final int DEFAULT_FREQUENCY = 1; - public static final int DEFAULT_VALUE = 0; - - private final Map result; - - public LottoResult(Map result) { - this.result = Collections.unmodifiableMap(result); - } - - public static LottoResult createLottoResult(Lotto lotto, Lotto winningLotto, LottoNumber bonusNumber) { - int matchingCount = lotto.countMatchingNumber(winningLotto); - boolean matchBonus = lotto.containsNumber(bonusNumber); - - int key = WinningRank.findByMatchCountAndBonus(matchingCount, matchBonus) - .getKey(); - - return new LottoResult(Map.of(key, DEFAULT_FREQUENCY)); - } - - public LottoResult merge(LottoResult other) { - return new LottoResult(Stream.of(this.result, other.result) - .flatMap(map -> map.entrySet().stream()) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - Integer::sum - ))); - } - - public int getMatchingCount(int count) { - return result.getOrDefault(count, DEFAULT_VALUE); - } - - public Map getResult() { - return result; - } -} diff --git a/src/main/java/org/duckstudy/model/lotto/constant/LottoNumberRange.java b/src/main/java/org/duckstudy/model/lotto/constant/LottoNumberRange.java deleted file mode 100644 index f499c792..00000000 --- a/src/main/java/org/duckstudy/model/lotto/constant/LottoNumberRange.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.duckstudy.model.lotto.constant; - -public enum LottoNumberRange { - - START_INCLUSIVE_NUMBER(1), - END_INCLUSIVE_NUMBER(45); - - private final int value; - - LottoNumberRange(int value) { - this.value = value; - } - - public int getValue() { - return value; - } -} diff --git a/src/main/java/org/duckstudy/model/lotto/constant/WinningRank.java b/src/main/java/org/duckstudy/model/lotto/constant/WinningRank.java deleted file mode 100644 index 277b72ce..00000000 --- a/src/main/java/org/duckstudy/model/lotto/constant/WinningRank.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.duckstudy.model.lotto.constant; - -import java.util.stream.Stream; - -public enum WinningRank { - - NONE(0, 0, 0), - FIFTH(3, 5000, 3), - FOURTH(4, 50000, 4), - THIRD(5, 1500000, 5), - SECOND(5, 30000000, -5), - FIRST(6, 2000000000, 6); - - private final int matchCount; - private final int price; - private final int key; - - WinningRank(int matchCount, int price, int key) { - this.matchCount = matchCount; - this.price = price; - this.key = key; - } - - public static WinningRank findByMatchCountAndBonus(int matchCount, boolean matchBonus) { - if (matchCount == SECOND.getMatchCount() && matchBonus) { - return SECOND; - } - return Stream.of(values()) - .filter(winningLank -> winningLank.getMatchCount() == matchCount) - .findFirst() - .orElse(NONE); - } - - public int getMatchCount() { - return matchCount; - } - - public int getPrice() { - return price; - } - - public int getKey() { - return key; - } -} diff --git a/src/main/java/org/duckstudy/view/OutputView.java b/src/main/java/org/duckstudy/view/OutputView.java deleted file mode 100644 index 460b6960..00000000 --- a/src/main/java/org/duckstudy/view/OutputView.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.duckstudy.view; - -import static org.duckstudy.model.lotto.constant.WinningRank.NONE; -import static org.duckstudy.model.lotto.constant.WinningRank.SECOND; - -import java.util.stream.Collectors; -import org.duckstudy.model.lotto.Lotto; -import org.duckstudy.model.lotto.LottoNumber; -import org.duckstudy.model.lotto.LottoResult; -import org.duckstudy.model.lotto.Lottos; -import org.duckstudy.model.lotto.constant.WinningRank; - -public class OutputView { - - public void printInputPrice() { - System.out.println("구입금액을 입력해 주세요."); - } - - public void printException(Exception e) { - System.out.println(e.getMessage()); - } - - public void printLottos(Lottos lottos) { - System.out.printf("\n%d개를 구매했습니다.\n", lottos.getSize()); - lottos.getLottos() - .forEach(this::printLotto); - } - - private void printLotto(Lotto lotto) { - System.out.println(lotto.getLotto() - .stream() - .map(LottoNumber::getValue) - .map(String::valueOf) - .collect(Collectors.joining(", ", "[", "]"))); - } - - public void printInputWinningLotto() { - System.out.println("\n지난 주 당첨 번호를 입력해 주세요."); - } - - public void printInputBonusNumber() { - System.out.println("\n보너스 볼을 입력해 주세요."); - } - - public void printLottoResult(LottoResult result) { - System.out.println("\n당첨 통계"); - System.out.println("---------"); - iterateLottoResult(result); - } - - private void iterateLottoResult(LottoResult result) { - for (WinningRank winningRank : WinningRank.values()) { - printMatchingResult(result, winningRank); - } - System.out.println(); - } - - private void printMatchingResult(LottoResult result, WinningRank winningRank) { - if (winningRank == NONE) { - return; - } - - int cnt = winningRank.getMatchCount(); - int key = winningRank.getKey(); - int price = winningRank.getPrice(); - - String matchPriceMessage = getMatchPrice(winningRank, cnt, price); - int matchingCount = result.getMatchingCount(key); - - System.out.println(matchPriceMessage + matchingCount + "개"); - } - - private String getMatchPrice(WinningRank winningRank, int cnt, int price) { - if (winningRank == SECOND) { - return String.format("%d개 일치, 보너스 볼 일치(%d원)- ", cnt, price); - } - return String.format("%d개 일치 (%d원)- ", cnt, price); - } - - public void printTotalProfit(double totalProfitRate) { - System.out.printf("총 수익률은 %.2f입니다.\n", totalProfitRate); - } - - public void printExceptionForBonusNumber() { - System.out.println("보너스 볼은 당첨 번호와 중복되면 안됩니다."); - } -} diff --git a/src/test/java/org/duckstudy/model/PriceTest.java b/src/test/java/org/duckstudy/domain/PriceTest.java similarity index 65% rename from src/test/java/org/duckstudy/model/PriceTest.java rename to src/test/java/org/duckstudy/domain/PriceTest.java index df9875ab..7fb21d70 100644 --- a/src/test/java/org/duckstudy/model/PriceTest.java +++ b/src/test/java/org/duckstudy/domain/PriceTest.java @@ -1,4 +1,4 @@ -package org.duckstudy.model; +package org.duckstudy.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; @@ -6,8 +6,8 @@ import java.util.HashMap; import java.util.Map; -import org.duckstudy.model.lotto.LottoResult; -import org.duckstudy.model.lotto.constant.WinningRank; +import org.duckstudy.domain.lotto.LottoResult; +import org.duckstudy.domain.lotto.constant.WinningRank; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -38,27 +38,57 @@ void validateFailWhenPriceIsLessThanZero() { } @Nested - @DisplayName("가격 계산 테스트") - class PriceCalculateTest { + @DisplayName("입력 가격 검증 테스트") + class InputPriceValidationTest { @Test - @DisplayName("가격을 더한다") - void addPrice() { + @DisplayName("입력 가격이 양수일 경우 성공한다") + void validateInputPriceSuccessWhenPriceIsEqualOrGreaterThanZero() { - Price price = new Price(1000); + Price price = new Price(1); - Price result = price.addPrice(2000); + assertThatCode(price::validateInputPrice) + .doesNotThrowAnyException(); + } - assertThat(result).isEqualTo(new Price(3000)); + @Test + @DisplayName("입력 가격이 0 이하일 경우 예외가 발생한다") + void validateInputPriceFailWhenPriceIsZero() { + + Price price = new Price(0); + + assertThatThrownBy(price::validateInputPrice) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("가격은 0원 이상이어야 합니다.\n"); + } + } + + @Nested + @DisplayName("가격 생성 테스트") + class PriceCreationTest { + + @Test + @DisplayName("0원으로 가격을 생성하면 성공한다") + void createPriceWhenZero() { + + Price price = Price.zero(); + + assertThat(price).isEqualTo(new Price(0)); } + } + + + @Nested + @DisplayName("가격 계산 테스트") + class PriceCalculateTest { @Test - @DisplayName("가격을 곱한다") - void multiplyPrice() { + @DisplayName("가격을 더한다") + void addPrice() { Price price = new Price(1000); - Price result = price.multiplyTimes(3); + Price result = price.addPrice(2000); assertThat(result).isEqualTo(new Price(3000)); } @@ -96,18 +126,18 @@ void calculateLottoCountWhenInputPrice() { Price price = new Price(10000); - assertThat(price.calculateLottoCount()).isEqualTo(10); + assertThat(price.calculateLottoCount().getCount()).isEqualTo(10); } @Test @DisplayName("로또 수익률을 계산한다") void calculateProfitRate() { Price purchasePrice = new Price(15000); - Map result = new HashMap<>(); - result.put(WinningRank.FIRST.getKey(), 1); + Map result = new HashMap<>(); + result.put(WinningRank.SECOND, 1); LottoResult lottoResult = new LottoResult(result); - assertThat(purchasePrice.calculateProfitRate(lottoResult)).isEqualTo(1.3333333333333334E7); + assertThat(purchasePrice.calculateProfitRate(lottoResult)).isEqualTo(200000.0); } } } diff --git a/src/test/java/org/duckstudy/domain/lotto/LottoCountTest.java b/src/test/java/org/duckstudy/domain/lotto/LottoCountTest.java new file mode 100644 index 00000000..535bdde3 --- /dev/null +++ b/src/test/java/org/duckstudy/domain/lotto/LottoCountTest.java @@ -0,0 +1,79 @@ +package org.duckstudy.domain.lotto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("로또 개수 테스트") +class LottoCountTest { + + @Nested + @DisplayName("로또 개수 생성 테스트") + class LottoCountCreationTest { + + @Test + @DisplayName("로또 개수를 입력하면 로또 개수를 생성한다") + public void createLottoCountWhenInputCount() { + + assertThatCode(() -> new LottoCount(10)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("로또 개수가 음수일 경우 예외가 발생한다") + public void createLottoCountFailWhenCountIsLessThanZero() { + + assertThatThrownBy(() -> new LottoCount(-1)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("로또 개수는 0개 이상이어야 합니다."); + } + } + + @Nested + @DisplayName("수동 로또 개수 검증 테스트") + class ManualLottoCountValidationTest { + + @Test + @DisplayName("수동으로 구매할 로또 수가 전체 로또 수를 초과하지 않으면 성공한다") + public void validateManualLottoCountSuccessWhenManualLottoCountIsNotGreaterThanTotalLottoCount() { + + LottoCount manualLottoCount = new LottoCount(10); + LottoCount totalLottoCount = new LottoCount(15); + + assertThatCode(() -> manualLottoCount.validateManualLottoCount(totalLottoCount)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("수동으로 구매할 로또 수가 전체 로또 수를 초과하면 예외가 발생한다") + public void validateManualLottoCountFailWhenManualLottoCountIsGreaterThanTotalLottoCount() { + + LottoCount manualLottoCount = new LottoCount(20); + LottoCount totalLottoCount = new LottoCount(15); + + assertThatThrownBy(() -> manualLottoCount.validateManualLottoCount(totalLottoCount)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("수동으로 구매할 로또 수가 전체 로또 수를 초과합니다.\n"); + } + } + + @Nested + @DisplayName("로또 개수 계산 테스트") + class LottoCountCalculateTest { + + @Test + @DisplayName("로또 개수를 뺀다") + public void subtractLottoCount() { + + LottoCount lottoCount = new LottoCount(10); + + LottoCount result = lottoCount.subtract(new LottoCount(5)); + + assertThat(result).isEqualTo(new LottoCount(5)); + } + } +} diff --git a/src/test/java/org/duckstudy/model/lotto/LottoNumberTest.java b/src/test/java/org/duckstudy/domain/lotto/LottoNumberTest.java similarity index 97% rename from src/test/java/org/duckstudy/model/lotto/LottoNumberTest.java rename to src/test/java/org/duckstudy/domain/lotto/LottoNumberTest.java index 88376eb6..311e24fd 100644 --- a/src/test/java/org/duckstudy/model/lotto/LottoNumberTest.java +++ b/src/test/java/org/duckstudy/domain/lotto/LottoNumberTest.java @@ -1,4 +1,4 @@ -package org.duckstudy.model.lotto; +package org.duckstudy.domain.lotto; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/src/test/java/org/duckstudy/model/lotto/LottoResultTest.java b/src/test/java/org/duckstudy/domain/lotto/LottoResultTest.java similarity index 51% rename from src/test/java/org/duckstudy/model/lotto/LottoResultTest.java rename to src/test/java/org/duckstudy/domain/lotto/LottoResultTest.java index 12624b4b..1be0dbcb 100644 --- a/src/test/java/org/duckstudy/model/lotto/LottoResultTest.java +++ b/src/test/java/org/duckstudy/domain/lotto/LottoResultTest.java @@ -1,10 +1,10 @@ -package org.duckstudy.model.lotto; +package org.duckstudy.domain.lotto; import static org.assertj.core.api.Assertions.assertThat; -import java.util.List; import java.util.Map; -import org.duckstudy.model.lotto.constant.WinningRank; +import java.util.Set; +import org.duckstudy.domain.lotto.constant.WinningRank; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,29 +12,29 @@ class LottoResultTest { @Test - @DisplayName("로또 결과를 생성한다") + @DisplayName("로또 당첨 결과를 생성한다") void createLottoResult() { - Lotto lotto = Lotto.from(List.of(1, 2, 3, 4, 5, 7)); - Lotto winningLotto = Lotto.from(List.of(1, 2, 3, 4, 5, 6)); + Lotto lotto = Lotto.from(Set.of(1, 2, 3, 4, 5, 7)); + Lotto winningLotto = Lotto.from(Set.of(1, 2, 3, 4, 5, 6)); LottoNumber bonusNumber = LottoNumber.valueOf(7); LottoResult lottoResult = LottoResult.createLottoResult(lotto, winningLotto, bonusNumber); assertThat(lottoResult.getResult()) - .containsEntry(WinningRank.SECOND.getKey(), 1); + .containsEntry(WinningRank.SECOND, 1); } @Test - @DisplayName("로또 결과를 병합한다") + @DisplayName("로또 당첨 결과를 병합한다") void mergeLottoResult() { - LottoResult lottoResult1 = new LottoResult(Map.of(3, 2)); - LottoResult lottoResult2 = new LottoResult(Map.of(3, 1)); + LottoResult lottoResult1 = new LottoResult(Map.of(WinningRank.FIFTH, 2)); + LottoResult lottoResult2 = new LottoResult(Map.of(WinningRank.FIFTH, 1)); LottoResult mergedLottoResult = lottoResult1.merge(lottoResult2); assertThat(mergedLottoResult.getResult()) - .containsEntry(3, 3); + .containsEntry(WinningRank.FIFTH, 3); } } diff --git a/src/test/java/org/duckstudy/model/lotto/LottoTest.java b/src/test/java/org/duckstudy/domain/lotto/LottoTest.java similarity index 71% rename from src/test/java/org/duckstudy/model/lotto/LottoTest.java rename to src/test/java/org/duckstudy/domain/lotto/LottoTest.java index ee561748..9c5c69ef 100644 --- a/src/test/java/org/duckstudy/model/lotto/LottoTest.java +++ b/src/test/java/org/duckstudy/domain/lotto/LottoTest.java @@ -1,10 +1,11 @@ -package org.duckstudy.model.lotto; +package org.duckstudy.domain.lotto; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.List; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -25,9 +26,9 @@ class LottoCreationFromLottoNumberTest { @DisplayName("로또를 생성한다") void createLottoSuccess() { - List lottoNumbers = Stream.of(1, 2, 3, 4, 5, 6) + Set lottoNumbers = Stream.of(1, 2, 3, 4, 5, 6) .map(LottoNumber::valueOf) - .toList(); + .collect(toSet()); assertThatCode(() -> new Lotto(lottoNumbers)) .doesNotThrowAnyException(); @@ -37,26 +38,26 @@ void createLottoSuccess() { @DisplayName("로또를 생성할 때 6개의 로또 번호가 아니면 예외를 발생한다") void createLottoFailWhenSizeIsNotSix() { - List lottoNumbers = List.of( + Set lottoNumbers = Set.of( LottoNumber.valueOf(1) ); assertThatThrownBy(() -> new Lotto(lottoNumbers)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("로또 번호는 6개여야 합니다."); + .hasMessage("로또 번호는 중복되지 않은 6개여야 합니다.\n"); } @Test @DisplayName("로또를 생성할 때 중복된 로또 번호가 있으면 예외를 발생한다") void createLottoFailWhenDuplicateNumberExists() { - List lottoNumbers = Stream.of(1, 1, 2, 3, 4, 5) + Set lottoNumbers = Stream.of(1, 1, 2, 3, 4, 5) .map(LottoNumber::valueOf) - .toList(); + .collect(toSet()); assertThatThrownBy(() -> new Lotto(lottoNumbers)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("로또 번호는 중복되지 않아야 합니다."); + .hasMessage("로또 번호는 중복되지 않은 6개여야 합니다.\n"); } } @@ -68,26 +69,17 @@ class LottoCreationFromIntegerTest { @DisplayName("로또를 생성한다") void createLottoSuccess() { - assertThatCode(() -> Lotto.from(List.of(1, 2, 3, 4, 5, 6))) + assertThatCode(() -> Lotto.from(Set.of(1, 2, 3, 4, 5, 6))) .doesNotThrowAnyException(); } @Test - @DisplayName("로또를 생성할 때 6개의 로또 번호가 아니면 예외를 발생한다") + @DisplayName("로또를 생성할 때 중복되지 않은 6개의 로또 번호가 아니면 예외를 발생한다") void createLottoFailWhenSizeIsNotSix() { - assertThatThrownBy(() -> Lotto.from(List.of(1))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("로또 번호는 6개여야 합니다."); - } - - @Test - @DisplayName("로또를 생성할 때 중복된 로또 번호가 있으면 예외를 발생한다") - void createLottoFailWhenDuplicateNumberExists() { - - assertThatThrownBy(() -> Lotto.from(List.of(1, 1, 3, 4, 5, 6))) + assertThatThrownBy(() -> Lotto.from(Set.of(1))) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("로또 번호는 중복되지 않아야 합니다."); + .hasMessage("로또 번호는 중복되지 않은 6개여야 합니다.\n"); } } @@ -113,8 +105,8 @@ class LottoComparisonTest { @DisplayName("일치하는 로또 번호의 개수를 반환한다") void countMatchingNumber() { - Lotto lotto = Lotto.from(List.of(1, 2, 3, 4, 5, 6)); - Lotto compareLotto = Lotto.from(List.of(1, 2, 3, 7, 8, 9)); + Lotto lotto = Lotto.from(Set.of(1, 2, 3, 4, 5, 6)); + Lotto compareLotto = Lotto.from(Set.of(1, 2, 3, 7, 8, 9)); assertThat(lotto.countMatchingNumber(compareLotto)).isEqualTo(3); } @@ -128,7 +120,7 @@ class LottoContainsTest { @DisplayName("로또 번호가 포함되어 있는지 확인한다") void containsNumber() { - Lotto lotto = Lotto.from(List.of(1, 2, 3, 4, 5, 6)); + Lotto lotto = Lotto.from(Set.of(1, 2, 3, 4, 5, 6)); LottoNumber lottoNumber = LottoNumber.valueOf(3); assertThat(lotto.containsNumber(lottoNumber)).isTrue(); diff --git a/src/test/java/org/duckstudy/domain/lotto/LottosTest.java b/src/test/java/org/duckstudy/domain/lotto/LottosTest.java new file mode 100644 index 00000000..f5681802 --- /dev/null +++ b/src/test/java/org/duckstudy/domain/lotto/LottosTest.java @@ -0,0 +1,87 @@ +package org.duckstudy.domain.lotto; + +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.duckstudy.domain.lotto.constant.WinningRank.FIRST; +import static org.duckstudy.domain.lotto.constant.WinningRank.NONE; +import static org.duckstudy.domain.lotto.constant.WinningRank.SECOND; + +import java.util.List; +import java.util.Set; +import org.duckstudy.domain.Price; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("로또 테스트") +class LottosTest { + + @Nested + @DisplayName("로또 묶음 생성 테스트") + class LottosCreationTest { + + @Test + @DisplayName("구매 개수를 입력하면 가격에 맞는 로또 묶음을 생성한다") + void createLottosWhenInputPrice() { + + Price purchasePrice = new Price(10000); + int lottoCount = purchasePrice.calculateLottoCount() + .getCount(); + + Lottos lottos = Lottos.generateLottos(lottoCount); + + assertThat(lottos.getLottos()).hasSize(10); + } + } + + @Nested + @DisplayName("로또 당첨 결과 테스트") + class LottosWinningTest { + + @Test + @DisplayName("당첨된 로또 번호와 보너스 번호를 입력하면 당첨된 로또 묶음의 결과를 계산한다") + void calculateWinningResultWhenInputWinningLotto() { + + Lotto winningLotto = Lotto.from(Set.of(1, 2, 3, 4, 5, 6)); + LottoNumber bonusNumber = LottoNumber.valueOf(7); + Lottos totalLottos = new Lottos(List.of( + winningLotto, + Lotto.from(Set.of(1, 2, 3, 4, 5, 7)), + Lotto.from(Set.of(8, 9, 10, 11, 12, 13)), + Lotto.from(Set.of(14, 15, 16, 17, 18, 19)), + Lotto.from(Set.of(20, 21, 22, 23, 24, 25)) + )); + + LottoResult lottoResult = totalLottos.accumulateLottoResult(winningLotto, bonusNumber); + + assertThat(lottoResult.getResult()) + .containsExactly( + entry(NONE, 3), + entry(SECOND, 1), + entry(FIRST, 1) + ); + } + } + + @Nested + @DisplayName("로또 묶음 병합 테스트") + class LottosMergeTest { + + @Test + @DisplayName("로또 묶음을 병합하면 하나의 로또 묶음으로 생성한다") + void mergeLottos() { + + Lottos lottos1 = new Lottos(List.of( + Lotto.from(Set.of(1, 2, 3, 4, 5, 6)) + )); + + Lottos lottos2 = new Lottos(List.of( + Lotto.from(Set.of(7, 8, 9, 10, 11, 12)) + )); + + Lottos mergedLottos = lottos1.merge(lottos2); + + assertThat(mergedLottos.getLottos()).hasSize(2); + } + } +} diff --git a/src/test/java/org/duckstudy/domain/lotto/constant/WinningRankTest.java b/src/test/java/org/duckstudy/domain/lotto/constant/WinningRankTest.java new file mode 100644 index 00000000..c14997d4 --- /dev/null +++ b/src/test/java/org/duckstudy/domain/lotto/constant/WinningRankTest.java @@ -0,0 +1,33 @@ +package org.duckstudy.domain.lotto.constant; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@DisplayName("당첨 순위 테스트") +class WinningRankTest { + + @DisplayName("당첨 횟수와 보너스 볼 여부를 입력하면 당첨 순위를 반환한다") + @ParameterizedTest + @MethodSource("generateMatchCountAndMatchBonus") + void findWinningRankSuccess(int matchCount, boolean matchBonus, + WinningRank expected) { + assertThat(WinningRank.findByMatchCountAndBonus(matchCount, matchBonus)) + .isEqualTo(expected); + } + + static Stream generateMatchCountAndMatchBonus() { + return Stream.of( + Arguments.of(0, false, WinningRank.NONE), + Arguments.of(3, false, WinningRank.FIFTH), + Arguments.of(4, false, WinningRank.FOURTH), + Arguments.of(5, false, WinningRank.THIRD), + Arguments.of(5, true, WinningRank.SECOND), + Arguments.of(6, false, WinningRank.FIRST) + ); + } +} diff --git a/src/test/java/org/duckstudy/model/lotto/LottosTest.java b/src/test/java/org/duckstudy/model/lotto/LottosTest.java deleted file mode 100644 index d71b4715..00000000 --- a/src/test/java/org/duckstudy/model/lotto/LottosTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.duckstudy.model.lotto; - -import static java.util.Map.entry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.duckstudy.model.lotto.constant.WinningRank.FIRST; -import static org.duckstudy.model.lotto.constant.WinningRank.NONE; -import static org.duckstudy.model.lotto.constant.WinningRank.SECOND; - -import java.util.List; -import org.duckstudy.model.Price; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -@DisplayName("로또 테스트") -class LottosTest { - - @Nested - @DisplayName("로또 묶음 생성 테스트") - class LottosCreationTest { - - @Test - @DisplayName("구매 가격을 입력하면 가격에 맞는 로또 묶음을 생성한다") - void createLottosWhenInputPrice() { - - Price purchasePrice = new Price(10000); - - Lottos lottos = Lottos.generateLottosByPrice(purchasePrice); - - assertThat(lottos.getLottos()).hasSize(10); - } - } - - @Nested - @DisplayName("로또 당첨 결과 테스트") - class LottosWinningTest { - - @Test - @DisplayName("당첨된 로또 번호와 보너스 번호를 입력하면 당첨된 로또 묶음의 결과를 계산한다") - void calculateWinningResultWhenInputWinningLotto() { - - Lotto winningLotto = Lotto.from(List.of(1, 2, 3, 4, 5, 6)); - LottoNumber bonusNumber = LottoNumber.valueOf(7); - Lottos totalLottos = new Lottos(List.of( - winningLotto, - Lotto.from(List.of(1, 2, 3, 4, 5, 7)), - Lotto.from(List.of(8, 9, 10, 11, 12, 13)), - Lotto.from(List.of(14, 15, 16, 17, 18, 19)), - Lotto.from(List.of(20, 21, 22, 23, 24, 25)) - )); - - LottoResult lottoResult = totalLottos.accumulateLottoResult(winningLotto, bonusNumber); - - assertThat(lottoResult.getResult()) - .containsExactly( - entry(NONE.getKey(), 3), - entry(SECOND.getKey(), 1), - entry(FIRST.getKey(), 1) - ); - } - } -}