diff --git a/.changeset/hip-pianos-live.md b/.changeset/hip-pianos-live.md new file mode 100644 index 00000000..5eb95022 --- /dev/null +++ b/.changeset/hip-pianos-live.md @@ -0,0 +1,5 @@ +--- +"es-hangul": minor +--- + +feat: 한국어를 로마자로 변환해주는 함수와 한국어를 표준 발음법으로 변환해주는 함수를 만들고 문서화를 진행합니다 diff --git a/docs/src/pages/docs/api/romanize.en.md b/docs/src/pages/docs/api/romanize.en.md new file mode 100644 index 00000000..e05a00ac --- /dev/null +++ b/docs/src/pages/docs/api/romanize.en.md @@ -0,0 +1,28 @@ +--- +title: romanize +--- + +# romanize + +Change the Hangul string to Roman. + +For detailed examples, see below. + +```typescript +function romanize(hangul: string): string; +``` + +## Examples + +```tsx +romanize('백마'); // 'baengma' +romanize('학여울'); // 'hangnyeoul' +romanize('해돋이'); // 'haedoji' +romanize('좋고'); // 'joko' +romanize('압구정'); // 'apgujeong' +romanize('구미'); // 'gumi' +romanize('대관령'); // 'daegwallyeong' +romanize('ㄱ'); // 'g' +romanize('한국어!'); // 'hangugeo!' +romanize('안녕하세요'); // 'annyeonghaseyo' +``` diff --git a/docs/src/pages/docs/api/romanize.ko.md b/docs/src/pages/docs/api/romanize.ko.md new file mode 100644 index 00000000..4e0630bd --- /dev/null +++ b/docs/src/pages/docs/api/romanize.ko.md @@ -0,0 +1,28 @@ +--- +title: romanize +--- + +# romanize + +한글 문자열을 로마자로 변경합니다. + +자세한 예시는 아래 Example을 참고하세요. + +```typescript +function romanize(hangul: string): string; +``` + +## Examples + +```tsx +romanize('백마'); // 'baengma' +romanize('학여울'); // 'hangnyeoul' +romanize('해돋이'); // 'haedoji' +romanize('좋고'); // 'joko' +romanize('압구정'); // 'apgujeong' +romanize('구미'); // 'gumi' +romanize('대관령'); // 'daegwallyeong' +romanize('ㄱ'); // 'g' +romanize('한국어!'); // 'hangugeo!' +romanize('안녕하세요'); // 'annyeonghaseyo' +``` diff --git a/docs/src/pages/docs/api/standardizePronunciation.en.md b/docs/src/pages/docs/api/standardizePronunciation.en.md new file mode 100644 index 00000000..a8eb87c9 --- /dev/null +++ b/docs/src/pages/docs/api/standardizePronunciation.en.md @@ -0,0 +1,42 @@ +--- +title: standardizePronunciation +--- + +# standardizePronunciation + +Change the Hangul string to standard pronunciation. + +For detailed examples, see below. + +```typescript +function standardizePronunciation( + // Input a Hangul string + hangul: string, + options: { + // Set whether to apply hard sounds. Default is true." + hardConversion: boolean; + } = { hardConversion: true } +): string; +``` + +## Examples + +```tsx +standardizePronunciation('디귿이'); // '디그시' +standardizePronunciation('굳이'); // '구지' +standardizePronunciation('담요'); // '딤뇨' +standardizePronunciation('침략'); // '침냑' +standardizePronunciation('먹는'); // '멍는' +standardizePronunciation('신라'); // '실라' +standardizePronunciation('놓고'); // '노코' +standardizePronunciation('곧이듣다'); // '고지듣따' +standardizePronunciation('곧이듣다', { hardConversion: false }); // '고지듣다' +standardizePronunciation('닦다'); // '닥따' +standardizePronunciation('닦다', { hardConversion: false }); // '닥다' +standardizePronunciation('있다'); // '읻따' +standardizePronunciation('있다', { hardConversion: false }); // '읻다' +standardizePronunciation('핥다'); // '할따' +standardizePronunciation('핥다', { hardConversion: false }); // '할다' +standardizePronunciation('젊다'); // '점따' +standardizePronunciation('젊다', { hardConversion: false }); // '점다' +``` diff --git a/docs/src/pages/docs/api/standardizePronunciation.ko.md b/docs/src/pages/docs/api/standardizePronunciation.ko.md new file mode 100644 index 00000000..b70e7b90 --- /dev/null +++ b/docs/src/pages/docs/api/standardizePronunciation.ko.md @@ -0,0 +1,42 @@ +--- +title: standardizePronunciation +--- + +# standardizePronunciation + +한글 문자열을 표준 발음법으로 변경합니다. + +자세한 예시는 아래 Example을 참고하세요. + +```typescript +function standardizePronunciation( + // 한글 문자열을 입력합니다. + hangul: string, + options: { + // 경음화 등의 된소리를 적용할지 여부를 설정합니다. 기본값은 true입니다. + hardConversion: boolean; + } = { hardConversion: true } +): string; +``` + +## Examples + +```tsx +standardizePronunciation('디귿이'); // '디그시' +standardizePronunciation('굳이'); // '구지' +standardizePronunciation('담요'); // '딤뇨' +standardizePronunciation('침략'); // '침냑' +standardizePronunciation('먹는'); // '멍는' +standardizePronunciation('신라'); // '실라' +standardizePronunciation('놓고'); // '노코' +standardizePronunciation('곧이듣다'); // '고지듣따' +standardizePronunciation('곧이듣다', { hardConversion: false }); // '고지듣다' +standardizePronunciation('닦다'); // '닥따' +standardizePronunciation('닦다', { hardConversion: false }); // '닥다' +standardizePronunciation('있다'); // '읻따' +standardizePronunciation('있다', { hardConversion: false }); // '읻다' +standardizePronunciation('핥다'); // '할따' +standardizePronunciation('핥다', { hardConversion: false }); // '할다' +standardizePronunciation('젊다'); // '점따' +standardizePronunciation('젊다', { hardConversion: false }); // '점다' +``` diff --git a/src/_internal/index.ts b/src/_internal/index.ts index 2bd625d8..4020b7b2 100644 --- a/src/_internal/index.ts +++ b/src/_internal/index.ts @@ -16,3 +16,17 @@ export default function assert(condition: boolean, errorMessage?: string): asser throw new Error(errorMessage ?? 'Invalid condition'); } } + +export function isNotUndefined(value: T | undefined): value is T { + return value !== undefined; +} + +export function defined(value: T | undefined): T { + assert(value !== undefined); + + return value as T; +} + +export function arrayIncludes(array: Type[] | readonly Type[], item: unknown, fromIndex?: number): item is Type { + return array.includes(item as Type, fromIndex); +} diff --git a/src/constants.ts b/src/constants.ts index f5b7592c..20fa1a03 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -200,6 +200,75 @@ export const QWERTY_KEYBOARD_MAP = { M: 'ㅡ', } as const; +export const 중성_알파벳_발음 = { + // ------- 단모음 + ㅏ: 'a', + ㅓ: 'eo', + ㅗ: 'o', + ㅜ: 'u', + ㅡ: 'eu', + ㅣ: 'i', + ㅐ: 'ae', + ㅔ: 'e', + ㅚ: 'oe', + ㅟ: 'wi', + // ------- + // ------- 이중모음 + ㅑ: 'ya', + ㅕ: 'yeo', + ㅛ: 'yo', + ㅠ: 'yu', + ㅒ: 'yae', + ㅖ: 'ye', + ㅘ: 'wa', + ㅙ: 'wae', + ㅝ: 'wo', + ㅞ: 'we', + ㅢ: 'ui', +} as const; + +export const 초성_알파벳_발음 = { + // ------- 파열음 + ㄱ: 'g', + ㄲ: 'kk', + ㅋ: 'k', + ㄷ: 'd', + ㄸ: 'tt', + ㅌ: 't', + ㅂ: 'b', + ㅃ: 'pp', + ㅍ: 'p', + // ------- + // ------- 파찰음 + ㅈ: 'j', + ㅉ: 'jj', + ㅊ: 'ch', + // ------- + // ------- 마찰음 + ㅅ: 's', + ㅆ: 'ss', + ㅎ: 'h', + // ------- + // ------- 비음 + ㄴ: 'n', + ㅁ: 'm', + ㅇ: '', + // ------- + // ------- 유음 + ㄹ: 'r', +} as const; + +export const 종성_알파벳_발음 = { + ㄱ: 'k', + ㄴ: 'n', + ㄷ: 't', + ㄹ: 'l', + ㅁ: 'm', + ㅂ: 'p', + ㅇ: 'ng', + '': '', +} as const; + export const SUSA_MAP = { 1: '하나', 2: '둘', diff --git a/src/index.ts b/src/index.ts index b0eef578..12bccb38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,18 @@ +export { acronymizeHangul } from './acronymizeHangul'; export { assembleHangul } from './assemble'; -export { chosungIncludes } from './chosungIncludes'; export { choseongIncludes } from './choseongIncludes'; +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 { extractHangul } from './extractHangul'; export { hangulIncludes } from './hangulIncludes'; export { josa } from './josa'; export { removeLastHangulCharacter } from './removeLastHangulCharacter'; +export { romanize } from './romanize'; +export { standardizePronunciation } from './standardizePronunciation'; +export { susa } from './susa'; export { canBeChosung, canBeJongsung, @@ -18,6 +23,3 @@ export { hasSingleBatchim, hasValueInReadOnlyStringList, } from './utils'; -export { extractHangul } from './extractHangul'; -export { acronymizeHangul } from './acronymizeHangul'; -export { susa } from './susa'; diff --git a/src/romanize.spec.ts b/src/romanize.spec.ts new file mode 100644 index 00000000..5f68a9e3 --- /dev/null +++ b/src/romanize.spec.ts @@ -0,0 +1,81 @@ +import { romanize } from './romanize'; + +describe('romanize', () => { + it('자음 사이에서 동화 작용이 일어나는 경우', () => { + expect(romanize('백마')).toBe('baengma'); + expect(romanize('종로')).toBe('jongno'); + expect(romanize('왕십리')).toBe('wangsimni'); + expect(romanize('별래')).toBe('byeollae'); + expect(romanize('신라')).toBe('silla'); + }); + + it('ㄴ, ㄹ’이 덧나는 경우', () => { + expect(romanize('학여울')).toBe('hangnyeoul'); + expect(romanize('알약')).toBe('allyak'); + }); + + it('구개음화가 되는 경우', () => { + expect(romanize('해돋이')).toBe('haedoji'); + expect(romanize('같이')).toBe('gachi'); + expect(romanize('굳히다')).toBe('guchida'); + }); + + it('"ㄱ, ㄷ, ㅂ, ㅈ"이 "ㅎ"과 합하여 거센소리로 소리 나는 경우', () => { + expect(romanize('좋고')).toBe('joko'); + expect(romanize('놓다')).toBe('nota'); + expect(romanize('잡혀')).toBe('japyeo'); + expect(romanize('낳지')).toBe('nachi'); + }); + + it('된소리되기는 표기에 반영하지 않는다', () => { + expect(romanize('압구정')).toBe('apgujeong'); + expect(romanize('낙동강')).toBe('nakdonggang'); + expect(romanize('죽변')).toBe('jukbyeon'); + expect(romanize('낙성대')).toBe('nakseongdae'); + expect(romanize('합정')).toBe('hapjeong'); + expect(romanize('팔당')).toBe('paldang'); + expect(romanize('샛별')).toBe('saetbyeol'); + expect(romanize('울산')).toBe('ulsan'); + }); + + it('"ㄱ, ㄷ, ㅂ"은 모음 앞에서는 "g, d, b"로, 자음 앞이나 어말에서는 "k, t, p"로 적는다', () => { + expect(romanize('구미')).toBe('gumi'); + expect(romanize('영동')).toBe('yeongdong'); + expect(romanize('백암')).toBe('baegam'); + expect(romanize('옥천')).toBe('okcheon'); + expect(romanize('합덕')).toBe('hapdeok'); + expect(romanize('호법')).toBe('hobeop'); + expect(romanize('월곶')).toBe('wolgot'); + expect(romanize('벚꽃')).toBe('beotkkot'); + expect(romanize('한밭')).toBe('hanbat'); + }); + + it('"ㄹ"은 모음 앞에서는 "r"로, 자음 앞이나 어말에서는 "l"로 적는다. 단, "ㄹㄹ"은 "ll"로 적는다', () => { + expect(romanize('구리')).toBe('guri'); + expect(romanize('설악')).toBe('seorak'); + expect(romanize('칠곡')).toBe('chilgok'); + expect(romanize('임실')).toBe('imsil'); + expect(romanize('울릉')).toBe('ulleung'); + expect(romanize('대관령')).toBe('daegwallyeong'); + }); + + it('완성된 음절이 아닌 경우에는 그대로 반환한다', () => { + expect(romanize('ㄱ')).toBe('g'); + expect(romanize('가나다라ㅁㅂㅅㅇ')).toBe('ganadarambs'); + expect(romanize('ㅏ')).toBe('a'); + expect(romanize('ㅘ')).toBe('wa'); + }); + + it('특수문자는 로마자 표기로 변경하지 않는다', () => { + expect(romanize('안녕하세요.')).toBe('annyeonghaseyo.'); + expect(romanize('한국어!')).toBe('hangugeo!'); + expect(romanize('')).toBe(''); + expect(romanize('!?/')).toBe('!?/'); + }); + + it('한글과 영어가 혼합된 경우에는 영어는 그대로 반환된다', () => { + expect(romanize('안녕하세요 es-hangul')).toBe('annyeonghaseyo es-hangul'); + expect(romanize('한국은korea')).toBe('hangugeunkorea'); + expect(romanize('고양이는cat')).toBe('goyangineuncat'); + }); +}); diff --git a/src/romanize.ts b/src/romanize.ts new file mode 100644 index 00000000..14955228 --- /dev/null +++ b/src/romanize.ts @@ -0,0 +1,55 @@ +import { isHangulCharacter } from './_internal/hangul'; +import { assembleHangul } from './assemble'; +import { 종성_알파벳_발음, 중성_알파벳_발음, 초성_알파벳_발음 } from './constants'; +import { disassembleCompleteHangulCharacter } from './disassembleCompleteHangulCharacter'; +import { standardizePronunciation } from './standardizePronunciation'; +import { canBeChoseong } from './utils'; + +/** + * 주어진 한글 문자열을 로마자로 변환합니다. + * @param hangul 한글 문자열을 입력합니다. + * @returns 변환된 로마자를 반환합니다. + */ +export function romanize(hangul: string): string { + const changedHangul = standardizePronunciation(hangul, { hardConversion: false }); + + return changedHangul + .split('') + .map((_, i, arrayHangul) => romanizeSyllableHangul(arrayHangul, i)) + .join(''); +} + +const romanizeSyllableHangul = (arrayHangul: string[], index: number): string => { + const syllable = arrayHangul[index]; + + if (isHangulCharacter(syllable)) { + const disassemble = disassembleCompleteHangulCharacter(syllable) as NonNullable< + ReturnType + >; + + let choseong: (typeof 초성_알파벳_발음)[keyof typeof 초성_알파벳_발음] | 'l' = 초성_알파벳_발음[disassemble.first]; + const jungseong = 중성_알파벳_발음[assembleHangul([disassemble.middle]) as keyof typeof 중성_알파벳_발음]; + const jongseong = 종성_알파벳_발음[disassemble.last as keyof typeof 종성_알파벳_발음]; + + // 'ㄹ'은 모음 앞에서는 'r'로, 자음 앞이나 어말에서는 'l'로 적는다. 단, 'ㄹㄹ'은 'll'로 적는다. (ex.울릉, 대관령), + if (disassemble.first === 'ㄹ' && index > 0 && isHangulCharacter(arrayHangul[index - 1])) { + const prevDisassemble = disassembleCompleteHangulCharacter(arrayHangul[index - 1]); + + if (prevDisassemble?.last === 'ㄹ') { + choseong = 'l'; + } + } + + return choseong + jungseong + jongseong; + } + + if (syllable in 중성_알파벳_발음) { + return 중성_알파벳_발음[syllable as keyof typeof 중성_알파벳_발음]; + } + + if (canBeChoseong(syllable)) { + return 초성_알파벳_발음[syllable as keyof typeof 초성_알파벳_발음]; + } + + return syllable; +}; diff --git a/src/standardizePronunciation/index.ts b/src/standardizePronunciation/index.ts new file mode 100644 index 00000000..847ef6b4 --- /dev/null +++ b/src/standardizePronunciation/index.ts @@ -0,0 +1,156 @@ +import { isNotUndefined, joinString } from '../_internal'; +import { isHangulAlphabet, isHangulCharacter } from '../_internal/hangul'; +import { combineHangulCharacter } from '../combineHangulCharacter'; +import { disassembleCompleteHangulCharacter } from '../disassembleCompleteHangulCharacter'; +import { + transform12th, + transform13And14th, + transform16th, + transform17th, + transform18th, + transform19th, + transform20th, + transform9And10And11th, + transformHardConversion, + transformNLAssimilation, + type Nullable, + type Syllable, +} from './rules'; + +type Options = { + hardConversion: boolean; +}; + +type NotHangul = { + index: number; + syllable: string; +}; + +/** + * 주어진 한글 문자열을 표준 발음으로 변환합니다. + * @param hangul 한글 문자열을 입력합니다. + * @param options 변환 옵션을 설정합니다. + * @param options.hardConversion 경음화 등의 된소리를 적용할지 여부를 설정합니다. 기본값은 true입니다. + * @returns 변환된 표준 발음 문자열을 반환합니다. + */ +export function standardizePronunciation(hangul: string, options: Options = { hardConversion: true }): string { + if (!hangul) { + return ''; + } + + const processSyllables = (syllables: Syllable[], phrase: string, options: Options) => + syllables.map((currentSyllable, I, array) => { + const nextSyllable = I < array.length - 1 ? array[I + 1] : null; + + const { current, next } = applyRules({ + currentSyllable, + phrase, + index: I, + nextSyllable, + options, + }); + + if (next) { + array[I + 1] = next; + } + + return current; + }); + + const transformHangulPhrase = (phrase: string, options: Options): string => { + const { notHangulPhrase, disassembleHangul } = 음절분해(phrase); + const processedSyllables = processSyllables(disassembleHangul, phrase, options); + + return assembleChangedHangul(processedSyllables, notHangulPhrase); + }; + + return hangul + .split(' ') + .map(phrase => transformHangulPhrase(phrase, options)) + .join(' '); +} + +function 음절분해(hangulPhrase: string): { + notHangulPhrase: NotHangul[]; + disassembleHangul: Syllable[]; +} { + const notHangulPhrase: NotHangul[] = []; + const disassembleHangul = Array.from(hangulPhrase) + .filter((syllable, index) => { + if (!isHangulCharacter(syllable) || isHangulAlphabet(syllable)) { + notHangulPhrase.push({ + index, + syllable, + }); + + return false; + } + + return true; + }) + .map(disassembleCompleteHangulCharacter) + .filter(isNotUndefined); + + return { notHangulPhrase, disassembleHangul }; +} + +type ApplyParameters = { + currentSyllable: Syllable; + nextSyllable: Nullable; + index: number; + phrase: string; + options: NonNullable[1]>; +}; + +function applyRules(params: ApplyParameters): { + current: Syllable; + next: Nullable; +} { + const { currentSyllable, nextSyllable, index, phrase, options } = params; + + let current = { ...currentSyllable }; + let next = nextSyllable ? { ...nextSyllable } : nextSyllable; + + if (next && options.hardConversion) { + ({ next } = transformHardConversion(current, next)); + } + + if (next) { + ({ current, next } = transform16th({ + currentSyllable: current, + nextSyllable: next, + index, + phrase, + })); + ({ current, next } = transform17th(current, next)); + ({ next } = transform19th(current, next)); + ({ current, next } = transformNLAssimilation(current, next)); + ({ current } = transform18th(current, next)); + ({ current, next } = transform20th(current, next)); + } + + ({ current, next } = transform12th(current, next)); + + if (next) { + ({ current, next } = transform13And14th(current, next)); + } + + ({ current } = transform9And10And11th(current, next)); + + return { + current, + next, + }; +} + +function assembleChangedHangul(disassembleHangul: Syllable[], notHangulPhrase: NotHangul[]): string { + const changedSyllables = disassembleHangul + .filter(isNotUndefined) + .map(syllable => combineHangulCharacter(syllable.first, syllable.middle, syllable.last)); + + for (const { index, syllable } of notHangulPhrase) { + changedSyllables.splice(index, 0, syllable); + } + + return joinString(...changedSyllables); +} diff --git a/src/standardizePronunciation/rules/index.ts b/src/standardizePronunciation/rules/index.ts new file mode 100644 index 00000000..f751a858 --- /dev/null +++ b/src/standardizePronunciation/rules/index.ts @@ -0,0 +1,11 @@ +export type { NonUndefined, Nullable, NullableReturnSyllables, ReturnSyllables, Syllable } from './rules.types'; +export { transform12th } from './transform12th'; +export { transform13And14th } from './transform13And14th'; +export { transform16th } from './transform16th'; +export { transform17th } from './transform17th'; +export { transform18th } from './transform18th'; +export { transform19th } from './transform19th'; +export { transform20th } from './transform20th'; +export { transform9And10And11th } from './transform9And10And11th'; +export { transformHardConversion } from './transformHardConversion'; +export { transformNLAssimilation } from './transformNLAssimilation'; diff --git a/src/standardizePronunciation/rules/rules.types.ts b/src/standardizePronunciation/rules/rules.types.ts new file mode 100644 index 00000000..46eca0d6 --- /dev/null +++ b/src/standardizePronunciation/rules/rules.types.ts @@ -0,0 +1,13 @@ +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; + +export type NonUndefined = T extends undefined ? never : T; +export type Nullable = T | null | undefined; +export type Syllable = NonUndefined>; +export type ReturnSyllables = { + current: Syllable; + next: Syllable; +}; +export type NullableReturnSyllables = { + current: Syllable; + next: Nullable; +}; diff --git a/src/standardizePronunciation/rules/rules.utils.ts b/src/standardizePronunciation/rules/rules.utils.ts new file mode 100644 index 00000000..fe0f330c --- /dev/null +++ b/src/standardizePronunciation/rules/rules.utils.ts @@ -0,0 +1,5 @@ +import { Syllable } from './rules.types'; + +export function replace받침ㅎ(currentSyllable: Syllable): Syllable['last'] { + return currentSyllable.last.replace('ㅎ', '') as Syllable['last']; +} diff --git a/src/standardizePronunciation/rules/transform12th.spec.ts b/src/standardizePronunciation/rules/transform12th.spec.ts new file mode 100644 index 00000000..be0c23d3 --- /dev/null +++ b/src/standardizePronunciation/rules/transform12th.spec.ts @@ -0,0 +1,127 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform12th } from './transform12th'; + +describe('transform12th', () => { + it('"ㅎ, ㄶ, ㅀ" 뒤에 "ㄱ, ㄷ, ㅈ"이 결합되는 경우에는, 뒤 음절 첫소리와 합쳐서 "ㅋ, ㅌ, ㅊ"으로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('놓')); + const next = defined(disassembleCompleteHangulCharacter('고')); + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㄴ', + middle: 'ㅗ', + last: '', + }, + next: { + first: 'ㅋ', + middle: 'ㅗ', + last: '', + }, + }); + }); + + it('받침 "ㄱ, ㄺ, ㄷ, ㅂ, ㄼ, ㅈ, ㄵ"이 뒤 음절 첫소리 "ㅎ"과 결합되는 경우에도, 역시 두 음을 합쳐서 "ㅋ, ㅌ, ㅍ, ㅊ"으로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('각')); + const next = defined(disassembleCompleteHangulCharacter('하')); + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㄱ', + middle: 'ㅏ', + last: '', + }, + next: { + first: 'ㅋ', + middle: 'ㅏ', + last: '', + }, + }); + }); + + it('"ㅎ, ㄶ, ㅀ" 뒤에 "ㅅ"이 결합되는 경우에는, "ㅅ"을 "ㅆ"으로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('닿')); + const next = defined(disassembleCompleteHangulCharacter('소')); + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㄷ', + middle: 'ㅏ', + last: '', + }, + next: { + first: 'ㅆ', + middle: 'ㅗ', + last: '', + }, + }); + }); + + it('"ㅎ" 뒤에 "ㄴ"이 결합되는 경우에는 "ㄴ"으로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('놓')); + const next = defined(disassembleCompleteHangulCharacter('는')); + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㄴ', + middle: 'ㅗ', + last: '', + }, + next: { + first: 'ㄴ', + middle: 'ㅡ', + last: 'ㄴ', + }, + }); + }); + + it('"ㄶ, ㅀ" 뒤에 "ㄴ"이 결합되는 경우에는, "ㅎ"을 발음하지 않는다', () => { + const current = defined(disassembleCompleteHangulCharacter('않')); + const next = defined(disassembleCompleteHangulCharacter('네')); + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㅇ', + middle: 'ㅏ', + last: 'ㄴ', + }, + next: { + first: 'ㄴ', + middle: 'ㅔ', + last: '', + }, + }); + }); + + it('"ㅎ, ㄶ, ㅀ" 뒤에 모음으로 시작된 어미나 접미사가 결합되는 경우에는 "ㅎ"을 발음하지 않는다', () => { + const current = defined(disassembleCompleteHangulCharacter('낳')); + const next = defined(disassembleCompleteHangulCharacter('은')); + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㄴ', + middle: 'ㅏ', + last: '', + }, + next: { + first: 'ㅇ', + middle: 'ㅡ', + last: 'ㄴ', + }, + }); + }); + + it('"ㅎ, ㄶ, ㅀ" 뒤에 모음으로 시작된 어미나 접미사가 결합되는 경우에는 "ㅎ"을 발음하지 않는다', () => { + const current = defined(disassembleCompleteHangulCharacter('많')); + const next = null; + + expect(transform12th(current, next)).toEqual({ + current: { + first: 'ㅁ', + middle: 'ㅏ', + last: 'ㄴ', + }, + next: null, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform12th.ts b/src/standardizePronunciation/rules/transform12th.ts new file mode 100644 index 00000000..70a64cdc --- /dev/null +++ b/src/standardizePronunciation/rules/transform12th.ts @@ -0,0 +1,114 @@ +import { arrayIncludes } from '../../_internal'; +import { + 발음변환_받침_ㅎ, + 발음변환_받침_ㅎ_발음, + 발음변환_첫소리_ㅎ, + 발음변환_첫소리_ㅎ_발음, + 음가가_없는_자음, +} from '../standardizePronunciation.constants'; +import { Nullable, NullableReturnSyllables, ReturnSyllables, Syllable } from './rules.types'; +import { replace받침ㅎ } from './rules.utils'; + +/** + * 제12항을 적용합니다. + * @description 제12항 받침 ‘ㅎ’의 발음은 다음과 같다. + * @description ‘ㅎ(ㄶ, ㅀ)’ 뒤에 ‘ㄱ, ㄷ, ㅈ’이 결합되는 경우에는, 뒤 음절 첫소리와 합쳐서 [ㅋ, ㅌ, ㅊ]으로 발음한다. + * @description [붙임] 받침 ‘ㄱ(ㄺ), ㄷ, ㅂ(ㄼ), ㅈ(ㄵ)’이 뒤 음절 첫소리 ‘ㅎ’과 결합되는 경우에도, 역시 두 음을 합쳐서 [ㅋ, ㅌ, ㅍ, ㅊ]으로 발음한다. + * @description ‘ㅎ(ㄶ, ㅀ)’ 뒤에 ‘ㅅ’이 결합되는 경우에는, ‘ㅅ’을 [ㅆ]으로 발음한다. + * @description ‘ㅎ’ 뒤에 ‘ㄴ’이 결합되는 경우에는, [ㄴ]으로 발음한다. + * @description [붙임] ‘ㄶ, ㅀ’ 뒤에 ‘ㄴ’이 결합되는 경우에는, ‘ㅎ’을 발음하지 않는다. + * @description ‘ㅎ(ㄶ, ㅀ)’ 뒤에 모음으로 시작된 어미나 접미사가 결합되는 경우에는, ‘ㅎ’을 발음하지 않는다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + */ +export function transform12th(currentSyllable: Syllable, nextSyllable: Nullable): NullableReturnSyllables { + let current = { ...currentSyllable }; + let next = nextSyllable ? { ...nextSyllable } : nextSyllable; + + if (!current.last) { + return { + current, + next, + }; + } + + if (arrayIncludes(발음변환_받침_ㅎ, current.last)) { + if (next) { + ({ current, next } = handleNextFirstIsㄱㄷㅈㅅ(current, next)); + ({ current, next } = handleNextFirstIsㄴ(current, next)); + ({ current, next } = handleNextFirstIsㅇ(current, next)); + } + + if (!next) { + ({ current } = handleCurrentLastIsㅇ(current)); + } + } + + ({ current, next } = handleNextFirstIsㅎ(current, next)); + + return { + current, + next, + }; +} + +function handleNextFirstIsㄱㄷㅈㅅ(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (arrayIncludes(['ㄱ', 'ㄷ', 'ㅈ', 'ㅅ'], updatedNext.first)) { + updatedNext.first = 발음변환_받침_ㅎ_발음[updatedNext.first as keyof typeof 발음변환_받침_ㅎ_발음]; + updatedCurrent.last = replace받침ㅎ(updatedCurrent); + } + + return { current: updatedCurrent, next: updatedNext }; +} + +function handleNextFirstIsㄴ(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (updatedNext.first === 'ㄴ' && arrayIncludes(['ㄴㅎ', 'ㄹㅎ'], updatedCurrent.last)) { + updatedCurrent.last = replace받침ㅎ(updatedCurrent); + } + return { current: updatedCurrent, next: updatedNext }; +} + +function handleNextFirstIsㅇ(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (updatedNext.first === 음가가_없는_자음) { + if (arrayIncludes(['ㄴㅎ', 'ㄹㅎ'], updatedCurrent.last)) { + updatedCurrent.last = replace받침ㅎ(updatedCurrent); + } else { + updatedCurrent.last = ''; + } + } else { + updatedCurrent.last = replace받침ㅎ(updatedCurrent); + } + return { current: updatedCurrent, next: updatedNext }; +} + +function handleCurrentLastIsㅇ(current: Syllable): Pick { + const updatedCurrent = { ...current }; + + updatedCurrent.last = replace받침ㅎ(updatedCurrent); + return { current: updatedCurrent }; +} + +function handleNextFirstIsㅎ(current: Syllable, next: Nullable): NullableReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = next ? { ...next } : next; + + if (arrayIncludes(발음변환_첫소리_ㅎ, updatedCurrent.last) && arrayIncludes(['ㅎ'], updatedNext?.first)) { + updatedNext.first = 발음변환_첫소리_ㅎ_발음[updatedCurrent.last]; + + if (updatedCurrent.last.length === 1) { + updatedCurrent.last = ''; + } else { + updatedCurrent.last = updatedCurrent.last[0] as Syllable['last']; + } + } + return { current: updatedCurrent, next: updatedNext }; +} diff --git a/src/standardizePronunciation/rules/transform13And14th.spec.ts b/src/standardizePronunciation/rules/transform13And14th.spec.ts new file mode 100644 index 00000000..671b9687 --- /dev/null +++ b/src/standardizePronunciation/rules/transform13And14th.spec.ts @@ -0,0 +1,41 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform13And14th } from './transform13And14th'; + +describe('transform13And14th', () => { + it('13항을 적용합니다.', () => { + const current = defined(disassembleCompleteHangulCharacter('깎')); + const next = defined(disassembleCompleteHangulCharacter('아')); + + expect(transform13And14th(current, next)).toEqual({ + current: { + first: 'ㄲ', + middle: 'ㅏ', + last: '', + }, + next: { + first: 'ㄲ', + middle: 'ㅏ', + last: '', + }, + }); + }); + + it('14항을 적용합니다.', () => { + const current = defined(disassembleCompleteHangulCharacter('닭')); + const next = defined(disassembleCompleteHangulCharacter('을')); + + expect(transform13And14th(current, next)).toEqual({ + current: { + first: 'ㄷ', + middle: 'ㅏ', + last: 'ㄹ', + }, + next: { + first: 'ㄱ', + middle: 'ㅡ', + last: 'ㄹ', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform13And14th.ts b/src/standardizePronunciation/rules/transform13And14th.ts new file mode 100644 index 00000000..faf02bd0 --- /dev/null +++ b/src/standardizePronunciation/rules/transform13And14th.ts @@ -0,0 +1,76 @@ +import { arrayIncludes } from '../../_internal'; +import { 음가가_없는_자음 } from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +const 받침의길이 = { + 홀받침: 1, + 쌍_겹받침: 2, +} as const; + +/** + * 제13, 14항을 적용합니다. + * @description 제13항 - 홑받침이나 쌍받침이 모음으로 시작된 조사나 어미, 접미사와 결합되는 경우에는, 제 음가대로 뒤 음절 첫소리로 옮겨 발음한다. + * @description 제14항 - 겹받침이 모음으로 시작된 조사나 어미, 접미사와 결합되는 경우에는, 뒤엣것만을 뒤 음절 첫소리로 옮겨 발음한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + * @returns 13, 14항이 적용되었는지의 여부를 반환합니다. + */ +export function transform13And14th(currentSyllable: Syllable, nextSyllable: Syllable): ReturnSyllables { + let current = { ...currentSyllable }; + let next = { ...nextSyllable }; + + const 제13_14항주요조건 = current.last && next.first === 음가가_없는_자음; + + if (!제13_14항주요조건) { + return { + current, + next, + }; + } + + ({ current, next } = handle홑받침or쌍받침(current, next)); + ({ current, next } = handle겹받침(current, next)); + + return { + current, + next, + }; +} + +function is홑받침(current: Syllable): boolean { + return current.last.length === 받침의길이['홀받침']; +} + +function is쌍받침(current: Syllable): boolean { + return current.last.length === 받침의길이['쌍_겹받침'] && current.last[0] === current.last[1]; +} + +function is겹받침(current: Syllable): boolean { + return current.last.length === 받침의길이['쌍_겹받침'] && current.last[0] !== current.last[1]; +} + +function handle홑받침or쌍받침(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (!arrayIncludes(['ㅇ', ''], updatedCurrent.last) && (is홑받침(updatedCurrent) || is쌍받침(updatedCurrent))) { + updatedNext.first = updatedCurrent.last; + updatedCurrent.last = ''; + } + return { current: updatedCurrent, next: updatedNext }; +} + +function handle겹받침(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (is겹받침(updatedCurrent)) { + if (updatedCurrent.last[1] === 'ㅅ') { + updatedNext.first = 'ㅆ'; + } else { + updatedNext.first = updatedCurrent.last[1] as Syllable['first']; + } + updatedCurrent.last = updatedCurrent.last.replace(updatedCurrent.last[1], '') as Syllable['last']; + } + return { current: updatedCurrent, next: updatedNext }; +} diff --git a/src/standardizePronunciation/rules/transform16th.spec.ts b/src/standardizePronunciation/rules/transform16th.spec.ts new file mode 100644 index 00000000..e226bdd1 --- /dev/null +++ b/src/standardizePronunciation/rules/transform16th.spec.ts @@ -0,0 +1,59 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform16th } from './transform16th'; + +describe('transform16th', () => { + it('한글 자모의 이름은 그 받침소리를 연음하되, "ㄷ, ㅈ, ㅊ, ㅋ, ㅌ, ㅍ, ㅎ"의 경우에는 특별히 다음과 같이 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('귿')); + const next = defined(disassembleCompleteHangulCharacter('이')); + const phrase = '디귿이'; + const index = 1; + + expect( + transform16th({ + currentSyllable: current, + nextSyllable: next, + index, + phrase, + }) + ).toEqual({ + current: { + first: 'ㄱ', + middle: 'ㅡ', + last: '', + }, + next: { + first: 'ㅅ', + middle: 'ㅣ', + last: '', + }, + }); + }); + + it('자모의 이름이 "ㄱ, ㄴ, ㄹ, ㅁ, ㅂ, ㅅ, ㅇ"일 경우', () => { + const current = defined(disassembleCompleteHangulCharacter('역')); + const next = defined(disassembleCompleteHangulCharacter('이')); + const phrase = '기역이'; + const index = 1; + + expect( + transform16th({ + currentSyllable: current, + nextSyllable: next, + index, + phrase, + }) + ).toEqual({ + current: { + first: 'ㅇ', + middle: 'ㅕ', + last: '', + }, + next: { + first: 'ㄱ', + middle: 'ㅣ', + last: '', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform16th.ts b/src/standardizePronunciation/rules/transform16th.ts new file mode 100644 index 00000000..1fcae938 --- /dev/null +++ b/src/standardizePronunciation/rules/transform16th.ts @@ -0,0 +1,86 @@ +import { arrayIncludes } from '../../_internal'; +import { + 음가가_없는_자음, + 특별한_한글_자모, + 특별한_한글_자모의_발음, + 한글_자모, +} from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +type Apply16항 = { + currentSyllable: Syllable; + nextSyllable: Syllable; + phrase: string; + index: number; +}; +/** + * 제16항을 적용합니다. + * @description 제16항 - 한글 자모의 이름은 그 받침소리를 연음하되, ‘ㄷ, ㅈ, ㅊ, ㅋ, ㅌ, ㅍ, ㅎ’의 경우에는 특별히 다음과 같이 발음한다. ㄷ, ㅈ, ㅊ, ㅌ, ㅎ > ㅅ (디귿이:디그시, 지읒이:지으시, 치읓이:치으시, 티읕이:티으시, 히읗이:히으시), ㅋ > ㄱ (키읔이:키으기), ㅍ > ㅂ (피읖이:피으비) + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + * @param phrase 분리되지 않은 한글 구절을 입력합니다. + * @param index 현재 음절의 순서를 입력합니다. + * @returns 16항이 적용되었는지의 여부를 반환합니다. + */ +export function transform16th({ currentSyllable, phrase, index, nextSyllable }: Apply16항): ReturnSyllables { + let current = { ...currentSyllable }; + let next = { ...nextSyllable }; + + const 제16항주요조건 = current.last && next.first === 음가가_없는_자음; + + if (!제16항주요조건) { + return { + current, + next, + }; + } + + const combinedSyllables = phrase[index - 1] + phrase[index]; + + ({ current, next } = handleSpecialHangulCharacters({ current, next, combinedSyllables })); + ({ current, next } = handleHangulCharacters({ current, next, combinedSyllables })); + + return { + current, + next, + }; +} + +function handleSpecialHangulCharacters({ + current, + next, + combinedSyllables, +}: ReturnSyllables & { + combinedSyllables: string; +}): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (arrayIncludes(특별한_한글_자모, combinedSyllables)) { + const 다음_음절의_초성 = 특별한_한글_자모의_발음[updatedCurrent.last as keyof typeof 특별한_한글_자모의_발음]; + + updatedCurrent.last = ''; + updatedNext.first = 다음_음절의_초성; + } + return { current: updatedCurrent, next: updatedNext }; +} + +function handleHangulCharacters({ + current, + next, + combinedSyllables, +}: ReturnSyllables & { + combinedSyllables: string; +}): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (arrayIncludes(한글_자모, combinedSyllables)) { + updatedNext.first = updatedCurrent.last as typeof updatedNext.first; + + if (updatedCurrent.last !== 'ㅇ') { + updatedCurrent.last = ''; + } + } + return { current: updatedCurrent, next: updatedNext }; +} diff --git a/src/standardizePronunciation/rules/transform17th.spec.ts b/src/standardizePronunciation/rules/transform17th.spec.ts new file mode 100644 index 00000000..aab32447 --- /dev/null +++ b/src/standardizePronunciation/rules/transform17th.spec.ts @@ -0,0 +1,41 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform17th } from './transform17th'; + +describe('transform17th', () => { + it('받침 "ㄷ", "ㅌ(ㄾ)"이 조사나 접미사의 모음 "ㅣ"와 결합되는 경우에는, "ㅈ", "ㅊ"으로 바꾸어서 뒤 음절 첫소리로 옮겨 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('굳')); + const next = defined(disassembleCompleteHangulCharacter('이')); + + expect(transform17th(current, next)).toEqual({ + current: { + first: 'ㄱ', + middle: 'ㅜ', + last: '', + }, + next: { + first: 'ㅈ', + middle: 'ㅣ', + last: '', + }, + }); + }); + + it('"ㄷ" 뒤에 접미사 "히"가 결합되어 "티"를 이루는 것은 "치"로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('굳')); + const next = defined(disassembleCompleteHangulCharacter('히')); + + expect(transform17th(current, next)).toEqual({ + current: { + first: 'ㄱ', + middle: 'ㅜ', + last: '', + }, + next: { + first: 'ㅊ', + middle: 'ㅣ', + last: '', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform17th.ts b/src/standardizePronunciation/rules/transform17th.ts new file mode 100644 index 00000000..bf3ab074 --- /dev/null +++ b/src/standardizePronunciation/rules/transform17th.ts @@ -0,0 +1,55 @@ +import { hasProperty } from '../../utils'; +import { 음의_동화_받침 } from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +/** + * 제17항을 적용합니다. + * @description 17항 - 받침 ‘ㄷ', 'ㅌ(ㄾ)’이 조사나 접미사의 모음 ‘ㅣ’와 결합되는 경우에는, [ㅈ, ㅊ]으로 바꾸어서 뒤 음절 첫소리로 옮겨 발음한다. + * @description [붙임] ‘ㄷ’ 뒤에 접미사 ‘히’가 결합되어 ‘티’를 이루는 것은 [치]로 발음한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + * @returns 17항이 적용되었는지의 여부를 반환합니다. + */ +export function transform17th(currentSyllable: Syllable, nextSyllable: Syllable): ReturnSyllables { + let current = { ...currentSyllable }; + let next = { ...nextSyllable }; + + const 제17항주요조건 = next.middle === 'ㅣ'; + + if (!제17항주요조건) { + return { + current, + next, + }; + } + + ({ current, next } = handleFirstIsㅇ(current, next)); + ({ current, next } = handleFirstIsㅎAndㄷ(current, next)); + + return { + current, + next, + }; +} + +function handleFirstIsㅇ(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (updatedNext.first === 'ㅇ' && hasProperty(음의_동화_받침, updatedCurrent.last)) { + updatedNext.first = 음의_동화_받침[updatedCurrent.last]; + updatedCurrent.last = updatedCurrent.last === 'ㄹㅌ' ? 'ㄹ' : ''; + } + return { current: updatedCurrent, next: updatedNext }; +} + +function handleFirstIsㅎAndㄷ(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (updatedNext.first === 'ㅎ' && updatedCurrent.last === 'ㄷ') { + updatedNext.first = 'ㅊ'; + updatedCurrent.last = ''; + } + return { current: updatedCurrent, next: updatedNext }; +} diff --git a/src/standardizePronunciation/rules/transform18th.spec.ts b/src/standardizePronunciation/rules/transform18th.spec.ts new file mode 100644 index 00000000..f7146469 --- /dev/null +++ b/src/standardizePronunciation/rules/transform18th.spec.ts @@ -0,0 +1,44 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform18th } from './transform18th'; + +describe('transform18th', () => { + it('받침 "ㄱ, ㄲ, ㅋ, ㄳ, ㄺ"일 경우', () => { + const current = defined(disassembleCompleteHangulCharacter('먹')); + const next = defined(disassembleCompleteHangulCharacter('는')); + + expect(transform18th(current, next)).toEqual({ + current: { + first: 'ㅁ', + middle: 'ㅓ', + last: 'ㅇ', + }, + }); + }); + + it('받침 "ㄷ, ㅅ, ㅆ, ㅈ, ㅊ, ㅌ, ㅎ"일 경우', () => { + const current = defined(disassembleCompleteHangulCharacter('닫')); + const next = defined(disassembleCompleteHangulCharacter('는')); + + expect(transform18th(current, next)).toEqual({ + current: { + first: 'ㄷ', + middle: 'ㅏ', + last: 'ㄴ', + }, + }); + }); + + it('받침 "ㅂ, ㅍ, ㄼ, ㄿ, ㅄ"일 경우', () => { + const current = defined(disassembleCompleteHangulCharacter('잡')); + const next = defined(disassembleCompleteHangulCharacter('는')); + + expect(transform18th(current, next)).toEqual({ + current: { + first: 'ㅈ', + middle: 'ㅏ', + last: 'ㅁ', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform18th.ts b/src/standardizePronunciation/rules/transform18th.ts new file mode 100644 index 00000000..1f695105 --- /dev/null +++ b/src/standardizePronunciation/rules/transform18th.ts @@ -0,0 +1,38 @@ +import { arrayIncludes } from '../../_internal'; +import { 비음화_받침_ㄴ_변환, 비음화_받침_ㅁ_변환, 비음화_받침_ㅇ_변환 } from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +/** + * 제18항을 적용합니다. + * @description 18항 - 받침 ‘ㄱ(ㄲ, ㅋ, ㄳ, ㄺ), ㄷ(ㅅ, ㅆ, ㅈ, ㅊ, ㅌ, ㅎ), ㅂ(ㅍ, ㄼ, ㄿ, ㅄ)’은 ‘ㄴ, ㅁ’ 앞에서 [ㅇ, ㄴ, ㅁ]으로 발음한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + * @returns 18항이 적용되었는지의 여부를 반환합니다. + */ +export function transform18th(currentSyllable: Syllable, nextSyllable: Syllable): Pick { + const current = { ...currentSyllable }; + + const 제18항주요조건 = current.last && arrayIncludes(['ㄴ', 'ㅁ'], nextSyllable.first); + + if (!제18항주요조건) { + return { + current, + }; + } + + if (arrayIncludes(비음화_받침_ㅇ_변환, current.last)) { + current.last = 'ㅇ'; + } + + if (arrayIncludes(비음화_받침_ㄴ_변환, current.last)) { + current.last = 'ㄴ'; + } + + if (arrayIncludes(비음화_받침_ㅁ_변환, current.last)) { + current.last = 'ㅁ'; + } + + return { + current, + }; +} diff --git a/src/standardizePronunciation/rules/transform19th.spec.ts b/src/standardizePronunciation/rules/transform19th.spec.ts new file mode 100644 index 00000000..b15c3434 --- /dev/null +++ b/src/standardizePronunciation/rules/transform19th.spec.ts @@ -0,0 +1,31 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform19th } from './transform19th'; + +describe('transform19th', () => { + it('받침 "ㅁ, ㅇ" 뒤에 연결되는 "ㄹ"은 "ㄴ"으로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('담')); + const next = defined(disassembleCompleteHangulCharacter('력')); + + expect(transform19th(current, next)).toEqual({ + next: { + first: 'ㄴ', + middle: 'ㅕ', + last: 'ㄱ', + }, + }); + }); + + it('받침 "ㄱ, ㅂ" 뒤에 연결되는 "ㄹ"도 "ㄴ"으로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('막')); + const next = defined(disassembleCompleteHangulCharacter('론')); + + expect(transform19th(current, next)).toEqual({ + next: { + first: 'ㄴ', + middle: 'ㅗ', + last: 'ㄴ', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform19th.ts b/src/standardizePronunciation/rules/transform19th.ts new file mode 100644 index 00000000..2a4b091e --- /dev/null +++ b/src/standardizePronunciation/rules/transform19th.ts @@ -0,0 +1,21 @@ +import { arrayIncludes } from '../../_internal'; +import { 자음동화_받침_ㄴ_변환 } from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +/** + * 제19항을 적용합니다. + * @description 19항 - 받침 ‘ㅁ, ㅇ’ 뒤에 연결되는 ‘ㄹ’은 [ㄴ]으로 발음한다. + * @description [붙임] 받침 ‘ㄱ, ㅂ’ 뒤에 연결되는 ‘ㄹ’도 [ㄴ]으로 발음한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + */ +export function transform19th(currentSyllable: Syllable, nextSyllable: Syllable): Pick { + const next = { ...nextSyllable }; + const 제19항조건 = arrayIncludes(자음동화_받침_ㄴ_변환, currentSyllable.last) && next.first === 'ㄹ'; + + if (제19항조건) { + next.first = 'ㄴ'; + } + + return { next }; +} diff --git a/src/standardizePronunciation/rules/transform20th.spec.ts b/src/standardizePronunciation/rules/transform20th.spec.ts new file mode 100644 index 00000000..c61ad2e0 --- /dev/null +++ b/src/standardizePronunciation/rules/transform20th.spec.ts @@ -0,0 +1,41 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform20th } from './transform20th'; + +describe('transform20th', () => { + it('"ㄴ"은 "ㄹ"의 앞이나 뒤에서 "ㄹ"로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('난')); + const next = defined(disassembleCompleteHangulCharacter('로')); + + expect(transform20th(current, next)).toEqual({ + current: { + first: 'ㄴ', + middle: 'ㅏ', + last: 'ㄹ', + }, + next: { + first: 'ㄹ', + middle: 'ㅗ', + last: '', + }, + }); + }); + + it('첫소리 "ㄴ"이 "ㅀ, ㄾ" 뒤에 연결되는 경우에도 "ㄹ"로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('닳')); + const next = defined(disassembleCompleteHangulCharacter('는')); + + expect(transform20th(current, next)).toEqual({ + current: { + first: 'ㄷ', + middle: 'ㅏ', + last: 'ㄹㅎ', + }, + next: { + first: 'ㄹ', + middle: 'ㅡ', + last: 'ㄴ', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform20th.ts b/src/standardizePronunciation/rules/transform20th.ts new file mode 100644 index 00000000..ef62bae5 --- /dev/null +++ b/src/standardizePronunciation/rules/transform20th.ts @@ -0,0 +1,40 @@ +import { arrayIncludes } from '../../_internal'; +import { ReturnSyllables, Syllable } from './rules.types'; + +/** + * 제20항을 적용합니다. + * @description 20항 - ‘ㄴ’은 ‘ㄹ’의 앞이나 뒤에서 [ㄹ]로 발음한다. + * @description [붙임] 첫소리 ‘ㄴ’이 ‘ㅀ’, ‘ㄾ’ 뒤에 연결되는 경우에도 이에 준한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + */ +export function transform20th(currentSyllable: Syllable, nextSyllable: Syllable): ReturnSyllables { + let current = { ...currentSyllable }; + let next = { ...nextSyllable }; + + ({ current } = applyMainCondition(current, next)); + ({ next } = applySupplementaryCondition(current, next)); + + return { + current, + next, + }; +} + +function applyMainCondition(current: Syllable, next: Syllable): Pick { + const updatedCurrent = { ...current }; + + if (updatedCurrent.last === 'ㄴ' && next.first === 'ㄹ') { + updatedCurrent.last = 'ㄹ'; + } + return { current: updatedCurrent }; +} + +function applySupplementaryCondition(current: Syllable, next: Syllable): Pick { + const updatedNext = { ...next }; + + if (updatedNext.first === 'ㄴ' && (current.last === 'ㄹ' || arrayIncludes(['ㄹㅎ', 'ㄹㅌ'], current.last))) { + updatedNext.first = 'ㄹ'; + } + return { next: updatedNext }; +} diff --git a/src/standardizePronunciation/rules/transform9And10And11th.spec.ts b/src/standardizePronunciation/rules/transform9And10And11th.spec.ts new file mode 100644 index 00000000..b19a7d43 --- /dev/null +++ b/src/standardizePronunciation/rules/transform9And10And11th.spec.ts @@ -0,0 +1,44 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transform9And10And11th } from './transform9And10And11th'; + +describe('transform9And10And11th', () => { + it('9항 - 받침 "ㄲ, ㅋ" / "ㅅ, ㅆ, ㅈ, ㅊ, ㅌ" / "ㅍ"은 어말 또는 자음 앞에서 각각 대표음 "ㄱ, ㄷ, ㅂ"으로 발음한다.', () => { + const current = defined(disassembleCompleteHangulCharacter('닦')); + const next = disassembleCompleteHangulCharacter('다'); + + expect(transform9And10And11th(current, next)).toEqual({ + current: { + first: 'ㄷ', + middle: 'ㅏ', + last: 'ㄱ', + }, + }); + }); + + it('10항 - 겹받침 "ㄳ" / "ㄵ" / "ㄼ, ㄽ, ㄾ" / "ㅄ"은 어말 또는 자음 앞에서 각각 "ㄱ, ㄴ, ㄹ, ㅂ"으로 발음한다.', () => { + const current = defined(disassembleCompleteHangulCharacter('앉')); + const next = disassembleCompleteHangulCharacter('다'); + + expect(transform9And10And11th(current, next)).toEqual({ + current: { + first: 'ㅇ', + middle: 'ㅏ', + last: 'ㄴ', + }, + }); + }); + + it('11항 - 겹받침 "ㄺ" / "ㄻ" / "ㄿ"은 어말 또는 자음 앞에서 각각 "ㄱ, ㅁ, ㅂ"으로 발음한다.', () => { + const current = defined(disassembleCompleteHangulCharacter('흙')); + const next = disassembleCompleteHangulCharacter('과'); + + expect(transform9And10And11th(current, next)).toEqual({ + current: { + first: 'ㅎ', + middle: 'ㅡ', + last: 'ㄱ', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transform9And10And11th.ts b/src/standardizePronunciation/rules/transform9And10And11th.ts new file mode 100644 index 00000000..5886c250 --- /dev/null +++ b/src/standardizePronunciation/rules/transform9And10And11th.ts @@ -0,0 +1,29 @@ +import { hasProperty } from '../../utils'; +import { 받침_대표음_발음, 음가가_없는_자음 } from '../standardizePronunciation.constants'; +import { Nullable, ReturnSyllables, Syllable } from './rules.types'; + +/** + * 제9, 10항, 11항을 적용합니다. + * @description 제9항 - 받침 ‘ㄲ, ㅋ’, ‘ㅅ, ㅆ, ㅈ, ㅊ, ㅌ’, ‘ㅍ’은 어말 또는 자음 앞에서 각각 대표음 [ㄱ, ㄷ, ㅂ]으로 발음한다. + * @description 제10항 - 겹받침 ‘ㄳ’, ‘ㄵ’, ‘ㄼ, ㄽ, ㄾ’, ‘ㅄ’은 어말 또는 자음 앞에서 각각 [ㄱ, ㄴ, ㄹ, ㅂ]으로 발음한다. + * @description 제11항 - 겹받침 ‘ㄺ, ㄻ, ㄿ’은 어말 또는 자음 앞에서 각각 [ㄱ, ㅁ, ㅂ]으로 발음한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + */ +export function transform9And10And11th( + currentSyllable: Syllable, + nextSyllable: Nullable +): Pick { + const current = { ...currentSyllable }; + + const is어말 = current.last && !nextSyllable; + const is음가있는자음앞 = current.last && nextSyllable?.first !== 음가가_없는_자음; + + const 제9_10_11항주요조건 = (is어말 || is음가있는자음앞) && hasProperty(받침_대표음_발음, current.last); + + if (제9_10_11항주요조건) { + current.last = 받침_대표음_발음[current.last as keyof typeof 받침_대표음_발음]; + } + + return { current }; +} diff --git a/src/standardizePronunciation/rules/transformHardConversion.spec.ts b/src/standardizePronunciation/rules/transformHardConversion.spec.ts new file mode 100644 index 00000000..827f85a5 --- /dev/null +++ b/src/standardizePronunciation/rules/transformHardConversion.spec.ts @@ -0,0 +1,44 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transformHardConversion } from './transformHardConversion'; + +describe('transformHardConversion', () => { + it('23항 - 받침 "ㄱ(ㄲ, ㅋ, ㄳ, ㄺ), ㄷ(ㅅ, ㅆ, ㅈ, ㅊ, ㅌ), ㅂ(ㅍ, ㄼ, ㄿ, ㅄ)" 뒤에 연결되는 "ㄱ, ㄷ, ㅂ, ㅅ, ㅈ"은 된소리로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('국')); + const next = defined(disassembleCompleteHangulCharacter('밥')); + + expect(transformHardConversion(current, next)).toEqual({ + next: { + first: 'ㅃ', + middle: 'ㅏ', + last: 'ㅂ', + }, + }); + }); + + it('24항 - 어간 받침 "ㄴ(ㄵ), ㅁ(ㄻ)" 뒤에 결합되는 어미의 첫소리 "ㄱ, ㄷ, ㅅ, ㅈ"은 된소리로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('신')); + const next = defined(disassembleCompleteHangulCharacter('고')); + + expect(transformHardConversion(current, next)).toEqual({ + next: { + first: 'ㄲ', + middle: 'ㅗ', + last: '', + }, + }); + }); + + it('25항 - 어간 받침 "ㄼ, ㄾ" 뒤에 결합되는 어미의 첫소리 "ㄱ, ㄷ, ㅅ, ㅈ"은 된소리로 발음한다', () => { + const current = defined(disassembleCompleteHangulCharacter('넓')); + const next = defined(disassembleCompleteHangulCharacter('게')); + + expect(transformHardConversion(current, next)).toEqual({ + next: { + first: 'ㄲ', + middle: 'ㅔ', + last: '', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transformHardConversion.ts b/src/standardizePronunciation/rules/transformHardConversion.ts new file mode 100644 index 00000000..3062ba46 --- /dev/null +++ b/src/standardizePronunciation/rules/transformHardConversion.ts @@ -0,0 +1,30 @@ +import { arrayIncludes } from '../../_internal'; +import { hasProperty } from '../../utils'; +import { 된소리, 된소리_받침, 어간_받침 } from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +/** + * 제6장 경음화를 적용합니다. + * @description 제23항 - 받침 ‘ㄱ(ㄲ, ㅋ, ㄳ, ㄺ), ㄷ(ㅅ, ㅆ, ㅈ, ㅊ, ㅌ), ㅂ(ㅍ, ㄼ, ㄿ, ㅄ)’ 뒤에 연결되는 ‘ㄱ, ㄷ, ㅂ, ㅅ, ㅈ’은 된소리로 발음한다. + * @description 제24항 - 어간 받침 ‘ㄴ(ㄵ), ㅁ(ㄻ)’ 뒤에 결합되는 어미의 첫소리 ‘ㄱ, ㄷ, ㅅ, ㅈ’은 된소리로 발음한다. + * @description 제25항 - 어간 받침 ‘ㄼ, ㄾ’ 뒤에 결합되는 어미의 첫소리 ‘ㄱ, ㄷ, ㅅ, ㅈ’은 된소리로 발음한다. + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + */ +export function transformHardConversion( + currentSyllable: Syllable, + nextSyllable: Syllable +): Pick { + const next = { ...nextSyllable }; + + if (hasProperty(된소리, next.first)) { + const 제23항조건 = arrayIncludes(된소리_받침, currentSyllable.last); + const 제24_25항조건 = arrayIncludes(어간_받침, currentSyllable.last) && next.first !== 'ㅂ'; + + if (제23항조건 || 제24_25항조건) { + next.first = 된소리[next.first]; + } + } + + return { next }; +} diff --git a/src/standardizePronunciation/rules/transformNLAssimilation.spec.ts b/src/standardizePronunciation/rules/transformNLAssimilation.spec.ts new file mode 100644 index 00000000..6ae0dfce --- /dev/null +++ b/src/standardizePronunciation/rules/transformNLAssimilation.spec.ts @@ -0,0 +1,59 @@ +import { defined } from '../../_internal'; +import { disassembleCompleteHangulCharacter } from '../../disassembleCompleteHangulCharacter'; +import { transformNLAssimilation } from './transformNLAssimilation'; + +describe('transformNLAssimilation', () => { + it('받침이 "ㄱ, ㄴ, ㄷ, ㅁ, ㅂ, ㅇ"이고 다음 음절이 "야, 여, 요, 유, 이, 얘, 예"로 이어지는 경우', () => { + const current = defined(disassembleCompleteHangulCharacter('맨')); + const next = defined(disassembleCompleteHangulCharacter('입')); + + expect(transformNLAssimilation(current, next)).toEqual({ + current: { + first: 'ㅁ', + middle: 'ㅐ', + last: 'ㄴ', + }, + next: { + first: 'ㄴ', + middle: 'ㅣ', + last: 'ㅂ', + }, + }); + }); + + it('받침이 "ㄹ"이고 다음 음절이 "야, 여, 요, 유, 이, 얘, 예"로 이어지는 경우', () => { + const current = defined(disassembleCompleteHangulCharacter('알')); + const next = defined(disassembleCompleteHangulCharacter('약')); + + expect(transformNLAssimilation(current, next)).toEqual({ + current: { + first: 'ㅇ', + middle: 'ㅏ', + last: 'ㄹ', + }, + next: { + first: 'ㄹ', + middle: 'ㅑ', + last: 'ㄱ', + }, + }); + }); + + it('ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다', () => { + const current = defined(disassembleCompleteHangulCharacter('양')); + const next = defined(disassembleCompleteHangulCharacter('이')); + + expect(transformNLAssimilation(current, next)).toEqual({ + current: { + first: 'ㅇ', + middle: 'ㅑ', + last: 'ㅇ', + }, + next: { + first: 'ㅇ', + middle: 'ㅣ', + last: '', + }, + }); + }); +}); diff --git a/src/standardizePronunciation/rules/transformNLAssimilation.ts b/src/standardizePronunciation/rules/transformNLAssimilation.ts new file mode 100644 index 00000000..3f5d0b93 --- /dev/null +++ b/src/standardizePronunciation/rules/transformNLAssimilation.ts @@ -0,0 +1,59 @@ +import { arrayIncludes } from '../../_internal'; +import { + ㄴㄹ이_덧나는_모음, + ㄴㄹ이_덧나는_후속음절_모음, + ㄴㄹ이_덧나서_받침_ㄴ_변환, + ㄴㄹ이_덧나서_받침_ㄹ_변환, +} from '../standardizePronunciation.constants'; +import { ReturnSyllables, Syllable } from './rules.types'; + +/** + * 'ㄴ,ㄹ'이 덧나는 경우(동화작용)를 적용합니다. + * @description 합성어에서 둘째 요소가 ‘야, 여, 요, 유, 얘, 예’ 등으로 시작되는 말이면 ‘ㄴ, ㄹ’이 덧난다 + * @link https://www.youtube.com/watch?v=Mm2JX2naqWk + * @link http://contents2.kocw.or.kr/KOCW/data/document/2020/seowon/choiyungon0805/12.pdf + * @param currentSyllable 현재 음절을 입력합니다. + * @param nextSyllable 다음 음절을 입력합니다. + */ +export function transformNLAssimilation(currentSyllable: Syllable, nextSyllable: Syllable): ReturnSyllables { + let current = { ...currentSyllable }; + let next = { ...nextSyllable }; + + const ㄴㄹ이덧나는조건 = + current.last && next.first === 'ㅇ' && arrayIncludes(ㄴㄹ이_덧나는_후속음절_모음, next.middle); + + if (!ㄴㄹ이덧나는조건) { + return { + current, + next, + }; + } + + ({ current, next } = applyㄴㄹ덧남(current, next)); + + return { + current, + next, + }; +} + +function applyㄴㄹ덧남(current: Syllable, next: Syllable): ReturnSyllables { + const updatedCurrent = { ...current }; + const updatedNext = { ...next }; + + if (arrayIncludes(ㄴㄹ이_덧나는_모음, updatedCurrent.middle)) { + if (arrayIncludes(ㄴㄹ이_덧나서_받침_ㄴ_변환, updatedCurrent.last)) { + updatedCurrent.last = updatedCurrent.last === 'ㄱ' ? 'ㅇ' : updatedCurrent.last; + updatedNext.first = 'ㄴ'; + } + + if (arrayIncludes(ㄴㄹ이_덧나서_받침_ㄹ_변환, updatedCurrent.last)) { + updatedNext.first = 'ㄹ'; + } + } else { + // ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다 + updatedNext.first = updatedCurrent.last as typeof updatedNext.first; + } + + return { current: updatedCurrent, next: updatedNext }; +} diff --git a/src/standardizePronunciation/standardizePronunciation.constants.ts b/src/standardizePronunciation/standardizePronunciation.constants.ts new file mode 100644 index 00000000..a4af39ed --- /dev/null +++ b/src/standardizePronunciation/standardizePronunciation.constants.ts @@ -0,0 +1,105 @@ +export const 음가가_없는_자음 = 'ㅇ'; + +export const 한글_자모 = ['기역', '니은', '리을', '미음', '비읍', '시옷', '이응'] as const; +export const 특별한_한글_자모 = ['디귿', '지읒', '치읓', '키읔', '티읕', '피읖', '히읗'] as const; +export const 특별한_한글_자모의_발음 = { + ㄷ: 'ㅅ', + ㅈ: 'ㅅ', + ㅊ: 'ㅅ', + ㅌ: 'ㅅ', + ㅎ: 'ㅅ', + ㅋ: 'ㄱ', + ㅍ: 'ㅂ', +} as const; + +// 17항 +export const 음의_동화_받침 = { + ㄷ: 'ㅈ', + ㅌ: 'ㅊ', + ㄹㅌ: 'ㅊ', +} as const; + +// 'ㄴ,ㄹ'이 덧나는 동화작용 +export const ㄴㄹ이_덧나는_모음 = ['ㅏ', 'ㅐ', 'ㅓ', 'ㅔ', 'ㅗ', 'ㅜ', 'ㅟ']; // 모음의 ∙(아래아)가 하나일 경우 +export const ㄴㄹ이_덧나는_후속음절_모음 = ['ㅑ', 'ㅕ', 'ㅛ', 'ㅠ', 'ㅣ', 'ㅒ', 'ㅖ'] as const; +export const ㄴㄹ이_덧나서_받침_ㄴ_변환 = ['ㄱ', 'ㄴ', 'ㄷ', 'ㅁ', 'ㅂ', 'ㅇ'] as const; +export const ㄴㄹ이_덧나서_받침_ㄹ_변환 = ['ㄹ'] as const; + +// 19항 +export const 자음동화_받침_ㄴ_변환 = ['ㅁ', 'ㅇ', 'ㄱ', 'ㅂ'] as const; + +// 18항 +export const 비음화_받침_ㅇ_변환 = ['ㄱ', 'ㄲ', 'ㅋ', 'ㄱㅅ', 'ㄹㄱ'] as const; +export const 비음화_받침_ㄴ_변환 = ['ㄷ', 'ㅅ', 'ㅆ', 'ㅈ', 'ㅊ', 'ㅌ', 'ㅎ'] as const; +export const 비음화_받침_ㅁ_변환 = ['ㅂ', 'ㅍ', 'ㄹㅂ', 'ㄹㅍ', 'ㅂㅅ'] as const; + +// 12항 +export const 발음변환_받침_ㅎ = ['ㅎ', 'ㄴㅎ', 'ㄹㅎ'] as const; +export const 발음변환_받침_ㅎ_발음 = { + ㄱ: 'ㅋ', + ㄷ: 'ㅌ', + ㅈ: 'ㅊ', + ㅅ: 'ㅆ', +} as const; +export const 발음변환_첫소리_ㅎ = ['ㄱ', 'ㄹㄱ', 'ㄷ', 'ㅂ', 'ㄹㅂ', 'ㅈ', 'ㄴㅈ'] as const; +export const 발음변환_첫소리_ㅎ_발음 = { + ㄱ: 'ㅋ', + ㄹㄱ: 'ㅋ', + ㄷ: 'ㅌ', + ㅂ: 'ㅍ', + ㄹㅂ: 'ㅍ', + ㅈ: 'ㅊ', + ㄴㅈ: 'ㅊ', +} as const; + +// 9항, 10항, 11항 +export const 받침_대표음_발음 = { + ㄲ: 'ㄱ', + ㅋ: 'ㄱ', + ㄱㅅ: 'ㄱ', + ㄹㄱ: 'ㄱ', + ㅅ: 'ㄷ', + ㅆ: 'ㄷ', + ㅈ: 'ㄷ', + ㅊ: 'ㄷ', + ㅌ: 'ㄷ', + ㅍ: 'ㅂ', + ㅂㅅ: 'ㅂ', + ㄹㅍ: 'ㅂ', + ㄴㅈ: 'ㄴ', + ㄹㅂ: 'ㄹ', + ㄹㅅ: 'ㄹ', + ㄹㅌ: 'ㄹ', + ㄹㅁ: 'ㅁ', +} as const; + +export const 된소리 = { + ㄱ: 'ㄲ', + ㄷ: 'ㄸ', + ㅂ: 'ㅃ', + ㅅ: 'ㅆ', + ㅈ: 'ㅉ', +} as const; + +// 23항 +export const 된소리_받침 = [ + 'ㄱ', + 'ㄲ', + 'ㅋ', + 'ㄱㅅ', + 'ㄹㄱ', + 'ㄷ', + 'ㅅ', + 'ㅆ', + 'ㅈ', + 'ㅊ', + 'ㅌ', + 'ㅂ', + 'ㅍ', + 'ㄹㅂ', + 'ㄹㅍ', + 'ㅂㅅ', +] as const; + +// 24항, 25항 +export const 어간_받침 = ['ㄴ', 'ㄴㅈ', 'ㅁ', 'ㄹㅁ', 'ㄹㅂ', 'ㄹㅌ'] as const; diff --git a/src/standardizePronunciation/standardizePronunciation.spec.ts b/src/standardizePronunciation/standardizePronunciation.spec.ts new file mode 100644 index 00000000..8c46f8f7 --- /dev/null +++ b/src/standardizePronunciation/standardizePronunciation.spec.ts @@ -0,0 +1,354 @@ +import { standardizePronunciation } from '.'; + +describe('standardizePronunciation', () => { + describe('음절이 완성된 한글을 제외한 문자는 변경하지 않는다', () => { + it('단일 자모는 그대로 반환한다', () => { + expect(standardizePronunciation('ㄱㄴㄷㄹㅏㅓㅑㅙ')).toBe('ㄱㄴㄷㄹㅏㅓㅑㅙ'); + }); + + it('특수문자는 그대로 반환한다', () => { + expect(standardizePronunciation('!?')).toBe('!?'); + }); + + it('영어는 그대로 반환한다', () => { + expect(standardizePronunciation('hello')).toBe('hello'); + }); + + it('숫자는 그대로 반환한다', () => { + expect(standardizePronunciation('1234567890')).toBe('1234567890'); + }); + + it('빈 문자열은 그대로 반환한다', () => { + expect(standardizePronunciation('')).toBe(''); + }); + }); + + describe('한글은 음성 표기법으로 변경한다', () => { + describe('16항', () => { + it('한글 자모의 이름은 그 받침소리를 연음하되, "ㄷ, ㅈ, ㅊ, ㅋ, ㅌ, ㅍ, ㅎ"의 경우에는 특별히 다음과 같이 발음한다', () => { + expect(standardizePronunciation('디귿이')).toBe('디그시'); + expect(standardizePronunciation('지읒이')).toBe('지으시'); + expect(standardizePronunciation('치읓이')).toBe('치으시'); + expect(standardizePronunciation('키읔이')).toBe('키으기'); + expect(standardizePronunciation('티읕이')).toBe('티으시'); + expect(standardizePronunciation('피읖이')).toBe('피으비'); + expect(standardizePronunciation('히읗이')).toBe('히으시'); + }); + + it('자모의 이름이 "ㄱ, ㄴ, ㄹ, ㅁ, ㅂ, ㅅ, ㅇ"일 경우', () => { + expect(standardizePronunciation('기역이')).toBe('기여기'); + expect(standardizePronunciation('니은이')).toBe('니으니'); + expect(standardizePronunciation('리을이')).toBe('리으리'); + expect(standardizePronunciation('미음이')).toBe('미으미'); + expect(standardizePronunciation('비읍이')).toBe('비으비'); + expect(standardizePronunciation('이응이')).toBe('이응이'); + }); + }); + + describe('17항', () => { + it('받침 "ㄷ", "ㅌ(ㄾ)"이 조사나 접미사의 모음 "ㅣ"와 결합되는 경우에는, "ㅈ", "ㅊ"으로 바꾸어서 뒤 음절 첫소리로 옮겨 발음한다', () => { + expect(standardizePronunciation('곧이듣다')).toBe('고지듣따'); + expect(standardizePronunciation('굳이')).toBe('구지'); + expect(standardizePronunciation('미닫이')).toBe('미다지'); + expect(standardizePronunciation('땀받이')).toBe('땀바지'); + expect(standardizePronunciation('밭이')).toBe('바치'); + expect(standardizePronunciation('벼훑이')).toBe('벼훌치'); + }); + + it('"ㄷ" 뒤에 접미사 "히"가 결합되어 "티"를 이루는 것은 "치"로 발음한다', () => { + expect(standardizePronunciation('굳히다')).toBe('구치다'); + expect(standardizePronunciation('닫히다')).toBe('다치다'); + expect(standardizePronunciation('묻히다')).toBe('무치다'); + }); + }); + + describe('"ㄴ/ㄹ"이 덧나는 경우', () => { + it('받침이 "ㄱ, ㄴ, ㄷ, ㅁ, ㅂ, ㅇ"이고 다음 음절이 "야, 여, 요, 유, 이, 얘, 예"로 이어지는 경우', () => { + expect(standardizePronunciation('학여울')).toBe('항녀울'); + expect(standardizePronunciation('맨입')).toBe('맨닙'); + expect(standardizePronunciation('담요')).toBe('담뇨'); + expect(standardizePronunciation('영업용')).toBe('영엄뇽'); + expect(standardizePronunciation('콩엿')).toBe('콩녇'); + expect(standardizePronunciation('알약')).toBe('알략'); + }); + + it('받침이 "ㄹ"이고 다음 음절이 "야, 여, 요, 유, 이, 얘, 예"로 이어지는 경우', () => { + expect(standardizePronunciation('알약')).toBe('알략'); + expect(standardizePronunciation('서울역')).toBe('서울력'); + expect(standardizePronunciation('불여우')).toBe('불려우'); + }); + + it('ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다', () => { + expect(standardizePronunciation('고양이')).toBe('고양이'); + expect(standardizePronunciation('윤여정')).toBe('윤녀정'); + }); + }); + + describe('19항', () => { + it('받침 "ㅁ, ㅇ" 뒤에 연결되는 "ㄹ"은 "ㄴ"으로 발음한다', () => { + expect(standardizePronunciation('담력')).toBe('담녁'); + expect(standardizePronunciation('침략')).toBe('침냑'); + expect(standardizePronunciation('강릉')).toBe('강능'); + expect(standardizePronunciation('항로')).toBe('항노'); + expect(standardizePronunciation('대통령')).toBe('대통녕'); + }); + + it('받침 "ㄱ, ㅂ" 뒤에 연결되는 "ㄹ"도 "ㄴ"으로 발음한다', () => { + expect(standardizePronunciation('막론')).toBe('망논'); + expect(standardizePronunciation('석류')).toBe('성뉴'); + expect(standardizePronunciation('협력')).toBe('혐녁'); + expect(standardizePronunciation('법리')).toBe('범니'); + }); + }); + + describe('18항: 받침 "ㄱ, ㄲ, ㅋ, ㄳ, ㄺ" / "ㄷ, ㅅ, ㅆ, ㅈ, ㅊ, ㅌ, ㅎ" / "ㅂ, ㅍ, ㄼ, ㄿ, ㅄ"은 "ㄴ, ㅁ" 앞에서 "ㅇ, ㄴ, ㅁ"으로 발음한다', () => { + it('받침 "ㄱ, ㄲ, ㅋ, ㄳ, ㄺ"일 경우', () => { + expect(standardizePronunciation('먹는')).toBe('멍는'); + expect(standardizePronunciation('깎는')).toBe('깡는'); + expect(standardizePronunciation('키읔만')).toBe('키응만'); + expect(standardizePronunciation('몫몫이')).toBe('몽목씨'); + expect(standardizePronunciation('긁는')).toBe('긍는'); + }); + + it('받침 "ㄷ, ㅅ, ㅆ, ㅈ, ㅊ, ㅌ, ㅎ"일 경우', () => { + expect(standardizePronunciation('닫는')).toBe('단는'); + expect(standardizePronunciation('짓는')).toBe('진는'); + expect(standardizePronunciation('있는')).toBe('인는'); + expect(standardizePronunciation('맞는')).toBe('만는'); + expect(standardizePronunciation('쫓는')).toBe('쫀는'); + expect(standardizePronunciation('붙는')).toBe('분는'); + expect(standardizePronunciation('놓는')).toBe('논는'); + }); + + it('받침 "ㅂ, ㅍ, ㄼ, ㄿ, ㅄ"일 경우', () => { + expect(standardizePronunciation('잡는')).toBe('잠는'); + expect(standardizePronunciation('앞마당')).toBe('암마당'); + expect(standardizePronunciation('밟는')).toBe('밤는'); + expect(standardizePronunciation('읊는')).toBe('음는'); + expect(standardizePronunciation('없는')).toBe('엄는'); + }); + }); + + describe('20항', () => { + it('"ㄴ"은 "ㄹ"의 앞이나 뒤에서 "ㄹ"로 발음한다', () => { + expect(standardizePronunciation('난로')).toBe('날로'); + expect(standardizePronunciation('신라')).toBe('실라'); + expect(standardizePronunciation('천리')).toBe('철리'); + expect(standardizePronunciation('대관령')).toBe('대괄령'); + expect(standardizePronunciation('칼날')).toBe('칼랄'); + }); + + it('첫소리 "ㄴ"이 "ㅀ, ㄾ" 뒤에 연결되는 경우에도 "ㄹ"로 발음한다', () => { + expect(standardizePronunciation('닳는')).toBe('달른'); + expect(standardizePronunciation('핥네')).toBe('할레'); + }); + }); + + describe('12항', () => { + it('"ㅎ, ㄶ, ㅀ" 뒤에 "ㄱ, ㄷ, ㅈ"이 결합되는 경우에는, 뒤 음절 첫소리와 합쳐서 "ㅋ, ㅌ, ㅊ"으로 발음한다', () => { + expect(standardizePronunciation('놓고')).toBe('노코'); + expect(standardizePronunciation('좋던')).toBe('조턴'); + expect(standardizePronunciation('쌓지')).toBe('싸치'); + expect(standardizePronunciation('많고')).toBe('만코'); + expect(standardizePronunciation('않던')).toBe('안턴'); + expect(standardizePronunciation('닳지')).toBe('달치'); + }); + + it('받침 "ㄱ, ㄺ, ㄷ, ㅂ, ㄼ, ㅈ, ㄵ"이 뒤 음절 첫소리 "ㅎ"과 결합되는 경우에도, 역시 두 음을 합쳐서 "ㅋ, ㅌ, ㅍ, ㅊ"으로 발음한다', () => { + expect(standardizePronunciation('각하')).toBe('가카'); + expect(standardizePronunciation('먹히다')).toBe('머키다'); + expect(standardizePronunciation('밝히다')).toBe('발키다'); + expect(standardizePronunciation('맏형')).toBe('마텽'); + expect(standardizePronunciation('좁히다')).toBe('조피다'); + expect(standardizePronunciation('넓히다')).toBe('널피다'); + expect(standardizePronunciation('꽂히다')).toBe('꼬치다'); + expect(standardizePronunciation('앉히다')).toBe('안치다'); + }); + + it('"ㅎ, ㄶ, ㅀ" 뒤에 "ㅅ"이 결합되는 경우에는, "ㅅ"을 "ㅆ"으로 발음한다', () => { + expect(standardizePronunciation('닿소')).toBe('다쏘'); + expect(standardizePronunciation('많소')).toBe('만쏘'); + expect(standardizePronunciation('싫소')).toBe('실쏘'); + }); + + it('"ㅎ" 뒤에 "ㄴ"이 결합되는 경우에는 "ㄴ"으로 발음한다', () => { + expect(standardizePronunciation('놓는')).toBe('논는'); + expect(standardizePronunciation('쌓네')).toBe('싼네'); + }); + + it('"ㄶ, ㅀ" 뒤에 "ㄴ"이 결합되는 경우에는, "ㅎ"을 발음하지 않는다', () => { + expect(standardizePronunciation('않네')).toBe('안네'); + expect(standardizePronunciation('않는')).toBe('안는'); + expect(standardizePronunciation('뚫네')).toBe('뚤레'); + expect(standardizePronunciation('뚫는')).toBe('뚤른'); + }); + + it('"ㅎ, ㄶ, ㅀ" 뒤에 모음으로 시작된 어미나 접미사가 결합되는 경우에는 "ㅎ"을 발음하지 않는다', () => { + expect(standardizePronunciation('낳은')).toBe('나은'); + expect(standardizePronunciation('놓아')).toBe('노아'); + expect(standardizePronunciation('쌓이다')).toBe('싸이다'); + expect(standardizePronunciation('많아')).toBe('마나'); + expect(standardizePronunciation('않은')).toBe('아는'); + expect(standardizePronunciation('닳아')).toBe('다라'); + expect(standardizePronunciation('싫어도')).toBe('시러도'); + }); + + it('"다음 음절이 없는 경우"', () => { + expect(standardizePronunciation('많')).toBe('만'); + expect(standardizePronunciation('싫')).toBe('실'); + }); + }); + + describe('13항', () => { + it('홑받침이나 쌍받침이 모음으로 시작된 조사나 어미, 접미사와 결합되는 경우에는, 제 음가대로 뒤 음절 첫소리로 옮겨 발음한다', () => { + expect(standardizePronunciation('깎아')).toBe('까까'); + expect(standardizePronunciation('옷이')).toBe('오시'); + expect(standardizePronunciation('있어')).toBe('이써'); + expect(standardizePronunciation('낮이')).toBe('나지'); + expect(standardizePronunciation('앞으로')).toBe('아프로'); + }); + }); + + describe('14항', () => { + it('겹받침이 모음으로 시작된 조사나 어미, 접미사와 결합되는 경우에는, 뒤엣것만을 뒤 음절 첫소리로 옮겨 발음한다', () => { + expect(standardizePronunciation('닭을')).toBe('달글'); + expect(standardizePronunciation('젊어')).toBe('절머'); + expect(standardizePronunciation('곬이')).toBe('골씨'); + expect(standardizePronunciation('핥아')).toBe('할타'); + }); + }); + + describe('9항', () => { + it('받침 "ㄲ, ㅋ" / "ㅅ, ㅆ, ㅈ, ㅊ, ㅌ" / "ㅍ"은 어말 또는 자음 앞에서 각각 대표음 "ㄱ, ㄷ, ㅂ"으로 발음한다', () => { + expect(standardizePronunciation('닦다')).toBe('닥따'); + expect(standardizePronunciation('키읔')).toBe('키윽'); + + expect(standardizePronunciation('옷')).toBe('옫'); + expect(standardizePronunciation('있다')).toBe('읻따'); + expect(standardizePronunciation('젖')).toBe('젇'); + expect(standardizePronunciation('빚다')).toBe('빋따'); + expect(standardizePronunciation('꽃')).toBe('꼳'); + expect(standardizePronunciation('솥')).toBe('솓'); + + expect(standardizePronunciation('앞')).toBe('압'); + expect(standardizePronunciation('덮다')).toBe('덥따'); + }); + }); + + describe('10항', () => { + it('겹받침 "ㄳ" / "ㄵ" / "ㄼ, ㄽ, ㄾ" / "ㅄ"은 어말 또는 자음 앞에서 각각 "ㄱ, ㄴ, ㄹ, ㅂ"으로 발음한다', () => { + expect(standardizePronunciation('넋')).toBe('넉'); + + expect(standardizePronunciation('앉다')).toBe('안따'); + + expect(standardizePronunciation('여덟')).toBe('여덜'); + expect(standardizePronunciation('외곬')).toBe('외골'); + expect(standardizePronunciation('핥다')).toBe('할따'); + + expect(standardizePronunciation('값')).toBe('갑'); + }); + }); + + describe('11항', () => { + it('겹받침 "ㄺ" / "ㄻ" / "ㄿ"은 어말 또는 자음 앞에서 각각 "ㄱ, ㅁ, ㅂ"으로 발음한다', () => { + expect(standardizePronunciation('닭')).toBe('닥'); + expect(standardizePronunciation('맑다')).toBe('막따'); + + expect(standardizePronunciation('삶')).toBe('삼'); + expect(standardizePronunciation('젊다')).toBe('점따'); + + expect(standardizePronunciation('읊고')).toBe('읍꼬'); + expect(standardizePronunciation('읊다')).toBe('읍따'); + }); + }); + + describe('23항', () => { + it('받침 "ㄱ(ㄲ, ㅋ, ㄳ, ㄺ), ㄷ(ㅅ, ㅆ, ㅈ, ㅊ, ㅌ), ㅂ(ㅍ, ㄼ, ㄿ, ㅄ)" 뒤에 연결되는 "ㄱ, ㄷ, ㅂ, ㅅ, ㅈ"은 된소리로 발음한다', () => { + expect(standardizePronunciation('국밥')).toBe('국빱'); + expect(standardizePronunciation('깎다')).toBe('깍따'); + expect(standardizePronunciation('넋받이')).toBe('넉빠지'); + expect(standardizePronunciation('삯돈')).toBe('삭똔'); + }); + }); + + describe('24항', () => { + it('어간 받침 "ㄴ(ㄵ), ㅁ(ㄻ)" 뒤에 결합되는 어미의 첫소리 "ㄱ, ㄷ, ㅅ, ㅈ"은 된소리로 발음한다', () => { + expect(standardizePronunciation('신고')).toBe('신꼬'); + expect(standardizePronunciation('껴안다')).toBe('껴안따'); + expect(standardizePronunciation('앉고')).toBe('안꼬'); + expect(standardizePronunciation('얹다')).toBe('언따'); + expect(standardizePronunciation('삼고')).toBe('삼꼬'); + expect(standardizePronunciation('더듬지')).toBe('더듬찌'); + expect(standardizePronunciation('닮고')).toBe('담꼬'); + expect(standardizePronunciation('젊지')).toBe('점찌'); + }); + }); + + describe('25항', () => { + it('어간 받침 "ㄼ, ㄾ" 뒤에 결합되는 어미의 첫소리 "ㄱ, ㄷ, ㅅ, ㅈ"은 된소리로 발음한다.', () => { + expect(standardizePronunciation('넓게')).toBe('널께'); + expect(standardizePronunciation('핥다')).toBe('할따'); + expect(standardizePronunciation('훑소')).toBe('훌쏘'); + expect(standardizePronunciation('떫지')).toBe('떨찌'); + }); + }); + }); + + describe('경음화 등의 된소리를 적용하지 않는다', () => { + it('9항', () => { + expect( + standardizePronunciation('닦다', { + hardConversion: false, + }) + ).toBe('닥다'); + }); + + it('10항', () => { + expect( + standardizePronunciation('앉다', { + hardConversion: false, + }) + ).toBe('안다'); + }); + + it('11항', () => { + expect( + standardizePronunciation('맑다', { + hardConversion: false, + }) + ).toBe('막다'); + }); + + it('17항', () => { + expect( + standardizePronunciation('곧이듣다', { + hardConversion: false, + }) + ).toBe('고지듣다'); + }); + + it('23항', () => { + expect( + standardizePronunciation('국밥', { + hardConversion: false, + }) + ).toBe('국밥'); + }); + + it('24항', () => { + expect( + standardizePronunciation('신고', { + hardConversion: false, + }) + ).toBe('신고'); + }); + + it('25항', () => { + expect( + standardizePronunciation('넓게', { + hardConversion: false, + }) + ).toBe('널게'); + }); + }); +}); diff --git a/src/utils.spec.ts b/src/utils.spec.ts index d2660a9f..5edaa379 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -1,3 +1,4 @@ +import { arrayIncludes, isNotUndefined } from './_internal'; import { canBeChoseong, canBeJongseong, @@ -29,6 +30,7 @@ describe('hasBatchim', () => { }); it('"서" 문자에서 받침이 없으므로 false를 반환한다.', () => { expect(hasBatchim('서')).toBe(false); + expect(hasBatchim('')).toBe(false); }); it('빈 문자열은 받침이 없으므로 false를 반환한다.', () => { expect(hasBatchim('')).toBe(false); @@ -251,3 +253,48 @@ describe('canBeJongseong', () => { }); }); }); + +describe('isNotUndefined', () => { + it('정의된 값에 대해 true를 반환해야 한다', () => { + expect(isNotUndefined(5)).toBe(true); + expect(isNotUndefined('test')).toBe(true); + expect(isNotUndefined({})).toBe(true); + expect(isNotUndefined([])).toBe(true); + expect(isNotUndefined(null)).toBe(true); + }); + + it('undefined에 대해 false를 반환해야 한다', () => { + expect(isNotUndefined(undefined)).toBe(false); + }); +}); + +describe('arrayIncludes', () => { + it('값이 배열에 포함된 경우 true를 반환해야 한다', () => { + const array = ['a', 'b', 'c'] as const; + const value = 'a'; + const result = arrayIncludes(array, value); + expect(result).toBe(true); + }); + + it('값이 배열에 포함되지 않은 경우 false를 반환해야 한다', () => { + const array = ['a', 'b', 'c'] as const; + const value = 'd'; + const result = arrayIncludes(array, value); + expect(result).toBe(false); + }); + + it('undefined 값에 대해 false를 반환해야 합니다', () => { + const array = ['a', 'b', 'c'] as const; + const value = undefined; + const result = arrayIncludes(array, value); + expect(result).toBe(false); + }); + + it('검색을 시작할 인덱스를 기반으로 값을 반환합니다', () => { + const array: Array<'a' | 'b' | 'c'> = ['a', 'b', 'c']; + + const element = 'a'; + expect(arrayIncludes(array, element, 0)).toBe(true); + expect(arrayIncludes(array, element, 1)).toBe(false); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 2a5bd3c8..feef565b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ +import assert from './_internal'; import { - COMPLETE_HANGUL_START_CHARCODE, COMPLETE_HANGUL_END_CHARCODE, + COMPLETE_HANGUL_START_CHARCODE, HANGUL_CHARACTERS_BY_FIRST_INDEX, HANGUL_CHARACTERS_BY_LAST_INDEX, HANGUL_CHARACTERS_BY_MIDDLE_INDEX,