diff --git a/README.md b/README.md new file mode 100644 index 000000000..599dd2e3f --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# java-lotto : 로또 - 클린코드 + +## Level1. 로또 자동 구매 + +### - 요구사항 + +- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다. +- [x] 로또 1장의 가격은 1000원이다. + +### - 새로운 프로그래밍 요구사항 + +- [x] 배열 대신 컬렉션을 사용한다. +- [x] 줄여 쓰지 않는다(축약 금지). +- [x] 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다. +- [x] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. + +## Level2. 로또 당첨 + +### - 요구사항 + +- [x] 로또 당첨 번호를 받아 일치한 번호 수에 따라 당첨 결과를 보여준다. + +### - 새로운 프로그래밍 요구사항 + +- [x] 모든 원시 값과 문자열을 포장한다. +- [x] 일급 컬렉션을 쓴다. + +## Level3. 로또 2등 당첨 + +### - 요구사항 + +- [ ] 2등을 위한 보너스볼을 추첨한다. +- [ ] 당첨 통계에 2등을 추가한다. +- [ ] 2등 당첨 조건은 당첨 번호 5개 일치 + 보너스 볼 일치다. + +### - 새로운 프로그래밍 요구사항 + +- [ ] Java Enum을 적용한다. + +## Level4. 로또 수동 구매 + +### - 요구사항 + +- [ ] 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다. +- [ ] 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다. + +## Level5. 리팩토링 + +### - 새로운 프로그래밍 요구사항 + +- [ ] 기존 프로그래밍 요구사항을 다시 한번 확인하고, 학습 테스트를 통해 학습한 내용을 반영한다. + +### - 기존 프로그래밍 요구사항 + +- [ ] 자바 코드 컨벤션을 지키면서 프로그래밍한다. +- [ ] 기본적으로 Java Style Guide을 원칙으로 한다. +- [ ] indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. +- [ ] 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 + 된다. +- [ ] 3항 연산자를 쓰지 않는다. +- [ ] else 예약어를 쓰지 않는다. +- [ ] else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. 힌트: if문에서 값을 반환하는 방식으로 + 구현하면 else 예약어를 사용하지 않아도 된다. +- [ ] 배열 대신 컬렉션을 사용한다. +- [ ] 줄여 쓰지 않는다(축약 금지). +- [ ] 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다. +- [ ] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. +- [ ] 모든 원시 값과 문자열을 포장한다. +- [ ] 일급 컬렉션을 쓴다. +- [ ] Java Enum을 적용한다. diff --git a/src/main/java/lotto/LottoController.java b/src/main/java/lotto/LottoController.java new file mode 100644 index 000000000..9b8cc8921 --- /dev/null +++ b/src/main/java/lotto/LottoController.java @@ -0,0 +1,12 @@ +package lotto; + +import lotto.view.LottoInputView; +import lotto.view.LottoOutputView; + +public class LottoController { + + public static void main(String[] args) { + LottoService service = new LottoService(new LottoInputView(), new LottoOutputView()); + service.start(); + } +} diff --git a/src/main/java/lotto/LottoService.java b/src/main/java/lotto/LottoService.java new file mode 100644 index 000000000..232651920 --- /dev/null +++ b/src/main/java/lotto/LottoService.java @@ -0,0 +1,48 @@ +package lotto; + +import java.util.List; +import java.util.stream.Collectors; +import lotto.model.LottoGenerator; +import lotto.model.Lottos; +import lotto.model.MatchCount; +import lotto.model.Money; +import lotto.model.PurchaseLotto; +import lotto.model.WinningNumbers; +import lotto.model.WinningResult; +import lotto.view.LottoInputView; +import lotto.view.LottoOutputView; + +public class LottoService { + + private final LottoInputView inputView; + private final LottoOutputView outputView; + private final LottoGenerator lottoGenerator; + + public LottoService(LottoInputView inputView, LottoOutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + this.lottoGenerator = new LottoGenerator(); + } + + public void start() { + Money money = new Money(inputView.inputMoney()); + PurchaseLotto purchaseLotto = new PurchaseLotto(money, lottoGenerator); + Lottos lottos = purchaseLotto.getLottos(); + + outputView.printPurchasedLottoCount(purchaseLotto.purchaseCount()); + outputView.printLottos(lottos.asList()); + + WinningNumbers winningNumbers = new WinningNumbers(inputView.inputWinningNumber()); + WinningResult winningResult = calculateWinningResult(lottos, winningNumbers); + + outputView.printWinningStatistics(money.getAmount(), winningResult.getWinningStatistics()); + } + + private WinningResult calculateWinningResult(Lottos lottos, WinningNumbers winningNumbers) { + List matchCounts = lottos.asList().stream() + .map(lotto -> lotto.match(winningNumbers)) + .collect(Collectors.toList()); + + return new WinningResult(matchCounts); + } +} diff --git a/src/main/java/lotto/model/LottoGenerator.java b/src/main/java/lotto/model/LottoGenerator.java new file mode 100644 index 000000000..c94061489 --- /dev/null +++ b/src/main/java/lotto/model/LottoGenerator.java @@ -0,0 +1,35 @@ +package lotto.model; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class LottoGenerator { + + private static final int MAX_NUMBER = 45; + private static final int LOTTO_SIZE = 6; + + public LottoNumbers generate() { + List shuffledNumbers = createShuffledNumbers(); + List pickSixNumber = pickRandomSixSorted(shuffledNumbers); + return new LottoNumbers(pickSixNumber); + } + + private List createShuffledNumbers() { + List numbers = IntStream.rangeClosed(1, MAX_NUMBER) + .boxed() + .collect(Collectors.toList()); + + Collections.shuffle(numbers); + return numbers; + } + + private List pickRandomSixSorted(List numbers) { + return numbers.stream() + .limit(LOTTO_SIZE) + .sorted() + .collect(Collectors.toList()); + } +} + diff --git a/src/main/java/lotto/model/LottoNumbers.java b/src/main/java/lotto/model/LottoNumbers.java new file mode 100644 index 000000000..7655f4b21 --- /dev/null +++ b/src/main/java/lotto/model/LottoNumbers.java @@ -0,0 +1,31 @@ +package lotto.model; + +import java.util.List; + +public class LottoNumbers { + + private static final int LOTTO_SIZE = 6; + private final List numbers; + + public LottoNumbers(List numbers) { + validate(numbers); + this.numbers = List.copyOf(numbers); + } + + private void validate(List numbers) { + if (numbers.size() != LOTTO_SIZE) { + throw new IllegalArgumentException("로또 번호는 6개여야 합니다."); + } + } + + public MatchCount match(WinningNumbers winningNumbers) { + long count = numbers.stream() + .filter(winningNumbers.getNumbers()::contains) + .count(); + return new MatchCount((int) count); + } + + public List getNumbers() { + return numbers; + } +} diff --git a/src/main/java/lotto/model/Lottos.java b/src/main/java/lotto/model/Lottos.java new file mode 100644 index 000000000..56351ee16 --- /dev/null +++ b/src/main/java/lotto/model/Lottos.java @@ -0,0 +1,21 @@ +package lotto.model; + +import java.util.List; + +public class Lottos { + + private final List lottoNumbers; + + public Lottos(List lottoNumbers) { + this.lottoNumbers = List.copyOf(lottoNumbers); + } + + public List asList() { + return lottoNumbers; + } + + public int size() { + return lottoNumbers.size(); + } + +} diff --git a/src/main/java/lotto/model/MatchCount.java b/src/main/java/lotto/model/MatchCount.java new file mode 100644 index 000000000..5b67705e8 --- /dev/null +++ b/src/main/java/lotto/model/MatchCount.java @@ -0,0 +1,14 @@ +package lotto.model; + +public class MatchCount { + + private final int count; + + public MatchCount(int count) { + this.count = count; + } + + public int getCount() { + return count; + } +} diff --git a/src/main/java/lotto/model/Money.java b/src/main/java/lotto/model/Money.java new file mode 100644 index 000000000..e32429486 --- /dev/null +++ b/src/main/java/lotto/model/Money.java @@ -0,0 +1,43 @@ +package lotto.model; + +import java.util.Objects; + +public class Money { + + private final int amount; + + public Money(int amount) { + validate(amount); + this.amount = amount; + } + + public int getAmount() { + return amount; + } + + private void validate(int amount) { + if (amount < 0) { + throw new IllegalArgumentException("금액은 음수일 수 없습니다."); + } + if (amount % 1000 != 0) { + throw new IllegalArgumentException("금액은 1000원 단위여야 합니다."); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Money money = (Money) o; + return amount == money.amount; + } + + @Override + public int hashCode() { + return Objects.hash(this.amount); + } +} diff --git a/src/main/java/lotto/model/PurchaseLotto.java b/src/main/java/lotto/model/PurchaseLotto.java new file mode 100644 index 000000000..31eda7474 --- /dev/null +++ b/src/main/java/lotto/model/PurchaseLotto.java @@ -0,0 +1,33 @@ +package lotto.model; + +import java.util.List; +import java.util.Objects; + +public class PurchaseLotto { + + public static final int PRICE = 1000; + private final Money money; + private final Lottos lottos; + + public PurchaseLotto(Money money, LottoGenerator lottoGenerator) { + Objects.requireNonNull(money); + Objects.requireNonNull(lottoGenerator); + + this.money = money; + this.lottos = new Lottos(generateLottos(lottoGenerator, purchaseCount())); + } + + public int purchaseCount() { + return money.getAmount() / PRICE; + } + + public Lottos getLottos() { + return lottos; + } + + private List generateLottos(LottoGenerator generator, int count) { + return java.util.stream.IntStream.range(0, count) + .mapToObj(i -> generator.generate()) + .collect(java.util.stream.Collectors.toList()); + } +} diff --git a/src/main/java/lotto/model/Rank.java b/src/main/java/lotto/model/Rank.java new file mode 100644 index 000000000..d223721b1 --- /dev/null +++ b/src/main/java/lotto/model/Rank.java @@ -0,0 +1,30 @@ +package lotto.model; + +public enum Rank { + MATCH_3(3, 5_000, "3개 일치 (5000원)"), + MATCH_4(4, 50_000, "4개 일치 (50000원)"), + MATCH_5(5, 1_500_000, "5개 일치 (1500000원)"), + MATCH_6(6, 2_000_000_000, "6개 일치 (2000000000원)"); + + private final int matchCount; + private final int prize; + private final String display; + + Rank(int matchCount, int prize, String display) { + this.matchCount = matchCount; + this.prize = prize; + this.display = display; + } + + public int getMatchCount() { + return matchCount; + } + + public int getPrize() { + return prize; + } + + public String getDisplay() { + return display; + } +} diff --git a/src/main/java/lotto/model/WinningNumbers.java b/src/main/java/lotto/model/WinningNumbers.java new file mode 100644 index 000000000..83cc3f955 --- /dev/null +++ b/src/main/java/lotto/model/WinningNumbers.java @@ -0,0 +1,16 @@ +package lotto.model; + +import java.util.List; + +public class WinningNumbers { + + private final LottoNumbers numbers; + + public WinningNumbers(List numbers) { + this.numbers = new LottoNumbers(numbers); + } + + public List getNumbers() { + return numbers.getNumbers(); + } +} diff --git a/src/main/java/lotto/model/WinningResult.java b/src/main/java/lotto/model/WinningResult.java new file mode 100644 index 000000000..b378b7d93 --- /dev/null +++ b/src/main/java/lotto/model/WinningResult.java @@ -0,0 +1,43 @@ +package lotto.model; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class WinningResult { + + private final Map matchResults; + + public WinningResult(List matchCounts) { + this.matchResults = calculateResults(matchCounts); + } + + private Map calculateResults(List matchCounts) { + Map results = new EnumMap<>(Rank.class); + + for (Rank rank : Rank.values()) { + results.put(rank, 0L); + } + + matchCounts.stream() + .map(mc -> findRankByMatchCount(mc.getCount())) + .filter(Objects::nonNull) + .forEach(rank -> results.put(rank, results.get(rank) + 1)); + + return results; + } + + private Rank findRankByMatchCount(int matchCount) { + for (Rank rank : Rank.values()) { + if (rank.getMatchCount() == matchCount) { + return rank; + } + } + return null; + } + + public Map getWinningStatistics() { + return new EnumMap<>(matchResults); + } +} diff --git a/src/main/java/lotto/view/LottoInputView.java b/src/main/java/lotto/view/LottoInputView.java new file mode 100644 index 000000000..4751583b9 --- /dev/null +++ b/src/main/java/lotto/view/LottoInputView.java @@ -0,0 +1,30 @@ +package lotto.view; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class LottoInputView { + + public int inputMoney() { + System.out.println("구입금액을 입력해 주세요."); + Scanner scanner = new Scanner(System.in); + + return scanner.nextInt(); + } + + public List inputWinningNumber() { + System.out.println("지난 주 당첨 번호를 입력해 주세요."); + Scanner scanner = new Scanner(System.in); + + String winningNumbersString = scanner.nextLine(); + String[] numbers = winningNumbersString.split(","); + ArrayList winningNumbers = new ArrayList<>(); + + for (String num : numbers) { + winningNumbers.add(Integer.parseInt(num.trim())); + } + + return winningNumbers; + } +} diff --git a/src/main/java/lotto/view/LottoOutputView.java b/src/main/java/lotto/view/LottoOutputView.java new file mode 100644 index 000000000..91067eb22 --- /dev/null +++ b/src/main/java/lotto/view/LottoOutputView.java @@ -0,0 +1,35 @@ +package lotto.view; + +import java.util.List; +import java.util.Map; +import lotto.model.LottoNumbers; +import lotto.model.Rank; + +public class LottoOutputView { + + public void printPurchasedLottoCount(int count) { + System.out.println(count + "개를 구매했습니다."); + } + + public void printLottos(List lottoNumbers) { + for (LottoNumbers lotto : lottoNumbers) { + System.out.println(lotto.getNumbers()); + } + } + + public void printWinningStatistics(int money, Map winningLottos) { + System.out.println("당첨 통계"); + System.out.println("---------"); + long totalPrize = 0L; + + for (Rank rank : Rank.values()) { + long count = winningLottos.getOrDefault(rank, 0L); + System.out.println(rank.getDisplay() + " - " + count + "개"); + totalPrize += count * rank.getPrize(); + } + + double profitRate = (double) totalPrize / money; + System.out.printf("총 수익률은 %.2f%%입니다.%n", profitRate); + + } +} diff --git a/src/test/java/lotto/LottoNumbersTest.java b/src/test/java/lotto/LottoNumbersTest.java new file mode 100644 index 000000000..53422fe7c --- /dev/null +++ b/src/test/java/lotto/LottoNumbersTest.java @@ -0,0 +1,32 @@ +package lotto; + +import lotto.model.LottoNumbers; +import lotto.model.MatchCount; +import lotto.model.WinningNumbers; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class LottoNumbersTest { + + @Test + @DisplayName("로또 번호가 6개가 아니면 예외를 발생") + void throwExceptionIfLottoNumbersAreNotSix() { + assertThrows(IllegalArgumentException.class, () -> + new LottoNumbers(Arrays.asList(1, 2, 3, 4, 5)) + ); + } + + @Test + @DisplayName("로또 번호와 당첨 번호를 비교해 일치 개수를 계산") + void calculateMatchedCountCorrectly() { + LottoNumbers lotto = new LottoNumbers(Arrays.asList(1, 2, 3, 4, 5, 6)); + WinningNumbers winning = new WinningNumbers(Arrays.asList(1, 2, 3, 7, 8, 9)); + + MatchCount result = lotto.match(winning); + assertThat(result.getCount()).isEqualTo(3); + } +} diff --git a/src/test/java/lotto/MoneyTest.java b/src/test/java/lotto/MoneyTest.java new file mode 100644 index 000000000..efd3174fb --- /dev/null +++ b/src/test/java/lotto/MoneyTest.java @@ -0,0 +1,33 @@ +package lotto; + +import lotto.model.Money; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MoneyTest { + + @Test + @DisplayName("음수 금액 입력 시 예외 발생") + void throwExceptionIfAmountIsNegative() { + assertThrows(IllegalArgumentException.class, () -> new Money(-1000)); + } + + @Test + @DisplayName("1000원 단위가 아니면 예외 발생") + void throwExceptionIfAmountIsNotThousandUnit() { + assertThrows(IllegalArgumentException.class, () -> new Money(1500)); + } + + @Test + @DisplayName("VO 객체가 아니면 예외 발생") + public void voCheck() { + Money first = new Money(1000); + Money second = new Money(1000); + + assertThat(first).isEqualTo(second); + assertThat(first.hashCode()).isEqualTo(second.hashCode()); + } +} diff --git a/src/test/java/lotto/PurchaseLottoTest.java b/src/test/java/lotto/PurchaseLottoTest.java new file mode 100644 index 000000000..8dc8b5b8f --- /dev/null +++ b/src/test/java/lotto/PurchaseLottoTest.java @@ -0,0 +1,23 @@ +package lotto; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import lotto.model.LottoGenerator; +import lotto.model.Money; +import lotto.model.PurchaseLotto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PurchaseLottoTest { + + @Test + @DisplayName("금액에 맞는 로또 개수인지 확인") + void calculateLottoCountByAmount() { + Money money = new Money(5000); + PurchaseLotto purchase = new PurchaseLotto(money, new LottoGenerator()); + + assertThat(purchase.purchaseCount()).isEqualTo(5); + assertThat(purchase.getLottos().size()).isEqualTo(5); + } + +}