오래전 숫자가 포함된 문자 배열을 정렬 할때 1다음에 10이오는걸 보고 당황한 적이 있습니다.

 

 

Array.Sort를 했음에도 제대로 정렬되지 않는다

 

바로 위 그림처럼 말이죠.

Array.Sort메서드로 정렬을 했음에도 불구하고 1다음에 10이오고, 2다음에 20이 오는등 원하던대로 정렬 되지 않습니다.

 

 

이건 프로그램 입장에선 당연한 겁니다. 왜냐하면 '문자열'을 기준으로 비교하는 거니까요.

사람이야 저게 숫자니까 1다음에 2가 오는게 맞다고 생각하지만, 프로그램은 저걸 '문자'로 인식하고 정렬 합니다.

 

 

사실 기존 Array.Sort를 사용하면서 숫자 정렬 하는 방법이 있긴합니다.

그냥 숫자 앞에 0을 붙여줘서 01, 02 ··· 이런식으로 자리수를 맞춰주면 되죠.

하지만 이 방법은 파일이름을 일일이 바꿔줘야 하고

파일이 수백, 수천개가 되면 자리수를 맞추기위해 앞에 0을 계속 붙여줘야 하는 문제가 있습니다.

따라서 이 글에서는 다른 방식으로 해결해 보겠습니다.

 

 

이 문제를 해결하기 위해서는 ArraySort메서드를 좀더 살펴봐야 합니다.

Array.Sort 메서드는 IComparer 인터페이스를 구현한 클래스를 대상으로, IComparer의 구현대로 정렬을 시행합니다.

하지만 기존 String클래스의 IComparer로는 우리가 원하는대로 정렬할수 없습니다.

 

 

그렇다면 숫자를 고려하여 정렬 하기 위해서는 어떻게 해야 할까요?

사실 Array.Sort 메서드에는 직접 구현한 IComparer을 사용할수 있게 오버로드가 되어 있습니다.

그렇다면 숫자까지 고려한 IComparer을 직접 만들어서 정렬 하면 문제는 해결 됩니다.

 

 

다행히도 구글링을 통해 스택 오버플로에서 구현한 걸 찾았습니다. 스택 오버플로에 웬만한 건 다 있네요

해당 링크는 본문 하단에 첨부했으니 관심있으시면 방문해 보세요.

 

 

아래는 구글링한 소스를 바탕으로 구현한 StringAsNumericComparer 클래스입니다.

public class StringAsNumericComparer : IComparer<string>
{
    public StringAsNumericComparer()
    { }

    public int Compare(object x, object y)
    {
        if ((x is string) && (y is string))
        {
            return StringLogicalComparer.Compare((string)x, (string)y);
        }

        return -1;
    }

    public int Compare(string x, string y)
    {
        return StringLogicalComparer.Compare(x, y);
    }

    private class StringLogicalComparer
    {
        public static int Compare(string s1, string s2)
        {
            //get rid of special cases
            if ((s1 == null) && (s2 == null)) return 0;
            else if (s1 == null) return -1;
            else if (s2 == null) return 1;

            if ((s1.Equals(string.Empty) && (s2.Equals(string.Empty)))) return 0;
            else if (s1.Equals(string.Empty)) return -1;
            else if (s2.Equals(string.Empty)) return -1;

            //WE style, special case
            bool sp1 = Char.IsLetterOrDigit(s1, 0);
            bool sp2 = Char.IsLetterOrDigit(s2, 0);
            if (sp1 && !sp2) return 1;
            if (!sp1 && sp2) return -1;

            int i1 = 0, i2 = 0; //current index
            int r = 0; //temp result
            while (true)
            {
                bool c1 = Char.IsDigit(s1, i1);
                bool c2 = Char.IsDigit(s2, i2);
                if (!c1 && !c2)
                {
                    bool letter1 = Char.IsLetter(s1, i1);
                    bool letter2 = Char.IsLetter(s2, i2);
                    if ((letter1 && letter2) || (!letter1 && !letter2))
                    {
                        if (letter1 && letter2)
                        {
                            r = Char.ToLower(s1[i1]).CompareTo(Char.ToLower(s2[i2]));
                        }
                        else
                        {
                            r = s1[i1].CompareTo(s2[i2]);
                        }
                        if (r != 0) return r;
                    }
                    else if (!letter1 && letter2) return -1;
                    else if (letter1 && !letter2) return 1;
                }
                else if (c1 && c2)
                {
                    r = CompareNum(s1, ref i1, s2, ref i2);
                    if (r != 0) return r;
                }
                else if (c1)
                {
                    return -1;
                }
                else if (c2)
                {
                    return 1;
                }
                i1++;
                i2++;
                if ((i1 >= s1.Length) && (i2 >= s2.Length))
                {
                    return 0;
                }
                else if (i1 >= s1.Length)
                {
                    return -1;
                }
                else if (i2 >= s2.Length)
                {
                    return -1;
                }
            }
        }

        private static int CompareNum(string s1, ref int i1, string s2, ref int i2)
        {
            int nzStart1 = i1, nzStart2 = i2; // nz = non zero
            int end1 = i1, end2 = i2;

            ScanNumEnd(s1, i1, ref end1, ref nzStart1);
            ScanNumEnd(s2, i2, ref end2, ref nzStart2);
            int start1 = i1; i1 = end1 - 1;
            int start2 = i2; i2 = end2 - 1;

            int nzLength1 = end1 - nzStart1;
            int nzLength2 = end2 - nzStart2;

            if (nzLength1 < nzLength2) return -1;
            else if (nzLength1 > nzLength2) return 1;

            for (int j1 = nzStart1, j2 = nzStart2; j1 <= i1; j1++, j2++)
            {
                int r = s1[j1].CompareTo(s2[j2]);
                if (r != 0) return r;
            }
            // the nz parts are equal
            int length1 = end1 - start1;
            int length2 = end2 - start2;
            if (length1 == length2) return 0;
            if (length1 > length2) return -1;
            return 1;
        }

        private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart)
        {
            nzStart = start;
            end = start;
            bool countZeros = true;
            while (Char.IsDigit(s, end))
            {
                if (countZeros && s[end].Equals('0'))
                {
                    nzStart++;
                }
                else countZeros = false;
                end++;
                if (end >= s.Length) break;
            }
        }
    }
}

 

이제 힘들게 구현했으니 사용해봐야겠죠?

사용할때는 Array.Sort(strArray new StringAsNumericComparer()); <- 이런식으로 사용하면 됩니다.

 

 

아래 코드는 실제 사용예 입니다.

static void Main(string[] args)
{
    string _dirPath = @"D:\Test";

    var dirInfo = new DirectoryInfo(_dirPath);
    var dirName = dirInfo.FullName;

    // 파일명 배열
    string[] fileArr = dirInfo.GetFiles().Select(o => o.Name).ToArray();

    Array.Sort(fileArr, new StringAsNumericComparer());
}

 

 

이제 결과를 살펴봅시다.

정렬 결과 확인

 

 

이제 우리가 원하던 대로 정렬 됐습니다

저같은 경우는 꽤나 쓸일이 많은지라, 따로 보관해 필요할때마다 쓰고있습니다.

이글이 도움이 됐으면 좋겠네요

 

 

 

 

 

https://stackoverflow.com/questions/23114201/sorting-issue-in-net

 

Sorting issue in .NET

I am using this linq query to sort a string column but the results I am getting does not seems to be in right order? Query: userList = users.OrderBy(u => u.FirstName) .Skip(off...

stackoverflow.com

 

+ Recent posts