"[C#] 초성을 포함한 StartWith 함수 만들기" (링크: https://pichen.tistory.com/56 ) 글에서 ㄱ부터 ㅎ까지를 모두 직접 적어서 switch 돌리는 것에 계산식이 있지 않겠냐고 하는 반응을 보았습니다.
[C#] 초성을 포함한 StartWith 함수 만들기
개발자 중 누군가 초성을 포함하여 검색하는 방법을 물었는데, 시간이 남아서 개발을 했다가 초안이 맘에 안들어서 퇴근 후 공부를 마친 시간에 보완작업을 해보았습니다. 현재 다니는 회사에
pichen.tistory.com
결론적으로 말하면 1편에서 쓰는 방법을 쓰는 것이 성능면에서 월등해보입니다만, char에 대한 이해도를 높이기 위해 계산을 하는 과정을 보여드리려고 합니다. 먼저 각 글자들의 유니코드를 외우고 다니고 있지 않기 때문에 지난 글의 소스인
아래 이미지 부분을 복사하여 엑셀을 열어 붙여넣기합니다. 엑셀을 활용하는 방법은 다른 글 또는 검색으로 찾아보세요!
(이 글을 작성하는 24년 5월 9일까지는 엑셀 관련글은 블로그에 없긴 합니다.)
엑셀에 붙여 넣기 후 [텍스트 나누기] 기능으로 텍스트 중 유니코드가 궁금한 '' 들을 분리해냅니다.
그리고 나서, 컬럼만 복사 후 값으로 붙여넣기한 다음에 로그를 찍을 모양을 만들어 줍니다.
아래 이미지 처럼 만들때 당연하게 손으로 쓰는 부분은 L 컬럼 뿐이어야 엑셀을 쓰는 보람이 있습니다.
M열 중 첫행 하나의 값을 만들고 + 눌러서 누르면 됩니다.
참고로 M1의 셀에 입력한 값은 =$L$1&I1&$L$2&$L$3&I1&$L$4&$L$1&J1&$L$2&$L$3&J1&$L$4&$L$1&K1&$L$2&$L$3&K1&$L$2
이건데요, 실제로는 셀 클릭 -> & -> 셀클릭 방식으로 만든거예요. 열과행에 $가 붙는 것은 F4를 누르면 만들 수 있고 자동완성시 고정될 셀에 지정하면 됩니다.(L컬럼의 값들)
로그를 찍어보려고 하는거라 결국은 복사하여 IDE에 붙여넣기 합니다!
Console.Write($@" 요기에 붙여넣기 하세요! ");
$@를 둘다 붙이는 이유는 $는 보간문자열을 이용하기 위해서고 (그래서 엑셀에서 굳이 {와 }를 붙여서 가져왔어요.
@를 안쓰면 문장 띄어쓰기할때마다
$" ~~~~ " +
$" ~~~~ " +
$" ~~~~ " 이렇게 붙여줘야하는데요, @를 쓰면 이 과정을 생략해도 일관된 값으로 처리됩니다! @의 다른 모든 기능들은 검색해보세요!
출력값은 이렇게 나옵니다.
이제 초성 검색을 위해 필요한 유니코드들을 알았죠!
유니코드간의 규칙을 찾기 위해 다시 엑셀로 붙여넣기 하고 앞서 설명드렸던 텍스트 나누기 - 기준 "공백"을 적용하여
숫자만 분리해낸 뒤, 제가 ㄲㄸㅃㅆㅉ를 제일 뒤에 적었어서, 자음의 유니코드 순서에 맞게 정렬합니다. 엑셀의 필터기능을 이용하면 쉬운데, 방법을 모르신다면 수동으로 정렬하셔도 좋습니다.
유니코드가 비어있는 부분을 혹시 캐치하셨나요? ㄲ과 ㄴ 사이 12595가 없고, ㄹ과 ㅁ 사이에 12602~12608이 없죠,
위에 로그를 찍던 방식으로 찍어보시면 알겠지만 ㄳ, ㄺ,ㄻ,ㄼ,ㄽ,ㄾ,ㄿ,ㅀ 로 초성으로는 올 수 없는 애들이 껴있어요.
가-힣에는 해당 초성이 받침 이외에서 쓰이는 글자는 없죠!
그래서 이 부분들만 남기면 약간의 규칙이 생긴답니다.
ㄱ 과 가 간에는 31439 차이가 있는데, 가 -> 까, 까 -> 나 등등 은 유니코드 차이가 정확히 588이라는 얘기입니다.
또한 가와 깋, 그러니까 ㄱ으로 시작하는 가장 첫 글자와 ㄱ으로 시작하는 가장마지막 글자의 차이가 587,
어떤 초성을 고르더라도 그 차이는 동일하게 587이라는 결과가 위 엑셀 캡처에서 드러났습니다.
따라서, char 'ㄱ'의 int 값 12593에 31439를 더하면 '가'의 int값이 되고, 여기서 587을 더하면 '깋'의 int 값이 됩니다.
char'ㄴ'의 int 값은, 12593 ('ㄱ') + 31439 + 588, 여기서 582을 더하면 '닣'의 int값이 됩니다.
이해가 되시나요?
이 내용을 c#에서 적어보면, 아래와 같습니다. 규칙이 보이시나요?
char gi_eok = 'ㄱ';
char ga = (char)((int)gi_eok + 31439);
char gih = (char)((int)ga + 587);
char ni_en = 'ㄴ';
char na = (char)((int)gi_eok + 31439 + 588);
char nih = (char)((int)na + 587);
char di_geut = 'ㄷ';
char da = (char)((int)gi_eok + 31439 + 588*2);
char dih = (char)((int)da + 587);
이 규칙을 토대로 초성일때는 초성이 들어가는 글자에서 모두 true를 리턴할 수 있도록 함수를 작성하였습니다.
bool IsChosung2(char OriginWordChar, char keywordChar)
{
List<char> chosungs = new() { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' };
if (chosungs.Contains(keywordChar))
{
int startCharacter = (int)'ㄱ' + 31439 + 588 * chosungs.IndexOf(keywordChar);
int endCharacter = startCharacter + 587;
return startCharacter <= OriginWordChar && OriginWordChar <= endCharacter;
}
else
{
return OriginWordChar == keywordChar;
}
}
///////////////////////////////////////////////////
// 참고로 지난 글에서 이부분의 함수는 아래와 같습니다.
bool IsChosung1(char OriginWordChar, char keywordChar) => keywordChar switch
{
'ㄱ' => '가' <= OriginWordChar && OriginWordChar <= '깋',
'ㄴ' => '나' <= OriginWordChar && OriginWordChar <= '닣',
'ㄷ' => '다' <= OriginWordChar && OriginWordChar <= '딯',
'ㄹ' => '라' <= OriginWordChar && OriginWordChar <= '맇',
'ㅁ' => '마' <= OriginWordChar && OriginWordChar <= '밓',
'ㅂ' => '바' <= OriginWordChar && OriginWordChar <= '빟',
'ㅅ' => '사' <= OriginWordChar && OriginWordChar <= '싷',
'ㅇ' => '아' <= OriginWordChar && OriginWordChar <= '잏',
'ㅈ' => '자' <= OriginWordChar && OriginWordChar <= '짛',
'ㅊ' => '차' <= OriginWordChar && OriginWordChar <= '칳',
'ㅋ' => '카' <= OriginWordChar && OriginWordChar <= '킿',
'ㅌ' => '타' <= OriginWordChar && OriginWordChar <= '팋',
'ㅍ' => '파' <= OriginWordChar && OriginWordChar <= '핗',
'ㅎ' => '하' <= OriginWordChar && OriginWordChar <= '힣',
'ㄲ' => '까' <= OriginWordChar && OriginWordChar <= '낗',
'ㄸ' => '따' <= OriginWordChar && OriginWordChar <= '띻',
'ㅃ' => '빠' <= OriginWordChar && OriginWordChar <= '삫',
'ㅆ' => '싸' <= OriginWordChar && OriginWordChar <= '앃',
'ㅉ' => '짜' <= OriginWordChar && OriginWordChar <= '찧',
_ => keywordChar == OriginWordChar
};
지난 글에서 작성한 부분 중 뒷부분에 대한 설명이라 앞부분은 그냥 가져왔습니다.
방금 작성한 IsChosung2()가 아닌 다른 부분이 궁금하시다면
지난글인 "[C#] 초성을 포함한 StartWith 함수 만들기" (링크: https://pichen.tistory.com/56 ) 를 참고해주세요!
//초성 검색 테스트
string keyword1 = "ㄱ나ㄷ";
string keyword2 = "aㄱ나";
List<string> words = new List<string> { "가나다", "가노", "가난", "가나두", "가나라", "고리", "기체", "깋체", "나다라", "다라마", "가나a", "a가나", "a가나다" };
List<string> list1 = words.FindAll(w => StartWithKeyword(keyword1, w));
List<string> list2 = words.FindAll(w => StartWithKeyword(keyword2, w));
Print(keyword1, list1);
Print(keyword2, list2);
void Print(string keyword, List<string> papers) => papers.ForEach(p => Console.WriteLine($"검색어: {keyword} - {p}"));
bool StartWithKeyword(string keyword, string word)
{
if (keyword.Length > word.Length)
{
return false;
}
for (var i = 0; i < word.Length && i < keyword.Length; i++)
{
//bool charResult = IsChosung1(word[i], keyword[i]);
bool charResult = IsChosung2(word[i], keyword[i]);
if (!charResult)
{
return false;
}
}
return true;
}
bool IsChosung2(char OriginWordChar, char keywordChar)
{
List<char> chosungs = new() { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' };
if (chosungs.Contains(keywordChar))
{
int startCharacter = (int)'ㄱ' + 31439 + 588 * chosungs.IndexOf(keywordChar);
int endCharacter = startCharacter + 587;
return startCharacter <= OriginWordChar && OriginWordChar <= endCharacter;
}
else
{
return OriginWordChar == keywordChar;
}
}
IsChosung함수의 길이는 짧아졌는데, 성능도 과연 그럴까?
정답은 아닙니다.
먼저 방금 작성된 함수는 숫자를 매번계산해야 합니다. => 따라서 성능을 개선하기 위해서는 변화하는 값이 아니기 때문에 미리 계산하거나 하드코딩으로 클래스로 초성, 시작글자, 마지막글자의 int나 char값을 작성 후 컬렉션에 넣어서 관리해야 첫번째 함수와 유사한 퍼포먼스를 낼 수 있을 것입니다.
그러나 이러한 개발과정과 검토가 개발실력에 많은 도움이 될 것이라 믿습니다. 감사합니다.
'language & Framework > C#' 카테고리의 다른 글
[C#] 깊이 우선 탐색 (DFS) 개발 적용 사례 - 폴더구조 찾기 (5) | 2024.12.17 |
---|---|
[C#] 초성을 포함한 StartWith 함수 만들기 (1) | 2024.05.08 |
[c# / .NET] 설정 파일을 읽는 법에 대하여(.json/.ini) (0) | 2024.03.19 |