반응형

폴더 형태의 구조를 담은 DB 또는 그러한 형태를 반환하는 API를 경험할 일이 많아졌다.

 

[상위폴더 여부 | 상위 폴더 Id | 폴더 Id | 폴더 이름]

 

간추려 위와 같은 컬럼이 있는 DB가 있거나,

유사형태의 값을 반환하는 API를 만났다.

 

DB만 있었다면 DB에서 recurrsive 테이블로 재귀로 뽑아 내는 것도 방법이겠지만

API를 사용해야만 하는 시점에서는 위 형태의 반환 값을 폴더구조로 풀어내는 것에 대한 고민이 있었다.

 

(공부 뒤 블로그나 github에 올리지 못한 내용이 더 많아서 공부를 뜨문뜨문한다고 보실 분들도 있겠지만...

필자는 계속 책을 구매하거나 인강을 듣거나 아주 작은 단위의 프로젝트를 혼자 진행하며

공부하고 찾아보면서 학습중이었다는 점....)

 

 

그러다 DFS를 여기 적용할 수 있겠다는 생각이 들었다.

이 포스트에서는 그러한 예제를 풀어가려한다.

 

 

재직회사에서 C#을 쓰고 있어 C#으로 개발하였으니 다른언어를 하셨더라도 원리로 가져가면 될 것 같다.

 

 

특히,

폴더명으로 검색하기에는

a \ b

a \ c \ b 

의 경우 처럼 이름은 같지만 다른 위치에 있는 것을 프로그램은 구분해서 조회할 수 없기 때문에 폴더구조로 뽑아오거나 id를 뽑아오는 것은 매우 필연적이기 떄문에 이 작업이 필요할때가 많다.

 

 

먼저 아래와 같은 클래스를 작성했다.

DB에서 4개 컬럼을 읽어다 넣었거나, API 반환값이거나, 혹은 읽거나 받은 정보를 여기에 담았다고 가정하자.

class Folder
{
    public int ParentId { get; set; }
    public int Id { get; set; }
    public bool HasParent { get; set; }
    public string Name { get; set; }
    
    public string NameHierarchy { get; set; }
    public string IdHierarchy { get; set; }
}

 

 

Folder 클래스의 인스턴스에 HasParent, ParentId, Id, Name이 기입 되어있는 List가 있다고 가정하자.

(NameHierarchy, IdHierarchy는 폴더구조를 \ 구분자로 만들 필드로 처음엔 빈칸으로 시작한다고 가정한다.)

List<Folder> wholeFolders = new List<Folder>();

 

 

DFS 알고리즘을 적용하기 위해 시작 노드를 정리할 필요가 있다.

다만, 폴더구조를 로그를 찍었을때 직관적으로 더 잘 보이게 하기위해 visited는 생성하지 않는다.

 

HasParent 같은 친절한 필드 또는 컬럼이 없는 경우가 있겠지만 그런경우에는 parent id가 null이거나 0 등 특별히 최상위 폴더를 알 수 있는 조건이 있을 것이다. 여기서는 HasParent가 true가 아니면 최상위 폴더이다.

List<Folder> rootfolders = wholeFolders.Where(f => !f.HasParent)
				.ToList();

 

 

위에서 filtering한  인스턴스들을 Stack을 선언하여 노드로 push한다.

최상위 폴더는 폴더이름, 폴더 id 자체가 폴더구조 이므로 이 값을 Hierarchy류 필드에 넣는다.

Stack<Folder> stack = new Stack<Folder>();
foreach (var rootfolder in rootfolders)
{             
    rootfolder.IdHierarchy = rootfolder.Id.ToString();
    rootfolder.NameHierarchy = rootfolder.Name;
    stack.Push(rootfolder);
}

 

 

이제 루프를 돌린다.

첫 번째 순회에서는 root folder들을 stack에 쌓았으므로

가장 마지막 node를 pop하여, 전체 폴더 중에서 parent id가 root folder의 id와 동일한 폴더들을 찾아 리스트로 반환하게 된다.

그렇게 조회한 하위 폴더를 상위폴더\현재폴더 값으로 Hierarchy류에 입력하고 stack에 push한다.

다음루프에서는 방금 중 가장마지막에 push한 폴더를 꺼내 다시 한번 절차를 거치게 된다.

이렇게. 최상위폴더에서 최하위 폴더까지 순회 후 다음 노드로 넘어가게 된다.

