방구석 개발자의 개발일지
한글 자판 영어를 한글로 변환하기 본문
전에 C++로 openframeworks 하면서 한글 입력을 구현해봤던 적이 있다.
코드는..... 끔찍했었다.
그래서 이번엔 전보다 더 깔끔하게 다시 만들어 보기로 했다.
예를 들어,
dkssudgktpdy!
처럼 영어로 한글 자판으로 친 문자열을
안녕하세요!
같이 한글로 바꿔서 출력해주는 프로그램을 만들 거다.
어려워 보일 수도 있지만,
사실 한글이 조합되는 규칙만 잘 파악한다면 그렇게 어렵진 않다.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
constexpr int UNICODE_HANGUL_START = 0xAC00; // 유니코드 한글 시작
constexpr int UNICODE_CONSONANT_START = 0x11A8 - 1; // 유니코드 자음 시작
constexpr int UNICODE_VOWEL_START = 0x1161; // 유니코드 모음 시작
const vector<string> first_list = { "r", "R", "s", "e", "E", "f", "a", "q",
"Q", "t", "T", "d", "w", "W", "c", "z", "x", "v", "g" };
const vector<string> middle_list = { "k", "o", "i", "O", "j", "p", "u", "P",
"h", "hk", "ho", "hl", "y", "n", "nj", "np", "nl", "b", "m",
"ml", "l" };
const vector<string> final_list = { " ", "r", "R", "rt", "s", "sw", "sg", "e",
"f", "fr", "fa", "fq", "ft", "fx", "fv", "fg", "a",
"q", "qt", "t", "T", "d", "w", "c", "z", "x", "v", "g" };
enum class Type {
First, // 초성
Middle, // 중성
Final, // 종성
Separate, // 독립된 글자
Other // 기타 문자
};
먼저 유니코드에서의 한글 시작 부분, 자모 리스트, 그리고 초성, 중성, 종성 등을 구분할 타입을 정의해준다.
UNICODE_HANGUL_START는 호환용 한글 자모 유니코드 시작점,
UNICODE_CONSONANT_START는 한글 자음 시작점,
UNICODE_VOWEL_START는 한글 모음 시작점이며
first_list, middle_list, final_list는 각각 초성, 중성, 종성들을 한글 배열로 영어로 쳤을 때의 문자열 리스트다.
한글 자음 시작점에서 1을 빼준 이유는 자음 시작점 + final_list의 인덱스로 자음을 구하는데,
final_list의 0번째 인덱스가 공백이기 때문이다.
받침이 없는 글자를 조합하기 위해 공백 종성을 포함시켰다
// 자모음 체크
int isConsonantOrVowel(char c) {
switch (c) {
case 'r': // ㄱ
case 'R': // ㄲ
case 's': // ㄴ
case 'e': // ㄷ
case 'E': // ㄸ
case 'f': // ㄹ
case 'a': // ㅁ
case 'q': // ㅂ
case 'Q': // ㅃ
case 't': // ㅅ
case 'T': // ㅆ
case 'd': // ㅇ
case 'w': // ㅈ
case 'W': // ㅉ
case 'c': // ㅊ
case 'z': // ㅋ
case 'x': // ㅌ
case 'v': // ㅍ
case 'g': // ㅎ
return 1;
case 'k': // ㅏ
case 'o': // ㅐ
case 'i': // ㅑ
case 'O': // ㅒ
case 'j': // ㅓ
case 'p': // ㅔ
case 'u': // ㅕ
case 'P': // ㅖ
case 'h': // ㅗ
case 'y': // ㅛ
case 'n': // ㅜ
case 'b': // ㅠ
case 'm': // ㅡ
case 'l': // ㅣ
return 2;
default:
return -1;
}
}
// 종성 결합
int combineFinal(char a, char b) {
string s(1, a);
s += b;
auto result = find(final_list.begin(), final_list.end(), s);
return result == final_list.end() ? -1 : result - final_list.begin();
}
// 중성 결합
int combineMiddle(char a, char b) {
string s(1, a);
s += b;
auto result = find(middle_list.begin(), middle_list.end(), s);
return result == middle_list.end() ? -1 : result - middle_list.begin();
}
그리고 자음/모음을 구분하기 위한 함수,
중성과 종성을 결합해 리스트에서 해당 문자의 인덱스를 반환해주는 함수를 정의해준다.
vector<Type> divide(string str) {
vector<Type> v;
for (int i = 0; i < str.length(); i++) {
if (isConsonantOrVowel(str[i]) == 1) { // 자음일때
if (i < str.length() - 1) { // 뒤에 다른 글자가 있을 때
if (isConsonantOrVowel(str[i + 1]) == 2) { // 그 글자가 모음이라면
v.push_back(Type::First); // 초성으로 넣어줌
continue;
}
if (i > 1) { // 앞에 다른 두 글자가 있을 때
if (v[i - 2] == Type::First && v[i - 1] == Type::Middle // 앞 두 글자가 초성 - 중성이고
&& isConsonantOrVowel(str[i + 1]) != 2) { // 뒤 글자가 자음이거나 기타 문자라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 2) { // 앞에 다른 세 글자가 있을 때
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 세 글자가 초성 - 중성 - 종성이고
&& isConsonantOrVowel(str[i + 1]) != 2 // 뒤 글자가 자음이거나 기타 문자이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Middle // 앞 세 글자가 초성 - 중성 - 중성이고
&& isConsonantOrVowel(str[i + 1]) != 2) { // 뒤 글자가 자음이거나 기타 문자라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 3) { // 앞에 다른 네 글자가 있을 때
if (v[i - 4] == Type::First && v[i - 3] == Type::Middle && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 네 글자가 초성 - 중성 - 중성 - 종성이고
&& isConsonantOrVowel(str[i + 1]) != 2 // 뒤 글자가 자음이거나 기타 문자이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
} else { // 뒤에 다른 글자가 없을 때
if (i > 1) { // 앞에 다른 두 글자가 있을 때
if (v[i - 2] == Type::First && v[i - 1] == Type::Middle) { // 앞 두 글자가 초성 - 중성이라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 2) { // 앞에 다른 세 글자가 있을 때
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 세 글자가 초성 - 중성 - 종성이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Middle) { // 앞 세 글자가 초성 - 중성 - 중성이라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 3) { // 앞에 다른 네 글자가 있을 때
if (v[i - 4] == Type::First && v[i - 3] == Type::Middle && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 네 글자가 초성 - 중성 - 중성 - 종성이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
}
v.push_back(Type::Separate); // 위 조건에 모두 해당되지 않는다면 독립된 글자로 넣어줌
continue;
} else if (isConsonantOrVowel(str[i]) == 2) { // 모음일때
if (i != 0) { // 앞에 다른 글자가 있을 때
if (v.back() == Type::First) { // 그 글자가 초성이라면
v.push_back(Type::Middle); // 중성으로 넣어줌
continue;
}
if (v.back() == Type::Middle // 그 글자가 중성이고
&& combineMiddle(str[i - 1], str[i]) != -1) { // 현재 모음과 결합된다면
v.push_back(Type::Middle); // 중성으로 넣어줌
continue;
}
}
v.push_back(Type::Separate); // 위 조건에 모두 해당되지 않는다면 독립된 글자로 넣어줌
continue;
} else if ((str[i] >= 32 && str[i] <= 64) || (str[i] >= 91 && str[i] <= 96) || (str[i] >= 123 && str[i] <= 126)) { // 기타 문자
v.push_back(Type::Other);
continue;
}
}
return v;
}
divide 함수는 좀 복잡하지만, 이 프로그램의 핵심 함수중 하나다.
영어로 입력받은 문자열의 각각의 문자가
초성, 중성, 종성, 독립된 문자, 기타 문자 중 어떤 것인지 분석하는 역할을 한다.
코드에 주석으로 조건을 다 적어 놨으니 참고하기 바란다.
wstring combine(string str, vector<Type> typelist) {
wstring result;
int i = 0;
while (i < str.length()) {
if (typelist[i] == Type::Separate) {
if (isConsonantOrVowel(str[i]) == 1) { // 자음이라면
if (i < str.length() - 1) { // 뒤에 글자가 더 있고
int combinedFinal = combineFinal(str[i], str[i + 1]);
if (isConsonantOrVowel(str[i + 1]) == 1 && combinedFinal != -1) { // 그 글자가 자음이고 현재 글자와 결합 가능할 때
result.push_back(UNICODE_CONSONANT_START + combinedFinal);
i++;
continue;
}
}
int idx = find(final_list.begin(), final_list.end(), string(1, str[i])) - final_list.begin();
result.push_back(UNICODE_CONSONANT_START + idx);
i++;
continue;
}
else if (isConsonantOrVowel(str[i]) == 2) { // 모음이라면
int idx = find(middle_list.begin(), middle_list.end(), string(1, str[i])) - middle_list.begin();
result.push_back(UNICODE_VOWEL_START + idx);
i++;
continue;
}
}
else if (typelist[i] == Type::Other) {
result.push_back(str[i]);
i++;
continue;
}
// 초성 : 무조건 1개
int firstIdx = 0;
if (typelist[i] == Type::First) {
firstIdx = find(first_list.begin(), first_list.end(), string(1, str[i])) - first_list.begin();
i++;
}
// 중성 : 1개 ~ 2개
int middleIdx = 0;
if (typelist[i] == Type::Middle) {
middleIdx = find(middle_list.begin(), middle_list.end(), string(1, str[i])) - middle_list.begin();
i++;
if (i < str.length()) {
if (typelist[i] == Type::Middle) {
int combinedMiddle = combineMiddle(str[i - 1], str[i]);
if (combinedMiddle != -1)
middleIdx = combinedMiddle;
i++;
}
}
}
// 종성 : 0개 ~ 2개
int finalIdx = 0;
if (i < str.length()) {
if (typelist[i] == Type::Final) {
finalIdx = find(final_list.begin(), final_list.end(), string(1, str[i])) - final_list.begin();
i++;
if (i < str.length()) {
if (typelist[i] == Type::Final) {
int combinedFinal = combineFinal(str[i - 1], str[i]);
if (combinedFinal != -1)
finalIdx = combinedFinal;
i++;
}
}
}
}
// 결합
result.push_back(UNICODE_HANGUL_START + (firstIdx * 21 * 28) + (middleIdx * 28) + finalIdx);
}
return result;
}
combine 함수 역시 이 프로그램의 핵심 함수중 하나인데,
아까 divide에서 분석해준 타입을 가지고 영어 문자열을 한글로 조합해주는 함수다.
int main(void) {
string temp;
wcout.imbue(std::locale("korean"));
while (1) {
getline(cin, temp);
auto tl = divide(temp);
wcout << combine(temp, tl) << endl;
}
return 0;
}
마지막으로 입력을 받고 위에서 정의한 함수를 이용해 한글로 바꿔서 출력해주는 메인 함수 코드다.
프로그램 전체 코드는 다음과 같다.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
constexpr int UNICODE_HANGUL_START = 0xAC00; // 유니코드 한글 시작
constexpr int UNICODE_CONSONANT_START = 0x11A8 - 1; // 유니코드 자음 시작
constexpr int UNICODE_VOWEL_START = 0x1161; // 유니코드 모음 시작
const vector<string> first_list = { "r", "R", "s", "e", "E", "f", "a", "q",
"Q", "t", "T", "d", "w", "W", "c", "z", "x", "v", "g" };
const vector<string> middle_list = { "k", "o", "i", "O", "j", "p", "u", "P",
"h", "hk", "ho", "hl", "y", "n", "nj", "np", "nl", "b", "m",
"ml", "l" };
const vector<string> final_list = { " ", "r", "R", "rt", "s", "sw", "sg", "e",
"f", "fr", "fa", "fq", "ft", "fx", "fv", "fg", "a",
"q", "qt", "t", "T", "d", "w", "c", "z", "x", "v", "g" };
enum class Type {
First, // 초성
Middle, // 중성
Final, // 종성
Separate, // 독립된 글자
Other // 기타 문자
};
// 자모음 체크
int isConsonantOrVowel(char c) {
switch (c) {
case 'r': // ㄱ
case 'R': // ㄲ
case 's': // ㄴ
case 'e': // ㄷ
case 'E': // ㄸ
case 'f': // ㄹ
case 'a': // ㅁ
case 'q': // ㅂ
case 'Q': // ㅃ
case 't': // ㅅ
case 'T': // ㅆ
case 'd': // ㅇ
case 'w': // ㅈ
case 'W': // ㅉ
case 'c': // ㅊ
case 'z': // ㅋ
case 'x': // ㅌ
case 'v': // ㅍ
case 'g': // ㅎ
return 1;
case 'k': // ㅏ
case 'o': // ㅐ
case 'i': // ㅑ
case 'O': // ㅒ
case 'j': // ㅓ
case 'p': // ㅔ
case 'u': // ㅕ
case 'P': // ㅖ
case 'h': // ㅗ
case 'y': // ㅛ
case 'n': // ㅜ
case 'b': // ㅠ
case 'm': // ㅡ
case 'l': // ㅣ
return 2;
default:
return -1;
}
}
// 종성 결합
int combineFinal(char a, char b) {
string s(1, a);
s += b;
auto result = find(final_list.begin(), final_list.end(), s);
return result == final_list.end() ? -1 : result - final_list.begin();
}
// 중성 결합
int combineMiddle(char a, char b) {
string s(1, a);
s += b;
auto result = find(middle_list.begin(), middle_list.end(), s);
return result == middle_list.end() ? -1 : result - middle_list.begin();
}
vector<Type> divide(string str) {
vector<Type> v;
for (int i = 0; i < str.length(); i++) {
if (isConsonantOrVowel(str[i]) == 1) { // 자음일때
if (i < str.length() - 1) { // 뒤에 다른 글자가 있을 때
if (isConsonantOrVowel(str[i + 1]) == 2) { // 그 글자가 모음이라면
v.push_back(Type::First); // 초성으로 넣어줌
continue;
}
if (i > 1) { // 앞에 다른 두 글자가 있을 때
if (v[i - 2] == Type::First && v[i - 1] == Type::Middle // 앞 두 글자가 초성 - 중성이고
&& isConsonantOrVowel(str[i + 1]) != 2) { // 뒤 글자가 자음이거나 기타 문자라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 2) { // 앞에 다른 세 글자가 있을 때
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 세 글자가 초성 - 중성 - 종성이고
&& isConsonantOrVowel(str[i + 1]) != 2 // 뒤 글자가 자음이거나 기타 문자이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Middle // 앞 세 글자가 초성 - 중성 - 중성이고
&& isConsonantOrVowel(str[i + 1]) != 2) { // 뒤 글자가 자음이거나 기타 문자라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 3) { // 앞에 다른 네 글자가 있을 때
if (v[i - 4] == Type::First && v[i - 3] == Type::Middle && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 네 글자가 초성 - 중성 - 중성 - 종성이고
&& isConsonantOrVowel(str[i + 1]) != 2 // 뒤 글자가 자음이거나 기타 문자이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
} else { // 뒤에 다른 글자가 없을 때
if (i > 1) { // 앞에 다른 두 글자가 있을 때
if (v[i - 2] == Type::First && v[i - 1] == Type::Middle) { // 앞 두 글자가 초성 - 중성이라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 2) { // 앞에 다른 세 글자가 있을 때
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 세 글자가 초성 - 중성 - 종성이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
if (v[i - 3] == Type::First && v[i - 2] == Type::Middle && v[i - 1] == Type::Middle) { // 앞 세 글자가 초성 - 중성 - 중성이라면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
if (i > 3) { // 앞에 다른 네 글자가 있을 때
if (v[i - 4] == Type::First && v[i - 3] == Type::Middle && v[i - 2] == Type::Middle && v[i - 1] == Type::Final // 앞 네 글자가 초성 - 중성 - 중성 - 종성이고
&& combineFinal(str[i - 1], str[i]) != -1) { // 앞 종성과 현재 자음이 결합된다면
v.push_back(Type::Final); // 종성으로 넣어줌
continue;
}
}
}
v.push_back(Type::Separate); // 위 조건에 모두 해당되지 않는다면 독립된 글자로 넣어줌
continue;
} else if (isConsonantOrVowel(str[i]) == 2) { // 모음일때
if (i != 0) { // 앞에 다른 글자가 있을 때
if (v.back() == Type::First) { // 그 글자가 초성이라면
v.push_back(Type::Middle); // 중성으로 넣어줌
continue;
}
if (v.back() == Type::Middle // 그 글자가 중성이고
&& combineMiddle(str[i - 1], str[i]) != -1) { // 현재 모음과 결합된다면
v.push_back(Type::Middle); // 중성으로 넣어줌
continue;
}
}
v.push_back(Type::Separate); // 위 조건에 모두 해당되지 않는다면 독립된 글자로 넣어줌
continue;
} else if ((str[i] >= 32 && str[i] <= 64) || (str[i] >= 91 && str[i] <= 96) || (str[i] >= 123 && str[i] <= 126)) { // 기타 문자
v.push_back(Type::Other);
continue;
}
}
return v;
}
wstring combine(string str, vector<Type> typelist) {
wstring result;
int i = 0;
while (i < str.length()) {
if (typelist[i] == Type::Separate) {
if (isConsonantOrVowel(str[i]) == 1) { // 자음이라면
if (i < str.length() - 1) { // 뒤에 글자가 더 있고
int combinedFinal = combineFinal(str[i], str[i + 1]);
if (isConsonantOrVowel(str[i + 1]) == 1 && combinedFinal != -1) { // 그 글자가 자음이고 현재 글자와 결합 가능할 때
result.push_back(UNICODE_CONSONANT_START + combinedFinal);
i++;
continue;
}
}
int idx = find(final_list.begin(), final_list.end(), string(1, str[i])) - final_list.begin();
result.push_back(UNICODE_CONSONANT_START + idx);
i++;
continue;
}
else if (isConsonantOrVowel(str[i]) == 2) { // 모음이라면
int idx = find(middle_list.begin(), middle_list.end(), string(1, str[i])) - middle_list.begin();
result.push_back(UNICODE_VOWEL_START + idx);
i++;
continue;
}
}
else if (typelist[i] == Type::Other) {
result.push_back(str[i]);
i++;
continue;
}
// 초성 : 무조건 1개
int firstIdx = 0;
if (typelist[i] == Type::First) {
firstIdx = find(first_list.begin(), first_list.end(), string(1, str[i])) - first_list.begin();
i++;
}
// 중성 : 1개 ~ 2개
int middleIdx = 0;
if (typelist[i] == Type::Middle) {
middleIdx = find(middle_list.begin(), middle_list.end(), string(1, str[i])) - middle_list.begin();
i++;
if (i < str.length()) {
if (typelist[i] == Type::Middle) {
int combinedMiddle = combineMiddle(str[i - 1], str[i]);
if (combinedMiddle != -1)
middleIdx = combinedMiddle;
i++;
}
}
}
// 종성 : 0개 ~ 2개
int finalIdx = 0;
if (i < str.length()) {
if (typelist[i] == Type::Final) {
finalIdx = find(final_list.begin(), final_list.end(), string(1, str[i])) - final_list.begin();
i++;
if (i < str.length()) {
if (typelist[i] == Type::Final) {
int combinedFinal = combineFinal(str[i - 1], str[i]);
if (combinedFinal != -1)
finalIdx = combinedFinal;
i++;
}
}
}
}
// 결합
result.push_back(UNICODE_HANGUL_START + (firstIdx * 21 * 28) + (middleIdx * 28) + finalIdx);
}
return result;
}
int main(void) {
string temp;
wcout.imbue(std::locale("korean"));
while (1) {
getline(cin, temp);
auto tl = divide(temp);
wcout << combine(temp, tl) << endl;
}
return 0;
}
전보다 나아졌긴 하지만 여전히 더럽긴 하다...