148 votes

Ordre de tri naturel en C#

Quelqu'un a-t-il une bonne ressource ou peut-il fournir un exemple de tri par ordre naturel en C# pour un FileInfo réseau ? J'implémente le IComparer dans mes sortes.

164voto

Greg Beech Points 55270

La chose la plus simple à faire est d'invoquer la fonction intégrée dans Windows et de l'utiliser comme fonction de comparaison dans l'application IComparer :

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

Michael Kaplan a quelques des exemples du fonctionnement de cette fonction ici et les modifications apportées à Vista pour rendre son fonctionnement plus intuitif. L'avantage 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 d'une version de Windows à l'autre, et vous devez donc déterminer si cela constitue un problème pour vous.

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

9 votes

Excellente réponse. Avertissement : cela ne fonctionnera pas avec Win2000, pour les quelques personnes qui utilisent encore 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.

13 votes

Ceci n'est pas portable, ne fonctionne que sous Win32, mais ne fonctionne pas sous Linux / MacOS / Silverlight / Windows Phone / Metro

24 votes

@linquize - Il a dit .NET et non 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é postée. Et combien de fois faites-vous des opérations sur des fichiers dans Silverlight ? Donc pour le PO, et probablement pour la plupart des autres personnes, c'était une réponse appropriée. Quoi qu'il en soit, vous êtes libre de fournir une meilleure réponse ; c'est ainsi que fonctionne ce site.

87voto

Matthew Horsley Points 91

J'ai juste pensé que je devais ajouter quelque chose à cela (avec la solution la plus concise que j'ai pu 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')));
}

L'opération ci-dessus écrase tous les chiffres de la chaîne jusqu'à la longueur maximale de tous les chiffres de toutes les chaînes et utilise la chaîne résultante pour trier.

Le casting pour ( int? ) est d'autoriser les collections de chaînes de caractères sans aucun chiffre ( .Max() sur un énumérateur vide lance un InvalidOperationException ).

1 votes

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

4 votes

C'est à la fois beau et impossible à lire. Je suppose que les avantages de Linq se traduiront (au moins) par les meilleures performances moyennes et dans le meilleur des cas, donc je pense que je vais faire avec. Malgré le manque de clarté. Merci beaucoup @Matthew Horsley

4 votes

C'est très bien, mais il y a un bug pour certains nombres décimaux, mon exemple était le tri de k8.11 contre k8.2. Pour corriger cela, j'ai implémenté la regex suivante : \d +([ \. ,] \d ) ?

38voto

J.D. Points 1026

Comme aucune des implémentations existantes n'avait l'air bien, j'ai é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 ait eu l'habitude (par exemple XP) de gérer 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 bien (bien qu'il ne compare pas numériquement les chiffres des paires de substitution ; Windows non plus), et 3) le mien ne peut pas distinguer différents types de poids de tri non primaires s'ils apparaissent dans différentes sections (par ex. par exemple, "e-1é" vs "é1e-" - les sections avant et après le chiffre présentent des différences de poids de 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 à la Comparison<string> délégué :

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

Voici une classe enveloppante à utiliser en tant que 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();

1 votes

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

0 votes

Vous pourriez essayer les données suivantes : public string[] filenames = { "-abc12.txt", " abc12.txt", "1abc_2.txt", "a0000012.txt", "a0000012c.txt", "a000012.txt", "a000012b.txt", "a0000012.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", "e0000120000012", "e0000012000012"} ;

0 votes

@XichenLi Merci pour ce bon cas d'essai. 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 manière identique à Server 2003 (et vraisemblablement XP), mais différente de Windows 8. Si j'en ai l'occasion, j'essaierai de comprendre comment Windows 8 s'y prend et je mettrai mon code à jour.

24voto

James McCormack Points 4828

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

3 votes

Ce code provient finalement de codeproject.com/KB/recipes/NaturalComparer.aspx (qui n'est pas orienté LINQ).

3 votes

L'article de blog attribue le mérite à Justin Jones ( codeproject.com/KB/string/NaturalSortComparer.aspx ) pour l'IComparer, et non Pascal Ganaye.

1 votes

Note mineure, cette solution ignore les espaces, ce qui n'est pas la même chose que ce que fait Windows, et n'est pas aussi bonne que le code de Matthew Horsley ci-dessous. Vous pouvez donc obtenir 'string01' 'string 01' 'string 02' 'string02' par exemple (ce qui est très laid). Si vous supprimez la suppression des espaces, les chaînes sont classées à l'envers, c'est-à-dire que 'string01' vient avant 'string 01', ce qui peut être acceptable ou non.

13voto

Jonathan Gilbert Points 497

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 coincent parfois dans des boucles infinies si la fonction de comparaison enfreint cette règle.

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

0 votes

Avez-vous des preuves de cette affirmation ? Après avoir fait des recherches sur Internet, je n'ai trouvé aucune indication de sa véracité.

3 votes

J'ai connu ces boucles infinies avec StrCmpLogicalW.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X