diff --git a/README.md b/README.md index 5fa2560b46..b348c9418b 100644 --- a/README.md +++ b/README.md @@ -1 +1,75 @@ # java-lotto-precourse + +## 개요 +간단한 로또 발매기를 구현한다. + +## 기능 목록 +- 입력 + - 로또 구입 금액 + - 1,000원 단위로 입력받는다. + - 예외 상황: 이하의 경우에 예외를 던진다. + - 입력값이 숫자가 아닌 경우 + - 금액이 1,000원 단위가 아닐 경우 + - 당첨 번호 + - 번호는 쉼표를 기준으로 구분한다. + - 예외 상항: 이하의 경우에 예외를 던진다. + - 쉼표(,)를 제외한 문자가 포함되어 있을 경우 + - 숫자가 범위(1~45)를 벗어날 경우 + - 숫자의 갯수가 6 초과인 경우 + - 숫자의 갯수가 6 미만인 경우 + - 중복된 숫자가 존재하는 경우 + - 보너스 번호 + - 예외 상황: 이하의 경우에 예외를 던진다. + - 입력값이 숫자가 아닌 경우 + - 숫자가 범위(1~45)를 벗어날 경우 + - 당첨 번호와 중복되는 경우 +- 예외 처리: 예외가 던져지면 에러 메세지를 출력한 후 입력을 다시 받는다. +- 로또 발행: 중복되지 않는 6개의 숫자를 뽑는다. + - 발행 갯수는 `구입 금액 / 1000`이다. +- 당첨 결과 계산 + - 발행한 로또와 당첨 번호 및 보너스 번호를 비교해 결과를 계산한다 + - 기준 + - 1등: 6개 번호 일치 / 2,000,000,000원 + - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - 3등: 5개 번호 일치 / 1,500,000원 + - 4등: 4개 번호 일치 / 50,000원 + - 5등: 3개 번호 일치 / 5,000원 +- 출력 + - 입력 시 안내 문구 + - 로또 구입 금액: `구입금액을 입력해 주세요.` + - 당첨 번호: `당첨 번호를 입력해주세요.` + - 보너스 번호: `보너스 번호를 입력해주세요.` + - 발행한 로또 수량 및 번호 + - 로또 번호는 오름차순으로 정렬 + - 예시 + ``` + 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] + ``` + - 결과 출력 시 문구 + - ``` + 당첨 통계 + --- + ``` + - 당첨 내역 + - 예시 + ``` + 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%입니다.` + - 에러 문구 + - 예외 상황 시에 출력 + - 에러 문구는 `[ERROR]`로 시작 diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..046d1f133d 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,23 @@ package lotto; +import lotto.controller.InputController; +import lotto.controller.LottoGameController; +import lotto.util.NumberConverter; +import lotto.util.StringParser; +import lotto.validator.NumberValidator; +import lotto.view.InputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView inputView = new InputView(); + NumberConverter numberConverter = new NumberConverter(); + StringParser stringParser = new StringParser(); + NumberValidator numberValidator = new NumberValidator(); + + InputController inputController = new InputController(inputView, numberConverter, stringParser, numberValidator); + + LottoGameController lottoGameController = new LottoGameController(inputController); + lottoGameController.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/InputController.java b/src/main/java/lotto/controller/InputController.java new file mode 100644 index 0000000000..bcff971309 --- /dev/null +++ b/src/main/java/lotto/controller/InputController.java @@ -0,0 +1,70 @@ +package lotto.controller; + +import java.util.List; +import lotto.util.NumberConverter; +import lotto.util.StringParser; +import lotto.validator.NumberValidator; +import lotto.view.InputView; + +public class InputController { + private final InputView inputView; + private final NumberConverter numberConverter; + private final StringParser stringParser; + private final NumberValidator numberValidator; + + public InputController(InputView inputView, NumberConverter numberConverter, StringParser stringParser, NumberValidator numberValidator) { + this.inputView = inputView; + this.numberConverter = numberConverter; + this.stringParser = stringParser; + this.numberValidator = numberValidator; + } + + public Integer getMoney() { + String input = inputView.getMoney(); + + try { + numberValidator.isNumber(input); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + getMoney(); + } + return numberConverter.convertToInteger(input); + } + + public List getWinningNumbers() { + List winningNumbers; + + List input = stringParser.parse(inputView.getWinningNumber()); + + try { + numberValidator.isNumber(input); + winningNumbers = numberConverter.convertToInteger(input); + + numberValidator.checkCountOfNumber(winningNumbers); + numberValidator.isDuplicateNumber(winningNumbers); + numberValidator.checkInRange(winningNumbers); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + getWinningNumbers(); + } + + return numberConverter.convertToInteger(input); + } + + public Integer getBonusNumber(List winningNumbers) { + String input = inputView.getBonusNumber(); + + try { + numberValidator.isNumber(input); + Integer bonusNumber = numberConverter.convertToInteger(input); + + numberValidator.checkInRange(bonusNumber); + numberValidator.isDuplicateNumber(winningNumbers, bonusNumber); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + getBonusNumber(winningNumbers); + } + + return numberConverter.convertToInteger(input); + } +} diff --git a/src/main/java/lotto/controller/LottoGameController.java b/src/main/java/lotto/controller/LottoGameController.java new file mode 100644 index 0000000000..c8e8e9a614 --- /dev/null +++ b/src/main/java/lotto/controller/LottoGameController.java @@ -0,0 +1,49 @@ +package lotto.controller; + +import java.util.List; +import java.util.Map; +import lotto.model.Lotto; +import lotto.model.LottoGame; +import lotto.model.LottoMachine; +import lotto.model.Prize; +import lotto.util.RandomGenerator; +import lotto.view.OutputView; + +public class LottoGameController { + private final InputController inputController; + + public LottoGameController(InputController inputController) { + this.inputController = inputController; + } + + public void run() { + LottoMachine lottoMachine; + List winningNumbers; + Integer bonusNumber; + RandomGenerator randomGenerator = new RandomGenerator(); + + while (true) { + try { + Integer money = inputController.getMoney(); + lottoMachine = new LottoMachine(money, randomGenerator); + break; + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + + List lottos = lottoMachine.makeLotto(); + OutputView.printLottoBuyHistory(lottos) + ; + winningNumbers = inputController.getWinningNumbers(); + bonusNumber = inputController.getBonusNumber(winningNumbers); + + LottoGame game = new LottoGame(lottos, winningNumbers, bonusNumber); + + Map result = game.getResult(); + double profitRate = game.getProfitRate(); + + OutputView.printResult(result, profitRate); + } + +} diff --git a/src/main/java/lotto/message/ExceptionMessage.java b/src/main/java/lotto/message/ExceptionMessage.java new file mode 100644 index 0000000000..b52c7499fc --- /dev/null +++ b/src/main/java/lotto/message/ExceptionMessage.java @@ -0,0 +1,21 @@ +package lotto.message; + +public enum ExceptionMessage { + ONLY_NUMBER("[ERROR] 숫자만 입력해야 합니다."), + LOTTO_SIZE("[ERROR] 로또 번호는 6개여야 합니다."), + DUPLICATE("[ERROR] 로또 번호는 중복된 숫자가 없어야 합니다."), + OUT_OF_RANGE("[ERROR] 로또 번호는 1에서 45 사이여야 합니다."), + PURCHASE_AMOUNT("[ERROR] 구매 금액은 1000으로 나누어 떨어져야 합니다."), + BONUS_NUMBER("[ERROR] 보너스 번호는 당첨 번호에 없는 번호여야 합니다.") + ; + + private String message; + + private ExceptionMessage(String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/lotto/model/Lotto.java b/src/main/java/lotto/model/Lotto.java new file mode 100644 index 0000000000..0821158490 --- /dev/null +++ b/src/main/java/lotto/model/Lotto.java @@ -0,0 +1,41 @@ +package lotto.model; + +import java.util.List; +import lotto.validator.NumberValidator; + +public class Lotto { + private final List numbers; + private final NumberValidator numberValidator = new NumberValidator(); + + public Lotto(List numbers) { + validate(numbers); + this.numbers = numbers; + } + + private void validate(List numbers) { + numberValidator.isDuplicateNumber(numbers); + if (numbers.size() != 6) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); + } + } + + public Prize checkResult(List winningNumbers, Integer bonusNumber) { + Integer count = 0; + Boolean bonus = false; + + for (Integer number : numbers) { + if (winningNumbers.contains(number)) + count++; + + if (number.equals(bonusNumber)) { + bonus = true; + } + } + + return Prize.getResult(count, bonus); + } + + public List getNumbers() { + return numbers; + } +} diff --git a/src/main/java/lotto/model/LottoGame.java b/src/main/java/lotto/model/LottoGame.java new file mode 100644 index 0000000000..bf882814b6 --- /dev/null +++ b/src/main/java/lotto/model/LottoGame.java @@ -0,0 +1,34 @@ +package lotto.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LottoGame { + private List lottos; + private List winningNumbers; + private Integer bonusNumber; + private Map results = new HashMap<>(); + + public LottoGame(List lottos, List winningNumbers, Integer bonusNumber) { + this.lottos = lottos; + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + public Map getResult() { + for (Lotto lotto : lottos) { + Prize result = lotto.checkResult(winningNumbers, bonusNumber); + results.put(result, results.getOrDefault(result, 0) + 1); + } + + return results; + } + + public double getProfitRate() { + Integer totalPrize = Prize.calculatePrize(results); + Integer usedMoney = 1000 * lottos.size(); + + return (double) totalPrize / usedMoney; + } +} diff --git a/src/main/java/lotto/model/LottoMachine.java b/src/main/java/lotto/model/LottoMachine.java new file mode 100644 index 0000000000..58e52fd8b2 --- /dev/null +++ b/src/main/java/lotto/model/LottoMachine.java @@ -0,0 +1,35 @@ +package lotto.model; + +import java.util.ArrayList; +import java.util.List; +import lotto.util.RandomGenerator; + +public class LottoMachine { + private static final String MONEY_ERROR_MESSAGE = "[ERROR] 구입 금액은 1,000원으로 나누어 떨어져야 합니다."; + + private Integer money; + private RandomGenerator randomGenerator; + + public LottoMachine(Integer money, RandomGenerator randomGenerator) { + validate(money); + this.money = money; + this.randomGenerator = randomGenerator; + } + + private void validate(Integer money) { + if (money % 1000 != 0) + throw new IllegalArgumentException(MONEY_ERROR_MESSAGE); + } + + public List makeLotto() { + List lottos = new ArrayList<>(); + Integer count = this.money / 1000; + + for (int i = 0; i < count; i++) { + Lotto lotto = new Lotto(randomGenerator.generate()); + lottos.add(lotto); + } + + return lottos; + } +} diff --git a/src/main/java/lotto/model/Prize.java b/src/main/java/lotto/model/Prize.java new file mode 100644 index 0000000000..0e43a5415e --- /dev/null +++ b/src/main/java/lotto/model/Prize.java @@ -0,0 +1,53 @@ +package lotto.model; + +import java.util.Map; + +public enum Prize { + NO_PRIZE(0, 0, "0개 일치 (0원)"), + FIRST(6, 2000000000, "6개 일치 (2,000,000,000원)"), + SECOND(5, 30000000, "5개 일치, 보너스 볼 일치 (30,000,000원)"), + THIRD(5, 1500000, "5개 일치 (1,500,000원)"), + FOURTH(4, 50000, "4개 일치 (50,000원)"), + FIFTH(3, 5000, "3개 일치 (5,000원)") + ; + + private final Integer matchCount; + private final Integer money; + private final String message; + + Prize(Integer matchCount, Integer money, String message) { + this.matchCount = matchCount; + this.money = money; + this.message = message; + } + + public static Prize getResult(Integer matchCount, boolean bonus) { + if(matchCount == FIRST.matchCount) { + return FIRST; + } else if(matchCount == SECOND.matchCount && bonus) { + return SECOND; + } else if(matchCount == THIRD.matchCount) { + return THIRD; + } else if(matchCount == FOURTH.matchCount) { + return FOURTH; + } else if(matchCount == FIFTH.matchCount) { + return FIFTH; + } + + return NO_PRIZE; + } + + public static Integer calculatePrize(Map result) { + Integer total = 0; + + for(Prize prize : result.keySet()) { + total += prize.money * result.get(prize); + } + + return total; + } + + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/lotto/util/NumberConverter.java b/src/main/java/lotto/util/NumberConverter.java new file mode 100644 index 0000000000..dbbf9807f0 --- /dev/null +++ b/src/main/java/lotto/util/NumberConverter.java @@ -0,0 +1,20 @@ +package lotto.util; + +import java.util.ArrayList; +import java.util.List; + +public class NumberConverter { + public Integer convertToInteger(String number) { + return Integer.parseInt(number); + } + + public List convertToInteger(List number) { + List result = new ArrayList<>(); + + for (String s : number) { + result.add(Integer.parseInt(s)); + } + + return result; + } +} diff --git a/src/main/java/lotto/util/RandomGenerator.java b/src/main/java/lotto/util/RandomGenerator.java new file mode 100644 index 0000000000..0c8792018f --- /dev/null +++ b/src/main/java/lotto/util/RandomGenerator.java @@ -0,0 +1,14 @@ +package lotto.util; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.List; + +public class RandomGenerator { + private final int START = 1; + private final int END = 45; + private final int COUNT = 6; + + public List generate() { + return Randoms.pickUniqueNumbersInRange(START, END, COUNT); + } +} diff --git a/src/main/java/lotto/util/StringParser.java b/src/main/java/lotto/util/StringParser.java new file mode 100644 index 0000000000..bcbb5aff34 --- /dev/null +++ b/src/main/java/lotto/util/StringParser.java @@ -0,0 +1,11 @@ +package lotto.util; + +import java.util.Arrays; +import java.util.List; + +public class StringParser { + private final String DELIMITER = ","; + public List parse(String input) { + return Arrays.stream(input.split(DELIMITER)).toList(); + } +} diff --git a/src/main/java/lotto/validator/NumberValidator.java b/src/main/java/lotto/validator/NumberValidator.java new file mode 100644 index 0000000000..a4a9278f3e --- /dev/null +++ b/src/main/java/lotto/validator/NumberValidator.java @@ -0,0 +1,55 @@ +package lotto.validator; + +import static lotto.message.ExceptionMessage.BONUS_NUMBER; +import static lotto.message.ExceptionMessage.DUPLICATE; +import static lotto.message.ExceptionMessage.LOTTO_SIZE; +import static lotto.message.ExceptionMessage.ONLY_NUMBER; +import static lotto.message.ExceptionMessage.OUT_OF_RANGE; + +import java.util.List; + +public class NumberValidator { + private static final Integer MIN = 1; + private static final Integer MAX = 45; + + public void isNumber(String number) { + try { + Integer.parseInt(number); + } catch (NumberFormatException e) { + throw new NumberFormatException(ONLY_NUMBER.getMessage()); + } + } + + public void isNumber(List number) { + for(String s : number) { + isNumber(s); + } + } + + public void checkInRange(Integer value) { + if (value < MIN || value > MAX) { + throw new IllegalArgumentException(OUT_OF_RANGE.getMessage()); + } + } + + public void checkInRange(List number) { + for (Integer i : number) { + checkInRange(i); + } + } + + public void isDuplicateNumber(List number) { + if(number.size() != number.stream().distinct().count()) + throw new IllegalArgumentException(DUPLICATE.getMessage()); + } + + public void isDuplicateNumber(List number, Integer value) { + if(number.contains(value)) + throw new IllegalArgumentException(BONUS_NUMBER.getMessage()); + } + + public void checkCountOfNumber(List number) { + if(number.size() != 6) + throw new IllegalArgumentException(LOTTO_SIZE.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..b3dcf63932 --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,30 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + private static final String MONEY_INFORM_MESSAGE = "구입금액을 입력해 주세요."; + private static final String WINNING_NUMBER_MESSAGE = "당첨 번호를 입력해 주세요."; + private static final String BONUS_NUMBER_MESSAGE = "보너스 번호를 입력해 주세요."; + + public String getMoney() { + System.out.println(MONEY_INFORM_MESSAGE); + + String input = Console.readLine(); + return input; + } + + public String getWinningNumber() { + System.out.println(WINNING_NUMBER_MESSAGE); + + String input = Console.readLine(); + return input; + } + + public String getBonusNumber() { + System.out.println(BONUS_NUMBER_MESSAGE); + + String input = Console.readLine(); + return input; + } +} diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000000..814d7ed58b --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,29 @@ +package lotto.view; + +import java.util.List; +import java.util.Map; +import lotto.model.Lotto; +import lotto.model.Prize; + +public class OutputView { + public static void printLottoBuyHistory(List lottos) { + System.out.println(lottos.size() + "개를 구매했습니다."); + for (Lotto lotto : lottos) { + List sortedNumbers = lotto.getNumbers() + .stream() + .sorted() + .toList(); + + System.out.println(sortedNumbers); + } + } + + public static void printResult(Map result, double profitRate) { + System.out.println("당첨 통계\n---"); + for (int i = Prize.values().length - 2; i >= 0; i--) { + Prize rank = Prize.values()[i]; + System.out.println(rank.getMessage() + " - " + result.get(rank) + "개"); + } + System.out.printf("총 수익률은 %.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;