diff --git a/README.md b/README.md index 15bb106b5..f298a3d09 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ # javascript-lotto-precourse +# 로또 시뮬레이터 + +# 기능 목록 +### Pure(순수 계산: 계산/검증/도메인) +- [x] `입력값을 필요한 형태로 파싱 계산` + - [x] 구입 금액 문자열을 숫자 타입으로 변환한다. (재사용성 검토) + - [x] 구입 금액을 로또 1장의 가격으로 나눈다. + - [x] 로또 당첨 번호를 쉼표(,)를 기준으로 나누어 숫자 배열을 생성한다. + - [x] 보너스 번호 문자열을 숫자 타입으로 변환한다. +- [x] `사용자의 입력값 검증 계산` + - [x] 구입 금액이 1000 단위의 양의 정수로 이루어진 문자열인지 검증한다. + - [x] 로또 당첨 번호가 6개인지 검증한다. + - [x] 번호가 정수인지 검증한다. + - [x] 로또 당첨 번호가 중복되지 않는 지 검증한다. + - [x] 로또 당첨 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. + - [x] 보너스 번호가 1 <= n <= 45를 만족하는 정수인지 검증한다. + - [x] 보너스 번호가 당첨 번호와 중복되지 않는 지 검증한다. +- [x] `구매량 만큼 로또를 만드는 계산` + - [x] <당첨 번호 6개> 의 데이터를 생성한다. + - [x] 구매량 만큼의 로또 데이터를 생성한다. +- [x] `당첨 번호와 로또 번호를 비교/채점하는 계산` + - [x] 당첨 번호와 일치하는 번호의 수를 계산한다. + - [x] 보너스 번호가 로또 번호에 존재하는 지 계산한다. + - [x] 로또의 등수를 계산한다. +- [x] `로또 수익률을 계산` + - [x] 전체 상금을 계산한다. + - [x] 퍼센트 수익률을 계산한다. + +### Effect(부수효과 액션: 입출력/난수/에러) +- [x] `사용자의 입력 액션` + - [x] 로또 구입 금액을 읽는다. + - [x] 로또 당첨 번호를 읽는다. + - [x] 보너스 번호를 읽는다. +- [x] `에러 발생 액션` + - [x] 검증을 통과하지 못하면 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨다. +- [x] `사용자에게 재입력 액션` + - [x] 로또 구입 금액에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [x] 당첨 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. + - [x] 보너스 번호에 대한 입력값이 검증을 통과하지 못하면 해당 지점부터 다시 입력을 받는다. +- [x] `난수를 생성 액션` + - [x] 1 <= n <= 45 범위의 난수 n을 생성한다. +- [x] `로또 시뮬레이션 결과 출력 액션` + - [x] 발행한 로또의 수량과 번호을 출력한다. + - [x] 당첨 내역을 출력한다. + - [x] 수익률을 출력한다. \ No newline at end of file diff --git a/__tests__/CreatLottoDomain.test.js b/__tests__/CreatLottoDomain.test.js new file mode 100644 index 000000000..3c691fc64 --- /dev/null +++ b/__tests__/CreatLottoDomain.test.js @@ -0,0 +1,31 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import { createLottos, creatOneLotto } from "../src/domains/createLottoNumbers"; +import { randomUniquesInRange } from "../src/utils/random"; +import Lotto from "../src/entities/Lotto"; + +const mockRandoms = (numbers) => { + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); +}; + +describe("로또 번호 생성 테스트", () => { + test("1~45 사이의 중복되지 않는 숫자 6개를 반환한다.", () => { + const expectedLotto = [1, 2, 3, 4, 44, 45]; + + mockRandoms([expectedLotto]); + const lottoInstance = creatOneLotto(randomUniquesInRange); + + expect(lottoInstance).toBeInstanceOf(Lotto); + expect(lottoInstance.numbers).toEqual(expectedLotto); + }); + test("quantity만큼의 로또 세트를 반환한다..", () => { + const sample = [1, 2, 3, 4, 44, 45]; + + mockRandoms([ sample, sample, sample, sample, sample, sample ]) // 6개 모킹 + + // 5개만 생성 확인 + expect(createLottos(5, randomUniquesInRange).length).toBe(5); + }) +}) \ No newline at end of file diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..08c09bce2 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,4 +1,4 @@ -import Lotto from "../src/Lotto"; +import Lotto from "../src/entities/Lotto"; describe("로또 클래스 테스트", () => { test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { diff --git a/__tests__/ProfitDomainUnit.test.js b/__tests__/ProfitDomainUnit.test.js new file mode 100644 index 000000000..9fa3ae474 --- /dev/null +++ b/__tests__/ProfitDomainUnit.test.js @@ -0,0 +1,26 @@ +import { accumulateProfit, getRateOfInvestmentByPercent } from "../src/domains/profit"; +import { PRIZE_TABLE } from "../src/constants/lotto"; + +describe("로또 수익 관련 로직 단위 테스트", () => { + test("1등, 4등을 했을 때 2,000,050,000을 반환한다.", () => { + const rankOflottos = [1, 4] + const profit = 2_000_050_000 // 1등(2억) + 4등(5만) + expect(accumulateProfit(rankOflottos, PRIZE_TABLE)).toBe(profit); + }); + test("6등을 했을 떄는 0을 반환한다.", () => { + const rank_6th = [6, 6, 6]; + const profit = 0; + expect(accumulateProfit(rank_6th, PRIZE_TABLE)).toBe(profit); + }); + test("퍼센트 환산된 수익률을 반환한다.", () => { + // 2000 / 1000 = 2 -> 200% + expect(getRateOfInvestmentByPercent(2000, 1000)).toBe(200); + // 995 / 1000 = 0.995 -> 99.5% + expect(getRateOfInvestmentByPercent(995, 1000)).toBe(99.5); + }); + test("소수점 이하의 결과를 정확히 반환한다.", () => { + // 3333 / 7000 = 0.47614... -> 47.614..% + const expectAboutResult = 47.614 + expect(getRateOfInvestmentByPercent(3333, 7000)).toBeCloseTo(expectAboutResult); + }); +}) \ No newline at end of file diff --git a/__tests__/RankingDomainUnit.test.js b/__tests__/RankingDomainUnit.test.js new file mode 100644 index 000000000..c591509b1 --- /dev/null +++ b/__tests__/RankingDomainUnit.test.js @@ -0,0 +1,35 @@ +import { calculateMatchCount, isBonusMatch, determineRankOf } from "../src/domains/ranking"; +import { RANK_TABLE } from "../src/constants/lotto"; + +describe("순위 결정 관련 비지니스 로직 단위 테스트", () => { + test("일치하는 개수를 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const winning = [1, 2, 3, 4, 5, 6]; + expect(calculateMatchCount(ticket, winning)).toBe(6); + }); + test("하나도 일치하지 않을 경우 0을 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const winning = [7, 8, 9, 10, 11, 12]; + expect(calculateMatchCount(ticket, winning)).toBe(0); + }); + test("보너스 넘버를 포함하고 있다면 true를 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const bonusInclude = 5 + expect(isBonusMatch(ticket, bonusInclude)).toBe(true); + }); + test("보너스 넘버를 포함하고 있지않다면 false를 반환한다.", () => { + const ticket = [1, 2, 3, 4, 5, 6]; + const bonusNotInclude = 45; + expect(isBonusMatch(ticket, bonusNotInclude)).toBe(false); + }); + test("일치하는 번호의 수가 5개이고 보너스 번호가 일치하면 2등이다.", () => { + const matchedCnt = 5; + const isBonusMatch = true; + expect(determineRankOf(matchedCnt, isBonusMatch, RANK_TABLE)).toBe(2); + }) + test("일치하는 번호의 수가 5개이고 보너스 번호가 일치하지 않으면 3등이다.", () => { + const matchedCnt = 5; + const isBonusMatch = false; + expect(determineRankOf(matchedCnt, isBonusMatch, RANK_TABLE)).toBe(3); + }) +}); \ No newline at end of file diff --git a/__tests__/UtilUnit.test.js b/__tests__/UtilUnit.test.js new file mode 100644 index 000000000..452add5bf --- /dev/null +++ b/__tests__/UtilUnit.test.js @@ -0,0 +1,13 @@ +import { toNumber, parseToArrayByComma, devisionNumber} from "../src/utils/parsing"; + +describe("파싱 유틸 검증", () => { + test("숫자로 이루어진 문자열을 숫자 타입으로 변환한다.", () => { + expect(toNumber("123")).toBe(123); + }); + test("쉼표를 포함하는 문자열을 쉼표로 나누어 숫자 배열로 변환한다.", () => { + expect(parseToArrayByComma("1,2,3")).toStrictEqual([1, 2, 3]); + }); + test("구입 금액을 단위(1000)으로 나누어 티켓 장수를 반환하다.", () => { + expect(devisionNumber(5000, 1000)).toBe(5); + }) +}) \ No newline at end of file diff --git a/__tests__/ValidateDomainUnit.test.js b/__tests__/ValidateDomainUnit.test.js new file mode 100644 index 000000000..cd1958f0e --- /dev/null +++ b/__tests__/ValidateDomainUnit.test.js @@ -0,0 +1,43 @@ +import { validateLottoNumbers, validateBonusNumber, validateCost } from "../src/domains/validate"; +import { ERROR_MSG } from "../src/constants/lotto"; + +describe("당첨 번호 도메인 검증", () => { + test("로또 번호는 6개이어야 한다.", () => { + expect(validateLottoNumbers([1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_SIZE); + }); + test("로또 번호는 정수이어야 한다.", () => { + expect(validateLottoNumbers(["m", 1, 2, 3, 4, 5])).toBe(ERROR_MSG.NUMBER_INTEGER); + expect(validateLottoNumbers([1.1, 1, 2, 3, 4, 5])).toBe(ERROR_MSG.NUMBER_INTEGER); + }) + test("각 로또 번호는 1~45 범위 안에 있어야 한다.", () => { + expect(validateLottoNumbers([0, 1, 2, 3, 4, 5])).toBe(ERROR_MSG.LOTTO_NUM_RANGE); // < 1 + expect(validateLottoNumbers([1, 2, 3, 4, 5, 46])).toBe(ERROR_MSG.LOTTO_NUM_RANGE); // > 45 + }); + test("로또 번호는 중복될 수 없다.", () => { + expect(validateLottoNumbers([1, 2, 3, 4, 5, 5])).toBe(ERROR_MSG.LOTTO_NUM_UNIQUE); + }); + test("정상적인 로또 번호는 true를 반환한다..", () => { + expect(validateLottoNumbers([1, 2, 3, 4, 5, 45])).toBe(true); + }); +}); + +describe("보너스 번호 도메인 검증", () => { + test("보너스 번호는 1~45 범위 안에 있어야 한다.", () => { + expect(validateBonusNumber(46)).toBe(ERROR_MSG.LOTTO_NUM_RANGE); + }); + test("보너스 번호는 로또 번호와 중복될 수 없다.", () => { + expect(validateBonusNumber(4, [1, 2, 3, 4, 5, 6])).toBe(ERROR_MSG.LOTTO_NUM_UNIQUE); + }); + test("정상적인 보너스 번호는 true를 반환한다.", () => { + expect(validateBonusNumber(25, [1, 2, 3, 4, 5, 6])).toBe(true); + }) +}); + +describe("구입 금액 도메인 검증", () => { + test("구입 금액은 단위가 1000이어야 한다.", () => { + expect(validateCost(2500)).toBe(ERROR_MSG.COST_UNIT); + }); + test("정상적인 구입 금액은 true를 반환한다.", () => { + expect(validateCost(2000)).toBe(true); + }) +}) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5d..a5573d474 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,38 @@ +import { LABELS, OUTPUT_MSG } from "./constants/ioMsg"; +import { LOTTO_CONSTANTS, PRIZE_TABLE, RANK_TABLE } from "./constants/lotto"; +import { readBonusNumberUntilValid, readPurchasedAmountUntilValid, readWinningNumbersUntilValid } from "./view/input"; +import { devisionNumber } from "./utils/parsing"; +import { createLottos } from "./domains/createLottoNumbers"; +import { MissionUtils } from "@woowacourse/mission-utils"; +import { randomUniquesInRange } from "./utils/random"; +import { calculateMatchCount, determineRankOf, getResultOfLotto, isBonusMatch } from "./domains/ranking"; +import { accumulateProfit, getRateOfInvestmentByPercent } from "./domains/profit"; +import { printGeneratedLottos, printResultStats } from "./view/output"; + class App { - async run() {} + async run() { + const purchasedAmount = await readPurchasedAmountUntilValid(); + const ticketAmount = devisionNumber(purchasedAmount, LOTTO_CONSTANTS.TICKET_PRICE); + + const lottos = createLottos(ticketAmount, randomUniquesInRange); // 난수 생성 함수를 주입 + + printGeneratedLottos(ticketAmount, lottos); + + const winningNums = await readWinningNumbersUntilValid(); + const bonusNum = await readBonusNumberUntilValid(winningNums); + + const results = getResultOfLotto(lottos, winningNums, bonusNum); + const rankResultArr = results.map(({rank}) => rank); + const totalProfit = accumulateProfit(rankResultArr, PRIZE_TABLE); + const rateOfInvestment = getRateOfInvestmentByPercent(totalProfit, purchasedAmount); // + + const rankCounts = results.reduce((acc, { rank }) => { + if (rank) acc[rank] = (acc[rank] || 0) + 1; + return acc; + }, {}); + + printResultStats(rankCounts, rateOfInvestment); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js deleted file mode 100644 index cb0b1527e..000000000 --- a/src/Lotto.js +++ /dev/null @@ -1,18 +0,0 @@ -class Lotto { - #numbers; - - constructor(numbers) { - this.#validate(numbers); - this.#numbers = numbers; - } - - #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} - -export default Lotto; diff --git a/src/constants/ioMsg.js b/src/constants/ioMsg.js new file mode 100644 index 000000000..0c635f1c9 --- /dev/null +++ b/src/constants/ioMsg.js @@ -0,0 +1,20 @@ +export const INPUT_QUESTION = Object.freeze({ + COST: "구입금액을 입력해 주세요.", + WINNING_NUMS: "당첨 번호를 입력해 주세요.", + BONUS_NUM: "보너스 번호를 입력해 주세요.", +}); + +export const OUTPUT_MSG = Object.freeze({ + PURCHASED_TICKETS: (count) => `${count}개를 구매했습니다.`, + WINNING_STATS_HEADER: "\n당첨 통계", + WINNING_STATS_DIVIDER: "---", + ROI_RESULT: (roi) => `총 수익률은 ${roi}%입니다.`, +}); + +export const LABELS = { + 5: "3개 일치", + 4: "4개 일치", + 3: "5개 일치", + 2: "5개 일치, 보너스 볼 일치", + 1: "6개 일치", +}; diff --git a/src/constants/lotto.js b/src/constants/lotto.js new file mode 100644 index 000000000..53658c41f --- /dev/null +++ b/src/constants/lotto.js @@ -0,0 +1,33 @@ +export const LOTTO_CONSTANTS = Object.freeze({ + TICKET_PRICE: 1000, + MIN_NUMBER: 1, + MAX_NUMBER: 45, + NUMBERS_PER_TICKET: 6, +}); + +export const RANK_TABLE = Object.freeze({ // 일치하는 숫자 개수 : { bonusTrue : 랭크, bonusFalse: 랭크} + 6: Object.freeze({true: 1, false: 2,}), + 5: Object.freeze({true: 2, false: 3,}), + 4: Object.freeze({true: 4, false: 4,}), + 3: Object.freeze({true: 5, false: 5,}), + 2: Object.freeze({true: 6, false: 6,}), + 1: Object.freeze({true: 6, false: 6,}), + 0: Object.freeze({true: 6, false: 6,}), +}); + +export const PRIZE_TABLE = Object.freeze({ + 1: 2_000_000_000, + 2: 30_000_000, // 5개 일치 + 보너스 번호 일치 + 3: 1_500_000, + 4: 50_000, + 5: 5_000, + 6: 0, +}); + +export const ERROR_MSG = Object.freeze({ + COST_UNIT: "[ERROR] 구입 금액은 1000원 단위의 숫자여야 합니다.\n", + LOTTO_SIZE: "[ERROR] 로또 번호는 6개여야 합니다.\n", + LOTTO_NUM_RANGE: "[ERROR] 로또 번호는 1~45만으로 이루어집니다.\n", + LOTTO_NUM_UNIQUE: "[ERROR] 하나의 로또에 중복된 숫자가 존재할 수 없습니다.\n", + NUMBER_INTEGER: "[ERROR] 로또 번호는 정수입니다.\n" +}) \ No newline at end of file diff --git a/src/domains/createLottoNumbers.js b/src/domains/createLottoNumbers.js new file mode 100644 index 000000000..2f780749a --- /dev/null +++ b/src/domains/createLottoNumbers.js @@ -0,0 +1,14 @@ +import { LOTTO_CONSTANTS } from "../constants/lotto" +import Lotto from "../entities/Lotto"; + +export function creatOneLotto(drawUniqueNumbers) { + const { MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET } = LOTTO_CONSTANTS + const numbers = drawUniqueNumbers( MIN_NUMBER, MAX_NUMBER, NUMBERS_PER_TICKET ); + return new Lotto(numbers); +}; + +export function createLottos(quantity, drawUniqueNumbers) { + return Array.from({ length: quantity }, () => + creatOneLotto(drawUniqueNumbers) + ); +}; diff --git a/src/domains/lottoRules.js b/src/domains/lottoRules.js new file mode 100644 index 000000000..0a0668e20 --- /dev/null +++ b/src/domains/lottoRules.js @@ -0,0 +1,36 @@ +import { LOTTO_CONSTANTS, ERROR_MSG } from "../constants/lotto" +import { toArray, includesNumber, isIntegerValue } from "../utils"; + +// 로또의 비지니스 규칙과 관련된 검증들 +export const isValidPurchaseAmount = (amount) => + amount % LOTTO_CONSTANTS.TICKET_PRICE === 0 || ERROR_MSG.COST_UNIT; + +export const hasExactSize = (arr) => + arr.length === LOTTO_CONSTANTS.NUMBERS_PER_TICKET || ERROR_MSG.LOTTO_SIZE + +export const inRange = (valueOrArr) => { + const arr = toArray(valueOrArr); + return arr.every( // every: 함수형, 즉시 종료 + n => + n >= LOTTO_CONSTANTS.MIN_NUMBER && + n <= LOTTO_CONSTANTS.MAX_NUMBER + ) || ERROR_MSG.LOTTO_NUM_RANGE; +} + +export const isIntegerArr = (valueOrArr) => { + const arr = toArray(valueOrArr); + return arr.every( + n => isIntegerValue(n) + ) || ERROR_MSG.NUMBER_INTEGER; +} + +export const isLottoNumUnique = (arr) => + new Set(arr).size === arr.length || ERROR_MSG.LOTTO_NUM_UNIQUE; + +export const isBonusUnique = (num, arr) => + !includesNumber(arr, num) || ERROR_MSG.LOTTO_NUM_UNIQUE; + +export const costRules = [isIntegerArr, isValidPurchaseAmount]; +export const lottoRules = [hasExactSize, isIntegerArr, inRange, isLottoNumUnique]; +export const bonusRules = [isIntegerArr, inRange, isBonusUnique]; + diff --git a/src/domains/profit.js b/src/domains/profit.js new file mode 100644 index 000000000..537409b22 --- /dev/null +++ b/src/domains/profit.js @@ -0,0 +1,15 @@ +export function accumulateProfit(rankArr, PRIZE_TABLE) { + const totalProfit = rankArr.reduce( + (profit, rank) => profit + PRIZE_TABLE[rank], + 0 + ); + return totalProfit; +}; + +// 출력 요구사항에 "수익률은 소수점 둘째 자리에서 반올림한다"를 통해 반올림만 명시 +// -> 퍼센트 변환는 계산, 반올림은 표현의 영역이라고 판단 +export function getRateOfInvestmentByPercent(totalProfit, investment) { + const ratio = totalProfit / investment; + const ratioByPercent = ratio * 100; + return ratioByPercent; +} diff --git a/src/domains/ranking.js b/src/domains/ranking.js new file mode 100644 index 000000000..2c23cc41e --- /dev/null +++ b/src/domains/ranking.js @@ -0,0 +1,25 @@ +import { includesNumber } from "../utils"; +import { RANK_TABLE } from "../constants/lotto"; + +export function calculateMatchCount(ticket, winning, numbersPerTicket = 6) { + const uniqueNumbers = new Set([...ticket, ...winning]); + return numbersPerTicket * 2 - uniqueNumbers.size; +}; + +export function isBonusMatch(ticket, bonusNum) { + return includesNumber(ticket, bonusNum); +}; + +export function determineRankOf(matchedCnt, bonusFlag, RANK_TABLE) { + const rank = RANK_TABLE[matchedCnt][bonusFlag]; + return rank; +}; + +export function getResultOfLotto(lottos, winningNums, bonusNum) { + return lottos.map((lotto) => { + const matched = calculateMatchCount(lotto.numbers, winningNums); + const bonusFlag = isBonusMatch(lotto.numbers, bonusNum); + const rank = determineRankOf(matched, bonusFlag, RANK_TABLE); + return { matched, bonusFlag, rank }; + }); +}; diff --git a/src/domains/validate.js b/src/domains/validate.js new file mode 100644 index 000000000..e2d88693e --- /dev/null +++ b/src/domains/validate.js @@ -0,0 +1,25 @@ +import { costRules,lottoRules, bonusRules } from "./lottoRules"; + +export function validateCost(cost) { + for(const rule of costRules) { + const result = rule(cost); + if(result !== true) return result; + } + return true; +} + +export function validateLottoNumbers(lottoArr) { + for(const rule of lottoRules) { + const result = rule(lottoArr); + if(result !== true) return result; // err_msg + } + return true; // 모두 통과 +} + +export function validateBonusNumber(bonusNum, lottoArr) { + for(const rule of bonusRules) { + const result = rule(bonusNum, lottoArr); + if(result !== true) return result; + } + return true; +} \ No newline at end of file diff --git a/src/entities/Lotto.js b/src/entities/Lotto.js new file mode 100644 index 000000000..4ad271d2a --- /dev/null +++ b/src/entities/Lotto.js @@ -0,0 +1,21 @@ +import { validateLottoNumbers } from "../domains/validate"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + const validateResult = validateLottoNumbers(numbers); + if(validateResult !== true) throw new Error(validateResult); + } + + get numbers() { + return this.#numbers; + } +} + +export default Lotto; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 000000000..772e67318 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,10 @@ +// 재사용을 하기에 util로 분리 +export const includesNumber = (arr, num) => arr.includes(num); + +export const toArray = (value) => { + if(Array.isArray(value)) return value; + return [value]; +} + +export const isIntegerValue = (value) => Number.isInteger(value); +export const isNaNValue = (value) => Number.isNaN(value); \ No newline at end of file diff --git a/src/utils/parsing.js b/src/utils/parsing.js new file mode 100644 index 000000000..db6b60e5c --- /dev/null +++ b/src/utils/parsing.js @@ -0,0 +1,5 @@ +export const toNumber = (str) => Number(str); + +export const parseToArrayByComma = (arr) => arr.split(",").map(n => toNumber(n.trim())); + +export const devisionNumber = (divdend, divisor) => divdend / divisor; \ No newline at end of file diff --git a/src/utils/random.js b/src/utils/random.js new file mode 100644 index 000000000..2d6832a52 --- /dev/null +++ b/src/utils/random.js @@ -0,0 +1,4 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; + +export const randomUniquesInRange = (min, max, quantity) => + MissionUtils.Random.pickUniqueNumbersInRange(min, max, quantity); \ No newline at end of file diff --git a/src/view/askUntilValid.js b/src/view/askUntilValid.js new file mode 100644 index 000000000..2e3139077 --- /dev/null +++ b/src/view/askUntilValid.js @@ -0,0 +1,14 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +export async function askUntilValid({ question, parse, validate }) { + while (true) { + try { + const raw = (await MissionUtils.Console.readLineAsync(question)).trim(); + const parsed = parse(raw); + const value = validate(parsed); + if(value !== true) throw new Error(value); + return parsed; + } catch (e) { + MissionUtils.Console.print(e.message); // "[ERROR] ..." 출력 후 루프 지속 + } + } +} diff --git a/src/view/input.js b/src/view/input.js new file mode 100644 index 000000000..8d2e386a5 --- /dev/null +++ b/src/view/input.js @@ -0,0 +1,28 @@ +import { askUntilValid } from "./askUntilValid"; +import { INPUT_QUESTION } from "../constants/ioMsg"; +import { parseToArrayByComma, toNumber } from "../utils/parsing"; +import { validateBonusNumber, validateCost, validateLottoNumbers } from "../domains/validate"; + +export async function readPurchasedAmountUntilValid() { + return askUntilValid({ + question: INPUT_QUESTION.COST, + parse: toNumber, + validate: validateCost, + }); +} + +export async function readWinningNumbersUntilValid() { + return askUntilValid({ + question: INPUT_QUESTION.WINNING_NUMS, + parse: parseToArrayByComma, + validate: validateLottoNumbers, + }); +} + +export async function readBonusNumberUntilValid(winning) { + return askUntilValid({ + question: INPUT_QUESTION.BONUS_NUM, + parse: toNumber, + validate: (n) => validateBonusNumber(n, winning), + }); +} \ No newline at end of file diff --git a/src/view/output.js b/src/view/output.js new file mode 100644 index 000000000..7faf51d31 --- /dev/null +++ b/src/view/output.js @@ -0,0 +1,24 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +import { OUTPUT_MSG, LABELS } from "../constants/ioMsg"; +import { PRIZE_TABLE } from "../constants/lotto"; + +export function printGeneratedLottos(ticketAmount, lottos) { + printUsingWoowa(OUTPUT_MSG.PURCHASED_TICKETS(ticketAmount)) + lottos.forEach((lotto) => { + printUsingWoowa(`[${lotto.numbers.join(", ")}]`); + }); +} + +export function printResultStats(rankCounts, roi) { + printUsingWoowa(OUTPUT_MSG.WINNING_STATS_HEADER); + printUsingWoowa(OUTPUT_MSG.WINNING_STATS_DIVIDER); + Object.entries(PRIZE_TABLE).forEach(([rank, prize]) => { + const count = rankCounts[rank] || 0; + printUsingWoowa(`${LABELS[rank]} (${prize.toLocaleString()}원) - ${count}개`); + }); + printUsingWoowa(OUTPUT_MSG.ROI_RESULT(roi)); +} + +function printUsingWoowa(msg) { + MissionUtils.Console.print(msg); +} \ No newline at end of file