Urutan Sortir Alami dalam C #

129

Adakah yang punya sumber daya yang bagus atau memberikan contoh jenis urutan alami dalam C # untuk sebuah FileInfoarray? Saya mengimplementasikan IComparerantarmuka dalam jenis saya.

Michael Kniskern
sumber

Jawaban:

148

Hal termudah untuk dilakukan adalah hanya P / Aktifkan fungsi bawaan di Windows, dan gunakan sebagai fungsi perbandingan di IComparer:

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

Michael Kaplan memiliki beberapa contoh tentang bagaimana fungsi ini bekerja di sini , dan perubahan yang dibuat untuk Vista agar berfungsi lebih intuitif. Sisi positif dari fungsi ini adalah ia akan memiliki perilaku yang sama dengan versi Windows yang dijalankannya, namun ini berarti bahwa ia berbeda antara versi Windows sehingga Anda perlu mempertimbangkan apakah ini masalah bagi Anda.

Jadi implementasi yang lengkap akan menjadi seperti:

[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);
    }
}
Greg Beech
sumber
8
Jawaban yang bagus Peringatan: Ini tidak akan bekerja dengan Win2000, karena beberapa orang itu masih menjalankan hal-hal pada sistem operasi itu. Di sisi lain, ada cukup petunjuk antara blog Kaplan dan dokumentasi MSDN untuk membuat fungsi serupa.
Chris Charabaruk
9
Ini tidak portabel, hanya bekerja di Win32, tetapi tidak bekerja di Linux / MacOS / Silverlight / Windows Phone / Metro
linquize
20
@linquize - Katanya. NET bukan Mono, jadi Linux / OSX tidak terlalu memprihatinkan. Windows Phone / Metro tidak ada pada 2008 ketika jawaban ini diposting. Dan seberapa sering Anda melakukan operasi file di Silverlight? Jadi untuk OP, dan mungkin sebagian besar orang lain, itu adalah jawaban yang cocok. Bagaimanapun, Anda bebas untuk memberikan jawaban yang lebih baik; begitulah cara kerja situs ini.
Greg Beech
6
Ini tidak berarti bahwa jawaban aslinya salah. Saya hanya menambahkan info tambahan dengan informasi terkini
linquize
2
FYI, jika Anda mewarisi dari Comparer<T>alih-alih mengimplementasikan IComparer<T>, Anda mendapatkan implementasi bawaan dari IComparerantarmuka (non-generik) yang memanggil metode generik Anda, untuk digunakan dalam API yang menggunakannya. Ini pada dasarnya gratis untuk dilakukan juga: cukup hapus "I" dan ubah public int Compare(...)ke public override int Compare(...). Sama untuk IEqualityComparer<T>dan EqualityComparer<T>.
Joe Amenta
75

Hanya berpikir saya akan menambahkan ini (dengan solusi paling ringkas yang bisa saya temukan):

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

Di atas bantalan nomor dalam string dengan panjang maksimal semua angka di semua string dan menggunakan string yang dihasilkan untuk mengurutkan.

Pemain ke ( int?) adalah untuk memungkinkan koleksi string tanpa nomor ( .Max()pada lemparan enumerable kosong a InvalidOperationException).

Matthew Horsley
sumber
1
+1 Tidak hanya itu yang paling ringkas itu adalah yang tercepat yang pernah saya lihat. kecuali untuk jawaban yang diterima tetapi saya tidak dapat menggunakannya karena ketergantungan mesin. Ini mengurutkan lebih dari 4 juta nilai dalam waktu sekitar 35 detik.
Gene S
4
Ini indah dan tidak mungkin dibaca. Saya berasumsi bahwa manfaat Linq akan berarti (setidaknya) kinerja rata-rata dan kasus terbaik, jadi saya pikir saya akan mengikutinya. Meski kurang jelas. Terima kasih banyak @Matthew Horsley
Ian Grainger
1
Ini sangat bagus, tetapi ada bug untuk angka desimal tertentu, contoh saya menyortir k8.11 vs k8.2. Untuk memperbaikinya saya menerapkan regex berikut: \ d + ([\.,] \ D)?
devzero
2
Anda juga perlu memperhitungkan panjang grup kedua (titik desimal + desimal) ketika Anda memasukkan kode ini m.Value.PadLeft (maks, '0')
devzero
3
Saya pikir Anda dapat menggunakan .DefaultIfEmpty().Max()alih-alih melakukan casting int?. Juga patut dilakukan source.ToList()untuk menghindari penghitungan ulang enumerable.
Teejay
30

