diff --git a/README.md b/README.md index 15bb106b5..8a0b130cd 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ # javascript-lotto-precourse +## 프로젝트 개요 +- 사용자로부터 입력받은 구매 금액만큼 로또를 발행하여, 사용자가 입력한 당첨 번호 및 보너스 번호를 바탕으로 로또의 총 당첨 내역 및 수익률을 출력하는 프로젝트 +## 서비스 프로세스 흐름 +1. 사용자로부터 로또 구입 금액을 입력 받는다. +2. 그 금액만큼 로또를 발행한다. +3. 당첨 번호를 입력 받는다. +4. 보너스 번호를 입력 받는다. +5. 각 로또를 순회하며 당첨 내역을 기록한다. +6. 기록된 당첨 내역을 바탕으로 당첨 통계 및 수익률을 출력한다. +## 구현할 기능 목록 +- [ ] 메인 함수(`App.run()`) +- [ ] `Lotto` 메서드 + - `validate` 메서드 내용 확장 + - [ ] 당첨 번호, 보너스 번호를 인자로 받아 비교 후 등수를 반환하는 메서드 (calculateRank) +- [ ] 로또 구입 금액을 입력받는 함수 (setupLottoBudget) + - 예외 케이스 1) 사용자가 정수를 입력하지 않는 경우 + - 예외 케이스 2) 입력 받은 금액이 1,000원 단위로 나누어 떨어지지 않는 경우 +- [ ] 중복되지 않는 숫자 6개를 생성해 로또를 발행하는 함수 (createLotto) +- [ ] 입력된 금액만큼 복권을 발행하는 함수 (purchaseLotto) +- [ ] 당첨 번호를 입력받는 함수 (setupWinningNum) + - 예외 케이스 1) 사용자가 입력한 값을 쉼표 기준으로 구분했을 때, 원소 개수가 6개가 아닌 경우 + - 예외 케이스 2) 원소 중 정수가 아닌 값이 있는 경우 + - 예외 케이스 3) 원소 중 1 ~ 45 범위를 벗어난 값이 있는 경우 + - 예외 케이스 4) 당첨 번호 중 중복되는 값이 있는 경우 +- [ ] 보너스 번호를 입력받는 함수(setupBonusNum) + - 예외 케이스 1) 정수가 아닌 값을 입력받은 경우 + - 예외 케이스 2) 입력받은 값이 1 ~ 45 범위를 벗어난 경우 + - 예외 케이스 3) 당첨 번호 중 보너스 번호와 중복되는 값이 있는 경우 +- [ ] 구매한 로또들의 당첨 내역을 기록하는 함수 (checkLotteriesResult) +- [ ] 기록된 당첨 내역을 바탕으로 수익률을 계산하고 이들을 출력하는 함수(calculateProfit) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5d..bc2221de0 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,141 @@ +import { Console, Random } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; + +const WINNER_PRIZE = [0, 2_000_000_000, 30_000_000, 1_500_000, 50_000, 5_000]; + + class App { - async run() {} + async run() { + const budget = await setupLottoBudget(); + const lotteries = purchaseLotto(budget); + const winningNumbers = await setupWinningNum(); + const bonusNumber = await setupBonusNum(winningNumbers); + const rankCount = checkLotteriesResult(lotteries, winningNumbers, bonusNumber); + calculateProfit(budget, rankCount); + } +} + +async function setupLottoBudget() { + let input = await Console.readLineAsync("구입 금액을 입력해 주세요.\n"); + if (Number.isNaN(input) || Number.isNaN(Number(input))) { + throw new Error("[ERROR] 구입 금액은 정수여야 합니다."); + } + input = Number(input); + if (input <= 0 || input % 1000 !== 0) { + throw new Error("[ERROR] 구입 금액은 1,000원 단위여야 합니다."); + } + Console.print(""); + return input; +} + +function createLotto() { + return new Lotto(Random.pickUniqueNumbersInRange(1, 45, 6).sort((a, b) => a - b)); +} + +function purchaseLotto(budget) { + const quantity = budget / 1000; + Console.print(`${quantity}개를 구매했습니다.\n`); + const lotteries = new Array(quantity); + for (let i = 0; i < quantity; i++) { + lotteries[i] = createLotto(); + Console.print(lotteries[i].toString()); + } + Console.print(""); + return lotteries; +} + +async function setupWinningNum() { + let winningNumbers = await Console.readLineAsync("당첨 번호를 입력해 주세요.\n"); + winningNumbers = winningNumbers.split(","); + if (winningNumbers.length !== 6) { + throw new Error("[ERROR] 당첨 번호는 쉼표를 기준으로 구분되는 6개의 문자여야 합니다."); + } + for (let i = 0, len = winningNumbers.length; i < len; i++) { + winningNumbers[i] = validateWinningNum(winningNumbers[i]); + } + checkDuplicationWinningNum(winningNumbers); + + Console.print(""); + return winningNumbers; +} + +function validateWinningNum(number) { + if (Number.isNaN(number) || Number.isNaN(Number(number))) { + throw new Error("[ERROR] 당첨 번호는 정수여야 합니다."); + } + number = Number(number); + if (number < 0 || number > 45) { + throw new Error("[ERROR] 당첨 번호는 1 ~ 45 사이의 정수여야 합니다."); + } + return number; } +function checkDuplicationWinningNum(array) { + const v = new Array(46).fill(false); + for (const number of array) { + if (v[number]) { + throw new Error("[ERROR] 당첨 번호는 중복될 수 없습니다."); + } + v[number] = true; + } +} + +async function setupBonusNum(winningNumbers) { + let bonusNumber = await Console.readLineAsync("보너스 번호를 입력해 주세요.\n"); + if (Number.isNaN(bonusNumber) || Number.isNaN(Number(bonusNumber))) { + throw new Error("[ERROR] 보너스 번호는 정수여야 합니다."); + } + bonusNumber = validateWinningNum(bonusNumber); + if (winningNumbers.indexOf(bonusNumber) >= 0) { + throw new Error("[ERROR] 당첨 번호와 보너스 번호는 중복될 수 없습니다."); + } + + Console.print(""); + return bonusNumber; +} + +function checkLotteriesResult(lotteries, winningNumbers, bonusNumber) { + const rankCount = new Array(6).fill(0); + for (const lotto of lotteries) { + const [matchedNumberCount, isBonusNumberExist] = lotto.checkWinningStatus(winningNumbers, bonusNumber); + const rank = calculateRank(matchedNumberCount, isBonusNumberExist); + rankCount[rank]++; + } + return rankCount; +} + +function calculateRank(matchedNumberCount, isBonusNumberExist) { + switch (matchedNumberCount) { + case 3: + return 5; + case 4: + return 4; + case 5: + if (isBonusNumberExist) return 2; + return 3; + case 6: + return 1; + } + return 0; +}; + +function calculateProfit(budget, rankCount) { + let totalProfit = 0; + let profitRate = 0; + Console.print("당첨 통계"); + Console.print("------"); + Console.print(`3개 일치 (5,000원) - ${rankCount[5]}개`); + totalProfit += WINNER_PRIZE[5] * rankCount[5]; + Console.print(`4개 일치 (50,000원) - ${rankCount[4]}개`); + totalProfit += WINNER_PRIZE[4] * rankCount[4]; + Console.print(`5개 일치 (1,500,000원) - ${rankCount[3]}개`); + totalProfit += WINNER_PRIZE[3] * rankCount[3]; + Console.print(`5개 일치, 보너스 볼 일치 (30,000,000원) - ${rankCount[2]}개`); + totalProfit += WINNER_PRIZE[2] * rankCount[2]; + Console.print(`6개 일치 (2,000,000,000원) - ${rankCount[1]}개`); + totalProfit += WINNER_PRIZE[1] * rankCount[1]; + profitRate = (totalProfit / budget * 100).toFixed(1); + Console.print(`총 수익률은 ${profitRate}%입니다.`); +} + export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..c5817d612 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -10,9 +10,29 @@ class Lotto { if (numbers.length !== 6) { throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); } + const v = new Array(46).fill(false); + for (const number of numbers) { + if (Number.isNaN(number) || Number.isNaN(Number(number))) { + throw new Error("[ERROR] 로또 번호는 정수여야 합니다."); + } + if (v[number]) { + throw new Error("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + v[number] = true; + } + } + + toString() { + return `[${this.#numbers.join(", ")}]`; } - // TODO: 추가 기능 구현 + checkWinningStatus(winningNumbers, bonusNumber) { + let matchedNumberCount = 0; + let isBonusNumberExist = false; + matchedNumberCount = this.#numbers.filter(num => winningNumbers.includes(num)).length; + isBonusNumberExist = this.#numbers.includes(bonusNumber); + return [matchedNumberCount, isBonusNumberExist]; + } } export default Lotto;