diff --git a/README.md b/README.md index 15bb106b5..87b58be90 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ -# javascript-lotto-precourse +# [3주차] 로또 + +---- + +## 1. 구현할 기능 목록 + +---- + +### 1. 입력값 받기 : 로또 구입 금액 입력받기 +### 2. 구매한 로또의 총 갯수를 구하고, 문구를 출력한다. +### 3. 구매한 로또들의 랜덤 번호 생성 +- 구매한 로또번호들의 6가지 랜덤 번호를 생성한다 +- 오름차순으로 정렬하여 저장한다 +- 구매한 로또 번호를 출력한다. +### 4. 당첨 번호를 입력받는다 +### 5. 보너스 번호를 입력받는다. +### 6. 당첨 통계를 구한다 +- 각각의 index가 일치한 숫자의 갯수를 의미하는 길이가 8인 배열을 만든다. (index = 6은 2등을, index = 7은 1등을 의미한다.) +- 구매한 로또들을 순회 + - 하나의 로또에서 몇개의 번호가 맞았는지 구한다. + - 배열[일치한 갯수] 에 값을 증가시킨다. + - 만약 5개 맞았다면, 보너스 번호 일치 여부를 확인한다 + - 만약 6개 맞았다면, index = 7에 저장한다. +### 7. 수익률을 계산한다 + +- 배열을 사용하여 (index = 7 은 1등 ... index = 3 은 5등) 당첨금의 합을 구한다 +- 당첨금/지불한 값으로 수익률을 계산한다 +- 소수점 둘째 자리에서 반올림한다 + +---- + +## 2. 예외 케이스 +### 1. 로또 구입 금액 입력받기 +- 1000원 단위 금액이 아닌 경우 +- 양의 정수를 입력했는가(/^[1-9]\d*$/) +- 최대값 제한 (SAFE INTEGER 범위) + +### 3. 구매한 로또들의 랜덤번호 생성 +- 로또 번호의 수가 6개인지 확인 +- 로또 번호에 중복이 없는지 확인 + +### 4. 로또 당첨 번호 입력 +- 당첨번호가 6개가 아닌 경우 +- 양의 정수를 입력했는가 (/^[1-9]\d*$/) +- 당첨번호가 유효범위 내에 있지 않은 경우 (1~45의 정수가 아님) +- 중복된 수를 입력한 경우 + +### 5. 보너스 번호 입력 +- 양의 정수를 입력했는가 +- 당첨번호가 유효범위 내에 있지 않은 경우 +- 당첨 번호와 중복되는지 여부 + +--- + +## 3. 고민한 부분 +- 지난 회차에서 미흡하다고 생각했던 Unit Test를 상세히 추가했습니다. + 특히, validation Check를 꼼꼼하게 하지 못했다고 생각해서, 검사하는 부분 및 순서에 시간을 많이 사용하였습니다. 또한, 해당 부분을 중점으로 unit test를 좀 더 꼼꼼하게 작성하였습니다. +- 상수화에 대한 고민. 상수화를 최대한 하되, 구분이 되도록 constant.js 파일 내에서도 관련 그룹으로 묶어 용도를 명확히 하고자 노력했습니다. +- 책임의 분리. 어떤 객체, 클래스에 책임을 두어야 할지 고민하면서, 객체가 너무 많아지지 않도록 여러번의 refactoring을 거쳤습니다. + +이번 과제는 구현하면서 세세한 부분이 생각보다 많아, 제가 생각한 것보다 코드를 깔끔하게 작성하기 어려웠습니다.
+또한, 코드와 함수가 많아지니 책임의 분리가 어려웠습니다. 부족한 코드지만 많은 피드백 부탁드립니다. \ No newline at end of file diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 872380c9c..6951a40d9 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,97 +1,99 @@ import App from "../src/App.js"; -import { MissionUtils } from "@woowacourse/mission-utils"; +import {MissionUtils} from "@woowacourse/mission-utils"; +import {ERROR_CODE} from "../src/constants/constants.js"; -const mockQuestions = (inputs) => { - MissionUtils.Console.readLineAsync = jest.fn(); +export const mockQuestions = (inputs) => { + MissionUtils.Console.readLineAsync = jest.fn(); - MissionUtils.Console.readLineAsync.mockImplementation(() => { - const input = inputs.shift(); + MissionUtils.Console.readLineAsync.mockImplementation(() => { + const input = inputs.shift(); - return Promise.resolve(input); - }); + return Promise.resolve(input); + }); }; const mockRandoms = (numbers) => { - MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); - numbers.reduce((acc, number) => { - return acc.mockReturnValueOnce(number); - }, MissionUtils.Random.pickUniqueNumbersInRange); + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); }; const getLogSpy = () => { - const logSpy = jest.spyOn(MissionUtils.Console, "print"); - logSpy.mockClear(); - return logSpy; + const logSpy = jest.spyOn(MissionUtils.Console, "print"); + logSpy.mockClear(); + return logSpy; }; const runException = async (input) => { - // given - 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"]; - - mockRandoms([RANDOM_NUMBERS_TO_END]); - mockQuestions([input, ...INPUT_NUMBERS_TO_END]); - - // when - const app = new App(); - await app.run(); - - // then - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); -}; - -describe("로또 테스트", () => { - beforeEach(() => { - jest.restoreAllMocks(); - }); - - test("기능 테스트", async () => { // given const logSpy = getLogSpy(); - mockRandoms([ - [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], - ]); - mockQuestions(["8000", "1,2,3,4,5,6", "7"]); + const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5, 6]; + const INPUT_NUMBERS_TO_END = ["1000", "1,2,3,4,5,6", "7"]; + + mockRandoms([RANDOM_NUMBERS_TO_END]); + mockQuestions([input, ...INPUT_NUMBERS_TO_END]); // when const app = new App(); await app.run(); // 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%입니다.", - ]; - - logs.forEach((log) => { - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); +}; + +describe("로또 테스트", () => { + beforeEach(() => { + jest.restoreAllMocks(); }); - }); - test("예외 테스트", async () => { - await runException("1000j"); - }); + test("기능 테스트", async () => { + // given + const logSpy = getLogSpy(); + + mockRandoms([ + [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], + ]); + mockQuestions(["8000", "1,2,3,4,5,6", "7"]); + + // when + const app = new App(); + await app.run(); + + // 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%입니다.", + ]; + + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + + + test("예외 테스트", async () => { + await runException("1000j"); + }); }); diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js index 409aaf69b..1ad91be01 100644 --- a/__tests__/LottoTest.js +++ b/__tests__/LottoTest.js @@ -1,18 +1,171 @@ -import Lotto from "../src/Lotto"; +import Lotto from "../src/Models/Lotto.js"; +import {MissionUtils, Console} from "@woowacourse/mission-utils"; +import {lottoUtils} from "../src/utils/lotto.utils.js"; +import {ERROR_CODE, LOTTO} from "../src/constants/constants.js"; +import LottoGame from "../src/Models/LottoGame.js"; + +const getLogSpy = () => { + const logSpy = jest.spyOn(MissionUtils.Console, "print"); + logSpy.mockClear(); + return logSpy; +}; + +const mockRandoms = (numbers) => { + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); +}; describe("로또 클래스 테스트", () => { - test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 6, 7]); - }).toThrow("[ERROR]"); - }); - - // TODO: 테스트가 통과하도록 프로덕션 코드 구현 - test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => { - expect(() => { - new Lotto([1, 2, 3, 4, 5, 5]); - }).toThrow("[ERROR]"); - }); - - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 + test("예외 테스트 : 로또 번호의 개수가 6개가 넘어가는 경우", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 6, 7]); + }).toThrow(ERROR_CODE.SIZE_OUT_OF_RANGE(LOTTO.SIZE)); + }); + + test("예외 테스트 : 로또 번호의 개수가 6개 이하인 경우", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5]); + }).toThrow(ERROR_CODE.SIZE_OUT_OF_RANGE(LOTTO.SIZE)); + }); + + test("예외 테스트 : 로또 번호 중복이 있는 경우", () => { + expect(() => { + new Lotto([1, 2, 3, 4, 5, 5]); + }).toThrow(ERROR_CODE.NUMBER_DUPLICATE); + }); + + test("정상 케이스 : 로또 오름차순 정렬 후 출력", () => { + const logSpy = getLogSpy(); + + const RANDOM_NUMBERS_TO_END = [7, 1, 43, 24, 35, 6]; + mockRandoms([RANDOM_NUMBERS_TO_END]); + + const log = "[1, 6, 7, 24, 35, 43]" + + const lottos = lottoUtils.generateNLottos(1) + lottos.forEach(lotto => { + Console.print(lotto.toString()) + expect(logSpy).toHaveBeenCalledWith(log); + }) + + }) + }); + + +describe("당첨 케이스 테스트", () => { + test("1등 당첨 케이스", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 38]) + ] + const winningNumbers = [1, 3, 5, 8, 11, 38] + const bonusNumber = 39 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + const lottoResult = [0, 0, 0, 0, 0, 0, 0, 1] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + + test("2등 당첨 케이스", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 39]) + ] + const winningNumbers = [1, 3, 5, 8, 11, 38] + const bonusNumber = 39 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + + const lottoResult = [0, 0, 0, 0, 0, 0, 1, 0] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + + test("3등 당첨 케이스", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 39]) + ] + const winningNumbers = [1, 3, 5, 8, 11, 38] + const bonusNumber = 40 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + + const lottoResult = [0, 0, 0, 0, 0, 1, 0, 0] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + + test("4등 당첨 케이스", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 39]) + ] + const winningNumbers = [1, 3, 5, 8, 14, 38] + const bonusNumber = 40 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + + const lottoResult = [0, 0, 0, 0, 1, 0, 0, 0] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + + test("5등 당첨 케이스", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 39]) + ] + const winningNumbers = [1, 3, 5, 10, 14, 38] + const bonusNumber = 40 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + + const lottoResult = [0, 0, 0, 1, 0, 0, 0, 0] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + + test("0개 일치 케이스", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 39]) + ] + const winningNumbers = [2, 4, 6, 10, 14, 38] + const bonusNumber = 40 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + + const lottoResult = [1, 0, 0, 0, 0, 0, 0, 0] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + + test("2개 이상 로또 ", () => { + const lottos = [ + new Lotto([1, 3, 5, 8, 11, 40]), + new Lotto([1, 2, 6, 8, 11, 39]) + ] + const winningNumbers = [1, 3, 5, 8, 11, 39] + const bonusNumber = 40 + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + + const lottoResult = [0, 0, 0, 0, 1, 0, 1, 0] //배열의 index는 일치하는 갯수를 위미( but, 6은 2등(5개 + 보너스)/ 7은 1등) + + expect( + lottoGame.getLottoMatchResultArray() + ).toStrictEqual(lottoResult); + }); + +}); \ No newline at end of file diff --git a/__tests__/unitTest/validation.test.js b/__tests__/unitTest/validation.test.js new file mode 100644 index 000000000..1bbfc205c --- /dev/null +++ b/__tests__/unitTest/validation.test.js @@ -0,0 +1,120 @@ +import {ERROR_CODE, PURCHASE_PRICE, WINNING_NUMBER} from "../../src/constants/constants.js"; +import { + bonusNumbersValidate, bonusNumbersValidateWithWinningNumber, + purchasePriceValidate, + validator, + winningNumbersValidate +} from "../../src/validation/validator.js"; + +describe("로또 구입 금액 테스트", () => { + + test("정상 테스트", () => { + const purchasePrice = "1000" + expect(purchasePriceValidate(purchasePrice)).toEqual(1000); + }); + + test("정상 테스트", () => { + const purchasePrice = 9007199254740000 + expect(purchasePriceValidate(purchasePrice)).toEqual(9007199254740000); + }); + + test("예외 테스트 : 숫자가 아닌 값", () => { + const purchasePrice = "12ab" + expect(() => purchasePriceValidate(purchasePrice)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 0 이하의 값 ", () => { + const purchasePrice = "0" + expect(() => purchasePriceValidate(purchasePrice)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 음수값 ", () => { + const purchasePrice = "-1000" + expect(() => purchasePriceValidate(purchasePrice)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 1000단위가 아닌 금액", () => { + const purchasePrice = "9007199254740991" + expect(() => purchasePriceValidate(purchasePrice)).toThrow(ERROR_CODE.NOT_DIVIDED_BY_VALUE(PURCHASE_PRICE.MIN_CURR_UNIT)); + }); + + test("예외 테스트 : 범위를 벗어난 값", () => { + const purchasePrice = "9007199254740993" + expect(() => purchasePriceValidate(purchasePrice)).toThrow(ERROR_CODE.OUT_OF_RANGE(1, Number.MAX_SAFE_INTEGER)); + }); +}); + + +describe("당첨 번호 유효성 테스트", () => { + + test("정상 테스트", () => { + const winningNumber = [1, 2, 3, 4, 5, 6] + expect(winningNumbersValidate(winningNumber)).toEqual(winningNumber); + }); + + test("정상 테스트", () => { + const winningNumber = [3, 12, 14, 35, 41, 45] + expect(winningNumbersValidate(winningNumber)).toEqual(winningNumber); + }); + + + test("예외 테스트 : 양의 정수가 아닌 값 포함", () => { + const winningNumber = [3, -12, 14, 35, 0, 42] + expect(() => winningNumbersValidate(winningNumber)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 양의 정수가 아닌 값 포함", () => { + const winningNumber = ["1ab", 12, 14, 35, 41, 42] + expect(() => winningNumbersValidate(winningNumber)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 유효범위(1~45) 이상의 값이 포함된 경우", () => { + const winningNumber = [1, 12, 14, 35, 41, 46] + expect(() => winningNumbersValidate(winningNumber)).toThrow(ERROR_CODE.OUT_OF_RANGE(WINNING_NUMBER.MIN_NUMBER, WINNING_NUMBER.MAX_NUMBER)); + }); + + test("예외 테스트 : 당첨 번호 값이 6개 미만 경우", () => { + const winningNumber = [1, 12, 14, 35, 41] + expect(() => winningNumbersValidate(winningNumber)).toThrow(ERROR_CODE.SIZE_OUT_OF_RANGE(WINNING_NUMBER.SIZE)); + }); + + test("예외 테스트 : 당첨 번호 값이 7개 이상 경우", () => { + const winningNumber = [1, 12, 14, 35, 41, 42, 43] + expect(() => winningNumbersValidate(winningNumber)).toThrow(ERROR_CODE.SIZE_OUT_OF_RANGE(WINNING_NUMBER.SIZE)); + }); + + test("예외 테스트 : 당첨 번호에 중복값이 존재하는 경우", () => { + const winningNumber = [1, 12, 12, 35, 41, 42] + expect(() => winningNumbersValidate(winningNumber)).toThrow(ERROR_CODE.NUMBER_DUPLICATE); + }); +}); + +describe("보너스 번호 유효성 테스트", () => { + + test("정상 테스트", () => { + const bonusNumber = 43 + expect(bonusNumbersValidate(bonusNumber)).toEqual(Number(bonusNumber)); + }); + + test("예외 테스트 : 양의 정수를 입력하지 않은 경우", () => { + const bonusNumber = "0" + expect(() => bonusNumbersValidate(bonusNumber)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 양의 정수를 입력하지 않은 경우", () => { + const bonusNumber = "abc" + expect(() => bonusNumbersValidate(bonusNumber)).toThrow(ERROR_CODE.NOT_POSITIVE_NUMBER); + }); + + test("예외 테스트 : 보너스 번호가 범위를 넘어간 경우(1~45)", () => { + const bonusNumber = "46" + expect(() => bonusNumbersValidate(bonusNumber)).toThrow(ERROR_CODE.OUT_OF_RANGE(WINNING_NUMBER.MIN_NUMBER, WINNING_NUMBER.MAX_NUMBER)); + }); + + test("예외 테스트 : 당첨 번호에 중복값이 존재하는 경우", () => { + const winningNumber = [1, 12, 14, 35, 41, 42] + const bonusNumber = 1 + expect(() => bonusNumbersValidateWithWinningNumber(bonusNumber, winningNumber)).toThrow(ERROR_CODE.BONUS_NUMBER_DUPLICATE); + }); + +}); diff --git a/src/App.js b/src/App.js index 091aa0a5d..8a5c4e0ba 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,35 @@ +import {IOHandler} from "./utils/IOHandler.js"; +import {INSTRUCTION} from "./constants/constants.js"; +import {purchasePriceUtils} from "./utils/purchasePrice.utils.js"; +import {Console} from '@woowacourse/mission-utils' +import {lottoUtils} from "./utils/lotto.utils.js"; +import { + purchasePriceValidate, +} from "./validation/validator.js"; +import LottoGame from "./Models/LottoGame.js"; + class App { - async run() {} + async run() { + try { + const purchasePrice = await IOHandler.getInput(INSTRUCTION.GET_PURCHASE_PRICE); + purchasePriceValidate(purchasePrice); + + const lottoAmount = purchasePriceUtils.getLottoAmount(purchasePrice); + IOHandler.printLottoAmount(lottoAmount); + const lottos = lottoUtils.generateNLottos(lottoAmount); + IOHandler.printLottoArray(lottos) + + const winningNumbers = await IOHandler.getInput(INSTRUCTION.GET_WINNING_NUMBERS, (str) => str.split(',')); + const bonusNumber = await IOHandler.getInput(INSTRUCTION.GET_BONUS_NUMBER); + + const lottoGame = new LottoGame(winningNumbers, bonusNumber, lottos); + IOHandler.printWinningStatisticsAll(lottoGame) + IOHandler.printProfitRate(lottoGame.calculateProfitRate(purchasePrice)) + + } catch (error) { + Console.print(error.message) + } + } } -export default App; +export default App; \ No newline at end of file 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/Models/Lotto.js b/src/Models/Lotto.js new file mode 100644 index 000000000..dfada5c44 --- /dev/null +++ b/src/Models/Lotto.js @@ -0,0 +1,43 @@ +import {ERROR_CODE, LOTTO} from "../constants/constants.js"; +import {validator} from "../validation/validator.js"; + +class Lotto { + #numbers; + + constructor(numbers) { + this.#validate(numbers); + this.#numbers = numbers; + } + + #validate(numbers) { + if (!validator.isCorrectSize(numbers, LOTTO.SIZE)) { + throw new Error(ERROR_CODE.SIZE_OUT_OF_RANGE(LOTTO.SIZE)); + } + if (validator.hasDuplicates(numbers)) { + throw new Error(ERROR_CODE.NUMBER_DUPLICATE) + } + } + + toString() { + return `[${this.#numbers.join(", ")}]`; + } + + #countLottoMatches(winningNumbers) { + return this.#numbers.filter(num => winningNumbers.includes(num)).length + } + + #isBonusNumberMatch(bonusNumber) { + return this.#numbers.includes(bonusNumber); + } + + getLottoResult(winningNumbers, bonusNumber) { + const matchNumber = this.#countLottoMatches(winningNumbers); + if (matchNumber === 5 && this.#isBonusNumberMatch(bonusNumber) || matchNumber === 6) { + return matchNumber + 1; + } + return matchNumber; + } + +} + +export default Lotto; diff --git a/src/Models/LottoGame.js b/src/Models/LottoGame.js new file mode 100644 index 000000000..e3776c6a3 --- /dev/null +++ b/src/Models/LottoGame.js @@ -0,0 +1,50 @@ +import { + bonusNumbersValidate, + bonusNumbersValidateWithWinningNumber, + winningNumbersValidate +} from "../validation/validator.js"; +import {lottoUtils} from "../utils/lotto.utils.js"; + +class LottoGame { + #winningNumber + #bonusNumber + #lottos //Lotto 객체 + + constructor(winningNumber, bonusNumber, lottos) { + this.#validate(winningNumber, bonusNumber) + this.#winningNumber = winningNumber.map(Number); + this.#bonusNumber = Number(bonusNumber); + this.#lottos = lottos; + } + + #validate(winningNumber, bonusNumber) { + winningNumbersValidate(winningNumber); + bonusNumbersValidate(bonusNumber); + bonusNumbersValidateWithWinningNumber(bonusNumber, winningNumber); + } + + getLottoMatchResultArray() { + let lottoResult = Array(8).fill(0); + this.#lottos.forEach((lotto) => { + const matchNumber = lotto.getLottoResult(this.#winningNumber, this.#bonusNumber); + lottoResult[matchNumber]++ + }) + return lottoResult; + } + + calculateProfitRate(purchasePrice) { + const profitRate = this.calculateTotalPrize() / purchasePrice * 100; + return Math.round(profitRate * 100) / 100; + } + + calculateTotalPrize() { + let totalPrize = 0; + this.getLottoMatchResultArray().forEach((amount, index) => { + totalPrize += amount * lottoUtils.getPrize(index); + }) + return totalPrize; + } + +} + +export default LottoGame; \ No newline at end of file diff --git a/src/constants/constants.js b/src/constants/constants.js new file mode 100644 index 000000000..ffbdbe288 --- /dev/null +++ b/src/constants/constants.js @@ -0,0 +1,41 @@ +export const ERROR_CODE = { + NOT_POSITIVE_NUMBER: "[ERROR] 1이상의 숫자를 입력해주세요.", + OUT_OF_RANGE: (min, max) => `[ERROR] ${min}과 ${max}사이의 값을 입력해주세요.`, + NOT_DIVIDED_BY_VALUE: (value) => `[ERROR] ${value}단위의 금액을 입력해주세요.`, + SIZE_OUT_OF_RANGE: (size) => `[ERROR] 번호는 ${size}개여야 합니다.`, + NUMBER_DUPLICATE: `[ERROR] 번호에 중복이 있습니다.`, + BONUS_NUMBER_DUPLICATE: '[ERROR] 보너스 번호가 당첨번호와 중복됩니다.' +}; + +export const PURCHASE_PRICE = { + MIN_CURR_UNIT: 1000, +} + +export const LOTTO = { + SIZE: 6, + MIN_NUMBER: 1, + MAX_NUMBER: 45, + FIRST_PRIZE: 2000000000, + SECOND_PRIZE: 30000000, + THIRD_PRIZE: 1500000, + FOURTH_PRIZE: 50000, + FIFTH_PRIZE: 5000, +} + + +export const WINNING_NUMBER = { + SIZE: 6, + MIN_NUMBER: 1, + MAX_NUMBER: 45, +} + +export const INSTRUCTION = { + GET_PURCHASE_PRICE: "구입금액을 입력해 주세요.\n", + PRINT_LOTTO_AMOUNT: (lottoAmount) => `${lottoAmount}개를 구매했습니다.`, + GET_WINNING_NUMBERS: "당첨 번호를 입력해 주세요.\n", + GET_BONUS_NUMBER: "\n보너스 번호를 입력해 주세요.\n", + EXTRA_MESSAGE_SECOND_PRIZE: ", 보너스 볼 일치", + PRINT_WINNING_STATISTICS: (matchNumber, prize, matchAmount, extraMessage = "") => `${matchNumber}개 일치${extraMessage} (${prize}원) - ${matchAmount}개`, + PRINT_TOTAL_WINNING_STATISTICS: "\n당첨 통계\n---", + PRINT_PROFIT_RATE: (profitRate) => `총 수익률은 ${profitRate}입니다.` +} \ No newline at end of file diff --git a/src/utils/IOHandler.js b/src/utils/IOHandler.js new file mode 100644 index 000000000..94966c698 --- /dev/null +++ b/src/utils/IOHandler.js @@ -0,0 +1,44 @@ +import {Console} from '@woowacourse/mission-utils' +import {INSTRUCTION} from "../constants/constants.js"; +import {lottoUtils} from "./lotto.utils.js"; + +export const IOHandler = { + async getInput(instruction, process) { + let input = await Console.readLineAsync(instruction) + + if (process) + input = process(input); + + return input + }, + printLottoArray(lottos) { + lottos.map(lotto => { + Console.print(lotto.toString()) + }) + }, + printWinningStatistics(matchNumber, prize, matchAmount) { + if (matchNumber < 3) return + if (matchNumber === 6) { //2등 ( 5개 + 보너스 번호 ) + Console.print(INSTRUCTION.PRINT_WINNING_STATISTICS(matchNumber - 1, lottoUtils.makeMoneyFormat(prize), matchAmount, INSTRUCTION.EXTRA_MESSAGE_SECOND_PRIZE)) + return + } + if (matchNumber === 7) { // 1등 ( 2등과 구분을 위해 맞은번호(6개) + 1개 = 7 ) + Console.print(INSTRUCTION.PRINT_WINNING_STATISTICS(matchNumber - 1, lottoUtils.makeMoneyFormat(prize), matchAmount)) + return + } + Console.print(INSTRUCTION.PRINT_WINNING_STATISTICS(matchNumber, lottoUtils.makeMoneyFormat(prize), matchAmount)) + }, + printWinningStatisticsAll(lottoGame) { + Console.print(INSTRUCTION.PRINT_TOTAL_WINNING_STATISTICS); + lottoGame.getLottoMatchResultArray().forEach((amount, index) => { + this.printWinningStatistics(index, lottoUtils.getPrize(index), amount) + }) + }, + printProfitRate(profitRate) { + const formattedRate = `${profitRate.toFixed(1)}%`; + Console.print(INSTRUCTION.PRINT_PROFIT_RATE(formattedRate)) + }, + printLottoAmount(lottoAmount) { + Console.print(INSTRUCTION.PRINT_LOTTO_AMOUNT(lottoAmount)); + } +} \ No newline at end of file diff --git a/src/utils/lotto.utils.js b/src/utils/lotto.utils.js new file mode 100644 index 000000000..8ce492841 --- /dev/null +++ b/src/utils/lotto.utils.js @@ -0,0 +1,37 @@ +import {LOTTO} from "../constants/constants.js"; +import {Random} from '@woowacourse/mission-utils'; +import Lotto from "../Models/Lotto.js"; + +export const lottoUtils = { + generateNLottos(n) { + let lottos = [] + Array(n).fill().map(() => { + const lotto = Random.pickUniqueNumbersInRange(LOTTO.MIN_NUMBER, LOTTO.MAX_NUMBER, LOTTO.SIZE) + lottos.push(new Lotto(lotto.sort((a, b) => a - b))) + }) + return lottos; + }, + getPrize(matchNumber) { + switch (matchNumber) { + case 7: + return LOTTO.FIRST_PRIZE + case 6: + return LOTTO.SECOND_PRIZE + case 5: + return LOTTO.THIRD_PRIZE + case 4: + return LOTTO.FOURTH_PRIZE + case 3: + return LOTTO.FIFTH_PRIZE + default: + return 0 + } + }, + makeMoneyFormat(money, separator = ",") { + return money.toString().split("").reverse().join("") + .replace(/(.{3})(?=.)/g, `$1${separator}`) + .split("").reverse().join(""); + }, + + +} \ No newline at end of file diff --git a/src/utils/purchasePrice.utils.js b/src/utils/purchasePrice.utils.js new file mode 100644 index 000000000..f1e6c9375 --- /dev/null +++ b/src/utils/purchasePrice.utils.js @@ -0,0 +1,7 @@ +import {PURCHASE_PRICE} from "../constants/constants.js"; + +export const purchasePriceUtils = { + getLottoAmount(purchasePrice) { + return purchasePrice / PURCHASE_PRICE.MIN_CURR_UNIT; + } +} \ No newline at end of file diff --git a/src/validation/validator.js b/src/validation/validator.js new file mode 100644 index 000000000..08e8bc546 --- /dev/null +++ b/src/validation/validator.js @@ -0,0 +1,61 @@ +import {ERROR_CODE, PURCHASE_PRICE, WINNING_NUMBER} from "../constants/constants.js"; + +export const validator = { + isPositiveNumber(number) { + return /^[1-9]\d*$/.test(number.toString().trim()) + }, + isInRange(number, minValue = 1, maxValue = Number.MAX_SAFE_INTEGER) { + return Number(number) <= maxValue && Number(number) >= minValue + }, + isDividedNumberByValue(number, value) { + return Number(number) % Number(value) === 0 + }, + hasDuplicates(numbers) { + return new Set(numbers).size !== numbers.length; + }, + isCorrectSize(number, size) { + return number.length === size; + } +} + +export function purchasePriceValidate(purchasePrice) { + if (!validator.isPositiveNumber(purchasePrice)) + throw new Error(ERROR_CODE.NOT_POSITIVE_NUMBER); + if (!validator.isInRange(purchasePrice)) { + throw new Error(ERROR_CODE.OUT_OF_RANGE(1, Number.MAX_SAFE_INTEGER)); + } + if (!validator.isDividedNumberByValue(purchasePrice, PURCHASE_PRICE.MIN_CURR_UNIT)) + throw new Error(ERROR_CODE.NOT_DIVIDED_BY_VALUE(PURCHASE_PRICE.MIN_CURR_UNIT)); + return Number(purchasePrice); +} + +export function winningNumbersValidate(winningNumbers) { + winningNumbers.forEach((number) => { + if (!validator.isPositiveNumber(number)) + throw new Error(ERROR_CODE.NOT_POSITIVE_NUMBER) + if (!validator.isInRange(number, WINNING_NUMBER.MIN_NUMBER, WINNING_NUMBER.MAX_NUMBER)) + throw new Error(ERROR_CODE.OUT_OF_RANGE(WINNING_NUMBER.MIN_NUMBER, WINNING_NUMBER.MAX_NUMBER)); + }) + if (!validator.isCorrectSize(winningNumbers, WINNING_NUMBER.SIZE)) + throw new Error(ERROR_CODE.SIZE_OUT_OF_RANGE(WINNING_NUMBER.SIZE)); + if (validator.hasDuplicates(winningNumbers)) { + throw new Error(ERROR_CODE.NUMBER_DUPLICATE) + } + return winningNumbers.map(Number); +} + +export function bonusNumbersValidate(bonusNumber) { + if (!validator.isPositiveNumber(bonusNumber)) + throw new Error(ERROR_CODE.NOT_POSITIVE_NUMBER); + if (!validator.isInRange(bonusNumber, WINNING_NUMBER.MIN_NUMBER, WINNING_NUMBER.MAX_NUMBER)) { + throw new Error(ERROR_CODE.OUT_OF_RANGE(WINNING_NUMBER.MIN_NUMBER, WINNING_NUMBER.MAX_NUMBER)); + } + return Number(bonusNumber); +} + +export function bonusNumbersValidateWithWinningNumber(bonusNumber, winningNumber) { + if (validator.hasDuplicates([...winningNumber, bonusNumber])) + throw new Error(ERROR_CODE.BONUS_NUMBER_DUPLICATE) +} + +