diff --git a/.changeset/itchy-nails-bake.md b/.changeset/itchy-nails-bake.md new file mode 100644 index 00000000..27322e5e --- /dev/null +++ b/.changeset/itchy-nails-bake.md @@ -0,0 +1,5 @@ +--- +"es-hangul": patch +--- + +feat: 숫자를 순 우리말 수사로 변환하거나 수 관형사로 변환하는 함수를 추가 diff --git a/docs/src/pages/docs/api/susa.en.md b/docs/src/pages/docs/api/susa.en.md new file mode 100644 index 00000000..fc191be1 --- /dev/null +++ b/docs/src/pages/docs/api/susa.en.md @@ -0,0 +1,33 @@ +--- +title: susa +--- + +# susa + +Convert numbers to native Korean numeral words or numeral determiners. The given number is valid when it is greater than 0 and less than or equal to 100. + +```typescript +function susa( + // Number to convert + num: number, + // Whether to use numeral determiners + classifier?: boolean +): string; +``` + +## Examples + +```typescript +susa(1); // '하나' +susa(2); // '둘' +susa(11); // '열하나' +susa(21); // '스물하나' +susa(99); // '아흔아홉' +susa(100); // '백' + +susa(1, true); // '한' +susa(2, true); // '두' +susa(11, true); // '열한' +susa(20, true); // '스무' +susa(21, true); // '스물한' +``` diff --git a/docs/src/pages/docs/api/susa.ko.md b/docs/src/pages/docs/api/susa.ko.md new file mode 100644 index 00000000..f3a5c9da --- /dev/null +++ b/docs/src/pages/docs/api/susa.ko.md @@ -0,0 +1,33 @@ +--- +title: susa +--- + +# susa + +숫자를 순 우리말 [수사](https://ko.dict.naver.com/#/entry/koko/d0ce2b674cae4b44b9028f648dd458b0)로 변환하거나 [수 관형사](https://ko.dict.naver.com/#/entry/koko/c513782b82554ff499c80ec616c5b611)로 변환합니다. 주어진 숫자가 0보다 크고 100 이하일 때 유효합니다. + +```typescript +function susa( + // 변환할 숫자 + num: number, + // 수 관형사를 사용할지 여부 + classifier?: boolean +): string; +``` + +## Examples + +```typescript +susa(1); // '하나' +susa(2); // '둘' +susa(11); // '열하나' +susa(21); // '스물하나' +susa(99); // '아흔아홉' +susa(100); // '백' + +susa(1, true); // '한' +susa(2, true); // '두' +susa(11, true); // '열한' +susa(20, true); // '스무' +susa(21, true); // '스물한' +``` diff --git a/src/constants.ts b/src/constants.ts index 3f95c04e..f5b7592c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -199,3 +199,33 @@ export const QWERTY_KEYBOARD_MAP = { m: 'ㅡ', M: 'ㅡ', } as const; + +export const SUSA_MAP = { + 1: '하나', + 2: '둘', + 3: '셋', + 4: '넷', + 5: '다섯', + 6: '여섯', + 7: '일곱', + 8: '여덟', + 9: '아홉', + 10: '열', + 20: '스물', + 30: '서른', + 40: '마흔', + 50: '쉰', + 60: '예순', + 70: '일흔', + 80: '여든', + 90: '아흔', + 100: '백', +} as const; + +export const SUSA_CLASSIFIER_MAP = { + 1: '한', + 2: '두', + 3: '세', + 4: '네', + 20: '스무', +} as const; diff --git a/src/index.ts b/src/index.ts index 0cb8f2e5..b0eef578 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,3 +20,4 @@ export { } from './utils'; export { extractHangul } from './extractHangul'; export { acronymizeHangul } from './acronymizeHangul'; +export { susa } from './susa'; diff --git a/src/susa.spec.ts b/src/susa.spec.ts new file mode 100644 index 00000000..e6dfe434 --- /dev/null +++ b/src/susa.spec.ts @@ -0,0 +1,42 @@ +import { susa } from './susa'; + +describe('susa', () => { + const validNumbers = [ + { num: 1, word: '하나', classifier: '한' }, + { num: 2, word: '둘', classifier: '두' }, + { num: 3, word: '셋', classifier: '세' }, + { num: 4, word: '넷', classifier: '네' }, + { num: 5, word: '다섯', classifier: '다섯' }, + { num: 6, word: '여섯', classifier: '여섯' }, + { num: 7, word: '일곱', classifier: '일곱' }, + { num: 8, word: '여덟', classifier: '여덟' }, + { num: 9, word: '아홉', classifier: '아홉' }, + { num: 10, word: '열', classifier: '열' }, + { num: 11, word: '열하나', classifier: '열한' }, + { num: 12, word: '열둘', classifier: '열두' }, + { num: 20, word: '스물', classifier: '스무' }, + { num: 21, word: '스물하나', classifier: '스물한' }, + { num: 30, word: '서른', classifier: '서른' }, + { num: 99, word: '아흔아홉', classifier: '아흔아홉' }, + { num: 100, word: '백', classifier: '백' }, + ]; + + const invalidNumbers = [0, -1, 101, 1.1, -1.1, Infinity, -Infinity, NaN]; + + validNumbers.forEach(({ num, word, classifier }) => { + it(`${num} - 순 우리말 수사로 바꿔 반환해야 한다.`, () => { + expect(susa(num, false)).toBe(word); + }); + + it(`${num} - 순 우리말 수 관형사가 있다면 수 관형사로 없다면 수사로 반환해야 한다.`, () => { + expect(susa(num, true)).toBe(classifier); + }); + }); + + invalidNumbers.forEach(num => { + it(`유효하지 않은 숫자 ${num}에 대해 오류를 발생시켜야 한다.`, () => { + expect(() => susa(num, false)).toThrow('지원하지 않는 숫자입니다.'); + expect(() => susa(num, true)).toThrow('지원하지 않는 숫자입니다.'); + }); + }); +}); diff --git a/src/susa.ts b/src/susa.ts new file mode 100644 index 00000000..6edb7a2a --- /dev/null +++ b/src/susa.ts @@ -0,0 +1,57 @@ +import { SUSA_MAP, SUSA_CLASSIFIER_MAP } from './constants'; +import { hasProperty } from './utils'; + +export function susa(num: number, classifier?: boolean): string { + validateNumber(num); + return classifier ? getClassifierWord(num) : getNumberWord(num); +} + +function getClassifierWord(num: number): string { + if (num === 20) { + return SUSA_CLASSIFIER_MAP[num]; + } + + const tens = Math.floor(num / 10) * 10; + const ones = num % 10; + + const tensWord = hasProperty(SUSA_MAP, tens) ? SUSA_MAP[tens] : ''; + + if (ones === 0) { + return tensWord; + } + + if (hasProperty(SUSA_CLASSIFIER_MAP, ones)) { + const onesWord = SUSA_CLASSIFIER_MAP[ones]; + + return `${tensWord}${onesWord}`; + } + + if (hasProperty(SUSA_MAP, ones)) { + const onesWord = SUSA_MAP[ones]; + + return `${tensWord}${onesWord}`; + } + + // `susa`에서` `validateNumber` 하기 때문에 도달할 수 없는 분기입니다. 타입 추론을 위해 에러를 던져줍니다. + throw new Error('지원하지 않는 숫자입니다.'); +} + +function validateNumber(num: number): void { + if (Number.isNaN(num) || num <= 0 || num > 100 || !Number.isInteger(num) || !Number.isFinite(num)) { + throw new Error('지원하지 않는 숫자입니다.'); + } +} + +function getNumberWord(num: number): string { + if (num === 100) { + return SUSA_MAP[100]; + } + + const tens = Math.floor(num / 10) * 10; + const ones = num % 10; + + const tensWord = hasProperty(SUSA_MAP, tens) ? SUSA_MAP[tens] : ''; + const onesWord = hasProperty(SUSA_MAP, ones) ? SUSA_MAP[ones] : ''; + + return `${tensWord}${onesWord}`; +}