Thứ tự sắp xếp tự nhiên trong C #


129

Bất cứ ai cũng có một tài nguyên tốt hoặc cung cấp một mẫu sắp xếp thứ tự tự nhiên trong C # cho một FileInfomảng? Tôi đang thực hiện IComparergiao diện trong các loại của tôi.

Câu trả lời:


148

Cách dễ nhất để làm chỉ là P / Gọi hàm tích hợp trong Windows và sử dụng nó làm hàm so sánh trong IComparer:

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

Michael Kaplan có một số ví dụ về cách chức năng này hoạt động ở đây và những thay đổi được thực hiện cho Vista để làm cho nó hoạt động trực quan hơn. Điểm cộng của chức năng này là nó sẽ có hành vi tương tự như phiên bản Windows mà nó chạy, tuy nhiên điều này có nghĩa là nó khác nhau giữa các phiên bản Windows, vì vậy bạn cần xem xét liệu đây có phải là vấn đề với bạn không.

Vì vậy, một thực hiện đầy đủ sẽ là một cái gì đó như:

[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
Câu trả lời chính xác. Hãy cẩn thận: Điều này sẽ không hoạt động với Win2000, vì một vài người vẫn đang chạy mọi thứ trên hệ điều hành đó. Mặt khác, có đủ gợi ý giữa blog của Kaplan và tài liệu MSDN để tạo ra một chức năng tương tự.
Chris Charabaruk

9
Đây không phải là di động, chỉ hoạt động trong Win32, nhưng không hoạt động trong Linux / MacOS / Silverlight / Windows Phone / Metro
nối tiếp

20
@linquize - Ông nói .NET không phải Mono, vì vậy Linux / OSX không thực sự là vấn đề đáng lo ngại. Windows Phone / Metro không tồn tại vào năm 2008 khi câu trả lời này được đăng. Và bạn có thường xuyên thực hiện các thao tác tệp trong Silverlight không? Vì vậy, đối với OP, và có lẽ hầu hết những người khác, đó là một câu trả lời phù hợp. Trong mọi trường hợp, bạn được tự do cung cấp một câu trả lời tốt hơn; đó là cách trang web này hoạt động.
Greg Beech

6
Điều này không có nghĩa là câu trả lời ban đầu là sai. Tôi chỉ cần thêm thông tin bổ sung với thông tin cập nhật
linquize

2
FYI, nếu bạn kế thừa từ Comparer<T>thay vì triển khai IComparer<T>, bạn sẽ có một triển khai tích hợp của IComparergiao diện (không chung chung) gọi phương thức chung của bạn, để sử dụng trong các API sử dụng thay thế. Về cơ bản nó cũng miễn phí: chỉ cần xóa "tôi" và đổi public int Compare(...)thành public override int Compare(...). Tương tự cho IEqualityComparer<T>EqualityComparer<T>.
Joe Amenta

75

Chỉ cần nghĩ rằng tôi sẽ thêm vào điều này (với giải pháp ngắn gọn nhất mà tôi có thể tìm thấy):

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

Các phần trên ở trên bất kỳ số nào trong chuỗi đến độ dài tối đa của tất cả các số trong tất cả các chuỗi và sử dụng chuỗi kết quả để sắp xếp.

Việc chuyển sang ( int?) là để cho phép các tập hợp các chuỗi mà không có bất kỳ số nào ( .Max()trên một số vô số trống ném một InvalidOperationException).


1
+1 Không chỉ là ngắn gọn nhất mà nó còn là nhanh nhất tôi từng thấy. ngoại trừ câu trả lời được chấp nhận nhưng tôi không thể sử dụng câu trả lời đó vì phụ thuộc vào máy. Nó sắp xếp hơn 4 triệu giá trị trong khoảng 35 giây.
Gene S

4
Điều này vừa đẹp vừa không thể đọc được. Tôi cho rằng những lợi ích của Linq sẽ có nghĩa (ít nhất là) hiệu suất trung bình và trường hợp tốt nhất, vì vậy tôi nghĩ rằng tôi sẽ đi theo nó. Mặc dù thiếu rõ ràng. Cảm ơn rất nhiều @Matthew Horsley
Ian Grainger

1
Điều này là rất tốt, nhưng có một lỗi cho các số thập phân nhất định, ví dụ của tôi là sắp xếp k8.11 so với k8.2. Để khắc phục điều này, tôi đã triển khai regex sau: \ d + ([\.,] \ D)?
devzero

2
Bạn cũng cần đưa chiều dài của nhóm thứ hai (số thập phân + số thập phân) vào tài khoản khi bạn nhập mã này m.Value.PadLeft (max, '0')
devzero

3
Tôi nghĩ rằng bạn có thể sử dụng .DefaultIfEmpty().Max()thay vì đúc int?. Ngoài ra, nó là giá trị để làm một source.ToList()để tránh liệt kê lại vô số.
Teejay

30

Không có triển khai nào hiện có vẻ tuyệt vời nên tôi đã tự viết. Các kết quả gần giống với cách sắp xếp được sử dụng bởi các phiên bản Windows Explorer hiện đại (Windows 7/8). Sự khác biệt duy nhất tôi thấy là 1) mặc dù Windows đã sử dụng (ví dụ XP) xử lý các số có độ dài bất kỳ, nhưng hiện tại nó bị giới hạn ở 19 chữ số - của tôi là không giới hạn, 2) Windows cho kết quả không nhất quán với một số chữ số Unicode nhất định - hoạt động của tôi tốt (mặc dù nó không so sánh các chữ số từ các cặp thay thế; Windows cũng không) và 3) tôi không thể phân biệt các loại trọng số không chính khác nhau nếu chúng xuất hiện trong các phần khác nhau (ví dụ: "e-1é" vs " é1e- "- các phần trước và sau số có chênh lệch trọng lượng dấu phụ và dấu chấm câu).

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

