diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..d1b17173d0 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,55 @@ package lotto; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lotto.domain.Lotto; +import lotto.domain.Money; +import lotto.domain.WinNumbers; +import lotto.domain.lottonumbercreator.AutoLottoNumberCreator; +import lotto.domain.lottonumbercreator.LottoNumberCreator; +import lotto.domain.lottorank.LottoRank; +import lotto.domain.lottorank.LottoRanks; +import lotto.view.InputView; + public class Application { + private static final LottoNumberCreator CREATOR = new AutoLottoNumberCreator(); + public static void main(String[] args) { - // TODO: 프로그램 구현 + System.out.println("구입금액을 입력해 주세요."); + + final Money money = new Money(InputView.requestInteger()); + final int lottoCount = money.getLottoCount(); + System.out.println(String.format("\n%d개를 구매했습니다.", lottoCount)); + + final List lottos = getAutoLottos(lottoCount); + lottos.forEach(System.out::println); + + System.out.println("\n당첨 번호를 입력해 주세요."); + final Lotto winLotto = new Lotto(InputView.requestWinNumbers()); + + System.out.println("\n보너스 번호를 입력해 주세요."); + int bonusBall = InputView.requestInteger(); + + final WinNumbers winNumbers = new WinNumbers(winLotto, bonusBall); + final LottoRanks lottoRanks = getLottoRanks(lottos, winNumbers); + + System.out.println("당첨 통계\n---"); + System.out.println(lottoRanks); + System.out.println(String.format("총 수익률은 %.1f%%입니다.", money.getRateOfProfit(lottoRanks.getProfit()))); + } + + private static List getAutoLottos(int lottoCount) { + return Stream.generate(() -> Lotto.from(CREATOR)) + .limit(lottoCount) + .collect(Collectors.toList()); + } + + private static LottoRanks getLottoRanks(List lottos, WinNumbers winNumbers) { + final List ranks = lottos.stream() + .map(lotto -> LottoRank.calculateRank(winNumbers, lotto)) + .collect(Collectors.toList()); + + return LottoRanks.from(ranks); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 519793d1f7..0000000000 --- a/src/main/java/lotto/Lotto.java +++ /dev/null @@ -1,20 +0,0 @@ -package lotto; - -import java.util.List; - -public class Lotto { - private final List numbers; - - public Lotto(List numbers) { - validate(numbers); - this.numbers = numbers; - } - - private void validate(List numbers) { - if (numbers.size() != 6) { - throw new IllegalArgumentException(); - } - } - - // TODO: 추가 기능 구현 -} diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000000..46f06c6fa9 --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,75 @@ +package lotto.domain; + +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import lotto.domain.lottonumbercreator.LottoNumberCreator; + +public class Lotto { + private static final int LOTTO_NUMBERS_SIZE = 6; + private static final int MIN_LOTTO_NUMBER = 1; + private static final int MAX_LOTTO_NUMBER = 45; + + private final List numbers; + + public Lotto(List numbers) { + validateSize(numbers); + validateDuplicated(numbers); + validateNumbersRange(numbers); + this.numbers = numbers; + } + + public static Lotto from(LottoNumberCreator creator) { + return new Lotto(creator.create(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER, LOTTO_NUMBERS_SIZE)); + } + + private void validateSize(List numbers) { + if (numbers.size() != LOTTO_NUMBERS_SIZE) { + throw new IllegalArgumentException(String.format("[ERROR] 로또 숫자의 개수는 %d일 수 없습니다.", numbers.size())); + } + } + + private void validateDuplicated(List numbers) { + if (numbers.size() != new HashSet<>(numbers).size()) { + throw new IllegalArgumentException("[ERROR] 중복되는 숫자가 존재합니다."); + } + } + + private void validateNumbersRange(List numbers) { + if (!numbers.stream().allMatch(this::isInRange)) { + throw new IllegalArgumentException( + String.format("[ERROR] %d ~ %d 범위를 넘어가는 숫자가 존재합니다.", MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER) + ); + } + } + + private boolean isInRange(int bonusBall) { + return bonusBall >= MIN_LOTTO_NUMBER && bonusBall <= MAX_LOTTO_NUMBER; + } + + public boolean isPossible(int bonusBall) { + final boolean isInRange = isInRange(bonusBall); + final boolean isNotLottoNumber = !numbers.contains(bonusBall); + + return isInRange && isNotLottoNumber; + } + + public int calculateSameLottoNumber(Lotto other) { + return (int) other.numbers.stream() + .filter(this::contain) + .count(); + } + + public boolean contain(Integer number) { + return this.numbers.contains(number); + } + + @Override + public String toString() { + return String.format("[%s]", + this.numbers.stream() + .sorted() + .map(number -> Integer.toString(number)) + .collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/lotto/domain/Money.java b/src/main/java/lotto/domain/Money.java new file mode 100644 index 0000000000..8d5595f549 --- /dev/null +++ b/src/main/java/lotto/domain/Money.java @@ -0,0 +1,25 @@ +package lotto.domain; + +public class Money { + private static final int MIN_VALUE = 1000; + private final int value; + + public Money(int value) { + validate(value); + this.value = value; + } + + private void validate(int value) { + if (value < MIN_VALUE || value % MIN_VALUE != 0) { + throw new IllegalArgumentException(String.format("[ERROR] 금액은 %d원일 수 없습니다.", value)); + } + } + + public int getLottoCount() { + return this.value / MIN_VALUE; + } + + public double getRateOfProfit(int profit) { + return (double) profit * 100 / value; + } +} diff --git a/src/main/java/lotto/domain/WinNumbers.java b/src/main/java/lotto/domain/WinNumbers.java new file mode 100644 index 0000000000..0eaef90f88 --- /dev/null +++ b/src/main/java/lotto/domain/WinNumbers.java @@ -0,0 +1,28 @@ +package lotto.domain; + +public class WinNumbers { + + private final Lotto lotto; + + private final int bonusBall; + + public WinNumbers(Lotto lotto, int bonusBall) { + validateBonusBall(lotto, bonusBall); + this.lotto = lotto; + this.bonusBall = bonusBall; + } + + private void validateBonusBall(Lotto lotto, int bonusBall) { + if (!lotto.isPossible(bonusBall)) { + throw new IllegalArgumentException(String.format("[ERROR] %d는 보너스 볼이 될 수 없습니다.", bonusBall)); + } + } + + public int calculateSameLottoNumber(Lotto lotto) { + return this.lotto.calculateSameLottoNumber(lotto); + } + + public boolean matchBonus(Lotto lotto) { + return lotto.contain(bonusBall); + } +} diff --git a/src/main/java/lotto/domain/lottonumbercreator/AutoLottoNumberCreator.java b/src/main/java/lotto/domain/lottonumbercreator/AutoLottoNumberCreator.java new file mode 100644 index 0000000000..944e7e18f2 --- /dev/null +++ b/src/main/java/lotto/domain/lottonumbercreator/AutoLottoNumberCreator.java @@ -0,0 +1,11 @@ +package lotto.domain.lottonumbercreator; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.List; + +public class AutoLottoNumberCreator implements LottoNumberCreator{ + @Override + public List create(int minLottoNumber, int maxLottoNumber, int lottoNumbersSize) { + return Randoms.pickUniqueNumbersInRange(minLottoNumber, maxLottoNumber, lottoNumbersSize); + } +} diff --git a/src/main/java/lotto/domain/lottonumbercreator/LottoNumberCreator.java b/src/main/java/lotto/domain/lottonumbercreator/LottoNumberCreator.java new file mode 100644 index 0000000000..b7e817f865 --- /dev/null +++ b/src/main/java/lotto/domain/lottonumbercreator/LottoNumberCreator.java @@ -0,0 +1,7 @@ +package lotto.domain.lottonumbercreator; + +import java.util.List; + +public interface LottoNumberCreator { + List create(int minLottoNumber, int maxLottoNumber, int lottoNumbersSize); +} diff --git a/src/main/java/lotto/domain/lottorank/LottoRank.java b/src/main/java/lotto/domain/lottorank/LottoRank.java new file mode 100644 index 0000000000..13e3efaeb8 --- /dev/null +++ b/src/main/java/lotto/domain/lottorank/LottoRank.java @@ -0,0 +1,43 @@ +package lotto.domain.lottorank; + +import java.util.Arrays; +import lotto.domain.Lotto; +import lotto.domain.WinNumbers; + +public enum LottoRank { + FIRST(2_000_000_000, 6, false), + SECOND(30_000_000, 5,true), + THIRD(1_500_000, 5, false), + FOURTH(50_000, 4, false), + FIFTH(5_000, 3, false), + OTHER(0, 0, false); + + + private final int prize; + private final int count; + private final boolean bonus; + + LottoRank(int prize, int count, boolean bonus) { + this.prize = prize; + this.count = count; + this.bonus = bonus; + } + + public static LottoRank calculateRank(WinNumbers winNumbers, Lotto lotto) { + return Arrays.stream(LottoRank.values()) + .filter(rank -> rank.count == winNumbers.calculateSameLottoNumber(lotto)) + .filter(rank -> !rank.bonus || winNumbers.matchBonus(lotto)) + .findAny() + .orElse(LottoRank.OTHER); + } + + + public int getPrize() { + return prize; + } + + @Override + public String toString() { + return String.format("%d개 일치%s (%,d원)", this.count, this.bonus ? ", 보너스 볼 일치" : "", this.prize); + } +} diff --git a/src/main/java/lotto/domain/lottorank/LottoRanks.java b/src/main/java/lotto/domain/lottorank/LottoRanks.java new file mode 100644 index 0000000000..c79199b358 --- /dev/null +++ b/src/main/java/lotto/domain/lottorank/LottoRanks.java @@ -0,0 +1,39 @@ +package lotto.domain.lottorank; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class LottoRanks { + + private final Map ranks; + + private LottoRanks(Map ranks) { + this.ranks = ranks; + } + + public static LottoRanks from(List ranks) { + EnumMap lottoRankMap = new EnumMap<>(LottoRank.class); + Arrays.stream(LottoRank.values()).forEach(lottoRank -> lottoRankMap.put(lottoRank, 0)); + + ranks.forEach(rank -> lottoRankMap.computeIfPresent(rank, (key, value) -> value + 1)); + return new LottoRanks(lottoRankMap); + } + + public int getProfit() { + return this.ranks.entrySet() + .stream() + .mapToInt(entry -> entry.getKey().getPrize() * entry.getValue()) + .sum(); + } + + @Override + public String toString() { + return ranks.entrySet() + .stream() + .map(entry -> String.format("%s - %d개", entry.getKey().toString(), entry.getValue())) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 0000000000..3421cafc04 --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,35 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class InputView { + private static final String DELIMITER = ","; + + private InputView() { + throw new UnsupportedOperationException(); + } + + public static int requestInteger() { + final String input = Console.readLine(); + return parseInt(input); + } + + public static List requestWinNumbers() { + final String input = Console.readLine(); + + return Arrays.stream(input.split(DELIMITER)) + .map(InputView::parseInt) + .collect(Collectors.toList()); + } + + private static int parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format("[ERROR] 입력값 %s는 숫자가 아닙니다.", value)); + } + } +} diff --git a/src/test/java/lotto/ApplicationTest.java b/src/test/java/lotto/ApplicationTest.java index a15c7d1f52..546eb4a2b2 100644 --- a/src/test/java/lotto/ApplicationTest.java +++ b/src/test/java/lotto/ApplicationTest.java @@ -8,6 +8,7 @@ import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomUniqueNumbersInRangeTest; import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class ApplicationTest extends NsTest { private static final String ERROR_MESSAGE = "[ERROR]"; @@ -48,10 +49,9 @@ class ApplicationTest extends NsTest { @Test void 예외_테스트() { - assertSimpleTest(() -> { - runException("1000j"); - assertThat(output()).contains(ERROR_MESSAGE); - }); + assertSimpleTest(() -> assertThatThrownBy(() -> runException("1000j")) + .isInstanceOf(IllegalArgumentException.class) + ); } @Override diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java deleted file mode 100644 index 0f3af0f6c4..0000000000 --- a/src/test/java/lotto/LottoTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package lotto; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class LottoTest { - @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.") - @Test - void createLottoByOverSize() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") - @Test - void createLottoByDuplicatedNumber() { - // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) - .isInstanceOf(IllegalArgumentException.class); - } - - // 아래에 추가 테스트 작성 가능 -} diff --git a/src/test/java/lotto/domain/LottoRankTest.java b/src/test/java/lotto/domain/LottoRankTest.java new file mode 100644 index 0000000000..6c0a6cdf0c --- /dev/null +++ b/src/test/java/lotto/domain/LottoRankTest.java @@ -0,0 +1,40 @@ +package lotto.domain; + +import java.util.List; +import java.util.stream.Stream; +import lotto.domain.lottorank.LottoRank; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LottoRankTest { + + public static Stream findRank() { + return Stream.of( + Arguments.of(List.of(1, 2, 3, 4, 5, 6), LottoRank.FIRST), + Arguments.of(List.of(1, 2, 3, 4, 5, 7), LottoRank.SECOND), + Arguments.of(List.of(1, 2, 3, 4, 5, 8), LottoRank.THIRD), + Arguments.of(List.of(1, 2, 3, 4, 7, 8), LottoRank.FOURTH), + Arguments.of(List.of(1, 2, 3, 7, 8, 9), LottoRank.FIFTH), + Arguments.of(List.of(1, 2, 7, 8, 9, 10), LottoRank.OTHER) + ); + } + + @ParameterizedTest + @MethodSource("findRank") + void 로또_당첨_등수를_확인한다(List numbers, LottoRank expect) { + // given + final Lotto winLotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + final int bonusBall = 7; + final WinNumbers winNumbers = new WinNumbers(winLotto, bonusBall); + + final Lotto lotto = new Lotto(numbers); + + // when + final LottoRank actual = LottoRank.calculateRank(winNumbers, lotto); + + // then + Assertions.assertThat(actual).isEqualTo(expect); + } +} diff --git a/src/test/java/lotto/domain/LottoTest.java b/src/test/java/lotto/domain/LottoTest.java new file mode 100644 index 0000000000..8123ecec6e --- /dev/null +++ b/src/test/java/lotto/domain/LottoTest.java @@ -0,0 +1,55 @@ +package lotto.domain; + +import lotto.domain.Lotto; +import lotto.domain.lottonumbercreator.LottoNumberCreator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTest { + @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.") + @Test + void createLottoByOverSize() { + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 숫자의 개수는 7일 수 없습니다."); + } + + @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") + @Test + void createLottoByDuplicatedNumber() { + // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 중복되는 숫자가 존재합니다."); + } + + @ParameterizedTest + @CsvSource(value = {"0,1,2,3,4,5", "1,2,3,4,5,46"}) + void 로또_번호의_범위를_벗어나는_숫자가_있으면_예외를_던진다(Integer n1, Integer n2, Integer n3, Integer n4, Integer n5, Integer n6) { + assertThatThrownBy(() -> new Lotto(List.of(n1, n2, n3, n4, n5, n6))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 1 ~ 45 범위를 넘어가는 숫자가 존재합니다."); + } + + @Test + void 자동_로또를_생성한다() { + // given + final List numbers = List.of(1, 2, 3, 4, 5, 6); + LottoNumberCreator creator = (min, max, size) -> numbers; + + // when + final Lotto lotto = Lotto.from(creator); + + // then + assertThat(lotto).extracting("numbers") + .isEqualTo(numbers); + } +} diff --git a/src/test/java/lotto/domain/MoneyTest.java b/src/test/java/lotto/domain/MoneyTest.java new file mode 100644 index 0000000000..d6b341e81b --- /dev/null +++ b/src/test/java/lotto/domain/MoneyTest.java @@ -0,0 +1,22 @@ +package lotto.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MoneyTest { + + @ParameterizedTest + @ValueSource(ints = {-1, 0, 999, 1001, 10001}) + void 금액이_1000원_미만이거나_1000으로_나눠지지_않는_경우_예외를_던진다(int value) { + Assertions.assertThatThrownBy(() -> new Money(value)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(ints = {1000, 12000}) + void 금액이_1000원으로_나눠지는_경우_성공(int value) { + Assertions.assertThatCode(() -> new Money(value)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/lotto/domain/WinNumbersTest.java b/src/test/java/lotto/domain/WinNumbersTest.java new file mode 100644 index 0000000000..10f49b1153 --- /dev/null +++ b/src/test/java/lotto/domain/WinNumbersTest.java @@ -0,0 +1,22 @@ +package lotto.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class WinNumbersTest { + + @Test + void 보너스볼이_로또_넘버와_중복되면_예외를_던진다() { + // given + final Integer bonusBall = 6; + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, bonusBall)); + + // when & then + Assertions.assertThatThrownBy(() -> new WinNumbers(lotto, bonusBall)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] %d는 보너스 볼이 될 수 없습니다.", bonusBall); + } +}