Tak satu pun dari implementasi yang ada tampak hebat jadi saya menulis sendiri. Hasilnya hampir identik dengan pengurutan yang digunakan oleh versi modern Windows Explorer (Windows 7/8). Satu-satunya perbedaan yang pernah saya lihat adalah 1) walaupun Windows dulu (misalnya XP) menangani angka berapa pun, sekarang dibatasi hingga 19 digit - milik saya tidak terbatas, 2) Windows memberikan hasil yang tidak konsisten dengan set angka Unicode tertentu - karya tambang baik (walaupun secara numerik tidak membandingkan angka dari pasangan pengganti; Windows juga tidak), dan 3) milik saya tidak dapat membedakan berbagai jenis bobot sortir non-primer jika terjadi dalam bagian yang berbeda (mis. "e-1é" vs " é1e- "- bagian sebelum dan sesudah nomor memiliki perbedaan berat diakritik dan tanda baca).

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

Tanda tangan cocok dengan Comparison<string>delegasi:

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

Ini kelas wrapper untuk digunakan sebagai 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);
    }
}

Contoh:

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

Berikut seperangkat nama file yang saya gunakan untuk pengujian:

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();
JD
sumber
Bagian digit harus dibandingkan dengan bagian-bijaksana, yaitu, 'abc12b' harus kurang dari 'abc123'.
SOUser
Anda dapat mencoba data berikut: public string [] nama file = {"-abc12.txt", " abc12.txt", "1abc_2.txt", "a0000012.txt", "a0000012c.txt", "a000012.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 Terima kasih atas kasus uji yang bagus. Jika Anda membiarkan Windows Explorer mengurutkan file-file itu, Anda akan mendapatkan hasil yang berbeda tergantung pada versi Windows yang Anda gunakan. Kode saya mengurutkan nama-nama itu secara identik ke Server 2003 (dan mungkin XP), tetapi berbeda dari Windows 8. Jika saya mendapatkan kesempatan, saya akan mencoba mencari tahu bagaimana Windows 8 melakukannya dan memperbarui kode saya.
JD
3
Memiliki bug. Indeks Di Luar Rentang
linquize
3
Solusi bagus! Ketika saya membandingkannya dalam skenario normal dengan sekitar 10.000 file, itu lebih cepat daripada contoh regex Matthew, dan tentang kinerja yang sama dengan StrCmpLogicalW (). Ada bug kecil dalam kode di atas: the "while (strA [jA] == zeroA) jA ++;" dan "while (strB [jB] == zeroB) jB ++;" seharusnya "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" dan "while (jB <strB.Length && strB [jB] == zeroB) jB ++;". Jika tidak, string yang hanya berisi nol akan mengeluarkan pengecualian.
kuroki
22

Solusi C # murni untuk 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;
    }
}
James McCormack
sumber
2
Kode itu pada akhirnya berasal dari codeproject.com/KB/recipes/NaturalComparer.aspx (yang tidak berorientasi pada LINQ).
mhenry1384
2
Posting blog memuji Justin Jones ( codeproject.com/KB/string/NaturalSortComparer.aspx ) untuk IComparer, bukan Pascal Ganaye.
James McCormack
1
Catatan kecil, solusi ini mengabaikan spasi yang tidak sama dengan apa yang dilakukan windows, dan tidak sebagus kode Matthew Horsley di bawah ini. Jadi Anda mungkin mendapatkan 'string01' 'string 01' 'string 02' 'string02' misalnya (yang terlihat jelek). Jika Anda menghapus stripping spasi, itu memerintahkan string ke belakang yaitu 'string01' sebelum 'string 01', yang mungkin atau mungkin tidak dapat diterima.
Michael Parker
Ini berfungsi untuk alamat, yaitu "1 Smith Rd", "10 Smith Rd", "2 Smith Rd", dll - Diurutkan secara alami. Iya! Yang bagus!
Piotr Kula
Ngomong-ngomong, saya perhatikan (dan komentar pada halaman tertaut itu juga menunjukkan) bahwa argumen Type <T> sama sekali tidak diperlukan.
jv-dev
18

