Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[로또] 윤선례 미션 제출합니다. #402

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
# javascript-lotto-precourse
### 로또 발매기

간단한 로또 발매기를 구현한다.

### 구현할 기능 목록

**프로그램 실행 시**

- 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받는다
- 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다
- 보너스 번호를 입력 받는다

**출력**

- 발행한 로또 수량 및 번호를 출력한다
- 로또 번호는 오름차순으로 정렬하여 보여준다
- 당첨 내역을 출력한다
- 수익률은 소수점 둘째 자리에서 반올림한다

**예외 처리**

- 구입 금액이 숫자가 아닌 경우 예외 처리한다
- 구입 금액이 0원인 경우 예외 처리한다
- 로또 구입 금액이 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다
- 로또 당첨 번호가 중복인 경우 예외 처리한다
- 로또 당첨 번호의 개수가 6개 초과일 경우 예외 처리한다
- 로또 번호가 1~45 사이의 숫자가 아닌 경우 예외 처리한다
50 changes: 25 additions & 25 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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;
};
Expand All @@ -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]);
Expand All @@ -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();

Expand All @@ -61,37 +61,37 @@ 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();
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%입니다.",
'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");
test('예외 테스트', async () => {
await runException('1000j');
});
});
18 changes: 11 additions & 7 deletions __tests__/LottoTest.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import Lotto from "../src/Lotto";
import Lotto from '../src/Model/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: 추가 기능 구현에 따른 테스트 코드 작성
test('로또 번호가 6개 미만일 경우 예외가 발생한다.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5]);
}).toThrow('[ERROR]');
});
});
163 changes: 162 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,166 @@
import { Random } from '@woowacourse/mission-utils';
import {
DELIMITER,
LOTTO_PRICE,
FIRST_PRIZE_MONEY,
SECOND_PRIZE_MONEY,
THIRD_PRIZE_MONEY,
FOURTH_PRIZE_MONEY,
FIFTH_PRIZE_MONEY,
} from './constants/constants.js';
import { ERROR_MESSAGES } from './constants/messages.js';
import Lotto from './Model/Lotto.js';
import InputView from './View/InputView.js';
import OutputView from './View/OutputView.js';