Folder folder = new Folder();
while (stack.Count > 0)
{
    folder = stack.Pop();               
    foreach (var child in wholeFolders.Where(f => f.ParentId == folder.Id))
    {
        child.NameHierarchy = $@"{folder.NameHierarchy}\{child.Name}";
        child.IdHierarchy = $@"{folder.IdHierarchy}\{child.Id}";
        stack.Push(child);
    }
}

 

한번에 작성해보면

List<Folder> wholeFolders = new List<Folder>(); // 무언가 조회된 것이라 가정한다.

Stack<Folder> stack = new Stack<Folder>();
foreach (var rootfolder in wholeFolders.Where(f => !f.HasParent)) // 1회 순회 - IEnumerable사용
{             
    rootfolder.IdHierarchy = rootfolder.Id.ToString();
    rootfolder.NameHierarchy = rootfolder.Name;
    stack.Push(rootfolder);
}


Folder folder = new Folder();
while (stack.Count > 0)
{
    folder = stack.Pop();               
    foreach (var child in wholeFolders.Where(f => f.ParentId == folder.Id))  // 1회 순회 - IEnumerable사용
    {
        child.NameHierarchy = $@"{folder.NameHierarchy}\{child.Name}";
        child.IdHierarchy = $@"{folder.IdHierarchy}\{child.Id}";
        stack.Push(child);
    }
}


class Folder
{
    public int ParentId { get; set; }
    public int Id { get; set; }
    public bool HasParent { get; set; }
    public string Name { get; set; }
    
    public string NameHierarchy { get; set; }
    public string IdHierarchy { get; set; }
}

 

DFS를 폴더구조화에 적용한 예제였다. DFS가 나같은 비전공자에게는 한번에 와닿지 않으나, DFS의 개념을 이해는 못한체 외우고만 있다가 필요할때 써보려고하니 어떤 로직으로 어떻게 적용되는지 이해가 되어 기뻤던 개발이었다.

 

 

반응형
반응형

 

 

"[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값을 작성 후 컬렉션에 넣어서 관리해야 첫번째 함수와 유사한 퍼포먼스를 낼 수 있을 것입니다.

 

그러나 이러한 개발과정과 검토가 개발실력에 많은 도움이 될 것이라 믿습니다. 감사합니다.

반응형
반응형

개발자 중 누군가 초성을 포함하여 검색하는 방법을 물었는데, 시간이 남아서 개발을 했다가 초안이 맘에 안들어서 퇴근 후 공부를 마친 시간에 보완작업을 해보았습니다.

 

현재 다니는 회사에서 사용하는 언어가 C#인데, 회사에 적용해도 좋을 것 같아서 C#을 사용했습니다.

 

일단 전체 검색대상 목록을 List<string>에 담았다고 가정하여, 

다음과 같은 List를 선언합니다.

List<string> words = new List<string> { "가나다", "가노", "가난", "가나두", "가나라", "고리", "기체", "깋체", "나다라", "다라마", "가나a", "a가나", "a가나다" };

 

그리고 검색어를 전체 구현이라면 앞단에서 받아야 겠지만, 콘솔을 통한 입력도 받지 않고, 선언을 하겠습니다.

 

검색어는 

string keyword1 = "ㄱ나ㄷ";
string keyword2 = "aㄱ나";

이렇게 두개로 하겠습니다.

 

컬렉션에서 어떤 조건을 만족하는 모든 값을 컬렉션으로 반환하는 함수인 FindAll()을 사용하도록 할텐데요,

words.FindAll(w => 어떤함수(w))로 사용할 어떤함수를 작성해야겠죠.

 

words.FindAll(word => 어떤함수( word )) 는 아래와 같은 기능을 한다고 이해하지면 됩니다. 실제로는 변수에 델리게이트를 사용하는 것이지만, 이해를 돕기 위해 작성한 것입니다.

List<string> 가짜FindAll(List<string> words){
    List<string> a = new();
    foreach(string word in words) 
    {
         if(어떤함수(word))
             a.Add(word)
    }
    return a;
}

 

그러면 어떻게 해야 초성에 맞는 한글들을 표현해낼 것인가를 고민해야겠죠.

 

우리는 char라는 자료형을 알고있죠. 유니코드로 숫자 범위로서 한글에 대한 범위를 지정해볼 수 있습니다.

따라서 검색어 중 어떤 글자가 'ㄱ', 'ㄴ' 등의 자음 초성일때, 가~깋, 나 ~닣가 포함되어있는지를 범위로 지정해볼 생각을 할 수 있습니다.

 

간단하게 검색어와 실제단어를 비교한다고 생각하고, n번째 글자가 ㄱ이면 원래 글자가 '가'와 '깋' 사이에 있는지 여부를 반환합니다.