Jawaban Matthews Horsley adalah metode tercepat yang tidak mengubah perilaku tergantung pada versi windows mana program Anda berjalan. Namun, itu bisa lebih cepat dengan membuat regex sekali, dan menggunakan RegexOptions.Compiled. Saya juga menambahkan opsi untuk memasukkan pembanding string sehingga Anda dapat mengabaikan case jika diperlukan, dan sedikit meningkatkan keterbacaan.

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

Digunakan oleh

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

Ini membutuhkan 450ms untuk mengurutkan 100.000 string dibandingkan 300ms untuk perbandingan .net string default - cukup cepat!

Michael Parker
sumber
2
Ini patut dibaca dengan tulisan di atas - Kompilasi dan Penggunaan Kembali dalam Ekspresi Reguler
mungflesh
16

Solusi saya:

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

Hasil:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2
Mpen
sumber
Saya suka itu. Mudah dimengerti dan tidak membutuhkan Linq.
11

Anda perlu berhati-hati - saya samar-samar ingat pernah membaca bahwa StrCmpLogicalW, atau sesuatu seperti itu, tidak sepenuhnya transitif, dan saya telah mengamati. Metode semacam NET untuk kadang-kadang terjebak dalam loop tak terbatas jika fungsi perbandingan melanggar aturan itu.

Perbandingan transitif akan selalu melaporkan bahwa <c jika a <b dan b <c. Ada fungsi yang melakukan perbandingan urutan jenis alami yang tidak selalu memenuhi kriteria itu, tapi saya tidak ingat apakah itu StrCmpLogicalW atau yang lainnya.

Jonathan Gilbert
sumber
Apakah Anda punya bukti pernyataan ini? Setelah mencari-cari di sekitar, saya tidak dapat menemukan indikasi bahwa itu benar.
mhenry1384
1
Saya sudah mengalami loop tak terhingga dengan StrCmpLogicalW.
thd
Item umpan balik Visual Studio 236900 tidak ada lagi, tapi di sini ada yang lebih up-to-date yang mengkonfirmasi masalah: connect.microsoft.com/VisualStudio/feedback/details/774540/… Ini juga memberi solusi: CultureInfomemiliki properti CompareInfo, dan objek yang dikembalikannya dapat memberi Anda SortKeyobjek. Ini, pada gilirannya, dapat dibandingkan dan menjamin transitivitas.
Jonathan Gilbert
9

Ini adalah kode saya untuk mengurutkan string yang memiliki karakter alfa dan numerik.

Pertama, metode ekstensi ini:

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

Kemudian, cukup gunakan di mana saja dalam kode Anda seperti ini:

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

Bagaimana cara kerjanya ? Dengan mengganti dengan nol:

  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"

Bekerja dengan nomor kelipatan:

 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"

Semoga itu bisa membantu.

Picsonald
sumber
6

Menambahkan ke jawaban Greg Beech (karena saya baru saja mencari itu), jika Anda ingin menggunakan ini dari Linq Anda dapat menggunakan OrderByyang membutuhkan IComparer. Misalnya:

var items = new List<MyItem>();

// fill items

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

Berikut adalah contoh yang relatif sederhana yang tidak menggunakan P / Invoke dan menghindari alokasi apa pun selama eksekusi.

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

Itu tidak mengabaikan nol terkemuka, jadi 01datang setelah 2.

Tes unit terkait:

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));
    }
}
Drew Noakes
sumber
2

Saya sebenarnya telah mengimplementasikannya sebagai metode ekstensi StringCompareragar Anda dapat melakukannya misalnya:

  • StringComparer.CurrentCulture.WithNaturalSort() atau
  • StringComparer.OrdinalIgnoreCase.WithNaturalSort().

Dihasilkan tersebut IComparer<string>dapat digunakan di semua tempat seperti OrderBy, OrderByDescending, ThenBy, ThenByDescending, SortedSet<string>, dll Dan Anda dapat sensitivitas kasus masih mudah Tweak, budaya, dll

Implementasinya cukup sepele dan harus berkinerja cukup baik bahkan pada urutan besar.


Saya juga menerbitkannya sebagai paket NuGet kecil , jadi Anda bisa melakukannya:

Install-Package NaturalSort.Extension

Kode termasuk komentar dokumentasi XML dan paket tes tersedia di repositori NaturalSort.Extension GitHub .


