diff --git a/README.md b/README.md index 15bb106b5..bebd76c83 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ # javascript-lotto-precourse + +## 프로젝트 개요 +이 프로젝트는 간단한 로또 발매기를 구현하는 것을 목표로 합니다. +사용자는 로또 구입 금액을 입력하고, 로또 번호를 발행받은 후 당첨 번호와 비교하여 당첨 결과를 확인할 수 있습니다. +프로젝트의 주요 기능은 사용자의 입력을 받아 로또를 발행하고, 당첨 결과를 계산하여 수익률을 계산하는 것입니다. + +## 기능 목록 + +- [x] **로또 구입 금액 입력 및 검증** + - 로또 구입 금액을 입력받는다. + - 1,000원 단위로 나누어 떨어지는지 확인하여 유효성을 검증한다. + - **예외**: 1,000원 단위로 나누어 떨어지지 않는 경우, `[ERROR]` 메시지 출력 후 재입력 요청. + +- [x] **로또 발행** + - 입력받은 금액을 바탕으로 로또 발행 개수를 계산한다. (로또 1장당 1,000원) + - 발행한 로또 번호는 1~45 사이의 중복되지 않는 6개의 숫자로 구성된다. + +- [x] **로또 번호 출력** + - 발행한 모든 로또 번호를 오름차순으로 정렬하여 출력한다. + - 출력 형식: `N개를 구매했습니다.` 및 각 로또 번호를 배열 형식으로 출력한다. + +- [ ] **당첨 번호 및 보너스 번호 입력 및 검증** + - 당첨 번호 6개를 입력받는다. 번호는 쉼표(,)로 구분한다. + - 보너스 번호 1개를 입력받는다. + - 당첨 번호와 보너스 번호는 1 ~ 45 사이의 중복되지 않는 숫자여야 한다. + - **예외**: 입력 형식이 올바르지 않거나 중복된 번호, 범위를 벗어난 경우, `[ERROR]` 메시지 출력 후 재입력 요청. + +- [ ] **당첨 결과 계산** + - 구매한 로또 번호와 당첨 번호를 비교하여 당첨 결과를 계산한다. + - 당첨 등수는 1등부터 5등까지 있으며, 각 등수는 아래 조건에 따른다: + - 1등: 6개 번호 일치 + - 2등: 5개 번호 + 보너스 번호 일치 + - 3등: 5개 번호 일치 + - 4등: 4개 번호 일치 + - 5등: 3개 번호 일치 + +- [ ] **당첨 통계 및 수익률 출력** + - 당첨 결과를 등수별로 정리하여 출력한다. + - 총 수익률을 계산하고, 소수점 둘째 자리에서 반올림하여 출력한다. + - 출력 형식: + - `3개 일치 (5,000원) - 1개` + - `총 수익률은 62.5%입니다.` + +- [ ] **에러 처리** + - 모든 사용자 입력 단계에서 잘못된 값이 들어왔을 경우, `[ERROR]`로 시작하는 메시지를 출력한다. + - 에러 발생 시 해당 단계부터 다시 입력을 받는다. + +## 테스트 목록 +TDD를 적용하여 기능별 테스트 케이스를 작성합니다. 다음은 각 기능에 대해 예상되는 테스트 목록입니다. + +- [x] **로또 구입 금액 입력 및 검증 테스트** + - 올바른 금액을 입력했을 때 정상적으로 처리되는지 확인한다. + - 1,000원 단위로 나누어 떨어지지 않는 금액을 입력했을 때 에러 메시지가 출력되는지 확인한다. + +- [x] **로또 발행 테스트** + - 입력한 금액에 따라 올바른 개수의 로또가 발행되는지 확인한다. + - 각 로또 번호가 1 ~ 45 사이의 중복되지 않는 6개의 숫자로 구성되어 있는지 확인한다. + +- [x] **로또 번호 출력 테스트** + - 발행한 로또 번호가 오름차순으로 출력되는지 확인한다. + +- [ ] **당첨 번호 및 보너스 번호 입력 및 검증 테스트** + - 올바른 형식의 당첨 번호와 보너스 번호를 입력했을 때 정상적으로 처리되는지 확인한다. + - 중복되거나 범위를 벗어난 번호를 입력했을 때 에러 메시지가 출력되는지 확인한다. + +- [ ] **당첨 결과 계산 테스트** + - 구매한 로또 번호와 당첨 번호를 비교하여 각 등수별로 당첨 결과가 정확하게 계산되는지 확인한다. + - 보너스 번호를 포함한 2등 당첨이 올바르게 계산되는지 확인한다. + +- [ ] **당첨 통계 및 수익률 출력 테스트** + - 당첨 결과가 등수별로 올바르게 출력되는지 확인한다. + - 총 수익률이 올바르게 계산되어 출력되는지 확인한다. + +- [ ] **에러 처리 테스트** + - 각 단계에서 잘못된 입력이 주어졌을 때 올바르게 에러 메시지가 출력되고, 재입력을 받는지 확인한다. + diff --git a/__tests__/bonus-number-test.js b/__tests__/bonus-number-test.js new file mode 100644 index 000000000..c735b2a09 --- /dev/null +++ b/__tests__/bonus-number-test.js @@ -0,0 +1,26 @@ +import validateBonusNumber from '../src/validations/bouns-number.js'; +import { ERROR_MESSAGES } from '../src/constants/constants.js'; +const { INVALID_NUMBER_INPUT, INVALID_BONUS_NUMBER, INVALID_LOTTO_NUMBER } = + ERROR_MESSAGES; + +describe('보너스 번호 입력 테스트', () => { + const testArray = [1, 2, 3, 4, 5, 6]; + + test('숫자가 아닌 값을 입력했을 때 에러 메시지가 발생하는지 확인', () => { + expect(() => validateBonusNumber('a', testArray)).toThrow( + INVALID_NUMBER_INPUT, + ); + }); + + test('중복된 번호를 입력했을 때 에러 메시지가 발생하는지 확인', () => { + expect(() => validateBonusNumber(3, testArray)).toThrow( + INVALID_BONUS_NUMBER, + ); + }); + + test('범위를 벗어난 번호를 입력했을 때 에러 메시지가 발생하는지 확인', () => { + expect(() => validateBonusNumber(46, testArray)).toThrow( + INVALID_LOTTO_NUMBER, + ); + }); +}); diff --git a/__tests__/generate-lotto-test.js b/__tests__/generate-lotto-test.js new file mode 100644 index 000000000..93d624832 --- /dev/null +++ b/__tests__/generate-lotto-test.js @@ -0,0 +1,34 @@ +import generateLottoList from '../src/utils/generate-lotto.js'; +import { LOTTO_TICKET_PRICE } from '../src/constants/constants.js'; + +describe('로또 발행 테스트', () => { + const amount = 5000; + const lottos = generateLottoList(amount); + + test('입력한 금액에 따라 올바른 개수의 로또가 발행되는지 확인한다', () => { + const expectedLottoCount = amount / LOTTO_TICKET_PRICE; + + expect(lottos.length).toBe(expectedLottoCount); + }); + + test('각 로또 번호가 1 ~ 45 사이의 중복되지 않는 6개의 숫자로 구성되어 있는지 확인한다', () => { + lottos.forEach((lotto) => { + const numbers = lotto.getNumbers(); + + expect(numbers.length).toBe(6); + expect(new Set(numbers).size).toBe(6); + numbers.forEach((number) => { + expect(number).toBeGreaterThanOrEqual(1); + expect(number).toBeLessThanOrEqual(45); + }); + }); + }); + + test('각 로또 번호가 오름차순으로 정렬되어 있는지 확인한다', () => { + lottos.forEach((lotto) => { + const numbers = lotto.getNumbers(); + const sortedNumbers = [...numbers].sort((a, b) => a - b); + expect(numbers).toEqual(sortedNumbers); + }); + }); +}); diff --git a/__tests__/purchase-amount-test.js b/__tests__/purchase-amount-test.js new file mode 100644 index 000000000..ae7d853d4 --- /dev/null +++ b/__tests__/purchase-amount-test.js @@ -0,0 +1,40 @@ +import validateAmount from '../src/validations/purchase-amount.js'; +import { ERROR_MESSAGES } from '../src/constants/constants.js'; + +const { + INVALID_EMPTY_INPUT, + INVALID_PURCHASE_AMOUNT_NOT_NUMBER, + INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT, + INVALID_PURCHASE_AMOUNT_LIMIT, +} = ERROR_MESSAGES; + +describe('로또 구입 금액 입력 테스트', () => { + test.each([1000, 8000, 14000])( + '올바른 금액 %i을 입력시 기능 테스트', + (amount) => { + const result = validateAmount(amount); + expect(result).toBe(amount); + }, + ); + + test.each([ + { input: '', expectedError: INVALID_EMPTY_INPUT }, + { input: '100j', expectedError: INVALID_PURCHASE_AMOUNT_NOT_NUMBER }, + { input: 'a', expectedError: INVALID_PURCHASE_AMOUNT_NOT_NUMBER }, + { input: '@#!', expectedError: INVALID_PURCHASE_AMOUNT_NOT_NUMBER }, + { input: '4500', expectedError: INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT }, + { input: '400', expectedError: INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT }, + ])( + '잘못된 입력 값 %s을 입력시 상응하는 에러 메시지 %s 발생', + ({ input, expectedError }) => { + expect(() => validateAmount(input)).toThrow(expectedError); + }, + ); + + test('구입 금액이 최대 금액을 초과할 경우 예외가 발생한다', () => { + const invalidAmount = 150000; // 최대 금액을 초과하는 금액 + expect(() => validateAmount(invalidAmount)).toThrow( + INVALID_PURCHASE_AMOUNT_LIMIT, + ); + }); +}); diff --git a/__tests__/winning-numbers-test.js b/__tests__/winning-numbers-test.js new file mode 100644 index 000000000..ec2dc1df3 --- /dev/null +++ b/__tests__/winning-numbers-test.js @@ -0,0 +1,53 @@ +import validateWinningNumbers from '../src/validations/winning-numbers.js'; +import { ERROR_MESSAGES } from '../src/constants/constants.js'; + +const { INVALID_FORMAT, INVALID_LOTTO_NUMBER, INVALID_DUPLICATE_NUMBERS } = + ERROR_MESSAGES; + +describe('당첨 번호 입력 테스트', () => { + test.each([ + [ + '올바른 형식의 당첨 번호를 입력할 때 정상적으로 처리되는지 확인', + '1,2,3,4,5,6', + [1, 2, 3, 4, 5, 6], + ], + ])('%s', (_, input, expected) => { + const result = validateWinningNumbers(input); + expect(result).toEqual(expected); + }); + + test.each([ + [ + '숫자 형식이 아닌 경우 에러 메시지가 발생하는지 확인', + '1,2,a,4,5,6', + INVALID_FORMAT, + ], + [ + '구분자가 , 가 아닌 경우 에러 메시지가 발생하는지 확인', + '1/2/3/4/5/6', + INVALID_FORMAT, + ], + [ + '6개의 숫자가 아닌 경우 에러 메시지가 발생하는지 확인', + '1,2,3,4,5', + INVALID_FORMAT, + ], + [ + '중복된 번호가 있는 경우 에러 메시지가 발생하는지 확인', + '1,2,3,4,5,5', + INVALID_DUPLICATE_NUMBERS, + ], + [ + '범위를 벗어난 번호가 있는 경우 에러 메시지가 발생하는지 확인 (0 포함)', + '0,2,3,4,5,6', + INVALID_LOTTO_NUMBER, + ], + [ + '범위를 벗어난 번호가 있는 경우 에러 메시지가 발생하는지 확인 (46 포함)', + '1,2,3,4,5,46', + INVALID_LOTTO_NUMBER, + ], + ])('%s', (_, input, expectedError) => { + expect(() => validateWinningNumbers(input)).toThrow(expectedError); + }); +}); diff --git a/src/App.js b/src/App.js index 091aa0a5d..089aa97fe 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,15 @@ +import { printMessage, printLottoList } from './utils/console.js'; +import getPurchaseAmount from './utils/get-parchase-amount.js'; +import generateLottoList from './utils/generate-lotto.js'; + class App { - async run() {} + async run() { + const purchaseAmount = await getPurchaseAmount(); + + const lottos = generateLottoList(purchaseAmount); + + printLottoList(lottos); + } } export default App; diff --git a/src/Lotto.js b/src/Lotto.js index cb0b1527e..2b21c37f2 100644 --- a/src/Lotto.js +++ b/src/Lotto.js @@ -1,3 +1,8 @@ +import { ERROR_MESSAGES } from './constants/constants.js'; + +const { INVALID_DUPLICATE_NUMBERS, INVALID_LOTTO_NUMBER_COUNT } = + ERROR_MESSAGES; + class Lotto { #numbers; @@ -8,11 +13,24 @@ class Lotto { #validate(numbers) { if (numbers.length !== 6) { - throw new Error("[ERROR] 로또 번호는 6개여야 합니다."); + throw new Error(INVALID_LOTTO_NUMBER_COUNT); } + + this.#checkUniqueNumbers(numbers); } // TODO: 추가 기능 구현 + #checkUniqueNumbers(numbers) { + const uniqueNumbers = new Set(numbers); + + if (uniqueNumbers.size !== numbers.length) { + throw new Error(INVALID_DUPLICATE_NUMBERS); + } + } + + getNumbers() { + return this.#numbers; + } } export default Lotto; diff --git a/src/constants/constants.js b/src/constants/constants.js new file mode 100644 index 000000000..361521b2b --- /dev/null +++ b/src/constants/constants.js @@ -0,0 +1,59 @@ +const LOTTO_TICKET_PRICE = 1000; +const MAX_PURCHASE_AMOUNT = 100000; + +const LOTTO_NUMBER_RANGE = { + MIN_NUMBER: 1, + MAX_NUMBER: 45, +}; + +const USER_PROMPT_MESSAGES = Object.freeze({ + GET_PURCHASE_AMOUNT: '구입금액을 입력해 주세요.\n', + GET_WINNING_NUMBER: '\n당첨 번호를 입력해 주세요.\n', + GET_BONUS_NUMBER: '\n보너스 번호를 입력해 주세요.\n', +}); + +const CONFIRMATION_MESSAGES = Object.freeze({ + PURCHASE_COMPLETE_MESSAGE: (count) => `${count}개를 구매했습니다.`, + WINNING_STATISTICS_HEADER: '당첨 통계\n---', + TOTAL_YIELD_MESSAGE: (rate) => `총 수익률은 ${rate}%입니다.`, +}); + +const ERROR_MESSAGES = Object.freeze({ + INVALID_EMPTY_INPUT: + '[ERROR] 아무것도 입력하지 않았습니다. 값을 입력해 주세요.', + INVALID_PURCHASE_AMOUNT_LIMIT: + '[ERROR] 구입 금액은 최대 100,000원까지 가능합니다.', + INVALID_PURCHASE_AMOUNT_NOT_NUMBER: + '[ERROR] 구입 금액은 숫자만 입력해 주세요.', + INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT: + '[ERROR] 구입 금액은 1,000원 단위로 입력해 주세요.', + INVALID_LOTTO_NUMBER_COUNT: '[ERROR] 로또 번호는 6개여야 합니다.', + INVALID_DUPLICATE_NUMBERS: '[ERROR] 로또 번호는 중복되지 않아야 합니다.', + INVALID_FORMAT: + '[ERROR] 당첨 번호는 콤마(,)로 구분된 숫자 6개여야 합니다. (예: 1,2,3,4,5,6)', + INVALID_LOTTO_NUMBER: '[ERROR] 로또 번호는 1~45 사이의 숫자여야 합니다.', + INVALID_BONUS_NUMBER: + '[ERROR] 보너스 번호는 당첨 번호와 중복되지 않아야 합니다.', + INVALID_NUMBER_INPUT: '[ERROR] 숫자만 입력해 주세요', +}); + +const PRIZE_AMOUNTS = Object.freeze({ + FIRST_PRIZE: 2000000000, + SECOND_PRIZE: 30000000, + THIRD_PRIZE: 1500000, + FOURTH_PRIZE: 50000, + FIFTH_PRIZE: 5000, +}); + +const WINNING_NUMBER_FORMAT_REGEX = /^(\d{1,2})(,\d{1,2}){5}$/; + +export { + LOTTO_TICKET_PRICE, + USER_PROMPT_MESSAGES, + CONFIRMATION_MESSAGES, + ERROR_MESSAGES, + PRIZE_AMOUNTS, + MAX_PURCHASE_AMOUNT, + LOTTO_NUMBER_RANGE, + WINNING_NUMBER_FORMAT_REGEX, +}; diff --git a/src/utils/console.js b/src/utils/console.js new file mode 100644 index 000000000..cec089646 --- /dev/null +++ b/src/utils/console.js @@ -0,0 +1,25 @@ +import { Console } from '@woowacourse/mission-utils'; +import { CONFIRMATION_MESSAGES } from '../constants/constants.js'; + +const { PURCHASE_COMPLETE_MESSAGE } = CONFIRMATION_MESSAGES; + +const printMessage = (message) => { + Console.print(message); +}; + +const promptUserInput = async (message) => { + return await Console.readLineAsync(message); +}; + +const printLotto = (lotto) => { + Console.print(`[${lotto.getNumbers().join(', ')}]`); +}; + +const printLottoList = (lottos) => { + printMessage(`\n${PURCHASE_COMPLETE_MESSAGE(lottos.length)}`); + lottos.forEach((lotto) => { + printLotto(lotto); + }); +}; + +export { printMessage, promptUserInput, printLottoList }; diff --git a/src/utils/generate-lotto.js b/src/utils/generate-lotto.js new file mode 100644 index 000000000..f8dd75212 --- /dev/null +++ b/src/utils/generate-lotto.js @@ -0,0 +1,30 @@ +import { Random } from '@woowacourse/mission-utils'; +import { LOTTO_TICKET_PRICE } from '../constants/constants.js'; +import sortNumbersAscending from './sort-numbers.js'; +import Lotto from '../Lotto.js'; + +const calculateLottoCount = (amount) => amount / LOTTO_TICKET_PRICE; + +const generateUniqueLottoNumbers = () => { + return Random.pickUniqueNumbersInRange(1, 45, 6); +}; + +const generateLotto = () => { + const lottoNumbers = generateUniqueLottoNumbers(); + const sortedLottoNumbers = sortNumbersAscending(lottoNumbers); + + return new Lotto(sortedLottoNumbers); +}; + +const generateLottoList = (amount) => { + const lottoCount = calculateLottoCount(amount); + const lottos = []; + + for (let i = 0; i < lottoCount; i++) { + lottos.push(generateLotto()); + } + + return lottos; +}; + +export default generateLottoList; diff --git a/src/utils/get-bouns-number.js b/src/utils/get-bouns-number.js new file mode 100644 index 000000000..0058797d9 --- /dev/null +++ b/src/utils/get-bouns-number.js @@ -0,0 +1,19 @@ +import { promptUserInput, printMessage } from './console.js'; +import { USER_PROMPT_MESSAGES } from '../constants/constants.js'; +import validateBonusNumber from '../validations/bouns-number.js'; +const { GET_BONUS_NUMBER } = USER_PROMPT_MESSAGES; + +const getBonusNumber = async (winningNumbers) => { + while (true) { + try { + const bonusNumber = await promptUserInput(GET_BONUS_NUMBER); + validateBonusNumber(bonusNumber, winningNumbers); + + return Number(bonusNumber); + } catch (error) { + printMessage(error.message); + } + } +}; + +export default getBonusNumber; diff --git a/src/utils/get-parchase-amount.js b/src/utils/get-parchase-amount.js new file mode 100644 index 000000000..aab5c31b2 --- /dev/null +++ b/src/utils/get-parchase-amount.js @@ -0,0 +1,25 @@ +import { USER_PROMPT_MESSAGES } from '../constants/constants.js'; +import { promptUserInput, printMessage } from './console.js'; +import validateAmount from '../validations/purchase-amount.js'; + +const { GET_PURCHASE_AMOUNT } = USER_PROMPT_MESSAGES; + +const promptUserAmount = () => { + return promptUserInput(GET_PURCHASE_AMOUNT); +}; + +const getPurchaseAmount = async () => { + while (true) { + try { + const amount = await promptUserAmount(); + + const validatedAmount = validateAmount(amount); + + return validatedAmount; + } catch (error) { + printMessage(error.message); + } + } +}; + +export default getPurchaseAmount; diff --git a/src/utils/get-winning-numbers.js b/src/utils/get-winning-numbers.js new file mode 100644 index 000000000..56591c5f6 --- /dev/null +++ b/src/utils/get-winning-numbers.js @@ -0,0 +1,25 @@ +import { promptUserInput, printMessage } from './console.js'; +import { USER_PROMPT_MESSAGES } from '../constants/constants.js'; +import validateWinningNumbers from '../validations/winning-numbers.js'; +import sortNumbersAscending from './sort-numbers.js'; + +const { GET_WINNING_NUMBER } = USER_PROMPT_MESSAGES; + +const getWinningNumbers = async () => { + while (true) { + try { + const winningNumbers = await promptUserInput(GET_WINNING_NUMBER); + + const validatedWinningNumbers = validateWinningNumbers(winningNumbers); + const sortedWinningNumbers = sortNumbersAscending( + validatedWinningNumbers, + ); + + return sortedWinningNumbers; + } catch (error) { + printMessage(error.message); + } + } +}; + +export default getWinningNumbers; diff --git a/src/utils/sort-numbers.js b/src/utils/sort-numbers.js new file mode 100644 index 000000000..8113960a6 --- /dev/null +++ b/src/utils/sort-numbers.js @@ -0,0 +1,4 @@ +const sortNumbersAscending = (lottoNumbers) => + lottoNumbers.sort((a, b) => a - b); + +export default sortNumbersAscending; diff --git a/src/validations/bouns-number.js b/src/validations/bouns-number.js new file mode 100644 index 000000000..3e2443235 --- /dev/null +++ b/src/validations/bouns-number.js @@ -0,0 +1,39 @@ +import { ERROR_MESSAGES, LOTTO_NUMBER_RANGE } from '../constants/constants.js'; + +const { INVALID_BONUS_NUMBER, INVALID_LOTTO_NUMBER, INVALID_NUMBER_INPUT } = + ERROR_MESSAGES; +const { MIN_NUMBER, MAX_NUMBER } = LOTTO_NUMBER_RANGE; + +const isValidRange = (number) => number >= MIN_NUMBER && number <= MAX_NUMBER; + +const validateNumberRange = (number) => { + if (!isValidRange(number)) { + throw new Error(INVALID_LOTTO_NUMBER); + } + return number; +}; + +const validateNumber = (input) => { + const parsedNumber = Number(input); + if (Number.isNaN(parsedNumber)) { + throw new Error(INVALID_NUMBER_INPUT); + } + return parsedNumber; +}; + +const validateDuplicateBonusNumber = (bonusNumber, winningNumbers) => { + if (winningNumbers.includes(bonusNumber)) { + throw new Error(INVALID_BONUS_NUMBER); + } +}; + +const validateBonusNumber = (input, winningNumbers) => { + const bonusNumber = validateNumber(input); + + validateDuplicateBonusNumber(bonusNumber, winningNumbers); + validateNumberRange(bonusNumber); + + return bonusNumber; +}; + +export default validateBonusNumber; diff --git a/src/validations/common.js b/src/validations/common.js new file mode 100644 index 000000000..70b4a796b --- /dev/null +++ b/src/validations/common.js @@ -0,0 +1,15 @@ +import { ERROR_MESSAGES } from '../constants/constants'; + +const { INVALID_EMPTY_INPUT } = ERROR_MESSAGES; + +const trimInputAndCheckEmpty = (input) => { + const trimmedInput = input.trim(); + + if (trimmedInput === '') { + throw new Error(INVALID_EMPTY_INPUT); + } + + return trimmedInput; +}; + +export { trimInputAndCheckEmpty }; diff --git a/src/validations/purchase-amount.js b/src/validations/purchase-amount.js new file mode 100644 index 000000000..89d6ebcea --- /dev/null +++ b/src/validations/purchase-amount.js @@ -0,0 +1,43 @@ +import { trimInputAndCheckEmpty } from './common.js'; +import { + ERROR_MESSAGES, + LOTTO_TICKET_PRICE, + MAX_PURCHASE_AMOUNT, +} from '../constants/constants.js'; + +const { + INVALID_PURCHASE_AMOUNT_NOT_NUMBER, + INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT, + INVALID_PURCHASE_AMOUNT_LIMIT, +} = ERROR_MESSAGES; + +const isPromptAmountNumber = (amount) => { + if (Number.isNaN(Number(amount))) { + throw new Error(INVALID_PURCHASE_AMOUNT_NOT_NUMBER); + } + return Number(amount); +}; + +const isAmountWithinLimit = (amount) => { + if (amount > MAX_PURCHASE_AMOUNT) { + throw new Error(INVALID_PURCHASE_AMOUNT_LIMIT); + } + return amount; +}; + +const isValidTicketUnit = (amount) => { + if (amount % LOTTO_TICKET_PRICE !== 0) { + throw new Error(INVALID_PURCHASE_AMOUNT_NOT_TICKET_UNIT); + } + return amount; +}; + +const validateAmount = (amount) => { + const amountAsString = String(amount); + const trimmedAmount = trimInputAndCheckEmpty(amountAsString); + const numericAmount = isPromptAmountNumber(trimmedAmount); + const result = isAmountWithinLimit(numericAmount); + return isValidTicketUnit(result); +}; + +export default validateAmount; diff --git a/src/validations/winning-numbers.js b/src/validations/winning-numbers.js new file mode 100644 index 000000000..03bc9f05f --- /dev/null +++ b/src/validations/winning-numbers.js @@ -0,0 +1,44 @@ +import { + ERROR_MESSAGES, + LOTTO_NUMBER_RANGE, + WINNING_NUMBER_FORMAT_REGEX, +} from '../constants/constants.js'; + +const { INVALID_LOTTO_NUMBER, INVALID_DUPLICATE_NUMBERS, INVALID_FORMAT } = + ERROR_MESSAGES; +const { MIN_NUMBER, MAX_NUMBER } = LOTTO_NUMBER_RANGE; + +const isValidFormat = (numbers) => { + if (!WINNING_NUMBER_FORMAT_REGEX.test(numbers)) { + throw new Error(INVALID_FORMAT); + } +}; + +const isValidRange = (number) => number >= MIN_NUMBER && number <= MAX_NUMBER; + +const validateUniqueNumbers = (numbers) => { + if (new Set(numbers).size !== numbers.length) { + throw new Error(INVALID_DUPLICATE_NUMBERS); + } +}; + +const validateNumberRange = (numbers) => { + numbers.forEach((number) => { + if (!isValidRange(number)) { + throw new Error(INVALID_LOTTO_NUMBER); + } + }); +}; + +const validateWinningNumbers = (input) => { + isValidFormat(input); + + const numberArray = input.split(',').map(Number); + + validateUniqueNumbers(numberArray); + validateNumberRange(numberArray); + + return numberArray; +}; + +export default validateWinningNumbers;