Natürliche Sortierreihenfolge in C #

129

Hat jemand eine gute Ressource oder stellt ein Beispiel einer Sortierung natürlicher Reihenfolge in C # für ein FileInfoArray bereit ? Ich implementiere die IComparerSchnittstelle in meiner Art.

Michael Kniskern
quelle

Antworten:

148

Am einfachsten ist es, die in Windows integrierte Funktion aufzurufen und als Vergleichsfunktion in Ihrem IComparer:

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

Michael Kaplan hat einige Beispiele dafür, wie diese Funktion hier funktioniert , und die Änderungen, die für Vista vorgenommen wurden, damit sie intuitiver funktioniert. Die positive Seite dieser Funktion ist, dass sie sich genauso verhält wie die Windows-Version, auf der sie ausgeführt wird. Dies bedeutet jedoch, dass sie sich zwischen den Windows-Versionen unterscheidet. Sie müssen also prüfen, ob dies ein Problem für Sie ist.

Eine vollständige Implementierung wäre also ungefähr so:

[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
quelle
8
Gute Antwort. Vorsichtsmaßnahme: Dies funktioniert nicht mit Win2000, da diese wenigen Leute immer noch Dinge auf diesem Betriebssystem ausführen. Andererseits gibt es zwischen Kaplans Blog und der MSDN-Dokumentation genügend Hinweise, um eine ähnliche Funktion zu erstellen.
Chris Charabaruk
9
Dies ist nicht portabel, funktioniert nur unter Win32, aber nicht unter Linux / MacOS / Silverlight / Windows Phone / Metro
linquize
20
@linquize - Er sagte, .NET nicht Mono, also ist Linux / OSX nicht wirklich ein Problem. Windows Phone / Metro gab es 2008 nicht, als diese Antwort veröffentlicht wurde. Und wie oft führen Sie Dateioperationen in Silverlight durch? Für das OP und wahrscheinlich die meisten anderen Menschen war dies eine geeignete Antwort. In jedem Fall können Sie eine bessere Antwort geben. So funktioniert diese Seite.
Greg Beech
6
Dies bedeutet nicht, dass die ursprüngliche Antwort falsch war. Ich füge nur zusätzliche Informationen mit aktuellen Informationen hinzu
Linquize
2
Zu Ihrer Information: Wenn Sie von erben Comparer<T>anstatt zu implementieren IComparer<T>, erhalten Sie eine integrierte Implementierung der IComparer(nicht generischen) Schnittstelle, die Ihre generische Methode aufruft, zur Verwendung in APIs, die diese stattdessen verwenden. Grundsätzlich ist es auch kostenlos: Löschen Sie einfach das "Ich" und wechseln Sie public int Compare(...)zu public override int Compare(...). Gleiches gilt für IEqualityComparer<T>und EqualityComparer<T>.
Joe Amenta
75

Ich dachte nur, ich würde das ergänzen (mit der prägnantesten Lösung, die ich finden konnte):

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

Mit dem obigen Befehl werden alle Zahlen in der Zeichenfolge auf die maximale Länge aller Zahlen in allen Zeichenfolgen aufgefüllt und die resultierende Zeichenfolge zum Sortieren verwendet.

Die Umwandlung in ( int?) soll Sammlungen von Zeichenfolgen ohne Zahlen ermöglichen ( .Max()bei einer leeren Aufzählung wirft an InvalidOperationException).

Matthew Horsley
quelle
1
+1 Es ist nicht nur das prägnanteste, sondern auch das schnellste, das ich je gesehen habe. mit Ausnahme der akzeptierten Antwort, aber ich kann diese aufgrund von Maschinenabhängigkeiten nicht verwenden. Es sortierte über 4 Millionen Werte in ungefähr 35 Sekunden.
Gene S
4
Dies ist sowohl schön als auch unmöglich zu lesen. Ich gehe davon aus, dass die Vorteile von Linq (zumindest) die beste durchschnittliche und beste Leistung bedeuten werden, also denke ich, dass ich damit weitermachen werde. Trotz mangelnder Klarheit. Vielen Dank @Matthew Horsley
Ian Grainger
1
Das ist sehr gut, aber es gibt einen Fehler für bestimmte Dezimalzahlen. Mein Beispiel war das Sortieren von k8.11 gegen k8.2. Um dies zu beheben, habe ich den folgenden regulären Ausdruck implementiert: \ d + ([\.,] \ D)?
Devzero
2
Sie müssen auch die Länge der zweiten Gruppe (Dezimalpunkt + Dezimalstellen) berücksichtigen, wenn Sie diesen Code m.Value.PadLeft (max, '0')
devzero
3
Ich denke, Sie können verwenden, .DefaultIfEmpty().Max()anstatt zu gießen int?. Es lohnt sich auch, a durchzuführen source.ToList(), um eine erneute Aufzählung der Aufzählungszeichen zu vermeiden.
Teejay
30

Keine der vorhandenen Implementierungen sah gut aus, also habe ich meine eigene geschrieben. Die Ergebnisse sind fast identisch mit der Sortierung, die von modernen Versionen von Windows Explorer (Windows 7/8) verwendet wird. Die einzigen Unterschiede, die ich gesehen habe, sind: 1) Obwohl Windows (z. B. XP) Zahlen beliebiger Länge verarbeitet hat, ist es jetzt auf 19 Stellen begrenzt - meine sind unbegrenzt. 2) Windows liefert inkonsistente Ergebnisse mit bestimmten Unicode-Stellen - meine funktioniert gut (obwohl es keine Ziffern von Ersatzpaaren numerisch vergleicht; Windows auch nicht) und 3) meine können verschiedene Arten von nicht primären Sortiergewichten nicht unterscheiden, wenn sie in verschiedenen Abschnitten vorkommen (z. B. "e-1é" vs " é1e- "- Die Abschnitte vor und nach der Zahl weisen diakritische und Interpunktionsgewichtsunterschiede auf.

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

Die Signatur entspricht dem Comparison<string>Delegierten:

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

Hier ist eine Wrapper-Klasse zur Verwendung als 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);
    }
}