if(검색어 중 n번째 글자 == 'ㄱ') {

  	원래 글자 중 n번째 글자 >= '가' && 원래 글자 중 n번째 글자 <= '깋'

}

 

받침 등이 자음마다 달라서 아주 일정한 규칙이 있는 것이 아니라서 이 작업을 ㄱ~ㅎ까지 모두 해주어야 합니다.

엑셀을 이용해도 됩니다.

 

그런데 우리는 switch 변수구문을 작성한 적이 있기 때문에 아래와 같이 작성합니다.

저 같은 경우는 ㄱ~ㅎ, 가~하, 깋~힣은 gpt에게 한글을 작성해달라고 하고, 엑셀에서 목록을 만들어서 다시 vs에 붙여넣기 했답니다. 초성이 아닌 경우에는 검색어와 원래 단어의 n번째 글자가 같으면 되기 때문에 _ => unicode == OriginChar로 작성하였습니다. OriginChar는 원래 목록에 있던 단어의 n번째 글자이고 unicode는 검색어의 n번째 글자입니다. 지금보니 변수명을 잘못지었지만, 이미 개발했으니 그대로 쓰겠습니다.

 

bool IsChosung(char OriginChar, char unicode) => unicode switch 
{
    'ㄱ' => '가' <= OriginChar && OriginChar <= '깋',
    'ㄴ' => '나' <= OriginChar && OriginChar <= '닣',
    'ㄷ' => '다' <= OriginChar && OriginChar <= '딯',
    'ㄹ' => '라' <= OriginChar && OriginChar <= '맇',
    'ㅁ' => '마' <= OriginChar && OriginChar <= '밓',
    'ㅂ' => '바' <= OriginChar && OriginChar <= '빟',
    'ㅅ' => '사' <= OriginChar && OriginChar <= '싷',
    'ㅇ' => '아' <= OriginChar && OriginChar <= '잏',
    'ㅈ' => '자' <= OriginChar && OriginChar <= '짛',
    'ㅊ' => '차' <= OriginChar && OriginChar <= '칳',
    'ㅋ' => '카' <= OriginChar && OriginChar <= '킿',
    'ㅌ' => '타' <= OriginChar && OriginChar <= '팋',
    'ㅍ' => '파' <= OriginChar && OriginChar <= '핗',
    'ㅎ' => '하' <= OriginChar && OriginChar <= '힣',
    'ㄲ' => '까' <= OriginChar && OriginChar <= '낗',
    'ㄸ' => '따' <= OriginChar && OriginChar <= '띻',
    'ㅃ' => '빠' <= OriginChar && OriginChar <= '삫',
    'ㅆ' => '싸' <= OriginChar && OriginChar <= '앃',
    'ㅉ' => '짜' <= OriginChar && OriginChar <= '찧',
    _ => unicode == OriginChar
};

 

이 작업을 검색어와 단어목록의 단어중 하나에서 글자단위로 진행해야하므로, foreach(char a in string값) 또는 for문에 대입하겠습니다.

 

여기서는 검색어와 단어를 index단위로 비교해야하므로, for문이 적절하고, 둘 중 짧은 곳에 맞추어 돌려야 하므로 조건에 둘다 작성합니다.

 

IsChosung의 값이 false이면 더이상 뒷부분을 확인하지 않아도 되므로, false를 반환하고 루프를 끝냅니다.

