Skip to content
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,28 @@
# javascript-planetlotto-precourse

# 행성 로또 기능 목록

## 입력

- [x] 구입 금액 입력 받기 (숫자 검증)
- [x] 당첨 번호 입력 받기 (쉼표 구분 / 5개 / 중복 없음 / 1~30)
- [x] 보너스 번호 입력 받기 (1~30 / 당첨 번호와 중복 없음)
- [x] 입력 오류 시 `[ERROR]` 출력 후 해당 입력부터 재시도

## 로또 발행

- [x] 구입 금액 / 500 만큼 로또 발행
- [x] `Random.pickUniqueNumbersInRange(1, 30, 5)` 사용
- [x] 각 로또 번호 오름차순 정렬 출력

## 당첨 계산

- [x] 구매 로또와 당첨 번호 비교하여 등수별 카운트 집계
- [x] 1~5등 기준과 상금 적용

## 출력

- [x] `n개를 구매했습니다.` 출력
- [x] 각 로또 `[x, x, x, x, x]` 형태 출력
- [x] 당첨 통계 헤더 및 구분선 출력
- [x] 등수별 결과 라인 출력
33 changes: 32 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import { InputView, OutputView } from "./view.js";
import { PlanetLottoGame } from "./domain/PlanetLottoGame.js";
import { retryUntilSuccess } from "./utils/retry.js";
class App {
async run() {}
async run() {
const game = new PlanetLottoGame();

const purchaseAmount = await retryUntilSuccess(
() => InputView.askAmount(),
(error) => OutputView.printErrorMessage(error.message)
);

const purchasedLottos = game.purchaseLottos(purchaseAmount);
OutputView.printPurchasedLottos(purchasedLottos);

const winningNumbers = await retryUntilSuccess(
() => InputView.askWinningLotto(),
(error) => OutputView.printErrorMessage(error.message)
);

const bonusNumber = await retryUntilSuccess(
() => InputView.askBonusNumber(),
(error) => OutputView.printErrorMessage(error.message)
);

const countsByRank = game.calculateCountsByRank(
purchasedLottos,
winningNumbers,
bonusNumber
);

OutputView.printResult(countsByRank);
}
}

export default App;
16 changes: 16 additions & 0 deletions src/constants/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const LOTTO_RULE = Object.freeze({
MIN: 1,
MAX: 30,
COUNT: 5,
});

export const PRICE = 500;

export const RANK = Object.freeze({
NONE: 0,
FIRST: 1,
SECOND: 2,
THIRD: 3,
FOURTH: 4,
FIFTH: 5,
});
38 changes: 38 additions & 0 deletions src/domain/PlanetLotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { LOTTO_RULE } from "../constants/config.js";

class PlanetLotto {
#numbers;

constructor(numbers) {
this.#validate(numbers);
this.#numbers = [...numbers].sort((first, second) => first - second);
}

#validate(numbers) {
if (!Array.isArray(numbers)) {
throw new Error("당첨 번호는 숫자여야 합니다.");
}
if (numbers.length !== LOTTO_RULE.COUNT) {
throw new Error("로또 번호는 5개여야 합니다.");
}
if (numbers.some((number) => !Number.isInteger(number))) {
throw new Error("당첨 번호는 숫자여야 합니다.");
}
if (
numbers.some(
(number) => number < LOTTO_RULE.MIN || number > LOTTO_RULE.MAX
)
) {
throw new Error("로또 번호는 1부터 30 사이의 숫자여야 합니다.");
}
if (new Set(numbers).size !== numbers.length) {
throw new Error("로또 번호에 중복이 있습니다.");
}
}

getNumbers() {
return [...this.#numbers];
}
}

export default PlanetLotto;
65 changes: 65 additions & 0 deletions src/domain/PlanetLottoGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { MissionUtils } from "@woowacourse/mission-utils";
import { LOTTO_RULE, RANK } from "../constants/config.js";
import PlanetLotto from "./PlanetLotto.js";
import {
validatePurchaseAmount,
validateBonusNumber,
} from "../utils/validator.js";

export class PlanetLottoGame {
purchaseLottos(amount) {
validatePurchaseAmount(amount);
const purchaseCount = amount / 500;

return Array.from({ length: purchaseCount }, () => this.#issueOneLotto());
}

#issueOneLotto() {
const numbers = MissionUtils.Random.pickUniqueNumbersInRange(
LOTTO_RULE.MIN,
LOTTO_RULE.MAX,
LOTTO_RULE.COUNT
);
return new PlanetLotto(numbers).getNumbers();
}

calculateCountsByRank(purchasedLottos, winningNumbers, bonusNumber) {
const winningLotto = new PlanetLotto(winningNumbers);
validateBonusNumber(bonusNumber, winningLotto.getNumbers());

const countsByRank = this.#createCountsMap();
purchasedLottos.forEach((lottoNumbers) => {
const rank = this.#determineRank(
lottoNumbers,
winningLotto.getNumbers(),
bonusNumber
);
countsByRank.set(rank, (countsByRank.get(rank) ?? 0) + 1);
});

return countsByRank;
}

#createCountsMap() {
const map = new Map();
Object.values(RANK).forEach((rank) => map.set(rank, 0));
return map;
}

#determineRank(lottoNumbers, winningNumbers, bonusNumber) {
const matchCount = this.#countMatches(lottoNumbers, winningNumbers);
const hasBonus = lottoNumbers.includes(bonusNumber);

if (matchCount === 5) return RANK.FIRST;
if (matchCount === 4 && hasBonus) return RANK.SECOND;
if (matchCount === 4) return RANK.THIRD;
if (matchCount === 3 && hasBonus) return RANK.FOURTH;
if (matchCount === 2 && hasBonus) return RANK.FIFTH;
return RANK.NONE;
}

#countMatches(lottoNumbers, winningNumbers) {
const winningSet = new Set(winningNumbers);
return lottoNumbers.filter((number) => winningSet.has(number)).length;
}
}
9 changes: 9 additions & 0 deletions src/utils/retry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const retryUntilSuccess = async (action, onError) => {
while (true) {
try {
return await action();
} catch (error) {
onError(error);
}
}
};
25 changes: 25 additions & 0 deletions src/utils/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { LOTTO_RULE, PRICE } from "../constants/config.js";

export const validatePurchaseAmount = (amount) => {
if (!Number.isInteger(amount)) {
throw new Error("구매금액은 숫자여야 합니다.");
}
if (amount <= 0) {
throw new Error("구매금액은 0보다 커야 합니다.");
}
if (amount % PRICE !== 0) {
throw new Error("구매금액은 500원 단위여야 합니다.");
}
};

export const validateBonusNumber = (bonusNumber, winningNumbers) => {
if (!Number.isInteger(bonusNumber)) {
throw new Error("보너스 번호는 숫자여야 합니다.");
}
if (bonusNumber < LOTTO_RULE.MIN || bonusNumber > LOTTO_RULE.MAX) {
throw new Error("로또 번호는 1부터 30 사이의 숫자여야 합니다.");
}
if (winningNumbers.includes(bonusNumber)) {
throw new Error("보너스 번호는 당첨 번호와 중복될 수 없습니다.");
}
};
10 changes: 10 additions & 0 deletions types/woowacourse__mission-utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare module "@woowacourse/mission-utils" {
export const Console: {
readLineAsync(prompt?: string): Promise<string>;
print(message: string): void;
};

export const Random: {
pickNumberInRange(min: number, max: number): number;
};
}