. 불필요한 공백 정리

문서를 작성하다 보면 줄 끝에 불필요한 공백이 생길 수 있다. 공백으로만 채워진 줄이라든가 마지막 문자 이후에 공백만 이어지다가 개행되는 경우는 분명히 필요치 않은 공백이 들어가 파일 용량만 차지하게 된다. 물론 이 공백들이 정말로 불필요한 것인지 아니면 당장은 안보이지만 의미있는 공백인지는 문서를 작성하는 사람만 알 수 있다. 다음 문서를 보자. 이 문서에서 -는 공백이다.

 

#include-<losedows.h>

\t\t

if-(Bill==Babo)-{---

\t

}

 

이 문서에서 두 번째 줄의 탭과 세 번째 줄 끝에 공백은 보이지 않는 불필요한 문자이다. 네 번째 줄의 탭도 당장은 불필요 하지만 다음에 이 블록에 어떤 코드를 작성한다면 필요해지는 탭이다. 공백 정리 기능은 이런 식으로 당장 보이지 않는 공백을 제거하여 문서를 날씬하게 만드는 기능이다. 특히 통신망에서 다운받은 문서들은 빈 줄이 삽입되어 있고 빈 줄이 공백으로 가득차 있어 문서 용량이 실제 용량보다 두 배나 되는 경우도 있는데 이럴 때 공백을 정리하면 용량상의 많은 이득을 볼 수 있다.

불필요한 공백을 정리하는 멤버함수를 다음과 같이 작성하도록 하자. 이 기능은 일단은 선택영역에 대해 공백을 정리하되 선택영역이 없다면 문서 전체를 대상으로 한다.

 

void CApiEdit::RemoveExtraSpace()

{

     int SelFirst, SelSecond;

     TCHAR *src,*dest;

     TCHAR *s,*d;

     int len;

     int nDiff;

 

     if (SelStart==SelEnd) {

          SelFirst=0;

          SelSecond=doclen;

     } else {

          SelFirst=min(SelStart,SelEnd);

          SelSecond=max(SelStart,SelEnd);

     }

     len=SelSecond-SelFirst;

 

     src=(TCHAR *)malloc(len+1);

     dest=(TCHAR *)malloc(len+1);

     lstrcpyn(src,buf+SelFirst,len+1);

 

     s=src+len;

     d=dest+len;

     nDiff=0;

     for (;;) {

          *d=*s;

          if (s==src)

              break;

          d--;

          s--;

 

          if (*(s+1)==‘\r’) {

              while (AeIsWhiteSpace(*s) && s!=src) {

                   s--;

                   nDiff--;

              }

          }

     }

 

     if (nDiff) {

          StartUndoGroup();

          Delete(SelFirst,len);

          Insert(SelFirst,d);

          EndUndoGroup();

 

          SelSecond += nDiff;

 

          if (SelStart==SelEnd) {

              off=SelStart=SelEnd=0;

          } else if (SelStart < SelEnd) {

              off=SelEnd=SelSecond;

          } else {

              SelStart=SelSecond;

          }

 

          Invalidate(SelFirst);

          SetCaret();

     }

 

     free(src);

     free(dest);

}

 

변환 후에 문서의 길이가 달라질 수 있으므로 버퍼는 src, dest 두 개가 필요하다. 하지만 삭제를 위한 동작을 하므로 원본보다 더 큰 결과 문자열이 만들어질 수는 없으며 버퍼는 둘 다 똑같은 크기로 할당하면 된다. dest의 정확한 필요량은 src의 길이-지워질 공백수로 계산할 수 있겠지만 남는 것은 문제가 되지 않으므로 이 계산을 굳이 할 필요가 없다. src에 선택영역의 문자열을 복사해놓고 이 문자열에서 불필요한 공백을 찾아 dest로 전송하면 된다.

이 함수가 앞에서 작성한 두 함수와 다른 점이 있다면 원본 문자열의 끝에서부터 앞쪽으로 검색을 해 나간다는 점이다. 불필요한 공백은 개행코드 이전에 있는 공백으로 정의되는데 공백을 먼저 찾고 개행코드가 뒤에 있는지 보는 것보다는 개행코드를 먼저 찾고 이어지는 공백을 찾는 것이 훨씬 더 쉽기 때문이다. 다음 그림을 보자.

순방향으로 검색을 하다가 공백을 만난 경우 이 공백이 불필요한 공백인지 아닌지 판단하려면 공백 이후에 비공백 문자가 있는지 다음 문자열을 더 읽어 봐야 한다. 만약 공백 이후 개행코드 이전에 다른 문자가 있다면 이 공백은 불필요한 공백이 아니다. 나는 다음에 있는 공백의 뒤쪽으로 자가 있기 때문에 이 공백은 의미가 있다. 문자열 다음의 공백 셋은 분명히 불필요한 공백인데 이 공백들 뒤로 공백과 개행코드만 있기 때문이다. 순방향 검색은 공백을 만났을 때 뒤쪽을 더 읽어 봐야 이 공백의 포함여부를 결정할 수 있고 공백을 만날 때마다 이 판단을 반복해야 한다.