bool 어떤함수?(string keyword, string word){
    for (var i = 0; i < word.Length && i < keyword.Length; i++)
    {
        bool charResult = IsChosung(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}

여기까지만 하면 문제가 발생하는데 찾으셨나요?

 

바로 검색어가 단어보다 길어도 앞부분까지만 매칭이 되면 true를 반환하는 문제가 생기죠.

 

그래서 앞에 길이를 비교하는 조건이 필요합니다.

bool 어떤함수?(string keyword, string word){
	if (keyword.Length > word.Length)
    {
        return false;
    }
    
    for (var i = 0; i < word.Length && i < keyword.Length; i++)
    {
        bool charResult = IsChosung(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}

 

이제 출력의 시간입니다.

독자분들의 편의를 위해서 앞서 작성한 부분들도 기입하겠습니다.

 

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);


// foreach console.writeline 두번안쓰려고 선언한 함수
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 = IsChosung(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}

bool IsChosung(char OriginChar, char unicode) => unicode switch 
{
    'ㄱ' => '가' <= OriginChar && OriginChar <= '깋',
    'ㄴ' => '나' <= OriginChar && OriginChar <= '닣',
    'ㄷ' => '다' <= OriginChar && OriginChar <= '딯',
    'ㄹ' => '라' <= OriginChar && OriginChar <= '맇',
    'ㅁ' => '마' <= OriginChar && OriginChar <= '밓',
    'ㅂ' => '바' <= OriginChar && OriginChar <= '빟',
    'ㅅ' => '사' <= OriginChar && OriginChar <= '싷',
    'ㅇ' => '아' <= OriginChar && OriginChar <= '잏',
    'ㅈ' => '자' <= OriginChar && OriginChar <= '짛',
    'ㅊ' => '차' <= OriginChar && OriginChar <= '칳',
    'ㅋ' => '카' <= OriginChar && OriginChar <= '킿',
    'ㅌ' => '타' <= OriginChar && OriginChar <= '팋',
    'ㅍ' => '파' <= OriginChar && OriginChar <= '핗',
    'ㅎ' => '하' <= OriginChar && OriginChar <= '힣',
    'ㄲ' => '까' <= OriginChar && OriginChar <= '낗',
    'ㄸ' => '따' <= OriginChar && OriginChar <= '띻',
    'ㅃ' => '빠' <= OriginChar && OriginChar <= '삫',
    'ㅆ' => '싸' <= OriginChar && OriginChar <= '앃',
    'ㅉ' => '짜' <= OriginChar && OriginChar <= '찧',
    _ => unicode == OriginChar
};

 

결과는 다음과 같습니다.

 

영어를 포함해도 뒤에 초성이 바르게 검색되는 것을 확인했습니다.

 

 

 

반복되는 형태의 문장들인데 계산식으로 만들 수 없을까? 궁금하신 분들은 2편을 참고해주세요.

https://pichen.tistory.com/57

 

[C#] 초성을 포함한 StartWith 함수 만들기 2편, char의 유니코드 값으로 계산해보자.

"[C#] 초성을 포함한 StartWith 함수 만들기" (링크: https://pichen.tistory.com/56 ) 글에서 ㄱ부터 ㅎ까지를 모두 직접 적어서 switch 돌리는 것에 계산식이 있지 않겠냐고 하는 반응을 보았습니다. [C#] 초성

pichen.tistory.com

 

 

반응형
반응형

파일을 읽는 방법은 여러가지가 있다. 필자가 최근에 가장 많이 사용하고 있는 방법은 커스터마이징 한 것으로 다른 글에서 잠깐 다뤄 볼 예정이고, 

 

본문에서는 .json 파일과 .ini 파일을 읽는 방법에 대해 알아보자.

 

 

 

먼저 [.ini 파일을 읽는 방법]

 

 

아래와 같은 Location.ini 파일을 바탕화면에 생성한다.

[Lo_1]
name=Jeju
stationID=A
PID=1234

[Lo_2]
name=Busan
stationID=2
PID=7654

 

 

ini 파일은 아주 구시대적일 수 있는 정보파일로 보통은 시작할때 읽어들이는 정보를 담은 파일을 뜻한다.

 

using System.Runtime.InteropServices; 라이브러리를 사용하여

kernel32.dll을 import하고, 그중에서도 GetPrivateProfileString()을 사용한다.

 

이를 사용하기 위한 세팅은 다음과 같다.

using System.Runtime.InteropServices;

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern uint GetPrivateProfileString(
        string lpAppName,
        string lpKeyName,
        string lpDefault,
        StringBuilder lpReturnedString,
        uint nSize,
        string lpFileName
);

 

 

가져올때에는 ini 파일내 변수들을 List나 Dictionary에 넣어두는 것이 루프문을 통해 읽어올 수 있어서 편하다.

 

List<string> iniKeys = new ()
{
	"name",  "stationID", "PID"
};

 

 

 

설정파일은 바탕화면에 있는 것으로 가정하였으므로 파일경로는 

Environment.GetFolderPath를 사용해도 좋다.

string iniPath = Path.Combine(
	Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
    "Location.ini"
);

Path.Combine()은 파일경로를 \가 없으면 붙이고 있으면 그대로 잇는 등

자동으로 파일경로를 결합시켜주는 역할을 한다.

 

Environment.GetFolderPath()는 운영체제 환경에서 특정 폴더경로를 가져오는 함수로

Environment.SpecialFolder 내에 바탕화면(Desktop), 즐겨찾기, appdata 등 여러 경로가 존재하고 저는 그 중에서 바탕화면을 썼던 것이다.

 

 

해당 ini파일이 존재하면 읽어서 출력해보자.

만일 없는 변수가 있으면 Warning으로 보이게 하면 구분이 쉽다.

 

아래와 같은 방법 말고도 특정 변수에 집어 넣어야만 확실한 사용이 될 것인데, for문의 i와 foreach의 key 값에 따라 변수에 집어 넣으면 된다.

Log.Information("iniPath:" + iniPath);

if (File.Exists(iniPath))
{
    for (int i = 1; i <= 2; i++)
    {
        StringBuilder sb = new StringBuilder(255);
        foreach (var key in iniKeys)
        {
            uint result = GetPrivateProfileString($"Lo_{i}", key, "", sb, (uint)sb.Capacity, iniPath);
            if (result > 0)
            {
                Log.Information("Value: " + sb.ToString());
            }
            else
            {
                Log.Warning("Value: " + sb.ToString());
            }
        }
    }
}

 

 

kernel32.dll의 GetPrivateProfileString() 함수는

1. ini 파일 내 [와 ] 사이값

2. ini 파일 내  [] 바로 아래 변수명 값

3. ini 파일 내  읽을 변수에 값이 없을때 대체 값

4. StringBuilder 변수값

5. 글자의 길이 제한

6. ini파일의 경로

 

로 구성되어있다.

 

 

 

(참고) 제 포스팅을 읽으실때 Log.Information()을 사용하지 못하시는분은 없겠죠?

만일 있다면 Serilog 포스팅도 읽어보기를 추천한다.

 

다만 부수적이라 생각하고 계속 진행할 독자라면 여기서 사용한 방법만 간단하게 설명할테니 따라하면 된다. 

nuget 패키지로 Serilog, Serilog.Sink.File을 설치한 후 

 

소스 최상단에 아래 내용을 추가하면된다. Serilog에 대해서는 자세한 설명은 하지 않겠다.

using Serilog;

string appName = "only_for_test";

Log.Logger = new LoggerConfiguration()
    // .WriteTo.Console()
    .WriteTo.File(
        $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}/{appName}_Logs/{appName}_log-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: null,
        fileSizeLimitBytes: 50 * 1024 * 1024,
        rollOnFileSizeLimit: true,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}")
    .MinimumLevel.Is(Serilog.Events.LogEventLevel.Information)
    .CreateLogger();

 

 

 

 

이번엔 [.json 파일을 읽는 방법]

 

json을 읽는 방법은 아주 간단하다.

우선 바탕화면에 Location.json 파일이 다음과 같다고 하자.

ini 파일과 준위를 맞추고자 아래 처럼 작성하였다.

{ 
	"Lo_1": {
		"name":"incheon", 
		"stationID": "A", 
		"PID": "1234"
	},
	"Lo_2": {
		"name":"busan", 
		"stationID": "1", 
		"PID": "1256"
	}
}

 

파일 경로는 다음과 같이 가져온다.

string jsonPath = Path.Combine(
	Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
    "Location.json"
);

 

 

File.ReadAllText(string filePath) 로 파일내의 모든 문자를 받아와

System.Text.Json 라이브러리에 속한 json을 클래스로 Deserialize 해준다.

 

Deseiralize를 제네릭으로 바로 인스턴스로 만들 수 있도록, .json 정보에 맞게 클래스를 선언해준다.(아래)

그러면 소스 내에서 바로 사용이 가능하다.

 

if (File.Exists(jsonPath))
{
    try
    {
        // 파일의 내용을 문자열로 읽어옴
        string jsonString = File.ReadAllText(jsonPath) ?? "";
        var Locations = JsonSerializer.Deserialize<Dictionary<string, Location>>(jsonString);
        if (Locations is not null)
        {
            foreach(var key in Locations.Keys)
            {
                Log.Information($"{key}= {Locations[key]}");
                if (Locations[key] is not null)
                {
                    Log.Information($"{key}=> name: {Locations[key].Name}");
                    Log.Information($"{key}=> stationID: {Locations[key].StationId}");
                    Log.Information($"{key}=> PID: {Locations[key].Pid}");
                }         
            }  
        }
    }
    catch (Exception e)
    {
        Log.Error($"{e}");
    }
}


class Location
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("stationID")]
    public string? StationId { get; set; }

    [JsonPropertyName("PID")]
    public string? Pid { get; set; }
}

 

 

설정파일을 읽는 방법들에 대해서 다뤄봤다. xml은 있는 것을 유지보수 할때 말고 직접 개발을 초기부터할때는 사용하지 않아서 다루지 않았다.

반응형

+ Recent posts