Seluruh kode adalah ini (jika Anda belum dapat menggunakan C # 7, cukup instal paket 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;
        }
    }
}
Tom Pažourek
sumber
2

Berikut ini adalah cara LINQ satu garis regex-less naif (dipinjam dari 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"]
mshsayem
sumber
Dihapus Dump () dan ditugaskan untuk var dan ini berfungsi seperti pesona!
Arne S
@ Andre: Itu ditulis dalam LinQPad; dan saya lupa menghapus Dump(). Terima kasih telah menunjukkan.
mshsayem
1

Memperluas pada beberapa jawaban sebelumnya dan menggunakan metode ekstensi, saya datang dengan yang berikut ini tidak memiliki peringatan potensi enumerasi enumerable ganda, atau masalah kinerja yang berkaitan dengan menggunakan beberapa objek regex, atau memanggil regex tidak perlu, yang dikatakan, ia menggunakan ToList (), yang dapat meniadakan manfaat dalam koleksi yang lebih besar.

Selector mendukung pengetikan generik untuk memungkinkan delegasi ditugaskan, elemen-elemen dalam kumpulan sumber dimutasi oleh pemilih, kemudian dikonversi ke string dengan 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);
    }
Vorspire
sumber
1

Terinspirasi oleh solusi Michael Parker, berikut ini adalah IComparerimplementasi yang bisa Anda gunakan di salah satu metode pemesanan LINQ:

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);
    }
}
Oliver
sumber
0

Kami memiliki kebutuhan untuk jenis alami untuk berurusan dengan teks dengan pola berikut:

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

Untuk beberapa alasan ketika saya pertama kali melihat SO, saya tidak menemukan posting ini dan mengimplementasikan posting kami sendiri. Dibandingkan dengan beberapa solusi yang disajikan di sini, sementara dalam konsep yang serupa, itu bisa memiliki manfaat yang lebih sederhana dan lebih mudah dipahami. Namun, sementara saya mencoba untuk melihat hambatan kinerja, ini masih jauh lebih lambat daripada implementasi standarOrderBy() .

Berikut adalah metode ekstensi yang saya terapkan:

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

Idenya adalah untuk membagi string asli menjadi blok digit dan non-digit ("\d+|\D+" ). Karena ini adalah tugas yang berpotensi mahal, itu dilakukan hanya sekali per entri. Kami kemudian menggunakan pembanding objek yang sebanding (maaf, saya tidak dapat menemukan cara yang lebih tepat untuk mengatakannya). Ini membandingkan setiap blok dengan blok yang sesuai di string lain.

Saya ingin umpan balik tentang bagaimana ini bisa diperbaiki dan apa kelemahan utamanya. Perhatikan bahwa pemeliharaan adalah penting bagi kami pada saat ini dan kami saat ini tidak menggunakannya dalam kumpulan data yang sangat besar.

Eric Liprandi
sumber
1
Ini macet ketika mencoba membandingkan string yang secara struktural berbeda - misalnya membandingkan "a-1" dengan "a-2" berfungsi dengan baik, tetapi membandingkan "a" dengan "1" tidak, karena "a" .CompareTo (1) melempar pengecualian.
jimrandomh
@ jimrandomh, Anda benar. Pendekatan ini khusus untuk pola kami.
Eric Liprandi
0

Versi yang lebih mudah dibaca / dipelihara.

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;
    }
}
Kelly Elton
sumber
-2

Biarkan saya menjelaskan masalah saya dan bagaimana saya bisa menyelesaikannya.

Masalah: - Mengurutkan file berdasarkan FileName dari objek FileInfo yang diambil dari Direktori.

Solusi: - Saya memilih nama file dari FileInfo dan memotong bagian ".png" dari nama file. Sekarang, lakukan saja List.Sort (), yang mengurutkan nama file dalam urutan penyortiran alami. Berdasarkan pengujian saya, saya menemukan bahwa .png mengacaukan urutan penyortiran. Lihatlah kode di bawah ini

var imageNameList = new DirectoryInfo(@"C:\Temp\Images").GetFiles("*.png").Select(x =>x.Name.Substring(0, x.Name.Length - 4)).ToList();
imageNameList.Sort();
girishkatta9
sumber
Bisakah saya tahu alasan -1 pada jawaban ini?
girishkatta9