C #의 자연 정렬 순서


129

누구나 좋은 리소스를 가지고 있거나 FileInfo배열에 대해 C #에서 자연 순서 정렬 샘플을 제공 합니까? IComparer내 인터페이스를 구현하고 있습니다.

답변:


148

가장 쉬운 방법은 Windows에서 내장 함수를 P / Invoke하고 다음에서 비교 함수로 사용하는 것입니다 IComparer.

[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern int StrCmpLogicalW(string psz1, string psz2);

Michael Kaplan 은이 기능이 어떻게 작동하는지에 대한 예 와 Vista가보다 직관적으로 작동하도록 변경되었습니다. 이 기능의 장점은 실행되는 Windows 버전과 동일한 동작을 수행한다는 것입니다. 그러나 이는 Windows 버전마다 다르므로 이것이 문제인지 여부를 고려해야합니다.

따라서 완전한 구현은 다음과 같습니다.

[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
    public static extern int StrCmpLogicalW(string psz1, string psz2);
}

public sealed class NaturalStringComparer : IComparer<string>
{
    public int Compare(string a, string b)
    {
        return SafeNativeMethods.StrCmpLogicalW(a, b);
    }
}

public sealed class NaturalFileInfoNameComparer : IComparer<FileInfo>
{
    public int Compare(FileInfo a, FileInfo b)
    {
        return SafeNativeMethods.StrCmpLogicalW(a.Name, b.Name);
    }
}

8
좋은 대답입니다. 주의 사항 : 운영 체제에서 여전히 작업을 수행하는 소수의 사람들에게는 Win2000에서 작동하지 않습니다. 반면, Kaplan의 블로그와 MSDN 설명서 사이에 유사한 기능을 만들기에 충분한 힌트가 있습니다.
Chris Charabaruk

9
이 단지는 Win32에서 작동, 휴대용 아니라, 리눅스 / 맥 OS / 실버 / 윈도우 폰 / 지하철에서 작동하지 않습니다
linquize

20
@linquize-그는 .NET이 Mono가 아니라고 말했기 때문에 Linux / OSX는 실제로 걱정하지 않습니다. 이 답변이 게시 된 2008 년에는 Windows Phone / Metro가 존재하지 않았습니다. 그리고 Silverlight에서 파일 작업을 얼마나 자주 수행합니까? 따라서 OP, 아마도 대부분의 다른 사람들에게 적합한 대답이었습니다. 어쨌든 더 나은 답변을 자유롭게 제공 할 수 있습니다. 이것이이 사이트의 작동 방식입니다.
Greg Beech

6
이것은 원래의 대답이 잘못되었다는 것을 의미하지는 않습니다. 난 그냥 최신 정보와 추가 정보를 추가
linquize

2
참고 Comparer<T>로 구현 대신 상속 IComparer<T>하면 IComparer일반 메소드를 호출 하는 (일반적이지 않은) 인터페이스 의 내장 구현 을 얻습니다 . 대신 대신 API를 사용합니다. 기본적으로 무료입니다 : "I"를 삭제하고로 변경 public int Compare(...)하십시오 public override int Compare(...). 동일에 IEqualityComparer<T>EqualityComparer<T>.
Joe Amenta 2016 년

75

방금 추가 할 것이라고 생각했습니다 (내가 찾을 수있는 가장 간결한 솔루션으로).

public static IOrderedEnumerable<T> OrderByAlphaNumeric<T>(this IEnumerable<T> source, Func<T, string> selector)
{
    int max = source
        .SelectMany(i => Regex.Matches(selector(i), @"\d+").Cast<Match>().Select(m => (int?)m.Value.Length))
        .Max() ?? 0;

    return source.OrderBy(i => Regex.Replace(selector(i), @"\d+", m => m.Value.PadLeft(max, '0')));
}

위의 내용은 문자열의 모든 숫자를 모든 문자열의 모든 숫자의 최대 길이까지 채우고 결과 문자열을 사용하여 정렬합니다.

캐스트 ( int?)는 숫자가없는 문자열 모음을 허용하는 것입니다 ( .Max()빈 열거 형 던지기에서 InvalidOperationException).


1
+1 가장 간결 할뿐만 아니라 내가 본 것 중 가장 빠릅니다. 수락 된 답변을 제외하고는 기계 종속성으로 인해 사용할 수 없습니다. 약 35 초 동안 4 백만 개가 넘는 값을 정렬했습니다.
유전자 S

4
이것은 아름답고 읽을 수 없습니다. Linq의 이점은 (최소한) 최고의 평균 성능과 최상의 성능을 의미한다고 가정합니다. 명확성의 부족에도 불구하고. 대단히 감사합니다 @Matthew Horsley
Ian Grainger

1
이것은 매우 좋지만 특정 10 진수에 대한 버그가 있습니다. 제 예제는 k8.11 대 k8.2의 정렬이었습니다. 이 문제를 해결하기 위해 다음 정규식을 구현했습니다. \ d + ([\.,] \ d)?
devzero

2
이 코드를 입력 할 때 두 번째 그룹 (소수점 + 소수)의 길이도 고려해야합니다. m.Value.PadLeft (max, '0')
devzero

3
.DefaultIfEmpty().Max()에 캐스팅 하는 대신 사용할 수 있다고 생각합니다 int?. 또한 source.ToList()열거 형을 다시 열거하지 않도록 하는 것이 좋습니다.
Teejay

30

기존 구현 중 어느 것도 멋지게 보이지 않았으므로 직접 작성했습니다. 결과는 최신 버전의 Windows 탐색기 (Windows 7/8)에서 사용하는 정렬과 거의 동일합니다. 내가 본 유일한 차이점은 1) Windows가 (예를 들어 XP) 모든 길이의 숫자를 처리하는 데 사용되었지만 이제는 19 자리로 제한되어 있습니다-광산은 무제한입니다. 괜찮음 (대리 쌍의 숫자를 숫자로 비교하지는 않지만 Windows도 마찬가지 임), 3) 다른 섹션에서 발생하는 기본이 아닌 정렬 가중치의 다른 유형을 구별 할 수 없음 (예 : "e-1é"대 " é1e- "-숫자 앞뒤에 분음 부호와 구두점 무게 차이가 있습니다).

