diff --git a/.cspell-ignore b/.cspell-ignore index 6ab6c60..a955251 100644 --- a/.cspell-ignore +++ b/.cspell-ignore @@ -279,6 +279,10 @@ webp регулярку ресайзе Релизнуть +Рантайм +рантайм +рантайме +Рантайме релизятся рендера рендере diff --git a/README.md b/README.md index 6304544..b9f298f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Table of contents - [declensionYear](#declensionyear) - [phone](#phone) - [formatPhoneToView](#formatphonetoview) - +- [number](#number) + - [formatNumberToCurrency](#formatNumberToCurrency) --- # Installation @@ -245,3 +246,39 @@ formatPhoneToView(); ``` --- + +# number + +Утилиты и функции для работы с числами + +## formatNumberToCurrency + +Форматирование чисел или строк в формат валюты + +```ts +import { formatNumberToCurrency } from '@astral/utils'; + +// "10 000 ₽" +formatNumberToCurrency({ amount: '10000' }); + +// "100 ₽" +formatNumberToCurrency({ amount: 100 }); + +// "0 ₽" +formatNumberToCurrency({ amount: 0 }); + +// "Бесплатно" +formatNumberToCurrency({ + amount: 0, + isTextInsteadOfZeroFormat: true, +}); + +// "Даром" +formatNumberToCurrency({ + amount: 0, + isTextInsteadOfZeroFormat: true, + zeroSumPlaceholder: "Даром", +}); +``` + +--- diff --git a/package/src/index.ts b/package/src/index.ts index 9bb0162..b2e8206 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -4,5 +4,11 @@ export { addDays, addMonths, addYears, isDate } from './date'; export { formatPhoneToView } from './phone'; -export { declensionDay, declensionMonth, declensionOfWords, declensionYear } from './declension'; - +export { + declensionDay, + declensionMonth, + declensionOfWords, + declensionYear, +} from './declension'; + +export { formatNumberToCurrency } from './number'; diff --git a/package/src/number/formatNumberToCurrency/formatNumberToCurrency.test.ts b/package/src/number/formatNumberToCurrency/formatNumberToCurrency.test.ts new file mode 100644 index 0000000..869c895 --- /dev/null +++ b/package/src/number/formatNumberToCurrency/formatNumberToCurrency.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; + +import { formatNumberToCurrency } from './formatNumberToCurrency'; + +const consoleMock = vi.spyOn(console, 'error'); + +// Если использовать обычные пробелы, то тест не проходит +const numbersToFormatCases = [ + { expectedValue: '100 ₽', numberToFormat: 100 }, + { expectedValue: '1 000 ₽', numberToFormat: 1000 }, + { expectedValue: '10 000 ₽', numberToFormat: 10000 }, + { expectedValue: '565 825 ₽', numberToFormat: 565825 }, +]; + +const notSafeInteger = '999999999999999999999999999999999'; + +const incorrectCases = [null, undefined, false, true, notSafeInteger]; + +describe('formatNumberToCurrency', () => { + it.each(numbersToFormatCases)( + 'Вернётся строка $expectedValue, если будет передано число $numberToFormat', + ({ expectedValue, numberToFormat }) => { + const result = formatNumberToCurrency({ + amount: numberToFormat, + }); + + expect(expectedValue).toBe(result); + }, + ); + + it('Вернётся кастомный текст, если переданное число - это 0', () => { + const value = 0; + const expectedPlaceholderMessage = 'Даром'; + + const result = formatNumberToCurrency({ + amount: value, + isTextInsteadOfZeroFormat: true, + zeroSumPlaceholder: 'Даром', + }); + + expect(result).toEqual(expectedPlaceholderMessage); + }); + + it('Вернётся отформатированное значение, если amount передан в качестве строки', () => { + const value = '100'; + const expectedCurrencyFormat = '100\u00A0₽'; + + const result = formatNumberToCurrency({ + amount: value, + }); + + expect(result).toEqual(expectedCurrencyFormat); + }); + + it('Вернётся "0 ₽", если передано число 0', () => { + const zeroValue = 0; + const expectedCurrencyFormat = '0\u00A0₽'; + + const result = formatNumberToCurrency({ + amount: zeroValue, + }); + + expect(result).toEqual(expectedCurrencyFormat); + }); + + it('Вернется текст "Бесплатно", если передан параметр для отображения текста', () => { + const zeroValue = 0; + const expectedCurrencyFormat = 'Бесплатно'; + + const result = formatNumberToCurrency({ + amount: zeroValue, + isTextInsteadOfZeroFormat: true, + }); + + expect(result).toEqual(expectedCurrencyFormat); + }); + + it.each(incorrectCases)( + 'В консоли появится сообщение о неподходящем формате переданных данных, если передано %s', + (value) => { + formatNumberToCurrency({ + // @ts-expect-error Здесь используется ts-ignore для проверки того, что в случае возникновения проблем в рантайме, мы не столкнемся с неожиданным поведением + amount: value, + isTextInsteadOfZeroFormat: true, + }); + + expect(consoleMock).toHaveBeenLastCalledWith( + 'formatNumberToCurrency: значение должно быть безопасным целым числом', + ); + }, + ); + + it.each(incorrectCases)('Вернется undefined, если передано %s', (value) => { + const result = formatNumberToCurrency({ + // @ts-expect-error Здесь используется ts-ignore для проверки того, что в случае возникновения проблем в рантайме, мы не столкнемся с неожиданным поведением + amount: value, + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/package/src/number/formatNumberToCurrency/formatNumberToCurrency.ts b/package/src/number/formatNumberToCurrency/formatNumberToCurrency.ts new file mode 100644 index 0000000..242eb47 --- /dev/null +++ b/package/src/number/formatNumberToCurrency/formatNumberToCurrency.ts @@ -0,0 +1,62 @@ +type FormatNumberToCurrencyParams = { + amount: number | string; + currencyCode?: 'RUB'; + isTextInsteadOfZeroFormat?: boolean; + zeroSumPlaceholder?: string; +}; + +/** + * Форматирует число в формат валюты. + * + * @param params - Параметры для форматирования. + * + * @param params.amount - Сумма в числовом или строковом формате. + * + * @param params.currencyCode - Код валюты в формате [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes). + * + * @param params.isTextInsteadOfZeroFormat - Флаг для замены отображения "0 ₽" на текст. По умолчанию "Бесплатно". + * + * @param params.zeroSumPlaceholder - Текст, который должен отображаться, если сумма равна "0". По умолчанию "Бесплатно". + * + * @example + * formatCurrency({ amount: 1000 }); // "1 000 ₽" + * formatCurrency({ amount: 0, zeroSumPlaceholder: "Даром", isFreeTextInsteadOfDefaultFormat: true }); // "Даром" + * formatCurrency({ amount: 0 }); // "0 ₽" + * formatCurrency({ amount: 10000, isFreeTextInsteadOfDefaultFormat: true }); // "10 000 ₽" + * formatNumberToCurrency({amount: 10000, currencyCode: "USD"}) // "10 000 $" + */ +export const formatNumberToCurrency = ( + params: FormatNumberToCurrencyParams, +) => { + const { + amount, + isTextInsteadOfZeroFormat, + currencyCode = 'RUB', + zeroSumPlaceholder = 'Бесплатно', + } = params; + + const preparedAmount = + // Если передана строка, то удаляем из неё пробелы + typeof amount === 'string' ? Number(amount.replace(/\s+/g, '')) : amount; + + // Защита на случай, если передано небезопасное целое число, либо в amount попадает не число (например, в рантайме) + if (!Number.isSafeInteger(preparedAmount)) { + console.error( + 'formatNumberToCurrency: значение должно быть безопасным целым числом', + ); + + return; + } + + const formatter = new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: currencyCode, + minimumFractionDigits: preparedAmount % 1 !== 0 ? 2 : 0, + }); + + if (amount === 0 && isTextInsteadOfZeroFormat) { + return zeroSumPlaceholder; + } + + return formatter.format(preparedAmount); +}; diff --git a/package/src/number/formatNumberToCurrency/index.ts b/package/src/number/formatNumberToCurrency/index.ts new file mode 100644 index 0000000..b076913 --- /dev/null +++ b/package/src/number/formatNumberToCurrency/index.ts @@ -0,0 +1 @@ +export * from './formatNumberToCurrency'; diff --git a/package/src/number/index.ts b/package/src/number/index.ts new file mode 100644 index 0000000..b076913 --- /dev/null +++ b/package/src/number/index.ts @@ -0,0 +1 @@ +export * from './formatNumberToCurrency';