Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 한글 문장과 문자가 담긴 배열을 인자로 받아 규칙에 맞게 합성하는 assemble 함수 추가 #64

Merged
merged 17 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-yaks-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"es-hangul": minor
---

feat: 한글 문장과 문자가 담긴 배열을 인자로 받아 규칙에 맞게 합성하는 `assemble` 함수 추가
4 changes: 0 additions & 4 deletions src/_internal.ts

This file was deleted.

110 changes: 110 additions & 0 deletions src/_internal/hangul.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, it, expect, assert } from 'vitest';
import { binaryAssembleHangulCharacters, binaryAssembleHangul, isHangulAlphabet, isHangulCharacter } from './hangul';

describe('isHangul*', () => {
it('isHangulCharacter는 완성된 한글 문자를 받으면 true를 반환한다', () => {
expect(isHangulCharacter('가')).toBe(true);
expect(isHangulCharacter('값')).toBe(true);
expect(isHangulCharacter('ㄱ')).toBe(false);
expect(isHangulCharacter('ㅏ')).toBe(false);
expect(isHangulCharacter('a')).toBe(false);
});
it('isHangulAlphabet은 조합되지않은 한글 문자를 받으면 true를 반환한다', () => {
expect(isHangulAlphabet('가')).toBe(false);
expect(isHangulAlphabet('값')).toBe(false);
expect(isHangulAlphabet('ㄱ')).toBe(true);
expect(isHangulAlphabet('ㅏ')).toBe(true);
expect(isHangulAlphabet('a')).toBe(false);
});
});

describe('binaryAssembleHangulCharacters', () => {
it('초성과 중성만 조합', () => {
expect(binaryAssembleHangulCharacters('ㄱ', 'ㅏ')).toEqual('가');
});

it('초성과 중성이 합쳐진 문자와 종성을 조합', () => {
expect(binaryAssembleHangulCharacters('가', 'ㅇ')).toEqual('강');
});

it('초성과 중성과 종성이 합쳐진 문자와 자음을 조합하여 겹받침 만들기', () => {
expect(binaryAssembleHangulCharacters('갑', 'ㅅ')).toEqual('값');
});

it('초성과 중성이 합쳐진 문자와 모음을 조립하여 겹모음 만들기', () => {
expect(binaryAssembleHangulCharacters('고', 'ㅏ')).toEqual('과');
});

it('모음만 있는 문자와 모음을 조합하여 겹모음 만들기', () => {
expect(binaryAssembleHangulCharacters('ㅗ', 'ㅏ')).toEqual('ㅘ');
});

it('초성과 중성과 종성이 합쳐진 문자의 연음 법칙', () => {
expect(binaryAssembleHangulCharacters('톳', 'ㅡ')).toEqual('토스');
});

it('초성과 중성과 종성(겹받침)이 합쳐진 문자의 연음 법칙', () => {
expect(binaryAssembleHangulCharacters('닭', 'ㅏ')).toEqual('달가');
expect(binaryAssembleHangulCharacters('깎', 'ㅏ')).toEqual('까까');
});

it('문법에 맞지 않는 문자를 조합하면 단순 Join 한다 (문법 순서 틀림)', () => {
expect(binaryAssembleHangulCharacters('ㅏ', 'ㄱ')).toEqual('ㅏㄱ');
expect(binaryAssembleHangulCharacters('까', 'ㅃ')).toEqual('까ㅃ');
expect(binaryAssembleHangulCharacters('ㅘ', 'ㅏ')).toEqual('ㅘㅏ');
});

it('순서대로 입력했을 때 조합이 불가능한 문자라면 단순 Join 한다', () => {
expect(binaryAssembleHangulCharacters('뼈', 'ㅣ')).toEqual('뼈ㅣ');
});

it('소스가 두 글자 이상이라면 Invalid source 에러를 발생시킨다.', () => {
assert.throws(
() => binaryAssembleHangulCharacters('가나', 'ㄴ'),
Error,
'Invalid source character: 가나. Source must be one character.'
);
assert.throws(
() => binaryAssembleHangulCharacters('ㄱㄴ', 'ㅏ'),
Error,
'Invalid source character: ㄱㄴ. Source must be one character.'
);
});

it('다음 문자가 한글 문자 한 글자가 아니라면 Invalid next character 에러를 발생시킨다.', () => {
assert.throws(
() => binaryAssembleHangulCharacters('ㄱ', 'a'),
Error,
'Invalid next character: a. Next character must be one of the chosung, jungsung, or jongsung.'
);
assert.throws(
() => binaryAssembleHangulCharacters('ㄱ', 'ㅡㅏ'),
Error,
'Invalid next character: ㅡㅏ. Next character must be one of the chosung, jungsung, or jongsung.'
);
});
});

