diff --git a/README.md b/README.md index 8a4f22ed0..a1057a970 100644 --- a/README.md +++ b/README.md @@ -1 +1,82 @@ # java-planetlotto-precourse + +# 개인 도전 과제 + +- 기능 확장 +- 날짜별 판매된 로또의 개수를 집계하는 기능을 추가했습니다. +- 시뮬레이션임을 고려, 로또가 판매되는 날짜는 랜덤하게 설정되도록 하였습니다. +- 날짜 집계는 출력 마지막 부분에서 출력됩니다. + +# 개요 + +# 구현 기능 목록 + +## 입력 + +### 구입 금앱 입력 + +- [x] 안내 메세지 출력 후, 사용자 입력 받음 +- [x] 입력 유효성 검사 + - [x] 입력이 숫자가 아닌경우 예외처리 + - [x] 입력이 양의 정수가 아닌 경우 예외처리 + - [x] !금액이 500원으로 나누어 떨어지지 않으면 예외처리 + +### 당첨 번호 입력 + +- [x] 안내 메세지 출력 후, 사용자 입력 받음 +- [x] 입력 유효성 검사 + - [x] 1~30범위가 아닌 수가 있는 경우 예외처리 + - [x] 중복된 수가 있는 경우 예외처리 + - [x] 5개의 숫자가 아닌 경우 예외처리 + +### 보너스 번호 입력 + +- [x] 안내 메세지 출력 후, 사용자 입력 받음 +- [x] 입력 유효성 검사 + - [x] 1~30범위가 아닌 수가 있는 경우 예외처리 + - [x] 기본번호와 중복되는 경우 예외처리 + +## 출력 + +### 발행된 로또 결과 출력 + +- [x] 발행된 로또들을 형식에 맞춰 출력 +- + +### 추첨 결과 출력 + +- [x] 당첨 결과를 통해 당첨 금액 집계 +- [x] 수익률 산출 +- [x] 정해진 형식에 맞추어 당첨 통계 출력 + - [x] 당첨 통계 메세지 출력 + - [x] 당첨 내역 출력 + - [x] 총 수익률 출력 + +### 날짜별 판매 통계 출력 + +- [x] 요일별 판매된 로또의 개수를 출력 + +## 비즈니스 로직 + +### 로또 발행 + +- [x] 발행할 로또 수량 계산 +- [x] 수량만큼 로또 발행 +- [x] 1~30 범위의 중복되지 않는 랜덤한 5개의 수 추출 + +### 로또 추첨 + +- [x] 발행 로또에 대해 당첨 검사 + - [x] 각 로또 번호가 당첨 번호와 일치하는지 비교 + - [x] 각 로또 번호가 보너스 번호와 일치하는지 비교 + - [x] 당첨 번호 일치 갯수와 보너스 번호 일치 여부를 통해 결과 산출 +- [x] 발행 수량만큼 반복 검사 + + +- [x] 당첨된 로또들의 당첨 금액을 집계 + +### 날짜 집계 기능 추가 + +- [x] 로또가 발행된 날짜 기록 +- [x] 당첨된 로또들의 발행 날짜 기록 + diff --git a/src/main/java/planetlotto/Application.java b/src/main/java/planetlotto/Application.java index 27d0a8f96..8fc09c65b 100644 --- a/src/main/java/planetlotto/Application.java +++ b/src/main/java/planetlotto/Application.java @@ -1,7 +1,13 @@ package planetlotto; +import planetlotto.controller.LottoController; +import planetlotto.service.IssueService; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + IssueService issueService = new IssueService(); + LottoController controller = new LottoController(issueService); + + controller.run(); } } diff --git a/src/main/java/planetlotto/controller/LottoController.java b/src/main/java/planetlotto/controller/LottoController.java new file mode 100644 index 000000000..aa672e5f4 --- /dev/null +++ b/src/main/java/planetlotto/controller/LottoController.java @@ -0,0 +1,81 @@ +package planetlotto.controller; + +import java.util.List; +import java.util.function.Supplier; +import planetlotto.domain.Amount; +import planetlotto.domain.DrawResult; +import planetlotto.domain.Lotto; +import planetlotto.domain.Lottos; +import planetlotto.domain.WinningNumbers; +import planetlotto.service.IssueService; +import planetlotto.view.InputView; +import planetlotto.view.OutputView; + +public class LottoController { + private static final String INVALID_INPUT_ERROR = "유효하지 않은 입력 값입니다. 다시 입력해 주세요."; + private final IssueService issueService; + + public LottoController(IssueService issueService) { + this.issueService = issueService; + } + + public void run() { + Amount amount = readAmount(); + Lottos purchasedLottos = issueService.issueByAmount(amount); + + OutputView.printPurchasedLottos(purchasedLottos.getLottosList()); + + WinningNumbers winningNumbers = readWinningNumbers(); + + DrawResult drawResult = purchasedLottos.drawBy(winningNumbers); + + OutputView.printResult(drawResult.get()); + OutputView.printDateResult(drawResult.getDayOfWeekMap()); + } + + private Amount readAmount() { + return retry(() -> { + int amountValue = InputView.askAmount(); + return Amount.of(amountValue); + }); + } + + private WinningNumbers readWinningNumbers() { + Lotto baseNumbers = readWinningBase(); + + return retry(() -> { + int bonusNumber = InputView.askBonusNumber(); + + return WinningNumbers.of(baseNumbers, bonusNumber); + }); + } + + private Lotto readWinningBase() { + return retry(() -> { + List winningBase = InputView.askWinningLotto(); + + return Lotto.of(winningBase); + }); + } + + + private T retry(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException exception) { + printExceptionMessage(exception); + } + } + } + + private void printExceptionMessage(IllegalArgumentException exception) { + String message = exception.getMessage(); + + if ((message == null) || message.isBlank()) { + message = INVALID_INPUT_ERROR; + } + + OutputView.printErrorMessage(message); + } +} diff --git a/src/main/java/planetlotto/domain/Amount.java b/src/main/java/planetlotto/domain/Amount.java new file mode 100644 index 000000000..5e507ed46 --- /dev/null +++ b/src/main/java/planetlotto/domain/Amount.java @@ -0,0 +1,33 @@ +package planetlotto.domain; + +public class Amount { + private final int amount; + + private Amount(int amount) { + validateAmount(amount); + this.amount = amount; + } + + public static Amount of(int amount) { + return new Amount(amount); + } + + public int getAmount() { + return amount; + } + + private void validateAmount(int amount) { + if (amount <= 0) { + throw new IllegalArgumentException(); + } + + if (amount % 500 != 0) { + throw new IllegalArgumentException(); + } + } + + public int availableQuantityByPolicy(int i) { + return amount / LottoPolicy.TICKET_PRICE.get(); + + } +} diff --git a/src/main/java/planetlotto/domain/DrawResult.java b/src/main/java/planetlotto/domain/DrawResult.java new file mode 100644 index 000000000..e481ef121 --- /dev/null +++ b/src/main/java/planetlotto/domain/DrawResult.java @@ -0,0 +1,53 @@ +package planetlotto.domain; + +import java.time.DayOfWeek; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +public class DrawResult { + private final Map drawResultMap; + private final Map issueLottoDayOfWeekMap; + + private DrawResult(Map drawResultMap) { + Map drawDayResultMap = new HashMap<>(); + + for (WinningRules rule : WinningRules.values()) { + drawDayResultMap.put(rule.ordinal(), 0); + } + this.drawResultMap = drawDayResultMap; + + Map issueLottoDayOfWeekMap = new EnumMap<>(DayOfWeek.class); + + for (DayOfWeek dayOfWeek : DayOfWeek.values()) { + issueLottoDayOfWeekMap.put(dayOfWeek, 0); + } + this.issueLottoDayOfWeekMap = issueLottoDayOfWeekMap; + } + + public static DrawResult create() { + Map drawResultMap = new HashMap<>(); + for (WinningRules rule : WinningRules.values()) { + drawResultMap.put(rule.ordinal(), 0); + } + return new DrawResult(drawResultMap); + } + + public void addWinner(WinningRules winningRule, DayOfWeek dayOfWeek) { + drawResultMap.merge(winningRule.ordinal(), 1, Integer::sum); + issueLottoDayOfWeekMap.merge(dayOfWeek, 1, Integer::sum); + } + + public int countOf(WinningRules winningRules) { + return drawResultMap.get(winningRules.ordinal()); + } + + public Map get() { + return Collections.unmodifiableMap(new HashMap<>(this.drawResultMap)); + } + + public Map getDayOfWeekMap() { + return Collections.unmodifiableMap(issueLottoDayOfWeekMap); + } +} diff --git a/src/main/java/planetlotto/domain/Lotto.java b/src/main/java/planetlotto/domain/Lotto.java new file mode 100644 index 000000000..a59a74049 --- /dev/null +++ b/src/main/java/planetlotto/domain/Lotto.java @@ -0,0 +1,93 @@ +package planetlotto.domain; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Lotto { + private final DayOfWeek dayOfWeek; + private final List numbers; + + public Lotto(List numbers, DayOfWeek dayOfWeek) { + validate(numbers); + + List newNumbers = new ArrayList<>(numbers); + newNumbers.sort(Comparator.naturalOrder()); + this.numbers = newNumbers; + this.dayOfWeek = dayOfWeek; + } + + public static Lotto of(List numbers) { + int dayOfWeekIndex = (int) (Math.random() * 7) + 1; + + return new Lotto(numbers, DayOfWeek.of(dayOfWeekIndex)); + } + + public static Lotto ofToday(List numbers) { + + LocalDate today = LocalDate.now(); + return new Lotto(numbers, today.getDayOfWeek()); + } + + private static void validate(List numbers) { + validateNumberCount(numbers); + validateNumberRange(numbers); + validateDuplicateNumber(numbers); + } + + private static void validateNumberRange(List numbers) { + if (hasOutOfRange(numbers)) { + throw new IllegalArgumentException(); + } + } + + private static boolean hasOutOfRange(List numbers) { + return numbers.stream() + .anyMatch(Lotto::isOutOfRange); + } + + private static boolean isOutOfRange(Integer number) { + return number < LottoPolicy.MIN_NUMBER.get() || LottoPolicy.MAX_NUMBER.get() < number; + } + + private static void validateDuplicateNumber(List numbers) { + if (hasDuplicates(numbers)) { + throw new IllegalArgumentException(); + } + } + + private static boolean hasDuplicates(List numbers) { + Set uniqueNumbers = new HashSet<>(numbers); + return uniqueNumbers.size() != numbers.size(); + } + + private static void validateNumberCount(List numbers) { + if (numbers.size() != LottoPolicy.SIZE.get()) { + throw new IllegalArgumentException(); + } + } + + public boolean contains(int bonus) { + return this.numbers.contains(bonus); + } + + public int countMatchesWith(Lotto other) { + long matchCount = this.numbers.stream() + .filter(other::contains) + .count(); + return (int) matchCount; + } + + public List getList() { + return List.copyOf(this.numbers); + } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } +} + diff --git a/src/main/java/planetlotto/domain/LottoPolicy.java b/src/main/java/planetlotto/domain/LottoPolicy.java new file mode 100644 index 000000000..43a50f428 --- /dev/null +++ b/src/main/java/planetlotto/domain/LottoPolicy.java @@ -0,0 +1,18 @@ +package planetlotto.domain; + +public enum LottoPolicy { + SIZE(5), + MIN_NUMBER(1), + MAX_NUMBER(30), + TICKET_PRICE(500); + + private final int value; + + LottoPolicy(int value) { + this.value = value; + } + + public int get() { + return value; + } +} diff --git a/src/main/java/planetlotto/domain/Lottos.java b/src/main/java/planetlotto/domain/Lottos.java new file mode 100644 index 000000000..ec83aa7fc --- /dev/null +++ b/src/main/java/planetlotto/domain/Lottos.java @@ -0,0 +1,29 @@ +package planetlotto.domain; + +import java.util.List; + +public class Lottos { + private final List lottos; + + public Lottos(List lottos) { + this.lottos = lottos; + } + + + public DrawResult drawBy(WinningNumbers winningNumbers) { + DrawResult drawResult = DrawResult.create(); + + for (Lotto lotto : lottos) { + WinningRules winningRules = winningNumbers.checkWin(lotto); + drawResult.addWinner(winningRules, lotto.getDayOfWeek()); + } + + return drawResult; + } + + public List> getLottosList() { + return lottos.stream() + .map(Lotto::getList) + .toList(); + } +} diff --git a/src/main/java/planetlotto/domain/WinningNumbers.java b/src/main/java/planetlotto/domain/WinningNumbers.java new file mode 100644 index 000000000..16a6afb27 --- /dev/null +++ b/src/main/java/planetlotto/domain/WinningNumbers.java @@ -0,0 +1,40 @@ +package planetlotto.domain; + +public class WinningNumbers { + private static final String ERROR_OUT_OF_RANGE_BONUS_NUMBER = "보너스 번호는 1부터 30 사이의 숫자여야 합니다."; + private static final String ERROR_DUPLICATES = "보너스 번호는 기본 당첨 번호와 중복되지 않게 해주세요."; + private final Lotto baseNumbers; + private final int bonusNumber; + + public WinningNumbers(Lotto base, int bonus) { + this.baseNumbers = base; + this.bonusNumber = bonus; + } + + + public static WinningNumbers of(Lotto base, int bonus) { + validateBonusNumber(bonus); + validateDuplicates(base, bonus); + return new WinningNumbers(base, bonus); + } + + + public static void validateBonusNumber(int bonusNumber) { + if (bonusNumber < LottoPolicy.MIN_NUMBER.get() || LottoPolicy.MAX_NUMBER.get() < bonusNumber) { + throw new IllegalArgumentException(ERROR_OUT_OF_RANGE_BONUS_NUMBER); + } + } + + private static void validateDuplicates(Lotto base, int bonus) { + if (base.contains(bonus)) { + throw new IllegalArgumentException(ERROR_DUPLICATES); + } + } + + public WinningRules checkWin(Lotto issuedLotto) { + int matchCount = baseNumbers.countMatchesWith(issuedLotto); + boolean bonusMatch = issuedLotto.contains(bonusNumber); + + return WinningRules.of(matchCount, bonusMatch); + } +} diff --git a/src/main/java/planetlotto/domain/WinningRules.java b/src/main/java/planetlotto/domain/WinningRules.java new file mode 100644 index 000000000..f8990e17c --- /dev/null +++ b/src/main/java/planetlotto/domain/WinningRules.java @@ -0,0 +1,45 @@ +package planetlotto.domain; + +import java.util.Arrays; +import java.util.function.BiPredicate; + +public enum WinningRules { + /* + 1등: 5개 번호 일치 / 100,000,000원 +2등: 4개 번호 + 보너스 번호 일치 / 10,000,000원 +3등: 4개 번호 일치 / 1,500,000원 +4등: 3개 번호 일치 + 보너스 번호 일치 / 500,000원 +5등: 2개 번호 일치 + 보너스 번호 일치 / 5,000원 + */ + NO_WIN(0, 0, (count, bonus) -> count == 0), + FIRST(5, 100_000_000, (count, bonus) -> count == 5), + SECOND(4, 10_000_000, (count, bonus) -> count == 4 && bonus), + THIRD(4, 1_500_000, (count, bonus) -> count == 4 && !bonus), + FOURTH(3, 500_000, (count, bonus) -> count == 3 && bonus), + FIFTH(2, 5_000, (count, bonus) -> count == 2 && bonus); + + private final int matchCount; + private final int prize; + private final BiPredicate condition; + + WinningRules(int matchCount, int prize, BiPredicate condition) { + this.matchCount = matchCount; + this.prize = prize; + this.condition = condition; + } + + public static WinningRules of(int matchCount, boolean bonusMatch) { + return Arrays.stream(values()) + .filter(rule -> rule.condition.test(matchCount, bonusMatch)) + .findFirst() + .orElse(NO_WIN); + } + + public int getPrize() { + return prize; + } + + public int getMatchCount() { + return matchCount; + } +} diff --git a/src/main/java/planetlotto/service/IssueService.java b/src/main/java/planetlotto/service/IssueService.java new file mode 100644 index 000000000..0af3ae428 --- /dev/null +++ b/src/main/java/planetlotto/service/IssueService.java @@ -0,0 +1,32 @@ +package planetlotto.service; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.List; +import java.util.stream.IntStream; +import planetlotto.domain.Amount; +import planetlotto.domain.Lotto; +import planetlotto.domain.LottoPolicy; +import planetlotto.domain.Lottos; + +public class IssueService { + + public Lottos issueByAmount(Amount amount) { + int quantity = amount.availableQuantityByPolicy(LottoPolicy.TICKET_PRICE.get()); + + return issueLottosByQuantity(quantity); + } + + + private Lottos issueLottosByQuantity(int quantity) { + List lottos = IntStream.range(0, quantity) + .mapToObj(i -> Lotto.of(getLottoNumbers())) + .toList(); + + return new Lottos(lottos); + } + + private List getLottoNumbers() { + return Randoms.pickUniqueNumbersInRange(LottoPolicy.MIN_NUMBER.get(), LottoPolicy.MAX_NUMBER.get(), + LottoPolicy.SIZE.get()); + } +} diff --git a/src/main/java/planetlotto/view/InputView.java b/src/main/java/planetlotto/view/InputView.java index c1eb55343..7158d76de 100644 --- a/src/main/java/planetlotto/view/InputView.java +++ b/src/main/java/planetlotto/view/InputView.java @@ -39,4 +39,5 @@ public static int askBonusNumber() { throw new IllegalArgumentException("보너스 번호는 숫자여야 합니다."); } } + } diff --git a/src/main/java/planetlotto/view/OutputView.java b/src/main/java/planetlotto/view/OutputView.java index 0452898da..8382204e9 100644 --- a/src/main/java/planetlotto/view/OutputView.java +++ b/src/main/java/planetlotto/view/OutputView.java @@ -1,11 +1,12 @@ package planetlotto.view; +import static java.lang.System.lineSeparator; + +import java.time.DayOfWeek; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import static java.lang.System.lineSeparator; - public class OutputView { public static void printPurchasedLottos(final List> lottos) { final String header = String.format("%d개를 구매했습니다.", lottos.size()); @@ -21,8 +22,7 @@ public static void printPurchasedLottos(final List> lottos) { } /** - * index 0번은 미당첨 - * index 1~5번은 1~5등 + * index 0번은 미당첨 index 1~5번은 1~5등 */ public static void printResult(final Map countsByRank) { final List lines = List.of( @@ -44,4 +44,14 @@ public static void printResult(final Map countsByRank) { public static void printErrorMessage(final String message) { System.out.printf("[ERROR] %s%n", message); } + + public static void printDateResult(Map dayOfWeekMap) { + for (DayOfWeek dayOfWeek : dayOfWeekMap.keySet()) { + System.out.print(dayOfWeek + "-발행된 수량: "); + System.out.println(dayOfWeekMap.get(dayOfWeek)); + ; + + } + + } } diff --git a/src/test/java/planetlotto/domain/AmountTest.java b/src/test/java/planetlotto/domain/AmountTest.java new file mode 100644 index 000000000..d05c702df --- /dev/null +++ b/src/test/java/planetlotto/domain/AmountTest.java @@ -0,0 +1,39 @@ +package planetlotto.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class AmountTest { + @Test + void 구입금액을_정상적으로_입력하면_객체정상생성() { + String input = "1000"; + + Amount amount = Amount.of(Integer.parseInt(input)); + + assertThat(amount.getAmount()) + .isEqualTo(1000); + } + + @Test + void 구입금액이_로또가격으로_나누어떨어지지않으면_예외처리() { + String input = "1100"; + + assertThatThrownBy(() -> Amount.of(Integer.parseInt(input))); + } + + + @ParameterizedTest() + @ValueSource(strings = {"1000", "1500"}) + void 구입금액에따른_로또발행수량을_정상적으로_계산(String input) { + Amount amount = Amount.of(Integer.parseInt(input)); + + int quantity = amount.availableQuantityByPolicy(LottoPolicy.TICKET_PRICE.get()); + + assertThat(quantity) + .isEqualTo(Integer.parseInt(input) / LottoPolicy.TICKET_PRICE.get()); + } +} diff --git a/src/test/java/planetlotto/domain/LottosTest.java b/src/test/java/planetlotto/domain/LottosTest.java new file mode 100644 index 000000000..8e7008d1c --- /dev/null +++ b/src/test/java/planetlotto/domain/LottosTest.java @@ -0,0 +1,33 @@ +package planetlotto.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LottosTest { + + @Test + @DisplayName("로또를 추첨하여 당첨 결과를 정상 반환") + void 로또를_추첨하여_당첨_결과를_정상_반환() { + Lotto base = Lotto.of(List.of(1, 2, 3, 4, 5)); + WinningNumbers winningNumbers = WinningNumbers.of(base, 6); + + Lottos lottos = new Lottos(List.of( + Lotto.of(List.of(1, 2, 3, 4, 5)), + Lotto.of(List.of(1, 2, 3, 4, 6)), + Lotto.of(List.of(1, 2, 3, 4, 7)), + Lotto.of(List.of(1, 2, 3, 6, 7)) + )); + + DrawResult result = lottos.drawBy(winningNumbers); + + assertThat(result.countOf(WinningRules.FIRST)).isEqualTo(1); + assertThat(result.countOf(WinningRules.SECOND)).isEqualTo(1); + assertThat(result.countOf(WinningRules.THIRD)).isEqualTo(1); + assertThat(result.countOf(WinningRules.FOURTH)).isEqualTo(1); + assertThat(result.countOf(WinningRules.FIFTH)).isZero(); + assertThat(result.countOf(WinningRules.NO_WIN)).isEqualTo(0); + } +} diff --git a/src/test/java/planetlotto/domain/WinningNumbersTest.java b/src/test/java/planetlotto/domain/WinningNumbersTest.java new file mode 100644 index 000000000..4c4b911eb --- /dev/null +++ b/src/test/java/planetlotto/domain/WinningNumbersTest.java @@ -0,0 +1,41 @@ +package planetlotto.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class WinningNumbersTest { + private WinningNumbers winningNumbers; + + + @Test + void 기본당첨번호_6개와_보너스번호_1개로_당첨번호_정상생성() { + Lotto baseNumbers = Lotto.of(List.of(1, 2, 3, 4, 5)); + int bonusNumber = 7; + + assertThatCode(() -> WinningNumbers.of(baseNumbers, bonusNumber)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0, 31}) + void 보너스_번호가_1부터_30_아닌_경우_예외처리(int bounsNumberValue) { + Lotto baseNumbers = Lotto.of(List.of(1, 2, 3, 4, 5)); + int bonusNumber = bounsNumberValue; + + assertThatThrownBy(() -> WinningNumbers.of(baseNumbers, bonusNumber)); + } + + @Test + void 보너스번호가_기본당첨번호와_중복인_경우_예외처리() { + Lotto baseNumbers = Lotto.of(List.of(1, 2, 3, 4, 5)); + int bonusNumber = 5; + + assertThatThrownBy(() -> WinningNumbers.of(baseNumbers, bonusNumber)); + } + +}