From 57ff5c1045bd6cee5df99ba3595e96057bf19a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gromit=20=28=EC=A0=84=EB=AF=BC=EC=9E=AC=29?= <64779472+ssi02014@users.noreply.github.com> Date: Sun, 12 Jan 2025 18:21:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(utils):=20formatNumberWithUnits,=20formatNu?= =?UTF-8?q?mberWithCurrency=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#666)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(utils): formatNumberWithUnits, formatNumberWithCurrency 로직 개선 및 인터페이스 변경 * feat: 소수점 처리 추가 * fix: formatNumberWithUnits/Currency 로직 변경 * refactor: formatNumberWithUnits 리팩토링 --- .changeset/curly-timers-hope.md | 5 + .../utils/formatter/formatNumberByUnits.md | 76 --------- .../utils/formatter/formatNumberCurrency.md | 109 ------------ .../formatter/formatNumberWithCurrency.md | 75 +++++++++ .../utils/formatter/formatNumberWithUnits.md | 91 ++++++++++ .../formatNumberByUnits.spec.ts | 75 --------- .../formatter/formatNumberByUnits/index.ts | 72 -------- .../formatNumberCurrency.spec.ts | 94 ----------- .../formatter/formatNumberCurrency/index.ts | 37 ----- .../formatNumberWithCurrency.spec.ts | 133 +++++++++++++++ .../formatNumberWithCurrency.types.ts | 9 + .../formatNumberWithCurrency.utils.ts | 63 +++++++ .../formatNumberWithCurrency/index.ts | 76 +++++++++ .../formatNumberWithUnits.spec.ts | 156 ++++++++++++++++++ .../formatNumberWithUnits.types.ts | 27 +++ .../formatNumberWithUnits.utils.ts | 78 +++++++++ .../formatter/formatNumberWithUnits/index.ts | 97 +++++++++++ packages/utils/src/formatter/index.ts | 4 +- .../reverseString/reverseString.spec.ts | 7 + packages/utils/vitest.config.ts | 1 + 20 files changed, 820 insertions(+), 465 deletions(-) create mode 100644 .changeset/curly-timers-hope.md delete mode 100644 docs/docs/utils/formatter/formatNumberByUnits.md delete mode 100644 docs/docs/utils/formatter/formatNumberCurrency.md create mode 100644 docs/docs/utils/formatter/formatNumberWithCurrency.md create mode 100644 docs/docs/utils/formatter/formatNumberWithUnits.md delete mode 100644 packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts delete mode 100644 packages/utils/src/formatter/formatNumberByUnits/index.ts delete mode 100644 packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts delete mode 100644 packages/utils/src/formatter/formatNumberCurrency/index.ts create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/index.ts create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/index.ts diff --git a/.changeset/curly-timers-hope.md b/.changeset/curly-timers-hope.md new file mode 100644 index 000000000..8744c96d7 --- /dev/null +++ b/.changeset/curly-timers-hope.md @@ -0,0 +1,5 @@ +--- +'@modern-kit/utils': minor +--- + +fix(utils): formatNumberWithUnits, formatNumberWithCurrency 로직 개선 및 인터페이스 변경 - @ssi02014 diff --git a/docs/docs/utils/formatter/formatNumberByUnits.md b/docs/docs/utils/formatter/formatNumberByUnits.md deleted file mode 100644 index 486c9c0e6..000000000 --- a/docs/docs/utils/formatter/formatNumberByUnits.md +++ /dev/null @@ -1,76 +0,0 @@ -# formatNumberByUnits - -인자로 받은 숫자를 `단위 별로 구분`해주는 함수입니다. - -
- -## Code -[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberByUnits/index.ts) - -## Interface -```ts title="typescript" -interface Unit { - unit: string; - value: number; -} - -type FloorUnit = - | 1 - | 10 - | 100 - | 1000 - | 10000 - | 100000 - | 1000000 - | 10000000 - | 100000000; - -interface FormatNumberByUnitsOption { - units?: Unit[]; // default: [] - withCommas?: boolean; // default: false, - floorUnit?: FloorUnit; // default: 1, - insertSpace?: boolean; // default: false, -} - -const formatNumberByUnits: ( - value: number, - options?: FormatNumberByUnitsOption -) => string; -``` - -## Usage -```ts title="typescript" -import { formatNumberByUnits } from '@modern-kit/utils'; - -const ONE_HUNDRED_MILLION = 100000000; -const TEN_THOUSAND = 10000; - -const units = [ - { unit: '억', value: ONE_HUNDRED_MILLION }, - { unit: '만', value: TEN_THOUSAND }, -]; - -const value1 = formatNumberByUnits(450000000, { - units: units, -}); // '4억5000만' - -// withCommas옵션으로 천 단위마다 콤마를 추가할 수 있습니다. -const value2 = formatNumberByUnits(450000000, { - withCommas: true, - units: units, -}); // '4억5,000만' - -// insertSpace옵션으로 단위 마다 띄어쓰기를 추가할 수 있습니다. -const value3 = formatNumberByUnits(450000000, { - units: units, - withCommas: true, - insertSpace: true, -}); // '4억 5,000만' - -// floorUnit 옵션으로 해당 단위 미만의 값들을 버릴 수 있습니다. -const value4 = formatNumberByUnits(459325300, { - units: units, - withCommas: false, - floorUnit: 10000000, -}); // '4억5000만' -``` \ No newline at end of file diff --git a/docs/docs/utils/formatter/formatNumberCurrency.md b/docs/docs/utils/formatter/formatNumberCurrency.md deleted file mode 100644 index 2a2c07e2d..000000000 --- a/docs/docs/utils/formatter/formatNumberCurrency.md +++ /dev/null @@ -1,109 +0,0 @@ -# formatNumberCurrency - -인자로 받은 숫자를 `단위 별로 구분`하고 `통화 단위`를 추가할 수 있습니다. - -
- -## Code -[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberCurrency/index.ts) - -## Interface -```ts title="typescript" -interface CurrencyOption { - currency: string; - currencyPosition: 'prefix' | 'suffix'; -} - -/* - units?: Unit[]; // default: [] - floorUnit?: FloorUnit; // default: 1, - withCommas?: boolean; // default: false, - insertSpace?: boolean; // default: false, - currency?: string; // default: '' - currencyPosition?: 'prefix' | 'suffix'; // default: 'suffix' -*/ -type FormatNumberCurrencyOptions = FormatNumberByUnitsOption & - Partial; - -const formatNumberCurrency: ( - value: number, - options?: FormatNumberCurrencyOptions -) => string; -``` - -## Usage -### Basic -기본적인 사용법은 아래와 같습니다. -```ts title="typescript" -import { formatNumberCurrency } from '@modern-kit/utils'; - -const ONE_HUNDRED_MILLION = 100000000; -const TEN_THOUSAND = 10000; - -const units = [ - { unit: '억', value: ONE_HUNDRED_MILLION }, - { unit: '만', value: TEN_THOUSAND }, -]; - -const value1 = formatNumberCurrency(450000000, { - units: units, - currency: '원', - currencyPosition: 'suffix', -}); // '4억5000만원' - -const value2 = formatNumberCurrency(4500, { - currency: '$', - currencyPosition: 'prefix', -}); // '$4500' - -// withCommas 옵션으로 천 단위마다 콤마를 추가할 수 있습니다. -const value2 = formatNumberCurrency(450000000, { - units: units, - withCommas: true, - currency: '원', - currencyPosition: 'suffix', -}); // '4억5,000만원' - -// insertSpace 옵션으로 단위 마다 띄어쓰기를 추가할 수 있습니다. -const value3 = formatNumberCurrency(450000000, { - units: units, - withCommas: true, - currency: '원', - currencyPosition: 'suffix', - insertSpace: true, -}); // '4억 5,000만원' - -// floorUnit 옵션으로 해당 단위 미만의 값들을 버릴 수 있습니다. -const value4 = formatNumberCurrency(459325300, { - units: units, - currency: '원', - currencyPosition: 'suffix', - floorUnit: 10000000, -}); // '4억5000만원' -``` - -
- -### ⭐️ Best Practice -아래와 같이 필요에 맞게 `추상화`해서 사용하는 것을 추천드립니다. - -```ts title="typescript" -const ONE_HUNDRED_MILLION = 100000000; -const TEN_THOUSAND = 10000; - -const units = [ - { unit: '억', value: ONE_HUNDRED_MILLION }, - { unit: '만', value: TEN_THOUSAND }, -]; - -const formatToKRW = (value: number) => { - return formatNumberCurrency(value, { - units: units, - currency: '원', - currencyPosition: 'suffix', - }); -} - -const value1 = formatToKRW(42000000); // 4200만원 -const value2 = formatToKRW(425000000); // 4억2500만원 -``` \ No newline at end of file diff --git a/docs/docs/utils/formatter/formatNumberWithCurrency.md b/docs/docs/utils/formatter/formatNumberWithCurrency.md new file mode 100644 index 000000000..97465fda0 --- /dev/null +++ b/docs/docs/utils/formatter/formatNumberWithCurrency.md @@ -0,0 +1,75 @@ +# formatNumberWithCurrency + +`숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `통화 기호`를 추가하는 함수입니다. + +
+ +## Code +[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberWithCurrency/index.ts) + +## Interface +```ts title="typescript" +type Locale = 'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR'; + +interface CurrencyOptions { + symbol?: string; // default: '' + position?: 'prefix' | 'suffix'; // default: 'suffix' + space?: boolean; // default: false + commas?: boolean; // default: true + locale?: Locale; +} +``` +```ts title="typescript" +function formatNumberWithCurrency( + value: number | string, + options?: CurrencyOptions +): string; +``` + +## Usage +### 기본 동작 +```ts title="typescript" +import { formatNumberWithCurrency } from '@modern-kit/utils'; + +// 기본 동작 +formatNumberWithCurrency(1000) // '1,000' + +// 통화 기호 추가 (기본 값: '') +formatNumberWithCurrency(1000, { symbol: '원' }) // '1,000원' +formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix' }) // '$1,000' + +// 숫자로 이루어진 문자열도 허용 +formatNumberWithCurrency('1000', { symbol: '원' }) // '1,000원' +formatNumberWithCurrency('1000', { symbol: '$', position: 'prefix' }) // '$1,000' + +// 음수 처리 +formatNumberWithCurrency(-1000, { symbol: '$', position: 'prefix' }) // '-$1,000' +formatNumberWithCurrency(-1000, { symbol: '원', position: 'suffix' }) // '-1,000원' +``` + +
+ +### 옵션 사용 +```ts title="typescript" +import { formatNumberWithCurrency } from '@modern-kit/utils'; + +// 통호 기호 위치 변경 (기본값: 'suffix') +formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix' }) // '$1,000' + +// 공백 추가 (기본값: false) +formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix', space: false }) // '$1000' +formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix', space: true }) // '$ 1000' + +// 천의 단위 구분 여부 (기본값: true) +formatNumberWithCurrency(1000, { symbol: '원', commas: false }) // '1000원' +formatNumberWithCurrency(1000, { symbol: '원', commas: true }) // '1,000원' + +// locale 사용 +// locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시됩니다. +formatNumberWithCurrency(1000, { locale: 'USD' }) // '$1,000' +formatNumberWithCurrency(1000, { locale: 'KRW' }) // '1,000원' +formatNumberWithCurrency(1000, { locale: 'KRW_SYMBOL' }) // '1,000₩' +formatNumberWithCurrency(1000, { locale: 'JPY' }) // '¥1,000' +formatNumberWithCurrency(1000, { locale: 'CNY' }) // '¥1,000' +formatNumberWithCurrency(1000, { locale: 'EUR' }) // '€1,000' +``` diff --git a/docs/docs/utils/formatter/formatNumberWithUnits.md b/docs/docs/utils/formatter/formatNumberWithUnits.md new file mode 100644 index 000000000..1bd6558e3 --- /dev/null +++ b/docs/docs/utils/formatter/formatNumberWithUnits.md @@ -0,0 +1,91 @@ +# formatNumberWithUnits + +`숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `단위` 별로 포맷팅하는 함수입니다. + +
+ +## Code +[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberWithUnits/index.ts) + +## Interface +```ts title="typescript" +interface Unit { + unit: string; + value: number; +} + +type FloorUnit = + | 1 + | 10 + | 100 + | 1_000 + | 10_000 + | 100_000 + | 1_000_000 + | 10_000_000 + | 100_000_000 + | 1_000_000_000 + | 10_000_000_000 + | 100_000_000_000 + | 1_000_000_000_000; + +interface FormatNumberWithUnitsOptions { + units?: Unit[] | readonly Unit[]; // default: [] + commas?: boolean; // default: true + floorUnit?: FloorUnit; // default: 1 + space?: boolean; // default: true +} +``` +```ts title="typescript" +function formatNumberWithUnits( + value: number | string, + options?: FormatNumberWithUnitsOptions +): string; +``` + +## Usage +### 기본 동작 +```ts title="typescript" +import { formatNumberWithUnits } from '@modern-kit/utils'; + +const KRW_UNITS = [ + { unit: '조', value: 1_000_000_000_000 }, + { unit: '억', value: 100_000_000 }, + { unit: '만', value: 10_000 }, +] as const; + +// 기본 동작 +formatNumberWithUnits(1234567) // "1,234,567" + +formatNumberWithUnits(1234567, { units: KRW_UNITS }) // "123만 4,567" +formatNumberWithUnits(-1234567, { units: KRW_UNITS }) // "-123만 4,567", 음수 처리 + +formatNumberWithUnits('1234567', { units: KRW_UNITS }) // "123만 4,567", 숫자로 이루어진 문자열 허용 +``` + +
+ +### 옵션 사용 +```ts title="typescript" +import { formatNumberWithUnits } from '@modern-kit/utils'; + +const KRW_UNITS = [ + { unit: '조', value: 1_000_000_000_000 }, + { unit: '억', value: 100_000_000 }, + { unit: '만', value: 10_000 }, +] as const; + +// 단위 사이 공백 추가 (기본값: true) +formatNumberWithUnits(1234567, { units: KRW_UNITS, space: true }) // "123만 4,567" +formatNumberWithUnits(1234567, { units: KRW_UNITS, space: false }) // "123만4,567" + +// 쉼표 사용 여부 (기본값: true) +formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: false }) // "123만 4567" +formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: true }) // "123만 4,567" + +// 버림 단위 (기본값: 1) +formatNumberWithUnits(1234567, { units: KRW_UNITS, floorUnit: 10000 }) // "123만" + +// 소수점 자리수 (기본값: 0) +formatNumberWithUnits(1234567.123, { units: KRW_UNITS, decimal: 2 }) // "123만 4,567.12" +``` \ No newline at end of file diff --git a/packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts b/packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts deleted file mode 100644 index 4904386a2..000000000 --- a/packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { formatNumberByUnits } from '.'; - -const ONE_HUNDRED_MILLION = 100000000; -const TEN_THOUSAND = 10000; - -const testUnits = [ - { unit: '억', value: ONE_HUNDRED_MILLION }, - { unit: '만', value: TEN_THOUSAND }, -]; - -describe('formatNumberByUnits', () => { - it('should format numbers based on the provided options', () => { - const testValue1 = formatNumberByUnits(4500000, { - withCommas: false, - units: testUnits, - }); - expect(testValue1).toBe('450만'); - - const testValue2 = formatNumberByUnits(450000000, { - withCommas: false, - units: testUnits, - }); - expect(testValue2).toBe('4억5000만'); - - const testValue3 = formatNumberByUnits(459005300, { - withCommas: false, - units: testUnits, - }); - expect(testValue3).toBe('4억5900만5300'); - - const testValue4 = formatNumberByUnits(459005300, { - withCommas: true, - units: testUnits, - }); - expect(testValue4).toBe('4억5,900만5,300'); - }); - - it('should add spaces between units when the insertSpace option is provided', () => { - const testValue2 = formatNumberByUnits(450000000, { - withCommas: true, - units: testUnits, - insertSpace: true, - }); - expect(testValue2).toBe('4억 5,000만'); - }); - - it('should discard values below the specified floorUnit when provided', () => { - const testValue1 = formatNumberByUnits(459325300, { - floorUnit: 10000, - withCommas: false, - units: testUnits, - }); - expect(testValue1).toBe('4억5932만'); - - const testValue2 = formatNumberByUnits(459325300, { - floorUnit: 10000000, - withCommas: false, - units: testUnits, - }); - expect(testValue2).toBe('4억5000만'); - - const testValue4 = formatNumberByUnits(4000000, { - floorUnit: 100000000, - withCommas: false, - units: testUnits, - }); - expect(testValue4).toBe('0'); - }); - - it('should format using default values if no options are provided', () => { - const testValue1 = formatNumberByUnits(450000000); - expect(testValue1).toBe('450000000'); - }); -}); diff --git a/packages/utils/src/formatter/formatNumberByUnits/index.ts b/packages/utils/src/formatter/formatNumberByUnits/index.ts deleted file mode 100644 index cf03b7a92..000000000 --- a/packages/utils/src/formatter/formatNumberByUnits/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { formatNumberWithCommas } from '../formatNumberWithCommas'; - -interface Unit { - unit: string; - value: number; -} - -type FloorUnit = - | 1 - | 10 - | 100 - | 1000 - | 10000 - | 100000 - | 1000000 - | 10000000 - | 100000000; - -export interface FormatNumberByUnitsOption { - units?: Unit[]; - withCommas?: boolean; - floorUnit?: FloorUnit; - insertSpace?: boolean; -} - -const getNumberWithConditionalCommas = (value: number, withCommas: boolean) => { - return withCommas ? formatNumberWithCommas(value) : String(value); -}; - -export function formatNumberByUnits( - value: number, - options: FormatNumberByUnitsOption = {} -) { - const { - units = [], - insertSpace = false, - withCommas = false, - floorUnit = 1, - } = options; - - if (value < floorUnit) { - return String(0); - } - - if (units.length === 0) { - return getNumberWithConditionalCommas(value, withCommas); - } - - const sortedUnits = [...units].sort((a, b) => b.value - a.value); - let result = ''; - let remainder = Math.floor(value / floorUnit) * floorUnit; - - sortedUnits.forEach(({ unit, value: unitValue }) => { - const quotient = Math.floor(remainder / unitValue); - - if (quotient > 0) { - result += `${getNumberWithConditionalCommas( - quotient, - withCommas - )}${unit}`; - - if (insertSpace) result += ' '; - remainder %= unitValue; - } - }); - - if (remainder > 0) { - result += `${getNumberWithConditionalCommas(remainder, withCommas)}`; - } - - return result.trim(); -} diff --git a/packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts b/packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts deleted file mode 100644 index 40924182c..000000000 --- a/packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { formatNumberCurrency } from '.'; - -const ONE_HUNDRED_MILLION = 100000000; -const TEN_THOUSAND = 10000; - -const testUnits = [ - { unit: '억', value: ONE_HUNDRED_MILLION }, - { unit: '만', value: TEN_THOUSAND }, -]; - -describe('formatNumberCurrency', () => { - it('should format numbers based on the provided options', () => { - const testValue1 = formatNumberCurrency(4500000, { - withCommas: false, - units: testUnits, - currency: '원', - currencyPosition: 'suffix', - }); - expect(testValue1).toBe('450만원'); - - const testValue2 = formatNumberCurrency(459005300, { - withCommas: false, - units: testUnits, - currency: '원', - currencyPosition: 'suffix', - }); - expect(testValue2).toBe('4억5900만5300원'); - - const testValue3 = formatNumberCurrency(4500, { - withCommas: false, - currency: '$', - currencyPosition: 'prefix', - }); - expect(testValue3).toBe('$4500'); - - const testValue4 = formatNumberCurrency(459005300, { - withCommas: true, - units: testUnits, - currency: '원', - currencyPosition: 'suffix', - }); - expect(testValue4).toBe('4억5,900만5,300원'); - - const testValue5 = formatNumberCurrency(4500, { - withCommas: true, - currency: '$', - currencyPosition: 'prefix', - }); - expect(testValue5).toBe('$4,500'); - }); - - it('should add spaces between units when the insertSpace option is provided', () => { - const testValue2 = formatNumberCurrency(450000000, { - withCommas: true, - units: testUnits, - insertSpace: true, - currency: '원', - currencyPosition: 'suffix', - }); - expect(testValue2).toBe('4억 5,000만원'); - }); - - it('should discard values below the specified floorUnit when provided', () => { - const testValue1 = formatNumberCurrency(459325300, { - floorUnit: 10000, - withCommas: false, - units: testUnits, - currency: '원', - }); - expect(testValue1).toBe('4억5932만원'); - - const testValue2 = formatNumberCurrency(459325300, { - floorUnit: 10000000, - withCommas: false, - units: testUnits, - currency: '원', - }); - expect(testValue2).toBe('4억5000만원'); - - const testValue4 = formatNumberCurrency(4000000, { - floorUnit: 100000000, - withCommas: false, - units: testUnits, - currency: '원', - }); - expect(testValue4).toBe('0원'); - }); - - it('should format using default values if no options are provided', () => { - const testValue = formatNumberCurrency(450000000); - expect(testValue).toBe('450000000'); - }); -}); diff --git a/packages/utils/src/formatter/formatNumberCurrency/index.ts b/packages/utils/src/formatter/formatNumberCurrency/index.ts deleted file mode 100644 index bd4cbe873..000000000 --- a/packages/utils/src/formatter/formatNumberCurrency/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - formatNumberByUnits, - FormatNumberByUnitsOption, -} from '../formatNumberByUnits'; - -interface CurrencyOption { - currency: string; - currencyPosition: 'prefix' | 'suffix'; -} - -type FormatNumberCurrencyOptions = FormatNumberByUnitsOption & - Partial; - -const addCurrency = (value: string, currencyOption: CurrencyOption) => { - const { currency, currencyPosition } = currencyOption; - - if (currencyPosition === 'prefix') { - return currency + value; - } - return value + currency; -}; - -export function formatNumberCurrency( - value: number, - options: FormatNumberCurrencyOptions = {} -) { - const { - currency = '', - currencyPosition = 'suffix', - ...restOptions - } = options; - - return addCurrency(formatNumberByUnits(value, restOptions), { - currency, - currencyPosition, - }); -} diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts new file mode 100644 index 000000000..cf63fc08c --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from 'vitest'; +import { formatNumberWithCurrency } from './index'; + +describe('formatNumberWithCurrency', () => { + describe('기본 동작', () => { + it('옵션 없이 호출하면 숫자만 반환해야 합니다.', () => { + expect(formatNumberWithCurrency(1000)).toBe('1,000'); + expect(formatNumberWithCurrency('1000')).toBe('1,000'); + }); + + it('기본적으로 접미사(suffix) 위치에 통화 기호를 추가해야 합니다.', () => { + expect(formatNumberWithCurrency(1000, { symbol: '원' })).toBe('1,000원'); + expect(formatNumberWithCurrency('1000', { symbol: '원' })).toBe( + '1,000원' + ); + }); + + it('음수일 때 통화 기호를 앞에 추가해야 합니다.', () => { + expect( + formatNumberWithCurrency(-1000, { symbol: '$', position: 'prefix' }) + ).toBe('-$1,000'); + expect( + formatNumberWithCurrency('-1000', { symbol: '$', position: 'prefix' }) + ).toBe('-$1,000'); + }); + }); + + describe('옵션 적용', () => { + it('position 옵션을 기반으로 기호를 추가해야 합니다.', () => { + expect( + formatNumberWithCurrency(1000, { + symbol: '$', + position: 'prefix', + }) + ).toBe('$1,000'); + + expect( + formatNumberWithCurrency(1000, { + symbol: '$', + position: 'suffix', + }) + ).toBe('1,000$'); + }); + + it('space가 true일 때 통화 기호와 숫자 사이에 공백을 추가해야 합니다.', () => { + expect( + formatNumberWithCurrency(1000, { + symbol: '$', + position: 'prefix', + space: true, + }) + ).toBe('$ 1,000'); + + expect( + formatNumberWithCurrency(1000, { + symbol: '$', + position: 'prefix', + space: false, + }) + ).toBe('$1,000'); + }); + + it('commas 옵션을 기반으로 쉼표를 추가해야 합니다.', () => { + expect( + formatNumberWithCurrency(1000, { + symbol: '$', + position: 'prefix', + commas: true, + }) + ).toBe('$1,000'); + + expect( + formatNumberWithCurrency(1000, { + symbol: '$', + position: 'prefix', + commas: false, + }) + ).toBe('$1000'); + }); + + it('locale 옵션으로 통화 기호를 자동 적용해야 합니다.', () => { + expect( + formatNumberWithCurrency(1000, { + locale: 'USD', + }) + ).toBe('$1,000'); + + expect( + formatNumberWithCurrency(1000, { + locale: 'KRW', + }) + ).toBe('1,000원'); + }); + + it('locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시되어야 합니다.', () => { + expect( + formatNumberWithCurrency(1000, { + locale: 'USD', + commas: true, + symbol: '*', // 무시 + position: 'prefix', // 무시 + space: true, // 무시 + }) + ).toBe('$1,000'); + + expect( + formatNumberWithCurrency(1000, { + locale: 'USD', + commas: false, + symbol: '*', // 무시 + position: 'prefix', // 무시 + space: true, // 무시 + }) + ).toBe('$1000'); + }); + }); + + describe('에러 처리', () => { + it('숫자가 아닌 값이 주어지면 에러를 던져야 합니다.', () => { + expect(() => formatNumberWithCurrency('10d00')).toThrow( + 'value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.' + ); + }); + + it('지원하지 않는 locale 입력시 에러를 던져야 합니다.', () => { + expect(() => + formatNumberWithCurrency(1000, { + locale: 'INVALID' as unknown as 'KRW', + }) + ).toThrow('유효하지 않은 locale 입니다.'); + }); + }); +}); diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts new file mode 100644 index 000000000..581b1e372 --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts @@ -0,0 +1,9 @@ +type Locale = 'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR'; + +export interface FormatNumberCurrencyOptions { + symbol?: string; + position?: 'prefix' | 'suffix'; + space?: boolean; + commas?: boolean; + locale?: Locale; +} diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts new file mode 100644 index 000000000..b6b94707d --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts @@ -0,0 +1,63 @@ +import { formatNumberWithCommas } from '../../formatter/formatNumberWithCommas'; +import { FormatNumberCurrencyOptions } from './formatNumberWithCurrency.types'; + +const LOCALE_CURRENCY_MAP = { + KRW: { symbol: '원', position: 'suffix', space: false }, + KRW_SYMBOL: { symbol: '₩', position: 'suffix', space: false }, + USD: { symbol: '$', position: 'prefix', space: false }, + JPY: { symbol: '¥', position: 'prefix', space: false }, + CNY: { symbol: '¥', position: 'prefix', space: false }, + EUR: { symbol: '€', position: 'prefix', space: false }, +} as const; + +/** + * @description 통화 옵션을 가져오는 함수 + * + * @param {Omit} options - 통화 옵션 + * @returns {Omit} locale을 제외한 통화 옵션 + */ +const getCurrencyOption = ( + options: Omit +): Omit => { + const { symbol, position, space, locale } = options; + + if (locale) { + if (!LOCALE_CURRENCY_MAP[locale]) { + throw new Error('유효하지 않은 locale 입니다.'); + } + + return LOCALE_CURRENCY_MAP[locale]; + } + + return { symbol, position, space }; +}; + +/** + * @description 숫자에 통화 기호를 추가하는 함수입니다. + * + * @param {number | string} value - 통화 기호를 추가할 숫자 또는 문자열 + * @param {FormatNumberCurrencyOptions & { isNegative: boolean }} currencyOption - 통화 단위 옵션 + * @param {string} [currencyOption.symbol=''] - 통화 기호 + * @param {'prefix' | 'suffix'} [currencyOption.position='suffix'] - 통화 기호 위치 + * @param {boolean} [currencyOption.space=false] - 숫자와 통화 기호 사이 공백 여부 + * @returns {string} 통화 기호가 추가된 문자열 + */ +export const getFormattedNumberWithCurrency = ( + value: number, + options: FormatNumberCurrencyOptions & { isNegative: boolean } +): string => { + const { commas, ...restOption } = options; + const { symbol, position, space } = getCurrencyOption(restOption); + + const valueToUse = commas ? formatNumberWithCommas(value) : String(value); + + if (position === 'prefix') { + // 음수 처리 + if (options.isNegative) { + const numericPart = valueToUse.slice(1); + return '-' + symbol + (space ? ' ' : '') + numericPart; + } + return symbol + (space ? ' ' : '') + valueToUse; + } + return valueToUse + (space ? ' ' : '') + symbol; +}; diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/index.ts b/packages/utils/src/formatter/formatNumberWithCurrency/index.ts new file mode 100644 index 000000000..7b7db37e2 --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithCurrency/index.ts @@ -0,0 +1,76 @@ +import { isNumber } from '../../validator/isNumber'; +import { FormatNumberCurrencyOptions } from './formatNumberWithCurrency.types'; +import { getFormattedNumberWithCurrency } from './formatNumberWithCurrency.utils'; + +/** + * @description `숫자 혹은 숫자로 이뤄진 문자열`을 주어진 `통화 기호`를 추가하는 함수입니다. + * + * @param {number | string} value - 포맷팅할 숫자 값 + * @param {FormatNumberCurrencyOptions} options - 포맷팅 옵션 + * @param {string} [options.symbol=''] - 통화 기호 + * @param {'prefix' | 'suffix'} [options.position='suffix'] - 통화 기호 위치 + * @param {boolean} [options.space=false] - 숫자와 통화 기호 사이 공백 여부 + * @param {boolean} [options.commas=true] - 천의 단위 구분 여부 + * @param {'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR'} [options.locale] - 통화 단위 + * @returns 통화 기호가 포함된 포맷팅된 문자열 + * + * @example + * // 기본 사용법 + * formatNumberWithCurrency(1000) // '1,000' + * + * // 통화 기호 추가 (기본값: '') + * formatNumberWithCurrency(1000, { symbol: '원' }) // '1,000원' + * + * // 음수 처리 + * formatNumberWithCurrency(-1000, { symbol: '$', position: 'prefix' }) // '-$1,000' + * + * // 숫자로 이뤄진 문자열 허용 + * formatNumberWithCurrency('1000', { symbol: '원' }) // '1,000원' + * + * @example + * // 통호 기호 위치 변경 (기본값: 'suffix') + * formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix' }) // '$1,000' + * + * // 공백 추가 (기본값: false) + * formatNumberWithCurrency(1000, { symbol: '₩', space: true }) // '1,000 원' + * + * // 천의 단위 구분 여부 (기본값: true) + * formatNumberWithCurrency(1000, { symbol: '원', commas: false }) // '1000원' + * formatNumberWithCurrency(1000, { symbol: '원', commas: true }) // '1,000원' + * + * // locale 사용 + * formatNumberWithCurrency(1000, { locale: 'USD' }) // '$1,000' + * formatNumberWithCurrency(1000, { locale: 'KRW' }) // '1,000원' + * + * // locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시됩니다. + * formatNumberWithCurrency(1000, { locale: 'KRW', commas: false }) // '1000원' + */ +export function formatNumberWithCurrency( + value: number | string, + options: FormatNumberCurrencyOptions = {} +) { + const { + symbol = '', + position = 'suffix', + space = false, + commas = true, + locale, + } = options; + const valueToUse = isNumber(value) ? value : Number(value); + const isNegative = valueToUse < 0; + + if (isNaN(valueToUse)) { + throw new Error('value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.'); + } + + const formattedResult = getFormattedNumberWithCurrency(valueToUse, { + symbol, + position, + space, + locale, + commas, + isNegative, + }); + + return formattedResult; +} diff --git a/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts new file mode 100644 index 000000000..7f2cb36eb --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts @@ -0,0 +1,156 @@ +import { describe, it, expect } from 'vitest'; +import { formatNumberWithUnits } from './index'; + +const KRW_UNITS = [ + { unit: '조', value: 1_000_000_000_000 }, + { unit: '억', value: 100_000_000 }, + { unit: '만', value: 10_000 }, +] as const; + +describe('formatNumberWithUnits', () => { + describe('기본 동작', () => { + it('units 옵션이 주어지면 해당 단위로 숫자를 포맷팅한다', () => { + expect(formatNumberWithUnits(1234567890000, { units: KRW_UNITS })).toBe( + '1조 2,345억 6,789만' + ); + expect(formatNumberWithUnits(1200000000, { units: KRW_UNITS })).toBe( + '12억' + ); + expect(formatNumberWithUnits(4010000, { units: KRW_UNITS })).toBe( + '401만' + ); + + // 문자열 + expect(formatNumberWithUnits('1234567890000', { units: KRW_UNITS })).toBe( + '1조 2,345억 6,789만' + ); + expect(formatNumberWithUnits('1200000000', { units: KRW_UNITS })).toBe( + '12억' + ); + expect(formatNumberWithUnits('4010000', { units: KRW_UNITS })).toBe( + '401만' + ); + }); + + it('음수를 처리할 수 있다', () => { + expect(formatNumberWithUnits(-1234567890000, { units: KRW_UNITS })).toBe( + '-1조 2,345억 6,789만' + ); + expect(formatNumberWithUnits(-1000000000, { units: KRW_UNITS })).toBe( + '-10억' + ); + + // 문자열 + expect( + formatNumberWithUnits('-1234567890000', { units: KRW_UNITS }) + ).toBe('-1조 2,345억 6,789만'); + expect(formatNumberWithUnits('-1000000000', { units: KRW_UNITS })).toBe( + '-10억' + ); + }); + + it('0과 작은 숫자를 처리한다', () => { + expect(formatNumberWithUnits(0)).toBe('0'); + expect(formatNumberWithUnits(9)).toBe('9'); + }); + }); + + // 문자열 케이스는 제외 + describe('옵션 적용', () => { + it('floorUnit 주어진 값에 따라 버림 단위를 적용해야 합니다.', () => { + expect( + formatNumberWithUnits(12345, { floorUnit: 10000, units: KRW_UNITS }) + ).toBe('1만'); + + // value보다 floorUnit이 크면 '0'을 반환 + expect( + formatNumberWithUnits(12345, { floorUnit: 100000, units: KRW_UNITS }) + ).toBe('0'); + + // units 옵션이 주어지지 않으면 기본 단위로 포맷팅 + expect(formatNumberWithUnits(12345, { floorUnit: 10000 })).toBe('10,000'); + }); + + it('commas가 true라면 ","를 추가하며, false라면 제외해야 합니다.', () => { + expect( + formatNumberWithUnits(1234567890000, { + commas: false, + units: KRW_UNITS, + }) + ).toBe('1조 2345억 6789만'); + + expect( + formatNumberWithUnits(1234567890000, { + commas: true, + units: KRW_UNITS, + }) + ).toBe('1조 2,345억 6,789만'); + + // units 옵션이 주어지지 않으면 기본 단위로 포맷팅 + expect(formatNumberWithUnits(1234567890000, { commas: true })).toBe( + '1,234,567,890,000' + ); + }); + + it('space가 true라면 unit 사이 공백을 추가하며, false라면 제외해야 합니다.', () => { + expect( + formatNumberWithUnits(1234567890000, { + space: false, + units: KRW_UNITS, + }) + ).toBe('1조2,345억6,789만'); + + expect( + formatNumberWithUnits(1234567890000, { + space: true, + units: KRW_UNITS, + }) + ).toBe('1조 2,345억 6,789만'); + + // units 옵션이 주어지지 않으면 기본 단위로 포맷팅 + expect(formatNumberWithUnits(1234567890000, { space: true })).toBe( + '1,234,567,890,000' + ); + }); + + it('decimal가 주어진 값에 따라 소수점 자리수를 적용해야 합니다.', () => { + expect( + formatNumberWithUnits(1234567.123, { decimal: 2, units: KRW_UNITS }) + ).toBe('123만 4,567.12'); + + // 만약, floorUnit가 1보다 크면 소수점 자리수를 적용하지 않습니다. + expect( + formatNumberWithUnits(1234567.123, { + decimal: 2, + floorUnit: 1000, + units: KRW_UNITS, + }) + ).toBe('123만 4,000'); + + // units 옵션이 주어지지 않으면 기본 단위로 포맷팅 + expect(formatNumberWithUnits(1234567.123, { decimal: 2 })).toBe( + '1,234,567.12' + ); + }); + }); + + describe('예외 케이스', () => { + it('value가 숫자 혹은 숫자로 이뤄진 문자열이 아니면 예외를 발생시킵니다.', () => { + expect(() => formatNumberWithUnits('d123ㅇ4567')).toThrow( + 'value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.' + ); + }); + + it('floorUnit가 1 이상의 정수가 아니면 예외를 발생시킵니다.', () => { + expect(() => + formatNumberWithUnits(1234567, { floorUnit: -1 as unknown as 10 }) + ).toThrow('floorUnit은 1을 포함한 10의 제곱수여야 합니다.'); + }); + + it('decimal이 0보다 작으면 예외를 발생시킵니다.', () => { + expect(() => + formatNumberWithUnits(1234567, { decimal: -1 as unknown as number }) + ).toThrow('decimal은 0 이상의 정수여야 합니다.'); + }); + }); +}); diff --git a/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts new file mode 100644 index 000000000..17df2a98c --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts @@ -0,0 +1,27 @@ +type FloorUnit = + | 1 + | 10 + | 100 + | 1_000 + | 10_000 + | 100_000 + | 1_000_000 + | 10_000_000 + | 100_000_000 + | 1_000_000_000 + | 10_000_000_000 + | 100_000_000_000 + | 1_000_000_000_000; + +export interface Unit { + unit: string; + value: number; +} + +export interface FormatNumberWithUnitsOptions { + units?: Unit[] | readonly Unit[]; + commas?: boolean; + floorUnit?: FloorUnit; + space?: boolean; + decimal?: number; +} diff --git a/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts new file mode 100644 index 000000000..cdad89803 --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts @@ -0,0 +1,78 @@ +import { formatNumberWithCommas } from '../../formatter/formatNumberWithCommas'; +import { + FormatNumberWithUnitsOptions, + Unit, +} from './formatNumberWithUnits.types'; + +/** + * @description 쉼표 사용 여부에 따라 숫자를 포맷팅하는 함수 + * + * @param {number} value - 포맷팅할 숫자 + * @param {boolean} commas - 쉼표 사용 여부 + * @returns {string} 포맷팅된 문자열 + */ +const getNumberWithConditionalCommas = ( + value: number | string, + commas: boolean +): string => { + return commas ? formatNumberWithCommas(value) : String(value); +}; + +/** + * @description 주어진 단위(units)에 따라 숫자를 포맷팅하는 함수 + * + * @param {number} value - 포맷팅할 숫자 값 + * @param {Unit[] | readonly Unit[]} units - 변환할 단위 배열 + * @param {Omit, 'units'>} options - 포맷팅 옵션 + * @param {boolean} options.commas - 천 단위 구분 쉼표 사용 여부입니다. + * @param {boolean} options.space - 단위 사이 공백 추가 여부입니다. + * @param {number} options.decimal - 소수점 자릿수입니다. + * @returns {string} 포맷팅된 문자열 + */ +export const getFormattedValueWithUnits = ( + value: number, + units: Unit[] | readonly Unit[], + options: Omit, 'units'> +): string => { + const { commas, space, decimal, floorUnit } = options; + + const absoluteValue = Math.abs(value); + const isNegative = value < 0; + + // value가 floorUnit(버림 단위)보다 작으면 '0'을 반환 + if (absoluteValue < floorUnit) { + return '0'; + } + + let formattedResult = ''; + let remainingValue = + floorUnit > 1 + ? Math.floor(absoluteValue / floorUnit) * floorUnit + : absoluteValue; + + // unit 별로 나누기 + for (let i = 0; i < units.length; i++) { + const { unit, value } = units[i]; + const quotient = Math.floor(remainingValue / value); + + if (quotient <= 0) continue; + + formattedResult += `${getNumberWithConditionalCommas( + quotient, + commas + )}${unit}${space ? ' ' : ''}`; + + remainingValue %= value; + } + + // 남은 remainingValue가 있으면 추가 + if (remainingValue > 0) { + formattedResult += `${getNumberWithConditionalCommas( + remainingValue.toFixed(decimal), + commas + )}`; + } + + // 음수일 경우 앞에 '-' 붙이며, 앞/뒤 공백 제거 + return (isNegative ? '-' : '') + formattedResult.trim(); +}; diff --git a/packages/utils/src/formatter/formatNumberWithUnits/index.ts b/packages/utils/src/formatter/formatNumberWithUnits/index.ts new file mode 100644 index 000000000..2c9b82f7e --- /dev/null +++ b/packages/utils/src/formatter/formatNumberWithUnits/index.ts @@ -0,0 +1,97 @@ +import { getFormattedValueWithUnits } from './formatNumberWithUnits.utils'; +import { isNumber } from '../../validator/isNumber'; +import { FormatNumberWithUnitsOptions } from './formatNumberWithUnits.types'; + +/** + * @description `숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `단위` 별로 포맷팅하는 함수입니다. + * + * @param {number | string} value - 포맷팅할 숫자 또는 숫자로 이루어진 문자열 + * @param {FormatNumberWithUnitsOptions} options - 포맷팅 옵션 + * @param {Unit[]} [options.units=DEFAULT_UNITS] - 사용할 단위 배열(조,억,만). 직접 정의해서 사용할 수 있습니다. + * @param {boolean} [options.commas=true] - 천 단위 구분 쉼표 사용 여부입니다. + * @param {number} [options.floorUnit=1] - 버림 단위이며, 10의 제곱수를 단위로 사용합니다. 만약, 버림 단위보다 작은 숫자는 '0'으로 반환합니다. + * @param {boolean} [options.space=true] - 단위 사이 공백 추가 여부입니다. + * @param {number} [options.decimal=0] - 소수점 자리수입니다. 버림 단위가 1보다 클 경우 소수점 자리수를 적용하지 않습니다. + * @returns {string} 포맷팅된 문자열 + * @throws 입력값이 올바른 형식이 아니거나 floorUnit, decimal이 올바른 형식이 아닐 경우 에러 발생 + * + * @example + * // 기본 동작 + * const KRW_UNITS = [ + * { unit: '조', value: 1_000_000_000_000 }, + * { unit: '억', value: 100_000_000 }, + * { unit: '만', value: 10_000 }, + * ] as const; + * + * formatNumberWithUnits(1234567) // "1,234,567" + * formatNumberWithUnits(1234567, { units: KRW_UNITS }) // "123만 4,567" + * formatNumberWithUnits('1234567', { units: KRW_UNITS }) // "123만 4,567" + * + * @example + * // 단위 사이 공백 추가 (기본값: true) + * formatNumberWithUnits(1234567, { units: KRW_UNITS, space: true }) // "123만 4,567" + * formatNumberWithUnits(1234567, { units: KRW_UNITS, space: false }) // "123만4,567" + * + * @example + * // 쉼표 사용 여부 (기본값: true) + * formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: false }) // "123만 4567" + * formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: true }) // "123만 4,567" + * + * @example + * // 버림 단위 (기본값: 1) + * formatNumberWithUnits(1234567, { units: KRW_UNITS, floorUnit: 10000 }) // "123만" + * + * @example + * // 소수점 자리수 (기본값: 0) + * formatNumberWithUnits(1234567.123, { units: KRW_UNITS, decimal: 2 }) // "123만 4,567.12" + * + * // floorUnit이 1보다 크면 소수점 자리수를 적용하지 않습니다. + * formatNumberWithUnits(1234567.123, { + * units: KRW_UNITS, + * decimal: 3, + * floorUnit: 1000 + * }) // "123만 4,000" + */ +export function formatNumberWithUnits( + value: number | string, + options: FormatNumberWithUnitsOptions = {} +): string { + const { + units = [], + space = true, + commas = true, + floorUnit = 1, + decimal = 0, + } = options; + + // value 값을 기준으로 내림차순 정렬 + const sortedUnits = [...units].sort((a, b) => b.value - a.value); + + const valueToUse = isNumber(value) ? value : Number(value); + // 에러 처리 + if (isNaN(valueToUse)) { + throw new Error('value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.'); + } + + const isValidFloorUnit = + !Number.isInteger(floorUnit) || + floorUnit < 1 || + (floorUnit !== 1 && floorUnit % 10 !== 0); + + if (isValidFloorUnit) { + throw new Error('floorUnit은 1을 포함한 10의 제곱수여야 합니다.'); + } + + if (!Number.isInteger(decimal) || decimal < 0) { + throw new Error('decimal은 0 이상의 정수여야 합니다.'); + } + + const formattedResult = getFormattedValueWithUnits(valueToUse, sortedUnits, { + commas, + space, + decimal, + floorUnit, + }); + + return formattedResult; +} diff --git a/packages/utils/src/formatter/index.ts b/packages/utils/src/formatter/index.ts index d9c13d3b7..4c9192339 100644 --- a/packages/utils/src/formatter/index.ts +++ b/packages/utils/src/formatter/index.ts @@ -1,4 +1,4 @@ -export * from './formatNumberByUnits'; -export * from './formatNumberCurrency'; export * from './formatNumberWithCommas'; +export * from './formatNumberWithCurrency'; +export * from './formatNumberWithUnits'; export * from './formatPhoneNumber'; diff --git a/packages/utils/src/string/reverseString/reverseString.spec.ts b/packages/utils/src/string/reverseString/reverseString.spec.ts index 20dad26f2..262c7648e 100644 --- a/packages/utils/src/string/reverseString/reverseString.spec.ts +++ b/packages/utils/src/string/reverseString/reverseString.spec.ts @@ -9,6 +9,13 @@ describe('reverseString', () => { expect(reversedString).toBe('다나가CBA'); }); + it('빈 문자열에 대해 빈 문자열을 반환해야 합니다.', () => { + const emptyString = ''; + const reversedString = reverseString(emptyString); + + expect(reversedString).toBe(''); + }); + it('특수 문자가 포함된 문자열에 대해 반전된 문자열을 반환해야 합니다.', () => { const stringWithSpecialCharacter = 'A!B@C'; const reversedString = reverseString(stringWithSpecialCharacter); diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts index c586873e0..85132775c 100644 --- a/packages/utils/vitest.config.ts +++ b/packages/utils/vitest.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ 'src/file', 'src/**/internal.ts', 'src/**/*.bench.ts', + 'src/**/*.utils.ts', ...coverageConfigDefaults.exclude, ], },