Beispiel:

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

Hier ist ein guter Satz von Dateinamen, die ich zum Testen verwende:

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
quelle
Die Ziffernabschnitte müssen abschnittsweise verglichen werden, dh 'abc12b' sollte kleiner als 'abc123' sein.
SOUser
Sie können die folgenden Daten ausprobieren: public string [] filenames = {"-abc12.txt", " abc12.txt", "1abc_2.txt", "a0000012.txt", "a0000012c.txt", "a000012.txt" , "a000012b.txt", "a012.txt", "a0000102.txt", "abc1_2.txt", "abc12 .txt", "abc12b.txt", "abc123.txt", "abccde.txt", " b0000.txt "," b00001.txt "," b0001.txt "," b001.txt "," c0000.txt "," c0000c.txt "," c00001.txt "," c000b.txt "," d0 ". 20.2b.txt "," d0.1000c.txt "," d0.2000y.txt "," d0.20000.2b.txt ","
SOUser
@XichenLi Danke für den guten Testfall. Wenn Sie Windows Explorer diese Dateien sortieren lassen, erhalten Sie je nach verwendeter Windows-Version unterschiedliche Ergebnisse. Mein Code sortiert diese Namen identisch mit Server 2003 (und vermutlich XP), unterscheidet sich jedoch von Windows 8. Wenn ich die Möglichkeit dazu bekomme, werde ich versuchen, herauszufinden, wie Windows 8 dies tut, und meinen Code aktualisieren.
JD
2
Hat Fehler. Index außerhalb des
Bereichs
3
Tolle Lösung! Als ich es in einem normalen Szenario mit ungefähr 10.000 Dateien verglichen habe, war es schneller als Matthews Regex-Beispiel und ungefähr so ​​leistungsfähig wie StrCmpLogicalW (). Der obige Code enthält einen kleinen Fehler: "while (strA [jA] == zeroA) jA ++;" und "while (strB [jB] == zeroB) jB ++;" sollte "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" sein und "while (jB <strB.Length && strB [jB] == zeroB) jB ++;". Andernfalls lösen Zeichenfolgen, die nur Nullen enthalten, eine Ausnahme aus.
Kuroki
22

Reine C # -Lösung für 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
quelle
2
Dieser Code stammt letztendlich von codeproject.com/KB/recipes/NaturalComparer.aspx (der nicht LINQ-orientiert ist).
Mhenry1384
2
Der Blog-Beitrag schreibt Justin Jones ( codeproject.com/KB/string/NaturalSortComparer.aspx ) den IComparer zu, nicht Pascal Ganaye.
James McCormack
1
Kleinere Anmerkung, diese Lösung ignoriert Leerzeichen, die nicht mit denen von Windows identisch sind und nicht so gut sind wie der folgende Code von Matthew Horsley. So könnten Sie zum Beispiel 'string01' 'string 01' 'string 02' 'string02' erhalten (was hässlich aussieht). Wenn Sie das Entfernen von Leerzeichen entfernen, werden die Zeichenfolgen rückwärts angeordnet, dh "Zeichenfolge01" steht vor "Zeichenfolge 01", was möglicherweise akzeptabel ist oder nicht.
Michael Parker
Dies funktionierte für Adressen, dh "1 Smith Rd", "10 Smith Rd", "2 Smith Rd" usw. - Natürlich sortiert. Ja! Schön!
Piotr Kula
Übrigens habe ich festgestellt (und Kommentare auf dieser verlinkten Seite scheinen auch darauf hinzudeuten), dass das Type-Argument <T> völlig unnötig ist.
JV-Dev
18

Matthews Horsleys Antwort ist die schnellste Methode, die das Verhalten nicht ändert, je nachdem, auf welcher Windows-Version Ihr Programm ausgeführt wird. Es kann jedoch noch schneller sein, indem der reguläre Ausdruck einmal erstellt und RegexOptions.Compiled verwendet wird. Ich habe auch die Option hinzugefügt, einen Zeichenfolgenvergleicher einzufügen, damit Sie bei Bedarf die Groß- und Kleinschreibung ignorieren können, und die Lesbarkeit ein wenig verbessert.

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

Verwendung durch

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

Das Sortieren von 100.000 Zeichenfolgen dauert 450 ms, verglichen mit 300 ms für den standardmäßigen .net-Zeichenfolgenvergleich - ziemlich schnell!

Michael Parker
quelle
2
Dies ist eine Lektüre wert, siehe oben - Zusammenstellung und Wiederverwendung in regulären Ausdrücken
Mungflesh
16

Meine Lösung:

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

Ergebnisse:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2
mpen
quelle
Ich mag das. Es ist leicht zu verstehen und erfordert kein Linq.
11

Sie müssen vorsichtig sein - ich erinnere mich vage daran, dass StrCmpLogicalW oder ähnliches nicht streng transitiv war, und ich habe die Sortiermethoden von .NET beobachtet, die manchmal in Endlosschleifen stecken bleiben, wenn die Vergleichsfunktion gegen diese Regel verstößt.

Ein transitiver Vergleich zeigt immer, dass a <c ist, wenn a <b und b <c. Es gibt eine Funktion, die einen natürlichen Vergleich der Sortierreihenfolge durchführt, der dieses Kriterium nicht immer erfüllt, aber ich kann mich nicht erinnern, ob es sich um StrCmpLogicalW oder etwas anderes handelt.

Jonathan Gilbert
quelle
Haben Sie einen Beweis für diese Aussage? Nach dem Googeln kann ich keinen Hinweis darauf finden, dass es wahr ist.
Mhenry1384
1
Ich habe diese Endlosschleifen mit StrCmpLogicalW erlebt.
28.
Das Visual Studio-Feedback-Element 236900 ist nicht mehr vorhanden, aber hier ist ein aktuelleres, das das Problem bestätigt: connect.microsoft.com/VisualStudio/feedback/details/774540/… Es gibt auch eine Problemumgehung : CultureInfohat eine Eigenschaft CompareInfound das zurückgegebene Objekt kann Sie mit SortKeyObjekten versorgen . Diese können wiederum verglichen werden und garantieren die Transitivität.
Jonathan Gilbert
9

Dies ist mein Code zum Sortieren einer Zeichenfolge mit alphanumerischen Zeichen.

Zunächst diese Erweiterungsmethode:

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

Verwenden Sie es dann einfach an einer beliebigen Stelle in Ihrem Code wie folgt:

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

Wie funktioniert es ? Durch Ersetzen durch Nullen:

  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"

Funktioniert mit mehreren Zahlen:

 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"

Hoffe das wird helfen.

Picsonald
quelle
6

Zusätzlich zu Greg Beech Antwort (weil ich gerade habe für die Suche), wenn Sie diese von Linq verwenden möchten , können Sie das verwenden , OrderBydie eine nimmt IComparer. Z.B:

var items = new List<MyItem>();

// fill items

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

Hier ist ein relativ einfaches Beispiel, das P / Invoke nicht verwendet und jegliche Zuordnung während der Ausführung vermeidet.

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

Führende Nullen werden nicht ignoriert, daher 01folgt sie 2.

Entsprechender Unit-Test:

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
quelle
2

Ich habe es tatsächlich als Erweiterungsmethode auf dem implementiert, StringComparerdamit Sie zum Beispiel Folgendes tun können:

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

Das resultierende IComparer<string>kann überall dort eingesetzt werden , wie OrderBy, OrderByDescending, ThenBy, ThenByDescending,SortedSet<string> , etc. Und Sie können immer noch leicht zwicken Groß- und Kleinschreibung, Kultur usw.

Die Implementierung ist ziemlich trivial und sollte auch bei großen Sequenzen recht gut funktionieren.


Ich habe es auch als winziges NuGet-Paket veröffentlicht , sodass Sie einfach Folgendes tun können:

Install-Package NaturalSort.Extension

Der Code mit Kommentaren zur XML-Dokumentation und einer Reihe von Tests ist im GitHub-Repository von NaturalSort.Extension verfügbar .


Der gesamte Code lautet wie folgt (wenn Sie C # 7 noch nicht verwenden können, installieren Sie einfach das NuGet-Paket):

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
quelle
2

Hier ist ein naiver einzeiliger LINQ-Weg ohne Regex (aus Python entlehnt):

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
quelle
Dump () entfernt und var zugewiesen und das funktioniert wie ein Zauber!
Arne S
@ArneS: Es wurde in LinQPad geschrieben; und ich habe vergessen das zu entfernen Dump(). Vielen Dank für den Hinweis.
Mshsayem
1

Ich habe einige der vorherigen Antworten erweitert und Erweiterungsmethoden verwendet. Dabei habe ich Folgendes gefunden, das nicht die Vorbehalte einer potenziellen Aufzählung mehrerer Aufzählungen oder Leistungsprobleme aufweist, die mit der Verwendung mehrerer Regex-Objekte oder dem unnötigen Aufrufen von Regex verbunden sind Allerdings wird ToList () verwendet, wodurch die Vorteile in größeren Sammlungen zunichte gemacht werden können.

Der Selektor unterstützt die generische Typisierung, damit jeder Delegat zugewiesen werden kann. Die Elemente in der Quellensammlung werden vom Selektor mutiert und dann mit ToString () in Zeichenfolgen konvertiert.

    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
quelle
1

Inspiriert von der Lösung von Michael Parker, finden Sie hier eine IComparerImplementierung, die Sie in eine der Linq-Bestellmethoden einbinden können:

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
quelle
0

Wir brauchten eine natürliche Sorte, um mit Text mit folgendem Muster umgehen zu können:

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

Aus irgendeinem Grund habe ich diesen Beitrag nicht gefunden und unseren eigenen implementiert, als ich SO zum ersten Mal angesehen habe. Im Vergleich zu einigen der hier vorgestellten Lösungen ist das Konzept zwar ähnlich, es könnte jedoch den Vorteil haben, dass es möglicherweise einfacher und verständlicher ist. Obwohl ich versucht habe, Leistungsengpässe zu untersuchen, ist die Implementierung immer noch viel langsamer als die StandardimplementierungOrderBy() .

Hier ist die Erweiterungsmethode, die ich implementiere:

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

Die Idee ist, die ursprünglichen Zeichenfolgen in Ziffernblöcke und Nicht-Ziffernblöcke ( "\d+|\D+") aufzuteilen . Da dies eine möglicherweise teure Aufgabe ist, wird sie nur einmal pro Eintrag ausgeführt. Wir verwenden dann einen Vergleich vergleichbarer Objekte (Entschuldigung, ich kann keinen angemesseneren Weg finden, dies auszudrücken). Es vergleicht jeden Block mit seinem entsprechenden Block in der anderen Zeichenfolge.

Ich hätte gerne Feedback, wie dies verbessert werden könnte und was die Hauptmängel sind. Beachten Sie, dass die Wartbarkeit an dieser Stelle für uns wichtig ist und wir sie derzeit nicht in extrem großen Datenmengen verwenden.

Eric Liprandi
quelle
1
Dies stürzt ab, wenn versucht wird, strukturell unterschiedliche Zeichenfolgen zu vergleichen - z. B. funktioniert der Vergleich von "a-1" mit "a-2" einwandfrei, der Vergleich von "a" mit "1" jedoch nicht, da "a" .CompareTo (1) löst eine Ausnahme aus.
Jimrandomh
@ Jimrandomh, du bist richtig. Dieser Ansatz war spezifisch für unsere Muster.
Eric Liprandi
0

Eine Version, die einfacher zu lesen / zu warten ist.

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
quelle
-2

Lassen Sie mich mein Problem erklären und wie ich es lösen konnte.

Problem: - Sortieren Sie Dateien basierend auf FileName aus FileInfo-Objekten, die aus einem Verzeichnis abgerufen werden.

Lösung: - Ich habe die Dateinamen aus FileInfo ausgewählt und den Teil ".png" des Dateinamens gekürzt. Führen Sie jetzt einfach List.Sort () aus, das die Dateinamen in natürlicher Sortierreihenfolge sortiert. Aufgrund meiner Tests stellte ich fest, dass .png die Sortierreihenfolge durcheinander bringt. Schauen Sie sich den folgenden Code an

var imageNameList = new DirectoryInfo(@"C:\Temp\Images").GetFiles("*.png").Select(x =>x.Name.Substring(0, x.Name.Length - 4)).ToList();
imageNameList.Sort();
girishkatta9
quelle
Kann ich den Grund für -1 bei dieser Antwort kennen?
Girishkatta9