Ordre de tri naturel en C #


129

Quelqu'un a-t-il une bonne ressource ou fournit un échantillon d'un tri par ordre naturel en C # pour un FileInfotableau? J'implémente l' IComparerinterface dans mon genre.

Réponses:


148

La chose la plus simple à faire est de simplement P / Invoquer la fonction intégrée de Windows et de l'utiliser comme fonction de comparaison dans votre IComparer:

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

Michael Kaplan a quelques exemples du fonctionnement de cette fonction ici , et les changements qui ont été apportés à Vista pour le rendre plus intuitif. Le côté positif de cette fonction est qu'elle aura le même comportement que la version de Windows sur laquelle elle s'exécute, mais cela signifie qu'elle diffère entre les versions de Windows, vous devez donc déterminer si cela vous pose un problème.

Une implémentation complète serait donc quelque chose comme:

[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
Très bonne réponse. Attention: cela ne fonctionnera pas avec Win2000, pour les quelques personnes qui exécutent encore des choses sur ce système d'exploitation. D'un autre côté, il y a suffisamment d'indices entre le blog de Kaplan et la documentation MSDN pour créer une fonction similaire.
Chris Charabaruk

9
Ce n'est pas portable, fonctionne uniquement sous Win32, mais ne fonctionne pas sous Linux / MacOS / Silverlight / Windows Phone / Metro
linquize

20
@linquize - Il a dit .NET pas Mono, donc Linux / OSX n'est pas vraiment un problème. Windows Phone / Metro n'existait pas en 2008 lorsque cette réponse a été publiée. Et à quelle fréquence effectuez-vous des opérations sur les fichiers dans Silverlight? Donc, pour l'OP, et probablement la plupart des autres personnes, c'était une réponse appropriée. Dans tous les cas, vous êtes libre de fournir une meilleure réponse; c'est ainsi que fonctionne ce site.
Greg Beech

6
Cela ne signifie pas que la réponse initiale était fausse. Je viens d'ajouter des informations supplémentaires avec des informations à jour
linquize le

2
Pour info, si vous héritez de Comparer<T>au lieu de l'implémenter IComparer<T>, vous obtenez une implémentation IComparerintégrée de l'interface (non générique) qui appelle votre méthode générique, à utiliser dans les API qui l'utilisent à la place. C'est fondamentalement gratuit de faire aussi: supprimez simplement le "I" et changez public int Compare(...)en public override int Compare(...). Idem pour IEqualityComparer<T>et EqualityComparer<T>.
Joe Amenta du

75

Je pensais juste ajouter à cela (avec la solution la plus concise que je puisse trouver):

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

Ce qui précède remplit tous les nombres de la chaîne à la longueur maximale de tous les nombres de toutes les chaînes et utilise la chaîne résultante pour trier.

Le transtypage en ( int?) est de permettre des collections de chaînes sans aucun nombre ( .Max()sur un énumérable vide lance un InvalidOperationException).


1
+1 Non seulement c'est le plus concis, c'est le plus rapide que j'ai vu. sauf pour la réponse acceptée mais je ne peux pas utiliser celle-là à cause des dépendances de la machine. Il a trié plus de 4 millions de valeurs en environ 35 secondes.
Gene S

4
C'est à la fois beau et impossible à lire. Je suppose que les avantages de Linq signifieront (au moins) les meilleures performances moyennes et les meilleures performances, donc je pense que je vais aller avec. Malgré le manque de clarté. Merci beaucoup @Matthew Horsley
Ian Grainger

1
C'est très bien, mais il y a un bogue pour certains nombres décimaux, mon exemple était de trier k8.11 vs k8.2. Pour résoudre ce problème, j'ai implémenté le regex suivant: \ d + ([\.,] \ D)?
devzero

2
Vous devez également prendre en compte la longueur du deuxième groupe (point décimal + décimales) lorsque vous complétez ce code m.Value.PadLeft (max, '0')
devzero

3
Je pense que vous pouvez utiliser .DefaultIfEmpty().Max()au lieu de lancer int?. Il vaut également la peine de faire un source.ToList()pour éviter de réénumérer l'énumérable.
Teejay

30

Aucune des implémentations existantes n'avait l'air super, j'ai donc écrit la mienne. Les résultats sont presque identiques au tri utilisé par les versions modernes de l'Explorateur Windows (Windows 7/8). Les seules différences que j'ai vues sont 1) bien que Windows utilisait (par exemple XP) des nombres de n'importe quelle longueur, il est maintenant limité à 19 chiffres - le mien est illimité, 2) Windows donne des résultats incohérents avec certains ensembles de chiffres Unicode - le mien fonctionne très bien (bien qu'il ne compare pas numériquement les chiffres des paires de substitution; pas plus que Windows), et 3) le mien ne peut pas distinguer différents types de poids de tri non primaires s'ils se produisent dans différentes sections (par exemple "e-1é" vs " é1e- "- les sections avant et après le nombre présentent des différences de poids diacritiques et de ponctuation).

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 signature correspond au Comparison<string>délégué:

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

Voici une classe wrapper à utiliser comme 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);
    }
}