역방향 검색은 문자를 읽는 족족 불필요한지 아닌지 바로 알 수 있다. 개행코드 \r을 만난 이후부터 나타나는 공백은 무조건 불필요하므로 앞뒤의 다른 문자를 볼 필요도 없다. src의 끝에서부터 한문자씩 dest로 복사를 해오다가 개행코드를 만나면, 이후의 공백만 건너뛰면 아주 간단하게 불필요한 공백을 걷어낼 수 있다. 검색의 방향에 따라 알고리즘의 복잡도에 엄청난 차이가 발생함을 알 수 있는데 만약 순방향으로 검색하도록 설계를 했다면 코드는 두 배 이상 복잡해질 것이고 속도도 당연히 느려진다.

불필요한 공백을 정리한 후 src를 삭제하고 dest를 삽입하는 것은 앞의 두 함수에서와 동일하다. , 이 경우 nDiff 0이면 즉, 어떠한 문자도 삭제되지 않았다면 아무 것도 할 필요가 없다. 이 경우 src dest는 동일한 내용을 가지며 대체를 해도 별 상관은 없지만 괜히 불필요하게 취소 레코드를 두 개나 더 쓴다는 점이 별로 마음에 들지 않는다. 변환 결과 선택영역을 조정하는 방법은 ConvertSpaceTab 함수와 동일하다.

이 함수는 비교적 완벽하지만 사실 한 가지 논리적인 문제점을 가지고 있는데 선택영역의 끝이 공백인 경우, 이 공백의 유효성을 알 수 없다는 점이다. 왜냐하면 선택된 문자열인 src에서는 이 공백 다음에 다른 문자가 있는지를 판단할 수 없기 때문이다. 버퍼의 끝에서 개행문자를 만나지 못했으므로 이 공백은 실제로 불필요하더라도 제거되지 않는다. 이 문제는 문단 관리 함수가 작성되면 해결할 수 있다.

이상으로 세 개의 문서 변환 함수들을 작성했는데 호스트의 OnCommand에서 편집 메뉴의 항목이 선택될 때 탭 변환 함수와 공백 정리 함수를 호출한다. 함수만 호출하면 모든 편집 동작은 ApiEdit가 알아서 할 것이다.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     case IDM_EDIT_TOTAB:

          pSi->Ae.ConvertSpaceTab(FALSE);

          break;

     case IDM_EDIT_TOSPACE:

          pSi->Ae.ConvertSpaceTab(TRUE);

          break;

     case IDM_EDIT_REMOVESPACE:

          pSi->Ae.RemoveExtraSpace();

          break;

 

지금까지 작성한 편집 명령 중 일부는 선택영역이 있어야만 사용할 수 있는데 이런 명령들은 OnInitMenu에서 메뉴항목들을 관리해야 한다.

 

void OnInitMenu(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

          ....

          pSi->Ae.GetSelect(s,e);

          if (s == e) {

              EnableMenuItem(hMenu, IDM_EDIT_CUT, MF_BYCOMMAND | MF_GRAYED);

              EnableMenuItem(hMenu, IDM_EDIT_COPY, MF_BYCOMMAND | MF_GRAYED);

           EnableMenuItem(hMenu, IDM_EDIT_UPPERSENT, MF_BYCOMMAND | MF_GRAYED);

           EnableMenuItem(hMenu, IDM_EDIT_TOTAB, MF_BYCOMMAND | MF_GRAYED);

           EnableMenuItem(hMenu, IDM_EDIT_TOSPACE, MF_BYCOMMAND | MF_GRAYED);

          } else {

              EnableMenuItem(hMenu, IDM_EDIT_CUT, MF_BYCOMMAND | MF_ENABLED);

              EnableMenuItem(hMenu, IDM_EDIT_COPY, MF_BYCOMMAND | MF_ENABLED);

           EnableMenuItem(hMenu, IDM_EDIT_UPPERSENT, MF_BYCOMMAND | MF_ENABLED);

           EnableMenuItem(hMenu, IDM_EDIT_TOTAB, MF_BYCOMMAND | MF_ENABLED);

           EnableMenuItem(hMenu, IDM_EDIT_TOSPACE, MF_BYCOMMAND | MF_ENABLED);

          }

 

, 공백간의 변환 명령과 문장 처음만 대문자 변환 명령은 선택영역이 있을 때만 사용할 수 있다. 이 명령의 상태는 복사, 잘라내기와 동일하게 관리되므로 같은 블록에 코드를 작성하였다.