Chữ ký phù hợp với Comparison<string>đại biểu:

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

Đây là một lớp bao bọc để sử dụng như 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);
    }
}

Thí dụ:

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

Đây là một bộ tên tệp tốt mà tôi sử dụng để thử nghiệm:

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

Các phần chữ số cần được so sánh theo phần khôn ngoan, nghĩa là, 'abc12b' phải nhỏ hơn 'abc123'.
SOUser

Bạn có thể thử các dữ liệu sau: chuỗi công khai [] 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 Cảm ơn trường hợp kiểm tra tốt. Nếu bạn để Windows Explorer sắp xếp các tệp đó, bạn sẽ nhận được các kết quả khác nhau tùy thuộc vào phiên bản Windows bạn đang sử dụng. Mã của tôi sắp xếp các tên đó giống hệt với Server 2003 (và có lẽ là XP), nhưng khác với Windows 8. Nếu có cơ hội tôi sẽ cố gắng tìm hiểu xem Windows 8 đang làm gì và cập nhật mã của tôi.
JD

2
Có lỗi. Index Out Of Range
sắp xếp thứ tự

3
Giải pháp tuyệt vời! Khi tôi đo điểm chuẩn trong một kịch bản bình thường với khoảng 10.000 tệp, nó nhanh hơn ví dụ regex của Matthew và về hiệu suất tương tự như StrCmpLogicalW (). Có một lỗi nhỏ trong đoạn mã trên: "while (strA [jA] == zeroA) jA ++;" và "while (strB [jB] == zeroB) jB ++;" nên là "while (jA <strA.Lpm && strA [jA] == zeroA) jA ++;" và "while (jB <strB.Lpm && strB [jB] == zeroB) jB ++;". Nếu không, các chuỗi chỉ chứa các số 0 sẽ đưa ra một ngoại lệ.
Kuroki

22

Giải pháp C # tinh khiết cho linq orderby:

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
Mã đó cuối cùng là từ codeproject.com/KB/recipes/NaturalComparer.aspx (không theo định hướng LINQ).
mhenry1384

2
Bài đăng trên blog ghi có Justin Jones ( codeproject.com/KB/opes/NaturalSortComparer.aspx ) cho IComparer, không phải Pascal Ganaye.
James McCormack

1
Lưu ý nhỏ, giải pháp này bỏ qua các không gian không giống với những gì cửa sổ thực hiện và không tốt như mã của Matthew Horsley bên dưới. Vì vậy, bạn có thể lấy 'chuỗi01' 'chuỗi 01' 'chuỗi 02' 'chuỗi02' chẳng hạn (trông xấu xí). Nếu bạn loại bỏ tước khoảng trắng, nó sẽ sắp xếp các chuỗi ngược lại, tức là 'chuỗi01' xuất hiện trước 'chuỗi 01', điều này có thể hoặc không thể chấp nhận được.
Michael Parker

Điều này làm việc cho các địa chỉ, ví dụ "1 Smith Rd", "10 Smith Rd", "2 Smith Rd", v.v. - Sắp xếp tự nhiên. Đúng! Đẹp quá
Piotr Kula

