C # 'da Doğal Sıralama Düzeni


129

İyi bir kaynağı olan veya bir FileInfodizi için C # 'da doğal bir sıralama örneği sağlayan var mı? IComparerArayüzü kendi türlerime göre uyguluyorum .

Yanıtlar:


149

Yapılması en kolay şey, Windows'taki yerleşik işlevi yalnızca P / Çağırmak ve aşağıdakilerinizde karşılaştırma işlevi olarak kullanmaktır IComparer:

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

Michael Kaplan'da bu işlevin burada nasıl çalıştığına dair bazı örnekler ve Vista'nın daha sezgisel çalışmasını sağlamak için yapılan değişiklikler var. Bu işlevin artı yanı, üzerinde çalıştığı Windows sürümüyle aynı davranışa sahip olmasıdır, ancak bu, Windows sürümleri arasında farklılık gösterdiği anlamına gelir, bu nedenle bunun sizin için bir sorun olup olmadığını düşünmeniz gerekir.

Yani tam bir uygulama şu şekilde olacaktır:

[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
Mükemmel cevap. Uyarı: Bu, hala o işletim sisteminde bir şeyler çalıştıran birkaç kişi için Win2000 ile çalışmayacaktır. Öte yandan, benzer bir işlev oluşturmak için Kaplan'ın blogu ile MSDN belgeleri arasında yeterince ipucu var.
Chris Charabaruk

9
Bu taşınabilir değil, yalnızca Win32'de çalışıyor, ancak Linux / MacOS / Silverlight / Windows Phone / Metro'da
çalışmıyor

20
@linquize - .NET'in Mono olmadığını söyledi, bu yüzden Linux / OSX gerçekten bir sorun değil. Windows Phone / Metro 2008'de bu yanıt gönderildiğinde mevcut değildi. Silverlight'ta ne sıklıkla dosya işlemleri yapıyorsunuz? Öyleyse, OP ve muhtemelen diğer birçok insan için uygun bir cevaptı. Her durumda, daha iyi bir yanıt vermekte özgürsünüz; bu site böyle çalışır.
Greg Beech

6
Bu, orijinal cevabın yanlış olduğu anlamına gelmez. Sadece güncel bilgilerle ek bilgiler ekliyorum
2012'de

2
Bilginize, Comparer<T>uygulamak yerine IComparer<T>ondan devralırsanız, bunun yerine ICompareronu kullanan API'lerde kullanmak için genel yönteminizi çağıran (genel olmayan) arabirimin yerleşik bir uygulamasını elde edersiniz . Temelde çok yapmak Ücretsizdir: sadece "Ben" ve değişikliği silmek public int Compare(...)için public override int Compare(...). Aynı için IEqualityComparer<T>ve EqualityComparer<T>.
Joe Amenta

75

Buna ekleyeceğimi düşündüm (bulabildiğim en kısa çözümle):

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

Yukarıdaki, dizedeki herhangi bir sayıyı tüm dizelerdeki tüm sayıların maksimum uzunluğuna kadar doldurur ve sıralamak için sonuçta elde edilen dizeyi kullanır.

( int?) 'E çevrim, herhangi bir sayı olmadan dizge koleksiyonlarına izin vermektir ( .Max()boş bir numaralandırılabilirde bir atar InvalidOperationException).


1
+1 Sadece en özlü değil, gördüğüm en hızlısı. kabul edilen cevap dışında ama makine bağımlılıkları nedeniyle bunu kullanamıyorum. Yaklaşık 35 saniyede 4 milyondan fazla değeri sıraladı.
Gene S

4
Bu hem güzel hem de okunması imkansız. Linq'in faydalarının (en azından) en iyi ortalama ve en iyi durum performansı anlamına geleceğini varsayıyorum, bu yüzden bununla devam edeceğimi düşünüyorum. Netlik olmamasına rağmen. Çok teşekkürler @Matthew Horsley
Ian Grainger

1
Bu çok iyi, ancak belirli ondalık sayılar için bir hata var, benim örneğim k8.11'e karşı k8.2'yi sıralamaktı. Bunu düzeltmek için şu normal ifadeyi uyguladım: \ d + ([\.,] \ D)?
devzero

2
Bu kodu m.Value.PadLeft (max, '0')
devzero

3
.DefaultIfEmpty().Max()Cast yerine kullanabileceğinizi düşünüyorum int?. Ayrıca source.ToList()numaralandırılabilir olanı yeniden numaralandırmaktan kaçınmak için a yapmaya değer .
Teejay

30

Mevcut uygulamaların hiçbiri harika görünmedi, bu yüzden kendim yazdım. Sonuçlar, Windows Gezgini'nin (Windows 7/8) modern sürümleri tarafından kullanılan sıralamayla neredeyse aynıdır. Gördüğüm tek fark 1) Windows (örneğin, XP) herhangi bir uzunluktaki sayıları işlese de, artık 19 basamakla sınırlıdır - benimki sınırsızdır, 2) Windows belirli Unicode basamak kümeleriyle tutarsız sonuçlar verir - maden işleri iyi (vekil çiftlerdeki rakamları sayısal olarak karşılaştırmasa da; Windows da yapmaz) ve 3) benimki farklı bölümlerde (örneğin "e-1é" vs ") farklı birincil olmayan sıralama ağırlıklarını ayırt edemez é1e- "- numaradan önceki ve sonraki bölümlerde aksan ve noktalama ağırlık farkları vardır).

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

İmza, Comparison<string>temsilciyle eşleşiyor :

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

İşte kullanım için bir sarmalayıcı sınıfı 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);
    }
}