describe('binaryAssembleHangul', () => {
it('문장과 모음을 조합하여 다음 글자를 생성한다', () => {
expect(binaryAssembleHangul('저는 고양이를 좋아합닏', 'ㅏ')).toEqual('저는 고양이를 좋아합니다');
});

it('문장과 자음을 조합하여 홑받침을 생성한다', () => {
expect(binaryAssembleHangul('저는 고양이를 좋아하', 'ㅂ')).toEqual('저는 고양이를 좋아합');
});

it('문장과 자음을 조합하여 겹받침을 생성한다', () => {
expect(binaryAssembleHangul('저는 고양이를 좋아합', 'ㅅ')).toEqual('저는 고양이를 좋아핪');
});

it('조합이 불가능한 자음이 입력되면 단순 Join 한다', () => {
expect(binaryAssembleHangul('저는 고양이를 좋아합', 'ㄲ')).toEqual('저는 고양이를 좋아합ㄲ');
expect(binaryAssembleHangul('저는 고양이를 좋아합', 'ㅂ')).toEqual('저는 고양이를 좋아합ㅂ');
});

it('조합이 불가능한 모음이 입력되면 단순 Join 한다', () => {
expect(binaryAssembleHangul('저는 고양이를 좋아하', 'ㅏ')).toEqual('저는 고양이를 좋아하ㅏ');
expect(binaryAssembleHangul('저는 고양이를 좋아합니다', 'ㅜ')).toEqual('저는 고양이를 좋아합니다ㅜ');
});
});
143 changes: 143 additions & 0 deletions src/_internal/hangul.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import assert, { excludeLastElement, isBlank, joinString } from '.';
import { combineHangulCharacter, combineVowels, curriedCombineHangulCharacter } from '../combineHangulCharacter';
import { disassembleHangulToGroups } from '../disassemble';
import { removeLastHangulCharacter } from '../removeLastHangulCharacter';
import { canBeChosung, canBeJongsung, canBeJungsung, hasSingleBatchim } from '../utils';

export function isHangulCharacter(character: string) {
return /^[가-힣]$/.test(character);
}

export function isHangulAlphabet(character: string) {
return /^[ㄱ-ㅣ]$/.test(character);
}

/**
* @name binaryAssembleHangulAlphabets
* @description
* 두 개의 한글 자모를 합칩니다. 완성된 한글 문자는 취급하지 않습니다.
* @example
* ```
* binaryAssembleHangulAlphabets('ㄱ', 'ㅏ') // 가
* binaryAssembleHangulAlphabets('ㅗ', 'ㅏ') // ㅘ
* ```
*/
export function binaryAssembleHangulAlphabets(source: string, nextCharacter: string) {
if (canBeJungsung(`${source}${nextCharacter}`)) {
return combineVowels(source, nextCharacter);
}

const isConsonantSource = canBeJungsung(source) === false;
if (isConsonantSource && canBeJungsung(nextCharacter)) {
return combineHangulCharacter(source, nextCharacter);
}

return joinString(source, nextCharacter);
}

/**
* @name linkHangulCharacters
* @description
* 연음 법칙을 적용하여 두 개의 한글 문자를 연결합니다.
*/
export function linkHangulCharacters(source: string, nextCharacter: string) {
const sourceJamo = disassembleHangulToGroups(source)[0];
const [, lastJamo] = excludeLastElement(sourceJamo);

return joinString(removeLastHangulCharacter(source), combineHangulCharacter(lastJamo, nextCharacter));
}