Nhân tiện, tôi nhận thấy (và các bình luận trên trang được liên kết đó dường như cũng chỉ ra) rằng đối số Loại <T> là hoàn toàn không cần thiết.
jv-dev

18

Câu trả lời của Matthews Horsley là phương pháp nhanh nhất không thay đổi hành vi tùy thuộc vào phiên bản cửa sổ nào mà chương trình của bạn đang chạy. Tuy nhiên, nó có thể nhanh hơn nữa bằng cách tạo regex một lần và sử dụng RegexOptions.Compiled. Tôi cũng đã thêm tùy chọn chèn một bộ so sánh chuỗi để bạn có thể bỏ qua trường hợp nếu cần và cải thiện khả năng đọc một chút.

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

Sử dụng bởi

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

Việc này mất 450ms để sắp xếp 100.000 chuỗi so với 300ms để so sánh chuỗi .net mặc định - khá nhanh!



16

Giải pháp của tôi:

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

Các kết quả:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2

Tôi thích nó. Thật dễ hiểu và không cần Linq.

11

Bạn cần phải cẩn thận - Tôi mơ hồ nhớ lại rằng StrCmpLogicalW, hoặc một cái gì đó tương tự, không hoàn toàn mang tính bắc cầu và tôi đã quan sát các phương pháp sắp xếp của .NET đôi khi bị mắc kẹt trong các vòng lặp vô hạn nếu hàm so sánh phá vỡ quy tắc đó.

Một so sánh bắc cầu sẽ luôn báo cáo rằng a <c nếu a <b và b <c. Tồn tại một hàm thực hiện so sánh thứ tự tự nhiên không phải lúc nào cũng đáp ứng tiêu chí đó, nhưng tôi không thể nhớ liệu đó có phải là StrCmpLogicalW hay cái gì khác không.


Bạn có bất cứ bằng chứng nào về tuyên bố này không? Sau khi googling xung quanh, tôi không thể tìm thấy bất kỳ dấu hiệu nào cho thấy đó là sự thật.
mhenry1384

1
Tôi đã trải nghiệm những vòng lặp vô hạn đó với StrCmpLogicalW.
THD


Visual Studio phản hồi mục 236.900 không còn tồn tại, nhưng đây là một one-to-date lên hơn là khẳng định vấn đề: connect.microsoft.com/VisualStudio/feedback/details/774540/... Nó cũng đưa ra một công việc xung quanh: CultureInfocó một tài sản CompareInfovà đối tượng mà nó trả về có thể cung cấp cho bạn SortKeycác đối tượng. Chúng, có thể được so sánh và đảm bảo tính siêu việt.
Jonathan Gilbert

9

Đây là mã của tôi để sắp xếp một chuỗi có cả ký tự alpha và số.

Đầu tiên, phương thức mở rộng này:

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

Sau đó, chỉ cần sử dụng nó ở bất cứ đâu trong mã của bạn như thế này:

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

Nó làm việc như thế nào ? Bằng cách thay thế bằng số không:

  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"

Hoạt động với nhiều số:

 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"

Hy vọng điều đó sẽ giúp.


6

Thêm vào câu trả lời Greg Beech của (vì tôi đã chỉ được tìm kiếm đó), nếu bạn muốn sử dụng này từ LINQ bạn có thể sử dụng OrderBymà phải mất một IComparer. Ví dụ:

var items = new List<MyItem>();

// fill items

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

2

Đây là một ví dụ tương đối đơn giản không sử dụng P / Gọi và tránh mọi phân bổ trong khi thực hiện.

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

Nó không bỏ qua các số 0 hàng đầu, vì vậy 01đến sau 2.

Kiểm tra đơn vị tương ứng:

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

Tôi thực sự đã triển khai nó như một phương thức mở rộng StringComparerđể bạn có thể làm ví dụ:

  • StringComparer.CurrentCulture.WithNaturalSort() hoặc là
  • StringComparer.OrdinalIgnoreCase.WithNaturalSort().

Các kết quả IComparer<string>có thể được sử dụng trong tất cả các nơi thích OrderBy, OrderByDescending, ThenBy, ThenByDescending, SortedSet<string>, vv Và bạn vẫn có thể dễ dàng tinh chỉnh trường hợp nhạy cảm, văn hóa vv

Việc thực hiện khá tầm thường và nó sẽ thực hiện khá tốt ngay cả trên các chuỗi lớn.


