Ordinamento naturale in C #


129

Qualcuno ha una buona risorsa o fornisce un campione di un ordinamento naturale in C # per un FileInfoarray? Sto implementando l' IComparerinterfaccia nel mio genere.

Risposte:


148

La cosa più semplice da fare è semplicemente P / Richiamare la funzione integrata in Windows e usarla come funzione di confronto nel tuo IComparer:

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

Michael Kaplan ha alcuni esempi di come questa funzione funziona qui e le modifiche apportate a Vista per farlo funzionare in modo più intuitivo. Il lato positivo di questa funzione è che avrà lo stesso comportamento della versione di Windows su cui viene eseguita, tuttavia ciò significa che differisce tra le versioni di Windows, quindi è necessario considerare se questo è un problema per te.

Quindi un'implementazione completa sarebbe qualcosa del tipo:

[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
Bella risposta. Avvertenza: questo non funzionerà con Win2000, per quelle poche persone che eseguono ancora cose su quel sistema operativo. D'altra parte, ci sono abbastanza suggerimenti tra il blog di Kaplan e la documentazione MSDN per creare una funzione simile.
Chris Charabaruk,

9
Questo non è portatile, funziona solo in Win32, ma non funziona in Linux / MacOS / Silverlight / Windows Phone / Metro
linquize,

20
@linquize - Ha detto che .NET non è Mono, quindi Linux / OSX non è un problema. Windows Phone / Metro non esisteva nel 2008 quando questa risposta è stata pubblicata. E con quale frequenza esegui le operazioni sui file in Silverlight? Quindi per l'OP, e probabilmente per la maggior parte delle altre persone, è stata una risposta adeguata. In ogni caso, sei libero di fornire una risposta migliore; è così che funziona questo sito.
Greg Beech,

6
Ciò non significa che la risposta originale fosse sbagliata. Aggiungo solo informazioni aggiuntive con informazioni aggiornate
linquize

2
Cordiali saluti, se si eredita Comparer<T>invece di implementare IComparer<T>, si ottiene un'implementazione integrata dell'interfaccia IComparer(non generica) che chiama il metodo generico, da utilizzare nelle API che lo utilizzano invece. In pratica è anche gratuito: basta eliminare l'io e passare public int Compare(...)a public override int Compare(...). Lo stesso per IEqualityComparer<T>e EqualityComparer<T>.
Joe Amenta,

75

Ho pensato di aggiungere a questo (con la soluzione più concisa che ho trovato):

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

Quanto sopra collega tutti i numeri nella stringa alla lunghezza massima di tutti i numeri in tutte le stringhe e utilizza la stringa risultante per ordinare.

Il cast su ( int?) è per consentire raccolte di stringhe senza numeri ( .Max()su un enumerabile vuoto genera un InvalidOperationException).


1
+1 Non solo è il più conciso, è il più veloce che abbia mai visto. fatta eccezione per la risposta accettata ma non posso usarla a causa delle dipendenze della macchina. Ha ordinato oltre 4 milioni di valori in circa 35 secondi.
Gene S,

4
Questo è sia bello che impossibile da leggere. Suppongo che i vantaggi di Linq significheranno (almeno) le migliori prestazioni nella media e nel caso migliore, quindi penso che andrò con esso. Nonostante la mancanza di chiarezza. Grazie mille @Matthew Horsley
Ian Grainger,

1
Questo è molto buono, ma c'è un bug per alcuni numeri decimali, il mio esempio è stato l'ordinamento di k8.11 contro k8.2. Per risolvere questo problema ho implementato il seguente regex: \ d + ([\.,] \ D)?
devzero

2
È inoltre necessario tenere conto della lunghezza del secondo gruppo (punto decimale + decimali) quando si inserisce questo codice m.Value.PadLeft (max, '0')
devzero

3
Penso che puoi usare .DefaultIfEmpty().Max()invece di lanciare int?. Vale anche la pena di fare un source.ToList()per evitare di enumerare nuovamente l'enumerabile.
Giovedì

30

Nessuna delle implementazioni esistenti sembrava eccezionale, quindi ho scritto la mia. I risultati sono quasi identici all'ordinamento utilizzato dalle versioni moderne di Windows Explorer (Windows 7/8). Le uniche differenze che ho visto sono 1) anche se Windows era solito (ad esempio XP) gestire numeri di qualsiasi lunghezza, ora è limitato a 19 cifre - il mio è illimitato, 2) Windows fornisce risultati incoerenti con alcuni set di cifre Unicode - il mio funziona bene (anche se non confronta numericamente le cifre da coppie surrogate; né Windows) e 3) il mio non può distinguere diversi tipi di pesi di ordinamento non primari se si verificano in sezioni diverse (ad es. "e-1é" vs " é1e- "- le sezioni prima e dopo il numero presentano differenze diacritiche e di punteggiatura).

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

La firma corrisponde al Comparison<string>delegato:

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

Ecco una classe wrapper da utilizzare come 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);
    }
}

Esempio:

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