Misal:

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

İşte test etmek için kullandığım iyi bir dosya adı grubu:

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

Rakam bölümleri bölüm bazında karşılaştırılmalıdır, yani "abc12b", "abc123" ten küçük olmalıdır.
SOUser

Şu verileri deneyebilirsiniz: public string [] dosya adları = {"-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 İyi test vakası için teşekkürler. Windows Gezgininin bu dosyaları sıralamasına izin verirseniz, kullandığınız Windows sürümüne bağlı olarak farklı sonuçlar alırsınız. Kodum bu isimleri Server 2003 (ve muhtemelen XP) ile aynı, ancak Windows 8'den farklı olarak sıralıyor. Bir şansım olursa, Windows 8'in bunu nasıl yaptığını anlamaya ve kodumu güncellemeye çalışacağım.
JD

3
Böcek var. Index Out Of Range
linquize

3
Harika çözüm! Yaklaşık 10.000 dosyayla normal bir senaryoda karşılaştırdığımda, Matthew'un normal ifade örneğinden daha hızlıydı ve StrCmpLogicalW () ile yaklaşık aynı performanstı. Yukarıdaki kodda küçük bir hata var: "while (strA [jA] == sıfırA) jA ++;" ve "while (strB [jB] == sıfırB) jB ++;" "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" olmalıdır ve "while (jB <strB.Length && strB [jB] == sıfırB) jB ++;". Aksi takdirde, yalnızca sıfır içeren dizeler bir istisna atar.
kuroki

22

Linq siparişi için saf C # çözümü:

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
Bu kod nihayetinde codeproject.com/KB/recipes/NaturalComparer.aspx'den (LINQ odaklı değildir).
mhenry1384

2
Blog gönderisi , Pascal Ganaye'ye değil, IComparer'a Justin Jones'a ( codeproject.com/KB/string/NaturalSortComparer.aspx ) atıfta bulunuyor.
James McCormack

1
Küçük bir not, bu çözüm pencerelerin yaptığı ile aynı olmayan ve Matthew Horsley'in aşağıdaki kodu kadar iyi olmayan boşlukları yok sayar. Yani örneğin 'string01' 'string 01' 'string 02' 'string02' elde edebilirsiniz (çirkin görünüyor). Boşlukların çıkarılmasını kaldırırsanız, dizeleri geriye doğru sıralar, yani 'dize01' kabul edilebilir veya olmayabilir 'dizge 01'den önce gelir.
Michael Parker

Bu, adresler için işe yaradı, örneğin "1 Smith Rd", "10 Smith Rd", "2 Smith Rd", vb. - Doğal olarak sıralanır. Evet! Güzel bir!
Piotr Kula

Bu arada, <T> Tipi bağımsız değişkeninin tamamen gereksiz olduğunu fark ettim (ve bu bağlantılı sayfadaki yorumlar da gösteriyor gibi görünüyor).
jv-dev

18

Matthews Horsleys cevabı, programınızın hangi Windows sürümünde çalıştığına bağlı olarak davranışı değiştirmeyen en hızlı yöntemdir. Ancak, normal ifadeyi bir kez oluşturarak ve RegexOptions.Compiled kullanarak daha da hızlı olabilir. Ayrıca, gerekirse büyük / küçük harf durumunu görmezden gelebilmeniz ve okunabilirliği biraz artırabilmeniz için bir dizi karşılaştırıcı ekleme seçeneği de ekledim.

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

Tarafından kullanım

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

Bu, varsayılan .net dizge karşılaştırması için 300 ms'ye kıyasla 100.000 dizeyi sıralamak için 450 ms sürer - oldukça hızlı!


2
Bu, yukarıdakilere göre okumaya değer - Düzenli İfadelerde Derleme ve Yeniden Kullanım
mungflesh

16

Çözümüm:

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

Sonuçlar:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2

Bunu sevdim. Anlaması kolaydır ve Linq gerektirmez.

11

Dikkatli olmalısınız - StrCmpLogicalW veya buna benzer bir şeyin tam anlamıyla geçişli olmadığını okuduğumu belli belirsiz hatırlıyorum ve .NET'in sıralama yöntemlerini, karşılaştırma işlevi bu kuralı ihlal ederse bazen sonsuz döngülerde sıkışıp kalmak için gözlemledim.

Geçişli bir karşılaştırma her zaman a <c eğer a <b ve b <c olduğunu bildirir. Bu kriteri her zaman karşılamayan doğal bir sıralama düzeni karşılaştırması yapan bir işlev var, ancak bunun StrCmpLogicalW veya başka bir şey olup olmadığını hatırlayamıyorum.


Bu ifadeye dair herhangi bir kanıtınız var mı? Google'da dolaştıktan sonra, bunun doğru olduğuna dair herhangi bir gösterge bulamıyorum.
mhenry1384

1
StrCmpLogicalW ile bu sonsuz döngüleri deneyimledim.
THD


Visual Studio geri bildirim öğesi 236900 artık mevcut değil, ancak sorunu doğrulayan daha güncel bir öğe burada: connect.microsoft.com/VisualStudio/feedback/details/774540/… Ayrıca bir çözüm sağlar: CultureInfobir özelliğe sahiptir CompareInfove döndürdüğü nesne size SortKeynesneler sağlayabilir . Bunlar da karşılaştırılabilir ve geçişliliği garanti eder.
Jonathan Gilbert

9

Bu, hem alfa hem de sayısal karakterlere sahip bir dizeyi sıralamak için kodumdur.

İlk olarak, bu uzatma yöntemi:

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

Ardından, kodunuzun herhangi bir yerinde şu şekilde kullanın:

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

Nasıl çalışır? Sıfırlarla değiştirerek:

  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"

Çoklu sayılarla çalışır:

 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"

Umarım bu yardımcı olur.


6

Ekleme Greg kayın cevabı (sadece o arıyordum çünkü) Eğer kullanabilirsiniz Linq bu kullanmak istiyorsanız, OrderBybir alır IComparer. Örneğin:

var items = new List<MyItem>();

// fill items

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

2

İşte P / Invoke kullanmayan ve yürütme sırasında herhangi bir tahsisattan kaçınan nispeten basit bir örnek.

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

Baştaki sıfırları görmezden gelmez, bu yüzden 01sonra gelir2 .

İlgili birim testi:

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

Aslında bunu bir uzantı yöntemi olarak uyguladım, StringComparerböylece örneğin şunları yapabilirsiniz:

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

Oluşan IComparer<string>her yerde ister kullanılabilir OrderBy, OrderByDescending, ThenBy, ThenByDescending, SortedSet<string>, vb Ve yapabilirsiniz hala kolayca çimdik harf duyarlılığı, kültür, vb

Uygulama oldukça önemsizdir ve büyük dizilerde bile oldukça iyi performans göstermelidir.


Ayrıca bunu küçük bir NuGet paketi olarak yayınladım , böylece şunları yapabilirsiniz:

Install-Package NaturalSort.Extension

XML belgeleri yorumlar ve dahil kod testlerinin paketi mevcuttur NaturalSort.Extension GitHub depo .


Kodun tamamı şudur (henüz C # 7 kullanamıyorsanız, sadece NuGet paketini yükleyin):

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

İşte saf bir tek satırlık regex içermeyen LINQ yolu (python'dan ödünç alınmıştır):

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 () kaldırıldı ve var'a atandı ve bu bir cazibe gibi çalışıyor!
Arne S

@ArneS: LinQPad ile yazılmıştır; ve Dump(). Gösterdiğiniz için teşekkürler.
mshsayem

1

Önceki yanıtların birkaçını genişleterek ve genişletme yöntemlerini kullanarak, aşağıdakileri buldum, potansiyel çoklu numaralandırılabilir numaralandırma veya birden fazla regex nesnesi kullanma veya regex'i gereksiz yere çağırma ile ilgili performans sorunları ile ilgili uyarılara sahip değil, Söylendiği gibi, ToList () kullanıyor ve bu da daha büyük koleksiyonlardaki faydaları etkisiz hale getiriyor.

Seçici, herhangi bir temsilcinin atanmasına izin vermek için genel yazmayı destekler, kaynak koleksiyonundaki öğeler seçici tarafından değiştirilir, ardından ToString () ile dizelere dönüştürülür.

    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'ın çözümünden esinlenerek, burada IComparerherhangi bir linq sipariş yöntemine dahil edebileceğiniz bir uygulama verilmiştir:

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

Aşağıdaki düzende metinle başa çıkmak için doğal bir sıralamaya ihtiyacımız vardı:

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

Nedense SO'ya ilk baktığımda, bu yazıyı bulamadım ve kendiminkini uyguladım. Burada sunulan bazı çözümlerle karşılaştırıldığında, konsept olarak benzer olsa da, belki daha basit ve anlaşılması daha kolay olma avantajına sahip olabilir. Bununla birlikte, performans darboğazlarına bakmaya çalışsam da, hala varsayılandan çok daha yavaş bir uygulamaOrderBy() .

İşte uyguladığım uzantı yöntemi:

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

Buradaki fikir, orijinal dizeleri rakam ve rakam olmayan bloklara bölmektir ("\d+|\D+" ) . Bu potansiyel olarak pahalı bir görev olduğundan, giriş başına yalnızca bir kez yapılır. Daha sonra karşılaştırılabilir nesnelerin bir karşılaştırmasını kullanırız (üzgünüm, bunu söylemenin daha uygun bir yolunu bulamıyorum). Her bloğu diğer dizedeki karşılık gelen bloğuyla karşılaştırır.

Bunun nasıl geliştirilebileceği ve ana kusurların neler olduğu konusunda geri bildirim almak istiyorum. Sürdürülebilirliğin bu noktada bizim için önemli olduğunu ve şu anda bunu çok büyük veri kümelerinde kullanmadığımızı unutmayın.


1
Bu, yapısal olarak farklı dizeleri karşılaştırmaya çalıştığında çöker - ör. "A-1" ile "a-2" yi karşılaştırmak iyi sonuç verir, ancak "a" ile "1" karşılaştırması işe yaramaz, çünkü "a" .CompareTo (1) bir istisna atar.
jimrandomh

@jimrandomh, haklısın. Bu yaklaşım, kalıplarımıza özeldi.
Eric Liprandi

0

Okuması / bakımı daha kolay bir sürüm.

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

Sorunumu ve nasıl çözebildiğimi açıklamama izin verin.

Sorun: - Dosyaları, Dizinden alınan FileInfo nesnelerinden FileName temelinde sıralayın.

Çözüm: - FileInfo'dan dosya adlarını seçtim ve dosya adının ".png" kısmını değiştirdim. Şimdi, dosya adlarını Doğal sıralama düzeninde sıralayan List.Sort () işlemini yapın. Testlerime dayanarak .png'ye sahip olmanın sıralama düzenini bozduğunu buldum. Aşağıdaki koda bir göz atın

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

Bu cevapta -1'in nedenini bilebilir miyim?
girishkatta9
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.