/**
* @name binaryAssembleHangulCharacters
* @description
* 인자로 받은 한글 문자 2개를 합성합니다.
* ```typescript
* binaryAssembleHangulCharacters(
* // 소스 문자
* source: string
* // 다음 문자
* nextCharacter: string
* ): string
* ```
* @example
* binaryAssembleHangulCharacters('ㄱ', 'ㅏ') // 가
* binaryAssembleHangulCharacters('가', 'ㅇ') // 강
* binaryAssembleHangulCharacters('갑', 'ㅅ') // 값
* binaryAssembleHangulCharacters('깎', 'ㅏ') // 까까
*/
export function binaryAssembleHangulCharacters(source: string, nextCharacter: string) {
assert(
isHangulCharacter(source) || isHangulAlphabet(source),
`Invalid source character: ${source}. Source must be one character.`
);
assert(
isHangulAlphabet(nextCharacter),
`Invalid next character: ${nextCharacter}. Next character must be one of the chosung, jungsung, or jongsung.`
);

const sourceJamos = disassembleHangulToGroups(source)[0];

const isSingleCharacter = sourceJamos.length === 1;
if (isSingleCharacter) {
const sourceCharacter = sourceJamos[0];
return binaryAssembleHangulAlphabets(sourceCharacter, nextCharacter);
}

const [restJamos, lastJamo] = excludeLastElement(sourceJamos);

const needLinking = canBeChosung(lastJamo) && canBeJungsung(nextCharacter);
if (needLinking) {
return linkHangulCharacters(source, nextCharacter);
}
Comment on lines +88 to +91
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막 자모가 초성이 될 수 있는 문자이고 다음 입력 문자가 모음이면 연음법칙을 적용합니다.


const fixConsonant = curriedCombineHangulCharacter;
const combineJungsung = fixConsonant(restJamos[0]);
Comment on lines +93 to +94
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source가 자음과 모음이 합성된 한글 문자이니, sourceJamos의 첫 알파벳은 자음으로 확정됩니다. (만약 모음이 첫 원소라면 source의 length가 1일 수 없음)

이제 자음 1개 + 모음 1개로 이루어진, 받침이 없는 문자에 대한 대응을 시작합니다.


if (canBeJungsung(`${lastJamo}${nextCharacter}`)) {
return combineJungsung(`${lastJamo}${nextCharacter}`)();
}

if (canBeJungsung(lastJamo) && canBeJongsung(nextCharacter)) {
return combineJungsung(lastJamo)(nextCharacter);
}

const fixVowel = combineJungsung;
const combineJongsung = fixVowel(restJamos[1]);

const lastConsonant = lastJamo;

if (hasSingleBatchim(source) && canBeJongsung(`${lastConsonant}${nextCharacter}`)) {
return combineJongsung(`${lastConsonant}${nextCharacter}`);
}
Comment on lines +107 to +111
Copy link
Member Author

@evan-moon evan-moon Apr 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 처음부터 받침을 가지고 있던 문자가 입력되었다면

  1. 연음법칙
  2. "홑받침 -> 겹받침"으로 변경
  3. 규칙에 위배되면 그냥 join

3개 케이스만 대응하면 됩니다. 연음법칙은 최상단에서 대응했으니, 겹받침을 생성하는 규칙만 적용해주면 됩니다.


return joinString(source, nextCharacter);
}

/**
* @name binaryAssembleHangul
* @description
* 인자로 받은 한글 문장과 한글 문자 하나를 합성합니다.
* ```typescript
* binaryAssembleHangul(
* // 한글 문장
* source: string
* // 한글 문자
* nextCharacter: string
* ): string
* ```
* @example
* binaryAssembleHangul('저는 고양이를 좋아합닏', 'ㅏ') // 저는 고양이를 좋아합니다
* binaryAssembleHangul('저는 고양이를 좋아합', 'ㅅ') // 저는 고양이를 좋아핪
* binaryAssembleHangul('저는 고양이를 좋아하', 'ㅏ') // 저는 고양이를 좋아하ㅏ
*/
export function binaryAssembleHangul(source: string, nextCharacter: string) {
const [rest, lastCharacter] = excludeLastElement(source.split(''));
const needJoinString = isBlank(lastCharacter) || isBlank(nextCharacter);

return joinString(
...rest,
needJoinString
? joinString(lastCharacter, nextCharacter)
: binaryAssembleHangulCharacters(lastCharacter, nextCharacter)
);
}
18 changes: 18 additions & 0 deletions src/_internal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function excludeLastElement(array: string[]): [string[], string] {
const lastElement = array.at(-1);
return [array.slice(0, -1), lastElement ?? ''];
}

export function joinString(...args: string[]) {
return args.join('');
}

export function isBlank(character: string) {
return /^\s$/.test(character);
}

export default function assert(condition: boolean, errorMessage?: string): asserts condition {
if (condition === false) {
throw new Error(errorMessage ?? 'Invalid condition');
}
}
Comment on lines +6 to +18
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excludeLastElement 함수를 제외한 나머지가 이번에 추가된 함수들입니다.

