diff --git a/README.md b/README.md index 5fa2560b46..3c321e89e0 100644 --- a/README.md +++ b/README.md @@ -1 +1,72 @@ # java-lotto-precourse +- 개요: 간단한 로또 발매기 프로그램 +- 프로그램 흐름 (사용자 입장) + - 구입 금액 입력 + - 구입 금액에 따라 발행된 로또 개수 및 로또별 번호 확인 + - 당첨 번호 입력 + - 보너스 번호 입력 + - 당첨 통계(당첨 내역 및 수익률) 확인 +- 당첨 정책 + - 1등: 6개 번호 일치 / 2,000,000,000원 + - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - 3등: 5개 번호 일치 / 1,500,000원 + - 4등: 4개 번호 일치 / 50,000원 + - 5등: 3개 번호 일치 / 5,000원 + +## 실행 결과 예시 +``` +구입금액을 입력해 주세요. +8000 + +8개를 구매했습니다. +[8, 21, 23, 41, 42, 43] +[3, 5, 11, 16, 32, 38] +[7, 11, 16, 35, 36, 44] +[1, 8, 11, 31, 41, 42] +[13, 14, 16, 38, 42, 45] +[7, 11, 30, 40, 42, 43] +[2, 13, 22, 32, 38, 45] +[1, 3, 5, 14, 22, 45] + +당첨 번호를 입력해 주세요. +1,2,3,4,5,6 + +보너스 번호를 입력해 주세요. +7 + +당첨 통계 +--- +3개 일치 (5,000원) - 1개 +4개 일치 (50,000원) - 0개 +5개 일치 (1,500,000원) - 0개 +5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 +6개 일치 (2,000,000,000원) - 0개 +총 수익률은 62.5%입니다. +``` + +## 기능 목록 +- [X] 로또 구입 금액 입력값 검증 + - 입력 형식이 숫자여야 함 + - 로또 1장의 가격(1000원)으로 나누어 떨어져야 함 +- [x] 구입 금액에 해당하는 로또 수 계산 +- [x] 중복되지 않는 무작위 번호 6개로 구성된 단일 로또 발행 + - 단, 로또 번호 정렬 +- [x] 로또 수 만큼 로또 발행 +- [x] 문자열 입력값에서 당첨 번호 추출 + - 쉼표 기준으로 구분 +- [x] 입력 받은 보너스 번호 검증 + - 당첨 번호 입력값들과 중복되면 안 됨 +- [x] 로또 번호의 숫자 범위 검증 (1~45) +- [x] 로또 번호 개수 검증 +- [x] 당첨 번호 추첨 (중복되지 않는 숫자 6개와 보너스 번호 1개) +- [x] 발행된 로또 수량 및 번호 출력 +- [x] 사용자가 구매한 로또 번호와 당첨 번호를 비교 +- [x] 당첨 내역 계산 +- [x] 당첨 내역 출력 +- [x] 총 상금 계산 +- [x] 수익률 계산 +- [x] 수익률 출력 + - 소수점 둘째 자리에서 반올림 (ex. 100.0%, 51.5%, 1,000,000.0%) + +- [ ] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException` 발생, 에러 메시지 출력 후 그 부분부터 입력을 다시 받기 + - 단, 출력 메세지는 "[ERROR]"로 시작 \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..f517cd9855 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,9 @@ package lotto; +import lotto.controller.LottoController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + LottoController.run(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 88fc5cf12b..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("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} diff --git a/src/main/java/lotto/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java new file mode 100644 index 0000000000..4d48787795 --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,42 @@ +package lotto.controller; + +import lotto.enums.Rank; +import lotto.model.Lotto; +import lotto.dto.WinnerLotto; +import lotto.service.LottoCalculator; +import lotto.service.LottoFactory; +import lotto.validator.InputValidator; +import lotto.validator.LottoValidator; +import lotto.view.InputView; +import lotto.view.OutputView; + +import java.util.List; +import java.util.Map; + +public class LottoController { + public static void run() { + int purchaseAmount = readPurchaseAmount(); + List lottos = LottoFactory.createLottos(LottoCalculator.calculateLottoAmount(purchaseAmount)); + OutputView.printPurchasedLotto(lottos); + + WinnerLotto inputLotto = readWinnerLotto(); + List ranks = LottoCalculator.getWinningRanks(lottos, inputLotto, LottoFactory.createRandomWinnerLotto()); + Map statistics = LottoCalculator.getRankStatistics(ranks); + + OutputView.printStatistics(LottoCalculator.calculateProfitRate(purchaseAmount, ranks), statistics); + } + + private static int readPurchaseAmount() { + String input = InputView.ReadInput("구입금액"); + LottoValidator.isValidPurchaseAmounts(input); + return Integer.parseInt(input); + } + + private static WinnerLotto readWinnerLotto() { + String input = InputView.ReadInput("당첨 번호"); + List winNumbers = InputView.parseNumbers(input); + input = InputView.ReadInput("보너스 번호"); + int bonus = InputValidator.validateBonusNumber(input, winNumbers); + return LottoFactory.createWinnerLotto(winNumbers, bonus); + } +} diff --git a/src/main/java/lotto/dto/WinnerLotto.java b/src/main/java/lotto/dto/WinnerLotto.java new file mode 100644 index 0000000000..d32748c1e8 --- /dev/null +++ b/src/main/java/lotto/dto/WinnerLotto.java @@ -0,0 +1,9 @@ +package lotto.dto; + +import java.util.List; + +public record WinnerLotto( + List lottoNumbers, + int bonusNumber +) { +} \ No newline at end of file diff --git a/src/main/java/lotto/enums/ErrorMessage.java b/src/main/java/lotto/enums/ErrorMessage.java new file mode 100644 index 0000000000..4d965b4f6d --- /dev/null +++ b/src/main/java/lotto/enums/ErrorMessage.java @@ -0,0 +1,26 @@ +package lotto.enums; + +import static lotto.validator.LottoValidator.*; + +public enum ErrorMessage { + EMPTY_INPUT("빈 칸은 입력 불가합니다."), + NOT_NUMBER("숫자 형식이어야 합니다."), + NOT_DIVIDED_BY_LOTTO_PRICE("단위가 아닙니다."), + NOT_MINIMUM_PURCHASE_AMOUNTS("최소 구매 가격은 " + LOTTO_PRICE + "원 입니다."), + WRONG_LOTTO_NUMBER_RANGE("로또 번호는 " + LOTTO_MIN_NUMBER + "~ " + LOTTO_MAX_NUMBER + "이내의 숫자여야 합니다."), + WRONG_LOTTO_NUMBER_SIZE("로또 번호는 " + LOTTO_SIZE + "개여야 합니다."), + CAN_NOT_MATCH_BONUS_NUMBER("보너스 번호는 당첨 번호들과 불일치해야 합니다."), + LOTTO_NOT_EXIST("로또가 존재하지 않습니다."), + RANK_NOT_EXIST("존재하지 않는 등수입니다."); + + private final String prefix = "[ERROR]"; + private String message; + + ErrorMessage(String message) { + this.message = prefix + message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/lotto/enums/Rank.java b/src/main/java/lotto/enums/Rank.java new file mode 100644 index 0000000000..07a1eb2ce2 --- /dev/null +++ b/src/main/java/lotto/enums/Rank.java @@ -0,0 +1,44 @@ +package lotto.enums; + +import java.util.Arrays; +import java.util.List; + +public enum Rank { + FIRST(6, 2000000000, false), + SECOND(5, 30000000, true), + THIRD(5, 1500000, false), + FOURTH(4, 50000, false), + FIFTH(3, 5000, false); + + private final int matchCount; + private final int prize; + private final boolean isBonusRequired; + + Rank(int machCount, int prize, boolean isBonusRequired) { + this.matchCount = machCount; + this.prize = prize; + this.isBonusRequired = isBonusRequired; + } + + public int getMatchCount() { + return matchCount; + } + + public int getPrize() { + return prize; + } + + public boolean isBonusRequired() { + return isBonusRequired; + } + + public static Rank calculateRank(int matchCount, boolean isBonusMatched) { + return Arrays + .stream(Rank.values()) + .filter(rank -> (rank.matchCount == matchCount) && (rank.isBonusRequired == isBonusMatched)) + .findFirst() // (matchCount, isBonusMatched) 조합으로는 정확히 하나의 Rank만 매칭 + .orElseThrow(() -> new IllegalArgumentException( + ErrorMessage.RANK_NOT_EXIST.getMessage() + )); + } +} diff --git a/src/main/java/lotto/model/Lotto.java b/src/main/java/lotto/model/Lotto.java new file mode 100644 index 0000000000..63515b54d9 --- /dev/null +++ b/src/main/java/lotto/model/Lotto.java @@ -0,0 +1,23 @@ +package lotto.model; + +import java.util.List; + +import static lotto.validator.LottoValidator.*; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + this.numbers = numbers; + } + + public void validate(List numbers) { + validateSize(numbers); + validateRange(numbers); + } + + public List getNumbers() { + return numbers; + } +} diff --git a/src/main/java/lotto/service/LottoCalculator.java b/src/main/java/lotto/service/LottoCalculator.java new file mode 100644 index 0000000000..9dab2216f7 --- /dev/null +++ b/src/main/java/lotto/service/LottoCalculator.java @@ -0,0 +1,62 @@ +package lotto.service; + +import lotto.enums.Rank; +import lotto.model.Lotto; +import lotto.dto.WinnerLotto; + +import java.util.*; +import java.util.stream.Collectors; + +import static lotto.validator.LottoValidator.LOTTO_PRICE; + +public class LottoCalculator { + public static int calculateLottoAmount(int purchaseAmounts) { + return purchaseAmounts / LOTTO_PRICE; + } + + public static List getWinningRanks(List purchasedLotto, WinnerLotto inputLotto, WinnerLotto winnerLotto) { + List ranks = new ArrayList<>(); + for (Lotto lotto : purchasedLotto) { + int matchCount = (int) lotto.getNumbers().stream() + .filter(winnerLotto.lottoNumbers()::contains) + .count(); + boolean isBonusMatched = lotto.getNumbers().contains(winnerLotto.bonusNumber()); + + try { + Rank rank = Rank.calculateRank(matchCount, isBonusMatched); + ranks.add(rank); + } catch (IllegalArgumentException e) { + // 의도적으로 무시: 해당 로또는 당첨 등수 없음 + } + } + return ranks; + } + + public static Map getRankStatistics(List ranks) { + Map statistics = Arrays.stream(Rank.values()) + .collect(Collectors.toMap(rank -> rank, rank -> 0)); + + ranks.forEach(rank -> statistics.merge(rank, 1, Integer::sum)); + + return statistics; + } + + private static int calculateTotalPrize(List ranks) { + if (ranks.isEmpty()) { + return 0; + } + + int totalPrize = 0; + + for (Rank rank : ranks) { + totalPrize += rank.getPrize(); + } + + return totalPrize; + } + + public static double calculateProfitRate(int purchaseAmount, List ranks) { + int totalPrize = calculateTotalPrize(ranks); + return (double) purchaseAmount / totalPrize * 100; + } +} diff --git a/src/main/java/lotto/service/LottoFactory.java b/src/main/java/lotto/service/LottoFactory.java new file mode 100644 index 0000000000..8d960b3820 --- /dev/null +++ b/src/main/java/lotto/service/LottoFactory.java @@ -0,0 +1,42 @@ +package lotto.service; + +import camp.nextstep.edu.missionutils.Randoms; +import lotto.model.Lotto; +import lotto.dto.WinnerLotto; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static lotto.validator.LottoValidator.*; + +public class LottoFactory { + public static Lotto createRandomLotto() { + List nums = Randoms.pickUniqueNumbersInRange(LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_SIZE); + Collections.sort(nums); + return new Lotto(nums); + } + + public static List createLottos(int totalAmount) { + List lottos = new ArrayList<>(); + for (int i=0; i nums = Randoms.pickUniqueNumbersInRange(LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_SIZE+1); + + int bonusNumber = nums.getLast(); + nums.removeLast(); + + return new WinnerLotto(nums, bonusNumber); + } + + public static WinnerLotto createWinnerLotto(List nums, int bonusNumber) { + return new WinnerLotto(nums, bonusNumber); + } + +} diff --git a/src/main/java/lotto/validator/InputValidator.java b/src/main/java/lotto/validator/InputValidator.java new file mode 100644 index 0000000000..45e9119472 --- /dev/null +++ b/src/main/java/lotto/validator/InputValidator.java @@ -0,0 +1,33 @@ +package lotto.validator; + +import lotto.enums.ErrorMessage; + +import java.util.List; + +public class InputValidator { + public static void validateInput(String input) { + if (input == null) { // !input.matches("\\d+") + throw new IllegalArgumentException(ErrorMessage.EMPTY_INPUT.getMessage()); + } + } + public static int canParseToNumber(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ErrorMessage.NOT_NUMBER.getMessage()); + } + } + + public static int validateBonusNumber(String input, List numbers) { + validateInput(input); + int bonus = canParseToNumber(input); + + for (int num : numbers) { + if (bonus == num) { + throw new IllegalArgumentException(ErrorMessage.CAN_NOT_MATCH_BONUS_NUMBER.getMessage()); + } + } + + return bonus; + } +} diff --git a/src/main/java/lotto/validator/LottoValidator.java b/src/main/java/lotto/validator/LottoValidator.java new file mode 100644 index 0000000000..c08974b658 --- /dev/null +++ b/src/main/java/lotto/validator/LottoValidator.java @@ -0,0 +1,47 @@ +package lotto.validator; + +import lotto.enums.ErrorMessage; + +import java.util.List; + +import static lotto.validator.InputValidator.canParseToNumber; +import static lotto.validator.InputValidator.validateInput; + +public class LottoValidator { + public static final int LOTTO_PRICE = 1000; + public static final int LOTTO_SIZE = 6; + public static final int LOTTO_MIN_NUMBER = 1; + public static final int LOTTO_MAX_NUMBER = 45; + + /** + * 로또 구입 금액 입력값 검증 + * @param input 사용자 입력 문자열 + * @return 유효하면 true, 아니면 false + */ + public static void isValidPurchaseAmounts(String input) { + validateInput(input); + int amount = canParseToNumber(input); + + if (amount < LOTTO_PRICE) { + throw new IllegalArgumentException(ErrorMessage.NOT_MINIMUM_PURCHASE_AMOUNTS.getMessage()); + } + + if (amount % LOTTO_PRICE != 0) { + throw new IllegalArgumentException(ErrorMessage.NOT_DIVIDED_BY_LOTTO_PRICE.getMessage()); + } + } + + public static void validateSize(List numbers) { + if (numbers.size() != LOTTO_SIZE) { + throw new IllegalArgumentException(ErrorMessage.WRONG_LOTTO_NUMBER_SIZE.getMessage()); + } + } + + public static void validateRange(List numbers) { + ; + for (int num : numbers) { + if (num < LOTTO_MIN_NUMBER || num > LOTTO_MAX_NUMBER) + throw new IllegalArgumentException(ErrorMessage.WRONG_LOTTO_NUMBER_RANGE.getMessage()); + } + } +} diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 0000000000..affabfc693 --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,33 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.ArrayList; +import java.util.List; + +import static lotto.validator.InputValidator.canParseToNumber; +import static lotto.validator.InputValidator.validateInput; + +public class InputView { + public static void guideInput(String need) { + System.out.println(need + "을 입력해 주세요."); + } + + public static String ReadInput(String guide) { + guideInput(guide); + String input = Console.readLine(); + validateInput(input); + + return input; + } + + public static List parseNumbers(String input) { + List numbers = new ArrayList<>(); + + for (String s : input.split(",")) { + int num = canParseToNumber(s); + numbers.add(num); + } + return numbers; + } +} diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000000..e841d57fd4 --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,44 @@ +package lotto.view; + +import lotto.enums.ErrorMessage; +import lotto.enums.Rank; +import lotto.model.Lotto; + +import java.util.List; +import java.util.Map; + +public class OutputView { + + public static void printPurchasedLotto(List lottos) { + printLottoCount(lottos); + printLottoNumbers(lottos); + } + + public static void printLottoCount(List lottos) { + System.out.println("\n" + lottos.size() + "개를 구매했습니다."); + } + + public static void printLottoNumbers(List lottos) { + if (lottos.isEmpty()) { + throw new IllegalArgumentException(ErrorMessage.LOTTO_NOT_EXIST.getMessage()); + } + + for (Lotto lotto: lottos) { + lotto.validate(lotto.getNumbers()); + System.out.println(lotto.getNumbers()); + } + } + + public static void printStatistics(double profitRate, Map rankAndCount) { + System.out.println("당첨 통계\n" + "---"); + for (Rank rank: rankAndCount.keySet()) { + System.out.println(rank.getMatchCount() + "개 일치 " + "(" + rank.getPrize() + ") " + + "- " + rankAndCount.get(rank) + "개"); + } + printProfitRate(profitRate); + } + + public static void printProfitRate(Double profitRate) { + System.out.println("총 수익률은 " + String.format("%.1f", profitRate) + "%입니다."); + } +} diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 309f4e50ae..c8ca9d63e1 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -1,5 +1,6 @@ package lotto; +import lotto.model.Lotto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;