Tôi cũng đã xuất bản nó dưới dạng gói NuGet nhỏ , vì vậy bạn chỉ có thể làm:

Install-Package NaturalSort.Extension

Mã bao gồm các nhận xét tài liệu XML và bộ kiểm tra có sẵn trong kho lưu trữ NaturalSort.Extension GitHub .


Toàn bộ mã là thế này (nếu bạn chưa thể sử dụng C # 7, chỉ cần cài đặt gói 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

Dưới đây là cách LINQ một dòng regex ngây thơ (mượn từ python):

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"]

Đã xóa Dump () và được gán cho var và điều này hoạt động như một bùa mê!
Arne S

@ArneS: Nó được viết bằng LinQPad; và tôi quên loại bỏ Dump(). Cảm ơn đã chỉ ra.
mshsayem

1

Mở rộng một vài câu trả lời trước và sử dụng các phương thức mở rộng, tôi đã đưa ra những điều sau đây không có khả năng liệt kê nhiều liệt kê, hoặc các vấn đề về hiệu suất liên quan đến việc sử dụng nhiều đối tượng regex hoặc gọi regex một cách không cần thiết, rằng đang nói, nó sử dụng ToList (), có thể phủ nhận lợi ích trong các bộ sưu tập lớn hơn.

Bộ chọn hỗ trợ gõ chung để cho phép bất kỳ đại biểu nào được chỉ định, các phần tử trong bộ sưu tập nguồn được thay đổi bởi bộ chọn, sau đó được chuyển đổi thành chuỗi bằng 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

Lấy cảm hứng từ giải pháp của Michael Park, đây là một IComparertriển khai mà bạn có thể tham gia vào bất kỳ phương thức đặt hàng linq nào:

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

Chúng tôi có nhu cầu sắp xếp tự nhiên để xử lý văn bản theo mẫu sau:

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

Vì một số lý do khi lần đầu tiên tôi nhìn vào SO, tôi đã không tìm thấy bài đăng này và tự thực hiện. So với một số giải pháp được trình bày ở đây, trong khi tương tự về khái niệm, nó có thể có lợi ích có thể đơn giản và dễ hiểu hơn. Tuy nhiên, trong khi tôi đã cố gắng xem xét các tắc nghẽn về hiệu suất, thì nó vẫn là một triển khai chậm hơn nhiều so với mặc định OrderBy().

Đây là phương pháp mở rộng tôi thực hiện:

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
        }
    }
}

Ý tưởng là chia các chuỗi gốc thành các khối chữ số và không chữ số ( "\d+|\D+"). Vì đây là một nhiệm vụ đắt tiền, nó chỉ được thực hiện một lần cho mỗi lần nhập. Sau đó, chúng tôi sử dụng một so sánh các đối tượng so sánh (xin lỗi, tôi không thể tìm thấy một cách thích hợp hơn để nói nó). Nó so sánh mỗi khối với khối tương ứng của nó trong chuỗi khác.

Tôi muốn phản hồi về cách cải thiện điều này và những sai sót lớn là gì. Lưu ý rằng khả năng bảo trì rất quan trọng đối với chúng tôi tại thời điểm này và chúng tôi hiện không sử dụng điều này trong các tập dữ liệu cực lớn.


1
Điều này gặp sự cố khi nó cố so sánh các chuỗi khác nhau về cấu trúc - ví dụ: so sánh "a-1" với "a-2" hoạt động tốt, nhưng so sánh "a" với "1" thì không, bởi vì "a" .CompareTo (1) ném một ngoại lệ.
jimrandomh

@jimrandomh, bạn đúng rồi. Cách tiếp cận này là cụ thể cho các mẫu của chúng tôi.
Eric Liprandi

0

Một phiên bản dễ đọc / bảo trì hơn.

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

Hãy để tôi giải thích vấn đề của tôi và làm thế nào tôi có thể giải quyết nó.

Sự cố: - Sắp xếp tệp dựa trên Tên tệp từ các đối tượng FileInfo được truy xuất từ ​​Thư mục.

Giải pháp: - Tôi đã chọn tên tệp từ FileInfo và truy xuất phần ".png" của tên tệp. Bây giờ, chỉ cần làm List.Sort (), sắp xếp tên tệp theo thứ tự sắp xếp tự nhiên. Dựa trên thử nghiệm của tôi, tôi thấy rằng có .png làm rối trật tự sắp xếp. Hãy xem đoạn mã dưới đây

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

Tôi có thể biết lý do cho -1 về câu trả lời này không?
girishkatta9
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.