Ecco un buon set di nomi di file che utilizzo per i test:

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

Le sezioni delle cifre devono essere confrontate per sezione, vale a dire "abc12b" dovrebbe essere inferiore a "abc123".
SOUser,

Potresti provare i seguenti dati: 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 Grazie per l'ottimo test. Se consenti a Esplora risorse di ordinare tali file, otterrai risultati diversi a seconda della versione di Windows che stai utilizzando. Il mio codice ordina questi nomi in modo identico a Server 2003 (e presumibilmente XP), ma diverso da Windows 8. Se avrò la possibilità, proverò a capire come lo sta facendo Windows 8 e ad aggiornare il mio codice.
JD

3
Ha un bug. Indice fuori
portata

3
Ottima soluzione! Quando l'ho confrontato in uno scenario normale con circa 10.000 file, è stato più veloce dell'esempio regex di Matthew e circa le stesse prestazioni di StrCmpLogicalW (). C'è un bug minore nel codice sopra: il "while (strA [jA] == zeroA) jA ++;" e "while (strB [jB] == zeroB) jB ++;" dovrebbe essere "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" e "while (jB <strB.Length && strB [jB] == zeroB) jB ++;". Altrimenti, le stringhe contenenti solo zeri genereranno un'eccezione.
Kuroki,

22

Soluzione Pure C # per linq orderby:

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

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

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

    #region IComparer<string> Members

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

    #endregion

    #region IComparer<string> Members

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

        string[] x1, y1;

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

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

        int returnVal;

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

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

        return isAscending ? returnVal : -returnVal;
    }

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

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

        return x.CompareTo(y);
    }

    #endregion

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

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

2
Tale codice è in definitiva da codeproject.com/KB/recipes/NaturalComparer.aspx (che non è orientato a LINQ).
mhenry1384,

2
Il post sul blog attribuisce a Justin Jones ( codeproject.com/KB/string/NaturalSortComparer.aspx ) l'IComparer, non Pascal Ganaye.
James McCormack,

1
Nota minore, questa soluzione ignora gli spazi che non coincidono con quelli di Windows e non è buono come il codice di Matthew Horsley di seguito. Quindi potresti ottenere 'string01' 'string 01' 'string 02' 'string02' per esempio (che sembra brutto). Se si rimuove lo stripping degli spazi, ordina le stringhe all'indietro, ovvero "string01" precede "string 01", il che può essere o non essere accettabile.
Michael Parker

Funzionava con indirizzi, ad esempio "1 Smith Rd", "10 Smith Rd", "2 Smith Rd", ecc. - Ordinati naturalmente. Sì! Ben fatto!
Piotr Kula,

A proposito, ho notato (e anche i commenti su quella pagina collegata sembrano indicare) che l'argomento Type <T> è completamente inutile.
jv-dev,

18

La risposta di Matthews Horsleys è il metodo più veloce che non cambia comportamento a seconda della versione di Windows su cui è in esecuzione il programma. Tuttavia, può essere ancora più veloce creando una volta regex e usando RegexOptions.Compiled. Ho anche aggiunto l'opzione di inserire un comparatore di stringhe in modo da poter ignorare il caso, se necessario, e migliorare un po 'la leggibilità.

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

Utilizzare per

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

Questo richiede 450ms per ordinare 100.000 stringhe rispetto a 300ms per il confronto predefinito delle stringhe .net - abbastanza velocemente!


2
Vale la pena leggere quanto sopra - Compilazione e riutilizzo in espressioni regolari
mungflesh

16

La mia soluzione:

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

risultati:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2

Mi piace. È facile da capire e non richiede Linq.

11

Devi stare attento - Ricordo vagamente di aver letto che StrCmpLogicalW, o qualcosa del genere, non era strettamente transitivo, e ho osservato che i metodi di ordinamento di .NET a volte rimangono bloccati in cicli infiniti se la funzione di confronto infrange quella regola.

Un confronto transitivo riporterà sempre che a <c se a <b e b <c. Esiste una funzione che esegue un confronto di ordinamento naturale che non soddisfa sempre tale criterio, ma non ricordo se si tratta di StrCmpLogicalW o qualcos'altro.


Hai qualche prova di questa affermazione? Dopo aver cercato su Google, non trovo alcuna indicazione che sia vero.
mhenry1384,

1
Ho sperimentato quegli infiniti loop con StrCmpLogicalW.
thd


L'elemento di feedback di Visual Studio 236900 non esiste più, ma eccone uno più aggiornato che conferma il problema: connect.microsoft.com/VisualStudio/feedback/details/774540/… Fornisce anche una soluzione: CultureInfoha una proprietà CompareInfoe l'oggetto che restituisce può fornirti SortKeyoggetti. Questi, a loro volta, possono essere confrontati e garantire la transitività.
Jonathan Gilbert,

9

Questo è il mio codice per ordinare una stringa con caratteri sia alfa che numerici.

Innanzitutto, questo metodo di estensione:

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

Quindi, basta usarlo ovunque nel tuo codice in questo modo:

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

