From 15fef24cac5205ed610163f743164cc7c7066797 Mon Sep 17 00:00:00 2001 From: Junghoon Kim <59908721+kay019@users.noreply.github.com> Date: Wed, 31 Dec 2025 18:29:01 +0900 Subject: [PATCH 1/4] feat : step3 --- src/main/java/lotto/BonusNumber.java | 24 +++++++++ src/main/java/lotto/MatchResult.java | 19 +++++++ src/main/java/lotto/Rank.java | 34 +++++++------ src/main/java/lotto/WinningNumbers.java | 24 +++++++++ src/main/java/lotto/WinningStatistics.java | 15 +++--- src/test/java/lotto/BonusNumberTest.java | 18 +++++++ src/test/java/lotto/RankTest.java | 41 ++++++++++----- src/test/java/lotto/WinningNumbersTest.java | 33 ++++++++++++ .../java/lotto/WinningStatisticsTest.java | 50 +++++++++++++------ 9 files changed, 206 insertions(+), 52 deletions(-) create mode 100644 src/main/java/lotto/BonusNumber.java create mode 100644 src/main/java/lotto/MatchResult.java create mode 100644 src/main/java/lotto/WinningNumbers.java create mode 100644 src/test/java/lotto/BonusNumberTest.java create mode 100644 src/test/java/lotto/WinningNumbersTest.java diff --git a/src/main/java/lotto/BonusNumber.java b/src/main/java/lotto/BonusNumber.java new file mode 100644 index 0000000000..cc093c8e2e --- /dev/null +++ b/src/main/java/lotto/BonusNumber.java @@ -0,0 +1,24 @@ +package lotto; + +public class BonusNumber { + private final int value; + + private BonusNumber(int value) { + validateRange(value); + this.value = value; + } + + public static BonusNumber of(int value) { + return new BonusNumber(value); + } + + public int value() { + return value; + } + + private void validateRange(int value) { + if (value < 1 || value > 45) { + throw new IllegalArgumentException("보너스 번호는 1~45 범위여야 한다."); + } + } +} diff --git a/src/main/java/lotto/MatchResult.java b/src/main/java/lotto/MatchResult.java new file mode 100644 index 0000000000..e42b00e6a1 --- /dev/null +++ b/src/main/java/lotto/MatchResult.java @@ -0,0 +1,19 @@ +package lotto; + +public class MatchResult { + private final int matchCount; + private final boolean bonusMatched; + + public MatchResult(int matchCount, boolean bonusMatched) { + this.matchCount = matchCount; + this.bonusMatched = bonusMatched; + } + + public int matchCount() { + return matchCount; + } + + public boolean bonusMatched() { + return bonusMatched; + } +} diff --git a/src/main/java/lotto/Rank.java b/src/main/java/lotto/Rank.java index d79d2d4e31..1ff6ba6097 100644 --- a/src/main/java/lotto/Rank.java +++ b/src/main/java/lotto/Rank.java @@ -3,26 +3,25 @@ import java.util.Arrays; public enum Rank { - FIRST(6, 2_000_000_000, "6개 일치"), - THIRD(5, 1_500_000, "5개 일치"), - FOURTH(4, 50_000, "4개 일치"), - FIFTH(3, 5_000, "3개 일치"), - MISS(0, 0, ""); + FIRST(6, false, 2_000_000_000, "6개 일치"), + SECOND(5, true, 30_000_000, "5개 일치, 보너스 볼 일치"), + THIRD(5, false, 1_500_000, "5개 일치"), + FOURTH(4, false, 50_000, "4개 일치"), + FIFTH(3, false, 5_000, "3개 일치"), + MISS(0, false, 0, ""); private final int matchCount; + private final boolean bonusMatched; private final long prize; private final String description; - Rank(int matchCount, long prize, String description) { + Rank(int matchCount, boolean bonusMatched, long prize, String description) { this.matchCount = matchCount; + this.bonusMatched = bonusMatched; this.prize = prize; this.description = description; } - public int matchCount() { - return matchCount; - } - public long prize() { return prize; } @@ -35,16 +34,19 @@ public boolean isWinning() { return this != MISS; } - public static Rank of(int matchCount) { + public static Rank of(MatchResult result) { return Arrays.stream(values()) - .filter(rank -> rank.matchCount == matchCount) + .filter(r -> r.matchCount == result.matchCount()) + .filter(r -> r.bonusMatched == result.bonusMatched()) .findFirst() - .orElse(MISS); + .orElseGet(() -> Arrays.stream(values()) + .filter(r -> r.matchCount == result.matchCount()) + .filter(r -> !r.bonusMatched) + .findFirst() + .orElse(MISS)); } public static Rank[] winningRanks() { - return Arrays.stream(values()) - .filter(Rank::isWinning) - .toArray(Rank[]::new); + return Arrays.stream(values()).filter(Rank::isWinning).toArray(Rank[]::new); } } diff --git a/src/main/java/lotto/WinningNumbers.java b/src/main/java/lotto/WinningNumbers.java new file mode 100644 index 0000000000..c6144ee887 --- /dev/null +++ b/src/main/java/lotto/WinningNumbers.java @@ -0,0 +1,24 @@ +package lotto; + +public class WinningNumbers { + private final Lotto winning; + private final BonusNumber bonus; + + public WinningNumbers(Lotto winning, BonusNumber bonus) { + validateBonusNotDuplicated(winning, bonus); + this.winning = winning; + this.bonus = bonus; + } + + public MatchResult match(Lotto ticket) { + int matchCount = ticket.matchCount(winning); + boolean bonusMatched = ticket.contains(bonus.value()); + return new MatchResult(matchCount, bonusMatched); + } + + private void validateBonusNotDuplicated(Lotto winning, BonusNumber bonus) { + if (winning.contains(bonus.value())) { + throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없다."); + } + } +} diff --git a/src/main/java/lotto/WinningStatistics.java b/src/main/java/lotto/WinningStatistics.java index 0e361bd8f1..02c1b95f5a 100644 --- a/src/main/java/lotto/WinningStatistics.java +++ b/src/main/java/lotto/WinningStatistics.java @@ -3,20 +3,21 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; +import lotto.MatchResult; -public class WinningStatistics { +public final class WinningStatistics { private final Map counts = new EnumMap<>(Rank.class); public WinningStatistics() { initCounts(); } - public void accumulate(List lottos, Lotto winning) { - lottos.forEach(lotto -> accumulateOne(lotto, winning)); + public void accumulate(List tickets, WinningNumbers winningNumbers) { + tickets.forEach(ticket -> accumulateOne(ticket, winningNumbers)); } public int countOf(Rank rank) { - return counts.get(rank); + return counts.getOrDefault(rank, 0); } public long totalPrize() { @@ -35,11 +36,11 @@ private void initCounts() { } } - private void accumulateOne(Lotto lotto, Lotto winning) { - Rank rank = Rank.of(lotto.matchCount(winning)); + private void accumulateOne(Lotto ticket, WinningNumbers winningNumbers) { + Rank rank = Rank.of(winningNumbers.match(ticket)); if (!rank.isWinning()) { return; } - counts.put(rank, counts.get(rank) + 1); + counts.put(rank, countOf(rank) + 1); } } diff --git a/src/test/java/lotto/BonusNumberTest.java b/src/test/java/lotto/BonusNumberTest.java new file mode 100644 index 0000000000..1367631a84 --- /dev/null +++ b/src/test/java/lotto/BonusNumberTest.java @@ -0,0 +1,18 @@ +package lotto; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class BonusNumberTest { + @Test + @DisplayName("보너스 번호는 1~45 범위를 벗어나면 예외가 발생한다") + void bonusNumberMustBeInRange() { + assertThatThrownBy(() -> BonusNumber.of(0)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> BonusNumber.of(46)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/lotto/RankTest.java b/src/test/java/lotto/RankTest.java index c6000a40cf..66c4cd1680 100644 --- a/src/test/java/lotto/RankTest.java +++ b/src/test/java/lotto/RankTest.java @@ -8,24 +8,39 @@ public class RankTest { @Test - @DisplayName("일치 개수로 등수를 찾는다 (6개=1등, 5개=3등, 4개=4등, 3개=5등)") - void rankOfMatchCount() { - assertThat(Rank.of(6)).isEqualTo(Rank.FIRST); - assertThat(Rank.of(5)).isEqualTo(Rank.THIRD); - assertThat(Rank.of(4)).isEqualTo(Rank.FOURTH); - assertThat(Rank.of(3)).isEqualTo(Rank.FIFTH); + @DisplayName("6개 일치이면 1등이다") + void firstRank() { + assertThat(Rank.of(new MatchResult(6, false))).isEqualTo(Rank.FIRST); } @Test - @DisplayName("당첨이 아닌 개수는 MISS로 반환한다") - void nonWinningIsMiss() { - assertThat(Rank.of(2)).isEqualTo(Rank.MISS); - assertThat(Rank.of(0)).isEqualTo(Rank.MISS); + @DisplayName("5개 일치 + 보너스 일치이면 2등이다") + void secondRank() { + assertThat(Rank.of(new MatchResult(5, true))).isEqualTo(Rank.SECOND); } @Test - @DisplayName("winningRanks는 MISS를 제외한 당첨 등수만 반환한다") - void winningRanksExcludesMiss() { - assertThat(Rank.winningRanks()).doesNotContain(Rank.MISS); + @DisplayName("5개 일치 + 보너스 불일치이면 3등이다") + void thirdRank() { + assertThat(Rank.of(new MatchResult(5, false))).isEqualTo(Rank.THIRD); } + + @Test + @DisplayName("4개 일치이면 4등이다") + void fourthRank() { + assertThat(Rank.of(new MatchResult(4, false))).isEqualTo(Rank.FOURTH); + } + + @Test + @DisplayName("3개 일치이면 5등이다") + void fifthRank() { + assertThat(Rank.of(new MatchResult(3, false))).isEqualTo(Rank.FIFTH); + } + + @Test + @DisplayName("2개 이하 일치이면 MISS이다") + void missRank() { + assertThat(Rank.of(new MatchResult(2, false))).isEqualTo(Rank.MISS); + } + } diff --git a/src/test/java/lotto/WinningNumbersTest.java b/src/test/java/lotto/WinningNumbersTest.java new file mode 100644 index 0000000000..005870ff08 --- /dev/null +++ b/src/test/java/lotto/WinningNumbersTest.java @@ -0,0 +1,33 @@ +package lotto; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class WinningNumbersTest { + @Test + @DisplayName("보너스 번호는 당첨 번호와 중복될 수 없다") + void bonusMustNotDuplicateWinningNumbers() { + Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); + BonusNumber bonus = BonusNumber.of(6); + + assertThatThrownBy(() -> new WinningNumbers(winning, bonus)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("티켓의 일치 개수와 보너스 일치 여부를 MatchResult로 반환한다") + void matchReturnsMatchResult() { + Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); + BonusNumber bonus = BonusNumber.of(7); + WinningNumbers winningNumbers = new WinningNumbers(winning, bonus); + + Lotto ticket = new Lotto(1, 2, 3, 4, 5, 7); // 5개 + bonus + MatchResult result = winningNumbers.match(ticket); + + assertThat(result.matchCount()).isEqualTo(5); + assertThat(result.bonusMatched()).isTrue(); + } +} diff --git a/src/test/java/lotto/WinningStatisticsTest.java b/src/test/java/lotto/WinningStatisticsTest.java index 939b7a9724..3561022585 100644 --- a/src/test/java/lotto/WinningStatisticsTest.java +++ b/src/test/java/lotto/WinningStatisticsTest.java @@ -8,35 +8,53 @@ public class WinningStatisticsTest { @Test - @DisplayName("로또 결과를 등수별로 집계한다") - void accumulateStatistics() { + @DisplayName("5개 일치 + 보너스 일치는 2등으로 집계된다") + void accumulateSecondRank() { Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - List lottos = List.of( - new Lotto(1, 2, 3, 10, 11, 12), - new Lotto(1, 2, 3, 4, 11, 12) + WinningNumbers winningNumbers = new WinningNumbers(winning, BonusNumber.of(7)); + + List tickets = List.of( + new Lotto(1, 2, 3, 4, 5, 7) // 5 + bonus => SECOND ); WinningStatistics stats = new WinningStatistics(); - stats.accumulate(lottos, winning); + stats.accumulate(tickets, winningNumbers); - assertThat(stats.countOf(Rank.FIFTH)).isEqualTo(1); - assertThat(stats.countOf(Rank.FOURTH)).isEqualTo(1); + assertThat(stats.countOf(Rank.SECOND)).isEqualTo(1); assertThat(stats.countOf(Rank.THIRD)).isEqualTo(0); - assertThat(stats.countOf(Rank.FIRST)).isEqualTo(0); } @Test - @DisplayName("총 당첨금은 등수별 상금의 합이다") - void totalPrizeCalculation() { + @DisplayName("총 당첨금은 등수별 (상금 * 당첨횟수)의 합이다") + void totalPrizeIsSumOfPrizeTimesCount() { Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - List lottos = List.of( - new Lotto(1, 2, 3, 10, 11, 12), - new Lotto(1, 2, 3, 4, 11, 12) + WinningNumbers winningNumbers = new WinningNumbers(winning, BonusNumber.of(7)); + + List tickets = List.of( + new Lotto(1, 2, 3, 4, 5, 7), // SECOND: 30,000,000 + new Lotto(1, 2, 3, 4, 5, 10) // THIRD: 1,500,000 + ); + + WinningStatistics stats = new WinningStatistics(); + stats.accumulate(tickets, winningNumbers); + + assertThat(stats.totalPrize()).isEqualTo(31_500_000L); + } + + @Test + @DisplayName("수익률은 총 당첨금을 구입 금액으로 나눈 값이다") + void profitRateIsTotalPrizeDividedByPurchaseAmount() { + Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); + WinningNumbers winningNumbers = new WinningNumbers(winning, BonusNumber.of(7)); + + List tickets = List.of( + new Lotto(1, 2, 3, 4, 5, 7) // 30,000,000 ); WinningStatistics stats = new WinningStatistics(); - stats.accumulate(lottos, winning); + stats.accumulate(tickets, winningNumbers); - assertThat(stats.totalPrize()).isEqualTo(55_000); + Money purchase = Money.of(10_000); + assertThat(stats.profitRate(purchase)).isEqualTo(3000.0); } } From 54a02ee7a8a24a996f7bd5f6fe6f393efe28c5fe Mon Sep 17 00:00:00 2001 From: Junghoon Kim <59908721+kay019@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:58:02 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix=20:=20code=20review=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/lotto/MatchResult.java | 19 -------- src/main/java/lotto/Rank.java | 44 ++++++++++++------- src/main/java/lotto/ResultView.java | 13 ++++-- src/main/java/lotto/WinningNumbers.java | 8 +++- src/main/java/lotto/WinningStatistics.java | 3 +- src/test/java/lotto/WinningNumbersTest.java | 27 +++++++----- .../java/lotto/WinningStatisticsTest.java | 40 +++++------------ 7 files changed, 71 insertions(+), 83 deletions(-) delete mode 100644 src/main/java/lotto/MatchResult.java diff --git a/src/main/java/lotto/MatchResult.java b/src/main/java/lotto/MatchResult.java deleted file mode 100644 index e42b00e6a1..0000000000 --- a/src/main/java/lotto/MatchResult.java +++ /dev/null @@ -1,19 +0,0 @@ -package lotto; - -public class MatchResult { - private final int matchCount; - private final boolean bonusMatched; - - public MatchResult(int matchCount, boolean bonusMatched) { - this.matchCount = matchCount; - this.bonusMatched = bonusMatched; - } - - public int matchCount() { - return matchCount; - } - - public boolean bonusMatched() { - return bonusMatched; - } -} diff --git a/src/main/java/lotto/Rank.java b/src/main/java/lotto/Rank.java index 1ff6ba6097..75868c6111 100644 --- a/src/main/java/lotto/Rank.java +++ b/src/main/java/lotto/Rank.java @@ -3,23 +3,25 @@ import java.util.Arrays; public enum Rank { - FIRST(6, false, 2_000_000_000, "6개 일치"), - SECOND(5, true, 30_000_000, "5개 일치, 보너스 볼 일치"), - THIRD(5, false, 1_500_000, "5개 일치"), - FOURTH(4, false, 50_000, "4개 일치"), - FIFTH(3, false, 5_000, "3개 일치"), - MISS(0, false, 0, ""); + FIRST(6, false, 2_000_000_000, "6개 일치", 5), + SECOND(5, true, 30_000_000, "5개 일치, 보너스 볼 일치", 4), + THIRD(5, false, 1_500_000, "5개 일치", 3), + FOURTH(4, false, 50_000, "4개 일치", 2), + FIFTH(3, false, 5_000, "3개 일치", 1), + MISS(0, false, 0, "", 99); private final int matchCount; private final boolean bonusMatched; private final long prize; private final String description; + private final int displayOrder; - Rank(int matchCount, boolean bonusMatched, long prize, String description) { + Rank(int matchCount, boolean bonusMatched, long prize, String description, int displayOrder) { this.matchCount = matchCount; this.bonusMatched = bonusMatched; this.prize = prize; this.description = description; + this.displayOrder = displayOrder; } public long prize() { @@ -30,23 +32,33 @@ public String description() { return description; } + public int displayOrder() { + return displayOrder; + } + public boolean isWinning() { return this != MISS; } - public static Rank of(MatchResult result) { + public static Rank of(int matchCount, boolean bonusMatched) { return Arrays.stream(values()) - .filter(r -> r.matchCount == result.matchCount()) - .filter(r -> r.bonusMatched == result.bonusMatched()) + .filter(r -> r.matchCount == matchCount) + .filter(r -> r.bonusMatched == bonusMatched) .findFirst() - .orElseGet(() -> Arrays.stream(values()) - .filter(r -> r.matchCount == result.matchCount()) - .filter(r -> !r.bonusMatched) - .findFirst() - .orElse(MISS)); + .orElseGet(() -> fallbackWithoutBonus(matchCount)); } public static Rank[] winningRanks() { - return Arrays.stream(values()).filter(Rank::isWinning).toArray(Rank[]::new); + return Arrays.stream(values()) + .filter(Rank::isWinning) + .toArray(Rank[]::new); + } + + private static Rank fallbackWithoutBonus(int matchCount) { + return Arrays.stream(values()) + .filter(r -> r.matchCount == matchCount) + .filter(r -> !r.bonusMatched) + .findFirst() + .orElse(MISS); } } diff --git a/src/main/java/lotto/ResultView.java b/src/main/java/lotto/ResultView.java index 8d66b83726..a322560fe7 100644 --- a/src/main/java/lotto/ResultView.java +++ b/src/main/java/lotto/ResultView.java @@ -1,5 +1,7 @@ package lotto; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; public class ResultView { @@ -13,11 +15,14 @@ public void printStatistics(WinningStatistics stats, Money money) { System.out.println("당첨 통계"); System.out.println("---------"); - for (Rank rank : Rank.winningRanks()) { - System.out.println(rank.description() + " (" + rank.prize() + "원)- " - + stats.countOf(rank) + "개"); - } + Arrays.stream(Rank.winningRanks()) + .sorted(Comparator.comparingInt(Rank::displayOrder)) + .forEach(rank -> printRank(stats, rank)); System.out.println("총 수익률은 " + stats.profitRate(money) + "입니다."); } + + private void printRank(WinningStatistics stats, Rank rank) { + System.out.println(rank.description() + " (" + rank.prize() + "원)- " + stats.countOf(rank) + "개"); + } } diff --git a/src/main/java/lotto/WinningNumbers.java b/src/main/java/lotto/WinningNumbers.java index c6144ee887..6799605dfa 100644 --- a/src/main/java/lotto/WinningNumbers.java +++ b/src/main/java/lotto/WinningNumbers.java @@ -10,10 +10,14 @@ public WinningNumbers(Lotto winning, BonusNumber bonus) { this.bonus = bonus; } - public MatchResult match(Lotto ticket) { + public WinningNumbers(Lotto winning, int bonus) { + this(winning, BonusNumber.of(bonus)); + } + + public Rank match(Lotto ticket) { int matchCount = ticket.matchCount(winning); boolean bonusMatched = ticket.contains(bonus.value()); - return new MatchResult(matchCount, bonusMatched); + return Rank.of(matchCount, bonusMatched); } private void validateBonusNotDuplicated(Lotto winning, BonusNumber bonus) { diff --git a/src/main/java/lotto/WinningStatistics.java b/src/main/java/lotto/WinningStatistics.java index 02c1b95f5a..9706c0c595 100644 --- a/src/main/java/lotto/WinningStatistics.java +++ b/src/main/java/lotto/WinningStatistics.java @@ -3,7 +3,6 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; -import lotto.MatchResult; public final class WinningStatistics { private final Map counts = new EnumMap<>(Rank.class); @@ -37,7 +36,7 @@ private void initCounts() { } private void accumulateOne(Lotto ticket, WinningNumbers winningNumbers) { - Rank rank = Rank.of(winningNumbers.match(ticket)); + Rank rank = winningNumbers.match(ticket); if (!rank.isWinning()) { return; } diff --git a/src/test/java/lotto/WinningNumbersTest.java b/src/test/java/lotto/WinningNumbersTest.java index 005870ff08..77bc9d3e0c 100644 --- a/src/test/java/lotto/WinningNumbersTest.java +++ b/src/test/java/lotto/WinningNumbersTest.java @@ -11,23 +11,28 @@ public class WinningNumbersTest { @DisplayName("보너스 번호는 당첨 번호와 중복될 수 없다") void bonusMustNotDuplicateWinningNumbers() { Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - BonusNumber bonus = BonusNumber.of(6); - assertThatThrownBy(() -> new WinningNumbers(winning, bonus)) + assertThatThrownBy(() -> new WinningNumbers(winning, 6)) .isInstanceOf(IllegalArgumentException.class); } @Test - @DisplayName("티켓의 일치 개수와 보너스 일치 여부를 MatchResult로 반환한다") - void matchReturnsMatchResult() { - Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - BonusNumber bonus = BonusNumber.of(7); - WinningNumbers winningNumbers = new WinningNumbers(winning, bonus); + @DisplayName("5개 일치 + 보너스 일치면 2등을 반환한다") + void matchReturnsSecondRank() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); + + Rank rank = winningNumbers.match(new Lotto(1, 2, 3, 4, 5, 7)); + + assertThat(rank).isEqualTo(Rank.SECOND); + } + + @Test + @DisplayName("5개 일치 + 보너스 불일치면 3등을 반환한다") + void matchReturnsThirdRank() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); - Lotto ticket = new Lotto(1, 2, 3, 4, 5, 7); // 5개 + bonus - MatchResult result = winningNumbers.match(ticket); + Rank rank = winningNumbers.match(new Lotto(1, 2, 3, 4, 5, 10)); - assertThat(result.matchCount()).isEqualTo(5); - assertThat(result.bonusMatched()).isTrue(); + assertThat(rank).isEqualTo(Rank.THIRD); } } diff --git a/src/test/java/lotto/WinningStatisticsTest.java b/src/test/java/lotto/WinningStatisticsTest.java index 3561022585..8a60bbe53f 100644 --- a/src/test/java/lotto/WinningStatisticsTest.java +++ b/src/test/java/lotto/WinningStatisticsTest.java @@ -8,31 +8,30 @@ public class WinningStatisticsTest { @Test - @DisplayName("5개 일치 + 보너스 일치는 2등으로 집계된다") - void accumulateSecondRank() { - Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - WinningNumbers winningNumbers = new WinningNumbers(winning, BonusNumber.of(7)); + @DisplayName("당첨 결과를 등수별로 집계한다") + void accumulateByRank() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7) // 5 + bonus => SECOND + new Lotto(1, 2, 3, 4, 5, 7), // 2등 + new Lotto(1, 2, 3, 4, 5, 10) // 3등 ); WinningStatistics stats = new WinningStatistics(); stats.accumulate(tickets, winningNumbers); assertThat(stats.countOf(Rank.SECOND)).isEqualTo(1); - assertThat(stats.countOf(Rank.THIRD)).isEqualTo(0); + assertThat(stats.countOf(Rank.THIRD)).isEqualTo(1); } @Test - @DisplayName("총 당첨금은 등수별 (상금 * 당첨횟수)의 합이다") - void totalPrizeIsSumOfPrizeTimesCount() { - Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - WinningNumbers winningNumbers = new WinningNumbers(winning, BonusNumber.of(7)); + @DisplayName("총 당첨금은 등수별 상금 * 당첨횟수의 합이다") + void totalPrize() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7), // SECOND: 30,000,000 - new Lotto(1, 2, 3, 4, 5, 10) // THIRD: 1,500,000 + new Lotto(1, 2, 3, 4, 5, 7), // 30,000,000 + new Lotto(1, 2, 3, 4, 5, 10) // 1,500,000 ); WinningStatistics stats = new WinningStatistics(); @@ -40,21 +39,4 @@ void totalPrizeIsSumOfPrizeTimesCount() { assertThat(stats.totalPrize()).isEqualTo(31_500_000L); } - - @Test - @DisplayName("수익률은 총 당첨금을 구입 금액으로 나눈 값이다") - void profitRateIsTotalPrizeDividedByPurchaseAmount() { - Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - WinningNumbers winningNumbers = new WinningNumbers(winning, BonusNumber.of(7)); - - List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7) // 30,000,000 - ); - - WinningStatistics stats = new WinningStatistics(); - stats.accumulate(tickets, winningNumbers); - - Money purchase = Money.of(10_000); - assertThat(stats.profitRate(purchase)).isEqualTo(3000.0); - } } From f173a645750656e2739a6a0f24f17f1fa3c2211d Mon Sep 17 00:00:00 2001 From: Junghoon Kim <59908721+kay019@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:42:22 +0900 Subject: [PATCH 3/4] fix : error fix --- src/main/java/calculator/App.java | 13 ------- src/main/java/lotto/Application.java | 3 -- src/main/java/lotto/InputView.java | 5 +++ src/main/java/lotto/Lotto.java | 20 +++------- src/main/java/lotto/LottoController.java | 11 +++--- src/main/java/lotto/LottoNumber.java | 38 +++++++++++++++++++ src/main/java/lotto/WinningNumbers.java | 8 ++++ src/test/java/lotto/LottoTest.java | 24 +++++++----- src/test/java/lotto/RankTest.java | 15 ++++---- src/test/java/lotto/WinningNumbersTest.java | 18 +++++++++ .../java/lotto/WinningStatisticsTest.java | 28 +++++++------- 11 files changed, 113 insertions(+), 70 deletions(-) delete mode 100644 src/main/java/calculator/App.java create mode 100644 src/main/java/lotto/LottoNumber.java diff --git a/src/main/java/calculator/App.java b/src/main/java/calculator/App.java deleted file mode 100644 index 2549f3a2b3..0000000000 --- a/src/main/java/calculator/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package calculator; - -import lotto.LottoController; -import lotto.LottoMachine; -import lotto.ShuffleNumberGenerator; - -public class Application { - public static void main(String[] args) { - LottoMachine machine = new LottoMachine(new ShuffleNumberGenerator()); - LottoController controller = new LottoController(new InputView(), new ResultView(), machine); - controller.run(); - } -} diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index 8a8546a8b9..31f5b45df5 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,8 +1,5 @@ package lotto; -import lotto.InputView; -import lotto.ResultView; - public class Application { public static void main(String[] args) { LottoMachine machine = new LottoMachine(new ShuffleNumberGenerator()); diff --git a/src/main/java/lotto/InputView.java b/src/main/java/lotto/InputView.java index 7eb3a7ad3e..ac8db7d376 100644 --- a/src/main/java/lotto/InputView.java +++ b/src/main/java/lotto/InputView.java @@ -20,4 +20,9 @@ public List readWinningNumbers() { .map(Integer::parseInt) .collect(Collectors.toList()); } + + public int readBonusNumber() { + System.out.println("보너스 볼을 입력해 주세요."); + return Integer.parseInt(scanner.nextLine().trim()); + } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 3b56c1c7cf..50022e548d 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -1,8 +1,6 @@ package lotto; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -10,15 +8,13 @@ public class Lotto { private static final int SIZE = 6; - private static final int MIN = 1; - private static final int MAX = 45; private final Set numbers; public Lotto(List numbers) { validateSize(numbers); - validateRange(numbers); validateUnique(numbers); + validateRangeByValueObject(numbers); this.numbers = Set.copyOf(numbers); } @@ -48,19 +44,13 @@ private void validateSize(List numbers) { } } - private void validateRange(List numbers) { - if (numbers.stream().anyMatch(this::isOutOfRange)) { - throw new IllegalArgumentException("로또 번호는 1~45 범위여야 한다."); - } - } - - private boolean isOutOfRange(int number) { - return number < MIN || number > MAX; - } - private void validateUnique(List numbers) { if (new HashSet<>(numbers).size() != SIZE) { throw new IllegalArgumentException("로또 번호는 중복될 수 없다."); } } + + private void validateRangeByValueObject(List numbers) { + numbers.forEach(LottoNumber::of); + } } diff --git a/src/main/java/lotto/LottoController.java b/src/main/java/lotto/LottoController.java index 99f3fe8310..2f177188ca 100644 --- a/src/main/java/lotto/LottoController.java +++ b/src/main/java/lotto/LottoController.java @@ -1,7 +1,6 @@ package lotto; -import lotto.InputView; -import lotto.ResultView; + import java.util.List; public class LottoController { @@ -18,13 +17,13 @@ public LottoController(InputView inputView, ResultView resultView, LottoMachine public void run() { Money money = Money.of(inputView.readPurchaseAmount()); - List lottos = lottoMachine.issue(money); - resultView.printLottos(lottos); + List tickets = lottoMachine.issue(money); + resultView.printLottos(tickets); Lotto winning = new Lotto(inputView.readWinningNumbers()); + WinningNumbers winningNumbers = new WinningNumbers(winning, inputView.readBonusNumber()); - WinningStatistics stats = new WinningStatistics(); - stats.accumulate(lottos, winning); + WinningStatistics stats = winningNumbers.match(tickets); resultView.printStatistics(stats, money); } diff --git a/src/main/java/lotto/LottoNumber.java b/src/main/java/lotto/LottoNumber.java new file mode 100644 index 0000000000..d4377e5a96 --- /dev/null +++ b/src/main/java/lotto/LottoNumber.java @@ -0,0 +1,38 @@ +package lotto; + +import java.util.Objects; + +public class LottoNumber { + private static final int MIN = 1; + private static final int MAX = 45; + + private final int value; + + private LottoNumber(int value) { + if (value < MIN || value > MAX) { + throw new IllegalArgumentException("로또 번호는 1~45 범위여야 한다."); + } + this.value = value; + } + + public static LottoNumber of(int value) { + return new LottoNumber(value); + } + + public int value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LottoNumber)) return false; + LottoNumber that = (LottoNumber) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/lotto/WinningNumbers.java b/src/main/java/lotto/WinningNumbers.java index 6799605dfa..5769861c92 100644 --- a/src/main/java/lotto/WinningNumbers.java +++ b/src/main/java/lotto/WinningNumbers.java @@ -1,5 +1,7 @@ package lotto; +import java.util.List; + public class WinningNumbers { private final Lotto winning; private final BonusNumber bonus; @@ -20,6 +22,12 @@ public Rank match(Lotto ticket) { return Rank.of(matchCount, bonusMatched); } + public WinningStatistics match(List tickets) { + WinningStatistics stats = new WinningStatistics(); + stats.accumulate(tickets, this); + return stats; + } + private void validateBonusNotDuplicated(Lotto winning, BonusNumber bonus) { if (winning.contains(bonus.value())) { throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없다."); diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index e2e34c19c5..894bd2aeb5 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -11,37 +11,41 @@ public class LottoTest { @Test @DisplayName("로또 번호는 반드시 6개여야 한다") void lottoMustHaveSixNumbers() { - assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5))) + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5))) .isInstanceOf(IllegalArgumentException.class); } @Test @DisplayName("로또 번호는 중복될 수 없다") void lottoNumbersMustBeUnique() { - assertThatThrownBy(() -> new Lotto(List.of(1,1,2,3,4,5))) + assertThatThrownBy(() -> new Lotto(List.of(1, 1, 2, 3, 4, 5))) .isInstanceOf(IllegalArgumentException.class); } @Test - @DisplayName("로또 번호는 1부터 45 사이여야 한다") + @DisplayName("로또 번호는 1~45 범위여야 한다") void lottoNumbersMustBeInRange() { - assertThatThrownBy(() -> new Lotto(List.of(0,1,2,3,4,5))) + assertThatThrownBy(() -> new Lotto(List.of(0, 2, 3, 4, 5, 6))) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> new Lotto(List.of(46, 2, 3, 4, 5, 6))) .isInstanceOf(IllegalArgumentException.class); } @Test - @DisplayName("로또 번호는 오름차순으로 정렬된다") + @DisplayName("로또 번호 목록은 오름차순으로 반환된다") void lottoNumbersAreSorted() { - Lotto lotto = new Lotto(List.of(8, 21, 23, 41, 42, 43)); - assertThat(lotto.numbers()) - .containsExactly(8, 21, 23, 41, 42, 43); + Lotto lotto = new Lotto(List.of(43, 21, 8, 42, 41, 23)); + + assertThat(lotto.numbers()).containsExactly(8, 21, 23, 41, 42, 43); } @Test @DisplayName("당첨 번호와 일치하는 개수를 계산한다") void matchCountCalculation() { - Lotto lotto = new Lotto(List.of(1,2,3,10,11,12)); + Lotto ticket = new Lotto(1, 2, 3, 10, 11, 12); + Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); - assertThat(lotto.matchCount(lotto)).isEqualTo(3); + assertThat(ticket.matchCount(winning)).isEqualTo(3); } } diff --git a/src/test/java/lotto/RankTest.java b/src/test/java/lotto/RankTest.java index 66c4cd1680..64e2e37af3 100644 --- a/src/test/java/lotto/RankTest.java +++ b/src/test/java/lotto/RankTest.java @@ -1,8 +1,8 @@ package lotto; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import java.util.regex.MatchResult; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,37 +10,36 @@ public class RankTest { @Test @DisplayName("6개 일치이면 1등이다") void firstRank() { - assertThat(Rank.of(new MatchResult(6, false))).isEqualTo(Rank.FIRST); + assertThat(Rank.of(6, false)).isEqualTo(Rank.FIRST); } @Test @DisplayName("5개 일치 + 보너스 일치이면 2등이다") void secondRank() { - assertThat(Rank.of(new MatchResult(5, true))).isEqualTo(Rank.SECOND); + assertThat(Rank.of(5, true)).isEqualTo(Rank.SECOND); } @Test @DisplayName("5개 일치 + 보너스 불일치이면 3등이다") void thirdRank() { - assertThat(Rank.of(new MatchResult(5, false))).isEqualTo(Rank.THIRD); + assertThat(Rank.of(5, false)).isEqualTo(Rank.THIRD); } @Test @DisplayName("4개 일치이면 4등이다") void fourthRank() { - assertThat(Rank.of(new MatchResult(4, false))).isEqualTo(Rank.FOURTH); + assertThat(Rank.of(4, false)).isEqualTo(Rank.FOURTH); } @Test @DisplayName("3개 일치이면 5등이다") void fifthRank() { - assertThat(Rank.of(new MatchResult(3, false))).isEqualTo(Rank.FIFTH); + assertThat(Rank.of(3, false)).isEqualTo(Rank.FIFTH); } @Test @DisplayName("2개 이하 일치이면 MISS이다") void missRank() { - assertThat(Rank.of(new MatchResult(2, false))).isEqualTo(Rank.MISS); + assertThat(Rank.of(2, false)).isEqualTo(Rank.MISS); } - } diff --git a/src/test/java/lotto/WinningNumbersTest.java b/src/test/java/lotto/WinningNumbersTest.java index 77bc9d3e0c..371189a7f0 100644 --- a/src/test/java/lotto/WinningNumbersTest.java +++ b/src/test/java/lotto/WinningNumbersTest.java @@ -3,10 +3,12 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class WinningNumbersTest { + @Test @DisplayName("보너스 번호는 당첨 번호와 중복될 수 없다") void bonusMustNotDuplicateWinningNumbers() { @@ -35,4 +37,20 @@ void matchReturnsThirdRank() { assertThat(rank).isEqualTo(Rank.THIRD); } + + @Test + @DisplayName("당첨 번호는 티켓 목록에 대한 당첨 통계를 생성해 반환한다") + void winningNumbersCreatesStatistics() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); + + List tickets = List.of( + new Lotto(1, 2, 3, 4, 5, 7), // 2등 + new Lotto(1, 2, 3, 4, 5, 10) // 3등 + ); + + WinningStatistics stats = winningNumbers.match(tickets); + + assertThat(stats.countOf(Rank.SECOND)).isEqualTo(1); + assertThat(stats.countOf(Rank.THIRD)).isEqualTo(1); + } } diff --git a/src/test/java/lotto/WinningStatisticsTest.java b/src/test/java/lotto/WinningStatisticsTest.java index 8a60bbe53f..c21c172a5c 100644 --- a/src/test/java/lotto/WinningStatisticsTest.java +++ b/src/test/java/lotto/WinningStatisticsTest.java @@ -7,36 +7,34 @@ import org.junit.jupiter.api.Test; public class WinningStatisticsTest { + @Test - @DisplayName("당첨 결과를 등수별로 집계한다") - void accumulateByRank() { + @DisplayName("총 당첨금은 등수별 상금 * 당첨횟수의 합이다") + void totalPrize() { WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7), // 2등 - new Lotto(1, 2, 3, 4, 5, 10) // 3등 + new Lotto(1, 2, 3, 4, 5, 7), // SECOND: 30,000,000 + new Lotto(1, 2, 3, 4, 5, 10) // THIRD: 1,500,000 ); - WinningStatistics stats = new WinningStatistics(); - stats.accumulate(tickets, winningNumbers); + WinningStatistics stats = winningNumbers.match(tickets); - assertThat(stats.countOf(Rank.SECOND)).isEqualTo(1); - assertThat(stats.countOf(Rank.THIRD)).isEqualTo(1); + assertThat(stats.totalPrize()).isEqualTo(31_500_000L); } @Test - @DisplayName("총 당첨금은 등수별 상금 * 당첨횟수의 합이다") - void totalPrize() { + @DisplayName("수익률은 총 당첨금을 구입 금액으로 나눈 값이다") + void profitRate() { WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7), // 30,000,000 - new Lotto(1, 2, 3, 4, 5, 10) // 1,500,000 + new Lotto(1, 2, 3, 4, 5, 7) // 30,000,000 ); - WinningStatistics stats = new WinningStatistics(); - stats.accumulate(tickets, winningNumbers); + WinningStatistics stats = winningNumbers.match(tickets); - assertThat(stats.totalPrize()).isEqualTo(31_500_000L); + Money purchase = Money.of(10_000); + assertThat(stats.profitRate(purchase)).isEqualTo(3000.0); } } From 372b2effe44a4fd66b66af7d41c897a93307de27 Mon Sep 17 00:00:00 2001 From: Junghoon Kim <59908721+kay019@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:45:40 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/lotto/BonusNumber.java | 24 -------- src/main/java/lotto/Lotto.java | 57 +++++++++---------- src/main/java/lotto/LottoController.java | 3 +- src/main/java/lotto/LottoMachine.java | 4 +- src/main/java/lotto/LottoNumber.java | 28 ++++++--- src/main/java/lotto/Lottos.java | 21 +++++++ src/main/java/lotto/MatchResult.java | 19 +++++++ src/main/java/lotto/Rank.java | 50 +++++++--------- src/main/java/lotto/ResultView.java | 4 +- src/main/java/lotto/WinningNumbers.java | 33 ++++++----- src/main/java/lotto/WinningStatistics.java | 31 +++------- src/test/java/lotto/BonusNumberTest.java | 18 ------ src/test/java/lotto/LottoMachineTest.java | 4 +- src/test/java/lotto/LottosTest.java | 25 ++++++++ src/test/java/lotto/RankTest.java | 13 ++--- src/test/java/lotto/WinningNumbersTest.java | 36 +++++------- .../java/lotto/WinningStatisticsTest.java | 25 ++++---- 17 files changed, 198 insertions(+), 197 deletions(-) delete mode 100644 src/main/java/lotto/BonusNumber.java create mode 100644 src/main/java/lotto/Lottos.java create mode 100644 src/main/java/lotto/MatchResult.java delete mode 100644 src/test/java/lotto/BonusNumberTest.java create mode 100644 src/test/java/lotto/LottosTest.java diff --git a/src/main/java/lotto/BonusNumber.java b/src/main/java/lotto/BonusNumber.java deleted file mode 100644 index cc093c8e2e..0000000000 --- a/src/main/java/lotto/BonusNumber.java +++ /dev/null @@ -1,24 +0,0 @@ -package lotto; - -public class BonusNumber { - private final int value; - - private BonusNumber(int value) { - validateRange(value); - this.value = value; - } - - public static BonusNumber of(int value) { - return new BonusNumber(value); - } - - public int value() { - return value; - } - - private void validateRange(int value) { - if (value < 1 || value > 45) { - throw new IllegalArgumentException("보너스 번호는 1~45 범위여야 한다."); - } - } -} diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 50022e548d..5816f18847 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -7,50 +7,49 @@ import java.util.stream.Collectors; public class Lotto { - private static final int SIZE = 6; + private static final int LOTTO_SIZE = 6; - private final Set numbers; + private final Set numbers; public Lotto(List numbers) { - validateSize(numbers); - validateUnique(numbers); - validateRangeByValueObject(numbers); - this.numbers = Set.copyOf(numbers); - } - - public Lotto(int... numbers) { - this(Arrays.stream(numbers).boxed().collect(Collectors.toList())); + this(toLottoNumbers(numbers)); } - public List numbers() { - return numbers.stream() - .sorted() - .collect(Collectors.toList()); + private Lotto(Set numbers) { + validate(numbers); + this.numbers = Set.copyOf(numbers); } - public boolean contains(int number) { - return numbers.contains(number); + private static Set toLottoNumbers(List numbers) { + Set result = new HashSet<>(); + for (int n : numbers) { + result.add(LottoNumber.from(n)); + } + return result; } - public int matchCount(Lotto winning) { - return (int) numbers.stream() - .filter(winning::contains) - .count(); + private void validate(Set numbers) { + if (numbers.size() != LOTTO_SIZE) { + throw new IllegalArgumentException("로또 번호는 중복 없이 " + LOTTO_SIZE + "개여야 합니다."); + } } - private void validateSize(List numbers) { - if (numbers.size() != SIZE) { - throw new IllegalArgumentException("로또 번호는 6개여야 한다."); - } + public boolean contains(LottoNumber number) { + return numbers.contains(number); } - private void validateUnique(List numbers) { - if (new HashSet<>(numbers).size() != SIZE) { - throw new IllegalArgumentException("로또 번호는 중복될 수 없다."); + public int matchCount(Lotto other) { + int count = 0; + for (LottoNumber n : numbers) { + if (other.contains(n)) count++; } + return count; } - private void validateRangeByValueObject(List numbers) { - numbers.forEach(LottoNumber::of); + public List numbers() { + return numbers.stream() + .map(LottoNumber::value) + .sorted() + .collect(Collectors.toList()); } } diff --git a/src/main/java/lotto/LottoController.java b/src/main/java/lotto/LottoController.java index 2f177188ca..d90aa4cec7 100644 --- a/src/main/java/lotto/LottoController.java +++ b/src/main/java/lotto/LottoController.java @@ -17,14 +17,13 @@ public LottoController(InputView inputView, ResultView resultView, LottoMachine public void run() { Money money = Money.of(inputView.readPurchaseAmount()); - List tickets = lottoMachine.issue(money); + Lottos tickets = lottoMachine.issue(money); resultView.printLottos(tickets); Lotto winning = new Lotto(inputView.readWinningNumbers()); WinningNumbers winningNumbers = new WinningNumbers(winning, inputView.readBonusNumber()); WinningStatistics stats = winningNumbers.match(tickets); - resultView.printStatistics(stats, money); } } diff --git a/src/main/java/lotto/LottoMachine.java b/src/main/java/lotto/LottoMachine.java index 42f2764120..e8becac43a 100644 --- a/src/main/java/lotto/LottoMachine.java +++ b/src/main/java/lotto/LottoMachine.java @@ -10,11 +10,11 @@ public LottoMachine(LottoNumberGenerator generator) { this.generator = generator; } - public List issue(Money money) { + public Lottos issue(Money money) { List lottos = new ArrayList<>(); for (int i = 0; i < money.ticketCount(); i++) { lottos.add(new Lotto(generator.generate())); } - return List.copyOf(lottos); + return new Lottos(lottos); } } diff --git a/src/main/java/lotto/LottoNumber.java b/src/main/java/lotto/LottoNumber.java index d4377e5a96..d5063c33b8 100644 --- a/src/main/java/lotto/LottoNumber.java +++ b/src/main/java/lotto/LottoNumber.java @@ -1,22 +1,30 @@ package lotto; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; public class LottoNumber { private static final int MIN = 1; private static final int MAX = 45; + private static final Map CACHE = new ConcurrentHashMap<>(); + private final int value; private LottoNumber(int value) { - if (value < MIN || value > MAX) { - throw new IllegalArgumentException("로또 번호는 1~45 범위여야 한다."); - } this.value = value; } - public static LottoNumber of(int value) { - return new LottoNumber(value); + public static LottoNumber from(int value) { + validateRange(value); + return CACHE.computeIfAbsent(value, LottoNumber::new); + } + + private static void validateRange(int value) { + if (value < MIN || value > MAX) { + throw new IllegalArgumentException("로또 번호는 " + MIN + " ~ " + MAX + " 사이여야 합니다. value=" + value); + } } public int value() { @@ -27,12 +35,16 @@ public int value() { public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof LottoNumber)) return false; - LottoNumber that = (LottoNumber) o; - return value == that.value; + return value == ((LottoNumber) o).value; } @Override public int hashCode() { - return Objects.hash(value); + return Integer.hashCode(value); + } + + @Override + public String toString() { + return String.valueOf(value); } } diff --git a/src/main/java/lotto/Lottos.java b/src/main/java/lotto/Lottos.java new file mode 100644 index 0000000000..bf7d7aec26 --- /dev/null +++ b/src/main/java/lotto/Lottos.java @@ -0,0 +1,21 @@ +package lotto; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Lottos { + private final List values; + + public Lottos(List values) { + this.values = List.copyOf(values); + } + + public List values() { + return values; + } + + public int size() { + return values.size(); + } +} diff --git a/src/main/java/lotto/MatchResult.java b/src/main/java/lotto/MatchResult.java new file mode 100644 index 0000000000..e42b00e6a1 --- /dev/null +++ b/src/main/java/lotto/MatchResult.java @@ -0,0 +1,19 @@ +package lotto; + +public class MatchResult { + private final int matchCount; + private final boolean bonusMatched; + + public MatchResult(int matchCount, boolean bonusMatched) { + this.matchCount = matchCount; + this.bonusMatched = bonusMatched; + } + + public int matchCount() { + return matchCount; + } + + public boolean bonusMatched() { + return bonusMatched; + } +} diff --git a/src/main/java/lotto/Rank.java b/src/main/java/lotto/Rank.java index 75868c6111..712d9aea54 100644 --- a/src/main/java/lotto/Rank.java +++ b/src/main/java/lotto/Rank.java @@ -1,6 +1,7 @@ package lotto; -import java.util.Arrays; + +import lotto.MatchResult; public enum Rank { FIRST(6, false, 2_000_000_000, "6개 일치", 5), @@ -8,23 +9,34 @@ public enum Rank { THIRD(5, false, 1_500_000, "5개 일치", 3), FOURTH(4, false, 50_000, "4개 일치", 2), FIFTH(3, false, 5_000, "3개 일치", 1), - MISS(0, false, 0, "", 99); + MISS(0, false, 0, "", 0); private final int matchCount; - private final boolean bonusMatched; - private final long prize; + private final boolean bonusNeeded; + private final int prize; private final String description; private final int displayOrder; - Rank(int matchCount, boolean bonusMatched, long prize, String description, int displayOrder) { + Rank(int matchCount, boolean bonusNeeded, int prize, String description, int displayOrder) { this.matchCount = matchCount; - this.bonusMatched = bonusMatched; + this.bonusNeeded = bonusNeeded; this.prize = prize; this.description = description; this.displayOrder = displayOrder; } - public long prize() { + public static Rank of(MatchResult result) { + int matchCount = result.matchCount(); + boolean bonusMatched = result.bonusMatched(); + + if (matchCount == 6) return FIRST; + if (matchCount == 5) return bonusMatched ? SECOND : THIRD; + if (matchCount == 4) return FOURTH; + if (matchCount == 3) return FIFTH; + return MISS; + } + + public int prize() { return prize; } @@ -36,29 +48,7 @@ public int displayOrder() { return displayOrder; } - public boolean isWinning() { - return this != MISS; - } - - public static Rank of(int matchCount, boolean bonusMatched) { - return Arrays.stream(values()) - .filter(r -> r.matchCount == matchCount) - .filter(r -> r.bonusMatched == bonusMatched) - .findFirst() - .orElseGet(() -> fallbackWithoutBonus(matchCount)); - } - public static Rank[] winningRanks() { - return Arrays.stream(values()) - .filter(Rank::isWinning) - .toArray(Rank[]::new); - } - - private static Rank fallbackWithoutBonus(int matchCount) { - return Arrays.stream(values()) - .filter(r -> r.matchCount == matchCount) - .filter(r -> !r.bonusMatched) - .findFirst() - .orElse(MISS); + return new Rank[]{FIFTH, FOURTH, THIRD, SECOND, FIRST}; } } diff --git a/src/main/java/lotto/ResultView.java b/src/main/java/lotto/ResultView.java index a322560fe7..6f14fb820d 100644 --- a/src/main/java/lotto/ResultView.java +++ b/src/main/java/lotto/ResultView.java @@ -5,9 +5,9 @@ import java.util.List; public class ResultView { - public void printLottos(List lottos) { + public void printLottos(Lottos lottos) { System.out.println(lottos.size() + "개를 구매했습니다."); - lottos.forEach(lotto -> System.out.println(lotto.numbers())); + lottos.values().forEach(lotto -> System.out.println(lotto.numbers())); } public void printStatistics(WinningStatistics stats, Money money) { diff --git a/src/main/java/lotto/WinningNumbers.java b/src/main/java/lotto/WinningNumbers.java index 5769861c92..51d630a1bc 100644 --- a/src/main/java/lotto/WinningNumbers.java +++ b/src/main/java/lotto/WinningNumbers.java @@ -1,36 +1,39 @@ package lotto; -import java.util.List; + +import lotto.MatchResult; public class WinningNumbers { private final Lotto winning; - private final BonusNumber bonus; + private final LottoNumber bonus; - public WinningNumbers(Lotto winning, BonusNumber bonus) { + public WinningNumbers(Lotto winning, int bonus) { + this(winning, LottoNumber.from(bonus)); + } + + public WinningNumbers(Lotto winning, LottoNumber bonus) { validateBonusNotDuplicated(winning, bonus); this.winning = winning; this.bonus = bonus; } - public WinningNumbers(Lotto winning, int bonus) { - this(winning, BonusNumber.of(bonus)); + private void validateBonusNotDuplicated(Lotto winning, LottoNumber bonus) { + if (winning.contains(bonus)) { + throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없습니다. bonus=" + bonus.value()); + } } public Rank match(Lotto ticket) { int matchCount = ticket.matchCount(winning); - boolean bonusMatched = ticket.contains(bonus.value()); - return Rank.of(matchCount, bonusMatched); + boolean bonusMatched = ticket.contains(bonus); + return Rank.of(new MatchResult(matchCount, bonusMatched)); } - public WinningStatistics match(List tickets) { + public WinningStatistics match(Lottos tickets) { WinningStatistics stats = new WinningStatistics(); - stats.accumulate(tickets, this); - return stats; - } - - private void validateBonusNotDuplicated(Lotto winning, BonusNumber bonus) { - if (winning.contains(bonus.value())) { - throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없다."); + for (Lotto ticket : tickets.values()) { + stats.accumulate(match(ticket)); } + return stats; } } diff --git a/src/main/java/lotto/WinningStatistics.java b/src/main/java/lotto/WinningStatistics.java index 9706c0c595..2af77bc588 100644 --- a/src/main/java/lotto/WinningStatistics.java +++ b/src/main/java/lotto/WinningStatistics.java @@ -1,18 +1,13 @@ package lotto; import java.util.EnumMap; -import java.util.List; import java.util.Map; public final class WinningStatistics { private final Map counts = new EnumMap<>(Rank.class); - public WinningStatistics() { - initCounts(); - } - - public void accumulate(List tickets, WinningNumbers winningNumbers) { - tickets.forEach(ticket -> accumulateOne(ticket, winningNumbers)); + public void accumulate(Rank rank) { + counts.merge(rank, 1, Integer::sum); } public int countOf(Rank rank) { @@ -20,26 +15,14 @@ public int countOf(Rank rank) { } public long totalPrize() { - return counts.entrySet().stream() - .mapToLong(e -> e.getKey().prize() * e.getValue()) - .sum(); + long sum = 0L; + for (Map.Entry e : counts.entrySet()) { + sum += (long) e.getKey().prize() * e.getValue(); + } + return sum; } public double profitRate(Money purchase) { return (double) totalPrize() / purchase.amount(); } - - private void initCounts() { - for (Rank rank : Rank.winningRanks()) { - counts.put(rank, 0); - } - } - - private void accumulateOne(Lotto ticket, WinningNumbers winningNumbers) { - Rank rank = winningNumbers.match(ticket); - if (!rank.isWinning()) { - return; - } - counts.put(rank, countOf(rank) + 1); - } } diff --git a/src/test/java/lotto/BonusNumberTest.java b/src/test/java/lotto/BonusNumberTest.java deleted file mode 100644 index 1367631a84..0000000000 --- a/src/test/java/lotto/BonusNumberTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package lotto; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public class BonusNumberTest { - @Test - @DisplayName("보너스 번호는 1~45 범위를 벗어나면 예외가 발생한다") - void bonusNumberMustBeInRange() { - assertThatThrownBy(() -> BonusNumber.of(0)) - .isInstanceOf(IllegalArgumentException.class); - - assertThatThrownBy(() -> BonusNumber.of(46)) - .isInstanceOf(IllegalArgumentException.class); - } -} diff --git a/src/test/java/lotto/LottoMachineTest.java b/src/test/java/lotto/LottoMachineTest.java index 74ceb633af..c2a4a50347 100644 --- a/src/test/java/lotto/LottoMachineTest.java +++ b/src/test/java/lotto/LottoMachineTest.java @@ -16,8 +16,8 @@ void issueLottosByMoney() { )); LottoMachine machine = new LottoMachine(generator); - List lottos = machine.issue(Money.of(2000)); + Lottos lottos = machine.issue(Money.of(2000)); - assertThat(lottos).hasSize(2); + assertThat(lottos.size()).isEqualTo(2); } } diff --git a/src/test/java/lotto/LottosTest.java b/src/test/java/lotto/LottosTest.java new file mode 100644 index 0000000000..cfda6385a2 --- /dev/null +++ b/src/test/java/lotto/LottosTest.java @@ -0,0 +1,25 @@ +package lotto; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LottosTest { + @Test + @DisplayName("Lottos는 외부 리스트 변경으로부터 안전해야 한다(방어적 복사)") + void defensiveCopy() { + List input = new ArrayList<>(); + input.add(new Lotto(List.of(1,2,3,4,5,6))); + + Lottos lottos = new Lottos(input); + + input.add(new Lotto(List.of(7,8,9,10,11,12))); // 외부 변경 + + assertThatThrownBy(() -> lottos.values().add(new Lotto(List.of(13,14,15,16,17,18)))) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/src/test/java/lotto/RankTest.java b/src/test/java/lotto/RankTest.java index 64e2e37af3..756244c790 100644 --- a/src/test/java/lotto/RankTest.java +++ b/src/test/java/lotto/RankTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import java.util.regex.MatchResult; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,36 +9,36 @@ public class RankTest { @Test @DisplayName("6개 일치이면 1등이다") void firstRank() { - assertThat(Rank.of(6, false)).isEqualTo(Rank.FIRST); + assertThat(Rank.of(new MatchResult(6, false))).isEqualTo(Rank.FIRST); } @Test @DisplayName("5개 일치 + 보너스 일치이면 2등이다") void secondRank() { - assertThat(Rank.of(5, true)).isEqualTo(Rank.SECOND); + assertThat(Rank.of(new MatchResult(5, true))).isEqualTo(Rank.SECOND); } @Test @DisplayName("5개 일치 + 보너스 불일치이면 3등이다") void thirdRank() { - assertThat(Rank.of(5, false)).isEqualTo(Rank.THIRD); + assertThat(Rank.of(new MatchResult(5, false))).isEqualTo(Rank.THIRD); } @Test @DisplayName("4개 일치이면 4등이다") void fourthRank() { - assertThat(Rank.of(4, false)).isEqualTo(Rank.FOURTH); + assertThat(Rank.of(new MatchResult(4, false))).isEqualTo(Rank.FOURTH); } @Test @DisplayName("3개 일치이면 5등이다") void fifthRank() { - assertThat(Rank.of(3, false)).isEqualTo(Rank.FIFTH); + assertThat(Rank.of(new MatchResult(3, false))).isEqualTo(Rank.FIFTH); } @Test @DisplayName("2개 이하 일치이면 MISS이다") void missRank() { - assertThat(Rank.of(2, false)).isEqualTo(Rank.MISS); + assertThat(Rank.of(new MatchResult(2, false))).isEqualTo(Rank.MISS); } } diff --git a/src/test/java/lotto/WinningNumbersTest.java b/src/test/java/lotto/WinningNumbersTest.java index 371189a7f0..70ec2a800a 100644 --- a/src/test/java/lotto/WinningNumbersTest.java +++ b/src/test/java/lotto/WinningNumbersTest.java @@ -12,41 +12,31 @@ public class WinningNumbersTest { @Test @DisplayName("보너스 번호는 당첨 번호와 중복될 수 없다") void bonusMustNotDuplicateWinningNumbers() { - Lotto winning = new Lotto(1, 2, 3, 4, 5, 6); + Lotto winning = new Lotto(List.of(1, 2, 3, 4, 5, 6)); assertThatThrownBy(() -> new WinningNumbers(winning, 6)) .isInstanceOf(IllegalArgumentException.class); } @Test - @DisplayName("5개 일치 + 보너스 일치면 2등을 반환한다") - void matchReturnsSecondRank() { - WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); + @DisplayName("WinningNumbers.match(ticket)은 Rank를 반환한다") + void matchReturnsRank() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(List.of(1,2,3,4,5,6)), 7); - Rank rank = winningNumbers.match(new Lotto(1, 2, 3, 4, 5, 7)); + Rank rank = winningNumbers.match(new Lotto(List.of(1,2,3,4,5,7))); assertThat(rank).isEqualTo(Rank.SECOND); } @Test - @DisplayName("5개 일치 + 보너스 불일치면 3등을 반환한다") - void matchReturnsThirdRank() { - WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); - - Rank rank = winningNumbers.match(new Lotto(1, 2, 3, 4, 5, 10)); - - assertThat(rank).isEqualTo(Rank.THIRD); - } - - @Test - @DisplayName("당첨 번호는 티켓 목록에 대한 당첨 통계를 생성해 반환한다") - void winningNumbersCreatesStatistics() { - WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); - - List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7), // 2등 - new Lotto(1, 2, 3, 4, 5, 10) // 3등 - ); + @DisplayName("WinningNumbers.match(lottos)은 통계를 만들어 반환한다") + void matchLottosReturnsStatistics() { + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(List.of(1,2,3,4,5,6)), 7); + + Lottos tickets = new Lottos(List.of( + new Lotto(List.of(1,2,3,4,5,7)), // SECOND + new Lotto(List.of(1,2,3,4,5,10)) // THIRD + )); WinningStatistics stats = winningNumbers.match(tickets); diff --git a/src/test/java/lotto/WinningStatisticsTest.java b/src/test/java/lotto/WinningStatisticsTest.java index c21c172a5c..fc43e69a30 100644 --- a/src/test/java/lotto/WinningStatisticsTest.java +++ b/src/test/java/lotto/WinningStatisticsTest.java @@ -11,30 +11,33 @@ public class WinningStatisticsTest { @Test @DisplayName("총 당첨금은 등수별 상금 * 당첨횟수의 합이다") void totalPrize() { - WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(List.of(1,2,3,4,5,6)), 7); - List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7), // SECOND: 30,000,000 - new Lotto(1, 2, 3, 4, 5, 10) // THIRD: 1,500,000 - ); + Lottos tickets = new Lottos(List.of( + new Lotto(List.of(1,2,3,4,5,7)), // SECOND + new Lotto(List.of(1,2,3,4,5,10)) // THIRD + )); WinningStatistics stats = winningNumbers.match(tickets); - assertThat(stats.totalPrize()).isEqualTo(31_500_000L); + long expected = (long) Rank.SECOND.prize() + (long) Rank.THIRD.prize(); + assertThat(stats.totalPrize()).isEqualTo(expected); } @Test @DisplayName("수익률은 총 당첨금을 구입 금액으로 나눈 값이다") void profitRate() { - WinningNumbers winningNumbers = new WinningNumbers(new Lotto(1, 2, 3, 4, 5, 6), 7); + WinningNumbers winningNumbers = new WinningNumbers(new Lotto(List.of(1,2,3,4,5,6)), 7); - List tickets = List.of( - new Lotto(1, 2, 3, 4, 5, 7) // 30,000,000 - ); + Lottos tickets = new Lottos(List.of( + new Lotto(List.of(1,2,3,4,5,7)) // SECOND + )); WinningStatistics stats = winningNumbers.match(tickets); Money purchase = Money.of(10_000); - assertThat(stats.profitRate(purchase)).isEqualTo(3000.0); + double expected = (double) Rank.SECOND.prize() / purchase.amount(); + + assertThat(stats.profitRate(purchase)).isEqualTo(expected); } }