Exemple:

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

Voici un bon ensemble de noms de fichiers que j'utilise pour les tests:

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

Les sections de chiffres doivent être comparées par section, c'est-à-dire que «abc12b» doit être inférieur à «abc123».
SOUser

Vous pouvez essayer les données suivantes: chaîne publique [] 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 Merci pour le bon cas de test. Si vous laissez l'Explorateur Windows trier ces fichiers, vous obtiendrez des résultats différents selon la version de Windows que vous utilisez. Mon code trie ces noms de la même manière que Server 2003 (et probablement XP), mais différent de Windows 8. Si j'en ai l'occasion, j'essaierai de comprendre comment Windows 8 le fait et je mettrai à jour mon code.
JD

3
A un bug. Index Out Of Range
linquize

3
Excellente solution! Quand je l'ai comparé dans un scénario normal avec environ 10 000 fichiers, c'était plus rapide que l'exemple de regex de Matthew, et à peu près les mêmes performances que StrCmpLogicalW (). Il y a un bug mineur dans le code ci-dessus: le "while (strA [jA] == zeroA) jA ++;" et "while (strB [jB] == zeroB) jB ++;" devrait être "while (jA <strA.Length && strA [jA] == zeroA) jA ++;" et "while (jB <strB.Length && strB [jB] == zeroB) jB ++;". Sinon, les chaînes contenant uniquement des zéros lèveront une exception.
kuroki

22