Come funziona ? Sostituendo con zeri:

  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"

Funziona con numeri multipli:

 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"

Spero che sia di aiuto.


6

Aggiungendo alla risposta di Greg Beech (perché l'ho appena cercato), se vuoi usare questo da Linq puoi usare quello OrderByche richiede un IComparer. Per esempio:

var items = new List<MyItem>();

// fill items

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

2

Ecco un esempio relativamente semplice che non utilizza P / Invoke ed evita qualsiasi allocazione durante l'esecuzione.

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

Non ignora gli zeri iniziali, quindi 01viene dopo2 .

Test unitario corrispondente:

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

L'ho effettivamente implementato come metodo di estensione in StringComparermodo che tu possa fare ad esempio:

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

La risultante IComparer<string>può essere utilizzato in tutti i luoghi come OrderBy, OrderByDescending, ThenBy, ThenByDescending, SortedSet<string>, ecc e si può ancora facilmente Tweak maiuscole e minuscole, la cultura, ecc

L'implementazione è abbastanza banale e dovrebbe funzionare abbastanza bene anche su grandi sequenze.


L'ho anche pubblicato come un piccolo pacchetto NuGet , quindi puoi semplicemente fare:

Install-Package NaturalSort.Extension

Il codice che include i commenti sulla documentazione XML e la suite di test è disponibile nel repository GitHub NaturalSort.Extension .


L'intero codice è questo (se non è ancora possibile utilizzare C # 7, basta installare il pacchetto NuGet):

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

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

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

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

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

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

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

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

2

Ecco un ingenuo modo LINQ senza regex di una riga (preso in prestito da 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"]

Rimosso Dump () e assegnato a var e questo funziona come un incantesimo!
Arne S,

@ArneS: è stato scritto in LinQPad; e ho dimenticato di rimuovere il file Dump(). Grazie per averlo segnalato.
mshsayem,

1

Espandendo su un paio di risposte precedenti e facendo uso di metodi di estensione, ho trovato quanto segue che non ha le avvertenze di potenziale enumerazione multipla enumerabile o problemi di prestazioni relativi all'uso di più oggetti regex o alla chiamata di regex inutilmente, che detto questo, usa ToList (), che può negare i vantaggi in raccolte più grandi.

Il selettore supporta la digitazione generica per consentire l'assegnazione di qualsiasi delegato, gli elementi nella raccolta di origine vengono mutati dal selettore, quindi convertiti in stringhe con ToString ().

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

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

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

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

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

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

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

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

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

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

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

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

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

1

Ispirato alla soluzione di Michael Parker, ecco IComparerun'implementazione che puoi inserire in uno qualsiasi dei metodi di ordinamento 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);
    }
}

0

Avevamo bisogno di un ordinamento naturale per gestire il testo con il seguente schema:

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

Per qualche ragione quando ho guardato per la prima volta SO, non ho trovato questo post e implementato il nostro. Rispetto ad alcune delle soluzioni presentate qui, sebbene di concezione simile, potrebbe avere il vantaggio di essere forse più semplice e più comprensibile. Tuttavia, mentre ho provato a esaminare i colli di bottiglia delle prestazioni, è ancora un'implementazione molto più lenta rispetto all'impostazione predefinita OrderBy().

Ecco il metodo di estensione che implemento:

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

L'idea è di dividere le stringhe originali in blocchi di cifre e non cifre ("\d+|\D+" ). Poiché si tratta di un'attività potenzialmente costosa, viene eseguita una sola volta per voce. Quindi utilizziamo un comparatore di oggetti comparabili (scusate, non riesco a trovare un modo più corretto per dirlo). Confronta ogni blocco con il blocco corrispondente nell'altra stringa.

Vorrei un feedback su come questo potrebbe essere migliorato e quali sono i principali difetti. Si noti che la manutenibilità è importante per noi a questo punto e non lo stiamo attualmente utilizzando in set di dati estremamente grandi.


1
Ciò si arresta in modo anomalo quando tenta di confrontare stringhe strutturalmente diverse, ad esempio il confronto tra "a-1" e "a-2" funziona correttamente, ma il confronto tra "a" e "1" non lo è, perché "a" .CompareTo (1) genera un'eccezione.
jimrandomh,

@jimrandomh, hai ragione. Questo approccio era specifico per i nostri modelli.
Eric Liprandi,

0

Una versione più facile da leggere / mantenere.

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

Lasciami spiegare il mio problema e come sono stato in grado di risolverlo.

Problema: - Ordinare i file in base a FileName dagli oggetti FileInfo che vengono recuperati da una directory.

Soluzione: - Ho selezionato i nomi dei file da FileInfo e ho eliminato la parte ".png" del nome del file. Ora, basta fare List.Sort (), che ordina i nomi dei file in ordine di ordinamento naturale. Sulla base dei miei test ho scoperto che avere .png incasina l'ordinamento. Dai un'occhiata al codice qui sotto

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

Posso sapere il motivo di -1 su questa risposta?
girishkatta9,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.