diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6d96350e..1bba496f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -48,3 +48,30 @@ ### 2.2 Description A clear and concise description of what the pr is about. + +## 3. Convention + +함수명에는 특별한 이유가 없다면 hangul을 포함하지 않습니다. + +```ts +// Don't +function getHangulSimilarity(); +// Do +function getSimilarity(); + +// Don't +function disassembleHangul(); +// Do +function disassemble(); +``` + +함수명을 지을 때 아래와 같이 import될 것을 고려해야 합니다. + +```ts +import { getSimilarity, disassemble, josa } from 'es-hangul' // 따로 나눠서도 제공 +import hangul from 'es-hangul' // hangul default export에 묶어서도 제공 + +hangul.getSimilarity(...) +hangul.disassemble(...) +hangul.josa(...) + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d921fbd..2a30ae47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # es-hangul +## 1.4.1 + +### Patch Changes + +- [#157](https://github.com/toss/es-hangul/pull/157) [`f7e60ae`](https://github.com/toss/es-hangul/commit/f7e60aeca9f315ac1e34eba0a1f8a82f55d79956) Thanks [@manudeli](https://github.com/manudeli)! - fix: 패키지가 노출하는 인터페이스를 명확히 하기 위해 index.ts를 named export로 수정합니다 + +## 1.4.0 + +### Minor Changes + +- [#144](https://github.com/toss/es-hangul/pull/144) [`b114897`](https://github.com/toss/es-hangul/commit/b1148973e6c2b640ce528fc8ba4b8e2e034b90de) Thanks [@Collection50](https://github.com/Collection50)! - fix: amountToHangul이 소수점, 숫자도 대응할 수 있도록 수정 + +## 1.3.10 + +### Patch Changes + +- [#148](https://github.com/toss/es-hangul/pull/148) [`f3c7fe9`](https://github.com/toss/es-hangul/commit/f3c7fe9f73138b932af817b8ac925d54c3283151) Thanks [@KNU-K](https://github.com/KNU-K)! - fix : getHangulacronym함수를 acronymizeHangul 메서드로 대체합니다 + +## 1.3.9 + +### Patch Changes + +- [#130](https://github.com/toss/es-hangul/pull/130) [`acd6edb`](https://github.com/toss/es-hangul/commit/acd6edb1d8aadced517f6b57a49c01152ff19d0a) Thanks [@Collection50](https://github.com/Collection50)! - feat: 문자열에서 한글만 반환하는 extractHangul을 구현합니다. + +## 1.3.8 + +### Patch Changes + +- [#133](https://github.com/toss/es-hangul/pull/133) [`b0e1184`](https://github.com/toss/es-hangul/commit/b0e1184204be0cb9f3c13937888c83c8a94e7ca6) Thanks [@KNU-K](https://github.com/KNU-K)! - feat : 문장의 각 단어 중 첫 문자만 뽑는 extractHangul 함수를 추가합니다. + +## 1.3.7 + +### Patch Changes + +- [#124](https://github.com/toss/es-hangul/pull/124) [`0f38431`](https://github.com/toss/es-hangul/commit/0f38431ee611cb89c7e121fd02ab34f749a0c386) Thanks [@crucifyer](https://github.com/crucifyer)! - fix: 코드 효율 개선 + ## 1.3.6 ### Patch Changes diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e2fbde80..8009477c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,47 @@ # docs +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`f7e60ae`](https://github.com/toss/es-hangul/commit/f7e60aeca9f315ac1e34eba0a1f8a82f55d79956)]: + - es-hangul@1.4.1 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`b114897`](https://github.com/toss/es-hangul/commit/b1148973e6c2b640ce528fc8ba4b8e2e034b90de)]: + - es-hangul@1.4.0 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies [[`f3c7fe9`](https://github.com/toss/es-hangul/commit/f3c7fe9f73138b932af817b8ac925d54c3283151)]: + - es-hangul@1.3.10 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`acd6edb`](https://github.com/toss/es-hangul/commit/acd6edb1d8aadced517f6b57a49c01152ff19d0a)]: + - es-hangul@1.3.9 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`b0e1184`](https://github.com/toss/es-hangul/commit/b0e1184204be0cb9f3c13937888c83c8a94e7ca6)]: + - es-hangul@1.3.8 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`0f38431`](https://github.com/toss/es-hangul/commit/0f38431ee611cb89c7e121fd02ab34f749a0c386)]: + - es-hangul@1.3.7 + ## 0.1.2 ### Patch Changes diff --git a/docs/package.json b/docs/package.json index 2b0bc353..9269fa8e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "0.1.2", + "version": "0.1.8", "private": true, "packageManager": "yarn@4.1.1", "scripts": { diff --git a/docs/public/favicon-dark.png b/docs/public/favicon-dark.png new file mode 100644 index 00000000..07608185 Binary files /dev/null and b/docs/public/favicon-dark.png differ diff --git a/docs/public/favicon-dark.svg b/docs/public/favicon-dark.svg new file mode 100644 index 00000000..c5c553b4 --- /dev/null +++ b/docs/public/favicon-dark.svg @@ -0,0 +1,2 @@ + +es-hangul \ No newline at end of file diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/docs/public/favicon.ico and /dev/null differ diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 00000000..e6aa5e00 --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1,2 @@ + +es-hangul \ No newline at end of file diff --git a/docs/public/next.svg b/docs/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/docs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/vercel.svg b/docs/public/vercel.svg deleted file mode 100644 index d2f84222..00000000 --- a/docs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/pages/docs/api/acronymizeHangul.en.mdx b/docs/src/pages/docs/api/acronymizeHangul.en.mdx new file mode 100644 index 00000000..57d026c9 --- /dev/null +++ b/docs/src/pages/docs/api/acronymizeHangul.en.mdx @@ -0,0 +1,16 @@ +# acronymizeHangul + +It receives the Korean sentence and returns the first letter of that Korean sentence. +(We don't deal with non-Korean sentences; we don't deal with additional Korean + English sentences.) + +```typescript +function acronymizeHangul( + // String consisting of plural nouns (e.g. '버스 충전', '치킨과 맥주') + hangul: string +): boolean; +``` + +```typescript +acronymizeHangul('치킨과 맥주').join(''); //치맥 +acronymizeHangul('버스 충전 카드').join(''); //버충카 +``` diff --git a/docs/src/pages/docs/api/acronymizeHangul.ko.mdx b/docs/src/pages/docs/api/acronymizeHangul.ko.mdx new file mode 100644 index 00000000..908e5251 --- /dev/null +++ b/docs/src/pages/docs/api/acronymizeHangul.ko.mdx @@ -0,0 +1,16 @@ +# acronymizeHangul + +한글 문장을 입력받아서, 해당 한글 문장의 첫글자를 리턴해줍니다. +(한글 문장이 아닌, 문장은 취급하지않습니다. 추가로 한글 문장 + 영어 문장의 경우에도 취급하지않습니다.) + +```typescript +function acronymizeHangul( + // 복수 명사로 이루어진 문자열 (e.g. '버스 충전', '치킨과 맥주') + hangul: string +): boolean; +``` + +```typescript +acronymizeHangul('치킨과 맥주').join(''); //치맥 +acronymizeHangul('버스 충전 카드').join(''); //버충카 +``` diff --git a/docs/src/pages/docs/api/amountToHangul.en.md b/docs/src/pages/docs/api/amountToHangul.en.md index c82c4dc4..19e3abcc 100644 --- a/docs/src/pages/docs/api/amountToHangul.en.md +++ b/docs/src/pages/docs/api/amountToHangul.en.md @@ -9,16 +9,14 @@ Converts numeric amounts to the Korean reading of the [National Institute of Kor For detailed examples, see below. ```typescript -function amountToHangul( - // A string of numeric amounts - str: string -): string; +function amountToHangul(amount: string | number): string; ``` ## Examples ```tsx -amountToHangul('15,201,100'); // '일천오백이십만천백'; -amountToHangul('120,030원'); // '일십이만삼십' - Ignore non-numeric characters -amountToHangul('392.24'); // '삼백구십이' - Ignore decimals +amountToHangul('15,201,100'); // '일천오백이십만천백' +amountToHangul('120,030원'); // '일십이만삼십' +amountToHangul('12345.6789'); // '일만이천삼백사십오점육칠팔구' +amountToHangul(15_201_100); // '일천오백이십만천백'' ``` diff --git a/docs/src/pages/docs/api/amountToHangul.ko.md b/docs/src/pages/docs/api/amountToHangul.ko.md index 9818d26d..dbafc07e 100644 --- a/docs/src/pages/docs/api/amountToHangul.ko.md +++ b/docs/src/pages/docs/api/amountToHangul.ko.md @@ -4,21 +4,19 @@ title: amountToHangul # amountToHangul -숫자로 된 금액을 [국립국어원](https://ko.dict.naver.com/#/correct/korean/info?seq=602) 규칙의 한글 읽기로 변환합니다. +숫자나 문자를 [국립국어원](https://ko.dict.naver.com/#/correct/korean/info?seq=602) 규칙의 한글 읽기 문자열로 변환합니다. 자세한 예시는 아래 Example을 참고하세요. ```typescript -function amountToHangul( - // 숫자로 된 금액 문자열 - str: string -): string; +function amountToHangul(amount: string | number): string; ``` ## Examples ```tsx -amountToHangul('15,201,100'); // '일천오백이십만천백'; -amountToHangul('120,030원'); // '일십이만삼십' - 숫자 외 문자 무시 -amountToHangul('392.24'); // '삼백구십이' - 소수점 무시 +amountToHangul('15,201,100'); // '일천오백이십만천백' +amountToHangul('120,030원'); // '일십이만삼십' +amountToHangul('12345.6789'); // '일만이천삼백사십오점육칠팔구' +amountToHangul(15_201_100); // '일천오백이십만천백'' ``` diff --git a/docs/src/pages/docs/api/extractHangul.en.md b/docs/src/pages/docs/api/extractHangul.en.md new file mode 100644 index 00000000..c685c84a --- /dev/null +++ b/docs/src/pages/docs/api/extractHangul.en.md @@ -0,0 +1,23 @@ +--- +title: extractHangul +--- + +# extractHangul + +Extracts and returns only Korean characters from the string. + +For detailed examples, see below. + +```typescript +function extractHangul(str: string): string; +``` + +## Examples + +```tsx +extractHangul('안녕하세요1234abc'); // '안녕하세요' +extractHangul('abcde'); // '' +extractHangul('안녕하세요ㄱㄴ'); // '안녕하세요ㄱㄴ' +extractHangul('안녕하세요 만나서 반갑습니다'); // '안녕하세요 만나서 반갑습니다' +extractHangul('가나다!-29~라마바.,,사'); // '가나다라마바사' +``` diff --git a/docs/src/pages/docs/api/extractHangul.ko.md b/docs/src/pages/docs/api/extractHangul.ko.md new file mode 100644 index 00000000..dc089bdb --- /dev/null +++ b/docs/src/pages/docs/api/extractHangul.ko.md @@ -0,0 +1,23 @@ +--- +title: extractHangul +--- + +# extractHangul + +문자열에서 한글만 추출하여 반환합니다. + +자세한 예시는 아래 Example을 참고하세요. + +```typescript +function extractHangul(str: string): string; +``` + +## Examples + +```tsx +extractHangul('안녕하세요1234abc'); // '안녕하세요' +extractHangul('abcde'); // '' +extractHangul('안녕하세요ㄱㄴ'); // '안녕하세요ㄱㄴ' +extractHangul('안녕하세요 만나서 반갑습니다'); // '안녕하세요 만나서 반갑습니다' +extractHangul('가나다!-29~라마바.,,사'); // '가나다라마바사' +``` diff --git a/docs/src/pages/docs/api/josa.ko.md b/docs/src/pages/docs/api/josa.ko.md index 91adb784..bb094723 100644 --- a/docs/src/pages/docs/api/josa.ko.md +++ b/docs/src/pages/docs/api/josa.ko.md @@ -18,7 +18,6 @@ function josa( | '으로/로' | '와/과' | '이나/나' - | '이에/에' | '이란/란' | '아/야' | '이랑/랑' diff --git a/package.json b/package.json index 3888a6dd..9f2f69e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "es-hangul", - "version": "1.3.6", + "version": "1.4.1", "keywords": [ "한글", "한국어", diff --git a/src/_internal/hangul.spec.ts b/src/_internal/hangul.spec.ts index 7cf63bbe..e5307cf0 100644 --- a/src/_internal/hangul.spec.ts +++ b/src/_internal/hangul.spec.ts @@ -1,4 +1,13 @@ -import { binaryAssembleHangulCharacters, binaryAssembleHangul, isHangulAlphabet, isHangulCharacter } from './hangul'; +import { + binaryAssembleHangulCharacters, + binaryAssembleHangul, + isHangulAlphabet, + isHangulCharacter, + isHangul, + assertHangul, + parseHangul, + safeParseHangul, +} from './hangul'; describe('isHangul*', () => { it('isHangulCharacter는 완성된 한글 문자를 받으면 true를 반환한다.', () => { @@ -16,6 +25,51 @@ describe('isHangul*', () => { expect(isHangulAlphabet('ㅏ')).toBe(true); expect(isHangulAlphabet('a')).toBe(false); }); + + it('isHangul은 한글 문자열을 받으면 true를 반환한다.', () => { + expect(isHangul('값')).toBe(true); + expect(isHangul('ㄱ')).toBe(true); + expect(isHangul('ㅏ')).toBe(true); + expect(isHangul('저는 고양이를 좋아합니다')).toBe(true); + expect(isHangul('a')).toBe(false); + expect(isHangul(111)).toBe(false); + expect(isHangul([111, 111])).toBe(false); + expect(isHangul({ a: 111 })).toBe(false); + }); +}); + +describe('parse', () => { + it('parseHangul은 한글 문자열을 받으면 그대로 반환한다.', () => { + expect(parseHangul('값')).toBe('값'); + expect(parseHangul('ㄱ')).toBe('ㄱ'); + expect(parseHangul('ㅏ')).toBe('ㅏ'); + expect(parseHangul('저는 고양이를 좋아합니다')).toBe('저는 고양이를 좋아합니다'); + }); + + it('parseHangul은 한글 문자열이 아닌 값을 받으면 에러를 발생시킨다.', () => { + expect(() => parseHangul(111)).toThrowError('111 is not a valid hangul string'); + expect(() => parseHangul([111, 111])).toThrowError('[111,111] is not a valid hangul string'); + expect(() => parseHangul({ a: 111 })).toThrowError('{"a":111} is not a valid hangul string'); + }); + + it('safeParseHangul은 한글 문자열을 받으면 성공 객체를 반환한다.', () => { + expect(safeParseHangul('값')).toEqual({ success: true, data: '값' }); + expect(safeParseHangul('ㄱ')).toEqual({ success: true, data: 'ㄱ' }); + expect(safeParseHangul('ㅏ')).toEqual({ success: true, data: 'ㅏ' }); + expect(safeParseHangul('저는 고양이를 좋아합니다')).toEqual({ success: true, data: '저는 고양이를 좋아합니다' }); + }); + + it('safeParseHangul은 한글 문자열이 아닌 값을 받으면 실패 객체를 반환한다.', () => { + expect(safeParseHangul(111)).toEqual({ success: false, error: Error('111 is not a valid hangul string') }); + expect(safeParseHangul([111, 111])).toEqual({ + success: false, + error: Error('[111,111] is not a valid hangul string'), + }); + expect(safeParseHangul({ a: 111 })).toEqual({ + success: false, + error: Error('{"a":111} is not a valid hangul string'), + }); + }); }); describe('binaryAssembleHangulCharacters', () => { @@ -107,4 +161,20 @@ describe('binaryAssembleHangul', () => { expect(binaryAssembleHangul('저는 고양이를 좋아하', 'ㅏ')).toEqual('저는 고양이를 좋아하ㅏ'); expect(binaryAssembleHangul('저는 고양이를 좋아합니다', 'ㅜ')).toEqual('저는 고양이를 좋아합니다ㅜ'); }); + + describe('assertHangul', () => { + it('한글 문자열을 받으면 에러를 발생시키지 않는다.', () => { + expect(() => assertHangul('ㄱ')).not.toThrow(); + expect(() => assertHangul('고양이')).not.toThrow(); + expect(() => assertHangul('저는 고양이를 좋아합니다')).not.toThrow(); + expect(() => assertHangul('저는 고양이를 좋아합니ㄷ')).not.toThrow(); + }); + + it("한글 문자열이 아닌 값을 받으면 '___ is not a valid hangul string' 에러를 발생시킨다.", () => { + expect(() => assertHangul('aaaaaa')).toThrowError('"aaaaaa" is not a valid hangul string'); + expect(() => assertHangul(111)).toThrowError('111 is not a valid hangul string'); + expect(() => assertHangul([111, 111])).toThrowError('[111,111] is not a valid hangul string'); + expect(() => assertHangul({ a: 111 })).toThrowError('{"a":111} is not a valid hangul string'); + }); + }); }); diff --git a/src/_internal/hangul.ts b/src/_internal/hangul.ts index 6a1479ca..1a746f5b 100644 --- a/src/_internal/hangul.ts +++ b/src/_internal/hangul.ts @@ -12,6 +12,40 @@ export function isHangulAlphabet(character: string) { return /^[ㄱ-ㅣ]$/.test(character); } +export function isHangul(actual: unknown): actual is string { + return typeof actual === 'string' && /^[가-힣ㄱ-ㅣ\s]+$/.test(actual); +} + +export function assertHangul(actual: unknown, message?: string): asserts actual is string { + assert(isHangul(actual), message || `${JSON.stringify(actual)} is not a valid hangul string`); +} + +export function parseHangul(actual: unknown): string { + assertHangul(actual); + return actual; +} + +type SafeParseSuccess = { + success: true; + data: string; + error?: never; +}; + +type SafeParseError = { + success: false; + error: unknown; + data?: never; +}; + +export function safeParseHangul(actual: unknown): SafeParseSuccess | SafeParseError { + try { + const parsedHangul = parseHangul(actual); + return { success: true, data: parsedHangul }; + } catch (error) { + return { success: false, error }; + } +} + /** * @name binaryAssembleHangulAlphabets * @description diff --git a/src/_internal/index.spec.ts b/src/_internal/index.spec.ts new file mode 100644 index 00000000..6c6bc393 --- /dev/null +++ b/src/_internal/index.spec.ts @@ -0,0 +1,61 @@ +import assert, { excludeLastElement, isBlank, joinString } from './index'; + +describe('excludeLastElement', () => { + it('마지막 요소를 제외한 모든 요소와 마지막 요소를 반환한다', () => { + const result = excludeLastElement(['apple', 'banana', 'cherry']); + + expect(result).toEqual([['apple', 'banana'], 'cherry']); + }); + + it('입력 배열이 비어 있으면 빈 배열과 빈 문자열을 반환한다', () => { + const result = excludeLastElement([]); + + expect(result).toEqual([[], '']); + }); + + it('배열에 단 하나의 요소만 있는 경우, 빈배열과 그 요소를 반환한다', () => { + const result = excludeLastElement(['apple']); + + expect(result).toEqual([[], 'apple']); + }); +}); + +describe('joinString', () => { + it('여러 문자열을 하나의 문자열로 연결한다', () => { + const result = joinString('Hello', ' ', 'World'); + + expect(result).toBe('Hello World'); + }); + + it('인자가 주어지지 않았을 때 빈 문자열을 반환한다', () => { + const result = joinString(); + + expect(result).toBe(''); + }); +}); + +describe('isBlank', () => { + it('문자가 공백이면 true를 반환한다', () => { + expect(isBlank(' ')).toBe(true); + }); + + it('문자가 공백이 아니면 false를 반환한다', () => { + expect(isBlank('a')).toBe(false); + }); +}); + +describe('assert', () => { + it('조건이 참이면 에러를 던지지 않는다', () => { + expect(() => assert(true)).not.toThrowError(); + }); + + it('조건이 거짓이면 에러를 던진다', () => { + expect(() => assert(false)).toThrowError('Invalid condition'); + }); + + it('조건이 거짓이고 에러 메시지가 제공된 경우 사용자 정의 에러 메시지를 던져야 한다', () => { + const customMessage = 'Custom error message'; + + expect(() => assert(false, customMessage)).toThrowError(customMessage); + }); +}); diff --git a/src/acronymizeHangul.spec.ts b/src/acronymizeHangul.spec.ts new file mode 100644 index 00000000..a4d8b91e --- /dev/null +++ b/src/acronymizeHangul.spec.ts @@ -0,0 +1,18 @@ +import { acronymizeHangul } from './acronymizeHangul'; + +describe('acronymizeHangul', () => { + it('한글 문장 단어중 첫 문자만 뽑은 리스트를 반환', () => { + expect(acronymizeHangul('치킨과 맥주')).toHaveLength(2); + expect(acronymizeHangul('치킨과 맥주').join('')).toBe('치맥'); + + expect(acronymizeHangul('버스 충전 카드')).toHaveLength(3); + expect(acronymizeHangul('버스 충전 카드').join('')).toBe('버충카'); + }); + it('한글이 아닌 문장 넣었을 때', () => { + expect(() => acronymizeHangul('test test')).toThrowError('"test test" is not a valid hangul string'); + }); + + it('한글과 영어가 섞인 문장을 넣었을 때', () => { + expect(() => acronymizeHangul('고기와 Cheese')).toThrowError('"고기와 Cheese" is not a valid hangul string'); + }); +}); diff --git a/src/acronymizeHangul.ts b/src/acronymizeHangul.ts new file mode 100644 index 00000000..9103ec40 --- /dev/null +++ b/src/acronymizeHangul.ts @@ -0,0 +1,12 @@ +import { parseHangul } from './_internal/hangul'; + +/** + * + * @param getHangulAcronym + * @description + * 한글 문장을 입력받아서, 해당 한글 문장의 초성을을 리턴해줍니다. + * 한글 문장이 아닌, 문장은 취급하지않습니다. 추가로 한글 문장 + 영어 문장의 경우에도 취급하지않습니다. + */ +export function acronymizeHangul(hangul: string) { +return parseHangul(hangul).split(' ').map(word => word.charAt(0)); +} diff --git a/src/amountToHangul.spec.ts b/src/amountToHangul.spec.ts index 9fcb3bbc..0ba591a6 100644 --- a/src/amountToHangul.spec.ts +++ b/src/amountToHangul.spec.ts @@ -1,16 +1,29 @@ import { amountToHangul } from './amountToHangul'; describe('amountToHangul', () => { - it('숫자로 된 금액을 한글로 표기', () => { + it('금액 문자열을 한글로 표기', () => { expect(amountToHangul('15,201,100')).toEqual('일천오백이십만천백'); - expect(amountToHangul('120,030원')).toEqual('일십이만삼십'); // 숫자 외 문자 무시 - expect(amountToHangul('392.24')).toEqual('삼백구십이'); // 소수점 무시 expect(amountToHangul('100000000')).toEqual('일억'); expect(amountToHangul('100000100')).toEqual('일억백'); }); it('숫자로 된 금액이 80글자를 넘을 시 에러 발생', () => { - const longNumberString = '1'.repeat(81); + const longNumberString = '1'.repeat(81); expect(() => amountToHangul(longNumberString)).toThrowError(`convert range exceeded : ${longNumberString}`); + }) + + it('숫자 외 문자를 무시하여 반환', () => { + expect(amountToHangul('120,030원')).toEqual('일십이만삼십'); + }); + + it('소수점이 있는 경우도 표기', () => { + expect(amountToHangul('392.24')).toEqual('삼백구십이점이사'); + expect(amountToHangul('12345.6789')).toEqual('일만이천삼백사십오점육칠팔구'); + }); + + it('금액 숫자를 한글로 표기', () => { + expect(amountToHangul(15_201_100)).toEqual('일천오백이십만천백'); + expect(amountToHangul(100000100)).toEqual('일억백'); + expect(amountToHangul(392.24)).toEqual('삼백구십이점이사'); }); }); diff --git a/src/amountToHangul.ts b/src/amountToHangul.ts index 8708e817..c0afbd53 100644 --- a/src/amountToHangul.ts +++ b/src/amountToHangul.ts @@ -1,35 +1,71 @@ -export const HANGUL_DIGITS = ['', '만', '억', '조', '경', '해', '자', '양', '구', '간', '정', '재', '극', '항하사', '아승기', '나유타', '불가사의', '무량대수', '겁', '업']; +export const HANGUL_DIGITS = [ + '', + '만', + '억', + '조', + '경', + '해', + '자', + '양', + '구', + '간', + '정', + '재', + '극', + '항하사', + '아승기', + '나유타', + '불가사의', + '무량대수', + '겁', + '업', +]; export const HANGUL_DIGITS_MAX = HANGUL_DIGITS.length * 4; export const HANGUL_NUMBERS = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']; export const HANGUL_CARDINAL = ['', '십', '백', '천']; -// https://ko.dict.naver.com/#/correct/korean/info?seq=602 -// https://github.com/crucifyer/koreanCardinalOrdinal -export function amountToHangul(str: string) { - str = str.replace(/\..*$/, '') // 소수점 지원 안함 - .replace(/[^\d]+/g, ''); // , 표기 등 오류내지 않음 - if(str.length > HANGUL_DIGITS_MAX) { - throw new Error('convert range exceeded : ' + str); +export function amountToHangul(amount: string | number) { + const [integerPart, decimalPart] = String(amount) + .replace(/[^\d.]+/g, '') + .split('.'); + + if (integerPart.length > HANGUL_DIGITS_MAX) { + throw new Error(`convert range exceeded : ${amount}`); } + const result = []; let pronunDigits = true; - for(let i = 0; i < str.length - 1; i ++) { - const d = str.length - i - 1; - if(str[i] > '1' || d % 4 === 0 || i === 0) { - const tnum = HANGUL_NUMBERS[parseInt(str[i])]; - if(tnum) { - result.push(tnum); + + for (let i = 0; i < integerPart.length - 1; i++) { + const digit = integerPart.length - i - 1; + + if (integerPart[i] > '1' || digit % 4 === 0 || i === 0) { + const hangulNumber = HANGUL_NUMBERS[Number(integerPart[i])]; + + if (hangulNumber) { + result.push(hangulNumber); pronunDigits = true; } } - if(pronunDigits && d % 4 === 0) { - result.push(HANGUL_DIGITS[d / 4]); + + if (pronunDigits && digit % 4 === 0) { + result.push(HANGUL_DIGITS[digit / 4]); pronunDigits = false; } - if(str[i] !== '0') { - result.push(HANGUL_CARDINAL[d % 4]); + + if (integerPart[i] !== '0') { + result.push(HANGUL_CARDINAL[digit % 4]); + } + } + result.push(HANGUL_NUMBERS[Number(integerPart[integerPart.length - 1])]); + + if (decimalPart) { + result.push('점'); + + for (let i = 0; i < decimalPart.length; i++) { + result.push(HANGUL_NUMBERS[Number(decimalPart[i])]); } } - result.push(HANGUL_NUMBERS[parseInt(str[str.length - 1])]); + return result.join(''); } diff --git a/src/chosungIncludes.ts b/src/chosungIncludes.ts index 254d1f30..3617a0c0 100644 --- a/src/chosungIncludes.ts +++ b/src/chosungIncludes.ts @@ -2,13 +2,13 @@ import { disassembleHangulToGroups } from './disassemble'; import { canBeChosung, getChosung } from './utils'; export function chosungIncludes(x: string, y: string) { - const trimmedY = y.replace(/\s/g, ''); + const trimmedY = y.replace(/\s+/g, ''); if (!isOnlyChosung(trimmedY)) { return false; } - const chosungX = getChosung(x).replace(/\s/g, ''); + const chosungX = getChosung(x).replace(/\s+/g, ''); const chosungY = trimmedY; return chosungX.includes(chosungY); diff --git a/src/constants.ts b/src/constants.ts index 217defe3..6579e3cc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,16 @@ export const COMPLETE_HANGUL_END_CHARCODE = '힣'.charCodeAt(0); export const NUMBER_OF_JONGSUNG = 28; export const NUMBER_OF_JUNGSUNG = 21; +const _JASO_HANGUL_NFD = [...'각힣'.normalize('NFD')].map(char => char.charCodeAt(0)); // NFC 에 정의되지 않은 문자는 포함하지 않음 +export const JASO_HANGUL_NFD = { + START_CHOSEONG: _JASO_HANGUL_NFD[0], // ㄱ + START_JUNGSEONG: _JASO_HANGUL_NFD[1], // ㅏ + START_JONGSEONG: _JASO_HANGUL_NFD[2], // ㄱ + END_CHOSEONG: _JASO_HANGUL_NFD[3], // ㅎ + END_JUNGSEONG: _JASO_HANGUL_NFD[4], // ㅣ + END_JONGSEONG: _JASO_HANGUL_NFD[5], // ㅎ +} + /** * ㄱ -> 'ㄱ' * ㄳ -> 'ㄱㅅ' 으로 나눈다. diff --git a/src/extractHangul.spec.ts b/src/extractHangul.spec.ts new file mode 100644 index 00000000..b3fd84dd --- /dev/null +++ b/src/extractHangul.spec.ts @@ -0,0 +1,27 @@ +import { extractHangul } from './extractHangul'; + +describe('extractHangul', () => { + it('숫자와 알파벳과 특수문자를 제외한 한글 반환', () => { + expect(extractHangul('안녕하세요1234abc!@#')).toBe('안녕하세요'); + }); + + it('한글이 없는 문자열', () => { + expect(extractHangul('1234abc')).toBe(''); + }); + + it('한글과 공백을 제외한 다른 문자는 제거', () => { + expect(extractHangul('한글과 영어가 섞인 문장입니다. Hello!')).toBe('한글과 영어가 섞인 문장입니다 '); + }); + + it('escape 문자열 유지', () => { + expect(extractHangul('한글과\n\t줄바꿈')).toBe('한글과\n\t줄바꿈'); + }); + + it('모음은 제거하지 않음', () => { + expect(extractHangul('ㅠㅠ')).toBe('ㅠㅠ'); + }); + + it('자음은 제거하지 않음', () => { + expect(extractHangul('ㄱㄴㄱㄴ')).toBe('ㄱㄴㄱㄴ'); + }); +}); diff --git a/src/extractHangul.ts b/src/extractHangul.ts new file mode 100644 index 00000000..d023a674 --- /dev/null +++ b/src/extractHangul.ts @@ -0,0 +1,18 @@ +/** + * @name extractHangul + * @description + * 문자열을 입력받고 한글만 추출해 반환합니다. + * + * @param {string} chars 모든 문자열 + * + * @example + * extractHangul('안녕하세요1234abc') // '안녕하세요' + * extractHangul('abcde') // '' + * extractHangul('안녕하세요ㄱㄴ') // '안녕하세요ㄱㄴ' + * extractHangul('안녕하세요 만나서 반갑습니다') // '안녕하세요 만나서 반갑습니다' + * extractHangul('가나다!-29~라마바.,,사') // '가나다라마바사' + */ + +export function extractHangul(str: string): string { + return str.replace(/[^ㄱ-ㅎㅏ-ㅣ가-힣\s]+/g, ''); +} diff --git a/src/index.ts b/src/index.ts index be7f02a3..5e839a05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,21 @@ -export * from './assemble'; -export * from './chosungIncludes'; -export * from './combineHangulCharacter'; -export * from './convertQwertyToHangulAlphabet'; -export * from './disassemble'; -export * from './disassembleCompleteHangulCharacter'; -export * from './hangulIncludes'; -export * from './josa'; -export * from './removeLastHangulCharacter'; -export * from './utils'; +export { assembleHangul } from './assemble'; +export { chosungIncludes } from './chosungIncludes'; +export { combineHangulCharacter, combineVowels, curriedCombineHangulCharacter } from './combineHangulCharacter'; +export { convertQwertyToHangul, convertQwertyToHangulAlphabet } from './convertQwertyToHangulAlphabet'; +export { disassembleHangul, disassembleHangulToGroups } from './disassemble'; +export { disassembleCompleteHangulCharacter } from './disassembleCompleteHangulCharacter'; +export { hangulIncludes } from './hangulIncludes'; +export { josa } from './josa'; +export { removeLastHangulCharacter } from './removeLastHangulCharacter'; +export { + canBeChosung, + canBeJongsung, + canBeJungsung, + getChosung, + hasBatchim, + hasProperty, + hasSingleBatchim, + hasValueInReadOnlyStringList, +} from './utils'; +export { extractHangul } from './extractHangul'; +export { acronymizeHangul } from './acronymizeHangul'; diff --git a/src/removeLastHangulCharacter.spec.ts b/src/removeLastHangulCharacter.spec.ts index 17ce98f7..0445bc43 100644 --- a/src/removeLastHangulCharacter.spec.ts +++ b/src/removeLastHangulCharacter.spec.ts @@ -3,6 +3,7 @@ import { removeLastHangulCharacter } from './removeLastHangulCharacter'; describe('removeLastHangulCharacter', () => { it('마지막 문자가 겹받침인 경우 홑받침으로 바꾼다.', () => { expect(removeLastHangulCharacter('안녕하세요 값')).toBe('안녕하세요 갑'); + expect(removeLastHangulCharacter('안녕하세요 값이')).toBe('안녕하세요 값ㅇ'); }); it('마지막 문자가 초성과 중성의 조합으로 끝날 경우 초성만 남긴다.', () => { expect(removeLastHangulCharacter('프론트엔드')).toBe('프론트엔ㄷ'); diff --git a/src/removeLastHangulCharacter.ts b/src/removeLastHangulCharacter.ts index 20c7ac15..59838522 100644 --- a/src/removeLastHangulCharacter.ts +++ b/src/removeLastHangulCharacter.ts @@ -20,25 +20,13 @@ import { excludeLastElement } from './_internal'; * removeLastHangulCharacter('신세계') // 신세ㄱ */ export function removeLastHangulCharacter(words: string) { - const disassembledGroups = disassembleHangulToGroups(words); - const lastCharacter = disassembledGroups[disassembledGroups.length - 1]; - + const lastCharacter = words[words.length - 1]; if (lastCharacter == null) { return ''; } - - const withoutLastCharacter = disassembledGroups - .filter(v => v !== lastCharacter) - .map(([first, middle, last]) => { - if (middle != null) { - return combineHangulCharacter(first, middle, last); - } - - return first; - }); - - const [[first, middle, last]] = excludeLastElement(lastCharacter); + const disassembleLastCharacter = disassembleHangulToGroups(lastCharacter); + const [[first, middle, last]] = excludeLastElement(disassembleLastCharacter[0]); const result = middle != null ? combineHangulCharacter(first, middle, last) : first; - return [...withoutLastCharacter, result].join(''); + return [words.substring(0, words.length - 1), result].join(''); } diff --git a/src/utils.spec.ts b/src/utils.spec.ts index cacb0ce0..58dc04e4 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -34,6 +34,18 @@ describe('hasBatchim', () => { expect(hasBatchim('')).toBe(false); }); }); + + describe('완성된 한글이 아닌 경우', () => { + it('한글이 자음 또는 모음으로만 구성된 경우 false를 반환한다.', () => { + expect(hasBatchim('ㄱ')).toBe(false); + expect(hasBatchim('ㅏ')).toBe(false); + }); + + it('한글 외의 문자를 입력하면 false를 반환한다', () => { + expect(hasBatchim('cat')).toBe(false); + expect(hasBatchim('!')).toBe(false); + }); + }); }); describe('hasSingleBatchim', () => { @@ -54,6 +66,12 @@ describe('hasSingleBatchim', () => { expect(hasSingleBatchim('토')).toBe(false); expect(hasSingleBatchim('서')).toBe(false); }); + + it('한글 외의 문자를 입력하면 false를 반환한다.', () => { + expect(hasSingleBatchim('cat')).toBe(false); + expect(hasSingleBatchim('')).toBe(false); + expect(hasSingleBatchim('?')).toBe(false); + }); }); }); diff --git a/src/utils.ts b/src/utils.ts index 5213d9c8..eb336371 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,22 @@ import { + COMPLETE_HANGUL_START_CHARCODE, + COMPLETE_HANGUL_END_CHARCODE, HANGUL_CHARACTERS_BY_FIRST_INDEX, HANGUL_CHARACTERS_BY_LAST_INDEX, HANGUL_CHARACTERS_BY_MIDDLE_INDEX, + NUMBER_OF_JONGSUNG, + JASO_HANGUL_NFD, } from './constants'; -import { disassembleHangul, disassembleHangulToGroups } from './disassemble'; -import { disassembleCompleteHangulCharacter } from './disassembleCompleteHangulCharacter'; +import { disassembleHangulToGroups } from './disassemble'; + +const EXTRACT_CHOSEONG_REGEX = new RegExp( + `[^\\u${JASO_HANGUL_NFD.START_CHOSEONG.toString(16)}-\\u${JASO_HANGUL_NFD.END_CHOSEONG.toString(16)}ㄱ-ㅎ\\s]+`, + 'ug' +); +const CHOOSE_NFD_CHOSEONG_REGEX = new RegExp( + `[\\u${JASO_HANGUL_NFD.START_CHOSEONG.toString(16)}-\\u${JASO_HANGUL_NFD.END_CHOSEONG.toString(16)}]`, + 'g' +); /** * @name hasBatchim @@ -26,9 +38,14 @@ export function hasBatchim(str: string) { if (lastChar == null) { return false; } + const charCode = lastChar.charCodeAt(0); + const isCompleteHangul = COMPLETE_HANGUL_START_CHARCODE <= charCode && charCode <= COMPLETE_HANGUL_END_CHARCODE; - const disassembled = disassembleCompleteHangulCharacter(lastChar); - return disassembled != null && disassembled.last !== ''; + if (!isCompleteHangul) { + return false; + } + + return (charCode - COMPLETE_HANGUL_START_CHARCODE) % NUMBER_OF_JONGSUNG > 0; } /** @@ -49,12 +66,18 @@ export function hasBatchim(str: string) { export function hasSingleBatchim(str: string) { const lastChar = str[str.length - 1]; - if (lastChar == null || hasBatchim(lastChar) === false) { + if (lastChar == null) { return false; } + const charCode = lastChar.charCodeAt(0); + const isCompleteHangul = COMPLETE_HANGUL_START_CHARCODE <= charCode && charCode <= COMPLETE_HANGUL_END_CHARCODE; - const disassembled = disassembleHangul(lastChar); - return disassembled.length === 3; + if (!isCompleteHangul) { + return false; + } + + const batchimCode = (charCode - COMPLETE_HANGUL_START_CHARCODE) % NUMBER_OF_JONGSUNG; + return HANGUL_CHARACTERS_BY_LAST_INDEX[batchimCode].length === 1; } /** @@ -73,9 +96,9 @@ export function hasSingleBatchim(str: string) { * getChosung('띄어 쓰기') // 'ㄸㅇ ㅆㄱ' */ export function getChosung(word: string) { - return disassembleHangulToGroups(word).reduce((chosung, [consonant]) => { - return `${chosung}${consonant}`; - }, ''); + return word.normalize('NFD') + .replace(EXTRACT_CHOSEONG_REGEX, '') // NFD ㄱ-ㅎ, NFC ㄱ-ㅎ 외 문자 삭제 + .replace(CHOOSE_NFD_CHOSEONG_REGEX, $0 => HANGUL_CHARACTERS_BY_FIRST_INDEX[$0.charCodeAt(0) - 0x1100]); // NFD to NFC } /** diff --git a/tsconfig.json b/tsconfig.json index 08a6927f..2fbc5a46 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "strict": true, "skipLibCheck": true }, - "include": ["src", ".eslintrc.js"] + "include": ["src", ".eslintrc.js", "packlint.config.mjs"] }