14 changes: 14 additions & 0 deletions src/assemble.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { assembleHangul } from './assemble';

describe('assembleHangul', () => {
it('온전한 한글과 한글 문자 조합', () => {
expect(assembleHangul(['아버지가', ' ', '방ㅇ', 'ㅔ ', '들ㅇ', 'ㅓ갑니다'])).toEqual('아버지가 방에 들어갑니다');
});
it('온전한 한글만 조합', () => {
expect(assembleHangul(['아버지가', ' ', '방에 ', '들어갑니다'])).toEqual('아버지가 방에 들어갑니다');
});
it('온전하지 않은 한글만 조합', () => {
expect(assembleHangul(['ㅇ', 'ㅏ', 'ㅂ', 'ㅓ', 'ㅈ', 'ㅣ'])).toEqual('아버지');
});
});
22 changes: 22 additions & 0 deletions src/assemble.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { disassembleHangul } from './disassemble';
import { binaryAssembleHangul } from './_internal/hangul';

/**
* @name assembleHangul
* @description
* 인자로 받은 배열에 담긴 한글 문장과 문자를 한글 규칙에 맞게 합성합니다.
* ```typescript
* assembleHangul(
* // 한글 문자와 문장을 담고 있는 배열
* words: string[]
* ): string
* ```
* @example
* assembleHangul(['아버지가', ' ', '방ㅇ', 'ㅔ ', '들ㅇ', 'ㅓ갑니다']) // 아버지가 방에 들어갑니다
* assembleHangul(['아버지가', ' ', '방에 ', '들어갑니다']) // 아버지가 방에 들어갑니다
* assembleHangul(['ㅇ', 'ㅏ', 'ㅂ', 'ㅓ', 'ㅈ', 'ㅣ']) // 아버지
*/
export function assembleHangul(words: string[]) {
const disassembled = disassembleHangul(words.join('')).split('');
return disassembled.reduce(binaryAssembleHangul);
}
3 changes: 1 addition & 2 deletions src/chosungIncludes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { HANGUL_CHARACTERS_BY_FIRST_INDEX } from './constants';
import { disassembleHangulToGroups } from './disassemble';
import { canBeChosung, getFirstConsonants, hasValueInReadOnlyStringList } from './utils';
import { canBeChosung, getFirstConsonants } from './utils';

export function chosungIncludes(x: string, y: string) {
if (!isOnlyInitialConsonant(y)) {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export * from './disassemble';
export * from './hangulIncludes';
export * from './josa';
export * from './utils';
export * from './assemble';
export * from './combineHangulCharacter';
export * from './removeLastHangulCharacter';
29 changes: 28 additions & 1 deletion src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { describe, expect, expectTypeOf, it } from 'vitest';
import { canBeChosung, canBeJongsung, canBeJungsung, getFirstConsonants, hasBatchim, hasProperty, hasValueInReadOnlyStringList } from './utils';
import {
canBeChosung,
canBeJongsung,
canBeJungsung,
getFirstConsonants,
hasBatchim,
hasProperty,
hasSingleBatchim,
hasValueInReadOnlyStringList,
} from './utils';

describe('hasBatchim', () => {
it('should return true for the character "값"', () => {
Expand All @@ -19,6 +28,24 @@ describe('hasBatchim', () => {
});
});

describe('hasSingleBatchim', () => {
it('홑받침을 받으면 true를 반환한다.', () => {
expect(hasSingleBatchim('공')).toBe(true);
expect(hasSingleBatchim('핫')).toBe(true);
expect(hasSingleBatchim('양')).toBe(true);
expect(hasSingleBatchim('신')).toBe(true);
});
it('겹받침을 받으면 false를 반환한다.', () => {
expect(hasSingleBatchim('값')).toBe(false);
expect(hasSingleBatchim('읊')).toBe(false);
});

it('받침이 없는 문자를 받으면 false를 반환한다.', () => {
expect(hasSingleBatchim('토')).toBe(false);
expect(hasSingleBatchim('서')).toBe(false);
});
});

describe('getFirstConsonants', () => {
it('should extract the initial consonants "ㅅㄱ" from the word "사과"', () => {
expect(getFirstConsonants('사과')).toBe('ㅅㄱ');
Expand Down
Loading