Solution pure C # pour 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
Ce code provient finalement de codeproject.com/KB/recipes/NaturalComparer.aspx (qui n'est pas orienté LINQ).
mhenry1384

2
Le billet de blog attribue à Justin Jones ( codeproject.com/KB/string/NaturalSortComparer.aspx ) l'IComparer, pas Pascal Ganaye.
James McCormack

1
Remarque mineure, cette solution ignore les espaces, ce qui n'est pas la même chose que Windows, et n'est pas aussi bonne que le code de Matthew Horsley ci-dessous. Ainsi, vous pourriez obtenir 'string01' 'string 01' 'string 02' 'string02' par exemple (qui a l'air moche). Si vous supprimez le dépouillement des espaces, il ordonne les chaînes à l'envers, c'est-à-dire que «string01» vient avant «string 01», ce qui peut être acceptable ou non.
Michael Parker

Cela a fonctionné pour les adresses, c'est-à-dire "1 Smith Rd", "10 Smith Rd", "2 Smith Rd", etc. - Trié naturellement. Oui! Joli!
Piotr Kula

En passant, j'ai remarqué (et les commentaires sur cette page liée semblent également indiquer) que l'argument Type <T> est complètement inutile.
jv-dev

18

La réponse de Matthews Horsleys est la méthode la plus rapide qui ne change pas de comportement en fonction de la version de Windows sur laquelle votre programme est exécuté. Cependant, cela peut être encore plus rapide en créant une fois l'expression régulière et en utilisant RegexOptions.Compiled. J'ai également ajouté la possibilité d'insérer un comparateur de chaînes afin que vous puissiez ignorer la casse si nécessaire, et amélioré un peu la lisibilité.

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

Utiliser par

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

Cela prend 450 ms pour trier 100 000 chaînes contre 300 ms pour la comparaison de chaînes .net par défaut - assez rapide!


2
Cela vaut la peine de lire ce qui précède - Compilation et réutilisation dans des expressions régulières
mungflesh

16

Ma solution:

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

Résultats:

1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2

Je l'aime. C'est facile à comprendre et ne nécessite pas Linq.

11

Vous devez être prudent - je me souviens vaguement avoir lu que StrCmpLogicalW, ou quelque chose comme ça, n'était pas strictement transitif, et j'ai observé que les méthodes de tri de .NET se coinçaient parfois dans des boucles infinies si la fonction de comparaison enfreignait cette règle.

Une comparaison transitive rapportera toujours que a <c si a <b et b <c. Il existe une fonction qui effectue une comparaison d'ordre de tri naturel qui ne répond pas toujours à ce critère, mais je ne me souviens pas si c'est StrCmpLogicalW ou autre.


Avez-vous une preuve de cette déclaration? Après avoir cherché sur Google, je ne trouve aucune indication que c'est vrai.
mhenry1384

1
J'ai expérimenté ces boucles infinies avec StrCmpLogicalW.
THd


L'élément de rétroaction Visual Studio 236900 n'existe plus, mais en voici un plus à jour qui confirme le problème: connect.microsoft.com/VisualStudio/feedback/details/774540/ ... Il donne également une solution : CultureInfoa une propriété CompareInfo, et l'objet qu'il renvoie peut vous fournir des SortKeyobjets. Ceux-ci, à leur tour, peuvent être comparés et garantir la transitivité.
Jonathan Gilbert

9

Ceci est mon code pour trier une chaîne comportant à la fois des caractères alphanumériques.

Tout d'abord, cette méthode d'extension:

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

Ensuite, utilisez-le simplement n'importe où dans votre code comme ceci:

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

Comment ça fonctionne ? En remplaçant par des zéros:

  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"

Fonctionne avec des nombres multiples:

 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"

J'espère que cela aidera.


6

En plus de la réponse de Greg Beech (parce que je viens de chercher cela), si vous voulez utiliser ceci de Linq, vous pouvez utiliser le OrderByqui prend un IComparer. Par exemple:

var items = new List<MyItem>();

// fill items

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

2

Voici un exemple relativement simple qui n'utilise pas P / Invoke et évite toute allocation pendant l'exécution.

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

Il n'ignore pas les zéros non significatifs, donc 01vient après 2.

Test unitaire correspondant:

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

Je l'ai en fait implémenté comme méthode d'extension sur le StringComparerafin que vous puissiez faire par exemple:

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

Résultant IComparer<string>peut être utilisé dans tous les endroits comme OrderBy, OrderByDescending, ThenBy, ThenByDescending,SortedSet<string> , etc. Et vous pouvez toujours la casse facilement tweak, la culture, etc.

L'implémentation est assez triviale et devrait fonctionner assez bien même sur de grandes séquences.


Je l'ai également publié sous forme de petit package NuGet , vous pouvez donc simplement le faire:

Install-Package NaturalSort.Extension

Le code comprenant les commentaires de documentation XML et la suite de tests est disponible dans le référentiel GitHub NaturalSort.Extension .


Le code entier est le suivant (si vous ne pouvez pas encore utiliser C # 7, installez simplement le package 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

Voici une méthode LINQ naïve d'une ligne sans regex (empruntée à 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"]

Supprimé Dump () et assigné à var et cela fonctionne comme un charme!
Arne S

@ArneS: Il a été écrit dans LinQPad; et j'ai oublié de supprimer le fichier Dump(). Merci de l'avoir signalé.
mshsayem

1

En développant quelques-unes des réponses précédentes et en utilisant des méthodes d'extension, j'ai proposé ce qui suit qui ne contient pas les mises en garde concernant une énumération potentielle multiple énumérable, ou des problèmes de performances liés à l'utilisation de plusieurs objets regex, ou à l'appel de regex inutilement, qui étant dit, il utilise ToList (), ce qui peut annuler les avantages dans les grandes collections.

Le sélecteur prend en charge le typage générique pour permettre à n'importe quel délégué d'être affecté, les éléments de la collection source sont mutés par le sélecteur, puis convertis en chaînes avec 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

Inspiré de la solution de Michael Parker, voici une IComparerimplémentation que vous pouvez ajouter à l'une des méthodes de commande 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

Nous avions besoin d'un tri naturel pour traiter le texte avec le modèle suivant:

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

Pour une raison quelconque, lorsque j'ai regardé SO pour la première fois, je n'ai pas trouvé cet article et mis en œuvre le nôtre. Comparée à certaines des solutions présentées ici, bien que de concept similaire, elle pourrait avoir l'avantage d'être peut-être plus simple et plus facile à comprendre. Cependant, même si j'ai essayé d'examiner les goulots d'étranglement des performances, c'est toujours une mise en œuvre beaucoup plus lente que la valeur par défaut OrderBy().

Voici la méthode d'extension que j'implémente:

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'idée est de diviser les chaînes d'origine en blocs de chiffres et de non-chiffres ("\d+|\D+" ). Comme il s'agit d'une tâche potentiellement coûteuse, elle n'est effectuée qu'une seule fois par entrée. Nous utilisons ensuite un comparateur d'objets comparables (désolé, je ne trouve pas de moyen plus approprié de le dire). Il compare chaque bloc à son bloc correspondant dans l'autre chaîne.

J'aimerais avoir des commentaires sur la façon dont cela pourrait être amélioré et quelles sont les principales lacunes. Notez que la maintenabilité est importante pour nous à ce stade et que nous ne l'utilisons pas actuellement dans des ensembles de données extrêmement volumineux.


1
Cela se bloque quand il essaie de comparer des chaînes qui sont structurellement différentes - par exemple, comparer "a-1" à "a-2" fonctionne bien, mais comparer "a" à "1" ne l'est pas, car "a" .CompareTo (1) lève une exception.
jimrandomh

@jimrandomh, vous avez raison. Cette approche était spécifique à nos modèles.
Eric Liprandi

0

Une version plus facile à lire / maintenir.

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

Laissez-moi vous expliquer mon problème et comment j'ai pu le résoudre.

Problème: - Trier les fichiers en fonction de FileName à partir des objets FileInfo qui sont extraits d'un répertoire.

Solution: - J'ai sélectionné les noms de fichier de FileInfo et découpé la partie ".png" du nom de fichier. Maintenant, faites juste List.Sort (), qui trie les noms de fichiers dans l'ordre de tri naturel. Sur la base de mes tests, j'ai constaté que le fait d'avoir .png gâche l'ordre de tri. Jetez un œil au code ci-dessous

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

Puis-je connaître la raison de -1 sur cette réponse?
girishkatta9
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.