diff --git a/README.md b/README.md index 15bb106b5..e9e40af12 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ # javascript-lotto-precourse + +# 기능 목록 + +## 입력 + +- [x] 1,000원 단위로 로또 구입 금액을 입력받는다. + - [ ] 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨다. 잘못된 값은 아래에 해당한다. + - [x] 1,000원으로 나누어 떨어지지 않는 경우 = 1,000원 미만인 경우 + - [x] 입력값이 숫자가 아닌 경우 + - [x] 에러 발생 후, 해당 지점부터 다시 입력받는다. +- [x] 로또발행 내역이 출력된 후, 쉼표(,)를 기준으로 6개의 당첨 번호를 입력받는다. + - [x] 예외처리 `Error` 예외의 경우는 밑에 해당한다. + - [x] 1~45 사이의 숫자가 아닌 경우 + - [x] 중복된 번호가 있는 경우 + - [x] 6개의 번호를 입력받지 않은 경우 +- [x] 당첨번호 입력받은 후, 보너스 번호를 입력받는다. + - [x] 예외처리 `Error` + - [x] 1~45 사이의 숫자가 아닌 경우 + - [x] 당첨번호 안에 이미 보너스 번호가 있는 경우 + +## 출력 + +- [x] 발행한 로또 수량을 출력한다. +- [x] 오름차순 정렬된 로또 번호들을 로또 수량만큼 출력한다. +- [x] 계산되어 반환된 당첨내역을 출력한다. +- [x] 계산되어 반환된 수익률을 출력한다. + +## 핵심 기능 + +- [x] 입력받은 로또 구입 금액에 해당하는 만큼 로또를 발행한다. (로또 1장은 1,000원이다) + - [x] 로또 1장 당 1~45 사이의 중복되지 않는 6개의 숫자를 뽑는다. + - [x] 숫자들을 오름차순 정렬한다. +- [x] 입력받은 당첨 번호 문자열을 구분자(,)를 기준으로 배열에 저장한다. +- [x] 구분자로 분리된 당첨 번호 문자열 배열을 숫자형으로 변환한다. +- [ ] 사용자가 구매한 로또 번호와, 당첨 번호를 비교해 당첨내역을 계산해 반환한다. + + - [x] 점수를 계산한다. + + - [x] 1등: 6개 번호 일치(6점) + - [x] 2등: 5개 번호(5점) + 보너스 번호 일치(true) + - [x] 3등: 5개 번호 일치(5점) + - [x] 4등: 4개 번호 일치(4점) + - [x] 5등: 3개 번호 일치(3점) + + - [x] 당첨금액을 계산해 반환한다. + - [x] 1등 : 2,000,000,000원 + - [x] 2등 : 30,000,000원 + - [x] 3등 : 1,500,000원 + - [x] 4등 : 50,000원 + - [x] 5등 : 5,000원 + - [x] 소수점 둘째 자리에서 반올림 한 수익률을 계산해 반환한다. diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..076df9655 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -7,12 +7,19 @@ describe("로또 클래스 테스트", () => { }).toThrow("[ERROR]"); }); - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + test("로또 번호에 중복된 숫자가 있다면 true를 리턴한다.", () => { + expect(Lotto.hasDuplicatedLottoNumber([1, 2, 3, 4, 5, 5])).toBe(true); + }); + + test("로또 번호에 중복된 숫자가 없다면 false를 리턴한다.", () => { + expect(Lotto.hasDuplicatedLottoNumber([1, 2, 3, 4, 5, 6])).toBe(false); }); - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + test("번호에 1과 45 사이의 값이 아니면 true를 반환한다.", () => { + expect(Lotto.isValidLottoNumberRange([46])).toBe(false); + }); + + test("번호에 1과 45 사이의 값이 있으면 false를 반환한다.", () => { + expect(Lotto.isValidLottoNumberRange([4])).toBe(true); + }); }); diff --git a/src/App.js b/src/App.js index 091aa0a5d..2b4399105 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,144 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import Lotto from "./Lotto.js"; + +const LOTTO_COST_MIN = 1000; +const GUIDE_MESSAGE_INPUT_COST = "구입금액을 입력해 주세요. (ex: 2000)\n"; +const GUIDE_MESSAGE_INPUT_USER_PICKED_NUMBERS = + "\n당첨 번호를 입력해 주세요. (ex: 1,2,3,4,5,6)\n"; +const GUIDE_MESSAGE_INPUT_USER_PICKED_BONUS_NUMBER = + "\n보너스 번호를 입력해 주세요. (ex: 7)\n"; +const ERROR_MESSAGE_INVALID_COST = + "[ERROR] 입력하신 금액이 1,000원 단위가 아닙니다. 다시"; +const ERROR_MESSAGE_NOT_NUMBER = "[ERROR] 입력값이 숫자가 아닙니다. 다시"; +const ERROR_MESSAGE_NOT_IN_VALID_RANGE = + "[ERROR] 입력값이 로또 번호 범위에 있지 않습니다. 다시"; +const ERROR_MESSAGE_DUPLICATED_LOTTO_NUMBER = + "[ERROR] 중복된 로또 번호가 있습니다. 다시"; +const ERROR_MESSAGE_NOT_SIX_NUMBERS = + "[ERROR] 6개의 당첨 번호를 입력해야합니다. 다시"; + class App { - async run() {} + async run() { + let lottoBudget; + let isValid = false; + + while (!isValid) { + try { + lottoBudget = Number(await this.getLottoCost()); + if (isNaN(lottoBudget)) { + throw new Error(ERROR_MESSAGE_NOT_NUMBER); + } else if (lottoBudget % LOTTO_COST_MIN !== 0) { + throw new Error(ERROR_MESSAGE_INVALID_COST); + } + isValid = true; + } catch (error) { + MissionUtils.Console.print(error.message); + } + } + + const lottoCnt = lottoBudget / LOTTO_COST_MIN; + let lottoNumbers = []; + for (let i = 0; i < lottoCnt; i++) { + const randomNumbers = Lotto.generateRandomNumbers(); + + lottoNumbers.push(new Lotto(randomNumbers)); + } + + MissionUtils.Console.print(`\n${lottoCnt}개를 구매했습니다.`); + + lottoNumbers.forEach((eachLotto) => { + MissionUtils.Console.print(`[${eachLotto.getNumbers().join(", ")}]`); + }); + + let userPickedNumbers = []; + + isValid = false; + while (!isValid) { + try { + userPickedNumbers = this.splitUserPickedNumbers( + await this.getUserPickedNumbers() + ).map(Number); + + if (userPickedNumbers.length != 6) { + throw new Error(ERROR_MESSAGE_NOT_SIX_NUMBERS); + } + + userPickedNumbers.forEach((number) => { + if (!Lotto.isValidLottoNumberRange(number)) { + throw new Error(ERROR_MESSAGE_NOT_IN_VALID_RANGE); + } + }); + + if (Lotto.hasDuplicatedLottoNumber(userPickedNumbers)) { + throw new Error(ERROR_MESSAGE_DUPLICATED_LOTTO_NUMBER); + } + + isValid = true; + } catch (error) { + MissionUtils.Console.print(error.message); + } + } + + let userPickedBonusNum; + + isValid = false; + while (!isValid) { + try { + userPickedBonusNum = Number(await this.getUserPickedBonusNumber()); + + if (userPickedNumbers.includes(userPickedBonusNum)) { + throw new Error(ERROR_MESSAGE_DUPLICATED_LOTTO_NUMBER); + } + + if (!Lotto.isValidLottoNumberRange(userPickedBonusNum)) { + throw new Error(ERROR_MESSAGE_NOT_IN_VALID_RANGE); + } + + isValid = true; + } catch (error) { + MissionUtils.Console.print(error.message); + } + } + + let score = Lotto.getScore( + userPickedNumbers, + lottoNumbers, + userPickedBonusNum + ); + + let counts = Lotto.getCounts(score); + + // 당첨 내역 반환된거 출력 + let resultMessage = Lotto.getResultMessage(...counts); + MissionUtils.Console.print(`${resultMessage}`); + + let totalPrizeMoney = Lotto.calculateTotalPrizeMoney(counts); + + const profitRatio = Lotto.getProfitRatio(lottoBudget, totalPrizeMoney); + // 수익률 반환된거 출력 + MissionUtils.Console.print(`총 수익률은 ${profitRatio}%입니다.`); + } + + getLottoCost() { + return MissionUtils.Console.readLineAsync(GUIDE_MESSAGE_INPUT_COST); + } + + getUserPickedNumbers() { + return MissionUtils.Console.readLineAsync( + GUIDE_MESSAGE_INPUT_USER_PICKED_NUMBERS + ); + } + + splitUserPickedNumbers(userPickedStr) { + return userPickedStr.split(","); + } + + getUserPickedBonusNumber() { + return MissionUtils.Console.readLineAsync( + GUIDE_MESSAGE_INPUT_USER_PICKED_BONUS_NUMBER + ); + j; + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..d4e1968d3 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,3 +1,14 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +const LOTTO_NUMBER_MIN = 1; +const LOTTO_NUMBER_MAX = 45; +const LOTTO_NUMBER_COUNT = 6; + +const FIRST_PRIZE_MONEY = 2000000000; +const SECOND_PRIZE_MONEY = 30000000; +const THIRD_PRIZE_MONEY = 1500000; +const FOURTH_PRIZE_MONEY = 50000; +const FIFTH_PRIZE_MONEY = 5000; + class Lotto { #numbers; @@ -12,7 +23,108 @@ class Lotto { } } - // TODO: 추가 기능 구현 + getNumbers() { + return this.#numbers; + } + + static generateRandomNumbers() { + return MissionUtils.Random.pickUniqueNumbersInRange( + LOTTO_NUMBER_MIN, + LOTTO_NUMBER_MAX, + LOTTO_NUMBER_COUNT + ).sort((a, b) => a - b); + } + + static getScore(userPickedNumbers, lottoNumbers, userPickedBonusNum) { + let scores = []; + lottoNumbers.forEach((lottoSet) => { + const winNum = lottoSet.getNumbers(); // 로또세트 순서대로 하나 가져오기 + let score = 0; // 각 로또세트 별 점수 세고 계속 초기화되는 i 이터레이터 + let isBonusMatched = 0; + + // 사용자 번호와 로또 번호 비교 + userPickedNumbers.forEach((userNumber) => { + if (winNum.includes(userNumber)) { + score++; + } + }); + + // 보너스 번호 확인 + if (winNum.includes(userPickedBonusNum)) { + isBonusMatched = 1; + } + + // 점수 객체를 scores 배열에 추가 + scores.push({ score, isBonusMatched }); + }); + + return scores; // {점수, 보너스여부(1)} 객체가 한 칸씩 들어가있는 배열 반환 + } + + static getCounts(scoreObj) { + let count3 = 0, + count4 = 0, + count5 = 0, + countBonus = 0, + count6 = 0; + + scoreObj.forEach(({ score, isBonusMatched }) => { + if (score === 6) count6++; + else if (score === 5 && isBonusMatched) countBonus++; + else if (score === 5) count5++; + else if (score === 4) count4++; + else if (score === 3) count3++; + }); + + return [count3, count4, count5, countBonus, count6]; + } + + static getResultMessage(count3, count4, count5, countB, count6) { + const RESULT_MESSAGE = `당첨 통계\n---\n + 3개 일치 (5,000원) - ${count3}개\n + 4개 일치 (50,000원) - ${count4}개\n + 5개 일치 (1,500,000원) - ${count5}개\n + 5개 일치, 보너스 볼 일치 (30,000,000원) - ${countB}개\n + 6개 일치 (2,000,000,000원) - ${count6}개\n`; + return RESULT_MESSAGE; + } + + static calculateTotalPrizeMoney(countArr) { + let totalPrizeMoney = 0; + for (let i = 0; i < countArr.length; i++) { + if (i === 0) { + totalPrizeMoney += FIFTH_PRIZE_MONEY * countArr[i]; + } else if (i === 1) { + totalPrizeMoney += FOURTH_PRIZE_MONEY * countArr[i]; + } else if (i === 2) { + totalPrizeMoney += THIRD_PRIZE_MONEY * countArr[i]; + } else if (i === 3) { + totalPrizeMoney += SECOND_PRIZE_MONEY * countArr[i]; + } else if (i === 4) { + totalPrizeMoney += FIRST_PRIZE_MONEY * countArr[i]; + } + } + + return totalPrizeMoney; + } + + static getProfitRatio(lottoCost, prize) { + let ratio = prize / lottoCost; + ratio = Math.trunc(ratio * 10000); + ratio = Math.round(ratio); + ratio = ratio / 100; + + return ratio; + } + + static isValidLottoNumberRange(number) { + return number >= LOTTO_NUMBER_MIN && number <= LOTTO_NUMBER_MAX; + } + + static hasDuplicatedLottoNumber(numbers) { + const numberSet = new Set(numbers); + return numberSet.size !== numbers.length; + } } export default Lotto;