public static int CompareNatural(string strA, string strB) {
    return CompareNatural(strA, strB, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase);
}

public static int CompareNatural(string strA, string strB, CultureInfo culture, CompareOptions options) {
    CompareInfo cmp = culture.CompareInfo;
    int iA = 0;
    int iB = 0;
    int softResult = 0;
    int softResultWeight = 0;
    while (iA < strA.Length && iB < strB.Length) {
        bool isDigitA = Char.IsDigit(strA[iA]);
        bool isDigitB = Char.IsDigit(strB[iB]);
        if (isDigitA != isDigitB) {
            return cmp.Compare(strA, iA, strB, iB, options);
        }
        else if (!isDigitA && !isDigitB) {
            int jA = iA + 1;
            int jB = iB + 1;
            while (jA < strA.Length && !Char.IsDigit(strA[jA])) jA++;
            while (jB < strB.Length && !Char.IsDigit(strB[jB])) jB++;
            int cmpResult = cmp.Compare(strA, iA, jA - iA, strB, iB, jB - iB, options);
            if (cmpResult != 0) {
                // Certain strings may be considered different due to "soft" differences that are
                // ignored if more significant differences follow, e.g. a hyphen only affects the
                // comparison if no other differences follow
                string sectionA = strA.Substring(iA, jA - iA);
                string sectionB = strB.Substring(iB, jB - iB);
                if (cmp.Compare(sectionA + "1", sectionB + "2", options) ==
                    cmp.Compare(sectionA + "2", sectionB + "1", options))
                {
                    return cmp.Compare(strA, iA, strB, iB, options);
                }
                else if (softResultWeight < 1) {
                    softResult = cmpResult;
                    softResultWeight = 1;
                }
            }
            iA = jA;
            iB = jB;
        }
        else {
            char zeroA = (char)(strA[iA] - (int)Char.GetNumericValue(strA[iA]));
            char zeroB = (char)(strB[iB] - (int)Char.GetNumericValue(strB[iB]));
            int jA = iA;
            int jB = iB;
            while (jA < strA.Length && strA[jA] == zeroA) jA++;
            while (jB < strB.Length && strB[jB] == zeroB) jB++;
            int resultIfSameLength = 0;
            do {
                isDigitA = jA < strA.Length && Char.IsDigit(strA[jA]);
                isDigitB = jB < strB.Length && Char.IsDigit(strB[jB]);
                int numA = isDigitA ? (int)Char.GetNumericValue(strA[jA]) : 0;
                int numB = isDigitB ? (int)Char.GetNumericValue(strB[jB]) : 0;
                if (isDigitA && (char)(strA[jA] - numA) != zeroA) isDigitA = false;
                if (isDigitB && (char)(strB[jB] - numB) != zeroB) isDigitB = false;
                if (isDigitA && isDigitB) {
                    if (numA != numB && resultIfSameLength == 0) {
                        resultIfSameLength = numA < numB ? -1 : 1;
                    }
                    jA++;
                    jB++;
                }
            }
            while (isDigitA && isDigitB);
            if (isDigitA != isDigitB) {
                // One number has more digits than the other (ignoring leading zeros) - the longer
                // number must be larger
                return isDigitA ? 1 : -1;
            }
            else if (resultIfSameLength != 0) {
                // Both numbers are the same length (ignoring leading zeros) and at least one of
                // the digits differed - the first difference determines the result
                return resultIfSameLength;
            }
            int lA = jA - iA;
            int lB = jB - iB;
            if (lA != lB) {
                // Both numbers are equivalent but one has more leading zeros
                return lA > lB ? -1 : 1;
            }
            else if (zeroA != zeroB && softResultWeight < 2) {
                softResult = cmp.Compare(strA, iA, 1, strB, iB, 1, options);
                softResultWeight = 2;
            }
            iA = jA;
            iB = jB;
        }
    }
    if (iA < strA.Length || iB < strB.Length) {
        return iA < strA.Length ? 1 : -1;
    }
    else if (softResult != 0) {
        return softResult;
    }
    return 0;
}

서명은 Comparison<string>대리인 과 일치합니다 .

