diff --git a/README.md b/README.md index 15bb106b5..4559a2dbd 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# javascript-lotto-precourse +๐ŸŽฐ Lotto Program + +์ฝ˜์†” ๊ธฐ๋ฐ˜ ๋กœ๋˜ ๋ฐœ๋งค ๋ฐ ๋‹น์ฒจ ๊ฒฐ๊ณผ ํ™•์ธ ํ”„๋กœ๊ทธ๋žจ + +์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ์ž… ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜๋ฉด, ์ž๋™์œผ๋กœ ๋กœ๋˜๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ  + +๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ๊ฒฐ๊ณผ ๋ฐ ์ˆ˜์ต๋ฅ ์„ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + + +๐Ÿš€ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +์ด ํ”„๋กœ๊ทธ๋žจ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ธˆ์•ก์— ๋งž๊ฒŒ ๋กœ๋˜๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ , + +๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ๋น„๊ตํ•˜์—ฌ ๊ฐ ๋“ฑ์ˆ˜๋ณ„ ๋‹น์ฒจ ๋‚ด์—ญ๊ณผ ์ด ์ˆ˜์ต๋ฅ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + +์ž…๋ ฅ ๊ฒ€์ฆ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, ๋‚œ์ˆ˜ ์ƒ์„ฑ ๋“ฑ ๊ธฐ๋ณธ์ ์ธ ๋กœ์ง ์„ค๊ณ„ ๋Šฅ๋ ฅ์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ ์ฝ˜์†” ๋ฏธ์…˜์ž…๋‹ˆ๋‹ค. + + +๐Ÿ“ ๊ธฐ๋Šฅ ๋ชฉ๋ก + +[V] ๋กœ๋˜ ๊ตฌ์ž… ๊ธˆ์•ก ์ž…๋ ฅ + +[V] ๋กœ๋˜ ๋ฐœํ–‰ + +[V] Lotto ํด๋ž˜์Šค + +[V] ๋กœ๋˜ ์ถœ๋ ฅ + +[V] ๋‹น์ฒจ ๋ฒˆํ˜ธ ์ž…๋ ฅ + +[V] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ž…๋ ฅ + +[V] ๋‹น์ฒจ ๋‚ด์—ญ ๊ณ„์‚ฐ + +[V] ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ ๋ฐ ์ถœ๋ ฅ \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5d..a9687d629 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,82 @@ +import InputView from './InputView.js'; +import OutputView from './OutputView.js'; +import Lotto from './Lotto.js'; +import Validator from './Validator.js'; +import LottoIssuer from './LottoIssuer.js'; +import StatisticsCalculator from './StatisticsCalculator.js'; + class App { - async run() {} + #lottos; + #purchaseAmount; + + async run() { + await this.#getPurchaseAmount(); + this.#issueLottos(); + + const winningLotto = await this.#getWinningLotto(); + const bonusNumber = await this.#getBonusNumber(winningLotto); + + this.#showResults(winningLotto, bonusNumber); + } + + async #getPurchaseAmount() { + while (true) { + try { + const amountStr = await InputView.readPurchaseAmount(); + const amount = Number(amountStr); + Validator.validatePurchaseAmount(amount); + this.#purchaseAmount = amount; + return; + } catch (error) { + OutputView.printError(error.message); + } + } + } + + #issueLottos() { + this.#lottos = LottoIssuer.issueLottos(this.#purchaseAmount); + OutputView.printPurchaseCount(this.#lottos.length); + OutputView.printLottos(this.#lottos); + } + + async #getWinningLotto() { + while (true) { + try { + const numbersStr = await InputView.readWinningNumbers(); + const numbers = numbersStr.split(',').map(Number); + Validator.validateWinningNumbers(numbers); + return new Lotto(numbers); + } catch (error) { + OutputView.printError(error.message); + } + } + } + + async #getBonusNumber(winningLotto) { + while (true) { + try { + const bonusStr = await InputView.readBonusNumber(); + const bonusNumber = Number(bonusStr); + Validator.validateBonusNumber(bonusNumber, winningLotto.getNumbers()); + return bonusNumber; + } catch (error) { + OutputView.printError(error.message); + } + } + } + + #showResults(winningLotto, bonusNumber) { + const results = StatisticsCalculator.calculateResults( + this.#lottos, winningLotto, bonusNumber + ); + + const profitRate = StatisticsCalculator.calculateProfitRate( + results, this.#purchaseAmount + ); + + OutputView.printStatistics(results); + OutputView.printProfitRate(profitRate); + } } -export default App; +export default App; \ No newline at end of file diff --git a/src/Constants.js b/src/Constants.js new file mode 100644 index 000000000..03be26cae --- /dev/null +++ b/src/Constants.js @@ -0,0 +1,26 @@ +export const LOTTO_RULES = Object.freeze({ + NUMBER_COUNT: 6, + MIN_NUMBER: 1, + MAX_NUMBER: 45, + PRICE: 1000, +}); + +export const ERROR_MESSAGES = Object.freeze({ + PREFIX: '[ERROR]', + NOT_A_NUMBER: '์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', + INVALID_AMOUNT_UNIT: `๊ตฌ์ž… ๊ธˆ์•ก์€ ${LOTTO_RULES.PRICE}์› ๋‹จ์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + AMOUNT_LESS_THAN_PRICE: `๊ตฌ์ž… ๊ธˆ์•ก์€ ${LOTTO_RULES.PRICE}์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + INVALID_NUMBER_COUNT: `๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.NUMBER_COUNT}๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + DUPLICATE_NUMBERS: '๋กœ๋˜ ๋ฒˆํ˜ธ์— ์ค‘๋ณต๋œ ์ˆซ์ž๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.', + OUT_OF_RANGE: `๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.MIN_NUMBER}๋ถ€ํ„ฐ ${LOTTO_RULES.MAX_NUMBER} ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + DUPLICATE_WITH_WINNING_NUMBERS: '๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๊ฐ€ ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค.', +}); + +export const WINNING_RANKS = Object.freeze({ + FIRST: { match: 6, prize: 2_000_000_000, name: 'FIRST' }, + SECOND: { match: 5, prize: 30_000_000, name: 'SECOND' }, + THIRD: { match: 5, prize: 1_500_000, name: 'THIRD' }, + FOURTH: { match: 4, prize: 50_000, name: 'FOURTH' }, + FIFTH: { match: 3, prize: 5_000, name: 'FIFTH' }, + NONE: { match: 0, prize: 0, name: 'NONE' }, +}); diff --git a/src/InputView.js b/src/InputView.js new file mode 100644 index 000000000..37f0b2774 --- /dev/null +++ b/src/InputView.js @@ -0,0 +1,18 @@ +import { Console } from '@woowacourse/mission-utils'; +import { PROMPT_MESSAGES } from './Constants.js'; + +const InputView = { + async readPurchaseAmount() { + return Console.readLineAsync(PROMPT_MESSAGES.GET_PURCHASE_AMOUNT); + }, + + async readWinningNumbers() { + return Console.readLineAsync(PROMPT_MESSAGES.GET_WINNING_NUMBERS); + }, + + async readBonusNumber() { + return Console.readLineAsync(PROMPT_MESSAGES.GET_BONUS_NUMBER); + }, +}; + +export default InputView; \ No newline at end of file diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..00671213b 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,18 +1,58 @@ +import { LOTTO_RULES, ERROR_MESSAGES, WINNING_RANKS } from "./Constants"; + class Lotto { #numbers; constructor(numbers) { this.#validate(numbers); - this.#numbers = numbers; + this.#numbers = numbers.sort((a, b) => a - b); } #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 6๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + if (numbers.length !== LOTTO_RULES.NUMBER_COUNT) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.INVALID_NUMBER_COUNT}`); + } + + if (new Set(numbers).size !== LOTTO_RULES.NUMBER_COUNT) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.DUPLICATE_NUMBERS}`); + } + + if (numbers.some(n => n < LOTTO_RULES.MIN_NUMBER || n > LOTTO_RULES.MAX_NUMBER)) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.OUT_OF_RANGE}`); + } + } + + getNumbers() { + return this.#numbers.#numbers; + } + + calculateRank(winningLotto, bonusNumber) { + const winningNumbers = winningLotto.getNumbers(); + + const matchCount = this.#numbers.filter(number => + winningNumbers.includes(number) + ).length; + + const hasBonus = this.#numbers.includes(bonusNumber); + + if (matchCount === WINNING_RANKS.FIRST.match) { + return WINNING_RANKS.FIRST.name; + } + if (matchCount === WINNING_RANKS.SECOND.match && hasBonus) { + return WINNING_RANKS.SECOND.name; + } + if (matchCount === WINNING_RANKS.THIRD.match) { + return WINNING_RANKS.THIRD.name; + } + if (matchCount === WINNING_RANKS.FOURTH.match) { + return WINNING_RANKS.FOURTH.name; + } + if (matchCount === WINNING_RANKS.FIFTH.match) { + return WINNING_RANKS.FIFTH.name; } + return WINNING_RANKS.NONE.name; } - // TODO: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ } export default Lotto; diff --git a/src/LottoIssuer.js b/src/LottoIssuer.js new file mode 100644 index 000000000..df5894b77 --- /dev/null +++ b/src/LottoIssuer.js @@ -0,0 +1,22 @@ +import { Random } from "@woowacourse/mission-utils"; +import { LOTTO_RULES } from "./Constants"; +import Lotto from "./Lotto"; + +const LottoIssuer = { + issueLottos(purchaseAmount) { + const count = purchaseAmount / LOTTO_RULES.PRICE; + const lottos = []; + + for (let i = 0; i < count; i += 1) { + const numbers = Random.pickUniqueNumbersInRange( + LOTTO_RULES.MIN_NUMBER, + LOTTO_RULES.MAX_NUMBER, + LOTTO_RULES.NUMBER_COUNT + ); + lottos.push(new Lotto(numbers)); + } + return lottos; + }, +}; + +export default LottoIssuer; diff --git a/src/OutputView.js b/src/OutputView.js new file mode 100644 index 000000000..3917400e4 --- /dev/null +++ b/src/OutputView.js @@ -0,0 +1,40 @@ +import { Console } from '@woowacourse/mission-utils'; +import { WINNING_RANKS } from './Constants.js'; + +function formatPrize(prize) { + return prize.toLocaleString('ko-KR'); +} + +const OutputView = { + printPurchaseCount(count) { + Console.print(`\n${count}๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.`); + }, + + printLottos(lottos) { + lottos.forEach(lotto => { + Console.print(`[${lotto.getNumbers().join(', ')}]`); + }); + }, + + printStatistics(results) { + Console.print('\n๋‹น์ฒจ ํ†ต๊ณ„\n---'); + + const R = WINNING_RANKS; + + Console.print(`3๊ฐœ ์ผ์น˜ (${formatPrize(R.FIFTH.prize)}์›) - ${results[R.FIFTH.name]}๊ฐœ`); + Console.print(`4๊ฐœ ์ผ์น˜ (${formatPrize(R.FOURTH.prize)}์›) - ${results[R.FOURTH.name]}๊ฐœ`); + Console.print(`5๊ฐœ ์ผ์น˜ (${formatPrize(R.THIRD.prize)}์›) - ${results[R.THIRD.name]}๊ฐœ`); + Console.print(`5๊ฐœ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ณผ ์ผ์น˜ (${formatPrize(R.SECOND.prize)}์›) - ${results[R.SECOND.name]}๊ฐœ`); + Console.print(`6๊ฐœ ์ผ์น˜ (${formatPrize(R.FIRST.prize)}์›) - ${results[R.FIRST.name]}๊ฐœ`); + }, + + printProfitRate(profitRate) { + Console.print(`์ด ์ˆ˜์ต๋ฅ ์€ ${profitRate}%์ž…๋‹ˆ๋‹ค.`); + }, + + printError(message) { + Console.print(message); + }, +}; + +export default OutputView; \ No newline at end of file diff --git a/src/StatisticsCalculator.js b/src/StatisticsCalculator.js new file mode 100644 index 000000000..c14319d85 --- /dev/null +++ b/src/StatisticsCalculator.js @@ -0,0 +1,38 @@ +import { WINNING_RANKS } from './Constants.js'; + +const StatisticsCalculator = { + calculateResults(lottos, winningLotto, bonusNumber) { + const results = { + [WINNING_RANKS.FIRST.name]: 0, + [WINNING_RANKS.SECOND.name]: 0, + [WINNING_RANKS.THIRD.name]: 0, + [WINNING_RANKS.FOURTH.name]: 0, + [WINNING_RANKS.FIFTH.name]: 0, + }; + + lottos.forEach(lotto => { + const rank = lotto.calculateRank(winningLotto, bonusNumber); + if (rank !== WINNING_RANKS.NONE.name) { + results[rank] += 1; + } + }); + + return results; + }, + + calculateProfitRate(results, purchaseAmount) { + let totalPrize = 0; + + Object.values(WINNING_RANKS).forEach(rankInfo => { + if (rankInfo.name !== WINNING_RANKS.NONE.name) { + totalPrize += results[rankInfo.name] * rankInfo.prize; + } + }); + + const profitRate = (totalPrize / purchaseAmount) * 100; + + return Math.round(profitRate * 10) / 10; + }, +}; + +export default StatisticsCalculator; \ No newline at end of file diff --git a/src/Validator.js b/src/Validator.js new file mode 100644 index 000000000..1032a7e91 --- /dev/null +++ b/src/Validator.js @@ -0,0 +1,38 @@ +import { LOTTO_RULES, ERROR_MESSAGES } from "./Constants"; +import Lotto from "./Lotto"; + +const Validator = { + validatePurchaseAmount(amount) { + if (isNaN(amount)) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.NOT_A_NUMBER}`); + } + if (amount < LOTTO_RULES.PRICE) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.AMOUNT_LESS_THAN_PRICE}`); + } + if (amount % LOTTO_RULES.PRICE !== 0) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.INVALID_AMOUNT_UNIT}`); + } + }, + + validateWinningNumbers(numbers) { + try { + new Lotto(numbers); + } catch (error) { + throw error; + } + }, + + validateBonusNumber(bonusNumber, winningNumbers) { + if (isNaN(bonusNumber)) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.NOT_A_NUMBER}`); + } + if (bonusNumber < LOTTO_RULES.MIN_NUMBER || bonusNumber > LOTTO_RULES.MAX_NUMBER) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.OUT_OF_RANGE}`); + } + if (winningNumbers.includes(bonusNumber)) { + throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.DUPLICATE_WITH_WINNING_NUMBERS}`); + } + }, +}; + +export default Validator; \ No newline at end of file diff --git a/test/Lotto.test.js b/test/Lotto.test.js new file mode 100644 index 000000000..9752d3230 --- /dev/null +++ b/test/Lotto.test.js @@ -0,0 +1,31 @@ +import Lotto from "../src/Lotto"; + +describe('Lotto ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ', () => { + test('๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ 6๊ฐœ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => new Lotto([1, 2, 3, 4, 5])).toThrow('[ERROR]'); + }); + + test('๋กœ๋˜ ๋ฒˆํ˜ธ์— ์ค‘๋ณต๋œ ์ˆซ์ž๊ฐ€ ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => new Lotto([1, 2, 3, 4, 5, 5])).toThrow('[ERROR]'); + }); + + test('๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ 1~45 ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.', () => { + expect(() => new Lotto([1, 2, 3, 4, 5, 46])).toThrow('[ERROR]'); + }); + + test('๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ 1๋ณด๋‹ค ์ž‘์€ ์ˆซ์ž๊ฐ€ ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.', () => { + expect(() => new Lotto([0, 1, 2, 3, 4, 5])).toThrow('[ERROR]'); + }); + + test('์œ ํšจํ•œ ๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ ์ฃผ์–ด์ง€๋ฉด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.', () => { + const numbers = [1, 2, 3, 4, 5, 6]; + const lotto = new Lotto(numbers); + expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + test('๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ƒ์„ฑ ์‹œ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ๋‹ค.', () => { + const numbers = [6, 5, 4, 3, 2, 1]; + const lotto = new Lotto(numbers); + expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5, 6]); + }); +}); \ No newline at end of file diff --git a/test/Validator.test.js b/test/Validator.test.js new file mode 100644 index 000000000..df57f7ce2 --- /dev/null +++ b/test/Validator.test.js @@ -0,0 +1,34 @@ +import Validator from "../src/Validator"; + +describe('Validator ํ…Œ์ŠคํŠธ', () => { + describe('๊ตฌ์ž… ๊ธˆ์•ก ๊ฒ€์ฆ', () => { + test('์ˆซ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ', () => { + expect(() => Validator.validatePurchaseAmount('1000j')).toThrow('[ERROR]'); + }); + test('1000์› ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ', () => { + expect(() => Validator.validatePurchaseAmount(500)).toThrow('[ERROR]'); + }); + test('1000์› ๋‹จ์œ„๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ', () => { + expect(() => Validator.validatePurchaseAmount(1500)).toThrow('[ERROR]'); + }); + test('์œ ํšจํ•œ ๊ธˆ์•ก', () => { + expect(() => Validator.validatePurchaseAmount(2000)).not.toThrow(); + }); + }); + + describe('๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ๊ฒ€์ฆ', () => { + const winningNumbers = [1, 2, 3, 4, 5, 6]; + test('์ˆซ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ', () => { + expect(() => Validator.validateBonusNumber('a', winningNumbers)).toThrow('[ERROR]'); + }); + test('๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ', () => { + expect(() => Validator.validateBonusNumber(46, winningNumbers)).toThrow('[ERROR]'); + }); + test('๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋˜๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ', () => { + expect(() => Validator.validateBonusNumber(6, winningNumbers)).toThrow('[ERROR]'); + }); + test('์œ ํšจํ•œ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ', () => { + expect(() => Validator.validateBonusNumber(7, winningNumbers)).not.toThrow(); + }); + }); +}); \ No newline at end of file