diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000..f9a104c5fc --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,28 @@ +module.exports = { + env: { + es2021: true, + node: true, + jest: true, + }, + extends: 'airbnb-base', + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, + }, + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + 'import/extensions': 'off', + 'max-depth': ['error', 2], + 'max-lines-per-function': ['error', { max: 15 }], + }, +}; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..acb462469b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "semi": true, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "auto" +} diff --git a/__tests__/BonusTest.js b/__tests__/BonusTest.js new file mode 100644 index 0000000000..9b034ba75e --- /dev/null +++ b/__tests__/BonusTest.js @@ -0,0 +1,29 @@ +import Bonus from '../src/Bonus.js'; + +describe('보너스 클래스 테스트', () => { + const winningNumbers = [1, 2, 3, 4, 5, 6]; + test('보너스 번호가 숫자가 아니면 예외가 발생한다.', () => { + const inputBonusNumber = 'ㄱ'; + expect(() => { + new Bonus(winningNumbers, inputBonusNumber); + }).toThrow('[ERROR]'); + }); + test('보너스 번호가 1보다 작으면 예외가 발생한다.', () => { + const inputBonusNumber = -1; + expect(() => { + new Bonus(winningNumbers, inputBonusNumber); + }).toThrow('[ERROR]'); + }); + test('보너스 번호가 45보다 크면 예외가 발생한다.', () => { + const inputBonusNumber = 77; + expect(() => { + new Bonus(winningNumbers, inputBonusNumber); + }).toThrow('[ERROR]'); + }); + test('보너스 번호가 로또 번호와 중복되면 예외가 발생한다.', () => { + const inputBonusNumber = 1; + expect(() => { + new Bonus(winningNumbers, inputBonusNumber); + }).toThrow('[ERROR]'); + }); +}); diff --git a/__tests__/CalculatorTest.js b/__tests__/CalculatorTest.js new file mode 100644 index 0000000000..6c63e8cc46 --- /dev/null +++ b/__tests__/CalculatorTest.js @@ -0,0 +1,16 @@ +import Calculator from '../src/Calculator.js'; + +describe('Calculator 클래스 테스트', () => { + const purchaseAmount = 8000; + const matchStats = new Map([ + [5000, 1], + [50000, 0], + [1500000, 0], + [30000000, 0], + [2000000000, 0], + ]); + const { rateOfReturn } = new Calculator(purchaseAmount, matchStats); + test('수익률이 정확한지 확인한다.(소수점 둘째자리에서 반올림)', () => { + expect(rateOfReturn).toEqual(62.5); + }); +}); diff --git a/__tests__/IssuerTest.js b/__tests__/IssuerTest.js new file mode 100644 index 0000000000..5ab5f2599b --- /dev/null +++ b/__tests__/IssuerTest.js @@ -0,0 +1,34 @@ +import Issuer from '../src/Issuer'; + +describe('Issuer 클래스 테스트', () => { + const { tickets } = new Issuer(3000); + test('구매한 로또 개수와 발행된 로또 개수가 같은지 확인한다.', () => { + expect(tickets).toHaveLength(3); + }); + test('발행된 각 티켓이 6개의 요소를 가지고 있는지 확인한다.', () => { + tickets.forEach((ticket) => { + expect(ticket).toHaveLength(6); + }); + }); + test('발행된 각 티켓의 요소가 전부 숫자인지 확인한다.', () => { + tickets.forEach((ticket) => { + ticket.forEach((value) => { + expect(typeof value).toEqual('number'); + }); + }); + }); + test('발행된 각 티켓의 요소가 전부 숫자 1 ~ 45 범위인지 확인한다.', () => { + tickets.forEach((ticket) => { + ticket.forEach((value) => { + expect(value).toBeGreaterThanOrEqual(1); + expect(value).toBeLessThanOrEqual(45); + }); + }); + }); + test('발행된 각 티켓이 오름차순으로 정렬되었는지 확인한다.', () => { + tickets.forEach((ticket) => { + const sortedTicket = [...ticket].sort((a, b) => a - b); + expect(ticket).toEqual(sortedTicket); + }); + }); +}); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 97bd457659..7e9c0b302d 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,18 +1,29 @@ -import Lotto from "../src/Lotto.js"; +import Lotto from '../src/Lotto.js'; -describe("로또 클래스 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { +describe('로또 클래스 테스트', () => { + test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); + }).toThrow('[ERROR]'); }); // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { + test('로또 번호에 중복된 숫자가 있으면 예외가 발생한다.', () => { expect(() => { new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); + }).toThrow('[ERROR]'); }); // 아래에 추가 테스트 작성 가능 + test('로또 번호에 1 미만인 숫자가 있으면 예외가 발생한다.', () => { + expect(() => { + new Lotto([0, 2, 3, 4, 5, 6]); + }).toThrow('[ERROR]'); + }); + + test('로또 번호에 45 초과인 숫자가 있으면 예외가 발생한다.', () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 77]); + }).toThrow('[ERROR]'); + }); }); diff --git a/__tests__/MatcherTest.js b/__tests__/MatcherTest.js new file mode 100644 index 0000000000..1992c24396 --- /dev/null +++ b/__tests__/MatcherTest.js @@ -0,0 +1,70 @@ +import Matcher from '../src/Matcher.js'; + +describe('Matcher 클래스 테스트', () => { + const tickets = [ + [12, 13, 19, 33, 36, 37], + [1, 4, 7, 11, 32, 34], + [1, 2, 9, 11, 15, 38], + ]; + const bonusNumber = 38; + test('3개 일치 개수가 정확한지 확인한다.', () => { + const winningNumbers = [12, 13, 19, 20, 30, 40]; + const matcher = new Matcher(tickets, winningNumbers, bonusNumber); + const expectedMatchStatus = new Map([ + [5000, 1], + [50000, 0], + [1500000, 0], + [30000000, 0], + [2000000000, 0], + ]); + expect(matcher.matchStatus).toEqual(expectedMatchStatus); + }); + test('4개 일치 개수가 정확한지 확인한다.', () => { + const winningNumbers = [1, 4, 7, 11, 30, 45]; + const matcher = new Matcher(tickets, winningNumbers, bonusNumber); + const expectedMatchStatus = new Map([ + [5000, 0], + [50000, 1], + [1500000, 0], + [30000000, 0], + [2000000000, 0], + ]); + expect(matcher.matchStatus).toEqual(expectedMatchStatus); + }); + test('5개 일치 개수가 정확한지 확인한다.', () => { + const winningNumbers = [1, 4, 7, 11, 21, 32]; + const matcher = new Matcher(tickets, winningNumbers, bonusNumber); + const expectedMatchStatus = new Map([ + [5000, 0], + [50000, 0], + [1500000, 1], + [30000000, 0], + [2000000000, 0], + ]); + expect(matcher.matchStatus).toEqual(expectedMatchStatus); + }); + test('5개 일치 개수와 보너스 볼 일치 개수가 정확한지 확인한다.', () => { + const winningNumbers = [1, 2, 9, 11, 15, 21]; + const matcher = new Matcher(tickets, winningNumbers, bonusNumber); + const expectedMatchStatus = new Map([ + [5000, 0], + [50000, 0], + [1500000, 0], + [30000000, 1], + [2000000000, 0], + ]); + expect(matcher.matchStatus).toEqual(expectedMatchStatus); + }); + test('6개 일치 개수가 정확한지 확인한다.', () => { + const winningNumbers = [1, 4, 7, 11, 32, 34]; + const matcher = new Matcher(tickets, winningNumbers, bonusNumber); + const expectedMatchStatus = new Map([ + [5000, 0], + [50000, 0], + [1500000, 0], + [30000000, 0], + [2000000000, 1], + ]); + expect(matcher.matchStatus).toEqual(expectedMatchStatus); + }); +}); diff --git a/__tests__/PurchaserTest.js b/__tests__/PurchaserTest.js new file mode 100644 index 0000000000..5ffa82caf1 --- /dev/null +++ b/__tests__/PurchaserTest.js @@ -0,0 +1,19 @@ +import Purchaser from '../src/Purchaser.js'; + +describe('구매자 클래스 테스트', () => { + test('구매 금액이 숫자가 아니면 예외가 발생한다.', () => { + expect(() => { + new Purchaser('안녕'); + }).toThrow('[ERROR]'); + }); + test('구매 금액이 1000원 보다 작으면 예외가 발생한다.', () => { + expect(() => { + new Purchaser('100'); + }).toThrow('[ERROR]'); + }); + test('구매 금액이 1000원 단위가 아니면 예외가 발생한다.', () => { + expect(() => { + new Purchaser('2400'); + }).toThrow('[ERROR]'); + }); +}); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..541ebff317 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,87 @@ +# 💫 javascript-lotto + +### 🎮 프로그램 동작 순서 + +1. 로또 구입금액을 입력받는다. +2. 로또 구입개수를 출력한다. +3. 로또를 구입개수만큼 발행한다. +4. 발행된 로또 번호를 출력한다. +5. 당첨 번호를 입력받는다. +6. 보너스 번호를 입력받는다. +7. 발행된 로또와 당첨 번호, 보너스 번호를 비교한다. +8. 당첨 통계를 출력한다. +9. 총 수익률을 구한다. +10. 총 수익률을 출력한다. + +### ✨ 기능구현목록 + +- [x] 로또 구입 금액을 입력받는 기능 + - [x] 사용자가 구입 금액을 잘 못 입력하는 경우 throw문을 사용해 예외를 발생시키고, '[ERROR]'로 시작하는 에러 메시지를 출력하고 해당 부분부터 입력을 다시 받는 기능 + - 숫자가 아닌 문자를 입력한 경우 + - 로또 구입 금액이 1000원 미만인 경우 + - 로또 구입 금액이 1000원 단위가 아닌 경우 +- [x] 로또 구입 개수를 구하고, 개수 만큼 로또를 발행하는 기능 +- [x] 로또 개수와, 로또 발행 번호를 출력하는 기능 +- [x] 당첨 번호를 입력 받는 기능 + - [x] 사용자가 당첨 번호를 잘 못 입력할 경우 throw문을 사용해 예외를 발생시키고, '[ERROR]'로 시작하는 에러 메시지를 출력하고 해당 부분부터 입력을 다시 받는 기능 + - ','로 구분하지 않은 경우 + - 숫자 6개를 입력하지 않은 경우 + - 1~45 범위에 해당 하는 숫자가 아닌 경우 + - 숫자가 중복되는 경우 +- [x] 보너스 번호를 입력 받는 기능 + - [x] 사용자가 보너스 번호를 잘 못 입력할 경우 throw문을 사용해 예외를 발생시키고, '[ERROR]'로 시작하는 에러 메시지를 출력하고 해당 부분부터 입력을 다시 받는 기능 + - 숫자가 아닌 문자를 입력한 경우 + - 당첨 번호에 포함된 번호를 입력한 경우 +- [x] 발행된 로또와 당첨 번호, 보너스 번호를 비교하여 당첨 동계를 구하는 기능 +- [x] 총 수익률을 구하는 기능 +- [x] 당첨 통계를 출력하는 기능 +- [x] 총 수익률을 출력하는 기능 + +### 🔨 리팩토링기준 + +- 요구사항 및 피드백에 걸맞게 구현한다. +- MVC 패턴에 따라 코드를 분리한다. +- 메소드가 하나의 기능만 하도록 구현한다. +- 불필요한 의존성이 없는 코드를 지향한다. +- 상수화에 적절한 문자와 숫자를 판단하여 상수화를 진행한다. + +### 🧪 테스트구현목록 + +- [x] Lotto 클래스 테스트 코드 구현 + + - 로또 번호의 개수가 6개가 넘어가면 예외가 발생한다. + - 로또 번호에 중복된 숫자가 있으면 예외가 발생한다. + - 로또 번호에 1 미만인 숫자가 있으면 예외가 발생한다. + - 로또 번호에 45 초과인 숫자가 있으면 예외가 발생한다. + +- [x] Purchaser 클래스 테스트 코드 구현 + + - 구매 금액이 숫자가 아니면 예외가 발생한다. + - 구매 금액이 1000원 보다 작으면 예외가 발생한다. + - 구매 금액이 1000원 단위가 아니면 예외가 발생한다. + +- [x] Bonus 클래스 테스트 코드 구현 + + - 보너스 번호가 숫자가 아니면 예외가 발생한다. + - 보너스 번호가 1보다 작으면 예외가 발생한다. + - 보너스 번호가 45보다 크면 예외가 발생한다. + - 보너스 번호가 로또 번호와 중복되면 예외가 발생한다. + +- [x] Issuer 클래스 테스트 코드 구현 + +- 구매한 로또 개수와 발행된 로또 개수가 같은지 확인한다. +- 발행된 각 티켓이 6개의 요소를 가지고 있는지 확인한다. +- 발행된 각 티켓의 요소가 전부 숫자인지 확인한다. +- 발행된 각 티켓의 요소가 전부 숫자 1 ~ 45 범위인지 확인한다. +- 발행된 각 티켓이 오름차순으로 정렬되었는지 확인한다. + +- [x] Matcher 클래스 테스트 코드 구현 + + - 3개 일치 개수가 정확한지 확인한다. + - 4개 일치 개수가 정확한지 확인한다. + - 5개 일치 개수가 정확한지 확인한다. + - 5개 일치 개수와 보너스 볼 일치 개수가 정확한지 확인한다. + - 6개 일치 개수가 정확한지 확인한다. + +- [x] Calculator 클래스 테스트 코드 구현 + - 수익률이 정확한지 확인한다.(소수점 둘째자리에서 반올림) diff --git a/src/App.js b/src/App.js index c38b30d5b2..1ee5529a46 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,13 @@ +import Controller from './Controller.js'; + class App { - async play() {} + constructor() { + this.controller = new Controller(); + } + + async play() { + await this.controller.progress(); + } } export default App; diff --git a/src/Bonus.js b/src/Bonus.js new file mode 100644 index 0000000000..8cd4ebbcc4 --- /dev/null +++ b/src/Bonus.js @@ -0,0 +1,30 @@ +import { BONUS_NUMBER } from './constants/Error.js'; +import { GAME } from './constants/Setting.js'; + +class Bonus { + #number; + + constructor(winningNumbers, inputBonusNumber) { + const bonusNumber = Number(inputBonusNumber); + this.#validate(winningNumbers, bonusNumber); + this.#number = bonusNumber; + } + + #validate(winningNumbers, bonusNumber) { + if (Number.isNaN(bonusNumber)) { + throw new Error(BONUS_NUMBER.notNumber); + } + if (bonusNumber < GAME.minNumber || bonusNumber > GAME.maxNumber) { + throw new Error(BONUS_NUMBER.invalidRange); + } + if (winningNumbers.includes(bonusNumber)) { + throw new Error(BONUS_NUMBER.notWinningNumber); + } + } + + get bonusNumber() { + return this.#number; + } +} + +export default Bonus; diff --git a/src/Calculator.js b/src/Calculator.js new file mode 100644 index 0000000000..14f8acd4aa --- /dev/null +++ b/src/Calculator.js @@ -0,0 +1,30 @@ +import { GAME } from './constants/Setting.js'; + +class Calculator { + #winningAmount; + + #rateOfReturn; + + constructor(purchaseAmount, matchStats) { + this.#winningAmount = 0; + this.#calculateWinningAmount(matchStats); + this.#calculateRateOfReturn(purchaseAmount); + } + + #calculateWinningAmount(matchStats) { + matchStats.forEach((count, amount) => { + this.#winningAmount += amount * count; + }); + } + + #calculateRateOfReturn(purchaseAmount) { + const rate = (this.#winningAmount / purchaseAmount) * GAME.percentage; + this.#rateOfReturn = Math.round(rate * GAME.roundDigit) / GAME.roundDigit; + } + + get rateOfReturn() { + return this.#rateOfReturn; + } +} + +export default Calculator; diff --git a/src/Controller.js b/src/Controller.js new file mode 100644 index 0000000000..b3dc06cfac --- /dev/null +++ b/src/Controller.js @@ -0,0 +1,113 @@ +import Bonus from './Bonus.js'; +import InputView from './InputView.js'; +import Lotto from './Lotto.js'; +import Issuer from './Issuer.js'; +import Matcher from './Matcher.js'; +import OutputView from './OutputView.js'; +import Calculator from './Calculator.js'; +import Purchaser from './Purchaser.js'; +import { WINNING_NUMBER } from './constants/Error.js'; +import { GAME, SYMBOL } from './constants/Setting.js'; + +class Controller { + #purchaseAmount; + + #winningNumbers; + + #bonusNumber; + + #tickets; + + async progress() { + await this.#handlerPurchasePhase(); + await this.#handlerDrawPhase(); + await this.#handlerResultPhase(); + } + + async #handlerPurchasePhase() { + await this.#handlerErrorAndProceed(this.#getPurchaseAmount); + this.#tickets = await this.#getLottoTicketList(); + this.#displayLottoTicket(this.#tickets); + } + + async #handlerDrawPhase() { + await this.#handlerErrorAndProceed(this.#getWinningNumbers); + await this.#handlerErrorAndProceed(this.#getBonusNumber); + } + + async #handlerResultPhase() { + const matchStatus = await this.#getMatchStatus( + this.#tickets, + this.#winningNumbers, + this.#bonusNumber, + ); + this.#displayWinningStat(matchStatus); + const rateOfReturn = await this.#getRateOfReturn(this.#purchaseAmount, matchStatus); + this.#displayRateOfReturn(rateOfReturn); + } + + async #handlerErrorAndProceed(method) { + try { + await method.call(this); + } catch (error) { + OutputView.printMessage(error.message); + await this.#handlerErrorAndProceed(method); + } + } + + async #getPurchaseAmount() { + const inputLottoPurchaseAmount = await InputView.readLottoPurchaseAmount(); + this.#purchaseAmount = new Purchaser(inputLottoPurchaseAmount).purchaseAmount; + } + + async #getLottoTicketList() { + return new Issuer(this.#purchaseAmount).tickets; + } + + #displayLottoTicket(tickets) { + OutputView.printLottoTicket(tickets); + } + + async #getWinningNumbers() { + const inputWinningNumbers = await InputView.readLottoWinningNumbers(); + this.#validateWinningNumbers(inputWinningNumbers); + const winningNumbers = this.#convertWinningNumbers(inputWinningNumbers); + this.#winningNumbers = new Lotto(winningNumbers).winningNumbers; + } + + #convertWinningNumbers(inputValue) { + return inputValue + .replace(GAME.removeSpaceRegex, SYMBOL.emptyString) + .split(SYMBOL.separator) + .map((number) => Number(number)); + } + + #validateWinningNumbers(inputWinningNumbers) { + if (!inputWinningNumbers.includes(SYMBOL.separator)) { + throw new Error(WINNING_NUMBER.withoutComma); + } + } + + async #getBonusNumber() { + const inputBonusNumber = await InputView.readLottoBonusNumber(); + this.#bonusNumber = new Bonus(this.#winningNumbers, inputBonusNumber).bonusNumber; + } + + async #getMatchStatus(tickets, winningNumbers, bonusNumber) { + return new Matcher(tickets, winningNumbers, bonusNumber).matchStatus; + } + + async #getRateOfReturn(purchaseAmount, matchStatus) { + return new Calculator(purchaseAmount, matchStatus).rateOfReturn; + } + + #displayWinningStat(matchStatus) { + OutputView.printWinningStat(matchStatus); + } + + #displayRateOfReturn(rateOfReturn) { + OutputView.printRateOfReturn(rateOfReturn); + } +} + +export default Controller; diff --git a/src/InputView.js b/src/InputView.js new file mode 100644 index 0000000000..1354d6279b --- /dev/null +++ b/src/InputView.js @@ -0,0 +1,21 @@ +import { Console } from '@woowacourse/mission-utils'; +import { INFO } from './constants/Setting.js'; + +const InputView = { + async readLottoPurchaseAmount() { + const inputValue = await Console.readLineAsync(INFO.inputPurchaseAmount); + return inputValue; + }, + + async readLottoWinningNumbers() { + const inputValue = await Console.readLineAsync(INFO.inputWinningNumbers); + return inputValue; + }, + + async readLottoBonusNumber() { + const inputValue = await Console.readLineAsync(INFO.inputBonusNumber); + return inputValue; + }, +}; + +export default InputView; diff --git a/src/Issuer.js b/src/Issuer.js new file mode 100644 index 0000000000..161c09fa5f --- /dev/null +++ b/src/Issuer.js @@ -0,0 +1,28 @@ +import { GAME } from './constants/Setting.js'; +import RandomNumberGenerator from './utils/RandomNumberGenerator.js'; + +class Issuer { + #lottoCount; + + #tickets; + + constructor(purchaseAmount) { + this.#lottoCount = purchaseAmount / GAME.unit; + this.#tickets = this.#generate(); + } + + #generate() { + const issuedLottoList = []; + while (issuedLottoList.length < this.#lottoCount) { + const sortedRandomNumbers = RandomNumberGenerator().sort((a, b) => a - b); + issuedLottoList.push(sortedRandomNumbers); + } + return issuedLottoList; + } + + get tickets() { + return this.#tickets; + } +} + +export default Issuer; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e9..3dedb7cf35 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,3 +1,6 @@ +import { WINNING_NUMBER } from './constants/Error.js'; +import { GAME } from './constants/Setting.js'; + class Lotto { #numbers; @@ -7,12 +10,20 @@ class Lotto { } #validate(numbers) { - if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + const uniqueNumbers = new Set(numbers); + if (uniqueNumbers.size !== GAME.lottoNumber) { + throw new Error(WINNING_NUMBER.notUnique); } + uniqueNumbers.forEach((number) => { + if (!(number >= GAME.minNumber && number <= GAME.maxNumber)) { + throw new Error(WINNING_NUMBER.invalidRange); + } + }); } - // TODO: 추가 기능 구현 + get winningNumbers() { + return this.#numbers; + } } export default Lotto; diff --git a/src/Matcher.js b/src/Matcher.js new file mode 100644 index 0000000000..9a5d1d9642 --- /dev/null +++ b/src/Matcher.js @@ -0,0 +1,36 @@ +import { PRIZE } from './constants/Setting.js'; + +class Matcher { + #prizeTable = new Map(PRIZE); + + #matchStatusCount = new Map([ + [5000, 0], + [50000, 0], + [1500000, 0], + [30000000, 0], + [2000000000, 0], + ]); + + constructor(tickets, winningNumbers, bonusNumber) { + this.#match(tickets, winningNumbers, bonusNumber); + } + + #match(tickets, winningNumbers, bonusNumber) { + tickets.map((ticket) => { + const matchedNumberCount = ticket.filter((number) => winningNumbers.includes(number)).length; + const isBonusMatch = ticket.includes(bonusNumber); + const keyOfPrizeTable = + isBonusMatch && matchedNumberCount === 5 ? '5+bonus' : matchedNumberCount; + if (this.#prizeTable.has(keyOfPrizeTable)) { + const prize = this.#prizeTable.get(keyOfPrizeTable); + this.#matchStatusCount.set(prize, this.#matchStatusCount.get(prize) + 1); + } + }); + } + + get matchStatus() { + return this.#matchStatusCount; + } +} + +export default Matcher; diff --git a/src/OutputView.js b/src/OutputView.js new file mode 100644 index 0000000000..953f1ccbe0 --- /dev/null +++ b/src/OutputView.js @@ -0,0 +1,28 @@ +import { Console } from '@woowacourse/mission-utils'; + +const OutputView = { + printMessage(message) { + Console.print(message); + }, + + printLottoTicket(tickets) { + Console.print(`\n${tickets.length}개를 구매했습니다.`); + tickets.map((ticket) => Console.print(`[${ticket.join(', ')}]`)); + }, + + printWinningStat(matchStatus) { + Console.print('\n당첨 통계'); + Console.print('---'); + Console.print(`3개 일치 (5,000원) - ${matchStatus.get(5000)}개`); + Console.print(`4개 일치 (50,000원) - ${matchStatus.get(50000)}개`); + Console.print(`5개 일치 (1,500,000원) - ${matchStatus.get(1500000)}개`); + Console.print(`5개 일치, 보너스 볼 일치 (30,000,000원) - ${matchStatus.get(30000000)}개`); + Console.print(`6개 일치 (2,000,000,000원) - ${matchStatus.get(2000000000)}개`); + }, + + printRateOfReturn(rateOfReturn) { + Console.print(`총 수익률은 ${rateOfReturn}%입니다.`); + }, +}; + +export default OutputView; diff --git a/src/Purchaser.js b/src/Purchaser.js new file mode 100644 index 0000000000..3cbe15df2d --- /dev/null +++ b/src/Purchaser.js @@ -0,0 +1,30 @@ +import { PURCHASE_AMOUNT } from './constants/Error.js'; +import { GAME } from './constants/Setting.js'; + +class Purchaser { + #number; + + constructor(inputValue) { + const purchaseAmount = Number(inputValue); + this.#validate(purchaseAmount); + this.#number = purchaseAmount; + } + + #validate(number) { + if (Number.isNaN(number)) { + throw new Error(PURCHASE_AMOUNT.notNumber); + } + if (number < GAME.unit) { + throw new Error(PURCHASE_AMOUNT.notMin); + } + if (number % GAME.unit !== 0) { + throw new Error(PURCHASE_AMOUNT.invalidUnit); + } + } + + get purchaseAmount() { + return this.#number; + } +} + +export default Purchaser; diff --git a/src/constants/Error.js b/src/constants/Error.js new file mode 100644 index 0000000000..dc94a17935 --- /dev/null +++ b/src/constants/Error.js @@ -0,0 +1,19 @@ +const PURCHASE_AMOUNT = Object.freeze({ + notNumber: '[ERROR] 구매 금액은 숫자여야 합니다.', + notMin: '[ERROR] 최소 구매 금액은 1000원입니다.', + invalidUnit: '[ERROR] 구매 금액은 1000원 단위여야 합니다.', +}); + +const BONUS_NUMBER = Object.freeze({ + notNumber: '[ERROR] 보너스 번호는 숫자여야 합니다.', + invalidRange: '[ERROR] 보너스 번호는 1 이상 45 이하여야 합니다.', + notWinningNumber: '[ERROR] 보너스 번호는 당첨 번호 외의 숫자여야 합니다.', +}); + +const WINNING_NUMBER = Object.freeze({ + withoutComma: '[ERROR] 로또 번호는 콤마로 구분하여 입력하여야 합니다.', + notUnique: '[ERROR] 로또 번호는 중복되지 않은 6개여야 합니다.', + invalidRange: '[ERROR] 로또 번호는 1 이상 45 이하여야 합니다.', +}); + +export { PURCHASE_AMOUNT, BONUS_NUMBER, WINNING_NUMBER }; diff --git a/src/constants/Setting.js b/src/constants/Setting.js new file mode 100644 index 0000000000..1c4837d5c0 --- /dev/null +++ b/src/constants/Setting.js @@ -0,0 +1,34 @@ +const INFO = Object.freeze({ + inputPurchaseAmount: '구입금액을 입력해 주세요.\n', + inputWinningNumbers: '\n당첨 번호를 입력해 주세요.\n', + inputBonusNumber: '\n보너스 번호를 입력해 주세요.\n', +}); + +const SYMBOL = Object.freeze({ + emptyString: '', + dash: '-', + newLine: '\n', + separator: ',', + colon: ':', + space: ' ', +}); + +const GAME = Object.freeze({ + lottoNumber: 6, + minNumber: 1, + maxNumber: 45, + removeSpaceRegex: /\s/g, + unit: 1000, + roundDigit: 10, + percentage: 100, +}); + +const PRIZE = Object.freeze([ + [3, 5000], + [4, 50000], + [5, 1500000], + ['5+bonus', 30000000], + [6, 2000000000], +]); + +export { INFO, SYMBOL, GAME, PRIZE }; diff --git a/src/utils/RandomNumberGenerator.js b/src/utils/RandomNumberGenerator.js new file mode 100644 index 0000000000..2e92407880 --- /dev/null +++ b/src/utils/RandomNumberGenerator.js @@ -0,0 +1,5 @@ +import { Random } from '@woowacourse/mission-utils'; + +const RandomNumberGenerator = () => Random.pickUniqueNumbersInRange(1, 45, 6); + +export default RandomNumberGenerator;