diff --git a/README.md b/README.md index 15bb106b5..384410e86 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ # javascript-lotto-precourse + +  + +  + +# ✅ 구현할 기능 목록 + +간단한 로또 발매기를 구현한다. + +  + +### 🟢 [입력] 로또 구입 금액을 입력받는다. + +#### ㄴ validation + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. +- 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나우어 떨어지지 않는 경우 예외 처리한다. + +  + +  + +### 🟢 [출력] 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다. + +  + +  + +### 🟢 [입력] 당첨번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다. + +#### ㄴ validation + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. + +  + +  + +### 🟢 [입력] 보너스 번호를 입력 받는다. + +#### ㄴ validation + +- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다. + +  + +  + +### 🟢 [출력] 당첨 내역을 출력한다. + +<출력 예시> +당첨 통계 + +\--- + +3개 일치 (5,000원) - 1개 + +  + +  + +### 🟢 [출력] 총 수익률을 출력한다. + +수익률은 소수점 둘쨰 자리에서 반올림 한다. (ex. 100.0%, 51.5%, 1,000,000.0%) + +- diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c9c..b8a446383 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,5 +1,5 @@ -import App from "../src/App.js"; -import { MissionUtils } from "@woowacourse/mission-utils"; +import App from '../src/App.js'; +import { MissionUtils } from '@woowacourse/mission-utils'; const mockQuestions = (inputs) => { MissionUtils.Console.readLineAsync = jest.fn(); @@ -19,7 +19,7 @@ const mockRandoms = (numbers) => { }; const getLogSpy = () => { - const logSpy = jest.spyOn(MissionUtils.Console, "print"); + const logSpy = jest.spyOn(MissionUtils.Console, 'print'); logSpy.mockClear(); return logSpy; }; @@ -29,7 +29,7 @@ const runException = async (input) => { const logSpy = getLogSpy(); const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5, 6]; - const INPUT_NUMBERS_TO_END = ["1000", "1,2,3,4,5,6", "7"]; + const INPUT_NUMBERS_TO_END = ['1000', '1,2,3,4,5,6', '7']; mockRandoms([RANDOM_NUMBERS_TO_END]); mockQuestions([input, ...INPUT_NUMBERS_TO_END]); @@ -39,15 +39,15 @@ const runException = async (input) => { await app.run(); // then - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[ERROR]')); }; -describe("로또 테스트", () => { +describe('로또 테스트', () => { beforeEach(() => { jest.restoreAllMocks(); }); - test("기능 테스트", async () => { + test('기능 테스트', async () => { // given const logSpy = getLogSpy(); @@ -61,7 +61,7 @@ describe("로또 테스트", () => { [2, 13, 22, 32, 38, 45], [1, 3, 5, 14, 22, 45], ]); - mockQuestions(["8000", "1,2,3,4,5,6", "7"]); + mockQuestions(['8000', '1,2,3,4,5,6', '7']); // when const app = new App(); @@ -69,21 +69,21 @@ describe("로또 테스트", () => { // then const logs = [ - "8개를 구매했습니다.", - "[8, 21, 23, 41, 42, 43]", - "[3, 5, 11, 16, 32, 38]", - "[7, 11, 16, 35, 36, 44]", - "[1, 8, 11, 31, 41, 42]", - "[13, 14, 16, 38, 42, 45]", - "[7, 11, 30, 40, 42, 43]", - "[2, 13, 22, 32, 38, 45]", - "[1, 3, 5, 14, 22, 45]", - "3개 일치 (5,000원) - 1개", - "4개 일치 (50,000원) - 0개", - "5개 일치 (1,500,000원) - 0개", - "5개 일치, 보너스 볼 일치 (30,000,000원) - 0개", - "6개 일치 (2,000,000,000원) - 0개", - "총 수익률은 62.5%입니다.", + '8개를 구매했습니다.', + '[8, 21, 23, 41, 42, 43]', + '[3, 5, 11, 16, 32, 38]', + '[7, 11, 16, 35, 36, 44]', + '[1, 8, 11, 31, 41, 42]', + '[13, 14, 16, 38, 42, 45]', + '[7, 11, 30, 40, 42, 43]', + '[2, 13, 22, 32, 38, 45]', + '[1, 3, 5, 14, 22, 45]', + '3개 일치 (5,000원) - 1개', + '4개 일치 (50,000원) - 0개', + '5개 일치 (1,500,000원) - 0개', + '5개 일치, 보너스 볼 일치 (30,000,000원) - 0개', + '6개 일치 (2,000,000,000원) - 0개', + '총 수익률은 62.5%입니다.', ]; logs.forEach((log) => { @@ -91,7 +91,7 @@ describe("로또 테스트", () => { }); }); - test("예외 테스트", async () => { - await runException("1000j"); + test('예외 테스트', async () => { + await runException('1000j'); }); }); diff --git a/__tests__/InputBonusModule.test.js b/__tests__/InputBonusModule.test.js new file mode 100644 index 000000000..3af424042 --- /dev/null +++ b/__tests__/InputBonusModule.test.js @@ -0,0 +1,41 @@ +import InputBonusModule from '../src/modules/InputBonusModule'; + +describe('로또 구입 금액 검증', () => { + // given + const bonusNumber = new InputBonusModule(); + + // 실패하는 코드 + test('당첨번호에 있는 값이 보너스번호로 입력되면 ERROR가 발생한다.', () => { + // when + const testBonusNumber = '1'; + const winningNumber = [1, 2, 3, 4, 5, 6]; + + // then + expect(() => { + bonusNumber.validateBonusNumber(testBonusNumber, winningNumber); + }).toThrow('[ERROR]'); + }); + + // 성공하는 코드 + test('당첨번호에 있는 값이 보너스번호로 입력될 수 없다.', () => { + // when + const testBonusNumber = '7'; + const winningNumber = [1, 2, 3, 4, 5, 6]; + + // then + expect(() => { + bonusNumber.validateBonusNumber(testBonusNumber, winningNumber); + }).toBeTruthy(); + }); + + // (추가적인 테스트 코드) + test('입력값이 빈값이면 ERROR가 발생한다.', () => { + // when + const testBonusNumber = ''; + + // then + expect(() => { + bonusNumber.validateBonusNumber(testBonusNumber); + }).toThrow('[ERROR]'); + }); +}); diff --git a/__tests__/InputPurchaseModule.test.js b/__tests__/InputPurchaseModule.test.js new file mode 100644 index 000000000..2a14199a3 --- /dev/null +++ b/__tests__/InputPurchaseModule.test.js @@ -0,0 +1,39 @@ +import InputPurchaseModule from '../src/modules/InputPurchaseModule'; + +describe('로또 구입 금액 검증', () => { + // given + const purchaseAmount = new InputPurchaseModule(); + + // 실패하는 코드 + test('1,000원으로 나누어 떨어지지 않으면 에러', () => { + // when + const testInput = '1500'; + + // then + expect(() => { + purchaseAmount.validatatePrice(testInput); + }).toThrow('[ERROR]'); + }); + + // 성공하는 코드 + test('1,000원 단위로 입력 가능해야 한다', () => { + // when + const testInput = '10000'; + + // then + expect(() => { + purchaseAmount.validatatePrice(testInput); + }).toBeTruthy(); + }); + + // (추가적인 테스트 코드) + test('입력값이 빈값이면 ERROR가 발생한다.', () => { + // when + const testInput = ''; + + // then + expect(() => { + purchaseAmount.validatatePrice(testInput); + }).toThrow('[ERROR]'); + }); +}); diff --git a/__tests__/InputWinningModule.test.js b/__tests__/InputWinningModule.test.js new file mode 100644 index 000000000..289f28a5d --- /dev/null +++ b/__tests__/InputWinningModule.test.js @@ -0,0 +1,61 @@ +import InputWinningModule from '../src/modules/InputWinningModule'; + +describe('로또 구입 금액 검증', () => { + // given + const winningNumber = new InputWinningModule(); + + // 실패하는 코드 + test('당첨번호는 6자리를 입력해야한다.', () => { + // when + const testInput = '1,2,3,4,5,6,7'; + + // then + expect(() => { + winningNumber.validateWinningNumber(testInput); + }).toThrow('[ERROR]'); + }); + + // 성공하는 코드 + test('당첨번호는 6자리를 입력해야한다.', () => { + // when + const testInput = '1,2,3,4,5,6'; + + // then + expect(() => { + winningNumber.validateWinningNumber(testInput); + }).toBeTruthy(); + }); + + // 실패하는 코드 + test('6자리 중 중복값은 없어야 한다.', () => { + // when + const testInput = '1,2,3,4,5,5'; + + // then + expect(() => { + winningNumber.validateWinningNumber(testInput); + }).toThrow('[ERROR]'); + }); + + // 성공하는 코드 + test('6자리 중 중복값은 없어야 한다.', () => { + // when + const testInput = '1,2,3,4,5,6'; + + // then + expect(() => { + winningNumber.validateWinningNumber(testInput); + }).toBeTruthy(); + }); + + // (추가적인 테스트 코드) + test('입력값이 빈값이면 ERROR가 발생한다.', () => { + // when + const testInput = ''; + + // then + expect(() => { + winningNumber.validateWinningNumber(testInput); + }).toThrow('[ERROR]'); + }); +}); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..19d576106 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,17 +1,17 @@ -import Lotto from "../src/Lotto"; +import Lotto from '../src/Lotto'; -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]'); }); // TODO: 추가 기능 구현에 따른 테스트 코드 작성 diff --git a/__tests__/PrintRandomNumber.test.js b/__tests__/PrintRandomNumber.test.js new file mode 100644 index 000000000..fc63fcb8d --- /dev/null +++ b/__tests__/PrintRandomNumber.test.js @@ -0,0 +1,54 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import PrintRandomNumber from '../src/modules/PrintRandomNumber'; +import OutputView from '../src/view/OutputView'; + +jest.mock('../src/view/OutputView'); +jest.mock('@woowacourse/mission-utils', () => ({ + MissionUtils: { + Random: { + pickUniqueNumbersInRange: jest.fn(), + }, + }, +})); + +describe('PrintRandomNumber', () => { + // given + beforeEach(() => { + OutputView.printPurchaseNumber.mockClear(); + OutputView.printLottoNumber.mockClear(); + OutputView.printSpace.mockClear(); + MissionUtils.Random.pickUniqueNumbersInRange.mockClear(); + }); + + const printRandomNumber = new PrintRandomNumber(); + const mockPurchaseQuantity = 3; + const mockLottoNumbers = [ + [1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18], + ]; + + test('로또 번호 생성 및 출력 검증', () => { + // when + MissionUtils.Random.pickUniqueNumbersInRange + .mockReturnValueOnce(mockLottoNumbers[0]) + .mockReturnValueOnce(mockLottoNumbers[1]) + .mockReturnValueOnce(mockLottoNumbers[2]); + + // then + expect(printRandomNumber.printRandomNumber(mockPurchaseQuantity)).toEqual([ + [1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18], + ]); + + expect(OutputView.printPurchaseNumber).toHaveBeenCalledWith(mockPurchaseQuantity); + expect(OutputView.printLottoNumber).toHaveBeenCalledTimes(mockPurchaseQuantity); + expect(OutputView.printSpace).toHaveBeenCalledTimes(1); + + expect(MissionUtils.Random.pickUniqueNumbersInRange).toHaveBeenCalledTimes( + mockPurchaseQuantity + ); + expect(MissionUtils.Random.pickUniqueNumbersInRange).toHaveBeenCalledWith(1, 45, 6); + }); +}); diff --git a/__tests__/PrintWinningDetails.test.js b/__tests__/PrintWinningDetails.test.js new file mode 100644 index 000000000..42f415bbe --- /dev/null +++ b/__tests__/PrintWinningDetails.test.js @@ -0,0 +1,48 @@ +import OutputView from '../src/view/OutputView'; +import PrintWinningDetails from '../src/modules/printWinningDetails'; + +jest.mock('../src/view/OutputView'); + +describe('당첨 내역 출력하기', () => { + // given + beforeEach(() => { + OutputView.printWinningMessage.mockClear(); + OutputView.printWinningStatistics.mockClear(); + OutputView.printTotalReturn.mockClear(); + }); + + const printWinningDsetails = new PrintWinningDetails(); + const mockLottoNumbers = [ + [1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18], + ]; + const mockWinningNumber = [13, 14, 15, 16, 17, 18]; + const mockBonusNumber = 12; + + test('당첨 내역 출력이 되었는가?', () => { + // when + printWinningDsetails.winningDetails(mockLottoNumbers, mockWinningNumber, mockBonusNumber); + + // then + expect(OutputView.printWinningMessage).toHaveBeenCalledTimes(1); + expect(OutputView.printWinningStatistics).toHaveBeenCalledTimes(1); + expect(OutputView.printTotalReturn).toHaveBeenCalledTimes(1); + }); + + test('당첨번호 매칭이 잘 되었는가?', () => { + // when + const result = printWinningDsetails.checkMatchedNumber( + mockLottoNumbers, + mockWinningNumber, + mockBonusNumber + ); + + // then + expect(result).toEqual([ + { matchNum: 0, matchBonus: 0 }, + { matchNum: 0, matchBonus: 1 }, + { matchNum: 6, matchBonus: 0 }, + ]); + }); +}); diff --git a/src/App.js b/src/App.js index 091aa0a5d..8b0404c4d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,9 @@ +import LottoController from './controller/LottoController.js'; + class App { - async run() {} + async run() { + await new LottoController().runLotto(); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..00bee98b9 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,18 +1,31 @@ +import { ERROR_MESSAGES } from './constants/errorMessages.js'; +import createThrowError from './utils/createError.js'; + class Lotto { #numbers; constructor(numbers) { this.#validate(numbers); + this.isDuplicatedInLottoNumbers(numbers); this.#numbers = numbers; } #validate(numbers) { if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + throw new Error('[ERROR] 로또 번호는 6개여야 합니다.'); } } // TODO: 추가 기능 구현 + // test코드를 확인해보니, 중복 값을 검증하는 코드가 필요합니다. + isDuplicatedInLottoNumbers(number) { + const removeDuplicated = new Set(number); // 🔥 출력이 배열로 나오지 않습니다. + // 🔥 Set은 배열과 다른 특별한 객체 타입입니다. + // 배열로 변환하려면 간단히 스프레드 연산자(...)나 Array.from()을 사용하면 됩니다. + const uniqueArray = [...removeDuplicated]; // 배열로 변환 + if (number.length !== uniqueArray.length) + createThrowError(ERROR_MESSAGES.isDuplicatedInLottoNumbers); + } } export default Lotto; diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js new file mode 100644 index 000000000..8f02a42bc --- /dev/null +++ b/src/constants/errorMessages.js @@ -0,0 +1,21 @@ +const ERROR_PREFIX = '[ERROR]'; + +const createMsg = (msg) => `${ERROR_PREFIX} ${msg} ${'다시 입력해주세요.'}\n`; + +const ERROR_MESSAGES = { + emptyValue: createMsg('값이 존재하지 않습니다.'), + regexpTest: createMsg('형식이 잘 못되었습니다.'), + startComma: createMsg('입력이 콤마(,)부터 시작할 수 없습니다.'), + endComma: createMsg('입력 끝에 콤마(,)로 끝날 수 없습니다.'), + enteredMoreFiveTimes: '5회 이상 잘못 입력하여 종료되없습니다. 다시 실행해주세요.', + limitDigits: createMsg('최소 4자리 숫자부터 6자리 숫자까지 입력 가능합니다.'), + negativeNumber: createMsg('음수가 입력될 수 없습니다.'), + thousandUnit: createMsg( + '로또 금액인 1000원 단위로만 입력 가능하며, 최대 10만원까지 입력 가능합니다.' + ), + winningNumberSixDigit: createMsg('당첨번호는 6자리를 입력해야합니다.'), + isDuplicatedInLottoNumbers: createMsg('입력하신 당첨번호에 중복값이 존재합니다.'), + isDuplicatedInWinningNumber: createMsg('당첨번호와 중복되는 번호를 입력하셨습니다.'), +}; + +export { ERROR_PREFIX, ERROR_MESSAGES }; diff --git a/src/constants/inputMessages.js b/src/constants/inputMessages.js new file mode 100644 index 000000000..b53bd6112 --- /dev/null +++ b/src/constants/inputMessages.js @@ -0,0 +1,7 @@ +const INPUT_MESSAGES = { + whatPurchaseAmount: '구매금액을 입력해주세요. \n => ', + winningNumber: '당첨번호를 입력해주세요. \n => ', + bonusNumber: '보너스번호를 입력해주세요. \n => ', +}; + +export default INPUT_MESSAGES; diff --git a/src/constants/outputMessages.js b/src/constants/outputMessages.js new file mode 100644 index 000000000..ed9e74e1f --- /dev/null +++ b/src/constants/outputMessages.js @@ -0,0 +1,14 @@ +const OUTPUT_MESSAGES = { + purchaseNumber: '개를 구매했습니다.', + winningStatistics: '당첨 통계\n--- ', + matchedThree: '3개 일치 (5,000원) - ', + matchedFour: '4개 일치 (50,000원) - ', + matchedFive: '5개 일치 (1,500,000원) - ', + matchedFiveBonus: '5개 일치, 보너스 볼 일치 (30,000,000원) - ', + matchedSix: '6개 일치 (2,000,000,000원) - ', + printQuantity: '개', + totalReturn: '총 수익률은 ', + totalReturnPercentage: '% 입니다.', +}; + +export default OUTPUT_MESSAGES; diff --git a/src/controller/LottoController.js b/src/controller/LottoController.js new file mode 100644 index 000000000..adf0063d8 --- /dev/null +++ b/src/controller/LottoController.js @@ -0,0 +1,26 @@ +import InputPurchaseModule from '../modules/InputPurchaseModule.js'; +import InputWinningModule from '../modules/InputWinningModule.js'; +import InputBonusModule from '../modules/InputBonusModule.js'; +import PrintWinningDetails from '../modules/printWinningDetails.js'; +import PrintRandomNumber from '../modules/PrintRandomNumber.js'; + +class LottoController { + constructor() { + this.inputPurchaseModule = new InputPurchaseModule(); + this.inputWinningModule = new InputWinningModule(); + this.printRandomNumber = new PrintRandomNumber(); + } + + async runLotto() { + const purchasPrice = await this.inputPurchaseModule.inputPurchaseAmount(); + const purchasQuantity = purchasPrice / 1000; + const lottoNumber = this.printRandomNumber.printRandomNumber(purchasQuantity); + const winningNumber = await this.inputWinningModule.inputWinningNumber(); + const inputBonusModule = new InputBonusModule(winningNumber); + const bonusNumber = await inputBonusModule.inputBonusNumber(); + const printWinningDetails = new PrintWinningDetails(purchasPrice); + printWinningDetails.winningDetails(lottoNumber, winningNumber, bonusNumber); + } +} + +export default LottoController; diff --git a/src/index.js b/src/index.js index 02a1d389e..9daefc93f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import App from "./App.js"; +import App from './App.js'; const app = new App(); await app.run(); diff --git a/src/modules/InputBonusModule.js b/src/modules/InputBonusModule.js new file mode 100644 index 000000000..20b5d5370 --- /dev/null +++ b/src/modules/InputBonusModule.js @@ -0,0 +1,52 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import validation from '../validation/validation.js'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; + +class InputBonusModule { + constructor(winningNumber) { + this.validation = new validation(); + this.winningNumber = winningNumber; + } + + async inputBonusNumber() { + const validatedBonusNumber = await this.repeatInput(); + const changeTypeNumber = Number(validatedBonusNumber); + OutputView.printSpace(); + return changeTypeNumber; + } + + async inputAndValidation() { + const input = await InputView.readBonusNumber(); + const winningNumber = this.winningNumber; + this.validateBonusNumber(input, winningNumber); + return input; + } + + validateBonusNumber(value, winningNumber) { + this.validation.empty(value); + const regExpPattern = /\d{1,2}/; + this.validation.regularExpression(value, regExpPattern); + this.validation.isDuplicatedInWinningNumber(value, winningNumber); + } + + async errorCatch() { + try { + const validatedInput = await this.inputAndValidation(); + return validatedInput; + } catch (error) { + OutputView.printError(error); + return false; + } + } + + async repeatInput() { + for (let i = 0; i < 10; i++) { + const vlaidatedInput = await this.errorCatch(); + if (vlaidatedInput) return vlaidatedInput; + if (i === 5) throw new Error(ERROR_MESSAGES.enteredMoreFiveTimes); + } + } +} + +export default InputBonusModule; diff --git a/src/modules/InputPurchaseModule.js b/src/modules/InputPurchaseModule.js new file mode 100644 index 000000000..3d13832c8 --- /dev/null +++ b/src/modules/InputPurchaseModule.js @@ -0,0 +1,52 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import validation from '../validation/validation.js'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; + +class InputPurchaseModule { + constructor() { + this.validation = new validation(); + } + + async inputPurchaseAmount() { + const validatedPurchaseAmount = await this.repeatInput(); + const changgeTypeNumber = Number(validatedPurchaseAmount); + OutputView.printSpace(); + return changgeTypeNumber; + } + + async inputAndValidation() { + const input = await InputView.readPurchaseAmount(); + this.validatatePrice(input); + return input; + } + + validatatePrice(value) { + this.validation.empty(value); + const regExpPattern = /\d/; + this.validation.regularExpression(value, regExpPattern); + this.validation.nagativeNumber(value); + this.validation.limitDigits(value); + this.validation.thousandUnit(value); + } + + async errorCatch() { + try { + const validatedInput = await this.inputAndValidation(); + return validatedInput; + } catch (error) { + OutputView.printError(error); + return false; + } + } + + async repeatInput() { + for (let i = 0; i < 10; i++) { + const vlaidatedInput = await this.errorCatch(); + if (vlaidatedInput) return vlaidatedInput; + if (i === 5) throw new Error(ERROR_MESSAGES.enteredMoreFiveTimes); + } + } +} + +export default InputPurchaseModule; diff --git a/src/modules/InputWinningModule.js b/src/modules/InputWinningModule.js new file mode 100644 index 000000000..6823744f7 --- /dev/null +++ b/src/modules/InputWinningModule.js @@ -0,0 +1,52 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import validation from '../validation/validation.js'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; + +class InputWinningModule { + constructor() { + this.validation = new validation(); + } + async inputWinningNumber() { + const validatedLottoNumber = await this.repeatInput(); + const changgeTypeArray = validatedLottoNumber.split(',').map((string) => Number(string)); + OutputView.printSpace(); + return changgeTypeArray; + } + + async inputAndValidation() { + const input = await InputView.readWinningNumber(); + this.validateWinningNumber(input); + return input; + } + + validateWinningNumber(value) { + this.validation.empty(value); + this.validation.startedComma(value); + this.validation.endedComma(value); + const regExpPattern = /^\d(,\s?\d)*/; + this.validation.regularExpression(value, regExpPattern); + this.validation.winningNumberSixDigit(value); + this.validation.isDuplicatedInLottoNumbers(value); + } + + async errorCatch() { + try { + const validatedInput = await this.inputAndValidation(); + return validatedInput; + } catch (error) { + OutputView.printError(error); + return false; + } + } + + async repeatInput() { + for (let i = 0; i < 10; i++) { + const vlaidatedInput = await this.errorCatch(); + if (vlaidatedInput) return vlaidatedInput; + if (i === 5) throw new Error(ERROR_MESSAGES.enteredMoreFiveTimes); + } + } +} + +export default InputWinningModule; diff --git a/src/modules/PrintRandomNumber.js b/src/modules/PrintRandomNumber.js new file mode 100644 index 000000000..9b560ddf8 --- /dev/null +++ b/src/modules/PrintRandomNumber.js @@ -0,0 +1,19 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import OutputView from '../view/OutputView.js'; + +class PrintRandomNumber { + printRandomNumber(purchasQuantity) { + OutputView.printPurchaseNumber(purchasQuantity); + let lottoNumberArrayss = []; + for (let i = 0; i < purchasQuantity; i++) { + const lottoNumber = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6); + const ascendingOrder = lottoNumber.sort((a, b) => a - b); + OutputView.printLottoNumber(ascendingOrder); + lottoNumberArrayss = [...lottoNumberArrayss, ascendingOrder]; + } + OutputView.printSpace(); + return lottoNumberArrayss; + } +} + +export default PrintRandomNumber; diff --git a/src/modules/PrintWinningDetails.js b/src/modules/PrintWinningDetails.js new file mode 100644 index 000000000..c234e0aea --- /dev/null +++ b/src/modules/PrintWinningDetails.js @@ -0,0 +1,72 @@ +import OutputView from '../view/OutputView.js'; + +class PrintWinningDetails { + constructor(purchasePrice) { + this.purchasePrice = purchasePrice; + } + + winningDetails(lottoNumber, winningNumber, bonusNumber) { + const commonNumbers = this.checkMatchedNumber(lottoNumber, winningNumber, bonusNumber); + const winningStatistics = this.getWinningStatistics(commonNumbers); + this.printWinningStatistics(winningStatistics); + const winningAmount = this.getWinningAmount(winningStatistics); + const purchasePrice = this.purchasePrice; + this.printProfitRate(purchasePrice, winningAmount); + } + + // service logic + checkMatchedNumber(lottoNumber, winningNumber, bonusNumber) { + let commonNumbers = []; + lottoNumber.map((lottoArray) => { + const matchedNumbers = lottoArray.filter((array) => winningNumber.includes(array)).length; + const matchedBonus = lottoArray.filter((array) => [bonusNumber].includes(array)).length; + const commonResult = { matchNum: matchedNumbers, matchBonus: matchedBonus }; + return (commonNumbers = [...commonNumbers, commonResult]); + }); + return commonNumbers; + } + + getWinningStatistics(commonNumbers) { + const winningStatistics = [ + { matchThree: commonNumbers.filter((o) => o.matchNum === 3).length }, + { matchFour: commonNumbers.filter((o) => o.matchNum === 4).length }, + { matchFive: commonNumbers.filter((o) => o.matchNum === 5 && o.matchBonus === 0).length }, + { + matchFiveAndBonus: commonNumbers.filter((o) => o.matchNum === 5 && o.matchBonus === 1) + .length, + }, + { matchSix: commonNumbers.filter((o) => o.matchNum === 6).length }, + ]; + return winningStatistics; + } + + printWinningStatistics(winningStatistics) { + OutputView.printWinningMessage(); + OutputView.printWinningStatistics( + winningStatistics[0].matchThree, + winningStatistics[1].matchFour, + winningStatistics[2].matchFive, + winningStatistics[3].matchFiveAndBonus, + winningStatistics[4].matchSix + ); + } + + getWinningAmount(winningStatistics) { + const calculThree = winningStatistics[0].matchThree * 5000; + const calculFour = winningStatistics[1].matchFour * 50000; + const calculFive = winningStatistics[2].matchFive * 1500000; + const calculFiveAndBonus = winningStatistics[3].matchFiveAndBonus * 30000000; + const calculSix = winningStatistics[4].matchSix * 2000000000; + const sum = calculThree + calculFour + calculFive + calculFiveAndBonus + calculSix; + return sum; + } + + printProfitRate(purchasePrice, getWinningAmount) { + const profitRateCalcul = (((getWinningAmount - purchasePrice) / purchasePrice) * 100).toFixed( + 2 + ); + OutputView.printTotalReturn(profitRateCalcul); + } +} + +export default PrintWinningDetails; diff --git a/src/utils/createError.js b/src/utils/createError.js new file mode 100644 index 000000000..849d04a44 --- /dev/null +++ b/src/utils/createError.js @@ -0,0 +1,9 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import { ERROR_PREFIX } from '../constants/errorMessages.js'; + +const createThrowError = (message) => { + MissionUtils.Console.print(ERROR_PREFIX); + throw new Error(message); +}; + +export default createThrowError; diff --git a/src/utils/makeArrayFromString.js b/src/utils/makeArrayFromString.js new file mode 100644 index 000000000..4726c576c --- /dev/null +++ b/src/utils/makeArrayFromString.js @@ -0,0 +1,5 @@ +const makeArrayFromString = (value) => { + return value.split(","); +}; + +export default makeArrayFromString; \ No newline at end of file diff --git a/src/validation/validation.js b/src/validation/validation.js new file mode 100644 index 000000000..9304349bc --- /dev/null +++ b/src/validation/validation.js @@ -0,0 +1,74 @@ +import { ERROR_MESSAGES } from '../constants/errorMessages.js'; +import createThrowError from '../utils/createError.js'; +import makeArrayFromString from '../utils/makeArrayFromString.js'; + +class validation { + empty(value) { + if (value === '') createThrowError(ERROR_MESSAGES.emptyValue); + return true; + } + + regularExpression(value, regExp) { + const regExpTest = regExp.test(value); + if (!regExpTest) createThrowError(ERROR_MESSAGES.regexpTest); + return true; + } + + startedComma(value) { + const regExp = /^,/; + const regExpTest = regExp.test(value); + if (regExpTest) createThrowError(ERROR_MESSAGES.startComma); + return true; + } + + endedComma(value) { + const regExp = /,$/; + const regExpTest = regExp.test(value); + if (regExpTest) createThrowError(ERROR_MESSAGES.endComma); + return true; + } + + nagativeNumber(value) { + const changeTypeNumber = Number(value); + if (0 > changeTypeNumber) createThrowError(ERROR_MESSAGES.negativeNumber); + return true; + } + + limitDigits(value) { + const regExp = /\d{4,6}$/; + const regExpTest = regExp.test(value); + if (!regExpTest) createThrowError(ERROR_MESSAGES.limitDigits); + return true; + } + + thousandUnit(value) { + const changeTypeNumber = Number(value); + const target = changeTypeNumber / 1000; + const isInteger = Number.isInteger(target); + if (!isInteger) createThrowError(ERROR_MESSAGES.thousandUnit); + if (0 < target && target > 101) createThrowError(ERROR_MESSAGES.thousandUnit); + return true; + } + + winningNumberSixDigit(value) { + const lottoNumberArray = value.split(','); + if (lottoNumberArray.length !== 6) createThrowError(ERROR_MESSAGES.winningNumberSixDigit); + return true; + } + + isDuplicatedInLottoNumbers(number) { + const changeArray = makeArrayFromString(number); + const removeDuplicated = new Set(changeArray).size; + if (changeArray.length !== removeDuplicated) + createThrowError(ERROR_MESSAGES.isDuplicatedInLottoNumbers); + } + + isDuplicatedInWinningNumber(bonusNumber, winningNumber) { + const isDuplicated = winningNumber.some( + (winningNumber) => Number(winningNumber) === Number(bonusNumber) + ); + if (isDuplicated) createThrowError(ERROR_MESSAGES.isDuplicatedInWinningNumber); + } +} + +export default validation; diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 000000000..23af82e78 --- /dev/null +++ b/src/view/InputView.js @@ -0,0 +1,18 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import INPUT_MESSAGES from '../constants/inputMessages.js'; + +const InputView = { + async readPurchaseAmount() { + return await MissionUtils.Console.readLineAsync(INPUT_MESSAGES.whatPurchaseAmount); + }, + + async readWinningNumber() { + return await MissionUtils.Console.readLineAsync(INPUT_MESSAGES.winningNumber); + }, + + async readBonusNumber() { + return await MissionUtils.Console.readLineAsync(INPUT_MESSAGES.bonusNumber); + }, +}; + +export default InputView; diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 000000000..19bf8e2d1 --- /dev/null +++ b/src/view/OutputView.js @@ -0,0 +1,38 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; +import OUTPUT_MESSAGES from '../constants/outputMessages.js'; + +const OutputView = { + async printPurchaseNumber(purchasQuantity) { + MissionUtils.Console.print(`${purchasQuantity}${OUTPUT_MESSAGES.purchaseNumber}`); + }, + + async printWinningMessage() { + MissionUtils.Console.print(OUTPUT_MESSAGES.winningStatistics); + }, + + async printWinningStatistics(three, four, five, fiveAndBonus, six) { + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedThree}${three}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedFour}${four}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedFive}${five}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedFiveBonus}${fiveAndBonus}${OUTPUT_MESSAGES.printQuantity}`); + MissionUtils.Console.print(`${OUTPUT_MESSAGES.matchedSix}${six}${OUTPUT_MESSAGES.printQuantity}`); + }, + + async printTotalReturn(percentage) { + MissionUtils.Console.print(`${OUTPUT_MESSAGES.totalReturn}${percentage}${OUTPUT_MESSAGES.totalReturnPercentage}`); + }, + + async printLottoNumber(value) { + MissionUtils.Console.print(value); + }, + + async printError(error) { + MissionUtils.Console.print(error); + }, + + async printSpace() { + MissionUtils.Console.print(''); + }, +}; + +export default OutputView;