diff --git a/README.md b/README.md index e7b064ab49..d655ef9d20 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ - 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발급한다. - 로또 1장의 가격은 1,000원이다. - 로또 번호는 1~45 사이의 숫자 6개로 구성된다. +- 수동으로 구매할 로또 수를 입력받는다. +- 수동으로 구매할 로또 번호를 입력받는다. - 당첨 번호 6개를 입력받는다. - 보너스 번호 1개를 입력받는다. - 발행한 로또와 당첨 번호를 비교하여 당첨 통계를 계산한다. @@ -51,7 +53,7 @@ ### 로또 기계 -- 구입 금액을 기준으로 발행할 로또 장수를 계산한다. +- 자동 발행 로또 수를 계산한다. - 각 로또와 당첨 번호를 비교하여 당첨 통계를 계산한다. - 당첨 금액 및 수익률을 계산한다. @@ -59,6 +61,8 @@ - 구입 금액을 입력받는다. - 1,000원 단위의 양수 +- 수동으로 구매할 로또 수를 입력받는다. +- 수동으로 구매할 번호를 입력받는다. - 당첨 번호 6개와 보너스 번호 1개를 입력받는다. - 1~45 사이의 숫자, 중복 불가 diff --git a/src/main/java/lotto/AutoLottosGenerator.java b/src/main/java/lotto/AutoLottosGenerator.java new file mode 100644 index 0000000000..b120645e71 --- /dev/null +++ b/src/main/java/lotto/AutoLottosGenerator.java @@ -0,0 +1,23 @@ +package lotto; + +import java.util.ArrayList; +import java.util.List; + +public class AutoLottosGenerator implements LottosGenerator { + + private final int count; + + public AutoLottosGenerator(int count) { + this.count = count; + } + + @Override + public Lottos generate() { + List lottos = new ArrayList<>(); + for (int i = 0; i < count; i++) { + lottos.add(LottoMachine.randomLotto()); + } + return new Lottos(lottos); + } + +} diff --git a/src/main/java/lotto/InputRetry.java b/src/main/java/lotto/InputRetry.java new file mode 100644 index 0000000000..f37f73d483 --- /dev/null +++ b/src/main/java/lotto/InputRetry.java @@ -0,0 +1,19 @@ +package lotto; + +import java.util.function.Supplier; + +public class InputRetry { + + private InputRetry() { + } + + public static T retry(Supplier action) { + while (true) { + try { + return action.get(); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + } +} diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 30ef703d04..e64c674eb7 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -14,6 +14,8 @@ public class Lotto { public static final int LOTTO_NUMBER_COUNT = 6; private static final String ERROR_INVALID_COUNT = "로또 번호는 6개여야 한다"; + private static final String ERROR_DUPLICATE_NUMBER = "로또 번호에 중복이 있을 수 없습니다."; + private static final String ERROR_INVALID_FORMAT = "숫자 형식이 올바르지 않습니다: "; private final Set numbers; @@ -34,6 +36,14 @@ public static Lotto fromIntegers(List numbers) { return new Lotto(toLottoNumbers(numbers)); } + public List numbers() { + return Collections.unmodifiableList(new ArrayList<>(numbers)); + } + + public boolean contains(LottoNumber number) { + return numbers.contains(number); + } + private static List toLottoNumbers(List numbers) { return numbers.stream() .map(LottoNumber::of) @@ -53,7 +63,7 @@ private static int parseIntOrThrow(String s) { try { return Integer.parseInt(s.trim()); } catch (NumberFormatException e) { - throw new IllegalArgumentException("숫자 형식이 올바르지 않습니다: " + s); + throw new IllegalArgumentException(ERROR_INVALID_FORMAT + s); } } @@ -61,14 +71,11 @@ private static void validateNumbersCount(List numbers) { if (numbers.size() != LOTTO_NUMBER_COUNT) { throw new IllegalArgumentException(ERROR_INVALID_COUNT); } - } - - public List numbers() { - return Collections.unmodifiableList(new ArrayList<>(numbers)); - } - public boolean contains(LottoNumber number) { - return numbers.contains(number); + long distinctCount = numbers.stream().distinct().count(); + if (distinctCount != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException(ERROR_DUPLICATE_NUMBER); + } } @Override diff --git a/src/main/java/lotto/LottoApplication.java b/src/main/java/lotto/LottoApplication.java index 9ecfe85d1b..768beddf8a 100644 --- a/src/main/java/lotto/LottoApplication.java +++ b/src/main/java/lotto/LottoApplication.java @@ -1,5 +1,6 @@ package lotto; +import java.util.List; import lotto.view.InputView; import lotto.view.ResultView; @@ -7,19 +8,47 @@ public class LottoApplication { public static void main(String[] args) { - int input = InputView.readPurchaseAmount(); - PurchaseAmount amount = new PurchaseAmount(input); + PurchaseAmount amount = createPurchaseAmount(); - Lottos lottos = LottoMachine.randomLottos(amount.ticketCount()); - ResultView.printPurchasedLottos(lottos); + ManualLottoCount manualCount = createManualLottoCount(amount); + Lottos manualLottos = createManualLottos(manualCount); + Lottos autoLottos = new AutoLottosGenerator(amount.autoCount(manualCount)).generate(); - Lotto winningNumbers = new Lotto(InputView.readWinningNumbers()); - LottoNumber bonusNumber = LottoNumber.of(InputView.readBonusNumber()); + Lottos purchased = manualLottos.merge(autoLottos); + ResultView.printPurchasedLottos(purchased, manualCount); - LottoMatchResult matchResult = lottos.matchResult( - new WinningNumbers(winningNumbers, bonusNumber)); + WinningNumbers winningNumbers = createWinningNumbers(); + LottoMatchResult matchResult = purchased.matchResult(winningNumbers); ProfitRate profitRate = new ProfitRate(matchResult.totalPrize(), amount); ResultView.printLottoResult(matchResult, profitRate); } + + private static PurchaseAmount createPurchaseAmount() { + return InputRetry.retry(() -> + new PurchaseAmount(InputView.readPurchaseAmount()) + ); + } + + private static ManualLottoCount createManualLottoCount(PurchaseAmount amount) { + return InputRetry.retry(() -> + new ManualLottoCount(InputView.readManualLottoCount(), amount) + ); + } + + private static Lottos createManualLottos(ManualLottoCount manualCount) { + return InputRetry.retry(() -> { + List manualNumbers = InputView.readManualLottos(manualCount.count()); + LottosGenerator generator = new ManualLottosGenerator(manualNumbers); + return generator.generate(); + }); + } + + private static WinningNumbers createWinningNumbers() { + return InputRetry.retry(() -> { + Lotto winning = new Lotto(InputView.readWinningNumbers()); + LottoNumber bonus = LottoNumber.of(InputView.readBonusNumber()); + return new WinningNumbers(winning, bonus); + }); + } } diff --git a/src/main/java/lotto/LottoMachine.java b/src/main/java/lotto/LottoMachine.java index bb05463e96..f4f39da4a5 100644 --- a/src/main/java/lotto/LottoMachine.java +++ b/src/main/java/lotto/LottoMachine.java @@ -6,15 +6,7 @@ public class LottoMachine { - public static Lottos randomLottos(int count) { - List lottos = new ArrayList<>(); - for (int i = 0; i < count; i++) { - lottos.add(randomLotto()); - } - return new Lottos(lottos); - } - - private static Lotto randomLotto() { + public static Lotto randomLotto() { List numbers = new ArrayList<>(); for (int i = Lotto.MIN_NUMBER; i <= Lotto.MAX_NUMBER; i++) { numbers.add(i); diff --git a/src/main/java/lotto/LottoNumber.java b/src/main/java/lotto/LottoNumber.java index 2b76090a55..e949ff0fca 100644 --- a/src/main/java/lotto/LottoNumber.java +++ b/src/main/java/lotto/LottoNumber.java @@ -1,5 +1,7 @@ package lotto; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; public class LottoNumber implements Comparable { @@ -9,11 +11,11 @@ public class LottoNumber implements Comparable { private static final String ERROR_OUT_OF_RANGE = "로또 번호는 1~45 범위의 숫자여야 한다"; - private static final LottoNumber[] CACHE = new LottoNumber[MAX_NUMBER + 1]; + private static final Map CACHE = new HashMap<>(); static { for (int i = MIN_NUMBER; i <= MAX_NUMBER; i++) { - CACHE[i] = new LottoNumber(i); + CACHE.put(i, new LottoNumber(i)); } } @@ -26,11 +28,7 @@ private LottoNumber(int number) { public static LottoNumber of(int number) { validateRange(number); - return CACHE[number]; - } - - public static LottoNumber of(String number) { - return of(Integer.parseInt(number)); + return CACHE.get(number); } public int number() { diff --git a/src/main/java/lotto/LottoRank.java b/src/main/java/lotto/LottoRank.java index 7211bdc5b8..3137bd8b9d 100644 --- a/src/main/java/lotto/LottoRank.java +++ b/src/main/java/lotto/LottoRank.java @@ -39,4 +39,11 @@ public static LottoRank of(int matchCount, boolean hasBonus) { .orElse(MISS); } + public boolean isMiss() { + return this == MISS; + } + + public boolean isSecond() { + return this == SECOND; + } } diff --git a/src/main/java/lotto/Lottos.java b/src/main/java/lotto/Lottos.java index f24a019faf..05dc0b68d2 100644 --- a/src/main/java/lotto/Lottos.java +++ b/src/main/java/lotto/Lottos.java @@ -11,6 +11,16 @@ public Lottos(List lottos) { this.lottos = lottos; } + public static Lottos manualLottos(List manualNumbers) { + return new Lottos(manualNumbers.stream().map(Lotto::new).toList()); + } + + public Lottos merge(Lottos other) { + List mergedLottos = new ArrayList<>(this.lottos); + mergedLottos.addAll(other.lottos); + return new Lottos(mergedLottos); + } + public int count() { return lottos.size(); } diff --git a/src/main/java/lotto/LottosGenerator.java b/src/main/java/lotto/LottosGenerator.java new file mode 100644 index 0000000000..88d97a6b82 --- /dev/null +++ b/src/main/java/lotto/LottosGenerator.java @@ -0,0 +1,6 @@ +package lotto; + +public interface LottosGenerator { + + Lottos generate(); +} diff --git a/src/main/java/lotto/ManualLottoCount.java b/src/main/java/lotto/ManualLottoCount.java new file mode 100644 index 0000000000..cba04f1909 --- /dev/null +++ b/src/main/java/lotto/ManualLottoCount.java @@ -0,0 +1,35 @@ +package lotto; + +public class ManualLottoCount { + + private static final String ERROR_NEGATIVE_COUNT = "수동 로또 수량은 0 이상이어야 합니다."; + private static final String ERROR_EXCEED_PURCHASE = "수동 장수는 구입 가능한 수량을 초과할 수 없습니다."; + private final int count; + + public ManualLottoCount(int count, int purchaseAmount) { + this(count, new PurchaseAmount(purchaseAmount)); + } + + public ManualLottoCount(int count, PurchaseAmount amount) { + validateNonNegativeCount(count); + validateNotExceedPurchaseAmount(count, amount); + this.count = count; + } + + public int count() { + return count; + } + + private static void validateNonNegativeCount(int count) { + if (count < 0) { + throw new IllegalArgumentException(ERROR_NEGATIVE_COUNT); + } + } + + private static void validateNotExceedPurchaseAmount(int count, PurchaseAmount amount) { + if (amount.ticketCount() < count) { + throw new IllegalArgumentException(ERROR_EXCEED_PURCHASE); + } + } + +} diff --git a/src/main/java/lotto/ManualLottosGenerator.java b/src/main/java/lotto/ManualLottosGenerator.java new file mode 100644 index 0000000000..859c8f6edd --- /dev/null +++ b/src/main/java/lotto/ManualLottosGenerator.java @@ -0,0 +1,17 @@ +package lotto; + +import java.util.List; + +public class ManualLottosGenerator implements LottosGenerator { + + private final List manualNumbers; + + public ManualLottosGenerator(List manualNumbers) { + this.manualNumbers = manualNumbers; + } + + @Override + public Lottos generate() { + return new Lottos(manualNumbers.stream().map(Lotto::new).toList()); + } +} diff --git a/src/main/java/lotto/PurchaseAmount.java b/src/main/java/lotto/PurchaseAmount.java index 5ff2076b5a..1ff67032f7 100644 --- a/src/main/java/lotto/PurchaseAmount.java +++ b/src/main/java/lotto/PurchaseAmount.java @@ -21,10 +21,13 @@ public int ticketCount() { return value / PRICE_PER_LOTTO; } + public int autoCount(ManualLottoCount manualCount) { + return ticketCount() - manualCount.count(); + } + private static void validate(int amount) { if (amount < PRICE_PER_LOTTO || amount % PRICE_PER_LOTTO != 0) { throw new IllegalArgumentException(ERROR_INVALID_PURCHASE_AMOUNT); } } - } diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java index 0506914e78..eaeb22bbf3 100644 --- a/src/main/java/lotto/view/InputView.java +++ b/src/main/java/lotto/view/InputView.java @@ -1,6 +1,10 @@ package lotto.view; +import java.util.ArrayList; +import java.util.InputMismatchException; +import java.util.List; import java.util.Scanner; +import lotto.InputRetry; public class InputView { @@ -8,7 +12,22 @@ public class InputView { public static int readPurchaseAmount() { System.out.println("구입금액을 입력해 주세요."); - return parseIntOrThrow(SCANNER.nextLine()); + return readInt(); + } + + public static int readManualLottoCount() { + System.out.println("수동으로 구매할 로또 수를 입력해 주세요."); + return readInt(); + } + + public static List readManualLottos(int count) { + System.out.println("수동으로 구매할 번호를 입력해 주세요."); + List input = new ArrayList<>(); + while (count-- > 0) { + input.add(SCANNER.nextLine().trim()); + } + System.out.println(); + return input; } public static String readWinningNumbers() { @@ -16,17 +35,23 @@ public static String readWinningNumbers() { return SCANNER.nextLine(); } - public static String readBonusNumber() { + public static int readBonusNumber() { System.out.println("보너스 볼을 입력해 주세요."); - return SCANNER.nextLine(); + return readInt(); } - private static int parseIntOrThrow(String input) { - try { - return Integer.parseInt(input.trim()); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("숫자만 입력 가능합니다: " + input); - } + private static int readInt() { + return InputRetry.retry(() -> { + try { + int value = SCANNER.nextInt(); + SCANNER.nextLine(); + System.out.println(); + return value; + } catch (InputMismatchException e) { + SCANNER.nextLine(); + throw new IllegalArgumentException("숫자를 올바르게 입력하세요."); + } + }); } } diff --git a/src/main/java/lotto/view/ResultView.java b/src/main/java/lotto/view/ResultView.java index 5406f88837..74c588c9e3 100644 --- a/src/main/java/lotto/view/ResultView.java +++ b/src/main/java/lotto/view/ResultView.java @@ -3,12 +3,14 @@ import lotto.LottoMatchResult; import lotto.LottoRank; import lotto.Lottos; +import lotto.ManualLottoCount; import lotto.ProfitRate; public class ResultView { - public static void printPurchasedLottos(Lottos lottos) { - System.out.printf("%d개를 구매했습니다.%n", lottos.count()); + public static void printPurchasedLottos(Lottos lottos, ManualLottoCount manualCount) { + System.out.printf("수동으로 %d장, 자동으로 %d개를 구매했습니다.%n", manualCount.count(), + lottos.count() - manualCount.count()); for (String line : lottos.toDisplayStrings()) { System.out.println(line); } @@ -16,7 +18,6 @@ public static void printPurchasedLottos(Lottos lottos) { } public static void printLottoResult(LottoMatchResult matchResult, ProfitRate profitRate) { - System.out.println(); System.out.println("당첨 통계"); System.out.println("---------"); @@ -35,12 +36,12 @@ public static void printLottoResult(LottoMatchResult matchResult, ProfitRate pro } private static void printRank(LottoMatchResult matchResult, LottoRank rank) { - if (rank == LottoRank.MISS) { + if (rank.isMiss()) { return; } int count = matchResult.countMatches(rank); int prize = rank.prize(); - if (rank == LottoRank.SECOND) { + if (rank.isSecond()) { System.out.printf("5개 일치, 보너스 볼 일치 (%d원) - %d개%n", prize, count); return; } diff --git a/src/test/java/lotto/AutoLottosGeneratorTest.java b/src/test/java/lotto/AutoLottosGeneratorTest.java new file mode 100644 index 0000000000..e7859a59ab --- /dev/null +++ b/src/test/java/lotto/AutoLottosGeneratorTest.java @@ -0,0 +1,14 @@ +package lotto; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class AutoLottosGeneratorTest { + + @Test + void generateAutoLottos() { + assertThat(new AutoLottosGenerator(5).generate().count()).isEqualTo(5); + } + +} \ No newline at end of file diff --git a/src/test/java/lotto/LottoMachineTest.java b/src/test/java/lotto/LottoMachineTest.java index 31755e28d3..9f827491d0 100644 --- a/src/test/java/lotto/LottoMachineTest.java +++ b/src/test/java/lotto/LottoMachineTest.java @@ -7,10 +7,9 @@ public class LottoMachineTest { - @DisplayName("구입한 수만큼 로또를 발행한다") + @DisplayName("자동 로또를 1개 발행한다") @Test - void randomLottos() { - Lottos lottos = LottoMachine.randomLottos(10); - assertThat(lottos.count()).isEqualTo(10); + void randomLotto() { + assertThat(LottoMachine.randomLotto().numbers()).hasSize(6); } } diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index bb5c9abff8..a594a5463b 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -39,6 +39,13 @@ void sortLottoNumbers() { assertThat(new Lotto("1,2,3,4,8,5")).isEqualTo(new Lotto("1,2,3,4,5,8")); } + @DisplayName("로또 번호는 중복 시 예외가 발생한다") + @Test + void duplicateLottoNumbers() { + assertThatIllegalArgumentException().isThrownBy(() -> new Lotto("1,2,3,4,8,8")) + .withMessageContaining("중복"); + } + static Stream> invalidLottoSizes() { return Stream.of( List.of(), diff --git a/src/test/java/lotto/LottosTest.java b/src/test/java/lotto/LottosTest.java index b2d33bedfb..60e583e31e 100644 --- a/src/test/java/lotto/LottosTest.java +++ b/src/test/java/lotto/LottosTest.java @@ -4,6 +4,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -29,4 +30,21 @@ void matchResult(int matchCount, int expectedCount) { assertThat(result.countMatches(rank)).isEqualTo(expectedCount); } + @DisplayName("문자열 리스트를 받으면 로또 리스트를 생성한다") + @Test + void createManualLottos() { + List manual = List.of("1,2,3,4,5,6", "7,8,9,10,11,12"); + Lottos lottos = Lottos.manualLottos(manual); + assertThat(lottos.count()).isEqualTo(2); + } + + @DisplayName("수동, 자동 Lottos를 합쳐 하나로 반환한다") + @Test + void mergeLottos() { + Lottos manualLottos = Lottos.manualLottos(List.of("1,2,3,4,5,6", "2,3,4,5,6,7")); + Lottos autoLottos = Lottos.manualLottos(List.of("1,2,3,4,5,6", "3,4,5,6,7,8")); + Lottos mergedLottos = manualLottos.merge(autoLottos); + assertThat(mergedLottos.count()).isEqualTo(4); + } + } diff --git a/src/test/java/lotto/ManualLottoCountTest.java b/src/test/java/lotto/ManualLottoCountTest.java new file mode 100644 index 0000000000..039b92a3fe --- /dev/null +++ b/src/test/java/lotto/ManualLottoCountTest.java @@ -0,0 +1,23 @@ +package lotto; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ManualLottoCountTest { + + @DisplayName("수동 장수가 구입 가능한 수량을 초과히면 예외를 발생한다") + @Test + void createManualLottoCount() { + assertThatIllegalArgumentException().isThrownBy(() -> new ManualLottoCount(10, 1000)) + .withMessageContaining("구입 가능한 수량을 초과"); + } + + @DisplayName("수동 장수가 음수인 경우 예외를 발생한다") + @Test + void negativeManualCount() { + assertThatIllegalArgumentException().isThrownBy(() -> new ManualLottoCount(-1, 1000)) + .withMessageContaining("0 이상"); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/ManualLottosGeneratorTest.java b/src/test/java/lotto/ManualLottosGeneratorTest.java new file mode 100644 index 0000000000..e8f41dfe58 --- /dev/null +++ b/src/test/java/lotto/ManualLottosGeneratorTest.java @@ -0,0 +1,15 @@ +package lotto; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ManualLottosGeneratorTest { + + @Test + void generateManualLottos() { + List inputs = List.of("1,2,3,4,5,6", "7,8,9,10,11,12"); + assertThat(new ManualLottosGenerator(inputs).generate().count()).isEqualTo(2); + } +} \ No newline at end of file