class App {
async run() {}
#result;

constructor() {
this.#result = [];
}

async run() {
const inputView = new InputView();
const outputView = new OutputView();

const amount = await inputView.getAmount();
this.validateAmount(amount);

const lottoCount = amount / LOTTO_PRICE;
const lottos = await this.buyLotto(lottoCount);

await outputView.printBoughtLottos(lottoCount, lottos);

const winningNumbers = await inputView.getWinningNumbers();
const splittedWinningNumbers = winningNumbers
.split(DELIMITER.trim())
.map((number) => Number(number));

const winningLotto = new Lotto(splittedWinningNumbers);
const bonusNumber = await inputView.getBonusNumber();

this.#result = this.getLottoMatchResults(lottos, winningLotto, bonusNumber);
const prizeCount = this.prizeCountCalculate();
const profitRate = this.calculateProfitRate(amount, prizeCount);

await outputView.printResult(prizeCount, profitRate);
}

validateAmount(amount) {
if (isNaN(amount)) {
throw new Error(ERROR_MESSAGES.NON_NUMERIC_AMOUNT);
}
if (!amount) {
throw new Error(ERROR_MESSAGES.EMPTY_LOTTO_PRICE);
}
if (amount % LOTTO_PRICE !== 0) {
throw new Error(ERROR_MESSAGES.INVALID_LOTTO_PRICE);
}
}

async pickLottoNumbers() {
const lottoNumbers = await Random.pickUniqueNumbersInRange(1, 45, 6);
return lottoNumbers;
}

async buyLotto(lottoCount) {
let lottos = [];
for (let i = 0; i < lottoCount; i++) {
const lottoNumbers = await this.pickLottoNumbers();
const sortedLotto = lottoNumbers.sort((a, b) => a - b);

const lotto = new Lotto(sortedLotto);

lottos.push(lotto);
}
return lottos;
}

getLottoMatchResults(lottos, winningLotto, bonusNumber) {
const winningLottoNumbers = winningLotto.getLottoNumbers();
const winningNumberSet = new Set(winningLottoNumbers);

let result = [];

for (let i = 0; i < lottos.length; i++) {
const lottoNumbers = lottos[i].getLottoNumbers(); // [1,2,3,8,15,43]
let matchCount = 0;
let bonusMatchCount = 0;

for (let j = 0; j < lottoNumbers.length; j++) {
if (winningNumberSet.has(lottoNumbers[j])) {
matchCount += 1;
}

if (bonusNumber === lottoNumbers[j]) {
bonusMatchCount += 1;
}
}

result.push({ matchCount: matchCount, bonusMatchCount: bonusMatchCount });
}

return result;
}

prizeCountCalculate() {
const prizeCount = {
firstPrizeCount: 0,
secondPrizeCount: 0,
thirdPrizeCount: 0,
fourthPrizeCount: 0,
fifthPrizeCount: 0,
};

this.#result.forEach(({ matchCount, bonusMatchCount }) => {
if (matchCount + bonusMatchCount === 3) {
prizeCount.fifthPrizeCount += 1;
}

if (matchCount + bonusMatchCount === 4) {
prizeCount.fourthPrizeCount += 1;
}

if (matchCount + bonusMatchCount === 5) {
prizeCount.thirdPrizeCount += 1;
}

if (matchCount === 5 && bonusMatchCount === 1) {
prizeCount.secondPrizeCount += 1;
}

if (matchCount === 6) {
prizeCount.firstPrizeCount += 1;
}
});
return prizeCount;
}

calculateProfitRate(amount, prizeCount) {
const {
firstPrizeCount,
secondPrizeCount,
thirdPrizeCount,
fourthPrizeCount,
fifthPrizeCount,
} = prizeCount;

const prizeMoney =
firstPrizeCount * FIRST_PRIZE_MONEY +
secondPrizeCount * SECOND_PRIZE_MONEY +
thirdPrizeCount * THIRD_PRIZE_MONEY +
fourthPrizeCount * FOURTH_PRIZE_MONEY +
fifthPrizeCount * FIFTH_PRIZE_MONEY;

const profitRate = (prizeMoney / amount) * 100;

if (!Number.isInteger(profitRate)) {
return profitRate.toFixed(1);
}

return profitRate;
}
}

export default App;
41 changes: 41 additions & 0 deletions src/Controller/Controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DELIMITER, LOTTO_PRICE } from '../constants/constants.js';
import { ERROR_MESSAGES } from '../constants/messages.js';
import View from '../View/View.js';

class Controller {
async #validateAmount(amount) {
if (amount % LOTTO_PRICE !== 0) {
throw new Error(ERROR_MESSAGES.INVALID_LOTTO_PRICE);
}
}

async #validateWinningNumbers(numbers) {
const splittedNumbers = numbers.split(DELIMITER);

if (!splittedNumbers) {
throw new Error(ERROR_MESSAGES.EMPTY_LOTTO_NUMBERS);
}

if (!numbers.includes(DELIMITER)) {
throw new Error(ERROR_MESSAGES.INVALID_LOTTO_NUMBERS);
}

if (splittedNumbers.length !== 6) {
throw new Error(ERROR_MESSAGES.INVALID_LOTTO_COUNT);
}
}

async execute() {
const view = new View();

const amount = await view.getAmount();
await this.#validateAmount(amount);

const winningNumbers = await view.getWinningNumbers();
await this.#validateWinningNumbers(winningNumbers);

await view.getBonusNumber();
}
}

export default Controller;
18 changes: 0 additions & 18 deletions src/Lotto.js

This file was deleted.

Loading