diff --git a/docs/README.md b/docs/README.md index e69de29bb2..6565a568d8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,22 @@ +## 기능 목록 + +- [x] 로또 Lotto + - [x] 여섯 개인지 검증 - validateCounts() + - [x] 로또 번호에 중복된 숫자가 있는지 검증 - validateDuplication() + - [x] 정답과 비교 후 등수 반환 - calculateRank() + - [x] 3항 연산자 제거 +- [x] 로또 판매기 Seller + - [x] 금액에 받아 구매 가능한 로또 개수 반환 - calculateLottoCount() + - [x] 로또 발행 - getLotto() + - [x] 오름차순으로 발행 + - [x] 개수만큼 로또 발행 - getLottos() + - [x] 로또 당첨 통계 반환 - getResult() + - [x] 수익률 계산 - calculateRate() + - [x] 수익률은 소수점 둘째자리에서 반올림 +- [x] UI + - [x] 구입금액 입력받기 - receivePurchaseMoney() + - [x] 구매한 로또 출력 - printBoughtLottos() + - [x] 당첨 번호 입력받기 - receiveAnswers() + - [x] 보너스 번호 입력받기 - receiveBonus() + - [x] 당첨 통계 출력 - printResult() + - [x] 수익률 출력 - printRate() \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..fd28b6ffca 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,31 @@ package lotto; +import java.util.List; + public class Application { + + public static final int LOTTO_PRICE = 1_000; + public static void main(String[] args) { - // TODO: 프로그램 구현 + + String purchaseMoneyStr = UI.receivePurchaseMoney(); + Seller seller = new Seller(LOTTO_PRICE); + + try { + int purchaseCount = seller.calculateLottoCount(seller.parsePurchaseMoneyStr(purchaseMoneyStr)); + + List lottos = seller.getLottos(purchaseCount); + UI.printBoughtLottos(lottos); + + List answers = UI.receiveAnswers(); + int bonus = UI.receiveBonus(); + + List result = seller.getResult(lottos, answers, bonus); + UI.printResult(result); + UI.printRate(seller.calculateRate(result, purchaseCount)); + } catch (IllegalArgumentException e) { + UI.handlingIllegalArgumentException(e); + } + } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 519793d1f7..ea2e0713e7 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -1,6 +1,9 @@ package lotto; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class Lotto { private final List numbers; @@ -11,10 +14,59 @@ public Lotto(List numbers) { } private void validate(List numbers) { + validateCounts(numbers); + validateDuplication(numbers); + } + + private void validateCounts(List numbers) { if (numbers.size() != 6) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("로또 번호의 개수는 6개여야 합니다."); + } + } + + private void validateDuplication(List numbers) { + if (new HashSet<>(numbers).size() != numbers.size()) { + throw new IllegalArgumentException("로또 번호는 중복되어선 안됩니다."); + } + } + + public int calculateRank(List answers, int bonus) { + + Set set = new HashSet<>(numbers); + + int correct = getCorrect(answers, set); + int bonusCorrect = getBonusCorrect(bonus, set, correct); + + return rankByCondition(correct, bonusCorrect); + } + + private static int getCorrect(List answers, Set set) { + return (int) answers.stream() + .filter(set::contains) + .count(); + } + + private static int getBonusCorrect(int bonus, Set set, int correct) { + if (set.contains(bonus) && correct == 5) { + return 1; } + return 0; } - // TODO: 추가 기능 구현 + private int rankByCondition(int correct, int bonusCorrect) { + if (correct == 6) { + return 1; + } + + if (correct + bonusCorrect <= 2) { + return 6; + } + + return 8 - (correct + bonusCorrect); + } + + @Override + public String toString() { + return numbers.toString(); + } } diff --git a/src/main/java/lotto/Seller.java b/src/main/java/lotto/Seller.java new file mode 100644 index 0000000000..bb61f7a343 --- /dev/null +++ b/src/main/java/lotto/Seller.java @@ -0,0 +1,65 @@ +package lotto; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Seller { + + private final int lottoPrice; + private static final List prize = List.of(2_000_000_000, 30_000_000, 1_500_000, 50_000, 5_000, 0); + + public Seller(int lottoPrice) { + this.lottoPrice = lottoPrice; + } + + public int parsePurchaseMoneyStr(String purchaseMoneyStr) { + try { + return Integer.parseInt(purchaseMoneyStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("입력은 1000으로 나눠지는 값이어야 합니다."); + } + } + + public int calculateLottoCount(int money) { + return money / lottoPrice; + } + + public Lotto getLotto() { + List randoms = new ArrayList<>(Randoms.pickUniqueNumbersInRange(1, 45, 6)); + Collections.sort(randoms); + return new Lotto(randoms); + } + + public List getLottos(int count) { + return IntStream.range(0, count) + .mapToObj(i -> getLotto()) + .collect(Collectors.toList()); + } + + public List getResult(List lottos, List answers, int bonus) { + List result = new ArrayList<>(List.of(0, 0, 0, 0, 0, 0)); + + for (Lotto lotto: lottos) { + int index = lotto.calculateRank(answers, bonus) - 1; + result.set(index, result.get(index) + 1); + } + return result; + } + + public BigDecimal calculateRate(List ranks, int count) { + + long prizeSum = IntStream.range(0, 6) + .mapToLong(index -> (long) ranks.get(index) * prize.get(index)) + .sum(); + int cost = lottoPrice * count; + + double rate = prizeSum * 100 / (double) cost; + return new BigDecimal(rate).setScale(1, BigDecimal.ROUND_HALF_UP); + } +} diff --git a/src/main/java/lotto/UI.java b/src/main/java/lotto/UI.java new file mode 100644 index 0000000000..34b1b1080c --- /dev/null +++ b/src/main/java/lotto/UI.java @@ -0,0 +1,62 @@ +package lotto; + +import camp.nextstep.edu.missionutils.Console; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class UI { + + private static final Map resultOutputByRank = Map.of( + 1, "6개 일치 (2,000,000,000원)", + 2, "5개 일치, 보너스 볼 일치 (30,000,000원)", + 3, "5개 일치 (1,500,000원)", + 4, "4개 일치 (50,000원)", + 5, "3개 일치 (5,000원)" + ); + + public static String receivePurchaseMoney() { + System.out.println("구입금액을 입력해 주세요."); + return Console.readLine(); + } + + public static void printBoughtLottos(List lottos) { + System.out.println("\n" + lottos.size() + "개를 구매했습니다."); + for (Lotto lotto : lottos) { + System.out.println(lotto); + } + } + + public static List receiveAnswers() { + System.out.println("\n당첨 번호를 입력해 주세요."); + String[] split = Console.readLine().split(","); + return Arrays.stream(split) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + public static int receiveBonus() { + System.out.println("\n보너스 번호를 입력해 주세요."); + return Integer.parseInt(Console.readLine()); + } + + public static void printResult(List result) { + System.out.println("\n당첨 통계"); + System.out.println("---"); + + for (int index = 4; index >= 0; index--) { + System.out.println(resultOutputByRank.get(index + 1) + " - " + result.get(index) + "개"); + } + } + + public static void printRate(BigDecimal rate) { + System.out.println("총 수익률은 " + rate.toPlainString() + "%입니다."); + } + + public static void handlingIllegalArgumentException(IllegalArgumentException e) { + System.out.println("[ERROR] " + e.getMessage()); + } +} diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 0f3af0f6c4..23f83bb657 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -1,10 +1,12 @@ package lotto; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class LottoTest { @@ -18,10 +20,97 @@ void createLottoByOverSize() { @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") @Test void createLottoByDuplicatedNumber() { - // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) .isInstanceOf(IllegalArgumentException.class); } - // 아래에 추가 테스트 작성 가능 + @DisplayName("6개 일치하면, 1을 반환한다.") + @Test + void calculateRank_1st() { + //given + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List answers = List.of(1, 2, 3, 4, 5, 6); + int bonus = 7; + + //when + int rank = lotto.calculateRank(answers, bonus); + + //then + assertThat(rank).isEqualTo(1); + } + + @DisplayName("5개 일치하고, 보너스 볼 일치하면, 2을 반환한다.") + @Test + void calculateRank_2nd() { + //given + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 7)); + List answers = List.of(1, 2, 3, 4, 5, 6); + int bonus = 7; + + //when + int rank = lotto.calculateRank(answers, bonus); + + //then + assertThat(rank).isEqualTo(2); + } + + @DisplayName("5개 일치하면, 3을 반환한다.") + @Test + void calculateRank_3rd() { + //given + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 8)); + List answers = List.of(1, 2, 3, 4, 5, 6); + int bonus = 7; + + //when + int rank = lotto.calculateRank(answers, bonus); + + //then + assertThat(rank).isEqualTo(3); + } + + @DisplayName("4개 일치하면, 4을 반환한다.") + @Test + void calculateRank_4th() { + //given + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 7, 8)); + List answers = List.of(1, 2, 3, 4, 5, 6); + int bonus = 7; + + //when + int rank = lotto.calculateRank(answers, bonus); + + //then + assertThat(rank).isEqualTo(4); + } + + @DisplayName("3개 일치하면, 5을 반환한다.") + @Test + void calculateRank_5th() { + //given + Lotto lotto = new Lotto(List.of(1, 2, 3, 7, 8, 9)); + List answers = List.of(1, 2, 3, 4, 5, 6); + int bonus = 7; + + //when + int rank = lotto.calculateRank(answers, bonus); + + //then + assertThat(rank).isEqualTo(5); + } + + @DisplayName("2개 이하 일치하면, 6을 반환한다.") + @Test + void calculateRank_6th() { + //given + Lotto lotto = new Lotto(List.of(1, 2, 7, 8, 9, 10)); + List answers = List.of(1, 2, 3, 4, 5, 6); + int bonus = 7; + + //when + int rank = lotto.calculateRank(answers, bonus); + + //then + assertThat(rank).isEqualTo(6); + } } diff --git a/src/test/java/lotto/SellerTest.java b/src/test/java/lotto/SellerTest.java new file mode 100644 index 0000000000..c10dfab307 --- /dev/null +++ b/src/test/java/lotto/SellerTest.java @@ -0,0 +1,162 @@ +package lotto; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.util.CollectionUtils; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomUniqueNumbersInRangeTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class SellerTest { + + @DisplayName("받은 금액을 lottoPrice로 나눈 몫을 반환한다.") + @Test + void calculateLottoCount() { + //given + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + int money = 10_000; + + //when + int count = seller.calculateLottoCount(money); + + //then + assertThat(count).isEqualTo(10); + } + + @DisplayName("random한 Lotto 한 개 반환") + @Test + void getLotto() { + //given + List answers = List.of(8, 21, 23, 41, 42, 43); + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + assertRandomUniqueNumbersInRangeTest( + () -> { + //when + Lotto lotto = seller.getLotto(); + + //then + int rank = lotto.calculateRank(answers, -1); + assertThat(rank).isEqualTo(1); + }, + answers + ); + } + + @DisplayName("발행된 로또는 오름차순이어야한다.") + @Test + void getLotto_Ascending() { + //given + List answers = List.of(8, 21, 23, 41, 42, 43); + List unsortedAnswers = List.of(42, 21, 43, 41, 8, 23); + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + assertRandomUniqueNumbersInRangeTest( + () -> { + //when + Lotto lotto = seller.getLotto(); + + //then + assertThat(lotto.toString()).isEqualTo(answers.toString()); + }, + unsortedAnswers + ); + } + + @DisplayName("개수만큼의 로또를 반환한다.") + @Test + void getLottos() { + //given + int count = 10; + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + //when + List lottos = seller.getLottos(10); + + //then + assertThat(lottos.size()).isEqualTo(count); + } + + @DisplayName("반환된 길이는 6이다.") + @Test + void getResult_length() { + //given + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + //when + List result = seller.getResult(List.of(), List.of(), -1); + + //then + assertThat(result.size()).isEqualTo(6); + } + + @DisplayName("1~6등의 개수를 반환한다.") + @Test + void getResult() { + //given + List answers = List.of(8, 21, 23, 41, 42, 43); + List second = List.of(8, 21, 23, 41, 42, 44); + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + assertRandomUniqueNumbersInRangeTest( + () -> { + List lottos = seller.getLottos(2); + + //when + List result = seller.getResult(lottos, answers, 44); + + //then + assertThat(result.equals(List.of(1, 1, 0, 0, 0, 0))).isTrue(); + }, + answers, + second + ); + } + + @DisplayName("수익률을 계산해 반환한다.") + @Test + void calculateRate() { + //given + List result = List.of(1, 1, 0, 0, 0, 0); + int count = 2; + double expectedRate = (2_000_000_000 + 30_000_000) / (double) 2_000 * 100; + + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + //when + BigDecimal rate = seller.calculateRate(result, count); + + //then + assertThat(rate.doubleValue()).isEqualTo(expectedRate); + } + + @DisplayName("수익률은 소수점 둘째자리에서 반올림") + @Test + void calculateRate_decimal() { + //given + List result = List.of(1, 1, 0, 0, 0, 0); + int count = 3; + + final int lottoPrice = 1_000; + Seller seller = new Seller(lottoPrice); + + //when + BigDecimal rate = seller.calculateRate(result, count); + + //then + assertThat(rate.toPlainString().split("\\.")[1].length()).isEqualTo(1); + } +} \ No newline at end of file