string[] files = Directory.GetFiles(@"C:\");
Array.Sort(files, CompareNatural);

다음과 같이 사용할 래퍼 클래스는 다음과 같습니다 IComparer<string>.

public class CustomComparer<T> : IComparer<T> {
    private Comparison<T> _comparison;

    public CustomComparer(Comparison<T> comparison) {
        _comparison = comparison;
    }

    public int Compare(T x, T y) {
        return _comparison(x, y);
    }
}

예:

string[] files = Directory.EnumerateFiles(@"C:\")
    .OrderBy(f => f, new CustomComparer<string>(CompareNatural))
    .ToArray();

테스트에 사용하는 좋은 파일 이름 세트는 다음과 같습니다.

Func<string, string> expand = (s) => { int o; while ((o = s.IndexOf('\\')) != -1) { int p = o + 1;
    int z = 1; while (s[p] == '0') { z++; p++; } int c = Int32.Parse(s.Substring(p, z));
    s = s.Substring(0, o) + new string(s[o - 1], c) + s.Substring(p + z); } return s; };
string encodedFileNames =
    "KDEqLW4xMiotbjEzKjAwMDFcMDY2KjAwMlwwMTcqMDA5XDAxNyowMlwwMTcqMDlcMDE3KjEhKjEtISox" +
    "LWEqMS4yNT8xLjI1KjEuNT8xLjUqMSoxXDAxNyoxXDAxOCoxXDAxOSoxXDA2NioxXDA2NyoxYSoyXDAx" +
    "NyoyXDAxOCo5XDAxNyo5XDAxOCo5XDA2Nio9MSphMDAxdGVzdDAxKmEwMDF0ZXN0aW5nYTBcMzEqYTAw" +
    "Mj9hMDAyIGE/YTAwMiBhKmEwMDIqYTAwMmE/YTAwMmEqYTAxdGVzdGluZ2EwMDEqYTAxdnNmcyphMSph" +
    "MWEqYTF6KmEyKmIwMDAzcTYqYjAwM3E0KmIwM3E1KmMtZSpjZCpjZipmIDEqZipnP2cgMT9oLW4qaG8t" +
    "bipJKmljZS1jcmVhbT9pY2VjcmVhbT9pY2VjcmVhbS0/ajBcNDE/ajAwMWE/ajAxP2shKmsnKmstKmsx" +
    "KmthKmxpc3QqbTAwMDNhMDA1YSptMDAzYTAwMDVhKm0wMDNhMDA1Km0wMDNhMDA1YSpuMTIqbjEzKm8t" +
    "bjAxMypvLW4xMipvLW40P28tbjQhP28tbjR6P28tbjlhLWI1Km8tbjlhYjUqb24wMTMqb24xMipvbjQ/" +
    "b240IT9vbjR6P29uOWEtYjUqb245YWI1Km/CrW4wMTMqb8KtbjEyKnAwMCpwMDEqcDAxwr0hKnAwMcK9" +
    "KnAwMcK9YSpwMDHCvcK+KnAwMipwMMK9KnEtbjAxMypxLW4xMipxbjAxMypxbjEyKnItMDAhKnItMDAh" +
    "NSpyLTAwIe+8lSpyLTAwYSpyLe+8kFwxIS01KnIt77yQXDEhLe+8lSpyLe+8kFwxISpyLe+8kFwxITUq" +
    "ci3vvJBcMSHvvJUqci3vvJBcMWEqci3vvJBcMyE1KnIwMCEqcjAwLTUqcjAwLjUqcjAwNSpyMDBhKnIw" +
    "NSpyMDYqcjQqcjUqctmg2aYqctmkKnLZpSpy27Dbtipy27Qqctu1KnLfgN+GKnLfhCpy34UqcuClpuCl" +
    "rCpy4KWqKnLgpasqcuCnpuCnrCpy4KeqKnLgp6sqcuCppuCprCpy4KmqKnLgqasqcuCrpuCrrCpy4Kuq" +
    "KnLgq6sqcuCtpuCtrCpy4K2qKnLgrasqcuCvpuCvrCpy4K+qKnLgr6sqcuCxpuCxrCpy4LGqKnLgsasq" +
    "cuCzpuCzrCpy4LOqKnLgs6sqcuC1puC1rCpy4LWqKnLgtasqcuC5kOC5lipy4LmUKnLguZUqcuC7kOC7" +
    "lipy4LuUKnLgu5UqcuC8oOC8pipy4LykKnLgvKUqcuGBgOGBhipy4YGEKnLhgYUqcuGCkOGClipy4YKU" +
    "KnLhgpUqcuGfoOGfpipy4Z+kKnLhn6UqcuGgkOGglipy4aCUKnLhoJUqcuGlhuGljCpy4aWKKnLhpYsq" +
    "cuGnkOGnlipy4aeUKnLhp5UqcuGtkOGtlipy4a2UKnLhrZUqcuGusOGutipy4a60KnLhrrUqcuGxgOGx" +
    "hipy4bGEKnLhsYUqcuGxkOGxlipy4bGUKnLhsZUqcuqYoFwx6pilKnLqmKDqmKUqcuqYoOqYpipy6pik" +
    "KnLqmKUqcuqjkOqjlipy6qOUKnLqo5UqcuqkgOqkhipy6qSEKnLqpIUqcuqpkOqplipy6qmUKnLqqZUq" +
    "cvCQkqAqcvCQkqUqcvCdn5gqcvCdn50qcu+8kFwxISpy77yQXDEt77yVKnLvvJBcMS7vvJUqcu+8kFwx" +
    "YSpy77yQXDHqmKUqcu+8kFwx77yO77yVKnLvvJBcMe+8lSpy77yQ77yVKnLvvJDvvJYqcu+8lCpy77yV" +
    "KnNpKnPEsSp0ZXN02aIqdGVzdNmi2aAqdGVzdNmjKnVBZS0qdWFlKnViZS0qdUJlKnVjZS0xw6kqdWNl" +
    "McOpLSp1Y2Uxw6kqdWPDqS0xZSp1Y8OpMWUtKnVjw6kxZSp3ZWlhMSp3ZWlhMip3ZWlzczEqd2Vpc3My" +
    "KndlaXoxKndlaXoyKndlacOfMSp3ZWnDnzIqeSBhMyp5IGE0KnknYTMqeSdhNCp5K2EzKnkrYTQqeS1h" +
    "Myp5LWE0KnlhMyp5YTQqej96IDA1MD96IDIxP3ohMjE/ejIwP3oyMj96YTIxP3rCqTIxP1sxKl8xKsKt" +
    "bjEyKsKtbjEzKsSwKg==";
string[] fileNames = Encoding.UTF8.GetString(Convert.FromBase64String(encodedFileNames))
    .Replace("*", ".txt?").Split(new[] { "?" }, StringSplitOptions.RemoveEmptyEntries)
    .Select(n => expand(n)).ToArray();

숫자 섹션은 섹션 단위로 비교해야합니다. 즉, 'abc12b'는 'abc123'보다 작아야합니다.
SOUser

다음 데이터를 시도 할 수 있습니다 : public string [] filenames = { "-abc12.txt", " abc12.txt", "1abc_2.txt", "a0000012.txt", "a0000012c.txt", "a000012.txt" , "a000012b.txt", "a012.txt", "a0000102.txt", "abc1_2.txt", "abc12 .txt", "abc12b.txt", "abc123.txt", "abccde.txt", " b0000.txt ","b00001.txt ","b0001.txt ","b001.txt ","c0000.txt ","c0000c.txt ","c00001.txt ","c000b.txt ","d0. 20.2b.txt ","d0.1000c.txt ","d0.2000y.txt ","d0.20000.2b.txt ","
SOUser

@XichenLi 좋은 테스트 사례에 감사드립니다. Windows 탐색기에서 해당 파일을 정렬하게하면 사용중인 Windows 버전에 따라 다른 결과가 나타납니다. 내 코드는 이러한 이름을 Server 2003 (및 아마도 XP)과 동일하게 정렬하지만 Windows 8과는 다릅니다.
JD

3
버그가 있습니다. 범위를 벗어난 색인
linquize

3
훌륭한 솔루션! 약 10,000 개의 파일이있는 일반적인 시나리오에서 벤치 마크했을 때 Matthew의 정규식 예제보다 빠르며 StrCmpLogicalW ()와 동일한 성능이었습니다. 위 코드에는 사소한 버그가 있습니다 : "while (strA [jA] == zeroA) jA ++;" 그리고 "(strB [jB] == zeroB) jB ++;" "(wA <strA.Length && strA [jA] == zeroA) jA ++이어야합니다." 및 "(jB <strB.Length && strB [jB] == zeroB) jB ++;". 그렇지 않으면 0 만 포함 된 문자열에서 예외가 발생합니다.
kuroki

22

linq 주문을위한 순수한 C # 솔루션 :

http://zootfroot.blogspot.com/2009/09/natural-sort-compare-with-linq-orderby.html

public class NaturalSortComparer<T> : IComparer<string>, IDisposable
{
    private bool isAscending;

    public NaturalSortComparer(bool inAscendingOrder = true)
    {
        this.isAscending = inAscendingOrder;
    }

    #region IComparer<string> Members

    public int Compare(string x, string y)
    {
        throw new NotImplementedException();
    }

    #endregion

    #region IComparer<string> Members

    int IComparer<string>.Compare(string x, string y)
    {
        if (x == y)
            return 0;

        string[] x1, y1;

        if (!table.TryGetValue(x, out x1))
        {
            x1 = Regex.Split(x.Replace(" ", ""), "([0-9]+)");
            table.Add(x, x1);
        }

        if (!table.TryGetValue(y, out y1))
        {
            y1 = Regex.Split(y.Replace(" ", ""), "([0-9]+)");
            table.Add(y, y1);
        }

        int returnVal;

        for (int i = 0; i < x1.Length && i < y1.Length; i++)
        {
            if (x1[i] != y1[i])
            {
                returnVal = PartCompare(x1[i], y1[i]);
                return isAscending ? returnVal : -returnVal;
            }
        }

        if (y1.Length > x1.Length)
        {
            returnVal = 1;
        }
        else if (x1.Length > y1.Length)
        { 
            returnVal = -1; 
        }
        else
        {
            returnVal = 0;
        }

        return isAscending ? returnVal : -returnVal;
    }

    private static int PartCompare(string left, string right)
    {
        int x, y;
        if (!int.TryParse(left, out x))
            return left.CompareTo(right);

        if (!int.TryParse(right, out y))
            return left.CompareTo(right);

        return x.CompareTo(y);
    }

    #endregion

    private Dictionary<string, string[]> table = new Dictionary<string, string[]>();

    public void Dispose()
    {
        table.Clear();
        table = null;
    }
}

2
이 코드는 궁극적으로 codeproject.com/KB/recipes/NaturalComparer.aspx(LINQ 지향이 아님) 에서 가져온 것 입니다.
mhenry1384

2
블로그 포스트는 Pascal Ganaye가 아니라 IComparer에 대해 Justin Jones ( codeproject.com/KB/string/NaturalSortComparer.aspx )를 인정합니다.
James McCormack

1
사소한 점은이 솔루션은 창과 동일하지 않으며 아래의 Matthew Horsley의 코드만큼 좋지 않은 공백을 무시합니다. 따라서 'string01' 'string 01' 'string 02' 'string02'를 얻을 수 있습니다 (예를 들어보기 흉한 모양). 공백 제거를 제거하면 문자열이 거꾸로 정렬됩니다. 즉 'string01'은 'string 01'앞에옵니다. 이는 허용되거나 허용되지 않을 수 있습니다.
마이클 파커

이것은 주소, 즉 "1 Smith Rd", "10 Smith Rd", "2 Smith Rd"등-자연적으로 정렬되었습니다. 예! 좋은 것!
Piotr Kula

그건 그렇고, Type 인수 <T>가 완전히 필요하지 않다는 것을 알았습니다 (그리고 링크 된 페이지의 주석도 나타납니다).
jv-dev 2016

18

Matthews Horsleys 답변은 프로그램이 실행중인 Windows 버전에 따라 동작을 변경하지 않는 가장 빠른 방법입니다. 그러나 정규 표현식을 한 번 작성하고 RegexOptions.Compiled를 사용하면 더 빠를 수 있습니다. 또한 문자열 비교자를 삽입하는 옵션을 추가하여 필요한 경우 대소 문자를 무시하고 가독성을 조금 향상 시켰습니다.

    public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
    {
        var regex = new Regex(@"\d+", RegexOptions.Compiled);

        int maxDigits = items
                      .SelectMany(i => regex.Matches(selector(i)).Cast<Match>().Select(digitChunk => (int?)digitChunk.Value.Length))
                      .Max() ?? 0;

        return items.OrderBy(i => regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
    }

사용

var sortedEmployees = employees.OrderByNatural(emp => emp.Name);

기본 .net 문자열 비교의 경우 300ms와 비교하여 100,000 개의 문자열을 정렬하는 데 450ms가 걸립니다.


2
- 이것은 위의 WRT 가치가 읽고 정규 표현식에서의 컴파일 및 재사용
mungflesh

16

내 해결책 :

void Main()
{
    new[] {"a4","a3","a2","a10","b5","b4","b400","1","C1d","c1d2"}.OrderBy(x => x, new NaturalStringComparer()).Dump();
}

public class NaturalStringComparer : IComparer<string>
{
    private static readonly Regex _re = new Regex(@"(?<=\D)(?=\d)|(?<=\d)(?=\D)", RegexOptions.Compiled);

    public int Compare(string x, string y)
    {
        x = x.ToLower();
        y = y.ToLower();
        if(string.Compare(x, 0, y, 0, Math.Min(x.Length, y.Length)) == 0)
        {
            if(x.Length == y.Length) return 0;
            return x.Length < y.Length ? -1 : 1;
        }
        var a = _re.Split(x);
        var b = _re.Split(y);
        int i = 0;
        while(true)
        {
            int r = PartCompare(a[i], b[i]);
            if(r != 0) return r;
            ++i;
        }
    }

    private static int PartCompare(string x, string y)
    {
        int a, b;
        if(int.TryParse(x, out a) && int.TryParse(y, out b))
            return a.CompareTo(b);
        return x.CompareTo(y);
    }
}

결과 :

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2

나는 그것을 좋아한다. Linq는 이해하기 쉽고 필요하지 않습니다.

11

조심해야합니다-StrCmpLogicalW 또는 이와 유사한 것이 엄격하게 전 이적이지 않다는 것을 모호하게 기억합니다. 비교 함수가 규칙을 위반하면 .NET의 정렬 메소드가 때때로 무한 루프에 빠지는 것을 관찰했습니다.

전이 비교는 a <b 및 b <c 인 경우 항상 a <c임을보고합니다. 항상 그 기준을 충족시키지 않는 자연 정렬 순서 비교를 수행하는 함수가 있지만 StrCmpLogicalW 또는 다른 것인지는 기억할 수 없습니다.


이 진술에 대한 증거가 있습니까? 인터넷 검색 후, 나는 그것이 사실이라는 표시를 찾을 수 없습니다.
mhenry1384

1
StrCmpLogicalW에서 무한 루프를 경험했습니다.
THD


비주얼 스튜디오 피드백 항목 236900가 더 이상 존재하지 않습니다,하지만 여기에 확인한다이 더 최신의 하나를 문제 : connect.microsoft.com/VisualStudio/feedback/details/774540/...는 또한-를 해결하려면를 제공합니다 CultureInfo속성을 가지고 CompareInfo반환되는 객체는 객체를 제공 할 수 있습니다 SortKey. 이들은 차례로 비교 될 수 있고 일시 적성을 보장 할 수 있습니다.
Jonathan Gilbert

9

이것은 알파벳과 숫자를 모두 가진 문자열을 정렬하는 코드입니다.

먼저이 확장 방법 :

public static IEnumerable<string> AlphanumericSort(this IEnumerable<string> me)
{
    return me.OrderBy(x => Regex.Replace(x, @"\d+", m => m.Value.PadLeft(50, '0')));
}

그런 다음 코드에서 다음과 같이 간단히 사용하십시오.

List<string> test = new List<string>() { "The 1st", "The 12th", "The 2nd" };
test = test.AlphanumericSort();

어떻게 작동합니까? 0으로 대체하여 :

  Original  | Regex Replace |      The      |   Returned
    List    | Apply PadLeft |    Sorting    |     List
            |               |               |
 "The 1st"  |  "The 001st"  |  "The 001st"  |  "The 1st"
 "The 12th" |  "The 012th"  |  "The 002nd"  |  "The 2nd"
 "The 2nd"  |  "The 002nd"  |  "The 012th"  |  "The 12th"

배수와 함께 작동 :

 Alphabetical Sorting | Alphanumeric Sorting
                      |
 "Page 21, Line 42"   | "Page 3, Line 7"
 "Page 21, Line 5"    | "Page 3, Line 32"
 "Page 3, Line 32"    | "Page 21, Line 5"
 "Page 3, Line 7"     | "Page 21, Line 42"

그것이 도움이되기를 바랍니다.


6

에 추가 그렉 비치의 대답 (I 그냥 찾던 때문에) 당신이 사용할 수있는 Linq에에서 이것을 사용하려는 경우, OrderBy걸리는 것을 IComparer. 예 :

var items = new List<MyItem>();

// fill items

var sorted = items.OrderBy(item => item.Name, new NaturalStringComparer());

2

다음은 P / Invoke를 사용하지 않고 실행 중에 할당을 피하는 비교적 간단한 예입니다.

internal sealed class NumericStringComparer : IComparer<string>
{
    public static NumericStringComparer Instance { get; } = new NumericStringComparer();

    public int Compare(string x, string y)
    {
        // sort nulls to the start
        if (x == null)
            return y == null ? 0 : -1;
        if (y == null)
            return 1;

        var ix = 0;
        var iy = 0;

        while (true)
        {
            // sort shorter strings to the start
            if (ix >= x.Length)
                return iy >= y.Length ? 0 : -1;
            if (iy >= y.Length)
                return 1;

            var cx = x[ix];
            var cy = y[iy];

            int result;
            if (char.IsDigit(cx) && char.IsDigit(cy))
                result = CompareInteger(x, y, ref ix, ref iy);
            else
                result = cx.CompareTo(y[iy]);

            if (result != 0)
                return result;

            ix++;
            iy++;
        }
    }

    private static int CompareInteger(string x, string y, ref int ix, ref int iy)
    {
        var lx = GetNumLength(x, ix);
        var ly = GetNumLength(y, iy);

        // shorter number first (note, doesn't handle leading zeroes)
        if (lx != ly)
            return lx.CompareTo(ly);

        for (var i = 0; i < lx; i++)
        {
            var result = x[ix++].CompareTo(y[iy++]);
            if (result != 0)
                return result;
        }

        return 0;
    }

    private static int GetNumLength(string s, int i)
    {
        var length = 0;
        while (i < s.Length && char.IsDigit(s[i++]))
            length++;
        return length;
    }
}

선행 0을 무시하지 않으므로 01이후에옵니다 2.

해당 단위 테스트 :

public class NumericStringComparerTests
{
    [Fact]
    public void OrdersCorrectly()
    {
        AssertEqual("", "");
        AssertEqual(null, null);
        AssertEqual("Hello", "Hello");
        AssertEqual("Hello123", "Hello123");
        AssertEqual("123", "123");
        AssertEqual("123Hello", "123Hello");

        AssertOrdered("", "Hello");
        AssertOrdered(null, "Hello");
        AssertOrdered("Hello", "Hello1");
        AssertOrdered("Hello123", "Hello124");
        AssertOrdered("Hello123", "Hello133");
        AssertOrdered("Hello123", "Hello223");
        AssertOrdered("123", "124");
        AssertOrdered("123", "133");
        AssertOrdered("123", "223");
        AssertOrdered("123", "1234");
        AssertOrdered("123", "2345");
        AssertOrdered("0", "1");
        AssertOrdered("123Hello", "124Hello");
        AssertOrdered("123Hello", "133Hello");
        AssertOrdered("123Hello", "223Hello");
        AssertOrdered("123Hello", "1234Hello");
    }

    private static void AssertEqual(string x, string y)
    {
        Assert.Equal(0, NumericStringComparer.Instance.Compare(x, y));
        Assert.Equal(0, NumericStringComparer.Instance.Compare(y, x));
    }

    private static void AssertOrdered(string x, string y)
    {
        Assert.Equal(-1, NumericStringComparer.Instance.Compare(x, y));
        Assert.Equal( 1, NumericStringComparer.Instance.Compare(y, x));
    }
}

2

실제로 확장 방법으로 구현 StringComparer하여 예를 들어 수행 할 수 있습니다.

  • StringComparer.CurrentCulture.WithNaturalSort() 또는
  • StringComparer.OrdinalIgnoreCase.WithNaturalSort().

결과가 IComparer<string>좋아하는 모든 장소에서 사용할 수있는 OrderBy, OrderByDescending, ThenBy, ThenByDescending,SortedSet<string> , 등을 할 수 있습니다 여전히 쉽게 팅겨 대소 문자 구분, 문화 등

구현은 매우 사소한 것이며 큰 시퀀스에서도 잘 수행되어야합니다.


또한 작은 NuGet 패키지 로 게시 했으므로 다음을 수행 할 수 있습니다.

Install-Package NaturalSort.Extension

XML 문서 주석 및 테스트 모음을 포함한 코드 는 NaturalSort.Extension GitHub 리포지토리 에서 사용할 수 있습니다 .


전체 코드는 다음과 같습니다 (C # 7을 아직 사용할 수없는 경우 NuGet 패키지 만 설치하십시오).

public static class StringComparerNaturalSortExtension
{
    public static IComparer<string> WithNaturalSort(this StringComparer stringComparer) => new NaturalSortComparer(stringComparer);

    private class NaturalSortComparer : IComparer<string>
    {
        public NaturalSortComparer(StringComparer stringComparer)
        {
            _stringComparer = stringComparer;
        }

        private readonly StringComparer _stringComparer;
        private static readonly Regex NumberSequenceRegex = new Regex(@"(\d+)", RegexOptions.Compiled | RegexOptions.CultureInvariant);
        private static string[] Tokenize(string s) => s == null ? new string[] { } : NumberSequenceRegex.Split(s);
        private static ulong ParseNumberOrZero(string s) => ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var result) ? result : 0;

        public int Compare(string s1, string s2)
        {
            var tokens1 = Tokenize(s1);
            var tokens2 = Tokenize(s2);

            var zipCompare = tokens1.Zip(tokens2, TokenCompare).FirstOrDefault(x => x != 0);
            if (zipCompare != 0)
                return zipCompare;

            var lengthCompare = tokens1.Length.CompareTo(tokens2.Length);
            return lengthCompare;
        }
        
        private int TokenCompare(string token1, string token2)
        {
            var number1 = ParseNumberOrZero(token1);
            var number2 = ParseNumberOrZero(token2);

            var numberCompare = number1.CompareTo(number2);
            if (numberCompare != 0)
                return numberCompare;

            var stringCompare = _stringComparer.Compare(token1, token2);
            return stringCompare;
        }
    }
}

2

다음은 순진한 한 줄 정규 표현식이없는 LINQ 방법입니다 (파이썬에서 차용).

var alphaStrings = new List<string>() { "10","2","3","4","50","11","100","a12","b12" };
var orderedString = alphaStrings.OrderBy(g => new Tuple<int, string>(g.ToCharArray().All(char.IsDigit)? int.Parse(g) : int.MaxValue, g));
// Order Now: ["2","3","4","10","11","50","100","a12","b12"]

Dump ()를 제거하고 var에 할당하면 매력처럼 작동합니다!
Arne S

@ArneS : LinQPad로 작성되었습니다. 그리고를 제거하는 것을 잊었습니다 Dump(). 지적 해 주셔서 감사합니다.
mshsayem

1

이전 답변 중 몇 가지를 확장하고 확장 방법을 사용하여 잠재적으로 여러 열거 가능한 열거의주의 또는 여러 정규 표현식 객체를 사용하거나 불필요하게 정규 표현식을 호출하는 것과 관련된 성능 문제가없는 다음을 생각해 냈습니다. 그러나 ToList ()를 사용하면 더 큰 컬렉션의 이점을 무시할 수 있습니다.

선택기는 대리자를 할당 할 수 있도록 일반 입력을 지원하고 소스 컬렉션의 요소는 선택자에 의해 변형 된 다음 ToString ()을 사용하여 문자열로 변환됩니다.

    private static readonly Regex _NaturalOrderExpr = new Regex(@"\d+", RegexOptions.Compiled);

    public static IEnumerable<TSource> OrderByNatural<TSource, TKey>(
        this IEnumerable<TSource> source, Func<TSource, TKey> selector)
    {
        int max = 0;

        var selection = source.Select(
            o =>
            {
                var v = selector(o);
                var s = v != null ? v.ToString() : String.Empty;

                if (!String.IsNullOrWhiteSpace(s))
                {
                    var mc = _NaturalOrderExpr.Matches(s);

                    if (mc.Count > 0)
                    {
                        max = Math.Max(max, mc.Cast<Match>().Max(m => m.Value.Length));
                    }
                }

                return new
                {
                    Key = o,
                    Value = s
                };
            }).ToList();

        return
            selection.OrderBy(
                o =>
                String.IsNullOrWhiteSpace(o.Value) ? o.Value : _NaturalOrderExpr.Replace(o.Value, m => m.Value.PadLeft(max, '0')))
                     .Select(o => o.Key);
    }

    public static IEnumerable<TSource> OrderByDescendingNatural<TSource, TKey>(
        this IEnumerable<TSource> source, Func<TSource, TKey> selector)
    {
        int max = 0;

        var selection = source.Select(
            o =>
            {
                var v = selector(o);
                var s = v != null ? v.ToString() : String.Empty;

                if (!String.IsNullOrWhiteSpace(s))
                {
                    var mc = _NaturalOrderExpr.Matches(s);

                    if (mc.Count > 0)
                    {
                        max = Math.Max(max, mc.Cast<Match>().Max(m => m.Value.Length));
                    }
                }

                return new
                {
                    Key = o,
                    Value = s
                };
            }).ToList();

        return
            selection.OrderByDescending(
                o =>
                String.IsNullOrWhiteSpace(o.Value) ? o.Value : _NaturalOrderExpr.Replace(o.Value, m => m.Value.PadLeft(max, '0')))
                     .Select(o => o.Key);
    }

1

Michael Parker의 솔루션에서 영감을 얻은 다음은 IComparerlinq 주문 방법 중 하나에 빠질 수 있는 구현입니다.

private class NaturalStringComparer : IComparer<string>
{
    public int Compare(string left, string right)
    {
        int max = new[] { left, right }
            .SelectMany(x => Regex.Matches(x, @"\d+").Cast<Match>().Select(y => (int?)y.Value.Length))
            .Max() ?? 0;

        var leftPadded = Regex.Replace(left, @"\d+", m => m.Value.PadLeft(max, '0'));
        var rightPadded = Regex.Replace(right, @"\d+", m => m.Value.PadLeft(max, '0'));

        return string.Compare(leftPadded, rightPadded);
    }
}

0

우리는 다음과 같은 패턴으로 텍스트를 처리 할 수있는 자연스러운 정렬이 필요했습니다.

"Test 1-1-1 something"
"Test 1-2-3 something"
...

내가 처음으로 SO를 보았을 때 어떤 이유로 나는이 게시물을 찾지 못하고 우리 자신을 구현했습니다. 여기에 제시된 일부 솔루션과 비교할 때 개념은 비슷하지만 더 단순하고 이해하기 쉽다는 이점이 있습니다. 그러나 성능 병목 현상을 보려고했지만 기본보다 훨씬 느린 구현입니다.OrderBy() .

내가 구현 한 확장 방법은 다음과 같습니다.

public static class EnumerableExtensions
{
    // set up the regex parser once and for all
    private static readonly Regex Regex = new Regex(@"\d+|\D+", RegexOptions.Compiled | RegexOptions.Singleline);

    // stateless comparer can be built once
    private static readonly AggregateComparer Comparer = new AggregateComparer();

    public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> source, Func<T, string> selector)
    {
        // first extract string from object using selector
        // then extract digit and non-digit groups
        Func<T, IEnumerable<IComparable>> splitter =
            s => Regex.Matches(selector(s))
                      .Cast<Match>()
                      .Select(m => Char.IsDigit(m.Value[0]) ? (IComparable) int.Parse(m.Value) : m.Value);
        return source.OrderBy(splitter, Comparer);
    }

    /// <summary>
    /// This comparer will compare two lists of objects against each other
    /// </summary>
    /// <remarks>Objects in each list are compare to their corresponding elements in the other
    /// list until a difference is found.</remarks>
    private class AggregateComparer : IComparer<IEnumerable<IComparable>>
    {
        public int Compare(IEnumerable<IComparable> x, IEnumerable<IComparable> y)
        {
            return
                x.Zip(y, (a, b) => new {a, b})              // walk both lists
                 .Select(pair => pair.a.CompareTo(pair.b))  // compare each object
                 .FirstOrDefault(result => result != 0);    // until a difference is found
        }
    }
}

아이디어는 원래 문자열을 숫자 블록과 숫자가 아닌 블록으로 나누는 것입니다."\d+|\D+" ) . 이 작업은 잠재적으로 비용이 많이 드는 작업이므로 항목 당 한 번만 수행됩니다. 그런 다음 비교 가능한 객체의 비교자를 사용합니다 (죄송합니다.보다 적절한 방법을 찾을 수 없습니다). 각 블록을 다른 문자열의 해당 블록과 비교합니다.

이것이 어떻게 개선 될 수 있고 주요 결함이 무엇인지에 대한 피드백을 원합니다. 이 시점에서 유지 관리가 중요하며 현재는 매우 큰 데이터 세트에서이를 사용하지 않습니다.


1
구조적으로 다른 문자열을 비교하려고 할 때 충돌이 발생합니다. 예를 들어 "a-1"과 "a-2"를 비교하면 문제가 없지만 "a"때문에 "a"와 "1"을 비교하는 것은 좋지 않습니다 .CompareTo (1) 예외가 발생합니다.
jimrandomh

@ jimrandomh, 당신은 맞습니다. 이 접근법은 우리의 패턴에 따라 다릅니다.
Eric Liprandi

0

읽기 / 유지하기 쉬운 버전.

public class NaturalStringComparer : IComparer<string>
{
    public static NaturalStringComparer Instance { get; } = new NaturalStringComparer();

    public int Compare(string x, string y) {
        const int LeftIsSmaller = -1;
        const int RightIsSmaller = 1;
        const int Equal = 0;

        var leftString = x;
        var rightString = y;

        var stringComparer = CultureInfo.CurrentCulture.CompareInfo;

        int rightIndex;
        int leftIndex;

        for (leftIndex = 0, rightIndex = 0;
             leftIndex < leftString.Length && rightIndex < rightString.Length;
             leftIndex++, rightIndex++) {
            var leftChar = leftString[leftIndex];
            var rightChar = rightString[leftIndex];

            var leftIsNumber = char.IsNumber(leftChar);
            var rightIsNumber = char.IsNumber(rightChar);

            if (!leftIsNumber && !rightIsNumber) {
                var result = stringComparer.Compare(leftString, leftIndex, 1, rightString, leftIndex, 1);
                if (result != 0) return result;
            } else if (leftIsNumber && !rightIsNumber) {
                return LeftIsSmaller;
            } else if (!leftIsNumber && rightIsNumber) {
                return RightIsSmaller;
            } else {
                var leftNumberLength = NumberLength(leftString, leftIndex, out var leftNumber);
                var rightNumberLength = NumberLength(rightString, rightIndex, out var rightNumber);

                if (leftNumberLength < rightNumberLength) {
                    return LeftIsSmaller;
                } else if (leftNumberLength > rightNumberLength) {
                    return RightIsSmaller;
                } else {
                    if(leftNumber < rightNumber) {
                        return LeftIsSmaller;
                    } else if(leftNumber > rightNumber) {
                        return RightIsSmaller;
                    }
                }
            }
        }

        if (leftString.Length < rightString.Length) {
            return LeftIsSmaller;
        } else if(leftString.Length > rightString.Length) {
            return RightIsSmaller;
        }

        return Equal;
    }

    public int NumberLength(string str, int offset, out int number) {
        if (string.IsNullOrWhiteSpace(str)) throw new ArgumentNullException(nameof(str));
        if (offset >= str.Length) throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be less than the length of the string.");

        var currentOffset = offset;

        var curChar = str[currentOffset];

        if (!char.IsNumber(curChar))
            throw new ArgumentException($"'{curChar}' is not a number.", nameof(offset));

        int length = 1;

        var numberString = string.Empty;

        for (currentOffset = offset + 1;
            currentOffset < str.Length;
            currentOffset++, length++) {

            curChar = str[currentOffset];
            numberString += curChar;

            if (!char.IsNumber(curChar)) {
                number = int.Parse(numberString);

                return length;
            }
        }

        number = int.Parse(numberString);

        return length;
    }
}

-2

내 문제와 내가 어떻게 해결할 수 있었는지 설명하겠습니다.

문제 :-디렉토리에서 검색된 FileInfo 객체에서 FileName을 기준으로 파일을 정렬합니다.

솔루션 :-FileInfo에서 파일 이름을 선택하고 파일 이름의 ".png"부분을 다듬 었습니다. 이제 파일 이름을 자연 정렬 순서로 정렬하는 List.Sort ()를 수행하십시오. 내 테스트를 기반으로 .png가 정렬 순서를 망칠 것으로 나타났습니다. 아래 코드를 살펴보십시오

var imageNameList = new DirectoryInfo(@"C:\Temp\Images").GetFiles("*.png").Select(x =>x.Name.Substring(0, x.Name.Length - 4)).ToList();
imageNameList.Sort();

이 답변에서 -1의 이유를 알 수 있습니까?
girishkatta9
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.