diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..e301b4571d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,47 @@ +## 설계 고민 +- 객체간의 종속성을 줄이는 방향 +- 객체의 기능과 책임을 어떻게 나눌 것인가.. + - 되도록이면 하나의 객체는 하나의 책임을! + - 정보를 많이 알고 있는 객체가 행위를! + - 직접 가져와서 쓰지 말고 요청을! + +validator를 따로 빼서 클래스로 갖고 있어야하나.. +그런데 그럼 검증은 정보를 가지고 있는 객체가 하기로 한 원칙을 위배해야 하는데.. + +* 참고 코드 = mjucraft -> recruit / 김영한 app config + +## 흐름 정리 +1. [Input]: 구입 금액 입력 -> [User] 전달 +2. [User]: 로또 개수 확정, +3. 각 로또의 번호 확정 +4. 당첨 번호 입력 +5. 보너스 번호 입력 +6. 당첨 여부 확정 +7. 수익률 계산 + +## 기능 정리 +1. 금액 -> 개수 +2. 랜덤 넘버 생성: random number creator +3. 당첨 넘버 생성 +4. [ 로또 넘버 <-> 당첨 넘버 체크 ] -> 각 당첨 등수별 개수 +5. 각 당첨 등수 개수 -> 금액 +6. 수익률 계산 +7. Error 발생 + +## 객체 정리 +### 1. Lotto + - 번호 6개 + - 번호 검증 함수(개수, 중복) + - 번호 일치 함수(순서 무관) + + +### 2. WinningLotto + - 번호 6개 + 보너스 1개 + - 번호 검증 함수(중복) + + + +### 5. Input / Output + - 입력과 출력만 담당 + - 그 이상의 책임을 가지지 않는다. + diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..c5af2dd968 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -2,6 +2,8 @@ public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + Controller controller = new Controller(); + + controller.run(); } } diff --git a/src/main/java/lotto/Calculator.java b/src/main/java/lotto/Calculator.java new file mode 100644 index 0000000000..0921990329 --- /dev/null +++ b/src/main/java/lotto/Calculator.java @@ -0,0 +1,24 @@ +package lotto; + +import java.util.Map; + +public class Calculator { + + public int calculateAmountOfLotteriesWithMoney(int money) { + return money / Receipt.LOTTO_PRICE; + } + + public Long calculatePrize(Map rankCount) { + Long prize = 0L; + + for (Rank rank : Rank.values()) { + prize += rankCount.get(rank).intValue() * rank.getPrize(); + } + + return prize; + } + + public double calculateProfitRate(int amountOfMoney, Long prize) { + return (double) amountOfMoney / prize; + } +} diff --git a/src/main/java/lotto/Controller.java b/src/main/java/lotto/Controller.java new file mode 100644 index 0000000000..9f7c3b18f9 --- /dev/null +++ b/src/main/java/lotto/Controller.java @@ -0,0 +1,72 @@ +package lotto; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class Controller { + private Input input = new Input(); + private Output output = new Output(); + private Service service = new Service(); + + public void run() { + Receipt receipt = createReceipt(); + int amountOfLotteries = receipt.getAmountOfLotteries(); + List lotteries = createLotteries(amountOfLotteries); + + WinningLotto winningLotto = createWinningLotto(); + + Map rankCounts = createRankCount(winningLotto, lotteries); + Result result = createResult(rankCounts, receipt); + + showProfitRate(result); + } + + private Receipt createReceipt() { + int purchaseAmount = input.readPurchaseAmount(); + Receipt receipt = service.createReceipt(purchaseAmount); + + int amountOfLotteries = receipt.getAmountOfLotteries(); + output.printPurchasedCount(amountOfLotteries); + + return receipt; + } + + private List createLotteries(int amountOfLotteries) { + List lotteries = service.createLotteries(amountOfLotteries); + List> lotteryNumbers = new ArrayList<>(); + + for (Lotto lotto : lotteries) { + lotteryNumbers.add(lotto.getNumbers()); + } + + output.printLotteries(lotteryNumbers); + + return lotteries; + } + + private WinningLotto createWinningLotto() { + List winningNumbers = input.readWinningNumbers(); + int bonusNumber = input.readBonusNumber(); + + return service.createWinningLotto(winningNumbers, bonusNumber); + } + + private Map createRankCount(WinningLotto winningLotto, List lotteries) { + Map rankCounts = service.createRankCounts(winningLotto, lotteries); + output.printWinningStatistics(rankCounts); + + return rankCounts; + } + + private Result createResult(Map rankCount,Receipt receipt) { + int amountOfMoney = receipt.getAmountOfMoney(); + return service.createResult(rankCount, amountOfMoney); + } + + private void showProfitRate(Result result) { + output.printProfitRate(result.profitRate); + } + + +} diff --git a/src/main/java/lotto/ErrorType.java b/src/main/java/lotto/ErrorType.java new file mode 100644 index 0000000000..a1808c1deb --- /dev/null +++ b/src/main/java/lotto/ErrorType.java @@ -0,0 +1,16 @@ +package lotto; + +public enum ErrorType { + + // TODO: Error Type 설정 및 Handler 구현 + + DUPLICATED_NUMBERS("[ERROR] 중복된 숫자는 허용되지 않습니다"), + LOTTO_COUNTS("[ERROR] 로또 번호는 6개여야 합니다."), + WRONG_AMOUNT_OF_MONEY("[ERROR] 금액은 1000원 단위여야 합니다."); + + private final String message; + + ErrorType(String message) { + this.message = message; + } +} diff --git a/src/main/java/lotto/Input.java b/src/main/java/lotto/Input.java new file mode 100644 index 0000000000..19ac2a5b0c --- /dev/null +++ b/src/main/java/lotto/Input.java @@ -0,0 +1,78 @@ +package lotto; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.ArrayList; +import java.util.List; + +public class Input { + + public int readPurchaseAmount() { + System.out.println("구입금액을 입력해 주세요."); + String input = Console.readLine(); + + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("[ERROR] 구입 금액은 숫자여야 합니다."); + } + + if (!input.matches("\\d+")) { + throw new IllegalArgumentException("[ERROR] 구입 금액은 숫자여야 합니다."); + } + + int amountOfMoney = Integer.parseInt(input); + + if (amountOfMoney % Receipt.LOTTO_PRICE !=0) { + throw new IllegalArgumentException("[ERROR] 구입 금액은 1000원 단위여야 합니다."); + } + return amountOfMoney; + } + + public List readWinningNumbers() { + System.out.println("당첨 번호를 입력해 주세요."); + String input = Console.readLine(); + + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("[ERROR] 당첨 번호 입력이 올바르지 않습니다."); + } + + String[] tokens = input.split(","); + if (tokens.length != Lotto.NUMBERS_COUNT) { + throw new IllegalArgumentException("[ERROR] 당첨 번호는 6개여야 합니다."); + } + + List numbers = new ArrayList<>(); + for (String token : tokens) { + String value = token.trim(); + if (!value.matches("\\d+")) { + throw new IllegalArgumentException("[ERROR] 당첨 번호는 숫자여야 합니다."); + } + numbers.add(Integer.parseInt(value)); + } + + for (int i = 0; i < numbers.size() - 1; i++) { + List restNumbers = numbers.subList(i + 1, numbers.size() - 1); + + if (restNumbers.contains(numbers.get(i))) { + throw new IllegalArgumentException("[ERROR] 당첨 번호들은 중복되면 안됩니다."); + } + } + + return numbers; + } + + public int readBonusNumber() { + System.out.println("보너스 번호를 입력해 주세요."); + String input = Console.readLine(); + + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("[ERROR] 보너스 번호는 숫자여야 합니다."); + } + + if (!input.matches("\\d+")) { + throw new IllegalArgumentException("[ERROR] 보너스 번호는 숫자여야 합니다."); + } + + return Integer.parseInt(input); + } + +} diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java index 88fc5cf12b..38e0350502 100644 --- a/src/main/java/lotto/Lotto.java +++ b/src/main/java/lotto/Lotto.java @@ -5,16 +5,57 @@ public class Lotto { private final List numbers; + public static final int MINIMUM_NUMBER = 1; + public static final int MAXIMUM_NUMBER = 45; + public static final int NUMBERS_COUNT = 6; + + // TODO: Lotto랑 WinningLotto가 유사한 면이 있음. abstract class나 interface를 사용하는 것을 고민해봐야 함. + public Lotto(List numbers) { - validate(numbers); + validateCount(numbers); + validateDuplication(); this.numbers = numbers; } - private void validate(List numbers) { - if (numbers.size() != 6) { + public List getNumbers() { + return numbers; + } + + public int matchWinningNumbers(List winningNumbers) { + int count = 0; + + for (int number : numbers) { + if (winningNumbers.contains(number)) { + count++; + } + } + + return count; + } + + public boolean matchBonusNumber(int bonusNumber) { + for(int number : numbers){ + if(number == bonusNumber){ + return true; + } + } + + return false; + } + + private void validateCount(List numbers) { + if (numbers.size() != NUMBERS_COUNT) { throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); } } - // TODO: 추가 기능 구현 + private void validateDuplication() { + for (int i = 0; i < numbers.size() -1 ; i++) { + List restNumbers = numbers.subList(i + 1, numbers.size() -1); + if (restNumbers.contains(numbers.get(i))) { + throw new IllegalArgumentException("[ERROR] 겹치는 로또 번호가 있습니다."); + } + } + } + } diff --git a/src/main/java/lotto/Output.java b/src/main/java/lotto/Output.java new file mode 100644 index 0000000000..ffc4105e1a --- /dev/null +++ b/src/main/java/lotto/Output.java @@ -0,0 +1,34 @@ +package lotto; + +import java.util.List; +import java.util.Map; + +public class Output { + + public void printPurchasedCount(int count) { + System.out.println(count + "개를 구매했습니다."); + } + + public void printLotteries(List> lotteries) { + for (List numbers : lotteries) { + System.out.println(numbers); + } + } + + public void printWinningStatistics(Map rankCounts) { + + System.out.println(); + System.out.println("당첨 통계"); + System.out.println("---"); + + System.out.println("3개 일치 (5,000원) - " + rankCounts.getOrDefault(Rank.Fifth, 0) + "개"); + System.out.println("4개 일치 (50,000원) - " + rankCounts.getOrDefault(Rank.Fourth, 0) + "개"); + System.out.println("5개 일치 (1,500,000원) - " + rankCounts.getOrDefault(Rank.Third, 0) + "개"); + System.out.println("5개 일치, 보너스 볼 일치 (30,000,000원) - " + rankCounts.getOrDefault(Rank.Second, 0) + "개"); + System.out.println("6개 일치 (2,000,000,000원) - " + rankCounts.getOrDefault(Rank.First, 0) + "개"); + } + + public void printProfitRate(double profitRatePercent) { + System.out.printf("총 수익률은 %.1f%%입니다.%n", profitRatePercent); + } +} diff --git a/src/main/java/lotto/RandomNumberCreator.java b/src/main/java/lotto/RandomNumberCreator.java new file mode 100644 index 0000000000..20839e36cf --- /dev/null +++ b/src/main/java/lotto/RandomNumberCreator.java @@ -0,0 +1,11 @@ +package lotto; + +import java.util.List; + +public class RandomNumberCreator { + public List createRandomLottoNumbers() { + List numbers = camp.nextstep.edu.missionutils.Randoms.pickUniqueNumbersInRange(Lotto.MINIMUM_NUMBER,Lotto.MAXIMUM_NUMBER,Lotto.NUMBERS_COUNT); + + return numbers; + } +} diff --git a/src/main/java/lotto/Rank.java b/src/main/java/lotto/Rank.java new file mode 100644 index 0000000000..bee13ca1b8 --- /dev/null +++ b/src/main/java/lotto/Rank.java @@ -0,0 +1,31 @@ +package lotto; + +public enum Rank { + First(6, false, 2000000000,"6개 일치 - 1등"), + Second(5, true, 30000000, "5개 일치 + 보너스 일치 - 2등"), + Third(5, false, 1500000, "5개 일치 + 보너스 불일치 - 3등"), + Fourth(4, false, 50000, "4개 일치 - 4등"), + Fifth(3,false, 5000, "3개 일치 - 5등"), + MISS(0, false, 0,""); // 굳이? + + + private final int matchCount; + private final boolean requiresBonus; + private final long prize; + private final String message; + + Rank(int matchCount, boolean requiresBonus, long prize, String message) { + this.matchCount = matchCount; + this.requiresBonus = requiresBonus; + this.prize = prize; + this.message = message; + } + + public long getPrize() { + return prize; + } + + public int getMatchCount() { + return matchCount; + } +} diff --git a/src/main/java/lotto/Receipt.java b/src/main/java/lotto/Receipt.java new file mode 100644 index 0000000000..76ab745ccf --- /dev/null +++ b/src/main/java/lotto/Receipt.java @@ -0,0 +1,23 @@ +package lotto; + +import java.util.List; + +public class Receipt { + public static final int LOTTO_PRICE = 1000; + + private int amountOfMoney; + private int amountOfLotteries; + + public Receipt(int amountOfMoney, int amountOfLotteries) { + this.amountOfMoney = amountOfMoney; + this.amountOfLotteries = amountOfLotteries; + } + + public int getAmountOfMoney() { + return amountOfMoney; + } + + public int getAmountOfLotteries() { + return amountOfLotteries; + } +} diff --git a/src/main/java/lotto/Result.java b/src/main/java/lotto/Result.java new file mode 100644 index 0000000000..f4dcb31840 --- /dev/null +++ b/src/main/java/lotto/Result.java @@ -0,0 +1,11 @@ +package lotto; + +public class Result { + Long amountOfPrize; + double profitRate; + + public Result(Long amountOfPrize, double profitability) { + this.amountOfPrize = amountOfPrize; + this.profitRate = profitability; + } +} diff --git a/src/main/java/lotto/Service.java b/src/main/java/lotto/Service.java new file mode 100644 index 0000000000..b2a7628024 --- /dev/null +++ b/src/main/java/lotto/Service.java @@ -0,0 +1,96 @@ +package lotto; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Service { + //TODO: AppConfig 도입하기 + + Calculator calculator = new Calculator(); + RandomNumberCreator randomNumberCreator = new RandomNumberCreator(); + + public Receipt createReceipt(int amountOfMoney) { + + int amountOfLotteries = calculator.calculateAmountOfLotteriesWithMoney(amountOfMoney); + + Receipt receipt = new Receipt(amountOfMoney, amountOfLotteries); + + return receipt; + } + + public List createLotteries(int amountOfLotteries) { + + List lotteries = new ArrayList<>(); + + for (int i = 0; i < amountOfLotteries; i++) { + List numbers = randomNumberCreator.createRandomLottoNumbers(); + Lotto lotto = new Lotto(numbers); + + lotteries.add(lotto); + } + return lotteries; + } + + public WinningLotto createWinningLotto(List numbers, int bonusNumber) { + WinningLotto winningLotto = new WinningLotto(numbers, bonusNumber); + + return winningLotto; + } + + public Map createRankCounts(WinningLotto winningLotto, List lotteries) { + Map rankCounts = new HashMap<>(); + + for (Rank rank : Rank.values()) { + rankCounts.put(rank, 0); + } + + List winningNumbers = winningLotto.getNumbers(); + int bonusNumber = winningLotto.getBonusNumber(); + + for (Lotto lotto : lotteries) { + int matchCount = lotto.matchWinningNumbers(winningNumbers); + boolean bonusMatch = lotto.matchBonusNumber(bonusNumber); + + Rank rank = determineRank(matchCount, bonusMatch); + rankCounts.put(rank, rankCounts.get(rank) + 1); + } + + return rankCounts; + } + + + public Result createResult(Map rankCount, int amountOfMoney){ + + Calculator calculator = new Calculator(); + Long prize = calculator.calculatePrize(rankCount); + + double profitRate = calculator.calculateProfitRate(amountOfMoney,prize); + + Result result = new Result(prize, profitRate); + + return result; + } + + // TODO: refactoring 해야 함. service가 rank를 결정해주는게 적절하지 않음. + private Rank determineRank(int matchCount, boolean bonusMatch) { + if (matchCount == Rank.First.getMatchCount()) { + return Rank.First; + } + if (matchCount == Rank.Second.getMatchCount() && bonusMatch) { + return Rank.Second; + } + if (matchCount == Rank.Third.getMatchCount()) { + return Rank.Third; + } + if (matchCount == Rank.Fourth.getMatchCount()) { + return Rank.Fourth; + } + if (matchCount == Rank.Fifth.getMatchCount()) { + return Rank.Fifth; + } + return Rank.MISS; + } + +} diff --git a/src/main/java/lotto/WinningLotto.java b/src/main/java/lotto/WinningLotto.java new file mode 100644 index 0000000000..2eeab77f1e --- /dev/null +++ b/src/main/java/lotto/WinningLotto.java @@ -0,0 +1,43 @@ +package lotto; + +import java.util.ArrayList; +import java.util.List; + +public class WinningLotto { + private List numbers; + private int bonusNumber; + + public WinningLotto(List numbers, int bonusNumber) { + validateDuplicationInNumbers(); + validateDuplicatedBonusNumberInNumbers(); + + this.numbers = numbers; + this.bonusNumber = bonusNumber; + } + + public List getNumbers() { + return numbers; + } + + public int getBonusNumber() { + return bonusNumber; + } + + private void validateDuplicationInNumbers() { + for (int i = 0; i < numbers.size() -1; i++) { + List restNumbers = numbers.subList(i + 1, numbers.size() - 1); + for (int number : numbers) { + if (restNumbers.contains(number)) { + throw new IllegalArgumentException("[ERROR] 중복되는 번호가 있을 수 없습니다."); + } + } + } + + } + + private void validateDuplicatedBonusNumberInNumbers() { + if (numbers.contains(bonusNumber)) { + throw new IllegalArgumentException("[ERROR] 보너스 숫자는 당첨 숫자들과 겹칠 수 없습니다."); + } + } +}