diff --git a/README.md b/README.md index 5fa2560b46..efe641a241 100644 --- a/README.md +++ b/README.md @@ -1 +1,29 @@ # java-lotto-precourse + +validate +* purchase amount + - ensure it only composed of digits + - ensure it's int range + - ensure % 1000 == 0 + + +* lotto numbers + - ensure each numbers are in range(1, 45) + - ensure number of lotto numbers are 6 + - ensure numbers are not duplicate + - ensure it's delimited by ',' + + +* bonus number + - ensure each numbers are in range(1, 45) + - ensure it's not in lotto number + +generate numbers + - generate 6 numbers and return the numbers + +calculate result + - see how many numbers match with lotto numbers + +calculate RoR + - profit : prize / money spent + - return profit diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..04b07e42ff 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,39 @@ package lotto; +import camp.nextstep.edu.missionutils.Console; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + List winningNumber; + String purchaseAmount; + while (true) { + try { + purchaseAmount = PurchaseAmountValidator.validate(Console.readLine()); + winningNumber = stringToList(Console.readLine()); + Lotto lotto = new Lotto(winningNumber); + String bonusNumber = Console.readLine(); + BonusNumberValidator.validate(bonusNumber, winningNumber); + break; + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + LottoGenerator lottoGenerator = new LottoGenerator(Integer.parseInt(purchaseAmount) / 1000); + LottoCalculator lottoCalculator = new LottoCalculator(LottoGenerator.getGeneratedTickets(), winningNumber, + BonusNumberValidator.getBonusNumber(), Integer.parseInt(purchaseAmount)); + + lottoCalculator.displayResults(); + + + + } + + static List stringToList(String str) { + return Arrays.stream(str.split(",")) + .map(Integer::parseInt) + .collect(Collectors.toList()); } } diff --git a/src/main/java/lotto/BonusNumberValidator.java b/src/main/java/lotto/BonusNumberValidator.java new file mode 100644 index 0000000000..57e7bd9d13 --- /dev/null +++ b/src/main/java/lotto/BonusNumberValidator.java @@ -0,0 +1,31 @@ +package lotto; + +import java.util.List; + +public class BonusNumberValidator extends LottoNumber { + private static int parsedBonusNumber; + + BonusNumberValidator(String bonusNumber, List winningNumbers) { + validate(bonusNumber, winningNumbers); + } + + public static void validate(String bonusNumber, List winningNumbers) { + int parsedBonusNumber; + try { + parsedBonusNumber = Integer.parseInt(bonusNumber); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] int 범위 숫자만 입력해 주세요.(원 단위)"); + } + validateNumberRange(parsedBonusNumber); + for (int num : winningNumbers) { + if (parsedBonusNumber == num) { + throw new IllegalArgumentException("[ERROR] bonus 숫자는 당첨 번호와 달라야합니다."); + } + } + } + + public static int getBonusNumber() { + return parsedBonusNumber; + } + +} diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 88fc5cf12b..5d159f026a 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -2,7 +2,7 @@ import java.util.List; -public class Lotto { +public class Lotto extends LottoNumber{ private final List numbers; public Lotto(List numbers) { @@ -11,10 +11,25 @@ public Lotto(List numbers) { } private void validate(List numbers) { - if (numbers.size() != 6) { + validateNumberSize(numbers); + validateDuplicate(numbers); + for (int number : numbers) + validateNumberRange(number); + } + + private void validateNumberSize(List numbers) { + if (numbers.size() != NUMBERS_SIZE) { throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); } } - // TODO: 추가 기능 구현 + private void validateDuplicate(List numbers) { + if (numbers.size() != numbers.stream().distinct().count()) { + throw new IllegalArgumentException("[ERROR] 중복된 번호가 있습니다."); + } + } + + public List getNumbers() { + return this.numbers; + } } diff --git a/src/main/java/lotto/LottoCalculator.java b/src/main/java/lotto/LottoCalculator.java new file mode 100644 index 0000000000..16d9f3de97 --- /dev/null +++ b/src/main/java/lotto/LottoCalculator.java @@ -0,0 +1,109 @@ +package lotto; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LottoCalculator { + private final List> generatedTickets; + private final List winningNumbers; + private final int bonusNumber; + private final int totalSpent; + private long totalPrizeAmount = 0; + private final Map prizeCount = new HashMap<>(); + + private enum Prize { + FIRST(6, 2_000_000_000, false), + SECOND(5, 30_000_000, true), + THIRD(5, 1_500_000, false), + FOURTH(4, 50_000, false), + FIFTH(3, 5_000, false); + + private final int matchCount; + private final int prizeAmount; + private final boolean requiresBonus; + + Prize(int matchCount, int prizeAmount, boolean requiresBonus) { + this.matchCount = matchCount; + this.prizeAmount = prizeAmount; + this.requiresBonus = requiresBonus; + } + + public boolean matches(int matchCount, boolean bonusMatch) { + return this.matchCount == matchCount && (!requiresBonus || bonusMatch); + } + + public int getPrizeAmount() { + return prizeAmount; + } + + public String getDescription() { + return matchCount + "개 일치" + (requiresBonus ? ", 보너스 번호 일치" : "") + " (" + prizeAmount + "원)"; + } + } + + public LottoCalculator(List> generatedTickets, List winningNumbers, int bonusNumber, int totalSpent) { + this.generatedTickets = generatedTickets; + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + this.totalSpent = totalSpent; + initializePrizeCount(); + } + + public void calculateResults() { + for (List ticket : generatedTickets) { + int matchCount = countMatches(ticket, winningNumbers); + boolean bonusMatch = ticket.contains(bonusNumber); + allocatePrize(matchCount, bonusMatch); + } + } + + private void allocatePrize(int matchCount, boolean bonusMatch) { + for (Prize prize : Prize.values()) { + if (prize.matches(matchCount, bonusMatch)) { + prizeCount.put(prize, prizeCount.get(prize) + 1); + totalPrizeAmount += prize.getPrizeAmount(); + break; + } + } + } + + private int countMatches(List ticket, List winningNumbers) { + int matches = 0; + for (int number : ticket) { + if (winningNumbers.contains(number)) { + matches++; + } + } + return matches; + } + + private void initializePrizeCount() { + for (Prize prize : Prize.values()) { + prizeCount.put(prize, 0); + } + } + + public double calculateReturnRate() { + return ((double) totalPrizeAmount / totalSpent) * 100; + } + + public Map getPrizeCount() { + return prizeCount; + } + + public long getTotalPrizeAmount() { + return totalPrizeAmount; + } + + public void displayResults() { + System.out.println("당첨 내역을 출력한다."); + System.out.println("\n"); + for (Prize prize : Prize.values()) { + int count = prizeCount.get(prize); + System.out.println(prize.getDescription() + " - " + count + "개"); + } + System.out.printf("총 수익률은 %.1f%%입니다.\n", calculateReturnRate()); + } + +} diff --git a/src/main/java/lotto/LottoGenerator.java b/src/main/java/lotto/LottoGenerator.java new file mode 100644 index 0000000000..691a6943a0 --- /dev/null +++ b/src/main/java/lotto/LottoGenerator.java @@ -0,0 +1,34 @@ +package lotto; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LottoGenerator extends LottoNumber { + private static final List> generatedTickets = new ArrayList<>(); + + public LottoGenerator(int numberOfTickets) { + generateAndStoreTickets(numberOfTickets); + displayTickets(); + } + + public static void generateAndStoreTickets(int numberOfTickets) { + for (int i = 0; i < numberOfTickets; i++) { + List ticket = Randoms.pickUniqueNumbersInRange(MIN_NUMBER, MAX_NUMBER, NUMBERS_SIZE); + Collections.sort(ticket); + generatedTickets.add(ticket); + } + } + + public static List> getGeneratedTickets() { + return generatedTickets; + } + + public static void displayTickets() { + System.out.println(generatedTickets.size() + "개를 구매했습니다."); + for (List ticket : generatedTickets) { + System.out.println(ticket); + } + } +} diff --git a/src/main/java/lotto/LottoNumber.java b/src/main/java/lotto/LottoNumber.java new file mode 100644 index 0000000000..61bf77a542 --- /dev/null +++ b/src/main/java/lotto/LottoNumber.java @@ -0,0 +1,13 @@ +package lotto; + +public class LottoNumber { + protected static final int NUMBERS_SIZE = 6; + protected static final int MIN_NUMBER = 1; + protected static final int MAX_NUMBER = 45; + + protected static void validateNumberRange(int number) { + if (number < MIN_NUMBER || MAX_NUMBER < number) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 1에서 45 사이입니다."); + } + } +} diff --git a/src/main/java/lotto/PurchaseAmountValidator.java b/src/main/java/lotto/PurchaseAmountValidator.java new file mode 100644 index 0000000000..6fd60b01dd --- /dev/null +++ b/src/main/java/lotto/PurchaseAmountValidator.java @@ -0,0 +1,21 @@ +package lotto; + +public class PurchaseAmountValidator { + private static final int UNIT = 1000; + + public static String validate(String purchaseAmount) { + int amount; + try { + amount = Integer.parseInt(purchaseAmount); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] int 범위 숫자만 입력해 주세요.(원 단위)"); + } + if (amount < UNIT) { + throw new IllegalArgumentException("[ERROR] 금액은 1000 이상입니다."); + } + if (amount % UNIT != 0) { + throw new IllegalArgumentException("[ERROR] 금액은 1000원 단위입니다."); + } + return purchaseAmount; + } +} diff --git a/src/test/java/lotto/BonusNumberValidatorTest.java b/src/test/java/lotto/BonusNumberValidatorTest.java new file mode 100644 index 0000000000..fd4388a21f --- /dev/null +++ b/src/test/java/lotto/BonusNumberValidatorTest.java @@ -0,0 +1,55 @@ +package lotto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +public class BonusNumberValidatorTest { + @Test + public void testValidBonusNumber() { + String bonusNumber = "7"; + List winningNumbers = List.of(1, 2, 3, 4, 5, 6); + + assertDoesNotThrow(() -> BonusNumberValidator.validate(bonusNumber, winningNumbers)); + } + @Test + public void testBonusNumberMatchingWinningNumber() { + String bonusNumber = "5"; + List winningNumbers = List.of(1, 2, 3, 4, 5, 6); + + Exception exception = assertThrows(IllegalArgumentException.class, () -> + BonusNumberValidator.validate(bonusNumber, winningNumbers) + ); + assertEquals("[ERROR] bonus 숫자는 당첨 번호와 달라야합니다.", exception.getMessage()); + } + @Test + public void testInvalidBonusNumber() { + String bonusNumber = "-5"; + List winningNumbers = List.of(1, 2, 3, 4, 5, 6); + + Exception exception = assertThrows(IllegalArgumentException.class, () -> + BonusNumberValidator.validate(bonusNumber, winningNumbers) + ); + assertEquals("[ERROR] 로또 번호는 1에서 45 사이입니다.", exception.getMessage()); + } + @Test + public void testInvalidBonusNumberFormat() { + String bonusNumber = "abc"; + List winningNumbers = List.of(1, 2, 3, 4, 5, 6); + + Exception exception = assertThrows(IllegalArgumentException.class, () -> + BonusNumberValidator.validate(bonusNumber, winningNumbers) + ); + assertEquals("[ERROR] int 범위 숫자만 입력해 주세요.(원 단위)", exception.getMessage()); + } + +} + diff --git a/src/test/java/lotto/LottoGeneratorTest.java b/src/test/java/lotto/LottoGeneratorTest.java new file mode 100644 index 0000000000..b82553d3f4 --- /dev/null +++ b/src/test/java/lotto/LottoGeneratorTest.java @@ -0,0 +1,92 @@ +package lotto; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class LottoGeneratorTest { + + @BeforeEach + void setUp() { + // Clear any previously generated tickets + LottoGenerator.getGeneratedTickets().clear(); + } + + @Test + void generateAndStoreTickets_ShouldGenerateRequestedNumberOfTickets() { + // When + LottoGenerator.generateAndStoreTickets(3); + + // Then + assertThat(LottoGenerator.getGeneratedTickets()).hasSize(3); + } + + @Test + void generateAndStoreTickets_ShouldGenerateTicketsWithCorrectSize() { + // When + LottoGenerator.generateAndStoreTickets(1); + + // Then + List ticket = LottoGenerator.getGeneratedTickets().get(0); + assertThat(ticket).hasSize(LottoNumber.NUMBERS_SIZE); + } + + @Test + void generateAndStoreTickets_ShouldGenerateTicketsWithinValidRange() { + // When + LottoGenerator.generateAndStoreTickets(5); + + // Then + for (List ticket : LottoGenerator.getGeneratedTickets()) { + for (int number : ticket) { + assertThat(number) + .isGreaterThanOrEqualTo(LottoNumber.MIN_NUMBER) + .isLessThanOrEqualTo(LottoNumber.MAX_NUMBER); + } + } + } + + @Test + void generateAndStoreTickets_ShouldGenerateTicketsWithUniqueNumbers() { + // When + LottoGenerator.generateAndStoreTickets(3); + + // Then + for (List ticket : LottoGenerator.getGeneratedTickets()) { + assertThat(ticket).doesNotHaveDuplicates(); + } + } + + @Test + void generateAndStoreTickets_ShouldGenerateSortedTickets() { + // When + LottoGenerator.generateAndStoreTickets(2); + + // Then + for (List ticket : LottoGenerator.getGeneratedTickets()) { + assertThat(ticket).isSorted(); + } + } + + @Test + void getGeneratedTickets_ShouldReturnEmptyListWhenNoTicketsGenerated() { + // When + List> tickets = LottoGenerator.getGeneratedTickets(); + + // Then + assertThat(tickets).isEmpty(); + } + + @Test + void generateAndStoreTickets_ShouldAccumulateTickets() { + // When + LottoGenerator.generateAndStoreTickets(2); + LottoGenerator.generateAndStoreTickets(3); + + // Then + assertThat(LottoGenerator.getGeneratedTickets()).hasSize(5); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 309f4e50ae..656062f0b5 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -1,10 +1,12 @@ package lotto; +import java.util.Arrays; 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 { @@ -21,5 +23,21 @@ class LottoTest { .isInstanceOf(IllegalArgumentException.class); } - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + @DisplayName("로또 번호에 범위(1, 45)를 벗어나면 예외가 발생한다.") + @Test + void num_range() { + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 50, 5))) + .isInstanceOf(IllegalArgumentException.class); + } + @DisplayName("로또 번호 생성이 성공하는 경우") + void createLotto_Success() { + // given + List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); + + // when + Lotto lotto = new Lotto(numbers); + + // then + assertThat(lotto.getNumbers()).containsExactly(1, 2, 3, 4, 5, 6); + } } diff --git a/src/test/java/lotto/PurchaseAmountValidatorTest.java b/src/test/java/lotto/PurchaseAmountValidatorTest.java new file mode 100644 index 0000000000..04673699b5 --- /dev/null +++ b/src/test/java/lotto/PurchaseAmountValidatorTest.java @@ -0,0 +1,69 @@ +package lotto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PurchaseAmountValidatorTest { + + @Test + @DisplayName("올바른 구매 금액을 입력하면 검증에 성공한다") + void validate_Success() { + // given + String validAmount = "5000"; + + // when & then + assertThatCode(() -> PurchaseAmountValidator.validate(validAmount)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("올바른 구매 금액을 입력하면 검증에 성공한다") + void validate_Unsuccess() { + // given + String validAmount = "500000000000000"; + + // when & then + assertThatCode(() -> PurchaseAmountValidator.validate(validAmount)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"abc", "1000a", "1,000", " ", ""}) + @DisplayName("숫자가 아닌 입력의 경우 예외가 발생한다") + void validate_WithNonNumericInput(String invalidAmount) { + assertThatThrownBy(() -> PurchaseAmountValidator.validate(invalidAmount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] int 범위 숫자만 입력해 주세요.(원 단위)"); + } + + @ParameterizedTest + @ValueSource(strings = {"-1000", "-1", "0", "999"}) + @DisplayName("0, 음수를 입력한 경우 예외가 발생한다") + void validate_WithNegativeAmount(String negativeAmount) { + assertThatThrownBy(() -> PurchaseAmountValidator.validate(negativeAmount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 금액은 1000 이상입니다."); + } + + @ParameterizedTest + @ValueSource(strings = {"1500", "2001"}) + @DisplayName("1000원 단위가 아닌 금액 입력시 예외가 발생한다") + void validate_WithNonThousandUnit(String invalidAmount) { + assertThatThrownBy(() -> PurchaseAmountValidator.validate(invalidAmount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 금액은 1000원 단위입니다."); + } + + @ParameterizedTest + @ValueSource(strings = {"1000", "2000", "5000", "10000"}) + @DisplayName("1000원 단위의 올바른 금액을 입력하면 검증에 성공한다") + void validate_WithValidThousandUnit(String validAmount) { + assertThatCode(() -> PurchaseAmountValidator.validate(validAmount)) + .doesNotThrowAnyException(); + } +} \ No newline at end of file